Skip to content

Commit 8d4726f

Browse files
committed
Add combine backtests function
1 parent eaf5e8f commit 8d4726f

File tree

6 files changed

+246
-6
lines changed

6 files changed

+246
-6
lines changed

investing_algorithm_framework/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
get_monthly_returns_heatmap_chart, create_weights, \
99
get_yearly_returns_bar_chart, get_entry_and_exit_signals, \
1010
get_ohlcv_data_completeness_chart
11-
from .domain import ApiException, \
11+
from .domain import ApiException, combine_backtests, \
1212
OrderType, OperationalException, OrderStatus, OrderSide, \
1313
TimeUnit, TimeInterval, Order, Portfolio, Backtest, \
1414
Position, TimeFrame, INDEX_DATETIME, MarketCredential, \
@@ -163,5 +163,6 @@
163163
"get_entry_and_exit_signals",
164164
"get_growth",
165165
"get_growth_percentage",
166-
"BacktestEvaluationFocus"
166+
"BacktestEvaluationFocus",
167+
"combine_backtests",
167168
]

investing_algorithm_framework/domain/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
csv_to_list, StoppableThread, load_csv_into_dict, tqdm, \
3535
is_timezone_aware, sync_timezones, get_timezone
3636
from .backtesting import BacktestRun, BacktestSummaryMetrics, \
37-
BacktestDateRange, Backtest, BacktestMetrics, \
37+
BacktestDateRange, Backtest, BacktestMetrics, combine_backtests, \
3838
BacktestPermutationTest, BacktestEvaluationFocus
3939

4040
__all__ = [
@@ -140,5 +140,6 @@
140140
"is_jupyter_notebook",
141141
"tqdm",
142142
"DEFAULT_DATETIME_FORMAT",
143-
"BacktestEvaluationFocus"
143+
"BacktestEvaluationFocus",
144+
'combine_backtests'
144145
]

investing_algorithm_framework/domain/backtesting/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from .backtest import Backtest
66
from .backtest_permutation_test import BacktestPermutationTest
77
from .backtest_evaluation_focuss import BacktestEvaluationFocus
8+
from .combine_backtests import combine_backtests
89

910
__all__ = [
1011
"Backtest",
@@ -13,5 +14,6 @@
1314
"BacktestMetrics",
1415
"BacktestRun",
1516
"BacktestPermutationTest",
16-
"BacktestEvaluationFocus"
17+
"BacktestEvaluationFocus",
18+
"combine_backtests"
1719
]

investing_algorithm_framework/domain/backtesting/backtest_metrics.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ class BacktestMetrics:
165165
best_year: Tuple[float, date] = None
166166
worst_month: Tuple[float, datetime] = None
167167
worst_year: Tuple[float, date] = None
168+
total_number_of_days: int = None
169+
170+
def __post_init__(self):
171+
self.total_number_of_days = (self.backtest_end_date -
172+
self.backtest_start_date).days
168173

