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
quantbt/backtester.py ADDED
@@ -0,0 +1,485 @@
1
+ """
2
+ quantbt.backtester
3
+ ------------------
4
+ BacktestEngine — single and multi-symbol futures backtest.
5
+
6
+ Key design decisions
7
+ ~~~~~~~~~~~~~~~~~~~~
8
+ * Signals enter as raw weights. Position scaling (units / notional / pct_equity)
9
+ is handled by quantbt.sizing.modes BEFORE passing to the numba kernel.
10
+ * BacktestEngine.__init__ does data alignment + scaling.
11
+ * BacktestEngine.run() executes the simulation and returns a BacktestResult.
12
+ * analyze() is the convenience entry point: prints a text report + quick_plot.
13
+ * tearsheet() is a separate opt-in call.
14
+
15
+ hedge_type values
16
+ ~~~~~~~~~~~~~~~~~
17
+ 'notional' constant notional per bar (recomputes units every bar)
18
+ 'unit' fixed unit count from first-bar price
19
+ 'signal_notional' anchor on signal change; stable between transitions ← recommended
20
+ '%_equity' dynamic sizing from live equity, no pre-scaling
21
+ 'dca_ladder' signed structural level; High/Low limit fills at grid triggers
22
+
23
+ Parameters (unchanged from original)
24
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
25
+ Datetime pd.Series | pd.DatetimeIndex
26
+ Position pd.Series | Dict[str, pd.Series] raw signal weight
27
+ Close pd.Series | Dict[str, pd.Series]
28
+ High / Low optional, used for intrabar liquidation
29
+ fee float round-trip fee (split internally to one-way)
30
+ use_pyramiding bool False → snap signal to {-1, 0, 1}
31
+ initial_capital float
32
+ leverage float
33
+ maintenance_ratio float Binance-style: notional × ratio
34
+ contract_size float | Dict[str, float]
35
+ use_funding_rate bool
36
+ funding_rate float | pd.Series | Dict[str, float | pd.Series]
37
+ alloc_per_trade float | Dict[str, float] notional per full signal unit
38
+ hedge_type str
39
+ slippage float e.g. 0.0001 = 1 bps
40
+ symbols List[str] | None
41
+ High / Low required for dca_ladder limit-fill detection
42
+ dca_base_notional base order notional; defaults to alloc_per_trade
43
+ dca_safety_notional safety order notional; defaults to alloc_per_trade
44
+ dca_step_pct AO1 distance from base entry, e.g. 0.01 = 1%
45
+ dca_step_scale multiplier for each next AO distance increment
46
+ dca_volume_scale multiplier for each next safety order notional
47
+ dca_take_profit_pct TP from weighted average entry; 0 disables internal TP
48
+ """
49
+
50
+ from __future__ import annotations
51
+
52
+ from typing import Dict, List, Optional, Tuple, Union
53
+
54
+ import numpy as np
55
+ import pandas as pd
56
+
57
+ from .core.engine import _engine_units, _engine_pct_equity, _engine_dca_ladder
58
+ from .core.types import BacktestResult
59
+ from .core.preprocessor import (
60
+ validate_datetime,
61
+ align_series,
62
+ prepare_funding,
63
+ build_arrays,
64
+ )
65
+ from .core.constraints import build_quantity_constraints, quantize_target_units_matrix
66
+ from .core.schema import InstrumentSpec
67
+ from .sizing.modes import compute_target_units
68
+ from .metrics.performance import full_report
69
+
70
+
71
+ class BacktestEngine:
72
+ """
73
+ Vectorised Binance-Futures backtest engine.
74
+
75
+ Usage
76
+ -----
77
+ >>> bt = BacktestEngine(Datetime=dt, Position=sig, Close=close, ...)
78
+ >>> result = bt.run() # BacktestResult
79
+ >>> bt.analyze() # text report + quick plot
80
+ >>> bt.tearsheet() # full dashboard (optional)
81
+ >>> bt.export_trade_log('log.csv')
82
+ """
83
+
84
+ def __init__(
85
+ self,
86
+ Datetime: Union[pd.Series, pd.DatetimeIndex],
87
+ Position: Union[pd.Series, Dict[str, pd.Series]],
88
+ Close: Union[pd.Series, Dict[str, pd.Series]],
89
+ fee: float = 0.0004,
90
+ use_pyramiding: bool = True,
91
+ initial_capital: float = 20_000.0,
92
+ leverage: float = 10.0,
93
+ maintenance_ratio: float = 0.005,
94
+ contract_size: Union[float, Dict[str, float]] = 1.0,
95
+ use_funding_rate: bool = True,
96
+ funding_rate: Union[float, pd.Series, Dict] = 0.0001,
97
+ alloc_per_trade: Union[float, Dict[str, float]] = 100_000.0,
98
+ hedge_type: str = "signal_notional",
99
+ slippage: float = 0.0001,
100
+ symbols: Optional[List[str]] = None,
101
+ High: Optional[Union[pd.Series, Dict[str, pd.Series]]] = None,
102
+ Low: Optional[Union[pd.Series, Dict[str, pd.Series]]] = None,
103
+ dca_base_notional: Optional[Union[float, Dict[str, float]]] = None,
104
+ dca_safety_notional: Optional[Union[float, Dict[str, float]]] = None,
105
+ dca_step_pct: Union[float, Dict[str, float]] = 0.01,
106
+ dca_step_scale: Union[float, Dict[str, float]] = 1.0,
107
+ dca_volume_scale: Union[float, Dict[str, float]] = 1.0,
108
+ dca_max_safety_orders: int = 5,
109
+ dca_take_profit_pct: Union[float, Dict[str, float]] = 0.0,
110
+ dca_allow_same_bar_exit: bool = False,
111
+ instruments: Optional[Union[Dict[str, InstrumentSpec], List[InstrumentSpec]]] = None,
112
+ qty_step: Optional[Union[float, Dict[str, float]]] = None,
113
+ lot_size: Optional[Union[float, Dict[str, float]]] = None,
114
+ slot_size: Optional[Union[float, Dict[str, float]]] = None,
115
+ min_qty: Optional[Union[float, Dict[str, float]]] = None,
116
+ min_notional: Optional[Union[float, Dict[str, float]]] = None,
117
+ # kept for backward compat, not used internally
118
+ run_portfolio: bool = True,
119
+ use_binance_netting: bool = True,
120
+ margin_buffer: float = 0.01,
121
+ ):
122
+ # ── store config ──────────────────────────────────────────────────
123
+ self.fee_oneway = fee / 2.0 # one-way
124
+ self.use_pyramiding = use_pyramiding
125
+ self.initial_capital = initial_capital
126
+ self.leverage = leverage
127
+ self.maintenance_ratio = maintenance_ratio
128
+ self.use_funding_rate = use_funding_rate
129
+ self.alloc_per_trade = alloc_per_trade
130
+ self.hedge_type = hedge_type
131
+ self._hedge_type_norm = hedge_type.lower().strip()
132
+ self._is_dca_ladder = self._hedge_type_norm in ("dca_ladder", "dca")
133
+ self.slippage = slippage
134
+ self.dca_max_safety_orders = int(dca_max_safety_orders)
135
+ self.dca_allow_same_bar_exit = bool(dca_allow_same_bar_exit)
136
+
137
+ if self.dca_max_safety_orders < 0:
138
+ raise ValueError("dca_max_safety_orders must be >= 0")
139
+ if self._is_dca_ladder and (High is None or Low is None):
140
+ raise ValueError("hedge_type='dca_ladder' requires High and Low for limit-fill detection")
141
+ if self.initial_capital <= 0.0:
142
+ raise ValueError("initial_capital must be > 0")
143
+ if self.leverage <= 0.0:
144
+ raise ValueError("leverage must be > 0")
145
+ if self.maintenance_ratio < 0.0:
146
+ raise ValueError("maintenance_ratio must be >= 0")
147
+
148
+ # ── datetime index ────────────────────────────────────────────────
149
+ self._idx = validate_datetime(Datetime)
150
+
151
+ # ── symbols ───────────────────────────────────────────────────────
152
+ if symbols is not None:
153
+ self.symbols = symbols
154
+ elif isinstance(Position, dict):
155
+ self.symbols = list(Position.keys())
156
+ else:
157
+ self.symbols = ["DEFAULT"]
158
+
159
+ self.n_syms = len(self.symbols)
160
+ self.n_bars = len(self._idx)
161
+
162
+ # ── align price / signal data ──────────────────────────────────────
163
+ self._closes = align_series(Close, self.symbols, self._idx)
164
+ self._highs = align_series(High, self.symbols, self._idx, fallback=self._closes)
165
+ self._lows = align_series(Low, self.symbols, self._idx, fallback=self._closes)
166
+ self._positions = align_series(Position, self.symbols, self._idx, fill_val=0.0)
167
+
168
+ # ── contract sizes ────────────────────────────────────────────────
169
+ if isinstance(contract_size, dict):
170
+ self._contract_sizes = np.array(
171
+ [contract_size.get(s, 1.0) for s in self.symbols], dtype=np.float64
172
+ )
173
+ else:
174
+ self._contract_sizes = np.full(self.n_syms, float(contract_size), dtype=np.float64)
175
+
176
+ if np.any(self._contract_sizes <= 0.0):
177
+ raise ValueError("contract_size must be > 0")
178
+
179
+ self._quantity_constraints = build_quantity_constraints(
180
+ self.symbols,
181
+ instruments=instruments,
182
+ qty_step=qty_step,
183
+ lot_size=lot_size,
184
+ slot_size=slot_size,
185
+ min_qty=min_qty,
186
+ min_notional=min_notional,
187
+ )
188
+
189
+ # ── funding rates ─────────────────────────────────────────────────
190
+ fr_input = funding_rate if use_funding_rate else 0.0
191
+ self._funding = prepare_funding(fr_input, self.symbols, self._idx)
192
+
193
+ # ── alloc dict ────────────────────────────────────────────────────
194
+ if isinstance(alloc_per_trade, dict):
195
+ self._alloc = {s: float(alloc_per_trade[s]) for s in self.symbols}
196
+ else:
197
+ self._alloc = {s: float(alloc_per_trade) for s in self.symbols}
198
+
199
+ if any(v < 0.0 for v in self._alloc.values()):
200
+ raise ValueError("alloc_per_trade must be >= 0")
201
+
202
+ def _per_symbol_array(value, default_map):
203
+ out = []
204
+ for s in self.symbols:
205
+ default = default_map[s] if isinstance(default_map, dict) else default_map
206
+ if value is None:
207
+ v = default
208
+ elif isinstance(value, dict):
209
+ v = value.get(s, default)
210
+ else:
211
+ v = value
212
+ out.append(float(v))
213
+ return np.array(out, dtype=np.float64)
214
+
215
+ # DCA ladder parameters. Defaults keep base and safety orders aligned
216
+ # with alloc_per_trade, while the grid geometry is explicit and stable.
217
+ self._dca_base_notional = _per_symbol_array(dca_base_notional, self._alloc)
218
+ self._dca_safety_notional = _per_symbol_array(dca_safety_notional, self._alloc)
219
+ self._dca_step_pct = _per_symbol_array(dca_step_pct, 0.01)
220
+ self._dca_step_scale = _per_symbol_array(dca_step_scale, 1.0)
221
+ self._dca_volume_scale = _per_symbol_array(dca_volume_scale, 1.0)
222
+ self._dca_take_profit_pct = _per_symbol_array(dca_take_profit_pct, 0.0)
223
+
224
+ if self._is_dca_ladder:
225
+ if self.dca_max_safety_orders > 0 and np.any(self._dca_step_pct <= 0.0):
226
+ raise ValueError("dca_step_pct must be > 0 when safety orders are enabled")
227
+ if np.any(self._dca_base_notional <= 0.0) or np.any(self._dca_safety_notional <= 0.0):
228
+ raise ValueError("DCA base/safety notionals must be > 0")
229
+ if np.any(self._dca_step_scale <= 0.0) or np.any(self._dca_volume_scale <= 0.0):
230
+ raise ValueError("DCA step/volume scales must be > 0")
231
+
232
+ # ── scale signals → target units ──────────────────────────────────
233
+ self._target_units: Dict[str, pd.Series] = {}
234
+ for sym in self.symbols:
235
+ if self._is_dca_ladder:
236
+ self._target_units[sym] = self._positions[sym].fillna(0.0)
237
+ else:
238
+ self._target_units[sym] = compute_target_units(
239
+ hedge_type = self.hedge_type,
240
+ signal = self._positions[sym],
241
+ close = self._closes[sym],
242
+ alloc = self._alloc[sym],
243
+ use_pyramiding = self.use_pyramiding,
244
+ )
245
+
246
+ # run on construction so result is immediately available
247
+ self._result: Optional[BacktestResult] = None
248
+ self.run()
249
+
250
+ # ── public interface ─────────────────────────────────────────────────────
251
+
252
+ def run(self) -> BacktestResult:
253
+ """
254
+ Execute the simulation and return a BacktestResult.
255
+ Also caches the result as self.result.
256
+ """
257
+ closes, highs, lows, signals, funding, is_funding = build_arrays(
258
+ symbols = self.symbols,
259
+ idx = self._idx,
260
+ closes_dict = self._closes,
261
+ highs_dict = self._highs,
262
+ lows_dict = self._lows,
263
+ signals_dict = self._target_units,
264
+ funding_dict = self._funding,
265
+ )
266
+
267
+ cs = self._contract_sizes
268
+ qc = self._quantity_constraints
269
+
270
+ if self._is_dca_ladder:
271
+ equity_arr, pos_arr, level_arr, liq_flag, liq_idx = _engine_dca_ladder(
272
+ n_bars = self.n_bars,
273
+ n_syms = self.n_syms,
274
+ highs = highs,
275
+ lows = lows,
276
+ closes = closes,
277
+ signals = signals,
278
+ funding_rates = funding,
279
+ is_funding_bar = is_funding,
280
+ init_capital = self.initial_capital,
281
+ leverage = self.leverage,
282
+ maint_ratio = self.maintenance_ratio,
283
+ fee_rate = self.fee_oneway,
284
+ contract_sizes = cs,
285
+ market_slippage = self.slippage,
286
+ base_notional = self._dca_base_notional,
287
+ safety_notional = self._dca_safety_notional,
288
+ step_pct = self._dca_step_pct,
289
+ step_scale = self._dca_step_scale,
290
+ volume_scale = self._dca_volume_scale,
291
+ max_safety_orders = self.dca_max_safety_orders,
292
+ take_profit_pct = self._dca_take_profit_pct,
293
+ allow_same_bar_exit = self.dca_allow_same_bar_exit,
294
+ qty_steps = qc.qty_step,
295
+ min_qtys = qc.min_qty,
296
+ min_notionals = qc.min_notional,
297
+ )
298
+ result_positions = pos_arr
299
+ elif self._hedge_type_norm in ("%_equity", "pct_equity"):
300
+ alloc_pct = np.array(
301
+ [self._alloc[s] for s in self.symbols], dtype=np.float64
302
+ )
303
+ # normalise: if value > 1 assume percentage was passed (e.g. 10 → 0.10)
304
+ alloc_pct = np.where(alloc_pct > 1.0, alloc_pct / 100.0, alloc_pct)
305
+
306
+ equity_arr, liq_flag, liq_idx = _engine_pct_equity(
307
+ n_bars = self.n_bars,
308
+ n_syms = self.n_syms,
309
+ highs = highs,
310
+ lows = lows,
311
+ closes = closes,
312
+ signals = signals,
313
+ funding_rates = funding,
314
+ is_funding_bar = is_funding,
315
+ init_capital = self.initial_capital,
316
+ leverage = self.leverage,
317
+ maint_ratio = self.maintenance_ratio,
318
+ fee_rate = self.fee_oneway,
319
+ contract_sizes = cs,
320
+ slippage = self.slippage,
321
+ alloc_pct = alloc_pct,
322
+ qty_steps = qc.qty_step,
323
+ min_qtys = qc.min_qty,
324
+ min_notionals = qc.min_notional,
325
+ )
326
+ result_positions = signals
327
+ else:
328
+ signals = quantize_target_units_matrix(signals, closes, cs, qc)
329
+ equity_arr, liq_flag, liq_idx = _engine_units(
330
+ n_bars = self.n_bars,
331
+ n_syms = self.n_syms,
332
+ highs = highs,
333
+ lows = lows,
334
+ closes = closes,
335
+ signals = signals,
336
+ funding_rates = funding,
337
+ is_funding_bar = is_funding,
338
+ init_capital = self.initial_capital,
339
+ leverage = self.leverage,
340
+ maint_ratio = self.maintenance_ratio,
341
+ fee_rate = self.fee_oneway,
342
+ contract_sizes = cs,
343
+ slippage = self.slippage,
344
+ )
345
+ result_positions = signals
346
+
347
+ equity = pd.Series(equity_arr, index=self._idx, name="equity")
348
+ returns = equity.pct_change().fillna(0)
349
+
350
+ # positions DataFrame
351
+ pos_df = pd.DataFrame(
352
+ {f"Position_{s}": result_positions[:, i] for i, s in enumerate(self.symbols)},
353
+ index=self._idx,
354
+ )
355
+ close_df = pd.DataFrame(
356
+ {f"Close_{s}": closes[:, i] for i, s in enumerate(self.symbols)},
357
+ index=self._idx,
358
+ )
359
+
360
+ self._result = BacktestResult(
361
+ equity = equity,
362
+ returns = returns,
363
+ positions = pos_df,
364
+ closes = close_df,
365
+ symbols = self.symbols,
366
+ initial_capital = self.initial_capital,
367
+ leverage = self.leverage,
368
+ liquidated = bool(liq_flag),
369
+ liquidation_bar = int(liq_idx),
370
+ metadata = {
371
+ "hedge_type": self.hedge_type,
372
+ "initial_buying_power": self.initial_capital * self.leverage,
373
+ "fee_oneway": self.fee_oneway,
374
+ "slippage": self.slippage,
375
+ "maintenance_ratio": self.maintenance_ratio,
376
+ "quantity_constraints": self._quantity_constraints.as_dict(),
377
+ "dca_actual_level": (
378
+ pd.DataFrame(
379
+ {f"Level_{s}": level_arr[:, i] for i, s in enumerate(self.symbols)},
380
+ index=self._idx,
381
+ )
382
+ if self._is_dca_ladder else None
383
+ ),
384
+ },
385
+ )
386
+ return self._result
387
+
388
+ @property
389
+ def result(self) -> BacktestResult:
390
+ if self._result is None:
391
+ self.run()
392
+ return self._result
393
+
394
+ # ── convenience methods ───────────────────────────────────────────────────
395
+
396
+ def analyze(
397
+ self,
398
+ trading_days: int = 365,
399
+ theme: str = "dark",
400
+ figsize: tuple = (14, 6),
401
+ ) -> None:
402
+ """
403
+ Print a concise performance report, then show cumulative return + drawdown.
404
+ """
405
+ from .viz.plots import quick_plot
406
+
407
+ self.print_metrics(trading_days=trading_days)
408
+ quick_plot(self.result, theme=theme, figsize=figsize)
409
+
410
+ def print_metrics(self, trading_days: int = 365) -> None:
411
+ """
412
+ Print a structured text report to stdout.
413
+ No separators, no banner lines — clean columnar output.
414
+ """
415
+ rpt = full_report(self.result, trading_days)
416
+ syms = ", ".join(self.symbols)
417
+
418
+ lines = [
419
+ ("Symbols", syms),
420
+ ("Hedge Type", self.hedge_type),
421
+ ("Initial Capital", f"${rpt['initial_capital']:>14,.0f}"),
422
+ ("Final Equity", f"${rpt['final_equity']:>14,.2f}"),
423
+ ("Total Return", f"{rpt['total_return_pct']:>+13.2f}%"),
424
+ ("CAGR", f"{rpt['cagr_pct']:>+13.2f}%"),
425
+ ("Sharpe Ratio", f"{rpt['sharpe']:>14.3f}"),
426
+ ("Sortino Ratio", f"{rpt['sortino']:>14.3f}"),
427
+ ("Calmar Ratio", f"{rpt['calmar']:>14.3f}"),
428
+ ("Omega Ratio", f"{rpt['omega']:>14.3f}"),
429
+ ("Max Drawdown", f"{rpt['max_drawdown_pct']:>13.2f}%"),
430
+ ("Avg Drawdown", f"{rpt['avg_drawdown_pct']:>13.2f}%"),
431
+ ("Max DD Duration", f"{rpt['max_dd_duration_days']:>11d} days"),
432
+ ("Profit Factor", f"{rpt['profit_factor']:>14.3f}"),
433
+ ("Long Hit Rate", f"{rpt['long_hitrate_pct']:>13.2f}%"),
434
+ ("Short Hit Rate", f"{rpt['short_hitrate_pct']:>13.2f}%"),
435
+ ("Avg Win", f"{rpt['avg_win_pct']:>+13.3f}%"),
436
+ ("Avg Loss", f"{rpt['avg_loss_pct']:>+13.3f}%"),
437
+ ("Expectancy", f"{rpt['expectancy_pct']:>+13.3f}%"),
438
+ ("Number of Trades", f"{rpt['num_trades']:>14,d}"),
439
+ ("Liquidated", f"{'Yes' if rpt['liquidated'] else 'No':>14}"),
440
+ ]
441
+
442
+ col_width = max(len(k) for k, _ in lines)
443
+ print()
444
+ for key, val in lines:
445
+ print(f" {key:<{col_width}} {val}")
446
+ print()
447
+
448
+ def tearsheet(
449
+ self,
450
+ theme: str = "light",
451
+ figsize: tuple = (18, 24),
452
+ trading_days: int = 365,
453
+ benchmark: Optional[pd.Series] = None,
454
+ ) -> None:
455
+ """Full dashboard. Optional; call explicitly when needed."""
456
+ from .viz.plots import tearsheet as _tearsheet
457
+
458
+ _tearsheet(
459
+ self.result,
460
+ theme = theme,
461
+ figsize = figsize,
462
+ trading_days = trading_days,
463
+ benchmark = benchmark,
464
+ )
465
+
466
+ def export_trade_log(
467
+ self,
468
+ filename: str = "trade_log.csv",
469
+ datetime_as_index: bool = True,
470
+ ) -> None:
471
+ r = self.result
472
+ log = pd.DataFrame({
473
+ "returns": r.returns,
474
+ "cumulative_return": (r.equity / self.initial_capital - 1) * 100,
475
+ }, index=r.equity.index)
476
+
477
+ for sym in self.symbols:
478
+ log[f"position_{sym}"] = r.positions[f"Position_{sym}"]
479
+ log[f"close_{sym}"] = r.closes[f"Close_{sym}"]
480
+
481
+ if not datetime_as_index:
482
+ log = log.reset_index()
483
+
484
+ log.to_csv(filename, index=datetime_as_index)
485
+ print(f"Trade log exported → {filename}")
@@ -0,0 +1 @@
1
+ """Benchmark helpers for quantbt upgrade phases."""