Go

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 (Ms suffix). Error types come from the root package ssisdk (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 token

Authenticate(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:

MethodTimeframe
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):

MethodTimeframe
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: GetOHLC1WeekHistorical and GetOHLC1MonthHistorical still exist in the SDK, but the server does not support the 1w and 1M timeframes (error 400210): both methods return a *ValidationError without calling the server. Request daily candles (GetOHLC1DayHistorical) and aggregate them on the client.

Historical parameters:

ParameterTypeDefaultDescription
symbolstring(required)Stock symbol
fromDatestring(required)Start date (yyyy/MM/dd)
toDatestring(required)End date (yyyy/MM/dd)
pageint1Page number
sizeint1000Records 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

MethodReturns
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 ...Historical methods above, GetOHLCAll or IterOHLC.

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, Floor and RefPrice in SecuritiesSummary are pointers and can be nil. 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)
}
MethodParametersReturnsDescription
GetCorporateActionsopts ...CorporateActionsOption[]CorporateAction, errorCorporate actions of today
GetCorporateActionsHistoricalfromDate, toDate string, opts ...CorporateActionsOption[]CorporateAction, errorCorporate actions in a past range. Dates are YYYY/MM/DD; toDate is at most 5 days after fromDate
OptionDefaultDescription
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 nil when the account has no data (for example no positions). Check for nil / len(...) == 0 before 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

