# 21. Клас210х10х7дц: похідні фіч по блоках з 21 секунди

Вкладка дашборду: [`/x7dc`](http://localhost:8080/x7dc)
Пара: `BTC_USDT` + 5 монет (`ETH_USDT`, `SOL_USDT`, `XRP_USDT`, `BNB_USDT`,
`DOGE_USDT`)

## Ціль

Побудувати нову родину класів події й подивитись на неї картою:

1. Подія — ті самі 210 секунд, що в `клас210х8`, але порізані на **10 блоків
   по 21 секунді**, і разом з вісьмома фічами взята ціна.
2. Рівні стакана поділити на обʼєм **свого ж блоку** — дістати `клас210х10х7`
   в одиницях обʼємів.
3. Замінити фічі на їх **перші похідні по блоках** — `клас210х10х7дц` — і
   окремо лишити тільки чисті похідні — `клас210х10-1х7дц`.
4. Побачити, чи лягають події цих класів у структуру, і чи видно на ній
   **місце події в нозі зігзага 2.3 %**: десятини ніг як когорти на 3д UMAP.

## Дані

| Поле | Значення |
| ---- | -------- |
| Джерело | робоча копія `data/<рік>/<місяць>/<PAIR>/1S_*.csv` |
| Монети | `BTC_USDT`, `ETH_USDT`, `SOL_USDT`, `XRP_USDT`, `BNB_USDT`, `DOGE_USDT` |
| Період | `2026-02-14` … `2026-09-01` (XRP і DOGE — з `2026-03-05`) |
| Подія | 210 секунд, крок 210 (події не перекриваються) |
| Пропуски часу | заповнюються до 60 с (`rules/01` → Пропуски часу) |
| Датасети | `data/datasets/<PAIR>/{class210x10x9,class210x10x7,class210x10x7dc,class210x10m1x7dc,class210x10m1x3p1dc}/` |

### Розбиття за датою

Межа спільна для всіх шести монет:

```
подія скінчилась до 2026-08-14 00:00 UTC          -> train
подія почалась з 2026-08-14 і скінчилась до
2026-09-04 00:00 UTC                              -> val
```

Нога-подія цілком лежить по один бік межі, тому одна подія на монету, що
перетинає межу, не потрапляє нікуди.

| Монета | подій | train | val | розривів | NaN у `клас210х10х7` |
| ------ | ----- | ----- | --- | -------- | -------------------- |
| `BTC_USDT` | 68 967 | 61 235 | 7 731 | 478 | 2 142 |
| `ETH_USDT` | 69 771 | 62 043 | 7 727 | 484 | 5 340 |
| `SOL_USDT` | 69 264 | 61 535 | 7 728 | 490 | 11 392 |
| `XRP_USDT` | 62 581 | 54 853 | 7 727 | 438 | 28 760 |
| `BNB_USDT` | 69 568 | 61 839 | 7 728 | 482 | 11 988 |
| `DOGE_USDT` | 62 580 | 54 852 | 7 727 | 436 | 108 706 |
| **разом** | **402 731** | **356 357** | **46 368** | | |

`розривів` — подій, у яких немає попередньої впритул: початок ряду, межа доби
і пропуск, довший за 60 секунд. `NaN` беруться з блоків, де середній обʼєм за
21 секунду дорівнює нулю: у `DOGE_USDT` таких блоків найбільше.

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

1. Чотири класи й скрипти до них — етап 1.
2. Побудова датасетів усіх шести монет з розбиттям за датою — етап 2.
3. Приклади класів з датасету — етап 3.
4. Карти подій: 3д і 2д UMAP × три метрики на кожен клас — етап 4.
5. Ноги зігзага 2.3 % на ема10 і десятини ніг як когорти — етап 5.
6. Вкладка дашборду — етап 6.
7. Порівняння подій біля півотів з рештою подій ніг — етап 7.
8. Профіль фіч уздовж ноги, десятина за десятиною — етап 8.
9. Подія «розворот був»: датасет у пачках і карта пачок — етап 9.
10. Ціна із зігзагом і кожна фіча під нею — етап 10.
11. Карта будь-якого набору фіч — етап 11.
12. Клас210х10-1х3+1дц і карта його наборів — етап 12.
13. Теплові карти фіч — етап 13.
14. Хід фіч від півота до півота — етап 14.

## Етап 1. Чотири класи — `підтверджено`

Класи описані в правилах, повний опис і формули —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х10х9).
Коротко:

### `клас210х10х9` — 10 × 9

Секунди події зводяться в 10 блоків по 21 секунді, значення блоку — середнє:

```
x[j, f] = Σ21(f) / 21,   j = 1 … 10
```

Фіч девʼять: вісім із `клас210х8` і `price_avg`. Разом з подією зберігаються
**час початку** і **час кінця** — у навчанні вони участі не беруть.

Скрипт: [`rules/scripts/class210x10x9.py`](../../rules/scripts/class210x10x9.py)

### `клас210х10х7` — 10 × 7

У кожному блоці рівні стакана діляться на обʼєм **свого ж блоку**:

```
resist       = const_resist / vol_buy
bal_resist   = (resist_plus − resist_minus) / vol_buy
support      = const_support / vol_sell
bal_support  = (support_plus − support_minus) / vol_sell
vol_buy, vol_sell, price_avg — середні блоку як є
```

Девʼять фіч стають сімома. Блок з нульовим обʼємом дає `NaN`, подія лишається.

Скрипт: [`rules/scripts/class210x10x7.py`](../../rules/scripts/class210x10x7.py)

### `клас210х10х7дц` — 10 × 7

Перші похідні фіч по блоках. Похідна — різниця між наступним і попереднім
значенням, індекс у неї **наступного**:

```
d[j] = x[j] − x[j−1],            j = 2 … 10
d[1] = x[1] − x_попередньої[10]  подія стоїть впритул до попередньої
d[1] = (d[2] + d[3] + d[4]) / 3  попередньої події немає
```

«Впритул» — це `t_start` подій, що різняться рівно на 210 секунд. Перша подія
датасету і перша подія після розриву даних попередньої не мають; вони
позначені прапорцем `seam`.

Похідні рахуються **на суцільному ряду монети, до розбиття на вибірки**:
інакше перша похідна першої події `val` взялась би нізвідки.

Імена фіч — імена батька з префіксом `d_`: `d_resist`, `d_bal_resist`,
`d_support`, `d_bal_support`, `d_vol_buy`, `d_vol_sell`, `d_price_avg`.

Скрипт: [`rules/scripts/class210x10x7dc.py`](../../rules/scripts/class210x10x7dc.py)

### `клас210х10-1х7дц` — 9 × 7

`клас210х10х7дц` без першої плашки: лишаються тільки девʼять похідних,
порахованих цілком усередині події. Клас не залежить ні від попередньої
події, ні від правила розриву.

Скрипт: [`rules/scripts/class210x10m1x7dc.py`](../../rules/scripts/class210x10m1x7dc.py)

Тести: [`tests/test_class210x10x7dc.py`](../../tests/test_class210x10x7dc.py) —
формули блоків і похідних, нульові обʼєми, пропуски секунд, розрив між
подіями, розбиття за датою, повний пайплайн і CLI.

## Етап 2. Побудова датасетів — `підтверджено`

Один скрипт робить увесь шлях для кожної монети:

```
data/<рік>/<місяць>/<PAIR>/*.csv
    -> клас210х10х9  ->  клас210х10х7  ->  клас210х10х7дц  ->  клас210х10-1х7дц
```

```bash
python3 rules/scripts/build_class210x10x7dc.py
python3 rules/scripts/build_class210x10x7dc.py --pair BTC_USDT
```

