# Дослідження 05 — когорти подій і прогноз наступної когорти

| | |
| --- | --- |
| Пара | BTC_USDT |
| Клас подій | `клас210х14х2` |
| Вибірка | `train` (тільки вона) |
| Статус | етапи 1–7 підтверджені 2026-08-23 |
| Правила | [`01-data-and-db.md`](../../rules/01-data-and-db.md), [`02-markup.md`](../../rules/02-markup.md), [`03-models.md`](../../rules/03-models.md), [`05-visualization.md`](../../rules/05-visualization.md) |

## Ціль

Два кроки, один за одним:

1. **Виділити когорти подій** — поділити події `клас210х14х2` на чотири
   когорти за місцем у зігзагу.
2. **Навчити їх ідентифікацію** — по **пачці з N підряд подій** сказати,
   у якій з цих когорт лежатиме **наступна** подія. Пачка починається
   з 2 подій; якщо результату немає — пачка росте на +1 подію, і так до
   стелі, тобто кількості подій у 4 рухах зігзагу 2.3%.

Другий крок рахується **математично**: без ітеративного навчання,
у закритій формі.

## Дані

| | |
| --- | --- |
| Вхід | `data/datasets/BTC_USDT/class210x14x2/train.npz` |
| Подій train | 24 150 |
| Форма події | `(15, 2)` |
| Період | 2026-02-14 … 2026-08-16 UTC |
| Півоти на ряді train | 116 (зігзаг 2.3) і 472 (вкладений 0.8) |
| Пачок (цілей) | 23 194 |
| Стеля пачки | 833 події = 4 × 208.15 (середній рух зігзагу 2.3, 115 рухів) |

`val` і `test` не використовуються прогнозом: і пачка, і ціль лежать у `train`.
Кількості когорт (етап 1) рахуються по всіх трьох вибірках.

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

| # | Етап | Статус |
| - | ---- | ------ |
| 1 | Когорти подій: рух 2.3 × відрізок 0.8, схема й кількості | **підтверджено** |
| 2 | Пачка й ціль: N минулих подій → когорта наступної; ціль у пачку не входить | **підтверджено** |
| 3 | Метод: лінійний дискримінант у закритій формі, два варіанти апріорних | **підтверджено** |
| 4 | Чесна перевірка всередині train: 5 блокових частин + вилучення смуги | **підтверджено** |
| 5 | Критерії «результат є / нема»: A — напрямок, B — усі чотири когорти | **підтверджено** |
| 6 | Сходинки пачки 2 → 833 кроком +1 і відповідь | **підтверджено** |
| 7 | Вкладка «Когорти подій і прогноз» на дашборді | **підтверджено** |

Усі етапи підтверджені 2026-08-23.

## Скрипти

| Скрипт | Що робить | Вихід |
| ------ | --------- | ----- |
| [`rules/scripts/build_class210x14x2.py`](../../rules/scripts/build_class210x14x2.py) | датасет `клас210х14х2` з `клас210х14х4` | `data/datasets/BTC_USDT/class210x14x2/` |
| [`scripts/class_schema.py`](scripts/class_schema.py) | `phases()` — когорта кожної події, схема й кількості | `results/classes.json` |
| [`scripts/next_cohort.py`](scripts/next_cohort.py) | пачки, дискримінант, сходинки, метрики, дані для вкладки | `results/` |

```bash
python3 rules/scripts/build_class210x14x2.py --pair BTC_USDT
python3 research/05-next-cohort-forecast/scripts/class_schema.py
python3 research/05-next-cohort-forecast/scripts/next_cohort.py
python3 dashboard/app.py          # вкладка «Когорти подій і прогноз»
```

Повний прогон — сходинки 2…833 кроком +1, 1 хв 42 с.

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

## Етап 1. Когорти подій

Півоти зігзагу 2.3 і вкладеного 0.8 складаються в один ряд за часом. Ділянка
між двома сусідніми півотами — **відрізок**; тип відрізка задають розміри
півотів на його кінцях. Подія лежить рівно в одному відрізку (півот належить
наступному руху), звідки бере дві мітки: напрямок великого руху 2.3, у який
вкладено відрізок, і напрямок самого відрізка. Пара міток і є **когорта**.

