14. Đặt/hủy lệnh hàng loạt (Batch)

Đặt và hủy nhiều lệnh trong một request, xử lý kết quả từng lệnh

Mục tiêu

Đặt và hủy nhiều lệnh trong một request đã ký RSA (POST /order/batch và DELETE /order/batch), rồi đọc kết quả của từng lệnh. Tương ứng Sample 14 trong repo tutorials (SDK v3.3.2). Cần OTP khi authenticate.

Cảnh báo: sample này gửi lệnh thật lên tài khoản đã cấu hình. Để an toàn, mọi lệnh là LO mua tại giá sàn (xa thị trường, chỉ nằm chờ) với khối lượng nhỏ, và script yêu cầu gõ YES trước khi gửi. Vẫn nên kiểm tra kỹ tài khoản, mã, giá, khối lượng trước khi chạy, và thử trên môi trường UAT trước.

Luồng xử lý

Market Data (giá sàn) → Dựng batch → Trading API (place batch) → Kết quả từng lệnh → Trading API (cancel batch)
  1. Lấy giá sàn từ master data, dựng danh sách BatchPlaceOrderItem
  2. Kiểm tra số lệnh không vượt Config.max_batch_orders (mặc định 20)
  3. Gọi place_batch_orders(items) và xử lý kết quả từng lệnh (success, error_code)
  4. Gọi cancel_batch_orders(items) cho các lệnh vừa đặt thành công (BatchCancelOrderItem)

Các mẫu trong repo

Ngôn ngữFile
Pythonpython/sample_14_batch_orders.py, python/sample_14_batch_orders_async.py
Node.jsnode/sample_14_batch_orders.js
.NETdotnet/Sample14BatchOrders.cs
Gogo/sample_14_batch_orders.go

Trang này minh họa bằng Python. Repo: ssi-fastconnect-v3-tutorials.

Chuẩn bị

  • Cấu hình credentials và số tài khoản (equity_account) trong config.json, xem Tutorial 1. Sample repo đọc cấu hình qua config.py; ở đây dùng placeholder để code gọn.
  • auth_helper.py (token cache, Tutorial 1) và OTP cho lần đăng nhập đầu.
  • helpers.py trong repo, gồm confirm_real_orders (cảnh báo và yêu cầu gõ YES), mask_account (che số tài khoản khi in), require_account (dừng nếu thiếu tài khoản) và enum_value (lấy chuỗi hiển thị an toàn cho trường enum).

Sample Code — Sync

Code dưới đây rút gọn từ sample trong repo, giữ nguyên luồng.

python/sample_14_batch_orders.py
import uuid
from decimal import Decimal

from ssi_sdk import Auth, Data, Trading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
from ssi_sdk.models import BatchCancelOrderItem, BatchPlaceOrderItem
from auth_helper import ensure_auth
from helpers import confirm_real_orders, enum_value, mask_account, require_account

config = Config(
    client_id="<your_client_id>",
    api_key="<your_api_key>",
    api_secret="<your_api_secret>",
    private_key="<your_private_key>",
)
ACCOUNT_NO = "<your_account_no>"
SYMBOL = "SSI"
QUANTITY = 100
NUM_ORDERS = 3

require_account(ACCOUNT_NO)
confirm_real_orders(f"{NUM_ORDERS} lệnh LO mua {SYMBOL} (batch)")

with Auth(config) as auth:
    ensure_auth(auth, otp="<your_otp>")

    # Bước 1: Lấy giá sàn làm giá đặt (lệnh nằm chờ, xa thị trường)
    with Data(auth) as data:
        master = next((m for m in data.market_data.get_master_data() if m.symbol == SYMBOL), None)
    if master is None or master.floor <= 0:
        raise SystemExit(f"Không có giá sàn của {SYMBOL}, dừng để an toàn.")
    floor_price = Decimal(str(master.floor))

    with Trading(auth) as trading:
        # Bước 2: Dựng batch, mỗi lệnh có client_request_id riêng
        batch_tag = uuid.uuid4().hex[:8]
        place_items: list[BatchPlaceOrderItem] = [
            {
                "account_no": ACCOUNT_NO,
                "symbol": SYMBOL,
                "side": OrderSide.BUY,
                "quantity": QUANTITY,
                "price": floor_price,
                "order_type": OrderType.LO,
                "client_request_id": f"bt{batch_tag}{i:02d}",  # <= 20 ký tự
            }
            for i in range(1, NUM_ORDERS + 1)
        ]
        if len(place_items) > config.max_batch_orders:
            raise SystemExit(f"Tối đa {config.max_batch_orders} lệnh / batch (Config.max_batch_orders).")

        # Bước 3: Đặt batch
        print(f"place_batch_orders: {len(place_items)} lệnh, tài khoản {mask_account(ACCOUNT_NO)}")
        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 as e:
            raise SystemExit(f"Batch id đã dùng trong ngày (409), kiểm tra sổ lệnh trước khi gửi lại: {e}")
        except APIError as e:
            raise SystemExit(f"Server từ chối batch: code={e.code} status={e.status_code} msg={e.message}")

        print(f"Batch ID: {response.batch_request_id}")
        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={enum_value(r.status)}")
            else:
                print(f"  FAIL {r.client_request_id} -> code={r.error_code} msg={r.error_message}")

        # Bước 4: Hủy batch các lệnh vừa đặt
        if not placed_ok:
            print("Không có lệnh nào được đặt thành công, bỏ qua bước hủy.")
        else:
            # Mỗi item chỉ được 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
            ]
            print(f"cancel_batch_orders: {len(cancel_items)} lệnh")
            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} "
                    f"status={enum_value(r.status)} {r.error_code} {r.error_message}".rstrip()
                )

