Go

API Services

Chi tiết các method API cho mỗi service trong Go SDK

Các ví dụ dùng giá trị giữ chỗ (YOUR_OTP, YOUR_ACCOUNT_NO, YOUR_ORDER_ID, YOUR_CLIENT_REQUEST_ID). Hãy đọc OTP, khóa và số tài khoản từ biến môi trường hoặc kho bí mật; đừng ghi cứng vào mã nguồn và đừng log token. Mọi khoảng thời gian trong SDK là số nguyên tính bằng millisecond (hậu tố Ms). Các loại error nằm ở package gốc ssisdk (github.com/SSI-Securities-Inc/ssi-sdk-go/v3), xem Xử lý lỗi.

1. Xác thực (TokenManager)

Truy cập qua auth.TokenManager hoặc trực tiếp trên auth (được uỷ quyền).

Xác thực với OTP

token, err := auth.Authenticate(os.Getenv("SSI_OTP")) // YOUR_OTP
if err != nil {
    log.Fatal(err)
}
_ = token // *auth.Token; đừng in hoặc log access token

Authenticate(otp, txID...) nhận thêm transaction ID tuỳ chọn (dùng cho Smart OTP). Authenticate("") chỉ dùng cho dữ liệu thị trường (không có quyền giao dịch).

Yêu cầu gửi OTP

// Map thô
result, err := auth.RequestOTP()

// Response có kiểu: Message và TransactionID
otpRes, err := auth.RequestOTPTyped()

Xác thực và chờ Smart OTP

// Trả về chuỗi access token
accessToken, err := auth.EnsureAuthenticated(os.Getenv("SSI_OTP"))

// Với tuỳ chọn (chu kỳ poll và số lần thử tối đa)
interval := 5000
retries := 12
accessToken, err = auth.EnsureAuthenticatedWith(ctx, authpkg.EnsureOptions{
    OTP:            os.Getenv("SSI_OTP"),
    PollIntervalMs: &interval,
    PollMaxRetries: &retries,
})

authpkg là alias import của github.com/SSI-Securities-Inc/ssi-sdk-go/v3/auth. Giá trị mặc định lấy từ Config.OTPPollIntervalMs (5000) và Config.OTPPollMaxWaitMs (60000), xem Cấu hình.

Làm mới token

token, err := auth.Refresh()

Kiểm tra trạng thái token

if auth.TokenManager.IsTokenExpired() { // lệch 30 s
    auth.Refresh()
}

accessToken := auth.AccessToken()

Các method khác trên *ssi.Auth: Config, Token, SetToken, HasRefreshToken, CanRefresh, Close.


2. Tài khoản (AccountService)

Truy cập qua t.Account.

Lấy danh sách tài khoản

accounts, err := t.Account.GetAccountInfo()

for _, acc := range accounts {
    accType := "unknown"
    if acc.AccountType != nil { // con trỏ: nil khi server không gửi loại tài khoản
        accType = string(*acc.AccountType)
    }
    fmt.Printf("%s (%s)\n", acc.AccountNo, accType)
}

Trả về: []Account — mỗi Account có AccountNo string và AccountType *account.AccountType (giá trị đã biết: Cash, Margin, Derivative; giá trị lạ từ server được giữ nguyên dạng chuỗi).


3. Dữ liệu thị trường (MarketDataService)

Truy cập qua data.MarketData. Không cần OTP.

3.1. Dữ liệu OHLC (nến)

Trong ngày:

MethodKhung thời gian
GetOHLC1Minute(symbol)1 phút
GetOHLC3Minute(symbol)3 phút
GetOHLC5Minute(symbol)5 phút
GetOHLC15Minute(symbol)15 phút
GetOHLC1Hour(symbol)1 giờ
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)
}

Không có GetOHLC1Day: nến ngày dùng GetOHLC1DayHistorical.

Lịch sử (với khoảng ngày + phân trang):

MethodKhung thời gian
GetOHLC1MinuteHistorical(symbol, fromDate, toDate, page, size)1 phút
GetOHLC3MinuteHistorical(...)3 phút
GetOHLC5MinuteHistorical(...)5 phút
GetOHLC15MinuteHistorical(...)15 phút
GetOHLC1HourHistorical(...)1 giờ
GetOHLC1DayHistorical(...)1 ngày
ohlc, err := data.MarketData.GetOHLC1DayHistorical(
    "SSI", "2026/03/27", "2026/04/22", 1, 100,
)

Lưu ý: GetOHLC1WeekHistorical và GetOHLC1MonthHistorical vẫn còn trong SDK, nhưng server không hỗ trợ khung 1w và 1M (lỗi 400210): cả hai method trả *ValidationError mà không gọi server. Hãy lấy nến ngày (GetOHLC1DayHistorical) rồi tự gộp phía client.

Tham số lịch sử:

Tham sốKiểuMặc địnhMô tả
symbolstring(bắt buộc)Mã chứng khoán
fromDatestring(bắt buộc)Ngày bắt đầu (yyyy/MM/dd)
toDatestring(bắt buộc)Ngày kết thúc (yyyy/MM/dd)
pageint1Số trang
sizeint1000Số bản ghi mỗi trang

Trả về: []OHLCData

Để đọc hết mọi trang trong một lần gọi, dùng GetOHLCAll(symbol, timeframe, query), hoặc IterOHLC(symbol, timeframe, query, yield) để nhận từng nến qua callback (trả false để dừng).

3.1b. Download OHLC

MethodTrả về
DownloadOHLC1Minute(symbol)[]OHLCData, error
DownloadOHLC1Day(symbol)[]OHLCData, error

Lưu ý: hai method này đã deprecated và hiện không trả dữ liệu: server chưa có endpoint tải file dữ liệu, nên chúng luôn trả market.ErrNotImplemented. Để lấy lịch sử, dùng các method ...Historical ở trên, GetOHLCAll hoặc IterOHLC.

3.2. Chỉ số thị trường

// Tất cả chỉ số
indexes, err := data.MarketData.GetIndexes()

// Theo sàn
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)
}

Trả về: []MarketIndexes

3.3. Tổng hợp chỉ số (Index Summary)

// Tổng hợp hiện tại
summary, err := data.MarketData.GetIndexSummary("VNINDEX")

// Tổng hợp lịch sử
summary, err := data.MarketData.GetIndexSummaryHistorical("VNINDEX", "2026/01/15")

// Tổng hợp theo sàn
summary, err := data.MarketData.GetBoardSummary(market.BoardHOSE)

// Tổng hợp theo sàn lịch sử
summary, err := data.MarketData.GetBoardSummaryHistorical(market.BoardHOSE, "2026/01/15")

Trả về: *MarketIndexSummary. Một số trường giao dịch khối ngoại là con trỏ (nil khi server không gửi), xem Models.

3.4. Thông tin chứng khoán

// Một mã
info, err := data.MarketData.GetSecuritiesInfo("SSI")
if err == nil && info == nil {
    fmt.Println("không tìm thấy mã") // nil khi không tìm thấy
}

// Theo chỉ số
securities, err := data.MarketData.GetSecuritiesInfoByIndex("VN30")

// Theo sàn
securities, err := data.MarketData.GetSecuritiesInfoByBoard(market.BoardHOSE)

Trả về: *SecuritiesInfo cho một mã (nil khi không tìm thấy), []SecuritiesInfo cho các method trả danh sách.

3.5. Tổng hợp chứng khoán (Securities Summary)

// Tổng hợp hiện tại
summary, err := data.MarketData.GetSecuritiesSummary("SSI")

// Tổng hợp lịch sử
summary, err := data.MarketData.GetSecuritiesSummaryHistorical(
    "SSI", "2026/03/01", "2026/03/31",
)

// Theo chỉ số
summary, err := data.MarketData.GetSecuritiesSummaryByIndex("VN30")

// Theo chỉ số lịch sử
summary, err := data.MarketData.GetSecuritiesSummaryByIndexHistorical(
    "VN30", "2026/03/01", "2026/03/31",
)

Trả về: []SecuritiesSummary

Lưu ý: Ceiling, Floor và RefPrice trong SecuritiesSummary là con trỏ và có thể nil. Để lấy giá trần, sàn và tham chiếu, dùng master data (mục 3.6).

3.6. Master Data

// Master data hiện tại
master, err := data.MarketData.GetMasterData()

// Master data lịch sử
master, err = data.MarketData.GetMasterDataHistorical("2026/03/01", "2026/03/31")

Trả về: []MasterData (giá trần, sàn và tham chiếu theo từng mã, xem Models).

3.7. Sự kiện quyền (Corporate Actions)

Cổ tức, cổ phiếu thưởng, phát hành thêm quyền mua và các sự kiện quyền khác (endpoint GET /api/v3/data/corporateActions). Có từ 3.3.2. Giống các method market data khác, các lời gọi này blocking và không nhận context.

