API Services
Chi tiết các method API cho mỗi service trong Node.js SDK
1. Xác thực (Auth)
Truy cập trực tiếp trên auth.
Xác thực với OTP
const token = await auth.authenticate('YOUR_OTP');
console.log(`Token hết hạn lúc (epoch giây): ${token.expiresAt}`);authenticate(otp?, transactionId?): otp và transactionId loại trừ lẫn nhau, chỉ truyền một trong hai. Với Smart OTP dạng push-approval, truyền transactionId (lấy từ requestOtp) sau khi người dùng đã bấm approve trên thiết bị:
const token = await auth.authenticate(undefined, 'YOUR_TRANSACTION_ID');Yêu cầu gửi OTP
// Kiểu lỏng: Record<string, unknown>
await auth.requestOtp();
// Kiểu rõ ràng: OTPResponse { message, transactionId? }
const otp = await auth.requestOtpTyped();
if (otp.transactionId) {
// chỉ có với Smart OTP
await auth.ensureAuthenticated(undefined, otp.transactionId);
}transactionId chỉ có với tài khoản Smart OTP. Với Smart OTP bạn cũng có thể đọc mã trực tiếp trên app rồi truyền vào authenticate('YOUR_OTP'), không cần gọi requestOtp() trước.
Làm mới token
const token = await auth.refresh();Đảm bảo xác thực
await auth.ensureAuthenticated('YOUR_OTP');ensureAuthenticated(otp?, transactionId?, pollIntervalMs?, pollMaxRetries?): refresh nếu còn refresh token dùng được, ngược lại dùng OTP hoặc transactionId. Token chưa biết hạn được coi là đã hết hạn, và token được coi là hết hạn sớm 30 giây. Khi không refresh được phiên và không có OTP, SDK ném ReauthenticationRequired (xem Xử lý lỗi).
Trạng thái token
const token = auth.getToken();
auth.setToken(token);
auth.isTokenExpired; // boolean
auth.hasRefreshToken; // boolean
auth.canRefresh; // refresh token còn dùng được?auth.token, auth.accessToken, auth.isTokenExpired, auth.hasRefreshToken và auth.canRefresh là getter. setToken nhận Token hoặc TokenInit. Chi tiết Token xem Models.
2. Tài khoản (AccountService)
Truy cập qua trading.account.
Lấy danh sách tài khoản
const accounts = await trading.account.getAccountInfo();
for (const acc of accounts) {
console.log(`${acc.accountNo} (${acc.accountType ?? 'không rõ'})`);
}Trả về: Account[]. Mỗi Account có accountNo: string và accountType?: string. accountType là undefined khi server không gửi, và là chuỗi gốc của server nếu giá trị không thuộc AccountType.
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:
| Method | Khung 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ờ |
const ohlc = await data.marketData.getOhlc1Minute('SSI');
for (const candle of ohlc) {
console.log(`${candle.tradingDate}: O=${candle.openPrice} H=${candle.highPrice} L=${candle.lowPrice} C=${candle.closePrice} V=${candle.volume}`);
}Lịch sử (với khoảng ngày + phân trang):
| Method | Khung thời gian |
|---|---|
getOhlc1MinuteHistorical(symbol, from, to, page?, size?) | 1 phút |
getOhlc3MinuteHistorical(...) | 3 phút |
getOhlc5MinuteHistorical(...) | 5 phút |
getOhlc15MinuteHistorical(...) | 15 phút |
getOhlc1HourHistorical(...) | 1 giờ |
getOhlc1DayHistorical(...) | 1 ngày |
getOhlc1WeekHistorical và getOhlc1MonthHistorical đã deprecated và luôn ném ValidationError (server từ chối khung 1 tuần/1 tháng với mã 400210). Cần nến tuần/tháng thì tự gộp từ nến ngày ở phía client.
const ohlc = await data.marketData.getOhlc1DayHistorical(
'SSI', '2026/03/27', '2026/04/22', 1, 100,
);Tham số lịch sử:
| Tham số | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
symbol | string | (bắt buộc) | Mã chứng khoán |
from | string | (bắt buộc) | Ngày bắt đầu (yyyy/MM/dd) |
to | string | (bắt buộc) | Ngày kết thúc (yyyy/MM/dd) |
page | number | 1 | Số trang. Nhỏ hơn 1 bị từ chối tại máy |
size | number | 1000 | Số bản ghi mỗi trang. Nhỏ hơn 1 bị từ chối tại máy |
Nếu from/to chỉ có ngày (không có giờ), SDK bổ sung 00:00:00 cho from và 23:59:59 cho to.
Trả về: OHLCData[]
Lấy toàn bộ các trang:
| Method | Mô tả |
|---|---|
iterOhlc(symbol, timeframe, from?, to?, size?) | Async generator, duyệt từng nến qua mọi trang. Dừng ở trang rỗng hoặc trang ngắn. Nến đến theo thứ tự server (mới nhất trước) |
getOhlcAll(symbol, timeframe, from?, to?, size?, ascending = false) | Gom mọi nến vào một mảng OHLCData[]. ascending = true để đảo thành cũ nhất trước |
timeframe chỉ nhận các giá trị trong ALLOWED_TIMEFRAMES (1d, 1m, 3m, 5m, 15m, 1h). Không truyền from/to thì SDK dùng đầu ngày và cuối ngày hôm nay. Các method downloadOhlc* chưa được triển khai và ném lỗi.
import { Timeframe } from '@ssi.developer/ssi-sdk';
for await (const bar of data.marketData.iterOhlc('SSI', Timeframe.DAY_1, '2026/03/01', '2026/03/31')) {
console.log(bar.tradingDate, bar.closePrice);
}
const all = await data.marketData.getOhlcAll('SSI', Timeframe.DAY_1, '2026/03/01', '2026/03/31', 1000, true);3.2. Chỉ số thị trường
// Tất cả chỉ số
const indexes = await data.marketData.getIndexes();
// Theo sàn
import { Board } from '@ssi.developer/ssi-sdk';
const indexes = await data.marketData.getIndexesByBoard(Board.HOSE);
for (const idx of indexes) {
console.log(`${idx.index} - ${idx.indexName}`);
}REST chỉ nhận sàn HOSE, HNX, UPCOM. Board.DERIVATIVES chỉ dùng cho stream và master data, truyền vào đây sẽ bị từ chối.
Trả về: MarketIndexes[]
3.3. Tổng hợp chỉ số (Index Summary)
// Tổng hợp hiện tại
const summary = await data.marketData.getIndexSummary('VNINDEX');
// Tổng hợp lịch sử
const summary = await data.marketData.getIndexSummaryHistorical('VNINDEX', '2026/01/15');
// Tổng hợp theo sàn
const summary = await data.marketData.getBoardSummary(Board.HOSE);
// Tổng hợp theo sàn lịch sử
const summary = await data.marketData.getBoardSummaryHistorical(Board.HOSE, '2026/01/15');Trả về: MarketIndexSummary | undefined (undefined khi server không có dữ liệu).
3.4. Thông tin chứng khoán
// Một mã
const info = await data.marketData.getSecuritiesInfo('SSI');
// Theo chỉ số
const securities = await data.marketData.getSecuritiesInfoByIndex('VN30');
// Theo sàn
const securities = await data.marketData.getSecuritiesInfoByBoard(Board.HOSE);Trả về: getSecuritiesInfo trả SecuritiesInfo | undefined. Các biến thể ByIndex/ByBoard trả SecuritiesInfo[].
3.5. Tổng hợp chứng khoán (Securities Summary)
// Tổng hợp hiện tại
const summary = await data.marketData.getSecuritiesSummary('SSI');
// Tổng hợp lịch sử
const summary = await data.marketData.getSecuritiesSummaryHistorical(
'SSI', '2026/03/01', '2026/03/31',
);
// Theo chỉ số
const summary = await data.marketData.getSecuritiesSummaryByIndex('VN30');
// Theo chỉ số lịch sử
const summary = await data.marketData.getSecuritiesSummaryByIndexHistorical(
'VN30', '2026/03/01', '2026/03/31',
);Trả về: SecuritiesSummary[]. SDK lấy hết mọi trang.
3.6. Master data (giá trần, sàn, tham chiếu)
// Hôm nay
const master = await data.marketData.getMasterData();
// Theo khoảng ngày
const history = await data.marketData.getMasterDataHistorical('2026/03/01', '2026/03/31');Trả về: MasterData[] (mỗi phần tử có symbol, tradingDate, ceiling, floor, refPrice và board tuỳ chọn). SDK lấy hết mọi trang.
3.7. Corporate Actions (sự kiện quyền)
Hai method, tách theo thời điểm (GET /api/v3/data/corporateActions):
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
getCorporateActions(symbol?, language?) | symbol?: string, language = 'vn' | Promise<CorporateAction[]> | Sự kiện quyền của hôm nay |
getCorporateActionsHistorical(from, to, symbol?, language?) | from: string, to: string, symbol?: string, language = 'vn' | Promise<CorporateAction[]> | Sự kiện quyền trong một khoảng ngày trong quá khứ |
// Hôm nay (mọi mã, hoặc một mã)
const today = await data.marketData.getCorporateActions();
const one = await data.marketData.getCorporateActions('SSI', 'en');
// Quá khứ: khoảng ngày tối đa 5 ngày
const past = await data.marketData.getCorporateActionsHistorical(
'2026/10/08',
'2026/10/12',
undefined, // undefined = tất cả mã
'vn', // 'vn' (mặc định) hoặc 'en'
);
for (const e of past) {
console.log(e.symbol, e.eventTitle, e.exrightDate, e.recordDate, e.issueDate, e.exchange, e.ratio);
}Trả về: CorporateAction[] (mỗi phần tử có symbol, eventTitle, exrightDate, recordDate, issueDate, exchange, ratio). Tất cả là string đúng như server trả, xem Models. SDK lấy hết mọi trang.
SDK kiểm tra ở phía client trước khi gọi mạng, sai thì ném ValidationError:
from,tođúng định dạngYYYY/MM/DD,fromkhông sauto, và khoảng cách giữa hai ngày không quá 5 ngày.getCorporateActions()dùng ngày hiện tại của máy bạn cho cả hai đầu.symbol(nếu truyền) là một mã chữ-số, tối đa 20 ký tự. Bỏ trống thì lấy tất cả mã.languagelà'vn'hoặc'en'(không phân biệt hoa thường).
Chưa chạy trên server thật
Hai method này mới được kiểm thử bằng unit test với HTTP adapter giả lập, chưa chạy trên server thật. Hãy tự kiểm tra kết quả trước khi dùng cho nghiệp vụ.
4. Danh mục (PortfolioService)
Truy cập qua trading.portfolio.
4.1. Số dư tài khoản
// Số dư cổ phiếu
const balance = await trading.portfolio.getEquityBalance('YOUR_ACCOUNT_NO');
// Số dư phái sinh
const derBalance = await trading.portfolio.getDerivativeBalance('YOUR_DERIVATIVE_ACCOUNT_NO');Cả hai trả về undefined khi server không có dữ liệu.
4.2. Vị thế
// Vị thế cổ phiếu
const positions = await trading.portfolio.getEquityPositions('YOUR_ACCOUNT_NO');
for (const pos of positions ?? []) {
console.log(`${pos.symbol}: ${pos.quantity} cổ | Giá vốn: ${pos.costPrice}`);
}
// Tất cả vị thế phái sinh
const derPositions = await trading.portfolio.getDerivativePositions('YOUR_DERIVATIVE_ACCOUNT_NO');
// Chỉ vị thế đang mở
const openPos = await trading.portfolio.getOpenDerivativePositions('YOUR_DERIVATIVE_ACCOUNT_NO');
// Chỉ vị thế đã đóng
const closedPos = await trading.portfolio.getClosedDerivativePositions('YOUR_DERIVATIVE_ACCOUNT_NO');getEquityPositions và getDerivativePositions trả về undefined khi không có dữ liệu. getOpenDerivativePositions và getClosedDerivativePositions trả về mảng rỗng [] khi server không gửi danh sách.
4.3. Sổ lệnh
// Lệnh trong ngày
const orders = await trading.portfolio.getTodayOrders('YOUR_ACCOUNT_NO');
for (const order of orders) {
console.log(`${order.orderId}: ${order.symbol} ${order.side} ${order.quantity}@${order.price} - ${order.status}`);
}
// Lệnh lịch sử
const orders = await trading.portfolio.getHistoricalOrders('YOUR_ACCOUNT_NO', '2026/01/01', '2026/01/31');4.4. PPMMR
// PPMMR cổ phiếu
const ppmmr = await trading.portfolio.getEquityPpmmr('YOUR_ACCOUNT_NO');
// PPMMR phái sinh
const derPPMMR = await trading.portfolio.getDerivativePpmmr('YOUR_DERIVATIVE_ACCOUNT_NO');Tổng quan method Portfolio
| Nhóm | Method | Trả về |
|---|---|---|
| Số dư | getEquityBalance(accountNo) | EquityAccountBalance | undefined |
getDerivativeBalance(accountNo) | DerivativeAccountBalance | undefined | |
| Vị thế | getEquityPositions(accountNo) | EquityPosition[] | undefined |
getDerivativePositions(accountNo) | AllDerivativePosition | undefined | |
getOpenDerivativePositions(accountNo) | DerivativePosition[] ([] nếu không có) | |
getClosedDerivativePositions(accountNo) | DerivativePosition[] ([] nếu không có) | |
| Sổ lệnh | getTodayOrders(accountNo) | Order[] |
getHistoricalOrders(accountNo, from, to) | Order[] | |
| PPMMR | getEquityPpmmr(accountNo) | EquityPPMMR | undefined |
getDerivativePpmmr(accountNo) | DerivativePPMMR | undefined |
5. Giao dịch (TradingService)
Truy cập qua trading.trading.
Giá (price và các tham số giá) nhận PriceLike (number hoặc string). Nên truyền chuỗi thập phân thường, không dùng dạng mũ hoặc dấu phân cách hàng nghìn. Mọi method đặt lệnh có tham số cuối clientRequestId? tuỳ chọn (tối đa 20 ký tự), là khóa chống trùng của bạn.
5.1. Đặt lệnh
import { OrderSide, OrderType } from '@ssi.developer/ssi-sdk';
// Lệnh giới hạn (LO)
const result = await trading.trading.placeLimitOrder('YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100, 66000);
// Lệnh thị trường (MTL)
const result = await trading.trading.placeMarketOrder('YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100);
// Lệnh ATO (mở cửa)
const result = await trading.trading.placeAtoOrder('YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100);
// Lệnh ATC (đóng cửa)
const result = await trading.trading.placeAtcOrder('YOUR_ACCOUNT_NO', 'SSI', OrderSide.SELL, 100);
// Loại lệnh tuỳ chỉnh
const result = await trading.trading.placeOrder(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100, 66000, OrderType.LO,
);
console.log(`Order ID: ${result.orderId}, Status: ${result.status}`);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).
// Sửa giá theo clientRequestId
const result = await trading.trading.modifyOrderPrice('YOUR_ACCOUNT_NO', 'YOUR_CLIENT_REQUEST_ID', 68000);
// Sửa giá theo orderId
const result = await trading.trading.modifyOrderPriceByOrderId('YOUR_ACCOUNT_NO', 'YOUR_ORDER_ID', 68000);
// Sửa khối lượng theo clientRequestId
const result = await trading.trading.modifyOrderQuantity('YOUR_ACCOUNT_NO', 'YOUR_CLIENT_REQUEST_ID', 200);
// Sửa khối lượng theo orderId
const result = await trading.trading.modifyOrderQuantityByOrderId('YOUR_ACCOUNT_NO', 'YOUR_ORDER_ID', 200);modifyOrderPriceById và modifyOrderQuantityById là alias cũ đã deprecated của modifyOrderPriceByOrderId và modifyOrderQuantityByOrderId.
5.3. Huỷ lệnh
// Huỷ theo clientRequestId
const result = await trading.trading.cancelOrder('YOUR_ACCOUNT_NO', 'YOUR_CLIENT_REQUEST_ID');
// Huỷ theo orderId
const result = await trading.trading.cancelOrderById('YOUR_ACCOUNT_NO', 'YOUR_ORDER_ID');5.4. Sức mua/bán tối đa
// Với giá cụ thể
const maxBS = await trading.trading.getMaxBuySell('YOUR_ACCOUNT_NO', 'SSI', 66000);
console.log(`Max mua: ${maxBS.maxBuyQuantity}, Max bán: ${maxBS.maxSellQuantity}`);
// Theo giá thị trường
const maxBS = await trading.trading.getMaxBuySellAtMarketPrice('YOUR_ACCOUNT_NO', 'SSI');Tài khoản phái sinh chỉ nhận hai số lượng maxBuyQuantity và maxSellQuantity. Xem Models cho MaxBuySellResponse.
5.5. 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 (GTD, Stop, Stop Limit, Trailing Stop, Trailing Stop Limit, OCO, Bull Bear), 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.
Mọi method placeFco* và cancelFco nhận thêm tham số cuối code? (OTP, tuỳ chọn), chỉ được gửi khi có giá trị. Ngày FCO có dạng YYYY/MM/DD HH:MM:SS, fromDate <= toDate và cửa sổ tối đa 31 ngày.
import { FCOOperator, OrderSide, OrderType, fromBeginningOfDay, fromEndOfDay } from '@ssi.developer/ssi-sdk';
const fromDate = fromBeginningOfDay(); // "YYYY/MM/DD 00:00:00"
const toDate = fromEndOfDay(); // "YYYY/MM/DD 23:59:59"
// Lệnh GTD (Good Till Date)
const gtd = await trading.trading.placeFcoGtd(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100, 25000, 500, fromDate, toDate,
);
console.log(`FCO ID: ${gtd.fcoId}`);
// Lệnh Stop Market
const stop = await trading.trading.placeFcoStop(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.SELL, 100, 24000, FCOOperator.LESSER_OR_EQUAL, fromDate, toDate,
);
// Lệnh Stop Limit
const stopLimit = await trading.trading.placeFcoStopLimit(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100, 25500, 500, 25000, FCOOperator.GREATER_OR_EQUAL, fromDate, toDate,
);
// Lệnh Trailing Stop
const trailing = await trading.trading.placeFcoTrailingStop(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.BUY, 100, 26000, 1000, fromDate, toDate,
);
// Lệnh OCO (One-Cancels-the-Other)
const oco = await trading.trading.placeFcoOco(
'YOUR_ACCOUNT_NO', 'SSI', OrderSide.SELL, 100, 30000, 24000, OrderType.MTL, OrderType.MTL, 500, 500, fromDate, toDate,
);
// Tra cứu danh sách lệnh FCO theo tài khoản
const fcoList = await trading.trading.getFcoByAccountNo('YOUR_ACCOUNT_NO', 1, 10);
console.log(`FCO items: ${fcoList.fcoList.length}`);
// Lịch sử trạng thái của một lệnh FCO (mảng rỗng nếu chưa có)
const history = await trading.trading.getFcoStatusHistory(gtd.fcoId);
// Huỷ lệnh FCO
await trading.trading.cancelFco(gtd.fcoId);| Method | Mô tả |
|---|---|
placeFcoGtd(accountNo, symbol, side, quantity, price, priceSlip, fromDate, toDate, code?) | Đặt lệnh GTD (Good Till Date) |
placeFcoStop(accountNo, symbol, side, quantity, stopPrice, operator, fromDate, toDate, code?) | Đặt lệnh Stop Market |
placeFcoStopLimit(accountNo, symbol, side, quantity, price, priceSlip, stopPrice, operator, fromDate, toDate, code?) | Đặt lệnh Stop Limit |
placeFcoTrailingStop(accountNo, symbol, side, quantity, activePrice, trailingAmount, fromDate, toDate, code?) | Đặt lệnh Trailing Stop Market |
placeFcoTrailingStopLimit(accountNo, symbol, side, quantity, activePrice, trailingAmount, priceSlip, fromDate, toDate, code?) | Đặt lệnh Trailing Stop Limit |
placeFcoOco(accountNo, symbol, side, quantity, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, code?) | Đặt lệnh OCO (One Cancels the Other) |
placeFcoBullBear(accountNo, symbol, side, quantity, price, priceSlip, tpActivePrice, slActivePrice, tpPrice, slPrice, tpSlip, slSlip, fromDate, toDate, code?) | Đặt lệnh Bull Bear |
cancelFco(fcoId, code?) | Hủy lệnh FCO theo fcoId |
getFcoByAccountNo(accountNo, pageIndex?, pageSize?) | Tra cứu danh sách FCO theo tài khoản |
getFcoBySymbol(accountNo, symbol, pageIndex?, pageSize?) | Tra cứu FCO lọc theo mã chứng khoán |
getFcoByStatus(accountNo, processStatus, pageIndex?, pageSize?) | Tra cứu FCO lọc theo trạng thái. processStatus nhận một giá trị hoặc một mảng |
getFcoByDate(accountNo, fromDate, toDate, pageIndex?, pageSize?) | Tra cứu FCO lọc theo khoảng ngày |
getFcoById(accountNo, fcoId) | Lấy thông tin 1 lệnh FCO theo ID (undefined nếu không có) |
getFcoOrderBook(fcoId, pageIndex?, pageSize?) | Lấy lịch sử thực thi (Order Book) của FCO. Mặc định pageIndex 1, pageSize 10 |
getFcoStatusHistory(fcoId) | Lấy lịch sử trạng thái của FCO (FCOStatusHistoryItem[], rỗng nếu chưa có) |
5.6. Batch Orders
Đặt hoặc hủy nhiều lệnh trong một request REST đã ký, thay vì gọi placeOrder từng lệnh (ví dụ khi rebalance danh mục). Đây là nơi mô tả đầy đủ về batch. Các kiểu dữ liệu xem Models, giới hạn số lệnh xem Cấu hình, lỗi xem Xử lý lỗi. Batch qua WebSocket ở mục 7.
Điều kiện
- Dùng client
Trading. Method nằm ởtrading.trading(TradingService). - Cần token có quyền giao dịch, tức là đã xác thực bằng OTP (
auth.authenticate(otp)hoặcauth.ensureAuthenticated(otp)). - Bắt buộc có
Config.privateKey: SDK ký RSA lên toàn bộ body và gửi chữ ký trong headerX-Signature. - Giới hạn số lệnh mỗi lô:
Config.maxBatchOrders(mặc định 20).
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
placeBatchOrders(orders) | orders: readonly BatchPlaceOrderItem[] | Promise<BatchOrderResponse> | Đặt một lô lệnh (POST /api/v3/trading/order/batch) |
cancelBatchOrders(orders) | orders: readonly BatchCancelOrderItem[] | Promise<BatchOrderResponse> | Hủy một lô lệnh (DELETE /api/v3/trading/order/batch) |
Cả hai method đều:
- kiểm tra toàn bộ lô ở phía client trước khi gửi. Một lệnh sai thì SDK ném
ValidationErrorvà không gửi request nào; - tự sinh
batchRequestId(ngẫu nhiên) vàbatchRequestTime(thời điểm hiện tại, epoch ms). Bạn không truyền hai giá trị này; - ký một lần trên đúng chuỗi JSON được gửi đi;
- không bao giờ retry (xem "Idempotency và retry" bên dưới).
Lô rỗng hoặc lớn hơn maxBatchOrders bị từ chối ngay tại máy bằng ValidationError. SDK chỉ kiểm tra giới hạn ở phía client. Mặc định của SDK là 20, trong khi tài liệu API reference nêu tối đa 50 lệnh mỗi lô: xem ghi chú ở Cấu hình.
Ví dụ 1: đặt lô lệnh và đọc kết quả từng lệnh
import {
Auth, Config, Trading, OrderSide, OrderType,
DuplicateRequestError, ValidationError,
} from '@ssi.developer/ssi-sdk';
const config = new Config({
clientId: 'YOUR_CLIENT_ID',
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
privateKey: 'YOUR_PRIVATE_KEY',
});
const auth = new Auth(config);
await auth.authenticate('YOUR_OTP'); // OTP của bạn
const trading = new Trading(auth);
const accountNo = 'YOUR_ACCOUNT_NO';
const tag = Date.now().toString(36); // clientRequestId tối đa 20 ký tự
const items = [
{ accountNo, symbol: 'SSI', side: OrderSide.BUY, quantity: 100, price: '25.5', orderType: OrderType.LO, clientRequestId: `b${tag}-1` },
{ accountNo, symbol: 'VNM', side: OrderSide.BUY, quantity: 100, price: '60', orderType: OrderType.LO, clientRequestId: `b${tag}-2` },
];
try {
const response = await trading.trading.placeBatchOrders(items);
console.log(`Batch: ${response.batchRequestId}`);
for (const r of response.results) {
if (r.success) {
console.log(`OK ${r.clientRequestId} -> orderId ${r.orderId}, status ${r.status}`);
} else {
console.log(`LOI ${r.clientRequestId}: ${r.errorCode} ${r.errorMessage}`);
}
}
} catch (error) {
if (error instanceof ValidationError) {
console.error(`Lô không hợp lệ, chưa có request nào được gửi: ${error.message}`);
} else if (error instanceof DuplicateRequestError) {
// Id đã dùng trong ngày: lệnh gốc có thể vẫn đang sống. Kiểm tra sổ lệnh, đừng gửi lại bằng id khác.
console.error(`Trùng id (HTTP 409): ${error.message}`);
} else {
throw error;
}
}Ví dụ 2: hủy lô lệnh theo orderId
Lấy orderId từ các lệnh đã được nhận ở ví dụ trên rồi hủy cả lô.
const accepted = response.results.filter((r) => r.success && r.orderId);
if (accepted.length > 0) {
const cancelled = await trading.trading.cancelBatchOrders(
accepted.map((r) => ({ accountNo, orderId: r.orderId })),
);
for (const r of cancelled.results) {
console.log(`${r.success ? 'OK ' : 'LOI'} orderId ${r.orderId} | ${r.status} ${r.errorCode} ${r.errorMessage}`);
}
}Có thể hủy theo clientRequestId thay vì orderId: { accountNo, clientRequestId: 'YOUR_CLIENT_REQUEST_ID' }. Mỗi lệnh hủy cần accountNo và đúng một trong hai id.
Chữ ký (signing)
SDK serialize body thành chuỗi JSON đúng một lần, ký RSA trên chuỗi đó rồi gửi chính chuỗi đó. Chữ ký bao phủ toàn bộ body gồm batchRequestId, batchRequestTime và mảng orders, được gửi trong header X-Signature. Vì vậy:
- Không tự sửa body sau khi ký (nếu bạn dùng REST client ở mức thấp).
- Thiếu
Config.privateKeythì không ký được. - Không log chữ ký, private key hoặc token.
Idempotency và retry
- Mỗi lệnh có
clientRequestId(tối đa 20 ký tự) làm khóa chống trùng. Nên tự đặt để còn đối chiếu được với sổ lệnh và sự kiện lệnh. - Mỗi lô có
batchRequestIdngẫu nhiên. Id đã dùng trong ngày bị server trả HTTP 409, SDK némDuplicateRequestError. - Lô không bao giờ được retry tự động, kể cả khi timeout hay lỗi mạng. Request có thể đã được server nhận.
- Khi nhận
DuplicateRequestErrorhoặc mất phản hồi, kiểm tra trạng thái lệnh trước (ví dụtrading.portfolio.getTodayOrders(accountNo), hoặc sự kiện lệnh qua Stream). Đừng gửi lại cùng lệnh bằng id mới. batchRequestTimenằm ngoài cửa sổ thời gian server chấp nhận (replay window) thì server từ chối với mã400111. Kiểm tra đồng hồ máy nếu gặp lỗi này liên tục. Tài liệu API reference nêu cửa sổ này là 30 giây.
Xử lý kết quả từng lệnh
- Theo mô tả trong SDK, server chấp nhận hoặc từ chối cả lô, và SDK cũng kiểm tra từng lệnh trước khi gửi. Hành vi thật của response khi một phần lệnh lỗi chưa được xác nhận từ mã nguồn.
- Dù vậy response luôn có một
BatchOrderResultcho mỗi lệnh. Hãy duyệtresultsvà kiểm trasuccess,errorCode,errorMessagecủa từng lệnh thay vì coi cả lô là thành công. resultsđúng thứ tự với mảng bạn gửi. Có thể ghép theoclientRequestId.- Để theo dõi trạng thái và khớp lệnh sau đó, dùng sự kiện lệnh trên Stream (
subscribeOrderStatus) hoặc sổ lệnh.
Xem thêm Tutorial 14: Đặt/hủy lệnh hàng loạt.
Tổng quan method Trading
| Nhóm | Method | Trả về |
|---|---|---|
| Đặt lệnh | placeOrder(accountNo, symbol, side, qty, price, orderType, clientRequestId?) | PlaceOrderResponse |
placeLimitOrder(accountNo, symbol, side, qty, price, clientRequestId?) | PlaceOrderResponse | |
placeMarketOrder(accountNo, symbol, side, qty, clientRequestId?) | PlaceOrderResponse | |
placeAtoOrder(accountNo, symbol, side, qty, clientRequestId?) | PlaceOrderResponse | |
placeAtcOrder(accountNo, symbol, side, qty, clientRequestId?) | PlaceOrderResponse | |
| Sửa lệnh | modifyOrderPrice(accountNo, clientRequestId, price) | ModifyOrderResponse |
modifyOrderPriceByOrderId(accountNo, orderId, price) | ModifyOrderResponse | |
modifyOrderQuantity(accountNo, clientRequestId, qty) | ModifyOrderResponse | |
modifyOrderQuantityByOrderId(accountNo, orderId, qty) | ModifyOrderResponse | |
| Huỷ lệnh | cancelOrder(accountNo, clientRequestId) | CancelOrderResponse |
cancelOrderById(accountNo, orderId) | CancelOrderResponse | |
| Batch (xem 5.6) | placeBatchOrders(orders) | BatchOrderResponse |
cancelBatchOrders(orders) | BatchOrderResponse | |
| Sức mua/bán | getMaxBuySell(accountNo, symbol, price) | MaxBuySellResponse |
getMaxBuySellAtMarketPrice(accountNo, symbol) | MaxBuySellResponse | |
| Lệnh điều kiện | placeFcoGtd(...) | FCOPlaceResponse |
placeFcoStop(...) / placeFcoStopLimit(...) | FCOPlaceResponse | |
placeFcoTrailingStop(...) / placeFcoTrailingStopLimit(...) | FCOPlaceResponse | |
placeFcoOco(...) / placeFcoBullBear(...) | FCOPlaceResponse | |
cancelFco(fcoId, code?) | FCOCancelResponse | |
getFcoByAccountNo(...) / getFcoBySymbol(...) / getFcoByStatus(...) / getFcoByDate(...) | FCOListResponse | |
getFcoById(accountNo, fcoId) | FCOInfo | undefined | |
getFcoOrderBook(fcoId, pageIndex?, pageSize?) | FCOOrderBookResponse | |
getFcoStatusHistory(fcoId) | FCOStatusHistoryItem[] |
Đặt lệnh qua WebSocket: xem mục 7.
6. Streaming Realtime (StreamingService)
Truy cập qua stream.streaming. Gọi await stream.streaming.connect() trước. Mọi method subscribe/unsubscribe trả về Promise<void>: luôn dùng await.
6.1. Kết nối
const stream = new Stream(auth);
await stream.streaming.connect();
// Chờ vô thời hạn
await stream.streaming.wait();
// Hoặc chờ tối đa 60 giây
await stream.streaming.wait(60_000);wait(timeoutMs?) chờ tới khi kết nối kết thúc hẳn hoặc hết thời gian. Thuộc tính liên quan: isConnected, lastError, reconnectCount (xem 6.8).
6.2. Thiết lập Callbacks
stream.streaming.onData = (msg) => {
console.log('[DATA]', msg);
};
stream.streaming.onTrading = (msg) => {
console.log('[TRADING]', msg);
};
stream.streaming.onHeartbeat = (msg) => {
console.log('[HEARTBEAT]', msg);
};Ngoài ba callback trên còn có các callback có kiểu: onMarketFlag (cần subscribeMarketFlag), onIndexSummary, onOrder, onOrderMatch, onPortfolio và onConnection (nhận ConnectionEvent). Message vẫn đến onData/onTrading nếu bạn đặt cả hai. Đặt callback không tự subscribe gì cả, nó chỉ chọn nơi nhận message. Callback onResponse truyền vào method subscribe chỉ nhận frame xác nhận (ack) của server. Callback ném lỗi thì SDK ghi log và vẫn gọi các callback còn lại.
6.3. Subscribe dữ liệu thị trường
import { Board, Timeframe } from '@ssi.developer/ssi-sdk';
// Tất cả kênh (trade + quote + room) cho mã
await stream.streaming.subscribeSymbol(['SSI', 'HPG', 'VIC']);
// Kênh riêng lẻ
await stream.streaming.subscribeSymbolTrade(['SSI', 'HPG']);
await stream.streaming.subscribeSymbolQuote(['SSI', 'HPG']);
await stream.streaming.subscribeSymbolRoom(['SSI', 'HPG']);
await stream.streaming.subscribeSymbolPutThrough(['SSI']);
await stream.streaming.subscribeSymbolOddLot(['SSI']);
// OHLCV theo timeframe (chỉ tick, 1m, 5m)
await stream.streaming.subscribeSymbolOhlcv(['SSI'], Timeframe.MINUTE_1);
// Theo sàn
await stream.streaming.subscribeBoard([Board.HOSE, Board.HNX]);
// Theo chỉ số (cổ phiếu thành phần)
await stream.streaming.subscribeIndex(['VNINDEX', 'VN30']);
// Cờ phiên thị trường và master data
await stream.streaming.subscribeMarketFlag();
await stream.streaming.subscribeMarket(['SSI']);subscribeIndex và subscribeBoard được gửi với hậu tố @* để server tách theo từng mã (ví dụ trade.VN30@*). Hậu tố @* này dựa trên việc đọc mã server, chưa được xác nhận trên server thật. Hai method này tạo lưu lượng lớn. Mỗi phần tử của danh sách là một mã: chuỗi có dấu phẩy bị từ chối bằng ValidationError. Dữ liệu của chính chỉ số (tick, nến, tổng hợp) xem 6.7.
Ngoài ra có các tên ngắn tương đương (alias) kèm cặp unsubscribe: subscribeTrades, subscribeQuotes, subscribeForeignRoom, subscribePutThrough, subscribeOddLot, subscribeCandles, subscribeExchangeTrades, subscribeIndexConstituents, subscribeSessionFlag, subscribeMasterData.
6.4. Unsubscribe
await stream.streaming.unsubscribeSymbol(['SSI', 'HPG']);
await stream.streaming.unsubscribeSymbolTrade(['SSI']);
await stream.streaming.unsubscribeSymbolQuote(['SSI']);
await stream.streaming.unsubscribeSymbolRoom(['SSI']);
await stream.streaming.unsubscribeSymbolPutThrough(['SSI']);
await stream.streaming.unsubscribeSymbolOddLot(['SSI']);
await stream.streaming.unsubscribeSymbolOhlcv(['SSI'], Timeframe.MINUTE_1);
await stream.streaming.unsubscribeBoard([Board.HOSE]);
await stream.streaming.unsubscribeIndex(['VNINDEX']);6.5. Subscribe sự kiện giao dịch
// Trạng thái lệnh (thường + FCO) — tất cả tài khoản
await stream.streaming.subscribeOrderStatus();
// Cho tài khoản cụ thể
await stream.streaming.subscribeOrderStatus('YOUR_ACCOUNT_NO');
// Danh mục — tất cả tài khoản
await stream.streaming.subscribePortfolio();
// Cho tài khoản cụ thể
await stream.streaming.subscribePortfolio('YOUR_ACCOUNT_NO');
// Hủy
await stream.streaming.unsubscribeOrderStatus('YOUR_ACCOUNT_NO');
await stream.streaming.unsubscribePortfolio('YOUR_ACCOUNT_NO');Kênh order.<account> trả về cả OrderStatusMessage (lệnh thường) lẫn FCOOrderUpdateMessage (lệnh điều kiện FCO). Trong onTrading, SDK phân loại dựa vào data.eventType: 'fcoEvent' là lệnh FCO, 'orderEvent' là lệnh thường, 'orderMatchEvent' là khớp lệnh, 'clientPortfolioEvent' là danh mục. Với frame không có eventType, tiền tố topic order. hoặc portfolio. quyết định. Nếu dùng callback có kiểu onOrder, onOrderMatch, onPortfolio thì không cần tự phân loại. subscribeFcoOrderStatus đã deprecated: cập nhật FCO đến trên chính kênh order.<account>, chỉ cần subscribeOrderStatus. Xem Models cho các message.
6.6. Heartbeat
await stream.streaming.ping();
// Hoặc bật vòng ping nền mỗi 30 giây
await stream.streaming.ping(undefined, 30_000);SDK không tự ping. Vòng ping nền dừng khi gọi disconnect().
6.7. Real-time Index Data
Nhận dữ liệu của chính chỉ số (VNINDEX, VN30, HNXINDEX...) theo thời gian thực: tick, nến 1 phút / 5 phút và bản tổng hợp chỉ số. Dùng khi bạn cần vẽ biểu đồ chỉ số, hiển thị độ rộng thị trường hoặc theo dõi chỉ số mà không phải tự tổng hợp từ giá từng mã. Nếu cần dữ liệu cổ phiếu thành phần của chỉ số thì dùng subscribeIndex (xem bảng phân biệt bên dưới). Bảng field của IndexTickMessage, IntervalMessage, IndexSummaryMessage nằm ở Models.
Điều kiện
- Dùng client
StreamvớiAuthđã xác thực (như ở 6.1). Method nằm ởstream.streaming(StreamingService). - Gọi
await stream.streaming.connect()trước khi subscribe.
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
subscribeIndexTrade(indices, interval?, onResponse?) | indices: string[], interval?: Timeframe | StreamInterval | null, onResponse? | Promise<void> | Tick của chỉ số (trade.index.<code>). Có interval 1m/5m thì nhận nến |
unsubscribeIndexTrade(indices, interval?) | indices: string[], interval? | Promise<void> | Hủy. Truyền cùng interval đã subscribe |
subscribeIndexSummary(indices, onResponse?) | indices: string[], onResponse? | Promise<void> | Tổng hợp chỉ số (indexsummary.<code>) |
unsubscribeIndexSummary(indices) | indices: string[] | Promise<void> | Hủy |
listSubscription(timeoutMs = 5000) | timeoutMs?: number | Promise<{ trading: string[]; data: string[] }> | Hỏi server connection này đang subscribe topic nào |
unsubscribeAll() | không | Promise<{ trading: string[]; data: string[] }> | Hủy mọi topic server báo cho connection này, trả về các topic đã gỡ |
Tên ngắn tương đương: subscribeIndexTicks = subscribeIndexTrade, unsubscribeIndexTicks = unsubscribeIndexTrade.
Quy tắc mã chỉ số và interval
- Mỗi phần tử của
indiceslà một mã (VNINDEX,VN30...). Mã được viết hoa tự động. - Truyền mảng
['VNINDEX', 'VN30'], không truyền chuỗi có dấu phẩy ('VNINDEX,VN30'bị từ chối). - Mã viết liền, ví dụ
HNXINDEX(không phảiHNX-INDEX).UPCOMINDEXđược SDK đổi thànhHNXUPCOMINDEXkhi gửi. intervalchỉ nhậntick,1mhoặc5m(STREAM_INTERVALS). Bỏ quaintervalhoặc dùngtickđể nhận tick. Giá trị khác bịValidationErrorngay tại máy.Timeframe.MINUTE_1('1m') vàTimeframe.MINUTE_5('5m') dùng được. CácTimeframekhác bị từ chối.- Với
trade.index, interval chỉ đi kèm một mã cụ thể, không dùng*hoặc khoảngA-B. - Mỗi tổ hợp mã và interval là một topic riêng (
trade.index.VN30vàtrade.index.VN30@1mđộc lập nhau).
Phân biệt subscribeIndex và subscribeIndexTrade
| Method | Nhận gì |
|---|---|
subscribeIndexTrade(['VN30']) | Tick của chính chỉ số VN30 (IndexTickMessage) |
subscribeIndex(['VN30']) | Trade, quote, room của các cổ phiếu thành phần VN30 (gửi dạng trade.VN30@*...) |
Handler
Tick chỉ số, nến và tổng hợp chỉ số đều đi qua onData (DataMessage). Riêng tổng hợp chỉ số có thể nhận bằng onIndexSummary (IndexSummaryMessage). Không có handler riêng cho tick chỉ số: dùng onData và nhận diện kiểu (xem ví dụ).
Ví dụ 1: tick và tổng hợp chỉ số, kèm sự kiện kết nối
import { Auth, Config, Stream } from '@ssi.developer/ssi-sdk';
import type { DataMessage, IndexTickMessage } from '@ssi.developer/ssi-sdk';
const config = new Config({
clientId: 'YOUR_CLIENT_ID',
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
});
const auth = new Auth(config);
await auth.authenticate('YOUR_OTP'); // OTP của bạn
const stream = new Stream(auth);
const s = stream.streaming;
// Chỉ IndexTickMessage có field `raw`
const isIndexTick = (msg: DataMessage): msg is IndexTickMessage => 'raw' in msg;
s.onConnection = (event) => {
console.log(`[CONNECTION] ${event.state} | reconnectCount: ${event.reconnectCount}` +
(event.error ? ` | lỗi: ${event.error.message}` : ''));
};
s.onData = (msg) => {
if (isIndexTick(msg)) {
console.log(`[INDEX] ${msg.symbol} ${msg.value} (${msg.change}, ${msg.changePercent}%)`);
}
};
s.onIndexSummary = (msg) => {
console.log(`[SUMMARY] ${msg.index} | ${Object.keys(msg.data).length} field`);
};
await s.connect();
await s.subscribeIndexTrade(['VNINDEX', 'VN30']);
await s.subscribeIndexSummary(['VNINDEX']);
await s.ping(undefined, 30_000); // SDK không tự ping, bật vòng ping nền
await s.wait(60_000); // nhận dữ liệu trong 60 giây
await s.unsubscribeIndexTrade(['VNINDEX', 'VN30']);
await s.unsubscribeIndexSummary(['VNINDEX']);
await stream.disconnect();Ví dụ 2: nến 1 phút của chỉ số và kiểm tra subscription
import { Timeframe } from '@ssi.developer/ssi-sdk';
import type { IntervalMessage } from '@ssi.developer/ssi-sdk';
const INDICES = ['VN30'];
s.onData = (msg) => {
// Nến có field intervalTime. Lọc theo symbol để chỉ lấy nến chỉ số
if ('intervalTime' in msg && INDICES.includes((msg as IntervalMessage).symbol)) {
const c = msg as IntervalMessage;
console.log(`[CANDLE] ${c.symbol} ${c.intervalTime} O${c.open} H${c.high} L${c.low} C${c.close} V${c.volume}`);
}
};
await s.subscribeIndexTrade(INDICES, Timeframe.MINUTE_1);
const subs = await s.listSubscription();
console.log(`data topics: ${subs.data.join(', ')}`);
// ... sau khi dùng xong ...
await s.unsubscribeIndexTrade(INDICES, Timeframe.MINUTE_1); // cùng interval đã subscribeLỗi và lưu ý
| Tình huống | Kết quả |
|---|---|
interval không phải tick/1m/5m, mã sai định dạng (có dấu phẩy, ký tự lạ), hoặc indices rỗng | ValidationError tại máy, chưa gửi gì |
Gọi subscribe khi chưa connect() | WebSocketError (Not connected. Call connect() first.). Gọi connect() trước |
listSubscription không có phản hồi trong timeoutMs, hoặc chưa kết nối | WebSocketError |
| Kết nối không phục hồi được | onConnection nhận failed, lastError có giá trị. connect() lần đầu thất bại thì ném WebSocketError hoặc AuthenticationError |
| Callback ném lỗi | SDK ghi log và vẫn gọi các callback còn lại |
- Hành vi
@*cho board/index và dữ liệuindexsummaryphụ thuộc server và chưa được xác nhận trên server thật. Nếu không nhận được dữ liệu, kiểm tra bằnglistSubscription()xem topic đã được ghi nhận chưa. - Tên field của tick chỉ số lấy theo viết tắt server gửi, ý nghĩa được suy ra từ viết tắt và chưa xác nhận trên frame thật (xem Models).
- Chỉ số cổ phiếu thành phần và nến cổ phiếu dùng các method symbol/board ở 6.3.
Xem thêm Tutorial 16: WebSocket nâng cao và Tutorial 10: WebSocket Market Data.
6.8. Connection events và reconnect
onConnection nhận ConnectionEvent:
| Field | Kiểu | Mô tả |
|---|---|---|
state | 'connected' | 'reconnected' | 'disconnected' | 'failed' | Trạng thái |
reconnectCount | number | Số lần mở lại socket sau khi rớt |
error | Error? | Lỗi kèm theo, nếu có |
Các thuộc tính liên quan: isConnected, reconnectCount, lastError (lỗi khiến kết nối kết thúc hẳn, null khi bình thường), và wait(timeoutMs?).
Khi Config.autoReconnect bật (mặc định true), SDK tự giữ kết nối: mở lại socket khi rớt với exponential backoff có jitter (trần 60000 ms), luôn dùng token mới, mở lại trước khi token hết hạn, và gửi lại mọi subscription. Bạn không cần subscribe lại thủ công sau reconnected. Nếu server từ chối kết nối bằng 401/403, SDK làm mới token một lần. Server cho tối đa 10 socket mỗi client, SDK luôn đóng socket cũ trước khi mở socket mới. Nếu server trả 429 khi kết nối, SDK chờ tối thiểu 30000 ms. Request subscribe lớn được tách thành các frame nhỏ hơn 4096 byte.
Tổng quan method Streaming
| Nhóm | Method | Mô tả |
|---|---|---|
| Kết nối | connect() | Kết nối WebSocket |
disconnect() | Ngắt kết nối (Promise<void>) | |
wait(timeoutMs?) | Chờ tới khi kết nối kết thúc hẳn hoặc hết thời gian | |
| Heartbeat | ping(onResponse?, intervalMs?) | Ping server (tuỳ chọn lặp) |
| Symbol | subscribeSymbol(symbols, onResponse?) | Trade + Quote + Room |
subscribeSymbolTrade(symbols, onResponse?) | Chỉ Trade | |
subscribeSymbolQuote(symbols, onResponse?) | Chỉ Quote | |
subscribeSymbolRoom(symbols, onResponse?) | Chỉ Room ngoại | |
subscribeSymbolPutThrough(symbols, onResponse?) | Chỉ thoả thuận | |
subscribeSymbolOddLot(symbols, onResponse?) | Chỉ lô lẻ | |
subscribeSymbolOhlcv(symbols, interval, onResponse?) | OHLCV theo timeframe (tick, 1m, 5m) | |
| Sàn/Chỉ số | subscribeBoard(boards, onResponse?) | Theo sàn |
subscribeIndex(indices, onResponse?) | Cổ phiếu thành phần của chỉ số | |
subscribeIndexTrade(indices, interval?, onResponse?) | Tick/nến của chính chỉ số (xem 6.7) | |
subscribeIndexSummary(indices, onResponse?) | Tổng hợp chỉ số | |
| Thị trường | subscribeMarketFlag() | Cờ phiên thị trường (cần cho onMarketFlag) |
subscribeMarket(symbols) | Master data | |
| Giao dịch | subscribeOrderStatus(accountNo = '*', onResponse?) | Trạng thái lệnh (thường + FCO) |
subscribePortfolio(accountNo = '*', onResponse?) | Thay đổi danh mục | |
subscribeFcoOrderStatus(accountNo?, onResponse?) | Deprecated, dùng subscribeOrderStatus | |
| Unsubscribe | unsubscribeSymbol(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 | |
unsubscribeBoard(boards) | Huỷ sàn | |
unsubscribeIndex(indices) | Huỷ chỉ số | |
unsubscribeIndexTrade(indices, interval?) / unsubscribeIndexSummary(indices) | Huỷ tick/nến, huỷ tổng hợp chỉ số | |
unsubscribeOrderStatus(accountNo?) / unsubscribePortfolio(accountNo?) | Huỷ sự kiện giao dịch | |
| Truy vấn | listSubscription(timeoutMs = 5000) | Topic đang subscribe, dạng { trading, data } |
unsubscribeAll() | Huỷ mọi topic server báo cho connection này |
7. Trading WebSocket
Trading WebSocket cho phép đặt, sửa, hủy lệnh, thao tác lệnh điều kiện (FCO) và truy vấn tài khoản trên một socket wss://.../ws/v3/trading, thay vì gọi từng request REST. Dùng khi bạn muốn gom thao tác lệnh, sự kiện lệnh và truy vấn vào cùng một kết nối. Bảng option xem Cấu hình, kiểu TradingWSResponse xem Models, lỗi xem Xử lý lỗi.
7.1. Điều kiện và kết nối
- Đã xác thực bằng OTP:
await auth.ensureAuthenticated(otp). Hàm kết nối lấy token từAuth. Config.privateKeybắt buộc cho các lệnh đổi trạng thái (đặt, sửa, hủy, batch, FCO). Thiếu key thì SDK némWebSocketError. Lệnh truy vấn và subscribe không cần chữ ký.- Chỉ chấp nhận
wss://. - Không dùng chung với
TradinghoặcStream: hai client đó không bao giờ kết nối tới socket này.
Mọi thứ được export từ package chính @ssi.developer/ssi-sdk. tradingWs là alias của asyncTradingWs.
Thay đổi breaking ở 3.3.2
Trading WebSocket không còn là tính năng thử nghiệm. Entry point @ssi.developer/ssi-sdk/experimental đã bị xóa, không có alias hay re-export cũ, nên import từ đó sẽ lỗi cho tới khi bạn đổi. Tên, chữ ký và giá trị mặc định không đổi.
Trước: import { asyncTradingWs } from '@ssi.developer/ssi-sdk/experimental';
Sau: import { Auth, asyncTradingWs } from '@ssi.developer/ssi-sdk';
import { Auth, Config, asyncTradingWs, TradingWSError } from '@ssi.developer/ssi-sdk';
const config = new Config({
clientId: 'YOUR_CLIENT_ID',
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
privateKey: 'YOUR_PRIVATE_KEY',
});
const auth = new Auth(config);
await auth.ensureAuthenticated('YOUR_OTP'); // OTP của bạn
const ws = await asyncTradingWs(auth); // kết nối tới Config.tradingWsUrl
// ... dùng ws ...
await ws.close();URL lấy từ Config.tradingWsDomain, mặc định wss://api.ssi.com.vn/ws/v3/trading. Một host trần (host[:port]) được thêm wss:// và /ws/v3/trading. URL không có path được thêm path /ws/v3/trading. Dùng getter config.tradingWsUrl để xem URL cuối cùng.
| Hàm | Tham số | Trả về | Mô tả |
|---|---|---|---|
asyncTradingWs(auth, options?) | auth: Auth, options: TradingWSClientOptions | Promise<TradingWSService> | Kết nối và trả về service. requestTimeoutMs và userAgent mặc định lấy từ Config |
7.2. TradingWSService
Các method của TradingWSService (trả về Promise). Tên command trên dây được ghi trong cột cuối.
Lệnh
| Method | Tham số | Trả về | Command |
|---|---|---|---|
placeOrder | accountNo, symbol, side, quantity, price, orderType, clientRequestId? | PlaceOrderResponse | order.place |
amendOrder | accountNo, price?, quantity?, orderId?, clientRequestId? | ModifyOrderResponse | order.amend |
cancelOrder | accountNo, orderId?, clientRequestId? | CancelOrderResponse | order.cancel |
batchNewOrders | orders: readonly BatchPlaceOrderItem[] | TradingWSResponse | order.batchNew |
batchCancelOrders | orders: readonly BatchCancelOrderItem[] | TradingWSResponse | order.batchCancel |
Các method đặt, sửa, hủy trả về ack tức thì với orderStatus là PD. Kết quả thật của lệnh đến qua sự kiện lệnh (subscribeOrderEvents). Lệnh không bao giờ được retry. Batch qua WebSocket dùng cùng kiểu item và giới hạn Config.maxBatchOrders như REST, xem 5.6.
Lệnh điều kiện (FCO)
| Method | Tham số | Trả về | Command |
|---|---|---|---|
placeFco | order: FcoParamsKind ({ kind: 'gtd' | 'stop' | 'trailingStop' | 'oco' | 'bullBear', params }) | FCOPlaceResponse | fco.place |
cancelFco | fcoId, code? | FCOCancelResponse | fco.cancel |
listFco | accountNo, options?: FcoListOptions | TradingWSResponse | fco.list |
fcoOrderBook | fcoId | TradingWSResponse | fco.orderbook |
fcoStatusHistory | fcoId | TradingWSResponse | fco.statusHistory |
listFco nhận một processStatus đơn, không nhận mảng. fcoOrderBook gửi mặc định pageIndex=1, pageSize=10.
Truy vấn
| Method | Tham số | Trả về | Command |
|---|---|---|---|
queryPosition | accountNo, querySummary = true | TradingWSResponse | query.position |
queryMaxBuySell | accountNo, symbol, price? | TradingWSResponse | query.maxBuySell |
queryAccountBalance | accountNo | TradingWSResponse | query.accountBalance |
queryPpmmrAccount | accountNo | TradingWSResponse | query.ppmmrAccount |
queryOrderBook | accountNo, query?: OrderBookQuery | TradingWSResponse | query.orderBook |
queryMaxBuySell: bỏpricethì server dùng giá tham chiếu.OrderBookQuerygồmfromDate,toDate(dạngYYYY/MM/DD),symbol,orderStatus,pageIndex,pageSize.
Sự kiện và tiện ích
| Method | Tham số | Mô tả |
|---|---|---|
subscribeOrderEvents(accountNo) | string | Nhận sự kiện lệnh của một tài khoản (order.<account>) |
unsubscribeOrderEvents(accountNo) | string | Hủy |
subscribePortfolioEvents(accountNo) | string | Nhận sự kiện danh mục của một tài khoản (portfolio.<account>) |
unsubscribePortfolioEvents(accountNo) | string | Hủy |
unsubscribeAllEvents() | không | Hủy mọi subscription trading của socket |
command(method, params) | string, Record<string, unknown> | Gửi command bất kỳ, trả về frame phản hồi đầu tiên (không retry) |
close() | không | Đóng socket |
client (getter) | TradingWSClient bên dưới: onEvent, rateLimit, isConnected, rejection |
Subscribe sự kiện chỉ nhận một tài khoản mỗi lần, không dùng wildcard * hoặc danh sách. Truyền * hoặc dấu phẩy sẽ ném ValidationError.
7.3. Ví dụ
Ví dụ 1: kết nối, nhận sự kiện, truy vấn
import { Auth, Config, asyncTradingWs, TradingWSError } from '@ssi.developer/ssi-sdk';
const config = new Config({
clientId: 'YOUR_CLIENT_ID',
apiKey: 'YOUR_API_KEY',
apiSecret: 'YOUR_API_SECRET',
privateKey: 'YOUR_PRIVATE_KEY',
});
const auth = new Auth(config);
await auth.ensureAuthenticated('YOUR_OTP'); // OTP của bạn
const accountNo = 'YOUR_ACCOUNT_NO';
let ws;
try {
ws = await asyncTradingWs(auth);
} catch (error) {
// Môi trường có thể chưa hỗ trợ trading WebSocket: dùng REST thay thế
console.error(`Không kết nối được: ${error.message}`);
process.exit(1);
}
// Frame không gắn với command nào (sự kiện lệnh, danh mục, pong...) đi vào onEvent.
// Frame là object thô, SDK không parse thành model.
ws.client.onEvent = (frame) => {
console.log('[EVENT]', frame);
};
try {
await ws.subscribeOrderEvents(accountNo);
await ws.subscribePortfolioEvents(accountNo);
const position = await ws.queryPosition(accountNo, true);
console.log(`position: status ${position.status}`, position.result);
const max = await ws.queryMaxBuySell(accountNo, 'SSI'); // không truyền price: server dùng giá tham chiếu
console.log(`maxBuySell: status ${max.status}`, max.result);
const book = await ws.queryOrderBook(accountNo, { pageIndex: 1, pageSize: 10 });
console.log(`orderBook: status ${book.status}`, book.result);
} catch (error) {
if (error instanceof TradingWSError) {
console.error(`Server từ chối: status=${error.status} code=${error.errorCode} msg=${error.serverMessage}`);
} else {
console.error(`Lỗi: ${error.message}`);
}
} finally {
await ws.unsubscribeAllEvents().catch(() => undefined);
await ws.close();
}Ví dụ 2: đặt lệnh, nhận ack, theo dõi rate limit
import { OrderSide, OrderType } from '@ssi.developer/ssi-sdk';
const clientRequestId = `w${Date.now().toString(36)}`; // tối đa 20 ký tự
const ack = await ws.placeOrder(
accountNo, 'SSI', OrderSide.BUY, 100, '25.5', OrderType.LO, clientRequestId,
);
console.log(`Ack: orderId ${ack.orderId || '-'} | status ${ack.status}`); // ack, status PD
console.log('rateLimit:', ws.client.rateLimit);
// Kết quả thật của lệnh đến qua ws.client.onEvent (đã subscribeOrderEvents)
if (ack.orderId) {
const cancelled = await ws.cancelOrder(accountNo, ack.orderId);
console.log(`Hủy: status ${cancelled.status}`);
}Giá truyền dạng chuỗi thập phân thường (PriceLike), không dùng dạng mũ hoặc dấu phân cách hàng nghìn. Lệnh trên là lệnh thật, chỉ chạy trên tài khoản thử nghiệm.
7.4. Lỗi và lưu ý
| Lỗi | Khi nào |
|---|---|
TradingWSError | Server trả frame lỗi, hoặc từ chối kết nối sau khi đã nhận socket. Có status, errorCode, serverMessage. Kế thừa WebSocketError |
WebSocketError | Chưa kết nối (not connected), gửi thất bại, hết thời gian chờ phản hồi (không retry), socket đóng trước khi có phản hồi, hoặc thiếu private key cho lệnh đổi trạng thái |
ValidationError | Dữ liệu sai bị bắt tại máy (ví dụ subscribe nhiều tài khoản, batch rỗng hoặc quá giới hạn). Vì các method là async, lỗi này làm Promise bị reject |
- Từ chối sau khi nâng cấp kết nối: server chấp nhận socket (HTTP 101) rồi mới gửi frame
{code, msg}để từ chối. Vì vậyconnectchờhandshakeGraceMs(500 ms) và némTradingWSErrornếu nhận được frame này. Có thể đọc thêm quaws.client.rejection. - Chữ ký bị từ chối: với mã lỗi
401006,401008,401013ở chế độsignatureEncoding: 'auto', SDK thử lại đúng một lần bằng cách ký còn lại (server từ chối chữ ký nên lệnh chưa được thực thi). Nếu cả hai đều bị từ chối, lỗi có kèm gợi ý kiểm traConfig.privateKeycó đúng với API key hay không. Nếu mọi lệnh ký đều lỗi, thử đặtsignatureEncodingcố định. - Mất ack: nếu hết thời gian chờ, lệnh có thể đã được server nhận. SDK không retry. Đối chiếu qua sự kiện lệnh (
order.<account>) hoặcqueryOrderBooktrước khi gửi lại. - Bảo mật: SDK không log
params, chữ ký, token hoặc số tài khoản. Khi bạn tự log frame hoặc kết quả, che số tài khoản trước. Không đưa private key, API secret hoặc OTP vào mã nguồn hay log, dùng biến môi trường hoặc secret manager.
7.5. Reconnect và rate limit
Reconnect. Trading WebSocket không tự reconnect và không tự subscribe lại. Nếu socket bị đóng, ws.client.isConnected trở thành false và các lệnh đang chờ báo WebSocketError. Khi đó gọi ws.close() rồi tạo kết nối mới bằng asyncTradingWs(auth) và subscribe lại. Mặc định SDK gửi ping (PING_PONG) mỗi 30000 ms để giữ kết nối.
Rate limit. Sau mỗi phản hồi, SDK lưu rateLimit server gửi kèm vào ws.client.rateLimit. Khi remaining bằng 0 và respectRateLimit đang bật, lệnh kế tiếp sẽ chờ tối đa maxRateLimitWaitMs (mặc định 2000 ms, hoặc resetMs nếu nhỏ hơn) rồi mới gửi. Nếu server vẫn trả 429, bạn nhận TradingWSError với status 429: hãy chờ rồi thử lại có kiểm soát. Các field khác của rateLimit ngoài remaining và resetMs do server quyết định và SDK không đảm bảo.
7.6. Quan hệ với Stream client
Stream client (Stream) | Trading WebSocket (asyncTradingWs) | |
|---|---|---|
| Endpoint | Config.streamingUrl (mặc định wss://stream.ssi.com.vn/ws/v3) | Config.tradingWsUrl (mặc định wss://api.ssi.com.vn/ws/v3/trading) |
| Mục đích | Dữ liệu thị trường real-time và sự kiện lệnh/danh mục | Đặt/sửa/hủy lệnh, FCO, truy vấn tài khoản, sự kiện lệnh/danh mục |
| Reconnect | Có: tự mở lại, làm mới token, subscribe lại | Không |
| Subscribe tài khoản | Cho phép * | Chỉ một tài khoản mỗi lần |
| Dữ liệu sự kiện | Model có kiểu (onOrder, onOrderMatch, onPortfolio) | Frame thô qua client.onEvent |
Hai client dùng socket riêng, độc lập nhau. Nếu cần dữ liệu thị trường, vẫn dùng Stream (xem mục 6).
Xem thêm Tutorial 17: Trading WebSocket.