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.
This commit is contained in:
2026-04-05 12:00:18 +07:00
parent 5a083cb7d8
commit ee236d1fe0
25 changed files with 2362 additions and 777 deletions
+160 -2
View File
@@ -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.
+32
View File
@@ -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")
}
}
+5 -8
View File
@@ -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
}
+30
View File
@@ -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())
}
}
+85
View File
@@ -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)
}
}
+12 -13
View File
@@ -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, ","))
+216
View File
@@ -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)
}
}
+20 -3
View File
@@ -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))
}
+2
View File
@@ -1,3 +1,5 @@
module github.com/tiennm99/tcbs-api
go 1.22.2
require nhooyr.io/websocket v1.8.17 // indirect
+2
View File
@@ -0,0 +1,2 @@
nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y=
nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
-737
View File
@@ -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"`
}
+80
View File
@@ -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"`
}
+245
View File
@@ -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"`
}
+305
View File
@@ -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"`
}
+227
View File
@@ -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"`
}
+41
View File
@@ -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"`
}
+205
View File
@@ -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"`
}
+4 -4
View File
@@ -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
+77
View File
@@ -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)
}
}
+36 -9
View File
@@ -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
}
+132
View File
@@ -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)
}
}
+1 -1
View File
@@ -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)
+79
View File
@@ -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)
}
}
+230
View File
@@ -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)
}
}
+136
View File
@@ -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)
}