실거래 주문이 브로커에 도달하기 전에 kill switch와 계정별 주문 한도를 검증해야 한다. API, CLI, client parser surface와 완료된 review archive를 함께 정리한다.
389 lines
13 KiB
Go
389 lines
13 KiB
Go
package socket
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
altv1 "git.toki-labs.com/toki/alt/packages/contracts/gen/go/alt/v1"
|
|
apiContracts "git.toki-labs.com/toki/alt/services/api/internal/contracts"
|
|
"git.toki-labs.com/toki/alt/services/api/internal/workerclient"
|
|
protoSocket "git.toki-labs.com/toki/proto-socket/go"
|
|
)
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerForward(t *testing.T) {
|
|
expectedCap := &altv1.LiveBrokerCapability{Broker: "kis"}
|
|
worker := &fakeWorkerClient{
|
|
liveCapRes: &altv1.GetLiveBrokerCapabilitiesResponse{BrokerCapabilities: expectedCap},
|
|
isConnected: true,
|
|
}
|
|
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetBrokerCapabilities() == nil {
|
|
t.Fatal("expected broker capabilities, got nil")
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 1 {
|
|
t.Errorf("expected worker call count 1, got %d", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
if resp.GetBrokerCapabilities().GetBroker() != "kis" {
|
|
t.Errorf("broker mismatch: %q", resp.GetBrokerCapabilities().GetBroker())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesNilWorker(t *testing.T) {
|
|
resp, err := handleGetLiveBrokerCapabilities(nil, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed unavailable response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorUnavailable {
|
|
t.Errorf("expected unavailable error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesEmptyAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: ""})
|
|
if err != nil {
|
|
t.Fatalf("expected typed invalid_request response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
|
|
t.Errorf("expected invalid_request error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
if resp.GetError().GetMessage() != "invalid live trading request: account_id is required" {
|
|
t.Errorf("unexpected error message: %q", resp.GetError().GetMessage())
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 0 {
|
|
t.Errorf("worker should not be called when account_id is empty, got %d calls", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerUnavailable(t *testing.T) {
|
|
worker := &fakeWorkerClient{connectErr: workerclient.ErrUnavailable, isConnected: false}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed unavailable response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorUnavailable {
|
|
t.Errorf("expected unavailable error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerTimeout(t *testing.T) {
|
|
worker := &fakeWorkerClient{connectErr: workerclient.ErrTimeout, isConnected: false}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed timeout response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorTimeout {
|
|
t.Errorf("expected timeout error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
// TestHandleGetLiveBrokerCapabilitiesWorkerCallTimeout verifies the live handler
|
|
// maps a worker-level call timeout (GetLiveBrokerCapabilities returning
|
|
// workerclient.ErrTimeout) to backtestErrorTimeout. This is separate from the
|
|
// Connect failure test above because Connect failure path is tested with
|
|
// connectErr: workerclient.ErrTimeout above.
|
|
func TestHandleGetLiveBrokerCapabilitiesWorkerCallTimeout(t *testing.T) {
|
|
worker := &fakeWorkerClient{err: workerclient.ErrTimeout, isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed timeout response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorTimeout {
|
|
t.Errorf("expected timeout error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
if worker.GetLiveBrokerCapabilitiesCallCount() != 1 {
|
|
t.Errorf("expected worker call count 1, got %d", worker.GetLiveBrokerCapabilitiesCallCount())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveBrokerCapabilitiesNilWorkerResponse(t *testing.T) {
|
|
worker := &fakeWorkerClient{liveCapRes: nil, isConnected: true}
|
|
resp, err := handleGetLiveBrokerCapabilities(worker, &altv1.GetLiveBrokerCapabilitiesRequest{AccountId: "test"})
|
|
if err != nil {
|
|
t.Fatalf("expected typed internal response, got error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorInternal {
|
|
t.Errorf("expected internal error code, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestSessionHandlersIncludeLive(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
handlers := sessionHandlers(worker)
|
|
seenLive := false
|
|
for _, h := range handlers {
|
|
if h.requestType == "alt.v1.GetLiveBrokerCapabilitiesRequest" {
|
|
seenLive = true
|
|
break
|
|
}
|
|
}
|
|
if !seenLive {
|
|
t.Error("sessionHandlers should include live capability handler")
|
|
}
|
|
}
|
|
|
|
func TestLiveParserMapContainsLiveMessages(t *testing.T) {
|
|
pm := apiContracts.ParserMap()
|
|
for _, m := range []proto.Message{
|
|
&altv1.GetLiveBrokerCapabilitiesRequest{},
|
|
&altv1.GetLiveBrokerCapabilitiesResponse{},
|
|
&altv1.LiveBrokerCapability{},
|
|
&altv1.SubmitLiveOrderRequest{},
|
|
&altv1.SubmitLiveOrderResponse{},
|
|
&altv1.CancelLiveOrderRequest{},
|
|
&altv1.CancelLiveOrderResponse{},
|
|
&altv1.GetLiveOrderRequest{},
|
|
&altv1.GetLiveOrderResponse{},
|
|
&altv1.GetLiveRiskPolicyRequest{},
|
|
&altv1.GetLiveRiskPolicyResponse{},
|
|
&altv1.LiveRiskPolicy{},
|
|
&altv1.GetLiveKillSwitchRequest{},
|
|
&altv1.GetLiveKillSwitchResponse{},
|
|
&altv1.SetLiveKillSwitchRequest{},
|
|
&altv1.SetLiveKillSwitchResponse{},
|
|
&altv1.LiveKillSwitchState{},
|
|
} {
|
|
name := protoSocket.TypeNameOf(m)
|
|
if _, ok := pm[name]; !ok {
|
|
t.Errorf("API parser map missing %q", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveRiskPolicyForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveRiskPolicyRes: &altv1.GetLiveRiskPolicyResponse{
|
|
Policy: &altv1.LiveRiskPolicy{MaxDailyOrders: 5, MaxOpenOrders: 3, AllowShortSelling: false},
|
|
},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleGetLiveRiskPolicy(worker, &altv1.GetLiveRiskPolicyRequest{AccountId: "acct-1"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetPolicy() == nil {
|
|
t.Fatal("expected policy, got nil")
|
|
}
|
|
if resp.GetPolicy().GetMaxDailyOrders() != 5 {
|
|
t.Errorf("expected max_daily_orders 5, got %d", resp.GetPolicy().GetMaxDailyOrders())
|
|
}
|
|
if worker.liveRiskPolicyReq == nil {
|
|
t.Error("expected worker to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveRiskPolicyMissingAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleGetLiveRiskPolicy(worker, &altv1.GetLiveRiskPolicyRequest{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected typed error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
|
|
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
|
|
}
|
|
if worker.liveRiskPolicyReq != nil {
|
|
t.Error("worker should not be called when account_id is missing")
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveKillSwitchForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveGetKillSwitchRes: &altv1.GetLiveKillSwitchResponse{
|
|
State: &altv1.LiveKillSwitchState{Halted: true, Reason: "kill switch active by default"},
|
|
},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleGetLiveKillSwitch(worker, &altv1.GetLiveKillSwitchRequest{AccountId: "acct-1"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetState() == nil {
|
|
t.Fatal("expected state, got nil")
|
|
}
|
|
if !resp.GetState().GetHalted() {
|
|
t.Error("expected halted=true")
|
|
}
|
|
if worker.liveGetKillSwitchReq == nil {
|
|
t.Error("expected worker to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleSetLiveKillSwitchForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveSetKillSwitchRes: &altv1.SetLiveKillSwitchResponse{
|
|
State: &altv1.LiveKillSwitchState{Halted: false, Reason: "operator cleared"},
|
|
},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleSetLiveKillSwitch(worker, &altv1.SetLiveKillSwitchRequest{
|
|
AccountId: "acct-1",
|
|
Halted: false,
|
|
Reason: "operator cleared",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetState() == nil {
|
|
t.Fatal("expected state, got nil")
|
|
}
|
|
if resp.GetState().GetHalted() {
|
|
t.Error("expected halted=false after operator clear")
|
|
}
|
|
if resp.GetState().GetReason() != "operator cleared" {
|
|
t.Errorf("unexpected reason: %q", resp.GetState().GetReason())
|
|
}
|
|
if worker.liveSetKillSwitchReq == nil {
|
|
t.Error("expected worker to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleSetLiveKillSwitchMissingAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleSetLiveKillSwitch(worker, &altv1.SetLiveKillSwitchRequest{Halted: false})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected typed error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
|
|
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveSubmitRes: &altv1.SubmitLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "submitted"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleSubmitLiveOrder(worker, &altv1.SubmitLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930", Side: "buy", Type: "market"},
|
|
OperatorConfirmation: &altv1.OperatorConfirmation{Confirmed: true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetId() != "lo-1" {
|
|
t.Errorf("unexpected order id: %q", resp.GetOrder().GetId())
|
|
}
|
|
if worker.liveSubmitReq == nil {
|
|
t.Error("expected worker to be called")
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderMissingAccountID(t *testing.T) {
|
|
worker := &fakeWorkerClient{isConnected: true}
|
|
resp, err := handleSubmitLiveOrder(worker, &altv1.SubmitLiveOrderRequest{
|
|
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected typed error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorInvalidRequest {
|
|
t.Errorf("expected invalid_request, got %q", resp.GetError().GetCode())
|
|
}
|
|
if worker.liveSubmitReq != nil {
|
|
t.Error("worker should not be called when account_id is missing")
|
|
}
|
|
}
|
|
|
|
func TestHandleSubmitLiveOrderNilWorker(t *testing.T) {
|
|
resp, err := handleSubmitLiveOrder(nil, &altv1.SubmitLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
Intent: &altv1.LiveOrderIntent{InstrumentId: "KRX:005930"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() == nil {
|
|
t.Fatal("expected typed error, got nil")
|
|
}
|
|
if resp.GetError().GetCode() != backtestErrorUnavailable {
|
|
t.Errorf("expected unavailable, got %q", resp.GetError().GetCode())
|
|
}
|
|
}
|
|
|
|
func TestHandleCancelLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveCancelRes: &altv1.CancelLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "canceled"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleCancelLiveOrder(worker, &altv1.CancelLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
LiveOrderId: "lo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetStatus() != "canceled" {
|
|
t.Errorf("unexpected order status: %q", resp.GetOrder().GetStatus())
|
|
}
|
|
}
|
|
|
|
func TestHandleGetLiveOrderForward(t *testing.T) {
|
|
worker := &fakeWorkerClient{
|
|
liveGetRes: &altv1.GetLiveOrderResponse{Order: &altv1.LiveOrder{Id: "lo-1", Status: "filled", BrokerStatus: "FILLED"}},
|
|
isConnected: true,
|
|
}
|
|
resp, err := handleGetLiveOrder(worker, &altv1.GetLiveOrderRequest{
|
|
AccountId: "acct-1",
|
|
LiveOrderId: "lo-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if resp.GetError() != nil {
|
|
t.Fatalf("unexpected typed error: %+v", resp.GetError())
|
|
}
|
|
if resp.GetOrder().GetBrokerStatus() != "FILLED" {
|
|
t.Errorf("expected broker_status FILLED, got %q", resp.GetOrder().GetBrokerStatus())
|
|
}
|
|
}
|