Скрипт: [`rules/scripts/build_class210x10x7dc.py`](../../rules/scripts/build_class210x10x7dc.py)
Вихід: `data/datasets/<PAIR>/<клас>/{train,val}.npz` + `report.json`,
зведення — `data/datasets/class210x10x7dc.json`.
Числа — у таблиці розділу [Дані](#розбиття-за-датою).

## Етап 3. Приклади класів з датасету — `підтверджено`

Одна подія `BTC_USDT` наскрізь по всіх чотирьох класах плюс подія на розриві,
де перша похідна береться середнім трьох наступних. Числа — з готових
датасетів, нічого не перераховується.

```bash
python3 research/21-class210x10x7dc/scripts/class_examples.py
```

Скрипт: [`scripts/class_examples.py`](scripts/class_examples.py)
Вихід: [`results/examples.json`](results/examples.json) — звичайна подія
`2026-02-14 17:57:46 … 18:01:15 UTC` і подія на розриві
`2026-02-14 22:00:00 UTC`.

## Етап 4. Карти подій: UMAP — `підтверджено`

Карта будується на **вибірці train усіх шести монет** — тій самій для всіх
чотирьох класів і всіх метрик, інакше карти не порівнюються між собою:

```
усі події train (356 357) -> випадкова вибірка 30 000, seed 0
-> вектор події (блоки × фічі, фіча за фічею) -> z-score на вибірці -> UMAP
```

| Параметр | Значення |
| -------- | -------- |
| Вибірка | 30 000 подій train, `seed 0`, одна на всі класи |
| Метрики | `cosine`, `euclidean`, `manhattan` (rules/05 → UMAP) |
| Вимірності | 2 і 3 |
| `n_neighbors` | 30 |
| `min_dist` | 0.1 |
| `random_state` | 0 |

Подія з `NaN` хоч в одному значенні не проєктується (rules/05); скільки таких
— у `meta.dropped_nan`.

| Клас | вектор події | спроєктовано | відкинуто з `NaN` |
| ---- | ------------ | ------------ | ----------------- |
| `клас210х10х9` | 90 | 30 000 | 0 |
| `клас210х10х7` | 70 | 26 821 | 3 179 |
| `клас210х10х7дц` | 70 | 26 617 | 3 383 |
| `клас210х10-1х7дц` | 63 | 26 821 | 3 179 |

Тим самим скриптом і на тій самій вибірці побудовані карти трьох класів,
які зʼявились у дослідженні пізніше (розділи 12, 15 і 17). На цій вкладці
їх немає — вони видно на вкладці [`/umap`](http://localhost:8080/umap),
де за правилом зібрані **всі** збережені класи датасету:

| Клас | вектор події | спроєктовано | відкинуто з `NaN` |
| ---- | ------------ | ------------ | ----------------- |
| `клас210х10-1х3+1дц` | 36 | 26 821 | 3 179 |
| `клас210х10-1х2д` | 18 | 26 821 | 3 179 |
| `клас210х10-1х7д/д` | 63 | 26 780 | 3 220 |

```bash
python3 research/21-class210x10x7dc/scripts/run_umap7dc.py
python3 research/21-class210x10x7dc/scripts/run_umap7dc.py \
    --class class210x10x7dc --metric cosine --dims 3
```

Скрипт: [`scripts/run_umap7dc.py`](scripts/run_umap7dc.py)
Вихід: `results/umap_train_<metric>_<клас>.{json,meta.json,npz}` — 2д,
`results/umap3d_train_<metric>_<клас>.*` — 3д, зведення —
[`results/umap_maps.json`](results/umap_maps.json). Усього 7 × 3 × 2 = 42 карти:
4 класи цієї вкладки і 3 класи вкладки `/umap`. Перелік `umap_maps.json`
збирається з `meta.json` усіх готових карт теки, тому прогін одного класу
не стирає з нього решту.

Кожна точка несе `d` — зміну ціни за подію (розфарбування розділу 1), `g` —
когорту десятини ноги (розділ 2) і `c` — індекс монети.

## Етап 5. Десятини ніг зігзага 2.3 % — `підтверджено`

Зігзаг стоїть на **ема10** ціни події, поріг **2.3 %** — той самий зігзаг, що
у дослідженні 19 (вкладка [`/leg23`](http://localhost:8080/leg23)).

```
ема10(price_open) -> півоти зігзага 2.3 % -> ноги -> десятини
```

Нога — відрізок від події після попереднього півота до самого півота включно.
Вона ділиться на **десять рівних частин за кількістю подій**: `1` — перші 10 %
подій ноги, `10` — останні 10 %. Місце міряється часом, не ціною. Напрямок
ноги — за півотом, яким вона закінчується: півот зверху (`high`) закриває ногу
**вгору**, знизу (`low`) — ногу **вниз**.

Когорта події кодується одним числом:

```
0            подія поза ногами (до першого півота і після останнього)
1 … 10       десятина ноги вгору
11 … 20      десятина ноги вниз
```

| Монета | подій | півотів | ніг | вгору | вниз | найкоротша | медіана | найдовша | поза ногами |
| ------ | ----- | ------- | --- | ----- | ---- | ---------- | ------- | -------- | ----------- |
| `BTC_USDT` | 68 966 | 92 | 91 | 45 | 46 | 40 | 566 | 3 099 | 255 |
| `ETH_USDT` | 69 770 | 126 | 125 | 62 | 63 | 36 | 439 | 3 025 | 70 |
| `SOL_USDT` | 69 263 | 137 | 136 | 68 | 68 | 36 | 351 | 3 074 | 77 |
| `XRP_USDT` | 62 580 | 124 | 123 | 61 | 62 | 11 | 363 | 2 721 | 43 |
| `BNB_USDT` | 69 567 | 94 | 93 | 46 | 47 | 11 | 466 | 2 952 | 254 |
| `DOGE_USDT` | 62 579 | 128 | 127 | 63 | 64 | 24 | 358 | 2 666 | 85 |

Довжина ноги — в подіях по 210 секунд: медіана `BTC_USDT` у 566 подій — це
близько 33 годин.

Десятина — рівна частка ноги за кількістю подій, а не остання подія: нога,
коротша за десять подій, лишає частину десятин порожніми (нога з чотирьох
подій дає `1, 3, 6, 8`). Це те саме правило, що в дослідженні 19.

```bash
python3 research/21-class210x10x7dc/scripts/leg_deciles.py
```

Скрипт: [`scripts/leg_deciles.py`](scripts/leg_deciles.py)
Вихід: [`results/legs_2_3.json`](results/legs_2_3.json) — ноги й когорти по
монетах, `results/legs_<PAIR>.json` — ряд для графіка, `results/deciles.npz` —
когорта кожної події кожної монети.

### Кольори когорт

rules/05 → Кольори: колір несе напрямок, насиченість — величину. Тут усередині
напрямку йде **категорія** — номер десятини, тому вона передається світлістю:

| Напрямок | Палітра | Десятина |
| -------- | ------- | -------- |
| нога вгору | зелений → жовтий | 1 — зелений, 10 — жовтий |
| нога вниз | червоний → фіолетовий | 1 — червоний, 10 — фіолетовий |
| поза ногами | сірий | — |

Десятину несе **відтінок** — він однаковий на обох темах. Світлість додає
другу підказку, і її напрямок обирає тема: на чорній кінець ноги світліший
(`hsl(h, 84%, 60 … 71%)`), на білій — темніший (`hsl(h, 76%, 32 … 21%)`).
Без цього жовтий кінець ноги на білому тлі просто зникає: контраст плашки
до свого тла лишається не нижчим за 3.1.

На графіку ряду нога вгору — зелена, нога вниз — синя; десятини там не
розфарбовуються, вони показані на карті подій. На білій темі беруться
темніші тони тих самих кольорів (`#1d7a4a` і `#1f5fd0` замість `#46c882`
і `#4c8dff`) — лінія в 1.6 пікселя на білому інакше губиться.

## Етап 6. Вкладка дашборду — `підтверджено`

Вкладка [`/x7dc`](http://localhost:8080/x7dc), дванадцять розділів. **Усі
плашки за замовчуванням згорнуті** — місце лишається під те, що читають;
розгорнута плашка лишається відкритою тільки в межах одного
перемальовування (зміна теми чи розміру вікна). Полотно, намальоване
в закритій плашці, знає лише запасну ширину, тому на розгортанні воно
малюється заново; важкі дані (карти UMAP, ряд монети) вантажаться теж
на розгортанні, а не на відкритті вкладки.

Натиснута кнопка **лишається на місці**: перемальовування замінює всю
розмітку блоку, він на мить порожніє, сторінка коротшає — і без якоря
екран стрибав би вгору. Перед перемальовуванням запамʼятовується
положення самої кнопки, після — сторінка зсувається на різницю
(`redrawInPlace` у `dashboard/static/app.js`). Те саме працює для
перемальовування від зміни теми чи розміру вікна, тільки якорем там
служить верх блоку.

**Розділ 1 — класи, формули, приклади і карти подій.** Порядок побудови,
таблиця розбиття за датою, картка на кожен клас з формулами, одиницями й
виміряними на train межами, приклади подій з датасету і 3д UMAP з
перемикачами класу й метрики. Розфарбування карти — зміна ціни за подію
(rules/05).

**Розділ 3 — події біля півотів проти решти подій ніг.** Три перемикачі:
довжина пачки (1 … 20 подій), окіл півота (±5 … ±80 подій) і згортка
(вісім, згруповані в три сімʼї — «де фіча стоїть», «чи більші коливання»,
«чи плавніше»). Таблиця різниці по фічах з обома AUC — пулом і всередині
ноги, кореляції пар фіч усередині пачки й матриці Спірмена між подіями
обох когорт, ті самі числа на кожній монеті окремо, AUC по кожному блоку
події і накладені розподіли когорт.

**Розділ 2 — десятини ніг зігзага 2.3 % на карті подій.** Графік ряду з
ема10 і півотами по кожній монеті, таблиці ніг і когорт, і та сама 3д карта,
розфарбована когортами десятин. На кожну десятину — своя кнопка; вимкнена
когорта не малюється.

**Розділ 4 — фічі вздовж ноги.** Ті самі перемикачі пачки й згортки плюс
вибір `m` (скільки подій береться з початку кожної десятини) і напрямку
ноги (усі / вгору / вниз). Таблиця середніх рангів по десятинах з нахилом
ρ, той самий профіль графіком, окрема плашка «ноги вгору проти ніг вниз»
з обома профілями й `AUC` між ними, матриця «десятина проти десятини» на
обрану фічу і кількість ніг у замірі.

**Розділ 5 — подія «розворот був».** Таблиця вибору довжини пачки з
силою й часткою живих, розмічений датасет по монетах, графік ряду кожної
монети (ема10 сірим, пачки з розворотом кольором, зігзаг поверх) і 3д
карта пачок, розфарбована міткою.

**Розділ 6 — ціна із зігзагом і кожна фіча під нею.** Кнопки монети,
фічі й згортки; два полотна на спільній осі X зі своєю шкалою Y на
кожному.

**Розділ 7 — карта будь-якого набору фіч.** Кнопки складають набір
(одна фіча … усі сім), окремо обирається довжина пачки (5 або 20) і
які мітки показувати.

**Розділ 8 — те саме для `клас210х10-1х3+1дц`.** Той самий код і ті самі
кнопки, інший клас події: чотири фічі, 15 наборів.

**Розділ 9 — теплові карти фіч.** Кнопки класу, довжини пачки й питання;
дві карти — фіча × згортка (`AUC`) і фіча × фіча (кореляція Спірмена).

**Розділ 10 — хід фіч від півота до півота.** Кнопки класу, довжини
пачки, напрямку ноги й згортки; графік ходу лінією на фічу, ті самі
числа таблицею і порівняння ніг росту з ногами падіння по клітинках.

**Розділ 11 — карта наборів `клас210х10-1х2д` і зона розвороту в нозі.**
Той самий код карти на класі з двох фіч, під ним — замір зони: кнопки
класу (`клас210х10-1х2д` або `клас210х10-1х7д/д`), довжини пачки й зони,
таблиця `AUC` усередині ноги, та сама по монетах і ваги моделі з її
числом на ногах val.

**Розділ 12 — карта наборів `клас210х10-1х7д/д`.** Той самий код і ті
самі кнопки: сім фіч, 127 наборів.

Ендпоінти: `/api/x7dc`, `/api/x7dc/umap`, `/api/x7dc/legs`,
`/api/x7dc/subsets`, `/api/x7dc/subset/<W>/<набір>`,
`/api/x7dc/subsets31`, `/api/x7dc/subset31/<W>/<набір>`,
`/api/x7dc/subsets2d`, `/api/x7dc/subset2d/<W>/<набір>`,
`/api/x7dc/subsets7dd`, `/api/x7dc/subset7dd/<W>/<набір>`,
`/api/x7dc/zone`, `/api/x7dc/heatmap`, `/api/x7dc/track`,
`/api/x7dc/series/<PAIR>`, `/api/x7dc/pivots`, `/api/x7dc/profile`,
`/api/x7dc/reversal`, `/api/x7dc/reversal/series/<PAIR>`,
`/api/x7dc/reversal/umap[/<карта>]`, `/api/x7dc/charts`,
`/api/x7dc/chart/<PAIR>/<згортка>`. Карти класів є і на вкладці
[`/umap`](http://localhost:8080/umap), описи класів — на вкладці
[`/dataset`](http://localhost:8080/dataset).

Тести: [`tests/test_dashboard.py`](../../tests/test_dashboard.py) →
`TestX7dcTab` (ендпоінти й дані), `TestX7dcPivots` (розділ 3),
`TestX7dcProfile` (розділ 4), `TestX7dcReversal` (розділ 5),
`TestX7dcChart` (розділ 6), `TestX7dcSubsets` (розділ 7),
`TestX7dcSubsets31` (розділ 8), `TestX7dcHeatmap` (розділ 9),
`TestX7dcTrack` (розділ 10), `TestX7dcSubsets2d` і `TestX7dcZone`
(розділ 11), `TestX7dcSubsets7dd` (розділ 12), `TestX7dcSubsetMarkup` і
`TestX7dcZoneMarkup` (розмітка плашок і перемикача класу) та
`TestX7dcDrawing` (що лягло на полотно:
кольори ніг на ряду, двадцять кольорів десятин, вимкнена когорта не
малюється), [`tests/test_x7dc.py`](../../tests/test_x7dc.py) (десятини,
вибірка карт, приклади).

## Етап 7. Події біля півотів проти решти подій ніг — `підтверджено`

Питання: **чим події `клас210х10-1х7дц` біля розвороту відрізняються від
решти подій ноги** — де фічі стоять, як вони коливаються всередині події
й як співвідносяться одна з одною. Півоти ті самі, що в етапі 5 — зігзаг
2.3 % на `ема10`.

### Дві когорти

```
біля півота   подія не далі за K подій від найближчого півота
решта ноги    решта подій, що лежать усередині ніг
```

- Відстань міряється **номером події в ряду своєї монети**, як горизонти
  в дослідженнях 19 і 20: подія — 210 секунд, але пропуски часу роблять
  крок нерівним.
- Окіл симетричний і перетинає півот — бере і кінець ноги, і початок
  наступної: розворот лежить між ними.
- Події поза ногами не беруть участі в жодній когорті.
- Вікна `K`: 5, 10, 20, 40, 80 подій. За замовчуванням 10 — це ±35 хвилин.

### Пачки подій

Подія коротка — 210 секунд. Тому все міряється не тільки на одній події,
а й на **пачках із W подій підряд**:

```
W = 1, 2, 3, 5, 10, 20        пачка = 3.5 хв … 70 хв
```

Пачка **закінчується** своєю подією і бере `W − 1` попередніх, тобто
дивиться тільки назад. Ряд блоків пачки — `9 · W` значень підряд, зшитих
через межі подій. Пачка існує, тільки якщо всі `W` подій ідуть впритул
(крок 210 секунд); на початку ряду й після розриву даних пачки немає, і
подія в замір не входить. Когорту задає **остання подія пачки**.

| Пачка | час | подій у ногах train | біля півота (±10) |
| ----- | --- | ------------------- | ----------------- |
| 1 | 3.5 хв | 355 793 | 12 671 |
| 2 | 7 хв | 354 141 | 12 606 |
| 3 | 10.5 хв | 350 887 | 12 357 |
| 5 | 17.5 хв | 346 460 | 12 083 |
| 10 | 35 хв | 336 291 | 11 526 |
| 20 | 70 хв | 317 880 | 10 663 |

### Вісім згорток

На кожну фічу пачка дає ряд `v[1] … v[9W]`. Щоб фіча дала одне число,
ряд згортається вісьмома способами — дві кажуть, **де фіча стоїть**,
решта шість — **як вона поводиться всередині пачки**:

| Згортка | Формула | Питання |
| ------- | ------- | ------- |
| `mean` | `mean(v)` | куди тягне, зі знаком |
| `abs` | `mean(|v|)` | наскільки взагалі рухалась |
| `range` | `max(v) − min(v)` | чи більші коливання |
| `std` | `σ(v)` | чи більші коливання |
| `step` | `mean(|v[j+1] − v[j]|)` | чи різкіші переходи |
| `straight` | `|v[кінець] − v[початок]| / Σ|v[j+1] − v[j]|` | чи плавніше |
| `acf1` | `corr(v[1…n−1], v[2…n])` | чи плавніше |
| `signs` | скільки разів `v` міняє знак | чи плавніше |

`straight` = 1 — ряд ішов в один бік без відкатів, 0 — вернувся туди,
звідки вийшов; це те саме правило, що в ціновій фічі `клас210х14х4ц`.
Ряд, що не рухався жодного разу, дає `NaN` у `straight` і `acf1`.

### Як фічі співвідносяться між собою

**Усередині пачки** — на кожну з 21 пари фіч кореляція Пірсона по блоках
цієї ж пачки, одне число на пару на подію; далі когорти порівнюються тим
самим `AUC`.

**Між подіями** — у кожній когорті окремо матриця кореляцій Спірмена 7 × 7
між згортками фіч по всіх подіях когорти. Різниця двох матриць показує,
які пари міняють звʼязок біля розвороту.

### Чим міряється різниця

| Міра | Що каже |
| ---- | ------- |
| `AUC` | ймовірність, що випадкова подія біля півота має **більше** значення, ніж випадкова подія решти ноги. `0.5` — різниці немає |
| `d` Коена | різниця середніх у спільних сигмах |
| медіани | самі числа обох когорт |

`AUC` рахується рангами (`scipy.stats.rankdata`), тому важкі хвости
похідних його не зсувають. Вибірки тут у сотні тисяч подій, тому будь-яка
різниця «статистично значуща»: читається **величина** ефекту.

Числа в таблицях вкладки розфарбовані знаком відхилення від 0.5: вище —
зелене, нижче — червоне, величину несуть насиченість і світлість
(`valueColor` у `dashboard/static/app.js`). Прозорість тут не годиться —
бліда зелень на білому тлі не читається, тому колір суцільний, а світлість
береться від теми: контраст числа до тла не нижчий за 4.0 на обох темах.

### Пулом міряти замало

Пулове порівняння має вбудований перекіс: **біля кожного півота стоїть
рівно `2K+1` подія**, скільки б не тривала нога. Тому когорта «біля
півота» набирає події з періодів, де розвороти часті, а «решта ноги» —
переважно з довгих спокійних ніг. Різниця між когортами може виявитись
різницею **режимів**, а не місця події в нозі.

Тому той самий замір робиться вдруге — **усередині кожної ноги окремо**:
`AUC` рахується тільки серед подій однієї ноги, і по всіх ногах беруться
медіана й квартилі. Нога бере участь, якщо в ній є хоча б 5 подій у кожній
когорті: при `W = 1, K = 10` таких ніг 600 з 695, при `W = 20` — 516.

```bash
python3 research/21-class210x10x7dc/scripts/pivot_compare.py
```

Скрипт: [`scripts/pivot_compare.py`](scripts/pivot_compare.py)
Вихід: [`results/pivot_compare.json`](results/pivot_compare.json)

### Результат: величина фіч

`AUC` **усередині ноги**, вікно ±10, згортка `mean(|v|)`, по довжині пачки:

| Фіча | W=1 | W=2 | W=3 | W=5 | W=10 | W=20 |
| ---- | --- | --- | --- | --- | ---- | ---- |
| `d_vol_buy` | 0.619 | 0.640 | 0.657 | 0.685 | 0.710 | **0.756** |
| `d_price_avg` | 0.633 | 0.659 | 0.680 | 0.703 | 0.733 | **0.756** |
| `d_vol_sell` | 0.614 | 0.632 | 0.643 | 0.665 | 0.703 | **0.750** |
| `d_bal_support` | 0.399 | 0.384 | 0.372 | 0.353 | 0.333 | **0.304** |
| `d_bal_resist` | 0.395 | 0.366 | 0.353 | 0.333 | 0.310 | **0.281** |
| `d_support` | 0.351 | 0.340 | 0.327 | 0.311 | 0.286 | **0.273** |
| `d_resist` | 0.344 | 0.326 | 0.314 | 0.291 | 0.257 | **0.233** |

Згортка `mean(v)` — зі знаком — не розрізняє когорти на жодній пачці:
0.475 … 0.514.

### Результат: коливання і плавність

`AUC` усередині ноги, вікно ±10, усі вісім згорток.

**Пачка 1:**

| Фіча | mean | abs | range | std | step | straight | acf1 | signs |
| ---- | ---- | --- | ----- | --- | ---- | -------- | ---- | ----- |
| `d_resist` | 0.501 | 0.344 | 0.347 | 0.345 | 0.344 | 0.501 | 0.497 | 0.499 |
| `d_support` | 0.496 | 0.351 | 0.359 | 0.358 | 0.356 | 0.504 | 0.503 | 0.492 |
| `d_vol_buy` | 0.491 | 0.619 | 0.604 | 0.610 | 0.616 | 0.502 | 0.508 | 0.496 |
| `d_price_avg` | 0.498 | 0.633 | 0.621 | 0.627 | 0.627 | 0.499 | 0.493 | 0.506 |

**Пачка 20:**

| Фіча | mean | abs | range | std | step | straight | acf1 | signs |
| ---- | ---- | --- | ----- | --- | ---- | -------- | ---- | ----- |
| `d_resist` | 0.475 | 0.233 | 0.296 | 0.265 | 0.235 | 0.535 | 0.501 | 0.470 |
| `d_support` | 0.499 | 0.273 | 0.323 | 0.302 | 0.273 | 0.529 | 0.508 | 0.446 |
| `d_vol_buy` | 0.484 | 0.756 | 0.677 | 0.717 | 0.757 | 0.498 | 0.549 | 0.454 |
| `d_price_avg` | 0.484 | 0.756 | 0.726 | 0.760 | 0.758 | 0.506 | 0.507 | 0.503 |

### Результат: співвідношення фіч

Кореляція пар **усередині пачки**, `AUC` у нозі, вікно ±10:

| Пачка | найсильніші пари |
| ----- | ---------------- |
| 1 | `bal_support ↔ vol_sell` 0.451, `bal_resist ↔ vol_buy` 0.452, `support ↔ vol_sell` 0.457 |
| 20 | `bal_resist ↔ bal_support` 0.388, `vol_buy ↔ vol_sell` 0.586, `bal_support ↔ vol_buy` 0.423 |

Різниця матриць Спірмена **між подіями** (`біля − решта`, згортка `abs`,
вікно ±10):

| Пара | пачка 1 | пачка 20 |
| ---- | ------- | -------- |
| `resist ↔ support` | +0.121 | +0.080 |
| `resist ↔ vol_buy` | +0.049 | **+0.172** |
| `support ↔ vol_sell` | +0.040 | **+0.157** |
| `bal_resist ↔ bal_support` | +0.100 | +0.069 |
| `resist ↔ price_avg` | −0.062 | **−0.149** |
| `support ↔ price_avg` | −0.061 | **−0.151** |

### Результат: ноги вгору проти ніг вниз

Перша десятина, згортка `mean(|v|)`, пачка 1, `m = 1`:

| Фіча | вгору | вниз | AUC вгору/вниз |
| ---- | ----- | ---- | -------------- |
| `d_resist` | **3.49** | 4.14 | 0.427 |
| `d_bal_resist` | **3.63** | 4.91 | 0.362 |
| `d_support` | 4.75 | **3.51** | 0.614 |
| `d_bal_support` | 5.46 | **3.70** | 0.673 |
| `d_vol_buy` | 6.73 | 6.58 | 0.514 |
| `d_vol_sell` | 6.38 | 7.04 | 0.426 |
| `d_price_avg` | 6.73 | 7.02 | 0.474 |

Те саме на пачці 20, `m = 10` — сильніше й з тим самим знаком:

| Фіча | вгору | вниз | AUC вгору/вниз | ніг вгору / вниз |
| ---- | ----- | ---- | -------------- | ---------------- |
| `d_support` | 5.15 | **2.74** | **0.753** | 41 / 42 |
| `d_bal_support` | 5.66 | **2.74** | **0.775** | 41 / 42 |
| `d_resist` | 3.75 | 3.42 | 0.541 | 71 / 71 |
| `d_bal_resist` | 4.15 | 3.79 | 0.543 | 71 / 71 |
| `d_vol_buy` | 7.15 | **8.28** | 0.351 | 213 / 224 |
| `d_vol_sell` | 7.55 | 7.55 | 0.509 | 213 / 224 |
| `d_price_avg` | 7.43 | **8.11** | 0.441 | 213 / 224 |

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

1. **Знаку немає, величина є.** Похідні біля розвороту не тягнуть кудись —
   вони інші за розміром. На згортці зі знаком когорти не розрізняються
   на жодній довжині пачки (0.475 … 0.514).
2. **Довша пачка бачить більше.** Це головне, що дала пачка: на одній
   події `|d_resist|` дає 0.344, на пачці з 20 подій — **0.233**;
   `|d_vol_buy|` росте з 0.619 до **0.756**. Різниця не в одній події, а
   в тому, як ринок поводиться протягом години навколо розвороту, і на
   пачці її видно втричі краще. Ефект росте монотонно по всіх шести
   довжинах.
3. **Біля розвороту більше обʼєму і сильніший рух ціни.** `|d_vol_buy|`
   медіана 13.6 проти 3.1 на пачці 20; `|d_price_avg|` — 0.756 у нозі при
   85 % ніг.
4. **Стакан на одиницю потоку тоншає.** `|d_resist|` і `|d_support|`
   різко менші (медіана 149 проти 479 на пачці 20). Це **окреме
   спостереження**, а не переказ пункту 3: `resist = const_resist /
   vol_buy` — це скільки заявок стоїть на кожен обʼєм угод. Спільний
   знаменник робить дві фічі повʼязаними, але не однаковими: падіння
   `resist` може йти і від того, що тануть самі рівні стакана. Скільки
   тут від чого — цим класом не видно: сирі `const_resist` і
   `const_support` лишились у `клас210х10х9` і окремо не мірялись.
5. **Коливання — це та сама величина, іншого не додають.** `range`, `std`
   і `step` на пачці 1 повторюють `abs` до третього знака (0.344 … 0.347
   у `d_resist`). На пачці 20 вони розходяться, і сильнішими лишаються
   `abs` і `step` (0.233 і 0.235 проти 0.296 у `range`): біля розвороту
   змінюється **типовий крок** ряду, а не окремий викид у ньому.
6. **Плавність майже не відрізняється.** `straight`, `acf1` і `signs` на
   одній події дають 0.49 … 0.51 — рівно нічого. На пачці 20 зʼявляються
   слабкі, але систематичні зсуви: рівні стакана йдуть рівніше
   (`straight` 0.529 … 0.535), обʼєми звʼязніші між сусідніми блоками
   (`acf1` 0.549), а `d_support` рідше міняє знак (`signs` 0.446). Це
   помітно, але вдесятеро слабше за різницю у величині.
7. **Фічі й співвідносяться інакше.** На пачці 20 біля розвороту рівні
   стакана й обʼєми звʼязані значно тісніше (`resist ↔ vol_buy` +0.172 до
   кореляції), а звʼязок рівнів з ціною слабшає (−0.15). Усередині пачки
   два баланси розходяться (`bal_resist ↔ bal_support` AUC 0.388), а
   обʼєми, навпаки, ходять разом тісніше (0.586). Тобто біля розвороту
   міняється не тільки де фічі стоять, а й **як вони тримаються одна
   одної**.
8. **Це не артефакт однієї монети.** На кожній з шести монет знак ефекту
   той самий: на пачці 1 `|d_resist|` — 0.283 … 0.361, `|d_vol_sell|` —
   0.629 … 0.670.
9. **Різниця розмазана по всій події.** AUC по кожному з девʼяти блоків
   окремо тримається на одному рівні (`|d_resist|` 0.372 … 0.375,
   `|d_vol_buy|` 0.572 … 0.576) — жоден блок не несе її сам.
10. **Ефект слабшає з вікном.** На пачці 20 `|d_resist|` у нозі: 0.233
    при ±10 і 0.326 при ±80 — що ширший окіл, то більше в ньому звичайних
    подій ноги. Різниця сидить біля самого розвороту.
11. **Розділити когорти цим усе одно не вийде.** Найсильніша колонка дає
    0.756 — це помітно більше за 0.633 з першого заміру на одній події,
    але це ознака, а не вирішальне правило.

Окремо варто тримати в голові, що когорти дуже різні за розміром (12 671
проти 343 122 при пачці 1 і ±10), а `val` — це лише три тижні й небагато
півотів, тому його числа гуляють сильніше за train.

## Етап 8. Фічі вздовж ноги: десятина за десятиною — `підтверджено`

Етап 7 порівнював окіл півота з рештою ноги однією парою когорт. Тут нога
ділиться на **всі десять частин**, і кожна порівнюється з кожною: чи
залежать фічі від того, де саме в нозі стоїть подія.

### Що з чого береться

Ноги й десятини — ті самі, що в етапі 5: зігзаг 2.3 % на `ема10`, нога
ділиться на десять рівних частин за кількістю подій.

З кожної десятини береться **початок** — перші `m` подій цієї десятини:

```
m = 1, 2, 3, 5, 10        подій з початку кожної десятини
```

`m = 1` — рівно перша подія десятини, далі вибірка росте. Значення
десятини — середнє згортки по цих `m` подіях. Згортки й пачки ті самі, що
в етапі 7: вісім згорток, `W = 1 … 20` подій у пачці.

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

Профіль рахується тричі: на **всіх ногах**, окремо на **ногах вгору** і
окремо на **ногах вниз**. Напрямок задає півот, яким нога закінчується:
нога вгору починається з нижнього півота, нога вниз — з верхнього
(rules/02 → зігзаг). Два напрямки порівнюються між собою `AUC` рангів:
ранг уже нормований усередині своєї ноги, тому ноги різних напрямків
порівнюються напряму.

| Пачка | ніг у замірі (`m = 1`) | вгору | вниз |
| ----- | ---------------------- | ----- | ---- |
| 1 | 601 | 301 | 300 |
| 2 | 601 | 301 | 300 |
| 3 | 601 | 301 | 300 |
| 5 | 597 | 298 | 299 |
| 10 | 569 | 280 | 289 |
| 20 | 532 | 264 | 268 |

### Як міряється залежність

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

| Міра | Що каже |
| ---- | ------- |
| **середній ранг** | десятина ранжується серед десяти своїх, ранг усереднюється по ногах. Немає залежності — усі десять близькі до 5.5 |
| **нахил ρ** | медіана кореляції Спірмена між номером десятини й значенням усередині ноги, і частка ніг з додатним нахилом |
| **матриця** | частка ніг, де десятина `i` більша за десятину `j`. Обидві з тієї самої ноги, тому це знаковий тест: `0.5` — різниці немає |
| **вгору проти вниз** | `AUC` рангів десятини між ногами вгору і ногами вниз. `0.5` — профілі однакові |

Нахил ловить **рівний хід** уздовж ноги і не ловить підйому на обох
кінцях, тому головний вивід — профіль рангів, а не ρ.

```bash
python3 research/21-class210x10x7dc/scripts/leg_profile.py
```

Скрипт: [`scripts/leg_profile.py`](scripts/leg_profile.py)
Вихід: [`results/leg_profile.json`](results/leg_profile.json)

### Результат: профіль по десятинах

Середній ранг десятини, згортка `mean(|v|)`, пачка 1, `m = 1`
(5.5 = місце в нозі ні на що не впливає):

| Фіча | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ніг |
| ---- | - | - | - | - | - | - | - | - | - | -- | --- |
| `d_price_avg` | **6.88** | 5.64 | 5.48 | 5.33 | 5.14 | 5.29 | 5.30 | 5.10 | 5.22 | 5.63 | 601 |
| `d_vol_sell` | **6.71** | 5.52 | 5.41 | 5.28 | 5.21 | 5.22 | 5.32 | 5.26 | 5.34 | 5.72 | 601 |
| `d_vol_buy` | **6.66** | 5.46 | 5.23 | 5.26 | 5.22 | 5.21 | 5.35 | 5.37 | 5.53 | 5.71 | 601 |
| `d_resist` | **3.81** | 5.58 | 5.80 | 5.71 | 5.81 | 5.81 | 5.64 | 5.90 | 5.75 | 5.20 | 457 |
| `d_support` | **4.14** | 5.24 | 5.69 | 5.98 | 5.84 | 5.81 | 5.83 | 5.82 | 5.51 | 5.15 | 389 |
| `d_bal_resist` | 4.26 | 5.72 | 5.75 | 5.70 | 5.75 | 5.77 | 5.56 | 5.65 | 5.58 | 5.26 | 457 |
| `d_bal_support` | 4.59 | 5.32 | 5.88 | 5.85 | 5.79 | 5.61 | 5.63 | 5.68 | 5.61 | 5.04 | 389 |

Те саме на пачці 20 при `m = 10` — профіль той самий, але різкіший:

| Фіча | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ніг |
| ---- | - | - | - | - | - | - | - | - | - | -- | --- |
| `d_price_avg` | **7.78** | 6.04 | 5.70 | 5.34 | 4.87 | 4.89 | 5.01 | 4.81 | 4.83 | 5.73 | 437 |
| `d_vol_buy` | **7.73** | 5.59 | 5.19 | 5.24 | 4.76 | 4.94 | 5.26 | 5.12 | 5.20 | 5.97 | 437 |
| `d_vol_sell` | **7.55** | 5.67 | 5.24 | 5.17 | 4.88 | 4.83 | 5.21 | 5.12 | 5.15 | 6.18 | 437 |
| `d_resist` | **3.58** | 5.45 | 5.72 | 5.46 | 5.80 | 5.91 | 5.91 | 6.23 | 5.96 | 4.98 | 142 |
| `d_support` | **3.93** | 5.33 | 5.90 | 5.57 | 5.55 | 6.12 | 5.84 | 5.53 | 5.86 | 5.37 | 83 |

Згортка `mean(v)` — зі знаком — рівна на всіх десятинах: 4.88 … 6.32 без
жодного порядку.

### Результат: десятина проти десятини

`mean(|d_price_avg|)`, пачка 20, `m = 10` — частка ніг, де рядок більший
за колонку:

| | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| - | - | - | - | - | - | - | - | - | - | -- |
| **1** | · | 0.753 | 0.746 | 0.751 | 0.783 | 0.792 | 0.730 | 0.778 | 0.748 | 0.698 |
| **10** | 0.302 | 0.453 | 0.522 | 0.540 | 0.579 | 0.581 | 0.561 | 0.579 | 0.609 | · |

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

1. **Уся різниця сидить у першій десятині.** Перші 10 % ноги стоять
   осторонь від решти: `|d_price_avg|` має там ранг 6.88 з 10 (7.78 на
   пачці 20), а решта девʼять десятин лежать у смузі 5.1 … 5.6. Перша
   десятина більша за будь-яку іншу в **70 … 79 % ніг**.
2. **Кінець ноги виражений набагато слабше.** Десята десятина ледь
   піднята: 5.63 проти 5.1 … 5.3 у середині, і виграє в середніх лише в
   54 … 58 % ніг. Проти першої десятини вона програє в 70 % ніг.
3. **Це міняє прочитання етапу 7.** Окіл `±K` навколо півота бере і кінець
   ноги, і початок наступної. Тепер видно, що майже весь ефект дає **бік
   після розвороту**, а не перед ним: фічі радше **реагують** на розворот,
   ніж передвіщають його. Для прогнозу це погана новина, і вона чесна.
4. **Рівні стакана дзеркальні й теж мають форму.** `|d_resist|` у першій
   десятині найнижчий (ранг 3.58), далі росте до 8-ї десятини (6.23) і
   знову падає в останній (4.98). Тобто після розвороту стакан на одиницю
   потоку найтонший, за ногу він відновлюється, і перед наступним
   розворотом знову тоншає.
5. **Знаку немає й тут.** Згортка `mean(v)` дає рівну лінію на всіх
   десятинах — напрямку похідних місце в нозі не задає, тільки величину.
6. **Пачка знову підсилює.** Ранг першої десятини `|d_price_avg|`: 6.88
   при `W = 1` і 7.78 при `W = 20`. Більше подій у пачці — чіткіший
   профіль.
7. **`m` майже нічого не міняє.** Профіль при `m = 1` і `m = 10`
   збігається до другого знака; росте тільки кількість ніг, що проходять
   умову. Тобто перша подія десятини вже несе те саме, що й перші десять.
8. **Нахил ρ мало що каже.** Найсильніші значення — `|d_price_avg|`
   ρ = −0.273 (29 % ніг вище нуля) на пачці 20: фіча загалом спадає
   вздовж ноги. Але це наслідок сходинки в першій десятині, а не рівного
   спаду: профіль U-подібний, і монотонна міра його не описує.
9. **Провалюється саме та сторона стакана, яку щойно пробили.** Ноги вгору
   і ноги вниз дають дзеркальну картину. Після **нижнього** півота (нога
   вгору) в першу десятину провалюється опір: `|d_resist|` ранг 3.49,
   `|d_bal_resist|` 3.63, а підтримка майже не зачеплена (4.75 і 5.46).
   Після **верхнього** півота (нога вниз) — навпаки: підтримка 3.51 і
   3.70, опір 4.14 і 4.91. `AUC` між напрямками: 0.673 на
   `|d_bal_support|` і 0.362 на `|d_bal_resist|`, і на пачці 20 це
   доростає до 0.775 і 0.753.
10. **Перекіс у бік падіння тримає обʼєм купівлі, а не продажу.** На
    одній події два обʼєми дзеркальні: після верхнього півота більший
    сплеск дає `|d_vol_sell|` (ранг 7.04 проти 6.38, `AUC` 0.426), після
    нижнього — трохи більший `|d_vol_buy|` (6.73 проти 6.58, `AUC` 0.514).
    На пачці 20 картина міняється: `|d_vol_sell|` стає **рівно
    симетричним** (7.55 і 7.55, `AUC` 0.509), а `|d_vol_buy|` — різко
    асиметричним: 8.28 на ногах вниз проти 7.15 на ногах вгору
    (`AUC` 0.351). Ціна йде за ним: 8.11 проти 7.43. Тобто годину після
    верхнього півота сильніше за все сіпається саме **потік купівлі** —
    той бік, що програв. Друга десятина ніг вниз це підтверджує:
    `|d_vol_buy|` там 6.09 проти 5.07 (`AUC` 0.394), тоді як
    `|d_vol_sell|` — 6.22 проти 5.09 (0.379).
11. **Перед нижнім півотом підтримка найтовща.** На ногах вниз девʼята
    десятина `|d_support|` має ранг 6.88 — найвищий у профілі після першої.
    Тобто перед самим розворотом угору стакан підтримки на одиницю потоку
    встигає нарости.

Обмеження, яке треба тримати в голові: у розділі за напрямком колонки
стакана на пачці 20 стоять на **41 … 71 нозі** в кожному напрямку — це
мало, і числа там гуляють. На пачці 1 їх 192 … 233 в кожному напрямку,
і знак ефекту той самий, тому висновок 9 тримається на обох.
На пачці 20 колонки стакана лишаються всього в **142 і 83 ногах** з 532 — `NaN` в одному блоці з 180
вбиває всю пачку, а блоки з нульовим обʼємом найчастіші саме в
`XRP_USDT` і `DOGE_USDT`. Обʼємні й цінова колонки на тій самій пачці
тримають 437 ніг. Тому числа стакана на довгих пачках стоять на меншій
і, можливо, зміщеній підвибірці.

## Етап 9. Подія «розворот був»: датасет у пачках — `підтверджено`

Етап 8 показав, що фічі **реагують** на розворот: уся різниця сидить у
першій десятині ноги, тобто одразу після півота. Тут ця реакція стає
власною подією.

### Розмітка

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

```
нижній розворот   у межах пачки стоїть півот low   — зелена
верхній розворот  у межах пачки стоїть півот high  — червона
інша              півота в пачці немає
```

Кольори — за напрямком руху, який розворот відкриває
([`rules/05`](../../rules/05-visualization.md) → Кольори): нижній півот
починає рух угору, тому зелений; верхній — рух униз, тому червоний.
Пачка з кількома півотами (нога коротша за `W`) бере **останній**.

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

### Вибір довжини

```
W = 1, 2, 3, 5, 10, 20, 40, 80        пачка = 3.5 хв … 4.7 год
```

На кожній довжині рахується `AUC` кожної колонки між пачками «розворот
був» і «інша» — **на кожній монеті окремо**, і по монетах береться
медіана. **Сила довжини** — середнє `|AUC − 0.5|` пʼяти найсильніших
колонок: пʼяти, а не однієї, щоб довжину не обирала колонка, яка
випадково стрибнула.

Разом із силою рахується **частка живих пачок** — тих, у яких усі 56
згорток скінченні. Блок з нульовим обʼємом дає `NaN` у колонках стакана,
і що довша пачка, то ймовірніше такий блок у ній трапиться. Пачка з `NaN`
не потрапляє ні на карту (rules/05: пропуски не проєктуються), ні в
жоден замір, який вимагає повного вектора. Тому довжина обирається
**серед тих, де живих не менше 70 %**.

| W | час | пачок train | нижніх | верхніх | живих | сила | найсильніша колонка | AUC |
| - | --- | ----------- | ------ | ------- | ----- | ---- | ------------------- | --- |
| 1 | 3.5 хв | 356 357 | 301 | 306 | **89 %** | 0.221 | `abs·d_price_avg` | 0.751 |
| 2 | 7 хв | 177 384 | 291 | 298 | **84 %** | 0.220 | `abs·d_price_avg` | 0.737 |
| 3 | 10.5 хв | 118 039 | 291 | 300 | **79 %** | 0.215 | `abs·d_price_avg` | 0.724 |
| **5** | **17.5 хв** | **70 396** | **280** | **298** | **73 %** | **0.224** | `abs·d_price_avg` | 0.728 |
| 10 | 35 хв | 34 764 | 276 | 295 | 63 % | 0.224 | `abs·d_resist` | 0.269 |
| 20 | 70 хв | 16 746 | 266 | 291 | 52 % | 0.224 | `abs·d_vol_buy` | 0.727 |
| 40 | 2.3 год | 8 007 | 248 | 265 | 41 % | 0.226 | `abs·d_vol_buy` | 0.735 |
| 80 | 4.7 год | 3 707 | 228 | 240 | 29 % | 0.230 | `abs·d_vol_buy` | 0.738 |

**Обрано `W = 5`** — найсильніша серед придатних за живістю.

### Розмічений датасет

`W = 5`, вибірка train:

| Монета | пачок | нижніх розворотів | верхніх | частка з розворотом |
| ------ | ----- | ----------------- | ------- | ------------------- |
| `BTC_USDT` | 12 097 | 38 | 41 | 0.65 % |
| `ETH_USDT` | 12 257 | 53 | 56 | 0.89 % |
| `SOL_USDT` | 12 151 | 58 | 60 | 0.97 % |
| `XRP_USDT` | 10 835 | 43 | 46 | 0.82 % |
| `BNB_USDT` | 12 219 | 39 | 42 | 0.66 % |
| `DOGE_USDT` | 10 837 | 49 | 53 | 0.94 % |
| **разом** | **70 396** | **280** | **298** | **0.82 %** |

```bash
python3 research/21-class210x10x7dc/scripts/reversal_batches.py
python3 research/21-class210x10x7dc/scripts/run_umap_reversal.py

# 9. ряди монет для графіка розділу 6
python3 research/21-class210x10x7dc/scripts/feature_chart.py

# 10. карти всіх 127 наборів фіч на двох довжинах пачки (близько години)
python3 research/21-class210x10x7dc/scripts/umap_subsets.py

# 11. те саме для клас210х10-1х3+1дц: 15 наборів, близько семи хвилин
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x3p1

# 12. теплові карти фіч: два класи × дві довжини пачки
python3 research/21-class210x10x7dc/scripts/feature_heatmap.py

# 13. хід фіч від півота до півота, ноги росту й падіння нарізно
python3 research/21-class210x10x7dc/scripts/leg_track.py
```

Скрипти: [`scripts/reversal_batches.py`](scripts/reversal_batches.py),
[`scripts/run_umap_reversal.py`](scripts/run_umap_reversal.py)
Вихід: [`results/reversal_pick.json`](results/reversal_pick.json),
`results/reversal_<PAIR>.json` — ряд монети для графіка,
`results/reversal_batches.npz` — пачки train,
`results/umapr[3d]_train_<metric>.*` — шість карт.

### Карта пачок

Вектор пачки — її **56 згорток** (вісім способів звести ряд у число × сім
фіч), z-score на вибірці, `n_neighbors 30`, `min_dist 0.1`, `seed 0`,
три метрики × дві вимірності.

Вибірка **розшарована**: узято всі 578 пачок з розворотом і випадкова
решта до 30 000. Пачки з розворотом малюються **більшими й поверх
решти** — 490 точок з 21 953 інакше губляться серед фону; порядок за
глибиною лишається всередині кожної з двох груп. Кнопки над картою
вмикають і вимикають мітки. На випадковій вибірці розворотів лишилось би кілька
десятків — менше відсотка. Через це щільність точок на карті вже не
відповідає щільності в датасеті: карта читається як сусідство, а не як
«де густіше». Спроєктовано 21 953 пачки, 8 047 відкинуто через `NaN`;
з розворотом на карті лишилось 490 (243 нижніх і 247 верхніх).

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

1. **Довжина пачки майже не впливає.** Сила тримається 0.215 … 0.230 на
   всій сітці від 3.5 хвилин до 4.7 години — розкид 0.015. Те, що
   розворот був, видно однаково добре будь-якою пачкою; міняється лише
   те, **яка колонка це несе**: на коротких — рух ціни
   (`abs·d_price_avg` 0.751 при `W = 1`), на довгих — обʼєм
   (`abs·d_vol_buy` 0.738 при `W = 80`). Приблизно на `W = 10` вони
   міняються місцями.
2. **Довжину вирішує не сила, а `NaN`.** Живих пачок: 89 % при `W = 1`
   і 29 % при `W = 80`. Одного блока з нульовим обʼємом досить, щоб
   викинути всю пачку, і на 720 блоках він майже неминучий. Саме це, а
   не якість розділення, ставить стелю довжині.
3. **Розділити нижній розворот від верхнього — тривіально, і це
   артефакт.** `mean·d_price_avg` дає `AUC` **0.997** при `W = 1`
   (0.993 … 0.999 на кожній монеті окремо). Причина не в фічах: зігзаг
   стоїть на `ема10`, яка запізнюється приблизно на 4–5 подій, тож у
   момент, коли ема доходить до свого мінімуму, сира ціна вже кілька
   подій росте. Знак похідної ціни в позначеній події і є цей запізнілий
   рух. З довжиною пачки артефакт тане: 0.982 при `W = 5`, 0.806 при
   `W = 20`, 0.457 при `W = 80` — довга пачка захоплює і те, що було до
   півота. **Це не прогноз і не знахідка про ринок**, це властивість
   розмітки, і тримати її в голові обовʼязково.
4. **Сама подія лишається слабкою ознакою.** Найкраща колонка на обраній
   довжині дає 0.728 — те саме, що в етапах 7 і 8. Пачок з розворотом
   0.82 %, тож навіть при такому `AUC` більшість відібраних пачок буде
   без розвороту.

## Етап 10. Ціна із зігзагом і кожна фіча під нею — `підтверджено`

Найпростіший погляд на клас: подивитись на самі ряди. Верхнє полотно —
`ема10` ціни події із зігзагом 2.3 % поверх, під ним на **спільній осі X**
— обрана фіча `клас210х10-1х7дц`. Монета, фіча й згортка обираються
кнопками, і так для всіх шести монет.

### Компоновка

За [`rules/05-visualization.md`](../../rules/05-visualization.md) →
Обовʼязкова компоновка:

```
опис: монета, ема, зігзаг, фіча, згортка
─────────────────────────────────────────
ема10 ціни + зігзаг 2.3 %          ← графік
─────────────────────────────────────────
фіча класу, зведена згорткою       ← підвал, спільна вісь X
─────────────────────────────────────────
легенда: що яким кольором
```

- **Спільна вісь X, спільний зум і зсув.** Обидва полотна тримають один
  вид (`dataset.view`), тому зсув графіка зсуває підвал на ту саму
  позицію.
- **Шкала Y на кожному полотні своя** і рахується з даних **видимого
  вікна**, а не з усього ряду: зумом у спокійну ділянку фіча
  розтягується на всю висоту. Фіксованих меж немає.
- Кольори фіч — категорії без напрямку, тому свої й від теми незалежні;
  зігзаг іде за правилом «рух вгору зелений, вниз червоний».

### Ряд

Подія — девʼять блоків на фічу, тому в ряд вона зводиться **згорткою**:
тими самими вісьмома, що в етапах 7–9 (`mean`, `abs`, `range`, `std`,
`step`, `straight`, `acf1`, `signs`).

Ряд монети — близько 70 тисяч подій, і повністю в браузер він не їде:
береться кожна пʼята подія, лишається 12.5–14 тисяч точок. Проріджування
рівномірне, тому **одиничний сплеск на графіку може бути пропущений** —
читаються форма й рівні, а для викидів є заміри етапів 7–9.

**Події півотів лишаються завжди**: вони додаються до проріджених, тому
зігзаг стоїть рівно там, де його порахували на повному ряду. На
`BTC_USDT` це 92 півоти з 92, на `SOL_USDT` — 137 з 137.

| Монета | подій | точок | крок | півотів |
| ------ | ----- | ----- | ---- | ------- |
| `BTC_USDT` | 68 966 | 13 871 | 5 | 92 |
| `ETH_USDT` | 69 770 | 14 037 | 5 | 126 |
| `SOL_USDT` | 69 263 | 13 942 | 5 | 137 |
| `XRP_USDT` | 62 580 | 12 613 | 5 | 124 |
| `BNB_USDT` | 69 567 | 13 993 | 5 | 94 |
| `DOGE_USDT` | 62 579 | 12 614 | 5 | 128 |

Вихід — **по файлу на монету й згортку**: усі вісім разом дають вісім
мегабайт на монету, і вкладка тягла б їх усі заради однієї. Один файл —
1.1–1.3 МБ.

```bash
python3 research/21-class210x10x7dc/scripts/feature_chart.py
python3 research/21-class210x10x7dc/scripts/feature_chart.py --points 8000
```

Скрипт: [`scripts/feature_chart.py`](scripts/feature_chart.py)
Вихід: `results/chart_<PAIR>_<згортка>.json`,
[`results/charts.json`](results/charts.json) — перелік монет і згорток.

## Етап 11. Карта будь-якого набору фіч — `підтверджено`

Розділи 4 і 9 будували карту по **всіх** фічах разом. Тут карта будується
по будь-якому набору: одна фіча, дві, три … усі сім — набір складається
кнопками. Так видно, яка саме фіча тримає структуру, а яка нічого не
додає.

### Точка карти

Точка — **пачка**, розмічена так само, як в етапі 9: у межах пачки стоїть
півот `low` (нижній розворот), `high` (верхній) або жодного (інша пачка).
Пачки не перекриваються, довжини дві: **`W = 5` і `W = 20`**.

### Вектор точки

Фіча в пачці — це ряд з `9W` блоків, тому в число вона зводиться
згорткою. Беруться **всі вісім** згорток етапів 7–9, тому набір з `k` фіч
дає вектор у `8k` значень: одна фіча — 8, усі сім — 56.

### Вибірка й метрика

Вибірка **одна на всі набори й обидві довжини пачки** — інакше карти не
порівнюються між собою: міняється тільки вектор. Вона розшарована: усі
пачки з розворотом і випадкова решта до 6 000, бо розворотів менше
відсотка.

`NaN` відкидається **на кожному наборі окремо** — і це саме по собі
показове:

| Набір, `W = 5` | точок | відкинуто `NaN` | нижніх | верхніх |
| -------------- | ----- | --------------- | ------ | ------- |
| `d_vol_buy`, `d_vol_sell`, `d_price_avg` | 6 000 | **0** | 280 | 298 |
| `d_resist`, `d_bal_resist` | 5 098 | 902 | 258 | 261 |
| `d_support`, `d_bal_support` | 4 696 | 1 304 | 248 | 264 |
| усі сім | 4 429 | 1 571 | 264 | 274 |

На `W = 20` те саме сильніше: обʼєми й ціна тримають усі 6 000 пачок,
`d_support` лишає 3 417, усі сім разом — 3 155. Причина одна: блок з
нульовим обʼємом дає `NaN` у колонках стакана, а обʼєм і ціна такого
блоку не бояться.

Підмножин 127, і на трьох метриках це вісім сотень карт. Вісь, уздовж
якої тут дивляться, — **набір фіч**, а не метрика, тому карти будуються
`cosine`: rules/05 називає її основною для читання. Три метрики
лишаються на картах класів (розділ 1 вкладки).

### Скільки карт

```
127 наборів × 2 довжини пачки = 254 карти
```

Кожна — свій файл (~0.18 МБ), точки лежать паралельними рядами
(`x`, `y`, `z`, `t`, `g`, `c`), а не списком обʼєктів: так файл утричі
менший, а вкладка розгортає їх один раз. Уся тека `results/` цього
дослідження після цього етапу важить близько 200 МБ.

```bash
python3 research/21-class210x10x7dc/scripts/umap_subsets.py
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --batch 5 --size 1
```

Скрипт: [`scripts/umap_subsets.py`](scripts/umap_subsets.py)
Вихід: `results/umapf_w<W>_<набір>.json`,
[`results/umap_subsets.json`](results/umap_subsets.json) — перелік наборів.

Ключ набору — `all` для повного, інакше короткі імена фіч (без префікса
`d_`) через дефіс **у порядку самого класу**, а не в порядку кліків:
`resist-vol_buy`. Той самий ключ будує вкладка, тому кнопки й імена
файлів сходяться.

Карта на вкладці стоїть **у плашці**, згорнутій за замовчуванням: кнопки
набору лишаються на видноті, а полотно розгортається, коли треба. Це
спільна поведінка всіх розділів, зроблених цим кодом, — 7, 8 і 11.

## Етап 12. Клас210х10-1х3+1дц і карта його наборів — `підтверджено`

Шість із семи фіч `клас210х10-1х7дц` ідуть парами «опір проти
підтримки». Тут кожна пара згортається в одну фічу, і виходить клас
удвічі вужчий.

### Клас

Повний опис і формули —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х10-1х3+1дц).
У кожному з девʼяти блоків:

```
b = (|a| − |b|) / (|a| + |b|)
```

| Фіча | `a` | `b` |
| ---- | --- | --- |
| `b_resist_support` | `d_resist` | `d_support` |
| `b_bal_resist_support` | `d_bal_resist` | `d_bal_support` |
| `b_vol_buy_sell` | `d_vol_buy` | `d_vol_sell` |

Сьома фіча — `d_price_avg` — пари не має і переноситься **як є**. Звідси
`3+1` в імені: три різниці плюс одна перенесена. Форма події `(9, 4)`.

**Модулі в чисельнику міняють питання.** Фіча каже, **чия сторона
сильніше зрушила**, а не куди вона зрушила: `+1` — рухався тільки опір,
`−1` — тільки підтримка, `0` — порівну. Знак самих похідних тут зникає;
куди тягне кожна сторона, лишається в батьківському класі.

Блок, де обидві похідні пари рівно нульові, дає `NaN`. Таких блоків мало
— від 50 на `BTC_USDT` до 314 на `DOGE_USDT`; решта `NaN` просто
успадкована від батька.

Клас будується тим самим пайплайном, що й решта чотирьох:

```bash
python3 rules/scripts/build_class210x10x7dc.py
```

Скрипт класу:
[`rules/scripts/class210x10m1x3p1dc.py`](../../rules/scripts/class210x10m1x3p1dc.py)
Датасет: `data/datasets/<PAIR>/class210x10m1x3p1dc/{train,val}.npz`,
розбиття те саме — 356 357 подій train і 46 368 val.

| Монета | `NaN` значень | блоків з нульовою парою |
| ------ | ------------- | ----------------------- |
| `BTC_USDT` | 2 222 | 50 |
| `ETH_USDT` | 7 678 | 56 |
| `SOL_USDT` | 18 010 | 76 |
| `XRP_USDT` | 47 091 | 81 |
| `BNB_USDT` | 18 935 | 73 |
| `DOGE_USDT` | 155 220 | 314 |

### Карта наборів

Те саме, що в етапі 11, на новому класі: точка — пачка, розмічена як в
етапі 9; фіча в пачці зводиться всіма вісьмома згортками, набір з `k`
фіч дає вектор у `8k` значень; вибірка одна на всі набори, розшарована;
метрика `cosine`.

Фіч чотири, тому наборів **15** замість 127:

```
15 наборів × 2 довжини пачки = 30 карт
```

Скрипт той самий, з іншим класом; файли розділені префіксом, тому карти
двох розділів не перетирають одна одну:

| `--class` | Клас | фіч | наборів | префікс |
| --------- | ---- | --- | ------- | ------- |
| `x7` | `клас210х10-1х7дц` | 7 | 127 | `umapf_` |
| `x3p1` | `клас210х10-1х3+1дц` | 4 | 15 | `umapg_` |

```bash
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x3p1
```

Вихід: `results/umapg_w<W>_<набір>.json`,
[`results/umap_subsets31.json`](results/umap_subsets31.json).

### Скільки пачок лишається

`NaN` відкидається на кожному наборі окремо, і згортання пар це добре
показує:

| Набір, `W = 5` | точок | відкинуто `NaN` |
| -------------- | ----- | --------------- |
| `d_price_avg` | 6 000 | 0 |
| `b_vol_buy_sell` | 5 970 | **30** |
| `b_resist_support` | 4 429 | 1 571 |
| `b_bal_resist_support` | 4 429 | 1 571 |
| усі чотири | 4 429 | 1 571 |

На `W = 20`: `b_vol_buy_sell` лишає 5 872 пачки, а обидві фічі стакана —
3 155.

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

1. **Згортання пари не рятує від `NaN` стакана.** `b_resist_support`
   лишає рівно стільки ж пачок, скільки й окрема `d_resist` у розділі 7
   (4 429 проти 4 429 при `W = 5`): `NaN` в одній з двох похідних робить
   `NaN` усю різницю. Проблема нульових обʼємів у блоці лишається там,
   де й була.
2. **Обʼєми згортанням виграють.** Окремі `d_vol_buy` і `d_vol_sell` не
   мали `NaN` зовсім, і різниця теж майже не має — 30 пачок з 6 000 при
   `W = 5`. Це ті блоки, де **обидва** обʼєми не рухались; поодинці такий
   блок дає нуль, а в парі — нульовий знаменник.
3. **Клас удвічі вужчий за той самий погляд.** Сім фіч × 8 згорток = 56
   значень на пачку, чотири фічі × 8 = 32. Наборів 15 замість 127, і
   кожен з них має власну карту.

## Етап 13. Теплові карти фіч — `підтверджено`

Дві карти на кожну комбінацію «клас × довжина пачки», разом чотири
комбінації: `клас210х10-1х7дц` і `клас210х10-1х3+1дц` × `W = 5` і
`W = 20`. Пачки нарізані й розмічені так само, як в етапі 9.

### Фіча × згортка

У клітинці — `AUC` цієї колонки між пачками «розворот був» і «інша»,
порахований **на кожній монеті окремо**, у клітинку йде медіана по
шести. Питань три: `розворот був`, `нижній розворот`, `верхній розворот`.

`клас210х10-1х7дц`, `W = 5`, питання «розворот був»:

| Фіча | mean | abs | range | std | step | straight | acf1 | signs |
| ---- | ---- | --- | ----- | --- | ---- | -------- | ---- | ----- |
| `d_resist` | 0.498 | **0.275** | 0.285 | 0.281 | **0.274** | 0.519 | 0.495 | 0.499 |
| `d_bal_resist` | 0.506 | 0.328 | 0.335 | 0.329 | 0.326 | 0.547 | 0.509 | 0.462 |
| `d_support` | 0.499 | 0.317 | 0.329 | 0.327 | 0.318 | 0.508 | 0.502 | 0.481 |
| `d_bal_support` | 0.500 | 0.344 | 0.358 | 0.352 | 0.346 | 0.517 | 0.507 | 0.463 |
| `d_vol_buy` | 0.468 | **0.695** | 0.632 | 0.651 | 0.689 | 0.520 | 0.527 | 0.467 |
| `d_vol_sell` | 0.506 | 0.688 | 0.637 | 0.659 | 0.682 | 0.527 | 0.514 | 0.497 |
| `d_price_avg` | 0.484 | **0.728** | 0.708 | 0.720 | 0.717 | 0.524 | 0.518 | 0.502 |

`клас210х10-1х3+1дц`, `W = 5`:

| Фіча | mean | abs | range | std | step | straight | acf1 | signs |
| ---- | ---- | --- | ----- | --- | ---- | -------- | ---- | ----- |
| `b_resist_support` | 0.504 | 0.366 | 0.394 | 0.364 | 0.469 | 0.494 | 0.454 | 0.492 |
| `b_bal_resist_support` | 0.512 | 0.357 | 0.385 | 0.343 | 0.457 | 0.454 | 0.428 | 0.511 |
| `b_vol_buy_sell` | 0.500 | 0.354 | 0.373 | 0.358 | 0.443 | 0.488 | 0.450 | 0.525 |
| `d_price_avg` | 0.484 | **0.728** | 0.708 | 0.720 | 0.717 | 0.524 | 0.518 | 0.502 |

### Фіча × фіча

У клітинці — кореляція Спірмена між фічами по всіх пачках train, окремо
на кожну згортку. Спірмен — бо похідні мають важкі хвости.

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

1. **Уся сила сидить у стовпці величини.** `abs` і `step` дають найдальші
   від 0.5 значення, `range` і `std` трохи слабші, а `mean`, `straight`,
   `acf1` і `signs` тримаються 0.45 … 0.55 майже скрізь. Карта показує це
   одним поглядом: два стовпці кольорові, шість блідих.
2. **Два блоки з різними знаками.** Обʼєми й ціна дають `AUC` **вище**
   0.5 (0.69 … 0.73) — біля розвороту вони більші; рівні стакана — **нижче**
   (0.27 … 0.36) — біля розвороту вони менші. Це та сама пара спостережень,
   що в етапі 7, тільки видно її відразу.
3. **Згортання пар силу гасить.** У `клас210х10-1х3+1дц` три нормовані
   різниці дають 0.35 … 0.39 при `W = 5` — слабше за окремі
   `d_vol_buy` (0.695) чи `d_price_avg` (0.728) і приблизно на рівні
   найслабших фіч стакана. Ділення однієї сторони на іншу прибирає саме
   те, що несло сигнал: **спільне зростання обох сторін**. Перенесена
   `d_price_avg` лишається найсильнішою колонкою і в цьому класі.
4. **Довжина пачки майже нічого не міняє.** Між `W = 5` і `W = 20` числа
   гуляють у третьому знаку: `abs·d_price_avg` 0.728 і 0.720,
   `abs·d_resist` 0.275 і 0.276. Це узгоджується з етапом 9, де сила
   довжини трималась 0.215 … 0.230 на всій сітці.

```bash
python3 research/21-class210x10x7dc/scripts/feature_heatmap.py
```

Скрипт: [`scripts/feature_heatmap.py`](scripts/feature_heatmap.py)
Вихід: [`results/heatmap.json`](results/heatmap.json)

## Етап 14. Хід фіч від півота до півота — `підтверджено`

Етап 8 ділив ногу на десять частин і брав **початок** кожної. Тут нога
проходиться **цілком**: від півота, яким вона почалась, до півота, яким
скінчилась. Два класи × дві довжини пачки, ноги росту й ноги падіння
нарізно.

### Сітка ноги

Ноги різної довжини, тому кожна розтягується на спільну сітку з **20
клітинок** по 5 %:

```
місце події = (номер у нозі) / (довжина ноги)      0 … 1
клітинка    = floor(місце × 20)                    0 … 19
```

Значення клітинки — середнє згортки по подіях, що в неї потрапили. Нога
бере участь, тільки якщо **жодна клітинка не порожня**: при 20 клітинках
це нога щонайменше в 20 придатних подій. Придатна подія — та, у якої є
повна пачка з `W` подій підряд.

| Пачка | ніг у замірі | вгору | вниз |
| ----- | ------------ | ----- | ---- |
| 5 | 579 | 285 | 294 |
| 20 | 423 | 210 | 213 |

### Що міряється

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

| Міра | Що каже |
| ---- | ------- |
| **середній ранг** | клітинка ранжується серед 20 своїх, ранг усереднюється по ногах. Немає ходу — усі клітинки біля **10.5** |
| **вгору проти вниз** | `AUC` рангів клітинки між ногами росту й ногами падіння. `0.5` — хід однаковий |

```bash
python3 research/21-class210x10x7dc/scripts/leg_track.py
```

Скрипт: [`scripts/leg_track.py`](scripts/leg_track.py)
Вихід: [`results/leg_track.json`](results/leg_track.json)

### Результат: хід по нозі

`клас210х10-1х7дц`, `W = 20`, згортка `mean(|v|)`, усі ноги
(10.5 = ходу немає):

| Фіча | 0% | 10% | 20% | 30% | 40% | 50% | 60% | 70% | 80% | 90% |
| ---- | -- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `d_price_avg` | **14.4** | 11.3 | 11.1 | 10.4 | 9.2 | 9.1 | 9.8 | **8.9** | 9.3 | 11.4 |
| `d_vol_buy` | **14.2** | 10.5 | 10.2 | 10.1 | 8.9 | 9.0 | 10.0 | 9.7 | 10.2 | 12.0 |
| `d_vol_sell` | **14.1** | 10.6 | 10.1 | 9.8 | 9.1 | **8.7** | 10.0 | 9.7 | 10.3 | 12.2 |
| `d_resist` | **7.7** | 10.3 | 10.6 | 10.9 | 11.4 | **11.8** | 11.4 | 11.5 | 10.9 | 9.2 |
| `d_support` | **7.8** | 10.3 | 10.4 | 11.0 | 10.8 | **12.1** | 10.8 | 11.1 | 10.9 | 9.9 |
| `d_bal_resist` | 8.4 | 10.9 | 10.9 | 10.6 | 11.2 | 11.4 | 11.2 | 10.9 | 10.3 | 9.4 |
| `d_bal_support` | 8.7 | 11.0 | 10.8 | 11.0 | 10.6 | 11.9 | 10.9 | 10.4 | 10.1 | 9.5 |

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

Найбільші відхилення (`клас210х10-1х7дц`, `W = 5`, `mean(|v|)`):

| Фіча | клітинка | AUC вгору/вниз |
| ---- | -------- | -------------- |
| `d_bal_support` | 5 % | **0.671** |
| `d_support` | 5 % | **0.640** |
| `d_bal_resist` | 0 % | **0.381** |
| `d_resist` | 35 % | 0.587 |

Обʼєми й ціна на всій довжині ноги тримаються 0.44 … 0.56 — їх хід від
напрямку не залежить.

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

1. **Хід ноги — це чаша, і вона симетрична.** `|d_price_avg|` починає з
   рангу 14.4 з 20, провалюється до 8.9 у другій половині й піднімається
   до 11.4 на кінці. Обʼєми так само: 14.2 → 8.9 → 12.0. Тобто рух
   найсильніший **на обох кінцях ноги**, а найспокійніший — усередині.
2. **Початок ноги різкіший за її кінець.** 14.4 проти 11.4 у ціни, 14.2
   проти 12.0 в обʼємі купівлі. Це те саме, що показав етап 8 на
   десятинах: сплеск після розвороту сильніший, ніж перед наступним.
3. **Рівні стакана — дзеркальна чаша.** `|d_resist|` починає з 7.7,
   доростає до 11.8 на середині й падає до 9.2 в кінці. Стакан на одиницю
   потоку найтонший одразу після розвороту, за ногу відновлюється і перед
   наступним розворотом знову тоншає.
4. **Різниця напрямків сидить у перших 5 % ноги.** `|d_bal_support|` дає
   там `AUC` 0.671, `|d_support|` — 0.640, `|d_bal_resist|` — 0.381. Далі
   по нозі все повертається до 0.5. Це той самий дзеркальний ефект етапу
   8: провалюється саме та сторона стакана, яку щойно пробили, і живе він
   тільки на початку ноги.
5. **Обʼєми й ціна напрямку не розрізняють.** На всій довжині 0.44 … 0.56
   — сплеск на розвороті однаковий, куди б ринок не пішов далі.
6. **Довша пачка робить чашу глибшою, але форму не міняє.** Ранг першої
   клітинки `|d_price_avg|`: 13.4 при `W = 5` і 14.4 при `W = 20`; провал
   у середині 9.1 і 8.9. Ціна цього — 423 ноги замість 579.
7. **Згортання пар знову гасить.** У `клас210х10-1х3+1дц` усі три різниці
   дають ту саму дзеркальну чашу (8.5 → 11.5 → 9.0), але різниця
   напрямків у перших 5 % падає з 0.671 до 0.541: відношення однієї
   сторони до іншої ховає те, **яка з них** просіла.

## Етап 15. Клас210х10-1х2д: обʼєм на свій рівень — `підтверджено`

Дві фічі замість семи: кожна сторона книги згортається в одну — похідна
обʼєму ділиться на похідну **свого** рівня.

### Клас

Повний опис і формули —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х10-1х2д).
У кожному з девʼяти блоків:

```
d_up   = d_vol_buy  / d_resist
d_down = d_vol_sell / d_support
```

- Форма події `(9, 2)`. Ціни в класі немає, тому й суфікса `ц` немає.
- Фічі безрозмірні: одиниця обʼєму скорочується з обʼємом, що вже стоїть
  у знаменнику рівня. Знак лишається — клас каже і наскільки, і в який бік.
- Блок з нульовим знаменником дає `NaN`. Таких у датасеті **немає
  жодного**: похідна рівня точно нульовою не буває. Увесь `NaN` класу
  успадкований від батька і дорівнює **рівно половині** його: з чотирьох
  колонок стакана батька клас читає дві (`d_resist`, `d_support`), а
  нульовий обʼєм блоку псує їх парами.

| Монета | `NaN` у класі | `NaN` у батька | блоків з нульовим знаменником |
| ------ | ------------- | -------------- | ----------------------------- |
| `BTC_USDT` | 1 774 | 3 548 | 0 |
| `ETH_USDT` | 4 563 | 9 126 | 0 |
| `SOL_USDT` | 9 908 | 19 816 | 0 |
| `XRP_USDT` | 25 178 | 50 356 | 0 |
| `BNB_USDT` | 10 388 | 20 776 | 0 |
| `DOGE_USDT` | 89 787 | 179 574 | 0 |

**Відношення двох похідних не обмежене**: коли рівень майже не зрушив,
значення велике. Тому всі міри на цьому класі — рангові (`AUC`, Спірмен),
а середні по самих значеннях не рахуються.

Клас будується тим самим пайплайном, що й решта пʼяти:

```bash
python3 rules/scripts/build_class210x10x7dc.py
```

Скрипт класу:
[`rules/scripts/class210x10m1x2d.py`](../../rules/scripts/class210x10m1x2d.py)
Датасет: `data/datasets/<PAIR>/class210x10m1x2d/{train,val}.npz`,
розбиття те саме — 356 357 подій train і 46 368 val.

### Де він міряється

Клас стає третім у замірах етапів 13 і 14 і отримує свій розділ карт
наборів — усе тими самими скриптами, з тією самою нарізкою пачок:

| Розділ вкладки | Скрипт | Що додалось |
| -------------- | ------ | ----------- |
| 9 — теплові карти | `feature_heatmap.py` | 2 картки: `W = 5` і `W = 20` |
| 10 — хід по нозі | `leg_track.py` | 2 картки на тих самих 579 і 423 ногах |
| 11 — карта наборів | `umap_subsets.py --class x2d` | 3 набори × 2 довжини = 6 карт |

| `--class` | Клас | фіч | наборів | префікс |
| --------- | ---- | --- | ------- | ------- |
| `x7` | `клас210х10-1х7дц` | 7 | 127 | `umapf_` |
| `x3p1` | `клас210х10-1х3+1дц` | 4 | 15 | `umapg_` |
| `x2d` | `клас210х10-1х2д` | 2 | 3 | `umaph_` |

```bash
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x2d
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x7dd
python3 research/21-class210x10x7dc/scripts/feature_heatmap.py
python3 research/21-class210x10x7dc/scripts/leg_track.py
```

Вихід: `results/umaph_w<W>_<набір>.json`,
[`results/umap_subsets2d.json`](results/umap_subsets2d.json),
[`results/heatmap.json`](results/heatmap.json),
[`results/leg_track.json`](results/leg_track.json).

### Результат: теплова карта

`AUC` пачки «розворот був» проти пачки без розвороту, медіана по шести
монетах:

| `W` | згортка | `d_up` | `d_down` |
| --- | ------- | ------ | -------- |
| 5 | `mean` | 0.374 | 0.368 |
| 5 | `abs` | **0.689** | **0.689** |
| 5 | `step` | 0.682 | 0.682 |
| 20 | `mean` | 0.335 | 0.362 |
| 20 | `abs` | **0.705** | 0.691 |
| 20 | `step` | 0.698 | 0.685 |

Кореляція Спірмена між двома фічами: `abs` — **0.894** (`W = 5`) і
**0.917** (`W = 20`), `mean` — 0.520 і 0.491.

### Результат: хід по нозі

Середній ранг клітинки серед 20 своїх, `W = 20`, усі ноги:

| Колонка | 0 % | 25 % | 50 % | 75 % | 95 % |
| ------- | --- | ---- | ---- | ---- | ---- |
| `mean(\|d_up\|)` | 13.3 | 10.6 | 8.8 | 10.4 | **14.4** |
| `mean(\|d_down\|)` | 13.4 | 10.5 | 9.2 | 10.0 | 13.6 |
| `mean(d_up)` | 9.0 | 10.3 | 11.7 | 10.7 | 7.9 |

Ноги росту проти ніг падіння: `abs·d_up` дає `AUC` 0.353 у клітинці
5–10 %, `abs·d_down` — 0.662 у клітинці 80–85 %; решта сітки 0.42 … 0.58.

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

1. **Дві сторони під модулем — майже одна фіча.** Спірмен 0.89 … 0.92 на
   `abs`: коли обʼєм рветься відносно свого рівня з одного боку, він
   рветься і з другого. Знак їх ще розрізняє (0.49 … 0.52 на `mean`), а
   величина — ні.
2. **Відношення нічого не додає до окремих фіч.** 0.689 … 0.705 проти
   0.727 … 0.728 у `|d_vol_buy|` і `|d_price_avg|` батька. Ділення на
   рівень не підсилює сигнал — воно розчиняє в одній фічі те, що батько
   тримав двома колонками з протилежними `AUC` (обʼєм 0.73, стакан 0.34).
3. **Зі знаком клас дивиться вниз.** `mean` дає 0.335 … 0.374 — стійко
   **нижче** 0.5 на всіх монетах і обох довжинах. Біля розвороту знакове
   відношення менше, ніж у спокійній пачці: обʼєм і рівень частіше
   рухаються в різні боки.
4. **Чаша перевернулась кінцями.** У батька початок ноги був різкіший за
   кінець (14.4 проти 11.4), тут навпаки: 13.3 на початку і **14.4** на
   кінці. Причина — знаменник: `|d_resist|` сам дає дзеркальну чашу й на
   кінці ноги тоншає, тому відношення там росте вдруге.
5. **Різниця напрямків слабша, ніж у батька.** 0.353 замість 0.671 і
   перекинута в інший бік сітки. Клас бачить, що стакан просів, але не
   бачить, **яка** сторона: обидві фічі під модулем ходять разом.
6. **Наборів усього три, і найчистіший з них — `d_up`.** `NaN`
   відкидається на кожному наборі окремо: `d_up` лишає 5 098 пачок з
   6 000, `d_down` — 4 696, обидві разом — 4 429. Набору без втрат тут
   немає взагалі, бо обидві фічі читають стакан.

## Етап 16. Зона розвороту в нозі — `підтверджено`

Етап 14 вів ногу по сітці й дивився на середній ранг клітинки. Тут з
ноги вирізається **зона розвороту**, і питання ставиться прямо: чи можна
відрізнити подію цієї зони від решти подій **тієї самої** ноги.

### Зона

```
місце події = (номер у нозі) / (довжина ноги)      0 … 1
розворот    = місце < 0.05  або  місце >= 0.90
```

Перші 5 % — хвіст щойно відбутого розвороту, останні 10 % — підхід до
наступного. Разом це рівно 15 % подій ноги; кінець удвічі довший за
початок, тому й подій у ньому вдвічі більше.

Місце міряється **по подіях**, як усюди в цьому дослідженні, а не по
пройденій ціні. Нога бере участь, тільки якщо в ній щонайменше **20**
придатних подій: інакше в 5 % не влучає жодна. Придатна подія — та, у
якої є повна пачка з `W` подій підряд; довжини три: `W = 1, 5, 20`.

Клас — `клас210х10-1х2д`, 2 фічі × 8 згорток = **16 колонок**. Той самий
замір іде і на `клас210х10-1х7д/д` (етап 17) — ноги, зона й модель ті самі,
міняються лише колонки; класи перемикаються кнопкою у своєму блоці.

| `W` | ніг train | ніг val | подій train | у зоні | решта |
| --- | --------- | ------- | ----------- | ------ | ----- |
| 1 | 600 | 87 | 350 660 | 52 626 | 298 034 |
| 5 | 597 | 87 | 341 318 | 50 929 | 290 389 |
| 20 | 542 | 85 | 307 102 | 45 731 | 261 371 |

### Міри

| Міра | Що каже |
| ---- | ------- |
| **AUC усередині ноги** | зона проти решти **тієї самої** ноги; медіана по ногах монети, потім по шести монетах |
| **початок / кінець** | те саме, але зона розрізана навпіл |
| **модель** | логістична регресія на всіх 16 колонках: навчена на ногах train, заміряна на ногах val |

Колонки для моделі беруться **рангом усередині своєї ноги**, тому
монета, режим і масштаб фічі скорочуються так само, як у мірі `AUC`.
Ноги val не перетинаються з ногами train ні за часом, ні за подіями:
нога, що лежить по обидва боки межі `2026-08-14`, відкидається цілком.

```bash
python3 research/21-class210x10x7dc/scripts/reversal_zone.py
```

Скрипт: [`scripts/reversal_zone.py`](scripts/reversal_zone.py)
Вихід: [`results/reversal_zone.json`](results/reversal_zone.json)

### Результат: AUC усередині ноги

`W = 20`, медіана по шести монетах:

| Колонка | зона | перші 5 % | останні 10 % |
| ------- | ---- | --------- | ------------ |
| `step·d_up` | **0.660** | **0.687** | 0.619 |
| `abs·d_up` | 0.654 | 0.688 | 0.632 |
| `std·d_up` | 0.637 | 0.654 | 0.611 |
| `abs·d_down` | 0.635 | 0.657 | 0.601 |
| `mean·d_up` | **0.401** | **0.362** | 0.422 |
| `mean·d_down` | 0.416 | 0.382 | 0.417 |
| `acf1·d_up` | 0.490 | 0.472 | 0.484 |
| `signs·d_down` | 0.500 | 0.488 | 0.498 |

На коротших пачках те саме, слабше: найсильніша колонка дає 0.603
(`abs·d_up`, `W = 1`) і 0.621 (`abs·d_down`, `W = 5`).

### Результат: чи можна відрізнити

| `W` | модель train | модель val | краща одинока колонка на val |
| --- | ------------ | ---------- | ---------------------------- |
| 1 | 0.622 | **0.629** | 0.604 (`abs·d_up`) |
| 5 | 0.635 | **0.638** | 0.586 (`abs·d_down`) |
| 20 | 0.628 | **0.625** | 0.577 (`step·d_up`) |

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

1. **Відрізнити можна, але слабко.** Стеля — `AUC` **0.638** на свіжих
   ногах val. Це не «трохи краще за монетку» і не робочий детектор: з
   такою силою на кожну вгадану подію зони припадає багато хибних.
2. **Модель не перевчається.** train 0.622 … 0.635 і val 0.625 … 0.638 —
   різниця в межах шуму, на всіх трьох довжинах пачки. Ранг усередині
   ноги прибирає монету й режим, і те, що лишається, переноситься на
   інший місяць без втрат.
3. **Разом колонки сильніші за будь-яку одну.** 0.638 проти 0.586 при
   `W = 5`. Виграє саме поєднання протилежних знаків: `abs` каже «рух
   сильніший», `mean` — «знакове відношення менше», і разом вони кажуть
   більше, ніж кожна окремо.
4. **Уся сила — в модулі й кроці.** `abs`, `step`, `range`, `std` дають
   0.59 … 0.66; `straight`, `acf1`, `signs` не дають нічого (0.49 …
   0.52). Плавність ряду в зоні розвороту така сама, як у середині ноги
   — міняється тільки величина.
5. **Знак дивиться в інший бік.** `mean` стійко **нижче** 0.5 (0.401 …
   0.427) на всіх монетах і всіх довжинах: у зоні розвороту обʼєм і
   рівень частіше рухаються в різні боки. Це та сама пара «більше
   потоку — тонший стакан», що й у батьківському класі, тільки згорнута
   в одну колонку.
6. **Початок ноги видно краще за кінець.** 0.687 проти 0.619 у
   `step·d_up`, 0.688 проти 0.632 у `abs·d_up`. Тобто клас ловить
   **слід** розвороту, що вже стався, краще, ніж підхід до наступного —
   той самий висновок, що й на етапах 8 і 14, тепер числом на кожному
   кінці ноги окремо.
7. **Довша пачка допомагає мірі, але не моделі.** Найсильніша колонка
   росте з 0.603 (`W = 1`) до 0.660 (`W = 20`), а модель на val стоїть
   на 0.625 … 0.638. Годинне вікно робить кожну колонку рівнішою, але
   нової інформації не приносить.

## Етап 17. Клас210х10-1х7д/д: похідна на похідну — `підтверджено`

Розширення етапу 15. `клас210х10-1х2д` ділив обʼєм на свій рівень; тут до
цих двох фіч додані **чотири зворотні** — рівень на свій обʼєм — і
перенесена ціна.

### Клас

Повний опис і формули —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х10-1х7дд).
У кожному з девʼяти блоків:

```
d_up               = d_vol_buy    / d_resist
d_down             = d_vol_sell   / d_support
d_resist_buy       = d_resist     / d_vol_buy
d_bal_resist_buy   = d_bal_resist / d_vol_buy
d_support_sell     = d_support    / d_vol_sell
d_bal_support_sell = d_bal_support/ d_vol_sell
d_price_avg        — переноситься з батька як є
```

- Форма події `(9, 7)`. Перші дві фічі — рівно ті самі, що в
  `клас210х10-1х2д`, тому клас порівнюється з ним колонка в колонку.
- `д/д` в імені — похідна на похідну: чисельник і знаменник в однакових
  одиницях, тому перші шість фіч безрозмірні. Сьома, ціна, перенесена як
  є; в імені вона не позначається, бо клас названий по своєму головному
  правилу.
- `d_up` і `d_resist_buy` — **не одна фіча навпаки**. Обидві не обмежені,
  і кожна велика там, де малий її **власний** знаменник: перша ловить
  тонкий рівень, друга — застиглий обʼєм. Взаємно оберненими вони є лише
  там, де обидві живі.
- Блок з нульовим знаменником дає `NaN`. Тут він зʼявляється **вперше**:
  похідна обʼєму рівно нульова буває (блок без угод), похідна рівня — ні.

| Монета | `NaN` у класі | `NaN` у `клас210х10-1х7дц` | `NaN` у `клас210х10-1х2д` | блоків з нульовим знаменником |
| ------ | ------------- | -------------------------- | ------------------------- | ----------------------------- |
| `BTC_USDT` | 5 338 | 3 548 | 1 774 | 288 |
| `ETH_USDT` | 13 707 | 9 126 | 4 563 | 446 |
| `SOL_USDT` | 29 740 | 19 816 | 9 908 | 756 |
| `XRP_USDT` | 76 067 | 50 356 | 25 178 | 2 010 |
| `BNB_USDT` | 31 420 | 20 776 | 10 388 | 1 022 |
| `DOGE_USDT` | 271 110 | 179 574 | 89 787 | 18 028 |

Клас будується тим самим пайплайном, що й решта шести:

```bash
python3 rules/scripts/build_class210x10x7dc.py
```

Скрипт класу:
[`rules/scripts/class210x10m1x7dd.py`](../../rules/scripts/class210x10m1x7dd.py)
Датасет: `data/datasets/<PAIR>/class210x10m1x7dd/{train,val}.npz`,
розбиття те саме — 356 357 подій train і 46 368 val.

### Де він міряється

Клас стає четвертим у замірах етапів 13 і 14, другим у замірі зони
розвороту (етап 16) і отримує свій розділ карт наборів:

| Розділ вкладки | Скрипт | Що додалось |
| -------------- | ------ | ----------- |
| 9 — теплові карти | `feature_heatmap.py` | 2 картки: `W = 5` і `W = 20` |
| 10 — хід по нозі | `leg_track.py` | 2 картки на тих самих 579 і 423 ногах |
| 11 — зона розвороту | `reversal_zone.py` | 3 картки на тих самих ногах, 56 колонок |
| 12 — карта наборів | `umap_subsets.py --class x7dd` | 127 наборів × 2 довжини = 254 карти |

| `--class` | Клас | фіч | наборів | префікс |
| --------- | ---- | --- | ------- | ------- |
| `x7` | `клас210х10-1х7дц` | 7 | 127 | `umapf_` |
| `x3p1` | `клас210х10-1х3+1дц` | 4 | 15 | `umapg_` |
| `x2d` | `клас210х10-1х2д` | 2 | 3 | `umaph_` |
| `x7dd` | `клас210х10-1х7д/д` | 7 | 127 | `umapi_` |

```bash
python3 research/21-class210x10x7dc/scripts/feature_heatmap.py
python3 research/21-class210x10x7dc/scripts/leg_track.py
python3 research/21-class210x10x7dc/scripts/reversal_zone.py
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x7dd
```

Вихід: [`results/heatmap.json`](results/heatmap.json),
[`results/leg_track.json`](results/leg_track.json),
[`results/reversal_zone.json`](results/reversal_zone.json),
`results/umapi_w<W>_<набір>.json`,
[`results/umap_subsets7dd.json`](results/umap_subsets7dd.json).

### Результат: теплова карта

`AUC` пачки «розворот був» проти пачки без розвороту, медіана по шести
монетах, `W = 20`:

| Фіча | `mean` | `abs` | `range` | `std` | `step` | `straight` | `acf1` | `signs` |
| ---- | ------ | ----- | ------- | ----- | ------ | ---------- | ------ | ------- |
| `d_up` | 0.335 | 0.705 | 0.679 | 0.683 | 0.698 | 0.517 | 0.538 | 0.487 |
| `d_down` | 0.362 | 0.691 | 0.657 | 0.660 | 0.685 | 0.513 | 0.529 | 0.503 |
| `d_resist_buy` | 0.682 | **0.265** | 0.276 | 0.274 | 0.266 | 0.543 | 0.491 | 0.487 |
| `d_bal_resist_buy` | 0.601 | 0.270 | 0.291 | 0.288 | 0.272 | 0.554 | 0.481 | 0.359 |
| `d_support_sell` | 0.665 | 0.315 | 0.332 | 0.329 | 0.314 | 0.539 | 0.514 | 0.503 |
| `d_bal_support_sell` | 0.575 | 0.332 | 0.347 | 0.343 | 0.332 | 0.562 | 0.529 | 0.361 |
| `d_price_avg` | 0.486 | **0.720** | 0.694 | 0.715 | 0.717 | 0.466 | 0.520 | 0.545 |

На `W = 5` те саме: `abs·d_resist_buy` 0.249, `abs·d_price_avg` 0.728,
`abs·d_up` 0.689.

### Результат: хід по нозі

Середній ранг клітинки на сітці з 20 клітинок, розмах по нозі (`W = 20`):

| Колонка | розмах | початок | середина | кінець |
| ------- | ------ | ------- | -------- | ------ |
| `abs·d_up` | 5.80 | 13.3 | 8.6 | 14.4 |
| `step·d_price_avg` | 5.75 | 14.4 | 9.2 | 14.2 |
| `abs·d_price_avg` | 5.75 | 14.4 | 9.1 | 14.1 |

Форма та сама «миска», що й у решти класів: обидва кінці ноги вище
середини. Зворотні фічі малюють її дзеркально — `abs·d_resist_buy` дає
7.3 на початку, **11.8** у середині й 7.0 у кінці, тобто горб замість
миски. Це той самий факт, тільки перевернутий знаменником.

### Результат: зона розвороту

Ноги, зона й розбиття ті самі, що на етапі 16, колонок 56 замість 16.
`AUC` усередині ноги, медіана по шести монетах, `W = 20`:

| Колонка | зона | перші 5 % | останні 10 % |
| ------- | ---- | --------- | ------------ |
| `abs·d_resist_buy` | **0.334** | **0.249** | 0.349 |
| `step·d_resist_buy` | 0.340 | 0.236 | 0.364 |
| `range·d_resist_buy` | 0.343 | 0.261 | 0.388 |
| `step·d_up` | 0.660 | 0.687 | 0.619 |
| `abs·d_up` | 0.654 | 0.688 | 0.632 |

| `W` | модель train | модель val | краща одинока колонка на val |
| --- | ------------ | ---------- | ---------------------------- |
| 1 | 0.641 | **0.662** | 0.362 (`abs·d_resist_buy`) |
| 5 | 0.659 | **0.680** | 0.351 (`abs·d_resist_buy`) |
| 20 | 0.667 | **0.700** | 0.360 (`abs·d_resist_buy`) |

Проти `клас210х10-1х2д` на тих самих ногах і подіях: 0.629, 0.638, 0.625.

### Результат: карта наборів

127 наборів × дві довжини пачки = **254 карти** на одній вибірці з 6 000
пачок, метрика `cosine`. `NaN` відкидається на кожному наборі окремо, і
він одразу ділить фічі на дві групи:

| Набір з однієї фічі | подій, `W = 5` | подій, `W = 20` |
| ------------------- | -------------- | --------------- |
| `price_avg` | **6 000** | **6 000** |
| `up`, `resist_buy`, `bal_resist_buy` | 5 087 … 5 098 | 4 102 … 4 124 |
| `down`, `support_sell`, `bal_support_sell` | 4 683 … 4 696 | 3 401 … 3 417 |
| усі сім (`all`) | 4 411 | 3 129 |

Ціна — єдиний набір без втрат; будь-яка фіча стакана втрачає пачки там,
де блок стояв без угод. Сторона продажу втрачає більше за сторону
купівлі, і на годинній пачці різниця росте: 3 401 проти 4 102.

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

1. **Зворотне відношення сильніше за пряме.** `abs·d_resist_buy` дає
   0.265 у тепловій карті — це 0.235 від середини, більше, ніж 0.205 у
   `abs·d_up`. Ділити рівень на обʼєм вигідніше, ніж обʼєм на рівень:
   біля розвороту знаменник (`d_vol_buy`) росте швидше, ніж чисельник, і
   відношення падає різкіше, ніж росте пряме.
2. **Дві фічі — не одна навпаки.** Спірмен між `abs·d_up` і
   `abs·d_resist_buy` — **−0.87**, а не −1: там, де обидві живі, вони
   взаємно обернені, але живі вони не в тих самих блоках, і кожна
   велика зі своєї причини. Саме тому обидві лишились у класі.
3. **Модель виросла з 0.638 до 0.700.** На тих самих ногах val, тій
   самій зоні й тому самому seed. Приріст дають чотири зворотні фічі й
   ціна: у ваг `W = 20` найбільші — `abs·d_bal_support_sell` (−1.24),
   `step·d_bal_support_sell` (−0.75), `abs·d_bal_resist_buy` (−0.73) і
   `abs·d_price_avg` (+0.67).
4. **Перевчання немає.** train 0.641 … 0.667 нижче за val 0.662 … 0.700
   на всіх трьох довжинах пачки: ноги val коротші й чистіші, а ранг
   усередині ноги прибирає монету й режим.
5. **Довша пачка тепер допомагає.** У `клас210х10-1х2д` модель стояла на
   0.625 … 0.638 незалежно від `W`; тут вона росте 0.662 → 0.680 →
   0.700. Годинне вікно згладжує саме ті фічі, у яких знаменник рідко
   зникає, — а це зворотні.
6. **Ціна тягне свою частину.** `abs·d_price_avg` — найсильніша клітинка
   теплової карти (0.720), і в моделі вона з великою додатною вагою.
   Клас, на відміну від `клас210х10-1х2д`, її має, і це видно числом.
7. **Плавність, як і раніше, мовчить.** `straight`, `acf1`, `signs` —
   0.36 … 0.56 без стійкого боку. Єдиний виняток — `signs` зворотних
   збалансованих фіч (0.359 і 0.361): біля розвороту знак їх похідної
   тримається одного боку довше, ніж у решті ноги.

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

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

# 2. картки класів для вкладки «Класи датасету»
python3 rules/scripts/class_card.py --pair BTC_USDT

# 3. ноги зігзага 2.3 % на ема10 і десятини
python3 research/21-class210x10x7dc/scripts/leg_deciles.py

# 4. приклади класів з датасету
python3 research/21-class210x10x7dc/scripts/class_examples.py

# 5. карти UMAP: 4 класи × 3 метрики × 2 вимірності
python3 research/21-class210x10x7dc/scripts/run_umap7dc.py

# 6. події біля півотів проти решти подій ніг
python3 research/21-class210x10x7dc/scripts/pivot_compare.py

# 7. профіль фіч уздовж ноги
python3 research/21-class210x10x7dc/scripts/leg_profile.py

# 8. подія «розворот був»: пачки, вибір довжини, карти пачок
python3 research/21-class210x10x7dc/scripts/reversal_batches.py
python3 research/21-class210x10x7dc/scripts/run_umap_reversal.py

# 9. теплові карти, хід по нозі й карти наборів фіч
python3 research/21-class210x10x7dc/scripts/umap_subsets.py
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x3p1
python3 research/21-class210x10x7dc/scripts/umap_subsets.py --class x2d
python3 research/21-class210x10x7dc/scripts/feature_heatmap.py
python3 research/21-class210x10x7dc/scripts/leg_track.py
python3 research/21-class210x10x7dc/scripts/reversal_zone.py

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

Робоча копія `data/` має містити доби всіх шести монет за період
`2026-02-14` … `2026-09-01`.

## Результат

- Родина з чотирьох класів побудована, описана в правилах і покрита тестами.
  Датасети зібрані на всіх шести монетах: **356 357** подій train і **46 368**
  подій val із спільною межею `2026-08-14`.
- Перша похідна першого блоку — єдине місце, де подія дивиться назовні себе.
  Вона перетинає межу подій; на початку ряду й після розриву даних береться
  середнім трьох наступних. Таких подій **2 808** з 402 731 (0.7 %), і
  `клас210х10-1х7дц` прибирає їх вплив зовсім.
- `NaN` у `клас210х10х7` — це блоки без жодної угоди на одному з боків за 21
  секунду. Їх частка тримається ліквідності монети: від 2 142 значень у
  `BTC_USDT` до 108 706 у `DOGE_USDT`. Подія з `NaN` на карту не потрапляє —
  звідси 3 179 … 3 383 відкинутих із вибірки в 30 000.
- Карти всіх чотирьох класів побудовані трьома метриками у двох вимірностях
  на одній вибірці — вони порівнюються між собою і з когортами десятин ніг
  зігзага 2.3 % на ема10.
- Події `клас210х10-1х7дц` біля розвороту **відрізняються від решти ноги
  величиною похідних, а не їх знаком**: біля півота більше обʼєму
  (`|d_vol_buy|` — AUC у нозі 0.619) і тонший стакан на одиницю цього
  обʼєму (`|d_resist|` — 0.344). Дві величини повʼязані спільним
  знаменником, але кажуть різне: перша — про потік угод, друга — про
  щільність заявок на цей потік. Згортка зі знаком не розрізняє когорти
  зовсім (0.475 … 0.514).
- **Пачка подій підсилює різницю втричі.** На пачці з 20 подій (70 хвилин)
  `|d_resist|` дає 0.233, а `|d_vol_buy|` і `|d_price_avg|` — 0.756. Ефект
  росте монотонно по всіх шести довжинах пачки: різниця не в одній події,
  а в тому, як ринок поводиться протягом години навколо розвороту.
- **Плавність рядів майже не відрізняється** (0.49 … 0.51 на одній події,
  до 0.55 на пачці 20), а от **співвідношення фіч міняється**: біля
  розвороту рівні стакана й обʼєми звʼязані тісніше (+0.17 до кореляції),
  а звʼязок рівнів з ціною слабшає (−0.15). Найсильніша колонка дає 0.756
  — цього досить, щоб її бачити, і замало, щоб на ній розділяти.
- **Уся різниця сидить у першій десятині ноги.** Профіль по десятинах
  показує сходинку відразу після розвороту (ранг 7.78 з 10 у
  `|d_price_avg|` на пачці 20) і рівну смугу в решті ноги; кінець ноги
  піднятий ледь-ледь. Тобто фічі **реагують** на розворот, а не
  передвіщають його — окіл `±K` з етапу 7 ловив переважно бік після
  півота. Згортка зі знаком рівна й тут.
- **Хід фіч по нозі — чаша.** Рух найсильніший на обох кінцях ноги
  (ранг 14.4 з 20 на початку, 11.4 в кінці) і найспокійніший усередині
  (8.9); рівні стакана дають дзеркальну картину. Різниця між ногами
  росту й падіння живе тільки в перших 5 % ноги (`AUC` 0.671 на
  `|d_bal_support|`), далі зникає.
- **Теплова карта показує все одним поглядом:** сила сидить у двох
  стовпцях з восьми (`abs` і `step`), обʼєми й ціна дають `AUC` вище 0.5,
  рівні стакана — нижче, а згортання пар у `клас210х10-1х3+1дц` силу
  гасить (0.35 … 0.39 проти 0.69 … 0.73 в окремих обʼємів).
- **`клас210х10-1х3+1дц` згортає пари «опір проти підтримки» в одну фічу
  кожну**: `(|a| − |b|) / (|a| + |b|)`, три різниці плюс перенесена ціна,
  форма події `(9, 4)`. Згортання не рятує від `NaN` стакана —
  `b_resist_support` лишає рівно стільки ж пачок, скільки й окрема
  `d_resist`; зате обʼєми в парі втрачають лише 30 пачок з 6 000.
- **Карта будується по будь-якому набору фіч.** 127 наборів × дві довжини
  пачки = 254 карти на одній вибірці. Уже сам відсів `NaN` розділяє фічі
  на дві групи: обʼєми й ціна тримають усі 6 000 пачок, а кожна колонка
  стакана втрачає від 900 (`W = 5`) до 2 600 (`W = 20`) — блок з нульовим
  обʼємом псує саме їх.
- **Ряди класу видно напряму.** Розділ 6 ставить `ема10` ціни із зігзагом
  2.3 % над обраною фічею на спільній осі X: 12.5–14 тисяч точок на
  монету, усі півоти на своїх місцях, шкала Y рахується з видимого вікна.
- **Подія «розворот був» розмічена й розкладена по пачках.** Довжина
  пачки майже не впливає на те, наскільки її видно (сила 0.215 … 0.230 на
  всій сітці 3.5 хв … 4.7 год); стелю ставить `NaN`: живих пачок 89 % при
  `W = 1` і 29 % при `W = 80`, тому обрано `W = 5`. Нижній розворот від
  верхнього відділяється майже ідеально (`AUC` 0.997), але це **артефакт
  запізнення `ема10`**, а не знахідка про ринок.
- **Ноги вгору і ноги вниз дзеркальні.** У першу десятину провалюється
  саме та сторона стакана, яку щойно пробили: після нижнього півота —
  опір (`|d_bal_resist|` ранг 3.63 проти 4.91 на ногах вниз), після
  верхнього — підтримка (`|d_bal_support|` 3.70 проти 5.46). `AUC` між
  напрямками 0.673 і 0.362 на одній події, до 0.775 і 0.753 на пачці 20.
  Обʼєм і ціна майже симетричні; на пачці 20 `|d_vol_sell|` стає рівно
  симетричним (7.55 і 7.55), а перекіс у бік падіння тримає `|d_vol_buy|`
  (8.28 проти 7.15).

- **`клас210х10-1х2д` згортає кожну сторону книги в одну фічу**:
  `d_up = d_vol_buy / d_resist`, `d_down = d_vol_sell / d_support`, форма
  події `(9, 2)`, ціни немає. Під модулем дві фічі майже дублюють одна
  одну (Спірмен 0.89 … 0.92), сигнал не сильніший за окремі фічі батька
  (0.705 проти 0.728), а різниця напрямків слабшає з 0.671 до 0.353:
  ділення на рівень бачить, що стакан просів, але не бачить, яка сторона.

- **Зону розвороту в нозі — перші 5 % і останні 10 % — відрізнити від
  решти ноги можна, але слабко.** Найсильніша одинока колонка дає `AUC`
  0.660 усередині ноги, логістична регресія на всіх 16 колонках —
  **0.638 на свіжих ногах val** проти 0.635 на train, тобто без
  перевчання й без запасу. Сила сидить у модулі й кроці (`abs`, `step`,
  `range`, `std`), плавність не дає нічого, а знакове відношення
  дивиться в інший бік (0.40). Початок ноги видно краще за кінець
  (0.688 проти 0.632): клас ловить слід розвороту, що вже стався,
  краще, ніж підхід до наступного.

- **`клас210х10-1х7д/д` — та сама пара, поділена в обидва боки, плюс
  ціна.** Чотири зворотні фічі (рівень на свій обʼєм) виявились
  **сильнішими за прямі**: `abs·d_resist_buy` дає 0.265 у тепловій карті
  проти 0.705 у `abs·d_up` — 0.235 від середини проти 0.205, — а Спірмен
  між ними −0.87, тобто це не одна фіча навпаки. На зоні розвороту
  модель піднялась з **0.638 до 0.700** на тих самих ногах val, і на
  довшій пачці вона тепер росте, а не стоїть.

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

- [`rules/01-data-and-db.md`](../../rules/01-data-and-db.md) — опис класів
- [`rules/02-markup.md`](../../rules/02-markup.md) — зігзаг і розмітка
- [`rules/05-visualization.md`](../../rules/05-visualization.md) — UMAP і кольори
- [`research/19-leg-end-lstm/`](../19-leg-end-lstm/19-leg-end-lstm.md) — той
  самий зігзаг 2.3 % на ема10
