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
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."""
|