NodeJS

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:

MethodKhung thời gian
getOhlc1Minute(symbol)1 phút
getOhlc3Minute(symbol)3 phút
getOhlc5Minute(symbol)5 phút
getOhlc15Minute(symbol)15 phút
getOhlc1Hour(symbol)1 giờ
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):

MethodKhung 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ểuMặc địnhMô tả
symbolstring(bắt buộc)Mã chứng khoán
fromstring(bắt buộc)Ngày bắt đầu (yyyy/MM/dd)
tostring(bắt buộc)Ngày kết thúc (yyyy/MM/dd)
pagenumber1Số trang. Nhỏ hơn 1 bị từ chối tại máy
sizenumber1000Số 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:

MethodMô 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):

MethodTham 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ạng YYYY/MM/DD, from không sau to, 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ã.
  • language là '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ómMethodTrả 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ệnhgetTodayOrders(accountNo)Order[]
getHistoricalOrders(accountNo, from, to)Order[]
PPMMRgetEquityPpmmr(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);
MethodMô 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ặc auth.ensureAuthenticated(otp)).
  • Bắt buộc có Config.privateKey: SDK ký RSA lên toàn bộ body và gửi chữ ký trong header X-Signature.
  • Giới hạn số lệnh mỗi lô: Config.maxBatchOrders (mặc định 20).
MethodTham 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 ValidationError và 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.privateKey thì 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ó batchRequestId ngẫu nhiên. Id đã dùng trong ngày bị server trả HTTP 409, SDK ném DuplicateRequestError.
  • 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 DuplicateRequestError hoặ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.
  • batchRequestTime nằ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 BatchOrderResult cho mỗi lệnh. Hãy duyệt results và kiểm tra success, errorCode, errorMessage củ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 theo clientRequestId.
  • Để 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ómMethodTrả về
