Машинная спека · agents.md

Спека для AI-агента

Полный контракт API в Markdown. Этот URL скормите Cursor, ChatGPT или своему боту.

Канонический URL
https://market-oracle.pro/ru/docs/agents.md
# Market Oracle — инструкция для AI-агента клиента API

Канонический URL на сайте продажи: `/{locale}/docs/agents.md` (например `/ru/docs/agents.md`). Этот файл — контракт API для кода. Обзор для человека — `/{locale}/docs`.

Документ для агента или разработчика, который пишет торгового бота, сигнализатор или любой клиент Market Oracle. Язык и платформа клиента — любые: Oracle отдаёт только данные по API.

Клиентские тарифы **`free` / `basic` / `pro`** (лимиты, кому какой, без цены) — раздел **«Клиентские тарифы»**. Служебный тариф оператора не предлагать. Цену называть только с сайта продажи.

## Назначение и основные правила

Market Oracle — только источник рыночных данных. Он не открывает сделки и не управляет позициями.

Главное правило: **торговые решения принимаются только по закрытому бару (`closed: true`)**. Live-триггер Oracle — закрытый **1m** (`bar_close`); стратегия на `5m`/`15m`/`1h` должна ждать закрытия **нативного** бара своего ТФ (`GET /v1/history?interval=` или live `context.htf`), не агрегировать минутки и не торговать по незакрытой свече. Не формировать сигнал повторно для одного и того же ключа дедупликации `` `${symbol}:${bar.ts}` `` (канонический формат строки).

Оракул:

- хранит закрытые бары и рассчитанные индикаторы на **всех** включённых ТФ (`1m` live + native `5m`/`15m`/`30m`/`1h`/`4h`/`1d`/`1w`);
- на каждом ТФ после warmup отдаёт один и тот же core: EMA/RSI/ATR/ADX(+DI)/VWAP/OBV/MFI/CVD, плюс slope, BB, volume$, swing;
- отдаёт список доступных символов и их готовность;
- передаёт свежие закрытые **1m**-бары через WebSocket (**lean payload**: бар + indicators; без `trade_cost`/`cost_risk`/`htf`);
- агрегирует top-20 стакан в закрытые минутные microstructure-факты;
- предоставляет 24h ticker и торговые ограничения инструмента;
- CVD и минутный стакан доступны только пока `/health.cvd.status == "ok"` (при `failover: true` свечи могут оставаться валидными);
- одним вызовом `GET /v1/context/{symbol}` отдаёт снимок для решения (бар, depth, **`trade_cost` / `cost_risk`**, **`htf`**, derivatives, macro, quotes, regime).

**Live-канал — только WebSocket.** Канон: `wss://api.market-oracle.pro/v1/stream` (ключ в `Authorization: Bearer` или один раз `?token=`). После `hello` клиент шлёт `subscribe`; сервер будит событием `bar_close` (закрытый **1m** + indicators). Данные для решения (`context`, `history`, `trade_cost`, `htf`) клиент забирает **REST**. Обязателен reconnect с backoff (1s → 2× → max 60s, jitter ±25%), дедуп `` `${symbol}:${ts}` `` и догрузка дыр через REST. **HTTP webhooks не поддерживаются** — не ждать `POST` на свой URL, не проектировать serverless «вместо сокета». Нет входящего WS — fallback `GET /v1/latest` каждые 30–60 с (см. раздел WebSocket). Не реализовывать webhooks на стороне клиента «в надежде, что Oracle начнёт слать».

**Edge-примитивы (обязательно для роботов с 2026-07):** не хардкодить комиссии и «стоимость в R» в стратегии. Читать `context.trade_cost` и `context.cost_risk` (см. раздел ниже). Бэктест, бумажная торговля и live должны использовать **один и тот же** набор комиссий (query override или значения по умолчанию сервера).

**Мультитаймфрейм (обязательно с 2026-08):** не агрегировать `5m`/`15m`/`30m` из локальных `1m` ради oracle-only полей (`adx14`, `vwap`, `cvd_delta`, `obv`, `mfi14`, DI, BB…). Брать нативные серии `GET /v1/history?interval=15m` (и т.д.). Live HTF-снимок — `context.htf`; для бэктеста и replay — history as-of по `bar.ts`.

## Что нового для агента стратегии (2026-08)

Это блок для кода стратегии (бот, сигналы, AI-агент). Oracle больше не «индикаторы только на 1m + HTF без `indicators`».

### Полные нативные `5m` / `15m` / `30m`

| Раньше (не делать так) | Сейчас |
|------------------------|--------|
| Клиент склеивал `5m`/`15m`/`30m` из `1m` | Oracle сам отдаёт нативные серии старших ТФ |
| `adx14` / `vwap` / `cvd_delta` / `obv` / `mfi14` на агрегате часто `null` | тот же core, что на `1m`, после warmup **на этом ТФ** |
| EMA/RSI на H1/H4/1d клиент считал сам | `GET /v1/history?interval=1h\|4h\|1d\|1w` уже несёт `bar.indicators` |

Интервалы history: `1m` (live WS) + native `5m` / `15m` / `30m` / `1h` / `4h` / `1d` / `1w`. Пример:

```http
GET /v1/history/BTCUSDT?interval=15m&limit=300
GET /v1/history/BTCUSDT?interval=5m&limit=500
POST /v1/history/batch   {"symbols":["BTCUSDT"],"interval":"15m","limit":200}
```

Глубина истории: `5m` 60д, `15m` 90д, `30m` 120д, `1h` ~9 мес, `4h` 2г, `1d` 3г, `1w` 5л.

### Новый каталог ключей в `bar.indicators` (все ТФ)

К старому ядру (`ema20/50/200`, `rsi14`, `atr14`, `vwap`, `vol_sma20`, `adx14`, `obv`, `mfi14`, `cvd_delta`, форма свечи) добавлены:

| Ключи | Зачем стратегии |
|-------|-----------------|
| `plus_di14` / `minus_di14` / `di_side` (`"1"`/`"-1"`/`"0"`) | направление тренда (ADX сам по себе силу, не сторону) |
| `ema50_slope_pct` / `ema_slope_abs` | флет для сетки (`\|slope\| ≤ 0.1` на своём ТФ) |
| `atr50` / `atr_ratio_14_50` | squeeze / «ATR низкий vs своей нормы» |
| `volume_usd` / `volume_rate` | `$` бара и `$/мин` (rate сравним между ТФ) |
| `bb_mid` / `bb_upper` / `bb_lower` / `bb_width` | Bollinger 20,2; squeeze/breakout |
| `swing_high` / `swing_low` / `last_swing_*` | fractal (strength=2), лаг 2 бара **этого** ТФ |

`indicators_ready` по-прежнему = есть `ema20`+`rsi14`+`atr14`. Остальные ключи появляются позже (ADX/DI ~ после 27 баров, EMA200 после 200 баров **этого интервала**, BB после 20). Не подставлять `0` вместо `null`.

### Live vs history: что где лежит

| Нужно стратегии | Откуда | Не откуда |
|-----------------|--------|-----------|
| Сигнал 1m | `WS bar_close` / `/v1/latest` / `context.bar` | — |
| Сигнал 5m / 15m / 30m | `/v1/history?interval=5m\|15m\|30m` | **нет** в WS; **нет** `5m`/`30m` в `context.htf` |
| Live старший ТФ (фильтр) | `context.htf.m15` / `.h1` / `.h4` / `.d1` | нет `.m5`, нет `.m30`, нет `.w1` |
| Replay / бэктест HTF | history as-of: последний бар с `ts <= decisionBar.ts` | не копировать live `context.htf` на прошлые бары |
| Ряд только indicators | `/v1/indicators/{symbol}` | **только 1m**; для 15m берите history |
| Холодный дамп | `POST /v1/sync/bootstrap` | **только 1m**; mid/HTF поднимает фоновый TF-sync |

`context.htf` — live as-of последних **закрытых** `15m`/`1h`/`4h`/`1d`. Для `5m`/`30m`/`1w` всегда history.

WebSocket **не** шлёт `5m`/`15m` `bar_close`. Цикл 15m-стратегии:

```text
WS 1m bar_close
  → это последняя минута 15m-бакета?  (bar.ts % 900_000 === 840_000)
  → подождать HTF-sync (обычно ≤ 60 с) или сразу GET /v1/history?interval=15m&limit=1
  → торговать только если 15m.closed && 15m.ts === open этого бакета
  → дедуп `${symbol}:${m15.ts}`  (ключ старшего бара, не 1m)
```

Для 5m: `bar.ts % 300_000 === 240_000`. Для 30m: `bar.ts % 1_800_000 === 1_740_000`.
Перед входом по-прежнему `GET /v1/context` (`trade_cost` / `cost_risk` / `htf` / quality) — этих полей нет в WS.

### Чего по-прежнему нет в Oracle

Не просить и не ждать в `bar.indicators`: MACD, Stochastic, Supertrend, Ichimoku, RSI-reclaim state, счётчик касаний, дневной HL-канал, `htf_trend.trend_z`, `rs_rank`. Это логика вашей стратегии, не API Oracle. Сырая история стакана — только live `GET /v1/depth` и минутная microstructure.

`vwap` — session VWAP с **UTC-day reset**. На `5m`/`15m`/`1h` это нормальный дневной VWAP. На `1d` это VWAP самой дневной свечи. На `1w` **не** weekly VWAP (сброс каждый UTC-день) — не использовать как недельную якорную цену.

`ready` (≥200 **1m**-баров) ≠ прогрев EMA200 на 15m. Для `interval=15m` проверяйте `bar.indicators.ema200` на 15m-баре (~200 свечей ≈ 50 ч native-истории).

## Гарантии корректности и восстановления в v0.1.1

- EMA, RSI, ATR(14/50), VWAP, volume SMA, OBV, MFI, CVD-delta, ADX(+DI), Bollinger(20,2), EMA50 slope, volume_usd/rate и fractal swings покрыты unit/batch-тестами где применимо. Форма свечи (`body`/`candle_q`/…) и `atr_pct` считаются O(1) на закрытии бара. `adx14` / DI используют стандартный Wilder seed: сначала полные 14 периодов DM/TR, затем smoothing. Во время прогрева значение `null`; нельзя заменять его нулём.
- Закрытый 1m-бар и состояние индикаторов сохраняются так, чтобы после рестарта Oracle восстановил отсутствующее или повреждённое состояние по сохранённой истории. Состояние mid/HTF ведётся отдельно от 1m.
- Gap-fill считается успешным только при полном непрерывном диапазоне валидных свечей. Если диапазон не восстановлен, новый бар не коммитится поверх дыры.
- Валидная закрытая свеча с нулевым объёмом принимается, если OHLC положительны. Любая свеча с `open/high/low/close <= 0` отклоняется как повреждённая.
- Startup bootstrap и TF sync (`5m`…`1w`) подтягивают только отсутствующие диапазоны и **пишут indicators** тем же engine, что 1m. Уже непрерывная история повторно не скачивается; серия обновляется после закрытия нового bucket. Legacy-бары с `indicators: null` пересчитываются при следующем sync.
- Settled funding после первого заполнения обновляется инкрементально и не чаще одного раза в час.
- Повторное добавление уже зарегистрированного символа не запускает лишнюю синхронизацию. Неподдерживаемые инструменты в live-набор не попадают.
- Свежие macro/calendar snapshots переиспользуются после рестарта до следующего планового refresh. Календарь **накапливается** (~90 дней) для as-of replay, а не затирается weekly feed. Market regime не публикует пустой «свежий» snapshot: после cold start он ждёт готовности native 1h-истории и повторяет расчёт.
- Эти гарантии относятся к целостности live-данных. Они **не** делают текущий `/v1/context` исторически безопасным: `context_scope="live_snapshot"` и `historical_safe=false` остаются обязательным ограничением. `context.htf` — live as-of последних закрытых mid/HTF баров, не join к произвольному историческому `bar.ts`.

## Microstructure Contract v1 в v0.2.1

- Top-20 агрегируется по UTC-минутам. Oracle сэмплирует последний подтверждённый стакан раз в секунду, пока поток книги здоров.
- `sample_count` — число 1Hz-сэмплов Oracle, `source_update_count` — число реальных обновлений книги за минуту. Не используйте `source_update_count < 40` как quality-gate: неизменившийся стакан не является stale.
- Snapshot следующей минуты не может попасть в предыдущую; reconnect-gap ограничивается двумя секундами в pressure integral.
- Opt-in событие `microstructure_close` уходит только после того, как минутный объект финализирован.
- Live-снимки стакана в историю не пишутся; в `/v1/microstructure` попадают только закрытые минутные признаки.
- Обычные бары не зависят от наличия microstructure. Если поток top-20 недоступен, незавершённая минута отбрасывается, а `bar_close` продолжает работать.

## Подключение и авторизация

Адрес API передаётся клиенту конфигурацией. Канон production: REST `https://api.market-oracle.pro`, стрим `wss://api.market-oracle.pro/v1/stream`. TLS включён: всегда `https://` и `wss://`. Не хардкодить IP и порт `:8080`.

```env
ORACLE_BASE_URL=https://api.market-oracle.pro
ORACLE_API_KEY=mo_...
```

Не вшивать ключ в публичный frontend или репозиторий. Для каждого REST-запроса к `/v1/*`:

```http
Authorization: Bearer mo_...
```

`GET /health`, `GET /v1/health` (тот же ответ) и `GET /metrics` публичные. Для WebSocket ключ передаётся заголовком `Authorization` или, если библиотека не умеет задавать WS-заголовки, один раз в URL:

```text
wss://api.market-oracle.pro/v1/stream?token=mo_...
```

Квоту смотрите в заголовках `X-Quota-*` после любого клиентского REST или в JSON `GET /v1/me` (`daily_*`, `rpm`, `min_interval_ms`). При HTTP `429` читайте `retry_after_ms` в теле. Полный разбор — раздел **«Заголовки лимитов»**.

## Клиентские тарифы

Имена: только **`free` / `basic` / `pro`**. Устаревшего `standard` нет. Служебный тариф оператора клиенту **не предлагать**.

**Цену не называть и не угадывать** — она только на сайте продажи ключей. Здесь лимиты, чтобы выбрать тариф под бота и объяснить пользователю *зачем* этот slug, не *сколько стоит*.

Жёсткие цифры **этого** ключа всегда из `GET /v1/me`. Канон ниже — лимиты тарифа. Если `/v1/me` разошёлся с таблицей — верьте `/v1/me`.

`daily` = платные REST за UTC-сутки (**1 HTTP = 1**). Кадры `WS /v1/stream` и служебные `GET /v1/me`, `/v1/symbols`, `/v1/meta/*`, `/v1/calendar` в `daily` **не входят**. Несколько ботов на одном ключе: WS параллельны; REST — общая очередь (`rpm` + `min_interval_ms` + `daily`).

| slug | Запросы/день | RPM | Мин. пауза REST | Рек. пауза клиента | Max WS | Кому предлагать |
|------|--------------|-----|-----------------|-------------------|--------|-----------------|
| `free` | **2000** | 30 | 2000 мс | 2000 мс | **1** | Знакомство с API, один бот. Не 24/7 live на 5m с несколькими парами. |
| `basic` | **5000** | 60 | 200 мс | 1000 мс | **2** | Live: 1–2 бота на ключ, ТФ 1m/5m/15m+, дни и недели без выключения. |
| `pro` | **8000** | 90 | 150 мс | 667 мс | **3** | Live 24/7: до 3 ботов на одном ключе (три WS). Нужно больше процессов — второй ключ, не «обход лимита». |

Как объяснить пользователю (без цены):

1. Спросить: сколько **одновременных** live-ботов и какой ТФ (1m / 5m / 15m+). Один процесс = один WS.
2. **1 бот, проба API** → `free`.
3. **1 или 2 live-бота 24/7** → `basic`.
4. **3 live-бота на одном ключе** → `pro`.
5. **Больше 3 процессов** → ещё один ключ нужного тарифа (у каждого свои `daily` / WS).
6. Если бот без WS долбит `latest` каждые 10 с — это ~8640 REST/сутки, не влезет ни в `free`, ни в `basic`; сначала Live на WebSocket, потом тариф.

Ключи с сайта продажи: **`free` — бессрочный**. **Basic / Pro** — месяц, затем продление. Жёсткие цифры лимитов — всегда `GET /v1/me`.

## Рекомендуемый запуск клиента

1. Вызвать `GET /health` (публичный). HTTP 200 значит только «процесс ответил». Верхний `status` бывает `"ok"` или `"degraded"`. Торговать по полям:
   - OHLC / 15m без CVD: `bars.status == "ok"` (плюс `ws_connected == true` предпочтительно). Не гейтить только верхним `status == "ok"`, если бары живы.
   - CVD и стакан: дополнительно `cvd.status == "ok"`. При `cvd.status != "ok"` не суммировать накопительный CVD.
   Не гейтить сессию одним `failover == false`: краткий failover при живых барах (`bars.status == "ok"`) для 15m без CVD допустим.
