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.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- 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
|