- Archive completed subtasks (02+01_risk_command, 03+02_order_lifecycle) - Add paper_order_lifecycle test data and expected output - Update paper trading proto and regenerate code (Dart, Go) - Fix order lifecycle handling in CLI operator, API socket, worker socket - Update parser maps across CLI, API, and worker services - Update backtest and paper trading tests
226 lines
9.1 KiB
Go
226 lines
9.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/packages/domain/market"
|
|
"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)
|
|
|
|
// Virtual order lifecycle: submit records a pending order, cancel transitions
|
|
// a pending order, fill simulates execution against the account portfolio.
|
|
SubmitPaperOrder(ctx context.Context, req papertrading.SubmitOrderRequest) (papertrading.PaperOrder, error)
|
|
CancelPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string) (papertrading.PaperOrder, error)
|
|
FillPaperOrder(ctx context.Context, accountID backtest.PaperAccountID, orderID string, fillPrice market.Price) (papertrading.PaperOrder, 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)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.SubmitPaperOrderRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.SubmitPaperOrderRequest, *altv1.SubmitPaperOrderResponse](
|
|
&client.Communicator,
|
|
func(req *altv1.SubmitPaperOrderRequest) (*altv1.SubmitPaperOrderResponse, error) {
|
|
return handleSubmitPaperOrder(deps, req)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.CancelPaperOrderRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.CancelPaperOrderRequest, *altv1.CancelPaperOrderResponse](
|
|
&client.Communicator,
|
|
func(req *altv1.CancelPaperOrderRequest) (*altv1.CancelPaperOrderResponse, error) {
|
|
return handleCancelPaperOrder(deps, req)
|
|
},
|
|
)
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.FillPaperOrderRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.FillPaperOrderRequest, *altv1.FillPaperOrderResponse](
|
|
&client.Communicator,
|
|
func(req *altv1.FillPaperOrderRequest) (*altv1.FillPaperOrderResponse, error) {
|
|
return handleFillPaperOrder(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 handleSubmitPaperOrder(deps Deps, req *altv1.SubmitPaperOrderRequest) (*altv1.SubmitPaperOrderResponse, error) {
|
|
submitReq, err := submitPaperOrderRequestFromProto(req)
|
|
if err != nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: invalidPaperRequest(err.Error())}, nil
|
|
}
|
|
if deps.Paper == nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
order, err := deps.Paper.SubmitPaperOrder(ctx, submitReq)
|
|
if err != nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.SubmitPaperOrderResponse{Order: paperOrderToProto(order)}, nil
|
|
}
|
|
|
|
func handleCancelPaperOrder(deps Deps, req *altv1.CancelPaperOrderRequest) (*altv1.CancelPaperOrderResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.CancelPaperOrderResponse{Error: invalidPaperRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetOrderId() == "" {
|
|
return &altv1.CancelPaperOrderResponse{Error: invalidPaperRequest("order_id is required")}, nil
|
|
}
|
|
if deps.Paper == nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
order, err := deps.Paper.CancelPaperOrder(ctx, backtest.PaperAccountID(req.GetAccountId()), req.GetOrderId())
|
|
if err != nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.CancelPaperOrderResponse{Order: paperOrderToProto(order)}, nil
|
|
}
|
|
|
|
func handleFillPaperOrder(deps Deps, req *altv1.FillPaperOrderRequest) (*altv1.FillPaperOrderResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetOrderId() == "" {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("order_id is required")}, nil
|
|
}
|
|
var fillPrice market.Price
|
|
if req.GetFillPrice() != nil {
|
|
price, err := priceFromProto(req.GetFillPrice())
|
|
if err != nil {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest("fill_price: " + err.Error())}, nil
|
|
}
|
|
if err := papertrading.ValidateOrderDecimal("fill_price", price.Amount.Value, false); err != nil {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidPaperRequest(err.Error())}, nil
|
|
}
|
|
fillPrice = price
|
|
}
|
|
if deps.Paper == nil {
|
|
return &altv1.FillPaperOrderResponse{Error: unavailableError("paper trading is not available")}, nil
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
|
|
defer cancel()
|
|
|
|
accountID := backtest.PaperAccountID(req.GetAccountId())
|
|
order, state, err := deps.Paper.FillPaperOrder(ctx, accountID, req.GetOrderId(), fillPrice)
|
|
if err != nil {
|
|
return &altv1.FillPaperOrderResponse{Error: paperBackendErrorInfo(err)}, nil
|
|
}
|
|
return &altv1.FillPaperOrderResponse{
|
|
Order: paperOrderToProto(order),
|
|
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), errors.Is(err, papertrading.ErrOrderNotFound):
|
|
return errorInfo(backtestErrorNotFound, err.Error())
|
|
case errors.Is(err, papertrading.ErrOrderNotPending), errors.Is(err, papertrading.ErrFillPriceRequired), errors.Is(err, papertrading.ErrInvalidOrderInput):
|
|
return errorInfo(backtestErrorInvalidRequest, err.Error())
|
|
case errors.Is(err, context.DeadlineExceeded):
|
|
return errorInfo(backtestErrorTimeout, err.Error())
|
|
default:
|
|
return errorInfo(backtestErrorInternal, err.Error())
|
|
}
|
|
}
|