#!/usr/bin/env python3 """Все числа TW-035 одним прогоном. Тема Y-03: что писать в журнал, что нет. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. Журнал сделок обычно предлагают вести колонок на двадцать: время, настроение, новости, фаза луны. Здесь проверяется другое: какая колонка вообще способна что-то показать НА ТОМ ЧИСЛЕ СДЕЛОК, которое у вас есть. ДВА РАЗНЫХ ТИПА КОЛОНОК, и их нельзя мешать. ОБЯЗАТЕЛЬНЫЕ — без них нельзя посчитать результат в R вовсе: цена входа, стоп, выход, размер. Они не «предсказывают», они делают арифметику возможной, и вопрос об их пользе не стоит. ПРИЗНАКИ — всё остальное. Их держат в журнале ради надежды, что когда- нибудь по ним найдётся разница. Вот их и проверяем. КАК ПРОВЕРЯЕТСЯ. Каждый признак делит сделки на две группы. Считается разница средних и её погрешность — это независимые группы, поэтому погрешность берётся по обеим. Разница меньше двух погрешностей не измерена. И главный вопрос ролика: СКОЛЬКО СДЕЛОК нужно, чтобы колонка могла показать разницу в одну десятую R. Ответ считается прямо, а не на глаз. ЧЕСТНОСТЬ УЧЁТА — та же, что во всех расчётах канала. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов, общее окно. """ from __future__ import annotations import json import statistics as st import sys from datetime import date from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] MARKET = STUDIO / "tickwright" / "04_assets" / "market" OUT = Path(__file__).resolve().parent / "results.txt" STOCKS = ["AAPL", "TSLA", "SPY"] CRYPTO = ["BTCUSDT", "ETHUSDT", "SOLUSDT"] NAMES = STOCKS + CRYPTO WING = 2 LIVE = 3.0 HOLD = 10 STOP = 1.0 TARGET = 3.0 BAND = 20 SEEN = 60 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 trades(name: str, r: list[dict], a: float) -> list[dict]: """Сделки одного сетапа с признаками, известными НА ВХОДЕ.""" raw = [] for i in range(max(WING, SEEN), 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 w = r[max(0, i - SEEN):i + 1] seen = (price >= max(b["h"] for b in w) if kind == "high" else price <= min(b["l"] for b in w)) hit = None tests = 0 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, "seen": seen, "i": i, "tests": tests}) best: dict[tuple[int, str], dict] = {} for s in raw: 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) sig = [s for s in sorted(best.values(), key=lambda x: x["hit"])] 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) res *= min(1.0, gap / (FLOOR * a[hit])) # пол риска (П11) w = r[day - BAND:day] lo_b = min(x["l"] for x in w) hi_b = max(x["h"] for x in w) mean = sum(x["c"] for x in w) / len(w) y = r[day - 1] out.append({ "r": res, "инструмент — крипта": name in CRYPTO, "сторона — продажа": kind == "high", "уровень виден всем": s["seen"], "полоса уже обычного": (hi_b - lo_b) / a[day] < 3.0, "цена далеко от среднего": abs(price - mean) / a[day] > 1.5, "вчера был большой день": (y["h"] - y["l"]) / a[day - 1] > 1.0, "вход в понедельник или пятницу": date.fromisoformat(r[day]["d"][:10]).weekday() in (0, 4), "уровню больше месяца": day - s["i"] > 21, }) return out def two_sample(a: list[float], b: list[float]) -> tuple[float, float]: """Разница средних и её погрешность для двух независимых групп.""" ma, mb = sum(a) / len(a), sum(b) / len(b) va = st.pvariance(a) / len(a) vb = st.pvariance(b) / len(b) return ma - mb, (va + vb) ** 0.5 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) raw = {n: bars(n) for n in NAMES} start = min(r[0]["d"][:10] for n, r in raw.items() if n in CRYPTO) raw = {n: [b for b in r if b["d"][:10] >= start] for n, r in raw.items()} all_t = [] for n, r in raw.items(): all_t += trades(n, r, adr(r)) rs = [t["r"] for t in all_t] sd = st.pstdev(rs) say("ДАННЫЕ") say(f" Общее окно с {start}. Один сетап, шесть инструментов.") say(f" Сделок: {len(all_t)}. Средний результат {sum(rs)/len(rs):+.3f} R.") say(f" Разброс одной сделки: {sd:.2f} R — это и есть та стена,") say(" о которую разбивается любая колонка журнала.") fields = [k for k in all_t[0] if k != "r"] say("") say("1 · КАЖДЫЙ ПРИЗНАК: ЕСТЬ ЛИ РАЗНИЦА, И ИЗМЕРЕНА ЛИ ОНА") say(" «да» — сделки, где признак верен; «нет» — остальные.") say(" Измерено, если разница не меньше двух своих погрешностей.") say("") found = [] for f in fields: yes = [t["r"] for t in all_t if t[f]] no = [t["r"] for t in all_t if not t[f]] if len(yes) < 30 or len(no) < 30: say(f" {f:32} мало сделок в одной из групп") continue diff, se = two_sample(yes, no) t_stat = abs(diff) / se if se else 0.0 mark = "измерено" if t_stat >= 2 else "шум" if t_stat >= 2: found.append(f) say(f" {f:32} да {len(yes):4} {sum(yes)/len(yes):+.3f} " f"нет {len(no):4} {sum(no)/len(no):+.3f} " f"разница {diff:+.3f} ± {se:.3f} " f"это {abs(diff / se) if se else 0:.1f} погрешности {mark}") say("") say(f" прошли проверку: {', '.join(found) if found else 'ни один'}") say("") say("2 · СКОЛЬКО СДЕЛОК НУЖНО КОЛОНКЕ, ЧТОБЫ ВООБЩЕ ЗАГОВОРИТЬ") say(" Чтобы разница δ при разбросе одной сделки σ дошла до двух погрешностей,") say(" нужно 8σ²/δ² сделок в КАЖДОЙ группе: погрешность разницы σ·√(2/n).") say(f" Здесь σ = {sd:.2f} R.") for delta in (0.05, 0.10, 0.20, 0.50): n = 8 * sd ** 2 / delta ** 2 say(f" разница {delta:.2f} R → {n:,.0f} сделок в каждой группе, " f"то есть {2 * n:,.0f} всего".replace(",", " ")) say(" Колонка делит журнал пополам в лучшем случае. Значит цифра") say(" справа — это сколько сделок надо СДЕЛАТЬ, чтобы колонка") say(" когда-нибудь ответила.") say("") say("3 · ЧТО ЕСЛИ КОЛОНОК МНОГО") say(" Двадцать колонок — двадцать проверок. При пороге «две") say(" погрешности» примерно одна проверка из двадцати проходит случайно.") say(" То есть в журнале из двадцати колонок в среднем одна «находка»") say(" ожидается даже тогда, когда ни одна колонка ничего не значит.") say(f" Здесь колонок {len(fields)}, прошло {len(found)}.") say("") say("4 · ОБЯЗАТЕЛЬНЫЕ ПОЛЯ — ТЕ, БЕЗ КОТОРЫХ НЕТ АРИФМЕТИКИ") say(" Цена входа, цена стопа, цена выхода, размер позиции, дата.") say(" Без них нельзя посчитать ни R, ни средний убыток, ни долю") say(" выигрышных — то есть ни одно число прошлого ролика.") say(" Их польза не в предсказании, и проверять её незачем.") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())