# Повнота ноги як фільтр хибних сигналів

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

## Ціль

Драбина дослідження 23 відповідає «півот був `N` подій тому» і на val дає
близько 50 % точності: **половина сигналів хибна**. Питання тут одне —
чи можна скасувати хибні **другим сигналом**.

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

Дослідження ділиться надвоє:

1. **Оцінювач повноти ноги** — на кожній події сказати частку від `0`
   (початок ноги) до `1` (кінець), маючи тільки причинні входи.
2. **Фільтр** — прийняти сигнал драбини, лише коли ця частка не менша за
   поріг `g`, і зміряти, скільки хибних це прибирає і скільки влучних
   коштує.

## Дані

Ті самі, що в дослідженнях 23 і 24: `data/datasets/<PAIR>/class210x10m1x7dd/`,
шість монет, подія 210 с, зігзаг **2.3 %** на **ема10**. Розбиття за
датою: train до `2026-08-14`, val до `2026-09-04`.

Подій усередині ніг: **355 793** у train і **46 112** у val.

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

1. Порахувати мітку — частку ноги на кожній події.
2. Порахувати причинний лічильник подій від **підтвердженого** півота.
3. Навчити оцінювач на чотирьох наборах входів, зміряти його на val.
4. Прогнати сигнал драбини на щаблях 43 і 103, накласти фільтр і зміряти
   точність, повноту й число скасованих сигналів на сітці порогів.
5. Повторити фільтр на **справжній** частці ноги — це стеля ідеї.
6. Підтвердити сигнал драбини **другою моделлю** — мережею констатації
   розвороту дослідження 25 — і зміряти, що два голоси дають разом.
7. Перебрати **функції втрат** на тій самій мережі й обрати найкращу за
   порядком подій у нозі.
8. Подвоїти історію, перебрати **ручки мережі** гридом і взяти найкращу
   точку.

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

| Блок | Ендпоінт | Скрипт |
| ---- | -------- | ------ |
| Оцінювач повноти ноги і таблиця фільтра | `/api/share` | [`scripts/leg_share.py`](scripts/leg_share.py) |
| Плашка монети: частка ноги деревами, мережею з памʼяттю і сигнал драбини | `/api/share/series/<PAIR>` | там само + [`scripts/share_lstm.py`](scripts/share_lstm.py) |
| Мережа з памʼяттю: епохи й фільтр пʼятьма способами | `/api/share/lstm` | [`scripts/share_lstm.py`](scripts/share_lstm.py) |
| Драбина разом з мережею констатації розвороту | `/api/share/two` | [`scripts/two_signals.py`](scripts/two_signals.py) |
| Перебір функцій втрат для частки ноги | `/api/share/loss` | [`scripts/loss_share.py`](scripts/loss_share.py) |
| Грід ручок мережі з памʼяттю | `/api/share/grid` | [`scripts/grid_share.py`](scripts/grid_share.py) |

Тести: [`tests/test_share.py`](../../tests/test_share.py),
[`tests/test_dashboard.py`](../../tests/test_dashboard.py) → `TestShare`,
`TestShareMarkup`.

## Етап 1. Чи скасовує повнота ноги хибні сигнали — `підтверджено`, 2026-09-29

### Мітка: частка ноги

Нога — відрізок між двома сусідніми півотами зігзага (`leg_deciles.cohorts`).
Для події на місці `i` всередині ноги довжини `L`:

    частка = i / L        десятина = 1 + ⌊10 · частка⌋

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

### Причинний лічильник

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

```
підтвердження(p) = перша подія c > p, де |ема[c] / ема[p] − 1| ≥ 2.3 %
елапс[e]         = e − p*, де p* — останній півот із підтвердженням ≤ e
```

Це те, що видно онлайн. `since_pivot` дослідження 23 — підказка з
майбутнього, і тут його немає.

### Входи

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

`елапс` стоїть у таблиці як базовий рівень: дослідження 20 показало, що на
десятині ноги простий лічильник б'є мережу.

Модель — `HistGradientBoostingRegressor` (200 ітерацій, крок 0.1, 31 лист),
ціль — частка ноги. Оцінки train беруться **з відкладеної монети**, val —
моделлю, навченою на всьому train.

```bash
python3 research/26-leg-share-filter/scripts/leg_share.py
```

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

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

