#!/usr/bin/env python3 """Все числа TW-020 одним прогоном. Тема F-06: плечо и маржа. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ЧТО ПРОВЕРЯЕТСЯ. Расхожее объяснение: «плечо увеличивает прибыль и убыток в N раз». Первая половина фразы неверна в том смысле, в каком её понимают, а вторая скрывает главное. Здесь измеряется то, что плечо делает на самом деле: оно ставит между вами и рынком ВТОРОЙ стоп, которого вы не выбирали. 1. Расстояние до ликвидации при плече N. Это арифметика, а не рынок: примерно 1/N минус поддерживающая маржа. Считается точно. 2. Как часто рынок проходит это расстояние за один день. Здесь уже данные: доля дней, где ход против позиции от открытия достал до ликвидации. 3. Сколько дней держится позиция до первой ликвидации — половина случаев. 4. Что происходит с одной и той же сделкой при разных плечах. Вход на открытии, стоп в один обычный дневной ход, цель в два. Риск на сделку ОДИН И ТОТ ЖЕ — 1 % счёта, размер позиции подобран под стоп. Меняется только плечо, то есть только расстояние до ликвидации. 5. Издержки. Комиссия берётся с размера позиции, а не с вашего капитала, поэтому на N она умножается, только если под маржу отдан весь счёт. ГЛАВНОЕ ПРО ПУНКТ 4. Если риск на сделку фиксирован, плечо не меняет ни прибыль, ни убыток — пока сделка доходит до своего стопа. Оно меняет то, какая доля сделок ДО стопа не доживает. Это и есть измеряемая разница, и она видна только симуляцией, а не рассуждением. ЧЕГО ЗДЕСЬ НЕТ. Фандинга, процентов за заём и налогов: они зависят от площадки и срока, а не от рынка, и в ролике названы отдельно как то, что нужно смотреть в своих условиях. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов, последний календарный год — то же окно, что в TW-019. """ from __future__ import annotations import json import math import statistics as st import sys from datetime import date, timedelta from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] MARKET = STUDIO / "tickwright" / "04_assets" / "market" STOCKS = ["AAPL", "TSLA", "SPY"] CRYPTO = ["BTCUSDT", "ETHUSDT", "SOLUSDT"] WINDOW_DAYS = 365 MAINT = 0.005 # поддерживающая маржа, типовая для крипты LEVERS = [2, 3, 5, 10, 20, 50, 100] RISK = 0.01 # риск на сделку — 1 % счёта, одинаковый везде def bars(name: str) -> list[dict]: d = json.loads((MARKET / f"{name}-1d.json").read_text(encoding="utf-8")) return d["bars"] if isinstance(d, dict) and "bars" in d else d def last_year(r: list[dict]) -> list[dict]: end = date.fromisoformat(r[-1]["d"][:10]) start = end - timedelta(days=WINDOW_DAYS) return [b for b in r if date.fromisoformat(b["d"][:10]) >= start] def liq_distance(n: int) -> float: """Насколько цена должна уйти против позиции, чтобы счёт обнулился. Арифметика, а не мнение: свои деньги в позиции — 1/N её стоимости, и когда убыток съедает всё, кроме поддерживающей маржи, позицию закрывают принудительно. """ return max(1.0 / n - MAINT, 0.0001) def adr(r: list[dict]) -> float: """Обычный дневной ход как доля цены.""" return st.median([(b["h"] - b["l"]) / b["c"] for b in r if b["c"]]) def hit_rate(r: list[dict], dist: float) -> float: """Доля дней, где ход ВНИЗ от открытия достал до расстояния.""" n = sum(1 for b in r if b["o"] and (b["o"] - b["l"]) / b["o"] >= dist) return n / len(r) if r else 0.0 def half_life(p: float) -> float: """Сколько дней держится позиция, пока шанс дожить не упал до половины.""" if p <= 0: return float("inf") if p >= 1: return 0.0 return math.log(0.5) / math.log(1 - p) def simulate(r: list[dict], n: int | None) -> dict: """Одна и та же сделка при плече N. Риск на сделку — всегда 1 % счёта. Вход на открытии, стоп в один обычный дневной ход, цель в два, сделка живёт до пяти дней. `n=None` — без плеча: ликвидации нет вовсе. ЧТО СРАБАТЫВАЕТ РАНЬШЕ. Порядок событий внутри дня четыре числа бара не содержат (об этом весь TW-018), но здесь он и не нужен: цена идёт вниз непрерывно, поэтому ближняя к входу отметка достигается первой. Ликвидация убивает сделку только тогда, когда она БЛИЖЕ стопа — то есть начиная с некоторого плеча, и это порог считается отдельно. СКОЛЬКО ТЕРЯЕТСЯ ПРИ ЛИКВИДАЦИИ. Не «всё», а вся маржа под эту позицию: размер подобран под стоп, поэтому убыток равен доле (1/N)/stop от того же одного процента: остаток поддерживающей маржи забирает биржа. Пока 1/N больше стопа, ликвидация обходится ДОРОЖЕ стопа. """ a = adr(r) stop, target, hold = a, 2 * a, 5 dist = liq_distance(n) if n else None wins = losses = liq = opened = 0 pnl = 0.0 for i in range(len(r) - hold): entry = r[i]["o"] if not entry: continue opened += 1 done = False for j in range(i, i + hold): low, high = r[j]["l"], r[j]["h"] down = (entry - low) / entry if dist is not None and dist <= stop and down >= dist: liq += 1 pnl -= RISK * ((1.0 / n) / stop) done = True break if down >= stop: losses += 1 pnl -= RISK done = True break if (high - entry) / entry >= target: wins += 1 pnl += 2 * RISK done = True break if not done: pnl += (r[i + hold - 1]["c"] - entry) / entry / stop * RISK return {"opened": opened, "wins": wins, "losses": losses, "liq": liq, "liq_share": liq / opened * 100 if opened else 0.0, "pnl": pnl * 100} def simulate_real(r: list[dict], fee: float, gaps: bool) -> float: """Та же сделка без плеча, но с комиссией и с гэпами. -> итог, % счёта. ЗАЧЕМ (A-25 второго аудита). `simulate` не платит комиссию и исполняет стоп ровно по его цене, а голос называет итог года Apple заголовком. Плечо до границы сделку не меняет — значит, и поправки одинаковы при любом плече ниже неё; поэтому они считаются один раз, без плеча. КОМИССИЯ — с позиции: позиция = риск / стоп, круг = две стороны, то есть 2 × fee / stop в долях R на каждую сделку. ГЭП: если день СЛЕДОМ за входом открылся уже за стопом (или за целью), сделка закрывается по открытию — хуже стопа (или лучше цели). """ a = adr(r) stop, target, hold = a, 2 * a, 5 pnl = 0.0 for i in range(len(r) - hold): entry = r[i]["o"] if not entry: continue res = None for j in range(i, i + hold): o = r[j]["o"] if gaps and j > i: if (entry - o) / entry >= stop: res = -(entry - o) / entry / stop break if (o - entry) / entry >= target: res = (o - entry) / entry / stop break if (entry - r[j]["l"]) / entry >= stop: res = -1.0 break if (r[j]["h"] - entry) / entry >= target: res = 2.0 break if res is None: res = (r[i + hold - 1]["c"] - entry) / entry / stop pnl += (res - 2 * fee / stop) * RISK return pnl * 100 def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") rows = {n: last_year(bars(n)) for n in STOCKS + CRYPTO} print("ДАННЫЕ") for n, r in rows.items(): print(f" {n:9} {len(r)} баров {r[0]['d']} … {r[-1]['d']}" f" обычный день {adr(r) * 100:.2f}% цены") print("\n1 · РАССТОЯНИЕ ДО ЛИКВИДАЦИИ — ЭТО АРИФМЕТИКА") print(f" Поддерживающая маржа принята {MAINT:.1%}") for n in LEVERS: print(f" плечо {n:3}× ликвидация при ходе против вас " f"{liq_distance(n) * 100:5.1f}%") print("\n2 · КАК ЧАСТО РЫНОК ПРОХОДИТ ЭТО ЗА ОДИН ДЕНЬ") print(f" {'':11}" + "".join(f"{str(n) + '×':>9}" for n in LEVERS)) for name, r in rows.items(): cells = "".join(f"{hit_rate(r, liq_distance(n)) * 100:8.1f}%" for n in LEVERS) print(f" {name:9} {cells}") print(" (доля дней, где ход вниз от открытия достал до ликвидации)") print("\n3 · СКОЛЬКО ДНЕЙ ДО ЛИКВИДАЦИИ — ПОЛОВИНА СЛУЧАЕВ") print(f" {'':11}" + "".join(f"{str(n) + '×':>9}" for n in LEVERS)) for name, r in rows.items(): cells = [] for n in LEVERS: h = sorted(next((j - i + 1 for j in range(i, len(r)) if (r[i]['o'] - r[j]['l']) / r[i]['o'] >= liq_distance(n)), float('inf')) for i in range(len(r)))[len(r) // 2] cells.append(" —" if h == float("inf") else " <1" if h < 1 else f"{h:8.0f}") print(f" {name:9} " + "".join(cells)) print(" (— значит: за весь год такого дня не случилось ни разу)") print("\n4 · ГДЕ ПРОХОДИТ ГРАНИЦА: ЧЕЙ СТОП СРАБОТАЕТ ПЕРВЫМ") print(" Пока ликвидация ДАЛЬШЕ вашего стопа, плечо на сделку не влияет") print(" вовсе. Как только она ближе — из позиции вас выносит биржа,") print(" а не ваш план. Граница считается: N = 1 / (стоп + маржа).") for name, r in rows.items(): stop = adr(r) edge = 1.0 / (stop + MAINT) safe = [n for n in LEVERS if liq_distance(n) > stop] print(f" {name:9} стоп в один день {stop * 100:5.2f}% → " f"граница {edge:5.1f}× (ваш стоп остаётся вашим до " f"{max(safe) if safe else 0}×)") # ГОЛОС s28 НАЗЫВАЕТ И ВТОРУЮ ГРАНИЦУ — для стопа в десятую дня. Число, # которое произносит ролик, обязано быть напечатано расчётом, иначе # сверять его не с чем: до 8 октября «a hundred and forty four» в # выводе не было вовсе, и ворота счётчиков его не находили. tenth = adr(rows["AAPL"]) / 10 print(f" AAPL стоп в десятую дня {tenth * 100:5.3f}% → " f"граница {1.0 / (tenth + MAINT):5.1f}×") print("\n5 · ОДНА И ТА ЖЕ СДЕЛКА ПРИ РАЗНЫХ ПЛЕЧАХ") print(" Вход на открытии, стоп в один обычный день, цель в два,") print(f" риск на сделку всегда {RISK:.0%} счёта. Меняется только плечо.") for name in ("AAPL", "SOLUSDT"): r = rows[name] print(f"\n {name}") print(f" {'плечо':>9}{'сделок':>9}{'ликвидаций':>13}" f"{'их доля':>10}{'итог, % счёта':>16}") for n in [None] + LEVERS: s_ = simulate(r, n) tag = "без плеча" if n is None else f"{n}×" print(f" {tag:>9}{s_['opened']:9}{s_['liq']:13}" f"{s_['liq_share']:9.1f}%{s_['pnl']:15.1f}%") print("\n5b · ТОТ ЖЕ ГОД С КОМИССИЕЙ И ГЭПАМИ") print(" Плечо до границы сделку не меняет, поэтому и эти поправки") print(" одинаковы при любом плече ниже неё. Итог, % счёта:") print(f" {'':9}{'как выше':>10}{'комиссия 0.1%':>15}{'гэпы':>8}" f"{'то и другое':>13}") for name, r in rows.items(): base_ = simulate(r, None)["pnl"] print(f" {name:9}{base_:9.1f}%{simulate_real(r, 0.001, False):14.1f}%" f"{simulate_real(r, 0.0, True):7.1f}%" f"{simulate_real(r, 0.001, True):12.1f}%") print("\n6 · ЧТО ПЛЕЧО ДЕЛАЕТ С ИЗДЕРЖКАМИ") print(" Комиссия берётся с размера позиции, а не с вашего капитала.") fee = 0.001 print(f" При комиссии {fee:.1%} с оборота, вход и выход, и позиции = плечо × ВЕСЬ счёт" f" (при размере от стопа комиссия от плеча НЕ зависит — см. раздел 5):") for n in [1] + LEVERS: tag = "без плеча" if n == 1 else f"{n}×" print(f" {tag:>9} {2 * fee * n * 100:5.1f}% счёта за один круг") print("\nВЫВОД. Плечо не увеличивает прибыль на единицу риска: если риск") print("на сделку задан, размер позиции считается от стопа, и плечо в этой") print("арифметике не участвует вовсе. Что оно меняет — расстояние до") print("принудительного закрытия. Это второй стоп, который вы не ставили:") print("он стоит на фиксированном расстоянии, не зависящем ни от уровня,") print("ни от вашего плана, и рынок доходит до него тем чаще, чем больше") print("плечо. Плюс издержки, которые умножаются на плечо, потому что") print("считаются от позиции, а не от счёта.") return 0 def _run() -> int: """Прогон со снимком: что напечатано, то и легло в `results.txt`. Пока файл клали руками, он отставал от скрипта, и сценарий писался по устаревшим числам. Здесь отставать нечему. Снимок пишется только при удачном прогоне. Обрубок опаснее отсутствия: он выглядит как настоящий отчёт. """ import io real = sys.stdout for s in (real, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") buf = io.StringIO() class _Tee: """Поток, который пишет в два места и не спорит с вызовами скрипта.""" encoding = "utf-8" errors = "replace" def write(self, text): real.write(text) buf.write(text) return len(text) def flush(self): real.flush() def reconfigure(self, **kw): pass def isatty(self): return False sys.stdout = _Tee() try: code = main() finally: sys.stdout = real if code == 0: out = Path(__file__).with_name("results.txt") out.write_text(buf.getvalue(), encoding="utf-8", newline="\n") print(f"\nснимок: {out}") return code if __name__ == "__main__": raise SystemExit(_run())