#!/usr/bin/env python3 """ WALK-FORWARD ВАЛИДАЦИЯ грид-параметров. Зачем: перебор по всему окну находит параметр, который лучше всего описал ПРОШЛОЕ. Канон арены знает эту ловушку как «V82a / small-sample illusion»: комбо со 100% WR на 30 сделках через 2 недели даёт 30%. Честный тест: делим историю на куски. На куске N подбираем лучший шаг, проверяем его на куске N+1 (который при подборе НЕ ВИДЕЛИ). Если параметр переносится — он настоящий. Если каждый раз новый — это подгонка. Запуск: python3 walk_forward.py SOLUSDT python3 walk_forward.py SOLUSDT --folds 6 """ import sys, os, statistics sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from grid_sim import load, simulate STEPS = [0.2, 0.3, 0.5, 0.8, 1.0, 1.2, 1.6, 2.0, 2.5, 3.0] LEVELS = 20 def main(): sym = sys.argv[1] if len(sys.argv) > 1 else "SOLUSDT" folds = int(sys.argv[sys.argv.index("--folds") + 1]) if "--folds" in sys.argv else 5 metric = "realized_apr" # хеджированный доход: только реализованное candles = load(sym) n = len(candles) // folds chunks = [candles[i*n:(i+1)*n] for i in range(folds)] print(f"=== WALK-FORWARD: {sym}, {folds} окон по {n/1440:.0f} дней ===") print(f"метрика: реализованный APR (то, что остаётся при хедже инвентаря)\n") print(f"{'окно':>5} {'цена%':>8} {'лучший шаг':>11} {'его APR':>9} → {'на след. окне':>13}") print("-" * 72) oos_results = [] prev_best = None for i, ch in enumerate(chunks): scores = {s: simulate(ch, s, LEVELS)[metric] for s in STEPS} best = max(scores, key=scores.get) pchg = (ch[-1][4] / ch[0][4] - 1) * 100 oos_txt = "" if prev_best is not None: oos = scores[prev_best] oos_results.append((prev_best, oos, scores[best])) gap = oos - scores[best] oos_txt = f"шаг {prev_best}% дал {oos:+7.1f}% (лучший {scores[best]:+.1f}%, разрыв {gap:+.1f})" print(f"{i+1:>5} {pchg:>+7.1f}% {best:>10}% {scores[best]:>+8.1f}% {oos_txt}") prev_best = best print("\n" + "=" * 72) if oos_results: oos_aprs = [o for _, o, _ in oos_results] best_aprs = [b for _, _, b in oos_results] print(f"ВНЕ-ВЫБОРОЧНЫЙ результат (переносим шаг с прошлого окна):") print(f" средний APR OOS : {statistics.mean(oos_aprs):+.1f}%") print(f" средний APR «идеальный»: {statistics.mean(best_aprs):+.1f}% (недостижим — знание будущего)") print(f" потеря на переносе : {statistics.mean(oos_aprs)-statistics.mean(best_aprs):+.1f} п.п.") print(f" положительных окон OOS : {sum(1 for a in oos_aprs if a>0)}/{len(oos_aprs)}") # устойчивость: как ведёт себя КАЖДЫЙ шаг на всех окнах print(f"\n=== УСТОЙЧИВОСТЬ ШАГА (реализ. APR по окнам) ===") print(f"{'шаг':>6} " + " ".join(f"{'окно'+str(i+1):>8}" for i in range(folds)) + f" {'среднее':>9} {'мин':>8}") print("-" * (7 + 9*folds + 19)) rows = [] for s in STEPS: vals = [simulate(ch, s, LEVELS)[metric] for ch in chunks] rows.append((statistics.mean(vals), min(vals), s, vals)) print(f"{s:>5}% " + " ".join(f"{v:>+8.1f}" for v in vals) + f" {statistics.mean(vals):>+9.1f} {min(vals):>+8.1f}") rows.sort(reverse=True) print(f"\nЛУЧШИЙ ПО СРЕДНЕМУ: шаг {rows[0][2]}% → {rows[0][0]:+.1f}% средний, худшее окно {rows[0][1]:+.1f}%") robust = [r for r in rows if r[1] > 0] if robust: print(f"ЛУЧШИЙ БЕЗ УБЫТОЧНЫХ ОКОН: шаг {robust[0][2]}% → {robust[0][0]:+.1f}% средний, " f"худшее {robust[0][1]:+.1f}% ← это и есть кандидат в прод") else: print("НИ ОДИН шаг не был прибыльным во всех окнах.") if __name__ == "__main__": main()