import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/market"

// Hôm nay, tất cả mã (ngôn ngữ "vn")
actions, err := data.MarketData.GetCorporateActions()

// Một mã, nội dung sự kiện tiếng Anh
actions, err = data.MarketData.GetCorporateActions(
    market.WithCorporateActionsSymbol("SSI"),
    market.WithCorporateActionsLanguage("en"), // "vn" (mặc định) hoặc "en"
)

// Khoảng ngày trong quá khứ (tối đa 5 ngày)
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)
}
MethodTham sốTrả vềMô tả
GetCorporateActionsopts ...CorporateActionsOption[]CorporateAction, errorSự kiện quyền của ngày hôm nay
GetCorporateActionsHistoricalfromDate, toDate string, opts ...CorporateActionsOption[]CorporateAction, errorSự kiện quyền trong một khoảng ngày quá khứ. Ngày có dạng YYYY/MM/DD; toDate cách fromDate tối đa 5 ngày
OptionMặc địnhMô tả
WithCorporateActionsSymbol(symbol)Không đặt (tất cả mã)Giới hạn kết quả theo một mã: chỉ gồm chữ và số, tối đa 20 ký tự
WithCorporateActionsLanguage(language)"vn"Ngôn ngữ của nội dung sự kiện: "vn" hoặc "en" (SDK tự trim và chuyển về chữ thường)

API có phân trang. SDK tự lấy hết mọi trang (page size 100) rồi gộp lại.

Validation. Trước khi gọi, SDK kiểm tra định dạng ngày, fromDate không sau toDate, khoảng ngày tối đa 5 ngày, mã và ngôn ngữ. Nếu sai, SDK trả *ssisdk.ValidationError và không gửi gì đi (xem Xử lý lỗi).

Trả về: []CorporateAction (Symbol, EventTitle, ExrightDate, RecordDate, IssueDate, Exchange, Ratio, xem Models). Mọi field là string đúng như server gửi: ngày giữ nguyên dạng text YYYY/MM/DD, Ratio không được parse (ý nghĩa tuỳ loại sự kiện). Field trống, null hoặc không có (ví dụ sự kiện mới công bố chưa có ngày ex-right hay tỷ lệ) sẽ là "".

Lưu ý — chưa kiểm chứng. Changelog của SDK ghi các method này được viết theo tài liệu API chính thức và chưa chạy thử trên server thật. Hãy test trên môi trường của bạn trước khi dùng.


4. Danh mục (PortfolioService)

Truy cập qua t.Portfolio.

Lưu ý: các getter trả về con trỏ, và slice hoặc con trỏ sẽ là nil khi tài khoản không có dữ liệu (ví dụ không có vị thế). Hãy kiểm tra nil / len(...) == 0 trước khi đọc.

4.1. Số dư tài khoản

// Số dư cổ phiếu
balance, err := t.Portfolio.GetEquityBalance("YOUR_ACCOUNT_NO")
if err == nil && balance != nil {
    fmt.Printf("Withdrawable: %.0f\n", balance.Withdrawable)
}

// Số dư phái sinh
derBalance, err := t.Portfolio.GetDerivativeBalance("YOUR_DERIVATIVE_ACCOUNT_NO")

4.2. Vị thế

// Vị thế cổ phiếu
positions, err := t.Portfolio.GetEquityPositions("YOUR_ACCOUNT_NO")
for _, pos := range positions { // nil khi rỗng
    fmt.Printf("%s: %d cổ | Giá vốn: %.0f\n", pos.Symbol, pos.Quantity, pos.CostPrice)
}

// Tất cả vị thế phái sinh (*AllDerivativePosition)
derPositions, err := t.Portfolio.GetDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")

// Chỉ vị thế đang mở
openPos, err := t.Portfolio.GetOpenDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")

// Chỉ vị thế đã đóng
closedPos, err := t.Portfolio.GetClosedDerivativePositions("YOUR_DERIVATIVE_ACCOUNT_NO")

4.3. Sổ lệnh

// Lệnh trong ngày
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)
}

// Lệnh lịch sử
orders, err = t.Portfolio.GetHistoricalOrders("YOUR_ACCOUNT_NO", "2026/01/01", "2026/01/31")

Với lệnh giá thị trường, server gửi một từ (ATO, ATC, MP...) thay vì số: khi đó Price là 0 và PriceText giữ từ đó.

4.4. PPMMR

// PPMMR cổ phiếu
ppmmr, err := t.Portfolio.GetEquityPPMMR("YOUR_ACCOUNT_NO")

// PPMMR phái sinh
derPPMMR, err := t.Portfolio.GetDerivativePPMMR("YOUR_DERIVATIVE_ACCOUNT_NO")

Tổng quan method Portfolio

NhómMethodTrả về
Số dưGetEquityBalance(accountNo)*EquityAccountBalance
GetDerivativeBalance(accountNo)*DerivativeAccountBalance
Vị thếGetEquityPositions(accountNo)[]EquityPosition
GetDerivativePositions(accountNo)*AllDerivativePosition
GetOpenDerivativePositions(accountNo)[]DerivativePosition
GetClosedDerivativePositions(accountNo)[]DerivativePosition
Sổ lệnhGetTodayOrders(accountNo)[]Order
GetHistoricalOrders(accountNo, fromDate, toDate)[]Order
PPMMRGetEquityPPMMR(accountNo)*EquityPPMMR
GetDerivativePPMMR(accountNo)*DerivativePPMMR

5. Giao dịch (TradingService)

Truy cập qua t.Trading. Mọi request ghi đều cần Config.PrivateKey (SDK ký body bằng RSA-SHA256), xem Cấu hình.

5.1. Đặt lệnh

import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/trading"

accountNo := "YOUR_ACCOUNT_NO"

// Lệnh giới hạn (LO)
result, err := t.Trading.PlaceLimitOrder(accountNo, "SSI", trading.OrderSideBuy, 100, 66000)

// Lệnh thị trường (MTL)
result, err = t.Trading.PlaceMarketOrder(accountNo, "SSI", trading.OrderSideBuy, 100)

// Lệnh ATO (mở cửa)
result, err = t.Trading.PlaceATOOrder(accountNo, "SSI", trading.OrderSideBuy, 100)

// Lệnh ATC (đóng cửa)
result, err = t.Trading.PlaceATCOrder(accountNo, "SSI", trading.OrderSideSell, 100)

// Loại lệnh tuỳ chỉnh
result, err = t.Trading.PlaceOrder(
    accountNo, "SSI", trading.OrderSideBuy, 100, 66000, trading.OrderTypeLO,
)

// Idempotency key do bạn tự đặt (tối đa 20 ký tự)
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)

Cảnh báo: đây là lệnh thật. Chỉ chạy trên môi trường và tài khoản bạn được phép giao dịch.

5.2. Sửa lệnh

Chỉ có thể sửa giá hoặc khối lượng (không sửa cả hai cùng lúc). Các method sửa lệnh không nhận option.

// Sửa giá theo client_request_id
result, err := t.Trading.ModifyOrderPrice(accountNo, "YOUR_CLIENT_REQUEST_ID", 68000)

// Sửa giá theo order_id
result, err = t.Trading.ModifyOrderPriceByOrderID(accountNo, "YOUR_ORDER_ID", 68000)

// Sửa khối lượng theo client_request_id
result, err = t.Trading.ModifyOrderQuantity(accountNo, "YOUR_CLIENT_REQUEST_ID", 200)

// Sửa khối lượng theo order_id
result, err = t.Trading.ModifyOrderQuantityByOrderID(accountNo, "YOUR_ORDER_ID", 200)

5.3. Huỷ lệnh

// Huỷ theo client_request_id
result, err := t.Trading.CancelOrder(accountNo, "YOUR_CLIENT_REQUEST_ID")

// Huỷ theo order_id
result, err = t.Trading.CancelOrderByOrderID(accountNo, "YOUR_ORDER_ID")

Để huỷ nhiều lệnh trong một request, xem 5.4. Batch Orders.

5.4. Batch Orders

PlaceBatchOrders (đặt) và CancelBatchOrders (huỷ) gửi nhiều lệnh trong một request. Server trả về kết quả riêng cho từng lệnh. Dùng khi cần đặt rổ lệnh hoặc huỷ hàng loạt cuối phiên; với một lệnh đơn lẻ, dùng các method theo từng lệnh ở trên.

Điều kiện cần

  • Client: ssi.NewTrading(auth), truy cập qua t.Trading.
  • Auth: phải gọi auth.Authenticate(otp) với OTP hợp lệ trước (giống mọi method giao dịch).
  • Config.PrivateKey: bắt buộc. Mọi request ghi đều có header X-Signature (RSA-SHA256) ký trên đúng nội dung body JSON được gửi đi. SDK tự ký, bạn không cần xử lý chữ ký.
