chore: init qmt_bridge repo (HTTP+WS bridge, MCP endpoint, docs, references)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user