# Дослідження 20. Функція втрат кінця ноги

Вкладка: [Функція втрат](http://localhost:8080/loss) · дані —
`/api/loss/walk`.

## Ціль

Подивитись на функцію втрат, під якою навчена краща модель дослідження
19, і відповісти на одне питання: **чи є в ній взагалі сила, яка тягне
видачу до кінця ноги**.

Питання виникло з розділу 11 дослідження 19: коли зігзаг перестає різати
ряд на ноги, мережа не каже клас 10 **жодного разу** на 42 192 подіях
шести монет. Пояснення шукається не в ручках і не в даних, а в тому, що
саме мінімізується.

## Дані

Нічого не читається з `data/`. Криві рахуються на побудованих логітах
викликом самої функції втрат дослідження 19
(`leg_end_train.masked_loss`), а крива навчання береться з готового
звіту того прогону:

- [`research/19-leg-end-lstm/results/train_steps_ema10_2_3.json`](../19-leg-end-lstm/results/train_steps_ema10_2_3.json)
  — модель `ширина 64 · увага 2 гол. · lr 0.001`, епоха 4 з 5,
  602 ноги train, межа `2026-08-14 15:27`.

## Розділ 1. Форма функції втрат · `підтверджено`

### Метод

[`scripts/loss_shape.py`](scripts/loss_shape.py).

```
L = Σ_маска CE(логіти, клас) / N
  + Σ_пари (jump·max(Δ−1, 0)² + down·max(−Δ, 0)²) / M

Δ = очікуваний клас(t+1) − очікуваний клас(t),  jump = 5, down = 25
```

Криві **не виводяться формулою наново** — це важливо. Щоб намальоване
було рівно тим, що оптимізується, під кожну точку будуються логіти з
потрібним очікуваним класом і викликається сама `masked_loss`:

```
щоб очікуваний клас дорівнював e:
  a = floor(e),  p(a) = 1 − (e − a),  p(a+1) = e − a
  логіти = log(p)          бо softmax(log p) = p
```

Штраф за сходинку виділяється відніманням: той самий прогін з
`jump = down = 0` дає чисту ентропію, різниця — це штраф.

### Результат

**1. Штраф за сходинку має дірку в нулі.**

| Δ | −2 | −1 | −0.5 | **0** | **0.5** | **1** | +2 | +3 |
| - | -: | -: | ---: | ----: | ------: | ----: | -: | -: |
| штраф | 100.0 | 25.0 | 6.25 | **0** | **0** | **0** | 5.0 | 20.0 |

Штраф дорівнює нулю на всьому проміжку `Δ ∈ [0, 1]`. **Стояти на місці
коштує рівно стільки ж, скільки йти вперед на один клас.** Функція
карає рух назад і стрибок через клас — тобто смикання, — але не має
жодного члена, який би тягнув лічильник уперед.

**2. Перехресна ентропія не бачить відстані.**

Правда — клас 10, відповідь упевнена на 0.9:

| Сказаний клас | 1 | 2 | … | 9 | 10 |
| ------------- | -: | -: | - | -: | -: |
| CE | 4.50 | 4.50 | 4.50 | 4.50 | **0.105** |

Сказати `9` замість `10` коштує рівно стільки ж, скільки сказати `1`.
Уся ординальність мітки — те, що десятини йдуть по порядку, — у
функцію втрат не заходить. Ординальність живе тільки в штрафі за
сходинку, а він, за пунктом 1, у робочій зоні нульовий.

**3. Крива навчання спадає, і це нічого не спростовує.**

| Епоха | 1 | 2 | 3 | 4 | 5 |
| ----- | -: | -: | -: | -: | -: |
| train | 2.615 | 2.320 | 2.268 | **2.194** | 2.148 |
| val | 2.315 | 2.289 | 2.191 | **2.094** | 2.156 |

Спад іде за рахунок ентропії — модель вчиться ставити ймовірність біля
середини ряду, — а не за рахунок того, що лічильник навчився доходити
до кінця.

### Висновки

1. **Функції втрат нічим не вигідно доводити видачу до класу 10.**
   Мінімум досягається на будь-якій неспадній послідовності з кроком
   не більшим за 1, зокрема на **сталій**.
2. **Разом з міткою це замкнене коло.** Мітка «десятина ноги»
   означена через довжину ноги, яку знає тільки зігзаг; функція втрат
   не вимагає дійти до її кінця. Тому рівномірний лічильник від
   відомого початку — не помилка навчання, а **точний оптимум цієї
   пари**.
3. **Це пояснює розділ 11 дослідження 19 повністю.** Нуль своїх
   обнулень — не збіг і не поріг: клас 10 не потрібен для мінімуму, і
   модель до нього не приходить.
4. **Що міняти — видно з графіка 1.** Потрібен член, який робить
   стояння на місці дорожчим за рух: або штраф за недохід до останнього
   класу в кінці ноги, або ординальна ціль замість перехресної ентропії
   (щоб «9 замість 10» коштувало менше, ніж «1 замість 10»), або обидва.

## Розділ 2. Навчання під виправленою втратою · `підтверджено`

### Метод

[`scripts/loss_train.py`](scripts/loss_train.py). До втрати додано два
члени (`leg_end_train.masked_loss`), обидва нульові за замовчуванням —
розділ 9 дослідження 19 відтворюється незмінним:

```
L = CE + jump·max(Δ−1,0)² + down·max(−Δ,0)²          було
  + ordinal · сер((E − y)²)                          відстань до правди
  + reach   · сер(max(C − E, 0)²) на подіях класу C  недохід до кінця
```

`ordinal` дає ентропії відстань, якої в ній немає; `reach` закриває
дірку в нулі — стояти на місці більше не безплатно.

Дані, розбиття, ручки мережі й seed — ті самі, що в розділі 9
дослідження 19: `клас210х14х4ц`, 608 ніг (602 train, 6 val), межа
`2026-08-14 15:27`, ширина 64 · увага 2 гол.

**Батч за бюджетом подій.** Причинна увага квадратична по довжині
ноги: батч із восьми ніг по 3000 подій дає матрицю на 549 МБ і жене
машину в підкачку (`rules/03-models.md` → Навчання без свопа). Тому
батч збирається за бюджетом `4800` подій: довгі ноги йдуть по одній,
короткі — вісімками. Медіана ноги train — 413 подій, у стелю 3000
упирається одна нога з 602, тому склад більшості батчів не міняється.
Пік уваги впав з 549 МБ до 69 МБ, приріст свопу — з 2.7 ГБ до 0.6 ГБ.

### Результат

Три прогони, 12.6 хвилини разом.

| Прогін | ordinal | reach | Епоха | Максимум сказаного класу на val | Подій класу 10 | Влучність класу | Відстань | Влучила | Пік у зоні | Вниз |
| ------ | ------: | ----: | ----: | ------------------------------: | -------------: | --------------: | -------: | ------: | ---------: | ---: |
| `свіжа` | 0.1 | 0.1 | 4 з 5 | **8.0** | 0 | 0.109 | 2.16 | 1 з 6 | 4 | 4 |
| `свіжа+` | 0.3 | 0.3 | 5 з 5 | **8.0** | 0 | 0.073 | 2.55 | 0 з 6 | 1 | 14 |
| `донавчання` | 0.1 | 0.1 | 3 з 3 | **8.0** | 0 | 0.109 | 2.19 | 1 з 6 | 4 | 0 |
| розділ 9 (стара втрата) | 0 | 0 | 4 з 5 | — | 0 | 0.101 | 2.14 | 0 з 6 | 0 | 1 |

- **Видача піднялась, але до 10 не дійшла.** Під старою втратою вона
  жила в межах 2 … 6; тепер максимум на val — `8.0`, і подій класу 10
  все одно нуль.
- **Сигнал покращився.** `свіжа` вперше влучила в зону кінця однією
  ногою з шести, пік у зоні 4 з 6 проти 0 у розділі 9.
- **Сильніші штрафи ламають.** `свіжа+` з утричі більшими ручками дала
  0 влучань, влучність 0.073 і 14 рухів вниз: видача починає смикатись,
  бо `ordinal` тягне до правди сильніше, ніж `down` тримає монотонність.
- **Донавчання працює.** Три епохи з `lr 3e-4` на вагах розділу 9 дають
  той самий рівень, що навчання з нуля, і **жодного руху вниз** — це
  найчистіші сходинки з усіх прогонів.

**val — це 6 ніг BTC.** Усі числа тут стоять на шести незалежних
одиницях, `se = 0.5/√6 = 0.20`, тому «1 з 6» і «0 з 6» між собою не
розрізняються. Це напрямок, а не доказ.

### Ціль і видача поруч

На вкладці під кривою кожного прогону стоять три полотна val на
спільній осі: ціна із зігзагом, **ціль** (десятина ноги) і **видача**
моделі — в одній шкалі `1 … 10`. Саме на них видно головне: ціль щоразу
доходить до 10 і падає в 1 на півоті, а видача зупиняється на 8 і в
одиницю не повертається.

### Висновки

1. **Обидва члени працюють у потрібний бік**: видача піднялась з
   2 … 6 до 8 і вперше влучила в зону кінця.
2. **Але класу 10 не досягла жодного разу.** Дірку в нулі закрито, а
   останній клас лишився недосяжним: `reach` штрафує недохід лише на
   подіях, які вже позначені класом 10, — а туди модель не приходить,
   бо не знає, де кінець ноги.
3. **Сила штрафу має середину.** Утричі більші ручки гірші за менші за
   всіма стовпцями одразу.
4. **Готову модель можна виправляти донавчанням** — три епохи з меншим
   `lr` дають той самий результат, що навчання з нуля, і чистіші
   сходинки.

## Розділ 3. Навчання з обнуленням від моделі · `підтверджено`

Розділи 1 і 2 міняли **функцію втрат**, але нарізка лишалась старою:
ряд різався по півотах зігзага, і кожна нога заходила в мережу з чистим
станом `h = c = 0`. Модель ніколи не була в ситуації, де межа ноги
залежить від неї, — тому й не вчилась її знаходити.

Тут нарізки немає.

### Метод

[`scripts/walk_train.py`](scripts/walk_train.py).

```
потік подій монети, без розрізання:
  крок t -> LSTM(стан) -> причинна увага на памʼять від обнулення
         -> сказаний клас десятини 1 ... 10 (ЕМА10)
  сказала 10  ->  h, c = 0, памʼять порожня
                  наступна подія починає нову ногу з класу 1
```

- **Зігзаг лишається тільки міткою** — каже, яка десятина правильна на
  кожній події. Де починати нову ногу, він більше не вирішує.
- **Стеля памʼяті `CAP = 1500` подій** обнуляє примусово; такі обнулення
  рахуються **окремо** — по них видно, чи робить модель це сама.
- **Усічений BPTT** на `WINDOW = 150` кроків: далі стан і памʼять
  відриваються (`detach`), інакше граф ріс би на весь потік.
- Втрата — та сама, що в розділі 2:
  `CE + ordinal·(E−y)² + reach·max(C−E,0)² + jump·max(Δ−1,0)² + down·max(−Δ,0)²`,
  `ordinal 0.1 · reach 0.1 · jump 5 · down 25`, `lr 3e-4`, seed 0.

Потоки train: BTC_USDT_29 40 059 подій, ETH 62 301, SOL 61 793,
XRP 55 111, BNB 62 097, DOGE 55 110 — разом 336 471 подія за епоху, усе
до межі `2026-08-14 15:27`. Val — BTC після межі, 3 800 подій.

### Результат

Дві епохи, 16.2 хвилини.

| Епоха | train | val | Своїх обнулень на val | Стелею | Максимум сказаного класу | Влучність класу |
| ----- | ----: | --: | --------------------: | -----: | -----------------------: | --------------: |
| 1 | 3.439 | 3.311 | **0** | 2 | 6.0 | 0.101 |
| 2 | 3.346 | 3.311 | **0** | 2 | 6.0 | 0.101 |

На val: 6 півотів зігзага у вікні, **0 власних обнулень**, 2 примусових
по стелі, подій останнього класу 0, відстань 2.49, влучність 0.101 —
рівно випадкова.

### Чому нуль: обнулення не має градієнта

Обнулення — **дискретна дія**: `said == 10 -> h, c = 0`. Через неї
градієнт не тече. Мережа отримує похідну тільки від втрати кроку — CE
до правильної десятини, — а сигналу «тут варто було обнулитись» не
отримує ніколи. Штрафи `ordinal` і `reach` тягнуть очікуваний клас до
правди, але й вони працюють лише на подіях, які **мітка** вже назвала
десятими; модель туди не приходить, бо не знає, де кінець ноги.

Виходить те саме коло, тільки в іншому місці: щоб навчитись обнулятись,
треба вже вміти доходити до 10, а щоб доходити до 10 — треба, щоб
обнулення давало сигнал.

### Висновки

1. **Обнулення від моделі саме по собі нічого не вчить.** Дві епохи
   на 336 471 події дали нуль власних обнулень і максимум класу 6.0 —
   гірше за розділ 2 з його 8.0 на нарізаних ногах.
2. **Причина — не в силі штрафу, а в тому, що дія недиференційована.**
   Через `if said == 10` градієнт не проходить; мережа не може
   дізнатись, що обнулення в цьому місці зменшило б втрату.
3. **Наступний крок — зробити обнулення диференційованим.** Мʼякі
   ворота замість жорсткого розриву: `h ← h·(1 − p₁₀)`, `c ← c·(1 − p₁₀)`.
   Тоді впевненість у кінці ноги **сама** стирає памʼять пропорційно, і
   градієнт проходить крізь неї: якщо стерти було корисно, `p₁₀`
   отримає плюс. Жорстке обнулення лишається тільки для прогону.

## Розділ 4. Втрата з памʼяттю про час · `підтверджено`

Розділ 3 дав нуль власних обнулень і максимум класу 6.0. Дві причини
закриваються тут разом: обнулення робиться диференційованим, а втрата
починає бачити **час**, а не окрему подію.

### Дані

Ряд не ріжеться на ноги. Розбиття — **за датою**, межа
`2026-08-14 15:27` UTC, перетину між вибірками немає.

| Потік | Вибірка | Подій |
| ----- | ------- | ----: |
| BTC_USDT_29 | навчання | 40 059 |
| ETH_USDT_fill | навчання | 62 301 |
| SOL_USDT_fill | навчання | 61 793 |
| XRP_USDT_fill | навчання | 55 111 |
| BNB_USDT_fill | навчання | 62 097 |
| DOGE_USDT_fill | навчання | 55 110 |
| **разом train** | | **336 471** |
| BTC_USDT_29 | **валідація** | **3 800** |

Клас `клас210х14х4ц`, скейлер `розподіл`, подія 210 с, 60 фіч. Ціль —
десятина ноги зігзага 2.3 % на ема10.

### Мʼякі ворота

Жорстке `if said == 10: h, c = 0` градієнта не має. Замість нього стан
стирається пропорційно впевненості в кінці ноги:

```
ворота = 1 − p₁₀^4        p₁₀ — імовірність останнього класу
h ← h · ворота            c ← c · ворота
```

Похідна проходить крізь ворота: якщо стерти памʼять тут було корисно,
`p₁₀` отримує плюс. Жорстке обнулення лишається як подія прогону —
коли згладжений сказаний клас дійшов до 10 або спрацювала стеля
`CAP = 1500`.

### Функція втрат

[`scripts/timed_loss.py`](scripts/timed_loss.py). Позначення на кроці
`t`: `y` — ціль, `E = Σ pᵢ·i` — очікуваний клас, `T` — тривалість тієї
десятини, у якій стоїть подія, у подіях.

```
L = CE(логіти, y)
  + late  · (кроків підряд позаду  / T) · max(y − E, 0)
  + ahead · (кроків підряд попереду / T) · max(E − y, 0)
  + back  · max(E_{t−1} − E_t, 0)²        (крім межі ноги)
  + jump  · max(Δ − 1, 0)²

late = 6 · ahead = 6 · back = 30 · jump = 5
```

| Член | Коли | Що карає |
| ---- | ---- | -------- |
| `CE` | завжди | перехресна ентропія до правильної десятини |
| затримка | `E < y` | ціна росте з кожною подією позаду, поділена на `T` |
| випередження | `E > y` | те саме в другий бік: нога скінчилась, а видача тримає старий клас |
| **пониження** | `E` впало, а ціль **не** падала | крок униз усередині ноги |
| стрибок | `Δ > 1` | видача має йти сходинками по +1 |

- **Влучила — платить тільки ентропію.** `E = y` дає нуль в обидва боки.
- **Ціна затримки росте з часом.** Лічильник рахує події підряд, на яких
  видача не дійшла до цілі, і обнуляється, щойно вона наздогнала. На
  десятині зі 100 подій одна подія затримки коштує 0.06, десять — 0.6,
  двадцять — 1.2.
- **Пониження — найдорожчий член.** Мітка десятини всередині ноги тільки
  росте, тому крок униз — `2 → 1`, `3 → 2`, … `10 → 9` — це рух, якого в
  даних не буває взагалі. Крок рівно на клас коштує `back = 30`: дорожче
  за повну десятину затримки і в шість разів дорожче за стрибок через
  клас. На межі ноги, де ціль падає з 10 в 1 сама, штрафу немає — там
  падіння видачі правильне.

### Коли зупинятись

Стовпець **схожість** — частка подій val, де видача промахнулась не
більше ніж на один клас. Поки він росте, навчання має сенс. Перестав
рости дві епохи підряд (`PATIENCE = 2`) — прогін зупиняється сам, і
міняється постановка, а не кількість епох.

## Розділ 5. Базові рівні й перевірка на перевчання · `підтверджено`

Розділ 4 дав ту саму видачу, що й розділ 3: максимум класу 6.0, дві
різні відповіді на всю валідацію, ворота `0.9995`. Підняте пониження
(`back 3 → 30`) зробило гірше за всіма стовпцями одразу. Крутити ручки
далі означало б шукати ключі під ліхтарем, тому спершу два питання, на
які ніхто досі не відповів: **з чим це порівнюється** і **чи працює
сам механізм навчання**.

### Базові рівні

[`scripts/baseline_check.py`](scripts/baseline_check.py) рахує дві
тривіальні відповіді на тих самих 608 ногах і 336 554 подіях, що йдуть
у навчання.

| Що відповідає | Що знає | Влучність | Схожість | Відстань |
| ------------- | ------- | --------: | -------: | -------: |
| **лічильник** | скільки подій минуло від початку ноги | **0.281** | **0.550** | 1.787 |
| константа | нічого | 0.100 | 0.301 | 3.695 |
| мережа розділу 4, 46 282 ваги | 60 фіч ціни й обʼєму | 0.089 | 0.299 | 2.499 |

Лічильник — таблиця пошуку по одному цілому числу, корзини по 10 подій,
без жодної фічі ціни. Мережа їй програє вдвічі за схожістю і втричі за
влучністю, а від константи не відрізняється зовсім. Це і є діагноз:
видача вироджена, і функція втрат тут ні до чого — вона чесно
мінімізується відмовою відповідати.

Заразом: **ентропія десятини при відомому елапсі — 1.393 ніт із 2.303**,
тобто час від початку ноги знімає 39 % невизначеності, а 61 % не
знімається ніколи. Довжина ноги відома лише заднім числом, і на події
`t` неможливо знати, чи це десятина 2 короткої ноги, чи десятина 1
довгої.

### Чи здатна мережа перевчити маленький шматок

[`scripts/overfit_check.py`](scripts/overfit_check.py). Стандартна
перевірка: дати шматок, який мережа фізично може запамʼятати, і вчити
доти, доки втрата не впаде майже в нуль. Перевчила — код і градієнт
справні. Не перевчила — баг у самому навчанні, і ручки втрати ні до
чого.

Шматок: перші 2 000 подій потоку train `BTC_USDT_29`, з міткою 1 860,
`2026-02-14 17:58` … `2026-02-22 15:49`. Оцінка — на тому самому
шматку, навмисно: тут міряється здатність запамʼятати, а не узагальнити.
60 епох, `lr 3e-4`, seed 0.

| Прогін | Втрата 1 → 60 | Схожість | Влучність | Значень видачі | Ворота min | Перевчила |
| ------ | ------------- | -------: | --------: | -------------: | ---------: | --------- |
| `ентропія` | 2.350 → **0.146** | **0.942** | **0.824** | 9 | 0.352 | епоха 52 |
| `ентропія+лічильник` | 2.343 → 0.210 | 0.923 | 0.736 | 9 | 0.513 | епоха 52 |
| `повна` | 34.56 → 4.223 | 0.833 | 0.362 | 8 | 0.574 | **ні** |

### Висновки

1. **Бага в навчанні немає.** Під чистою ентропією мережа перевчила
   шматок: втрата впала в 16 разів, схожість `0.942`, влучність `0.824`,
   видача набрала 9 різних значень. Усічений BPTT, мʼякі ворота і
   обнулення памʼяті працюють — ворота дійшли до `0.352`, тобто памʼять
   реально стирається.
2. **Втрата з памʼяттю про час заважає.** Та сама мережа, той самий
   шматок, той самий seed: під повною втратою схожість `0.833` замість
   `0.942`, влучність `0.362` замість `0.824` — удвічі гірше. Шматок,
   який ентропія вивчила напамʼять, повна втрата не вивчила за 60 епох.
   Три перероблення втрати поспіль тягнули не в той бік.
3. **Плато оманливе.** До 25-ї епохи схожість повзла `0.321 → 0.471` і
   виглядала як зупинка — на епохах 1–5 вона стояла на `0.32`. Далі
   пішов різкий підйом до `0.94`. Критерій `PATIENCE = 2` обривав
   навчання рівно в цій фазі.
4. **Лічильник напамʼять не допомагає** — на шматку 2 000 подій він
   трохи гірший (`0.923` проти `0.942`), і це очікувано: запамʼятати
   можна й без нього. Його користь — в узагальненні, і міряти її треба
   на валідації проти рівня `0.550`, а не тут.
5. **Ворота `POWER = 4` глушили сигнал.** При `p₁₀ = 0.1` вони
   дорівнюють `0.9999`, а похідна по `p₁₀` — `−0.004`. Лінійні ворота
   `1 − p₁₀` пропускають градієнт у повну силу; у перевчанні вони
   закрились до `0.35`, чого не було жодного разу на повному ряді.

### Повний ряд під тим, що виграло

Чиста ентропія виграла на шматку, тому повний ряд пішов під нею: `late = ahead = back = jump = 0`, ворота `POWER 1`, лічильник
увімкнений, терпіння 4 замість 2.

| Епоха | train | val | Схожість | Ворота min | Значень видачі | Макс класу |
| ----- | ----: | --: | -------: | ---------: | -------------: | ---------: |
| 1 | 2.3448 | 2.3150 | **0.3291** | 0.8268 | 2 | 6.0 |
| 2 | 2.3190 | 2.3185 | 0.3261 | 0.8148 | 2 | 6.0 |
| 3 | 2.3096 | 2.3357 | 0.3089 | 0.7901 | 2 | 6.0 |
| 4 | 2.2987 | 2.3851 | 0.2872 | 0.7092 | 2 | 6.0 |
| 5 | 2.2858 | **2.4290** | 0.2762 | 0.6393 | 4 | 7.0 |

Зупинка за схожістю на 5-й епосі. Найкраща епоха — **перша**.

6. **На повному ряді ентропія не рятує.** `train` падає `2.345 → 2.286`,
   `val` росте `2.315 → 2.429`, схожість падає `0.329 → 0.276` — нижче
   за константу. Мережа запамʼятовує train і не узагальнює.
7. **Ворота полагоджені, стеля та сама.** `ворота min` дійшли до
   `0.639` — памʼять стирається дедалі сильніше, — але власних обнулень
   так само `0`, максимум класу `7.0`, різних значень видачі `4` на
   3 800 подій. Отже справа не у воротах і не у втраті: обидва
   полагоджені, а результат не зрушив.
8. **Різниця між перевчанням і повним рядом — це різниця між
   запамʼятати й узагальнити.** На 2 000 подій мережа дійшла до `0.942`,
   бо вивчила ті конкретні ноги напамʼять. На 336 471 події
   запамʼятати неможливо, а узагальнити не виходить.

### Скільки сигналу є у самих фічах

[`scripts/signal_check.py`](scripts/signal_check.py). Останнє питання,
якого ще ніхто не ставив: **чи є в 60 фічах події взагалі інформація
про десятину ноги**. Перевіряється без памʼяті, без уваги і без
обнулень — звичайним двошаровим перцептроном подія → десятина,
розбиття за ногами (нога цілком в одній вибірці), повне перемішування,
15 епох.

| Модель | Що бачить | Вхід | Влучність | Схожість | Відстань |
| ------ | --------- | ---: | --------: | -------: | -------: |
| `лічильник` | тільки подій від початку ноги | 1 | 0.272 | **0.5625** | 1.755 |
| `фічі` | тільки 60 фіч ціни й обʼєму | 60 | 0.107 | **0.2927** | 3.159 |
| `фічі+лічильник` | і те, і те | 61 | 0.262 | 0.5452 | 1.806 |
| випадковість | — | — | 0.100 | 0.300 | — |

9. **У фічах події сигналу про десятину немає.** Сама по собі подія дає
   `0.107` влучності й `0.2927` схожості — це рівно випадковість. Її
   втрата за 15 епох майже не зрушила: `2.302 → 2.273` проти
   `2.139 → 1.911` у лічильника.
10. **Фічі не додають нічого понад час.** `фічі+лічильник` дають
    `0.5452` проти `0.5625` в одного лічильника — трохи **гірше**: 60
    колонок заходять як шум і псують те, що дає одне число.
11. **Отже справа не в мережі.** Ані функція втрат, ані ворота, ані
    памʼять, ані критерій зупинки ні до чого: у даних, які подаються на
    вхід, відповіді на поставлене питання немає. Найкраще, що можна
    витягти з `клас210х14х4ц` на цій цілі, — `0.5625` схожості, і це
    дає таблиця пошуку по одному цілому числу.
12. **Причина — в самій цілі.** Десятина визначається **повною
    довжиною ноги**, а вона відома лише заднім числом. На події `t`
    неможливо відрізнити десятину 2 короткої ноги від десятини 1
    довгої: медіана ноги 413 подій, чверть коротші за 221, чверть
    довші за 738, найдовша 3 000. Ентропія десятини при відомому
    елапсі — `1.393` ніт із `2.303`: час знімає 39 % невизначеності,
    решта не знімається нічим.

## Розділ 6. Чотири горизонти замість десятини · `підтверджено`

Розділ 5 закінчився діагнозом: у фічах події немає інформації про
десятину ноги, і жодна архітектура її там не знайде. Тут питання до
даних змінюється на таке, на яке вони відповідають.

### Чому питання інше

Десятина визначається **повною довжиною ноги**, відомою лише заднім
числом. «Півот близько» — локальна властивість ціни, і її видно з
самої події. Той самий перцептрон без памʼяті, на тих самих фічах і тих
самих ногах ([`scripts/hazard_check.py`](scripts/hazard_check.py)):

| Горизонт | Час | Частка мітки | AUC фіч | AUC лічильника | AUC разом |
| -------- | --- | -----------: | ------: | -------------: | --------: |
| 5 подій | 18 хв | 0.009 | 0.644 | 0.537 | 0.674 |
| **10 подій** | **35 хв** | 0.018 | 0.655 | 0.540 | **0.687** |
| 20 подій | 1.2 год | 0.036 | **0.658** | 0.539 | 0.685 |
| 40 подій | 2.3 год | 0.072 | 0.631 | 0.530 | 0.653 |
| 80 подій | 4.7 год | 0.142 | 0.604 | 0.518 | 0.624 |
| 160 подій | 9.3 год | 0.274 | 0.577 | 0.477 | 0.584 |

Ролі помінялись місцями. На десятині все давав лічильник (`0.5625`
схожості проти `0.2927` у фіч); на цій цілі працює ціна, а лічильник
майже нічого не додає (`0.540`). Сигнал найсильніший на **короткому**
горизонті: що ширше вікно, то більше в мітку потрапляє середини ноги.

### Чотири виходи

Мережа каже чотири ймовірності — не чотири відповіді, а **одну криву**,
функцію розподілу того, скільки подій лишилось до кінця ноги:

```
p₁₀ = P(лишилось ≤ 10)     p₄₀ = P(лишилось ≤ 40)
p₁₆₀ = P(лишилось ≤ 160)   p₆₄₀ = P(лишилось ≤ 640)
```

Події вкладені, тому крива не має права спадати. Це вбудовано в саму
мережу ([`scripts/hazard_net.py`](scripts/hazard_net.py)):

```
h_k = сигмоїда(логіт_k)                 умовний ризик корзини k
F_1 = h_1
F_k = F_{k−1} + (1 − F_{k−1}) · h_k     k = 2, 3, 4
```

`F_k` за побудовою лежить у `[0, 1]` і не спадає. Кожен логіт має свій
сенс: `h_k` — ймовірність кінця в корзині `k`, **якщо нога дожила** до
її початку.

### Навіщо четвертий горизонт

Три горизонти лишають дірку: `73 %` подій падають у хвіст «лишилось
понад 160», де оцінка стискається в одне число на всіх. Стеля методу
при **ідеальних** ймовірностях:

| Горизонти | Влучність десятини | Схожість | Подій у хвості |
| --------- | -----------------: | -------: | -------------: |
| 10 · 40 · 160 | 0.399 | 0.765 | 73 % |
| **10 · 40 · 160 · 640** | 0.508 | **0.951** | 26 % |
| 10 · 40 · 160 · 400 · 900 | 0.613 | 0.992 | 15 % |

Один додатковий вихід піднімає стелю схожості з `0.765` до `0.951`.
Далі приріст уже не вартий ще однієї голови.

### Де я в нозі

Різниці сусідніх точок дають масу в корзинах, маса на середнє
«лишилось» усередині корзини дає очікуване лишилось:

```
m₁ = F₁   m₂ = F₂ − F₁   m₃ = F₃ − F₂   m₄ = F₄ − F₃   m₅ = 1 − F₄

лишилось ≈ 4.5·m₁ + 24.5·m₂ + 98.1·m₃ + 359.7·m₄ + 1129.0·m₅
```

Середні по корзинах рахуються **тільки на train** і лежать у звіті
прогону. Далі довжина ноги оцінюється, а не вгадується:

```
пройдено = подій від останнього обнулення     ← лічильник, точно
довжина  ≈ пройдено + лишилось
десятина = 1 + ⌊10 · пройдено / довжина⌋
```

Жодного знання з майбутнього. Десятина перестає бути міткою, яку треба
вгадати, і стає наслідком причинної оцінки.

**Приклад.** Мережа сказала `p₁₀ = 0.02`, `p₄₀ = 0.10`, `p₁₆₀ = 0.45`,
`p₆₄₀ = 0.80`, лічильник показує 120 подій від обнулення. Маси
`0.02 · 0.08 · 0.35 · 0.35 · 0.20`, лишилось ≈ 386, довжина ≈ 506,
десятина = `1 + ⌊10·120/506⌋` = **3**.

### Функція втрат

Проста двійкова ентропія на кожен горизонт, зважена рідкістю мітки:

```
L = Σ_k L_k / 4
L_k = −( w_k · y_k · ln F_k + (1 − y_k) · ln(1 − F_k) )

y_k = [лишилось < n_k]      w_k = 54 · 13 · 3 · 0.4
```

Жодних штрафів за сходинку, пониження чи затримку: розділ 5 заміряв, що
вони гальмують навчання. Монотонність, заради якої вони колись
зʼявились, тут забезпечена побудовою кривої.

Вага додатного класу пишеться руками:
`torch.nn.functional.binary_cross_entropy(weight=…)` множить обидва
класи однаково і рідку мітку не піднімає.

### Калібрування виходу

Втрата множить додатний клас на `w`, тому її оптимум зсунутий угору.
Для події з базовою частотою `q` найкраща відповідь — не `q`, а

```
p* = w·q / (w·q + 1 − q)
```

Підставляємо базу `q = 0.0182` і `w = 54`: `p* = 0.497`. **Половина на
виході означає звичайну подію, а не півот.** Заміряно на val: медіана
`p₁₀` = 0.332, 95-й процентиль 0.534, максимум 0.871 — тобто поріг 0.5
перетинали 212 подій із 3 800 при шести справжніх півотах.

Зворотне перетворення повертає шкалу, на якій число означає написане:

```
q = p / ( w·(1 − p) + p )
```

Перевірка: `p = 0.5`, `w = 54` дає `q = 0.018` — точно базову частоту.

### Обнулення

По короткому горизонту, від **каліброваної** ймовірності. Мʼякі ворота
стирають памʼять пропорційно тому, наскільки `q` перевищує базову
частоту цього горизонту; жорсткий розрив — коли згладжене по ЕМА10 `q`
перейшло поріг. Стеля `CAP = 1500` лишається запобіжником і рахується
окремо.

```
ворота = 1 − (q − база) / (1 − база),   обрізано в [0, 1]
```

Відлік від бази, а не від нуля, прибирає залежність від горизонту:
**звичайна подія памʼять не чіпає** ні на 10 подіях, ні на 100, а
стирає її тільки те, що вище за звичайне. При базі `0.018` формула
збігається зі старою `1 − q` з точністю до 1.8 %; при базі `0.19`
стара множила б стан на `0.81` щокроку і памʼять помирала б за десять
кроків.

Поріг задається **кратністю базовій частоті**, а не числом:

```
поріг = LIFT × базова частота = 10 × 0.0182 = 0.182
```

Абсолютне значення залежить від горизонту й від монети, а «рвати
памʼять, коли модель упевнена вдесятеро сильніше, ніж буває на
випадковій події» — не залежить.

### Чому мережа з памʼяттю програвала перцептрону без неї

Замір без памʼяті дав `AUC 0.687`, та сама задача в LSTM з увагою —
`0.531`. Причини дві, обидві в побудові кроку.

**1. Ворота стирали памʼять щокроку.** Вони бралися від **зваженої**
ймовірності, а вона для середньої події дорівнює `0.5`. Стан множився
на `0.65` на кожному кроці: за десять кроків від нього лишалось
`0.65¹⁰ ≈ 0.013`. Памʼять не доживала до наступної події. Ворота від
каліброваної `q` з відліком від бази дають на середній події рівно
`1.0`.

**2. Голова була лінійна.** Перцептрон, який дав `0.687`, мав два шари
по 128 із `ReLU` над сирими фічами. Голова мережі — один `Linear` над
виходом LSTM, тобто на подію припадало **менше** нелінійності, ніж у
базового рівня, який вона мала побити.

Обидві правки в `HazardNet` ([`scripts/hazard_net.py`](scripts/hazard_net.py)):
ворота від `q` і **прямий шлях сирої події в голову**. Голова бачить
`[вихід LSTM, увага, сама подія]` і не може виявитись гіршою за
перцептрон без памʼяті: у неї є все, що було в нього, і памʼять зверху.
Ваг стало 86 596 замість 45 764.

### Як міряється: AUC

Влучність на цій цілі не міряє нічого: мітка `p₁₀` стоїть на 1.8 %
подій, тому модель, яка завжди каже «ні», має 98.2 %. Питання інше —
**чи ставить модель подію з міткою вище за подію без неї**:

```
AUC = ( Σ_{i ∈ додатні} rank_i − P·(P+1)/2 ) / ( P · N )
```

- `rank_i` — місце події у списку, впорядкованому за виданою оцінкою
  від меншої до більшої, рахуючи з 1;
- `P` — скільки подій із міткою, `N` — скільки без неї.

Це рівно ймовірність того, що навмання взята подія з міткою дістане
вищу оцінку, ніж навмання взята подія без неї. `0.5` — підкидання
монети, `1.0` — ідеальний розділ, `0.0` — ідеальний навпаки. Віднімання
`P·(P+1)/2` прибирає ранги, які додатні події займають одна щодо одної:
рахується тільки те, скільки разів подія з міткою обійшла подію без
неї.

### Скільки епох

Стелі немає: прогін іде, **поки росте схожість**, і зупиняється після
`PATIENCE = 3` епох підряд без нового максимуму. У звіт і у збережені
ваги йде **найкраща** епоха, а не остання: після максимуму `val` уже
росте.

### Дані

Ті самі, що в розділі 4: `клас210х14х4ц`, шість монет одним потоком,
розбиття за датою на межі `2026-08-14 15:27` UTC, train 336 471 подія,
val 3 800. Змінилась лише мітка: замість десятини — чотири нулі й
одиниці на кожній події.

## Розділ 7. Один горизонт: півот за 100 подій · `підтверджено`

**Ціль.** Звузити питання до одного числа й подивитись, скільки в ньому
лишається сигналу. Мережа, потік, втрата й обнулення ті самі, що в
розділі 6 — міняється тільки ширина видачі: замість чотирьох
ймовірностей одна, `P(півот у наступні 100 подій)`.

### Що саме змінилось

| | Розділ 6 | Розділ 7 |
| -- | -------- | -------- |
| горизонти | 10 · 40 · 160 · 640 | **100** |
| виходів голови | 4 | **1** |
| крива розподілу | чотири точки | вироджена в точку |
| виведена десятина | з чотирьох мас | вироджена: дві корзини |
| найкраща епоха за | схожістю десятини | **AUC** |

Горизонт `100` подій — це `100 × 210 с = 5.8 год`. Мітка на ньому
стоїть значно частіше, ніж на `p₁₀`, і це змінює дві речі.

### Чому довелось міняти ворота

Ворота `1 − q` прив'язані до горизонту, і на `100 подіях` вони ламають
мережу. Базова частота там близько `0.19` замість `0.018`: середня
подія множила б стан на `0.81`, за десять кроків від памʼяті лишалось
би `0.12`. Це рівно та поломка, яку розділ 6 уже ловив і виправляв
калібруванням.

Виправлення — рахувати ворота від **перевищення** над базовою
частотою, а не від самої ймовірності:

```
ворота = 1 − (q − база) / (1 − база),   обрізано в [0, 1]
```

Тепер звичайна подія не чіпає памʼять **на будь-якому горизонті**, а
стирає її тільки те, що вище за звичайне. На базі `0.018` формула
збігається зі старою з точністю до 1.8 %, тому розділ 6 від неї не
змінюється.

### Чому найкраща епоха вибирається за AUC

Виведення десятини лишається тим самим, що в розділі 6:

```
десятина = 1 + ⌊10 · пройдено / (пройдено + лишилось)⌋
```

але при одному горизонті корзин дві — із середнім «лишилось» `48.8` і
`577.7` подій, — тому оцінка може бути тільки відрізком між ними.
Заміряно на val: `249 … 409` подій, медіана `336`. Знаменник майже не
рухається, і десятина виходить практично цілком із **пройденого**, а
не з видачі мережі: це лічильник із фіксованим знаменником.

| | Схожість | Влучність |
| -- | -------: | --------: |
| константа | 0.3008 | 0.0999 |
| **один горизонт** | **0.3837** | 0.171 |
| лічильник (таблиця) | 0.5501 | 0.2812 |
| чотири горизонти | 0.5703 | — |

Гірше за звичайний лічильник, тобто міряти цим числом тут нічого.
Питання до однієї ймовірності одне — **чи ставить вона подію перед
півотом вище за звичайну**, тобто AUC.

### Метод

1. Той самий потік: шість монет до межі `2026-08-14 15:27` UTC у train,
   BTC після межі у val; `клас210х14х4ц`, 61 фіча з лічильником.
2. Мітка на події — одна: `лишилось < 100`.
3. Втрата — зважена двійкова ентропія, вага додатного класу з train.
4. Ворота від перевищення над базою, поріг розриву — `LIFT × база`.
5. Зупинка — `PATIENCE = 3` епохи підряд без нового максимуму AUC,
   у ваги йде найкраща епоха.

### Результат

Прогін 54.7 хв, 86 209 ваг, зупинка на шостій епосі.

| Епоха | train | val | AUC p₁₀₀ | Схожість | Ворота min |
| ----- | ----: | --: | -------: | -------: | ---------: |
| 1 | 1.1450 | 1.1085 | 0.502 | 0.3812 | 0.9949 |
| **2** | 1.1346 | 1.0886 | **0.604** | 0.3837 | 0.8981 |
| 3 | 1.1101 | **1.0804** | 0.604 | 0.3793 | 0.7926 |
| 4 | 1.0990 | 1.0982 | 0.571 | 0.3927 | 0.8288 |
| 5 | 1.0844 | 1.0985 | 0.579 | 0.3697 | 0.7549 |
| 6 | 1.0643 | 1.0962 | 0.577 | 0.3793 | 0.7542 |

Базова частота мітки `0.1772`, вага додатного класу `4.6`, середнє
«лишилось» по двох корзинах `48.8` і `577.7` подій.

### Висновки

1. **Сигнал є, і він менший, ніж на короткому горизонті.** `AUC 0.604`
   проти `0.633` у чотиригоризонтної моделі на `p₁₀`. Мітка тут стоїть
   на 17.7 % подій замість 1.8 %, тобто питання значно легше — і саме
   тому відповідь на нього менш інформативна: «півот десь у наступні
   5.8 год» справджується майже на кожній п'ятій події.

2. **Перша епоха дала рівно монету** (`0.502`). Сигнал з'явився на
   другій і одразу вперся: епохи 3–6 його не побили, `val` з четвертої
   росте при `train`, що падає. Стеля цієї постановки — `0.604`.

3. **Ворота зі старою формулою зламали б прогін.** Мінімум воріт за
   першу епоху `0.9949` — памʼять проходить цілою. Просте `1 − q` при
   базі `0.1772` множило б стан середньої події на `0.82` щокроку.

4. **Модель почала різати потік сама.** Мінімум воріт пішов
   `0.9949 → 0.8981 → 0.7926`: вона знайшла місця, де впевненість
   помітно вище за базові 17.7 %, і памʼять там притискає. Але це
   найсильніше стирання **однієї** події за епоху, а не міра якості:
   на епосі 5 мінімум `0.7549` при AUC, що вже впав.

5. **Своїх обнулень нуль.** Поріг `LIFT × база = 10 × 0.1772 = 1.772`
   обрізається до `0.99`, а калібрована ймовірність на val не піднімає
   вище `0.2610`:

   | | `q` на val |
   | -- | ---: |
   | медіана | 0.1534 |
   | 95-й процентиль | 0.2025 |
   | 99-й | 0.2302 |
   | максимум | **0.2610** |

   Максимум перевищує базу лише в 1.47 раза. Кратність 10 на частій
   мітці недосяжна за побудовою: щоб `q` дійшло до `0.99` при базі
   `0.18`, модель мала б бути практично впевненою — а на такому
   горизонті впевненості такого рівня в даних немає.

   **І це ще не вся причина.** Поріг порівнюється не з `q`, а з її
   згладженням по ЕМА10 — саме воно рве памʼять. Згладження зрізає
   піки:

   | | сира `q` | згладжена ЕМА10 |
   | -- | -------: | --------------: |
   | медіана | 0.1534 | 0.1533 |
   | 99-й процентиль | 0.2302 | 0.2167 |
   | максимум | 0.2610 | **0.2309** |

   Скільки разів поріг узагалі перетинається на 3 800 подіях val:

   | Кратність | Поріг | Сирою `q` | Згладженою |
   | --------- | ----: | --------: | ---------: |
   | 1.1 | 0.1949 | 273 | 184 |
   | 1.2 | 0.2126 | 109 | 55 |
   | 1.3 | 0.2304 | 38 | **4** |
   | 1.4 | 0.2481 | 6 | **0** |

   Тобто працездатний діапазон кратності — `1.1 … 1.2`, і на ньому
   обнулення стається 55–184 рази при **шести** справжніх півотах.
   Порогу, при якому обнулення і рідке, і влучне, у цієї моделі
   немає взагалі: її впевненість надто рівна.

6. **Епохи 2 і 3 нерозрізненні.** AUC однаковий до трьох знаків, у
   третьої `val` навіть нижчий. Вибір вирішило строге «більше» при
   рівності, тобто округлення. Зберігаються ваги тільки найкращої, тому
   порівняти їх напряму можна лише новим прогоном.

## Розділ 8. Грід ручок на `p₁₀₀` · `підтверджено`

**Ціль.** Розділ 7 дав `AUC 0.604` однією конфігурацією — тією, що
дісталась від чотиригоризонтної моделі. Питання одне: **чи впирається
результат у постановку, чи в ручки**. Якщо жодна конфігурація не
перебиває базову, стеля належить даним, а не мережі.

### Чому на урізаному train

Повний прогін — 55 хв, гріду з тридцяти прогонів на нього не вистачить
доби. Тому кожен прогін іде на **однаковому урізаному** шматку: перші
10 000 подій кожної монети (60 000 замість 336 471), 4 епохи, той самий
seed. Порівнюються конфігурації між собою на тому самому вході, а не з
повним прогоном.

Урізання чесне рівно настільки, наскільки порядок конфігурацій на
частині даних збігається з порядком на всіх. Це не гарантовано, тому
**базова конфігурація йде в грід теж**: її місце в таблиці показує, чи
зберігся масштаб чисел.

### Стадії

Повний перебір усіх ручок — тисячі прогонів. Грід іде стадіями: кожна
бере переможця попередньої і крутить дві нові ручки.

| Стадія | Що крутить | Значення | Прогонів |
| ------ | ---------- | -------- | -------: |
| `розмір` | шарів LSTM × ширина стану | 1·2·3 × 32·64·128 | 9 |
| `голова` | ширина голови × dropout | 64·128·256 × 0·0.1·0.3 | 9 |
| `навчання` | крок навчання × вікно градієнта | 1e-4·3e-4·1e-3 × 75·150·300 | 9 |
| `будова` | абляція архітектури | — | 6 |

Стадія `будова` — не пошук ручки, а **абляція**: кожен прогін вимикає
одну частину — ворота, увагу, лічильник — або міняє стелю памʼяті.
Падіння AUC каже, що частина працює; відсутність падіння — що вона не
потрібна. Переможця ця стадія не вибирає.

### Стадія «розмір»

| Прогін | AUC | Ваг |
| ------ | --: | --: |
| `hidden=32` | 0.509 | 48 225 |
| **база** (1 шар × 64) | 0.522 | 86 209 |
| `layers=2 · hidden=32` | 0.536 | 56 673 |
| `layers=3 · hidden=32` | 0.540 | 65 121 |
| `layers=3 · hidden=128` | 0.552 | 469 377 |
| `layers=2 · hidden=128` | 0.555 | 337 281 |
| `hidden=128` | 0.556 | 205 185 |
| `layers=3` | 0.558 | 152 769 |
| **`layers=2`** | **0.561** | 119 489 |

**Одного шару мало.** Він програє при кожній ширині: `0.509 · 0.522 ·
0.556` проти `0.536 · 0.561 · 0.555` у двох шарів. Другий шар дає
`+0.039` до бази — більше, ніж уся памʼять дає над мережею без неї
(розділ 9, `+0.022`).

Третій шар уже нічого не додає (`0.558` проти `0.561`), а ширина 128
при двох шарах навіть шкодить (`0.555`). Найкраще — **два шари по 64**,
119 489 ваг.

### Стадія «голова»

| Прогін | AUC |
| ------ | --: |
| `head=64 · dropout=0.3` | 0.541 |
| `head=256 · dropout=0.3` | 0.543 |
| `head=256 · dropout=0.0` | 0.547 |
| `head=64 · dropout=0.0` | 0.549 |
| `head=64` | 0.553 |
| `dropout=0.0` | 0.553 |
| `head=256` | 0.555 |
| `dropout=0.3` | 0.555 |
| **база голови** (`head=128 · dropout=0.1`) | **0.561** |

**Голову крутити нема сенсу.** Жоден із восьми варіантів базу не
перебив, розкид усього `0.020`. Обидві крайності dropout гірші за
`0.1`, обидві крайності ширини — за `128`. Переможець стадії
лишився той самий, `layers=2`.

### Стадія «навчання»

Усі прогони від `layers=2`, крок навчання × вікно градієнта.

| Крок | Вікно 75 | Вікно 150 | Вікно 300 |
| ---- | -------: | --------: | --------: |
| `1e-4` | 0.555 | 0.544 | 0.542 |
| **`3e-4`** | **0.575** | 0.561 | 0.549 |
| `1e-3` | 0.550 | 0.562 | 0.543 |

**Коротше вікно градієнта краще.** При базовому кроці `3e-4` вікно 75
дає `0.575` проти `0.561` у 150 і `0.549` у 300 — і та сама залежність
тримається при `1e-4`. Крок навчання чіпати не треба: `3e-4`
виграє при кожному вікні, крім 150.

Вікно — це на скільки кроків назад іде навчання (truncated BPTT).
Що воно **коротше** тим краще, означає одне: залежності, які модель
справді ловить, короткі. На 75 подіях (4.4 год) сигнал є, на 300
(17.5 год) градієнт уже переважно шум.

### Стадія «будова» — абляція

Усі прогони від `layers=2 · window=75`, кожен вимикає одну частину.

| Прогін | AUC | Проти бази |
| ------ | --: | ---------: |
| без лічильника | 0.535 | **−0.040** |
| без уваги | 0.547 | **−0.028** |
| стеля 3000 | 0.553 | −0.022 |
| без воріт | 0.572 | −0.003 |
| **база стадії** | 0.575 | — |
| **стеля 500** | **0.595** | **+0.020** |

1. **Ворота не роблять нічого.** `−0.003` — це нуль. Мʼяке стирання
   памʼяті, від якого залежала вся конструкція обнулення і на
   виправлення якого пішов розділ 7, на результат не впливає. Разом з
   розділом 11, де жорсткий поріг або не спрацьовує, або спрацьовує
   каскадом, висновок один: **механізм обнулення в цій постановці не
   працює ні мʼяко, ні жорстко.**

2. **Коротша памʼять краща.** Стеля 500 подій дає `+0.020`, стеля 3000
   забирає `−0.022`. Разом із вікном градієнта 75 це те саме
   твердження, заміряне двічі різними способами: **корисний контекст
   тут короткий**, приблизно 500 подій — 29 годин — і все, що далі,
   тільки заважає.

3. **Увага потрібна** (`−0.028` без неї) — і вона єдина частина
   архітектури, яка справді працює.

4. **Лічильник потрібен** (`−0.040` без нього) — і це прямо
   суперечить розділу 9, де без памʼяті він шкодив (`0.5806` з ним
   проти `0.5823` без). Різниця в тому, що там він був окремою
   пилкою, а тут мережа має чим його зіставити: стан LSTM пам'ятає,
   що було на початку відрізка, і лічильник каже, як давно це було.

### Найкраща конфігурація

`layers=2 · window=75 · cap=500` — **`0.595`** проти `0.522` у бази,
`+0.073` на урізаному train. Два шари по 64, голова 128 із dropout 0.1,
крок `3e-4`, увага і лічильник на місці.

### Що довелось додати в мережу

- `layers` — кілька шарів LSTM один над одним. Стан `zero()` став
  `(шарів, 1, ширина)`: інакше другий шар починав би кожну ногу з
  чужого стану.
- `attend` — увагу можна вимкнути. Голова тоді бачить
  `[вихід LSTM, сама подія]` і стає вужчою рівно на ширину стану.
- `gate` у прогоні — ворота можна вимкнути, щоб заміряти, чи вони
  взагалі допомагають.

За замовчуванням усе лишилось як було: один шар, увага й ворота
ввімкнені, `86 209` ваг — розділи 6 і 7 не зсуваються.

## Розділ 9. Чи потрібна памʼять на `p₁₀₀` · `підтверджено`

**Ціль.** Розділ 7 дав `AUC 0.604` мережею з памʼяттю. Розділ 6 міряв
перцептрон без памʼяті й отримав на сусідніх горизонтах `0.624` (N=80)
і `0.584` (N=160), а прямий замір на N=100 дав `0.612`. Числа
співмірні, і напрошується висновок «памʼять не додає нічого».

**Порівнювати їх не можна.** Замір без памʼяті йде по **ногах** на всіх
336 554 подіях шести монет; модель з памʼяттю — по **даті**, і val у неї
3 800 подій самого BTC. Різні вибірки — різні числа.

### Метод

Той самий потік, те саме розбиття за датою на межі `2026-08-14 15:27`
UTC, та сама мітка `лишилось < 100`, та сама вага класу `4.6`, той
самий val — і мережа **без жодної памʼяті**: подія на вхід, ймовірність
на вихід. Два шари по 128 із `ReLU`, 30 епох, `lr = 1e-3`, seed 0.

Лічильник подається **детермінованою пилкою** з періодом `CAP = 1500`.
У прогоні з памʼяттю обнулень на val було рівно два, обидва по стелі,
своїх — жодного; отже модель бачила саме пилку, і базовий рівень має
бачити її ж.

### Результат

| Вхід | Чисел | AUC p₁₀₀ |
| ---- | ----: | -------: |
| фічі | 60 | **0.5823** |
| лічильник | 1 | 0.4553 |
| фічі + лічильник | 61 | 0.5806 |
| **з памʼяттю** (розділ 7) | 61 | **0.604** |

### Висновки

1. **Памʼять дає `+0.022` AUC** — 0.604 проти 0.5823. Внесок є, але
   малий: LSTM, увага, ворота і 86 209 ваг піднімають результат на
   стільки ж, скільки коштує одна зайва епоха шуму.

2. **Лічильник сам по собі — `0.4553`, тобто нижче за випадковість.**
   На цьому val пилка «подій від обнулення» веде в протилежний бік. Це
   не дивно: обнулення там по стелі памʼяті, а не по півотах, тому
   пилка з ціллю не пов'язана взагалі — а куди саме її зсуне на
   3 800 подіях, вирішує випадок.

3. **Лічильник у вході шкодить.** `0.5806` з ним проти `0.5823` без
   нього. Він потрапив у фічі з попередніх розділів, де обнулення були
   змістовними; тут його місце під питанням, і абляція гріду
   (`counter=False`) міряє те саме на мережі з памʼяттю.

4. **Цифра розділу 6 була завищена.** Прямий замір без памʼяті на N=100
   по ногах дав `0.612`, на розбитті за датою — `0.5823`. Різниця
   `0.03` — це ціна розбиття, а не властивість моделі. Усе, що
   порівнюється з `0.604`, має міряти той самий val.

## Розділ 10. Готова модель на всіх шести монетах · `підтверджено`

**Ціль.** Val у розділах 7–9 — це 3 800 подій самого BTC і **шість**
півотів. На такій вибірці одне число AUC мало що каже, а все, що
рахується по півотах, не рахується взагалі.

Після межі `2026-08-14 15:27` UTC дані є не тільки в BTC: решта пʼяти
монет дає ще 25 070 подій, яких у train не було — train обрізаний тією
самою датою. Разом **28 870 подій і 63 півоти**, у сім разів більше.

Навчання тут не йде: береться **готова** модель розділу 7 (епоха 2,
86 209 ваг) і проганяється по кожній монеті окремо.

### Результат

| Монета | Подій | Мітка | Півотів | AUC p₁₀₀ |
| ------ | ----: | ----: | ------: | -------: |
| BNB | 5 014 | 0.1084 | 5 | 0.5620 |
| **BTC** | 3 800 | 0.1644 | 6 | **0.6037** |
| ETH | 5 014 | 0.0867 | 4 | 0.7119 |
| SOL | 5 014 | 0.1789 | 9 | 0.7221 |
| DOGE | 5 014 | 0.2540 | 14 | 0.8260 |
| XRP | 5 014 | 0.3708 | 25 | 0.8342 |
| **середнє по монетах** | | | | **0.7100** |
| спільне (усі події в один список) | 28 870 | 0.2130 | 63 | 0.7615 |

### Висновки

1. **`0.604` був найгіршим із шести чисел, а не типовим.** Середнє по
   монетах — `0.710`. Модель працює помітно краще, ніж показував
   вузький val.

2. **BTC — єдина монета із заниженою оцінкою через відбір.** Найкраща
   епоха вибиралась саме за AUC на BTC, тобто його число має відбір
   на собі. Решта пʼяти в цьому не брали участі й дають `0.7312`
   середнього. Тобто відбір по BTC не тільки нічого не завищив — він
   зупинив навчання за найгіршою з шести монет.

3. **AUC росте з частотою мітки.** BNB `0.1084 → 0.5620`, XRP
   `0.3708 → 0.8342`. Це очікувано і це **не заслуга моделі**: чим
   більше півотів у вікні, тим менша частка подій припадає на довгий
   хвіст «до кінця ще далеко», де оцінка стискається в одне число на
   всіх. Порівнювати монети між собою за AUC через це не можна —
   порівнюється тільки монета сама з собою.

4. **Спільне число `0.7615` вище за середнє `0.7100`, і це артефакт.**
   Наскрізне ранжування 28 870 подій з різних монет частково міряє
   різницю **між** монетами: у XRP мітка стоїть на 37 % подій, у ETH
   на 8.7 %, і високі оцінки на XRP автоматично опиняються серед
   додатних. Основне число тут — середнє по монетах.

5. **Своїх обнулень нуль на всіх шести монетах.** Поріг `0.99`
   недосяжний ніде, не тільки на BTC. Механізм обнулення за власною
   видачею жодного разу не спрацював за все дослідження.

## Розділ 11. Досяжний поріг обнулення · `підтверджено`

**Ціль.** Механізм, заради якого будувалась уся архітектура, — модель
сама вирішує, де кінець ноги, і стирає памʼять, — за розділи 7–10 не
спрацював **жодного разу**. Поріг `LIFT = 10` на частій мітці
недосяжний за побудовою. Тут він опущений до `1.3` — на межу того, що
згладжена ймовірність узагалі досягає, — і питання одне: **що
станеться з навчанням, коли обнулення почне спрацьовувати**.

### Дані й ручки

Ті самі, що в розділі 7. Змінена одна: `--lift 1.3`, тобто поріг
`0.1772 × 1.3 = 0.230` каліброваної ймовірності проти `0.99` раніше.

### Результат

| Епоха | train | val | AUC p₁₀₀ | Схожість | Своїх обнулень | Ворота min |
| ----- | ----: | --: | -------: | -------: | -------------: | ---------: |
| 1 | 1.1441 | 1.1017 | 0.557 | 0.3713 | 0 | 0.9889 |
| 2 | 1.1372 | 1.1245 | 0.491 | 0.4020 | 0 | 0.9820 |
| 3 | 1.1447 | 1.1066 | 0.564 | 0.3686 | 0 | 0.9880 |
| 4 | 1.1362 | 1.1412 | 0.471 | 0.4042 | 0 | 0.9539 |
| 5 | 1.1297 | 1.1120 | 0.562 | 0.1872 | **61** | 0.7932 |
| **6** | 1.1254 | **1.0462** | **0.703** | 0.4059 | 0 | 0.9663 |
| 7 | 1.1146 | 1.1057 | 0.552 | 0.1842 | **250** | 0.8169 |
| 8 | 1.1126 | 1.1230 | 0.561 | 0.4135 | 3 | 0.8830 |
| 9 | 1.1238 | 1.1218 | 0.547 | 0.1913 | **63** | 0.7578 |

### Висновки

1. **Навчання стало двостійким.** Епохи розпались на два режими, і
   третього немає:

   | Режим | Обнулень | Схожість | AUC |
   | ----- | -------: | -------: | --: |
   | обнулення не спрацьовує | 0–3 | 0.37 … 0.41 | 0.47 … 0.70 |
   | обнулення каскадом | 61 … 250 | 0.18 … 0.19 | 0.55 … 0.56 |

   Щойно модель стає впевненою настільки, щоб перетнути поріг, вона
   перетинає його **всюди**: 250 обнулень на 3 800 подій при шести
   справжніх півотах. Памʼять розсипається, схожість падає вдвічі, і
   наступна епоха відкочується назад.

2. **Найкраща епоха — з тих, де обнулення не спрацювало.** `AUC 0.703`
   на епосі 6 при нулі власних обнулень. Це найвище число за все
   дослідження на BTC, і воно **не** заслуга механізму обнулення.

3. **`val` на епосі 6 найнижчий за прогін** — `1.0462` проти `1.1017`
   на першій. Тобто `0.703` підтверджене другим, незалежним від
   ранжування числом, а не тільки стрибком AUC.

4. **Але AUC стрибає** — `0.557 · 0.491 · 0.564 · 0.471 · 0.562 ·
   0.703 · 0.552 · 0.561 · 0.547`. Розкид від 0.47 до 0.70 на тій
   самій моделі й тих самих даних. На val із 3 800 подій і шести
   півотів одне число нічого не вирішує; `0.703` треба перевіряти на
   широкій вибірці розділу 10.

5. **Опущений поріг не полагодив механізм, а показав, що він не
   працює.** Або обнулення немає взагалі, або його в сорок разів
   більше, ніж півотів. Проміжку немає, бо впевненість моделі надто
   рівна: між «звичайною» подією і «майже певний півот» у неї
   різниця в 1.47 раза.

### `0.703` виявився шумом

Модель цієї епохи прогнана по всіх шести монетах тим самим способом,
що й у розділі 10:

| Монета | Розділ 7 | Розділ 11 | Різниця |
| ------ | -------: | --------: | ------: |
| BTC | 0.6037 | **0.7017** | **+0.0980** |
| ETH | 0.7119 | 0.7711 | +0.0592 |
| BNB | 0.5620 | 0.5645 | +0.0025 |
| DOGE | 0.8260 | 0.7657 | −0.0603 |
| SOL | 0.7221 | 0.5301 | **−0.1920** |
| XRP | 0.8342 | 0.6200 | **−0.2142** |
| **середнє по монетах** | **0.7100** | **0.6589** | **−0.0511** |

Модель, яка на BTC краща на `+0.098`, у середньому по шести монетах
**гірша на 0.051**. Пік на BTC не переносився нікуди: на SOL і XRP
вона провалилась на дві десятих.

### Головний висновок: вибір епохи по одному BTC — помилка

Найкраща епоха всі розділи вибиралась за AUC на 3 800 подіях BTC із
шістьма півотами. На такій вибірці максимум AUC — це переважно шум, і
розділ 11 показав це в чистому вигляді: `patience` зупинив навчання на
епосі, яка виграла **випадково**.

Виправлення — рахувати епоху за середнім AUC **усіх шести монет** після
межі: 28 870 подій і 63 півоти замість 3 800 і 6. Це ключ
`--wide-val`, і підсумковий прогін розділу 12 іде вже з ним.

## Розділ 12. Переможець гріду на всіх даних · `підтверджено`

**Ціль.** Розділи 8 і 11 дали дві поправки: кращу конфігурацію ручок і
розуміння, що епоху не можна вибирати по одному BTC. Тут обидві
застосовані разом — на повному train і на обох постановках, один
горизонт і чотири.

**Ручки.** `layers 2 · window 75 · cap 500`, епоха за середнім AUC шести
монет (`--wide-val`). Для чотиригоризонтної моделі середнє береться ще й
по горизонтах — інакше вибір ішов би за одним `p₁₀`.

### Один горизонт: `p₁₀₀`

119 489 ваг, епоха 8 з 11, 80.5 хв.

| Монета | AUC p₁₀₀ | було в розділі 10 |
| ------ | -------: | ----------------: |
| BTC | 0.6463 | 0.6037 |
| ETH | 0.7190 | 0.7119 |
| SOL | 0.7411 | 0.7221 |
| XRP | 0.8392 | 0.8342 |
| BNB | 0.5550 | 0.5620 |
| DOGE | 0.8215 | 0.8260 |
| **середнє по монетах** | **0.7203** | **0.7100** |

### Чотири горизонти

119 876 ваг, епоха 5 з 8, 60.1 хв.

| Монета | p₁₀ | p₄₀ | p₁₆₀ | p₆₄₀ |
| ------ | --: | --: | ---: | ---: |
| BTC | 0.6022 | 0.5463 | 0.5463 | 0.7312 |
| ETH | 0.7557 | 0.7465 | 0.7285 | 0.8665 |
| SOL | 0.7905 | 0.7395 | 0.7038 | 0.7163 |
| XRP | 0.7769 | 0.8021 | 0.8457 | 0.8100 |
| BNB | 0.7149 | 0.6602 | 0.6112 | 0.7144 |
| DOGE | 0.7554 | 0.8052 | 0.8921 | 0.9541 |
| **середнє** | **0.7326** | **0.7166** | **0.7213** | **0.7988** |

Середнє по чотирьох горизонтах — **0.7423**. Це найкраще число
дослідження, і всі наступні розділи порівнюються з ним.

### Висновки

1. **Грід переніс менше, ніж обіцяв.** На урізаному train він давав
   `+0.073`; на шести монетах з повного train лишилось `+0.0103`
   (`0.7100 → 0.7203`). Ручки допомагають, але не в тому масштабі, в
   якому їх міряв грід.

2. **Чотири горизонти сильніші за один.** `0.7326` проти `0.7203` на
   тому самому `p₁₀`-подібному питанні. Додаткові виходи працюють як
   спільне навчання: дальній горизонт тримає ту саму памʼять, з якої
   читає ближній.

3. **Найдальший горизонт найлегший.** `p₆₄₀` дає `0.7988`, `p₄₀` —
   `0.7166`. «Нога скінчиться колись у наступні півтори доби» —
   питання про фазу ноги, а не про момент, і фаза видно.

4. **BTC — найгірша монета в обох прогонах.** `0.6022` проти `0.9541`
   у DOGE на `p₆₄₀`. Порівнювати монети між собою за AUC не можна
   (розділ 10, висновок 3), але сам факт, що навчання, звірене по
   шести монетах, лишає BTC позаду, стоїть.

5. **Своїх обнулень нуль.** На всіх монетах, обох прогонах, усіх
   горизонтах.

## Розділ 13. Детектор: півот у межах останніх N подій · `підтверджено`

**Ціль.** Питання розділів 6–12 — прогнозне: `лишилось < N`, «нога
скінчиться в наступні N подій». Тут воно дзеркальне: `пройдено < N`,
**«розворот уже стався, і не далі ніж N подій тому»**. Дві відстані
доповнюють одна одну: `лишилось + пройдено = довжина ноги` (перевірено
на подіях: `597 + 36 = 633`).

Дзеркальне питання мало б бути легшим: подія після півота вже **бачить**,
що ціна повернула, а подія перед ним мусить це вгадати.

**Горизонти.** `10 · 40 · 160 · 500`. Замість 640 узято 500: далі
починається довжина самої ноги, і мітка «півот був не далі ніж 640 подій
тому» стоїть майже скрізь.

### Скільки свідчень взагалі є в памʼяті

Мітка вимагає, щоб півот лежав **усередині** памʼяті: після примусового
обнулення по стелі мережа не бачить нічого раніше за нього. Частка
додатних подій, на яких півот справді в памʼяті
(`scripts/detector_check.py`, train, шість монет):

| Стеля | p₁₀ | p₄₀ | p₁₆₀ | p₅₀₀ |
| ----- | --: | --: | ---: | ---: |
| 500 | 99.5 % | 96.7 % | 83.9 % | 58.0 % |
| 1500 | 99.9 % | 98.8 % | 94.5 % | 85.1 % |

Тобто стеля обмежує тільки найдовший горизонт: для «півот був у
останні 10 подій» досить десяти подій памʼяті, і вони є майже завжди.

### Два прогони

| Прогін | Памʼять | Вікно градієнта | Епоха | Широке AUC |
| ------ | ------- | --------------: | ----: | ---------: |
| з обнуленням | стеля 1500 | 75 | 2 з 5 | 0.5745 |
| ковзна | не обнуляється | 500 | 3 з 6 | **0.6121** |

Другий прогін виправляє те, що в першому не сходилось: **вікно градієнта
має бути не коротшим за найдовший горизонт.** Для прогнозу це не так —
мітка про майбутнє, і градієнт на 75 кроків назад учить читати поточну
подію. Для детектора свідчення лежить **у памʼяті**, за 500 кроків
назад, і градієнт на 75 туди просто не дістає.

| Горизонт | з обнуленням | ковзна памʼять |
| -------- | -----------: | -------------: |
| p₁₀ | 0.5745 | **0.6121** |
| p₄₀ | 0.5952 | 0.5890 |
| p₁₆₀ | 0.5582 | 0.5282 |
| p₅₀₀ | 0.5465 | 0.5109 |

### Висновки

1. **Дзеркальне питання виявилось важчим, а не легшим.** `0.6121`
   проти `0.7423` у прогнозу на тих самих даних, тій самій мережі й
   тому самому розбитті.

2. **І це не брак памʼяті.** На стелі 1500, з якою йшов перший
   прогін, свідчення в памʼяті є на `99.9 %` подій `p₁₀` і на `85.1 %`
   подій `p₅₀₀`. Модель має що читати — вона цього не читає. Півот у
   фічах не позначений: щоб сказати «розворот був 30 подій тому», мережа
   мусить сама впізнати розворот по ціні й відлічити від нього кроки, і
   саме цей лічильник у неї не тримається.

3. **Довші горизонти гіршають, коротші кращають.** Ковзна памʼять
   підняла `p₁₀` на `+0.038` і опустила `p₅₀₀` на `−0.036`. Памʼять,
   яка не обнуляється, пливе: на короткому горизонті свіже свідчення
   перебиває дрейф, на довгому — ні.

4. **Своїх обнулень нуль в обох прогонах.**

## Розділ 14. Що в події видно перед кінцем ноги · `підтверджено`

**Ціль.** Мережа з памʼяттю дає `0.73` на `p₁₀`, мережа без памʼяті на
тих самих фічах — `0.58` (розділ 9). Питання: **що саме у фічах події
несе цей сигнал** і чи однаково він видно перед верхнім і нижнім
півотом. Модель тут не бере участі — ранжують самі фічі.

### Метод

Подія `клас210х14х4ц` — 15 блоків по 14 с × 4 фічі. З кожної фічі
беруться два числа події:

| Число | Формула |
| ----- | ------- |
| середнє | `mean` по 15 блоках |
| розмах | `max − min` по 15 блоках |

Кожне окремо ранжує події проти мітки «нога скінчиться в наступні 10
подій». Розмах іде **зі знаком мінус**: гіпотеза була, що перед півотом
блоки однорідні. Додатні події діляться за типом півота, який завершує
їхню ногу; відʼємні спільні для обох.

Замір іде на **обох** вибірках, і це не «навчання і перевірка», а
розмір: до межі розбиття 608 півотів, після неї 63.

### Результат

Середнє по шести монетах, AUC:

| Фіча | розмах, train | середнє, train | розмах, val |
| ---- | ------------: | -------------: | ----------: |
| balance | 0.5883 | 0.5008 | 0.5426 |
| d_balance | 0.5640 | 0.5092 | 0.5834 |
| **vol_balance** | **0.6490** | 0.4908 | **0.7024** |
| ціна | 0.5213 | 0.4934 | 0.5859 |

### Верхні й нижні півоти

| Фіча | train, верхні | train, нижні | val, верхні | val, нижні |
| ---- | ------------: | -----------: | ----------: | ---------: |
| balance | 0.5724 | 0.6043 | 0.5681 | 0.5162 |
| d_balance | 0.5455 | 0.5851 | 0.6103 | 0.5330 |
| vol_balance | 0.6545 | 0.6452 | 0.7595 | 0.6527 |
| ціна | 0.5100 | 0.5328 | 0.6245 | 0.5454 |

Півотів: train `305` вгору / `303` вниз, val `30` / `33`.

### Висновки

1. **Середнє по події не каже нічого.** `0.49 … 0.52` на всіх чотирьох
   фічах в обох вибірках. Скільки в події було дисбалансу — байдуже.

2. **Каже розмах.** `vol_balance` дає `0.649` однією ознакою проти
   `0.7326` у всієї мережі з памʼяттю. Перед кінцем ноги подія
   **однорідна всередині**: 15 блоків по 14 с перестають різнитись між
   собою. Це і є «виснаження руху» в тому вигляді, в якому воно є в
   даних.

3. **Різниці між верхніми й нижніми півотами немає.** На val вона
   виглядає великою — `vol_balance` `0.7595` проти `0.6527`, — але на
   train, де півотів у десять разів більше, вона зникає: `0.6545`
   проти `0.6452`. Це розмір вибірки, а не властивість ринку: 30
   верхніх півотів на val не тримають десятої частки AUC.

4. **Замір пояснює, чому мережа без памʼяті дає `0.58`.** Одна ознака
   `vol_balance` уже дає `0.649` — тобто перцептрону не бракує
   інформації, він її гірше згортає. Різниця `0.73 − 0.58` — це не
   «памʼять знає щось інше», а «памʼять читає ту саму однорідність по
   послідовності подій, а не по одній».

## Розділ 15. Розмах блоків у фічах · `підтверджено`

**Ціль.** Розділ 14 показав, що розмах блоків — найсильніша одинична
ознака. Тут вона віддається мережі напряму: до 60 колонок події
додається 15 колонок `max − min` (по чотири на фічу плюс лічильник),
разом 65 входів і 121 412 ваг проти 119 876. Решта — та сама постановка
розділу 12, чотири горизонти.

### Результат

Епоха 12 з 15, 113.8 хв.

| Горизонт | без розмаху | з розмахом |
| -------- | ----------: | ---------: |
| p₁₀ | 0.7326 | **0.7334** |
| p₄₀ | 0.7166 | **0.7264** |
| p₁₆₀ | **0.7213** | 0.7184 |
| p₆₄₀ | **0.7988** | 0.7762 |
| **середнє по горизонтах** | **0.7423** | 0.7386 |

По монетах на `p₁₀`: BTC `0.6022 → 0.6218`, ETH `0.7557 → 0.7931`,
SOL `0.7905 → 0.7953`, XRP `0.7769 → 0.7753`, BNB `0.7149 → 0.6702`,
DOGE `0.7554 → 0.7445`.

### Висновки

1. **Приросту немає:** `−0.0037` по середньому. Короткі горизонти
   трохи вгору, найдовший помітно вниз, по монетах різнонаправлено.

2. **Причина — мережа вже це рахує.** Розмах є згорткою тих самих 15
   блоків, які лежать на вході. LSTM бачить їх усі; давати їй готовий
   `max − min` означає повторити те, що вона будує сама, і заплатити
   зайвими вагами.

3. **Розділ 14 від цього не скасовується.** Ознака справді несе
   сигнал — просто вона його не **додає**: він уже всередині.

## Розділ 16. Інші питання про ногу · `підтверджено`

**Ціль.** Мережа, розбиття, ручки переможця гріду і спосіб вибору епохи
ті самі — міняється **тільки мітка**. Три питання, кожне бінарне:

| Мітка | Питання |
| ----- | ------- |
| `вгору` | чи буде наступний півот вище поточної ціни |
| `ближче` | чи ближчий наступний півот, ніж минулий |
| `перша_десятина` | чи лежить подія в перших 10 % ноги |

**Еталон** для кожної — та сама постановка мережею **без** памʼяті
(`hazard_flat.py --target ...`), на тому самому розбитті. Три входи:
самі фічі, сам лічильник, разом.

### Вгору

Епоха 3 з 6, база мітки `0.5026`.

| Монета | AUC | база мітки |
| ------ | --: | ---------: |
| XRP | 0.5947 | 0.493 |
| BNB | 0.5943 | 0.504 |
| DOGE | 0.5918 | 0.719 |
| BTC | 0.5453 | 0.721 |
| SOL | 0.5433 | 0.634 |
| ETH | 0.5073 | 0.775 |
| **середнє** | **0.5628** | |

Без памʼяті: фічі `0.5615`, лічильник `0.4366`, разом `0.5539`.

### Ближче

Епоха 1 з 4, ковзна памʼять, вікно градієнта 500.

Широке AUC **0.5071**, розкид по монетах `0.4162 … 0.6419`. Train і val
стоять на `0.6939` — це `ln 2`, тобто модель не зрушила з рівномірного
виходу за чотири епохи.

Без памʼяті: фічі `0.4933`, лічильник `0.6290`, разом `0.5229`.

### Висновки

1. **Напрямок наступного півота майже не читається.** `0.5628` проти
   `0.5615` без памʼяті — памʼять додає `+0.001`. Найкраще там, де
   мітка збалансована (XRP, BNB ≈ 0.5); де ринок односторонній (ETH,
   мітка `0.775`) модель угадує більшість і рангує на рівні монетки.

2. **`ближче` не вчиться зовсім** — `0.5071`, гірше за власний
   безпамʼятний рівень. Мітка `лишилось < пройдено` вимагає знати
   **обидва** боки ноги; прогнозний бік сам дає `0.73` і тільки на
   коротких горизонтах, а різниця двох величин, одна з яких відома
   погано, — шум.

3. **Лічильник у безпамʼятному еталоні `ближче` дає `0.629` — і це не
   сигнал.** Лічильник там не фіча, а пилка `номер % 1500`. Її
   виграш означає рівно те, що мітка залежить від **місця в потоці**,
   і пилка з періодом порядку довжини ноги частково з ним збігається.
   Порівнювати з ним модель не можна; він стоїть у таблиці як
   попередження, а не як рівень.

4. **Порівняння з основною задачею.** Прогнозне питання «чи
   скінчиться нога скоро» дає `0.7423`; питання про напрямок і про
   місце в нозі — `0.51 … 0.56`. У фічах є **виснаження руху**
   (розділ 14), а не його напрямок.

## Відтворення

```bash
# розділ 1: форма функції втрат
python3 research/20-leg-loss/scripts/loss_shape.py

# розділ 2: три прогони під виправленою втратою (12.6 хв)
python3 research/20-leg-loss/scripts/loss_train.py

# один прогін
python3 research/20-leg-loss/scripts/loss_train.py --runs свіжа

# розділ 3: навчання з обнуленням від моделі (16.2 хв)
python3 research/20-leg-loss/scripts/walk_train.py --back 25 --late 2 --ahead 2

# розділ 4: мʼякі ворота і втрата з памʼяттю про час
python3 research/20-leg-loss/scripts/walk_train.py --epochs 8 \
    --power 4 --no-counter --back 30

# розділ 5: базові рівні (секунди) і перевірка на перевчання (48 хв)
python3 research/20-leg-loss/scripts/baseline_check.py
python3 research/20-leg-loss/scripts/overfit_check.py --epochs 60

# навчання під тим, що виграло: чиста ентропія, ворота 1, лічильник
python3 research/20-leg-loss/scripts/walk_train.py \
    --late 0 --ahead 0 --back 0 --jump 0 --epochs 10 --patience 4

# скільки сигналу взагалі є у фічах події (секунди)
python3 research/20-leg-loss/scripts/signal_check.py

# розділ 6: який горизонт брати (хвилини)
python3 research/20-leg-loss/scripts/hazard_check.py

# розділ 6: навчання на чотири горизонти
python3 research/20-leg-loss/scripts/hazard_train.py

# розділ 7: та сама мережа на один горизонт — півот за 100 подій
python3 research/20-leg-loss/scripts/hazard_train.py --horizons 100 \
    --out research/20-leg-loss/results/hazard_one.json

# розділ 8: грід ручок на урізаному train (4 год)
python3 research/20-leg-loss/scripts/hazard_grid.py --force

# розділ 11: досяжний поріг обнулення
python3 research/20-leg-loss/scripts/hazard_train.py --horizons 100 \
    --lift 1.3 --name hazard100_lift \
    --out research/20-leg-loss/results/hazard_one_lift.json

# розділ 10: готова модель на всіх шести монетах після межі (хвилини)
python3 research/20-leg-loss/scripts/hazard_wide.py --force
python3 research/20-leg-loss/scripts/hazard_wide.py --force \
    --source research/20-leg-loss/results/hazard_one_lift.json \
    --out research/20-leg-loss/results/hazard_wide_lift.json

# розділ 9: скільки дає памʼять — та сама задача без неї (хвилини)
python3 research/20-leg-loss/scripts/hazard_flat.py --force
python3 research/20-leg-loss/scripts/hazard_check.py --horizons 100 \
    --out research/20-leg-loss/results/hazard_check100.json

# одна стадія
python3 research/20-leg-loss/scripts/hazard_grid.py --stages будова --force

# розділ 12: переможець гріду на повному train, епоха за шістьма монетами
python3 research/20-leg-loss/scripts/hazard_train.py --horizons 100 \
    --layers 2 --window 75 --cap 500 --wide-val --name hazard100_win \
    --out research/20-leg-loss/results/hazard_win.json --force
python3 research/20-leg-loss/scripts/hazard_train.py \
    --layers 2 --window 75 --cap 500 --wide-val --name hazard4_win \
    --out research/20-leg-loss/results/hazard_win4.json --force

# розділ 13: детектор — півот у межах останніх N подій
python3 research/20-leg-loss/scripts/hazard_train.py --target пройдено \
    --horizons 10 40 160 500 --layers 2 --window 75 --cap 1500 \
    --wide-val --name hazard4_det \
    --out research/20-leg-loss/results/hazard_det4.json --force
python3 research/20-leg-loss/scripts/hazard_train.py --target пройдено \
    --horizons 10 40 160 500 --layers 2 --window 500 --cap 1000 --slide \
    --wide-val --name hazard4_det_slide \
    --out research/20-leg-loss/results/hazard_det4_slide.json --force
python3 research/20-leg-loss/scripts/detector_check.py --force

# розділ 14: що в події видно перед кінцем ноги — без моделі (хвилини)
python3 research/20-leg-loss/scripts/spread_check.py --force

# розділ 15: розмах блоків у фічах
python3 research/20-leg-loss/scripts/hazard_train.py --spread \
    --layers 2 --window 75 --cap 500 --wide-val --name hazard4_spread \
    --out research/20-leg-loss/results/hazard_spread4.json --force

# розділ 16: інші питання про ногу
python3 research/20-leg-loss/scripts/hazard_train.py --target вгору \
    --layers 2 --window 75 --cap 500 --wide-val --name hazard_up \
    --out research/20-leg-loss/results/hazard_up.json --force
python3 research/20-leg-loss/scripts/hazard_train.py --target ближче \
    --layers 2 --window 500 --cap 1000 --slide --wide-val --name hazard_near \
    --out research/20-leg-loss/results/hazard_near.json --force
for мітка in вгору ближче перша_десятина; do
    python3 research/20-leg-loss/scripts/hazard_flat.py --force \
        --target $мітка \
        --out research/20-leg-loss/results/hazard_flat_$мітка.json
done

# широка перевірка будь-якої готової моделі: шість монет після межі
python3 research/20-leg-loss/scripts/hazard_wide.py --force \
    --source research/20-leg-loss/results/hazard_spread4.json \
    --out research/20-leg-loss/results/hazard_wide_spread4.json

python3 -m unittest discover -s tests -t tests -p "test_dashboard.py"
```

Навчання не почнеться при браку вільної памʼяті
(`rules/03-models.md` → Навчання без свопа); свідомий обхід — `--force`.

Вивід — [`results/walk_train.json`](results/walk_train.json), на
вкладці — `/api/loss/walk`.

## Файли

| Файл | Що робить |
| ---- | --------- |
| [`scripts/loss_shape.py`](scripts/loss_shape.py) | криві функції втрат викликом `masked_loss`, крива навчання кращої моделі |
| [`results/loss_shape.json`](results/loss_shape.json) | вивід: штраф за сходинку, ентропія, ординальність, крива |
| [`scripts/loss_train.py`](scripts/loss_train.py) | розділ 2: три прогони під виправленою втратою, ряд val з ціллю і видачею |
| [`results/loss_train.json`](results/loss_train.json) | вивід: криві, метрики, ряд val кожного прогону |
| `models/loss_<прогін>/model.pt` | ваги трьох прогонів розділу 2 |
| [`scripts/walk_train.py`](scripts/walk_train.py) | розділи 3 і 4: навчання потоком, памʼять обнуляє сама модель |
| [`scripts/timed_loss.py`](scripts/timed_loss.py) | розділ 4: втрата з памʼяттю про час — свій штраф на кожну ціль, ціна затримки |
| [`results/walk_train_back3.json`](results/walk_train_back3.json) | вивід прогону з `back = 3` |
| [`results/walk_train_back30.json`](results/walk_train_back30.json) | вивід прогону з `back = 30` — підняте пониження |
| [`scripts/baseline_check.py`](scripts/baseline_check.py) | розділ 5: базові рівні — лічильник і константа, ентропія десятини |
| [`results/baseline_check.json`](results/baseline_check.json) | вивід: два рівні, довжини ніг, ентропія при відомому елапсі |
| [`scripts/overfit_check.py`](scripts/overfit_check.py) | розділ 5: чи перевчить мережа 2 000 подій — три прогони |
| [`results/overfit_check.json`](results/overfit_check.json) | вивід: криві трьох прогонів, вердикт по кожному |
| [`scripts/signal_check.py`](scripts/signal_check.py) | розділ 5: скільки сигналу про десятину є у фічах події без памʼяті |
| [`results/signal_check.json`](results/signal_check.json) | вивід: три моделі без памʼяті проти базових рівнів |
| [`results/walk_train_ce.json`](results/walk_train_ce.json) | вивід повного ряду під чистою ентропією |
| [`scripts/hazard_check.py`](scripts/hazard_check.py) | розділ 6: скільки сигналу на кожному горизонті, який N брати |
| [`results/hazard_check.json`](results/hazard_check.json) | вивід: частка мітки й AUC трьох моделей на шість горизонтів |
| [`scripts/hazard_net.py`](scripts/hazard_net.py) | розділ 6: монотонна крива, виведення десятини, зважена втрата, AUC |
| [`scripts/hazard_train.py`](scripts/hazard_train.py) | розділ 6: навчання потоком на чотири горизонти |
| [`results/hazard_train.json`](results/hazard_train.json) | вивід: крива по епохах, AUC горизонтів, ряд val |
| `models/hazard/model.pt` | ваги розділу 6 |
| [`results/hazard_one.json`](results/hazard_one.json) | розділ 7: вивід прогону на один горизонт — 100 подій |
| `models/hazard100/model.pt` | ваги розділу 7 |
| [`scripts/hazard_grid.py`](scripts/hazard_grid.py) | розділ 8: грід ручок стадіями плюс абляція архітектури |
| [`results/hazard_grid.json`](results/hazard_grid.json) | вивід: усі прогони гріду, переможець кожної стадії |
| [`scripts/hazard_flat.py`](scripts/hazard_flat.py) | розділ 9: та сама задача мережею без памʼяті, на тому самому розбитті |
| [`results/hazard_flat.json`](results/hazard_flat.json) | вивід: три входи без памʼяті проти моделі з памʼяттю |
| [`results/hazard_check100.json`](results/hazard_check100.json) | розділ 9: замір без памʼяті на N=100 з розбиттям по ногах |
| [`scripts/hazard_wide.py`](scripts/hazard_wide.py) | розділ 10: готова модель на всіх шести монетах після межі |
| [`results/hazard_wide.json`](results/hazard_wide.json) | вивід: AUC на кожну монету, середнє й спільне |
| [`results/hazard_one_lift.json`](results/hazard_one_lift.json) | розділ 11: прогін із досяжним порогом обнулення |
| [`results/hazard_wide_lift.json`](results/hazard_wide_lift.json) | розділ 11: модель із досяжним порогом на всіх шести монетах |
| `models/hazard100_lift/model.pt` | ваги розділу 11 |
| [`results/walk_train.json`](results/walk_train.json) | вивід: крива по епохах, метрики val, ряд з власними обнуленнями |
| `models/walk_self_reset/model.pt` | ваги розділу 3 |
| [`results/hazard_win.json`](results/hazard_win.json) | розділ 12: переможець гріду на `p₁₀₀`, епоха за шістьма монетами |
| [`results/hazard_wide_win.json`](results/hazard_wide_win.json) | розділ 12: він же на всіх шести монетах |
| `models/hazard100_win/model.pt` | ваги розділу 12, один горизонт |
| [`results/hazard_win4.json`](results/hazard_win4.json) | розділ 12: він же на чотири горизонти — головна модель дослідження |
| [`results/hazard_wide_win4.json`](results/hazard_wide_win4.json) | розділ 12: чотири горизонти на всіх шести монетах |
| `models/hazard4_win/model.pt` | ваги розділу 12, чотири горизонти |
| [`results/hazard_det4.json`](results/hazard_det4.json) | розділ 13: детектор із обнуленням памʼяті |
| [`results/hazard_det4_slide.json`](results/hazard_det4_slide.json) | розділ 13: детектор із ковзною памʼяттю, вікно градієнта 500 |
| [`results/hazard_wide_det4.json`](results/hazard_wide_det4.json), [`_slide`](results/hazard_wide_det4_slide.json) | розділ 13: обидва детектори на шести монетах |
| `models/hazard4_det/model.pt`, `models/hazard4_det_slide/model.pt` | ваги розділу 13 |
| [`scripts/detector_check.py`](scripts/detector_check.py) | розділ 13: чи лежить свідчення детектора в памʼяті |
| [`results/detector_check.json`](results/detector_check.json) | вивід: частка додатних подій із півотом у памʼяті, дві стелі |
| [`scripts/spread_check.py`](scripts/spread_check.py) | розділ 14: розмах блоків проти середнього, верхні й нижні півоти |
| [`results/spread_check.json`](results/spread_check.json) | вивід: AUC кожної фічі на обох вибірках |
| [`results/hazard_spread4.json`](results/hazard_spread4.json) | розділ 15: розмах блоків у фічах — 65 входів |
| [`results/hazard_wide_spread4.json`](results/hazard_wide_spread4.json) | розділ 15: він же на шести монетах |
| `models/hazard4_spread/model.pt` | ваги розділу 15 |
| [`results/hazard_up.json`](results/hazard_up.json), [`hazard_near.json`](results/hazard_near.json) | розділ 16: мітки `вгору` і `ближче` |
| [`results/hazard_wide_up.json`](results/hazard_wide_up.json), [`_near`](results/hazard_wide_near.json) | розділ 16: обидві на шести монетах |
| `models/hazard_up/model.pt`, `models/hazard_near/model.pt` | ваги розділу 16 |
| `results/hazard_flat_<мітка>.json` | розділ 16: безпамʼятний рівень кожної мітки |

## Вкладка

[Функція втрат](http://localhost:8080/loss) — розділ 6 цілком:

| Частина вкладки | Що показує |
| --------------- | ---------- |
| Чому не десятина | замір, з якого виросла архітектура: у фічах немає десятини, але є «півот близько» |
| Дані | таблиця потоків: пара, вибірка, події, від якої до якої дати; межа розбиття |
| Чотири виходи | побудова кривої, горизонти в подіях і в годинах, ваги класів, поріг обнулення |
| Де я в нозі | формула виведення десятини, середні по корзинах, стеля методу |
| Функція втрат | зважена двійкова ентропія на кожен горизонт |
| З чим порівнюємо | лічильник `0.5625` · константа `0.300` · мережа |
| Епохи | train, val, **AUC кожного горизонту**, схожість, свої обнулення проти примусових, ворота |
| Полотна val | ціна із зігзагом, ціль, виведена десятина, `p₁₀` і оцінене «лишилось»; вертикаль — власне обнулення |
| Один горизонт | розділ 7: епохи окремої моделі, AUC, базова частота, чому ворота рахуються від перевищення |
| Чи потрібна памʼять | розділ 9: три входи без памʼяті проти моделі з памʼяттю, на тому самому val |
| Грід ручок | розділ 8: таблиця на кожну стадію, база всередині гріду, абляція архітектури |
| Полотно `p₁₀₀` | **окремий графік знизу**, спільна вісь X з рештою: калібрована `P(півот ≤ 100 подій)`, пунктир — базова частота |
| Переможець гріду | розділ 12: обидві моделі на шести монетах, різниця з розділом 10 по кожній |
| Полотна чотирьох горизонтів | розділ 12: **своє** полотно з власною віссю, зумом і власними обнуленнями |
| Що в події видно перед кінцем ноги | розділ 14: розмах проти середнього, верхні проти нижніх, обидві вибірки поруч |
| Та сама мережа під іншими питаннями | розділи 13, 15, 16: рядок на прогін, кожен зі своїм еталоном, і таблиця по монетах під кожним |

Дані — `/api/loss/walk` (`results/hazard_train.json`) і `/api/loss/one`
(`results/hazard_one.json`). Ряди зшиваються по номеру події: обидва
прогони йдуть тим самим потоком val. Різна довжина рядів означає різні
дані — тоді нижнє полотно не малюється взагалі, а не малюється зі
зсувом.

Решта ендпоінтів вкладки: `/api/loss/grid`, `/api/loss/flat`,
`/api/loss/wide`, `/api/loss/win`, `/api/loss/win4`, `/api/loss/spread`,
`/api/loss/targets`. Кожен вантажиться окремо — поки котрийсь прогін
рахується, решта вкладки показується; готові прогони «інших питань»
приходять одним `targets`, а ті, що ще рахуються, стоять під таблицею
рядком «ще рахується».
