alt/services/worker/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

108 lines
4.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/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)
}
// 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)
},
)
},
},
}
}
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 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):
return errorInfo(backtestErrorNotFound, err.Error())
case errors.Is(err, context.DeadlineExceeded):
return errorInfo(backtestErrorTimeout, err.Error())
default:
return errorInfo(backtestErrorInternal, err.Error())
}
}