# Сигнал розвороту на клас210х10-1х7д/д

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

## Ціль

Дослідження 21 показало: **у середньому** край ноги виглядає інакше, ніж її
середина — на ході по нозі сім фіч розходяться біля обох півотів і злипаються
в середині. Дослідження 22 показало інше: **окрема пачка з десяти подій** цієї
картини не має, унікального в ній немає, а стеля поділу — `AUC` 0.70–0.74.

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

1. локальний ранг замість пульної бази — прибрати різницю волатильності;
2. пара всередині ноги — порівнювати край із серединою **тієї самої** ноги;
3. довша пачка — зменшити шум усередненням;
4. база за прогресом ноги — підняти частку розворотів умовою на контекст;
5. контекст ноги на вхід моделі — дати те, чого в класі немає взагалі;
6. драбина підтверджень — перепитувати «чи був півот `N` подій тому» з
   дедалі більшою затримкою.

Клас один — `клас210х10-1х7д/д`, той самий, що в дослідженні 22.

## Дані

- Пари: `BTC_USDT`, `ETH_USDT`, `SOL_USDT`, `XRP_USDT`, `BNB_USDT`, `DOGE_USDT`.
- Клас: `data/datasets/<PAIR>/class210x10m1x7dd/{train,val}.npz`
  ([`rules/01-data-and-db.md`](../../rules/01-data-and-db.md) →
  клас210х10-1х7д/д).
- Зігзаг 2.3 % на ема10, півоти й ноги —
  [`research/21-class210x10x7dc/scripts/leg_deciles.py`](../21-class210x10x7dc/scripts/leg_deciles.py).
- Розбиття за датою: **train — усе до `2026-08-14`**. Усі пʼять етапів
  міряються тільки на train.
- Пачка розвороту — сім подій до півота разом із ним і три після
  (`LEAD = 7`, `TAIL = 3`), як у дослідженні 22.

Спільний збір — [`scripts/signal_data.py`](scripts/signal_data.py): значення
класу, сила події, ноги, ціна, ема і контекст.

### Сила події

`mag[подія, фіча]` — модуль по девʼяти плашках, поділений на медіану цієї ж
фічі по подіях **звичайних десяток** монети. Це та сама база, що в теплових
картах дослідження 22: `1.0` означає «як у звичайній десятці цієї монети».

### Контекст ноги

Рахується **причинно** — тільки з того, що видно на момент події. Ковзний
екстремум ема за `LOOK = 400` подій назад:

| Число | Що це |
| ----- | ----- |
| `run` | хід угору від ковзного мінімуму, у частках порогу 2.3 % |
| `draw` | хід униз від ковзного максимуму, у тих самих частках |
| `move` | `max(run, draw)` — наскільки далеко зайшов рух |
| `since_top` | подій від ковзного максимуму |
| `since_low` | подій від ковзного мінімуму |
| `since_pivot` | подій від останнього півота — **з підказкою** |

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

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

1. Локальний ранг замість пульної бази.
2. Край ноги проти середини тієї самої ноги.
3. Чи росте сигнал з довжиною пачки.
4. База розвороту за прогресом ноги.
5. Контекст ноги поруч із числами класу.
6. Драбина підтверджень із дедалі більшою затримкою.

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

| Розділ | Плашка | Ендпоінт | Скрипт |
| ------ | ------ | -------- | ------ |
| 1 | Локальний ранг замість пульної бази | `/api/signal/rank` | [`scripts/local_rank.py`](scripts/local_rank.py) |
| 2 | Край ноги проти середини тієї самої ноги | `/api/signal/pair` | [`scripts/leg_pair.py`](scripts/leg_pair.py) |
| 3 | Чи росте сигнал з довжиною пачки | `/api/signal/length` | [`scripts/pack_length.py`](scripts/pack_length.py) |
| 4 | База розвороту за прогресом ноги | `/api/signal/progress` | [`scripts/leg_progress.py`](scripts/leg_progress.py) |
| 5 | Контекст ноги поруч із числами класу | `/api/signal/context` | [`scripts/leg_context.py`](scripts/leg_context.py) |
| 6 | Драбина підтверджень: чи був півот N подій тому | `/api/signal/delay` | [`scripts/pivot_delay.py`](scripts/pivot_delay.py) |