| Набір | Входів | train MAE | **val MAE** | схожість val | AUC останньої десятини |
| ----- | -----: | --------: | ----------: | -----------: | ---------------------: |
| `елапс` | 1 | 0.2393 | 0.2440 | 0.335 | 0.552 |
| `вікна` | 20 | 0.2161 | 0.2545 | 0.343 | 0.647 |
| `вікна+елапс` | 21 | 0.2107 | 0.2555 | 0.347 | **0.653** |
| `вікна+елапс+клас` | 83 | 0.2098 | 0.2534 | 0.344 | **0.656** |
| **константа 0.5** | 0 | — | **0.2484** | 0.310 | 0.500 |

`MAE` — середній модуль похибки за часткою, `схожість` — частка подій, де
сказана десятина промахнулась не більше ніж на одну, `AUC останньої
десятини` — чи ставить оцінка подію з частки ≥ 0.9 вище за решту.

Дві речі відразу:

1. **За `MAE` жоден набір не б'є константу 0.5.** Найкращий — 0.2534 проти
   0.2484 у числа, яке нічого не знає. На train `MAE` 0.21, на val 0.25:
   те, що модель вивчила, за межі свого часу не переноситься.
2. **Але «кінець ноги» вона таки трохи бачить**: `AUC` останньої десятини
   0.65 проти 0.5 у константи й 0.55 у самого лічильника. Це слабко, але
   не нуль, і саме це число вирішує долю фільтра.

Клас додає на третьому знаку (0.656 проти 0.653), тобто нічого — той самий
висновок, що в дослідженнях 23, 24 і 25.

### Результат 2: сама ідея працює — на справжній частці

Щабель `N` = 43, сигнал набору `вікна+клас` із порогом 0.9906 на повноті
30 % чесних оцінок train. Фільтр дивиться на частку ноги **тієї події,
куди показує сигнал** — на `e − N`.

**Справжня частка ноги — стеля:**

| Поріг | Точність val | Повнота val | Спіймано / хибних | Скасовано хибних | Скасовано влучних |
| ----- | ------------ | ----------- | ----------------- | ---------------- | ----------------- |
| без фільтра | 51.0 % | 54.8 % | 51 / 49 | 0 | 0 |
| 0.5 | 58.0 % | 54.8 % | 51 / 37 | 12 | 0 |
| 0.7 | 72.9 % | 54.8 % | 51 / 19 | 30 | 0 |
| 0.8 | 85.0 % | 54.8 % | 51 / 9 | 40 | 0 |
| **0.9** | **92.7 %** | **54.8 %** | 51 / 4 | 45 | **0** |
| 0.95 | 98.1 % | 54.8 % | 51 / 1 | 48 | 0 |

Те саме на щаблі `N` = 103: 73.3 % → **97.1 %** на порозі 0.9, повнота
не змінюється зовсім.

Це і є відповідь на питання дослідження: **хибні сигнали справді стоять у
середині ніг**. Фільтр за справжньою повнотою ноги прибирає 45 хибних
сигналів із 49 і не чіпає жодного влучного.

### Результат 3: оцінена частка цього не робить

Той самий щабель, той самий сигнал, але фільтр за **оцінкою**:

| Поріг | Точність val | Повнота val | Спіймано / хибних | Скасовано хибних | Скасовано влучних |
| ----- | ------------ | ----------- | ----------------- | ---------------- | ----------------- |
| без фільтра | 51.0 % | 54.8 % | 51 / 49 | 0 | 0 |
| **0.5** | **52.9 %** | 49.5 % | 46 / 41 | 8 | **5** |
| 0.6 | 50.0 % | 39.8 % | 37 / 37 | 12 | 14 |
| 0.7 | 49.0 % | 25.8 % | 24 / 25 | 24 | 27 |
| 0.8 | 37.9 % | 11.8 % | 11 / 18 | 31 | 40 |
| 0.9 | 37.5 % | 3.2 % | 3 / 5 | 44 | 48 |
| 0.95 | 0.0 % | 0.0 % | 0 / 2 | 47 | 51 |

Оцінка ріже **влучні сигнали швидше за хибні**. На порозі 0.9, де стеля
дає 92.7 % без втрат, оцінка лишає 3 сигнали з 100 і точність **падає**
до 37.5 %.

