Skip to content

Commit 710e59c

Browse files
committed
Fix backtest parallelization
1 parent 1739cc8 commit 710e59c

13 files changed

Lines changed: 1328 additions & 299 deletions

File tree

investing_algorithm_framework/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .app import App, Algorithm, generate_algorithm_id, \
1+
from .app import App, Algorithm, \
22
TradingStrategy, StatelessAction, Task, AppHook, Context, \
33
add_html_report, BacktestReport, generate_rolling_backtest_windows, \
44
pretty_print_trades, pretty_print_positions, \
@@ -16,7 +16,7 @@
1616
Trade, APP_MODE, AppMode, DATETIME_FORMAT, load_backtests_from_directory, \
1717
BacktestDateRange, convert_polars_to_pandas, BacktestRun, \
1818
DEFAULT_LOGGING_CONFIG, DataType, DataProvider, StopLossRule, \
19-
TradeStatus, generate_backtest_summary_metrics, \
19+
TradeStatus, generate_backtest_summary_metrics, generate_algorithm_id, \
2020
APPLICATION_DIRECTORY, DataSource, OrderExecutor, PortfolioProvider, \
2121
SnapshotInterval, AWS_S3_STATE_BUCKET_NAME, BacktestEvaluationFocus, \
2222
save_backtests_to_directory, BacktestMetrics

investing_algorithm_framework/app/__init__.py

Lines changed: 1 addition & 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, generate_algorithm_id, generate_rolling_backtest_windows
17+
create_weights, generate_rolling_backtest_windows
1818

1919

2020
__all__ = [
@@ -42,6 +42,5 @@
4242
"create_weights",
4343
"get_entry_and_exit_signals",
4444
"get_equity_curve_chart",
45-
"generate_algorithm_id",
4645
"generate_rolling_backtest_windows"
4746
]

investing_algorithm_framework/app/algorithm/algorithm.py

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import inspect
22
import logging
3-
import re
43
from typing import List
54

65
from investing_algorithm_framework.app.app_hook import AppHook
@@ -17,8 +16,9 @@ class Algorithm:
1716
strategies that are executed in a specific order.
1817
1918
Attributes:
20-
_name: The name of the algorithm. It should be a string and
21-
can only contain letters and numbers.
19+
algorithm_id: The unique identifier of the algorithm. This id
20+
should be a string and also will be used for all the
21+
registered strategies within the algorithm.
2222
_description: The description of the algorithm. It should be a string.
2323
_strategies: A list of strategies that are part of the algorithm.
2424
_tasks: A list of tasks that are part of the algorithm.
@@ -27,8 +27,7 @@ class Algorithm:
2727
"""
2828
def __init__(
2929
self,
30-
id: str = None,
31-
name: str = None,
30+
algorithm_id: str = None,
3231
description: str = None,
3332
strategy=None,
3433
strategies=None,
@@ -37,8 +36,7 @@ def __init__(
3736
on_strategy_run_hooks=None,
3837
metadata=None
3938
):
40-
self.id = id
41-
self._name = name
39+
self.algorithm_id = algorithm_id
4240
self._context = {}
4341
self._description = None
4442

@@ -67,51 +65,6 @@ def __init__(
6765
for hook in on_strategy_run_hooks:
6866
self.add_on_strategy_run_hook(hook)
6967

70-
@staticmethod
71-
def _validate_name(name):
72-
"""
73-
Function to validate the name of the algorithm. This function
74-
will check if the name of the algorithm is a string and raise
75-
an exception if it is not.
76-
77-
Name can only contain letters, numbers
78-
79-
Parameters:
80-
name: The name of the algorithm
81-
82-
Returns:
83-
None
84-
"""
85-
if not isinstance(name, str):
86-
raise OperationalException(
87-
"The name of the algorithm must be a string"
88-
)
89-
90-
pattern = re.compile(r"^[a-zA-Z0-9]*$")
91-
92-
if not pattern.match(name):
93-
raise OperationalException(
94-
"The name of the algorithm can only contain" +
95-
" letters and numbers"
96-
)
97-
98-
illegal_chars = r"[\/:*?\"<>|]"
99-
100-
if re.search(illegal_chars, name):
101-
raise OperationalException(
102-
f"Illegal characters detected in algorithm: {name}. "
103-
f"Illegal characters: / \\ : * ? \" < > |"
104-
)
105-
106-
@property
107-
def name(self):
108-
return self._name
109-
110-
@name.setter
111-
def name(self, name):
112-
Algorithm._validate_name(name)
113-
self._name = name
114-
11568
@property
11669
def data_sources(self):
11770
return self._data_sources
@@ -200,20 +153,19 @@ def add_strategy(self, strategy, throw_exception=True) -> None:
200153
else:
201154
return
202155

203-
has_duplicates = False
156+
strategy_ids = []
204157

205-
for i in range(len(self._strategies)):
206-
for j in range(i + 1, len(self._strategies)):
207-
if self._strategies[i].worker_id == strategy.worker_id:
208-
has_duplicates = True
209-
break
158+
for s in self._strategies:
159+
strategy_ids.append(s.strategy_id)
210160

211-
if has_duplicates:
161+
# Check for duplicate strategy IDs
162+
if strategy.strategy_id in strategy_ids:
212163
raise OperationalException(
213164
"Can't add strategy, there already exists a strategy "
214165
"with the same id in the algorithm"
215166
)
216167

168+
strategy.algorithm_id = self.algorithm_id
217169
self._strategies.append(strategy)
218170

219171
def add_task(self, task):
Lines changed: 69 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,8 @@
1-
import re
2-
from .algorithm import Algorithm
3-
from investing_algorithm_framework.domain import OperationalException
4-
5-
6-
def validate_algorithm_name(name, illegal_chars=r"[\/:*?\"<>|]"):
7-
"""
8-
Validate an algorithm name for illegal characters and throw an
9-
exception if any are found.
10-
11-
Args:
12-
name (str): The name to validate.
13-
illegal_chars (str): A regex pattern for characters considered
14-
illegal (default: r"[/:*?\"<>|]").
1+
import inspect
152

