# Дослідження 22. Краї ноги на клас210х10-1х7д/д

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

## Ціль

Дослідження працює з **одним класом** — `клас210х10-1х7д/д`.

На ході фіч від півота до півота (дослідження 21, етап 17) сім фіч класу
розходяться саме **на краях ноги**: біля обох півотів розкид між ними на
всю висоту графіка, а від чверті ноги до чотирьох пʼятих криві злипаються
біля середнього рангу. Питання: **скільки подій брати з початку і з кінця
ноги**, щоб розкид усередині краю лишався великим, а подій у ньому
вистачало на стійке число.

Розкид тут — відстань **між сімома фічами в одному місці ноги**, а не
різниця початку з кінцем.

## Дані

- Датасет: `data/datasets/<PAIR>/class210x10m1x7dd/{train,val}.npz` —
  побудова описана в етапі 1.
- Ноги: зігзаг **2.3 %** на `ема10` ряду подій, як у дослідженні 21.
- Розбиття за датою спільне для всіх монет: `train` — до `2026-08-14`,
  `val` — `2026-08-14 … 2026-09-04`. Нога, що лежить по обидва боки межі,
  у замір не йде.

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

1. Клас `клас210х10-1х7д/д` і його побудова — етап 1.
2. Розкид фіч на краях ноги: чим задається край і яка довжина пачки —
   етап 2.
3. Зона півота: скільки в ній подій, як вона лягає на ряд і чи є в
   датасеті сильніші пачки — етап 3.
4. Чи відрізнить мережа пачку розвороту від решти датасету — етап 4.

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

Файл вкладки — цей. Будь-яка правка вкладки починається з нього і ним же
закінчується (`CLAUDE.md` розділ 5).

| Розділ | Блок | Ендпоінт | Скрипт |
| ------ | ---- | -------- | ------ |
| 1. Клас210х10-1х7д/д: як створити | `x7dd-class` | `/api/x7dd/class` | [`class_chain.py`](scripts/class_chain.py) |
| 2. Розкид фіч на краях ноги | `x7dd-spread` | `/api/x7dd/spread` | [`edge_spread.py`](scripts/edge_spread.py) |
| 3. Зона півота: скільки її і чи вона сильна | `x7dd-zone` | `/api/x7dd/zone`, `/api/x7dd/zone/series/<PAIR>` | [`pivot_zone.py`](scripts/pivot_zone.py) |
| 4. Чи відрізнить мережа пачку розвороту | `x7dd-net` | `/api/x7dd/net`, `/api/x7dd/net/umap` | [`pack_net.py`](scripts/pack_net.py) |

Розділи вантажаться незалежно один від одного: збій одного не гасить
решту. Усі плашки за замовчуванням згорнуті; ряд монети малюється при
розгортанні своєї плашки.

**Розділ 1.** Кнопки ведуть по ланцюгу справжніх класів проєкту —
`клас210х8` → `клас210х10х9` → `клас210х10х7` → `клас210х10х7дц` →
`клас210х10-1х7дц` → `клас210х10-1х7д/д`. Плашки: **«Формули»** (формула
й одиниця кожної фічі з картки класу), **«Приклад події»** (одна
випадкова подія, та сама на всіх кроках ланцюга), **«Побудова по
монетах»**.

**Розділ 2.** Кнопки: чим задається край (`у відсотках ноги`, `у
відсотках до попередньої ноги`, `у кількості подій`), довжина пачки
`W` і вибірка (`train` / `val`). Плашки: **«Розкид фіч у краю ноги»**
(розмір краю × згортка, у клітинці початок / кінець) і **«Обраний край
і довжина пачки»**.

**Розділ 3.** Плашка **«Скільки подій у зоні півота»**; кнопки монети й
«медіана / середнє»; плашки **«Сила пачки: <PAIR>»**, **«Чи є сильніші
десятки поза зоною»**, **«Те саме на сирих сумах»** — і по плашці на
кожну монету (**«<PAIR>: теплові карти й ряд»**): чотири теплові карти
на спільній шкалі, під ними ряд монети — ема10 із зігзагом і зоною
розвороту, а нижче три мітки на спільній осі X: пачка розвороту,
найсильніші пачки поза зоною (червоним) і пачки, сильніші за медіанну
пачку розвороту (синім). Ряд малюється стандартним рушієм полотен: колесо — масштаб
по часу, перетягування — зсув, подвійний клік — скинути, шкала Y
рахується з видимого вікна (`rules/05`).

Тести: [`tests/test_dashboard.py`](../../tests/test_dashboard.py) →
`TestX7ddClass`, `TestX7ddSpread`, `TestX7ddZone`, `TestX7ddMarkup`;
[`tests/test_x7dd.py`](../../tests/test_x7dd.py) → `TestClassChain`,
`TestEdgeSpread`, `TestPivotZone`.

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

