# 17. Трансформер 2.3

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

## Ціль

Повторити постановку [дослідження 16](../16-zigzag23-wattn/16-zigzag23-wattn.md)
на іншій мережі — `transformer` — і побачити, що змінює **зчитування**.
Дані, мітка, нога, зона півота, функція втрат, розбиття, скейлер, seed і
метрики ті самі, тому кожне число тут порівнюється з `wattn` напряму.

На відміну від 16, мережа тут не задана готовою: її ручки **підбираються**
(розділ 2 — грубий грід, точний грід в околі кращої, тривалість
навчання), і вже підібраними йдуть у розділи 3 і 4.

Ціль міряється **відстанню до своїх вершин**: нормована нога — це `−1` у
нижньому півоті й `+1` у верхньому, і питання не «яке число видасть
модель у середньому», а **чи доходить видача до вершини**. Робочий окіл
один — `0.1`, поверх нього стоїть робоча зона півота `|y| ≥ 0.90` і
±40 подій (розділ 1 дослідження 16).

## Дані

| Що | Значення |
| -- | -------- |
| Клас фіч | [`клас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 подій |
| Джерело | `data/datasets/BTC_USDT/class210x4bema10x10x130/` |

Мітку (`ема90 × 2.3`), ногу (`розподіл`), зону півота (`|y| ≥ 0.90`, ±40)
і згладжування цілі (`ема20`) дослідження **не перевибирає**: це
властивості даних, а не мережі, і вони встановлені етапом 1 та розділами
1 і «Згладжування» дослідження 16. Згладжений ряд лежить поруч
(`results/smooth_fill_2_3.npz`), щоб дослідження відтворювалось зі своєї
папки.

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

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

## Мережа `transformer`

Родина береться готовою з дослідження 12 (`train.MODELS`), а не пишеться
заново ([`scripts/nets.py`](scripts/nets.py)). Крок послідовності — один
період ЕМА (10 … 130), позиція кроку вчиться окремим параметром.

Уся відмінність від `wattn` — **зчитування**:

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

`transformer` розмазує відповідь по всіх 13 періодах рівно, `wattn`
вирішує навченим запитом, з яких періодів її брати. Решта — та сама
`nn.TransformerEncoder`.

### Скільки чисел доходить до мережі

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

| Форма | Крок послідовності | Чисел на крок | Перший шар |
| ----- | ------------------ | ------------: | ---------- |
| одна подія | період ЕМА | **4** | `Linear(4 → width)`, будь-яка ширина гріду ≥ 4 |
| вікно з K подій | ціла подія | **52** = 13 × 4 | `Linear(52 → width)`, ширина **≥ 52** |

Тому у вікні ширина піднімається до найближчої, яка вміщує подію цілком і
ділиться на `heads` (`nets.window_width`, `nets.window_config`), а
вужчі ширини з гріду розділу 4 просто прибираються. `WindowTransformer`
з шириною меншою за 52 не будується взагалі — це помилка, а не гірший
варіант.

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

### Метод

[`scripts/losses_run.py`](scripts/losses_run.py) над
[`wattn_losses.py`](../16-zigzag23-wattn/scripts/wattn_losses.py)
дослідження 16: код кроку той самий, підмінена тільки родина
([`scripts/base.py`](scripts/base.py)). Функція втрат одна — штраф за
розмах:

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

Ручки типові для родини: `width` 32, `heads` 4, `layers` 2, `dropout` 0.1,
lr 1e-3, батч 256, seed 0, α 3.0, β 2.0, 400 епох, терпіння 30. Розбиття:
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) | у зоні | епоха |
| ------- | --- | -- | ------ | ---- | --------- | -------------------- | ------ | ----- |
| `transformer` | **0.4757** | **−0.046** | **0.2823** | **0.5781** | 0.629 | **0.843** | **0.858** | 3 |
| `wattn` (дослідження 16) | 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 | — |

Робоча зона `|y| ≥ 0.90` і ±40 подій, test: 653 події (6.6 %), сама ціль
стоїть за `0.031` від вершини. Модель сказала «вершина» 16 разів,
влучила 7 — з зони взято 1.1 %, точність 43.8 %.

### Висновки

1. **Помилка менша, розмах менший.** MAE 0.476 проти 0.554 в `wattn` і
   0.479 у константи, R² −0.046 проти −0.462: усереднене зчитування
   тримається біля константи замість того, щоб розтягуватись. Ціна цього
   — σ 0.629 від цілі проти 0.969 у `wattn`: штраф за розмах трансформер
   виконує наполовину.
2. **До вершини це не наближає**: 0.843 в околі 0.1 проти 0.821 у
   `wattn` при підлозі 0.041. Обидві мережі проходять близько п'ятої
   частини потрібної відстані.
3. **Обережність міняє характер помилок.** `wattn` каже «вершина» 798
   разів на test і влучає 25 (точність 3 %), `transformer` — 16 разів і
   влучає 7 (точність 44 %). З зони при цьому взято 1.1 % проти 3.8 %:
   модель мовчить майже завжди.
4. **Зупинка на 3-й епосі** — привід перевірити ручки: під типовими
   значеннями val перестає покращуватись одразу. Цим і займається
   розділ 2.

## Розділ 2. Ручки мережі · `підтверджено`, 2026-08-28

### Метод

[`scripts/grid.py`](scripts/grid.py), три кроки стандарту
`rules/03-models.md` → Типи мереж → Тестові:

| Крок | Що перебирається |
| ---- | ---------------- |
| 2.1 грубий грід | ширина (16, 32, 64) × шари (1, 2) × dropout (0.1, 0.3) × lr (3e-4, 1e-3) — 24 конфігурації |
| 2.2 точний грід | окіл кращої: сусідні ширина, dropout і lr по драбині значень, шари від кращої — 27 конфігурацій |
| 2.3 тривалість | терпіння ранньої зупинки: 10, 30, 60 на обраній конфігурації |

Дані, ціль, розбиття, скейлер, штраф і seed ті самі, що в розділі 1.
Вибір **тільки по val**, test у підборі не бере участі. Мірка вибору —
**влучання в зону півота** (`vertex.hits.recall`), за рівних — менший
MAE: дослідження міряє попадання в зону, ним і обирається.

Бюджет кроків 2.1 і 2.2 — 200 епох, терпіння 20; крок 2.3 іде на повному
бюджеті (400 епох).

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

**2.1 Грубий грід**, три найкращі з 24 (val, 4 441 подія, 275 у зоні):

| Конфігурація | Параметрів | val MAE | знак | Пірсон | Влучань | З зони | Точність | Епоха |
| ------------ | ---------: | ------: | ---: | -----: | ------: | -----: | -------: | ----: |
| **ширина 64, 2 шари, dropout 0.1, lr 3e-4** | 101 185 | 0.626 | 48.8 % | 0.068 | **49** | **17.8 %** | 16.3 % | 20 |
| ширина 64, 1 шар, dropout 0.1, lr 1e-3 | 51 201 | 0.627 | 49.6 % | 0.078 | 39 | 14.2 % | 10.0 % | 29 |
| ширина 16, 2 шари, dropout 0.3, lr 1e-3 | 6 865 | 0.634 | 49.8 % | 0.098 | 26 | 9.5 % | 7.8 % | 30 |

Решта 21 конфігурація дала **0 влучань** з 275: 14 із них зупинились на
2–8 епосі, так і не почавши доходити до вершин.

**2.2 Точний грід** в околі кращої (ширини 48/64/96 × dropout
0.05/0.1/0.2 × lr 1e-4/3e-4/5e-4, 2 шари):

| Конфігурація | Параметрів | val MAE | знак | Пірсон | Влучань | З зони | Епоха |
| ------------ | ---------: | ------: | ---: | -----: | ------: | -----: | ----: |
| **ширина 64, dropout 0.1, lr 3e-4** | 101 185 | 0.626 | 48.8 % | 0.068 | **49** | **17.8 %** | 20 |
| ширина 96, dropout 0.05, lr 5e-4 | 225 505 | 0.631 | 45.6 % | −0.007 | 17 | 6.2 % | 18 |
| ширина 96, dropout 0.1, lr 3e-4 | 225 505 | 0.626 | 45.3 % | 0.007 | 12 | 4.4 % | 8 |

Окіл кращу не побив: 21 з 27 сусідів дали 0 влучань, і перемогла та сама
конфігурація, що вийшла з грубого гріду.

**2.3 Тривалість навчання** на ній, бюджет 400 епох:

| Терпіння | Краща епоха | val MAE | Влучань | З зони |
| -------- | ----------: | ------: | ------: | -----: |
| 10 | 3 | **0.615** | 0 | 0 % |
| **30** | **20** | 0.626 | **49** | **17.8 %** |
| 60 | 20 | 0.626 | 49 | 17.8 % |

### Ручки під подальшу роботу

```
width 64, heads 4, layers 2, dropout 0.1, lr 3e-4, батч 256, seed 0
терпіння 30, бюджет 400 епох, краща епоха 20
```

Ця сама конфігурація, прогнана на розбитті розділу 1
(`losses_run.py --tag picked`), на test:

| Прогноз | MAE | R² | Пірсон | знак | σ до цілі | до вершин | у зоні | Сказала | Влучань | З зони | Точність |
| ------- | --- | -- | ------ | ---- | --------- | --------- | ------ | ------: | ------: | -----: | -------: |
| підібрані ручки | 0.630 | −0.851 | 0.243 | 54.6 % | **1.064** | 0.840 | 0.861 | 1 758 | 30 | 4.6 % | 1.7 % |
| типові ручки (розділ 1) | **0.476** | **−0.046** | **0.282** | **57.8 %** | 0.629 | 0.843 | 0.858 | 16 | 7 | 1.1 % | **43.8 %** |

### Висновки

1. **Грід нічого не знайшов за межами першої знахідки.** 21 з 24
   конфігурацій грубого гріду і 21 з 27 точного не влучають у зону
   жодного разу; окіл кращої не дав нічого кращого за неї саму. Це не
   плато навколо оптимуму, а поодинокий прогін, який відірвався від
   решти.
2. **Рання зупинка ховала єдиний робочий режим.** З терпінням 10
   навчання спиняється на 3-й епосі й дає 0 влучань, з терпінням 30 —
   доходить до 20-ї і бере 17.8 % зони. Терпіння 60 повторює 30 подія в
   подію: після 20-ї епохи val більше не покращується. Саме тому
   тривалість навчання тут окремий крок, а не ручка за замовчуванням.
3. **Влучання в зону і помилка тягнуть у різні боки.** Підібрані ручки
   ставлять σ рівно на місце (1.064 від цілі проти 0.629 у типових) і
   беруть учетверо більше зони — але MAE росте з 0.476 до 0.630, R²
   падає до −0.851, а точність слова «вершина» — з 43.8 % до 1.7 %:
   модель каже «вершина» 1 758 разів на 9 861 подію. Ширший розмах — це
   більше влучань і на порядок більше хибних тривог.
4. Обидві конфігурації однаково далекі від самих вершин: 0.840 і 0.843 в
   околі `0.1` при підлозі 0.041. Ручки міняють поведінку виходу, а не
   те, наскільки мережа бачить вершину.

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

### Метод

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

Ручки — підібрані розділом 2 (ширина 64, 2 шари, dropout 0.1, lr 3e-4,
терпіння 30). Ширина тут не просто підходить, а **обов'язкова**: крок
послідовності — 52 числа, і 64 — перша ширина драбини, яка вміщує подію
цілком.

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

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

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

| K | Хвилин | Вікон train/val/test | test MAE | Знак | У зоні | Влучань | З зони | До вершини |
| - | -----: | -------------------- | -------: | ---: | -----: | ------: | -----: | ---------: |
| 5 | 17.5 | 36 672 / 6 928 / 14 959 | 0.544 | 56.7 % | 603 | 55 | 9.1 % | 0.746 |
| 10 | 35 | 33 858 / 6 262 / 13 569 | 0.571 | 53.5 % | 549 | **109** | 19.9 % | 0.787 |
| 15 | 52.5 | 31 520 / 5 752 / 12 378 | 0.541 | 54.1 % | 511 | 0 | 0 % | 0.783 |
| 20 | 70 | 29 520 / 5 340 / 11 389 | 0.549 | 53.3 % | 477 | 0 | 0 % | 0.773 |
| 25 | 87.5 | 27 732 / 4 992 / 10 536 | 0.517 | 53.1 % | 446 | 0 | 0 % | 0.858 |
| 30 | 105 | 26 132 / 4 693 / 9 780 | 0.551 | 52.2 % | 416 | 19 | 4.6 % | 0.788 |
| 35 | 122.5 | 24 687 / 4 425 / 9 142 | 0.560 | 52.4 % | 395 | 56 | 14.2 % | 0.729 |
| 40 | 140 | 23 352 / 4 181 / 8 571 | 0.522 | 54.5 % | 377 | 0 | 0 % | 0.756 |
| 45 | 157.5 | 22 159 / 3 969 / 8 083 | 0.579 | 52.0 % | 362 | **93** | **25.7 %** | **0.714** |
| 50 | 175 | 21 059 / 3 772 / 7 632 | 0.512 | 52.7 % | 344 | 0 | 0 % | 0.886 |

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

| K | Вікон train/val/test | Від подій train | test MAE | Знак | У зоні | Влучань |
| - | -------------------- | --------------: | -------: | ---: | -----: | ------: |
| 5 | 9 514 / 1 356 / 3 011 | 40.8 % | 0.558 | 56.2 % | 237 | 3 |
| 10 | 5 829 / 626 / 1 485 | 25.0 % | 0.614 | 49.3 % | 103 | 7 |
| 20 | 3 161 / 254 / 537 | 13.5 % | 0.552 | 52.1 % | 23 | 3 |
| 30 | 1 924 / 151 / 223 | 8.2 % | 0.660 | 70.9 % | 10 | 4 |
| 40 | 1 213 / 98 / 109 | 5.2 % | 0.619 | 86.2 % | 6 | 0 |
| 50 | 797 / 59 / 55 | 3.4 % | 0.977 | 40.0 % | 2 | 0 |

### Висновки

1. **Контекст не додає ні точності, ні напрямку.** MAE тримається в
   0.512 … 0.579 при 0.630 в одної події, знак — 52 … 57 % без жодної
   залежності від K. Кривої немає, є розкид навколо тих самих чисел.
2. **Влучання стрибають, а не ростуть**: 9.1 % при K = 5, 19.9 % при
   K = 10, нуль на 15, 20, 25, 4.6 % на 30, 14.2 % на 35, 25.7 % на 45 і
   знову нуль на 50. Сусідні K дають протилежний результат — це
   нестійкість навчання під штрафом за розмах, а не властивість
   довжини вікна. Те саме бачило дослідження 16 на `wattn`.
3. **Трансформер влучає частіше за `wattn`** на тих самих K: 19.9 %
   проти 10.0 % на K = 10 і 25.7 % проти 18.8 % на K = 45. Але нулі
   стоять на тих самих місцях, тому це та сама нестійкість, тільки з
   більшою амплітудою.
4. **Сирий датасет для довгих вікон не годиться**: при K = 50 лишається
   3.4 % подій train і 55 вікон на test, а «86.2 % знаку» при K = 40 —
   це 109 вікон і 6 подій у зоні. Порівнювати K між собою можна тільки
   на заповненому датасеті.
5. Захист від витоку відпрацював: на межах вибірок викинуто 4 … 49
   вікон, жодне вікно не змішує дві вибірки.

**У розділ 4 ідуть K = 45 і K = 10** — два розміри з найбільшими
влучаннями.

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

### Метод

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

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

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

Ширини гріду починаються з тієї, що вміщує подію цілком (52 числа):
K = 10 іде по 64/96/128 — 24 конфігурації, як у дослідженні 16; K = 45 —
по ширині 64, 8 конфігурацій. Довге вікно на ширинах 96 і 128 коштує
10–20 хвилин на конфігурацію, і повний перебір там дорожчий за все
дослідження разом.

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

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

| Модель | Конфігурація | Вікон train/val | val MAE | Знак | Пірсон | Влучань | З зони | Точність | Епоха |
| ------ | ------------ | --------------- | ------: | ---: | -----: | ------: | -----: | -------: | ----: |
| K = 10 | ширина 128, 1 шар, dropout 0.3, lr 1e-3 | 42 998 / 10 822 | 0.561 | 60.3 % | 0.227 | **62 з 463** | **13.4 %** | 6.3 % | 1 |
| K = 45 | ширина 64, 2 шари, dropout 0.3, lr 1e-3 | 27 825 / 6 483 | 0.586 | 56.8 % | 0.161 | 27 з 325 | 8.3 % | 8.2 % | 64 |

Викидання фіч (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 | **13.4 %** | 0.561 | 8.3 % | 0.586 |
| 1 | 11.0 % | 0.554 | 7.7 % | 0.625 |
| 4 | 9.1 % | 0.553 | 0 % | 0.618 |
| 8 | 1.7 % | 0.552 | 10.2 % | 0.642 |
| 9 | 0.7 % | 0.545 | **10.8 %** | 0.625 |
| 12 | 5.2 % | 0.556 | 10.5 % | 0.641 |

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

| Модель | Вікон | MAE | R² | Пірсон | Знак | У зоні | Сказала | Влучань | З зони |
| ------ | ----: | --: | -: | -----: | ---: | -----: | ------: | ------: | -----: |
| K = 10 | 3 423 | 0.736 | −1.17 | −0.214 | 43.1 % | 155 | 253 | **9** | 5.8 % |
| K = 45 | 2 072 | 0.669 | −0.72 | 0.030 | 52.8 % | 77 | 101 | **0** | 0 % |

### Висновки

1. **На val обидві моделі щось беруть, на невідомому — майже нічого.**
   K = 10 бере 13.4 % зони на val і 5.8 % після 16 серпня, K = 45 — 8.3 %
   на val і **жодного** влучання на невідомому. Та сама картина, що в
   `wattn` (12.3 % / 15.7 % і 18.8 % / 0 %).
2. **Знак і Пірсон перевертаються за межею навченого відрізка**: 60.3 % і
   +0.227 на val проти 43.1 % і −0.214 після 16 серпня в K = 10. K = 45
   тримається біля монетки з обох боків (56.8 % і 52.8 %) — не тому, що
   переносить сигнал, а тому, що його там мало з самого початку.
3. **Краща конфігурація K = 10 зупиняється на першій епосі.** Її 206 465
   параметрів на 42 998 вікон: val лосс найкращий одразу після першого
   проходу і далі тільки росте. Модель, що виграла грід, — це майже
   неначена модель, і 13.4 % зони вона бере випадковим розкидом виходу,
   а не вивченим правилом.
4. **Викидання фіч працює по-різному на різних вікнах.** На K = 10
   будь-яке викидання шкодить (13.4 % → 11.0 % з першої фічі, обвал до
   1.7 % на восьмій). На K = 45 навпаки: після восьмої-девʼятої фічі
   влучання ростуть з 8.3 % до 10.8 %, при тому що MAE росте теж. Обидві
   криві немонотонні — це знову нестійкість прогону, а не знайдена
   підмножина фіч.
5. **Згладжена ціль дала те саме, що в дослідженні 16**: цифри в тих
   самих межах, висновок теж — жодна з двох моделей не придатна до
   торгівлі, обидві програють монетці там, де їх ніхто не вчив.

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

### Картка моделі

Усе, чим модель відтворюється з нуля. Ваги й скейлер —
`models/transformer_2_3_spread_picked_v1/` (`model.pt`, `scaler.npz`).

| Що | Значення |
| -- | -------- |
| Родина | `transformer` — увага по осі періодів ЕМА, зчитування `mean` |
| Вхід | одна подія `(13, 4)` — **усі 52 фічі, жодна не викинута** |
| Ширина / голови / шари / dropout | 64 / 4 / 2 / 0.1 |
| Перший шар | `Linear(4 → 64)` на кожен із 13 періодів, спільні ваги |
| Параметрів | 101 185 |
| lr / батч / seed | 3e-4 / 256 / 0 |
| Оптимізатор | Adam |
| Епох / терпіння / краща епоха | 400 / 30 / **20** |
| Лосс | `WMSE + λ·(σ_цілі − σ_прогнозу)²`, λ = 1, `w = 1 + α·|y|^β`, α = 3.0, β = 2.0 |
| Скейлер фіч | квантиль, порахований **тільки на train**, поклітинково по `(період, фіча)` |
| Ціль | зігзаг 2.3 % нормований, **ема90** ціни, нога `розподіл` (minmax) |
| Розбиття | по часу: train 23 072 / val 4 441 / test 9 861, проміжки по півотах ≥ 130 подій |
| Звіт прогону | [`results/picked_model_2_3.json`](results/picked_model_2_3.json) |

Фічі — усі 13 періодів ЕМА × 4 фічі класу
[`клас210х4бема10х10х130`](../../rules/01-data-and-db.md#клас210х4бема10х10х130):

```
періоди ЕМА  10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120, 130
фічі         balance, d_balance, vol_balance, price_balance
```

Ручки взяті з розділу 2 (грубий грід → точний грід → тривалість
навчання), решта — з розділу 1. Прогін:

```bash
python3 research/17-zigzag23-transformer/scripts/losses_run.py --tag picked
```

### Метод

Найкраща модель дослідження — одноподієва, на **підібраних ручках**
розділу 2 (`losses_run.py --tag picked`,
`models/transformer_2_3_spread_picked_v1`): вона єдина бере зону і на
навченому, і на невідомому відрізку.

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

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

| Відрізок | Подій | MAE | R² | Пірсон | Знак | У зоні | Сказала | Влучань | З зони | Точність | До межі | До вершини | Подій до півота |
| -------- | ----: | --: | -: | -----: | ---: | -----: | ------: | ------: | -----: | -------: | ------: | ---------: | --------------: |
| train | 23 072 | 0.308 | 0.511 | 0.789 | 80.0 % | 1 606 | 3 119 | **833** | 51.9 % | 26.7 % | 0.108 | 0.224 | 87 |
| val | 4 441 | 0.626 | −0.649 | 0.068 | 48.8 % | 275 | 300 | 49 | 17.8 % | 16.3 % | 0.419 | 0.712 | 157 |
| test | 9 861 | 0.630 | −0.851 | 0.243 | 54.6 % | 653 | 1 758 | 30 | 4.6 % | 1.7 % | 0.432 | 0.861 | 281 |
| увесь датасет | 39 767 | 0.440 | −0.019 | 0.544 | 69.1 % | 2 620 | 5 486 | 927 | 35.4 % | 16.9 % | 0.231 | 0.452 | 165 |
| **невідомий відрізок** | 2 945 | 0.777 | −1.489 | −0.150 | 45.8 % | 184 | 509 | **39** | **21.2 %** | 7.7 % | 0.306 | 0.826 | 269 |

Невідомий відрізок: 2026-08-16 20:00 … 2026-08-27 00:00 UTC, 2 947 подій,
4 півоти, 3 ноги, 29 добових файлів із розігрівом від 2026-07-31. Решта
його метрик: RMSE 0.943, f1 0.324.

### Висновки

1. **Зона дається переважно там, де модель училась**: 833 влучання з
   1 606 на train (51.9 %), 49 з 275 на val, 30 з 653 на test. Але на
   невідомому відрізку — 39 зі 184, тобто 21.2 %: більше, ніж на val і
   test разом. Це не перенесення сигналу, а наслідок ширшого розмаху —
   модель сказала «вершина» 509 разів на 2 945 подій.
2. **Хибних тривог набагато більше за влучання**: точність 1.7 % на test
   і 7.7 % на невідомому відрізку. Медіана відстані від слова «вершина»
   до найближчого півота — 281 подія на test (≈ 16 годин): вершину
   модель бачить посеред ноги.
3. **Напрямок за межею навченого відрізка перевертається**: Пірсон
   −0.150, знак 45.8 %, R² −1.489. Модель, навчена до 2026-08-16, на
   наступних десяти днях гірша за монетку — так само, як `wattn`.
4. **Проти `wattn` трансформер бере вдвічі більше зони на невідомих
   даних** (21.2 % проти 9.5 %) при вдвічі кращій точності (7.7 % проти
   5.3 %), але жодна з двох мереж не тримає напрямок.

## Трансформер проти `wattn`

Однакові дані, ціль, штраф, розбиття і seed — різне тільки зчитування.

| Що | `transformer` | `wattn` (дослідження 16) |
| -- | ------------- | ------------------------ |
| Зчитування | середнє по 13 періодах | навчений запит |
| Розділ 1, test MAE | **0.476** | 0.554 |
| Розділ 1, R² | **−0.046** | −0.462 |
| Розділ 1, σ до цілі | 0.629 | **0.969** |
| Розділ 1, до вершин | 0.843 | **0.821** |
| Вікна: краще влучання (K = 45) | **25.7 %** | 18.8 % |
| Вікна: K = 10 | **19.9 %** | 10.0 % |
| Розділ 4, val (краща модель) | 13.4 % | **18.8 %** |
| Невідомий відрізок, з зони | **21.2 %** | 9.5 % |
| Невідомий відрізок, знак | 45.8 % | 44.9 % |
| Невідомий відрізок, Пірсон | −0.150 | −0.226 |

### Висновок дослідження

**Зчитування вирішує, як мережа помиляється, а не чи вона вгадує.**
Усереднення дає меншу помилку і меншу схильність вигадувати вершини
(розділ 1: MAE 0.476 проти 0.554, точність слова «вершина» 43.8 % проти
3 %), а підібрані ручки і вікна вивертають це в протилежне — ширший
розмах, більше зони, більше хибних тривог. Але жоден варіант не тримає
напрямок за межею навченого відрізка: знак 43–53 %, Пірсон від −0.21 до
+0.03, R² від −0.7 до −1.5.

Обидві мережі впираються в ту саму стелю, що й дослідження 11: звʼязок
фіч класу з ціллю на train — `|r| ≈ 0.29`, і нелінійності всередині
однієї події його не збільшують. Наступний крок — не третя архітектура,
а інші фічі.

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

```bash
# розділ 1 — transformer під штрафом за розмах, типові ручки
python3 research/17-zigzag23-transformer/scripts/losses_run.py

# розділ 2 — ручки мережі: грубий грід, точний грід, тривалість навчання
python3 research/17-zigzag23-transformer/scripts/grid.py --step coarse
python3 research/17-zigzag23-transformer/scripts/grid.py --step fine
python3 research/17-zigzag23-transformer/scripts/grid.py --step epochs

# модель на підібраних ручках — вона й іде у видачу
python3 research/17-zigzag23-transformer/scripts/losses_run.py --tag picked

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

# розділ 4 — моделі на згладженій цілі: грід, фінальна, фічі, невідомий
for K in 10 45; do
  W=$([ $K = 45 ] && echo 64 || echo 64,96,128)
  python3 research/17-zigzag23-transformer/scripts/models_run.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step grid --widths $W
  python3 research/17-zigzag23-transformer/scripts/models_run.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step final
  python3 research/17-zigzag23-transformer/scripts/models_run.py \
      --pair BTC_USDT_fill --way ема20 --k $K --step ablation
  python3 research/17-zigzag23-transformer/scripts/fresh_run.py \
      --run final_fill_ема20_k$K
done

# видача кращої моделі: увесь датасет і відрізок після його кінця
python3 research/17-zigzag23-transformer/scripts/predict_run.py
python3 research/17-zigzag23-transformer/scripts/fresh_run.py

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

Потрібен готовий `data/datasets/BTC_USDT/class210x4bema10x10x130/` з
цілями на ЕМА ціни (`<part>_targets.npz`) і той самий датасет із
заповненими пропусками (`BTC_USDT_fill`) —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х4бема10х10х130).
Згладжений ряд цілі лежить у `results/smooth_fill_2_3.npz` (побудований
`smooth.py` дослідження 16). Нічого зовнішнього. Розділ 1 — близько
хвилини на MPS, кожен грід розділу 2 — 12–20 хв, розділ 3 на заповненому
датасеті — близько 15 хв, розділ 4 — від півгодини (K = 45) до години
(K = 10).