2. Вызвать `GET /v1/me` — узнать `tier`, `daily_limit` (**штуки платных REST за UTC-сутки**, не веса), `rpm`, `min_interval_ms`, `max_ws`, `expires_at`. Поле `daily_load` — нагрузка для отладки, **не** второй лимит. Не делить `daily_remaining` на `weights.context`.
3. Вызвать `GET /v1/symbols`.
4. Для анализа брать только пары с `ready == true`; предпочтительно `history == "full_day"`.
5. Проверить `ready`/`history`/`last_closed_ts`. `POST /v1/sync/bootstrap` вызывать только если история ещё не прогрета или требуется явное восстановление; серверный startup bootstrap уже синхронизирует недостающие диапазоны.
6. Подключиться к `WS /v1/stream` и отправить подписку; для стаканных стратегий добавить `"microstructure": true`.
7. На каждое событие `bar_close` обновлять локальное состояние и запускать анализ ровно один раз (дедуп `` `${symbol}:${bar.ts}` ``). `microstructure_close` присоединять по тому же `symbol + ts`, не по времени получения.
   Если стратегия использует live execution-filter, через 1–2 секунды запросить `GET /v1/depth/{symbol}?seconds=3`; это не меняет исторический сигнал закрытого бара.
8. Держать фоновый polling по разделу **«Рекомендуемые задержки между REST-запросами»**: для `basic` **`latest` 10–15 с**, **`context` 60–90 с** на активные пары; на `free` запрашивать `context` значительно реже, ориентир **~30 мин**. **Перед входом** обязательно один раз `GET /v1/context/{symbol}` и проверить `cost_risk.tradable` (+ `data_quality`, freshness depth, при необходимости `htf.h1`/`htf.h4`). `trade_cost`/`cost_risk`/`htf` есть **только** в context, не в WS.
9. После реконнекта — пауза **≥ min_interval_ms**, затем сначала `/v1/history/{symbol}`. Для стаканной стратегии после следующей паузы запросить `/v1/microstructure/{symbol}` за тот же диапазон и выполнить exact join. `POST /v1/sync/bootstrap` нужен только при подтверждённом пропуске/неготовой истории; не запускать параллельно десятками.
10. Если WebSocket недоступен — `GET /v1/latest` **каждые 30–60 с**, `context` **не чаще 60–120 с**.
11. Старший ТФ: live — читать `context.htf` (последние закрытые `m15`/`h1`/`h4`/`d1`); история/replay — `/v1/history?interval=5m|15m|30m|1h|4h|1d|1w` с кэшем **≥ 5 мин**. Не считать EMA/RSI/ADX на клиенте, если Oracle уже отдал их в `bar.indicators`.

## Рекомендуемые задержки между REST-запросами

Два независимых ограничения на **каждый** REST-вызов (кроме публичных `/health`, `/metrics`):

1. **`min_interval_ms`** — минимальная пауза между **любыми** двумя REST-запросами одного ключа. Нарушение → `429 too_fast` + `retry_after_ms`.
2. **`rpm`** — скользящее окно 60 с: не больше N запросов за минуту. Нарушение → `429 rate_limited`.

Практика: после **каждого** REST держите очередь с `await sleep(max(min_interval_ms, retry_after_ms))`. Не запускайте параллельные «шторма» из `Promise.all` на десятки `/v1/context` — serial queue или p-limit=1–2.

**Базовое правило spacing:** `delay_ms ≥ max(min_interval_ms, ceil(60000 / rpm))` — нижняя граница, не целевой polling. Для `free` (рек. **2000 мс** при 30 RPM), `basic` (рек. **1000 мс** при 60 RPM) и `pro` (рек. **667 мс** при 90 RPM) ориентируйтесь на колонку «рек. spacing» в таблице тарифов ниже; жёсткий floor сервера — `min_interval_ms` из `/v1/me`. Несколько live-ботов на одном ключе должны либо делить одну REST-очередь, либо каждый ждать ≥ `рек. spacing × число_ws` (кадры WS при этом не ждут очередь).

### Заголовки лимитов

На каждом успешном REST-запросе с клиентским API-ключом Oracle добавляет заголовки квоты. Их можно читать после `/v1/context`, `/v1/latest` и т.д. — отдельный запрос не нужен.

`GET /v1/me` тоже отдаёт эти заголовки, но **сам дневную квоту не тратит**. Актуальные лимиты дублируются в JSON `/v1/me`: `daily_used`, `daily_limit`, `daily_remaining`, `rpm`, `min_interval_ms`.

Имена заголовков в HTTP не чувствительны к регистру (`X-Quota-Used` = `x-quota-used`). В CORS они проброшены (`Access-Control-Expose-Headers`).

#### Дневная квота (смотреть сюда)

Пример после **платного** запроса (например `GET /v1/context/BTCUSDT`):

```http
X-Quota-Used: 150
X-Quota-Limit: 2000
X-Quota-Remaining: 1850
X-Quota-Cost: 1
X-Quota-Weight: 3
X-Quota-Reset: 1784217600
```

| Заголовок | Смысл |
|-----------|--------|
| `X-Quota-Used` | Сколько платных REST-запросов уже учтено за текущие UTC-сутки |
| `X-Quota-Limit` | Дневной потолок тарифа. `0` = без дневного лимита |
| `X-Quota-Remaining` | Сколько осталось. Если лимита нет — строка `unlimited`, не число |
| `X-Quota-Cost` | Сколько этот запрос добавил к `Used` (`1` или `0`) |
| `X-Quota-Weight` | Нагрузка (вес endpoint’а). Это не второй лимит, стопа по нему нет |
| `X-Quota-Reset` | Unix time (UTC), когда обнулится дневная квота (следующая полночь UTC) |

Бесплатные для квоты маршруты (`GET /v1/me`, `/v1/symbols`, `/v1/calendar`, `/v1/meta/…`): `X-Quota-Cost: 0`, `X-Quota-Weight: 0`, счётчик не растёт.

Парсить остаток так:

```text
remaining = header X-Quota-Remaining
если remaining == "unlimited" → лимита нет
иначе remaining — целое число
ждать до X-Quota-Reset, если remaining == 0
```

Не используйте `X-RateLimit-*` как дневной остаток.

#### RPM и пауза между запросами (429)

Слишком частые запросы: HTTP **429**, в теле `code`: `too_fast` (меньше `min_interval_ms`) или `rate_limited` (превышен RPM). Дневной потолок исчерпан: `code`: `quota_exceeded`.

На 429 дополнительно:

```http
Retry-After: 1
X-RateLimit-Reset: 1784217660
```

В JSON того же ответа есть `retry_after_ms`. Ждите этот интервал (или `Retry-After` секунд) и повторите.

`Retry-After` на обычном **200 нет**.

`X-RateLimit-Limit` / `X-RateLimit-Remaining` на успешном ответе не равны «RPM минус этот запрос». Для бота достаточно: дневной остаток из `X-Quota-*`, пауза из тарифа (`min_interval_ms` в `/v1/me`) и `retry_after_ms` при 429.

### Таблица по endpoint'ам (production-бот с WS)

**Суточный стоп = штуки HTTP.** Один платный REST = **1** из `daily`, даже если `context`/`history` «тяжёлые». Веса (`weights.*`) копятся только в `daily_load` и **не** отключают ключ. Не делить `daily_remaining` на вес. Служебные `GET /v1/me`, `/v1/symbols`, `/v1/meta/*`, `/v1/calendar` и кадры WS в `daily` не входят.

Предполагается: **`WS /v1/stream`** подключён, сигнал только по `bar_close`. Интервалы — **между повторными вызовами того же endpoint'а**; между разными endpoint'ами всё равно соблюдайте `min_interval_ms`.

| Endpoint | Daily (запросы) | Нагрузка (weight) | Рекомендуемый интервал | Зачем |
|----------|-----------------|-------------------|------------------------|--------|
| `WS /v1/stream` | **0** | **0** | постоянное соединение | триггер `bar_close`, без REST на каждую минуту |
| `GET /v1/context/{symbol}` | **1** | **3** | `basic`: **60–90 с** на активную пару; `free`: **~30 мин**; **+ 1× перед входом** | depth/macro/regime/quality + **`trade_cost`/`cost_risk`/`htf`** |
| `GET /v1/latest?symbols=` | **1** | **1** | **10–15 с** (ticker UI); **30–60 с** если WS жив и bar уже есть | bid/ask/24h без полного context |
| `GET /v1/history/{symbol}` | **1** | **3** | **≥ 5 мин** кэш на TF; только смена TF / reconnect backfill | график, mid/HTF с indicators |
| `GET /v1/depth/{symbol}` | **1** | **1** | один раз после сигнала; не постоянный REST polling | текущий/recent top-20 |
| `GET /v1/microstructure/{symbol}` | **1** | **3** | старт / reconnect backfill | минутная история стакана |
| `GET /v1/microstructure/{symbol}/latest` | **1** | **3** | по необходимости; WS предпочтительнее | последний закрытый microstructure |
| `POST /v1/history/batch` | **1** (один HTTP) | **3×N** | **≥ 10 мин** или одноразово при старте | несколько пар сразу |
| `POST /v1/sync/bootstrap` | **1** | **10** | только если `ready/history/last_closed_ts` подтверждают неполную историю | явный прогрев/восстановление; load flat 10 (без доплаты за объём history) |
| `GET /v1/symbols` | **0** | **0** | **3–15 мин** | список пар, `ready`/`history` |
| `GET /v1/calendar` | **0** | **0** | **20–60 мин** live; в бэктесте — `?as_of=bar.ts&window_min=60` | макро-окна (в context уже live-флаг ≤60 мин) |
| `GET /v1/meta/{symbol}` | **0** | **0** | **1× на пару** (или при смене инструмента) | tick/step/min_notional |
| `GET /v1/me` | **0** | **0** | **1× при старте + ~1 ч** | tier/RPM/expiry, не для quota-счётчика |
| `GET /v1/indicators/{symbol}` | **1** | **1** | кэш / reconnect; не штатный polling | **только 1m**; mid/HTF — `/v1/history?interval=` |
| `GET /v1/status/{symbol}` | **1** | **1** | **≥ 60 с** или не вызывать — дублирует context/status-поля | диагностика одной пары |
| `GET /v1/derivatives/{symbol}` | **1** | **1** | **≥ 60 с** или не вызывать — есть в context | raw liq events |
| `GET /v1/quotes/{symbol}` | **1** | **1** | **≥ 60 с** или не вызывать — есть в context | venue-only debug |
| `GET /v1/market-regime` | **1** | **1** | **≥ 15–60 мин** (сервер обновляет ~1 ч) | глобальный regime |
| `GET /v1/macro` | **1** | **1** | **≥ 20–60 мин**; history F&G — реже | без `fng_days` дублирует context |

**Без WebSocket (fallback):** на `basic` `GET /v1/latest` **каждые 30–60 с**, дедуп по `bar.ts`; `context` **не чаще 60–120 с**. На `free` сохраняйте ориентир **~30 мин** для `context`. WS предпочтительнее — экономит запросы и RPM.

**Пример суточного бюджета (`basic`, 5000 запросов/день):** WS бесплатно. Каждый платный REST = 1. `latest` каждые 10 с круглосуточно ≈ 8640 — не влезет. Для UI держите `latest` только пока экран открыт, `context` — раз в 15–30 мин (плюс обязательно перед входом). Нагрузка (`daily_load`) при этом выше (context весит 3), но стоп только по числу запросов.

### Ориентир polling для одного бота на `free` (~2000 запросов/день)

Live-триггер — WS. Не крутить `context` каждую минуту: ориентир **~30 мин** на пару + **1× перед входом**. Таблица ниже — бюджет, не «долбить всё сразу».

| Данные | Endpoint | Интервал | Daily cost |
|--------|----------|----------|------------|
| 1m `bar_close` | `WS /v1/stream` | постоянное соединение | **0** |
| Ticker UI (только пока экран открыт) | `GET /v1/latest` | **10–15 с** | **1** за вызов |
| Снимок решения | `GET /v1/context` | **~30 мин** + перед входом | **1** |
| Symbols | `GET /v1/symbols` | **3–15 мин** | **0** |
| Calendar | `GET /v1/calendar` | **20–60 мин** | **0** |
| History (график) | `GET /v1/history` | кэш **≥ 5 мин** | **1** |

Светофор, vol, funding на клиенте считайте из уже загруженного `context`; отдельных REST под виджеты нет.

### Live 24/7 (Basic 2 WS / Pro 3 WS)

Один API-ключ = несколько **параллельных** WebSocket. Каждый сокет независимо получает тот же broadcast `bar_close` (1m + indicators). Upgrade `/v1/stream` **не** проходит RPM/`min_interval` и **не** списывает `daily`. Режется только числом сокетов (`max_ws`).

REST с этих ботов — наоборот, **одна** труба на ключ: `min_interval` между любыми двумя HTTP, общий RPM, общий `daily`.

WS **не** шлёт закрытие `5m`/`15m`/`1h`. Live-цикл:

| ТФ бота | Триггер | Платный REST на закрытие бара | `/v1/context` |
|---------|---------|--------------------------------|---------------|
| **1m** | WS `bar_close` (0) | не нужен | только **перед входом** (`trade_cost` / `cost_risk` / quality). Не каждый 1m. |
| **5m** | WS 1m, последняя минута бакета | `GET /v1/history?interval=5m&limit=1` (нет `htf.m5`) | перед входом |
| **15m** | то же, бакет 15m | history `interval=15m` **или** `context.htf.m15` если достаточно снимка | перед входом |
| **1h+** | бакет старшего ТФ | history `interval=1h`… или `context.htf.h1/h4/d1` | перед входом |

Бюджет на **сутки 24h**, до **3 пар** на бота, без polling `latest` (тикер не нужен — бар уже в WS):

| ТФ | REST/бот/день (3 пары) | Basic × 2 WS | Pro × 3 WS |
|----|------------------------|--------------|------------|
| 1m (WS + context только на вход, запас 100 входов) | ~120 | ~240 | ~360 |
| 15m (history на каждый close + вход) | ~350 | ~700 | ~1050 |
| **5m** (самый «голодный» live) history на close | ~900 | ~1800 | ~2700 |
| 5m + context на каждый close (наивно) | ~1800 | ~3600 | ~5400 |
| Запас реконнект / warmup / depth | +200 | +400 | +600 |

Итого с запасом ×1.5: **Basic 5000** хватает на 2 live-бота даже при наивном 5m+context. **Pro 8000** — на 3 бота. Не закладывать `latest` каждые 10 с (это 8640/бот и съест любой тариф).

Не делать: `Promise.all` context с трёх ботов в одну миллисекунду — будет `too_fast`. Общая очередь на ключ или пауза ≥ рек. spacing.

## Основные REST API

### Свой тариф и лимиты (обязательно для клиента)

```http
GET /v1/me
Authorization: Bearer mo_...
```

Возвращает лимиты **текущего** клиентского ключа. Вызывать при старте и периодически (например раз в час), чтобы подстроить частоту REST и число WS.

Каталог **`free` / `basic` / `pro`** (кому какой, без цены) — раздел **«Клиентские тарифы»** выше. Здесь только снимок **этого** ключа: `tier`, `daily_*`, `rpm`, `min_interval_ms`, `max_ws`. Имя `standard` не существует.

Пример ответа:

```json
{
  "status": "ok",
  "key_id": "key_ab12…",
  "name": "home-bot",
  "key_prefix": "mo_41ec884",
  "tier": "basic",
  "tier_label": "Basic",
  "expires_at": "2026-08-16T00:00:00+00:00",
  "expired": false,
  "created_at": "2026-07-16T04:00:00+00:00",
  "last_used_at": "2026-07-16T06:30:00+00:00",
  "daily_used": 420,
  "daily_limit": 5000,
  "daily_remaining": 4580,
  "daily_load": 1260,
  "rpm": 60,
  "min_interval_ms": 200,
  "max_ws": 2,
  "ws_open": 1,
  "weights": {
    "default": 1,
    "context": 3,
    "history": 3,
    "history_batch_per_symbol": 3,
    "bootstrap": 10
  }
}
```

Как использовать:

| Поле | Стратегия клиента |
|------|-------------------|
| `tier` | `free` / `basic` / `pro`. Кому какой — раздел **«Клиентские тарифы»**. Не путать с устаревшим именем `standard` |
| `tier_label` | Человекочитаемое имя тарифа |
| `daily_used` / `daily_limit` / `daily_remaining` | **платные REST-запросы** за UTC-сутки (1 HTTP = 1). При `daily_limit=0` поле `daily_remaining` в JSON = `null`; строка `"unlimited"` бывает только в заголовке `X-Quota-Remaining` |
| `daily_load` | сумма weights за тот же день; **не** сравнивать с `daily_limit` |
| `daily_remaining` | если `< 20%` от `daily_limit` — реже `context`, больше опираться на WS |
| `rpm` / `min_interval_ms` | не слать REST чаще этих лимитов; ориентир клиента — рек. spacing тарифа |
| `max_ws` / `ws_open` | не открывать больше `max_ws` сокетов |
| `expires_at` / `expired` | для **Basic/Pro** — заранее предупредить о продлении. Для **free** ключ бессрочный: не останавливать бота из‑за `expires_at` |
| `weights.context` | нагрузка одного `GET /v1/context` (не стоимость в daily) |

**Daily = запросы:** `daily_used` / `daily_limit` / `daily_remaining` считаются в штуках платного REST, не в весах. `GET /v1/context` списывает **1** с лимита и **3** в `daily_load` (если `weights.context=3`). Batch на N пар = **1** запрос и **N × history_batch_per_symbol** нагрузки.

Счётчик после каждого REST — заголовки `X-Quota-*` (раздел **«Заголовки лимитов»**). Не опрашивайте `/v1/me` ради обновления остатка. `/v1/me`, `/v1/symbols`, `/v1/meta/*`, `/v1/calendar` не расходуют daily, но остаются под auth + RPM/min-interval.

При JSON-поле `status` / `code` = `key_expired` на любом маршруте — остановить торговлю и запросить новый/продлённый ключ у оператора. Не путать с HTTP status code (см. раздел ошибок).

### Проверка сервера

```http
GET /health
```

Важные поля: `status` (`ok` \| `degraded`), `ws_connected`, `failover`, `bars`, `cvd`, `bar_lag_sec`, `uptime_sec`. Тот же JSON на публичном `GET /v1/health`.

`ws_connected == true` — поток закрытых баров подключён. Это не гарантия свежего тика по каждой паре: смотрите `bars.lag_sec` / per-symbol `lag_sec`.

Пока `bars.status == "ok"`, OHLC можно использовать. `failover == true` — резервный режим: тогда `cvd.status` обычно `down` (`reason: "failover"`), а минутный microstructure на этом баре может отсутствовать.

### Список символов

```http
GET /v1/symbols
Authorization: Bearer mo_...
```

Для каждой пары возвращаются:

- `symbol`, `market`;
- `bars_1m` — количество сохранённых минутных баров;
- `span_hours` — приблизительная глубина истории;
- `ready` — **≥200** закрытых 1m-баров (порог прогрева EMA200). Это **не** то же самое, что `indicators_ready`;
- `history` — `warming` / `ready` / `full_day` (≥1440 баров);
- `last_closed_ts` — timestamp последнего закрытого бара (**мс UTC**);
- `added_at` — когда пара включена в реестр узла (UTC);
- `ttl_days` — сколько дней хранятся закрытые 1m-бары на этом узле.

Не считать пару пригодной к торговому анализу только по наличию в списке. Проверять `ready`, свежесть и `indicators_ready`. Незарегистрированный символ на `GET /v1/status` и `GET /v1/context` обычно даёт HTTP **200** с `registered: false` (пустой бар), а не `400`. Пустой path → `400`. `GET /v1/derivatives/{symbol}` для неизвестной пары → `404 symbol_not_found`.

### Детальный статус символа

```http
GET /v1/status/BTCUSDT
Authorization: Bearer mo_...
```

Важные поля: `registered`, `bar_count`, `history`, `last_closed_ts`, `lag_sec`, `indicators_ready`, `data_quality`, `failover`.

**`lag_sec` (возраст закрытого бара, не сетевой RTT):**

```text
lag_sec = (now_ms − bar.ts) / 1000
```

`bar.ts` — **open time** последней уже закрытой 1m-свечи. Это не «рынок отстал на N секунд» и не сетевой RTT клиента: это возраст canonical closed bar относительно часов сервера Oracle.

Для 1m mid-minute **ожидаемо ~60–120s** (следующая минута ещё не закрылась). Это нормальный здоровый поток, не авария.

| `lag_sec` (1m) | Смысл для агента |
|----------------|------------------|
| ≤ 120 | норма / info |
| > 120 | осторожность — следующая 1m-свеча запаздывает |
| > 180 | skip / не открывать новые позиции по паре |

Сверяйте с `data_quality.flags` (`lag_sec>120`, `lag_sec>150`, `lag_sec>180`) и `data_quality.score`. Live mid — из `ticker` / quotes; **сигнал только** из `bar` с `closed: true`.

**`data_quality` (Phase 11):** `{ "score": 0..100, "flags": ["lag_sec>120", ...] }`. Скор собирается из lag (пороги выше), ready/indicators, history, failover, fresh venues / divergence / reconnects и свежести live-компонентов. Возможные freshness-флаги: `ticker_stale`, `depth_stale`, `macro_stale`, `market_regime_stale`. Флаг `failover` снижает score на 10 — это не запрет 15m без CVD, если `/health.bars.status == "ok"`; для CVD смотрите `/health.cvd`, не только этот флаг. Для нового торгового решения **обязательно требовать `score >= 70`**; `lag_sec > 120` жёстко ограничивает score максимумом 69, поэтому такой бар не проходит gate независимо от остальных фидов.

**`ready` vs `indicators_ready`:**

| Флаг | Условие | Смысл |
|------|---------|--------|
| `ready` | `bar_count ≥ 200` | Хватает истории для EMA200 / «прогретого» анализа |
| `indicators_ready` | на последнем баре есть **базовый набор**: `ema20` + `rsi14` + `atr14` | Ядро уже посчитано; при `bar_count == 150` может быть `true`, даже если `ready == false` |

Для стратегий с EMA200 требуйте **оба**: `ready == true` и `indicators_ready == true`. Для коротких стратегий на ema20/rsi/atr достаточно `indicators_ready`.

Перед созданием сигнала отклонять устаревшие данные по таблице `lag_sec` выше (и/или по `data_quality`).

### Начальная синхронизация

```http
POST /v1/sync/bootstrap
Authorization: Bearer mo_...
Content-Type: application/json

{
  "symbols": ["BTCUSDT", "ETHUSDT"],
  "days": 7,
  "include_indicators": true,
  "include_microstructure": true,
  "limit_per_symbol": 10080
}
```

Пустой или отсутствующий `symbols` означает все зарегистрированные пары. Если список передан явно — максимум **50** символов. `days` ограничивается диапазоном 1–30, но фактически доступная глубина — скользящее окно 1m (обычно ~60 дней). Максимум ответа на символ — 10080 баров. По умолчанию `include_indicators=true`, `include_microstructure=false`, `days=7`, `limit_per_symbol=10080`. Один POST = **1** запрос квоты (независимо от числа символов/баров); в `daily_load` уходит flat `weights.bootstrap` (обычно **10**).

Дамп — **только live-интервал `1m`**. Серии `5m`…`1w` bootstrap не отдаёт: их пишет фоновый TF-sync; клиент читает `GET /v1/history?interval=…` (или batch с тем же `interval`).

Ответ содержит массив `symbols`; у каждого элемента есть `symbol`, `market`, `count`, `last_closed_ts`, `bars`. При `include_microstructure=true` также возвращается массив `microstructure`, который клиент объединяет с барами строго по `symbol + ts`. Исторический стакан задним числом не восстанавливается: поле содержит только минуты, уже собранные Oracle.

Синхронизация инкрементальная: уже сохранённые непрерывные диапазоны повторно не скачиваются, подтягиваются только разрывы. Для недавно добавленного инструмента Oracle не запрашивает время до начала доступной истории при каждом рестарте.

### История одной пары

```http
GET /v1/history/BTCUSDT?interval=1m&limit=500
Authorization: Bearer mo_...
```

Дополнительные query-параметры:

- `interval` — `1m` (по умолчанию), `5m`, `15m`, `30m`, `1h`, `4h`, `1d`, `1w`;
- `from` и `to` — Unix timestamp в **миллисекундах UTC**; если `from` нет, окно = `limit × длительность interval` (минимум 7 суток);
- `limit` — 1..10080, **по умолчанию 1000**;
- `market=spot`;

Доступная глубина по интервалам (старшие ТФ — нативные свечи, не агрегат из минуток):

| Interval | Глубина | Индикаторы в барах |
|----------|---------|--------------------|
| `1m` | ~60 дней | да |
| `5m` | 60 дней | да (включая adx/vwap/cvd) |
| `15m` | 90 дней | да |
| `30m` | 120 дней | да |
| `1h` | ~9 месяцев | да (тот же core, что на 1m) |
| `4h` | 2 года | да |
| `1d` | 3 года | да |
| `1w` | 5 лет | да |

Для мультитаймфреймового анализа запрашивайте нужные интервалы через `/v1/history` (или `POST /v1/history/batch` с тем же `interval`). После прогрева **полный каталог** уже в `bar.indicators` — не пересчитывать EMA/RSI/ADX/BB на клиенте. Предпочитайте нативные `5m`/`15m`/`30m` вместо агрегации из `1m`. Неизвестный `interval` история не валидирует жёстко: ошибочный id вернёт пустой ряд, а не `400`.

### Microstructure стакана (v0.2.1)

Для каждой закрытой UTC-минуты сохраняется компактный `MicrostructureBar` с тем же `ts`, что и 1m-бар. Live-снимки стакана в эту историю не входят.

```http
GET /v1/microstructure/BTCUSDT?interval=1m&from=1783574400000&to=1784179200000&limit=10080
Authorization: Bearer mo_…
```

```http
GET /v1/microstructure/BTCUSDT/latest?interval=1m
Authorization: Bearer mo_…
```

История возвращает `{ symbol, market, interval, from, to, count, items }`, сортировка `items` — по `ts` по возрастанию. В v1 поддерживается только `interval=1m`, максимум 10080 элементов. Глубина совпадает с окном 1m-баров (обычно ~60 дней).

Query-параметры: `interval=1m`, `from`, `to`, `limit=1..10080`, `market=spot`. `from >= to` возвращает `400 bad_range`, другой interval — `400 bad_interval`. Latest endpoint возвращает непосредственно один `MicrostructureBar`; до первой собранной минуты ответ — `404 microstructure_not_ready`. Оба маршрута — обычный Bearer auth, **1** запрос квоты + нагрузка как у history (обычно **3**), те же RPM/`min_interval_ms`, что `/v1/history`.

Каноническая форма `MicrostructureBar`:

```json
{
  "symbol": "BTCUSDT",
  "market": "spot",
  "interval": "1m",
  "ts": 1784179200000,
  "closed": true,
  "sample_count": 58,
  "source_update_count": 21,
  "expected_samples": 60,
  "coverage_pct": "96.7",
  "spread": {
    "avg_pct": "0.000307",
    "max_pct": "0.000615",
    "close_pct": "0.000292"
  },
  "imbalance": {
    "open": "0.61",
    "high": "0.84",
    "low": "0.52",
    "close": "0.58",
    "avg": "0.69",
    "pressure_integral_above_0_65": "3.42",
    "seconds_above_0_65": 38
  },
  "bid_wall": {
    "price": "65000",
    "qty": "12.5",
    "notional": "812500",
    "distance_pct": "0.063",
    "significance": "0.0101",
    "first_seen_ts": 1784179210000,
    "last_seen_ts": 1784179255000,
    "persistence_ms": 45000,
    "sample_hits": 45,
    "qty_min": "11.8",
    "qty_max": "13.1",
    "qty_change_pct": "4.8",
    "touched": true,
    "survived_touch": true,
    "pulled_before_touch": false
  },
  "ask_wall": null,
  "wall_ratio": {
    "open": "1.42",
    "close": "2.68",
    "min": "1.20",
    "max": "3.10",
    "change_pct": "88.732394"
  },
  "quality": {
    "score": 85,
    "flags": ["missing_ask_wall"]
  }
}
```

Семантика sampling и формул:

- Oracle снимает состояние последнего подтверждённого top-20 ровно раз в секунду, пока поток книги здоров. Это sampling уже полученной книги, а не отдельный запрос «прямо сейчас»;
- `sample_count` — число использованных 1Hz-состояний; `source_update_count` — число реальных обновлений книги внутри минуты. Для спокойной пары второе значение закономерно может быть 10–35 при `sample_count≈60`;
- `spread_pct = (best_ask - best_bid) / mid × 100`;
- `imbalance = bid_qty_top20 / (bid_qty_top20 + ask_qty_top20)`, диапазон 0..1;
- `pressure_integral_above_0_65 = Σ max(0, imbalance_i - 0.65) × delta_seconds_i`, каждый `delta` ограничивается 2 секундами;
- wall выбирается и сопровождается по visible notional `price × qty`; соседняя цена сохраняет identity при смещении не более `max(2 × tick_size, mid × 0.0005)`;
- `significance = wall.notional / avg_quote_volume` предыдущих 20 закрытых 1m-баров; до прогрева значение `null`;
- `wall_ratio` рассчитывается в каждом snapshot как `largest_bid_wall_notional / largest_ask_wall_notional`, затем агрегируется по минуте;
- `pulled_before_touch=true` означает только исчезновение видимого уровня до касания без повторного появления рядом в течение 5 секунд. Это не доказательство spoofing или намерения участника.

Decimal-поля сериализуются строками. При `sample_count < 40` ненадёжные spread/imbalance/wall/ratio равны `null`, а score ограничен максимумом 49. Для depth-entry требуйте одновременно:

```text
sample_count >= 40
coverage_pct >= 66.7
quality.score >= 70
microstructure.ts == bar.ts
microstructure.closed == true
```

Возможные quality flags: `low_coverage`, `depth_stale`, `provider_changed`, `wall_identity_reset`, `quote_volume_warming`, `missing_bid_wall`, `missing_ask_wall`, `spread_above_limit`.

`depth_stale` ограничивает score максимумом 69; `provider_changed` и `low_coverage` — максимумом 49; `spread_above_limit` выставляется при максимальном spread выше 0.5%. No-lookahead гарантируется бакетом `[bar.ts, bar.ts + 60000)`: snapshot с timestamp следующей минуты не попадает в текущий объект. Если поток top-20 недоступен, незавершённый бакет отбрасывается; обычный `bar_close` продолжает поступать.

Минутный объект immutable; HTTP `/v1/microstructure` отдаёт JSON. История стакана до появления microstructure на узле не восстанавливается.

Глубина 1m/microstructure обычно ~60 дней. Одни сутки (1440 точек/символ) — smoke-check; семь суток (10080) — минимальный backtest-цикл с днями недели; 60 дней покрывает несколько режимов ликвидности. Сырые снимки стакана в эту историю не входят.

### Текущий top-20 для REAL execution (v1.0.0)

```http
GET /v1/depth/BTCUSDT
GET /v1/depth/BTCUSDT?seconds=3
Authorization: Bearer mo_…
```

- без `seconds` или `seconds=0` — только последний 1Hz top-20 sample;
- `seconds=1..10` возвращает текущее окно по возрастанию времени, включая текущий sample;
- endpoint отдаёт уже полученные сэмплы и **не запрашивает новый снимок** по этому вызову;
- это live-окно (максимум 15 samples на символ), в историю microstructure оно не попадает;
- **1** запрос квоты, нагрузка `weights.default` (обычно 1); действуют обычные auth, RPM и `min_interval_ms`;
- до первого здорового sample ответ `404 depth_not_ready`; `seconds > 10` — `400 bad_seconds` (`seconds` допустим в диапазоне **0..10**).

```json
{
  "symbol": "BTCUSDT",
  "market": "spot",
  "sample_interval_ms": 1000,
  "requested_seconds": 3,
  "count": 4,
  "items": [
    {
      "symbol": "BTCUSDT",
      "market": "spot",
      "sampled_ts": 1784179202000,
      "source_received_ts": 1784179201250,
      "source_age_ms": 750,
      "last_update_id": 123456789,
      "metrics": {
        "ts": 1784179202000,
        "best_bid": "65000",
        "best_ask": "65000.1",
        "spread_pct": "0.000154",
        "bid_volume": "18.4",
        "ask_volume": "15.9",
        "imbalance": "0.536443",
        "bid_wall": null,
        "ask_wall": null,
        "levels": 20
      },
      "snapshot": {
        "received_ts": 1784179202000,
        "source_received_ts": 1784179201250,
        "last_update_id": 123456789,
        "bids": [{ "price": "65000", "qty": "1.2" }],
        "asks": [{ "price": "65000.1", "qty": "0.8" }]
      }
    }
  ]
}
```