| Когорта | Рух 2.3 | Відрізок | train | val | test |
| ------- | ------- | -------- | ----- | --- | ---- |
| `up` — зростання | вгору | вгору | 8 878 | 2 404 | 3 798 |
| `up_pull` — протирух у зростанні | вгору | вниз | 2 742 | 676 | 1 032 |
| `down` — падіння | вниз | вниз | 9 123 | 2 249 | 3 913 |
| `down_pull` — протирух у падінні | вниз | вгору | 3 195 | 660 | 1 239 |
| поза зігзагом | — | — | 212 | 43 | 78 |

Когорту рахує `phases()` у [`scripts/class_schema.py`](scripts/class_schema.py):
півоти 2.3 і 0.8 зливаються в один ряд за індексом, подія `i` лежить у відрізку
`left.index <= i < right.index` (півот належить наступному руху), останній півот
ряду лишається у своєму відрізку.

Когорти не перетинаються: сума по когортах плюс події поза зігзагом дорівнює
розміру вибірки (перевіряється тестом `test_counts_add_up_to_sample_size`).

Розклад по типах відрізків, train:

| Тип | Що це | `up` | `up_pull` | `down` | `down_pull` |
| --- | ----- | ---: | ---: | ---: | ---: |
| 2.3-0.8 | від великого півота до першої корекції, за трендом | 2 324 | 0 | 2 864 | 0 |
| 0.8-0.8 | між двома корекціями, за трендом | 3 105 | 0 | 3 570 | 0 |
| **0.8-0.8 проти** | між двома корекціями, проти тренду | 0 | 2 742 | 0 | 3 195 |
| 0.8-2.3 | від останньої корекції до великого півота, за трендом | 2 469 | 0 | 1 771 | 0 |
| 2.3-2.3 | рух без жодної корекції | 980 | 0 | 918 | 0 |

Протирух винесений в окремий тип відрізка `0.8-0.8 проти` — це окрема когорта,
вона не змішується з рухом за трендом у тому самому місці зігзагу. Решта нулів
структурні: перший і останній відрізки великого руху впираються в його ж
вершини, тому завжди йдуть за трендом.

Протируху по вибірках: train 5 937, val 1 336, test 2 271.

**Цілями прогнозу стають не всі події.** Перші 833 (стеля пачки) працюють
тільки як пачка. Цілей — 23 194: `up` 8 719, `up_pull` 2 721, `down` 8 739,
`down_pull` 3 015; 212 подій поза зігзагом у цілі не входять.

## Етап 2. Пачка й ціль

**Точка події** — середнє по її 15 блоках, окремо по `balance` і `d_balance`:
2 числа на подію. Так само, як у дослідженні 04 (там усереднення по події
розділяло напрямок вдвічі краще за окремі блоки).

**Пачка** — N подій підряд **перед** ціллю, у хронологічному порядку вибірки.
Фічі пачки — `2 × N` чисел, колонки йдуть за лагом: лаг 1 (подія перед ціллю),
лаг 2, …, лаг N. Тому пачка розміру N — це перші `2N` колонок матриці,
побудованої на стелі.

**Ціль** — когорта наступної події. Її власні фічі в прогнозі **не беруть
участі**: інакше це було б не передбачення, а опис.

Набір цілей один для всіх N (береться по стелі 833) — сходинки порівнюються
на однакових подіях.

Чого немає в пачці:

- **міток минулих подій.** Когорта будується зігзагом, а зігзаг підтверджується
  майбутнім рухом, тому в реальному часі мітка попередньої події невідома.
  Наскільки це сильна інформація — видно з оракула персистентності нижче.
- **ціни.** Пачка — це `клас210х14х2` і нічого крім нього.

## Етап 3. Метод

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

```
score_c(x) = xᵀ Σ⁻¹ μ_c − ½ μ_cᵀ Σ⁻¹ μ_c + ln π_c
```

Усе рахується в закритій формі з сум по навчальній частині:

```
μ_c = (Σ x у когорті c) / n_c
W   = Σ_c (X_c − μ_c)ᵀ(X_c − μ_c) = G − Σ_c n_c μ_c μ_cᵀ,   G = XᵀX
Σ   = W / (n − 4)
```

Ітерацій, кроків навчання, seed і підганяльних коефіцієнтів немає —
результат однозначний. Коваріація обертається як є: подій 23 194 проти
1 666 колонок на найбільшій пачці, обумовленість не перевищує 65.
Обумовленість перевіряється на кожному прогоні: гірша за 10¹² — помилка.

**Апріорні `π_c` беруться у двох варіантах**, бо когорти дуже різні за
розміром:

| Варіант | `π_c` | Що максимізує | Метрика |
| ------- | ----- | ------------- | ------- |
| `частоти когорт` | частка когорти | загальну частку влучень | точність |
| `рівні апріорні` | 0.25 | влучення в кожну когорту однаково | збалансована точність |

Ваги `w = Σ⁻¹ μ` в обох однакові, різниця тільки у зсуві `ln π_c`.

## Етап 4. Чесна перевірка всередині train

Пачки сусідніх цілей перекриваються, тому рендомний поділ протік би: у пачці
перевірочної цілі лежали б події, на яких вчилися. Тому:

- train ділиться на **5 блокових частин** підряд у часі;
- прогноз для кожної частини рахується моделлю, навченою на решті;
- з навчальної частини вилучається смуга **± 833 події** (стеля пачки) навколо
  перевірочної — жодна навчальна пачка не перетинає перевірочну.

Смуга однакова для всіх N, тому сходинки порівнюються між собою напряму.

Мітки когорт будуються зігзагом по ряду цін усієї вибірки — це властивість
самої розмітки (когорта відома лише заднім числом), а не витік фіч.

## Етап 5. Критерії «результат є / нема»

| Критерій | Що перевіряє | Умова |
| -------- | ------------ | ----- |
| **A** — напрямок | чи взагалі є сигнал | точність > база + 2 похибки |
| **B** — усі 4 когорти | чи прогноз працює по кожній когорті | збалансована точність (рівні апріорні) > 0.25 + 2 похибки |

- **База** — «завжди найчастіша когорта»: `down`, точність **0.3768**,
  похибка 0.0032 (біноміальна на 23 194 пачках).
- **Збалансована точність** — середнє `recall` по чотирьох когортах; у бази
  вона рівно 0.25, як і у випадкового вибору.
- 2 похибки ≈ рівень 95%.

## Етап 6. Сходинки

Пачка 2 → 833 кроком +1, усі 832 сходинки пораховані.

| Пачка N | Точність | Проти бази | Збалансована | Макро-F1 | `recall` по когортах |
| ---: | --- | --- | --- | --- | --- |
| 2 | 0.3971 | +6.4 похибки | 0.2840 | 0.2265 | 0.545 / 0.000 / 0.510 / 0.000 |
| 3 | 0.4013 | +7.7 | 0.2878 | 0.2289 | 0.545 / 0.000 / 0.521 / 0.000 |
| 4 | 0.4061 | +9.2 | **0.2905** | 0.2316 | 0.553 / 0.000 / 0.526 / 0.000 |
| 5 | 0.4069 | +9.5 | 0.2886 | 0.2321 | 0.553 / 0.000 / 0.528 / 0.000 |
| 10 | 0.4086 | +10.0 | 0.2826 | 0.2331 | 0.550 / 0.000 / 0.536 / 0.000 |
| 100 | 0.4098 | +10.4 | 0.2646 | 0.2338 | 0.556 / 0.000 / 0.533 / 0.000 |
| **203** | **0.4182** | **+13.0** | 0.2833 | 0.2495 | 0.552 / 0.001 / 0.551 / 0.022 |
| 400 | 0.4032 | +8.3 | 0.2648 | 0.2541 | 0.527 / 0.006 / 0.525 / 0.050 |
| 600 | 0.3816 | +1.5 | 0.2512 | 0.2453 | 0.495 / 0.015 / 0.498 / 0.047 |
| 833 | 0.3821 | +1.7 | 0.2610 | 0.2648 | 0.474 / 0.041 / 0.501 / 0.081 |

