API Services
Chi tiết các method API cho từng service trong Python SDK 3.3.2
1. Xác thực (TokenManager)
Truy cập qua auth.token_manager hoặc trực tiếp trên auth (delegate). Xem thêm Client Classes.
Xác thực với OTP
token = auth.authenticate(otp="<OTP>")
print(f"Expires at: {token.expires_at}")authenticate(otp=None, transaction_id=None): otp và transaction_id loại trừ nhau.
Yêu cầu gửi OTP
# Trả về OTPResponse(message, transaction_id)
# transaction_id là None khi OTP gửi qua SMS/email
otp = auth.request_otp_typed()
# Bản cũ: trả về dict thô của server
raw = auth.request_otp()Với Smart OTP, duyệt trên app rồi truyền transaction_id:
auth.ensure_authenticated(transaction_id=otp.transaction_id)SDK poll mỗi Config.otp_poll_interval_ms (mặc định 5000), tối đa otp_poll_max_wait_ms (mặc định 60000, tức 12 lần). Hết thời gian chưa duyệt: SmartOTPPendingError, gọi lại với cùng transaction_id. Bị từ chối hoặc hết hạn: SmartOTPRejectedError. Xem Exceptions.
Làm mới token
token = auth.refresh()Tự động đảm bảo xác thực
access_token = auth.ensure_authenticated(
otp=None, transaction_id=None, poll_interval_ms=None, poll_max_retries=None,
)Mỗi apiKey chỉ có 1 phiên hoạt động. Token được coi là hết hạn sớm 30 giây.
2. Tài khoản (AccountService)
Truy cập qua trading.account.
Lấy danh sách tài khoản
accounts = trading.account.get_account_info()
for acc in accounts:
print(f"{acc.account_no} - {acc.account_type}")Trả về: list[Account] — mỗi Account có account_no: str, account_type: AccountType.
3. Dữ liệu thị trường (MarketDataService)
Truy cập qua data.market_data. Chỉ cần auth.authenticate(), không cần OTP.
Lưu ý: Các method trả về một đối tượng đơn (
get_index_summary,get_index_summary_historical,get_board_summary,get_board_summary_historical,get_securities_info) trả vềNonekhi server không có dữ liệu (không phải exception). Phản hồi{"code": 204}cho kết quả là list rỗng ở các method trả list.
3.1. Dữ liệu OHLC (nến)
Trong ngày:
| Method | Timeframe |
|---|---|
get_ohlc_1minute(symbol) | 1 phút |
get_ohlc_3minute(symbol) | 3 phút |
get_ohlc_5minute(symbol) | 5 phút |
get_ohlc_15minute(symbol) | 15 phút |
get_ohlc_1hour(symbol) | 1 giờ |
ohlc = data.market_data.get_ohlc_1minute("SSI")
for candle in ohlc:
print(f"{candle.trading_date}: O={candle.open_price} H={candle.high_price} L={candle.low_price} C={candle.close_price} V={candle.volume}")Lịch sử (truyền khoảng ngày + phân trang):
| Method | Timeframe |
|---|---|
get_ohlc_1minute_historical(symbol, from_date, to_date, page, size) | 1 phút |
get_ohlc_3minute_historical(...) | 3 phút |
get_ohlc_5minute_historical(...) | 5 phút |
get_ohlc_15minute_historical(...) | 15 phút |
get_ohlc_1hour_historical(...) | 1 giờ |
get_ohlc_1day_historical(...) | 1 ngày |
ohlc = data.market_data.get_ohlc_1day_historical(
symbol="SSI",
from_date="2026/03/27 00:00:00",
to_date="2026/03/27 00:00:00",
page=1,
size=100,
)Tham số historical:
| Tham số | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
symbol | str | (bắt buộc) | Mã chứng khoán |
from_date | str | (bắt buộc) | Ngày bắt đầu, YYYY/MM/DD hoặc YYYY/MM/DD HH:MM:SS |
to_date | str | (bắt buộc) | Ngày kết thúc, cùng định dạng, from_date <= to_date |
page | int | 1 | Trang (>= 1) |
size | int | 1000 | Số bản ghi/trang (>= 1) |
Trả về: list[OHLCData]
Lưu ý:
get_ohlc_1week_historicalvàget_ohlc_1month_historicalluôn raiseValidationErrorvì server không hỗ trợ1w/1M(lỗi 400210). Timeframe hợp lệ: 1d, 1m, 3m, 5m, 15m, 1h.download_ohlc_1minutevàdownload_ohlc_1dayraiseNotImplementedError. Với nến phút/giờ,to_dateở tương lai được cắt về hôm nay.
Duyệt toàn bộ nến qua nhiều trang:
| Method | Mô tả |
|---|---|
get_ohlc_all(symbol, timeframe, from_date=None, to_date=None, size=1000, ascending=False) | Trả về list[OHLCData] của cả khoảng. ascending=True để cũ nhất trước |
iter_ohlc(symbol, timeframe, from_date=None, to_date=None, size=1000) | Iterator từng nến (bản async dùng async for) |
from ssi_sdk.enums import Timeframe
# Mặc định from_date/to_date là hôm nay
bars = data.market_data.get_ohlc_all("SSI", Timeframe.MINUTE_5, ascending=True)
for bar in data.market_data.iter_ohlc("SSI", Timeframe.DAY_1, "2026/01/01", "2026/03/31"):
print(bar.trading_date, bar.close_price)Endpoint OHLC trả nến mới nhất trước và không cho biết số trang, nên SDK dừng khi gặp trang rỗng hoặc trang ngắn.
3.2. Danh sách chỉ số thị trường
# Tất cả chỉ số
indices = data.market_data.get_indexes()
# Theo sàn
from ssi_sdk.enums import Board
indices = data.market_data.get_indexes_by_board(Board.HOSE)
for idx in indices:
print(f"{idx.index} - {idx.index_name}")Trả về: list[MarketIndexes]
3.3. Tổng hợp chỉ số (Index Summary)
# Summary hiện tại
summary = data.market_data.get_index_summary("VNINDEX")
# Summary lịch sử
summary = data.market_data.get_index_summary_historical("VNINDEX", "2025/01/15")
# Summary theo sàn
summary = data.market_data.get_board_summary(Board.HOSE)
# Summary sàn lịch sử
summary = data.market_data.get_board_summary_historical(Board.HOSE, "2025/01/15")Trả về: MarketIndexSummary | None
Lưu ý:
Board.DERIVATIVESchỉ hợp lệ trên stream và master data, không dùng được với các endpoint REST dữ liệu.
3.4. Thông tin chứng khoán
# 1 mã
info = data.market_data.get_securities_info("SSI")
# Theo chỉ số
securities = data.market_data.get_securities_info_by_index("VN30")
# Theo sàn
securities = data.market_data.get_securities_info_by_board(Board.HOSE)Trả về: SecuritiesInfo | None cho 1 mã; list[SecuritiesInfo] cho theo chỉ số / sàn.
3.5. Tổng hợp chứng khoán (Securities Summary)
# Summary hiện tại
summary = data.market_data.get_securities_summary("SSI")
# Summary lịch sử
summary = data.market_data.get_securities_summary_historical(
symbol="SSI",
from_date="2025/01/01",
to_date="2025/01/31",
)
# Summary theo chỉ số
summary = data.market_data.get_securities_summary_by_index("VN30")
# Summary theo chỉ số lịch sử
summary = data.market_data.get_securities_summary_by_index_historical(
index="VN30",
from_date="2025/01/01",
to_date="2025/01/31",
)Trả về: list[SecuritiesSummary]
securitiesSummary và masterdata được SDK tự phân trang theo pagesCount. Summary/master data của ngày hôm nay có thể được cache phía server tới 1 giờ.
3.6. Master data (giá trần, sàn, tham chiếu)
today = data.market_data.get_master_data()
history = data.market_data.get_master_data_historical("2026/03/01", "2026/03/05")
for m in today[:3]:
print(m.board, m.symbol, m.ceiling, m.floor, m.ref_price)Trả về: list[MasterData] (board, symbol, trading_date, ceiling, floor, ref_price).
3.7. Corporate actions (sự kiện quyền)
GET /api/v3/data/corporateActions. Có hai method, tách theo thời điểm: một cho hôm nay, một cho khoảng ngày trong quá khứ. SDK tự gọi hết các trang (page size 100) và trả về một list gộp.
# Hôm nay (mọi mã, hoặc một mã)
today = data.market_data.get_corporate_actions()
ssi_today = data.market_data.get_corporate_actions(symbol="SSI", language="en")
# Quá khứ: khoảng ngày tối đa 5 ngày
past = data.market_data.get_corporate_actions_historical(
from_date="2026/10/08",
to_date="2026/10/12",
symbol=None, # None = tất cả mã
language="vn", # "vn" (mặc định) hoặc "en"
)
for e in past:
print(e.symbol, e.event_title, e.exright_date, e.record_date, e.issue_date, e.exchange, e.ratio)Với AsyncData, các method này có cùng tên và tham số; gọi bằng await.
| Method | Tham số | Mô tả |
|---|---|---|
get_corporate_actions | symbol: str | None = None, language: str = "vn" | Sự kiện quyền của hôm nay (cổ tức, thưởng, phát hành thêm, ...) |
get_corporate_actions_historical | from_date: str, to_date: str, symbol: str | None = None, language: str = "vn" | Sự kiện quyền trong một khoảng ngày trong quá khứ |
| Tham số | Mô tả |
|---|---|
from_date / to_date | YYYY/MM/DD |
symbol | Một mã, hoặc None để lấy tất cả mã |
language | "vn" (mặc định) hoặc "en" |
Trả về: list[CorporateAction] (symbol, event_title, exright_date, record_date, issue_date, exchange, ratio). Mọi trường đều là str đúng như server trả: ngày giữ nguyên dạng text YYYY/MM/DD, ratio không ép kiểu. Trường nào server để trống (ví dụ sự kiện mới công bố chưa có ngày giao dịch không hưởng quyền hoặc tỷ lệ) sẽ là "". Khi server không có dữ liệu, list trả về rỗng. Xem Models: CorporateAction.
SDK kiểm tra các quy tắc sau trước khi gửi; vi phạm thì ném ValidationError và không gọi request nào:
from_datevàto_datephải đúng định dạngYYYY/MM/DD.from_datekhông được sauto_date.to_datetối đa 5 ngày saufrom_date.symbol(nếu có) phải là một mã gồm chữ và số, tối đa 20 ký tự.languagephải làvnhoặcen(không phân biệt hoa thường).
get_corporate_actions không nhận ngày: nó dùng ngày hôm nay. Lỗi từ server ném APIError, response có cấu trúc không đúng cũng ném APIError.
Lưu ý: Hai method này được viết theo tài liệu API chính thức và chưa được chạy thử với server thật (xem changelog của SDK). Hãy test trong môi trường của bạn trước.
4. Danh mục đầu tư (PortfolioService)
Truy cập qua trading.portfolio.
4.1. Số dư tài khoản
# Số dư cơ sở
equity_balance = trading.portfolio.get_equity_balance("<EQUITY_ACCOUNT_NO>")
if equity_balance:
print(f"Account balance: {equity_balance.account_balance}")
# Số dư phái sinh
derivative_balance = trading.portfolio.get_derivative_balance("<DERIVATIVE_ACCOUNT_NO>")
if derivative_balance:
print(f"Withdrawable: {derivative_balance.withdrawable}")4.2. Vị thế (Positions)
# Vị thế cơ sở: None khi không có vị thế
positions = trading.portfolio.get_equity_positions("<EQUITY_ACCOUNT_NO>") or []
for pos in positions:
print(f"{pos.symbol}: {pos.quantity} cp | Giá vốn: {pos.cost_price}")
# Vị thế phái sinh (tất cả)
derivative_positions = trading.portfolio.get_derivative_positions("<DERIVATIVE_ACCOUNT_NO>")
# Chỉ vị thế mở phái sinh
open_pos = trading.portfolio.get_open_derivative_positions("<DERIVATIVE_ACCOUNT_NO>")
# Chỉ vị thế đóng phái sinh
closed_pos = trading.portfolio.get_closed_derivative_positions("<DERIVATIVE_ACCOUNT_NO>")Lưu ý: Khi tài khoản không có vị thế (hoặc server trả
{"code": 204}),get_equity_positionsvàget_derivative_positionstrả vềNone(type hint ghilist). Hãy kiểm traNonetrước khi duyệt. Trong 3.3.2,get_open_derivative_positionsvàget_closed_derivative_positionsđọc trực tiếp từ kết quả đó nên có thể raiseAttributeErrorkhi không có vị thế; nếu cần an toàn, gọiget_derivative_positionsvà kiểm traNone.get_equity_balance,get_derivative_balance,get_equity_ppmmr,get_derivative_ppmmrcũng có thể trả vềNone.
4.3. Sổ lệnh (Order Book)
# Lệnh trong ngày
today_orders = trading.portfolio.get_today_orders("<EQUITY_ACCOUNT_NO>")
for order in today_orders:
print(f"{order.order_id}: {order.symbol} {order.side} {order.quantity}@{order.price} - {order.status}")
# Lệnh lịch sử
historical_orders = trading.portfolio.get_historical_orders(
"<EQUITY_ACCOUNT_NO>",
from_date="2025/01/01",
to_date="2025/01/31",
)Phản hồi {"code": 204} cho list rỗng. Order.os_quantity có kiểu int | None (là None trên REST).
4.4. PPMMR
# PPMMR cơ sở
equity_ppmmr = trading.portfolio.get_equity_ppmmr("<EQUITY_ACCOUNT_NO>")
if equity_ppmmr:
print(f"Purchasing power: {equity_ppmmr.purchasing_power}")
# PPMMR phái sinh
derivative_ppmmr = trading.portfolio.get_derivative_ppmmr("<DERIVATIVE_ACCOUNT_NO>")Tổng quan method Portfolio
| Nhóm | Method | Trả về |
|---|---|---|
| Số dư | get_equity_balance(account_no) | EquityAccountBalance | None |
get_derivative_balance(account_no) | DerivativeAccountBalance | None | |
| Vị thế | get_equity_positions(account_no) | list[EquityPosition] (None khi rỗng) |
get_derivative_positions(account_no) | list[AllDerivativePosition] (None khi rỗng) | |
get_open_derivative_positions(account_no) | list[DerivativePosition] | |
get_closed_derivative_positions(account_no) | list[DerivativePosition] | |
| Sổ lệnh | get_today_orders(account_no) | list[Order] |
get_historical_orders(account_no, from_date, to_date) | list[Order] | |
| PPMMR | get_equity_ppmmr(account_no) | EquityPPMMR | None |
get_derivative_ppmmr(account_no) | DerivativePPMMR | None |
5. Giao dịch (TradingService)
Truy cập qua trading.trading. Các lệnh thay đổi trạng thái (đặt, sửa, hủy, batch, FCO) được ký RSA-SHA256, cần Config.private_key. SDK không tự retry POST/PUT/DELETE.
5.1. Đặt lệnh
from ssi_sdk.enums import OrderSide, OrderType
# Lệnh giới hạn (LO)
result = trading.trading.place_limit_order(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
price=66000,
)
print(f"Order ID: {result.order_id}, Status: {result.status}")
# Lệnh thị trường (MTL)
result = trading.trading.place_market_order(
account_no="<ACCOUNT_NO>", symbol="SSI", side=OrderSide.BUY, quantity=100,
)
# Lệnh ATO (mở cửa)
result = trading.trading.place_ato_order(
account_no="<ACCOUNT_NO>", symbol="SSI", side=OrderSide.BUY, quantity=100,
)
# Lệnh ATC (đóng cửa)
result = trading.trading.place_atc_order(
account_no="<ACCOUNT_NO>", symbol="SSI", side=OrderSide.SELL, quantity=100,
)
# Lệnh tuỳ chỉnh
result = trading.trading.place_order(
account_no="<ACCOUNT_NO>", symbol="SSI", side=OrderSide.BUY,
quantity=100, price=66000, order_type=OrderType.LO,
)client_request_id là tham số tuỳ chọn ở mọi method place_* (tối đa 20 ký tự). Bỏ trống thì SDK tự sinh. Dùng lại một id đã dùng trong ngày: server trả 409, SDK raise DuplicateRequestError và không tự gửi lại. Giá có thể truyền int, float, Decimal hoặc str; nên dùng Decimal/str để tránh sai số dấu phẩy động.
5.2. Sửa lệnh
Chỉ sửa giá hoặc số lượng (không đồng thời).
# Sửa giá theo client_request_id
result = trading.trading.modify_order_price(
account_no="<ACCOUNT_NO>", client_request_id="REQ123", price=68000,
)
# Sửa giá theo order_id
result = trading.trading.modify_order_price_by_order_id(
account_no="<ACCOUNT_NO>", order_id="ORD123", price=68000,
)
# Sửa số lượng theo client_request_id
result = trading.trading.modify_order_quantity(
account_no="<ACCOUNT_NO>", client_request_id="REQ123", quantity=200,
)
# Sửa số lượng theo order_id
result = trading.trading.modify_order_quantity_by_order_id(
account_no="<ACCOUNT_NO>", order_id="ORD123", quantity=200,
)5.3. Huỷ lệnh
# Huỷ theo client_request_id
result = trading.trading.cancel_order(
account_no="<ACCOUNT_NO>", client_request_id="REQ123",
)
# Huỷ theo order_id
result = trading.trading.cancel_order_by_order_id(
account_no="<ACCOUNT_NO>", order_id="ORD123",
)5.4. Batch Order
Gửi nhiều lệnh trong một request POST /order/batch (đặt) hoặc DELETE /order/batch (hủy). Toàn bộ body được ký RSA-SHA256 một lần, kết quả trả về theo từng lệnh. Dùng khi cần đặt/hủy nhiều lệnh cùng lúc (ví dụ rổ lệnh). Với từng lệnh riêng lẻ, dùng place_order / cancel_order ở trên.
Điều kiện: Trading / AsyncTrading (trading.trading), đã xác thực OTP hoặc Smart OTP, Config.private_key đã cấu hình.
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
place_batch_orders(orders) | orders: Sequence[BatchPlaceOrderItem] | BatchOrderResponse | POST /order/batch: đặt nhiều lệnh |
cancel_batch_orders(orders) | orders: Sequence[BatchCancelOrderItem] | BatchOrderResponse | DELETE /order/batch: hủy nhiều lệnh |
Giới hạn số lệnh
- Một batch có từ 1 đến
Config.max_batch_orderslệnh. Mặc định là 20. - Vượt giới hạn hoặc batch rỗng: SDK raise
ValidationErrorngay, chưa gửi gì. max_batch_orderskhông hợp lệ hoặc nhỏ hơn 1 sẽ được đặt lại về 20. Xem Configuration.
Lưu ý: API reference (Đặt batch lệnh) ghi giới hạn phía server là 50 lệnh mỗi request, trong khi mặc định của SDK là 20. Việc nâng
Config.max_batch_ordersdo bên gọi quyết định; hãy theo giới hạn áp dụng cho API Key của bạn.
Item đặt lệnh (BatchPlaceOrderItem)
TypedDict, dùng key snake_case:
| Key | Bắt buộc | Mô tả |
|---|---|---|
account_no | Có | Số tài khoản |
symbol | Có | Mã chứng khoán |
side | Có | OrderSide.BUY / OrderSide.SELL |
quantity | Có | Khối lượng (số nguyên dương) |
price | Có | Giá. Có thể là None với loại lệnh không cần giá (ATO, ATC, MP) |
order_type | Có | OrderType (LO, ATO, ATC, MP...) |
client_request_id | Không | Khóa idempotency của từng lệnh, tối đa 20 ký tự. Bỏ trống thì SDK tự sinh |
Item hủy lệnh (BatchCancelOrderItem)
| Key | Bắt buộc | Mô tả |
|---|---|---|
account_no | Có | Số tài khoản |
order_id | Một trong hai | Mã lệnh do server cấp |
client_request_id | Một trong hai | Khóa idempotency khi đặt lệnh |
Mỗi item chỉ được có đúng một trong order_id / client_request_id.
Kết quả
Cả hai method trả về BatchOrderResponse(batch_request_id, results: list[BatchOrderResult]). Mỗi BatchOrderResult có client_request_id, order_id, status, success, error_code, error_message, client_cancel_id (chỉ khi hủy batch). Chi tiết thuộc tính xem Models: Batch Order.
Ký và idempotency
- SDK dựng body camelCase
{"batchRequestId", "batchRequestTime", "orders": [...]}.batchRequestIdlà id ngẫu nhiên,batchRequestTimelà epoch mili giây. - Toàn bộ body được ký RSA-SHA256 và gửi trong header
X-Signature. Bạn không cần tự ký. - Server từ chối batch có
batchRequestTimenằm ngoài cửa sổ chống replay (mã400111). Giữ đồng hồ máy chủ chính xác. - Gửi lại batch với id đã dùng trong ngày: HTTP 409, SDK raise
DuplicateRequestError. client_request_idcủa từng lệnh là khóa idempotency. Tự đặt để còn tra cứu và hủy sau này.- SDK không tự retry request
POST/DELETEkhi hết thời gian chờ. Lệnh có thể đã nằm trên sàn: kiểm tra sổ lệnh trước khi gửi lại.
Lưu ý: Mọi lệnh trong batch được validate trước khi gửi. Một lệnh sai khiến cả batch bị từ chối (
ValidationErrorphía SDK, hoặcAPIErrorphía server). Ngược lại, khi batch đã được nhận, một số lệnh vẫn có thể thất bại riêng lẻ: luôn đọcsuccesscủa từngBatchOrderResult.
Ví dụ sync: đặt batch rồi hủy batch
Ví dụ gửi lệnh thật. Lệnh là LO mua ở giá sàn (xa thị trường) với khối lượng nhỏ.
import uuid
from decimal import Decimal
from ssi_sdk import Auth, Trading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
from ssi_sdk.models import BatchCancelOrderItem, BatchPlaceOrderItem
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
ACCOUNT_NO = "<ACCOUNT_NO>"
FLOOR_PRICE = Decimal("<FLOOR_PRICE>") # lấy từ master data (mục 3.6)
with Auth(config) as auth:
auth.ensure_authenticated(otp="<OTP>")
with Trading(auth) as trading:
tag = uuid.uuid4().hex[:8]
place_items: list[BatchPlaceOrderItem] = [
{
"account_no": ACCOUNT_NO,
"symbol": "SSI",
"side": OrderSide.BUY,
"quantity": 100,
"price": FLOOR_PRICE,
"order_type": OrderType.LO,
"client_request_id": f"bt{tag}{i:02d}", # <= 20 ký tự
}
for i in range(1, 4)
]
if len(place_items) > config.max_batch_orders:
raise SystemExit(f"Tối đa {config.max_batch_orders} lệnh mỗi batch")
try:
response = trading.trading.place_batch_orders(place_items)
except ValidationError as e:
raise SystemExit(f"Batch không hợp lệ (chưa gửi gì): {e}")
except DuplicateRequestError:
raise SystemExit("Batch id đã dùng trong ngày (409): kiểm tra sổ lệnh trước khi gửi lại")
except APIError as e:
raise SystemExit(f"Server từ chối batch: code={e.code} status={e.status_code} msg={e.message}")
placed_ok: list[str] = []
for r in response.results:
if r.success:
placed_ok.append(r.order_id)
print(f"OK {r.client_request_id} -> orderId={r.order_id} status={r.status}")
else:
print(f"FAIL {r.client_request_id} -> code={r.error_code} msg={r.error_message}")
if placed_ok:
# Mỗi item chỉ có ĐÚNG MỘT trong order_id / client_request_id
cancel_items: list[BatchCancelOrderItem] = [
{"account_no": ACCOUNT_NO, "order_id": oid} for oid in placed_ok
]
try:
cancel_resp = trading.trading.cancel_batch_orders(cancel_items)
except (ValidationError, DuplicateRequestError, APIError) as e:
raise SystemExit(f"Hủy batch thất bại: {type(e).__name__}: {e}")
for r in cancel_resp.results:
state = "OK " if r.success else "FAIL"
print(f"{state} orderId={r.order_id} cancelId={r.client_cancel_id} status={r.status}")Ví dụ async
import asyncio
import uuid
from decimal import Decimal
from ssi_sdk import AsyncAuth, AsyncTrading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
ACCOUNT_NO = "<ACCOUNT_NO>"
async def main():
async with AsyncAuth(config) as auth:
await auth.ensure_authenticated(otp="<OTP>")
async with AsyncTrading(auth) as trading:
tag = uuid.uuid4().hex[:8]
items = [
{
"account_no": ACCOUNT_NO,
"symbol": "SSI",
"side": OrderSide.BUY,
"quantity": 100,
"price": Decimal("<FLOOR_PRICE>"),
"order_type": OrderType.LO,
"client_request_id": f"bt{tag}{i:02d}",
}
for i in range(1, 4)
]
try:
response = await trading.trading.place_batch_orders(items)
except ValidationError as e:
raise SystemExit(f"Batch không hợp lệ (chưa gửi gì): {e}")
except DuplicateRequestError:
raise SystemExit("Batch id đã dùng trong ngày (409)")
except APIError as e:
raise SystemExit(f"Server từ chối batch: code={e.code} msg={e.message}")
ok_ids = [r.order_id for r in response.results if r.success]
for r in response.results:
if not r.success:
print(f"FAIL {r.client_request_id}: {r.error_code} {r.error_message}")
if ok_ids:
cancel_items = [{"account_no": ACCOUNT_NO, "order_id": oid} for oid in ok_ids]
cancel_resp = await trading.trading.cancel_batch_orders(cancel_items)
print(f"Đã hủy: {sum(1 for r in cancel_resp.results if r.success)}/{len(cancel_items)}")
asyncio.run(main())Xử lý lỗi batch
| Exception | Khi nào | Đã gửi lên server? |
|---|---|---|
ValidationError | Batch rỗng, vượt max_batch_orders, một lệnh thiếu/sai field, item hủy có cả hai hoặc không có order_id / client_request_id | Chưa |
DuplicateRequestError (APIError, HTTP 409) | Batch id đã dùng trong ngày | Đã tới server. Kiểm tra sổ lệnh trước khi gửi lại |
APIError | Server từ chối cả batch (có code, status_code, message), ví dụ 400111 khi thời gian ngoài cửa sổ replay | Đã tới server |
RateLimitError | Server trả HTTP 429 (có retry_after_ms, limit, remaining) | Đã tới server, bị từ chối |
AuthenticationError | Server trả HTTP 401/403 (token không hợp lệ hoặc không đủ quyền), hoặc chưa xác thực | Bị từ chối |
Lỗi theo từng lệnh không phải exception: nằm trong BatchOrderResult.success, error_code, error_message. Xem Exceptions. Tham khảo thêm: Tutorial: Batch Orders, API: Đặt batch lệnh, API: Hủy batch lệnh. Gửi lệnh qua WebSocket: xem mục 7 (batch_new_orders, batch_cancel_orders).
5.5. Sức mua/bán tối đa
# Với giá cụ thể
max_bs = trading.trading.get_max_buy_sell(
account_no="<ACCOUNT_NO>", symbol="SSI", price=66000,
)
print(f"Max buy: {max_bs.max_buy_quantity}")
print(f"Max sell: {max_bs.max_sell_quantity}")
# Với giá thị trường
max_bs = trading.trading.get_max_buy_sell_at_market_price(
account_no="<ACCOUNT_NO>", symbol="SSI",
)Tổng quan method Trading
| Nhóm | Method | Trả về |
|---|---|---|
| Đặt lệnh | place_order(account_no, symbol, side, quantity, price, order_type, client_request_id=None) | PlaceOrderResponse |
place_limit_order(account_no, symbol, side, quantity, price, client_request_id=None) | PlaceOrderResponse | |
place_market_order(account_no, symbol, side, quantity, client_request_id=None) | PlaceOrderResponse | |
place_ato_order(account_no, symbol, side, quantity, client_request_id=None) | PlaceOrderResponse | |
place_atc_order(account_no, symbol, side, quantity, client_request_id=None) | PlaceOrderResponse | |
| Sửa lệnh | modify_order_price(account_no, client_request_id, price) | ModifyOrderResponse |
modify_order_price_by_order_id(account_no, order_id, price) | ModifyOrderResponse | |
modify_order_quantity(account_no, client_request_id, quantity) | ModifyOrderResponse | |
modify_order_quantity_by_order_id(account_no, order_id, quantity) | ModifyOrderResponse | |
| Huỷ lệnh | cancel_order(account_no, client_request_id) | CancelOrderResponse |
cancel_order_by_order_id(account_no, order_id) | CancelOrderResponse | |
| Batch | place_batch_orders(orders) | BatchOrderResponse |
cancel_batch_orders(orders) | BatchOrderResponse | |
| Sức mua/bán | get_max_buy_sell(account_no, symbol, price) | MaxBuySellResponse |
get_max_buy_sell_at_market_price(account_no, symbol) | MaxBuySellResponse | |
| Lệnh điều kiện | place_fco_gtd(account_no, symbol, side, quantity, price, price_slip, from_date, to_date, code=None) | FCOPlaceResponse |
place_fco_stop(account_no, symbol, side, quantity, stop_price, operator, from_date, to_date, code=None) | FCOPlaceResponse | |
place_fco_stop_limit(account_no, symbol, side, quantity, price, price_slip, stop_price, operator, from_date, to_date, code=None) | FCOPlaceResponse | |
place_fco_trailing_stop(account_no, symbol, side, quantity, active_price, trailing_amount, from_date, to_date, code=None) | FCOPlaceResponse | |
place_fco_trailing_stop_limit(account_no, symbol, side, quantity, active_price, trailing_amount, price_slip, from_date, to_date, code=None) | FCOPlaceResponse | |
place_fco_oco(account_no, symbol, side, quantity, tp_active_price, sl_active_price, tp_price, sl_price, tp_slip, sl_slip, from_date, to_date, code=None) | FCOPlaceResponse | |
place_fco_bull_bear(account_no, symbol, side, quantity, price, price_slip, tp_active_price, sl_active_price, tp_price, sl_price, tp_slip, sl_slip, from_date, to_date, code=None) | FCOPlaceResponse | |
cancel_fco(fco_id, code=None) | FCOCancelResponse | |
get_fco_by_account_no(account_no, page_index, page_size) | FCOListResponse | |
get_fco_by_symbol(account_no, symbol, page_index, page_size) | FCOListResponse | |
get_fco_by_status(account_no, process_status, page_index, page_size) | FCOListResponse | |
get_fco_by_date(account_no, from_date, to_date, page_index, page_size) | FCOListResponse | |
get_fco_by_id(account_no, fco_id) | FCOInfo | None | |
get_fco_order_book(fco_id, page_index, page_size) | FCOOrderBookResponse | |
get_fco_status_history(fco_id) | list[FCOStatusHistoryItem] |
5.6. Giao dịch điều kiện (FCO)
Hỗ trợ các loại lệnh điều kiện linh hoạt (Flexible Conditional Order): GTD, Stop, Stop Limit, Trailing Stop, Trailing Stop Limit, OCO, và Bull Bear. Cả Trading (sync) và AsyncTrading (async) đều hỗ trợ đầy đủ các method này.
Quy tắc: from_date / to_date theo định dạng YYYY/MM/DD HH:MM:SS, from_date <= to_date và cửa sổ không quá 31 ngày. Mọi place_fco_* và cancel_fco có tham số tuỳ chọn code (mã OTP) nếu server yêu cầu xác nhận.
from ssi_sdk.enums import OrderSide, OrderType, FCOOperator
# Lệnh GTD (Good Till Date)
gtd_res = trading.trading.place_fco_gtd(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
price=OrderType.MTL, # Hoặc giá số cụ thể
price_slip=0.5,
from_date="2026/07/21 00:00:00",
to_date="2026/07/28 23:00:00",
)
# Lệnh Stop (dừng thị trường)
stop_res = trading.trading.place_fco_stop(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
stop_price=20000,
operator=FCOOperator.GREATER_OR_EQUAL,
from_date="2026/07/21 00:00:00",
to_date="2026/07/28 23:00:00",
)
# Lệnh Stop Limit (dừng giới hạn)
stop_limit_res = trading.trading.place_fco_stop_limit(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
stop_price=20000,
price=20500,
price_slip=0.5,
operator=FCOOperator.GREATER_OR_EQUAL,
from_date="2026/07/21 00:00:00",
to_date="2026/07/28 23:00:00",
)
# Lệnh Trailing Stop (bám sát xu hướng)
ts_res = trading.trading.place_fco_trailing_stop(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
active_price=20000,
trailing_amount=1000,
from_date="2026/07/21 00:00:00",
to_date="2026/07/28 23:00:00",
)
# Lệnh OCO (huỷ lệnh kia khi 1 trong 2 khớp)
oco_res = trading.trading.place_fco_oco(
account_no="<ACCOUNT_NO>",
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
tp_active_price=30000, # Giá kích hoạt chốt lời
sl_active_price=20000, # Giá kích hoạt cắt lỗ
tp_price=OrderType.MP,
sl_price=OrderType.MP,
tp_slip=0.1,
sl_slip=0,
from_date="2026/07/21 00:00:00",
to_date="2026/07/28 23:00:00",
)
# Huỷ lệnh điều kiện
cancel_res = trading.trading.cancel_fco(fco_id="<FCO_ID>")
# Bất đồng bộ: cú pháp await tương tự trên trading từ AsyncTrading
gtd_res = await trading.trading.place_fco_gtd(
account_no="<ACCOUNT_NO>", symbol="SSI", side=OrderSide.BUY, quantity=100,
price=OrderType.MTL, price_slip=0.5,
from_date="2026/07/21 00:00:00", to_date="2026/07/28 23:00:00",
)Trả về: FCOPlaceResponse (fco_id) khi đặt lệnh, FCOCancelResponse (fco_id) khi huỷ lệnh.
# Lấy toàn bộ lệnh FCO theo tài khoản
fcos = trading.trading.get_fco_by_account_no(account_no="<ACCOUNT_NO>")
# Lấy FCO theo mã chứng khoán
fcos_symbol = trading.trading.get_fco_by_symbol(account_no="<ACCOUNT_NO>", symbol="SSI")
# Lấy FCO theo trạng thái xử lý
fcos_status = trading.trading.get_fco_by_status(account_no="<ACCOUNT_NO>", process_status="TRIT")
# Lấy FCO theo khoảng thời gian (ngày trần được chấp nhận)
fcos_date = trading.trading.get_fco_by_date(account_no="<ACCOUNT_NO>", from_date="2026/07/01", to_date="2026/07/21")
# Lấy một lệnh FCO cụ thể bằng ID
fco_info = trading.trading.get_fco_by_id(account_no="<ACCOUNT_NO>", fco_id="<FCO_ID>")
# Lấy nhật ký khớp lệnh (order book) của lệnh FCO
order_book = trading.trading.get_fco_order_book(fco_id="<FCO_ID>")
# Lịch sử trạng thái của lệnh FCO (rỗng thì trả về [])
history = trading.trading.get_fco_status_history(fco_id="<FCO_ID>")
for h in history:
print(h.state, h.time, h.code, h.detail)Lưu ý: Các method truy vấn danh sách hỗ trợ tham số tuỳ chọn
page_indexvàpage_sizeđể phân trang, và đều gọi được bất đồng bộ vớiawait.FCOInfocó thêmprice_slipvàorder_type;FCOOrdercó thêmtype.
6. Streaming realtime (StreamingService)
Truy cập qua stream.streaming. Cần gọi connect() trước. Client: Stream (sync) hoặc AsyncStream (async, dùng await; callback có thể là coroutine). Stream / AsyncStream tự ngắt kết nối khi thoát with / async with.
Kênh dữ liệu thị trường không yêu cầu OTP riêng; kênh trading (on_order, on_order_match, on_portfolio) cần OTP.
6.1. Kết nối
stream.streaming.connect() # Kết nối WebSocket
stream.streaming.disconnect() # Ngắt kết nối
stream.streaming.wait(timeout_ms=None) # Chờ nhận dữ liệu (block)6.2. Thiết lập callback
Đăng ký callback trước connect() để không lỡ sự kiện connected.
def on_market_data(msg):
print(f"Market data: {msg}")
def on_trading_data(msg):
print(f"Trading: {msg}")
def on_heartbeat(msg):
print(f"Heartbeat: {msg.status}")
stream.streaming.on_data = on_market_data
stream.streaming.on_trading = on_trading_data
stream.streaming.on_heartbeat = on_heartbeat| Thuộc tính | Nhận | Ghi chú |
|---|---|---|
on_data | Mọi message dữ liệu thị trường | Gồm cả IndexTickMessage, IntervalMessage |
on_trading | Mọi message kênh trading | |
on_heartbeat | Heartbeat | |
on_market_flag | MarketFlagMessage | |
on_index_summary | IndexSummaryMessage | |
on_order, on_order_match, on_portfolio | Message trading theo loại | Cần OTP |
on_connection | ConnectionEvent | Trạng thái kết nối, xem 6.9 |
Callback typed không thay thế on_data / on_trading (vẫn nhận mọi message) và đặt callback không tự subscribe. Callback chạy trên thread/task của SDK: giữ NGẮN (đếm, ghi nhận, đẩy vào queue).
6.3. Subscribe dữ liệu thị trường
# Tất cả kênh (trade + quote + room) cho mã
stream.streaming.subscribe_symbol(["SSI", "HPG", "VIC"])
# Từng kênh riêng
stream.streaming.subscribe_symbol_trade(["SSI", "HPG"])
stream.streaming.subscribe_symbol_quote(["SSI", "HPG"])
stream.streaming.subscribe_symbol_room(["SSI", "HPG"])
stream.streaming.subscribe_symbol_put_through(["SSI"])
stream.streaming.subscribe_symbol_odd_lot(["SSI"])
# OHLCV theo interval
from ssi_sdk.enums import Timeframe
stream.streaming.subscribe_symbol_ohlcv(["SSI"], interval=Timeframe.MINUTE_1)
# Theo sàn
from ssi_sdk.enums import Board
stream.streaming.subscribe_board([Board.HOSE, Board.HNX])
# Theo chỉ số (mã thành phần), xem 6.7
stream.streaming.subscribe_index(["VNINDEX", "VN30"])
# Thị trường
stream.streaming.subscribe_market(["HOSE"])Quy tắc topic áp dụng cho mọi subscribe: danh sách rỗng gây ValidationError("at least one symbol is required"); mã được viết hoa; mỗi topic chỉ nhận một bộ lọc (*, một mã, hoặc range A-B), dấu phẩy bị từ chối. Các topic asset.* và margin.* không được hỗ trợ (ValidationError). Tham số sai raise ValidationError, chưa gửi gì lên server. Chi tiết interval xem 6.7.3.
6.4. Unsubscribe
stream.streaming.unsubscribe_symbol(["SSI", "HPG"])
stream.streaming.unsubscribe_symbol_trade(["SSI"])
stream.streaming.unsubscribe_symbol_quote(["SSI"])
stream.streaming.unsubscribe_symbol_room(["SSI"])
stream.streaming.unsubscribe_symbol_put_through(["SSI"])
stream.streaming.unsubscribe_symbol_odd_lot(["SSI"])
# Huỷ OHLCV
stream.streaming.unsubscribe_symbol_ohlcv(["SSI"], interval=Timeframe.MINUTE_1)
stream.streaming.unsubscribe_board([Board.HOSE])
stream.streaming.unsubscribe_index(["VNINDEX"])6.5. Subscribe giao dịch
# Trạng thái lệnh (tất cả tài khoản)
stream.streaming.subscribe_order_status()
# Cho tài khoản cụ thể
stream.streaming.subscribe_order_status(account_no="<ACCOUNT_NO>")
# Portfolio
stream.streaming.subscribe_portfolio()
stream.streaming.subscribe_portfolio(account_no="<ACCOUNT_NO>")6.6. Heartbeat
# Ping một lần
stream.streaming.ping()
# Ping tự động theo chu kỳ (mili giây)
stream.streaming.ping(interval_ms=30000)on_response của ping và của các method subscribe là callback nhận ack của server (không phải dữ liệu).
6.7. Dữ liệu chỉ số thời gian thực
Các method streaming cho dữ liệu chỉ số (VN30, VNINDEX, HNXINDEX...): tick của chính chỉ số, nến 1 phút/5 phút, số liệu tổng hợp, cờ phiên, và dữ liệu của các mã thành phần. Dùng cho dashboard/bot theo dõi chỉ số theo thời gian thực. Với dữ liệu lịch sử hoặc snapshot, dùng REST (mục 3).
6.7.1. Phân biệt các loại đăng ký
| Cần | Method | Topic | Message nhận |
|---|---|---|---|
| Tick của chính chỉ số | subscribe_index_trade (alias subscribe_index_ticks) | trade.index.<code> | IndexTickMessage |
| Nến 1m/5m của chỉ số | subscribe_index_trade(..., interval="1m"/"5m") | trade.index.<code>@1m / @5m | IntervalMessage |
| Số liệu tổng hợp của chỉ số | subscribe_index_summary | indexsummary.<code> | IndexSummaryMessage |
| Giao dịch/báo giá/room của các mã thành phần | subscribe_index (alias subscribe_index_constituents) | trade.<idx>@*, quote.<idx>@*, room.<idx>@* | TradeMessage, QuoteMessage, ForeignRoomMessage |
| Giao dịch/báo giá/room của cả sàn | subscribe_board (alias subscribe_exchange_trades) | trade/quote/room.<BOARD>@* | như trên |
| Cờ phiên | subscribe_market_flag (alias subscribe_session_flag) | market.flag | MarketFlagMessage |
Lưu ý:
subscribe_boardcó thể tạo hàng nghìn message mỗi giây. Chỉ dùng khi thật sự cần, và giữ callback rất ngắn.
Chưa xác nhận:
subscribe_indexvàsubscribe_boardgửi topic dạngtrade.<code>@*kèm quote và room. Ý nghĩa của@*(nhận dữ liệu của các mã thành phần/cả sàn) là cách SDK hiểu về routing của server, chưa được xác nhận trên server thực. Hãy kiểm tra bằnglist_subscription()và dữ liệu nhận được trên môi trường của bạn.
6.7.2. Method đăng ký
| Method | Tham số | Mô tả |
|---|---|---|
subscribe_index(indices, on_response=None) | indices: list[str] | Trade, quote, room của các mã thành phần của chỉ số |
subscribe_board(boards, on_response=None) | boards: list[Board] | Trade, quote, room của cả sàn |
subscribe_index_trade(indices, interval=None, on_response=None) | interval: None (tick), "1m" hoặc "5m" | Tick hoặc nến của chính chỉ số |
subscribe_index_summary(indices, on_response=None) | indices: list[str] | Số liệu tổng hợp của chỉ số |
subscribe_market_flag(on_response=None) | Cờ phiên giao dịch |
Mã chỉ số được tự động viết hoa (vn30 thành VN30) và UPCOMINDEX được đổi thành HNXUPCOMINDEX. Board có HOSE, HNX, UPCOM, DERIVATIVES.
6.7.3. Quy tắc interval
- Interval hợp lệ trên stream:
"tick","1m","5m"(hoặcTimeframe.MINUTE_1). Không có1w,1M. - Chỉ áp dụng cho topic
tradevàtrade.index. - Với
trade.index, interval cần một mã chỉ số cụ thể (không dùng*hoặc range). - Mỗi topic chỉ nhận một bộ lọc:
*, một mã, hoặc rangeA-B. Dấu phẩy bị từ chối; truyền list mã thay vào đó. - Ký hiệu
@*(STREAM_INTERVAL_ALL) dùng cho trade, quote, room, put, oddlot với một mã chỉ số hoặc mã sàn cụ thể.
6.7.4. Hủy đăng ký và kiểm tra
| Method | Mô tả |
|---|---|
unsubscribe_index(indices) | Hủy các mã thành phần của chỉ số |
unsubscribe_board(boards) | Hủy cả sàn (alias unsubscribe_exchange_trades) |
unsubscribe_index_trade(indices, interval=None) | Hủy tick/nến chỉ số. Phải dùng cùng interval đã subscribe (alias unsubscribe_index_ticks) |
unsubscribe_index_summary(indices) | Hủy số liệu tổng hợp |
unsubscribe_market_flag() | Hủy cờ phiên (alias unsubscribe_session_flag) |
unsubscribe_market(symbols) | Hủy topic thị trường |
unsubscribe_all() | Hỏi server rồi hủy đúng những subscription còn lại. Trả về {"data": [...], "trading": [...]} |
list_subscription(timeout_ms=5000) | Xem server đang giữ subscription nào. Trả về {"trading": [...], "data": [...]} |
list_subscription raise WebSocketError nếu chưa kết nối hoặc quá timeout_ms.
6.7.5. Callback và model
- Không có callback typed riêng cho
IndexTickMessage: tick của chỉ số chỉ đếnon_data. Hãy kiểm traisinstance(msg, IndexTickMessage). on_index_summarynhậnIndexSummaryMessage;on_market_flagnhậnMarketFlagMessage.- Cấu trúc các model: xem Models: Streaming Messages (
IndexTickMessage,IntervalMessage,IndexSummaryMessage,MarketFlagMessage).
Chưa xác nhận: Tên thuộc tính của
IndexTickMessageđược suy ra từ viết tắt của server, chưa được xác nhận trên frame thực tế. Khi cần chắc chắn, đọcmsg.raw. Các field trongIndexSummaryMessage.datachưa được tài liệu hóa công khai.
6.8. Ví dụ: theo dõi chỉ số
Sync
from collections import Counter
from ssi_sdk import Auth, Stream, Config
from ssi_sdk.models import IndexSummaryMessage, IndexTickMessage, MarketFlagMessage
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
counts: Counter = Counter()
def on_connection(event):
# state: connected | reconnected | disconnected | failed
err = f" | lỗi: {type(event.error).__name__}" if event.error else ""
print(f"[CONN] {event.state} (reconnect_count={event.reconnect_count}){err}")
def on_data(msg):
counts[type(msg).__name__] += 1
if isinstance(msg, IndexTickMessage): # không có callback typed riêng cho tick chỉ số
print(f"[TICK] {msg.symbol} value={msg.value} change={msg.change} change_percent={msg.change_percent}")
def on_index_summary(msg: IndexSummaryMessage):
if counts["IndexSummary"] == 0: # chỉ in lần đầu để xem cấu trúc
print(f"[SUMMARY] {msg.index} | số trường: {len(msg.data)}")
counts["IndexSummary"] += 1
def on_market_flag(msg: MarketFlagMessage):
print(f"[FLAG] {msg.board} | {msg.flag} | {msg.trading_time}")
with Auth(config) as auth:
auth.ensure_authenticated(otp="<OTP>")
with Stream(auth) as stream:
ws = stream.streaming
# Đăng ký callback TRƯỚC khi connect
ws.on_connection = on_connection
ws.on_data = on_data
ws.on_index_summary = on_index_summary
ws.on_market_flag = on_market_flag
ws.connect()
ws.subscribe_index_ticks(["VNINDEX"]) # tick của chính chỉ số
ws.subscribe_index_trade(["VN30"], interval="1m") # nến 1 phút của VN30
ws.subscribe_index_summary(["VN30"]) # số liệu tổng hợp
ws.subscribe_market_flag() # cờ phiên
ws.subscribe_index(["VN30"]) # trade/quote/room của mã thành phần
ws.wait(timeout_ms=30_000) # nghe 30 giây
subs = ws.list_subscription()
print(f"data={len(subs['data'])} topic, trading={len(subs['trading'])} topic")
ws.unsubscribe_index_ticks(["VNINDEX"])
ws.unsubscribe_index_trade(["VN30"], interval="1m") # cùng interval đã subscribe
removed = ws.unsubscribe_all() # hủy mọi thứ còn lại
print(f"unsubscribe_all: data={len(removed['data'])}, trading={len(removed['trading'])}")
print(dict(counts))Async
import asyncio
from ssi_sdk import AsyncAuth, AsyncStream, Config
from ssi_sdk.models import IndexTickMessage
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
async def on_data(msg): # callback có thể là coroutine
if isinstance(msg, IndexTickMessage):
print(f"[TICK] {msg.symbol} value={msg.value}")
async def main():
async with AsyncAuth(config) as auth:
await auth.ensure_authenticated(otp="<OTP>")
async with AsyncStream(auth) as stream:
ws = stream.streaming
ws.on_data = on_data
await ws.connect()
await ws.subscribe_index_ticks(["VNINDEX"])
await ws.subscribe_index_summary(["VN30"])
await ws.wait(timeout_ms=30_000)
subs = await ws.list_subscription()
print(f"data={len(subs['data'])} topic")
await ws.unsubscribe_all()
asyncio.run(main())6.9. Trạng thái kết nối và reconnect
| Thuộc tính | Mô tả |
|---|---|
is_connected | Đang kết nối hay không |
reconnect_count | Số lần đã reconnect |
last_error | Lỗi gần nhất |
on_connection nhận ConnectionEvent(state, reconnect_count=0, error=None) (dataclass bất biến, định nghĩa trong ssi_sdk.transport.reconnect, không export ở top level). state là một trong:
state | Ý nghĩa |
|---|---|
connected | Kết nối lần đầu thành công |
reconnected | Đã tự nối lại và subscribe lại |
disconnected | Mất kết nối (SDK thử nối lại nếu auto_reconnect bật) |
failed | Không thể phục hồi (ví dụ cần OTP mới). SDK không reconnect nữa |
Reconnect tự động khi Config.auto_reconnect bật (mặc định True):
- Backoff tăng dần có jitter, trần 60000 ms, cơ sở là
Config.retry_delay_ms(mặc định 2000 ms). - Mỗi lần reconnect dùng token mới; SDK chủ động đổi socket trước khi token hết hạn 60000 ms.
- Subscription được subscribe lại tự động.
- Gặp 429, backoff tối thiểu 30000 ms.
- Tối đa 10 socket mỗi client. Frame lớn hơn 4000 byte được tự chia nhỏ (giới hạn server là 4096).
6.10. Xử lý lỗi streaming
| Exception | Khi nào |
|---|---|
ValidationError | Topic/interval/bộ lọc không hợp lệ, danh sách rỗng. Chưa gửi gì |
WebSocketError | connect() lần đầu thất bại sau max_retries hoặc quá Config.timeout_ms; gửi khi chưa kết nối (Not connected. Call connect() first.); list_subscription quá thời gian |
AuthenticationError | Credential bị từ chối khi kết nối (kể cả ReauthenticationRequired) |
Lỗi xảy ra sau khi đã kết nối (mất mạng, token hết hạn) được báo qua on_connection (disconnected, reconnected, failed) chứ không raise ra luồng chính. Khi nhận failed, bạn cần xác thực lại (ví dụ OTP mới) và tạo kết nối mới. Xem Exceptions. Tham khảo thêm: Tutorial: Streaming nâng cao, Tutorial: WebSocket dữ liệu.
Message types nhận qua on_data
| Topic | Message Type | Mô tả |
|---|---|---|
trade.* | TradeMessage | Dữ liệu khớp lệnh |
trade.*.<timeframe> | IntervalMessage | Dữ liệu OHLCV theo interval |
trade.index.* | IndexTickMessage | Tick của chỉ số |
quote.* | QuoteMessage | Dữ liệu giá (bid/ask) |
room.* | ForeignRoomMessage | Room nước ngoài |
market.* | MarketStatusMessage | Trạng thái thị trường |
put.* | PutMessage | Thoả thuận |
oddlot.* | OddLotMessage | Lô lẻ |
Message types nhận qua on_trading
| Topic | Message Type | Mô tả |
|---|---|---|
order.* | OrderStatusMessage | Trạng thái lệnh thường |
FCOOrderUpdateMessage | Trạng thái lệnh điều kiện (khi event_type == "fcoEvent") | |
portfolio.* | PortfolioMessage | Thay đổi danh mục |
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 | |
wait(timeout_ms=None) | Chờ nhận dữ liệu | |
list_subscription(timeout_ms=5000) | Xem subscription server đang giữ | |
| Heartbeat | ping(on_response=None, interval_ms=None) | Ping server |
| Subscribe mã | subscribe_symbol(symbols) | Trade + Quote + Room |
subscribe_symbol_trade(symbols) | Chỉ trade | |
subscribe_symbol_quote(symbols) | Chỉ quote | |
subscribe_symbol_room(symbols) | Chỉ foreign room | |
subscribe_symbol_put_through(symbols) | Chỉ thoả thuận | |
subscribe_symbol_odd_lot(symbols) | Chỉ lô lẻ | |
subscribe_symbol_ohlcv(symbols, interval) | OHLCV theo timeframe | |
subscribe_market(symbols) | Topic thị trường | |
| Subscribe sàn/chỉ số | subscribe_board(boards) | Theo sàn |
subscribe_index(indices) | Mã thành phần của chỉ số | |
subscribe_index_trade(indices, interval=None) | Tick/nến của chính chỉ số | |
subscribe_index_summary(indices) | Số liệu tổng hợp của chỉ số | |
subscribe_market_flag() | Cờ phiên | |
| Subscribe giao dịch | subscribe_order_status(account_no="*") | Trạng thái lệnh |
subscribe_portfolio(account_no="*") | Portfolio changes | |
| Unsubscribe | unsubscribe_symbol(symbols) | Huỷ tất cả kênh cho mã |
unsubscribe_symbol_trade(symbols) | Huỷ trade | |
unsubscribe_symbol_quote(symbols) | Huỷ quote | |
unsubscribe_symbol_room(symbols) | Huỷ foreign room | |
unsubscribe_symbol_put_through(symbols) | Huỷ thoả thuận | |
unsubscribe_symbol_odd_lot(symbols) | Huỷ lô lẻ | |
unsubscribe_symbol_ohlcv(symbols, interval) | Huỷ OHLCV | |
unsubscribe_market(symbols) | Huỷ topic thị trường | |
unsubscribe_board(boards) | Huỷ sàn | |
unsubscribe_index(indices) | Huỷ chỉ số | |
unsubscribe_index_trade(indices, interval=None) | Huỷ tick/nến chỉ số | |
unsubscribe_index_summary(indices) | Huỷ số liệu tổng hợp | |
unsubscribe_market_flag() | Huỷ cờ phiên | |
unsubscribe_all() | Huỷ mọi subscription còn lại |
Ngoài tên chính, SDK còn có các alias: subscribe_trades, subscribe_candles, subscribe_quotes, subscribe_foreign_room, subscribe_put_through, subscribe_odd_lot, subscribe_exchange_trades, subscribe_index_constituents, subscribe_index_ticks, subscribe_master_data, subscribe_session_flag, subscribe_orders, subscribe_positions (và các alias unsubscribe_* tương ứng).
7. Trading WebSocket
Trading WebSocket cho phép gửi lệnh (đặt, sửa, hủy, batch, FCO) và truy vấn (vị thế, sức mua/bán, số dư, sổ lệnh) trên một kết nối WebSocket thay vì gọi từng request REST. Kết nối này dùng cùng token và private key với REST.
Khi nào dùng: bạn cần gửi nhiều lệnh/truy vấn liên tiếp với độ trễ thấp trên một kết nối, và muốn giữ một kết nối mở. Nếu bạn muốn mỗi lần gọi là một request đơn giản, hãy dùng trading.trading (REST, mục 5).
7.1. Điều kiện tiên quyết
- Đã xác thực với
Auth/AsyncAuth(cần OTP hoặc Smart OTP như khi đặt lệnh REST). Trading WS lấy token từauth.token_manager, không có client riêng nhưTrading. Xem Client Classes. Config.private_keyđã cấu hình: các lệnh thay đổi trạng thái (đặt, sửa, hủy, batch, FCO) được ký RSA-SHA256 và bị từ chối ngay tại SDK (raiseWebSocketError) nếu không có private key (fail closed). Truy vấn không cần ký.- Token có đủ scope (thiếu scope server trả 403):
| Nhóm | Scope |
|---|---|
| Lệnh | tradingws:order:post, tradingws:order:put, tradingws:order:delete |
| FCO | tradingws:fco:post, tradingws:fco:delete, tradingws:fco:get |
| Truy vấn | tradingws:position:get, tradingws:maxBuySell:get, tradingws:accountBalance:get, tradingws:ppmmrAccount:get, tradingws:orderBook:get |
7.2. Kết nối
URL lấy từ Config.trading_ws_domain, mặc định wss://api.ssi.com.vn/ws/v3/trading (xem Configuration). Chỉ chấp nhận wss:// (scheme khác gây WebSocketError). Bearer token được gửi trong header khi handshake.
| Hàm | Trả về | Mô tả |
|---|---|---|
trading_ws(auth, **options) | TradingWSService | Kết nối sync, dùng với Auth |
async_trading_ws(auth, **options) | AsyncTradingWSService | Kết nối async (await), dùng với AsyncAuth |
Import từ ssi_sdk:
from ssi_sdk import (
trading_ws, async_trading_ws,
TradingWSClient, AsyncTradingWSClient,
TradingWSResponse, TradingWSError,
)Nâng cấp từ 3.3.1 trở về trước: các tên này trước đây nằm trong
ssi_sdk.experimental. Module đó đã bị xoá ở 3.3.2 và không có alias, nênfrom ssi_sdk.experimental import ...giờ némModuleNotFoundError. Hãy đổi thànhfrom ssi_sdk import .... Tên, signature và giá trị mặc định không đổi.
Sau khi connect, server có thể chấp nhận socket (101) rồi mới từ chối bằng frame {"code": 401|403|429|500, "msg": ...}. SDK chờ handshake_grace_ms để bắt trường hợp này và raise TradingWSError ngay trong trading_ws(...).
Tùy chọn (options)
Mọi thời lượng tính bằng mili giây. Truyền dưới dạng keyword argument vào trading_ws(auth, ...).
| Option | Mặc định | Mô tả |
|---|---|---|
request_timeout_ms | Config.timeout_ms (mặc định 60000; 10000 nếu giá trị rỗng) khi tạo qua trading_ws; 10000 khi dùng trực tiếp TradingWSClient | Thời gian chờ phản hồi của một command |
signature_encoding | "auto" | "auto", "base64" hoặc "hex": kiểu mã hóa chữ ký |
handshake_grace_ms | 500 | Thời gian chờ frame từ chối sau khi socket mở |
respect_rate_limit | True | Tự chờ khi hết quota thay vì gửi tiếp |
max_rate_limit_wait_ms | 2000 | Chờ tối đa khi respect_rate_limit bật |
ping_interval_ms | 30000 | Chu kỳ ping; None để tắt |
user_agent | Config.user_agent | User-Agent gửi lúc handshake |
Lưu ý:
signature_encoding="auto"ký base64 trước. Nếu server trả 401006, 401008 hoặc 401013 (command chưa được thực thi), SDK gửi lại một lần với chữ ký hex. Nếu cả hai đều bị từ chối, message lỗi được bổ sung thông tin.
7.3. Các thao tác (service)
TradingWSService (sync) và AsyncTradingWSService (async) có cùng tên method; bản async dùng await. Method trả về model typed dùng chung với REST, hoặc TradingWSResponse cho command chưa có model riêng.
Lệnh
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
place_order | account_no, symbol, side, quantity, price, order_type, client_request_id=None | PlaceOrderResponse | Đặt lệnh |
amend_order | account_no, price=None, quantity=None, order_id=None, client_request_id=None | ModifyOrderResponse | Sửa lệnh |
cancel_order | account_no, order_id=None, client_request_id=None | CancelOrderResponse | Hủy lệnh |
batch_new_orders | orders | TradingWSResponse | Đặt nhiều lệnh (order.batchNew); result.results có clientRequestId, orderStatus, success cho từng lệnh. Item giống Batch Order REST; giới hạn theo Config.max_batch_orders |
batch_cancel_orders | orders | TradingWSResponse | Hủy nhiều lệnh (order.batchCancel) |
FCO
| Method | Tham số | Trả về | Mô tả |
|---|---|---|---|
place_fco | params (GTDParams, StopParams, TrailingStopParams, OCOParams hoặc BullBearParams) | FCOPlaceResponse | Đặt FCO |
cancel_fco | fco_id, code=None | FCOCancelResponse | Hủy FCO |
list_fco | account_no, fco_id, type, process_status, symbol, side, from_date, to_date, page_index, page_size | TradingWSResponse | Danh sách FCO. process_status là một giá trị đơn, không phải list |
fco_order_book | fco_id | TradingWSResponse | Sổ lệnh của FCO (fco.orderbook) |
fco_status_history | fco_id | TradingWSResponse | Lịch sử trạng thái FCO (fco.statusHistory) |
Truy vấn
| Method | Tham số | Mô tả |
|---|---|---|
query_position | account_no, query_summary=True | Vị thế (query.position) |
query_max_buy_sell | account_no, symbol, price=None | Sức mua/bán tối đa (query.maxBuySell) |
query_account_balance | account_no | Số dư (query.accountBalance) |
query_ppmmr_account | account_no | Thông tin PPMMR (query.ppmmrAccount) |
query_order_book | account_no, from_date, to_date, symbol, order_status, page_index, page_size | Sổ lệnh (query.orderBook); ngày định dạng YYYY/MM/DD |
Trả về: TradingWSResponse (có status và result). Xem Models: Trading WebSocket.
Sự kiện (subscription)
| Method | Mô tả |
|---|---|
subscribe_order_events(account_no) / unsubscribe_order_events(account_no) | Sự kiện lệnh của một tài khoản |
subscribe_portfolio_events(account_no) / unsubscribe_portfolio_events(account_no) | Sự kiện danh mục của một tài khoản |
unsubscribe_all_events() | Hủy mọi subscription trading |
Wildcard hoặc tài khoản rỗng bị từ chối bằng ValidationError (không gửi gì lên server).
Khác
| Method | Mô tả |
|---|---|
command(method, params) | Gửi command tùy ý theo tên method của server |
close() | Đóng socket, dừng vòng ping và reader. Bản async là coroutine |
Tham số price nhận Decimal, int, float hoặc str. Nên dùng Decimal hoặc str để tránh sai số dấu phẩy động.
7.4. Nhận sự kiện qua on_event
Gắn hook trên service.client. Hook nhận mọi frame không có command id: ack subscription và sự kiện dạng {"channel", "topic", "data"} với data.eventType là orderEvent, orderMatchEvent hoặc clientPortfolioEvent.
- Bản sync: hook chạy trên thread đọc của SDK.
- Bản async: hook chạy trong task của SDK và có thể là coroutine.
- Ngoại lệ trong hook được ghi log, không làm đứt kết nối.
- Giữ hook ngắn. Không in nguyên frame:
topicchứa số tài khoản.
7.5. Rate limit
service.client.rate_limit là dict gồm limit, remaining, resetMs, cập nhật theo phản hồi của server. Theo sample chính thức, quota là 50 command mỗi cửa sổ cho lệnh và 100 cho truy vấn.
Khi remaining == 0 và respect_rate_limit=True, command kế tiếp chờ min(resetMs, max_rate_limit_wait_ms) rồi mới gửi. Nếu vẫn hết quota, server trả 429 và SDK raise TradingWSError.
7.6. Mất kết nối: KHÔNG tự reconnect
Khác với Stream client, Trading WS không tự reconnect và không tự subscribe lại. Khi socket đứt:
client.is_connectedtrở thànhFalse;client.rejectiongiữ lỗiTradingWSErrordo frame từ chối của server (nếu có).- Command đang chờ nhận
WebSocketError("connection closed before the response arrived"); command gửi sau đó nhậnWebSocketError("not connected"). - Bạn phải tự gọi lại
trading_ws(auth), rồi subscribe lại các sự kiện.
Một command hết thời gian chờ không bao giờ được SDK gửi lại, vì lệnh có thể đã tới sàn. Hãy đối soát qua sự kiện order.<account> hoặc query_order_book trước khi gửi lại, và giữ nguyên client_request_id để tránh đặt trùng.
7.7. Idempotency
client_request_idtối đa 20 ký tự. Nếu bỏ trống SDK tự sinh.- Dùng lại một
client_request_idđã dùng: server trả 409. - Ack đặt lệnh thường có
status=PD(đang chờ xử lý). Kết quả thật đến qua sự kiện lệnh, không phải qua ack.
7.8. Ví dụ
Sync: subscribe, đặt lệnh, hủy lệnh, truy vấn
Chỉ dùng placeholder. Ví dụ gửi lệnh thật, hãy đặt giá xa thị trường và kiểm tra kỹ trước khi chạy.
import uuid
from decimal import Decimal
from ssi_sdk import Auth, Config, TradingWSError, trading_ws
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import ValidationError, WebSocketError
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
ACCOUNT_NO = "<ACCOUNT_NO>"
def on_event(frame: dict):
# Chỉ giữ loại sự kiện và trạng thái, không giữ topic (có số tài khoản)
data = frame.get("data") if isinstance(frame.get("data"), dict) else {}
print(f"[EVENT] {data.get('eventType')} | status={data.get('orderStatus')}")
with Auth(config) as auth:
auth.ensure_authenticated(otp="<OTP>")
try:
ws = trading_ws(auth, respect_rate_limit=True, max_rate_limit_wait_ms=2000)
except (TradingWSError, WebSocketError) as e:
raise SystemExit(f"Không kết nối được trading WS: {type(e).__name__}: {e}")
try:
ws.client.on_event = on_event
ws.subscribe_order_events(ACCOUNT_NO)
try:
ack = ws.place_order(
account_no=ACCOUNT_NO,
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
price=Decimal("<GIA_SAN>"), # lệnh LO xa thị trường
order_type=OrderType.LO,
client_request_id=f"ws{uuid.uuid4().hex[:12]}", # <= 20 ký tự
)
print(f"Ack: orderId={ack.order_id} status={ack.status}") # thường là PD
cancel = ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
print(f"Ack hủy: status={cancel.status}")
except ValidationError as e:
print(f"Dữ liệu không hợp lệ (chưa gửi gì): {e}")
except TradingWSError as e:
print(f"Server từ chối: status={e.status} code={e.error_code} msg={e.server_message}")
book = ws.query_order_book(ACCOUNT_NO, symbol="SSI", page_size=10)
print(f"query_order_book: status={book.status}")
print("rate_limit:", ws.client.rate_limit)
ws.unsubscribe_all_events()
finally:
ws.close() # luôn đóng socketAsync
import asyncio
import uuid
from decimal import Decimal
from ssi_sdk import AsyncAuth, Config, TradingWSError, async_trading_ws
from ssi_sdk.enums import OrderSide, OrderType
config = Config(
client_id="<CLIENT_ID>",
api_key="<API_KEY>",
api_secret="<API_SECRET>",
private_key="<PRIVATE_KEY_CONTENT>",
)
ACCOUNT_NO = "<ACCOUNT_NO>"
async def on_event(frame: dict): # có thể là hàm thường hoặc coroutine
data = frame.get("data") if isinstance(frame.get("data"), dict) else {}
print(f"[EVENT] {data.get('eventType')} | status={data.get('orderStatus')}")
async def main():
async with AsyncAuth(config) as auth:
await auth.ensure_authenticated(otp="<OTP>")
ws = await async_trading_ws(auth)
try:
ws.client.on_event = on_event
await ws.subscribe_order_events(ACCOUNT_NO)
try:
ack = await ws.place_order(
account_no=ACCOUNT_NO,
symbol="SSI",
side=OrderSide.BUY,
quantity=100,
price=Decimal("<GIA_SAN>"),
order_type=OrderType.LO,
client_request_id=f"ws{uuid.uuid4().hex[:12]}",
)
await ws.cancel_order(account_no=ACCOUNT_NO, order_id=ack.order_id)
except TradingWSError as e:
print(f"Server từ chối: status={e.status} code={e.error_code}")
await ws.unsubscribe_all_events()
finally:
await ws.close()
asyncio.run(main())7.9. Xử lý lỗi
TradingWSError(status, code, message) kế thừa WebSocketError (và SSIError). Thuộc tính: .status, .error_code, .server_message. Exception không chứa params của command nên an toàn để log, nhưng vẫn không nên log kèm số tài khoản. Xem Exceptions.
| Exception | Khi nào | Command đã gửi? |
|---|---|---|
ValidationError | Tham số sai (thiếu field, wildcard khi subscribe...) | Chưa gửi gì |
TradingWSError | Server từ chối: status 401 (token hết hạn), 403 (thiếu scope), 409 (trùng client_request_id), 429 (hết quota), 5xx | Đã tới server, đã bị từ chối |
WebSocketError | Thiếu private key cho command cần ký (chưa gửi gì), chưa kết nối, URL không phải wss://, mất kết nối, hoặc hết thời gian chờ (timed out waiting for a response (not retried)) | Tùy trường hợp. Với mất kết nối và hết thời gian chờ thì không rõ: đối soát trước khi gửi lại |
Frame lỗi của server có dạng {"id", "status": 4xx | "error", "error": {"code", "message"}}. Các command fco.* trả status: "error" với code FCO_ERROR.
7.10. Quan hệ với Stream client
| Trading WebSocket | Stream (stream.streaming) | |
|---|---|---|
| Import | from ssi_sdk import trading_ws | from ssi_sdk import Stream |
| URL | Config.trading_ws_domain (/ws/v3/trading) | wss://stream.ssi.com.vn/ws/v3 (mặc định) |
| Mục đích | Gửi lệnh/truy vấn, nhận sự kiện lệnh | Nhận dữ liệu thị trường và sự kiện lệnh |
| Tự reconnect | Không | Có (Config.auto_reconnect), tự subscribe lại |
Hai kết nối độc lập. Có thể dùng Stream để nhận dữ liệu thị trường và Trading WS để gửi lệnh với cùng một auth. Hướng dẫn từng bước xem Tutorial: Trading WebSocket.