#!/usr/bin/env python3 """Все числа TW-015 одним прогоном. Тема F-04: во что обходится сделка. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. python costs.py # всё python costs.py # сам пишет results.txt (G-18) ГЛАВНАЯ МЫСЛЬ, КОТОРУЮ ПРОВЕРЯЕТ РАСЧЁТ. Издержки берут процентом от СУММЫ сделки, а платятся они из СТОПА. Пока меришь их процентом от оборота, они выглядят мелочью — «десятая доля процента». Переведи в доли стопа, и на одном рынке это два процента риска, а на другом больше половины. ПОЧЕМУ ОТНОШЕНИЕ К СТОПУ — ЧЕСТНАЯ МЕРА. И издержка, и риск считаются на ЕДИНИЦУ инструмента, поэтому их отношение не зависит от размера позиции. Купите одну акцию или тысячу — доля риска, съеденная комиссией, та же. ДВЕ МОДЕЛИ КОМИССИИ, И В НИХ ВСЯ РАЗНИЦА. Брокер на акциях берёт за ШТУКУ (порядка полцента за акцию), биржа на крипте — ПРОЦЕНТ ОТ СУММЫ. Первая в долях цены тем больше, чем дешевле акция; вторая в долях цены постоянна. Доля стопа = тариф в % цены ÷ (0,1 × ход в % цены): разрыв рынков — в тарифе. ДОПУЩЕНИЯ. Тарифы не сняты с сайтов площадок, а ЗАДАНЫ параметрами ниже и названы в ролике вслух: полцента за акцию и цент спреда на акциях, 0,1 % за сторону и 0,01 % спреда на крипте, плюс отдельно 0,4 % как тариф нижнего объёма. Зритель подставляет свои — метод от этого не меняется. Стоп — десятая доля среднего дневного хода за двадцать сессий, правило из K-02. ДАННЫЕ. `04_assets/market/*-1d.json`, последние двадцать сессий каждого инструмента. """ from __future__ import annotations import io import json import sys from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] MARKET = STUDIO / "tickwright" / "04_assets" / "market" TAIL = 20 STOCKS = ["AAPL", "TSLA", "SPY"] CRYPTO = ["BTCUSDT", "ETHUSDT", "SOLUSDT"] PER_SHARE = 0.005 # доллар за акцию, за сторону SPREAD_ST = 0.01 # цент спреда на ликвидной акции TAKER = 0.001 # 0,1 % за сторону SPREAD_CR = 0.0001 # 0,01 % спреда TAKER_BAD = 0.004 # 0,4 % — тариф нижнего объёма TARGET = 3 # цель в три стопа, как в K-01 YEAR = 200 # сделок в год 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 stats(name: str) -> tuple[float, float, float]: b = bars(name)[-TAIL:] price = b[-1]["c"] adr = sum(x["h"] - x["l"] for x in b) / len(b) return price, adr, adr / 10 def report(name: str, cost: float) -> float: price, adr, stop = stats(name) share = cost / stop win, loss = TARGET - share, 1 + share need = loss / (win + loss) * 100 print(f" {name:9} цена {price:9.2f} · ход {adr:7.2f} · стоп {stop:6.2f}" f" · туда-обратно {cost:8.3f}") print(f" {'':9} это {share * 100:5.1f}% стопа · безубыточность " f"{need:4.1f}% · за {YEAR} сделок {share * YEAR:.0f}R") return share def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") print("ДАННЫЕ") for n in STOCKS + CRYPTO: b = bars(n)[-TAIL:] print(f" {n:9} {len(b)} сессий {b[0]['d']} … {b[-1]['d']}") print(f"\nБЕЗ ИЗДЕРЖЕК при цели в {TARGET} стопа нужно " f"{1 / (TARGET + 1) * 100:.0f}% верных сделок.\n") print("АКЦИИ — за штуку: полцента за сторону плюс цент спреда") st = [report(n, PER_SHARE * 2 + SPREAD_ST) for n in STOCKS] print("\nКРИПТА — процентом: 0,1 % за сторону плюс 0,01 % спреда") cr = [report(n, stats(n)[0] * (TAKER * 2 + SPREAD_CR)) for n in CRYPTO] print("\nКРИПТА на тарифе нижнего объёма: 0,4 % за сторону") bad = [report(n, stats(n)[0] * (TAKER_BAD * 2 + SPREAD_CR)) for n in CRYPTO] print("\nИТОГ") print(f" акции: {min(st) * 100:.0f}–{max(st) * 100:.0f}% стопа") print(f" крипта 0,1 %: {min(cr) * 100:.0f}–{max(cr) * 100:.0f}% стопа") print(f" крипта 0,4 %: {min(bad) * 100:.0f}–{max(bad) * 100:.0f}% стопа") print("\n На акциях издержка — заметная мелочь. На крипте по обычному") print(" тарифу она съедает от трети до половины риска. На тарифе") print(" нижнего объёма риск съеден целиком: сделка проигрышна до того,") print(" как цена куда-то пошла.") print("\nПОЧЕМУ SPY ДОРОЖЕ ДВУХ ДРУГИХ АКЦИЙ") p, adr, _ = stats("SPY") p2, adr2, _ = stats("SOLUSDT") print(f" дневной ход SPY {adr / p * 100:.2f}% цены, " f"у SOL {adr2 / p2 * 100:.2f}%") print(" Спокойный инструмент — короткий стоп, и та же издержка") print(" занимает в нём большую долю. Это то же самое, что в K-03:") print(" чем спокойнее инструмент, тем он требовательнее.") return 0 def _run() -> int: """Прогон со снимком: что напечатано, то и легло в `results.txt`. Раньше снимок писался перенаправлением (`python x.py > results.txt`), и прогон без перенаправления оставлял прежний файл: сравнение «снимок совпал» сравнивало копию саму с собой (G-18 второго аудита). Теперь — как у остальных этюдов курса. Снимок пишется только при удачном прогоне: обрубок опаснее отсутствия, он выглядит как настоящий отчёт. """ 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())