Чтобы определить новый объём, сравнивайте `last_update_id` и уровни одинаковой цены в соседних `items`. Одинаковый `last_update_id` означает, что Oracle повторно сэмплировал неизменившийся подтверждённый стакан.
Большой `source_age_ms` сам по себе не означает stale: на спокойной книге новые обновления могут не приходить, пока top-20 не изменился. При реальном disconnect endpoint перестаёт возвращать старую книгу.

Это **execution-filter после сигнала**, а не исторический признак закрытой свечи. Если решение принимается в `bar.ts + 2s`, зафиксируйте эту latency в стратегии. Бэктест по минутной microstructure не должен притворяться, что заранее знал post-close top-20; отсутствие исторического post-close snapshot допускает только моделирование сигнала без этого live-фильтра.

### История нескольких пар

```http
POST /v1/history/batch
Authorization: Bearer mo_...
Content-Type: application/json

{
  "symbols": ["BTCUSDT", "ETHUSDT"],
  "interval": "1m",
  "limit": 500
}
```

Можно передать `from` и `to` в миллисекундах. **Без `from` batch всегда берёт последние 7 суток** (не `limit × interval`, в отличие от `GET /v1/history`). Максимум 20 символов за запрос и 10080 баров на символ. `limit` по умолчанию 1000. `interval` — тот же набор, что у `/v1/history` (`1m`/`5m`/`15m`/`30m`/`1h`/`4h`/`1d`/`1w`). Один POST = **1** запрос квоты (не N); нагрузка = `N × history_batch_per_symbol` (обычно 3×N).

### Последние данные

```http
GET /v1/latest?symbols=BTCUSDT,ETHUSDT
Authorization: Bearer mo_...
```

Максимум 50 пар; пустой `symbols` означает все зарегистрированные.

**Форма ответа всегда одна и та же**, даже если запрошен один символ:

```ts
{ ts: number; symbols: Record<string, { bar, ticker, indicators_ready }> }
```

Нельзя читать `response.bar` — только `response.symbols["BTCUSDT"].bar` (или итерация по `Object.keys(response.symbols)`).

Внутри каждой пары:

- `bar` — последний закрытый бар с индикаторами (может быть `null`, если пары ещё нет в истории);
- `ticker` — текущая цена и статистика за 24 часа (тоже `Option`);
- `indicators_ready` — базовый набор (`ema20`+`rsi14`+`atr14`) присутствует (см. таблицу выше).

`ticker` может иметь timestamp новее бара и предназначен для отображения рынка. Сигнал стратегии всё равно должен формироваться по закрытому `bar`.

### Только индикаторы

```http
GET /v1/indicators/BTCUSDT?limit=200
Authorization: Bearer mo_...
```

Поддерживает `from`, `to`, `limit` (до 10080). **Интервал всегда `1m`** (query `interval` нет). Для `5m`/`15m`/HTF берите `/v1/history/{symbol}?interval=…` и читайте `bars[].indicators`. Возвращает элементы `{ "ts": ..., "indicators": ... }`.

### Сводный контекст (рекомендуемый основной запрос)

```http
GET /v1/context/BTCUSDT
Authorization: Bearer mo_...
```

**Это главный endpoint для торгового бота/агента.** Один запрос вместо 8–10.
Возвращает всё необходимое для решения по закрытому бару:

| Поле | Назначение |
|------|------------|
| `context_scope`, `historical_safe` | всегда `"live_snapshot"` и `false`: ответ является текущим снимком, а не исторической строкой данных |
| `ts` | **часы сервера `now` (мс UTC)**, не `bar.ts`. Open-time закрытого бара — только `bar.ts` |
| `bar` | закрытый **1m** + индикаторы (сигнал 1m-стратегии). Может быть `null`. Для `5m`/`15m`/`1h` сигнал — native `/v1/history?interval=`, не этот 1m-бар |
| `ticker` | 24h объём / change / bid-ask (отображение и sizing, не сигнал). `null`, пока тикера нет |
| `current_bar` | **пока не отдаётся API** (planned). Когда появится — незакрытая «bar 0» для симуляции исполнения / экстренного выхода. ⚠️ **НЕ ДЛЯ СИГНАЛОВ** — сигнал только по `bar` с `closed: true` |
| `depth` | spread / imbalance / walls. Для `trade_cost` свежим считается только `freshness.depth.fresh` (`max_age_sec` обычно **10**). Объект может ещё быть в JSON при `fresh: false` — тогда спред в cost берётся из значения по умолчанию, не из стакана. Через ~120 с поле может стать `null` |
| `trade_cost` | единый round-trip `c_spot` (preset fees + half_spread + slip). **Только REST context, не WS** |
| `cost_risk` | `c_r`, `p_be`, `tradable` — gate «пара слишком дорогая». **Только REST context**. `null`, если нет ATR/`close` |
| `htf` | последние закрытые бары `m15`/`h1`/`h4`/`d1` с indicators (live as-of). Для replay — `/v1/history?interval=` |
| `bar_count`, `ready`, `history`, `lag_sec`, `indicators_ready` | можно ли торговать пару (`ready` ≠ `indicators_ready`, см. выше; `lag_sec` — возраст closed bar от open ts, см. раздел статуса). `lag_sec` может быть `null` |
| `data_quality` | Phase 11: `{ score: 0–100, flags: string[] }`. Предпочитать торговлю при `score >= 70`; низкий score — skip |
| `freshness` | доступность, возраст и порог свежести текущих `ticker` (30 с), `depth` (10 с), `macro_snapshot`, `market_regime`; проверять перед использованием live-компонента |
| `volatility_regime` | Phase 11: копия label для этой пары из market-regime (`low_vol_range` / `breakout_expansion` / `high_vol_mean_reversion` / `crisis`) — выбор режима стратегии, не сигнал |
| `decoupling_detected` | Phase 11: short BTC-corr резко упал vs 7d — пара «живёт своей жизнью» |
| `macro_event_soon`, `next_macro_event` | риск новостного окна (**от now**, не as-of; для бэктеста — `/v1/calendar?as_of=`) |
| `derivatives`, `liquidations` | funding перегрев / каскад ликвидаций |
| `macro_snapshot` | F&G, dominance, стейблы, USD index, 10Y (см. timestamps / age ниже) |
| `cross_exchange` | median mid, `fresh_venues`, `divergence_warning`, массив `venues[]` (`id`: `v1`/`v2`/`v3`; `divergence_bps` **на venue**, не на корне) |
| `market_regime` | correlation к BTC, vol, breadth + per-symbol fields. **Не** путать с `htf_trend.trend_z` / `rs_rank` (их в Oracle пока нет) |
| `failover` | `true` ⇒ CVD с этого бара невалиден; OHLC на 15m можно использовать при `/health.bars.status == "ok"` |

#### `context.htf` (live)

```json
"htf": {
  "m15": { "ts": 0, "open": "…", "close": "…", "indicators": { "ema20": "…", "adx14": "…" } },
  "h1":  { },
  "h4":  { },
  "d1":  { }
}
```

Каждое поле — последний **закрытый** бар interval (или отсутствует, пока серия не прогрета). Indicators те же, что в `/v1/history`. Полей `m5` / `m30` / `w1` **нет** — для них только history. Для NY Open / H1-confluence в **live**. В бэктесте не копировать `htf` на прошлые бары — грузить history и брать последний бар с `ts <= decisionBar.ts`. После закрытия 15m/1h нового снимка в `htf` можно ждать до ~60 с.

Свои комиссии и порог `tradable` можно передать query-параметрами:
`GET /v1/context/BTCUSDT?fee_buy=0.001&fee_sell=0.001&slippage=0.0002&k_sl=2&rr=2&tradable_c_r_max=0.45`

**Запрещено** применять один ответ `/v1/context/{symbol}` к нескольким историческим барам, подмешивать его live-поля в backtest или размножать текущий context по исторической временной шкале.

Исторически безопасны только данные, привязанные к времени наблюдения:

- OHLCV самого бара и встроенные в этот бар `indicators` (на любом `interval`);
- mid/HTF history из `/v1/history?interval=…`, присоединённая **as-of** (`bar.ts` старшего ТФ ≤ decision bar.ts, без look-ahead);
- история Fear & Greed из `/v1/macro?fng_days=...`, as-of `fng.ts <= bar.ts`;
- макро-календарь `/v1/calendar?as_of=bar.ts&window_min=60&high_only=true` (события в окне после якоря; `historical_safe=true` в ответе).

Только live/current snapshot: `ticker`, `depth`, `trade_cost`, `cost_risk`, **`htf`**, текущие dominance/macro snapshot, `market_regime`/volatility regime, `derivatives`/`liquidations`, `cross_exchange`, `macro_event_soon` (от `now`) и `failover`. Для исторического исследования этих факторов нужна timestamped история / as-of API выше.

Не торгуй, если `bar` отсутствует, `ready == false`, `indicators_ready == false`, `data_quality.score < 70`, `lag_sec > 120`,
`cost_risk` нет или `cost_risk.tradable == false`, `macro_event_soon == true` или `cross_exchange.divergence_warning == true`
без дополнительной проверки. `lag_sec > 120` автоматически удерживает `data_quality.score` ниже 70; дождитесь свежего `bar_close`.

Отдельно нужны только: live `WS /v1/stream`, история `/v1/history`, лот-фильтры `/v1/meta`,
полный календарь `/v1/calendar`, история F&G `/v1/macro?fng_days=`.

Стакан в `context.depth` может присутствовать, пока снимок не устарел окончательно (~120 с → поле `null`). Для `trade_cost` он считается свежим только пока `freshness.depth.fresh` (`max_age_sec`, обычно 10 с). При `fresh: false` **не** кормите `depth` в свою оценку стоимости — сервер уже подставил спред по умолчанию:

- `best_bid` / `best_ask`, `spread_pct` — для оценки проскальзывания в симуляторе;
- `imbalance` — 0..1, доля бидов в видимом объёме; > 0.5 = давление покупателей;
- `bid_wall` / `ask_wall` — крупнейший лимитный уровень каждой стороны с `distance_pct` от mid.

Фьючерсные данные в context:

- `derivatives.snapshot`: mark/index price, predicted funding, next funding time, open interest;
- `derivatives.funding_7d`: settled funding sample count, `funding_ma_7d`, `funding_std_7d`;
- `derivatives.stale`: `true`, если mark stream не обновлялся более 10 минут;
- `liquidations.windows.five_min` / `one_hour`: `count`, `total_long_qty` / `total_short_qty`, `total_long_value` / `total_short_value`.

Фьючерсные данные используются только как контекст spot-решения; Oracle не торгует фьючерсами.

### Детальный derivatives-контекст

```http
GET /v1/derivatives/BTCUSDT?liquidation_limit=20
Authorization: Bearer mo_...
```

Возвращает те же derivatives/liquidation windows и до 100 последних raw liquidation events. Направление уже нормализовано: `side=long` означает ликвидацию long-позиции, `side=short` — short-позиции. Агенту не нужно интерпретировать сырой order side.

Если `macro_event_soon == true`, в ближайшие 60 минут важное макро-событие (FOMC, CPI…) — стратегии стоит поставить на паузу или сузить риск.

### Макро-календарь

```http
GET /v1/calendar?hours=168&high_only=true
Authorization: Bearer mo_...

# Бэктест / replay: high-impact в следующие window_min минут после bar.ts
GET /v1/calendar?as_of=1700000000000&window_min=60&high_only=true
```

Возвращает экономические события: `ts` (мс UTC), `title`, `country`, `impact` (`low`/`medium`/`high`), `forecast`, `previous`. Поля ответа: `now`, `as_of`, `to`, `count`, `events`, `historical_safe` (`true` если `as_of` задан явно).

Обновляется на сервере раз в час; события **накапливаются** (~90 дней), weekly feed больше не затирает прошлые недели. После рестарта свежий снимок используется до следующего refresh.

Live-флаг `context.macro_event_soon` смотрит от **текущего** `now` (+60 мин) — его нельзя размножать по истории; для каждого бара бэктеста вызывайте calendar с `as_of=bar.ts`.

### Глобальный макро-снимок (Phase 7)

```http
GET /v1/macro?fng_days=30
Authorization: Bearer mo_...
```

Ответ: `snapshot` + опционально `fear_greed_history` (до 90 дней при `fng_days>0`):

```json
{
  "ts": 1784112000000,
  "snapshot": {
    "ts": 1784112000000,
    "fear_greed": { "ts": 1784073600000, "value": 25, "classification": "Extreme Fear" },
    "fear_greed_updated_ts": 1784112000000,
    "btc_dominance_pct": "54.32",
    "eth_dominance_pct": "16.78",
    "total_market_cap_usd": "2345678901234.5",
    "total_volume_24h_usd": "98765432109.8",
    "global_updated_ts": 1784112000000,
    "stablecoin_total_usd": "151000000000",
    "usdt_supply_usd": "112000000000",
    "usdc_supply_usd": "34000000000",
    "stablecoins_updated_ts": 1784112000000,
    "usd_index": "121.4523",
    "usd_index_ts": 1783987200000,
    "us_10y_yield_pct": "4.45",
    "us_10y_ts": 1783987200000
  },
  "fear_greed_history": [ { "ts": 1783987200000, "value": 31, "classification": "Fear" } ]
}
```

**Timestamps:** все `ts` / `*_ts` в API Oracle — **миллисекунды UTC** (13 цифр), включая `usd_index_ts` и `us_10y_ts`. `fear_greed_updated_ts`, `global_updated_ts`, `stablecoins_updated_ts` меняются только после успешного обновления соответствующего блока; общий `snapshot.ts` — если обновился хотя бы один блок. Поэтому при строгой стратегии дополнительно проверяйте timestamp нужного поля, а не только общий `snapshot.ts`. Индекс доллара и доходность 10Y — дневная гранулярность, не live-тик.

**Staleness:** в отдельном `/v1/macro` у `macro_snapshot` нет поля `stale`, поэтому клиент считает `ageSec = (Date.now() - snapshot.ts) / 1000`. В `/v1/context` используйте `freshness.macro_snapshot` (`available`, `fresh`, `age_sec`, `max_age_sec`); аналогичные поля есть для ticker, depth и market regime.

Как интерпретировать:

- `fear_greed < 20` — экстремальный страх, исторически зона разворота вверх; `> 80` — перегрев;
- рост `btc_dominance_pct` — капитал уходит из альтов в BTC, лонги по альтам рискованны;
- рост `usdt_supply_usd`/`stablecoin_total_usd` — накопление покупательной способности (бычий фон);
- рост `usd_index` — укрепление доллара, risk-off для крипты; резкий рост → осторожнее с long;
- рост `us_10y_yield_pct` — дороже деньги, давление на risk assets; >5% historically headwind для альтов;
- обновление раз в час; данные глобальные, одинаковы для всех символов.

Тот же снимок дублируется полем `macro_snapshot` в `GET /v1/context/{symbol}` — отдельный запрос `/v1/macro` нужен только ради истории Fear & Greed. Если поле `null` — первый цикл ещё не завершён или макро на узле выключено.

### Venue consensus (Phase 8)

```http
GET /v1/quotes/BTCUSDT
Authorization: Bearer mo_...
```

Это проверка качества canonical-цены, не арбитражный сигнал. В `venues[]` — непрозрачные `id` (`v1`, `v2`, `v3`). Используй только элементы с `stale=false`. `consensus_mid` — median mid свежих элементов; **`divergence_bps` лежит на venue**, не на корне ответа. При `divergence_warning=true` не открывай новую позицию без дополнительной проверки. Поля `age_ms` и `connected` помогают отличить расхождение рынка от зависшего тика.

### Market regime из собственных данных

```http
GET /v1/market-regime
Authorization: Bearer mo_...
```

Oracle раз в час считает только из собственных `1h` баров:

- `correlation_btc_7d` / `correlation_btc_24h` — связь доходностей символа с BTC;
- `decoupling_detected` — short corr резко упал vs 7d (или abs short очень низкий при высоком 7d);
- `decoupling_count` — сколько пар сейчас в decoupling;
- `realized_volatility_24h_pct` / `7d` — annualized volatility;
- `volatility_regime` — label: `low_vol_range` | `breakout_expansion` | `high_vol_mean_reversion` | `crisis` (режим стратегии, не entry-сигнал);
- `breadth_above_ema20_pct` / `ema50` — доля рынка выше своих EMA;
- `median_return_1h_pct` / `24h` — направление широкого рынка.

Поля `cross_exchange` и `market_regime` также входят в
`GET /v1/context/{symbol}` (плюс удобные копии `volatility_regime` / `decoupling_detected` для запрошенной пары). Отсутствующий regime означает прогрев HTF-истории,
а не ошибку spot feed. На cold start расчёт ждёт native 1h-бары и повторяется через короткий интервал; пустой snapshot с `symbol_count=0` не должен считаться готовым regime. Клиент всё равно проверяет `freshness.market_regime.available/fresh` и наличие запрошенной пары в `market_regime.symbols`.

