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()) } }