quantex 0.4.4__tar.gz → 0.4.5__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.4
3
+ Version: 0.4.5
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.4"
3
+ version = "0.4.5"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -1,10 +1,11 @@
1
1
  """
2
2
  Monte Carlo simulation module for quantex backtesting.
3
3
 
4
- This module provides Monte Carlo simulation capabilities to test strategy
5
- robustness through two approaches:
6
- 1. Trade Order Randomization - shuffles the sequence of executed trades
7
- 2. Price Path Resampling (Bootstrap) - resamples historical returns to create synthetic paths
4
+ This module provides Monte Carlo simulation capabilities to test strategy
5
+ robustness through three approaches:
6
+ 1. Trade Order Randomization - shuffles the sequence of executed trades
7
+ 2. Trade Shuffle With Replacement - resamples trade returns with replacement
8
+ 3. Price Path Resampling (Bootstrap) - resamples historical returns to create synthetic paths
8
9
  """
9
10
 
10
11
  import copy
@@ -33,6 +34,7 @@ class MonteCarloMode(Enum):
33
34
  BOTH: Run both analyses and combine results
34
35
  """
35
36
  TRADE_ORDER = "trade_order"
37
+ TRADE_SHUFFLE_WITH_REPLACEMENT = "trade_shuffle_with_replacement"
36
38
  PRICE_PATH = "price_path"
37
39
  BOTH = "both"
38
40
 
@@ -358,6 +360,56 @@ def _run_trade_order_simulation(
358
360
  return pd.Series(equity, index=index)
359
361
 
360
362
 
363
+ def _run_trade_shuffle_with_replacement_simulation(
364
+ original_orders: list[Order],
365
+ original_cash: float,
366
+ original_equity: pd.Series,
367
+ commission: float,
368
+ commission_type,
369
+ lot_size: int,
370
+ seed: int | None = None,
371
+ ) -> pd.Series:
372
+ """
373
+ Run a Monte Carlo simulation that samples trade outcomes with replacement.
374
+
375
+ This mode allows some trades to be repeated while others may be omitted.
376
+ The sampled trade outcomes are then applied as percentage returns to the
377
+ portfolio curve.
378
+ """
379
+ if seed is not None:
380
+ random.seed(seed)
381
+
382
+ index = original_equity.index
383
+ equity = np.full(len(index), original_cash, dtype=np.float64)
384
+
385
+ equity_values = np.asarray(original_equity.values, dtype=np.float64)
386
+ equity_returns = np.zeros_like(equity_values)
387
+ if len(equity_values) > 1:
388
+ prev = np.empty_like(equity_values)
389
+ prev[0] = original_cash
390
+ prev[1:] = equity_values[:-1]
391
+ equity_returns[1:] = np.where(prev[1:] > 0, (equity_values[1:] / prev[1:]) - 1.0, 0.0)
392
+
393
+ trade_returns = equity_returns[1:].tolist()
394
+ if not trade_returns:
395
+ return pd.Series(equity, index=index)
396
+
397
+ # Build a replacement-sampled sequence that may include repeated trade
398
+ # outcomes and omit others entirely by only sampling a subset of the trade
399
+ # return pool on each simulation, then pad the rest with neutral returns so
400
+ # the full curve length is preserved.
401
+ sample_size = max(1, int(len(trade_returns) * 0.75))
402
+ sampled_returns = [random.choice(trade_returns) for _ in range(sample_size)]
403
+ sampled_returns.extend([0.0] * (len(trade_returns) - sample_size))
404
+
405
+ equity_returns = np.concatenate(([0.0], np.asarray(sampled_returns, dtype=np.float64)))
406
+
407
+ for i in range(1, len(equity)):
408
+ equity[i] = equity[i - 1] * (1.0 + equity_returns[i])
409
+
410
+ return pd.Series(equity, index=index)
411
+
412
+
361
413
  def _run_price_path_simulation(
362
414
  strategy: Strategy,
363
415
  data_sources: dict[str, DataSource],
@@ -583,6 +635,21 @@ def monte_carlo(
583
635
  # No trades, just return original equity
584
636
  curve = original_equity.copy()
585
637
  equity_curves.append(curve)
638
+
639
+ if mode == MonteCarloMode.TRADE_SHUFFLE_WITH_REPLACEMENT:
640
+ if len(original_orders) > 0:
641
+ curve = _run_trade_shuffle_with_replacement_simulation(
642
+ original_orders,
643
+ original_cash,
644
+ original_equity,
645
+ self.commission,
646
+ self.commission_type,
647
+ self.lot_size,
648
+ seed=iter_seed,
649
+ )
650
+ else:
651
+ curve = original_equity.copy()
652
+ equity_curves.append(curve)
586
653
 
587
654
  if mode == MonteCarloMode.PRICE_PATH or mode == MonteCarloMode.BOTH:
588
655
  # Price path resampling
@@ -637,6 +704,15 @@ def monte_carlo(
637
704
  "trade_order": trade_result.percentile_results,
638
705
  "price_path": price_result.percentile_results,
639
706
  }
707
+ elif mode == MonteCarloMode.TRADE_SHUFFLE_WITH_REPLACEMENT:
708
+ result = MonteCarloResult(
709
+ mode=mode,
710
+ equity_curves=equity_curves,
711
+ original_equity=original_equity,
712
+ simulations=simulations,
713
+ starting_cash=original_cash,
714
+ )
715
+ result._compute_statistics()
640
716
  else:
641
717
  # Create result object
642
718
  result = MonteCarloResult(
File without changes
File without changes
File without changes
File without changes
File without changes