MethodHTTPEndpoint
PlaceBatchOrdersPOST/api/v3/trading/order/batch
CancelBatchOrdersDELETE/api/v3/trading/order/batch

Method

MethodTham sốTrả vềMô tả
PlaceBatchOrders(orders)[]trading.BatchPlaceOrderItem*trading.BatchOrderResponse, errorĐặt 1..Config.MaxBatchOrders lệnh trong một request
CancelBatchOrders(orders)[]trading.BatchCancelOrderItem*trading.BatchOrderResponse, errorHuỷ 1..Config.MaxBatchOrders lệnh trong một request

Bảng trường của BatchPlaceOrderItem, BatchCancelOrderItem, BatchOrderResponse và BatchOrderResult nằm ở Models. Hai quy tắc cần nhớ: mỗi lệnh đặt có các trường giống tham số của PlaceOrder (ClientRequestID tối đa 20 ký tự, để trống thì SDK tự sinh), và mỗi lệnh huỷ phải có đúng một trong hai: OrderID hoặc ClientRequestID (có cả hai hoặc không có cái nào thì SDK trả *ValidationError).

Giới hạn kích thước lô. Config.MaxBatchOrders mặc định là 20 (hằng ssisdk.MaxBatchOrders). Giá trị nhỏ hơn 1 được chuẩn hóa về 20. Lô rỗng hoặc lớn hơn giới hạn bị SDK từ chối ngay với *ValidationError, không gọi mạng. Theo chú thích trong code SDK, 20 là mặc định phía server. Con số này chưa được đối chiếu với API reference, nơi có thể ghi mức tối đa khác (50): xem Cấu hình và hỏi SSI trước khi gửi lô lớn.

Cách hoạt động

  • Validate cả lô trước khi gửi. SDK kiểm tra từng lệnh (trường bắt buộc, khối lượng, giá cho LO/PLO, độ dài ClientRequestID, quy tắc OrderID xor ClientRequestID). Một lệnh sai thì cả lô bị từ chối và chưa có gì được gửi.
  • Ký trên toàn bộ body. Body có dạng {"batchRequestId", "batchRequestTime", "orders": [...]}. Chữ ký X-Signature tính trên đúng chuỗi JSON này.
  • Envelope của lô. batchRequestId là chuỗi ngẫu nhiên SDK tự sinh; server từ chối batchRequestId đã dùng (HTTP 409). batchRequestTime là thời điểm hiện tại tính bằng epoch milliseconds; nếu lệch ra ngoài cửa sổ chống replay của server thì server trả mã lỗi 400111. Vì vậy hãy giữ đồng hồ máy chủ chạy SDK chính xác (NTP).
  • Idempotency. Dùng lại một ClientRequestID trong cùng ngày thì server trả HTTP 409, SDK ánh xạ thành *DuplicateRequestError. SDK không bao giờ tự gửi lại lệnh dưới id mới, vì lệnh gốc có thể đang còn hiệu lực (hằng ssisdk.MaxClientRequestIDLength = 20).
  • Không tự retry. Request POST / PUT / DELETE không được replay khi timeout hoặc khi nhận 429. Lỗi được trả thẳng cho bạn.
  • Kết quả theo từng lệnh. Request thành công không có nghĩa mọi lệnh trong lô đều được nhận. Luôn duyệt Results và kiểm tra Success. Lỗi của riêng một lệnh không được trả dưới dạng error; nó nằm trong ErrorCode / ErrorMessage của lệnh đó.
  • Thứ tự kết quả. Theo chú thích trong code, Results theo thứ tự request. Điều này chưa được xác nhận trên server thật: khi có thể, hãy đối chiếu kết quả theo ClientRequestID thay vì theo vị trí. Nếu server không trả results (hoặc không phải danh sách), Results là slice rỗng: hãy so len(res.Results) với số lệnh đã gửi.

Cảnh báo: đây là lệnh thật. Chỉ chạy các ví dụ trên môi trường và tài khoản bạn được phép giao dịch.

Ví dụ 1: đặt lô và đọc kết quả từng lệnh

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") // bắt buộc để ký lệnh

    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, // thay bằng giá hợp lệ theo bước giá
            OrderType:       trading.OrderTypeLO,
            ClientRequestID: "BATCH-0001", // <= 20 ký tự; để trống thì SDK tự sinh
        },
        {
            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) // phân loại bằng reportBatchError bên dưới
    }

    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)
        }
    }
}

Ví dụ 2: huỷ lô theo ClientRequestID hoặc OrderID

cancels := []trading.BatchCancelOrderItem{
    {AccountNo: accountNo, ClientRequestID: "BATCH-0001"}, // huỷ theo id bạn đặt
    {AccountNo: accountNo, OrderID: "YOUR_ORDER_ID"},      // huỷ theo id server cấp
}

cres, err := t.Trading.CancelBatchOrders(cancels)
if err != nil {
    log.Printf("CancelBatchOrders: %v", err)
    // Chưa chắc lệnh nào đã huỷ: kiểm tra trạng thái lệnh trước khi làm gì tiếp.
    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)
}

Xử lý lỗi. Mọi lỗi của SDK đều hỗ trợ 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 hoặc BatchRequestID đã dùng trong ngày.
        fmt.Printf("%s: trùng id (409): %v\n", op, dup)
    case errors.As(err, &vErr):
        // Lỗi cục bộ: cả lô bị từ chối, chưa gửi gì lên server.
        fmt.Printf("%s: dữ liệu không hợp lệ: %v\n", op, vErr)
    case errors.As(err, &rl):
        wait := "không rõ"
        if rl.RetryAfterMs != nil {
            wait = fmt.Sprintf("%d ms", *rl.RetryAfterMs)
        }
        fmt.Printf("%s: bị giới hạn tốc độ, thử lại sau %s\n", op, wait)
    default:
        fmt.Printf("%s: lỗi: %v\n", op, err)
    }
}

Bảng lỗi của batch (ValidationError, DuplicateRequestError, RateLimitError, AuthenticationError, APIError với 400111) và cách xử lý từng loại nằm ở Xử lý lỗi.

Khi timeout hoặc mất kết nối giữa chừng: vì SDK không retry request ghi, bạn không biết chắc lô đã vào hệ thống hay chưa. Đối chiếu trạng thái lệnh (ví dụ qua sự kiện lệnh, xem 6.5. Subscribe sự kiện giao dịch) trước khi gửi lại. Nếu buộc phải gửi lại, dùng lại đúng các ClientRequestID cũ thay vì sinh id mới, để cơ chế chặn trùng (409) còn tác dụng.

Liên quan: Hướng dẫn: Đặt/hủy lệnh hàng loạt (Batch), Trạng thái lệnh và Hủy lệnh. Các lệnh batch cũng có trên client Trading WebSocket, xem 7. Trading WebSocket.

5.5. Sức mua/bán tối đa

// Với giá cụ thể
price := 66000.0
maxBS, err := t.Trading.GetMaxBuySell(accountNo, "SSI", &price)
fmt.Printf("Max mua: %d, Max bán: %d\n", maxBS.MaxBuyQuantity, maxBS.MaxSellQuantity)

// Theo giá thị trường
maxBS, err = t.Trading.GetMaxBuySellAtMarketPrice(accountNo, "SSI")

MarginRatio và PurchasingPower là con trỏ và là nil với tài khoản phái sinh.

Tổng quan method Trading