### Метаданные инструмента

```http
GET /v1/meta/BTCUSDT
Authorization: Bearer mo_...
```

Поля: `base_asset`, `quote_asset`, `status`, `tick_size`, `step_size`, `min_notional`, `updated_at`. Использовать при подготовке торгового приказа: округление цены и количества нельзя вычислять из количества знаков текущей цены.

## Формат бара и индикаторов

Пример структуры:

```json
{
  "ts": 1784098980000,
  "open": "0.32730000",
  "high": "0.32730000",
  "low": "0.32720000",
  "close": "0.32720000",
  "volume": "101557.20000000",
  "quote_volume": "33239.33722000",
  "trades": 86,
  "closed": true,
  "taker_buy_volume": "49770.10000000",
  "indicators": {
    "ema20": "0.32718746",
    "ema50": "0.32706362",
    "ema200": "0.32676985",
    "rsi14": "53.3606",
    "atr14": "0.00011636",
    "atr50": "0.00014000",
    "atr_ratio_14_50": "0.831000",
    "vwap": "0.32659678",
    "vol_sma20": "77102.62500000",
    "adx14": "58.6860",
    "plus_di14": "32.1000",
    "minus_di14": "18.4000",
    "di_side": "1",
    "obv": "4214111.1000",
    "mfi14": "84.3805",
    "cvd_delta": "-2017.00000000",
    "ema50_slope_pct": "0.012000",
    "ema_slope_abs": "0.012000",
    "volume_usd": "33239.33722000",
    "volume_rate": "33239.33722000",
    "bb_mid": "0.32700000",
    "bb_upper": "0.32850000",
    "bb_lower": "0.32550000",
    "bb_width": "0.009174",
    "body": "0.500000",
    "wick_balance": "0.100000",
    "close_pos": "0.800000",
    "candle_q": "0.410000",
    "range_ratio": "0.859000",
    "atr_pct": "0.035560",
    "last_swing_high": "0.32800000",
    "last_swing_low": "0.32500000"
  }
}
```

Дополнительно на баре подтверждения fractal: `swing_high` / `swing_low` (цена пивота). `last_swing_*` несутся вперёд на каждом баре после первого подтверждения.

Числа цены, объёма и индикаторов сериализуются как **строки** для сохранения точности. В TypeScript не предполагать тип `number` в DTO. Для денежных вычислений использовать decimal-библиотеку; преобразование в `number` допустимо только для графиков и приблизительной аналитики.

Поля индикаторов могут отсутствовать во время прогрева **или на старых барах**, сохранённых до появления новых ключей в каталоге. Проверять `indicators_ready` и наличие конкретного значения. После обновления каталога новые closed bars заполняют поля без сброса истории; mid/HTF series пересчитываются на следующем TF sync.

### Каталог индикаторов (все торговые ТФ)

Один и тот же набор ключей на `1m` и на native mid/HTF после warmup. `indicators_ready` = наличие **базового** набора `ema20` + `rsi14` + `atr14` (не всех полей ниже). Во время прогрева отдельные поля — `null` (не подставлять `0`). Все значения зависят от ТФ: EMA200 на `15m` — это 200 пятнадцатиминуток (~50 часов), а не те же 200 минут, что на `1m`. Ниже — расширенный разбор для человека и AI-агента: что измеряет индикатор, как читать значения и как применять в торговом боте, сигнализаторе входа или скринере.

Краткая сводка:

| Ключ | Смысл одним словом |
|------|-------------------|
| `ema20` / `ema50` / `ema200` | тренд (быстрая / средняя / долгая скользящая) |
| `rsi14` | импульс-перекупленность (0–100) |
| `atr14` / `atr_pct` | волатильность в цене и в % |
| `atr50` / `atr_ratio_14_50` | норма волатильности и сжатие/расширение |
| `vwap` | дневной якорь цены (UTC-day reset) |
| `vol_sma20` / `volume_usd` / `volume_rate` | объём и ликвидность бара |
| `adx14` / `plus_di14` / `minus_di14` / `di_side` | сила тренда и его направление |
| `obv` / `mfi14` | накопление/распределение и денежный поток |
| `cvd_delta` | агрессивный поток за бар (taker buy − sell) |
| `ema50_slope_pct` / `ema_slope_abs` | наклон тренда, детектор флета |
| `bb_mid` / `bb_upper` / `bb_lower` / `bb_width` | Bollinger 20,2: канал и squeeze |
| `body` / `wick_balance` / `close_pos` / `candle_q` / `range_ratio` | форма и качество свечи |
| `swing_high` / `swing_low` / `last_swing_*` | фрактальные уровни с лагом 2 бара |

#### `ema20` / `ema50` / `ema200` — экспоненциальные скользящие средние цены закрытия

EMA сглаживает шум и показывает направление тренда: цена выше EMA — покупатели сильнее, ниже — продавцы. Короткая `ema20` реагирует быстро и годится для входа по отбою или пересечению с `ema50`, длинная `ema200` — «граница бычьего/медвежьего рынка», ниже которой лонги без подтверждения старших ТФ рискованны. В боте типичны три приёма: фильтр «торгуем лонг только выше EMA200 своего ТФ», кросс `ema20/ema50` как триггер импульса, отбой от EMA после её теста тенью. Подводные камни: EMA200 появляется только после 200 баров **этого интервала** — на `15m` это ~2 суток native-истории, проверяйте наличие поля, а не `ready` 1m-серии.

#### `rsi14` — индекс относительной силы, 0–100

RSI показывает, насколько «перегрет» импульс: значения выше 70 традиционно читают как перекупленность, ниже 30 — как перепроданность, а середина ~50 — баланс. Для торгового бота это не кнопка «покупай/продавай», а фильтр mean-reversion: возврат RSI из экстремума к середине подтверждает затухание импульса, а залипание RSI выше 60 при растущей цене — признак сильного тренда, где контртрендовый шорт опасен. Сигнализатор входа обычно ждёт не сам уровень, а связку «RSI вышел из зоны + цена удержала EMA/VWAP + объём выше нормы». Во время сильного тренда RSI может долго стоять в экстремуме — не ставьте слепой контртренд только по `rsi14 > 70`.

#### `atr14` / `atr_pct` — средний истинный диапазон и его доля от цены

ATR измеряет «обычный» размер свечи за 14 баров и отвечает на вопрос, сколько цена реально ходит, без оглядки на направление. `atr_pct = atr14 / close × 100` нормализует это к цене, поэтому по нему можно сравнивать пары между собой: `0.05%` на 1m — спокойный BTC, `0.5%` — разогнанный альт. Боты используют ATR для стопов (`стоп = k × ATR`, в Oracle `d_sl = k_sl × ATR/close`), размера позиции (риск в % от ATR) и фильтра шума (`range_ratio = range / ATR << 1` — пропускать вялые бары). `trade_cost`/`cost_risk` в `context` уже считают отношение комиссий к ATR-стопу — не хардкодьте свой ATR параллельно.

#### `atr50` / `atr_ratio_14_50` — длинная норма волатильности и сжатие

`atr50` — тот же ATR, но за 50 баров: «какая волатильность здесь норма». Отношение `atr_ratio_14_50 = atr14 / atr50` ниже ~0.8 означает, что рынок сжался относительно своей нормы (squeeze, энергия копится), выше ~1.2 — расширение, импульс уже идёт. Скринеры любят условие «ratio вышел снизу вверх + пробой Bollinger + объём ×2 от SMA» как заготовку breakout-сигнала, а контртрендовые системы наоборот пропускают входы при ratio >> 1. Поле появляется позже базового `atr14`, проверяйте `null` в первые десятки баров ТФ.

#### `vwap` — сессионный средневзвешенный якорь цены (сброс в 00:00 UTC)

VWAP показывает, где прошла основная торговля дня с учётом объёма, поэтому институциональные алгоритмы меряют исполнение относительно него: выше VWAP — день за покупателями, ниже — за продавцами. Внутридневной бот использует его как динамический уровень: отбой от VWAP по направлению дневного тренда — вход, залипание под ним при падающем дне — запрет лонга. Важно: на `5m`/`15m`/`1h` это корректный дневной VWAP, на `1d` — VWAP самой дневной свечи, а на `1w` **не** недельный VWAP (сброс всё равно каждый UTC-день) — недельным якорем его считать нельзя.

#### `vol_sma20` / `volume_usd` / `volume_rate` — объём и ликвидность

`vol_sma20` — средняя база объёма за 20 баров, точка отсчёта «много/мало». `volume_usd` — деньги бара в долларах (`quote_volume`), отвечает, можно ли здесь вообще исполниться без проскальзывания. `volume_rate = volume_usd / минут_интервала` ($/мин) делает объёмы сравнимыми между ТФ: 1m-бар на $30k и 15m-бар на $450k — одинаковый темп. Боты фильтруют входы условием «текущий $/мин ≥ 1.5–2 × SMA», сигнализаторы подсвечивают всплеск объёма как подтверждение пробоя, а скринеры сортируют пары по `volume_rate` для выбора ликвидных. На свежих парах до прогрева SMA может отсутствовать — тогда ориентируйтесь на абсолютный `volume_usd`.

#### `adx14` — сила тренда без направления (0–100)

ADX отвечает «есть ли тренд», а не «куда»: значения до ~20–25 — флет/пилa, 25–30 — зарождение движения, выше 30 — выраженный тренд, выше 50 — сильный разгон. Сеточные и mean-reversion боты торгуют только при ADX ≤ 25 на своём ТФ, трендовые — только при ADX ≥ 25–30 плюс DI-подтверждение. ADX считается по Wilder-сглаживанию и появляется примерно после 27 баров ТФ — до этого поле `null`, и это нормально, а не «ноль тренда».

#### `plus_di14` / `minus_di14` / `di_side` — направление тренда

Пара DI дополняет ADX стороной: `plus_di14 > minus_di14` — давление покупателей, наоборот — продавцов, а строковый `di_side` сворачивает это до `"1"` (лонг), `"-1"` (шорт), `"0"` (равенство). Типовой фильтр: `adx14 ≥ 25 AND di_side == "1"` разрешает только лонги, смена `di_side` при высоком ADX — ранний признак разворота. Не читайте ADX как направление: ADX=40 при `di_side="-1"` — это сильный падающий тренд, лонговать его «потому что ADX высокий» — ошибка.

#### `obv` — балансовый объём (кумулятивный)

OBV прибавляет объём бара со знаком закрытия (вверх — плюс, вниз — минус) и показывает, накапливают актив или раздают: растущий OBV при боковой цене — бычья дивергенция, падающий при растущей цене — слабость. В боте OBV — подтверждение, а не триггер: пробой уровня с растущим OBV берут, без него — пропускают. Абсолютное число OBV бессмысленно само по себе (зависит от истории), важны наклон и дивергенции; на старте истории ряд короткий — ждите накопления.

#### `mfi14` — денежный поток, 0–100 (объёмный RSI)

MFI похож на RSI, но взвешивает движение на объём: значения выше 80 — перекупленность с деньгами, ниже 20 — перепроданность. Сигнализаторы используют его как второй голос к RSI: «RSI > 70 И MFI > 80» — перегрев надёжнее, чем один RSI, а бычья дивергенция MFI (цена ниже, MFI выше) — ранний лонг-сигнал. На тонких парах MFI шумит сильнее RSI — подтверждайте объёмным фильтром (`volume_rate`) и не стройте вход на одном MFI.

#### `cvd_delta` — дельта кумулятивной ленты за бар (taker buy − sell)

`cvd_delta` показывает, кто бил маркетом внутри бара: положительная — агрессивно покупали, отрицательная — продавали. Суммируя её на клиенте по закрытым барам, получают кривую CVD для дивергенций с ценой (цена вверх + CVD вниз = покупки выдыхаются). Критично: дельта валидна только пока `/health.cvd.status == "ok"`; при `failover: true` поле теряет смысл накопительного потока — CVD-боту в этот момент надо стоять в стороне, а OHLC-сигналы 15m без CVD при `bars.status == "ok"` допустимы.

#### `ema50_slope_pct` / `ema_slope_abs` — наклон тренда за бар

Наклон EMA50 в процентах показывает скорость тренда и служит детектором флета: `|slope| ≤ 0.1` на своём ТФ — классическое условие «пила, включаем сетку», выше — трендовый режим. Сеточные боты берут это как переключатель режимов вместо глазомера, трендовые — как фильтр «не входим в дрейф без наклона». Знак `ema50_slope_pct` задаёт сторону, `ema_slope_abs` — модуль для порога; на старших ТФ порог флета подбирают отдельно, копировать 0.1 с 1m на 1h нельзя.

#### `bb_mid` / `bb_upper` / `bb_lower` / `bb_width` — Bollinger 20,2 и ширина канала

Полосы строятся как SMA20 ± 2σ: касание верхней при растущем канале — сила, хождение вдоль полосы — тренд, возврат внутрь — затухание. Нормированная ширина `bb_width = (U−L)/mid` — главный squeeze-метр: минимальные значения за N баров + выход цены за полосу с объёмом — классический breakout-вход. Mean-reversion системы наоборот торгуют возврат к `bb_mid` только при широком канале и спокойном ADX. Полосы появляются после 20 баров ТФ; на тонком рынке ложных проколов много — подтверждайте объёмом и `range_ratio`.

#### `body` / `wick_balance` / `close_pos` / `candle_q` / `range_ratio` — форма и качество свечи

Пакет описывает «как» бар закрылся, а не «где»: `body` — направленность тела (−1…1), `wick_balance` — перевес нижней/верхней тени (rejection без именных паттернов), `close_pos` — где закрылись внутри диапазона (0…1), `candle_q = 0.4·body + 0.3·wick_balance + 0.3·(2·close_pos−1)` — сводная оценка качества одним числом, `range_ratio = range / ATR` — шум (`<< 1`) или расширение (`>> 1`). Боты отсекают «пустые» дожи (`|body| < 0.2`), требуют `close_pos > 0.7` для лонга и `candle_q` выше порога как фильтр мусора. При `range = 0` поля отсутствуют — это вырожденный бар, а не «ноль сигнала».

#### `swing_high` / `swing_low` / `last_swing_high` / `last_swing_low` — фрактальные уровни (strength=2)

Фрактал фиксирует локальный экстремум с подтверждением через 2 бара **этого** ТФ: поля `swing_high`/`swing_low` присутствуют только на баре подтверждения (лаг 2), а `last_swing_*` тянутся вперёд на каждом баре как актуальные уровни. Это готовые опоры для стопов за экстремум, целей по Фибо, пробойных входов и счётчика касаний на клиенте. Не читайте swing как сигнал сам по себе — это каркас уровней; вход строится как «касание/пробой уровня + форма бара + объём + старший фильтр», а лаг 2 бара запрещает перерисовку задним числом.

**Не просить у Oracle:** RSI-reclaim state, счётчик касаний, фазы стратегии, дневной HL-канал, MACD/Stoch/Supertrend/Ichimoku, `trend_z` / `rs_rank`, raw depth history / wall_* для скальпа стакана.

### Форма свечи и нормализованный риск (на баре)

Считаются O(1) на закрытии бара; попадают в `bar.indicators` (и в WS `bar_close` для 1m). `range = high − low`; при `range = 0` поля формы отсутствуют.

| Поле | Формула / смысл | Как использовать в роботе |
|------|-----------------|---------------------------|
| `body` | `(close − open) / range` (signed) | Направленность тела; отсечь «пустые» свечи |
| `wick_balance` | `(lower_wick − upper_wick) / range` | Rejection / pin без именных паттернов |
| `close_pos` | `(close − low) / range`, 0..1 | Лонг сильнее при close у high |
| `candle_q` | `0.4·body + 0.3·wick_balance + 0.3·(2·close_pos − 1)` | Один порог «качество формы» |
| `range_ratio` | `range / atr14` | Шум (`<< 1`) vs расширение (`>> 1`) |
| `atr_pct` | `atr14 / close × 100` | Риск в % цены; сравнивать пары между собой |

Не принимать решение по одному индикатору. Комиссию и «дороговизну» входа брать из `context.trade_cost` / `context.cost_risk`, а не хардкодить в стратегии.

## Edge: `trade_cost` и `cost_risk`

Доступны **только** в `GET /v1/context/{symbol}` (не дублируются в WS — намеренно, чтобы не раздувать stream на 10–30 API-ключей).

Комиссии по умолчанию задаёт сервер. Привязки fee к Oracle API-key **нет**: клиент передаёт свои комиссии query-параметрами или принимает значения сервера. Биржевой ключ пользователя Oracle не использует.

