quantex 0.4.7__tar.gz → 0.4.8__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.7
3
+ Version: 0.4.8
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.7"
3
+ version = "0.4.8"
4
4
  description = "A simple quant strategy creation and backtesting package."
5
5
  authors = [
6
6
  {name = "Daniel Green",email = "dangreen07@outlook.com"}
@@ -38,6 +38,7 @@ class SimpleBacktester:
38
38
  - Position management
39
39
  - Margin calls
40
40
  - P&L tracking
41
+ - Leverage for amplified position sizing
41
42
 
42
43
  Example:
43
44
  >>> from quantex import SimpleBacktester, CSVDataSource
@@ -54,7 +55,8 @@ class SimpleBacktester:
54
55
  commission: float = 0.002,
55
56
  commission_type: CommissionType = CommissionType.PERCENTAGE,
56
57
  lot_size: int = 1,
57
- margin_call: float = 0.5 ## 50% of the cash lost
58
+ margin_call: float = 0.5, ## 50% of the cash lost
59
+ leverage: float = 1.0 ## Leverage multiplier (1.0 = no leverage)
58
60
  ):
59
61
  """
60
62
  Initialize the backtester with strategy and configuration parameters.
@@ -70,16 +72,25 @@ class SimpleBacktester:
70
72
  lot_size (int, optional): Size of trading lots. Defaults to 1.
71
73
  margin_call (float, optional): Margin call threshold as fraction of
72
74
  cash value. Defaults to 0.5 (50%).
75
+ leverage (float, optional): Leverage multiplier for position sizing.
76
+ Defaults to 1.0 (no leverage). For example:
77
+ - 2.0 = 2x leverage (control 2x the position with same cash)
78
+ - 0.5 = half leverage (control half the position)
73
79
 
74
80
  Raises:
75
81
  ValueError: If strategy is None or commission rate is negative.
76
82
  """
83
+ if leverage < 0.1:
84
+ raise ValueError("leverage must be at least 0.1")
85
+ if leverage > 100:
86
+ raise ValueError("leverage cannot exceed 100")
77
87
  self.strategy = copy.deepcopy(strategy)
78
88
  self.cash = cash
79
89
  self.commission = commission
80
90
  self.commission_type = commission_type
81
91
  self.lot_size = lot_size
82
92
  self.margin_call = margin_call
93
+ self.leverage = leverage
83
94
  source = self.strategy.positions[list(self.strategy.positions.keys())[0]].source
84
95
  self.PnLRecord = np.zeros(len(source.data['Close']), dtype=np.float64)
85
96
 
@@ -140,6 +151,7 @@ class SimpleBacktester:
140
151
  broker.cash = per_position_cash
141
152
  broker.lot_size = self.lot_size
142
153
  broker.margin_call = self.margin_call
154
+ broker.leverage = self.leverage
143
155
  broker.commision = np.float64(self.commission)
144
156
  broker.commision_type = self.commission_type
145
157
 
@@ -302,6 +314,7 @@ class SimpleBacktester:
302
314
  commission=self.commission,
303
315
  commission_type=self.commission_type,
304
316
  lot_size=self.lot_size,
317
+ leverage=self.leverage,
305
318
  )
306
319
  report = bt.run(progress_bar=False)
307
320
 
@@ -504,6 +517,7 @@ class SimpleBacktester:
504
517
  self.commission,
505
518
  self.commission_type,
506
519
  self.lot_size,
520
+ self.leverage,
507
521
  ),
508
522
  ) as exe:
509
523
  # map the worker over param item tuples
@@ -561,6 +575,7 @@ class SimpleBacktester:
561
575
  commission=self.commission,
562
576
  commission_type=self.commission_type,
563
577
  lot_size=self.lot_size,
578
+ leverage=self.leverage,
564
579
  )
565
580
  best_report = bt.run(progress_bar=False)
566
581
 
@@ -679,6 +694,7 @@ class SimpleBacktester:
679
694
  commission: float,
680
695
  commission_type: CommissionType,
