API Services
Detailed API methods for each service in Go SDK
The examples use placeholders (
YOUR_OTP,YOUR_ACCOUNT_NO,YOUR_ORDER_ID,YOUR_CLIENT_REQUEST_ID). Read the OTP, keys and account numbers from environment variables or a secret store; do not hard-code them and do not log tokens. All durations in the SDK are integers in milliseconds (Mssuffix). Error types come from the root packagessisdk(github.com/SSI-Securities-Inc/ssi-sdk-go/v3), see Error Handling.
1. Authentication (TokenManager)
Access via auth.TokenManager or directly on auth (delegated).
Authenticate with OTP
token, err := auth.Authenticate(os.Getenv("SSI_OTP")) // YOUR_OTP
if err != nil {
log.Fatal(err)
}
_ = token // *auth.Token; do not print or log the access tokenAuthenticate(otp, txID...) takes an optional transaction ID (used for Smart OTP). Authenticate("") is used for market data only (no trading permission).
Request OTP
// Raw map
result, err := auth.RequestOTP()
// Typed response: Message and TransactionID
otpRes, err := auth.RequestOTPTyped()Authenticate and wait for Smart OTP
// Returns the access token string
accessToken, err := auth.EnsureAuthenticated(os.Getenv("SSI_OTP"))
// With options (poll interval and max retries, in the same units as Config)
interval := 5000
retries := 12
accessToken, err = auth.EnsureAuthenticatedWith(ctx, authpkg.EnsureOptions{
OTP: os.Getenv("SSI_OTP"),
PollIntervalMs: &interval,
PollMaxRetries: &retries,
})authpkg is the import alias of github.com/SSI-Securities-Inc/ssi-sdk-go/v3/auth. Defaults come from Config.OTPPollIntervalMs (5000) and Config.OTPPollMaxWaitMs (60000), see Configuration.
Refresh token
token, err := auth.Refresh()Check token status
if auth.TokenManager.IsTokenExpired() { // 30 s skew
auth.Refresh()
}
accessToken := auth.AccessToken()Other methods on *ssi.Auth: Config, Token, SetToken, HasRefreshToken, CanRefresh, Close.
2. Account (AccountService)
Access via t.Account.
Get account list
accounts, err := t.Account.GetAccountInfo()
for _, acc := range accounts {
accType := "unknown"
if acc.AccountType != nil { // pointer: nil when the server did not send a type
accType = string(*acc.AccountType)
}
fmt.Printf("%s (%s)\n", acc.AccountNo, accType)
}Returns: []Account — each Account has AccountNo string and AccountType *account.AccountType (known values Cash, Margin, Derivative; an unknown value from the server is kept as a raw string).
3. Market Data (MarketDataService)
Access via data.MarketData. No OTP required.
3.1. OHLC Data (candlestick)
Intraday:
| Method | Timeframe |
|---|---|
GetOHLC1Minute(symbol) | 1 minute |
GetOHLC3Minute(symbol) | 3 minutes |
GetOHLC5Minute(symbol) | 5 minutes |
GetOHLC15Minute(symbol) | 15 minutes |
GetOHLC1Hour(symbol) | 1 hour |
ohlc, err := data.MarketData.GetOHLC1Minute("SSI")
if err != nil {
log.Fatal(err)
}
for _, candle := range ohlc {
fmt.Printf("%s: O=%.0f H=%.0f L=%.0f C=%.0f V=%d\n",
candle.TradingDate, candle.OpenPrice, candle.HighPrice,
candle.LowPrice, candle.ClosePrice, candle.Volume)
}There is no GetOHLC1Day: for daily candles use GetOHLC1DayHistorical.
Historical (with date range + pagination):
| Method | Timeframe |
|---|---|
GetOHLC1MinuteHistorical(symbol, fromDate, toDate, page, size) | 1 minute |
GetOHLC3MinuteHistorical(...) | 3 minutes |
GetOHLC5MinuteHistorical(...) | 5 minutes |
GetOHLC15MinuteHistorical(...) | 15 minutes |
GetOHLC1HourHistorical(...) | 1 hour |
GetOHLC1DayHistorical(...) | 1 day |
ohlc, err := data.MarketData.GetOHLC1DayHistorical(
"SSI", "2026/03/27", "2026/04/22", 1, 100,
)Note:
GetOHLC1WeekHistoricalandGetOHLC1MonthHistoricalstill exist in the SDK, but the server does not support the1wand1Mtimeframes (error400210): both methods return a*ValidationErrorwithout calling the server. Request daily candles (GetOHLC1DayHistorical) and aggregate them on the client.
Historical parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
symbol | string | (required) | Stock symbol |
fromDate | string | (required) | Start date (yyyy/MM/dd) |
toDate | string | (required) | End date (yyyy/MM/dd) |
page | int | 1 | Page number |
size | int | 1000 | Records per page |
Returns: []OHLCData
To read every page in one call use GetOHLCAll(symbol, timeframe, query), or IterOHLC(symbol, timeframe, query, yield) to stream the candles through a callback (return false to stop).
3.1b. Download OHLC
| Method | Returns |
|---|---|
DownloadOHLC1Minute(symbol) | []OHLCData, error |
DownloadOHLC1Day(symbol) | []OHLCData, error |
Note: these methods are deprecated and currently do not return data: the server has no data-file endpoint yet, so they always return
market.ErrNotImplemented. To get history, use the...Historicalmethods above,GetOHLCAllorIterOHLC.
3.2. Market Indexes
// All indexes
indexes, err := data.MarketData.GetIndexes()
// By board
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/market"
indexes, err := data.MarketData.GetIndexesByBoard(market.BoardHOSE)
for _, idx := range indexes {
fmt.Printf("%s - %s\n", idx.Index, idx.IndexName)
}Returns: []MarketIndexes
3.3. Index Summary
// Current summary
summary, err := data.MarketData.GetIndexSummary("VNINDEX")
// Historical summary
summary, err := data.MarketData.GetIndexSummaryHistorical("VNINDEX", "2026/01/15")
// Board summary
summary, err := data.MarketData.GetBoardSummary(market.BoardHOSE)
// Historical board summary
summary, err := data.MarketData.GetBoardSummaryHistorical(market.BoardHOSE, "2026/01/15")Returns: *MarketIndexSummary. Some foreign-trading fields are pointers (nil when the server did not send them), see Models.
3.4. Securities Info
// Single symbol
info, err := data.MarketData.GetSecuritiesInfo("SSI")
if err == nil && info == nil {
fmt.Println("symbol not found") // nil when not found
}
// By index
securities, err := data.MarketData.GetSecuritiesInfoByIndex("VN30")
// By board
securities, err := data.MarketData.GetSecuritiesInfoByBoard(market.BoardHOSE)Returns: *SecuritiesInfo for a single symbol (nil when not found), []SecuritiesInfo for the list methods.
3.5. Securities Summary
// Current summary
summary, err := data.MarketData.GetSecuritiesSummary("SSI")
// Historical summary
summary, err := data.MarketData.GetSecuritiesSummaryHistorical(
"SSI", "2026/03/01", "2026/03/31",
)
// By index
summary, err := data.MarketData.GetSecuritiesSummaryByIndex("VN30")
// By index historical
summary, err := data.MarketData.GetSecuritiesSummaryByIndexHistorical(
"VN30", "2026/03/01", "2026/03/31",
)Returns: []SecuritiesSummary
Note:
Ceiling,FloorandRefPriceinSecuritiesSummaryare pointers and can benil. For ceiling, floor and reference price use master data (section 3.6).
3.6. Master Data
// Current master data
master, err := data.MarketData.GetMasterData()
// Historical master data
master, err = data.MarketData.GetMasterDataHistorical("2026/03/01", "2026/03/31")Returns: []MasterData (ceiling, floor and reference price per symbol, see Models).
3.7. Corporate Actions
Dividends, bonus shares, rights issues and other corporate actions (endpoint GET /api/v3/data/corporateActions). Added in 3.3.2. Like the other market data methods, these calls are blocking and take no context.
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/market"
// Today, every security (language "vn")
actions, err := data.MarketData.GetCorporateActions()
// One symbol, English event text
actions, err = data.MarketData.GetCorporateActions(
market.WithCorporateActionsSymbol("SSI"),
market.WithCorporateActionsLanguage("en"), // "vn" (default) or "en"
)
// A past date range (at most 5 days)
hist, err := data.MarketData.GetCorporateActionsHistorical(
"2026/10/19", "2026/10/24",
market.WithCorporateActionsSymbol("SSI"),
)
for _, a := range hist {
fmt.Printf("%s %s: ex-right=%s record=%s ratio=%s\n",
a.Symbol, a.EventTitle, a.ExrightDate, a.RecordDate, a.Ratio)
}| Method | Parameters | Returns | Description |
|---|---|---|---|
GetCorporateActions | opts ...CorporateActionsOption | []CorporateAction, error | Corporate actions of today |
GetCorporateActionsHistorical | fromDate, toDate string, opts ...CorporateActionsOption | []CorporateAction, error | Corporate actions in a past range. Dates are YYYY/MM/DD; toDate is at most 5 days after fromDate |
| Option | Default | Description |
|---|---|---|
WithCorporateActionsSymbol(symbol) | Not set (every security) | Limit the result to one symbol: alphanumeric, at most 20 characters |
WithCorporateActionsLanguage(language) | "vn" | Language of the event text: "vn" or "en" (the SDK trims it and converts it to lower case) |
The API is paginated. The SDK fetches every page (page size 100) and returns all rows combined.
Validation. Before any request, the SDK checks the date format, that fromDate is not after toDate, that the range is at most 5 days, the symbol and the language. A failure comes back as a *ssisdk.ValidationError and nothing is sent (see Error Handling).
Returns: []CorporateAction (Symbol, EventTitle, ExrightDate, RecordDate, IssueDate, Exchange, Ratio, see Models). Every field is a string exactly as the server sends it: dates stay YYYY/MM/DD text and Ratio is not parsed (its meaning depends on the event type). A field that is blank, null or missing (for example a newly announced event has no ex-right date or ratio yet) is "".
Note — not verified. The SDK changelog says these methods were written from the official API documentation and have not yet been run against the live server. Test against your environment before relying on them.
4. Portfolio (PortfolioService)
Access via t.Portfolio.
Note: the getters return pointers, and a slice or pointer is
nilwhen the account has no data (for example no positions). Check fornil/len(...) == 0before reading.
4.1. Account Balance
// Equity balance
balance, err := t.Portfolio.GetEquityBalance("YOUR_ACCOUNT_NO")
if err == nil && balance != nil {
fmt.Printf("Withdrawable: %.0f\n", balance.Withdrawable)
}
// Derivative balance
derBalance, err := t.Portfolio.GetDerivativeBalance("YOUR_DERIVATIVE_ACCOUNT_NO")4.2. Positions
// Equity positions
positions, err := t.Portfolio.GetEquityPositions("YOUR_ACCOUNT_NO")
for _, pos := range positions { // nil when empty
fmt.Printf("%s: %d shares | Cost: %.0f\n", pos.Symbol, pos.Quantity, pos.CostPrice)
}
// All derivative positions (*AllDerivativePosition)
derPositions, err := t.Portfolio.GetDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")
// Open derivative positions only
openPos, err := t.Portfolio.GetOpenDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")
// Closed derivative positions only
closedPos, err := t.Portfolio.GetClosedDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")4.3. Order Book
// Today's orders
orders, err := t.Portfolio.GetTodayOrders("YOUR_ACCOUNT_NO")
for _, order := range orders {
fmt.Printf("%s: %s %s %d@%.0f - %s\n",
order.OrderID, order.Symbol, order.Side, order.Quantity, order.Price, order.Status)
}
// Historical orders
orders, err = t.Portfolio.GetHistoricalOrders("YOUR_ACCOUNT_NO", "2026/01/01", "2026/01/31")For a market-priced order the server sends a word (ATO, ATC, MP...) instead of a number: Price is then 0 and PriceText holds the word.
4.4. PPMMR
// Equity PPMMR
ppmmr, err := t.Portfolio.GetEquityPPMMR("YOUR_ACCOUNT_NO")
// Derivative PPMMR
derPPMMR, err := t.Portfolio.GetDerivativePPMMR("YOUR_DERIVATIVE_ACCOUNT_NO")Portfolio Method Overview
| Group | Method | Returns |
|---|---|---|
| Balance | GetEquityBalance(accountNo) | *EquityAccountBalance |
GetDerivativeBalance(accountNo) | *DerivativeAccountBalance | |
| Positions | GetEquityPositions(accountNo) | []EquityPosition |
GetDerivativePositions(accountNo) | *AllDerivativePosition | |
GetOpenDerivativePositions(accountNo) | []DerivativePosition | |
GetClosedDerivativePositions(accountNo) | []DerivativePosition | |
| Order Book | GetTodayOrders(accountNo) | []Order |
GetHistoricalOrders(accountNo, fromDate, toDate) | []Order | |
| PPMMR | GetEquityPPMMR(accountNo) | *EquityPPMMR |
GetDerivativePPMMR(accountNo) | *DerivativePPMMR |
5. Trading (TradingService)
Access via t.Trading. Every write request needs Config.PrivateKey (the SDK signs the body with RSA-SHA256), see Configuration.
5.1. Place Orders
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/trading"
accountNo := "YOUR_ACCOUNT_NO"
// Limit order (LO)
result, err := t.Trading.PlaceLimitOrder(accountNo, "SSI", trading.OrderSideBuy, 100, 66000)
// Market order (MTL)
result, err = t.Trading.PlaceMarketOrder(accountNo, "SSI", trading.OrderSideBuy, 100)
// ATO order (at the open)
result, err = t.Trading.PlaceATOOrder(accountNo, "SSI", trading.OrderSideBuy, 100)
// ATC order (at the close)
result, err = t.Trading.PlaceATCOrder(accountNo, "SSI", trading.OrderSideSell, 100)
// Custom order type
result, err = t.Trading.PlaceOrder(
accountNo, "SSI", trading.OrderSideBuy, 100, 66000, trading.OrderTypeLO,
)
// Your own idempotency key (at most 20 characters)
result, err = t.Trading.PlaceLimitOrder(accountNo, "SSI", trading.OrderSideBuy, 100, 66000,
trading.WithClientRequestID("YOUR_CLIENT_REQUEST_ID"))
fmt.Printf("Order ID: %s, Status: %s\n", result.OrderID, result.Status)Warning: these are real orders. Run them only on an environment and account you are allowed to trade on.
5.2. Modify Orders
Only price or quantity can be modified (not both at once). Modify methods take no options.
// Modify price by client_request_id
result, err := t.Trading.ModifyOrderPrice(accountNo, "YOUR_CLIENT_REQUEST_ID", 68000)
// Modify price by order_id
result, err = t.Trading.ModifyOrderPriceByOrderID(accountNo, "YOUR_ORDER_ID", 68000)
// Modify quantity by client_request_id
result, err = t.Trading.ModifyOrderQuantity(accountNo, "YOUR_CLIENT_REQUEST_ID", 200)
// Modify quantity by order_id
result, err = t.Trading.ModifyOrderQuantityByOrderID(accountNo, "YOUR_ORDER_ID", 200)5.3. Cancel Orders
// Cancel by client_request_id
result, err := t.Trading.CancelOrder(accountNo, "YOUR_CLIENT_REQUEST_ID")
// Cancel by order_id
result, err = t.Trading.CancelOrderByOrderID(accountNo, "YOUR_ORDER_ID")To cancel many orders in one request, see 5.4. Batch Orders.
5.4. Batch Orders
PlaceBatchOrders (place) and CancelBatchOrders (cancel) send several orders in one request. The server returns a separate result for each order. Use them for a basket of orders or a mass cancel at the end of the session; for a single order use the per-order methods above.
Prerequisites
- Client:
ssi.NewTrading(auth), accessed throught.Trading. - Auth: call
auth.Authenticate(otp)with a valid OTP first (same as every trading method). Config.PrivateKey: required. Every write request carries anX-Signatureheader (RSA-SHA256) computed over the exact JSON body that is sent. The SDK signs for you.
| Method | HTTP | Endpoint |
|---|---|---|
PlaceBatchOrders | POST | /api/v3/trading/order/batch |
CancelBatchOrders | DELETE | /api/v3/trading/order/batch |
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
PlaceBatchOrders(orders) | []trading.BatchPlaceOrderItem | *trading.BatchOrderResponse, error | Place 1..Config.MaxBatchOrders orders in one request |
CancelBatchOrders(orders) | []trading.BatchCancelOrderItem | *trading.BatchOrderResponse, error | Cancel 1..Config.MaxBatchOrders orders in one request |
The field tables of BatchPlaceOrderItem, BatchCancelOrderItem, BatchOrderResponse and BatchOrderResult are in Models. Two rules to remember: each place item mirrors the arguments of PlaceOrder (ClientRequestID is at most 20 characters, generated by the SDK when empty), and each cancel item needs exactly one of OrderID or ClientRequestID (both, or neither, gives a *ValidationError).
Batch size limit. Config.MaxBatchOrders defaults to 20 (constant ssisdk.MaxBatchOrders). A value below 1 is normalized to 20. An empty batch, or one larger than the limit, is rejected by the SDK immediately with a *ValidationError and no network call. According to the SDK code comments, 20 is the server-side default. This was not confirmed against the API reference, which may state a different maximum (50): see Configuration and check with SSI before sending large batches.
How it behaves
- The whole batch is validated before sending. The SDK checks every order (required fields, quantity, price for
LO/PLO,ClientRequestIDlength, theOrderIDxorClientRequestIDrule). One bad order rejects the whole batch, and nothing has been sent. - Signed over the whole body. The body looks like
{"batchRequestId", "batchRequestTime", "orders": [...]}. TheX-Signatureis computed over exactly this JSON text. - Batch envelope.
batchRequestIdis a random string the SDK generates; the server rejects abatchRequestIdthat was already used (HTTP 409).batchRequestTimeis the current time in epoch milliseconds; if it falls outside the server's replay window, the server returns error code400111. Keep the clock of the machine running the SDK accurate (NTP). - Idempotency. Reusing a
ClientRequestIDon the same day makes the server return HTTP 409, which the SDK maps to*DuplicateRequestError. The SDK never re-sends an order under a new id, because the original order may still be live (constantssisdk.MaxClientRequestIDLength= 20). - No automatic retry.
POST/PUT/DELETErequests are not replayed on timeout or on 429. The error is returned to you. - Per-order results. A successful request does not mean every order in the batch was accepted. Always walk
Resultsand checkSuccess. A per-order failure is not returned as anerror; it is reported in that order'sErrorCode/ErrorMessage. - Result order. According to the code comments,
Resultsfollows the request order. This is not confirmed against a live server: match results byClientRequestIDrather than by position when you can. If the server sends noresults(or not a list),Resultsis an empty slice: comparelen(res.Results)with the number of orders you sent.
Warning: these are real orders. Run the examples only on an environment and account you are allowed to trade on.
Example 1: place a batch and read each order's result
package main
import (
"fmt"
"log"
"os"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/ssi"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/trading"
)
func main() {
config := ssi.NewConfig(os.Getenv("SSI_CLIENT_ID"))
config.APIKey = os.Getenv("SSI_API_KEY")
config.APISecret = os.Getenv("SSI_API_SECRET")
config.PrivateKey = os.Getenv("SSI_PRIVATE_KEY") // required to sign orders
a := ssi.NewAuth(config)
defer a.Close()
if _, err := a.Authenticate(os.Getenv("SSI_OTP")); err != nil {
log.Fatalf("Authenticate: %v", err)
}
t := ssi.NewTrading(a)
accountNo := "YOUR_ACCOUNT_NO"
items := []trading.BatchPlaceOrderItem{
{
AccountNo: accountNo,
Symbol: "SSI",
Side: trading.OrderSideBuy,
Quantity: 100,
Price: 25000, // use a valid price for the tick size
OrderType: trading.OrderTypeLO,
ClientRequestID: "BATCH-0001", // <= 20 chars; leave empty to auto-generate
},
{
AccountNo: accountNo,
Symbol: "HPG",
Side: trading.OrderSideBuy,
Quantity: 200,
Price: 26000,
OrderType: trading.OrderTypeLO,
ClientRequestID: "BATCH-0002",
},
}
res, err := t.Trading.PlaceBatchOrders(items)
if err != nil {
log.Fatalf("PlaceBatchOrders: %v", err) // classify with reportBatchError below
}
fmt.Printf("BatchRequestID: %s\n", res.BatchRequestID)
for i, r := range res.Results {
if r.Success {
fmt.Printf("#%d OK | %s | OrderID: %s | Status: %s\n",
i+1, r.ClientRequestID, r.OrderID, r.Status)
} else {
fmt.Printf("#%d FAIL | %s | %s: %s\n",
i+1, r.ClientRequestID, r.ErrorCode, r.ErrorMessage)
}
}
}Example 2: cancel a batch by ClientRequestID or OrderID
cancels := []trading.BatchCancelOrderItem{
{AccountNo: accountNo, ClientRequestID: "BATCH-0001"}, // cancel by the id you set
{AccountNo: accountNo, OrderID: "YOUR_ORDER_ID"}, // cancel by the server-issued id
}
cres, err := t.Trading.CancelBatchOrders(cancels)
if err != nil {
log.Printf("CancelBatchOrders: %v", err)
// You cannot be sure which orders were cancelled: check order status before acting further.
return
}
for i, r := range cres.Results {
fmt.Printf("#%d success=%v | Status: %s | %s %s\n",
i+1, r.Success, r.Status, r.ErrorCode, r.ErrorMessage)
}Error handling. All SDK errors work with errors.As:
import (
"errors"
"fmt"
ssisdk "github.com/SSI-Securities-Inc/ssi-sdk-go/v3"
)
func reportBatchError(op string, err error) {
var dup *ssisdk.DuplicateRequestError
var vErr *ssisdk.ValidationError
var rl *ssisdk.RateLimitError
switch {
case errors.As(err, &dup):
// HTTP 409: ClientRequestID or BatchRequestID already used today.
fmt.Printf("%s: duplicate id (409): %v\n", op, dup)
case errors.As(err, &vErr):
// Local error: the whole batch is rejected, nothing was sent to the server.
fmt.Printf("%s: invalid input: %v\n", op, vErr)
case errors.As(err, &rl):
wait := "unknown"
if rl.RetryAfterMs != nil {
wait = fmt.Sprintf("%d ms", *rl.RetryAfterMs)
}
fmt.Printf("%s: rate limited, retry after %s\n", op, wait)
default:
fmt.Printf("%s: error: %v\n", op, err)
}
}The table of batch errors (ValidationError, DuplicateRequestError, RateLimitError, AuthenticationError, APIError with 400111) and what to do for each is in Error Handling.
On a timeout or a dropped connection: because the SDK does not retry write requests, you cannot know whether the batch reached the system. Reconcile order status (for example through order events, see 6.5. Subscribe Trading Events) before resending. If you must resend, reuse the same ClientRequestID values instead of generating new ones, so the duplicate protection (409) still applies.
Related: Tutorials: Batch Orders, Order Status and Cancel Order. The same batch commands are also available over the Trading WebSocket client, see 7. Trading WebSocket.
5.5. Max Buy/Sell
// With specific price
price := 66000.0
maxBS, err := t.Trading.GetMaxBuySell(accountNo, "SSI", &price)
fmt.Printf("Max buy: %d, Max sell: %d\n", maxBS.MaxBuyQuantity, maxBS.MaxSellQuantity)
// At market price
maxBS, err = t.Trading.GetMaxBuySellAtMarketPrice(accountNo, "SSI")MarginRatio and PurchasingPower are pointers and are nil on derivative accounts.
Trading Method Overview
| Group | Method | Returns |
|---|---|---|
| Place | PlaceOrder(accountNo, symbol, side, qty, price, orderType, opts...) | *PlaceOrderResponse |
PlaceLimitOrder(accountNo, symbol, side, qty, price, opts...) | *PlaceOrderResponse | |
PlaceMarketOrder(accountNo, symbol, side, qty, opts...) | *PlaceOrderResponse | |
PlaceATOOrder(accountNo, symbol, side, qty, opts...) | *PlaceOrderResponse | |
PlaceATCOrder(accountNo, symbol, side, qty, opts...) | *PlaceOrderResponse | |
| Modify | ModifyOrderPrice(accountNo, clientRequestID, price) | *ModifyOrderResponse |
ModifyOrderPriceByOrderID(accountNo, orderID, price) | *ModifyOrderResponse | |
ModifyOrderQuantity(accountNo, clientRequestID, qty) | *ModifyOrderResponse | |
ModifyOrderQuantityByOrderID(accountNo, orderID, qty) | *ModifyOrderResponse | |
| Cancel | CancelOrder(accountNo, clientRequestID) | *CancelOrderResponse |
CancelOrderByOrderID(accountNo, orderID) | *CancelOrderResponse | |
| Batch | PlaceBatchOrders(orders []BatchPlaceOrderItem) | *BatchOrderResponse |
CancelBatchOrders(orders []BatchCancelOrderItem) | *BatchOrderResponse | |
| Max Buy/Sell | GetMaxBuySell(accountNo, symbol, price *float64) | *MaxBuySellResponse |
GetMaxBuySellAtMarketPrice(accountNo, symbol) | *MaxBuySellResponse | |
| Conditional Orders | PlaceFcoGtd(accountNo, symbol, side, qty, price, priceSlip, fromDate, toDate, opts...) | *FCOPlaceResponse |
PlaceFcoStop(accountNo, symbol, side, qty, stopPrice, operator, fromDate, toDate, opts...) | *FCOPlaceResponse | |
PlaceFcoStopLimit(accountNo, symbol, side, qty, price, priceSlip, stopPrice, operator, fromDate, toDate, opts...) | *FCOPlaceResponse | |
PlaceFcoTrailingStop(accountNo, symbol, side, qty, activePrice, trailingAmount, fromDate, toDate, opts...) | *FCOPlaceResponse | |
PlaceFcoTrailingStopLimit(accountNo, symbol, side, qty, activePrice, trailingAmount, priceSlip, fromDate, toDate, opts...) | *FCOPlaceResponse | |
PlaceFcoOco(accountNo, symbol, side, qty, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, opts...) | *FCOPlaceResponse | |
PlaceFcoBullBear(accountNo, symbol, side, qty, price, priceSlip, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, opts...) | *FCOPlaceResponse | |
CancelFco(fcoID, opts...) | *FCOCancelResponse | |
GetFcoByAccountNo(accountNo, pageIndex, pageSize) | *FCOListResponse | |
GetFcoBySymbol(accountNo, symbol, pageIndex, pageSize) | *FCOListResponse | |
GetFcoByStatus(accountNo, status, pageIndex, pageSize) | *FCOListResponse | |
GetFcoByStatuses(accountNo, statuses []FCOStatus, pageIndex, pageSize) | *FCOListResponse | |
GetFcoByDate(accountNo, fromDate, toDate, pageIndex, pageSize) | *FCOListResponse | |
GetFcoById(accountNo, fcoID) | *FCOInfo | |
GetFcoOrderBook(fcoID, pageIndex, pageSize) | *FCOOrderBookResponse | |
GetFcoStatusHistory(fcoID) | []FCOStatusHistoryItem |
5.6. Conditional Orders (FCO)
The SDK supports 7 flexible conditional order types (FCO), FCO list lookups, FCO order book history, FCO status history, and FCO cancellation.
The fromDate / toDate strings use the format YYYY/MM/DD HH:MM:SS (a bare date YYYY/MM/DD is also accepted), and the window must be at most 31 days. Where a price is interface{} (for example price in PlaceFcoGtd, tpPrice / slPrice in PlaceFcoOco), pass a number or an order-type word such as trading.OrderTypeMTL (a market word carries no slip).
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/trading"
accountNo := "YOUR_ACCOUNT_NO"
fromDate := "2026/07/01 00:00:00"
toDate := "2026/07/21 23:59:59"
// 1. GTD order (Good Till Date)
gtd, err := t.Trading.PlaceFcoGtd(
accountNo, "SSI", trading.OrderSideBuy, 100, 25000, 500, fromDate, toDate,
)
// 2. Stop Market order
stop, err := t.Trading.PlaceFcoStop(
accountNo, "SSI", trading.OrderSideSell, 100, 24000, trading.FCOOperatorLesserOrEqual, fromDate, toDate,
)
// 3. Stop Limit order
stopLimit, err := t.Trading.PlaceFcoStopLimit(
accountNo, "SSI", trading.OrderSideBuy, 100, 25500, 500, 25000, trading.FCOOperatorGreaterOrEqual, fromDate, toDate,
)
// 4. Trailing Stop order
trailing, err := t.Trading.PlaceFcoTrailingStop(
accountNo, "SSI", trading.OrderSideBuy, 100, 26000, 1000, fromDate, toDate,
)
// 5. Trailing Stop Limit order
trailingLimit, err := t.Trading.PlaceFcoTrailingStopLimit(
accountNo, "SSI", trading.OrderSideBuy, 100, 26000, 1000, 500, fromDate, toDate,
)
// 6. OCO order (One-Cancels-the-Other)
oco, err := t.Trading.PlaceFcoOco(
accountNo, "SSI", trading.OrderSideSell, 100, 30000, 24000, trading.OrderTypeMTL, trading.OrderTypeMTL, 500, 500, fromDate, toDate,
)
// 7. Bull Bear order
bullBear, err := t.Trading.PlaceFcoBullBear(
accountNo, "SSI", trading.OrderSideBuy, 1000, 100.5, 10, 30, 20, 30, 10, 0.1, 0, fromDate, toDate,
)
// Cancel a conditional order
cancelRes, err := t.Trading.CancelFco(gtd.FCOID)Returns: FCOPlaceResponse (FCOID) when placing an order, FCOCancelResponse (FCOID) when cancelling.
Optional OTP code. Every PlaceFco* method and CancelFco accepts an optional trading.WithOTPCode(code) for accounts that require a one-time code on conditional orders. Without it the request is sent with no code.
cancelRes, err = t.Trading.CancelFco(gtd.FCOID, trading.WithOTPCode(os.Getenv("SSI_FCO_OTP")))Queries:
// Get all FCO orders for an account
fcoList, err := t.Trading.GetFcoByAccountNo(accountNo, 1, 10)
// Get FCO orders by symbol
fcoList, err = t.Trading.GetFcoBySymbol(accountNo, "SSI", 1, 10)
// Get FCO orders by processing status
fcoList, err = t.Trading.GetFcoByStatus(accountNo, trading.FCOStatusTrit, 1, 10)
// Get FCO orders matching any of several statuses
fcoList, err = t.Trading.GetFcoByStatuses(accountNo,
[]trading.FCOStatus{trading.FCOStatusWait, trading.FCOStatusTri}, 1, 10)
// Get FCO orders by date range
fcoList, err = t.Trading.GetFcoByDate(accountNo, "2026/07/01", "2026/07/21", 1, 10)
// Get a single FCO order by ID
fcoInfo, err := t.Trading.GetFcoById(accountNo, "YOUR_FCO_ID")
// Get the order book / execution log for an FCO order
orderBook, err := t.Trading.GetFcoOrderBook("YOUR_FCO_ID", 1, 10)
// Get the state transitions of an FCO order
history, err := t.Trading.GetFcoStatusHistory("YOUR_FCO_ID") // []FCOStatusHistoryItemNote: all list-query methods take
pageIndex/pageSizefor pagination.PagesCountin a list response can miss the last partial page: page until a page comes back empty, or useItemsCount.
Note:
SubscribeFcoOrderStatuswas removed from the stream client in 3.3.0. FCO events arrive on theorder.<account>topic: see 6.5. Subscribe Trading Events.
6. Realtime Streaming (StreamingService)
Access via s.Streaming (type *stream.Service). Call s.Connect() first.
Prerequisites
- Client:
ssi.NewStream(auth). - Auth: call
auth.Authenticate(otp)with a valid OTP beforeConnect. The stream takes its token from theTokenManagerof thisAuth. - Each client may have at most 10 sockets open at once (
ssisdk.WSServerMaxConnections). Beyond that the server refuses with 429. - A request frame is at most 4000 bytes (
ssisdk.WSMaxFrameBytes); the SDK splits a long request into several frames for you.
6.1. Connection
s := ssi.NewStream(auth)
defer s.Disconnect()
if err := s.Connect(); err != nil {
log.Fatal(err)
}
// Wait until disconnected
s.Wait(nil)
// Wait with timeout (milliseconds)
timeoutMs := 30000
s.Wait(&timeoutMs)6.2. Setup Callbacks
SetOnData receives every DATA-channel message already parsed into a typed value. The typed callbacks below receive only their own message type; they do not subscribe to anything, so you still call the matching subscribe method. Pass nil to clear a callback. A callback that panics is recovered and logged, and does not affect the others.
| Method | Callback | Receives |
|---|---|---|
SetOnData | func(interface{}) | Every DATA-channel message (typed, or the raw map[string]interface{} when the topic has no model, including acks) |
SetOnTrading | func(interface{}) | Every TRADING-channel message |
SetOnHeartbeat | func(*stream.HeartbeatMessage) | HEARTBEAT-channel messages |
SetOnMarketFlag | func(*stream.MarketFlagMessage) | Only MarketFlagMessage |
SetOnIndexSummary | func(*stream.IndexSummaryMessage) | Only IndexSummaryMessage |
SetOnOrder | func(*stream.OrderStatusMessage) | Only OrderStatusMessage |
SetOnOrderMatch | func(*stream.OrderMatchMessage) | Only OrderMatchMessage (fills) |
SetOnPortfolio | func(*stream.PortfolioMessage) | Only PortfolioMessage |
SetOnConnection | func(transport.ConnectionEvent) | Connection state changes, see 6.9 |
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/stream"
s.Streaming.SetOnData(func(msg interface{}) {
switch m := msg.(type) {
case *stream.TradeMessage:
fmt.Printf("[TRADE] %s | Price: %.0f | Qty: %d\n", m.Symbol, m.Price, m.Quantity)
case *stream.QuoteMessage:
fmt.Printf("[QUOTE] %s | Bids: %v | Asks: %v\n", m.Symbol, m.BidPrices, m.AskPrices)
case *stream.ForeignRoomMessage:
fmt.Printf("[ROOM] %s | BuyQty: %d | SellQty: %d\n", m.Symbol, m.BuyQuantity, m.SellQuantity)
case *stream.MarketStatusMessage:
fmt.Printf("[MARKET] %s | Status: %s\n", m.Market, m.Status)
case *stream.MarketDataMessage:
if m.RefPrice != nil { // nil = the server did not send this key
fmt.Printf("[MASTER] %s | Ref: %.0f\n", m.Symbol, *m.RefPrice)
}
case *stream.PutMessage:
fmt.Printf("[PUT] %s | %.0f x %d\n", m.Symbol, m.Price, m.Quantity)
case *stream.OddLotMessage:
fmt.Printf("[ODDLOT] %s | %.0f x %d\n", m.Symbol, m.Price, m.Quantity)
case *stream.IntervalMessage:
fmt.Printf("[OHLCV] %s | O=%.0f H=%.0f L=%.0f C=%.0f V=%d\n",
m.Symbol, m.Open, m.High, m.Low, m.Close, m.Volume)
case *stream.IndexTickMessage:
if m.Value != nil {
fmt.Printf("[INDEX] %s | Value: %.2f\n", m.Symbol, *m.Value)
}
}
})
s.Streaming.SetOnTrading(func(msg interface{}) {
switch m := msg.(type) {
case *stream.OrderStatusMessage:
fmt.Printf("[ORDER] %s %s | Status: %s\n", m.Symbol, m.Side, m.Status)
case *stream.OrderMatchMessage:
fmt.Printf("[MATCH] %s %s | %.0f x %d\n", m.Symbol, m.Side, m.MatchPrice, m.MatchQty)
case *stream.FCOOrderUpdateMessage:
fmt.Printf("[FCO ORDER] %s | ProcessStatus: %s\n", m.Symbol, m.ProcessStatus)
case *stream.PortfolioMessage:
// Derivative accounts only. TotalAsset, CashBalance and StockValue are always nil.
fmt.Printf("[PORTFOLIO] %s | %s | Net: %d\n", m.AccountNo, m.Symbol, m.Net)
}
})
s.Streaming.SetOnHeartbeat(func(msg *stream.HeartbeatMessage) {
fmt.Printf("[HEARTBEAT] %v\n", msg)
})6.3. Subscribe Market Data
import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/market"
// All channels (trade + quote + room) for symbols
s.Streaming.SubscribeSymbol([]string{"SSI", "HPG", "VIC"}, nil)
// Individual channels
s.Streaming.SubscribeSymbolTrade([]string{"SSI", "HPG"}, nil)
s.Streaming.SubscribeSymbolQuote([]string{"SSI", "HPG"}, nil)
s.Streaming.SubscribeSymbolRoom([]string{"SSI", "HPG"}, nil)
s.Streaming.SubscribeSymbolPutThrough([]string{"SSI"}, nil)
s.Streaming.SubscribeSymbolOddLot([]string{"SSI"}, nil)
// OHLCV by timeframe
s.Streaming.SubscribeSymbolOhlcv([]string{"SSI"}, market.TimeframeMinute1, nil)
// Master data (ceiling / floor / reference price)
s.Streaming.SubscribeMarket([]string{"SSI"}, nil)
// By board
s.Streaming.SubscribeBoard([]market.Board{market.BoardHOSE, market.BoardHNX}, nil)
// Constituent symbols of an index (not the index itself, see 6.8)
s.Streaming.SubscribeIndex([]string{"VNINDEX", "VN30"}, nil)Index ticks, 1-minute and 5-minute index candles, index summaries and session flags are in 6.8. Index subscriptions and interval rules.
6.4. Unsubscribe
s.Streaming.UnsubscribeSymbol([]string{"SSI", "HPG"})
s.Streaming.UnsubscribeSymbolTrade([]string{"SSI"})
s.Streaming.UnsubscribeSymbolQuote([]string{"SSI"})
s.Streaming.UnsubscribeSymbolRoom([]string{"SSI"})
s.Streaming.UnsubscribeSymbolPutThrough([]string{"SSI"})
s.Streaming.UnsubscribeSymbolOddLot([]string{"SSI"})
s.Streaming.UnsubscribeSymbolOhlcv([]string{"SSI"}, market.TimeframeMinute1)
s.Streaming.UnsubscribeMarket([]string{"SSI"})
s.Streaming.UnsubscribeBoard([]market.Board{market.BoardHOSE})
s.Streaming.UnsubscribeIndex([]string{"VNINDEX"})Every Subscribe* method has an Unsubscribe* counterpart. To remove everything at once, see UnsubscribeAll in 6.10.
6.5. Subscribe Trading Events
// Order status and fills: all accounts (an empty account means "*")
s.Streaming.SubscribeOrderStatus("", nil)
// For a specific account
s.Streaming.SubscribeOrderStatus("YOUR_ACCOUNT_NO", nil)
// Portfolio (derivative position updates): all accounts
s.Streaming.SubscribePortfolio("", nil)
// For a specific account
s.Streaming.SubscribePortfolio("YOUR_ACCOUNT_NO", nil)
// Unsubscribe
s.Streaming.UnsubscribeOrderStatus("YOUR_ACCOUNT_NO")
s.Streaming.UnsubscribePortfolio("YOUR_ACCOUNT_NO")Conditional order (FCO) events. There is no separate FCO subscription: SubscribeFcoOrderStatus was removed in 3.3.0. FCO updates arrive on the same order.<account> topic as *stream.FCOOrderUpdateMessage (EventType == "fcoEvent"), through SetOnTrading. Subscribe with SubscribeOrderStatus.
6.6. Heartbeat
// Single ping (no callback, no repeat)
s.Streaming.Ping(nil, nil)
// Repeating ping on an interval (milliseconds; first ping after one interval)
intervalMs := 30000
s.Streaming.Ping(nil, &intervalMs)
// Stop automatic ping loop
s.Streaming.StopPingLoop()An interval <= 0 is rejected.
6.7. Connection Status
if s.Streaming.IsConnected() {
fmt.Println("WebSocket connected")
}
fmt.Println(s.Streaming.ReconnectCount()) // how many times the connection was re-established
if err := s.Streaming.LastError(); err != nil {
fmt.Printf("last connection error: %v\n", err)
}6.8. Index subscriptions and interval rules
Use these methods to follow VNINDEX, VN30, HNXINDEX... continuously instead of calling REST repeatedly. The REST list of indices is covered in the tutorial Market Indices. In the tables below, indices is a list of index codes (for example "VNINDEX", "VN30"), and onResponse is a handler for the server's ack (transport.MessageHandler); pass nil if you do not need it.
Subscribe and unsubscribe indices
| Method | Parameters | Returns | Description |
|---|---|---|---|
SubscribeIndexTrade (alias SubscribeIndexTicks) | indices []string, interval market.Timeframe, onResponse | error | Ticks of the index itself (trade.index.<CODE>). With interval 1m / 5m the data arrives as candles |
UnsubscribeIndexTrade (alias UnsubscribeIndexTicks) | indices []string, interval market.Timeframe | error | Remove the topic above. Pass the same interval you subscribed with |
SubscribeIndexSummary | indices []string, onResponse | error | Index summary (indexsummary.<CODE>) |
UnsubscribeIndexSummary | indices []string | error | Remove the topic above |
SubscribeIndex (alias SubscribeIndexConstituents) | indices []string, onResponse | error | Trade, quote and room of the index's constituent symbols, sent as trade.<CODE>@*, quote.<CODE>@*, room.<CODE>@* |
UnsubscribeIndex (alias UnsubscribeIndexConstituents) | indices []string | error | Remove the three @* topics above |
SubscribeMarketFlag (alias SubscribeSessionFlag) | onResponse | error | Session flags (ATO, LO, ATC...) of all boards (market.flag) |
UnsubscribeMarketFlag (alias UnsubscribeSessionFlag) | none | error | Remove the topic above |
SubscribeIndex differs from SubscribeIndexTrade: SubscribeIndex gives data of the constituent symbols, while SubscribeIndexTrade gives the ticks of the index itself.
Not verified: according to the SDK CHANGELOG, the
@*mechanism used bySubscribeIndex(fan-out of a whole basket / exchange) has not been confirmed against a live server. Try it in your own environment before relying on it.
Handlers. Index ticks have no dedicated handler: receive them through SetOnData and use a type switch on *stream.IndexTickMessage (ticks) and *stream.IntervalMessage (candles). Dedicated handlers exist for *stream.IndexSummaryMessage (SetOnIndexSummary) and *stream.MarketFlagMessage (SetOnMarketFlag), see 6.2. The message fields are in Models. Pointer fields (*float64) are nil when the server did not send that key, which is different from a value of 0.
Not verified: the server sends abbreviated keys for index ticks, and the SDK inferred the meaning of each
IndexTickMessagefield from the abbreviation. If you need to be sure, readRaw(a copy of the original object).
Topic and interval rules
| Rule | Detail |
|---|---|
| Valid intervals | stream.IntervalTick ("tick"), stream.IntervalMinute1 ("1m"), stream.IntervalMinute5 ("5m"). Anything else is a *ValidationError |
| Intervals apply only to | trade and trade.index |
| Interval on an index | Needs one concrete code, no wildcard (*) |
| No interval | Pass the empty string "" to subscribe trade.index.<CODE> with no suffix |
| Index codes | Upper-cased automatically. UPCOMINDEX is rewritten to HNXUPCOMINDEX |
| Filter | Each element is *, a code, or a range A-B. Do not put a comma-separated string in one element; pass several elements in the slice |
| Empty list | *ValidationError (at least one code is required) |
Interval * (stream.StreamIntervalAll) | Used internally by the SDK for SubscribeIndex / SubscribeBoard (@*) |
Topics are built like this:
| Method | Topic sent to the server | Message received |
|---|---|---|
SubscribeIndexTrade(["VNINDEX"], stream.IntervalTick, nil) | trade.index.VNINDEX@tick | *stream.IndexTickMessage |
SubscribeIndexTrade(["VNINDEX"], "", nil) | trade.index.VNINDEX | *stream.IndexTickMessage |
SubscribeIndexTrade(["VNINDEX"], stream.IntervalMinute1, nil) | trade.index.VNINDEX@1m | *stream.IntervalMessage (candle) |
SubscribeIndexSummary(["VN30"], nil) | indexsummary.VN30 | *stream.IndexSummaryMessage |
SubscribeIndex(["VN30"], nil) | trade.VN30@*, quote.VN30@*, room.VN30@* | *stream.TradeMessage, *stream.QuoteMessage, *stream.ForeignRoomMessage |
SubscribeMarketFlag(nil) | market.flag | *stream.MarketFlagMessage |
Example 1: follow VNINDEX ticks and connection events
package main
import (
"fmt"
"log"
"os"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/ssi"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/stream"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/transport"
)
func main() {
config := ssi.NewConfig(os.Getenv("SSI_CLIENT_ID"))
config.APIKey = os.Getenv("SSI_API_KEY")
config.APISecret = os.Getenv("SSI_API_SECRET")
a := ssi.NewAuth(config)
defer a.Close()
if _, err := a.Authenticate(os.Getenv("SSI_OTP")); err != nil {
log.Fatalf("Authenticate: %v", err)
}
s := ssi.NewStream(a)
defer s.Disconnect()
s.Streaming.SetOnConnection(func(e transport.ConnectionEvent) {
fmt.Printf("connection: state=%s reconnects=%d\n", e.State, e.ReconnectCount)
if e.Err != nil {
fmt.Printf("connection error: %v\n", e.Err)
}
})
s.Streaming.SetOnData(func(msg interface{}) {
switch m := msg.(type) {
case *stream.IndexTickMessage:
if m.Value != nil { // nil = the server did not send this key
fmt.Printf("%s value=%.2f\n", m.Symbol, *m.Value)
}
case *stream.IntervalMessage:
fmt.Printf("%s candle %s O=%.2f H=%.2f L=%.2f C=%.2f V=%d\n",
m.Symbol, m.IntervalTime, m.Open, m.High, m.Low, m.Close, m.Volume)
}
})
if err := s.Connect(); err != nil {
log.Fatalf("Connect: %v", err)
}
// Ticks of the index itself.
if err := s.Streaming.SubscribeIndexTrade([]string{"VNINDEX"}, stream.IntervalTick, nil); err != nil {
log.Fatalf("SubscribeIndexTrade: %v", err) // ValidationError if interval/topic is wrong
}
waitMs := 60000
s.Wait(&waitMs) // wait at most 60 seconds (illustration only)
}Example 2: 1-minute candles, index summaries and session flags
// A 1-minute candle needs exactly one concrete code, no wildcard.
if err := s.Streaming.SubscribeIndexTrade([]string{"VN30"}, stream.IntervalMinute1, nil); err != nil {
log.Printf("SubscribeIndexTrade 1m: %v", err)
}
s.Streaming.SetOnIndexSummary(func(m *stream.IndexSummaryMessage) {
// The fields are not publicly listed: use Get with a key you know, or inspect Data.
fmt.Printf("summary %s: %d keys\n", m.Index, len(m.Data))
})
if err := s.Streaming.SubscribeIndexSummary([]string{"VNINDEX", "VN30"}, nil); err != nil {
log.Printf("SubscribeIndexSummary: %v", err)
}
// Session flags of all boards.
s.Streaming.SetOnMarketFlag(func(m *stream.MarketFlagMessage) {
fmt.Printf("flag: board=%s flag=%s\n", m.Board, m.Flag)
})
_ = s.Streaming.SubscribeMarketFlag(nil)Errors. A *ssisdk.ValidationError is returned by the subscribe methods for an unknown topic type or invalid filter, an empty list, an interval other than tick / 1m / 5m, an interval on a topic that does not support it, or an interval on an index with a wildcard. Nothing is sent in that case.
import (
"errors"
ssisdk "github.com/SSI-Securities-Inc/ssi-sdk-go/v3"
)
if err := s.Streaming.SubscribeIndexTrade([]string{"VNINDEX"}, "10s", nil); err != nil {
var vErr *ssisdk.ValidationError
if errors.As(err, &vErr) {
log.Printf("bad input, nothing sent: %v", vErr)
}
}See Error Handling for the other streaming errors.
6.9. Connection events and reconnect
Config.AutoReconnect defaults to true, so when the socket drops:
- The SDK reconnects with exponential backoff and jitter, starting from
Config.RetryDelayMs, capped at 60000 ms. - Every connection attempt uses a fresh token; the SDK also reconnects proactively about 60000 ms before the token expires.
- After a reconnect, subscriptions are restored automatically.
- If the server answers 429 on connect, the SDK waits at least 30000 ms.
- An unrecoverable error (for example a new OTP is needed) stops reconnecting.
ConnectionEvent (transport.ConnectionEvent) has State, ReconnectCount and Err. The State values:
| State | Meaning |
|---|---|
"connected" | First connection |
"reconnected" | Re-opened after a drop, subscriptions restored |
"disconnected" | The socket just dropped; a reconnect follows if AutoReconnect is on |
"failed" | Unrecoverable (Err says why), no reconnect will happen. You need to authenticate again (for example with a new OTP) |
s.Streaming.SetOnConnection(func(e transport.ConnectionEvent) {
switch e.State {
case "failed":
log.Printf("stream failed: %v (authenticate again with a new OTP)", e.Err)
default:
log.Printf("stream state=%s reconnects=%d", e.State, e.ReconnectCount)
}
})Errors after the connection is up (network drop, token expiry) are not returned by the subscribe methods: watch them through SetOnConnection (State, Err) and LastError(). When State == "failed", the SDK no longer reconnects. See Configuration for AutoReconnect and RetryDelayMs.
6.10. Subscription management
| Method | Parameters | Returns | Description |
|---|---|---|---|
ListSubscription | timeoutMs int | map[string][]string, error | Ask the server which topics this connection is subscribed to. The result has the keys "data" and "trading"; a channel with nothing subscribed is an empty slice. timeoutMs <= 0 means 5000 |
UnsubscribeAll | none | map[string][]string, error | Ask the server, then remove exactly those topics (including topics left by an earlier run of the same session). Returns the topics removed |
Ping | onResponse, intervalMs *int | error | See 6.6 |
StopPingLoop | none | none | Stop the background ping loop |
IsConnected | none | bool | The socket is connected |
LastError | none | error | The last connection error, or nil |
ReconnectCount | none | int | How many times the connection was re-established |
// See what this connection is subscribed to.
current, err := s.Streaming.ListSubscription(5000) // 5000 ms
if err != nil {
log.Printf("ListSubscription: %v", err) // *WebSocketError on timeout
} else {
fmt.Printf("data topics: %d, trading topics: %d\n",
len(current["data"]), len(current["trading"]))
}
// Remove one specific group: pass the same interval you subscribed with.
_ = s.Streaming.UnsubscribeIndexTrade([]string{"VN30"}, stream.IntervalMinute1)
// Or remove everything the server holds for this connection.
removed, err := s.Streaming.UnsubscribeAll()
if err != nil {
log.Printf("UnsubscribeAll: %v", err)
} else {
fmt.Printf("removed data topics: %d\n", len(removed["data"]))
}A *ssisdk.WebSocketError from ListSubscription / UnsubscribeAll means the reply timed out or a frame failed to send: check IsConnected() and retry once the connection is stable.
Related: Tutorials: WebSocket Market Data, Advanced WebSocket and Market Indices.
Message types via SetOnData
| Topic | Message Type | Description |
|---|---|---|
trade.* | *stream.TradeMessage | Trade/match data |
quote.* | *stream.QuoteMessage | Bid/ask data |
room.* | *stream.ForeignRoomMessage | Foreign room |
market.<symbol> | *stream.MarketDataMessage | Master data (ceiling, floor, reference price) |
market.* (legacy shape) | *stream.MarketStatusMessage | Market status |
market.flag | *stream.MarketFlagMessage | Session flag |
put.* | *stream.PutMessage | Put-through deals |
oddlot.* | *stream.OddLotMessage | Odd lot |
trade.<symbol>@1m / @5m | *stream.IntervalMessage | OHLCV interval data |
trade.index.<code> | *stream.IndexTickMessage | Index tick |
trade.index.<code>@1m / @5m | *stream.IntervalMessage | Index candle |
indexsummary.* | *stream.IndexSummaryMessage | Index summary |
A topic with no model (including acks) is passed as the raw map[string]interface{}.
Message types via SetOnTrading
The type is chosen by data.eventType:
| Topic | Message Type | Description |
|---|---|---|
order.* | *stream.OrderStatusMessage | Regular order status updates (orderEvent) |
*stream.OrderMatchMessage | Fills (orderMatchEvent) | |
*stream.FCOOrderUpdateMessage | Conditional order updates (fcoEvent) | |
portfolio.* | *stream.PortfolioMessage | Derivative position changes (clientPortfolioEvent) |
Streaming Method Overview
| Group | Method | Description |
|---|---|---|
| Connection | Connect() | Connect WebSocket |
Disconnect() | Disconnect | |
Wait(timeoutMs *int) | Wait for data (nil waits until disconnected) | |
IsConnected() | Check connection status | |
LastError() / ReconnectCount() | Last connection error / reconnect count | |
| Heartbeat | Ping(onResponse, intervalMs *int) | Ping server (single ping if intervalMs is nil) |
StopPingLoop() | Stop automatic ping loop | |
| Symbol | SubscribeSymbol(symbols, callback) | Trade + Quote + Room |
SubscribeSymbolTrade(symbols, callback) | Trade only | |
SubscribeSymbolQuote(symbols, callback) | Quote only | |
SubscribeSymbolRoom(symbols, callback) | Foreign room only | |
SubscribeSymbolPutThrough(symbols, callback) | Put-through only | |
SubscribeSymbolOddLot(symbols, callback) | Odd lot only | |
SubscribeSymbolOhlcv(symbols, interval, callback) | OHLCV by timeframe | |
SubscribeMarket(symbols, callback) | Master data (alias SubscribeMasterData) | |
| Board/Index | SubscribeBoard(boards, callback) | By board |
SubscribeIndex(indices, callback) | Constituent symbols of an index | |
SubscribeIndexTrade(indices, interval, callback) | Ticks / candles of the index itself | |
SubscribeIndexSummary(indices, callback) | Index summary | |
SubscribeMarketFlag(callback) | Session flags | |
| Trading | SubscribeOrderStatus(accountNo, callback) | Order status, fills and FCO events (empty = all accounts) |
SubscribePortfolio(accountNo, callback) | Portfolio changes (empty = all accounts) | |
| Unsubscribe | UnsubscribeSymbol(symbols) | Unsubscribe all channels |
UnsubscribeSymbolTrade(symbols) | Unsubscribe trade | |
UnsubscribeSymbolQuote(symbols) | Unsubscribe quote | |
UnsubscribeSymbolRoom(symbols) | Unsubscribe room | |
UnsubscribeSymbolPutThrough(symbols) | Unsubscribe put-through | |
UnsubscribeSymbolOddLot(symbols) | Unsubscribe odd lot | |
UnsubscribeSymbolOhlcv(symbols, interval) | Unsubscribe OHLCV | |
UnsubscribeMarket(symbols) | Unsubscribe master data | |
UnsubscribeBoard(boards) | Unsubscribe board | |
UnsubscribeIndex(indices) | Unsubscribe index constituents | |
UnsubscribeIndexTrade(indices, interval) | Unsubscribe index ticks / candles (same interval) | |
UnsubscribeIndexSummary(indices) | Unsubscribe index summary | |
UnsubscribeMarketFlag() | Unsubscribe session flags | |
UnsubscribeOrderStatus(accountNo) / UnsubscribePortfolio(accountNo) | Unsubscribe trading events | |
UnsubscribeAll() | Remove every topic of this connection | |
| Subscriptions | ListSubscription(timeoutMs) | List the topics held by the server |
7. Trading WebSocket
The tradingws package is a client for the Trading WebSocket (/ws/v3/trading): send commands (place, amend, cancel, batch, conditional orders), query the account and receive order events on one socket. It is not wired into the Trading / Stream facades; create it from an *ssi.Auth. Use it when you want to send orders over a long-lived connection instead of REST calls (5. Trading), or receive acks and order events on the same socket.
Migrating from 3.3.1 or earlier: until 3.3.1 this client lived in the
experimentalpackage. Since 3.3.2 it is the public packagetradingws, with the same names, signatures, defaults and behaviour. The old package was removed without an alias, so an old import no longer compiles. Change the import togithub.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingwsand the qualifierexperimental.totradingws.(for exampletradingws.TradingWS(ctx, auth)).TradingWSErrorstays in the root packagessisdk. The client is still an explicit opt-in:TradingandStreamdo not connect to it.
7.1. Connecting and options
Prerequisites
- Import:
github.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingws. - Auth: call
auth.Authenticate(otp)on anssi.NewAuth(config)client first.tradingws.TradingWStakes an*ssi.Authand asks theTokenManagerfor a valid token on everyConnect. Config.PrivateKey: required for commands that change state (place, amend, cancel, batch,fco.place,fco.cancel). Without a private key the SDK sends nothing (fail closed). Queries are not signed.- Token scopes (the server answers 403 when one is missing):
- Orders:
tradingws:order:post,:put,:delete(place, amend, cancel, batch). - Conditional orders:
tradingws:fco:post,:delete,:get. - Queries:
tradingws:position:get,:maxBuySell:get,:accountBalance:get,:ppmmrAccount:get,:orderBook:get.
- Orders:
- URL:
Config.TradingWSURL()(built fromConfig.TradingWSDomain, see Configuration). Onlywss://is accepted: the client refuses any other URL because the bearer token travels in the header.
ws, err := tradingws.TradingWS(ctx, a,
tradingws.WithRequestTimeoutMs(15000),
tradingws.WithRespectRateLimit(true),
tradingws.WithMaxRateLimitWaitMs(5000),
)
if err != nil {
return err
}
defer ws.Close() // required: closes the socket and wakes every waiting requestTradingWS(ctx, auth, opts...) creates the client, connects and returns an *tradingws.Service. Defaults come from Config: request timeout = Config.TimeoutMs (or 10000 if unset), User-Agent = Config.UserAgent; opts override them.
The server accepts the socket first (HTTP 101) and only then refuses with a {"code": 401|403|429|500, "msg": ...} frame. So Connect waits a short grace period and returns a *ssisdk.TradingWSError if a refusal arrives.
Options. Every duration is an integer in milliseconds with an Ms suffix.
| Option | Default | Description |
|---|---|---|
WithRequestTimeoutMs(ms) | 10000 (through TradingWS: Config.TimeoutMs) | How long to wait for a response before giving up. No retry |
WithSignatureEncoding(e) | EncodingAuto | EncodingAuto, EncodingBase64 or EncodingHex. Any other value is rejected with a *ValidationError |
WithUserAgent(ua) | The SDK User-Agent (or Config.UserAgent through TradingWS) | User-Agent of the upgrade request. The gateway firewall answers 403 to a bare library User-Agent |
WithHandshakeGraceMs(ms) | 500 | How long Connect waits for the refusal frame after HTTP 101. 0 skips the wait |
WithRespectRateLimit(on) | true | When remaining reaches 0, the client waits for resetMs instead of getting a 429 |
WithMaxRateLimitWaitMs(ms) | 2000 | Upper bound on that wait |
WithPingIntervalMs(ms) | 30000 | Send ping_pong this often to notice a hung connection early. 0 disables |
7.2. Orders
Each method builds and validates the payload like the REST trading.Service (device id, idempotency key, plain decimal prices). Order methods return the immediate ack (orderStatus = PD); the real outcome arrives later through the order.<account> event stream.
| Method | Parameters | Returns | Description |
|---|---|---|---|
PlaceOrder | ctx, accountNo, symbol string, side trading.OrderSide, quantity int, price float64, orderType trading.OrderType, clientRequestID string | *trading.PlaceOrderResponse, error | Place an order (order.place). clientRequestID is at most 20 characters; if empty the SDK generates one |
AmendOrder | ctx, accountNo string, price *float64, quantity *int, orderID, clientRequestID string | *trading.ModifyOrderResponse, error | Change the price or the quantity (not both), identifying the order by exactly one of orderID / clientRequestID (order.amend) |
CancelOrder | ctx, accountNo, orderID, clientRequestID string | *trading.CancelOrderResponse, error | Cancel one order, exactly one of orderID / clientRequestID (order.cancel) |
BatchNewOrders | ctx, []trading.BatchPlaceOrderItem | *tradingws.Response, error | Place up to Config.MaxBatchOrders orders (order.batchNew), validated as a whole. result.results lists clientRequestId, orderStatus, success |
BatchCancelOrders | ctx, []trading.BatchCancelOrderItem | *tradingws.Response, error | Cancel up to Config.MaxBatchOrders orders (order.batchCancel) |
The batch item types are the same as in REST, see 5.4. Batch Orders.
7.3. Conditional orders (FCO)
| Method | Parameters | Returns | Description |
|---|---|---|---|
PlaceFCO | ctx, trading.FCOParamsRequest | *trading.FCOPlaceResponse, error | Place a conditional order (fco.place), using the same params types as REST |
CancelFCO | ctx, fcoID, code string | *trading.FCOCancelResponse, error | Cancel a conditional order (fco.cancel). code is the one-time code (OTP) when the account requires one; omitted when empty |
ListFCO | ctx, *trading.FCOListRequest | *tradingws.Response, error | List conditional orders (fco.list). ProcessStatus takes at most one status |
FCOOrderBook | ctx, fcoID string, pageIndex, pageSize int | *tradingws.Response, error | Execution log of one conditional order (fco.orderbook). Page defaults to 1 / 10 |
FCOStatusHistory | ctx, fcoID string | *tradingws.Response, error | State transitions of one conditional order (fco.statusHistory) |
7.4. Queries
| Method | Parameters | Returns | Description |
|---|---|---|---|
QueryPosition | ctx, accountNo string, querySummary bool | *Response, error | Positions of an account (query.position); querySummary adds the account summary |
QueryMaxBuySell | ctx, accountNo, symbol string, price *float64 | *Response, error | Maximum buy/sell quantity (query.maxBuySell). With price == nil the server uses its reference price |
QueryAccountBalance | ctx, accountNo string | *Response, error | Cash balance (query.accountBalance) |
QueryPPMMRAccount | ctx, accountNo string | *Response, error | Purchasing power (query.ppmmrAccount) |
QueryOrderBook | ctx, tradingws.OrderBookQuery | *Response, error | Order book of an account (query.orderBook) |
OrderBookQuery and *tradingws.Response (ID, Method, Status, Result, RateLimit, ReceivedTime) are described in Models.
7.5. Events (subscribe)
| Method | Parameters | Description |
|---|---|---|
SubscribeOrderEvents | accountNo string | Receive order.<account> (order status, fills). data.eventType: orderEvent / orderMatchEvent |
UnsubscribeOrderEvents | accountNo string | Remove the subscription above |
SubscribePortfolioEvents | accountNo string | Receive portfolio.<account> (derivative position updates, eventType = clientPortfolioEvent) |
UnsubscribePortfolioEvents | accountNo string | Remove the subscription above |
UnsubscribeAllEvents | none | Drop every trading subscription on this socket |
Subscribe to one account at a time. An empty account, or one containing * or ,, is rejected immediately (*ValidationError) before anything is sent, because the server refuses wildcards and accounts the token does not own.
Events arrive at the Client().SetOnEvent(...) handler as {"channel","topic","data"} frames; the handler also receives subscribe acks and ping_pong acks (frames with no command id). A panic in the handler is recovered and logged; it does not crash the client.
7.6. Other methods
| Method | Description |
|---|---|
Service.Command(ctx, method, params) | Send any command and return its first response frame (no retry). Use it when the server has a method the SDK does not wrap yet |
Service.Client() | Returns the low-level *tradingws.Client |
Service.Close() | Close the socket |
Client.Connect(ctx) | Reconnect. Does nothing if already connected |
Client.IsConnected() | The socket is open and the reader is running |
Client.Request(ctx, method, params) | Send a command and return the first ack |
Client.RequestStream(ctx, method, params) | Send a command and return a *Call to read several frames with the same id (for example the PD ack, then the 200 frame carrying the event). Read with Call.Next(ctx, timeoutMs), release with Call.Close() |
Client.Subscribe(channel, topics) / Unsubscribe(channel, topics) | Low-level subscription frames |
Client.Ping() | Send PING_PONG; the ack arrives at OnEvent |
Client.RateLimit() | Copy of the latest quota the server reported (remaining, resetMs, ...) |
Client.Rejection() | The post-upgrade refusal (*TradingWSError) the server sent, if any |
Client.SetOnEvent(fn) | Set the handler for frames without a command id. nil clears it |
tradingws.IsMutating(method) | Whether a method needs a signature |
7.7. How it behaves
- Frame format.
{"id":"<non-empty>","method":"order.place","params":{...},"signature":"<base64 or hex>","timestamp":<epoch ms>}. For commands that change state,signatureis computed (RSA-SHA256) over the exact text ofparams; that text is serialized only once and inserted verbatim into the frame. Queries carry no signature. - Signature encoding. The default is
auto: sign as base64; if the server refuses the signature with401006,401008or401013(the command was not executed), send it once more as hex and remember whichever works. If both are refused, the error asks you to check thatConfig.PrivateKeyis the key registered for this API key. To fix the encoding, useWithSignatureEncoding. - Ack and event. One command id can receive several frames: an immediate ack (
orderStatus=PD), later a status 200 frame carrying the event.Requestreturns only the first frame; useRequestStreamif you need the later ones. The final outcome of an order always arrives throughSubscribeOrderEvents. - No retry. A command that times out is not re-sent because it may already have been accepted. The only exception is the hex re-sign above (when the command did not run).
- Rate limit. According to the code comments: 50 commands per window for orders and 100 for queries. These figures come from comments in the SDK code, not from a published server specification; check them against the server documentation. Each response carries
rateLimit; whenremainingreaches 0, the client waits forresetMs(at mostWithMaxRateLimitWaitMs) ifWithRespectRateLimit(true). Usews.Client().RateLimit()to see the current quota. - No automatic reconnect. Unlike the
streampackage, this client does not reconnect or resubscribe by itself. If the connection drops (IsConnected()returnsfalse), callws.Client().Connect(ctx)(the token provider fetches a valid token each time) and subscribe again. - Context. Every method takes a
ctx. Ifctxis cancelled or expires, the method returnsctx.Err()right away (different from the internal request timeout). If you cancelctxafter a command was already sent, the server may still have accepted it. - No sensitive data in logs. Params, signatures, tokens and account numbers are never logged and never carried in an error.
Relation to the stream package
stream (facade ssi.NewStream) | tradingws (Trading WebSocket) | |
|---|---|---|
| Purpose | Receive market data and events (subscribe, read-only) | Send orders, run queries, and receive order events |
| Reconnect | Reconnects and resubscribes automatically | No, call Client().Connect(ctx) |
| Durations | Integer Ms | Integer Ms |
The two packages use separate sockets and endpoints. If you only need to watch order events and do not place orders over the socket, use stream (6.5. Subscribe Trading Events).
7.8. Examples
Warning: these are real orders. Run them only on an environment and account you are allowed to trade on.
Example 1: connect, subscribe to events and place an order
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/ssi"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/trading"
"github.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingws"
)
func main() {
config := ssi.NewConfig(os.Getenv("SSI_CLIENT_ID"))
config.APIKey = os.Getenv("SSI_API_KEY")
config.APISecret = os.Getenv("SSI_API_SECRET")
config.PrivateKey = os.Getenv("SSI_PRIVATE_KEY") // required to sign orders
a := ssi.NewAuth(config)
defer a.Close()
if _, err := a.Authenticate(os.Getenv("SSI_OTP")); err != nil {
log.Fatalf("Authenticate: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
ws, err := tradingws.TradingWS(ctx, a,
tradingws.WithRequestTimeoutMs(15000),
tradingws.WithRespectRateLimit(true),
tradingws.WithMaxRateLimitWaitMs(5000),
)
if err != nil {
log.Fatalf("TradingWS: %v", err)
}
defer ws.Close()
// Events and acks without a command id arrive here.
// Print only the channel and eventType, not the whole frame.
ws.Client().SetOnEvent(func(frame map[string]interface{}) {
data, _ := frame["data"].(map[string]interface{})
fmt.Printf("event: channel=%v eventType=%v\n", frame["channel"], data["eventType"])
})
accountNo := "YOUR_ACCOUNT_NO"
if err := ws.SubscribeOrderEvents(accountNo); err != nil {
log.Fatalf("SubscribeOrderEvents: %v", err)
}
placed, err := ws.PlaceOrder(ctx, accountNo, "SSI", trading.OrderSideBuy,
100, 25000, trading.OrderTypeLO, "WS-0001")
if err != nil {
reportWSError("PlaceOrder", err) // see 7.9
return
}
// This is only the ack (usually PD). The real outcome arrives as an order event.
fmt.Printf("ack: OrderID=%s Status=%s\n", placed.OrderID, placed.Status)
time.Sleep(5 * time.Second) // wait for a few events, then exit (illustration only)
}Example 2: query and read the quota
book, err := ws.QueryOrderBook(ctx, tradingws.OrderBookQuery{
AccountNo: accountNo,
FromDate: "2026/01/01",
ToDate: "2026/01/01",
Symbol: "SSI",
PageIndex: 1,
PageSize: 20,
})
if err != nil {
reportWSError("QueryOrderBook", err)
return
}
fmt.Printf("status=%d method=%s\n", book.Status, book.Method)
if result, ok := book.Result.(map[string]interface{}); ok {
fmt.Printf("keys in result: %d\n", len(result))
}
// Latest quota the server reported (remaining, resetMs, ...).
fmt.Printf("rateLimit: %v\n", ws.Client().RateLimit())Example 3: reconnect when the socket drops
if !ws.Client().IsConnected() {
if err := ws.Client().Connect(ctx); err != nil {
reportWSError("Connect", err)
return
}
// Subscriptions are not restored automatically.
if err := ws.SubscribeOrderEvents(accountNo); err != nil {
log.Printf("resubscribe: %v", err)
}
// A command sent before the drop may have been accepted: reconcile with
// QueryOrderBook or order events before sending it again.
}7.9. Error handling
import (
"context"
"errors"
"fmt"
ssisdk "github.com/SSI-Securities-Inc/ssi-sdk-go/v3"
)
func reportWSError(op string, err error) {
var wsErr *ssisdk.TradingWSError
var vErr *ssisdk.ValidationError
var connErr *ssisdk.WebSocketError
switch {
case errors.As(err, &wsErr):
// The server refused: Status, ErrorCode, ServerMessage.
// Status 429 is a rate limit; 401/403 is token or scope.
fmt.Printf("%s: server refused status=%d code=%s: %s\n",
op, wsErr.Status, wsErr.ErrorCode, wsErr.ServerMessage)
case errors.As(err, &vErr):
fmt.Printf("%s: invalid input (nothing was sent): %v\n", op, vErr)
case errors.Is(err, context.DeadlineExceeded), errors.Is(err, context.Canceled):
fmt.Printf("%s: context ended: %v\n", op, err)
case errors.As(err, &connErr):
// Timeout (not retried), lost connection, or not connected.
fmt.Printf("%s: websocket error: %v\n", op, connErr)
default:
fmt.Printf("%s: error: %v\n", op, err)
}
}| Error | When | What to do |
|---|---|---|
*ssisdk.TradingWSError (Status, ErrorCode, ServerMessage) | A server error frame (status 4xx or "error", for example FCO_ERROR for fco.*), or a post-upgrade refusal frame (401 token, 403 missing tradingws:* scope, 429, 500) | Read Status and ErrorCode. For 401, re-authenticate; for 403, check scopes; 429 is a rate limit, wait according to the quota (Client().RateLimit()) and resend in a controlled way; for 409 caused by a reused clientRequestID, do not re-place under a new id until you know whether the original order is live |
*ssisdk.ValidationError | Bad input (empty account or wildcard, clientRequestID over 20 characters, invalid signature encoding, params that are not valid JSON, ...) | Fix the input. Nothing was sent |
*ssisdk.WebSocketError | Not connected, send failure, connection lost before a response, timeout waiting for a response (not retried), a URL that is not wss://, missing private key for a state-changing command | For a timeout: the command may have been accepted, reconcile through order events or QueryOrderBook before resending |
context.DeadlineExceeded / context.Canceled | ctx expired or was cancelled | Treat like a timeout: do not assume the command was not accepted |
The WebSocket client returns rate limiting as a *TradingWSError with Status 429, not as *RateLimitError (that type is REST-only). A refused signature (401006, 401008, 401013) in auto mode is re-sent once with the other encoding; if it still fails, check Config.PrivateKey or fix the encoding with WithSignatureEncoding. See Error Handling for the full list.
Related: Tutorial: Trading WebSocket, Client Classes.