#!/usr/bin/env python3 """Все числа TW-038 одним прогоном. Тема K-09: когда не входить — и чего это стоит. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Отказ от сделки — такое же решение, как вход, и его тоже можно измерить: сколько сделок правило снимает и что было бы с теми, которые оно сняло. СДЕЛКИ — ТЕ ЖЕ, ЧТО В УРОКЕ «КРИПТА ПРОТИВ АКЦИЙ» (TW-026, расчёт `samesetup.py`), импортом, а не копией: первое касание непроверенного излома, вход лимитной заявкой по уровню, стоп на половину дневного хода за уровнем, цель два стопа, выход по времени через десять дней, тот же честный учёт дня входа. Восемьсот восемнадцать сделок на шести инструментах. ТРИ ПРАВИЛА ОТКАЗА — КАЖДОЕ ИЗ УЖЕ СКАЗАННОГО В КУРСЕ: * НЕТ МЕСТА (R-03, TW-014): следующий уровень по ходу сделки ближе цели. Уровень — излом по правилу R-03 (выше или ниже пяти баров с каждой стороны), известный к закрытию дня ДО касания: излом становится известен только через пять баров после себя. Цель — два стопа, то есть один дневной ход от входа; * ОТКРЫТИЕ УЖЕ ЗА УРОВНЕМ (K-01, TW-004; F-06, TW-019): день касания открылся по ту сторону уровня — гэп через линию, на которой стоит заявка; * ИЗДЕРЖКИ ДОРОЖЕ ДЕСЯТОЙ ДОЛИ R (K-06): круг (0,05 % позиции на акциях, 0,20 % на крипте), делённый на стоп в процентах цены, больше 0,1. КАК МЕРИТСЯ. Снятые сделки против оставшихся, средний результат в R до издержек и после, разница с погрешностью; порог канала — две погрешности. Отдельно — на двух половинах окна: правило, которое помогает только на одной, правилом не считается. ДАННЫЕ. `04_assets/market/*-1d.json`, общее окно, как в TW-026. """ from __future__ import annotations import importlib.util import math import statistics as st import sys from pathlib import Path HERE = Path(__file__).resolve().parent OUT = HERE / "results.txt" SAME = HERE.parent / "TW-026-study" / "samesetup.py" sys.path.insert(0, str(HERE.parents[2] / "shared" / "pipeline")) from tw_accounting import err, verdict # noqa: E402 PIVOT = 5 # излом для «места» — как в R-03 (TW-014, room.py) COST_CAP = 0.10 # круг дороже десятой доли R — отказ lines: list[str] = [] def say(s: str = "") -> None: print(s) lines.append(s) def load_same(): """Расчёт урока K-06 — импортом: две копии одного сетапа разъедутся.""" spec = importlib.util.spec_from_file_location("samesetup", SAME) mk = importlib.util.module_from_spec(spec) spec.loader.exec_module(mk) return mk def pivots(r: list[dict]) -> tuple[list[tuple[int, float]], list[tuple[int, float]]]: """Изломы по правилу R-03: максимум (минимум) окна из пяти баров с каждой стороны.""" highs, lows = [], [] for i in range(PIVOT, len(r) - PIVOT): w = r[i - PIVOT:i + PIVOT + 1] if r[i]["h"] == max(b["h"] for b in w): highs.append((i, r[i]["h"])) if r[i]["l"] == min(b["l"] for b in w): lows.append((i, r[i]["l"])) return highs, lows def room(t: dict, highs, lows, a: float) -> float: """Место до следующего уровня по ходу сделки, в дневных ходах. Уровни — только известные к закрытию дня ДО касания: излом на баре j становится изломом на закрытии бара j + PIVOT. """ known_by = t["i"] - 1 if t["kind"] == "high": # продажа от сопротивления: место вниз below = [p for j, p in lows if j + PIVOT <= known_by and p < t["price"]] return (t["price"] - max(below)) / a if below else math.inf above = [p for j, p in highs if j + PIVOT <= known_by and p > t["price"]] return (min(above) - t["price"]) / a if above else math.inf def mean(xs: list[float]) -> float: return st.fmean(xs) if xs else float("nan") def compare(kept: list[dict], cut: list[dict], key: str) -> str: a_ = [x[key] for x in kept] b_ = [x[key] for x in cut] if len(a_) < 2 or len(b_) < 2: return "мало сделок" measured, d, two = verdict(a_, b_) k = abs(d) / (two / 2) if two else 0.0 return (f"оставшиеся {mean(a_):+.3f} против снятых {mean(b_):+.3f}: " f"разница {d:+.3f} ± {two / 2:.3f} ({k:.1f} погр.) — " f"{'ИЗМЕРЕНА' if measured else 'шум'}") def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") mk = load_same() names = mk.STOCKS + mk.CRYPTO rows = {n: mk.bars(n) for n in names} start = min(r[0]["d"][:10] for n, r in rows.items() if n in mk.CRYPTO) rows = {n: [b for b in r if b["d"][:10] >= start] for n, r in rows.items()} end = min(r[-1]["d"][:10] for r in rows.values()) days = sorted({b["d"][:10] for r in rows.values() for b in r if b["d"][:10] <= end}) middle = days[len(days) // 2] trades = [] for n, r in rows.items(): a = mk.adr(r) highs, lows = pivots(r) fee = mk.FEE_CRYPTO if n in mk.CRYPTO else mk.FEE_STOCK for t in mk.trades(r, a): i = t["i"] x = dict(t) x["name"] = n x["date"] = r[i]["d"][:10] x["half"] = 1 if x["date"] < middle else 2 x["room"] = room(t, highs, lows, a[i]) x["gap"] = (r[i]["o"] > t["price"]) if t["kind"] == "high" else (r[i]["o"] < t["price"]) x["cost_r"] = fee / (t["stop_pct"] / 100) x["after"] = t["res"] - x["cost_r"] trades.append(x) target_ranges = mk.TARGET * mk.BUFFER # цель в дневных ходах от входа say("ДАННЫЕ") say(f" Сделки урока TW-026 (samesetup.py), общее окно {start} … {end}") say(f" всего сделок: {len(trades)}") for n in names: t = [x for x in trades if x["name"] == n] say(f" {n:9} {len(t):4} до издержек {mean([x['res'] for x in t]):+.3f}R " f"после {mean([x['after'] for x in t]):+.3f}R") say(f" все: до издержек {mean([x['res'] for x in trades]):+.3f}R, " f"после {mean([x['after'] for x in trades]):+.3f}R") say(f" половины окна: до {middle} и с {middle}") rules = { "нет места": lambda x: x["room"] < target_ranges, "открытие за уровнем": lambda x: x["gap"], "издержки дороже 0.1R": lambda x: x["cost_r"] > COST_CAP, } say("") say("1 · КАЖДОЕ ПРАВИЛО ОТКАЗА ПО ОТДЕЛЬНОСТИ") say(f" цель — {target_ranges:.1f} дневного хода от входа; «нет места» — следующий излом ближе неё") for name, f in rules.items(): cut = [x for x in trades if f(x)] kept = [x for x in trades if not f(x)] say("") say(f" {name}: снимает {len(cut)} из {len(trades)} ({len(cut) / len(trades) * 100:.0f}%)") say(f" до издержек: {compare(kept, cut, 'res')}") say(f" после издержек: {compare(kept, cut, 'after')}") for h in (1, 2): ch = [x for x in cut if x["half"] == h] kh = [x for x in kept if x["half"] == h] say(f" половина {h}: снято {len(ch):3}, {compare(kh, ch, 'after')}") by = [] for n in names: c_ = [x for x in cut if x["name"] == n] by.append(f"{n} {len(c_)}") say(f" снято по инструментам: {', '.join(by)}") say("") say("2 · ВСЕ ТРИ ВМЕСТЕ") anyf = lambda x: any(f(x) for f in rules.values()) # noqa: E731 cut = [x for x in trades if anyf(x)] kept = [x for x in trades if not anyf(x)] say(f" снято {len(cut)} из {len(trades)} ({len(cut) / len(trades) * 100:.0f}%), осталось {len(kept)}") say(f" до издержек: {compare(kept, cut, 'res')}") say(f" после издержек: {compare(kept, cut, 'after')}") for h in (1, 2): ch = [x for x in cut if x["half"] == h] kh = [x for x in kept if x["half"] == h] say(f" половина {h}: снято {len(ch):3}, {compare(kh, ch, 'after')}") k_after = [x["after"] for x in kept] m_, e_ = mean(k_after), err(k_after) where = ("выше нуля на две погрешности и больше" if m_ > 2 * e_ else "ниже нуля на две погрешности и больше" if m_ < -2 * e_ else "не отличимо от нуля") say(f" оставшиеся после издержек: {m_:+.3f} ± {e_:.3f} R на сделку ({where}, {abs(m_) / e_:.1f} погр.)") for n in names: t = [x["after"] for x in kept if x["name"] == n] say(f" {n:9} осталось {len(t):4}, после издержек {mean(t):+.3f}R") say("") say("3 · ДВА РОДА ОТКАЗА: АРИФМЕТИКА И ПРОГНОЗ") say(" Издержки известны ДО сделки: круг ÷ стоп. Разница в издержках между снятыми") say(" и оставшимися — не замер с погрешностью, а точное число:") cf = rules["издержки дороже 0.1R"] cut_c = [x["cost_r"] for x in trades if cf(x)] kept_c = [x["cost_r"] for x in trades if not cf(x)] say(f" снятые платят в среднем {mean(cut_c):.3f}R за круг, оставшиеся {mean(kept_c):.3f}R — " f"разница {mean(cut_c) - mean(kept_c):.3f}R на сделку, без погрешности") say(" Размер позиции тоже известен до сделки (K-03): риск 1% счёта ÷ стоп в % цены.") say(" Позиция больше счёта — значит заёмные деньги:") over = [x for x in trades if 0.01 / (x["stop_pct"] / 100) > 1.0] say(f" таких сделок {len(over)} из {len(trades)} ({len(over) / len(trades) * 100:.0f}%)") for n in names: t = [x for x in trades if x["name"] == n] o = [x for x in t if 0.01 / (x["stop_pct"] / 100) > 1.0] med = st.median(0.01 / (x["stop_pct"] / 100) for x in t) say(f" {n:9} {len(o):4} из {len(t):4}; медианная позиция {med:.2f} счёта") say(" «Нет места» и «открытие за уровнем» — прогнозы: они говорят, что сделка") say(" хуже, и это надо мерить. На этих сделках мерка их не различает (раздел 1).") say("") say("ПРИМЕРЫ ДЛЯ КАДРА — самая поздняя сделка Apple под каждым правилом-прогнозом,") say("без выбора по исходу:") r_ap = rows["AAPL"] a_ap = mk.adr(r_ap) for name in ("нет места", "открытие за уровнем"): f = rules[name] ex = [x for x in trades if x["name"] == "AAPL" and f(x)] if not ex: continue x = ex[-1] i = x["i"] side = "продажа" if x["kind"] == "high" else "покупка" risk = x["risk"] target = x["price"] - 2 * risk if x["kind"] == "high" else x["price"] + 2 * risk stop = x["price"] + risk if x["kind"] == "high" else x["price"] - risk nxt = (x["price"] - x["room"] * a_ap[i]) if x["kind"] == "high" else (x["price"] + x["room"] * a_ap[i]) say(f" {name}: {x['date']}, {side} от {x['price']:.2f}; стоп {stop:.2f}, цель {target:.2f}; " f"следующий излом {nxt:.2f} ({x['room']:.2f} хода от входа); открытие {r_ap[i]['o']:.2f}; " f"дневной ход {a_ap[i]:.2f}; результат {x['res']:+.2f}R") say("") say("4 · ВЫВОД") meas = [] for name, f in rules.items(): cut_ = [x["after"] for x in trades if f(x)] kept_ = [x["after"] for x in trades if not f(x)] m, d, two = verdict(kept_, cut_) both = all(verdict([x["after"] for x in trades if not f(x) and x["half"] == h], [x["after"] for x in trades if f(x) and x["half"] == h])[1] > 0 for h in (1, 2)) meas.append((name, m and d > 0, both)) good = [n for n, m, b in meas if m and b] say(" Правило отказа, которое измеримо улучшает оставшиеся сделки и делает это") say(f" на обеих половинах окна: {', '.join(good) if good else 'ни одного'}.") say(f" Оставшиеся сделки после всех отказов: {mean(k_after):+.3f}R — " f"{'в плюсе' if mean(k_after) > 0 else 'по-прежнему в минусе'}.") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())