실거래 주문 생성/조회/취소 흐름이 contracts, worker, API, CLI, Flutter parser까지 같은 메시지 경계로 이어져야 한다.\n\n브로커 호출 전 operator confirmation과 malformed order validation을 worker-owned runtime에서 막고, 리뷰 완료 산출물을 archive에 보존한다.
161 lines
6.5 KiB
Go
161 lines
6.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"
|
|
)
|
|
|
|
// apiLiveHandlers returns the API session handlers for the live trading surface.
|
|
// Each handler validates the request shape and forwards it to the worker through
|
|
// the injected WorkerClient; live execution stays worker-owned.
|
|
func apiLiveHandlers(worker workerclient.WorkerClient) []sessionHandler {
|
|
return []sessionHandler{
|
|
{
|
|
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(worker, 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(worker, 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(worker, 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(worker, req)
|
|
})
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleGetLiveBrokerCapabilities(worker workerclient.WorkerClient, 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 worker == nil {
|
|
return &altv1.GetLiveBrokerCapabilitiesResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.GetLiveBrokerCapabilitiesResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.GetLiveBrokerCapabilities(ctx, req)
|
|
if err != nil {
|
|
return &altv1.GetLiveBrokerCapabilitiesResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.GetLiveBrokerCapabilitiesResponse{Error: internalError("worker returned no live capability response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleSubmitLiveOrder(worker workerclient.WorkerClient, 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 worker == nil {
|
|
return &altv1.SubmitLiveOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.SubmitLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.SubmitLiveOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.SubmitLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.SubmitLiveOrderResponse{Error: internalError("worker returned no submit live order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleCancelLiveOrder(worker workerclient.WorkerClient, 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 worker == nil {
|
|
return &altv1.CancelLiveOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.CancelLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.CancelLiveOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.CancelLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.CancelLiveOrderResponse{Error: internalError("worker returned no cancel live order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleGetLiveOrder(worker workerclient.WorkerClient, 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 worker == nil {
|
|
return &altv1.GetLiveOrderResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.GetLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.GetLiveOrder(ctx, req)
|
|
if err != nil {
|
|
return &altv1.GetLiveOrderResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.GetLiveOrderResponse{Error: internalError("worker returned no get live order response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|