#!/usr/bin/env python3 """Все числа TW-031 одним прогоном. Тема S-07: три способа торговать один уровень. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. Один и тот же механизм — цена подошла к уровню — допускает разные способы им торговать. Обычно их подают как школы: «скальпер», «свинг», «позиционщик», и спор идёт о том, какая правильная. Спор пустой: у каждого способа своя цена, и цена эта измерима. ТРИ СПОСОБА, ОБЕЗЛИЧЕННО. Ни одной фамилии: важен не автор, а устройство. БЫСТРЫЙ: вход по касанию, стоп 0,5 дневного хода, цель в один стоп, держим не дольше двух дней. СРЕДНИЙ: вход по подтверждению (бар закрылся обратно от уровня), стоп 1,0 хода, цель в три стопа, до десяти дней. МЕДЛЕННЫЙ: вход по подтверждению, стоп 2,0 хода, выход сопровождением по экстремуму предыдущего дня, до тридцати дней. ЧТО МЕРЯЕТСЯ, КРОМЕ СРЕДНЕГО R. Средний R сравнивает способы так, будто они стоят одинаково. Они не стоят: · сколько сделок в год — это частота решений, то есть нагрузка; · доля выигрышных — это то, что человек переживает, а не считает; · сколько дней в позиции — это капитал, занятый и недоступный; · R на день в позиции — тот же результат, но с учётом занятого времени; · какая доля итога пришла из ОДНОЙ лучшей сделки — концентрация. Последнее важнее всех остальных. Способ, у которого половина результата пришла из одной сделки, не воспроизводим: у зрителя этой сделки не будет. ЧЕСТНОСТЬ УЧЁТА — та же, что в TW-026…TW-030: · один день — одна сделка в сторону: одно движение может задеть три излома, и это одно решение, а не три; · вход, уже стоящий за стопом, — не сделка: геп унёс цену дальше стопа, позиция закрылась бы в момент открытия; · в день входа засчитывается стоп, но не цель; · подход к уровню — с правильной стороны; · выбор проверяется на второй половине окна. ДАННЫЕ. `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 # уровень «живой», если цена подошла ближе трёх ходов ROLL = 60 # обычный ход: медиана high−low за 60 баров до бара STYLES = { "быстрый": {"entry": "touch", "stop": 0.5, "exit": "target", "target": 1.0, "hold": 2}, "средний": {"entry": "confirm", "stop": 1.0, "exit": "target", "target": 3.0, "hold": 10}, "медленный": {"entry": "confirm", "stop": 2.0, "exit": "trail", "target": None, "hold": 30}, } MAX_HOLD = max(s["hold"] for s in STYLES.values()) 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 - MAX_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) - MAX_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}) # ОДИН ДЕНЬ — ОДНО КАСАНИЕ В СТОРОНУ. Один бар задевает три излома, # трейдер входит один раз. Правило заведено во все расчёты канала # 10 сентября 2026 — см. open-tasks. 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 trade(r: list[dict], a: float, s: dict, st_: dict) -> dict | None: """Одна сделка одним способом. Возвращает результат и занятые дни.""" hit, price, kind = s["hit"], s["price"], s["kind"] if st_["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): return None day, fill = hit + 1, r[hit + 1]["o"] stop = (price + st_["stop"] * a[hit] if kind == "high" else price - st_["stop"] * a[hit]) # Вход, уже стоящий за стопом, — не сделка. if (kind == "high" and fill >= stop) or (kind == "low" and fill <= stop): return None gap = abs(stop - fill) if gap <= 0: return None target = (fill - st_["target"] * gap if st_["target"] and kind == "high" else fill + st_["target"] * gap if st_["target"] else None) res, days = None, st_["hold"] for j in range(day, min(day + 1 + st_["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, days = -1.0, j - day break if first: continue if target is not None and lo <= target: res, days = st_["target"], j - day break if st_["exit"] == "trail" and c > r[j - 1]["h"]: res, days = (fill - c) / gap, j - day break else: if lo <= stop: res, days = -1.0, j - day break if first: continue if target is not None and hi >= target: res, days = st_["target"], j - day break if st_["exit"] == "trail" and c < r[j - 1]["l"]: res, days = (c - fill) / gap, j - day break if res is None: last = r[min(day + st_["hold"], len(r) - 1)]["c"] res = ((fill - last) / gap if kind == "high" else (last - fill) / gap) return {"r": res * min(1.0, gap / (FLOOR * a[hit])), "days": max(days, 1)} # пол риска (П11) def collect(rows, sigs, name: str, half=None) -> list[dict]: st_ = STYLES[name] out = [] for n, r in rows.items(): a = adr(r) mid = len(r) // 2 for s in sigs[n]: if half is not None and (s["hit"] < mid) != (half == 0): continue t = trade(r, a, s, st_) if t: out.append(dict(t, name=n)) return out def summarise(t: list[dict]) -> dict: if not t: return {} rs = [x["r"] for x in t] total = sum(rs) best = max(rs) gross = sum(x for x in rs if x > 0) return {"n": len(t), "win": sum(1 for x in rs if x > 0) / len(rs) * 100, "avg": total / len(rs), "days": st.median([x["days"] for x in t]), "per_day": total / sum(x["days"] for x in t), # Доля ВАЛОВОЙ прибыли, пришедшая из одной лучшей сделки. # Делить на итог нельзя: у убыточного способа итог отрицателен, # и доля выходит бессмысленной (первый прогон дал 440 % и nan). "top": (best / gross * 100) if gross > 0 else 0.0, # Сколько сделок решилось в первый же день после входа. # Для быстрого способа это почти все, и это не результат, # а признак того, что дневных баров для него не хватает. "fast": sum(1 for x in t if x["days"] <= 1) / len(t) * 100} 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()} years = len(rows["AAPL"]) / 252 say("ДАННЫЕ") say(f" Общее окно с {start}, около {years:.1f} года торгов.") say(f" Касаний уровней: {sum(len(v) for v in sigs.values())} " f"на шесть инструментов.") say("") say("1 · ТРИ СПОСОБА НА ОДНИХ И ТЕХ ЖЕ СИГНАЛАХ") say(f" {'способ':11}{'сделок':>8}{'в год':>7}{'выигр.':>8}" f"{'R/сделку':>10}{'дней':>6}{'R/день':>9}{'из лучшей':>11}" f"{'решилось за день':>18}") data = {} for name in STYLES: t = collect(rows, sigs, name) d = summarise(t) data[name] = (t, d) say(f" {name:11}{d['n']:8}{d['n'] / years / 6:7.0f}{d['win']:7.0f}%" f"{d['avg']:+10.3f}{d['days']:6.0f}{d['per_day']:+9.3f}" f"{d['top']:10.0f}%{d['fast']:17.0f}%") say(" («в год» — на один инструмент; «из лучшей» — какая доля общего") say(" результата пришла из одной самой удачной сделки)") say("") say("1b · МОЖНО ЛИ БЫСТРЫЙ СПОСОБ ВООБЩЕ ИЗМЕРИТЬ ДНЕВНЫМИ БАРАМИ") say(" У сделки, которая входит по касанию и целится в один стоп,") say(" и стоп, и цель часто лежат ВНУТРИ дня входа. Четыре числа бара") say(" не говорят, что случилось раньше, поэтому учёт засчитывает") say(" худшее — стоп. Считаем, как часто выбор вообще есть.") both = only_stop = only_tgt = neither = 0 stf = STYLES["быстрый"] for n, r in rows.items(): a = adr(r) for sg in sigs[n]: hit, price, kind = sg["hit"], sg["price"], sg["kind"] stop = (price + stf["stop"] * a[hit] if kind == "high" else price - stf["stop"] * a[hit]) gap = abs(stop - price) tgt = (price - stf["target"] * gap if kind == "high" else price + stf["target"] * gap) b = r[hit] hit_stop = b["h"] >= stop if kind == "high" else b["l"] <= stop hit_tgt = b["l"] <= tgt if kind == "high" else b["h"] >= tgt if hit_stop and hit_tgt: both += 1 elif hit_stop: only_stop += 1 elif hit_tgt: only_tgt += 1 else: neither += 1 tot = both + only_stop + only_tgt + neither say(f" в день входа задеты И стоп, И цель: {both} из {tot} " f"({both * 100 / tot:.0f}%) — порядок неизвестен") say(f" только стоп: {only_stop} ({only_stop * 100 / tot:.0f}%), " f"только цель: {only_tgt} ({only_tgt * 100 / tot:.0f}%), " f"ни то ни другое: {neither} ({neither * 100 / tot:.0f}%)") say(" Значит у быстрого способа есть не число, а вилка: худший учёт") say(" (цель в день входа не считается) и лучший (считается).") opt = [] for n, r in rows.items(): a = adr(r) for sg in sigs[n]: hit, price, kind = sg["hit"], sg["price"], sg["kind"] stop = (price + stf["stop"] * a[hit] if kind == "high" else price - stf["stop"] * a[hit]) gap = abs(stop - price) tgt = (price - stf["target"] * gap if kind == "high" else price + stf["target"] * gap) res = None for j in range(hit, min(hit + 1 + stf["hold"], len(r))): hi, lo = r[j]["h"], r[j]["l"] tgt_hit = lo <= tgt if kind == "high" else hi >= tgt stop_hit = hi >= stop if kind == "high" else lo <= stop if tgt_hit: res = stf["target"] break if stop_hit: res = -1.0 break if res is None: last = r[min(hit + stf["hold"], len(r) - 1)]["c"] res = ((price - last) / gap if kind == "high" else (last - price) / gap) opt.append(res) say(f" худший учёт {data['быстрый'][1]['avg']:+.3f} R " f"лучший учёт {sum(opt) / len(opt):+.3f} R на {len(opt)} сделках") say(" Такая вилка — честный ответ. Одно число здесь было бы враньём.") say("") say("2 · ЧТО ОСТАНЕТСЯ, ЕСЛИ УБРАТЬ САМУЮ УДАЧНУЮ СДЕЛКУ") for name, (t, d) in data.items(): rs = sorted(x["r"] for x in t) cut = rs[:-1] say(f" {name:11} было {d['avg']:+.3f} R без лучшей " f"{sum(cut) / len(cut):+.3f} R") say("") say("3 · ПО ИНСТРУМЕНТАМ") say(f" {'способ':11}" + "".join(f"{n:>10}" for n in NAMES)) for name, (t, _) in data.items(): cells = [] for n in NAMES: rs = [x["r"] for x in t if x["name"] == n] cells.append(f"{sum(rs) / len(rs):+10.2f}" if rs else f"{'—':>10}") say(f" {name:11}" + "".join(cells)) say("") say("4 · ВЫБОР НА ПЕРВОЙ ПОЛОВИНЕ, ЗАМЕР НА ВТОРОЙ") first = {name: summarise(collect(rows, sigs, name, half=0)) for name in STYLES} second = {name: summarise(collect(rows, sigs, name, half=1)) for name in STYLES} pick = max(first, key=lambda k: first[k]["avg"]) say(f" лучший способ на первой половине: {pick}") say(f" первая половина {first[pick]['n']:5} сделок " f"{first[pick]['avg']:+.3f} R") say(f" вторая половина {second[pick]['n']:5} сделок " f"{second[pick]['avg']:+.3f} R") say(" все три на второй половине:") for name in STYLES: # ЧЕТЫРЕ ЗНАКА: на трёх медленный способ печатался как −0.125, и # нельзя было решить, «zero point one two» или «one three» (B-46). say(f" {name:11}{second[name]['n']:6} сделок " f"{second[name]['avg']:+.4f} R") say("") say("5 · ПРОВЕРКА НА ПОДОЗРИТЕЛЬНО ОДИНАКОВЫЙ ИСХОД") smelly = 0 for name, (t, _) in data.items(): for n in NAMES: rs = [round(x["r"], 6) for x in t if x["name"] == n] if len(rs) >= 10 and len(set(rs)) == 1: smelly += 1 say(f" ПОДОЗРИТЕЛЬНО {name} · {n}: {len(rs)} сделок, " f"все {rs[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())