### Формулы

```text
half_spread = spread_bps / 20000          # доля номинала; spread из fresh depth или default_spread_bps
C_spot      = fee_buy + fee_sell + 2·slippage + half_spread
C_perp      = C_spot + |funding_rate|    # грубая оценка на один funding-интервал
d_sl        = k_sl · (atr14 / close)     # стоп как доля цены
c_R         = C_spot / d_sl
p_be        = (1 + c_R) / (RR + 1)       # безубыточный winrate при заданном RR
tradable    = c_R <= tradable_c_r_max    # default 0.45
```

### Поля ответа

| Объект / поле | Смысл |
|---------------|--------|
| `trade_cost.source` | всегда `"preset"` (пока нет account-fee API) |
| `trade_cost.fee_buy` / `fee_sell` | доли номинала (0.001 = 0.1%) |
| `trade_cost.spread_bps` | полный спред в bps |
| `trade_cost.spread_from_depth` | `true`, если взят из fresh `depth`; иначе значение по умолчанию сервера |
| `trade_cost.slip_bps` | per-side slip в bps (из `slippage` preset/query) |
| `trade_cost.half_spread` | половина спреда как доля (входит в `c_spot` один раз) |
| `trade_cost.c_spot` | **главное**: round-trip cost spot |
| `trade_cost.funding_rate` / `c_perp` | опционально из derivatives |
| `cost_risk.atr_pct` | ATR в % цены |
| `cost_risk.d_sl_pct` | `k_sl · atr_pct` |
| `cost_risk.c_r` | стоимость сделки в единицах стопа |
| `cost_risk.p_be` | какой winrate нужен, чтобы выйти в ноль |
| `cost_risk.tradable` | жёсткий gate до входа |
| `cost_risk.params` | фактически использованные `k_sl`, `rr`, `tradable_c_r_max` |

### Рекомендуемый decision path робота

```text
bar_close (WS)
  → обновить локальный бар / candle_q / atr_pct / cvd_delta
  → GET /v1/context/{symbol}[?fee_buy=…&rr=…]   # перед входом или по расписанию
  → if data_quality.score < 70 or lag_sec > 120 → SKIP
  → if cost_risk.tradable == false             → SKIP  (шум съедают комиссии)
  → if candle_q / close_pos не в вашу сторону  → SKIP  (опциональный фильтр формы)
  → своя логика сигнала (EMA/RSI/ADX/regime…)
  → размер позиции от atr_pct и C_spot
```

**Бэктест и live:** одни и те же комиссии в query или в значениях сервера, иначе EV и `tradable` разъедутся. Не считайте свой `C` параллельно «на глаз», если уже есть `trade_cost.c_spot`.

Ticker содержит `last`, `bid`, `ask`, `open_24h`, `high_24h`, `low_24h`, `volume_24h`, `quote_volume_24h`, `price_change_pct_24h`, `weighted_avg_24h` (суточный VWAP). Отдельные nullable-поля могут отсутствовать.

## WebSocket

**Webhooks нет.** Live только `WSS /v1/stream` + reconnect с backoff, как в примерах ниже. Не регистрировать callback-URL и не ждать входящий HTTP от Oracle.

После подключения сервер отправляет `hello` (`type`, `version`, `failover`). До явной подписки события не поступают. После `subscribe` приходит служебное **`{"type":"subscribed","symbols":[...],"all":bool,"microstructure":bool}`** — это не бар, парсить по `type`.

Подписка на выбранные пары. Microstructure является opt-in, поэтому старые клиенты продолжают получать только `bar_close`:

```json
{"op":"subscribe","symbols":["BTCUSDT","ETHUSDT"],"microstructure":true}
```

Подписка на все пары:

```json
{"op":"subscribe","symbols":["*"]}
```

Событие:

```json
{
  "type": "bar_close",
  "symbol": "BTCUSDT",
  "interval": "1m",
  "failover": false,
  "bar": {}
}
```

`bar` содержит OHLCV + `indicators` (полный каталог после прогрева). Есть флаг **`failover`**. **`trade_cost` / `cost_risk` / `htf` в WS не приходят** — запрашивайте `/v1/context` перед входом. Накопительный CVD клиент считает сам из `cvd_delta`.

После финализации минутного объекта подписчик с `microstructure:true` получает отдельное additive-событие:

```json
{
  "type": "microstructure_close",
  "symbol": "BTCUSDT",
  "interval": "1m",
  "microstructure": {
    "ts": 1784179200000,
    "closed": true,
    "sample_count": 58,
    "expected_samples": 60,
    "coverage_pct": "96.7",
    "quality": { "score": 95, "flags": [] }
  }
}
```

`bar_close` не блокируется при недоступном microstructure. Клиент временно хранит оба объекта и объединяет их строго по `symbol + ts`; отсутствие microstructure запрещает только depth-зависимый вход, а не обработку обычного бара. Не полагайтесь на сетевой порядок двух типов событий: это независимые broadcast-каналы.

Также поддерживаются:

```json
{"op":"unsubscribe","symbols":["ETHUSDT"]}
{"op":"ping"}
```

Ответ на ping: `{"type":"pong"}` (не `op`).

Клиент обязан реализовать reconnect с backoff, дедупликацию бара и microstructure по ключу `` `${symbol}:${ts}` `` и восстановление пропусков через REST. После reconnect сначала загрузите `/v1/history`, затем `/v1/microstructure` за тот же диапазон и выполните exact join. WebSocket — уведомление, REST — источник восстановления состояния.

Рекомендуемый reconnect:

| Параметр | Значение |
|----------|----------|
| initial delay | **1s** |
| multiplier | **2×** |
| max delay | **60s** |
| jitter | **±25%** |

Не ставить initial delay в миллисекунды — при массовом рестарте клиентов это бьёт по серверу.

## Минимальный TypeScript-пример

```typescript
const baseUrl = process.env.ORACLE_BASE_URL!;
const apiKey = process.env.ORACLE_API_KEY!;

async function oracle<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(`${baseUrl}${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });

  if (!response.ok) {
    throw new Error(`Oracle ${response.status}: ${await response.text()}`);
  }
  return response.json() as Promise<T>;
}

const symbols = await oracle("/v1/symbols");
const latest = await oracle<{
  ts: number;
  symbols: Record<string, { bar: unknown; ticker: unknown; indicators_ready: boolean }>;
}>("/v1/latest?symbols=BTCUSDT,ETHUSDT");
const btcBar = latest.symbols["BTCUSDT"]?.bar; // всегда через .symbols[…]
```

Для Node.js WebSocket предпочтительно передавать ключ заголовком, а не query-параметром, чтобы секрет не попадал в access logs.

## Ошибки и эксплуатационные ограничения

### HTTP status code ≠ JSON `status`

| Слой | Что это | Пример |
|------|---------|--------|
| **HTTP status** | код ответа протокола (`response.status` / `res.status`) | `200`, `401`, `429`, `503` |
| **JSON `status`** | машинный код в теле; на ошибках **равен** `code` | `"ok"`, `"key_expired"`, `"quota_exceeded"` |

На успехе (`/health`, `/v1/me`) JSON `status` обычно `"ok"`. На ошибке auth JSON `status` будет `"key_expired"` и т.п., а HTTP — `401`. Не пишите парсер только по `body.status === "ok"` без проверки HTTP.

```javascript
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
const body = await res.json().catch(() => ({}));

