#!/usr/bin/env python3 """Все числа TW-033 одним прогоном. Тема Y-01: что на экране до открытия. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ВОПРОС. «Подготовка до открытия решает больше, чем решения внутри сессии» — так это обычно подают. Подготовка состоит из того, что ВИДНО ЗАРАНЕЕ: вчерашний ход, геп, место цены в диапазоне, вчерашнее направление, день недели. Здесь проверяется, что из этого о сегодняшнем дне что-то говорит. РАЗДЕЛЕНИЕ, КОТОРОЕ РЕШАЕТ ВСЁ. У дня есть РАЗМЕР и НАПРАВЛЕНИЕ, и это два разных вопроса. Ответ на них может быть разным, и обычно он разный — поэтому меряются они отдельно, а не одной «предсказуемостью». ЧТО СЧИТАЕТСЯ. Для каждого дня берутся признаки, известные ДО его открытия: ВЧЕРАШНИЙ ХОД: вчерашний размах в дневных ходах (дневной ход — медиана 20 прошлых сессий); ГЕП: открытие сегодня против вчерашнего закрытия; МЕСТО В ПОЛОСЕ: где вчерашнее закрытие внутри двадцатидневной полосы; ВЧЕРАШНИЙ ЗНАК: вчера закрылись выше или ниже позавчерашнего; ДЕНЬ НЕДЕЛИ. И два ответа на сегодня: РАЗМЕР — сегодняшний размах в дневных ходах; НАПРАВЛЕНИЕ — закрытие выше или ниже открытия. ЧЕСТНОСТЬ. Ни один признак не подсматривает сегодняшний день дальше открытия. Геп известен на открытии — это единственное, что берётся из сегодняшнего бара, и оно названо вслух. Каждое утверждение проверяется на второй половине окна, которая в выборе не участвовала. ДАННЫЕ. `04_assets/market/*-1d.json`, шесть инструментов, общее окно. """ from __future__ import annotations import json import random import statistics as st import sys from datetime import date 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"] BAND = 20 BOOT = 400 # розыгрышей для погрешности разницы медиан WINDOW, STEP = 250, 25 # упражнение из ролика: окно в год торговли def boot_err(lo: list[dict], hi: list[dict], seed: int) -> float: """Погрешность разницы медиан размера — бутстрэп с постоянным зерном. ЗАЧЕМ (B-34 второго аудита). Медиана шести разниц «+0.07» прятала устойчивый эффект на одном инструменте: у фонда −0.39 на обеих половинах. Без погрешности по каждому инструменту «почти ничего» и «по-разному на разных инструментах» не отличить. """ rng = random.Random(seed) a, b = [x["size"] for x in lo], [x["size"] for x in hi] ds = [] for _ in range(BOOT): ra = [a[rng.randrange(len(a))] for _ in a] rb = [b[rng.randrange(len(b))] for _ in b] ds.append(st.median(rb) - st.median(ra)) return st.pstdev(ds) 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]) -> float: return st.median([b["h"] - b["l"] for b in r]) def rows(r: list[dict]) -> list[dict]: """Признаки, известные до открытия, и два ответа на сегодня.""" out = [] for i in range(BAND + 1, len(r)): y, t = r[i - 1], r[i] w = r[i - BAND:i] lo = min(b["l"] for b in w) hi = max(b["h"] for b in w) width = hi - lo if width <= 0 or y["c"] <= 0: continue out.append({ "d": t["d"][:10], # --- известно до открытия "prev_range": (y["h"] - y["l"]) / adr(w), "place": (y["c"] - lo) / width, # 0 — у низа, 1 — у верха "prev_up": y["c"] > r[i - 2]["c"], "dow": date.fromisoformat(t["d"][:10]).weekday(), # --- известно на открытии "gap": abs(t["o"] - y["c"]) / adr(w), "gap_up": t["o"] > y["c"], # --- ответы "size": (t["h"] - t["l"]) / adr(w), "up": t["c"] > t["o"], }) return out def split(data: list[dict], key, cut) -> tuple[list[dict], list[dict]]: return ([x for x in data if key(x) <= cut], [x for x in data if key(x) > cut]) def size_of(g: list[dict]) -> float: return st.median([x["size"] for x in g]) if g else float("nan") def up_of(g: list[dict]) -> float: return (sum(1 for x in g if x["up"]) / len(g) * 100) if g else float("nan") 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) data = {} raw = {n: bars(n) for n in NAMES} start = min(r[0]["d"][:10] for n, r in raw.items() if "USDT" in n) raw = {n: [b for b in r if b["d"][:10] >= start] for n, r in raw.items()} for n, r in raw.items(): data[n] = rows(r) say("ДАННЫЕ") say(f" Общее окно с {start}. Шесть инструментов, дневные бары.") for n in NAMES: say(f" {n:9} {len(data[n])} дней с полным набором признаков") say("") say("ВСЕ ПРИЗНАКИ ИЗВЕСТНЫ ДО ОТКРЫТИЯ. Единственное исключение — геп:") say("он известен В МОМЕНТ открытия, и это сказано вслух, а не спрятано.") TESTS = [ ("вчерашний ход больше обычного", lambda x: x["prev_range"], 1.0), ("геп больше четверти хода", lambda x: x["gap"], 0.25), ("цена в верхней половине полосы", lambda x: x["place"], 0.5), ] say("") say("1 · РАЗМЕР СЕГОДНЯШНЕГО ДНЯ") say(" Медиана сегодняшнего размаха в дневных ходах инструмента.") for label, key, cut in TESTS: say("") say(f" {label}") diffs = [] for j, n in enumerate(NAMES): lo, hi = split(data[n], key, cut) if len(lo) < 30 or len(hi) < 30: say(f" {n:9} мало дней") continue d = size_of(hi) - size_of(lo) diffs.append(d) e = boot_err(lo, hi, seed=1000 * len(label) + j) mid = len(data[n]) // 2 halves = [] for half in (data[n][:mid], data[n][mid:]): hl, hh = split(half, key, cut) halves.append(size_of(hh) - size_of(hl) if len(hl) >= 20 and len(hh) >= 20 else float("nan")) say(f" {n:9} нет {size_of(lo):.2f} да {size_of(hi):.2f} " f"разница {d:+.2f} ± {e:.2f} ({abs(d) / e:.1f} погр.) " f"половины {halves[0]:+.2f} / {halves[1]:+.2f}") if diffs: say(f" медиана разницы: {st.median(diffs):+.2f} дневного хода") say("") say("2 · НАПРАВЛЕНИЕ СЕГОДНЯШНЕГО ДНЯ") say(" Доля дней, закрывшихся выше своего открытия.") base = [] for n in NAMES: base.append(up_of(data[n])) say(f" база: от {min(base):.0f}% до {max(base):.0f}%, " f"медиана {st.median(base):.0f}%") for label, key, cut in TESTS + [ ("вчера закрылись выше", lambda x: 1 if x["prev_up"] else 0, 0), ("геп вверх", lambda x: 1 if x["gap_up"] else 0, 0)]: say("") say(f" {label}") diffs = [] for n in NAMES: lo, hi = split(data[n], key, cut) if len(lo) < 30 or len(hi) < 30: say(f" {n:9} мало дней") continue d = up_of(hi) - up_of(lo) diffs.append(d) say(f" {n:9} нет {up_of(lo):.0f}% да {up_of(hi):.0f}% " f"разница {d:+.0f} п.п.") if diffs: say(f" медиана разницы: {st.median(diffs):+.1f} п.п.") say("") say("3 · ДЕНЬ НЕДЕЛИ") say(" Тот же вопрос, но признак, который не про рынок вовсе.") names = ["понедельник", "вторник", "среда", "четверг", "пятница", "суббота", "воскресенье"] for k in range(5): sizes, ups = [], [] for n in NAMES: g = [x for x in data[n] if x["dow"] == k] if len(g) >= 30: sizes.append(size_of(g)) ups.append(up_of(g)) if sizes: say(f" {names[k]:12} размер {st.median(sizes):.2f} " f"вверх {st.median(ups):.0f}%") # ВЫХОДНЫЕ ЕСТЬ ТОЛЬКО У МОНЕТ — И ДЛЯ РАЗМЕРА ОНИ НЕ ПУСТЫЕ (B-33). # Строки выше — будни на всех шести; выходные раньше молча выпадали # из печати, а голос говорил «рынок не знает, какой сегодня день». # Для направления это так, для размера — нет: суббота у монет # примерно вдвое меньше будня. say(" монеты отдельно (у акций выходных нет):") for k in range(7): sizes, ups = [], [] for n in NAMES: if "USDT" not in n: continue g = [x for x in data[n] if x["dow"] == k] if len(g) >= 30: sizes.append(size_of(g)) ups.append(up_of(g)) if sizes: say(f" {names[k]:12} размер {min(sizes):.2f}–{max(sizes):.2f} " f"вверх {min(ups):.0f}–{max(ups):.0f}%") say("") say("4 · ПРОВЕРКА НА ВТОРОЙ ПОЛОВИНЕ ОКНА") say(" Признак, который держится только на одной половине, — не признак.") for label, key, cut in TESTS: first, second = [], [] for n in NAMES: mid = len(data[n]) // 2 for half, box in ((data[n][:mid], first), (data[n][mid:], second)): lo, hi = split(half, key, cut) if len(lo) >= 20 and len(hi) >= 20: box.append(size_of(hi) - size_of(lo)) if first and second: say(f" {label:34} первая {st.median(first):+.2f} " f"вторая {st.median(second):+.2f}") # УПРАЖНЕНИЕ РОЛИКА — ПРОВЕРЕНО НА ТЕХ ЖЕ ДАННЫХ (B-34). Прежнее # упражнение смотрело на три дня, а три дня эффект почти не различают: # «все три больше медианы» выходило в четверти списков на настоящих # данных и в каждом десятом на перемешанных. Новое: год торговли, # норма — двадцать сессий ДО дня, медиана дней после большого против # медианы дней после тихого. say("") say("5 · УПРАЖНЕНИЕ: ГОД ТОРГОВЛИ, ПОСЛЕ БОЛЬШОГО ДНЯ ПРОТИВ ПОСЛЕ ТИХОГО") say(f" Окна по {WINDOW} сессий с шагом {STEP}, норма — {BAND} сессий до дня.") pos = tot = 0 for n in NAMES: p_n = t_n = 0 for s0 in range(0, len(data[n]) - WINDOW + 1, STEP): w = data[n][s0:s0 + WINDOW] lo, hi = split(w, lambda x: x["prev_range"], 1.0) if len(lo) < 20 or len(hi) < 20: continue t_n += 1 p_n += size_of(hi) > size_of(lo) pos += p_n tot += t_n say(f" {n:9} после большого больше в {p_n} окнах из {t_n}") say(f" всего: в {pos} окнах из {tot}") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())