diff --git a/backend_api_python/app/services/backtest_limits.py b/backend_api_python/app/services/backtest_limits.py index aa72feaa..37025b0a 100644 --- a/backend_api_python/app/services/backtest_limits.py +++ b/backend_api_python/app/services/backtest_limits.py @@ -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, @@ -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", @@ -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, } diff --git a/backend_api_python/tests/test_backtest_limits.py b/backend_api_python/tests/test_backtest_limits.py new file mode 100644 index 00000000..eb49e738 --- /dev/null +++ b/backend_api_python/tests/test_backtest_limits.py @@ -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"]