#!/usr/bin/env python3 """Все числа TW-016 одним прогоном. Тема S-04: вход на ложном пробое. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ЧТО ПРОВЕРЯЕТСЯ. Расхожее описание входа: цена прокалывает уровень и возвращается — ставим заявку за уровнем, защитный стоп, тейк. Предыдущий ролик (S-03) объяснил, ПОЧЕМУ прокол случается. Здесь проверяется, даёт ли он вход. ОПРЕДЕЛЕНИЯ НАРОЧНО ГРУБЫЕ, чтобы их нельзя было подкрутить под ответ: уровень — бар, чей максимум выше пяти баров с каждой стороны (то же правило, что в R-03); ложный пробой — первый день после уровня, чей максимум выше уровня, а закрытие ниже; настоящий — то же, но закрытие выше уровня; КОНТРОЛЬ — день, который подошёл к уровню ближе половины процента, но НЕ проколол его. Контроль здесь главное. Без него измеряется погода, а не прокол: цена приходит к уровню и в спокойном рынке, и в шоке, и сравнивать надо с тем, что от прокола не зависит. Тот же приём, что в L-03 (rules.md §9). СДЕЛКА. Шорт по закрытию дня события, защитный стоп над максимумом этого дня, цель кратна стопу. Держим не дольше двадцати сессий; сделки, которые за это время не решились, в счёт не идут. ЧЕСТНОЕ ОГРАНИЧЕНИЕ. Дневные бары не видят середины дня, а урок учит входить ВНУТРИ дня, сразу за уровнем. Здесь измерена версия «по закрытию» — единственная, которую эти данные позволяют. Это сказано и в ролике. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов целиком. """ 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" NAMES = ["AAPL", "TSLA", "SPY", "BTCUSDT", "ETHUSDT", "SOLUSDT"] K = 5 # баров с каждой стороны у разворотной точки HOLD = 20 # сессий в сделке AFTER = 5 # сессий для замера движения NEAR = 0.995 # «подошёл к уровню» — ближе половины процента ROLL = 60 # обычный ход: медиана high−low за 60 дней ДО дня события 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 usual(rows: list[dict], i: int) -> float: """Обычный дневной ход на день i: медиана high−low за ROLL дней ДО него. Среднее по всему ряду знало будущее, а цена за восемь лет выросла в разы: «один ход» в начале ряда и в конце — разные доли цены. """ w = rows[max(0, i - ROLL):i] if i >= 20 else rows[:20] return st.median([b["h"] - b["l"] for b in w]) def pivots(rows: list[dict]) -> list[int]: return [i for i in range(K, len(rows) - K) if all(rows[i]["h"] > rows[j]["h"] for j in range(i - K, i)) and all(rows[i]["h"] > rows[j]["h"] for j in range(i + 1, i + K + 1))] def trade(rows: list[dict], i: int, mult: float) -> int | None: entry, stop = rows[i]["c"], rows[i]["h"] risk = stop - entry if risk <= 0: return None target = entry - mult * risk for b in rows[i + 1:i + 1 + HOLD]: if b["h"] >= stop: return 0 if b["l"] <= target: return 1 return None TALLY: dict[str, dict] = {} DEDUP: dict[str, list[int]] = {"false": [0, 0], "real": [0, 0], "touch": [0, 0]} LEVELS: list[tuple[str, float]] = [] # (инструмент, уровень) — в порядке ev["false"] def events(): """Три группы событий: прокол, настоящий пробой, просто касание. ПО ИНСТРУМЕНТАМ СЧИТАЕТСЯ ЗДЕСЬ ЖЕ, в `TALLY`. Голос s10 называет «a hundred and thirteen levels over eight years produced fifty one of them» и «under five» у индексного фонда — а вывод до 8 октября печатал только итог по всем шести, и сверять эти числа было не с чем. """ out = {"false": [], "real": [], "touch": []} for n in NAMES: rows = bars(n) piv = pivots(rows) before = len(out["false"]) shown = [] # ОДНО ЗАКРЫТИЕ — ОДНО РЕШЕНИЕ (B-35 второго аудита, правило TW-028). # День, который прошёл три уровня разом, раньше давал три события # одной группы, а день, проколовший один уровень и подошедший к # другому, стоял и в проколах, и в контроле — то есть пример урока # (Apple, 4 февраля 2026) сравнивался сам с собой. Теперь кандидаты # собираются по дням, и каждый день попадает в одну группу: # прокол важнее настоящего пробоя, тот важнее касания. cand = {"false": {}, "real": {}, "touch": {}} raw = {"false": 0, "real": 0, "touch": 0} for p in piv: lvl = rows[p]["h"] for i in range(p + K + 1, len(rows) - HOLD): if rows[i]["h"] < lvl: continue key = "false" if rows[i]["c"] < lvl else "real" raw[key] += 1 cand[key].setdefault(i, (p, lvl)) break for i in range(p + K + 1, len(rows) - HOLD): if rows[i]["h"] < lvl * NEAR: continue if rows[i]["h"] >= lvl: break raw["touch"] += 1 cand["touch"].setdefault(i, (p, lvl)) break taken: set[int] = set() for key in ("false", "real", "touch"): for i in sorted(cand[key]): if i in taken: continue taken.add(i) out[key].append((rows, i, usual(rows, i))) if key == "false": p, lvl = cand[key][i] LEVELS.append((n, lvl)) shown.append({"d": rows[i]["d"], "pivot": rows[p]["d"], "lvl": lvl, "h": rows[i]["h"], "c": rows[i]["c"]}) for key in raw: DEDUP[key][0] += raw[key] TALLY[n] = {"levels": len(piv), "false": len(out["false"]) - before, "years": len(rows) / 252, "shown": shown} for key in out: DEDUP[key][1] = len(out[key]) return out def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") ev = events() names = {"false": "ложный пробой", "real": "настоящий пробой", "touch": "подошёл, но не проколол"} print("ДАННЫЕ") for n in NAMES: b = bars(n) print(f" {n:9} {len(b)} баров {b[0]['d']} … {b[-1]['d']}") print(f"\nСОБЫТИЙ НАЙДЕНО (день — одно событие, в одной группе)") for k, v in ev.items(): print(f" {names[k]:24} {len(v)} (пар «уровень × день» было " f"{DEDUP[k][0]})") print(" по инструментам: уровней · ложных пробоев · в год") for n, t in TALLY.items(): print(f" {n:9} {t['levels']:12} {t['false']:12} " f"{t['false'] / t['years']:10.1f}") # ПРИМЕР ДЛЯ КАДРОВ s23–s25 ПЕЧАТАЕТСЯ ИЗ ТЕХ ЖЕ СОБЫТИЙ. Голос называет # уровень 277.84, прокол до 278.95, закрытие 276.49, риск 2.46 и цель # 269.11 — до 8 октября ни одного из этих чисел в выводе не было. # Печатаются все ложные пробои Apple за 2026 год, чтобы пример был # виден среди соседей, а не выбран молча. print(" ложные пробои AAPL в 2026 году — шорт по закрытию, стоп над " "максимумом, цель 3R:") for e in TALLY["AAPL"]["shown"]: if not e["d"].startswith("2026"): continue risk = e["h"] - e["c"] print(f" {e['d']} уровень {e['lvl']:.2f} (излом {e['pivot']}) · " f"максимум {e['h']:.2f} · закрытие {e['c']:.2f} · " f"риск {risk:.2f} · цель {e['c'] - 3 * risk:.2f}") print(f"\nДВИЖЕНИЕ ЗА {AFTER} СЕССИЙ, в дневных ходах") import statistics as st for k, v in ev.items(): down, up = [], [] for rows, i, adr in v: a = rows[i + 1:i + 1 + AFTER] down.append((rows[i]["c"] - min(b["l"] for b in a)) / adr) up.append((max(b["h"] for b in a) - rows[i]["c"]) / adr) print(f" {names[k]:24} вниз {st.median(down):.2f} · " f"вверх {st.median(up):.2f}") print("\nСДЕЛКА: шорт по закрытию, стоп над максимумом дня") # ПОГРЕШНОСТЬ У КАЖДОГО ОЖИДАНИЯ. Голос s33 говорит «enough to say the # entry loses money» — без погрешности это не проверить. Исход сделки # здесь двузначный (+цель или −1R), поэтому погрешность среднего — # (цель + 1) · √(p(1 − p) / n). for mult in (1, 2, 3): need = 1 / (mult + 1) * 100 print(f"\n цель {mult}R — безубыточность {need:.0f}%") got = {} for k in ("false", "touch", "real"): res = [r for rows, i, _ in ev[k] if (r := trade(rows, i, mult)) is not None] if not res: continue p = sum(res) / len(res) exp = (sum(res) * mult - (len(res) - sum(res))) / len(res) e = (mult + 1) * (p * (1 - p) / len(res)) ** 0.5 got[k] = (exp, e) print(f" {names[k]:24} {len(res):4} сделок · " f"{p * 100:4.0f}% · {exp:+.2f}R ± {e:.2f} " f"({abs(exp) / e:.1f} погр.)") (ef, sf), (et, s_t) = got["false"], got["touch"] se = (sf ** 2 + s_t ** 2) ** 0.5 print(f" прокол против касания: {ef - et:+.2f}R ± {se:.2f} " f"({abs(ef - et) / se:.1f} погр.)") print("\nВЫВОД. При любой из трёх целей вход отрицателен, и прокол") print("почти ничего не добавляет к обычному касанию уровня: при цели") print("в два и в три стопа они совпадают. Механика объясняет, почему") print("вас выбило, но входом сама по себе не является.") print() print("ЗАПАС ДО УРОВНЯ НИЖЕ ÷ СТОП НАД ПРОКОЛОМ (s36, s38)") print(" уровень ниже — разворотный минимум, известный к дню события") for k in ("false", "touch"): ratios, kept, short = [], [], [] for rows, i, _ in ev[k]: entry, risk = rows[i]["c"], rows[i]["h"] - rows[i]["c"] lows = [rows[j]["l"] for j in range(K, i - K + 1) if rows[j]["l"] < entry and all(rows[j]["l"] < rows[m]["l"] for m in range(j - K, j + K + 1) if m != j)] if risk <= 0 or not lows: continue r = (entry - max(lows)) / risk ratios.append(r) if (t := trade(rows, i, 3)) is not None: (kept if r >= 6 else short).append(t) six = sum(1 for r in ratios if r >= 6) / len(ratios) * 100 def line(box): win = sum(box) / len(box) * 100 exp = (sum(box) * 3 - (len(box) - sum(box))) / len(box) return f"{len(box)} сделок, {win:.0f}%, {exp:+.2f}R" print(f" {names[k]:24} медиана {st.median(ratios):.1f} стопа · " f"шесть и больше {six:.0f}% · из них цель 3R: {line(kept)}") # МЕНЬШЕ ШЕСТИ — ТОЖЕ (B-36). Голос s36 говорил «under six, there is # nowhere for the trade to go», а без запаса цель достигалась так же # часто — это видно только рядом со второй строкой. print(f" {'':24} меньше шести · цель 3R: {line(short)}") # ГДЕ ЗАКАНЧИВАЕТСЯ ПРОКОЛ (B-38, B-39). Голос TW-005 говорит о заступе # «на тик-два», а TW-016 s37 — что стоп модуля риска «обычно дальше» # удобного максимума прокола. Оба утверждения — про длину заступа. print("\nЗАСТУП ПРОКОЛА ЗА УРОВЕНЬ (ложные пробои)") over, beyond, ticks = [], 0, [] passed_high = closed_above = 0 for (rows, i, adr), (n, lvl) in zip(ev["false"], LEVELS): o = rows[i]["h"] - lvl over.append(o / adr) beyond += o > 0.1 * adr if "USDT" not in n: ticks.append(round(o / 0.01)) nxt = rows[i + 1:i + 1 + AFTER] passed_high += max(b["h"] for b in nxt) > rows[i]["h"] closed_above += any(b["c"] > lvl for b in nxt) nf = len(over) print(f" медиана заступа: {st.median(over):.2f} дневного хода") print(f" заступ больше стоп-дистанции модуля риска (десятая хода): " f"{beyond / nf * 100:.0f}% — там стоп модуля стоит НИЖЕ максимума прокола") print(f" акции: заступ не больше двух тиков — {sum(t <= 2 for t in ticks)} " f"из {len(ticks)}, медиана {st.median(ticks):.0f} тиков") print(f" за {AFTER} сессий после: максимум прокола пройден в " f"{passed_high / nf * 100:.0f}%, закрытие выше уровня — в " f"{closed_above / nf * 100:.0f}%") 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())