NhómMethodTrả về
Đặt lệnhPlaceOrder(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
Sửa lệnhModifyOrderPrice(accountNo, clientRequestID, price)*ModifyOrderResponse
ModifyOrderPriceByOrderID(accountNo, orderID, price)*ModifyOrderResponse
ModifyOrderQuantity(accountNo, clientRequestID, qty)*ModifyOrderResponse
ModifyOrderQuantityByOrderID(accountNo, orderID, qty)*ModifyOrderResponse
Huỷ lệnhCancelOrder(accountNo, clientRequestID)*CancelOrderResponse
CancelOrderByOrderID(accountNo, orderID)*CancelOrderResponse
BatchPlaceBatchOrders(orders []BatchPlaceOrderItem)*BatchOrderResponse
CancelBatchOrders(orders []BatchCancelOrderItem)*BatchOrderResponse
Max Mua/BánGetMaxBuySell(accountNo, symbol, price *float64)*MaxBuySellResponse
GetMaxBuySellAtMarketPrice(accountNo, symbol)*MaxBuySellResponse
Lệnh điều kiệnPlaceFcoGtd(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. Lệnh điều kiện (Flexible Conditional Orders - FCO)

SDK hỗ trợ 7 loại lệnh điều kiện linh hoạt (FCO), tra cứu danh sách lệnh FCO, nhật ký sổ lệnh FCO, lịch sử trạng thái FCO và hủy lệnh FCO.

Chuỗi fromDate / toDate có định dạng YYYY/MM/DD HH:MM:SS (cũng chấp nhận ngày trần YYYY/MM/DD), và khoảng thời gian tối đa 31 ngày. Ở những chỗ giá là interface{} (ví dụ price trong PlaceFcoGtd, tpPrice / slPrice trong PlaceFcoOco), truyền một số hoặc một từ loại lệnh như trading.OrderTypeMTL (từ thị trường không kèm 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. Đặt lệnh GTD (Good Till Date)
gtd, err := t.Trading.PlaceFcoGtd(
    accountNo, "SSI", trading.OrderSideBuy, 100, 25000, 500, fromDate, toDate,
)

// 2. Đặt lệnh Stop Market
stop, err := t.Trading.PlaceFcoStop(
    accountNo, "SSI", trading.OrderSideSell, 100, 24000, trading.FCOOperatorLesserOrEqual, fromDate, toDate,
)

// 3. Đặt lệnh Stop Limit
stopLimit, err := t.Trading.PlaceFcoStopLimit(
    accountNo, "SSI", trading.OrderSideBuy, 100, 25500, 500, 25000, trading.FCOOperatorGreaterOrEqual, fromDate, toDate,
)

// 4. Đặt lệnh Trailing Stop
trailing, err := t.Trading.PlaceFcoTrailingStop(
    accountNo, "SSI", trading.OrderSideBuy, 100, 26000, 1000, fromDate, toDate,
)

// 5. Đặt lệnh Trailing Stop Limit
trailingLimit, err := t.Trading.PlaceFcoTrailingStopLimit(
    accountNo, "SSI", trading.OrderSideBuy, 100, 26000, 1000, 500, fromDate, toDate,
)

// 6. Đặt lệnh OCO (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. Đặt lệnh Bull Bear
bullBear, err := t.Trading.PlaceFcoBullBear(
    accountNo, "SSI", trading.OrderSideBuy, 1000, 100.5, 10, 30, 20, 30, 10, 0.1, 0, fromDate, toDate,
)

// Hủy lệnh FCO
cancelRes, err := t.Trading.CancelFco(gtd.FCOID)

Trả về: FCOPlaceResponse (FCOID) khi đặt lệnh, FCOCancelResponse (FCOID) khi huỷ lệnh.

Mã OTP tuỳ chọn. Mọi method PlaceFco* và CancelFco nhận thêm trading.WithOTPCode(code) (tuỳ chọn) cho tài khoản yêu cầu mã dùng một lần với lệnh điều kiện. Nếu không truyền, request được gửi không kèm mã.

cancelRes, err = t.Trading.CancelFco(gtd.FCOID, trading.WithOTPCode(os.Getenv("SSI_FCO_OTP")))

Tra cứu:

// Tra cứu danh sách lệnh FCO theo tài khoản
fcoList, err := t.Trading.GetFcoByAccountNo(accountNo, 1, 10)

// Tra cứu theo mã chứng khoán
fcoList, err = t.Trading.GetFcoBySymbol(accountNo, "SSI", 1, 10)

// Tra cứu theo trạng thái xử lý
fcoList, err = t.Trading.GetFcoByStatus(accountNo, trading.FCOStatusTrit, 1, 10)

// Tra cứu theo một trong nhiều trạng thái
fcoList, err = t.Trading.GetFcoByStatuses(accountNo,
    []trading.FCOStatus{trading.FCOStatusWait, trading.FCOStatusTri}, 1, 10)

// Tra cứu theo khoảng ngày
fcoList, err = t.Trading.GetFcoByDate(accountNo, "2026/07/01", "2026/07/21", 1, 10)

// Lấy một lệnh FCO cụ thể theo ID
fcoInfo, err := t.Trading.GetFcoById(accountNo, "YOUR_FCO_ID")

// Lấy nhật ký khớp lệnh (order book) của lệnh FCO
orderBook, err := t.Trading.GetFcoOrderBook("YOUR_FCO_ID", 1, 10)

// Lấy các lần chuyển trạng thái của lệnh FCO
history, err := t.Trading.GetFcoStatusHistory("YOUR_FCO_ID") // []FCOStatusHistoryItem

Lưu ý: các method tra cứu danh sách đều nhận pageIndex / pageSize để phân trang. PagesCount trong response danh sách có thể bỏ sót trang cuối chưa đầy: hãy phân trang cho tới khi một trang trả về rỗng, hoặc dùng ItemsCount.

Lưu ý: SubscribeFcoOrderStatus đã bị xoá khỏi stream client từ 3.3.0. Sự kiện FCO đến trên topic order.<account>: xem 6.5. Subscribe sự kiện giao dịch.


6. Streaming Realtime (StreamingService)

Truy cập qua s.Streaming (kiểu *stream.Service). Gọi s.Connect() trước.

Điều kiện cần

  • Client: ssi.NewStream(auth).
  • Auth: phải gọi auth.Authenticate(otp) với OTP hợp lệ trước Connect. Stream lấy token từ TokenManager của Auth này.
  • Mỗi client mở tối đa 10 socket cùng lúc (ssisdk.WSServerMaxConnections). Vượt quá thì server từ chối với 429.
  • Một frame request tối đa 4000 byte (ssisdk.WSMaxFrameBytes); SDK tự tách request dài thành nhiều frame.

6.1. Kết nối

s := ssi.NewStream(auth)
defer s.Disconnect()

if err := s.Connect(); err != nil {
    log.Fatal(err)
}

// Chờ cho tới khi ngắt kết nối
s.Wait(nil)

// Chờ với timeout (millisecond)
timeoutMs := 30000
s.Wait(&timeoutMs)

6.2. Thiết lập Callbacks

SetOnData nhận mọi message của kênh DATA đã được parse thành kiểu cụ thể. Các callback có kiểu bên dưới chỉ nhận đúng loại message của chúng; chúng không tự subscribe gì cả, nên bạn vẫn phải gọi method subscribe tương ứng. Truyền nil để xoá callback. Callback bị panic sẽ được recover và log, không ảnh hưởng các callback khác.

MethodCallbackNhận
SetOnDatafunc(interface{})Mọi message kênh DATA (có kiểu, hoặc map[string]interface{} thô khi topic chưa có model, kể cả ack)
SetOnTradingfunc(interface{})Mọi message kênh TRADING
SetOnHeartbeatfunc(*stream.HeartbeatMessage)Message kênh HEARTBEAT
SetOnMarketFlagfunc(*stream.MarketFlagMessage)Chỉ MarketFlagMessage
SetOnIndexSummaryfunc(*stream.IndexSummaryMessage)Chỉ IndexSummaryMessage
SetOnOrderfunc(*stream.OrderStatusMessage)Chỉ OrderStatusMessage
SetOnOrderMatchfunc(*stream.OrderMatchMessage)Chỉ OrderMatchMessage (khớp lệnh)
SetOnPortfoliofunc(*stream.PortfolioMessage)Chỉ PortfolioMessage
SetOnConnectionfunc(transport.ConnectionEvent)Thay đổi trạng thái kết nối, xem 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 | Giá: %.0f | KL: %d\n", m.Symbol, m.Price, m.Quantity)
    case *stream.QuoteMessage:
        fmt.Printf("[QUOTE] %s | Mua: %v | Bán: %v\n", m.Symbol, m.BidPrices, m.AskPrices)
    case *stream.ForeignRoomMessage:
        fmt.Printf("[ROOM] %s | KL Mua: %d | KL Bán: %d\n", m.Symbol, m.BuyQuantity, m.SellQuantity)
    case *stream.MarketStatusMessage:
        fmt.Printf("[MARKET] %s | Trạng thái: %s\n", m.Market, m.Status)
    case *stream.MarketDataMessage:
        if m.RefPrice != nil { // nil = server không gửi key này
            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 | Trạng thái: %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:
        // Chỉ tài khoản phái sinh. TotalAsset, CashBalance và StockValue luôn là 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 dữ liệu thị trường

import "github.com/SSI-Securities-Inc/ssi-sdk-go/v3/market"

// Tất cả kênh (trade + quote + room) cho mã
s.Streaming.SubscribeSymbol([]string{"SSI", "HPG", "VIC"}, nil)

// Kênh riêng lẻ
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 theo timeframe
s.Streaming.SubscribeSymbolOhlcv([]string{"SSI"}, market.TimeframeMinute1, nil)

// Master data (giá trần / sàn / tham chiếu)
s.Streaming.SubscribeMarket([]string{"SSI"}, nil)

// Theo sàn
s.Streaming.SubscribeBoard([]market.Board{market.BoardHOSE, market.BoardHNX}, nil)

// Các mã thành phần của một chỉ số (không phải bản thân chỉ số, xem 6.8)
s.Streaming.SubscribeIndex([]string{"VNINDEX", "VN30"}, nil)

Tick chỉ số, nến chỉ số 1 phút và 5 phút, tóm tắt chỉ số và cờ phiên nằm ở 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"})

Mỗi method Subscribe* đều có method Unsubscribe* tương ứng. Để xoá toàn bộ một lần, xem UnsubscribeAll ở 6.10.

6.5. Subscribe sự kiện giao dịch

// Trạng thái lệnh và khớp lệnh: tất cả tài khoản (account rỗng nghĩa là "*")
s.Streaming.SubscribeOrderStatus("", nil)

// Cho tài khoản cụ thể
s.Streaming.SubscribeOrderStatus("YOUR_ACCOUNT_NO", nil)

// Danh mục (cập nhật vị thế phái sinh): tất cả tài khoản
s.Streaming.SubscribePortfolio("", nil)

// Cho tài khoản cụ thể
s.Streaming.SubscribePortfolio("YOUR_ACCOUNT_NO", nil)

// Huỷ subscribe
s.Streaming.UnsubscribeOrderStatus("YOUR_ACCOUNT_NO")
s.Streaming.UnsubscribePortfolio("YOUR_ACCOUNT_NO")

Sự kiện lệnh điều kiện (FCO). Không có subscribe riêng cho FCO: SubscribeFcoOrderStatus đã bị xoá từ 3.3.0. Cập nhật FCO đến trên cùng topic order.<account> dưới dạng *stream.FCOOrderUpdateMessage (EventType == "fcoEvent"), qua SetOnTrading. Hãy subscribe bằng SubscribeOrderStatus.

6.6. Heartbeat

// Ping một lần (không callback, không lặp)
s.Streaming.Ping(nil, nil)

// Ping lặp lại theo chu kỳ (millisecond; ping đầu tiên sau một chu kỳ)
intervalMs := 30000
s.Streaming.Ping(nil, &intervalMs)

// Dừng ping loop tự động
s.Streaming.StopPingLoop()

Chu kỳ <= 0 bị từ chối.

6.7. Trạng thái kết nối

if s.Streaming.IsConnected() {
    fmt.Println("WebSocket connected")
}

fmt.Println(s.Streaming.ReconnectCount()) // số lần kết nối được thiết lập lại
if err := s.Streaming.LastError(); err != nil {
    fmt.Printf("lỗi kết nối gần nhất: %v\n", err)
}

6.8. Index subscriptions and interval rules

Dùng các method này để theo dõi liên tục VNINDEX, VN30, HNXINDEX... thay vì gọi REST lặp lại. Danh sách chỉ số qua REST có trong hướng dẫn Danh sách chỉ số. Trong các bảng bên dưới, indices là danh sách mã chỉ số (ví dụ "VNINDEX", "VN30"), và onResponse là handler cho ack của server (transport.MessageHandler); truyền nil nếu không cần.

Subscribe và unsubscribe chỉ số

MethodTham sốTrả vềMô tả
SubscribeIndexTrade (alias SubscribeIndexTicks)indices []string, interval market.Timeframe, onResponseerrorTick của chính chỉ số (trade.index.<CODE>). Với interval 1m / 5m dữ liệu đến dưới dạng nến
UnsubscribeIndexTrade (alias UnsubscribeIndexTicks)indices []string, interval market.TimeframeerrorHuỷ topic ở trên. Truyền đúng interval đã subscribe
SubscribeIndexSummaryindices []string, onResponseerrorTóm tắt chỉ số (indexsummary.<CODE>)
UnsubscribeIndexSummaryindices []stringerrorHuỷ topic ở trên
SubscribeIndex (alias SubscribeIndexConstituents)indices []string, onResponseerrorTrade, quote và room của các mã thành phần của chỉ số, gửi dưới dạng trade.<CODE>@*, quote.<CODE>@*, room.<CODE>@*
UnsubscribeIndex (alias UnsubscribeIndexConstituents)indices []stringerrorHuỷ ba topic @* ở trên
SubscribeMarketFlag (alias SubscribeSessionFlag)onResponseerrorCờ phiên (ATO, LO, ATC...) của mọi sàn (market.flag)
UnsubscribeMarketFlag (alias UnsubscribeSessionFlag)khôngerrorHuỷ topic ở trên

SubscribeIndex khác SubscribeIndexTrade: SubscribeIndex cho dữ liệu của các mã thành phần, còn SubscribeIndexTrade cho tick của chính chỉ số.

Chưa xác minh: theo CHANGELOG của SDK, cơ chế @* mà SubscribeIndex dùng (fan-out cả rổ / cả sàn) chưa được xác nhận trên server thật. Hãy thử trong môi trường của bạn trước khi dựa vào nó.

Handler. Tick chỉ số không có handler riêng: nhận qua SetOnData và dùng type switch trên *stream.IndexTickMessage (tick) và *stream.IntervalMessage (nến). Có handler riêng cho *stream.IndexSummaryMessage (SetOnIndexSummary) và *stream.MarketFlagMessage (SetOnMarketFlag), xem 6.2. Các trường của message nằm ở Models. Trường con trỏ (*float64) là nil khi server không gửi key đó, khác với giá trị 0.

Chưa xác minh: server gửi key viết tắt cho tick chỉ số, và SDK suy ra ý nghĩa của từng trường IndexTickMessage từ chữ viết tắt. Nếu cần chắc chắn, hãy đọc Raw (bản sao của object gốc).

Quy tắc topic và interval

Quy tắcChi tiết
Interval hợp lệstream.IntervalTick ("tick"), stream.IntervalMinute1 ("1m"), stream.IntervalMinute5 ("5m"). Giá trị khác là *ValidationError
Interval chỉ áp dụng chotrade và trade.index
Interval trên chỉ sốCần đúng một mã cụ thể, không dùng wildcard (*)
Không intervalTruyền chuỗi rỗng "" để subscribe trade.index.<CODE> không có hậu tố
Mã chỉ sốTự động viết hoa. UPCOMINDEX được đổi thành HNXUPCOMINDEX
Bộ lọcMỗi phần tử là *, một mã, hoặc một khoảng A-B. Không đặt chuỗi phân tách bằng dấu phẩy vào một phần tử; hãy truyền nhiều phần tử trong slice
Danh sách rỗng*ValidationError (cần ít nhất một mã)
Interval * (stream.StreamIntervalAll)SDK dùng nội bộ cho SubscribeIndex / SubscribeBoard (@*)

Topic được tạo như sau:

MethodTopic gửi lên serverMessage nhận được
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 (nến)
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

Ví dụ 1: theo dõi tick VNINDEX và sự kiện kết nối

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 = server không gửi key này
                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)
    }

    // Tick của chính chỉ số.
    if err := s.Streaming.SubscribeIndexTrade([]string{"VNINDEX"}, stream.IntervalTick, nil); err != nil {
        log.Fatalf("SubscribeIndexTrade: %v", err) // ValidationError nếu interval/topic sai
    }

    waitMs := 60000
    s.Wait(&waitMs) // chờ tối đa 60 giây (chỉ để minh hoạ)
}

Ví dụ 2: nến 1 phút, tóm tắt chỉ số và cờ phiên

// Nến 1 phút cần đúng một mã cụ thể, không dùng 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) {
    // Các trường chưa được công bố: dùng Get với key bạn biết, hoặc xem 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)
}

