# Перевірка моделей на val

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

## Ціль

Дослідження 23 міряло все на **train** із перевіркою відкладеною монетою:
модель не бачила монету, але бачила **той самий час**. Тут перевірка чесніша —
**відкладений час**:

- навчання на всьому `train` (до `2026-08-14`), усі шість монет;
- видача на `val` (`2026-08-14` … `2026-09-04`), ті самі шість монет.

Питання два:

1. **До якого числа падає точність** поза часом навчання.
2. **На скількох подіях** це виміряно.

## Дані

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

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

1. Взяти драбину дослідження 23 (етап 6) без змін.
2. Навчити кожен набір входів на train заново.
3. Взяти поріг із **чесних** оцінок train і застосувати його до val як є.
4. Виписати, скільки в val пачок і розворотів на кожному щаблі.
5. Розділити сигнали на влучні, «у рівні півота» (окіл ±0.1 % за ціною)
   і хибні, а півоти — на проґавлені й ті, чия відповідь ще попереду.

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

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

Тести: [`tests/test_valcheck.py`](../../tests/test_valcheck.py),
[`tests/test_dashboard.py`](../../tests/test_dashboard.py) → `TestValcheck`,
`TestValcheckMarkup`.

## Етап 1. Видача на val — `підтверджено`, 2026-09-29

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

Та сама драбина: пачка з десяти подій закінчується на події `e`, мітка на
затримці `N`:

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

Приклад іде в train або val **цілком**: і пачка кандидата
(`e − N − 6 … e − N + 3`, `LEAD` подій до кандидата включно і `TAIL`
після), і подія «зараз» (`e`) лежать по один бік межі. Інших умов немає:
питання ставиться на кожній події й про кожен півот, тому розворотів на
кожному щаблі однаково — **607** у train і 88–93 у val.

### Набори входів

| Набір | Входів | Що подається |
| ----- | ------ | ------------ |
| `клас` | 62 | ознаки пачки кандидата |
| `контекст` | 5 | пʼять причинних чисел зараз, вікно 400 подій |
| `вікна` | 20 | ті самі пʼять на чотирьох вікнах: 200, 400, 800, 1600 |
| `вікна+клас` | 82 | чотири вікна разом з ознаками кандидата |

### Як береться поріг

Поріг стоїть на заданій повноті **чесних** оцінок train — тих, що дала
модель, яка цієї монети не бачила. Оцінки самої навченої моделі на train
завищені (вона їх памʼятає), і поріг на них виявився б надто високим.

Видача на val рахується моделлю, навченою на **всьому** train, і поріг
застосовується до неї як є — без жодного підбору за val.

```bash
python3 research/24-val-check/scripts/val_check.py
```

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

### Результат: скільки в val

| `N` | train пачок | train розворотів | **val пачок** | **val розворотів** | монетодіб |
| --- | ----------- | ---------------- | ------------- | ------------------ | --------- |
| 3 | 356 303 | 607 | **46 314** | **93** | 112.6 |
| 23 | 356 237 | 607 | 46 194 | 93 | 112.3 |
| 43 | 356 117 | 607 | 46 074 | 93 | 112.0 |
| 103 | 355 757 | 607 | 45 714 | 88 | 111.1 |

Val — це три тижні на шести монетах: **112 монетодіб і 88–93 розвороти**.
Шість монет × 21 доба = 126 монетодіб, і 112 — це вони мінус хвіст, у
якому відповідь ще попереду. Розворотів менше 93 стає тільки на щаблі
103: там останні 103 події ряду відповіді ще не мають.

Це все одно мало, і числа нижче треба читати з цим на увазі: при 93
розворотах похибка точності — одиниці відсотків.

### Допуск: промах у подіях, але не в рівні

Модель відповідає «півот був `N` подій тому» і тим самим показує на
подію. Якщо ціна тієї події збігається з ціною справжнього півота з
точністю до **±0.1 %**, відповідь промазала в подіях, але не в рівні:
входити довелося б там само. Такий сигнал рахується окремо — не як
влучання і не як помилка.

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

Скрипт: [`signal_data.py`](../23-reversal-signal/scripts/signal_data.py)
→ `pivot_gap`, поріг `NEAR = 0.001`.