- Розділи вантажаться незалежно: збій одного не гасить решту.
- Невідомий розділ віддає `404`, порожній замір — плашку з командою запуску.
- Тести: [`tests/test_signal.py`](../../tests/test_signal.py) — скрипти,
  [`tests/test_dashboard.py`](../../tests/test_dashboard.py) → `TestSignalApi`,
  `TestSignalMarkup` — ендпоінти й розмітка.

## Етап 1. Локальний ранг замість пульної бази — `підтверджено`

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

Сила події береться **рангом серед попередніх `look` подій**:

    rank[подія, фіча] = частка попередніх `look` подій, у яких сила менша

Ранг причинний: дивиться тільки назад і саму подію не рахує. У найпершої події
ряду історії немає — там `NaN`, і така пачка відкидається сама. `look = 0` —
стара пульна база, точка відліку.

Пачка дає десять чисел: медіана рангу кожної з семи фіч, `direct` (середнє
рангів `d_up`, `d_down`, `d_price_avg`), `inverse` (середнє чотирьох зворотних
відношень) і `ratio` — їхня різниця. Набори ті самі, що в дослідженні 22,
етап 5: 204 785 пачок train, з них 437 розворотів.

```bash
python3 research/23-reversal-signal/scripts/local_rank.py
```

### Результат: локальна база не додала

| Вікно | Краща ознака | AUC | Точність | До бази | `alone` |
| ----- | ------------ | --- | -------- | ------- | ------- |
| пул | `inverse` | **0.744** | 0.0092 | ×4.29 | 1 |
| 100 | `ratio` | 0.685 | 0.0058 | ×2.70 | 0 |
| 300 | `ratio` | 0.739 | 0.0054 | ×2.53 | 0 |
| 500 | `ratio` | 0.744 | 0.0064 | ×3.02 | 0 |
| 1000 | `ratio` | **0.755** | 0.0084 | ×3.92 | 0 |

Точність — при повноті 10 %.

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

1. **Короткий окіл гірший за пул.** На вікні 100 подій (6 годин) `AUC` падає
   до 0.685: у такому вікні сама пачка розвороту займає десяту частину
   історії й нормується сама на себе.
2. **Довге вікно наздоганяє пул і трохи його переганяє**: 0.755 проти 0.744.
   Виграш є, але це третя цифра після коми — не той порядок, який змінює
   задачу.
3. **Точність при цьому нижча**: ×3.9 проти ×4.3 у пулу. Ранг рівняє хвости,
   а саме в хвості й сидить те, чим розворот відрізняється.

Волатильність — не причина слабкого поділу.

## Етап 2. Край ноги проти середини тієї самої ноги — `підтверджено`

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

| Частина ноги | Які події |
| ------------ | --------- |
| `початок` | перші `edge` подій ноги |
| `кінець` | останні `edge` подій ноги |
| `край` | обидва разом |
| `контроль` | `edge` подій, узятих випадково всередині середини |

Середина — центральна половина ноги, від 25 % до 75 % її довжини. Нога бере
участь, якщо в ній не менше `MIN_EVENTS = 20` подій, вона цілком лежить у
train, а середина не коротша за край.

Число ноги:

    відношення = медіана сили краю / медіана сили середини тієї самої ноги

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

```bash
python3 research/23-reversal-signal/scripts/leg_pair.py
```

### Результат: різниця є майже в кожній нозі

Край 5 + 5 подій, 600 ніг, `p` нижче `1e-9` у кожному рядку:

| Фіча | Частка ніг, де край сильніший | Медіана відношення | Контроль |
| ---- | ----------------------------- | ------------------ | -------- |
| `d_up` | **0.853** | ×3.77 | 0.510 |
| `d_down` | **0.843** | ×3.73 | 0.497 |
| `d_price_avg` | **0.830** | ×1.39 | 0.495 |
| `d_resist_buy` | 0.167 | ×0.175 | 0.488 |
| `d_bal_resist_buy` | 0.188 | ×0.281 | 0.505 |
| `d_support_sell` | 0.190 | ×0.165 | 0.487 |
| `d_bal_support_sell` | 0.202 | ×0.290 | 0.507 |

Контроль — 0.487…0.510, `p` від 0.54 до 0.90. Тобто сам спосіб рахунку дає
рівно монетку, і всі цифри вище — не артефакт методу.

Початок і кінець ноги нарізно (край 5):

