Эксперимент 264 · часть A — аудит «держать дольше 180 минут»

Проверка чужого результата на боевом леджере, не свип. Входы заморожены ровно так, как случились; двигается только жёсткий клок выхода. Переоценка идёт по той же ленте и тем же исполнителем, что и живая книга, поэтому руки сравнимы между собой. Окно 25.08 – 07.09.2026 · 15 боевых линий · общая когорта 12 641 позиций · потолок капитала = собственный пик живой книги.

Живой леджер
$57 882
13 770 закрытых позиций
Базис: та же книга, переоценена
$51 310
общая когорта 12 641 · смещение -0.79 пп/сделку
Рука 600 под потолком
$97 197
+89% к базису · 11.6% отказов
Рука 600 без потолка
$114 332
пик капитала $128 302 (2.2×)
Тайм-аут в живой книге
−$43 025
9 199 позиций — единственная минусовая статья
Прирост несут
2 017 позиций
$73 839 · клок-возвраты −$10 817
Концентрация
52% в топ-3 токенах
положительных токенов 126 из 264
Режим или эдж
+1.58 vs +1.69 пп
наш дрейф против безусловного — эджа нет

1 · Вердикт

Проверяемое утверждение: «если бы живая книга держала позиции дольше 180 минут, она бы заработала ~$98k вместо $58k». На боевом леджере, с настоящими издержками выхода и под капитальным потолком самой живой книги — утверждение воспроизводится.

Живая книга, леджер
$57 882
13 770 закрытых позиций · 25.08 – 07.09
→
Переоценка той же книги
$51 310
тот же исполнитель, общая когорта 12 641 — это базис сравнения
→
Рука 600 мин, без потолка
$114 332
+63 022 к базису · ×2.23 · но пик капитала $128 302
→
Рука 600 мин под потолком
$97 197
потолок $58 840 · FCFS · 11.6% отказов · ×1.89

Разница между леджером ($57 882) и его же переоценкой ($51 921 на тех же 13 770 позициях) — это систематическое смещение переоценки: -0.79 пп на сделку в среднем, медиана -0.34 пп, корреляция с леджером 0.992. Поэтому руку 600 надо сравнивать не с леджером, а с базисом $51 310: обе величины посчитаны одним кодом на одной ленте, и смещение сокращается. Сравнение с леджером было бы сравнением двух разных измерителей.

Ни одно число здесь не является прогнозом. Это переигровка уже случившихся входов при сдвинутом жёстком клоке: входы заморожены, меняется только момент выхода.

2 · Как это устроено

У всех 15 боевых моделей жёсткий горизонт planned_exit_ts − entry_ts постоянен и равен ровно 180 минутам; единственное исключение — C_tmo120__thm1__tp40_q97_ens5_exit5 (линия t26) с 120 минутами. Разброса внутри модели нет: min = max = median. Именно этот клок и двигает аудит.

Восстановленный take-profit — медиана реализованной pnl_pct по фактическим take_profit-выходам самой модели (03_replay_timeouts.py): не ярлык из имени __tp40, а факт леджера. У 2 линий его нет вовсе — значит, в переигровке у них нет и правила take-profit.

модель — боевая линияplanned_exit, минпозицийPnL леджераtake-profit, восстановленный
optimized_K4__r14_zm05_q97_ens5_exit51803 374$21 23825.7%
optimized_K4__r13_w01_q97_ens5_exit51801 903$2 08725.3%
t20__exec967_train950__thm2__tpnone_q97_ens5_exit51801 473$7 322—
optimized_K4__r11_g8_a6_t180_q98_ens5_exit51801 182$5 20340.3%
F_gap5ea6_q967__thm2__tp40_q97_ens5_exit5180911$2 35341.0%
optimized_K4__r11_g8_a6_t180_q98_ens5_exit5__sp1_ddf9e5e9…180868−$29039.5%
C_tmo120__thm1__tp40_q97_ens5_exit5120819$5 32840.0%
C_gap8__thm2__tp40_q97_ens5_exit5180811$4 30640.2%
m033_v3_q98_ens5_exit5180801$7 61626.3%
C_hur15__thm2__tp40_q97_ens5_exit5180733$3 38239.9%
optimized_K4__r14_zm05_q97_ens5_exit5__sp1_9b683ba6…180340−$61424.2%
optimized_K4__r11_g8_a6_t180_q98_ens5_exit5__sp1_1244b26c…180210−$17437.7%
optimized_K4__r11_g8_a6_t180_q98_ens5_exit5__sp1_5bca6d64…180185−$12438.5%
t19__exec979_train977__thp0__tpnone_q98_ens5_exit518085$345—
optimized_K4__r11_g8_a6_t180_q98_ens5_exit5__sp1_21541edb…18075−$9539.4%

