package socket import ( "context" "errors" "testing" "time" 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/packages/domain/market" "git.toki-labs.com/toki/alt/services/worker/internal/papertrading" protoSocket "git.toki-labs.com/toki/proto-socket/go" ) type fakePaperService struct { gotStart papertrading.StartRequest gotAccount backtest.PaperAccountID state papertrading.State startErr error stateErr error // Order lifecycle captures and canned results. gotSubmit papertrading.SubmitOrderRequest gotCancelOrder string gotFillOrder string gotFillPrice market.Price order papertrading.PaperOrder submitErr error cancelErr error fillErr error } func (f *fakePaperService) StartPaperTrading(ctx context.Context, req papertrading.StartRequest) (papertrading.State, error) { f.gotStart = req if f.startErr != nil { return papertrading.State{}, f.startErr } return f.state, nil } func (f *fakePaperService) GetPaperTradingState(ctx context.Context, accountID backtest.PaperAccountID) (papertrading.State, error) { f.gotAccount = accountID if f.stateErr != nil { return papertrading.State{}, f.stateErr } return f.state, nil } func (f *fakePaperService) SubmitPaperOrder(ctx context.Context, req papertrading.SubmitOrderRequest) (papertrading.PaperOrder, error) { f.gotSubmit = req if f.submitErr != nil { return papertrading.PaperOrder{}, f.submitErr } return f.order, nil } func (f *fakePaperService) CancelPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string) (papertrading.PaperOrder, error) { f.gotAccount = accountID f.gotCancelOrder = orderID if f.cancelErr != nil { return papertrading.PaperOrder{}, f.cancelErr } return f.order, nil } func (f *fakePaperService) FillPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string, fillPrice market.Price) (papertrading.PaperOrder, papertrading.State, error) { f.gotAccount = accountID f.gotFillOrder = orderID f.gotFillPrice = fillPrice if f.fillErr != nil { return papertrading.PaperOrder{}, papertrading.State{}, f.fillErr } return f.order, f.state, nil } func sampleOrder() papertrading.PaperOrder { return papertrading.PaperOrder{ OrderID: "paper-order-paper-1-1", AccountID: "paper-1", Intent: backtest.OrderIntent{ InstrumentID: "KRX:005930", Side: backtest.OrderSideBuy, Quantity: market.Quantity{Amount: market.Decimal{Value: "1"}}, Type: backtest.OrderTypeMarket, }, Status: papertrading.PaperOrderStatusPending, } } func krw(v string) market.Price { return market.Price{Currency: market.CurrencyKRW, Amount: market.Decimal{Value: v}} } func validStartPaperRequest() *altv1.StartPaperTradingRequest { return &altv1.StartPaperTradingRequest{ AccountId: "paper-1", Spec: &altv1.BacktestRunSpec{ StrategyId: "strategy-v1", Market: altv1.Market_MARKET_KR, Timeframe: altv1.Timeframe_TIMEFRAME_DAILY, FromUnixMs: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC).UnixMilli(), ToUnixMs: time.Date(2026, 5, 15, 0, 0, 0, 0, time.UTC).UnixMilli(), }, StartingCash: &altv1.Price{ Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "10000000"}, }, } } func sampleState() papertrading.State { return papertrading.State{ Run: backtest.Run{ ID: "paper-run-1", Status: backtest.RunStatusSucceeded, Spec: backtest.RunSpec{StrategyID: "strategy-v1", Market: market.MarketKR, Timeframe: market.TimeframeDaily}, }, Cash: krw("9998950"), Positions: []backtest.Position{ {InstrumentID: "KRX:005930", Quantity: market.Quantity{Amount: market.Decimal{Value: "1"}}, LastPrice: krw("1150")}, }, Fills: []backtest.Fill{ {InstrumentID: "KRX:005930", Side: backtest.OrderSideBuy, Quantity: market.Quantity{Amount: market.Decimal{Value: "1"}}, Price: krw("1050"), Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC)}, }, EquityCurve: []backtest.EquityPoint{ {Timestamp: time.Date(2026, 5, 2, 0, 0, 0, 0, time.UTC), Equity: krw("10000100")}, }, Rejected: []papertrading.RejectedOrder{ { Order: backtest.OrderIntent{ InstrumentID: "KRX:000660", Side: backtest.OrderSideSell, Quantity: market.Quantity{Amount: market.Decimal{Value: "2"}}, }, Reason: "insufficient position quantity for sell order", BarTime: time.Date(2026, 5, 3, 0, 0, 0, 0, time.UTC), Instrument: "KRX:000660", }, }, } } func TestHandleStartPaperTradingSuccess(t *testing.T) { paper := &fakePaperService{state: sampleState()} deps := Deps{Paper: paper} resp, err := handleStartPaperTrading(deps, validStartPaperRequest()) if err != nil { t.Fatalf("unexpected error: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } st := resp.GetState() if st.GetAccountId() != "paper-1" { t.Errorf("account id mismatch: %q", st.GetAccountId()) } if st.GetRun().GetStatus() != altv1.BacktestRunStatus_BACKTEST_RUN_STATUS_SUCCEEDED { t.Errorf("expected succeeded run status, got %v", st.GetRun().GetStatus()) } if st.GetCash().GetAmount().GetValue() != "9998950" { t.Errorf("cash mismatch: %q", st.GetCash().GetAmount().GetValue()) } if len(st.GetPositions()) != 1 || st.GetPositions()[0].GetInstrumentId() != "KRX:005930" { t.Errorf("positions mismatch: %+v", st.GetPositions()) } if len(st.GetFills()) != 1 || st.GetFills()[0].GetPrice().GetAmount().GetValue() != "1050" { t.Errorf("fills mismatch: %+v", st.GetFills()) } if len(st.GetEquityCurve()) != 1 { t.Errorf("equity curve mismatch: %+v", st.GetEquityCurve()) } if len(st.GetRiskRejections()) != 1 { t.Fatalf("risk rejections mismatch: %+v", st.GetRiskRejections()) } rej := st.GetRiskRejections()[0] if rej.GetInstrumentId() != "KRX:000660" || rej.GetSide() != "sell" { t.Errorf("rejection instrument/side mismatch: %+v", rej) } if rej.GetReason() != "insufficient position quantity for sell order" { t.Errorf("rejection reason mismatch: %q", rej.GetReason()) } if rej.GetQuantity().GetAmount().GetValue() != "2" { t.Errorf("rejection quantity mismatch: %q", rej.GetQuantity().GetAmount().GetValue()) } if paper.gotStart.AccountID != "paper-1" || paper.gotStart.StartingCash.Amount.Value != "10000000" { t.Errorf("service received wrong start request: %+v", paper.gotStart) } } func TestHandleStartPaperTradingInvalidRequest(t *testing.T) { deps := Deps{Paper: &fakePaperService{}} // missing account_id resp, err := handleStartPaperTrading(deps, &altv1.StartPaperTradingRequest{ Spec: validStartPaperRequest().GetSpec(), StartingCash: validStartPaperRequest().GetStartingCash(), }) if err != nil { t.Fatalf("expected typed validation response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) // missing starting cash resp, err = handleStartPaperTrading(deps, &altv1.StartPaperTradingRequest{ AccountId: "paper-1", Spec: validStartPaperRequest().GetSpec(), }) if err != nil { t.Fatalf("expected typed validation response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } func TestHandleStartPaperTradingUnavailable(t *testing.T) { resp, err := handleStartPaperTrading(Deps{}, validStartPaperRequest()) if err != nil { t.Fatalf("expected typed unavailable response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorUnavailable) } func TestHandleStartPaperTradingServiceError(t *testing.T) { deps := Deps{Paper: &fakePaperService{startErr: errors.New("boom")}} resp, err := handleStartPaperTrading(deps, validStartPaperRequest()) if err != nil { t.Fatalf("expected typed internal response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInternal) } func TestHandleGetPaperTradingStateSuccess(t *testing.T) { paper := &fakePaperService{state: sampleState()} deps := Deps{Paper: paper} resp, err := handleGetPaperTradingState(deps, &altv1.GetPaperTradingStateRequest{AccountId: "paper-1"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } if resp.GetState().GetAccountId() != "paper-1" { t.Errorf("account id mismatch: %q", resp.GetState().GetAccountId()) } if paper.gotAccount != "paper-1" { t.Errorf("service received wrong account id: %q", paper.gotAccount) } } func TestHandleGetPaperTradingStateNotFound(t *testing.T) { deps := Deps{Paper: &fakePaperService{stateErr: papertrading.ErrAccountNotFound}} resp, err := handleGetPaperTradingState(deps, &altv1.GetPaperTradingStateRequest{AccountId: "missing"}) if err != nil { t.Fatalf("expected typed not_found response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorNotFound) } func TestHandleGetPaperTradingStateRequiresAccountID(t *testing.T) { deps := Deps{Paper: &fakePaperService{}} resp, err := handleGetPaperTradingState(deps, &altv1.GetPaperTradingStateRequest{}) if err != nil { t.Fatalf("expected typed validation response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } func TestHandleGetPaperTradingStateUnavailable(t *testing.T) { resp, err := handleGetPaperTradingState(Deps{}, &altv1.GetPaperTradingStateRequest{AccountId: "paper-1"}) if err != nil { t.Fatalf("expected typed unavailable response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorUnavailable) } func TestHandleSubmitPaperOrderSuccess(t *testing.T) { paper := &fakePaperService{order: sampleOrder()} deps := Deps{Paper: paper} resp, err := handleSubmitPaperOrder(deps, &altv1.SubmitPaperOrderRequest{ AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, Type: "market", }) if err != nil { t.Fatalf("unexpected error: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } if resp.GetOrder().GetOrderId() != "paper-order-paper-1-1" { t.Errorf("order id mismatch: %q", resp.GetOrder().GetOrderId()) } if resp.GetOrder().GetStatus() != "pending" { t.Errorf("expected pending status, got %q", resp.GetOrder().GetStatus()) } if paper.gotSubmit.Intent.InstrumentID != "KRX:005930" || paper.gotSubmit.Intent.Side != backtest.OrderSideBuy { t.Errorf("service received wrong intent: %+v", paper.gotSubmit.Intent) } } func TestHandleSubmitPaperOrderInvalidRequest(t *testing.T) { deps := Deps{Paper: &fakePaperService{order: sampleOrder()}} cases := []*altv1.SubmitPaperOrderRequest{ {InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}}, // missing account {AccountId: "paper-1", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}}, // missing instrument {AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "hold", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}}, // bad side {AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "buy"}, // missing quantity {AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, Type: "limit"}, // limit without price } for i, req := range cases { resp, err := handleSubmitPaperOrder(deps, req) if err != nil { t.Fatalf("case %d: expected typed validation response, got error: %v", i, err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } } func TestHandleSubmitPaperOrderInvalidDecimal(t *testing.T) { deps := Deps{Paper: &fakePaperService{order: sampleOrder()}} base := func() *altv1.SubmitPaperOrderRequest { return &altv1.SubmitPaperOrderRequest{ AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, Type: "market", } } // Non-numeric, zero, and negative quantities are rejected at the socket // boundary before reaching the service. for _, q := range []string{"0", "-1", "abc"} { req := base() req.Quantity = &altv1.Quantity{Amount: &altv1.Decimal{Value: q}} resp, err := handleSubmitPaperOrder(deps, req) if err != nil { t.Fatalf("quantity %q: unexpected error: %v", q, err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } // Malformed and negative limit prices are rejected too. for _, p := range []string{"-1", "abc"} { req := base() req.Type = "limit" req.LimitPrice = &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: p}} resp, err := handleSubmitPaperOrder(deps, req) if err != nil { t.Fatalf("limit price %q: unexpected error: %v", p, err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } } func TestHandleFillPaperOrderInvalidFillPrice(t *testing.T) { deps := Deps{Paper: &fakePaperService{order: sampleOrder(), state: sampleState()}} for _, p := range []string{"-1", "abc"} { resp, err := handleFillPaperOrder(deps, &altv1.FillPaperOrderRequest{ AccountId: "paper-1", OrderId: "paper-order-paper-1-1", FillPrice: &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: p}}, }) if err != nil { t.Fatalf("fill price %q: unexpected error: %v", p, err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } } func TestHandleSubmitPaperOrderUnavailable(t *testing.T) { resp, err := handleSubmitPaperOrder(Deps{}, &altv1.SubmitPaperOrderRequest{ AccountId: "paper-1", InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, }) if err != nil { t.Fatalf("expected typed unavailable response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorUnavailable) } func TestHandleSubmitPaperOrderAccountNotFound(t *testing.T) { deps := Deps{Paper: &fakePaperService{submitErr: papertrading.ErrAccountNotFound}} resp, err := handleSubmitPaperOrder(deps, &altv1.SubmitPaperOrderRequest{ AccountId: "missing", InstrumentId: "KRX:005930", Side: "buy", Quantity: &altv1.Quantity{Amount: &altv1.Decimal{Value: "1"}}, }) if err != nil { t.Fatalf("expected typed not_found response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorNotFound) } func TestHandleCancelPaperOrderSuccess(t *testing.T) { order := sampleOrder() order.Status = papertrading.PaperOrderStatusCanceled paper := &fakePaperService{order: order} deps := Deps{Paper: paper} resp, err := handleCancelPaperOrder(deps, &altv1.CancelPaperOrderRequest{AccountId: "paper-1", OrderId: "paper-order-paper-1-1"}) if err != nil { t.Fatalf("unexpected error: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } if resp.GetOrder().GetStatus() != "canceled" { t.Errorf("expected canceled status, got %q", resp.GetOrder().GetStatus()) } if paper.gotCancelOrder != "paper-order-paper-1-1" { t.Errorf("service received wrong order id: %q", paper.gotCancelOrder) } } func TestHandleCancelPaperOrderValidation(t *testing.T) { deps := Deps{Paper: &fakePaperService{order: sampleOrder()}} for _, req := range []*altv1.CancelPaperOrderRequest{ {OrderId: "o-1"}, // missing account {AccountId: "paper-1"}, // missing order id } { resp, err := handleCancelPaperOrder(deps, req) if err != nil { t.Fatalf("expected typed validation response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } } func TestHandleCancelPaperOrderNotPending(t *testing.T) { deps := Deps{Paper: &fakePaperService{cancelErr: papertrading.ErrOrderNotPending}} resp, err := handleCancelPaperOrder(deps, &altv1.CancelPaperOrderRequest{AccountId: "paper-1", OrderId: "o-1"}) if err != nil { t.Fatalf("expected typed invalid_request response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } func TestHandleCancelPaperOrderNotFound(t *testing.T) { deps := Deps{Paper: &fakePaperService{cancelErr: papertrading.ErrOrderNotFound}} resp, err := handleCancelPaperOrder(deps, &altv1.CancelPaperOrderRequest{AccountId: "paper-1", OrderId: "missing"}) if err != nil { t.Fatalf("expected typed not_found response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorNotFound) } func TestHandleFillPaperOrderSuccess(t *testing.T) { order := sampleOrder() order.Status = papertrading.PaperOrderStatusFilled order.Fill = &backtest.Fill{ InstrumentID: "KRX:005930", Side: backtest.OrderSideBuy, Quantity: market.Quantity{Amount: market.Decimal{Value: "1"}}, Price: krw("1000"), Timestamp: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), } paper := &fakePaperService{order: order, state: sampleState()} deps := Deps{Paper: paper} resp, err := handleFillPaperOrder(deps, &altv1.FillPaperOrderRequest{ AccountId: "paper-1", OrderId: "paper-order-paper-1-1", FillPrice: &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "1000"}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } if resp.GetOrder().GetStatus() != "filled" { t.Errorf("expected filled status, got %q", resp.GetOrder().GetStatus()) } if resp.GetOrder().GetFill().GetPrice().GetAmount().GetValue() != "1000" { t.Errorf("fill price mismatch: %q", resp.GetOrder().GetFill().GetPrice().GetAmount().GetValue()) } if resp.GetState().GetAccountId() != "paper-1" { t.Errorf("expected state snapshot in response, got %+v", resp.GetState()) } if paper.gotFillPrice.Amount.Value != "1000" { t.Errorf("service received wrong fill price: %+v", paper.gotFillPrice) } } func TestHandleFillPaperOrderRejectedIsTypedSuccess(t *testing.T) { order := sampleOrder() order.Status = papertrading.PaperOrderStatusRejected order.Reason = "insufficient position quantity for sell order" paper := &fakePaperService{order: order, state: sampleState()} deps := Deps{Paper: paper} resp, err := handleFillPaperOrder(deps, &altv1.FillPaperOrderRequest{ AccountId: "paper-1", OrderId: "paper-order-paper-1-1", FillPrice: &altv1.Price{Currency: altv1.Currency_CURRENCY_KRW, Amount: &altv1.Decimal{Value: "1000"}}, }) if err != nil { t.Fatalf("unexpected error: %v", err) } // A business rejection is a successful response carrying status=rejected. if resp.GetError() != nil { t.Fatalf("rejection must not surface as a typed error: %+v", resp.GetError()) } if resp.GetOrder().GetStatus() != "rejected" || resp.GetOrder().GetReason() == "" { t.Errorf("expected rejected status with reason, got %+v", resp.GetOrder()) } } func TestHandleFillPaperOrderFillPriceRequired(t *testing.T) { deps := Deps{Paper: &fakePaperService{fillErr: papertrading.ErrFillPriceRequired}} resp, err := handleFillPaperOrder(deps, &altv1.FillPaperOrderRequest{AccountId: "paper-1", OrderId: "o-1"}) if err != nil { t.Fatalf("expected typed invalid_request response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } func TestHandleFillPaperOrderValidation(t *testing.T) { deps := Deps{Paper: &fakePaperService{order: sampleOrder()}} for _, req := range []*altv1.FillPaperOrderRequest{ {OrderId: "o-1"}, // missing account {AccountId: "paper-1"}, // missing order id } { resp, err := handleFillPaperOrder(deps, req) if err != nil { t.Fatalf("expected typed validation response, got error: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorInvalidRequest) } } func TestPaperHandlersCoverAllRequests(t *testing.T) { registered := make(map[string]bool) for _, h := range paperHandlers(Deps{}) { if h.requestType == "" { t.Error("handler has empty request type") } if h.register == nil { t.Errorf("handler %q has nil register", h.requestType) } registered[h.requestType] = true } for _, req := range []string{ "alt.v1.StartPaperTradingRequest", "alt.v1.GetPaperTradingStateRequest", "alt.v1.SubmitPaperOrderRequest", "alt.v1.CancelPaperOrderRequest", "alt.v1.FillPaperOrderRequest", } { if !registered[req] { t.Errorf("missing handler for %q", req) } } } func TestWorkerPaperSocketUnavailableReturnsTypedError(t *testing.T) { client := startBacktestWorkerTestClient(t, Deps{}) resp, err := protoSocket.SendRequestTyped[*altv1.StartPaperTradingRequest, *altv1.StartPaperTradingResponse]( &client.Communicator, validStartPaperRequest(), 500*time.Millisecond, ) if err != nil { t.Fatalf("request should return typed error response without timeout: %v", err) } requireBacktestError(t, resp.GetError(), backtestErrorUnavailable) } func TestWorkerPaperSocketStartRoundTrip(t *testing.T) { client := startBacktestWorkerTestClient(t, Deps{Paper: &fakePaperService{state: sampleState()}}) resp, err := protoSocket.SendRequestTyped[*altv1.StartPaperTradingRequest, *altv1.StartPaperTradingResponse]( &client.Communicator, validStartPaperRequest(), time.Second, ) if err != nil { t.Fatalf("round trip failed: %v", err) } if resp.GetError() != nil { t.Fatalf("unexpected typed error: %+v", resp.GetError()) } if resp.GetState().GetAccountId() != "paper-1" { t.Errorf("account id mismatch: %q", resp.GetState().GetAccountId()) } }