#!/usr/bin/env python3 """Все числа TW-017 одним прогоном. Тема F-01: кто на самом деле двигает цену. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Канал обещает: всё измерено, а не заявлено. Обещание стоит ровно столько, сколько стоит возможность пересчитать. ЧТО ПРОВЕРЯЕТСЯ. Расхожее объяснение: на рынке две стороны, и движение возникает там, где одной из них ПРИШЛОСЬ действовать. Утверждение качественное, но из него следуют три проверяемых: 1. Если стороны в целом уравновешивают друг друга, почти всё пройденное движение должно взаимно гаситься. 2. Если цену двигает вынужденность, а не сама торговля, заметная часть движения должна случаться, когда торговать нельзя, — между закрытием и открытием. 3. Если движение приходит редкими эпизодами вынужденности, а не ровным потоком, небольшое число дней должно давать весь итог года. МЕРЫ. гашение — сумма дневных ходов против модуля чистого изменения цены за тот же период; вне сессии — |открытие минус вчерашнее закрытие| против |закрытие минус открытие|, суммарно за период. У круглосуточной крипты первое должно быть около нуля, и это встроенная проверка самой меры: если бы она врала, крипта дала бы не ноль; концентрация — что остаётся от чистого изменения года (со знаком), если убрать пять лучших дней. ДАННЫЕ. `04_assets/market/*-1d.json`, последние 250 сессий каждого инструмента — примерно год. """ from __future__ import annotations import json 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"] TAIL = 250 def bars(name: str) -> list[dict]: d = json.loads((MARKET / f"{name}-1d.json").read_text(encoding="utf-8")) rows = d["bars"] if isinstance(d, dict) and "bars" in d else d return rows[-TAIL:] def main() -> int: for s in (sys.stdout, sys.stderr): if hasattr(s, "reconfigure"): s.reconfigure(encoding="utf-8", errors="replace") print("ДАННЫЕ") for n in NAMES: r = bars(n) print(f" {n:9} {len(r)} сессий {r[0]['d']} … {r[-1]['d']}") print("\n1 · СКОЛЬКО ДВИЖЕНИЯ ГАСИТСЯ") print(f" {'':9} {'сумма ходов':>13} {'чистое':>11} {'гасится':>9}") for n in NAMES: r = bars(n) total = sum(b["h"] - b["l"] for b in r) net = abs(r[-1]["c"] - r[0]["c"]) print(f" {n:9} {total:13.1f} {net:11.1f} {(1 - net / total) * 100:8.1f}%") print("\n2 · СКОЛЬКО ДВИЖЕНИЯ СЛУЧАЕТСЯ, ПОКА ТОРГОВАТЬ НЕЛЬЗЯ") print(f" {'':9} {'вне сессии':>11} {'внутри':>10} {'доля вне':>10}") for n in NAMES: r = bars(n) over = sum(abs(r[i]["o"] - r[i - 1]["c"]) for i in range(1, len(r))) intra = sum(abs(b["c"] - b["o"]) for b in r) print(f" {n:9} {over:11.1f} {intra:10.1f} " f"{over / (over + intra) * 100:9.1f}%") print(" Крипта торгуется круглосуточно — у неё доля близка к нулю.") print(" Это встроенная проверка самой меры, а не результат.") print("\n3 · СКОЛЬКО ДНЕЙ ДАЮТ ВЕСЬ ГОД") for n in NAMES: r = bars(n) moves = sorted((r[i]['c'] - r[i - 1]['c'] for i in range(1, len(r))), reverse=True) net = r[-1]['c'] - r[0]['c'] rest = net - sum(moves[:5]) # Прежний счёт «сколько дней покрывают год» складывал МОДУЛИ # изменений и не смотрел на знак: у Tesla «один день» шёл # против итога года (аудит 3 октября 2026). Теперь — что # остаётся от итога, если убрать пять лучших дней. # print(f' {n:9} итог {net:+11.2f}, без пяти лучших дней {rest:+11.2f} ' f'— осталось {rest / net * 100:+.0f}% итога') # ЧИСЛО ДНЕВНЫХ ИЗМЕНЕНИЙ ПЕЧАТАЕТСЯ: голос s18 говорит «five out of two # hundred and forty nine» — у 250 сессий 249 изменений от закрытия к # закрытию, и до 8 октября этого числа в выводе не было. print(f" дневных изменений у каждого: {len(bars(NAMES[0])) - 1} " f"(из {len(bars(NAMES[0]))} сессий)") # МОНЕТКА С ТЕМИ ЖЕ РАЗМАХАМИ (A-52 второго аудита). Мера «без пяти # лучших дней» сама по себе не отличает рынок от шума: у случайного # блуждания с теми же дневными размахами пять лучших дней тоже велики # рядом с чистым итогом. Здесь — те же 249 изменений по модулю, знаки # разыграны монеткой, 2000 раз. Акции: у них итог положительный и мера # «лучших дней» читается прямо; у крипты год отрицательный. import random print("\n3b · ТО ЖЕ ДЛЯ МОНЕТКИ С ТЕМИ ЖЕ ДНЕВНЫМИ РАЗМАХАМИ") rng = random.Random(17) for n in NAMES[:3]: r = bars(n) ch = [r[i]['c'] - r[i - 1]['c'] for i in range(1, len(r))] absch = [abs(x) for x in ch] top5 = sum(sorted(ch, reverse=True)[:5]) net = r[-1]['c'] - r[0]['c'] nets, tops = [], [] for _ in range(2000): s = [a if rng.random() < 0.5 else -a for a in absch] nets.append(abs(sum(s))) tops.append(sum(sorted(s, reverse=True)[:5])) nets.sort() tops.sort() med_net, med_top = nets[1000], tops[1000] pct = sum(1 for t in tops if t < top5) / len(tops) * 100 print(f" {n:9} пять лучших дней: рынок {top5:7.2f}, монетка в медиане " f"{med_top:7.2f} (рынок на {pct:3.0f}-м перцентиле монетки)") print(f" итог: рынок {net:+8.2f}, монетка в медиане ±{med_net:7.2f}; " f"пять лучших дней монетки — {med_top / med_net * 100:3.0f}% её " f"типичного итога") # ПРИМЕР ДЛЯ КАДРОВ s21–s23 — выбран ПРАВИЛОМ: самый большой ночной # разрыв Tesla за год. Голос называет его цены (374.01, 341, 319.69), # и до 8 октября ни одной из них в выводе не было. r = bars("TSLA") i = max(range(1, len(r)), key=lambda k: abs(r[k]["o"] - r[k - 1]["c"])) print("\nПРИМЕР ДЛЯ КАДРОВ s21–s23 — самый большой ночной разрыв TSLA") print(f" закрытие {r[i - 1]['d']}: {r[i - 1]['c']:.2f}") print(f" открытие {r[i]['d']}: {r[i]['o']:.2f} " f"(разрыв {r[i]['o'] - r[i - 1]['c']:+.2f})") print(f" закрытие {r[i]['d']}: {r[i]['c']:.2f} " f"(за день {r[i]['c'] - r[i - 1]['c']:+.2f})") print("\nВЫВОД. Почти всё, что рынок проходит, взаимно уничтожается:") print("две стороны в целом уравновешены. Остаётся тонкий остаток, и он") 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())