Archived
ЧТО: code/ (стенд и методы), results/ (сводки), charts/ (3 графика), reports/ (издержки, плацебо, по годам), docs/REJECTED.md. ПОЧЕМУ: цель — найти кандидатов на 5м или доказать с цифрами их отсутствие. КАК ПРОВЕРЕНО: движок сверен с прод-движком TON-бота 1:1; плацебо и рандом-контроль в отчёте; бутстрап по дням.
71 lines
3.3 KiB
Python
71 lines
3.3 KiB
Python
#!/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())
|