Skip to content

Commit dfc83fc

Browse files
committed
Add save and load from directory utils for backtests
1 parent 3f2defc commit dfc83fc

File tree

6 files changed

+120
-15
lines changed

6 files changed

+120
-15
lines changed

investing_algorithm_framework/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from .app import App, Algorithm, \
22
TradingStrategy, StatelessAction, Task, AppHook, Context, \
3-
add_html_report, BacktestReport, \
3+
add_html_report, BacktestReport, save_backtests_to_directory, \
44
pretty_print_trades, pretty_print_positions, \
55
pretty_print_orders, pretty_print_backtest, select_backtest_date_ranges, \
6-
get_equity_curve_with_drawdown_chart, \
6+
get_equity_curve_with_drawdown_chart, load_backtests_from_directory, \
77
get_rolling_sharpe_ratio_chart, rank_results, \
88
get_monthly_returns_heatmap_chart, create_weights, \
99
get_yearly_returns_bar_chart, get_entry_and_exit_signals, \
@@ -189,5 +189,7 @@
189189
"get_negative_trades",
190190
"get_positive_trades",
191191
"get_number_of_trades",
192-
"BacktestRun"
192+
"BacktestRun",
193+
"load_backtests_from_directory",
194+
"save_backtests_to_directory"
193195
]

investing_algorithm_framework/app/__init__.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
get_yearly_returns_bar_chart, get_equity_curve_chart, \
1515
get_ohlcv_data_completeness_chart, get_entry_and_exit_signals
1616
from .analysis import select_backtest_date_ranges, rank_results, \
17-
create_weights
17+
create_weights, load_backtests_from_directory, save_backtests_to_directory
1818

1919

2020
__all__ = [
@@ -41,5 +41,7 @@
4141
"rank_results",
4242
"create_weights",
4343
"get_entry_and_exit_signals",
44-
"get_equity_curve_chart"
44+
"get_equity_curve_chart",
45+
"load_backtests_from_directory",
46+
"save_backtests_to_directory"
4547
]
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from .backtest_data_ranges import select_backtest_date_ranges
22
from .ranking import rank_results, create_weights, combine_backtest_metrics
33
from .permutation import create_ohlcv_permutation
4+
from .backtest_utils import load_backtests_from_directory, \
5+
save_backtests_to_directory
46

57
__all__ = [
68
"select_backtest_date_ranges",
79
"rank_results",
810
"create_weights",
911
"create_ohlcv_permutation",
10-
"combine_backtest_metrics"
12+
"combine_backtest_metrics",
13+
"load_backtests_from_directory",
14+
"save_backtests_to_directory"
1115
]
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import os
2+
from pathlib import Path
3+
from typing import List, Union
4+
from logging import getLogger
5+
from random import Random
6+
7+
from investing_algorithm_framework.domain import Backtest
8+
9+
10+
logger = getLogger("investing_algorithm_framework")
11+
12+
def save_backtests_to_directory(
13+
backtests: List[Backtest],
14+
directory_path: Union[str, Path]
15+
) -> None:
16+
"""
17+
Saves a list of Backtest objects to the specified directory.
18+
19+
Args:
20+
backtests (List[Backtest]): List of Backtest objects to save.
21+
directory_path (str): Path to the directory where backtests
22+
will be saved.
23+
24+
Returns:
25+
None
26+
"""
27+
28+
29+
if not os.path.exists(directory_path):
30+
os.makedirs(directory_path)
31+
32+
for backtest in backtests:
33+
# Check if there is an ID in the backtest metadata
34+
backtest_id = backtest.metadata.get('id')
35+
36+
if backtest_id is None:
37+
logger.warning(
38+
"Backtest is missing 'id' in metadata. "
39+
"Generating a random ID as name for backtest file."
40+
)
41+
backtest_id = str(Random().randint(100000, 999999))
42+
43+
backtest.save(os.path.join(directory_path, backtest_id))
44+
45+
46+
def load_backtests_from_directory(
47+
directory_path: Union[str, Path]
48+
) -> List[Backtest]:
49+
"""
50+
Loads Backtest objects from the specified directory.
51+
52+
Args:
53+
directory_path (str): Path to the directory from which backtests
54+
will be loaded.
55+
56+
Returns:
57+
List[Backtest]: List of loaded Backtest objects.
58+
"""
59+
60+
backtests = []
61+
62+
if not os.path.exists(directory_path):
63+
logger.warning(
64+
f"Directory {directory_path} does not exist. "
65+
"No backtests loaded."
66+
)
67+
return backtests
68+
69+
for file_name in os.listdir(directory_path):
70+
file_path = os.path.join(directory_path, file_name)
71+
72+
try:
73+
backtest = Backtest.open(file_path)
74+
backtests.append(backtest)
75+
except Exception as e:
76+
logger.error(
77+
f"Failed to load backtest from {file_path}: {e}"
78+
)
79+
80+
return backtests

investing_algorithm_framework/app/app.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,13 +1070,12 @@ def run_vector_backtest(
10701070
except Exception as e:
10711071
logger.error(
10721072
f"Error occurred during vector backtest for strategy "
1073-
f"{strategy.name}: {str(e)}"
1073+
f"{strategy.strategy_id}: {str(e)}"
10741074
)
10751075
if continue_on_error:
10761076
backtest = Backtest(
10771077
backtest_runs=[],
10781078
risk_free_rate=risk_free_rate,
1079-
backtest_summary={}
10801079
)
10811080
else:
10821081
raise e

investing_algorithm_framework/domain/backtesting/backtest_run.py

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,17 +251,35 @@ def save(self, directory_path: Union[str, Path]) -> None:
251251
# Remove backtest_metrics to avoid redundancy
252252
data.pop("backtest_metrics", None)
253253

254-
data["backtest_start_date"] = self.backtest_start_date.strftime(
254+
# Ensure datetime objects are in UTC before formatting
255+
backtest_start_date = self.backtest_start_date
256+
257+
if backtest_start_date.tzinfo is None:
258+
# Naive datetime - treat as UTC
259+
backtest_start_date = backtest_start_date.replace(tzinfo=timezone.utc)
260+
else:
261+
# Timezone-aware - convert to UTC
262+
backtest_start_date = backtest_start_date.astimezone(timezone.utc)
263+
264+
backtest_end_date = self.backtest_end_date
265+
if backtest_end_date.tzinfo is None:
266+
backtest_end_date = backtest_end_date.replace(tzinfo=timezone.utc)
267+
else:
268+
backtest_end_date = backtest_end_date.astimezone(timezone.utc)
269+
270+
created_at = self.created_at
271+
if created_at.tzinfo is None:
272+
created_at = created_at.replace(tzinfo=timezone.utc)
273+
else:
274+
created_at = created_at.astimezone(timezone.utc)
275+
276+
data["backtest_start_date"] = backtest_start_date.strftime(
255277
"%Y-%m-%d %H:%M:%S"
256278
)
257-
data["backtest_end_date"] = self.backtest_end_date.strftime(
279+
data["backtest_end_date"] = backtest_end_date.strftime(
258280
"%Y-%m-%d %H:%M:%S"
259281
)
260-
261-
if self.created_at.tzinfo is None:
262-
self.created_at = self.created_at.replace(tzinfo=timezone.utc)
263-
264-
data["created_at"] = self.created_at.strftime(
282+
data["created_at"] = created_at.strftime(
265283
"%Y-%m-%d %H:%M:%S"
266284
)
267285
json.dump(data, f, default=str)

0 commit comments

Comments
 (0)