# 35. Повнота ноги: мережа на неповній нозі

**Пара:** BTC_USDT + 5 монет (ETH, SOL, XRP, BNB, DOGE)
**Клас:** `клас210х10-1х7д/д` (`class210x10m1x7dd`)
**Вкладка:** [`/fullness`](http://localhost:8080/fullness)

## Ціль

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

Дослідження закриває три питання:

1. **Чим відрізняються частини ноги.** Нога не однорідна: у неї є
   початок, кінець і внутрішні корекції, а в кожної корекції два краї —
   де нога спинилась і де рушила далі. Якщо край корекції за фічами
   такий самий, як край ноги, то по одній події повноту не скажеш
   ніколи, і питати треба з послідовності.
2. **Як подавати різну довжину.** Сформована частина ноги подається
   цілком, усіма числами класу, у сталу матрицю: довша нога стискається,
   коротша роздувається — клас перераховується на свій крок з
   посекундних даних.
3. **Скільки це дає понад лічильник.** Простий лічильник подій від
   підтвердженого півота — базовий рівень, який у дослідженні 26 не
   вдалось побити взагалі (val MAE 0.244 проти 0.248 у константи 0.5).
   Мережа тут або бʼє його, або дослідження закривається тим самим
   числом.

## Дані

`data/datasets/<PAIR>/class210x10m1x7dd/` — шість монет, подія 210 с,
форма події `(9, 7)`. Розбиття за датою: `train` до 2026-08-14, `val`
до 2026-09-04. Подій у ногах: **352 454** train і **50 371** val.

Ноги — стандарт `rules/01-data-and-db.md` → Ноги датасету: 603 ноги
train і 106 val, медіана довжини 433 і 331 подія.

## Розмітка

```
ряд подій частини -> ема10(price_open) -> півоти зігзага 2.3
    -> точка кінця ноги (екстремум price_close, ±25 подій) -> ноги
усередині кожної ноги -> зігзаг 0.8 по тій самій ема
    -> та сама точка кінця ноги -> краї корекцій
```

Обидва зігзаги проходять крок `snap`: точна точка кінця ноги — стандарт
для порогу будь-якого розміру (`rules/02-markup.md` → Точка кінця ноги).
Вкладений зігзаг 0.8 рахується **в межах своєї ноги**, краї ноги в
корекції не потрапляють.

**Мітка повноти** для події на місці `i` всередині ноги довжини `L`:

```
повнота = i / (L - 1)        0 на першій події ноги, 1 на останній
```

`L` відома лише заднім числом, тому це мітка, а не вхід.

## Правила дослідження

**Нормування — тільки на саме вікно.** Вхід мережі нормується лише
числами того вікна, яке їй подано: від першого запису після півота до
поточного `t`. Медіана, перцентиль, будь-яка статистика беруться з живих
рядків цього вікна і більше ні звідки — ні з решти ноги після `t`, ні з
наступної ноги, ні з ноги цілком. Нормування на всю ногу для неповного
вікна — заглядання вперед: масштаб ще не пройденої частини ноги
підказує мережі, скільки її лишилось. Коли вікно — уся нога (етапи 3–5),
нормування на вікно і на ногу збігаються.

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

1. **Ролі подій** — чим краї корекцій відрізняються від країв ноги і
   від її тіла на самих фічах класу.
2. **Канва ноги** — префікс ноги в матрицю 500 × 9 × 7, мережа
   «згортка + увага» на дві голови (повна / частка), контроль «хвіст»,
   замір на train, відкладених ногах train і val проти лічильника й
   константи.
3. **Стандарт 30 с** — те саме без класу подій: секунди сумуються в
   записи по 30 с, на них півоти за стандартом, фічі ходом
   `клас210х10-1х7д/д`, ноги train і val, канва 500 × 7; вкладка
   [`/fullness30`](http://localhost:8080/fullness30) показує ногу, її
   фічі, канву і вхід мережі. Навчання — окремою командою.
4. **Класи проєкту на записах 30 с** — усі 18 класів
   `rules/01-data-and-db.md` як алгоритми перетворення ряду записів і
   канви; вкладка показує все етапу 3 для обраного кнопкою класу.
5. **Початок і кінець ноги** — голова й хвіст канви (по 5 %) між усіма
   ногами на кожному класі: кореляції, розрив, влучність з підлогою,
   контроль і розклад по фічах; UMAP ніг — нога, голова, хвіст.
6. **Частка ноги у вікні** — мережа отримує вікно від півота до `t`
   (канва `500 × F` будь-якого класу) і каже, яку частку ноги подано,
   зокрема коли подано більше за ногу; перший крок — чи корелюють ноги
   між собою по рівних частках і по надлишках.
7. **Уся канва без стиснення** — MLP, Mixer, LSTM і трансформер на
   500 рядках.
8. **Півоти ноги на канві** — у канву потрапляє нога з півотами зверху
   й знизу і шматками до і після; мережа на кожному рядку каже «нога
   завершилась» — перетин 95 … 105 % тривалості ноги і 10 % її цінової
   довжини від ціни півота.

## Скрипти

| Скрипт | Що робить | Вихід |
| ------ | --------- | ----- |
| [`scripts/roles.py`](scripts/roles.py) | етап 1: ролі подій, AUC пар ролей, форма ніг | [`results/roles.json`](results/roles.json) |
| [`scripts/canvas_data.py`](scripts/canvas_data.py) | етап 2: канви префіксів ніг з посекундних даних | `data/canvas_500/` |
| [`scripts/canvas_net.py`](scripts/canvas_net.py) | етап 2: мережа на канві, контроль «хвіст» | [`results/canvas.json`](results/canvas.json), [`results/tail.json`](results/tail.json) |
| [`scripts/report.py`](scripts/report.py) | етап 2: звід прогонів і похибка перевибіркою ніг | [`results/fullness_runs.json`](results/fullness_runs.json) |
| [`scripts/bars30.py`](scripts/bars30.py) | етапи 3 і 4: записи 30 с, ноги, фічі й канва 500 кожного класу | `data/bars30/`, [`results/bars30.json`](results/bars30.json) |
| [`scripts/classes30.py`](scripts/classes30.py) | етап 4: 18 класів проєкту як алгоритми перетворення ряду блоків | — |
| [`scripts/head_tail30.py`](scripts/head_tail30.py) | етап 5: голова проти хвоста ноги на канві кожного класу | [`results/head_tail30.json`](results/head_tail30.json) |
| [`scripts/umap_legs30.py`](scripts/umap_legs30.py) | етап 5: UMAP ніг — нога, голова, хвіст × три метрики | `results/umap_legs30/` |
| [`scripts/fullness_data30.py`](scripts/fullness_data30.py) | етап 6: навчальні вікна з випадковою часткою, мітки сходинок і рядків; `--series` — ряд val з кроком 0.025 частки | `data/fullness30/<клас>/` |
| [`scripts/fullness_net30.py`](scripts/fullness_net30.py) | етап 6: мережа на два виходи, грід, фінальна модель з епохами біля найкращої, контроль «хвіст»; етап 7: кола 4–10 — згортка з меншим кроком, сирі фічі, MLP, MLP-Mixer, LSTM і трансформер на всій канві | `results/fullness30/`, `models/fullness30_v1/`, `data/fullness30/x10m1x7dd/runs/` |
| [`scripts/edges30_data.py`](scripts/edges30_data.py) | етап 8: вікна навколо ніг з півотами всередині, мітки зон 10 %, ковзні вікна ряду val | `data/edges30/` |
| [`scripts/edges30_net.py`](scripts/edges30_net.py) | етап 8: трансформер «тут півот» на кожен рядок канви, видача на val, канва руками | `models/edges30/`, `results/edges30/` |
| [`scripts/probe30.py`](scripts/probe30.py) | етап 6: подача і видача мережі на одному вікні ноги val — частка ноги 0.1 … 2.0, канва тим самим ходом, що вікна навчання, модель обраної епохи або, з `--run d1` / `d0` / `raw` / `flat`, прогін кола 4 (250 / 500 кроків уваги / 500 кроків сирих фіч / канва одним вектором без уваги, ваги `data/fullness30/x10m1x7dd/runs/g4_*.pt`); для вкладки, наживо | — |
| [`scripts/windows30.py`](scripts/windows30.py) | етап 6: канви вікон 16 часток ноги і кореляції між ногами по частках | `data/bars30/windows_<part>_<клас>.npz`, [`results/windows30.json`](results/windows30.json) |

```bash
python3 research/35-leg-fullness/scripts/roles.py
python3 research/35-leg-fullness/scripts/canvas_data.py --cuts 12
python3 research/35-leg-fullness/scripts/canvas_net.py --epochs 24 --tag canvas
python3 research/35-leg-fullness/scripts/canvas_net.py --epochs 24 --tail 16 --tag tail
python3 research/35-leg-fullness/scripts/report.py
```

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

| Блок | Що показує | Ендпоінт | Скрипт |
| ---- | ---------- | -------- | ------ |
| опис | клас, обидва зігзаги, правило ролі, підлога перестановок | `/api/fullness` | [`scripts/roles.py`](scripts/roles.py) |
| ноги й корекції | монета за монетою: ніг, корекцій, медіани, обидва Спірмени | `/api/fullness` | там само |
| пари ролей | пʼять найсильніших колонок кожної пари, AUC проти підлоги, розкид по монетах, та сама колонка на іншій частині; перемикач train / val | `/api/fullness` | там само |
| повнота за корекціями | квартилі повноти після `N` корекцій, перемикач монети | `/api/fullness` | там само |
| етап 2 · канва ноги | метод: крок канви, межа роздування, обриви, причинний лік корекцій | `/api/fullness/runs` | [`scripts/report.py`](scripts/report.py) |
| прогони | обидві мережі × train / відкладені ноги / val, базові рівні сірим; похибка на val перевибіркою ніг і різниця мереж | `/api/fullness/runs` | там само |
| епохи | AUC на відкладених ногах після кожної епохи, обидві мережі, усі й важкі обриви, обрана епоха | `/api/fullness/runs` | там само |
| val: сказана частка проти справжньої | кожен префікс val точкою, колір — вид обриву, пунктир — ідеальна відповідь | `/api/fullness/runs` | там само |

Колонка, яка не вийшла за підлогу перестановок, стоїть у таблиці сірою.

Тести: [`tests/test_leg_fullness.py`](../../tests/test_leg_fullness.py),
[`tests/test_leg_canvas.py`](../../tests/test_leg_canvas.py),
[`tests/test_dashboard.py`](../../tests/test_dashboard.py) →
`TestFullnessTab`, `TestFullnessMarkup`, `TestFullnessRuns`.

## Етап 1. Ролі подій у нозі — `підтверджено`, 2026-09-23

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

Кожна подія ноги дістає роль. Якір ролі — подія розмітки, група — якір
і `--width = 2` події **з одного боку**: роль «нога спинилась» дивиться
назад (підхід до якоря), роль «нога рушила» — вперед. Обидві групи по
три події, обидві причинні.

| Роль | Якір | Подій train | Подій val |
| ---- | ---- | ----------: | --------: |
| `кінець` | остання подія ноги (півот 2.3) | 1809 | 318 |
| `початок` | перша подія ноги | 1809 | 315 |
| `корекція-як-кінець` | внутрішній півот 0.8 того самого боку, що кінець ноги | 2512 | 441 |
| `корекція-як-початок` | внутрішній півот 0.8 того самого боку, що початок | 3047 | 533 |
| `тіло` | подія далі ніж 10 подій від усіх якорів | 304 487 | 42 083 |

Фіча класу — відношення похідних, тому міри **рангові**. Кожна з 7 фіч
згортається по 9 блоках події у три числа — середнє, розкид і остання
плашка, — і кожна з 21 колонки переводиться в ранг усередині монети,
база рангу — події `train` цієї ж монети.

Міра пари ролей — **AUC** колонки на всіх шести монетах разом. Підлога —
20 перестановок мітки ролі при тих самих розмірах груп: береться
найбільший відхід від 0.5. Колонка, чия AUC не виходить за підлогу,
нічого не показала.

### Результат 1: край ноги різко відрізняється від тіла

Найсильніші колонки, `train` (у дужках — підлога перестановок), поруч
`val` і розкид по шести монетах:

| Пара ролей | Колонка | train | val | монети |
| ---------- | ------- | ----: | --: | ------ |
| `кінець` проти `тіло` | `d_up·std` | **0.762** (0.475…0.525) | 0.795 | 0.745…0.782 |
| `кінець` проти `тіло` | `d_price_avg·std` | **0.761** (0.488…0.512) | 0.810 | 0.734…0.784 |
| `кінець` проти `тіло` | `d_resist_buy·std` | **0.227** (0.481…0.519) | 0.196 | 0.192…0.258 |
| `початок` проти `тіло` | `d_resist_buy·std` | **0.231** (0.485…0.515) | 0.219 | 0.194…0.262 |
| `початок` проти `тіло` | `d_price_avg·std` | **0.742** (0.486…0.514) | 0.784 | 0.723…0.772 |
| `корекція-як-кінець` проти `тіло` | `d_price_avg·std` | **0.721** (0.489…0.511) | 0.752 | 0.704…0.741 |
| `корекція-як-початок` проти `тіло` | `d_price_avg·std` | **0.686** (0.491…0.509) | 0.711 | 0.658…0.710 |

Поза підлогою на train — 18–19 колонок із 21 у кожній парі з тілом.
Підпис краю читається однаково в усіх чотирьох ролях: **розкид ціни й
обʼємних відношень по блоках події високий, розкид рівневих відношень
низький**. Знак той самий на всіх монетах і той самий на val.

### Результат 2: край корекції — той самий підпис, слабший

| Пара ролей | Колонка | train | val | монети |
| ---------- | ------- | ----: | --: | ------ |
| `кінець` проти `корекція-як-кінець` | `d_resist_buy·std` | **0.428** (0.478…0.522) | 0.411 | 0.364…0.471 |
| `кінець` проти `корекція-як-кінець` | `d_resist_buy·mean` | **0.562** (0.476…0.524) | 0.596 | 0.536…0.600 |
| `початок` проти `корекція-як-початок` | `d_support_sell·std` | **0.390** (0.479…0.521) | 0.402 | 0.345…0.417 |
| `початок` проти `корекція-як-початок` | `d_support_sell·mean` | **0.593** (0.478…0.522) | 0.587 | 0.556…0.625 |
| `кінець` проти `початок` | `d_up·last` | 0.436 (0.479…0.521) | 0.445 | 0.410…0.460 |
| `корекція-як-кінець` проти `корекція-як-початок` | `d_down·std` | 0.557 (0.484…0.516) | 0.570 | 0.483…0.609 |

Три речі:

1. **Справжній край ноги — це посилений край корекції, а не інший
   підпис.** Колонки, за якими край відрізняється від тіла (`0.23` і
   `0.76`), у парі «кінець проти корекції» стоять з того самого боку від
   0.5 (`0.43` і `0.56`): у справжнього краю те саме, тільки більше.
   Різниця 0.07–0.11 AUC проти 0.26 у пари з тілом.
2. **Поза підлогою — 17 колонок із 21**, і знак тримається на val і на
   кожній монеті. Тобто різниця є, вона просто мала.
3. **Кінець ноги від її початку по одній події майже не відрізняється**
   (`0.44`, поза підлогою лише 11 колонок із 21). Клас каже «тут край»,
   а не «тут верх» — це той самий висновок, що в дослідженні 34.

### Результат 3: скільки в нозі корекцій і що вони кажуть про повноту

| Монета | Ніг train | Довжина, медіана | Корекцій на ногу (q25 / медіана / q75 / max) | Спірмен «повнота ~ число корекцій» | Спірмен «повнота ~ крок» |
| ------ | --------: | ---------------: | -------------------------------------------- | ---------------------------------: | -----------------------: |
| BTC_USDT | 83 | 566 | 1 / 3 / 5 / 15 | 0.561 | 0.729 |
| ETH_USDT | 114 | 430 | 1 / 3 / 4 / 14 | 0.536 | 0.724 |
| SOL_USDT | 121 | 349 | 1 / 3 / 5 / 16 | 0.587 | 0.695 |
| XRP_USDT | 92 | 458 | 1 / 3 / 4 / 13 | 0.552 | 0.708 |
| BNB_USDT | 86 | 446 | 1 / 2 / 4 / 17 | 0.565 | 0.668 |
| DOGE_USDT | 107 | 368 | 1 / 2 / 4 / 21 | 0.497 | 0.699 |

Повнота ноги за числом корекцій, які **вже сталися** (BTC train):

| Корекцій позаду | Подій | Повнота q25 | медіана | q75 |
| --------------: | ----: | ----------: | ------: | --: |
| 0 | 10 362 | 0.09 | 0.22 | 0.43 |
| 1 | 15 683 | 0.14 | 0.31 | 0.54 |
| 2 | 10 161 | 0.30 | 0.48 | 0.73 |
| 3 | 7 273 | 0.38 | 0.58 | 0.77 |
| 4 | 5 875 | 0.47 | 0.70 | 0.86 |
| 5 | 4 365 | 0.57 | 0.71 | 0.88 |

Число корекцій позаду — **причинна величина**, і вона рухається разом з
повнотою: медіана йде 0.22 → 0.71 від нуля до пʼяти корекцій, Спірмен
0.50–0.59 на всіх монетах. Але квартилі широкі: після трьох корекцій
нога буває пройдена і на 38 %, і на 77 %. Сам по собі лічильник кроку
корелює сильніше (0.67–0.73), тому корекції мають сенс тільки як
**додаток** до нього — це міряється на етапі 2.

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

- Край ноги видно в класі впевнено (AUC 0.72–0.81 проти тіла, той самий
  знак на шести монетах і на val).
- Але **край корекції виглядає так само**, тільки слабше. По одній
  події відрізнити розворот від корекції не можна: різниця 0.07–0.11
  AUC. Мережа, яка каже «нога повна», має дивитись на **послідовність**
  — на те, скільки таких країв уже було і який між ними пройдено рух.
- Структура ноги причинно спостережна: число корекцій позаду відоме
  онлайн і йде разом з повнотою.

## Етап 2. Канва ноги — `підтверджено`, 2026-09-23

### Канва

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

```
префікс ноги: n подій = n · 210 секунд
канва:        500 рядків, крок рядка s = n · 210 / 500 секунд
блок:         s / 10 секунд — 10 блоків, як у звичайній події
рядок:        той самий хід, що в класі: середні блоку → рівні на
              обʼєм свого блоку → похідні по блоках без першої плашки
              → шість відношень похідних плюс ціна = 9 × 7
```

- `n > 500` — рядок бере більше секунд за звичайну подію, нога
  **стискається**; `n < 500` — менше, нога **роздувається**. Кількість
  фіч стала, міняється величина. Нічого не інтерполюється: кожен рядок —
  справжня подія класу з посекундних даних `data/2026/<міс>/<PAIR>/`.
- Суми блоків беруться з **кумулятивних сум** секунд, тому канва будь-
  якого кроку збирається кількома тисячами звертань.
- **Межа роздування** — блок не коротший за `MIN_BLOCK = 3` с: коротший
  блок порожній, і похідна виродиться. Закороткий префікс будується на
  цій межі й лягає у **хвіст** канви, початок іде маскою.
- **Діри**: блок живий, коли в ньому є хоча б половина секунд; рядок з
  мертвим блоком — нулі під маскою.
- **Числа**: відношення похідних доходять до `1e21`, тому в файл лягає
  `sign(v) · log1p(|v|)` (строго зростає, знак і порядок лишаються), а
  потім ділиться на 99-й перцентиль модуля **свого кошика кроку** — 12
  кошиків, рівних за логарифмом кроку, база — канви train. Похідна за
  23 с більша за похідну за 17 с, і спільна одиниця склеїла б короткі
  ноги з довгими.
- **Скаляри** поруч: подій у префіксі, крок канви, пройдений рух від
  півота, **підтверджених** корекцій позаду і частка живих рядків.
  Корекція йде в лік лише тоді, коли її видно: ема відійшла від півота
  0.8 % (`c ≤ e`) і вікно точки кінця ноги закрилось (`p + 25 ≤ e`).
  За самою позицією півота рахувати не можна — це заглядання вперед.

### Етап 3. Повнота ноги на стандарті 30 с — `підтверджено`, 2026-09-23

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

### Промпт

Той самий хід, що в етапі 2, але без класу подій — з посекундних даних:

1. **Секунди → записи по 30 с.** Кожна доба робочої копії
   `data/2026/<міс>/<PAIR>/` читається стандартним `data_source.load_day`
   (10 колонок, рядки з `NaN` відкидаються, з дублікатів `timestamp`
   лишається перший). Секунди кладуться на сітку UTC, кратну 30 с, і
   **сумуються**: запис — це суми девʼяти колонок (`const_resist`,
   `resist_plus`, `resist_minus`, `const_support`, `support_plus`,
   `support_minus`, `vol_buy`, `vol_sell`, `price_avg`) і кількість
   секунд `n`, що в ньому є. Запис без жодної секунди не існує. Ціни
   запису для зігзага: `price_open` — `price_avg` першої секунди,
   `price_close` — останньої.
2. **Півоти — на записах 30 с, без ема.** Зігзаг 2.3 % прямо по
   `price_open` записів → точка кінця ноги: екстремум `price_close` у ±25
   **записах** (`rules/02-markup.md` → Точка кінця ноги). Згладжування
   немає — рішення користувача 2026-09-23. Частини
   ріжуться окремо: `train` — до 2026-08-14, `val` — з 2026-08-14 по
   2026-09-22 включно (остання повна доба). Нога — від запису після
   півота до наступного півота; ряд обрізається по ногах.
3. **Фічі — хід `клас210х10-1х7д/д`, крок за кроком, де блоком є запис 30 с:**
   - `клас210х10х9`: середні запису `x = Σf / n` для девʼяти колонок;
   - `клас210х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` як є;
   - `клас210х10х7дц`: перша похідна `d[j] = x[j] − x[j−1]`;
   - `-1`: похідна рахується лише тоді, коли попередній запис стоїть
     **впритул** (30 с). Через розрив похідної немає — так само, як у
     класі прибрано плашку, що тягнеться в попередню подію;
   - `клас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`
     як є. **Нульовий знаменник дає 0** — і в рівнях на обʼєм, і у
     відношеннях похідних: там, де 0, так і лишається 0 (рішення
     користувача 2026-09-23). Запис без похідної (попередній не впритул)
     лишається без значення.
4. **Ноги train і val** — кожна з рядом записів і рядом семи фіч.
5. **Канва 500.** Проміжок ноги ділиться на 500 рівних часових відрізків,
   відрізок — **сума секунд**. Коли в нозі 500 записів і більше, межі
   стають на межі записів — нога стискається цілими записами; коли
   менше — відрізок коротший за запис і береться з посекундних сум, нога
   роздувається. Відрізок не коротший за 3 с; закоротка нога лягає у
   хвіст канви під маску. Далі по 500 рядках — той самий хід пункту 3,
   виходить `500 × 7`; у першого рядка похідної немає.
6. **Вхід мережі** — кожна нога нормується ізольовано, кожен канал
   окремо, без бази з інших ніг і монет (рішення користувача 2026-09-23):
   1. `v / m`, де `m` — медіана `|v|` **ненульових** живих рядків своєї
      ноги;
   2. `sign(v) · log1p(|v|)`;
   3. ділення на 99-й перцентиль `|v|` живих рядків своєї ноги.

   Медіана йде першою, бо `log1p` залежить від масштабу: сирі `d_up`
   у BTC ≈ 0.002 (там `log1p` майже лінійний і сплеск лишається
   сплеском), у DOGE ≈ 35 (там він стискає). Після ділення на медіану
   типове значення кожної ноги ≈ 1, і `log1p` стискає однаково на всіх
   монетах. Канал без ненульових значень або з нульовим перцентилем —
   одиниця 1; мертві рядки — 0. Маска рядків, скаляри поруч. Навчання —
   **після окремої команди**.
7. **Вкладка**: алгоритм пунктами; вибір пари, частини й номера ноги
   (кнопки ◀ ▶); графік ноги; під ним сім фіч по записах; під ним сім
   фіч канви 500; під ним вхід мережі.

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

| Блок | Що показує | Ендпоінт |
| ---- | ---------- | -------- |
| клас | 18 кнопок класів проєкту (етап 4), під ними формула обраного класу на записі й кількість фіч; вибір класу перемальовує таблицю й ногу, монета, частина й номер ноги лишаються | `/api/fullness30` |
| початок і кінець ноги | плашка, що розгортається (етап 5): кнопки класів, таблиця розриву й влучності з підлогою і контролем, по фічах з перемикачем груп ніг, UMAP ніг — нога, голова, хвіст, перемикачі частини й метрики | `/api/fullness30/headtail/<клас>`, `/api/fullness30/umap/<клас>` |
| мережа частки ноги (етап 6) | опис входу й виходів; таблиця гріда — кожен прогін: ручки, найкраща епоха, втрата і AUC сходинок / рядків проти лічильника на відкладених ногах і на val, обрана конфігурація жирним; кнопки монети val і кнопки епох — найкраща (★) і по дві з кожного боку; для обраних — таблиця AUC кожної сходинки проти лічильника і частка `q ≥ c`, AUC рядків; ряд val монети: ціна з півотами, під нею на спільній осі часу три теплові карти — сходинки вікна від поточного півота (`q ≤ 1`), сходинки вікна від попереднього півота (`q > 1`), рядки канви вікна від попереднього півота (50 клітинок по 10 рядків), правда — жовтим; назва кожної смуги — над її рамкою, по Y сходинок — пороги 0.1, 0.5, 0.9, 1.0+, 1.2, 1.5, по Y рядків — 1, 250, 500; під смугами — пояснення, що таке `q` і як читати осі; зум і зсув спільні, малюнок не виходить за рамку | `/api/fullness30/net`, `/api/fullness30/net/<PAIR>/<епоха>` |
| алгоритм | пункти промпту (`bars30.STEPS`) і параметри: запис, «без ема», поріг, вікно точки кінця ноги, канва, межі частин | `/api/fullness30` |
| записи й ноги | монета × частина: вікно дат, записів, секунд, розривів, ніг, вгору / вниз, медіани, частка записів із `NaN` обраного класу | `/api/fullness30` |
| нога | перемикачі монети й частини, номер ноги кнопками ◀ ▶ або числом; під ними чотири блоки на одній нозі | `/api/fullness30/leg/<PAIR>/<part>/<N>/<клас>` |
| · ціна | ціна запису від півота до півота, півоти — зелений угорі, червоний унизу | там само |
| · фічі записів | ряди ходу обраного класу по записах на спільній осі часу, по одному на фічу; шкала `sign·log1p(|v|)` | там само |
| · канва 500 | ті самі фічі на 500 відрізках ноги, точка — у середині свого відрізка на тій самій осі часу | там само |
| · вхід мережі | теплова карта 500 × фічі класу після нормування на свою ногу (медіана → log1p → p99), рядок маски, скаляри | там само |
| подача і видача на одній нозі (етап 6) | унизу вкладки, тільки val, увесь блок — на один екран (~650 px): керування одним рядком — кнопки монети, нога кнопками ◀ ▶ або числом, комірка «% ноги» 10 … 200, кнопки епох 4 … 8 (★ — найкраща) і кнопки «250 кр.» / «500 кр.» / «500 сирих» / «без уваги» — прогони кола 4 `d1`, `d0`, `raw` і `flat` з найкращою за відкладеними епохою, назва моделі — у підписі сходинок; кнопки й комірки не перебудовуються, тіло блоку тримає висоту на час запиту — сторінка при натисканні не зсувається; рядок ноги: напрямок, `L`, вікно `n`, `q = n / L`, живі рядки, відрізок, скаляри; ціна ноги й вікна з запасом 10 % ноги, вікно затінене, його кінець — синя риска; під ціною на спільній осі часу — подача (теплова карта канви 500 × фічі і маска, кожен рядок на своєму відрізку часу; скаляри в підписі) і вихід 2 (`P(рядок — наступна нога)` синім, правда жовтим); під ними вихід 1 — 16 сходинок `P(q ≥ c)` стовпчиками з правдою. Вікно рахується наживо `probe30.probe` — перевірено: на вікнах val дає ті самі виходи, що `final_outputs_val_e06.npz` (різниця ≤ 3·10⁻⁴) | `/api/fullness30/probe/<PAIR>/<N>/<відсоток>/<епоха або d1, d0, raw, flat>` |

Нога віддається окремим запитом: у найдовшої до 20 тисяч записів.

Скрипт: [`scripts/bars30.py`](scripts/bars30.py) з класами
[`scripts/classes30.py`](scripts/classes30.py) → `data/bars30/`
(`<PAIR>_<part>.npz` — записи: час, секунди, ціни; `<PAIR>_<part>_<клас>.npz`
— фічі класу на записах; `legs.json` — ноги; `canvas_<part>_<клас>.npz` —
канва класу сира й нормована) і
[`results/bars30.json`](results/bars30.json) — звід. Тести:
[`tests/test_bars30.py`](../../tests/test_bars30.py),
[`tests/test_classes30.py`](../../tests/test_classes30.py),
[`tests/test_dashboard.py`](../../tests/test_dashboard.py) → `TestFullness30`.

```bash
python3 research/35-leg-fullness/scripts/bars30.py
```

Вікно точки кінця ноги — ±25 записів, тобто ±12.5 хвилини.

### Результат 5: записи й ноги стандарту 30 с

| Монета | Частина | Записів | Розривів | Ніг | вгору / вниз | Медіана записів | Медіана годин | Медіана руху | Записів без похідної |
| ------ | ------- | ------: | -------: | --: | ------------ | --------------: | ------------: | -----------: | -------------------: |
| BTC_USDT | train | 435343 | 2971 | 146 | 73 / 73 | 2049 | 17.1 | 4.02 % | 0.68 % |
| BTC_USDT | val | 112754 | 329 | 26 | 13 / 13 | 3551 | 29.6 | 3.99 % | 0.29 % |
| ETH_USDT | train | 440984 | 2969 | 229 | 114 / 115 | 1171 | 9.8 | 4.05 % | 0.67 % |
| ETH_USDT | val | 112708 | 330 | 40 | 20 / 20 | 2110 | 17.6 | 4.40 % | 0.29 % |
| SOL_USDT | train | 437429 | 2970 | 288 | 144 / 144 | 893 | 7.4 | 4.04 % | 0.68 % |
| SOL_USDT | val | 112693 | 327 | 56 | 28 / 28 | 1250 | 10.4 | 4.67 % | 0.29 % |
| XRP_USDT | train | 390401 | 2904 | 166 | 83 / 83 | 1488 | 12.4 | 4.08 % | 0.74 % |
| XRP_USDT | val | 112682 | 335 | 100 | 50 / 50 | 355 | 3.0 | 4.83 % | 0.30 % |
| BNB_USDT | train | 439507 | 2975 | 118 | 59 / 59 | 2310 | 19.2 | 4.35 % | 0.68 % |
| BNB_USDT | val | 112681 | 333 | 34 | 17 / 17 | 2146 | 17.9 | 3.75 % | 0.30 % |
| DOGE_USDT | train | 390380 | 2908 | 263 | 132 / 131 | 931 | 7.8 | 3.62 % | 0.75 % |
| DOGE_USDT | val | 112666 | 341 | 96 | 48 / 48 | 533 | 4.4 | 4.24 % | 0.30 % |

Разом: **1210 ніг train** і **352 ноги val** — на подіях 210 с з ема10
їх було 603 і 106. Записів без похідної 0.3–0.7 % — це рівно записи
після розриву ряду: нульових знаменників серед них немає, бо нуль
лишається нулем.

## Етап 4. Класи проєкту на записах 30 с — `підтверджено`, 2026-09-23

Вкладка: [`/fullness30`](http://localhost:8080/fullness30) — кнопки класів над алгоритмом.

### Промпт

Взяти всі класи проєкту, додати кнопками вибір класу — це алгоритми
перетворення для цього досліду — і далі все, що зроблено в етапі 3,
виводити для обраного класу.

### Метод

Кожен клас таблиці `rules/01-data-and-db.md` → Класи (події) стає
**алгоритмом перетворення ряду блоків**. Блок — запис 30 с (ряд записів
частини) або відрізок канви (ряд 500 відрізків ноги). Формули класу ті
самі; міняється тільки те, що є блоком:

- класи з блоками всередині події (`х14`, `х10`) — блоком стає запис;
  у `клас210х14х4` «середній обʼєм події» — середній обʼєм самого запису;
- класи з одним блоком на подію (`х210х210`) — подією стає запис;
  середнє по одному блоку дорівнює блоку, тому на записі
  `клас210х210х3` ≡ `клас210х14х3`, `клас210х210х4ц` ≡ `клас210х14х4ц`;
- ЕМА-класи — ЕМА по ряду блоків, період **у блоках**: 200 записів
  (100 хв) у `клас210ема4` і `клас210ема4ц`, 10 … 130 записів у
  `клас210х4бема10х10х130`; на канві — у відрізках, старт із першого
  живого відрізка; ЕМА записів іде наскрізь по частині;
- `price_balance` — сума змін `price_avg` між сусідніми секундами
  всередині блоку, поділена на суму їх модулів; `ema_price` — ЕМА ціни
  першої секунди блоку;
- похідні — до попереднього блоку, лише коли він стоїть впритул
  (сусідній запис рівно за 30 с; на канві — обидва відрізки мають
  секунди); `клас210х10х7дц` на блоці без сусіда бере середнє трьох
  наступних похідних (правило першого блоку класу), класи `-1` лишають
  його без значення.

Спільне для всіх класів:

- нульовий знаменник дає **0** (рішення користувача 2026-09-23);
- блок без жодної секунди значень не має і на канві йде під маску;
- рядок канви живий, коли у відрізку є секунди і клас дав хоча б одне
  значення;
- вхід мережі — нормування етапу 3 на свою ногу, кожен канал окремо:
  медіана `|v|` ненульових живих рядків → `sign·log1p` → 99-й перцентиль.

`клас210х10-1х7д/д` — типовий клас вкладки; його записи, канва і вхід
мережі **збігаються з етапом 3 біт у біт** (перевірено на BTC_USDT train
і val, і тестом `test_the_approved_class_matches_the_stage_three_chain`).

| Клас | Ключ у файлах | Фіч | Формула на записі |
| ---- | ------------- | --: | ----------------- |
| `клас210х8` | `x8` | 8 | середні запису восьми колонок стакана й обʼємів |
| `клас210х14х4` | `x14x4` | 4 | Σ запису рівня / середній обʼєм запису: resist, d_resist = Σ(plus − minus), support, d_support |
| `клас210х14х2` | `x14x2` | 2 | нормовані різниці (a − b)/(|a| + |b|) пар клас210х14х4: balance, d_balance |
| `клас210х14х5` | `x14x5` | 5 | клас210х14х4 + vol_balance = (Σvol_buy − Σvol_sell)/(Σvol_buy + Σvol_sell) |
| `клас210х14х3` | `x14x3` | 3 | balance, d_balance, vol_balance на записі |
| `клас210х14х4ц` | `x14x4c` | 4 | клас210х14х3 + price_balance = Σ змін ціни / Σ|змін| усередині запису |
| `клас210х210х3` | `x210x3` | 3 | середнє клас210х14х3 по події з одного блоку — на записі збігається з клас210х14х3 |
| `клас210х210х4ц` | `x210x4c` | 4 | середнє клас210х14х4ц по події з одного блоку — на записі збігається з клас210х14х4ц |
| `клас210ема4` | `ema4` | 4 | ЕМА200 записів: ціна першої секунди запису і три фічі клас210х210х3 |
| `клас210ема4ц` | `ema4c` | 4 | ЕМА200 записів чотирьох фіч клас210х210х4ц |
| `клас210х4бема10х10х130` | `bema` | 52 | ЕМА 10, 20 … 130 записів чотирьох фіч клас210х210х4ц — 13 × 4 = 52 канали |
| `клас210х10х9` | `x10x9` | 9 | середні запису девʼяти колонок, з ціною |
| `клас210х10х7` | `x10x7` | 7 | рівні на обʼєм свого запису, обʼєми й ціна як є |
| `клас210х10х7дц` | `x10x7dc` | 7 | похідна клас210х10х7 до запису впритул; запис без сусіда — середнє трьох наступних похідних |
| `клас210х10-1х7дц` | `x10m1x7dc` | 7 | похідна клас210х10х7 лише до запису впритул, інакше без значення |
| `клас210х10-1х3+1дц` | `x10m1x3p1dc` | 4 | (|a| − |b|)/(|a| + |b|) трьох пар похідних «опір проти підтримки» + d_price_avg |
| `клас210х10-1х2д` | `x10m1x2d` | 2 | d_up = d_vol_buy / d_resist, d_down = d_vol_sell / d_support |
| `клас210х10-1х7д/д` | `x10m1x7dd` | 7 | шість відношень похідних клас210х10-1х7дц + d_price_avg |

Скрипт класів: [`scripts/classes30.py`](scripts/classes30.py) — реєстр
`CLASSES` (імʼя, ключ, формула, фічі, функція) і `features(key, block)`.
Збирач — той самий [`scripts/bars30.py`](scripts/bars30.py): записи й
канва рахуються на всіх класах одним проходом по секундах. Тести:
[`tests/test_classes30.py`](../../tests/test_classes30.py) — формули,
межі нормованих різниць, нульові знаменники, похідні й правило першого
блоку, ЕМА, збіг відрізків канви з посекундних сум із записами.

```bash
python3 research/35-leg-fullness/scripts/bars30.py
python3 research/35-leg-fullness/scripts/bars30.py --classes x14x4c bema
```

### Результат 6: 18 класів на записах і канві

Усі шість монет, 1210 ніг train і 352 ноги val — ті самі ноги, що в
етапі 3. Вхід мережі — `|v|` живих рядків канви після нормування на свою
ногу; «живих» — частка живих рядків канви (короткі ноги лягають у хвіст
канви, голова під маскою); «без значення» — частка записів, де клас не
дав значення, середнє по монетах і частинах.

| Клас | Фіч | train: живих | train: медіана | train: макс | val: живих | val: медіана | val: макс | Записів без значення |
| ---- | --: | -----------: | -------------: | ----------: | ---------: | -----------: | --------: | -------------------: |
| `клас210х8` | 8 | 0.904 | 0.41 | 5.1 | 0.906 | 0.42 | 3.7 | 0.00 % |
| `клас210х14х4` | 4 | 0.904 | 0.27 | 4.2 | 0.906 | 0.27 | 2.6 | 0.00 % |
| `клас210х14х2` | 2 | 0.904 | 0.62 | 1.2 | 0.906 | 0.59 | 1.1 | 0.00 % |
| `клас210х14х5` | 5 | 0.904 | 0.31 | 4.2 | 0.906 | 0.31 | 2.6 | 0.00 % |
| `клас210х14х3` | 3 | 0.904 | 0.59 | 1.2 | 0.906 | 0.57 | 1.2 | 0.00 % |
| `клас210х14х4ц` | 4 | 0.904 | 0.55 | 1.6 | 0.906 | 0.55 | 1.4 | 0.00 % |
| `клас210х210х3` | 3 | 0.904 | 0.59 | 1.2 | 0.906 | 0.57 | 1.2 | 0.00 % |
| `клас210х210х4ц` | 4 | 0.904 | 0.55 | 1.6 | 0.906 | 0.55 | 1.4 | 0.00 % |
| `клас210ема4` | 4 | 0.904 | 0.61 | 1.2 | 0.906 | 0.61 | 1.1 | 0.00 % |
| `клас210ема4ц` | 4 | 0.904 | 0.44 | 1.2 | 0.906 | 0.44 | 1.1 | 0.00 % |
| `клас210х4бема10х10х130` | 52 | 0.904 | 0.40 | 1.6 | 0.906 | 0.40 | 1.5 | 0.00 % |
| `клас210х10х9` | 9 | 0.904 | 0.45 | 5.1 | 0.906 | 0.45 | 3.7 | 0.00 % |
| `клас210х10х7` | 7 | 0.904 | 0.31 | 3.8 | 0.906 | 0.32 | 2.8 | 0.00 % |
| `клас210х10х7дц` | 7 | 0.900 | 0.24 | 2.6 | 0.906 | 0.24 | 2.6 | 0.33 % |
| `клас210х10-1х7дц` | 7 | 0.898 | 0.24 | 2.8 | 0.902 | 0.24 | 2.6 | 0.50 % |
| `клас210х10-1х3+1дц` | 4 | 0.898 | 0.57 | 2.2 | 0.902 | 0.57 | 2.5 | 0.50 % |
| `клас210х10-1х2д` | 2 | 0.898 | 0.14 | 3.3 | 0.902 | 0.15 | 2.9 | 0.50 % |
| `клас210х10-1х7д/д` | 7 | 0.898 | 0.17 | 3.3 | 0.902 | 0.17 | 2.9 | 0.50 % |

- Нормування на свою ногу тримає вхід у межах ~1 для всіх класів; вище
  3 виходять тільки класи з сирими рівнями й обʼємами (`клас210х8`,
  `клас210х10х9`, `клас210х14х4`, `клас210х14х5`, `клас210х10х7`).
- Записи без значення є лише в похідних класах — це записи після
  розриву ряду; `клас210х10х7дц` закриває частину з них правилом
  першого блоку.
- `клас210х10-1х7д/д` збігається з етапом 3 біт у біт: записи, сира
  канва, маска й вхід мережі.

## Етап 5. Початок і кінець ноги на канві — `підтверджено`, 2026-09-23

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

### Промпт

Порахувати між усіма ногами, чи є кореляція між фічами перших 5 % ноги
(канва входу 500 рядків × кількість фіч) і між фічами останніх 5 % —
тобто чи є щось, що відрізняє початок ноги від її кінця, і чи залежить
ця різниця від фіч. Обчислення — в окремій плашці, що розгортається, з
вибором класу перетворення зверху. Далі — UMAP усіх ніг: зростаючі
зелені, падаючі червоні, у тій самій плашці; розрахунки подавати
лаконічно.

### Метод

1. Вхід — канва етапу 4 кожного з 18 класів, вхід мережі (`net`, після
   нормування на свою ногу). Нога береться, коли живих рядків ≥ 90 %
   канви: 1027–1028 ніг train і 293–298 ніг val.
2. **Голова** — перші 25 **живих** рядків канви (5 % з 500), **хвіст** —
   останні 25 живих. Вікно — `25 × F` чисел одним вектором. Мертві
   рядки у вікна не йдуть: у похідних класів перший рядок канви завжди
   мертвий, і нулі на одному місці робили б голови схожими формально.
3. Кореляція Пірсона кожного вікна з вікнами **інших** ніг (пари однієї
   ноги не беруть участі): `гг` — голови між собою, `хх` — хвости між
   собою, `гх` — голова однієї ноги з хвостом іншої.
   **Розрив** = `(гг + хх) / 2 − гх`.
4. **Влучність** — вікно відноситься до голів, коли його сума кореляцій
   з головами інших ніг більша, ніж з їхніми хвостами; частка правильних.
5. **Підлога** — 200 перемішувань (seed 35): у кожній нозі голова й
   хвіст міняються місцями з імовірністю ½; 95-й перцентиль розриву й
   влучності.
6. **Контроль** — той самий розрив на двох вікнах із середини ноги (з
   30-го і 65-го відсотка живих рядків): метод без упередження дає тут
   підлогу.
7. По кожній фічі окремо — те саме на вікні `25 × 1` і **AUC рівня**
   (середнє вікна голови проти хвоста) з межею `0.5 ± 95-й перцентиль
   |AUC − 0.5|` на перемішуваннях.
8. Групи ніг: усі, вгору, вниз — train і val окремо.

Скрипт: [`scripts/head_tail30.py`](scripts/head_tail30.py) →
[`results/head_tail30.json`](results/head_tail30.json). Тести:
[`tests/test_head_tail30.py`](../../tests/test_head_tail30.py) — вікна по
живих рядках, шум лишається на підлозі, закладений візерунок голови
знаходиться, своя нога не голосує, контроль, збирач.

```bash
python3 research/35-leg-fullness/scripts/head_tail30.py
```

### Результат 7: голова проти хвоста

Влучність усі / вгору / вниз, жирне — вище підлоги; розрив на val; контроль
val — розрив / підлога на двох серединах.

| Клас | Ніг train / val | Влучність train | Влучність val | Розрив val вгору / вниз | Контроль val вгору | Контроль val вниз |
| ---- | --------------- | --------------- | ------------- | ----------------------- | ------------------ | ----------------- |
| `клас210х8` | 1028 / 298 | **0.81** / **0.82** / **0.81** | **0.82** / **0.82** / **0.81** | 0.1311 / 0.1010 | 0.0026 / 0.0020 | 0.0000 / 0.0016 |
| `клас210х14х4` | 1028 / 298 | **0.76** / **0.77** / **0.80** | **0.79** / **0.83** / **0.83** | 0.0429 / 0.0385 | 0.0030 / 0.0022 | -0.0004 / 0.0019 |
| `клас210х14х2` | 1028 / 298 | 0.49 / **0.68** / **0.72** | **0.55** / **0.72** / **0.74** | 0.0311 / 0.0294 | 0.0033 / 0.0028 | -0.0018 / 0.0025 |
| `клас210х14х5` | 1028 / 298 | **0.68** / **0.70** / **0.75** | **0.72** / **0.72** / **0.63** | 0.0243 / 0.0173 | 0.0031 / 0.0024 | -0.0014 / 0.0023 |
| `клас210х14х3` | 1028 / 298 | 0.49 / **0.67** / **0.72** | 0.54 / **0.74** / **0.72** | 0.0258 / 0.0257 | 0.0032 / 0.0030 | -0.0020 / 0.0024 |
| `клас210х14х4ц` | 1028 / 298 | 0.51 / **0.74** / **0.75** | 0.52 / **0.78** / **0.76** | 0.0359 / 0.0310 | 0.0029 / 0.0027 | -0.0015 / 0.0023 |
| `клас210х210х3` | 1028 / 298 | 0.49 / **0.67** / **0.72** | 0.54 / **0.74** / **0.72** | 0.0258 / 0.0257 | 0.0032 / 0.0030 | -0.0020 / 0.0024 |
| `клас210х210х4ц` | 1028 / 298 | 0.51 / **0.74** / **0.75** | 0.52 / **0.78** / **0.76** | 0.0359 / 0.0310 | 0.0029 / 0.0027 | -0.0015 / 0.0023 |
| `клас210ема4` | 1028 / 298 | 0.50 / **0.71** / **0.77** | 0.50 / **0.74** / **0.77** | 0.0452 / 0.0935 | 0.0010 / 0.0037 | 0.0207 / 0.0023 |
| `клас210ема4ц` | 1028 / 298 | 0.52 / **0.62** / **0.58** | 0.43 / 0.54 / **0.56** | 0.0174 / 0.0256 | 0.0066 / 0.0071 | 0.0434 / 0.0042 |
| `клас210х4бема10х10х130` | 1028 / 298 | 0.51 / **0.63** / **0.59** | 0.49 / **0.57** / **0.60** | 0.0451 / 0.0545 | 0.0030 / 0.0066 | 0.0096 / 0.0076 |
| `клас210х10х9` | 1028 / 298 | **0.79** / **0.82** / **0.77** | **0.78** / **0.82** / **0.78** | 0.0854 / 0.0672 | 0.0015 / 0.0011 | -0.0000 / 0.0008 |
| `клас210х10х7` | 1028 / 298 | **0.74** / **0.81** / **0.69** | **0.80** / **0.78** / **0.71** | 0.0366 / 0.0258 | 0.0020 / 0.0012 | -0.0000 / 0.0010 |
| `клас210х10х7дц` | 1028 / 298 | **0.68** / **0.85** / **0.87** | **0.66** / **0.87** / **0.86** | 0.0379 / 0.0351 | 0.0011 / 0.0021 | -0.0008 / 0.0016 |
| `клас210х10-1х7дц` | 1027 / 293 | **0.66** / **0.84** / **0.85** | **0.61** / **0.86** / **0.83** | 0.0359 / 0.0265 | 0.0002 / 0.0019 | -0.0013 / 0.0019 |
| `клас210х10-1х3+1дц` | 1027 / 293 | 0.50 / **0.76** / **0.79** | 0.49 / **0.79** / **0.79** | 0.0241 / 0.0197 | -0.0003 / 0.0020 | 0.0001 / 0.0025 |
| `клас210х10-1х2д` | 1027 / 293 | **0.72** / **0.71** / **0.74** | **0.73** / **0.73** / **0.74** | 0.0310 / 0.0345 | 0.0004 / 0.0022 | -0.0007 / 0.0022 |
| `клас210х10-1х7д/д` | 1027 / 293 | **0.72** / **0.83** / **0.85** | **0.71** / **0.87** / **0.85** | 0.0490 / 0.0413 | 0.0002 / 0.0014 | 0.0021 / 0.0018 |

- **Початок ноги відрізняється від її кінця**, коли ноги взяти нарізно
  за напрямком: у 17 класах із 18 влучність вище підлоги на train і val
  в обох напрямках — 0.56–0.87 на val проти підлоги ~0.55. Виняток —
  `клас210ема4ц` на val вгору (0.54). Найсильніше — похідні класи:
  `клас210х10х7дц`, `клас210х10-1х7дц`, `клас210х10-1х7д/д` дають
  0.83–0.87.
- **Разом ноги вгору й вниз гасять одна одну** в класах із нормованими
  різницями (`х14х2`, `х14х3`, `х14х4ц`, `х210х…`, ЕМА-класи,
  `клас210х10-1х3+1дц`): влучність на рівні 0.5 — ці фічі мають знак
  напрямку, і голова ноги вгору дзеркальна голові ноги вниз.
- **Різниця залежить від фіч.** На `клас210х10-1х7д/д` її несуть
  `d_price_avg` (0.82–0.89), `d_support_sell`, `d_down`, `d_up`,
  `d_resist_buy` (0.65–0.72); `d_bal_*` — слабше (0.55–0.64). На
  рівнях стакана (`клас210х8`, `клас210х10х9`) — `resist_plus/minus`,
  `support_plus/minus`, `vol_buy`, `vol_sell` (0.74–0.85), а
  `const_resist`, `const_support` не проходять підлогу на всіх
  групах. Сире `price_avg`, `ema_price` і `price_balance`
  розділяють голову й хвіст за визначенням ноги (ціна в голові нижча, ніж
  у хвості ноги вгору), це не знахідка.
- **Контроль**: розрив на двох серединах ноги — до 0.0033 у класах без
  ЕМА, у 10–50 разів менший за розрив голови й хвоста тих самих ніг
  (0.017–0.131); подекуди він трохи вище підлоги (на val вгору), але на
  порядок нижче справжнього розриву. У ЕМА-класах (`клас210ема4`,
  `клас210ема4ц`, `клас210х4бема10х10х130`) контроль сам помітно вище
  підлоги (до 0.043): памʼять ЕМА тягнеться через ногу, і на них розрив
  читається з поправкою.

### UMAP ніг

Кожна нога — точка, вгору зелена, вниз червона, насиченість — рух ноги.
Три карти поруч: **нога** (уся канва `500 × F`), **голова**, **хвіст** (по
25 живих рядків) — ті самі ноги, що в заміру. UMAP 2д, три метрики
(`cosine` за замовчуванням, `euclidean`, `manhattan`), `n_neighbors 15`,
`min_dist 0.1`, seed 42 на всіх картах; train і val окремо; наведення на
точку показує ногу, напрямок, рух і час.

Скрипт: [`scripts/umap_legs30.py`](scripts/umap_legs30.py) →
`results/umap_legs30/<клас>.json`.

```bash
python3 research/35-leg-fullness/scripts/umap_legs30.py
```

### Результат 8: UMAP ніг

Частка 15 найближчих сусідів на карті `cosine` з тим самим напрямком ноги
(0.5 — вгору й униз перемішані):

| Клас | train: нога / голова / хвіст | val: нога / голова / хвіст |
| ---- | ---------------------------- | -------------------------- |
| `клас210х8` | 0.53 / 0.52 / 0.52 | 0.53 / 0.51 / 0.50 |
| `клас210х14х4` | 0.51 / 0.62 / 0.52 | 0.53 / 0.53 / 0.57 |
| `клас210х14х2` | 0.61 / 0.74 / 0.71 | 0.61 / 0.68 / 0.73 |
| `клас210х14х5` | 0.58 / 0.64 / 0.69 | 0.58 / 0.54 / 0.73 |
| `клас210х14х3` | 0.66 / 0.72 / 0.73 | 0.60 / 0.61 / 0.76 |
| `клас210х14х4ц` | 0.69 / 0.81 / 0.81 | 0.60 / 0.79 / 0.84 |
| `клас210х210х3` | 0.66 / 0.72 / 0.73 | 0.60 / 0.61 / 0.76 |
| `клас210х210х4ц` | 0.69 / 0.81 / 0.81 | 0.60 / 0.79 / 0.84 |
| `клас210ема4` | 0.79 / 0.83 / 0.60 | 0.76 / 0.78 / 0.63 |
| `клас210ема4ц` | 0.93 / 0.91 / 0.85 | 0.86 / 0.84 / 0.86 |
| `клас210х4бема10х10х130` | 0.95 / 0.93 / 0.86 | 0.89 / 0.85 / 0.92 |
| `клас210х10х9` | 0.54 / 0.52 / 0.52 | 0.53 / 0.53 / 0.52 |
| `клас210х10х7` | 0.52 / 0.53 / 0.53 | 0.55 / 0.55 / 0.52 |
| `клас210х10х7дц` | 0.85 / 0.94 / 0.92 | 0.85 / 0.91 / 0.93 |
| `клас210х10-1х7дц` | 0.82 / 0.92 / 0.92 | 0.78 / 0.89 / 0.94 |
| `клас210х10-1х3+1дц` | 0.73 / 0.78 / 0.83 | 0.66 / 0.69 / 0.88 |
| `клас210х10-1х2д` | 0.51 / 0.50 / 0.51 | 0.53 / 0.50 / 0.50 |
| `клас210х10-1х7д/д` | 0.93 / 0.96 / 0.94 | 0.95 / 0.95 / 0.97 |

- Ноги вгору й униз розходяться на карті в класах, де є ціна або знак
  руху: похідні з `d_price_avg`, `price_balance`, ЕМА-класи — 0.8–0.97.
  Це напрямок ноги, записаний у самих фічах, а не прихована ознака.
- Без ціни (`клас210х8`, `клас210х10х9`, `клас210х10х7`, `клас210х10-1х2д`)
  карти перемішані — 0.50–0.57: рівні стакана й обʼємів напрямку ноги
  не кажуть.

## Етап 6. Частка ноги у вікні — `підтверджено`, 2026-09-24

### Промпт

Мережі подається вікно від підтвердженого півота до поточного запису
`t` — канва `500 × F` (зараз `клас210х10-1х7д/д`, F = 7, далі будь-який
клас етапу 4). Мережа каже, яку частку ноги подано: 10 % — `0.1`,
30 % — `0.3`; коли подано ногу і ще шматок наступної — число понад 1.
Три зони: нога неповна, нога повна (98–100 %), подано більше за ногу.
Виводяться **всі сирі значення голів**; формула, що зводить їх у
відповідь, підбирається потім.

### Ціль

`n` — записів у вікні від першого після півота, `L` — записів у нозі:

```
q = n / L
```

`q = 0.3` — подано 30 % ноги; `q = 1.0` — рівно нога; `q = 1.2` — вікно
на 20 % довше за ногу: нога скінчилась, і ще `0.2 · L` записів іде
наступна. Число понад 1 міряється довжиною **першої** ноги — його
видно з того, де в канві стоїть півот; довжина наступної ноги для нього
не потрібна.

### Заглядання вперед у вікнах

Вікна етапів 6 і 7 побудовані з розмітки заднім числом, тому мережа
отримує те, чого в живому часі ще немає:

- **Початок вікна — півот розмітки, а не підтверджений півот.** Промпт
  каже «від підтвердженого півота», а вікно береться від першого запису
  після півота зігзага 2.3 %, з частки `q = 0.05`. Живцем півот стає
  відомим лише після руху 2.3 % від нього — до того вікна з цим початком
  не існує. Мережі на кожному вікні сказано, де почалась нога.
- **Вікна `q > 1` тягнуться до 1.5 за наступний півот.** Живцем після
  підтвердження наступного півота відлік іде вже від нього — вікно від
  старого півота далі не подається.
- **Вікна розставлені за справжньою довжиною ноги**: 24 на ногу (18
  рівномірно по `q`, 6 біля кінця на `[0.9, 1.1]`), ряд val — кроком
  0.025 від справжньої `L`. Кількість вікон на ногу не залежить від її
  довжини, а біля кінця ноги вікон утричі густіше — живцем такого
  розподілу немає.

Через це числа етапів 6 і 7 (втрата, AUC сходинок і рядків, ряд val) —
не оцінка мережі в живому часі.

### Виходи мережі

**Вихід 1 — сходинки, 16 чисел від 0 до 1**: імовірність `P(q ≥ c)` на
кожному порозі

```
c = 0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  0.98  1.0⁺  1.1  1.2  1.3  1.4  1.5
```

`1.0⁺` — у вікні є хоча б один запис наступної ноги. Втрата — BCE на
кожному порозі. Сходинки не зростають зі зростанням `c`: на вікні
частки 0.3 відповідь схожа на `0.1→0.99, 0.2→0.95, 0.3→0.60,
0.4→0.15, 0.5→0.03, решта ≈ 0`.

**Вихід 2 — рядки канви, 500 чисел від 0 до 1**: імовірність, що рядок
належить уже **наступній** нозі. Усі близько 0 — розвороту у вікні
немає; стрибок на 1 з рядка `r` — там півот.

Обидва виходи пишуться **сирими**, усі 516 чисел на вікно. Як із них
складаються частка, три зони й узгодженість двох виходів — вирішується
після навчання, на збережених видачах.

Кодер — «згортка + увага» етапу 2 з меншим кроком згорток, щоб вихід 2
мав роздільність рядка; кількість каналів входу береться з класу.
Нормування — тільки на вікно (розділ «Правила дослідження»). Контроли
етапу 2: «хвіст 16 рядків», лічильник, константа; відкладений шматок —
за **ногами**.

### Крок 1: чи корелюють ноги по рівних частках і по надлишках

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

1. **Вікна** — для кожної ноги 16 часток `q` зі сходинок (`q = 1.0` —
   рівно нога); `n = round(q · L)` записів від першого після півота.
   `q > 1` бере наступну ногу впритул і не виходить за її кінець,
   інакше цієї частки в нозі немає; в останньої ноги частини надлишків
   немає.
2. **Канва вікна** — хід етапу 3 на самому вікні: 500 відрізків, клас,
   нормування на вікно.
3. **Кореляція Пірсона між вікнами різних ніг** — по числах, живих в
   **обох** вікнах: у короткого вікна голова канви мертва, і нулі на
   одному місці робили б короткі вікна схожими формально. Вікно
   стандартизоване по своїх живих числах. Пари однієї ноги не беруть
   участі; пара з менш ніж 50 спільних живих рядків дає 0.
4. **Матриця `M[a, b]`** — середня кореляція вікон частки `a` з
   вікнами частки `b` інших ніг. **Розрив частки** `a` =
   `M[a, a] − середнє M[a, b ≠ a]`. Звід — середній розрив по рівних
   частках (`q < 1`), по надлишках (`q > 1`) і по всіх.
5. **Влучність** — вікну дається частка, з вікнами якої в інших ногах у
   нього найбільша середня кореляція: точна частка (випадково 1/16),
   сусідня (±1 щабель), три зони і середня похибка `|q̂ − q|`.
6. **Підлога** — 200 перемішувань (seed 36): у кожній нозі частки її
   вікон переставляються між собою. 95-й перцентиль розриву й
   влучності, 5-й — похибки.
7. Групи ніг: усі, вгору, вниз (напрямок першої ноги вікна). Train і
   val окремо.

Скрипт: [`scripts/windows30.py`](scripts/windows30.py) →
`data/bars30/windows_<part>_<клас>.npz` (канви вікон — вхід для
мережі), [`results/windows30.json`](results/windows30.json). Тести:
[`tests/test_windows30.py`](../../tests/test_windows30.py) — довжина
вікна й межа наступної ноги, нормування тільки на вікно (секунди після
`t` канву не міняють), кореляція по спільних живих рядках, своя нога не
голосує, шум на підлозі, закладений візерунок частки знаходиться,
збирач.

```bash
python3 research/35-leg-fullness/scripts/windows30.py
python3 research/35-leg-fullness/scripts/windows30.py --classes x10m1x7dd x10x9
```

### Результат 9: рівні частки не корелюють, надлишки — корелюють

`клас210х10-1х7д/д`. Жирне — вище підлоги (у дужках); похибка — нижче
підлоги. Розрив — у кореляції Пірсона.

| Частина | Ноги | Ніг | Вікон | Розрив рівних | Розрив надлишків | Точна частка | ±1 щабель | Три зони | Похибка |
| ------- | ---- | --: | ----: | ------------- | ---------------- | ------------ | --------- | -------- | ------- |
| train | усі | 1210 | 18310 | **0.0002** (0.0002) | **0.0057** (-0.0001) | **0.17** (0.07) | **0.29** (0.19) | 0.47 (0.50) | **0.42** (0.45) |
| train | вгору | 605 | 9196 | **0.0005** (0.0003) | **0.0095** (-0.0002) | **0.27** (0.07) | **0.36** (0.19) | 0.51 (0.51) | **0.37** (0.45) |
| train | вниз | 605 | 9114 | **0.0003** (0.0002) | **0.0116** (-0.0001) | **0.25** (0.07) | **0.33** (0.19) | 0.47 (0.49) | **0.44** (0.46) |
| val | усі | 352 | 5228 | **0.0005** (0.0005) | **0.0042** (-0.0006) | **0.16** (0.07) | **0.29** (0.20) | 0.49 (0.52) | **0.40** (0.44) |
| val | вгору | 176 | 2539 | **0.0015** (0.0007) | **0.0090** (-0.0006) | **0.26** (0.07) | **0.37** (0.20) | **0.54** (0.53) | **0.34** (0.44) |
| val | вниз | 176 | 2689 | **0.0004** (0.0002) | **0.0113** (0.0001) | **0.25** (0.07) | **0.33** (0.19) | 0.46 (0.47) | **0.44** (0.47) |

Розрив кожної частки (жирне — вище підлоги):

| q | train усі | train вгору | train вниз | val усі | val вгору | val вниз |
| - | --------- | ----------- | ---------- | ------- | --------- | -------- |
| 0.1 | -0.0060 | -0.0078 | -0.0046 | -0.0048 | -0.0044 | -0.0045 |
| 0.2 | -0.0023 | -0.0033 | -0.0013 | -0.0013 | 0.0003 | -0.0019 |
| 0.3 | -0.0003 | -0.0004 | 0.0000 | -0.0001 | 0.0007 | -0.0005 |
| 0.4 | **0.0006** | **0.0009** | **0.0008** | 0.0005 | 0.0009 | 0.0003 |
| 0.5 | **0.0010** | **0.0014** | **0.0010** | **0.0011** | 0.0012 | **0.0011** |
| 0.6 | **0.0014** | **0.0017** | **0.0013** | **0.0013** | **0.0018** | **0.0014** |
| 0.7 | **0.0015** | **0.0021** | **0.0013** | **0.0020** | **0.0024** | **0.0021** |
| 0.8 | **0.0018** | **0.0022** | **0.0017** | **0.0017** | **0.0024** | **0.0018** |
| 0.9 | **0.0020** | **0.0033** | **0.0014** | **0.0021** | **0.0033** | **0.0018** |
| 0.98 | **0.0028** | **0.0053** | **0.0017** | **0.0028** | **0.0062** | **0.0022** |
| 1 | **0.0047** | **0.0103** | **0.0057** | **0.0040** | **0.0110** | **0.0053** |
| 1.1 | **0.0062** | **0.0117** | **0.0106** | **0.0046** | **0.0118** | **0.0099** |
| 1.2 | **0.0056** | **0.0093** | **0.0113** | **0.0037** | **0.0096** | **0.0098** |
| 1.3 | **0.0055** | **0.0089** | **0.0117** | **0.0038** | **0.0087** | **0.0112** |
| 1.4 | **0.0057** | **0.0090** | **0.0123** | **0.0044** | **0.0082** | **0.0122** |
| 1.5 | **0.0053** | **0.0085** | **0.0123** | **0.0045** | **0.0066** | **0.0135** |

- **Кореляція між ногами взагалі слабка**: `M` на train від 0.018 до
  0.040. Канви різних ніг майже незалежні.
- **Рівні частки між собою не корелюють.** Розрив по `q < 1` — 0.0002–
  0.0015, на рівні підлоги. На `q` = 0.1 і 0.2 він **відʼємний**: коротке
  вікно, розтягнуте на 500 рядків, менше схоже навіть на таке саме
  коротке вікно, ніж на довші. Від 0.4 до 0.9 розрив росте, але це
  тисячні частки кореляції. За кореляцією канви префікс 20 % ноги від
  префікса 50 % не відрізняється.
- **Надлишки корелюють.** Розрив по `q > 1` у 10–40 разів більший
  (0.004–0.013) і тримається на train і val в обох напрямках. Те саме
  на `q` = 1.0 (0.004–0.011): вікно, що кінчається на півоті. Спільне
  в них одне — **півот стоїть на тому самому рядку канви**, `500 / q`.
  Однакова частка надлишку — однакове місце розвороту, і це Пірсон
  бачить.
- **Влучність за найближчою часткою** — 0.16–0.27 проти 0.07; похибка
  `|q̂ − q|` 0.34–0.44 проти 0.44–0.47. Виграш дають вікна `q ≥ 0.98`.
  Три зони — на підлозі: по сирій кореляції вікно «неповне» від
  «повного» не відрізняється.
- **Напрямок ноги знову розділяє**: нарізно вгору й униз розрив
  надлишків і точна частка вищі, ніж на всіх разом, як і в етапі 5.

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

- У форматі канви сам по собі несе частку тільки **півот усередині
  вікна**, тобто вихід 2 мережі. Ноги по рівних частках `q < 1` лінійно
  не схожі: сходинкам мережі нижче 0.98 нема за що зачепитися в сирій
  кореляції, і цю частку їй доведеться брати або з нелінійних ознак,
  або з кінця вікна, як в етапі 2.

### Крок 2: мережа і грід

**Розбиття**: train — ноги до 2026-08-14, val — 14.08 … 22.09 (22.09 —
остання повна доба робочої копії). Відкладений шматок — 20 % **ніг**
train (seed 0); епоха й прогін гріда вибираються за найменшою втратою
на ньому, val у виборі не бере участі.

**Вікна для навчання** ([`scripts/fullness_data30.py`](scripts/fullness_data30.py),
seed 6): на кожну ногу 24 вікна від першого запису після півота —
18 з часткою `q` рівномірно на `[0.05, q_max]` і 6 біля кінця ноги,
`q` на `[0.9, 1.1]`. `q_max` = 1.5, але вікно не виходить за кінець
наступної ноги; в останньої ноги частини `q_max` = 1. `n = max(2,
round(q · L))`, мітка `q = n / L`. Канва — `windows30.window_canvas`
(нормування тільки на вікно).

- **Мітка рядка** — 1, коли середина відрізка рядка лежить не раніше
  першої секунди наступної ноги; мертві рядки — під маскою.
- **Скаляри** (причинні): `log1p(n)`, `log1p(крок відрізка, с)`, рух
  ціни від першого запису вікна до останнього, частка живих рядків.

| Частина | Ніг | Вікон | `q > 1` | `0.98 ≤ q ≤ 1` | Рядків наступної ноги |
| ------- | --: | ----: | ------: | -------------: | --------------------: |
| train | 1210 | 29 040 | 0.34 | 0.04 | 0.045 |
| val | 352 | 8 448 | 0.32 | 0.05 | 0.043 |
| ряд val | 352 | 19 644 | — | — | — |

**Мережа** ([`scripts/fullness_net30.py`](scripts/fullness_net30.py)):

```
(F каналів + маска) -> DOWN блоків Conv1d(5, stride 2) + GroupNorm + GELU
                    -> T = 500 / 2^DOWN кроків × WIDTH + позиція
                    -> LAYERS шарів уваги, 4 голови
   вихід 1: увага-пулінг + 4 скаляри -> 64 -> 16 логітів   P(q ≥ c)
   вихід 2: кожен крок -> 1 логіт -> лінійно до 500 рядків  P(рядок — наступна нога)
```

Втрата: `BCE(сходинки)` — середнє по 16 порогах, `+ row_weight ·
BCE(рядки)` по живих рядках. Adam, батч 64, seed 0, `mps`; канва
читається memmap.

**Базові рівні** — лічильник `n / медіана L` ніг навчання: для
сходинок — AUC самого `n`, для рядків — півот на рядку
`500 · медіана / n`.

**Грід**:

| Коло | Що міняється | Від чого |
| ---- | ------------ | -------- |
| 1 | ширина {64, 128} × глибина згортки {2, 3} × шари уваги {1, 2} — 8 прогонів | lr 1e-3, dropout 0, row_weight 1, 12 епох |
| 2 | по одній ручці: lr {3e-4, 3e-3}, dropout 0.1, row_weight {0.3, 3} | кращий кола 1 |
| 3 | тривалість: 30 епох | кращий колів 1–2 |
| контроль | «хвіст 16 рядків» | фінальна |

Епоха всередині прогону вибирається за його власною відкладеною
втратою; прогони між собою — за **незваженою** відкладеною втратою
`BCE(сходинки) + BCE(рядки)`: з вагою рядків у сумі прогін з малою
`row_weight` вигравав би сам себе.

**Фінальний прогін** — обрана конфігурація заново, на дві епохи довше
за її найкращу; ваги кожної епохи зберігаються, і лишаються найкраща
(за відкладеною втратою) і по дві з кожного боку:
`models/fullness30_v1/epoch_<NN>.pt`, найкраща ще й `model.pt`,
налаштування — `config.json`. Для кожної з цих епох:

- числа val кожної монети і всіх разом —
  `results/fullness30/final_epochs.json`;
- сирі виходи на вікнах val (16 логітів і 500 логітів на вікно) —
  `results/fullness30/final_outputs_val_e<NN>.npz`;
- **ряд val** — вікна кожної ноги val від її півота з часткою від 0.025
  до стелі кроком 0.025 (`fullness_data30.py --series`, 19 644 вікна):
  сходинки ймовірностями і рядки — середня ймовірність у 50 клітинках
  по 10 рядків, `results/fullness30/series_val_e<NN>.npz`. На кожному
  записі ряду видно два вікна: від поточного півота (`q ≤ 1`) і від
  попереднього (`q > 1`).

```bash
python3 research/35-leg-fullness/scripts/fullness_data30.py
python3 research/35-leg-fullness/scripts/fullness_data30.py --series
python3 research/35-leg-fullness/scripts/fullness_net30.py --grid 1
python3 research/35-leg-fullness/scripts/fullness_net30.py --grid 2
python3 research/35-leg-fullness/scripts/fullness_net30.py --grid 3
python3 research/35-leg-fullness/scripts/fullness_net30.py --final
python3 research/35-leg-fullness/scripts/fullness_net30.py --tail 16
```

### Результат 10: грід

Незважена відкладена втрата `BCE(сходинки) + BCE(рядки)`; AUC
сходинок — середнє по 16 порогах; у дужках — лічильник. Відкладені —
242 ноги, 5 808 вікон; val — 352 ноги, 8 448 вікон.

| Коло | Ширина | Згортка | Увага | lr | dropout | Вага рядків | Епоха | Відкл. втрата | Відкл. AUC сходинок | Відкл. AUC рядків | Val AUC сходинок | Val AUC рядків |
| ---- | -----: | ------: | ----: | -: | ------: | ----------: | ----: | ------------: | ------------------: | ----------------: | ---------------: | -------------: |
| 1 | 64 | 2 | 1 | 1e-3 | 0 | 1 | 8 | 0.4473 | 0.821 | 0.956 | 0.731 | 0.947 |
| 1 | 64 | 2 | 2 | 1e-3 | 0 | 1 | 3 | 0.4590 | 0.785 | 0.946 | 0.738 | 0.938 |
| 1 | 64 | 3 | 1 | 1e-3 | 0 | 1 | 3 | 0.4639 | 0.799 | 0.946 | 0.746 | 0.940 |
| 1 | 64 | 3 | 2 | 1e-3 | 0 | 1 | 7 | 0.4453 | 0.832 | 0.963 | 0.726 | 0.948 |
| 1 | 128 | 2 | 1 | 1e-3 | 0 | 1 | 6 | 0.4465 | 0.810 | 0.953 | 0.734 | 0.940 |
| 1 | 128 | 2 | 2 | 1e-3 | 0 | 1 | 5 | 0.4297 | 0.828 | 0.966 | 0.735 | 0.955 |
| 1 | 128 | 3 | 1 | 1e-3 | 0 | 1 | 5 | 0.4608 | 0.808 | 0.949 | 0.720 | 0.939 |
| 1 | 128 | 3 | 2 | 1e-3 | 0 | 1 | 2 | 0.4635 | 0.790 | 0.944 | 0.739 | 0.936 |
| 2 | 128 | 2 | 2 | 3e-4 | 0 | 1 | 4 | 0.4375 | 0.830 | 0.961 | 0.734 | 0.950 |
| 2 | 128 | 2 | 2 | 3e-3 | 0 | 1 | 8 | 0.4485 | 0.821 | 0.956 | 0.715 | 0.946 |
| 2 | 128 | 2 | 2 | 1e-3 | 0.1 | 1 | 5 | 0.4370 | 0.831 | 0.967 | 0.744 | 0.956 |
| 2 | 128 | 2 | 2 | 1e-3 | 0 | 0.3 | 5 | 0.4592 | 0.817 | 0.948 | 0.710 | 0.936 |
| **2** | **128** | **2** | **2** | **1e-3** | **0** | **3** | **6** | **0.4245** | **0.825** | **0.968** | **0.781** | **0.960** |
| 3 | те саме, 30 епох | | | | | | 6 | 0.4245 | 0.825 | 0.968 | 0.781 | 0.960 |

Лічильник: відкладені — AUC сходинок 0.683, рядків 0.712; val — 0.616
і 0.639. Val у виборі не бере участі.

- Обрано **ширина 128, згортка 2 (125 кроків), 2 шари уваги, lr 1e-3,
  dropout 0, вага рядків 3**.
- **Коло 3**: на 30 епохах найкраща та сама епоха 6; далі відкладена
  втрата (зважена) росте з 0.604 до 1.13 — мережа вчить ноги train
  напамʼять, як і в етапі 2.

### Результат 11: фінальна модель на val

Епоха 6 (найкраща за відкладеною втратою). Train 968 ніг / 23 232
вікна, відкладені 242 / 5 808, val 352 / 8 448.

| Шматок | AUC сходинок | лічильник | AUC рядків | лічильник |
| ------ | -----------: | --------: | ---------: | --------: |
| train | 0.897 | 0.670 | 0.985 | 0.713 |
| відкладені | 0.825 | 0.683 | 0.968 | 0.712 |
| **val** | **0.781** | 0.616 | **0.960** | 0.639 |

Кожна сходинка на val:

| Поріг c | 0.1 | 0.2 | 0.3 | 0.4 | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 0.98 | 1.0+ | 1.1 | 1.2 | 1.3 | 1.4 | 1.5 |
| ------- | --: | --: | --: | --: | --: | --: | --: | --: | --: | ---: | ---: | --: | --: | --: | --: | --: |
| мережа | 0.845 | 0.828 | 0.826 | 0.804 | 0.782 | 0.769 | 0.765 | 0.747 | 0.738 | 0.714 | 0.752 | 0.794 | 0.828 | 0.831 | 0.817 | 0.660 |
| лічильник | 0.795 | 0.762 | 0.744 | 0.714 | 0.688 | 0.675 | 0.667 | 0.645 | 0.638 | 0.593 | 0.621 | 0.582 | 0.575 | 0.558 | 0.538 | 0.068 |
| частка `q ≥ c` | 0.974 | 0.919 | 0.861 | 0.801 | 0.745 | 0.690 | 0.633 | 0.577 | 0.521 | 0.374 | 0.320 | 0.162 | 0.115 | 0.075 | 0.035 | 0.001 |

Епохи 4–8 по монетах val (найкраща — 6):

| Епоха | Монета | Ніг | Вікон | AUC сходинок | лічильник | AUC 0.98 | AUC 1.0+ | AUC рядків | лічильник |
| ----: | ------ | --: | ----: | -----------: | --------: | -------: | -------: | ---------: | --------: |
| 4 | усі | 352 | 8448 | 0.781 | 0.616 | 0.703 | 0.742 | 0.956 | 0.639 |
| 5 | усі | 352 | 8448 | 0.754 | 0.616 | 0.694 | 0.737 | 0.959 | 0.639 |
| **6** | **усі** | 352 | 8448 | **0.781** | 0.616 | **0.714** | **0.752** | **0.960** | 0.639 |
| 7 | усі | 352 | 8448 | 0.769 | 0.616 | 0.710 | 0.758 | 0.961 | 0.639 |
| 8 | усі | 352 | 8448 | 0.741 | 0.616 | 0.700 | 0.745 | 0.960 | 0.639 |
| 6 | BTC_USDT | 26 | 624 | 0.833 | 0.681 | 0.754 | 0.789 | 0.966 | 0.801 |
| 6 | ETH_USDT | 40 | 960 | 0.833 | 0.693 | 0.786 | 0.804 | 0.970 | 0.728 |
| 6 | SOL_USDT | 56 | 1344 | 0.799 | 0.676 | 0.732 | 0.753 | 0.963 | 0.678 |
| 6 | XRP_USDT | 100 | 2400 | 0.752 | 0.611 | 0.691 | 0.735 | 0.959 | 0.582 |
| 6 | BNB_USDT | 34 | 816 | 0.821 | 0.718 | 0.733 | 0.749 | 0.962 | 0.717 |
| 6 | DOGE_USDT | 96 | 2304 | 0.774 | 0.632 | 0.691 | 0.746 | 0.953 | 0.634 |

Решта епох по монетах — у `results/fullness30/final_epochs.json` і на
вкладці.

- **Мережа бʼє лічильник на кожній сходинці** і на кожній монеті val.
  Різниця найбільша там, де лічильник безсилий: надлишки 1.1–1.4 —
  0.79–0.83 проти 0.54–0.58.
- **Найслабше — межа ноги**: `0.98` — 0.714, `1.0+` — 0.752. Це саме
  три зони «неповна / повна / більше».
- **Рядки** — 0.960 проти 0.639: де в канві стоїть півот, мережа бачить
  майже завжди, коли він у вікні є. Це узгоджується з кроком 1: надлишки
  корелюють через місце півота в канві.
- `1.5` на val — 0.1 % вікон (близько восьми), число на ній не
  читається.
- **Перенавчання**: train 0.897 проти val 0.781; після 6-ї епохи val
  AUC сходинок падає (0.769, 0.741), рядки тримаються.

**Контроль «хвіст 16 рядків»** — та сама конфігурація, 8 епох, мережі
лишено тільки останні 16 рядків канви вікна:

| Шматок | AUC сходинок | повна канва | лічильник | AUC 0.98 | AUC 1.0+ | AUC 0.5 | AUC 1.2 |
| ------ | -----------: | ----------: | --------: | -------: | -------: | ------: | ------: |
| відкладені | 0.728 | 0.825 | 0.683 | 0.672 | 0.659 | 0.766 | 0.650 |
| val | 0.682 | 0.781 | 0.616 | 0.606 | 0.625 | 0.711 | 0.647 |

- **Тут повна канва бʼє хвіст** на 0.10 AUC сходинок на val — на відміну
  від етапу 2, де вся нога понад хвіст нічого не додавала. Різниця — в
  постановці: вікно, що заходить у наступну ногу, несе півот усередині
  канви, і його видно лише з усієї канви.
- AUC рядків хвоста (0.53) не читається як міра: рядки поза хвостом
  мережа не бачить, а міряються всі живі рядки.


## Етап 7. Уся канва без стиснення — `підтверджено`, 2026-09-29

### Промпт

> спробуй без стиснення і вдвічі … хай фічі йдуть як є … розгорнути всю
> матрицю в один вектор … грід параметрів на флет млп та млп-міксер —
> їхні можливості треба вивчити повністю … для повного вектора 500 × 7
> спробуй лстм та трансформер спроектувати та навчити — і зроби це в
> новій вкладці … без маски … якщо зробити 8 голів та 4 шари який буде
> результат? … викладеш на вкладку з графіком вал і для кожної ноги
> окремо

### Ціль

Обрана модель етапу 6 стискає канву 500 рядків згорткою з кроком 2 двічі
до 125 кроків уваги. Питання: чи губить щось це стиснення — і чи вміє
мережа, що бачить усі 500 × 8 чисел (7 фіч + маска) без стиснення,
більше.
Вікна ті самі, що в етапі 6, — з тим самим загляданням вперед (етап 6 →
«Заглядання вперед у вікнах»).

### Метод

Усе — на тих самих вікнах, відкладених ногах (20 % ніг train, seed 0),
втраті й мірах, що етап 6; база ручок — обрана модель (lr 1e-3, батч 64,
вага рядків 3, dropout 0). Кращий прогін кола — за незваженою втратою
на відкладених (сходинки + рядки з вагою 1). Скрипт —
[`scripts/fullness_net30.py`](scripts/fullness_net30.py).

| Коло | Мережа | Що перебирається | Епох |
| ---- | ------ | ---------------- | ---: |
| 4 | згортка + увага | згортка ×1 з кроком (250 кроків), згортка без кроку (500), сирі 500 × 8 прямо в увагу (ширина 8), вектор 4000 у MLP 4000 → 256 → 256 | 12 |
| 5 | MLP на вектор 4000 (`flat`) | ширина 64 / 128 / 256 × dropout 0 / 0.3 / 0.5 × weight decay 0 / 1e-2 / 1e-1 (AdamW), повним перебором | 20 |
| 6 | `flat` від кращого кола 5 | по одній: lr 3e-4 / 3e-3, шарів 1 / 3, dropout входу 0.1 / 0.3, канва 50 × 8 (середнє по 10 рядків) | 20 |
| 7 | MLP-Mixer | каналів на рядок 32 / 64 × приховане уздовж рядків 64 / 256 × шарів 2 / 4 × dropout 0 / 0.3, повним перебором | 20 |
| 8 | Mixer від кращого кола 7 | по одній: weight decay 1e-2 / 1e-1, lr 3e-4 / 3e-3, dropout входу 0.1, канва 50 × 8 | 20 |
| 9 | двонапрямний LSTM на 500 рядках | ширина 128 × 1 шар, 128 × 2, 256 × 1 | 10 |
| 10 | трансформер на 500 рядках | рядок → ширина лінійно, вивчена позиція, 2 шари по 4 голови; ширина 64 / 128 | 10 |
| 11 | без маски на вході | обрана модель (12 епох) і кращі LSTM, трансформер і Mixer кіл 9, 10, 8 — 8-й канал нулі, скаляр частки живих рядків нуль (з нього маску можна відновити); втрата рядків і далі по живих рядках | як у бази |
| 12 | трансформер на 500 рядках, глибший | ширина 64, 8 голів по 8 чисел, 4 шари; вхід — канва 500 × 7 **без стовпця маски** (рядок 7 → 64 лінійно), скаляри ті самі; батч 64 шматками по 16 | до перенавчання: стоп, коли 2 епохи поспіль відкладена втрата не краща за найкращу, стеля 50 |

- **MLP-Mixer**: рядок 8 → `c` каналів однією вагою на всі рядки; шар —
  MLP уздовж 500 рядків для кожного каналу (500 → приховане → 500, одна
  вага на всі канали) і MLP уздовж каналів для кожного рядка (`c` → 2`c`
  → `c`), обидва з LayerNorm і залишком; середнє по рядках + 4 скаляри →
  64 → 16 сходинок, кожен рядок → свій логіт.
- **LSTM** — `width / 2` на напрям; **трансформер** — norm first,
  ff = 2 · ширина. В обох вихід 1 — запит збирає увагою 500 рядків в
  один вектор + 4 скаляри → 64 → 16 сходинок, вихід 2 — кожен рядок →
  свій логіт.
- **Памʼять**: увага на 500 кроках при батчі 64 бере 2.3–2.4 GB MPS.
  Тому батч рахується шматками по 32 з одним кроком оптимізатора
  (`step_loss`: кожен шматок несе свою частку тієї самої суми — втрата
  й градієнт ті самі, різниця 7·10⁻⁷), прохід без градієнта — по 32
  вікна; пік — 1.25 GB.

Запуск:

```bash
python3 research/35-leg-fullness/scripts/fullness_net30.py --grid 4
python3 research/35-leg-fullness/scripts/fullness_net30.py --grid 5   # і 6, 7, 8, 9, 10, 11, 12 по черзі
python3 research/35-leg-fullness/scripts/fullness_net30.py --outputs g12_w64_d2_l4_lr0.001_do0_rw3_e50_t0_tf_h8_pt2_nc
```

Після кожної епохи повний стан прогону (ваги, оптимізатор, генератори
випадкових чисел, історія) лягає в `runs/<тег>.state.pt`: перерваний
прогін тією самою командою продовжує з наступної епохи з тими самими
числами, `--stop-after N` — пауза після епохи `N`.

Коло 12 пише ваги кожної епохи в `runs/<тег>_epochs/epoch_NN.pt`.
`--outputs <тег>` — видача найкращої епохи прогону і по дві до і після
на val для вкладки `/seq30`: `results/fullness30/seq30_<тег>.json`
(епохи, по кожній — втрата, AUC кожної сходинки проти лічильника і AUC
рядків, по кожній монеті й разом) і `seq30_<тег>_series_val_eNN.npz` (ряд val: вікна від півота з кроком 0.025
частки ноги, сходинки й рядки 50 клітинок по 10 — той самий хід, що
`series_val_eNN.npz` обраної моделі).

Прогін, що вже має `results/fullness30/<тег>.json`, пропускається;
`--grid 4 --only d1|d0|raw|flat` — один прогін кола 4. Ваги кращої
епохи кожного прогону — `data/fullness30/x10m1x7dd/runs/<тег>.pt`.

### Результат 12: без стиснення — не краще

Кращий прогін кожної мережі за відкладеними:

| Мережа | Параметрів | Епоха | Відкл. втрата | Відкл. AUC сходинок | Відкл. AUC рядків | Val втрата | Val AUC сходинок | Val AUC рядків |
| ------ | ---------: | ----: | ------------: | ------------------: | ----------------: | ---------: | ---------------: | -------------: |
| **обрана: згортка ×2 + увага, 125 кроків** | 445 тис. | 6 | **0.425** | **0.825** | 0.968 | 0.505 | 0.781 | 0.960 |
| згортка ×1 + увага, 250 кроків | 378 тис. | 6 | 0.437 | 0.826 | 0.962 | 0.513 | 0.749 | 0.956 |
| згортка без кроку + увага, 500 кроків | 410 тис. | 4 | 0.440 | 0.830 | 0.960 | 0.512 | 0.765 | 0.953 |
| сирі 500 × 8 в увагу, ширина 8 | 7 тис. | 10 | 0.465 | 0.813 | 0.936 | 0.517 | 0.730 | 0.925 |
| MLP 4000 → 256 → 256 (коло 4) | 1.24 млн | 1 | 0.497 | 0.763 | 0.936 | 0.540 | 0.706 | 0.926 |
| MLP, кращий кола 5 (ширина 64, dropout 0.5, wd 1e-1) | 298 тис. | 11 | 0.491 | 0.777 | 0.934 | 0.560 | 0.670 | 0.923 |
| MLP, канва 50 × 8 (коло 6) | 68 тис. | 20 | 0.456 | 0.813 | 0.952 | 0.520 | 0.744 | 0.941 |
| Mixer, кращий кола 7 (64 канали, приховане 256, 4 шари) | 1.10 млн | 5 | 0.463 | 0.781 | 0.951 | 0.542 | 0.746 | 0.941 |
| Mixer, lr 3e-3 (коло 8) | 1.10 млн | 6 | 0.439 | 0.825 | 0.957 | **0.494** | 0.774 | 0.947 |
| Mixer, канва 50 × 8 (коло 8) | 177 тис. | 6 | 0.429 | 0.810 | **0.970** | 0.499 | **0.789** | **0.963** |

- **Стиснення нічого не губить.** 250 і 500 кроків уваги на відкладених
  не кращі за 125, на val AUC сходинок нижчий (0.749, 0.765 проти
  0.781). Сирі фічі без згортки — ще нижче, але в них і ширина 8 (по 2
  числа на голову уваги) — ширина й згортка тут не розділені.
- **MLP на вектор 4000 перенавчається з першої епохи**: кожна позиція
  канви має свої ваги; dropout і weight decay зсувають відкладені на
  0.01, val від цього гіршає.
- **Стиснення допомагає й мережам без уваги**: канва 50 × 8 — кращий
  MLP і кращий Mixer. Mixer на ній — нарівні з обраною моделлю
  (відкладені 0.429 проти 0.425, val трохи краще на всіх трьох мірах) за
  1 прогін з 1 seed.

### Результат 13: LSTM і трансформер на 500 рядках, маска

Кола 9–10 — уся канва без стиснення послідовністю:

| Мережа | Параметрів | Епоха | Відкл. втрата | Відкл. AUC сходинок | Відкл. AUC рядків | Val втрата | Val AUC сходинок | Val AUC рядків | с на епоху |
| ------ | ---------: | ----: | ------------: | ------------------: | ----------------: | ---------: | ---------------: | -------------: | ---------: |
| трансформер, ширина 64, 2 шари | 122 тис. | 6 | **0.444** | **0.815** | **0.950** | 0.504 | **0.766** | **0.942** | 94 |
| трансформер, ширина 128, 2 шари | 406 тис. | 5 | 0.452 | 0.812 | 0.949 | 0.543 | 0.738 | 0.940 | 108 |
| LSTM, ширина 128, 2 шари | 213 тис. | 9 | 0.461 | 0.812 | 0.946 | 0.517 | 0.752 | 0.939 | 103 |
| LSTM, ширина 256, 1 шар | 423 тис. | 10 | 0.463 | 0.810 | 0.938 | **0.503** | 0.764 | 0.933 | 54 |
| LSTM, ширина 128, 1 шар | 114 тис. | 10 | 0.469 | 0.798 | 0.939 | 0.521 | 0.742 | 0.934 | 33 |
| обрана модель | 445 тис. | 6 | 0.425 | 0.825 | 0.968 | 0.505 | 0.781 | 0.960 | — |

- **Ні LSTM, ні трансформер на всіх 500 рядках не кращі за обрану
  модель**: кращий — трансформер ширини 64, 0.444 на відкладених проти
  0.425; на val втрата нарівні (0.504 проти 0.505), AUC сходинок і рядків
  нижчі (0.766 і 0.942 проти 0.781 і 0.960). Ширший трансформер —
  гірше: перенавчається.
- LSTM ширини 128 × 1 і 256 × 1 ще вчились на 10-й епосі.

Коло 11 — ті самі мережі без маски на вході. Мертвих рядків у канві
1.5 %, вони розкидані по канві (записи, де клас дав `NaN`), і всі 7 фіч
у них рівно 0 — у живому рядку всі сім нулями не бувають ніколи:

| Мережа | Маска | Епоха | Відкл. втрата | Відкл. AUC сходинок | Відкл. AUC рядків | Val втрата | Val AUC сходинок | Val AUC рядків |
| ------ | ----- | ----: | ------------: | ------------------: | ----------------: | ---------: | ---------------: | -------------: |
| обрана модель (12 епох) | є | 6 | 0.425 | 0.825 | 0.968 | 0.505 | 0.781 | 0.960 |
| | немає | 5 | **0.410** | **0.842** | **0.971** | 0.512 | 0.759 | 0.960 |
| Mixer, канва 50 × 8 | є | 6 | 0.429 | 0.810 | 0.970 | 0.499 | **0.789** | **0.963** |
| | немає | 6 | 0.426 | 0.820 | 0.971 | **0.495** | 0.787 | 0.961 |
| трансформер, ширина 64 | є | 6 | 0.444 | 0.815 | 0.950 | 0.504 | 0.766 | 0.942 |
| | немає | 5 | 0.452 | 0.821 | 0.949 | 0.518 | 0.756 | 0.941 |
| LSTM, ширина 128, 2 шари | є | 9 | 0.461 | 0.812 | 0.946 | 0.517 | 0.752 | 0.939 |
| | немає | 10 | 0.471 | 0.799 | 0.942 | 0.501 | 0.749 | 0.940 |

- **Маска не потрібна**: без неї два прогони з чотирьох кращі на
  відкладених, два — гірші, на val так само впереміш; різниці 0.003–0.02
  — одного порядку з різницею між сусідніми епохами. Мертві рядки мережа
  бачить і так — за сімома нулями.
- Обрана модель без маски на відкладених — 0.410 (краща за всі прогони
  етапів 6–7), але на val AUC сходинок падає 0.781 → 0.759: виграш на
  відкладених на val не переходить.

### Результат 14: 8 голів × 4 шари, 500 × 7 без стовпця маски

Коло 12 — трансформер ширини 64, 8 голів, 4 шари, 189 тис. параметрів,
до перенавчання (стоп після 2 епох без покращення на відкладених):
7 епох, найкраща 5, ~420 с на епоху.

| Епоха | Відкл. втрата (вага рядків 3) | Відкл. AUC сходинок | Відкл. AUC рядків | Val втрата | Val AUC сходинок | Val AUC рядків |
| ----: | ----------------------------: | ------------------: | ----------------: | ---------: | ---------------: | -------------: |
| 1 | 0.714 | 0.768 | 0.938 | — | — | — |
| 2 | 0.676 | 0.814 | 0.945 | — | — | — |
| 3 | 0.702 | 0.795 | 0.945 | 0.511 | **0.771** | 0.937 |
| 4 | 0.655 | 0.820 | 0.951 | **0.502** | 0.764 | **0.941** |
| **5 ★** | **0.651** | **0.824** | **0.953** | 0.529 | 0.739 | 0.938 |
| 6 | 0.678 | 0.813 | 0.948 | 0.534 | 0.734 | 0.935 |
| 7 | 0.674 | 0.814 | 0.950 | 0.547 | 0.754 | 0.935 |

Val — незважена втрата (сходинки + рядки), видача є для епох 3–7
(найкраща ± 2). Лічильник на val: AUC сходинок 0.616.

| Мережа | Параметрів | Епоха | Відкл. втрата | Відкл. AUC сходинок | Відкл. AUC рядків | Val втрата | Val AUC сходинок | Val AUC рядків |
| ------ | ---------: | ----: | ------------: | ------------------: | ----------------: | ---------: | ---------------: | -------------: |
| трансформер 64, 8 голів × 4 шари, без стовпця маски | 189 тис. | 5 | 0.449 | 0.824 | 0.953 | 0.529 | 0.739 | 0.938 |
| трансформер 64, 4 голови × 2 шари, без маски (коло 11) | 122 тис. | 5 | 0.452 | 0.821 | 0.949 | 0.518 | 0.756 | 0.941 |
| обрана модель | 445 тис. | 6 | **0.425** | **0.825** | **0.968** | **0.505** | **0.781** | **0.960** |

- **Глибший трансформер не кращий**: на відкладених нарівні з 2 шарами
  (0.449 проти 0.452), на val гірше (AUC сходинок 0.739 проти 0.756),
  обрану модель не наздоганяє.
- **Найкраща за відкладеними епоха на val не найкраща**: епоха 5 на val
  — 0.739, епохи 3–4 — 0.771 і 0.764.

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

| Блок | Що показує | Ендпоінт |
| ---- | ---------- | -------- |
| опис | вхід (канва 500 × 7 + маска без стиснення), спільні вікна, відкладені ноги, втрата й ручки, будова LSTM і трансформера | `/api/seq30` |
| таблиця | кожен прогін кіл 9–10 і 12, під ними окремо прогони кола 11 без маски: мережа, ручки, параметрів, найкраща епоха з усіх, секунд на епоху, незважена втрата і AUC сходинок / рядків на відкладених і на val, кращий жирним; нижче для порівняння — кращий за відкладеними прогін кожної мережі: обрана модель, три варіанти кола 4, MLP кіл 5–6, Mixer кіл 7–8 | там само |
| втрата по епохах | зважена втрата на відкладених (вага рядків 3) кожного прогону кіл 9–10 і обраної моделі (пунктир), точка — найкраща епоха | там само |
| видача на val | прогін з видачею (`SEQ30_SHOWN`: `g12` — трансформер кола 12): кнопки «усі» й монет val, кнопки епох — найкраща за відкладеними (★) і по дві до і після, таблиця AUC кожної сходинки проти лічильника і частка `q ≥ c`, втрата й AUC рядків; для монети — ряд val: ціна з півотами і під нею на спільній осі часу три теплові карти — сходинки вікна від поточного півота (`q ≤ 1`), від попереднього (`q > 1`), рядки канви 50 клітинок по 10, правда жовтим; назви смуг над рамками, пояснення `q` і осей під смугами; зум і зсув спільні — той самий малюнок, що блок мережі на `/fullness30` | `/api/seq30`, `/api/seq30/series/<PAIR>/<прогін>/<епоха>` |
| нога val | блок «подача і видача на одній нозі val» з `/fullness30` на прогоні: кнопки монети, нога ◀ ▶ або числом, «% ноги» 10 … 200, кнопки епох прогону (★ — найкраща, по дві до і після); ціна ноги й вікна, подача (канва 500 × 7 і маска — маска тільки показується, на вхід не йде), вихід 2 по рядках з правдою, вихід 1 — 16 сходинок з правдою; вікно рахується наживо `probe30.probe` з `--run g12 --epoch N` | `/api/fullness30/probe/<PAIR>/<N>/<відсоток>/g12-<епоха>` |

## Етап 8. Півоти ноги на канві — `підтверджено`, 2026-09-25

### Промпт

> мені тепер потрібно щоб у канву попадала нога з півотами зверху та
> знизу — щоб мережа бачила і початок ноги і кінець — ціль — визначити
> на канві початок і кінець ноги зігзага 2.3 … твоя задача — знайти
> справжній півот або декілька в цій канві і виявити і дати сигнал де
> вони … відсоток від довжини (по ціні) попередньої ноги, має бути не
> більше 10 %: якщо попало в цю зону — добре, не попало — штрафуй … на
> графіку позначити початок і кінець канви, а мережа все розрахувати в
> цій канві і показати під графіком … зміни ціль для мережі — нога
> завершилась (перетин множин 95–105 % по тривалості та 10 % по ціні від
> півота) = 1

### Ціль

Мережа отримує канву — відрізок ряду записів 30 с — і на кожному рядку
каже одне: **нога завершилась** (1) чи ні (0). «Нога завершилась» — це
перетин двох множин навколо півота зігзага 2.3 %, що закінчує ногу
тривалістю `L` записів і ціновою довжиною `M`:

- **за часом** — 95 … 105 % тривалості ноги: записи `p ± 0.05·L`;
- **за ціною** — ближче за 10 % `M` до ціни півота.

Зон у канві буває одна, кілька або жодної. Сигнал — рядок канви, де
мережа бачить завершення ноги.

### Скільки даних іде в канву

- **У навчанні** — вікно навколо ноги довжини `L` записів між півотами
  `s` і `e`: від `s − a·L` до `e + b·L`, `a ~ U[−0.4, 0.6]`,
  `b ~ U[−0.6, 0.6]`, 16 вікон на ногу, не коротше 20 записів. Вікно
  займає від 0.2 до 2.2 довжини ноги: у канві буває вся нога з обома
  півотами і шматками сусідніх ніг, лише її початок або кінець, або
  шматок без жодного півота — на будь-яких рядках.
- **Ряд val** — ковзні вікна по 2000 записів (≈ 1.6 медіанної ноги,
  16.7 год) з кроком 250, без розмітки.
- **Канва руками** — рівно відрізок, виділений на графіку двома кліками.
- Будь-яке вікно стає канвою 500 × 7 тим самим ходом (етап 6): нормування
  тільки на вікно, без стовпця маски, плюс 4 скаляри.

### Живий час

На кожному новому записі `t` канва — від останнього знайденого півота
мінус запас до `t`, так щоб попередній півот був у канві. Мережа
знаходить у ній півоти; новий півот біля правого краю — сигнал, і далі
відлік іде від нього з тим самим запасом.

### Міра

- **Зона** півота `p` — ціль «нога завершилась»: `p ± 0.05·L` записів і
  ціна ближче за `0.1·M`, `L` і `M` — тривалість і цінова довжина ноги,
  що закінчується в `p` (`M = |close[p] − close[p₋]|`); у першого
  півота ряду — наступної ноги.
- **Сигнал добрий**, коли його рядок у зоні півота канви; поза зонами —
  хибний.
- **Точність** — частка добрих сигналів; **повнота** — частка півотів
  канви, у зоні яких є сигнал; F1.

### Метод

- **Мітка рядка** (`edges30_data.py`): 1 — рядок у зоні півота в канві
  (за часом — записи рядка перетинають `p ± 0.05·L`; за ціною для
  вершини береться найбільша `close` рядка, для дна — найменша); 0 —
  поза зонами; −1 — у зоні півота поза канвою (мережа його не бачить:
  без штрафу й нагороди). **Відстань** рядка — на скільки зон він далі
  від найближчого півота своєї ноги: більше з двох, за часом (у
  `0.05·L`) і за ціною (у `0.1·M`).
- **Мережа** (`edges30_net.py`) — трансформер на 500 рядках, як на
  `/seq30`: рядок + 4 скаляри → ширина 64, вивчена позиція рядка, 2 шари
  по 4 голови (norm first, ff 128, dropout 0) → на кожен рядок логіт
  «нога завершилась». 100 тис. параметрів.
- **Втрата** — BCE по живих рядках з вагою: зона — 3, поза зоною —
  `1 + min(відстань, 9)`: що далі від зони, то дорожче; −1 і мертвий
  рядок — 0.
- **Сигнал** — пік видачі: рядок з імовірністю ≥ порогу, найбільшою в
  ±10 рядках; на ряді val — ±60 записів. Поріг — найкращий F1 на
  відкладених.
- **Відкладені** — 20 % ніг train (seed 0): за втратою на них епоха, за
  F1 — поріг; val тільки міряється. Adam lr 1e-3, батч 64, стоп після 2
  епох без покращення, стеля 30. Seed вікон 8.
- **Видача на val** (`--outputs`) — найкраща епоха ± 2: міри на вікнах
  val по монетах і ряд val — імовірність запису найбільша з ковзних
  вікон, сигнали — піки ряду.

```bash
python3 research/35-leg-fullness/scripts/edges30_data.py
python3 research/35-leg-fullness/scripts/edges30_data.py --series
python3 research/35-leg-fullness/scripts/edges30_net.py
python3 research/35-leg-fullness/scripts/edges30_net.py --outputs
python3 research/35-leg-fullness/scripts/edges30_net.py --grid   # грід штрафів з епохи 6
```

### Результат 15: «нога завершилась» — утричі краще за випадковий рядок, але сигналів забагато

Навчання — 8 епох, стоп після 2 без покращення, найкраща 6 (~68 с на
епоху). Рядків у зоні — 5.5 %: випадковий рядок влучає з імовірністю
0.055.

| Епоха | Відкл. втрата | Поріг | Відкл. точність | Відкл. повнота | Відкл. F1 | Val точність | Val повнота | Val F1 |
| ----: | ------------: | ----: | --------------: | -------------: | --------: | -----------: | ----------: | -----: |
| 4 | 0.1215 | 0.10 | 0.146 | **0.414** | **0.216** | 0.146 | **0.376** | **0.211** |
| 5 | 0.1190 | 0.10 | 0.153 | 0.335 | 0.210 | 0.143 | 0.329 | 0.200 |
| **6 ★** | **0.1186** | 0.10 | **0.189** | 0.217 | 0.202 | **0.163** | 0.202 | 0.181 |
| 7 | 0.1195 | 0.10 | 0.164 | 0.289 | 0.209 | 0.155 | 0.279 | 0.199 |
| 8 | 0.1203 | 0.10 | 0.174 | 0.238 | 0.201 | 0.164 | 0.229 | 0.191 |

Ряд val, епоха 6 (ковзні вікна без розмітки):

| Монета | Сигналів | Добрих | Півотів | Знайдено | Точність | Повнота |
| ------ | -------: | -----: | ------: | -------: | -------: | ------: |
| BTC_USDT | 229 | 19 | 27 | 14 | 0.083 | 0.519 |
| ETH_USDT | 293 | 29 | 41 | 17 | 0.099 | 0.415 |
| SOL_USDT | 252 | 25 | 57 | 18 | 0.099 | 0.316 |
| XRP_USDT | 268 | 22 | 102 | 19 | 0.082 | 0.186 |
| BNB_USDT | 208 | 18 | 35 | 12 | 0.087 | 0.343 |
| DOGE_USDT | 315 | 24 | 97 | 20 | 0.076 | 0.206 |

- На вікнах val точність 0.14–0.16 — утричі вища за випадковий рядок
  (0.055), повнота 0.20–0.38.
- На ряді val точність 0.08–0.10: сигналів у 3–10 разів більше, ніж
  півотів, 9 з 10 — поза зоною.

### Результат 16: грід штрафів — висота видачі росте, точність стоїть

З ваг епохи 6 донавчання на кожній парі ваг (`edges30_net.py --grid`):
рядок зони важить `pos` ∈ {1, 3, 10, 30} — ціна пропуску, рядок поза
зонами — `1 + slope · min(відстань, 9)`, `slope` ∈ {0, 1, 3} — ціна
хибного рядка. Adam lr 3e-4, батч 64, до 4 епох, стоп після 2 без
покращення F1 на відкладених ногах; епоха й поріг — найкращий F1 на
відкладених (ті самі 20 % ніг train, seed 0), val — тільки замір на
вікнах val. ~72 с на епоху, 45 епох, ~55 хв. Вихід —
`models/edges30/grid/pos<p>_slope<s>/epoch_NN.pt`,
`results/edges30/grid.json`.

| pos | slope | Епоха | Поріг | Відкладені F1 | Val точність | Val повнота | Val F1 | p99 видачі val |
| --: | ----: | ----: | ----: | ------------: | -----------: | ----------: | -----: | -------------: |
| епоха 6 | (3, 1) | — | 0.10 | 0.202 | 0.163 | 0.202 | 0.181 | 0.12 |
| **1 ★** | **0** | 4 | 0.15 | **0.217** | 0.148 | 0.324 | **0.204** | 0.40 |
| 1 | 1 | 4 | 0.05 | 0.197 | 0.159 | 0.192 | 0.174 | 0.22 |
| 1 | 3 | 3 | 0.05 | 0.089 | 0.153 | 0.060 | 0.086 | 0.12 |
| 3 | 0 | 4 | 0.35 | 0.217 | 0.146 | 0.333 | 0.203 | 0.67 |
| 3 | 1 | 4 | 0.10 | 0.216 | 0.147 | 0.306 | 0.198 | 0.42 |
| 3 | 3 | 4 | 0.05 | 0.210 | 0.154 | 0.240 | 0.188 | 0.26 |
| 10 | 0 | 4 | 0.65 | 0.214 | 0.146 | 0.326 | 0.202 | 0.86 |
| 10 | 1 | 4 | 0.25 | 0.212 | 0.138 | 0.369 | 0.201 | 0.73 |
| 10 | 3 | 2 | 0.10 | 0.212 | 0.142 | 0.326 | 0.198 | 0.36 |
| 30 | 0 | 4 | 0.85 | 0.211 | 0.146 | 0.296 | 0.195 | 0.94 |
| 30 | 1 | 4 | 0.55 | 0.212 | 0.144 | 0.302 | 0.195 | 0.89 |
| 30 | 3 | 4 | 0.30 | 0.215 | 0.142 | 0.332 | 0.199 | 0.78 |

- **Ваги міняють висоту видачі, не розділення.** p99 видачі йде від 0.12
  до 0.94, поріг іде за нею від 0.05 до 0.85, а точність на val стоїть
  на 0.14–0.16 в усіх варіантах — утричі вище випадкового рядка (0.055),
  як і до гріду.
- **F1 на val — 0.19–0.20 в усіх варіантах, крім `pos 1, slope ≥ 1`.**
  Там видача опускається нижче найменшого порогу 0.05 і сигналів мало.
- **Найкращий за відкладеними — `pos 1, slope 0`** (звичайна BCE без
  ваг), val F1 0.204 проти 0.181 епохи 6: приріст — з повноти, точність
  не зросла.

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

| Блок | Що показує | Ендпоінт |
| ---- | ---------- | -------- |
| опис | ціль «нога завершилась» (перетин 95 … 105 % тривалості і 10 % ціни ноги), канва, вікна навчання, сигнал і штраф, мережа, точність і повнота | `/api/edges30` |
| епохи | кожна епоха: втрата train / відкладені / val, поріг, точність, повнота, F1, добрих / сигналів, знайдено / півотів на відкладених і val, секунди; ★ — найкраща | там само |
| видача на val | кнопки епох (★ і по дві до і після); таблиця по монетах — вікна val і ряд val; кнопки монети; ціна val з півотами і зонами «нога завершилась» — прямокутник 95 … 105 % тривалості ноги × 10 % її цінової довжини (зелений — вершина, червоний — дно), сигнали (синій — добрий, сірий — хибний), під нею на спільній осі часу — імовірність «нога завершилась» і поріг пунктиром; зум і зсув | `/api/edges30/series/<PAIR>/<епоха>` |
| грід штрафів | під епохами: на кожен варіант `pos × slope` — найкраща епоха за F1 відкладених і з скількох, поріг, точність / повнота / F1 на відкладених і val, p99 і максимум видачі на рядках val; ★ — найкращий F1 на відкладених | `/api/edges30` (`grid`) |
| канва руками | кнопки монети й епохи; ціна val — два кліки ставлять початок і кінець канви (виділення жовтим), край виділення хапається мишею (±6 px, курсор ↔) і перетягується — канва рахується заново, коли відпустили; сервер будує канву з відрізка і проганяє мережу (`edges30_net.probe`); під ціною — відрізок крупно: ціна, півоти й зони, сигнали мережі, смуга 500 рядків «нога завершилась» з порогом, жовте під смугою — рядки в зоні півота канви, риски — сигнали; нижче на тій самій осі — смуга кожного варіанта гріду штрафів (його найкраща епоха й поріг, сигнали рисками, синя — у зоні, сіра — хибна, ★ — найкращий F1 на відкладених; `edges30_net.grid_models`) | `/api/edges30/price/<PAIR>`, `/api/edges30/probe/<PAIR>/<від>/<до>/<епоха>` |

## Приклади

Прикладом є **префікс** ноги. На ногу — один позитив (обрив на
справжньому кінці) і `--cuts 12` обривів: спершу краї корекцій 0.8 —
найважчий негатив, бо етап 1 показав, що підпис там той самий, — решта
випадкові точки ноги. Краї корекцій тут тільки **вибирають точку
обриву**, у входи вони не заходять.

| Частина | Ніг | Прикладів | Повних | Обривів на краях корекцій | Випадкових |
| ------- | --: | --------: | -----: | ------------------------: | ---------: |
| train | 603 | 7837 | 603 | 1904 | 5330 |
| val | 106 | 1367 | 106 | — | — |

Мітки: `повна = 1` на справжньому кінці, `частка = (e − first) / (last − first)`.

### Мережа

```
(63 канали + маска) → 4 блоки Conv1d(ядро 5, stride 2) + GroupNorm + GELU
                    → 32 кроки × 128 → 2 шари уваги, 4 голови
                    → увага-пулінг → 128 + 5 скалярів → 64 → 2 голови
```

| Ручка | Значення |
| ----- | -------- |
| втрата | `BCE(повна, pos_weight = негативів / позитивів) + 1.0 · MSE(частка)` |
| оптимізатор | Adam, `lr = 1e-3`, батч 48, 24 епохи, dropout 0 |
| seed | 0 |
| відкладений шматок | 20 % **ніг** train (120 ніг): префікси однієї ноги не стоять по обидва боки |
| вибір епохи | найбільша AUC повноти на відкладених ногах; val у виборі не бере участі |
| пристрій | `mps`; канва читається memmap, у памʼяті лише батч |

**Контроль «хвіст»** — та сама мережа, якій лишено тільки останні 16
рядків канви (`--tail 16`), тобто останні 3.2 % префікса; пʼять
скалярів вона отримує так само. Якщо повна канва його не бʼє, форма
ноги понад її кінець нічого не дає.

**Базові рівні**: лічильник — подій від півота, поділене на медіану
довжини ноги train (433) і обрізане до 1; константа 0.5.

### Результат 4: мережа бʼє лічильник, але форма ноги понад її хвіст нічого не додає

| Мережа | Шматок | Ніг | Прикладів | AUC повноти | важкі | легкі | лічильник | частка MAE | лічильник | константа | AUC останньої десятини | лічильник |
| ------ | ------ | --: | --------: | ----------: | ----: | ----: | --------: | ---------: | --------: | --------: | ---------------------: | --------: |
| повна канва (епоха 20) | train | 483 | 6277 | 0.990 | 0.981 | 0.993 | 0.726 | 0.173 | 0.221 | 0.274 | 0.861 | 0.743 |
| | відкладені | 120 | 1560 | 0.769 | 0.667 | 0.806 | 0.741 | 0.192 | 0.216 | 0.271 | 0.761 | 0.736 |
| | **val** | 106 | 1367 | **0.768** | **0.674** | 0.802 | 0.699 | **0.184** | 0.237 | 0.271 | 0.764 | 0.711 |
| хвіст 16 рядків (епоха 10) | train | 483 | 6277 | 0.910 | 0.830 | 0.938 | 0.726 | 0.194 | 0.221 | 0.274 | 0.814 | 0.743 |
| | відкладені | 120 | 1560 | 0.783 | 0.663 | 0.827 | 0.741 | 0.201 | 0.216 | 0.271 | 0.750 | 0.736 |
| | **val** | 106 | 1367 | **0.784** | **0.672** | 0.825 | 0.699 | **0.183** | 0.237 | 0.271 | 0.764 | 0.711 |

Похибка на val — 1000 перевибірок **ніг** (нога йде цілком, з усіма
своїми префіксами), межі 5–95 %
([`scripts/report.py`](scripts/report.py)):

| | AUC повноти | важкі | частка MAE | AUC понад лічильник | MAE нижче лічильника |
| - | ----------: | ----: | ---------: | ------------------: | -------------------: |
| повна канва | 0.738 … 0.798 | 0.629 … 0.727 | 0.175 … 0.193 | **+0.031 … +0.102** | **+0.031 … +0.074** |
| хвіст 16 рядків | 0.760 … 0.810 | 0.629 … 0.719 | 0.175 … 0.192 | **+0.056 … +0.114** | **+0.031 … +0.075** |
| канва − хвіст | −0.050 … +0.017 | −0.037 … +0.041 | −0.003 … +0.004 | — | — |

Чотири речі:

1. **Обидві мережі бʼють лічильник на val**, і межі цього не
   накривають нуль: AUC повноти +0.03…+0.11, похибка частки нижча на
   0.03…0.07. Константу 0.5 обидві бʼють теж (MAE 0.18 проти 0.27).
2. **Повна канва не бʼє хвіст.** Усі три різниці накривають нуль. Те,
   що мережа знає про повноту, вона бере з **кінця** префікса — з
   останніх 16 рядків канви, тобто з останніх 3.2 % префікса, і зі
   скалярів. Форма всієї
   ноги понад це нічого не додає.
3. **Важкий обрив лишається важким**: на краях корекцій AUC 0.67 проти
   0.80–0.83 на випадкових точках. Це той самий висновок, що в етапі 1:
   край корекції — слабша копія краю ноги.
4. **Повна канва вчить ноги напамʼять**: 0.990 на train проти 0.769 на
   відкладених ногах. 31 500 входів на 483 ноги — вхід набагато більший
   за число незалежних одиниць, і стискання 500 рядків це не лікує.

**Порядок усередині ноги** (Спірмен по ногах) у лічильника вищий
(0.905 проти 0.801 і 0.868): лічильник росте з кожною подією, тобто
впорядковує префікси однієї ноги ідеально за побудовою. Ця міра
показує, що мережа подекуди каже «менше» на пізнішому префіксі, а не
те, що лічильник краще знає повноту.

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

- Постановка «префікс ноги → повна / частка» працює: на val мережа
  відрізняє справжній кінець ноги від обриву з AUC 0.77–0.78 і каже
  частку з похибкою 0.18 проти 0.24 у лічильника.
- Але працює **кінець префікса**, а не форма ноги: хвіст у 16 рядків
  дає те саме, що вся канва.
- Найслабше місце — обрив на краю корекції (AUC 0.67).

## Приклади

`results/roles.json` — увесь вихід етапу 1:

```json
{"class": "class210x10m1x7dd", "width": 2, "body": 10, "shuffles": 20,
 "train": {"couples": [{"left": "кінець", "right": "корекція-як-кінець",
   "n_left": 1809, "n_right": 2512,
   "columns": {"d_resist_buy·std": {"auc": 0.428,
     "floor_low": 0.478, "floor_high": 0.522}}}]}}
```

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

```bash
# 1. датасети шести монет — rules/01-data-and-db.md
python3 rules/scripts/build_class210x10x7dc.py

# 2. ноги датасету
python3 rules/scripts/leg_markup.py

# 3. етап 1: ролі подій
python3 research/35-leg-fullness/scripts/roles.py

# 4. етап 2: канви (читає посекундну робочу копію data/2026/<міс>/<PAIR>/)
python3 research/35-leg-fullness/scripts/canvas_data.py --cuts 12

# 5. етап 2: мережа на повній канві і контроль «хвіст», потім звід
python3 research/35-leg-fullness/scripts/canvas_net.py --epochs 24 --tag canvas
python3 research/35-leg-fullness/scripts/canvas_net.py --epochs 24 --tail 16 --tag tail
python3 research/35-leg-fullness/scripts/report.py

# 6. етапи 3 і 4: записи 30 с, ноги, фічі й канва всіх 18 класів
#    (робоча копія до 22.09)
python3 research/35-leg-fullness/scripts/bars30.py

# 7. етап 5: голова проти хвоста і UMAP ніг
python3 research/35-leg-fullness/scripts/head_tail30.py
python3 research/35-leg-fullness/scripts/umap_legs30.py

# 8. етап 6: вікна часток ноги і кореляції між ногами
python3 research/35-leg-fullness/scripts/windows30.py

# 9. етап 8: півоти ноги на канві
python3 research/35-leg-fullness/scripts/edges30_data.py
python3 research/35-leg-fullness/scripts/edges30_data.py --series
python3 research/35-leg-fullness/scripts/edges30_net.py
python3 research/35-leg-fullness/scripts/edges30_net.py --outputs
python3 research/35-leg-fullness/scripts/edges30_net.py --grid   # грід штрафів з епохи 6
```
