quantex 0.4.8__tar.gz → 0.5.0__tar.gz
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.
- {quantex-0.4.8 → quantex-0.5.0}/PKG-INFO +1 -1
- {quantex-0.4.8 → quantex-0.5.0}/pyproject.toml +1 -1
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/__init__.py +13 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/backtester.py +7 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/metrics.py +27 -26
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/reports.py +8 -2
- quantex-0.5.0/src/quantex/backtester/walk_forward.py +787 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/broker/broker.py +288 -137
- {quantex-0.4.8 → quantex-0.5.0}/LICENSE.md +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/README.md +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/__init__.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/constants.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/data_splits.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/montecarlo.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/backtester/parallel.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/broker/__init__.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/broker/types.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/datasource.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/helpers.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/indicators.py +0 -0
- {quantex-0.4.8 → quantex-0.5.0}/src/quantex/strategy.py +0 -0
|
@@ -18,6 +18,14 @@ from .parallel import _worker_init, _worker_eval
|
|
|
18
18
|
# Monte Carlo simulation
|
|
19
19
|
from .montecarlo import MonteCarloResult, MonteCarloMode
|
|
20
20
|
|
|
21
|
+
# Walk-forward optimization
|
|
22
|
+
from .walk_forward import (
|
|
23
|
+
WalkForwardResult,
|
|
24
|
+
WalkForwardWindow,
|
|
25
|
+
WalkForwardAnalyzer,
|
|
26
|
+
walk_forward_analyze,
|
|
27
|
+
)
|
|
28
|
+
|
|
21
29
|
__all__ = [
|
|
22
30
|
# Constants
|
|
23
31
|
"DataSplitMode",
|
|
@@ -42,4 +50,9 @@ __all__ = [
|
|
|
42
50
|
# Monte Carlo
|
|
43
51
|
"MonteCarloResult",
|
|
44
52
|
"MonteCarloMode",
|
|
53
|
+
# Walk-forward optimization
|
|
54
|
+
"WalkForwardResult",
|
|
55
|
+
"WalkForwardWindow",
|
|
56
|
+
"WalkForwardAnalyzer",
|
|
57
|
+
"walk_forward_analyze",
|
|
45
58
|
]
|
|
@@ -604,6 +604,7 @@ class SimpleBacktester:
|
|
|
604
604
|
random_seed: int | None = None,
|
|
605
605
|
workers: int | None = None,
|
|
606
606
|
progress_bar: bool = True,
|
|
607
|
+
verbose: bool = False,
|
|
607
608
|
) -> OptimizationResult:
|
|
608
609
|
"""
|
|
609
610
|
Optimize strategy parameters using Optuna (Bayesian optimization).
|
|
@@ -639,6 +640,8 @@ class SimpleBacktester:
|
|
|
639
640
|
workers (int | None, optional): Number of parallel workers for Optuna
|
|
640
641
|
study. Defaults to None (sequential).
|
|
641
642
|
progress_bar (bool, optional): Whether to show progress bar. Defaults to True.
|
|
643
|
+
verbose (bool, optional): Whether to show Optuna trial logs. Defaults to False
|
|
644
|
+
(suppresses verbose output).
|
|
642
645
|
|
|
643
646
|
Returns:
|
|
644
647
|
OptimizationResult: Object containing:
|
|
@@ -672,6 +675,10 @@ class SimpleBacktester:
|
|
|
672
675
|
"""
|
|
673
676
|
try:
|
|
674
677
|
import optuna
|
|
678
|
+
# Control optuna verbosity - logs every trial by default
|
|
679
|
+
optuna.logging.set_verbosity(
|
|
680
|
+
optuna.logging.INFO if verbose else optuna.logging.WARNING
|
|
681
|
+
)
|
|
675
682
|
except ImportError:
|
|
676
683
|
raise ImportError(
|
|
677
684
|
"optuna is required for optimize_optuna. "
|
|
@@ -2,6 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
4
|
import numpy as np
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from statistics import median
|
|
5
7
|
from typing import TYPE_CHECKING, Any
|
|
6
8
|
|
|
7
9
|
if TYPE_CHECKING:
|
|
@@ -31,43 +33,37 @@ def max_drawdown(equity: pd.Series) -> float:
|
|
|
31
33
|
max_dd = drawdown.min()
|
|
32
34
|
return float(abs(max_dd)) # return as positive percentage
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
def _infer_periods_per_year(
|
|
37
|
+
index: pd.Index,
|
|
38
|
+
default: int = 252 * 24 * 60,
|
|
39
|
+
) -> int:
|
|
36
40
|
"""
|
|
37
41
|
Infer the number of trading periods per year from a datetime index.
|
|
38
|
-
|
|
39
|
-
This function analyzes the time differences in the index to determine
|
|
40
|
-
the appropriate number of periods per year for annualized calculations.
|
|
42
|
+
|
|
41
43
|
Falls back to minute-level trading (252 trading days * 24 hours * 60 minutes)
|
|
42
44
|
if the index cannot be analyzed or contains insufficient data.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
index (pd.Index): DatetimeIndex containing timestamps.
|
|
46
|
-
default (int, optional): Default periods per year for minute trading.
|
|
47
|
-
Defaults to 252 * 24 * 60 (minute-level data).
|
|
48
|
-
|
|
49
|
-
Returns:
|
|
50
|
-
int: Estimated number of trading periods per year.
|
|
51
|
-
|
|
52
|
-
Example:
|
|
53
|
-
>>> dates = pd.date_range('2020-01-01', periods=100, freq='D')
|
|
54
|
-
>>> _infer_periods_per_year(dates)
|
|
55
|
-
252 # Daily trading periods
|
|
56
45
|
"""
|
|
57
|
-
# Simple inference; falls back to minute trading year if uncertain
|
|
58
46
|
if not isinstance(index, pd.DatetimeIndex) or len(index) < 3:
|
|
59
47
|
return default
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
|
|
49
|
+
times = index.to_pydatetime()
|
|
50
|
+
|
|
51
|
+
deltas = []
|
|
52
|
+
for prev, curr in zip(times[:-1], times[1:]):
|
|
53
|
+
delta = curr - prev
|
|
54
|
+
if isinstance(delta, timedelta) and delta.total_seconds() > 0:
|
|
55
|
+
deltas.append(delta.total_seconds())
|
|
56
|
+
|
|
57
|
+
if not deltas:
|
|
62
58
|
return default
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
|
|
60
|
+
med_sec = median(deltas)
|
|
61
|
+
if med_sec <= 0:
|
|
65
62
|
return default
|
|
63
|
+
|
|
66
64
|
periods_per_day = 86400.0 / med_sec
|
|
67
|
-
# Assume 252 trading days/year
|
|
68
65
|
return int(round(252 * periods_per_day))
|
|
69
66
|
|
|
70
|
-
|
|
71
67
|
def _compute_backtest_metrics(report: BacktestReport) -> dict[str, Any]:
|
|
72
68
|
"""Compute performance metrics from a BacktestReport."""
|
|
73
69
|
equity = report.PnlRecord.astype(float)
|
|
@@ -76,7 +72,12 @@ def _compute_backtest_metrics(report: BacktestReport) -> dict[str, Any]:
|
|
|
76
72
|
annual_rf = report.annual_rf
|
|
77
73
|
rf_per_period = annual_rf / report.periods_per_year
|
|
78
74
|
|
|
79
|
-
|
|
75
|
+
# If margin calls occurred, the backtest is invalid - position was forcibly closed
|
|
76
|
+
# This means the strategy was using too much leverage and the Sharpe is meaningless
|
|
77
|
+
margin_calls = len(report.margin_call_events or [])
|
|
78
|
+
if margin_calls > 0:
|
|
79
|
+
sharpe = float("nan")
|
|
80
|
+
elif len(returns) < 2 or returns.std(ddof=1) == 0:
|
|
80
81
|
sharpe = float("nan")
|
|
81
82
|
else:
|
|
82
83
|
excess = returns - rf_per_period
|
|
@@ -178,7 +178,14 @@ class BacktestReport:
|
|
|
178
178
|
# Risk-free per period from an annual rate
|
|
179
179
|
rf_per_period = self.annual_rf / self.periods_per_year
|
|
180
180
|
|
|
181
|
-
|
|
181
|
+
# If margin calls occurred, the backtest is invalid - position was forcibly closed
|
|
182
|
+
# This means the strategy was using too much leverage and the Sharpe is meaningless
|
|
183
|
+
margin_calls = len(self.margin_call_events or [])
|
|
184
|
+
if margin_calls > 0:
|
|
185
|
+
sharpe = np.nan
|
|
186
|
+
lo = np.nan
|
|
187
|
+
hi = np.nan
|
|
188
|
+
elif len(returns) < 2 or returns.std(ddof=1) == 0:
|
|
182
189
|
sharpe = np.nan
|
|
183
190
|
lo = np.nan
|
|
184
191
|
hi = np.nan
|
|
@@ -203,7 +210,6 @@ class BacktestReport:
|
|
|
203
210
|
tot_return = float(equity.iloc[-1] / equity.iloc[0] - 1.0)
|
|
204
211
|
annualized_return = float((1.0 + tot_return) ** (self.periods_per_year / max(len(returns), 1)) - 1.0)
|
|
205
212
|
tot_orders = len(self.orders)
|
|
206
|
-
margin_calls = len(self.margin_call_events or [])
|
|
207
213
|
|
|
208
214
|
return (
|
|
209
215
|
f"Starting Cash: ${self.starting_cash:,.2f}\n"
|