Куда живая книга девает деньги: −$43 025 на 9 199 тайм-аут-выходах, тогда как take-profit приносит $72 786, а learned exit — $28 338. Тайм-аут — единственная крупная статья с отрицательным знаком, и это ровно те позиции, которых касается удлинение.

выходпозицийPnLна позициюдоля |PnL|
timeout9 199−$43 025−$4.6829.8%
unsellable2−$239−$119.630.2%
manual6$23$3.780.0%
exit_model1 709$28 338$16.5819.6%
take_profit2 854$72 786$25.5050.4%
итого13 770$57 882$4.20100.0%

Что именно меняет удлинение. В руке 180 мин 4 569 позиций из 13 770 закрылись до клока — по take-profit, по learned exit или вручную. Их удлинение не трогает вообще: они переносятся в любую длинную руку с тем же реализованным PnL (rule = pre_clock_fill). Остальные 9 201 упёрлись в клок, и только для них переигровка продолжает ленту. Внутри продления действует собственный take-profit модели (первое касание побеждает) и новый клок руки; learned exit внутри продления не действует — см. секцию 8.

3 · Таблица рук

Общая когорта — 12 641 позиций, которые целиком помещаются в ленту даже на самой длинной руке (720 мин). Это единственное честное сравнение: колонка обрезано показывает, сколько позиций пришлось бы отмечать по последней доступной цене, если бы когорту не фиксировали.

Удлинение не бесплатно. Без потолка PnL растёт в 2.23 раза, но пик открытого ноушенала — с $58 840 до $128 302 (2.2×), а средний занятый капитал — с $9 352 до $24 175 (2.6×). Под потолком самой живой книги рука 600 всё равно даёт $97 197, но уже отказывает 11.6% входов — и это первое место, где «держать дольше» начинает конкурировать само с собой. На 720 мин капитальный потолок уже съедает прирост целиком.

рукапозицийPnL, без потолкаΔ к 180×PnLпик капитала×пиксредний капитал×средн.PnL под потолкомотказовпик под потолкомобрезано в n_all
180 мин
живой клок
12 641$51 310—1.00×$58 8401.00×$9 3521.00×$51 3120.0%$58 66755
240 мин12 641$59 467+8 1571.16×$75 4291.28×$11 8471.27×$59 1051.6%$58 837132
300 мин12 641$69 988+18 6781.36×$98 9441.68×$14 1851.52×$66 1593.8%$58 839371
360 мин12 641$80 621+29 3111.57×$111 5801.90×$16 3971.75×$71 6455.5%$58 840418
480 мин12 641$100 779+49 4691.96×$124 1652.11×$20 4992.19×$84 8188.7%$58 839668
600 мин
рука из утверждения
12 641$114 332+63 0222.23×$128 3022.18×$24 1752.58×$97 19711.6%$58 840940
720 мин12 641$119 599+68 2892.33×$129 2192.20×$27 5522.95×$96 80514.8%$58 8401 106
накопленный PnL по рукам
Каждая точка — выход одной позиции; PnL признаётся в момент выхода руки, поэтому кривые расходятся не по календарю входов, а по календарю выходов.
180 мин240 мин300 мин360 мин480 мин600 мин720 мин

Форензика вместо разглядывания. Ниже — вывод lib.forensics.read_series на тех же массивах, что нарисованы выше; ни одно утверждение о форме кривой не прочитано с картинки.

рука 180 мин

TIMEFRAME  n=12,641 · 5s spacing (irregular/event-time) · 13.0d span · 2026-08-25 01:32..2026-09-07 01:57 UTC
FINDINGS (4 fired, 12 clean)
[HIGH] REGIME/segment · rate spread 23.7x across segments: +727.5/h (2026-09-05 06:41) -> +30.64/h (2026-08-25 01:32)
       verify: forensics.segment_series(t, cum)