681
696
  lot_size: int,
697
+ leverage: float,
682
698
  objective: str,
683
699
  risk_tolerance: dict[str, float] | None,
684
700
  constraint: Callable[[dict[str, Any]], bool] | None,
@@ -750,6 +766,7 @@ class SimpleBacktester:
750
766
  commission=commission,
751
767
  commission_type=commission_type,
752
768
  lot_size=lot_size,
769
+ leverage=leverage,
753
770
  )
754
771
  report = bt.run(progress_bar=False)
755
772
 
@@ -790,6 +807,7 @@ class SimpleBacktester:
790
807
  commission=self.commission,
791
808
  commission_type=self.commission_type,
792
809
  lot_size=self.lot_size,
810
+ leverage=self.leverage,
793
811
  objective=objective,
794
812
  risk_tolerance=risk_tolerance,
795
813
  constraint=constraint,
@@ -843,6 +861,7 @@ class SimpleBacktester:
843
861
  commission=self.commission,
844
862
  commission_type=self.commission_type,
845
863
  lot_size=self.lot_size,
864
+ leverage=self.leverage,
846
865
  )
847
866
  best_report = bt.run(progress_bar=False)
848
867
  best_metrics = _compute_backtest_metrics(best_report)
@@ -1024,6 +1043,7 @@ class SimpleBacktester:
1024
1043
  commission=self.commission,
1025
1044
  commission_type=self.commission_type,
1026
1045
  lot_size=self.lot_size,
1046
+ leverage=self.leverage,
1027
1047
  )
1028
1048
  report = bt.run(progress_bar=False)
1029
1049
  metrics = _compute_backtest_metrics(report)
@@ -1086,7 +1106,7 @@ class SimpleBacktester:
1086
1106
  best_params = {}
1087
1107
  best_validate_score = -np.inf
1088
1108
 
1089
- # Get full reports for best parameters
1109
+ # Get full reports for best parameters
1090
1110
  train_report = None
1091
1111
  validate_report = None
1092
1112
  test_report = None
@@ -1108,6 +1128,7 @@ class SimpleBacktester:
1108
1128
  commission=self.commission,
1109
1129
  commission_type=self.commission_type,
1110
1130
  lot_size=self.lot_size,
1131
+ leverage=self.leverage,
1111
1132
  )
1112
1133
  report = bt.run(progress_bar=False)
1113
1134
  metrics = _compute_backtest_metrics(report)
@@ -1302,6 +1323,7 @@ class SimpleBacktester:
1302
1323
  commission=self.commission,
1303
1324
  commission_type=self.commission_type,
1304
1325
  lot_size=self.lot_size,
1326
+ leverage=self.leverage,
1305
1327
  )
1306
1328
  report = bt.run(progress_bar=False)
1307
1329
  metrics = _compute_backtest_metrics(report)
@@ -1425,6 +1447,7 @@ class SimpleBacktester:
1425
1447
  commission=self.commission,
1426
1448
  commission_type=self.commission_type,
1427
1449
  lot_size=self.lot_size,
1450
+ leverage=self.leverage,
1428
1451
  )
1429
1452
  report = bt.run(progress_bar=False)
1430
1453
  metrics = _compute_backtest_metrics(report)
@@ -462,6 +462,7 @@ def _run_price_path_simulation(
462
462
  commission: float,
463
463
  commission_type,
464
464
  lot_size: int,
465
+ leverage: float = 1.0,
465
466
  seed: int | None = None,
466
467
  ) -> pd.Series:
467
468
  """
@@ -477,6 +478,7 @@ def _run_price_path_simulation(
477
478
  commission (float): Commission rate.
478
479
  commission_type: Commission type enum.
479
480
  lot_size (int): Lot size for trading.
481
+ leverage (float): Leverage multiplier for position sizing.
480
482
  seed (int | None): Random seed for reproducibility.
481
483
 
482
484
  Returns:
@@ -581,6 +583,7 @@ def _run_price_path_simulation(
581
583
  commission=commission,
582
584
  commission_type=commission_type,
583
585
  lot_size=lot_size,
586
+ leverage=leverage,
584
587
  )
585
588
  report = bt.run(progress_bar=False)
586
589
 
@@ -705,6 +708,7 @@ def monte_carlo(
705
708
  self.commission,
706
709
  self.commission_type,
707
710
  self.lot_size,
711
+ leverage=self.leverage,
708
712
  seed=iter_seed,
709
713
  )
710
714
  equity_curves.append(curve)
@@ -20,7 +20,8 @@ def _worker_init(
20
20
  cash: float,
21
21
  commision: float,
22
22
  commision_type: Any,
23
- lot_size: int
23
+ lot_size: int,
24
+ leverage: float,
24
25
  ) -> None:
25
26
  """
26
27
  Initializer for worker processes in parallel optimization.
@@ -35,6 +36,7 @@ def _worker_init(
35
36
  commision (float): Commission rate for trades.
36
37
  commision_type: Type of commission calculation (CommissionType enum).
37
38
  lot_size (int): Size of trading lots.
39
+ leverage (float): Leverage multiplier for position sizing.
38
40
 
39
41
  Note:
40
42
  This function is designed to be called by worker processes
@@ -47,6 +49,7 @@ def _worker_init(
47
49
  "commision": commision,
48
50
  "commision_type": commision_type,
49
51
  "lot_size": lot_size,
52
+ "leverage": leverage,
50
53
  }
51
54
 
52
55
 
@@ -176,6 +179,7 @@ def _worker_eval(param_items: tuple[tuple[str, Any], ...]) -> dict[str, Any]:
176
179
  commission=config["commision"],
177
180
  commission_type=config["commision_type"],
178
181
  lot_size=config["lot_size"],
182
+ leverage=config["leverage"],
179
183
  )
180
184
  report = bt.run(progress_bar=False)
181
185
 
@@ -52,6 +52,7 @@ class Broker:
52
52
  - Commission calculations
53
53
  - Stop loss and take profit order management
54
54
  - P&L record tracking
55
+ - Leverage for amplified position sizing
55
56
 
56
57
  Example:
57
58
  >>> source = CSVDataSource("data.csv")
@@ -74,6 +75,7 @@ class Broker:
74
75
  self.commision_type: CommissionType = CommissionType.PERCENTAGE
75
76
  self.lot_size: int = 1
76
77
  self.margin_call: float = 0.5 ## 50% of the cash value
78
+ self.leverage: float = 1.0 ## Leverage multiplier (1.0 = no leverage)
77
79
  self.share_decimals = 1
78
80
  self.orders: list[Order] = []
79
81
  self.complete_orders = []
@@ -154,7 +156,9 @@ class Broker:
154
156
  during the next iteration.
155
157
  - Market orders execute immediately at current open price.
156
158
  - Limit orders only execute when price reaches the specified level.
157
-
159
+ - Leverage amplifies position size - with 2x leverage and quantity=1,
160
+ you control 2x the shares while only using 1x cash as margin.
161
+
158
162
  Example:
159
163
  >>> broker = Broker(source)
160
164
  >>> # Buy with 25% of available cash
@@ -176,9 +180,15 @@ class Broker:
176
180
  else:
177
181
  type = OrderType.MARKET
178
182
  current_price = self.source.Close[-1]
179
- total_shares = round((self.cash * quantity) / current_price, self.share_decimals)
180
183
  if (amount):
184
+ # When using absolute amount, still apply leverage to the base calculation
185
+ # but the user-provided amount is the final leveraged position size
181
186
  total_shares = amount
