# Мережа з памʼяттю на розворот

Вкладка: [`/lstm`](http://localhost:8080/lstm)

## Ціль

Дослідження 23 і 24 працювали з **агрегатами**: перш ніж дійти до моделі,
десять подій згорталися в медіани, нахили й позиції піка, а вікна контексту
(200, 400, 800, 1600 подій) задавалися руками.

Мережа з памʼяттю нічого згортати не просить: їй подається **послідовність
подій як є**, і довгий контекст вона має будувати сама. Питання одне:

> **Чи вистачає LSTM самого класу?**

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

## Дані

- Пари: `BTC_USDT`, `ETH_USDT`, `SOL_USDT`, `XRP_USDT`, `BNB_USDT`, `DOGE_USDT`.
- Клас: `клас210х10-1х7д/д`,
  `data/datasets/<PAIR>/class210x10m1x7dd/{train,val}.npz`.
- Спільний збір — [`signal_data.py`](../23-reversal-signal/scripts/signal_data.py)
  дослідження 23.
- Розбиття за датою: train до `2026-08-14`, val — далі. Вікно цілком лежить
  по один бік межі.

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

1. Порізати ряд на вікна подій і поставити мітку драбини.
2. Навчити LSTM на трьох наборах каналів: клас, ціна, обидва.
3. Взяти чесні оцінки train відкладеною монетою, обрати за ними епоху
   й поставити поріг.
4. Порівняти видачу на val з деревами дослідження 24.

## Вкладка `/lstm`

| Блок | Ендпоінт | Скрипт |
| ---- | -------- | ------ |
| Три набори каналів і хід епох | `/api/lstm` | [`scripts/lstm_pivot.py`](scripts/lstm_pivot.py) |
| Плашка монети: сигнал мережі на кожній події, поріг повзунком | `/api/lstm/series/<PAIR>` | там само |

Тести: [`tests/test_lstm.py`](../../tests/test_lstm.py),
[`tests/test_dashboard.py`](../../tests/test_dashboard.py) → `TestLstm`,
`TestLstmMarkup`.

## Етап 1. LSTM на послідовності подій — `підтверджено`, 2026-09-29

### Приклад і мітка

Вікно з `LENGTH` подій підряд, що закінчується на події `e`. Кожна подія —
`9 × 7 = 63` числа класу. Мітка та сама, що в драбині дослідження 23:

    y[e] = 1, якщо півот стоїть на події e − LAG

`LAG = 3` — перший щабель драбини, «півот був десять хвилин тому».

### Довжина вікна

Вікно береться на **кожній** події: умов на розриви й `NaN` немає. Довше
вікно все одно коштує прикладів на початку ряду:

| Монета | Вікон | Розворотів | train | val | край |
| ------ | ----- | ---------- | ----- | --- | ---- |
| `BTC_USDT` | 24 414 | 92 | 16 683 | 7 672 | 59 |
| `ETH_USDT` | 30 641 | 125 | 22 914 | 7 668 | 59 |
| `SOL_USDT` | 32 049 | 136 | 24 321 | 7 669 | 59 |
| `XRP_USDT` | 26 219 | 122 | 18 492 | 7 668 | 59 |
| `BNB_USDT` | 25 215 | 94 | 17 487 | 7 669 | 59 |
| `DOGE_USDT` | 29 033 | 127 | 21 306 | 7 668 | 59 |
| **разом** | **167 571** | **696** | 121 203 | **46 014** | **354** |

Це вже після проріджування негативів (`NEG_RATE` на позитив у train; val
береться цілком). Розворотів 696 — це всі півоти всіх шести монет. Край —
вікна через межу train | val: у числа вони не йдуть, але закривають перші
`LENGTH` подій ряду val сигналом.

Узято `LENGTH = 60` — три з половиною години. Далі розворотів лишається
менше двох сотень на все навчання.

Це саме по собі важливо: у дослідженні 23 контекст працював на вікнах до
**1600 подій**, а мережа бачить 60. Усе, що довше за її вікно, вона з класу
дістати не може.

### Три набори каналів

| Набір | Каналів | Що перевіряє |
| ----- | ------- | ------------ |
| `клас` | 63 | чи дістане мережа все потрібне з самого класу |
| `ціна` | 20 | скільки дає сама лише ціна: пʼять чисел контексту × чотири вікна |
| `клас+ціна` | 83 | чи додає клас щось поверх ціни |

Кожен канал переводиться в **квантиль свого ряду** (`x → 2u − 1`,
[`rules/scripts/scalers.py`](../../rules/scripts/scalers.py)); скейлер
рахується на подіях train **своєї монети**.

### Мережа й навчання

| Ручка | Значення |
| ----- | -------- |
| `LSTM` | `HIDDEN` 96, один шар, `batch_first` |
| голова | `LayerNorm` → `Dropout` 0.1 → лінійний шар в один логіт |
| втрата | `BCEWithLogits`, `pos_weight` 20 |
| пачка | 256, `Adam`, крок `1e-3`, 10 епох |
| негативів | 200 на один позитив, вибір із seed `0` |
| пристрій | `mps` |

Параметрів: 45 601 (`ціна`), 62 113 (`клас`), 69 793 (`клас+ціна`).

### Поріг і вибір епохи

Мережа запамʼятовує навчання: до десятої епохи train `AUC` доходить до
0.99 при val 0.75. Тому **поріг не можна брати з її оцінок на власному
train** — квантиль запамʼятаних позитивів стає вищим за все, що мережа
видасть на val, і сигналів не буде взагалі.

Порядок такий самий, як у дослідженні 24:

1. шість прогонів з **відкладеною монетою** дають чесні оцінки train:
   кожне вікно оцінене мережею, яка його монети не бачила;
2. окремий прогін на всіх монетах дає оцінки val;
3. **епоха обирається за чесним `AP`**, а не за val — інакше val бере
   участь у виборі й перестає бути перевіркою поза часом;
4. поріг стоїть на чесних оцінках **тієї самої** епохи, на заданій
   повноті, і йде на val як є.

Числа таблиць і мітки в плашках монет беруться з однієї епохи — обраної.

Сім прогонів на набір, разом 241 с (`клас`), 218 с (`ціна`), 254 с
(`клас+ціна`) на `mps`.

```bash
python3 research/25-leg-lstm/scripts/lstm_pivot.py
```

Скрипт: [`scripts/lstm_pivot.py`](scripts/lstm_pivot.py)
Вихід: [`results/lstm_pivot.json`](results/lstm_pivot.json) — числа навчання,
`results/lstm_<PAIR>.json` — ряд val однієї монети з мітками

### Результат: класу не вистачає

Val — 46 014 вікон, 93 розвороти, 111.8 монетодіб. `AUC` і `AP` — на
обраній епосі; «чесний» — на відкладеній монеті всередині train.

| Набір | Каналів | Епоха | **чесний AUC** | **val AUC** | **val AP** |
| ----- | ------- | ----- | -------------- | ----------- | ---------- |
| `клас` | 63 | 9 | **0.772** | **0.735** | **0.015** |
| `ціна` | 20 | 8 | **0.965** | **0.950** | **0.070** |
| `клас+ціна` | 83 | 9 | 0.926 | 0.927 | 0.044 |

Розрив між класом і ціною став різким: 0.772 проти 0.965 чесного `AUC`.

Поріг із чесних оцінок train на повноті 30 %:

| Набір | Поріг | **Точність val** | **З допуском ±0.1 %** | Влучив / у рівні / хибних | Повнота val |
| ----- | ----- | ---------------- | --------------------- | ------------------------- | ----------- |
| `клас` | 0.192 | **1.12 %** | 5.09 % | 26 / 92 / 2198 | 28 % |
| `ціна` | 0.824 | **8.49 %** | **27.06 %** | 32 / 70 / 275 | 34 % |
| `клас+ціна` | 0.771 | 6.12 % | 23.02 % | 17 / 47 / 214 | 18 % |

### Результат: дерева на агрегатах кращі

### Результат: дерева на агрегатах кращі

Той самий щабель `LAG = 3`, той самий val, дослідження 24:

| Модель | Вхід | val AUC | val AP | val точність | Влучив / хибних |
| ------ | ---- | ------- | ------ | ------------ | --------------- |
| LSTM | `клас`, 63 канали × 60 подій | 0.735 | 0.015 | 1.1 % | 26 / 2198 |
| LSTM | `ціна`, 20 каналів × 60 подій | 0.950 | 0.070 | 8.5 % | 32 / 275 |
| LSTM | `клас+ціна`, 83 канали × 60 подій | 0.927 | 0.044 | 6.1 % | 17 / 214 |
| дерева | `контекст`, 5 чисел | 0.946 | — | **21.5 %** | 51 / 174 |
| дерева | `вікна`, 20 чисел | **0.987** | — | **21.4 %** | 50 / 175 |

Виміряно на однакових обсягах: 93 розвороти val, близько 112 монетодіб.
Дерева на пʼятьох агрегатах б'ють мережу на 20 каналах послідовності
вдвічі за точністю.

### Результат: плашка кожної монети

Кожна монета віддає свій ряд val — ема ціни, зігзаг 2.3 % поверх, під ним
**сирий сигнал мережі на кожній події ряду**, до порога, як є, і пʼять
смуг того, що з нього вийшло:

| Мітка | Що означає |
| ----- | ---------- |
| **влучив** | сигнал стоїть на півоті |
| **у рівні півота** | сигнал показує на подію в околі ±0.1 % за ціною |
| **хибний сигнал** | сигнал далеко від півота і за подіями, і за ціною |
| **проґавлений півот** | сигналу не було |
| **відповідь ще попереду** | півот у хвості ряду: подія `p + LAG` ще не настала |

Остання смуга показується, лише коли вона не порожня: таких півотів на
всі шість монет **один**. Питання ставиться на кожній події й про кожен
півот — вікно береться підряд без умов на розриви й `NaN`.

Смуги не приходять готовими: файл монети несе **сирий сигнал** і вид
кожної події (`kind`), а мітки рахуються з них **порогом**, який у плашці
рухають повзунком; кнопка поруч повертає поріг із чесних оцінок train.

Перші 59 подій ряду val закриті вікнами **краю** — тими, що заходять за
межу навчання. Сигнал на них є, у числа вони не йдуть (`kind = -1`):
інакше ряд починався б з дірки завдовжки у вікно.

Допуск ±0.1 % — той самий, що в дослідженні 24
([`pivot_gap`](../23-reversal-signal/scripts/signal_data.py)): мережа
відповідає «півот був три події тому» і показує на подію; якщо ціна тієї
події збігається з ціною справжнього півота, промах вийшов у подіях, а не
в рівні входу.

Поріг — з чесних оцінок train на повноті 30 %, окремо для кожного набору
каналів і з обраної для нього епохи.

| Монета | Подій val | Півотів | `клас` | `ціна` | `клас+ціна` |
| ------ | --------- | ------- | ------ | ------ | ----------- |
| `BTC_USDT` | 7 731 | 9 | 0 / 8 / 303 / 9 / 0 | 0 / 7 / 41 / 9 / 0 | 0 / 11 / 35 / 9 / 0 |
| `ETH_USDT` | 7 727 | 11 | 3 / 8 / 342 / 8 / 0 | 3 / 8 / 36 / 8 / 0 | 1 / 4 / 35 / 10 / 0 |
| `SOL_USDT` | 7 728 | 15 | 5 / 19 / 403 / 10 / 0 | 5 / 12 / 59 / 10 / 0 | 4 / 4 / 48 / 11 / 0 |
| `XRP_USDT` | 7 727 | 31 | 10 / 34 / 514 / 20 / 1 | 12 / 19 / 55 / 18 / 1 | 7 / 9 / 23 / 23 / 1 |
| `BNB_USDT` | 7 728 | 7 | 1 / 3 / 282 / 6 / 0 | 3 / 3 / 54 / 4 / 0 | 0 / 4 / 38 / 7 / 0 |
| `DOGE_USDT` | 7 727 | 21 | 7 / 20 / 354 / 14 / 0 | 9 / 21 / 30 / 12 / 0 | 5 / 15 / 35 / 16 / 0 |
| **разом** | | 94 | **26 / 92 / 2198 / 67 / 1** | **32 / 70 / 275 / 61 / 1** | **17 / 47 / 214 / 76 / 1** |

У клітинці: **влучив / у рівні / хибних / проґавив / ще попереду**.
`влучив + проґавив + ще попереду` дорівнює всім 94 півотам val у кожному
наборі.

Порівняння з деревами дослідження 24 на щаблі `N` = 3, набір `вікна`:
**50 / 9 / 175 / 43 / 1**.

Плашка з графіком — на вкладці [`/lstm`](http://localhost:8080/lstm),
перемикачі монети й набору каналів, повзунок порога.

### Що з цього читається

1. **Ні, самого класу не вистачає, і це видно різко.** Чесний `AUC`
   набору `клас` — 0.772 проти 0.965 у `ціна`. Приріст ціни
   `d_price_avg` у класі є, але мережа не відновлює з нього те, що дають
   готові числа положення.
2. **Ціна наодинці сильніша за ціну з класом.** `ціна` — чесний `AUC`
   0.965 і точність val 8.5 %; `клас+ціна` — 0.926 і 6.1 %. Шістдесят три
   канали класу не просто нічого не додають, вони заважають: мережа
   витрачає на них ємність.
3. **Мережа більше не запамʼятовує так грубо.** Чесний `AUC` 0.965 і val
   `AUC` 0.950 у `ціна` майже збігаються — прикладів стало втричі більше
   (696 розворотів проти 223), і розрив train | val закрився.
4. **Вікно закоротке за побудовою.** 60 подій — три з половиною години, а
   масштаб, який вирішує, — 1600 подій (чотири доби). Це видно з
   порівняння: дерева на `вікна` дають `AUC` 0.987 проти 0.950 у мережі
   на тих самих пʼятьох числах, розкладених на чотири вікна.
5. **Дерева на агрегатах виграють** — 21.4 % проти 8.5 % точності на val
   при однаковій вибірці. LSTM тут не «наступний крок після дерев», а
   інша постановка, якій бракує довжини вікна.
6. **Допуск ±0.1 % піднімає мережу втричі** (8.5 % → 27.1 % у `ціна`):
   вона добре показує **рівень** розвороту й погано — його **момент**.
   Це та сама межа, що в дерев на одному вікні.

### Чого замір не показує

- **Ручки не перебирались.** `HIDDEN`, `DROPOUT`, `NEG_RATE`,
  `POS_WEIGHT` узяті одним набором. Грід міг би підняти `клас`, але не на
  ту відстань, що відділяє 0.744 від 0.941 дерев.
- **Прорідженої довгої історії не пробували.** Це прямий наступний крок:
  подавати кожну `k`-ту подію на тисячі подій назад, щоб масштаб ноги
  вліз у вікно тієї самої довжини.

## Як відтворити з нуля

```bash
# 1. датасети всіх шести монет
python3 rules/scripts/build_class210x10x7dc.py

# 2. навчання мережі на трьох наборах каналів
python3 research/25-leg-lstm/scripts/lstm_pivot.py

# один набір і коротко — для перевірки
python3 research/25-leg-lstm/scripts/lstm_pivot.py --sets клас --epochs 2

# 3. тести і дашборд
python3 -m unittest discover -s tests -t tests
python3 dashboard/app.py        # http://localhost:8080/lstm
```

## Повʼязані файли

- [`research/23-reversal-signal/`](../23-reversal-signal/23-reversal-signal.md)
  — звідки взялися канали ціни й драбина
- [`research/24-val-check/`](../24-val-check/24-val-check.md) — дерева на тих
  самих даних, з якими тут порівняння
- [`research/19-leg-end-lstm/`](../19-leg-end-lstm/19-leg-end-lstm.md) —
  мережа з памʼяттю в постановці «вести ногу»
- [`rules/03-models.md`](../../rules/03-models.md) — навчання моделей
