Python

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ề None khi 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:

MethodTimeframe
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):

MethodTimeframe
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ểuMặc địnhMô tả
symbolstr(bắt buộc)Mã chứng khoán
from_datestr(bắt buộc)Ngày bắt đầu, YYYY/MM/DD hoặc YYYY/MM/DD HH:MM:SS
to_datestr(bắt buộc)Ngày kết thúc, cùng định dạng, from_date <= to_date
pageint1Trang (>= 1)
sizeint1000Số bản ghi/trang (>= 1)

Trả về: list[OHLCData]

Lưu ý: get_ohlc_1week_historical và get_ohlc_1month_historical luôn raise ValidationError vì server không hỗ trợ 1w / 1M (lỗi 400210). Timeframe hợp lệ: 1d, 1m, 3m, 5m, 15m, 1h. download_ohlc_1minute và download_ohlc_1day raise NotImplementedError. 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:

MethodMô 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.DERIVATIVES chỉ 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.

MethodTham sốMô tả
get_corporate_actionssymbol: 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_historicalfrom_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_dateYYYY/MM/DD
symbolMộ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_date và to_date phải đúng định dạng YYYY/MM/DD.
  • from_date không được sau to_date.
  • to_date tối đa 5 ngày sau from_date.
  • symbol (nếu có) phải là một mã gồm chữ và số, tối đa 20 ký tự.
  • language phải là vn hoặc en (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_positions và get_derivative_positions trả về None (type hint ghi list). Hãy kiểm tra None trước khi duyệt. Trong 3.3.2, get_open_derivative_positions và get_closed_derivative_positions đọc trực tiếp từ kết quả đó nên có thể raise AttributeError khi không có vị thế; nếu cần an toàn, gọi get_derivative_positions và kiểm tra None. get_equity_balance, get_derivative_balance, get_equity_ppmmr, get_derivative_ppmmr cũ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ómMethodTrả 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ệnhget_today_orders(account_no)list[Order]
get_historical_orders(account_no, from_date, to_date)list[Order]
PPMMRget_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.

MethodTham sốTrả vềMô tả
place_batch_orders(orders)orders: Sequence[BatchPlaceOrderItem]BatchOrderResponsePOST /order/batch: đặt nhiều lệnh
cancel_batch_orders(orders)orders: Sequence[BatchCancelOrderItem]BatchOrderResponseDELETE /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_orders lệnh. Mặc định là 20.
  • Vượt giới hạn hoặc batch rỗng: SDK raise ValidationError ngay, chưa gửi gì.
  • max_batch_orders khô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_orders do bên gọi quyết định; hãy theo giới hạn áp dụng cho API Key của bạn.

Item đặt lệnh (BatchPlaceOrderItem)

TypedDict, dùng key snake_case:

KeyBắt buộcMô tả
account_noCóSố tài khoản
symbolCóMã chứng khoán
sideCóOrderSide.BUY / OrderSide.SELL
quantityCóKhối lượng (số nguyên dương)
priceCóGiá. Có thể là None với loại lệnh không cần giá (ATO, ATC, MP)
order_typeCóOrderType (LO, ATO, ATC, MP...)
client_request_idKhôngKhó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)

KeyBắt buộcMô tả
account_noCóSố tài khoản
order_idMột trong haiMã lệnh do server cấp
client_request_idMột trong haiKhó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": [...]}. batchRequestId là id ngẫu nhiên, batchRequestTime là 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ó batchRequestTime nằ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_id củ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 / DELETE khi 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 (ValidationError phía SDK, hoặc APIError phí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 đọc success của từng BatchOrderResult.

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

ExceptionKhi nàoĐã gửi lên server?
ValidationErrorBatch 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_idChư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
APIErrorServer 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
RateLimitErrorServer trả HTTP 429 (có retry_after_ms, limit, remaining)Đã tới server, bị từ chối
AuthenticationErrorServer trả HTTP 401/403 (token không hợp lệ hoặc không đủ quyền), hoặc chưa xác thựcBị 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ómMethodTrả về
Đặt lệnhplace_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ệnhmodify_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ệnhcancel_order(account_no, client_request_id)CancelOrderResponse
cancel_order_by_order_id(account_no, order_id)CancelOrderResponse
Batchplace_batch_orders(orders)BatchOrderResponse
cancel_batch_orders(orders)BatchOrderResponse
Sức mua/bánget_max_buy_sell(account_no, symbol, price)MaxBuySellResponse
get_max_buy_sell_at_market_price(account_no, symbol)MaxBuySellResponse
Lệnh điều kiệnplace_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_index và page_size để phân trang, và đều gọi được bất đồng bộ với await. FCOInfo có thêm price_slip và order_type; FCOOrder có thêm type.


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ínhNhậnGhi chú
on_dataMọi message dữ liệu thị trườngGồm cả IndexTickMessage, IntervalMessage
on_tradingMọi message kênh trading
on_heartbeatHeartbeat
on_market_flagMarketFlagMessage
on_index_summaryIndexSummaryMessage
on_order, on_order_match, on_portfolioMessage trading theo loạiCần OTP
on_connectionConnectionEventTrạ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ầnMethodTopicMessage 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 / @5mIntervalMessage
Số liệu tổng hợp của chỉ sốsubscribe_index_summaryindexsummary.<code>IndexSummaryMessage
Giao dịch/báo giá/room của các mã thành phầnsubscribe_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ànsubscribe_board (alias subscribe_exchange_trades)trade/quote/room.<BOARD>@*như trên
Cờ phiênsubscribe_market_flag (alias subscribe_session_flag)market.flagMarketFlagMessage

