Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion backend_api_python/app/services/backtest_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,16 @@ def backtest_range_policy(market: str, timeframe: str) -> BacktestRangePolicy:
)


def _date_limit_start(end_date: datetime, max_days: int, warmup_seconds: int) -> datetime:
"""Return a date-only friendly start that keeps the fetch window under max_days."""
return end_date - timedelta(days=max(0, int(max_days) - 1)) + timedelta(seconds=warmup_seconds)


def _date_limit_end(fetch_start: datetime, max_days: int) -> datetime:
"""Return a date-only friendly end that keeps the fetch window under max_days."""
return fetch_start + timedelta(days=max(0, int(max_days) - 1))


def validate_backtest_range(
*,
market: str,
Expand All @@ -106,11 +116,21 @@ def validate_backtest_range(
warmup_note = ""
if warmup_bars:
warmup_note = f" including {int(warmup_bars)} warmup bars"
recommended_start = _date_limit_start(end_date, policy.max_days, warmup_seconds)
if recommended_start > end_date:
recommended_start = end_date
recommended_end = _date_limit_end(fetch_start, policy.max_days)
if recommended_end > end_date:
recommended_end = end_date
recommended_start_str = recommended_start.strftime("%Y-%m-%d")
recommended_end_str = recommended_end.strftime("%Y-%m-%d")
msg = (
f"Backtest range exceeds limit: {market}:{symbol} timeframe {timeframe} "
f"supports up to {policy.label} ({policy.max_days} days) because of the "
f"{policy.reason}, but this request needs {fetch_days} days{warmup_note}. "
f"Please shorten the date range or use a higher timeframe."
f"Please shorten the date range or use a higher timeframe. "
f"Suggested range: {recommended_start_str} to {end_date.strftime('%Y-%m-%d')} "
f"(or keep the current start and end by {recommended_end_str})."
)
return {
"error_type": "BACKTEST_RANGE_LIMIT",
Expand All @@ -127,4 +147,6 @@ def validate_backtest_range(
"fetch_start": fetch_start.strftime("%Y-%m-%d"),
"requested_start": start_date.strftime("%Y-%m-%d"),
"requested_end": end_date.strftime("%Y-%m-%d"),
"recommended_start": recommended_start_str,
"recommended_end": recommended_end_str,
}
39 changes: 39 additions & 0 deletions backend_api_python/tests/test_backtest_limits.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from datetime import datetime

from app.services.backtest_limits import validate_backtest_range


def test_forex_intraday_range_error_includes_actionable_recommendation():
err = validate_backtest_range(
market="Forex",
symbol="EURUSD",
timeframe="15m",
start_date=datetime(2024, 1, 1),
end_date=datetime(2024, 4, 1, 23, 59, 59),
)

assert err is not None
assert err["error_type"] == "BACKTEST_RANGE_LIMIT"
assert err["max_days"] == 60
assert err["recommended_start"] == "2024-02-02"
assert err["recommended_end"] == "2024-02-29"
assert "Suggested range: 2024-02-02 to 2024-04-01" in err["msg"]
assert "end by 2024-02-29" in err["msg"]


def test_recommendation_accounts_for_indicator_warmup_bars():
err = validate_backtest_range(
market="Forex",
symbol="EURUSD",
timeframe="15m",
start_date=datetime(2024, 1, 1),
end_date=datetime(2024, 4, 1, 23, 59, 59),
warmup_bars=96,
)

assert err is not None
assert err["warmup_bars"] == 96
assert err["fetch_start"] == "2023-12-31"
assert err["recommended_start"] == "2024-02-03"
assert err["recommended_end"] == "2024-02-28"
assert "including 96 warmup bars" in err["msg"]