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,415 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 7 profiling follow-up.
4
+
5
+ This script decomposes the two Phase 7 threshold misses into timing buckets so
6
+ optimization work can target the real layer: pandas normalization, ndarray
7
+ packing, order-array construction, pure Numba kernels, or result/report
8
+ construction.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import math
16
+ import statistics
17
+ import sys
18
+ import time
19
+ from dataclasses import asdict, dataclass
20
+ from pathlib import Path
21
+ from typing import Dict, List, Optional, Sequence, Tuple
22
+
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.benchmarks.run_phase7 import PROFILES, BenchmarkProfile, _make_market_frames, _make_orders, _make_signals
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ProfileStage:
34
+ backend: str
35
+ profile: str
36
+ stage: str
37
+ seconds: float
38
+ percent_of_profile: float
39
+ repeats: int
40
+ notes: str = ""
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class BackendProfile:
45
+ backend: str
46
+ profile: str
47
+ bars: int
48
+ symbols: int
49
+ orders: int
50
+ total_seconds: float
51
+ stages: List[ProfileStage]
52
+
53
+
54
+ def main(argv: Optional[List[str]] = None) -> int:
55
+ parser = argparse.ArgumentParser(description="Profile QuantBT Phase 7 backend layers.")
56
+ parser.add_argument("--profile", choices=sorted(PROFILES), default="smoke")
57
+ parser.add_argument("--repeats", type=int, default=3)
58
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_profile.json")
59
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase7_profile.md")
60
+ args = parser.parse_args(argv)
61
+
62
+ base = PROFILES[args.profile]
63
+ profile = BenchmarkProfile(
64
+ name=base.name,
65
+ bars=base.bars,
66
+ symbols=base.symbols,
67
+ order_count=base.order_count,
68
+ repeats=max(1, int(args.repeats)),
69
+ )
70
+ records = [profile_native_vectorized(profile), profile_native_event(profile)]
71
+ write_outputs(records, args.json_out, args.md_out)
72
+ for record in records:
73
+ print(f"{record.backend}: total={record.total_seconds:.6f}s")
74
+ for stage in record.stages:
75
+ print(f" {stage.stage}: {stage.seconds:.6f}s ({stage.percent_of_profile:.1f}%)")
76
+ return 0
77
+
78
+
79
+ def profile_native_vectorized(profile: BenchmarkProfile) -> BackendProfile:
80
+ import numpy as np
81
+ import pandas as pd
82
+
83
+ from quantbt import AccountConfig, ExecutionConfig
84
+ from quantbt.core.preprocessor import align_series, build_arrays, prepare_funding, validate_datetime
85
+ from quantbt.core.results import BacktestResultV2
86
+ from quantbt.core.vectorized import _engine_units_v2
87
+ from quantbt.sizing.fast import scale_signal_notional_matrix
88
+
89
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
90
+ signals = _make_signals(idx, profile.symbols)
91
+ symbols = list(frames.keys())
92
+ account = AccountConfig(initial_capital=1_000_000.0, leverage=10.0)
93
+ execution = ExecutionConfig()
94
+
95
+ def normalize():
96
+ local_idx = validate_datetime(idx)
97
+ closes = {symbol: frames[symbol]["close"] for symbol in symbols}
98
+ highs = {symbol: frames[symbol]["high"] for symbol in symbols}
99
+ lows = {symbol: frames[symbol]["low"] for symbol in symbols}
100
+ close_dict = align_series(closes, symbols, local_idx)
101
+ high_dict = align_series(highs, symbols, local_idx, fallback=close_dict)
102
+ low_dict = align_series(lows, symbols, local_idx, fallback=close_dict)
103
+ signal_dict = align_series(signals, symbols, local_idx, fill_val=0.0)
104
+ funding_dict = prepare_funding(0.0, symbols, local_idx)
105
+ return local_idx, close_dict, high_dict, low_dict, signal_dict, funding_dict
106
+
107
+ idx_n, close_dict, high_dict, low_dict, signal_dict, funding_dict = normalize()
108
+
109
+ def pack_arrays():
110
+ return build_arrays(
111
+ symbols=symbols,
112
+ idx=idx_n,
113
+ closes_dict=close_dict,
114
+ highs_dict=high_dict,
115
+ lows_dict=low_dict,
116
+ signals_dict=signal_dict,
117
+ funding_dict=funding_dict,
118
+ )
119
+
120
+ closes_m, highs_m, lows_m, signals_m, funding_m, is_funding = pack_arrays()
121
+ allocs = np.full(len(symbols), 10_000.0, dtype=np.float64)
122
+
123
+ def size_targets():
124
+ return scale_signal_notional_matrix(signals_m, closes_m, allocs, use_pyramiding=True)
125
+
126
+ target_m = size_targets()
127
+ leverages = np.full(len(symbols), account.leverage, dtype=np.float64)
128
+ fee_rates = np.zeros(len(symbols), dtype=np.float64)
129
+ contract_sizes = np.ones(len(symbols), dtype=np.float64)
130
+
131
+ def kernel():
132
+ return _engine_units_v2(
133
+ n_bars=len(idx_n),
134
+ n_syms=len(symbols),
135
+ highs=highs_m,
136
+ lows=lows_m,
137
+ closes=closes_m,
138
+ target_units=target_m,
139
+ funding_rates=funding_m,
140
+ is_funding_bar=is_funding,
141
+ init_capital=account.initial_capital,
142
+ leverages=leverages,
143
+ maint_ratio=account.maintenance_ratio,
144
+ fee_rates=fee_rates,
145
+ contract_sizes=contract_sizes,
146
+ slippage=execution.slippage_rate,
147
+ use_funding=False,
148
+ )
149
+
150
+ kernel_out = kernel()
151
+
152
+ def build_result():
153
+ (
154
+ equity_arr,
155
+ pos_arr,
156
+ fee_arr,
157
+ turnover_arr,
158
+ funding_arr,
159
+ init_margin_arr,
160
+ maint_margin_arr,
161
+ rejected_arr,
162
+ reject_code_arr,
163
+ liq_flag,
164
+ liq_idx,
165
+ _liq_reason,
166
+ ) = kernel_out
167
+ equity = pd.Series(equity_arr, index=idx_n, name="equity")
168
+ return BacktestResultV2(
169
+ equity=equity,
170
+ returns=equity.pct_change().fillna(0.0),
171
+ positions=pd.DataFrame({f"Position_{s}": pos_arr[:, j] for j, s in enumerate(symbols)}, index=idx_n),
172
+ closes=pd.DataFrame({f"Close_{s}": closes_m[:, j] for j, s in enumerate(symbols)}, index=idx_n),
173
+ symbols=symbols,
174
+ initial_capital=account.initial_capital,
175
+ leverage=account.leverage,
176
+ liquidated=bool(liq_flag),
177
+ liquidation_bar=int(liq_idx),
178
+ fees=pd.Series(fee_arr, index=idx_n, name="fees"),
179
+ funding=pd.Series(funding_arr, index=idx_n, name="funding"),
180
+ margin=pd.DataFrame({"initial_margin": init_margin_arr, "maintenance_margin": maint_margin_arr}, index=idx_n),
181
+ diagnostics=pd.DataFrame(
182
+ {"turnover": turnover_arr, "rejected_orders": rejected_arr, "reject_code": reject_code_arr},
183
+ index=idx_n,
184
+ ),
185
+ metadata={"backend": "native_vectorized", "engine": "units_v2_profile"},
186
+ )
187
+
188
+ stage_defs = [
189
+ ("data_normalization", normalize, "validate_datetime + align OHLC/signals/funding"),
190
+ ("pandas_to_ndarray", pack_arrays, "build contiguous market/signal arrays"),
191
+ ("target_sizing", size_targets, "compute fast signal_notional target-unit matrix"),
192
+ ("pure_numba_kernel", kernel, "compiled _engine_units_v2 only"),
193
+ ("result_report_construction", build_result, "Series/DataFrame/BacktestResultV2 construction"),
194
+ ]
195
+ return _profile_backend("native_vectorized", profile, profile.order_count, stage_defs)
196
+
197
+
198
+ def profile_native_event(profile: BenchmarkProfile) -> BackendProfile:
199
+ import numpy as np
200
+ import pandas as pd
201
+
202
+ from quantbt import AccountConfig, ExecutionConfig
203
+ from quantbt.core.event import _engine_event_v1
204
+ from quantbt.core.order_compiler import compile_order_intents
205
+ from quantbt.core.preprocessor import align_series, build_market_arrays, prepare_funding, validate_datetime
206
+ from quantbt.core.results import BacktestResultV2
207
+
208
+ idx, frames = _make_market_frames(profile.bars, profile.symbols)
209
+ orders = _make_orders(idx, profile.order_count, profile.symbols)
210
+ symbols = list(frames.keys())
211
+ account = AccountConfig(initial_capital=1_000_000.0, leverage=10.0)
212
+ execution = ExecutionConfig()
213
+
214
+ def normalize():
215
+ local_idx = validate_datetime(idx)
216
+ closes = {symbol: frames[symbol]["close"] for symbol in symbols}
217
+ highs = {symbol: frames[symbol]["high"] for symbol in symbols}
218
+ lows = {symbol: frames[symbol]["low"] for symbol in symbols}
219
+ close_dict = align_series(closes, symbols, local_idx)
220
+ high_dict = align_series(highs, symbols, local_idx, fallback=close_dict)
221
+ low_dict = align_series(lows, symbols, local_idx, fallback=close_dict)
222
+ funding_dict = prepare_funding(0.0, symbols, local_idx)
223
+ return local_idx, close_dict, high_dict, low_dict, funding_dict
224
+
225
+ idx_n, close_dict, high_dict, low_dict, funding_dict = normalize()
226
+
227
+ def pack_arrays():
228
+ return build_market_arrays(
229
+ symbols=symbols,
230
+ idx=idx_n,
231
+ closes_dict=close_dict,
232
+ highs_dict=high_dict,
233
+ lows_dict=low_dict,
234
+ funding_dict=funding_dict,
235
+ )
236
+
237
+ market_arrays = pack_arrays()
238
+ symbol_to_col = {symbol: j for j, symbol in enumerate(symbols)}
239
+
240
+ def build_order_arrays():
241
+ return compile_order_intents(idx=idx_n, orders=orders, symbol_to_col=symbol_to_col)
242
+
243
+ order_arrays = build_order_arrays()
244
+ leverages = np.full(len(symbols), account.leverage, dtype=np.float64)
245
+ fee_rates = np.zeros(len(symbols), dtype=np.float64)
246
+ contract_sizes = np.ones(len(symbols), dtype=np.float64)
247
+
248
+ def kernel():
249
+ return _engine_event_v1(
250
+ n_bars=len(idx_n),
251
+ n_syms=len(symbols),
252
+ n_orders=len(orders),
253
+ order_ptr=order_arrays.order_ptr,
254
+ order_symbol=order_arrays.order_symbol,
255
+ order_side=order_arrays.order_side,
256
+ order_type=order_arrays.order_type,
257
+ order_qty=order_arrays.order_qty,
258
+ order_price=order_arrays.order_price,
259
+ order_tif=order_arrays.order_tif,
260
+ highs=market_arrays.highs,
261
+ lows=market_arrays.lows,
262
+ closes=market_arrays.closes,
263
+ funding_rates=market_arrays.funding,
264
+ is_funding_bar=market_arrays.is_funding_bar,
265
+ init_capital=account.initial_capital,
266
+ leverages=leverages,
267
+ maint_ratio=account.maintenance_ratio,
268
+ fee_rates=fee_rates,
269
+ contract_sizes=contract_sizes,
270
+ slippage=execution.slippage_rate,
271
+ use_funding=False,
272
+ )
273
+
274
+ kernel_out = kernel()
275
+
276
+ def build_result():
277
+ (
278
+ equity_arr,
279
+ pos_arr,
280
+ fee_arr,
281
+ turnover_arr,
282
+ funding_arr,
283
+ init_margin_arr,
284
+ maint_margin_arr,
285
+ rejected_bar,
286
+ canceled_bar,
287
+ order_status,
288
+ reject_code,
289
+ fill_bar,
290
+ fill_qty,
291
+ fill_price,
292
+ fill_fee,
293
+ liq_flag,
294
+ liq_idx,
295
+ _liq_reason,
296
+ ) = kernel_out
297
+ equity = pd.Series(equity_arr, index=idx_n, name="equity")
298
+ order_report = pd.DataFrame(
299
+ {
300
+ "original_index": order_arrays.original_index,
301
+ "status": order_status,
302
+ "reject_code": reject_code,
303
+ "fill_bar": fill_bar,
304
+ "fill_qty": fill_qty,
305
+ "fill_price": fill_price,
306
+ "fill_fee": fill_fee,
307
+ }
308
+ ).sort_values("original_index", kind="stable")
309
+ return BacktestResultV2(
310
+ equity=equity,
311
+ returns=equity.pct_change().fillna(0.0),
312
+ positions=pd.DataFrame({f"Position_{s}": pos_arr[:, j] for j, s in enumerate(symbols)}, index=idx_n),
313
+ closes=pd.DataFrame({f"Close_{s}": market_arrays.closes[:, j] for j, s in enumerate(symbols)}, index=idx_n),
314
+ symbols=symbols,
315
+ initial_capital=account.initial_capital,
316
+ leverage=account.leverage,
317
+ liquidated=bool(liq_flag),
318
+ liquidation_bar=int(liq_idx),
319
+ orders=tuple(orders),
320
+ fees=pd.Series(fee_arr, index=idx_n, name="fees"),
321
+ funding=pd.Series(funding_arr, index=idx_n, name="funding"),
322
+ margin=pd.DataFrame({"initial_margin": init_margin_arr, "maintenance_margin": maint_margin_arr}, index=idx_n),
323
+ diagnostics=pd.DataFrame(
324
+ {"turnover": turnover_arr, "rejected_orders": rejected_bar, "canceled_orders": canceled_bar},
325
+ index=idx_n,
326
+ ),
327
+ metadata={"backend": "native_event", "engine": "event_v1_profile", "order_report": order_report},
328
+ )
329
+
330
+ stage_defs = [
331
+ ("data_normalization", normalize, "validate_datetime + align OHLC/funding"),
332
+ ("pandas_to_ndarray", pack_arrays, "build contiguous market arrays"),
333
+ ("order_array_construction", build_order_arrays, "compile orders with vectorized timestamp mapping"),
334
+ ("pure_numba_kernel", kernel, "compiled _engine_event_v1 only"),
335
+ ("result_report_construction", build_result, "order report + Series/DataFrame/BacktestResultV2"),
336
+ ]
337
+ return _profile_backend("native_event", profile, len(orders), stage_defs)
338
+
339
+
340
+ def _profile_backend(
341
+ backend: str,
342
+ profile: BenchmarkProfile,
343
+ orders: int,
344
+ stage_defs: Sequence[Tuple[str, object, str]],
345
+ ) -> BackendProfile:
346
+ raw: List[Tuple[str, float, str]] = []
347
+ for name, fn, notes in stage_defs:
348
+ fn()
349
+ timings = []
350
+ for _ in range(profile.repeats):
351
+ start = time.perf_counter()
352
+ fn()
353
+ timings.append(time.perf_counter() - start)
354
+ raw.append((name, statistics.mean(timings), notes))
355
+ total = sum(seconds for _, seconds, _ in raw)
356
+ stages = [
357
+ ProfileStage(
358
+ backend=backend,
359
+ profile=profile.name,
360
+ stage=name,
361
+ seconds=seconds,
362
+ percent_of_profile=(seconds / total * 100.0) if total > 0.0 else 0.0,
363
+ repeats=profile.repeats,
364
+ notes=notes,
365
+ )
366
+ for name, seconds, notes in raw
367
+ ]
368
+ return BackendProfile(
369
+ backend=backend,
370
+ profile=profile.name,
371
+ bars=profile.bars,
372
+ symbols=profile.symbols,
373
+ orders=orders,
374
+ total_seconds=total,
375
+ stages=stages,
376
+ )
377
+
378
+
379
+ def write_outputs(records: Sequence[BackendProfile], json_out: Path, md_out: Path) -> None:
380
+ json_out.parent.mkdir(parents=True, exist_ok=True)
381
+ md_out.parent.mkdir(parents=True, exist_ok=True)
382
+ payload = {"records": [asdict(record) for record in records]}
383
+ json_out.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
384
+ md_out.write_text(markdown_report(records), encoding="utf-8")
385
+
386
+
387
+ def markdown_report(records: Sequence[BackendProfile]) -> str:
388
+ lines = [
389
+ "# Phase 7 Profiling Results",
390
+ "",
391
+ "| backend | stage | seconds | share | notes |",
392
+ "| --- | --- | ---: | ---: | --- |",
393
+ ]
394
+ for record in records:
395
+ for stage in record.stages:
396
+ lines.append(
397
+ f"| `{stage.backend}` | `{stage.stage}` | {_fmt(stage.seconds)} | {stage.percent_of_profile:.1f}% | {stage.notes} |"
398
+ )
399
+ lines.extend(
400
+ [
401
+ "",
402
+ "Interpretation rule: optimize the largest measured bucket first. Cython/C++ is only justified after pure Numba kernel profiling remains the bottleneck.",
403
+ ]
404
+ )
405
+ return "\n".join(lines) + "\n"
406
+
407
+
408
+ def _fmt(value: Optional[float]) -> str:
409
+ if value is None or (isinstance(value, float) and math.isnan(value)):
410
+ return "-"
411
+ return f"{value:.6f}"
412
+
413
+
414
+ if __name__ == "__main__":
415
+ raise SystemExit(main())
@@ -0,0 +1,160 @@
1
+ """
2
+ Phase E arbitrage benchmark smoke runner.
3
+
4
+ Run from repository root:
5
+
6
+ PYTHONPATH=/root/bobby/pool_alpha python3 quantbt/benchmarks/run_arbitrage_phase_e.py
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from dataclasses import dataclass
12
+ from time import perf_counter
13
+
14
+ import pandas as pd
15
+
16
+ from quantbt import (
17
+ ArbExecutionPolicy,
18
+ ArbitrageLeg,
19
+ BasisArbitrageSpec,
20
+ ContractType,
21
+ HedgePolicy,
22
+ HedgePolicyKind,
23
+ NativeEventBackend,
24
+ NativeEventConfig,
25
+ NativeVectorizedBackend,
26
+ NativeVectorizedConfig,
27
+ PackageExecutionKind,
28
+ SizingPolicy,
29
+ SizingPolicyKind,
30
+ StatArbPairSpec,
31
+ )
32
+ from quantbt.core.schema import AccountConfig
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class ArbBenchmarkProfile:
37
+ name: str
38
+ bars: int
39
+
40
+
41
+ PROFILES = {
42
+ "smoke": ArbBenchmarkProfile(name="smoke", bars=512),
43
+ "standard": ArbBenchmarkProfile(name="standard", bars=10_000),
44
+ }
45
+
46
+
47
+ def run(profile: ArbBenchmarkProfile = PROFILES["smoke"]) -> list[dict]:
48
+ idx = pd.date_range("2024-01-01", periods=profile.bars, freq="1h", tz="UTC")
49
+ records = []
50
+ for name, runner in (
51
+ ("basis_event", _run_basis_event),
52
+ ("basis_vectorized", _run_basis_vectorized),
53
+ ("stat_event", _run_stat_event),
54
+ ("stat_vectorized", _run_stat_vectorized),
55
+ ):
56
+ started = perf_counter()
57
+ result = runner(idx)
58
+ elapsed = perf_counter() - started
59
+ records.append(
60
+ {
61
+ "name": name,
62
+ "bars": profile.bars,
63
+ "seconds": elapsed,
64
+ "final_equity": float(result.equity.iloc[-1]),
65
+ "engine": result.metadata["engine"],
66
+ }
67
+ )
68
+ return records
69
+
70
+
71
+ def _basis_spec():
72
+ return BasisArbitrageSpec(
73
+ arb_id="BENCH_BASIS",
74
+ legs=(
75
+ ArbitrageLeg(
76
+ symbol="PERP",
77
+ ratio=-1.0,
78
+ role="perp",
79
+ contract_type=ContractType.LINEAR,
80
+ qty_step=0.001,
81
+ min_qty=0.001,
82
+ ),
83
+ ArbitrageLeg(
84
+ symbol="QUARTERLY",
85
+ ratio=1.0,
86
+ role="quarterly",
87
+ contract_type=ContractType.LINEAR,
88
+ qty_step=0.001,
89
+ min_qty=0.001,
90
+ ),
91
+ ),
92
+ hedge_policy=HedgePolicy(kind=HedgePolicyKind.BASE_QTY_EQUAL),
93
+ sizing_policy=SizingPolicy(
94
+ kind=SizingPolicyKind.TARGET_NOTIONAL_TO_BASE_QTY,
95
+ notional=10_000.0,
96
+ reference_symbol="PERP",
97
+ ),
98
+ execution_policy=ArbExecutionPolicy(kind=PackageExecutionKind.ATOMIC_ALL_OR_NONE),
99
+ )
100
+
101
+
102
+ def _stat_spec():
103
+ return StatArbPairSpec(
104
+ arb_id="BENCH_STAT",
105
+ legs=(ArbitrageLeg(symbol="BASE", ratio=1.0), ArbitrageLeg(symbol="HEDGE", ratio=-0.5)),
106
+ hedge_policy=HedgePolicy(kind=HedgePolicyKind.BETA_NEUTRAL),
107
+ sizing_policy=SizingPolicy(kind=SizingPolicyKind.TARGET_GROSS_NOTIONAL, notional=10_000.0),
108
+ )
109
+
110
+
111
+ def _basis_data(idx):
112
+ steps = pd.Series(range(len(idx)), index=idx, dtype=float)
113
+ base = 100.0 + (steps % 31) * 0.1
114
+ return {"PERP": base, "QUARTERLY": base + 2.0}
115
+
116
+
117
+ def _stat_data(idx):
118
+ steps = pd.Series(range(len(idx)), index=idx, dtype=float)
119
+ base = 50.0 + (steps % 17) * 0.2
120
+ return {"BASE": base, "HEDGE": base * 2.0 + 1.0}
121
+
122
+
123
+ def _signal(idx):
124
+ signal = pd.Series(0.0, index=idx)
125
+ signal.iloc[1::200] = 1.0
126
+ signal.iloc[100::200] = 0.0
127
+ return signal.ffill()
128
+
129
+
130
+ def _event_backend():
131
+ return NativeEventBackend(
132
+ NativeEventConfig(account=AccountConfig(initial_capital=100_000.0, leverage=10.0), use_funding=False)
133
+ )
134
+
135
+
136
+ def _vectorized_backend():
137
+ return NativeVectorizedBackend(
138
+ NativeVectorizedConfig(account=AccountConfig(initial_capital=100_000.0, leverage=10.0), use_funding=False)
139
+ )
140
+
141
+
142
+ def _run_basis_event(idx):
143
+ return _event_backend().run_basis_arbitrage(idx, _basis_spec(), _signal(idx), _basis_data(idx))
144
+
145
+
146
+ def _run_basis_vectorized(idx):
147
+ return _vectorized_backend().run_basis_arbitrage(idx, _basis_spec(), _signal(idx), _basis_data(idx))
148
+
149
+
150
+ def _run_stat_event(idx):
151
+ return _event_backend().run_stat_arb_pair_arbitrage(idx, _stat_spec(), _signal(idx), _stat_data(idx))
152
+
153
+
154
+ def _run_stat_vectorized(idx):
155
+ return _vectorized_backend().run_stat_arb_pair_arbitrage(idx, _stat_spec(), _signal(idx), _stat_data(idx))
156
+
157
+
158
+ if __name__ == "__main__":
159
+ for record in run(PROFILES["standard"]):
160
+ print(record)