Đặt lệnhplaceOrder(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ệnhmodifyOrderPrice(accountNo, clientRequestId, price)ModifyOrderResponse
modifyOrderPriceByOrderId(accountNo, orderId, price)ModifyOrderResponse
modifyOrderQuantity(accountNo, clientRequestId, qty)ModifyOrderResponse
modifyOrderQuantityByOrderId(accountNo, orderId, qty)ModifyOrderResponse
Huỷ lệnhcancelOrder(accountNo, clientRequestId)CancelOrderResponse
cancelOrderById(accountNo, orderId)CancelOrderResponse
Batch (xem 5.6)placeBatchOrders(orders)BatchOrderResponse
cancelBatchOrders(orders)BatchOrderResponse
Sức mua/bángetMaxBuySell(accountNo, symbol, price)MaxBuySellResponse
getMaxBuySellAtMarketPrice(accountNo, symbol)MaxBuySellResponse
Lệnh điều kiệnplaceFcoGtd(...)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 Stream với Auth đã xác thực (như ở 6.1). Method nằm ở stream.streaming (StreamingService).
  • Gọi await stream.streaming.connect() trước khi subscribe.
MethodTham 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?: numberPromise<{ trading: string[]; data: string[] }>Hỏi server connection này đang subscribe topic nào
unsubscribeAll()khôngPromise<{ 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 indices là 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ải HNX-INDEX). UPCOMINDEX được SDK đổi thành HNXUPCOMINDEX khi gửi.
  • interval chỉ nhận tick, 1m hoặc 5m (STREAM_INTERVALS). Bỏ qua interval hoặc dùng tick để nhận tick. Giá trị khác bị ValidationError ngay tại máy.
  • Timeframe.MINUTE_1 ('1m') và Timeframe.MINUTE_5 ('5m') dùng được. Các Timeframe khá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ảng A-B.
  • Mỗi tổ hợp mã và interval là một topic riêng (trade.index.VN30 và trade.index.VN30@1m độc lập nhau).

Phân biệt subscribeIndex và subscribeIndexTrade

MethodNhậ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 đã subscribe

Lỗi và lưu ý

Tình huốngKế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ỗngValidationError 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ốiWebSocketError
Kết nối không phục hồi đượconConnection 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ỗiSDK ghi log và vẫn gọi các callback còn lại
  • Hành vi @* cho board/index và dữ liệu indexsummary phụ 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ằng listSubscription() 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:

FieldKiểuMô tả
state'connected' | 'reconnected' | 'disconnected' | 'failed'Trạng thái
reconnectCountnumberSố lần mở lại socket sau khi rớt
errorError?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ómMethodMô tả
Kết nốiconnect()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
Heartbeatping(onResponse?, intervalMs?)Ping server (tuỳ chọn lặp)
SymbolsubscribeSymbol(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ườngsubscribeMarketFlag()Cờ phiên thị trường (cần cho onMarketFlag)
subscribeMarket(symbols)Master data
Giao dịchsubscribeOrderStatus(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
UnsubscribeunsubscribeSymbol(symbols)Huỷ tất cả kênh
unsubscribeSymbolTrade(symbols)Huỷ trade
unsubscribeSymbolQuote(symbols)Huỷ quote
unsubscribeSymbolRoom(symbols)Huỷ room
unsubscribeSymbolPutThrough(symbols)Huỷ thoả thuận
unsubscribeSymbolOddLot(symbols)Huỷ lô lẻ
unsubscribeSymbolOhlcv(symbols, interval)Huỷ OHLCV
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ấnlistSubscription(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.privateKey bắ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ém WebSocketError. 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 Trading hoặc Stream: 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àmTham sốTrả vềMô tả
asyncTradingWs(auth, options?)auth: Auth, options: TradingWSClientOptionsPromise<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

MethodTham sốTrả vềCommand
placeOrderaccountNo, symbol, side, quantity, price, orderType, clientRequestId?PlaceOrderResponseorder.place
amendOrderaccountNo, price?, quantity?, orderId?, clientRequestId?ModifyOrderResponseorder.amend
cancelOrderaccountNo, orderId?, clientRequestId?CancelOrderResponseorder.cancel
batchNewOrdersorders: readonly BatchPlaceOrderItem[]TradingWSResponseorder.batchNew
batchCancelOrdersorders: readonly BatchCancelOrderItem[]TradingWSResponseorder.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)

MethodTham sốTrả vềCommand
placeFcoorder: FcoParamsKind ({ kind: 'gtd' | 'stop' | 'trailingStop' | 'oco' | 'bullBear', params })FCOPlaceResponsefco.place
cancelFcofcoId, code?FCOCancelResponsefco.cancel
listFcoaccountNo, options?: FcoListOptionsTradingWSResponsefco.list
fcoOrderBookfcoIdTradingWSResponsefco.orderbook
fcoStatusHistoryfcoIdTradingWSResponsefco.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

MethodTham sốTrả vềCommand
queryPositionaccountNo, querySummary = trueTradingWSResponsequery.position
queryMaxBuySellaccountNo, symbol, price?TradingWSResponsequery.maxBuySell
queryAccountBalanceaccountNoTradingWSResponsequery.accountBalance
queryPpmmrAccountaccountNoTradingWSResponsequery.ppmmrAccount
queryOrderBookaccountNo, query?: OrderBookQueryTradingWSResponsequery.orderBook
  • queryMaxBuySell: bỏ price thì server dùng giá tham chiếu.
  • OrderBookQuery gồm fromDate, toDate (dạng YYYY/MM/DD), symbol, orderStatus, pageIndex, pageSize.

Sự kiện và tiện ích

MethodTham sốMô tả
subscribeOrderEvents(accountNo)stringNhận sự kiện lệnh của một tài khoản (order.<account>)
unsubscribeOrderEvents(accountNo)stringHủy
subscribePortfolioEvents(accountNo)stringNhận sự kiện danh mục của một tài khoản (portfolio.<account>)
unsubscribePortfolioEvents(accountNo)stringHủy
unsubscribeAllEvents()khôngHủ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ỗiKhi nào
TradingWSErrorServer 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
WebSocketErrorChư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
ValidationErrorDữ 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ậy connect chờ handshakeGraceMs (500 ms) và ném TradingWSError nếu nhận được frame này. Có thể đọc thêm qua ws.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 tra Config.privateKey có đúng với API key hay không. Nếu mọi lệnh ký đều lỗi, thử đặt signatureEncoding cố đị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ặc queryOrderBook trướ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)
EndpointConfig.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 đíchDữ 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
ReconnectCó: tự mở lại, làm mới token, subscribe lạiKhông
Subscribe tài khoảnCho phép *Chỉ một tài khoản mỗi lần
Dữ liệu sự kiệnModel 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.

Trên trang này