16-
Raises:
17-
ValueError: If illegal characters are found in the filename.
18-
"""
19-
if re.search(illegal_chars, name):
20-
raise OperationalException(
21-
f"Illegal characters detected in filename: {name}. "
22-
f"Illegal characters: {illegal_chars}"
23-
)
3+
from .algorithm import Algorithm
4+
from investing_algorithm_framework.app.strategy import TradingStrategy
5+
from investing_algorithm_framework.domain import generate_algorithm_id
246

257

268
class AlgorithmFactory:
@@ -29,86 +11,108 @@ class AlgorithmFactory:
2911
"""
3012

3113
@staticmethod
32-
def create_algorithm_name(algorithm):
14+
def _instantiate_strategy(strategy):
3315
"""
34-
Create a name for the algorithm based on its
35-
strategies.
16+
Instantiate a strategy if it's a class, otherwise return as-is.
3617
3718
Args:
38-
algorithm (Algorithm): Instance of Algorithm.
19+
strategy: Either a TradingStrategy class or instance.
3920
4021
Returns:
41-
str: Name of the algorithm.
22+
TradingStrategy instance.
4223
"""
43-
first_strategy = algorithm.strategies[0] \
44-
if algorithm.strategies else None
45-
46-
if first_strategy is not None:
47-
return f"{first_strategy.__class__.__name__}"
48-
49-
return "DefaultAlgorithm"
24+
if inspect.isclass(strategy):
25+
if issubclass(strategy, TradingStrategy):
26+
return strategy()
27+
return strategy
5028

5129
@staticmethod
5230
def create_algorithm(
53-
name=None,
5431
algorithm=None,
5532
strategy=None,
5633
strategies=None,
5734
tasks=None,
5835
on_strategy_run_hooks=None,
5936
) -> Algorithm:
6037
"""
61-
Create an instance of the specified algorithm type.
38+
Create an instance of an Algorithm with the given parameters.
39+
40+
If the app already has strategies, tasks, or hooks defined,
41+
they will be merged with the provided ones.
42+
43+
If there is no algorithm id provided, an id will be generated and
44+
also set to each strategy within the algorithm.
6245
6346
Args:
64-
name (str): Name of the algorithm.
6547
algorithm (Algorithm): Instance of Algorithm to be used.
66-
strategy (TradingStrategy): Single TradingStrategy instance.
67-
strategies (list): List of TradingStrategy instances.
48+
strategy (TradingStrategy): Single TradingStrategy instance
49+
or class.
50+
strategies (list): List of TradingStrategy instances or classes.
6851
tasks (list): List of Task instances.
6952
on_strategy_run_hooks (list): List of hooks to be called
7053
when a strategy is run.
7154
7255
Returns:
7356
Algorithm: Instance of Algorithm.
7457
"""
75-
name = name
76-
strategies = strategies or []
58+
final_strategies = []
7759
tasks = tasks or []
7860
on_strategy_run_hooks = on_strategy_run_hooks or []
79-
data_sources = []
61+
algorithm_id = None
8062