На щаблі 103 те саме, лише мʼякше: 73.3 % → 75.6 % на порозі 0.5 і 50.0 %
на порозі 0.9.

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

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

| Мітка | Що означає |
| ----- | ---------- |
| **влучив, пройшов** | сигнал на півоті, фільтр його пропустив |
| **хибний, пройшов** | сигнал не на півоті, фільтр його пропустив |
| **хибний, скасовано** | те, заради чого фільтр і робиться |
| **влучний, скасовано** | ціна фільтра |
| **проґавлений півот** | сигналу не було зовсім |

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

`вікна+клас` на щаблі 43, поріг фільтра 0.5 за оціненою часткою:

| Монета | Подій val | Півотів | влучив / хибний / скас. хибних / скас. влучних / проґавив |
| ------ | --------- | ------- | -------------------------------------------------------- |
| `BTC_USDT` | 7 731 | 9 | 8 / 8 / 1 / 0 / 1 |
| `ETH_USDT` | 7 727 | 11 | 7 / 5 / 1 / 1 / 3 |
| `SOL_USDT` | 7 728 | 15 | 6 / 7 / 2 / 3 / 6 |
| `XRP_USDT` | 7 727 | 31 | 11 / 5 / 1 / 0 / 19 |
| `BNB_USDT` | 7 728 | 7 | 5 / 9 / 1 / 0 / 2 |
| `DOGE_USDT` | 7 727 | 21 | 9 / 7 / 2 / 1 / 11 |

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

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

1. **Ідея правильна.** Хибні сигнали драбини стоять у середині ніг:
   фільтр за справжньою повнотою ноги піднімає точність з 51 % до
   **92.7 %** на щаблі 43 і з 73.3 % до **97.1 %** на щаблі 103, не
   втрачаючи жодного влучного сигналу.
2. **Оцінювач повноти ноги — ні.** На val він не б'є константу за `MAE`
   і має `AUC` останньої десятини всього 0.65. Цього замало: фільтр з
   ним ріже влучні швидше за хибні й точність не піднімає.
3. **Уся задача звелась до одного числа** — `AUC` останньої десятини.
   Стеля показує, скільки за нього дають: кожен пункт тут коштує десятків
   відсотків точності сигналу.
4. **Причинний лічильник сам по собі порожній** (`AUC` 0.552): знання,
   скільки подій минуло від підтвердженого півота, майже нічого не каже
   про те, чи нога добігає кінця. Це узгоджується з дослідженням 20, де
   на десятині ноги лічильник бив мережу — тільки там мітка була
   вимірювана заднім числом, а тут питання поставлено онлайн.
5. **Клас у цьому питанні знову ні до чого**: 0.656 проти 0.653 без нього.

## Етап 2. Мережа з памʼяттю на повноту ноги — `підтверджено`, 2026-09-29

Дерево бачить одну подію. Частка ноги — властивість **усього руху**: щоб
сказати «ми в кінці», треба памʼятати, де нога почалась і скільки таких
ніг уже було. Тут те саме питання ставиться мережі з памʼяттю.

### Вікно: 5000 подій памʼяті

| Вікно, подій | Годин | Діб | Півотів: медіана | чверть | три чверті |
| -----------: | ----: | --: | ---------------: | -----: | ---------: |
| 1 000 | 58 | 2.4 | 1 | 1 | 2 |
| 1 500 | 88 | 3.6 | 2 | 1 | 3 |
| 2 000 | 117 | 4.9 | 3 | 2 | 4 |
| 2 500 | 146 | 6.1 | 4 | 3 | 6 |
| **5 000** | **292** | **12.2** | **8** | **5** | **11** |

Узято **5 000 подій** — дванадцять діб, у медіані вісім півотів. Подія за
подією це 5 000 кроків `LSTM`, тому вікно **проріджується**: `LENGTH` =
500 кроків через `STEP` = 10 подій. Довжина вікна й крок — ручки гріду
(етап 5).

Памʼять вікна val вільно дістає з train: це минуле, і саме так мережа
працює наживо. Тому видача є на **кожній** події val — з першої, — і
фільтр міряється на всьому val, як у дерев етапу 1: 46 074 пачки, 93
розвороти, 112 монетодіб.

### Мережа

