#!/usr/bin/env python3 """Все числа TW-022 одним прогоном. Тема L-04: уровни из изломов и широких баров. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ЧТО ПРОВЕРЯЕТСЯ. Два самых распространённых основания уровня: ИЗЛОМ — бар, у которого максимум выше соседей слева и справа (или минимум ниже). Отсюда рисуют горизонталь и ждут, что цена её заметит. ШИРОКИЙ БАР — день с ходом заметно больше обычного. Считается, что его край оставляет след: там кто-то большой действовал, и он вернётся. Оба утверждения проверяются одинаково и в лоб: сколько раз цена вернулась к отметке, что произошло при первом возврате, и — главное — чем это отличается от СЛУЧАЙНОЙ горизонтали в том же месте графика. БЕЗ КОНТРОЛЬНОЙ ГРУППЫ ЗДЕСЬ НЕЛЬЗЯ. Цена весь день движется, поэтому к любой линии она рано или поздно приходит, и «уровень сработал» звучит убедительно всегда. Контроль — случайная цена внутри того же диапазона за тот же период. Если уровень не отличается от неё, он ничего не добавил. ЧТО СЧИТАЕТСЯ РЕАКЦИЕЙ. Цена подошла к отметке ближе чем на четверть обычного дневного хода и в течение пяти дней отошла от неё не меньше чем на половину обычного дня в сторону, откуда пришла. Это не «сигнал», а измеримое событие: подошла — оттолкнулась. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов целиком. """ from __future__ import annotations import json import random import statistics as st import sys from pathlib import Path STUDIO = Path(__file__).resolve().parents[3] MARKET = STUDIO / "tickwright" / "04_assets" / "market" NAMES = ["AAPL", "TSLA", "SPY", "BTCUSDT", "ETHUSDT", "SOLUSDT"] WING = 2 # сколько баров с каждой стороны должно быть ниже/выше WIDE = 1.8 # «широкий бар» — во столько раз больше обычного дня NEAR = 0.25 # подход: ближе четверти обычного дня ROLL = 60 # обычный ход: медиана high−low за 60 баров до бара AWAY = 0.5 # отскок: полдня в обратную сторону AHEAD = 5 # за сколько дней должен случиться отскок SEED = 20260909 # контрольная группа воспроизводима 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 def adr(r: list[dict]) -> list[float]: """Обычный дневной ход НА КАЖДЫЙ БАР: медиана high−low за ROLL баров ДО него. Одна медиана в долларах за весь ряд мерила годы, а не дни: цена Apple выросла в пять с половиной раз, и «широких» баров выходило 0 % в 2019-м и 60 % в 2026-м, а в разметку TW-025 попадало спрятанное будущее. Первые 20 баров ряда берут медиану первых двадцати. """ return [st.median([b["h"] - b["l"] for b in # без будущего (B-47): первые 20 баров — по тому, что было до них (r[max(0, i - ROLL):i] if i >= 20 else (r[:i] or r[:1]))]) for i in range(len(r))] def turns(r: list[dict]) -> list[tuple[int, float, str]]: """Изломы: максимум выше WING соседей с каждой стороны, минимум — ниже.""" out = [] for i in range(WING, len(r) - WING): left = r[i - WING:i] right = r[i + 1:i + 1 + WING] if all(r[i]["h"] > b["h"] for b in left + right): out.append((i, r[i]["h"], "high")) if all(r[i]["l"] < b["l"] for b in left + right): out.append((i, r[i]["l"], "low")) return out def wides(r: list[dict], a: float) -> list[tuple[int, float, str]]: """Края аномально широких баров.""" out = [] for i, b in enumerate(r): if b["h"] - b["l"] > WIDE * a[i]: out.append((i, b["h"], "high")) out.append((i, b["l"], "low")) return out def shifted(marks: list[tuple[int, float, str]], a: float, rng: random.Random) -> list[tuple[int, float, str]]: """Тот же излом, но отметка сдвинута на 0,5–1,5 обычного дня. ЭТО ГЛАВНЫЙ КОНТРОЛЬ, и он важнее случайной линии. Излом стоит на краю диапазона, и цена, возвращаясь к нему, приходит с одной стороны — «отскок» ей засчитывается почти даром. Случайная линия внутри диапазона такого преимущества не имеет, поэтому сравнение с ней завышает эффект уровня и меряет, по сути, «край против середины». Сдвинутая отметка сохраняет край и теряет только точность цены. Если отскок от неё такой же, значит работает РАЙОН, а не линия, — и тогда рисовать её с точностью до цента бессмысленно. """ out = [] for i, price, kind in marks: step = rng.uniform(0.5, 1.5) * a[i] * rng.choice([1, -1]) out.append((i, price + step, kind)) return out def controls(r: list[dict], n: int, rng: random.Random) -> list[tuple[int, float, str]]: """Случайные горизонтали: та же дата, цена из диапазона соседних 40 дней. Именно так контроль и должен быть устроен: событие берётся в том же месте графика и в том же ценовом окне, отличается только основание — у уровня оно есть, у контроля нет. """ out = [] for _ in range(n): i = rng.randrange(WING, max(len(r) - AHEAD - 1, WING + 1)) window = r[max(0, i - 40):i + 1] lo = min(b["l"] for b in window) hi = max(b["h"] for b in window) price = rng.uniform(lo, hi) out.append((i, price, rng.choice(["high", "low"]))) return out def reaction(r: list[dict], marks: list[tuple[int, float, str]], a: float, away: float | None = None, wait: int = 0) -> dict: """Сколько отметок дождались подхода и сколько из них дали отскок. ОДИН ДЕНЬ — ОДНО КАСАНИЕ В СТОРОНУ. Один бар может задеть три отметки подряд, и каждая записалась бы отдельным наблюдением с почти одинаковым исходом. Здесь это важно вдвойне: дедупликация применяется одинаково и к изломам, и к контролю, иначе сравнение поедет. Найдено 10 сентября 2026 на расчёте TW-030 и заведено во все расчёты канала, где событие привязано к уровню. """ seen: dict[tuple[int, str], tuple[float, bool]] = {} for i, price, kind in marks: # допуск касания — четверть обычного хода на день касания: a[j] hit = None # ИЗЛОМ ИЗВЕСТЕН ТОЛЬКО ЧЕРЕЗ WING БАРОВ (9 октября 2026). Его # определяют соседи справа, и до закрытия бара i + WING уровня ещё нет. # Поиск касания с бара i + 1 засчитывал как раз эти бары: у максимума они # по определению ниже его, часто в пределах допуска, и цена после них уже # уходит прочь — отскок им дарило само определение излома. В расчёте # TW-023 так легли 828 из 1701 первых касаний (49 %), и «отскакивали» они # в 97 % случаев против 81 % у остальных. Торговать их нельзя. # Изломы и их двойники передают wait=WING; край широкого бара известен # на закрытии самого бара, случайная линия — на своей дате: у них 0. for j in range(i + 1 + wait, min(i + 1 + 120, len(r))): if r[j]["l"] - NEAR * a[j] <= price <= r[j]["h"] + NEAR * a[j]: hit = j break if hit is None: continue end = min(hit + AHEAD, len(r) - 1) seg = r[hit + 1:end + 1] if not seg: continue away_ = AWAY if away is None else away if kind == "high": back = min(b["l"] for b in seg) <= price - away_ * a[hit] else: back = max(b["h"] for b in seg) >= price + away_ * a[hit] key = (hit, kind) # ВЧЕРАШНЕЕ ЗАКРЫТИЕ, А НЕ ЗАКРЫТИЕ ДНЯ КАСАНИЯ (B-47, 9 октября 2026). # При входе по касанию закрытия этого дня ещё нет, а выбор по нему # сдвигал результат: у TW-027 вход по касанию со стопом в ход и трейлом # −0.028R → −0.063R, у TW-034 вилка 0.144R → 0.203R. Выбор по открытию # и по вчерашнему закрытию дают одно и то же — выбросом было закрытие. dist = abs(r[hit - 1]["c"] - price) if key not in seen or dist < seen[key][0]: seen[key] = (dist, back) touched = len(seen) bounced = sum(1 for _, back in seen.values() if back) return {"marks": len(marks), "touched": touched, "bounce": bounced / touched * 100 if touched else 0.0} TWINS = 100 # сколько раз разыгрывается сдвиг двойника (A-15) def twin(r: list[dict], marks, a, away: float | None = None, wait: int = 0) -> tuple[float, float]: """-> (средняя доля отскоков сдвинутого двойника по TWINS розыгрышам, разброс между розыгрышами). ОДИН РОЗЫГРЫШ — ОДНО ЧИСЛО ИЗ МНОГИХ (A-15 второго аудита). Сдвиг берётся случайно — полтора дня вверх или полдня вниз, — и «излом выиграл на всех шести» держалось на одной такой выборке: у индексного фонда новый розыгрыш клал разрыв по любую сторону нуля. """ vals = [reaction(r, shifted(marks, a, random.Random(SEED + 1 + k)), a, away, wait)["bounce"] for k in range(TWINS)] return st.mean(vals), st.pstdev(vals) def gap_err(p1: float, n1: int, p2: float, n2: int) -> float: """Погрешность разницы двух долей, в пунктах (без учёта парности — то есть с запасом: двойник сделан из тех же отметок).""" e1 = (p1 / 100 * (1 - p1 / 100) / n1) ** 0.5 * 100 if n1 else 0.0 e2 = (p2 / 100 * (1 - p2 / 100) / n2) ** 0.5 * 100 if n2 else 0.0 return (e1 * e1 + e2 * e2) ** 0.5 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 NAMES} print("ДАННЫЕ") for n, r in rows.items(): print(f" {n:9} {len(r)} баров {r[0]['d']} … {r[-1]['d']}") print(f"\n1 · СКОЛЬКО ТАКИХ ОСНОВАНИЙ НА ГРАФИКЕ") print(f" Излом: экстремум выше (ниже) {WING} соседей с каждой стороны.") print(f" Широкий бар: ход больше {WIDE:.1f} обычного дня.") print(f" {'':11}{'изломов':>10}{'широких баров':>16}{'на 100 дней':>14}") dens = [] for n, r in rows.items(): a = adr(r) t, w = turns(r), wides(r, a) dens.append((len(t) + len(w)) / len(r) * 100) print(f" {n:9} {len(t):9} {len(w) // 2:15} " f"{(len(t) + len(w)) / len(r) * 100:13.0f}") print(" Примерно одно основание на два дня. Поэтому вопрос не «есть ли уровень»,") print(" а «чем он отличается от любой другой линии».") # 1b · ПОЧЕМУ СЧЁТ НАЧИНАЕТСЯ ЧЕРЕЗ WING БАРОВ (9 октября 2026). Прежде # касание искали с бара i + 1, и половина первых касаний ложилась на # два бара, которые сами определяют излом. Раздел печатает, сколько их # было и как они «отскакивали», — чтобы фраза ролика об этом опиралась # на прогон, а не на память о прежней версии расчёта. print("") print(f"1b · КАСАНИЯ ДО ТОГО, КАК ИЗЛОМ СУЩЕСТВУЕТ (первые {WING} бара после него)") print(f" {'':11}{'первых касаний':>16}{'в окне':>9}{'отскок в окне':>15}" f"{'отскок после':>14}") tot_in = tot_in_b = tot_out = tot_out_b = 0 for n, r in rows.items(): a = adr(r) first: dict[tuple[int, str], tuple[float, int, bool]] = {} for i, price, kind in turns(r): hit = None for j in range(i + 1, min(i + 1 + 120, len(r))): if r[j]["l"] - NEAR * a[j] <= price <= r[j]["h"] + NEAR * a[j]: hit = j break if hit is None: continue seg = r[hit + 1:min(hit + AHEAD, len(r) - 1) + 1] if not seg: continue back = (min(b["l"] for b in seg) <= price - AWAY * a[hit] if kind == "high" else max(b["h"] for b in seg) >= price + AWAY * a[hit]) key = (hit, kind) # ВЧЕРАШНЕЕ ЗАКРЫТИЕ, А НЕ ЗАКРЫТИЕ ДНЯ КАСАНИЯ (B-47, 9 октября 2026). # При входе по касанию закрытия этого дня ещё нет, а выбор по нему # сдвигал результат: у TW-027 вход по касанию со стопом в ход и трейлом # −0.028R → −0.063R, у TW-034 вилка 0.144R → 0.203R. Выбор по открытию # и по вчерашнему закрытию дают одно и то же — выбросом было закрытие. dist = abs(r[hit - 1]["c"] - price) if key not in first or dist < first[key][0]: first[key] = (dist, hit - i, back) vals = list(first.values()) inn = [b for _, age, b in vals if age <= WING] out = [b for _, age, b in vals if age > WING] tot_in += len(inn) tot_in_b += sum(inn) tot_out += len(out) tot_out_b += sum(out) print(f" {n:9} {len(vals):15} {len(inn) / len(vals) * 100:7.0f}% " f"{sum(inn) / max(len(inn), 1) * 100:13.0f}% " f"{sum(out) / max(len(out), 1) * 100:12.0f}%") print(f" → все шесть: в окне {tot_in} из {tot_in + tot_out} " f"({tot_in / (tot_in + tot_out) * 100:.0f}%), отскок в окне " f"{tot_in_b / tot_in * 100:.0f}% против {tot_out_b / tot_out * 100:.0f}% после.") print(" Торговать касание в окне нельзя: излома ещё нет. Дальше") print(f" касания изломов ищутся с бара i + 1 + {WING}.") print(f"\n2 · ЧТО ПРОИСХОДИТ ПРИ ВОЗВРАТЕ К ОТМЕТКЕ") print(f" Подход — ближе {NEAR:.2f} обычного дня, отскок — " f"{AWAY:.1f} дня за {AHEAD} дней.") print(f" {'':11}{'излом':>9}{'сдвинутый':>12}{'случайная':>12}" f"{'излом−сдвинутый':>18}") # РАЗНИЦА ПОМНИТ СВОЙ ИНСТРУМЕНТ. Вывод внизу утверждает, # что на двух конкретных бумагах эффект в пределах шума; # без имён это утверждение нечем пересчитать. gaps_t, gaps_rnd, by_name, errs = [], [], {}, {} for n, r in rows.items(): a = adr(r) rng = random.Random(SEED) t = reaction(r, turns(r), a, wait=WING) shifted(turns(r), a, rng) # тот же расход rng, что и раньше: c = reaction(r, controls(r, 400, rng), a) # контроль не сдвигается sh_mean, sh_sd = twin(r, turns(r), a, wait=WING) g = t["bounce"] - sh_mean e = gap_err(t["bounce"], t["touched"], sh_mean, t["touched"]) gaps_t.append(g) gaps_rnd.append(t["bounce"] - c["bounce"]) by_name[n] = g errs[n] = e print(f" {n:9} {t['bounce']:8.1f}% {sh_mean:11.1f}% " f"{c['bounce']:11.1f}% {g:+17.1f} ±{e:.1f}" f" (розыгрыши двойника ±{sh_sd:.1f})") print(f" → излом против СДВИНУТОГО излома: от {min(gaps_t):+.1f} " f"до {max(gaps_t):+.1f} п.п., медиана {st.median(gaps_t):+.1f}") # ПО ШЕСТИ ВМЕСТЕ (9 октября 2026). Разница каждого инструмента # складывается с весами по точности — так же, как в расчёте TW-023. w2 = [1 / errs[n] ** 2 for n in by_name] pooled = sum(by_name[n] * w_ for n, w_ in zip(by_name, w2)) / sum(w2) pooled_e = sum(w2) ** -0.5 print(f" → по шести вместе, с весами по точности: {pooled:+.1f} ± " f"{pooled_e:.1f} п.п. — " f"{'ПОМЕРЕНА' if abs(pooled) >= 2 * pooled_e else 'не померена'}" f" ({abs(pooled) / pooled_e:.1f} погр.)") print(f" → излом против случайной линии: медиана " f"{st.median(gaps_rnd):+.1f} п.п. — но эта разница меряет") print(" «край против середины», а не «уровень против не-уровня»") print("\n3 · ШИРОКИЙ БАР: ОБА КРАЯ И ТОТ ЖЕ КОНТРОЛЬ") print(" Широкий бар даёт две отметки: свой максимум и свой минимум.") print(f" {'':11}{'верх':>9}{'низ':>9}{'сдвинутый':>12}{'разница':>10}") gaps_w = [] for n, r in rows.items(): a = adr(r) rng = random.Random(SEED) w = wides(r, a) up = reaction(r, [m for m in w if m[2] == "high"], a) dn = reaction(r, [m for m in w if m[2] == "low"], a) both = reaction(r, w, a) sh_mean, _ = twin(r, w, a) gaps_w.append(both["bounce"] - sh_mean) print(f" {n:9} {up['bounce']:8.1f}% {dn['bounce']:8.1f}% " f"{sh_mean:11.1f}% {both['bounce'] - sh_mean:+9.1f}") print(f" → широкий бар против сдвинутого: от {min(gaps_w):+.1f} " f"до {max(gaps_w):+.1f} п.п., медиана {st.median(gaps_w):+.1f}") # КАЖДЫЙ КРАЙ — ПРОТИВ СВОЕГО ДВОЙНИКА (A-14 второго аудита). «Нижний # край отскакивает чаще верхнего» может быть просто направлением рынка # за окно: если сдвинутые нижние отметки тоже отскакивают чаще # сдвинутых верхних, перекос принадлежит выборке, а не краю. print("\n3b · ВЕРХ И НИЗ ПРОТИВ СВОИХ СДВИНУТЫХ ДВОЙНИКОВ") print(f" {'':11}{'низ':>8}{'двойник':>9}{'верх':>8}{'двойник':>9}" f"{'низ−дв.':>9}{'верх−дв.':>10}") sh_low_wins = low_beats = 0 for n, r in rows.items(): a = adr(r) w = wides(r, a) lows = [m for m in w if m[2] == "low"] highs = [m for m in w if m[2] == "high"] dn = reaction(r, lows, a)["bounce"] up = reaction(r, highs, a)["bounce"] tl, _ = twin(r, lows, a) th, _ = twin(r, highs, a) sh_low_wins += tl > th low_beats += (dn - tl) > (up - th) print(f" {n:9} {dn:7.1f}% {tl:8.1f}% {up:7.1f}% {th:8.1f}% " f"{dn - tl:+8.1f} {up - th:+9.1f}") print(f" → сдвинутый низ отскакивает чаще сдвинутого верха на " f"{sh_low_wins} из {len(rows)}") print(f" → против своего двойника низ лучше верха на {low_beats} " f"из {len(rows)}") # ПОРОГ ОТСКОКА — ВЫБОР, И ВЫВОД ПРОВЕРЯЕТСЯ НА ТРЁХ (A-16). Голос # говорил «разрыв не меняется»; здесь видно, как он меняется. print("\n2b · РАЗРЫВ ИЗЛОМ − ДВОЙНИК ПРИ РАЗНЫХ ПОРОГАХ ОТСКОКА") for away in (0.25, 0.5, 1.0): gs = [] for n, r in rows.items(): a = adr(r) tb = reaction(r, turns(r), a, away, wait=WING)["bounce"] sb, _ = twin(r, turns(r), a, away, wait=WING) gs.append(tb - sb) print(f" порог {away:4.2f} дня: медиана {st.median(gs):+.1f} п.п., " f"от {min(gs):+.1f} до {max(gs):+.1f}") # ТОЧНОСТЬ ОТМЕТКИ — СДВИГ НА ФИКСИРОВАННУЮ ДОЛЮ ДНЯ (A-31 второго # аудита). Голос s46 говорил «на полхода — и большей части эффекта нет», # а двойник выше сдвигается на случайные 0,5–1,5 дня и про полхода # ничего не говорит. Здесь двойник стоит ровно на d хода от излома # (знак случайный), и доля эффекта, ушедшая на d, — это разрыв # «излом − двойник(d)» против разрыва на дальнем двойнике (1,5 дня). print("\n2c · ТОЧНОСТЬ ОТМЕТКИ: ДВОЙНИК РОВНО НА d ОБЫЧНОГО ДНЯ") fixed: dict[float, list[float]] = {} pooled_d: dict[float, tuple[float, float]] = {} for d in (0.25, 0.5, 1.0, 1.5): gs = [] for n, r in rows.items(): a = adr(r) tb = reaction(r, turns(r), a, wait=WING)["bounce"] vals = [] for k in range(TWINS): rng_d = random.Random(SEED + 101 + k) moved = [(i, p + d * a[i] * rng_d.choice([1, -1]), kd) for i, p, kd in turns(r)] vals.append(reaction(r, moved, a, wait=WING)["bounce"]) gs.append(tb - st.mean(vals)) fixed[d] = gs wd = [1 / errs[n] ** 2 for n in rows] pd_ = sum(g * w_ for g, w_ in zip(gs, wd)) / sum(wd) pe_ = sum(wd) ** -0.5 pooled_d[d] = (pd_, pe_) print(f" d = {d:4.2f} дня: разрыв излом − двойник, медиана " f"{st.median(gs):+.1f} п.п. (от {min(gs):+.1f} до {max(gs):+.1f}); " f"по шести вместе {pd_:+.1f} ± {pe_:.1f} — " f"{'ПОМЕРЕН' if abs(pd_) >= 2 * pe_ else 'не померен'}") # ДОЛЯ ЭФФЕКТА ИМЕЕТ СМЫСЛ, ТОЛЬКО ЕСЛИ САМ ЭФФЕКТ ПОМЕРЕН. После правки # окна подтверждения излома (9 октября 2026) разрыв на 0,25 и 0,5 дня # стал отрицательным, и «ушло минус восемнадцать процентов эффекта» # было бы арифметикой над шумом. far, far_e = pooled_d[1.5] if abs(far) >= 2 * far_e: for d in (0.25, 0.5, 1.0): print(f" → на {d:4.2f} дня ушло {pooled_d[d][0] / far * 100:3.0f}% " f"эффекта, который виден на 1,5 дня") else: print(" → эффект на 1,5 дня сам не померен — делить на него нечего") print("\n4 · СКОЛЬКО ОТМЕТОК ВООБЩЕ ДОЖИДАЮТСЯ ВОЗВРАТА") print(" За 120 дней после появления:") for n, r in rows.items(): a = adr(r) t = reaction(r, turns(r), a, wait=WING) print(f" {n:9} изломов {t['marks']:5}, до цены дошло " f"{t['touched'] / t['marks'] * 100:5.1f}%") # ВЫВОД СЧИТАЕТСЯ. Прежде те же утверждения стояли словами — # «около шестидесяти», «плюс восемь с половиной», «на крипте # достигает четырнадцати». Таблицы менялись, слова нет. # ШУМ — ИЗМЕРЕННЫЙ, А НЕ НАЗНАЧЕННЫЙ (A-15). Было «меньше двух пунктов # — шум»; теперь разница считается различимой, если она не меньше двух # своих погрешностей. quiet = sorted(n for n, x in by_name.items() if abs(x) < 2 * errs[n]) loud = [n for n in by_name if n not in quiet] ok = abs(pooled) >= 2 * pooled_e print("") print("ВЫВОД, ПОСЧИТАННЫЙ ЭТИМ ЖЕ ПРОГОНОМ.") print("Основание на графике — примерно раз в два дня:") print(f"от {min(dens):.0f} до {max(dens):.0f} на сотню баров, медиана" f" {st.median(dens):.0f}.") print("Линию можно провести почти везде, и это первое, что нужно знать.") print("") print("Излом против того же излома, сдвинутого на 0,5–1,5 дневного хода:") print(f"по шести вместе {pooled:+.1f} ± {pooled_e:.1f} пункта — " + ("померено." if ok else "не померено, в пределах двух погрешностей.")) print(f"Медиана по инструментам {st.median(gaps_t):+.1f}, разброс от" f" {min(gaps_t):+.1f} до {max(gaps_t):+.1f}.") if loud: print(f"Больше двух своих погрешностей — только {', '.join(loud)};" f" у остальных {len(quiet)} разница в пределах шума.") else: print("Ни на одном инструменте разница не выходит за две погрешности.") near_d = [d for d in (0.25, 0.5) if abs(pooled_d[d][0]) < 2 * pooled_d[d][1]] if near_d: print("Сдвиг на " + " и ".join(f"{d:.2f}" for d in near_d) + " дня не стоит ничего измеримого.") print("") print(f"Сравнение со случайной горизонталью даёт медиану" f" {st.median(gaps_rnd):+.1f} пункта,") print("но эта разница меряет «край против середины», а не «уровень против") print("не-уровня»: экстремум выигрывает уже тем, что цена подходит к нему") print("с одной стороны.") print("") print(f"Широкий бар против своего двойника: медиана {st.median(gaps_w):+.1f}" f" пункта, у излома {st.median(gaps_t):+.1f}.") print("") if ok: print("Значит точная цена излома что-то добавляет, но основание даёт") print("КАНДИДАТА, а не сигнал.") else: print("Значит работает РАЙОН края, а не точная линия: отметка, сдвинутая") print("на полдня, отскакивает так же. Основание даёт КАНДИДАТА, а не сигнал.") print("Отбор между кандидатами — тема следующего ролика модуля.") 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())