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,211 @@
1
+ """
2
+ Arbitrage domain audit helpers.
3
+
4
+ These functions validate accounting invariants on completed native arbitrage
5
+ results. They are intentionally report-level checks, not execution logic.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Dict, Iterable, Optional
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from ..core.results import BacktestResultV2
16
+
17
+
18
+ def build_arbitrage_domain_audit(
19
+ result: BacktestResultV2,
20
+ *,
21
+ tolerance: float = 1e-9,
22
+ raise_on_fail: bool = False,
23
+ ) -> Dict:
24
+ """
25
+ Return a compact audit summary for a native arbitrage result.
26
+
27
+ The audit checks that package PnL reconciles to equity deltas, leg PnL sums
28
+ to package PnL, fees reconcile to the result fee series, target-unit symbols
29
+ match result symbols, and final target/position units are flat when the
30
+ strategy exits.
31
+ """
32
+ metadata = result.metadata or {}
33
+ missing = [
34
+ name
35
+ for name in ("package_pnl_report", "leg_pnl_report", "package_target_units")
36
+ if name not in metadata or metadata.get(name) is None
37
+ ]
38
+
39
+ package_report = _frame(metadata.get("package_pnl_report"))
40
+ leg_report = _frame(metadata.get("leg_pnl_report"))
41
+ target_units = _frame(metadata.get("package_target_units"))
42
+ rejection_report = _frame(metadata.get("package_rejection_report"))
43
+ order_report = _frame(metadata.get("order_report", metadata.get("orders_report")))
44
+
45
+ max_package_residual = _max_abs(package_report.get("pnl_residual")) if not package_report.empty else np.nan
46
+ leg_vs_package_diff = np.nan
47
+ if not leg_report.empty and not package_report.empty and "timestamp" in leg_report and "total_pnl" in leg_report:
48
+ leg_sum = leg_report.groupby(pd.to_datetime(leg_report["timestamp"], utc=True), sort=False)["total_pnl"].sum()
49
+ pkg = _series_from_report(package_report, "package_pnl")
50
+ leg_vs_package_diff = _max_abs((leg_sum.reindex(pkg.index, fill_value=0.0) - pkg).to_numpy(dtype=float))
51
+
52
+ fee_residual = np.nan
53
+ if not leg_report.empty and "fee" in leg_report:
54
+ fee_sum = float(pd.to_numeric(leg_report["fee"], errors="coerce").fillna(0.0).sum())
55
+ result_fee = float(result.fees.sum()) if isinstance(result.fees, pd.Series) and not result.fees.empty else 0.0
56
+ fee_residual = abs(fee_sum - result_fee)
57
+
58
+ target_symbols = set(map(str, target_units.columns)) if not target_units.empty else set()
59
+ result_symbols = set(map(str, result.symbols))
60
+ target_symbols_match = bool(target_symbols) and target_symbols == result_symbols
61
+ final_target_gross = float(target_units.iloc[-1].abs().sum()) if not target_units.empty else np.nan
62
+ final_position_gross = _final_position_gross(result.positions, result.symbols)
63
+
64
+ checks = {
65
+ "has_required_reports": not missing,
66
+ "package_pnl_residual_ok": _ok(max_package_residual, tolerance),
67
+ "leg_pnl_reconciles_to_package": _ok(leg_vs_package_diff, tolerance),
68
+ "fees_reconcile": _ok(fee_residual, tolerance),
69
+ "target_symbols_match": target_symbols_match,
70
+ "final_target_flat": _ok(final_target_gross, tolerance),
71
+ "final_position_flat": _ok(final_position_gross, tolerance),
72
+ }
73
+ passed = all(checks.values())
74
+ status = "pass" if passed else "fail"
75
+ audit = {
76
+ "status": status,
77
+ "passed": passed,
78
+ "tolerance": float(tolerance),
79
+ "engine": metadata.get("engine"),
80
+ "backend": metadata.get("backend"),
81
+ "arb_id": metadata.get("arb_id"),
82
+ "arb_type": metadata.get("arb_type"),
83
+ "missing_reports": missing,
84
+ "checks": checks,
85
+ "max_abs_package_pnl_residual": _float_or_none(max_package_residual),
86
+ "max_abs_leg_vs_package_pnl_diff": _float_or_none(leg_vs_package_diff),
87
+ "max_abs_fee_residual": _float_or_none(fee_residual),
88
+ "final_gross_target_units": _float_or_none(final_target_gross),
89
+ "final_gross_position_units": _float_or_none(final_position_gross),
90
+ "target_symbols": sorted(target_symbols),
91
+ "result_symbols": sorted(result_symbols),
92
+ "order_count": int(len(order_report)),
93
+ "fill_count": int(len(getattr(result, "fills", ()) or ())),
94
+ "rejection_count": int(len(rejection_report)),
95
+ }
96
+ if raise_on_fail and not passed:
97
+ raise AssertionError(f"arbitrage domain audit failed: {audit}")
98
+ return audit
99
+
100
+
101
+ def compare_native_arbitrage_results(
102
+ event_result: BacktestResultV2,
103
+ vectorized_result: BacktestResultV2,
104
+ *,
105
+ tolerance: float = 1e-9,
106
+ raise_on_fail: bool = False,
107
+ ) -> Dict:
108
+ """
109
+ Compare native event and native vectorized arbitrage outputs.
110
+
111
+ This is a high-signal parity check for mock/golden tests. It does not
112
+ require identical order reports, only accounting-equivalent equity,
113
+ positions, target units, and package residuals.
114
+ """
115
+ equity_diff = _max_abs((event_result.equity - vectorized_result.equity.reindex(event_result.equity.index)).to_numpy(dtype=float))
116
+ position_diff = _max_abs((_position_units(event_result) - _position_units(vectorized_result).reindex(event_result.equity.index)).to_numpy(dtype=float))
117
+
118
+ event_target = _frame(event_result.metadata.get("package_target_units"))
119
+ vector_target = _frame(vectorized_result.metadata.get("package_target_units"))
120
+ target_diff = np.nan
121
+ if not event_target.empty and not vector_target.empty:
122
+ target_diff = _max_abs((event_target - vector_target.reindex(event_target.index)).to_numpy(dtype=float))
123
+
124
+ event_package = _series_from_report(_frame(event_result.metadata.get("package_pnl_report")), "pnl_residual")
125
+ vector_package = _series_from_report(_frame(vectorized_result.metadata.get("package_pnl_report")), "pnl_residual")
126
+ residual_diff = np.nan
127
+ if not event_package.empty and not vector_package.empty:
128
+ residual_diff = max(_max_abs(event_package.to_numpy(dtype=float)), _max_abs(vector_package.to_numpy(dtype=float)))
129
+
130
+ checks = {
131
+ "equity_matches": _ok(equity_diff, tolerance),
132
+ "positions_match": _ok(position_diff, tolerance),
133
+ "target_units_match": _ok(target_diff, tolerance),
134
+ "package_residuals_ok": _ok(residual_diff, tolerance),
135
+ }
136
+ passed = all(checks.values())
137
+ report = {
138
+ "status": "pass" if passed else "fail",
139
+ "passed": passed,
140
+ "tolerance": float(tolerance),
141
+ "event_engine": event_result.metadata.get("engine"),
142
+ "vectorized_engine": vectorized_result.metadata.get("engine"),
143
+ "checks": checks,
144
+ "max_abs_equity_diff": _float_or_none(equity_diff),
145
+ "max_abs_position_diff": _float_or_none(position_diff),
146
+ "max_abs_target_unit_diff": _float_or_none(target_diff),
147
+ "max_abs_package_residual": _float_or_none(residual_diff),
148
+ }
149
+ if raise_on_fail and not passed:
150
+ raise AssertionError(f"native arbitrage parity failed: {report}")
151
+ return report
152
+
153
+
154
+ def _frame(value) -> pd.DataFrame:
155
+ return value if isinstance(value, pd.DataFrame) else pd.DataFrame()
156
+
157
+
158
+ def _series_from_report(report: pd.DataFrame, column: str) -> pd.Series:
159
+ if report.empty or column not in report:
160
+ return pd.Series(dtype=float)
161
+ series = pd.to_numeric(report[column], errors="coerce").fillna(0.0)
162
+ if isinstance(report.index, pd.DatetimeIndex):
163
+ series.index = pd.to_datetime(report.index, utc=True)
164
+ return series.astype(float)
165
+
166
+
167
+ def _position_units(result: BacktestResultV2) -> pd.DataFrame:
168
+ out = result.positions.copy()
169
+ rename = {col: str(col).replace("Position_", "", 1) for col in out.columns}
170
+ out = out.rename(columns=rename)
171
+ return out.reindex(columns=list(result.symbols)).fillna(0.0)
172
+
173
+
174
+ def _final_position_gross(positions: pd.DataFrame, symbols: Iterable[str]) -> float:
175
+ if positions.empty:
176
+ return 0.0
177
+ frame = positions.rename(columns={col: str(col).replace("Position_", "", 1) for col in positions.columns})
178
+ cols = [symbol for symbol in symbols if symbol in frame.columns]
179
+ if not cols:
180
+ return float(positions.iloc[-1].abs().sum())
181
+ return float(frame[cols].iloc[-1].abs().sum())
182
+
183
+
184
+ def _max_abs(value) -> float:
185
+ if value is None:
186
+ return np.nan
187
+ arr = np.asarray(value, dtype=np.float64)
188
+ arr = arr[np.isfinite(arr)]
189
+ if arr.size == 0:
190
+ return np.nan
191
+ return float(np.max(np.abs(arr)))
192
+
193
+
194
+ def _ok(value: Optional[float], tolerance: float) -> bool:
195
+ if value is None:
196
+ return False
197
+ try:
198
+ numeric = float(value)
199
+ except (TypeError, ValueError):
200
+ return False
201
+ return bool(np.isfinite(numeric) and numeric <= float(tolerance))
202
+
203
+
204
+ def _float_or_none(value):
205
+ try:
206
+ numeric = float(value)
207
+ except (TypeError, ValueError):
208
+ return None
209
+ if not np.isfinite(numeric):
210
+ return None
211
+ return numeric