63+
# First, process algorithm if provided
8164
if algorithm is not None and isinstance(algorithm, Algorithm):
82-
if name is None:
83-
name = algorithm.name
84-
85-
strategies.extend(algorithm.strategies)
65+
final_strategies.extend(algorithm.strategies)
8666
tasks.extend(algorithm.tasks)
8767
on_strategy_run_hooks.extend(algorithm.on_strategy_run_hooks)
88-
89-
if hasattr(algorithm, 'data_sources'):
90-
data_sources.extend(algorithm.data_sources)
91-
68+
algorithm_id = algorithm.algorithm_id
69+
70+
# Then, add strategies from the strategies list
71+
if strategies is not None:
72+
for strat in strategies:
73+
# Instantiate if it's a class
74+
strat = AlgorithmFactory._instantiate_strategy(strat)
75+
# Avoid duplicates by checking strategy_id
76+
if not any(
77+
s.strategy_id == strat.strategy_id
78+
for s in final_strategies
79+
):
80+
final_strategies.append(strat)
81+
82+
# Finally, add single strategy if provided and not already in list
9283
if strategy is not None:
93-
strategies.append(strategy)
94-
data_sources.extend(strategy.data_sources)
95-
96-
for strategy_entry in strategies:
97-
if strategy_entry.data_sources is not None \
98-
and len(strategy_entry.data_sources):
99-
data_sources.extend(strategy_entry.data_sources)
84+
# Instantiate if it's a class
85+
strategy = AlgorithmFactory._instantiate_strategy(strategy)
86+
if not any(
87+
s.strategy_id == strategy.strategy_id
88+
for s in final_strategies
89+
):
90+
final_strategies.append(strategy)
91+
92+
# Collect data sources from all strategies (avoiding duplicates)
93+
data_sources = []
94+
seen_data_source_ids = set()
95+
96+
for strategy_entry in final_strategies:
97+
if strategy_entry.data_sources is not None:
98+
for ds in strategy_entry.data_sources:
99+
ds_id = ds.get_identifier() \
100+
if hasattr(ds, 'get_identifier') else id(ds)
101+
if ds_id not in seen_data_source_ids:
102+
data_sources.append(ds)
103+
seen_data_source_ids.add(ds_id)
104+
105+
# Generate algorithm_id if not provided
106+
if algorithm_id is None and len(final_strategies) > 0:
107+
algorithm_id = generate_algorithm_id(
108+
strategy=final_strategies[0]
109+
)
100110

101111
algorithm = Algorithm(
102-
name=name,
103-
strategies=strategies,
112+
algorithm_id=algorithm_id,
113+
strategies=final_strategies,
104114
tasks=tasks,
105115
on_strategy_run_hooks=on_strategy_run_hooks,
106116
data_sources=data_sources
107117
)
108-
109-
if algorithm.name is None:
110-
algorithm.name = AlgorithmFactory.create_algorithm_name(algorithm)
111-
112-
# Validate the algorithm name
113-
validate_algorithm_name(algorithm.name)
114118
return algorithm

investing_algorithm_framework/app/analysis/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
generate_rolling_backtest_windows
33
from .ranking import rank_results, create_weights, combine_backtest_metrics
44
from .permutation import create_ohlcv_permutation
5-
from .algorithm_id import generate_algorithm_id
65

76
__all__ = [
87
"select_backtest_date_ranges",
@@ -11,5 +10,4 @@
1110
"create_weights",
1211
"create_ohlcv_permutation",
1312
"combine_backtest_metrics",
14-
"generate_algorithm_id"
1513
]

0 commit comments

Comments
 (0)