- 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
183 lines
7.5 KiB
Go
183 lines
7.5 KiB
Go
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/services/api/internal/workerclient"
|
|
)
|
|
|
|
// paperHandlers returns the API session handlers for the paper trading surface.
|
|
// Each handler validates the request shape and forwards it to the worker through
|
|
// the injected Worker client; paper execution and state stay worker-owned.
|
|
func paperHandlers(worker workerclient.WorkerClient) []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(worker, 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(worker, 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(worker, 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(worker, 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(worker, req)
|
|
})
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleStartPaperTrading(worker workerclient.WorkerClient, req *altv1.StartPaperTradingRequest) (*altv1.StartPaperTradingResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.StartPaperTradingResponse{Error: invalidRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetSpec() == nil {
|
|
return &altv1.StartPaperTradingResponse{Error: invalidRequest("spec is required")}, nil
|
|
}
|
|
if req.GetStartingCash() == nil {
|
|
return &altv1.StartPaperTradingResponse{Error: invalidRequest("starting_cash is required")}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.StartPaperTradingResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.StartPaperTradingResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.StartPaperTrading(ctx, req)
|
|
if err != nil {
|
|
return &altv1.StartPaperTradingResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.StartPaperTradingResponse{Error: internalError("worker returned no paper start response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleGetPaperTradingState(worker workerclient.WorkerClient, req *altv1.GetPaperTradingStateRequest) (*altv1.GetPaperTradingStateResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.GetPaperTradingStateResponse{Error: invalidRequest("account_id is required")}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.GetPaperTradingState(ctx, req)
|
|
if err != nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.GetPaperTradingStateResponse{Error: internalError("worker returned no paper state response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleSubmitPaperOrder(worker workerclient.WorkerClient, req *altv1.SubmitPaperOrderRequest) (*altv1.SubmitPaperOrderResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.SubmitPaperOrderResponse{Error: invalidRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetInstrumentId() == "" {
|
|
return &altv1.SubmitPaperOrderResponse{Error: invalidRequest("instrument_id is required")}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.SubmitPaperOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.SubmitPaperOrderResponse{Error: internalError("worker returned no submit order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleCancelPaperOrder(worker workerclient.WorkerClient, req *altv1.CancelPaperOrderRequest) (*altv1.CancelPaperOrderResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.CancelPaperOrderResponse{Error: invalidRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetOrderId() == "" {
|
|
return &altv1.CancelPaperOrderResponse{Error: invalidRequest("order_id is required")}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.CancelPaperOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.CancelPaperOrderResponse{Error: internalError("worker returned no cancel order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleFillPaperOrder(worker workerclient.WorkerClient, req *altv1.FillPaperOrderRequest) (*altv1.FillPaperOrderResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidRequest("account_id is required")}, nil
|
|
}
|
|
if req.GetOrderId() == "" {
|
|
return &altv1.FillPaperOrderResponse{Error: invalidRequest("order_id is required")}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.FillPaperOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.FillPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.FillPaperOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.FillPaperOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.FillPaperOrderResponse{Error: internalError("worker returned no fill order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|