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,430 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 12B benchmark follow-up and Nautilus portfolio certification runner.
4
+
5
+ The runner keeps production claims narrow and auditable:
6
+
7
+ * benchmark stages separate full facade time from array preparation, pure
8
+ Numba portfolio kernel time, and report-construction residual time;
9
+ * Nautilus portfolio validation is optional because it depends on the external
10
+ NautilusTrader package and venue adapter state;
11
+ * all-or-none basket package semantics are certified through the deterministic
12
+ QuantBT depth preflight used before Nautilus package replay.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import statistics
20
+ import sys
21
+ import time
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional
24
+
25
+ import numpy as np
26
+ import pandas as pd
27
+
28
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
29
+ PROJECT_DIR = PACKAGE_DIR.parent
30
+ if str(PROJECT_DIR) not in sys.path:
31
+ sys.path.insert(0, str(PROJECT_DIR))
32
+
33
+ from quantbt import ( # noqa: E402
34
+ AccountConfig,
35
+ NautilusExecutionDepthConfig,
36
+ OrderIntent,
37
+ OrderSide,
38
+ OrderType,
39
+ PortfolioBacktestEngine,
40
+ QuantBTEndpoint,
41
+ TimeInForce,
42
+ simulate_nautilus_order_package_depth,
43
+ )
44
+ from quantbt.backends import NativePortfolioBackend, NativePortfolioConfig # noqa: E402
45
+ from quantbt.core.engine import _engine_portfolio # noqa: E402
46
+ from quantbt.core.preprocessor import align_series, build_market_arrays, build_signal_matrix, prepare_funding, validate_datetime # noqa: E402
47
+ from quantbt.sizing.fast import scale_signal_notional_matrix # noqa: E402
48
+
49
+
50
+ def run_certification(
51
+ *,
52
+ rows: int = 2_000,
53
+ symbols: int = 6,
54
+ repeats: int = 3,
55
+ include_nautilus: bool = False,
56
+ ) -> Dict:
57
+ benchmark = _benchmark_native_portfolio(rows=rows, symbols=symbols, repeats=repeats)
58
+ all_or_none = _all_or_none_basket_depth_smoke()
59
+ nautilus = _optional_real_nautilus_portfolio(include_nautilus=include_nautilus)
60
+ passed = (
61
+ benchmark["status"] == "pass"
62
+ and all_or_none["status"] == "pass"
63
+ and nautilus["status"] in {"pass", "skipped", "diff"}
64
+ )
65
+ return {
66
+ "status": "pass" if passed else "fail",
67
+ "benchmark_followup": benchmark,
68
+ "all_or_none_basket": all_or_none,
69
+ "nautilus_portfolio": nautilus,
70
+ "cython_cpp_recommendation": _cython_cpp_recommendation(benchmark),
71
+ }
72
+
73
+
74
+ def make_markdown(report: Dict) -> str:
75
+ bench = report["benchmark_followup"]
76
+ lines = [
77
+ "# Phase 12B Benchmark And Nautilus Portfolio Certification",
78
+ "",
79
+ f"Status: **{report['status']}**",
80
+ "",
81
+ "## Benchmark Follow-Up",
82
+ "",
83
+ f"- Bars: `{bench['rows']}`",
84
+ f"- Symbols: `{bench['symbols']}`",
85
+ f"- Repeats: `{bench['repeats']}`",
86
+ f"- Full facade seconds: `{bench['stages']['full_facade_seconds']:.6f}`",
87
+ f"- Prepared reuse facade seconds: `{bench['stages']['prepared_reuse_facade_seconds']:.6f}`",
88
+ f"- Prepared reuse speedup: `{bench['stages']['prepared_reuse_speedup']:.3f}x`",
89
+ f"- Array preparation seconds: `{bench['stages']['array_preparation_seconds']:.6f}`",
90
+ f"- Pure Numba kernel seconds: `{bench['stages']['pure_numba_kernel_seconds']:.6f}`",
91
+ f"- Report construction residual seconds: `{bench['stages']['report_construction_estimate_seconds']:.6f}`",
92
+ f"- Pure kernel share: `{bench['stages']['pure_kernel_share_pct']:.2f}%`",
93
+ "",
94
+ "## Nautilus Portfolio",
95
+ "",
96
+ f"- Status: `{report['nautilus_portfolio']['status']}`",
97
+ f"- Validation status: `{report['nautilus_portfolio'].get('validation_status')}`",
98
+ f"- Equity tolerance profile: `{report['nautilus_portfolio'].get('equity_tolerance')}`",
99
+ f"- Position tolerance profile: `{report['nautilus_portfolio'].get('position_tolerance')}`",
100
+ f"- Final equity diff: `{report['nautilus_portfolio'].get('final_equity_diff')}`",
101
+ f"- Max position diff: `{report['nautilus_portfolio'].get('max_abs_position_diff')}`",
102
+ "",
103
+ "## All-Or-None Basket",
104
+ "",
105
+ f"- Status: `{report['all_or_none_basket']['status']}`",
106
+ f"- Input orders: `{report['all_or_none_basket']['input_orders']}`",
107
+ f"- Accepted orders: `{report['all_or_none_basket']['accepted_orders']}`",
108
+ f"- Rejected orders: `{report['all_or_none_basket']['rejected_orders']}`",
109
+ f"- Depth model: `{report['all_or_none_basket']['depth_model']}`",
110
+ "",
111
+ "## Cython/C++ Decision",
112
+ "",
113
+ report["cython_cpp_recommendation"],
114
+ ]
115
+ return "\n".join(lines) + "\n"
116
+
117
+
118
+ def _benchmark_native_portfolio(rows: int, symbols: int, repeats: int) -> Dict:
119
+ idx, positions, closes, highs, lows = _make_portfolio_fixture(rows, symbols)
120
+ account = AccountConfig(initial_capital=250_000.0, leverage=5.0, maintenance_ratio=0.005)
121
+ alloc = 10_000.0
122
+ fee_rate = 0.0002
123
+ fee_oneway = fee_rate / 2.0
124
+
125
+ def full_facade():
126
+ return PortfolioBacktestEngine(
127
+ positions=positions,
128
+ closes=closes,
129
+ highs=highs,
130
+ lows=lows,
131
+ datetime_index=idx,
132
+ mode="longshort",
133
+ backend="native_portfolio",
134
+ account=account,
135
+ fee_rate=fee_rate,
136
+ alloc_per_trade=alloc,
137
+ hedge_type="signal_notional",
138
+ use_funding=False,
139
+ ).result
140
+
141
+ backend = NativePortfolioBackend(NativePortfolioConfig(account=account, fee_rate=fee_oneway, use_funding=False))
142
+ symbol_list = list(positions.keys())
143
+ prepared_market = backend.prepare_market_arrays(
144
+ datetime_index=idx,
145
+ closes=closes,
146
+ highs=highs,
147
+ lows=lows,
148
+ funding_rate=0.0,
149
+ symbols=symbol_list,
150
+ )
151
+ prepared_signals = backend.prepare_signal_matrix(positions, idx, symbol_list)
152
+
153
+ def prepared_reuse():
154
+ return backend.run_signals(
155
+ positions=None,
156
+ closes=closes,
157
+ highs=highs,
158
+ lows=lows,
159
+ datetime_index=idx,
160
+ mode="longshort",
161
+ alloc_per_trade=alloc,
162
+ contract_size=1.0,
163
+ hedge_type="signal_notional",
164
+ funding_rate=0.0,
165
+ leverage=account.leverage,
166
+ maintenance_ratio=account.maintenance_ratio,
167
+ symbols=symbol_list,
168
+ use_pyramiding=True,
169
+ market_arrays=prepared_market,
170
+ raw_signal_matrix=prepared_signals,
171
+ )
172
+
173
+ prepared = _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_oneway)
174
+ _kernel_portfolio(prepared)
175
+ full_facade()
176
+ prepared_reuse()
177
+
178
+ prep_seconds = _timeit(lambda: _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_oneway), repeats)
179
+ kernel_seconds = _timeit(lambda: _kernel_portfolio(prepared), repeats)
180
+ full_seconds = _timeit(full_facade, repeats)
181
+ prepared_reuse_seconds = _timeit(prepared_reuse, repeats)
182
+ report_seconds = max(0.0, full_seconds - prep_seconds - kernel_seconds)
183
+ status = "pass" if full_seconds > 0.0 and kernel_seconds > 0.0 else "fail"
184
+ return {
185
+ "status": status,
186
+ "rows": int(rows),
187
+ "symbols": int(symbols),
188
+ "bar_symbols": int(rows * symbols),
189
+ "repeats": int(repeats),
190
+ "stages": {
191
+ "full_facade_seconds": float(full_seconds),
192
+ "prepared_reuse_facade_seconds": float(prepared_reuse_seconds),
193
+ "array_preparation_seconds": float(prep_seconds),
194
+ "pure_numba_kernel_seconds": float(kernel_seconds),
195
+ "report_construction_estimate_seconds": float(report_seconds),
196
+ "prepared_reuse_speedup": float(full_seconds / prepared_reuse_seconds) if prepared_reuse_seconds > 0.0 else 0.0,
197
+ "array_preparation_share_pct": float(prep_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
198
+ "pure_kernel_share_pct": float(kernel_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
199
+ "report_construction_share_pct": float(report_seconds / full_seconds * 100.0) if full_seconds > 0.0 else 0.0,
200
+ },
201
+ "notes": (
202
+ "Prepared-array cache targets WFO/service loops. Pure Numba kernel "
203
+ "remains separated from pandas normalization and report construction."
204
+ ),
205
+ }
206
+
207
+
208
+ def _optional_real_nautilus_portfolio(include_nautilus: bool) -> Dict:
209
+ if not include_nautilus:
210
+ return {"status": "skipped", "reason": "run with --include-nautilus"}
211
+ try:
212
+ from quantbt.adapters.nautilus import NautilusBackendConfig, NautilusBacktestEngine
213
+
214
+ NautilusBacktestEngine.check_available()
215
+ idx, raw_positions, raw_closes, raw_highs, raw_lows = _make_portfolio_fixture(rows=96, symbols=2)
216
+ symbols = ["BTCUSDT-PERP.BINANCE", "ETHUSDT-PERP.BINANCE"]
217
+ raw_symbols = list(raw_positions.keys())
218
+ positions = {symbols[i]: raw_positions[raw_symbols[i]] for i in range(2)}
219
+ closes = {symbols[i]: raw_closes[raw_symbols[i]] for i in range(2)}
220
+ highs = {symbols[i]: raw_highs[raw_symbols[i]] for i in range(2)}
221
+ lows = {symbols[i]: raw_lows[raw_symbols[i]] for i in range(2)}
222
+ data = {
223
+ symbol: pd.DataFrame(
224
+ {
225
+ "open": closes[symbol],
226
+ "high": highs[symbol],
227
+ "low": lows[symbol],
228
+ "close": closes[symbol],
229
+ "volume": 1_000.0,
230
+ },
231
+ index=idx,
232
+ )
233
+ for symbol in symbols
234
+ }
235
+ endpoint = QuantBTEndpoint.portfolio(
236
+ portfolio_mode="market_neutral",
237
+ backend="nautilus",
238
+ initial_capital=100_000.0,
239
+ leverage=3.0,
240
+ fee_rate=0.0002,
241
+ use_funding=False,
242
+ hedge_type="signal_notional",
243
+ alloc_per_trade={symbols[0]: 1_000_000.0, symbols[1]: 750_000.0},
244
+ metadata={
245
+ "portfolio_nautilus_equity_tolerance": 1.0,
246
+ "portfolio_nautilus_position_tolerance": 0.005,
247
+ },
248
+ nautilus_config=NautilusBackendConfig(instrument_id=symbols[0], timeframe="1h", bypass_risk=True),
249
+ )
250
+ result = endpoint.simulate(data=data, positions=pd.DataFrame(positions), symbols=symbols)
251
+ validation = result.metadata.get("portfolio_nautilus_validation_report", {})
252
+ return {
253
+ "status": "pass" if validation.get("status") == "pass" else "diff",
254
+ "validation_status": validation.get("status"),
255
+ "checks": validation.get("checks", {}),
256
+ "equity_tolerance": validation.get("equity_tolerance"),
257
+ "position_tolerance": validation.get("position_tolerance"),
258
+ "expected_order_count": validation.get("expected_order_count"),
259
+ "nautilus_orders": validation.get("nautilus_orders"),
260
+ "nautilus_fills": validation.get("nautilus_fills"),
261
+ "final_equity_diff": validation.get("final_equity_diff"),
262
+ "max_abs_position_diff": validation.get("max_abs_position_diff"),
263
+ }
264
+ except Exception as exc:
265
+ return {"status": "skipped", "reason": f"{type(exc).__name__}: {exc}"}
266
+
267
+
268
+ def _all_or_none_basket_depth_smoke() -> Dict:
269
+ idx = pd.date_range("2024-01-01", periods=4, freq="1h", tz="UTC")
270
+ data = {
271
+ "BTC": _depth_frame(idx, close=100.0, high=101.0, low=95.0),
272
+ "ETH": _depth_frame(idx, close=50.0, high=51.0, low=49.0),
273
+ }
274
+ meta = {"package_id": "PHASE12-BASKET", "package_type": "basket_package"}
275
+ orders = (
276
+ OrderIntent(idx[1], "BTC", OrderSide.BUY, OrderType.LIMIT, qty=1.0, price=96.0, tif=TimeInForce.GTC, metadata=meta),
277
+ OrderIntent(idx[1], "ETH", OrderSide.BUY, OrderType.LIMIT, qty=1.0, price=45.0, tif=TimeInForce.GTC, metadata=meta),
278
+ )
279
+ result = simulate_nautilus_order_package_depth(
280
+ orders,
281
+ data,
282
+ NautilusExecutionDepthConfig(all_or_none_packages=True),
283
+ )
284
+ package_status = result.package_report["status"].tolist() if not result.package_report.empty else []
285
+ passed = len(result.orders) == 0 and result.metadata.get("rejected_orders") == 2 and package_status == ["rejected"]
286
+ return {
287
+ "status": "pass" if passed else "fail",
288
+ "input_orders": int(result.metadata.get("input_orders", len(orders))),
289
+ "accepted_orders": int(result.metadata.get("accepted_orders", len(result.orders))),
290
+ "rejected_orders": int(result.metadata.get("rejected_orders", 0)),
291
+ "package_status": package_status,
292
+ "depth_model": result.metadata.get("depth_model"),
293
+ }
294
+
295
+
296
+ def _make_portfolio_fixture(rows: int, symbols: int, symbol_prefix: str = "SYM"):
297
+ idx = pd.date_range("2022-01-01", periods=rows, freq="1h", tz="UTC")
298
+ grid = np.arange(rows)
299
+ base = 100.0 + np.cumsum(np.sin(grid / 19.0) * 0.08 + np.cos(grid / 37.0) * 0.02)
300
+ positions = {}
301
+ closes = {}
302
+ highs = {}
303
+ lows = {}
304
+ for j in range(symbols):
305
+ symbol = f"{symbol_prefix}{j:03d}" if symbol_prefix.endswith("SYM") else f"{symbol_prefix}{j}"
306
+ close = pd.Series(base * (1.0 + j * 0.015) + j * 3.0, index=idx)
307
+ raw = np.where(((grid // (18 + j % 4)) + j) % 4 == 0, 1.0, 0.0)
308
+ sign = 1.0 if j % 2 == 0 else -1.0
309
+ positions[symbol] = pd.Series(raw * sign, index=idx)
310
+ closes[symbol] = close
311
+ highs[symbol] = close * 1.002
312
+ lows[symbol] = close * 0.998
313
+ return idx, positions, closes, highs, lows
314
+
315
+
316
+ def _prepare_portfolio_arrays(idx, positions, closes, highs, lows, account, alloc, fee_rate):
317
+ idx = validate_datetime(idx)
318
+ symbols = list(positions.keys())
319
+ close_dict = align_series(closes, symbols, idx)
320
+ high_dict = align_series(highs, symbols, idx, fallback=close_dict)
321
+ low_dict = align_series(lows, symbols, idx, fallback=close_dict)
322
+ pos_dict = align_series(positions, symbols, idx, fill_val=0.0)
323
+ funding_dict = prepare_funding(0.0, symbols, idx)
324
+ market = build_market_arrays(symbols, idx, close_dict, high_dict, low_dict, funding_dict)
325
+ raw_signals = build_signal_matrix(symbols, idx, pos_dict)
326
+ alloc_arr = np.full(len(symbols), float(alloc), dtype=np.float64)
327
+ contract_sizes = np.ones(len(symbols), dtype=np.float64)
328
+ leverages = np.full(len(symbols), float(account.leverage), dtype=np.float64)
329
+ target_units = scale_signal_notional_matrix(raw_signals, market.closes, alloc_arr, use_pyramiding=True)
330
+ return {
331
+ "n_bars": len(idx),
332
+ "n_syms": len(symbols),
333
+ "highs": market.highs,
334
+ "lows": market.lows,
335
+ "closes": market.closes,
336
+ "target_units": target_units,
337
+ "funding": market.funding,
338
+ "is_funding_bar": market.is_funding_bar,
339
+ "initial_capital": float(account.initial_capital),
340
+ "leverages": leverages,
341
+ "maintenance_ratio": float(account.maintenance_ratio),
342
+ "fee_rate": float(fee_rate),
343
+ "slippage_rate": 0.0,
344
+ "contract_sizes": contract_sizes,
345
+ "tradable": np.ones_like(market.closes, dtype=np.bool_),
346
+ }
347
+
348
+
349
+ def _kernel_portfolio(prepared: Dict):
350
+ return _engine_portfolio(
351
+ n_bars=prepared["n_bars"],
352
+ n_syms=prepared["n_syms"],
353
+ highs=prepared["highs"],
354
+ lows=prepared["lows"],
355
+ closes=prepared["closes"],
356
+ target_pos=prepared["target_units"],
357
+ funding_rates=prepared["funding"],
358
+ is_funding_bar=prepared["is_funding_bar"],
359
+ init_capital=prepared["initial_capital"],
360
+ leverages=prepared["leverages"],
361
+ maint_ratio=prepared["maintenance_ratio"],
362
+ fee_rate=prepared["fee_rate"],
363
+ slippage_rate=prepared["slippage_rate"],
364
+ contract_sizes=prepared["contract_sizes"],
365
+ use_funding=False,
366
+ tradable=prepared["tradable"],
367
+ )
368
+
369
+
370
+ def _depth_frame(idx, close: float, high: float, low: float) -> pd.DataFrame:
371
+ return pd.DataFrame(
372
+ {
373
+ "open": close,
374
+ "high": high,
375
+ "low": low,
376
+ "close": close,
377
+ "volume": 100.0,
378
+ },
379
+ index=idx,
380
+ )
381
+
382
+
383
+ def _timeit(fn, repeats: int) -> float:
384
+ samples: List[float] = []
385
+ for _ in range(max(1, int(repeats))):
386
+ start = time.perf_counter()
387
+ fn()
388
+ samples.append(time.perf_counter() - start)
389
+ return float(statistics.mean(samples))
390
+
391
+
392
+ def _cython_cpp_recommendation(benchmark: Dict) -> str:
393
+ share = benchmark.get("stages", {}).get("pure_kernel_share_pct", 100.0)
394
+ if share >= 35.0:
395
+ return "Pure kernel share is large enough to justify investigating Cython/C++ after correctness locks."
396
+ return "Cython/C++ is not justified yet; optimize cached array preparation and report construction first."
397
+
398
+
399
+ def _json_default(value):
400
+ if isinstance(value, (np.bool_,)):
401
+ return bool(value)
402
+ if isinstance(value, (np.integer,)):
403
+ return int(value)
404
+ if isinstance(value, (np.floating,)):
405
+ return float(value)
406
+ if isinstance(value, pd.Timestamp):
407
+ return value.isoformat()
408
+ raise TypeError(f"{type(value).__name__} is not JSON serializable")
409
+
410
+
411
+ def main(argv: Optional[List[str]] = None) -> int:
412
+ parser = argparse.ArgumentParser(description=__doc__)
413
+ parser.add_argument("--rows", type=int, default=2_000)
414
+ parser.add_argument("--symbols", type=int, default=6)
415
+ parser.add_argument("--repeats", type=int, default=3)
416
+ parser.add_argument("--include-nautilus", action="store_true")
417
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase12_benchmark_nautilus_cert.json")
418
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase12_benchmark_nautilus_cert.md")
419
+ args = parser.parse_args(argv)
420
+ report = run_certification(rows=args.rows, symbols=args.symbols, repeats=args.repeats, include_nautilus=args.include_nautilus)
421
+ args.json_out.parent.mkdir(parents=True, exist_ok=True)
422
+ args.md_out.parent.mkdir(parents=True, exist_ok=True)
423
+ args.json_out.write_text(json.dumps(report, indent=2, default=_json_default) + "\n", encoding="utf-8")
424
+ args.md_out.write_text(make_markdown(report), encoding="utf-8")
425
+ print(make_markdown(report))
426
+ return 0 if report["status"] == "pass" else 1
427
+
428
+
429
+ if __name__ == "__main__":
430
+ raise SystemExit(main())
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Phase 13B native portfolio report-construction benchmark.
4
+
5
+ The runner reuses the Phase 12B decomposition and writes a focused artifact for
6
+ the report-construction optimization pass.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import sys
14
+ from pathlib import Path
15
+ from typing import Dict
16
+
17
+ PACKAGE_DIR = Path(__file__).resolve().parents[1]
18
+ PROJECT_DIR = PACKAGE_DIR.parent
19
+ if str(PROJECT_DIR) not in sys.path:
20
+ sys.path.insert(0, str(PROJECT_DIR))
21
+
22
+ from quantbt.benchmarks.run_phase12_benchmark_nautilus_cert import run_certification # noqa: E402
23
+
24
+
25
+ def run_report(rows: int = 2_000, symbols: int = 6, repeats: int = 3) -> Dict:
26
+ report = run_certification(rows=rows, symbols=symbols, repeats=repeats, include_nautilus=False)
27
+ bench = report["benchmark_followup"]
28
+ stages = bench["stages"]
29
+ return {
30
+ "status": "pass" if report["status"] == "pass" and bench["status"] == "pass" else "fail",
31
+ "rows": int(rows),
32
+ "symbols": int(symbols),
33
+ "repeats": int(repeats),
34
+ "full_facade_seconds": float(stages["full_facade_seconds"]),
35
+ "prepared_reuse_facade_seconds": float(stages["prepared_reuse_facade_seconds"]),
36
+ "array_preparation_seconds": float(stages["array_preparation_seconds"]),
37
+ "pure_numba_kernel_seconds": float(stages["pure_numba_kernel_seconds"]),
38
+ "report_construction_estimate_seconds": float(stages["report_construction_estimate_seconds"]),
39
+ "report_construction_share_pct": float(stages["report_construction_share_pct"]),
40
+ "pure_kernel_share_pct": float(stages["pure_kernel_share_pct"]),
41
+ "prepared_reuse_speedup": float(stages["prepared_reuse_speedup"]),
42
+ "cython_cpp_recommendation": report["cython_cpp_recommendation"],
43
+ "notes": (
44
+ "Phase 13B keeps accounting unchanged and optimizes report construction "
45
+ "with ndarray-first calculations for funding, diagnostics, exposure, "
46
+ "and rebalance reports."
47
+ ),
48
+ }
49
+
50
+
51
+ def make_markdown(report: Dict) -> str:
52
+ return "\n".join(
53
+ [
54
+ "# Phase 13B Native Portfolio Report Construction",
55
+ "",
56
+ f"Status: **{report['status']}**",
57
+ "",
58
+ f"- Rows: `{report['rows']}`",
59
+ f"- Symbols: `{report['symbols']}`",
60
+ f"- Repeats: `{report['repeats']}`",
61
+ f"- Full facade seconds: `{report['full_facade_seconds']:.6f}`",
62
+ f"- Prepared reuse seconds: `{report['prepared_reuse_facade_seconds']:.6f}`",
63
+ f"- Array preparation seconds: `{report['array_preparation_seconds']:.6f}`",
64
+ f"- Pure Numba kernel seconds: `{report['pure_numba_kernel_seconds']:.6f}`",
65
+ f"- Report construction residual seconds: `{report['report_construction_estimate_seconds']:.6f}`",
66
+ f"- Report construction share: `{report['report_construction_share_pct']:.2f}%`",
67
+ f"- Pure kernel share: `{report['pure_kernel_share_pct']:.2f}%`",
68
+ f"- Prepared reuse speedup: `{report['prepared_reuse_speedup']:.3f}x`",
69
+ "",
70
+ "## Notes",
71
+ "",
72
+ report["notes"],
73
+ "",
74
+ "## Cython/C++ Decision",
75
+ "",
76
+ report["cython_cpp_recommendation"],
77
+ ]
78
+ ) + "\n"
79
+
80
+
81
+ def main() -> int:
82
+ parser = argparse.ArgumentParser()
83
+ parser.add_argument("--rows", type=int, default=2_000)
84
+ parser.add_argument("--symbols", type=int, default=6)
85
+ parser.add_argument("--repeats", type=int, default=3)
86
+ parser.add_argument("--json", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase13_portfolio_report.json")
87
+ parser.add_argument("--markdown", type=Path, default=PACKAGE_DIR / "benchmarks" / "phase13_portfolio_report.md")
88
+ args = parser.parse_args()
89
+ report = run_report(rows=args.rows, symbols=args.symbols, repeats=args.repeats)
90
+ args.json.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n")
91
+ args.markdown.write_text(make_markdown(report))
92
+ print(json.dumps(report, indent=2, sort_keys=True))
93
+ return 0 if report["status"] == "pass" else 1
94
+
95
+
96
+ if __name__ == "__main__":
97
+ raise SystemExit(main())