feat: add Quant OS A-share baseline

This commit is contained in:
2026-07-26 12:54:04 +08:00
commit 48c5f64bbd
98 changed files with 31874 additions and 0 deletions
+132
View File
@@ -0,0 +1,132 @@
import hashlib
import json
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from platforms import joinquant_strategy, qmt_builtin_strategy
from tools.bundle_platforms import bundle_all, effective_platform_config
from tools.capability_probe import probe_capabilities
class BundleTest(unittest.TestCase):
def test_source_wrappers_match_canonical_baseline_config(self):
baseline = json.loads(
(ROOT / "configs/baseline.json").read_text(encoding="utf-8")
)
self.assertEqual(
joinquant_strategy.CONFIG,
effective_platform_config(baseline, "joinquant"),
)
self.assertEqual(
qmt_builtin_strategy.CONFIG,
effective_platform_config(baseline, "qmt_builtin"),
)
def test_bundles_inline_same_core_compile_and_match_manifest(self):
with tempfile.TemporaryDirectory() as temp:
output = Path(temp)
manifest = bundle_all(ROOT, output)
manifest_file = output / "bundle_manifest.json"
stored = json.loads(manifest_file.read_text(encoding="ascii"))
for name, filename in {
"joinquant": "joinquant_strategy.py",
"qmt_builtin": "qmt_builtin_strategy.py",
}.items():
artifact = output / filename
payload = artifact.read_bytes()
compile(payload, str(artifact), "exec")
self.assertNotIn(b"from quant60.portable_core import", payload)
self.assertEqual(
hashlib.sha256(payload).hexdigest(),
stored["artifacts"][name]["output_sha256"],
)
(output / "qmt_builtin_strategy.py").read_bytes().decode("ascii")
self.assertEqual(
stored["artifacts"]["joinquant"]["core"],
"src/quant60/portable_core.py",
)
self.assertNotIn(
"/Users/", json.dumps(stored, ensure_ascii=True)
)
self.assertEqual(
stored["portable_core_sha256"],
manifest["portable_core_sha256"],
)
self.assertEqual(stored["baseline_config"], "configs/baseline.json")
self.assertEqual(
stored["baseline_config_sha256"],
hashlib.sha256(
(ROOT / "configs/baseline.json").read_bytes()
).hexdigest(),
)
def test_committed_dist_is_byte_for_byte_fresh(self):
with tempfile.TemporaryDirectory() as temp:
output = Path(temp)
bundle_all(ROOT, output)
for name in (
"joinquant_strategy.py",
"qmt_builtin_strategy.py",
"bundle_manifest.json",
):
self.assertEqual(
(ROOT / "dist" / name).read_bytes(),
(output / name).read_bytes(),
f"committed dist artifact is stale: {name}",
)
def test_joinquant_bundle_ignores_host_shadowed_sum_name(self):
with tempfile.TemporaryDirectory() as temp:
output = Path(temp)
bundle_all(ROOT, output)
source = (output / "joinquant_strategy.py").read_text(
encoding="utf-8"
)
namespace = {"sum": lambda values: values}
exec(compile(source, "joinquant_strategy.py", "exec"), namespace)
weights = namespace["capped_target_weights"](
{
"600000.XSHG": 3.0,
"000001.XSHE": 2.0,
"300750.XSHE": 1.0,
},
max_weight=0.4,
gross_target=0.95,
)
self.assertAlmostEqual(
namespace["_sum_numeric"](weights.values()),
0.95,
)
quantities = namespace["target_quantities"](
weights,
{symbol: 10.0 for symbol in weights},
equity=1_000_000,
cash_buffer=0.02,
)
self.assertTrue(all(value >= 0 for value in quantities.values()))
class CapabilityProbeTest(unittest.TestCase):
def test_shallow_probe_does_not_claim_external_verification(self):
report = probe_capabilities(ROOT, deep=False)
self.assertTrue(report["artifacts"]["portable_core"])
self.assertTrue(report["artifacts"]["colab_notebook"])
self.assertTrue(report["artifacts"]["jqdata_snapshot_adapter"])
self.assertTrue(report["artifacts"]["qmt_shadow_planner"])
self.assertTrue(report["artifacts"]["qmt_shadow_cli"])
self.assertIn("no external platform", report["verification_boundary"])
self.assertTrue(report["matrix"]["xttrader"]["safe_default"])
self.assertIn("rejects BJ", report["matrix"]["xttrader"]["market_scope"])
for probe in report["optional_runtimes"].values():
self.assertIs(probe["import_ok"], None)
if __name__ == "__main__":
unittest.main()
+460
View File
@@ -0,0 +1,460 @@
import datetime
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from platforms import joinquant_strategy, qmt_builtin_strategy
from platforms.fake_joinquant_harness import FakeJoinQuantHarness, FakePosition
from platforms.fake_qmt_harness import FakeQmtContext, FakeQmtHarness
def price_histories():
return {
"600000.XSHG": [15.0 - index * 0.02 for index in range(21)],
"000001.XSHE": [10.0 + index * 0.10 for index in range(21)],
"300750.XSHE": [100.0 + index * 1.5 for index in range(21)],
"000333.XSHE": [20.0 + index * 0.05 for index in range(21)],
}
class JoinQuantContractTest(unittest.TestCase):
def setUp(self):
self.harness = FakeJoinQuantHarness(price_histories())
self.harness.initialize(joinquant_strategy)
def test_initialize_enables_future_data_guard_and_weekly_open(self):
self.assertIs(self.harness.options["avoid_future_data"], True)
self.assertIs(self.harness.options["use_real_price"], True)
self.assertEqual(
self.harness.options["order_volume_ratio"],
0.1,
)
self.assertEqual(self.harness.benchmark, "000905.XSHG")
self.assertEqual(self.harness.order_cost["type"], "stock")
self.assertAlmostEqual(
self.harness.order_cost["cost"].open_commission,
0.00021,
)
self.assertAlmostEqual(
self.harness.order_cost["cost"].close_tax,
0.0005,
)
self.assertEqual(self.harness.slippage["type"], "stock")
self.assertAlmostEqual(
self.harness.slippage["slippage"].value,
0.0004,
)
self.assertEqual(len(self.harness.schedules), 1)
_, run_time = self.harness.schedules[0]
self.assertEqual(run_time, "open")
def test_rebalance_submits_exact_portable_executable_deltas(self):
self.assertIsNone(self.harness.run_scheduled())
self.harness.advance_session()
plan = self.harness.run_scheduled()
self.assertEqual(plan, self.harness.g.quant60_last_plan)
self.assertTrue(self.harness.orders)
ordered_symbols = {symbol for symbol, _ in self.harness.orders}
expected = {
symbol: int(delta)
for symbol, delta in plan["orders"].items()
if int(delta) != 0
}
self.assertEqual(ordered_symbols, set(expected))
self.assertEqual(
dict(self.harness.orders),
expected,
)
def test_missing_current_data_fails_closed_for_new_position(self):
best = "300750.XSHE"
del self.harness.current_data[best]
self.assertIsNone(self.harness.run_scheduled())
self.harness.advance_session()
self.harness.run_scheduled()
ordered_symbols = {symbol for symbol, _ in self.harness.orders}
self.assertNotIn(best, ordered_symbols)
self.assertEqual(
self.harness.g.quant60_skipped_orders[-1]["reason"],
"current_data_unavailable",
)
def test_star_market_buy_uses_daily_high_limit_protection(self):
histories = price_histories()
star = "688301.XSHG"
histories[star] = [20.0 + index * 0.3 for index in range(21)]
harness = FakeJoinQuantHarness(histories)
harness.current_data[star].high_limit = 31.28
harness.initialize(joinquant_strategy)
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
plan = harness.run_scheduled()
self.assertGreater(plan["orders"][star], 0)
styles = dict(harness.order_styles)
self.assertEqual(styles[star].kind, "limit")
self.assertAlmostEqual(styles[star].limit_price, 31.28)
def test_star_market_sell_uses_daily_low_limit_protection(self):
histories = price_histories()
star = "688301.XSHG"
histories[star] = [20.0 + index * 0.3 for index in range(21)]
harness = FakeJoinQuantHarness(
histories,
total_value=1000.0,
positions={star: FakePosition(1000)},
)
harness.current_data[star].low_limit = 17.06
harness.initialize(joinquant_strategy)
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
plan = harness.run_scheduled()
self.assertEqual(plan["orders"][star], -1000)
styles = dict(harness.order_styles)
self.assertEqual(styles[star].kind, "limit")
self.assertAlmostEqual(styles[star].limit_price, 17.06)
def test_invalid_star_protection_price_fails_closed(self):
histories = price_histories()
star = "688301.XSHG"
histories[star] = [20.0 + index * 0.3 for index in range(21)]
harness = FakeJoinQuantHarness(histories)
harness.current_data[star].high_limit = float("nan")
harness.initialize(joinquant_strategy)
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
plan = harness.run_scheduled()
self.assertGreater(plan["orders"][star], 0)
self.assertNotIn(star, dict(harness.orders))
self.assertEqual(
harness.g.quant60_skipped_orders[-1]["reason"],
"star_protection_price_invalid",
)
def test_incomplete_history_aborts_entire_rebalance_without_orders(self):
histories = price_histories()
histories["300750.XSHE"] = histories["300750.XSHE"][1:]
harness = FakeJoinQuantHarness(histories)
harness.initialize(joinquant_strategy)
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
with self.assertRaises(joinquant_strategy.PriceHistoryUnavailable):
harness.run_scheduled()
self.assertEqual(harness.orders, [])
def test_joinquant_unmanaged_position_aborts_without_orders(self):
harness = FakeJoinQuantHarness(
price_histories(),
positions={"601318.XSHG": FakePosition(1000)},
)
harness.initialize(joinquant_strategy)
harness.g.quant60_config["universe_mode"] = "fixed"
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
with self.assertRaises(joinquant_strategy.UnmanagedPositionError):
harness.run_scheduled()
self.assertEqual(harness.orders, [])
def test_executable_sell_delta_preserves_t1_cap_and_odd_lot(self):
position = FakePosition(total_amount=1050, closeable_amount=200)
harness = FakeJoinQuantHarness(
price_histories(),
total_value=1000.0,
positions={"600000.XSHG": position},
)
harness.initialize(joinquant_strategy)
self.assertIsNone(harness.run_scheduled())
harness.advance_session()
plan = harness.run_scheduled()
self.assertEqual(plan["orders"]["600000.XSHG"], -200)
self.assertEqual(dict(harness.orders)["600000.XSHG"], 850)
def test_one_session_week_executes_on_next_available_session(self):
self.assertIsNone(self.harness.run_scheduled())
# Model a holiday week with no second session: the next available
# session is the first session of the next ISO week.
self.harness.advance_session(days=7)
plan = self.harness.run_scheduled()
self.assertIsNotNone(plan)
self.assertTrue(self.harness.orders)
self.assertEqual(
self.harness.g.quant60_pending_first_session,
self.harness.context.current_dt.date(),
)
class CrossPlatformParityTest(unittest.TestCase):
def test_same_completed_close_produces_same_portable_plan(self):
jq = FakeJoinQuantHarness(price_histories())
jq.initialize(joinquant_strategy)
jq.context.previous_date = datetime.date(2026, 7, 24)
jq.context.current_dt = datetime.datetime(2026, 7, 27, 9, 30)
jq_plan = joinquant_strategy.compute_plan(jq.context)
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(
qmt_histories,
bar_time=datetime.datetime(2026, 7, 24, 15, 0),
)
qmt = FakeQmtHarness(context).install(qmt_builtin_strategy)
qmt_builtin_strategy.init(context)
self.assertEqual(context.commission["type"], 0)
self.assertEqual(
context.commission["values"],
[0.0, 0.0005, 0.00021, 0.00021, 0.0, 5.0],
)
self.assertEqual(
context.slippage,
{"type": 2, "value": 0.0002},
)
qmt_plan = qmt_builtin_strategy.compute_plan(
context, datetime.datetime(2026, 7, 24, 15, 0)
)
self.assertEqual(jq_plan, qmt_plan)
self.assertEqual(joinquant_strategy.CONFIG["skip"], 0)
self.assertEqual(qmt_builtin_strategy.CONFIG["skip"], 0)
self.assertEqual(context.last_market_request["end_time"], "20260724")
self.assertIs(context.last_market_request["fill_data"], False)
self.assertEqual(
jq.last_index_request,
{
"index_symbol": "000905.XSHG",
"date": jq.context.previous_date,
},
)
self.assertEqual(
jq.last_extras_request["info"],
"is_st",
)
self.assertEqual(
jq.last_extras_request["end_date"],
jq.context.previous_date,
)
self.assertEqual(
context.last_sector_request,
{
"sector_name": "中证500",
"timetag": context.get_bar_timetag(context.barpos),
},
)
def test_pit_st_exclusion_is_identical_across_hosted_wrappers(self):
st_canonical = "300750.XSHE"
jq = FakeJoinQuantHarness(
price_histories(),
st_symbols={st_canonical},
)
jq.initialize(joinquant_strategy)
jq.context.previous_date = datetime.date(2026, 7, 24)
jq.context.current_dt = datetime.datetime(2026, 7, 27, 9, 30)
jq_plan = joinquant_strategy.compute_plan(jq.context)
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
qmt_symbol = "300750.SZ"
context = FakeQmtContext(
qmt_histories,
bar_time=datetime.datetime(2026, 7, 24, 15, 0),
st_periods={
qmt_symbol: {
"ST": [["20260701", "20260731"]],
}
},
)
FakeQmtHarness(context).install(qmt_builtin_strategy)
qmt_builtin_strategy.init(context)
qmt_plan = qmt_builtin_strategy.compute_plan(
context,
datetime.datetime(2026, 7, 24, 15, 0),
)
self.assertEqual(jq_plan, qmt_plan)
self.assertNotIn(st_canonical, jq_plan["scores"])
self.assertNotIn(st_canonical, qmt_plan["scores"])
def test_qmt_live_orders_are_off_by_default(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(
qmt_histories,
trade_mode="trading",
bar_time=datetime.datetime(2026, 7, 24, 15, 0),
)
harness = FakeQmtHarness(context)
self.assertIsNone(harness.run(qmt_builtin_strategy))
context._bar_time += datetime.timedelta(days=7)
plan = qmt_builtin_strategy.handlebar(context)
self.assertIsNotNone(plan)
self.assertEqual(harness.orders, [])
def test_qmt_builtin_live_mutation_cannot_be_enabled_by_parameters(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(
qmt_histories,
trade_mode="trading",
bar_time=datetime.datetime(2026, 7, 24, 15, 0),
)
harness = FakeQmtHarness(context).install(qmt_builtin_strategy)
qmt_builtin_strategy.init(context)
context.do_back_test = "False"
self.assertFalse(qmt_builtin_strategy._is_backtest(context))
self.assertFalse(qmt_builtin_strategy._orders_allowed(context))
# Even an injected ContextInfo knob cannot enable broker mutation.
context.q60_config = {
"enable_live_orders": True,
"live_confirmation": "Q60_LIVE_ACK",
}
self.assertFalse(qmt_builtin_strategy._orders_allowed(context))
def test_qmt_module_state_survives_contextinfo_rollback_between_bars(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
first_context = FakeQmtContext(
qmt_histories,
trade_mode="backtest",
bar_time=datetime.datetime(2026, 7, 20, 15, 0),
)
harness = FakeQmtHarness(first_context).install(qmt_builtin_strategy)
qmt_builtin_strategy.init(first_context)
self.assertFalse(
any(name.startswith("q60_") for name in vars(first_context))
)
self.assertIsNone(qmt_builtin_strategy.handlebar(first_context))
# QMT documents that ContextInfo user attributes may roll back between
# handlebar calls. Rebuild a clean platform context to model that
# boundary; the module-global clock must still recognize a new week.
rebuilt_context = FakeQmtContext(
qmt_histories,
trade_mode="backtest",
bar_time=datetime.datetime(2026, 7, 27, 15, 0),
)
self.assertFalse(
any(name.startswith("q60_") for name in vars(rebuilt_context))
)
plan = qmt_builtin_strategy.handlebar(rebuilt_context)
self.assertIsNotNone(plan)
self.assertGreater(len(harness.orders), 0)
self.assertIs(qmt_builtin_strategy.g.last_plan, plan)
def test_qmt_backtest_submits_once_per_iso_week(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(qmt_histories, trade_mode="backtest")
harness = FakeQmtHarness(context)
self.assertIsNone(harness.run(qmt_builtin_strategy))
context._bar_time += datetime.timedelta(days=7)
first = qmt_builtin_strategy.handlebar(context)
first_count = len(harness.orders)
second = qmt_builtin_strategy.handlebar(context)
self.assertIsNotNone(first)
self.assertGreater(first_count, 0)
self.assertIsNone(second)
self.assertEqual(len(harness.orders), first_count)
first_ids = {item["user_order_id"] for item in harness.orders}
context._bar_time += datetime.timedelta(days=7)
third = qmt_builtin_strategy.handlebar(context)
second_ids = {
item["user_order_id"] for item in harness.orders[first_count:]
}
self.assertIsNotNone(third)
self.assertTrue(first_ids.isdisjoint(second_ids))
self.assertTrue(all(len(value.encode("ascii")) <= 24 for value in second_ids))
def test_qmt_incomplete_history_aborts_without_passorder(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
qmt_histories["300750.SZ"] = qmt_histories["300750.SZ"][1:]
context = FakeQmtContext(qmt_histories, trade_mode="backtest")
harness = FakeQmtHarness(context)
self.assertIsNone(harness.run(qmt_builtin_strategy))
context._bar_time += datetime.timedelta(days=7)
with self.assertRaises(qmt_builtin_strategy.PriceHistoryUnavailable):
qmt_builtin_strategy.handlebar(context)
self.assertEqual(harness.orders, [])
def test_qmt_unmanaged_position_aborts_without_passorder(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
position = type(
"Position",
(),
{
"stock_code": "601318.SH",
"volume": 1000,
"can_use_volume": 1000,
},
)()
context = FakeQmtContext(
qmt_histories,
positions=[position],
trade_mode="backtest",
)
harness = FakeQmtHarness(context)
self.assertIsNone(harness.run(qmt_builtin_strategy))
qmt_builtin_strategy.g.config["universe_mode"] = "fixed"
context._bar_time += datetime.timedelta(days=7)
with self.assertRaises(qmt_builtin_strategy.UnmanagedPositionError):
qmt_builtin_strategy.handlebar(context)
self.assertEqual(harness.orders, [])
def test_qmt_minute_driver_is_rejected_before_orders(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(
qmt_histories,
trade_mode="backtest",
params={"period": "1m"},
)
harness = FakeQmtHarness(context)
with self.assertRaises(qmt_builtin_strategy.UnsupportedStrategyPeriod):
harness.run(qmt_builtin_strategy)
self.assertEqual(harness.orders, [])
def test_qmt_builtin_do_back_test_flag_enables_backtest_orders(self):
qmt_histories = {
symbol.replace(".XSHE", ".SZ").replace(".XSHG", ".SH"): values
for symbol, values in price_histories().items()
}
context = FakeQmtContext(qmt_histories, trade_mode="backtest")
del context.trade_mode
context._param.pop("trade_mode")
context.do_back_test = True
harness = FakeQmtHarness(context)
self.assertIsNone(harness.run(qmt_builtin_strategy))
context._bar_time += datetime.timedelta(days=7)
plan = qmt_builtin_strategy.handlebar(context)
self.assertIsNotNone(plan)
self.assertGreater(len(harness.orders), 0)
def test_qmt_source_is_ascii_despite_gbk_header(self):
payload = (ROOT / "platforms/qmt_builtin_strategy.py").read_bytes()
payload.decode("ascii")
self.assertTrue(payload.startswith(b"# coding: gbk"))
if __name__ == "__main__":
unittest.main()
+363
View File
@@ -0,0 +1,363 @@
import importlib.util
import os
import struct
import sys
import tempfile
import unittest
from contextlib import redirect_stderr
from io import StringIO
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from platforms.qlib_runner import (
QlibUnavailableError,
_main,
build_alpha158_lightgbm_task,
generate_portable_momentum_signal,
run_native_momentum_backtest,
)
from tools.build_qlib_tiny_fixture import build_default_fixture
class QlibFixtureTest(unittest.TestCase):
def test_standard_library_fixture_has_expected_bin_header(self):
with tempfile.TemporaryDirectory() as temp:
report = build_default_fixture(Path(temp), days=40)
feature = Path(temp) / "features/sh600000/close.day.bin"
payload = feature.read_bytes()
values = struct.unpack("<" + "f" * (len(payload) // 4), payload)
self.assertEqual(values[0], 0.0)
self.assertEqual(len(values), 41)
self.assertEqual(report["calendar_count"], 40)
self.assertTrue((Path(temp) / "calendars/day.txt").is_file())
self.assertTrue((Path(temp) / "instruments/csi300.txt").is_file())
def test_alpha158_lightgbm_config_uses_stable_097_paths(self):
config = build_alpha158_lightgbm_task(
market="csi300",
benchmark="SH000300",
train=("2018-01-01", "2020-12-31"),
valid=("2021-01-01", "2021-12-31"),
test=("2022-01-01", "2022-12-31"),
)
self.assertEqual(
config["task"]["model"]["module_path"],
"qlib.contrib.model.gbdt",
)
self.assertEqual(
config["task"]["dataset"]["kwargs"]["handler"]["module_path"],
"qlib.contrib.data.handler",
)
self.assertEqual(
config["portfolio_analysis"]["executor"]["class"],
"SimulatorExecutor",
)
self.assertEqual(
config["portfolio_analysis"]["strategy"]["class"],
"TopkDropoutStrategy",
)
@unittest.skipUnless(
importlib.util.find_spec("qlib") is not None
and os.environ.get("QUANT60_RUN_QLIB_NATIVE_SMOKE") == "1",
"set QUANT60_RUN_QLIB_NATIVE_SMOKE=1 in a pyqlib 0.9.7 env",
)
def test_optional_native_momentum_smoke(self):
with tempfile.TemporaryDirectory() as temp:
build_default_fixture(Path(temp), days=80)
result = run_native_momentum_backtest(
provider_uri=temp,
market="csi300",
benchmark="SH000300",
start_time="2024-02-01",
end_time="2024-04-19",
topk=1,
n_drop=1,
)
self.assertGreater(len(result["signal"]), 0)
def test_missing_provider_fails_before_optional_import(self):
with self.assertRaises(FileNotFoundError):
run_native_momentum_backtest(
provider_uri="/definitely/missing/quant60-provider",
market="csi300",
benchmark="SH000300",
start_time="2024-01-01",
end_time="2024-02-01",
)
@unittest.skipUnless(
importlib.util.find_spec("pandas") is not None,
"pandas is optional outside the Qlib environment",
)
def test_weekly_signal_uses_true_first_provider_session(self):
import pandas
dates = pandas.bdate_range("2024-01-02", periods=20)
index = pandas.MultiIndex.from_product(
[["SH600000"], dates],
names=["instrument", "datetime"],
)
frame = pandas.DataFrame(
{"$close": [10.0 + index * 0.1 for index in range(len(dates))]},
index=index,
)
class FakeD:
@staticmethod
def features(*args, **kwargs):
del args, kwargs
return frame
signal = generate_portable_momentum_signal(
FakeD,
["SH600000"],
start_time="2024-01-02",
end_time="2024-01-31",
feature_start_time="2024-01-02",
lookback=2,
rebalance="weekly",
)
selected_dates = list(
signal.index.get_level_values("datetime").unique()
)
# Warm-up finishes mid-week, so that partial week is skipped. Every
# emitted date thereafter is the actual first provider session.
self.assertNotIn(pandas.Timestamp("2024-01-04"), selected_dates)
for selected in selected_dates:
week_dates = [
value
for value in dates
if value.to_period("W") == selected.to_period("W")
]
self.assertEqual(selected, min(week_dates))
class QlibCliTest(unittest.TestCase):
def test_legacy_momentum_cli_remains_default_and_writes_json(self):
fake_result = {
"qlib_version": "0.9.7",
"signal": [0.1, 0.2],
"portfolio_metrics": {"1day": object()},
"clock_contract": "test-clock",
"strategy_fidelity": "signal-only",
"a_share_rule_fidelity": "approximation",
}
with tempfile.TemporaryDirectory() as temp:
output = Path(temp) / "nested" / "summary.json"
with mock.patch(
"platforms.qlib_runner.run_native_momentum_backtest",
return_value=fake_result,
) as runner, mock.patch("builtins.print") as printer:
status = _main(
[
"--provider-uri",
temp,
"--start",
"2024-01-02",
"--end",
"2024-03-29",
"--feature-start",
"2023-12-01",
"--lookback",
"10",
"--skip",
"2",
"--topk",
"20",
"--n-drop",
"3",
"--result-json",
str(output),
]
)
self.assertEqual(status, 0)
runner.assert_called_once_with(
provider_uri=temp,
market="csi300",
benchmark="SH000300",
start_time="2024-01-02",
end_time="2024-03-29",
feature_start_time="2023-12-01",
lookback=10,
skip=2,
topk=20,
n_drop=3,
rebalance="weekly",
)
payload = __import__("json").loads(output.read_text(encoding="utf-8"))
self.assertEqual(payload["workflow"], "momentum")
self.assertEqual(payload["signal_rows"], 2)
self.assertEqual(payload["portfolio_frequencies"], ["1day"])
self.assertIn('"workflow": "momentum"', printer.call_args.args[0])
def test_alpha158_cli_dispatches_with_validated_segments(self):
fake_result = {
"qlib_version": "0.9.7",
"experiment_name": "quant-os-test",
"recorder_id": "rec-123",
}
with tempfile.TemporaryDirectory() as temp:
with mock.patch(
"platforms.qlib_runner.run_alpha158_lightgbm_workflow",
return_value=fake_result,
) as runner, mock.patch("builtins.print"):
status = _main(
[
"--workflow",
"alpha158",
"--provider-uri",
temp,
"--market",
"csi500",
"--benchmark",
"SH000905",
"--train-start",
"2018-01-01",
"--train-end",
"2020-12-31",
"--valid-start",
"2021-01-01",
"--valid-end",
"2021-12-31",
"--test-start",
"2022-01-01",
"--test-end",
"2022-12-31",
"--experiment-name",
"quant-os-test",
]
)
self.assertEqual(status, 0)
runner.assert_called_once_with(
provider_uri=temp,
market="csi500",
benchmark="SH000905",
train=("2018-01-01", "2020-12-31"),
valid=("2021-01-01", "2021-12-31"),
test=("2022-01-01", "2022-12-31"),
experiment_name="quant-os-test",
topk=50,
n_drop=5,
)
def test_alpha158_cli_rejects_missing_or_overlapping_segments(self):
base = [
"--workflow",
"alpha158",
"--provider-uri",
"/provider",
"--train-start",
"2018-01-01",
"--train-end",
"2020-12-31",
"--valid-start",
"2020-12-31",
"--valid-end",
"2021-12-31",
"--test-start",
"2022-01-01",
"--test-end",
"2022-12-31",
]
with mock.patch(
"platforms.qlib_runner.run_alpha158_lightgbm_workflow"
) as runner, redirect_stderr(StringIO()):
with self.assertRaises(SystemExit) as overlap:
_main(base)
with self.assertRaises(SystemExit) as missing:
_main(base[:-2])
self.assertEqual(overlap.exception.code, 2)
self.assertEqual(missing.exception.code, 2)
runner.assert_not_called()
def test_momentum_cli_rejects_invalid_dates_and_position_counts(self):
with mock.patch(
"platforms.qlib_runner.run_native_momentum_backtest"
) as runner, redirect_stderr(StringIO()):
with self.assertRaises(SystemExit) as dates:
_main(
[
"--provider-uri",
"/provider",
"--start",
"2024-02-01",
"--end",
"2024-01-01",
]
)
with self.assertRaises(SystemExit) as counts:
_main(
[
"--provider-uri",
"/provider",
"--start",
"2024-01-01",
"--end",
"2024-02-01",
"--topk",
"2",
"--n-drop",
"3",
]
)
self.assertEqual(dates.exception.code, 2)
self.assertEqual(counts.exception.code, 2)
runner.assert_not_called()
def test_cli_rejects_date_arguments_from_the_other_workflow(self):
with mock.patch(
"platforms.qlib_runner.run_native_momentum_backtest"
) as momentum_runner, redirect_stderr(StringIO()):
with self.assertRaises(SystemExit) as momentum:
_main(
[
"--provider-uri",
"/provider",
"--start",
"2024-01-01",
"--end",
"2024-02-01",
"--train-start",
"2020-01-01",
]
)
with mock.patch(
"platforms.qlib_runner.run_alpha158_lightgbm_workflow"
) as alpha_runner, redirect_stderr(StringIO()):
with self.assertRaises(SystemExit) as alpha:
_main(
[
"--workflow",
"alpha158",
"--provider-uri",
"/provider",
"--start",
"2022-01-01",
"--train-start",
"2018-01-01",
"--train-end",
"2020-12-31",
"--valid-start",
"2021-01-01",
"--valid-end",
"2021-12-31",
"--test-start",
"2022-01-01",
"--test-end",
"2022-12-31",
]
)
self.assertEqual(momentum.exception.code, 2)
self.assertEqual(alpha.exception.code, 2)
momentum_runner.assert_not_called()
alpha_runner.assert_not_called()
if __name__ == "__main__":
unittest.main()
+155
View File
@@ -0,0 +1,155 @@
import sys
import json
import hashlib
import tempfile
import types
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from platforms import qmt_research_runner
class FakeFrame:
def __len__(self):
return 1
class QmtResearchRunnerTest(unittest.TestCase):
def test_build_params_normalizes_official_timestamps(self):
params = qmt_research_runner.build_qmt_parameters(
stock_code="600000.sh",
start_time="20260102",
end_time="2026-01-30",
)
self.assertEqual(params["stock_code"], "600000.SH")
self.assertEqual(params["start_time"], "2026-01-02 00:00:00")
self.assertEqual(params["end_time"], "2026-01-30 23:59:59")
self.assertEqual(params["trade_mode"], "backtest")
self.assertEqual(params["quote_mode"], "history")
self.assertEqual(params["account_id"], "test")
self.assertEqual(params["benchmark"], "000905.SH")
baseline_bytes = (ROOT / "configs/baseline.json").read_bytes()
baseline = json.loads(baseline_bytes)
self.assertEqual(
params["max_vol_rate"],
baseline["execution"]["participation_rate"],
)
self.assertEqual(
params["slippage"],
baseline["execution"]["slippage_bps"] / 10_000,
)
expected_commission = (
baseline["fees"]["commission_rate"]
+ baseline["fees"]["transfer_fee_rate"]
)
self.assertEqual(params["open_commission"], expected_commission)
self.assertEqual(params["close_commission"], expected_commission)
self.assertEqual(
params["q60_baseline_config_sha256"],
hashlib.sha256(baseline_bytes).hexdigest(),
)
def test_live_mode_overrides_are_rejected(self):
with self.assertRaises(ValueError):
qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
overrides={"trade_mode": "trading"},
)
with self.assertRaises(ValueError):
qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
overrides={"period": "1m"},
)
with self.assertRaises(ValueError):
qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
overrides={"benchmark": "000300.SH"},
)
def test_non_finite_asset_and_rate_are_rejected(self):
for kwargs in (
{"asset": float("nan")},
{"asset": float("inf")},
{"overrides": {"max_vol_rate": float("nan")}},
{"overrides": {"open_commission": float("inf")}},
):
with self.subTest(kwargs=kwargs):
with self.assertRaisesRegex(ValueError, "finite"):
qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
**kwargs,
)
def test_intraday_period_is_rejected_to_prevent_daily_bar_lookahead(self):
with self.assertRaisesRegex(ValueError, "period must be 1d"):
qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
period="1m",
)
def test_preflight_and_run_use_param_keyword(self):
params = qmt_research_runner.build_qmt_parameters(
stock_code="600000.SH",
start_time="20260102",
end_time="20260130",
)
calls = {}
def run_strategy_file(user_script, *, param):
calls["user_script"] = user_script
calls["param"] = param
return object()
fake_qmttools = types.SimpleNamespace(run_strategy_file=run_strategy_file)
def get_market_data_ex(**kwargs):
calls["data"] = kwargs
return {"600000.SH": FakeFrame()}
fake_xtdata = types.SimpleNamespace(get_market_data_ex=get_market_data_ex)
fake_xtquant = types.SimpleNamespace(__version__="fake")
def import_module(name):
return {
"xtquant": fake_xtquant,
"xtquant.qmttools": fake_qmttools,
"xtquant.xtdata": fake_xtdata,
}[name]
with tempfile.TemporaryDirectory() as temp:
strategy = Path(temp) / "strategy.py"
strategy.write_text("def init(C): pass\n", encoding="ascii")
with mock.patch.object(
qmt_research_runner.importlib.util,
"find_spec",
return_value=object(),
), mock.patch.object(
qmt_research_runner.importlib,
"import_module",
side_effect=import_module,
):
result = qmt_research_runner.run_qmt_backtest(strategy, params)
self.assertIsNotNone(result)
self.assertEqual(calls["param"], params)
self.assertEqual(calls["data"]["start_time"], "20260102000000")
self.assertEqual(calls["data"]["end_time"], "20260130235959")
if __name__ == "__main__":
unittest.main()
+859
View File
@@ -0,0 +1,859 @@
import datetime as dt
import sys
import threading
import unittest
from pathlib import Path
from types import SimpleNamespace
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
sys.path.insert(0, str(ROOT / "src"))
from adapters.xttrader_live import (
IdempotencyConflict,
LiveOrderBlocked,
NotConnectedError,
ReconnectFailed,
XtTraderLiveAdapter,
map_order_status,
)
class Constants:
STOCK_BUY = 23
STOCK_SELL = 24
FIX_PRICE = 11
class FakeTrader:
def __init__(self, connect_code=0, orders=None):
self.connect_code = connect_code
self.callback = None
self.started = False
self.stopped = False
self.order_calls = []
self.cancel_calls = []
self.orders = list(orders or [])
def register_callback(self, callback):
self.callback = callback
def start(self):
self.started = True
def connect(self):
return self.connect_code
def subscribe(self, account):
self.account = account
return 0
def stop(self):
self.stopped = True
def query_stock_asset(self, account):
del account
return SimpleNamespace(
account_id="test", cash=10_000, market_value=20_000, total_asset=30_000
)
def query_stock_orders(self, account, cancelable_only=False):
del account, cancelable_only
return list(self.orders)
def query_stock_positions(self, account):
del account
return [
SimpleNamespace(
account_id="test",
stock_code="600000.SH",
volume=1000,
can_use_volume=700,
market_value=10_000,
open_price=10,
)
]
def query_stock_trades(self, account):
del account
return []
def order_stock_async(self, *args):
self.order_calls.append(args)
return 101
def cancel_order_stock(self, account, order_id):
self.cancel_calls.append((account, order_id))
return 0
def passing_live_decision(**overrides):
decision = {
"authorization_id": "auth-test-001",
"snapshot_id": "snapshot-test-001",
"as_of": dt.datetime.now(dt.timezone.utc).isoformat(),
"reconciliation_safe": True,
"account_allowed": True,
"within_notional_limit": True,
"within_order_rate_limit": True,
"operator_approved": True,
"kill_switch_ready": True,
"compliance_approved": True,
}
decision.update(overrides)
return decision
def adapter_for(trader, allow=False, live_guard=None):
if allow is True and live_guard is None:
live_guard = lambda intent: passing_live_decision()
return XtTraderLiveAdapter(
trader_factory=lambda: trader,
account=SimpleNamespace(account_id="test"),
constants=Constants,
allow_live_orders=allow,
live_guard=live_guard,
)
class XtTraderLiveAdapterTest(unittest.TestCase):
def test_real_status_mapping_uses_domain_spelling(self):
self.assertEqual(map_order_status(48), "NEW")
self.assertEqual(map_order_status(50), "ACCEPTED")
self.assertEqual(map_order_status(51), "CANCEL_PENDING")
self.assertEqual(map_order_status(54), "CANCELLED")
self.assertEqual(map_order_status(55), "PARTIALLY_FILLED")
self.assertEqual(map_order_status(56), "FILLED")
self.assertEqual(map_order_status(57), "REJECTED")
self.assertEqual(map_order_status(999), "UNKNOWN")
def test_default_is_dry_run_and_idempotent_without_broker_call(self):
trader = FakeTrader()
adapter = adapter_for(trader)
first = adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
)
second = adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
)
self.assertEqual(first, second)
self.assertEqual(first["state"], "DRY_RUN")
self.assertEqual(trader.order_calls, [])
def test_allow_live_orders_requires_a_real_bool(self):
trader = FakeTrader()
for value in ("False", "true", 1, None):
with self.subTest(value=value):
with self.assertRaisesRegex(TypeError, "must be a bool"):
adapter_for(trader, allow=value)
self.assertEqual(trader.order_calls, [])
def test_live_mode_requires_callable_guard_but_dry_run_does_not(self):
trader = FakeTrader()
dry_run = XtTraderLiveAdapter(
trader_factory=lambda: trader,
account=SimpleNamespace(account_id="test"),
constants=Constants,
)
self.assertFalse(dry_run.allow_live_orders)
with self.assertRaisesRegex(TypeError, "requires a callable live_guard"):
XtTraderLiveAdapter(
trader_factory=lambda: trader,
account=SimpleNamespace(account_id="test"),
constants=Constants,
allow_live_orders=True,
)
with self.assertRaisesRegex(TypeError, "live_guard must be callable"):
XtTraderLiveAdapter(
trader_factory=lambda: trader,
account=SimpleNamespace(account_id="test"),
constants=Constants,
allow_live_orders=True,
live_guard="allow",
)
def test_idempotency_key_conflict_is_rejected(self):
adapter = adapter_for(FakeTrader())
adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
)
with self.assertRaises(IdempotencyConflict):
adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=200,
limit_price=10.5,
)
def test_non_finite_live_price_is_rejected_before_broker_call(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in (float("nan"), float("inf"), float("-inf")):
with self.subTest(value=value):
with self.assertRaisesRegex(ValueError, "finite and positive"):
adapter.submit_order(
client_order_id="bad-price",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=value,
confirm_live=True,
)
self.assertEqual(trader.order_calls, [])
def test_limit_price_rejects_bool_non_real_and_non_positive(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in (True, False, "10.5", None, 10 + 0j, 0, -1):
with self.subTest(value=value):
with self.assertRaisesRegex(ValueError, "limit_price"):
adapter.submit_order(
client_order_id="strict-price",
symbol="600000.SH",
side="SELL",
quantity=100,
limit_price=value,
confirm_live=True,
)
self.assertEqual(trader.order_calls, [])
def test_submit_confirm_live_requires_a_real_bool(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in ("False", "true", 1, None):
with self.subTest(value=value):
with self.assertRaisesRegex(TypeError, "must be a bool"):
adapter.submit_order(
client_order_id="strict-confirm",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=value,
)
self.assertEqual(trader.order_calls, [])
def test_quantity_rejects_bool_fraction_float_and_non_positive(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in (True, False, 100.0, 100.5, "100", None, 0, -100):
with self.subTest(value=value):
with self.assertRaisesRegex(ValueError, "positive integer"):
adapter.submit_order(
client_order_id="strict-quantity",
symbol="600000.SH",
side="BUY",
quantity=value,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(trader.order_calls, [])
def test_real_order_requires_double_opt_in_and_remark_round_trip(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
with self.assertRaises(LiveOrderBlocked):
adapter.submit_order(
client_order_id="12345678901234567890",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
)
result = adapter.submit_order(
client_order_id="12345678901234567890",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(result["state"], "NEW")
self.assertEqual(len(trader.order_calls), 1)
remark = trader.order_calls[0][-1]
self.assertEqual(remark, "q60:12345678901234567890")
self.assertLessEqual(len(remark), 24)
self.assertEqual(
result["live_policy"]["authorization_id"], "auth-test-001"
)
def test_live_guard_receives_normalized_submit_and_cancel_intents(self):
intents = []
def guard(intent):
intents.append(intent)
return passing_live_decision(
authorization_id=f"auth-{len(intents)}"
)
trader = FakeTrader()
adapter = adapter_for(trader, allow=True, live_guard=guard)
adapter.connect()
result = adapter.submit_order(
client_order_id="guard-intent",
symbol="600000.SH",
side="buy",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
adapter.cancel_order(88, confirm_live=True)
self.assertEqual(
intents,
[
{
"action": "SUBMIT_ORDER",
"account_id": "test",
"client_order_id": "guard-intent",
"symbol": "600000.XSHG",
"side": "BUY",
"quantity": 100,
"limit_price": 10.5,
"notional": 1050.0,
"strategy_name": "quant60",
},
{
"action": "CANCEL_ORDER",
"account_id": "test",
"broker_order_id": 88,
"strategy_name": "quant60",
},
],
)
self.assertEqual(result["live_policy"]["authorization_id"], "auth-1")
self.assertEqual(len(trader.order_calls), 1)
self.assertEqual(len(trader.cancel_calls), 1)
audit = adapter.live_authorization_audit
self.assertEqual(len(audit), 2)
self.assertEqual(audit[0]["decision"]["account_id"], "test")
self.assertEqual(audit[1]["intent"]["action"], "CANCEL_ORDER")
self.assertEqual(audit[1]["outcome"], "BROKER_ACCEPTED")
def test_live_guard_is_bound_to_a_non_empty_account_id(self):
trader = FakeTrader()
calls = []
adapter = XtTraderLiveAdapter(
trader_factory=lambda: trader,
account=SimpleNamespace(account_id=" "),
constants=Constants,
allow_live_orders=True,
live_guard=lambda intent: (
calls.append(intent) or passing_live_decision()
),
)
with self.assertRaisesRegex(NotConnectedError, "account_id"):
adapter.connect()
self.assertEqual(calls, [])
self.assertEqual(trader.order_calls, [])
def test_live_guard_missing_false_non_bool_or_exception_blocks_submit(self):
def raising_guard(intent):
del intent
raise RuntimeError("policy backend unavailable")
cases = [
(
"missing",
lambda intent: {
key: value
for key, value in passing_live_decision().items()
if key != "compliance_approved"
},
),
(
"false",
lambda intent: passing_live_decision(
reconciliation_safe=False
),
),
(
"non-bool",
lambda intent: passing_live_decision(account_allowed=1),
),
(
"empty-id",
lambda intent: passing_live_decision(
authorization_id=" "
),
),
("exception", raising_guard),
]
for name, guard in cases:
with self.subTest(name=name):
trader = FakeTrader()
adapter = adapter_for(
trader, allow=True, live_guard=guard
)
adapter.connect()
with self.assertRaises(LiveOrderBlocked):
adapter.submit_order(
client_order_id=f"blocked-{name}",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(trader.order_calls, [])
def test_live_guard_rejects_stale_naive_and_future_decisions(self):
now = dt.datetime.now(dt.timezone.utc)
cases = {
"stale": (now - dt.timedelta(seconds=61)).isoformat(),
"naive": now.replace(tzinfo=None).isoformat(),
"future": (now + dt.timedelta(seconds=6)).isoformat(),
}
for name, as_of in cases.items():
with self.subTest(name=name):
trader = FakeTrader()
adapter = adapter_for(
trader,
allow=True,
live_guard=lambda intent, value=as_of: (
passing_live_decision(as_of=value)
),
)
adapter.connect()
with self.assertRaises(LiveOrderBlocked):
adapter.submit_order(
client_order_id=f"clock-{name}",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(trader.order_calls, [])
def test_live_guard_failure_blocks_cancel_before_broker(self):
trader = FakeTrader()
adapter = adapter_for(
trader,
allow=True,
live_guard=lambda intent: passing_live_decision(
kill_switch_ready=False
),
)
adapter.connect()
with self.assertRaisesRegex(
LiveOrderBlocked, "kill_switch_ready"
):
adapter.cancel_order(88, confirm_live=True)
self.assertEqual(trader.cancel_calls, [])
def test_concurrent_same_client_id_submits_once(self):
class BlockingTrader(FakeTrader):
def __init__(self):
super().__init__()
self.submit_entered = threading.Event()
self.release_submit = threading.Event()
def order_stock_async(self, *args):
self.order_calls.append(args)
self.submit_entered.set()
if not self.release_submit.wait(2):
raise AssertionError("test did not release broker submit")
return 101
trader = BlockingTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
results = []
failures = []
def submit():
try:
results.append(
adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
)
except BaseException as exc:
failures.append(exc)
first = threading.Thread(target=submit)
first.start()
self.assertTrue(trader.submit_entered.wait(1))
# The broker boundary is inside the idempotency critical section.
acquired = adapter._lock.acquire(blocking=False)
if acquired:
adapter._lock.release()
self.assertFalse(acquired)
second = threading.Thread(target=submit)
second.start()
trader.release_submit.set()
first.join(2)
second.join(2)
self.assertFalse(first.is_alive())
self.assertFalse(second.is_alive())
self.assertEqual(failures, [])
self.assertEqual(len(trader.order_calls), 1)
self.assertEqual(len(results), 2)
self.assertEqual(results[0], results[1])
def test_synchronous_order_error_matches_pre_submit_reservation(self):
class SynchronousErrorTrader(FakeTrader):
def order_stock_async(self, *args):
self.order_calls.append(args)
self.callback.on_order_error(
SimpleNamespace(
order_id=0,
order_remark=args[-1],
error_id=42,
error_msg="synchronous rejection",
)
)
return 101
trader = SynchronousErrorTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
result = adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(result["state"], "REJECTED")
self.assertEqual(result["error_id"], 42)
self.assertEqual(len(trader.order_calls), 1)
def test_synchronous_order_error_by_seq_is_drained_from_orphans(self):
class SynchronousErrorTrader(FakeTrader):
def order_stock_async(self, *args):
self.order_calls.append(args)
self.callback.on_order_error(
SimpleNamespace(
order_id=None,
order_remark="",
seq=101,
error_id=43,
error_msg="fast seq rejection",
)
)
return 101
trader = SynchronousErrorTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
result = adapter.submit_order(
client_order_id="decision-002",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(result["state"], "REJECTED")
self.assertEqual(result["error_id"], 43)
self.assertEqual(adapter._orphan_order_errors, [])
def test_malformed_async_seq_never_leaves_submitting_reservation(self):
class MalformedSeqTrader(FakeTrader):
def __init__(self, seq):
super().__init__()
self.seq = seq
def order_stock_async(self, *args):
self.order_calls.append(args)
return self.seq
for index, seq in enumerate(("bad", 1.5, True, object())):
with self.subTest(seq=repr(seq)):
trader = MalformedSeqTrader(seq)
adapter = adapter_for(trader, allow=True)
adapter.connect()
client_id = f"bad-seq-{index}"
with self.assertRaisesRegex(
Exception, "malformed async request sequence"
):
adapter.submit_order(
client_order_id=client_id,
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
record = adapter.journal.get(client_id)
self.assertEqual(record["state"], "UNKNOWN")
self.assertNotEqual(record["state"], "SUBMITTING")
self.assertEqual(len(trader.order_calls), 1)
def test_query_normalizes_position_and_disconnect_fails_fast(self):
trader = FakeTrader()
adapter = adapter_for(trader)
adapter.connect()
position = adapter.query_positions()[0]
self.assertEqual(position["sellable"], 700)
self.assertEqual(position["symbol"], "600000.XSHG")
trader.callback.on_disconnected()
with self.assertRaises(NotConnectedError):
adapter.query_orders()
def test_connect_recovers_idempotency_from_broker_remark(self):
broker_order = SimpleNamespace(
account_id="test",
order_id=88,
order_sysid="sys-88",
order_remark="q60:decision-001",
stock_code="600000.SH",
order_type=23,
order_volume=100,
traded_volume=0,
price=10.5,
traded_price=0,
order_status=50,
status_msg="",
)
trader = FakeTrader(orders=[broker_order])
adapter = adapter_for(trader, allow=True)
adapter.connect()
recovered = adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
self.assertEqual(recovered["broker_order_id"], 88)
self.assertEqual(recovered["state"], "ACCEPTED")
self.assertEqual(trader.order_calls, [])
def test_broker_payload_conflict_for_same_remark_fails_closed(self):
orders = [
SimpleNamespace(
account_id="test",
order_id=88 + index,
order_sysid="sys",
order_remark="q60:decision-001",
stock_code="600000.SH",
order_type=23,
order_volume=quantity,
traded_volume=0,
price=10.5,
traded_price=0,
order_status=50,
status_msg="",
)
for index, quantity in enumerate((100, 200))
]
adapter = adapter_for(FakeTrader(orders=orders), allow=True)
with self.assertRaises(IdempotencyConflict):
adapter.connect()
self.assertNotEqual(adapter.state, "READY")
def test_order_error_updates_matching_journal_to_rejected(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
adapter.submit_order(
client_order_id="decision-001",
symbol="600000.SH",
side="BUY",
quantity=100,
limit_price=10.5,
confirm_live=True,
)
trader.callback.on_order_error(
SimpleNamespace(
order_id=999,
order_remark="q60:decision-001",
error_id=42,
error_msg="rejected",
)
)
record = adapter.journal.get("decision-001")
self.assertEqual(record["state"], "REJECTED")
self.assertEqual(record["error_id"], 42)
def test_cancel_none_is_not_reported_as_success(self):
trader = FakeTrader()
trader.cancel_order_stock = lambda account, order_id: None
adapter = adapter_for(trader, allow=True)
adapter.connect()
with self.assertRaisesRegex(Exception, "cancel failed"):
adapter.cancel_order(88, confirm_live=True)
def test_cancel_confirm_live_requires_a_real_bool(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in ("False", "true", 1, None):
with self.subTest(value=value):
with self.assertRaisesRegex(TypeError, "must be a bool"):
adapter.cancel_order(88, confirm_live=value)
self.assertEqual(trader.cancel_calls, [])
def test_cancel_order_id_requires_exact_positive_integer(self):
trader = FakeTrader()
adapter = adapter_for(trader, allow=True)
adapter.connect()
for value in (True, False, 88.0, 88.9, "88", None, 0, -1):
with self.subTest(value=value):
with self.assertRaisesRegex(
ValueError, "positive integer"
):
adapter.cancel_order(value, confirm_live=True)
self.assertEqual(trader.cancel_calls, [])
def test_stale_order_callback_cannot_regress_filled_terminal_state(self):
filled = SimpleNamespace(
account_id="test",
order_id=88,
order_sysid="sys-88",
order_remark="q60:decision-001",
stock_code="600000.SH",
order_type=23,
order_volume=100,
traded_volume=100,
price=10.5,
traded_price=10.5,
order_status=56,
status_msg="filled",
)
stale = SimpleNamespace(
account_id="test",
order_id=88,
order_sysid="sys-88",
order_remark="q60:decision-001",
stock_code="600000.SH",
order_type=23,
order_volume=100,
traded_volume=0,
price=10.5,
traded_price=0,
order_status=50,
status_msg="stale accepted",
)
trader = FakeTrader(orders=[filled])
adapter = adapter_for(trader)
adapter.connect()
# An exact duplicate is harmless and idempotent.
trader.callback.on_stock_order(filled)
with self.assertRaisesRegex(
IdempotencyConflict, "cumulative fill regressed"
):
trader.callback.on_stock_order(stale)
record = adapter.journal.get("decision-001")
self.assertEqual(adapter.state, "DEGRADED")
self.assertEqual(record["state"], "FILLED")
self.assertEqual(record["filled_quantity"], 100)
self.assertEqual(adapter._orders[88]["state"], "FILLED")
def test_configured_account_anchors_every_broker_query(self):
class WrongAccountTrader(FakeTrader):
def query_stock_asset(self, account):
del account
return SimpleNamespace(
account_id="wrong",
cash=10_000,
market_value=20_000,
total_asset=30_000,
)
trader = WrongAccountTrader()
adapter = adapter_for(trader)
with self.assertRaisesRegex(
NotConnectedError,
"configured account",
):
adapter.connect()
self.assertEqual(adapter.state, "FAILED")
self.assertTrue(trader.stopped)
def test_trade_query_preserves_account_side_and_stable_trade_id(self):
class TradeTrader(FakeTrader):
def query_stock_trades(self, account):
del account
return [
SimpleNamespace(
account_id="test",
traded_id="trade-1",
order_id=88,
stock_code="600000.SH",
order_type=23,
traded_volume=100,
traded_price=10.5,
traded_amount=1050,
)
]
adapter = adapter_for(TradeTrader())
adapter.connect()
self.assertEqual(
adapter.query_trades(),
[
{
"account_id": "test",
"trade_id": "trade-1",
"broker_order_id": 88,
"symbol": "600000.XSHG",
"side": "BUY",
"quantity": 100,
"price": 10.5,
"amount": 1050.0,
}
],
)
def test_reconnect_uses_fresh_instance_and_exhausts_bounded_attempts(self):
created = []
def factory():
trader = FakeTrader(connect_code=9)
created.append(trader)
return trader
adapter = XtTraderLiveAdapter(
trader_factory=factory,
account=SimpleNamespace(account_id="test"),
constants=Constants,
)
with self.assertRaises(ReconnectFailed):
adapter.reconnect(attempts=2)
self.assertEqual(len(created), 2)
self.assertEqual(adapter.state, "FAILED")
def test_bj_is_explicitly_outside_v1_live_adapter(self):
adapter = adapter_for(FakeTrader())
with self.assertRaises(ValueError):
adapter.submit_order(
client_order_id="bj",
symbol="430001.BJ",
side="BUY",
quantity=100,
limit_price=10,
)
if __name__ == "__main__":
unittest.main()
+212
View File
@@ -0,0 +1,212 @@
import hashlib
import json
import tempfile
import unittest
from datetime import datetime
from pathlib import Path
from quant60.backtest import BacktestEngine, verify_artifact_manifest
from quant60.cli import main
from quant60.config import config_from_dict, load_config
from quant60.ledger import HashChainLedger
from quant60.synthetic import generate_synthetic_bars
PROJECT = Path(__file__).resolve().parents[1]
class BacktestCliTests(unittest.TestCase):
def test_empty_config_uses_valid_defaults(self):
config = config_from_dict({})
config.validate()
def test_non_finite_config_values_are_rejected(self):
cases = (
{"initial_cash": "NaN"},
{"fees": {"commission_rate": float("inf")}},
{"execution": {"participation_rate": float("nan")}},
{"execution": {"slippage_bps": float("inf")}},
{"strategy": {"max_weight": float("nan")}},
{"strategy": {"gross_target": float("inf")}},
{"strategy": {"cash_buffer": float("nan")}},
{"initial_cash": True},
{"execution": {"lot_size": 100.0}},
{"execution": {"cancel_open_orders_at_end": 1}},
{"strategy": {"lookback": True}},
{"strategy": {"rebalance_every": 5.0}},
{"synthetic": {"trading_days": 90.0}},
{"symbols": "600000.XSHG"},
)
for raw in cases:
with self.subTest(raw=raw):
with self.assertRaises(ValueError):
config_from_dict(raw)
def test_backtest_is_byte_deterministic(self):
config = load_config(PROJECT / "configs" / "baseline.json")
bars = generate_synthetic_bars(
config.symbols,
config.synthetic.start_date,
config.synthetic.trading_days,
config.synthetic.seed,
config.synthetic.base_volume,
)
first = BacktestEngine(config).run(bars)
second = BacktestEngine(config).run(bars)
self.assertEqual(first.run_id, second.run_id)
self.assertEqual(first.report, second.report)
with tempfile.TemporaryDirectory() as left, tempfile.TemporaryDirectory() as right:
left_paths = first.write_artifacts(left)
right_paths = second.write_artifacts(right)
for key in left_paths:
left_hash = hashlib.sha256(left_paths[key].read_bytes()).hexdigest()
right_hash = hashlib.sha256(right_paths[key].read_bytes()).hexdigest()
self.assertEqual(left_hash, right_hash, key)
self.assertTrue(HashChainLedger.read_jsonl(left_paths["events"]).verify())
report = json.loads(left_paths["report"].read_text(encoding="utf-8"))
self.assertFalse(report["investment_value_claim"])
self.assertGreater(report["fill_count"], 0)
records = [
json.loads(line)
for line in left_paths["events"].read_text(
encoding="utf-8"
).splitlines()
]
timestamps = [
datetime.fromisoformat(record["timestamp"]) for record in records
]
self.assertEqual(timestamps, sorted(timestamps))
def test_decisions_use_first_trading_session_close_of_each_week(self):
config = load_config(PROJECT / "configs" / "baseline.json")
bars = generate_synthetic_bars(
config.symbols,
config.synthetic.start_date,
config.synthetic.trading_days,
config.synthetic.seed,
config.synthetic.base_volume,
)
result = BacktestEngine(config).run(bars)
sessions = sorted({item.trading_date for item in bars})
first_by_week = {}
for session in sessions:
first_by_week.setdefault(session.isocalendar()[:2], session)
decision_dates = {
datetime.fromisoformat(item["as_of"]).date()
for item in result.targets
}
self.assertTrue(decision_dates)
for decision_date in decision_dates:
self.assertEqual(
decision_date,
first_by_week[decision_date.isocalendar()[:2]],
)
def test_incomplete_market_session_fails_closed(self):
config = load_config(PROJECT / "configs" / "baseline.json")
bars = generate_synthetic_bars(
config.symbols,
config.synthetic.start_date,
config.synthetic.trading_days,
config.synthetic.seed,
config.synthetic.base_volume,
)
removed = bars.pop(len(config.symbols) * 30)
with self.assertRaisesRegex(
ValueError,
"incomplete market-data session.*" + removed.symbol.replace(".", r"\."),
):
BacktestEngine(config).run(bars)
def test_cli_smoke_and_ledger_verify(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "run"
self.assertEqual(
main(
[
"smoke",
"--config",
str(PROJECT / "configs" / "baseline.json"),
"--output",
str(output),
]
),
0,
)
self.assertEqual(main(["verify-ledger", str(output / "events.jsonl")]), 0)
self.assertEqual(
main(["verify-manifest", str(output / "manifest.json")]),
0,
)
self.assertTrue(
verify_artifact_manifest(output / "manifest.json")["ok"]
)
def test_manifest_verifier_rejects_tampered_or_incomplete_runs(self):
config = load_config(PROJECT / "configs" / "baseline.json")
bars = generate_synthetic_bars(
config.symbols,
config.synthetic.start_date,
config.synthetic.trading_days,
config.synthetic.seed,
config.synthetic.base_volume,
)
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "run"
BacktestEngine(config).run(bars).write_artifacts(output)
report_path = output / "report.json"
report_path.write_bytes(report_path.read_bytes() + b" ")
with self.assertRaisesRegex(ValueError, "report artifact hash mismatch"):
verify_artifact_manifest(output / "manifest.json")
(output / ".quant60-incomplete").write_text(
"interrupted\n",
encoding="ascii",
)
with self.assertRaisesRegex(ValueError, "publication is incomplete"):
verify_artifact_manifest(output / "manifest.json")
second_output = Path(directory) / "second-run"
BacktestEngine(config).run(bars).write_artifacts(second_output)
manifest_path = second_output / "manifest.json"
unexpected = second_output / "untracked-result.txt"
unexpected.write_text("not part of the run\n", encoding="utf-8")
with self.assertRaisesRegex(
ValueError,
"artifact directory is not an exact completed set",
):
verify_artifact_manifest(manifest_path)
unexpected.unlink()
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
manifest["engine_sources"]["portable_core.py"] = "0" * 64
manifest_path.write_text(
json.dumps(manifest, sort_keys=True),
encoding="utf-8",
)
with self.assertRaisesRegex(
ValueError,
"engine_sources aggregate hash mismatch",
):
verify_artifact_manifest(manifest_path)
def test_cli_backtest_alias(self):
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "run"
self.assertEqual(
main(
[
"backtest",
"--config",
str(PROJECT / "configs" / "baseline.json"),
"--output",
str(output),
]
),
0,
)
self.assertTrue((output / "report.json").is_file())
if __name__ == "__main__":
unittest.main()
+155
View File
@@ -0,0 +1,155 @@
import unittest
from datetime import date
from quant60.config import FeeConfig
from quant60.costs import (
CostModelParameters,
DatedFeeRule,
VersionedFeeSchedule,
forecast_trade_cost,
)
from quant60.domain import Side
from quant60.optimizer import (
OptimizationError,
PortfolioCandidate,
PortfolioConstraints,
optimize_cvxpy,
optimize_deterministic,
)
from quant60.universe import UniverseState
class CostOptimizerTests(unittest.TestCase):
def test_fee_schedule_switches_by_effective_date(self):
first = DatedFeeRule(
date(2024, 1, 1),
date(2024, 6, 30),
FeeConfig(stamp_duty_rate=0.001),
"old",
)
second = DatedFeeRule(
date(2024, 7, 1),
None,
FeeConfig(stamp_duty_rate=0.0005),
"new",
)
schedule = VersionedFeeSchedule([second, first])
self.assertEqual(schedule.at(date(2024, 6, 30)).version, "old")
self.assertEqual(schedule.at(date(2024, 7, 1)).version, "new")
def test_exact_minimum_fee_and_capacity_are_visible(self):
rule = DatedFeeRule(
date(2024, 1, 1),
None,
FeeConfig(
commission_rate=0.0002,
minimum_commission=5,
stamp_duty_rate=0.0005,
transfer_fee_rate=0.00001,
),
"account-v1",
)
forecast = forecast_trade_cost(
symbol="600000.SH",
side=Side.BUY,
quantity=100,
price=10,
adv_quantity=500,
daily_volatility=0.02,
fee_rule=rule,
parameters=CostModelParameters(max_participation=0.10),
)
self.assertGreaterEqual(forecast.explicit_fees, 5)
self.assertTrue(forecast.capacity_breached)
self.assertLess(forecast.low, forecast.base)
self.assertLess(forecast.base, forecast.high)
def _candidates(self):
return [
PortfolioCandidate(
"600000.SH",
score=0.04,
variance=0.04,
current_weight=0.10,
cost_bps=10,
max_adv_weight=0.30,
industry="bank",
),
PortfolioCandidate(
"000001.SZ",
score=0.03,
variance=0.03,
current_weight=0.10,
cost_bps=12,
max_adv_weight=0.30,
industry="bank",
),
PortfolioCandidate(
"300750.SZ",
score=0.02,
variance=0.05,
current_weight=0.05,
cost_bps=15,
max_adv_weight=0.30,
industry="industry",
state=UniverseState.FROZEN,
),
PortfolioCandidate(
"000333.SZ",
score=-0.01,
variance=0.04,
current_weight=0.10,
cost_bps=10,
max_adv_weight=0.30,
industry="consumer",
state=UniverseState.SELL_ONLY,
),
]
def test_deterministic_optimizer_honors_frozen_caps_and_turnover(self):
constraints = PortfolioConstraints(
gross_target=0.70,
single_name_cap=0.30,
industry_cap=0.45,
one_way_turnover_cap=0.10,
)
result = optimize_deterministic(self._candidates(), constraints)
self.assertAlmostEqual(result.weights["300750.XSHE"], 0.05)
self.assertLessEqual(result.weights["000333.XSHE"], 0.10)
self.assertLessEqual(result.one_way_turnover, 0.10 + 1e-9)
self.assertLessEqual(result.gross_weight, 0.70 + 1e-9)
self.assertEqual(result.solver, "deterministic_reference")
def test_held_excluded_candidate_fails_closed(self):
candidate = PortfolioCandidate(
"600000.SH",
score=0,
variance=0.04,
current_weight=0.1,
cost_bps=1,
max_adv_weight=0.2,
industry="bank",
state=UniverseState.EXCLUDED,
)
with self.assertRaisesRegex(OptimizationError, "cannot disappear"):
optimize_deterministic([candidate])
def test_cvxpy_path_when_dependency_is_available(self):
constraints = PortfolioConstraints(
gross_target=0.70,
single_name_cap=0.30,
industry_cap=0.45,
one_way_turnover_cap=0.20,
)
try:
result = optimize_cvxpy(self._candidates(), constraints)
except OptimizationError as exc:
if "CVXPY is unavailable" in str(exc):
self.skipTest(str(exc))
raise
self.assertIn(result.status, {"OPTIMAL", "OPTIMAL_INACCURATE"})
self.assertLessEqual(result.one_way_turnover, 0.20 + 1e-6)
if __name__ == "__main__":
unittest.main()
+418
View File
@@ -0,0 +1,418 @@
import json
import tempfile
import unittest
from datetime import date, datetime, timezone
from pathlib import Path
from adapters.jqdata_local import (
JQDataUnavailableError,
authenticate,
fetch_index_daily_snapshot,
)
from quant60.data_snapshot import (
CanonicalBarRecord,
DataQualityError,
verify_data_snapshot,
)
class FakeFrame:
def __init__(self, rows):
self.rows = rows
def iterrows(self):
return iter(self.rows)
class FakeJQData:
__version__ = "fake-1"
def __init__(self):
self.authenticated = False
self.last_get_bars_kwargs = None
self.last_get_extras_kwargs = None
self.trade_day_queries = []
self.full_calendar_queries = 0
def auth(self, username, password):
self.authenticated = username == "user" and password == "password"
def is_auth(self):
return self.authenticated
def get_trade_days(self, *, start_date, end_date):
self.trade_day_queries.append(
{
"start_date": start_date,
"end_date": end_date,
}
)
return [start_date, end_date]
def get_all_trade_days(self):
self.full_calendar_queries += 1
return [
date(2024, 1, 2),
date(2024, 1, 3),
date(2024, 1, 4),
]
def get_index_stocks(self, index_symbol, *, date):
return ["600000.XSHG", "000001.XSHE"]
def get_extras(
self,
info,
security_list,
*,
start_date,
end_date,
df,
):
self.last_get_extras_kwargs = {
"info": info,
"security_list": list(security_list),
"start_date": start_date,
"end_date": end_date,
"df": df,
}
return FakeFrame(
[
(
start_date,
{symbol: False for symbol in security_list},
),
(
end_date,
{symbol: False for symbol in security_list},
),
]
)
def get_bars(self, symbol, **kwargs):
self.last_get_bars_kwargs = dict(kwargs)
offset = 0 if symbol.startswith("600000") else 10
return FakeFrame(
[
(
0,
{
"date": kwargs["start_dt"],
"open": 10 + offset,
"high": 11 + offset,
"low": 9 + offset,
"close": 10.5 + offset,
"volume": 1000,
"money": 10_500 + offset * 1000,
"factor": 1.0,
"paused": False,
"high_limit": 11.5 + offset,
"low_limit": 8.5 + offset,
"pre_close": 10 + offset,
},
),
(
1,
{
"date": kwargs["end_dt"],
"open": 10.5 + offset,
"high": 11.5 + offset,
"low": 10 + offset,
"close": 11 + offset,
"volume": 1200,
"money": 13_200 + offset * 1200,
"factor": 1.0,
"paused": False,
"high_limit": 12 + offset,
"low_limit": 9 + offset,
"pre_close": 10.5 + offset,
},
),
]
)
class DataJQDataTests(unittest.TestCase):
def test_authentication_never_accepts_failed_login(self):
sdk = FakeJQData()
with self.assertRaisesRegex(JQDataUnavailableError, "not accepted"):
authenticate(
sdk,
username="user",
password="wrong",
interactive=False,
)
authenticate(
sdk,
username="user",
password="password",
interactive=False,
)
self.assertTrue(sdk.authenticated)
def test_fake_jqdata_snapshot_is_canonical_and_verifiable(self):
sdk = FakeJQData()
authenticate(
sdk,
username="user",
password="password",
interactive=False,
)
with tempfile.TemporaryDirectory() as directory:
paths = fetch_index_daily_snapshot(
sdk,
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(2024, 1, 4, tzinfo=timezone.utc),
)
verified = verify_data_snapshot(paths["manifest"])
self.assertTrue(verified["ok"])
self.assertEqual(verified["bar_count"], 4)
self.assertEqual(verified["membership_count"], 4)
self.assertEqual(
sdk.last_get_bars_kwargs["end_dt"].date(),
date(2024, 1, 3),
)
self.assertEqual(
sdk.last_get_bars_kwargs["end_dt"].time(),
datetime.strptime("23:59:59", "%H:%M:%S").time(),
)
self.assertIs(
sdk.last_get_bars_kwargs["include_now"],
True,
)
self.assertEqual(
sdk.last_get_extras_kwargs["info"],
"is_st",
)
manifest = json.loads(
Path(paths["manifest"]).read_text(encoding="utf-8")
)
self.assertEqual(
manifest["next_trading_session"],
"2024-01-04",
)
self.assertEqual(
manifest["query"]["trading_sessions"],
["2024-01-02", "2024-01-03", "2024-01-04"],
)
self.assertEqual(sdk.full_calendar_queries, 1)
content = Path(paths["manifest"]).read_text(encoding="utf-8")
self.assertNotIn("password", content)
self.assertNotIn("user", content)
bars = (
Path(directory) / "bars.jsonl"
).read_text(encoding="utf-8")
self.assertIn('"adjustment_factor":1.0', bars)
self.assertIn('"limit_up":', bars)
def test_next_session_comes_from_provider_calendar_across_holiday(self):
class NationalDayCalendarJQData(FakeJQData):
def get_all_trade_days(self):
self.full_calendar_queries += 1
return [
date(2024, 9, 27),
date(2024, 9, 30),
date(2024, 10, 8),
date(2024, 10, 9),
]
sdk = NationalDayCalendarJQData()
with tempfile.TemporaryDirectory() as directory:
paths = fetch_index_daily_snapshot(
sdk,
index_symbol="000905.XSHG",
start_date=date(2024, 9, 27),
end_date=date(2024, 9, 30),
output_dir=directory,
retrieved_at=datetime(2024, 10, 9, tzinfo=timezone.utc),
)
manifest = json.loads(
Path(paths["manifest"]).read_text(encoding="utf-8")
)
self.assertEqual(
manifest["next_trading_session"],
"2024-10-08",
)
self.assertEqual(
manifest["query"]["trading_sessions"][-1],
"2024-10-08",
)
def test_invalid_next_session_calendar_response_fails_closed(self):
class BadCalendarJQData(FakeJQData):
def get_all_trade_days(self):
return [date(2024, 1, 2), date(2024, 1, 3)]
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
JQDataUnavailableError,
"unique next trading session",
):
fetch_index_daily_snapshot(
BadCalendarJQData(),
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
4,
tzinfo=timezone.utc,
),
)
def test_inconsistent_range_and_full_calendars_fail_closed(self):
class MissingMiddleSessionJQData(FakeJQData):
def get_all_trade_days(self):
return [
date(2024, 1, 2),
date(2024, 1, 3),
date(2024, 1, 4),
]
def get_trade_days(self, *, start_date, end_date):
return [start_date]
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
JQDataUnavailableError,
"inconsistent sessions",
):
fetch_index_daily_snapshot(
MissingMiddleSessionJQData(),
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
4,
tzinfo=timezone.utc,
),
)
def test_same_shanghai_day_is_rejected_before_query(self):
sdk = FakeJQData()
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
JQDataUnavailableError,
"24:00 finalization",
):
fetch_index_daily_snapshot(
sdk,
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
3,
6,
tzinfo=timezone.utc,
),
)
self.assertIsNone(sdk.last_get_bars_kwargs)
self.assertIsNone(sdk.last_get_extras_kwargs)
def test_missing_pit_st_status_fails_closed(self):
class MissingStatusJQData(FakeJQData):
def get_extras(self, *args, **kwargs):
frame = super().get_extras(*args, **kwargs)
frame.rows[-1][1].pop("600000.XSHG")
return frame
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
JQDataUnavailableError,
"omitted required field 600000.XSHG",
):
fetch_index_daily_snapshot(
MissingStatusJQData(),
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
4,
tzinfo=timezone.utc,
),
)
def test_missing_required_provider_rule_field_fails_closed(self):
class MissingLimitJQData(FakeJQData):
def get_bars(self, symbol, **kwargs):
frame = super().get_bars(symbol, **kwargs)
del frame.rows[0][1]["high_limit"]
return frame
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
JQDataUnavailableError,
"omitted required field high_limit",
):
fetch_index_daily_snapshot(
MissingLimitJQData(),
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
4,
tzinfo=timezone.utc,
),
)
def test_membership_without_same_session_bar_fails_quality_gate(self):
class MissingEndBarJQData(FakeJQData):
def get_bars(self, symbol, **kwargs):
frame = super().get_bars(symbol, **kwargs)
frame.rows.pop()
return frame
with tempfile.TemporaryDirectory() as directory:
with self.assertRaisesRegex(
DataQualityError,
"membership has no same-session canonical bar",
):
fetch_index_daily_snapshot(
MissingEndBarJQData(),
index_symbol="000905.XSHG",
start_date=date(2024, 1, 2),
end_date=date(2024, 1, 3),
output_dir=directory,
retrieved_at=datetime(
2024,
1,
4,
tzinfo=timezone.utc,
),
)
def test_bad_ohlc_fails_quality_gate(self):
with self.assertRaisesRegex(DataQualityError, "open must lie"):
CanonicalBarRecord(
trading_date=date(2024, 1, 2),
symbol="600000.SH",
open=12,
high=11,
low=9,
close=10,
volume=100,
money=1000,
paused=False,
source="fixture",
)
if __name__ == "__main__":
unittest.main()
+247
View File
@@ -0,0 +1,247 @@
import unittest
from datetime import date
from decimal import Decimal
from quant60.broker import SimBroker
from quant60.config import ExecutionConfig, FeeConfig
from quant60.domain import (
Bar,
Fill,
Order,
OrderStatus,
Portfolio,
Position,
Side,
)
from quant60.ledger import HashChainLedger
def bar(
day,
*,
symbol="600000.XSHG",
price="10",
volume=1000,
suspended=False,
limit_up=None,
limit_down=None,
):
value = Decimal(price)
return Bar(
trading_date=day,
symbol=symbol,
open=value,
high=value,
low=value,
close=value,
volume=volume,
suspended=suspended,
limit_up=Decimal(limit_up) if limit_up else None,
limit_down=Decimal(limit_down) if limit_down else None,
)
class DomainBrokerTests(unittest.TestCase):
def setUp(self):
self.portfolio = Portfolio(Decimal("100000"))
self.ledger = HashChainLedger()
self.broker = SimBroker(
self.portfolio,
ExecutionConfig(
lot_size=100,
participation_rate=0.1,
slippage_bps=0,
),
FeeConfig(
commission_rate=0,
minimum_commission=0,
stamp_duty_rate=0,
transfer_fee_rate=0,
),
self.ledger,
)
def test_duplicate_state_callback_is_idempotent(self):
order = Order("O1", "600000.SH", Side.BUY, 100, date(2024, 1, 2))
order.transition(OrderStatus.ACCEPTED)
order.transition(OrderStatus.ACCEPTED)
self.assertEqual(order.status, OrderStatus.ACCEPTED)
order.transition(OrderStatus.UNKNOWN)
order.transition(OrderStatus.UNKNOWN)
order.transition(OrderStatus.ACCEPTED)
self.assertEqual(order.status, OrderStatus.ACCEPTED)
def test_late_fill_during_cancel_pending_is_accepted(self):
order = Order("O1", "600000.SH", Side.BUY, 200, date(2024, 1, 2))
order.transition(OrderStatus.ACCEPTED)
order.transition(OrderStatus.CANCEL_PENDING)
order.apply_fill(100, Decimal("10"))
self.assertEqual(order.status, OrderStatus.PARTIALLY_FILLED)
self.assertEqual(order.filled_quantity, 100)
order.transition(OrderStatus.CANCEL_PENDING)
order.apply_fill(100, Decimal("10.1"))
self.assertEqual(order.status, OrderStatus.FILLED)
self.assertEqual(order.filled_quantity, 200)
def test_partial_fill_and_t_plus_one(self):
submit_day = date(2024, 1, 2)
fill_day = date(2024, 1, 3)
self.broker.execute_session(
{"600000.XSHG": bar(submit_day, volume=2000)}
)
self.broker.submit("600000.SH", Side.BUY, 500, submit_day)
fills = self.broker.execute_session(
{"600000.XSHG": bar(fill_day, volume=100_000)}
)
self.assertEqual(sum(item.quantity for item in fills), 200)
position = self.portfolio.position("600000.XSHG")
self.assertEqual(position.quantity, 200)
self.assertEqual(position.sellable_quantity, 0)
with self.assertRaises(ValueError):
position.sell(100)
self.portfolio.start_session()
self.assertEqual(position.sellable_quantity, 200)
def test_suspension_and_locked_limit_block(self):
submit_day = date(2024, 1, 2)
next_day = date(2024, 1, 3)
self.broker.execute_session(
{"600000.XSHG": bar(submit_day, volume=1000)}
)
self.broker.submit("600000.SH", Side.BUY, 100, submit_day)
self.assertEqual(
self.broker.execute_session(
{"600000.XSHG": bar(next_day, suspended=True, volume=0)}
),
[],
)
reasons = [
record.payload["reason"]
for record in self.ledger.records
if record.event_type == "ORDER_BLOCKED"
]
self.assertIn("SUSPENDED", reasons)
other = SimBroker(
Portfolio(Decimal("100000")),
self.broker.execution,
self.broker.fees,
HashChainLedger(),
)
other.execute_session(
{"600000.XSHG": bar(submit_day, volume=1000)}
)
other.submit("600000.SH", Side.BUY, 100, submit_day)
self.assertEqual(
other.execute_session(
{
"600000.XSHG": bar(
next_day,
price="11",
limit_up="11",
limit_down="9",
)
}
),
[],
)
def test_open_fill_capacity_uses_only_prior_session_volume(self):
submit_day = date(2024, 1, 2)
fill_day = date(2024, 1, 3)
def executed_quantity(current_day_volume):
broker = SimBroker(
Portfolio(Decimal("100000")),
self.broker.execution,
self.broker.fees,
HashChainLedger(),
)
broker.execute_session(
{"600000.XSHG": bar(submit_day, volume=2000)}
)
broker.submit("600000.SH", Side.BUY, 500, submit_day)
fills = broker.execute_session(
{
"600000.XSHG": bar(
fill_day,
volume=current_day_volume,
)
}
)
return sum(item.quantity for item in fills)
self.assertEqual(executed_quantity(0), 200)
self.assertEqual(executed_quantity(10_000_000), 200)
def test_fill_cash_and_position_conservation(self):
fill = Fill(
"F1",
"O1",
date(2024, 1, 3),
"600000.SH",
Side.BUY,
100,
Decimal("10"),
Decimal("5"),
Decimal("0"),
Decimal("0.01"),
)
self.portfolio.apply_fill(fill)
self.assertEqual(self.portfolio.position("600000.SH").quantity, 100)
self.assertEqual(self.portfolio.cash, Decimal("98994.990000"))
def test_complete_odd_lot_sell_is_accepted_and_filled(self):
submit_day = date(2024, 1, 2)
fill_day = date(2024, 1, 3)
symbol = "600000.XSHG"
self.portfolio.positions[symbol] = Position(
symbol,
quantity=1050,
sellable_quantity=1050,
today_bought=0,
average_cost=Decimal("9"),
)
self.broker.execute_session(
{symbol: bar(submit_day, volume=200_000)}
)
order = self.broker.submit(
symbol,
Side.SELL,
1050,
submit_day,
)
self.assertEqual(order.status, OrderStatus.ACCEPTED)
fills = self.broker.execute_session(
{symbol: bar(fill_day, volume=200_000)}
)
self.assertEqual([item.quantity for item in fills], [1050])
self.assertEqual(order.status, OrderStatus.FILLED)
self.assertEqual(self.portfolio.position(symbol).quantity, 0)
def test_partial_odd_lot_and_star_subminimum_are_rejected(self):
submit_day = date(2024, 1, 2)
symbol = "600000.XSHG"
self.portfolio.positions[symbol] = Position(
symbol,
quantity=1050,
sellable_quantity=1050,
today_bought=0,
average_cost=Decimal("9"),
)
odd = self.broker.submit(symbol, Side.SELL, 50, submit_day)
self.assertEqual(odd.status, OrderStatus.REJECTED)
self.assertEqual(odd.status_reason, "NOT_BOARD_LOT")
star = self.broker.submit(
"688301.XSHG",
Side.BUY,
100,
submit_day,
)
self.assertEqual(star.status, OrderStatus.REJECTED)
self.assertEqual(star.status_reason, "BELOW_MARKET_MINIMUM")
if __name__ == "__main__":
unittest.main()
+104
View File
@@ -0,0 +1,104 @@
import unittest
from datetime import datetime, timedelta, timezone
from quant60.domain import Side
from quant60.execution import (
ExecutionBucket,
ParentOrderIntent,
ResidualPolicy,
market_data_is_fresh,
plan_guarded_twap_pov,
)
UTC8 = timezone(timedelta(hours=8))
class ExecutionPlannerTests(unittest.TestCase):
def _parent(self, quantity=1000):
return ParentOrderIntent(
decision_id="D-20260726",
symbol="600000.SH",
side=Side.BUY,
quantity=quantity,
limit_price=10.0,
window_start=datetime(2026, 7, 27, 9, 35, tzinfo=UTC8),
window_end=datetime(2026, 7, 27, 10, 0, tzinfo=UTC8),
market_data_as_of=datetime(2026, 7, 27, 9, 34, 59, tzinfo=UTC8),
max_participation=0.10,
max_child_quantity=400,
max_child_notional=3000,
residual_policy=ResidualPolicy.DEFER,
)
def test_plan_conserves_quantity_and_honors_all_caps(self):
parent = self._parent()
buckets = [
ExecutionBucket(
datetime(2026, 7, 27, 9, 35 + index * 5, tzinfo=UTC8),
2000,
)
for index in range(5)
]
plan = plan_guarded_twap_pov(parent, buckets)
self.assertEqual(
plan.planned_quantity + plan.residual_quantity,
parent.quantity,
)
self.assertTrue(plan.children)
for child in plan.children:
self.assertLessEqual(child.quantity, 200)
self.assertLessEqual(child.quantity * child.limit_price, 3000)
self.assertLessEqual(
child.quantity,
child.participation_cap_quantity,
)
self.assertEqual(
len({child.child_id for child in plan.children}),
len(plan.children),
)
def test_zero_volume_buckets_leave_explicit_residual(self):
parent = self._parent(quantity=500)
buckets = [
ExecutionBucket(
datetime(2026, 7, 27, 9, 35, tzinfo=UTC8),
0,
)
]
plan = plan_guarded_twap_pov(parent, buckets)
self.assertEqual(plan.planned_quantity, 0)
self.assertEqual(plan.residual_quantity, 500)
self.assertEqual(plan.residual_policy, ResidualPolicy.DEFER)
def test_market_data_freshness_rejects_old_and_future_quotes(self):
now = datetime(2026, 7, 27, 9, 35, tzinfo=UTC8)
self.assertTrue(
market_data_is_fresh(
as_of=now - timedelta(seconds=2),
now=now,
)
)
self.assertFalse(
market_data_is_fresh(
as_of=now - timedelta(seconds=6),
now=now,
)
)
self.assertFalse(
market_data_is_fresh(
as_of=now + timedelta(seconds=2),
now=now,
)
)
def test_parent_identity_is_decision_symbol_side(self):
parent = self._parent()
self.assertEqual(
parent.idempotency_key,
"D-20260726:600000.XSHG:BUY",
)
if __name__ == "__main__":
unittest.main()
+46
View File
@@ -0,0 +1,46 @@
import hashlib
import json
import tempfile
import unittest
from pathlib import Path
from quant60.experiment import (
run_research_smoke,
verify_research_manifest,
write_research_artifacts,
)
class ResearchExperimentTests(unittest.TestCase):
def test_research_smoke_is_deterministic_and_verifiable(self):
first = run_research_smoke(trading_days=260, seed=11)
second = run_research_smoke(trading_days=260, seed=11)
self.assertEqual(first, second)
self.assertGreater(first["report"]["fold_count"], 0)
self.assertFalse(first["report"]["investment_value_claim"])
self.assertTrue(first["targets"])
with tempfile.TemporaryDirectory() as left_dir, tempfile.TemporaryDirectory() as right_dir:
left = write_research_artifacts(first, left_dir)
right = write_research_artifacts(second, right_dir)
for name in left:
self.assertEqual(
hashlib.sha256(Path(left[name]).read_bytes()).hexdigest(),
hashlib.sha256(Path(right[name]).read_bytes()).hexdigest(),
name,
)
verified = verify_research_manifest(left["manifest"])
self.assertTrue(verified["ok"])
def test_research_verifier_rejects_tamper(self):
result = run_research_smoke(trading_days=260, seed=12)
with tempfile.TemporaryDirectory() as directory:
paths = write_research_artifacts(result, directory)
report = Path(paths["report"])
report.write_bytes(report.read_bytes() + b" ")
with self.assertRaisesRegex(ValueError, "hash mismatch"):
verify_research_manifest(paths["manifest"])
if __name__ == "__main__":
unittest.main()
+91
View File
@@ -0,0 +1,91 @@
import unittest
from quant60.factor_risk import (
FactorExposureSnapshot,
RiskModelError,
build_factor_risk_forecast,
estimate_factor_returns,
factor_stress_loss,
portfolio_factor_variance,
)
class FactorRiskTests(unittest.TestCase):
def _exposure(self):
return FactorExposureSnapshot(
as_of="2026-07-24T15:00:00+08:00",
exposures={
"600000.SH": {"market": 1.0, "style": 1.0},
"000001.SZ": {"market": 1.0, "style": 1.0},
"300750.SZ": {"market": 1.0, "style": -1.0},
},
)
def _forecast(self):
return build_factor_risk_forecast(
factor_return_history=[
{"market": 0.01, "style": 0.02},
{"market": -0.01, "style": -0.02},
{"market": 0.02, "style": 0.01},
{"market": -0.02, "style": -0.01},
],
residual_history={
"600000.SH": [0.01, -0.01, 0.005],
"000001.SZ": [0.01, -0.01, 0.005],
"300750.SZ": [0.01, -0.01, 0.005],
},
shrinkage=0.2,
)
def test_factor_return_fit_and_residuals_are_complete(self):
fit = estimate_factor_returns(
returns={
"600000.SH": 0.03,
"000001.SZ": 0.02,
"300750.SZ": -0.01,
},
exposure=self._exposure(),
)
self.assertEqual(set(fit.factor_returns), {"market", "style"})
self.assertEqual(len(fit.residuals), 3)
def test_correlated_pair_has_more_risk_than_style_diversified_pair(self):
exposure = self._exposure()
forecast = self._forecast()
correlated = portfolio_factor_variance(
portfolio_weights={
"600000.SH": 0.5,
"000001.SZ": 0.5,
},
exposure=exposure,
forecast=forecast,
)
diversified = portfolio_factor_variance(
portfolio_weights={
"600000.SH": 0.5,
"300750.SZ": 0.5,
},
exposure=exposure,
forecast=forecast,
)
self.assertGreater(correlated, diversified)
def test_missing_risk_input_fails_closed(self):
with self.assertRaisesRegex(RiskModelError, "missing"):
portfolio_factor_variance(
portfolio_weights={"000333.SZ": 1.0},
exposure=self._exposure(),
forecast=self._forecast(),
)
def test_market_down_stress_is_positive_loss(self):
loss = factor_stress_loss(
portfolio_weights={"600000.SH": 0.5, "300750.SZ": 0.5},
exposure=self._exposure(),
factor_shocks={"market": -0.10},
)
self.assertAlmostEqual(loss, 0.10)
if __name__ == "__main__":
unittest.main()
+83
View File
@@ -0,0 +1,83 @@
import math
import unittest
from datetime import date
from quant60.features import (
TrainOnlyFeaturePreprocessor,
build_executable_label,
compute_price_features,
)
from quant60.synthetic import generate_synthetic_bars
class FeatureTests(unittest.TestCase):
def setUp(self):
self.bars = generate_synthetic_bars(
["600000.XSHG"],
start_date="2024-01-02",
trading_days_count=150,
seed=7,
include_market_events=False,
)
def test_feature_as_of_does_not_change_when_future_bars_are_appended(self):
decision = self.bars[99].trading_date
early = compute_price_features(self.bars[:100], as_of=decision)
with_future = compute_price_features(self.bars, as_of=decision)
self.assertEqual(early.values, with_future.values)
self.assertEqual(early.history_end, decision)
def test_long_window_features_are_explicitly_missing(self):
snapshot = compute_price_features(self.bars[:30])
self.assertIsNone(snapshot.values["momentum_60"])
self.assertIsNotNone(snapshot.values["momentum_20"])
def test_preprocessor_uses_training_statistics_and_missing_indicator(self):
processor = TrainOnlyFeaturePreprocessor(winsor_fraction=0).fit(
[{"a": 1.0}, {"a": 2.0}, {"a": 3.0}]
)
transformed = processor.transform_one({"a": None})
self.assertEqual(transformed["a__missing"], 1.0)
self.assertAlmostEqual(transformed["a"], 0.0)
extreme = processor.transform_one({"a": 1_000_000.0})
expected = processor.transform_one({"a": 3.0})
self.assertEqual(extreme["a"], expected["a"])
def test_executable_label_uses_sessions_after_decision(self):
decision_index = 80
label = build_executable_label(
self.bars,
decision_date=self.bars[decision_index].trading_date,
horizon_sessions=5,
)
self.assertEqual(label.status, "OK")
self.assertEqual(
label.entry_date,
self.bars[decision_index + 1].trading_date,
)
self.assertEqual(
label.exit_date,
self.bars[decision_index + 6].trading_date,
)
self.assertTrue(math.isfinite(label.value))
def test_suspended_entry_is_censored_not_dropped(self):
bars = generate_synthetic_bars(
["600000.XSHG"],
start_date=date(2024, 1, 2),
trading_days_count=40,
seed=7,
include_market_events=True,
)
decision = bars[10].trading_date
label = build_executable_label(
bars,
decision_date=decision,
horizon_sessions=5,
)
self.assertEqual(label.status, "NON_EXECUTABLE_CENSORED")
self.assertIsNone(label.value)
if __name__ == "__main__":
unittest.main()
+987
View File
@@ -0,0 +1,987 @@
import copy
import json
import math
import re
import tempfile
import unittest
from datetime import date, datetime
from decimal import Decimal
from pathlib import Path
from quant60.broker import SimBroker
from quant60.config import ExecutionConfig, FeeConfig
from quant60.contracts import (
ContractValidationError as RuntimeContractValidationError,
validate_broker_snapshot,
)
from quant60.domain import Bar, Portfolio, Side
from quant60.ledger import (
DuplicateEventError,
HashChainLedger,
LedgerIntegrityError,
)
from quant60.reconcile import reconcile_snapshot
from quant60.replay import (
ConflictingDuplicateError,
LedgerProjector,
LedgerReplayError,
OutOfOrderError,
OverfillError,
UnknownOrderError,
project_ledger,
)
from quant60.snapshot_pipeline import first_executable_window
class ContractValidationError(AssertionError):
pass
def validate_json_schema(instance, schema, *, root=None, path="$"):
"""Validate the JSON-Schema subset used by the replay contract.
This deliberately small validator keeps the contract test dependency-free;
it is not exposed as a general JSON-Schema implementation.
"""
root = schema if root is None else root
if "$ref" in schema:
reference = schema["$ref"]
if not reference.startswith("#/"):
raise ContractValidationError(
f"{path}: only local schema references are supported"
)
target = root
for component in reference[2:].split("/"):
target = target[component.replace("~1", "/").replace("~0", "~")]
validate_json_schema(instance, target, root=root, path=path)
return
expected_type = schema.get("type")
if expected_type is not None:
expected = (
expected_type
if isinstance(expected_type, list)
else [expected_type]
)
def matches_type(name):
if name == "null":
return instance is None
if name == "object":
return isinstance(instance, dict)
if name == "array":
return isinstance(instance, list)
if name == "string":
return isinstance(instance, str)
if name == "boolean":
return isinstance(instance, bool)
if name == "integer":
return isinstance(instance, int) and not isinstance(instance, bool)
if name == "number":
return (
isinstance(instance, (int, float))
and not isinstance(instance, bool)
and math.isfinite(float(instance))
)
raise ContractValidationError(
f"{path}: unsupported schema type {name}"
)
if not any(matches_type(name) for name in expected):
raise ContractValidationError(
f"{path}: expected type {expected}, got {type(instance).__name__}"
)
if "const" in schema and instance != schema["const"]:
raise ContractValidationError(
f"{path}: expected constant {schema['const']!r}"
)
if "enum" in schema and instance not in schema["enum"]:
raise ContractValidationError(f"{path}: value is outside enum")
if "minLength" in schema and len(instance) < schema["minLength"]:
raise ContractValidationError(f"{path}: string is too short")
if "pattern" in schema and re.search(schema["pattern"], instance) is None:
raise ContractValidationError(f"{path}: string does not match pattern")
if "minimum" in schema and instance < schema["minimum"]:
raise ContractValidationError(f"{path}: value is below minimum")
if (
"exclusiveMinimum" in schema
and instance <= schema["exclusiveMinimum"]
):
raise ContractValidationError(
f"{path}: value is not above exclusive minimum"
)
if schema.get("format") == "date":
try:
date.fromisoformat(instance)
except (TypeError, ValueError) as exc:
raise ContractValidationError(
f"{path}: value is not an ISO date"
) from exc
if schema.get("format") == "date-time":
try:
parsed = datetime.fromisoformat(instance.replace("Z", "+00:00"))
except (AttributeError, ValueError) as exc:
raise ContractValidationError(
f"{path}: value is not an ISO date-time"
) from exc
if parsed.tzinfo is None or parsed.utcoffset() is None:
raise ContractValidationError(
f"{path}: date-time has no timezone offset"
)
if isinstance(instance, dict):
required = schema.get("required", [])
missing = [key for key in required if key not in instance]
if missing:
raise ContractValidationError(
f"{path}: missing required fields {missing}"
)
properties = schema.get("properties", {})
if schema.get("additionalProperties") is False:
unknown = sorted(set(instance) - set(properties))
if unknown:
raise ContractValidationError(
f"{path}: unsupported fields {unknown}"
)
for key, subschema in properties.items():
if key in instance:
validate_json_schema(
instance[key],
subschema,
root=root,
path=f"{path}.{key}",
)
for trigger, dependencies in schema.get(
"dependentRequired",
{},
).items():
if trigger in instance:
absent = [key for key in dependencies if key not in instance]
if absent:
raise ContractValidationError(
f"{path}: {trigger} requires {absent}"
)
if "oneOf" in schema:
matches = 0
failures = []
for candidate in schema["oneOf"]:
try:
validate_json_schema(
instance,
candidate,
root=root,
path=path,
)
matches += 1
except ContractValidationError as exc:
failures.append(str(exc))
if matches != 1:
raise ContractValidationError(
f"{path}: expected exactly one schema branch, got {matches}; "
f"{failures}"
)
def order_payload(
order_id,
side,
quantity,
status,
*,
filled=0,
average="0",
):
return {
"order_id": order_id,
"symbol": "600000.XSHG",
"side": side,
"quantity": quantity,
"filled_quantity": filled,
"average_fill_price": average,
"status": status,
"submitted_at": "2024-01-02",
"metadata": {},
"reason": None,
}
def fill_payload(
fill_id,
order_id,
side,
quantity,
price,
*,
commission="5",
stamp_duty="0",
transfer_fee="0",
cash_delta=None,
):
payload = {
"fill_id": fill_id,
"order_id": order_id,
"trading_date": "2024-01-03",
"symbol": "600000.XSHG",
"side": side,
"quantity": quantity,
"price": price,
"commission": commission,
"stamp_duty": stamp_duty,
"transfer_fee": transfer_fee,
}
if cash_delta is not None:
payload["cash_delta"] = cash_delta
return payload
def golden_ledger():
ledger = HashChainLedger()
ledger.append(
"ORDER_STATUS",
order_payload("O1", "BUY", 200, "ACCEPTED"),
timestamp="2024-01-03T09:30:00+08:00",
event_id="order:O1:accepted",
)
ledger.append(
"FILL",
fill_payload("F1", "O1", "BUY", 100, "10", cash_delta="-1005"),
timestamp="2024-01-03T09:31:00+08:00",
event_id="fill:F1",
)
ledger.append(
"ORDER_STATUS",
order_payload(
"O1",
"BUY",
200,
"PARTIALLY_FILLED",
filled=100,
average="10",
),
timestamp="2024-01-03T09:31:00+08:00",
event_id="order:O1:partial:100",
)
ledger.append(
"FILL",
fill_payload("F2", "O1", "BUY", 100, "11", cash_delta="-1105"),
timestamp="2024-01-03T09:32:00+08:00",
event_id="fill:F2",
)
ledger.append(
"ORDER_STATUS",
order_payload(
"O1",
"BUY",
200,
"FILLED",
filled=200,
average="10.5",
),
timestamp="2024-01-03T09:32:00+08:00",
event_id="order:O1:filled",
)
ledger.append(
"ORDER_STATUS",
order_payload("O2", "SELL", 100, "ACCEPTED"),
timestamp="2024-01-03T09:33:00+08:00",
event_id="order:O2:accepted",
)
ledger.append(
"FILL",
fill_payload(
"F3",
"O2",
"SELL",
100,
"12",
commission="5",
stamp_duty="0.6",
transfer_fee="0.01",
cash_delta="1194.39",
),
timestamp="2024-01-03T09:34:00+08:00",
event_id="fill:F3",
)
ledger.append(
"ORDER_STATUS",
order_payload(
"O2",
"SELL",
100,
"FILLED",
filled=100,
average="12",
),
timestamp="2024-01-03T09:34:00+08:00",
event_id="order:O2:filled",
)
return ledger
class LedgerReconcileTests(unittest.TestCase):
def test_broker_snapshot_schema_requires_fresh_source_and_order_intent(self):
snapshot = {
"schema_version": "1.0",
"snapshot_id": "snapshot-1",
"signal_as_of": "2024-01-02T15:00:00+08:00",
"next_trading_session": "2024-01-03",
"first_executable_window": first_executable_window(
date(2024, 1, 3)
),
"observation_as_of": "2024-01-03T09:10:00+08:00",
"as_of": "2024-01-03T09:10:00+08:00",
"source_time": "2024-01-03T09:09:59+08:00",
"broker": "fake",
"account_hash": "account-hash",
"cash": 1000.0,
"total_asset": 2000.0,
"positions": [
{
"symbol": "600000.XSHG",
"quantity": 100,
"sellable_quantity": 100,
}
],
"open_orders": [
{
"order_id": "C1",
"broker_order_id": "B1",
"symbol": "600000.XSHG",
"side": "BUY",
"quantity": 100,
"filled_quantity": 0,
"status": "ACCEPTED",
"limit_price": 10.0,
"order_type": "LIMIT",
"time_in_force": "DAY",
}
],
}
validate_broker_snapshot(snapshot)
no_source = copy.deepcopy(snapshot)
del no_source["source_time"]
with self.assertRaises(RuntimeContractValidationError):
validate_broker_snapshot(no_source)
no_price = copy.deepcopy(snapshot)
del no_price["open_orders"][0]["limit_price"]
with self.assertRaises(RuntimeContractValidationError):
validate_broker_snapshot(no_price)
def test_schema_validates_actual_sim_broker_replay_envelopes(self):
ledger = HashChainLedger()
broker = SimBroker(
Portfolio(Decimal("100000")),
ExecutionConfig(
lot_size=100,
participation_rate=0.1,
slippage_bps=0,
),
FeeConfig(
commission_rate=0,
minimum_commission=0,
stamp_duty_rate=0,
transfer_fee_rate=0,
),
ledger,
)
broker.submit(
"600000.SH",
Side.BUY,
100,
date(2024, 1, 2),
)
value = Decimal("10")
broker.execute_session(
{
"600000.XSHG": Bar(
trading_date=date(2024, 1, 2),
symbol="600000.XSHG",
open=value,
high=value,
low=value,
close=value,
volume=1000,
)
}
)
broker.execute_session(
{
"600000.XSHG": Bar(
trading_date=date(2024, 1, 3),
symbol="600000.XSHG",
open=value,
high=value,
low=value,
close=value,
volume=1000,
)
}
)
envelopes = [
record.as_dict()
for record in ledger.records
if record.event_type in {"ORDER_STATUS", "FILL"}
]
self.assertEqual(
[record["event_type"] for record in envelopes],
["ORDER_STATUS", "FILL", "ORDER_STATUS"],
)
schema_path = (
Path(__file__).resolve().parents[1]
/ "schemas"
/ "order_event.schema.json"
)
schema = json.loads(schema_path.read_text(encoding="utf-8"))
for envelope in envelopes:
with self.subTest(event_id=envelope["event_id"]):
validate_json_schema(envelope, schema)
projection = project_ledger(envelopes)
self.assertEqual(projection.cash_delta, Decimal("-1000.000000"))
self.assertEqual(
projection.position_deltas,
{"600000.XSHG": 100},
)
self.assertEqual(projection.orders["O00000001"].status, "FILLED")
def test_schema_discriminates_payloads_and_projector_enforces_envelope(self):
schema_path = (
Path(__file__).resolve().parents[1]
/ "schemas"
/ "order_event.schema.json"
)
schema = json.loads(schema_path.read_text(encoding="utf-8"))
accepted = golden_ledger().records[0].as_dict()
flat_legacy = {
"schema_version": "1.0",
"event_id": accepted["event_id"],
"event_time": accepted["timestamp"],
**accepted["payload"],
}
with self.assertRaises(ContractValidationError):
validate_json_schema(flat_legacy, schema)
with self.assertRaises(LedgerReplayError):
project_ledger([flat_legacy])
crossed = copy.deepcopy(accepted)
crossed["event_type"] = "FILL"
with self.assertRaises(ContractValidationError):
validate_json_schema(crossed, schema)
unexpected = copy.deepcopy(accepted)
unexpected["payload"]["undocumented"] = True
with self.assertRaises(ContractValidationError):
validate_json_schema(unexpected, schema)
with self.assertRaises(LedgerReplayError):
project_ledger([unexpected])
unpaired_hash = copy.deepcopy(accepted)
del unpaired_hash["hash"]
with self.assertRaises(ContractValidationError):
validate_json_schema(unpaired_hash, schema)
with self.assertRaises(LedgerReplayError):
project_ledger([unpaired_hash])
no_sequence = copy.deepcopy(accepted)
del no_sequence["sequence"]
with self.assertRaises(ContractValidationError):
validate_json_schema(no_sequence, schema)
with self.assertRaises(LedgerReplayError):
project_ledger([no_sequence])
alias_fixture = copy.deepcopy(accepted)
del alias_fixture["previous_hash"]
del alias_fixture["hash"]
alias_fixture["payload"]["order_quantity"] = (
alias_fixture["payload"].pop("quantity")
)
alias_fixture["payload"]["cumulative_filled_quantity"] = (
alias_fixture["payload"].pop("filled_quantity")
)
with self.assertRaises(ContractValidationError):
validate_json_schema(alias_fixture, schema)
self.assertEqual(
project_ledger([alias_fixture]).orders["O1"].quantity,
200,
)
conflicting_alias = copy.deepcopy(accepted)
del conflicting_alias["previous_hash"]
del conflicting_alias["hash"]
conflicting_alias["payload"]["order_quantity"] = 300
with self.assertRaises(ConflictingDuplicateError):
project_ledger([conflicting_alias])
def test_hash_chain_duplicate_and_tamper_detection(self):
ledger = HashChainLedger()
first = ledger.append(
"TEST",
{"value": 1},
timestamp="2024-01-02T15:00:00+08:00",
event_id="event-1",
)
self.assertIs(
ledger.append(
"TEST",
{"value": 1},
timestamp="2024-01-02T15:00:00+08:00",
event_id="event-1",
),
first,
)
with self.assertRaises(DuplicateEventError):
ledger.append(
"TEST",
{"value": 2},
timestamp="2024-01-02T15:00:00+08:00",
event_id="event-1",
)
with tempfile.TemporaryDirectory() as directory:
path = ledger.write_jsonl(Path(directory) / "events.jsonl")
self.assertTrue(HashChainLedger.read_jsonl(path).verify())
line = json.loads(path.read_text(encoding="utf-8").splitlines()[0])
line["payload"]["value"] = 99
path.write_text(json.dumps(line) + "\n", encoding="utf-8")
with self.assertRaises(LedgerIntegrityError):
HashChainLedger.read_jsonl(path)
def test_legacy_reconciliation_api_remains_compatible(self):
okay = reconcile_snapshot(
local_cash="100",
local_positions={"600000.SH": 100},
broker_cash="100.001",
broker_positions={"SH600000": 100},
cash_tolerance="0.01",
)
self.assertTrue(okay.balanced)
self.assertFalse(okay.complete)
self.assertFalse(okay.safe_to_open)
bad = reconcile_snapshot(
local_cash="100",
local_positions={"600000.SH": 100},
broker_cash="99",
broker_positions={"600000.SH": 0},
)
self.assertFalse(bad.safe_to_open)
self.assertEqual(
{item.kind for item in bad.differences},
{"CASH", "POSITION"},
)
def test_identical_duplicate_open_order_identity_fails_closed(self):
order = {
"order_id": "DUPLICATE",
"broker_order_id": "B-DUPLICATE",
"symbol": "600000.XSHG",
"side": "BUY",
"quantity": 100,
"filled_quantity": 0,
"status": "ACCEPTED",
"limit_price": 10.0,
"order_type": "LIMIT",
"time_in_force": "DAY",
}
local = {
"cash": "100",
"positions": [],
"open_orders": [order, copy.deepcopy(order)],
"as_of": "2024-01-03T12:00:00+08:00",
}
broker = {
**local,
"open_orders": [copy.deepcopy(order), copy.deepcopy(order)],
"source_time": "2024-01-03T12:00:00+08:00",
}
report = reconcile_snapshot(
local_snapshot=local,
broker_snapshot=broker,
now="2024-01-03T12:00:00+08:00",
)
self.assertTrue(report.complete)
self.assertFalse(report.balanced)
self.assertFalse(report.safe_to_open)
self.assertIn(
("DUPLICATE_OPEN_ORDER", "DUPLICATE"),
{(item.kind, item.key) for item in report.differences},
)
def test_complete_snapshot_checks_sellable_orders_and_freshness(self):
local = {
"cash": "1000",
"positions": [
{
"symbol": "600000.XSHG",
"quantity": 200,
"sellable_quantity": 100,
}
],
"open_orders": [
{
"order_id": "C1",
"broker_order_id": "B-C1",
"symbol": "600000.XSHG",
"side": "SELL",
"quantity": 100,
"filled_quantity": 0,
"status": "ACCEPTED",
"limit_price": 10.0,
"order_type": "LIMIT",
"time_in_force": "DAY",
}
],
"as_of": "2024-01-03T12:00:09+08:00",
}
broker = {
"cash": "1000.001",
"positions": [
{
"symbol": "600000.SH",
"quantity": 200,
"sellable_quantity": 100,
}
],
"open_orders": [
{
"order_id": "C1",
"broker_order_id": "B-C1",
"symbol": "SH600000",
"side": "SELL",
"quantity": 100,
"filled_quantity": 0,
"status": "ACCEPTED",
"limit_price": 10.0,
"order_type": "LIMIT",
"time_in_force": "DAY",
}
],
"as_of": "2024-01-03T12:00:08+08:00",
"source_time": "2024-01-03T12:00:05+08:00",
}
report = reconcile_snapshot(
local_snapshot=local,
broker_snapshot=broker,
now="2024-01-03T12:00:10+08:00",
max_snapshot_age_seconds=15,
max_source_age_seconds=15,
)
self.assertTrue(report.safe_to_open)
wrong_price = copy.deepcopy(broker)
wrong_price["open_orders"][0]["limit_price"] = 10.01
price_report = reconcile_snapshot(
local_snapshot=local,
broker_snapshot=wrong_price,
now="2024-01-03T12:00:10+08:00",
max_snapshot_age_seconds=15,
max_source_age_seconds=15,
)
self.assertFalse(price_report.safe_to_open)
self.assertIn(
("OPEN_ORDER", "C1"),
{
(item.kind, item.key)
for item in price_report.differences
},
)
unsafe_broker = copy.deepcopy(broker)
unsafe_broker["positions"][0]["sellable_quantity"] = 50
unsafe_broker["open_orders"][0]["status"] = "UNKNOWN"
unsafe_broker["source_time"] = "2024-01-03T11:55:00+08:00"
unsafe = reconcile_snapshot(
local_snapshot=local,
broker_snapshot=unsafe_broker,
now="2024-01-03T12:00:10+08:00",
max_snapshot_age_seconds=15,
max_source_age_seconds=15,
)
self.assertFalse(unsafe.safe_to_open)
self.assertEqual(
{item.kind for item in unsafe.differences},
{"OPEN_ORDER", "SELLABLE", "STALE_SOURCE", "UNKNOWN_ORDER"},
)
def test_complete_snapshot_missing_or_stale_as_of_fails_closed(self):
local = {
"cash": "100",
"positions": [],
"open_orders": [],
"as_of": "2024-01-03T11:00:00+08:00",
}
broker = {
"cash": "100",
"positions": [],
"open_orders": [],
"as_of": "2024-01-03T11:00:00+08:00",
"source_time": "2024-01-03T11:00:00+08:00",
}
stale = reconcile_snapshot(
local_snapshot=local,
broker_snapshot=broker,
now="2024-01-03T12:00:00+08:00",
)
self.assertFalse(stale.safe_to_open)
self.assertIn(
"STALE_SNAPSHOT",
{item.kind for item in stale.differences},
)
missing = copy.deepcopy(broker)
del missing["open_orders"]
report = reconcile_snapshot(
local_snapshot={
**local,
"as_of": "2024-01-03T12:00:00+08:00",
},
broker_snapshot={
**missing,
"as_of": "2024-01-03T12:00:00+08:00",
"source_time": "2024-01-03T12:00:00+08:00",
},
now="2024-01-03T12:00:00+08:00",
)
self.assertFalse(report.safe_to_open)
self.assertIn(
("MISSING_FIELD", "broker.open_orders"),
{(item.kind, item.key) for item in report.differences},
)
def test_freshness_tolerances_reject_nonfinite_and_negative_values(self):
snapshot = {
"cash": "100",
"positions": [],
"open_orders": [],
"as_of": "2024-01-03T12:00:00+08:00",
"source_time": "2024-01-03T12:00:00+08:00",
}
base = {
"local_snapshot": {
key: value
for key, value in snapshot.items()
if key != "source_time"
},
"broker_snapshot": snapshot,
"now": "2024-01-03T12:00:00+08:00",
}
tolerance_names = (
"max_snapshot_age_seconds",
"max_source_age_seconds",
"max_clock_skew_seconds",
"max_age_seconds",
)
invalid_values = (
float("nan"),
float("inf"),
float("-inf"),
-0.001,
)
for name in tolerance_names:
for value in invalid_values:
with self.subTest(tolerance=name, value=value):
with self.assertRaisesRegex(
ValueError,
rf"{name} must be a finite non-negative number",
):
reconcile_snapshot(**base, **{name: value})
def test_invalid_specific_tolerance_is_not_hidden_by_age_alias(self):
legacy = {
"local_cash": "100",
"local_positions": {},
"broker_cash": "100",
"broker_positions": {},
"max_age_seconds": 10,
}
for name in (
"max_snapshot_age_seconds",
"max_source_age_seconds",
):
with self.subTest(tolerance=name):
with self.assertRaisesRegex(
ValueError,
rf"{name} must be a finite non-negative number",
):
reconcile_snapshot(**legacy, **{name: float("nan")})
def test_replay_frozen_golden_digest_and_accounting(self):
projection = project_ledger(golden_ledger())
self.assertEqual(projection.cash_delta, Decimal("-915.610000"))
self.assertEqual(
projection.position_deltas,
{"600000.XSHG": 100},
)
self.assertEqual(projection.orders["O1"].filled_quantity, 200)
self.assertEqual(
projection.orders["O1"].average_fill_price,
Decimal("10.5000"),
)
self.assertEqual(projection.orders["O2"].position_delta, -100)
self.assertTrue(projection.safe_to_reconcile)
self.assertEqual(
projection.digest,
"b9b48e5f11f43262e435318f6b6a39be74d3090506a3e5952bef4af9944545e5",
)
def test_replay_identical_duplicates_are_idempotent(self):
ledger = golden_ledger()
baseline = project_ledger(ledger)
records = [record.as_dict() for record in ledger.records]
duplicate_fill = copy.deepcopy(records[6])
duplicate_fill["sequence"] = 9
duplicate_fill["event_id"] = "fill:F3:duplicate-delivery"
duplicate_status = copy.deepcopy(records[7])
duplicate_status["sequence"] = 10
duplicate_status["event_id"] = "order:O2:filled:duplicate-delivery"
replayed = project_ledger(records + [duplicate_fill, duplicate_status])
self.assertEqual(replayed.digest, baseline.digest)
self.assertEqual(replayed.cash_delta, baseline.cash_delta)
self.assertEqual(replayed.fill_ids, baseline.fill_ids)
def test_replay_duplicate_id_cannot_hide_sequence_jump(self):
first = {
"sequence": 1,
"event_id": "order:e1",
"event_type": "ORDER_STATUS",
"timestamp": "2024-01-03T09:30:00+08:00",
"payload": order_payload(
"E1",
"BUY",
100,
"ACCEPTED",
),
}
exact_retry = copy.deepcopy(first)
second = copy.deepcopy(first)
second["sequence"] = 2
second["event_id"] = "order:e2"
legitimate = LedgerProjector()
legitimate.process(first)
legitimate.process(exact_retry)
legitimate.process(second)
projection = legitimate.projection()
self.assertEqual(projection.source_last_sequence, 2)
self.assertEqual(projection.relevant_event_count, 1)
renumbered_retry = copy.deepcopy(first)
renumbered_retry["sequence"] = 2
wrong_sequence = LedgerProjector()
wrong_sequence.process(first)
with self.assertRaisesRegex(
ConflictingDuplicateError,
"first observed at sequence 1, not 2",
):
wrong_sequence.process(renumbered_retry)
changed_timestamp = copy.deepcopy(first)
changed_timestamp["timestamp"] = "2024-01-03T09:30:01+08:00"
wrong_timestamp = LedgerProjector()
wrong_timestamp.process(first)
with self.assertRaises(ConflictingDuplicateError):
wrong_timestamp.process(changed_timestamp)
jumped_retry = copy.deepcopy(first)
jumped_retry["sequence"] = 100
poisoned = LedgerProjector()
poisoned.process(first)
with self.assertRaisesRegex(OutOfOrderError, "sequence gap"):
poisoned.process(jumped_retry)
with self.assertRaisesRegex(
LedgerReplayError,
"fail-closed after a prior replay error",
):
poisoned.process(second)
late_retry = LedgerProjector()
late_retry.process(first)
late_retry.process(second)
with self.assertRaises(OutOfOrderError):
late_retry.process(exact_retry)
def test_replay_rejects_conflicting_duplicates(self):
ledger = golden_ledger()
records = [record.as_dict() for record in ledger.records]
conflict = copy.deepcopy(records[6])
conflict["sequence"] = 9
conflict["event_id"] = "fill:F3:conflicting-delivery"
conflict["payload"]["price"] = "13"
with self.assertRaises(ConflictingDuplicateError):
project_ledger(records + [conflict])
event_conflict = copy.deepcopy(records[0])
event_conflict["payload"]["quantity"] = 300
with self.assertRaises(ConflictingDuplicateError):
project_ledger([records[0], event_conflict])
def test_replay_rejects_unknown_order_overfill_and_out_of_order(self):
unknown_fill = {
"sequence": 1,
"event_id": "fill:unknown",
"event_type": "FILL",
"timestamp": "2024-01-03T09:31:00+08:00",
"payload": fill_payload("FU", "MISSING", "BUY", 100, "10"),
}
with self.assertRaises(UnknownOrderError):
project_ledger([unknown_fill])
accepted = {
"sequence": 1,
"event_id": "order:small",
"event_type": "ORDER_STATUS",
"timestamp": "2024-01-03T09:30:00+08:00",
"payload": order_payload("SMALL", "BUY", 100, "ACCEPTED"),
}
overfill = {
"sequence": 2,
"event_id": "fill:too-much",
"event_type": "FILL",
"timestamp": "2024-01-03T09:31:00+08:00",
"payload": fill_payload("F-OVER", "SMALL", "BUY", 200, "10"),
}
with self.assertRaises(OverfillError):
project_ledger([accepted, overfill])
projector = LedgerProjector()
projector.process(accepted)
with self.assertRaises(OverfillError):
projector.process(overfill)
with self.assertRaises(LedgerReplayError):
projector.projection()
with self.assertRaises(LedgerReplayError):
projector.process(
{
**accepted,
"sequence": 3,
"event_id": "order:after-failure",
}
)
second = copy.deepcopy(accepted)
second["sequence"] = 2
second["event_id"] = "order:second"
first_late = copy.deepcopy(accepted)
first_late["event_id"] = "order:first-late"
with self.assertRaises(OutOfOrderError):
project_ledger([second, first_late])
regression = copy.deepcopy(accepted)
regression["sequence"] = 2
regression["event_id"] = "order:regression"
regression["payload"]["status"] = "NEW"
with self.assertRaises(OutOfOrderError):
project_ledger([accepted, regression])
time_regression = copy.deepcopy(accepted)
time_regression["sequence"] = 2
time_regression["event_id"] = "order:time-regression"
time_regression["timestamp"] = "2024-01-03T09:29:59+08:00"
time_regression["payload"]["status"] = "UNKNOWN"
with self.assertRaises(OutOfOrderError):
project_ledger([accepted, time_regression])
if __name__ == "__main__":
unittest.main()
+35
View File
@@ -0,0 +1,35 @@
import json
import tempfile
import unittest
from pathlib import Path
from tools.export_mock_parity import verify_report, write_report
class MockParityReportTests(unittest.TestCase):
def test_report_is_deterministic_and_explicitly_not_platform_evidence(self):
with tempfile.TemporaryDirectory() as directory:
first = Path(directory) / "first.json"
second = Path(directory) / "second.json"
report = write_report(first)
write_report(second)
self.assertEqual(first.read_bytes(), second.read_bytes())
self.assertTrue(report["comparisons"]["whole_plan_exact"])
self.assertEqual(report["evidence_class"], "mock_contract_only")
self.assertFalse(report["real_platform_pass"])
self.assertEqual(report["gate_credit"], [])
self.assertTrue(verify_report(first)["ok"])
def test_tampering_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "report.json"
write_report(path)
report = json.loads(path.read_text(encoding="utf-8"))
report["comparisons"]["whole_plan_exact"] = False
path.write_text(json.dumps(report), encoding="utf-8")
with self.assertRaisesRegex(ValueError, "hash mismatch"):
verify_report(path)
if __name__ == "__main__":
unittest.main()
+192
View File
@@ -0,0 +1,192 @@
import unittest
from datetime import datetime, timezone
from quant60.pit import (
PITIntegrityError,
PITRecord,
PointInTimeTable,
assert_information_available,
)
from quant60.universe import (
SecurityEligibility,
UniverseState,
build_universe,
classify_security,
)
UTC = timezone.utc
def record(
*,
value,
effective="2024-03-31T00:00:00+08:00",
available="2024-04-30T18:00:00+08:00",
revision=0,
):
return PITRecord(
entity_id="600000.XSHG",
field="net_profit",
effective_time=effective,
available_time=available,
value=value,
source="fixture",
source_version="v1",
revision=revision,
)
class PITUniverseTests(unittest.TestCase):
def test_pit_query_never_uses_future_announcement(self):
table = PointInTimeTable([record(value=100)])
before = table.query(
"600000.XSHG",
"net_profit",
as_of="2024-04-30T17:59:59+08:00",
)
after = table.query(
"600000.XSHG",
"net_profit",
as_of="2024-04-30T18:00:00+08:00",
)
self.assertIsNone(before)
self.assertEqual(after.value, 100)
def test_revision_is_visible_only_after_its_own_available_time(self):
table = PointInTimeTable(
[
record(value=100),
record(
value=90,
available="2024-05-05T09:00:00+08:00",
revision=1,
),
]
)
first = table.query(
"600000.XSHG",
"net_profit",
as_of="2024-05-01T15:00:00+08:00",
)
revised = table.query(
"600000.XSHG",
"net_profit",
as_of="2024-05-06T15:00:00+08:00",
)
self.assertEqual(first.value, 100)
self.assertEqual(revised.value, 90)
def test_future_injection_gate_fails_closed(self):
future = record(
value=100,
available="2024-05-02T09:00:00+08:00",
)
with self.assertRaisesRegex(PITIntegrityError, "future-available"):
assert_information_available(
[future],
as_of="2024-05-01T15:00:00+08:00",
)
def test_naive_timestamps_and_duplicate_identity_are_rejected(self):
with self.assertRaisesRegex(PITIntegrityError, "timezone"):
record(
value=100,
effective=datetime(2024, 3, 31),
)
item = record(value=100)
with self.assertRaisesRegex(PITIntegrityError, "duplicate"):
PointInTimeTable([item, item])
def test_data_version_is_order_independent(self):
left = record(value=100)
right = PITRecord(
entity_id="000001.XSHE",
field="net_profit",
effective_time=datetime(2024, 3, 31, tzinfo=UTC),
available_time=datetime(2024, 4, 30, tzinfo=UTC),
value=80,
source="fixture",
source_version="v1",
)
self.assertEqual(
PointInTimeTable([left, right]).data_version,
PointInTimeTable([right, left]).data_version,
)
def test_universe_states_keep_untradable_holdings_visible(self):
frozen = classify_security(
SecurityEligibility(
symbol="600000.SH",
index_member=False,
listing_sessions=500,
median_turnover=1_000_000,
minimum_turnover=100_000,
suspended=True,
held_quantity=1000,
sellable_quantity=1000,
)
)
sell_only = classify_security(
SecurityEligibility(
symbol="000001.SZ",
index_member=True,
listing_sessions=500,
median_turnover=1_000_000,
minimum_turnover=100_000,
is_st=True,
held_quantity=1000,
sellable_quantity=1000,
)
)
self.assertEqual(frozen.state, UniverseState.FROZEN)
self.assertTrue(frozen.can_hold)
self.assertFalse(frozen.can_sell)
self.assertEqual(sell_only.state, UniverseState.SELL_ONLY)
self.assertTrue(sell_only.can_sell)
hold_only = classify_security(
SecurityEligibility(
symbol="000333.SZ",
index_member=True,
listing_sessions=500,
median_turnover=50_000,
minimum_turnover=100_000,
held_quantity=1000,
sellable_quantity=1000,
)
)
self.assertEqual(hold_only.state, UniverseState.HOLD_ONLY)
self.assertTrue(hold_only.can_hold)
self.assertFalse(hold_only.can_open)
def test_openable_and_excluded_are_distinct(self):
decisions = build_universe(
[
SecurityEligibility(
symbol="600000.SH",
index_member=True,
listing_sessions=500,
median_turnover=1_000_000,
minimum_turnover=100_000,
),
SecurityEligibility(
symbol="300750.SZ",
index_member=True,
listing_sessions=20,
median_turnover=1_000_000,
minimum_turnover=100_000,
),
]
)
self.assertEqual(
decisions["600000.XSHG"].state,
UniverseState.OPENABLE,
)
self.assertEqual(
decisions["300750.XSHE"].state,
UniverseState.EXCLUDED,
)
if __name__ == "__main__":
unittest.main()
+171
View File
@@ -0,0 +1,171 @@
import unittest
from quant60.portable_core import (
build_rebalance_plan,
capped_target_weights,
momentum_score,
normalize_symbol,
order_deltas,
target_quantities,
)
class PortableCoreTests(unittest.TestCase):
def test_symbol_round_trip(self):
inputs = [
("600000.XSHG", "600000.SH", "SH600000"),
("000001.XSHE", "000001.SZ", "SZ000001"),
("830799.XBSE", "830799.BJ", "BJ830799"),
]
for canonical, qmt, qlib in inputs:
self.assertEqual(normalize_symbol(qmt), canonical)
self.assertEqual(normalize_symbol(qlib), canonical)
self.assertEqual(normalize_symbol(canonical, "qmt"), qmt)
self.assertEqual(normalize_symbol(canonical, "qlib"), qlib)
def test_momentum_uses_latest_completed_bar(self):
self.assertAlmostEqual(momentum_score([10, 11, 12], 2, 0), 0.2)
self.assertAlmostEqual(momentum_score([10, 11, 12], 1, 1), 0.1)
def test_caps_and_cash_buffer_are_not_double_counted(self):
weights = capped_target_weights(
{"600000.XSHG": 3, "000001.XSHE": 2, "300750.XSHE": 1},
max_weight=0.4,
gross_target=0.95,
)
self.assertAlmostEqual(sum(weights.values()), 0.95)
self.assertLessEqual(max(weights.values()), 0.4 + 1e-12)
quantities = target_quantities(
weights,
{symbol: 10 for symbol in weights},
equity=1_000_000,
lot_size=100,
cash_buffer=0.02,
)
invested = sum(quantity * 10 for quantity in quantities.values())
self.assertGreater(invested, 930_000)
self.assertLessEqual(invested, 980_000)
def test_non_finite_or_out_of_range_portfolio_inputs_are_rejected(self):
for value in (float("nan"), float("inf"), float("-inf")):
with self.subTest(kind="score", value=value):
with self.assertRaises(ValueError):
capped_target_weights({"600000.SH": value})
with self.subTest(kind="weight", value=value):
with self.assertRaises(ValueError):
target_quantities(
{"600000.SH": value},
{"600000.SH": 10.0},
100_000,
)
with self.subTest(kind="equity", value=value):
with self.assertRaises(ValueError):
target_quantities(
{"600000.SH": 0.5},
{"600000.SH": 10.0},
value,
)
for max_weight, gross_target in ((1.1, 0.9), (0.2, 1.1)):
with self.subTest(max_weight=max_weight, gross_target=gross_target):
with self.assertRaises(ValueError):
capped_target_weights(
{"600000.SH": 1.0},
max_weight=max_weight,
gross_target=gross_target,
)
def test_sell_delta_respects_sellable(self):
deltas = order_deltas(
{"600000.XSHG": 0},
{"600000.XSHG": 1000},
{"600000.XSHG": 300},
lot_size=100,
)
self.assertEqual(deltas, {"600000.XSHG": -300})
def test_star_targets_and_partial_orders_respect_200_share_minimum(self):
self.assertEqual(
target_quantities(
{"688301.XSHG": 1.0},
{"688301.XSHG": 300.0},
equity=50_000,
lot_size=100,
cash_buffer=0.02,
),
{"688301.XSHG": 0},
)
self.assertEqual(
target_quantities(
{"688301.XSHG": 1.0},
{"688301.XSHG": 200.0},
equity=50_000,
lot_size=100,
cash_buffer=0.02,
),
{"688301.XSHG": 200},
)
self.assertEqual(
order_deltas(
{"688301.XSHG": 300},
{"688301.XSHG": 200},
{"688301.XSHG": 200},
lot_size=100,
),
{},
)
self.assertEqual(
order_deltas(
{"688301.XSHG": 1300},
{"688301.XSHG": 1400},
{"688301.XSHG": 1400},
lot_size=100,
),
{},
)
def test_complete_odd_lot_liquidation_is_preserved(self):
self.assertEqual(
order_deltas(
{"600000.XSHG": 0},
{"600000.XSHG": 1050},
{"600000.XSHG": 1050},
lot_size=100,
),
{"600000.XSHG": -1050},
)
self.assertEqual(
order_deltas(
{"688301.XSHG": 0},
{"688301.XSHG": 150},
{"688301.XSHG": 150},
lot_size=100,
),
{"688301.XSHG": -150},
)
def test_complete_plan_is_deterministic(self):
histories = {
"600000.SH": [10 + index * 0.1 for index in range(25)],
"000001.SZ": [10 + index * 0.05 for index in range(25)],
}
arguments = dict(
price_history=histories,
current={},
sellable={},
equity=100_000,
lookback=20,
skip=0,
top_n=2,
max_weight=0.5,
gross_target=0.95,
cash_buffer=0.02,
lot_size=100,
)
self.assertEqual(
build_rebalance_plan(**arguments),
build_rebalance_plan(**arguments),
)
if __name__ == "__main__":
unittest.main()
+49
View File
@@ -0,0 +1,49 @@
import json
import subprocess
import sys
import unittest
from pathlib import Path
PROJECT = Path(__file__).resolve().parents[1]
class ProjectScaffoldTests(unittest.TestCase):
def test_colab_notebook_is_valid_plain_python(self):
result = subprocess.run(
[
sys.executable,
"tools/run_colab_notebook.py",
"notebooks/quant_os_colab.ipynb",
],
cwd=PROJECT,
check=True,
capture_output=True,
text=True,
)
payload = json.loads(result.stdout.strip().splitlines()[-1])
self.assertTrue(payload["ok"])
self.assertGreaterEqual(payload["code_cells"], 5)
self.assertFalse(payload["executed"])
def test_secret_files_are_ignored(self):
ignore = (PROJECT / ".gitignore").read_text(encoding="utf-8")
for entry in (".env", "secrets/", "credentials/", "userdata_mini/"):
self.assertIn(entry, ignore)
def test_claude_project_contract_exists(self):
content = (PROJECT / "CLAUDE.md").read_text(encoding="utf-8")
self.assertIn("executable source of truth for Quant OS", content)
self.assertIn("make local", content)
def test_gitea_workflow_is_at_monorepo_root(self):
workflow = PROJECT.parent / ".gitea/workflows/quant-os-ci.yml"
self.assertTrue(workflow.is_file())
content = workflow.read_text(encoding="utf-8")
self.assertIn("working-directory: quant-os", content)
self.assertIn("bash scripts/validate_all.sh", content)
self.assertFalse((PROJECT / ".gitea/workflows/ci.yml").exists())
if __name__ == "__main__":
unittest.main()
File diff suppressed because it is too large Load Diff
+102
View File
@@ -0,0 +1,102 @@
import os
import stat
import tempfile
import unittest
from contextlib import redirect_stderr
from io import StringIO
from pathlib import Path
from tools.qmt_shadow_plan import (
_load_existing_account_hash_key,
_load_or_create_account_hash_key,
build_parser,
)
class QmtShadowToolTests(unittest.TestCase):
def test_account_hash_key_is_created_once_with_private_permissions(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "private" / "account-hash.key"
first = _load_or_create_account_hash_key(path)
second = _load_or_create_account_hash_key(path)
self.assertEqual(first, second)
self.assertEqual(len(first), 32)
if os.name != "nt":
self.assertEqual(
stat.S_IMODE(path.stat().st_mode),
0o600,
)
def test_short_account_hash_key_is_rejected(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "account-hash.key"
path.write_bytes(b"too-short")
path.chmod(0o600)
with self.assertRaisesRegex(ValueError, "at least 32 bytes"):
_load_or_create_account_hash_key(path)
def test_verify_key_loader_never_creates_missing_key(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "missing-account-hash.key"
with self.assertRaisesRegex(
ValueError, "does not exist"
):
_load_existing_account_hash_key(path)
self.assertFalse(path.exists())
def test_existing_parent_permissions_are_not_changed(self):
if os.name == "nt":
self.skipTest("POSIX permission invariant")
with tempfile.TemporaryDirectory() as directory:
parent = Path(directory) / "shared-parent"
parent.mkdir(mode=0o755)
parent.chmod(0o755)
_load_or_create_account_hash_key(parent / "account-hash.key")
self.assertEqual(
stat.S_IMODE(parent.stat().st_mode),
0o755,
)
def test_cli_rejects_widened_safety_thresholds(self):
base = [
"plan",
"--decision",
"decision-manifest.json",
"--output",
"shadow-output",
]
for option, value in (
("--max-query-duration-seconds", "10.0001"),
("--market-value-tolerance", "1.0001"),
):
with self.subTest(option=option), redirect_stderr(StringIO()):
with self.assertRaises(SystemExit):
build_parser().parse_args(base + [option, value])
def test_cli_accepts_only_equal_or_stricter_thresholds(self):
parsed = build_parser().parse_args(
[
"plan",
"--decision",
"decision-manifest.json",
"--output",
"shadow-output",
"--max-query-duration-seconds",
"5",
"--market-value-tolerance",
"0.5",
]
)
self.assertEqual(parsed.max_query_duration_seconds, 5.0)
self.assertEqual(parsed.market_value_tolerance, 0.5)
def test_cli_verify_requires_original_decision(self):
with redirect_stderr(StringIO()):
with self.assertRaises(SystemExit):
build_parser().parse_args(
["verify", "--manifest", "shadow-manifest.json"]
)
if __name__ == "__main__":
unittest.main()
+99
View File
@@ -0,0 +1,99 @@
import math
import unittest
from quant60.research import (
DeterministicRidge,
NumpyRidge,
pearson_correlation,
purged_walk_forward,
spearman_correlation,
)
from quant60.risk import check_target_weights, diagonal_variance
class ResearchRiskTests(unittest.TestCase):
def test_purged_walk_forward_is_time_ordered(self):
folds = list(
purged_walk_forward(
40,
train_size=20,
test_size=5,
purge=2,
embargo=1,
)
)
self.assertTrue(folds)
for fold in folds:
self.assertLess(max(fold.train), min(fold.purged))
self.assertLess(max(fold.purged), min(fold.test))
self.assertTrue(set(fold.train).isdisjoint(fold.test))
def test_risk_check_rejects_concentration(self):
result = check_target_weights(
{"600000.SH": 0.6, "000001.SZ": 0.2},
max_single_weight=0.5,
max_gross_weight=0.9,
)
self.assertFalse(result.approved)
self.assertIn("SINGLE_NAME_CAP", {item.code for item in result.violations})
def test_diagonal_variance_floor(self):
result = diagonal_variance(
{"600000.SH": [0.01], "000001.SZ": [0.0, 0.0, 0.0]}
)
self.assertGreater(result["600000.XSHG"], 0)
self.assertGreater(result["000001.XSHE"], 0)
def test_non_finite_weights_fail_closed(self):
for value in (float("nan"), float("inf"), float("-inf")):
with self.subTest(value=value):
result = check_target_weights(
{"600000.SH": value},
max_single_weight=0.5,
max_gross_weight=0.9,
)
self.assertFalse(result.approved)
self.assertIn(
"NON_FINITE_WEIGHT",
{item.code for item in result.violations},
)
self.assertTrue(math.isinf(result.gross_weight))
self.assertTrue(math.isinf(result.one_way_turnover))
def test_non_finite_returns_are_rejected(self):
for value in (float("nan"), float("inf"), float("-inf")):
with self.subTest(value=value):
with self.assertRaisesRegex(ValueError, "returns must be finite"):
diagonal_variance({"600000.SH": [0.01, value]})
def test_optional_numpy_ridge_when_numpy_is_present(self):
try:
model = NumpyRidge(alpha=0.1).fit([[0], [1], [2]], [1, 3, 5])
except Exception as exc:
if type(exc).__name__ == "ResearchDependencyUnavailable":
self.skipTest(str(exc))
raise
prediction = model.predict([[3]])
self.assertGreater(float(prediction[0]), 6.0)
def test_dependency_free_ridge_fits_linear_relationship(self):
model = DeterministicRidge(alpha=0.001).fit(
[[0], [1], [2], [3]],
[1, 3, 5, 7],
)
prediction = model.predict([[4]])[0]
self.assertAlmostEqual(prediction, 9, places=2)
def test_rank_and_linear_correlation(self):
self.assertAlmostEqual(
pearson_correlation([1, 2, 3], [2, 4, 6]),
1.0,
)
self.assertAlmostEqual(
spearman_correlation([10, 30, 20], [1, 3, 2]),
1.0,
)
if __name__ == "__main__":
unittest.main()
+544
View File
@@ -0,0 +1,544 @@
import hashlib
import json
import tempfile
import unittest
from datetime import date, datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
import quant60.snapshot_backtest as snapshot_backtest_module
from quant60.config import BacktestConfig, StrategyConfig
from quant60.backtest import verify_artifact_manifest
from quant60.data_snapshot import (
CanonicalBarRecord,
IndexMembershipRecord,
build_snapshot_payload,
write_data_snapshot,
)
from quant60.snapshot_pipeline import (
SnapshotDecisionError,
build_snapshot_decision,
first_executable_window,
verify_snapshot_decision,
write_snapshot_decision,
)
from quant60.snapshot_backtest import run_snapshot_backtest
def business_days(start: date, count: int) -> list[date]:
values = []
current = start
while len(values) < count:
if current.weekday() < 5:
values.append(current)
current += timedelta(days=1)
return values
def build_provider_snapshot(
root: Path,
*,
st_symbol_on_last_session: str | None = None,
) -> tuple[str, date]:
frozen_calendar = business_days(date(2024, 1, 2), 31)
sessions = frozen_calendar[:-1]
next_trading_session = frozen_calendar[-1]
bars = []
memberships = []
symbols = ("600000.XSHG", "000001.XSHE")
for index, session in enumerate(sessions):
for symbol in symbols:
if symbol == "600000.XSHG":
raw = 10.0 * (1.005**index)
factor = 1.0
else:
unsplit = 20.0 * (1.01**index)
raw = unsplit if index < 15 else unsplit / 2.0
factor = 1.0 if index < 15 else 2.0
bars.append(
CanonicalBarRecord(
trading_date=session,
symbol=symbol,
open=raw * 0.999,
high=raw * 1.01,
low=raw * 0.99,
close=raw,
volume=1_000_000,
money=raw * 1_000_000,
paused=False,
source="fixture-provider",
is_st=(
symbol == st_symbol_on_last_session
and session == sessions[-1]
),
adjustment="none",
adjustment_factor=factor,
previous_close=raw / 1.005,
limit_up=raw * 1.10,
limit_down=raw * 0.90,
)
)
memberships.append(
IndexMembershipRecord(
effective_date=session,
index_symbol="000905.XSHG",
member_symbol=symbol,
source="fixture-provider",
)
)
payload = build_snapshot_payload(
bars=bars,
memberships=memberships,
provider="fixture-provider",
provider_version="1",
retrieved_at=datetime(2024, 3, 1, tzinfo=timezone.utc),
next_trading_session=next_trading_session,
query={
"index_symbol": "000905.XSHG",
"start_date": sessions[0].isoformat(),
"end_date": sessions[-1].isoformat(),
"trading_sessions": [
item.isoformat() for item in frozen_calendar
],
"adjustment": "none",
},
)
paths = write_data_snapshot(payload, root)
return paths["manifest"], sessions[-1]
class SnapshotPipelineTests(unittest.TestCase):
@staticmethod
def _broker_snapshot(
as_of: date,
*,
observed_at: datetime | None = None,
next_session: date = date(2024, 2, 13),
) -> dict:
signal_timestamp = f"{as_of.isoformat()}T15:00:00+08:00"
executable_window = first_executable_window(next_session)
observation_timestamp = (
observed_at.isoformat()
if observed_at is not None
else executable_window["start"]
)
return {
"schema_version": "1.0",
"snapshot_id": "broker-fixture",
"signal_as_of": signal_timestamp,
"next_trading_session": next_session.isoformat(),
"first_executable_window": executable_window,
"observation_as_of": observation_timestamp,
"as_of": observation_timestamp,
"source_time": observation_timestamp,
"broker": "fixture",
"account_hash": "fixture-account-hash",
"cash": 1_000_000,
"total_asset": 1_000_000,
"market_value": 0,
"positions": [],
"open_orders": [],
"metadata": {},
}
def test_next_day_broker_observation_binds_to_prior_close_signal(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(lookback=5, top_n=1),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot_manifest, as_of = build_provider_snapshot(
root / "snapshot"
)
observed_at = datetime(
as_of.year,
as_of.month,
as_of.day,
9,
0,
tzinfo=timezone(timedelta(hours=8)),
) + timedelta(days=1)
broker = self._broker_snapshot(
as_of,
observed_at=observed_at,
)
result = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
broker_snapshot=broker,
)
self.assertEqual(
result["decision"]["signal_as_of"],
f"{as_of.isoformat()}T15:00:00+08:00",
)
self.assertEqual(
result["decision"]["broker_snapshot"][
"observation_as_of"
],
observed_at.isoformat(),
)
paths = write_snapshot_decision(
result,
root / "decision",
)
verified = verify_snapshot_decision(paths["manifest"])
self.assertEqual(
verified["signal_as_of"],
f"{as_of.isoformat()}T15:00:00+08:00",
)
def test_verified_snapshot_builds_deterministic_portable_target(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(
lookback=5,
top_n=1,
max_weight=0.50,
gross_target=0.50,
cash_buffer=0.10,
),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot_manifest, as_of = build_provider_snapshot(
root / "snapshot"
)
first = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
equity=1_000_000,
)
second = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
equity=1_000_000,
)
self.assertEqual(first, second)
self.assertEqual(len(first["signals"]), 2)
self.assertGreater(
next(
row["score"]
for row in first["signals"]
if row["symbol"] == "000001.XSHE"
),
next(
row["score"]
for row in first["signals"]
if row["symbol"] == "600000.XSHG"
),
)
selected = [
row
for row in first["targets"]
if row["target_weight"] > 0
]
self.assertEqual(
[row["symbol"] for row in selected],
["000001.XSHE"],
)
paths = write_snapshot_decision(first, root / "decision")
verified = verify_snapshot_decision(paths["manifest"])
self.assertTrue(verified["ok"])
self.assertEqual(verified["signal_count"], 2)
self.assertEqual(verified["target_count"], 2)
def test_pit_st_member_is_excluded_before_portable_ranking(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(
lookback=5,
top_n=1,
max_weight=0.50,
gross_target=0.50,
cash_buffer=0.10,
),
)
with tempfile.TemporaryDirectory() as directory:
snapshot_manifest, as_of = build_provider_snapshot(
Path(directory) / "snapshot",
st_symbol_on_last_session="000001.XSHE",
)
result = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
equity=1_000_000,
)
self.assertEqual(
result["decision"]["universe"]["000001.XSHE"]["state"],
"excluded",
)
self.assertIn(
"ST_EXCLUDED",
result["decision"]["universe"]["000001.XSHE"]["reasons"],
)
self.assertNotIn(
"000001.XSHE",
result["decision"]["targets"],
)
def test_missing_exact_membership_fails_closed(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(lookback=5, top_n=1),
)
with tempfile.TemporaryDirectory() as directory:
snapshot_manifest, as_of = build_provider_snapshot(
Path(directory) / "snapshot"
)
with self.assertRaisesRegex(
SnapshotDecisionError,
"no next session",
):
build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of + timedelta(days=1),
equity=1_000_000,
)
def test_provider_snapshot_runs_local_event_backtest(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(
lookback=5,
top_n=1,
max_weight=0.50,
gross_target=0.50,
cash_buffer=0.10,
),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot_manifest, unused_as_of = build_provider_snapshot(
root / "snapshot"
)
del unused_as_of
with patch.object(
snapshot_backtest_module,
"load_verified_data_snapshot",
wraps=(
snapshot_backtest_module.load_verified_data_snapshot
),
) as snapshot_loader:
result = run_snapshot_backtest(
snapshot_manifest=snapshot_manifest,
config=config,
)
self.assertEqual(snapshot_loader.call_count, 1)
self.assertGreater(result.report["decision_count"], 0)
self.assertEqual(
result.report["evidence_class"],
"provider-snapshot-local-backtest",
)
self.assertFalse(result.report["real_platform_backtest"])
self.assertFalse(result.report["investment_value_claim"])
self.assertGreater(len(result.signals), 0)
paths = result.write_artifacts(root / "backtest")
self.assertTrue(
verify_artifact_manifest(paths["manifest"])["ok"]
)
def test_decision_tampering_is_rejected(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(lookback=5, top_n=1),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot_manifest, as_of = build_provider_snapshot(
root / "snapshot"
)
result = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
equity=1_000_000,
)
paths = write_snapshot_decision(result, root / "decision")
decision_path = Path(paths["decision"])
decision = json.loads(decision_path.read_text(encoding="utf-8"))
decision["orders"] = {}
decision_path.write_text(json.dumps(decision), encoding="utf-8")
with self.assertRaisesRegex(
SnapshotDecisionError,
"artifact hash mismatch",
):
verify_snapshot_decision(paths["manifest"])
def test_copied_input_calendar_cannot_be_rebound_by_artifact_hash(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(lookback=5, top_n=1),
)
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot_manifest, as_of = build_provider_snapshot(
root / "snapshot"
)
result = build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
equity=1_000_000,
)
paths = write_snapshot_decision(result, root / "decision")
input_path = Path(paths["input_data_manifest"])
copied = json.loads(input_path.read_text(encoding="utf-8"))
copied["query"]["trading_sessions"][-1] = "2024-02-14"
input_path.write_text(
json.dumps(
copied,
ensure_ascii=False,
indent=2,
sort_keys=True,
allow_nan=False,
)
+ "\n",
encoding="utf-8",
)
manifest_path = Path(paths["manifest"])
manifest = json.loads(
manifest_path.read_text(encoding="utf-8")
)
manifest["artifact_sha256"][
"input_data_manifest.json"
] = hashlib.sha256(input_path.read_bytes()).hexdigest()
manifest_path.write_text(
json.dumps(
manifest,
ensure_ascii=False,
indent=2,
sort_keys=True,
allow_nan=False,
)
+ "\n",
encoding="utf-8",
)
with self.assertRaisesRegex(
SnapshotDecisionError,
"calendar manifest hash binding mismatch",
):
verify_snapshot_decision(manifest_path)
def test_wrong_window_stale_source_or_duplicate_broker_facts_fail_closed(self):
config = BacktestConfig(
symbols=("600000.XSHG", "000001.XSHE"),
strategy=StrategyConfig(lookback=5, top_n=1),
)
with tempfile.TemporaryDirectory() as directory:
snapshot_manifest, as_of = build_provider_snapshot(
Path(directory) / "snapshot"
)
observed_at = datetime(
as_of.year,
as_of.month,
as_of.day,
9,
2,
tzinfo=timezone(timedelta(hours=8)),
) + timedelta(days=1)
for source_time, message in (
(
(observed_at - timedelta(seconds=61)).isoformat(),
"more than 60s stale",
),
(
(observed_at + timedelta(seconds=6)).isoformat(),
"more than 5s after",
),
):
with self.subTest(source_time=source_time):
broker = self._broker_snapshot(
as_of,
observed_at=observed_at,
)
broker["source_time"] = source_time
with self.assertRaisesRegex(
SnapshotDecisionError,
message,
):
build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
broker_snapshot=broker,
)
signal_time = datetime(
as_of.year,
as_of.month,
as_of.day,
15,
0,
tzinfo=timezone(timedelta(hours=8)),
)
for observed_at, message in (
(
signal_time + timedelta(minutes=1),
"outside the unique next session",
),
(
observed_at.replace(hour=9, minute=30),
"outside the unique next session",
),
(
observed_at + timedelta(days=3),
"outside the unique next session",
),
):
with self.subTest(observed_at=observed_at):
broker = self._broker_snapshot(
as_of,
observed_at=observed_at,
)
with self.assertRaisesRegex(
SnapshotDecisionError,
message,
):
build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
broker_snapshot=broker,
)
for field in ("signal_as_of", "observation_as_of"):
with self.subTest(missing=field):
broker = self._broker_snapshot(as_of)
del broker[field]
with self.assertRaisesRegex(
SnapshotDecisionError,
rf"missing required fields.*{field}",
):
build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
broker_snapshot=broker,
)
duplicate = self._broker_snapshot(as_of)
position = {
"symbol": "600000.XSHG",
"quantity": 100,
"sellable_quantity": 100,
"average_cost": 10,
"market_value": 1000,
}
duplicate["positions"] = [position, dict(position)]
with self.assertRaisesRegex(
SnapshotDecisionError,
"duplicate position",
):
build_snapshot_decision(
snapshot_manifest=snapshot_manifest,
config=config,
as_of=as_of,
broker_snapshot=duplicate,
)
if __name__ == "__main__":
unittest.main()