// Cờ phiên của mọi sàn.
s.Streaming.SetOnMarketFlag(func(m *stream.MarketFlagMessage) {
    fmt.Printf("flag: board=%s flag=%s\n", m.Board, m.Flag)
})
_ = s.Streaming.SubscribeMarketFlag(nil)

Lỗi. Các method subscribe trả *ssisdk.ValidationError khi topic không rõ loại hoặc bộ lọc không hợp lệ, danh sách rỗng, interval khác tick / 1m / 5m, interval trên topic không hỗ trợ, hoặc interval trên chỉ số có wildcard. Trường hợp đó chưa có gì được gửi đi.

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("dữ liệu sai, chưa gửi gì: %v", vErr)
    }
}

Xem Xử lý lỗi cho các lỗi streaming khác.

6.9. Connection events and reconnect

Config.AutoReconnect mặc định là true, nên khi socket bị rớt:

  • SDK kết nối lại với exponential backoff và jitter, bắt đầu từ Config.RetryDelayMs, tối đa 60000 ms.
  • Mỗi lần thử kết nối dùng token mới; SDK cũng chủ động kết nối lại khoảng 60000 ms trước khi token hết hạn.
  • Sau khi kết nối lại, các subscription được khôi phục tự động.
  • Nếu server trả 429 khi connect, SDK chờ ít nhất 30000 ms.
  • Lỗi không thể phục hồi (ví dụ cần OTP mới) làm dừng việc kết nối lại.

