.NET

API Services

Chi tiết các method API cho từng service trong .NET SDK

1. Xác thực (TokenManager)

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

Xác thực với OTP

var token = await auth.AuthenticateAsync("YOUR_OTP");
Console.WriteLine($"Expires at (epoch seconds): {token.ExpiresAt}");

ExpiresAt / RefreshTokenExpiresAt là epoch giây (0 = không rõ, SDK coi như đã hết hạn). Không log AccessToken.

AuthenticateAsync(otp?, transactionId?) là một lần gọi duy nhất. Với Smart OTP, truyền transactionId (không truyền cùng lúc với otp, nếu không sẽ ném AuthenticationException). Khi người dùng chưa duyệt trên app, SDK ném SmartOtpPendingException (HTTP 202 / code 401114).

Yêu cầu gửi OTP

OtpResponse otp = await auth.RequestOtpTypedAsync();
Console.WriteLine(otp.Message);
// otp.TransactionId (string?) dùng cho luồng Smart OTP

RequestOtpAsync() vẫn còn và trả về JsonElement thô; nên dùng RequestOtpTypedAsync().

Làm mới token

var token = await auth.RefreshAsync();

Tự động đảm bảo xác thực

// OTP thường
var accessToken = await auth.EnsureAuthenticatedAsync("YOUR_OTP");

// Smart OTP (duyệt trên app): polling theo Config.OtpPollIntervalMs / OtpPollMaxWaitMs
var otp = await auth.RequestOtpTypedAsync();
await auth.EnsureAuthenticatedAsync(transactionId: otp.TransactionId);

Thứ tự xử lý: token hiện tại, refresh, OTP, rồi polling Smart OTP. Chi tiết và các exception xem Approve OTP.


2. Tài khoản (AccountService)

Truy cập qua trading.Account.

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

var accounts = await trading.Account.GetAccountInfoAsync();

foreach (var acc in accounts)
{
    Console.WriteLine($"{acc.AccountNo} - {acc.AccountType}");
}

Trả về: List<Account> — mỗi Account có AccountNo: string, AccountType: string?.


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:

MethodTimeframe
GetOhlc1MinuteAsync(symbol)1 phút
GetOhlc3MinuteAsync(symbol)3 phút
GetOhlc5MinuteAsync(symbol)5 phút
GetOhlc15MinuteAsync(symbol)15 phút
GetOhlc1HourAsync(symbol)1 giờ
var ohlc = await data.MarketData.GetOhlc1MinuteAsync("SSI");
foreach (var candle in ohlc)
{
    Console.WriteLine($"{candle.TradingDate}: O={candle.OpenPrice} H={candle.HighPrice} L={candle.LowPrice} C={candle.ClosePrice} V={candle.Volume}");
}

Lịch sử (truyền khoảng ngày + phân trang):

MethodTimeframe
GetOhlc1MinuteHistoricalAsync(symbol, fromDate, toDate, page, size)1 phút
GetOhlc3MinuteHistoricalAsync(...)3 phút
GetOhlc5MinuteHistoricalAsync(...)5 phút
GetOhlc15MinuteHistoricalAsync(...)15 phút
GetOhlc1HourHistoricalAsync(...)1 giờ
GetOhlc1DayHistoricalAsync(...)1 ngày
var ohlc = await data.MarketData.GetOhlc1DayHistoricalAsync(
    symbol: "SSI",
    fromDate: "2026/03/27",
    toDate: "2026/04/22",
    page: 1,
    size: 100);

Tham số historical:

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
toDatestring(bắt buộc)Ngày kết thúc
pageint1Trang
sizeint1000Số bản ghi/trang

Trả về: List<OhlcData>

Quy ước ngày: nếu fromDate chỉ có ngày thì SDK thêm 00:00:00, toDate thêm 23:59:59. Với timeframe khác 1d, toDate ở tương lai bị kẹp về hôm nay 23:59:59. Không truyền ngày nghĩa là lấy trong ngày hôm nay.

Đã ngừng hỗ trợ: GetOhlc1WeekHistoricalAsync và GetOhlc1MonthHistoricalAsync được đánh dấu [Obsolete] và luôn ném ValidationException (code 400210). Timeframe.Allowed gồm 1d, 1m, 3m, 5m, 15m, 1h.

Duyệt toàn bộ trang:

// IAsyncEnumerable<OhlcData>, tự phân trang (size mặc định 1000)
await foreach (var c in data.MarketData.IterOhlcAsync("SSI", Timeframe.Minute5, "2026/03/01", "2026/03/31"))
{
    Console.WriteLine($"{c.TradingDate} C={c.ClosePrice}");
}

// Gom thành List (ascending = false là mặc định)
List<OhlcData> all = await data.MarketData.GetOhlcAllAsync("SSI", Timeframe.Day1, "2026/03/01", "2026/03/31");

3.1b. Download OHLC

Các method DownloadOhlc*Async trả về Task<List<OhlcData>> nhưng chưa được hỗ trợ: gọi sẽ ném NotImplementedException. Dùng GetOhlc*HistoricalAsync hoặc IterOhlcAsync.

3.2. Danh sách chỉ số thị trường

// Tất cả chỉ số
var indexes = await data.MarketData.GetIndexesAsync();

// Theo sàn
var indexesByBoard = await data.MarketData.GetIndexesByBoardAsync(Board.HOSE);

foreach (var idx in indexes)
{
    Console.WriteLine($"{idx.Index} - {idx.IndexName}");
}

Trả về: List<MarketIndexes>

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

// Summary hiện tại
var summary = await data.MarketData.GetIndexSummaryAsync("VNINDEX");

// Summary lịch sử
var summaryHist = await data.MarketData.GetIndexSummaryHistoricalAsync("VNINDEX", "2026/01/15");

// Summary theo sàn
var boardSummary = await data.MarketData.GetBoardSummaryAsync(Board.HOSE);

// Summary sàn lịch sử
var boardSummaryHist = await data.MarketData.GetBoardSummaryHistoricalAsync(Board.HOSE, "2026/01/15");

Trả về: MarketIndexSummary?

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

// 1 mã
var info = await data.MarketData.GetSecuritiesInfoAsync("SSI");

// Theo chỉ số
var byIndex = await data.MarketData.GetSecuritiesInfoByIndexAsync("VN30");

// Theo sàn
var byBoard = await data.MarketData.GetSecuritiesInfoByBoardAsync(Board.HOSE);

Trả về: SecuritiesInfo? / List<SecuritiesInfo>

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

// Summary hiện tại
var summary = await data.MarketData.GetSecuritiesSummaryAsync("SSI");

// Summary lịch sử
var summaryHist = await data.MarketData.GetSecuritiesSummaryHistoricalAsync(
    symbol: "SSI", fromDate: "2026/03/01", toDate: "2026/03/31");

// Summary theo chỉ số
var byIndex = await data.MarketData.GetSecuritiesSummaryByIndexAsync("VN30");

// Summary theo chỉ số lịch sử
var byIndexHist = await data.MarketData.GetSecuritiesSummaryByIndexHistoricalAsync(
    index: "VN30", fromDate: "2026/03/01", toDate: "2026/03/31");

Trả về: List<SecuritiesSummary> (SDK tự phân trang cho các method summary).

3.6. Master data

// Hôm nay
List<MasterData> today = await data.MarketData.GetMasterDataAsync();

// Lịch sử
List<MasterData> hist = await data.MarketData.GetMasterDataHistoricalAsync("2026/03/01", "2026/03/31");

Trả về: List<MasterData> (tự phân trang).

3.7. Corporate actions (sự kiện quyền)

Sự kiện quyền của một mã hoặc của toàn thị trường: cổ tức, cổ phiếu thưởng, phát hành thêm... Hai method tách theo thời điểm. Cả hai tự gọi hết các trang (pageSize 100) và trả về List<CorporateAction>.

// Hôm nay (mọi mã, hoặc một mã)
List<CorporateAction> today = await data.MarketData.GetCorporateActionsAsync();
List<CorporateAction> ssi = await data.MarketData.GetCorporateActionsAsync("SSI", "en");

// Quá khứ: khoảng ngày tối đa 5 ngày
List<CorporateAction> past = await data.MarketData.GetCorporateActionsHistoricalAsync(
    fromDate: "2026/10/08", toDate: "2026/10/12",
    symbol: null,       // null = tất cả mã
    language: "vn");    // "vn" (mặc định) hoặc "en"

foreach (var e in past)
{
    Console.WriteLine($"{e.Symbol} {e.EventTitle} {e.ExrightDate} {e.RecordDate} {e.IssueDate} {e.Exchange} {e.Ratio}");
}
MethodTham sốTrả vềMô tả
GetCorporateActionsAsyncstring? symbol = null, string language = "vn", ctTask<List<CorporateAction>>Sự kiện quyền của hôm nay
GetCorporateActionsHistoricalAsyncstring fromDate, string toDate, string? symbol = null, string language = "vn", ctTask<List<CorporateAction>>Sự kiện quyền trong khoảng ngày fromDate đến toDate

SDK kiểm tra tham số trước khi gọi. Sai thì ném ValidationException và không gọi mạng:

  • fromDate / toDate đúng định dạng YYYY/MM/DD, fromDate không sau toDate, khoảng cách giữa hai ngày không quá 5 ngày.
  • symbol (nếu có) là một mã chữ-số duy nhất, tối đa 20 ký tự. null nghĩa là tất cả mã.
  • language là vn hoặc en.