GroupMethodReturns
BalanceGetEquityBalance(accountNo)*EquityAccountBalance
GetDerivativeBalance(accountNo)*DerivativeAccountBalance
PositionsGetEquityPositions(accountNo)[]EquityPosition
GetDerivativePositions(accountNo)*AllDerivativePosition
GetOpenDerivativePositions(accountNo)[]DerivativePosition
GetClosedDerivativePositions(accountNo)[]DerivativePosition
Order BookGetTodayOrders(accountNo)[]Order
GetHistoricalOrders(accountNo, fromDate, toDate)[]Order
PPMMRGetEquityPPMMR(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 through t.Trading.
  • Auth: call auth.Authenticate(otp) with a valid OTP first (same as every trading method).
  • Config.PrivateKey: required. Every write request carries an X-Signature header (RSA-SHA256) computed over the exact JSON body that is sent. The SDK signs for you.
MethodHTTPEndpoint
PlaceBatchOrdersPOST/api/v3/trading/order/batch
CancelBatchOrdersDELETE/api/v3/trading/order/batch

Methods

MethodParametersReturnsDescription
PlaceBatchOrders(orders)[]trading.BatchPlaceOrderItem*trading.BatchOrderResponse, errorPlace 1..Config.MaxBatchOrders orders in one request
CancelBatchOrders(orders)[]trading.BatchCancelOrderItem*trading.BatchOrderResponse, errorCancel 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, ClientRequestID length, the OrderID xor ClientRequestID rule). One bad order rejects the whole batch, and nothing has been sent.
  • Signed over the whole body. The body looks like {"batchRequestId", "batchRequestTime", "orders": [...]}. The X-Signature is computed over exactly this JSON text.
  • Batch envelope. batchRequestId is a random string the SDK generates; the server rejects a batchRequestId that was already used (HTTP 409). batchRequestTime is the current time in epoch milliseconds; if it falls outside the server's replay window, the server returns error code 400111. Keep the clock of the machine running the SDK accurate (NTP).
  • Idempotency. Reusing a ClientRequestID on 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 (constant ssisdk.MaxClientRequestIDLength = 20).
  • No automatic retry. POST / PUT / DELETE requests 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 Results and check Success. A per-order failure is not returned as an error; it is reported in that order's ErrorCode / ErrorMessage.
  • Result order. According to the code comments, Results follows the request order. This is not confirmed against a live server: match results by ClientRequestID rather than by position when you can. If the server sends no results (or not a list), Results is an empty slice: compare len(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

GroupMethodReturns
PlacePlaceOrder(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
ModifyModifyOrderPrice(accountNo, clientRequestID, price)*ModifyOrderResponse
ModifyOrderPriceByOrderID(accountNo, orderID, price)*ModifyOrderResponse
ModifyOrderQuantity(accountNo, clientRequestID, qty)*ModifyOrderResponse
ModifyOrderQuantityByOrderID(accountNo, orderID, qty)*ModifyOrderResponse
CancelCancelOrder(accountNo, clientRequestID)*CancelOrderResponse
CancelOrderByOrderID(accountNo, orderID)*CancelOrderResponse
BatchPlaceBatchOrders(orders []BatchPlaceOrderItem)*BatchOrderResponse
CancelBatchOrders(orders []BatchCancelOrderItem)*BatchOrderResponse
Max Buy/SellGetMaxBuySell(accountNo, symbol, price *float64)*MaxBuySellResponse
GetMaxBuySellAtMarketPrice(accountNo, symbol)*MaxBuySellResponse
Conditional OrdersPlaceFcoGtd(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") // []FCOStatusHistoryItem

Note: all list-query methods take pageIndex / pageSize for pagination. PagesCount in a list response can miss the last partial page: page until a page comes back empty, or use ItemsCount.

Note: SubscribeFcoOrderStatus was removed from the stream client in 3.3.0. FCO events arrive on the order.<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 before Connect. The stream takes its token from the TokenManager of this Auth.
  • 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.

MethodCallbackReceives
SetOnDatafunc(interface{})Every DATA-channel message (typed, or the raw map[string]interface{} when the topic has no model, including acks)
SetOnTradingfunc(interface{})Every TRADING-channel message
SetOnHeartbeatfunc(*stream.HeartbeatMessage)HEARTBEAT-channel messages
SetOnMarketFlagfunc(*stream.MarketFlagMessage)Only MarketFlagMessage
SetOnIndexSummaryfunc(*stream.IndexSummaryMessage)Only IndexSummaryMessage
SetOnOrderfunc(*stream.OrderStatusMessage)Only OrderStatusMessage
SetOnOrderMatchfunc(*stream.OrderMatchMessage)Only OrderMatchMessage (fills)
SetOnPortfoliofunc(*stream.PortfolioMessage)Only PortfolioMessage
SetOnConnectionfunc(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

MethodParametersReturnsDescription
SubscribeIndexTrade (alias SubscribeIndexTicks)indices []string, interval market.Timeframe, onResponseerrorTicks of the index itself (trade.index.<CODE>). With interval 1m / 5m the data arrives as candles
UnsubscribeIndexTrade (alias UnsubscribeIndexTicks)indices []string, interval market.TimeframeerrorRemove the topic above. Pass the same interval you subscribed with
SubscribeIndexSummaryindices []string, onResponseerrorIndex summary (indexsummary.<CODE>)
UnsubscribeIndexSummaryindices []stringerrorRemove the topic above
SubscribeIndex (alias SubscribeIndexConstituents)indices []string, onResponseerrorTrade, quote and room of the index's constituent symbols, sent as trade.<CODE>@*, quote.<CODE>@*, room.<CODE>@*
UnsubscribeIndex (alias UnsubscribeIndexConstituents)indices []stringerrorRemove the three @* topics above
SubscribeMarketFlag (alias SubscribeSessionFlag)onResponseerrorSession flags (ATO, LO, ATC...) of all boards (market.flag)
UnsubscribeMarketFlag (alias UnsubscribeSessionFlag)noneerrorRemove 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 by SubscribeIndex (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 IndexTickMessage field from the abbreviation. If you need to be sure, read Raw (a copy of the original object).

Topic and interval rules

RuleDetail
Valid intervalsstream.IntervalTick ("tick"), stream.IntervalMinute1 ("1m"), stream.IntervalMinute5 ("5m"). Anything else is a *ValidationError
Intervals apply only totrade and trade.index
Interval on an indexNeeds one concrete code, no wildcard (*)
No intervalPass the empty string "" to subscribe trade.index.<CODE> with no suffix
Index codesUpper-cased automatically. UPCOMINDEX is rewritten to HNXUPCOMINDEX
FilterEach 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:

MethodTopic sent to the serverMessage 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:

StateMeaning
"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

MethodParametersReturnsDescription
ListSubscriptiontimeoutMs intmap[string][]string, errorAsk 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
UnsubscribeAllnonemap[string][]string, errorAsk the server, then remove exactly those topics (including topics left by an earlier run of the same session). Returns the topics removed
PingonResponse, intervalMs *interrorSee 6.6
StopPingLoopnonenoneStop the background ping loop
IsConnectednoneboolThe socket is connected
LastErrornoneerrorThe last connection error, or nil
ReconnectCountnoneintHow 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

TopicMessage TypeDescription
trade.**stream.TradeMessageTrade/match data
quote.**stream.QuoteMessageBid/ask data
room.**stream.ForeignRoomMessageForeign room
market.<symbol>*stream.MarketDataMessageMaster data (ceiling, floor, reference price)
market.* (legacy shape)*stream.MarketStatusMessageMarket status
market.flag*stream.MarketFlagMessageSession flag
put.**stream.PutMessagePut-through deals
oddlot.**stream.OddLotMessageOdd lot
trade.<symbol>@1m / @5m*stream.IntervalMessageOHLCV interval data
trade.index.<code>*stream.IndexTickMessageIndex tick
trade.index.<code>@1m / @5m*stream.IntervalMessageIndex candle
indexsummary.**stream.IndexSummaryMessageIndex 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:

TopicMessage TypeDescription
order.**stream.OrderStatusMessageRegular order status updates (orderEvent)
*stream.OrderMatchMessageFills (orderMatchEvent)
*stream.FCOOrderUpdateMessageConditional order updates (fcoEvent)
portfolio.**stream.PortfolioMessageDerivative position changes (clientPortfolioEvent)

Streaming Method Overview

GroupMethodDescription
ConnectionConnect()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
HeartbeatPing(onResponse, intervalMs *int)Ping server (single ping if intervalMs is nil)
StopPingLoop()Stop automatic ping loop
SymbolSubscribeSymbol(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/IndexSubscribeBoard(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
TradingSubscribeOrderStatus(accountNo, callback)Order status, fills and FCO events (empty = all accounts)
SubscribePortfolio(accountNo, callback)Portfolio changes (empty = all accounts)
UnsubscribeUnsubscribeSymbol(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
SubscriptionsListSubscription(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 experimental package. Since 3.3.2 it is the public package tradingws, 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 to github.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingws and the qualifier experimental. to tradingws. (for example tradingws.TradingWS(ctx, auth)). TradingWSError stays in the root package ssisdk. The client is still an explicit opt-in: Trading and Stream do 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 an ssi.NewAuth(config) client first. tradingws.TradingWS takes an *ssi.Auth and asks the TokenManager for a valid token on every Connect.
  • 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.
  • URL: Config.TradingWSURL() (built from Config.TradingWSDomain, see Configuration). Only wss:// 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 request

TradingWS(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.

OptionDefaultDescription
WithRequestTimeoutMs(ms)10000 (through TradingWS: Config.TimeoutMs)How long to wait for a response before giving up. No retry
WithSignatureEncoding(e)EncodingAutoEncodingAuto, 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)500How long Connect waits for the refusal frame after HTTP 101. 0 skips the wait
WithRespectRateLimit(on)trueWhen remaining reaches 0, the client waits for resetMs instead of getting a 429
WithMaxRateLimitWaitMs(ms)2000Upper bound on that wait
WithPingIntervalMs(ms)30000Send 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.

MethodParametersReturnsDescription
PlaceOrderctx, accountNo, symbol string, side trading.OrderSide, quantity int, price float64, orderType trading.OrderType, clientRequestID string*trading.PlaceOrderResponse, errorPlace an order (order.place). clientRequestID is at most 20 characters; if empty the SDK generates one
AmendOrderctx, accountNo string, price *float64, quantity *int, orderID, clientRequestID string*trading.ModifyOrderResponse, errorChange the price or the quantity (not both), identifying the order by exactly one of orderID / clientRequestID (order.amend)
CancelOrderctx, accountNo, orderID, clientRequestID string*trading.CancelOrderResponse, errorCancel one order, exactly one of orderID / clientRequestID (order.cancel)
BatchNewOrdersctx, []trading.BatchPlaceOrderItem*tradingws.Response, errorPlace up to Config.MaxBatchOrders orders (order.batchNew), validated as a whole. result.results lists clientRequestId, orderStatus, success
BatchCancelOrdersctx, []trading.BatchCancelOrderItem*tradingws.Response, errorCancel 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)

MethodParametersReturnsDescription
PlaceFCOctx, trading.FCOParamsRequest*trading.FCOPlaceResponse, errorPlace a conditional order (fco.place), using the same params types as REST
CancelFCOctx, fcoID, code string*trading.FCOCancelResponse, errorCancel a conditional order (fco.cancel). code is the one-time code (OTP) when the account requires one; omitted when empty
ListFCOctx, *trading.FCOListRequest*tradingws.Response, errorList conditional orders (fco.list). ProcessStatus takes at most one status
FCOOrderBookctx, fcoID string, pageIndex, pageSize int*tradingws.Response, errorExecution log of one conditional order (fco.orderbook). Page defaults to 1 / 10
FCOStatusHistoryctx, fcoID string*tradingws.Response, errorState transitions of one conditional order (fco.statusHistory)

7.4. Queries

MethodParametersReturnsDescription
QueryPositionctx, accountNo string, querySummary bool*Response, errorPositions of an account (query.position); querySummary adds the account summary
QueryMaxBuySellctx, accountNo, symbol string, price *float64*Response, errorMaximum buy/sell quantity (query.maxBuySell). With price == nil the server uses its reference price
QueryAccountBalancectx, accountNo string*Response, errorCash balance (query.accountBalance)
QueryPPMMRAccountctx, accountNo string*Response, errorPurchasing power (query.ppmmrAccount)
QueryOrderBookctx, tradingws.OrderBookQuery*Response, errorOrder book of an account (query.orderBook)

OrderBookQuery and *tradingws.Response (ID, Method, Status, Result, RateLimit, ReceivedTime) are described in Models.

7.5. Events (subscribe)

MethodParametersDescription
SubscribeOrderEventsaccountNo stringReceive order.<account> (order status, fills). data.eventType: orderEvent / orderMatchEvent
UnsubscribeOrderEventsaccountNo stringRemove the subscription above
SubscribePortfolioEventsaccountNo stringReceive portfolio.<account> (derivative position updates, eventType = clientPortfolioEvent)
UnsubscribePortfolioEventsaccountNo stringRemove the subscription above
UnsubscribeAllEventsnoneDrop 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

MethodDescription
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, signature is computed (RSA-SHA256) over the exact text of params; 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 with 401006, 401008 or 401013 (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 that Config.PrivateKey is the key registered for this API key. To fix the encoding, use WithSignatureEncoding.
  • Ack and event. One command id can receive several frames: an immediate ack (orderStatus = PD), later a status 200 frame carrying the event. Request returns only the first frame; use RequestStream if you need the later ones. The final outcome of an order always arrives through SubscribeOrderEvents.
  • 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; when remaining reaches 0, the client waits for resetMs (at most WithMaxRateLimitWaitMs) if WithRespectRateLimit(true). Use ws.Client().RateLimit() to see the current quota.
  • No automatic reconnect. Unlike the stream package, this client does not reconnect or resubscribe by itself. If the connection drops (IsConnected() returns false), call ws.Client().Connect(ctx) (the token provider fetches a valid token each time) and subscribe again.
  • Context. Every method takes a ctx. If ctx is cancelled or expires, the method returns ctx.Err() right away (different from the internal request timeout). If you cancel ctx after 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)
PurposeReceive market data and events (subscribe, read-only)Send orders, run queries, and receive order events
ReconnectReconnects and resubscribes automaticallyNo, call Client().Connect(ctx)
DurationsInteger MsInteger 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)
    }
}
ErrorWhenWhat 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.ValidationErrorBad 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.WebSocketErrorNot 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 commandFor a timeout: the command may have been accepted, reconcile through order events or QueryOrderBook before resending
context.DeadlineExceeded / context.Canceledctx expired or was cancelledTreat 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.

On this page