ConnectionEvent (transport.ConnectionEvent) có State, ReconnectCount và Err. Các giá trị State:

StateÝ nghĩa
"connected"Kết nối lần đầu
"reconnected"Mở lại sau khi rớt, subscription đã được khôi phục
"disconnected"Socket vừa rớt; nếu AutoReconnect bật thì sẽ kết nối lại
"failed"Không thể phục hồi (Err cho biết lý do), sẽ không kết nối lại. Bạn cần xác thực lại (ví dụ bằng OTP mới)
s.Streaming.SetOnConnection(func(e transport.ConnectionEvent) {
    switch e.State {
    case "failed":
        log.Printf("stream failed: %v (xác thực lại bằng OTP mới)", e.Err)
    default:
        log.Printf("stream state=%s reconnects=%d", e.State, e.ReconnectCount)
    }
})

Lỗi xảy ra sau khi kết nối đã lên (rớt mạng, token hết hạn) không được các method subscribe trả về: hãy theo dõi qua SetOnConnection (State, Err) và LastError(). Khi State == "failed", SDK không kết nối lại nữa. Xem Cấu hình cho AutoReconnect và RetryDelayMs.

6.10. Subscription management

MethodTham sốTrả vềMô tả
ListSubscriptiontimeoutMs intmap[string][]string, errorHỏi server kết nối này đang subscribe những topic nào. Kết quả có các key "data" và "trading"; kênh không subscribe gì là slice rỗng. timeoutMs <= 0 nghĩa là 5000
UnsubscribeAllkhôngmap[string][]string, errorHỏi server, rồi huỷ đúng các topic đó (kể cả topic còn sót từ lần chạy trước của cùng session). Trả về các topic đã huỷ
PingonResponse, intervalMs *interrorXem 6.6
StopPingLoopkhôngkhôngDừng ping loop chạy nền
IsConnectedkhôngboolSocket đang kết nối
LastErrorkhôngerrorLỗi kết nối gần nhất, hoặc nil
ReconnectCountkhôngintSố lần kết nối được thiết lập lại
// Xem kết nối này đang subscribe gì.
current, err := s.Streaming.ListSubscription(5000) // 5000 ms
if err != nil {
    log.Printf("ListSubscription: %v", err) // *WebSocketError khi timeout
} else {
    fmt.Printf("data topics: %d, trading topics: %d\n",
        len(current["data"]), len(current["trading"]))
}

// Huỷ một nhóm cụ thể: truyền đúng interval đã subscribe.
_ = s.Streaming.UnsubscribeIndexTrade([]string{"VN30"}, stream.IntervalMinute1)

// Hoặc huỷ mọi thứ server đang giữ cho kết nối này.
removed, err := s.Streaming.UnsubscribeAll()
if err != nil {
    log.Printf("UnsubscribeAll: %v", err)
} else {
    fmt.Printf("removed data topics: %d\n", len(removed["data"]))
}

*ssisdk.WebSocketError từ ListSubscription / UnsubscribeAll nghĩa là phản hồi bị timeout hoặc gửi frame thất bại: hãy kiểm tra IsConnected() và thử lại khi kết nối đã ổn định.

Liên quan: Hướng dẫn: WebSocket Market Data, WebSocket nâng cao và Danh sách chỉ số.

Loại message qua SetOnData

TopicMessage TypeMô tả
trade.**stream.TradeMessageDữ liệu khớp lệnh
quote.**stream.QuoteMessageDữ liệu mua/bán
room.**stream.ForeignRoomMessageRoom ngoại
market.<symbol>*stream.MarketDataMessageMaster data (giá trần, sàn, tham chiếu)
market.* (dạng legacy)*stream.MarketStatusMessageTrạng thái thị trường
market.flag*stream.MarketFlagMessageCờ phiên
put.**stream.PutMessageThoả thuận
oddlot.**stream.OddLotMessageLô lẻ
trade.<symbol>@1m / @5m*stream.IntervalMessageDữ liệu OHLCV theo interval
trade.index.<code>*stream.IndexTickMessageTick chỉ số
trade.index.<code>@1m / @5m*stream.IntervalMessageNến chỉ số
indexsummary.**stream.IndexSummaryMessageTóm tắt chỉ số

Topic chưa có model (kể cả ack) được trả dưới dạng map[string]interface{} thô.

Loại message qua SetOnTrading

Kiểu được chọn theo data.eventType:

TopicMessage TypeMô tả
order.**stream.OrderStatusMessageCập nhật trạng thái lệnh thường (orderEvent)
*stream.OrderMatchMessageKhớp lệnh (orderMatchEvent)
*stream.FCOOrderUpdateMessageCập nhật lệnh điều kiện (fcoEvent)
portfolio.**stream.PortfolioMessageThay đổi vị thế phái sinh (clientPortfolioEvent)

Tổng quan method Streaming

NhómMethodMô tả
Kết nốiConnect()Kết nối WebSocket
Disconnect()Ngắt kết nối
Wait(timeoutMs *int)Chờ dữ liệu (nil là chờ tới khi ngắt kết nối)
IsConnected()Kiểm tra trạng thái kết nối
LastError() / ReconnectCount()Lỗi kết nối gần nhất / số lần kết nối lại
HeartbeatPing(onResponse, intervalMs *int)Ping server (một lần nếu intervalMs là nil)
StopPingLoop()Dừng ping loop tự động
SymbolSubscribeSymbol(symbols, callback)Trade + Quote + Room
SubscribeSymbolTrade(symbols, callback)Chỉ Trade
SubscribeSymbolQuote(symbols, callback)Chỉ Quote
SubscribeSymbolRoom(symbols, callback)Chỉ Room ngoại
SubscribeSymbolPutThrough(symbols, callback)Chỉ thoả thuận
SubscribeSymbolOddLot(symbols, callback)Chỉ lô lẻ
SubscribeSymbolOhlcv(symbols, interval, callback)OHLCV theo timeframe
SubscribeMarket(symbols, callback)Master data (alias SubscribeMasterData)
Sàn/Chỉ sốSubscribeBoard(boards, callback)Theo sàn
SubscribeIndex(indices, callback)Các mã thành phần của chỉ số
SubscribeIndexTrade(indices, interval, callback)Tick / nến của chính chỉ số
SubscribeIndexSummary(indices, callback)Tóm tắt chỉ số
SubscribeMarketFlag(callback)Cờ phiên
Giao dịchSubscribeOrderStatus(accountNo, callback)Trạng thái lệnh, khớp lệnh và sự kiện FCO (rỗng = tất cả tài khoản)
SubscribePortfolio(accountNo, callback)Thay đổi danh mục (rỗng = tất cả tài khoản)
UnsubscribeUnsubscribeSymbol(symbols)Huỷ tất cả kênh
UnsubscribeSymbolTrade(symbols)Huỷ trade
UnsubscribeSymbolQuote(symbols)Huỷ quote
UnsubscribeSymbolRoom(symbols)Huỷ room
UnsubscribeSymbolPutThrough(symbols)Huỷ thoả thuận
UnsubscribeSymbolOddLot(symbols)Huỷ lô lẻ
UnsubscribeSymbolOhlcv(symbols, interval)Huỷ OHLCV
UnsubscribeMarket(symbols)Huỷ master data
UnsubscribeBoard(boards)Huỷ sàn
UnsubscribeIndex(indices)Huỷ các mã thành phần của chỉ số
UnsubscribeIndexTrade(indices, interval)Huỷ tick / nến chỉ số (cùng interval)
UnsubscribeIndexSummary(indices)Huỷ tóm tắt chỉ số
UnsubscribeMarketFlag()Huỷ cờ phiên
UnsubscribeOrderStatus(accountNo) / UnsubscribePortfolio(accountNo)Huỷ sự kiện giao dịch
UnsubscribeAll()Huỷ mọi topic của kết nối này
SubscriptionsListSubscription(timeoutMs)Liệt kê các topic server đang giữ

7. Trading WebSocket

Package tradingws là client cho Trading WebSocket (/ws/v3/trading): gửi lệnh (đặt, sửa, huỷ, batch, lệnh điều kiện), truy vấn tài khoản và nhận sự kiện lệnh trên một socket. Package này không được nối vào facade Trading / Stream; hãy tạo client từ một *ssi.Auth. Dùng khi bạn muốn gửi lệnh qua một kết nối duy trì lâu dài thay vì gọi REST (5. Giao dịch), hoặc muốn nhận ack và sự kiện lệnh trên cùng socket.

