chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)

This commit is contained in:
Docker
2026-08-26 16:53:15 +08:00
commit 22a5b8ca04
210 changed files with 68176 additions and 0 deletions
@@ -0,0 +1,286 @@
import csv
import json
import os
import sys
import tempfile
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, os.path.join(ROOT, "src"))
from bigqmt_backtest.data_feed import CsvBarFeed
from bigqmt_backtest.engine import BacktestConfig, BacktestEngine
FIELDS = (
"datetime",
"symbol",
"open",
"high",
"low",
"close",
"volume",
"prev_close",
)
def _write_bars(path, rows):
with open(path, "w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=FIELDS)
writer.writeheader()
writer.writerows(rows)
def _rows():
return [
{
"datetime": "2026-01-05 09:30:00",
"symbol": "600000.SH",
"open": 10.00,
"high": 10.10,
"low": 9.95,
"close": 10.05,
"volume": 100000,
"prev_close": 9.90,
},
{
"datetime": "2026-01-05 09:31:00",
"symbol": "600000.SH",
"open": 10.10,
"high": 10.20,
"low": 10.00,
"close": 10.15,
"volume": 100000,
"prev_close": 9.90,
},
{
"datetime": "2026-01-05 09:32:00",
"symbol": "600000.SH",
"open": 10.20,
"high": 10.25,
"low": 10.10,
"close": 10.18,
"volume": 100000,
"prev_close": 9.90,
},
{
"datetime": "2026-01-06 09:30:00",
"symbol": "600000.SH",
"open": 10.30,
"high": 10.35,
"low": 10.20,
"close": 10.25,
"volume": 100000,
"prev_close": 10.18,
},
{
"datetime": "2026-01-06 09:31:00",
"symbol": "600000.SH",
"open": 10.25,
"high": 10.30,
"low": 10.15,
"close": 10.20,
"volume": 100000,
"prev_close": 10.18,
},
]
def _config(run_id, output_dir):
return BacktestConfig(
run_id=run_id,
output_dir=output_dir,
initial_cash=100000,
buy_commission_rate=0.0003,
sell_commission_rate=0.0003,
min_commission=5,
stamp_tax_rate=0.0005,
transfer_fee_rate=0.00001,
max_volume_participation=1.0,
slippage_bps=0,
)
class CsvBarFeedTest(unittest.TestCase):
def test_loads_chronologically_and_never_exposes_future_rows(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "bars.csv")
rows = list(reversed(_rows()))
_write_bars(path, rows)
feed = CsvBarFeed(path)
self.assertEqual(len(feed), 5)
self.assertEqual(feed.frame(0)["datetime"], "2026-01-05 09:30:00")
self.assertEqual(
list(feed.history("600000.SH", end_index=1, count=10, fields=["close"]))[-1]["close"],
10.15,
)
self.assertNotIn(10.18, [item["close"] for item in feed.history("600000.SH", 1, 10)])
self.assertEqual(len(feed.data_hash), 64)
def test_duplicate_symbol_timestamp_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "bars.csv")
rows = _rows()
_write_bars(path, rows + [dict(rows[0])])
with self.assertRaisesRegex(ValueError, "duplicate bar"):
CsvBarFeed(path)
class BacktestEngineTest(unittest.TestCase):
def _engine(self, tmp, run_id="run-a"):
path = os.path.join(tmp, "bars.csv")
_write_bars(path, _rows())
return BacktestEngine(CsvBarFeed(path), _config(run_id, os.path.join(tmp, run_id)))
def test_order_is_filled_at_next_bar_open_not_current_close(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self._engine(tmp)
started = engine.start()
order = engine.submit_order(
{"symbol": "600000.SH", "side": "BUY", "quantity": 100, "order_type": "MARKET"}
)
self.assertEqual(started["frame_index"], 0)
self.assertEqual(order["status"], "PENDING")
self.assertEqual(engine.state()["positions"], {})
advanced = engine.next_bar()
self.assertEqual(advanced["frame_index"], 1)
self.assertEqual(advanced["fills"][0]["price"], 10.10)
self.assertEqual(advanced["fills"][0]["commission"], 5.0)
self.assertEqual(advanced["fills"][0]["transfer_fee"], 0.01)
self.assertEqual(advanced["cash"], 98984.99)
self.assertEqual(advanced["positions"]["600000.SH"]["quantity"], 100)
self.assertEqual(advanced["positions"]["600000.SH"]["available"], 0)
def test_t_plus_one_rejects_same_day_sell_and_allows_next_day(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self._engine(tmp)
engine.start()
engine.submit_order(
{"symbol": "600000.SH", "side": "BUY", "quantity": 100, "order_type": "MARKET"}
)
engine.next_bar()
rejected = engine.submit_order(
{"symbol": "600000.SH", "side": "SELL", "quantity": 100, "order_type": "MARKET"}
)
self.assertEqual(rejected["status"], "REJECTED")
self.assertEqual(rejected["reject_reason"], "t_plus_one_unavailable")
engine.next_bar()
next_day = engine.next_bar()
self.assertEqual(next_day["positions"]["600000.SH"]["available"], 100)
accepted = engine.submit_order(
{"symbol": "600000.SH", "side": "SELL", "quantity": 100, "order_type": "MARKET"}
)
self.assertEqual(accepted["status"], "PENDING")
filled = engine.next_bar()
self.assertEqual(filled["fills"][0]["side"], "SELL")
self.assertEqual(filled["fills"][0]["stamp_tax"], 0.51)
self.assertEqual(filled["total_fees"], 10.53)
self.assertEqual(filled["cash"], 100004.47)
self.assertEqual(filled["positions"], {})
def test_limit_locked_bar_does_not_assume_fill(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "locked.csv")
rows = _rows()[:2]
rows[1].update({"open": 10.89, "high": 10.89, "low": 10.89, "close": 10.89, "prev_close": 9.90})
_write_bars(path, rows)
engine = BacktestEngine(CsvBarFeed(path), _config("locked", os.path.join(tmp, "locked")))
engine.start()
engine.submit_order(
{"symbol": "600000.SH", "side": "BUY", "quantity": 100, "order_type": "MARKET"}
)
state = engine.next_bar()
self.assertEqual(state["fills"], [])
self.assertEqual(engine.orders()[0]["status"], "EXPIRED")
self.assertEqual(engine.orders()[0]["reject_reason"], "limit_up_locked")
def test_client_order_id_is_idempotent_but_cannot_change_payload(self):
with tempfile.TemporaryDirectory() as tmp:
engine = self._engine(tmp)
engine.start()
payload = {
"client_order_id": "stable-1",
"symbol": "600000.SH",
"side": "BUY",
"quantity": 100,
"order_type": "MARKET",
}
first = engine.submit_order(payload)
repeated = engine.submit_order(dict(payload))
self.assertEqual(first["order_id"], repeated["order_id"])
self.assertEqual(len(engine.orders()), 1)
changed = dict(payload, quantity=200)
with self.assertRaisesRegex(ValueError, "different order payload"):
engine.submit_order(changed)
def test_volume_participation_is_shared_across_orders_on_the_same_bar(self):
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "volume.csv")
rows = _rows()[:2]
rows[1]["volume"] = 1000
_write_bars(path, rows)
config = _config("volume", os.path.join(tmp, "volume"))
config.max_volume_participation = 0.1
engine = BacktestEngine(CsvBarFeed(path), config)
engine.start()
for order_id in ("first", "second"):
engine.submit_order(
{
"client_order_id": order_id,
"symbol": "600000.SH",
"side": "BUY",
"quantity": 100,
"order_type": "MARKET",
}
)
advanced = engine.next_bar()
self.assertEqual(sum(fill["quantity"] for fill in advanced["fills"]), 100)
self.assertEqual(engine.orders()[1]["status"], "EXPIRED")
self.assertEqual(engine.orders()[1]["reject_reason"], "volume_participation_exhausted")
def test_finish_writes_evidence_and_is_deterministic(self):
signatures = []
with tempfile.TemporaryDirectory() as tmp:
for run_id in ("det-a", "det-b"):
engine = self._engine(tmp, run_id=run_id)
engine.start()
engine.submit_order(
{
"client_order_id": "buy-1",
"symbol": "600000.SH",
"side": "BUY",
"quantity": 100,
"order_type": "MARKET",
}
)
while not engine.next_bar()["done"]:
pass
result = engine.finish()
signatures.append(result["deterministic_signature"])
output_dir = os.path.join(tmp, run_id)
for name in ("meta.json", "result.json", "orders.csv", "fills.csv", "equity.csv", "positions.csv"):
self.assertTrue(os.path.isfile(os.path.join(output_dir, name)), name)
with open(os.path.join(output_dir, "meta.json"), encoding="utf-8") as handle:
meta = json.load(handle)
self.assertEqual(meta["data_hash"], engine.feed.data_hash)
self.assertFalse(meta["live_ready"])
self.assertEqual(signatures[0], signatures[1])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,162 @@
import datetime as dt
import os
import sys
import tempfile
import threading
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
SRC = os.path.join(ROOT, "src")
sys.path.insert(0, SRC)
from bigqmt_backtest.data_feed import StreamingBarFeed
from bigqmt_backtest.engine import BacktestConfig, StreamingBacktestEngine
from bigqmt_backtest.qmt_runtime import QmtBarExtractor, QmtNativeBacktestSession
class FakeQmtContext(object):
stockcode = "600000"
market = "SH"
period = "1m"
barpos = 0
do_back_test = True
values = {
"open": [10.0],
"high": [10.2],
"low": [9.9],
"close": [10.1],
"volume": [10000],
"amount": [101000],
"preClose": [9.8],
}
def get_bar_timetag(self, barpos):
value = dt.datetime(2026, 1, 5, 9, 30)
return int(value.timestamp() * 1000)
def get_history_data(self, count, period, field):
return {"600000.SH": self.values.get(field, [])}
def set_account(self, account_id):
self.account_id = account_id
class QmtBarExtractorTest(unittest.TestCase):
def test_extracts_qmt_bar_without_live_account_or_order_api(self):
row = QmtBarExtractor().extract(FakeQmtContext())
self.assertEqual(row["symbol"], "600000.SH")
self.assertEqual(row["datetime"], "2026-01-05 09:30:00")
self.assertEqual(row["open"], 10.0)
self.assertEqual(row["prev_close"], 9.8)
def test_qmt_entry_is_isolated_and_binds_native_order_callbacks(self):
path = os.path.join(SRC, "BIGQMT_ZMQ_BACKTEST.py")
with open(path, "r", encoding="gbk") as handle:
source = handle.read()
self.assertNotIn("bigqmt_signal_trader", source)
self.assertIn("passorder", source)
self.assertIn("get_trade_detail_data", source)
self.assertIn("order_callback", source)
self.assertIn("deal_callback", source)
self.assertNotIn("_importlib.import_module =", source)
runtime_path = os.path.join(SRC, "bigqmt_backtest", "qmt_runtime.py")
with open(runtime_path, "r", encoding="utf-8") as handle:
runtime_source = handle.read()
self.assertNotIn("StreamingBacktestEngine", runtime_source)
self.assertNotIn("SimulatedBroker", runtime_source)
def test_native_session_executes_passorder_on_qmt_callback_thread(self):
calls = []
def fake_passorder(*args):
calls.append((threading.get_ident(), args))
return "qmt-order-1"
session = QmtNativeBacktestSession(
config={
"run_id": "qmt-native-test",
"account_id": "test-account",
"bar_wait_timeout_seconds": 1,
},
qmt_api={"passorder": fake_passorder},
)
qmt_thread = threading.Thread(target=session.on_bar, args=(FakeQmtContext(),))
qmt_thread.start()
state = session.start()
queued = session.submit_order({
"symbol": "600000.SH",
"side": "BUY",
"quantity": 100,
"order_type": "MARKET",
"client_order_id": "external-1",
})
session.finish()
qmt_thread.join(timeout=2)
self.assertFalse(qmt_thread.is_alive())
self.assertEqual(state["execution_backend"], "QMT_NATIVE")
self.assertEqual(queued["status"], "QUEUED")
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0][0], qmt_thread.ident)
self.assertEqual(calls[0][1][0], 23)
self.assertEqual(calls[0][1][2], "test-account")
self.assertEqual(calls[0][1][-2], "external-1")
self.assertEqual(session.orders()[0]["status"], "SUBMITTED")
def test_native_session_rejects_non_backtest_qmt_context(self):
context = FakeQmtContext()
context.do_back_test = False
session = QmtNativeBacktestSession(config={"run_id": "guard"})
with self.assertRaisesRegex(RuntimeError, "outside QMT backtest mode"):
session.bind_context(context)
class StreamingEngineTest(unittest.TestCase):
def test_stream_does_not_report_done_until_qmt_closes_feed(self):
with tempfile.TemporaryDirectory() as tmp:
feed = StreamingBarFeed()
feed.append(
{
"datetime": "2026-01-05 09:30:00",
"symbol": "600000.SH",
"open": 10,
"high": 10.1,
"low": 9.9,
"close": 10,
"volume": 10000,
"prev_close": 9.9,
}
)
engine = StreamingBacktestEngine(
feed,
BacktestConfig(run_id="stream", output_dir=os.path.join(tmp, "out")),
bar_wait_timeout_seconds=0.1,
)
self.assertFalse(engine.start()["done"])
feed.append(
{
"datetime": "2026-01-05 09:31:00",
"symbol": "600000.SH",
"open": 10.1,
"high": 10.2,
"low": 10,
"close": 10.15,
"volume": 10000,
"prev_close": 9.9,
}
)
self.assertFalse(engine.next_bar()["done"])
feed.close()
self.assertTrue(engine.state()["done"])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,250 @@
import csv
import os
import socket
import sys
import tempfile
import threading
import unittest
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
sys.path.insert(0, os.path.join(ROOT, "src"))
from bigqmt_backtest.client import BacktestZmqClient
from bigqmt_backtest.data_feed import CsvBarFeed
from bigqmt_backtest.engine import BacktestConfig, BacktestEngine
from bigqmt_backtest.protocol import BacktestBridgeProtocol
from bigqmt_backtest.qmt_runtime import QmtNativeBacktestSession
from bigqmt_backtest.zmq_server import ZmqBacktestServer
def _free_port():
sock = socket.socket()
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
sock.close()
return port
def _feed(tmp):
path = os.path.join(tmp, "bars.csv")
with open(path, "w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(
handle,
fieldnames=("datetime", "symbol", "open", "high", "low", "close", "volume", "prev_close"),
)
writer.writeheader()
writer.writerows(
[
{
"datetime": "2026-01-05 09:30:00",
"symbol": "000001.SZ",
"open": 10,
"high": 10.1,
"low": 9.9,
"close": 10,
"volume": 10000,
"prev_close": 9.9,
},
{
"datetime": "2026-01-05 09:31:00",
"symbol": "000001.SZ",
"open": 10.1,
"high": 10.2,
"low": 10,
"close": 10.15,
"volume": 10000,
"prev_close": 9.9,
},
]
)
return CsvBarFeed(path)
class BacktestProtocolTest(unittest.TestCase):
def test_run_and_client_identity_are_enforced_and_requests_are_idempotent(self):
with tempfile.TemporaryDirectory() as tmp:
engine = BacktestEngine(
_feed(tmp),
BacktestConfig(run_id="identity", output_dir=os.path.join(tmp, "out")),
)
protocol = BacktestBridgeProtocol(engine)
start = {
"schema_version": 1,
"request_id": "req-start",
"run_id": "identity",
"client_id": "client-a",
"method": "start",
"params": {},
}
first = protocol.handle(start)
repeated = protocol.handle(dict(start))
self.assertTrue(first["ok"])
self.assertEqual(first, repeated)
reused = dict(start, method="state")
reused_response = protocol.handle(reused)
self.assertFalse(reused_response["ok"])
self.assertIn("different payload", reused_response["error"])
wrong_run = dict(start, request_id="wrong-run", run_id="other", method="state")
self.assertFalse(protocol.handle(wrong_run)["ok"])
wrong_client = dict(start, request_id="wrong-client", client_id="client-b", method="next_bar")
self.assertFalse(protocol.handle(wrong_client)["ok"])
class ZmqRoundTripTest(unittest.TestCase):
def test_external_client_can_complete_a_backtest_over_zmq(self):
with tempfile.TemporaryDirectory() as tmp:
engine = BacktestEngine(
_feed(tmp),
BacktestConfig(
run_id="zmq-run",
output_dir=os.path.join(tmp, "out"),
max_volume_participation=1.0,
),
)
endpoint = "tcp://127.0.0.1:%d" % _free_port()
server = ZmqBacktestServer(
BacktestBridgeProtocol(engine), endpoint=endpoint, exit_on_finish=True
)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
self.assertTrue(server.wait_until_ready(3.0))
client = BacktestZmqClient(
endpoint=endpoint,
run_id="zmq-run",
client_id="external-strategy",
timeout_seconds=3.0,
)
started = client.start()
order = client.submit_order(
symbol="000001.SZ", side="BUY", quantity=100, order_type="MARKET"
)
advanced = client.next_bar()
history = client.history("000001.SZ", count=10, fields=["close"])
result = client.finish()
client.close()
thread.join(timeout=3.0)
self.assertEqual(started["frame_index"], 0)
self.assertEqual(order["status"], "PENDING")
self.assertEqual(advanced["fills"][0]["price"], 10.1)
self.assertEqual([row["close"] for row in history], [10.0, 10.15])
self.assertEqual(result["run_id"], "zmq-run")
self.assertFalse(thread.is_alive())
def test_qmt_native_service_bridges_orders_to_qmt_matching(self):
endpoint = "tcp://127.0.0.1:%d" % _free_port()
calls = []
holder = {}
class Context(object):
do_back_test = True
stockcode = "600000"
market = "SH"
period = "1m"
def __init__(self, barpos, close):
self.barpos = barpos
self.close_value = close
def set_account(self, account_id):
self.account_id = account_id
def get_bar_timetag(self, barpos):
return int((1704072600 + barpos * 60) * 1000)
def get_history_data(self, count, period, field):
values = {
"open": self.close_value,
"high": self.close_value + 0.1,
"low": self.close_value - 0.1,
"close": self.close_value,
"volume": 10000,
"amount": self.close_value * 10000,
"preClose": self.close_value - 0.1,
}
return {"600000.SH": [values[field]]}
def fake_passorder(*args):
calls.append((threading.get_ident(), args))
session = holder["session"]
session.on_order({
"m_strOrderSysID": "qmt-order-1",
"m_strRemark": args[-2],
"m_strInstrumentID": "600000",
"m_strExchangeID": "SH",
"m_nOffsetFlag": 48,
"m_nVolumeTotalOriginal": args[6],
"m_nVolumeTraded": args[6],
"m_nOrderStatus": "FILLED",
})
session.on_trade({
"m_strTradeID": "qmt-fill-1",
"m_strOrderSysID": "qmt-order-1",
"m_strRemark": args[-2],
"m_strInstrumentID": "600000",
"m_strExchangeID": "SH",
"m_nOffsetFlag": 48,
"m_nVolume": args[6],
"m_dPrice": 10.1,
"m_strTradeTime": "09:31:00",
})
return "qmt-order-1"
session = QmtNativeBacktestSession(
config={
"run_id": "qmt-zmq-native",
"account_id": "backtest-account",
"bar_wait_timeout_seconds": 2,
},
qmt_api={"passorder": fake_passorder},
)
holder["session"] = session
server = ZmqBacktestServer(
BacktestBridgeProtocol(session), endpoint=endpoint, exit_on_finish=True
)
server_thread = threading.Thread(target=server.serve_forever, daemon=True)
server_thread.start()
self.assertTrue(server.wait_until_ready(2))
def qmt_backtest_loop():
session.bind_context(Context(0, 10.0))
session.on_bar(Context(0, 10.0))
session.on_bar(Context(1, 10.1))
session.on_qmt_stop()
qmt_thread = threading.Thread(target=qmt_backtest_loop)
qmt_thread.start()
client = BacktestZmqClient(endpoint, run_id="", timeout_seconds=2)
description = client.describe()
first = client.start()
queued = client.submit_order("600000.SH", "BUY", 100, client_order_id="native-buy-1")
second = client.next_bar()
done = client.next_bar()
result = client.finish()
client.close()
qmt_thread.join(timeout=2)
server_thread.join(timeout=2)
self.assertEqual(description["engine_owner"], "QMT")
self.assertEqual(description["matching_owner"], "QMT")
self.assertEqual(client.run_id, "qmt-zmq-native")
self.assertEqual(first["frame_index"], 0)
self.assertEqual(queued["status"], "QUEUED")
self.assertEqual(second["frame_index"], 1)
self.assertEqual(second["fills"][0]["fill_id"], "qmt-fill-1")
self.assertTrue(done["done"])
self.assertEqual(result["result_owner"], "QMT")
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0][0], qmt_thread.ident)
self.assertFalse(qmt_thread.is_alive())
self.assertFalse(server_thread.is_alive())
if __name__ == "__main__":
unittest.main()