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,302 @@
1
+ #!/usr/bin/env python3
2
+ """Compare Phase 9 optimized paths against legacy-equivalent construction."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import json
8
+ import math
9
+ import sys
10
+ from pathlib import Path
11
+ from typing import Dict, Optional
12
+
13
+ import numpy as np
14
+ import pandas as pd
15
+
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 import AccountConfig, BacktestEngineV2, OrderIntent, OrderSide, OrderType, TimeInForce
23
+ from quantbt.backends import NativeEventBackend, NativeEventConfig
24
+ from quantbt.core.order_compiler import compile_order_intents
25
+ from quantbt.core.preprocessor import align_series, build_market_arrays, prepare_funding, validate_datetime
26
+ from quantbt.sizing.fast import scale_signal_notional_matrix
27
+ from quantbt.sizing.modes import compute_target_units
28
+
29
+
30
+ def main(argv: Optional[list[str]] = None) -> int:
31
+ parser = argparse.ArgumentParser(description="Run Phase 9 parity checks.")
32
+ parser.add_argument("--json-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase9_parity.json")
33
+ parser.add_argument("--md-out", type=Path, default=PACKAGE_DIR / "benchmarks" / "out" / "phase9_parity.md")
34
+ args = parser.parse_args(argv)
35
+
36
+ report = run_parity()
37
+ args.json_out.parent.mkdir(parents=True, exist_ok=True)
38
+ args.json_out.write_text(json.dumps(report, indent=2, sort_keys=True), encoding="utf-8")
39
+ args.md_out.write_text(markdown_report(report), encoding="utf-8")
40
+ print(json.dumps(report, indent=2, sort_keys=True))
41
+ return 0 if report["passed"] else 1
42
+
43
+
44
+ def run_parity() -> Dict:
45
+ idx, data, signals = _market()
46
+ symbols = ["A", "B"]
47
+ alloc = {"A": 10_000.0, "B": 5_000.0}
48
+
49
+ target_diff = _target_unit_diff(data, signals, symbols, alloc)
50
+ vectorized_diff = _vectorized_result_diff(data, signals, alloc)
51
+ order_diff = _order_array_diff(idx)
52
+ event_diff = _event_result_diff(data, idx)
53
+ prepared_diff = _prepared_event_reuse_diff(data, idx)
54
+
55
+ report = {
56
+ "target_unit_max_abs_diff": target_diff,
57
+ "vectorized_equity_max_abs_diff": vectorized_diff["equity"],
58
+ "vectorized_position_max_abs_diff": vectorized_diff["positions"],
59
+ "order_array_max_abs_diff": order_diff,
60
+ "event_equity_max_abs_diff": event_diff["equity"],
61
+ "event_order_report_max_abs_diff": event_diff["order_report"],
62
+ "event_fill_count_diff": event_diff["fill_count"],
63
+ "event_fill_price_max_abs_diff": event_diff["fill_price"],
64
+ "prepared_event_equity_max_abs_diff": prepared_diff["equity"],
65
+ "prepared_event_order_report_max_abs_diff": prepared_diff["order_report"],
66
+ "prepared_event_fill_count_diff": prepared_diff["fill_count"],
67
+ }
68
+ report["passed"] = all(
69
+ [
70
+ report["target_unit_max_abs_diff"] <= 1e-12,
71
+ report["vectorized_equity_max_abs_diff"] <= 1e-10,
72
+ report["vectorized_position_max_abs_diff"] <= 1e-12,
73
+ report["order_array_max_abs_diff"] <= 0.0,
74
+ report["event_equity_max_abs_diff"] <= 1e-10,
75
+ report["event_order_report_max_abs_diff"] <= 1e-12,
76
+ report["event_fill_count_diff"] == 0,
77
+ report["event_fill_price_max_abs_diff"] <= 1e-12,
78
+ report["prepared_event_equity_max_abs_diff"] <= 1e-10,
79
+ report["prepared_event_order_report_max_abs_diff"] <= 1e-12,
80
+ report["prepared_event_fill_count_diff"] == 0,
81
+ ]
82
+ )
83
+ return report
84
+
85
+
86
+ def _market():
87
+ idx = pd.date_range("2024-01-01", periods=128, freq="15min", tz="UTC")
88
+ grid = np.arange(len(idx), dtype=float)
89
+ close_a = pd.Series(100.0 + np.sin(grid / 7.0) * 2.0 + grid * 0.01, index=idx)
90
+ close_b = pd.Series(50.0 + np.cos(grid / 11.0) * 1.5 + grid * 0.005, index=idx)
91
+ data = {
92
+ "A": pd.DataFrame({"open": close_a, "high": close_a + 0.8, "low": close_a - 0.8, "close": close_a, "volume": 1_000.0}),
93
+ "B": pd.DataFrame({"open": close_b, "high": close_b + 0.5, "low": close_b - 0.5, "close": close_b, "volume": 1_000.0}),
94
+ }
95
+ sig_a = np.where((grid.astype(int) // 9) % 4 == 0, 1.0, np.where((grid.astype(int) // 9) % 4 == 2, -0.5, 0.0))
96
+ sig_b = np.where((grid.astype(int) // 13) % 3 == 0, -1.0, np.where((grid.astype(int) // 13) % 3 == 1, 0.5, 0.0))
97
+ signals = {"A": pd.Series(sig_a, index=idx), "B": pd.Series(sig_b, index=idx)}
98
+ return idx, data, signals
99
+
100
+
101
+ def _orders(idx):
102
+ return [
103
+ OrderIntent(idx[5], "A", OrderSide.BUY, OrderType.MARKET, qty=1.0, tif=TimeInForce.IOC),
104
+ OrderIntent(idx[12], "B", OrderSide.SELL, OrderType.LIMIT, qty=2.0, price=52.0, tif=TimeInForce.GTC),
105
+ OrderIntent(idx[12], "A", OrderSide.SELL, OrderType.MARKET, qty=0.5, tif=TimeInForce.IOC),
106
+ OrderIntent(idx[40], "B", OrderSide.BUY, OrderType.MARKET, qty=2.0, tif=TimeInForce.IOC),
107
+ OrderIntent(idx[90], "A", OrderSide.SELL, OrderType.LIMIT, qty=0.5, price=102.0, tif=TimeInForce.GTC),
108
+ ]
109
+
110
+
111
+ def _target_unit_diff(data, signals, symbols, alloc):
112
+ closes_m = np.column_stack([data[s]["close"].to_numpy(dtype=float) for s in symbols])
113
+ signals_m = np.column_stack([signals[s].to_numpy(dtype=float) for s in symbols])
114
+ allocs = np.array([alloc[s] for s in symbols], dtype=np.float64)
115
+ fast = scale_signal_notional_matrix(signals_m, closes_m, allocs, use_pyramiding=True)
116
+ legacy = np.column_stack(
117
+ [
118
+ compute_target_units("signal_notional", signals[s], data[s]["close"], alloc[s], True).to_numpy()
119
+ for s in symbols
120
+ ]
121
+ )
122
+ return float(np.max(np.abs(fast - legacy)))
123
+
124
+
125
+ def _vectorized_result_diff(data, signals, alloc):
126
+ account = AccountConfig(initial_capital=100_000.0, leverage=5.0)
127
+ fast = BacktestEngineV2(
128
+ data=data,
129
+ signals=signals,
130
+ backend="native_vectorized",
131
+ account=account,
132
+ alloc_per_trade=alloc,
133
+ hedge_type="signal_notional",
134
+ use_funding=False,
135
+ ).result
136
+ target_units = {
137
+ s: compute_target_units("signal_notional", signals[s], data[s]["close"], alloc[s], True)
138
+ for s in signals
139
+ }
140
+ legacy_route = BacktestEngineV2(
141
+ data=data,
142
+ target_units=target_units,
143
+ backend="native_vectorized",
144
+ account=account,
145
+ use_funding=False,
146
+ ).result
147
+ return {
148
+ "equity": float(np.max(np.abs(fast.equity.to_numpy() - legacy_route.equity.to_numpy()))),
149
+ "positions": float(np.max(np.abs(fast.positions.to_numpy() - legacy_route.positions.to_numpy()))),
150
+ }
151
+
152
+
153
+ def _order_array_diff(idx):
154
+ orders = _orders(idx)
155
+ symbol_to_col = {"A": 0, "B": 1}
156
+ compiled = compile_order_intents(idx, orders, symbol_to_col)
157
+ legacy = _legacy_order_arrays(idx, orders, symbol_to_col)
158
+ diffs = [
159
+ np.max(np.abs(compiled.order_ptr - legacy[0])),
160
+ np.max(np.abs(compiled.order_symbol - legacy[1])),
161
+ np.max(np.abs(compiled.order_side - legacy[2])),
162
+ np.max(np.abs(compiled.order_type - legacy[3])),
163
+ np.max(np.abs(compiled.order_qty - legacy[4])),
164
+ np.max(np.abs(compiled.order_price - legacy[5])),
165
+ np.max(np.abs(compiled.order_tif - legacy[6])),
166
+ np.max(np.abs(compiled.original_index - legacy[7])),
167
+ ]
168
+ return float(max(diffs))
169
+
170
+
171
+ def _event_result_diff(data, idx):
172
+ orders = _orders(idx)
173
+ result = BacktestEngineV2(
174
+ data=data,
175
+ orders=orders,
176
+ backend="native_event",
177
+ account=AccountConfig(initial_capital=100_000.0, leverage=5.0),
178
+ use_funding=False,
179
+ ).result
180
+ rerun = BacktestEngineV2(
181
+ data=data,
182
+ orders=orders,
183
+ backend="native_event",
184
+ account=AccountConfig(initial_capital=100_000.0, leverage=5.0),
185
+ use_funding=False,
186
+ ).result
187
+ report = result.metadata["order_report"].sort_values("original_index")
188
+ rerun_report = rerun.metadata["order_report"].sort_values("original_index")
189
+ fill_prices = np.array([fill.price for fill in result.fills], dtype=float)
190
+ rerun_fill_prices = np.array([fill.price for fill in rerun.fills], dtype=float)
191
+ fill_price_diff = 0.0
192
+ if len(fill_prices) or len(rerun_fill_prices):
193
+ if len(fill_prices) != len(rerun_fill_prices):
194
+ fill_price_diff = math.inf
195
+ else:
196
+ fill_price_diff = float(np.max(np.abs(fill_prices - rerun_fill_prices)))
197
+ return {
198
+ "equity": float(np.max(np.abs(result.equity.to_numpy() - rerun.equity.to_numpy()))),
199
+ "order_report": float(np.max(np.abs(report.to_numpy(dtype=float) - rerun_report.to_numpy(dtype=float)))),
200
+ "fill_count": int(len(result.fills) - len(rerun.fills)),
201
+ "fill_price": fill_price_diff,
202
+ }
203
+
204
+
205
+ def _prepared_event_reuse_diff(data, idx):
206
+ symbols = ["A", "B"]
207
+ orders = _orders(idx)
208
+ closes = {symbol: data[symbol]["close"] for symbol in symbols}
209
+ highs = {symbol: data[symbol]["high"] for symbol in symbols}
210
+ lows = {symbol: data[symbol]["low"] for symbol in symbols}
211
+ backend = NativeEventBackend(
212
+ NativeEventConfig(account=AccountConfig(initial_capital=100_000.0, leverage=5.0), use_funding=False)
213
+ )
214
+ normal = backend.run_orders(idx, orders, closes, highs=highs, lows=lows, symbols=symbols)
215
+ idx_n = validate_datetime(idx)
216
+ close_dict = align_series(closes, symbols, idx_n)
217
+ high_dict = align_series(highs, symbols, idx_n, fallback=close_dict)
218
+ low_dict = align_series(lows, symbols, idx_n, fallback=close_dict)
219
+ funding_dict = prepare_funding(0.0, symbols, idx_n)
220
+ market_arrays = build_market_arrays(symbols, idx_n, close_dict, high_dict, low_dict, funding_dict)
221
+ compiled = compile_order_intents(idx_n, orders, {"A": 0, "B": 1})
222
+ reused = backend.run_orders(
223
+ idx,
224
+ orders,
225
+ closes,
226
+ highs=highs,
227
+ lows=lows,
228
+ symbols=symbols,
229
+ market_arrays=market_arrays,
230
+ compiled_orders=compiled,
231
+ )
232
+ return {
233
+ "equity": float(np.max(np.abs(normal.equity.to_numpy() - reused.equity.to_numpy()))),
234
+ "order_report": float(
235
+ np.max(
236
+ np.abs(
237
+ normal.metadata["order_report"].to_numpy(dtype=float)
238
+ - reused.metadata["order_report"].to_numpy(dtype=float)
239
+ )
240
+ )
241
+ ),
242
+ "fill_count": int(len(normal.fills) - len(reused.fills)),
243
+ }
244
+
245
+
246
+ def _legacy_order_arrays(idx, orders, symbol_to_col):
247
+ def bar_index(timestamp):
248
+ ts = pd.Timestamp(timestamp)
249
+ if ts.tz is None:
250
+ ts = ts.tz_localize("UTC")
251
+ else:
252
+ ts = ts.tz_convert("UTC")
253
+ pos = idx.searchsorted(ts, side="left")
254
+ if pos >= len(idx):
255
+ raise ValueError("order timestamp is after the available data")
256
+ return int(pos)
257
+
258
+ sorted_orders = sorted(enumerate(orders), key=lambda item: bar_index(item[1].timestamp))
259
+ n = len(sorted_orders)
260
+ order_bar = np.zeros(n, dtype=np.int64)
261
+ order_symbol = np.zeros(n, dtype=np.int64)
262
+ order_side = np.zeros(n, dtype=np.int64)
263
+ order_type = np.zeros(n, dtype=np.int64)
264
+ order_qty = np.zeros(n, dtype=np.float64)
265
+ order_price = np.zeros(n, dtype=np.float64)
266
+ order_tif = np.zeros(n, dtype=np.int64)
267
+ original_index = np.zeros(n, dtype=np.int64)
268
+ for k, (orig_idx, order) in enumerate(sorted_orders):
269
+ order_bar[k] = bar_index(order.timestamp)
270
+ order_symbol[k] = symbol_to_col[order.symbol]
271
+ order_side[k] = 1 if order.side is OrderSide.BUY else -1
272
+ order_type[k] = 0 if order.order_type is OrderType.MARKET else 1
273
+ order_qty[k] = order.qty
274
+ order_price[k] = 0.0 if order.price is None else order.price
275
+ order_tif[k] = {TimeInForce.GTC: 0, TimeInForce.IOC: 1, TimeInForce.FOK: 2, TimeInForce.GTD: 3}[order.tif]
276
+ original_index[k] = orig_idx
277
+ order_ptr = np.zeros(len(idx) + 1, dtype=np.int64)
278
+ for bar in order_bar:
279
+ order_ptr[bar + 1] += 1
280
+ for i in range(1, len(order_ptr)):
281
+ order_ptr[i] += order_ptr[i - 1]
282
+ return order_ptr, order_symbol, order_side, order_type, order_qty, order_price, order_tif, original_index
283
+
284
+
285
+ def markdown_report(report: Dict) -> str:
286
+ lines = [
287
+ "# Phase 9 Optimization Parity Report",
288
+ "",
289
+ f"Passed: `{report['passed']}`",
290
+ "",
291
+ "| check | value |",
292
+ "| --- | ---: |",
293
+ ]
294
+ for key, value in report.items():
295
+ if key == "passed":
296
+ continue
297
+ lines.append(f"| `{key}` | {value} |")
298
+ return "\n".join(lines) + "\n"
299
+
300
+
301
+ if __name__ == "__main__":
302
+ raise SystemExit(main())