#!/usr/bin/env python3 """Все числа TW-039 одним прогоном. Тема S-08: что делать с открытой позицией. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Курс мерил вход, стоп и выход — но не то, что трейдер делает МЕЖДУ ними: переносит стоп в безубыток, забирает половину, «даёт прибыли бежать». СДЕЛКИ — ОТБОИ ИЗ ПЕРВОГО УРОКА МОДУЛЯ S (TW-027, расчёт `bounce.py`), касания — его же функцией, импортом: первое касание непроверенного излома с правильной стороны. Настройки — средние из того урока: вход по касанию лимитной заявкой, стоп на один дневной ход за уровнем, цель два стопа, выход по времени через десять дней. ЧЕТЫРЕ СПОСОБА ВЕСТИ ОДНУ И ТУ ЖЕ СДЕЛКУ: A · не трогать; B · безубыток: когда цена прошла +1R, со СЛЕДУЮЩЕГО дня стоп стоит на входе; C · половину на +1R: половина позиции закрывается лимитной заявкой на +1R, остаток идёт к цели, стопу или времени, как в A; D · половину на +1R и остаток в безубыток (C и B вместе). ЧЕСТНОСТЬ УЧЁТА — ТА ЖЕ, ЧТО В МОДУЛЕ S: в день входа засчитывается только стоп, ни +1R, ни цель; если в один день задеты и стоп, и +1R (или цель), считается стоп — порядок внутри дня четыре цены не хранят. Перенос стопа действует со следующего дня: днём, когда цена прошла +1R, трейдер на дневных барах узнаёт об этом только на закрытии. КАК МЕРИТСЯ. Все четыре способа — на ОДНИХ И ТЕХ ЖЕ сделках, поэтому сравнение парное: разница считается сделка к сделке, у неё и погрешность. Порог канала — две погрешности; отдельно — на двух половинах окна. """ from __future__ import annotations import importlib.util import statistics as st import sys from pathlib import Path HERE = Path(__file__).resolve().parent OUT = HERE / "results.txt" BOUNCE = HERE.parent / "TW-027-study" / "bounce.py" STOP = 1.0 # стоп в дневных ходах за уровнем TARGET = 2.0 # цель в стопах HOLD = 10 # выход по времени STEP = 1.0 # где срабатывает правило ведения, в R lines: list[str] = [] def say(s: str = "") -> None: print(s) lines.append(s) def load_bounce(): spec = importlib.util.spec_from_file_location("bounce", BOUNCE) mk = importlib.util.module_from_spec(spec) spec.loader.exec_module(mk) return mk def walk(r: list[dict], s: dict, a: list, way: str) -> dict: """Одна сделка одним способом. Результат — в R всей позиции.""" hit, price, kind = s["hit"], s["price"], s["kind"] short = kind == "high" fill = price gap = STOP * a[hit] stop0 = price + gap if short else price - gap target = fill - TARGET * gap if short else fill + TARGET * gap step = fill - STEP * gap if short else fill + STEP * gap def r_of(px: float) -> float: return ((fill - px) if short else (px - fill)) / gap stop = stop0 half_done = False # половина уже закрыта на +1R trig = False # +1R пройден move_next = False # перенести стоп со следующего дня banked = 0.0 # закрыто по половине size = 1.0 # доля позиции в рынке touched_step = False for j in range(hit, min(hit + 1 + HOLD, len(r))): if move_next: stop = fill move_next = False first = j == hit hi, lo = r[j]["h"], r[j]["l"] stopped = hi >= stop if short else lo <= stop if stopped: return {"res": banked + size * r_of(stop), "step": touched_step} if first: continue reached_step = lo <= step if short else hi >= step if reached_step: touched_step = True if reached_step and not trig: trig = True if way in ("C", "D"): banked += 0.5 * STEP size = 0.5 half_done = True if way in ("B", "D"): move_next = True if (lo <= target) if short else (hi >= target): return {"res": banked + size * TARGET, "step": True} last = r[min(hit + HOLD, len(r) - 1)]["c"] return {"res": banked + size * r_of(last), "step": touched_step} def paired(x: list[float], y: list[float]) -> tuple[float, float]: d = [a - b for a, b in zip(x, y)] return st.fmean(d), (st.pstdev(d) / len(d) ** 0.5 if len(d) > 1 else float("inf")) def trades() -> tuple[list[dict], dict]: """Сделки урока: касания TW-027, каждая — всеми четырьмя способами. Вынесено из `main`, чтобы урок о проверке своей версии (TW-041) брал ТЕ ЖЕ сделки импортом, а не копией кода: копия разошлась бы с этим уроком при первой же правке здесь.""" mk = load_bounce() 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] T = [] for n, r in rows.items(): a = mk.adr(r) for s in mk.signals(r, a): row = {"name": n, "date": r[s["hit"]]["d"][:10]} for way in "ABCD": w = walk(r, s, a, way) row[way] = w["res"] if way == "A": row["step"] = w["step"] row["half"] = 1 if row["date"] < middle else 2 T.append(row) return T, {"start": start, "end": end, "middle": middle, "names": names} def main() -> int: for s_ in (sys.stdout, sys.stderr): if hasattr(s_, "reconfigure"): s_.reconfigure(encoding="utf-8", errors="replace") T, meta = trades() start, end, middle, names = meta["start"], meta["end"], meta["middle"], meta["names"] say("ДАННЫЕ") say(f" Касания урока TW-027 (bounce.py), общее окно {start} … {end}") say(f" вход по касанию, стоп {STOP:.0f} ход за уровнем, цель {TARGET:.0f} стопа, выход через {HOLD} дней") say(f" сделок: {len(T)}; половины окна: до {middle} и с {middle}") names_ = {"A": "не трогать", "B": "безубыток после +1R", "C": "половину на +1R", "D": "половину и безубыток"} say("") say("1 · ЧЕТЫРЕ СПОСОБА ВЕСТИ ОДНИ И ТЕ ЖЕ СДЕЛКИ") say(f" {'способ':24}{'R/сделку':>10}{'в плюсе':>10}{'средний выигрыш':>17}{'средний убыток':>16}") for w in "ABCD": xs = [t[w] for t in T] wins = [x for x in xs if x > 1e-9] losses = [x for x in xs if x < -1e-9] say(f" {w} · {names_[w]:20}{st.fmean(xs):+10.3f}{len(wins) / len(xs) * 100:9.0f}%" f"{(st.fmean(wins) if wins else 0):+16.2f}{(st.fmean(losses) if losses else 0):+16.2f}") flat = sum(1 for t in T if abs(t["B"]) < 1e-9) say(f" у B ровно ноль — {flat} сделок ({flat / len(T) * 100:.0f}%): выбило на входе после +1R") say("") say("2 · РАЗНИЦА С «НЕ ТРОГАТЬ», СДЕЛКА К СДЕЛКЕ") for w in "BCD": m, se = paired([t[w] for t in T], [t["A"] for t in T]) k = abs(m) / se if se else 0.0 say(f" {w} − A: {m:+.3f} ± {se:.3f} ({k:.1f} погр.) — {'ИЗМЕРЕНА' if k >= 2 else 'шум'}") for h in (1, 2): th = [t for t in T if t["half"] == h] mh, sh = paired([t[w] for t in th], [t["A"] for t in th]) say(f" половина {h}: {mh:+.3f} ± {sh:.3f} на {len(th)}") per = [] for n in names: tn = [t for t in T if t["name"] == n] mn, _ = paired([t[w] for t in tn], [t["A"] for t in tn]) per.append(f"{n} {mn:+.2f}") say(f" по инструментам: {', '.join(per)}") say("") say("3 · ГДЕ ПЛАТИТ БЕЗУБЫТОК") reached = [t for t in T if t["step"]] say(f" до +1R дошли {len(reached)} сделок из {len(T)} ({len(reached) / len(T) * 100:.0f}%)") better = sum(1 for t in reached if t["B"] > t["A"] + 1e-9) worse = sum(1 for t in reached if t["B"] < t["A"] - 1e-9) same = len(reached) - better - worse say(f" из них безубыток помог {better}, навредил {worse}, ничего не изменил {same}") helped = [t["B"] - t["A"] for t in reached if t["B"] > t["A"] + 1e-9] hurt = [t["B"] - t["A"] for t in reached if t["B"] < t["A"] - 1e-9] if helped and hurt: say(f" помог в среднем на {st.fmean(helped):+.2f}R, навредил в среднем на {st.fmean(hurt):+.2f}R") say("") say("4 · ВЫВОД") meas = [] for w in "BCD": m, se = paired([t[w] for t in T], [t["A"] for t in T]) meas.append((w, m, se, abs(m) >= 2 * se)) good = [f"{names_[w]} ({m:+.3f})" for w, m, se, ok in meas if ok] say(" Способ ведения, который измеримо отличается от «не трогать»:") say(f" {', '.join(good) if good else 'ни один'}.") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())