galaxis-po/backend/tests/unit/test_walkforward.py
머니페니 f818bd3290 feat: add walk-forward analysis for backtests
- 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>
2026-03-18 22:33:41 +09:00

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