Paper trading readiness에서 API/worker/CLI가 같은 protobuf 계약으로 paper state를 시작하고 조회할 수 있어야 한다. Headless 운영 경로를 먼저 닫기 위해 contract, worker runtime, API forwarding, CLI scenario, client parser map과 검증 artifact를 함께 반영한다.
108 lines
4.1 KiB
Go
108 lines
4.1 KiB
Go
package socket
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
|
|
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
|
|
|
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
|
"git.toki-labs.com/toki/alt/packages/domain/backtest"
|
|
"git.toki-labs.com/toki/alt/services/worker/internal/papertrading"
|
|
)
|
|
|
|
// PaperService is the worker-owned paper trading runtime the socket layer drives.
|
|
// The papertrading package provides the concrete process-local implementation;
|
|
// the narrow interface keeps the handlers testable with a fake.
|
|
type PaperService interface {
|
|
StartPaperTrading(ctx context.Context, req papertrading.StartRequest) (papertrading.State, error)
|
|
GetPaperTradingState(ctx context.Context, accountID backtest.PaperAccountID) (papertrading.State, error)
|
|
}
|
|
|
|
// paperHandlers returns the session handlers for the paper trading command and
|
|
// query surface, each closing over the shared dependencies.
|
|
func paperHandlers(deps Deps) []sessionHandler {
|
|
return []sessionHandler{
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.StartPaperTradingRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.StartPaperTradingRequest, *altv1.StartPaperTradingResponse](
|
|
&client.Communicator,
|
|
func(req *altv1.StartPaperTradingRequest) (*altv1.StartPaperTradingResponse, error) {
|
|
return handleStartPaperTrading(deps, req)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.GetPaperTradingStateRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.GetPaperTradingStateRequest, *altv1.GetPaperTradingStateResponse](
|
|
&client.Communicator,
|
|
func(req *altv1.GetPaperTradingStateRequest) (*altv1.GetPaperTradingStateResponse, error) {
|
|
return handleGetPaperTradingState(deps, req)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleStartPaperTrading(deps Deps, req *altv1.StartPaperTradingRequest) (*altv1.StartPaperTradingResponse, error) {
|
|
startReq, err := startPaperRequestFromProto(req)
|
|
if err != nil {
|
|
return &altv1.StartPaperTradingResponse{Error: invalidPaperRequest(err.Error())}, nil
|
|
}
|
|
if deps.Paper == nil {
|
|
return &altv1.StartPaperTradingResponse{Error: unavailableError("paper trading is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
state, err := deps.Paper.StartPaperTrading(ctx, startReq)
|
|
if err != nil {
|
|
return &altv1.StartPaperTradingResponse{Error: paperBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.StartPaperTradingResponse{State: paperStateToProto(startReq.AccountID, state)}, nil
|
|
}
|
|
|
|
func handleGetPaperTradingState(deps Deps, req *altv1.GetPaperTradingStateRequest) (*altv1.GetPaperTradingStateResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.GetPaperTradingStateResponse{Error: invalidPaperRequest("account_id is required")}, nil
|
|
}
|
|
if deps.Paper == nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: unavailableError("paper trading is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
accountID := backtest.PaperAccountID(req.GetAccountId())
|
|
state, err := deps.Paper.GetPaperTradingState(ctx, accountID)
|
|
if err != nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: paperBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.GetPaperTradingStateResponse{State: paperStateToProto(accountID, state)}, nil
|
|
}
|
|
|
|
func invalidPaperRequest(reason string) *altv1.ErrorInfo {
|
|
return errorInfo(backtestErrorInvalidRequest, "invalid paper trading request: "+reason)
|
|
}
|
|
|
|
// paperBackendErrorInfo maps paper runtime errors onto the shared typed error
|
|
// vocabulary: a missing account is not_found, a deadline is timeout, everything
|
|
// else is internal.
|
|
func paperBackendErrorInfo(err error) *altv1.ErrorInfo {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case errors.Is(err, papertrading.ErrAccountNotFound):
|
|
return errorInfo(backtestErrorNotFound, err.Error())
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return errorInfo(backtestErrorTimeout, err.Error())
|
|
default:
|
|
return errorInfo(backtestErrorInternal, err.Error())
|
|
}
|
|
}
|