From ee236d1fe0a6d1f2441961950da68b40491b0989 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Sun, 5 Apr 2026 12:00:18 +0700 Subject: [PATCH] refactor: rewrite SDK to align with OpenAPI spec - Split monolithic models.go (738 lines) into 6 domain files - Fix schema drift: BasicInfo, PersonalInfo, TotalCashDerivativeResponse, derivative order types, money transfer types aligned to spec - Add missing REST endpoints: bsa-ext, bsa-month (supply/demand) - Add WebSocket support for 5 streaming endpoints (nhooyr.io/websocket) - Add 45 httptest-based tests (74.9% coverage) - Rewrite README with full API coverage table BREAKING CHANGE: struct fields and types changed to match OpenAPI spec. BasicInfo reduced to 5 fields, TokenResponse uses 'token' field, PlaceOrderRequest uses int types, derivative order types renamed. --- README.md | 162 +++++++++- account_test.go | 32 ++ auth.go | 13 +- auth_test.go | 30 ++ client_test.go | 85 +++++ derivative.go | 25 +- derivative_test.go | 216 +++++++++++++ example/main.go | 23 +- go.mod | 2 + go.sum | 2 + models.go | 737 ------------------------------------------- models_account.go | 80 +++++ models_asset.go | 245 ++++++++++++++ models_derivative.go | 305 ++++++++++++++++++ models_market.go | 227 +++++++++++++ models_money.go | 41 +++ models_order.go | 205 ++++++++++++ money.go | 8 +- money_test.go | 77 +++++ stock_market.go | 45 ++- stock_market_test.go | 132 ++++++++ stock_order.go | 2 +- stock_order_test.go | 79 +++++ stock_query_test.go | 230 ++++++++++++++ websocket.go | 136 ++++++++ 25 files changed, 2362 insertions(+), 777 deletions(-) create mode 100644 account_test.go create mode 100644 auth_test.go create mode 100644 client_test.go create mode 100644 derivative_test.go create mode 100644 go.sum delete mode 100644 models.go create mode 100644 models_account.go create mode 100644 models_asset.go create mode 100644 models_derivative.go create mode 100644 models_market.go create mode 100644 models_money.go create mode 100644 models_order.go create mode 100644 money_test.go create mode 100644 stock_market_test.go create mode 100644 stock_order_test.go create mode 100644 stock_query_test.go create mode 100644 websocket.go diff --git a/README.md b/README.md index 7c1d7db..7ece537 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,160 @@ -# tcbs-api -Api for [TCBS](https://tcinvest.tcbs.com.vn/) trading +# tcbs-api + +Go SDK for [TCBS](https://tcinvest.tcbs.com.vn/) OpenAPI trading platform. + +## Install + +```bash +go get github.com/tiennm99/tcbs-api +``` + +## Quick Start + +```go +package main + +import ( + "context" + "fmt" + "log" + + tcbs "github.com/tiennm99/tcbs-api" +) + +func main() { + client := tcbs.NewClient() + ctx := context.Background() + + // Authenticate + token, err := client.GetToken(ctx, "your-api-key", "your-otp") + if err != nil { + log.Fatal(err) + } + fmt.Println("Authenticated:", token.Token) + + // Get stock prices + prices, err := client.GetStockPrices(ctx, []string{"FPT", "VNM"}) + if err != nil { + log.Fatal(err) + } + for _, p := range prices { + fmt.Printf("%s: %.0f\n", p.Ticker, p.MatchPrice) + } +} +``` + +## Configuration + +```go +// Production (default) +client := tcbs.NewClient() + +// SIT environment +client := tcbs.NewClient(tcbs.WithBaseURL(tcbs.SITBaseURL)) + +// Custom HTTP client +client := tcbs.NewClient(tcbs.WithHTTPClient(&http.Client{Timeout: 60 * time.Second})) + +// Pre-set token +client := tcbs.NewClient(tcbs.WithToken("your-jwt-token")) +``` + +## API Coverage + +### Authentication +| Method | Description | +|--------|-------------| +| `GetToken` | Exchange API Key + OTP for JWT token | + +### Account +| Method | Description | +|--------|-------------| +| `GetSubAccountInfo` | Get sub-account profile information | + +### Stock Orders +| Method | Description | +|--------|-------------| +| `PlaceOrder` | Place a stock order | +| `UpdateOrder` | Modify an existing order | +| `CancelOrder` | Cancel existing orders | + +### Stock Queries +| Method | Description | +|--------|-------------| +| `GetOrders` | Get order book | +| `GetOrderByID` | Get specific order by ID | +| `GetMatchingDetails` | Get order matching details | +| `GetPurchasingPower` | Get purchasing power | +| `GetPurchasingPowerBySymbol` | Get purchasing power for symbol | +| `GetPurchasingPowerBySymbolPrice` | Get purchasing power for symbol at price | +| `GetMarginQuota` | Get margin quota | +| `GetMarginAccountInfo` | Get margin account risk info | +| `GetSupplementaryLoanPackages` | Get loan package details | +| `GetLoans` | Get loan list | +| `GetStockAssets` | Get stock holdings | +| `GetCashBalance` | Get cash balance | +| `GetCashStatements` | Get cash statement history | +| `GetMarginInfo` | Get debt inquiry info | + +### Market Data +| Method | Description | +|--------|-------------| +| `GetStockPrices` | Get stock ticker pricing | +| `GetForeignRoom` | Get foreign investor room info | +| `GetPutThroughInfo` | Get put-through match info | +| `GetIntradayHistory` | Get intraday price history | +| `GetSupplyDemand` | Get 15-min supply/demand data | +| `GetSupplyDemandExt` | Get extended supply/demand data | +| `GetSupplyDemandMonth` | Get monthly supply/demand data | + +### Money Management +| Method | Description | +|--------|-------------| +| `TransferMoney` | Transfer between sub-accounts | +| `DepositMargin` | Deposit margin for derivatives | +| `WithdrawMargin` | Withdraw margin for derivatives | + +### Derivatives +| Method | Description | +|--------|-------------| +| `GetDerivativeCashStatus` | Get derivative cash/margin overview | +| `GetDerivativeClosedPositions` | Get closed positions | +| `GetDerivativeOpenPositions` | Get open positions | +| `GetDerivativeNormalOrders` | List normal orders | +| `GetDerivativeConditionOrders` | List conditional orders | +| `PlaceDerivativeNormalOrder` | Place normal order | +| `PlaceDerivativeConditionOrder` | Place conditional order | +| `ChangeDerivativeNormalOrder` | Modify normal order | +| `ChangeDerivativeConditionOrder` | Modify conditional order | +| `CancelDerivativeNormalOrder` | Cancel normal order | +| `CancelDerivativeConditionOrder` | Cancel conditional order | +| `GetDerivativeMarketInfo` | Get derivative contract pricing | + +### WebSocket Streams +| Method | Description | +|--------|-------------| +| `ConnectStockMatch` | Stock match information stream | +| `ConnectDerivativeMatch` | Derivative match information stream | +| `ConnectCenter` | General WebSocket center | +| `ConnectStockPrice` | Normal stock price stream | +| `ConnectDerivativePrice` | Derivative price stream | + +## WebSocket Usage + +```go +ctx := context.Background() +ws, err := client.ConnectStockPrice(ctx, func(msgType websocket.MessageType, data []byte) { + fmt.Println("Received:", string(data)) +}) +if err != nil { + log.Fatal(err) +} +defer ws.Close() + +// Send subscription message +ws.SendJSON(ctx, map[string]string{"action": "subscribe", "ticker": "FPT"}) +``` + +## License + +See [LICENSE](LICENSE) for details. diff --git a/account_test.go b/account_test.go new file mode 100644 index 0000000..ef08798 --- /dev/null +++ b/account_test.go @@ -0,0 +1,32 @@ +package tcbs + +import ( + "context" + "net/http" + "testing" +) + +func TestGetSubAccountInfo(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + t.Errorf("expected GET, got %s", r.Method) + } + if r.URL.Path != "/eros/v2/get-profile/by-username/105C001" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("fields") != "basicInfo" { + t.Errorf("unexpected fields param: %s", r.URL.Query().Get("fields")) + } + writeJSON(t, w, AccountInformationResponse{ + BasicInfo: &BasicInfo{Code105C: "105C001", Status: "active"}, + }) + }) + + resp, err := client.GetSubAccountInfo(context.Background(), "105C001", "basicInfo") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.BasicInfo == nil || resp.BasicInfo.Code105C != "105C001" { + t.Error("unexpected response") + } +} diff --git a/auth.go b/auth.go index 1400f2e..7896475 100644 --- a/auth.go +++ b/auth.go @@ -4,25 +4,22 @@ import "context" // TokenRequest represents the request body for exchanging API Key + OTP for JWT Token. type TokenRequest struct { - APIKey string `json:"apiKey"` OTP string `json:"otp"` + APIKey string `json:"apiKey"` } // TokenResponse represents a successful token exchange response. type TokenResponse struct { - AccessToken string `json:"accessToken"` - TokenType string `json:"tokenType"` - ExpiresIn int64 `json:"expiresIn"` + Token string `json:"token"` } // TokenErrorResponse represents a failed token exchange response. type TokenErrorResponse struct { - Error string `json:"error"` - ErrorDescription string `json:"error_description"` + Code string `json:"code"` + Message string `json:"message"` } // GetToken exchanges an API Key and OTP for a JWT token. -// The returned token is valid for up to 8 hours. func (c *Client) GetToken(ctx context.Context, apiKey, otp string) (*TokenResponse, error) { var resp TokenResponse err := c.doRequest(ctx, "POST", "/gaia/v1/oauth2/openapi/token", nil, &TokenRequest{ @@ -32,6 +29,6 @@ func (c *Client) GetToken(ctx context.Context, apiKey, otp string) (*TokenRespon if err != nil { return nil, err } - c.SetToken(resp.AccessToken) + c.SetToken(resp.Token) return &resp, nil } diff --git a/auth_test.go b/auth_test.go new file mode 100644 index 0000000..0ed81d7 --- /dev/null +++ b/auth_test.go @@ -0,0 +1,30 @@ +package tcbs + +import ( + "context" + "net/http" + "testing" +) + +func TestGetToken(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/gaia/v1/oauth2/openapi/token" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, TokenResponse{Token: "jwt-123"}) + }) + + resp, err := client.GetToken(context.Background(), "key", "otp") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Token != "jwt-123" { + t.Errorf("expected token 'jwt-123', got %q", resp.Token) + } + if client.currentToken() != "jwt-123" { + t.Errorf("expected client token updated to 'jwt-123', got %q", client.currentToken()) + } +} diff --git a/client_test.go b/client_test.go new file mode 100644 index 0000000..b989ee4 --- /dev/null +++ b/client_test.go @@ -0,0 +1,85 @@ +package tcbs + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +// newTestServer creates a test HTTP server and a Client pointing at it. +func newTestServer(t *testing.T, handler http.HandlerFunc) (*Client, *httptest.Server) { + t.Helper() + srv := httptest.NewServer(handler) + t.Cleanup(srv.Close) + client := NewClient(WithBaseURL(srv.URL), WithToken("test-token")) + return client, srv +} + +// writeJSON is a test helper to write JSON responses. +func writeJSON(t *testing.T, w http.ResponseWriter, v any) { + t.Helper() + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(v); err != nil { + t.Fatalf("failed to encode response: %v", err) + } +} + +func TestNewClient_Defaults(t *testing.T) { + c := NewClient() + if c.baseURL != ProductionBaseURL { + t.Errorf("expected base URL %s, got %s", ProductionBaseURL, c.baseURL) + } + if c.httpClient == nil { + t.Error("expected non-nil http client") + } +} + +func TestNewClient_Options(t *testing.T) { + c := NewClient(WithBaseURL(SITBaseURL), WithToken("tok")) + if c.baseURL != SITBaseURL { + t.Errorf("expected base URL %s, got %s", SITBaseURL, c.baseURL) + } + if c.currentToken() != "tok" { + t.Errorf("expected token 'tok', got %q", c.currentToken()) + } +} + +func TestSetToken(t *testing.T) { + c := NewClient() + c.SetToken("abc") + if c.currentToken() != "abc" { + t.Errorf("expected token 'abc', got %q", c.currentToken()) + } +} + +func TestAPIError(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`{"error":"bad"}`)) + }) + err := client.get(context.Background(), "/fail", nil, nil) + if err == nil { + t.Fatal("expected error") + } + apiErr, ok := err.(*APIError) + if !ok { + t.Fatalf("expected *APIError, got %T", err) + } + if apiErr.StatusCode != 400 { + t.Errorf("expected status 400, got %d", apiErr.StatusCode) + } +} + +func TestAuthorizationHeader(t *testing.T) { + var gotAuth string + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + gotAuth = r.Header.Get("Authorization") + writeJSON(t, w, []MarketStockInfo{}) + }) + _, _ = client.GetStockPrices(context.Background(), []string{"FPT"}) + if gotAuth != "Bearer test-token" { + t.Errorf("expected 'Bearer test-token', got %q", gotAuth) + } +} diff --git a/derivative.go b/derivative.go index 26478eb..6e06d06 100644 --- a/derivative.go +++ b/derivative.go @@ -113,8 +113,8 @@ func (c *Client) GetDerivativeConditionOrders(ctx context.Context, accountID, su } // PlaceDerivativeNormalOrder places a normal derivative order. -func (c *Client) PlaceDerivativeNormalOrder(ctx context.Context, req *DerivativeNormalOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) PlaceDerivativeNormalOrder(ctx context.Context, req *DerivativeNormalOrderRequest) (*DerivativeResponse[*DerivativeNormalOrderPlaceResponse], error) { + var resp DerivativeResponse[*DerivativeNormalOrderPlaceResponse] err := c.post(ctx, "/khronos/v1/order/place", req, &resp) if err != nil { return nil, err @@ -123,8 +123,8 @@ func (c *Client) PlaceDerivativeNormalOrder(ctx context.Context, req *Derivative } // PlaceDerivativeConditionOrder places a conditional derivative order (SL/TP, Arbitrage, etc.). -func (c *Client) PlaceDerivativeConditionOrder(ctx context.Context, req *DerivativeConditionOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) PlaceDerivativeConditionOrder(ctx context.Context, req *DerivativeConditionOrderRequest) (*DerivativeResponse[*DerivativeConditionOrderPlaceResponse], error) { + var resp DerivativeResponse[*DerivativeConditionOrderPlaceResponse] err := c.post(ctx, "/khronos/v1/order/condition/place", req, &resp) if err != nil { return nil, err @@ -133,8 +133,8 @@ func (c *Client) PlaceDerivativeConditionOrder(ctx context.Context, req *Derivat } // ChangeDerivativeNormalOrder modifies an existing normal derivative order. -func (c *Client) ChangeDerivativeNormalOrder(ctx context.Context, req *DerivativeChangeOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) ChangeDerivativeNormalOrder(ctx context.Context, req *DerivativeChangeNormalOrderRequest) (*DerivativeResponse[string], error) { + var resp DerivativeResponse[string] err := c.post(ctx, "/khronos/v1/order/change", req, &resp) if err != nil { return nil, err @@ -143,8 +143,8 @@ func (c *Client) ChangeDerivativeNormalOrder(ctx context.Context, req *Derivativ } // ChangeDerivativeConditionOrder modifies an existing conditional derivative order. -func (c *Client) ChangeDerivativeConditionOrder(ctx context.Context, req *DerivativeChangeOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) ChangeDerivativeConditionOrder(ctx context.Context, req *DerivativeChangeConditionOrderRequest) (*DerivativeResponse[string], error) { + var resp DerivativeResponse[string] err := c.post(ctx, "/khronos/v2/order/condition/change", req, &resp) if err != nil { return nil, err @@ -153,8 +153,8 @@ func (c *Client) ChangeDerivativeConditionOrder(ctx context.Context, req *Deriva } // CancelDerivativeNormalOrder cancels a normal derivative order. -func (c *Client) CancelDerivativeNormalOrder(ctx context.Context, req *DerivativeCancelOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) CancelDerivativeNormalOrder(ctx context.Context, req *DerivativeCancelNormalOrderRequest) (*DerivativeResponse[*DerivativeCancelNormalOrderResponse], error) { + var resp DerivativeResponse[*DerivativeCancelNormalOrderResponse] err := c.post(ctx, "/khronos/v1/order/cancel", req, &resp) if err != nil { return nil, err @@ -163,8 +163,8 @@ func (c *Client) CancelDerivativeNormalOrder(ctx context.Context, req *Derivativ } // CancelDerivativeConditionOrder cancels a conditional derivative order. -func (c *Client) CancelDerivativeConditionOrder(ctx context.Context, req *DerivativeCancelOrderRequest) (*OrderIDResponse, error) { - var resp OrderIDResponse +func (c *Client) CancelDerivativeConditionOrder(ctx context.Context, req *DerivativeCancelConditionOrderRequest) (*DerivativeResponse[string], error) { + var resp DerivativeResponse[string] err := c.post(ctx, "/khronos/v1/order/condition/cancel", req, &resp) if err != nil { return nil, err @@ -173,7 +173,6 @@ func (c *Client) CancelDerivativeConditionOrder(ctx context.Context, req *Deriva } // GetDerivativeMarketInfo retrieves derivative contract pricing and information. -// tickers is a list of derivative contract symbols. func (c *Client) GetDerivativeMarketInfo(ctx context.Context, tickers []string) ([]DerivativeMarketInfo, error) { query := url.Values{} query.Set("tickers", strings.Join(tickers, ",")) diff --git a/derivative_test.go b/derivative_test.go new file mode 100644 index 0000000..7d24b4e --- /dev/null +++ b/derivative_test.go @@ -0,0 +1,216 @@ +package tcbs + +import ( + "context" + "net/http" + "testing" +) + +func TestGetDerivativeCashStatus(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/khronos/v1/account/status" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("accountId") != "ACC1" { + t.Errorf("unexpected accountId: %s", r.URL.Query().Get("accountId")) + } + writeJSON(t, w, DerivativeResponse[*TotalCashDerivativeResponse]{ + RC: "0", + Data: &TotalCashDerivativeResponse{NAV: 50000000, Cash: 10000000}, + }) + }) + + resp, err := client.GetDerivativeCashStatus(context.Background(), "ACC1", "SUB1", "0") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Data == nil || resp.Data.NAV != 50000000 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetDerivativeClosedPositions(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[[]AssetPositionCloseDerivativeResponse]{ + Data: []AssetPositionCloseDerivativeResponse{{Symbol: "VN30F2503", Side: "B"}}, + }) + }) + + resp, err := client.GetDerivativeClosedPositions(context.Background(), DerivativePositionCloseParams{ + AccountID: "ACC1", SubAccountID: "SUB1", PageNo: 1, PageSize: 10, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].Symbol != "VN30F2503" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetDerivativeOpenPositions(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[[]AssetPositionOpenDerivativeResponse]{ + Data: []AssetPositionOpenDerivativeResponse{{Symbol: "VN30F2503", Net: 5}}, + }) + }) + + resp, err := client.GetDerivativeOpenPositions(context.Background(), "ACC1", "SUB1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].Net != 5 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetDerivativeNormalOrders(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[[]DerivativeNormalOrderResponse]{ + Data: []DerivativeNormalOrderResponse{{OrderNo: "N001", Symbol: "VN30F2503"}}, + }) + }) + + resp, err := client.GetDerivativeNormalOrders(context.Background(), DerivativeOrdersParams{ + PageNo: 1, PageSize: 10, AccountID: "ACC1", Symbol: "ALL,ALL", Status: "0", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].OrderNo != "N001" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestPlaceDerivativeNormalOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + writeJSON(t, w, DerivativeResponse[*DerivativeNormalOrderPlaceResponse]{ + RC: "0", + Data: &DerivativeNormalOrderPlaceResponse{OrderNo: "N002", Symbol: "VN30F2503"}, + }) + }) + + resp, err := client.PlaceDerivativeNormalOrder(context.Background(), &DerivativeNormalOrderRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", Side: "B", + Symbol: "VN30F2503", Price: 1200, Volume: 1, OrderType: "LO", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Data == nil || resp.Data.OrderNo != "N002" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetDerivativeConditionOrders(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/khronos/v1/order/condition/detail" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, DerivativeResponse[[]DerivativeConditionOrderResponse]{ + Data: []DerivativeConditionOrderResponse{{OrderNo: "C001", Symbol: "VN30F2503"}}, + }) + }) + + resp, err := client.GetDerivativeConditionOrders(context.Background(), "ACC1", "SUB1", 1, 10) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 || resp.Data[0].OrderNo != "C001" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestPlaceDerivativeConditionOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[*DerivativeConditionOrderPlaceResponse]{ + Data: &DerivativeConditionOrderPlaceResponse{OrderNo: 101, Symbol: "VN30F2503"}, + }) + }) + + resp, err := client.PlaceDerivativeConditionOrder(context.Background(), &DerivativeConditionOrderRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", Side: "B", + Symbol: "VN30F2503", Price: 1200, Volume: 1, OrderType: "LO", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Data == nil || resp.Data.OrderNo != 101 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestChangeDerivativeNormalOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[string]{RC: "0", Data: "ok"}) + }) + + _, err := client.ChangeDerivativeNormalOrder(context.Background(), &DerivativeChangeNormalOrderRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", OrderNo: "N001", RefID: "ref1", NVol: 2, NPrice: 1300, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestChangeDerivativeConditionOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[string]{RC: "0", Data: "ok"}) + }) + + _, err := client.ChangeDerivativeConditionOrder(context.Background(), &DerivativeChangeConditionOrderRequest{ + AccountID: "ACC1", PKOrderNo: "PK001", Type: "SL", RefID: "ref1", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestCancelDerivativeNormalOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[*DerivativeCancelNormalOrderResponse]{ + Data: &DerivativeCancelNormalOrderResponse{OrderNo: "N001", Status: "cancelled"}, + }) + }) + + resp, err := client.CancelDerivativeNormalOrder(context.Background(), &DerivativeCancelNormalOrderRequest{ + AccountID: "ACC1", OrderNo: "N001", Cmd: "cancel", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Data == nil || resp.Data.Status != "cancelled" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestCancelDerivativeConditionOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, DerivativeResponse[string]{RC: "0", Data: "ok"}) + }) + + _, err := client.CancelDerivativeConditionOrder(context.Background(), &DerivativeCancelConditionOrderRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", OrderNo: "C001", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetDerivativeMarketInfo(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, []DerivativeMarketInfo{ + {Ticker: "VN30F2503", LastPrice: 1250.5, OpenInterest: 30000}, + }) + }) + + resp, err := client.GetDerivativeMarketInfo(context.Background(), []string{"VN30F2503"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].Ticker != "VN30F2503" { + t.Errorf("unexpected response: %+v", resp) + } +} diff --git a/example/main.go b/example/main.go index fce8fd5..312961b 100644 --- a/example/main.go +++ b/example/main.go @@ -22,7 +22,7 @@ func main() { if err != nil { log.Fatalf("Failed to get token: %v", err) } - fmt.Printf("Token obtained, expires in %d seconds\n", token.ExpiresIn) + fmt.Printf("Token obtained: %s\n", token.Token) // Or set token directly if you already have one: // client.SetToken("your-jwt-token") @@ -33,7 +33,10 @@ func main() { log.Fatalf("Failed to get account info: %v", err) } if account.BasicInfo != nil { - fmt.Printf("Account: %s - %s\n", account.BasicInfo.Code105C, account.BasicInfo.FullName) + fmt.Printf("Account: %s (status: %s)\n", account.BasicInfo.Code105C, account.BasicInfo.Status) + } + if account.PersonalInfo != nil { + fmt.Printf("Name: %s\n", account.PersonalInfo.FullName) } // 3. Get stock prices @@ -49,7 +52,7 @@ func main() { order, err := client.PlaceOrder(ctx, "0001170730", &tcbs.PlaceOrderRequest{ Symbol: "FPT", ExecType: "NB", // Buy - OrderQtty: 100, + Quantity: 100, Price: 120000, PriceType: "LO", // Limit order }) @@ -89,4 +92,18 @@ func main() { for _, d := range derivatives { fmt.Printf("%s: last=%.1f OI=%.0f\n", d.Ticker, d.LastPrice, d.OpenInterest) } + + // 9. Get supply/demand (15-minute) + sd, err := client.GetSupplyDemand(ctx, "FPT", "all") + if err != nil { + log.Fatalf("Failed to get supply/demand: %v", err) + } + fmt.Printf("Supply/demand data points: %d\n", len(sd.Data)) + + // 10. Get monthly supply/demand + sdm, err := client.GetSupplyDemandMonth(ctx, "FPT", "all") + if err != nil { + log.Fatalf("Failed to get monthly supply/demand: %v", err) + } + fmt.Printf("Monthly supply/demand data points: %d\n", len(sdm.Data)) } diff --git a/go.mod b/go.mod index a623904..58178c1 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/tiennm99/tcbs-api go 1.22.2 + +require nhooyr.io/websocket v1.8.17 // indirect diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9c3072b --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/models.go b/models.go deleted file mode 100644 index c0e2bdf..0000000 --- a/models.go +++ /dev/null @@ -1,737 +0,0 @@ -package tcbs - -// DerivativeResponse is a generic wrapper for derivative API responses. -type DerivativeResponse[T any] struct { - Cmd string `json:"cmd"` - RC string `json:"rc"` - RS string `json:"rs"` - OID string `json:"oID"` - Data T `json:"data"` -} - -// --- Account Models --- - -// AccountInformationResponse represents sub-account information. -type AccountInformationResponse struct { - BasicInfo *BasicInfo `json:"basicInfo,omitempty"` - PersonalInfo *PersonalInfo `json:"personalInfo,omitempty"` - BankSubAccounts []BankSubAccount `json:"bankSubAccounts,omitempty"` - BankAccounts []BankAccount `json:"bankAccounts,omitempty"` -} - -// BasicInfo holds basic account information. -type BasicInfo struct { - TcbsID string `json:"tcbsId"` - Code105C string `json:"code105C"` - Status string `json:"status"` - FullName string `json:"fullName"` - Email string `json:"email"` - Phone string `json:"phone"` - CustodyCD string `json:"custodycd"` - BranchCode string `json:"branchCode"` -} - -// PersonalInfo holds personal information. -type PersonalInfo struct { - IDNumber string `json:"idNumber"` - IDIssueDate string `json:"idIssueDate"` - IDPlace string `json:"idPlace"` - DateOfBirth string `json:"dateOfBirth"` - Gender string `json:"gender"` - Address string `json:"address"` -} - -// BankSubAccount represents a sub-account linked to a bank. -type BankSubAccount struct { - AccountNo string `json:"accountNo"` - AccountType string `json:"accountType"` - Status string `json:"status"` -} - -// BankAccount represents a linked bank account. -type BankAccount struct { - BankName string `json:"bankName"` - BankAccount string `json:"bankAccount"` - BankBranch string `json:"bankBranch"` - IsDefault string `json:"isDefault"` -} - -// --- Order Models --- - -// PlaceOrderRequest represents a stock order placement request. -type PlaceOrderRequest struct { - Symbol string `json:"symbol"` - ExecType string `json:"execType"` - OrderQtty float64 `json:"orderQtty"` - Price float64 `json:"price"` - PriceType string `json:"priceType"` - Via string `json:"via,omitempty"` -} - -// PlaceOrderResponse represents the response after placing a stock order. -type PlaceOrderResponse struct { - Object string `json:"object"` - OrderID string `json:"orderID"` - Status string `json:"status"` -} - -// UpdateOrderRequest represents a stock order update request. -type UpdateOrderRequest struct { - OrderQtty float64 `json:"orderQtty"` - Price float64 `json:"price"` - PriceType string `json:"priceType"` -} - -// UpdateOrderResponse represents the response after updating a stock order. -type UpdateOrderResponse struct { - Object string `json:"object"` - OrderID string `json:"orderID"` - Status string `json:"status"` -} - -// CancelOrderRequest represents a stock order cancellation request. -type CancelOrderRequest struct { - OrderID string `json:"orderID"` -} - -// CancelOrderResponse represents the response after cancelling a stock order. -type CancelOrderResponse struct { - Object string `json:"object"` - OrderID string `json:"orderID"` - Status string `json:"status"` -} - -// OrderSearchResponse represents the order book response. -type OrderSearchResponse struct { - Object string `json:"object"` - PageSize int `json:"pageSize"` - PageIndex string `json:"pageIndex"` - TotalCount int64 `json:"totalCount"` - Data []OrderInfo `json:"data"` -} - -// OrderInfo represents a single order in the order book. -type OrderInfo struct { - Object string `json:"object"` - AccountNo string `json:"accountNo"` - OrderID string `json:"orderID"` - ExecType string `json:"execType"` - OrderQtty float64 `json:"orderQtty"` - ExecQtty float64 `json:"execQtty"` - Symbol string `json:"symbol"` - PriceType string `json:"priceType"` - TxTime string `json:"txtime"` - TxDate string `json:"txdate"` - ExpDate string `json:"expDate"` - TimeType string `json:"timeType"` - OrStatus string `json:"orStatus"` - FeeAcr float64 `json:"feeAcr"` - LimitPrice float64 `json:"limitPrice"` - CancelQtty float64 `json:"cancelQtty"` - RemainQtty float64 `json:"remainQtty"` - Via string `json:"via"` - QuotePrice float64 `json:"quotePrice"` - MatchPrice float64 `json:"matchPrice"` - TradePlace string `json:"tradePlace"` - MatchType string `json:"matchType"` - IsDisposal string `json:"isDisposal"` - IsCancel string `json:"isCancel"` - IsAmend string `json:"isAmend"` - UserName string `json:"userName"` - OrsOrderID string `json:"orsOrderID"` - SecType string `json:"sectype"` - IsFOOrder string `json:"isFOOrder"` - OdTimeStamp string `json:"odTimeStamp"` - MatchAmount float64 `json:"matchAmount"` - BRatio float64 `json:"bRatio"` - TaxSellAmt float64 `json:"taxSellAmout"` -} - -// CommandMatchInformationResponse represents matching details. -type CommandMatchInformationResponse struct { - Object string `json:"object"` - PageSize int `json:"pageSize"` - PageIndex string `json:"pageIndex"` - TotalCount int64 `json:"totalCount"` - Data []CommandMatchInformation `json:"data"` -} - -// CommandMatchInformation represents a single matching detail. -type CommandMatchInformation struct { - Object string `json:"object"` - OrderID string `json:"orderID"` - AccountNo string `json:"accountNo"` - Symbol string `json:"symbol"` - ExecType string `json:"execType"` - MatchQtty float64 `json:"matchQtty"` - MatchPrice float64 `json:"matchPrice"` - MatchDate string `json:"matchDate"` - MatchTime string `json:"matchTime"` - PriceType string `json:"priceType"` -} - -// --- Purchasing Power Models --- - -// PurchasingPowerResponse represents purchasing power information. -type PurchasingPowerResponse struct { - AccountNo string `json:"accountNo"` - CustodyID string `json:"custodyID"` - Symbol string `json:"symbol"` - Price float64 `json:"price"` - PP0 float64 `json:"pp0"` - PPSE float64 `json:"ppse"` - PPSERef float64 `json:"ppseref"` - MaxBuyQuantity float64 `json:"maxBuyQuantity"` - RealMaxBuyQty float64 `json:"realMaxBuyQuantity"` - MinBuyQuantity float64 `json:"minBuyQuantity"` - MarginRatioLoan float64 `json:"marginRatioLoan"` - MarginPriceLoan float64 `json:"marginPriceLoan"` - RateBrkS string `json:"rateBrkS"` - RateBrkB string `json:"rateBrkB"` -} - -// MarginQuotaResponse represents margin quota information. -type MarginQuotaResponse struct { - CustodyID string `json:"custodyID"` - AccountNo string `json:"accountNo"` - AFType string `json:"aftype"` - VSDStatus string `json:"vsdStatus"` - AccountStatus string `json:"accountStatus"` - MarginLimit float64 `json:"marginLimit"` - IsIA string `json:"isIA"` - BankName string `json:"bankName"` - BankAccount string `json:"bankAccount"` - AccountType string `json:"accountType"` -} - -// MarginAccountInfoResponse represents margin account details. -type MarginAccountInfoResponse struct { - AccountNo string `json:"accountNo"` - RiskPolicy *RiskPolicy `json:"riskPolicy,omitempty"` - RTT float64 `json:"rtt"` - Outstanding float64 `json:"outstanding"` - AccruedInterest float64 `json:"accruedInterest"` - DueAmount float64 `json:"dueAmount"` - OverdueAmount float64 `json:"overdueAmount"` - RiskStatus *RiskStatus `json:"riskStatus,omitempty"` - TotalFeeDebt float64 `json:"totalFeeDebt"` -} - -// RiskPolicy represents margin risk policy parameters. -type RiskPolicy struct { - MaintenanceMargin float64 `json:"maintenanceMargin"` - InitialMargin float64 `json:"initialMargin"` - LiquidationMargin float64 `json:"liquidationMargin"` -} - -// RiskStatus represents RTT status. -type RiskStatus struct { - Code string `json:"code"` - Description string `json:"description"` -} - -// --- Asset Models --- - -// SeInfoDTO represents stock asset information. -type SeInfoDTO struct { - Object string `json:"object"` - AccountNo string `json:"accountNo"` - CustodyID string `json:"custodyID"` - FullName string `json:"fullName"` - Stock []StockHoldingInfo `json:"stock"` -} - -// StockHoldingInfo represents a single stock holding. -type StockHoldingInfo struct { - Symbol string `json:"symbol"` - SecType string `json:"secType"` - SecTypeName string `json:"secTypeName"` - AvailableTrading float64 `json:"availableTrading"` - Mortgaged float64 `json:"mortgaged"` - T0 float64 `json:"t0"` - T1 float64 `json:"t1"` - T2 float64 `json:"t2"` - Blocked float64 `json:"blocked"` - SecuredQuantity float64 `json:"securedQuantity"` - SellRemain float64 `json:"sellRemain"` - ExercisedCA float64 `json:"exercisedCA"` - UnexercisedCA float64 `json:"unexercisedCA"` - StockDividend float64 `json:"stockDividend"` - CashDividend float64 `json:"cashDividend"` - WaitForTrade float64 `json:"waitForTrade"` - WaitForTransfer float64 `json:"waitForTransfer"` - WaitForWithdraw float64 `json:"waitForWithdraw"` - CurrentPrice float64 `json:"currentPrice"` - CostPrice float64 `json:"costPrice"` - SellExec float64 `json:"sellExec"` - OnHold float64 `json:"onHold"` - TotalQtty float64 `json:"totalQtty"` - Settlement float64 `json:"settlement"` -} - -// CashInvestmentResponse represents cash balance information. -type CashInvestmentResponse struct { - Object string `json:"object"` - TotalCount int `json:"totalCount"` - PageSize int `json:"pageSize"` - PageIndex int `json:"pageIndex"` - Data []CashInvestment `json:"data"` -} - -// CashInvestment represents a single cash investment record. -type CashInvestment struct { - Object string `json:"object"` - IAInfos []IAInfo `json:"iaInfos"` - PP0ForBF float64 `json:"pp0forBF"` - BankAvlBalanceBF float64 `json:"bankAvlBalanceBF"` - BodBalance float64 `json:"bodBalance"` - CashBalance float64 `json:"cashBalance"` - AccountNo string `json:"accountNo"` - CustodyID string `json:"custodyID"` - FullName string `json:"fullName"` - Balance float64 `json:"balance"` - AvlAdvanceAmount float64 `json:"avlAdvanceAmount"` - BuyingAmount float64 `json:"buyingAmount"` - BlockAmount float64 `json:"blockAmount"` - CashDividend float64 `json:"cashDevident"` - BankAvlBalance float64 `json:"bankAvlBalance"` - BankBlockAmount float64 `json:"bankBlockAmount"` - AvlWithdraw float64 `json:"avlWithdraw"` - PP0 float64 `json:"pp0"` - SecureAmtPO float64 `json:"secureAmtPO"` - BondBlockAmount float64 `json:"bondBlockAmount"` - MBlockAmount float64 `json:"mBlockAmount"` - FundBlockAmount float64 `json:"fundBlockAmount"` - AvalBondBlock float64 `json:"avalBondBlockAmount"` - DepoFee float64 `json:"depoFee"` - BCashDividend float64 `json:"bCashDividend"` - SCashDividend float64 `json:"sCashDividend"` - DSecured float64 `json:"dsecured"` - AdUsed float64 `json:"adused"` - MrUsed float64 `json:"mrused"` -} - -// IAInfo represents instant account (IA) source information. -type IAInfo struct { - Partner string `json:"partner"` - Available float64 `json:"available"` - Hold float64 `json:"hold"` -} - -// TransHistCashStatementsResponse represents cash statement history. -type TransHistCashStatementsResponse struct { - Response *TransHistCashStatementsData `json:"response"` -} - -// TransHistCashStatementsData holds the paged data of cash statements. -type TransHistCashStatementsData struct { - PageIndex int `json:"pageIndex"` - PageSize int `json:"pageSize"` - TotalCreditAmt int64 `json:"totalCreditAmount"` - TotalDebitAmt int64 `json:"totalDebitAmount"` - TotalCount int `json:"totalCount"` - Data []CashStatementEntry `json:"data"` -} - -// CashStatementEntry represents a single cash statement entry. -type CashStatementEntry struct { - CustodyID string `json:"custodyID"` - TransactionCode string `json:"transactionCode"` - DebitAmount float64 `json:"debitAmount"` - TransactionName string `json:"transactionName"` - Descriptions string `json:"descriptions"` - BusinessDate string `json:"businessDate"` - TransactionNum string `json:"transactionNum"` - AccountNo string `json:"accountNo"` - TransactionDate string `json:"transationDate"` - CreditAmount float64 `json:"creditAmount"` -} - -// MarginInfoResponse represents debt inquiry response. -type MarginInfoResponse struct { - Response *MarginInfoData `json:"response"` -} - -// MarginInfoData holds paged margin info data. -type MarginInfoData struct { - TotalRow int `json:"totalRow"` - TotalPage int `json:"totalPage"` - Data []MarginInfoItem `json:"data"` -} - -// MarginInfoItem represents a single margin/debt record. -type MarginInfoItem struct { - RemainingInterestFee float64 `json:"remainingInterestFee"` - ReleasedDay int `json:"releasedDay"` - PrintAmount float64 `json:"printAmount"` - PaidInterestFee float64 `json:"paidInterestFee"` - IntAmount float64 `json:"intAmount"` - ReleaseDate string `json:"releaseDate"` - Rate2 float64 `json:"rate2"` - OverDueDate string `json:"overDueDate"` - PaidFee float64 `json:"paidFee"` - ReleasedAmount float64 `json:"releasedAmount"` - RemainingFee float64 `json:"remainingFee"` - IntPaid float64 `json:"intPaid"` - PrinPaid float64 `json:"prinPaid"` -} - -// SupplementaryLoanPackageResponse represents supplementary loan package info. -type SupplementaryLoanPackageResponse struct { - MarginSureViews []MarginSureView `json:"marginSureViews"` - TPlus *TPlusData `json:"tplus,omitempty"` -} - -// MarginSureView represents a margin-sure insurance package. -type MarginSureView struct { - ID float64 `json:"id"` - Name string `json:"name"` - Code string `json:"code"` - SubscriptionFee float64 `json:"subscriptionFee"` - Status string `json:"status"` - Proposals []MarginSureProposal `json:"proposals"` - Default bool `json:"default"` -} - -// MarginSureProposal represents a proposal within a margin-sure package. -type MarginSureProposal struct { - ID float64 `json:"id"` - MarginInsuranceID float64 `json:"marginInsuranceId"` - InterestAdjustmentValue float64 `json:"interestAdjustmentValue"` - InterestPercentThreshold float64 `json:"interestPercentThreshold"` - ThresholdType string `json:"thresholdType"` -} - -// TPlusData contains T+ loan package info. -type TPlusData struct { - Data []TPlusPackage `json:"data"` -} - -// TPlusPackage represents a single T+ loan package. -type TPlusPackage struct { - FirstRate float64 `json:"firstRate"` - ID float64 `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - UndueInterestType string `json:"undueInterestType"` - UndueLadderValue []TPlusLadder `json:"undueLadderValue"` - OverdueInterest float64 `json:"overdueInterest"` - ExtensionInterest float64 `json:"extensionInterest"` - ExtensionInterestBeforeInterestSettlement float64 `json:"extensionInterestBeforeInterestSettlement"` - InterestCalculationBasis float64 `json:"interestCalculationBasis"` - UndueFee float64 `json:"undueFee"` - OverdueFee float64 `json:"overdueFee"` - ExtensionFee float64 `json:"extensionFee"` - DebtCollectionFee float64 `json:"debtCollectionFee"` - Description string `json:"description"` - ValidFrom string `json:"validFrom"` -} - -// TPlusLadder represents a ladder interest rate tier. -type TPlusLadder struct { - ID float64 `json:"id"` - Rate float64 `json:"rate"` - StartDate float64 `json:"startDate"` - DueDate float64 `json:"dueDate"` -} - -// LoanResponse represents the loan list response. -type LoanResponse struct { - Size int `json:"size"` - Content []LoanItem `json:"content"` -} - -// LoanItem represents a single loan. -type LoanItem struct { - OpeningDate string `json:"openingDate"` - DueDate string `json:"dueDate"` - RenewTime int `json:"renewTime"` - MaxRenewTime int `json:"maxRenewTime"` - IsRenewable bool `json:"isRenewable"` - ReasonList []string `json:"reasonList"` - Symbol string `json:"symbol"` - ID float64 `json:"id"` - AccountNo string `json:"accountNo"` - Principal float64 `json:"principal"` - RemainingPrincipal float64 `json:"remainingPrincipal"` - Interest float64 `json:"interest"` - Rate float64 `json:"rate"` - Status string `json:"status"` - LoanDays int `json:"loanDays"` - MrxLoanID float64 `json:"mrxLoanId"` - Fee float64 `json:"fee"` - UndueLoanFee float64 `json:"undueLoanFee"` - PricingPolicyType string `json:"pricingPolicyType"` -} - -// --- Money Management Models --- - -// MoneyTransferRequest represents an internal money transfer request. -type MoneyTransferRequest struct { - SenderAccount string `json:"senderAccount"` - ReceiverAccount string `json:"receiverAccount"` - Amount float64 `json:"amount"` -} - -// MoneyTransferResponse represents the transfer response. -type MoneyTransferResponse struct { - Status string `json:"status"` - Message string `json:"message"` -} - -// MarginDepositWithdrawRequest represents a margin deposit or withdrawal request. -type MarginDepositWithdrawRequest struct { - AccountID string `json:"accountId"` - SubAccountID string `json:"subAccountId"` - Amount float64 `json:"amount"` -} - -// MarginDepositWithdrawResponse represents the deposit/withdraw response. -type MarginDepositWithdrawResponse struct { - Cmd string `json:"cmd"` - RC string `json:"rc"` - RS string `json:"rs"` - OID string `json:"oID"` -} - -// --- Market Information Models --- - -// MarketStockInfo represents stock ticker information. -type MarketStockInfo struct { - Ticker string `json:"ticker"` - Exchange string `json:"exchange"` - RefPrice float64 `json:"refPrice"` - CeilingPrice float64 `json:"ceilingPrice"` - FloorPrice float64 `json:"floorPrice"` - HighPrice float64 `json:"highPrice"` - LowPrice float64 `json:"lowPrice"` - MatchPrice float64 `json:"matchPrice"` - MatchQtty float64 `json:"matchQtty"` - TotalMatchQtty float64 `json:"totalMatchQtty"` - TotalMatchValue float64 `json:"totalMatchValue"` - Best1BidPrice float64 `json:"best1BidPrice"` - Best1BidQtty float64 `json:"best1BidQtty"` - Best2BidPrice float64 `json:"best2BidPrice"` - Best2BidQtty float64 `json:"best2BidQtty"` - Best3BidPrice float64 `json:"best3BidPrice"` - Best3BidQtty float64 `json:"best3BidQtty"` - Best1OfferPrice float64 `json:"best1OfferPrice"` - Best1OfferQtty float64 `json:"best1OfferQtty"` - Best2OfferPrice float64 `json:"best2OfferPrice"` - Best2OfferQtty float64 `json:"best2OfferQtty"` - Best3OfferPrice float64 `json:"best3OfferPrice"` - Best3OfferQtty float64 `json:"best3OfferQtty"` -} - -// ForeignRoomInfo represents foreign investor room information. -type ForeignRoomInfo struct { - Ticker string `json:"ticker"` - TotalRoom float64 `json:"totalRoom"` - CurrentRoom float64 `json:"currentRoom"` - BuyVol float64 `json:"buyVol"` - SellVol float64 `json:"sellVol"` -} - -// PutThroughInfo represents put-through agreement information. -type PutThroughInfo struct { - Ticker string `json:"ticker"` - Vol float64 `json:"vol"` - Val float64 `json:"val"` -} - -// IntradayHistoryResponse represents intraday price matching history. -type IntradayHistoryResponse struct { - Ticker string `json:"ticker"` - Page int `json:"page"` - Size int `json:"size"` - Data []IntradayHistoryItem `json:"data"` -} - -// IntradayHistoryItem represents a single intraday trade. -type IntradayHistoryItem struct { - P float64 `json:"p"` - V float64 `json:"v"` - CP float64 `json:"cp"` - RCP float64 `json:"rcp"` - A string `json:"a"` - BA string `json:"ba"` - SA string `json:"sa"` - HL string `json:"hl"` - PCP float64 `json:"pcp"` - T string `json:"t"` -} - -// SupplyDemandResponse represents supply and demand data. -type SupplyDemandResponse struct { - Ticker string `json:"ticker"` - Data []SupplyDemandItem `json:"data"` -} - -// SupplyDemandItem represents a single supply/demand data point. -type SupplyDemandItem struct { - BU float64 `json:"bu"` - BMS float64 `json:"bms"` - BUP float64 `json:"bup"` - SD float64 `json:"sd"` - SMS float64 `json:"sms"` - SDP float64 `json:"sdp"` - BSR float64 `json:"bsr"` - T string `json:"t"` - S int64 `json:"s"` -} - -// --- Derivative Models --- - -// TotalCashDerivativeResponse represents derivative cash/margin overview. -type TotalCashDerivativeResponse struct { - Fee float64 `json:"fee"` - Tax float64 `json:"tax"` - Others float64 `json:"others"` - CashWithdraw float64 `json:"cashWithdraw"` - TienBoSung float64 `json:"tienbosung"` - CashAvailWithdraw float64 `json:"cashavaiwithdraw"` - Assets float64 `json:"assets"` - NAV float64 `json:"nav"` - CashOut float64 `json:"cashOut"` - VSDDeposit float64 `json:"vsdDeposit"` - IM float64 `json:"im"` - Cash float64 `json:"cash"` - PL float64 `json:"pl"` - VM float64 `json:"vm"` - EE float64 `json:"ee"` -} - -// AssetPositionCloseDerivativeResponse represents a closed derivative position. -type AssetPositionCloseDerivativeResponse struct { - Symbol string `json:"symbol"` - Side string `json:"side"` - OpenPrice float64 `json:"openPrice"` - ClosePrice float64 `json:"closePrice"` - ClosePosition any `json:"closePosition"` - Fee float64 `json:"fee"` - Tax float64 `json:"tax"` - CloseVM float64 `json:"closeVM"` - Unrealize float64 `json:"unrealize"` - ClosePC float64 `json:"closePC"` - Time string `json:"time"` -} - -// AssetPositionOpenDerivativeResponse represents an open derivative position. -type AssetPositionOpenDerivativeResponse struct { - Symbol string `json:"symbol"` - IM string `json:"im"` - Deliver string `json:"deliver"` - Receive string `json:"receive"` - Net float64 `json:"net"` - Side string `json:"side"` - Account string `json:"account"` - WASP float64 `json:"wasp"` - WAPB float64 `json:"wapb"` - LastPrice float64 `json:"lastPrice"` - IMValue float64 `json:"imValue"` - VMValue float64 `json:"vmValue"` - MRValue float64 `json:"mrValue"` - DueDate string `json:"duedate"` - NetOffVol float64 `json:"netoffvol"` - AvgRemain float64 `json:"avg_remain"` - VMRemain float64 `json:"vm_remain"` - PCRemain string `json:"pc_remain"` - StopLoss string `json:"stoploss"` - TakeProfit string `json:"takeprofit"` -} - -// DerivativeNormalOrderResponse represents a normal derivative order. -type DerivativeNormalOrderResponse struct { - OrderNo string `json:"orderNo"` - PKOrderNo string `json:"pk_orderNo"` - RefID string `json:"refId"` - OrderTime string `json:"orderTime"` - AccountCode string `json:"accountCode"` - Side string `json:"side"` - Symbol string `json:"symbol"` - Volume string `json:"volume"` - ShowPrice string `json:"showPrice"` - MatchVolume string `json:"matchVolume"` - Status string `json:"status"` - OrderStatus string `json:"orderStatus"` - Channel string `json:"channel"` - Group string `json:"group"` -} - -// DerivativeConditionOrderResponse represents a conditional derivative order. -type DerivativeConditionOrderResponse struct { - OrderNo string `json:"orderNo"` - RefID string `json:"refId"` - OrderTime string `json:"orderTime"` - AccountCode string `json:"accountCode"` - Side string `json:"side"` - Symbol string `json:"symbol"` - Volume string `json:"volume"` - Price string `json:"price"` - Status string `json:"status"` - OrderType string `json:"orderType"` -} - -// DerivativeNormalOrderRequest represents a request to place a normal derivative order. -type DerivativeNormalOrderRequest struct { - AccountID string `json:"accountId"` - SubAccountID string `json:"subAccountId"` - Symbol string `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"orderType"` - Volume int `json:"volume"` - Price string `json:"price"` -} - -// DerivativeConditionOrderRequest represents a request to place a conditional derivative order. -type DerivativeConditionOrderRequest struct { - AccountID string `json:"accountId"` - SubAccountID string `json:"subAccountId"` - Symbol string `json:"symbol"` - Side string `json:"side"` - OrderType string `json:"orderType"` - Volume int `json:"volume"` - Price string `json:"price"` - StopPrice string `json:"stopPrice,omitempty"` - TakeProfit string `json:"takeProfit,omitempty"` - StopLoss string `json:"stopLoss,omitempty"` -} - -// DerivativeChangeOrderRequest represents a request to modify a derivative order. -type DerivativeChangeOrderRequest struct { - AccountID string `json:"accountId"` - SubAccountID string `json:"subAccountId"` - RefID string `json:"refId"` - Volume int `json:"volume"` - Price string `json:"price"` -} - -// DerivativeCancelOrderRequest represents a request to cancel a derivative order. -type DerivativeCancelOrderRequest struct { - AccountID string `json:"accountId"` - SubAccountID string `json:"subAccountId"` - RefID string `json:"refId"` -} - -// DerivativeMarketInfo represents derivative contract pricing and information. -type DerivativeMarketInfo struct { - Ticker string `json:"ticker"` - RefPrice float64 `json:"refPrice"` - CeilingPrice float64 `json:"ceilingPrice"` - FloorPrice float64 `json:"floorPrice"` - HighPrice float64 `json:"highPrice"` - LowPrice float64 `json:"lowPrice"` - LastPrice float64 `json:"lastPrice"` - LastVol float64 `json:"lastVol"` - TotalVol float64 `json:"totalVol"` - OpenInterest float64 `json:"openInterest"` -} - -// OrderIDResponse represents a generic order ID response from derivative endpoints. -type OrderIDResponse struct { - Cmd string `json:"cmd"` - RC string `json:"rc"` - RS string `json:"rs"` - OID string `json:"oID"` - Data string `json:"data"` -} diff --git a/models_account.go b/models_account.go new file mode 100644 index 0000000..e0d1bdd --- /dev/null +++ b/models_account.go @@ -0,0 +1,80 @@ +package tcbs + +// AccountInformationResponse represents sub-account information. +type AccountInformationResponse struct { + BasicInfo *BasicInfo `json:"basicInfo,omitempty"` + PersonalInfo *PersonalInfo `json:"personalInfo,omitempty"` + BankAccounts []BankAccount `json:"bankAccounts,omitempty"` + BankSubAccounts []BankSubAccount `json:"bankSubAccounts,omitempty"` +} + +// BasicInfo holds basic account information. +type BasicInfo struct { + TcbsID string `json:"tcbsId"` + Code105C string `json:"code105C"` + Status string `json:"status"` + Type string `json:"type"` + Depository bool `json:"depository"` +} + +// PersonalInfo holds personal information. +type PersonalInfo struct { + FullName string `json:"fullName"` + FullNameNoAccent string `json:"fullNameNoAccent"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Email string `json:"email"` + PhoneNumber string `json:"phoneNumber"` + Gender string `json:"gender"` + Birthday string `json:"birthday"` + ContactAddress string `json:"contactAddress"` + PermanentAddress string `json:"permanentAddress"` + Nationality string `json:"nationality"` + NationalityName string `json:"nationalityName"` + TaxIDNumber string `json:"taxIdNumber"` + Acronym string `json:"acronym"` + CreatedDate string `json:"createdDate"` + UpdatedDate string `json:"updatedDate"` + FlowOpenAccount string `json:"flowOpenAccount"` + AvatarURL string `json:"avatarUrl"` + BusinessType string `json:"businessType"` + PPBusinessType string `json:"ppBusinessType"` + PPBusinessField string `json:"ppBusinessField"` + PPBusinessTypeName string `json:"ppBusinessTypeName"` + PPBusinessFieldName string `json:"ppBusinessFieldName"` + IdentityCard *IdentityCard `json:"identityCard,omitempty"` +} + +// IdentityCard holds identity document information. +type IdentityCard struct { + Object string `json:"object"` + IDNumber string `json:"idNumner"` // note: typo in spec + IDPlace string `json:"idPlace"` + IDDate string `json:"idDate"` + ExpireDate string `json:"expireDate"` + IDType string `json:"idType"` +} + +// BankSubAccount represents a sub-account linked to a bank. +type BankSubAccount struct { + AccountNo string `json:"accountNo"` + AccountName string `json:"accountName"` + AccountType string `json:"accountType"` + AccountTypeName string `json:"accountTypeName"` + Status string `json:"status"` + IsDefault string `json:"isDefault"` +} + +// BankAccount represents a linked bank account. +type BankAccount struct { + AccountNo string `json:"accountNo"` + AccountName string `json:"accountName"` + AccountNameNoAccent string `json:"accountNameNoAccent"` + BankCode string `json:"bankCode"` + BankName string `json:"bankName"` + BranchCode string `json:"branchCode"` + BankType string `json:"bankType"` + BankSys string `json:"bankSys"` + Authorized string `json:"authorized"` + BankAccountType string `json:"bankAccountType"` +} diff --git a/models_asset.go b/models_asset.go new file mode 100644 index 0000000..087d71d --- /dev/null +++ b/models_asset.go @@ -0,0 +1,245 @@ +package tcbs + +// --- Stock Assets --- + +// SeInfoDTO represents stock asset information. +type SeInfoDTO struct { + Object string `json:"object"` + AccountNo string `json:"accountNo"` + CustodyID string `json:"custodyID"` + FullName string `json:"fullName"` + Stock []StockHoldingInfo `json:"stock"` +} + +// StockHoldingInfo represents a single stock holding. +type StockHoldingInfo struct { + Symbol string `json:"symbol"` + SecType string `json:"secType"` + SecTypeName string `json:"secTypeName"` + AvailableTrading float64 `json:"availableTrading"` + Mortgaged float64 `json:"mortgaged"` + T0 float64 `json:"t0"` + T1 float64 `json:"t1"` + T2 float64 `json:"t2"` + Blocked float64 `json:"blocked"` + SecuredQuantity float64 `json:"securedQuantity"` + SellRemain float64 `json:"sellRemain"` + ExercisedCA float64 `json:"exercisedCA"` + UnexercisedCA float64 `json:"unexercisedCA"` + StockDividend float64 `json:"stockDividend"` + CashDividend float64 `json:"cashDividend"` + WaitForTrade float64 `json:"waitForTrade"` + WaitForTransfer float64 `json:"waitForTransfer"` + WaitForWithdraw float64 `json:"waitForWithdraw"` + CurrentPrice float64 `json:"currentPrice"` + CostPrice float64 `json:"costPrice"` + SellExec float64 `json:"sellExec"` + OnHold float64 `json:"onHold"` + TotalQtty float64 `json:"totalQtty"` + Settlement float64 `json:"settlement"` +} + +// --- Cash Balance --- + +// CashInvestmentResponse represents cash balance information. +type CashInvestmentResponse struct { + Object string `json:"object"` + TotalCount int `json:"totalCount"` + PageSize int `json:"pageSize"` + PageIndex int `json:"pageIndex"` + Data []CashInvestment `json:"data"` +} + +// CashInvestment represents a single cash investment record. +type CashInvestment struct { + Object string `json:"object"` + IAInfos []IAInfo `json:"iaInfos"` + PP0ForBF float64 `json:"pp0forBF"` + BankAvlBalanceBF float64 `json:"bankAvlBalanceBF"` + BodBalance float64 `json:"bodBalance"` + CashBalance float64 `json:"cashBalance"` + AccountNo string `json:"accountNo"` + CustodyID string `json:"custodyID"` + FullName string `json:"fullName"` + Balance float64 `json:"balance"` + AvlAdvanceAmount float64 `json:"avlAdvanceAmount"` + BuyingAmount float64 `json:"buyingAmount"` + BlockAmount float64 `json:"blockAmount"` + CashDividend float64 `json:"cashDevident"` // note: typo in spec + BankAvlBalance float64 `json:"bankAvlBalance"` + BankBlockAmount float64 `json:"bankBlockAmount"` + AvlWithdraw float64 `json:"avlWithdraw"` + PP0 float64 `json:"pp0"` + SecureAmtPO float64 `json:"secureAmtPO"` + BondBlockAmount float64 `json:"bondBlockAmount"` + MBlockAmount float64 `json:"mBlockAmount"` + FundBlockAmount float64 `json:"fundBlockAmount"` + AvalBondBlock float64 `json:"avalBondBlockAmount"` + DepoFee float64 `json:"depoFee"` + BCashDividend float64 `json:"bCashDividend"` + SCashDividend float64 `json:"sCashDividend"` + DSecured float64 `json:"dsecured"` + AdUsed float64 `json:"adused"` + MrUsed float64 `json:"mrused"` +} + +// IAInfo represents instant account (IA) source information. +type IAInfo struct { + Partner string `json:"partner"` + Available float64 `json:"available"` + Hold float64 `json:"hold"` +} + +// --- Cash Statements --- + +// TransHistCashStatementsResponse represents cash statement history. +type TransHistCashStatementsResponse struct { + Response *TransHistCashStatementsData `json:"response"` +} + +// TransHistCashStatementsData holds the paged data of cash statements. +type TransHistCashStatementsData struct { + PageIndex int `json:"pageIndex"` + PageSize int `json:"pageSize"` + TotalCreditAmt int64 `json:"totalCreditAmount"` + TotalDebitAmt int64 `json:"totalDebitAmount"` + TotalCount int `json:"totalCount"` + Data []CashStatementEntry `json:"data"` +} + +// CashStatementEntry represents a single cash statement entry. +type CashStatementEntry struct { + CustodyID string `json:"custodyID"` + TransactionCode string `json:"transactionCode"` + DebitAmount float64 `json:"debitAmount"` + TransactionName string `json:"transactionName"` + Descriptions string `json:"descriptions"` + BusinessDate string `json:"businessDate"` + TransactionNum string `json:"transactionNum"` + AccountNo string `json:"accountNo"` + TransactionDate string `json:"transationDate"` // note: typo in spec + CreditAmount float64 `json:"creditAmount"` +} + +// --- Margin Info --- + +// MarginInfoResponse represents debt inquiry response. +type MarginInfoResponse struct { + Response *MarginInfoData `json:"response"` +} + +// MarginInfoData holds paged margin info data. +type MarginInfoData struct { + TotalRow int `json:"totalRow"` + TotalPage int `json:"totalPage"` + Data []MarginInfoItem `json:"data"` +} + +// MarginInfoItem represents a single margin/debt record. +type MarginInfoItem struct { + RemainingInterestFee float64 `json:"remainingInterestFee"` + ReleasedDay int `json:"releasedDay"` + PrintAmount float64 `json:"printAmount"` + PaidInterestFee float64 `json:"paidInterestFee"` + IntAmount float64 `json:"intAmount"` + ReleaseDate string `json:"releaseDate"` + Rate2 float64 `json:"rate2"` + OverDueDate string `json:"overDueDate"` + PaidFee float64 `json:"paidFee"` + ReleasedAmount float64 `json:"releasedAmount"` + RemainingFee float64 `json:"remainingFee"` + IntPaid float64 `json:"intPaid"` + PrinPaid float64 `json:"prinPaid"` +} + +// --- Supplementary Loan Package --- + +// SupplementaryLoanPackageResponse represents supplementary loan package info. +type SupplementaryLoanPackageResponse struct { + MarginSureViews []MarginSureView `json:"marginSureViews"` + TPlus *TPlusData `json:"tplus,omitempty"` +} + +// MarginSureView represents a margin-sure insurance package. +type MarginSureView struct { + ID float64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + SubscriptionFee float64 `json:"subscriptionFee"` + Status string `json:"status"` + Proposals []MarginSureProposal `json:"proposals"` + Default bool `json:"default"` +} + +// MarginSureProposal represents a proposal within a margin-sure package. +type MarginSureProposal struct { + ID float64 `json:"id"` + MarginInsuranceID float64 `json:"marginInsuranceId"` + InterestAdjustmentValue float64 `json:"interestAdjustmentValue"` + InterestPercentThreshold float64 `json:"interestPercentThreshold"` + ThresholdType string `json:"thresholdType"` +} + +// TPlusData contains T+ loan package info. +type TPlusData struct { + Data []TPlusPackage `json:"data"` +} + +// TPlusPackage represents a single T+ loan package. +type TPlusPackage struct { + FirstRate float64 `json:"firstRate"` + ID float64 `json:"id"` + Name string `json:"name"` + Status string `json:"status"` + UndueInterestType string `json:"undueInterestType"` + UndueLadderValue []TPlusLadder `json:"undueLadderValue"` + OverdueInterest float64 `json:"overdueInterest"` + ExtensionInterest float64 `json:"extensionInterest"` + ExtensionInterestBeforeInterestSettlement float64 `json:"extensionInterestBeforeInterestSettlement"` + InterestCalculationBasis float64 `json:"interestCalculationBasis"` + UndueFee float64 `json:"undueFee"` + OverdueFee float64 `json:"overdueFee"` + ExtensionFee float64 `json:"extensionFee"` + DebtCollectionFee float64 `json:"debtCollectionFee"` + Description string `json:"description"` + ValidFrom string `json:"validFrom"` +} + +// TPlusLadder represents a ladder interest rate tier. +type TPlusLadder struct { + ID float64 `json:"id"` + Rate float64 `json:"rate"` + StartDate float64 `json:"startDate"` + DueDate float64 `json:"dueDate"` +} + +// --- Loans --- + +// LoanResponse represents the loan list response. +type LoanResponse struct { + Size int `json:"size"` + Content []LoanItem `json:"content"` +} + +// LoanItem represents a single loan. +type LoanItem struct { + OpeningDate string `json:"openingDate"` + DueDate string `json:"dueDate"` + RenewTime int `json:"renewTime"` + MaxRenewTime int `json:"maxRenewTime"` + IsRenewable bool `json:"isRenewable"` + ReasonList []string `json:"reasonList"` + Symbol string `json:"symbol"` + ID float64 `json:"id"` + AccountNo string `json:"accountNo"` + Principal float64 `json:"principal"` + RemainingPrincipal float64 `json:"remainingPrincipal"` + Interest float64 `json:"interest"` + Rate float64 `json:"rate"` + Status string `json:"status"` + LoanDays int `json:"loanDays"` + MrxLoanID float64 `json:"mrxLoanId"` + Fee float64 `json:"fee"` + UndueLoanFee float64 `json:"undueLoanFee"` + PricingPolicyType string `json:"pricingPolicyType"` +} diff --git a/models_derivative.go b/models_derivative.go new file mode 100644 index 0000000..491902f --- /dev/null +++ b/models_derivative.go @@ -0,0 +1,305 @@ +package tcbs + +// DerivativeResponse is a generic wrapper for derivative API responses. +type DerivativeResponse[T any] struct { + Cmd string `json:"cmd"` + RC string `json:"rc"` + RS string `json:"rs"` + OID string `json:"oID"` + Data T `json:"data"` +} + +// --- Cash & Positions --- + +// TotalCashDerivativeResponse represents derivative cash/margin overview. +type TotalCashDerivativeResponse struct { + Cash float64 `json:"cash"` + Stock float64 `json:"stock"` + Collateral float64 `json:"collateral"` + Type string `json:"type"` + Net string `json:"net"` + Tyle string `json:"tyle"` + IM float64 `json:"im"` + VM float64 `json:"vm"` + DM float64 `json:"dm"` + MR float64 `json:"mr"` + AvaiCash float64 `json:"avaiCash"` + AvaiColla float64 `json:"avaiColla"` + VMUnpay float64 `json:"vmunpay"` + Info string `json:"info"` + Color string `json:"color"` + VMEod string `json:"vm_eod"` + Others float64 `json:"others"` + Tax float64 `json:"tax"` + FeeCTCK float64 `json:"feeCTCK"` + FeeHNX float64 `json:"feeHNX"` + CashWithdraw float64 `json:"cashWithdraw"` + TienBoSung float64 `json:"tienbosung"` + CashAvailWithdraw float64 `json:"cashavaiwithdraw"` + Assets float64 `json:"assets"` + NAV float64 `json:"nav"` + CashOut float64 `json:"cashOut"` + UnrealizeVM float64 `json:"unrelizeVM"` + FeePos float64 `json:"feePos"` + FeeMan float64 `json:"feeMan"` + Product string `json:"product"` + Status string `json:"status"` + Debt string `json:"debt"` + W1 float64 `json:"w1"` + W2 float64 `json:"w2"` + Limit float64 `json:"limit"` + Package string `json:"package"` +} + +// AssetPositionCloseDerivativeResponse represents a closed derivative position. +type AssetPositionCloseDerivativeResponse struct { + Symbol string `json:"symbol"` + Side string `json:"side"` + OpenPrice string `json:"openPrice"` + ClosePrice string `json:"closePrice"` + ClosePosition string `json:"closePosition"` + Fee string `json:"fee"` + Tax string `json:"tax"` + CloseVM string `json:"closeVM"` + Unrealize string `json:"unrealize"` + ClosePC string `json:"closePC"` + Time string `json:"time"` +} + +// AssetPositionOpenDerivativeResponse represents an open derivative position. +type AssetPositionOpenDerivativeResponse struct { + Symbol string `json:"symbol"` + IM string `json:"im"` + Deliver int `json:"deliver"` + Receive int `json:"receive"` + Net int `json:"net"` + Side string `json:"side"` + Account string `json:"account"` + WASP float64 `json:"wasp"` + WAPB float64 `json:"wapb"` + LastPrice float64 `json:"lastPrice"` + IMValue float64 `json:"imValue"` + VMValue float64 `json:"vmValue"` + MRValue float64 `json:"mrValue"` + DueDate string `json:"duedate"` + NetOffVol int `json:"netoffvol"` + AvgRemain float64 `json:"avg_remain"` + VMRemain float64 `json:"vm_remain"` + PCRemain float64 `json:"pc_remain"` + StopLoss string `json:"stoploss"` + TakeProfit string `json:"takeprofit"` +} + +// --- Normal Orders --- + +// DerivativeNormalOrderRequest represents a request to place a normal derivative order. +type DerivativeNormalOrderRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + Side string `json:"side"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Volume int `json:"volume"` + Advance string `json:"advance,omitempty"` + RefID string `json:"refId,omitempty"` + OrderType string `json:"orderType"` + Pin string `json:"pin,omitempty"` +} + +// DerivativeNormalOrderResponse represents a normal derivative order in list responses. +type DerivativeNormalOrderResponse struct { + OrderNo string `json:"orderNo"` + PKOrderNo string `json:"pk_orderNo"` + RefID string `json:"refId"` + OrderTime string `json:"orderTime"` + AccountCode string `json:"accountCode"` + Side string `json:"side"` + Symbol string `json:"symbol"` + Volume float64 `json:"volume"` + ShowPrice float64 `json:"showPrice"` + MatchVolume float64 `json:"matchVolume"` + MatchPriceBQ float64 `json:"matchPriceBQ"` + Status string `json:"status"` + OrderStatus string `json:"orderStatus"` + Channel string `json:"channel"` + Group string `json:"group"` + CancelTime string `json:"cancelTime"` + IsCancel float64 `json:"isCancel"` + IsAmend float64 `json:"isAmend"` + Info string `json:"info"` + MaxPrice float64 `json:"maxPrice"` + MatchValue float64 `json:"matchValue"` + Quote string `json:"quote"` + AutoType string `json:"autoType"` + Product string `json:"product"` + OrderType string `json:"orderType"` + Source string `json:"source"` +} + +// DerivativeNormalOrderPlaceResponse represents the response after placing a normal order. +type DerivativeNormalOrderPlaceResponse struct { + Symbol string `json:"symbol"` + ShareStatus string `json:"shareStatus"` + Status string `json:"status"` + MsgType string `json:"msg_type"` + ShowPrice float64 `json:"showPrice"` + OrderTime string `json:"orderTime"` + Type string `json:"type"` + AccountCode string `json:"accountCode"` + OrderNo string `json:"orderNo"` + Market string `json:"market"` + MatchVolume float64 `json:"matchVolume"` + Side string `json:"side"` + Volume float64 `json:"volume"` + PKOrderNo string `json:"pk_orderNo"` + Channel string `json:"channel"` + RefID string `json:"refID"` + Group string `json:"group"` + AccType string `json:"accType"` + Quote string `json:"quote"` + AutoType string `json:"autoType"` + Product string `json:"product"` +} + +// --- Condition Orders --- + +// DerivativeConditionOrderRequest represents a request to place a conditional order. +type DerivativeConditionOrderRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + Side string `json:"side"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Volume float64 `json:"volume"` + Advance string `json:"advance,omitempty"` + RefID string `json:"refId,omitempty"` + OrderType string `json:"orderType"` + Pin string `json:"pin,omitempty"` + Type string `json:"type,omitempty"` + Cmd string `json:"cmd,omitempty"` + CallbackPoint float64 `json:"callbackPoint,omitempty"` + ActivationPrice float64 `json:"activationPrice,omitempty"` + SOPrice float64 `json:"soPrice,omitempty"` +} + +// DerivativeConditionOrderResponse represents a conditional order in list responses. +type DerivativeConditionOrderResponse struct { + OrderNo string `json:"orderNo"` + GroupOrder string `json:"groupOrder"` + PKOrderNo string `json:"pk_orderNo"` + AccountCode string `json:"accountCode"` + Side string `json:"side"` + Symbol string `json:"symbol"` + ShowPrice float64 `json:"showPrice"` + Volume float64 `json:"volume"` + Condition string `json:"condition"` + Result string `json:"result"` + ActiveTime string `json:"active_time"` + SendTime string `json:"send_time"` + CancelTime string `json:"cancel_time"` + Group string `json:"group"` + Channel string `json:"channel"` + MaxPrice string `json:"maxPrice"` + SOPrice float64 `json:"soPrice"` + OrderType string `json:"orderType"` + FromTime string `json:"from_time"` + ExpTime string `json:"exp_time"` + Status string `json:"status"` + Details string `json:"details"` + Notes string `json:"notes"` +} + +// DerivativeConditionOrderPlaceResponse represents the response after placing a condition order. +type DerivativeConditionOrderPlaceResponse struct { + Symbol string `json:"symbol"` + ShareStatus string `json:"shareStatus"` + Status string `json:"status"` + MsgType string `json:"msg_type"` + ShowPrice float64 `json:"showPrice"` + OrderTime string `json:"orderTime"` + Type string `json:"type"` + AccountCode string `json:"accountCode"` + OrderNo int `json:"orderNo"` + Market string `json:"market"` + MatchVolume float64 `json:"matchVolume"` + Side string `json:"side"` + Volume float64 `json:"volume"` + PKOrderNo string `json:"pk_orderNo"` + Channel string `json:"channel"` + RefID string `json:"refID"` + Group string `json:"group"` + AccType string `json:"accType"` + Quote string `json:"quote"` + AutoType string `json:"autoType"` + Product string `json:"product"` +} + +// --- Edit Orders --- + +// DerivativeChangeNormalOrderRequest represents a request to modify a normal order. +type DerivativeChangeNormalOrderRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + OrderNo string `json:"orderNo"` + RefID string `json:"refId"` + NVol float64 `json:"nvol"` + NPrice float64 `json:"nprice"` +} + +// DerivativeChangeConditionOrderRequest represents a request to modify a conditional order. +type DerivativeChangeConditionOrderRequest struct { + AccountID string `json:"accountId"` + PKOrderNo string `json:"pkOrderNo"` + Type string `json:"type"` + RefID string `json:"refId"` + SOPrice float64 `json:"soPrice"` + Cmd string `json:"cmd"` +} + +// --- Cancel Orders --- + +// DerivativeCancelNormalOrderRequest represents a request to cancel a normal order. +type DerivativeCancelNormalOrderRequest struct { + AccountID string `json:"accountId"` + OrderNo string `json:"orderNo"` + Cmd string `json:"cmd"` + Pin string `json:"pin,omitempty"` + RefID string `json:"refId,omitempty"` +} + +// DerivativeCancelNormalOrderResponse represents the response after cancelling a normal order. +type DerivativeCancelNormalOrderResponse struct { + OrderNo string `json:"orderNo"` + MsgType string `json:"msg_type"` + Status string `json:"status"` + PKOrderNo string `json:"pk_orderNo"` + CancelTime string `json:"cancelTime"` +} + +// DerivativeCancelConditionOrderRequest represents a request to cancel a conditional order. +type DerivativeCancelConditionOrderRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + OrderNo string `json:"orderNo"` +} + +// --- Market Info --- + +// DerivativeMarketInfo represents derivative contract pricing from REST API. +type DerivativeMarketInfo struct { + Ticker string `json:"ticker"` + RefPrice float64 `json:"refPrice"` + CeilingPrice float64 `json:"ceilingPrice"` + FloorPrice float64 `json:"floorPrice"` + HighPrice float64 `json:"highPrice"` + LowPrice float64 `json:"lowPrice"` + LastPrice float64 `json:"lastPrice"` + LastVol float64 `json:"lastVol"` + TotalVol float64 `json:"totalVol"` + OpenInterest float64 `json:"openInterest"` +} + +// OrderIDResponse represents a generic order ID response from derivative endpoints. +type OrderIDResponse struct { + OrderID string `json:"orderID"` +} diff --git a/models_market.go b/models_market.go new file mode 100644 index 0000000..3161b83 --- /dev/null +++ b/models_market.go @@ -0,0 +1,227 @@ +package tcbs + +// --- Stock Price (REST) --- + +// MarketStockInfo represents stock ticker information from REST API. +type MarketStockInfo struct { + Ticker string `json:"ticker"` + Exchange string `json:"exchange"` + RefPrice float64 `json:"refPrice"` + CeilingPrice float64 `json:"ceilingPrice"` + FloorPrice float64 `json:"floorPrice"` + HighPrice float64 `json:"highPrice"` + LowPrice float64 `json:"lowPrice"` + MatchPrice float64 `json:"matchPrice"` + MatchQtty float64 `json:"matchQtty"` + TotalMatchQtty float64 `json:"totalMatchQtty"` + TotalMatchValue float64 `json:"totalMatchValue"` + Best1BidPrice float64 `json:"best1BidPrice"` + Best1BidQtty float64 `json:"best1BidQtty"` + Best2BidPrice float64 `json:"best2BidPrice"` + Best2BidQtty float64 `json:"best2BidQtty"` + Best3BidPrice float64 `json:"best3BidPrice"` + Best3BidQtty float64 `json:"best3BidQtty"` + Best1OfferPrice float64 `json:"best1OfferPrice"` + Best1OfferQtty float64 `json:"best1OfferQtty"` + Best2OfferPrice float64 `json:"best2OfferPrice"` + Best2OfferQtty float64 `json:"best2OfferQtty"` + Best3OfferPrice float64 `json:"best3OfferPrice"` + Best3OfferQtty float64 `json:"best3OfferQtty"` +} + +// --- Foreign Room (REST) --- + +// ForeignRoomInfo represents foreign investor room information. +type ForeignRoomInfo struct { + Ticker string `json:"ticker"` + TotalRoom float64 `json:"totalRoom"` + CurrentRoom float64 `json:"currentRoom"` + BuyVol float64 `json:"buyVol"` + SellVol float64 `json:"sellVol"` +} + +// --- Put-Through (REST) --- + +// PutThroughMatchInfo represents put-through match information. +type PutThroughMatchInfo struct { + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Vol float64 `json:"vol"` + Val float64 `json:"val"` + Time string `json:"time"` + AccumulatedValue float64 `json:"accumulatedValue"` +} + +// PutThroughAdvertisementInfo represents put-through advertisement information. +type PutThroughAdvertisementInfo struct { + Symbol string `json:"symbol"` + Price float64 `json:"price"` + Vol float64 `json:"vol"` + Time string `json:"time"` + Status int `json:"status"` + Color int `json:"color"` + OrderID string `json:"orderId"` + Side string `json:"side"` +} + +// --- Intraday History --- + +// IntradayHistoryResponse represents intraday price matching history. +type IntradayHistoryResponse struct { + Ticker string `json:"ticker"` + Page int `json:"page"` + Size int `json:"size"` + Data []IntradayHistoryItem `json:"data"` +} + +// IntradayHistoryItem represents a single intraday trade. +type IntradayHistoryItem struct { + P float64 `json:"p"` + V float64 `json:"v"` + CP float64 `json:"cp"` + RCP float64 `json:"rcp"` + A string `json:"a"` + BA float64 `json:"ba"` + SA float64 `json:"sa"` + HL bool `json:"hl"` + PCP float64 `json:"pcp"` + T string `json:"t"` +} + +// --- Supply & Demand --- + +// SupplyDemandResponse represents supply and demand data. +type SupplyDemandResponse struct { + Ticker string `json:"ticker"` + Data []SupplyDemandItem `json:"data"` +} + +// SupplyDemandItem represents a single supply/demand data point (bsa-month). +type SupplyDemandItem struct { + BUP float64 `json:"bup"` + SDP float64 `json:"sdp"` + BSR float64 `json:"bsr"` + T string `json:"t"` +} + +// SupplyDemand15mItem represents a 15-minute supply/demand data point (bsa, bsa-ext). +type SupplyDemand15mItem struct { + BU float64 `json:"bu"` + BMS float64 `json:"bms"` + BUP float64 `json:"bup"` + SD float64 `json:"sd"` + SMS string `json:"sms"` + SDP float64 `json:"sdp"` + BSR float64 `json:"bsr"` + T string `json:"t"` + S int64 `json:"s"` +} + +// SupplyDemand15mResponse wraps a list of 15-minute supply/demand items. +type SupplyDemand15mResponse struct { + Ticker string `json:"ticker"` + Data []SupplyDemand15mItem `json:"data"` +} + +// --- WebSocket Market DTOs --- + +// WSStockInfo represents stock information from WebSocket stream. +type WSStockInfo struct { + Symbol string `json:"symbol"` + CeilPrice float64 `json:"ceilPrice"` + FloorPrice float64 `json:"floorPrice"` + RefPrice float64 `json:"refPrice"` + BidPrice01 float64 `json:"bidPrice01"` + BidPrice02 float64 `json:"bidPrice02"` + BidPrice03 float64 `json:"bidPrice03"` + BidQtty01 float64 `json:"bidQtty01"` + BidQtty02 float64 `json:"bidQtty02"` + BidQtty03 float64 `json:"bidQtty03"` + OfferPrice01 float64 `json:"offerPrice01"` + OfferPrice02 float64 `json:"offerPrice02"` + OfferPrice03 float64 `json:"offerPrice03"` + OfferQtty01 float64 `json:"offerQtty01"` + OfferQtty02 float64 `json:"offerQtty02"` + OfferQtty03 float64 `json:"offerQtty03"` + MatchPrice float64 `json:"matchPrice"` + MatchQtty float64 `json:"matchQtty"` + Change float64 `json:"change"` + ChangePercent float64 `json:"changePercent"` + Open float64 `json:"open"` + High float64 `json:"high"` + Low float64 `json:"low"` + TotalVol float64 `json:"totalVol"` + TotalVal float64 `json:"totalVal"` + OpenVol float64 `json:"openVol"` + BuyForeignQtty float64 `json:"buyForeignQtty"` + SellForeignQtty float64 `json:"sellForeignQtty"` + Room string `json:"room"` + Avg float64 `json:"avg"` + IndexNumber float64 `json:"indexNumber"` +} + +// WSDerivativeInfo represents derivative information from WebSocket stream. +type WSDerivativeInfo struct { + Symbol string `json:"symbol"` + CeilPrice float64 `json:"ceilPrice"` + FloorPrice float64 `json:"floorPrice"` + RefPrice float64 `json:"refPrice"` + BidPrice01 float64 `json:"bidPrice01"` + BidPrice02 float64 `json:"bidPrice02"` + BidPrice03 float64 `json:"bidPrice03"` + BidQtty01 float64 `json:"bidQtty01"` + BidQtty02 float64 `json:"bidQtty02"` + BidQtty03 float64 `json:"bidQtty03"` + OfferPrice01 float64 `json:"offerPrice01"` + OfferPrice02 float64 `json:"offerPrice02"` + OfferPrice03 float64 `json:"offerPrice03"` + OfferQtty01 float64 `json:"offerQtty01"` + OfferQtty02 float64 `json:"offerQtty02"` + OfferQtty03 float64 `json:"offerQtty03"` + MatchPrice float64 `json:"matchPrice"` + MatchQtty float64 `json:"matchQtty"` + Change float64 `json:"change"` + ChangePercent float64 `json:"changePercent"` + Open float64 `json:"open"` + High float64 `json:"high"` + Low float64 `json:"low"` + TotalVol float64 `json:"totalVol"` + OpenVol float64 `json:"openVol"` + BuyForeignQtty float64 `json:"buyForeignQtty"` + SellForeignQtty float64 `json:"sellForeignQtty"` + ExpiryDate string `json:"expiryDate"` + Avg float64 `json:"avg"` +} + +// WSForeignIndexInfo represents foreign index information from WebSocket stream. +type WSForeignIndexInfo struct { + Symbol string `json:"symbol"` + CeilPrice float64 `json:"ceilPrice"` + FloorPrice float64 `json:"floorPrice"` + RefPrice float64 `json:"refPrice"` + BidPrice01 float64 `json:"bidPrice01"` + BidPrice02 float64 `json:"bidPrice02"` + BidPrice03 float64 `json:"bidPrice03"` + BidQtty01 float64 `json:"bidQtty01"` + BidQtty02 float64 `json:"bidQtty02"` + BidQtty03 float64 `json:"bidQtty03"` + OfferPrice01 float64 `json:"offerPrice01"` + OfferPrice02 float64 `json:"offerPrice02"` + OfferPrice03 float64 `json:"offerPrice03"` + OfferQtty01 float64 `json:"offerQtty01"` + OfferQtty02 float64 `json:"offerQtty02"` + OfferQtty03 float64 `json:"offerQtty03"` + MatchPrice float64 `json:"matchPrice"` + MatchQtty float64 `json:"matchQtty"` + Change float64 `json:"change"` + ChangePercent float64 `json:"changePercent"` + Open float64 `json:"open"` + High float64 `json:"high"` + Low float64 `json:"low"` + TotalVolume float64 `json:"totalVolume"` + TotalValue float64 `json:"totalValue"` + BuyForeignQtty float64 `json:"buyForeignQtty"` + SellForeignQtty float64 `json:"sellForeignQtty"` + Room string `json:"room"` + Avg float64 `json:"avg"` +} diff --git a/models_money.go b/models_money.go new file mode 100644 index 0000000..bb8ba06 --- /dev/null +++ b/models_money.go @@ -0,0 +1,41 @@ +package tcbs + +// MoneyTransferRequest represents an internal money transfer request. +type MoneyTransferRequest struct { + SourceAccountNumber string `json:"sourceAccountNumber"` + DestinationAccountNumber string `json:"destinationAccountNumber"` + Amount float64 `json:"amount"` + Description float64 `json:"description"` // number type per spec +} + +// MoneyTransferResponse represents the transfer response. +type MoneyTransferResponse struct { + Status string `json:"status"` + Message string `json:"message"` +} + +// MarginDepositRequest represents a margin deposit request for derivative accounts. +type MarginDepositRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + Amount float64 `json:"amount"` + PaymentContent float64 `json:"paymentContent"` +} + +// MarginWithdrawRequest represents a margin withdrawal request for derivative accounts. +type MarginWithdrawRequest struct { + AccountID string `json:"accountId"` + SubAccountID string `json:"subAccountId"` + Amount float64 `json:"amount"` + PaymentContent float64 `json:"paymentContent"` +} + +// MarginDepositResponse represents the deposit response. +type MarginDepositResponse struct { + TransactionID string `json:"transactionId"` +} + +// MarginWithdrawResponse represents the withdrawal response. +type MarginWithdrawResponse struct { + TransactionID string `json:"transactionId"` +} diff --git a/models_order.go b/models_order.go new file mode 100644 index 0000000..0a392e3 --- /dev/null +++ b/models_order.go @@ -0,0 +1,205 @@ +package tcbs + +// --- Place Order --- + +// PlaceOrderRequest represents a stock order placement request. +type PlaceOrderRequest struct { + ExecType string `json:"execType"` + Price int `json:"price"` + PriceType string `json:"priceType"` + Quantity int `json:"quantity"` + Symbol string `json:"symbol"` +} + +// PlaceOrderResponse represents the response after placing a stock order. +type PlaceOrderResponse struct { + Error string `json:"error"` + Message string `json:"message"` + OrderID string `json:"orderId"` +} + +// --- Update Order --- + +// UpdateOrderRequest represents a stock order update request. +type UpdateOrderRequest struct { + Price int `json:"price"` + Quantity int `json:"quantity"` +} + +// UpdateOrderResponse represents the response after updating a stock order. +type UpdateOrderResponse struct { + Error string `json:"error"` + Message string `json:"message"` + OrderID string `json:"orderId"` +} + +// --- Cancel Order --- + +// CancelOrderRequest represents a stock order cancellation request. +type CancelOrderRequest struct { + OrdersList []OrderIDRef `json:"ordersList"` +} + +// OrderIDRef represents an order ID reference used in cancel requests. +type OrderIDRef struct { + OrderID string `json:"orderID"` +} + +// CancelOrderResponse represents the response after cancelling stock orders. +type CancelOrderResponse struct { + Object string `json:"object"` + PageSize int `json:"pageSize"` + PageIndex int `json:"pageIndex"` + TotalCount int `json:"totalCount"` + Data []DataX `json:"data"` +} + +// DataX holds cancel order result details. +type DataX struct { + Object string `json:"object"` + Details []Detail `json:"details"` +} + +// Detail holds a single order cancellation result. +type Detail struct { + Deleted string `json:"deleted"` + ErrorCode string `json:"errorCode"` + ErrorMessage string `json:"errorMesage"` // note: typo in spec + OrderID string `json:"orderID"` +} + +// --- Order Query --- + +// OrderSearchResponse represents the order book response. +type OrderSearchResponse struct { + Object string `json:"object"` + PageSize int `json:"pageSize"` + PageIndex int `json:"pageIndex"` + TotalCount int `json:"totalCount"` + Data []OrderInfo `json:"data"` +} + +// OrderInfo represents a single order in the order book. +type OrderInfo struct { + Object string `json:"object"` + AccountNo string `json:"accountNo"` + OrderID string `json:"orderID"` + ExecType string `json:"execType"` + OrderQtty float64 `json:"orderQtty"` + ExecQtty float64 `json:"execQtty"` + CodeID string `json:"codeID"` + Symbol string `json:"symbol"` + PriceType string `json:"priceType"` + TxTime string `json:"txtime"` + TxDate string `json:"txdate"` + ExpDate string `json:"expDate"` + TimeType string `json:"timeType"` + OrStatus string `json:"orStatus"` + FeeAcr float64 `json:"feeAcr"` + LimitPrice float64 `json:"limitPrice"` + CancelQtty float64 `json:"cancelQtty"` + RemainQtty float64 `json:"remainQtty"` + Via string `json:"via"` + QuotePrice float64 `json:"quotePrice"` + MatchPrice float64 `json:"matchPrice"` + TradePlace string `json:"tradePlace"` + MatchType string `json:"matchType"` + IsDisposal string `json:"isDisposal"` + IsCancel string `json:"isCancel"` + IsAmend string `json:"isAmend"` + UserName string `json:"userName"` + OrsOrderID string `json:"orsOrderID"` + SecType string `json:"sectype"` + IsFOOrder string `json:"isFOOrder"` + OdTimeStamp string `json:"odTimeStamp"` + MatchAmount float64 `json:"matchAmount"` + MMType string `json:"mmType"` + BRatio float64 `json:"bRatio"` + TaxSellAmt float64 `json:"taxSellAmout"` // note: typo in spec +} + +// --- Match Information --- + +// CommandMatchInformationResponse represents matching details. +type CommandMatchInformationResponse struct { + Object string `json:"object"` + TotalCount int `json:"totalCount"` + PageSize int `json:"pageSize"` + PageIndex int `json:"pageIndex"` + Data []CommandMatchInformationDetail `json:"data"` +} + +// CommandMatchInformationDetail represents a single matching detail. +type CommandMatchInformationDetail struct { + OrderID string `json:"orderId"` + Side string `json:"side"` + Symbol string `json:"symbol"` + QuoteQtty float64 `json:"quoteQtty"` + QuotePrice float64 `json:"quotePrice"` + TradeID string `json:"tradeId"` + Qtty float64 `json:"qtty"` + Price float64 `json:"price"` + TimeExec float64 `json:"timeExec"` +} + +// --- Purchasing Power --- + +// PurchasingPowerResponse represents purchasing power information. +type PurchasingPowerResponse struct { + AccountNo string `json:"accountNo"` + CustodyID string `json:"custodyID"` + Symbol string `json:"symbol"` + Price float64 `json:"price"` + PP0 float64 `json:"pp0"` + PPSE float64 `json:"ppse"` + PPSERef float64 `json:"ppseref"` + MaxBuyQuantity float64 `json:"maxBuyQuantity"` + RealMaxBuyQty float64 `json:"realMaxBuyQuantity"` + MinBuyQuantity float64 `json:"minBuyQuantity"` + MarginRatioLoan float64 `json:"marginRatioLoan"` + MarginPriceLoan float64 `json:"marginPriceLoan"` + RateBrkS string `json:"rateBrkS"` + RateBrkB string `json:"rateBrkB"` +} + +// --- Margin --- + +// MarginQuotaResponse represents margin quota information. +type MarginQuotaResponse struct { + CustodyID string `json:"custodyID"` + AccountNo string `json:"accountNo"` + AFType string `json:"aftype"` + VSDStatus string `json:"vsdStatus"` + AccountStatus string `json:"accountStatus"` + MarginLimit float64 `json:"marginLimit"` + IsIA string `json:"isIA"` + BankName string `json:"bankName"` + BankAccount string `json:"bankAccount"` + AccountType string `json:"accountType"` +} + +// MarginAccountInfoResponse represents margin account details. +type MarginAccountInfoResponse struct { + AccountNo string `json:"accountNo"` + RiskPolicy *RiskPolicy `json:"riskPolicy,omitempty"` + RTT float64 `json:"rtt"` + Outstanding float64 `json:"outstanding"` + AccruedInterest float64 `json:"accruedInterest"` + DueAmount float64 `json:"dueAmount"` + OverdueAmount float64 `json:"overdueAmount"` + RiskStatus *RiskStatus `json:"riskStatus,omitempty"` + TotalFeeDebt float64 `json:"totalFeeDebt"` +} + +// RiskPolicy represents margin risk policy parameters. +type RiskPolicy struct { + MaintenanceMargin float64 `json:"maintenanceMargin"` + InitialMargin float64 `json:"initialMargin"` + LiquidationMargin float64 `json:"liquidationMargin"` +} + +// RiskStatus represents RTT status. +type RiskStatus struct { + Code string `json:"code"` + Description string `json:"description"` +} diff --git a/money.go b/money.go index 4e7d8fb..d0ba20a 100644 --- a/money.go +++ b/money.go @@ -13,8 +13,8 @@ func (c *Client) TransferMoney(ctx context.Context, req *MoneyTransferRequest) ( } // WithdrawMargin withdraws margin for derivative accounts. -func (c *Client) WithdrawMargin(ctx context.Context, req *MarginDepositWithdrawRequest) (*MarginDepositWithdrawResponse, error) { - var resp MarginDepositWithdrawResponse +func (c *Client) WithdrawMargin(ctx context.Context, req *MarginWithdrawRequest) (*MarginWithdrawResponse, error) { + var resp MarginWithdrawResponse err := c.post(ctx, "/khronos/v1/cash/withdraw/update", req, &resp) if err != nil { return nil, err @@ -23,8 +23,8 @@ func (c *Client) WithdrawMargin(ctx context.Context, req *MarginDepositWithdrawR } // DepositMargin deposits margin for derivative accounts. -func (c *Client) DepositMargin(ctx context.Context, req *MarginDepositWithdrawRequest) (*MarginDepositWithdrawResponse, error) { - var resp MarginDepositWithdrawResponse +func (c *Client) DepositMargin(ctx context.Context, req *MarginDepositRequest) (*MarginDepositResponse, error) { + var resp MarginDepositResponse err := c.post(ctx, "/khronos/v1/cash/deposit/update", req, &resp) if err != nil { return nil, err diff --git a/money_test.go b/money_test.go new file mode 100644 index 0000000..a742b17 --- /dev/null +++ b/money_test.go @@ -0,0 +1,77 @@ +package tcbs + +import ( + "context" + "encoding/json" + "net/http" + "testing" +) + +func TestTransferMoney(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/physis/v1/stock/transfer" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + var req MoneyTransferRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode: %v", err) + } + if req.SourceAccountNumber != "ACC1" || req.Amount != 1000000 { + t.Errorf("unexpected request: %+v", req) + } + writeJSON(t, w, MoneyTransferResponse{Status: "ok"}) + }) + + resp, err := client.TransferMoney(context.Background(), &MoneyTransferRequest{ + SourceAccountNumber: "ACC1", + DestinationAccountNumber: "ACC2", + Amount: 1000000, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Status != "ok" { + t.Errorf("expected status 'ok', got %q", resp.Status) + } +} + +func TestDepositMargin(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/khronos/v1/cash/deposit/update" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, MarginDepositResponse{TransactionID: "TXN-1"}) + }) + + resp, err := client.DepositMargin(context.Background(), &MarginDepositRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", Amount: 5000000, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TransactionID != "TXN-1" { + t.Errorf("expected TXN-1, got %q", resp.TransactionID) + } +} + +func TestWithdrawMargin(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/khronos/v1/cash/withdraw/update" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, MarginWithdrawResponse{TransactionID: "TXN-2"}) + }) + + resp, err := client.WithdrawMargin(context.Background(), &MarginWithdrawRequest{ + AccountID: "ACC1", SubAccountID: "SUB1", Amount: 3000000, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TransactionID != "TXN-2" { + t.Errorf("expected TXN-2, got %q", resp.TransactionID) + } +} diff --git a/stock_market.go b/stock_market.go index c9e68fa..4601d89 100644 --- a/stock_market.go +++ b/stock_market.go @@ -8,7 +8,6 @@ import ( ) // GetStockPrices retrieves stock ticker information and pricing. -// tickers is a comma-separated list of stock symbols. func (c *Client) GetStockPrices(ctx context.Context, tickers []string) ([]MarketStockInfo, error) { query := url.Values{} query.Set("tickers", strings.Join(tickers, ",")) @@ -22,7 +21,6 @@ func (c *Client) GetStockPrices(ctx context.Context, tickers []string) ([]Market } // GetForeignRoom retrieves foreign investor room information. -// tickers is a comma-separated list of stock symbols. func (c *Client) GetForeignRoom(ctx context.Context, tickers []string) ([]ForeignRoomInfo, error) { query := url.Values{} query.Set("tickers", strings.Join(tickers, ",")) @@ -35,13 +33,12 @@ func (c *Client) GetForeignRoom(ctx context.Context, tickers []string) ([]Foreig return resp, nil } -// GetPutThroughInfo retrieves put-through agreement information. -// tickers is a comma-separated list of stock symbols. -func (c *Client) GetPutThroughInfo(ctx context.Context, tickers []string) ([]PutThroughInfo, error) { +// GetPutThroughInfo retrieves put-through match information. +func (c *Client) GetPutThroughInfo(ctx context.Context, tickers []string) ([]PutThroughMatchInfo, error) { query := url.Values{} query.Set("tickers", strings.Join(tickers, ",")) - var resp []PutThroughInfo + var resp []PutThroughMatchInfo err := c.get(ctx, "/tartarus/v1/putThroughSnaps", query, &resp) if err != nil { return nil, err @@ -70,18 +67,48 @@ func (c *Client) GetIntradayHistory(ctx context.Context, params IntradayHistoryP return &resp, nil } -// GetSupplyDemand retrieves supply and demand data for a ticker. +// GetSupplyDemand retrieves supply and demand data for a ticker (15-minute intervals). // investorType is one of: "sheep", "wolf", "shark", "all". -func (c *Client) GetSupplyDemand(ctx context.Context, ticker, investorType string) (*SupplyDemandResponse, error) { +func (c *Client) GetSupplyDemand(ctx context.Context, ticker, investorType string) (*SupplyDemand15mResponse, error) { query := url.Values{} if investorType != "" { query.Set("type", investorType) } - var resp SupplyDemandResponse + var resp SupplyDemand15mResponse err := c.get(ctx, fmt.Sprintf("/nyx/v1/intraday/%s/bsa", ticker), query, &resp) if err != nil { return nil, err } return &resp, nil } + +// GetSupplyDemandExt retrieves extended supply and demand data for a ticker (15-minute intervals). +func (c *Client) GetSupplyDemandExt(ctx context.Context, ticker, investorType string) (*SupplyDemand15mResponse, error) { + query := url.Values{} + if investorType != "" { + query.Set("type", investorType) + } + + var resp SupplyDemand15mResponse + err := c.get(ctx, fmt.Sprintf("/nyx/v1/intraday/%s/bsa-ext", ticker), query, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} + +// GetSupplyDemandMonth retrieves monthly supply and demand data for a ticker. +func (c *Client) GetSupplyDemandMonth(ctx context.Context, ticker, investorType string) (*SupplyDemandResponse, error) { + query := url.Values{} + if investorType != "" { + query.Set("type", investorType) + } + + var resp SupplyDemandResponse + err := c.get(ctx, fmt.Sprintf("/nyx/v1/intraday/%s/bsa-month", ticker), query, &resp) + if err != nil { + return nil, err + } + return &resp, nil +} diff --git a/stock_market_test.go b/stock_market_test.go new file mode 100644 index 0000000..9fc7d83 --- /dev/null +++ b/stock_market_test.go @@ -0,0 +1,132 @@ +package tcbs + +import ( + "context" + "net/http" + "testing" +) + +func TestGetStockPrices(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/tartarus/v1/tickerCommons" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + tickers := r.URL.Query().Get("tickers") + if tickers != "FPT,VNM" { + t.Errorf("unexpected tickers: %s", tickers) + } + writeJSON(t, w, []MarketStockInfo{ + {Ticker: "FPT", MatchPrice: 120000}, + {Ticker: "VNM", MatchPrice: 80000}, + }) + }) + + resp, err := client.GetStockPrices(context.Background(), []string{"FPT", "VNM"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 2 { + t.Fatalf("expected 2 items, got %d", len(resp)) + } + if resp[0].Ticker != "FPT" || resp[0].MatchPrice != 120000 { + t.Errorf("unexpected FPT data: %+v", resp[0]) + } +} + +func TestGetIntradayHistory(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/nyx/v1/intraday/FPT/his/paging" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, IntradayHistoryResponse{ + Ticker: "FPT", + Data: []IntradayHistoryItem{{P: 120000, V: 100}}, + }) + }) + + resp, err := client.GetIntradayHistory(context.Background(), IntradayHistoryParams{ + Ticker: "FPT", Page: 0, Size: 20, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 { + t.Errorf("expected 1 item, got %d", len(resp.Data)) + } +} + +func TestGetSupplyDemand(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/nyx/v1/intraday/FPT/bsa" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, SupplyDemand15mResponse{ + Ticker: "FPT", + Data: []SupplyDemand15mItem{{BU: 100, SD: 50}}, + }) + }) + + resp, err := client.GetSupplyDemand(context.Background(), "FPT", "all") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Data) != 1 { + t.Errorf("expected 1 item, got %d", len(resp.Data)) + } +} + +func TestGetSupplyDemandExt(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/nyx/v1/intraday/FPT/bsa-ext" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, SupplyDemand15mResponse{Ticker: "FPT"}) + }) + + _, err := client.GetSupplyDemandExt(context.Background(), "FPT", "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetSupplyDemandMonth(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/nyx/v1/intraday/FPT/bsa-month" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, SupplyDemandResponse{Ticker: "FPT"}) + }) + + _, err := client.GetSupplyDemandMonth(context.Background(), "FPT", "all") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestGetForeignRoom(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, []ForeignRoomInfo{{Ticker: "FPT", TotalRoom: 1000}}) + }) + + resp, err := client.GetForeignRoom(context.Background(), []string{"FPT"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].Ticker != "FPT" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetPutThroughInfo(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, []PutThroughMatchInfo{{Symbol: "FPT", Vol: 500}}) + }) + + resp, err := client.GetPutThroughInfo(context.Background(), []string{"FPT"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].Symbol != "FPT" { + t.Errorf("unexpected response: %+v", resp) + } +} diff --git a/stock_order.go b/stock_order.go index 131b509..dae2ce3 100644 --- a/stock_order.go +++ b/stock_order.go @@ -25,7 +25,7 @@ func (c *Client) UpdateOrder(ctx context.Context, accountNo, orderID string, req return &resp, nil } -// CancelOrder cancels an existing stock order. +// CancelOrder cancels existing stock orders. func (c *Client) CancelOrder(ctx context.Context, accountNo string, req *CancelOrderRequest) (*CancelOrderResponse, error) { var resp CancelOrderResponse err := c.put(ctx, fmt.Sprintf("/akhlys/v1/accounts/%s/cancel-orders", accountNo), req, &resp) diff --git a/stock_order_test.go b/stock_order_test.go new file mode 100644 index 0000000..1c802a9 --- /dev/null +++ b/stock_order_test.go @@ -0,0 +1,79 @@ +package tcbs + +import ( + "context" + "encoding/json" + "net/http" + "testing" +) + +func TestPlaceOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + if r.URL.Path != "/akhlys/v1/accounts/ACC001/orders" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + var req PlaceOrderRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + t.Fatalf("failed to decode request: %v", err) + } + if req.Symbol != "FPT" || req.Quantity != 100 { + t.Errorf("unexpected request: %+v", req) + } + writeJSON(t, w, PlaceOrderResponse{OrderID: "ORD-1"}) + }) + + resp, err := client.PlaceOrder(context.Background(), "ACC001", &PlaceOrderRequest{ + Symbol: "FPT", + ExecType: "NB", + Quantity: 100, + Price: 120000, + PriceType: "LO", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.OrderID != "ORD-1" { + t.Errorf("expected order ID 'ORD-1', got %q", resp.OrderID) + } +} + +func TestUpdateOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + writeJSON(t, w, UpdateOrderResponse{OrderID: "ORD-1", Message: "ok"}) + }) + + resp, err := client.UpdateOrder(context.Background(), "ACC001", "ORD-1", &UpdateOrderRequest{ + Price: 125000, Quantity: 200, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.OrderID != "ORD-1" { + t.Errorf("expected 'ORD-1', got %q", resp.OrderID) + } +} + +func TestCancelOrder(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPut { + t.Errorf("expected PUT, got %s", r.Method) + } + writeJSON(t, w, CancelOrderResponse{TotalCount: 1}) + }) + + resp, err := client.CancelOrder(context.Background(), "ACC001", &CancelOrderRequest{ + OrdersList: []OrderIDRef{{OrderID: "ORD-1"}}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TotalCount != 1 { + t.Errorf("expected totalCount 1, got %d", resp.TotalCount) + } +} diff --git a/stock_query_test.go b/stock_query_test.go new file mode 100644 index 0000000..2e82d95 --- /dev/null +++ b/stock_query_test.go @@ -0,0 +1,230 @@ +package tcbs + +import ( + "context" + "net/http" + "testing" +) + +func TestGetOrders(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/aion/v1/accounts/ACC001/orders" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, OrderSearchResponse{TotalCount: 5}) + }) + + resp, err := client.GetOrders(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TotalCount != 5 { + t.Errorf("expected 5, got %d", resp.TotalCount) + } +} + +func TestGetPurchasingPower(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, PurchasingPowerResponse{PP0: 100000000}) + }) + + resp, err := client.GetPurchasingPower(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.PP0 != 100000000 { + t.Errorf("expected PP0=100000000, got %.0f", resp.PP0) + } +} + +func TestGetMarginQuota(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, []MarginQuotaResponse{{AccountNo: "ACC001", MarginLimit: 500000}}) + }) + + resp, err := client.GetMarginQuota(context.Background(), "CUS001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].AccountNo != "ACC001" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetStockAssets(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, SeInfoDTO{ + AccountNo: "ACC001", + Stock: []StockHoldingInfo{{Symbol: "FPT", TotalQtty: 1000}}, + }) + }) + + resp, err := client.GetStockAssets(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.Stock) != 1 || resp.Stock[0].Symbol != "FPT" { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetOrderByID(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/aion/v1/accounts/ACC001/orders/ORD-1" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + writeJSON(t, w, OrderSearchResponse{TotalCount: 1}) + }) + + resp, err := client.GetOrderByID(context.Background(), "ACC001", "ORD-1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TotalCount != 1 { + t.Errorf("expected 1, got %d", resp.TotalCount) + } +} + +func TestGetMatchingDetails(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, CommandMatchInformationResponse{TotalCount: 2}) + }) + + resp, err := client.GetMatchingDetails(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TotalCount != 2 { + t.Errorf("expected 2, got %d", resp.TotalCount) + } +} + +func TestGetPurchasingPowerBySymbol(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, PurchasingPowerResponse{PP0: 50000000}) + }) + + resp, err := client.GetPurchasingPowerBySymbol(context.Background(), "ACC001", "FPT") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.PP0 != 50000000 { + t.Errorf("unexpected PP0: %.0f", resp.PP0) + } +} + +func TestGetPurchasingPowerBySymbolPrice(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, PurchasingPowerResponse{MaxBuyQuantity: 100}) + }) + + resp, err := client.GetPurchasingPowerBySymbolPrice(context.Background(), "ACC001", "FPT", "120000") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.MaxBuyQuantity != 100 { + t.Errorf("unexpected MaxBuyQuantity: %.0f", resp.MaxBuyQuantity) + } +} + +func TestGetMarginAccountInfo(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, []MarginAccountInfoResponse{{AccountNo: "ACC001", RTT: 1.5}}) + }) + + resp, err := client.GetMarginAccountInfo(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp) != 1 || resp[0].RTT != 1.5 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetSupplementaryLoanPackages(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, SupplementaryLoanPackageResponse{ + MarginSureViews: []MarginSureView{{Name: "pkg1"}}, + }) + }) + + resp, err := client.GetSupplementaryLoanPackages(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.MarginSureViews) != 1 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetLoans(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, LoanResponse{Size: 1, Content: []LoanItem{{Symbol: "FPT"}}}) + }) + + resp, err := client.GetLoans(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Size != 1 { + t.Errorf("expected size 1, got %d", resp.Size) + } +} + +func TestGetCashBalance(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, CashInvestmentResponse{TotalCount: 1}) + }) + + resp, err := client.GetCashBalance(context.Background(), "ACC001") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.TotalCount != 1 { + t.Errorf("expected 1, got %d", resp.TotalCount) + } +} + +func TestGetMarginInfo(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + writeJSON(t, w, MarginInfoResponse{ + Response: &MarginInfoData{TotalRow: 3}, + }) + }) + + resp, err := client.GetMarginInfo(context.Background(), MarginInfoParams{ + AccountNo: "ACC001", FromDate: "2025-01-01", ToDate: "2025-01-31", + Page: "0", Size: "10", CustodyCD: "CUS001", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Response == nil || resp.Response.TotalRow != 3 { + t.Errorf("unexpected response: %+v", resp) + } +} + +func TestGetCashStatements(t *testing.T) { + client, _ := newTestServer(t, func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/erebos/v2/digital/trans-hist-cashStatements" { + t.Errorf("unexpected path: %s", r.URL.Path) + } + if r.URL.Query().Get("fromDate") != "2025-01-01" { + t.Errorf("unexpected fromDate: %s", r.URL.Query().Get("fromDate")) + } + writeJSON(t, w, TransHistCashStatementsResponse{ + Response: &TransHistCashStatementsData{TotalCount: 3}, + }) + }) + + resp, err := client.GetCashStatements(context.Background(), CashStatementParams{ + AccountNo: "ACC001", FromDate: "2025-01-01", ToDate: "2025-01-31", + PageSize: "10", PageIndex: "0", TransactionCode: "", + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.Response == nil || resp.Response.TotalCount != 3 { + t.Errorf("unexpected response: %+v", resp) + } +} diff --git a/websocket.go b/websocket.go new file mode 100644 index 0000000..b8cdcac --- /dev/null +++ b/websocket.go @@ -0,0 +1,136 @@ +package tcbs + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + "sync" + + "nhooyr.io/websocket" +) + +// WSEndpoint represents a known WebSocket endpoint. +type WSEndpoint string + +const ( + // WSStockMatch is the WebSocket endpoint for stock match information. + WSStockMatch WSEndpoint = "/ws/aither" + // WSDerivativeMatch is the WebSocket endpoint for derivative match information. + WSDerivativeMatch WSEndpoint = "/ws/nesoi" + // WSCenter is the general WebSocket center endpoint. + WSCenter WSEndpoint = "/ws/ouranos/v1/stream" + // WSStockPrice is the WebSocket endpoint for normal stock prices. + WSStockPrice WSEndpoint = "/ws/thesis/v1/stream/normal" + // WSDerivativePrice is the WebSocket endpoint for derivative prices. + WSDerivativePrice WSEndpoint = "/ws/thesis/v1/stream/derivative" +) + +// MessageHandler is a callback invoked for each received WebSocket message. +type MessageHandler func(msgType websocket.MessageType, data []byte) + +// WSConn represents a managed WebSocket connection. +type WSConn struct { + conn *websocket.Conn + cancel context.CancelFunc + done chan struct{} + mu sync.Mutex + closed bool +} + +// Close gracefully closes the WebSocket connection. +func (ws *WSConn) Close() error { + ws.mu.Lock() + defer ws.mu.Unlock() + if ws.closed { + return nil + } + ws.closed = true + ws.cancel() + <-ws.done + return ws.conn.Close(websocket.StatusNormalClosure, "client closed") +} + +// Send sends a text message over the WebSocket connection. +func (ws *WSConn) Send(ctx context.Context, msg []byte) error { + return ws.conn.Write(ctx, websocket.MessageText, msg) +} + +// SendJSON marshals v to JSON and sends it as a text message. +func (ws *WSConn) SendJSON(ctx context.Context, v any) error { + data, err := json.Marshal(v) + if err != nil { + return fmt.Errorf("tcbs: marshal ws message: %w", err) + } + return ws.conn.Write(ctx, websocket.MessageText, data) +} + +// ConnectWS establishes a WebSocket connection to the given endpoint. +// The handler is called for each message received. The connection reads +// messages in a background goroutine until the context is cancelled or +// Close is called. +func (c *Client) ConnectWS(ctx context.Context, endpoint WSEndpoint, handler MessageHandler) (*WSConn, error) { + wsURL := c.baseURL + string(endpoint) + wsURL = strings.Replace(wsURL, "https://", "wss://", 1) + wsURL = strings.Replace(wsURL, "http://", "ws://", 1) + + header := http.Header{} + if token := c.currentToken(); token != "" { + header.Set("Authorization", "Bearer "+token) + } + + conn, _, err := websocket.Dial(ctx, wsURL, &websocket.DialOptions{ + HTTPHeader: header, + }) + if err != nil { + return nil, fmt.Errorf("tcbs: ws dial %s: %w", endpoint, err) + } + + readCtx, cancel := context.WithCancel(ctx) + done := make(chan struct{}) + + ws := &WSConn{ + conn: conn, + cancel: cancel, + done: done, + } + + go func() { + defer close(done) + for { + msgType, data, err := conn.Read(readCtx) + if err != nil { + return + } + handler(msgType, data) + } + }() + + return ws, nil +} + +// ConnectStockMatch connects to the stock match information WebSocket. +func (c *Client) ConnectStockMatch(ctx context.Context, handler MessageHandler) (*WSConn, error) { + return c.ConnectWS(ctx, WSStockMatch, handler) +} + +// ConnectDerivativeMatch connects to the derivative match information WebSocket. +func (c *Client) ConnectDerivativeMatch(ctx context.Context, handler MessageHandler) (*WSConn, error) { + return c.ConnectWS(ctx, WSDerivativeMatch, handler) +} + +// ConnectCenter connects to the general WebSocket center. +func (c *Client) ConnectCenter(ctx context.Context, handler MessageHandler) (*WSConn, error) { + return c.ConnectWS(ctx, WSCenter, handler) +} + +// ConnectStockPrice connects to the normal stock price WebSocket. +func (c *Client) ConnectStockPrice(ctx context.Context, handler MessageHandler) (*WSConn, error) { + return c.ConnectWS(ctx, WSStockPrice, handler) +} + +// ConnectDerivativePrice connects to the derivative price WebSocket. +func (c *Client) ConnectDerivativePrice(ctx context.Context, handler MessageHandler) (*WSConn, error) { + return c.ConnectWS(ctx, WSDerivativePrice, handler) +}