# Дослідження 01 — спостерігання подій: UMAP і графік ціни (train)

| | |
| --- | --- |
| Пара | BTC_USDT |
| Класи подій | `клас210х14х4` (етапи 1–5); усі збережені класи датасету (етап 6); ціна події `клас210х10х7` без його фіч (етапи 10–11) |
| Вибірка | `train` (етапи 1–9); повний ряд шести монет `train` + `val` (етапи 10–11) |
| Статус | етапи 1–11 підтверджені: 1–4 — 2026-08-17, 5 — 2026-08-20, 6–7 — 2026-08-23, 9–11 — 2026-09-09, 8 — 2026-09-29 |
| Правила | [`01-data-and-db.md`](../../rules/01-data-and-db.md), [`03-models.md`](../../rules/03-models.md), [`05-visualization.md`](../../rules/05-visualization.md) |

## Ціль

Подивитись на структуру подій: чи розпадається train-вибірка на окремі згустки
у просторі значень події, чи це одна суцільна хмара. Розвідка перед розміткою.

Починалось з `клас210х14х4` (60 значень). Етап 6 розширив погляд на **усі
збережені класи датасету** — від 3 до 90 значень на подію, кожен трьома
метриками відстані. Події в усіх класах однієї вибірки ті самі, різниця
тільки в тому, чим описана подія, тому карти прямо порівнюються між собою.

## Дані

| | |
| --- | --- |
| Вхід | `data/datasets/BTC_USDT/class210x14x4/train.npz` |
| Вхід (етап 6) | `train.npz` кожного збереженого класу датасету |
| Подій | 24 150 у кожному класі, ті самі події й той самий порядок |
| Період | 2026-02-14 … 2026-08-16 |
| Метадані (не фічі) | `timestamp`, `price_open`, `price_close` |

Класи цього дослідження, для яких будуються карти (етап 6):

| Клас | Форма події | Вектор | Спроєктовано | Фічі |
| ---- | ----------- | -----: | -----------: | ---- |
| `клас210х14х2` | `(15, 2)` | 30 | 24 150 | `balance`, `d_balance` |
| `клас210х14х3` | `(15, 3)` | 45 | 24 150 | `balance`, `d_balance`, `vol_balance` |
| `клас210х14х4` | `(15, 4)` | 60 | 24 150 | `resist`, `d_resist`, `support`, `d_support` |
| `клас210х14х4ц` | `(15, 4)` | 60 | 24 075 | три фічі `клас210х14х3` + ціна |
| `клас210х14х5` | `(15, 5)` | 75 | 24 150 | чотири фічі `клас210х14х4` + `vol_balance` |
| `клас210х210х3` | `(3,)` | 3 | 24 150 | `balance`, `d_balance`, `vol_balance` на всю подію |
| `клас210х210х4ц` | `(4,)` | 4 | 24 075 | три попередні + `price_balance` |
| `клас210ема4` | `(4,)` | 4 | 24 150 | ЕМА ціни і трьох фіч `клас210х210х3` |
| `клас210ема4ц` | `(4,)` | 4 | 24 075 | те саме, але ЕМА `price_balance` замість ЕМА ціни |
| `клас210х4бема10х10х130` | `(13, 4)` | 52 | 24 075 | чотири фічі `клас210х210х4ц` по 13 блоках ЕМА |

Подія з `NaN` хоч в одному значенні не проєктується — стільки, скільки
`meta.dropped_nan`: у класів із ціновою фічею це 75 подій із 24 150.

Клас на один блок (`клас210х210х3`, `клас210х210х4ц`, `клас210ема4`,
`клас210ема4ц`) зберігається як `(N, F)` — розгортати нічого, вектор події
вже готовий: 3 або 4 значення. Карта в такого класу все одно своя, бо UMAP
читає сусідство подій, а не форму профілю.