if (!res.ok) {
  // смотрите body.code (или body.status — то же значение на ошибках)
  if (body.code === "key_expired") {
    // остановить торговлю, запросить продление
  } else if (res.status === 429) {
    // too_fast | rate_limited | quota_exceeded | ws_limit → backoff
  }
  throw new Error(`${res.status} ${body.code}: ${body.error}`);
}
// успех: HTTP 2xx; для /health: body.bars.status === "ok" (верхний status может быть "degraded");
// CVD-стратегия дополнительно body.cvd.status === "ok"
```

- `401` — проблема с ключом. Смотри JSON **`code`** / **`status`** (одинаковы):
  - `key_expired` — срок истёк (`expires_at` в теле);
  - `key_revoked` — ключ отозван;
  - `key_invalid` — неизвестный ключ;
  - `key_missing` — нет Bearer.
- `503` / JSON `status: unavailable` — Oracle/auth временно недоступен; backoff и повторить.
- `429` — лимит тарифа. Смотри `code` (на REST-ошибках обычно равен JSON `status`):
  - `too_fast` — min_interval между REST-запросами;
  - `rate_limited` — RPM (ключ или global);
  - `quota_exceeded` — кончились дневные **платные REST-запросы** (`daily_used >= daily_limit`). **Не** ретраить data-REST до UTC midnight (`X-Quota-Reset` / `retry_after_ms`). WebSocket и служебные `/v1/me`/`/v1/symbols` в этот стоп не входят;
  - `ws_limit` — слишком много одновременных WS на ключ; тело upgrade-ответа содержит `code`/`error`/`max_ws`/`retry_after_ms` и **может не содержать** поле `status` — парсите `code`.
  Заголовки REST и парсинг остатка — раздел **«Заголовки лимитов»**. В теле REST есть `retry_after_ms`. Для `too_fast` / `rate_limited` — backoff с jitter; для `quota_exceeded` цикл ниже **не** подходит (будет долбить до полуночи):

```javascript
async function oracleGet(url, key, attempt = 0) {
  const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
  if (res.status !== 429) return res;
  const body = await res.json().catch(() => ({}));
  if (body.code === "quota_exceeded") return res; // ждать UTC midnight, не крутить REST
  const waitMs = body.retry_after_ms
    ?? (Number(res.headers.get("Retry-After") || 1) * 1000);
  const jitter = Math.floor(Math.random() * 250);
  await new Promise((r) => setTimeout(r, Math.min(60_000, waitMs * (2 ** attempt) + jitter)));
  return oracleGet(url, key, attempt + 1);
}
```

**Клиентские тарифы** (каталог без цены — раздел **«Клиентские тарифы»**): `free` **2000**/день · 1 WS, **бессрочный**; `basic` **5000** · 2 WS · месяц; `pro` **8000** · 3 WS · месяц. Имени `standard` нет. Жёсткие цифры ключа — `GET /v1/me`.

- `400` — пустой symbol, кривой body/time range (`bad_range`, `bad_interval`, `bad_seconds`, …). Незарегистрированная пара на context/status/latest — обычно **200** с пустыми данными, не 400;
- `502` / `code: meta_unavailable` — внешний источник не ответил при получении `/v1/meta`;
- `5xx` — внутренняя ошибка; повторять с ограниченным exponential backoff.

Не делать tight polling: см. раздел **«Рекомендуемые задержки между REST-запросами»**. Live — **`WS /v1/stream`**; `/latest` и `/context` — с интервалами, не «каждые 100 ms».

1m и microstructure — скользящее окно (обычно ~60 дней), а не вечный архив. Если платформе нужна долговременная статистика или обучение моделей, она должна сохранять полученные закрытые бары в собственной БД.

## Что этот документ не покрывает

Как устроен сервис изнутри, **не публикуется**. Контракт клиента — поля API, лимиты тарифа и правила выше.

Отдельных endpoint'ов `/v1/xray`, `/v1/report`, `/v1/showcase` **нет**. Любой UI (светофор, отчёт, «x-ray») строится у вас на клиенте из `GET /v1/context/{symbol}`. Источник правды — поля ответа, не виджет.

Этот документ описывает текущий API Oracle `1.0.0` (нативные mid-TF `5m`/`15m`/`30m` + полный каталог indicators на всех ТФ).

---

## Приложение: примеры запросов и ответов

Ниже — типовые вызовы с **JSON-ответом** и расшифровкой полей. Числа-цены в API часто приходят **строками** (Decimal). Все `/v1/*` кроме публичных `GET /health` и `GET /v1/health` требуют:

`Authorization: Bearer mo_…`

База примеров: `https://api.market-oracle.pro`.

---

### 1. `GET /health` (публичный)

**Запрос**

```http
GET /health
```

**Ответ**

```json
{
  "status": "ok",
  "uptime_sec": 31641,
  "ws_connected": true,
  "failover": false,
  "bar_lag_sec": 85,
  "bars": { "status": "ok", "lag_sec": 85 },
  "cvd": { "status": "ok" },
  "version": "1.0.0"
}
```

Пример на failover (свечи живы, CVD выключен): `"failover":true`, `"bars":{"status":"ok"}`, `"cvd":{"status":"down","reason":"failover"}`. Верхний `status` при этом может остаться `"ok"` или стать `"degraded"` — для OHLC смотрите `bars.status`.

| Поле | Описание |
|------|----------|
| `status` | `ok` если поток закрытых баров подключён (`ws_connected`) и **бары** свежие (`bars.status == "ok"`). Иначе `"degraded"`. **Не** падает из‑за `failover` и **не** отражает CVD. HTTP 200 ≠ можно торговать; верхний `status=ok` ≠ можно торговать CVD. |
| `bars` | Canonical 1m. `ok` — OHLC можно использовать, типичный `lag_sec` mid-minute ~60–120, stale порог **180 с**. `down` + `reason: "stale"` — дыра в свечах, не входить. |
| `cvd` | `ok` — накопительный CVD/`cvd_delta` можно суммировать. Иначе `down` / `reason: "failover"` или `unavailable`. Боты с CVD **обязаны** смотреть это поле. |
| `bar_lag_sec` | Худший (наибольший) возраст закрытого 1m среди зарегистрированных пар; дублирует `bars.lag_sec`. |
| `uptime_sec` | Сколько секунд Oracle непрерывно работает после старта. Резкий сброс = был рестарт: проверьте `history`/`lag_sec`; сервер сам заполняет только отсутствующие диапазоны. |
| `ws_connected` | Поток закрытых баров подключён. Это не гарантия свежего тика по каждой паре: смотрите `bars.lag_sec`. |
| `failover` | `true` — резервный режим. Для 15m без CVD на ликвидных USDT это нормально. CVD при этом `down`; microstructure на этом баре нет. |
| `version` | Версия API-узла. |

Перед входом на 15m: `GET /health` (`bars` / при необходимости `cvd`) + `GET /v1/status/{symbol}` (`data_quality.score >= 70`, `lag_sec`, `indicators_ready`) + `GET /v1/context/{symbol}` (`cost_risk.tradable`, freshness). Native 15m — `history?interval=15m` / `context.htf.m15`, не агрегат из 1m.

---

### 2. `GET /v1/me` — тариф текущего ключа

**Запрос**

```http
GET /v1/me
Authorization: Bearer mo_…
```

**Ответ**

```json
{
  "status": "ok",
  "key_id": "key_bf6ed463bc8d112f",
  "name": "home-bot",
  "key_prefix": "mo_41ec884",
  "tier": "basic",
  "tier_label": "Basic",
  "expires_at": "2026-08-16T00:00:00+00:00",
  "expired": false,
  "created_at": "2026-07-16T04:00:00+00:00",
  "last_used_at": "2026-07-16T06:30:00+00:00",
  "daily_used": 420,
  "daily_limit": 5000,
  "daily_remaining": 4580,
  "daily_load": 1260,
  "rpm": 60,
  "min_interval_ms": 200,
  "max_ws": 2,
  "ws_open": 1,
  "weights": {
    "default": 1,
    "context": 3,
    "history": 3,
    "history_batch_per_symbol": 3,
    "bootstrap": 10
  }
}
```

| Поле | Описание |
|------|----------|
| `status` | Здесь всегда `ok` при успехе. |
| `key_id` | Внутренний id ключа (не секрет). Нужен для поддержки/логов. |
| `name` | Человекочитаемое имя ключа. |
| `key_prefix` | Короткий префикс секрета (`mo_…`) для опознания ключа без полного раскрытия. |
| `tier` | Клиентский тариф: `free` / `basic` / `pro` (имени `standard` нет). Каталог лимитов и кому какой — раздел **«Клиентские тарифы»**. |
| `tier_label` | Отображаемое имя тарифа. |
| `expires_at` | UTC-момент окончания. У **free** ключ бессрочный. У Basic/Pro после этой даты REST вернёт `key_expired`. |
| `expired` | Уже просрочен ли ключ на момент ответа (удобная булева проверка). |
| `created_at` / `last_used_at` | Когда создан и когда последний раз успешно прошёл auth. |
| `daily_used` | Сколько **платных REST-запросов** израсходовано за текущие UTC-сутки (1 HTTP = 1). |
| `daily_limit` | Дневной потолок запросов. Канон тарифа: free=2000, basic=5000, pro=8000. `0` = без лимита. |
| `daily_remaining` | Остаток дневного бюджета в **запросах**. При `daily_limit=0` в JSON будет `null` (не строка `unlimited`). |
| `daily_load` | Сумма weights за тот же день (нагрузка). Не сравнивать с `daily_limit`. |
| `rpm` | Максимум REST-запросов в скользящую минуту на этот ключ. |
| `min_interval_ms` | Минимальная пауза между двумя REST-вызовами одного ключа. |
| `max_ws` | Сколько одновременных `/v1/stream` разрешено. |
| `ws_open` | Сколько WS сейчас реально открыто этим ключом. |
| `weights.*` | Справка по нагрузке endpoint’ов (`daily_load`): `context`/`history` обычно тяжелее `latest`. На `daily` не влияют. |

---

### 3. `GET /v1/symbols`

**Запрос**

```http
GET /v1/symbols
Authorization: Bearer mo_…
```

**Ответ (фрагмент)**

```json
{
  "symbols": [
    {
      "symbol": "BTCUSDT",
      "market": "spot",
      "bars_1m": 30240,
      "span_hours": 504.0,
      "ready": true,
      "history": "full_day",
      "last_closed_ts": 1784179200000,
      "added_at": "2026-06-01T00:00:00+00:00",
      "ttl_days": 60
    }
  ]
}
```

| Поле | Описание |
|------|----------|
| `symbol` | Торговая пара в верхнем регистре. |
| `market` | Рынок (`spot`). |
| `bars_1m` | Сколько закрытых 1m-баров доступно по паре. |
| `span_hours` | Грубая глубина истории ≈ `bars_1m / 60`. |
| `ready` | `true`, если ≥200 баров — хватает для EMA200. **Не** означает наличие ema20/rsi/atr. |
| `history` | `warming` / `ready` / `full_day` (≥1440 баров). Для боевых стратегий предпочтителен `full_day`. |
| `last_closed_ts` | Timestamp (**мс UTC**) последнего закрытого бара или `null`. |
| `added_at` | Когда пара включена в реестр (UTC). |
| `ttl_days` | Окно хранения закрытых 1m на узле. |

---

### 4. `GET /v1/status/{symbol}`

**Запрос**

```http
GET /v1/status/BTCUSDT
Authorization: Bearer mo_…
```

**Ответ**

```json
{
  "symbol": "BTCUSDT",
  "market": "spot",
  "interval": "1m",
  "registered": true,
  "bar_count": 30240,
  "span_hours": 504.0,
  "ready": true,
  "history": "full_day",
  "last_closed_ts": 1784179200000,
  "lag_sec": 12,
  "indicators_ready": true,
  "data_quality": { "score": 95, "flags": ["fresh_venues=2"] },
  "failover": false
}
```

| Поле | Описание |
|------|----------|
| `registered` | Пара есть в рабочем наборе узла. |
| `bar_count` / `span_hours` / `ready` / `history` | То же, что в `/v1/symbols`, но для одной пары детальнее. |
| `interval` | Интервал live-серии (обычно `1m`). |
| `lag_sec` | Возраст последнего закрытого бара: `(now − bar.ts) / 1000`, где `bar.ts` — **open time** closed 1m. Mid-minute ~60–120s — норма; `>120` — caution; `>180` — skip. Не путать с сетевым RTT. |
| `indicators_ready` | На последнем баре есть **базовый набор** (`ema20` + `rsi14` + `atr14`). Может быть `true` при `bar_count < 200` (`ready == false`). Для EMA200 дополнительно требуйте `ready`. |
| `data_quality.score` | 0–100 композит качества фидов. Для нового решения обязательно `>= 70`; при `lag_sec > 120` score принудительно ниже 70. |
| `data_quality.flags` | Теги штрафов (`lag_sec>120`, `lag_sec>180`, `divergence_warning`, `fresh_venues=1`, …). |
| `failover` | Резервный режим. При `true` не суммировать CVD. |

---

### 5. `GET /v1/latest?symbols=`

**Запрос**

```http
GET /v1/latest?symbols=BTCUSDT
Authorization: Bearer mo_…
```

**Ответ (сокращённо)**

Форма **всегда** `{ "ts": number, "symbols": { "<PAIR>": { ... } } }` — даже для одного символа. Нельзя `response.bar`; только `response.symbols.BTCUSDT.bar`.

```json
{
  "ts": 1784179200000,
  "symbols": {
    "BTCUSDT": {
      "bar": {
        "ts": 1784179200000,
        "open": "65000.00",
        "high": "65080.00",
        "low": "64950.00",
        "close": "65040.00",
        "volume": "123.45",
        "quote_volume": "8023456.78",
        "trades": 4120,
        "closed": true,
        "taker_buy_volume": "60.10",
        "indicators": {
          "ema20": "64910.12",
          "ema50": "64500.00",
          "ema200": "62000.00",
          "rsi14": "54.2",
          "atr14": "180.5",
          "vwap": "64980.00",
          "vol_sma20": "95.0",
          "adx14": "22.1",
          "obv": "1500000",
          "mfi14": "48.0",
          "cvd_delta": "-3.25",
          "body": "0.307692",
          "wick_balance": "0.153846",
          "close_pos": "0.692308",
          "candle_q": "0.261538",
          "range_ratio": "0.720222",
          "atr_pct": "0.277521"
        }
      },
      "ticker": {
        "ts": 1784179235000,
        "last": "65042.10",
        "bid": "65042.00",
        "ask": "65042.20",
        "volume_24h": "28000.5",
        "quote_volume_24h": "1800000000",
        "price_change_pct_24h": "1.25",
        "weighted_avg_24h": "64890.00"
      },
      "indicators_ready": true
    }
  }
}
```

| Поле | Описание |
|------|----------|
| `ts` | Максимальный `bar.ts` среди возвращённых символов (**мс UTC**). |
| `symbols` | Обязательная обёртка `Record<string, SymbolLatest>`; ключ = символ в верхнем регистре. |
| `symbols.<PAIR>.bar` | Последний **закрытый** 1m-бар. Это и есть вход для сигнала (`closed: true`). |
| `bar.ts` | Время открытия закрытой свечи (**мс UTC**), выровнено по минуте. |
| `bar.open/high/low/close/volume` | Классический OHLCV. Цены — строки. |
| `bar.quote_volume` | Объём в quote-активе (USDT). |
| `bar.trades` | Число сделок внутри бара. |
| `bar.taker_buy_volume` | Объём агрессивных покупок; нужен для `cvd_delta`. |
| `bar.indicators.*` | Инкрементальные индикаторы на закрытии бара (EMA/RSI/ATR/ADX+DI/VWAP/OBV/MFI/CVD, slope, BB, volume$, swing, форма/`atr_pct`). Один каталог на всех ТФ. Не пересчитывайте без причины. `trade_cost`/`cost_risk`/`htf` здесь **нет** — только в `/v1/context`. |
| `ticker` | Живой 24h-снимок (может быть новее бара). Для UI/sizing/slippage, **не** для генерации входа. |
| `indicators_ready` | Базовый набор ema20+rsi14+atr14 на баре (не замена `ready` ≥200). Не требует `candle_q`. |

---

### 6. `GET /v1/history/{symbol}`

**Запрос**

```http
GET /v1/history/BTCUSDT?interval=1m&limit=3
GET /v1/history/BTCUSDT?interval=15m&limit=200
Authorization: Bearer mo_…
```

**Ответ**

```json
{
  "symbol": "BTCUSDT",
  "market": "spot",
  "interval": "15m",
  "from": 1784178300000,
  "to": 1784179200000,
  "count": 2,
  "bars": [
    {
      "ts": 1784178300000,
      "open": "64950.00",
      "high": "65120.00",
      "low": "64910.00",
      "close": "65060.00",
      "volume": "410.25",
      "quote_volume": "26680000.00",
      "trades": 18500,
      "closed": true,
      "taker_buy_volume": "215.40",
      "indicators": {
        "ema20": "64890.12",
        "ema50": "64520.00",
        "ema200": "62100.00",
        "rsi14": "58.40",
        "atr14": "210.50",
        "atr50": "245.00",
        "atr_ratio_14_50": "0.859184",
        "atr_pct": "0.323596",
        "vwap": "65010.00",
        "vol_sma20": "380.00",
        "volume_usd": "26680000.00",
        "volume_rate": "1778666.67",
        "adx14": "24.80",
        "plus_di14": "26.10",
        "minus_di14": "18.20",
        "di_side": "1",
        "obv": "1520000.00",
        "mfi14": "55.20",
        "cvd_delta": "18.60",
        "ema50_slope_pct": "0.042000",
        "ema_slope_abs": "0.042000",
        "bb_mid": "64900.00",
        "bb_upper": "65300.00",
        "bb_lower": "64500.00",
        "bb_width": "0.012320",
        "body": "0.523810",
        "wick_balance": "0.095238",
        "close_pos": "0.714286",
        "candle_q": "0.366667",
        "range_ratio": "0.997626",
        "last_swing_high": "65200.00",
        "last_swing_low": "64400.00"
      }
    },
    {
      "ts": 1784179200000,
      "open": "65060.00",
      "high": "65180.00",
      "low": "65010.00",
      "close": "65140.00",
      "volume": "385.10",
      "quote_volume": "25070000.00",
      "trades": 17200,
      "closed": true,
      "taker_buy_volume": "205.00",
      "indicators": {
        "ema20": "64920.40",
        "ema50": "64560.00",
        "ema200": "62150.00",
        "rsi14": "61.10",
        "atr14": "205.20",
        "atr_pct": "0.314992",
        "vwap": "65080.00",
        "vol_sma20": "382.00",
        "adx14": "27.30",
        "plus_di14": "28.40",
        "minus_di14": "16.90",
        "di_side": "1",
        "obv": "1538000.00",
        "mfi14": "62.40",
        "cvd_delta": "24.90",
        "ema50_slope_pct": "0.061957",
        "bb_mid": "64940.00",
        "bb_upper": "65340.00",
        "bb_lower": "64540.00",
        "bb_width": "0.012320",
        "body": "0.470588",
        "close_pos": "0.764706",
        "candle_q": "0.385000",
        "range_ratio": "0.828460",
        "swing_high": "65180.00",
        "last_swing_high": "65180.00",
        "last_swing_low": "64400.00"
      }
    }
  ]
}
```

| Поле | Описание |
|------|----------|
| `interval` | Запрошенный ТФ: `1m` / mid `5m`/`15m`/`30m` / HTF `1h`/`4h`/`1d`/`1w` (core indicators после прогрева). |
| `from` / `to` | Фактический диапазон выборки (мс). |
| `count` | Число баров в массиве. |
| `bars` | Закрытые свечи по возрастанию времени. Используйте для backfill после disconnect. |

Query: `interval`, `limit`, `from`, `to`.

---

### 7. `GET /v1/context/{symbol}` — главный снимок решения

**Запрос**

```http
GET /v1/context/BTCUSDT
Authorization: Bearer mo_…
```

**Ответ — полный реалистичный пример (сокращены только длинные `htf.indicators`, структура 1-в-1)**

```json
{
  "symbol": "BTCUSDT",
  "market": "spot",
  "interval": "1m",
  "ts": 1784179235000,
  "context_scope": "live_snapshot",
  "historical_safe": false,
  "bar": {
    "ts": 1784179200000,
    "open": "65000.00",
    "high": "65080.00",
    "low": "64950.00",
    "close": "65040.00",
    "volume": "123.45",
    "quote_volume": "8023456.78",
    "trades": 4120,
    "closed": true,
    "taker_buy_volume": "60.10",
    "indicators": {
      "ema20": "64910.12",
      "ema50": "64500.00",
      "ema200": "62000.00",
      "rsi14": "54.20",
      "atr14": "180.50",
      "atr50": "210.00",
      "atr_ratio_14_50": "0.859524",
      "atr_pct": "0.277521",
      "vwap": "64980.00",
      "vol_sma20": "95.00",
      "volume_usd": "8023456.78",
      "volume_rate": "8023456.78",
      "adx14": "22.10",
      "plus_di14": "24.50",
      "minus_di14": "17.80",
      "di_side": "1",
      "obv": "1500000.00",
      "mfi14": "48.00",
      "cvd_delta": "-3.25",
      "ema50_slope_pct": "0.031000",
      "ema_slope_abs": "0.031000",
      "bb_mid": "64900.00",
      "bb_upper": "65200.00",
      "bb_lower": "64600.00",
      "bb_width": "0.009244",
      "body": "0.307692",
      "wick_balance": "0.153846",
      "close_pos": "0.692308",
      "candle_q": "0.261538",
      "range_ratio": "0.720222",
      "last_swing_high": "65200.00",
      "last_swing_low": "64400.00"
    }
  },
  "ticker": {
    "ts": 1784179235000,
    "last": "65042.10",
    "bid": "65042.00",
    "ask": "65042.20",
    "open_24h": "64200.00",
    "high_24h": "65300.00",
    "low_24h": "63800.00",
    "volume_24h": "28000.50",
    "quote_volume_24h": "1800000000.00",
    "price_change_pct_24h": "1.25",
    "weighted_avg_24h": "64890.00"
  },
  "depth": {
    "ts": 1784179230000,
    "best_bid": "65042.00",
    "best_ask": "65042.20",
    "spread_pct": "0.000307",
    "imbalance": "0.55",
    "bid_wall": { "price": "65000.00", "qty": "12.50", "notional": "812500.00", "distance_pct": "0.064615" },
    "ask_wall": { "price": "65100.00", "qty": "8.00", "notional": "520800.00", "distance_pct": "0.088942" }
  },
  "bar_count": 30240,
  "ready": true,
  "history": "full_day",
  "lag_sec": 75,
  "indicators_ready": true,
  "data_quality": { "score": 92, "flags": [] },
  "freshness": {
    "ticker": { "available": true, "fresh": true, "age_sec": 1, "max_age_sec": 30 },
    "depth": { "available": true, "fresh": true, "age_sec": 1, "max_age_sec": 10 },
    "macro_snapshot": { "available": true, "fresh": true, "age_sec": 900, "max_age_sec": 7200 },
    "market_regime": { "available": true, "fresh": true, "age_sec": 900, "max_age_sec": 7200 }
  },
  "macro_event_soon": false,
  "next_macro_event": { "ts": 1784246400000, "title": "CPI m/m", "country": "USD", "impact": "high" },
  "derivatives": {
    "snapshot": {
      "ts": 1784179230000,
      "mark_price": "65045.10",
      "index_price": "65040.00",
      "funding_rate": "0.000100",
      "next_funding_ts": 1784196000000,
      "open_interest": "85000.50"
    },
    "funding_7d": { "samples": 21, "funding_ma_7d": "0.000080", "funding_std_7d": "0.000050" },
    "stale": false
  },
  "liquidations": {
    "windows": {
      "five_min": { "count": 3, "total_long_qty": "1.80", "total_short_qty": "0.60", "total_long_value": "120000.00", "total_short_value": "40000.00" },
      "one_hour": { "count": 40, "total_long_qty": "14.00", "total_short_qty": "8.00", "total_long_value": "900000.00", "total_short_value": "500000.00" }
    }
  },
  "macro_snapshot": {
    "ts": 1784179100000,
    "fear_greed": { "ts": 1784073600000, "value": 25, "classification": "Extreme Fear" },
    "btc_dominance_pct": "56.27",
    "eth_dominance_pct": "12.10",
    "total_market_cap_usd": "2400000000000.00",
    "total_volume_24h_usd": "90000000000.00",
    "stablecoin_total_usd": "300000000000.00",
    "usd_index": "120.50",
    "usd_index_ts": 1783987200000,
    "us_10y_yield_pct": "4.62",
    "us_10y_ts": 1783987200000
  },
  "cross_exchange": {
    "symbol": "BTCUSDT",
    "ts": 1784179235000,
    "consensus_mid": "65052.77",
    "fresh_venues": 3,
    "divergence_warning": false,
    "venues": [
      { "id": "v1", "mid": "65052.76", "divergence_bps": "1.20", "stale": false, "age_ms": 300 }
    ]
  },
  "market_regime": {
    "volatility_regime": "low_vol_range",
    "correlation_btc_7d": 1.0,
    "breadth_above_ema20_pct": 80.0,
    "median_return_24h_pct": 1.20
  },
  "volatility_regime": "low_vol_range",
  "decoupling_detected": false,
  "trade_cost": {
    "source": "preset",
    "fee_buy": "0.001",
    "fee_sell": "0.001",
    "spread_bps": "2.0",
    "spread_from_depth": true,
    "slip_bps": "2.0",
    "half_spread": "0.0001",
    "c_spot": "0.0025",
    "funding_rate": "0.0001",
    "c_perp": "0.0026"
  },
  "cost_risk": {
    "atr_pct": "0.28",
    "d_sl_pct": "0.56",
    "c_r": "0.446429",
    "p_be": "0.482143",
    "tradable": true,
    "params": { "k_sl": "2.0", "rr": "2.0", "tradable_c_r_max": "0.45" }
  },
  "htf": {
    "m15": { "ts": 1784178300000, "open": "…", "close": "…", "closed": true, "indicators": { "ema20": "…", "adx14": "…" } },
    "h1":  { },
    "h4":  { },
    "d1":  { }
  },
  "failover": false
}
```

Поле `current_bar` в текущем API **отсутствует** (planned). Когда появится — незакрытая bar 0 для симуляции fill / аварийного выхода; ⚠️ **не для сигналов**.

Свои комиссии и порог риска (query):  
`GET /v1/context/BTCUSDT?fee_buy=0.001&fee_sell=0.001&slippage=0.0002&k_sl=2&rr=2&tradable_c_r_max=0.45`

| Поле | Описание |
|------|----------|
| `context_scope` / `historical_safe` | Всегда `live_snapshot` / `false`. Весь ответ — текущий point-in-time snapshot; не присоединять его к историческим барам. |
| `bar` / `ticker` | Как в `/v1/latest`: сигнал только из `bar`, исполнение/UI из `ticker`. На баре также `body`/`wick_balance`/`close_pos`/`candle_q`/`range_ratio`/`atr_pct`. |
| `depth` | Производные метрики стакана (не сырой book): спред, imbalance 0..1, ближайшие «стены». Нужны для оценки fill/slippage. |
| `trade_cost` | Единый round-trip `c_spot` из preset fees + half_spread + 2×slip (+ optional `c_perp`). **Не в WS.** |
| `cost_risk` | `c_r = C / (k_sl·ATR/close)`, `p_be`, `tradable`. Главный gate «слишком дорого для TF». **Не в WS.** |
| `bar_count` / `ready` / `history` / `lag_sec` / `indicators_ready` | Готовность пары. `ready` (≥200) ≠ `indicators_ready` (ema20+rsi+atr). `lag_sec` — возраст closed bar от open ts (см. выше). |
| `data_quality` | Phase 11: скор 0–100 + `flags`. Для нового решения обязательно `score >= 70`; `lag_sec > 120` ограничивает score ниже 70. |
| `freshness` | По каждому live-компоненту: `available`, `fresh`, `age_sec`, `max_age_sec`. Проверять до использования ticker/depth/macro/regime. |
| `volatility_regime` | Phase 11: label режима волатильности для этой пары (не entry). |
| `decoupling_detected` | Phase 11: раскорреляция с BTC за ~24h vs 7d. |
| `macro_event_soon` | `true`, если high-impact событие ≤60 мин — лучше пауза или уменьшение риска. |
| `next_macro_event` | Ближайшее событие календаря или `null`. |
| `derivatives` / `liquidations` | Фьючерсный контекст (funding/OI/liqs) — только как фильтр качества spot-сигнала. |
| `macro_snapshot` | Глобальный макро. Все `ts`/`*_ts` — **мс UTC**; серверного `stale` нет — считайте age от `macro_snapshot.ts`. |
| `cross_exchange` | Consensus mid, stale, divergence — проверка bad tick. |
| `market_regime` | Корреляция к BTC, vol, breadth + per-symbol regime fields. Не заменяет HTF `trend_z` / RS-rank. |
| `failover` | Резервный режим. При `true` не суммировать CVD. |

Подробные формулы и decision path — раздел **«Edge: trade_cost и cost_risk»** выше.

Вложенные объекты совпадают с ответами `/v1/derivatives`, `/v1/macro`, `/v1/quotes`, `/v1/market-regime`.

---

### 8. `GET /v1/derivatives/{symbol}`

**Запрос**

```http
GET /v1/derivatives/BTCUSDT?liquidation_limit=5
Authorization: Bearer mo_…
```

**Ответ (схема)**

```json
{
  "symbol": "BTCUSDT",
  "market": "futures",
  "ts": 1784179230000,
  "derivatives": {
    "snapshot": {
      "ts": 1784179230000,
      "mark_price": "65045.1",
      "index_price": "65040.0",
      "funding_rate": "0.0001",
      "next_funding_ts": 1784196000000,
      "open_interest": "85000.5"
    },
    "funding_7d": {
      "samples": 21,
      "funding_ma_7d": "0.00008",
      "funding_std_7d": "0.00005"
    },
    "stale": false
  },
  "liquidations": {
    "windows": {
      "five_min": {
        "count": 3,
        "total_long_qty": "1.8",
        "total_short_qty": "0.6",
        "total_long_value": "120000",
        "total_short_value": "40000"
      },
      "one_hour": {
        "count": 40,
        "total_long_qty": "14.0",
        "total_short_qty": "8.0",
        "total_long_value": "900000",
        "total_short_value": "500000"
      }
    },
    "recent": [
      { "ts": 1784179205000, "side": "long", "qty": "1.2", "price": "65010", "total_value": "78012" }
    ]
  }
}
```

| Поле | Описание |
|------|----------|
| `derivatives.snapshot` | Текущие mark/index, predicted funding, OI. |
| `funding_7d` | MA/STD по settled funding за ~7 дней — перегретый funding = осторожнее с лонгами/шортами. |
| `stale` | Mark-поток устарел (>~10 мин) — не доверяйте snapshot. |
| `liquidations.windows` | Агрегаты forced liq за 5м/1ч: `count`, `total_long_qty` / `total_short_qty`, `total_long_value` / `total_short_value`. |
| `liquidations.recent[].side` | Уже нормализовано: `long` = ликвидирован лонг, `short` = шорт. |

---

### 9. `GET /v1/calendar`

**Запрос**

```http
GET /v1/calendar?hours=48&high_only=true
Authorization: Bearer mo_…

# Бэктест / replay: события в окне после bar.ts
GET /v1/calendar?as_of=1700000000000&window_min=60&high_only=true
```

`as_of` — якорь (мс); по умолчанию `now`. `window_min` перекрывает `hours`. Ответ включает `as_of` и `historical_safe=true` когда якорь задан явно. Календарь накапливается (~90 дней), а не затирается weekly feed.

**Ответ**

```json
{
  "now": 1784179200000,
  "as_of": 1784179200000,
  "to": 1784352000000,
  "count": 2,
  "historical_safe": false,
  "events": [
    {
      "ts": 1784246400000,
      "title": "CPI m/m",
      "country": "USD",
      "impact": "high",
      "forecast": "0.2%",
      "previous": "0.1%"
    }
  ]
}
```

При `?as_of=` якорь копируется в `as_of`, `historical_safe` становится `true`. Окно: `[as_of, as_of + hours|window_min]`.

| Поле | Описание |
|------|----------|
| `now` / `as_of` / `to` | Якорь и конец окна (мс). Без `as_of` в query — `as_of == now`. |
| `historical_safe` | `true` только если клиент явно передал `as_of` (replay). |
| `events[].impact` | `low` / `medium` / `high`. High + близкий `ts` → снижать риск. |
| `forecast` / `previous` | Ожидание и прошлое значение (строки, могут быть пустыми). |

---

### 10. `GET /v1/macro`

**Запрос**

```http
GET /v1/macro?fng_days=3
Authorization: Bearer mo_…
```

**Ответ**

```json
{
  "ts": 1784179200000,
  "snapshot": {
    "ts": 1784179100000,
    "fear_greed": { "ts": 1784073600000, "value": 25, "classification": "Extreme Fear" },
    "btc_dominance_pct": "56.27",
    "eth_dominance_pct": "12.10",
    "total_market_cap_usd": "2.4e12",
    "total_volume_24h_usd": "9.0e10",
    "stablecoin_total_usd": "3.0e11",
    "usdt_supply_usd": "1.1e11",
    "usdc_supply_usd": "3.4e10",
    "usd_index": "120.50",
    "usd_index_ts": 1783987200000,
    "us_10y_yield_pct": "4.62",
    "us_10y_ts": 1783987200000
  },
  "fear_greed_history": [
    { "ts": 1783987200000, "value": 31, "classification": "Fear" }
  ]
}
```

| Поле | Описание |
|------|----------|
| `fear_greed.value` | 0..100; экстремумы (<20 / >80) — зоны возможного разворота настроений, не сигнал сами по себе. |
| `btc_dominance_pct` | Рост → капитал в BTC из альтов; лонги альтов рискованнее. |
| `stablecoin_*` | Предложение стейблов / USDT / USDC — косвенный «сухой порох» рынка. |
| `usd_index` / `us_10y_yield_pct` | Укрепление USD / рост доходностей = risk-off фон для крипты. Дневная гранулярность, не live-тик. |
| `usd_index_ts` / `us_10y_ts` / `snapshot.ts` | Все **мс UTC**. Это момент обновления Oracle (дневная гранулярность источника ≠ live tick). |
| `fear_greed_history` | Появляется при `fng_days>0` (до 90). |
| (нет `stale`) | Серверного флага нет; клиент: `ageSec = (now - snapshot.ts) / 1000`. |

---

### 11. `GET /v1/quotes/{symbol}`

**Запрос**

```http
GET /v1/quotes/BTCUSDT
Authorization: Bearer mo_…
```

**Ответ**

```json
{
  "symbol": "BTCUSDT",
  "ts": 1784179230000,
  "consensus_mid": "65052.765",
  "fresh_venues": 3,
  "divergence_warning": false,
  "venues": [
    {
      "id": "v1",
      "quote": { "bid": "65052.76", "ask": "65052.77", "source_ts": 1784179229000, "received_ts": 1784179230000 },
      "mid": "65052.765",
      "divergence_bps": "0.00",
      "age_ms": 300,
      "stale": false,
      "connected": true
    }
  ]
}
```

Состав `venues` смотрите в ответе, не хардкодьте. Торговать только по `stale=false`; при `divergence_warning=true` — пауза.

| Поле | Описание |
|------|----------|
| `consensus_mid` | Median mid свежих элементов `venues[]`. Опора для проверки, не торговый сигнал. |
| `fresh_venues` | Сколько venue не stale. Мало свежих → данные хрупкие. |
| `divergence_warning` | Кто-то сильно ушёл от consensus. Не открывайте сделку без проверки. |
| `venues[].stale` / `age_ms` | Устарел ли BBO и насколько. |
| `venues[].divergence_bps` | Отклонение mid этого `id` от consensus в базисных пунктах (на корне ответа поля нет). |
| `venues[].id` | Непрозрачный id (`v1`…). |
| `venues[].connected` | Состояние feed, если известно. |

---

### 12. `GET /v1/market-regime`

**Запрос**

```http
GET /v1/market-regime
Authorization: Bearer mo_…
```

**Ответ — полный пример (в реальном ответе `symbols` содержит все активные пары)**

```json
{
  "ts": 1784179200000,
  "interval": "1h",
  "lookback_days": 7,
  "benchmark": "BTCUSDT",
  "symbol_count": 5,
  "breadth_above_ema20_pct": 80.0,
  "breadth_above_ema50_pct": 60.0,
  "median_return_1h_pct": -0.05,
  "median_return_24h_pct": 1.2,
  "decoupling_count": 1,
  "symbols": [
    {
      "symbol": "BTCUSDT",
      "correlation_btc_7d": 1.0,
      "correlation_btc_24h": 1.0,
      "realized_volatility_24h_pct": 38.5,
      "realized_volatility_7d_pct": 42.0,
      "volatility_regime": "low_vol_range",
      "decoupling_detected": false,
      "return_1h_pct": -0.05,
      "return_24h_pct": 1.25,
      "above_ema20": true,
      "above_ema50": true
    },
    {
      "symbol": "ETHUSDT",
      "correlation_btc_7d": 0.85,
      "correlation_btc_24h": 0.40,
      "realized_volatility_24h_pct": 45.0,
      "realized_volatility_7d_pct": 48.0,
      "volatility_regime": "low_vol_range",
      "decoupling_detected": true,
      "return_1h_pct": -0.1,
      "return_24h_pct": 2.5,
      "above_ema20": true,
      "above_ema50": true
    }
  ]
}
```

| Поле | Описание |
|------|----------|
| `breadth_above_ema*` | Доля рынка выше своих EMA — «широкий» риск-on/off. |
| `median_return_*` | Медиана доходностей по символам Oracle за 1h/24h. |
| `decoupling_count` | Сколько пар сейчас с `decoupling_detected`. |
| `symbols[].correlation_btc_7d` | Связь с BTC по 1h returns (~7d); ближе к 1 — альт ходит вместе с BTC. |
| `symbols[].correlation_btc_24h` | Та же метрика на ~24 последних aligned returns. |
| `symbols[].decoupling_detected` | Short corr упал vs 7d — момент самостоятельной динамики альта. |
| `symbols[].volatility_regime` | `low_vol_range` / `breakout_expansion` / `high_vol_mean_reversion` / `crisis` — выбор режима стратегии. |
| `realized_volatility_*_pct` | Annualized realized vol; высокий = урезать размер позиции. |

---

### 13. WebSocket `/v1/stream`

**Подключение**

```text
WS /v1/stream
Authorization: Bearer mo_…
```

или `?token=mo_…`

**Сервер → клиент после connect**

```json
{ "type": "hello", "version": "1.0.0" }
```

**Клиент → сервер**

```json
{ "op": "subscribe", "symbols": ["BTCUSDT"], "microstructure": true }
```

**Событие закрытия бара**

```json
{
  "type": "bar_close",
  "symbol": "BTCUSDT",
  "interval": "1m",
  "bar": {
    "ts": 1784179200000,
    "open": "65000",
    "high": "65080",
    "low": "64950",
    "close": "65040",
    "volume": "120",
    "quote_volume": "7800000",
    "trades": 4000,
    "closed": true,
    "indicators": {
      "ema20": "64910",
      "rsi14": "54.2",
      "atr14": "180.5",
      "candle_q": "0.26",
      "atr_pct": "0.28"
    }
  }
}
```

| Поле | Описание |
|------|----------|
| `type` | Всегда `bar_close` для торгового триггера. |
| `symbol` / `interval` | Какая пара и ТФ закрылись. |
| `bar` | Полный закрытый бар (+ indicators на 1m, включая форму свечи). Дедуп `` `${symbol}:${bar.ts}` ``. **Без** `trade_cost` / `cost_risk`. |

При opt-in подписке Oracle дополнительно отправляет `microstructure_close` после финализации минутного объекта. Объединяйте его с баром по `symbol + ts`; REST `/v1/microstructure/{symbol}` является источником восстановления пропусков.

После `bar_close` обновляют локальный стейт; **`GET /v1/context/{symbol}`** — по расписанию **60–90 с** и **обязательно перед входом** (там `trade_cost` / `cost_risk` / freshness). Live `depth` в context и закрытая минутная microstructure — разные сущности.

Reconnect: initial **1s**, multiplier **2×**, max **60s**, jitter **±25%**. После reconnect — **serial** backfill (`history` / `bootstrap`) с паузами **≥ min_interval_ms**.

---

### 14. Ошибки: `key_expired` и `429`

Сначала проверяйте **HTTP** (`res.ok` / `res.status`), затем JSON **`code`** (на ошибках равен `status`). Не смешивайте HTTP 401 с полем `body.status`.

**Истёкший ключ (любой REST)**

```http
GET /v1/latest?symbols=BTCUSDT
Authorization: Bearer mo_expired…
```

HTTP `401` + тело:

```json
{
  "status": "key_expired",
  "code": "key_expired",
  "error": "API key expired",
  "message": "API key expired",
  "key_id": "key_bf6ed463bc8d112f",
  "expires_at": "2026-06-01T00:00:00+00:00"
}
```

| Поле | Описание |
|------|----------|
| `status` / `code` | Машинный код в **теле** (не HTTP status). Остановите торговлю, запросите продление у оператора. |
| `key_id` | Какой ключ истёк. |
| `expires_at` | Когда именно закончился срок (ISO-8601 UTC в теле ошибки). |

Аналогично: `key_revoked`, `key_invalid`, `key_missing`, `unavailable` (HTTP 503).

**Превышен лимит** — HTTP `429`:

```json
{
  "status": "quota_exceeded",
  "code": "quota_exceeded",
  "error": "daily REST request quota exceeded",
  "message": "daily REST request quota exceeded",
  "retry_after_ms": 3600000
}
```

| Поле | Описание |
|------|----------|
| `status` / `code` | `too_fast` / `rate_limited` / `quota_exceeded` / `ws_limit`. `quota_exceeded` — кончились дневные **платные REST-запросы** (1 HTTP = 1), не веса endpoint’ов. Для `ws_limit` на WS upgrade поле `status` может отсутствовать — ориентируйтесь на `code`. |
| `retry_after_ms` | Рекомендуемая пауза до повтора (мс). Добавляйте jitter; см. backoff выше. |

Также смотрите раздел **«Заголовки лимитов»** (`X-Quota-*` на 200, `Retry-After` / `retry_after_ms` на 429).