| Частина | `d_up` | `d_down` | `d_price_avg` | `d_resist_buy` |
| ------- | ------ | -------- | ------------- | -------------- |
| початок | 0.736 | 0.716 | 0.717 | 0.255 |
| кінець | 0.781 | 0.782 | 0.772 | 0.204 |
| обидва | 0.853 | 0.843 | 0.830 | 0.167 |

Розмір краю майже нічого не міняє:

| Край | Ніг | `d_up`, частка ніг | Медіана | Контроль |
| ---- | --- | ------------------ | ------- | -------- |
| 3 | 600 | 0.840 | ×4.22 | 0.550 |
| 5 | 600 | **0.853** | ×3.77 | 0.510 |
| 10 | 600 | 0.848 | ×3.40 | 0.538 |
| 20 | 583 | 0.837 | ×3.06 | 0.532 |

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

1. **Ефект справжній і дуже стабільний.** У 85 % ніг край сильніший за
   середину за `d_up`, у 83 % — за `d_price_avg`, і в 81–83 % зворотні
   відношення на краю **менші**. Це не середнє по пулу, це рахунок по ногах.
2. **Медіана відношення ×3.8** — край не «трохи інший», а в кілька разів
   активніший за середину своєї ноги.
3. **Кінець ноги сильніший за початок** (0.78 проти 0.74), а разом вони дають
   0.85: обидва краї несуть той самий знак.
4. **Довжина краю не критична**: від 3 до 20 подій частка ніг тримається
   0.84–0.85, падає лише сила відношення (×4.2 → ×3.1). Тобто «край» — це
   десятки подій, а не рівно пʼять.
5. **Чому пул цього не бачив.** Порівняння з рештою датасету питає «чи ця
   десятка сильна взагалі», а сильних десяток у датасеті багато. Питання «чи
   ця десятка сильніша за середину **своєї** ноги» відповіді має інші —
   і саме воно відповідає тому, що видно на ході по нозі.

## Етап 3. Чи росте сигнал з довжиною пачки — `підтверджено`

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

Пропорція пачки та сама: `lead = round(0.7 · W)`, `tail = W − lead`. Розворот —
одна пачка на півот, решта — усі пачки тієї ж довжини з кроком в одну подію,
що не торкаються зони. Ознаки — ті самі десять, що в етапі 1 на пульній базі.

```bash
python3 research/23-reversal-signal/scripts/pack_length.py
```

### Результат: росте, але вчетверо повільніше за корінь

| W | до / після | Пачок | Розворотів | Краща ознака | AUC | До бази | Підйом | Корінь |
| - | ---------- | ----- | ---------- | ------------ | --- | ------- | ------ | ------ |
| 10 | 7 / 3 | 204 785 | 437 | `inverse` | 0.744 | ×4.29 | 1.00 | 1.00 |
| 20 | 14 / 6 | 154 318 | 346 | `inverse` | 0.748 | ×4.95 | 1.02 | 1.41 |
| 40 | 28 / 12 | 100 819 | 262 | `inverse` | 0.754 | ×5.12 | 1.04 | 2.00 |
| 60 | 42 / 18 | 70 365 | 186 | `d_up` | **0.767** | ×5.58 | 1.10 | 2.45 |
| 100 | 70 / 30 | 36 429 | 122 | `d_up` | 0.765 | ×10.27 | 1.09 | 3.16 |

Підйом — у скільки разів `AUC` відійшла від монетки порівняно з `W = 10`;
корінь — скільки цей підйом мав би бути за `sqrt(W / 10)`.

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

1. **Поділ росте**: 0.744 → 0.767, і точність при повноті 10 % росте разом із
   ним, з ×4.3 до ×10.3 на сотні подій.
2. **Але росте вчетверо повільніше за корінь**: підйом 1.10 замість 2.45 на
   `W = 60`. Тобто це **не** чистий зсув середнього з незалежним шумом:
   сусідні події корельовані, і додаткові події додають менше нової
   інформації, ніж дав би незалежний замір.
3. **Після 60 подій зупиняється**: на 100 `AUC` уже не росте (0.765), а пачок
   лишається 36 тисяч. Довша пачка накриває пів ноги і починає мішати край із
   серединою.
4. **Робоча довжина — 40–60 подій**, тобто 2.3–3.5 години. Це і є той масштаб,
   на якому «розширення» з ходу по нозі стає помітним в окремій пачці.