169174
def to_dict(self) -> dict:
170175
"""

investing_algorithm_framework/domain/backtesting/backtest_summary_metrics.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,18 @@ class BacktestSummaryMetrics:
2020
total_net_gain (float): Total net gain from the backtest.
2121
total_net_gain_percentage (float): Total net gain percentage
2222
from the backtest.
23+
average_total_net_gain (float): Average total net gain across
24+
multiple backtests.
25+
average_total_net_gain_percentage (float): Average total net gain
26+
percentage across multiple backtests.
2327
gross_loss (float): Total gross loss from all trades.
28+
average_gross_loss (float): Average gross loss across
29+
multiple backtests.
2430
growth (float): Total growth from the backtest.
2531
growth_percentage (float): Total growth percentage from the backtest.
32+
average_growth (float): Average growth across multiple backtests.
33+
average_growth_percentage (float): Average growth percentage across
34+
multiple backtests.
2635
trades_average_return (float): Average return per trade.
2736
cagr (float): Compound annual growth rate of the backtest.
2837
sharpe_ratio (float): Sharpe ratio, risk-adjusted return.
@@ -41,9 +50,14 @@ class BacktestSummaryMetrics:
4150
"""
4251
total_net_gain: float = None
4352
total_net_gain_percentage: float = None
53+
average_total_net_gain: float = None
54+
average_total_net_gain_percentage: float = None
4455
gross_loss: float = None
56+
average_gross_loss: float = None
4557
growth: float = None
4658
growth_percentage: float = None
59+
average_growth: float = None
60+
average_growth_percentage: float = None
4761
trades_average_return: float = None
4862
cagr: float = None
4963
sharpe_ratio: float = None
@@ -67,9 +81,15 @@ def to_dict(self) -> dict:
6781
return {
6882
"total_net_gain": self.total_net_gain,
6983
"total_net_gain_percentage": self.total_net_gain_percentage,
84+
"average_total_net_gain": self.average_total_net_gain,
85+
"average_total_net_gain_percentage":
86+
self.average_total_net_gain_percentage,
7087
"gross_loss": self.gross_loss,
88+
"average_gross_loss": self.average_gross_loss,
7189
"growth": self.growth,
7290
"growth_percentage": self.growth_percentage,
91+
"average_growth": self.average_growth,
92+
"average_growth_percentage": self.average_growth_percentage,
7393
"trades_average_return": self.trades_average_return,
7494
"cagr": self.cagr,
7595
"sharpe_ratio": self.sharpe_ratio,
@@ -120,7 +140,8 @@ def safe_mean(a, b):
120140
self.cumulative_exposure = other.cumulative_exposure
121141
else:
122142
self.cumulative_exposure = safe_mean(
123-
self.cumulative_exposure, other.cumulative_exposure
143+
self.cumulative_exposure,
144+
other.cumulative_exposure
124145
)
125146

126147
if self.exposure_ratio is None:
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
from typing import List
2+
3+
from investing_algorithm_framework.domain.backtesting import Backtest, BacktestDateRange
4+
from investing_algorithm_framework.domain.backtesting import \
5+
BacktestSummaryMetrics
6+
7+
8+
def safe_weighted_mean(values, weights):
9+
"""
10+
Calculate the weighted mean of a list of values,
11+
ignoring None values and weights <= 0.
12+
13+
Args:
14+
values (List[float | None]): List of values to average.
15+
weights (List[float | None]): Corresponding weights for the values.
16+
17+
Returns:
18+
float | None: The weighted mean, or None if no valid values.
19+
"""
20+
vals = [(v, w) for v, w in zip(values, weights) if
21+
v is not None and w is not None and w > 0]
22+
if not vals:
23+
return None
24+
total_weight = sum(w for _, w in vals)
25+
return sum(
26+
v * w for v, w in vals
27+
) / total_weight if total_weight > 0 else None
28+
29+
30+
def combine_backtests(
31+
backtests: List[Backtest],
32+
backtest_date_range: BacktestDateRange = None
33+
) -> Backtest:
34+
"""
35+
Combine multiple backtests into a single backtest by aggregating
36+
their results.
37+
38+
Args:
39+
backtests (List[Backtest]): List of Backtest instances to combine.
40+
backtest_date_range (BacktestDateRange, optional): The date range
41+
for the combined backtest.
42+
43+
Returns:
44+
Backtest: A new Backtest instance representing the combined results.
45+
"""
46+
backtest_metrics = []
47+
backtest_runs = []
48+
49+
for backtest in backtests:
50+
backtest_metric = None
51+
backtest_run = None
52+
53+
if backtest_date_range is not None:
54+
backtest_metric = \
55+
backtest.get_backtest_metrics(backtest_date_range)
56+
backtest_run = \
57+
backtest.get_backtest_run(backtest_date_range)
58+
else:
59+
backtest_run = backtest.backtest_runs[0] \
60+
if len(backtest.backtest_runs) > 0 else None
61+
62+
if backtest_run is not None:
63+
backtest_metric = backtest_run.backtest_metrics
64+
65+
if backtest_metric is not None:
66+
backtest_metrics.append(backtest_metric)
67+
backtest_runs.append(backtest_run)
68+
69+
total_net_gain = sum(
70+
b.total_net_gain for b in backtest_metrics
71+
if b.total_net_gain is not None
72+
)
73+
average_total_net_gain = safe_weighted_mean(
74+
[b.total_net_gain for b in backtest_metrics],
75+
[b.total_number_of_days for b in backtest_metrics]
76+
)
77+
average_total_net_gain_percentage = safe_weighted_mean(
78+
[b.total_net_gain_percentage for b in backtest_metrics],
79+
[b.total_number_of_days for b in backtest_metrics]
80+
)
81+
total_net_gain_percentage = sum(
82+
b.total_net_gain_percentage for b in backtest_metrics
83+
if b.total_net_gain_percentage is not None
84+
)
85+
gross_loss = sum(
86+
b.gross_loss for b in backtest_metrics
87+
if b.gross_loss is not None
88+
)
89+
average_gross_loss = safe_weighted_mean(
90+
[b.gross_loss for b in backtest_metrics],
91+
[b.total_number_of_days for b in backtest_metrics]
92+
)
93+
growth = sum(
94+
b.growth for b in backtest_metrics
95+
if b.growth is not None
96+
)
97+
growth_percentage = sum(
98+
b.growth_percentage for b in backtest_metrics
99+
if b.growth_percentage is not None
100+
)
101+
average_growth = safe_weighted_mean(
102+
[b.growth for b in backtest_metrics],
103+
[b.total_number_of_days for b in backtest_metrics]
104+
)
105+
average_growth_percentage = safe_weighted_mean(
106+
[b.growth_percentage for b in backtest_metrics],
107+
[b.total_number_of_days for b in backtest_metrics]
108+
)
109+
trades_average_return = safe_weighted_mean(
110+
[b.trades_average_return for b in backtest_metrics],
111+
[b.total_number_of_days for b in backtest_metrics]
112+
)
113+
cagr = safe_weighted_mean(
114+
[b.cagr for b in backtest_metrics],
115+
[b.total_number_of_days for b in backtest_metrics]
116+
)
117+
sharp_ratio = safe_weighted_mean(
118+
[b.sharpe_ratio for b in backtest_metrics],
119+
[b.total_number_of_days for b in backtest_metrics]
120+
)
121+
sortino_ratio = safe_weighted_mean(
122+
[b.sortino_ratio for b in backtest_metrics],
123+
[b.total_number_of_days for b in backtest_metrics]
124+
)
125+
calmar_ratio = safe_weighted_mean(
126+
[b.calmar_ratio for b in backtest_metrics],
127+
[b.total_number_of_days for b in backtest_metrics]
128+
)
129+
profit_factor = safe_weighted_mean(
130+
[b.profit_factor for b in backtest_metrics],
131+
[b.total_number_of_days for b in backtest_metrics]
132+
)
133+
annual_volatility = safe_weighted_mean(
134+
[b.annual_volatility for b in backtest_metrics],
135+
[b.total_number_of_days for b in backtest_metrics]
136+
)
137+
max_drawdown = max(
138+
(b.max_drawdown for b in backtest_metrics
139+
if b.max_drawdown is not None), default=None
140+
)
141+
max_drawdown_duration = max(
142+
(b.max_drawdown_duration for b in backtest_metrics
143+
if b.max_drawdown_duration is not None), default=None
144+
)
145+
trades_per_year = safe_weighted_mean(
146+
[b.trades_per_year for b in backtest_metrics],
147+
[b.total_number_of_days for b in backtest_metrics]
148+
)
149+
win_rate = safe_weighted_mean(
150+
[b.win_rate for b in backtest_metrics],
151+
[b.total_number_of_days for b in backtest_metrics]
152+
)
153+
win_loss_ratio = safe_weighted_mean(
154+
[b.win_loss_ratio for b in backtest_metrics],
155+
[b.total_number_of_days for b in backtest_metrics]
156+
)
157+
number_of_trades = sum(
158+
b.number_of_trades for b in backtest_metrics
159+
if b.number_of_trades is not None
160+
)
161+
cumulative_exposure = safe_weighted_mean(
162+
[b.cumulative_exposure for b in backtest_metrics],
163+
[b.total_number_of_days for b in backtest_metrics]
164+
)
165+
exposure_ratio = safe_weighted_mean(
166+
[b.exposure_ratio for b in backtest_metrics],
167+
[b.total_number_of_days for b in backtest_metrics]
168+
)
169+
summary = BacktestSummaryMetrics(
170+
total_net_gain=total_net_gain,
171+
total_net_gain_percentage=total_net_gain_percentage,
172+
average_total_net_gain=average_total_net_gain,
173+
average_total_net_gain_percentage=average_total_net_gain_percentage,
174+
gross_loss=gross_loss,
175+
average_gross_loss=average_gross_loss,
176+
growth=growth,
177+
growth_percentage=growth_percentage,
178+
average_growth=average_growth,
179+
average_growth_percentage=average_growth_percentage,
180+
trades_average_return=trades_average_return,
181+
cagr=cagr,
182+
sharpe_ratio=sharp_ratio,
183+
sortino_ratio=sortino_ratio,
184+
calmar_ratio=calmar_ratio,
185+
profit_factor=profit_factor,
186+
annual_volatility=annual_volatility,
187+
max_drawdown=max_drawdown,
188+
max_drawdown_duration=max_drawdown_duration,
189+
trades_per_year=trades_per_year,
190+
win_rate=win_rate,
191+
win_loss_ratio=win_loss_ratio,
192+
number_of_trades=number_of_trades,
193+
cumulative_exposure=cumulative_exposure,
194+
exposure_ratio=exposure_ratio
195+
)
196+
197+
metadata = None
198+
199+
# Get first non-empty metadata
200+
for backtest in backtests:
201+
if backtest.metadata:
202+
metadata = backtest.metadata
203+
break
204+
205+
backtest = Backtest(
206+
backtest_summary=summary,
207+
metadata=metadata,
208+
backtest_runs=backtest_runs
209+
)
210+
return backtest

0 commit comments

Comments
 (0)