#!/usr/bin/env python3 """Все числа TW-036 одним прогоном. Тема Y-06: где опытные расходятся. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. Опытные трейдеры дают противоположные советы про одно и то же: стоп тесный или широкий, входить сразу или ждать подтверждения, цель или сопровождение. Обычно это объясняют характером или школой. Здесь проверяется другое объяснение: они расходятся ровно там, где ОТВЕТ ЗАВИСИТ ОТ СЕТАПА, и сходятся там, где не зависит. Тогда расхождение — это не спор о правоте, а сведения о самом вопросе. КАК ПРОВЕРЯЕТСЯ. Два разных события на одних и тех же уровнях: ОТБОЙ: цена подошла к уровню и торгуется ОТ него; ПРОБОЙ: цена закрылась ЗА уровнем и торгуется СКВОЗЬ него. Три решения — вход, стоп, выход — меряются на каждом событии отдельно, одними и теми же вариантами. Смотрим, где лучший вариант МЕНЯЕТСЯ между событиями, а где остаётся тем же. ЧЕСТНОСТЬ УЧЁТА — та же, что во всех расчётах канала: один день — одна сделка в сторону; вход, уже стоящий за стопом, — не сделка; в день входа засчитывается стоп, но не цель. Разница между вариантами внутри одного события считается ПОПАРНО, по общим сделкам, с погрешностью. ДАННЫЕ. `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" 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 баров до бара STOPS = [0.5, 1.0, 1.5] EXITS = ["target", "time", "trail"] TARGET = 2.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 dedup(raw: list[dict], r: list[dict], key: str) -> list[dict]: best: dict[tuple[int, str], dict] = {} for s in raw: k = (s[key], s["kind"]) # ВЧЕРАШНЕЕ ЗАКРЫТИЕ, А НЕ ЗАКРЫТИЕ ДНЯ КАСАНИЯ (B-47, 9 октября 2026). # При входе по касанию закрытия этого дня ещё нет, а выбор по нему # сдвигал результат: у TW-027 вход по касанию со стопом в ход и трейлом # −0.028R → −0.063R, у TW-034 вилка 0.144R → 0.203R. Выбор по открытию # и по вчерашнему закрытию дают одно и то же — выбросом было закрытие. near = abs(r[s[key] - 1]["c"] - s["price"]) if k not in best or near < best[k]["_near"]: best[k] = dict(s, _near=near) return [{a: b for a, b in s.items() if a != "_near"} for s in sorted(best.values(), key=lambda x: x[key])] def swings(r: list[dict]): 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 ok: yield i, kind, price def bounce_signals(r: list[dict], a: float) -> list[dict]: """Цена подошла к уровню с правильной стороны — торгуем ОТ него.""" raw = [] for i, kind, price in swings(r): 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 # Отбой: от сопротивления вниз, от поддержки вверх. raw.append({"hit": hit, "price": price, "kind": kind, "side": "short" if kind == "high" else "long", "event": "отбой"}) return dedup(raw, r, "hit") def break_signals(r: list[dict], a: float) -> list[dict]: """Первое закрытие ЗА уровнем — торгуем СКВОЗЬ него.""" raw = [] for i, kind, price in swings(r): day = None for j in range(i + 1, min(i + 1 + 120, len(r) - HOLD - 2)): if (r[j]["c"] > price if kind == "high" else r[j]["c"] < price): day = j break if day is None: continue # Пробой: вверх через сопротивление, вниз через поддержку. raw.append({"hit": day, "price": price, "kind": kind, "side": "long" if kind == "high" else "short", "event": "пробой"}) return dedup(raw, r, "hit") def trade(r, a, s, entry, stop_rule, exit_rule) -> float | None: hit, price, side = s["hit"], s["price"], s["side"] if entry == "now": # ЗАГЛЯДЫВАНИЕ ВПЕРЁД У ПРОБОЯ. Сигнал пробоя — ПЕРВОЕ ЗАКРЫТИЕ # за уровнем, и оно известно только к концу дня. Войти по цене # самого уровня внутри этого дня можно лишь заранее стоящей # заявкой — но тогда она срабатывала бы и в те дни, когда цена # уровень задела и вернулась, а такие дни в выборку не попали. # То есть «вход сразу» отбирал бы удачные дни задним числом. # Для отбоя этой беды нет: там сигнал и есть касание. if s.get("event") == "пробой": return None day, fill = hit, price else: if hit + 1 >= len(r): return None day, fill = hit + 1, r[hit + 1]["o"] if a[hit] is None: # бар без истории хода не торгуется return None stop = price - stop_rule * a[hit] if side == "long" else price + stop_rule * a[hit] if (side == "long" and fill <= stop) or (side == "short" and fill >= stop): return None gap = abs(fill - stop) if gap <= 0: return None target = (fill + TARGET * gap if side == "long" else fill - TARGET * gap) 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 side == "long": if lo <= stop: res = -1.0 break if first: continue if exit_rule == "target" and hi >= target: res = TARGET break if exit_rule == "trail" and c < r[j - 1]["l"]: res = (c - fill) / gap break else: if hi >= stop: res = -1.0 break if first: continue if exit_rule == "target" and lo <= target: res = TARGET break if exit_rule == "trail" and c > r[j - 1]["h"]: res = (fill - c) / gap break if res is None: last = r[min(day + HOLD, len(r) - 1)]["c"] res = ((last - fill) / gap if side == "long" else (fill - last) / gap) return res * min(1.0, gap / (FLOOR * a[hit])) # пол риска (П11) def series(rows, sigs, entry, stop_rule, exit_rule) -> list[float]: out = [] for n, r in rows.items(): a = adr(r) for s in sigs[n]: v = trade(r, a, s, entry, stop_rule, exit_rule) if v is not None: out.append(v) return out def paired(rows, sigs, base, idx, opt_a, opt_b) -> tuple[float, float, int]: """Разница между двумя вариантами одного решения, по общим сделкам.""" ca, cb = list(base), list(base) ca[idx], cb[idx] = opt_a, opt_b diffs = [] for n, r in rows.items(): a = adr(r) for s in sigs[n]: va = trade(r, a, s, *ca) vb = trade(r, a, s, *cb) if va is not None and vb is not None: diffs.append(va - vb) if len(diffs) < 30: return 0.0, 0.0, len(diffs) m = sum(diffs) / len(diffs) se = st.pstdev(diffs) / (len(diffs) ** 0.5) return m, se, len(diffs) 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()} setups = { "отбой": {n: bounce_signals(r, adr(r)) for n, r in rows.items()}, "пробой": {n: break_signals(r, adr(r)) for n, r in rows.items()}, } say("ДАННЫЕ") say(f" Общее окно с {start}. Два события на одних и тех же уровнях.") for name, sg in setups.items(): say(f" {name:7} сигналов: {sum(len(v) for v in sg.values())}") say(" Варианты решений одни и те же, чтобы сравнивать было что с чем.") BASE = ("next", 1.0, "time") say("") say("1 · ОДНО И ТО ЖЕ РЕШЕНИЕ НА ДВУХ СОБЫТИЯХ") say(" Средний результат в R при базовых прочих: вход на следующий день,") say(" стоп один дневной ход, выход по времени.") table = {} for which, idx, options in (("стоп", 1, STOPS), ("выход", 2, EXITS), ("вход", 0, ["now", "next"])): say("") say(f" {which}") for name, sg in setups.items(): cells = [] for o in options: combo = list(BASE) combo[idx] = o v = series(rows, sg, *combo) if v: cells.append((o, sum(v) / len(v), len(v))) table[(which, name)] = cells best = max(cells, key=lambda c: c[1]) row = " ".join(f"{str(o):8} {m:+.3f}" for o, m, n in cells) skipped = [o for o in options if o not in [c[0] for c in cells]] if skipped: row += f" ({', '.join(map(str, skipped))}: заглядывание" row += " вперёд, исключено)" say(f" {name:7} {row} лучший: {best[0]}") say("") say("2 · ГДЕ ЛУЧШИЙ ВАРИАНТ МЕНЯЕТСЯ МЕЖДУ СОБЫТИЯМИ") flips = [] for which, idx, options in (("стоп", 1, STOPS), ("выход", 2, EXITS), ("вход", 0, ["now", "next"])): b1 = max(table[(which, "отбой")], key=lambda c: c[1])[0] b2 = max(table[(which, "пробой")], key=lambda c: c[1])[0] same = "одинаково" if b1 == b2 else "РАСХОДИТСЯ" if b1 != b2: flips.append(which) say(f" {which:8} отбой → {str(b1):8} пробой → {str(b2):8} {same}") say("") say(f" расходится: {', '.join(flips) if flips else 'ни одно'}") say(" Похоже на объяснение — но следующий раздел его отменяет:") say(" почти все эти разницы меньше собственной погрешности.") say("") say("3 · А ИЗМЕРЕНА ЛИ САМА РАЗНИЦА") say(" Крайняя пара вариантов внутри каждого события, попарно,") say(" по общим сделкам, с погрешностью. Меньше двух погрешностей — шум.") for which, idx, options in (("стоп", 1, STOPS), ("выход", 2, EXITS), ("вход", 0, ["now", "next"])): say("") say(f" {which}") for name, sg in setups.items(): best_t, best = 0.0, None for i in range(len(options)): for j in range(i + 1, len(options)): m, se, n = paired(rows, sg, BASE, idx, options[i], options[j]) t = abs(m) / se if se else 0.0 if t > best_t: best_t, best = t, (options[i], options[j], m, se, n) if best: o1, o2, m, se, n = best say(f" {name:7} {str(o1):8} против {str(o2):8} " f"{m:+.3f} ± {se:.3f} " f"({abs(m / se) if se else 0:.1f} погр.) на {n:4} — " f"{'измерено' if best_t >= 2 else 'шум'}") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())