## Етап 4. База розвороту за прогресом ноги — `підтверджено`

Питання «чи ця десятка — розворот», задане без контексту, має базу 0.21 %:
437 розворотів на 212 452 пачки train. Але ми знаємо, скільки рух уже пройшов.
Тут база рахується **всередині кожної клітинки контексту**, і поруч — той
самий поділ ознакою класу (`ratio`) у самій клітинці.

Контекст береться на **останній події** пачки.

```bash
python3 research/23-reversal-signal/scripts/leg_progress.py
```

### Результат: `move` піднімає базу вдвічі

| `move` | Пачок | Розворотів | База | До загальної | AUC усередині |
| ------ | ----- | ---------- | ---- | ------------ | ------------- |
| 0…0.25 | 3 685 | 2 | 0.05 % | ×0.26 | 0.976 |
| 0.25…0.5 | 27 926 | 3 | 0.01 % | ×0.05 | 0.690 |
| 0.5…0.75 | 35 893 | 21 | 0.06 % | ×0.28 | 0.713 |
| 0.75…1.0 | 35 536 | 39 | 0.11 % | ×0.53 | 0.664 |
| ≥ 1.0 | 109 412 | 372 | **0.34 %** | ×1.65 | 0.669 |

### Результат: `since_low` і `since_top` піднімають її в 35 разів

| `since_low` | Пачок | Розворотів | База | До загальної | AUC усередині |
| ----------- | ----- | ---------- | ---- | ------------ | ------------- |
| 0…3 | 16 611 | 0 | 0 % | ×0 | — |
| **3…5** | **2 509** | **180** | **7.17 %** | **×34.9** | 0.623 |
| 5…10 | 5 111 | 0 | 0 % | ×0 | — |
| 10…20 | 7 716 | 1 | 0.01 % | ×0.06 | 0.714 |
| 20…50 | 15 439 | 6 | 0.04 % | ×0.19 | 0.837 |
| 50…100 | 17 678 | 13 | 0.07 % | ×0.36 | 0.876 |
| 100…200 | 25 817 | 25 | 0.10 % | ×0.47 | 0.793 |
| ≥ 200 | 121 571 | 212 | 0.17 % | ×0.85 | 0.742 |

`since_top` дає дзеркальну картину: клітинка `3…5` — 2 654 пачки, 187
розворотів, база 7.05 %, ×34.3.

Межа зверху, `since_pivot` (**з підказкою**): клітинка `0…3` — 1 304 пачки,
437 розворотів, база 33.5 %, ×162.9. Усі розвороти лежать у ній, бо це та сама
відповідь, подана числом.

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

1. **Найсильніший контекст — не «як далеко зайшов рух», а «коли востаннє був
   екстремум».** `move ≥ 1` дає лише ×1.65, а `since_low ∈ [3, 5)` — ×34.9.
2. **Чому саме 3…5.** Пачка закінчується через `TAIL = 3` події після півота, а
   півот зігзага 2.3 % — це екстремум ема. Отже в пачці розвороту ковзний
   екстремум був рівно три події тому. Це **геометрія мітки**, а не властивість
   класу: контекст ловить кандидатів, а не підтверджені розвороти.
3. **Кандидатів усе одно багато.** У клітинці `3…5` — 2 509 пачок і 180
   розворотів: 93 % таких пачок розворотом не стали. Підтвердження — це рух на
   2.3 % у інший бік, а він стається пізніше, і жодне причинне число його
   зараз не знає.
4. **AUC класу всередині клітинки не росте** (0.623 у `3…5` проти 0.744 по
   пулу). Клас відділяє розворот від далеких від екстремуму подій, а від інших
   кандидатів у розворот — ні. Це та сама межа, що в дослідженні 22.

## Етап 5. Контекст ноги поруч із числами класу — `підтверджено`

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

| Набір | Що подається |
| ----- | ------------ |
| `клас` | 62 ознаки пачки з дослідження 22, етап 5 |
| `контекст` | `move`, `run`, `draw`, `since_top`, `since_low` |
| `клас+контекст` | обидва разом |
| `клас+підказка` | плюс `since_pivot` — число, якого онлайн немає |

