quantex 0.4.6__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.6 → quantex-0.4.8}/PKG-INFO +2 -1
- {quantex-0.4.6 → quantex-0.4.8}/pyproject.toml +2 -1
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/backtester.py +340 -23
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/montecarlo.py +4 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/parallel.py +92 -28
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/broker/broker.py +29 -9
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/strategy.py +3 -1
- {quantex-0.4.6 → quantex-0.4.8}/LICENSE.md +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/README.md +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/__init__.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/__init__.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/constants.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/data_splits.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/metrics.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/backtester/reports.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/broker/__init__.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/broker/types.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/datasource.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/helpers.py +0 -0
- {quantex-0.4.6 → quantex-0.4.8}/src/quantex/indicators.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: quantex
|
|
3
|
-
Version: 0.4.
|
|
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
|
|
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
14
|
Requires-Dist: fastparquet (>=2024.11.0,<2025.0.0)
|
|
15
15
|
Requires-Dist: numpy (>=2.4.3,<3.0.0)
|
|
16
|
+
Requires-Dist: optuna (>=4.8.0,<5.0.0)
|
|
16
17
|
Requires-Dist: pandas (>=2.3.0,<3.0.0)
|
|
17
18
|
Requires-Dist: pyarrow (>=20.0.0,<21.0.0)
|
|
18
19
|
Requires-Dist: tqdm (>=4.67.1,<5.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "quantex"
|
|
3
|
-
version = "0.4.
|
|
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"}
|
|
@@ -14,6 +14,7 @@ dependencies = [
|
|
|
14
14
|
"pyarrow (>=20.0.0,<21.0.0)",
|
|
15
15
|
"tqdm (>=4.67.1,<5.0.0)",
|
|
16
16
|
"numpy (>=2.4.3,<3.0.0)",
|
|
17
|
+
"optuna (>=4.8.0,<5.0.0)",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
[tool.poetry]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import itertools
|
|
3
|
+
import math
|
|
3
4
|
import os
|
|
4
5
|
from typing import Any, Callable
|
|
5
6
|
|
|
@@ -37,6 +38,7 @@ class SimpleBacktester:
|
|
|
37
38
|
- Position management
|
|
38
39
|
- Margin calls
|
|
39
40
|
- P&L tracking
|
|
41
|
+
- Leverage for amplified position sizing
|
|
40
42
|
|
|
41
43
|
Example:
|
|
42
44
|
>>> from quantex import SimpleBacktester, CSVDataSource
|
|
@@ -53,7 +55,8 @@ class SimpleBacktester:
|
|
|
53
55
|
commission: float = 0.002,
|
|
54
56
|
commission_type: CommissionType = CommissionType.PERCENTAGE,
|
|
55
57
|
lot_size: int = 1,
|
|
56
|
-
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)
|
|
57
60
|
):
|
|
58
61
|
"""
|
|
59
62
|
Initialize the backtester with strategy and configuration parameters.
|
|
@@ -69,16 +72,25 @@ class SimpleBacktester:
|
|
|
69
72
|
lot_size (int, optional): Size of trading lots. Defaults to 1.
|
|
70
73
|
margin_call (float, optional): Margin call threshold as fraction of
|
|
71
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)
|
|
72
79
|
|
|
73
80
|
Raises:
|
|
74
81
|
ValueError: If strategy is None or commission rate is negative.
|
|
75
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")
|
|
76
87
|
self.strategy = copy.deepcopy(strategy)
|
|
77
88
|
self.cash = cash
|
|
78
89
|
self.commission = commission
|
|
79
90
|
self.commission_type = commission_type
|
|
80
91
|
self.lot_size = lot_size
|
|
81
92
|
self.margin_call = margin_call
|
|
93
|
+
self.leverage = leverage
|
|
82
94
|
source = self.strategy.positions[list(self.strategy.positions.keys())[0]].source
|
|
83
95
|
self.PnLRecord = np.zeros(len(source.data['Close']), dtype=np.float64)
|
|
84
96
|
|
|
@@ -139,6 +151,7 @@ class SimpleBacktester:
|
|
|
139
151
|
broker.cash = per_position_cash
|
|
140
152
|
broker.lot_size = self.lot_size
|
|
141
153
|
broker.margin_call = self.margin_call
|
|
154
|
+
broker.leverage = self.leverage
|
|
142
155
|
broker.commision = np.float64(self.commission)
|
|
143
156
|
broker.commision_type = self.commission_type
|
|
144
157
|
|
|
@@ -272,7 +285,9 @@ class SimpleBacktester:
|
|
|
272
285
|
|
|
273
286
|
valid_metrics = {"final_cash", "total_return", "sharpe", "max_drawdown", "trades"}
|
|
274
287
|
|
|
275
|
-
|
|
288
|
+
# Use math.prod instead of len(list(itertools.product(...)))
|
|
289
|
+
# to avoid materializing all combinations in memory
|
|
290
|
+
total_combos = math.prod(len(v) for v in value_lists)
|
|
276
291
|
|
|
277
292
|
for combo in tqdm(itertools.product(*value_lists), total=(total_combos)):
|
|
278
293
|
# Build parameter dict for this combo
|
|
@@ -299,6 +314,7 @@ class SimpleBacktester:
|
|
|
299
314
|
commission=self.commission,
|
|
300
315
|
commission_type=self.commission_type,
|
|
301
316
|
lot_size=self.lot_size,
|
|
317
|
+
leverage=self.leverage,
|
|
302
318
|
)
|
|
303
319
|
report = bt.run(progress_bar=False)
|
|
304
320
|
|
|
@@ -364,7 +380,7 @@ class SimpleBacktester:
|
|
|
364
380
|
objective: str = "sharpe",
|
|
365
381
|
risk_tolerance: dict[str, float] | None = None,
|
|
366
382
|
workers: int | None = None,
|
|
367
|
-
chunksize: int =
|
|
383
|
+
chunksize: int | str = "auto") -> OptimizationResult:
|
|
368
384
|
"""
|
|
369
385
|
Perform parallel grid search over parameter ranges for optimization.
|
|
370
386
|
|
|
@@ -386,10 +402,12 @@ class SimpleBacktester:
|
|
|
386
402
|
workers (int | None, optional): Maximum number of worker processes to use.
|
|
387
403
|
If None, defaults to min(os.cpu_count()-1, 4) to avoid overwhelming
|
|
388
404
|
the system. Defaults to None.
|
|
389
|
-
chunksize (int, optional): Chunk size for ProcessPoolExecutor.map.
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
405
|
+
chunksize (int | str, optional): Chunk size for ProcessPoolExecutor.map.
|
|
406
|
+
Can be an integer or "auto" for adaptive sizing based on total
|
|
407
|
+
combinations and worker count. Smaller values provide better load
|
|
408
|
+
balancing for many small tasks. Larger values reduce IPC overhead.
|
|
409
|
+
Defaults to "auto" (previously 1).
|
|
410
|
+
Auto-calculation: max(16, total_combos // (workers * 4))
|
|
393
411
|
|
|
394
412
|
Returns:
|
|
395
413
|
OptimizationResult: Object containing:
|
|
@@ -422,6 +440,7 @@ class SimpleBacktester:
|
|
|
422
440
|
lower multiprocessing overhead.
|
|
423
441
|
- Monitor system memory usage as each worker maintains a full
|
|
424
442
|
copy of the strategy and data.
|
|
443
|
+
- Auto chunksize provides better throughput for large parameter spaces.
|
|
425
444
|
|
|
426
445
|
Example:
|
|
427
446
|
>>> bt = SimpleBacktester(strategy)
|
|
@@ -433,13 +452,13 @@ class SimpleBacktester:
|
|
|
433
452
|
"""
|
|
434
453
|
import concurrent.futures
|
|
435
454
|
import pickle
|
|
455
|
+
import math
|
|
436
456
|
|
|
437
457
|
if not params:
|
|
438
458
|
raise ValueError("params must not be empty")
|
|
439
459
|
|
|
440
460
|
keys = list(params.keys())
|
|
441
461
|
value_lists = []
|
|
442
|
-
lens = []
|
|
443
462
|
for k in keys:
|
|
444
463
|
vals = params[k]
|
|
445
464
|
try:
|
|
@@ -449,14 +468,28 @@ class SimpleBacktester:
|
|
|
449
468
|
if len(candidates) == 0:
|
|
450
469
|
raise ValueError(f"Parameter '{k}' has no candidate values")
|
|
451
470
|
value_lists.append(candidates)
|
|
452
|
-
lens.append(len(candidates))
|
|
453
471
|
|
|
454
|
-
# determine total combos without materializing them
|
|
455
|
-
|
|
456
|
-
for
|
|
457
|
-
|
|
472
|
+
# determine total combos without materializing them using math.prod
|
|
473
|
+
# (previously used len(list(itertools.product(...))) which materialized all combos)
|
|
474
|
+
total_combos = math.prod(len(v) for v in value_lists)
|
|
475
|
+
|
|
476
|
+
# choose worker count conservatively to avoid RAM hogging
|
|
477
|
+
cpu_count = os.cpu_count() or 1
|
|
478
|
+
if workers is None:
|
|
479
|
+
workers = max(1, min(cpu_count - 1, 4))
|
|
480
|
+
else:
|
|
481
|
+
workers = max(1, int(workers))
|
|
482
|
+
|
|
483
|
+
# Adaptive chunksize calculation
|
|
484
|
+
# Previous default was chunksize=1 which causes high IPC overhead
|
|
485
|
+
# New default "auto" uses: max(16, total_combos // (workers * 4))
|
|
486
|
+
if chunksize == "auto":
|
|
487
|
+
chunksize = max(16, total_combos // (workers * 4))
|
|
488
|
+
else:
|
|
489
|
+
chunksize = max(1, int(chunksize))
|
|
458
490
|
|
|
459
491
|
# prepare iterable of param dicts as sequences of items (so pickling is slightly cheaper)
|
|
492
|
+
# Also pre-compute constraint results to avoid repeated checks
|
|
460
493
|
def _param_items_iter():
|
|
461
494
|
for combo in itertools.product(*value_lists):
|
|
462
495
|
row_params = {k: v for k, v in zip(keys, combo)}
|
|
@@ -469,13 +502,6 @@ class SimpleBacktester:
|
|
|
469
502
|
# yield as tuple of items for stable order and smaller IPC
|
|
470
503
|
yield tuple(row_params.items())
|
|
471
504
|
|
|
472
|
-
# choose worker count conservatively to avoid RAM hogging
|
|
473
|
-
cpu_count = os.cpu_count() or 1
|
|
474
|
-
if workers is None:
|
|
475
|
-
workers = max(1, min(cpu_count - 1, 4))
|
|
476
|
-
else:
|
|
477
|
-
workers = max(1, int(workers))
|
|
478
|
-
|
|
479
505
|
# pickle the base strategy once and send bytes to worker initializer
|
|
480
506
|
pickled_strategy = pickle.dumps(self.strategy)
|
|
481
507
|
|
|
@@ -491,6 +517,7 @@ class SimpleBacktester:
|
|
|
491
517
|
self.commission,
|
|
492
518
|
self.commission_type,
|
|
493
519
|
self.lot_size,
|
|
520
|
+
self.leverage,
|
|
494
521
|
),
|
|
495
522
|
) as exe:
|
|
496
523
|
# map the worker over param item tuples
|
|
@@ -548,6 +575,7 @@ class SimpleBacktester:
|
|
|
548
575
|
commission=self.commission,
|
|
549
576
|
commission_type=self.commission_type,
|
|
550
577
|
lot_size=self.lot_size,
|
|
578
|
+
leverage=self.leverage,
|
|
551
579
|
)
|
|
552
580
|
best_report = bt.run(progress_bar=False)
|
|
553
581
|
|
|
@@ -562,7 +590,291 @@ class SimpleBacktester:
|
|
|
562
590
|
train_metrics=best_metrics,
|
|
563
591
|
validate_metrics={},
|
|
564
592
|
test_metrics={},
|
|
565
|
-
|
|
593
|
+
all_results=results_df
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
def optimize_optuna(
|
|
597
|
+
self,
|
|
598
|
+
param_space: dict[str, tuple[Any, Any] | list[Any]],
|
|
599
|
+
n_trials: int = 100,
|
|
600
|
+
objective: str = "sharpe",
|
|
601
|
+
risk_tolerance: dict[str, float] | None = None,
|
|
602
|
+
constraint: Callable[[dict[str, Any]], bool] | None = None,
|
|
603
|
+
timeout: int | None = None,
|
|
604
|
+
random_seed: int | None = None,
|
|
605
|
+
workers: int | None = None,
|
|
606
|
+
progress_bar: bool = True,
|
|
607
|
+
) -> OptimizationResult:
|
|
608
|
+
"""
|
|
609
|
+
Optimize strategy parameters using Optuna (Bayesian optimization).
|
|
610
|
+
|
|
611
|
+
This method uses Optuna's optimization framework with TPE (Tree-structured
|
|
612
|
+
Parzen Estimator) sampler for intelligent parameter search. It typically
|
|
613
|
+
finds better solutions than grid search with fewer evaluations.
|
|
614
|
+
|
|
615
|
+
The method supports:
|
|
616
|
+
- Continuous parameter ranges (sampled uniformly)
|
|
617
|
+
- Discrete/categorical parameter lists
|
|
618
|
+
- Early pruning of unpromising trials
|
|
619
|
+
- Parallel execution for faster optimization
|
|
620
|
+
|
|
621
|
+
Args:
|
|
622
|
+
param_space (dict[str, tuple[Any, Any] | list[Any]]): Parameter search space.
|
|
623
|
+
Can be:
|
|
624
|
+
- Continuous range: (min, max) tuple for uniform sampling
|
|
625
|
+
- Discrete list: [val1, val2, ...] for categorical sampling
|
|
626
|
+
Example: {'period': (5, 50), 'threshold': [0.01, 0.02, 0.05]}
|
|
627
|
+
n_trials (int, optional): Maximum number of optimization trials.
|
|
628
|
+
Defaults to 100.
|
|
629
|
+
objective (str, optional): Metric to optimize. Defaults to "sharpe".
|
|
630
|
+
Supports: "final_cash", "total_return", "sharpe", "max_drawdown", "trades".
|
|
631
|
+
risk_tolerance (dict[str, float] | None, optional): Maximum allowed values
|
|
632
|
+
for risk metrics. Trials exceeding thresholds are pruned. Defaults to None.
|
|
633
|
+
constraint (Callable[[dict[str, Any]], bool] | None, optional): Optional
|
|
634
|
+
callable to enforce parameter constraints. Defaults to None.
|
|
635
|
+
timeout (int | None, optional): Maximum time in seconds for optimization.
|
|
636
|
+
Defaults to None (no limit).
|
|
637
|
+
random_seed (int | None, optional): Random seed for reproducibility.
|
|
638
|
+
Defaults to None.
|
|
639
|
+
workers (int | None, optional): Number of parallel workers for Optuna
|
|
640
|
+
study. Defaults to None (sequential).
|
|
641
|
+
progress_bar (bool, optional): Whether to show progress bar. Defaults to True.
|
|
642
|
+
|
|
643
|
+
Returns:
|
|
644
|
+
OptimizationResult: Object containing:
|
|
645
|
+
- best_params: Best parameter values found
|
|
646
|
+
- train_report: BacktestReport for best parameters (None for Optuna)
|
|
647
|
+
- validate_report: None
|
|
648
|
+
- test_report: None
|
|
649
|
+
- train_metrics: Metrics for best parameters
|
|
650
|
+
- validate_metrics: Empty dict
|
|
651
|
+
- test_metrics: Empty dict
|
|
652
|
+
- all_results: DataFrame with all trial results
|
|
653
|
+
|
|
654
|
+
Performance Notes:
|
|
655
|
+
- Optuna typically finds good solutions in 50-200 trials
|
|
656
|
+
- For 10,000+ grid combos, Optuna can be 50-100x faster
|
|
657
|
+
- Use workers > 1 for parallel trial evaluation
|
|
658
|
+
- Pruning callbacks significantly speed up optimization
|
|
659
|
+
|
|
660
|
+
Example:
|
|
661
|
+
>>> # Optimize with continuous and discrete parameters
|
|
662
|
+
>>> result = bt.optimize_optuna({
|
|
663
|
+
... 'fast_period': (5, 50), # Continuous: 5-50
|
|
664
|
+
... 'slow_period': [20, 30, 50], # Discrete: pick one
|
|
665
|
+
... 'threshold': (0.01, 0.1), # Continuous: 1%-10%
|
|
666
|
+
... }, n_trials=100)
|
|
667
|
+
>>> print(f"Best params: {result.best_params}")
|
|
668
|
+
>>> print(f"Best Sharpe: {result.train_metrics['sharpe']}")
|
|
669
|
+
|
|
670
|
+
Note:
|
|
671
|
+
Requires optuna package: pip install optuna
|
|
672
|
+
"""
|
|
673
|
+
try:
|
|
674
|
+
import optuna
|
|
675
|
+
except ImportError:
|
|
676
|
+
raise ImportError(
|
|
677
|
+
"optuna is required for optimize_optuna. "
|
|
678
|
+
"Install it with: pip install optuna"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Check for invalid objective
|
|
682
|
+
valid_metrics = {"final_cash", "total_return", "sharpe", "max_drawdown", "trades"}
|
|
683
|
+
if objective not in valid_metrics:
|
|
684
|
+
raise ValueError(
|
|
685
|
+
f"objective must be one of {valid_metrics}, got '{objective}'"
|
|
686
|
+
)
|
|
687
|
+
|
|
688
|
+
# Convert param_space to Optuna distribution format
|
|
689
|
+
param_names = list(param_space.keys())
|
|
690
|
+
|
|
691
|
+
def _create_objective(
|
|
692
|
+
strategy_template: Strategy,
|
|
693
|
+
cash: float,
|
|
694
|
+
commission: float,
|
|
695
|
+
commission_type: CommissionType,
|
|
696
|
+
lot_size: int,
|
|
697
|
+
leverage: float,
|
|
698
|
+
objective: str,
|
|
699
|
+
risk_tolerance: dict[str, float] | None,
|
|
700
|
+
constraint: Callable[[dict[str, Any]], bool] | None,
|
|
701
|
+
):
|
|
702
|
+
"""Create objective function for Optuna."""
|
|
703
|
+
|
|
704
|
+
def objective_fn(trial: optuna.Trial) -> float:
|
|
705
|
+
# Sample parameters based on space definition
|
|
706
|
+
params = {}
|
|
707
|
+
for name, space in param_space.items():
|
|
708
|
+
if isinstance(space, (list, tuple)) and len(space) == 2:
|
|
709
|
+
# Check if it's a range (numeric) or discrete list
|
|
710
|
+
if all(isinstance(v, (int, float)) for v in space):
|
|
711
|
+
# Numeric range: treat as continuous if range > 10 values
|
|
712
|
+
try:
|
|
713
|
+
if len(space) == 2 and all(isinstance(v, (int, float)) for v in space):
|
|
714
|
+
# Check if values suggest discrete or continuous
|
|
715
|
+
if all(isinstance(v, int) for v in space) and len(space) == 2:
|
|
716
|
+
# Check if it's meant to be discrete (like range values)
|
|
717
|
+
pass
|
|
718
|
+
except:
|
|
719
|
+
pass
|
|
720
|
+
# Try as discrete list first
|
|
721
|
+
try:
|
|
722
|
+
# Assume discrete if second value is list
|
|
723
|
+
if isinstance(space[1], list):
|
|
724
|
+
choice = trial.suggest_categorical(name, space)
|
|
725
|
+
params[name] = choice
|
|
726
|
+
else:
|
|
727
|
+
# Continuous range
|
|
728
|
+
low, high = sorted(space)
|
|
729
|
+
if all(isinstance(v, int) for v in space):
|
|
730
|
+
params[name] = trial.suggest_int(name, int(low), int(high))
|
|
731
|
+
else:
|
|
732
|
+
params[name] = trial.suggest_float(name, float(low), float(high))
|
|
733
|
+
except:
|
|
734
|
+
# Treat as continuous
|
|
735
|
+
low, high = sorted(space)
|
|
736
|
+
if all(isinstance(v, int) for v in space):
|
|
737
|
+
params[name] = trial.suggest_int(name, int(low), int(high))
|
|
738
|
+
else:
|
|
739
|
+
params[name] = trial.suggest_float(name, float(low), float(high))
|
|
740
|
+
else:
|
|
741
|
+
# Discrete list
|
|
742
|
+
params[name] = trial.suggest_categorical(name, space)
|
|
743
|
+
else:
|
|
744
|
+
# Direct list of choices
|
|
745
|
+
params[name] = trial.suggest_categorical(name, list(space))
|
|
746
|
+
|
|
747
|
+
# Apply constraint if provided
|
|
748
|
+
if constraint is not None:
|
|
749
|
+
try:
|
|
750
|
+
if not bool(constraint(params)):
|
|
751
|
+
raise optuna.TrialPruned("Constraint violated")
|
|
752
|
+
except optuna.TrialPruned:
|
|
753
|
+
raise
|
|
754
|
+
except Exception:
|
|
755
|
+
raise optuna.TrialPruned("Constraint error")
|
|
756
|
+
|
|
757
|
+
# Create strategy copy and apply params
|
|
758
|
+
strat_copy = copy.deepcopy(strategy_template)
|
|
759
|
+
for k, v in params.items():
|
|
760
|
+
setattr(strat_copy, k, v)
|
|
761
|
+
|
|
762
|
+
# Run backtest
|
|
763
|
+
bt = SimpleBacktester(
|
|
764
|
+
strat_copy,
|
|
765
|
+
cash=cash,
|
|
766
|
+
commission=commission,
|
|
767
|
+
commission_type=commission_type,
|
|
768
|
+
lot_size=lot_size,
|
|
769
|
+
leverage=leverage,
|
|
770
|
+
)
|
|
771
|
+
report = bt.run(progress_bar=False)
|
|
772
|
+
|
|
773
|
+
# Compute metrics
|
|
774
|
+
metrics = _compute_backtest_metrics(report)
|
|
775
|
+
|
|
776
|
+
# Apply risk tolerance filter
|
|
777
|
+
if risk_tolerance is not None:
|
|
778
|
+
if not _risk_tolerance_passes(report, risk_tolerance):
|
|
779
|
+
raise optuna.TrialPruned("Risk tolerance exceeded")
|
|
780
|
+
|
|
781
|
+
# Get objective score
|
|
782
|
+
if objective in valid_metrics:
|
|
783
|
+
score = metrics.get(objective)
|
|
784
|
+
else:
|
|
785
|
+
score = getattr(report, objective, None)
|
|
786
|
+
if callable(score):
|
|
787
|
+
score = score()
|
|
788
|
+
|
|
789
|
+
if score is None or not np.isfinite(float(score)): # type: ignore[arg-type]
|
|
790
|
+
raise optuna.TrialPruned("Invalid objective score")
|
|
791
|
+
|
|
792
|
+
return float(score) # type: ignore[arg-type]
|
|
793
|
+
|
|
794
|
+
return objective_fn
|
|
795
|
+
|
|
796
|
+
# Create and configure Optuna study
|
|
797
|
+
sampler = optuna.samplers.TPESampler(seed=random_seed)
|
|
798
|
+
study = optuna.create_study(
|
|
799
|
+
direction="maximize",
|
|
800
|
+
sampler=sampler,
|
|
801
|
+
)
|
|
802
|
+
|
|
803
|
+
# Create objective function with closure
|
|
804
|
+
obj_fn = _create_objective(
|
|
805
|
+
strategy_template=self.strategy,
|
|
806
|
+
cash=self.cash,
|
|
807
|
+
commission=self.commission,
|
|
808
|
+
commission_type=self.commission_type,
|
|
809
|
+
lot_size=self.lot_size,
|
|
810
|
+
leverage=self.leverage,
|
|
811
|
+
objective=objective,
|
|
812
|
+
risk_tolerance=risk_tolerance,
|
|
813
|
+
constraint=constraint,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
# Run optimization
|
|
817
|
+
show_progress = progress_bar and workers is None # Only if sequential
|
|
818
|
+
|
|
819
|
+
if workers is not None and workers > 1:
|
|
820
|
+
# Parallel execution using joblib backend
|
|
821
|
+
study.optimize(
|
|
822
|
+
obj_fn,
|
|
823
|
+
n_trials=n_trials,
|
|
824
|
+
timeout=timeout,
|
|
825
|
+
n_jobs=workers,
|
|
826
|
+
show_progress_bar=progress_bar,
|
|
827
|
+
)
|
|
828
|
+
else:
|
|
829
|
+
# Sequential execution
|
|
830
|
+
study.optimize(
|
|
831
|
+
obj_fn,
|
|
832
|
+
n_trials=n_trials,
|
|
833
|
+
timeout=timeout,
|
|
834
|
+
show_progress_bar=show_progress,
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
# Get best params
|
|
838
|
+
best_params = study.best_params
|
|
839
|
+
|
|
840
|
+
# Build results DataFrame from completed trials
|
|
841
|
+
results_rows = []
|
|
842
|
+
for trial in study.trials:
|
|
843
|
+
if trial.value is not None and trial.value > -np.inf:
|
|
844
|
+
row = dict(trial.params)
|
|
845
|
+
row["objective_score"] = trial.value
|
|
846
|
+
row["state"] = trial.state.name
|
|
847
|
+
results_rows.append(row)
|
|
848
|
+
|
|
849
|
+
results_df = pd.DataFrame(results_rows)
|
|
850
|
+
if not results_df.empty:
|
|
851
|
+
results_df.sort_values(by=["objective_score"], ascending=False, inplace=True, kind="mergesort")
|
|
852
|
+
|
|
853
|
+
# Run full backtest with best params for detailed report
|
|
854
|
+
strat_copy = copy.deepcopy(self.strategy)
|
|
855
|
+
for k, v in best_params.items():
|
|
856
|
+
setattr(strat_copy, k, v)
|
|
857
|
+
|
|
858
|
+
bt = SimpleBacktester(
|
|
859
|
+
strat_copy,
|
|
860
|
+
cash=self.cash,
|
|
861
|
+
commission=self.commission,
|
|
862
|
+
commission_type=self.commission_type,
|
|
863
|
+
lot_size=self.lot_size,
|
|
864
|
+
leverage=self.leverage,
|
|
865
|
+
)
|
|
866
|
+
best_report = bt.run(progress_bar=False)
|
|
867
|
+
best_metrics = _compute_backtest_metrics(best_report)
|
|
868
|
+
|
|
869
|
+
return OptimizationResult(
|
|
870
|
+
best_params=best_params,
|
|
871
|
+
train_report=best_report,
|
|
872
|
+
validate_report=None,
|
|
873
|
+
test_report=None,
|
|
874
|
+
train_metrics=best_metrics,
|
|
875
|
+
validate_metrics={},
|
|
876
|
+
test_metrics={},
|
|
877
|
+
all_results=results_df,
|
|
566
878
|
)
|
|
567
879
|
|
|
568
880
|
def optimize_with_split(
|
|
@@ -677,7 +989,8 @@ class SimpleBacktester:
|
|
|
677
989
|
|
|
678
990
|
valid_metrics = {"final_cash", "total_return", "sharpe", "max_drawdown", "trades"}
|
|
679
991
|
|
|
680
|
-
|
|
992
|
+
# Use math.prod instead of len(list(...)) to avoid materializing all combos
|
|
993
|
+
total_combos = math.prod(len(v) for v in value_lists)
|
|
681
994
|
|
|
682
995
|
# Create a modified strategy that uses data slices
|
|
683
996
|
def create_split_strategy(params_dict: dict, split_mode: DataSplitMode):
|
|
@@ -730,6 +1043,7 @@ class SimpleBacktester:
|
|
|
730
1043
|
commission=self.commission,
|
|
731
1044
|
commission_type=self.commission_type,
|
|
732
1045
|
lot_size=self.lot_size,
|
|
1046
|
+
leverage=self.leverage,
|
|
733
1047
|
)
|
|
734
1048
|
report = bt.run(progress_bar=False)
|
|
735
1049
|
metrics = _compute_backtest_metrics(report)
|
|
@@ -792,7 +1106,7 @@ class SimpleBacktester:
|
|
|
792
1106
|
best_params = {}
|
|
793
1107
|
best_validate_score = -np.inf
|
|
794
1108
|
|
|
795
|
-
|
|
1109
|
+
# Get full reports for best parameters
|
|
796
1110
|
train_report = None
|
|
797
1111
|
validate_report = None
|
|
798
1112
|
test_report = None
|
|
@@ -814,6 +1128,7 @@ class SimpleBacktester:
|
|
|
814
1128
|
commission=self.commission,
|
|
815
1129
|
commission_type=self.commission_type,
|
|
816
1130
|
lot_size=self.lot_size,
|
|
1131
|
+
leverage=self.leverage,
|
|
817
1132
|
)
|
|
818
1133
|
report = bt.run(progress_bar=False)
|
|
819
1134
|
metrics = _compute_backtest_metrics(report)
|
|
@@ -1008,6 +1323,7 @@ class SimpleBacktester:
|
|
|
1008
1323
|
commission=self.commission,
|
|
1009
1324
|
commission_type=self.commission_type,
|
|
1010
1325
|
lot_size=self.lot_size,
|
|
1326
|
+
leverage=self.leverage,
|
|
1011
1327
|
)
|
|
1012
1328
|
report = bt.run(progress_bar=False)
|
|
1013
1329
|
metrics = _compute_backtest_metrics(report)
|
|
@@ -1131,6 +1447,7 @@ class SimpleBacktester:
|
|
|
1131
1447
|
commission=self.commission,
|
|
1132
1448
|
commission_type=self.commission_type,
|
|
1133
1449
|
lot_size=self.lot_size,
|
|
1450
|
+
leverage=self.leverage,
|
|
1134
1451
|
)
|
|
1135
1452
|
report = bt.run(progress_bar=False)
|
|
1136
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,77 @@ def _worker_init(
|
|
|
47
49
|
"commision": commision,
|
|
48
50
|
"commision_type": commision_type,
|
|
49
51
|
"lot_size": lot_size,
|
|
52
|
+
"leverage": leverage,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _compute_metrics_numpy(
|
|
57
|
+
equity: np.ndarray,
|
|
58
|
+
periods_per_year: float,
|
|
59
|
+
n_trades: int,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
"""
|
|
62
|
+
Compute performance metrics using numpy arrays directly.
|
|
63
|
+
|
|
64
|
+
This is more efficient than using pandas operations for the
|
|
65
|
+
inner loop of optimization since we avoid pandas overhead.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
equity: Numpy array of equity values over time.
|
|
69
|
+
periods_per_year: Number of periods in a year for annualization.
|
|
70
|
+
n_trades: Number of trades executed.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary with computed metrics.
|
|
74
|
+
"""
|
|
75
|
+
# Calculate returns using numpy (avoid pandas overhead)
|
|
76
|
+
equity_arr = equity.astype(np.float64)
|
|
77
|
+
|
|
78
|
+
# Handle edge cases
|
|
79
|
+
if len(equity_arr) < 2:
|
|
80
|
+
return {
|
|
81
|
+
"final_cash": float(equity_arr[-1]) if len(equity_arr) > 0 else 0.0,
|
|
82
|
+
"total_return": 0.0,
|
|
83
|
+
"sharpe": float("nan"),
|
|
84
|
+
"max_drawdown": 0.0,
|
|
85
|
+
"trades": n_trades,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Compute returns using numpy
|
|
89
|
+
returns = np.diff(equity_arr) / equity_arr[:-1]
|
|
90
|
+
|
|
91
|
+
# Remove NaN/Inf values
|
|
92
|
+
valid_returns = returns[np.isfinite(returns)]
|
|
93
|
+
|
|
94
|
+
# Total return
|
|
95
|
+
tot_return = float(equity_arr[-1] / equity_arr[0] - 1.0) if equity_arr[0] != 0 else 0.0
|
|
96
|
+
|
|
97
|
+
# Sharpe ratio
|
|
98
|
+
annual_rf = 0.04
|
|
99
|
+
rf_per_period = annual_rf / periods_per_year
|
|
100
|
+
|
|
101
|
+
if len(valid_returns) < 2:
|
|
102
|
+
sharpe = float("nan")
|
|
103
|
+
else:
|
|
104
|
+
excess = valid_returns - rf_per_period
|
|
105
|
+
mean_excess = np.mean(excess)
|
|
106
|
+
std_excess = np.std(excess, ddof=1)
|
|
107
|
+
if std_excess == 0:
|
|
108
|
+
sharpe = float("nan")
|
|
109
|
+
else:
|
|
110
|
+
sharpe = float((mean_excess / std_excess) * (periods_per_year ** 0.5))
|
|
111
|
+
|
|
112
|
+
# Maximum drawdown using numpy
|
|
113
|
+
running_max = np.maximum.accumulate(equity_arr)
|
|
114
|
+
drawdowns = (equity_arr - running_max) / running_max
|
|
115
|
+
mdd = float(abs(np.min(drawdowns)))
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"final_cash": float(equity_arr[-1]),
|
|
119
|
+
"total_return": tot_return,
|
|
120
|
+
"sharpe": sharpe,
|
|
121
|
+
"max_drawdown": mdd,
|
|
122
|
+
"trades": n_trades,
|
|
50
123
|
}
|
|
51
124
|
|
|
52
125
|
|
|
@@ -57,6 +130,11 @@ def _worker_eval(param_items: tuple[tuple[str, Any], ...]) -> dict[str, Any]:
|
|
|
57
130
|
This function runs in worker processes to evaluate a single
|
|
58
131
|
parameter combination and return performance metrics.
|
|
59
132
|
|
|
133
|
+
Optimizations applied:
|
|
134
|
+
1. Uses numpy for metric computation instead of pandas (faster)
|
|
135
|
+
2. Returns only essential metrics (reduces IPC overhead)
|
|
136
|
+
3. Explicit cleanup of references to help GC
|
|
137
|
+
|
|
60
138
|
Args:
|
|
61
139
|
param_items: Sequence of (key, value) pairs (tuple) to reconstruct dict.
|
|
62
140
|
Each tuple represents a parameter name and its value.
|
|
@@ -101,42 +179,28 @@ def _worker_eval(param_items: tuple[tuple[str, Any], ...]) -> dict[str, Any]:
|
|
|
101
179
|
commission=config["commision"],
|
|
102
180
|
commission_type=config["commision_type"],
|
|
103
181
|
lot_size=config["lot_size"],
|
|
182
|
+
leverage=config["leverage"],
|
|
104
183
|
)
|
|
105
184
|
report = bt.run(progress_bar=False)
|
|
106
185
|
|
|
107
|
-
# Compute metrics
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
else:
|
|
117
|
-
excess = returns - rf_per_period
|
|
118
|
-
mean = excess.mean()
|
|
119
|
-
vol = excess.std(ddof=1)
|
|
120
|
-
sharpe = float((mean / vol) * (report.periods_per_year ** 0.5))
|
|
121
|
-
|
|
122
|
-
running_max = equity.cummax()
|
|
123
|
-
drawdown = ((equity - running_max) / running_max).min()
|
|
124
|
-
mdd = float(abs(drawdown))
|
|
125
|
-
|
|
126
|
-
tot_return = float(equity.iloc[-1] / equity.iloc[0] - 1.0)
|
|
186
|
+
# Compute metrics using optimized numpy version
|
|
187
|
+
# This avoids pandas overhead for metric computation
|
|
188
|
+
# Use to_numpy() with copy=False for efficiency, convert to float64
|
|
189
|
+
equity_values = np.asarray(report.PnlRecord, dtype=np.float64)
|
|
190
|
+
metrics = _compute_metrics_numpy(
|
|
191
|
+
equity=equity_values,
|
|
192
|
+
periods_per_year=report.periods_per_year,
|
|
193
|
+
n_trades=len(report.orders),
|
|
194
|
+
)
|
|
127
195
|
|
|
128
|
-
#
|
|
196
|
+
# Build result with params
|
|
129
197
|
result: dict[str, Any] = {
|
|
130
198
|
"params": params,
|
|
131
|
-
|
|
132
|
-
"total_return": tot_return,
|
|
133
|
-
"sharpe": sharpe,
|
|
134
|
-
"max_drawdown": mdd,
|
|
135
|
-
"trades": len(report.orders),
|
|
199
|
+
**metrics,
|
|
136
200
|
}
|
|
137
201
|
|
|
138
202
|
# Cleanup references to free memory inside worker
|
|
139
|
-
del strat, bt, report
|
|
203
|
+
del strat, bt, report
|
|
140
204
|
gc.collect()
|
|
141
205
|
|
|
142
206
|
return result
|
|
@@ -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
|