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.
- {quantex-0.4.7 → quantex-0.4.8}/PKG-INFO +1 -1
- {quantex-0.4.7 → quantex-0.4.8}/pyproject.toml +1 -1
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/backtester.py +25 -2
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/montecarlo.py +4 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/parallel.py +5 -1
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/broker/broker.py +29 -9
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/strategy.py +3 -1
- {quantex-0.4.7 → quantex-0.4.8}/LICENSE.md +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/README.md +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/__init__.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/__init__.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/constants.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/data_splits.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/metrics.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/backtester/reports.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/broker/__init__.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/broker/types.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/datasource.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/helpers.py +0 -0
- {quantex-0.4.7 → quantex-0.4.8}/src/quantex/indicators.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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":
|
|
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
|
-
...
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|