`клас210х8` у `data/datasets/` не зберігається, тому карти для нього немає
([`rules/01-data-and-db.md`](../../rules/01-data-and-db.md#класи-події)).

Решта класів датасету — `клас210х10х9`, `клас210х10х7`, `клас210х10х7дц`,
`клас210х10-1х7дц`, `клас210х10-1х3+1дц`, `клас210х10-1х2д`
і `клас210х10-1х7д/д` — будується
[своїм скриптом дослідження 21](../21-class210x10x7dc/21-class210x10x7dc.md)
на вибірці train усіх шести монет; на вкладку вони приходять із його теки.

`val` і `test` не використовуються.

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

| # | Етап | Статус |
| - | ---- | ------ |
| 1 | Розгортання події `(15, 4)` → вектор 60, фіча за фічею | **підтверджено** |
| 2 | z-score по кожному з 60 значень, статистики рахуються на train | **підтверджено** |
| 3 | UMAP → 2 виміри, зафіксовані параметри й seed | **підтверджено** |
| 4 | Три карти з різними метриками відстані: `cosine`, `euclidean`, `manhattan` | **підтверджено** |
| 5 | Графік ціни по подіях із зігзагом 2.3 і вкладеним 0.8 | **підтверджено** |
| 6 | Вкладка дашборду: кожен збережений клас датасету × три метрики | **підтверджено** |
| 7 | Перемикач вимірності над класами: юмап 2д і юмап 3д | **підтверджено** |
| 8 | Десять кнопок десятин ноги зігзага 2.3 і дві кнопки напрямку ноги: події обводяться кружечком, поруч — період і лічильники подій у ногах росту й падіння | підтверджено, 2026-09-29 |
| 9 | Лічильник ноги над кнопками: номер ноги вводиться числом або гортається стрілками, події ноги обводяться синім, ряд «частка ноги» 10 % … 100 % ріже її з початку; праворуч — події ноги на карті і скільки з них угору й вниз за ціною. Працює на кожному класі вкладки | **підтверджено** |
| 10 | Події зростання і падіння в нозі: чи повʼязаний рух ноги з подіями, з яких він складається — і чим саме, тим, що подій одного знака більше, чи тим, що вони більші | **підтверджено** |
| 11 | Ноги, де кількість або розмір подій іде проти напрямку ноги: скільки їх, чим вони роблять свій рух і де в нозі він стається | **підтверджено** |

Етапи 1–4 підтверджені 2026-08-17, етап 5 — 2026-08-20, етапи 6 і 7 —
2026-08-23, етапи 9, 10 і 11 — 2026-09-09.

## Скрипти

| Скрипт | Що робить | Вихід |
| ------ | --------- | ----- |
| [`scripts/run_umap.py`](scripts/run_umap.py) | UMAP-проєкція подій, клас обирається `--class` | `umap_train_<metric>[_<клас>].*` |
| [`scripts/build_chart.py`](scripts/build_chart.py) | ряд ціни по подіях + зігзаг 2.3 і вкладений 0.8 | `chart_train.json` |
| [`scripts/render_png.py`](scripts/render_png.py) | рендер карти `клас210х14х4`/`cosine` і графіка ціни у PNG (200 dpi) | `umap_train_cosine.png`, `chart_train.png` |
| [`scripts/price_heatmap.py`](scripts/price_heatmap.py) | ціна + хітмап подій під нею, спільна вісь часу | `price_heatmap.png` |
| [`scripts/price_heatmap_diff.py`](scripts/price_heatmap_diff.py) | різниці й нормовані метрики під ціною | `price_heatmap_diff.png` |
| [`scripts/leg_deciles.py`](scripts/leg_deciles.py) | десятина ноги зігзага 2.3 % для кожної події ряду карт | `deciles.json` |
| [`scripts/leg_index.py`](scripts/leg_index.py) | ноги обох тек карт одним переліком із наскрізними номерами | `legs_index.json` |
| [`scripts/leg_events.py`](scripts/leg_events.py) | події зростання й падіння в нозі, розклад суми їх рухів | `leg_events.json` |
| [`scripts/leg_mismatch.py`](scripts/leg_mismatch.py) | ноги, де кількість або розмір подій іде проти напрямку ноги | `leg_mismatch.json` |

```bash
python3 research/01-umap-class210x14x4/scripts/run_umap.py --all-metrics
python3 research/01-umap-class210x14x4/scripts/run_umap.py --all-classes  # етап 6
python3 research/01-umap-class210x14x4/scripts/run_umap.py \
    --all-classes --all-dims                                             # етап 7
python3 research/01-umap-class210x14x4/scripts/run_umap.py \
    --class class210ema4 --all-metrics --dims 3          # один клас, одна вимірність
python3 research/01-umap-class210x14x4/scripts/build_chart.py
python3 research/01-umap-class210x14x4/scripts/render_png.py
python3 research/01-umap-class210x14x4/scripts/leg_deciles.py     # етап 8
python3 research/01-umap-class210x14x4/scripts/leg_index.py       # етап 9
python3 research/01-umap-class210x14x4/scripts/leg_events.py      # етап 10
python3 research/01-umap-class210x14x4/scripts/leg_mismatch.py    # етап 11
```

### run_umap.py

1. **Розгортання.** `(N, B, F)` → `(N, B·F)` транспонуванням `(0, 2, 1)`.
   Значення однієї фічі лежать поруч: для `клас210х14х4` це `0..14` — `resist`,
   `15..29` — `d_resist`, `30..44` — `support`, `45..59` — `d_support`.
   Клас на один блок зберігається як `(N, F)` і йде далі як є — вектор події
   вже готовий. Клас обирається `--class`, за замовчуванням `class210x14x4`.
2. **z-score.** Середнє й σ рахуються на train. Нульова σ замінюється на 1.
3. **UMAP** з `n_neighbors=30`, `min_dist=0.1`, `random_state=0`. Карти на
   однакових даних і однакових параметрах, різниця тільки в метриці відстані
   (опис метрик — [`05-visualization.md`](../../rules/05-visualization.md#umap))
   і у вимірності проєкції: `--dims 2` (площина) або `--dims 3` (обʼєм).
4. **Зміна ціни за подію** — `(price_close - price_open) / price_open`,
   частками. Не фіча, лише колір точок.

### build_chart.py

Ряд ціни — `price_open` подій train у хронологічному порядку (той самий набір
подій, що йде в UMAP), 24 150 точок. По ньому будується зігзаг **2.3**
(поріг 2,3%) і всередині кожного руху — вкладений зігзаг **0.8**
(rules/02-markup.md). Реалізація — [`rules/scripts/zigzag.py`](../../rules/scripts/zigzag.py).

Результат: **116 півотів** зігзагу 2.3 і **472 півоти** вкладеного 0.8.

**Рішення (2026-08-20):** зігзаг рахується саме по train-ряду, а не по суцільній
ціні. Наслідок прийнятий свідомо: розбиття рендомне, тому train-ряд дірявий —
підряд (крок 210 с) ідуть лише 9 879 пар із 24 149, медіанний розрив між
сусідніми подіями 384 с, максимальний ~30 год. Півоти відповідають тому ряду,
який бачить модель, а не фактичним розворотам ціни.

### leg_deciles.py — десятина ноги для кнопок вкладки

Рецепт той самий, що в дослідженні 21
([`leg_deciles.py`](../21-class210x10x7dc/21-class210x10x7dc.md)), щоб кнопка
означала на всіх картах вкладки одне й те саме:

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

- **Нога** — від події після попереднього півота до самого півота включно.
- Ділиться на **десять рівних частин за кількістю подій**: `1` — перші 10 %
  подій ноги, `10` — останні 10 %. Місце міряється часом, не ціною.
- Напрямок ноги — за півотом, яким вона закінчується: `high` закриває ногу
  вгору, `low` — вниз. Когорта події: `0` поза ногами, `1…10` нога вгору,
  `11…20` нога вниз.
- Ряд — події **самої карти** в хронологічному порядку: час і ціна відкриття
  беруться з `results/umap_train_cosine.npz`, а не з датасету. Датасет
  перебудовується (розбиття рендомне, і після перебудови train інший), карта
  ж лишається тією, що на екрані, — кнопки мусять лягати на неї подія
  в подію. Ряд дірявий, і півоти відповідають саме йому — те саме рішення,
  що в `build_chart.py`.

Числа по train BTC_USDT (24 150 подій, 2026-02-14 … 2026-08-16):

| | |
| --- | --- |
| Півотів зігзага 2.3 на ема10 | 74 |
| Ніг | 73 — 36 вгору, 37 вниз |
| Довжина ноги, подій | найкоротша 69, медіана 292, найдовша 936 |
| Поза ногами | 210 подій |
| У когорті | від 1162 до 1231 подій |

Вихід: `results/deciles.json` — `t` і `g` подія за подією, перелік ніг
(`legs.list`: номер, напрямок, події, період, рух ціни) плюс звіт по
когортах; `results/deciles.meta.json` — той самий звіт без рядів.

### leg_index.py — ноги обох тек для лічильника

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

| Тека | Звідки ноги | Ніг |
| ---- | ----------- | --: |
| `research/01-umap-class210x14x4/results` | `deciles.json` → `legs.list` | 73 |
| `research/21-class210x10x7dc/results` | `deciles.npz` (когорта події) + півоти `legs_<PAIR>.json` | 695 |

- Ноги дослідження 21 збираються з когорт: напрямок змінився або десятина
  пішла назад — почалась нова нога; нуль обриває ногу. Кількість ніг кожної
  монети збігається зі звітом `legs_2_3.json` (91 · 125 · 136 · 123 · 93 · 127).
- Рух ноги там береться від півота до півота за ема10, як його й побудовано.
- Нумерація в межах теки наскрізна: монети йдуть у порядку `PAIRS`, ноги
  кожної монети — за часом. Тому номер ноги на вкладці однозначний навіть
  тоді, коли на карті шість монет.

Вихід: `results/legs_index.json` — `sources` по теці карти, у кожній
`pairs` і `legs`.

### price_heatmap.py

Ціна `price_avg` (крок 60 с для рендера) і під нею хітмап подій
`клас210х14х4` на **тій самій осі часу**: 4 смуги по 15 рядків (блоки по 14 с).
Колонка — година, значення усереднені по подіях цієї години.

```bash
python3 research/01-umap-class210x14x4/scripts/price_heatmap.py \
    --from 2026-05-01 --to 2026-08-21 --bucket 3600
```

Вікно 2026-05-01 … 2026-08-21: 113 днів, 20 575 подій, 2 700 колонок,
з них 608 порожніх (години без жодної події), у середньому 9.8 події
на колонку.

Шкали обрізані по 5-му і 95-му процентилю вікна — інакше викиди з'їдають
картину. `resist` і `support` не мають знаку → послідовна шкала від темного
до світлого. `d_resist` і `d_support` мають знак → зелене плюс, червоне мінус.

**Вікно виходить за відсічку основного датасету (2026-08-16).** Це вивід
на екран, не навчальні дані.

### price_heatmap_diff.py — різниці й нормовані метрики

**Підтверджено 2026-08-21.**

Вікно 2026-05-01 … 2026-08-21, **усі 20 575 подій вікна** (не тільки train) —
це оглядовий вивід, не навчальні дані. Вікно виходить за відсічку основного
датасету (2026-08-16).

Компоновка (rules/05-visualization.md):

| Рядок | Що |
| ----- | -- |
| ціна | `price_avg`, крок 60 с; зверху зігзаг 2.3 (56 півотів) і вкладений 0.8 (254), побудовані по ряду `price_open` подій |
| осцилятор 1 | `mean(vol_buy) / mean(vol_sell)`, логарифмічна шкала, база 1 |
| осцилятор 2 | `(vol_buy − vol_sell) / (vol_buy + vol_sell)`, межі [−1, 1] |
| смуга 1 | `resist − support` |
| смуга 2 | `(resist − support) / (resist + support)` |
| смуга 3 | `d_resist − d_support` |
| смуга 4 | `(d_resist − d_support) / (d_resist + d_support)` |

Різниці рахуються **поблочно**: значення блоку 14 с однієї фічі мінус значення
того самого блоку іншої, у тій самій події. Колонка хітмапа — година
(середнє по подіях години), 15 рядків — блоки по 14 с.

Числа по вікну:

| Метрика | Медіана | Додатних |
| ------- | ------- | -------- |
| `resist − support` | +51.6 | 55.7% |
| `(resist − support) / (resist + support)` | +0.0499 | 55.7% |
| `d_resist − d_support` | +24.1 | 53.5% |
| `(d_resist − d_support) / (d_resist + d_support)` | +0.0544 | 53.8% |
| `mean(vol_buy) / mean(vol_sell)` | 0.986 | 49.3% понад 1 |
| `(vol_buy − vol_sell) / (vol_buy + vol_sell)` | −0.0069 | 49.3% |

`(d_resist − d_support) / (d_resist + d_support)` не визначена там, де сума ≤ 0
(обидві величини мають знак) — 3.5% клітинок, вони порожні. У пари
`resist/support` знаменник завжди додатний.

Вивід: `results/price_heatmap_diff.png`.

### leg_events.py — події зростання і падіння в нозі (етап 10)

**Питання:** нога зігзага 2.3 % проходить свій рух подіями по 210 с. Чи
повʼязаний рух ноги з подіями, з яких він складається, і **чим саме** —
тим, що подій одного напрямку більше, чи тим, що вони більші за модулем.

Рахується на **повному ряді кожної монети** (`train` + `val` класу
`клас210х10х7`, 409 998 подій), а не на дірявому train дослідження 01:
питання про рух усередині ноги, тому ряд має бути суцільним. Ноги — той
самий рецепт, що на вкладці: ема10 ціни події, півоти зігзага 2.3 %.
Разом **712 ніг** (355 вгору, 357 вниз), медіана 416 подій
на ногу, медіана руху ноги 4.41 % за модулем.

| Величина | Формула |
| -------- | ------- |
| рух події | `r = ln(price_close / price_open)` |
| рух ноги | `ln(price_close останньої / price_open першої)` |
| розриви | рух ноги − сума рухів її подій |

Сума рухів подій точно розкладається на дві частини — обидві в тих самих
одиницях, тому їх видно поруч:

```
Σr = m·(n↑ − n↓)                 «скільки»  — подій одного знака більше
   + [n↑·(m↑ − m) − n↓·(m↓ − m)]  «які»     — вони ще й більші за модулем
```

`m↑`, `m↓` — середній модуль руху зростаючих і спадних подій ноги,
`m` — середній модуль по всій нозі.

#### Нога — це сума своїх подій

| | |
| --- | --- |
| Кореляція руху ноги із сумою рухів її подій | **+0.949** |
| Знак суми збігається зі знаком ноги | 0.993 |
| Сума рухів подій до руху ноги | 0.892 |
| На 217 ногах без розривів ряду | **0.999** |

Стик двох суміжних подій — одна секунда, рух там нульовий. Помітні розриви
дає лише те, що ряд подекуди рветься (пропуск у джерелі, подія з нульовим
обʼємом): медіана розривів по нозі 0.037 її руху, і лише
на 175 ногах з 712 вони зʼїли понад 10 %. На чистих ногах рух
ноги — це рівно сума рухів її подій.

#### Перевага в кількості подій є, але вона мала

| | Частка подій зростання |
| --- | ---: |
| нога вгору | **0.5195** |
| нога вниз | **0.4670** |
| у більшості подій знак ноги | 0.830 ніг |

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

#### Розмір подій важить більше за їх кількість

| Частина суми | Вага | Знак збігається з ногою |
| ------------ | ---: | ----------------------: |
| «скільки» — подій одного знака більше | 0.429 | 0.869 |
| «які» — вони більші за модулем | **0.571** | **0.935** |

| Кореляція руху ноги з | |
| --------------------- | ---: |
| часткою подій зростання | +0.534 (ранги +0.610) |
| частиною «скільки» | +0.720 |
| частиною «які» | **+0.842** |
| кількістю подій у нозі (рух за модулем) | +0.420 |

Середній модуль руху події, частками:

| Нога | зростання | падіння | перекіс |
| ---- | --------: | ------: | ------: |
| вгору | 0.001239 | 0.001059 | **+0.000180** |
| вниз | 0.001070 | 0.001236 | **−0.000167** |

У нозі вгору події зростання не тільки трохи частіші — вони ще й більші
на 0.018 % за подію; у нозі вниз усе дзеркально. Саме цей перекіс
розміру, а не перевага в кількості, тягне рух ноги: 57 % суми проти
43 %, і його знак збігається з напрямком ноги на 93.5 % ніг.

#### Де в нозі перевага сильніша

Частка подій зростання по десятинах ноги (1 — перші 10 % подій, 10 — останні):

| Нога | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| ---- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| вгору | 0.548 | 0.519 | 0.512 | 0.516 | 0.510 | 0.507 | 0.519 | 0.507 | 0.534 | 0.519 |
| вниз | 0.428 | 0.467 | 0.481 | 0.471 | 0.469 | 0.479 | 0.477 | 0.477 | 0.456 | 0.466 |

Найсильніша перевага — **у першій десятині**, одразу після півота
(0.548 у ногах угору проти 0.428 у ногах униз, розрив 12.0 п. п.
проти 3.7 п. п. у середині ноги). Далі вона майже рівна до кінця
ноги.

#### По монетах

| Монета | Ніг | ↑ у нозі вгору | ↑ у нозі вниз | Кореляція руху з часткою ↑ |
| ------ | --: | -------------: | ------------: | -------------------------: |
| `BTC_USDT` | 93 | 0.5194 | 0.4787 | +0.504 |
| `ETH_USDT` | 128 | 0.5245 | 0.4760 | +0.525 |
| `SOL_USDT` | 139 | 0.5141 | 0.4593 | +0.533 |
| `XRP_USDT` | 125 | 0.5180 | 0.4580 | +0.570 |
| `BNB_USDT` | 95 | 0.5251 | 0.4753 | +0.577 |
| `DOGE_USDT` | 132 | 0.5178 | 0.4602 | +0.553 |

Картина однакова на всіх шести монетах: перевага в кількості мала й того
самого порядку, кореляція руху з часткою подій зростання
+0.504 … +0.577.

#### Висновок етапу

1. **Рух ноги повʼязаний з її подіями повністю** — це та сама величина:
   на чистих ногах сума рухів подій дає 0.999 руху ноги, знак
   збігається на 99.3 % ніг. Питання не в тому, чи повʼязаний,
   а в тому, як перевага розподілена.
2. **Кількістю подій нога не пояснюється.** 0.519 проти 0.467 —
   перевага в 5.3 п. п. на сотні подій. По одній події напрямок
   ноги не читається.
3. **Тягне рух розмір подій:** у нозі вгору події зростання більші за події
   падіння, у нозі вниз — навпаки. Частина «які» дає 57 % суми і
   збігається з напрямком ноги на 93.5 % ніг, частина «скільки» —
   43 % і 86.9 %.
4. **Перевага найсильніша одразу після півота** — у першій десятині ноги,
   далі вона рівна.

Що це означає для мітки: ознака напрямку ноги має міряти **перекіс
розміру** подій (наскільки більші рухи одного знака), а не їх кількість,
і на вікні в десятки подій, бо на одній події перевага тоне в шумі.

### leg_mismatch.py — ноги, де події не збігаються з напрямком (етап 11)

Етап 10 показав, що перевага в нозі мала. Отже, є ноги, де її **немає**:
нога йде вгору, а подій падіння в ній більше, або події падіння в ній
більші. Етап бере саме такі ноги.

| Група | Означення |
| ----- | --------- |
| кількість незбіг | подій потрібного знака **менше**: `n↑ < n↓` у нозі вгору |
| розмір незбіг | частина «які» тягне **проти** ноги: події потрібного знака **менші** |
| обидві проти | і кількість, і розмір проти ноги |

Ряд, ноги й рух події — ті самі, що на етапі 10: повний ряд монети
(`train` + `val` класу `клас210х10х7`), ема10, зігзаг 2.3 %,
`r = ln(price_close / price_open)`. Усе, що «за ногою», рахується в її
бік, тому ноги вгору й вниз лягають в один ряд.

#### Скільки таких ніг

| | Розмір збіг | Розмір незбіг | Разом |
| --- | ---: | ---: | ---: |
| Кількість збіг | 576 | 43 | 619 |
| Кількість незбіг | 84 | **3** | 87 |
| Кількість порівну | 6 | 0 | 6 |

**87 ніг з 712** (12.2 %) пройшли свій рух, маючи менше подій
потрібного знака; **46** (6.5 %) — маючи менші події.
Обидва провали разом — лише 3 ноги.

#### Чим такі ноги роблять свій рух

| Група | Ніг | Подій (медіана) | Рух ноги | Перекіс розміру | Топ-10 подій | Топ-10 % подій |
| ----- | --: | --------------: | -------: | --------------: | -----------: | -------------: |
| кількість збіг | 619 | 392 | 4.45 % | +0.111 | 0.404 | 0.674 |
| кількість незбіг | 87 | 523 | 4.15 % | +0.193 | 0.621 | 1.063 |
| розмір збіг | 666 | 438 | 4.53 % | +0.130 | 0.457 | 0.722 |
| розмір незбіг | 46 | 272 | 2.96 % | −0.041 | 0.127 | 0.231 |

- **Перекіс розміру** — `(m за ногою − m проти ноги) / m`, наскільки події
  в бік ноги більші за протилежні.
- **Топ-10 подій** — яку частку чистого руху ноги дають її десять
  найбільших за модулем подій; **топ-10 %** — те саме для 10 % найбільших.

Читається так:

1. **Нога без переваги в кількості бере розміром — і бере його
   стрибками.** Перекіс розміру там 0.193 проти 0.111 у звичайної ноги —
   майже вдвічі більший. Десять найбільших подій дають 62.1 % її руху
   проти 40.4 %, а 10 % найбільших — 106.3 %, тобто **більше за весь
   рух ноги**: решта 90 % подій у сумі тягне назад. Такі ноги
   ще й довші: медіана 523 подій проти 392.
2. **Нога без переваги в розмірі бере кількістю — і бере її потроху.**
   Перекіс −0.041 (тобто події проти ноги там **більші**), топ-10 подій
   дають лише 12.7 % руху, топ-10 % — 23.1 %. Рух складається з багатьох
   дрібних подій потрібного знака. Це найслабші ноги: медіана руху
   2.96 % проти 4.53 %, і найкоротші: 272 подій проти 438.
3. **Обидві частини проти ноги — це діри в ряді, а не ринок.** Усі 3 ноги
   мають суму рухів подій **проти** свого напрямку, а рух прийшов
   із розривів: медіана пропущеного ряду в них 59.4 год проти 0.1 год
   у звичайної ноги. Розриви там 1.4 руху ноги — більше, ніж сам рух.

#### Де в нозі стається рух

Частка руху групи по десятинах ноги (1 — перші 10 % подій, 10 — останні):

| Група | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| ----- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| кількість збіг | +0.182 | +0.094 | +0.076 | +0.067 | +0.059 | +0.063 | +0.072 | +0.056 | +0.139 | +0.192 |
| кількість незбіг | +0.266 | +0.075 | +0.059 | +0.065 | +0.061 | +0.005 | +0.061 | +0.076 | +0.126 | +0.206 |
| розмір незбіг | +0.249 | +0.127 | -0.028 | +0.090 | +0.048 | +0.071 | +0.039 | +0.025 | +0.222 | +0.157 |

Форма та сама в усіх групах: рух ноги збирається на **краях** — у першій
і в останній десятині, найменше в середині. У ніг без переваги в кількості
перший стрибок ще різкіший: 26.6 % руху в першій десятині проти
18.2 %.

#### По монетах

| Монета | Кількість збіг | Незбіг | Порівну | Розмір збіг | Незбіг |
| ------ | --: | --: | --: | --: | --: |
| `BTC_USDT` | 81 | 12 | 0 | 87 | 6 |
| `ETH_USDT` | 113 | 14 | 1 | 123 | 5 |
| `SOL_USDT` | 121 | 18 | 0 | 131 | 8 |
| `XRP_USDT` | 103 | 21 | 1 | 116 | 9 |
| `BNB_USDT` | 86 | 7 | 2 | 89 | 6 |
| `DOGE_USDT` | 115 | 15 | 2 | 120 | 12 |

Незбіг за кількістю трапляється на кожній монеті приблизно однаково —
від 7 % до 17 % ніг монети.

#### Висновок етапу

- Незбіг за кількістю — не рідкість (12 % ніг) і не збій: такі ноги
  роблять свій рух **кількома великими подіями** проти більшості дрібних
  протилежних. Мітка, що рахує події одного знака, на них помиляється,
  мітка, що міряє перекіс розміру, — ні.
- Незбіг за розміром — рідший (6 %) і трапляється на **слабких коротких
  ногах**, які ледве набрали свої 2.3 %: там рух зібраний потроху,
  великих подій немає.
- Ноги, де проти напрямку йде і кількість, і розмір, у ринку не існують:
  усі 3 такі ноги стоять на дірах у джерелі (десятки годин пропуску).
  Це причина фільтрувати ноги з розривами, а не окремий вид руху.

## Приклади

Вхід (подія `x[0]`, перші 3 з 15 рядків):

```
        resist  d_resist  support  d_support
  0     96.949    77.479  344.784      6.953
  1     40.361    -1.915  708.610    750.223
  2     52.243     3.274  541.714    513.797
```

Вихід — перша точка `results/umap_train_euclidean.json`:

```json
{"x": -11.319, "y": 10.46, "t": 1771092104, "d": 0.00115}
```

Та сама подія на карті іншого класу
(`results/umap_train_cosine_class210x14x3.json`):

```json
{"x": 8.052, "y": 5.231, "t": 1771092104, "d": 0.00115}
```

`x`, `y` — координати UMAP; `t` — `timestamp` першої секунди події (Unix, UTC);
`d` — зміна ціни за подію, частками. `t` і `d` в усіх класах однакові —
рухається лише місце точки.

## Результат

[`results/`](results/):

| Файл | Що всередині |
| ---- | ------------ |
| `umap_train_<metric>.npz` | `coords (24150, 2)`, `timestamp`, `price_open`, `price_close`, `price_change`, `feature_mean`, `feature_std` — `клас210х14х4` |
| `umap_train_<metric>.json` | те саме для дашборду |
| `umap_train_<metric>.meta.json` | параметри прогону: клас, розмір вектора, фічі, метрика, seed |
| `umap_train_<metric>_<клас>.*` | те саме для решти класів (етап 6) — 27 карт |
| `umap3d_train_<metric>[_<клас>].*` | обʼємні карти (етап 7) — 30 карт, `coords (N, 3)` |
| `chart_train.json` | ряд ціни по подіях, півоти 2.3 і 0.8 |
| `deciles.json`, `deciles.meta.json` | десятина ноги 2.3 для кожної події ряду карт і перелік ніг (етап 8) |
| `legs_index.json` | ноги обох тек карт із наскрізними номерами (етап 9) |
| `leg_events.json`, `leg_events.meta.json` | нога за ногою: події зростання й падіння і розклад суми їх рухів (етап 10) |
| `leg_mismatch.json`, `leg_mismatch.meta.json` | ноги, де кількість або розмір подій іде проти напрямку (етап 11) |
| `umap_train_cosine.png`, `chart_train.png` | одобрені виводи, 200 dpi |
| `price_heatmap.png`, `price_heatmap_diff.png` | ціна з хітмапом подій, + `.meta.json` |

**Шістдесят карт**: 2 вимірності × 10 класів × 3 метрики, по 24 150 точок
у кожній (24 075 у класів із ціновою фічею), усі з `n_neighbors 30`,
`min_dist 0.1`, `random_state 0`. Час прогону 15–40 с на карту.

Зміна ціни за подію, однакова для всіх карт: від −1.132% до +2.656%, σ = 0.123%.

Тести: `python3 -m unittest discover -s tests -t tests`
(`tests/test_research_umap.py`, `tests/test_dashboard.py`).

## Вкладка дашборду (етап 6, підтверджено 2026-08-23)

Власний URL [`/umap`](http://localhost:8080/umap), дані з `results/` через
`/api/umap` (перелік карт) і `/api/umap/<файл>` (точки однієї карти).

**Карти всіх збережених класів датасету: дві вимірності × клас × три
метрики.** Перелік іде по реєстру карт у `dashboard/app.py`, а не по списку
імен: класи цього дослідження (`UMAP_CLASSES`, десять), класи вкладки
дослідження 21 (`X7DC_CLASSES`, чотири) і класи, добудовані в дослідженні 21
пізніше (`X7DD_CLASSES`, три) — **сімнадцять класів, разом 102 карти**.
Це всі класи датасету, які зберігаються в `data/datasets/`.
Три метрики на кожен клас — вимога
[`rules/05-visualization.md`](../../rules/05-visualization.md#umap):
`cosine` читає форму профілю, `euclidean` і `manhattan` показують, наскільки
картина тримається величин, а не форми. За замовчуванням відкривається
`юмап 2д` / `клас210х14х4` / `cosine`.

### Правило вкладки: перемикачі й порядок

Три перемикачі, **строго в цьому порядку зверху вниз**:

| # | Перемикач | Значення |
| - | --------- | -------- |
| 1 | **вимірність** | `юмап 2д` · `юмап 3д` |
| 2 | клас | усі збережені класи датасету, у порядку реєстру карт |
| 3 | метрика | `cosine` · `euclidean` · `manhattan` |

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

### Правило вкладки: лічильник ноги і частка ноги

**Над кнопками десятин стоїть лічильник ноги** — він веде ноги зігзага
2.3 % по черзі, одну за одною.

| | |
| --- | --- |
| Номер | вводиться числом у поле або гортається стрілками `◀` і `▶` |
| Клавіші | `←` і `→` у полі номера роблять те саме, що стрілки |
| `0` | ноги не обрано, лічильник мовчить |
| Межі | від `1` до кількості ніг цієї теки; гортання йде по колу |
| Обвідка | події ноги обводяться **синім** (`#4c8dff`, на світлій темі `#1f5fd0`) |

- Синій не збігається з кольорами десятин і напрямків, тому нога видно
  на будь-якій карті.
- **Обрана нога звужує все інше**: поки вона обрана, на карті світиться
  тільки вона — кнопки десятин і напрямку мовчать. Знято ногу — вони
  працюють як раніше.
- Колір точки, як і скрізь на вкладці, лишається зміною ціни за подію.
- Наведення на точку показує номер її ноги поряд із десятиною.

**Другий рядок — частка ноги: десять кнопок `10 %` … `100 %`.** Вони ріжуть
ногу **з початку**: `10 %` — перші 10 % її подій, `100 %` — уся нога.
Кнопки взаємовиключні, за замовчуванням стоїть `100 %` — уся нога.
Частка міряється тією самою десятиною, що й кнопки нижче: обводиться
подія, чия десятина не більша за обрану межу.

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

**Лічильник працює на кожному класі вкладки.** Ноги приходять
з `/api/umap/legs` → `results/legs_index.json`, розкладені по теці карти:
73 ноги ряду дослідження 01 і 695 ніг шести монет дослідження 21.
Ногу події шукають за її часом (у картах дослідження 21 — ще й за монетою
точки, `c`), тому карті не потрібен зайвий стовпчик.

### Правило вкладки: кнопки десятин і напрямку ноги

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

| | |
| --- | --- |
| Кнопок | 10: `1` … `10` — місце події в нозі |
| `1` | перші 10 % подій ноги |
| `10` | останні 10 % подій ноги, разом із самим півотом |
| Вибір | кнопки незалежні, вмикається скільки завгодно; спершу жодної |
| Колір обвідки | колір десятини (той самий ряд, що на вкладці `/x7dc`) |
| Колір точки | не міняється — це зміна ціни за подію (rules/05) |

- **Ноги вгору і вниз рахуються разом.** Десятина міряє місце в нозі,
  а не її напрямок: когорти `1…10` (нога вгору) і `11…20` (нога вниз)
  зводяться в одну десятину `1…10`.
- Подія поза ногами (до першого півота і після останнього) не обводиться
  ніколи.
- Натиск кнопки **не перезавантажує точки** — карта перемальовується
  на місці, обвідка малюється після всіх точок, щоб її не затирали сусіди.
- Наведення на точку показує напрямок ноги і десятину поряд із часом
  і зміною ціни.

**Другий рядок — дві кнопки напрямку ноги: «вгору» і «вниз».** Вони
обводять усі події ноги свого напрямку кольором напрямку (нога вгору
зелена, нога вниз синя — той самий ряд, що на `/leg23` і `/x7dc`).

| Обрано | Що обводиться | Колір обвідки |
| ------ | ------------- | ------------- |
| тільки десятини | ці десятини в обох напрямках | колір десятини |
| тільки напрямок | уся нога цього напрямку | колір напрямку |
| і те, і те | **перетин** — ці десятини лише в цьому напрямку | колір десятини |
| нічого | нічого не обводиться | — |

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

Для карт дослідження 01 (train `BTC_USDT`, 2026-02-14 … 2026-08-16,
24 150 подій) цей рядок показує: у ногах росту **11 783**, у ногах
падіння **12 157**, поза ногами **210**. Ніг вгору 36,
ніг вниз 37 — ноги падіння тут довші в подіях, хоча самих ніг
майже порівну.

Звідки береться десятина:

| Карти | Джерело |
| ----- | ------- |
| дослідження 21 (сім класів на 10 блоків) | когорта лежить у самій точці (`g`) |
| дослідження 01 (десять класів) | `/api/umap/deciles` → `results/deciles.json`, зводиться за `timestamp` події |

Звідки береться нога лічильника:

| Карти | Джерело |
| ----- | ------- |
| усі класи вкладки | `/api/umap/legs` → `results/legs_index.json`, нога шукається за часом події і монетою точки |

Обидва джерела рахують десятини одним рецептом — ема10 ціни події,
зігзаг 2.3 %, десять рівних частин за кількістю подій
(`leg_deciles.py`). Вибірки різні (train BTC_USDT проти 30 000 подій
train шести монет), тому збігаються правила, а не самі події.

### Правило вкладки: звідки беруться карти

Карта знає свою теку. Класи цього дослідження лежать у
`research/01-umap-class210x14x4/results/`; сім класів на 10 блоків —
`клас210х10х9`, `клас210х10х7`, `клас210х10х7дц`, `клас210х10-1х7дц`,
`клас210х10-1х3+1дц`, `клас210х10-1х2д` і `клас210х10-1х7д/д` — будуються своїм
скриптом дослідження 21 і лежать у
[`research/21-class210x10x7dc/results/`](../21-class210x10x7dc/21-class210x10x7dc.md).

- Імена файлів однакові в обох теках (`umap_train_<metric>_<клас>`), тому
  вкладка читає їх одним і тим самим кодом; тека береться з реєстру карт.
- У теці дослідження 21 лежать карти семи класів, а на його власній вкладці
  [`/x7dc`](http://localhost:8080/x7dc) показані чотири: `/api/x7dc/umap`
  звужує перелік і текою, і своїми класами. Три пізніші класи
  (`клас210х10-1х3+1дц`, `клас210х10-1х2д`, `клас210х10-1х7д/д`) видно
  тільки тут.
- **Вибірка своя на дослідження.** Тут це `train` `BTC_USDT` цілком; у
  дослідженні 21 — 30 000 подій `train` усіх шести монет, `seed 0`. Тому
  кількість точок звіряється в межах теки, а не між ними.
- Новий клас додається рядком у свій перелік класів — перемикач і тести
  підхоплюють його самі.

### Правило вкладки: чим 3д відрізняється від 2д

| | `юмап 2д` | `юмап 3д` |
| --- | --- | --- |
| Проєкція | `n_components=2` | `n_components=3` |
| Файли | `umap_train_<metric>[_<клас>].*` | `umap3d_train_<metric>[_<клас>].*` |
| Точка | `{x, y, t, d}` | `{x, y, z, t, d}` |
| Перетягування | зсув | **поворот** навколо центру хмари |
| Порядок малювання | перемішано (seed 0) | **за глибиною**, від дальніх до ближніх |
| Каркас | немає | куб меж хмари — видно, як її повернуто |

- В обʼємі порядок малювання **не перемішується**: ближча точка має бути
  зверху, інакше глибина читається неправильно. Це свідоме відхилення від
  практики 2д, де перемішування рятує від того, що один клас лягає поверх
  решти.
- Хмара нормується в куб зі **збереженням пропорцій**: ділиться на найдовшу
  з трьох осей, тому жодна вісь не розтягується.
- Насиченість точки додатково слабшає з глибиною — дальні бліді, ближні
  щільні. Колір при цьому лишається кольором зміни ціни.
- Колесо — масштаб, перетягування — поворот, подвійний клік — початковий вид.
  У підписі під картою — поточні кути повороту в градусах.

Карти одного дослідження побудовані **однаковими параметрами й одним seed**
(`n_neighbors 30`, `min_dist 0.1`, `random_state 0`), інакше карти не
порівнюються між собою. Відрізняється тільки вхід — 30, 45, 60 або 75 значень
на подію — і вимірність виходу. Події, їх порядок і зміна ціни за подію в усіх
класах ті самі, тому колір точки на всіх картах однаковий — рухається лише
її місце.

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

- Точка = подія на своєму місці проєкції, без зсувів.
- Колір — зміна ціни за подію: зростання зелене, падіння червоне,
  насиченість — величина; повний колір на максимумі за модулем по вибірці.
- Порядок малювання перемішано (seed 0), щоб жоден колір не лягав поверх решти.
- Наведення показує час події (UTC), зміну ціни й координати проєкції.
- Полотно рендериться з урахуванням `devicePixelRatio`; межі осей рахуються
  з самих координат, фіксованих меж немає.
- У легенді сказано, що осі UMAP власних значень не мають і відстані на карті
  не пропорційні відстаням у вихідному просторі.

Імена файлів: `umap_train_<metric>_<клас>.{npz,json,meta.json}` для 2д
і `umap3d_train_<metric>_<клас>.*` для 3д. Виняток — `клас210х14х4`, його карти
лишились без суфікса класу (`umap_train_cosine.*`, `umap3d_train_cosine.*`):
це одобрений вивід етапів 1–4, перейменування зламало б посилання на нього.

У PNG рендериться одна карта — `клас210х14х4` / `cosine` (`render_png.py`,
етап 4). Решта зберігається як `.npz` + `.json` + `.meta.json`
і малюється на вкладці; цього досить, щоб перерахувати й перемалювати будь-яку
з нуля (CLAUDE.md, розділ 7).

## Перевірка перед завершенням

- [ ] всі етапи підтверджені, непідтверджене видалено
- [ ] логіка послідовна
- [ ] відтворюється з цього файлу + даних
- [ ] стаття почищена
