Skip to content

Commit cbfd597

Browse files
authored
Merge pull request #398 from coding-kitties/feature/warn-scheduling-interval-faster-than-data-timeframe-396
feat: Warn or prevent scheduling interval faster than data timeframe (#396)
2 parents be80c86 + c052ced commit cbfd597

3 files changed

Lines changed: 199 additions & 3 deletions

File tree

investing_algorithm_framework/app/strategy.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from investing_algorithm_framework.domain import OperationalException, \
88
Position, PositionSize, TimeUnit, StrategyProfile, Trade, \
9-
DataSource, OrderSide, StopLossRule, TakeProfitRule, Order, \
9+
DataSource, DataType, OrderSide, StopLossRule, TakeProfitRule, Order, \
1010
INDEX_DATETIME
1111
from .context import Context
1212

@@ -156,6 +156,32 @@ def __init__(
156156
f"Interval not set for strategy instance {self.strategy_id}"
157157
)
158158

159+
# Check if scheduling interval is faster than the smallest
160+
# OHLCV data source timeframe
161+
ohlcv_timeframes = [
162+
ds.time_frame.amount_of_minutes
163+
for ds in self.data_sources
164+
if ds.time_frame is not None
165+
and DataType.OHLCV.equals(ds.data_type)
166+
]
167+
168+
if ohlcv_timeframes:
169+
scheduling_interval = \
170+
self.time_unit.amount_of_minutes * self.interval
171+
smallest_timeframe = min(ohlcv_timeframes)
172+
173+
if scheduling_interval < smallest_timeframe:
174+
raise OperationalException(
175+
f"Strategy '{self.strategy_id}' scheduling interval "
176+
f"({self.interval} {self.time_unit.value.lower()}"
177+
f"{'s' if self.interval > 1 else ''}"
178+
f" = {scheduling_interval} min) is faster than "
179+
f"the smallest OHLCV data source timeframe "
180+
f"({smallest_timeframe} min). The strategy would "
181+
f"run without new data. Increase the scheduling "
182+
f"interval or use a smaller data timeframe."
183+
)
184+
159185
# Initialize stop_losses as a new list per instance
160186
if stop_losses is not None:
161187
self.stop_losses = list(stop_losses)

tests/app/test_data_completeness.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from tests.resources import TestBase
1111

1212
class TestStrategy(TradingStrategy):
13-
time_unit = "MINUTE"
13+
time_unit = "DAY"
1414
interval = 1
1515
data_sources = [
1616
DataSource(
@@ -35,7 +35,7 @@ def generate_buy_signals(
3535

3636

3737
class TestStrategyIncompleteData(TradingStrategy):
38-
time_unit = "MINUTE"
38+
time_unit = "DAY"
3939
interval = 1
4040
data_sources = [
4141
DataSource(
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
"""
2+
Tests for the scheduling interval vs OHLCV data timeframe validation
3+
added in TradingStrategy.__init__ (issue #396).
4+
5+
The validation raises OperationalException when the strategy's scheduling
6+
interval (time_unit.amount_of_minutes * interval) is strictly less than
7+
the smallest OHLCV data source timeframe.
8+
"""
9+
from unittest import TestCase
10+
11+
from investing_algorithm_framework.app.strategy import TradingStrategy
12+
from investing_algorithm_framework.domain import (
13+
OperationalException, TimeUnit, DataType, TimeFrame,
14+
)
15+
from investing_algorithm_framework.domain.models.data.data_source import (
16+
DataSource,
17+
)
18+
19+
20+
class _ConcreteStrategy(TradingStrategy):
21+
"""Minimal concrete subclass for testing __init__ validation."""
22+
23+
def run_strategy(self, context, data):
24+
pass
25+
26+
27+
class TestStrategyIntervalValidation(TestCase):
28+
29+
# ------------------------------------------------------------------
30+
# 1. Should raise when interval is faster than the OHLCV timeframe
31+
# ------------------------------------------------------------------
32+
def test_raises_when_interval_faster_than_ohlcv_timeframe(self):
33+
"""1 minute interval < 60 minute (1h) OHLCV → must raise."""
34+
with self.assertRaises(OperationalException):
35+
_ConcreteStrategy(
36+
time_unit=TimeUnit.MINUTE,
37+
interval=1,
38+
data_sources=[
39+
DataSource(
40+
symbol="BTC/EUR",
41+
data_type=DataType.OHLCV,
42+
time_frame=TimeFrame.ONE_HOUR,
43+
market="BITVAVO",
44+
warmup_window=100,
45+
)
46+
],
47+
)
48+
49+
# ------------------------------------------------------------------
50+
# 2. No error when interval exactly matches the OHLCV timeframe
51+
# ------------------------------------------------------------------
52+
def test_no_error_when_interval_matches_ohlcv_timeframe(self):
53+
"""1 hour interval == 60 min == 1h OHLCV → should NOT raise."""
54+
strategy = _ConcreteStrategy(
55+
time_unit=TimeUnit.HOUR,
56+
interval=1,
57+
data_sources=[
58+
DataSource(
59+
symbol="BTC/EUR",
60+
data_type=DataType.OHLCV,
61+
time_frame=TimeFrame.ONE_HOUR,
62+
market="BITVAVO",
63+
warmup_window=100,
64+
)
65+
],
66+
)
67+
self.assertIsNotNone(strategy)
68+
69+
# ------------------------------------------------------------------
70+
# 3. No error when interval is slower than the OHLCV timeframe
71+
# ------------------------------------------------------------------
72+
def test_no_error_when_interval_slower_than_ohlcv_timeframe(self):
73+
"""1 day interval (1440 min) > 60 min (1h) OHLCV → should NOT raise."""
74+
strategy = _ConcreteStrategy(
75+
time_unit=TimeUnit.DAY,
76+
interval=1,
77+
data_sources=[
78+
DataSource(
79+
symbol="BTC/EUR",
80+
data_type=DataType.OHLCV,
81+
time_frame=TimeFrame.ONE_HOUR,
82+
market="BITVAVO",
83+
warmup_window=100,
84+
)
85+
],
86+
)
87+
self.assertIsNotNone(strategy)
88+
89+
# ------------------------------------------------------------------
90+
# 4. Validation uses the *smallest* OHLCV timeframe among sources
91+
# ------------------------------------------------------------------
92+
def test_uses_smallest_ohlcv_timeframe_for_validation(self):
93+
"""5 min interval < 15 min (smallest OHLCV) → must raise."""
94+
with self.assertRaises(OperationalException):
95+
_ConcreteStrategy(
96+
time_unit=TimeUnit.MINUTE,
97+
interval=5,
98+
data_sources=[
99+
DataSource(
100+
symbol="BTC/EUR",
101+
data_type=DataType.OHLCV,
102+
time_frame=TimeFrame.ONE_HOUR,
103+
market="BITVAVO",
104+
warmup_window=100,
105+
),
106+
DataSource(
107+
symbol="ETH/EUR",
108+
data_type=DataType.OHLCV,
109+
time_frame=TimeFrame.FIFTEEN_MINUTE,
110+
market="BITVAVO",
111+
warmup_window=100,
112+
),
113+
],
114+
)
115+
116+
# ------------------------------------------------------------------
117+
# 5. No validation when there are no OHLCV data sources at all
118+
# ------------------------------------------------------------------
119+
def test_no_validation_when_no_ohlcv_data_sources(self):
120+
"""No OHLCV sources → nothing to compare → should NOT raise."""
121+
strategy = _ConcreteStrategy(
122+
time_unit=TimeUnit.MINUTE,
123+
interval=1,
124+
data_sources=[],
125+
)
126+
self.assertIsNotNone(strategy)
127+
128+
# ------------------------------------------------------------------
129+
# 6. Non-OHLCV sources (no time_frame) are skipped
130+
# ------------------------------------------------------------------
131+
def test_skips_data_sources_without_timeframe(self):
132+
"""Ticker source has no time_frame → should NOT raise."""
133+
strategy = _ConcreteStrategy(
134+
time_unit=TimeUnit.MINUTE,
135+
interval=1,
136+
data_sources=[
137+
DataSource(
138+
symbol="BTC/EUR",
139+
data_type=DataType.TICKER,
140+
market="BITVAVO",
141+
)
142+
],
143+
)
144+
self.assertIsNotNone(strategy)
145+
146+
# ------------------------------------------------------------------
147+
# 7. Error message is descriptive
148+
# ------------------------------------------------------------------
149+
def test_raises_with_descriptive_error_message(self):
150+
"""Exception message should contain interval info AND timeframe info."""
151+
with self.assertRaises(OperationalException) as cm:
152+
_ConcreteStrategy(
153+
time_unit=TimeUnit.MINUTE,
154+
interval=1,
155+
data_sources=[
156+
DataSource(
157+
symbol="BTC/EUR",
158+
data_type=DataType.OHLCV,
159+
time_frame=TimeFrame.ONE_HOUR,
160+
market="BITVAVO",
161+
warmup_window=100,
162+
)
163+
],
164+
)
165+
166+
msg = str(cm.exception)
167+
# Verify scheduling interval info is present
168+
self.assertIn("1 min", msg)
169+
# Verify OHLCV timeframe info is present
170+
self.assertIn("60 min", msg)

0 commit comments

Comments
 (0)