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,694 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 14C real WFO and service-loop benchmark.
4
+
5
+ This runner measures the remaining higher-level performance debt without
6
+ changing engine semantics. It is intentionally a benchmark/certification
7
+ artifact, not an optimization pass.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import gc
14
+ import json
15
+ import statistics
16
+ import sys
17
+ import time
18
+ import tracemalloc
19
+ from dataclasses import asdict
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional
22
+
23
+ import numpy as np
24
+ import pandas as pd
25
+
26
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
27
+ PROJECT_DIR = PACKAGE_DIR.parent
28
+ if str(PROJECT_DIR) not in sys.path:
29
+ sys.path.insert(0, str(PROJECT_DIR))
30
+
31
+ from quantbt import ( # noqa: E402
32
+ AccountConfig,
33
+ ArbExecutionPolicy,
34
+ ArbitrageLeg,
35
+ BasisArbitrageSpec,
36
+ ContractType,
37
+ ExecutionConfig,
38
+ HedgePolicy,
39
+ HedgePolicyKind,
40
+ NativeEventBackend,
41
+ NativeEventConfig,
42
+ NativeVectorizedBackend,
43
+ NativeVectorizedConfig,
44
+ OrderIntent,
45
+ OrderSide,
46
+ OrderType,
47
+ PackageExecutionKind,
48
+ QuantBTEndpoint,
49
+ SizingPolicy,
50
+ SizingPolicyKind,
51
+ TimeInForce,
52
+ build_arbitrage_domain_audit,
53
+ compare_native_arbitrage_results,
54
+ )
55
+ from quantbt.benchmarks.profile_phase7 import profile_native_event, profile_native_vectorized # noqa: E402
56
+ from quantbt.benchmarks.run_phase7 import BenchmarkProfile, _make_market_frames, _make_orders # noqa: E402
57
+ from quantbt.benchmarks.run_phase12_benchmark_nautilus_cert import _benchmark_native_portfolio # noqa: E402
58
+
59
+
60
+ def run_benchmark(
61
+ *,
62
+ rows: int = 720,
63
+ symbols: int = 4,
64
+ trials: int = 8,
65
+ folds: int = 1,
66
+ order_count: Optional[int] = None,
67
+ repeats: int = 2,
68
+ ) -> Dict:
69
+ order_count = int(order_count if order_count is not None else max(20, rows // 3))
70
+ stage_profile = BenchmarkProfile(
71
+ name="phase14b",
72
+ bars=int(rows),
73
+ symbols=max(2, int(symbols)),
74
+ order_count=order_count,
75
+ repeats=max(1, int(repeats)),
76
+ )
77
+
78
+ vectorized_profile = profile_native_vectorized(stage_profile)
79
+ event_profile = profile_native_event(stage_profile)
80
+ portfolio_profile = _benchmark_native_portfolio(
81
+ rows=int(rows),
82
+ symbols=max(2, int(symbols)),
83
+ repeats=max(1, int(repeats)),
84
+ )
85
+ single_wfo = _single_symbol_wfo_benchmark(rows=rows, trials=trials, repeats=repeats)
86
+ portfolio_wfo = _portfolio_wfo_benchmark(rows=rows, trials=trials, repeats=repeats)
87
+ event_replay = _native_event_replay_benchmark(rows=rows, symbols=symbols, order_count=order_count, repeats=repeats)
88
+ arbitrage_sweep = _arbitrage_package_sweep(rows=rows, repeats=repeats)
89
+ report_cost = _report_level_benchmark(rows=rows, symbols=symbols, repeats=repeats)
90
+
91
+ parity = {
92
+ "single_symbol_wfo": single_wfo["parity_passed"],
93
+ "portfolio_wfo": portfolio_wfo["parity_passed"],
94
+ "native_event_replay": event_replay["parity_passed"],
95
+ "arbitrage_package_sweep": arbitrage_sweep["parity_passed"],
96
+ "report_heavy_vs_light": report_cost["parity_passed"],
97
+ }
98
+ pure_kernel_share_pct = max(
99
+ _stage_share(vectorized_profile, "pure_numba_kernel"),
100
+ _stage_share(event_profile, "pure_numba_kernel"),
101
+ float(portfolio_profile["stages"]["pure_kernel_share_pct"]),
102
+ )
103
+ status = "pass" if all(parity.values()) and portfolio_profile["status"] == "pass" else "fail"
104
+ return {
105
+ "status": status,
106
+ "rows": int(rows),
107
+ "symbols": max(2, int(symbols)),
108
+ "trials": int(trials),
109
+ "folds": int(folds),
110
+ "order_count": order_count,
111
+ "repeats": int(repeats),
112
+ "decomposition": {
113
+ "native_vectorized": asdict(vectorized_profile),
114
+ "native_event": asdict(event_profile),
115
+ "native_portfolio": portfolio_profile,
116
+ },
117
+ "service_loops": {
118
+ "single_symbol_wfo": single_wfo,
119
+ "portfolio_wfo": portfolio_wfo,
120
+ "native_event_replay": event_replay,
121
+ "arbitrage_package_sweep": arbitrage_sweep,
122
+ "report_heavy_vs_light": report_cost,
123
+ },
124
+ "parity": parity,
125
+ "cython_cpp_recommendation": _cython_cpp_recommendation(pure_kernel_share_pct),
126
+ "next_optimization_targets": _next_targets(vectorized_profile, event_profile, portfolio_profile),
127
+ }
128
+
129
+
130
+ def make_markdown(report: Dict) -> str:
131
+ loops = report["service_loops"]
132
+ lines = [
133
+ "# Phase 14C Prepared Cache And Report-Level Benchmark",
134
+ "",
135
+ f"Status: **{report['status']}**",
136
+ "",
137
+ "## Profile",
138
+ "",
139
+ f"- Rows: `{report['rows']}`",
140
+ f"- Symbols: `{report['symbols']}`",
141
+ f"- Optuna trials: `{report['trials']}`",
142
+ f"- Order count: `{report['order_count']}`",
143
+ f"- Repeats: `{report['repeats']}`",
144
+ "",
145
+ "## Service Loop Timings",
146
+ "",
147
+ "| workload | cold/full seconds | prepared/light seconds | speedup | peak MB | parity | notes |",
148
+ "| --- | ---: | ---: | ---: | ---: | --- | --- |",
149
+ ]
150
+ for key, label in (
151
+ ("single_symbol_wfo", "single-symbol WFO"),
152
+ ("portfolio_wfo", "portfolio WFO"),
153
+ ("native_event_replay", "native-event replay"),
154
+ ("arbitrage_package_sweep", "arbitrage sweep"),
155
+ ("report_heavy_vs_light", "portfolio report levels"),
156
+ ):
157
+ item = loops[key]
158
+ lines.append(
159
+ "| {label} | `{cold:.6f}` | `{prepared:.6f}` | `{speedup:.3f}x` | `{peak:.3f}` | `{parity}` | {notes} |".format(
160
+ label=label,
161
+ cold=float(item.get("full_seconds", item.get("cold_seconds", 0.0))),
162
+ prepared=float(item.get("prepared_seconds", item.get("light_seconds", 0.0))),
163
+ speedup=float(item.get("speedup", 0.0)),
164
+ peak=float(item.get("peak_memory_mb", 0.0)),
165
+ parity=bool(item.get("parity_passed", False)),
166
+ notes=item.get("notes", ""),
167
+ )
168
+ )
169
+
170
+ lines.extend(
171
+ [
172
+ "",
173
+ "## Stage Decomposition",
174
+ "",
175
+ "| backend | stage | seconds | share |",
176
+ "| --- | --- | ---: | ---: |",
177
+ ]
178
+ )
179
+ for backend in ("native_vectorized", "native_event"):
180
+ record = report["decomposition"][backend]
181
+ for stage in record["stages"]:
182
+ lines.append(
183
+ f"| `{backend}` | `{stage['stage']}` | `{stage['seconds']:.6f}` | `{stage['percent_of_profile']:.2f}%` |"
184
+ )
185
+ p = report["decomposition"]["native_portfolio"]["stages"]
186
+ for stage, label in (
187
+ ("array_preparation_seconds", "array_preparation"),
188
+ ("pure_numba_kernel_seconds", "pure_numba_kernel"),
189
+ ("report_construction_estimate_seconds", "report_construction_estimate"),
190
+ ):
191
+ share_key = {
192
+ "array_preparation_seconds": "array_preparation_share_pct",
193
+ "pure_numba_kernel_seconds": "pure_kernel_share_pct",
194
+ "report_construction_estimate_seconds": "report_construction_share_pct",
195
+ }[stage]
196
+ lines.append(f"| `native_portfolio` | `{label}` | `{p[stage]:.6f}` | `{p[share_key]:.2f}%` |")
197
+
198
+ lines.extend(
199
+ [
200
+ "",
201
+ "## Parity Guards",
202
+ "",
203
+ ]
204
+ )
205
+ for name, passed in report["parity"].items():
206
+ lines.append(f"- `{name}`: `{passed}`")
207
+
208
+ lines.extend(
209
+ [
210
+ "",
211
+ "## Next Optimization Targets",
212
+ "",
213
+ ]
214
+ )
215
+ for target in report["next_optimization_targets"]:
216
+ lines.append(f"- {target}")
217
+
218
+ lines.extend(
219
+ [
220
+ "",
221
+ "## Cython/C++ Decision",
222
+ "",
223
+ report["cython_cpp_recommendation"],
224
+ "",
225
+ "This report is a measurement artifact. It must not be used to justify changing accounting, fill policy, margin, or report semantics.",
226
+ ]
227
+ )
228
+ return "\n".join(lines) + "\n"
229
+
230
+
231
+ def _single_symbol_wfo_benchmark(*, rows: int, trials: int, repeats: int) -> Dict:
232
+ _quiet_optuna()
233
+ data = _single_frame(rows)
234
+
235
+ def run_once(use_cache: bool):
236
+ endpoint = QuantBTEndpoint.train_test_split(
237
+ strategy_class=_single_wfo_strategy,
238
+ test_start=data.index[max(20, len(data) // 2)],
239
+ target_mode="signal_notional",
240
+ backend="native_vectorized",
241
+ optimization_mode="mode_5_full_robust",
242
+ optimization_config={
243
+ "scoring_backend": "endpoint",
244
+ "use_prepared_scoring_cache": bool(use_cache),
245
+ "candidate_selection_metric": "full_plateau_robust",
246
+ "top_is_fraction": 0.3,
247
+ "scoring_trading_days": 365,
248
+ "use_numba": True,
249
+ },
250
+ optuna_trials=max(2, int(trials)),
251
+ random_seed=42,
252
+ initial_capital=20_000.0,
253
+ leverage=3.0,
254
+ alloc_per_trade=5_000.0,
255
+ fee_rate=0.0001,
256
+ use_funding=False,
257
+ use_pyramiding=False,
258
+ )
259
+ return endpoint.backtest(data=data, param_ranges={"threshold": (0.2, 1.2, 0.1)})
260
+
261
+ cached = run_once(True)
262
+ uncached = run_once(False)
263
+ cached_seconds = _timeit(lambda: run_once(True), repeats)
264
+ uncached_seconds = _timeit(lambda: run_once(False), repeats)
265
+ peak_memory_mb = _peak_memory_mb(lambda: run_once(True))
266
+ equity_diff = float(abs(cached.equity.iloc[-1] - uncached.equity.iloc[-1]))
267
+ objective_diff = float(abs(cached.metadata["walk_forward"]["best_trial"]["objective"] - uncached.metadata["walk_forward"]["best_trial"]["objective"]))
268
+ return {
269
+ "full_seconds": float(uncached_seconds),
270
+ "prepared_seconds": float(cached_seconds),
271
+ "speedup": float(uncached_seconds / cached_seconds) if cached_seconds > 0.0 else 0.0,
272
+ "parity_passed": bool(equity_diff <= 1e-9 and objective_diff <= 1e-12),
273
+ "peak_memory_mb": peak_memory_mb,
274
+ "final_equity_diff": equity_diff,
275
+ "objective_diff": objective_diff,
276
+ "cache_metadata": cached.metadata["walk_forward"].get("prepared_scoring_cache", {}),
277
+ "best_params": cached.metadata["walk_forward"].get("params", {}),
278
+ "notes": "compares uncached vs prepared single-symbol native-vectorized WFO endpoint scoring",
279
+ }
280
+
281
+
282
+ def _portfolio_wfo_benchmark(*, rows: int, trials: int, repeats: int) -> Dict:
283
+ _quiet_optuna()
284
+ data = _portfolio_data(rows, 2)
285
+
286
+ def run_once(use_cache: bool):
287
+ endpoint = QuantBTEndpoint.train_test_split(
288
+ strategy_class=_portfolio_wfo_strategy,
289
+ test_start=next(iter(data.values())).index[max(20, rows // 2)],
290
+ target_mode="portfolio",
291
+ portfolio_mode="longshort",
292
+ optimization_mode="mode_1_decay",
293
+ optimization_config={
294
+ "scoring_backend": "endpoint",
295
+ "use_prepared_scoring_cache": bool(use_cache),
296
+ "top_is_fraction": 0.3,
297
+ },
298
+ optuna_trials=max(2, int(trials)),
299
+ random_seed=7,
300
+ initial_capital=100_000.0,
301
+ leverage=4.0,
302
+ alloc_per_trade=1_000.0,
303
+ fee=0.0,
304
+ use_funding=False,
305
+ )
306
+ return endpoint.backtest(data=data, param_ranges={"scale": (0.5, 1.5, 0.1)})
307
+
308
+ cached = run_once(True)
309
+ uncached = run_once(False)
310
+ cached_seconds = _timeit(lambda: run_once(True), repeats)
311
+ uncached_seconds = _timeit(lambda: run_once(False), repeats)
312
+ peak_memory_mb = _peak_memory_mb(lambda: run_once(True))
313
+ equity_diff = float(abs(cached.equity.iloc[-1] - uncached.equity.iloc[-1]))
314
+ objective_diff = float(abs(cached.metadata["walk_forward"]["best_trial"]["objective"] - uncached.metadata["walk_forward"]["best_trial"]["objective"]))
315
+ return {
316
+ "full_seconds": float(uncached_seconds),
317
+ "prepared_seconds": float(cached_seconds),
318
+ "speedup": float(uncached_seconds / cached_seconds) if cached_seconds > 0.0 else 0.0,
319
+ "parity_passed": bool(equity_diff <= 1e-9 and objective_diff <= 1e-12),
320
+ "peak_memory_mb": peak_memory_mb,
321
+ "final_equity_diff": equity_diff,
322
+ "objective_diff": objective_diff,
323
+ "cache_metadata": cached.metadata["walk_forward"].get("prepared_scoring_cache", {}),
324
+ "notes": "compares uncached vs prepared portfolio WFO endpoint scoring",
325
+ }
326
+
327
+
328
+ def _native_event_replay_benchmark(*, rows: int, symbols: int, order_count: int, repeats: int) -> Dict:
329
+ idx, frames = _make_market_frames(int(rows), max(2, int(symbols)))
330
+ symbols_list = list(frames.keys())
331
+ orders = _make_orders(idx, int(order_count), len(symbols_list))
332
+ closes = {symbol: frame["close"] for symbol, frame in frames.items()}
333
+ highs = {symbol: frame["high"] for symbol, frame in frames.items()}
334
+ lows = {symbol: frame["low"] for symbol, frame in frames.items()}
335
+ backend = NativeEventBackend(
336
+ NativeEventConfig(
337
+ account=AccountConfig(initial_capital=100_000.0, leverage=5.0),
338
+ execution=ExecutionConfig(slippage_bps=0.0),
339
+ fee_rate=0.0,
340
+ use_funding=False,
341
+ )
342
+ )
343
+ market = backend.prepare_market_arrays(idx, closes=closes, highs=highs, lows=lows, symbols=symbols_list)
344
+ compiled = backend.compile_orders(idx, orders=orders, symbols=symbols_list)
345
+
346
+ def cold():
347
+ return backend.run_orders(idx, orders, closes, highs=highs, lows=lows, symbols=symbols_list)
348
+
349
+ def prepared():
350
+ return backend.run_orders(
351
+ idx,
352
+ orders,
353
+ closes,
354
+ highs=highs,
355
+ lows=lows,
356
+ symbols=symbols_list,
357
+ market_arrays=market,
358
+ compiled_orders=compiled,
359
+ )
360
+
361
+ cold_result = cold()
362
+ prepared_result = prepared()
363
+ cold_seconds = _timeit(cold, repeats)
364
+ prepared_seconds = _timeit(prepared, repeats)
365
+ peak_memory_mb = _peak_memory_mb(prepared)
366
+ equity_diff = float(np.max(np.abs(cold_result.equity.to_numpy() - prepared_result.equity.to_numpy())))
367
+ positions_diff = float(np.max(np.abs(cold_result.positions.to_numpy() - prepared_result.positions.to_numpy())))
368
+ return {
369
+ "cold_seconds": float(cold_seconds),
370
+ "prepared_seconds": float(prepared_seconds),
371
+ "speedup": float(cold_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
372
+ "parity_passed": bool(equity_diff <= 1e-12 and positions_diff <= 1e-12),
373
+ "peak_memory_mb": peak_memory_mb,
374
+ "equity_diff": equity_diff,
375
+ "positions_diff": positions_diff,
376
+ "orders": int(len(orders)),
377
+ "notes": "prepared replay reuses market arrays and compiled order arrays",
378
+ }
379
+
380
+
381
+ def _arbitrage_package_sweep(*, rows: int, repeats: int) -> Dict:
382
+ idx = pd.date_range("2023-01-01", periods=int(rows), freq="1h", tz="UTC")
383
+ x = np.linspace(0.0, 8.0, len(idx))
384
+ perp = pd.Series(100.0 + np.sin(x) * 2.0 + np.arange(len(idx)) * 0.01, index=idx)
385
+ quarterly = pd.Series(perp.to_numpy() + 1.5 + np.cos(x) * 0.5, index=idx)
386
+ closes = {"PERP": perp, "QUARTERLY": quarterly}
387
+ signal = pd.Series(0.0, index=idx)
388
+ signal.iloc[len(idx) // 4 : len(idx) // 2] = 1.0
389
+ signal.iloc[-3:] = 0.0
390
+ spec = BasisArbitrageSpec(
391
+ arb_id="PHASE14B_BASIS",
392
+ legs=(
393
+ ArbitrageLeg("PERP", 1.0, role="perp", contract_type=ContractType.LINEAR, funding_enabled=True),
394
+ ArbitrageLeg("QUARTERLY", -1.0, role="quarterly", contract_type=ContractType.LINEAR),
395
+ ),
396
+ hedge_policy=HedgePolicy(HedgePolicyKind.BASE_QTY_EQUAL, freeze_on_entry=True),
397
+ sizing_policy=SizingPolicy(
398
+ SizingPolicyKind.TARGET_NOTIONAL_TO_BASE_QTY,
399
+ notional=10_000.0,
400
+ reference_symbol="PERP",
401
+ ),
402
+ execution_policy=ArbExecutionPolicy(PackageExecutionKind.ATOMIC_ALL_OR_NONE),
403
+ )
404
+ account = AccountConfig(initial_capital=100_000.0, leverage=5.0)
405
+ event = NativeEventBackend(NativeEventConfig(account=account, fee_rate=0.0001, use_funding=True))
406
+ vector = NativeVectorizedBackend(NativeVectorizedConfig(account=account, fee_rate=0.0001, use_funding=True))
407
+ funding = {"PERP": pd.Series(0.00005, index=idx), "QUARTERLY": 0.0}
408
+
409
+ market = event.prepare_market_arrays(idx, closes=closes, highs=closes, lows=closes, funding_rate=funding, symbols=list(closes))
410
+
411
+ def run_event():
412
+ return event.run_basis_arbitrage(idx, spec, signal, closes, funding_rate=funding)
413
+
414
+ def run_event_prepared():
415
+ return event.run_basis_arbitrage(idx, spec, signal, closes, funding_rate=funding, market_arrays=market)
416
+
417
+ def run_vector():
418
+ return vector.run_basis_arbitrage(idx, spec, signal, closes, funding_rate=funding)
419
+
420
+ event_result = run_event()
421
+ prepared_result = run_event_prepared()
422
+ vector_result = run_vector()
423
+ audit = build_arbitrage_domain_audit(event_result)
424
+ parity = compare_native_arbitrage_results(event_result, vector_result)
425
+ event_seconds = _timeit(run_event, repeats)
426
+ prepared_seconds = _timeit(run_event_prepared, repeats)
427
+ peak_memory_mb = _peak_memory_mb(run_event_prepared)
428
+ prepared_equity_diff = float(np.max(np.abs(event_result.equity.to_numpy() - prepared_result.equity.to_numpy())))
429
+ return {
430
+ "full_seconds": float(event_seconds),
431
+ "prepared_seconds": float(prepared_seconds),
432
+ "speedup": float(event_seconds / prepared_seconds) if prepared_seconds > 0.0 else 0.0,
433
+ "parity_passed": bool(audit["passed"] and parity["passed"] and prepared_equity_diff <= 1e-10),
434
+ "peak_memory_mb": peak_memory_mb,
435
+ "audit_status": audit["status"],
436
+ "parity_status": parity["status"],
437
+ "max_equity_diff": parity["max_abs_equity_diff"],
438
+ "prepared_equity_diff": prepared_equity_diff,
439
+ "max_package_residual": parity["max_abs_package_residual"],
440
+ "notes": "compares native-event arbitrage package cold vs prepared market-array replay; vectorized parity remains audited",
441
+ }
442
+
443
+
444
+ def _report_level_benchmark(*, rows: int, symbols: int, repeats: int) -> Dict:
445
+ def make_endpoint(report_level: str):
446
+ return QuantBTEndpoint.portfolio(
447
+ portfolio_mode="market_neutral",
448
+ backend="native_portfolio",
449
+ initial_capital=100_000.0,
450
+ leverage=4.0,
451
+ alloc_per_trade=1_000.0,
452
+ fee_rate=0.0,
453
+ use_funding=False,
454
+ report_level=report_level,
455
+ )
456
+
457
+ full_endpoint = make_endpoint("full")
458
+ minimal_endpoint = make_endpoint("minimal")
459
+ data = _portfolio_data(rows, max(2, symbols))
460
+ positions = _portfolio_positions(next(iter(data.values())).index, max(2, symbols))
461
+
462
+ def run_full():
463
+ return make_endpoint("full").backtest(data=data, positions=positions)
464
+
465
+ def run_minimal():
466
+ return make_endpoint("minimal").backtest(data=data, positions=positions)
467
+
468
+ full_result = full_endpoint.backtest(data=data, positions=positions)
469
+ minimal_result = minimal_endpoint.backtest(data=data, positions=positions)
470
+ full_seconds = _timeit(run_full, repeats)
471
+ minimal_seconds = _timeit(run_minimal, repeats)
472
+ peak_memory_mb = _peak_memory_mb(run_full)
473
+ equity_diff = float(np.max(np.abs(full_result.equity.to_numpy() - minimal_result.equity.to_numpy())))
474
+ position_diff = float(np.max(np.abs(full_result.positions.to_numpy() - minimal_result.positions.to_numpy())))
475
+ return {
476
+ "full_seconds": float(full_seconds),
477
+ "light_seconds": float(minimal_seconds),
478
+ "speedup": float(full_seconds / minimal_seconds) if minimal_seconds > 0.0 else 0.0,
479
+ "parity_passed": bool(equity_diff <= 1e-10 and position_diff <= 1e-12),
480
+ "peak_memory_mb": peak_memory_mb,
481
+ "equity_diff": equity_diff,
482
+ "position_diff": position_diff,
483
+ "full_reports": sorted(k for k in full_result.metadata if k.endswith("_report")),
484
+ "minimal_reports_omitted": tuple(minimal_result.metadata.get("reports_omitted", ())),
485
+ "notes": "compares native-portfolio report_level='full' vs 'minimal' construction with core accounting parity",
486
+ }
487
+
488
+
489
+ def _legacy_report_heavy_vs_light(*, rows: int, symbols: int, repeats: int) -> Dict:
490
+ endpoint = QuantBTEndpoint.portfolio(
491
+ portfolio_mode="market_neutral",
492
+ backend="native_portfolio",
493
+ initial_capital=100_000.0,
494
+ leverage=4.0,
495
+ alloc_per_trade=1_000.0,
496
+ fee_rate=0.0,
497
+ use_funding=False,
498
+ )
499
+ data = _portfolio_data(rows, max(2, symbols))
500
+ positions = _portfolio_positions(next(iter(data.values())).index, max(2, symbols))
501
+ result = endpoint.backtest(data=data, positions=positions)
502
+
503
+ def light():
504
+ return {
505
+ "final_equity": float(result.equity.iloc[-1]),
506
+ "fees": float(result.fees.sum()),
507
+ "funding": float(result.funding.sum()),
508
+ "rows": int(len(result.equity)),
509
+ }
510
+
511
+ def heavy():
512
+ return result.full_report(scope="full")
513
+
514
+ light_summary = light()
515
+ heavy_report = heavy()
516
+ light_seconds = _timeit(light, repeats)
517
+ heavy_seconds = _timeit(heavy, repeats)
518
+ peak_memory_mb = _peak_memory_mb(heavy)
519
+ return {
520
+ "full_seconds": float(heavy_seconds),
521
+ "light_seconds": float(light_seconds),
522
+ "speedup": float(heavy_seconds / light_seconds) if light_seconds > 0.0 else 0.0,
523
+ "parity_passed": bool(abs(light_summary["final_equity"] - heavy_report["final_equity"]) <= 1e-9),
524
+ "peak_memory_mb": peak_memory_mb,
525
+ "final_equity": light_summary["final_equity"],
526
+ "notes": "legacy measurement of metrics/report export cost",
527
+ }
528
+
529
+
530
+ def _single_wfo_strategy(data, params, train_index, test_index, fold):
531
+ threshold = float(params["threshold"])
532
+ frame = data.loc[: test_index[-1]]
533
+ ret = frame["close"].pct_change().fillna(0.0)
534
+ signal = np.where(ret > threshold / 10_000.0, 1.0, np.where(ret < -threshold / 10_000.0, -1.0, 0.0))
535
+ return pd.Series(signal, index=frame.index).reindex(test_index).fillna(0.0)
536
+
537
+
538
+ def _portfolio_wfo_strategy(data, params, train_index, test_index, fold):
539
+ scale = float(params["scale"])
540
+ return pd.DataFrame({"SYM000": scale, "SYM001": -scale}, index=test_index)
541
+
542
+
543
+ def _single_frame(rows: int) -> pd.DataFrame:
544
+ idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
545
+ x = np.linspace(0.0, 14.0, len(idx))
546
+ close = 100.0 + np.cumsum(np.sin(x) * 0.05 + np.cos(x / 2.0) * 0.03)
547
+ return pd.DataFrame(
548
+ {
549
+ "open": close,
550
+ "high": close * 1.002,
551
+ "low": close * 0.998,
552
+ "close": close,
553
+ "volume": 1_000.0,
554
+ },
555
+ index=idx,
556
+ )
557
+
558
+
559
+ def _portfolio_data(rows: int, symbols: int) -> Dict[str, pd.DataFrame]:
560
+ idx = pd.date_range("2021-01-01", periods=int(rows), freq="1h", tz="UTC")
561
+ out = {}
562
+ for j in range(int(symbols)):
563
+ x = np.linspace(0.0, 10.0 + j, len(idx))
564
+ close = 100.0 + j * 3.0 + np.cumsum(np.sin(x) * 0.03 + 0.01)
565
+ out[f"SYM{j:03d}"] = pd.DataFrame(
566
+ {
567
+ "open": close,
568
+ "high": close * 1.002,
569
+ "low": close * 0.998,
570
+ "close": close,
571
+ "volume": 1_000.0 + j,
572
+ },
573
+ index=idx,
574
+ )
575
+ return out
576
+
577
+
578
+ def _portfolio_positions(idx: pd.DatetimeIndex, symbols: int) -> Dict[str, pd.Series]:
579
+ grid = np.arange(len(idx))
580
+ out = {}
581
+ for j in range(int(symbols)):
582
+ active = np.where(((grid // (18 + j % 5)) + j) % 4 == 0, 1.0, 0.0)
583
+ out[f"SYM{j:03d}"] = pd.Series(active * (1.0 if j % 2 == 0 else -1.0), index=idx)
584
+ return out
585
+
586
+
587
+ def _stage_share(profile, stage_name: str) -> float:
588
+ for stage in profile.stages:
589
+ if stage.stage == stage_name:
590
+ return float(stage.percent_of_profile)
591
+ return 0.0
592
+
593
+
594
+ def _quiet_optuna() -> None:
595
+ try:
596
+ import optuna
597
+
598
+ optuna.logging.set_verbosity(optuna.logging.WARNING)
599
+ except Exception:
600
+ return
601
+
602
+
603
+ def _largest_stage(profile) -> str:
604
+ stage = max(profile.stages, key=lambda item: item.seconds)
605
+ return f"{profile.backend}: `{stage.stage}` ({stage.percent_of_profile:.1f}%)"
606
+
607
+
608
+ def _next_targets(vectorized_profile, event_profile, portfolio_profile: Dict) -> List[str]:
609
+ p = portfolio_profile["stages"]
610
+ return [
611
+ _largest_stage(vectorized_profile),
612
+ _largest_stage(event_profile),
613
+ "native_portfolio: `report_construction_estimate` ({:.1f}%)".format(
614
+ float(p["report_construction_share_pct"])
615
+ ),
616
+ "Next step should be real workload profiling before considering Cython/C++; Phase 14C moved the main cache/report controls into opt-in APIs.",
617
+ ]
618
+
619
+
620
+ def _cython_cpp_recommendation(pure_kernel_share_pct: float) -> str:
621
+ if pure_kernel_share_pct >= 35.0:
622
+ return (
623
+ "Pure Numba kernel share is now large enough to investigate Cython/C++ "
624
+ "after adding parity locks around the target kernel."
625
+ )
626
+ return (
627
+ "Cython/C++ is not justified yet. The measured bottleneck remains in "
628
+ "facade/report/preparation layers. Phase 14C added opt-in cache "
629
+ "threading and report-level controls; larger real service-loop profiles "
630
+ "should come before any Cython/C++ decision."
631
+ )
632
+
633
+
634
+ def _timeit(fn, repeats: int) -> float:
635
+ samples: List[float] = []
636
+ for _ in range(max(1, int(repeats))):
637
+ start = time.perf_counter()
638
+ fn()
639
+ samples.append(time.perf_counter() - start)
640
+ return float(statistics.mean(samples))
641
+
642
+
643
+ def _peak_memory_mb(fn) -> float:
644
+ gc.collect()
645
+ tracemalloc.start()
646
+ try:
647
+ fn()
648
+ _current, peak = tracemalloc.get_traced_memory()
649
+ return float(peak / (1024 * 1024))
650
+ finally:
651
+ tracemalloc.stop()
652
+
653
+
654
+ def _json_default(value):
655
+ if isinstance(value, (np.bool_,)):
656
+ return bool(value)
657
+ if isinstance(value, (np.integer,)):
658
+ return int(value)
659
+ if isinstance(value, (np.floating,)):
660
+ return float(value)
661
+ if isinstance(value, pd.Timestamp):
662
+ return value.isoformat()
663
+ raise TypeError(f"{type(value).__name__} is not JSON serializable")
664
+
665
+
666
+ def main(argv: Optional[List[str]] = None) -> int:
667
+ parser = argparse.ArgumentParser(description=__doc__)
668
+ parser.add_argument("--rows", type=int, default=720)
669
+ parser.add_argument("--symbols", type=int, default=4)
670
+ parser.add_argument("--trials", type=int, default=8)
671
+ parser.add_argument("--folds", type=int, default=1)
672
+ parser.add_argument("--order-count", type=int, default=None)
673
+ parser.add_argument("--repeats", type=int, default=2)
674
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase14_service_loop.json")
675
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase14_service_loop.md")
676
+ args = parser.parse_args(argv)
677
+ report = run_benchmark(
678
+ rows=args.rows,
679
+ symbols=args.symbols,
680
+ trials=args.trials,
681
+ folds=args.folds,
682
+ order_count=args.order_count,
683
+ repeats=args.repeats,
684
+ )
685
+ args.json_out.parent.mkdir(parents=True, exist_ok=True)
686
+ args.md_out.parent.mkdir(parents=True, exist_ok=True)
687
+ args.json_out.write_text(json.dumps(report, indent=2, sort_keys=True, default=_json_default) + "\n", encoding="utf-8")
688
+ args.md_out.write_text(make_markdown(report), encoding="utf-8")
689
+ print(make_markdown(report))
690
+ return 0 if report["status"] == "pass" else 1
691
+
692
+
693
+ if __name__ == "__main__":
694
+ raise SystemExit(main())