`LSTM` на 64 стани, один шар, шар нормування, `dropout` 0.1, лінійна
голова в одне число, `sigmoid` і втрата `L1` — та сама шкала, що в
мітці. Пачка 128, крок `3e-4`, скейлер — квантиль свого ряду на подіях
train своєї монети. У навчання йде кожна десята подія: сусідні вікна
перекриваються на 99.8 %. Ширина, глибина й крок навчання — з гріду
етапу 5.

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

```bash
python3 research/26-leg-share-filter/scripts/share_lstm.py
```

Скрипт: [`scripts/share_lstm.py`](scripts/share_lstm.py)
Вихід: [`results/share_lstm.json`](results/share_lstm.json) — числа навчання
й фільтра, `results/share_<набір>.pt` — ваги кращої епохи кожного набору,
`results/mem_<PAIR>.json` — ряд val монети

### Результат 5: памʼять піднімає оцінювач

Val шести монет, 46 тисяч подій усередині ніг:

| Модель | Набір | Каналів | val MAE | схожість val | AUC останньої десятини |
| ------ | ----- | ------: | ------: | -----------: | ---------------------: |
| константа 0.5 | — | 0 | 0.2484 | 0.310 | 0.500 |
| дерева | `елапс` | 1 | 0.2440 | 0.335 | 0.552 |
| дерева | `вікна+елапс` | 21 | 0.2555 | 0.347 | 0.653 |
| дерева | `вікна+елапс+клас` | 83 | 0.2534 | 0.344 | 0.656 |
| мережа | `елапс` | 1 | 0.2440 | 0.335 | 0.589 |
| **мережа** | **`вікна+елапс`** | **21** | **0.2411** | **0.355** | **0.693** |
| мережа | `вікна+елапс+клас` | 84 | 0.2431 | 0.343 | 0.690 |

1. **Памʼять дає, але небагато.** Найкращий набір мережею — `AUC` 0.693
   проти 0.653 у дерева на тих самих входах. Один лічильник у памʼяті
   (0.589) б'є той самий лічильник деревом (0.552), але не дотягує до
   дерева з вікнами.
2. **Найкращий набір — `вікна+елапс`**, і він єдиний б'є константу за
   `MAE`: 0.2411 проти 0.2484.
3. **Клас знову ні до чого**: 0.690 проти 0.693 без нього. Той самий
   висновок, що в дослідженнях 23, 24 і 25.
4. **Краща епоха — перша або друга.** Далі втрата падає, а `MAE` хвоста
   росте: мережа запамʼятовує навчання з третьої епохи.

### Ряд мережі на плашці

Оцінка кращого набору лягає на плашку монети окремою смугою під смугою
дерев: та сама справжня частка сірим, оцінка мережею кольором
(`results/mem_<PAIR>.json`). Порожніх місць у ній немає — памʼять
береться з train.

### Результат 6: фільтр не платить

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

Щабель `N` = 43: 46 074 пачки, 93 розвороти, 51 сигнал влучний і 49 хибних.

| Фільтр за | без фільтра | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 0.95 |
| --------- | ----------- | --- | --- | --- | --- | --- | ---- |
| мережа | 51.0 % | 50.0 % | 50.0 % | 46.9 % | 35.7 % | — | — |
| дерева | 51.0 % | 52.9 % | 50.0 % | 49.0 % | 37.9 % | 37.5 % | — |
| мережа, ранг | 51.0 % | 51.9 % | 51.5 % | 50.0 % | 47.9 % | 46.9 % | 36.8 % |
| дерева, ранг | 51.0 % | 52.8 % | 52.5 % | 50.0 % | 50.0 % | 42.5 % | 37.0 % |
| **справжня** | 51.0 % | 58.0 % | 66.2 % | 72.9 % | 85.0 % | **92.7 %** | 98.1 % |

Щабель `N` = 103: 45 714 пачок, 88 розворотів, 33 влучні сигнали і 12 хибних.