Trường của CorporateAction xem Models.

Lưu ý: hai method này được viết theo tài liệu API chính thức và chưa được chạy thử trên server thật (theo changelog SDK 3.3.2). Hãy tự kiểm tra kết quả trước khi dùng trong luồng quan trọng.


4. Danh mục đầu tư (PortfolioService)

Truy cập qua trading.Portfolio.

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

// Số dư cơ sở
var equityBalance = await trading.Portfolio.GetEquityBalanceAsync("YOUR_ACCOUNT_NO");
Console.WriteLine($"Available cash: {equityBalance?.AccountBalance}");

// Số dư phái sinh
var derivativeBalance = await trading.Portfolio.GetDerivativeBalanceAsync("YOUR_ACCOUNT_NO");
Console.WriteLine($"Withdrawable: {derivativeBalance?.Withdrawable}");

4.2. Vị thế (Positions)

// Vị thế cơ sở
var positions = await trading.Portfolio.GetEquityPositionsAsync("YOUR_ACCOUNT_NO");
foreach (var pos in positions ?? new List<EquityPosition>()) // có thể trả về null
{
    Console.WriteLine($"{pos.Symbol}: {pos.Quantity} cp | Giá vốn: {pos.CostPrice}");
}

// Tất cả vị thế phái sinh (chia Open/Closed)
var allDerivative = await trading.Portfolio.GetDerivativePositionsAsync("YOUR_ACCOUNT_NO");

// Chỉ vị thế mở phái sinh
var openPos = await trading.Portfolio.GetOpenDerivativePositionsAsync("YOUR_ACCOUNT_NO");

// Chỉ vị thế đóng phái sinh
var closedPos = await trading.Portfolio.GetClosedDerivativePositionsAsync("YOUR_ACCOUNT_NO");

4.3. Sổ lệnh (Order Book)

// Lệnh trong ngày
var todayOrders = await trading.Portfolio.GetTodayOrdersAsync("YOUR_ACCOUNT_NO");
foreach (var order in todayOrders)
{
    Console.WriteLine($"{order.OrderId}: {order.Symbol} {order.Side} {order.Quantity}@{order.Price} - {order.Status}");
}

// Lệnh lịch sử
var historicalOrders = await trading.Portfolio.GetHistoricalOrdersAsync(
    "YOUR_ACCOUNT_NO", fromDate: "2026/01/01", toDate: "2026/01/31");

4.4. PPMMR

// PPMMR cơ sở
var equityPpmmr = await trading.Portfolio.GetEquityPpmmrAsync("YOUR_ACCOUNT_NO");
Console.WriteLine($"Purchasing power: {equityPpmmr?.PurchasingPower}");

// PPMMR phái sinh
var derivativePpmmr = await trading.Portfolio.GetDerivativePpmmrAsync("YOUR_ACCOUNT_NO");

Tổng quan method Portfolio

NhómMethodTrả về
Số dưGetEquityBalanceAsync(accountNo)EquityAccountBalance?
GetDerivativeBalanceAsync(accountNo)DerivativeAccountBalance?
Vị thếGetEquityPositionsAsync(accountNo)List<EquityPosition>?
GetDerivativePositionsAsync(accountNo)AllDerivativePosition?
GetOpenDerivativePositionsAsync(accountNo)List<DerivativePosition>
GetClosedDerivativePositionsAsync(accountNo)List<DerivativePosition>
Sổ lệnhGetTodayOrdersAsync(accountNo)List<Order>
GetHistoricalOrdersAsync(accountNo, fromDate, toDate)List<Order>
PPMMRGetEquityPpmmrAsync(accountNo)EquityPPMMR?
GetDerivativePpmmrAsync(accountNo)DerivativePPMMR?

5. Giao dịch (TradingService)

Truy cập qua trading.Trading.

5.1. Đặt lệnh

Giá (price) là decimal, khối lượng (quantity) là int. clientRequestId là tham số tuỳ chọn, đứng cuối: tối đa 20 ký tự, SDK tự sinh khi để null. Mọi lệnh POST/PUT/DELETE được ký số bằng PrivateKey.

Kiểm soát đã áp dụng cho các ví dụ giao dịch: số tài khoản chỉ là placeholder, PrivateKey/OTP lấy từ biến môi trường hoặc nhập tay (không hardcode), SDK validate clientRequestId (tối đa 20 ký tự) trước khi gửi, lệnh ghi không bị retry tự động.

// Lệnh giới hạn (LO)
var result = await trading.Trading.PlaceLimitOrderAsync(
    accountNo: "YOUR_ACCOUNT_NO", symbol: "SSI", side: OrderSide.Buy, quantity: 100, price: 66000);
Console.WriteLine($"Order ID: {result.OrderId}, Status: {result.Status}");

