summaryrefslogtreecommitdiff
path: root/services/backtester/tests/test_walk_forward.py
diff options
context:
space:
mode:
authorTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 18:23:46 +0900
committerTheSiahxyz <164138827+TheSiahxyz@users.noreply.github.com>2026-04-01 18:23:46 +0900
commit9e82c51dfde3941189db1b2d62dcc239442b9dc6 (patch)
tree2c19068a4b2e381d44d22d662ec095794e144180 /services/backtester/tests/test_walk_forward.py
parentcd3a06c7788ad8a747b1b4579fb6c45b6c43008e (diff)
feat(backtester): add walk-forward analysis engine
Diffstat (limited to 'services/backtester/tests/test_walk_forward.py')
-rw-r--r--services/backtester/tests/test_walk_forward.py103
1 files changed, 103 insertions, 0 deletions
diff --git a/services/backtester/tests/test_walk_forward.py b/services/backtester/tests/test_walk_forward.py
new file mode 100644
index 0000000..e672dac
--- /dev/null
+++ b/services/backtester/tests/test_walk_forward.py
@@ -0,0 +1,103 @@
+"""Tests for walk-forward analysis."""
+import sys
+from pathlib import Path
+from decimal import Decimal
+from datetime import datetime, timedelta, timezone
+
+import pytest
+
+sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
+sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "strategy-engine"))
+
+from shared.models import Candle
+from backtester.walk_forward import WalkForwardEngine, WalkForwardResult
+from strategies.rsi_strategy import RsiStrategy
+
+
+def _generate_candles(n=100, base_price=100.0):
+ candles = []
+ for i in range(n):
+ # Simple oscillating price
+ price = base_price + (i % 20) - 10
+ candles.append(Candle(
+ symbol="BTCUSDT", timeframe="1h",
+ open_time=datetime(2025, 1, 1, tzinfo=timezone.utc) + timedelta(hours=i),
+ open=Decimal(str(price)),
+ high=Decimal(str(price + 5)),
+ low=Decimal(str(price - 5)),
+ close=Decimal(str(price)),
+ volume=Decimal("100"),
+ ))
+ return candles
+
+
+def test_walk_forward_basic():
+ param_grid = [
+ {"period": 5, "oversold": 30, "overbought": 70, "quantity": "0.1"},
+ {"period": 10, "oversold": 25, "overbought": 75, "quantity": "0.1"},
+ ]
+ engine = WalkForwardEngine(
+ strategy_factory=RsiStrategy,
+ param_grid=param_grid,
+ initial_balance=Decimal("10000"),
+ num_windows=3,
+ )
+ candles = _generate_candles(150)
+ result = engine.run(candles)
+
+ assert isinstance(result, WalkForwardResult)
+ assert result.strategy_name == "rsi"
+ assert result.num_windows > 0
+
+
+def test_walk_forward_efficiency_ratio():
+ param_grid = [
+ {"period": 5, "oversold": 30, "overbought": 70, "quantity": "0.1"},
+ ]
+ engine = WalkForwardEngine(
+ strategy_factory=RsiStrategy,
+ param_grid=param_grid,
+ num_windows=2,
+ )
+ candles = _generate_candles(100)
+ result = engine.run(candles)
+
+ # Efficiency ratio should be a finite number
+ assert isinstance(result.efficiency_ratio, float)
+
+
+def test_walk_forward_empty_candles():
+ engine = WalkForwardEngine(
+ strategy_factory=RsiStrategy,
+ param_grid=[{"period": 5}],
+ )
+ result = engine.run([])
+ assert result.num_windows == 0
+
+
+def test_walk_forward_too_few_candles():
+ engine = WalkForwardEngine(
+ strategy_factory=RsiStrategy,
+ param_grid=[{"period": 5}],
+ num_windows=10,
+ )
+ candles = _generate_candles(20)
+ result = engine.run(candles)
+ assert result.num_windows == 0 # window_size < 10
+
+
+def test_walk_forward_selects_best_params():
+ param_grid = [
+ {"period": 3, "oversold": 40, "overbought": 60, "quantity": "0.1"}, # very aggressive
+ {"period": 14, "oversold": 30, "overbought": 70, "quantity": "0.1"}, # standard
+ ]
+ engine = WalkForwardEngine(
+ strategy_factory=RsiStrategy,
+ param_grid=param_grid,
+ num_windows=2,
+ )
+ candles = _generate_candles(200)
+ result = engine.run(candles)
+
+ for window in result.windows:
+ assert window.best_params in param_grid