| Фільтр за | без фільтра | 0.5 | 0.6 | 0.7 | 0.8 | 0.9 | 0.95 |
| --------- | ----------- | --- | --- | --- | --- | --- | ---- |
| мережа | 73.3 % | 75.0 % | 72.4 % | 70.6 % | 50.0 % | — | — |
| дерева | 73.3 % | 75.6 % | 75.0 % | 68.0 % | 61.5 % | 50.0 % | — |
| мережа, ранг | 73.3 % | 76.3 % | 74.3 % | 71.9 % | 70.4 % | 70.6 % | 61.5 % |
| дерева, ранг | 73.3 % | 75.6 % | 76.9 % | 75.0 % | 70.0 % | 63.2 % | 58.3 % |
| **справжня** | 73.3 % | 80.5 % | 82.5 % | 84.6 % | 89.2 % | **97.1 %** | 100 % |

«—» — на цьому порозі оцінювач не пропускає жодного сигналу.

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

1. **Фільтр не платить на жодному щаблі.** Найкраще на 43 — 52.9 %
   проти 51.0 % без фільтра, на 103 — 76.9 % проти 73.3 %. Обидва
   виграші менші за пункт-два й куплені третиною сигналів.
2. **Стеля тримається.** Справжня частка на 43 дає **92.7 %**, не
   втративши **жодного** з 51 влучного сигналу: 45 хибних із 49 стоять
   не в кінці ноги. На 103 — 97.1 %.
3. **Ранг обовʼязковий.** Без нього поріг 0.9 на видачі мережі лишає
   нуль сигналів: шкала стиснута, і «0.9» вона не каже.
4. **Розрив зі стелею — головне число дослідження.** Оцінювач має
   `AUC` 0.693 там, де потрібне щось близьке до 0.95.

## Етап 3. Драбина разом з мережею констатації розвороту — `підтверджено`, 2026-09-29

Етапи 1 і 2 фільтрували драбину **оцінкою повноти ноги**. Тут її
фільтрує **інша модель того самого розвороту** — мережа дослідження 25.
Вона стоїть на щаблі `N = 3`, тобто констатує півот майже одразу після
нього, тоді як драбина висловлюється на щаблі `N`. Про той самий півот
`c` мережа каже на події `c + 3`, драбина — на події `c + N`, і слово
мережі приходить **раніше**.

### Правило разом

Сигнал драбини на події `e` приймається, лише коли мережа десь у вікні
`±W` навколо `c + 3` теж піднялась вище свого порога:

    брати(e) = драбина(e) ≥ край  і  max мережа[c + 3 − W … c + 3 + W] ≥ поріг

де `c = e − N`. Вікно обмежене причинністю: `c + 3 + W ≤ e`, тобто
`W ≤ N − 3`. Вікна `0, 5, 10, 20, 40` подій, пороги мережі
`0, 0.5, 0.8, 0.9, 0.95, 0.99`.

### Мережа

Береться найкращий набір дослідження 25 за `AP` на val — **`ціна`**,
20 каналів контексту на чотирьох вікнах: val `AUC` 0.9504, `AP` 0.0704,
поріг із train 0.8238. Мережа не перенавчається: її видача на кожній
події val уже лежить у `research/25-leg-lstm/results/lstm_<PAIR>.json`.

### Результат 7: другий голос не платить на 43 і шкодить на 103

Щабель `N = 43`, вікно `±20`, val шести монет: 46 074 пачки,
93 розвороти, 112 монетодіб.

| Поріг мережі | Точність | Повнота | Спіймано / хибних | На добу |
| ------------ | -------: | ------: | ----------------: | ------: |
| без мережі | 51.0 % | 55 % | 51 / 49 | 0.893 |
| 0.50 | 48.2 % | 44 % | 41 / 44 | 0.759 |
| 0.80 | 47.8 % | 35 % | 33 / 36 | 0.616 |
| 0.90 | 49.1 % | 30 % | 28 / 29 | 0.509 |
| 0.95 | 57.9 % | 24 % | 22 / 16 | 0.339 |
| **0.99** | **71.4 %** | 11 % | 10 / 4 | 0.125 |

Щабель `N = 103`, вікно `±20`: 45 714 пачок, 88 розворотів.

| Поріг мережі | Точність | Повнота | Спіймано / хибних | На добу |
| ------------ | -------: | ------: | ----------------: | ------: |
| без мережі | 73.3 % | 38 % | 33 / 12 | 0.405 |
| 0.50 | 68.4 % | 30 % | 26 / 12 | 0.342 |
| 0.90 | 68.2 % | 17 % | 15 / 7 | 0.198 |
| 0.99 | 80.0 % | 5 % | 4 / 1 | 0.045 |

