package socket import ( "context" 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/trading" "git.toki-labs.com/toki/alt/services/worker/internal/livetrading" ) // LiveService is the narrow interface for live broker capability discovery, // the live order lifecycle, the risk/kill switch control surface, and account // snapshot sync. The livetrading package provides the concrete process-local // implementation; the narrow interface keeps the handlers testable with a fake. type LiveService interface { GetLiveBrokerCapabilities(ctx context.Context, accountID string) (trading.BrokerCapability, error) SubmitLiveOrder(ctx context.Context, req livetrading.SubmitRequest) (livetrading.LiveOrder, error) CancelLiveOrder(ctx context.Context, accountID, liveOrderID string) (livetrading.LiveOrder, error) GetLiveOrder(ctx context.Context, accountID, liveOrderID string) (livetrading.LiveOrder, error) GetRiskPolicy() trading.RiskPolicy GetKillSwitch() trading.KillSwitchState SetKillSwitch(ks trading.KillSwitchState) trading.KillSwitchState SyncLiveAccount(ctx context.Context, accountID string) (trading.AccountSnapshot, error) GetLiveAccountSnapshot(ctx context.Context, accountID string) (trading.AccountSnapshot, error) ListLiveAuditEvents(ctx context.Context, filter trading.AuditFilter) ([]trading.AuditEvent, error) } func liveHandlers(deps Deps) []sessionHandler { return []sessionHandler{ { requestType: protoSocket.TypeNameOf(&altv1.SyncLiveAccountRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.SyncLiveAccountRequest, *altv1.SyncLiveAccountResponse]( &client.Communicator, func(req *altv1.SyncLiveAccountRequest) (*altv1.SyncLiveAccountResponse, error) { return handleSyncLiveAccount(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.GetLiveAccountSnapshotRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.GetLiveAccountSnapshotRequest, *altv1.GetLiveAccountSnapshotResponse]( &client.Communicator, func(req *altv1.GetLiveAccountSnapshotRequest) (*altv1.GetLiveAccountSnapshotResponse, error) { return handleGetLiveAccountSnapshot(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.GetLiveBrokerCapabilitiesRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.GetLiveBrokerCapabilitiesRequest, *altv1.GetLiveBrokerCapabilitiesResponse]( &client.Communicator, func(req *altv1.GetLiveBrokerCapabilitiesRequest) (*altv1.GetLiveBrokerCapabilitiesResponse, error) { return handleGetLiveBrokerCapabilities(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.SubmitLiveOrderRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.SubmitLiveOrderRequest, *altv1.SubmitLiveOrderResponse]( &client.Communicator, func(req *altv1.SubmitLiveOrderRequest) (*altv1.SubmitLiveOrderResponse, error) { return handleSubmitLiveOrder(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.CancelLiveOrderRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.CancelLiveOrderRequest, *altv1.CancelLiveOrderResponse]( &client.Communicator, func(req *altv1.CancelLiveOrderRequest) (*altv1.CancelLiveOrderResponse, error) { return handleCancelLiveOrder(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.GetLiveOrderRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.GetLiveOrderRequest, *altv1.GetLiveOrderResponse]( &client.Communicator, func(req *altv1.GetLiveOrderRequest) (*altv1.GetLiveOrderResponse, error) { return handleGetLiveOrder(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.GetLiveRiskPolicyRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.GetLiveRiskPolicyRequest, *altv1.GetLiveRiskPolicyResponse]( &client.Communicator, func(req *altv1.GetLiveRiskPolicyRequest) (*altv1.GetLiveRiskPolicyResponse, error) { return handleGetLiveRiskPolicy(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.GetLiveKillSwitchRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.GetLiveKillSwitchRequest, *altv1.GetLiveKillSwitchResponse]( &client.Communicator, func(req *altv1.GetLiveKillSwitchRequest) (*altv1.GetLiveKillSwitchResponse, error) { return handleGetLiveKillSwitch(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.SetLiveKillSwitchRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.SetLiveKillSwitchRequest, *altv1.SetLiveKillSwitchResponse]( &client.Communicator, func(req *altv1.SetLiveKillSwitchRequest) (*altv1.SetLiveKillSwitchResponse, error) { return handleSetLiveKillSwitch(deps, req) }, ) }, }, { requestType: protoSocket.TypeNameOf(&altv1.ListLiveAuditEventsRequest{}), register: func(client *protoSocket.WsClient) { protoSocket.AddRequestListenerTyped[*altv1.ListLiveAuditEventsRequest, *altv1.ListLiveAuditEventsResponse]( &client.Communicator, func(req *altv1.ListLiveAuditEventsRequest) (*altv1.ListLiveAuditEventsResponse, error) { return handleListLiveAuditEvents(deps, req) }, ) }, }, } } func handleGetLiveRiskPolicy(deps Deps, req *altv1.GetLiveRiskPolicyRequest) (*altv1.GetLiveRiskPolicyResponse, error) { if req.GetAccountId() == "" { return &altv1.GetLiveRiskPolicyResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_risk_policy: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.GetLiveRiskPolicyResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } p := deps.Live.GetRiskPolicy() return &altv1.GetLiveRiskPolicyResponse{Policy: riskPolicyToProto(p)}, nil } func handleGetLiveKillSwitch(deps Deps, req *altv1.GetLiveKillSwitchRequest) (*altv1.GetLiveKillSwitchResponse, error) { if req.GetAccountId() == "" { return &altv1.GetLiveKillSwitchResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_kill_switch: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.GetLiveKillSwitchResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ks := deps.Live.GetKillSwitch() return &altv1.GetLiveKillSwitchResponse{State: killSwitchToProto(ks)}, nil } func handleSetLiveKillSwitch(deps Deps, req *altv1.SetLiveKillSwitchRequest) (*altv1.SetLiveKillSwitchResponse, error) { if req.GetAccountId() == "" { return &altv1.SetLiveKillSwitchResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "set_live_kill_switch: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.SetLiveKillSwitchResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } newKS := trading.KillSwitchState{ Halted: req.GetHalted(), Reason: req.GetReason(), } result := deps.Live.SetKillSwitch(newKS) return &altv1.SetLiveKillSwitchResponse{State: killSwitchToProto(result)}, nil } func handleGetLiveBrokerCapabilities(deps Deps, req *altv1.GetLiveBrokerCapabilitiesRequest) (*altv1.GetLiveBrokerCapabilitiesResponse, error) { if req.GetAccountId() == "" { return &altv1.GetLiveBrokerCapabilitiesResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "invalid live trading request: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.GetLiveBrokerCapabilitiesResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() cap, err := deps.Live.GetLiveBrokerCapabilities(ctx, req.GetAccountId()) if err != nil { return &altv1.GetLiveBrokerCapabilitiesResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInternal, Message: err.Error()}, }, nil } return &altv1.GetLiveBrokerCapabilitiesResponse{ BrokerCapabilities: liveCapabilityToProto(cap), }, nil } func handleSubmitLiveOrder(deps Deps, req *altv1.SubmitLiveOrderRequest) (*altv1.SubmitLiveOrderResponse, error) { if req.GetAccountId() == "" { return &altv1.SubmitLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "submit_live_order: account_id is required"}, }, nil } if req.GetIntent() == nil { return &altv1.SubmitLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "submit_live_order: intent is required"}, }, nil } if deps.Live == nil { return &altv1.SubmitLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() domainReq, err := protoToSubmitRequest(req) if err != nil { return &altv1.SubmitLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: err.Error()}, }, nil } order, err := deps.Live.SubmitLiveOrder(ctx, domainReq) if err != nil { code, msg := liveOrderErrorCode(err) resp := &altv1.SubmitLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: code, Message: msg}, } // Risk-blocked submissions return both a rejected order record and the // denial decision so headless operators can see which guard triggered. if isLiveErr(err, livetrading.ErrRiskBlocked) && order.ID != "" { resp.Order = liveOrderToProto(order) resp.RiskDecision = riskDecisionToProto(trading.RiskDecision{ Allowed: false, Reason: order.RejectionReason, }) } return resp, nil } return &altv1.SubmitLiveOrderResponse{Order: liveOrderToProto(order)}, nil } func handleCancelLiveOrder(deps Deps, req *altv1.CancelLiveOrderRequest) (*altv1.CancelLiveOrderResponse, error) { if req.GetAccountId() == "" { return &altv1.CancelLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "cancel_live_order: account_id is required"}, }, nil } if req.GetLiveOrderId() == "" { return &altv1.CancelLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "cancel_live_order: live_order_id is required"}, }, nil } if deps.Live == nil { return &altv1.CancelLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() order, err := deps.Live.CancelLiveOrder(ctx, req.GetAccountId(), req.GetLiveOrderId()) if err != nil { code, msg := liveOrderErrorCode(err) return &altv1.CancelLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: code, Message: msg}, }, nil } return &altv1.CancelLiveOrderResponse{Order: liveOrderToProto(order)}, nil } func handleGetLiveOrder(deps Deps, req *altv1.GetLiveOrderRequest) (*altv1.GetLiveOrderResponse, error) { if req.GetAccountId() == "" { return &altv1.GetLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_order: account_id is required"}, }, nil } if req.GetLiveOrderId() == "" { return &altv1.GetLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_order: live_order_id is required"}, }, nil } if deps.Live == nil { return &altv1.GetLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() order, err := deps.Live.GetLiveOrder(ctx, req.GetAccountId(), req.GetLiveOrderId()) if err != nil { code, msg := liveOrderErrorCode(err) return &altv1.GetLiveOrderResponse{ Error: &altv1.ErrorInfo{Code: code, Message: msg}, }, nil } return &altv1.GetLiveOrderResponse{Order: liveOrderToProto(order)}, nil } // liveOrderErrorCode maps a livetrading service error to the ALT typed error // vocabulary so the API and CLI receive stable, discriminable codes. func liveOrderErrorCode(err error) (code, message string) { switch { case isLiveErr(err, livetrading.ErrConfirmationRequired): return backtestErrorInvalidRequest, err.Error() case isLiveErr(err, livetrading.ErrInvalidIntent): return backtestErrorInvalidRequest, err.Error() case isLiveErr(err, livetrading.ErrRiskBlocked): return backtestErrorInvalidRequest, err.Error() case isLiveErr(err, livetrading.ErrBrokerUnavailable): return backtestErrorUnavailable, err.Error() case isLiveErr(err, livetrading.ErrOrderNotFound): return backtestErrorNotFound, err.Error() default: return backtestErrorInternal, err.Error() } } func handleSyncLiveAccount(deps Deps, req *altv1.SyncLiveAccountRequest) (*altv1.SyncLiveAccountResponse, error) { if req.GetAccountId() == "" { return &altv1.SyncLiveAccountResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "sync_live_account: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.SyncLiveAccountResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() snap, err := deps.Live.SyncLiveAccount(ctx, req.GetAccountId()) if err != nil { code, msg := liveAccountErrorCode(err) return &altv1.SyncLiveAccountResponse{ Error: &altv1.ErrorInfo{Code: code, Message: msg}, }, nil } return &altv1.SyncLiveAccountResponse{Snapshot: accountSnapshotToProto(snap)}, nil } func handleGetLiveAccountSnapshot(deps Deps, req *altv1.GetLiveAccountSnapshotRequest) (*altv1.GetLiveAccountSnapshotResponse, error) { if req.GetAccountId() == "" { return &altv1.GetLiveAccountSnapshotResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_account_snapshot: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.GetLiveAccountSnapshotResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() snap, err := deps.Live.GetLiveAccountSnapshot(ctx, req.GetAccountId()) if err != nil { code, msg := liveAccountErrorCode(err) return &altv1.GetLiveAccountSnapshotResponse{ Error: &altv1.ErrorInfo{Code: code, Message: msg}, }, nil } return &altv1.GetLiveAccountSnapshotResponse{Snapshot: accountSnapshotToProto(snap)}, nil } func handleListLiveAuditEvents(deps Deps, req *altv1.ListLiveAuditEventsRequest) (*altv1.ListLiveAuditEventsResponse, error) { if req.GetAccountId() == "" { return &altv1.ListLiveAuditEventsResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "list_live_audit_events: account_id is required"}, }, nil } if deps.Live == nil { return &altv1.ListLiveAuditEventsResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorUnavailable, Message: "live trading is not available"}, }, nil } ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout) defer cancel() filter := protoToAuditFilter(req) events, err := deps.Live.ListLiveAuditEvents(ctx, filter) if err != nil { return &altv1.ListLiveAuditEventsResponse{ Error: &altv1.ErrorInfo{Code: backtestErrorInternal, Message: err.Error()}, }, nil } protoEvents := make([]*altv1.LiveAuditEvent, 0, len(events)) for _, ev := range events { protoEvents = append(protoEvents, auditEventToProto(ev)) } return &altv1.ListLiveAuditEventsResponse{Events: protoEvents}, nil } func liveAccountErrorCode(err error) (code, message string) { switch { case isLiveErr(err, livetrading.ErrBrokerUnavailable): return backtestErrorUnavailable, err.Error() case isLiveErr(err, livetrading.ErrAccountSnapshotNotFound): return backtestErrorNotFound, err.Error() default: return backtestErrorInternal, err.Error() } }