Lưu ý: subscribe_board có 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_index và subscribe_board gửi topic dạng trade.<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ằng list_subscription() và dữ liệu nhận được trên môi trường của bạn.

6.7.2. Method đăng ký

MethodTham 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ặc Timeframe.MINUTE_1). Không có 1w, 1M.
  • Chỉ áp dụng cho topic trade và 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 range A-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

MethodMô 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ỉ đến on_data. Hãy kiểm tra isinstance(msg, IndexTickMessage).
  • on_index_summary nhận IndexSummaryMessage; on_market_flag nhận MarketFlagMessage.
  • 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, đọc msg.raw. Các field trong IndexSummaryMessage.data chư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ínhMô tả
is_connectedĐang kết nối hay không
reconnect_countSố lần đã reconnect
last_errorLỗ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
connectedKết nối lần đầu thành công
reconnectedĐã tự nối lại và subscribe lại
disconnectedMất kết nối (SDK thử nối lại nếu auto_reconnect bật)
failedKhô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

ExceptionKhi nào
ValidationErrorTopic/interval/bộ lọc không hợp lệ, danh sách rỗng. Chưa gửi gì
WebSocketErrorconnect() 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
AuthenticationErrorCredential 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

TopicMessage TypeMô tả
trade.*TradeMessageDữ liệu khớp lệnh
trade.*.<timeframe>IntervalMessageDữ liệu OHLCV theo interval
trade.index.*IndexTickMessageTick của chỉ số
quote.*QuoteMessageDữ liệu giá (bid/ask)
room.*ForeignRoomMessageRoom nước ngoài
market.*MarketStatusMessageTrạng thái thị trường
put.*PutMessageThoả thuận
oddlot.*OddLotMessageLô lẻ

Message types nhận qua on_trading

TopicMessage TypeMô tả
order.*OrderStatusMessageTrạng thái lệnh thường
FCOOrderUpdateMessageTrạng thái lệnh điều kiện (khi event_type == "fcoEvent")
portfolio.*PortfolioMessageThay đổi danh mục

Tổng quan method Streaming

NhómMethodMô tả
Kết nốiconnect()Kết nối WebSocket
disconnect()Ngắt kết nối
wait(timeout_ms=None)Chờ nhận dữ liệu
list_subscription(timeout_ms=5000)Xem subscription server đang giữ
Heartbeatping(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ịchsubscribe_order_status(account_no="*")Trạng thái lệnh
subscribe_portfolio(account_no="*")Portfolio changes
Unsubscribeunsubscribe_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 (raise WebSocketError) 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ómScope
Lệnhtradingws:order:post, tradingws:order:put, tradingws:order:delete
FCOtradingws:fco:post, tradingws:fco:delete, tradingws:fco:get
Truy vấntradingws: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àmTrả vềMô tả
trading_ws(auth, **options)TradingWSServiceKết nối sync, dùng với Auth
async_trading_ws(auth, **options)AsyncTradingWSServiceKế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ên from ssi_sdk.experimental import ... giờ ném ModuleNotFoundError. Hãy đổi thành from 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, ...).

OptionMặc địnhMô tả
request_timeout_msConfig.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 TradingWSClientThờ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_ms500Thời gian chờ frame từ chối sau khi socket mở
respect_rate_limitTrueTự chờ khi hết quota thay vì gửi tiếp
max_rate_limit_wait_ms2000Chờ tối đa khi respect_rate_limit bật
ping_interval_ms30000Chu kỳ ping; None để tắt
user_agentConfig.user_agentUser-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

MethodTham sốTrả vềMô tả
place_orderaccount_no, symbol, side, quantity, price, order_type, client_request_id=NonePlaceOrderResponseĐặt lệnh
amend_orderaccount_no, price=None, quantity=None, order_id=None, client_request_id=NoneModifyOrderResponseSửa lệnh
cancel_orderaccount_no, order_id=None, client_request_id=NoneCancelOrderResponseHủy lệnh
batch_new_ordersordersTradingWSResponseĐặ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_ordersordersTradingWSResponseHủy nhiều lệnh (order.batchCancel)

