#!/usr/bin/env python3 """Все числа TW-019 одним прогоном. Тема F-05: акции против крипты. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ЧТО ПРОВЕРЯЕТСЯ. Ролик утверждает: механика одна, режим торгов разный. Утверждение проверяется в обе стороны — и то, что совпадает, и то, что нет. ЧТО НЕ МЕНЯЕТСЯ (одна механика) 1. Доля хода, которая гасится: сумма дневных ходов против чистого итога. 2. Доля тела в дневном диапазоне — насколько стороны спорят. 3. База «следующий день выше» — та самая, без которой любой процент врёт. 4. Работает ли размер бара: после крупного дня следующий проходит больше. ЧТО МЕНЯЕТСЯ (разный режим торгов) 5. Размер обычного дня в процентах цены — насколько инструмент подвижен. 6. Доля движения вне сессии. У акций биржа закрывается, у крипты нет, и это не мелочь: закрытые часы — часы, в которые нельзя выйти. 7. Гэп через выходные и через ночь: у акций разрыв, у крипты его нет. 8. Частота дней, где цена изменилась больше чем на десять процентов — стоимость ошибки при одинаковом стопе. 9. Сколько дней в году торгуется. Крипта не закрывается, и это меняет не только удобство: год у неё длиннее на треть. ВАЖНО ПРО СРАВНЕНИЕ. Числа акций и крипты нельзя ставить рядом в долларах: цена разная. Всё, что можно, приводится к доле цены или к доле собственного обычного хода инструмента — тогда сравнение честное. ДАННЫЕ. `04_assets/market/*-1d.json`. Акции: AAPL, TSLA, SPY по 2000 баров. Крипта: BTCUSDT, ETHUSDT, SOLUSDT по 1000 баров. Разная длина рядов — поэтому всё, что зависит от длины, считается на общем последнем окне. """ from __future__ import annotations import json import statistics as st import sys 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 # общее окно — КАЛЕНДАРНОЕ, а не в барах. # 250 баров у акций — это год, у крипты — восемь месяцев: она торгуется # без выходных. Сравнивать на равном числе баров значит сравнивать разные # отрезки календаря, и «сколько дней изменились больше чем на десять # процентов» на таком окне просто неверно. BIG = 0.10 # «большой день» — изменение больше десяти процентов def last_year(r: list[dict]) -> list[dict]: """Последний календарный год ряда — общее окно для всех инструментов.""" from datetime import date, timedelta 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 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 cancelled(r: list[dict]) -> float: """Какая доля пройденного пути взаимно погасилась.""" travelled = sum(b["h"] - b["l"] for b in r) net = abs(r[-1]["c"] - r[0]["c"]) return (1 - net / travelled) * 100 if travelled else 0.0 def body_share(r: list[dict]) -> float: v = [abs(b["c"] - b["o"]) / (b["h"] - b["l"]) for b in r if b["h"] > b["l"]] return st.median(v) * 100 if v else 0.0 def up_next(r: list[dict]) -> float: v = [1 if r[i + 1]["c"] > r[i]["c"] else 0 for i in range(len(r) - 1)] return sum(v) / len(v) * 100 if v else 0.0 def size_matters(r: list[dict]) -> tuple[float, float]: """Ход следующего дня после крупного бара и после тихого, в дневных ходах.""" adr = sum(x["h"] - x["l"] for x in r) / len(r) big, small = [], [] for j in range(len(r) - 1): rng = r[j]["h"] - r[j]["l"] nxt = abs(r[j + 1]["c"] - r[j]["c"]) / adr if rng > 1.5 * adr: big.append(nxt) elif rng < 0.6 * adr: small.append(nxt) return (st.median(big) if big else 0.0, st.median(small) if small else 0.0) def day_size(r: list[dict]) -> float: """Обычный дневной ход в процентах цены — сравнимая мерка подвижности.""" return st.median([(b["h"] - b["l"]) / b["c"] * 100 for b in r if b["c"]]) def outside(r: list[dict]) -> float: """Доля движения, случившаяся между закрытием и следующим открытием.""" gap = sum(abs(r[j]["o"] - r[j - 1]["c"]) for j in range(1, len(r))) live = sum(abs(b["c"] - b["o"]) for b in r[1:]) return gap / (gap + live) * 100 if gap + live else 0.0 def worst_gap(r: list[dict]) -> float: """Самый большой разрыв через ночь, в процентах цены. Это и есть цена закрытой биржи: стоп стоял, а торгов не было, и сделка открылась там, где открылась. """ return max(abs(r[j]["o"] - r[j - 1]["c"]) / r[j - 1]["c"] * 100 for j in range(1, len(r))) def big_days(r: list[dict]) -> float: v = [abs(r[j]["c"] - r[j - 1]["c"]) / r[j - 1]["c"] for j in range(1, len(r))] return sum(1 for x in v if x > BIG) / len(v) * 100 if v else 0.0 def trading_days(r: list[dict]) -> float: """Сколько баров приходится на календарный год ряда.""" from datetime import date a = date.fromisoformat(r[0]["d"][:10]) b = date.fromisoformat(r[-1]["d"][:10]) years = (b - a).days / 365.25 return (len(r) - 1) / years if years else 0.0 def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") rows = {n: bars(n) for n in STOCKS + CRYPTO} win = {n: last_year(r) for n, r in rows.items()} print("ДАННЫЕ") for n, r in rows.items(): print(f" {n:9} {len(r)} баров {r[0]['d']} … {r[-1]['d']}" f" в окне года: {len(win[n])} баров") print("\nЧТО НЕ МЕНЯЕТСЯ — те же вопросы, те же ответы " "(последний календарный год)") print(f" {'':11}{'гасится':>10}{'тело':>9}{'след. выше':>12}" f"{'после большого':>16}{'после тихого':>14}") for n in STOCKS + CRYPTO: r = win[n] b, s_ = size_matters(r) print(f" {n:9} {cancelled(r):8.1f}% {body_share(r):7.0f}% " f"{up_next(r):10.1f}% {b:14.2f} {s_:13.2f}") stock_c = [cancelled(win[n]) for n in STOCKS] crypto_c = [cancelled(win[n]) for n in CRYPTO] # Граница, которая округлилась бы до 100, — с одним знаком: «100 %» # значило бы, что цена за год не сдвинулась вовсе, а у Tesla 99,6 %. def pct(v: float) -> str: return f"{v:.1f}" if round(v) >= 100 else f"{v:.0f}" print(f" → гасится: акции {pct(min(stock_c))}–{pct(max(stock_c))} %, " f"крипта {pct(min(crypto_c))}–{pct(max(crypto_c))} %") bodies = [body_share(win[n]) for n in STOCKS + CRYPTO] print(f" → тело: {min(bodies):.0f}–{max(bodies):.0f} % диапазона " f"у всех шести") print("\nЧТО МЕНЯЕТСЯ — тот же вопрос, другой ответ " "(последний календарный год)") print(f" {'':11}{'обычный день':>14}{'вне сессии':>12}" f"{'худший гэп':>12}{'дней >10%':>11}{'баров в год':>13}") for n in STOCKS + CRYPTO: r = win[n] print(f" {n:9} {day_size(r):12.2f}% {outside(r):10.0f}% " f"{worst_gap(r):10.1f}% {big_days(r):9.1f}% " f"{trading_days(rows[n]):11.0f}") s_day = [day_size(win[n]) for n in STOCKS] c_day = [day_size(win[n]) for n in CRYPTO] print(f" → обычный день: акции {min(s_day):.1f}–{max(s_day):.1f} %, " f"крипта {min(c_day):.1f}–{max(c_day):.1f} % цены") s_out = [outside(win[n]) for n in STOCKS] c_out = [outside(win[n]) for n in CRYPTO] print(f" → вне сессии: акции {min(s_out):.0f}–{max(s_out):.0f} %, " f"крипта {min(c_out):.0f}–{max(c_out):.0f} %") s_gap = [worst_gap(win[n]) for n in STOCKS] c_gap = [worst_gap(win[n]) for n in CRYPTO] print(f" → худший разрыв: акции {min(s_gap):.1f}–{max(s_gap):.1f} %, " f"крипта {min(c_gap):.1f}–{max(c_gap):.1f} %") print("\nСТОИМОСТЬ ОДНОГО И ТОГО ЖЕ СТОПА") print(" Стоп в один обычный дневной ход — сколько это процентов счёта") print(" при одинаковом риске в 1 % и сколько при одинаковом РАЗМЕРЕ") print(" позиции. Второе и есть ловушка: размер, привычный на акциях,") print(" на крипте означает другой риск.") base = st.median(s_day) for n in CRYPTO: k = day_size(win[n]) / base print(f" {n:9} обычный день в {k:.1f} раза больше, чем у акций " f"({day_size(win[n]):.1f}% против {base:.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())