Задача: пачка з десяти подій підряд, крок в одну подію, мітка `1`, якщо півот
стоїть на сьомій події пачки. Беруться **всі** пачки train — 212 452, з них 437
розворотів. Модель — `HistGradientBoosting`, перевірка **відкладеною монетою**:
навчання на пʼятьох монетах, оцінка на шостій.

```bash
python3 research/23-reversal-signal/scripts/leg_context.py
```

### Результат: контекст важить у тридцять разів більше за клас

| Набір | Входів | AUC | AP | Точність | До бази |
| ----- | ------ | --- | -- | -------- | ------- |
| клас | 62 | 0.721 | 0.0055 | 0.008 | ×3.8 |
| **контекст** | **5** | **0.982** | **0.191** | **0.250** | **×121.5** |
| клас+контекст | 67 | 0.971 | 0.160 | 0.210 | ×101.9 |
| клас+підказка ⚠ | 68 | 0.9999 | 0.971 | 1.000 | ×486.2 |

Точність — при повноті 10 %, база — 0.21 %.

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

Одна точка нічого не каже про робочу точку. Поріг ставиться на задану
повноту, і рахується, скільки чужих пачок при цьому проходить. Сигналів на
добу — на одну монету; у замірі **516 монетодіб**.

`контекст`, пʼять причинних чисел:

| Повнота | Розворотів | Хибних | Точність | До бази | Сигналів на добу |
| ------- | ---------- | ------ | -------- | ------- | ---------------- |
| 5 % | 26 | 65 | **28.6 %** | ×138.9 | 0.18 |
| 10 % | 54 | 162 | 25.0 % | ×121.5 | 0.42 |
| 20 % | 90 | 275 | 24.7 % | ×119.9 | 0.71 |
| 30 % | 132 | 390 | 25.3 % | ×122.9 | 1.01 |
| 50 % | 219 | 904 | 19.5 % | ×94.8 | 2.17 |
| 70 % | 306 | 1 492 | 17.0 % | ×82.7 | 3.48 |
| 90 % | 393 | 8 180 | 4.6 % | ×22.3 | 16.60 |

`клас`, ті самі пороги:

| Повнота | Розворотів | Хибних | Точність | До бази | Сигналів на добу |
| ------- | ---------- | ------ | -------- | ------- | ---------------- |
| 5 % | 23 | 3 168 | 0.72 % | ×3.5 | 6.18 |
| 10 % | 44 | 5 557 | 0.79 % | ×3.8 | 10.85 |
| 20 % | 88 | 10 241 | **0.85 %** | ×4.1 | 20.00 |
| 50 % | 219 | 39 851 | 0.55 % | ×2.7 | 77.60 |
| 90 % | 393 | 153 135 | 0.26 % | ×1.2 | 297.32 |

Межа зверху `клас+підказка`: точність **100 %** до повноти 20 % і 92 % на
повноті 90 % — задача розвʼязна до кінця, щойно в неї подати відповідь.

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

1. **Пʼять причинних чисел контексту дають ×121 до бази, всі 62 числа класу —
   ×3.8.** Серед відібраних пачок кожна четверта справді розворот проти двох
   на тисячу без відбору.
2. **Клас поверх контексту не допомагає, а трохи заважає**: 0.971 проти 0.982,
   ×102 проти ×121. Шістдесят два слабких входи на 437 позитивів — це шум, у
   якому дерева знаходять неіснуючі правила.
3. **Це не означає, що розворот розпізнано.** Контекст ловить **кандидатів** —
   пачки біля свіжого екстремуму ема; підтвердження руху на 2.3 % лежить у
   майбутньому. Тому й `AP` 0.19, а не 0.9: серед кандидатів розворотів
   меншість.
4. **Межа зверху** (`клас+підказка`) показує, що задача сама по собі
   розвʼязна до одиниці, щойно в неї подати відповідь. Різниця між 0.971 і
   0.9999 — це рівно те, чого причинно знати не можна.
5. **Куди це веде.** Сигнал розвороту в цих даних будується не з класу, а з
   **положення в русі**; клас може лише сортувати кандидатів, і саме там його
   ×3.8 має сенс — усередині клітинки контексту, а не по всьому датасету.
6. **Робоча точка — повнота 20–30 %.** Точність тримається 25 % аж до
   повноти 30 %, а після 70 % завалюється до 4.6 %: там поріг уже пускає
   все підряд. Тобто відповісти «півот був десять хвилин тому» можна на
   кожен четвертий сигнал, ловлячи при цьому кожен третій півот, ціною
   одного сигналу на монету за добу.

