# 16. Навчання моделі 2.3

Пара: `BTC_USDT`. Клас фіч: `клас210х4бема10х10х130`.

## Ціль

Навчити **одну** мережу — `wattn` — на мітці порогу **2.3%**: нормований
зігзаг на **ЕМА90** ціни (мітка етапу 1). Підбору технології тут немає:
мережа задана умовою дослідження, а не вибором. Грід є тільки в
розділі 4 і крутить ручки самої `wattn`.

Ціль міряється **відстанню до своїх вершин**. Нормована нога — це `−1` у
нижньому півоті й `+1` у верхньому; питання дослідження не «яке число
видасть модель у середньому», а **чи доходить видача до вершини**. Тому в
кожному звіті поруч зі звичайними метриками стоїть середнє
`|вершина − прогноз|` на подіях, де ціль стоїть в околі вершини. Окіл
один — **`0.1`** (`vertex.BANDS`); на ньому ж стоїть робоча зона півота
з розділу 1.

## Дані

| Що | Значення |
| -- | -------- |
| Клас фіч | [`клас210х4бема10х10х130`](../../rules/01-data-and-db.md#клас210х4бема10х10х130) |
| Форма події | `(13, 4)` — 13 періодів ЕМА × 4 фічі, усі в `[-1, 1]` |
| Фічі | `balance`, `d_balance`, `vol_balance`, `price_balance` |
| Ціль | зігзаг **2.3%** нормований, **ема90** ціни (`kind = ema`) |
| Нога цілі | `розподіл` (minmax): `2·(price − low)/(high − low) − 1` |
| Скейлер фіч | квантиль, порахований **тільки на train** |
| Розбиття | по часу: `train \| val \| test` підряд, проміжки по півотах ≥ 130 подій |
| Модель | `wattn` — увага по осі ЕМА-періодів, 26 049 параметрів; у розділі 3 та сама увага по осі подій |
| Джерело | `data/datasets/BTC_USDT/class210x4bema10x10x130/` |

Порядок побудови цілі (`rules/02-markup.md` → Нормований зігзаг):

```
ема90(ціна) -> півоти зігзага 2.3% -> нормована нога
```

## План роботи

1. Етап 1 — підбір мітки: кореляція всіх 52 фіч з усіма 26 кандидатами
   на train, вибір мітки під поріг 2.3%.
2. Розділ 1 — нога обраного зігзага: вибір нормалізації і **зона
   півота**, у яку модель має влучати.
3. Розділ 2 — мережа `wattn` під **штрафом за розмах**: σ прогнозу
   штрафується прямо в лоссі, і міряється відстань видачі до вершин.
4. Видача моделі на **всьому датасеті** і на **невідомому відрізку** —
   даних після кінця датасету, яких не бачив ніхто.
5. Згладжування цілі між зонами: коливання на середині ноги гасяться,
   зони й вершини лишаються як є.
6. Розділ 3 — пачка подій: вікно з K подій поспіль, K = 5 … 50.
7. Розділ 4 — дві кращі моделі на згладженій цілі: грід по ручках,
   викидання фіч і невідомий відрізок для кожної.

## Мережа `wattn`

Увага по осі **періодів ЕМА**: крок послідовності — один період
(10 … 130), позиція кроку вчиться окремим параметром
([`scripts/nets.py`](scripts/nets.py)).

Відмінність від `transformer` дослідження 12 — **зчитування**: там кроки
усереднюються, тут за них відповідає навчений запит.

```
h       = енкодер(into(x) + place)              (N, 13, width)
ваги    = softmax(q · h / √width)               Σ ваг = 1
вихід   = голова(Σ ваг · h)
```

| Ручка | Значення |
| ----- | -------- |
| `width` / `heads` / `layers` / `dropout` | 32 / 4 / 2 / 0.1 |
| lr, батч, seed | 1e-3, 256, 0 |
| WMSE | α = 3.0, β = 2.0 |
| Епохи, терпіння | 400, 30 (рання зупинка по val) |

## Метрика: відстань до вершин

[`scripts/vertex.py`](scripts/vertex.py). Подія вважається **верхньою**,
поки `y ≥ 1 − окіл`, і **нижньою**, поки `y ≤ −1 + окіл`. На кожен окіл
рахується:

| Поле | Що це |
| ---- | ----- |
| `events` / `share` | скільки подій потрапило в окіл вершини |
| `gap_pred` | середнє `\|вершина − прогноз\|` — те, заради чого метрика |
| `gap_true` | те саме для самої цілі: підлога, нижче якої не буде |
| `mae` | середнє `\|прогноз − ціль\|` на цих подіях |
| `reach` | середній прогноз: доходить він до вершини чи ні |

Ціль дослідження — **попадати в зону**, тому поруч рахуються влучання
(`vertex.hits`):

| Поле | Що це |
| ---- | ----- |
| `inside` / `said` | подій у зоні / подій, де модель сказала «вершина» (`\|p\| ≥ 0.90`) |
| `hits` / `missed` / `false` | влучань того самого знака / пропущено / хибних тривог |
| `recall` / `precision` | яка частка зони взята / яка частка сказаного влучила |
| `to_edge` | скільки прогнозу бракує до межі зони: `max(0, 0.90 − \|p\|)` |
| `to_pivot` | відстань у **подіях** до найближчого півота там, де модель сказала «вершина» |

Окіл `0.1` на test бере **11.4 %** подій; сама ціль у ньому стоїть за
`0.041` від вершини, константа train — за `1.000`.

Поверх околів рахується **робоча зона півота** — `|y| ≥ 0.90` **і** не
далі за **±40 подій** від півота (розділ 1). Самих меж мало: у довгій
нозі ціна відкочується майже до її початку, і `|y| ≥ 0.95` трапляється
за 972 події від будь-якого півота.

## Етап 1. Підбір мітки · `підтверджено`, 2026-08-28

### Метод

Кореляція всіх 52 фіч з усіма 26 кандидатами в ціль (13 періодів ЕМА × 2
пороги), Пірсоном і Спірменом, **тільки на train** — вибір мітки це
агрегат по датасету ([`scripts/target_pick.py`](../../scripts/target_pick.py)).

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

Максимальна `|r|` по періодах ЕМА цілі, поріг 2.3%:

| Період цілі | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100 | 110 | 120 | 130 |
| ----------- | -- | -- | -- | -- | -- | -- | -- | -- | -- | --- | --- | --- | --- |
| макс | .206 | .247 | .252 | .247 | .253 | .280 | .281 | .283 | **.294** | .284 | .282 | .246 | .243 |
| середня | .082 | .121 | .144 | .143 | .149 | .169 | .175 | **.177** | .167 | .164 | .164 | .143 | .142 |

| Поріг | Мітка | Макс `\|r\|` | Найсильніша фіча | Подій з міткою |
| ----- | ----- | ----------- | ---------------- | -------------- |
| 2.3% | нормований зігзаг на ЕМА **90** (5.3 год) | 0.2936 | `vol_balance ема130` (Пірсон −0.2936, Спірмен −0.288) | 23 927 |

### Висновки

Крива має горб на 60–110 і падає на краях: короткі цілі шумні, довгі
розмиті. Обрана мітка — **ема90**; вона й іде в розділи 1 і 2.

## Розділ 1. Нога зігзага і зона півота · `підтверджено`, 2026-08-28

### Метод

[`scripts/legs.py`](scripts/legs.py). Питання одне — де подія стоїть у
своїй нозі (`rules/02-markup.md` → Нормалізація ноги); відповідь
рахується **по своїй нозі окремо**, на цінах самої ноги. У дослідженні
один ряд — **розподіл**; робаста й квантиля на вкладці немає.

Чим це підтверджено, рахує
[`scripts/leg_pick.py`](scripts/leg_pick.py) — двома мірками, обома
**тільки на train**: звʼязок з 52 фічами класу (той самий критерій, що
на етапі 1) і розподіл подій біля вершин. Цілі будуються на всьому ряду
і лише потім ріжуться по train, як вони лежать у датасеті; контроль —
колонка `розподіл` збігається з `train_targets.npz`.

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

Зігзаг 2.3% на ема90: **44 півоти, 43 ноги** на 40 242 події датасету
(39 879 з них мають ціль), 2026-02-14 17:58 … 2026-08-16 19:51 UTC.

Графік іде на **весь датасет**, а не вікном: середня нога зігзага 2.3% —
близько 900 подій, і в короткому вікні ніг не видно зовсім.

| Ряд | Формула | Межі | σ | Подій |
| --- | ------- | ---- | - | ----- |
| розподіл (minmax) | `2·(price − low)/(high − low) − 1` | −1.0 … 1.0 | 0.569 | 39 879 |

Чому саме він — порівняння кандидатів на train (24 150 подій, 23 927
з ціллю), [`leg_pick.py`](scripts/leg_pick.py):

| Ряд | σ | макс \|r\| | сер \|r\| | макс \|ρ\| | сер \|ρ\| | Найсильніша фіча |
| --- | - | -------- | -------- | -------- | -------- | ---------------- |
| **розподіл** | 0.5693 | **0.2936** | **0.1667** | **0.2880** | **0.1579** | `vol_balance ема130` |
| квантиль | 0.5774 | 0.2846 | 0.1585 | 0.2823 | 0.1514 | `vol_balance ема130` |

Частка подій в околі вершини (`0.1`) і відстань самої цілі до вершини
в ньому:

| Ряд | Подій в околі | Ціль до вершини |
| --- | ------------- | --------------- |
| розподіл | 9.8 % | **0.042** |
| квантиль | 10.0 % | 0.051 |

### Зона півота в розподілі

[`scripts/pivot_band.py`](scripts/pivot_band.py). У самому півоті ціль
дорівнює **рівно** `−1` або `+1` — усі 44 півоти (22 верхні, 22 нижні),
без діапазону, значення однакові до девʼятого знака. Але близьке до
одиниці `|y|` півота ще не означає: у довгій нозі ціна відкочується
майже до її початку, і `|y| ≥ 0.95` трапляється за 972 події від
найближчого півота.

Тому зона задається **двома** показниками: межами розподілу `|y| ≥ 1 − b`
і вікном `вліво-вправо` від півота. Перевіряється чотирма умовами:
всередину не потрапляє жоден **чужий півот** (вкладений зігзаг 0.8 % на
тому самому ряду, 63 таких), на кожен півот припадає рівно **одне
суцільне вікно**, покриті **всі** півоти, а саме вікно **коротше** за
відстань до найближчого чужого півота (52 події) — тоді чужий півот не
влазить у зону за побудовою.

Суцільна смуга навколо півота, подій (медіана / 90 % / макс):

| Межі | вліво | вправо |
| ---- | ----- | ------ |
| `\|y\| ≥ 0.98` | 9 / 24 / 41 | 10 / 25 / 48 |
| `\|y\| ≥ 0.95` | 19 / 38 / 84 | 17 / 50 / 102 |
| `\|y\| ≥ 0.90` | 34 / 75 / 140 | 29 / 81 / 162 |
| `\|y\| ≥ 0.85` | 56 / 110 / 182 | 41 / 111 / 196 |
| `\|y\| ≥ 0.80` | 64 / 132 / 205 | 50 / 143 / 397 |
| `\|y\| ≥ 0.70` | 89 / 212 / 325 | 74 / 203 / 521 |

Зони, що пройшли всі чотири умови (найбільші):

| Межі | Вікно | Подій | Частка ряду |
| ---- | ----- | ----- | ----------- |
| `\|y\| ≥ 0.70` | ±50 | 4 099 | 10.19 % |
| `\|y\| ≥ 0.80` | ±50 | 3 779 | 9.39 % |
| `\|y\| ≥ 0.80` | ±40 | 3 207 | 7.97 % |
| `\|y\| ≥ 0.85` | ±40 | 3 028 | 7.52 % |
| `\|y\| ≥ 0.90` | ±40 | 2 624 | 6.52 % |

Наскільки далеко ціна відходить від ціни свого півота всередині двох
робочих зон (мінімум / середнє / медіана / максимум):

| Зона | Ряд | мін | сер | медіана | макс | макс, USDT |
| ---- | --- | --- | --- | ------- | ---- | ---------- |
| `\|y\| ≥ 0.85`, ±40 | ема90 | 0 % | 0.124 % | 0.092 % | **1.273 %** | 777.82 |
| `\|y\| ≥ 0.85`, ±40 | ціна події | 0 % | 0.411 % | 0.264 % | **3.877 %** | 2 405.28 |
| `\|y\| ≥ 0.90`, ±40 | ема90 | 0 % | 0.099 % | 0.075 % | **1.273 %** | 777.82 |
| `\|y\| ≥ 0.90`, ±40 | ціна події | 0 % | 0.371 % | 0.255 % | **3.877 %** | 2 405.28 |

Нуль у мінімумі — це сам півот. Зігзаг стоїть на **ема90**, тому
гарантія зони стосується саме її: усередині зони ема90 не відходить від
вершини далі ніж на 1.27 % (0.1–0.12 % у середньому). Сира ціна події
гуляє навколо ема90 значно ширше — до 3.88 %, і це та сама межа в обох
зонах: її задає один викид, не самі межі розподілу.

Жодна з зон не містить чужого півота, і в кожній рівно 44 вікна на 44
півоти. Ширші вікна (±60 і більше) теж виходять чистими на цих даних,
але вже без запасу: 60 > 52, тобто чистота там — властивість цього
ряду, а не побудови.

### Висновки

У навчання йде **розподіл**, і на це дві причини.

1. **Звʼязок з фічами сильніший** — 0.2936 проти 0.2846 за максимумом і
   0.167 проти 0.159 за середнім, те саме на Спірмені. Різниця невелика
   (3–5 % відносних), але вона на боці розподілу за всіма чотирма
   числами, а мітку етапу 1 обирали саме цим критерієм.
2. **Він міряє те, що міряє метрика.** Розподіл — частка пройденого
   шляху ноги, тому `±1` це самі півоти, і відстань до вершини має
   ціновий сенс. Квантиль — ранг події в нозі: його `±1` означає «подія
   в хвості CDF ноги», і відстань до вершини рахувалась би в рангах, а
   не в русі ціни.

Головна перевага квантиля — рівномірність за побудовою (рівно 10 % подій
на кожен окіл 0.1) — тут майже нічого не додає: розподіл і так дає
9.8 / 18.5 / 28.2 %, і згущення біля вершин у нього немає.

**Робоча зона дослідження — `|y| ≥ 0.90` і ±40 подій** від півота
(2 624 події, 6.5 % ряду). Вікно вдвічі коротше за відстань до
найближчого чужого півота, тому той не влазить у зону за побудовою;
межі тримають ема90 не далі ніж за 1.27 % від вершини. Ця пара чисел
живе у [`vertex.py`](scripts/vertex.py) (`BAND`, `WINDOW`) і йде в
метрику кожного прогону окремим рядком `zone`.

Ряд «розподіл» тут — **той самий масив**, що йде в розділ 2: колонка
`ема90 × 2.3` з `<part>_targets.npz` збігається з ним подія в подію
(розбіжність 5·10⁻⁵ — округлення JSON до 4 знаків).

## Розділ 2. Функція втрат · `підтверджено`, 2026-08-28

### Метод

[`scripts/wattn_losses.py`](scripts/wattn_losses.py). Функція втрат у
дослідженні **одна** — штраф за розмах, з переліку вкладки «Навчання
мережі»
([`research/12-zigzag-net/scripts/losses.py`](../12-zigzag-net/scripts/losses.py)
→ `variants`):

```
L = WMSE + λ·(σ_цілі − σ_прогнозу)²,   λ = 1,  w = 1 + α·|y|^β
```

σ прогнозу штрафується **прямо в лоссі**, а не ставиться після навчання
калібруванням: відстань до вершин має бути здобута навчанням, а не
розтягуванням готового виходу. Решта переліку (голий WMSE,
пост-калібрування, кореляційний лосс, пінбол, псевдо-Huber) у
дослідження не входить (`LOSSES`).

Розбиття: train 23 072 (2026-02-14 … 05-12), val 4 441 (05-14 … 06-22),
test 9 861 подій, 10 ніг (06-29 … 08-16 UTC).

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

| Прогноз | MAE | R² | Пірсон | знак | σ до цілі | до вершин (окіл 0.1) | у зоні | епоха |
| ------- | --- | -- | ------ | ---- | --------- | -------------------- | ------ | ----- |
| штраф за розмах | 0.5538 | −0.462 | 0.2737 | 0.5713 | 0.969 | 0.821 | 0.891 | 25 |
| константа train | 0.4791 | — | — | 0.4929 | 0.000 | 1.000 | 1.000 | — |

Відстань до вершин в околі `0.1` — 11.4 % подій test:

| Прогноз | До вершини |
| ------- | ---------- |
| штраф за розмах | 0.821 |
| константа train | 1.000 |
| сама ціль | 0.041 |

Робоча зона `|y| ≥ 0.90` і ±40 подій — окремо по вибірках:

| Вибірка | Подій у зоні | Частка | До вершини | Сама ціль | MAE | Вгору | Вниз |
| ------- | -----------: | -----: | ---------: | --------: | --: | ----: | ---: |
| train | 1 606 | 6.96 % | **0.161** | 0.035 | 0.158 | 0.153 | 0.169 |
| val | 275 | 6.19 % | 0.858 | 0.030 | 0.829 | 0.774 | 0.963 |
| test | 653 | 6.62 % | 0.891 | 0.031 | 0.861 | 0.727 | 1.058 |
| константа train на test | 653 | 6.62 % | 1.000 | 0.031 | — | — | — |

### Висновки

1. **У зоні півота модель вивчила train і не перенесла це далі.** На
   train до вершини лишається 0.161 при підлозі 0.035 — тобто вершини
   майже відтворені. На val 0.858, на test 0.891 при константі 1.000:
   поза навченим відрізком зона не дається взагалі. 26 049 параметрів на
   23 тисячі подій — модель має чим запамʼятати.
2. **σ штраф ставить, вершини — ні.** Розмах виходу піднявся до 0.969
   від σ цілі, але на test до вершини лишається 0.821 в околі 0.1: з
   відстані, яку треба пройти, видача проходить близько пʼятої частини.
3. **σ на місці ще не означає влучання.** R² −0.462, MAE 0.554 проти
   0.479 у константи: розтягнутий вихід платить помилкою. Знак 0.571 —
   єдине, що тримається вище монетки.
4. **Верх дається краще за низ**: у зоні 0.727 проти 1.058 на test.
   Верхні вершини модель бачить, нижні — ні.
5. Пірсон 0.274 не дотягує до стелі кореляцій дослідження 11
   (`|r| ≈ 0.3`): нелінійності всередині однієї події `wattn` не додала,
   а штраф за розмах ще й забрав частину форми.

## Видача моделі · `підтверджено`, 2026-08-28

### Метод

[`scripts/predict.py`](scripts/predict.py) — увесь датасет,
[`scripts/fresh.py`](scripts/fresh.py) — відрізок **після** його кінця
(2026-08-16 19:51 UTC): сирі CSV проходять той самий ланцюг класів, модель
і скейлер беруться збережені з навчання
(`models/wattn_2_3_spread_v1/`).

Метрики на всьому датасеті рахуються по **вікнах прогону**
(`train | val | test` підряд за часом), а не по вибірках датасету:
власний поділ датасету перемішаний, усі три його частини лежать на
всьому періоді.

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

| Відрізок | Подій | MAE | Знак | У зоні | Сказала | Влучань | З зони | Точність | До межі | До вершини | Подій до півота |
| -------- | ----: | --: | ---: | -----: | ------: | ------: | -----: | -------: | ------: | ---------: | --------------: |
| train | 23 072 | 0.258 | 82.9 % | 1 606 | 3 548 | **1 049** | 65.3 % | 29.6 % | 0.059 | 0.161 | 73 |
| val | 4 441 | 0.644 | 47.9 % | 275 | 313 | 12 | 4.4 % | 3.8 % | 0.471 | 0.858 | 212 |
| test | 9 861 | 0.554 | 57.1 % | 653 | 798 | 25 | 3.8 % | 3.1 % | 0.450 | 0.891 | 252 |
| увесь датасет | 39 767 | 0.390 | 71.4 % | 2 620 | 4 855 | 1 103 | 42.1 % | 22.7 % | 0.209 | 0.432 | 101 |
| **невідомий відрізок** | 2 869 | 0.752 | 44.9 % | 158 | 281 | 15 | 9.5 % | 5.3 % | 0.359 | 0.855 | 113 |

Невідомий відрізок: 2026-08-16 20:00 … 2026-08-27 00:00 UTC, 2 947 подій
(2 871 з ціллю), 4 півоти, 3 ноги, 28 добових файлів із розігрівом від
2026-08-01. Решта його метрик: RMSE 0.912, R² −1.352, Пірсон −0.226,
f1 0.301.

### Висновки

1. **Зона дається тільки там, де модель училась.** На train — 1 049
   влучань із 1 606 (65 %), на val 12 із 275, на test 25 із 653, на
   невідомому відрізку 15 із 158. Це не «слабкий сигнал», це
   запамʼятовування: поза train recall падає в 15 разів.
2. **Хибних тривог більше, ніж влучань.** Модель каже «вершина» 798
   разів на test, влучає 25 — точність 3 %. Медіана відстані від такого
   слова до найближчого півота — 252 події (≈ 15 годин), тобто вершину
   вона бачить посеред ноги.
3. **На невідомих даних напрямок перевертається**: Пірсон −0.226, знак
   44.9 %, R² −1.35. Модель, навчена до 2026-08-16, на наступних десяти
   днях гірша за монетку.
4. Єдине, що тримається на всіх відрізках, — сама постановка метрики:
   зона стабільно бере 6–7 % подій, і в ній видно, що видача не доходить
   до вершини 0.85–0.89 при підлозі 0.03.

## Згладжування цілі між зонами · `підтверджено`, 2026-08-28

### Метод

[`scripts/smooth.py`](scripts/smooth.py). Усередині зони півота ціль має
лишитись такою, як є — саме туди модель влучає. А між зонами, на
середині ноги, ціль смикається слідом за кожним відкотом ціни. Ці
коливання гасяться за трьома правилами: **зони недоторкані**, **нога не
перетікає в ногу** (кожен рух між сусідніми півотами згладжується
окремо), **межі лишаються** — ряд не виходить за `[-1, 1]`.

Коливання міряються між зонами, і саме коливання, а не рух: крок `|Δy|`
майже весь складається з ходу ноги, тому головна мірка —
**шорсткість**, друга різниця ряду `|y[i−1] − 2·y[i] + y[i+1]|`. Поруч —
частка розворотів і звʼязок з фічами на train: гасити має сенс, поки
ціль не втрачає звʼязок із входом.

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

Сирий датасет `BTC_USDT`, між зонами 37 255 подій із 40 242:

| Спосіб | Шорсткість | Падіння | Розвороти | Падіння | Макс \|r\| | Сер \|r\| |
| ------ | ---------: | ------: | --------: | ------: | --------: | -------: |
| сирий | 0.000677 | — | 0.0563 | — | 0.2936 | 0.1667 |
| сер9 | 0.000230 | ×2.94 | 0.0195 | ×2.89 | 0.2936 | 0.1665 |
| сер21 | 0.000144 | ×4.70 | 0.0132 | ×4.26 | 0.2937 | 0.1660 |
| сер41 | 0.000096 | ×7.05 | 0.0086 | ×6.55 | 0.2937 | 0.1651 |
| мед21 | 0.000590 | ×1.15 | 0.0001 | ×563 | 0.2937 | 0.1666 |
| **ема10** | 0.000211 | **×3.21** | 0.0185 | ×3.04 | **0.2943** | 0.1706 |
| **ема20** | 0.000144 | **×4.70** | 0.0132 | ×4.26 | 0.2938 | **0.1725** |
| ема40 | 0.000095 | ×7.13 | 0.0095 | ×5.93 | 0.2901 | 0.1723 |
| пряма | 0.000000 | — | 0.0000 | — | 0.2414 | 0.1211 |

На датасеті із заповненими пропусками (`BTC_USDT_fill`, 66 508 подій)
картина та сама: `ема20` дає ×4.80 при `макс |r|` 0.2060, `ема40` —
×7.33 при 0.2121, `пряма` знову зриває звʼязок (0.1488).

### Висновки

1. **Ковзна медіана коливання не гасить** — вона прибирає розвороти
   (×563), але шорсткість лишає майже такою ж (×1.15): ряд перестає
   смикатись угору-вниз, а сходинки лишаються.
2. **Пряма вставка гасить усе — і звʼязок теж**: `макс |r|` падає з
   0.2936 до 0.2414. Ціль без коливань стає ціллю без інформації.
3. **ЕМА гасить і трохи підсилює звʼязок**: `ема10` дає ×3.21 при
   `макс |r|` 0.2943 (сирий 0.2936), `ема20` — ×4.70 при найбільшій
   середній `|r|` 0.1725. Обидва беруться далі; у роботі стоїть
   **ема20** — вимогу «мінімум удвічі, а то й утричі» він перекриває
   майже вп'ятеро.
4. Ціна ЕМА — **зсув у часі**: причинне середнє відстає приблизно на
   пів періоду. `сер21` дає те саме падіння без зсуву, але з меншим
   звʼязком (0.1660 проти 0.1725).

## Розділ 3. Пачка подій · `підтверджено`, 2026-08-28

### Метод

[`scripts/windows.py`](scripts/windows.py). На вхід іде **вікно з K подій
поспіль** — `(K, 13, 4)`, ціль лишається ціллю **останньої** події вікна.
Родина та сама, але увага тепер по осі подій
([`nets.WindowAttn`](scripts/nets.py)): крок послідовності — ціла подія,
розгорнута в 52 значення, позиція кроку вчиться, зчитування — той самий
навчений запит. Штраф, скейлер, seed і розбиття ті самі, що в розділі 2;
`K` перебирається зовні: 5, 10 … 50.

**Захист від витоку** — потрійний:

1. вікно будується тільки з подій **підряд** (крок рівно 210 с) — розрив
   у даних воно не перестрибує;
2. вікно **цілком лежить в одній вибірці**: ті, що перетинали межу,
   викидаються, і скільки саме — у звіті (`crossing`, 4 … 49 вікон);
3. проміжок між вибірками ріжеться по півотах і має щонайменше 130
   подій — це втричі більше за найдовше вікно, тому дотягнутись до
   сусідньої вибірки воно не може.

Перевірено не тільки кодом: на справжніх даних межі вибірок
`0 … 23 346 | 23 871 … 28 311 | 30 182 … 40 241`, і для K = 5, 20, 50
жодне вікно не виходить за свою межу, а всі кроки всередині вікон рівно
210 с.

Скейлер фіч рахується **на подіях train** і застосовується до всіх вікон.

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

Сирий датасет губить вікна на розривах, тому той самий перебір зроблено
ще й на датасеті **із заповненими пропусками часу** (`BTC_USDT_fill`).
Основна таблиця — по ньому (test, 16 627 подій):

| K | Хвилин | Вікон train/val/test | test MAE | Знак | У зоні | Влучань | З зони | До вершини |
| - | -----: | -------------------- | -------: | ---: | -----: | ------: | -----: | ---------: |
| 5 | 17.5 | 36 672 / 6 928 / 14 959 | 0.543 | 57.1 % | 603 | 41 | 6.8 % | 0.718 |
| 10 | 35 | 33 858 / 6 262 / 13 569 | 0.561 | 57.5 % | 549 | 55 | 10.0 % | **0.653** |
| 15 | 52.5 | 31 520 / 5 752 / 12 378 | 0.566 | 52.8 % | 511 | 0 | 0 % | 0.786 |
| 20 | 70 | 29 520 / 5 340 / 11 389 | 0.611 | 50.7 % | 477 | 0 | 0 % | 0.872 |
| 25 | 87.5 | 27 732 / 4 992 / 10 536 | 0.564 | 51.5 % | 446 | 0 | 0 % | 0.913 |
| 30 | 105 | 26 132 / 4 693 / 9 780 | 0.539 | 52.2 % | 416 | 0 | 0 % | 0.883 |
| 35 | 122.5 | 24 687 / 4 425 / 9 142 | 0.562 | 51.0 % | 395 | 0 | 0 % | 0.806 |
| 40 | 140 | 23 352 / 4 181 / 8 571 | 0.566 | 51.9 % | 377 | 1 | 0.3 % | 0.839 |
| 45 | 157.5 | 22 159 / 3 969 / 8 083 | 0.635 | 50.7 % | 362 | **68** | **18.8 %** | 0.664 |
| 50 | 175 | 21 059 / 3 772 / 7 632 | **0.525** | 54.7 % | 344 | 0 | 0 % | 0.857 |

Той самий перебір на сирому датасеті — для порівняння, скільки вікон
лишається без заповнення пропусків:

| K | Вікон train/val/test | Від подій train | test MAE | Знак | У зоні | Влучань |
| - | -------------------- | --------------: | -------: | ---: | -----: | ------: |
| 5 | 9 514 / 1 356 / 3 011 | 40.8 % | 0.585 | 52.4 % | 237 | 5 |
| 10 | 5 829 / 626 / 1 485 | 25.0 % | 0.511 | 53.1 % | 103 | 0 |
| 20 | 3 161 / 254 / 537 | 13.5 % | 0.591 | 50.5 % | 23 | 0 |
| 30 | 1 924 / 151 / 223 | 8.2 % | 0.481 | 78.5 % | 10 | 4 |
| 40 | 1 213 / 98 / 109 | 5.2 % | 0.730 | 45.0 % | 6 | 0 |
| 50 | 797 / 59 / 55 | 3.4 % | 0.910 | 40.0 % | 2 | 0 |

### Висновки

1. **Контекст не допоміг.** Ні MAE (0.525 … 0.635 проти 0.554 в одної
   події), ні знак (50.7 … 57.5 % проти 57.1 %) не ростуть з K. Кривої
   немає — є розкид навколо тих самих чисел.
2. **Влучання стрибають, а не ростуть**: 6.8 % при K = 5, 10 % при
   K = 10, нуль на K = 15 … 35, 18.8 % при K = 45 і знову нуль при
   K = 50. Сусідні K дають протилежний результат, тому 18.8 % — не
   знахідка, а нестійкість навчання під штрафом за розмах: модель або
   каже «вершина» часто, або не каже взагалі.
3. **Сирий датасет для довгих вікон не годиться**: при K = 50 лишається
   3.4 % подій train і 55 вікон на test, а «78.5 % знаку» при K = 30 —
   це 223 вікна й 10 подій у зоні, тобто шум. Порівнювати K між собою
   можна тільки на заповненому датасеті.
4. Захист від витоку відпрацював: на межах вибірок викинуто 4 … 49
   вікон, і жодне вікно не змішує дві вибірки.

## Розділ 4. Моделі на згладженій цілі · `підтверджено`, 2026-08-28

### Метод

[`scripts/final_models.py`](scripts/final_models.py). Ціль — **згладжена
`ема20`** (коливання між зонами впали в 4.8 раза), датасет — із
заповненими пропусками часу, вхід — **вікно з K подій**. Узяті два
розміри, які дали найбільше влучань у розділі 3: **K = 10** і **K = 45**.

Розбиття: датасет ділиться тільки на `train | val`, а роль test грає
**невідомий відрізок** після його кінця. Межа `train | val` ріжеться по
півоту з проміжком не меншим за 130 подій. Вибір скрізь — по val і по
одній мірці: **влучання в зону півота**, за рівних — менший MAE.

Кроки: грід по ручках родини (24 конфігурації: ширина × шари × dropout ×
lr), фінальна модель кращої конфігурації зі збереженням ваг, потім
**викидання фіч по одній** — від найслабшої за |r| з ціллю на train,
12 кроків. Фіча викидається маскою: клітинка `(період, фіча)` після
скейлера зануляється, архітектура не міняється.

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

Фінальні моделі:

| Модель | Конфігурація | Вікон train/val | val MAE | Знак | Пірсон | Влучань | З зони | Точність |
| ------ | ------------ | --------------- | ------: | ---: | -----: | ------: | -----: | -------: |
| K = 10 | ширина 64, 1 шар, dropout 0.3, lr 1e-3 | 42 998 / 10 822 | 0.559 | 61.8 % | 0.226 | 57 з 463 | 12.3 % | 4.2 % |
| K = 45 | ширина 64, 2 шари, dropout 0.3, lr 3e-4 | 27 825 / 6 483 | 0.599 | 59.3 % | 0.291 | 61 з 325 | **18.8 %** | 5.4 % |

Викидання фіч (val, найслабші три однакові в обох: `price_balance ема10`
|r| 0.068, `d_balance ема10` 0.085, `price_balance ема20` 0.087):

| Викинуто | K = 10: влучань | K = 10: MAE | K = 45: влучань | K = 45: MAE |
| -------- | --------------: | ----------: | --------------: | ----------: |
| 0 | 12.3 % | 0.559 | 18.8 % | 0.599 |
| 1 | **14.9 %** | 0.560 | 18.5 % | 0.605 |
| 2 | 14.7 % | 0.559 | **19.7 %** | 0.610 |
| 4 | 10.4 % | 0.544 | 15.7 % | 0.588 |
| 8 | 0.2 % | 0.534 | 4.6 % | 0.573 |
| 12 | 4.1 % | 0.537 | 11.1 % | 0.578 |

**Невідомий відрізок** (2026-08-16 20:00 … 08-27 00:00 UTC, 4 171 подія
після заповнення пропусків, 4 зони, 191 подія в зонах):

| Модель | Вікон | MAE | R² | Пірсон | Знак | У зоні | Сказала | Влучань |
| ------ | ----: | --: | -: | -----: | ---: | -----: | ------: | ------: |
| K = 10 | 3 423 | 0.710 | −1.03 | −0.211 | 45.7 % | 153 | 363 | 24 |
| K = 45 | 2 072 | 0.742 | −1.07 | −0.232 | 46.2 % | 77 | 189 | **0** |

### Висновки

1. **На val обидві моделі щось ловлять, на невідомому — ні.** K = 45 бере
   18.8 % зони на val і **жодного разу** не влучає на невідомому
   відрізку; K = 10 бере 12.3 % на val і 24 влучання з 153 (15.7 %), але
   з 363 заяв «вершина» — точність 6.6 %.
2. **Знак і Пірсон перевертаються на невідомому**: 61.8 % і +0.226 на
   val проти 45.7 % і −0.211 після 16 серпня. Те саме в K = 45. Це та
   сама картина, що в розділі 2 з однією подією: за межею навченого
   відрізка сигналу немає.
3. **Викидання найслабшої фічі не шкодить, а трохи допомагає**: +2.6 п.п.
   влучань у K = 10 (одна фіча) і +0.9 п.п. у K = 45 (дві). Далі, з
   четвертої фічі, влучання просідають, хоча MAE навіть падає — модель
   стає обережнішою й перестає доходити до вершин.
4. **Згладжування ціль не зіпсувало**: `макс |r|` фіч із ціллю навіть
   трохи виріс (0.2060 проти 0.2022 сирої), а коливання між зонами
   впали в 4.8 раза.
5. **Обидві моделі лишаються в дослідженні**: вони не однакові — K = 45
   бере більше зони на val, K = 10 єдиний, хто взагалі влучає на
   невідомому відрізку. Але жодна з них не придатна до торгівлі: обидві
   програють монетці там, де їх ніхто не вчив.

## Підсумок

Дослідження закрило своє питання: **`wattn` не влучає в зону півота
зігзага 2.3% за межами навченого відрізка.**

| Що зроблено | Результат |
| ----------- | --------- |
| Мітка (етап 1) | зігзаг 2.3% на **ема90**, `\|r\|` 0.2936, 23 927 подій |
| Нога (розділ 1) | **розподіл** (minmax): звʼязок сильніший за квантиль і вимірюється в русі ціни |
| Зона півота (розділ 1) | `\|y\| ≥ 0.90` **і** ±40 подій — 6.5 % ряду, рівно одне вікно на півот, чужих півотів немає |
| Штраф (розділ 2) | розмах ставиться прямо в лоссі: σ виходу 0.969 від σ цілі |
| Одна подія (розділ 2) | зона на train 65 % влучань, на test 3.8 % — це запамʼятовування |
| Згладжування | **ема20**: коливання між зонами ×4.7, звʼязок з фічами не втрачено |
| Пачка подій (розділ 3) | K не дає кривої: влучання стрибають 0 … 18.8 % на сусідніх K |
| Дві моделі (розділ 4) | K = 10 і K = 45 після гріду: на val 12.3 % і 18.8 % зони |
| Фічі (розділ 4) | одна-дві найслабші зайві: +2.6 і +0.9 п.п. влучань |
| Невідомий відрізок | K = 10 — 24 влучання з 153 при 363 заявах, K = 45 — **0** |

Три висновки, які переносяться далі:

1. **Розрив train ↔ решта — головне, що видно в усіх розділах.** На
   навченому відрізку зона береться на 65 %, поза ним — на 4 %. Ні
   контекст (розділ 3), ні згладжена ціль з грідом (розділ 4) цього не
   змінили: 26 049 параметрів на 23 тисячі подій вистачає, щоб
   запамʼятати, і не вистачає, щоб узагальнити.
2. **Знак перевертається за межею датасету**: 61.8 % і Пірсон +0.226 на
   val проти 45.7 % і −0.211 після 16 серпня. Це не слабкий сигнал, це
   відсутність сигналу — модель гірша за монетку там, де її не вчили.
3. **Постановка метрики себе виправдала.** Зона з двох чисел
   (`\|y\| ≥ 0.90`, ±40 подій) стабільно бере 6–7 % подій на будь-якому
   відрізку, і саме вона показала розрив, якого не видно ні по MAE
   (0.55 проти 0.48 у константи), ні по σ. Її варто нести в наступні
   дослідження як мірку, а не рахувати MAE по всій нозі.

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

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

```bash
# етап 1 — підбір мітки (обидва пороги, беремо 2.3)
python3 scripts/target_pick.py --pair BTC_USDT \
    --out research/16-zigzag23-wattn/results/target_pick.json

# розділ 1 — нормалізації ноги зігзага 2.3 на ема90 і вибір ноги
python3 research/16-zigzag23-wattn/scripts/legs.py --target 2.3
python3 research/16-zigzag23-wattn/scripts/leg_pick.py --target 2.3
python3 research/16-zigzag23-wattn/scripts/pivot_band.py --target 2.3

# розділ 2 — мережа wattn під штрафом за розмах (зберігає ваги й скейлер)
python3 research/16-zigzag23-wattn/scripts/wattn_losses.py --target 2.3

# згладжування цілі між зонами (обидва датасети)
python3 research/16-zigzag23-wattn/scripts/smooth.py --target 2.3
python3 research/16-zigzag23-wattn/scripts/smooth.py --target 2.3 \
    --pair BTC_USDT_fill

# розділ 3 — пачка подій: K = 5 … 50, сирий і заповнений датасети
python3 research/16-zigzag23-wattn/scripts/windows.py --target 2.3
python3 research/16-zigzag23-wattn/scripts/windows.py --target 2.3 \
    --pair BTC_USDT_fill

# розділ 4 — моделі на згладженій цілі: грід, фінальна, фічі, невідомий
for K in 10 45; do
  python3 research/16-zigzag23-wattn/scripts/final_models.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step grid
  python3 research/16-zigzag23-wattn/scripts/final_models.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step final
  python3 research/16-zigzag23-wattn/scripts/final_models.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step ablation --steps 12
  python3 research/16-zigzag23-wattn/scripts/fresh.py \
      --run final_fill_ема20_k$K
done

# видача: увесь датасет і відрізок після його кінця
python3 research/16-zigzag23-wattn/scripts/predict.py --target 2.3
python3 research/16-zigzag23-wattn/scripts/fresh.py \
    --from '2026-08-16 20:00' --to '2026-08-27 00:00'

# тести
python3 -m unittest discover -s tests -t tests -p "test_zigzag23.py"
python3 -m unittest discover -s tests -t tests -p "test_dashboard.py"
```

Потрібен готовий `data/datasets/BTC_USDT/class210x4bema10x10x130/` з
цілями на ЕМА ціни (`<part>_targets.npz`) — як він будується,
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х4бема10х10х130).
Нічого зовнішнього. Прогін розділу 2 — близько 40 секунд на MPS, `fresh.py` — близько
хвилини (28 добових CSV через увесь ланцюг класів).