[HIGH] STRUCTURE/segment-dominance · one slow_grind segment holds 66.4% of total (2026-08-30 07:18..2026-09-05 06:34, 46.0% of elapsed time)
[MED ] REGIME/sign-flip · 1 of 5 segments net-negative; worst -7,249.32 at 2026-09-06 01:33
[MED ] DD/recovery · recovery took 18.1h vs 5.0h to fall (3.6x slower)
SHAPE  slow_grind(5.2d, +30.6$/h) -> slow_grind(6.0d, +238$/h) -> steep_rally(18.5h, +727$/h) -> steep_selloff(3.1h, -2.32e+03$/h) -> slow_grind(21.0h, +341$/h)
CLEAN: top-trade, top-day, drop-top-3, gini, arrival, dd-depth (-7,671), dd-repeat, dd-single-row, ladder, worst_half, sign-consistency, evenness

рука 600 мин

TIMEFRAME  n=12,641 · 1min spacing (irregular/event-time) · 13.3d span · 2026-08-25 01:32..2026-09-07 08:56 UTC
FINDINGS (3 fired, 13 clean)
[HIGH] REGIME/segment · rate spread 36.8x across segments: +1,686/h (2026-09-05 07:01) -> +45.81/h (2026-09-06 10:31)
       verify: forensics.segment_series(t, cum)
[HIGH] STRUCTURE/segment-dominance · one slow_grind segment holds 58.7% of total (2026-08-25 01:32..2026-09-05 06:35, 84.4% of elapsed time)
[MED ] DD/repeat · second drawdown is 93% as deep as the worst — not a one-off tail event
SHAPE  slow_grind(11.2d, +249$/h) -> steep_rally(27.4h, +1.69e+03$/h) -> plateau(22.4h)
CLEAN: top-trade, top-day, drop-top-3, gini, segment-sign, arrival, dd-depth (-4,225), dd-recovery, dd-single-row, ladder, worst_half, sign-consistency, evenness
капитал по рукам
Пик и средний занятый ноушенал по рукам; красная линия — пик самой живой книги, он же потолок в правой части таблицы.

4 · Откуда прирост

Весь прирост $63 022 несут 2 017 позиций, которые в продлении дошли до take-profit: $73 839. 6 221 позиций снова упёрлись в клок и в сумме теряют −$10 817. Улучшилась при этом лишь 35.1% позиций когорты: распределение односторонне длинное вправо, а не сдвинутое целиком.

Прирост реализован, а не переоценён: в общей когорте на руке 600 обрезанных позиций 0 (05_mtm_split.csv), поэтому uplift_total и uplift_realised совпадают: $63 022.

правило выхода на 600 минпозицийдоля когортысуммарная дельтамедиана дельтыулучшились
take_profit
дошли до take-profit внутри продления
2 01716.0%$73 839$26.4999.9%
clock
снова упёрлись в клок, уже на 600-й минуте
6 22149.2%−$10 817−$1.6739.0%
pre_clock_fill
закрылись до 180-й минуты — не тронуты
4 40334.8%$0$0.000.0%
итого12 641100.0%$63 022$0.0035.1%
распределение дельты 600 минус 180
Дельта PnL каждой позиции: 600 мин минус 180 мин. Ось частот логарифмическая — иначе виден только нулевой столбик.
FINDINGS (3 fired, 2 clean)
[MED ] DIST/skew · skew = +2.35 — mean is not the typical outcome
[MED ] DIST/kurtosis · excess kurtosis = 13.1 — fat tails
[MED ] DIST/outliers · 2852 values beyond 5x MAD (max 312.9)
QUANTILES (value)
   q       value
0.00 -110.267036
0.01  -45.519562
0.05  -20.639890
0.25   -1.510313
0.50    0.000000
0.75    5.952469
0.95   51.693225
0.99   95.147945
1.00  312.894530
CLEAN: tail-mass (10.3%), zero-mass
NOTE: n=12,641 · mean +4.986 · median +0 · std 23.3 · total +6.302e+04

5 · Концентрация

Топ-1 токен несёт 30.4% прироста, топ-3 — 52%, топ-10 — 77%. Положительный вклад дают 126 токенов из 264; ещё 108 тянут вниз, поэтому накопленная кривая сначала уходит выше 100% и только потом спускается к итогу.

