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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantex
3
- Version: 0.4.8
3
+ Version: 0.5.0
4
4
  Summary: A simple quant strategy creation and backtesting package.
5
5
  License: MIT
6
6
  Author: Daniel Green
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "quantex"
3
- version = "0.4.8"
3
+ version = "0.5.0"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -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
- def _infer_periods_per_year(index: pd.Index, default: int = 252 * 24 * 60) -> int:
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
- dt = np.diff(index.values).astype("timedelta64[s]").astype(float)
61
- if not np.isfinite(dt).any():
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
- med_sec = np.median(dt[dt > 0])
64
- if not np.isfinite(med_sec) or med_sec <= 0:
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
- if len(returns) < 2 or returns.std(ddof=1) == 0:
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
- if len(returns) < 2 or returns.std(ddof=1) == 0:
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"