XtQuant / MiniQMT Broker Adapter Contract (v1)
Audience: Contributors implementing
qteasy-xtquantor reviewing MiniQMT integration against qteasy 2.5.2+.
Language: English (normative for collaboration).
Related: :doc:4-broker-adapter-and-integration,qteasy.trade_io,qteasy.broker.
This document is the v1 contract for wiring XtQuant / MiniQMT into qteasy’s live-trading stack. It does not replace the general Broker adapter guide; it adds QMT-specific rules on top of it.
0. Scope and non-goals
In scope (v1)
live_trade_broker_type='xtquant'(configuration whitelist; factory registration via extension package)Real broker order IDs from QMT at accept time and in fill reports
submit_with_ack+poll_fillsas the primary Trader pathtransaction()as an internal generator yielding 4- or 5-tuples (QMT adapters must yield 5-tuples with real IDs)
Out of scope (v1)
Merging fork
trader.pyblocks (pre-check locks, DIAG spam, manualresult_queuedrain, MySQL-only data policy)Hard
import brokersorfrom qteasy_xtquantinsideqteasy/broker.pyPrice channel
live_price_acquire_channel='xtquant'(see separate PR forxtfuncs/data_channels)
1. Configuration and registration
1.1 Configuration
build_live_trade_config(..., live_trade_broker_type='xtquant') and qt.configure(live_trade_broker_type='xtquant') are allowed as of qteasy 2.5.2 (PR-0b).
This only validates the string; it does not install XtQuant or register the broker class.
1.2 Extension package registration (required for real QMT)
import qteasy_xtquant
qteasy_xtquant.register() # calls register_broker_factory('xtquant', XtQuantBroker)
Do not patch qteasy.broker.get_broker() to from brokers import ....
Until register() runs, get_broker('xtquant') may fall back to SimulatorBroker (misleading for production). Live runs must call register() before Operator.run(..., mode=0).
2. Data contracts (qteasy.trade_io)
All orders and raw fills must pass:
validate_trade_order()— shape of orders entering the Broker layervalidate_raw_trade_result()— shape of fills leaving the Broker layer
TypedDict references (informative; validators are authoritative):
Name |
Module |
Purpose |
|---|---|---|
|
|
Order dict before / during Broker handling |
|
|
One fill/cancel slice before |
2.1 TradeOrderDict (required keys)
Key |
Type |
Notes |
|---|---|---|
|
int |
Local DB order id |
|
int |
Position id |
|
str |
|
|
str |
|
|
float |
> 0 |
|
float |
> 0 |
|
str |
Must be |
|
str or None |
ISO-like timestamp string |
Optional: symbol, position (validated if present).
2.2 RawTradeResultDict (required keys)
Key |
Type |
Notes |
|---|---|---|
|
int |
Matches local order |
|
float |
≥ 0 |
|
float |
≥ 0; 0 when canceled-only slice |
|
float |
≥ 0 |
|
str |
Parseable datetime string |
|
float |
≥ 0 |
|
float |
Often 0 at Broker layer |
|
str |
e.g. |
Optional (recommended for QMT):
Key |
Type |
Notes |
|---|---|---|
|
str |
Real QMT order id when known |
|
str |
|
|
str |
Broker-native status text/code |
3. submit_with_ack — accept phase
XtQuantBroker must override the base implementation that only enqueues and returns a synthetic id (SimulatorBroker:order_id:seq).
3.1 Call flow
Trader.submit_trade_order()
→ record_trade_order (local row, status created)
→ broker.connect()
→ broker.submit_with_ack(order)
→ if accepted: update_trade_order(status='submitted', broker_order_id=..., broker_name=...)
→ if rejected: update_trade_order(status='rejected'), broker_order_id empty
3.2 Return dict (normative)
Field |
Type |
When |
When |
|---|---|---|---|
|
bool |
|
|
|
int or None |
Local |
Same |
|
str |
Real QMT order id (non-empty) |
|
|
str |
|
English explanation |
|
str |
|
e.g. exception class name or broker code |
DB rule: On success, Trader persists broker_order_id to sys_op_trade_orders before async fills are processed. Reconciliation and cancel must use this id.
3.3 QMT-specific accept rules
Perform one QMT submit per local
order_idintent insubmit_with_ack.Do not submit again in
transaction()for the same intent (see §5).On QMT reject, return
accepted=Falsewith clearreason/reason_code; do not leave the local rowsubmitted.
4. Fill delivery: poll_fills, result_queue, and broker.run
qteasy 2.5.2 uses two fill paths. XtQuant adapters must support the Trader main loop path.
4.1 Primary path (Trader live loop)
Trader main loop (trading day)
→ broker.poll_fills(timeout=0)
→ for each raw dict: validate_raw_trade_result
→ add_task('process_result', raw)
→ process_trade_result → ledger / order status updates
Implement poll_fills to return fills produced by QMT callbacks (deque / internal buffer), each dict satisfying RawTradeResultDict.
4.2 Legacy path (broker.run thread)
broker.run() loop
→ order_queue.get()
→ _get_result(order)
→ for slice in transaction(...): build raw_trade_result → result_queue.put()
poll_fills also drains result_queue when connected (compat). For QMT:
Callback thread should push normalized fills into the structure
poll_fillsreads (preferred), orBridge callback →
result_queuewithout blocking Trader.
4.3 Relationship diagram
Invariant: Accept phase owns placement; fill phase owns execution reports. Same local order_id + same QMT broker_order_id tie the two phases together.
5. transaction() — 4/5-tuple generator
Base class Broker.transaction() may yield:
4-tuple:
(result_type, qty, price, fee)— built-in simulators5-tuple:
(result_type, qty, price, fee, broker_order_id)— required for XtQuant
Component |
Meaning |
|---|---|
|
|
|
This slice quantity; must be |
|
Fill price; |
|
Fee for this slice; ≥ 0 |
|
str or None; if str, copied to |
_get_result validates each yield against the order total order_qty (not the previous slice qty). Partial fills must not reuse the loop variable name qty for order size (fixed in qteasy PR-0a).
5.1 Forbidden: double submit to QMT
Pattern |
Verdict |
|---|---|
|
Correct |
|
Wrong (double submit risk) |
Both |
Forbidden |
6. submit() vs submit_with_ack (extension testing)
Broker.submit() runs transaction() synchronously and appends to _pending_fills. Useful for unit tests; Trader production path uses submit_with_ack + poll_fills.
If submit() is used in tests with 5-tuples, broker_order_id in each fill should still be the real QMT id when simulating production.
7. Differences from helloeveroneday fork (ext-dev)
Topic |
Fork tendency |
qteasy 2.5.2 + this contract |
|---|---|---|
Trader changes |
Large pre-check / DIAG / queue drain |
No merge; use |
Tuple contract |
5-tuple only |
4-tuple allowed for simulators; XtQuant uses 5-tuple |
Broker registration |
|
|
Accept API |
Often queue-only |
|
Data layer |
MySQL-only assumptions |
Not required for upstream PR |
Problem definitions from fork production incidents (ghost orders, rebalance race) remain valuable; implementations belong in Broker / extension package / docs, not fork-style trader.py patches.
8. Minimal acceptance (Spike / S0)
submit_with_ackreturnsaccepted=Trueand non-syntheticbroker_order_idon Windows + miniQMTDB row
sys_op_trade_orders.broker_order_idmatches QMT UI order idpoll_fills→process_resultprocesses at least one fillNo duplicate QMT order for one local
order_idMac/Linux CI: mock
xtquantmodule tests without real QMT
9. References
General adapter guide: :doc:
4-broker-adapter-and-integrationOrder lifecycle: :doc:
3-risk-and-order-lifecycleCode:
qteasy/broker.py,qteasy/trader.py,qteasy/trade_io.pyUpstream tests:
tests/test_broker_order_id_persistence_20260429.py,tests/test_trade_io_contracts.py
Contract version: v1 (2026-05; qteasy 2.5.2, PR-0a/0b). Changes should be discussed via GitHub issue before implementation drift.