- Add WalkForwardResult model with train/test window metrics
- Create WalkForwardEngine that reuses existing BacktestEngine
with rolling train/test window splits
- Add POST/GET /api/backtest/{id}/walkforward endpoints
- Add Walk-forward tab to backtest detail page with parameter
controls, cumulative return chart, and window results table
- Add Alembic migration for walkforward_results table
- Add 8 unit tests for window generation logic (100 total passed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""
|
|
Unit tests for WalkForwardEngine window generation logic.
|
|
"""
|
|
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from app.services.backtest.walkforward_engine import WalkForwardEngine, Window
|
|
|
|
|
|
class TestGenerateWindows:
|
|
"""Test _generate_windows static method."""
|
|
|
|
def test_basic_windows(self):
|
|
"""2-year period with 12m train, 3m test, 3m step -> 4 windows."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2020, 1, 1),
|
|
end=date(2021, 12, 31),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=3,
|
|
)
|
|
assert len(windows) == 4
|
|
assert windows[0].index == 0
|
|
assert windows[0].train_start == date(2020, 1, 1)
|
|
assert windows[0].train_end == date(2020, 12, 31)
|
|
assert windows[0].test_start == date(2021, 1, 1)
|
|
assert windows[0].test_end == date(2021, 3, 31)
|
|
|
|
def test_single_window(self):
|
|
"""Exactly 15 months -> 1 window with 12m train + 3m test."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2020, 1, 1),
|
|
end=date(2021, 3, 31),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=3,
|
|
)
|
|
assert len(windows) == 1
|
|
assert windows[0].train_start == date(2020, 1, 1)
|
|
assert windows[0].test_end == date(2021, 3, 31)
|
|
|
|
def test_no_windows_period_too_short(self):
|
|
"""Period shorter than train + test -> 0 windows."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2020, 1, 1),
|
|
end=date(2020, 12, 31),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=3,
|
|
)
|
|
assert len(windows) == 0
|
|
|
|
def test_partial_last_window(self):
|
|
"""Last window with partial test period is included."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2020, 1, 1),
|
|
end=date(2021, 2, 15),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=3,
|
|
)
|
|
assert len(windows) == 1
|
|
assert windows[0].test_end == date(2021, 2, 15)
|
|
|
|
def test_step_larger_than_test(self):
|
|
"""step_months > test_months creates non-overlapping test windows."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2019, 1, 1),
|
|
end=date(2022, 12, 31),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=6,
|
|
)
|
|
assert len(windows) >= 2
|
|
# test windows should not overlap
|
|
for i in range(1, len(windows)):
|
|
assert windows[i].test_start > windows[i - 1].test_end
|
|
|
|
def test_monthly_step(self):
|
|
"""step_months=1 creates many overlapping windows."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2020, 1, 1),
|
|
end=date(2021, 6, 30),
|
|
train_months=6,
|
|
test_months=3,
|
|
step_months=1,
|
|
)
|
|
assert len(windows) >= 9
|
|
|
|
def test_window_indices_sequential(self):
|
|
"""Window indices should be sequential starting from 0."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2019, 1, 1),
|
|
end=date(2022, 12, 31),
|
|
train_months=12,
|
|
test_months=3,
|
|
step_months=3,
|
|
)
|
|
for i, w in enumerate(windows):
|
|
assert w.index == i
|
|
|
|
def test_window_dates_consistent(self):
|
|
"""train_end < test_start and test_start <= test_end for all windows."""
|
|
windows = WalkForwardEngine._generate_windows(
|
|
start=date(2019, 1, 1),
|
|
end=date(2023, 12, 31),
|
|
train_months=12,
|
|
test_months=6,
|
|
step_months=3,
|
|
)
|
|
for w in windows:
|
|
assert w.train_start < w.train_end
|
|
assert w.train_end < w.test_start
|
|
assert w.test_start <= w.test_end
|