Но это не одна удачная сделка. В топ-токене 645 позиций, максимальная дельта по одной — $182 (худшая — −$32). То есть эффективная ширина эффекта — десяток токен-эпизодов, а внутри эпизода он размазан по сотням мелких сделок. Позиций на токен: медиана 9, p95 216, максимум 645.

токен — топ-25 из 06_uplift_by_token.csvвклад в приростдолянакопленным итогомпозиций в когорте
0x507b6f34…15b220$19 13130.4%30.4%645
0x420393b7…f27c27$10 50316.7%47.0%490
0x20024e48…0a7777$3 1325.0%52.0%588
0xcacb0e9c…8bcb68$2 9814.7%56.7%314
0x298348d5…bdc8c7$2 6714.2%61.0%177
0x15d36b6a…af5c4d$2 4743.9%64.9%100
0x0762c170…357506$1 9383.1%68.0%114
0x5d6ef090…c61e18$1 9213.0%71.0%105
0x000b2164…f164b8$1 8532.9%73.9%29
0xfdae23ce…0e8312$1 8102.9%76.8%432
0xb9972ca7…073277$1 7122.7%79.5%254
0x18e67423…4cff5c$1 6342.6%82.1%217
0x0ff9072a…8ce2b2$1 4792.3%84.5%239
0x862cdccb…447777$1 1811.9%86.4%419
0xd2932910…527777$1 1811.9%88.2%189
0x3450598e…897777$1 1711.9%90.1%247
0x51fb76be…6b4fea$1 1671.9%91.9%122
0xa934ba4f…e35563$1 1371.8%93.7%266
0xae2df3c1…e7cb99$1 1171.8%95.5%129
0x1abf16f6…582228$1 0661.7%97.2%121
0x2103faa9…1f9bf1$8911.4%98.6%97
0x75c8258e…0bcbe2$8771.4%100.0%159
0xe6864e46…777712$8501.3%101.4%208
0x385b36ff…6471d5$7411.2%102.5%101
0x8ecea3d0…3d6878$5890.9%103.5%179
кривая концентрации прироста по токенам
Накопленная доля прироста по токенам, отсортированным по вкладу. Кривая поднимается выше 100% ровно потому, что отрицательные токены возвращают её обратно.
FINDINGS (5 fired, 0 clean)
[HIGH] TOKEN/top1 · "0x507b6f349a80114097a67b8b4677367acc15b220" = 30.4% of total (+1.913e+04)
[HIGH] TOKEN/breadth · effective N = 10.2 of 264 token (4%) — the chart is wider than the edge
[MED ] TOKEN/gini · gini = 0.832
[MED ] TOKEN/long-tail · 89% of token contribute <1% each
[MED ] TOKEN/mixed-sign · 108 of 264 token are negative (-1.158e+04 drag)
TOP 5 of 264 token
                                     token        value    share
0x507b6f349a80114097a67b8b4677367acc15b220 19130.919121 0.303558
0x420393b70a34d6aef855ef56e87d4d4161f27c27 10503.197710 0.166659
0x20024e485c0b22b42855589700721b28320a7777  3131.687338 0.049692
0xcacb0e9caccee63ec4d82952e561a291c68bcb68  2980.761102 0.047297
0x298348d5b2e45c774e3ee4f1a0924071dfbdc8c7  2671.217676 0.042385
NOTE: 264 token · total +6.302e+04

6 · Режим или эдж — главная секция

Прирост существует. Вопрос в том, наш ли он. Контроль: безусловная 420-минутная форвардная доходность (ровно 180 → 600) в тех же пулах в те же дни, снятая по равномерному 10-минутному клоку — 05_regime_control.py.

Лента, безусловно
+1.69 пп
доля положительных 0.543 · n = 68 977 сэмплов, взвешено по дням
против
Наш дрейф 180 → 600
+1.58 пп
доля положительных 0.530 · n = 9 178 клок-позиций

Вывод: удержание дольше в этом окне — это бета ленты, а не улучшение отбора. Наш условный дрейф не отличим от того, что дало бы удержание чего угодно из торгуемой вселенной те же семь часов. Медиана ниже безусловной, доля положительных — тоже. Отбор входов у нас есть (это видно по PnL книги), но дополнительное время после 180-й минуты не приносит собственного эджа.

