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,389 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 16 performance-debt closure benchmark.
4
+
5
+ This runner measures the remaining facade/service-loop overhead after Phase
6
+ 13/14 and verifies that prepared service contexts do not change accounting.
7
+ It is intentionally focused on pandas normalization/report construction, not on
8
+ changing domain kernels.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ import time
17
+ import tracemalloc
18
+ from pathlib import Path
19
+ from typing import Callable, Dict, List
20
+
21
+ import numpy as np
22
+ import pandas as pd
23
+
24
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
25
+ PROJECT_DIR = PACKAGE_DIR.parent
26
+ if str(PROJECT_DIR) not in sys.path:
27
+ sys.path.insert(0, str(PROJECT_DIR))
28
+
29
+ from quantbt import QuantBTEndpoint # noqa: E402
30
+ from quantbt.benchmarks.run_phase14_service_loop import run_benchmark as run_phase14_benchmark # noqa: E402
31
+
32
+
33
+ def run_phase16_benchmark(
34
+ *,
35
+ rows: int = 1_440,
36
+ symbols: int = 6,
37
+ replays: int = 8,
38
+ repeats: int = 2,
39
+ include_large_wfo: bool = True,
40
+ ) -> Dict:
41
+ single = _single_service_context_benchmark(rows=rows, replays=replays, repeats=repeats)
42
+ portfolio = _portfolio_service_context_benchmark(rows=rows, symbols=symbols, replays=replays, repeats=repeats)
43
+ report = _portfolio_report_benchmark(rows=rows, symbols=symbols, repeats=repeats)
44
+ large_wfo = (
45
+ run_phase14_benchmark(
46
+ rows=max(rows, 1_440),
47
+ symbols=max(symbols, 6),
48
+ trials=max(8, replays),
49
+ order_count=max(240, rows // 4),
50
+ repeats=max(1, repeats),
51
+ )
52
+ if include_large_wfo
53
+ else {"status": "skipped", "reason": "include_large_wfo=False"}
54
+ )
55
+ parity = {
56
+ "single_service_context": bool(single["parity_passed"]),
57
+ "portfolio_service_context": bool(portfolio["parity_passed"]),
58
+ "portfolio_report_levels": bool(report["parity_passed"]),
59
+ "large_wfo_service_loop": bool(large_wfo.get("status") == "pass") if include_large_wfo else True,
60
+ }
61
+ status = "pass" if all(parity.values()) else "fail"
62
+ return {
63
+ "phase": "16",
64
+ "status": status,
65
+ "rows": int(rows),
66
+ "symbols": int(symbols),
67
+ "replays": int(replays),
68
+ "repeats": int(repeats),
69
+ "service_context": {
70
+ "single_signal_notional": single,
71
+ "native_portfolio": portfolio,
72
+ },
73
+ "report_construction": report,
74
+ "large_wfo_service_loop": _compact_phase14(large_wfo),
75
+ "parity": parity,
76
+ "cython_cpp_recommendation": _cython_cpp_recommendation(large_wfo),
77
+ "closed_debt": [
78
+ "facade-level repeated pandas market normalization can now be avoided with endpoint.prepare_service_context(...)",
79
+ "report construction has an explicit full/minimal benchmark and parity guard",
80
+ "larger WFO/service-loop benchmark is archived before any Cython/C++ decision",
81
+ ],
82
+ "remaining_notes": [
83
+ "normal endpoint.backtest(...) remains backward-compatible and still normalizes defensively per call",
84
+ "prepared service context is opt-in and currently covers native_vectorized signal_notional plus native_portfolio",
85
+ "Cython/C++ should wait until pure kernels, not pandas/report facades, dominate measured runtime",
86
+ ],
87
+ }
88
+
89
+
90
+ def make_markdown(report: Dict) -> str:
91
+ single = report["service_context"]["single_signal_notional"]
92
+ portfolio = report["service_context"]["native_portfolio"]
93
+ rpt = report["report_construction"]
94
+ lines = [
95
+ "# Phase 16 Performance Debt Closure",
96
+ "",
97
+ f"Status: **{report['status']}**",
98
+ "",
99
+ "## Prepared Service Context",
100
+ "",
101
+ "| workload | normal seconds | prepared seconds | speedup | peak MB | parity |",
102
+ "| --- | ---: | ---: | ---: | ---: | --- |",
103
+ _row("single signal_notional", single),
104
+ _row("native portfolio", portfolio),
105
+ "",
106
+ "## Report Construction",
107
+ "",
108
+ "| workload | full seconds | minimal seconds | speedup | parity |",
109
+ "| --- | ---: | ---: | ---: | --- |",
110
+ "| native portfolio reports | `{full_seconds:.6f}` | `{minimal_seconds:.6f}` | `{speedup:.3f}x` | `{parity}` |".format(
111
+ full_seconds=float(rpt["full_seconds"]),
112
+ minimal_seconds=float(rpt["minimal_seconds"]),
113
+ speedup=float(rpt["speedup"]),
114
+ parity=bool(rpt["parity_passed"]),
115
+ ),
116
+ "",
117
+ "## Large WFO / Service Loop",
118
+ "",
119
+ f"- Status: `{report['large_wfo_service_loop'].get('status')}`",
120
+ f"- Rows: `{report['large_wfo_service_loop'].get('rows')}`",
121
+ f"- Symbols: `{report['large_wfo_service_loop'].get('symbols')}`",
122
+ f"- Cython/C++ recommendation: {report['cython_cpp_recommendation']}",
123
+ "",
124
+ "## Closed Debt",
125
+ "",
126
+ ]
127
+ for item in report["closed_debt"]:
128
+ lines.append(f"- {item}")
129
+ lines.extend(["", "## Remaining Notes", ""])
130
+ for item in report["remaining_notes"]:
131
+ lines.append(f"- {item}")
132
+ lines.append("")
133
+ return "\n".join(lines)
134
+
135
+
136
+ def _row(label: str, item: Dict) -> str:
137
+ return "| {label} | `{normal:.6f}` | `{prepared:.6f}` | `{speedup:.3f}x` | `{peak:.3f}` | `{parity}` |".format(
138
+ label=label,
139
+ normal=float(item["normal_seconds"]),
140
+ prepared=float(item["prepared_seconds"]),
141
+ speedup=float(item["speedup"]),
142
+ peak=float(item["peak_memory_mb"]),
143
+ parity=bool(item["parity_passed"]),
144
+ )
145
+
146
+
147
+ def _single_service_context_benchmark(*, rows: int, replays: int, repeats: int) -> Dict:
148
+ data = _single_frame(rows)
149
+ signals = _single_signals(data.index, replays)
150
+
151
+ normal_endpoint = _single_endpoint()
152
+ prepared_endpoint = _single_endpoint()
153
+ context = prepared_endpoint.prepare_service_context(data=data, symbols=["BTC"])
154
+
155
+ normal_results = _run_single_replays(normal_endpoint, data, signals)
156
+ prepared_results = _run_single_context_replays(context, signals)
157
+ normal_seconds = _timeit(lambda: _run_single_replays(normal_endpoint, data, signals), repeats)
158
+ prepared_seconds = _timeit(lambda: _run_single_context_replays(context, signals), repeats)
159
+ peak = _peak_memory_mb(lambda: _run_single_context_replays(context, signals))
160
+ equity_diff = max(
161
+ float(abs(normal.equity.iloc[-1] - prepared.equity.iloc[-1]))
162
+ for normal, prepared in zip(normal_results, prepared_results)
163
+ )
164
+ position_diff = max(
165
+ float(np.max(np.abs(normal.positions.to_numpy() - prepared.positions.to_numpy())))
166
+ for normal, prepared in zip(normal_results, prepared_results)
167
+ )
168
+ return {
169
+ "normal_seconds": float(normal_seconds),
170
+ "prepared_seconds": float(prepared_seconds),
171
+ "speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
172
+ "peak_memory_mb": float(peak),
173
+ "parity_passed": bool(equity_diff <= 1e-9 and position_diff <= 1e-12),
174
+ "final_equity_max_abs_diff": equity_diff,
175
+ "position_max_abs_diff": position_diff,
176
+ "context_metadata": context.metadata,
177
+ }
178
+
179
+
180
+ def _portfolio_service_context_benchmark(*, rows: int, symbols: int, replays: int, repeats: int) -> Dict:
181
+ data, positions_list, symbol_list = _portfolio_inputs(rows, symbols, replays)
182
+ normal_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
183
+ prepared_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
184
+ context = prepared_endpoint.prepare_service_context(data=data, symbols=symbol_list)
185
+
186
+ normal_results = _run_portfolio_replays(normal_endpoint, data, positions_list, symbol_list)
187
+ prepared_results = _run_portfolio_context_replays(context, positions_list)
188
+ normal_seconds = _timeit(lambda: _run_portfolio_replays(normal_endpoint, data, positions_list, symbol_list), repeats)
189
+ prepared_seconds = _timeit(lambda: _run_portfolio_context_replays(context, positions_list), repeats)
190
+ peak = _peak_memory_mb(lambda: _run_portfolio_context_replays(context, positions_list))
191
+ equity_diff = max(
192
+ float(abs(normal.equity.iloc[-1] - prepared.equity.iloc[-1]))
193
+ for normal, prepared in zip(normal_results, prepared_results)
194
+ )
195
+ margin_diff = max(
196
+ float(np.max(np.abs(normal.margin.to_numpy() - prepared.margin.to_numpy())))
197
+ for normal, prepared in zip(normal_results, prepared_results)
198
+ )
199
+ return {
200
+ "normal_seconds": float(normal_seconds),
201
+ "prepared_seconds": float(prepared_seconds),
202
+ "speedup": float(normal_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
203
+ "peak_memory_mb": float(peak),
204
+ "parity_passed": bool(equity_diff <= 1e-8 and margin_diff <= 1e-8),
205
+ "final_equity_max_abs_diff": equity_diff,
206
+ "margin_max_abs_diff": margin_diff,
207
+ "context_metadata": context.metadata,
208
+ }
209
+
210
+
211
+ def _portfolio_report_benchmark(*, rows: int, symbols: int, repeats: int) -> Dict:
212
+ data, positions_list, symbol_list = _portfolio_inputs(rows, symbols, 1)
213
+ full_endpoint = _portfolio_endpoint(symbol_list, report_level="full")
214
+ minimal_endpoint = _portfolio_endpoint(symbol_list, report_level="minimal")
215
+ full = full_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list)
216
+ minimal = minimal_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list)
217
+ full_seconds = _timeit(lambda: full_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list), repeats)
218
+ minimal_seconds = _timeit(lambda: minimal_endpoint.backtest(data=data, positions=positions_list[0], symbols=symbol_list), repeats)
219
+ equity_diff = float(np.max(np.abs(full.equity.to_numpy() - minimal.equity.to_numpy())))
220
+ positions_diff = float(np.max(np.abs(full.positions.to_numpy() - minimal.positions.to_numpy())))
221
+ return {
222
+ "full_seconds": float(full_seconds),
223
+ "minimal_seconds": float(minimal_seconds),
224
+ "speedup": float(full_seconds / minimal_seconds) if minimal_seconds > 0.0 else 0.0,
225
+ "parity_passed": bool(equity_diff <= 1e-8 and positions_diff <= 1e-12),
226
+ "equity_max_abs_diff": equity_diff,
227
+ "positions_max_abs_diff": positions_diff,
228
+ }
229
+
230
+
231
+ def _single_endpoint() -> QuantBTEndpoint:
232
+ return QuantBTEndpoint.signal_notional(
233
+ initial_capital=20_000.0,
234
+ leverage=4.0,
235
+ alloc_per_trade=5_000.0,
236
+ fee_rate=0.0002,
237
+ use_funding=False,
238
+ slippage=0.0001,
239
+ use_pyramiding=True,
240
+ )
241
+
242
+
243
+ def _portfolio_endpoint(symbols: List[str], *, report_level: str) -> QuantBTEndpoint:
244
+ return QuantBTEndpoint.portfolio(
245
+ portfolio_mode="market_neutral",
246
+ backend="native_portfolio",
247
+ hedge_type="signal_notional",
248
+ initial_capital=100_000.0,
249
+ leverage=4.0,
250
+ alloc_per_trade={symbol: 5_000.0 for symbol in symbols},
251
+ fee=0.0004,
252
+ use_funding=False,
253
+ report_level=report_level,
254
+ )
255
+
256
+
257
+ def _single_frame(rows: int) -> pd.DataFrame:
258
+ idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
259
+ close = 100.0 + np.cumsum(np.sin(np.linspace(0.0, 32.0, len(idx))) * 0.03 + 0.002)
260
+ return pd.DataFrame(
261
+ {
262
+ "open": close,
263
+ "high": close * 1.002,
264
+ "low": close * 0.998,
265
+ "close": close,
266
+ "volume": 1_000.0,
267
+ },
268
+ index=idx,
269
+ )
270
+
271
+
272
+ def _single_signals(idx: pd.DatetimeIndex, replays: int) -> List[pd.Series]:
273
+ out = []
274
+ base = np.linspace(0.0, 20.0, len(idx))
275
+ for replay in range(int(replays)):
276
+ out.append(pd.Series(np.sign(np.sin(base + replay * 0.3)), index=idx))
277
+ return out
278
+
279
+
280
+ def _portfolio_inputs(rows: int, symbols: int, replays: int):
281
+ idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
282
+ symbol_list = [f"S{i:02d}" for i in range(int(symbols))]
283
+ data = {}
284
+ for j, symbol in enumerate(symbol_list):
285
+ close = 100.0 + j * 5.0 + np.cumsum(np.sin(np.linspace(0.0, 18.0, len(idx)) + j) * 0.02 + 0.001)
286
+ data[symbol] = pd.DataFrame(
287
+ {
288
+ "open": close,
289
+ "high": close * 1.002,
290
+ "low": close * 0.998,
291
+ "close": close,
292
+ "volume": 1_000.0,
293
+ },
294
+ index=idx,
295
+ )
296
+ positions = []
297
+ base = np.linspace(0.0, 16.0, len(idx))
298
+ for replay in range(int(replays)):
299
+ matrix = {
300
+ symbol: np.sign(np.sin(base + replay * 0.2 + j * 0.5))
301
+ for j, symbol in enumerate(symbol_list)
302
+ }
303
+ positions.append(pd.DataFrame(matrix, index=idx))
304
+ return data, positions, symbol_list
305
+
306
+
307
+ def _run_single_replays(endpoint: QuantBTEndpoint, data: pd.DataFrame, signals: List[pd.Series]):
308
+ return [endpoint.backtest(data=data, signal=signal, symbols=["BTC"]) for signal in signals]
309
+
310
+
311
+ def _run_single_context_replays(context, signals: List[pd.Series]):
312
+ return [context.backtest(signal=signal) for signal in signals]
313
+
314
+
315
+ def _run_portfolio_replays(endpoint: QuantBTEndpoint, data, positions_list, symbols):
316
+ return [endpoint.backtest(data=data, positions=positions, symbols=symbols) for positions in positions_list]
317
+
318
+
319
+ def _run_portfolio_context_replays(context, positions_list):
320
+ return [context.backtest(positions=positions) for positions in positions_list]
321
+
322
+
323
+ def _timeit(func: Callable[[], object], repeats: int) -> float:
324
+ values = []
325
+ for _ in range(max(1, int(repeats))):
326
+ start = time.perf_counter()
327
+ func()
328
+ values.append(time.perf_counter() - start)
329
+ return float(min(values))
330
+
331
+
332
+ def _peak_memory_mb(func: Callable[[], object]) -> float:
333
+ tracemalloc.start()
334
+ func()
335
+ _, peak = tracemalloc.get_traced_memory()
336
+ tracemalloc.stop()
337
+ return float(peak / (1024 * 1024))
338
+
339
+
340
+ def _compact_phase14(report: Dict) -> Dict:
341
+ if report.get("status") == "skipped":
342
+ return report
343
+ return {
344
+ "status": report.get("status"),
345
+ "rows": report.get("rows"),
346
+ "symbols": report.get("symbols"),
347
+ "trials": report.get("trials"),
348
+ "order_count": report.get("order_count"),
349
+ "parity": report.get("parity"),
350
+ "cython_cpp_recommendation": report.get("cython_cpp_recommendation"),
351
+ "next_optimization_targets": report.get("next_optimization_targets"),
352
+ }
353
+
354
+
355
+ def _cython_cpp_recommendation(large_wfo: Dict) -> str:
356
+ if large_wfo.get("status") != "pass":
357
+ return "defer; benchmark did not pass all parity/status gates"
358
+ text = str(large_wfo.get("cython_cpp_recommendation", "")).lower()
359
+ if "not justified" in text or "not yet" in text:
360
+ return "not justified yet; facade/report overhead remains the larger measured bucket"
361
+ return large_wfo.get("cython_cpp_recommendation", "defer until pure kernel bottleneck is proven")
362
+
363
+
364
+ def main() -> None:
365
+ parser = argparse.ArgumentParser()
366
+ parser.add_argument("--rows", type=int, default=1_440)
367
+ parser.add_argument("--symbols", type=int, default=6)
368
+ parser.add_argument("--replays", type=int, default=8)
369
+ parser.add_argument("--repeats", type=int, default=2)
370
+ parser.add_argument("--skip-large-wfo", action="store_true")
371
+ parser.add_argument("--output-json", default=str(PACKAGE_DIR / "benchmarks" / "phase16_performance_debt.json"))
372
+ parser.add_argument("--output-md", default=str(PACKAGE_DIR / "benchmarks" / "phase16_performance_debt.md"))
373
+ args = parser.parse_args()
374
+ report = run_phase16_benchmark(
375
+ rows=args.rows,
376
+ symbols=args.symbols,
377
+ replays=args.replays,
378
+ repeats=args.repeats,
379
+ include_large_wfo=not args.skip_large_wfo,
380
+ )
381
+ json_path = Path(args.output_json)
382
+ md_path = Path(args.output_md)
383
+ json_path.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")
384
+ md_path.write_text(make_markdown(report), encoding="utf-8")
385
+ print(json.dumps(report, indent=2, default=str))
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()
@@ -0,0 +1,161 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import time
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+
11
+ from quantbt import OrderCommand, OrderSide, OrderType, QuantBTEndpoint, TimeInForce
12
+ from quantbt.core.orders import OrderAction
13
+
14
+
15
+ def _bars(n: int) -> pd.DataFrame:
16
+ idx = pd.date_range("2024-01-01", periods=n, freq="1h", tz="UTC")
17
+ x = np.arange(n, dtype=np.float64)
18
+ close = 100.0 + 0.002 * x + 2.0 * np.sin(x / 27.0) + 0.7 * np.sin(x / 7.0)
19
+ return pd.DataFrame(
20
+ {
21
+ "open": close,
22
+ "high": close + 1.25,
23
+ "low": close - 1.25,
24
+ "close": close,
25
+ "volume": 10_000.0 + 100.0 * np.cos(x / 11.0),
26
+ },
27
+ index=idx,
28
+ )
29
+
30
+
31
+ class ReactiveGridStrategy:
32
+ def __init__(self, *, levels: int, reseed_every: int) -> None:
33
+ self.levels = int(levels)
34
+ self.reseed_every = int(reseed_every)
35
+ self.cycle = 0
36
+
37
+ def on_bar_close(self, context):
38
+ commands = []
39
+ if context.bar_index % self.reseed_every == 0:
40
+ self.cycle += 1
41
+ commands.append(
42
+ OrderCommand(
43
+ timestamp=context.timestamp,
44
+ action=OrderAction.CANCEL_ALL,
45
+ symbol=context.symbols[0],
46
+ tag_prefix="GRID-",
47
+ )
48
+ )
49
+ center = float(context.close[0])
50
+ for level in range(1, self.levels + 1):
51
+ commands.append(
52
+ OrderCommand(
53
+ timestamp=context.timestamp,
54
+ symbol=context.symbols[0],
55
+ side=OrderSide.BUY,
56
+ order_type=OrderType.LIMIT,
57
+ qty=0.01,
58
+ price=center - 0.05 * level,
59
+ tif=TimeInForce.GTC,
60
+ order_id=f"grid-{self.cycle}-{level}",
61
+ tag=f"GRID-C{self.cycle}-L{level}",
62
+ metadata={"campaign_id": "GRID", "cycle_id": str(self.cycle), "level_id": str(level)},
63
+ )
64
+ )
65
+ if context.positions[context.symbols[0]] > 0.0 and context.bar_index % (self.reseed_every + 7) == 0:
66
+ commands.append(
67
+ OrderCommand(
68
+ timestamp=context.timestamp,
69
+ symbol=context.symbols[0],
70
+ side=OrderSide.SELL,
71
+ order_type=OrderType.MARKET,
72
+ qty=abs(float(context.positions[context.symbols[0]])),
73
+ tif=TimeInForce.IOC,
74
+ reduce_only=True,
75
+ order_id=f"flatten-{context.bar_index}",
76
+ )
77
+ )
78
+ return commands
79
+
80
+
81
+ def run(*, bars: int, levels: int, reseed_every: int, out_dir: Path) -> dict:
82
+ data = _bars(bars)
83
+ strategy = ReactiveGridStrategy(levels=levels, reseed_every=reseed_every)
84
+ endpoint = QuantBTEndpoint.native_event_strategy(initial_capital=100_000, leverage=5, use_funding=False)
85
+
86
+ t0 = time.perf_counter()
87
+ reactive = endpoint.simulate(data=data, strategy=strategy, symbols=["BTC"])
88
+ reactive_seconds = time.perf_counter() - t0
89
+
90
+ t1 = time.perf_counter()
91
+ replay = QuantBTEndpoint.native_event_lifecycle(initial_capital=100_000, leverage=5, use_funding=False).simulate(
92
+ data=data,
93
+ order_commands=reactive.metadata["emitted_command_tape"],
94
+ symbols=["BTC"],
95
+ )
96
+ replay_seconds = time.perf_counter() - t1
97
+
98
+ equity_diff = float(np.max(np.abs(reactive.equity.to_numpy() - replay.equity.to_numpy())))
99
+ pos_diff = float(
100
+ np.max(
101
+ np.abs(
102
+ reactive.positions["Position_BTC"].to_numpy()
103
+ - replay.positions["Position_BTC"].to_numpy()
104
+ )
105
+ )
106
+ )
107
+ report = {
108
+ "phase": "30E",
109
+ "bars": int(bars),
110
+ "levels": int(levels),
111
+ "reseed_every": int(reseed_every),
112
+ "emitted_commands": int(reactive.metadata["emitted_command_count"]),
113
+ "fills": int(len(reactive.fills)),
114
+ "reactive_seconds": reactive_seconds,
115
+ "static_replay_seconds": replay_seconds,
116
+ "total_seconds": reactive_seconds + replay_seconds,
117
+ "equity_max_abs_diff": equity_diff,
118
+ "position_max_abs_diff": pos_diff,
119
+ "context_builder": reactive.metadata["reactive_context_builder"],
120
+ "incremental_compile_replays": reactive.metadata["reactive_incremental_compile_replays"],
121
+ "final_equity": float(reactive.equity.iloc[-1]),
122
+ }
123
+ out_dir.mkdir(parents=True, exist_ok=True)
124
+ json_path = out_dir / "phase30e_reactive_runner.json"
125
+ md_path = out_dir / "phase30e_reactive_runner.md"
126
+ json_path.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
127
+ md_path.write_text(
128
+ "\n".join(
129
+ [
130
+ "# Phase 30E Reactive Runner Benchmark",
131
+ "",
132
+ f"- Bars: {bars:,}",
133
+ f"- Grid levels: {levels}",
134
+ f"- Emitted commands: {report['emitted_commands']:,}",
135
+ f"- Fills: {report['fills']:,}",
136
+ f"- Reactive runner seconds: {reactive_seconds:.6f}",
137
+ f"- Static replay seconds: {replay_seconds:.6f}",
138
+ f"- Max equity diff: {equity_diff:.12f}",
139
+ f"- Max position diff: {pos_diff:.12f}",
140
+ f"- Context builder: {report['context_builder']}",
141
+ "",
142
+ "Final accounting is still produced by one static native-event v2 replay.",
143
+ ]
144
+ ),
145
+ encoding="utf-8",
146
+ )
147
+ return report
148
+
149
+
150
+ def main() -> None:
151
+ parser = argparse.ArgumentParser()
152
+ parser.add_argument("--bars", type=int, default=25_000)
153
+ parser.add_argument("--levels", type=int, default=20)
154
+ parser.add_argument("--reseed-every", type=int, default=50)
155
+ parser.add_argument("--out-dir", type=Path, default=Path("benchmarks/out"))
156
+ args = parser.parse_args()
157
+ print(json.dumps(run(bars=args.bars, levels=args.levels, reseed_every=args.reseed_every, out_dir=args.out_dir), indent=2))
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()