research(5m): прогон 14 методов на 250 парах, сверка движка, издержки, графики

ЧТО: code/ (стенд и методы), results/ (сводки), charts/ (3 графика), reports/ (издержки, плацебо, по годам), docs/REJECTED.md.

ПОЧЕМУ: цель — найти кандидатов на 5м или доказать с цифрами их отсутствие.

КАК ПРОВЕРЕНО: движок сверен с прод-движком TON-бота 1:1; плацебо и рандом-контроль в отчёте; бутстрап по дням.
This commit is contained in:
2026-09-29 02:46:08 +03:00
parent 7b048534ff
commit c6671410dc
26 changed files with 5514 additions and 2 deletions
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Сверка движка 5м-стенда с ПРОДОВЫМ движком TON-бота (обязательный шаг Ф1).
Делает ровно то, что требует цель: берёт 5м-данные, ресемплит в 30м, считает индикаторы
нашим стендом и прогоняет ОДНИ И ТЕ ЖЕ правила двумя движками:
* прод: app.trading.mean_reversion.run_mr_backtest (импорт из /opt/data/work/tonbot_src);
* стенд: tools/mr_engine.stand_mr_backtest (независимая машина состояний).
Расхождение PF больше 5% означает ошибку в машине состояний (не в правилах входа) — тогда
правится движок, а не цифры.
Результат: reports/stand_reconcile.json {pairs: {SYM: {prod_n, stand_n, prod_pf, stand_pf}}}
"""
from __future__ import annotations
import json
import os
import sys
import pandas as pd
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
sys.path.insert(0, "/opt/data/work/tonbot_src")
from tools.mr_engine import (MRParamsStand, load_5m, prepare_frame, resample_30m, # noqa: E402
metrics as stand_metrics, stand_mr_backtest)
from app.trading.mean_reversion import MRParams, prepare_mr_frame, run_mr_backtest # noqa: E402
FEE_RT = 0.0011 # 0,055% тейкер × 2 ноги = 0,11% на круг
def main() -> int:
pairs = sys.argv[1:] or ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "ZECUSDT"]
out = {}
for sym in pairs:
path = os.path.join(ROOT, "data", "5m", f"{sym}_5m.csv")
if not os.path.isfile(path):
print(f"{sym}: нет файла 5м — пропуск")
continue
df30 = resample_30m(load_5m(path))
frame = prepare_frame(df30)
prod_frame = prepare_mr_frame(frame.copy(), MRParams())
prod = run_mr_backtest(prod_frame, MRParams(), FEE_RT)
prod_m = prod["trades"]
stand = stand_mr_backtest(frame, MRParamsStand(), FEE_RT)
stand_m = stand["metrics"]
out[sym] = {"prod_n": int(prod_m["total_trades"]), "stand_n": int(stand_m["total_trades"]),
"prod_pf": round(float(prod_m["profit_factor"]), 4),
"stand_pf": round(float(stand_m["profit_factor"]), 4),
"prod_sum_pnl": round(float(prod_m.get("total_return", 0.0)), 4),
"stand_sum_pnl": round(float(stand_m["sum_pnl"]), 4),
"bars_30m": int(len(df30))}
print(f"{sym}: 30м баров {len(df30)}, прод n={out[sym]['prod_n']} PF={out[sym]['prod_pf']}, "
f"стенд n={out[sym]['stand_n']} PF={out[sym]['stand_pf']}")
os.makedirs(os.path.join(ROOT, "reports"), exist_ok=True)
with open(os.path.join(ROOT, "reports", "stand_reconcile.json"), "w") as fh:
json.dump({"pairs": out, "fee_rt": FEE_RT, "engine": "app.trading.mean_reversion.run_mr_backtest",
"stand": "tools/mr_engine.stand_mr_backtest", "source": "30м из ресемпла 5м данных"},
fh, ensure_ascii=False, indent=1)
print("записано reports/stand_reconcile.json, пар:", len(out))
return 0
if __name__ == "__main__":
sys.exit(main())