Знак ленты гуляет по дням, и это не шум вокруг устойчивого плюса: 29.08 медиана ленты -6.11 пп — семичасовое удержание в тот день было бы систематически убыточным. Наш собственный дрейф отрицателен 01.09, 04.09, 07.09. Одно окно 25 авг – 7 сен — это одна реализация режима, а не выборка режимов.

день входаn, нашимедиана, нашидоля +, нашиn, лентамедиана, лентадоля +, лентаэдж по медиане
25.08 · лента отрицательна415+9.540.6274 807-1.280.472+10.81
26.08232+4.320.6034 883+0.070.501+4.25
27.08484+7.120.6014 663+3.380.608+3.74
28.08235+9.740.6344 141+2.230.567+7.51
29.08 · лента отрицательна369+3.620.5285 157-6.110.365+9.73
30.08493+4.490.5356 712+1.640.531+2.86
31.08 · лента отрицательна358+4.660.5615 597-0.300.493+4.96
01.09 · наш дрейф отрицателен440-3.850.4185 684+5.990.643-9.84
02.09351+3.110.5415 615+0.760.520+2.35
03.09650+2.800.5355 536+0.910.518+1.90
04.09 · наш дрейф отрицателен906-2.190.4194 778+8.910.697-11.10
05.09961+10.270.6885 653+1.400.536+8.87
06.092 382+0.000.5005 090+4.590.612-4.58
07.09 · наш дрейф отрицателен902-1.100.451661+3.820.608-4.92
наш дрейф против безусловного по дням
Сверху — медианный дрейф в процентных пунктах, наш против безусловного; снизу — доля положительных исходов и линия 0.50.
Чего этот контроль не доказывает
  • Безусловная выборка берётся без комиссий и импакта: чистая разница цен на 10-минутной сетке. Наш дрейф — уже после издержек выхода. То есть контроль занижает нашу планку, и настоящий эдж ещё ближе к нулю или отрицателен.
  • Выборка ограничена пулами, которые мы реально торговали, и живут они ровно тем, что дожили до сделки: это выживаемость пулов, а не вселенная.
  • Средние здесь бесполезны: 31.08 среднее по ленте +261 пп против медианы -0.30 пп. Один пул с ×3 уносит среднее куда угодно, поэтому вся секция читается по медианам и долям.
  • Пулированная по сэмплам медиана ленты, напечатанная прогоном 05_regime_control.py, — +1.73 пп; сырые сэмплы не сохранены, поэтому страница считает взвешенное по дневным n значение (+1.69 пп). Вывод не меняется ни при одном из них.

7 · Риск и капитал

Контринтуитивное: максимальная просадка у длинных рук меньше. −$7 671 на 180 мин против −$3 910 на 600 мин, и это не ошибка знака. Тайм-аут на 180-й минуте закрывал убыточные позиции ровно на дне — фиксировал просадку, вместо того чтобы дать ей отыграться. Длинная рука того же не делает, поэтому её кривая проседает мельче — но платит за это капиталом (секция 3) и концентрацией (секция 5).

Sharpe в этой таблице не считается сознательно. В общей когорте 14 дневных наблюдений, а lib.metrics.sharpe возвращает NaN ниже 30 — правило репозитория, а не оценка на глаз. Сравнивать руки по риску здесь можно только просадкой, худшим днём и отношением итога к просадке.

Просадки взяты из 08_risk_by_arm.csv (lib.metrics.max_dd). lib.forensics на той же кривой руки 600 ранжирует худшей просадку −$4 225: он строит свой путь внутри совпадающих меток времени, поэтому расходится на пару сотен долларов. Файл главнее.

рукаитогмакс. просадкаитог / просадкахудший деньднейSharpe
180 мин$51 310−$7 6716.7×−$1 43214не считается
240 мин$59 467−$9 4696.3×−$1 75414не считается
300 мин$69 988−$8 7188.0×−$1 95714не считается
360 мин$80 621−$4 97816.2×−$1 65114не считается
480 мин$100 779−$2 91334.6×−$1 03814не считается
600 мин$114 332−$3 91029.2×−$72614не считается
720 мин$119 599−$4 12229.0×−$92714не считается