Nâng cấp từ 3.3.1 trở về trước: đến 3.3.1 client này nằm trong package experimental. Từ 3.3.2 nó là package public tradingws, giữ nguyên tên, signature, giá trị mặc định và hành vi. Package cũ đã bị xoá, không có alias, nên import cũ sẽ không compile được. Đổi import thành github.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingws và đổi qualifier experimental. thành tradingws. (ví dụ tradingws.TradingWS(ctx, auth)). TradingWSError vẫn nằm ở package gốc ssisdk. Client vẫn là opt-in tường minh: Trading và Stream không tự kết nối tới nó.

7.1. Connecting and options

Điều kiện cần

  • Import: github.com/SSI-Securities-Inc/ssi-sdk-go/v3/tradingws.
  • Auth: phải gọi auth.Authenticate(otp) trên client ssi.NewAuth(config) trước. tradingws.TradingWS nhận một *ssi.Auth và xin TokenManager một token hợp lệ ở mỗi lần Connect.
  • Config.PrivateKey: bắt buộc với các lệnh làm thay đổi trạng thái (đặt, sửa, huỷ, batch, fco.place, fco.cancel). Không có private key thì SDK không gửi gì (fail closed). Truy vấn không cần ký.
  • Scope của token (server trả 403 khi thiếu):
    • Lệnh: tradingws:order:post, :put, :delete (đặt, sửa, huỷ, batch).
    • Lệnh điều kiện: tradingws:fco:post, :delete, :get.
    • Truy vấn: tradingws:position:get, :maxBuySell:get, :accountBalance:get, :ppmmrAccount:get, :orderBook:get.
  • URL: Config.TradingWSURL() (dựng từ Config.TradingWSDomain, xem Cấu hình). Chỉ chấp nhận wss://: client từ chối mọi URL khác vì bearer token đi trong header.
ws, err := tradingws.TradingWS(ctx, a,
    tradingws.WithRequestTimeoutMs(15000),
    tradingws.WithRespectRateLimit(true),
    tradingws.WithMaxRateLimitWaitMs(5000),
)
if err != nil {
    return err
}
defer ws.Close() // bắt buộc: đóng socket và đánh thức mọi request đang chờ

TradingWS(ctx, auth, opts...) tạo client, kết nối và trả về *tradingws.Service. Giá trị mặc định lấy từ Config: request timeout = Config.TimeoutMs (hoặc 10000 nếu chưa đặt), User-Agent = Config.UserAgent; opts ghi đè chúng.

Server chấp nhận socket trước (HTTP 101) rồi mới từ chối bằng frame {"code": 401|403|429|500, "msg": ...}. Vì vậy Connect chờ một khoảng ngắn và trả *ssisdk.TradingWSError nếu có frame từ chối đến.

Option. Mọi khoảng thời gian là số nguyên tính bằng millisecond với hậu tố Ms.

OptionMặc địnhMô tả
WithRequestTimeoutMs(ms)10000 (qua TradingWS: Config.TimeoutMs)Chờ response bao lâu thì bỏ cuộc. Không retry
WithSignatureEncoding(e)EncodingAutoEncodingAuto, EncodingBase64 hoặc EncodingHex. Giá trị khác bị từ chối với *ValidationError
WithUserAgent(ua)User-Agent của SDK (hoặc Config.UserAgent qua TradingWS)User-Agent của request upgrade. Firewall của gateway trả 403 cho User-Agent trần của thư viện
WithHandshakeGraceMs(ms)500Connect chờ frame từ chối bao lâu sau HTTP 101. 0 bỏ qua việc chờ
WithRespectRateLimit(on)trueKhi remaining về 0, client chờ resetMs thay vì nhận 429
WithMaxRateLimitWaitMs(ms)2000Giới hạn trên của thời gian chờ đó
WithPingIntervalMs(ms)30000Gửi ping_pong theo chu kỳ này để sớm phát hiện kết nối treo. 0 là tắt

7.2. Orders

Mỗi method dựng và validate payload giống trading.Service của REST (device id, idempotency key, giá thập phân thuần). Các method lệnh trả về ack tức thời (orderStatus = PD); kết quả thật đến sau qua luồng sự kiện order.<account>.

MethodTham sốTrả vềMô tả
PlaceOrderctx, accountNo, symbol string, side trading.OrderSide, quantity int, price float64, orderType trading.OrderType, clientRequestID string*trading.PlaceOrderResponse, errorĐặt lệnh (order.place). clientRequestID tối đa 20 ký tự; để trống thì SDK tự sinh
AmendOrderctx, accountNo string, price *float64, quantity *int, orderID, clientRequestID string*trading.ModifyOrderResponse, errorĐổi giá hoặc khối lượng (không đổi cả hai), xác định lệnh bằng đúng một trong orderID / clientRequestID (order.amend)
CancelOrderctx, accountNo, orderID, clientRequestID string*trading.CancelOrderResponse, errorHuỷ một lệnh, đúng một trong orderID / clientRequestID (order.cancel)
BatchNewOrdersctx, []trading.BatchPlaceOrderItem*tradingws.Response, errorĐặt tối đa Config.MaxBatchOrders lệnh (order.batchNew), validate cả lô. result.results liệt kê clientRequestId, orderStatus, success
BatchCancelOrdersctx, []trading.BatchCancelOrderItem*tradingws.Response, errorHuỷ tối đa Config.MaxBatchOrders lệnh (order.batchCancel)

Các kiểu item của batch giống REST, xem 5.4. Batch Orders.

7.3. Conditional orders (FCO)

MethodTham sốTrả vềMô tả
PlaceFCOctx, trading.FCOParamsRequest*trading.FCOPlaceResponse, errorĐặt lệnh điều kiện (fco.place), dùng cùng các kiểu params như REST
CancelFCOctx, fcoID, code string*trading.FCOCancelResponse, errorHuỷ lệnh điều kiện (fco.cancel). code là mã dùng một lần (OTP) khi tài khoản yêu cầu; bỏ qua khi để trống
ListFCOctx, *trading.FCOListRequest*tradingws.Response, errorLiệt kê lệnh điều kiện (fco.list). ProcessStatus nhận tối đa một trạng thái
FCOOrderBookctx, fcoID string, pageIndex, pageSize int*tradingws.Response, errorNhật ký khớp của một lệnh điều kiện (fco.orderbook). Trang mặc định 1 / 10
FCOStatusHistoryctx, fcoID string*tradingws.Response, errorCác lần chuyển trạng thái của một lệnh điều kiện (fco.statusHistory)

7.4. Queries

MethodTham sốTrả vềMô tả
QueryPositionctx, accountNo string, querySummary bool*Response, errorVị thế của tài khoản (query.position); querySummary thêm phần tóm tắt tài khoản
QueryMaxBuySellctx, accountNo, symbol string, price *float64*Response, errorKhối lượng mua/bán tối đa (query.maxBuySell). Với price == nil server dùng giá tham chiếu của nó
QueryAccountBalancectx, accountNo string*Response, errorSố dư tiền (query.accountBalance)
QueryPPMMRAccountctx, accountNo string*Response, errorSức mua (query.ppmmrAccount)
QueryOrderBookctx, tradingws.OrderBookQuery*Response, errorSổ lệnh của tài khoản (query.orderBook)

OrderBookQuery và *tradingws.Response (ID, Method, Status, Result, RateLimit, ReceivedTime) được mô tả ở Models.

7.5. Events (subscribe)

MethodTham sốMô tả
SubscribeOrderEventsaccountNo stringNhận order.<account> (trạng thái lệnh, khớp lệnh). data.eventType: orderEvent / orderMatchEvent
UnsubscribeOrderEventsaccountNo stringHuỷ subscription ở trên
SubscribePortfolioEventsaccountNo stringNhận portfolio.<account> (cập nhật vị thế phái sinh, eventType = clientPortfolioEvent)
UnsubscribePortfolioEventsaccountNo stringHuỷ subscription ở trên
UnsubscribeAllEventskhôngBỏ mọi subscription giao dịch trên socket này

Chỉ subscribe một tài khoản mỗi lần. Tài khoản rỗng, hoặc chứa * hay ,, bị từ chối ngay (*ValidationError) trước khi gửi gì, vì server từ chối wildcard và các tài khoản mà token không sở hữu.

Sự kiện đến handler Client().SetOnEvent(...) dưới dạng frame {"channel","topic","data"}; handler cũng nhận ack của subscribe và ack ping_pong (các frame không có command id). Panic trong handler được recover và log; không làm sập client.

7.6. Other methods

