quantbt-engine 1.0.7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. quantbt/__init__.py +862 -0
  2. quantbt/adapters/__init__.py +5 -0
  3. quantbt/adapters/nautilus/__init__.py +42 -0
  4. quantbt/adapters/nautilus/_dependency.py +53 -0
  5. quantbt/adapters/nautilus/backend.py +639 -0
  6. quantbt/adapters/nautilus/instruments.py +275 -0
  7. quantbt/adapters/nautilus/options.py +465 -0
  8. quantbt/adapters/nautilus/reports.py +231 -0
  9. quantbt/backends/__init__.py +29 -0
  10. quantbt/backends/_native_event_rust.py +2168 -0
  11. quantbt/backends/native_event.py +5048 -0
  12. quantbt/backends/native_option.py +796 -0
  13. quantbt/backends/native_portfolio.py +928 -0
  14. quantbt/backends/native_vectorized.py +1299 -0
  15. quantbt/backtester.py +485 -0
  16. quantbt/benchmarks/__init__.py +1 -0
  17. quantbt/benchmarks/compare_phase9_parity.py +302 -0
  18. quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
  19. quantbt/benchmarks/profile_phase7.py +415 -0
  20. quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
  21. quantbt/benchmarks/run_optimization_overhead.py +195 -0
  22. quantbt/benchmarks/run_options_engine.py +272 -0
  23. quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
  24. quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
  25. quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
  26. quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
  27. quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
  28. quantbt/benchmarks/run_phase14_service_loop.py +694 -0
  29. quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
  30. quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
  31. quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
  32. quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
  33. quantbt/benchmarks/run_phase31_intrabar.py +441 -0
  34. quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
  35. quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
  36. quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
  37. quantbt/benchmarks/run_phase7.py +654 -0
  38. quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
  39. quantbt/core/__init__.py +323 -0
  40. quantbt/core/arbitrage.py +767 -0
  41. quantbt/core/basket.py +234 -0
  42. quantbt/core/certification.py +274 -0
  43. quantbt/core/constraints.py +155 -0
  44. quantbt/core/engine.py +1144 -0
  45. quantbt/core/event.py +869 -0
  46. quantbt/core/execution_contract.py +197 -0
  47. quantbt/core/execution_depth.py +613 -0
  48. quantbt/core/intrabar_kernel.py +1906 -0
  49. quantbt/core/intrabar_reference.py +907 -0
  50. quantbt/core/intrabar_session.py +156 -0
  51. quantbt/core/market_tape.py +480 -0
  52. quantbt/core/native_event_capabilities.py +121 -0
  53. quantbt/core/native_event_parity.py +336 -0
  54. quantbt/core/order_compiler.py +422 -0
  55. quantbt/core/orders.py +319 -0
  56. quantbt/core/portfolio.py +392 -0
  57. quantbt/core/preprocessor.py +256 -0
  58. quantbt/core/reactive.py +173 -0
  59. quantbt/core/results.py +341 -0
  60. quantbt/core/schema.py +211 -0
  61. quantbt/core/scopes.py +96 -0
  62. quantbt/core/structured_orders.py +378 -0
  63. quantbt/core/types.py +87 -0
  64. quantbt/core/vectorized.py +203 -0
  65. quantbt/endpoint.py +4465 -0
  66. quantbt/engines.py +1014 -0
  67. quantbt/metrics/__init__.py +45 -0
  68. quantbt/metrics/options_analytics.py +46 -0
  69. quantbt/metrics/performance.py +541 -0
  70. quantbt/optimization/__init__.py +94 -0
  71. quantbt/optimization/callbacks.py +116 -0
  72. quantbt/optimization/candidate_selection.py +397 -0
  73. quantbt/optimization/config.py +84 -0
  74. quantbt/optimization/constraints.py +22 -0
  75. quantbt/optimization/evaluator.py +21 -0
  76. quantbt/optimization/evaluators/__init__.py +32 -0
  77. quantbt/optimization/evaluators/arbitrage.py +22 -0
  78. quantbt/optimization/evaluators/generic.py +34 -0
  79. quantbt/optimization/evaluators/grid_dca.py +22 -0
  80. quantbt/optimization/evaluators/intrabar.py +54 -0
  81. quantbt/optimization/evaluators/native_event.py +51 -0
  82. quantbt/optimization/evaluators/options.py +22 -0
  83. quantbt/optimization/evaluators/portfolio.py +42 -0
  84. quantbt/optimization/evaluators/signal.py +43 -0
  85. quantbt/optimization/multiseed.py +176 -0
  86. quantbt/optimization/objectives.py +221 -0
  87. quantbt/optimization/optimizer.py +500 -0
  88. quantbt/optimization/result.py +80 -0
  89. quantbt/optimization/samplers.py +84 -0
  90. quantbt/optimization/space.py +223 -0
  91. quantbt/options/__init__.py +215 -0
  92. quantbt/options/cache.py +116 -0
  93. quantbt/options/conventions.py +158 -0
  94. quantbt/options/data.py +177 -0
  95. quantbt/options/execution.py +600 -0
  96. quantbt/options/fees.py +135 -0
  97. quantbt/options/greeks.py +173 -0
  98. quantbt/options/hedging.py +232 -0
  99. quantbt/options/iv.py +188 -0
  100. quantbt/options/ledger.py +263 -0
  101. quantbt/options/lifecycle.py +94 -0
  102. quantbt/options/margin.py +311 -0
  103. quantbt/options/packages.py +147 -0
  104. quantbt/options/pricing.py +191 -0
  105. quantbt/options/schema.py +227 -0
  106. quantbt/options/selectors.py +281 -0
  107. quantbt/options/strategy.py +249 -0
  108. quantbt/options/surface.py +142 -0
  109. quantbt/options/tape.py +226 -0
  110. quantbt/options/templates/__init__.py +33 -0
  111. quantbt/options/templates/packages.py +354 -0
  112. quantbt/portfolio.py +606 -0
  113. quantbt/py.typed +1 -0
  114. quantbt/reporting/__init__.py +38 -0
  115. quantbt/reporting/arbitrage_audit.py +211 -0
  116. quantbt/reporting/nautilus_bundle.py +758 -0
  117. quantbt/reporting/nautilus_certification.py +226 -0
  118. quantbt/reporting/nautilus_diagnostics.py +225 -0
  119. quantbt/reporting/parity.py +496 -0
  120. quantbt/reporting/portfolio_audit.py +206 -0
  121. quantbt/reporting/portfolio_nautilus.py +140 -0
  122. quantbt/sizing/__init__.py +3 -0
  123. quantbt/sizing/fast.py +67 -0
  124. quantbt/sizing/modes.py +162 -0
  125. quantbt/viz/__init__.py +4 -0
  126. quantbt/viz/plots.py +310 -0
  127. quantbt/viz/themes.py +102 -0
  128. quantbt/walkforward.py +3136 -0
  129. quantbt_engine-1.0.7.dist-info/METADATA +891 -0
  130. quantbt_engine-1.0.7.dist-info/RECORD +133 -0
  131. quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
  132. quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
  133. quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
