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,654 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 7 benchmark runner.
4
+
5
+ This is a lightweight stdlib CLI around the public V2 engines. It intentionally
6
+ does not require pytest or a benchmark plugin so it can run inside notebooks,
7
+ SSH shells, and CI jobs with the same command.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import argparse
13
+ import gc
14
+ import json
15
+ import math
16
+ import resource
17
+ import statistics
18
+ import sys
19
+ import time
20
+ import tracemalloc
21
+ from dataclasses import asdict, dataclass
22
+ from pathlib import Path
23
+ from typing import Dict, Iterable, List, Optional, Tuple
24
+
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
+
32
+ @dataclass(frozen=True)
33
+ class BenchmarkProfile:
34
+ name: str
35
+ bars: int
36
+ symbols: int
37
+ order_count: int
38
+ repeats: int
39
+
40
+
41
+ @dataclass
42
+ class BenchmarkRecord:
43
+ backend: str
44
+ profile: str
45
+ status: str
46
+ bars: int
47
+ symbols: int
48
+ bar_symbols: int
49
+ order_count: int
50
+ event_count: int
51
+ signal_transitions: int
52
+ warmup_seconds: Optional[float]
53
+ runtime_seconds: Optional[float]
54
+ runtime_min_seconds: Optional[float]
55
+ runtime_max_seconds: Optional[float]
56
+ peak_memory_mb: Optional[float]
57
+ rss_delta_mb: Optional[float]
58
+ throughput_bar_symbols_per_second: Optional[float] = None
59
+ throughput_orders_per_second: Optional[float] = None
60
+ threshold_metric: Optional[str] = None
61
+ threshold_value: Optional[float] = None
62
+ threshold_limit: Optional[float] = None
63
+ threshold_passed: Optional[bool] = None
64
+ error: Optional[str] = None
65
+
66
+
67
+ PROFILES = {
68
+ "smoke": BenchmarkProfile(name="smoke", bars=1_000, symbols=4, order_count=500, repeats=2),
69
+ "standard": BenchmarkProfile(name="standard", bars=25_000, symbols=20, order_count=25_000, repeats=5),
70
+ "large": BenchmarkProfile(name="large", bars=100_000, symbols=50, order_count=100_000, repeats=3),
71
+ }
72
+
73
+
74
+ def main(argv: Optional[List[str]] = None) -> int:
75
+ parser = argparse.ArgumentParser(description="Run quantbt Phase 7 benchmarks.")
76
+ parser.add_argument("--profile", choices=sorted(PROFILES), default="smoke")
77
+ parser.add_argument("--repeats", type=int, default=None)
78
+ parser.add_argument("--include-nautilus", action="store_true")
79
+ parser.add_argument("--no-tracemalloc", action="store_true", help="Measure runtime without Python allocation tracing.")
80
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_results.json")
81
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_results.md")
82
+ args = parser.parse_args(argv)
83
+
84
+ profile = PROFILES[args.profile]
85
+ if args.repeats is not None:
86
+ profile = BenchmarkProfile(
87
+ name=profile.name,
88
+ bars=profile.bars,
89
+ symbols=profile.symbols,
90
+ order_count=profile.order_count,
91
+ repeats=max(1, args.repeats),
92
+ )
93
+
94
+ records = run_all(profile=profile, include_nautilus=args.include_nautilus, trace_memory=not args.no_tracemalloc)
95
+ write_outputs(records=records, profile=profile, json_out=args.json_out, md_out=args.md_out)
96
+ for record in records:
97
+ print(_record_line(record))
98
+ return 0 if all(r.status in {"passed", "skipped"} for r in records) else 1
99
+
100
+
101
+ def run_all(profile: BenchmarkProfile, include_nautilus: bool = False, trace_memory: bool = True) -> List[BenchmarkRecord]:
102
+ records = [
103
+ run_native_vectorized(profile, trace_memory=trace_memory),
104
+ run_native_event(profile, trace_memory=trace_memory),
105
+ run_native_event_prepared(profile, trace_memory=trace_memory),
106
+ run_portfolio_legacy(profile, trace_memory=trace_memory),
107
+ run_native_portfolio(profile, trace_memory=trace_memory),
108
+ ]
109
+ if include_nautilus:
110
+ records.append(run_nautilus(profile, trace_memory=trace_memory))
111
+ else:
112
+ records.append(_skipped("nautilus", profile, "pass --include-nautilus to run optional backend"))
113
+ return records
114
+
115
+
116
+ def run_native_vectorized(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
117
+ try:
118
+ import pandas as pd
119
+
120
+ from quantbt import AccountConfig, BacktestEngineV2
121
+
122
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
123
+ signals = _make_signals(idx, profile.symbols)
124
+ transitions = _count_signal_transitions(signals.values())
125
+
126
+ def workload():
127
+ engine = BacktestEngineV2(
128
+ data=frames,
129
+ signals=signals,
130
+ backend="native_vectorized",
131
+ account=AccountConfig(initial_capital=1_000_000.0, leverage=10.0),
132
+ alloc_per_trade=10_000.0,
133
+ hedge_type="signal_notional",
134
+ use_funding=False,
135
+ )
136
+ return engine.result.equity.iloc[-1]
137
+
138
+ return _measure(
139
+ backend="native_vectorized",
140
+ profile=profile,
141
+ workload=workload,
142
+ order_count=transitions,
143
+ event_count=profile.bars * profile.symbols,
144
+ signal_transitions=transitions,
145
+ trace_memory=trace_memory,
146
+ )
147
+ except Exception as exc:
148
+ return _failed("native_vectorized", profile, exc)
149
+
150
+
151
+ def run_native_event(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
152
+ try:
153
+ from quantbt import AccountConfig, BacktestEngineV2
154
+
155
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
156
+ orders = _make_orders(idx, profile.order_count, profile.symbols)
157
+
158
+ def workload():
159
+ engine = BacktestEngineV2(
160
+ data=frames,
161
+ backend="native_event",
162
+ orders=orders,
163
+ account=AccountConfig(initial_capital=1_000_000.0, leverage=10.0),
164
+ use_funding=False,
165
+ )
166
+ return engine.result.equity.iloc[-1]
167
+
168
+ return _measure(
169
+ backend="native_event",
170
+ profile=profile,
171
+ workload=workload,
172
+ order_count=len(orders),
173
+ event_count=profile.bars + len(orders),
174
+ signal_transitions=0,
175
+ trace_memory=trace_memory,
176
+ )
177
+ except Exception as exc:
178
+ return _failed("native_event", profile, exc)
179
+
180
+
181
+ def run_native_event_prepared(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
182
+ try:
183
+ from quantbt import AccountConfig
184
+ from quantbt.backends import NativeEventBackend, NativeEventConfig
185
+
186
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
187
+ orders = _make_orders(idx, profile.order_count, profile.symbols)
188
+ symbols = list(frames.keys())
189
+ closes = {symbol: frame["close"] for symbol, frame in frames.items()}
190
+ highs = {symbol: frame["high"] for symbol, frame in frames.items()}
191
+ lows = {symbol: frame["low"] for symbol, frame in frames.items()}
192
+ backend = NativeEventBackend(
193
+ NativeEventConfig(
194
+ account=AccountConfig(initial_capital=1_000_000.0, leverage=10.0),
195
+ use_funding=False,
196
+ )
197
+ )
198
+ market_arrays = backend.prepare_market_arrays(
199
+ datetime_index=idx,
200
+ closes=closes,
201
+ highs=highs,
202
+ lows=lows,
203
+ symbols=symbols,
204
+ )
205
+ compiled_orders = backend.compile_orders(datetime_index=idx, orders=orders, symbols=symbols)
206
+
207
+ def workload():
208
+ result = backend.run_orders(
209
+ datetime_index=idx,
210
+ orders=orders,
211
+ closes=closes,
212
+ highs=highs,
213
+ lows=lows,
214
+ symbols=symbols,
215
+ market_arrays=market_arrays,
216
+ compiled_orders=compiled_orders,
217
+ )
218
+ return result.equity.iloc[-1]
219
+
220
+ return _measure(
221
+ backend="native_event_prepared",
222
+ profile=profile,
223
+ workload=workload,
224
+ order_count=len(orders),
225
+ event_count=profile.bars + len(orders),
226
+ signal_transitions=0,
227
+ trace_memory=trace_memory,
228
+ )
229
+ except Exception as exc:
230
+ return _failed("native_event_prepared", profile, exc)
231
+
232
+
233
+ def run_portfolio_legacy(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
234
+ try:
235
+ from quantbt import AccountConfig, PortfolioBacktestEngine
236
+
237
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
238
+ positions = _make_portfolio_positions(idx, profile.symbols)
239
+ closes = {symbol: frame["close"] for symbol, frame in frames.items()}
240
+ transitions = _count_signal_transitions(positions.values())
241
+
242
+ def workload():
243
+ engine = PortfolioBacktestEngine(
244
+ positions=positions,
245
+ closes=closes,
246
+ highs=closes,
247
+ lows=closes,
248
+ datetime_index=idx,
249
+ mode="longshort",
250
+ account=AccountConfig(initial_capital=1_000_000.0, leverage=10.0),
251
+ fee_rate=0.0,
252
+ alloc_per_trade=10_000.0,
253
+ use_funding=False,
254
+ )
255
+ return engine.result.equity.iloc[-1]
256
+
257
+ return _measure(
258
+ backend="portfolio_legacy",
259
+ profile=profile,
260
+ workload=workload,
261
+ order_count=transitions,
262
+ event_count=profile.bars * profile.symbols,
263
+ signal_transitions=transitions,
264
+ trace_memory=trace_memory,
265
+ )
266
+ except Exception as exc:
267
+ return _failed("portfolio_legacy", profile, exc)
268
+
269
+
270
+ def run_native_portfolio(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
271
+ try:
272
+ from quantbt import AccountConfig, PortfolioBacktestEngine
273
+
274
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
275
+ positions = _make_portfolio_positions(idx, profile.symbols)
276
+ closes = {symbol: frame["close"] for symbol, frame in frames.items()}
277
+ transitions = _count_signal_transitions(positions.values())
278
+
279
+ def workload():
280
+ engine = PortfolioBacktestEngine(
281
+ positions=positions,
282
+ closes=closes,
283
+ highs=closes,
284
+ lows=closes,
285
+ datetime_index=idx,
286
+ mode="longshort",
287
+ backend="native_portfolio",
288
+ account=AccountConfig(initial_capital=1_000_000.0, leverage=10.0),
289
+ fee_rate=0.0,
290
+ alloc_per_trade=10_000.0,
291
+ hedge_type="signal_notional",
292
+ use_funding=False,
293
+ )
294
+ return engine.result.equity.iloc[-1]
295
+
296
+ return _measure(
297
+ backend="native_portfolio",
298
+ profile=profile,
299
+ workload=workload,
300
+ order_count=transitions,
301
+ event_count=profile.bars * profile.symbols,
302
+ signal_transitions=transitions,
303
+ trace_memory=trace_memory,
304
+ )
305
+ except Exception as exc:
306
+ return _failed("native_portfolio", profile, exc)
307
+
308
+
309
+ def run_nautilus(profile: BenchmarkProfile, trace_memory: bool = True) -> BenchmarkRecord:
310
+ try:
311
+ from quantbt import AccountConfig, BacktestEngineV2
312
+ from quantbt.adapters.nautilus import NautilusBacktestEngine
313
+
314
+ NautilusBacktestEngine.check_available()
315
+ idx, frames = _make_market_frames(min(profile.bars, 10_000), 1)
316
+ symbol, frame = next(iter(frames.items()))
317
+ signal = _make_signals(idx, 1)[symbol]
318
+ transitions = _count_signal_transitions([signal])
319
+
320
+ def workload():
321
+ engine = BacktestEngineV2(
322
+ data=frame,
323
+ signals=signal,
324
+ symbols=["BTCUSDT-PERP.BINANCE"],
325
+ backend="nautilus",
326
+ account=AccountConfig(initial_capital=10_000.0, leverage=10.0),
327
+ alloc_per_trade=1_000.0,
328
+ use_funding=False,
329
+ )
330
+ return engine.result.equity.iloc[-1]
331
+
332
+ nautilus_profile = BenchmarkProfile(
333
+ name=profile.name,
334
+ bars=len(idx),
335
+ symbols=1,
336
+ order_count=transitions,
337
+ repeats=max(1, min(profile.repeats, 2)),
338
+ )
339
+ return _measure(
340
+ backend="nautilus",
341
+ profile=nautilus_profile,
342
+ workload=workload,
343
+ order_count=transitions,
344
+ event_count=len(idx),
345
+ signal_transitions=transitions,
346
+ trace_memory=trace_memory,
347
+ )
348
+ except ImportError as exc:
349
+ return _skipped("nautilus", profile, str(exc))
350
+ except Exception as exc:
351
+ return _failed("nautilus", profile, exc)
352
+
353
+
354
+ def _measure(
355
+ backend: str,
356
+ profile: BenchmarkProfile,
357
+ workload,
358
+ order_count: int,
359
+ event_count: int,
360
+ signal_transitions: int,
361
+ trace_memory: bool = True,
362
+ ) -> BenchmarkRecord:
363
+ gc.collect()
364
+ rss_before = _rss_mb()
365
+ if trace_memory:
366
+ tracemalloc.start()
367
+ warmup_start = time.perf_counter()
368
+ workload()
369
+ warmup_seconds = time.perf_counter() - warmup_start
370
+
371
+ runtimes: List[float] = []
372
+ for _ in range(profile.repeats):
373
+ start = time.perf_counter()
374
+ workload()
375
+ runtimes.append(time.perf_counter() - start)
376
+ peak = 0
377
+ if trace_memory:
378
+ current, peak = tracemalloc.get_traced_memory()
379
+ del current
380
+ tracemalloc.stop()
381
+ rss_after = _rss_mb()
382
+
383
+ runtime = statistics.mean(runtimes)
384
+ record = BenchmarkRecord(
385
+ backend=backend,
386
+ profile=profile.name,
387
+ status="passed",
388
+ bars=profile.bars,
389
+ symbols=profile.symbols,
390
+ bar_symbols=profile.bars * profile.symbols,
391
+ order_count=order_count,
392
+ event_count=event_count,
393
+ signal_transitions=signal_transitions,
394
+ warmup_seconds=warmup_seconds,
395
+ runtime_seconds=runtime,
396
+ runtime_min_seconds=min(runtimes),
397
+ runtime_max_seconds=max(runtimes),
398
+ peak_memory_mb=(peak / (1024 * 1024)) if trace_memory else None,
399
+ rss_delta_mb=max(0.0, rss_after - rss_before),
400
+ throughput_bar_symbols_per_second=(profile.bars * profile.symbols / runtime) if runtime > 0.0 else None,
401
+ throughput_orders_per_second=(order_count / runtime) if runtime > 0.0 and order_count > 0 else None,
402
+ )
403
+ return _attach_threshold(record)
404
+
405
+
406
+ def _make_market_frames(bars: int, symbols: int):
407
+ import numpy as np
408
+ import pandas as pd
409
+
410
+ idx = pd.date_range("2020-01-01", periods=bars, freq="1min", tz="UTC")
411
+ base = 100.0 + np.cumsum(np.sin(np.arange(bars) / 37.0) * 0.05)
412
+ frames = {}
413
+ for j in range(symbols):
414
+ close = base + j * 0.25
415
+ frames[f"SYM{j:03d}"] = pd.DataFrame(
416
+ {
417
+ "open": close,
418
+ "high": close * 1.001,
419
+ "low": close * 0.999,
420
+ "close": close,
421
+ "volume": 1_000.0 + j,
422
+ },
423
+ index=idx,
424
+ )
425
+ return idx, frames
426
+
427
+
428
+ def _make_signals(idx, symbols: int):
429
+ import numpy as np
430
+ import pandas as pd
431
+
432
+ out = {}
433
+ n = len(idx)
434
+ grid = np.arange(n)
435
+ for j in range(symbols):
436
+ raw = np.where(((grid // (25 + j % 5)) + j) % 4 == 0, 1.0, 0.0)
437
+ out[f"SYM{j:03d}"] = pd.Series(raw, index=idx)
438
+ return out
439
+
440
+
441
+ def _make_portfolio_positions(idx, symbols: int):
442
+ import numpy as np
443
+ import pandas as pd
444
+
445
+ out = {}
446
+ n = len(idx)
447
+ grid = np.arange(n)
448
+ for j in range(symbols):
449
+ active = np.where(((grid // (40 + j % 7)) + j) % 5 == 0, 1.0, 0.0)
450
+ sign = 1.0 if j % 2 == 0 else -1.0
451
+ out[f"SYM{j:03d}"] = pd.Series(active * sign, index=idx)
452
+ return out
453
+
454
+
455
+ def _make_orders(idx, order_count: int, symbols: int):
456
+ import numpy as np
457
+
458
+ from quantbt import OrderIntent, OrderSide, OrderType, TimeInForce
459
+
460
+ if order_count <= 0:
461
+ return []
462
+ positions = np.linspace(1, len(idx) - 1, num=order_count, dtype=int)
463
+ orders = []
464
+ for k, bar in enumerate(positions):
465
+ side = OrderSide.BUY if k % 2 == 0 else OrderSide.SELL
466
+ orders.append(
467
+ OrderIntent(
468
+ timestamp=idx[int(bar)],
469
+ symbol=f"SYM{k % symbols:03d}",
470
+ side=side,
471
+ order_type=OrderType.MARKET,
472
+ qty=1.0,
473
+ tif=TimeInForce.IOC,
474
+ )
475
+ )
476
+ return orders
477
+
478
+
479
+ def _count_signal_transitions(signals: Iterable) -> int:
480
+ count = 0
481
+ for sig in signals:
482
+ values = sig.to_numpy()
483
+ if len(values) > 1:
484
+ count += int((values[1:] != values[:-1]).sum())
485
+ return count
486
+
487
+
488
+ def write_outputs(
489
+ records: List[BenchmarkRecord],
490
+ profile: BenchmarkProfile,
491
+ json_out: Path,
492
+ md_out: Path,
493
+ ) -> None:
494
+ json_out.parent.mkdir(parents=True, exist_ok=True)
495
+ md_out.parent.mkdir(parents=True, exist_ok=True)
496
+ payload = {
497
+ "profile": asdict(profile),
498
+ "records": [asdict(record) for record in records],
499
+ "thresholds": _load_thresholds(),
500
+ }
501
+ json_out.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
502
+ md_out.write_text(_markdown_report(records, profile), encoding="utf-8")
503
+
504
+
505
+ def _markdown_report(records: List[BenchmarkRecord], profile: BenchmarkProfile) -> str:
506
+ lines = [
507
+ "# Phase 7 Benchmark Results",
508
+ "",
509
+ f"Profile: `{profile.name}`",
510
+ "",
511
+ "| backend | status | bars | symbols | orders | events | warmup s | runtime s | peak MB | throughput | threshold | note |",
512
+ "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- | --- |",
513
+ ]
514
+ for record in records:
515
+ threshold = "-"
516
+ if record.threshold_metric is not None:
517
+ verdict = "pass" if record.threshold_passed else "fail"
518
+ threshold = f"{record.threshold_metric}={_fmt(record.threshold_value)} <= {_fmt(record.threshold_limit)} ({verdict})"
519
+ lines.append(
520
+ "| {backend} | {status} | {bars} | {symbols} | {orders} | {events} | {warmup} | {runtime} | {peak} | {throughput} | {threshold} | {note} |".format(
521
+ backend=record.backend,
522
+ status=record.status,
523
+ bars=record.bars,
524
+ symbols=record.symbols,
525
+ orders=record.order_count,
526
+ events=record.event_count,
527
+ warmup=_fmt(record.warmup_seconds),
528
+ runtime=_fmt(record.runtime_seconds),
529
+ peak=_fmt(record.peak_memory_mb),
530
+ throughput=_fmt(record.throughput_bar_symbols_per_second),
531
+ threshold=threshold,
532
+ note=record.error or "",
533
+ )
534
+ )
535
+ lines.append("")
536
+ lines.append("Thresholds: see `benchmarks/phase7_thresholds.json`.")
537
+ return "\n".join(lines) + "\n"
538
+
539
+
540
+ def _record_line(record: BenchmarkRecord) -> str:
541
+ return (
542
+ f"{record.backend}: {record.status} "
543
+ f"warmup={_fmt(record.warmup_seconds)}s runtime={_fmt(record.runtime_seconds)}s "
544
+ f"peak={_fmt(record.peak_memory_mb)}MB {record.error or ''}"
545
+ )
546
+
547
+
548
+ def _fmt(value: Optional[float]) -> str:
549
+ if value is None or (isinstance(value, float) and math.isnan(value)):
550
+ return "-"
551
+ return f"{value:.6f}"
552
+
553
+
554
+ def _rss_mb() -> float:
555
+ try:
556
+ value = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
557
+ except Exception:
558
+ return 0.0
559
+ if sys.platform == "darwin":
560
+ return value / (1024 * 1024)
561
+ return value / 1024
562
+
563
+
564
+ def _failed(backend: str, profile: BenchmarkProfile, exc: Exception) -> BenchmarkRecord:
565
+ return BenchmarkRecord(
566
+ backend=backend,
567
+ profile=profile.name,
568
+ status="failed",
569
+ bars=profile.bars,
570
+ symbols=profile.symbols,
571
+ bar_symbols=profile.bars * profile.symbols,
572
+ order_count=0,
573
+ event_count=0,
574
+ signal_transitions=0,
575
+ warmup_seconds=None,
576
+ runtime_seconds=None,
577
+ runtime_min_seconds=None,
578
+ runtime_max_seconds=None,
579
+ peak_memory_mb=None,
580
+ rss_delta_mb=None,
581
+ error=f"{type(exc).__name__}: {exc}",
582
+ )
583
+
584
+
585
+ def _skipped(backend: str, profile: BenchmarkProfile, reason: str) -> BenchmarkRecord:
586
+ return BenchmarkRecord(
587
+ backend=backend,
588
+ profile=profile.name,
589
+ status="skipped",
590
+ bars=profile.bars,
591
+ symbols=profile.symbols,
592
+ bar_symbols=profile.bars * profile.symbols,
593
+ order_count=0,
594
+ event_count=0,
595
+ signal_transitions=0,
596
+ warmup_seconds=None,
597
+ runtime_seconds=None,
598
+ runtime_min_seconds=None,
599
+ runtime_max_seconds=None,
600
+ peak_memory_mb=None,
601
+ rss_delta_mb=None,
602
+ error=reason,
603
+ )
604
+
605
+
606
+ def _load_thresholds() -> Dict:
607
+ path = PACKAGE_DIR / "benchmarks" / "phase7_thresholds.json"
608
+ try:
609
+ return json.loads(path.read_text(encoding="utf-8"))
610
+ except Exception:
611
+ return {}
612
+
613
+
614
+ def _attach_threshold(record: BenchmarkRecord) -> BenchmarkRecord:
615
+ thresholds = _load_thresholds()
616
+ backend_thresholds = thresholds.get(record.backend, {})
617
+ if record.runtime_seconds is None:
618
+ return record
619
+
620
+ metric = None
621
+ value = None
622
+ limit = None
623
+ if record.profile == "smoke" and "smoke_max_runtime_seconds" in backend_thresholds:
624
+ metric = "runtime_seconds"
625
+ value = record.runtime_seconds
626
+ limit = float(backend_thresholds["smoke_max_runtime_seconds"])
627
+ elif record.backend in {"native_vectorized", "portfolio_legacy", "native_portfolio"}:
628
+ key = f"{record.profile}_max_seconds_per_million_bar_symbols"
629
+ if key in backend_thresholds and record.bar_symbols > 0:
630
+ metric = "seconds_per_million_bar_symbols"
631
+ value = record.runtime_seconds / (record.bar_symbols / 1_000_000.0)
632
+ limit = float(backend_thresholds[key])
633
+ elif record.backend in {"native_event", "native_event_prepared"}:
634
+ key = f"{record.profile}_max_seconds_per_100k_orders"
635
+ if key in backend_thresholds and record.order_count > 0:
636
+ metric = "seconds_per_100k_orders"
637
+ value = record.runtime_seconds / (record.order_count / 100_000.0)
638
+ limit = float(backend_thresholds[key])
639
+ elif record.backend == "nautilus":
640
+ key = f"{record.profile}_max_seconds_per_100k_bars"
641
+ if key in backend_thresholds and record.bars > 0:
642
+ metric = "seconds_per_100k_bars"
643
+ value = record.runtime_seconds / (record.bars / 100_000.0)
644
+ limit = float(backend_thresholds[key])
645
+
646
+ record.threshold_metric = metric
647
+ record.threshold_value = value
648
+ record.threshold_limit = limit
649
+ record.threshold_passed = None if value is None or limit is None else bool(value <= limit)
650
+ return record
651
+
652
+
653
+ if __name__ == "__main__":
654
+ raise SystemExit(main())