### Результат: до якого числа падає точність

Набір `вікна+клас`, поріг на повноті 30 %:

| `N`, подій | Хвилин | train | **val** | **val з допуском** | Спіймано / у рівні / хибних | Повнота val | val AUC |
| ---------- | ------ | ----- | ------- | ------------------ | --------------------------- | ----------- | ------- |
| 3 | 10 | 24.1 % | **0 %** | 0 % | 0 / 0 / 0 | 0 % | 0.988 |
| 23 | 80 | 51.1 % | **0 %** | 0 % | 0 / 0 / 0 | 0 % | 0.979 |
| **43** | **150** | 55.0 % | **51.0 %** | **55.0 %** | 51 / 4 / 45 | 55 % | 0.983 |
| 103 | 360 | 67.9 % | **73.3 %** | 73.3 % | 33 / 0 / 12 | 38 % | 0.982 |

Усі чотири набори на щаблі `N` = 43:

| Набір | Входів | train | val | val з допуском | Спіймано / у рівні / хибних | Повнота val | val AUC |
| ----- | ------ | ----- | --- | -------------- | --------------------------- | ----------- | ------- |
| `контекст` | 5 | 35.0 % | 54.2 % | 54.2 % | 13 / 0 / 11 | 14 % | 0.944 |
| `клас+контекст` | 67 | 30.7 % | 38.6 % | 50.0 % | 17 / 5 / 22 | 18 % | 0.966 |
| `вікна` | 20 | 49.9 % | 49.1 % | 52.6 % | 56 / 4 / 54 | **60 %** | 0.977 |
| `вікна+клас` | 82 | 55.0 % | **51.0 %** | **55.0 %** | 51 / 4 / 45 | 55 % | 0.983 |

І на щаблі `N` = 103, де числа найвищі:

| Набір | train | val | val з допуском | Спіймано / у рівні / хибних | Повнота val |
| ----- | ----- | --- | -------------- | --------------------------- | ----------- |
| `контекст` | 29.5 % | 24.3 % | **56.9 %** | 35 / 47 / 62 | 40 % |
| `клас+контекст` | 28.8 % | 19.2 % | 55.8 % | 30 / 57 / 69 | 34 % |
| `вікна` | 71.5 % | 66.2 % | 70.4 % | 47 / 3 / 21 | **53 %** |
| `вікна+клас` | 67.9 % | **73.3 %** | 73.3 % | 33 / 0 / 12 | 38 % |

На щаблі 23 **жоден** набір, крім `клас+контекст`, не дає на val сигналу,
а на щаблі 3 мовчить `вікна+клас`: поріг із чесних оцінок train
опиняється вище за весь ряд val при `AUC` 0.91–0.99. Модель розрізняє,
поріг не переноситься. Зрив перескакує між наборами й щаблями, тому
плашка монети показує всі чотири набори, а поріг у ній рухають рукою —
видно, чого саме не вистачило.

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

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

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

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

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

| `kind` події | Що це |
| ------------ | ----- |
| `1` | півот стоїть на події `e − N` |
| `2` | півота там немає, але ціна тієї події в його рівні (±0.1 %) |
| `0` | ні те, ні те |
| `-1` | приклад є, у рахунок не йде: пачка лежить через межу навчання |

`-1` закриває перші `N + LEAD − 1` подій ряду val: сигнал на них є, у
числа вони не йдуть. На щаблі `N = 3` останні три події ряду прикладу не
мають зовсім — пачка кандидата виходить за край даних.

Сигнал іде для **всіх чотирьох** наборів, і плашка їх перемикає.
Фіксувати один набір не можна: поріг із train на val зриває то в одного
набору, то в іншого.

`вікна+клас`, повнота 30 %:

| Монета | Подій val | Півотів | `N` = 3 | `N` = 43 | `N` = 103 |
| ------ | --------- | ------- | ------- | -------- | --------- |
| `BTC_USDT` | 7 731 | 9 | 0 / 0 / 0 / 9 / 0 | 8 / 2 / 7 / 1 / 0 | 6 / 0 / 2 / 2 / 1 |
| `ETH_USDT` | 7 727 | 11 | 0 / 0 / 0 / 11 / 0 | 8 / 0 / 6 / 3 / 0 | 8 / 0 / 5 / 2 / 1 |
| `SOL_USDT` | 7 728 | 15 | 0 / 0 / 0 / 15 / 0 | 9 / 0 / 9 / 6 / 0 | 7 / 0 / 0 / 7 / 1 |
| `XRP_USDT` | 7 727 | 31 | 0 / 0 / 0 / 30 / 1 | 11 / 0 / 6 / 19 / 1 | 8 / 0 / 0 / 22 / 1 |
| `BNB_USDT` | 7 728 | 7 | 0 / 0 / 0 / 7 / 0 | 5 / 0 / 10 / 2 / 0 | 2 / 0 / 2 / 4 / 1 |
| `DOGE_USDT` | 7 727 | 21 | 0 / 0 / 0 / 21 / 0 | 10 / 2 / 7 / 11 / 0 | 5 / 0 / 3 / 15 / 1 |
| **разом** | | 94 | 0 / 0 / 0 / 93 / 1 | 51 / 4 / 45 / 42 / 1 | 36 / 0 / 12 / 52 / 6 |

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

Стовпець `N` = 3 порожній не тому, що модель нічого не знайшла, а тому,
що поріг із train не пустив на val жодного сигналу. Сирий сигнал у плашці
показує, наскільки саме не вистачило: пунктирна горизонталь порога йде
вище за весь ряд оцінок, і повзунок дає її опустити. На щаблі 3 решта
трьох наборів при цьому працюють — їх видно перемикачем.

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

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

1. **Точність поза часом більше не падає.** На щаблі 43 `вікна+клас` дає
   55.0 % на train і **51.0 %** на val; `вікна` — 49.9 % і 49.1 %. На
   щаблі 103 val навіть вище за train: **73.3 %** проти 67.9 %.
2. **Робоча точка — щабель 43** (150 хвилин): найвища повнота (55–60 %)
   при точності близько 50 %. Щабель 103 точніший (66–73 %), але ловить
   лише 38–53 % півотів.
3. **Каліброваність порога — головна вціліла проблема.** На щаблі 23
   мовчать три набори з чотирьох, на щаблі 3 — `вікна+клас`, при `AUC`
   0.91–0.99: поріг із чесних оцінок train опиняється вище за весь ряд
   val. Модель розрізняє, поріг не переноситься. Зрив перескакує між
   наборами й щаблями, тому плашка монети показує всі чотири набори й дає
   зрушити поріг рукою.
4. **Допуск ±0.1 % найбільше важить для одного вікна.** На щаблі 103
   `контекст` строго дає 24.3 %, а з допуском **56.9 %**: із 144 його
   сигналів 35 стоять на півоті і ще 47 — на його рівні. Чотирьом вікнам
   допуск майже не потрібен (73.3 % в обох колонках у `вікна+клас`) — вони
   влучають і в рівень, і в момент. Пʼять чисел одного вікна показують
   **де**, чотири вікна ще й **коли**.
5. **Клас додає на межі похибки.** `вікна+клас` проти `вікна` — 51.0 % і
   49.1 % на щаблі 43, 73.3 % і 66.2 % на щаблі 103, але з удвічі меншою
   повнотою. Знак різниці не тримається.
6. **Вибірка все ще мала.** 88–93 розвороти на val — це три тижні. Числа
   узгоджені з train і між щаблями, але для рішень їх треба підтвердити на
   довшому val.
7. **Один-два півоти на монету лишаються без відповіді** на далекому
   щаблі. Це край даних, а не промах: щоб відповісти «півот був 103 події
   тому», треба дочекатись цих 103 подій.

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

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

# 2. видача на val
python3 research/24-val-check/scripts/val_check.py

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

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

- [`research/23-reversal-signal/`](../23-reversal-signal/23-reversal-signal.md)
  — звідки взялися моделі й драбина
- [`research/25-leg-lstm/`](../25-leg-lstm/25-leg-lstm.md) — те саме питання
  мережею з памʼяттю
- [`rules/03-models.md`](../../rules/03-models.md) — навчання моделей