@@ -0,0 +1,514 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 15A Nautilus certification bundle runner.
4
+
5
+ This runner is an evidence-layer tool. It does not change engine semantics.
6
+ Nautilus workflows are optional because they require the external
7
+ `nautilus_trader` dependency and supported test instruments.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Callable, Dict, List, Optional, Tuple
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+
21
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
22
+ PROJECT_DIR = PACKAGE_DIR.parent
23
+ if str(PROJECT_DIR) not in sys.path:
24
+ sys.path.insert(0, str(PROJECT_DIR))
25
+
26
+ from quantbt import ( # noqa: E402
27
+ AccountConfig,
28
+ ArbExecutionPolicy,
29
+ ArbitrageLeg,
30
+ BasisArbitrageSpec,
31
+ BasketLegSpec,
32
+ BasketSpec,
33
+ ContractType,
34
+ HedgePolicy,
35
+ HedgePolicyKind,
36
+ NativeEventBackend,
37
+ NativeEventConfig,
38
+ NautilusToleranceProfile,
39
+ OrderIntent,
40
+ OrderSide,
41
+ OrderType,
42
+ PackageExecutionKind,
43
+ QuantBTEndpoint,
44
+ SizingPolicy,
45
+ SizingPolicyKind,
46
+ TimeInForce,
47
+ export_nautilus_report_bundle,
48
+ write_nautilus_certification_artifacts,
49
+ )
50
+
51
+
52
+ WORKFLOWS = (
53
+ "pct_equity_signal",
54
+ "explicit_orders",
55
+ "basket_package",
56
+ "portfolio_package",
57
+ "basis_arbitrage_package",
58
+ )
59
+
60
+
61
+ def run_certification(
62
+ *,
63
+ rows: int = 96,
64
+ include_nautilus: bool = False,
65
+ output_dir: str | Path = PACKAGE_DIR / "benchmarks" / "phase15a_nautilus_bundles",
66
+ make_quantstats: bool = False,
67
+ ) -> Dict:
68
+ output_path = Path(output_dir)
69
+ output_path.mkdir(parents=True, exist_ok=True)
70
+ if not include_nautilus:
71
+ workflows = [
72
+ {
73
+ "workflow": name,
74
+ "status": "skipped",
75
+ "reason": "run with --include-nautilus",
76
+ "bundle_dir": None,
77
+ }
78
+ for name in WORKFLOWS
79
+ ]
80
+ return _summary(workflows=workflows, output_dir=output_path, include_nautilus=False)
81
+
82
+ availability = _nautilus_available()
83
+ if availability is not None:
84
+ workflows = [
85
+ {
86
+ "workflow": name,
87
+ "status": "skipped",
88
+ "reason": availability,
89
+ "bundle_dir": None,
90
+ }
91
+ for name in WORKFLOWS
92
+ ]
93
+ return _summary(workflows=workflows, output_dir=output_path, include_nautilus=True)
94
+
95
+ runners: Dict[str, Callable[[int, Path, bool], Dict]] = {
96
+ "pct_equity_signal": _run_pct_equity_signal,
97
+ "explicit_orders": _run_explicit_orders,
98
+ "basket_package": _run_basket_package,
99
+ "portfolio_package": _run_portfolio_package,
100
+ "basis_arbitrage_package": _run_basis_arbitrage_package,
101
+ }
102
+ workflows = []
103
+ for name in WORKFLOWS:
104
+ try:
105
+ workflows.append(runners[name](int(rows), output_path, bool(make_quantstats)))
106
+ except ImportError as exc:
107
+ workflows.append({"workflow": name, "status": "skipped", "reason": str(exc), "bundle_dir": None})
108
+ except NotImplementedError as exc:
109
+ workflows.append({"workflow": name, "status": "skipped", "reason": str(exc), "bundle_dir": None})
110
+ except Exception as exc: # pragma: no cover - only hit with optional external backend drift
111
+ workflows.append({"workflow": name, "status": "failed", "reason": f"{type(exc).__name__}: {exc}", "bundle_dir": None})
112
+ return _summary(workflows=workflows, output_dir=output_path, include_nautilus=True)
113
+
114
+
115
+ def make_markdown(report: Dict) -> str:
116
+ lines = [
117
+ "# Phase 15A Nautilus Certification Bundles",
118
+ "",
119
+ f"Status: **{report['status']}**",
120
+ "",
121
+ f"- Include Nautilus: `{report['include_nautilus']}`",
122
+ f"- Output directory: `{report['output_dir']}`",
123
+ f"- Passed workflows: `{report['passed_workflows']}`",
124
+ f"- Skipped workflows: `{report['skipped_workflows']}`",
125
+ f"- Failed workflows: `{report['failed_workflows']}`",
126
+ "",
127
+ "## Workflow Matrix",
128
+ "",
129
+ "| workflow | status | bundle | tolerance status | reason |",
130
+ "| --- | --- | --- | --- | --- |",
131
+ ]
132
+ for item in report["workflows"]:
133
+ lines.append(
134
+ "| `{workflow}` | `{status}` | `{bundle}` | `{tol}` | {reason} |".format(
135
+ workflow=item["workflow"],
136
+ status=item["status"],
137
+ bundle=item.get("bundle_dir") or "",
138
+ tol=item.get("tolerance_status") or "",
139
+ reason=item.get("reason", ""),
140
+ )
141
+ )
142
+ lines.extend(
143
+ [
144
+ "",
145
+ "## Required Bundle Files",
146
+ "",
147
+ "- `config.json`",
148
+ "- `run_manifest.json`",
149
+ "- `metrics_summary.json`",
150
+ "- `equity_curve.csv`, `returns.csv`, `account_report.csv`",
151
+ "- `orders_report.csv`, `fills_report.csv`, `positions_report.csv`",
152
+ "- `trade_log.csv`, `fill_log.txt`",
153
+ "- `native_vs_nautilus_parity.csv`",
154
+ "- `tolerance_profile.json`",
155
+ "- `known_differences.md`",
156
+ "",
157
+ "## Interpretation",
158
+ "",
159
+ "A skipped workflow is not a pass claim. It means the optional Nautilus dependency or instrument route was not available in this environment. A pass means the workflow produced a bundle and satisfied the declared tolerance profile.",
160
+ ]
161
+ )
162
+ return "\n".join(lines) + "\n"
163
+
164
+
165
+ def _run_pct_equity_signal(rows: int, output_dir: Path, make_quantstats: bool) -> Dict:
166
+ data = _single_data(rows)
167
+ signal = _signal(data.index)
168
+ native = QuantBTEndpoint.pct_equity(
169
+ initial_capital=20_000.0,
170
+ leverage=3.0,
171
+ alloc_per_trade=0.4,
172
+ fee=0.0004,
173
+ slippage=0.0,
174
+ use_funding=False,
175
+ use_pyramiding=False,
176
+ )
177
+ native_result = native.backtest(data=data, signal=signal)
178
+
179
+ from quantbt.adapters.nautilus import NautilusBackendConfig
180
+
181
+ nt = QuantBTEndpoint.nautilus_validation(
182
+ initial_capital=20_000.0,
183
+ leverage=3.0,
184
+ alloc_per_trade=0.4,
185
+ hedge_type="%_equity",
186
+ fee_rate=0.0002,
187
+ use_funding=False,
188
+ use_pyramiding=False,
189
+ nautilus_config=NautilusBackendConfig(
190
+ instrument_id="BTCUSDT-PERP.BINANCE",
191
+ timeframe="1h",
192
+ trade_notional=0.4,
193
+ bypass_risk=True,
194
+ close_positions_on_stop=False,
195
+ ),
196
+ )
197
+ nt_result = nt.simulate(data=data, signal=signal, symbols=["BTCUSDT-PERP.BINANCE"])
198
+ return _export_workflow(
199
+ workflow="pct_equity_signal",
200
+ native_result=native_result,
201
+ nautilus_result=nt_result,
202
+ output_dir=output_dir,
203
+ make_quantstats=make_quantstats,
204
+ known_differences=[
205
+ "Nautilus signal validation uses adapter-generated market orders from target signals.",
206
+ "Custom slippage and funding support depend on the current Nautilus adapter route.",
207
+ ],
208
+ tolerance=NautilusToleranceProfile(equity_tolerance=5.0, position_tolerance=0.01, quantity_tolerance=0.01),
209
+ )
210
+
211
+
212
+ def _run_explicit_orders(rows: int, output_dir: Path, make_quantstats: bool) -> Dict:
213
+ data = _single_data(rows)
214
+ idx = data.index
215
+ orders = (
216
+ OrderIntent(idx[5], "BTCUSDT-PERP.BINANCE", OrderSide.BUY, OrderType.MARKET, qty=0.05, tif=TimeInForce.IOC),
217
+ OrderIntent(idx[20], "BTCUSDT-PERP.BINANCE", OrderSide.SELL, OrderType.MARKET, qty=0.05, tif=TimeInForce.IOC),
218
+ )
219
+ native = QuantBTEndpoint.orders(
220
+ backend="native_event",
221
+ initial_capital=20_000.0,
222
+ leverage=3.0,
223
+ fee_rate=0.0002,
224
+ use_funding=False,
225
+ ).simulate(data=data, orders=orders, symbols=["BTCUSDT-PERP.BINANCE"])
226
+
227
+ from quantbt.adapters.nautilus import NautilusBackendConfig
228
+
229
+ nt = QuantBTEndpoint.orders(
230
+ backend="nautilus",
231
+ initial_capital=20_000.0,
232
+ leverage=3.0,
233
+ fee_rate=0.0002,
234
+ use_funding=False,
235
+ nautilus_config=NautilusBackendConfig(
236
+ instrument_id="BTCUSDT-PERP.BINANCE",
237
+ timeframe="1h",
238
+ bypass_risk=True,
239
+ ),
240
+ ).simulate(data=data, orders=orders, symbols=["BTCUSDT-PERP.BINANCE"])
241
+ return _export_workflow("explicit_orders", native, nt, output_dir, make_quantstats)
242
+
243
+
244
+ def _run_basket_package(rows: int, output_dir: Path, make_quantstats: bool) -> Dict:
245
+ data = _multi_data(rows, ("BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"))
246
+ idx = next(iter(data.values())).index
247
+ signal = pd.Series(0.0, index=idx)
248
+ signal.iloc[5:30] = 1.0
249
+ basket = BasketSpec(
250
+ basket_id="PHASE15A_BASKET",
251
+ legs=(BasketLegSpec("BTCUSDT-PERP.BINANCE", 1.0), BasketLegSpec("ETHUSDT-PERP.BINANCE", -1.0)),
252
+ gross_notional=5_000.0,
253
+ freeze_hedge=True,
254
+ )
255
+ native = QuantBTEndpoint.basket(
256
+ basket=basket,
257
+ backend="native_event",
258
+ initial_capital=50_000.0,
259
+ leverage=3.0,
260
+ fee_rate=0.0002,
261
+ use_funding=False,
262
+ ).simulate(data=data, signal=signal, symbols=list(data))
263
+
264
+ from quantbt.adapters.nautilus import NautilusBackendConfig
265
+
266
+ nt = QuantBTEndpoint.basket(
267
+ basket=basket,
268
+ backend="nautilus",
269
+ initial_capital=50_000.0,
270
+ leverage=3.0,
271
+ fee_rate=0.0002,
272
+ use_funding=False,
273
+ nautilus_config=NautilusBackendConfig(
274
+ instrument_id="BTCUSDT-PERP.BINANCE",
275
+ timeframe="1h",
276
+ bypass_risk=True,
277
+ ),
278
+ ).simulate(data=data, signal=signal, symbols=list(data))
279
+ return _export_workflow("basket_package", native, nt, output_dir, make_quantstats)
280
+
281
+
282
+ def _run_portfolio_package(rows: int, output_dir: Path, make_quantstats: bool) -> Dict:
283
+ symbols = ("BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE")
284
+ data = _multi_data(rows, symbols)
285
+ idx = next(iter(data.values())).index
286
+ positions = pd.DataFrame(
287
+ {
288
+ symbols[0]: np.where(np.arange(len(idx)) % 24 < 12, 1.0, 0.0),
289
+ symbols[1]: np.where(np.arange(len(idx)) % 24 < 12, -1.0, 0.0),
290
+ },
291
+ index=idx,
292
+ )
293
+ native = QuantBTEndpoint.portfolio(
294
+ portfolio_mode="market_neutral",
295
+ backend="native_portfolio",
296
+ initial_capital=50_000.0,
297
+ leverage=3.0,
298
+ fee_rate=0.0002,
299
+ hedge_type="signal_notional",
300
+ alloc_per_trade={symbols[0]: 2_500.0, symbols[1]: 2_500.0},
301
+ use_funding=False,
302
+ ).backtest(data=data, positions=positions, symbols=list(symbols))
303
+
304
+ from quantbt.adapters.nautilus import NautilusBackendConfig
305
+
306
+ nt = QuantBTEndpoint.portfolio(
307
+ portfolio_mode="market_neutral",
308
+ backend="nautilus",
309
+ initial_capital=50_000.0,
310
+ leverage=3.0,
311
+ fee_rate=0.0002,
312
+ hedge_type="signal_notional",
313
+ alloc_per_trade={symbols[0]: 2_500.0, symbols[1]: 2_500.0},
314
+ use_funding=False,
315
+ metadata={"portfolio_nautilus_equity_tolerance": 5.0, "portfolio_nautilus_position_tolerance": 0.01},
316
+ nautilus_config=NautilusBackendConfig(
317
+ instrument_id=symbols[0],
318
+ timeframe="1h",
319
+ bypass_risk=True,
320
+ ),
321
+ ).simulate(data=data, positions=positions, symbols=list(symbols))
322
+ return _export_workflow(
323
+ "portfolio_package",
324
+ native,
325
+ nt,
326
+ output_dir,
327
+ make_quantstats,
328
+ known_differences=["Portfolio route submits native transformed target-unit deltas to Nautilus package replay."],
329
+ tolerance=NautilusToleranceProfile(equity_tolerance=5.0, position_tolerance=0.01, quantity_tolerance=0.01),
330
+ )
331
+
332
+
333
+ def _run_basis_arbitrage_package(rows: int, output_dir: Path, make_quantstats: bool) -> Dict:
334
+ symbols = ("BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE")
335
+ data = _multi_data(rows, symbols)
336
+ closes = {symbol: frame["close"] for symbol, frame in data.items()}
337
+ idx = next(iter(data.values())).index
338
+ signal = pd.Series(0.0, index=idx)
339
+ signal.iloc[8:40] = 1.0
340
+ spec = BasisArbitrageSpec(
341
+ arb_id="PHASE15A_BASIS",
342
+ legs=(
343
+ ArbitrageLeg(symbols[0], 1.0, role="perp", contract_type=ContractType.LINEAR, funding_enabled=True),
344
+ ArbitrageLeg(symbols[1], -1.0, role="quarterly", contract_type=ContractType.LINEAR),
345
+ ),
346
+ hedge_policy=HedgePolicy(HedgePolicyKind.BASE_QTY_EQUAL, freeze_on_entry=True),
347
+ sizing_policy=SizingPolicy(
348
+ SizingPolicyKind.TARGET_NOTIONAL_TO_BASE_QTY,
349
+ notional=5_000.0,
350
+ reference_symbol=symbols[0],
351
+ ),
352
+ execution_policy=ArbExecutionPolicy(PackageExecutionKind.ATOMIC_ALL_OR_NONE),
353
+ )
354
+ native = NativeEventBackend(
355
+ NativeEventConfig(account=AccountConfig(initial_capital=50_000.0, leverage=3.0), fee_rate=0.0002, use_funding=False)
356
+ ).run_basis_arbitrage(idx, spec, signal, closes, funding_rate=0.0)
357
+
358
+ from quantbt.adapters.nautilus import NautilusBackendConfig
359
+
360
+ nt = QuantBTEndpoint.arbitrage(
361
+ "basis",
362
+ spec=spec,
363
+ backend="nautilus",
364
+ initial_capital=50_000.0,
365
+ leverage=3.0,
366
+ fee_rate=0.0002,
367
+ use_funding=False,
368
+ nautilus_config=NautilusBackendConfig(
369
+ instrument_id=symbols[0],
370
+ timeframe="1h",
371
+ bypass_risk=True,
372
+ ),
373
+ ).simulate(data=data, signal=signal, symbols=list(symbols))
374
+ return _export_workflow(
375
+ "basis_arbitrage_package",
376
+ native,
377
+ nt,
378
+ output_dir,
379
+ make_quantstats,
380
+ known_differences=[
381
+ "This smoke uses supported perpetual test instruments as a package proxy, not a real delivery-futures venue model.",
382
+ ],
383
+ tolerance=NautilusToleranceProfile(equity_tolerance=5.0, position_tolerance=0.01, quantity_tolerance=0.01),
384
+ )
385
+
386
+
387
+ def _export_workflow(
388
+ workflow: str,
389
+ native_result,
390
+ nautilus_result,
391
+ output_dir: Path,
392
+ make_quantstats: bool,
393
+ known_differences: Optional[List[str]] = None,
394
+ tolerance: NautilusToleranceProfile | None = None,
395
+ ) -> Dict:
396
+ bundle_dir = export_nautilus_report_bundle(
397
+ result=nautilus_result,
398
+ output_dir=output_dir / workflow,
399
+ strategy_id=workflow,
400
+ config={"certification_workflow": workflow},
401
+ make_quantstats=make_quantstats,
402
+ fill_log_limit=200,
403
+ )
404
+ artifacts = write_nautilus_certification_artifacts(
405
+ native_result=native_result,
406
+ nautilus_result=nautilus_result,
407
+ report_dir=bundle_dir,
408
+ workflow=workflow,
409
+ tolerance=tolerance or NautilusToleranceProfile(equity_tolerance=5.0, position_tolerance=0.01, quantity_tolerance=0.01),
410
+ known_differences=known_differences,
411
+ )
412
+ profile = artifacts["tolerance_profile"]
413
+ return {
414
+ "workflow": workflow,
415
+ "status": "pass" if profile["passed"] else "diff",
416
+ "bundle_dir": str(bundle_dir),
417
+ "tolerance_status": profile["status"],
418
+ "checks": profile["checks"],
419
+ "artifact_files": artifacts["artifact_files"],
420
+ }
421
+
422
+
423
+ def _summary(workflows: List[Dict], output_dir: Path, include_nautilus: bool) -> Dict:
424
+ failed = [item for item in workflows if item["status"] == "failed"]
425
+ passed = [item for item in workflows if item["status"] == "pass"]
426
+ skipped = [item for item in workflows if item["status"] == "skipped"]
427
+ diff = [item for item in workflows if item["status"] == "diff"]
428
+ status = "fail" if failed else "pass"
429
+ return {
430
+ "status": status,
431
+ "include_nautilus": bool(include_nautilus),
432
+ "output_dir": str(output_dir),
433
+ "workflows": workflows,
434
+ "passed_workflows": len(passed),
435
+ "skipped_workflows": len(skipped),
436
+ "diff_workflows": len(diff),
437
+ "failed_workflows": len(failed),
438
+ }
439
+
440
+
441
+ def _nautilus_available() -> Optional[str]:
442
+ try:
443
+ from quantbt.adapters.nautilus import NautilusBacktestEngine
444
+
445
+ NautilusBacktestEngine.check_available()
446
+ return None
447
+ except Exception as exc:
448
+ return f"{type(exc).__name__}: {exc}"
449
+
450
+
451
+ def _single_data(rows: int) -> pd.DataFrame:
452
+ idx = pd.date_range("2024-01-01", periods=int(rows), freq="1h", tz="UTC")
453
+ x = np.linspace(0.0, 10.0, len(idx))
454
+ close = 100.0 + np.cumsum(np.sin(x) * 0.08 + np.cos(x / 2.0) * 0.03)
455
+ return pd.DataFrame(
456
+ {"open": close, "high": close * 1.002, "low": close * 0.998, "close": close, "volume": 1_000.0},
457
+ index=idx,
458
+ )
459
+
460
+
461
+ def _multi_data(rows: int, symbols: Tuple[str, ...]) -> Dict[str, pd.DataFrame]:
462
+ base = _single_data(rows)
463
+ out = {}
464
+ for i, symbol in enumerate(symbols):
465
+ frame = base.copy()
466
+ scale = 1.0 + i * 0.15
467
+ frame[["open", "high", "low", "close"]] = frame[["open", "high", "low", "close"]] * scale
468
+ frame["volume"] = frame["volume"] * (1.0 + i)
469
+ out[symbol] = frame
470
+ return out
471
+
472
+
473
+ def _signal(idx: pd.DatetimeIndex) -> pd.Series:
474
+ signal = pd.Series(0.0, index=idx)
475
+ signal.iloc[5 : max(6, len(idx) // 3)] = 1.0
476
+ signal.iloc[max(8, len(idx) // 2) : max(9, len(idx) * 2 // 3)] = -1.0
477
+ return signal
478
+
479
+
480
+ def _json_default(value):
481
+ if isinstance(value, (np.bool_,)):
482
+ return bool(value)
483
+ if isinstance(value, (np.integer,)):
484
+ return int(value)
485
+ if isinstance(value, (np.floating,)):
486
+ return float(value)
487
+ if isinstance(value, pd.Timestamp):
488
+ return value.isoformat()
489
+ raise TypeError(f"{type(value).__name__} is not JSON serializable")
490
+
491
+
492
+ def main(argv: Optional[List[str]] = None) -> int:
493
+ parser = argparse.ArgumentParser(description=__doc__)
494
+ parser.add_argument("--rows", type=int, default=96)
495
+ parser.add_argument("--include-nautilus", action="store_true")
496
+ parser.add_argument("--make-quantstats", action="store_true")
497
+ parser.add_argument("--output-dir", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase15a_nautilus_bundles")
498
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase15a_nautilus_certification.json")
499
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase15a_nautilus_certification.md")
500
+ args = parser.parse_args(argv)
501
+ report = run_certification(
502
+ rows=args.rows,
503
+ include_nautilus=args.include_nautilus,
504
+ output_dir=args.output_dir,
505
+ make_quantstats=args.make_quantstats,
506
+ )
507
+ args.json_out.write_text(json.dumps(report, indent=2, sort_keys=True, default=_json_default), encoding="utf-8")
508
+ args.md_out.write_text(make_markdown(report), encoding="utf-8")
509
+ print(make_markdown(report))
510
+ return 0 if report["status"] == "pass" else 1
511
+
512
+
513
+ if __name__ == "__main__":
514
+ raise SystemExit(main())
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 15B synthetic depth evidence runner.
4
+
5
+ This script creates deterministic OHLCV and synthetic-book depth cases. It is
6
+ not a venue L2 replay benchmark; it is an audit artifact for package-depth
7
+ invariants before optional Nautilus validation.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from typing import Dict, List
17
+
18
+ import pandas as pd
19
+
20
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
21
+ PROJECT_DIR = PACKAGE_DIR.parent
22
+ if str(PROJECT_DIR) not in sys.path:
23
+ sys.path.insert(0, str(PROJECT_DIR))
24
+
25
+ from quantbt import ( # noqa: E402
26
+ NautilusExecutionDepthConfig,
27
+ OrderIntent,
28
+ OrderSide,
29
+ OrderType,
30
+ l2_replay_available,
31
+ simulate_nautilus_order_package_depth,
32
+ )
33
+
34
+
35
+ def run_phase15b_synthetic_depth() -> Dict:
36
+ data = {"BTCUSDT-PERP.BINANCE": _frame()}
37
+ idx = data["BTCUSDT-PERP.BINANCE"].index
38
+ cases = [
39
+ (
40
+ "synthetic_market_vwap",
41
+ [
42
+ OrderIntent(
43
+ timestamp=idx[1],
44
+ symbol="BTCUSDT-PERP.BINANCE",
45
+ side=OrderSide.BUY,
46
+ order_type=OrderType.MARKET,
47
+ qty=2.0,
48
+ )
49
+ ],
50
+ NautilusExecutionDepthConfig(
51
+ depth_model="synthetic_book",
52
+ allow_partial_fills=True,
53
+ synthetic_spread_bps=10.0,
54
+ synthetic_level_spacing_bps=10.0,
55
+ synthetic_levels=3,
56
+ synthetic_base_depth_qty=1.0,
57
+ ),
58
+ ),
59
+ (
60
+ "synthetic_partial_queue",
61
+ [
62
+ OrderIntent(
63
+ timestamp=idx[1],
64
+ symbol="BTCUSDT-PERP.BINANCE",
65
+ side=OrderSide.BUY,
66
+ order_type=OrderType.MARKET,
67
+ qty=3.0,
68
+ )
69
+ ],
70
+ NautilusExecutionDepthConfig(
71
+ depth_model="synthetic_book",
72
+ allow_partial_fills=True,
73
+ synthetic_levels=2,
74
+ synthetic_base_depth_qty=1.0,
75
+ queue_ahead_qty=0.5,
76
+ ),
77
+ ),
78
+ (
79
+ "ohlcv_all_or_none_baseline",
80
+ [
81
+ OrderIntent(
82
+ timestamp=idx[1],
83
+ symbol="BTCUSDT-PERP.BINANCE",
84
+ side=OrderSide.BUY,
85
+ order_type=OrderType.MARKET,
86
+ qty=1.0,
87
+ metadata={"package_id": "P1", "package_type": "basket_package"},
88
+ )
89
+ ],
90
+ NautilusExecutionDepthConfig(all_or_none_packages=True),
91
+ ),
92
+ ]
93
+ results: List[Dict] = []
94
+ for name, orders, cfg in cases:
95
+ preflight = simulate_nautilus_order_package_depth(orders, data, cfg)
96
+ row = preflight.order_report.iloc[0].to_dict()
97
+ results.append(
98
+ {
99
+ "case": name,
100
+ "depth_model": cfg.depth_model,
101
+ "status": str(row.get("status")),
102
+ "filled_qty": float(row.get("filled_qty", 0.0)),
103
+ "fill_price": float(row.get("fill_price", 0.0)),
104
+ "levels_consumed": int(row.get("levels_consumed", 0)),
105
+ "accepted_orders": int(preflight.metadata["accepted_orders"]),
106
+ "rejected_orders": int(preflight.metadata["rejected_orders"]),
107
+ }
108
+ )
109
+ return {
110
+ "phase": "15B",
111
+ "status": "pass" if all(item["status"] in {"filled", "partial"} for item in results) else "review",
112
+ "l2_replay_available": bool(l2_replay_available()),
113
+ "cases": results,
114
+ "claim_scope": "Level-2 synthetic stress only; not venue L2 replay.",
115
+ }
116
+
117
+
118
+ def make_markdown(report: Dict) -> str:
119
+ lines = [
120
+ "# Phase 15B Synthetic Depth Evidence",
121
+ "",
122
+ f"Status: **{report['status']}**",
123
+ "",
124
+ f"- L2 replay provider available: `{report['l2_replay_available']}`",
125
+ f"- Claim scope: {report['claim_scope']}",
126
+ "",
127
+ "| case | depth model | status | filled qty | fill price | levels | accepted | rejected |",
128
+ "| --- | --- | --- | ---: | ---: | ---: | ---: | ---: |",
129
+ ]
130
+ for item in report["cases"]:
131
+ lines.append(
132
+ "| `{case}` | `{depth_model}` | `{status}` | {filled_qty:.8f} | {fill_price:.8f} | {levels_consumed} | {accepted_orders} | {rejected_orders} |".format(
133
+ **item
134
+ )
135
+ )
136
+ lines.extend(
137
+ [
138
+ "",
139
+ "## Interpretation",
140
+ "",
141
+ "Synthetic depth proves deterministic queue, participation, spread and level-consumption behavior. It does not certify real exchange queue priority. Real L2 certification remains gated by venue snapshots, incremental updates and trade prints.",
142
+ ]
143
+ )
144
+ return "\n".join(lines) + "\n"
145
+
146
+
147
+ def _frame() -> pd.DataFrame:
148
+ idx = pd.date_range("2024-01-01", periods=4, freq="1h", tz="UTC")
149
+ return pd.DataFrame(
150
+ {
151
+ "open": [100.0, 100.0, 100.0, 100.0],
152
+ "high": [101.0, 101.0, 101.0, 101.0],
153
+ "low": [99.0, 99.0, 99.0, 99.0],
154
+ "close": [100.0, 100.0, 100.0, 100.0],
155
+ "volume": [100.0, 100.0, 100.0, 100.0],
156
+ },
157
+ index=idx,
158
+ )
159
+
160
+
161
+ def main() -> None:
162
+ parser = argparse.ArgumentParser()
163
+ parser.add_argument("--output-json", default=str(PACKAGE_DIR / "benchmarks" / "phase15b_synthetic_depth.json"))
164
+ parser.add_argument("--output-md", default=str(PACKAGE_DIR / "benchmarks" / "phase15b_synthetic_depth.md"))
165
+ args = parser.parse_args()
166
+ report = run_phase15b_synthetic_depth()
167
+ json_path = Path(args.output_json)
168
+ md_path = Path(args.output_md)
169
+ json_path.write_text(json.dumps(report, indent=2), encoding="utf-8")
170
+ md_path.write_text(make_markdown(report), encoding="utf-8")
171
+ print(json.dumps(report, indent=2))
172
+
173
+
174
+ if __name__ == "__main__":
175
+ main()