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,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