#!/usr/bin/env python3 """Все числа TW-028 одним прогоном. Тема S-02: пробой и его условия. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ОТКУДА ГИПОТЕЗА. TW-024 измерил видимые уровни — шестидесятидневные экстремумы — и нашёл две вещи: ложных пробоев у них МЕНЬШЕ, а после закрытия за уровнем цена уходит ДАЛЬШЕ. Для отбоя это плохая новость, и TW-023 её подтвердил: видимый уровень отскакивает хуже незаметного. Значит один и тот же признак должен работать в разные стороны для двух сетапов. Для отбоя видимость — помеха. Для пробоя — условие. Здесь это проверяется прямо. ЧТО СЧИТАЕТСЯ. Сигнал — закрытие за уровнем. Дальше три решения, как в TW-027, и четыре условия отбора. ВХОД: по открытию следующего дня после закрытия за уровнем; или по возврату к уровню (ретест) в течение пяти дней. СТОП: за уровнем на 0,5 или 1,0 дневного хода; или за экстремумом бара пробоя со стороны, откуда пришли. ВЫХОД: цель в два стопа; выход по времени; сопровождение по экстремуму предыдущего дня. УСЛОВИЯ: видимый уровень или нет; бар пробоя широкий или обычный; уровень проверяли до пробоя или нет. ЧЕСТНОСТЬ УЧЁТА — та же, что в TW-026 и TW-027: в день входа засчитывается стоп, но не цель; ретест засчитывается только если цена действительно вернулась к уровню; выбор лучшего сочетания проверяется на второй половине окна, которая в выборе не участвовала. ДАННЫЕ. `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" STOCKS = ["AAPL", "TSLA", "SPY"] CRYPTO = ["BTCUSDT", "ETHUSDT", "SOLUSDT"] WING = 2 WIDE = 1.8 SEEN = 60 HOLD = 10 TARGET = 2.0 ROLL = 60 # обычный ход: медиана high−low за 60 баров до бара RETEST = 5 # сколько дней ждём возврата к уровню ENTRIES = ["next", "retest"] STOPS = [0.5, 1.0, "bar"] EXITS = ["target", "time", "trail"] 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, err, FLOOR # noqa: E402 def breaks(r: list[dict], a: float) -> list[dict]: """Пробои: закрытие за уровнем-изломом, с признаками уровня и бара.""" out = [] for i in range(WING, len(r) - WING - HOLD - RETEST - 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)) # Первое закрытие за уровнем — и это пробой. Дальше уровень # считается отработанным: второй пробой той же линии — другое # событие, и мешать их в одну выборку нельзя. day, tests = None, 0 touching = False for j in range(i + 1, min(i + 1 + 120, len(r) - HOLD - RETEST - 2)): if kind == "high": if r[j]["c"] > price: day = j break hit = r[j]["h"] >= price else: if r[j]["c"] < price: day = j break hit = r[j]["l"] <= price if hit and not touching: tests += 1 if not hit: touching = False else: touching = True if day is None or a[day] is None: continue # бар без истории хода не торгуется wide = (r[day]["h"] - r[day]["l"]) > WIDE * a[day] out.append({"day": day, "price": price, "kind": kind, "seen": seen, "wide": wide, "tests": tests}) # ОДИН ДЕНЬ — ОДНА СДЕЛКА В СТОРОНУ. Одно закрытие может оказаться за # тремя изломами подряд, и каждый из них формально «пробит впервые». # Считать это тремя сделками нельзя: трейдер видит одно движение и входит # один раз, а выборка раздувается одинаковыми исходами и делает вид, # что независимых наблюдений втрое больше. # # Найдено 10 сентября 2026 при работе над TW-030: на TSLA двадцать две # короткие сделки дали ровно −1R каждая при тринадцати днях входа. # Из совпавших остаётся уровень, ближайший к закрытию дня: остальные # тем же движением пройдены раньше и отдельным событием не являются. best: dict[tuple[int, str], dict] = {} for s in out: key = (s["day"], s["kind"]) near = abs(r[s["day"]]["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["day"])] def run(r: list[dict], a: float, sig: list[dict], entry: str, stop_rule, exit_rule: str, cond=None, out=None) -> tuple[int, float]: """Один набор решений. cond — необязательный фильтр по условию. `out` — необязательный список, куда кладётся результат КАЖДОЙ сделки: без него у среза нет погрешности, а фраза «тридцать четыре сделки не отличают +0.19 от нуля» (B-01 второго аудита) не была бы померена. """ n, total = 0, 0.0 for s in sig: if cond and not cond(s): continue day0, price, kind = s["day"], s["price"], s["kind"] if entry == "next": if day0 + 1 >= len(r): continue day, fill = day0 + 1, r[day0 + 1]["o"] else: # РЕТЕСТ. Цена должна вернуться к уровню в ближайшие дни; # вход по цене уровня, потому что заявка стоит именно там. day = None for j in range(day0 + 1, min(day0 + 1 + RETEST, len(r))): if r[j]["l"] <= price <= r[j]["h"]: day = j break if day is None: continue fill = price if a[day0] is None: # бар без истории хода не торгуется continue if stop_rule == "bar": stop = (r[day0]["l"] if kind == "high" else r[day0]["h"]) else: stop = (price - stop_rule * a[day0] if kind == "high" else price + stop_rule * a[day0]) if ((kind == "high" and fill <= stop) or (kind == "low" and fill >= stop)): # ВХОД, УЖЕ СТОЯЩИЙ ЗА СТОПОМ, — НЕ СДЕЛКА: геп унёс цену # дальше стопа, и позиция закрылась бы в момент открытия. # Расчёт брал её и записывал ровно −1R. Найдено 10 сентября # 2026 на TSLA в расчёте TW-030. continue gap = abs(fill - stop) 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 lo <= stop: res = -1.0 break if exit_rule == "target" and not first and hi >= target: res = TARGET break if (exit_rule == "trail" and not first and r[j]["c"] < r[j - 1]["l"]): res = (r[j]["c"] - fill) / gap break else: if hi >= stop: res = -1.0 break if exit_rule == "target" and not first and lo <= target: res = TARGET break if (exit_rule == "trail" and not first and r[j]["c"] > r[j - 1]["h"]): res = (fill - r[j]["c"]) / gap break if res is None: last = r[min(day + HOLD, len(r) - 1)]["c"] res = ((last - fill) / gap if kind == "high" else (fill - last) / gap) n += 1 val = res * min(1.0, gap / (FLOOR * a[day0])) # пол риска (П11) total += val if out is not None: out.append(val) return n, total def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") rows = {n: bars(n) for n in STOCKS + CRYPTO} start = min(r[0]["d"][:10] for n, r in rows.items() if n in CRYPTO) rows = {n: [b for b in r if b["d"][:10] >= start] for n, r in rows.items()} print("ДАННЫЕ") print(f" Общее окно с {start}.") for n, r in rows.items(): print(f" {n:9} {len(r)} баров") sig = {n: breaks(r, adr(r)) for n, r in rows.items()} print("\nСЫРЬЁ: ПРОБОЕВ УРОВНЕЙ") for n in rows: v = sig[n] print(f" {n:9} {len(v):5} из них видимых " f"{sum(1 for s in v if s['seen']):4}, " f"на широком баре {sum(1 for s in v if s['wide']):4}") print("\n1 · ВОСЕМНАДЦАТЬ СОЧЕТАНИЙ, СРЕДНЕЕ ПО ШЕСТИ ИНСТРУМЕНТАМ") print(f" {'вход':>8}{'стоп':>7}{'выход':>9}{'сделок':>9}{'R/сделку':>11}") table = [] for entry in ENTRIES: for stop_rule in STOPS: for exit_rule in EXITS: k_all, r_all = 0, 0.0 for n, r in rows.items(): k, s_ = run(r, adr(r), sig[n], entry, stop_rule, exit_rule) k_all += k r_all += s_ if not k_all: continue avg = r_all / k_all table.append((avg, entry, stop_rule, exit_rule, k_all)) name = (stop_rule if isinstance(stop_rule, str) else f"{stop_rule:.1f}") print(f" {entry:>8}{name:>7}{exit_rule:>9}{k_all:9}" f"{avg:+10.3f}R") table.sort(reverse=True) # РЕШЕНИЯ ХРАНЯТСЯ ПОИМЕННО: вывод внизу сравнивает вход # на следующий день с входом по ретесту, и это сравнение # должно пересчитываться, а не помниться. dec: dict[tuple[str, str], float] = {} print("\n2 · ЧТО РЕШАЕТ КАЖДОЕ ИЗ ТРЁХ РЕШЕНИЙ") for label, idx, options in (("вход", 1, ENTRIES), ("стоп", 2, STOPS), ("выход", 3, EXITS)): print(f" {label}:") for opt in options: vals = [t[0] for t in table if t[idx] == opt] if vals: name = opt if isinstance(opt, str) else f"{opt:.1f}" dec[(label, name)] = st.mean(vals) print(f" {name:>8} среднее {st.mean(vals):+.3f}R" f" лучшее {max(vals):+.3f}R") # СТРОКИ РАЗБОРА ХРАНЯТСЯ. Вывод внизу файла собирается из них, # а не пишется словами: иначе он отвечает таблицам ровно один # день — тот, в который его набрали. keep: dict[str, tuple[float, int]] = {} print("\n3 · УСЛОВИЯ ОТБОРА — ГЛАВНЫЙ ВОПРОС ЭТОГО РОЛИКА") print(" Базовое сочетание: вход на следующий день, стоп 1.0, выход") print(" сопровождением. Меняется только условие допуска сигнала.") conds = [ ("все пробои", None), ("видимый уровень", lambda s: s["seen"]), ("незаметный уровень", lambda s: not s["seen"]), ("бар пробоя широкий", lambda s: s["wide"]), ("бар обычный", lambda s: not s["wide"]), ("уровень не тестировали", lambda s: s["tests"] == 0), ("тестировали 2 раза и больше", lambda s: s["tests"] >= 2), ("видимый И широкий", lambda s: s["seen"] and s["wide"]), ] each = {} for label, cond in conds: k_all, r_all = 0, 0.0 each[label] = [] for n, r in rows.items(): k, s_ = run(r, adr(r), sig[n], "next", 1.0, "trail", cond, out=each[label]) k_all += k r_all += s_ avg = r_all / k_all if k_all else 0.0 keep[label] = (avg, k_all) print(f" {label:30} {k_all:5} сделок {avg:+.3f}R") # ТОНКИЙ СРЕЗ — С ПОГРЕШНОСТЬЮ. «Тестировали 2+» стоит на трёх с # небольшим десятках сделок; без погрешности его плюс читается как # находка, а с ней видно, что отличить его от нуля нельзя. for label in ("тестировали 2 раза и больше", "видимый И широкий"): xs = each[label] e = err(xs) print(f" {label}: погрешность {e:.3f}, это " f"{abs(st.mean(xs)) / e:.1f} погрешности от нуля") # ШИРОКИЙ БАР ПРОТИВ ОБЫЧНОГО — РАЗНИЦА С ПОГРЕШНОСТЬЮ. Голос s34–s36 # подавал разрыв как находку и объяснял его (B-13 второго аудита); # без погрешности разницы этого не проверить. w_, o_ = each["бар пробоя широкий"], each["бар обычный"] d_ = st.mean(w_) - st.mean(o_) se = (err(w_) ** 2 + err(o_) ** 2) ** 0.5 print(f" широкий против обычного: разница {d_:+.3f}R, погрешность " f"{se:.3f}, это {abs(d_) / se:.1f} погрешности") print("\n4 · ВВЕРХ И ВНИЗ ПО ОТДЕЛЬНОСТИ — ПРОВЕРКА НА ТРЕНД") print(" Окно 2024–2026 у акций и крипты в основном растущее, а пробой") print(" вверх по определению идёт в сторону этого роста. Если весь") print(" плюс сидит в лонгах, это измерение рынка, а не сетапа.") for label, cond in ( ("пробои вверх", lambda s: s["kind"] == "high"), ("пробои вниз", lambda s: s["kind"] == "low"), ("видимый, вверх", lambda s: s["seen"] and s["kind"] == "high"), ("видимый, вниз", lambda s: s["seen"] and s["kind"] == "low"), ("незаметный, вверх", lambda s: not s["seen"] and s["kind"] == "high"), ("незаметный, вниз", lambda s: not s["seen"] and s["kind"] == "low")): k_all, r_all = 0, 0.0 each[label] = [] for n, r in rows.items(): k, s_ = run(r, adr(r), sig[n], "next", 1.0, "trail", cond, out=each[label]) k_all += k r_all += s_ avg = r_all / k_all if k_all else 0.0 keep[label] = (avg, k_all) print(f" {label:20} {k_all:5} сделок {avg:+.3f}R") # ВИДИМЫЙ ПРОТИВ НЕЗАМЕТНОГО ВНУТРИ НАПРАВЛЕНИЯ — С ПОГРЕШНОСТЬЮ (B-12). # Голос s49 говорит «visible beats unremarkable within each direction»; # печать показывала только видимые уровни, и проверить фразу было нечем. strat_d, strat_v = 0.0, 0.0 n_all = keep["пробои вверх"][1] + keep["пробои вниз"][1] for side in ("вверх", "вниз"): xa, xb = each[f"видимый, {side}"], each[f"незаметный, {side}"] d_ = st.mean(xa) - st.mean(xb) se = (err(xa) ** 2 + err(xb) ** 2) ** 0.5 print(f" видимый против незаметного, {side}: разница {d_:+.3f}R, " f"погрешность {se:.3f}, это {abs(d_) / se:.1f} погрешности") w = keep[f"пробои {side}"][1] / n_all strat_d += w * d_ strat_v += (w * se) ** 2 # ОБЩЕЕ СРАВНЕНИЕ СМЕШИВАЕТ НАПРАВЛЕНИЯ: среди видимых уровней пробоев # вверх больше, чем среди незаметных, а вверх в этом окне шло лучше. # Разницы внутри направлений, взвешенные долей пробоев каждого # направления, — это сравнение при равном составе. strat_e = strat_v ** 0.5 keep["видимость при равном составе"] = (strat_d, strat_e) print(f" при равном составе направлений: разница {strat_d:+.3f}R, " f"погрешность {strat_e:.3f}, это {abs(strat_d) / strat_e:.1f} погрешности") print("\n5 · ПРОВЕРКА ВЫБОРА НА ОТДЕЛЬНОЙ ПОЛОВИНЕ") print(" Как в предыдущем ролике: выбираем на первой половине окна,") print(" измеряем на второй, которая в выборе не участвовала.") half = {n: (r[:len(r) // 2], r[len(r) // 2:]) for n, r in rows.items()} best, best_avg, best_n = None, -99.0, 0 for entry in ENTRIES: for stop_rule in STOPS: for exit_rule in EXITS: k_all, r_all = 0, 0.0 for n, (first, _) in half.items(): a = adr(first) k, s_ = run(first, a, breaks(first, a), entry, stop_rule, exit_rule) k_all += k r_all += s_ if k_all and r_all / k_all > best_avg: best_avg, best, best_n = r_all / k_all, ( entry, stop_rule, exit_rule), k_all entry, stop_rule, exit_rule = best name = stop_rule if isinstance(stop_rule, str) else f"{stop_rule:.1f}" print(f" Выбрано на первой половине: {entry}, стоп {name}, " f"{exit_rule} — {best_avg:+.3f}R на {best_n} сделках") k_all, r_all = 0, 0.0 for n, (_, second) in half.items(): a = adr(rows[n])[len(half[n][0]):] # ход второй половины знает только прошлое k, s_ = run(second, a, breaks(second, a), entry, stop_rule, exit_rule) k_all += k r_all += s_ out_half = r_all / k_all if k_all else 0.0 out_half_n = k_all print(f" На второй половине: {out_half:+.3f}R " f"на {out_half_n} сделках") print("") # ВЫВОД СЧИТАЕТСЯ. def v(label): return keep[label][0] def k(label): return keep[label][1] # ВЕРДИКТЫ ТОЖЕ СЧИТАЮТСЯ (B-27 второго аудита). Прежняя печать # держала готовые слова — «не уцелело», «мешает», «прежний плюс # состоял из повторов» — и после правки учёта они спорили с числами # в той же фразе: тест 2+ остался в плюсе (+0.188R), исчезла выборка, # а «мешает» стояло при разнице в 0.2 погрешности. Порог курса — # две погрешности: меньше — «этой выборкой не отличить». def gap(a, b): """-> (разница средних, её погрешность, во сколько погрешностей).""" xa, xb = each[a], each[b] d = st.mean(xa) - st.mean(xb) e = (err(xa) ** 2 + err(xb) ** 2) ** 0.5 return d, e, abs(d) / e if e else 0.0 def verdict(z): return ("измерено" if z >= 2 else "этой выборкой от нуля не отличить") base_avg, base_n = keep["все пробои"] base_e = err(each["все пробои"]) base_z = abs(base_avg) / base_e if base_e else 0.0 vis = gap("видимый уровень", "незаметный уровень") tst = gap("тестировали 2 раза и больше", "уровень не тестировали") wid = gap("бар пробоя широкий", "бар обычный") print("ВЫВОД, ПОСЧИТАННЫЙ ЭТИМ ЖЕ ПРОГОНОМ.") print("") print("НУЛЕВОЕ, И ОНО ПЕРЕПИСАЛО ОСТАЛЬНЫЕ. Две ошибки учёта.") print("Первая: одно закрытие может оказаться за тремя изломами сразу,") print("и каждый считался отдельным первым пробоем. Вторая: если геп") print("унёс цену дальше стопа, сделку брать нельзя, а расчёт брал") print("и писал ровно −1R. Выборка была 1064 сделки — это число") print("из прежней версии кода, пересчитать его нечем, — а стала") print(f"{base_n}. Срез «тестировали дважды» держался на 111 сделках") print("(тоже прежняя версия), теперь — см. ТРЕТЬЕ.") print("") print(f"ПЕРВОЕ. Базовое сочетание даёт {base_avg:+.3f}R на сделку") print(f"на {base_n} пробоях, погрешность {base_e:.3f} — это" f" {base_z:.1f} погрешности: {verdict(base_z)}.") print("") print("ВТОРОЕ. Видимость уровня: " f"{v('видимый уровень'):+.3f}R у шестидесятидневного экстремума") print(f"({k('видимый уровень')} сделок) против" f" {v('незаметный уровень'):+.3f}R у незаметного" f" ({k('незаметный уровень')}).") print(f"Разница {vis[0]:+.3f}R, погрешность {vis[1]:.3f} — это" f" {vis[2]:.1f} погрешности: {verdict(vis[2])}" + (", впритык." if 2 <= vis[2] < 2.5 else ".")) print("Сравнение внутреннее для выборки, но окно растущее — см. ШЕСТОЕ.") print("") print("ТРЕТЬЕ. «Тестированный дважды уровень пробивается лучше» —" f" {v('тестировали 2 раза и больше'):+.3f}R") print(f"на {k('тестировали 2 раза и больше')} сделках против" f" {v('уровень не тестировали'):+.3f}R у нетестированного" f" ({k('уровень не тестировали')}).") print(f"Разница {tst[0]:+.3f}R, погрешность {tst[1]:.3f} — это" f" {tst[2]:.1f} погрешности: {verdict(tst[2])}.") print("Плюс правка учёта не убрала — она убрала выборку: кластер") print("изломов — ровно то место, где одно закрытие проходит три") print("уровня разом, и прежний счёт записывал его трижды.") print("") grew = "УЦЕЛЕЛО" if out_half >= best_avg else "НЕ УЦЕЛЕЛО" print(f"ЧЕТВЁРТОЕ, И ОНО {grew}. Выбор настроек на первой половине") print(f"окна: {best_avg:+.3f}R там, где выбирали, и {out_half:+.3f}R") print(f"на второй половине ({out_half_n} сделок), которая в выборе") print("не участвовала.") print("") nx = dec.get(("вход", "next")) rt = dec.get(("вход", "retest")) if nx is not None and rt is not None: print("ПЯТОЕ, ПРОТИВ РАСХОЖЕГО СОВЕТА. Ждать возврата к уровню после" if rt < nx else "ПЯТОЕ. Ждать возврата к уровню после") print(f"пробоя {'хуже' if rt < nx else 'не хуже'}: вход на следующий" f" день {nx:+.3f}R, вход по ретесту {rt:+.3f}R (среднее по всем" " сочетаниям).") print(f"Широкий бар пробоя против обычного: {v('бар пробоя широкий'):+.3f}R" f" против {v('бар обычный'):+.3f}R, разница {wid[0]:+.3f}R,") print(f"погрешность {wid[1]:.3f} — это {wid[2]:.1f} погрешности:" f" {verdict(wid[2])}.") print("") up, dn = v("пробои вверх"), v("пробои вниз") lead = ("Весь плюс сидит в пробоях ВВЕРХ" if dn <= 0 < up else "Пробои вверх и вниз") print(f"ШЕСТОЕ. {lead}: {up:+.3f}R против") print(f"{dn:+.3f}R у пробоев вниз. Окно в основном растущее, и пробой") print("вверх идёт в сторону этого роста. Разделить сетап и тренд") print("этой выборкой невозможно — кроме одного места: видимость") print("уровня сравнивается ВНУТРИ каждого направления.") sd, se_ = keep["видимость при равном составе"] zu = gap("видимый, вверх", "незаметный, вверх")[2] zd = gap("видимый, вниз", "незаметный, вниз")[2] alone = ("по отдельности оба в пределах шума" if zu < 2 and zd < 2 else "по отдельности оба измерены" if zu >= 2 and zd >= 2 else "по отдельности измерено только " + ("вверх" if zu >= 2 else "вниз")) print(f"Видимый против незаметного: вверх" f" {v('видимый, вверх') - v('незаметный, вверх'):+.3f}R" f" ({zu:.1f} погр.), вниз" f" {v('видимый, вниз') - v('незаметный, вниз'):+.3f}R" f" ({zd:.1f} погр.) — {alone},") print(f"вместе при равном составе направлений {sd:+.3f}R ± {se_:.3f}" f" ({abs(sd) / se_:.1f} погрешности): {verdict(abs(sd) / se_)}.") return 0 def _run() -> int: """Прогон со снимком: что напечатано, то и легло в `results.txt`. Пока файл клали руками, он отставал от скрипта, и сценарий писался по устаревшим числам. Здесь отставать нечему. Снимок пишется только при удачном прогоне. Обрубок опаснее отсутствия: он выглядит как настоящий отчёт. """ import io real = sys.stdout for s in (real, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") buf = io.StringIO() class _Tee: """Поток, который пишет в два места и не спорит с вызовами скрипта.""" encoding = "utf-8" errors = "replace" def write(self, text): real.write(text) buf.write(text) return len(text) def flush(self): real.flush() def reconfigure(self, **kw): pass def isatty(self): return False sys.stdout = _Tee() try: code = main() finally: sys.stdout = real if code == 0: out = Path(__file__).with_name("results.txt") out.write_text(buf.getvalue(), encoding="utf-8", newline="\n") print(f"\nснимок: {out}") return code if __name__ == "__main__": raise SystemExit(_run())