import math import numpy as np import pandas as pd from ktrade.backtest.metrics import block_bootstrap_ci, deflated_sharpe, drawdown_series from ktrade.backtest.validation import param_grid, walk_forward from ktrade.strategy.meanrev import ou_half_life from ktrade.strategy.momentum import TimeSeriesMomentum from tests.conftest import synthetic_candles def test_drawdown(): dd = drawdown_series(pd.Series([100, 110, 99, 120, 60])) assert math.isclose(dd.min(), 0.5 - 1) def test_ou_half_life_detects_mean_reversion_and_random_walk(): rng = np.random.default_rng(0) x = np.zeros(2000) for i in range(1, 2000): x[i] = 0.9 * x[i - 1] + rng.normal() hl = ou_half_life(pd.Series(x)) assert 4 < hl < 10 # theory: -ln2/ln(0.9) ≈ 6.6 rw = pd.Series(np.cumsum(rng.normal(size=2000))) assert math.isnan(ou_half_life(rw)) or ou_half_life(rw) > 100 def test_bootstrap_ci_contains_point_estimate(): rng = np.random.default_rng(1) r = rng.normal(0.0005, 0.01, 2000) bpy = 365 * 24 ci = block_bootstrap_ci(r, bpy, n_boot=200) s = r.mean() / r.std(ddof=1) * math.sqrt(bpy) assert ci["sharpe_ci"][0] <= s <= ci["sharpe_ci"][1] def test_deflated_sharpe_decreases_with_trials(): a = deflated_sharpe(1.5, 2000, 365 * 24, 1, 0.0, 0.0)["dsr"] b = deflated_sharpe(1.5, 2000, 365 * 24, 100, 0.0, 0.0)["dsr"] assert a > b def test_walk_forward_produces_oos_folds(): df = synthetic_candles(n=1500, seed=11, drift=0.0005) wf = walk_forward(TimeSeriesMomentum, df, interval_min=60, grid={"lookback": [24, 48], "ema_fast": [10], "ema_slow": [40]}, n_folds=3) assert len(wf["folds"]) == 3 and wf["n_trials"] == 2 for f in wf["folds"]: assert f["train"][1] < f["test"][0] # OOS strictly after IS assert len(param_grid(TimeSeriesMomentum, {"lookback": [1, 24]})) == 1 # 1 < min → dropped