## Етап 6. Драбина підтверджень: чи був півот `N` подій тому — `підтверджено`

Етап 5 питав одне: «півот був три події тому» — і відповідав з точністю
25 %. Три події це 10.5 хвилини, і підтвердити розворот за такий час
нічим: зігзаг ставить півот, коли ціна пройшла 2.3 % назад.

Тут те саме питання ставиться **знову і знову, з дедалі більшою
затримкою**: `N` = 3, 13, 23, … 203 події тому. Чим більше `N`, тим
більше ціна встигла показати.

### Мітка

Пачка з десяти подій закінчується на події `e` — це «зараз».

    y[e] = 1, якщо півот стоїть на події e − N

При `N = 3` це рівно мітка етапу 5, тому перший щабель із ним
збігається.

Питання ставиться на **кожній події** ряду і про **кожен** півот. Умов
немає: ні на довжину ноги, ні на суцільність пачки, ні на `NaN` у фічах
класу — де ознак немає, вони йдуть нулями, а контекст ціни визначений
завжди. Тому розворотів на кожному щаблі однаково: **607**.

Це навмисно. Будь-який відбір прикладів прибирає з заміру саме ті місця,
де даних бракує, — а в роботі модель питатимуть і там теж.

Ноги: 695 штук, подій у нозі — медіана **423**, чверть 216, три чверті
793.

### Набори входів

| Набір | Що подається |
| ----- | ------------ |
| `контекст` | пʼять причинних чисел **зараз**, вікно 400 подій |
| `клас+контекст` | плюс 62 ознаки пачки **кандидата** — `LEAD` подій до `e − N` включно і `TAIL` після, тобто пачки, що закінчилась на `e − N + 3` |
| `вікна` | ті самі пʼять чисел на **чотирьох** вікнах: 200, 400, 800, 1600 подій |
| `вікна+клас` | чотири вікна разом з ознаками кандидата |

Клас береться біля кандидата, а не «зараз»: на події `e` його пачка вже
в минулому — остання її подія `e − N + TAIL` не пізніша за «зараз» на
жодному щаблі. Контекст навпаки береться зараз — у ньому лежить те, що
сталося після кандидата.

```bash
python3 research/23-reversal-signal/scripts/pivot_delay.py
```

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

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

Точність при повноті 30 %, перевірка відкладеною монетою. Розворотів на
кожному щаблі 607, пачок 356 тисяч.

| `N`, подій | Хвилин | `контекст` | `вікна` | AUC `вікна` | AP `вікна` |
| ---------- | ------ | ---------- | ------- | ----------- | ---------- |
| 3 | 10 | 22.2 % | 25.6 % | 0.990 | 0.209 |
| 13 | 46 | 26.7 % | 41.4 % | 0.991 | 0.344 |
| 23 | 80 | 31.1 % | 47.5 % | 0.989 | 0.415 |
| 33 | 116 | 27.5 % | 48.2 % | 0.991 | 0.433 |
| 43 | 150 | 35.0 % | 49.9 % | 0.988 | 0.431 |
| 53 | 186 | 25.3 % | 49.3 % | 0.986 | 0.468 |
| 63 | 220 | 32.5 % | 57.0 % | 0.984 | 0.492 |
| 73 | 256 | 27.2 % | 61.4 % | 0.983 | 0.517 |
| 83 | 290 | 33.1 % | 64.7 % | 0.985 | 0.529 |
| 93 | 326 | 30.3 % | 69.2 % | 0.985 | 0.561 |
| **103** | **360** | 29.5 % | **71.5 %** | 0.982 | **0.568** |
| 113 | 396 | 30.4 % | 58.6 % | 0.980 | 0.549 |
| 123 | 430 | 25.9 % | 47.1 % | 0.984 | 0.454 |
| 133 | 466 | 26.2 % | 50.0 % | 0.984 | 0.431 |
| 143 | 500 | 24.2 % | 49.0 % | 0.981 | 0.429 |
| 153 | 536 | 21.7 % | 66.3 % | 0.982 | 0.549 |
| 163 | 570 | 23.9 % | 67.8 % | 0.988 | 0.575 |
| 173 | 606 | 22.9 % | 56.8 % | 0.980 | 0.504 |
| 183 | 640 | 25.3 % | 51.5 % | 0.977 | 0.470 |
| 193 | 676 | 23.3 % | 49.2 % | 0.983 | 0.429 |
| 203 | 710 | 24.3 % | 48.7 % | 0.972 | 0.460 |

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

