52 lines
2.0 KiB
Python
52 lines
2.0 KiB
Python
from datetime import date, timedelta
|
|
from decimal import Decimal
|
|
|
|
from app.service.allocation_backtest_service import AllocationBacktestService, BacktestPrice
|
|
|
|
|
|
def series(start: str, daily_change_pct: str, turnover: str | None) -> list[BacktestPrice]:
|
|
value = Decimal("100")
|
|
start_date = date.fromisoformat(start)
|
|
points: list[BacktestPrice] = []
|
|
for offset in range(150):
|
|
value *= Decimal("1") + Decimal(daily_change_pct) / Decimal("100")
|
|
points.append(BacktestPrice(
|
|
trade_date=start_date + timedelta(days=offset),
|
|
close_price=value,
|
|
turnover_amount=Decimal(turnover) if turnover is not None else None,
|
|
))
|
|
return points
|
|
|
|
|
|
def test_walk_forward_backtest_compares_dynamic_and_static_without_lookahead() -> None:
|
|
result = AllocationBacktestService.run(
|
|
{
|
|
"cash_management_etf": series("2026-01-01", "0.01", "50000000"),
|
|
"bond_etf": series("2026-01-01", "0.03", "8000000"),
|
|
"equity_etf": series("2026-01-01", "0.08", "10000000"),
|
|
},
|
|
{"cash_management_etf": 15, "bond_etf": 45, "equity_etf": 40},
|
|
return_target_lower_pct=Decimal("6"),
|
|
max_drawdown_pct=Decimal("15"),
|
|
liquidity_requirement="within_30_days",
|
|
)
|
|
|
|
assert result.status == "ready"
|
|
assert result.observation_count > 0
|
|
assert result.dynamic_rebalance_count > 0
|
|
assert result.dynamic_total_return_pct is not None
|
|
assert result.static_total_return_pct is not None
|
|
|
|
|
|
def test_backtest_refuses_to_compare_when_a_quality_approved_asset_class_is_missing() -> None:
|
|
result = AllocationBacktestService.run(
|
|
{"bond_etf": series("2026-01-01", "0.03", None)},
|
|
{"cash_management_etf": 15, "bond_etf": 45, "equity_etf": 40},
|
|
return_target_lower_pct=Decimal("6"),
|
|
max_drawdown_pct=Decimal("15"),
|
|
liquidity_requirement="within_30_days",
|
|
)
|
|
|
|
assert result.status == "data_quality_required"
|
|
assert result.limitations == ("missing_quality_approved_asset_class",)
|