#!/usr/bin/env python3 """Что покупает подтверждение уровня. Тема L-05 против L-06. ОТКУДА ВОПРОС. L-05 (TW-006) учит: не рисуй торгуемый уровень по одному резкому бару — дождись ВТОРОЙ реакции на той же цене, и тогда торгуй следующий подход. L-06 (TW-023) измеряет другое: позднее касание держит хуже первого, на всех шести инструментах, по медиане ≈11 пунктов. Отсюда вывод, что L-05 учит правилу, которое курс сам опровергает. ЭТО ДВА РАЗНЫХ ВОПРОСА, и в этом всё дело. L-06 считает ВСЕ вторые касания — в том числе те, где первое касание уровень НЕ удержало. А правило L-05 требует именно удержания: вторая реакция и есть подтверждение. Группа «второе касание» и группа «второе касание после сработавшего первого» — не одна и та же группа. Пока это не померено, обе формулировки урока — догадка: и «подтверждение улучшает шансы», и «подтверждение шансы не улучшает». Поэтому здесь считается ровно то, что утверждает урок. ЧТО СРАВНИВАЕТСЯ (определения ИМПОРТИРОВАНЫ из расчёта TW-023 — менять мерку между роликами нельзя, иначе числа перестают быть сравнимыми): A · линия по одному бару, вход на следующем касании — исход ПЕРВОГО возврата. Это фольклор, который урок называет догадкой. B · правило урока: вход на ВТОРОМ возврате, но только там, где первый возврат ОТСКОЧИЛ, то есть подтверждение действительно случилось. C · для сравнения — второй возврат БЕЗ этого условия. Это и есть число L-06. МЕРКА ТА ЖЕ, ЧТО В МОДУЛЕ L: доля отскоков (уход на полдня обратно за пять дней). Разница считается в процентных пунктах, и по правилу канала (TW-035 s15) считается померенной только если она не меньше двух погрешностей от нуля. Для доли погрешность — биномиальная: sqrt(p(1−p)/n), для разницы двух долей — корень суммы квадратов. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов целиком. python confirm.py """ from __future__ import annotations import io import statistics as st import sys from math import sqrt from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] # Определения уровня, касания и отскока берутся из расчёта TW-023 как есть. sys.path.insert(0, str(STUDIO / "tickwright" / "04_assets" / "TW-023-study")) from ranking import (AHEAD, AWAY, HORIZON, NEAR, NAMES, ROLL, WING, # noqa: E402 adr, bars, bounced, levels, share, touches) def err(p: float, n: int) -> float: """Погрешность доли в процентных пунктах. Ноль наблюдений — нет мерки.""" if n <= 1: return float("inf") q = p / 100 return sqrt(q * (1 - q) / n) * 100 def observations(name: str) -> list[dict]: """Все касания всех уровней инструмента, с исходом ПЕРВОГО касания. `first_held` — то самое подтверждение: отскочил ли уровень на своём первом возврате. Хранится на каждом наблюдении, поэтому дедуп ниже не может его потерять. """ r = bars(name) a = adr(r) got = [] for lv in levels(r, a): ts = touches(r, lv, a) if not ts: continue outs = [bounced(r, lv, t, a) for t in ts] for k, (t, ok) in enumerate(zip(ts, outs)): got.append(dict(lv, hit=t, back=ok, tests=k, first_held=outs[0], age=t - lv["i"])) # ОДИН ДЕНЬ — ОДНО КАСАНИЕ В СТОРОНУ, тот же дедуп и тот же ключ, что # в TW-023: один бар задевает несколько изломов подряд, и без этого # выборка выглядит независимее, чем есть. best: dict[tuple[int, str], dict] = {} for g in got: key = (g["hit"], g["kind"]) # ВЧЕРАШНЕЕ ЗАКРЫТИЕ, А НЕ ЗАКРЫТИЕ ДНЯ КАСАНИЯ (B-47, 9 октября 2026). # При входе по касанию закрытия этого дня ещё нет, а выбор по нему # сдвигал результат: у TW-027 вход по касанию со стопом в ход и трейлом # −0.028R → −0.063R, у TW-034 вилка 0.144R → 0.203R. Выбор по открытию # и по вчерашнему закрытию дают одно и то же — выбросом было закрытие. near = abs(r[g["hit"] - 1]["c"] - g["price"]) if key not in best or near < best[key]["_near"]: best[key] = dict(g, _near=near) return [{k: v for k, v in g.items() if k != "_near"} for g in sorted(best.values(), key=lambda x: x["hit"])] def main() -> int: print("ОПРЕДЕЛЕНИЯ (из расчёта TW-023, не переписаны)") print(f" излом: экстремум выше (ниже) {WING} соседей с каждой стороны") print(f" обычный ход: медиана high−low за {ROLL} баров ДО бара") print(f" касание: подход ближе {NEAR} обычного дня, уровень жив " f"{HORIZON} дней") print(f" отскок: {AWAY} обычного дня обратно за {AHEAD} дней") data = {n: observations(n) for n in NAMES} print("\nДАННЫЕ") for n, got in data.items(): print(f" {n:9} {len(got):5} касаний уровней") print("\n1 · ТРИ ПРАВИЛА НА ОДНИХ ДАННЫХ") print(" A — вход на первом возврате (линия по одному бару)") print(" B — вход на втором возврате, если первый ОТСКОЧИЛ " "(правило L-05)") print(" C — вход на втором возврате без условия (число L-06)") print(f"\n {'':11}{'A, %':>8}{'nA':>7}{'B, %':>8}{'nB':>6}" f"{'C, %':>8}{'nC':>6}{'B−A':>8}{'C−A':>8}") rows = {} for n, got in data.items(): A = [g["back"] for g in got if g["tests"] == 0] B = [g["back"] for g in got if g["tests"] == 1 and g["first_held"]] C = [g["back"] for g in got if g["tests"] == 1] a_, b_, c_ = share(A), share(B), share(C) rows[n] = dict(a=a_, b=b_, c=c_, na=len(A), nb=len(B), nc=len(C)) print(f" {n:9} {a_:7.1f}%{len(A):7}{b_:7.1f}%{len(B):6}" f"{c_:7.1f}%{len(C):6}{b_ - a_:+8.1f}{c_ - a_:+8.1f}") ba = [r["b"] - r["a"] for r in rows.values()] ca = [r["c"] - r["a"] for r in rows.values()] print(f"\n медиана B−A: {st.median(ba):+.1f} п.п. " f"медиана C−A: {st.median(ca):+.1f} п.п.") print(f" B лучше A на {sum(1 for x in ba if x > 0)} инструментах из " f"{len(ba)}, C лучше A на {sum(1 for x in ca if x > 0)}") print("\n2 · ТА ЖЕ РАЗНИЦА, СЛОЖЕННАЯ ПО ВСЕМ ИНСТРУМЕНТАМ") print(" Правило канала: разница померена, только если она не меньше") print(" двух своих погрешностей.") pools = {} for tag, pick in (("A", lambda g: g["tests"] == 0), ("B", lambda g: g["tests"] == 1 and g["first_held"]), ("C", lambda g: g["tests"] == 1)): vals = [g["back"] for got in data.values() for g in got if pick(g)] pools[tag] = (share(vals), len(vals)) p, n = pools[tag] print(f" {tag}: {p:5.1f}% на {n:5} касаниях, погрешность " f"±{err(p, n):.1f} п.п.") for one, two in (("B", "A"), ("C", "A"), ("B", "C")): p1, n1 = pools[one] p2, n2 = pools[two] d = p1 - p2 se = sqrt(err(p1, n1) ** 2 + err(p2, n2) ** 2) print(f" {one} − {two} = {d:+.1f} п.п., две погрешности " f"{2 * se:.1f} → {'ПОМЕРЕНА' if abs(d) >= 2 * se else 'не померена'}" f" ({abs(d) / se:.1f} погрешности)") print("\n3 · ЧЕМ ПЛАТИТ ОЖИДАНИЕ") na = pools["A"][1] nb = pools["B"][1] print(f" Сделок у правила A: {na}. У правила B: {nb} — это " f"{nb / na * 100:.0f}% от числа A.") waits = [g["age"] for got in data.values() for g in got if g["tests"] == 1 and g["first_held"]] firsts = [g["age"] for got in data.values() for g in got if g["tests"] == 0] print(f" Медиана ожидания от бара-основания до входа: " f"A {st.median(firsts):.0f} дней, B {st.median(waits):.0f} дней.") print("\n4 · ЧТО ИЗ ЭТОГО СЛЕДУЕТ ДЛЯ УРОКА") pb, nbb = pools["B"] pa, naa = pools["A"] d = pb - pa se = sqrt(err(pb, nbb) ** 2 + err(pa, naa) ** 2) same = sum(1 for x in ba if x > 0) if abs(d) < 2 * se: print(" Подтверждение НЕ МЕНЯЕТ долю отскоков на измеримую " "величину:") print(f" разница {d:+.1f} п.п. при двух погрешностях {2 * se:.1f}. " f"Это значит,") print(" что подтверждение нельзя продавать как улучшение шансов. " "Оно") print(" отвечает на другой вопрос: нарисована ли линия рынком или " "вами.") elif d > 0: print(f" Подтверждение ПОДНИМАЕТ долю отскоков на {d:+.1f} п.п. " f"при двух") print(f" погрешностях {2 * se:.1f} — померено, и на " f"{same} инструментах из {len(ba)}") print(" из шести направление то же. Тогда правило урока — не " "фильтр разметки,") print(" а правило входа, и так его и надо называть.") else: print(f" Подтверждение СНИЖАЕТ долю отскоков на {d:+.1f} п.п. при " f"двух") print(f" погрешностях {2 * se:.1f} — померено. Тогда урок обязан " f"сказать это прямо.") print(f" Число L-06 («второе касание против первого», без условия) " f"здесь: {st.median(ca):+.1f} п.п.") return 0 def _run() -> int: """Прогон со снимком: что напечатано, то и легло в `results.txt`.""" 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())