1. **Затримка сама по собі не допомагає.** Одне вікно на 400 подій дає
   22–33 % і через десять хвилин, і через дванадцять годин. За двадцять
   один щабель — жодного тренду.
2. **Річ у довжині вікна.** Півот зігзага — це екстремум **усієї ноги**,
   а нога має медіану 423 події. У вікні 400 подій «ціна впала на 2.3 %
   від максимуму» трапляється всередині довгої ноги багато разів: кожен
   нижчий максимум дає ту саму картину, а півотом був тільки перший.
   Чотири вікна — 200, 400, 800, 1600 — дозволяють відрізнити максимум
   ноги від локального.
3. **З чотирма вікнами драбина працює як задумано**: 26 % на десяти
   хвилинах, 47 % на восьмидесяти, 57 % на трьох з половиною годинах,
   71 % на шести. `AP` росте з 0.21 до 0.57.
4. **Насичення близько 100 подій** (шість годин). Далі числа гуляють
   47–68 % без тренду: це шум вибірки при повноті 30 %, тобто на 180
   спійманих із 607.
5. **Клас не додає нічого і тут.** `вікна+клас` — 82 входи проти 20 —
   дає то більше, то менше (24.1 %, 55.0 %, 67.9 % на щаблях 3, 43 і
   103 проти 25.6 %, 49.9 % і 71.5 %). Той самий висновок, що в етапі 5.
6. **Практичний режим із цього такий:** ставити кандидата одразу (26 %
   на десятій хвилині), а далі **перепитувати кожні десять подій** —
   через годину впевненість піднімається до 47 %, через три з половиною
   до 57 %, через шість до 71 %. Це і є підтвердження, якого не давав
   одиничний замір.

## Загальний висновок

| Спосіб | Що дав |
| ------ | ------ |
| локальний ранг | нічого: 0.755 проти 0.744 |
| пара всередині ноги | **85 % ніг** — різниця справжня і стабільна |
| довша пачка | росте до 0.767 на `W` 40–60, далі впирається |
| база за прогресом | ×34.9 у клітинці `since_low ∈ [3, 5)` |
| контекст на вхід | **×121 до бази** проти ×3.8 у класу |
| драбина затримок | одне вікно — 22–35 % на будь-якій затримці; **чотири вікна — до 71 %** |

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

Етап 6 добудовує це до робочої схеми: положення в русі треба міряти
**на кількох масштабах** і перепитувати з часом. Тоді «чи був півот
`N` подій тому» відповідається з 26 % одразу і 71 % через шість годин.

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

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

# 2. локальний ранг замість пульної бази
python3 research/23-reversal-signal/scripts/local_rank.py

# 3. край ноги проти середини тієї самої ноги
python3 research/23-reversal-signal/scripts/leg_pair.py

# 4. довжина пачки
python3 research/23-reversal-signal/scripts/pack_length.py

# 5. база за прогресом ноги
python3 research/23-reversal-signal/scripts/leg_progress.py

# 6. контекст поруч із класом
python3 research/23-reversal-signal/scripts/leg_context.py

# 7. драбина підтверджень
python3 research/23-reversal-signal/scripts/pivot_delay.py

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

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

- [`rules/01-data-and-db.md`](../../rules/01-data-and-db.md) — опис класу
- [`rules/02-markup.md`](../../rules/02-markup.md) — зігзаг і розмітка
- [`rules/03-models.md`](../../rules/03-models.md) — навчання моделей
- [`research/22-leg-edge-spread/`](../22-leg-edge-spread/22-leg-edge-spread.md)
  — краї ноги й пошук унікального в пачці розвороту
- [`research/21-class210x10x7dc/`](../21-class210x10x7dc/21-class210x10x7dc.md)
  — звідки взявся клас і хід фіч по нозі
- [`research/24-val-check/`](../24-val-check/24-val-check.md) — ті самі моделі
  на val, поза часом навчання
- [`research/25-leg-lstm/`](../25-leg-lstm/25-leg-lstm.md) — те саме питання
  мережею з памʼяттю, без агрегатів
