실거래 주문이 브로커에 도달하기 전에 kill switch와 계정별 주문 한도를 검증해야 한다. API, CLI, client parser surface와 완료된 review archive를 함께 정리한다.
257 lines
11 KiB
Go
257 lines
11 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)
|
|
})
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.GetLiveRiskPolicyRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.GetLiveRiskPolicyRequest, *altv1.GetLiveRiskPolicyResponse](&client.Communicator, func(req *altv1.GetLiveRiskPolicyRequest) (*altv1.GetLiveRiskPolicyResponse, error) {
|
|
return handleGetLiveRiskPolicy(worker, req)
|
|
})
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.GetLiveKillSwitchRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.GetLiveKillSwitchRequest, *altv1.GetLiveKillSwitchResponse](&client.Communicator, func(req *altv1.GetLiveKillSwitchRequest) (*altv1.GetLiveKillSwitchResponse, error) {
|
|
return handleGetLiveKillSwitch(worker, req)
|
|
})
|
|
},
|
|
},
|
|
{
|
|
requestType: protoSocket.TypeNameOf(&altv1.SetLiveKillSwitchRequest{}),
|
|
register: func(client *protoSocket.WsClient) {
|
|
protoSocket.AddRequestListenerTyped[*altv1.SetLiveKillSwitchRequest, *altv1.SetLiveKillSwitchResponse](&client.Communicator, func(req *altv1.SetLiveKillSwitchRequest) (*altv1.SetLiveKillSwitchResponse, error) {
|
|
return handleSetLiveKillSwitch(worker, req)
|
|
})
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func handleGetLiveRiskPolicy(worker workerclient.WorkerClient, req *altv1.GetLiveRiskPolicyRequest) (*altv1.GetLiveRiskPolicyResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.GetLiveRiskPolicyResponse{
|
|
Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_risk_policy: account_id is required"},
|
|
}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.GetLiveRiskPolicyResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.GetLiveRiskPolicyResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.GetLiveRiskPolicy(ctx, req)
|
|
if err != nil {
|
|
return &altv1.GetLiveRiskPolicyResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.GetLiveRiskPolicyResponse{Error: internalError("worker returned no live risk policy response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleGetLiveKillSwitch(worker workerclient.WorkerClient, req *altv1.GetLiveKillSwitchRequest) (*altv1.GetLiveKillSwitchResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.GetLiveKillSwitchResponse{
|
|
Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "get_live_kill_switch: account_id is required"},
|
|
}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.GetLiveKillSwitchResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.GetLiveKillSwitchResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.GetLiveKillSwitch(ctx, req)
|
|
if err != nil {
|
|
return &altv1.GetLiveKillSwitchResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.GetLiveKillSwitchResponse{Error: internalError("worker returned no live kill switch response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
func handleSetLiveKillSwitch(worker workerclient.WorkerClient, req *altv1.SetLiveKillSwitchRequest) (*altv1.SetLiveKillSwitchResponse, error) {
|
|
if req.GetAccountId() == "" {
|
|
return &altv1.SetLiveKillSwitchResponse{
|
|
Error: &altv1.ErrorInfo{Code: backtestErrorInvalidRequest, Message: "set_live_kill_switch: account_id is required"},
|
|
}, nil
|
|
}
|
|
if worker == nil {
|
|
return &altv1.SetLiveKillSwitchResponse{Error: workerUnavailable()}, nil
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), workerRequestTimeout)
|
|
defer cancel()
|
|
if err := worker.Connect(ctx); err != nil {
|
|
return &altv1.SetLiveKillSwitchResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
res, err := worker.SetLiveKillSwitch(ctx, req)
|
|
if err != nil {
|
|
return &altv1.SetLiveKillSwitchResponse{Error: workerErrorInfo(err)}, nil
|
|
}
|
|
if res == nil {
|
|
return &altv1.SetLiveKillSwitchResponse{Error: internalError("worker returned no set live kill switch response")}, nil
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
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
|
|
}
|