## Файли

| Файл | Що |
| ---- | -- |
| [`scripts/nets.py`](scripts/nets.py) | родина `transformer`: одна подія і вікно з K подій, ширина під вхід |
| [`scripts/base.py`](scripts/base.py) | підміна родини: кроки дослідження 16 працюють над цією мережею |
| [`scripts/losses_run.py`](scripts/losses_run.py) | розділ 1 і модель на підібраних ручках |
| [`scripts/grid.py`](scripts/grid.py) | розділ 2: грубий грід, точний грід, тривалість навчання |
| [`scripts/windows_run.py`](scripts/windows_run.py) | розділ 3: вікна з K подій |
| [`scripts/models_run.py`](scripts/models_run.py) | розділ 4: грід, фінальна модель, викидання фіч |
| [`scripts/predict_run.py`](scripts/predict_run.py) | видача на всьому датасеті |
| [`scripts/fresh_run.py`](scripts/fresh_run.py) | видача на відрізку після кінця датасету |
| [`results/losses_transformer_2_3.json`](results/losses_transformer_2_3.json) | розділ 1: метрики, вершини, розподіл прогнозів |
| [`results/grid_coarse_2_3.json`](results/grid_coarse_2_3.json), [`grid_fine_2_3.json`](results/grid_fine_2_3.json) | розділ 2: гріди |
| [`results/epochs_2_3.json`](results/epochs_2_3.json), [`picked_2_3.json`](results/picked_2_3.json) | розділ 2: тривалість навчання і обрані ручки |
| [`results/picked_model_2_3.json`](results/picked_model_2_3.json) | модель на підібраних ручках — та, що йде у видачу |
| [`results/windows_2_3.json`](results/windows_2_3.json), [`windows_fill_2_3.json`](results/windows_fill_2_3.json) | розділ 3: перебір K на обох датасетах |
| [`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), [`fresh_2_3.json`](results/fresh_2_3.json) | видача кращої моделі |
| [`results/smooth_fill_2_3.npz`](results/smooth_fill_2_3.npz) | згладжена ціль `ема20` (`smooth.py` дослідження 16), поруч її звіт |
| [`models/transformer_2_3_spread_base_v1/`](models/) | ваги розділу 1 і скейлер його train |
| [`models/transformer_2_3_spread_picked_v1/`](models/) | краща модель дослідження |
| [`tests/test_zigzag23tr.py`](../../tests/test_zigzag23tr.py) | тести родини, підміни, гріду і ширини під вхід |
| [`tests/test_dashboard.py`](../../tests/test_dashboard.py) | тести вкладки «Трансформер 2.3» |

## Вкладка

[Трансформер 2.3](http://localhost:8080/transformer23) — розділ 1
«Функція втрат», розділ 2 «Ручки мережі» з трьома кроками підбору,
розділ 3 «Пачка подій» на обох датасетах, розділ 4 «Моделі на згладженій
цілі», далі видача кращої моделі на всьому датасеті й на невідомих даних
— із зонами півотів прямо на графіках. Під кожним блоком графіків стоїть
**легенда з формулами метрик** (MAE, R², Пірсон, знак, σ, відстань до
вершини і до межі зони). Дані вкладки: `/api/transformer23`,
`/api/transformer23/{predict,fresh,windows,models}`
([`dashboard/app.py`](../../dashboard/app.py) → `TRANS_DIR`).