Колонка «Збалансована» — варіант «рівні апріорні» (це критерій B), решта
колонок — варіант «частоти когорт». `recall` — у порядку `up` / `up_pull` /
`down` / `down_pull`. Повна таблиця всіх 832 сходинок —
`results/forecast.json` і вкладка дашборду.

**Відповідь на питання дослідження: результат є вже на пачці з 2 подій.**
Обидва критерії виконуються на N = 2 — додавати події не довелось. Далі:

- точність росте до **0.4182 на пачці 203 події** (+13.0 похибки над базою),
  потім падає — на пачці 600+ подій майже до бази;
- збалансована точність найбільша на пачці **4 події** (0.2905) і на 203
  (0.2833);
- **оракул персистентності — 0.9757.** Це «когорта цілі = когорта попередньої
  події»: мітки заднім числом, тому не модель, а верхня межа. Структура
  в задачі є (когорти йдуть довгими відрізками), але з фіч пачки дістається
  мала її частина.

### Що саме прогнозується

Матриця помилок на найкращій пачці (203 події, апріорні = частоти):

| насправді \ прогноз | зростання | протирух у зростанні | падіння | протирух у падінні |
| --- | ---: | ---: | ---: | ---: |
| **зростання** (8 719) | **4 815** | 7 | 3 821 | 76 |
| **протирух у зростанні** (2 721) | 1 009 | **4** | 1 676 | 32 |
| **падіння** (8 739) | 3 809 | 17 | **4 814** | 99 |
| **протирух у падінні** (3 015) | 1 580 | 4 | 1 365 | **66** |

Прогноз фактично **двійковий**: обидві когорти протируху майже не
називаються (`recall` 0.001 і 0.022), бо вони втричі менші за свої когорти
руху за трендом, а поле їх не відділяє. Це та сама картина, що в дослідженні
04: поле розділяє напрямок, і слабко.

Варіант «рівні апріорні» називає всі чотири когорти, але точність падає
нижче бази (0.2029 на пачці 2): рівні апріорні виправляють дисбаланс і
показують, скільки в фічах є насправді — збалансована точність 0.28…0.29
при випадковій 0.25.

## Етап 7. Вкладка «Когорти подій і прогноз»

