quantex 0.4.4__tar.gz → 0.4.6__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.4 → quantex-0.4.6}/PKG-INFO +1 -1
- {quantex-0.4.4 → quantex-0.4.6}/pyproject.toml +2 -1
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/backtester.py +6 -1
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/montecarlo.py +148 -27
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/reports.py +9 -5
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/broker/broker.py +9 -0
- {quantex-0.4.4 → quantex-0.4.6}/LICENSE.md +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/README.md +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/__init__.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/__init__.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/constants.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/data_splits.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/metrics.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/backtester/parallel.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/broker/__init__.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/broker/types.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/datasource.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/helpers.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/indicators.py +0 -0
- {quantex-0.4.4 → quantex-0.4.6}/src/quantex/strategy.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quantex"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.6"
|
|
4
4
|
description = "A simple quant strategy creation and backtesting package."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Daniel Green",email = "dangreen07@outlook.com"}
|
|
@@ -39,6 +39,7 @@ mkdocs-mermaid2-plugin = "^1.2.1"
|
|
|
39
39
|
mkdocs-print-site-plugin = "^2.7.3"
|
|
40
40
|
pytest-xdist = "^3.8.0"
|
|
41
41
|
matplotlib = "^3.10.3"
|
|
42
|
+
ipykernel = "^7.2.0"
|
|
42
43
|
|
|
43
44
|
[build-system]
|
|
44
45
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
@@ -175,7 +175,12 @@ class SimpleBacktester:
|
|
|
175
175
|
final_cash=self.PnLRecord[-1],
|
|
176
176
|
PnlRecord=pd.Series(self.PnLRecord, index=index),
|
|
177
177
|
orders=orders,
|
|
178
|
-
tradeRecord=tradeRecord
|
|
178
|
+
tradeRecord=tradeRecord,
|
|
179
|
+
margin_call_events=[
|
|
180
|
+
event
|
|
181
|
+
for broker in self.strategy.positions.values()
|
|
182
|
+
for event in getattr(broker, "margin_call_events", [])
|
|
183
|
+
] or None)
|
|
179
184
|
|
|
180
185
|
def optimize(
|
|
181
186
|
self,
|
|
@@ -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
|
|
6
|
-
1. Trade Order Randomization - shuffles the sequence of executed trades
|
|
7
|
-
2.
|
|
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
|
|
@@ -12,6 +13,7 @@ import math
|
|
|
12
13
|
import random
|
|
13
14
|
import numpy as np
|
|
14
15
|
import pandas as pd
|
|
16
|
+
import matplotlib.dates as mdates
|
|
15
17
|
from dataclasses import dataclass, field
|
|
16
18
|
from enum import Enum
|
|
17
19
|
from typing import Any
|
|
@@ -33,6 +35,7 @@ class MonteCarloMode(Enum):
|
|
|
33
35
|
BOTH: Run both analyses and combine results
|
|
34
36
|
"""
|
|
35
37
|
TRADE_ORDER = "trade_order"
|
|
38
|
+
TRADE_SHUFFLE_WITH_REPLACEMENT = "trade_shuffle_with_replacement"
|
|
36
39
|
PRICE_PATH = "price_path"
|
|
37
40
|
BOTH = "both"
|
|
38
41
|
|
|
@@ -62,6 +65,7 @@ class MonteCarloResult:
|
|
|
62
65
|
simulations: int = 0
|
|
63
66
|
starting_cash: float = 0.0
|
|
64
67
|
drawdown_stats: dict = field(default_factory=dict)
|
|
68
|
+
plot_max_curves: int = 150
|
|
65
69
|
|
|
66
70
|
def _compute_statistics(self):
|
|
67
71
|
"""Compute summary statistics from equity curves."""
|
|
@@ -113,7 +117,7 @@ class MonteCarloResult:
|
|
|
113
117
|
self,
|
|
114
118
|
target_return: float,
|
|
115
119
|
drawdown_threshold: float,
|
|
116
|
-
horizon: int | None = None,
|
|
120
|
+
horizon: int | str | pd.Timedelta | None = None,
|
|
117
121
|
as_percent: bool = True,
|
|
118
122
|
) -> dict:
|
|
119
123
|
"""
|
|
@@ -125,8 +129,10 @@ class MonteCarloResult:
|
|
|
125
129
|
True, this is treated as a decimal return (e.g. 0.05 for 5%).
|
|
126
130
|
drawdown_threshold (float): Drawdown threshold. If `as_percent` is
|
|
127
131
|
True, this is treated as a decimal drawdown (e.g. 0.05 for 5%).
|
|
128
|
-
horizon (int | None, optional):
|
|
129
|
-
|
|
132
|
+
horizon (int | str | pd.Timedelta | None, optional): Evaluation horizon.
|
|
133
|
+
If an integer is provided, it is treated as a number of steps.
|
|
134
|
+
If a string or Timedelta is provided, it is treated as a time span
|
|
135
|
+
relative to the first timestamp in each equity curve.
|
|
130
136
|
as_percent (bool, optional): Whether thresholds are provided as
|
|
131
137
|
decimal percentages. Defaults to True.
|
|
132
138
|
|
|
@@ -142,8 +148,26 @@ class MonteCarloResult:
|
|
|
142
148
|
"drawdown_threshold": drawdown_threshold,
|
|
143
149
|
}
|
|
144
150
|
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
def _resolve_horizon(curve: pd.Series, horizon_value: int | str | pd.Timedelta | None) -> int:
|
|
152
|
+
if horizon_value is None:
|
|
153
|
+
return len(curve)
|
|
154
|
+
if isinstance(horizon_value, (int, np.integer)):
|
|
155
|
+
return max(1, min(int(horizon_value), len(curve)))
|
|
156
|
+
|
|
157
|
+
if not isinstance(curve.index, pd.DatetimeIndex):
|
|
158
|
+
return max(1, min(len(curve), len(curve)))
|
|
159
|
+
|
|
160
|
+
delta = pd.Timedelta(horizon_value)
|
|
161
|
+
if delta <= pd.Timedelta(0):
|
|
162
|
+
return 1
|
|
163
|
+
|
|
164
|
+
start_time = curve.index[0]
|
|
165
|
+
end_time = start_time + delta
|
|
166
|
+
resolved = int(curve.index.searchsorted(end_time, side="right"))
|
|
167
|
+
return max(1, min(resolved, len(curve)))
|
|
168
|
+
|
|
169
|
+
first_curve = self.equity_curves[0]
|
|
170
|
+
horizon_steps = _resolve_horizon(first_curve, horizon)
|
|
147
171
|
|
|
148
172
|
if as_percent:
|
|
149
173
|
target_return = float(target_return)
|
|
@@ -152,7 +176,7 @@ class MonteCarloResult:
|
|
|
152
176
|
return_hits = 0
|
|
153
177
|
drawdown_hits = 0
|
|
154
178
|
for curve in self.equity_curves:
|
|
155
|
-
sampled = curve.iloc[:
|
|
179
|
+
sampled = curve.iloc[:horizon_steps]
|
|
156
180
|
start_value = float(sampled.iloc[0])
|
|
157
181
|
end_value = float(sampled.iloc[-1])
|
|
158
182
|
achieved_return = (end_value / start_value) - 1.0 if start_value != 0 else 0.0
|
|
@@ -173,7 +197,7 @@ class MonteCarloResult:
|
|
|
173
197
|
}
|
|
174
198
|
|
|
175
199
|
def plot(self, figsize: tuple = (12, 8), show_original: bool = True,
|
|
176
|
-
show_percentiles: bool = True) -> None:
|
|
200
|
+
show_percentiles: bool = True, max_curves: int | None = None) -> None:
|
|
177
201
|
"""
|
|
178
202
|
Plot all Monte Carlo simulation equity curves.
|
|
179
203
|
|
|
@@ -188,6 +212,8 @@ class MonteCarloResult:
|
|
|
188
212
|
curve. Defaults to True.
|
|
189
213
|
show_percentiles (bool, optional): Whether to show percentile bands.
|
|
190
214
|
Defaults to True.
|
|
215
|
+
max_curves (int | None, optional): Maximum number of simulation curves
|
|
216
|
+
to render. Defaults to ``self.plot_max_curves``.
|
|
191
217
|
|
|
192
218
|
Note:
|
|
193
219
|
This method uses matplotlib to display the plots and requires
|
|
@@ -196,11 +222,13 @@ class MonteCarloResult:
|
|
|
196
222
|
from matplotlib import pyplot as plt
|
|
197
223
|
|
|
198
224
|
fig, ax = plt.subplots(figsize=figsize)
|
|
225
|
+
|
|
226
|
+
max_curves = self.plot_max_curves if max_curves is None else max_curves
|
|
199
227
|
|
|
200
|
-
# Plot
|
|
201
|
-
#
|
|
228
|
+
# Plot against the datetime index so the x-axis reflects the actual
|
|
229
|
+
# backtest timeline instead of a generic simulation step axis.
|
|
202
230
|
if not self.equity_curves:
|
|
203
|
-
ax.set_xlabel("
|
|
231
|
+
ax.set_xlabel("Datetime")
|
|
204
232
|
ax.set_ylabel("Portfolio Value")
|
|
205
233
|
ax.set_title(f"Monte Carlo Simulation Results ({self.simulations} simulations)")
|
|
206
234
|
ax.grid(alpha=0.3)
|
|
@@ -208,14 +236,35 @@ class MonteCarloResult:
|
|
|
208
236
|
plt.show()
|
|
209
237
|
return
|
|
210
238
|
|
|
211
|
-
|
|
239
|
+
base_index = self.equity_curves[0].index
|
|
240
|
+
if not isinstance(base_index, pd.DatetimeIndex):
|
|
241
|
+
base_index = pd.to_datetime(base_index)
|
|
242
|
+
|
|
243
|
+
def _plot_x_values(curve: pd.Series) -> pd.Index:
|
|
244
|
+
if isinstance(curve.index, pd.DatetimeIndex):
|
|
245
|
+
return curve.index
|
|
246
|
+
return pd.to_datetime(curve.index)
|
|
212
247
|
|
|
213
248
|
# Plot all simulation curves with low alpha (transparency)
|
|
214
249
|
# This makes the average path appear lightest due to overlap
|
|
215
|
-
|
|
216
|
-
|
|
250
|
+
color_cycle = plt.rcParams["axes.prop_cycle"].by_key().get("color", ["steelblue"])
|
|
251
|
+
curve_count = len(self.equity_curves)
|
|
252
|
+
if max_curves is not None and max_curves > 0 and curve_count > max_curves:
|
|
253
|
+
plot_indices = np.linspace(0, curve_count - 1, max_curves, dtype=int)
|
|
254
|
+
else:
|
|
255
|
+
plot_indices = range(curve_count)
|
|
256
|
+
|
|
257
|
+
for i in plot_indices:
|
|
258
|
+
curve = self.equity_curves[i]
|
|
259
|
+
x_vals = _plot_x_values(curve)
|
|
217
260
|
y_vals = np.asarray(curve.values, dtype=np.float64)
|
|
218
|
-
ax.plot(
|
|
261
|
+
ax.plot(
|
|
262
|
+
x_vals,
|
|
263
|
+
y_vals,
|
|
264
|
+
color=color_cycle[i % len(color_cycle)],
|
|
265
|
+
alpha=0.08,
|
|
266
|
+
linewidth=0.5,
|
|
267
|
+
)
|
|
219
268
|
|
|
220
269
|
# Compute mean and median curves for highlighting
|
|
221
270
|
if self.equity_curves:
|
|
@@ -225,18 +274,20 @@ class MonteCarloResult:
|
|
|
225
274
|
median_curve = aligned.median(axis=1)
|
|
226
275
|
|
|
227
276
|
# Plot mean curve (thicker, lighter)
|
|
228
|
-
x_mean =
|
|
277
|
+
x_mean = base_index
|
|
229
278
|
y_mean = np.asarray(mean_curve.values, dtype=np.float64)
|
|
230
279
|
ax.plot(x_mean, y_mean, color="darkblue", alpha=0.8, linewidth=2, label="Mean")
|
|
231
280
|
|
|
232
281
|
# Plot median curve
|
|
233
|
-
x_med =
|
|
282
|
+
x_med = base_index
|
|
234
283
|
y_med = np.asarray(median_curve.values, dtype=np.float64)
|
|
235
284
|
ax.plot(x_med, y_med, color="navy", alpha=0.6, linewidth=1.5, linestyle="--", label="Median")
|
|
236
285
|
|
|
237
286
|
# Show original equity curve if requested
|
|
238
287
|
if show_original and self.original_equity is not None:
|
|
239
|
-
x_orig =
|
|
288
|
+
x_orig = self.original_equity.index
|
|
289
|
+
if not isinstance(x_orig, pd.DatetimeIndex):
|
|
290
|
+
x_orig = pd.to_datetime(x_orig)
|
|
240
291
|
y_orig = np.asarray(self.original_equity.values, dtype=np.float64)
|
|
241
292
|
ax.plot(x_orig, y_orig, color="red", alpha=0.9, linewidth=2, label="Original Backtest")
|
|
242
293
|
|
|
@@ -245,19 +296,20 @@ class MonteCarloResult:
|
|
|
245
296
|
aligned = pd.concat(self.equity_curves, axis=1)
|
|
246
297
|
p5 = aligned.quantile(0.05, axis=1)
|
|
247
298
|
p95 = aligned.quantile(0.95, axis=1)
|
|
248
|
-
x_p5 =
|
|
299
|
+
x_p5 = p5.index
|
|
300
|
+
if not isinstance(x_p5, pd.DatetimeIndex):
|
|
301
|
+
x_p5 = pd.to_datetime(x_p5)
|
|
249
302
|
y_p5 = np.asarray(p5.values, dtype=np.float64)
|
|
250
303
|
y_p95 = np.asarray(p95.values, dtype=np.float64)
|
|
251
304
|
ax.fill_between(x_p5, y_p5, y_p95, alpha=0.2, color="steelblue", label="5th-95th Percentile")
|
|
252
305
|
|
|
253
|
-
ax.set_xlabel("
|
|
306
|
+
ax.set_xlabel("Datetime")
|
|
254
307
|
ax.set_ylabel("Portfolio Value")
|
|
255
308
|
ax.set_title(f"Monte Carlo Simulation Results ({self.simulations} simulations)")
|
|
256
309
|
ax.legend(loc="best")
|
|
257
310
|
ax.grid(alpha=0.3)
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
ax.set_xlim(step_index[0], step_index[-1])
|
|
311
|
+
ax.xaxis.set_major_formatter(mdates.DateFormatter("%Y-%m"))
|
|
312
|
+
fig.autofmt_xdate()
|
|
261
313
|
|
|
262
314
|
plt.tight_layout()
|
|
263
315
|
plt.show()
|
|
@@ -358,6 +410,51 @@ def _run_trade_order_simulation(
|
|
|
358
410
|
return pd.Series(equity, index=index)
|
|
359
411
|
|
|
360
412
|
|
|
413
|
+
def _run_trade_shuffle_with_replacement_simulation(
|
|
414
|
+
original_orders: list[Order],
|
|
415
|
+
original_cash: float,
|
|
416
|
+
original_equity: pd.Series,
|
|
417
|
+
commission: float,
|
|
418
|
+
commission_type,
|
|
419
|
+
lot_size: int,
|
|
420
|
+
seed: int | None = None,
|
|
421
|
+
) -> pd.Series:
|
|
422
|
+
"""
|
|
423
|
+
Run a Monte Carlo simulation that samples trade outcomes with replacement.
|
|
424
|
+
|
|
425
|
+
This mode reuses the original trade-return sequence as a return pool and
|
|
426
|
+
reconstructs the curve using a replacement-sampled path. Any remaining
|
|
427
|
+
steps are kept neutral so the resulting equity curve always spans the same
|
|
428
|
+
time axis as the original backtest.
|
|
429
|
+
"""
|
|
430
|
+
if seed is not None:
|
|
431
|
+
random.seed(seed)
|
|
432
|
+
|
|
433
|
+
index = original_equity.index
|
|
434
|
+
equity = np.full(len(index), original_cash, dtype=np.float64)
|
|
435
|
+
|
|
436
|
+
equity_values = np.asarray(original_equity.values, dtype=np.float64)
|
|
437
|
+
equity_returns = np.zeros_like(equity_values)
|
|
438
|
+
if len(equity_values) > 1:
|
|
439
|
+
prev = np.empty_like(equity_values)
|
|
440
|
+
prev[0] = original_cash
|
|
441
|
+
prev[1:] = equity_values[:-1]
|
|
442
|
+
equity_returns[1:] = np.where(prev[1:] > 0, (equity_values[1:] / prev[1:]) - 1.0, 0.0)
|
|
443
|
+
|
|
444
|
+
trade_returns = equity_returns[1:].tolist()
|
|
445
|
+
if not trade_returns:
|
|
446
|
+
return pd.Series(equity, index=index)
|
|
447
|
+
|
|
448
|
+
sampled_returns = [random.choice(trade_returns) for _ in range(len(trade_returns))]
|
|
449
|
+
|
|
450
|
+
equity_returns = np.concatenate(([0.0], np.asarray(sampled_returns, dtype=np.float64)))
|
|
451
|
+
|
|
452
|
+
for i in range(1, len(equity)):
|
|
453
|
+
equity[i] = equity[i - 1] * (1.0 + equity_returns[i])
|
|
454
|
+
|
|
455
|
+
return pd.Series(equity, index=index)
|
|
456
|
+
|
|
457
|
+
|
|
361
458
|
def _run_price_path_simulation(
|
|
362
459
|
strategy: Strategy,
|
|
363
460
|
data_sources: dict[str, DataSource],
|
|
@@ -511,7 +608,7 @@ def monte_carlo(
|
|
|
511
608
|
- "trade_order": Randomize trade execution order
|
|
512
609
|
- "price_path": Resample price returns to create synthetic paths
|
|
513
610
|
- "both": Run both analyses and combine results
|
|
514
|
-
Defaults to "
|
|
611
|
+
Defaults to "trade_order".
|
|
515
612
|
seed (int | None, optional): Random seed for reproducibility.
|
|
516
613
|
Defaults to None.
|
|
517
614
|
progress_bar (bool, optional): Whether to show progress bar during simulation.
|
|
@@ -583,6 +680,21 @@ def monte_carlo(
|
|
|
583
680
|
# No trades, just return original equity
|
|
584
681
|
curve = original_equity.copy()
|
|
585
682
|
equity_curves.append(curve)
|
|
683
|
+
|
|
684
|
+
if mode == MonteCarloMode.TRADE_SHUFFLE_WITH_REPLACEMENT:
|
|
685
|
+
if len(original_orders) > 0:
|
|
686
|
+
curve = _run_trade_shuffle_with_replacement_simulation(
|
|
687
|
+
original_orders,
|
|
688
|
+
original_cash,
|
|
689
|
+
original_equity,
|
|
690
|
+
self.commission,
|
|
691
|
+
self.commission_type,
|
|
692
|
+
self.lot_size,
|
|
693
|
+
seed=iter_seed,
|
|
694
|
+
)
|
|
695
|
+
else:
|
|
696
|
+
curve = original_equity.copy()
|
|
697
|
+
equity_curves.append(curve)
|
|
586
698
|
|
|
587
699
|
if mode == MonteCarloMode.PRICE_PATH or mode == MonteCarloMode.BOTH:
|
|
588
700
|
# Price path resampling
|
|
@@ -637,6 +749,15 @@ def monte_carlo(
|
|
|
637
749
|
"trade_order": trade_result.percentile_results,
|
|
638
750
|
"price_path": price_result.percentile_results,
|
|
639
751
|
}
|
|
752
|
+
elif mode == MonteCarloMode.TRADE_SHUFFLE_WITH_REPLACEMENT:
|
|
753
|
+
result = MonteCarloResult(
|
|
754
|
+
mode=mode,
|
|
755
|
+
equity_curves=equity_curves,
|
|
756
|
+
original_equity=original_equity,
|
|
757
|
+
simulations=simulations,
|
|
758
|
+
starting_cash=original_cash,
|
|
759
|
+
)
|
|
760
|
+
result._compute_statistics()
|
|
640
761
|
else:
|
|
641
762
|
# Create result object
|
|
642
763
|
result = MonteCarloResult(
|
|
@@ -46,16 +46,18 @@ class BacktestReport:
|
|
|
46
46
|
metrics such as Sharpe ratio and maximum drawdown.
|
|
47
47
|
|
|
48
48
|
Attributes:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
starting_cash (np.float64): Initial cash amount at start of backtest.
|
|
50
|
+
final_cash (np.float64): Final cash amount at end of backtest.
|
|
51
|
+
PnlRecord (pd.Series): Time series of P&L values throughout the backtest.
|
|
52
|
+
orders (list[Order]): List of all orders executed during the backtest.
|
|
53
|
+
margin_call_events (list[dict]): Margin call events triggered during the run.
|
|
53
54
|
"""
|
|
54
55
|
starting_cash: np.float64
|
|
55
56
|
final_cash: np.float64
|
|
56
57
|
PnlRecord: pd.Series
|
|
57
58
|
orders: list
|
|
58
59
|
tradeRecord: list[np.float64]
|
|
60
|
+
margin_call_events: list[dict] | None = None
|
|
59
61
|
|
|
60
62
|
@property
|
|
61
63
|
def annual_rf(self):
|
|
@@ -201,6 +203,7 @@ class BacktestReport:
|
|
|
201
203
|
tot_return = float(equity.iloc[-1] / equity.iloc[0] - 1.0)
|
|
202
204
|
annualized_return = float((1.0 + tot_return) ** (self.periods_per_year / max(len(returns), 1)) - 1.0)
|
|
203
205
|
tot_orders = len(self.orders)
|
|
206
|
+
margin_calls = len(self.margin_call_events or [])
|
|
204
207
|
|
|
205
208
|
return (
|
|
206
209
|
f"Starting Cash: ${self.starting_cash:,.2f}\n"
|
|
@@ -215,5 +218,6 @@ class BacktestReport:
|
|
|
215
218
|
) + (
|
|
216
219
|
f"\nMax Drawdown: {mdd:.2%}\n"
|
|
217
220
|
f"Kelly Fraction: {self.kelly_criterion:.3}\n"
|
|
218
|
-
f"Total Trades: {tot_orders:,}"
|
|
221
|
+
f"Total Trades: {tot_orders:,}\n"
|
|
222
|
+
f"Margin Calls: {margin_calls:,}"
|
|
219
223
|
)
|
|
@@ -79,6 +79,8 @@ class Broker:
|
|
|
79
79
|
self.complete_orders = []
|
|
80
80
|
self.active_order: Order | None = None
|
|
81
81
|
self.pending_close_order: Order | None = None
|
|
82
|
+
self.margin_call_triggered: bool = False
|
|
83
|
+
self.margin_call_events: list[dict] = []
|
|
82
84
|
self._i = 0
|
|
83
85
|
self.source = source
|
|
84
86
|
self.PnLRecord = np.full(len(self.source.data['Close']), self.cash, dtype=np.float64)
|
|
@@ -604,5 +606,12 @@ class Broker:
|
|
|
604
606
|
equity = self.cash + unrealized
|
|
605
607
|
margin_call = self.margin_call * abs(self.position) * self.source.CClose
|
|
606
608
|
if equity < margin_call and self.position < 0:
|
|
609
|
+
self.margin_call_triggered = True
|
|
610
|
+
self.margin_call_events.append({
|
|
611
|
+
"timestamp": self.source.Index[self._i],
|
|
612
|
+
"equity": equity,
|
|
613
|
+
"margin_call_threshold": margin_call,
|
|
614
|
+
"position": self.position,
|
|
615
|
+
})
|
|
607
616
|
self.close() ## Close all positions immediately, margin call
|
|
608
617
|
self.PnLRecord[self._i] = equity
|
|
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
|
|
File without changes
|