Sample Code — Async

Async dùng cùng luồng; chỉ khác AsyncAuth / AsyncData / AsyncTrading và await.

python/sample_14_batch_orders_async.py
import asyncio
import uuid
from decimal import Decimal

from ssi_sdk import AsyncAuth, AsyncData, AsyncTrading, Config
from ssi_sdk.enums import OrderSide, OrderType
from ssi_sdk.exceptions import APIError, DuplicateRequestError, ValidationError
from auth_helper import ensure_auth_async
from helpers import confirm_real_orders, enum_value, require_account

config = Config(...)
ACCOUNT_NO = "<your_account_no>"
SYMBOL = "SSI"

async def main():
    require_account(ACCOUNT_NO)
    confirm_real_orders(f"3 lệnh LO mua {SYMBOL} (batch)")

    async with AsyncAuth(config) as auth:
        await ensure_auth_async(auth, otp="<your_otp>")

        async with AsyncData(auth) as data:
            master_data = await data.market_data.get_master_data()
        master = next((m for m in master_data if m.symbol == SYMBOL), None)
        if master is None or master.floor <= 0:
            raise SystemExit(f"Không có giá sàn của {SYMBOL}, dừng để an toàn.")
        floor_price = Decimal(str(master.floor))

        async with AsyncTrading(auth) as trading:
            batch_tag = uuid.uuid4().hex[:8]
            place_items = [
                {
                    "account_no": ACCOUNT_NO, "symbol": SYMBOL, "side": OrderSide.BUY,
                    "quantity": 100, "price": floor_price, "order_type": OrderType.LO,
                    "client_request_id": f"bt{batch_tag}{i:02d}",  # <= 20 ký tự
                }
                for i in range(1, 4)
            ]
            try:
                response = await trading.trading.place_batch_orders(place_items)
            except (ValidationError, DuplicateRequestError, APIError) as e:
                raise SystemExit(f"Batch thất bại: {type(e).__name__}: {e}")

            placed_ok = [r.order_id for r in response.results if r.success]
            for r in response.results:
                print(f"  {'OK' if r.success else 'FAIL'} {r.client_request_id} {enum_value(r.status)}")

            if placed_ok:
                cancel_items = [{"account_no": ACCOUNT_NO, "order_id": oid} for oid in placed_ok]
                cancel_resp = await trading.trading.cancel_batch_orders(cancel_items)
                for r in cancel_resp.results:
                    print(f"  {'OK' if r.success else 'FAIL'} orderId={r.order_id}")

asyncio.run(main())

Kết quả từng lệnh

TrườngÝ nghĩa
response.batch_request_idID của cả batch
r.successTrue nếu lệnh này được chấp nhận
r.order_id, r.statusMã lệnh và trạng thái ban đầu khi success
r.error_code, r.error_messageLý do lỗi khi lệnh này bị từ chối
r.client_request_idKhóa bạn tự đặt, dùng để đối chiếu lệnh trong request với kết quả
r.client_cancel_idKhóa của yêu cầu hủy (kết quả cancel_batch_orders)

Lưu ý

  • Validate cả batch trước: một lệnh sai sẽ làm cả batch bị từ chối (ValidationError hoặc APIError). Chỉ khi batch được nhận thì mới có kết quả riêng cho từng lệnh, nên luôn duyệt response.results thay vì chỉ kiểm tra "không có exception".
  • Idempotency: client_request_id (tối đa 20 ký tự) là khóa idempotency của từng lệnh. Tự đặt để còn tra cứu và hủy sau này.
  • Gửi lại batch với id đã dùng trong ngày trả về DuplicateRequestError (HTTP 409). SDK không tự retry request POST/DELETE; lệnh gốc có thể đã nằm trên sàn, hãy kiểm tra sổ lệnh trước khi gửi lại.
  • Hủy: mỗi BatchCancelOrderItem chỉ được có đúng một trong order_id hoặc client_request_id.
  • Giới hạn số lệnh: sample kiểm tra theo Config.max_batch_orders (mặc định 20) trước khi gọi. Quy định phía API (số lệnh tối đa mỗi request, chống replay...) xem trade-order-batch-place và trade-order-batch-cancel.
  • Theo dõi trạng thái các lệnh vừa đặt bằng Trạng thái lệnh hoặc WebSocket Trading.
  • Cách phân loại lỗi và retry an toàn: Xử lý lỗi & Rate limit. Kiểm tra sức mua và vị thế trước khi đặt: Vị thế, PPMMR & Max mua/bán.

Trên trang này