8 · Чему верить нельзя

  1. learned exit за 180-й минутой физически отсутствует. Прод перестаёт скорить позицию в момент её закрытия, поэтому trader_exit_decisions за 180-й минутой не существует ни на какой ленте. В живой книге learned exit заработал $28 338 на 1 709 позициях; длинные руки этой защиты лишены полностью — внутри продления работает только take-profit и новый клок. Это делает оценку консервативной (модельный выход добавил бы денег), но одновременно нереалистичной: боевая система так себя никогда не вела.
  2. выход обучен под конкретный горизонт. Среди фич learned exit есть elapsed_fraction_of_timeout и path_completed_minutes (lib/rh_exit_pair.py), то есть модель выхода знает, какая доля её собственного горизонта истекла. При смене горизонта эти фичи меняют смысл, и модель надо переобучать. Овердей на леджере этого не заменяет ни при каком качестве переигровки.
  3. лента после 180-й минуты содержит наши же продажи. В контрфактуале мы держим позицию, а в ленте, по которой мы её переоцениваем, уже стоит наша собственная реальная продажа — она случилась ровно на 180-й минуте. Цена сразу после клока поэтому занижена, а прирост длинных рук в первые минуты продления — занижен тоже. Направление известно, величина — нет.
  4. импакт считается изолированно на каждую продажу. Каждый выход в переигровке платит свой exit_impact так, будто он один в стакане, а выходим мы кучей: на токен приходится медианно 9 позиций, p95 — 216, максимум 645. Смягчающее обстоятельство измеримо: кластеризация выходов у длинной руки почти та же, что у короткой — максимум $3 928 против $3 600 ноушенала в одну минуту, p99 $1 251 против $1 248. Абсолютный уровень PnL этим искажён, дельта между руками — почти нет.
  5. всё измерено на боевом масштабе. Средняя стоимость позиции в когорте — $92.90, медианная — $71.42, медианный импакт выхода — 0.134%. Это масштаб, на котором стакан нас не замечает. На большем размере и импакт, и кластеризация выходов растут нелинейно, и вывод не переносится.
  6. окно и когорта. Измерено окно 25.08 – 07.09 целиком, одним куском. Когорта сравнения общая для всех рук — 12 641 позиций; обрезанных (marked-to-market) в ней 0, поэтому прирост $63 022 реализован, а не переоценен по рынку. Это единственная хорошая новость в списке.

9 · Что это значит для свипа

Удлинение горизонта нельзя проверить овердеем на леджере. Всё, что показано выше, — это верхняя граница арифметики при замороженных входах и отключённом модельном выходе. Чтобы получить число, на которое можно ставить капитал, нужен свип с LABEL_TIMEOUT_MIN > 180 и переобучением всей цепочки: разметка, вход и выход учатся под новый горизонт вместе.

Любой результат такого свипа обязан пройти два контроля, и оба взяты прямо из этого аудита:

  • Контроль на бету. Длинная рука должна показать эдж над безусловным дрейфом ленты на тех же днях — иначе она просто загружает режим. В окне этого аудита такого эджа нет.
  • Контроль на концентрацию. Снятие топ-токенов не должно убивать результат: сейчас топ-3 токена несут 52% прироста.
  • Контроль на капитал. Сравнение только под общим потолком: без него рука 600 берёт 2.2× пикового ноушенала и сравнение теряет смысл.

Источники

файлразмеризменён
results/01_arms_uncapped.csv1 КБ2026-09-07 11:39
results/02_arms_capped.csv1 КБ2026-09-07 11:40
results/03_audit_context.json2 КБ2026-09-07 11:40
results/04_per_day.csv1 КБ2026-09-07 11:40
results/05_mtm_split.csv0 КБ2026-09-07 11:41
results/06_uplift_by_token.csv1 КБ2026-09-07 11:41
results/07_drift_by_day.csv0 КБ2026-09-07 11:41
results/08_risk_by_arm.csv0 КБ2026-09-07 11:41
results/09_unconditional_drift.csv0 КБ2026-09-07 11:44
results/10_regime_edge.csv1 КБ2026-09-07 11:44
artifacts/replayed_arms.parquet6 674 КБ2026-09-07 11:39
artifacts/delta_600_vs_180.parquet720 КБ2026-09-07 11:40
artifacts/parity_actual.parquet2 478 КБ2026-09-07 11:38

Отчёт собран из этих файлов скриптом lib/06_build_audit_report.py; графики нарисованы matplotlib и вшиты как base64-PNG, поэтому страница открывается офлайн одним файлом. Числа в тексте подставлены из тех же файлов, а не набраны руками.