Вікно важить менше за поріг: від `±0` до `±40` точність на порозі 0.9
йде 41.5 → 50.0 % на щаблі 43 і 53.3 → 69.2 % на 103. Вужче вікно ріже
влучні сигнали, бо мережа рідко влучає рівно в подію `c + 3`.

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

1. **На щаблі 103 підтвердження мережею тільки шкодить.** Драбина сама
   дає 73.3 %, будь-який поріг мережі до 0.95 опускає її нижче, і те
   саме робить із повнотою.
2. **На щаблі 43 виграш є лише в самому кінці шкали.** Поріг 0.99 дає
   71.4 % проти 51.0 % — але це 10 розворотів із 93 і один сигнал на
   вісім діб. За тим порогом мережа мовчить майже завжди.
3. **Другий голос не незалежний.** Обидві моделі дивляться на той самий
   контекст ціни, і там, де помиляється одна, найчастіше помиляється й
   друга: на порозі 0.9 з 49 хибних сигналів мережа скасувала 20, але
   разом з ними — 23 влучних.

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

Видача етапу 2 вийшла **стиснутою**: розмах 0.186 проти 0.294 у мітки,
а від першої десятини до останньої середнє проходить лише 0.38 → 0.59.
`L1` тягне до умовної медіани, мітка всередині ноги майже рівномірна —
медіана й виявляється серединою ноги.

Тут та сама мережа, ті самі канали (`вікна+елапс`), розбиття й seed.
Міняється **тільки функція втрат**. Ручки — ті, з якими етап 2 ішов до
гріду: памʼять `250 × 10` = 2500 подій, `hidden` 96, один шар,
`rate = 1e-3`. Вони закріплені в самому скрипті, щоб числа етапу
відтворювались після того, як грід етапу 5 поміняв ручки основної
мережі.

### Що перебрано

| Втрата | Голів | Ідея |
| ------ | ----: | ---- |
| `L1` | 1 | базова: умовна медіана |
| `MSE` | 1 | умовне середнє |
| `Huber` | 1 | між ними, `δ = 0.2` |
| `десятини` | 10 | розподіл по десятинах, видача — очікувана десятина |
| `сходинки` | 9 | порядкові голови «частка ≥ k/10», видача — середнє |
| `ранг` | 1 | пари всередині пачки: хто ближче до кінця ноги |
| `сходинки+ранг` | 9 | пороги й порядок разом |
| `край` | 1 | своя: `L1` з вагою на краях ноги |
| `розмах` | 1 | своя: `L1` зі штрафом за стиснуту видачу |
| `край+ранг` | 1 | своя: вага краю плюс пари |

Три власні:

    край:   w(y) = 1 + 3 · |2y − 1|²        середина коштує 1, край 4
    розмах: L1 + relu( std(y) − std(ŝ) )    штраф за стиснуту видачу
    ранг:   BCE( ŝᵢ − ŝⱼ, [yᵢ > yⱼ] )       по всіх парах пачки, |yᵢ − yⱼ| > 0.05

`ранг` — це прямо та величина, яку міряє `AUC`, і вона не залежить від
шкали.

### Вибір епохи

Епоха береться за **`MAE` хвоста train**. Правило «епоха за `AUC`
хвоста» перевірено й відкинуте: хвіст — 1722 вікна, його `AUC` скаче й
раз за разом обирає пізні епохи, гірші на val на 0.05–0.14. Обидва
числа лишились у файлі результату, щоб це було видно.

### Калібрування

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

### Результат 8: втрата майже нічого не міняє

Епоха за `MAE` хвоста — у **всіх десяти** втрат це епоха 1. Val, шість
монет, 31 208 подій усередині ніг:

| Втрата | Голів | val `AUC` | val `MAE` | `MAE` після калібрування | Розмах видачі |
| ------ | ----: | --------: | --------: | -----------------------: | ------------: |
| `MSE` | 1 | **0.7608** | 0.2353 | 0.2614 | 0.130 |
| `край+ранг` | 1 | 0.7565 | 0.2373 | 0.2587 | 0.174 |
| `сходинки` | 9 | 0.7562 | 0.2325 | 0.2648 | 0.140 |
| `ранг` | 1 | 0.7549 | 0.2731 | 0.2609 | 0.137 |
| `розмах` | 1 | 0.7525 | 0.2661 | 0.2635 | 0.302 |
| `край` | 1 | 0.7522 | 0.2510 | 0.2582 | 0.260 |
| `Huber` | 1 | 0.7492 | 0.2331 | 0.2631 | 0.172 |
| `сходинки+ранг` | 9 | 0.7483 | 0.2329 | 0.2623 | 0.142 |
| `L1` (етап 2) | 1 | 0.7406 | 0.2339 | 0.2626 | 0.186 |
| `десятини` | 10 | 0.7394 | 0.2345 | 0.2693 | 0.123 |
| константа 0.5 | 0 | 0.5000 | 0.2484 | — | 0.000 |
| мітка | — | — | — | — | 0.294 |

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

1. **Функція втрат майже нічого не міняє.** Уся десятка вкладається в
   0.739–0.761 `AUC` — розкид 0.022 при десяти кандидатах, обраних за
   тим самим val. Це шум відбору, а не виграш: сказати «`MSE` краща за
   `L1`» на таких числах не можна.
2. **Своя вага краю не спрацювала.** `край` дає 0.7522 проти 0.7406 —
   у тій самій смузі. Платити вчетверо за краї ноги можна, але сигналу
   в даних від цього не додається.
3. **Стиснення видачі — не помилка, а оптимум.** `розмах` розтягнув
   видачу з 0.186 до 0.302, тобто рівно до розмаху мітки, — і `MAE`
   погіршився з 0.2339 до 0.2661 при тому самому `AUC`. Те саме показує
   калібрування: воно псує `MAE` кожній втраті без винятку. Під слабким
   сигналом тулитись до середини — правильна поведінка, і боротись із
   нею немає сенсу.
4. **Головне тут не втрата, а зупинка.** Краща епоха — **перша**, у всіх
   десяти. Правило «епоха за `AUC` хвоста» обирає 3-ю, 5-ту, 6-ту, 7-му
   і втрачає до 0.14 `AUC`: у `L1` 0.7406 → 0.6064, у `MSE`
   0.7608 → 0.6427. Хвіст на 1722 вікна для `AUC` замалий.
5. **Стеля дослідження не зрушила.** Найкраще з десятки — 0.7608 проти
   0.7406. Фільтру потрібне щось близьке до 0.95, і функція втрат цієї
   відстані не долає.

## Етап 5. Ручки мережі: грід — `підтверджено`, 2026-09-29

Етап 4 показав, що функція втрат нічого не міняє. Отже питати треба саму
мережу: скільки їй памʼяті, якої ширини й глибини, з яким кроком учити.

### Що перебрано

| Ручка | Значення | Про що |
| ----- | -------- | ------ |
| `кроків × крок` | `250 × 20`, `500 × 10` | ті самі 5000 подій: рідше й довше проти частіше й коротше |
| `hidden` | 64, 96, 160 | ширина стану |
| `layers` | 1, 2 | глибина |
| `rate` | 1e-3, 3e-4 | крок навчання |

24 точки, по 3 епохи, епоха точки — за `MAE` хвоста train. Плюс
**контрольна** точка зі старою памʼяттю `250 × 10` = 2500 подій і
ручками етапу 2, щоб побачити, що дало саме подвоєння історії.

### Як обирається точка

Ручки беруться за **маргінальними середніми** `MAE` хвоста: для кожного
значення ручки береться середнє по всіх точках гріду, де вона так
стоїть, і перемагає найменше. Одна точка — це 1722 вікна хвоста, її
`MAE` шумить; за маргіналом стоїть вісім-дванадцять прогонів.

Вибір іде **тільки по хвосту**: вибирати за val означало б вибирати по
тому, на чому потім звітуєш.

```bash
python3 research/26-leg-share-filter/scripts/grid_share.py
python3 research/26-leg-share-filter/scripts/grid_share.py \
    --windows 250x10 --hidden 96 --layers 1 --rates 0.001 --append
```

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

### Результат 9: ручки майже нічого не міняють

Маргінальні середні по 24 точках:

| Ручка | Значення | Точок | `MAE` хвоста | val `MAE` | val `AUC` |
| ----- | -------- | ----: | -----------: | --------: | --------: |
| вікно | 250 × 20 | 12 | 0.2530 | 0.2598 | 0.6423 |
| вікно | **500 × 10** | 12 | **0.2464** | 0.2580 | 0.6492 |
| hidden | **64** | 8 | **0.2469** | 0.2584 | 0.6431 |
| hidden | 96 | 9 | 0.2476 | 0.2560 | 0.6620 |
| hidden | 160 | 8 | 0.2545 | 0.2624 | 0.6361 |
| шарів | **1** | 13 | **0.2488** | 0.2534 | 0.6622 |
| шарів | 2 | 12 | 0.2505 | 0.2645 | 0.6318 |
| rate | **3e-4** | 12 | **0.2460** | 0.2500 | 0.6651 |
| rate | 1e-3 | 13 | 0.2529 | 0.2679 | 0.6314 |

Обрана точка — **500 × 10, `hidden` 64, один шар, `rate` 3e-4**: хвіст
`MAE` 0.2397, val `AUC` **0.693**. Вона ж найкраща за val, хоча вибір
цього не бачив.

| | Памʼять | Ручки | val `AUC` |
| --- | ---: | --- | ---: |
| контроль (етап 2 до гріду) | 2 500 | h96, 1 шар, 1e-3 | 0.6922 |
| **обрана гридом** | **5 000** | **h64, 1 шар, 3e-4** | **0.6930** |
| найкращий один рядок хвоста | 5 000 | h96, 2 шари, 3e-4 | 0.6678 |

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

1. **Подвоєння памʼяті не дало нічого.** 0.6930 проти 0.6922 на тому
   самому повному val. Вісім півотів у вікні працюють рівно так само, як
   чотири.
2. **Ручки теж нічого не дали.** Розкид усіх 24 точок за val `AUC` —
   0.58…0.69. Найкраща точка збігається з контрольною з точністю до
   третього знака.
3. **Дві ручки все ж мають напрямок**, і обидва однакові в хвості й на
   val: менший крок навчання (`3e-4` проти `1e-3`) і **один** шар замість
   двох. Це не про якість оцінки, а про те, що мережа перевчається з
   другої епохи, і все, що сповільнює навчання, трохи допомагає.
4. **Раніші 0.741 були завищені популяцією.** Стільки давав етап 2, коли
   мережа мала відповідь лише на пізній частині val (31 208 вікон) — там,
   де вікно цілком лягало після межі навчання. Коли памʼять беруть із
   train і val міряється весь (46 368 вікон), та сама мережа дає 0.6922.
   Число впало не від зміни моделі, а від того, що додались перші
   дванадцять діб val.

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

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

# 2. оцінювач повноти ноги і фільтр — дерева
python3 research/26-leg-share-filter/scripts/leg_share.py

# 3. те саме мережею з памʼяттю на 2500 подій
python3 research/26-leg-share-filter/scripts/share_lstm.py

# 4. драбина разом з мережею констатації розвороту
#    (потрібні готові ряди дослідження 25)
python3 research/25-leg-lstm/scripts/lstm_pivot.py
python3 research/26-leg-share-filter/scripts/two_signals.py

# 5. перебір функцій втрат
python3 research/26-leg-share-filter/scripts/loss_share.py

# 6. грід ручок мережі й контрольна точка на старій памʼяті
python3 research/26-leg-share-filter/scripts/grid_share.py
python3 research/26-leg-share-filter/scripts/grid_share.py \
    --windows 250x10 --hidden 96 --layers 1 --rates 0.001 --append

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

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

- [`research/23-reversal-signal/`](../23-reversal-signal/23-reversal-signal.md) — драбина, сигнал якої тут фільтрується
- [`research/24-val-check/`](../24-val-check/24-val-check.md) — той самий сигнал на val, без фільтра
- [`research/20-leg-loss/`](../20-leg-loss/20-leg-loss.md) — десятина ноги як ціль і чому від неї відмовились
- [`research/25-leg-lstm/`](../25-leg-lstm/25-leg-lstm.md) — мережа констатації розвороту, чиїм словом драбина підтверджується в етапі 3
- [`rules/01-data-and-db.md`](../../rules/01-data-and-db.md) — клас `клас210х10-1х7д/д`
- [`rules/03-models.md`](../../rules/03-models.md) — навчання моделей
