#!/usr/bin/env python3 """Планка издержек на 5м (этап Э3): средние движения, комиссии, порог безубыточной точности. Считает по фактическим 5м-данным: средний модуль 5м-доходности и 1ч-доходности (12 баров) по каждой паре, агрегаты, и таблицу p* = (Стоп + Издержки) / (Цель + Стоп) для тейкера и мейкера. Выход: reports/costs.json (машинный контракт гейта) + reports/COSTS.md (человекочитаемо). """ from __future__ import annotations import glob import json import os import sys import numpy as np import pandas as pd ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) FEE = {"taker": 0.055, "maker": 0.02} # % на ногу SCENARIOS = [(0.1, 0.1), (0.2, 0.2), (0.3, 0.2), (0.5, 0.3), (1.0, 0.5), (1.0, 1.0), (2.0, 1.0)] def main() -> int: files = sorted(glob.glob(os.path.join(ROOT, "data", "5m", "*_5m.csv"))) if not files: print("нет 5м-данных") return 1 rows = [] for path in files: sym = os.path.basename(path).replace("_5m.csv", "") close = pd.read_csv(path, header=None, usecols=[4], names=["c"])["c"].to_numpy(float) if len(close) < 1000: continue r5 = np.diff(np.log(close)) r1h = np.convolve(r5, np.ones(12), "valid") rows.append({"symbol": sym, "bars": int(len(close)), "mean_abs_r5m_bp": float(np.abs(r5).mean() * 1e4), "std_r5m_bp": float(r5.std() * 1e4), "p95_abs_r5m_bp": float(np.percentile(np.abs(r5), 95) * 1e4), "mean_abs_r1h_bp": float(np.abs(r1h).mean() * 1e4)}) df = pd.DataFrame(rows) agg = {"pairs": int(len(df)), "bars_total": int(df["bars"].sum()), "mean_abs_r5m_bp": round(float(df["mean_abs_r5m_bp"].mean()), 2), "median_abs_r5m_bp": round(float(df["mean_abs_r5m_bp"].median()), 2), "mean_abs_r1h_bp": round(float(df["mean_abs_r1h_bp"].mean()), 2)} p_star = [] for target, stop in SCENARIOS: p_star.append({"target_pct": target, "stop_pct": stop, "taker": round((stop + 2 * FEE["taker"]) / (target + stop) * 100, 1), "maker": round((stop + 2 * FEE["maker"]) / (target + stop) * 100, 1)}) out = {"fees_pct_per_leg": FEE, "fee_rt_pct": {"taker": 2 * FEE["taker"], "maker": 2 * FEE["maker"]}, "slippage_bp_per_leg": 1.0, "slippage_bp_per_leg_illiquid": 3.0, "mean_abs_r5m_bp": agg["mean_abs_r5m_bp"], "mean_abs_r1h_bp": agg["mean_abs_r1h_bp"], "aggregate": agg, "per_pair": rows, "p_star": p_star} os.makedirs(os.path.join(ROOT, "reports"), exist_ok=True) with open(os.path.join(ROOT, "reports", "costs.json"), "w") as fh: json.dump(out, fh, ensure_ascii=False, indent=1) md = ["# Планка издержек на 5-минутных свечах", "", f"Посчитано скриптом `tools/costs.py` по {len(df)} парам, {agg['bars_total']:,} барам 5м " f"(данные Bybit).".replace(",", " "), "", "## Что стоит торговля", "", f"- Комиссия тейкера: {FEE['taker']}% на ногу, **{2*FEE['taker']:.2f}% на круг** (вход+выход);", f"- Комиссия мейкера: {FEE['maker']}% на ногу, **{2*FEE['maker']:.2f}% на круг**;", "- Проскальзывание: 1 б.п. на ногу для топ-30 по обороту, 3 б.п. для остальных пар;", "- Funding учитывается только при удержании через 00/08/16 UTC (0,006% за 8ч на BTC — " "на фоне комиссии незначимо, но в движке учтено).", "", "## Типичные движения (факт по данным)", "", f"- Средний модуль доходности за 5м: **{agg['mean_abs_r5m_bp']} б.п.** " f"(медиана по парам {agg['median_abs_r5m_bp']} б.п.);", f"- Средний модуль доходности за 1 час: **{agg['mean_abs_r1h_bp']} б.п.**", "", "Вывод: комиссия на круг (11 б.п.) сопоставима с типичным движением за 5 минут. " "Значит «скальпинг по одному 5м-бару» убыточен по построению, а осмысленны конструкции, " "где цель в 3–10 раз больше комиссии (сигнал на 5м, удержание десятки минут) либо " "мейкер-исполнение, либо кросс-секционные корзины.", "", "## Порог безубыточной точности p* = (Стоп + Издержки) / (Цель + Стоп)", "", "| Цель | Стоп | Тейкер (11 б.п.) | Мейкер (4 б.п.) |", "|---|---|---|---|"] for r in p_star: md.append(f"| {r['target_pct']}% | {r['stop_pct']}% | нужно выигрывать {r['taker']}% " f"| нужно выигрывать {r['maker']}% |") md += ["", "## Топ-15 пар по среднему движению 5м", "", "| Пара | баров | |r5м|, б.п. | σ 5м, б.п. | |r1ч|, б.п. |", "|---|---|---|---|---|"] for r in sorted(rows, key=lambda x: -x["mean_abs_r5m_bp"])[:15]: md.append(f"| {r['symbol']} | {r['bars']} | {r['mean_abs_r5m_bp']:.2f} | " f"{r['std_r5m_bp']:.2f} | {r['mean_abs_r1h_bp']:.2f} |") with open(os.path.join(ROOT, "reports", "COSTS.md"), "w", encoding="utf-8") as fh: fh.write("\n".join(md) + "\n") print(f"пар {agg['pairs']}, баров {agg['bars_total']}, |r5м| {agg['mean_abs_r5m_bp']} б.п., " f"|r1ч| {agg['mean_abs_r1h_bp']} б.п.") print("записано reports/costs.json и reports/COSTS.md") return 0 if __name__ == "__main__": sys.exit(main())