alt/services/api/internal/socket/paper.go
toki aaf1af6387 feat(paper-trading): paper 상태 모니터링을 연결한다
Paper trading readiness에서 API/worker/CLI가 같은 protobuf 계약으로 paper state를 시작하고 조회할 수 있어야 한다. Headless 운영 경로를 먼저 닫기 위해 contract, worker runtime, API forwarding, CLI scenario, client parser map과 검증 artifact를 함께 반영한다.
2026-06-05 15:14:41 +09:00

84 lines
3.4 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)
})
},
},
}
}
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
}