#!/usr/bin/env python3 """ СИМУЛЯТОР ГРИДА на реальных минутных свечах Binance. Что моделирует честно: • лимитные заявки на уровнях, исполнение по касанию high/low свечи • парный выход: купил на уровне L → продаёшь на L*(1+step) • КОМИССИИ с обеих сторон (по умолчанию 0.075% — Binance spot с BNB) • инвентарь: сколько капитала реально связано в каждый момент • просадка по инвентарю (нереализованная) — главный риск грида • лимит инвентаря вместо аварийного закрытия (замена regime_flatten) Главная метрика — НЕ PnL, а доходность на ПИКОВЫЙ связанный капитал. Грид, заработавший $500 на $10k инвентаря, лучше грида на $1000 при $100k. Запуск: python3 grid_sim.py SOLUSDT --step 0.5 --levels 20 python3 grid_sim.py BTCUSDT --sweep # перебор параметров """ import csv, os, sys, math HERE = os.path.dirname(os.path.abspath(__file__)) FEE = 0.00075 # 0.075% за сторону UNIT_USD = 100.0 # размер одной покупки def load(symbol, interval="1m"): p = os.path.join(HERE, f"{symbol}_{interval}.csv") rows = [] with open(p) as f: for r in csv.DictReader(f): rows.append((int(r["ts"]), float(r["o"]), float(r["h"]), float(r["l"]), float(r["c"]))) return rows def simulate(candles, step_pct, n_levels, inv_cap=None, recenter_pct=None, unit=UNIT_USD, fee=FEE): """ step_pct — шаг сетки в % n_levels — сколько уровней ВНИЗ от центра (глубина сетки) inv_cap — максимум одновременных позиций (None = без лимита) recenter_pct — если цена ушла дальше этого % от центра, пересобрать сетку (None = не пересобирать) """ step = step_pct / 100.0 center = candles[0][4] buy_levels = [center * (1 - step * (i + 1)) for i in range(n_levels)] open_pos = {} # level_index -> entry_price realized = 0.0 fills = 0 peak_inv_usd = 0.0 max_unreal_dd = 0.0 inv_series = [] recenters = 0 for ts, o, h, l, c in candles: # --- покупки: цена коснулась незанятого уровня for i, lvl in enumerate(buy_levels): if i in open_pos: continue if l <= lvl: if inv_cap is not None and len(open_pos) >= inv_cap: continue open_pos[i] = lvl # --- продажи: цена достигла уровня выхода for i in list(open_pos.keys()): entry = open_pos[i] target = entry * (1 + step) if h >= target: gross = unit * step cost = unit * fee + unit * (1 + step) * fee realized += gross - cost fills += 1 del open_pos[i] # --- учёт инвентаря inv_usd = len(open_pos) * unit peak_inv_usd = max(peak_inv_usd, inv_usd) unreal = sum(unit * (c - e) / e for e in open_pos.values()) max_unreal_dd = min(max_unreal_dd, unreal) inv_series.append(inv_usd) # --- пересборка сетки if recenter_pct and abs(c - center) / center > recenter_pct / 100.0 and not open_pos: center = c buy_levels = [center * (1 - step * (i + 1)) for i in range(n_levels)] recenters += 1 last = candles[-1][4] unreal_final = sum(unit * (last - e) / e for e in open_pos.values()) days = (candles[-1][0] - candles[0][0]) / 86400000 cap = max(peak_inv_usd, unit) return { "fills": fills, "realized": realized, "unreal_final": unreal_final, "total": realized + unreal_final, "open_at_end": len(open_pos), "peak_inv": peak_inv_usd, "max_unreal_dd": max_unreal_dd, "days": days, # доходность на пиковый связанный капитал, годовых "roi_pct": (realized + unreal_final) / cap * 100, "apr_pct": (realized + unreal_final) / cap * 100 * (365 / days) if days else 0, "realized_apr": realized / cap * 100 * (365 / days) if days else 0, "recenters": recenters, "fill_per_day": fills / days if days else 0, } def fmt(r): return (f"филлов {r['fills']:>5} ({r['fill_per_day']:.1f}/д) | " f"реализ ${r['realized']:>8,.0f} | нереализ ${r['unreal_final']:>9,.0f} | " f"итог ${r['total']:>9,.0f} | пик.инв ${r['peak_inv']:>8,.0f} | " f"просадка ${r['max_unreal_dd']:>9,.0f} | APR {r['apr_pct']:>7.1f}%") def main(): if len(sys.argv) < 2: sys.exit("укажи символ: SOLUSDT / BTCUSDT / ETHUSDT") sym = sys.argv[1] candles = load(sym) print(f"{sym}: {len(candles):,} свечей, {(candles[-1][0]-candles[0][0])/86400000:.0f} дней, " f"цена {candles[0][4]:.2f} → {candles[-1][4]:.2f} " f"({(candles[-1][4]/candles[0][4]-1)*100:+.1f}%)\n") if "--sweep" in sys.argv: print(f"{'шаг%':>6} {'уровн':>6} {'лимит':>6} результат") print("-" * 118) best = [] for step in (0.2, 0.3, 0.5, 0.8, 1.2, 2.0): for lv in (10, 20, 40): for cap in (None, 10, 20): if cap and cap > lv: continue r = simulate(candles, step, lv, inv_cap=cap) best.append((r["apr_pct"], step, lv, cap, r)) print(f"{step:>6} {lv:>6} {str(cap):>6} {fmt(r)}") best.sort(reverse=True, key=lambda x: x[0]) print("\n" + "=" * 118) print("ТОП-5 по APR на связанный капитал:") for apr, step, lv, cap, r in best[:5]: print(f" шаг {step}% · {lv} уровней · лимит {cap} → APR {apr:.1f}% " f"(реализ APR {r['realized_apr']:.1f}%, просадка ${r['max_unreal_dd']:,.0f})") else: step = float(sys.argv[sys.argv.index("--step") + 1]) if "--step" in sys.argv else 0.5 lv = int(sys.argv[sys.argv.index("--levels") + 1]) if "--levels" in sys.argv else 20 cap = int(sys.argv[sys.argv.index("--cap") + 1]) if "--cap" in sys.argv else None r = simulate(candles, step, lv, inv_cap=cap) print(f"шаг {step}% · {lv} уровней · лимит {cap}") print(fmt(r)) if __name__ == "__main__": main()