#!/usr/bin/env python3 """Все числа TW-032 одним прогоном. Тема Y-04: какие свои числа читать. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. Трейдеру советуют считать по себе десяток показателей: долю выигрышных, профит-фактор, среднюю сделку, просадку, длину серии убытков. Считать их легко. Молчат о другом: какие из них ПЕРЕНОСЯТСЯ, то есть измеренные на прошлом, что-то говорят о будущем, а какие — просто описывают уже случившееся и на следующем отрезке будут другими. Вопрос ставится так: посчитать каждый показатель на первой половине истории и на второй, и посмотреть, насколько они совпали. Показатель, который на двух половинах даёт разное, не бесполезен — он просто не про будущее, и знать это надо до того, как на него опираются. ОТКУДА СДЕЛКИ. Из уже измеренного сетапа: касание уровня, вход по подтверждению, стоп в один дневной ход, цель в три стопа, до десяти дней — «средний способ» из TW-031. Он выбран не за результат (он около нуля), а за то, что сделок много и они разложены по всему окну. ЧЕСТНОСТЬ УЧЁТА — та же, что во всех расчётах канала после 10 сентября: один день — одна сделка в сторону; вход, уже стоящий за стопом, не сделка; в день входа засчитывается стоп, но не цель. ДАННЫЕ. `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 STOP = 1.0 TARGET = 3.0 HOLD = 10 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 # ОБЩИЙ УЧЁТ СДЕЛОК (П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 trades(r: list[dict], a: float, sig: list[dict]) -> list[float]: """Результаты сделок в R, в порядке времени.""" out = [] for s in sig: hit, price, kind = s["hit"], s["price"], s["kind"] 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"] stop = price + STOP * a[hit] if kind == "high" else price - STOP * a[hit] if (kind == "high" and fill >= stop) or (kind == "low" and fill <= stop): continue gap = abs(stop - fill) if gap <= 0: continue target = (fill - TARGET * gap if kind == "high" else fill + TARGET * gap) res = None for j in range(day, min(day + 1 + HOLD, len(r))): first = (j == day) hi, lo = r[j]["h"], r[j]["l"] if kind == "high": if hi >= stop: res = -1.0 break if not first and lo <= target: res = TARGET break else: if lo <= stop: res = -1.0 break if not first and hi >= target: res = TARGET 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) out.append(res * min(1.0, gap / (FLOOR * a[hit]))) # пол риска (П11) return out def streak(rs: list[float]) -> int: """Самая длинная серия убыточных подряд.""" best = cur = 0 for x in rs: cur = cur + 1 if x <= 0 else 0 best = max(best, cur) return best def drawdown(rs: list[float]) -> float: """Самая глубокая просадка кривой в R.""" peak = run = worst = 0.0 for x in rs: run += x peak = max(peak, run) worst = min(worst, run - peak) return -worst def stats(rs: list[float]) -> dict: if len(rs) < 10: return {} wins = [x for x in rs if x > 0] loss = [x for x in rs if x <= 0] gross_w = sum(wins) gross_l = -sum(loss) return { "доля выигрышных, %": len(wins) / len(rs) * 100, "средняя сделка, R": sum(rs) / len(rs), "профит-фактор": (gross_w / gross_l) if gross_l else float("inf"), "средний выигрыш, R": (sum(wins) / len(wins)) if wins else 0.0, "средний убыток, R": (sum(loss) / len(loss)) if loss else 0.0, "макс. просадка, R": drawdown(rs), "серия убытков подряд": float(streak(rs)), "доля из лучшей сделки, %": (max(rs) / gross_w * 100) if gross_w else 0.0, } 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()} halves = {} for n, r in rows.items(): a = adr(r) sig = signals(r, a) mid = len(r) // 2 first = trades(r, a, [s for s in sig if s["hit"] < mid]) second = trades(r, a, [s for s in sig if s["hit"] >= mid]) halves[n] = (first, second) say("ДАННЫЕ") say(f" Общее окно с {start}. Сетап — «средний способ» из TW-031:") say(" касание уровня, вход по подтверждению, стоп один дневной ход,") say(" цель три стопа, до десяти дней.") for n, (a1, a2) in halves.items(): say(f" {n:9} первая половина {len(a1):4} сделок, " f"вторая {len(a2):4}") keys = list(stats([1.0] * 20).keys()) say("") say("1 · КАЖДЫЙ ПОКАЗАТЕЛЬ НА ДВУХ ПОЛОВИНАХ ОДНОЙ ИСТОРИИ") say(" Показатель, который на двух половинах даёт разное, описывает") say(" прошлое, а не сетап. Ниже — по инструментам: первая → вторая.") diffs = {k: [] for k in keys} for k in keys: say("") say(f" {k}") for n, (a1, a2) in halves.items(): s1, s2 = stats(a1), stats(a2) if not s1 or not s2: say(f" {n:9} мало сделок") continue d = s2[k] - s1[k] diffs[k].append(abs(d)) say(f" {n:9} {s1[k]:8.2f} → {s2[k]:8.2f} разница {d:+8.2f}") if diffs[k]: say(f" медиана |разницы|: {st.median(diffs[k]):.2f}") # ПЕРЕВЕРНУЛСЯ ЗНАК — ЕЩЁ НЕ ЗНАЧИТ «ИЗМЕНИЛОСЬ» (B-45 второго аудита). # «Три из шести перевернули знак» без погрешности не говорит, у кого # перемена больше того, что столько сделок умеют различить. say("") say("1b · СРЕДНЯЯ СДЕЛКА: РАЗНИЦА ПОЛОВИН В ПОГРЕШНОСТЯХ") for n, (a1, a2) in halves.items(): if len(a1) < 2 or len(a2) < 2: continue d = sum(a2) / len(a2) - sum(a1) / len(a1) e = (st.pvariance(a1) / len(a1) + st.pvariance(a2) / len(a2)) ** 0.5 say(f" {n:9} разница {d:+.2f} R, погрешность {e:.2f} — " f"{abs(d) / e:.1f} погрешности" f"{' — больше двух' if abs(d) > 2 * e else ''}") say("") say("2 · ЧТО ПЕРЕНОСИТСЯ, А ЧТО НЕТ") say(" Разница между половинами в долях самого показателя: чем меньше,") say(" тем устойчивее число. Сравнивать проценты с R напрямую нельзя,") say(" поэтому всё приведено к масштабу самого показателя.") rank = [] for k in keys: scale = [] for n, (a1, a2) in halves.items(): s1, s2 = stats(a1), stats(a2) if not s1 or not s2: continue base = max(abs(s1[k]), abs(s2[k]), 1e-9) scale.append(abs(s2[k] - s1[k]) / base) if scale: rank.append((st.median(scale), k)) for v, k in sorted(rank): say(f" {k:28} {v * 100:6.0f}%") say("") say("3 · СКОЛЬКО СДЕЛОК НУЖНО, ЧТОБЫ ЧИСЛО ПЕРЕСТАЛО ПРЫГАТЬ") say(" Берём все сделки канала вместе и режем на куски по N штук.") say(" Считаем разброс показателя между кусками.") allr = [x for n in NAMES for x in halves[n][0] + halves[n][1]] say(f" всего сделок: {len(allr)}") for size in (20, 50, 100, 200): chunks = [allr[i:i + size] for i in range(0, len(allr), size)] chunks = [c for c in chunks if len(c) == size] if len(chunks) < 3: continue wr = [len([x for x in c if x > 0]) / size * 100 for c in chunks] av = [sum(c) / size for c in chunks] say(f" по {size:3} сделок ({len(chunks)} кусков): " f"доля выигрышных {min(wr):.0f}–{max(wr):.0f}%, " f"средняя сделка {min(av):+.2f}…{max(av):+.2f} R") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())