quantex 0.4.3__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.3
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.3"
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
 
@@ -340,8 +342,10 @@ def _run_trade_order_simulation(
340
342
  equity_values = np.asarray(original_equity.values, dtype=np.float64)
341
343
  equity_returns = np.zeros_like(equity_values)
342
344
  if len(equity_values) > 1:
343
- prev = np.where(np.arange(len(equity_values)) == 0, original_cash, equity_values[:-1])
344
- equity_returns[1:] = np.where(prev > 0, (equity_values[1:] / prev) - 1.0, 0.0)
345
+ prev = np.empty_like(equity_values)
346
+ prev[0] = original_cash
347
+ prev[1:] = equity_values[:-1]
348
+ equity_returns[1:] = np.where(prev[1:] > 0, (equity_values[1:] / prev[1:]) - 1.0, 0.0)
345
349
 
346
350
  # Keep the starting cash anchored at index 0 and randomize the remaining
347
351
  # returns so the path always begins from the actual initial capital.
@@ -356,6 +360,56 @@ def _run_trade_order_simulation(
356
360
  return pd.Series(equity, index=index)
357
361
 
358
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
+
359
413
  def _run_price_path_simulation(
360
414
  strategy: Strategy,
361
415
  data_sources: dict[str, DataSource],
@@ -581,6 +635,21 @@ def monte_carlo(
581
635
  # No trades, just return original equity
582
636
  curve = original_equity.copy()
583
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)
584
653
 
585
654
  if mode == MonteCarloMode.PRICE_PATH or mode == MonteCarloMode.BOTH:
586
655
  # Price path resampling
@@ -635,6 +704,15 @@ def monte_carlo(
635
704
  "trade_order": trade_result.percentile_results,
636
705
  "price_path": price_result.percentile_results,
637
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()
638
716
  else:
639
717
  # Create result object
640
718
  result = MonteCarloResult(
File without changes
File without changes
File without changes
File without changes
File without changes