// Lệnh thị trường (MTL)
var market = await trading.Trading.PlaceMarketOrderAsync(
    "YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100);

// Lệnh ATO (mở cửa)
var ato = await trading.Trading.PlaceAtoOrderAsync(
    "YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100);

// Lệnh ATC (đóng cửa)
var atc = await trading.Trading.PlaceAtcOrderAsync(
    "YOUR_ACCOUNT_NO", "SSI", OrderSide.Sell, 100);

// Lệnh tuỳ chỉnh (chỉ định OrderType)
var custom = await trading.Trading.PlaceOrderAsync(
    "YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100, 66000, OrderType.LO);

5.2. Sửa lệnh

Chỉ sửa giá hoặc khối lượng (không đồng thời).

// Sửa giá theo ClientRequestId
var r1 = await trading.Trading.ModifyOrderPriceAsync("YOUR_ACCOUNT_NO", "REQ123", 68000);

// Sửa giá theo OrderId
var r2 = await trading.Trading.ModifyOrderPriceByOrderIdAsync("YOUR_ACCOUNT_NO", "ORD123", 68000);

// Sửa khối lượng theo ClientRequestId
var r3 = await trading.Trading.ModifyOrderQuantityAsync("YOUR_ACCOUNT_NO", "REQ123", 200);

// Sửa khối lượng theo OrderId
var r4 = await trading.Trading.ModifyOrderQuantityByOrderIdAsync("YOUR_ACCOUNT_NO", "ORD123", 200);

5.3. Huỷ lệnh

// Huỷ theo ClientRequestId
var c1 = await trading.Trading.CancelOrderAsync("YOUR_ACCOUNT_NO", "REQ123");

// Huỷ theo OrderId
var c2 = await trading.Trading.CancelOrderByOrderIdAsync("YOUR_ACCOUNT_NO", "ORD123");

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

// Với giá cụ thể
var maxBS = await trading.Trading.GetMaxBuySellAsync("YOUR_ACCOUNT_NO", "SSI", 66000);
Console.WriteLine($"Max buy: {maxBS.MaxBuyQuantity}");
Console.WriteLine($"Max sell: {maxBS.MaxSellQuantity}");

// Với giá thị trường
var maxBSMarket = await trading.Trading.GetMaxBuySellAtMarketPriceAsync("YOUR_ACCOUNT_NO", "SSI");

Tổng quan method Trading

NhómMethodTrả về
Đặt lệnhPlaceOrderAsync(accountNo, symbol, side, quantity, price?, orderType, clientRequestId?)PlaceOrderResponse
PlaceLimitOrderAsync(accountNo, symbol, side, quantity, price, clientRequestId?)PlaceOrderResponse
PlaceMarketOrderAsync(accountNo, symbol, side, quantity, clientRequestId?)PlaceOrderResponse
PlaceAtoOrderAsync(accountNo, symbol, side, quantity, clientRequestId?)PlaceOrderResponse
PlaceAtcOrderAsync(accountNo, symbol, side, quantity, clientRequestId?)PlaceOrderResponse
Lệnh hàng loạtPlaceBatchOrdersAsync(orders)BatchOrderResponse
CancelBatchOrdersAsync(orders)BatchOrderResponse
Sửa lệnhModifyOrderPriceAsync(accountNo, clientRequestId, price)ModifyOrderResponse
ModifyOrderPriceByOrderIdAsync(accountNo, orderId, price)ModifyOrderResponse
ModifyOrderQuantityAsync(accountNo, clientRequestId, quantity)ModifyOrderResponse
ModifyOrderQuantityByOrderIdAsync(accountNo, orderId, quantity)ModifyOrderResponse
Huỷ lệnhCancelOrderAsync(accountNo, clientRequestId)CancelOrderResponse
CancelOrderByOrderIdAsync(accountNo, orderId)CancelOrderResponse
Sức mua/bánGetMaxBuySellAsync(accountNo, symbol, price)MaxBuySellResponse
GetMaxBuySellAtMarketPriceAsync(accountNo, symbol)MaxBuySellResponse
Lệnh điều kiệnPlaceFcoGtdAsync(accountNo, symbol, side, quantity, price, priceSlip, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoStopAsync(accountNo, symbol, side, quantity, stopPrice, operatorType, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoStopLimitAsync(accountNo, symbol, side, quantity, price, priceSlip, stopPrice, operatorType, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoTrailingStopAsync(accountNo, symbol, side, quantity, activePrice, trailingAmount, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoTrailingStopLimitAsync(accountNo, symbol, side, quantity, activePrice, trailingAmount, priceSlip, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoOcoAsync(accountNo, symbol, side, quantity, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, code?)FCOPlaceResponse
PlaceFcoBullBearAsync(accountNo, symbol, side, quantity, price, priceSlip, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, code?)FCOPlaceResponse
CancelFcoAsync(fcoId, code?)FCOCancelResponse
GetFcoByAccountNoAsync(accountNo, pageIndex?, pageSize?)FCOListResponse
GetFcoBySymbolAsync(accountNo, symbol, pageIndex?, pageSize?)FCOListResponse
GetFcoByStatusAsync(accountNo, processStatus, pageIndex?, pageSize?)FCOListResponse
GetFcoByDateAsync(accountNo, fromDate, toDate, pageIndex?, pageSize?)FCOListResponse
GetFcoByIdAsync(accountNo, fcoId)FCOInfo?
GetFcoOrderBookAsync(fcoId, pageIndex = 1, pageSize = 10)FCOOrderBookResponse
GetFcoStatusHistoryAsync(fcoId)List<FCOStatusHistoryItem>

5.5. Lệnh điều kiện (FCO)

Hỗ trợ các loại lệnh điều kiện linh hoạt (Flexible Conditional Order): GTD, Stop, Stop Limit, Trailing Stop, Trailing Stop Limit, OCO và Bull Bear. Lệnh gửi đi cũng được ký số tự động như lệnh thường.

using SsiSdk;

string accountNo = "YOUR_ACCOUNT_NO";
string fromDate = "2026/07/21 00:00:00";
string toDate = "2026/07/28 23:00:00";

// Lệnh GTD (Good Till Date)
var gtd = await trading.Trading.PlaceFcoGtdAsync(
    accountNo, "SSI", OrderSide.Buy, 100, 25000, 0.5m, fromDate, toDate);

// Lệnh Stop (dừng thị trường)
var stop = await trading.Trading.PlaceFcoStopAsync(
    accountNo, "SSI", OrderSide.Sell, 100, 24000m, FCOOperator.LesserOrEqual, fromDate, toDate);

// Lệnh Stop Limit (dừng giới hạn)
var stopLimit = await trading.Trading.PlaceFcoStopLimitAsync(
    accountNo, "SSI", OrderSide.Buy, 100, 25500, 0.5m, 25000m, FCOOperator.GreaterOrEqual, fromDate, toDate);

// Lệnh Trailing Stop (bám sát xu hướng)
var trailing = await trading.Trading.PlaceFcoTrailingStopAsync(
    accountNo, "SSI", OrderSide.Buy, 100, 26000m, 1000m, fromDate, toDate);

// Lệnh Trailing Stop Limit
var trailingLimit = await trading.Trading.PlaceFcoTrailingStopLimitAsync(
    accountNo, "SSI", OrderSide.Buy, 100, 26000m, 1000m, 0.5m, fromDate, toDate);

// Lệnh OCO (huỷ lệnh kia khi 1 trong 2 khớp)
var oco = await trading.Trading.PlaceFcoOcoAsync(
    accountNo, "SSI", OrderSide.Sell, 100, 30000m, 24000m, OrderType.MTL, OrderType.MTL, 0.5m, 0.5m, fromDate, toDate);

// Lệnh Bull Bear
var bullBear = await trading.Trading.PlaceFcoBullBearAsync(
    accountNo, "SSI", OrderSide.Buy, 1000, 100.5m, 0.5m, 30m, 20m, 30, 10, 0.1m, 0m, fromDate, toDate);

// Huỷ lệnh điều kiện
var cancelRes = await trading.Trading.CancelFcoAsync(gtd.FCOID);

Quy tắc ngày của lệnh đặt FCO: định dạng yyyy/MM/dd HH:mm:ss, fromDate <= toDate, khoảng không quá 31 ngày (Constants.MaxFcoWindowDays). Tham số price / tpPrice / slPrice kiểu FcoPrice (nhận ngầm từ decimal, int, long hoặc chuỗi ATO/ATC/MTL/MP/MOK/MAK; với các lệnh thị trường này không dùng priceSlip). Mọi method PlaceFco* và CancelFcoAsync nhận thêm code tuỳ chọn.

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

// Lấy toàn bộ lệnh FCO theo tài khoản
var fcos = await trading.Trading.GetFcoByAccountNoAsync("YOUR_ACCOUNT_NO");

// Lấy FCO theo mã chứng khoán
var fcosBySymbol = await trading.Trading.GetFcoBySymbolAsync("YOUR_ACCOUNT_NO", "SSI");

// Lấy FCO theo trạng thái xử lý
var fcosByStatus = await trading.Trading.GetFcoByStatusAsync("YOUR_ACCOUNT_NO", FCOStatus.Trit);

// Lấy FCO theo khoảng thời gian
var fcosByDate = await trading.Trading.GetFcoByDateAsync("YOUR_ACCOUNT_NO", "2026/07/01", "2026/07/21");

// Lấy một lệnh FCO cụ thể bằng ID
var fcoInfo = await trading.Trading.GetFcoByIdAsync("YOUR_ACCOUNT_NO", "fco-id-here");

// Lấy nhật ký khớp lệnh (order book) của lệnh FCO
var orderBook = await trading.Trading.GetFcoOrderBookAsync("fco-id-here");

// Lịch sử trạng thái của lệnh FCO
List<FCOStatusHistoryItem> history = await trading.Trading.GetFcoStatusHistoryAsync("fco-id-here");

Lưu ý: Các method truy vấn danh sách (GetFcoByAccountNoAsync, GetFcoBySymbolAsync, GetFcoByStatusAsync, GetFcoByDateAsync) có pageIndex và pageSize kiểu int?: để null thì dùng giá trị mặc định của server (SDK không tự đặt mặc định). Riêng GetFcoOrderBookAsync có mặc định pageIndex = 1, pageSize = 10. GetFcoByStatusAsync có thêm overload nhận IReadOnlyCollection<string> để lọc nhiều trạng thái. Chưa xác nhận GetFcoByDateAsync có bắt buộc phần giờ hay không; ví dụ trên chỉ dùng ngày.

5.6. Lệnh hàng loạt (Batch)

PlaceBatchOrdersAsync và CancelBatchOrdersAsync gửi nhiều lệnh trong một request REST có chữ ký: POST /order/batch để đặt, DELETE /order/batch để huỷ (đường dẫn đầy đủ /api/v3/trading/order/batch). Mỗi lệnh có một kết quả riêng trong response. Dùng khi cần đặt/huỷ nhiều lệnh cùng lúc (cân bằng danh mục, huỷ cả rổ lệnh) và muốn ký một lần, một round trip, một lượt tính rate limit thay vì N. Với một lệnh đơn lẻ, dùng PlaceOrderAsync / CancelOrderAsync (5.1, 5.3).

Cảnh báo: các method batch đặt và huỷ lệnh thật. Thử trên tài khoản thử nghiệm và xác nhận nội dung batch trước khi gửi.

Điều kiện tiên quyết: Config có ApiKey, ApiSecret, PrivateKey (mọi request batch đều được ký RSA PKCS#1 v1.5 + SHA-256); AuthClient đã xác thực (Client Classes); API Key có quyền đặt lệnh (trading:order:post theo API reference) trên mọi tài khoản trong batch.

MethodTham sốTrả vềMô tả
PlaceBatchOrdersAsyncIReadOnlyCollection<BatchPlaceOrderItem> orders, ctTask<BatchOrderResponse>Đặt từ 1 đến Config.MaxBatchOrders lệnh (POST /order/batch)
CancelBatchOrdersAsyncIReadOnlyCollection<BatchCancelOrderItem> orders, ctTask<BatchOrderResponse>Huỷ từ 1 đến Config.MaxBatchOrders lệnh (DELETE /order/batch)

Cấu trúc các item và response (BatchPlaceOrderItem, BatchCancelOrderItem, BatchOrderResponse, BatchOrderResult) xem Models lệnh hàng loạt.

Giới hạn số lệnh mỗi batch. Config.MaxBatchOrders mặc định 20 (giá trị nhỏ hơn 1 được đặt lại về 20). Giới hạn được kiểm tra trước khi gửi: batch rỗng hoặc vượt giới hạn ném ValidationException và không có request nào được gửi. Batch qua Trading WebSocket (mục 7) dùng cùng giới hạn.

Lưu ý (chưa thống nhất): API reference (trade-order-batch-place) ghi giới hạn phía server là 50 lệnh mỗi request, trong khi mặc định của SDK là 20. Việc nâng Config.MaxBatchOrders do bên gọi quyết định; hãy theo giới hạn áp dụng cho API Key của bạn.

Cách hoạt động

  • Validate trước. SDK validate mọi lệnh (số lượng, giá, độ dài ClientRequestId, mã huỷ) trước khi dựng body. Một lệnh không hợp lệ làm ném ValidationException và từ chối cả batch ngay tại client. Mỗi item huỷ cần đúng một trong OrderId hoặc ClientRequestId.
  • Một chữ ký cho toàn bộ body. SDK serialize body một lần, ký đúng chuỗi đó và gửi cùng chuỗi đó kèm chữ ký trong header X-Signature. Bạn không phải tự ký.
  • Phong bì batch. Body gồm batchRequestId, batchRequestTime (epoch mili-giây) và orders. SDK sinh batchRequestId mới ở mỗi lần gọi. Mỗi lệnh mang deviceId (mã máy của hệ điều hành, không đặt được trong Config) và userAgent (Config.UserAgent, nếu không có thì dùng SSI .NET SDK/<version>).
  • Kết quả theo từng lệnh. Khi batch đã qua validate, các lệnh được xử lý độc lập: một lệnh Success = false (ví dụ không đủ sức mua) không ảnh hưởng lệnh khác. Luôn kiểm tra từng phần tử của Results.
  • Không tự gửi lại. POST và DELETE không bao giờ được gửi lại sau timeout hay 429, vì lệnh lặp có thể bị tạo hai lần.

Idempotency. SDK tạo batchRequestId mới cho mỗi lần gọi, nên gọi PlaceBatchOrdersAsync hai lần với cùng danh sách là hai batch khác nhau theo SDK. Thứ định danh một lệnh giữa các lần thử là ClientRequestId do bạn đặt: gán một id ổn định cho mỗi lệnh (tối đa 20 ký tự), giữ lại và dùng lại khi retry hoặc huỷ. Nếu server nhận ra trùng lặp, SDK ném DuplicateRequestException (HTTP 409).

Lưu ý: API reference mô tả batchRequestId là khoá idempotency của batch (duy nhất theo API Key, theo hành động đặt/huỷ, theo ngày). SDK không cho bạn truyền giá trị này, nên hãy dựa vào ClientRequestId.

Kiểm soát đã áp dụng cho các ví dụ batch: khoá/OTP đọc từ biến môi trường hoặc nhập tay, tài khoản là placeholder, SDK validate toàn bộ batch trước khi gửi, ClientRequestId ổn định để chống lệnh trùng, không retry tự động với POST/DELETE.

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

using SsiSdk;
using SsiSdk.Models;

var runId = Guid.NewGuid().ToString("N")[..8];
var items = new List<BatchPlaceOrderItem>
{
    new("YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100, 25000m, OrderType.LO, ClientRequestId: $"b-{runId}-1"),
    new("YOUR_ACCOUNT_NO", "VNM", OrderSide.Buy, 100, 60000m, OrderType.LO, ClientRequestId: $"b-{runId}-2"),
};

try
{
    var response = await trading.Trading.PlaceBatchOrdersAsync(items);
    foreach (var r in response.Results)
    {
        Console.WriteLine(r.Success
            ? $"OK   {r.ClientRequestId} -> OrderId={r.OrderId} Status={r.Status}"
            : $"FAIL {r.ClientRequestId}: {r.ErrorCode} {r.ErrorMessage}");
    }
}
catch (ValidationException ex)
{
    // Batch rỗng, vượt giới hạn, hoặc có lệnh không hợp lệ: chưa gửi gì
    Console.WriteLine($"SDK từ chối: {ex.Message}");
}
catch (DuplicateRequestException ex)
{
    Console.WriteLine($"Trùng lặp (409): {ex.Message}");
}
catch (RateLimitException ex)
{
    // SDK không gửi lại POST; tự chờ và quyết định
    Console.WriteLine($"Bị rate limit, thử lại sau {ex.RetryAfterMs} ms");
}
catch (AuthenticationException ex)
{
    Console.WriteLine($"Lỗi xác thực/chữ ký: {ex.Message}");
}
catch (ApiException ex)
{
    Console.WriteLine($"Lỗi API [{ex.Code}] HTTP {ex.StatusCode}: {ex.Message}");
}

DuplicateRequestException kế thừa ApiException, nên phải catch trước ApiException.

Ví dụ 2: huỷ lệnh từ kết quả đặt

static async Task CancelPlacedAsync(TradingClient trading, BatchOrderResponse placed)
{
    // Ưu tiên OrderId; nếu không có thì dùng ClientRequestId. Mỗi item đúng một mã.
    var cancelItems = placed.Results
        .Where(r => r.Success)
        .Select(r => !string.IsNullOrEmpty(r.OrderId)
            ? new BatchCancelOrderItem("YOUR_ACCOUNT_NO", OrderId: r.OrderId)
            : new BatchCancelOrderItem("YOUR_ACCOUNT_NO", ClientRequestId: r.ClientRequestId))
        .ToList();

    if (cancelItems.Count == 0) return;

    var cancelResponse = await trading.Trading.CancelBatchOrdersAsync(cancelItems);
    foreach (var r in cancelResponse.Results)
        Console.WriteLine($"{(r.Success ? "OK  " : "FAIL")} {r.ClientRequestId} {r.ErrorCode} {r.ErrorMessage}");
}

Ví dụ 3: chia danh sách lớn thành nhiều batch

// allItems: List<BatchPlaceOrderItem> dựng như ví dụ 1
var limit = config.MaxBatchOrders;
foreach (var chunk in allItems.Chunk(limit))
{
    // Gửi lần lượt; để exception dừng vòng lặp thay vì gửi tiếp phần còn lại một cách mù quáng.
    var resp = await trading.Trading.PlaceBatchOrdersAsync(chunk);
    var failed = resp.Results.Count(r => !r.Success);
    Console.WriteLine($"batch xong: {resp.Results.Count} lệnh, {failed} lỗi");
}

Sau timeout của một POST, kết quả là không xác định: hãy kiểm tra sổ lệnh (ví dụ trading.Portfolio.GetTodayOrdersAsync) theo ClientRequestId trước khi gửi lại.

Xử lý lỗi batch

Tình huốngSDK némGhi chú
Batch rỗng, vượt MaxBatchOrders, lệnh không hợp lệ, item huỷ có 0 hoặc 2 mãValidationExceptionChưa gửi gì
Thiếu hoặc sai chữ kýAuthenticationException (HTTP 401/403)Các mã như 401006, 401008, 401013 (xem ServerErrorCode). Kiểm tra PrivateKey.
Rate limit (HTTP 429)RateLimitExceptionCó RetryAfterMs, Limit, Remaining, Reset. Không tự chờ với POST/DELETE.
Trùng lặp (HTTP 409)DuplicateRequestExceptionKế thừa ApiException
Lỗi mức batch từ server (HTTP 4xx/5xx)ApiExceptionCode, StatusCode, ResponseBody. Cả batch bị từ chối.
Timeout với POST/DELETEException timeout lan raKết quả không xác định, đừng gửi lại mù quáng
Một lệnh bị từ chốiKhông có exceptionSuccess = false kèm ErrorCode / ErrorMessage trong Results

Phụ lục mã lỗi liệt kê các mã mức batch như 400108 (rỗng hoặc quá nhiều lệnh), 400109 (thiếu batchRequestId / batchRequestTime), 400111 (batchRequestTime ngoài cửa sổ replay), 403101 (tài khoản không được phép làm cả batch bị từ chối) và 409. Một số mã trùng số với lỗi lệnh đơn: dựa vào endpoint đã gọi và nội dung thông báo để phân biệt. Xem Mã lỗi.

Hướng dẫn từng bước: Tutorial 14: Đặt/huỷ lệnh hàng loạt.


6. Streaming realtime (StreamingService)

Truy cập qua stream.Streaming. Cần gọi stream.ConnectAsync() trước.

6.1. Kết nối

using var stream = new StreamClient(auth);

await stream.ConnectAsync();       // Kết nối WebSocket

// Chờ vô thời hạn
await stream.WaitAsync();

// Chờ với timeout (mili-giây)
await stream.WaitAsync(timeoutMs: 30000);

await stream.DisconnectAsync();     // Ngắt kết nối thủ công (nếu không dùng using); cũng có bản đồng bộ Disconnect()

Socket tự reconnect sau khi rớt và gửi lại các subscription đã ghi nhận. Theo dõi trạng thái qua OnConnection (ConnectionEvent với State là connected, reconnected, disconnected hoặc failed, kèm ReconnectCount và Error), hoặc stream.IsConnected, stream.LastError, stream.ReconnectCount.

6.2. Thiết lập callback

using SsiSdk.Models;

stream.Streaming.SetOnData(msg =>
{
    switch (msg)
    {
        case TradeMessage t: Console.WriteLine($"Trade: {t.Symbol} {t.Price}"); break;
        case QuoteMessage q: Console.WriteLine($"Quote: {q.Symbol}"); break;
    }
});

stream.Streaming.SetOnTrading(msg =>
{
    if (msg is OrderStatusMessage order)
        Console.WriteLine($"Order: {order.Symbol} {order.Status}");
});

stream.Streaming.SetOnHeartbeat(hb => Console.WriteLine($"Heartbeat: {hb.Status}"));

Đặt handler trước khi subscribe để không mất message. Ngoài SetOn*, có thể gán trực tiếp các property: OnData, OnTrading (Action<object>), OnHeartbeat (HeartbeatMessage), OnMarketFlag (MarketFlagMessage), OnIndexSummary (IndexSummaryMessage), OnOrder (OrderStatusMessage), OnOrderMatch (OrderMatchMessage), OnPortfolio (PortfolioMessage), OnConnection (ConnectionEvent). Nếu callback ném exception, SDK nuốt exception, chỉ ghi log tên kiểu và giữ kết nối.

6.3. Subscribe dữ liệu thị trường

// Từng kênh riêng
await stream.Streaming.SubscribeSymbolTradeAsync(["SSI", "HPG"]);
await stream.Streaming.SubscribeSymbolQuoteAsync(["SSI", "HPG"]);
await stream.Streaming.SubscribeSymbolRoomAsync(["SSI", "HPG"]);
await stream.Streaming.SubscribeSymbolPutThroughAsync(["SSI"]);
await stream.Streaming.SubscribeSymbolOddLotAsync(["SSI"]);

// Tất cả kênh (trade + quote + room) cho mã
await stream.Streaming.SubscribeSymbolAsync(["SSI", "HPG", "VIC"]);

// OHLCV theo interval
await stream.Streaming.SubscribeSymbolOhlcvAsync(["SSI"], Timeframe.Minute1);

// Theo sàn (fan-out @*, xem lưu ý bên dưới)
await stream.Streaming.SubscribeBoardAsync([Board.HOSE, Board.HNX]);

// Theo chỉ số (fan-out @* các mã trong rổ, xem lưu ý bên dưới)
await stream.Streaming.SubscribeIndexAsync(["VNINDEX", "VN30"]);

// Master data (trần, sàn, tham chiếu) của mã hoặc sàn
await stream.Streaming.SubscribeMarketAsync(["SSI"]);

// Cờ phiên giao dịch (ATO, LO, ATC, ...) của các sàn
await stream.Streaming.SubscribeMarketFlagAsync();

// Tick, nến và tổng hợp của chính chỉ số: xem 6.7

Lưu ý (chưa xác nhận): SubscribeBoardAsync và SubscribeIndexAsync gửi topic dạng trade.CODE@*, quote.CODE@*, room.CODE@*. Ngữ nghĩa fan-out @* chưa được xác nhận với server thật; hãy kiểm tra thực tế trước khi dựa vào. Cả hai có thể sinh rất nhiều message.

6.4. Unsubscribe

await stream.Streaming.UnsubscribeSymbolTradeAsync(["SSI"]);
await stream.Streaming.UnsubscribeSymbolQuoteAsync(["SSI"]);
await stream.Streaming.UnsubscribeSymbolRoomAsync(["SSI"]);
await stream.Streaming.UnsubscribeSymbolPutThroughAsync(["SSI"]);
await stream.Streaming.UnsubscribeSymbolOddLotAsync(["SSI"]);
await stream.Streaming.UnsubscribeSymbolAsync(["SSI", "HPG"]);

// Huỷ OHLCV
await stream.Streaming.UnsubscribeSymbolOhlcvAsync(["SSI"], Timeframe.Minute1);

await stream.Streaming.UnsubscribeBoardAsync([Board.HOSE]);
await stream.Streaming.UnsubscribeIndexAsync(["VNINDEX"]);
await stream.Streaming.UnsubscribeMarketAsync(["SSI"]);
await stream.Streaming.UnsubscribeMarketFlagAsync();

// Huỷ tất cả và xem server đang giữ gì
var removed = await stream.Streaming.UnsubscribeAllAsync();               // Dictionary<string, List<string>>
var active = await stream.Streaming.ListSubscriptionAsync(timeoutMs: 5000); // khoá "trading" và "data"

6.5. Subscribe giao dịch

// Trạng thái lệnh (tất cả tài khoản, mặc định "*")
await stream.Streaming.SubscribeOrderStatusAsync();

// Cho tài khoản cụ thể
await stream.Streaming.SubscribeOrderStatusAsync("YOUR_ACCOUNT_NO");

// Portfolio
await stream.Streaming.SubscribePortfolioAsync();
await stream.Streaming.SubscribePortfolioAsync("YOUR_ACCOUNT_NO");

6.6. Heartbeat

// Ping một lần
await stream.Streaming.PingAsync();

// Ping tự động theo interval (mili-giây)
await stream.Streaming.PingAsync(intervalMs: 30000);

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

Message types nhận qua SetOnData

TopicMessage TypeMô tả
trade.*TradeMessageDữ liệu khớp lệnh
trade.*.<timeframe>IntervalMessageDữ liệu OHLCV theo interval
quote.*QuoteMessageDữ liệu giá (bid/ask)
room.*ForeignRoomMessageRoom nước ngoài
trade.index.*IndexTickMessage (hoặc IntervalMessage với @1m/@5m)Tick / nến của chỉ số (xem 6.7)
indexsummary.*IndexSummaryMessageTổng hợp chỉ số (xem 6.7)
market.flagMarketFlagMessageCờ phiên giao dịch
market.*MarketDataMessage (nếu có s/ce/fl/ref), ngược lại MarketStatusMessageMaster data / trạng thái thị trường
put.*PutMessageThoả thuận
oddlot.*OddLotMessageLô lẻ

Message không khớp kiểu nào ở trên được giữ nguyên dạng JsonElement.

Message types nhận qua SetOnTrading

SDK chọn kiểu theo trường eventType của message:

eventTypeMessage TypeMô tả
orderEventOrderStatusMessageTrạng thái lệnh
orderMatchEventOrderMatchMessageKhớp lệnh
clientPortfolioEventPortfolioMessageThay đổi danh mục
fcoEventFCOOrderUpdateMessageCập nhật lệnh điều kiện (FCO)

Nếu eventType rỗng thì tiền tố topic order. / portfolio. quyết định kiểu. Không có handler riêng cho fcoEvent: nhận qua OnTrading/SetOnTrading. Message không khớp được giữ nguyên dạng JsonElement.

Lưu ý (chưa xác nhận): code SDK parse eventType == "fcoEvent" thành FCOOrderUpdateMessage, nhưng CHANGELOG của SDK liệt kê streaming fcoEvent là chưa triển khai. Chưa kiểm chứng với server thật; đừng dựa vào luồng này cho logic quan trọng.

Tổng quan method Streaming

NhómMethodMô tả
Kết nốiConnectAsync()Kết nối WebSocket
DisconnectAsync() / Disconnect()Ngắt kết nối
WaitAsync(timeoutMs?)Chờ nhận dữ liệu
IsConnected, LastError, ReconnectCountTrạng thái kết nối
OnConnectionCallback ConnectionEvent
HeartbeatPingAsync(intervalMs?)Ping server (tuỳ chọn lặp)
StopPingLoop()Dừng ping loop tự động
Subscribe mãSubscribeSymbolAsync(symbols)Trade + Quote + Room
SubscribeSymbolTradeAsync(symbols)Chỉ trade
SubscribeSymbolQuoteAsync(symbols)Chỉ quote
SubscribeSymbolRoomAsync(symbols)Chỉ foreign room
SubscribeSymbolPutThroughAsync(symbols)Chỉ thoả thuận
SubscribeSymbolOddLotAsync(symbols)Chỉ lô lẻ
SubscribeSymbolOhlcvAsync(symbols, interval)OHLCV theo timeframe
Subscribe sàn/chỉ sốSubscribeBoardAsync(boards)Theo sàn (fan-out @*, chưa xác nhận)
SubscribeIndexAsync(indices)Mã trong rổ chỉ số (fan-out @*, chưa xác nhận)
SubscribeIndexTradeAsync(indices, interval?)Tick / nến của chính chỉ số
SubscribeIndexSummaryAsync(indices)Tổng hợp chỉ số
Subscribe thị trườngSubscribeMarketAsync(symbols)Master data (trần, sàn, tham chiếu)
SubscribeMarketFlagAsync()Cờ phiên giao dịch
Subscribe giao dịchSubscribeOrderStatusAsync(accountNo = "*")Trạng thái lệnh (bao gồm cả sự kiện FCO)
SubscribeFcoOrderStatusAsync(accountNo = "*")Alias của SubscribeOrderStatusAsync
SubscribePortfolioAsync(accountNo = "*")Portfolio changes
UnsubscribePortfolioAsync(accountNo = "*") / UnsubscribeOrderStatusAsync(accountNo = "*")Huỷ subscribe giao dịch
Quản lýListSubscriptionAsync(timeoutMs = 5000)Subscription server đang giữ (Dictionary<string, List<string>>)
UnsubscribeAllAsync()Huỷ mọi subscription, trả về những gì đã huỷ
UnsubscribeUnsubscribeSymbolAsync(symbols)Huỷ tất cả kênh cho mã
UnsubscribeSymbolTradeAsync(symbols)Huỷ trade
UnsubscribeSymbolQuoteAsync(symbols)Huỷ quote
UnsubscribeSymbolRoomAsync(symbols)Huỷ foreign room
UnsubscribeSymbolPutThroughAsync(symbols)Huỷ thoả thuận
UnsubscribeSymbolOddLotAsync(symbols)Huỷ lô lẻ
UnsubscribeSymbolOhlcvAsync(symbols, interval)Huỷ OHLCV
UnsubscribeBoardAsync(boards)Huỷ sàn
UnsubscribeIndexAsync(indices)Huỷ chỉ số
UnsubscribeIndexTradeAsync(indices, interval?)Huỷ tick / nến chỉ số (cùng interval lúc subscribe)
UnsubscribeIndexSummaryAsync(indices)Huỷ tổng hợp chỉ số
UnsubscribeMarketAsync(symbols) / UnsubscribeMarketFlagAsync()Huỷ master data / cờ phiên

Mọi method có thêm CancellationToken ct; các method Subscribe* có thêm Action<JsonElement>? onResponse để nhận frame xác nhận của server. SDK còn nhiều alias (ví dụ SubscribeOrdersAsync, SubscribeTradesAsync, SubscribeQuotesAsync, SubscribeIndexTicksAsync, SubscribeIndexConstituentsAsync) trỏ về các method ở trên.

6.7. Dữ liệu chỉ số thời gian thực

StreamingService stream dữ liệu về chỉ số thị trường (VNINDEX, VN30, HNXINDEX, ...) qua WebSocket dữ liệu thị trường. Có ba loại dữ liệu chỉ số, mỗi loại một topic:

Bạn muốnTopicMethodKiểu message
Tick của chính chỉ số (giá trị, thay đổi, số mã tăng/giảm, tổng)trade.index.CODESubscribeIndexTradeAsyncIndexTickMessage
Nến chỉ số (open, high, low, close, volume) mỗi 1 hoặc 5 phúttrade.index.CODE@1m hoặc @5mSubscribeIndexTradeAsync kèm intervalIntervalMessage
Tổng hợp chỉ số (ảnh chụp các thống kê của chỉ số)indexsummary.CODESubscribeIndexSummaryAsyncIndexSummaryMessage
Trade, quote và room khối ngoại của các mã trong rổ chỉ sốtrade, quote, room với .CODE@*SubscribeIndexAsyncTradeMessage, QuoteMessage, ForeignRoomMessage

Dùng khi xây bảng giá, biểu đồ chỉ số hoặc dashboard. Danh sách chỉ số và tổng hợp chỉ số lịch sử qua REST xem mục 3.2 và 3.3.

Điều kiện tiên quyết: AuthClient đã xác thực; một StreamClient đã ConnectAsync (method nằm ở stream.Streaming); API Key có quyền stream dữ liệu thị trường (API reference ghi scope stream:DATA:indexsummary cho tổng hợp chỉ số). Streaming chỉ số là chỉ đọc, không cần PrivateKey.

MethodTham sốMô tả
SubscribeIndexTradeAsyncstring[] indices, string? interval = nullTick của chỉ số (trade.index.CODE). Với interval là "1m" hoặc "5m" thì là nến chỉ số.
UnsubscribeIndexTradeAsyncstring[] indices, string? interval = nullHuỷ subscribe. Truyền cùng interval đã dùng khi subscribe.
SubscribeIndexSummaryAsyncstring[] indicesTổng hợp chỉ số (indexsummary.CODE). Không có interval.
UnsubscribeIndexSummaryAsyncstring[] indicesHuỷ subscribe
SubscribeIndexAsync / UnsubscribeIndexAsyncstring[] indicesTrade, quote, room của các mã trong rổ, gửi dưới dạng trade.CODE@*, quote.CODE@*, room.CODE@*

Lưu ý (chưa xác nhận): ngữ nghĩa fan-out @* của SubscribeIndexAsync / SubscribeBoardAsync chưa được xác nhận với server thật. SDK chỉ dựng topic và gửi đi. Ngoài ra, ý nghĩa chính xác của từng trường tick (đơn vị của Value, TotalMatchQuantity... có phải cộng dồn trong phiên hay không) chưa được xác nhận với server; bảng model ở Utilities chỉ là ánh xạ khoá server sang thuộc tính. Dùng Raw / Data khi cần đối chiếu.

Quy tắc topic. SDK kiểm tra topic trước khi gửi (Topic.Build), vi phạm sẽ ném ValidationException mà không có lời gọi mạng nào.

  • Mỗi topic một chỉ số. Mảng string[] indices thành một topic cho mỗi phần tử; danh sách dấu phẩy trong một chuỗi bị từ chối, mảng rỗng cũng bị từ chối.
  • Mã gồm chữ và số. SDK tự viết hoa, và alias UPCOMINDEX được đổi thành HNXUPCOMINDEX.
  • interval chỉ hợp lệ với trade.index, và phải là "tick", "1m" hoặc "5m". Khi có interval, chỉ số phải là một mã cụ thể (không phải wildcard).
  • indexsummary không nhận interval. Theo API reference, nó chỉ nhận mã chỉ số, không nhận mã sàn hay mã chứng khoán.
  • Fan-out @* của SubscribeIndexAsync cần một mã chỉ số cụ thể. Không hợp lệ với trade.index hay indexsummary.
  • SubscribeIndexAsync sinh rất nhiều message (mọi mã trong rổ): dùng có chủ đích, nhất là với tiến trình chạy lâu.

Handler. Đặt handler trước khi ConnectAsync/subscribe. Dùng OnData (hoặc SetOnData) cho mọi message dữ liệu đã parse (IndexTickMessage, IntervalMessage, IndexSummaryMessage, TradeMessage, ...) và pattern matching theo kiểu; OnIndexSummary chỉ nhận tổng hợp chỉ số; OnConnection theo dõi kết nối. Không có handler riêng theo kiểu cho tick hoặc nến chỉ số.

Cấu trúc IndexTickMessage, IntervalMessage, IndexSummaryMessage xem Models streaming. Parser trả về IntervalMessage khi topic có hậu tố @1m hoặc @5m, ngược lại trả về IndexTickMessage. Trường số của tick là null khi server không gửi.

API reference liệt kê các khoá của tổng hợp chỉ số: s, b, ss, t, v, c, cr, adv, dec, nc, ce, fl, tmv, tmq, tvp, tqp, tq, tv; tq bằng tmq cộng tqp, và tv bằng tmv cộng tvp. IndexSummaryMessage không ánh xạ sang thuộc tính có kiểu: đọc bằng Get(params string[] keys) (trả về JsonElement?) hoặc Data.

Ví dụ 1: stream tick VNINDEX và nến 1 phút

using SsiSdk;
using SsiSdk.Models;

stream.Streaming.OnConnection = e =>
    Console.WriteLine($"[CONN] {e.State} (reconnect #{e.ReconnectCount})" +
                      (e.Error is null ? "" : $" lỗi: {e.Error.GetType().Name}"));

stream.Streaming.OnData = msg =>
{
    switch (msg)
    {
        case IntervalMessage bar:
            Console.WriteLine($"[NẾN] {bar.Symbol} {bar.IntervalTime} O:{bar.Open} H:{bar.High} L:{bar.Low} C:{bar.Close} V:{bar.Volume}");
            break;
        case IndexTickMessage idx:
            Console.WriteLine($"[INDEX] {idx.Symbol} {idx.Value} ({idx.ChangePercent}%) tăng/giảm: {idx.Advances}/{idx.Declines}");
            break;
    }
};

await stream.ConnectAsync();

await stream.Streaming.SubscribeIndexTradeAsync(["VNINDEX", "VN30"]);       // tick
await stream.Streaming.SubscribeIndexTradeAsync(["VNINDEX"], "1m");         // nến 1 phút

await stream.WaitAsync(60_000);   // lắng nghe 60 giây

await stream.Streaming.UnsubscribeIndexTradeAsync(["VNINDEX", "VN30"]);
await stream.Streaming.UnsubscribeIndexTradeAsync(["VNINDEX"], "1m");      // cùng interval với lúc subscribe
await stream.DisconnectAsync();

Ví dụ 2: tổng hợp chỉ số với handler có kiểu

stream.Streaming.OnIndexSummary = m =>
    Console.WriteLine($"[SUMMARY] {m.Index} value={m.Get("v")} change%={m.Get("cr")}");

await stream.ConnectAsync();
await stream.Streaming.SubscribeIndexSummaryAsync(["VNINDEX", "HNXINDEX"]);

await stream.WaitAsync(60_000);

Ví dụ 3: liệt kê và dọn subscription

var subs = await stream.Streaming.ListSubscriptionAsync(timeoutMs: 5000);
foreach (var (channel, topics) in subs)
    Console.WriteLine($"{channel}: {topics.Count} topic");

var removed = await stream.Streaming.UnsubscribeAllAsync();
Console.WriteLine($"Đã huỷ {removed.Values.Sum(v => v.Count)} topic");

Channel trading của ListSubscriptionAsync có thể chứa topic có số tài khoản: đừng in nguyên văn.

Xử lý lỗi streaming chỉ số

Tình huốngSDK némGhi chú
Mã chỉ số sai, danh sách dấu phẩy, mảng rỗng, interval saiValidationExceptionChưa gửi gì
Subscribe trước ConnectAsyncWebSocketException ("Not connected. Call ConnectAsync() first.")Gọi ConnectAsync trước
Kết nối thất bại sau các lần thử lạiWebSocketExceptionDo ConnectAsync ném
Mất kết nốiKhông có exception cho bên gọiSDK tự reconnect và gửi lại subscription; theo dõi OnConnection. Nếu bỏ cuộc thì State là failed và LastError được đặt.
Exception trong callback của bạnSDK nuốt và ghi logTự xử lý lỗi bên trong callback

Tài liệu này chưa mô tả cách server dữ liệu thị trường báo thiếu scope. Xử lý SsiException như lớp cơ sở chung (xem Xử lý lỗi) và kiểm tra scope của API Key.

Hướng dẫn thêm: Tutorial 16: WebSocket nâng cao và WebSocket dữ liệu thị trường.


7. Trading WebSocket

Các class TradingWs* (TradingWs, TradingWsOptions, TradingWsClient, TradingWsService, TradingWsResponse) nằm trong namespace gốc SsiSdk, cùng chỗ với AuthClient, Config và TradingWsError.

Chuyển từ 3.3.1 sang 3.3.2 (breaking): từ 3.3.2 Trading WebSocket không còn là tính năng thử nghiệm và các class trên đã chuyển từ SsiSdk.Experimental sang SsiSdk. Namespace cũ bị xoá, không có alias hay [Obsolete], nên using SsiSdk.Experimental; sẽ không compile. Chỉ cần đổi using SsiSdk.Experimental; thành using SsiSdk;. Tên, chữ ký và giá trị mặc định không đổi. Nguồn log đổi từ ssi_sdk.experimental.trading_ws thành ssi_sdk.trading_ws.

Trading WebSocket là một socket riêng (mặc định wss://api.ssi.com.vn/ws/v3/trading) dành cho lệnh giao dịch. Bạn gửi một frame lệnh (order.place, query.position, ...) và nhận lại một frame xác nhận, đồng thời có thể subscribe sự kiện lệnh và danh mục của một tài khoản trên cùng socket. Dùng khi muốn gửi lệnh và truy vấn qua một kết nối duy trì thay vì mỗi lần một request REST. Đây không phải socket dữ liệu thị trường: với giá và dữ liệu thị trường hãy dùng StreamClient (so sánh).

7.1. Điều kiện tiên quyết và kết nối

  • API Key được phép dùng trading WebSocket (scope tradingws:*:* theo API reference; tối đa 10 kết nối cho mỗi API Key).
  • Config có ApiKey, ApiSecret và PrivateKey. PrivateKey là bắt buộc với mọi lệnh làm thay đổi trạng thái (7.3).
  • AuthClient đã xác thực. TradingWs.ConnectAsync lấy access token từ auth.EnsureAuthenticatedAsync, nên hãy xác thực trước (OTP / Smart OTP, xem Client Classes).
  • URL được suy ra từ Config.TradingWsDomain (mặc định wss://api.ssi.com.vn/ws/v3/trading); Config.TradingWsUrl chỉ đọc. SDK tự thêm wss:// nếu thiếu scheme và /ws/v3/trading nếu thiếu path. URL cuối cùng phải là wss://, nếu không constructor của client ném WebSocketException.
using SsiSdk;
using SsiSdk.Models;

await using var ws = await TradingWs.ConnectAsync(auth);
Console.WriteLine($"Đã kết nối: {ws.Client.IsConnected}");
Thành phầnTham sốTrả vềMô tả
TradingWs.ConnectAsyncAuthClient auth, TradingWsOptions? options = null, ctTask<TradingWsService>Mở socket bằng token hiện tại và trả về service. Nếu kết nối lỗi, đóng client và ném lại exception.
TradingWsService.CloseAsync / DisposeAsynckhôngTask / ValueTaskĐóng socket
TradingWsService.ClientkhôngTradingWsClientClient mức thấp: OnEvent, RateLimit, IsConnected, RequestAsync, SubscribeAsync, UnsubscribeAsync, PingAsync

Server có thể chấp nhận socket rồi từ chối ngay sau đó (token sai, thiếu scope, rate limit). SDK chờ HandshakeGraceMs để nhận frame từ chối này rồi ném TradingWsError. Frame từ chối không có id và mang code là 401, 403, 429 hoặc 500.

TradingWsOptions

Tuỳ chọnKiểuMặc địnhMô tả
RequestTimeoutMsint?nullThời gian chờ response của một lệnh. Qua TradingWs.ConnectAsync, null trở thành Config.TimeoutMs (mặc định 60000, hoặc 10000 nếu giá trị đó nhỏ hơn hoặc bằng 0). TradingWsClient tạo trực tiếp dùng 10000.
SignatureEncodingstring"auto""auto", "base64" hoặc "hex". Giá trị khác ném ValidationException.
HandshakeGraceMsint500Thời gian chờ frame từ chối sau khi kết nối
RespectRateLimitbooltrueChờ khi server báo đã hết hạn mức
MaxRateLimitWaitMsint2000Thời gian chờ tối đa cho rate limit
PingIntervalMsint?30000Chu kỳ ping. null hoặc 0 tắt vòng ping.
UserAgentstring?nullnull dùng Config.UserAgent

Mọi khoảng thời gian là số nguyên mili-giây. Comment XML của RequestTimeoutMs trong SDK vẫn ghi "10000 when unset"; bảng trên phản ánh đúng theo code.

7.2. Các thao tác

Tất cả thao tác nằm trong TradingWsService. Mỗi thao tác gửi một lệnh và trả về sau khi nhận frame xác nhận. Lệnh không bao giờ được gửi lại (xem "Reconnect, timeout và rate limit" ở 7.3).

Lệnh thường

MethodLệnhTham sốTrả vềMô tả
PlaceOrderAsyncorder.placeaccountNo, symbol, side, int quantity, decimal? price, orderType, clientRequestId?, ctPlaceOrderResponseĐặt lệnh mới. Validate giống PlaceOrderAsync của REST.
AmendOrderAsyncorder.amendaccountNo, decimal? price, int? quantity, orderId?, clientRequestId?, ctModifyOrderResponseSửa giá hoặc số lượng (không sửa cả hai)
CancelOrderAsyncorder.cancelaccountNo, orderId?, clientRequestId?, ctCancelOrderResponseHuỷ lệnh theo orderId hoặc clientRequestId
BatchNewOrdersAsyncorder.batchNewIReadOnlyCollection<BatchPlaceOrderItem> orders, ctTradingWsResponseĐặt batch (1 đến Config.MaxBatchOrders)
BatchCancelOrdersAsyncorder.batchCancelIReadOnlyCollection<BatchCancelOrderItem> orders, ctTradingWsResponseHuỷ batch

Lưu ý: theo API reference, frame xác nhận lệnh qua WebSocket chỉ mang trạng thái PD (chờ xử lý) cho mỗi lệnh và không có orderId; kết quả thật đến sau dưới dạng sự kiện trên order.<accountNo>. Hãy subscribe sự kiện lệnh nếu cần trạng thái cuối. Model item của batch giống REST, xem 5.6 và Models lệnh hàng loạt.

Lệnh điều kiện (FCO)

MethodLệnhMô tả
PlaceFcoGtdAsync, PlaceFcoStopAsync, PlaceFcoStopLimitAsync, PlaceFcoTrailingStopAsync, PlaceFcoTrailingStopLimitAsync, PlaceFcoOcoAsync, PlaceFcoBullBearAsyncfco.placeĐặt lệnh điều kiện (FCOPlaceResponse). Tham số giống các method FCO của REST (5.5), thêm code tuỳ chọn.
CancelFcoAsync(fcoId, code?, ct)fco.cancelHuỷ lệnh điều kiện (FCOCancelResponse)
ListFcoAsync(accountNo, fcoId?, type?, processStatus?, symbol?, side?, fromDate?, toDate?, pageIndex?, pageSize?, ct)fco.listLiệt kê lệnh điều kiện (TradingWsResponse). Chỉ nhận một processStatus.
FcoOrderBookAsync(fcoId, pageIndex?, pageSize?, ct)fco.orderbookCác lệnh sinh ra từ một lệnh điều kiện
FcoStatusHistoryAsync(fcoId, ct)fco.statusHistoryLịch sử trạng thái của một lệnh điều kiện

Truy vấn (chỉ đọc) — tất cả trả về TradingWsResponse.

MethodLệnhTham sốMô tả
QueryPositionAsyncquery.positionaccountNo, bool querySummary = trueVị thế
QueryMaxBuySellAsyncquery.maxBuySellaccountNo, symbol, decimal? price = nullKhối lượng mua/bán tối đa
QueryAccountBalanceAsyncquery.accountBalanceaccountNoSố dư tài khoản
QueryPpmmrAccountAsyncquery.ppmmrAccountaccountNoThông tin PPMMR
QueryOrderBookAsyncquery.orderBookaccountNo, fromDate?, toDate?, symbol?, orderStatus?, pageIndex?, pageSize?Sổ lệnh

Subscribe sự kiện. Sự kiện được gửi trên channel TRADING với topic order.<accountNo> và portfolio.<accountNo>.

MethodTham sốMô tả
SubscribeOrderEventsAsync / UnsubscribeOrderEventsAsyncaccountNo, ctSubscribe / huỷ sự kiện lệnh của một tài khoản
SubscribePortfolioEventsAsync / UnsubscribePortfolioEventsAsyncaccountNo, ctSubscribe / huỷ sự kiện danh mục của một tài khoản
UnsubscribeAllEventsAsyncctHuỷ mọi subscription giao dịch của socket này

Subscribe mỗi lần một tài khoản. Ký tự đại diện * hoặc danh sách ngăn cách bằng dấu phẩy ném ValidationException. Sự kiện đến ở ws.Client.OnEvent, một Action<JsonElement> nhận JSON thô (socket này không có handler theo kiểu).

Lệnh tổng quát: CommandAsync(string method, object? parameters, ct) gửi lệnh bất kỳ và trả về frame phản hồi đầu tiên (TradingWsResponse), không retry. Cấu trúc TradingWsResponse xem Utilities.

7.3. Chữ ký

Các lệnh làm thay đổi trạng thái được ký: order.place, order.amend, order.cancel, order.batchNew, order.batchCancel, fco.place và fco.cancel (không phân biệt hoa thường). Truy vấn và subscribe không ký.

  • Frame có dạng {"id", "method", "params", "signature", "timestamp"}. Chữ ký tính trên đúng chuỗi compact của params, mặc định mã hoá Base64.
  • Lệnh thay đổi trạng thái mà không có PrivateKey ném WebSocketException.
  • Ở chế độ "auto", nếu server từ chối lệnh thay đổi trạng thái vì lỗi chữ ký (401006, 401008 hoặc 401013), SDK thử lại một lần với kiểu mã hoá còn lại. Cách này an toàn vì chữ ký bị từ chối nghĩa là lệnh chưa được thực thi. Nếu cả hai kiểu đều bị từ chối, SDK ném TradingWsError kèm gợi ý (BothRefused). Đặt SignatureEncoding là "base64" hoặc "hex" để tránh lần thử thêm.
  • Chữ ký và params đã ký không bao giờ xuất hiện trong thông báo exception.

Reconnect, timeout và rate limit

  • Không tự reconnect. Nếu kết nối rớt, tạo TradingWsService mới bằng TradingWs.ConnectAsync và tự subscribe lại. Các lệnh đang chờ sẽ lỗi với WebSocketException.
  • Không retry lệnh (trừ lần thử lại kiểu mã hoá chữ ký ở trên). Timeout ném WebSocketException("timed out waiting for a response (not retried)"). Nếu socket đóng trước, thông báo là "connection closed before the response arrived". Gửi khi chưa kết nối ném "not connected". Sau timeout hoặc socket đóng, kết quả của lệnh thay đổi trạng thái là không xác định: kiểm tra sổ lệnh hoặc sự kiện trước khi gửi lại.
  • Ping. SDK ping mỗi PingIntervalMs. Vòng ping dừng khi gặp WebSocketException.
  • Rate limit. Khi RespectRateLimit bật và response gần nhất báo remaining == 0, lệnh kế tiếp chờ min(resetMs, MaxRateLimitWaitMs) rồi mới gửi. ws.Client.RateLimit giữ hạn mức gần nhất. Frame từ chối có code 429 được báo bằng TradingWsError.

7.4. Khác biệt với StreamClient

StreamClient (Streaming)Trading WebSocket (TradingWs)
Mục đíchDữ liệu thị trường và sự kiện giao dịch, kiểu đẩyLệnh giao dịch, truy vấn và sự kiện lệnh, kiểu lệnh/xác nhận
URLConfig.StreamingUrlConfig.TradingWsUrl (suy ra từ TradingWsDomain)
ReconnectTự động, subscribe lại các topic đã ghi nhậnKhông có, bạn tự kết nối và subscribe lại
Sự kiệnHandler theo kiểu (OnData, OnOrder, ...) và OnConnectionJsonElement thô ở Client.OnEvent
SubscriptionNhiều topic, từ chối danh sách dấu phẩy, quy tắc topic ở TopicMỗi lần một tài khoản, không wildcard

Cả hai nằm trong namespace SsiSdk. Hai socket độc lập nhau: có thể dùng đồng thời.

7.5. Ví dụ

Kiểm soát đã áp dụng cho các ví dụ Trading WebSocket: khoá và OTP lấy từ biến môi trường hoặc nhập tay, tài khoản là placeholder, chữ ký do SDK tạo bằng PrivateKey (không bao giờ vào log hay exception), clientRequestId tối đa 20 ký tự và được SDK validate trước khi gửi, ví dụ in hình dạng kết quả thay vì nội dung (tránh lộ số tài khoản), lệnh không bị retry tự động.

Ví dụ 1: truy vấn chỉ đọc và sự kiện lệnh. Chỉ đọc; in tên method và tên trường, không in nội dung vì kết quả có thể chứa số tài khoản.

var account = "YOUR_ACCOUNT_NO";

static string Shape(TradingWsResponse r)
{
    var keys = r.Result.ValueKind == System.Text.Json.JsonValueKind.Object
        ? string.Join(",", r.Result.EnumerateObject().Select(p => p.Name).Take(8))
        : r.Result.ValueKind.ToString();
    return $"status={r.Status} method={r.Method} result=[{keys}]";
}

try
{
    await using var ws = await TradingWs.ConnectAsync(auth, new TradingWsOptions
    {
        RequestTimeoutMs = 10_000,
        PingIntervalMs = 30_000,
    });

    var events = 0;
    ws.Client.OnEvent = _ => Interlocked.Increment(ref events);

    Console.WriteLine(Shape(await ws.QueryAccountBalanceAsync(account)));
    Console.WriteLine(Shape(await ws.QueryPositionAsync(account)));
    Console.WriteLine(Shape(await ws.QueryMaxBuySellAsync(account, "SSI")));

    await ws.SubscribeOrderEventsAsync(account);
    await Task.Delay(TimeSpan.FromSeconds(30));
    await ws.UnsubscribeOrderEventsAsync(account);
    Console.WriteLine($"Số sự kiện nhận được: {events}");
}
catch (TradingWsError ex)
{
    // Server từ chối socket hoặc một lệnh
    Console.WriteLine($"Trading WS bị từ chối: status={ex.Status} code={ex.ErrorCode} {ex.ServerMessage}");
}
catch (SsiException ex)
{
    Console.WriteLine($"Lỗi: {ex.GetType().Name} [{ex.Code}] {ex.Message}");
}

Ví dụ 2: đặt rồi huỷ một lệnh.

Cảnh báo: ví dụ này gửi lệnh thật. Dùng tài khoản thử nghiệm và giá xa thị trường để lệnh nằm chờ, không khớp.

var account = "YOUR_ACCOUNT_NO";
var clientRequestId = $"ws-{Guid.NewGuid().ToString("N")[..8]}";   // tối đa 20 ký tự

try
{
    await using var ws = await TradingWs.ConnectAsync(auth);

    // Theo dõi kết quả thật trên luồng sự kiện lệnh
    ws.Client.OnEvent = _ => Console.WriteLine("đã nhận sự kiện lệnh");
    await ws.SubscribeOrderEventsAsync(account);

    var placed = await ws.PlaceOrderAsync(
        account, "SSI", OrderSide.Buy, 100, 20000m, OrderType.LO, clientRequestId);
    Console.WriteLine("order.place đã được xác nhận");

    // Frame xác nhận có thể không có orderId: huỷ theo clientRequestId
    var cancelled = await ws.CancelOrderAsync(account, clientRequestId: clientRequestId);
    Console.WriteLine("order.cancel đã được xác nhận");
}
catch (TradingWsError ex)
{
    Console.WriteLine($"Bị từ chối: status={ex.Status} code={ex.ErrorCode} {ex.ServerMessage}");
}
catch (ValidationException ex)
{
    Console.WriteLine($"Đầu vào không hợp lệ (chưa gửi gì): {ex.Message}");
}
catch (WebSocketException ex)
{
    // Timeout, socket đóng hoặc thiếu PrivateKey. Sau timeout, lệnh có thể đã hoặc chưa được tạo.
    Console.WriteLine($"Lỗi WebSocket: {ex.Message}");
}

TradingWsError kế thừa WebSocketException, nên phải catch nó trước.

Ví dụ 3: batch qua WebSocket.

var runId = Guid.NewGuid().ToString("N")[..8];
var items = new List<BatchPlaceOrderItem>
{
    new("YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100, 20000m, OrderType.LO, ClientRequestId: $"w-{runId}-1"),
    new("YOUR_ACCOUNT_NO", "SSI", OrderSide.Buy, 100, 19950m, OrderType.LO, ClientRequestId: $"w-{runId}-2"),
};

var response = await ws.BatchNewOrdersAsync(items);   // ws: TradingWsService lấy từ ConnectAsync

// Kết quả là JSON thô: chỉ in hình dạng, rồi đọc trạng thái cuối từ sự kiện lệnh
var kind = response.Result.ValueKind;
Console.WriteLine($"status={response.Status} method={response.Method} result={kind}");

7.6. Xử lý lỗi

Tình huốngExceptionGhi chú
Đầu vào không hợp lệ (accountNo rỗng, wildcard hoặc danh sách khi subscribe, batch sai)ValidationExceptionChưa gửi gì
SignatureEncoding không hợp lệValidationExceptionDo constructor của TradingWsClient ném
URL Trading WS không phải wss://WebSocketExceptionDo constructor của TradingWsClient ném
Server từ chối socket hoặc một lệnhTradingWsErrorStatus, ErrorCode, ServerMessage. Không bao giờ chứa params đã ký.
Chữ ký bị từ chối ở cả hai kiểu mã hoáTradingWsErrorKèm gợi ý; kiểm tra PrivateKey và định dạng khoá
Lệnh thay đổi trạng thái không có PrivateKeyWebSocketExceptionChưa gửi gì
Timeout, socket đóng, chưa kết nốiWebSocketExceptionKhông retry. Kết quả của lệnh thay đổi trạng thái là không xác định.
Lỗi token trước khi kết nốiAuthenticationExceptionTừ auth.EnsureAuthenticatedAsync

ErrorCode thường gặp là các mã chữ ký 401006 (thiếu chữ ký), 401008 (body chữ ký rỗng) và 401013 (chữ ký không khớp). Xem Mã lỗi cho danh sách đầy đủ và Xử lý lỗi cho các exception cơ sở.

Hướng dẫn từng bước: Tutorial 17: Trading WebSocket.

Trên trang này