quantex 0.2.3__tar.gz → 0.2.4__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.2.3 → quantex-0.2.4}/PKG-INFO +1 -1
- {quantex-0.2.3 → quantex-0.2.4}/pyproject.toml +1 -1
- quantex-0.2.4/src/quantex/__init__.py +82 -0
- {quantex-0.2.3 → quantex-0.2.4}/src/quantex/backtester.py +282 -25
- quantex-0.2.4/src/quantex/broker.py +633 -0
- quantex-0.2.4/src/quantex/datasource.py +260 -0
- quantex-0.2.4/src/quantex/enums.py +18 -0
- quantex-0.2.4/src/quantex/helpers.py +299 -0
- quantex-0.2.4/src/quantex/strategy.py +159 -0
- quantex-0.2.3/src/quantex/__init__.py +0 -4
- quantex-0.2.3/src/quantex/broker.py +0 -307
- quantex-0.2.3/src/quantex/datasource.py +0 -81
- quantex-0.2.3/src/quantex/enums.py +0 -6
- quantex-0.2.3/src/quantex/helpers.py +0 -146
- quantex-0.2.3/src/quantex/strategy.py +0 -32
- {quantex-0.2.3 → quantex-0.2.4}/LICENSE.md +0 -0
- {quantex-0.2.3 → quantex-0.2.4}/README.md +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""
|
|
2
|
+
QuantEx: A comprehensive backtesting framework for quantitative trading strategies.
|
|
3
|
+
|
|
4
|
+
This package provides a complete framework for developing, testing, and optimizing
|
|
5
|
+
trading strategies using historical market data. It includes tools for data
|
|
6
|
+
management, strategy development, backtesting, and performance optimization.
|
|
7
|
+
|
|
8
|
+
Key Components:
|
|
9
|
+
- Data Sources: Load and manage OHLCV market data from various formats
|
|
10
|
+
- Strategy Framework: Abstract base class for implementing trading strategies
|
|
11
|
+
- Broker Simulation: Realistic order execution and position management
|
|
12
|
+
- Backtesting Engine: Run historical simulations with performance tracking
|
|
13
|
+
- Optimization Tools: Grid search and parallel optimization for strategy parameters
|
|
14
|
+
- Time-Aware Arrays: Specialized numpy arrays for progressive data access
|
|
15
|
+
|
|
16
|
+
Features:
|
|
17
|
+
- Support for multiple data sources (CSV, Parquet)
|
|
18
|
+
- Realistic order execution (market, limit, stop-loss, take-profit)
|
|
19
|
+
- Commission modeling (percentage or fixed amount)
|
|
20
|
+
- Position management with margin calls
|
|
21
|
+
- Performance metrics (Sharpe ratio, max drawdown, total return)
|
|
22
|
+
- Parallel parameter optimization
|
|
23
|
+
- Time-series data validation and handling
|
|
24
|
+
- Walk-forward analysis with train/test splits
|
|
25
|
+
|
|
26
|
+
Example Usage:
|
|
27
|
+
>>> from quantex import CSVDataSource, Strategy, SimpleBacktester
|
|
28
|
+
>>>
|
|
29
|
+
>>> class MovingAverageStrategy(Strategy):
|
|
30
|
+
... def init(self):
|
|
31
|
+
... self.add_data(CSVDataSource("AAPL.csv"), "AAPL")
|
|
32
|
+
... self.fast_ma = self.Indicator(np.zeros(len(self.data["AAPL"])))
|
|
33
|
+
... self.slow_ma = self.Indicator(np.zeros(len(self.data["AAPL"])))
|
|
34
|
+
...
|
|
35
|
+
... def next(self):
|
|
36
|
+
... if len(self.data["AAPL"].Close) >= 20:
|
|
37
|
+
... self.fast_ma[-1] = np.mean(self.data["AAPL"].Close[-10:])
|
|
38
|
+
... self.slow_ma[-1] = np.mean(self.data["AAPL"].Close[-20:])
|
|
39
|
+
...
|
|
40
|
+
... if self.fast_ma[-1] > self.slow_ma[-1] and not self.positions["AAPL"].is_long():
|
|
41
|
+
... self.positions["AAPL"].buy(quantity=0.1)
|
|
42
|
+
... elif self.fast_ma[-1] < self.slow_ma[-1] and not self.positions["AAPL"].is_short():
|
|
43
|
+
... self.positions["AAPL"].sell(quantity=0.1)
|
|
44
|
+
>>>
|
|
45
|
+
>>> # Run backtest
|
|
46
|
+
>>> strategy = MovingAverageStrategy()
|
|
47
|
+
>>> backtester = SimpleBacktester(strategy)
|
|
48
|
+
>>> report = backtester.run()
|
|
49
|
+
>>> print(report)
|
|
50
|
+
|
|
51
|
+
Classes:
|
|
52
|
+
- Strategy: Abstract base class for trading strategies
|
|
53
|
+
- SimpleBacktester: Main backtesting engine with optimization capabilities
|
|
54
|
+
- CSVDataSource: Load market data from CSV files
|
|
55
|
+
- ParquetDataSource: Load market data from Parquet files
|
|
56
|
+
- CommissionType: Enumeration for commission calculation types
|
|
57
|
+
|
|
58
|
+
Data Requirements:
|
|
59
|
+
All data sources must contain OHLCV columns:
|
|
60
|
+
- 'Open': Opening prices
|
|
61
|
+
- 'High': High prices
|
|
62
|
+
- 'Low': Low prices
|
|
63
|
+
- 'Close': Closing prices
|
|
64
|
+
- 'Volume': Trading volume
|
|
65
|
+
|
|
66
|
+
Index should be datetime values for proper period calculations.
|
|
67
|
+
|
|
68
|
+
Performance Metrics:
|
|
69
|
+
- Total Return: Overall strategy performance
|
|
70
|
+
- Sharpe Ratio: Risk-adjusted returns with confidence intervals
|
|
71
|
+
- Maximum Drawdown: Largest peak-to-trough decline
|
|
72
|
+
- Number of Trades: Total executed trades
|
|
73
|
+
- Commission Impact: Total trading costs
|
|
74
|
+
|
|
75
|
+
Author: QuantEx Development Team
|
|
76
|
+
License: See LICENSE file for details
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
from .datasource import CSVDataSource as CSVDataSource, ParquetDataSource as ParquetDataSource
|
|
80
|
+
from .strategy import Strategy as Strategy
|
|
81
|
+
from .backtester import SimpleBacktester as SimpleBacktester
|
|
82
|
+
from .enums import CommissionType as CommissionType
|
|
@@ -17,12 +17,50 @@ import os
|
|
|
17
17
|
import gc
|
|
18
18
|
|
|
19
19
|
def max_drawdown(equity: pd.Series) -> float:
|
|
20
|
+
"""
|
|
21
|
+
Calculate the maximum drawdown of an equity curve.
|
|
22
|
+
|
|
23
|
+
The maximum drawdown represents the largest peak-to-trough decline
|
|
24
|
+
in the equity curve, expressed as a positive percentage.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
equity (pd.Series): Time series of equity values.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
float: Maximum drawdown as a positive percentage (e.g., 0.15 for 15%).
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
>>> equity = pd.Series([100, 110, 95, 105, 90])
|
|
34
|
+
>>> max_drawdown(equity)
|
|
35
|
+
0.18181818181818182 # ~18.18% drawdown
|
|
36
|
+
"""
|
|
20
37
|
running_max = equity.cummax()
|
|
21
38
|
drawdown = (equity - running_max) / running_max
|
|
22
39
|
max_dd = drawdown.min()
|
|
23
40
|
return float(abs(max_dd)) # return as positive percentage
|
|
24
41
|
|
|
25
42
|
def _infer_periods_per_year(index: pd.Index, default: int = 252 * 24 * 60) -> int:
|
|
43
|
+
"""
|
|
44
|
+
Infer the number of trading periods per year from a datetime index.
|
|
45
|
+
|
|
46
|
+
This function analyzes the time differences in the index to determine
|
|
47
|
+
the appropriate number of periods per year for annualized calculations.
|
|
48
|
+
Falls back to minute-level trading (252 trading days * 24 hours * 60 minutes)
|
|
49
|
+
if the index cannot be analyzed or contains insufficient data.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
index (pd.Index): DatetimeIndex containing timestamps.
|
|
53
|
+
default (int, optional): Default periods per year for minute trading.
|
|
54
|
+
Defaults to 252 * 24 * 60 (minute-level data).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
int: Estimated number of trading periods per year.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> dates = pd.date_range('2020-01-01', periods=100, freq='D')
|
|
61
|
+
>>> _infer_periods_per_year(dates)
|
|
62
|
+
252 # Daily trading periods
|
|
63
|
+
"""
|
|
26
64
|
# Simple inference; falls back to minute trading year if uncertain
|
|
27
65
|
if not isinstance(index, pd.DatetimeIndex) or len(index) < 3:
|
|
28
66
|
return default
|
|
@@ -39,8 +77,22 @@ def _infer_periods_per_year(index: pd.Index, default: int = 252 * 24 * 60) -> in
|
|
|
39
77
|
def _worker_init(pickled_strategy: bytes, cash: float, commision: float,
|
|
40
78
|
commision_type, lot_size: int):
|
|
41
79
|
"""
|
|
42
|
-
Initializer for worker processes
|
|
43
|
-
|
|
80
|
+
Initializer for worker processes in parallel optimization.
|
|
81
|
+
|
|
82
|
+
This function stores a pickled strategy and backtest configuration
|
|
83
|
+
in module globals so each worker process can reuse them for
|
|
84
|
+
parallel parameter optimization.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
pickled_strategy (bytes): Serialized strategy instance.
|
|
88
|
+
cash (float): Initial cash amount for backtesting.
|
|
89
|
+
commision (float): Commission rate for trades.
|
|
90
|
+
commision_type: Type of commission calculation (CommissionType enum).
|
|
91
|
+
lot_size (int): Size of trading lots.
|
|
92
|
+
|
|
93
|
+
Note:
|
|
94
|
+
This function is designed to be called by worker processes
|
|
95
|
+
during parallel optimization and should not be used directly.
|
|
44
96
|
"""
|
|
45
97
|
global _WORKER_PICKLED_STRAT, _WORKER_BT_CONFIG
|
|
46
98
|
_WORKER_PICKLED_STRAT = pickled_strategy
|
|
@@ -53,10 +105,27 @@ def _worker_init(pickled_strategy: bytes, cash: float, commision: float,
|
|
|
53
105
|
|
|
54
106
|
def _worker_eval(param_items):
|
|
55
107
|
"""
|
|
56
|
-
Worker evaluation function.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
108
|
+
Worker evaluation function for parallel parameter optimization.
|
|
109
|
+
|
|
110
|
+
This function runs in worker processes to evaluate a single
|
|
111
|
+
parameter combination and return performance metrics.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
param_items: Sequence of (key, value) pairs (tuple) to reconstruct dict.
|
|
115
|
+
Each tuple represents a parameter name and its value.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
dict: Dictionary containing metrics for the evaluated parameters:
|
|
119
|
+
- 'params': Dictionary of parameter values used
|
|
120
|
+
- 'final_cash': Final cash amount after backtest
|
|
121
|
+
- 'total_return': Total return as decimal (e.g., 0.15 for 15%)
|
|
122
|
+
- 'sharpe': Sharpe ratio (or NaN if invalid)
|
|
123
|
+
- 'max_drawdown': Maximum drawdown as decimal
|
|
124
|
+
- 'trades': Number of trades executed
|
|
125
|
+
|
|
126
|
+
Note:
|
|
127
|
+
This function is designed for use in worker processes during
|
|
128
|
+
parallel optimization and should not be called directly.
|
|
60
129
|
"""
|
|
61
130
|
global _WORKER_PICKLED_STRAT, _WORKER_BT_CONFIG
|
|
62
131
|
# Reconstruct params dict
|
|
@@ -118,6 +187,19 @@ def _worker_eval(param_items):
|
|
|
118
187
|
|
|
119
188
|
@dataclass
|
|
120
189
|
class BacktestReport:
|
|
190
|
+
"""
|
|
191
|
+
Container for backtest results and performance metrics.
|
|
192
|
+
|
|
193
|
+
This class encapsulates the complete results of a backtest run,
|
|
194
|
+
including P&L records, orders executed, and calculated performance
|
|
195
|
+
metrics such as Sharpe ratio and maximum drawdown.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
starting_cash (np.float64): Initial cash amount at start of backtest.
|
|
199
|
+
final_cash (np.float64): Final cash amount at end of backtest.
|
|
200
|
+
PnlRecord (pd.Series): Time series of P&L values throughout the backtest.
|
|
201
|
+
orders (list[Order]): List of all orders executed during the backtest.
|
|
202
|
+
"""
|
|
121
203
|
starting_cash: np.float64
|
|
122
204
|
final_cash: np.float64
|
|
123
205
|
PnlRecord: pd.Series
|
|
@@ -125,15 +207,32 @@ class BacktestReport:
|
|
|
125
207
|
|
|
126
208
|
@property
|
|
127
209
|
def periods_per_year(self):
|
|
210
|
+
"""
|
|
211
|
+
Calculate the number of trading periods per year.
|
|
212
|
+
|
|
213
|
+
This property infers the appropriate number of periods per year
|
|
214
|
+
from the P&L record index, useful for annualized calculations.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
int: Number of trading periods per year (e.g., 252 for daily data).
|
|
218
|
+
"""
|
|
128
219
|
return _infer_periods_per_year(self.PnlRecord.astype(float).index, 252 * 24 * 60)
|
|
129
220
|
|
|
130
221
|
def plot(self, figsize: tuple = (10, 5)) -> None:
|
|
131
222
|
"""
|
|
132
|
-
Plot the equity curve and
|
|
133
|
-
|
|
223
|
+
Plot the equity curve and drawdown charts.
|
|
224
|
+
|
|
225
|
+
Creates a two-panel plot showing:
|
|
226
|
+
1. The equity curve over time
|
|
227
|
+
2. The drawdown curve as a percentage
|
|
228
|
+
|
|
134
229
|
Args:
|
|
135
|
-
|
|
136
|
-
|
|
230
|
+
figsize (tuple, optional): Figure size as (width, height) in inches.
|
|
231
|
+
Defaults to (10, 5).
|
|
232
|
+
|
|
233
|
+
Note:
|
|
234
|
+
This method uses matplotlib to display the plots and requires
|
|
235
|
+
an interactive environment to show the figures.
|
|
137
236
|
"""
|
|
138
237
|
equity = self.PnlRecord.astype(float)
|
|
139
238
|
running_max = equity.cummax()
|
|
@@ -167,6 +266,16 @@ class BacktestReport:
|
|
|
167
266
|
plt.show()
|
|
168
267
|
|
|
169
268
|
def __str__(self) -> str:
|
|
269
|
+
"""
|
|
270
|
+
Generate a formatted string summary of backtest results.
|
|
271
|
+
|
|
272
|
+
Returns a human-readable string containing key performance
|
|
273
|
+
metrics including total return, Sharpe ratio with confidence
|
|
274
|
+
intervals, maximum drawdown, and total number of trades.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
str: Formatted string with backtest summary statistics.
|
|
278
|
+
"""
|
|
170
279
|
equity = self.PnlRecord.astype(float)
|
|
171
280
|
returns = equity.pct_change().dropna()
|
|
172
281
|
|
|
@@ -214,6 +323,29 @@ class BacktestReport:
|
|
|
214
323
|
)
|
|
215
324
|
|
|
216
325
|
class SimpleBacktester():
|
|
326
|
+
"""
|
|
327
|
+
Simple backtester for executing trading strategies on historical data.
|
|
328
|
+
|
|
329
|
+
This class provides functionality to run backtests on trading strategies,
|
|
330
|
+
calculate performance metrics, and perform parameter optimization through
|
|
331
|
+
grid search (both sequential and parallel).
|
|
332
|
+
|
|
333
|
+
The backtester simulates realistic trading conditions including:
|
|
334
|
+
- Order execution with market and limit orders
|
|
335
|
+
- Commission calculations
|
|
336
|
+
- Position management
|
|
337
|
+
- Margin calls
|
|
338
|
+
- P&L tracking
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> from quantex import SimpleBacktester, CSVDataSource
|
|
342
|
+
>>> # Create strategy and data source
|
|
343
|
+
>>> source = CSVDataSource("data.csv")
|
|
344
|
+
>>> # strategy = MyStrategy() # Your custom strategy
|
|
345
|
+
>>> bt = SimpleBacktester(strategy, cash=10000)
|
|
346
|
+
>>> report = bt.run()
|
|
347
|
+
>>> print(report)
|
|
348
|
+
"""
|
|
217
349
|
def __init__(self,
|
|
218
350
|
strategy: Strategy,
|
|
219
351
|
cash: float = 10_000,
|
|
@@ -222,6 +354,24 @@ class SimpleBacktester():
|
|
|
222
354
|
lot_size: int = 1,
|
|
223
355
|
margin_call: float = 0.5 ## 50% of the cash lost
|
|
224
356
|
):
|
|
357
|
+
"""
|
|
358
|
+
Initialize the backtester with strategy and configuration parameters.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
strategy (Strategy): Trading strategy to backtest. Must implement
|
|
362
|
+
the Strategy interface with init() and next() methods.
|
|
363
|
+
cash (float, optional): Initial cash amount. Defaults to 10,000.
|
|
364
|
+
commission (float, optional): Commission rate per trade. Defaults to 0.002 (0.2%).
|
|
365
|
+
commission_type (CommissionType, optional): Type of commission calculation.
|
|
366
|
+
Can be CommissionType.PERCENTAGE or CommissionType.CASH.
|
|
367
|
+
Defaults to CommissionType.PERCENTAGE.
|
|
368
|
+
lot_size (int, optional): Size of trading lots. Defaults to 1.
|
|
369
|
+
margin_call (float, optional): Margin call threshold as fraction of
|
|
370
|
+
cash value. Defaults to 0.5 (50%).
|
|
371
|
+
|
|
372
|
+
Raises:
|
|
373
|
+
ValueError: If strategy is None or commission rate is negative.
|
|
374
|
+
"""
|
|
225
375
|
self.strategy = copy.deepcopy(strategy)
|
|
226
376
|
self.cash = cash
|
|
227
377
|
self.commission = commission
|
|
@@ -231,6 +381,30 @@ class SimpleBacktester():
|
|
|
231
381
|
source = self.strategy.positions[list(self.strategy.positions.keys())[0]].source
|
|
232
382
|
self.PnLRecord = np.zeros(len(source.data['Close']), dtype=np.float64)
|
|
233
383
|
def run(self, progress_bar: bool = False) -> BacktestReport:
|
|
384
|
+
"""
|
|
385
|
+
Execute the backtest for the configured strategy.
|
|
386
|
+
|
|
387
|
+
This method runs the complete backtest simulation, iterating through
|
|
388
|
+
all data points in the strategy's data sources, executing strategy logic,
|
|
389
|
+
processing orders, and tracking performance metrics.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
progress_bar (bool, optional): Whether to show a progress bar during
|
|
393
|
+
backtest execution. Useful for long-running backtests.
|
|
394
|
+
Defaults to False.
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
BacktestReport: Object containing complete backtest results including:
|
|
398
|
+
- Starting and final cash amounts
|
|
399
|
+
- P&L record over time
|
|
400
|
+
- List of all executed orders
|
|
401
|
+
- Calculated performance metrics
|
|
402
|
+
|
|
403
|
+
Note:
|
|
404
|
+
This method modifies the internal state of the strategy and
|
|
405
|
+
should not be called multiple times on the same instance
|
|
406
|
+
without resetting.
|
|
407
|
+
"""
|
|
234
408
|
for key in self.strategy.positions.keys():
|
|
235
409
|
self.strategy.positions[key].cash = np.float64(self.cash)
|
|
236
410
|
self.strategy.positions[key].lot_size = self.lot_size
|
|
@@ -264,16 +438,53 @@ class SimpleBacktester():
|
|
|
264
438
|
def optimize(self, params: dict[str, range], constraint: Callable[[dict[str, Any]], bool] | None = None):
|
|
265
439
|
"""
|
|
266
440
|
Perform a grid search over the provided parameter ranges.
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
441
|
+
|
|
442
|
+
This method systematically tests all combinations of parameter values
|
|
443
|
+
to find the optimal configuration for the trading strategy. Each
|
|
444
|
+
parameter combination is backtested individually to evaluate performance.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
params (dict[str, range]): Dictionary mapping strategy attribute names
|
|
448
|
+
to iterables of candidate values. For example:
|
|
449
|
+
```python
|
|
450
|
+
{
|
|
451
|
+
'fast_period': range(5, 21, 5), # [5, 10, 15, 20]
|
|
452
|
+
'slow_period': range(20, 51, 10), # [20, 30, 40, 50]
|
|
453
|
+
'threshold': np.linspace(0.01, 0.1, 10)
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
constraint (Callable[[dict[str, Any]], bool] | None, optional):
|
|
457
|
+
Optional callable that takes a candidate parameter dict and returns
|
|
458
|
+
True to evaluate the combo or False to skip it. Useful for enforcing
|
|
459
|
+
logical constraints like ensuring fast_period < slow_period.
|
|
460
|
+
Defaults to None (no constraints).
|
|
461
|
+
|
|
273
462
|
Returns:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
463
|
+
tuple: A tuple containing (best_params, best_report, results):
|
|
464
|
+
- best_params (dict[str, Any]): Dictionary of parameter values
|
|
465
|
+
that produced the best performance.
|
|
466
|
+
- best_report (BacktestReport): Complete backtest report for the
|
|
467
|
+
best parameter combination.
|
|
468
|
+
- results (pd.DataFrame): DataFrame with metrics for all valid
|
|
469
|
+
parameter combinations, sorted by performance.
|
|
470
|
+
|
|
471
|
+
Raises:
|
|
472
|
+
ValueError: If params is empty or contains parameters with no values.
|
|
473
|
+
TypeError: If any parameter values are not iterable.
|
|
474
|
+
|
|
475
|
+
Note:
|
|
476
|
+
The optimization uses Sharpe ratio as the primary selection criterion.
|
|
477
|
+
If Sharpe ratio is invalid (NaN), it falls back to total return,
|
|
478
|
+
then to final cash amount.
|
|
479
|
+
|
|
480
|
+
Example:
|
|
481
|
+
>>> bt = SimpleBacktester(strategy)
|
|
482
|
+
>>> best_params, best_report, results = bt.optimize({
|
|
483
|
+
... 'fast_period': [5, 10, 20],
|
|
484
|
+
... 'slow_period': [20, 50, 100]
|
|
485
|
+
... }, constraint=lambda p: p['fast_period'] < p['slow_period'])
|
|
486
|
+
>>> print(f"Best parameters: {best_params}")
|
|
487
|
+
>>> print(f"Best Sharpe ratio: {best_report.periods_per_year}")
|
|
277
488
|
"""
|
|
278
489
|
if not params:
|
|
279
490
|
raise ValueError("params must not be empty")
|
|
@@ -404,12 +615,58 @@ class SimpleBacktester():
|
|
|
404
615
|
workers: int | None = None,
|
|
405
616
|
chunksize: int = 1):
|
|
406
617
|
"""
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
618
|
+
Perform parallel grid search over parameter ranges for optimization.
|
|
619
|
+
|
|
620
|
+
This method is identical to optimize() but uses multiprocessing to
|
|
621
|
+
distribute parameter combinations across multiple worker processes,
|
|
622
|
+
significantly reducing computation time for large parameter spaces.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
params (dict[str, range]): Dictionary mapping strategy attribute names
|
|
626
|
+
to iterables of candidate values (same format as optimize()).
|
|
627
|
+
constraint (Callable[[dict[str, Any]], bool] | None, optional):
|
|
628
|
+
Optional callable for parameter constraints (same as optimize()).
|
|
629
|
+
Defaults to None.
|
|
630
|
+
workers (int | None, optional): Maximum number of worker processes to use.
|
|
631
|
+
If None, defaults to min(os.cpu_count()-1, 4) to avoid overwhelming
|
|
632
|
+
the system. Defaults to None.
|
|
633
|
+
chunksize (int, optional): Chunk size for ProcessPoolExecutor.map.
|
|
634
|
+
Smaller values provide better load balancing for many small tasks.
|
|
635
|
+
Larger values reduce overhead for fewer, larger tasks.
|
|
636
|
+
Defaults to 1.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
tuple: Same return format as optimize():
|
|
640
|
+
(best_params, best_report, results_df)
|
|
641
|
+
|
|
642
|
+
Raises:
|
|
643
|
+
ValueError: If params is empty or contains parameters with no values.
|
|
644
|
+
TypeError: If any parameter values are not iterable.
|
|
645
|
+
|
|
646
|
+
Note:
|
|
647
|
+
- This method creates separate processes, so the strategy must be
|
|
648
|
+
picklable for multiprocessing to work.
|
|
649
|
+
- The main process re-runs the best configuration to get the full
|
|
650
|
+
BacktestReport (parallel workers only return summary metrics).
|
|
651
|
+
- Uses ProcessPoolExecutor for true parallelism across CPU cores.
|
|
652
|
+
- Memory usage scales with the number of workers as each worker
|
|
653
|
+
maintains a copy of the strategy.
|
|
654
|
+
|
|
655
|
+
Performance Tips:
|
|
656
|
+
- For parameter spaces with many combinations (>1000), prefer
|
|
657
|
+
optimize_parallel over optimize for better performance.
|
|
658
|
+
- For small parameter spaces, optimize() may be faster due to
|
|
659
|
+
lower multiprocessing overhead.
|
|
660
|
+
- Monitor system memory usage as each worker maintains a full
|
|
661
|
+
copy of the strategy and data.
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> bt = SimpleBacktester(strategy)
|
|
665
|
+
>>> # Use 4 workers for parallel optimization
|
|
666
|
+
>>> best_params, best_report, results = bt.optimize_parallel(
|
|
667
|
+
... {'period1': range(5, 50, 5), 'period2': range(20, 100, 10)},
|
|
668
|
+
... workers=4
|
|
669
|
+
... )
|
|
413
670
|
"""
|
|
414
671
|
if not params:
|
|
415
672
|
raise ValueError("params must not be empty")
|