Дашборд, власний URL [`/cohorts`](http://localhost:8080/cohorts). Одна вкладка
на все дослідження, порядок згори вниз повторює порядок роботи: спочатку
когорти, потім прогноз.

**Когорти** — дані з `results/classes.json` через `/api/classes`:

- схема зігзагу, кожен відрізок пофарбовано за когортою і підписано типом,
  когортою і кількістю подій;
- підвал зі спільною віссю X — скільки подій у кожному відрізку;
- наведення на відрізок показує його тип, когорту і кількість;
- таблиця кількостей по типах відрізків, перемикач `train / val / test`.

Відхилення від [`rules/05-visualization.md`](../../rules/05-visualization.md):
насиченість кольору тут кодує не величину, а належність відрізка до тренду 2.3
(бліді відтінки — протирух). Без цього дві когорти протируху злилися б із двома
когортами руху за трендом.

**Прогноз** — дані з `results/forecast.json` через `/api/forecast`:

- ключові числа: з якої пачки виконано кожен критерій, найкраща точність,
  база, оракул;
- графік: точність від розміру пачки, лінія бази і лінія «база + 2 похибки»;
- підвал зі спільною віссю X — збалансована точність, лінія випадкового 0.25;
- наведення показує метрики сходинки й `recall` по всіх чотирьох когортах;
- клік по графіку обирає пачку — під графіком її матриця помилок і таблиця
  `precision` / `recall` / `f1`;
- перемикач апріорних: `частоти когорт` / `рівні апріорні`;
- колесо миші — масштаб, перетягування — зсув, подвійний клік — скинути;
  зум графіка й підвалу спільний.

## Приклади

Ціль — подія `933` вибірки train (`timestamp` 1771620450, UTC), пачка N = 2.
Точки двох подій перед нею:

```
лаг 1 — подія 932:  balance -0.4129   d_balance -0.3941
лаг 2 — подія 931:  balance -0.4971   d_balance -0.3535
```

Пачка — вектор із 4 чисел у порядку лагів:

```
x = [-0.4129, -0.3941, -0.4971, -0.3535]
```

Оцінки чотирьох когорт (модель навчена без частини, у якій лежить ця ціль;
апріорні = частоти когорт):

```
up        -0.882      <- найбільша, це і є прогноз
down      -1.354
down_pull -1.941
up_pull   -2.632
```

Насправді подія 933 лежить у когорті `up` — прогноз збігся.

Відтворення:

```python
import sys; sys.path.insert(0, "research/05-next-cohort-forecast/scripts")
import next_cohort as nc

sample = nc.load_sample(nc.DEFAULT_INPUT)
y = nc.cohorts(sample["price"], 0.023, 0.008)["y"]
rows = nc.packet_rows(y, sample["points"], 833)
F = nc.design(sample["points"], rows, 833)
blocks = nc.blocked_folds(len(rows), 5)
keep = nc.band(len(rows), blocks[0], 833)
model = nc.discriminant(
    nc.subtract(nc.moments(F, y[rows]), nc.moments(F[keep], y[rows][keep])), 4)
print(F[100, :4] @ model["w"] + model["b"]["freq"])   # оцінки для події 933
```

Рядок сходинки N = 2 у `results/forecast.json`:

```json
{"n": 2,
 "freq":  {"accuracy": 0.3971, "balanced": 0.2638, "macro_f1": 0.2265,
           "recall": [0.545, 0.0, 0.510, 0.0],
           "confusion": [[4752, 0, 3967, 0], [1099, 0, 1622, 0],
                         [4281, 0, 4458, 0], [1759, 0, 1256, 0]]},
 "equal": {"accuracy": 0.2029, "balanced": 0.2840, "macro_f1": 0.2029},
 "verdict": {"a": true, "a_sigma": 6.4, "b": true, "b_sigma": 9.7}}
```

## Результат

[`results/`](results/):

| Файл | Що всередині |
| ---- | ------------ |
| `classes.json` | когорти: схема зігзагу, кількості по типах відрізків і вибірках |
| `forecast.json` | усі 832 сходинки: метрики, матриці помилок, вердикти, метод, параметри |
| `forecast.meta.json` | те саме без сходинок — для картки дослідження |

## Висновки

1. **Результат є на пачці з 2 подій.** Точність 0.3971 проти бази 0.3768 —
   +6.4 похибки; збалансована 0.2840 проти 0.25 — +9.7 похибки. Рости пачкою
   до 4 зігзагів не знадобилось.

2. **Виграш малий: +2 п.п. на пачці 2, +4.1 п.п. на найкращій пачці 203.**
   Це ознака, а не сигнал — торгового рішення сама по собі не дає
   (як і в дослідженні 04).

3. **Прогноз двійковий, не чотиричастинний.** Когорти протируху майже не
   називаються: вони втричі менші, і фічі пачки їх не відділяють. Формально
   критерій B виконано, фактично модель ділить поле на «вгору» і «вниз».

4. **Довга пачка не допомагає.** Точність має максимум на ~200 подіях
   (приблизно один рух зігзагу 2.3) і далі падає до бази: далеке минуле шуму
   додає більше, ніж інформації.

5. **Мітки минулого коштували б набагато більше за фічі.** Оракул
   персистентності — 0.9757 проти 0.4182 у моделі. Когорти йдуть довгими
   відрізками, і майже вся структура задачі — у самій послідовності міток,
   а не в стані стакана за попередні 210·N секунд. Мітка в реальному часі
   невідома, тому наступний крок — не збільшувати пачку, а вчитися оцінювати
   когорту **поточної** події.

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

- [x] всі етапи підтверджені — 2026-08-23, за виводом вкладки
- [x] логіка послідовна
- [x] відтворюється з цього файлу + даних: один прогін `next_cohort.py`
- [x] стаття почищена — лишився тільки підтверджений шлях