Повний опис класу —
[`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#клас210х10-1х7дд).

### Звідки він росте

```
data/<рік>/<місяць>/<PAIR>/*.csv
    -> клас210х10х9        10 блоків по 21 с, середні 9 фіч блоку
    -> клас210х10х7        рівні стакана на обʼєм свого блоку
    -> клас210х10х7дц      перші похідні по блоках
    -> клас210х10-1х7дц    без першої плашки: тільки чисті похідні
    -> клас210х10-1х7д/д   шість відношень похідних плюс ціна
```

Похідна першого блоку переходить через межу подій, тому вона рахується на
**суцільному ряду монети**, до розбиття на вибірки. Клас цю плашку не
бачить взагалі: батько `клас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        — переноситься з батька як є
```

| Фіча | чисельник | знаменник | одиниця |
| ---- | --------- | --------- | ------- |
| `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` | — | — | приріст ціни за 21 с |

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

### Побудова

Одна команда на всі шість монет:

```bash
python3 rules/scripts/build_class210x10x7dc.py
python3 rules/scripts/class_card.py --pair BTC_USDT   # картка для вкладки
```

Скрипт класу:
[`rules/scripts/class210x10m1x7dd.py`](../../rules/scripts/class210x10m1x7dd.py)
Датасет: `data/datasets/<PAIR>/class210x10m1x7dd/{train,val}.npz`.

| Монета | подій | train | val | `NaN` у класі | `NaN` у батька | нульовий знаменник |
| ------ | ----- | ----- | --- | ------------- | -------------- | ------------------ |
| `BTC_USDT` | 68 966 | 61 235 | 7 731 | 5 338 | 3 548 | 288 |
| `ETH_USDT` | 69 770 | 62 043 | 7 727 | 13 707 | 9 126 | 446 |
| `SOL_USDT` | 69 263 | 61 535 | 7 728 | 29 740 | 19 816 | 756 |
| `XRP_USDT` | 62 580 | 54 853 | 7 727 | 76 067 | 50 356 | 2 010 |
| `BNB_USDT` | 69 567 | 61 839 | 7 728 | 31 420 | 20 776 | 1 022 |
| `DOGE_USDT` | 62 579 | 54 852 | 7 727 | 271 110 | 179 574 | 18 028 |

Разом 356 357 подій `train` і 46 368 `val`.

### Ланцюг з прикладом

Клас не стоїть сам по собі, тому розділ 1 вкладки
[`/x7dd`](http://localhost:8080/x7dd) веде **кнопками по всьому ланцюгу**
справжніх класів проєкту — тих самих, що на вкладці
[`/dataset`](http://localhost:8080/dataset):

| Клас | форма | фіч | приклад |
| ---- | ----- | --- | ------- |
| `клас210х8` | 210 × 8 | 8 | немає: у датасетах не зберігається |
| `клас210х10х9` | 10 × 9 | 9 | є |
| `клас210х10х7` | 10 × 7 | 7 | є |
| `клас210х10х7дц` | 10 × 7 | 7 | є |
| `клас210х10-1х7дц` | 9 × 7 | 7 | є, з другої плашки |
| `клас210х10-1х7д/д` | 9 × 7 | 7 | є, з другої плашки |

На кожному класі — плашка **«Формули»** (з картки класу, тобто з того
самого джерела, що й вкладка «Класи датасету») і плашка **«Приклад
події»**. Подія **одна на весь ланцюг**: береться випадкова подія без
`NaN` у кінцевому класі, і кожна ланка показує ту саму подію на своєму
кроці. Вибір відтворюваний — `--seed`.

```bash
python3 research/22-leg-edge-spread/scripts/class_chain.py
```

Скрипт: [`scripts/class_chain.py`](scripts/class_chain.py)
Вихід: [`results/class_chain.json`](results/class_chain.json)
Ендпоінт `/api/x7dd/class`, тести `TestX7ddClass`, `TestX7ddMarkup`,
[`tests/test_x7dd.py`](../../tests/test_x7dd.py) → `TestClassChain`.

## Етап 2. Розкид фіч на краях ноги — `підтверджено`

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

Кожна фіча зводиться в число згорткою (вісім згорток
[`pivot_compare.AGGREGATES`](../21-class210x10x7dc/scripts/pivot_compare.py)),
береться її **ранг серед подій своєї ноги**, приведений до `[0, 1]`, і
розкид у краю — це відстань між сімома фічами однієї згортки:

```
ранг(подія, колонка)  ранг значення серед подій своєї ноги, [0, 1]
r(фіча, край)         середній ранг фічі по подіях краю
розкид(край)          max(r) − min(r)   по семи фічах однієї згортки
```

`0` — усі сім фіч стоять в одному місці ноги, `1` — розведені по її
краях. Ранг усередині ноги прибирає монету, режим і масштаб фічі.

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

### Чим задається край

| Міра | Розмір краю | Розміри в гріді |
| ---- | ----------- | --------------- |
| `percent` | частка подій **своєї** ноги | 1, 2, 3, 5, 7, 10, 15, 20, 30, 40 % |
| `previous` | та сама частка, але від довжини **попередньої** ноги | ті самі |
| `events` | стільки самих подій | 1, 2, 3, 5, 8, 12, 20, 30, 50, 80 |

Початок і кінець рахуються нарізно. Край не може накрити більше за
половину ноги — інакше нога пропускається. Довжина пачки `W` — четверта
ручка того самого заміру: `1, 5, 10, 20, 40`.

**Робочий край** — найдовший, у якого розкид ще не впав нижче **95 %**
від найкращого на цій мірі. Край, коротший за **3 події**, у вибір не
йде: середній ранг по одній події — це її власний ранг, розкид у ньому
роздутий. У таблиці такий край лишається, у виборі — ні.

```bash
python3 research/22-leg-edge-spread/scripts/edge_spread.py
```

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

### Результат: розкид падає з довжиною краю

Згортка `abs`, `W = 20`, вибірка train, 593 ноги:

| Край | подій у краю | розкид початку | розкид кінця |
| ---- | ------------ | -------------- | ------------ |
| 1 % ноги | 5.3 | **0.698** | **0.655** |
| 3 % | 15.9 | 0.636 | 0.564 |
| 5 % | 26.4 | 0.588 | 0.500 |
| 10 % | 52.8 | 0.507 | 0.434 |
| 20 % | 105.6 | 0.367 | 0.324 |
| 40 % | 211.3 | 0.259 | 0.232 |

У подіях, та сама згортка й пачка:

| Край | розкид початку | розкид кінця |
| ---- | -------------- | ------------ |
| 3 події | **0.706** | **0.647** |
| 5 подій | 0.694 | 0.635 |
| 8 подій | 0.664 | 0.615 |
| 20 подій | 0.588 | 0.526 |
| 80 подій | 0.366 | 0.307 |

Крива падає **монотонно**: ширший край тягне в себе середину ноги, де
фічі злипаються. Плато тримається до 5–8 подій, далі спад помітний.

### Результат: робочий край

| Міра | початок | кінець | розкид | ніг |
| ---- | ------- | ------ | ------ | --- |
| `percent` | 1 % (5.3 події) | 1 % (5.3 події) | 0.698 / 0.655 | 593 |
| `previous` | 1 % (5.2 події) | 1 % (5.2 події) | 0.690 / 0.650 | 593 |
| `events` | **5 подій** | **8 подій** | 0.694 / 0.615 | 593 |

Три міри сходяться на одному: **край — це приблизно пʼять подій**, тобто
близько 17 хвилин ряду. У відсотках це 1 % ноги; більша частка вже
розмиває розкид, менша — не додає нічого, крім шуму.

`previous` не дає нічого нового проти `percent`, поки частка мала (0.690
проти 0.698), а на великих частках втрачає ноги: при 40 % їх лишається
348 з 593, бо край, порахований від довшої сусідки, не влазить у свою
ногу.

### Результат: довжина пачки

Розкид робочого краю на кожній `W` (згортка `abs`, train):

| `W` | час пачки | початок | кінець | середнє |
| --- | --------- | ------- | ------ | ------- |
| 1 | 3.5 хв | 0.602 | 0.606 | 0.604 |
| 5 | 18 хв | **0.710** | 0.656 | **0.683** |
| 10 | 35 хв | 0.689 | **0.665** | 0.677 |
| 20 | 1.2 год | 0.694 | 0.615 | 0.655 |
| 40 | 2.3 год | 0.576 | 0.476 | 0.526 |

**Оптимальна пачка — `W = 5`**, з рівним плато 5 … 20. Одна подія
(`W = 1`) фічі не розводить: у ряду з девʼяти блоків згортка ще шумна.
Дві години (`W = 40`) вже перетягує через півот — пачка накриває обидві
ноги, і край перестає бути краєм.

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

Той самий край на 87 ногах свіжого відрізка 14.08 … 04.09:

| `W` | край | train | val |
| --- | ---- | ----- | --- |
| 5 | 3 події | 0.710 / 0.668 | **0.681 / 0.659** |
| 5 | 5 подій | 0.669 / 0.656 | 0.632 / 0.644 |
| 20 | 3 події | 0.706 / 0.647 | **0.688 / 0.634** |
| 20 | 5 подій | 0.694 / 0.635 | 0.676 / 0.641 |

Розкид тримається поза часом навчання: втрата 0.02 … 0.04, тобто в межах
різниці між сусідніми розмірами краю.

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

1. **Край ноги — це пʼять подій, а не відсоток.** Три міри дали одне й те
   саме число, і в подіях воно не залежить від довжини ноги: 1 % ноги
   середньої довжини — це ті самі 5.3 події.
2. **Розкид падає монотонно.** Максимуму «десь усередині» немає: чим
   ширший край, тим більше в ньому середини ноги. Тому вибір — це не
   пошук піка, а компроміс «розкид проти кількості подій», і 5 подій
   тримають 0.69 з максимальних 0.71.
3. **Кінець ноги розводить фічі слабше за початок** — 0.62 … 0.66 проти
   0.69 … 0.71 на тому самому краю. Той самий висновок, що й у
   дослідженні 21: слід розвороту, що вже стався, видно краще, ніж
   підхід до наступного.
4. **Пачка має свій оптимум.** `W = 5` (18 хвилин) — найкраще; `W = 1` і
   `W = 40` гірші на 0.08 … 0.16. Пачка має бути довшим за подію, але
   коротшим за ногу.
5. **Розкид тримає тільки величина.** При 5 подіях і `W = 20`: `abs`
   0.694, `step` 0.678, `std` 0.672, `range` 0.622 — і `straight` 0.313,
   тобто **нижче** за випадкове. Плавність ходу на краю ноги фічі не
   розводить узагалі.
6. **Ноги не втрачаються.** На робочому краю в замір ідуть усі 593 ноги
   train і 87 val: край з пʼяти подій влазить навіть у найкоротшу ногу з
   двадцяти подій.

### Вкладка

Розділ 2 вкладки [`/x7dd`](http://localhost:8080/x7dd): кнопки міри,
довжини пачки й вибірки, плашка **«Розкид фіч у краю ноги»** (розмір
краю × згортка, у клітинці початок / кінець) і плашка **«Обраний край і
довжина пачки»**. Ендпоінт `/api/x7dd/spread`, тести `TestX7ddSpread`,
`TestX7ddMarkup`, [`tests/test_x7dd.py`](../../tests/test_x7dd.py) →
`TestEdgeSpread`.

## Етап 3. Зона півота: скільки її і чи вона сильна — `підтверджено`

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

### Скільки подій у зоні півота

Подій класу **402 725** на шести монетах (train 356 357, val 46 368),
півотів зігзага 2.3 % на ема10 — **701**.

| Зона | подій на півот | подій усього | частка класу | train | val |
| ---- | -------------- | ------------ | ------------ | ----- | --- |
| ±2 | 5 | 3 504 | 0.87 % | 3 035 | 469 |
| ±3 | 7 | 4 905 | 1.22 % | 4 249 | 656 |
| **±5** | **11** | **7 707** | **1.91 %** | 6 677 | 1 030 |
| ±10 | 21 | 14 692 | 3.65 % | 12 737 | 1 955 |
| ±20 | 41 | 28 601 | 7.10 % | 24 840 | 3 761 |

Числа менші за «півоти × подій на півот», бо в найкоротших ногах зони
сусідніх півотів перекриваються, і спільна подія порахована один раз.

### Показник пачки

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

```
A(пачка)[блок, фіча] = Σ |x[подія, блок, фіча]|      по десяти подіях
total(пачка)         = Σ A[блок, фіча]               по всіх клітинках
```

Пачка бере участь, тільки якщо всі десять подій ідуть підряд (210 секунд
між сусідніми) і в них немає жодного `NaN`: інакше суми рахувались би по
різній кількості чисел.

| Набір пачок | Що це | Пачок |
| ----------- | ----- | ----- |
| `pivot` | пачки розвороту, по одній на півот | 499 |
| `step` | увесь ряд, нарізаний підряд по десять подій | 23 967 |
| `scan` | ряд пачками по десять подій із кроком в одну подію | 239 576 |

**Скрізь поруч із середнім стоїть медіана, і за точку відліку береться
вона.** Фічі не обмежені: коли знаменник майже нульовий, значення
велике, і середнє по пачках злітає на одному викиді — у `step` воно
доходить до `1e19`, тобто ні про що не говорить.

```bash
python3 research/22-leg-edge-spread/scripts/pivot_zone.py
```

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

### Результат: пачка розвороту проти всього ряду

Медіанна сума модулів пачки розвороту, поділена на медіанну суму
десятки всього ряду. `> 1` — у зоні фіча більша, `< 1` — менша:

| Монета | `up` | `down` | `resist_buy` | `bal_resist_buy` | `support_sell` | `bal_support_sell` | `price_avg` |
| ------ | ---- | ------ | ------------ | ---------------- | -------------- | ------------------ | ----------- |
| `BTC_USDT` | ×3.74 | ×3.45 | ×0.06 | ×0.14 | ×0.06 | ×0.15 | ×1.58 |
| `ETH_USDT` | ×5.22 | ×4.78 | ×0.07 | ×0.15 | ×0.08 | ×0.17 | ×1.53 |
| `SOL_USDT` | ×2.58 | ×2.89 | ×0.13 | ×0.24 | ×0.15 | ×0.20 | ×1.45 |
| `XRP_USDT` | ×3.59 | ×3.33 | ×0.08 | ×0.14 | ×0.09 | ×0.12 | ×1.46 |
| `BNB_USDT` | ×2.25 | ×2.91 | ×0.20 | ×0.35 | ×0.30 | ×0.34 | ×1.52 |
| `DOGE_USDT` | ×2.94 | ×1.89 | ×0.17 | ×0.24 | ×0.09 | ×0.16 | ×1.25 |

Знак однаковий на всіх шести монетах: **прямі відношення в зоні більші в
2–5 разів, зворотні — менші в 3–17 разів, ціна більша в 1.3–1.6 раза.**

По плашках події картина рівна: на `BTC_USDT` `d_up` дає 0.115 … 0.153 на
кожній з девʼяти плашок пачки розвороту проти 0.029 … 0.032 на ряду —
різниця тримається від першої плашки до останньої, а не сидить в одній.

### Теплові карти: одна шкала на чотири набори

Щоб набори пачок можна було покласти на **одну шкалу**, значення беруться
відносні:

```
v[подія, фіча] = mean(|x[подія, блок, фіча]|) / база[фіча]
```

База — медіана `mean|x|` по подіях **звичайних десяток**: тих, що не
торкаються зони розвороту і йдуть без `NaN`. Тому `1.00` скрізь означає
«як у звичайній десятці цієї монети», а карта `rest` за побудовою стоїть
на одиниці (на `BTC_USDT` 0.94 … 1.09, на `DOGE_USDT` 0.78 … 1.40).

Карта — **10 позицій пачки × 7 фіч**, у клітинці медіана `v` по пачках
набору. Карт чотири, шкала спільна:

| Карта | Пачки | `BTC_USDT`, середнє по позиціях |
| ----- | ----- | ------------------------------- |
| `reversal` | пачки розвороту (82) | `up` 5.34, `down` 5.10, `price` 1.62, зворотні 0.08 … 0.22 |
| `rest` | десятки поза зоною (5 975) | усе 1.00 |
| `top` | верхній 1 % `rest` за сумою всіх фіч (60) | `resist_buy` 7.41, `support_sell` 8.53, `up` 0.49, `price` 0.71 |
| `top_direct` | верхній 1 % `rest` за прямими фічами (60) | `up` 30.1, `down` 26.2, `price` 2.26, зворотні 0.02 … 0.08 |

Карт найсильніших дві, бо сума по всіх семи фічах тягнеться зворотними
відношеннями, а вони в зоні розвороту якраз **менші**. `top` — це пачки,
де майже став обʼєм; `top_direct` — пачки, де фічі поводяться **як у
розвороті, тільки сильніше**.

Пачка розвороту на `BTC_USDT` по позиціях:

| Подія | `up` | `down` | `resist_buy` | `bal_resist_buy` | `support_sell` | `bal_support_sell` | `price_avg` |
| ----- | ---- | ------ | ------------ | ---------------- | -------------- | ------------------ | ----------- |
| 1 | 5.22 | 2.69 | 0.09 | 0.21 | 0.10 | 0.32 | 1.56 |
| 3 | 6.57 | 5.64 | 0.06 | 0.16 | 0.04 | 0.15 | 1.72 |
| **7 (півот)** | **9.07** | **8.92** | 0.03 | 0.13 | 0.08 | 0.19 | **2.04** |
| 8 | 3.66 | 4.29 | 0.07 | 0.21 | 0.09 | 0.25 | 1.56 |
| 10 | 3.66 | 4.31 | 0.11 | 0.24 | 0.10 | 0.21 | 1.43 |

Пік стоїть **на самій події півота** (сьома в десятці) і різко спадає
одразу після неї — те саме, що показував профіль позицій у розділі 2.

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

Головне питання розділу. Ряд проходиться пачками по десять подій із кроком в одну подію;
беруться тільки пачки, які **не торкаються** зони розвороту. «Сильніша» —
сума відносних модулів більша, ніж у медіанної пачки розвороту:

| Монета | десяток `rest` | пачок поза зоною | сильніших: усі фічі | частка | сильніших: прямі | частка | медіана розвороту | найкраща пачка розвороту | найкраще поза зоною |
| ------ | -------------- | ---------------- | ------------------- | ------ | ---------------- | ------ | ----------------- | ------------------------ | ------------------- |
| `BTC_USDT` | 5 975 | 59 823 | 9 014 | 15.1 % | 13 571 | 22.7 % | 345 | 16 169 | 766 306 |
| `ETH_USDT` | 5 196 | 51 879 | 24 223 | 46.7 % | 10 250 | 19.8 % | 398 | 120 909 | 10 582 969 |
| `SOL_USDT` | 4 033 | 40 331 | 13 692 | 33.9 % | 10 239 | 25.4 % | 216 | 65 493 | 12 702 739 |
| `XRP_USDT` | 2 293 | 22 693 | 5 109 | 22.5 % | 5 610 | 24.7 % | 258 | 158 234 | 160 448 |
| `BNB_USDT` | 4 140 | 41 493 | 566 | 1.4 % | 12 848 | 31.0 % | 177 | 379 959 | 2 739 844 |
| `DOGE_USDT` | 1 415 | 14 043 | 8 247 | 58.7 % | 4 775 | 34.0 % | 177 | 20 651 | 167 652 |
| **разом** | **22 952** | **230 262** | **60 851** | **26.4 %** | **57 293** | **24.9 %** | — | — | — |

**Так, сильніші десятки є.** По прямих фічах їх **24.9 %** — кожна
четверта непереривна десятка поза зоною сильніша за медіанна пачка
розвороту, і частка тримається на всіх шести монетах (19.8 … 34.0 %).
Найсильніша десятка поза зоною на `BTC_USDT` дає 766 306 проти 16 169 у
найсильнішої пачки розвороту — **у 47 разів більше**.

На сирих сумах (без нормування) картина та сама, але число стрибає від
1.4 % на `BNB_USDT` до 58.7 % на `DOGE_USDT`: сира сума міряє масштаб
фічі монети, а не поведінку.

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

На кожну монету — своя плашка. Зверху `ема10` ціни події із зігзагом
2.3 % і зоною розвороту точками, під ним на **спільній осі X** три
мітки, по полотну на кожну:

| Мітка | Колір | Що це |
| ----- | ----- | ----- |
| пачка розвороту | той самий, що на карті пачок | сім подій до півота разом з ним і три після |
| найсильніші пачки поза зоною | червоний | верхній 1 % пачок за прямими фічами |
| сильніші за пачку розвороту | синій | усі пачки поза зоною, чия сума прямих фіч більша за медіанну пачку розвороту |

На `BTC_USDT` це 920 подій розвороту, 600 подій найсильніших пачок і
22 547 подій пачок, сильніших за медіанну, — останніх у 24 рази більше,
ніж самої зони.

Зум і зсув спільні на всіх чотирьох полотнах, шкала Y на кожному своя і
рахується з видимого вікна. Унизу — шкала часу, за мишкою йде **пунктирне
перехрестя**: вертикаль наскрізь по всіх полотнах з часом події внизу,
горизонталь — у полотні під курсором зі значенням його шкали зліва. Train і val на одному полотні, межа
`2026-08-14` — вертикальна лінія. Ряд проріджений до 12 тисяч точок на монету (кожна 5-та подія), але
**події півотів і всіх трьох міток лишаються завжди** — інакше мітка
стояла б не там, де її порахували, а мітка, яку проріджують, виглядала б
вужчою за ту, яку ні. На `BTC_USDT` точок виходить 32 582 з 68 966.

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

1. **Зона розвороту — це два відсотки датасету.** ±5 подій навколо
   півота дають 7 707 подій з 402 725. Навіть ±20 подій — це 7 %.
2. **Прямі й зворотні відношення дивляться в різні боки.** У пачці
   розвороту `d_up` і `d_down` більші в 2–5 разів, а `d_resist_buy` і
   `d_support_sell` — менші в 3–17 разів. Це та сама пара «більше потоку
   на тонший рівень», тільки видно її як розрив у модулях, а не як `AUC`.
3. **Ціна росте помірно.** ×1.25 … ×1.58 — тобто зона розвороту це не
   просто «свічка більша»: рух ціни в ній лише в півтора раза сильніший
   за звичайний, тоді як стакан міняється в рази.
4. **Пік стоїть на самій події півота.** У пачці `−6 … +3` найбільші
   числа дає сьома позиція — сама подія розвороту (`up` 9.07, `price`
   2.04 на `BTC_USDT`), і одразу після неї спад удвічі.
5. **Сильніші десятки поза зоною є, і це кожна четверта.** По прямих
   фічах **24.9 %** непереривних десяток сильніші за медіанна пачка
   розвороту, стійко на всіх монетах (19.8 … 34.0 %). Найсильніша з них
   на `BTC_USDT` у 47 разів сильніша за найкращу пачку розвороту.
   Відповідь на питання розділу: **розворот не є найсильнішою десяткою
   ряду навіть близько**.
6. **Карта `top_direct` — це «розворот, тільки сильніше».** У ній `up`
   30.1, `down` 26.2, ціна 2.26 — той самий малюнок, що у пачці
   розвороту (5.34 / 5.10 / 1.62), лише вп'ятеро яскравіший, а півота
   там немає. Отже, самі величини модулів розворот **не визначають**:
   потрібне те, що відрізняє ці дві групи, а не сила руху.
7. **Карта `top` показує іншу річ.** Верхній відсоток за сумою всіх фіч —
   це пачки, де майже став обʼєм (`resist_buy` 7.41, `support_sell`
   8.53), а прямі фічі в них **нижчі за звичайні** (0.49 і 0.47). Це не
   розворот, а протилежний йому режим.
8. **Масштаб фіч не порівнюється між монетами.** На сирих сумах
   `BNB_USDT` дає 1.4 % сильніших пачок, `DOGE_USDT` — 58.7 %, і це не
   про ринок, а про те, що медіана відношень на кожній монеті своя. Тому
   всі порівняння в дослідженні — рангові або відносні, а не за самим
   значенням.

### Вкладка

Розділ 3 вкладки [`/x7dd`](http://localhost:8080/x7dd): таблиця зон,
кнопки монети й «медіана / середнє», плашка **«Сила пачки»** (сума по
фічах і матриця 9 × 7 по плашках, розворот проти ряду), плашка **«Чи є
сильніші десятки поза зоною»**, плашка з тим самим на сирих сумах — і
**по плашці на кожну монету**, де стоять усі чотири теплові карти на
спільній шкалі, а під ними ряд монети. Ряд малюється стандартним
рушієм полотен: колесо — масштаб по часу, перетягування — зсув,
подвійний клік — скинути, шкала Y рахується з видимого вікна часу. Ендпоінти
`/api/x7dd/zone` і `/api/x7dd/zone/series/<PAIR>`, тести `TestX7ddZone`,
`TestX7ddMarkup`, [`tests/test_x7dd.py`](../../tests/test_x7dd.py) →
`TestPivotZone`.

## Етап 4. Чи відрізнить мережа пачку розвороту — `підтверджено`, 2026-09-29

Етапи 2 і 3 міряли зону числом. Тут питання поставлене як задача
класифікації: **пачка розвороту проти будь-якої іншої десятки датасету**.

### Постановка

| Клас | Що це | Мітка | Пачок |
| ---- | ----- | ----- | ----- |
| розворот | сім подій до півота разом з ним і три після | `1` | 499 |
| решта | десятки поза зоною розвороту, без перекриття | `0` | 23 052 |

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

| Монета | пачок | розворотів | частка |
| ------ | ----- | ---------- | ------ |
| `BTC_USDT` | 6 057 | 82 | 1.35 % |
| `ETH_USDT` | 5 300 | 104 | 1.96 % |
| `SOL_USDT` | 4 140 | 107 | 2.58 % |
| `XRP_USDT` | 2 378 | 85 | 3.57 % |
| `BNB_USDT` | 4 205 | 65 | 1.55 % |
| `DOGE_USDT` | 1 471 | 56 | 3.81 % |
| **разом** | **23 551** | **499** | **2.12 %** |

**Вектор пачки — 630 чисел**: 10 подій × 9 плашок × 7 фіч, жодне число не
згортається. Фічі не обмежені й мають важкі хвости, тому кожна
переводиться в **квантиль свого ряду** (`x → 2u − 1`, межі `[-1, 1]`,
[`rules/scripts/scalers.py`](../../rules/scripts/scalers.py)). Скейлер
рахується на подіях `train` **своєї монети** й застосовується до `val` —
інакше val підглядав би у власний розподіл.

Розбиття за датою: train до `2026-08-14` — 20 903 пачки (437 розворотів,
2.09 %), val — 2 648 пачок (62 розвороти, 2.34 %).

| Модель | Що це |
| ------ | ----- |
| `logistic` | логістична регресія, `class_weight="balanced"` |
| `mlp` | персептрон 64, `relu`, `adam`, 60 епох; розвороти дублюються ×20 |

```bash
python3 research/22-leg-edge-spread/scripts/pack_net.py
```

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

### Результат: моделі

`AP` — площа під precision-recall; її точка відліку — частка розворотів у
вибірці, тому в дужках стоїть відношення до неї.

| Модель | Вибірка | AUC | AP | до бази |
| ------ | ------- | --- | -- | ------- |
| логістична | train | 0.909 | 0.109 | ×5.2 |
| логістична | **val** | **0.695** | **0.043** | ×1.8 |
| мережа | train | **1.000** | 1.000 | ×47.8 |
| мережа | **val** | **0.687** | **0.047** | ×2.0 |

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

UMAP на тих самих 630-вимірних векторах: усі 499 пачок розвороту й
випадкова решта до 6 000 точок, `cosine`, три виміри, seed 0. Пачки
розвороту **не збираються в свою область** — вони розсіяні по всій хмарі
разом з рештою датасету.

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

1. **Мережа розвороту не відрізняє.** `AUC` 0.687 на val проти 0.695 у
   логістичної регресії — персептрон не додав нічого. `AP` 0.047 при
   базі 0.023 означає: якщо взяти найпевніші пачки, розворотів серед них
   буде близько 5 % замість 2 % — це не детектор.
2. **Мережа вивчила train напамʼять.** train `AUC` 1.000 і `AP` 1.000
   проти 0.687 на val. 630 входів, 437 розворотів у train — параметрів
   більше, ніж прикладів, тому запамʼятати легше, ніж узагальнити.
3. **Регресія тримається рівно.** train 0.909 → val 0.695: падіння є, але
   не до монетки. Тобто слабкий сигнал у пачці **є** — той самий, що
   дав 0.700 в етапі 16 дослідження 21 і 24.9 % сильніших десяток в
   етапі 3.
4. **Стеля не в моделі, а в постановці.** Три різні підходи — ранги
   всередині ноги (0.700), сила модулів (кожна четверта десятка не
   слабша) і мережа на всіх 630 числах (0.687) — дають те саме: пачка
   розвороту не має власної підписи, яку видно з самої пачки.
5. **Карта це підтверджує.** Розвороти на UMAP не мають свого згустка:
   вони лежать серед звичайних десяток. Якби підпис існував, він би дав
   окрему область — як дають, наприклад, класи подій у дослідженні 21.

### Вкладка

Розділ 4 вкладки [`/x7dd`](http://localhost:8080/x7dd): плашки
**«Моделі: чи відрізняють»**, **«Пачки по монетах»** і **«Карта пачок:
розворот проти решти»** (3д UMAP, кнопки міток, точки розвороту більші й
поверх решти). Ендпоінти `/api/x7dd/net` і `/api/x7dd/net/umap`, тести
`TestX7ddNet`, [`tests/test_x7dd.py`](../../tests/test_x7dd.py) →
`TestPackNet`.

## Етап 5. Чи є в пачці розвороту щось, чого немає ні в кого — `підтверджено`, 2026-09-29

Етап 4 питав, чи навчиться модель. Тут питання жорсткіше і без моделі:
**чи існує в пачці розвороту ознака, значення якої немає в жодної іншої
десятки датасету**. Не «більше в середньому», а «є тільки тут».

### Постановка

Тільки `train` — усе до `2026-08-14`, по всіх шести монетах.

| Набір | Що це | Крок |
| ----- | ----- | ---- |
| `розворот` | сім подій до півота разом з ним і три після | по одній пачці на півот |
| `решта` | десятки, що **не торкаються** жодної зони розвороту | крок в **одну подію** |

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

| Монета | пачок | розворотів | частка |
| ------ | ----- | ---------- | ------ |
| `BTC_USDT` | 53 394 | 75 | 0.14 % |
| `ETH_USDT` | 46 652 | 96 | 0.21 % |
| `SOL_USDT` | 35 964 | 97 | 0.27 % |
| `XRP_USDT` | 20 005 | 62 | 0.31 % |
| `BNB_USDT` | 36 694 | 61 | 0.17 % |
| `DOGE_USDT` | 12 076 | 46 | 0.38 % |
| **разом** | **204 785** | **437** | **0.21 %** |

### 62 ознаки пачки

Подія зводиться до семи чисел на фічу — модуль по девʼяти плашках,
поділений на медіану звичайної десятки цієї монети (та сама база, що в
теплових картах етапу 3), — плюс знакове середнє і крайні плашки. З
десяти подій рахуються вісім ознак на кожну фічу і шість спільних:
`8 × 7 + 6 = 62`.

| Ознака | Що міряє |
| ------ | -------- |
| `mag_<фіча>` | медіана сили по десяти подіях |
| `max_<фіча>` | найсильніша подія пачки |
| `turn_<фіча>` | перекіс: сім перших подій проти трьох останніх |
| `arg_<фіча>` | позиція найсильнішої події, `0…9` |
| `peak_<фіча>` | у скільки разів пік вищий за медіану пачки |
| `trend_<фіча>` | нахил сили вздовж пачки |
| `flip_<фіча>` | зміна знака: середній знак перших семи проти трьох останніх |
| `edge_<фіча>` | остання плашка події проти першої |
| `spread` | розкид між сімома фічами, порядків |
| `direct` | сума сили прямих фіч: `d_up`, `d_down`, `d_price_avg` |
| `inverse` | сума сили чотирьох зворотних відношень |
| `ratio` | пряме проти зворотного |
| `updown` | наскільки `d_up` і `d_down` різні |
| `swap` | зміна ролей `d_up` / `d_down` між початком і хвостом |

`turn`, `flip`, `swap` і `arg` тут ключові: саме вони описують **перелом
усередині пачки**, а не її силу. Якщо розворот має власну підпис — вона
має бути в них.

### Чим міряється унікальність

| Число | Що означає |
| ----- | ---------- |
| `alone` | скільки розворотів лежать **за краєм усієї решти** — значення, якого більше ні в кого немає |
| `auc` | загальний поділ двох наборів |
| `prec_at_n` | частка розворотів у топ-`N`, де `N` = 437 |
| крива порога | точність, коли поріг затягується від повноти 50 % до 1 % |

Розвороти стоять у наборі першими, а `arg` і `flip` беруть мало різних
значень, тому перед топ-`N` набір **перемішується**: інакше топ діставався
б розворотам просто за порядком у масиві.

```bash
python3 research/22-leg-edge-spread/scripts/pack_unique.py
```

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

### Результат: унікального немає

`alone` по всіх 62 ознаках — **0 або 1**. І та одиниця — це щоразу
**одна й та сама пачка**: `BNB_USDT`, `2026-06-01 09:39:57`, яка вийшла
за край решти одразу десятьма ознаками. Одна з 437, 0.2 %. Решта 436
пачок розвороту цілком лежать усередині розкиду звичайних десяток.

| Ознака | AUC | бік розвороту | медіана розвороту | медіана решти | `alone` |
| ------ | --- | ------------- | ----------------- | ------------- | ------- |
| `ratio` | 0.744 | вище | 0.783 | −0.349 | 1 |
| `inverse` | 0.744 | нижче | 1.149 | 6.418 | 1 |
| `mag_d_resist_buy` | 0.741 | нижче | 0.154 | 1.202 | 1 |
| `direct` | 0.735 | вище | 8.114 | 2.937 | 1 |
| `mag_d_support_sell` | 0.732 | нижче | 0.142 | 1.222 | 1 |
| `mag_d_bal_resist_buy` | 0.729 | вище | 0.320 | 1.202 | 0 |
| `mag_d_down` | 0.728 | вище | 3.122 | 0.882 | 1 |
| `mag_d_up` | 0.725 | вище | 3.317 | 0.876 | 1 |
| `mag_d_bal_support_sell` | 0.717 | нижче | 0.282 | 1.234 | 1 |
| `max_d_price_avg` | 0.682 | вище | 2.634 | 1.810 | 0 |
| `spread` | 0.573 | вище | 1.740 | 1.525 | 1 |
| `turn_d_price_avg` | 0.564 | вище | 0.059 | 0.019 | 0 |
| `swap` | 0.507 | вище | −0.023 | 0.000 | 0 |
| `flip_d_price_avg` | 0.506 | вище | 0.476 | 0.476 | 0 |

Найкраща ознака — та сама сила, що й в етапі 3 (`AUC` 0.744). Ознаки
**перелому** — `flip`, `swap`, `turn` — дають 0.51–0.56, тобто майже
монетку: зміна знака всередині пачки в розвороті трапляється не частіше,
ніж будь-де.

### Результат: жорсткіший поріг не чистить хвіст

Найсильніша ознака `mag_d_bal_support_sell`, поріг затягується від
медіани розвороту до найкращого 1 %:

| Повнота | Розворотів | Чужих пачок | Точність | До бази |
| ------- | ---------- | ----------- | -------- | ------- |
| 50 % | 219 | 39 675 | 0.55 % | ×2.6 |
| 25 % | 110 | 15 864 | 0.69 % | ×3.2 |
| 10 % | 44 | 4 760 | 0.92 % | ×4.3 |
| 5 % | 22 | 2 360 | 0.92 % | ×4.3 |
| 2 % | 9 | 791 | 1.12 % | ×5.3 |
| 1 % | 5 | 440 | 1.12 % | ×5.3 |

Це і є відповідь. Якби в розвороту була **межа**, за нею точність росла
б до одиниці. Тут вона впирається в 1.1 % — на пʼять розворотів, які
пройшли найжорсткіший поріг, припадає **440 звичайних десяток**, що
пройшли той самий поріг. Хвіст решти влаштований так само, як хвіст
розворотів, тільки в ньому в 470 разів більше пачок.

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

Найточніша пара умов з `PAIR_TOP` найкращих одиночних (повнота не нижче
10 %):

| Умова | Повнота | Розворотів | Чужих | Точність |
| ----- | ------- | ---------- | ----- | -------- |
| `mag_d_support_sell ≤ 0.142` і `turn_d_price_avg ≥ 0.059` | 27 % | 119 | 15 244 | 0.77 % |
| `inverse ≤ 1.149` і `turn_d_price_avg ≥ 0.059` | 27 % | 117 | 15 913 | 0.73 % |
| `mag_d_bal_support_sell ≤ 0.282` і `mag_d_resist_buy ≤ 0.154` | 40 % | 177 | 25 136 | 0.70 % |

Дерева на всіх 62 ознаках з перевіркою **відкладеною монетою**
(`HistGradientBoosting`, навчання на пʼятьох монетах, оцінка на шостій):
`AUC` **0.727**, `AP` 0.0059 при базі 0.0021 (×2.8), у топ-437 —
1.1 % розворотів. `alone` моделі — **0**: жодна пачка розвороту не
дістала оцінки, вищої за всі 204 348 чужих.

Перевірка сусідом у 62-вимірному просторі (`z`-нормовані ознаки, 60 000
випадкових чужих пачок): медіана відстані до найближчого **свого**
розвороту — 4.65, до найближчої **чужої** пачки — 3.66. Чужа пачка
зазвичай **ближча**, ніж інший розворот; тільки 10 розворотів із 437
(2.3 %) не мають чужої пачки ближче за свою.

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

1. **Унікального немає.** Ні одна з 62 ознак, ні пара умов, ні дерева на
   всіх 62 не дали межі, за якою стоять лише розвороти. Єдина пачка за
   краєм решти — одна на `BNB_USDT`, і це викид, а не правило.
2. **Розворот — це зсув, а не інша природа.** Розподіли розвороту й решти
   збігаються формою, розворот лише зсунутий: прямі фічі сильніші,
   зворотні відношення слабші. Тому жорсткіший поріг не чистить хвіст —
   він відрізає обидва набори однаково.
3. **Перелому в пачці не видно.** `flip`, `swap`, `turn` — 0.51–0.56.
   Ознаки, які мали б спіймати саме зміну напрямку, не працюють:
   у полі класу півот нічим не позначений.
4. **Стеля та сама.** 0.727 тут, 0.695 у логістичної на 630 числах
   (етап 4), 0.700 рангами всередині ноги (дослідження 21, етап 16).
   Три різні постановки впираються в одне й те саме число.
5. **Сусід ставить крапку.** Розворот у просторі ознак оточений
   звичайними пачками щільніше, ніж іншими розворотами. Своєї області в
   них немає — є область, у якій їх трохи більше, ніж випадково.

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

```bash
# 1. датасети всіх шести монет (клас будується в цьому ж пайплайні)
python3 rules/scripts/build_class210x10x7dc.py

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

# 3. ланцюг класів з прикладом однієї події
python3 research/22-leg-edge-spread/scripts/class_chain.py

# 4. розкид фіч на краях ноги
python3 research/22-leg-edge-spread/scripts/edge_spread.py

# 5. зона півота: скільки її, сила пачки і ряди монет
python3 research/22-leg-edge-spread/scripts/pivot_zone.py

# 6. мережа на пачках і карта пачок
python3 research/22-leg-edge-spread/scripts/pack_net.py

# 7. пошук ознаки, якої немає в інших десяток
python3 research/22-leg-edge-spread/scripts/pack_unique.py

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

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

- [`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) — кольори
- [`research/21-class210x10x7dc/`](../21-class210x10x7dc/21-class210x10x7dc.md)
  — звідки взявся клас і хід фіч по нозі
