Skip to content

Commit b088f37

Browse files
committed
run_backtest(): allow single-strategy algorithms= on the vector engine
Previously algorithms= (independent Algorithms, each with its own Tasks/hooks) was unconditionally rejected on the vector engine. Each Algorithm already gets its own portfolio, so multiple single-strategy Algorithms are functionally equivalent to strategies= for the vector engine and should be allowed. What genuinely isn't supported (and now raises a clearer, specific error) is: - an Algorithm that itself combines more than one strategy onto a single shared portfolio (same concern as the existing algorithm= check), and - an Algorithm with tasks/hooks registered, which the vector engine has no way to execute and would otherwise silently drop.
1 parent e4881ba commit b088f37

2 files changed

Lines changed: 193 additions & 12 deletions

File tree

investing_algorithm_framework/app/app.py

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1641,9 +1641,13 @@ def run_backtest(
16411641
Raises:
16421642
OperationalException: If study is missing, has no
16431643
backtest_windows, or no strategy can be resolved. Also
1644-
raised when ``algorithm=``/``algorithms=`` resolves to
1645-
the vector engine (not supported), or when more than one
1646-
of ``strategy=``/``strategies=``/``algorithm=``/
1644+
raised when the vector engine is used with
1645+
``algorithm=``/``algorithms=`` in a way it can't
1646+
represent — more than one strategy combined onto a
1647+
single shared portfolio, or an Algorithm with
1648+
tasks/hooks registered (silently dropped by the vector
1649+
engine otherwise) — or when more than one of
1650+
``strategy=``/``strategies=``/``algorithm=``/
16471651
``algorithms=`` is provided at once.
16481652
"""
16491653
_modes_given = sum(
@@ -1818,15 +1822,45 @@ def run_backtest(
18181822
)
18191823

18201824
if use_vector and independent_algorithms is not None:
1821-
raise OperationalException(
1822-
"algorithms= (independent Algorithms, each with its own "
1823-
"Tasks/hooks) is only supported by the event-driven "
1824-
"engine. Set study.engine=BacktestEngine.EVENT_DRIVEN, "
1825-
"implement generate_signals(...) instead of "
1826-
"generate_signal_series(...) on your strategies, or "
1827-
"backtest each strategy independently via strategy=/"
1828-
"strategies=."
1829-
)
1825+
# Each Algorithm gets its own portfolio (mirrors
1826+
# strategies=), so multiple single-strategy Algorithms are
1827+
# fine for the vector engine. What genuinely isn't
1828+
# supported is (a) any one Algorithm internally combining
1829+
# more than one strategy onto its own shared portfolio
1830+
# (same concern as `algorithm=` above), and (b) any
1831+
# Algorithm carrying tasks/hooks — the vector engine only
1832+
# ever consumes the flattened strategy list, so those
1833+
# would be silently dropped rather than executed.
1834+
_multi_strategy_algorithms = [
1835+
alg for alg in independent_algorithms
1836+
if len(alg.strategies) > 1
1837+
]
1838+
_algorithms_with_extras = [
1839+
alg for alg in independent_algorithms
1840+
if alg.tasks or alg.on_strategy_run_hooks
1841+
]
1842+
if _multi_strategy_algorithms or _algorithms_with_extras:
1843+
reasons = []
1844+
if _multi_strategy_algorithms:
1845+
reasons.append(
1846+
"one or more Algorithms combine more than one "
1847+
"strategy onto a single shared portfolio"
1848+
)
1849+
if _algorithms_with_extras:
1850+
reasons.append(
1851+
"one or more Algorithms have tasks/hooks "
1852+
"registered, which the vector engine does not "
1853+
"execute and would silently drop"
1854+
)
1855+
raise OperationalException(
1856+
"algorithms= is only supported by the event-driven "
1857+
"engine when " + " and ".join(reasons) + ". Set "
1858+
"study.engine=BacktestEngine.EVENT_DRIVEN, implement "
1859+
"generate_signals(...) instead of "
1860+
"generate_signal_series(...) on your strategies, or "
1861+
"backtest each strategy independently via strategy=/"
1862+
"strategies=."
1863+
)
18301864

18311865
if use_vector:
18321866
if not skip_data_sources_initialization:
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Regression tests for `run_backtest(algorithms=...)` with the
2+
vector engine.
3+
4+
Each `Algorithm` in `algorithms=` gets its own portfolio (mirrors
5+
`strategies=`), so multiple single-strategy Algorithms should be able
6+
to run on the vector engine just like plain `strategies=` would. What
7+
genuinely isn't supported is (a) an Algorithm that itself combines more
8+
than one strategy onto a shared portfolio, and (b) an Algorithm with
9+
tasks/hooks registered, since the vector engine only ever consumes the
10+
flattened strategy list and would silently drop those.
11+
"""
12+
import os
13+
from datetime import timezone, datetime
14+
from pathlib import Path
15+
from typing import Dict, Any
16+
from unittest import TestCase
17+
from uuid import uuid4
18+
19+
from investing_algorithm_framework import create_app, RESOURCE_DIRECTORY, \
20+
TradingStrategy, Algorithm, BacktestDateRange, Schedule, TimeUnit, \
21+
Study, Universe, BacktestWindow, BacktestEngine, Task, \
22+
OperationalException, CSVOHLCVDataProvider, DataSource, DataType
23+
24+
CSV_FILENAME = "OHLCV_BTC-EUR_BITVAVO_2h_LONG_SHORT_CYCLE.csv"
25+
START = datetime(2020, 12, 20, 10, tzinfo=timezone.utc)
26+
END = datetime(2020, 12, 21, 6, tzinfo=timezone.utc)
27+
28+
29+
class VectorTestStrategy(TradingStrategy):
30+
schedule = Schedule.every(2, TimeUnit.HOUR)
31+
market = "BITVAVO"
32+
symbols = ["BTC"]
33+
34+
def __init__(self, algorithm_id):
35+
super().__init__(
36+
algorithm_id=algorithm_id,
37+
data_sources=[
38+
DataSource(
39+
identifier="BTC_EUR_OHLCV",
40+
data_type=DataType.OHLCV,
41+
time_frame="2h",
42+
market="BITVAVO",
43+
symbol="BTC/EUR",
44+
warmup_window=5,
45+
pandas=True,
46+
)
47+
],
48+
)
49+
50+
def generate_signal_series(self, data: Dict[str, Any]):
51+
return iter(())
52+
53+
54+
class NoOpTask(Task):
55+
schedule = Schedule.every(1, TimeUnit.MINUTE)
56+
57+
def run(self, algorithm):
58+
pass
59+
60+
61+
class TestIndependentAlgorithmsVectorEngine(TestCase):
62+
def setUp(self) -> None:
63+
self.resource_dir = os.path.join(
64+
os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
65+
"resources"
66+
)
67+
68+
def _app(self):
69+
app = create_app(
70+
name=f"VectorIndependentAlgorithms{uuid4().hex}",
71+
config={RESOURCE_DIRECTORY: self.resource_dir},
72+
)
73+
app.add_market(market="BITVAVO", trading_symbol="EUR")
74+
csv_path = Path(self.resource_dir) / "test_data" / "ohlcv" \
75+
/ CSV_FILENAME
76+
app.add_data_provider(
77+
data_provider=CSVOHLCVDataProvider(
78+
storage_path=str(csv_path),
79+
symbol="BTC/EUR",
80+
time_frame="2h",
81+
market="BITVAVO",
82+
warmup_window=5,
83+
),
84+
priority=1,
85+
)
86+
return app
87+
88+
def _study(self):
89+
return Study(
90+
universe=Universe(market="BITVAVO", trading_symbol="EUR"),
91+
initial_capital=1000,
92+
risk_free_rate=0.027,
93+
backtest_windows=[
94+
BacktestWindow(
95+
train_range=BacktestDateRange(
96+
start_date=START, end_date=END
97+
)
98+
)
99+
],
100+
engines=[BacktestEngine.VECTOR],
101+
)
102+
103+
def test_single_strategy_algorithms_allowed_with_vector_engine(self):
104+
app = self._app()
105+
algorithm_one = Algorithm(
106+
algorithm_id="algo_one",
107+
strategy=VectorTestStrategy(algorithm_id="algo_one"),
108+
)
109+
algorithm_two = Algorithm(
110+
algorithm_id="algo_two",
111+
strategy=VectorTestStrategy(algorithm_id="algo_two"),
112+
)
113+
114+
backtests = app.run_backtest(
115+
algorithms=[algorithm_one, algorithm_two], study=self._study()
116+
)
117+
118+
self.assertEqual(2, len(backtests))
119+
120+
def test_multi_strategy_algorithm_rejected_with_vector_engine(self):
121+
app = self._app()
122+
strategy_a = VectorTestStrategy(algorithm_id="algo_multi")
123+
strategy_a.strategy_id = "strategy_a"
124+
strategy_b = VectorTestStrategy(algorithm_id="algo_multi")
125+
strategy_b.strategy_id = "strategy_b"
126+
algorithm = Algorithm(
127+
algorithm_id="algo_multi",
128+
strategies=[strategy_a, strategy_b],
129+
)
130+
131+
with self.assertRaises(OperationalException) as ctx:
132+
app.run_backtest(algorithms=[algorithm], study=self._study())
133+
134+
self.assertIn("shared portfolio", str(ctx.exception))
135+
136+
def test_algorithm_with_tasks_rejected_with_vector_engine(self):
137+
app = self._app()
138+
algorithm = Algorithm(
139+
algorithm_id="algo_tasks",
140+
strategy=VectorTestStrategy(algorithm_id="algo_tasks"),
141+
tasks=[NoOpTask],
142+
)
143+
144+
with self.assertRaises(OperationalException) as ctx:
145+
app.run_backtest(algorithms=[algorithm], study=self._study())
146+
147+
self.assertIn("tasks/hooks", str(ctx.exception))

0 commit comments

Comments
 (0)