187
+ else:
188
+ # Calculate shares: base on cash * quantity, then apply leverage
189
+ base_shares = round((self.cash * quantity) / current_price, self.share_decimals)
190
+ # Apply leverage to increase position size
191
+ total_shares = round(base_shares * self.leverage, self.share_decimals)
182
192
  order = Order(
183
193
  side=OrderSide.BUY,
184
194
  quantity=total_shares,
@@ -489,7 +499,9 @@ class Broker:
489
499
  self.position_avg_price = (old_pos * self.position_avg_price + order.quantity * order.price) / new_pos
490
500
  else:
491
501
  self.position_avg_price = order.price
492
- self._debit(order.price * order.quantity)
502
+ # Calculate margin (cash used) for leveraged positions
503
+ margin = order.price * order.quantity / self.leverage
504
+ self._debit(margin)
493
505
  self._apply_commission(order.quantity, order.price)
494
506
  self.position = new_pos
495
507
  else:
@@ -504,7 +516,9 @@ class Broker:
504
516
  self.position_avg_price = (old_pos * self.position_avg_price + order.quantity * order.price) / new_pos
505
517
  else:
506
518
  self.position_avg_price = order.price
507
- self._credit(order.price * order.quantity)
519
+ # Calculate margin released for leveraged positions
520
+ margin = order.price * order.quantity / self.leverage
521
+ self._credit(margin)
508
522
  self._apply_commission(order.quantity, order.price)
509
523
  self.position = new_pos
510
524
  if (order.stop_loss or order.take_profit):
@@ -526,7 +540,9 @@ class Broker:
526
540
  self.position_avg_price = (old_pos * self.position_avg_price + order.quantity * price) / new_pos
527
541
  else:
528
542
  self.position_avg_price = price
529
- self._debit(self.source.COpen * order.quantity)
543
+ # Calculate margin (cash used) for leveraged positions
544
+ margin = self.source.COpen * order.quantity / self.leverage
545
+ self._debit(margin)
530
546
  self._apply_commission(order.quantity, self.source.COpen)
531
547
  self.position = new_pos
532
548
  else:
@@ -540,7 +556,9 @@ class Broker:
540
556
  self.position_avg_price = (old_pos * self.position_avg_price + order.quantity * price) / new_pos
541
557
  else:
542
558
  self.position_avg_price = price
543
- self._credit(self.source.COpen * order.quantity)
559
+ # Calculate margin released for leveraged positions
560
+ margin = self.source.COpen * order.quantity / self.leverage
561
+ self._credit(margin)
544
562
  self._apply_commission(order.quantity, self.source.COpen)
545
563
  self.position = new_pos
546
564
  if (order.stop_loss or order.take_profit):
@@ -604,13 +622,15 @@ class Broker:
604
622
  self.pending_close_order = None
605
623
  unrealized = self.position * self.source.CClose
606
624
  equity = self.cash + unrealized
607
- margin_call = self.margin_call * abs(self.position) * self.source.CClose
608
- if equity < margin_call and self.position < 0:
625
+ # Calculate actual margin used, accounting for leverage
626
+ actual_margin = abs(self.position) * self.source.CClose / self.leverage
627
+ margin_call_threshold = self.margin_call * actual_margin
628
+ if equity < margin_call_threshold and self.position < 0:
609
629
  self.margin_call_triggered = True
610
630
  self.margin_call_events.append({
611
631
  "timestamp": self.source.Index[self._i],
612
632
  "equity": equity,
613
- "margin_call_threshold": margin_call,
633
+ "margin_call_threshold": margin_call_threshold,
614
634
  "position": self.position,
615
635
  })
616
636
  self.close() ## Close all positions immediately, margin call
@@ -119,7 +119,9 @@ class Strategy(ABC):
119
119
  >>> class MyStrategy(Strategy):
120
120
  ... def init(self):
121
121
  ... self.add_data(CSVDataSource("AAPL.csv"), "AAPL")
122
- ... self.add_data(CSVDataSource("EURUSD.csv"), "EURUSD")
122
+ ... def next(self):
123
+ ... if self.data['AAPL'].CClose > self.data['AAPL'].Close[-1]:
124
+ ... self.positions['AAPL'].buy(quantity=0.1)
123
125
  """
124
126
  self.data[symbol] = source
125
127
  self.positions[symbol] = Broker(source)
File without changes
File without changes
File without changes
File without changes