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,496 @@
|
|
|
1
|
+
"""Parity helpers for native-vs-Nautilus audit reports."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ..core.orders import Fill, OrderIntent
|
|
11
|
+
from ..core.results import BacktestResultV2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
PARITY_COLUMNS = [
|
|
15
|
+
"row",
|
|
16
|
+
"timestamp",
|
|
17
|
+
"symbol",
|
|
18
|
+
"side",
|
|
19
|
+
"requested_qty",
|
|
20
|
+
"requested_price",
|
|
21
|
+
"native_fill_price",
|
|
22
|
+
"nautilus_fill_price",
|
|
23
|
+
"fill_price_diff",
|
|
24
|
+
"native_fee",
|
|
25
|
+
"nautilus_fee",
|
|
26
|
+
"fee_diff",
|
|
27
|
+
"native_position_after",
|
|
28
|
+
"nautilus_position_after",
|
|
29
|
+
"position_diff",
|
|
30
|
+
"native_equity",
|
|
31
|
+
"nautilus_equity",
|
|
32
|
+
"equity_diff",
|
|
33
|
+
"native_status",
|
|
34
|
+
"nautilus_status",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
DEPTH_EXECUTION_COLUMNS = [
|
|
39
|
+
"row",
|
|
40
|
+
"timestamp",
|
|
41
|
+
"symbol",
|
|
42
|
+
"side",
|
|
43
|
+
"depth_status",
|
|
44
|
+
"depth_filled_qty",
|
|
45
|
+
"nautilus_filled_qty",
|
|
46
|
+
"filled_qty_diff",
|
|
47
|
+
"depth_fill_price",
|
|
48
|
+
"nautilus_fill_price",
|
|
49
|
+
"fill_price_diff",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def build_nautilus_depth_execution_report(result: BacktestResultV2) -> pd.DataFrame:
|
|
54
|
+
"""
|
|
55
|
+
Compare depth-preflight filled rows with Nautilus package fills.
|
|
56
|
+
|
|
57
|
+
Rows are sequence-aligned because package orders are submitted to Nautilus
|
|
58
|
+
after deterministic preflight filtering. This is the package-level analogue
|
|
59
|
+
of the explicit-order parity table.
|
|
60
|
+
"""
|
|
61
|
+
metadata = result.metadata or {}
|
|
62
|
+
depth = _report_frame(metadata.get("nautilus_depth_order_report"))
|
|
63
|
+
if depth.empty:
|
|
64
|
+
return pd.DataFrame(columns=DEPTH_EXECUTION_COLUMNS)
|
|
65
|
+
depth = depth[depth["status"].astype(str).str.lower().isin({"filled", "partial"})].reset_index(drop=True)
|
|
66
|
+
fills = _fills_from_result(result)
|
|
67
|
+
row_count = max(len(depth), len(fills))
|
|
68
|
+
rows: List[Dict[str, Any]] = []
|
|
69
|
+
for row in range(row_count):
|
|
70
|
+
depth_row = _row(depth, row)
|
|
71
|
+
fill_row = _row(fills, row)
|
|
72
|
+
depth_qty = _safe_float(_get(depth_row, "filled_qty"))
|
|
73
|
+
fill_qty = _safe_float(_get(fill_row, "qty"))
|
|
74
|
+
depth_price = _safe_float(_get(depth_row, "fill_price"))
|
|
75
|
+
fill_price = _safe_float(_get(fill_row, "price"))
|
|
76
|
+
rows.append(
|
|
77
|
+
{
|
|
78
|
+
"row": row,
|
|
79
|
+
"timestamp": _first_non_null(_get(fill_row, "timestamp"), _get(depth_row, "effective_timestamp"), _get(depth_row, "timestamp")),
|
|
80
|
+
"symbol": _first_non_null(_get(fill_row, "symbol"), _get(depth_row, "symbol")),
|
|
81
|
+
"side": _first_non_null(_get(fill_row, "side"), _get(depth_row, "side")),
|
|
82
|
+
"depth_status": _get(depth_row, "status"),
|
|
83
|
+
"depth_filled_qty": depth_qty,
|
|
84
|
+
"nautilus_filled_qty": fill_qty,
|
|
85
|
+
"filled_qty_diff": _diff(depth_qty, fill_qty),
|
|
86
|
+
"depth_fill_price": depth_price,
|
|
87
|
+
"nautilus_fill_price": fill_price,
|
|
88
|
+
"fill_price_diff": _diff(depth_price, fill_price),
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
return pd.DataFrame(rows, columns=DEPTH_EXECUTION_COLUMNS)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def build_nautilus_depth_parity_summary(
|
|
95
|
+
result: BacktestResultV2,
|
|
96
|
+
fill_price_tolerance: float = 1e-9,
|
|
97
|
+
qty_tolerance: float = 1e-9,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Summarize preflight-vs-Nautilus package execution counts.
|
|
101
|
+
|
|
102
|
+
This helper is for Phase 5.4 package workflows where an optional
|
|
103
|
+
execution-depth preflight may reject, cancel, or partially adjust orders
|
|
104
|
+
before accepted orders are submitted to Nautilus.
|
|
105
|
+
"""
|
|
106
|
+
metadata = result.metadata or {}
|
|
107
|
+
depth_order_report = _report_frame(metadata.get("nautilus_depth_order_report"))
|
|
108
|
+
depth_package_report = _report_frame(metadata.get("nautilus_depth_package_report"))
|
|
109
|
+
package_order_map = _report_frame(metadata.get("package_order_map"))
|
|
110
|
+
orders_report = _report_frame(metadata.get("orders_report", metadata.get("order_report")))
|
|
111
|
+
fills_report = _report_frame(metadata.get("fills_report"))
|
|
112
|
+
depth_enabled = bool(metadata.get("nautilus_depth_enabled", False))
|
|
113
|
+
accepted = int(metadata.get("order_count_after_depth", len(package_order_map)))
|
|
114
|
+
before = int(metadata.get("order_count_before_depth", accepted))
|
|
115
|
+
nautilus_orders = int(metadata.get("orders_count", len(orders_report)))
|
|
116
|
+
nautilus_fills = int(metadata.get("fills_count", len(fills_report)))
|
|
117
|
+
rejected = _status_count(depth_order_report, "rejected")
|
|
118
|
+
partial = _status_count(depth_order_report, "partial")
|
|
119
|
+
canceled = _status_count(depth_order_report, "canceled")
|
|
120
|
+
submitted_matches = nautilus_orders == accepted or (accepted == 0 and nautilus_orders == 0)
|
|
121
|
+
execution_report = build_nautilus_depth_execution_report(result)
|
|
122
|
+
max_fill_price_diff = _max_abs(execution_report, "fill_price_diff")
|
|
123
|
+
max_qty_diff = _max_abs(execution_report, "filled_qty_diff")
|
|
124
|
+
execution_matches = max_fill_price_diff <= float(fill_price_tolerance) and max_qty_diff <= float(qty_tolerance)
|
|
125
|
+
summary = {
|
|
126
|
+
"status": "pass" if submitted_matches and execution_matches else "execution_diff",
|
|
127
|
+
"passed": bool(submitted_matches and execution_matches),
|
|
128
|
+
"depth_enabled": depth_enabled,
|
|
129
|
+
"input_orders": before,
|
|
130
|
+
"accepted_after_depth": accepted,
|
|
131
|
+
"nautilus_orders": nautilus_orders,
|
|
132
|
+
"nautilus_fills": nautilus_fills,
|
|
133
|
+
"depth_rejected": rejected,
|
|
134
|
+
"depth_partial": partial,
|
|
135
|
+
"depth_canceled": canceled,
|
|
136
|
+
"package_rows": int(len(depth_package_report)),
|
|
137
|
+
"execution_rows": int(len(execution_report)),
|
|
138
|
+
"max_abs_fill_price_diff": float(max_fill_price_diff),
|
|
139
|
+
"max_abs_filled_qty_diff": float(max_qty_diff),
|
|
140
|
+
"fill_price_tolerance": float(fill_price_tolerance),
|
|
141
|
+
"qty_tolerance": float(qty_tolerance),
|
|
142
|
+
"engine": metadata.get("engine"),
|
|
143
|
+
"input_mode": metadata.get("input_mode"),
|
|
144
|
+
}
|
|
145
|
+
if not submitted_matches:
|
|
146
|
+
summary["status"] = "execution_count_diff"
|
|
147
|
+
if not depth_enabled:
|
|
148
|
+
summary["status"] = "not_enabled"
|
|
149
|
+
summary["passed"] = False
|
|
150
|
+
return summary
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def summarize_native_nautilus_parity_report(
|
|
154
|
+
parity_report: pd.DataFrame,
|
|
155
|
+
fill_price_tolerance: float = 1e-9,
|
|
156
|
+
fee_tolerance: float = 1e-9,
|
|
157
|
+
position_tolerance: float = 1e-9,
|
|
158
|
+
equity_tolerance: float = 1e-6,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""Return compact institutional audit diagnostics for a parity table."""
|
|
161
|
+
frame = parity_report.copy()
|
|
162
|
+
summary = {
|
|
163
|
+
"rows": int(len(frame)),
|
|
164
|
+
"native_filled_rows": int(frame["native_fill_price"].notna().sum()) if "native_fill_price" in frame else 0,
|
|
165
|
+
"nautilus_filled_rows": int(frame["nautilus_fill_price"].notna().sum()) if "nautilus_fill_price" in frame else 0,
|
|
166
|
+
"max_abs_fill_price_diff": _max_abs(frame, "fill_price_diff"),
|
|
167
|
+
"max_abs_fee_diff": _max_abs(frame, "fee_diff"),
|
|
168
|
+
"max_abs_position_diff": _max_abs(frame, "position_diff"),
|
|
169
|
+
"max_abs_equity_diff": _max_abs(frame, "equity_diff"),
|
|
170
|
+
"fill_price_tolerance": float(fill_price_tolerance),
|
|
171
|
+
"fee_tolerance": float(fee_tolerance),
|
|
172
|
+
"position_tolerance": float(position_tolerance),
|
|
173
|
+
"equity_tolerance": float(equity_tolerance),
|
|
174
|
+
}
|
|
175
|
+
summary["passed"] = bool(
|
|
176
|
+
summary["max_abs_fill_price_diff"] <= fill_price_tolerance
|
|
177
|
+
and summary["max_abs_fee_diff"] <= fee_tolerance
|
|
178
|
+
and summary["max_abs_position_diff"] <= position_tolerance
|
|
179
|
+
and summary["max_abs_equity_diff"] <= equity_tolerance
|
|
180
|
+
)
|
|
181
|
+
if summary["passed"]:
|
|
182
|
+
summary["status"] = "pass"
|
|
183
|
+
elif summary["max_abs_fill_price_diff"] > fill_price_tolerance or summary["max_abs_position_diff"] > position_tolerance:
|
|
184
|
+
summary["status"] = "execution_diff"
|
|
185
|
+
else:
|
|
186
|
+
summary["status"] = "accounting_diff"
|
|
187
|
+
return summary
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def build_native_nautilus_parity_report(
|
|
191
|
+
native_result: BacktestResultV2,
|
|
192
|
+
nautilus_result: BacktestResultV2,
|
|
193
|
+
) -> pd.DataFrame:
|
|
194
|
+
"""
|
|
195
|
+
Build an execution audit table comparing native event and Nautilus results.
|
|
196
|
+
|
|
197
|
+
The report is intentionally tolerant of source formats. Native results are
|
|
198
|
+
usually dataclass-backed (`orders` and `fills`), while Nautilus results are
|
|
199
|
+
report-backed (`orders_report`, `fills_report`, `package_order_map`). Rows
|
|
200
|
+
are aligned by order/fill sequence, which is stable for deterministic
|
|
201
|
+
single-symbol explicit-order replay.
|
|
202
|
+
"""
|
|
203
|
+
native_orders = _orders_from_result(native_result)
|
|
204
|
+
native_fills = _fills_from_result(native_result)
|
|
205
|
+
nautilus_orders = _orders_from_result(nautilus_result)
|
|
206
|
+
nautilus_fills = _fills_from_result(nautilus_result)
|
|
207
|
+
row_count = max(len(native_orders), len(nautilus_orders), len(native_fills), len(nautilus_fills))
|
|
208
|
+
|
|
209
|
+
rows: List[Dict[str, Any]] = []
|
|
210
|
+
for row in range(row_count):
|
|
211
|
+
native_order = _row(native_orders, row)
|
|
212
|
+
nautilus_order = _row(nautilus_orders, row)
|
|
213
|
+
native_fill = _row(native_fills, row)
|
|
214
|
+
nautilus_fill = _row(nautilus_fills, row)
|
|
215
|
+
|
|
216
|
+
timestamp = _first_non_null(
|
|
217
|
+
_get(native_fill, "timestamp"),
|
|
218
|
+
_get(nautilus_fill, "timestamp"),
|
|
219
|
+
_get(native_order, "timestamp"),
|
|
220
|
+
_get(nautilus_order, "timestamp"),
|
|
221
|
+
)
|
|
222
|
+
if timestamp is not None and not pd.isna(timestamp):
|
|
223
|
+
timestamp = _coerce_ts(timestamp)
|
|
224
|
+
symbol = _first_non_null(
|
|
225
|
+
_get(native_order, "symbol"),
|
|
226
|
+
_get(nautilus_order, "symbol"),
|
|
227
|
+
_get(native_fill, "symbol"),
|
|
228
|
+
_get(nautilus_fill, "symbol"),
|
|
229
|
+
)
|
|
230
|
+
side = _first_non_null(
|
|
231
|
+
_get(native_order, "side"),
|
|
232
|
+
_get(nautilus_order, "side"),
|
|
233
|
+
_get(native_fill, "side"),
|
|
234
|
+
_get(nautilus_fill, "side"),
|
|
235
|
+
)
|
|
236
|
+
requested_qty = _first_float(_get(native_order, "qty"), _get(nautilus_order, "qty"))
|
|
237
|
+
requested_price = _first_float(_get(native_order, "price"), _get(nautilus_order, "price"))
|
|
238
|
+
native_price = _safe_float(_get(native_fill, "price"))
|
|
239
|
+
nautilus_price = _safe_float(_get(nautilus_fill, "price"))
|
|
240
|
+
native_fee = _safe_float(_get(native_fill, "fee"))
|
|
241
|
+
nautilus_fee = _safe_float(_get(nautilus_fill, "fee"))
|
|
242
|
+
native_equity = _equity_at(native_result, timestamp)
|
|
243
|
+
nautilus_equity = _equity_at(nautilus_result, timestamp)
|
|
244
|
+
native_pos = _position_at(native_result, symbol, timestamp)
|
|
245
|
+
nautilus_pos = _position_at(nautilus_result, symbol, timestamp)
|
|
246
|
+
|
|
247
|
+
rows.append(
|
|
248
|
+
{
|
|
249
|
+
"row": row,
|
|
250
|
+
"timestamp": timestamp,
|
|
251
|
+
"symbol": symbol,
|
|
252
|
+
"side": side,
|
|
253
|
+
"requested_qty": requested_qty,
|
|
254
|
+
"requested_price": requested_price,
|
|
255
|
+
"native_fill_price": native_price,
|
|
256
|
+
"nautilus_fill_price": nautilus_price,
|
|
257
|
+
"fill_price_diff": _diff(native_price, nautilus_price),
|
|
258
|
+
"native_fee": native_fee,
|
|
259
|
+
"nautilus_fee": nautilus_fee,
|
|
260
|
+
"fee_diff": _diff(native_fee, nautilus_fee),
|
|
261
|
+
"native_position_after": native_pos,
|
|
262
|
+
"nautilus_position_after": nautilus_pos,
|
|
263
|
+
"position_diff": _diff(native_pos, nautilus_pos),
|
|
264
|
+
"native_equity": native_equity,
|
|
265
|
+
"nautilus_equity": nautilus_equity,
|
|
266
|
+
"equity_diff": _diff(native_equity, nautilus_equity),
|
|
267
|
+
"native_status": _get(native_order, "status"),
|
|
268
|
+
"nautilus_status": _get(nautilus_order, "status"),
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
return pd.DataFrame(rows, columns=PARITY_COLUMNS)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _orders_from_result(result: BacktestResultV2) -> pd.DataFrame:
|
|
275
|
+
package_map = _report_frame(result.metadata.get("package_order_map"))
|
|
276
|
+
if not package_map.empty:
|
|
277
|
+
out = pd.DataFrame(
|
|
278
|
+
{
|
|
279
|
+
"timestamp": pd.to_datetime(package_map.get("timestamp"), utc=True, errors="coerce"),
|
|
280
|
+
"symbol": package_map.get("symbol", package_map.get("instrument_id")),
|
|
281
|
+
"side": package_map.get("side"),
|
|
282
|
+
"qty": pd.to_numeric(package_map.get("qty"), errors="coerce"),
|
|
283
|
+
"price": pd.to_numeric(package_map.get("price"), errors="coerce"),
|
|
284
|
+
"status": pd.Series([None] * len(package_map), dtype=object),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
orders_report = _report_frame(result.metadata.get("orders_report", result.metadata.get("order_report")))
|
|
288
|
+
if not orders_report.empty and "status" in orders_report:
|
|
289
|
+
out.loc[: len(orders_report) - 1, "status"] = list(orders_report["status"].head(len(out)))
|
|
290
|
+
return out
|
|
291
|
+
|
|
292
|
+
if result.orders:
|
|
293
|
+
rows = []
|
|
294
|
+
for order in result.orders:
|
|
295
|
+
rows.append(
|
|
296
|
+
{
|
|
297
|
+
"timestamp": _coerce_ts(order.timestamp),
|
|
298
|
+
"symbol": order.symbol,
|
|
299
|
+
"side": _enum_value(order.side),
|
|
300
|
+
"qty": float(order.qty),
|
|
301
|
+
"price": order.price,
|
|
302
|
+
"status": None,
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
order_report = _report_frame(result.metadata.get("order_report"))
|
|
306
|
+
if not order_report.empty and "status" in order_report:
|
|
307
|
+
for idx, status in enumerate(order_report["status"].head(len(rows))):
|
|
308
|
+
rows[idx]["status"] = status
|
|
309
|
+
return pd.DataFrame(rows)
|
|
310
|
+
return pd.DataFrame(columns=["timestamp", "symbol", "side", "qty", "price", "status"])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _fills_from_result(result: BacktestResultV2) -> pd.DataFrame:
|
|
314
|
+
fills_report = _report_frame(result.metadata.get("fills_report"))
|
|
315
|
+
if fills_report.empty:
|
|
316
|
+
fills_report = _report_frame(result.metadata.get("orders_report"))
|
|
317
|
+
if not fills_report.empty:
|
|
318
|
+
filled = fills_report
|
|
319
|
+
if "status" in filled:
|
|
320
|
+
filled = filled[filled["status"].astype(str).str.upper().eq("FILLED")]
|
|
321
|
+
return pd.DataFrame(
|
|
322
|
+
{
|
|
323
|
+
"timestamp": _timestamp_column(filled),
|
|
324
|
+
"symbol": filled.get("instrument_id", filled.get("symbol")),
|
|
325
|
+
"side": filled.get("side"),
|
|
326
|
+
"qty": pd.to_numeric(filled.get("filled_qty", filled.get("quantity")), errors="coerce"),
|
|
327
|
+
"price": pd.to_numeric(filled.get("avg_px", filled.get("price")), errors="coerce"),
|
|
328
|
+
"fee": filled.apply(_row_fee, axis=1),
|
|
329
|
+
}
|
|
330
|
+
).reset_index(drop=True)
|
|
331
|
+
|
|
332
|
+
if result.fills:
|
|
333
|
+
rows = []
|
|
334
|
+
for fill in result.fills:
|
|
335
|
+
rows.append(
|
|
336
|
+
{
|
|
337
|
+
"timestamp": _coerce_ts(fill.timestamp),
|
|
338
|
+
"symbol": fill.symbol,
|
|
339
|
+
"side": _enum_value(fill.side),
|
|
340
|
+
"qty": float(fill.qty),
|
|
341
|
+
"price": float(fill.price),
|
|
342
|
+
"fee": float(fill.fee),
|
|
343
|
+
}
|
|
344
|
+
)
|
|
345
|
+
return pd.DataFrame(rows)
|
|
346
|
+
return pd.DataFrame(columns=["timestamp", "symbol", "side", "qty", "price", "fee"])
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _report_frame(value: Any) -> pd.DataFrame:
|
|
350
|
+
if isinstance(value, pd.DataFrame):
|
|
351
|
+
return value.copy()
|
|
352
|
+
if value is None:
|
|
353
|
+
return pd.DataFrame()
|
|
354
|
+
try:
|
|
355
|
+
return pd.DataFrame(value).copy()
|
|
356
|
+
except Exception:
|
|
357
|
+
return pd.DataFrame()
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _status_count(frame: pd.DataFrame, status: str) -> int:
|
|
361
|
+
if frame.empty or "status" not in frame:
|
|
362
|
+
return 0
|
|
363
|
+
return int(frame["status"].astype(str).str.lower().eq(status).sum())
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _max_abs(frame: pd.DataFrame, column: str) -> float:
|
|
367
|
+
if column not in frame:
|
|
368
|
+
return 0.0
|
|
369
|
+
series = pd.to_numeric(frame[column], errors="coerce").abs().dropna()
|
|
370
|
+
return float(series.max()) if not series.empty else 0.0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _timestamp_column(frame: pd.DataFrame) -> pd.Series:
|
|
374
|
+
for key in ("ts_last", "ts_event", "timestamp", "ts_init"):
|
|
375
|
+
if key in frame:
|
|
376
|
+
return pd.to_datetime(frame[key], utc=True, errors="coerce")
|
|
377
|
+
return pd.Series(pd.NaT, index=frame.index)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _row_fee(row: pd.Series) -> float:
|
|
381
|
+
for key in ("fee", "fees", "commissions"):
|
|
382
|
+
if key in row and row[key] is not None:
|
|
383
|
+
return _money_float(row[key])
|
|
384
|
+
return 0.0
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _money_float(value: Any) -> float:
|
|
388
|
+
if isinstance(value, (int, float, np.number)):
|
|
389
|
+
return float(value)
|
|
390
|
+
text = str(value).replace(",", "").replace("[", "").replace("]", "").strip()
|
|
391
|
+
if not text or text.lower() == "nan":
|
|
392
|
+
return 0.0
|
|
393
|
+
try:
|
|
394
|
+
return float(text.split()[0])
|
|
395
|
+
except (TypeError, ValueError, IndexError):
|
|
396
|
+
return 0.0
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _equity_at(result: BacktestResultV2, timestamp: Any) -> float:
|
|
400
|
+
if timestamp is None or pd.isna(timestamp) or result.equity.empty:
|
|
401
|
+
return float("nan")
|
|
402
|
+
ts = _coerce_ts(timestamp)
|
|
403
|
+
series = result.equity.sort_index()
|
|
404
|
+
loc = series.index.searchsorted(ts, side="right") - 1
|
|
405
|
+
if loc < 0:
|
|
406
|
+
return float("nan")
|
|
407
|
+
return float(series.iloc[loc])
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _position_at(result: BacktestResultV2, symbol: Any, timestamp: Any) -> float:
|
|
411
|
+
if symbol is None or timestamp is None or pd.isna(timestamp) or result.positions.empty:
|
|
412
|
+
return float("nan")
|
|
413
|
+
col = _position_column(result.positions, str(symbol))
|
|
414
|
+
if col is None:
|
|
415
|
+
return float("nan")
|
|
416
|
+
ts = _coerce_ts(timestamp)
|
|
417
|
+
frame = result.positions.sort_index()
|
|
418
|
+
loc = frame.index.searchsorted(ts, side="right") - 1
|
|
419
|
+
if loc < 0:
|
|
420
|
+
return float("nan")
|
|
421
|
+
return _safe_float(frame[col].iloc[loc])
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _position_column(positions: pd.DataFrame, symbol: str) -> Optional[str]:
|
|
425
|
+
candidates = [f"Position_{symbol}", symbol]
|
|
426
|
+
if symbol.endswith("-PERP.BINANCE"):
|
|
427
|
+
candidates.append(f"Position_{symbol.removesuffix('-PERP.BINANCE')}")
|
|
428
|
+
for col in candidates:
|
|
429
|
+
if col in positions.columns:
|
|
430
|
+
return col
|
|
431
|
+
suffix = symbol.split(".")[0]
|
|
432
|
+
for col in positions.columns:
|
|
433
|
+
if str(col).endswith(symbol) or str(col).endswith(suffix):
|
|
434
|
+
return col
|
|
435
|
+
return None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _coerce_ts(value: Any) -> pd.Timestamp:
|
|
439
|
+
ts = pd.Timestamp(value)
|
|
440
|
+
if ts.tz is None:
|
|
441
|
+
return ts.tz_localize("UTC")
|
|
442
|
+
return ts.tz_convert("UTC")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _safe_float(value: Any) -> float:
|
|
446
|
+
try:
|
|
447
|
+
if value is None or pd.isna(value):
|
|
448
|
+
return float("nan")
|
|
449
|
+
return float(value)
|
|
450
|
+
except (TypeError, ValueError):
|
|
451
|
+
return _money_float(value)
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _first_float(*values: Any) -> float:
|
|
455
|
+
for value in values:
|
|
456
|
+
number = _safe_float(value)
|
|
457
|
+
if not np.isnan(number):
|
|
458
|
+
return number
|
|
459
|
+
return float("nan")
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _diff(left: float, right: float) -> float:
|
|
463
|
+
if np.isnan(left) or np.isnan(right):
|
|
464
|
+
return float("nan")
|
|
465
|
+
return float(right - left)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _row(frame: pd.DataFrame, idx: int) -> Optional[pd.Series]:
|
|
469
|
+
if frame is None or frame.empty or idx >= len(frame):
|
|
470
|
+
return None
|
|
471
|
+
return frame.iloc[idx]
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _get(row: Optional[pd.Series], key: str) -> Any:
|
|
475
|
+
if row is None or key not in row:
|
|
476
|
+
return None
|
|
477
|
+
return row[key]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _first_non_null(*values: Any) -> Any:
|
|
481
|
+
for value in values:
|
|
482
|
+
if value is not None and not (isinstance(value, float) and np.isnan(value)) and not pd.isna(value):
|
|
483
|
+
if key := _maybe_enum_value(value):
|
|
484
|
+
return key
|
|
485
|
+
return value
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def _maybe_enum_value(value: Any) -> Any:
|
|
490
|
+
if hasattr(value, "value"):
|
|
491
|
+
return value.value
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _enum_value(value: Any) -> Any:
|
|
496
|
+
return value.value if hasattr(value, "value") else value
|