#!/usr/bin/env python3 """Все числа TW-034 одним прогоном. Тема Y-02: план как заранее принятые решения. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. «План — это заранее принятые решения» звучит как лозунг, пока не спросить: а сколько стоит РЕШЕНИЕ, принятое не заранее? Здесь это считается прямо. КАК СЧИТАЕТСЯ. Берётся один измеренный сетап — касание уровня — и три решения: где входить, где стоп, как выходить. Каждое решение имеет несколько вариантов, вместе они дают сетку сочетаний. · ЗАПИСАННЫЙ ПЛАН — одно сочетание, выбранное заранее и не меняемое. · НЕЗАПИСАННОЕ РЕШЕНИЕ — вы каждый раз берёте какой-то вариант этого решения, а какой именно, решает настроение. Значит ваш результат — случайная точка внутри разброса по этому решению. Цена незаписанного решения = ШИРИНА этого разброса. Не «вы получите худшее» — вы получите неизвестно что внутри вилки, и ширина вилки и есть то, чем вы рискуете, не написав строчку. ПОЧЕМУ ЭТО НЕ ТО ЖЕ, ЧТО «ВЫБРАТЬ ЛУЧШЕЕ». Лучшее из сетки не переживает второй половины окна — это измерено в четырёх роликах подряд. Поэтому здесь меряется не «какой вариант лучше», а «насколько дорого НЕ выбрать никакой». Это разные вопросы, и второй устойчивее. ЧЕСТНОСТЬ УЧЁТА — та же, что во всех расчётах канала: один день — одна сделка в сторону; вход, уже стоящий за стопом, — не сделка; в день входа засчитывается стоп, но не цель; порядок решений проверяется на второй половине окна. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов, общее окно. """ from __future__ import annotations import json import statistics as st import sys from itertools import product from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] MARKET = STUDIO / "tickwright" / "04_assets" / "market" OUT = Path(__file__).resolve().parent / "results.txt" NAMES = ["AAPL", "TSLA", "SPY", "BTCUSDT", "ETHUSDT", "SOLUSDT"] WING = 2 LIVE = 3.0 HOLD = 10 ROLL = 60 # обычный ход: медиана high−low за 60 баров до бара ENTRIES = ["touch", "confirm"] STOPS = [0.5, 1.0, 1.5] EXITS = ["target2", "target3", "time", "trail"] TARGETS = {"target2": 2.0, "target3": 3.0} 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 # ОБЩИЙ УЧЁТ СДЕЛОК (П11). Свой `adr` и своё понятие R у каждого # этюда были источником противоречий курса: одно сочетание на одних и # тех же касаниях давало разный знак в разных уроках. Теперь обычный # ход, пол риска и погрешность — одни на все этюды модулей S и Y. sys.path.insert(0, str(Path(__file__).resolve().parents[3] / "shared" / "pipeline")) from tw_accounting import adr, FLOOR # noqa: E402 def signals(r: list[dict], a: float) -> list[dict]: out = [] for i in range(WING, len(r) - WING - HOLD - 2): left, right = r[i - WING:i], r[i + 1:i + 1 + WING] for kind, price in (("high", r[i]["h"]), ("low", r[i]["l"])): ok = (all(price > b["h"] for b in left + right) if kind == "high" else all(price < b["l"] for b in left + right)) if not ok: continue hit = None for j in range(i + 1, min(i + 1 + 120, len(r) - HOLD - 2)): if r[j]["l"] <= price <= r[j]["h"]: hit = j break if hit is None or a[hit] is None: continue # бар без истории хода не торгуется prev = r[hit - 1]["c"] if kind == "high" and prev >= price: continue if kind == "low" and prev <= price: continue if abs(prev - price) / a[hit] > LIVE: continue out.append({"hit": hit, "price": price, "kind": kind}) best: dict[tuple[int, str], dict] = {} for s in out: key = (s["hit"], s["kind"]) # ВЧЕРАШНЕЕ ЗАКРЫТИЕ, А НЕ ЗАКРЫТИЕ ДНЯ КАСАНИЯ (B-47, 9 октября 2026). # При входе по касанию закрытия этого дня ещё нет, а выбор по нему # сдвигал результат: у TW-027 вход по касанию со стопом в ход и трейлом # −0.028R → −0.063R, у TW-034 вилка 0.144R → 0.203R. Выбор по открытию # и по вчерашнему закрытию дают одно и то же — выбросом было закрытие. near = abs(r[s["hit"] - 1]["c"] - s["price"]) if key not in best or near < best[key]["_near"]: best[key] = dict(s, _near=near) return [{k: v for k, v in s.items() if k != "_near"} for s in sorted(best.values(), key=lambda x: x["hit"])] def run(r, a, sig, entry, stop_rule, exit_rule, half=None) -> tuple[int, float]: n, total = 0, 0.0 mid = len(r) // 2 for s in sig: hit, price, kind = s["hit"], s["price"], s["kind"] if half is not None and (hit < mid) != (half == 0): continue if entry == "touch": day, fill = hit, price else: b = r[hit] back = (b["c"] < price if kind == "high" else b["c"] > price) if not back or hit + 1 >= len(r): continue day, fill = hit + 1, r[hit + 1]["o"] if a[hit] is None: # бар без истории не торгуется continue stop = (price + stop_rule * a[hit] if kind == "high" else price - stop_rule * a[hit]) if (kind == "high" and fill >= stop) or (kind == "low" and fill <= stop): continue gap = abs(stop - fill) if gap <= 0: continue mult = TARGETS.get(exit_rule) target = (fill - mult * gap if mult and kind == "high" else fill + mult * gap if mult else None) res = None for j in range(day, min(day + 1 + HOLD, len(r))): first = (j == day) hi, lo, c = r[j]["h"], r[j]["l"], r[j]["c"] if kind == "high": if hi >= stop: res = -1.0 break if first: continue if target is not None and lo <= target: res = mult break if exit_rule == "trail" and c > r[j - 1]["h"]: res = (fill - c) / gap break else: if lo <= stop: res = -1.0 break if first: continue if target is not None and hi >= target: res = mult break if exit_rule == "trail" and c < r[j - 1]["l"]: res = (c - fill) / gap break if res is None: last = r[min(day + HOLD, len(r) - 1)]["c"] res = ((fill - last) / gap if kind == "high" else (last - fill) / gap) n += 1 # ПОЛ РИСКА: сделка не может считать своим риском меньше # четверти дневного хода. Без этого стоп в 0,037 хода (AAPL, # 21 января 2025) давал +117,9 R и в одиночку делал «вилку # 0,4 R» этого урока. total += res * min(1.0, gap / (FLOOR * a[hit])) return n, total def grid(rows, sigs, half=None) -> dict[tuple, float]: """Средний R по каждому сочетанию, по всем шести инструментам вместе.""" out = {} for combo in product(ENTRIES, STOPS, EXITS): n = tot = 0 for name, r in rows.items(): k, t = run(r, adr(r), sigs[name], *combo, half=half) n += k tot += t if n: out[combo] = tot / n return out def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") lines: list[str] = [] def say(t: str = "") -> None: print(t) lines.append(t) rows = {n: bars(n) for n in NAMES} start = min(r[0]["d"][:10] for n, r in rows.items() if "USDT" in n) rows = {n: [b for b in r if b["d"][:10] >= start] for n, r in rows.items()} sigs = {n: signals(r, adr(r)) for n, r in rows.items()} say("ДАННЫЕ") say(f" Общее окно с {start}. Сетап — касание уровня, тот же, что") say(" в предыдущих роликах. Касаний всего: " f"{sum(len(v) for v in sigs.values())}.") say(f" Решений три: вход ({len(ENTRIES)} варианта), " f"стоп ({len(STOPS)}), выход ({len(EXITS)}).") say(f" Сочетаний: {len(ENTRIES) * len(STOPS) * len(EXITS)}.") g = grid(rows, sigs) vals = sorted(g.values()) say("") say("1 · ЦЕНА ПЛАНА, КОТОРОГО НЕТ ВОВСЕ") say(f" худшее сочетание {vals[0]:+.3f} R на сделку") say(f" лучшее сочетание {vals[-1]:+.3f} R") say(f" медиана по сетке {st.median(vals):+.3f} R") say(f" ШИРИНА ВИЛКИ {vals[-1] - vals[0]:.3f} R") say(" Не написав ни одной строчки, вы берёте случайную точку внутри") say(" этой вилки — каждый раз заново.") say("") say("2 · ЦЕНА КАЖДОГО ОТДЕЛЬНОГО НЕЗАПИСАННОГО РЕШЕНИЯ") say(" Два решения закреплены, третье оставлено на настроение.") say(" Ширина разброса усреднена по всем способам закрепить остальные.") price = {} for which, options in (("вход", ENTRIES), ("стоп", STOPS), ("выход", EXITS)): spreads = [] for combo in product(ENTRIES, STOPS, EXITS): fixed = [c for i, c in enumerate(combo) if i != ["вход", "стоп", "выход"].index(which)] group = [v for k, v in g.items() if [c for i, c in enumerate(k) if i != ["вход", "стоп", "выход"].index(which)] == fixed] if len(group) == len(options): spreads.append(max(group) - min(group)) price[which] = st.median(spreads) say(f" {which:8} вариантов {len(options)} " f"средняя ширина {price[which]:.3f} R") order = sorted(price, key=price.get, reverse=True) say(f" порядок по цене: {' > '.join(order)}") say("") say("3 · ТОТ ЖЕ ПОРЯДОК НА ДВУХ ПОЛОВИНАХ ОКНА") say(" Если порядок держится, план надо писать именно в этом порядке.") for half, label in ((0, "первая"), (1, "вторая")): gh = grid(rows, sigs, half=half) p = {} for which, options in (("вход", ENTRIES), ("стоп", STOPS), ("выход", EXITS)): idx = ["вход", "стоп", "выход"].index(which) spreads = [] for combo in product(ENTRIES, STOPS, EXITS): fixed = [c for i, c in enumerate(combo) if i != idx] group = [v for k, v in gh.items() if [c for i, c in enumerate(k) if i != idx] == fixed] if len(group) == len(options): spreads.append(max(group) - min(group)) p[which] = st.median(spreads) if spreads else 0.0 say(f" {label:8} " + " ".join(f"{w} {p[w]:.3f}" for w in ("вход", "стоп", "выход")) + f" порядок: {' > '.join(sorted(p, key=p.get, reverse=True))}") say("") say("4 · А ЧТО С ВЫБОРОМ ЛУЧШЕГО") say(" Для сравнения: выбрать лучшее сочетание на первой половине") say(" и измерить его на второй — то, что ломалось четыре ролика подряд.") g0 = grid(rows, sigs, half=0) g1 = grid(rows, sigs, half=1) pick = max(g0, key=g0.get) say(f" лучшее на первой половине: {pick[0]} · стоп {pick[1]} · {pick[2]}") say(f" первая половина {g0[pick]:+.3f} R") say(f" вторая половина {g1[pick]:+.3f} R") say(f" а ширина вилки на второй половине: " f"{max(g1.values()) - min(g1.values()):.3f} R") say(" То есть выбор лучшего не переносится, а ЦЕНА ОТСУТСТВИЯ ВЫБОРА —") say(" переносится: вилка никуда не девается.") say("") say("4b · ИЗМЕРЕНА ЛИ ЭТА ВИЛКА ВООБЩЕ") say(" Варианты одного решения считаются на ОДНИХ И ТЕХ ЖЕ сделках,") say(" поэтому сравнивать их надо попарно: разница берётся по каждой") say(" сделке, и у среднего этих разниц есть своя погрешность.") say(" Разница меньше двух погрешностей — это не находка, а шум.") say("") for which, options, idx in (("вход", ENTRIES, 0), ("стоп", STOPS, 1), ("выход", EXITS, 2)): best_pair, best_t = None, 0.0 for i in range(len(options)): for j in range(i + 1, len(options)): diffs = [] base = list(("confirm", 1.0, "time")) a_combo, b_combo = base[:], base[:] a_combo[idx], b_combo[idx] = options[i], options[j] for name, r in rows.items(): aa = adr(r) for sg in sigs[name]: ka, ta = run(r, aa, [sg], *a_combo) kb, tb = run(r, aa, [sg], *b_combo) if ka and kb: diffs.append(ta - tb) if len(diffs) < 30: continue m = sum(diffs) / len(diffs) sd = st.pstdev(diffs) se = sd / (len(diffs) ** 0.5) t = abs(m) / se if se else 0.0 if t > best_t: best_pair, best_t = (options[i], options[j], m, se, len(diffs)), t if best_pair: o1, o2, m, se, n = best_pair if which == "вход": say(" ВНИМАНИЕ: у входа парное сравнение недоступно трейдеру.") say(" «Подтверждение» меняет НЕ ТОЛЬКО цену входа, но и то,") say(" какие сделки вообще берутся. Сравнить их на общих") say(" сделках можно только задним числом: в момент касания") say(" ещё неизвестно, закроется ли бар обратно.") verdict = "измерено" if best_t >= 2 else "НЕ измерено — шум" say(f" {which:8} крайняя пара: {o1} против {o2}") # СКОЛЬКО ЭТО ПОГРЕШНОСТЕЙ — печатаем числом, а не оставляем # зрителю делить в уме. Это главная мерка ролика («измерено # только то, что дальше двух погрешностей от нуля»), и голос # её называет: без печати сверить её было нечем. say(f" разница {m:+.3f} R, погрешность {se:.3f}, " f"это {best_t:.1f} погрешности, " f"на {n} сделках — {verdict}") say("5 · ПРОВЕРКА НА ПОДОЗРИТЕЛЬНО ОДИНАКОВЫЙ ИСХОД") smelly = 0 for name, r in rows.items(): a = adr(r) for combo in product(ENTRIES, STOPS, EXITS): res = [] for s in sigs[name]: k, t = run(r, a, [s], *combo) if k: res.append(round(t, 6)) if len(res) >= 10 and len(set(res)) == 1: smelly += 1 say(f" ПОДОЗРИТЕЛЬНО {name} · {combo}: {len(res)} сделок, " f"все {res[0]:+.3f} R") if not smelly: say(" чисто: срезов с одинаковым исходом нет") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())