#!/usr/bin/env python3 """Все числа TW-042 одним прогоном. Финал курса: что выдержало проверку. ЗАЧЕМ ЭТОТ ФАЙЛ ЛЕЖИТ В ПРОЕКТЕ. Финал повторяет числа десяти уроков, и повторить их можно только одним способом — взять из самих уроков. Здесь ничего не считается заново: каждое число СОБИРАЕТСЯ из снимка `results.txt` своего урока, и только из свежего — снимок не старше своего скрипта и общего учёта (`tw_accounting.py`), если скрипт им пользуется. Устаревший снимок останавливает расчёт: финал не имеет права пересказывать прошлую версию чужого урока. КАЖДОЕ УТВЕРЖДЕНИЕ ПРОВЕРЯЕТСЯ, А НЕ ТОЛЬКО ЦИТИРУЕТСЯ. У каждого из десяти есть условие, при котором оно перестаёт быть правдой («размер переносится, если после большого дня ход хотя бы вдвое больше, чем после тихого»). Пересчитали урок и условие не выполнено — расчёт падает с объяснением, а не печатает старую формулировку над новыми числами. ИСТОЧНИКИ: TW-014, TW-015, TW-018, TW-019, TW-020, TW-022, TW-026, TW-027, TW-040 — снимки их этюдов. """ from __future__ import annotations import re import sys from pathlib import Path HERE = Path(__file__).resolve().parent ASSETS = HERE.parent OUT = HERE / "results.txt" ACCOUNTING = HERE.parents[2] / "shared" / "pipeline" / "tw_accounting.py" lines: list[str] = [] def say(s: str = "") -> None: print(s) lines.append(s) def fresh(study: str, script: str) -> str: """Текст снимка этюда — только если он не старше своего скрипта.""" d = ASSETS / f"{study}-study" res, src = d / "results.txt", d / script t_res = res.stat().st_mtime uses_acc = "tw_accounting" in src.read_text(encoding="utf-8") older = [p.name for p in ([src] + ([ACCOUNTING] if uses_acc else [])) if p.stat().st_mtime > t_res] if older: sys.exit(f"Снимок {study} старше {', '.join(older)} — перезапустить {script} и повторить.") return res.read_text(encoding="utf-8") def grab(text: str, pattern: str, study: str, flags: int = re.M) -> re.Match: m = re.search(pattern, text, flags) if not m: sys.exit(f"В снимке {study} не нашлась строка по образцу «{pattern}» — снимок изменил форму.") return m def holds(cond: bool, claim: str, why: str) -> None: if not cond: sys.exit(f"УТВЕРЖДЕНИЕ «{claim}» БОЛЬШЕ НЕ ДЕРЖИТСЯ: {why}. Финал нужно переписать.") def span(s: str) -> tuple[float, float]: a, _, b = s.replace("–", "-").partition("-") return float(a), float(b or a) def main() -> int: for s_ in (sys.stdout, sys.stderr): if hasattr(s_, "reconfigure"): s_.reconfigure(encoding="utf-8", errors="replace") src = { "TW-014": fresh("TW-014", "room.py"), "TW-015": fresh("TW-015", "costs.py"), "TW-018": fresh("TW-018", "readbar.py"), "TW-019": fresh("TW-019", "twomarkets.py"), "TW-020": fresh("TW-020", "leverage.py"), "TW-022": fresh("TW-022", "turns.py"), "TW-026": fresh("TW-026", "samesetup.py"), "TW-027": fresh("TW-027", "bounce.py"), "TW-040": fresh("TW-040", "daily_limits.py"), } say("ИСТОЧНИКИ — снимки свежие (не старше своих скриптов)") say(" " + " · ".join(src)) say("") say("1 · РАЗМЕР ДНЯ ПЕРЕНОСИТСЯ, ФОРМА — НЕТ (TW-018)") t = src["TW-018"] big = grab(t, r"после большого бара \(>([\d.]+) обычного хода\): следующий день двигает ([\d.]+) хода, n=(\d+)", "TW-018") small = grab(t, r"после маленького \(<([\d.]+) хода\):\s+([\d.]+) хода, n=(\d+)", "TW-018") ratio = float(big.group(2)) / float(small.group(2)) say(f" после большого дня (>{big.group(1)} хода): следующий двигает {big.group(2)} хода (n={big.group(3)})") say(f" после тихого (<{small.group(1)} хода): {small.group(2)} хода (n={small.group(3)}) — в {ratio:.1f} раза меньше") rows = re.findall(r"^\s+(верхняя треть|середина|нижняя треть)\s+\d+ баров · следующий день выше в ([\d.]+)%", t, re.M) base = grab(t, r"все бары\s+\d+ баров · выше в ([\d.]+)%", "TW-018") ups = [float(p) for _, p in rows] say(f" где бы ни закрылся бар, следующий день выше в {min(ups):.1f}–{max(ups):.1f}% (база {base.group(1)}%)") holds(ratio >= 2.0, "размер переносится", f"после большого дня ход больше лишь в {ratio:.1f} раза") holds(len(ups) == 3 and max(abs(u - float(base.group(1))) for u in ups) < 1.0, "форма не говорит ничего", "место закрытия разошлось с базой больше чем на процент") say("") say("2 · ПОЧТИ ВСЁ ДВИЖЕНИЕ ГАСИТСЯ (TW-019)") t = src["TW-019"] g = grab(t, r"→ гасится: акции ([\d.–]+) %, крипта ([\d.–]+) %", "TW-019") say(f" за год гасится: акции {g.group(1)}%, крипта {g.group(2)}%") holds(min(span(g.group(1))[0], span(g.group(2))[0]) >= 90, "почти всё гасится", "доля погашенного ниже 90 %") say("") say("3 · ТРЕТЬ ХОДА АКЦИЙ — КОГДА ТОРГОВАТЬ НЕЛЬЗЯ (TW-019)") o = grab(t, r"→ вне сессии: акции ([\d–]+) %", "TW-019") gap = grab(t, r"→ худший разрыв: акции ([\d.–]+) %", "TW-019") say(f" вне сессии: акции {o.group(1)}% движения; худший разрыв через ночь: {gap.group(1)}% цены") holds(span(o.group(1))[0] >= 25, "треть хода вне сессии", "доля вне сессии ниже четверти") say("") say("4 · ЗА УРОВНЕМ ЦЕНА ДОХОДИТ ДО СЛЕДУЮЩЕГО ДВА РАЗА ИЗ ТРЁХ (TW-014)") t = src["TW-014"] n = grab(t, r"переходов уровня вверх: (\d+)", "TW-014") p = grab(t, r"дошли до следующего уровня: (\d+) %", "TW-014") say(f" переходов: {n.group(1)}; дошли до следующего уровня: {p.group(1)}% (утверждение — 80%)") holds(55 <= int(p.group(1)) < 80, "два раза из трёх, не восемь из десяти", f"доля {p.group(1)}%") say("") say("5 · ИЗДЕРЖКИ — АРИФМЕТИКА, И НА КРИПТЕ ОНИ РЕШАЮТ (TW-015)") t = src["TW-015"] st_ = grab(t, r"акции:\s+([\d–]+)% стопа", "TW-015") c1 = grab(t, r"крипта 0,1 %:\s+([\d–]+)% стопа", "TW-015") c4 = grab(t, r"крипта 0,4 %:\s+([\d–]+)% стопа", "TW-015") say(f" круг в долях стопа: акции {st_.group(1)}%, крипта по обычному тарифу {c1.group(1)}%, " f"на тарифе нижнего объёма {c4.group(1)}%") holds(span(st_.group(1))[1] < 10 and span(c1.group(1))[0] >= 25 and span(c4.group(1))[0] >= 100, "на акциях мелочь, на крипте решает", "соотношение издержек изменилось") say("") say("6 · ПЛЕЧО НЕ МЕНЯЕТ R — ОНО СТАВИТ ВТОРОЙ СТОП (TW-020)") t = src["TW-020"] liq = {int(k): float(v) for k, v in re.findall(r"плечо\s+(\d+)×\s+ликвидация при ходе против вас\s+([\d.]+)%", t)} sec2 = t.split("2 · КАК ЧАСТО", 1)[1].split("3 ·", 1)[0] head = re.search(r"^\s+((?:\d+×\s*)+)$", sec2, re.M) cols = [int(c[:-1]) for c in head.group(1).split()] if head else [] tsla = re.search(r"^\s+TSLA\s+((?:[\d.]+%\s+)+)", sec2, re.M) vals = [float(v[:-1]) for v in tsla.group(1).split()] if tsla else [] at50 = dict(zip(cols, vals)).get(50) holds(10 in liq and 50 in liq and at50 is not None, "плечо — второй стоп", "таблица ликвидаций изменила форму") say(f" ликвидация: при 10× — ход {liq[10]}% против вас, при 50× — {liq[50]}%") say(f" у Tesla ход от открытия доставал до ликвидации при 50× в {at50}% дней") say("") say("7 · ТЕСНЕЕ СТОП — ДОРОЖЕ СДЕЛКА В R, ВО СТОЛЬКО ЖЕ РАЗ (TW-040)") t = src["TW-040"] spy = grab(t, r"^\s+SPY\s+([\d.]+)%\s+([\d.]+)%\s+([\d.]+)R\s+([\d.]+)R\s+([\d.]+)R\s+([\d.]+)R\s+([\d.]+)R", "TW-040") r1, r4, r8 = float(spy.group(3)), float(spy.group(5)), float(spy.group(7)) say(f" индексный фонд, круг за сделку: дневной стоп {r1:.2f}R, вчетверо теснее {r4:.2f}R, в восемь раз {r8:.2f}R") holds(abs(r4 / r1 - 4) < 0.5 and abs(r8 / r1 - 8) < 1.0, "издержки растут со стопом", "масштаб не линейный") say("") say("8 · ЛИНИЮ МОЖНО ПРОВЕСТИ ПОЧТИ ВЕЗДЕ (TW-022)") t = src["TW-022"] b = grab(t, r"от (\d+) до (\d+) на сотню баров, медиана (\d+)", "TW-022") say(f" оснований на сотню баров: от {b.group(1)} до {b.group(2)}, медиана {b.group(3)}") holds(int(b.group(3)) >= 30, "линия почти везде", f"медиана {b.group(3)} на сотню") say("") say("9 · ЛУЧШЕЕ ИЗ МНОГИХ НЕ ПЕРЕЖИВАЕТ НОВЫХ ДАННЫХ (TW-027)") t = src["TW-027"] first = grab(t, r"Выбрано на первой половине: .+? — ([+-][\d.]+)R на (\d+) сделках", "TW-027") second = grab(t, r"На второй половине то же сочетание: ([+-][\d.]+)R на (\d+) сделках", "TW-027") worse = grab(t, r"хуже на второй половине: (\d+) сочетаний из (\d+)", "TW-027") say(f" лучшее из {worse.group(2)} сочетаний на первой половине: {first.group(1)}R ({first.group(2)} сделок)") say(f" то же на второй половине: {second.group(1)}R ({second.group(2)} сделок); хуже на второй " f"{worse.group(1)} из {worse.group(2)}") holds(float(second.group(1)) < 0 < float(first.group(1)), "лучшее не переживает новых данных", "знаки половин изменились") say("") say("10 · ЧЕСТНЫЙ УЧЁТ РЕШАЕТ ИТОГ (TW-026)") t = src["TW-026"] all_ = grab(t, r"(\d+) из (\d+)\s*\nсочетаний «инструмент × буфер» ниже нуля", "TW-026") st0 = grab(t, r"первый прогон: день входа не смотрели вовсе\s+(\d+)\s+(\d+)%\s+([+-][\d.]+)", "TW-026") st9 = grab(t, r"честный расчёт\s+(\d+)\s+(\d+)%\s+([+-][\d.]+)", "TW-026") say(f" первый прогон: {st0.group(1)} сделок, {st0.group(2)}% выигрышных, {st0.group(3)}R на сделку") say(f" честный расчёт: {st9.group(1)} сделок, {st9.group(2)}% выигрышных, {st9.group(3)}R на сделку") say(f" ниже нуля: {all_.group(1)} из {all_.group(2)} сочетаний «инструмент × буфер»") holds(float(st9.group(3)) < 0 < float(st0.group(3)) and all_.group(1) == all_.group(2), "честный учёт решает итог", "итог честного расчёта больше не отрицателен везде") say("") say("ВЫВОД") say(" Все десять утверждений держатся на нынешних снимках своих уроков.") OUT.write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\nнаписано: {OUT}") return 0 if __name__ == "__main__": raise SystemExit(main())