MethodMô tả
Service.Command(ctx, method, params)Gửi một command bất kỳ và trả về frame response đầu tiên (không retry). Dùng khi server có method mà SDK chưa bọc
Service.Client()Trả về *tradingws.Client cấp thấp
Service.Close()Đóng socket
Client.Connect(ctx)Kết nối lại. Không làm gì nếu đã kết nối
Client.IsConnected()Socket đang mở và reader đang chạy
Client.Request(ctx, method, params)Gửi command và trả về ack đầu tiên
Client.RequestStream(ctx, method, params)Gửi command và trả về *Call để đọc nhiều frame cùng id (ví dụ ack PD, rồi frame 200 mang sự kiện). Đọc bằng Call.Next(ctx, timeoutMs), giải phóng bằng Call.Close()
Client.Subscribe(channel, topics) / Unsubscribe(channel, topics)Frame subscribe cấp thấp
Client.Ping()Gửi PING_PONG; ack đến OnEvent
Client.RateLimit()Bản sao quota mới nhất server báo (remaining, resetMs, ...)
Client.Rejection()Frame từ chối sau upgrade (*TradingWSError) mà server đã gửi, nếu có
Client.SetOnEvent(fn)Đặt handler cho frame không có command id. nil là xoá
tradingws.IsMutating(method)Method có cần chữ ký hay không

7.7. How it behaves

  • Định dạng frame. {"id":"<không rỗng>","method":"order.place","params":{...},"signature":"<base64 hoặc hex>","timestamp":<epoch ms>}. Với command làm thay đổi trạng thái, signature được tính (RSA-SHA256) trên đúng chuỗi văn bản của params; chuỗi này chỉ được serialize một lần và chèn nguyên văn vào frame. Truy vấn không có chữ ký.
  • Encoding của chữ ký. Mặc định là auto: ký dạng base64; nếu server từ chối chữ ký với 401006, 401008 hoặc 401013 (command chưa được thực thi), gửi lại một lần dạng hex và nhớ lại cách nào chạy được. Nếu cả hai đều bị từ chối, error sẽ yêu cầu kiểm tra Config.PrivateKey có đúng là khóa đã đăng ký cho API key này không. Để cố định encoding, dùng WithSignatureEncoding.
  • Ack và event. Một command id có thể nhận nhiều frame: một ack tức thời (orderStatus = PD), sau đó là frame status 200 mang sự kiện. Request chỉ trả frame đầu; dùng RequestStream nếu cần các frame sau. Kết quả cuối cùng của lệnh luôn đến qua SubscribeOrderEvents.
  • Không retry. Command bị timeout không được gửi lại vì có thể đã được nhận. Ngoại lệ duy nhất là việc ký lại bằng hex ở trên (khi command chưa chạy).
  • Rate limit. Theo chú thích trong code: 50 command mỗi cửa sổ cho lệnh và 100 cho truy vấn. Các con số này lấy từ chú thích trong code SDK, không phải từ đặc tả server đã công bố; hãy đối chiếu với tài liệu server. Mỗi response mang rateLimit; khi remaining về 0, client chờ resetMs (tối đa WithMaxRateLimitWaitMs) nếu WithRespectRateLimit(true). Dùng ws.Client().RateLimit() để xem quota hiện tại.
  • Không tự reconnect. Khác với package stream, client này không tự kết nối lại hay subscribe lại. Nếu kết nối rớt (IsConnected() trả false), hãy gọi ws.Client().Connect(ctx) (token provider lấy token hợp lệ mỗi lần) và subscribe lại.
  • Context. Mọi method nhận một ctx. Nếu ctx bị huỷ hoặc hết hạn, method trả ctx.Err() ngay (khác với request timeout nội bộ). Nếu bạn huỷ ctx sau khi command đã được gửi, server vẫn có thể đã nhận nó.
  • Không có dữ liệu nhạy cảm trong log. Params, chữ ký, token và số tài khoản không bao giờ được log và không bao giờ nằm trong error.

Quan hệ với package stream

stream (facade ssi.NewStream)tradingws (Trading WebSocket)
Mục đíchNhận dữ liệu thị trường và sự kiện (subscribe, chỉ đọc)Gửi lệnh, truy vấn và nhận sự kiện lệnh
ReconnectTự kết nối lại và subscribe lạiKhông, gọi Client().Connect(ctx)
Khoảng thời gianSố nguyên MsSố nguyên Ms

Hai package dùng socket và endpoint riêng. Nếu bạn chỉ cần theo dõi sự kiện lệnh và không đặt lệnh qua socket, hãy dùng stream (6.5. Subscribe sự kiện giao dịch).

7.8. Examples

Cảnh báo: đây là lệnh thật. Chỉ chạy trên môi trường và tài khoản bạn được phép giao dịch.

Ví dụ 1: kết nối, subscribe sự kiện và đặt lệnh

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") // bắt buộc để ký lệnh

    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()

    // Sự kiện và ack không có command id đến đây.
    // Chỉ in channel và eventType, đừng in cả 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) // xem 7.9
        return
    }
    // Đây mới chỉ là ack (thường là PD). Kết quả thật đến dưới dạng sự kiện lệnh.
    fmt.Printf("ack: OrderID=%s Status=%s\n", placed.OrderID, placed.Status)

    time.Sleep(5 * time.Second) // chờ vài sự kiện rồi thoát (chỉ để minh hoạ)
}

Ví dụ 2: truy vấn và đọc 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))
}

// Quota mới nhất server báo (remaining, resetMs, ...).
fmt.Printf("rateLimit: %v\n", ws.Client().RateLimit())

Ví dụ 3: kết nối lại khi socket rớt

if !ws.Client().IsConnected() {
    if err := ws.Client().Connect(ctx); err != nil {
        reportWSError("Connect", err)
        return
    }
    // Subscription không được khôi phục tự động.
    if err := ws.SubscribeOrderEvents(accountNo); err != nil {
        log.Printf("resubscribe: %v", err)
    }
    // Command gửi trước khi rớt có thể đã được nhận: đối chiếu bằng
    // QueryOrderBook hoặc sự kiện lệnh trước khi gửi lại.
}

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):
        // Server từ chối: Status, ErrorCode, ServerMessage.
        // Status 429 là rate limit; 401/403 là token hoặc scope.
        fmt.Printf("%s: server từ chối status=%d code=%s: %s\n",
            op, wsErr.Status, wsErr.ErrorCode, wsErr.ServerMessage)
    case errors.As(err, &vErr):
        fmt.Printf("%s: dữ liệu không hợp lệ (chưa gửi gì): %v\n", op, vErr)
    case errors.Is(err, context.DeadlineExceeded), errors.Is(err, context.Canceled):
        fmt.Printf("%s: context đã kết thúc: %v\n", op, err)
    case errors.As(err, &connErr):
        // Timeout (không retry), mất kết nối, hoặc chưa kết nối.
        fmt.Printf("%s: lỗi websocket: %v\n", op, connErr)
    default:
        fmt.Printf("%s: lỗi: %v\n", op, err)
    }
}
LỗiKhi nàoNên làm gì
*ssisdk.TradingWSError (Status, ErrorCode, ServerMessage)Frame lỗi từ server (status 4xx hoặc "error", ví dụ FCO_ERROR với fco.*), hoặc frame từ chối sau upgrade (401 token, 403 thiếu scope tradingws:*, 429, 500)Đọc Status và ErrorCode. Với 401, xác thực lại; với 403, kiểm tra scope; 429 là rate limit, chờ theo quota (Client().RateLimit()) rồi gửi lại có kiểm soát; với 409 do dùng lại clientRequestID, đừng đặt lại dưới id mới cho tới khi biết lệnh gốc có đang sống không
*ssisdk.ValidationErrorDữ liệu sai (tài khoản rỗng hoặc có wildcard, clientRequestID quá 20 ký tự, signature encoding không hợp lệ, params không phải JSON hợp lệ, ...)Sửa dữ liệu. Chưa có gì được gửi
*ssisdk.WebSocketErrorChưa kết nối, gửi thất bại, mất kết nối trước khi có response, timeout chờ response (không retry), URL không phải wss://, thiếu private key với command làm thay đổi trạng tháiVới timeout: command có thể đã được nhận, hãy đối chiếu qua sự kiện lệnh hoặc QueryOrderBook trước khi gửi lại
context.DeadlineExceeded / context.Canceledctx hết hạn hoặc bị huỷXử lý như timeout: đừng giả định command chưa được nhận

Client WebSocket trả rate limit dưới dạng *TradingWSError với Status 429, không phải *RateLimitError (kiểu đó chỉ dành cho REST). Chữ ký bị từ chối (401006, 401008, 401013) ở chế độ auto được gửi lại một lần với encoding còn lại; nếu vẫn lỗi, hãy kiểm tra Config.PrivateKey hoặc cố định encoding bằng WithSignatureEncoding. Xem Xử lý lỗi để biết danh sách đầy đủ.

Liên quan: Hướng dẫn: Trading WebSocket, Client Classes.

Trên trang này