FCO

MethodTham sốTrả vềMô tả
place_fcoparams (GTDParams, StopParams, TrailingStopParams, OCOParams hoặc BullBearParams)FCOPlaceResponseĐặt FCO
cancel_fcofco_id, code=NoneFCOCancelResponseHủy FCO
list_fcoaccount_no, fco_id, type, process_status, symbol, side, from_date, to_date, page_index, page_sizeTradingWSResponseDanh sách FCO. process_status là một giá trị đơn, không phải list
fco_order_bookfco_idTradingWSResponseSổ lệnh của FCO (fco.orderbook)
fco_status_historyfco_idTradingWSResponseLịch sử trạng thái FCO (fco.statusHistory)

Truy vấn

MethodTham sốMô tả
query_positionaccount_no, query_summary=TrueVị thế (query.position)
query_max_buy_sellaccount_no, symbol, price=NoneSức mua/bán tối đa (query.maxBuySell)
query_account_balanceaccount_noSố dư (query.accountBalance)
query_ppmmr_accountaccount_noThông tin PPMMR (query.ppmmrAccount)
query_order_bookaccount_no, from_date, to_date, symbol, order_status, page_index, page_sizeSổ 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)

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

MethodMô 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: topic chứ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_connected trở thành False; client.rejection giữ lỗi TradingWSError do 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ận WebSocketError("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_id tố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 socket

Async

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.

ExceptionKhi nàoCommand đã gửi?
ValidationErrorTham số sai (thiếu field, wildcard khi subscribe...)Chưa gửi gì
TradingWSErrorServer 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
WebSocketErrorThiế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 WebSocketStream (stream.streaming)
Importfrom ssi_sdk import trading_wsfrom ssi_sdk import Stream
URLConfig.trading_ws_domain (/ws/v3/trading)wss://stream.ssi.com.vn/ws/v3 (mặc định)
Mục đíchGửi lệnh/truy vấn, nhận sự kiện lệnhNhận dữ liệu thị trường và sự kiện lệnh
Tự reconnectKhôngCó (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.

Trên trang này

1. Xác thực (TokenManager)Xác thực với OTPYêu cầu gửi OTPLàm mới tokenTự động đảm bảo xác thực2. Tài khoản (AccountService)Lấy danh sách tài khoản3. Dữ liệu thị trường (MarketDataService)3.1. Dữ liệu OHLC (nến)3.2. Danh sách chỉ số thị trường3.3. Tổng hợp chỉ số (Index Summary)3.4. Thông tin chứng khoán3.5. Tổng hợp chứng khoán (Securities Summary)3.6. Master data (giá trần, sàn, tham chiếu)3.7. Corporate actions (sự kiện quyền)4. Danh mục đầu tư (PortfolioService)4.1. Số dư tài khoản4.2. Vị thế (Positions)4.3. Sổ lệnh (Order Book)4.4. PPMMRTổng quan method Portfolio5. Giao dịch (TradingService)5.1. Đặt lệnh5.2. Sửa lệnh5.3. Huỷ lệnh5.4. Batch OrderGiới hạn số lệnhItem đặt lệnh (BatchPlaceOrderItem)Item hủy lệnh (BatchCancelOrderItem)Kết quảKý và idempotencyVí dụ sync: đặt batch rồi hủy batchVí dụ asyncXử lý lỗi batch5.5. Sức mua/bán tối đaTổng quan method Trading5.6. Giao dịch điều kiện (FCO)6. Streaming realtime (StreamingService)6.1. Kết nối6.2. Thiết lập callback6.3. Subscribe dữ liệu thị trường6.4. Unsubscribe6.5. Subscribe giao dịch6.6. Heartbeat6.7. Dữ liệu chỉ số thời gian thực6.7.1. Phân biệt các loại đăng ký6.7.2. Method đăng ký6.7.3. Quy tắc interval6.7.4. Hủy đăng ký và kiểm tra6.7.5. Callback và model6.8. Ví dụ: theo dõi chỉ sốSyncAsync6.9. Trạng thái kết nối và reconnect6.10. Xử lý lỗi streamingMessage types nhận qua on_dataMessage types nhận qua on_tradingTổng quan method Streaming7. Trading WebSocket7.1. Điều kiện tiên quyết7.2. Kết nốiTùy chọn (options)7.3. Các thao tác (service)LệnhFCOTruy vấnSự kiện (subscription)Khác7.4. Nhận sự kiện qua on_event7.5. Rate limit7.6. Mất kết nối: KHÔNG tự reconnect7.7. Idempotency7.8. Ví dụSync: subscribe, đặt lệnh, hủy lệnh, truy vấnAsync7.9. Xử lý lỗi7.10. Quan hệ với Stream client