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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: quantex
3
- Version: 0.2.3
3
+ Version: 0.2.4
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.2.3"
3
+ version = "0.2.4"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -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. Stores a pickled strategy and
43
- backtest config in module globals so each worker reuses them.
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
- param_items: sequence of (key, value) pairs (tuple) to reconstruct dict
59
- Returns a small dict with metrics (no heavy objects).
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 optionally the drawdown.
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
- show_drawdown (bool): Whether to include drawdown chart.
136
- figsize (tuple): Figure size for matplotlib.
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
- params: dict mapping strategy attribute names to iterables of candidate values.
269
- constraint: optional callable that takes the candidate parameter dict and returns True
270
- to evaluate the combo or False to skip it. For example:
271
- lambda p: p["fast_period"] < p["slow_period"]
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
- best_params: dict[str, Any]
275
- best_report: BacktestReport
276
- results: pd.DataFrame with metrics per combination (only valid/kept combos)
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
- Parallel grid search.
408
-
409
- - workers: max number of worker processes to use (default: min(os.cpu_count()-1, 4))
410
- - chunksize: passed to Executor.map -- tune when you have many small tasks.
411
-
412
- Returns (best_params, best_report, results_df)
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")