## Файли

| Файл | Що |
| ---- | -- |
| [`scripts/nets.py`](scripts/nets.py) | мережа `wattn` і конфігурація прогону |
| [`scripts/vertex.py`](scripts/vertex.py) | відстань до вершин цілі −1 і +1 в околах |
| [`scripts/legs.py`](scripts/legs.py) | розділ 1: нормалізації ноги зігзага 2.3 на ема90 |
| [`scripts/leg_pick.py`](scripts/leg_pick.py) | розділ 1: вибір ноги — звʼязок з фічами і розподіл біля вершин |
| [`scripts/pivot_band.py`](scripts/pivot_band.py) | розділ 1: зона півота — межі розподілу × вікно вліво-вправо |
| [`scripts/wattn_losses.py`](scripts/wattn_losses.py) | розділ 2: прогін під штрафом за розмах, збереження ваг |
| [`scripts/smooth.py`](scripts/smooth.py) | згладжування цілі між зонами і мірки коливань |
| [`scripts/windows.py`](scripts/windows.py) | розділ 3: вікна з K подій, захист від витоку, перебір K |
| [`scripts/final_models.py`](scripts/final_models.py) | розділ 4: грід, фінальні моделі, викидання фіч |
| [`scripts/saved.py`](scripts/saved.py) | завантаження збереженої `wattn` повз загальний реєстр моделей |
| [`scripts/predict.py`](scripts/predict.py) | видача на всьому датасеті: ряди, зони, влучання |
| [`scripts/fresh.py`](scripts/fresh.py) | те саме на відрізку після кінця датасету |
| [`results/target_pick.json`](results/target_pick.json) | етап 1: кореляції фіч з кандидатами |
| [`results/legs_2_3.json`](results/legs_2_3.json) | розділ 1: ряди «розподіл» і «розподіл згладжений», зони півотів |
| [`results/legs_2_3.meta.json`](results/legs_2_3.meta.json) | той самий звіт без гістограм і вікна — картка дослідження |
| [`results/leg_pick_2_3.json`](results/leg_pick_2_3.json) | розділ 1: числа вибору ноги |
| [`results/pivot_band_2_3.json`](results/pivot_band_2_3.json) | розділ 1: перебір зон півота і чисті з них |
| [`results/losses_wattn_2_3.json`](results/losses_wattn_2_3.json) | розділ 2: метрики, вершини й розподіли прогнозів |
| [`models/wattn_2_3_spread_v1/`](models/wattn_2_3_spread_v1/) | ваги моделі й квантильний скейлер її train |
| [`results/smooth_2_3.json`](results/smooth_2_3.json) | згладжування: мірки коливань і звʼязку (сирий датасет) |
| [`results/smooth_fill_2_3.json`](results/smooth_fill_2_3.json) | те саме на заповненому датасеті; поруч `.npz` з рядами |
| [`results/windows_2_3.json`](results/windows_2_3.json) | розділ 3: перебір K на сирому датасеті |
| [`results/windows_fill_2_3.json`](results/windows_fill_2_3.json) | розділ 3: те саме на датасеті із заповненими пропусками |
| [`results/grid_fill_ема20_k10_2_3.json`](results/grid_fill_ема20_k10_2_3.json), [`…k45…`](results/grid_fill_ема20_k45_2_3.json) | розділ 4: гріди по ручках |
| [`results/final_fill_ема20_k10_2_3.json`](results/final_fill_ема20_k10_2_3.json), [`…k45…`](results/final_fill_ема20_k45_2_3.json) | розділ 4: фінальні моделі |
| [`results/ablation_fill_ема20_k10_2_3.json`](results/ablation_fill_ема20_k10_2_3.json), [`…k45…`](results/ablation_fill_ема20_k45_2_3.json) | розділ 4: викидання фіч |
| [`results/fresh_final_fill_ема20_k10_2_3.json`](results/fresh_final_fill_ема20_k10_2_3.json), [`…k45…`](results/fresh_final_fill_ема20_k45_2_3.json) | розділ 4: невідомий відрізок кожної моделі |
| [`results/predict_2_3.json`](results/predict_2_3.json) | видача на всьому датасеті: ряди, зони, метрики |
| [`results/fresh_2_3.json`](results/fresh_2_3.json) | невідомий відрізок: ті самі ряди й метрики |
| [`research/12-zigzag-net/scripts/train.py`](../12-zigzag-net/scripts/train.py) | навчання й метрики; `wattn` у його реєстр **не** додається — родина подається `factory` |
| [`research/12-zigzag-net/scripts/losses.py`](../12-zigzag-net/scripts/losses.py) | перелік функцій втрат і калібрування |
| [`research/12-zigzag-net/scripts/walk_forward.py`](../12-zigzag-net/scripts/walk_forward.py) | розбиття по часу і прогін на вікні |
| [`rules/scripts/zigzag_targets.py`](../../rules/scripts/zigzag_targets.py) | цілі: зігзаг на ЕМА ціни, нормалізація ноги |
| [`tests/test_zigzag23.py`](../../tests/test_zigzag23.py) | тести мережі, вершин і розділу 1 |
| [`tests/test_dashboard.py`](../../tests/test_dashboard.py) | тести вкладки «Навчання моделі 2.3» |

## Вкладка

[Навчання моделі 2.3](http://localhost:8080/training23):

| Блок | Що показує | Дані |
| ---- | ---------- | ---- |
| Розділ 1 | нога зігзага 2.3 на ема90 — увесь датасет, ряд «розподіл», згладжений ряд і **зони півотів прямо на графіку**; вибір ноги і перебір зон таблицями | `/api/training23` |
| Розділ 2 | штраф за розмах: метрики, відстань до вершин, зона по вибірках, розподіл прогнозів | `/api/training23` |
| Розділ 3 | пачка подій, K = 5 … 50, обидва датасети | `/api/training23/windows` |
| Розділ 4 | грід, фінальні моделі, викидання фіч і невідомий відрізок кожної | `/api/training23/models` |
| Видача моделі | ряди ціни, цілі й прогнозу з зонами — увесь датасет і невідомий відрізок | `/api/training23/predict`, `/api/training23/fresh` |
| Етап 1 | підбір мітки, тільки поріг 2.3 | `/api/training23` |

Шляхи до файлів звітів — [`dashboard/app.py`](../../dashboard/app.py) →
`WATTN_DIR`.
