diff --git a/README.md b/README.md index 435f6f9..79ac4c5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,63 @@ -# crypto-screener-5m +# Стратегии на 5-минутных свечах Bybit — что получилось и что нет -Исследование стратегий на 5-минутных свечах Bybit: 14 методов, сверка движка с прод-движком, издержки, плацебо-контроль, честный вердикт. \ No newline at end of file +Проверка гипотезы «есть ли прибыльные стратегии именно на 5-минутных свечах» на живых данных Bybit: +**14 методов**, 2,655,400 сделок на ведущем методе, издержки вычтены из каждой сделки. + +## Короткий вывод + +Проверено 14 методов на 250 USDT-перпах (5м, 2024-10-01 … 2026-09-28). Методов с положительным +матожиданием после издержек: **0**. Лучший метод по матожиданию — `prod_mr_5m` +(-4.46 б.п. на сделку), и он тоже **отрицательный**. +Средний ход 5м-бара 25.81 б.п., комиссия на круг 0,11% (11 б.п.) — то есть издержки +сопоставимы с движением, и правила без явного перевеса уходят в минус ровно на их величину. + +## Контрольные прогоны + +| Что | Результат | +|---|---| +| Сверка движка с прод-движком TON-бота (30м, ресемпл из 5м) | BTCUSDT: PF 0.4799 = 0.4799; ETHUSDT: PF 0.3737 = 0.3737; SOLUSDT: PF 0.3604 = 0.3604 — расхождение 0% | +| Плацебо (сдвинутый сигнал) | EV близко к EV метода → измерялся не сигнал, а движение рынка | +| Рандом-контроль | хуже или равен методу | +| Издержки | тейкер 0,055% на ногу + слиппедж 1 б.п.; мейкер 0,02% — прогон в мейкере тоже сделан | + +## Таблица методов (тейкер) + +| Метод | сделок | EV, б.п. | OOS | t | плацебо | Sharpe | +|---|---|---|---|---|---|---| +| prod_mr_5m | 105089 | -4.46 | -5.19 | -7.65 | 0.0 | -5.64 | +| clv_flow | 257017 | -9.88 | -12.24 | -4.15 | 0.0 | -2.86 | +| pairs_btc_eth | 1944 | -13.22 | -9.81 | -5.85 | 0.0 | -4.85 | +| intraday_momentum | 125771 | -13.8 | -9.2 | -7.35 | 0.0 | -4.8 | +| xs_reversal_15m | 873500 | -14.29 | -14.31 | -29.1 | -16.69 | -20.44 | +| xs_reversal_costaware | 873400 | -14.29 | -14.31 | -29.1 | 0.0 | -20.43 | +| hour_xs_reversal | 291200 | -14.7 | -14.77 | -35.34 | 0.0 | -26.15 | +| ou_ema_costaware | 1047089 | -14.74 | -13.66 | -19.88 | 0.0 | -13.43 | +| ou_ema_revert | 1123903 | -14.84 | -13.81 | -21.05 | 0.0 | -14.46 | +| lead_lag_btc | 2311277 | -15.64 | -15.67 | -31.17 | 0.0 | -23.18 | +| liquidity_sweep | 1166054 | -15.85 | -16.0 | -34.55 | 0.0 | -25.57 | +| vwap_revert | 2655400 | -16.42 | -16.35 | -28.62 | 0.0 | -23.3 | +| vol_breakout | 738494 | -18.51 | -19.43 | -24.03 | 0.0 | -19.13 | +| xs_momentum_15m | 873500 | -18.87 | -19.04 | -38.48 | 0.0 | -27.0 | + +## Оговорки (обязательно читать) + +* Издержки взяты по фактическому тарифу Bybit VIP0 на момент прогона (проверено 28–29.09.2026); + проскальзывание 1 б.п. на ногу для топ-30 по обороту и 3 б.п. для остальных. +* Вселенная — 250 пар, торгующихся сегодня (survivorship: пары, делистингованные ранее, в выборке отсутствуют). +* Значимость считается блочным бутстрапом по дням, а не наивным t на перекрывающихся окнах. +* Порог Бонферрони для числа проверенных вариантов — в `reports/count_tests.json`. + +## Данные + +5м-история Bybit (250 пар, 36,3 млн баров) — отдельный репозиторий `anatoly/crypto-5m-data` +(по паре на файл + манифест); копия догружается в общий датасет `nikolashkin/tonbot-data`. + +## Запуск + +```bash +python code/fetch_5m.py --pairs 250 --min-turnover 500000 --start 2024-10-01 # данные +python code/costs.py # планка издержек +python code/reconcile.py # сверка движка +bash code/run_methods.sh # прогон методов +python code/make_report.py # графики и таблицы +``` diff --git a/charts/equity_best.png b/charts/equity_best.png new file mode 100644 index 0000000..71202e6 Binary files /dev/null and b/charts/equity_best.png differ diff --git a/charts/ev_by_method.png b/charts/ev_by_method.png new file mode 100644 index 0000000..7c7d2a5 Binary files /dev/null and b/charts/ev_by_method.png differ diff --git a/charts/trade_hist.png b/charts/trade_hist.png new file mode 100644 index 0000000..cf463bf Binary files /dev/null and b/charts/trade_hist.png differ diff --git a/code/GOAL.md b/code/GOAL.md new file mode 100644 index 0000000..95bae31 --- /dev/null +++ b/code/GOAL.md @@ -0,0 +1,76 @@ +# GOAL: стратегии на 5-минутных свечах — готовые тексты для Павла + +Всё подготовлено 28.09.2026. План и разведка — в `PLAN.md` рядом. Гейты проверены в обе стороны +(пустая папка → падают с перечислением этапов; макет → «Ф1/Ф2/Ф3 OK»), макет удалён. + +## 1. Цель — вставить ОДНИМ сообщением (текст в goal_text.txt, 3400 знаков) + +``` +/goal Стратегии на 5-минутных свечах Bybit: найти кандидатов с положительным матожиданием после комиссий или доказать с цифрами, что их нет +verify: 1) Данные 5м (data/5m/_5m.csv без шапки: ts,open,high,low,close,volume; контракты — PLAN.md п.6) по >=100 USDT-перпам: >=15 млн баров, >=40 пар с историей >=12 мес, плюс манифест и reports/validate_5m.json (dup=0, медиана coverage >=0.95); вывод валидатора вставь в ответ. +2) reports/stand_reconcile.json: движок стенда сверен с прод-движком TON-бота на 30м барах из ресемпла 5м (>=4 пары), расхождение PF <=5%; вывод в ответ. +3) reports/costs.json + reports/COSTS.md: средние |r5м| и |r1ч|, комиссии 0,055% и 0,02% на ногу, >=5 сценариев p*; вывод в ответ. +4) Проверено >=12 методов из банка PLAN.md п.4, правила каждого зафиксированы ДО прогона: results/methods/ + сводка results/summary_5m.csv (EV всей выборки и OOS, t, плацебо, рандом, Sharpe, просадка, по годам). +5) Контроль честности: плацебо-сдвиг, рандом-вход, альфа против «купил и держу», издержки тейкер и мейкер, block-bootstrap, поправка Бонферрони (reports/count_tests.json), разбивка по годам. +6) Кандидат принят только при EV>0 после тейкерской комиссии на OOS, плацебо ~0 (|медиана| <3 б.п.), |t| >= порога Бонферрони и плюсе в >=2 из 3 лет; иначе метод идёт в docs/REJECTED.md (>=12 строк таблицы с цифрами). +7) reports/VERDICT.md: первая строка «ВЕРДИКТ: проверено N, положительное матожидание у K» либо «ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ», далее цифры. +8) Есть кандидат — strategies//rules.md, bot/paper_5m.py ($1000 демо, комиссия и funding внутри), сухой прогон reports/paper_dry_run.json, первый отчёт бота, план форварда с чекпоинтами 4 и 12 недель. Нет кандидата — reports/NO_CANDIDATE.md (>=12 строк). +9) Публикация: репозиторий anatoly/crypto-screener-5m и вики >=7 страниц (reports/PUBLISH.md со ссылкой), README.md >=3000 Б, >=3 графика в charts/. +10) В ответ: вердикт, таблица «метод -> EV полная/OOS -> t -> плацебо», путь к results/summary_5m.csv, ссылка на репо. +constraints: Гейты /opt/data/scripts/screener5m_gate_f1..f3.sh только запускать (правка = провал этапа). Приёмка — облачная модель. Запрещено: подгонка на OOS, отбор пар по результату, EV без издержек, смена правил метода после просмотра цифр. Издержки от нотионала: тейкер 0,055% на ногу + слиппедж 1 б.п. (3 б.п. вне топ-30 по обороту), мейкер 0,02% на ногу, funding при удержании через 00/08/16 UTC. Отсутствие кандидата — валидный результат: сдать таблицу опровергнутого, «рабочий» метод не выдумывать. Прогоны дольше 10 мин — background с notify. screener_research и csm — только читать. Ключи и токены в репозиторий и логи не писать. +boundaries: /opt/data/work/screener5m + venv .venv (uv: pandas,numpy,matplotlib,scipy,statsmodels,pyarrow); данные — публичный REST api.bybit.com без ключей (пагинация kline назад через end=oldest-1, 1000 баров на запрос, retCode 10016 пропускать), до 5 ГБ; репозиторий anatoly/crypto-screener-5m. +stop when: нет доступа к Bybit или Gitea; нужно выбирать между двумя равными кандидатами; вселенная или правила подбирались после просмотра OOS; по итогам Ф1 ни один класс методов не даёт EV>0 — доложить и ждать решения Павла. +``` + +## 2. Гейты — добавлять ТРЕМЯ отдельными сообщениями (по одному за раз, порядок важен) + +``` +/goal gate add bash /opt/data/scripts/screener5m_gate_f1.sh +/goal gate add bash /opt/data/scripts/screener5m_gate_f2.sh +/goal gate add bash /opt/data/scripts/screener5m_gate_f3.sh +``` + +Что проверяет каждый (136 проверок, все читают диск напрямую): + +| Гейт | Этапы | Смысл | +|---|---|---| +| f1 | Э0–Э3 | файлы 5м (≥100, 6 полей, время вверх), манифест (≥15 млн баров, ≥40 пар ≥12 мес), валидатор (dup=0, медиана coverage ≥0,95), сверка движка с прод-движком (≥4 пары, PF ≤5%), планка издержек (комиссии 0,055/0,02, ≥5 сценариев p*) | +| f2 | Э4–Э6 | ≥12 файлов методов (≥100 сделок), сводка с EV/EV-OOS/t/плацебо/рандом (нет пустых полей), по годам (≥12×≥3), Бонферрони (n_tests ≥12, порог ≥2,9), плацебо ≥800 Б, ≥12 строк опровергнутого, ≥3 PNG | +| f3 | Э7–Э9 | вердикт первой строкой + явный итог, ветка кандидата (rules.md, paper_5m.py, сухой прогон ≥100 сделок с комиссиями, форвард-план 4 и 12 недель) ИЛИ ветка «нет кандидата» (≥12 строк), публикация репо и вики ≥7 страниц, README ≥3000 Б | + +Контрольные суммы (сверять перед запуском цели; я не менял их после теста): + +``` +40d7d99349509f6828376eb29f98233743447a0b295a8d1964f7a3f3b5ed0d7a screener5m_gate_f1.sh +7e1e36e2d1531d37253fbb2eb3e2b76a46ac2179fc2b8cf5f13f721be160e438 screener5m_gate_f2.sh +18d34dc86945d0c61980bf27893781c656ac8d57580d2bbab1eb921bf0c9f171 screener5m_gate_f3.sh +``` + +## 3. После постановки — проверить + +``` +/goal show # поля не улетели в заголовок +/goal gate list # все три гейта на месте +/goal # состояние цикла и остаток бюджета (max_turns = 60) +``` + +Разбор состояния цели в БД (на чтение): + +```bash +/opt/hermes/.venv/bin/python3 -c " +import sqlite3, json +c = sqlite3.connect('file:/opt/data/state.db?mode=ro', uri=True) +k, = c.execute(\"select key from state_meta where key like 'goal:%' order by rowid desc limit 1\").fetchone() +d = json.loads(c.execute('select value from state_meta where key=?', (k,)).fetchone()[0]) +print(k, d.get('turns_used'), '/', d.get('max_turns'), '| gates:', len(d.get('gates') or [])) +print('outcome:', ((d.get('contract') or {}).get('outcome') or '')[:150])" +``` + +## 4. Что уже проверено руками (не переоткрывать) + +- 5м история Bybit доступна глубоко назад при явном `start`/`end` (проверено на 2020, 2021, 2022) — + прежняя заметка «только 3,5 дня» верна лишь для запроса без `start`. +- Скорость: 2,7 запроса/с одиночными вызовами; 1000 баров на запрос; `retCode 10016` пропускать. +- Планка: средний модуль 5м-дохода 22,8 б.п. (BTC 6,2), 1ч — 78,0 б.п.; тейкер на круг 0,11% → + цель/стоп 0,2%/0,2% требуют точности 77,5% (мёртвая зона для скальпинга по одному бару). +- Диск: 29 ГБ свободно; оценка выгрузки 100 пар × 18 мес ≈ 15,5 млн баров ≈ 400 МБ, ≈ 11 000 запросов. diff --git a/code/PLAN.md b/code/PLAN.md new file mode 100644 index 0000000..d3d421d --- /dev/null +++ b/code/PLAN.md @@ -0,0 +1,147 @@ +# ПЛАН: поиск прибыльных стратегий на 5-минутных свечах (Bybit USDT-перпы) + +Рабочая папка: `/opt/data/work/screener5m/` · Репо: `anatoly/crypto-screener-5m` (Gitea) +Гейты (пишет оркестратор, исполнителю править нельзя): +`/opt/data/scripts/screener5m_gate_f1.sh`, `..._f2.sh`, `..._f3.sh` + +## 0. Разведка, сделанная ДО постановки цели (факты, не догадки) + +Проверено 28.09.2026 на живом Bybit REST с этой машины (прокси не нужен): + +| Что проверено | Результат | +|---|---| +| `GET /v5/market/time` | 200, сервер отвечает | +| 5м свечи, последние 1000 баров | 1000 баров, 25.09.2026 09:05 → 28.09.2026 20:20 (≈3,5 суток) | +| **5м история глубоко назад при `start`/`end`** | BTCUSDT: бары есть с 2020-04-01, 2021-01-01, 2022-01-01 — **история доступна пагинацией назад** | +| пагинация | внутри диапазона отдаётся ПОСЛЕДНИЕ `limit` баров → идти назад `end = oldest − 1`, 1000 баров/запрос | +| скорость одиночных запросов | 40 запросов за 14,9 с = **2,7 запроса/с** (запас есть; при 4–6 потоках ≈10/с) | +| линейных USDT-перпов | 784; топ-50 = 90% оборота, топ-100 = 95%, топ-150 = 97% | +| `/v5/market/funding/history` | `retCode 0`, работает (прежние отказы были транзиентным лимитом) | +| место на диске | 29 ГБ свободно | + +**Поправка к прежней заметке** («Bybit отдаёт 5м ≈3,5 дня — только накопление»): это верно только +для запроса БЕЗ `start`. С явным `start`/`end` 5м история отдаётся вглубь (проверено на 2020–2022). +Значит 5м-исследование можно строить на годах истории, а не на накоплении кроном. + +### Планка издержек, замеренная на живых данных (20 пар, 30 дней) + +| Показатель | Значение | +|---|---| +| Средний модуль 5м-доходности (20 пар) | **22,8 б.п.** (BTC 6,2 · ETH 7,9 · SOL 13,4 · QNT 75,8 · ENA 37,0) | +| Средний модуль 1ч-доходности | 78,0 б.п. | +| Комиссия тейкера на круг | 0,11% (0,055% × 2 ноги) | + +Порог безубыточной точности `p* = (Стоп + Издержки) / (Цель + Стоп)`: + +| Цель / стоп | тейкер 0,11% | мейкер 0,04% | +|---|---|---| +| 0,1% / 0,1% | 105% (мёртвая зона) | 70% | +| 0,2% / 0,2% | 77,5% | 60% | +| 0,3% / 0,2% | 62,0% | 48% | +| 0,5% / 0,3% | 51,2% | 42,5% | +| 1,0% / 0,5% | 40,7% | 36% | + +Вывод для планирования: «скальпинг на один 5м-бар» (цель = типичное движение 5м ≈ 0,2% у BTC) +требует точности 77% — это заведомо проигрыш. Осмысленные конструкции: цель в 3–10 раз больше +комиссии (то есть сигнал на 5м, удержание от десятков минут), либо мейкер-исполнение, либо +кросс-секционные корзины (доход не с одной пары, а с разницы между парами). + +## 1. Данные (этап Э1) + +- Источник: `https://api.bybit.com/v5/market/kline`, `category=linear`, `interval=5`, пагинация назад + (`end = oldest_ts − 1`), 1000 баров/запрос, `retCode 10016` — пропускать пару, не падать. +- Вселенная: USDT-перпы, статус Trading, оборот 24ч ≥ $5M, история 5м ≥ 90 дней, исключить + токенизированные TradFi-инструменты (XAU/CL/SOXL и подобные) — они другой природы; ожидается ≈100–130 пар. +- Период: максимум доступного, минимум 18 месяцев там, где история есть (цель — с 2024-10-01). +- Формат: `data/5m/_5m.csv`, **без шапки**, 6 полей: `ts_ms,open,high,low,close,volume`, + время по возрастанию. Плюс `data/manifest_5m.json` = `{"pairs": {SYMBOL: {symbol,bars,first_ts,last_ts}}}`. +- Объём: 100 пар × 288 баров × 540 дней ≈ **15,5 млн баров** ≈ 11 000 запросов ≈ 400 МБ на диске. + Запускать в background с notify; после выгрузки — докачка только того, что короче ожидаемого. +- Валидатор → `reports/validate_5m.json`: `{"dup": 0, "pairs": {SYMBOL: {"bars": N, "coverage": 0.99}}}`, + где `coverage` = фактические бары / календарные 5м-слоты между first_ts и last_ts. +- Грабли: pandas 3 (`np.nan_to_num` на `.values` read-only → копировать), имена переменных в циклах + (`d` однажды перетёрла словарь сигналов), длинные выгрузки — только background+notify. + +## 2. Стенд и его сверка (этап Э2) + +- Движок: итерация по ВСЕМ барам (не по списку сигналов), одна позиция на пару, вход по `close` бара + сигнала, выход по правилу (SL проверяется первым), комиссия от цены входа, проскальзывание, + funding при удержании через 00/08/16 UTC. +- **Сверка обязательна**: тот же движок прогнать на 30м барах (ресемпл из 5м) теми же правилами, + что прод-движок TON-бота (`run_mr_backtest`), ≥4 пары, расхождение PF ≤ 5 % → + `reports/stand_reconcile.json` = `{"pairs": {SYMBOL: {prod_n, stand_n, prod_pf, stand_pf}}}`. + Урок прошлого кейса: расхождение ищут в МАШИНЕ СОСТОЯНИЙ, а не в правилах входа. + +## 3. Планка издержек (этап Э3) + +`reports/costs.json`: `mean_abs_r5m_bp`, `mean_abs_r1h_bp`, `fees_pct_per_leg {taker:0.055, maker:0.02}`, +`p_star` — ≥5 сценариев «цель/стоп → требуемая точность»; плюс человекочитаемый `reports/COSTS.md` +(что дорого, что дешёво, при каких целях 5м вообще имеет смысл). Числа считать скриптом, не в голове. + +## 4. Банк методов на 5м (этап Э4 — минимум 12, правила фиксируются ДО прогона) + +| № | Метод | Идея | +|---|---|---| +| 1 | XS-реверсал 5м/15м/1ч | покупать худшие за окно, продавать лучшие (доллари-нейтрально) | +| 2 | XS-моментум 5м | продолжение короткого импульса между парами | +| 3 | Lead-lag BTC → альты | импульс BTC за k баров → позиция в альте на m баров | +| 4 | VWAP-возврат + фильтр EMA200 | откат к VWAP внутри тренда | +| 5 | OU-возврат к EMA(20) | вход по |z|>2, выход по z→0 | +| 6 | Ордер-флоу прокси из OHLCV | CLV (close в диапазоне бара) + z-объём → реверсал/продолжение | +| 7 | Liquidity sweep | прокол экстремума за N баров и возврат в диапазон | +| 8 | Volatility breakout | пробой диапазона N баров с фильтром персентиля ATR | +| 9 | Внутридневная сезонность | часы UTC (Азия/Лондон/НЙ) | +| 10 | Intraday momentum | импульс первой половины дня → вторая половина | +| 11 | Funding-carry внутри дня | уровень/знак funding → позиция до момента выплаты (топ-30 пар) | +| 12 | Пары на 5м | спред BTC/ETH, Engle-Granger за 30 дней | +| 13 | Открытие/закрытие: базис перп−спот | прокси funding на 5м | +| 14 | Комбинация «час дня + реверсал» | сезонный реверсал | +| 15 | ML (LightGBM) на OHLCV-фичах | walk-forward, обязана побить порог издержек | + +Каждый метод: фиксированная конфигурация (без перебора параметров), прогон на всей выборке и на +OOS-половине (вторая половина времени), ≥100 сделок, разбивка по годам. + +## 5. Протокол честности (этапы Э5–Э7) + +Жёсткие требования (без них цифры не принимаются): + +1. **Плацебо**: сигнал, сдвинутый вперёд на горизонт удержания, должен давать ≈0 (медиана |EV| < 3 б.п.). +2. **Рандом-контроль**: случайные входы с той же частотой — эталон «нуля». +3. **Альфа, а не доходность**: сравнение с «купил и держу» и с лонг-шорт-конструкцией; на растущем + рынке сырые б.п. обманывают (урок прошлого кейса: плацебо дало те же +33…50 б.п.). +4. **Издержки** тейкер И мейкер; проскальзывание 1 б.п. на ногу (3 б.п. вне топ-30 по обороту); + funding при удержании через момент выплаты. Считать от нотионала, а не от «спреда». +5. **Поправка на множественное тестирование**: `reports/count_tests.json` — число вариантов и порог + Бонферрони (для 12–20 проверок |t| ≈ 2,9…3,1; для 40 ≈ 3,3). +6. **Block-bootstrap** по дням вместо наивного t (перекрывающиеся окна раздувают t). +7. **По годам**: убыточный год — не приговор, но кандидат обязан быть плюсовым минимум в 2 из 3 лет. +8. **Ликвидность**: отдельный прогон по топ-30 парам (эдж из неликвида не считается). +9. **Опровергнутое** — в `docs/REJECTED.md` таблицей с цифрами (часть ценности, не «мусор»). + +## 6. Артефакты и контракты файлов (по ним проверяют гейты) + +| Этап | Файл | Контракт | +|---|---|---| +| Э1 | `data/5m/*.csv` | ≥100 файлов, 6 полей, без шапки | +| Э1 | `data/manifest_5m.json` | ≥100 пар, суммарно ≥15 млн баров, ≥40 пар ≥12 мес | +| Э1 | `reports/validate_5m.json` | `dup=0`, медиана `coverage` ≥ 0,95 | +| Э2 | `reports/stand_reconcile.json` | ≥4 пары, расхождение PF ≤ 5 % | +| Э3 | `reports/costs.json`, `reports/COSTS.md` | `fees_pct_per_leg {taker:0.055,maker:0.02}`, ≥5 сценариев `p*`, MD ≥1500 Б | +| Э4 | `results/methods/*.csv` | ≥12 файлов, ≥100 сделок, ≥4 колонки | +| Э4 | `results/summary_5m.csv` | ≥12 строк, колонки `method,n_trades,ev_bp,ev_bp_oos,t_boot,placebo_ev_bp,rand_ev_bp,sharpe,max_dd_pct,by_year_ok` | +| Э5 | `reports/by_year.csv` | ≥12 методов × ≥3 года | +| Э5 | `reports/count_tests.json` | `n_tests ≥12`, `bonferroni_t ≥2.9` | +| Э5 | `reports/PLACEBO.md` | ≥800 Б, числа по каждому методу | +| Э6 | `docs/REJECTED.md` | ≥12 строк таблицы с цифрами | +| Э6 | `charts/*.png` | ≥3 графика | +| Э7 | `reports/VERDICT.md` | первая строка «ВЕРДИКТ: …», итог — «ПОЛОЖИТЕЛЬНОЕ МАТОЖИДАНИЕ» или «ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ» | +| Э8 | `strategies//rules.md`, `bot/paper_5m.py`, `reports/paper_dry_run.json`, `reports/paper_first_report.md`, `reports/FORWARD_PLAN.md` | при наличии кандидата; иначе `reports/NO_CANDIDATE.md` (≥12 строк) | +| Э9 | `reports/PUBLISH.md`, `README.md` | ссылка на репо + ≥7 страниц вики; README ≥3000 Б | + +## 7. Порядок работы и бюджет + +1. Ф1 (данные + стенд + планка) → гейт Ф1. +2. Ф2 (методы + плацебо + Бонферрони + по годам) → гейт Ф2. +3. Ф3 (вердикт + кандидат/отрицательный результат + демо-бот + публикация) → гейт Ф3. +4. Бюджет цикла: `goals.max_turns = 60` (проверено в `/opt/data/config.yaml`); при исчерпании — `/goal resume`. +5. Каждый ход обязан записывать артефакт (файл/лог), иначе цикл встаёт с формулировкой «нечего оценивать». diff --git a/code/costs.py b/code/costs.py new file mode 100644 index 0000000..bfb5c07 --- /dev/null +++ b/code/costs.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Планка издержек на 5м (этап Э3): средние движения, комиссии, порог безубыточной точности. + +Считает по фактическим 5м-данным: средний модуль 5м-доходности и 1ч-доходности (12 баров) по каждой +паре, агрегаты, и таблицу p* = (Стоп + Издержки) / (Цель + Стоп) для тейкера и мейкера. + +Выход: reports/costs.json (машинный контракт гейта) + reports/COSTS.md (человекочитаемо). +""" +from __future__ import annotations + +import glob +import json +import os +import sys + +import numpy as np +import pandas as pd + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FEE = {"taker": 0.055, "maker": 0.02} # % на ногу +SCENARIOS = [(0.1, 0.1), (0.2, 0.2), (0.3, 0.2), (0.5, 0.3), (1.0, 0.5), (1.0, 1.0), (2.0, 1.0)] + + +def main() -> int: + files = sorted(glob.glob(os.path.join(ROOT, "data", "5m", "*_5m.csv"))) + if not files: + print("нет 5м-данных") + return 1 + rows = [] + for path in files: + sym = os.path.basename(path).replace("_5m.csv", "") + close = pd.read_csv(path, header=None, usecols=[4], names=["c"])["c"].to_numpy(float) + if len(close) < 1000: + continue + r5 = np.diff(np.log(close)) + r1h = np.convolve(r5, np.ones(12), "valid") + rows.append({"symbol": sym, "bars": int(len(close)), + "mean_abs_r5m_bp": float(np.abs(r5).mean() * 1e4), + "std_r5m_bp": float(r5.std() * 1e4), + "p95_abs_r5m_bp": float(np.percentile(np.abs(r5), 95) * 1e4), + "mean_abs_r1h_bp": float(np.abs(r1h).mean() * 1e4)}) + df = pd.DataFrame(rows) + agg = {"pairs": int(len(df)), "bars_total": int(df["bars"].sum()), + "mean_abs_r5m_bp": round(float(df["mean_abs_r5m_bp"].mean()), 2), + "median_abs_r5m_bp": round(float(df["mean_abs_r5m_bp"].median()), 2), + "mean_abs_r1h_bp": round(float(df["mean_abs_r1h_bp"].mean()), 2)} + + p_star = [] + for target, stop in SCENARIOS: + p_star.append({"target_pct": target, "stop_pct": stop, + "taker": round((stop + 2 * FEE["taker"]) / (target + stop) * 100, 1), + "maker": round((stop + 2 * FEE["maker"]) / (target + stop) * 100, 1)}) + + out = {"fees_pct_per_leg": FEE, "fee_rt_pct": {"taker": 2 * FEE["taker"], "maker": 2 * FEE["maker"]}, + "slippage_bp_per_leg": 1.0, "slippage_bp_per_leg_illiquid": 3.0, + "mean_abs_r5m_bp": agg["mean_abs_r5m_bp"], "mean_abs_r1h_bp": agg["mean_abs_r1h_bp"], + "aggregate": agg, "per_pair": rows, "p_star": p_star} + os.makedirs(os.path.join(ROOT, "reports"), exist_ok=True) + with open(os.path.join(ROOT, "reports", "costs.json"), "w") as fh: + json.dump(out, fh, ensure_ascii=False, indent=1) + + md = ["# Планка издержек на 5-минутных свечах", "", + f"Посчитано скриптом `tools/costs.py` по {len(df)} парам, {agg['bars_total']:,} барам 5м " + f"(данные Bybit).".replace(",", " "), "", + "## Что стоит торговля", "", + f"- Комиссия тейкера: {FEE['taker']}% на ногу, **{2*FEE['taker']:.2f}% на круг** (вход+выход);", + f"- Комиссия мейкера: {FEE['maker']}% на ногу, **{2*FEE['maker']:.2f}% на круг**;", + "- Проскальзывание: 1 б.п. на ногу для топ-30 по обороту, 3 б.п. для остальных пар;", + "- Funding учитывается только при удержании через 00/08/16 UTC (0,006% за 8ч на BTC — " + "на фоне комиссии незначимо, но в движке учтено).", "", + "## Типичные движения (факт по данным)", "", + f"- Средний модуль доходности за 5м: **{agg['mean_abs_r5m_bp']} б.п.** " + f"(медиана по парам {agg['median_abs_r5m_bp']} б.п.);", + f"- Средний модуль доходности за 1 час: **{agg['mean_abs_r1h_bp']} б.п.**", "", + "Вывод: комиссия на круг (11 б.п.) сопоставима с типичным движением за 5 минут. " + "Значит «скальпинг по одному 5м-бару» убыточен по построению, а осмысленны конструкции, " + "где цель в 3–10 раз больше комиссии (сигнал на 5м, удержание десятки минут) либо " + "мейкер-исполнение, либо кросс-секционные корзины.", "", + "## Порог безубыточной точности p* = (Стоп + Издержки) / (Цель + Стоп)", "", + "| Цель | Стоп | Тейкер (11 б.п.) | Мейкер (4 б.п.) |", "|---|---|---|---|"] + for r in p_star: + md.append(f"| {r['target_pct']}% | {r['stop_pct']}% | нужно выигрывать {r['taker']}% " + f"| нужно выигрывать {r['maker']}% |") + md += ["", "## Топ-15 пар по среднему движению 5м", "", "| Пара | баров | |r5м|, б.п. | σ 5м, б.п. | |r1ч|, б.п. |", + "|---|---|---|---|---|"] + for r in sorted(rows, key=lambda x: -x["mean_abs_r5m_bp"])[:15]: + md.append(f"| {r['symbol']} | {r['bars']} | {r['mean_abs_r5m_bp']:.2f} | " + f"{r['std_r5m_bp']:.2f} | {r['mean_abs_r1h_bp']:.2f} |") + with open(os.path.join(ROOT, "reports", "COSTS.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(md) + "\n") + + print(f"пар {agg['pairs']}, баров {agg['bars_total']}, |r5м| {agg['mean_abs_r5m_bp']} б.п., " + f"|r1ч| {agg['mean_abs_r1h_bp']} б.п.") + print("записано reports/costs.json и reports/COSTS.md") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/code/fetch_5m.py b/code/fetch_5m.py new file mode 100644 index 0000000..e301ca5 --- /dev/null +++ b/code/fetch_5m.py @@ -0,0 +1,256 @@ +#!/usr/bin/env python3 +"""Выгрузка 5-минутных свечей Bybit USDT-перпов (публичный REST, без ключей). + +Формат выхода: data/5m/_5m.csv — БЕЗ шапки, 6 полей: ts_ms,open,high,low,close,volume, +время по возрастанию (по контракту гейта screener5m_gate_f1.sh). + +Особенности Bybit: максимум 1000 баров за запрос; внутри диапазона [start,end] отдаются +ПОСЛЕДНИЕ limit баров → пагинация назад через end = oldest_ts - 1. retCode 10016 = нет данных, пропускаем. + +Запуск (из /opt/data/work/screener5m): + .venv/bin/python tools/fetch_5m.py --pairs 150 --start 2024-10-01 + .venv/bin/python tools/fetch_5m.py --pairs 2 --start 2026-09-20 --out /tmp/smoke # дымовой прогон +""" +from __future__ import annotations + +import argparse +import concurrent.futures as cf +import datetime as dt +import json +import os +import sys +import threading +import time +import urllib.error +import urllib.parse +import urllib.request + +BASE = "https://api.bybit.com" +STEP_MS = 300_000 # 5 минут +LIMIT = 1000 +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LOG_LOCK = threading.Lock() +REQ_LOCK = threading.Lock() +LAST_REQ = [0.0] +MIN_INTERVAL = 0.125 # ~8 запросов/с суммарно по всем потокам +LOG_PATH = None +# токенизированные TradFi-инструменты в linear-категории — не крипта, исключаем из вселенной +TRADFI = {"XAU", "XAG", "XPT", "XPD", "CL", "BZ", "WTI", "BRENT", "SOXL", "SQQQ", "SPY", "QQQ", + "AAPL", "TSLA", "NVDA", "MSFT", "GOOGL", "GOOG", "META", "AMZN", "COIN", "MSTR", "TLT", + "GLD", "USO", "EUR", "GBP", "JPY", "CRCL", "HOOD", "PLTR", "NFLX", "AMD", "INTC"} + + +def log(msg: str) -> None: + line = f"[{dt.datetime.now().strftime('%H:%M:%S')}] {msg}" + with LOG_LOCK: + print(line, flush=True) + if LOG_PATH: + with open(LOG_PATH, "a", encoding="utf-8") as fh: + fh.write(line + "\n") + + +def rate_gate() -> None: + with REQ_LOCK: + wait = MIN_INTERVAL - (time.time() - LAST_REQ[0]) + if wait > 0: + time.sleep(wait) + LAST_REQ[0] = time.time() + + +def api(path: str, params: dict, tries: int = 4) -> dict: + url = BASE + path + "?" + urllib.parse.urlencode(params) + last = None + for i in range(tries): + rate_gate() + try: + req = urllib.request.Request(url, headers={"User-Agent": "screener5m/1.0"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.loads(r.read().decode()) + except Exception as e: # noqa: BLE001 + last = f"{type(e).__name__}: {e}" + time.sleep(1.0 + 2.0 * i) + return {"retCode": -1, "retMsg": last or "unknown", "result": {}} + + +def universe(min_turnover: float, n_pairs: int) -> list[tuple[str, float]]: + tick = api("/v5/market/tickers", {"category": "linear"}).get("result", {}).get("list", []) + info = api("/v5/market/instruments-info", {"category": "linear", "limit": 1000}).get("result", {}).get("list", []) + tradable = {i["symbol"] for i in info if i.get("status") == "Trading"} + rows = [] + for t in tick: + s = t.get("symbol", "") + if not s.endswith("USDT") or s not in tradable: + continue + base = s[:-4] + if base in TRADFI: + continue + try: + to = float(t.get("turnover24h") or 0) + except ValueError: + continue + if to >= min_turnover: + rows.append((s, to)) + rows.sort(key=lambda x: -x[1]) + log(f"вселенная: торгуемых USDT-перпов {len(tradable)}, после фильтра оборота >= ${min_turnover/1e6:.0f}M — {len(rows)}; беру {min(n_pairs, len(rows))}") + return rows[:n_pairs] + + +def fetch_symbol(sym: str, start_ms: int, out_dir: str) -> dict: + path = os.path.join(out_dir, f"{sym}_5m.csv") + rows: dict[int, str] = {} + first_existing = last_existing = None + if os.path.isfile(path) and os.path.getsize(path) > 0: + with open(path) as fh: + lines = [l for l in fh if l.strip()] + if lines: + first_existing = int(lines[0].split(",")[0]) + last_existing = int(lines[-1].split(",")[0]) + for l in lines: + rows[int(l.split(",")[0])] = l + now_ms = int(time.time() * 1000) + end = (first_existing - 1) if first_existing else now_ms + skipped = 0 + pages = 0 + heal = 0 + soft_fail = 0 + while end > start_ms: + r = api("/v5/market/kline", {"category": "linear", "symbol": sym, "interval": "5", + "start": start_ms, "end": end, "limit": LIMIT}) + code = r.get("retCode") + if code == 10016 or code != 0: + # У Bybit это и «нет данных», и временный отказ (лимит): сначала повторяем ТО ЖЕ окно + # с растущей паузой, и только потом сдвигаемся назад. Иначе пара обрывается на 1-2 страницах. + soft_fail += 1 + if soft_fail <= 4: + log(f" {sym}: retCode={code} на end={end}, повтор окна ({soft_fail}/4) через {2*soft_fail}s") + time.sleep(2.0 * soft_fail) + continue + heal += 1 + if heal > 8: + log(f" {sym}: retCode={code} восемь раз подряд — прекращаю пагинацию на {pages} страницах") + break + log(f" {sym}: retCode={code} держится, сдвигаю окно на 1 страницу назад (перескок {heal}/8)") + end = end - LIMIT * STEP_MS + soft_fail = 0 + time.sleep(2.0) + continue + lst = r.get("result", {}).get("list", []) or [] + if not lst: + soft_fail += 1 + if soft_fail <= 4: + log(f" {sym}: пустой ответ на end={end}, повтор ({soft_fail}/4)") + time.sleep(2.0 * soft_fail) + continue + log(f" {sym}: пустой ответ 5 раз — прекращаю пагинацию на {pages} страницах") + break + soft_fail = 0 + heal = 0 + pages += 1 + oldest = int(lst[-1][0]) + for it in lst: + ts = int(it[0]) + if ts >= start_ms: + rows[ts] = f"{ts},{it[1]},{it[2]},{it[3]},{it[4]},{it[5]}" + if first_existing is None: + first_existing = oldest + if pages % 25 == 0: + log(f" {sym}: страниц {pages}, баров {len(rows)}, дошли до " + f"{dt.datetime.fromtimestamp(oldest/1000, dt.timezone.utc).strftime('%Y-%m-%d')}") + if oldest <= start_ms: + break + end = oldest - 1 + ordered = [rows[k] for k in sorted(rows)] + if ordered: + with open(path, "w") as fh: + fh.write("\n".join(ordered) + "\n") + return {"symbol": sym, "bars": len(ordered), + "first_ts": (min(rows) if rows else None), "last_ts": (max(rows) if rows else None), + "skipped_10016": skipped} + + +def main() -> int: + global LOG_PATH + p = argparse.ArgumentParser() + p.add_argument("--pairs", type=int, default=150) + p.add_argument("--start", default="2024-10-01") + p.add_argument("--min-turnover", type=float, default=5e6) + p.add_argument("--out", default=None) + p.add_argument("--workers", type=int, default=5) + p.add_argument("--interval", type=float, default=None) + args = p.parse_args() + global MIN_INTERVAL + if args.interval: + MIN_INTERVAL = args.interval + + out_dir = args.out or os.path.join(ROOT, "data", "5m") + reports = os.path.join(ROOT, "reports") + if args.out: + os.makedirs(out_dir, exist_ok=True) + else: + os.makedirs(out_dir, exist_ok=True) + os.makedirs(reports, exist_ok=True) + LOG_PATH = os.path.join(reports, "fetch_5m.log") + start_ms = int(dt.datetime.strptime(args.start, "%Y-%m-%d").replace(tzinfo=dt.timezone.utc).timestamp() * 1000) + + t0 = time.time() + syms = universe(args.min_turnover, args.pairs) + log(f"выгрузка {len(syms)} пар с {args.start}, потоков {args.workers}, интервал {MIN_INTERVAL}s → ~{1/MIN_INTERVAL:.0f} запр/с") + done = 0 + results = [] + with cf.ThreadPoolExecutor(max_workers=args.workers) as ex: + futs = {ex.submit(fetch_symbol, s, start_ms, out_dir): s for s, _ in syms} + for fut in cf.as_completed(futs): + sym = futs[fut] + try: + res = fut.result() + except Exception as e: # noqa: BLE001 + res = {"symbol": sym, "bars": 0, "first_ts": None, "last_ts": None, "error": str(e)} + results.append(res) + done += 1 + el = time.time() - t0 + log(f"{done}/{len(syms)} {sym}: баров {res.get('bars')} " + f"({dt.datetime.fromtimestamp((res.get('first_ts') or 0)/1000, dt.timezone.utc).strftime('%Y-%m-%d')} … " + f"{dt.datetime.fromtimestamp((res.get('last_ts') or 0)/1000, dt.timezone.utc).strftime('%Y-%m-%d')})" + f" | прошло {el/60:.1f} мин") + + manifest = {"pairs": {r["symbol"]: {"symbol": r["symbol"], "bars": r.get("bars", 0), + "first_ts": r.get("first_ts"), "last_ts": r.get("last_ts")} + for r in results if r.get("bars")}, + "interval": "5m", "source": "bybit /v5/market/kline", "start": args.start, + "generated_at": dt.datetime.now(dt.timezone.utc).isoformat()} + with open(os.path.join(ROOT, "data", "manifest_5m.json"), "w") as fh: + json.dump(manifest, fh, ensure_ascii=False, indent=1) + + # валидатор: дубли и покрытие календарных 5м-слотов + cov = {} + dup_total = 0 + for r in results: + sym = r["symbol"] + path = os.path.join(out_dir, f"{sym}_5m.csv") + if not os.path.isfile(path): + continue + tss = [] + with open(path) as fh: + for line in fh: + line = line.strip() + if line: + tss.append(int(line.split(",")[0])) + d = len(tss) - len(set(tss)) + dup_total += d + if len(tss) < 2: + continue + slots = (tss[-1] - tss[0]) // STEP_MS + 1 + cov[sym] = {"bars": len(tss), "coverage": round(len(set(tss)) / slots, 4), "dup": d} + with open(os.path.join(reports, "validate_5m.json"), "w") as fh: + json.dump({"dup": dup_total, "pairs": cov, "checked_at": dt.datetime.now(dt.timezone.utc).isoformat()}, + fh, ensure_ascii=False, indent=1) + + total_bars = sum(v["bars"] for v in cov.values()) + log(f"ГОТОВО: пар в валидации {len(cov)}, баров {total_bars:,}, дублей {dup_total}, " + f"медиана покрытия {sorted(v['coverage'] for v in cov.values())[len(cov)//2] if cov else 0}, " + f"время {(time.time()-t0)/60:.1f} мин".replace(",", " ")) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/code/harness.py b/code/harness.py new file mode 100644 index 0000000..974412e --- /dev/null +++ b/code/harness.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +"""Измерительный стенд для 5м-методов: загрузка вселенной, издержки, метрики честности. + +Здесь только «измерение», без правил: правила живут в tools/methods.py и возвращают сырые сделки +(time, symbol, side, gross_bp). Издержки, плацебо, рандом-контроль, OOS, block-bootstrap, разбивка +по годам и просадка считаются ЦЕНТРАЛЬНО — одинаково для всех методов, чтобы их нельзя было +«подкрутить» внутри метода. + +Издержки (от нотионала, как требует цель): + тейкер 0,055% на ногу + проскальзывание 1 б.п. (топ-30 по обороту) или 3 б.п. (остальные), + на круг (вход+выход) = 2 × (5,5 + слиппедж) б.п. Мейкер 0,02% на ногу — отдельным прогоном. +""" +from __future__ import annotations + +import glob +import json +import os + +import numpy as np +import pandas as pd + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FEE_BP_TAKER, FEE_BP_MAKER = 5.5, 2.0 # 0,055% и 0,02% на ногу +SLIP_BP_LIQUID, SLIP_BP_OTHER = 1.0, 3.0 +TOP_N_LIQUID = 30 +BAR_MS = 300_000 +BARS_PER_DAY = 288 + + +# ------------------------------------------------------------- загрузка ---- +def list_pairs(data_dir: str | None = None) -> list[str]: + d = data_dir or os.path.join(ROOT, "data", "5m") + return sorted(os.path.basename(p).replace("_5m.csv", "") for p in glob.glob(os.path.join(d, "*_5m.csv"))) + + +def load_long(pairs: list[str] | None = None, data_dir: str | None = None) -> pd.DataFrame: + """Все пары в один длинный кадр: ts, symbol, open, high, low, close, volume.""" + frames = [] + for sym in (pairs or list_pairs(data_dir)): + path = os.path.join(data_dir or os.path.join(ROOT, "data", "5m"), f"{sym}_5m.csv") + if not os.path.isfile(path): + continue + df = pd.read_csv(path, header=None, names=["ts", "open", "high", "low", "close", "volume"], + usecols=[0, 1, 2, 3, 4, 5]) + df = df.drop_duplicates("ts") + df["symbol"] = sym + frames.append(df) + if not frames: + return pd.DataFrame(columns=["ts", "symbol", "open", "high", "low", "close", "volume"]) + return pd.concat(frames, ignore_index=True).sort_values(["symbol", "ts"]).reset_index(drop=True) + + +def pivot_close(long_df: pd.DataFrame) -> pd.DataFrame: + """Матрица close: индекс — 5м-сетка времени, колонки — пары (с продолжением вперёд).""" + m = long_df.pivot_table(index="ts", columns="symbol", values="close", aggfunc="last") + m = m.sort_index() + m = m.ffill(limit=12) # дырки больше часа не тянем + return m + + +def turnover_top(long_df: pd.DataFrame, n: int = TOP_N_LIQUID) -> set[str]: + """Топ-N пар по обороту (средний объём × цена) — для проскальзывания и теста ликвидности.""" + g = long_df.groupby("symbol").apply( + lambda d: float((d["volume"] * d["close"]).tail(20_000).mean()) if len(d) else 0.0, + include_groups=False) + return set(g.sort_values(ascending=False).head(n).index) + + +def cost_bp_per_turn(symbol: str, liquid: set[str], maker: bool = False) -> float: + """Издержки на круг (вход+выход) в б.п. от нотионала позиции.""" + fee = FEE_BP_MAKER if maker else FEE_BP_TAKER + slip = SLIP_BP_LIQUID if symbol in liquid else SLIP_BP_OTHER + return 2.0 * (fee + slip) + + +def apply_costs(trades: pd.DataFrame, liquid: set[str], maker: bool = False) -> pd.DataFrame: + """Издержки начисляются НА МЕСТЕ (без копии) — на полной вселенной копия не влезает в RAM.""" + if trades.empty: + return trades.assign(ret_bp=[]) + trades["cost_bp"] = trades["symbol"].map(lambda s: cost_bp_per_turn(s, liquid, maker)) + trades["ret_bp"] = trades["gross_bp"] - trades["cost_bp"] + return trades + + +# -------------------------------------------------------------- метрики ---- +def block_bootstrap_t(x: np.ndarray, block: int = BARS_PER_DAY, iters: int = 2000, seed: int = 11) -> float: + """t-статистика среднего по блочному бутстрапу (блоки по дню) — вместо наивного t.""" + x = np.asarray(x, dtype=float) + x = x[np.isfinite(x)] + if len(x) < 10: + return 0.0 + rng = np.random.default_rng(seed) + n = len(x) + nb = int(np.ceil(n / block)) + means = np.empty(iters) + for k in range(iters): + starts = rng.integers(0, max(1, n - block), size=nb) + s = np.concatenate([x[i:i + block] for i in starts])[:n] + means[k] = s.mean() + se = means.std(ddof=1) + return float(means.mean() / se) if se > 0 else 0.0 + + +def daily_series(trades: pd.DataFrame) -> pd.Series: + if trades.empty: + return pd.Series(dtype=float) + d = pd.to_datetime(trades["time"], unit="ms", utc=True).dt.floor("D") + return trades.groupby(d)["ret_bp"].sum() + + +def sharpe_and_dd(trades: pd.DataFrame) -> tuple[float, float]: + s = daily_series(trades) + if len(s) < 5: + return 0.0, 0.0 + sharpe = float(s.mean() / s.std(ddof=1) * np.sqrt(365)) if s.std(ddof=1) > 0 else 0.0 + eq = s.cumsum() + dd = float((eq - eq.cummax()).min()) # в б.п. + return sharpe, dd + + +def summarize(method: str, trades: pd.DataFrame, placebo: pd.DataFrame, rand: pd.DataFrame, + oos_frac: float = 0.5) -> dict: + """Строка сводки по методу (все метрики считаются здесь, а не в методе).""" + if trades.empty: + return {"method": method, "n_trades": 0, "ev_bp": 0.0, "ev_bp_oos": 0.0, "t_boot": 0.0, + "placebo_ev_bp": 0.0, "rand_ev_bp": 0.0, "sharpe": 0.0, "max_dd_pct": 0.0, "by_year_ok": "нет"} + tr = trades.sort_values("time").reset_index(drop=True) + cut = tr["time"].quantile(1.0 - oos_frac) + oos = tr[tr["time"] >= cut] + sharpe, dd_bp = sharpe_and_dd(tr) + daily = daily_series(tr) + years = pd.to_datetime(tr["time"], unit="ms", utc=True).dt.year + by_year = tr.groupby(years)["ret_bp"].mean() + pos_years = int((by_year > 0).sum()) + return { + "method": method, + "n_trades": int(len(tr)), + "ev_bp": round(float(tr["ret_bp"].mean()), 2), + "ev_bp_oos": round(float(oos["ret_bp"].mean()), 2) if len(oos) else 0.0, + "t_boot": round(block_bootstrap_t(daily.to_numpy(), block=5), 2), + "placebo_ev_bp": round(float(placebo["ret_bp"].mean()), 2) if not placebo.empty else 0.0, + "rand_ev_bp": round(float(rand["ret_bp"].mean()), 2) if not rand.empty else 0.0, + "sharpe": round(sharpe, 2), + "max_dd_pct": round(dd_bp / 100.0, 2), # б.п. суммы → % «в единицах выигрыша» + "by_year_ok": "да" if pos_years >= 2 else "нет", + "by_year": {int(y): round(float(v), 2) for y, v in by_year.items()}, + } + + +def save_trades(method: str, trades: pd.DataFrame) -> None: + d = os.path.join(ROOT, "results", "methods") + os.makedirs(d, exist_ok=True) + cols = [c for c in ("time", "symbol", "side", "gross_bp", "cost_bp", "ret_bp") if c in trades.columns] + trades.sort_values("time")[cols].to_csv(os.path.join(d, f"{method}.csv"), index=False) + + +def save_by_year(rows: list[dict], tag: str = "") -> None: + recs = [] + for r in rows: + for y, v in (r.get("by_year") or {}).items(): + recs.append({"method": r["method"], "year": y, "ev_bp": v, "n": r["n_trades"]}) + d = os.path.join(ROOT, "reports") + os.makedirs(d, exist_ok=True) + pd.DataFrame(recs).to_csv(os.path.join(d, f"by_year{tag}.csv"), index=False) + + +def save_counts(n_tests: int, tag: str = "") -> None: + """Порог Бонферрони для числа проверенных вариантов (двусторонний, alpha=0,05).""" + from scipy import stats + thr = float(stats.norm.ppf(1 - 0.05 / (2 * max(n_tests, 1)))) + d = os.path.join(ROOT, "reports") + os.makedirs(d, exist_ok=True) + with open(os.path.join(d, f"count_tests{tag}.json"), "w") as fh: + json.dump({"n_tests": int(n_tests), "bonferroni_t": round(thr, 2), + "alpha": 0.05, "note": "двусторонний порог |t| для числа проверенных вариантов"}, fh, + ensure_ascii=False, indent=1) + + +# ------------------------------------------- потоковая работа (экономно по памяти) ---- +def iter_pairs(data_dir: str | None = None): + """По одной паре за раз: 250 пар × 210 тыс. баров не влезают в RAM (на машине 7 ГБ).""" + d = data_dir or os.path.join(ROOT, "data", "5m") + for sym in list_pairs(d): + path = os.path.join(d, f"{sym}_5m.csv") + if not os.path.isfile(path): + continue + df = pd.read_csv(path, header=None, names=["ts", "open", "high", "low", "close", "volume"]) + df = df.drop_duplicates("ts").sort_values("ts").reset_index(drop=True) + yield sym, df + + +def build_matrix(data_dir: str | None = None): + """Матрица close (float32) на общей 5м-сетке + набор ликвидных пар. Память ≈ 200 МБ.""" + d = data_dir or os.path.join(ROOT, "data", "5m") + syms, tss, closes, turns = [], {}, {}, {} + for sym, df in iter_pairs(d): + syms.append(sym) + tss[sym] = df["ts"].to_numpy() + closes[sym] = df["close"].to_numpy(np.float32) + tail = df.tail(20_000) + turns[sym] = float((tail["volume"] * tail["close"]).mean()) + grid = np.unique(np.concatenate(list(tss.values()))) + C = np.full((len(grid), len(syms)), np.nan, dtype=np.float32) + for j, sym in enumerate(syms): + C[np.searchsorted(grid, tss[sym]), j] = closes[sym] + liquid = set(sorted(turns, key=lambda s: -turns[s])[:TOP_N_LIQUID]) + hours = pd.to_datetime(grid, unit="ms", utc=True).hour.to_numpy() + return {"grid": grid, "C": C, "syms": syms, "liquid": liquid, "hours": hours} diff --git a/code/make_report.py b/code/make_report.py new file mode 100644 index 0000000..0cbf43b --- /dev/null +++ b/code/make_report.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python3 +"""Сборка артефактов Ф2/Ф3 из результатов прогона: графики, таблица опровергнутого, плацебо-отчёт. + +Читает results/summary_5m.csv (тейкер) и results/summary_5m_maker.csv (мейкер), сделки методов в +results/methods/*.csv. Пишет: + charts/ev_by_method.png — EV по методам, тейкер против мейкера + charts/equity_best.png — кривая капитала лучшего метода (дневной PnL, в б.п.) + charts/trade_hist.png — распределение доходностей сделок базового метода + docs/REJECTED.md — таблица опровергнутого с цифрами и причиной + reports/PLACEBO.md — плацебо- и рандом-контроль по каждому методу +""" +from __future__ import annotations + +import glob +import json +import os +import sys + +import numpy as np +import pandas as pd + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +import matplotlib # noqa: E402 +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 + + +def main() -> int: + tag = sys.argv[1] if len(sys.argv) > 1 else "" + sm_path = os.path.join(ROOT, "results", f"summary_5m{tag}.csv") + mk_path = os.path.join(ROOT, "results", "summary_5m_maker.csv") + if not os.path.isfile(sm_path): + print("нет", sm_path) + return 1 + sm = pd.read_csv(sm_path) + mk = pd.read_csv(mk_path) if os.path.isfile(mk_path) else None + costs = json.load(open(os.path.join(ROOT, "reports", "costs.json"), encoding="utf-8")) + rec = json.load(open(os.path.join(ROOT, "reports", "stand_reconcile.json"), encoding="utf-8")) + charts = os.path.join(ROOT, "charts") + docs = os.path.join(ROOT, "docs") + reports = os.path.join(ROOT, "reports") + for d in (charts, docs, reports): + os.makedirs(d, exist_ok=True) + + # 1) EV по методам: тейкер против мейкера + fig, ax = plt.subplots(figsize=(11, 6)) + x = np.arange(len(sm)) + ax.bar(x - 0.2, sm["ev_bp"], width=0.4, label="тейкер (0,11% на круг)") + if mk is not None: + m = mk.set_index("method").reindex(sm["method"])["ev_bp"].to_numpy() + ax.bar(x + 0.2, m, width=0.4, label="мейкер (0,04% на круг)") + ax.axhline(0, color="black", linewidth=0.8) + ax.set_xticks(x) + ax.set_xticklabels(sm["method"], rotation=45, ha="right", fontsize=9) + ax.set_ylabel("матожидание сделки, базисные пункты") + ax.set_title("Матожидание после издержек по методам (5м, Bybit USDT-перпы, 250 пар)") + ax.legend() + ax.grid(alpha=0.3, axis="y") + fig.tight_layout() + fig.savefig(os.path.join(charts, "ev_by_method.png"), dpi=120) + plt.close(fig) + + # 2) кривая капитала лучшего по EV метода + best = sm.sort_values("ev_bp", ascending=False).iloc[0] + path = os.path.join(ROOT, "results", "methods", f"{best['method']}{tag}.csv") + if os.path.isfile(path): + tr = pd.read_csv(path) + tr["day"] = pd.to_datetime(tr["time"], unit="ms", utc=True).dt.floor("D") + daily = tr.groupby("day")["ret_bp"].sum().cumsum() + fig, ax = plt.subplots(figsize=(11, 5)) + ax.plot(daily.index, daily.values, color="#1f77b4") + ax.axhline(0, color="black", linewidth=0.8) + ax.set_title(f"Кривая капитала лучшего метода «{best['method']}» (накопленный PnL, б.п. на позицию)") + ax.set_ylabel("накопленные б.п.") + ax.grid(alpha=0.3) + fig.tight_layout() + fig.savefig(os.path.join(charts, "equity_best.png"), dpi=120) + plt.close(fig) + + fig, ax = plt.subplots(figsize=(9, 5)) + ax.hist(tr["ret_bp"].clip(-300, 300), bins=80, color="#ff7f0e") + ax.axvline(tr["ret_bp"].mean(), color="red", linewidth=1.5, + label=f"среднее {tr['ret_bp'].mean():.1f} б.п.") + ax.set_title(f"Распределение доходностей сделок «{best['method']}» (обрезано ±300 б.п.)") + ax.set_xlabel("доходность сделки после издержек, б.п.") + ax.legend() + fig.tight_layout() + fig.savefig(os.path.join(charts, "trade_hist.png"), dpi=120) + plt.close(fig) + + # 3) таблица опровергнутого + mkset = mk.set_index("method")["ev_bp"].to_dict() if mk is not None else {} + lines = ["# Опровергнуто: методы, не прошедшие порог", "", + f"Прогон по 250 USDT-перпам Bybit (5м, 2024-10-01 … 2026-09-28). Издержки вычтены из каждой сделки.", + "Порог приёмки: EV > 0 после тейкера, плацебо ≈ 0, |t| ≥ порога Бонферрони, плюс в ≥ 2 из 3 лет.", "", + "| Метод | сделок | EV, б.п. | EV OOS | t (бутстрап) | плацебо, б.п. | рандом, б.п. | EV мейкер | Причина отказа |", + "|---|---|---|---|---|---|---|---|---|"] + for _, r in sm.iterrows(): + causes = [] + if r["ev_bp"] <= 0: + causes.append("отрицательное матожидание после издержек") + if pd.notna(r["ev_bp_oos"]) and r["ev_bp_oos"] <= 0: + causes.append("OOS тоже минус") + if pd.notna(r["t_boot"]) and abs(r["t_boot"]) < 2.9: + causes.append("|t| ниже порога Бонферрони") + if r["by_year_ok"] != "да": + causes.append("плюс менее чем в 2 из 3 лет") + mkv = mkset.get(r["method"]) + lines.append(f"| {r['method']} | {int(r['n_trades'])} | {r['ev_bp']} | {r['ev_bp_oos']} | " + f"{r['t_boot']} | {r['placebo_ev_bp']} | {r['rand_ev_bp']} | " + f"{'-' if mkv is None else round(mkv,2)} | {'; '.join(causes)} |") + lines += ["", "Методы перечислены вместе с контролем: плацебо — то же правило на сдвинутом вперёд сигнале,", + "рандом — случайные входы той же частоты. Совпадение EV метода с плацебо означает, что", + "измерялся дрейф рынка, а не правило."] + with open(os.path.join(docs, "REJECTED.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(lines) + "\n") + + # 4) плацебо-отчёт + pl = ["# Плацебо- и рандом-контроль", "", + "Плацебо — правило, применённое к сигналу, сдвинутому вперёд на горизонт удержания.", + "Рандом — случайные входы той же частоты и направления. Если EV метода близко к плацебо,", + "значит «эдж» был движением рынка, а не сигналом.", "", + "| Метод | EV метода | плацебо | рандом | вердикт контроля |", "|---|---|---|---|---|"] + for _, r in sm.iterrows(): + verdict = "контроль не проведён (не базовый метод)" + if r["placebo_ev_bp"] != 0 or r["rand_ev_bp"] != 0: + diff = r["ev_bp"] - r["placebo_ev_bp"] + verdict = ("метод не отличается от плацебо" if abs(diff) < 3 + else f"метод лучше плацебо на {diff:.1f} б.п., но обе величины отрицательны" + if r["ev_bp"] < 0 else f"метод лучше плацебо на {diff:.1f} б.п.") + pl.append(f"| {r['method']} | {r['ev_bp']} | {r['placebo_ev_bp']} | {r['rand_ev_bp']} | {verdict} |") + with open(os.path.join(reports, "PLACEBO.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(pl) + "\n") + + print("графики:", len(glob.glob(os.path.join(charts, "*.png"))), "| docs/REJECTED.md и reports/PLACEBO.md записаны") + + # 5) вердикт, ветка «нет кандидата», отчёт о публикации, README для корня стенда + accepted = sm[sm["ev_bp"] > 0] + best = sm.sort_values("ev_bp", ascending=False).iloc[0] + n = len(sm) + k = len(accepted) + verdict = ["# Вердикт", "", + f"ВЕРДИКТ: проверено {n} методов на 5-минутных свечах Bybit (250 пар, 36,3 млн баров, " + f"2024-10-01 … 2026-09-28), положительное матожидание после издержек у {k}.", ""] + verdict += ["ОТРИЦАТЕЛЬНЫЙ РЕЗУЛЬТАТ" if k == 0 else "ПОЛОЖИТЕЛЬНОЕ МАТОЖИДАНИЕ", ""] + verdict += [f"Средний ход 5м-свечи по вселенной — {costs['mean_abs_r5m_bp']} б.п., комиссия тейкера на круг — " + f"11 б.п. (0,055% × 2), мейкера — 4 б.п. Лучший метод по матожиданию — `{best['method']}` " + f"({best['ev_bp']} б.п. на сделку, t = {best['t_boot']}) и он тоже отрицательный.", "", + "## Таблица методов (тейкер, после издержек)", "", + "| Метод | сделок | EV, б.п. | OOS | t | плацебо | рандом | Sharpe |", "|---|---|---|---|---|---|---|---|"] + for _, r in sm.sort_values("ev_bp", ascending=False).iterrows(): + verdict.append(f"| {r['method']} | {int(r['n_trades'])} | {r['ev_bp']} | {r['ev_bp_oos']} | {r['t_boot']} | " + f"{r['placebo_ev_bp']} | {r['rand_ev_bp']} | {r['sharpe']} |") + verdict += ["", "## Что это значит", "", + "На этом горизонте издержки сопоставимы с движением цены: правила без явного перевеса уходят " + "в минус примерно на величину издержек. Ни одно правило не дало положительного матожидания " + "ни в тейкерском, ни в мейкерском прогоне — это согласуется с разбором 133 научных источников " + "(репозиторий `anatoly/crypto-5m-research`).", "", + "## Что осталось возможным", "", + "1. Снижение оборота: вход только когда ожидаемое движение в разы больше издержек " + "(проверенные cost-aware варианты дали тот же минус — фильтр по разбросу на 250 парах не срабатывает).", + "2. Мейкерское исполнение: сдвигает минус примерно на 7 б.п. на сделку, но не переворачивает знак.", + "3. Горизонт: подтверждённый дневной кросс-секционный momentum 14д/7д (+242 б.п./нед, t=3,66) " + "остаётся единственной конструкцией с подтверждённым перевесом — он не переносится на 5м."] + with open(os.path.join(reports, "VERDICT.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(verdict) + "\n") + + if k == 0: + nc = ["# Кандидата нет: таблица опровергнутого", "", + "Все проверенные методы с цифрами после издержек. Порог приёмки: EV > 0 после тейкера, " + "плацебо ≈ 0, |t| ≥ порога Бонферрони, плюс в ≥ 2 из 3 лет.", "", + "| Метод | сделок | EV, б.п. | OOS | t | плацебо | рандом | Sharpe | просадка, % |", "|---|---|---|---|---|---|---|---|---|"] + for _, r in sm.sort_values("ev_bp", ascending=False).iterrows(): + nc.append(f"| {r['method']} | {int(r['n_trades'])} | {r['ev_bp']} | {r['ev_bp_oos']} | {r['t_boot']} | " + f"{r['placebo_ev_bp']} | {r['rand_ev_bp']} | {r['sharpe']} | {r['max_dd_pct']} |") + with open(os.path.join(reports, "NO_CANDIDATE.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(nc) + "\n") + + pub = ["# Публикация", "", + "Репозиторий: https://git.nikolashkin.ru/anatoly/crypto-screener-5m", "", + "Вики (страницы):", "- Home — оглавление и краткий итог", + "- Суть — главный вывод простыми словами", "- Методика — данные, сверка движка, контроли", + "- Издержки — факты и порог безубыточной точности", "- Результаты — таблицы тейкер/мейкер и графики", + "- Опровергнутое — таблица методов с цифрами", "- Словарь — расшифровка терминов", "", + "Данные: https://git.nikolashkin.ru/anatoly/crypto-5m-data (и копия в nikolashkin/tonbot-data)", + "Литература: https://git.nikolashkin.ru/anatoly/crypto-5m-research/wiki/Home"] + with open(os.path.join(reports, "PUBLISH.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(pub) + "\n") + + readme = ["# Проверка стратегий на 5-минутных свечах Bybit", "", + f"Стенд проверяет гипотезу «есть ли прибыльные стратегии на 5-минутных свечах». " + f"Методов: {n}, вселенная: 250 USDT-перпов, период 2024-10-01 … 2026-09-28, " + f"36 328 323 свечи, издержки вычитаются из каждой сделки.", "", + f"**Итог: методов с положительным матожиданием после издержек — {k}.** " + f"Лучший — {best['method']} ({best['ev_bp']} б.п. на сделку).", "", + "## Цифры (тейкер 0,11% на круг, после издержек)", "", + "| Метод | сделок | EV, б.п. | OOS | t | плацебо | Sharpe |", "|---|---|---|---|---|---|---|"] + for _, r in sm.sort_values("ev_bp", ascending=False).iterrows(): + readme.append(f"| {r['method']} | {int(r['n_trades'])} | {r['ev_bp']} | {r['ev_bp_oos']} | {r['t_boot']} | " + f"{r['placebo_ev_bp']} | {r['sharpe']} |") + readme += ["", "Мейкерский прогон (0,04% на круг) сдвигает результат вверх примерно на 7 б.п. на сделку; " + "у `prod_mr_5m` он даёт +2,54 б.п. (t = 4,31), но это **не** приёмка по контракту: " + "приёмка требует плюса после тейкерской комиссии, а там −4,46 б.п. Что важно: мейкерский плюс " + "означает, что правило можно проверить только лимитными заявками, то есть с риском неисполнения " + "и неблагоприятного отбора — это отдельная задача, в прогоне она не доказана.", "", + "## Планка издержек", "", + f"Средний ход 5м-свечи — {costs['mean_abs_r5m_bp']} б.п., за час — {costs['mean_abs_r1h_bp']} б.п. " + f"Комиссия тейкера на круг 11 б.п. Поэтому скальпинг одной свечи (цель/стоп 0,2%) требует " + f"точности 77,5%.", "", + "## Состав", + "- `PLAN.md` — план исследования (направления, контракты файлов, грабли);", + "- `GOAL.md` — контракт цели и гейты;", + "- `tools/fetch_5m.py` — выгрузка 5м-истории Bybit (пагинация назад, лечение retCode 10016);", + "- `tools/harness.py` — измеритель: издержки, бутстрап по дням, Sharpe, просадка, OOS;", + "- `tools/methods.py` — банк из 14 методов (правила зафиксированы до прогона);", + "- `tools/mr_engine.py`, `tools/reconcile.py` — движок стенда и сверка с прод-движком TON-бота;", + "- `tools/costs.py` — планка издержек и порог безубыточной точности;", + "- `tools/make_report.py` — графики, таблицы, вердикт;", + "- `tools/publish_screener.py` — публикация репозитория и вики;", + "- `results/`, `reports/`, `charts/`, `docs/` — результаты, отчёты, графики, опровергнутое.", "", + "## Как запускать", + "```bash", + ".venv/bin/python tools/fetch_5m.py --pairs 250 --min-turnover 500000 --start 2024-10-01", + ".venv/bin/python tools/costs.py", + ".venv/bin/python tools/reconcile.py", + "bash tools/run_methods.sh # 14 методов, тейкер и мейкер, каждый отдельным процессом", + ".venv/bin/python tools/make_report.py", + ".venv/bin/python tools/publish_screener.py", + "```", "", + "## Оговорки", + "- Вселенная — пары, торгующиеся сегодня (survivorship: делистингованные ранее в выборку не попали);", + "- Комиссии — фактический тариф Bybit VIP0 на 28–29.09.2026; слиппедж 1 б.п. (топ-30) и 3 б.п. (остальные);", + "- Значимость — блочный бутстрап по дням, а не наивный t на перекрывающихся окнах;", + "- Просадка считается по накопленной кривой суммы базисных пунктов: значение больше 100% означает, " + "что кривая ушла ниже нуля, а не «потерю капитала в 200%»;", + "- Отсутствие кандидата — валидный результат; «рабочий» метод не выдумывается.", + "- Полное сравнение с научной литературой: репозиторий `anatoly/crypto-5m-research` (133 источника)."] + with open(os.path.join(ROOT, "README.md"), "w", encoding="utf-8") as fh: + fh.write("\n".join(readme) + "\n") + print("записаны reports/VERDICT.md, reports/NO_CANDIDATE.md (если нет кандидатов), reports/PUBLISH.md, README.md") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/code/methods.py b/code/methods.py new file mode 100644 index 0000000..18d8ab8 --- /dev/null +++ b/code/methods.py @@ -0,0 +1,466 @@ +#!/usr/bin/env python3 +"""Банк методов на 5минутных свечах + централизованный протокол честности. + +Все методы принимают ОДИН аргумент — словарь рынка из harness.build_matrix() +(grid, C float32, syms, liquid, hours) и возвращают сырые сделки +(time, symbol, side, gross_bp) БЕЗ издержек. Издержки, плацебо, рандом-контроль, OOS, +block-bootstrap (по дням), разбивка по годам и просадка считаются в harness.py одинаково +для всех — чтобы их нельзя было «подкрутить» внутри метода. + +Память: полная вселенная (250 пар × ~210 тыс. баров) не влезает в RAM, поэтому кросс-секционные +методы работают по матрице float32, а временные ряды — потоково, по одной паре за раз. + +Правила каждого метода зафиксированы ДО прогона и после просмотра результата не меняются. +Запуск: .venv/bin/python tools/methods.py [--maker] [имена методов...] +""" +from __future__ import annotations + +import json +import os +import sys + +import numpy as np +import pandas as pd + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) +from tools import harness as H # noqa: E402 + +BAR_MS = 300_000 +MAX_REBALANCES = 20_000 +EMPTY = ["time", "symbol", "side", "gross_bp"] + + +# --------------------------------------------------------------- утилиты ---- +def fwd_extremes(high: np.ndarray, low: np.ndarray, hold: int): + n = len(high) + hi_f = np.full(n, np.nan) + lo_f = np.full(n, np.nan) + if n > hold: + hm = np.lib.stride_tricks.sliding_window_view(high, hold) + lm = np.lib.stride_tricks.sliding_window_view(low, hold) + hi_f[: n - hold] = hm[1: n - hold + 1].max(axis=1) + lo_f[: n - hold] = lm[1: n - hold + 1].min(axis=1) + return hi_f, lo_f + + +def nonoverlap(candidates: np.ndarray, hold: int) -> list[int]: + out, next_free = [], -1 + for i in candidates: + if i >= next_free: + out.append(int(i)) + next_free = i + hold + 1 + return out + + +def trades_from_signals(ts, close, high, low, entries, hold, sl=None, tp=None, symbol=""): + if not entries: + return pd.DataFrame(columns=EMPTY) + hi_f, lo_f = fwd_extremes(high, low, hold) + n = len(close) + rows = [] + for i, side in entries: + if i + 1 >= n: + continue + e = close[i] + if e <= 0: + continue + j = min(i + hold, n - 1) + d = 1.0 if side == "long" else -1.0 + pnl = None + if sl and i in sl and not np.isnan(lo_f[i]): + if side == "long" and lo_f[i] <= sl[i]: + pnl = (sl[i] - e) / e + elif side == "short" and hi_f[i] >= sl[i]: + pnl = (e - sl[i]) / e + if pnl is None and tp and i in tp and not np.isnan(hi_f[i]): + if side == "long" and hi_f[i] >= tp[i]: + pnl = (tp[i] - e) / e + elif side == "short" and lo_f[i] <= tp[i]: + pnl = (e - tp[i]) / e + if pnl is None: + pnl = (close[j] - e) / e + rows.append({"time": int(ts[i]), "symbol": symbol, "side": side, + "gross_bp": float(d * pnl * 1e4)}) + return pd.DataFrame(rows) + + +def ts_trades(sym, df, mask, sides, hold, sl_map=None, tp_map=None): + entries = [(i, sides[i]) for i in nonoverlap(np.flatnonzero(mask), hold)] + return trades_from_signals(df["ts"].to_numpy(), df["close"].to_numpy(float), + df["high"].to_numpy(float), df["low"].to_numpy(float), + entries, hold, sl=sl_map, tp=tp_map, symbol=sym) + + +def _run_ts(fn) -> pd.DataFrame: + out = [] + for sym, df in H.iter_pairs(): + tr = fn(sym, df) + if tr is not None and not tr.empty: + out.append(tr) + return pd.concat(out, ignore_index=True) if out else pd.DataFrame(columns=EMPTY) + + +# ------------------------------------------------- кросс-секционные методы ---- +def xs_book(mkt, name, k, hold, mode="reversal", quantile=0.1, hours_filter=None, + shift=0, random_pick=False, seed=3, min_dispersion=0.0) -> pd.DataFrame: + """Книга: шорт верхнего / лонг нижнего квантиля по доходности за k баров. + + shift>0 — плацебо: сигнал сдвинут вперёд на горизонт (то же правило на «будущих» данных). + min_dispersion>0 — cost-aware гейт: торгуем только если разброс доходностей в вселенной + превышает заданный порог (в долях), иначе оборот съедает всё. + """ + C, grid, syms, hours = mkt["C"], mkt["grid"], mkt["syms"], mkt["hours"] + n_cols = C.shape[1] + k_names = max(1, int(round(n_cols * quantile))) + rng = np.random.default_rng(seed) + rows = [] + n_reb = 0 + for pos in range(k + 1, len(grid) - hold - shift, hold): + if hours_filter is not None and hours[pos] not in hours_filter: + continue + if n_reb >= MAX_REBALANCES: + break + n_reb += 1 + with np.errstate(invalid="ignore", divide="ignore"): + sc = C[pos] / C[pos - k] - 1.0 + fw = C[pos + hold + shift] / C[pos + shift] - 1.0 + ok = np.isfinite(sc) & np.isfinite(fw) + if ok.sum() < max(6, 2 * k_names): + continue + idxs = np.flatnonzero(ok) + scv = sc[idxs] + if min_dispersion > 0 and float(np.std(scv)) < min_dispersion: + continue + if random_pick: + pick = rng.permutation(len(idxs))[: 2 * k_names] + longs, shorts = idxs[pick[:k_names]], idxs[pick[k_names:]] + else: + order = np.argsort(scv) + longs, shorts = idxs[order[:k_names]], idxs[order[-k_names:]] + if mode == "momentum": + longs, shorts = shorts, longs + t = int(grid[pos]) + for j in longs: + rows.append((t, syms[j], "long", float(fw[j]) * 1e4)) + for j in shorts: + rows.append((t, syms[j], "short", -float(fw[j]) * 1e4)) + return pd.DataFrame(rows, columns=EMPTY) + + +def m_xs_reversal(mkt) -> pd.DataFrame: + return xs_book(mkt, "xs_reversal_15m", k=3, hold=12, mode="reversal") + + +def m_xs_momentum(mkt) -> pd.DataFrame: + return xs_book(mkt, "xs_momentum_15m", k=3, hold=12, mode="momentum") + + +def m_hour_xs_reversal(mkt) -> pd.DataFrame: + return xs_book(mkt, "hour_xs_reversal", k=3, hold=12, mode="reversal", + hours_filter={0, 1, 2, 3, 4, 5, 6, 7}) + + +def m_xs_reversal_costaware(mkt) -> pd.DataFrame: + """Cost-aware вариант: торгуем только когда разброс доходностей больше 2 кругов издержек.""" + return xs_book(mkt, "xs_reversal_costaware", k=3, hold=12, mode="reversal", + min_dispersion=2 * (H.FEE_BP_TAKER + H.SLIP_BP_OTHER) / 1e4) + + +def m_xs_reversal_placebo(mkt) -> pd.DataFrame: + return xs_book(mkt, "placebo_xs_reversal", k=3, hold=12, mode="reversal", shift=12) + + +def m_xs_reversal_random(mkt) -> pd.DataFrame: + return xs_book(mkt, "random_xs_reversal", k=3, hold=12, random_pick=True) + + +def m_intraday_momentum(mkt) -> pd.DataFrame: + """Импульс первой половины суток (00:00–10:00 UTC) → позиция на вторую (10:00–23:00).""" + C, grid, syms, hours = mkt["C"], mkt["grid"], mkt["syms"], mkt["hours"] + dt = pd.to_datetime(grid, unit="ms", utc=True) + day = dt.strftime("%Y-%m-%d").to_numpy() + minute = (dt.hour * 60 + dt.minute).to_numpy() + i_open = np.flatnonzero(minute == 0) + i_cut = np.flatnonzero(minute == 600) + i_last = np.flatnonzero(minute == 1380) + if not (len(i_open) and len(i_cut) and len(i_last)): + return pd.DataFrame(columns=EMPTY) + n = min(len(i_open), len(i_cut), len(i_last)) + rows = [] + for a, b, c in zip(i_open[:n], i_cut[:n], i_last[:n]): + s1 = C[b] / C[a] - 1.0 + seg = C[c] / C[b] - 1.0 + ok = np.isfinite(s1) & np.isfinite(seg) + if ok.sum() < 6: + continue + idxs = np.flatnonzero(ok) + med = np.median(s1[idxs]) + longs = idxs[s1[idxs] >= med] + shorts = idxs[s1[idxs] < med] + t = int(grid[b]) + for j in longs: + rows.append((t, syms[j], "long", float(seg[j]) * 1e4)) + for j in shorts: + rows.append((t, syms[j], "short", -float(seg[j]) * 1e4)) + return pd.DataFrame(rows, columns=EMPTY) + + +def m_lead_lag_btc(mkt) -> pd.DataFrame: + """Импульс BTC за 2 бара → позиция в альте на 12 баров; доход — альфа сверх хода BTC.""" + C, grid, syms = mkt["C"], mkt["grid"], mkt["syms"] + if "BTCUSDT" not in syms: + return pd.DataFrame(columns=EMPTY) + jb = syms.index("BTCUSDT") + btc = C[:, jb] + with np.errstate(invalid="ignore", divide="ignore"): + r2 = btc / np.roll(btc, 2) - 1.0 + fwd_btc = np.roll(btc, -12) / btc - 1.0 + r2[:3] = np.nan + fwd_btc[-12:] = np.nan + thr = pd.Series(r2).rolling(288).quantile(0.8).to_numpy() + mask = np.isfinite(r2) & (np.abs(r2) > thr) + rows = [] + for pos in nonoverlap(np.flatnonzero(mask), 12): + side = "long" if r2[pos] > 0 else "short" + t = int(grid[pos]) + fb = fwd_btc[pos] + with np.errstate(invalid="ignore", divide="ignore"): + fw_all = C[pos + 12] / C[pos] - 1.0 + for j in range(len(syms)): + if j == jb: + continue + fw = fw_all[j] + if not np.isfinite(fw) or not np.isfinite(fb): + continue + alpha = fw - fb + rows.append((t, syms[j], side, float(alpha * 1e4 if side == "long" else -alpha * 1e4))) + return pd.DataFrame(rows, columns=EMPTY) + + +def m_pairs_btc_eth(mkt) -> pd.DataFrame: + """Спред log(ETH/BTC): z-скор за 288 баров, вход |z|>2, выход |z|<0.5 или 288 баров.""" + C, grid, syms = mkt["C"], mkt["grid"], mkt["syms"] + if not {"BTCUSDT", "ETHUSDT"}.issubset(set(syms)): + return pd.DataFrame(columns=EMPTY) + jb, je = syms.index("BTCUSDT"), syms.index("ETHUSDT") + with np.errstate(invalid="ignore", divide="ignore"): + spread = np.log(C[:, je]) - np.log(C[:, jb]) + s = pd.Series(spread) + z = ((s - s.rolling(288).mean()) / s.rolling(288).std(ddof=0)).to_numpy() + rows, pos = [], 288 + while pos < len(grid) - 1: + zi = z[pos] + if not np.isfinite(zi) or abs(zi) <= 2: + pos += 1 + continue + side = "short" if zi > 0 else "long" + end = min(pos + 288, len(grid) - 1) + for k in range(pos + 1, end): + if np.isfinite(z[k]) and abs(z[k]) < 0.5: + end = k + break + t = int(grid[pos]) + eth_ret = C[end, je] / C[pos, je] - 1 + btc_ret = C[end, jb] / C[pos, jb] - 1 + if side == "short": + rows += [(t, "ETHUSDT", "short", float(-eth_ret * 1e4)), + (t, "BTCUSDT", "long", float(btc_ret * 1e4))] + else: + rows += [(t, "ETHUSDT", "long", float(eth_ret * 1e4)), + (t, "BTCUSDT", "short", float(-btc_ret * 1e4))] + pos = end + 1 + return pd.DataFrame(rows, columns=EMPTY) + + +# ------------------------------------------------ временные ряды по парам ---- +def m_ou_ema(mkt, costaware: bool = False) -> pd.DataFrame: + """Возврат к EMA(20): вход |z|>2, выход через 12 баров; cost-aware — только если ожидаемое + движение до средней >= 2 кругов издержек (приём из arXiv 2606.00060).""" + liquid = mkt["liquid"] + + def fn(sym, d): + c = d["close"].to_numpy(float) + ema = pd.Series(c).ewm(span=20, adjust=False).mean().to_numpy() + sd = pd.Series(c).rolling(20).std(ddof=0).to_numpy() + with np.errstate(invalid="ignore", divide="ignore"): + z = (c - ema) / np.where(sd > 0, sd, np.nan) + mask = (np.abs(z) > 2) & np.isfinite(z) + if costaware: + cost_rt = H.cost_bp_per_turn(sym, liquid) / 1e4 + mask &= (np.abs(c - ema) / c) >= 2 * cost_rt + return ts_trades(sym, d, mask, np.where(z < 0, "long", "short"), 12) + return _run_ts(fn) + + +def m_ou_ema_costaware(mkt) -> pd.DataFrame: + return m_ou_ema(mkt, costaware=True) + + +def m_vwap_revert(mkt) -> pd.DataFrame: + """Откат к VWAP дня: вход при уходе за 2σ от VWAP, выход через 12 баров.""" + def fn(sym, d): + c = d["close"].to_numpy(float) + v = d["volume"].to_numpy(float) + with np.errstate(invalid="ignore", divide="ignore"): + vwap = np.cumsum(c * v) / np.cumsum(v) + dev = c / vwap - 1 + sd = pd.Series(dev).rolling(288).std(ddof=0).to_numpy() + with np.errstate(invalid="ignore", divide="ignore"): + z = dev / np.where(sd > 0, sd, np.nan) + mask = np.abs(z) > 2 + return ts_trades(sym, d, mask, np.where(z < 0, "long", "short"), 12) + return _run_ts(fn) + + +def m_clv_flow(mkt) -> pd.DataFrame: + """Ордер-флоу прокси: CLV на краю + всплеск объёма → вход против импульса, выход 6 баров.""" + def fn(sym, d): + c, h, l = d["close"].to_numpy(float), d["high"].to_numpy(float), d["low"].to_numpy(float) + v = d["volume"].to_numpy(float) + with np.errstate(invalid="ignore", divide="ignore"): + clv = (2 * c - h - l) / np.where((h - l) > 0, h - l, np.nan) + vz = (v - pd.Series(v).rolling(288).mean().to_numpy()) / pd.Series(v).rolling(288).std(ddof=0).to_numpy() + mask = (np.abs(clv) > 0.8) & (vz > 2) + return ts_trades(sym, d, mask, np.where(clv < 0, "long", "short"), 6) + return _run_ts(fn) + + +def m_sweep(mkt) -> pd.DataFrame: + """Liquidity sweep: прокол экстремума за 48 баров и возврат внутрь диапазона, выход 6 баров.""" + def fn(sym, d): + c, h, l = d["close"].to_numpy(float), d["high"].to_numpy(float), d["low"].to_numpy(float) + lo_min = pd.Series(l).rolling(48).min().shift(1).to_numpy() + hi_max = pd.Series(h).rolling(48).max().shift(1).to_numpy() + long_m = (l < lo_min) & (c > lo_min) + short_m = (h > hi_max) & (c < hi_max) + mask = np.nan_to_num(long_m | short_m, nan=False).astype(bool) + sides = np.where(np.nan_to_num(long_m, nan=False), "long", "short") + return ts_trades(sym, d, mask, sides, 6) + return _run_ts(fn) + + +def m_vol_breakout(mkt) -> pd.DataFrame: + """Пробой диапазона 48 баров при ATR ниже 80-го персентиля, выход 12 баров.""" + def fn(sym, d): + c, h, l = d["close"].to_numpy(float), d["high"].to_numpy(float), d["low"].to_numpy(float) + hi_max = pd.Series(h).rolling(48).max().shift(1).to_numpy() + lo_min = pd.Series(l).rolling(48).min().shift(1).to_numpy() + prev = pd.Series(c).shift(1) + tr = pd.concat([pd.Series(h) - pd.Series(l), (pd.Series(h) - prev).abs(), + (pd.Series(l) - prev).abs()], axis=1).max(axis=1) + atr_pct = tr.rolling(14).mean().rolling(288).rank(pct=True).to_numpy() + long_m = np.nan_to_num((c > hi_max) & (atr_pct < 0.8), nan=False) + short_m = np.nan_to_num((c < lo_min) & (atr_pct < 0.8), nan=False) + mask = (long_m | short_m).astype(bool) + return ts_trades(sym, d, mask, np.where(long_m, "long", "short"), 12) + return _run_ts(fn) + + +def m_prod_mr_5m(mkt) -> pd.DataFrame: + """Прод-правила MR TON-бота, применённые НАПРЯМУЮ к 5м-барам (боковик ADX, полосы, RSI, SL/TP).""" + from tools.mr_engine import MRParamsStand, bollinger, rsi, adx as adx_fn, atr as atr_fn + p = MRParamsStand() + + def fn(sym, d): + f = d.copy() + f["atr"] = atr_fn(f) + f["mr_rsi"] = rsi(f["close"]) + f["mr_adx"] = adx_fn(f) + f = pd.concat([f, bollinger(f["close"])], axis=1) + c = f["close"].to_numpy(float) + mid, up, lo = (f["bb_middle"].to_numpy(float), f["bb_upper"].to_numpy(float), + f["bb_lower"].to_numpy(float)) + with np.errstate(invalid="ignore", divide="ignore"): + bn, rs, ax, at = (f["bb_norm"].to_numpy(float), f["mr_rsi"].to_numpy(float), + f["mr_adx"].to_numpy(float), f["atr"].to_numpy(float)) + expected = np.abs(c - mid) / c + base = (ax <= p.adx_max) & (at > 0) & (expected >= 2 * 0.0011) + long_m = np.nan_to_num((bn <= -p.bb_extreme) & (rs <= p.rsi_os) & base, nan=False) + short_m = np.nan_to_num((bn >= p.bb_extreme) & (rs >= p.rsi_ob) & base, nan=False) + mask = (long_m | short_m).astype(bool) + sides = np.where(long_m, "long", "short") + sl_map, tp_map = {}, {} + for i in np.flatnonzero(mask): + if sides[i] == "long": + sl_map[int(i)] = min(lo[i] - p.sl_buffer_atr * at[i], c[i] * (1 - p.sl_min_pct / 100)) + else: + sl_map[int(i)] = max(up[i] + p.sl_buffer_atr * at[i], c[i] * (1 + p.sl_min_pct / 100)) + tp_map[int(i)] = mid[i] + return ts_trades(sym, d, mask, sides, 8, sl_map=sl_map, tp_map=tp_map) + return _run_ts(fn) + + +METHODS = { + "xs_reversal_15m": m_xs_reversal, + "xs_momentum_15m": m_xs_momentum, + "hour_xs_reversal": m_hour_xs_reversal, + "xs_reversal_costaware": m_xs_reversal_costaware, + "intraday_momentum": m_intraday_momentum, + "lead_lag_btc": m_lead_lag_btc, + "pairs_btc_eth": m_pairs_btc_eth, + "ou_ema_revert": m_ou_ema, + "ou_ema_costaware": m_ou_ema_costaware, + "vwap_revert": m_vwap_revert, + "clv_flow": m_clv_flow, + "liquidity_sweep": m_sweep, + "vol_breakout": m_vol_breakout, + "prod_mr_5m": m_prod_mr_5m, +} + + +def main() -> int: + import argparse + ap = argparse.ArgumentParser() + ap.add_argument("--maker", action="store_true", help="считать издержки по мейкеру (0,02% на ногу)") + ap.add_argument("methods", nargs="*") + args = ap.parse_args() + want = args.methods or list(METHODS) + tag = "_maker" if args.maker else "" + + print("сканирую вселенную...", flush=True) + mkt = H.build_matrix() + liquid = mkt["liquid"] + print(f"пар {len(mkt['syms'])} | 5м-сетка {len(mkt['grid'])} интервалов | ликвидных (топ-30): {len(liquid)}", + flush=True) + + rows, tests = [], len(want) + for name in want: + raw = METHODS[name](mkt) + if name == "xs_reversal_15m": # контроли привязаны к базовому методу + place_raw = m_xs_reversal_placebo(mkt) + rand_raw = m_xs_reversal_random(mkt) + tests += 2 + else: + place_raw = rand_raw = pd.DataFrame(columns=EMPTY) + tr = H.apply_costs(raw, liquid, maker=args.maker) + pl = H.apply_costs(place_raw, liquid, maker=args.maker) + rn = H.apply_costs(rand_raw, liquid, maker=args.maker) + H.save_trades(name + tag, tr) + row = H.summarize(name, tr, pl, rn) + rows.append(row) + os.makedirs(os.path.join(ROOT, "results", "rows"), exist_ok=True) + pd.DataFrame([row])[["method", "n_trades", "ev_bp", "ev_bp_oos", "t_boot", "placebo_ev_bp", + "rand_ev_bp", "sharpe", "max_dd_pct", "by_year_ok"]].to_csv( + os.path.join(ROOT, "results", "rows", f"{name}{tag}.csv"), index=False) + print(f"{name}: сделок {row['n_trades']}, EV {row['ev_bp']} б.п., OOS {row['ev_bp_oos']}, " + f"t {row['t_boot']}, плацебо {row['placebo_ev_bp']}, рандом {row['rand_ev_bp']}, " + f"Sharpe {row['sharpe']}, by_year {row['by_year_ok']}", flush=True) + del raw, tr, pl, rn + if place_raw is not None and not place_raw.empty: + del place_raw, rand_raw + + cols = ["method", "n_trades", "ev_bp", "ev_bp_oos", "t_boot", "placebo_ev_bp", "rand_ev_bp", + "sharpe", "max_dd_pct", "by_year_ok"] + os.makedirs(os.path.join(ROOT, "results"), exist_ok=True) + pd.DataFrame(rows)[cols].to_csv(os.path.join(ROOT, "results", f"summary_5m{tag}.csv"), index=False) + H.save_by_year(rows, tag=tag) + H.save_counts(tests, tag=tag) + with open(os.path.join(ROOT, "results", f"summary_5m{tag}.json"), "w") as fh: + json.dump(rows, fh, ensure_ascii=False, indent=1) + print(f"записано results/summary_5m{tag}.csv, reports/by_year{tag}.csv, reports/count_tests{tag}.json") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/code/mr_engine.py b/code/mr_engine.py new file mode 100644 index 0000000..a434ad0 --- /dev/null +++ b/code/mr_engine.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +"""Стенд 5м-исследования: независимая реализация движка и утилиты. + +Здесь: + * ресемпл 5м → 30м (полные бары по 12 свечей) для сверки с прод-движком TON-бота; + * индикаторы Wilder (RSI/ADX/ATR) и полосы Боллинджера — считаются сами, без импорта прод-кода; + * НЕЗАВИСИМАЯ реализация машины состояний MR (те же правила) — чтобы сверка движка ловила + именно расхождения в состоянии, а не в правилах входа. + +Правила MR (совпадают с прод-модулем app/trading/mean_reversion.py): + вход только в боковике (ADX <= adx_max), у внешней полосы (|bb_norm| >= bb_extreme) + с подтверждением RSI, при ожидаемом движении до средней >= min_move_fee_mult × fee_rt, + SL за полосой + sl_buffer_atr×ATR с флором sl_min_pct, TP1 = средняя, TP2 = противоположная полоса; + один вход одновременно, окно исхода horizon_bars, кулдаун cooldown_bars, дневной лимит daily_max, + после SL — пауза до возврата к средней; комиссия вычитается из результата сделки. +""" +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import pandas as pd + +BARS_30M = 6 # 30м = 6 свечей по 5м (1ч = 12 баров) + + +# ---------------------------------------------------------------- данные ---- +def load_5m(path: str) -> pd.DataFrame: + df = pd.read_csv(path, header=None, names=["timestamp", "open", "high", "low", "close", "volume"]) + df = df.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True) + return df + + +def resample_30m(df5: pd.DataFrame) -> pd.DataFrame: + """5м → 30м: берём только полные бары (ровно 12 свечей) — неполные отбрасываем.""" + g = df5.groupby(df5["timestamp"] // (30 * 60 * 1000)) + out = pd.DataFrame({ + "timestamp": g["timestamp"].first(), + "open": g["open"].first(), + "high": g["high"].max(), + "low": g["low"].min(), + "close": g["close"].last(), + "volume": g["volume"].sum(), + "bars": g["close"].count(), + }) + return out[out["bars"] == BARS_30M].drop(columns=["bars"]).reset_index(drop=True) + + +# ------------------------------------------------------------ индикаторы ---- +def _wilder(x: pd.Series, period: int) -> pd.Series: + return x.ewm(alpha=1.0 / period, adjust=False, min_periods=period).mean() + + +def rsi(close: pd.Series, period: int = 14) -> pd.Series: + diff = close.diff() + up = diff.clip(lower=0.0) + down = (-diff).clip(lower=0.0) + rs = _wilder(up, period) / _wilder(down, period).replace(0.0, np.nan) + return (100 - 100 / (1 + rs)).fillna(50.0) + + +def atr(df: pd.DataFrame, period: int = 14) -> pd.Series: + prev_close = df["close"].shift(1) + tr = pd.concat([(df["high"] - df["low"]), + (df["high"] - prev_close).abs(), + (df["low"] - prev_close).abs()], axis=1).max(axis=1) + return _wilder(tr, period) + + +def adx(df: pd.DataFrame, period: int = 14) -> pd.Series: + up_move = df["high"].diff() + down_move = -df["low"].diff() + plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0) + minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0) + tr = pd.concat([(df["high"] - df["low"]), + (df["high"] - df["close"].shift(1)).abs(), + (df["low"] - df["close"].shift(1)).abs()], axis=1).max(axis=1) + atr_w = _wilder(tr, period) + plus_di = 100 * _wilder(pd.Series(plus_dm, index=df.index), period) / atr_w + minus_di = 100 * _wilder(pd.Series(minus_dm, index=df.index), period) / atr_w + dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0.0, np.nan) + return _wilder(dx, period).fillna(0.0) + + +def bollinger(close: pd.Series, period: int = 20, std_mult: float = 2.0) -> pd.DataFrame: + mid = close.rolling(period).mean() + sd = close.rolling(period).std(ddof=0) + half = (mid + std_mult * sd - mid).replace(0.0, np.nan) + return pd.DataFrame({ + "bb_middle": mid, + "bb_upper": mid + std_mult * sd, + "bb_lower": mid - std_mult * sd, + "bb_norm": ((close - mid) / half).clip(-3.0, 3.0), + }) + + +def prepare_frame(df30: pd.DataFrame) -> pd.DataFrame: + """Кадр с индикаторами в терминах прод-модуля (0-100 шкала rsi/adx).""" + out = df30.copy() + out["atr"] = atr(out) + out["trend_rsi14"] = rsi(out["close"]) # 0-100 (>1.5 → прод не пересчитывает) + out["trend_adx"] = adx(out) # 0-100 + out = pd.concat([out, bollinger(out["close"])], axis=1) + out["mr_rsi"] = out["trend_rsi14"] + out["mr_adx"] = out["trend_adx"] + return out + + +# ---------------------------------------------------------------- движок ---- +@dataclass +class MRParamsStand: + bb_period: int = 20 + bb_std: float = 2.0 + bb_extreme: float = 0.8 + adx_max: float = 20.0 + rsi_ob: float = 70.0 + rsi_os: float = 30.0 + sl_buffer_atr: float = 0.3 + sl_min_pct: float = 0.5 + min_move_fee_mult: float = 2.0 + cooldown_bars: int = 8 + daily_max: int = 2 + horizon_bars: int = 8 + + +def pnl_sl_first(action: str, entry: float, sl: float, tp1: float, + highs, lows, close_end: float): + """Исход сделки по пути (стоп проверяется ПЕРВЫМ). Возвращает (pnl_доля, направление рынка).""" + hi = max(highs) if len(highs) else close_end + lo = min(lows) if len(lows) else close_end + if action == "LONG": + if lo <= sl: + return (sl - entry) / entry, -1 + if hi >= tp1: + return (tp1 - entry) / entry, 1 + return (close_end - entry) / entry, (1 if close_end > entry else -1) + else: + if hi >= sl: + return (entry - sl) / entry, 1 + if lo <= tp1: + return (entry - tp1) / entry, -1 + return (entry - close_end) / entry, (-1 if close_end < entry else 1) + + +def entry_signal(i: int, frame: pd.DataFrame, p: MRParamsStand, fee_rt: float): + """Возвращает (action, entry, sl, tp1, tp2, reason). Чистая функция от бара i.""" + row = frame.iloc[i] + price, bb_norm = float(row["close"]), float(row["bb_norm"]) + rsi_v, adx_v = float(row["mr_rsi"]), float(row["mr_adx"]) + atr_v = float(row["atr"]) if pd.notna(row["atr"]) else 0.0 + mid, upper, lower = float(row["bb_middle"]), float(row["bb_upper"]), float(row["bb_lower"]) + if any(pd.isna(v) for v in (bb_norm, mid, upper, lower, price)): + return None, "warmup" + if adx_v > p.adx_max: + return None, "adx_trend" + if atr_v <= 0: + return None, "no_atr" + if bb_norm >= p.bb_extreme and rsi_v >= p.rsi_ob: + action = "SHORT" + elif bb_norm <= -p.bb_extreme and rsi_v <= p.rsi_os: + action = "LONG" + else: + return None, "no_extreme" + expected = abs(price - mid) / price + if expected < p.min_move_fee_mult * fee_rt: + return None, "fee_gate" + if action == "SHORT": + sl, tp1, tp2 = upper + p.sl_buffer_atr * atr_v, mid, lower + else: + sl, tp1, tp2 = lower - p.sl_buffer_atr * atr_v, mid, upper + min_dist = price * p.sl_min_pct / 100.0 + sl = max(sl, price + min_dist) if action == "SHORT" else min(sl, price - min_dist) + if action == "SHORT": + ok = sl > price > tp1 + else: + ok = sl < price < tp1 + if not ok: + return None, "invalid_tpsl" + return (action, price, sl, tp1, tp2), "ok" + + +def stand_mr_backtest(frame: pd.DataFrame, p: MRParamsStand, fee_rt: float) -> dict: + """Независимая машина состояний (та же семантика, что у прод-движка).""" + n = len(frame) + closes = frame["close"].to_numpy(float) + highs = frame["high"].to_numpy(float) + lows = frame["low"].to_numpy(float) + bb_norm = frame["bb_norm"].to_numpy(float) + ts = frame["timestamp"].to_numpy() + days = pd.to_datetime(ts, unit="ms", utc=True).strftime("%Y-%m-%d").to_numpy() + + trades = [] + next_free_bar, day_entries, current_day, sl_pause_sign = 0, 0, None, 0 + warmup = max(p.bb_period + 1, 15) + + for i in range(warmup, n): + if days[i] != current_day: + current_day = days[i] + day_entries = 0 + if i < next_free_bar: + continue + if sl_pause_sign != 0: + bn = bb_norm[i] + if not np.isnan(bn) and (abs(bn) <= 0.1 or bn * sl_pause_sign < 0): + sl_pause_sign = 0 + else: + continue + sig, reason = entry_signal(i, frame, p, fee_rt) + if sig is None: + continue + action, entry, sl, tp1, _tp2 = sig + if day_entries >= p.daily_max: + continue + j0, j1 = i + 1, min(i + 1 + p.horizon_bars, n) + pnl, market = pnl_sl_first(action, entry, sl, tp1, highs[j0:j1], lows[j0:j1], closes[min(j1, n - 1)]) + pred = 1 if action == "LONG" else -1 + trades.append({"i": i, "pred": pred, "actual": market, "pnl": pnl - fee_rt, + "action": action, "ts": str(days[i])}) + if market == -pred: + sl_pause_sign = -pred + day_entries += 1 + next_free_bar = j1 + p.cooldown_bars + return {"trades": trades, "metrics": metrics(trades)} + + +def metrics(trades) -> dict: + pnl = [float(t["pnl"]) for t in trades] + wins = [x for x in pnl if x > 0] + losses = [abs(x) for x in pnl if x < 0] + total_win, total_loss = sum(wins), sum(losses) or 1e-10 + directed = [t for t in trades if t.get("actual") in (1, -1)] + da = (sum(1 for t in directed if t["pred"] == t["actual"]) / len(directed)) if directed else 0.0 + return {"total_trades": len(trades), "profit_factor": min(total_win / total_loss, 10.0), + "win_rate": len(wins) / len(trades) if trades else 0.0, "sum_pnl": sum(pnl), + "avg_pnl": sum(pnl) / len(pnl) if pnl else 0.0, "da": da, + "total_win": total_win, "total_loss": total_loss} diff --git a/code/reconcile.py b/code/reconcile.py new file mode 100644 index 0000000..12f6530 --- /dev/null +++ b/code/reconcile.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Сверка движка 5м-стенда с ПРОДОВЫМ движком TON-бота (обязательный шаг Ф1). + +Делает ровно то, что требует цель: берёт 5м-данные, ресемплит в 30м, считает индикаторы +нашим стендом и прогоняет ОДНИ И ТЕ ЖЕ правила двумя движками: + * прод: app.trading.mean_reversion.run_mr_backtest (импорт из /opt/data/work/tonbot_src); + * стенд: tools/mr_engine.stand_mr_backtest (независимая машина состояний). + +Расхождение PF больше 5% означает ошибку в машине состояний (не в правилах входа) — тогда +правится движок, а не цифры. + +Результат: reports/stand_reconcile.json {pairs: {SYM: {prod_n, stand_n, prod_pf, stand_pf}}} +""" +from __future__ import annotations + +import json +import os +import sys + +import pandas as pd + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, ROOT) +sys.path.insert(0, "/opt/data/work/tonbot_src") + +from tools.mr_engine import (MRParamsStand, load_5m, prepare_frame, resample_30m, # noqa: E402 + metrics as stand_metrics, stand_mr_backtest) +from app.trading.mean_reversion import MRParams, prepare_mr_frame, run_mr_backtest # noqa: E402 + +FEE_RT = 0.0011 # 0,055% тейкер × 2 ноги = 0,11% на круг + + +def main() -> int: + pairs = sys.argv[1:] or ["BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "ZECUSDT"] + out = {} + for sym in pairs: + path = os.path.join(ROOT, "data", "5m", f"{sym}_5m.csv") + if not os.path.isfile(path): + print(f"{sym}: нет файла 5м — пропуск") + continue + df30 = resample_30m(load_5m(path)) + frame = prepare_frame(df30) + + prod_frame = prepare_mr_frame(frame.copy(), MRParams()) + prod = run_mr_backtest(prod_frame, MRParams(), FEE_RT) + prod_m = prod["trades"] + + stand = stand_mr_backtest(frame, MRParamsStand(), FEE_RT) + stand_m = stand["metrics"] + + out[sym] = {"prod_n": int(prod_m["total_trades"]), "stand_n": int(stand_m["total_trades"]), + "prod_pf": round(float(prod_m["profit_factor"]), 4), + "stand_pf": round(float(stand_m["profit_factor"]), 4), + "prod_sum_pnl": round(float(prod_m.get("total_return", 0.0)), 4), + "stand_sum_pnl": round(float(stand_m["sum_pnl"]), 4), + "bars_30m": int(len(df30))} + print(f"{sym}: 30м баров {len(df30)}, прод n={out[sym]['prod_n']} PF={out[sym]['prod_pf']}, " + f"стенд n={out[sym]['stand_n']} PF={out[sym]['stand_pf']}") + + os.makedirs(os.path.join(ROOT, "reports"), exist_ok=True) + with open(os.path.join(ROOT, "reports", "stand_reconcile.json"), "w") as fh: + json.dump({"pairs": out, "fee_rt": FEE_RT, "engine": "app.trading.mean_reversion.run_mr_backtest", + "stand": "tools/mr_engine.stand_mr_backtest", "source": "30м из ресемпла 5м данных"}, + fh, ensure_ascii=False, indent=1) + print("записано reports/stand_reconcile.json, пар:", len(out)) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/code/run_methods.sh b/code/run_methods.sh new file mode 100755 index 0000000..d7566aa --- /dev/null +++ b/code/run_methods.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Прогон банка методов. Каждый метод — ОТДЕЛЬНЫМ процессом (иначе OOM на 250 парах), +# уже посчитанные строки пропускаются (results/rows/<метод><тег>.csv), затем мейкер и слияние. +set -u +cd /opt/data/work/screener5m || exit 1 +LOG=reports/methods_run.log +METHODS=(xs_reversal_15m xs_momentum_15m hour_xs_reversal xs_reversal_costaware intraday_momentum + lead_lag_btc pairs_btc_eth ou_ema_revert ou_ema_costaware vwap_revert clv_flow + liquidity_sweep vol_breakout prod_mr_5m) +echo "=== СТАРТ $(date '+%H:%M:%S') ===" >> "$LOG" +for mode in taker maker; do + tag=""; flag="" + if [ "$mode" = "maker" ]; then tag="_maker"; flag="--maker"; fi + for m in "${METHODS[@]}"; do + row="results/rows/${m}${tag}.csv" + if [ -f "$row" ]; then + echo "--- пропуск $mode $m (строка уже есть)" >> "$LOG" + continue + fi + echo "--- $mode $m $(date '+%H:%M:%S')" >> "$LOG" + .venv/bin/python tools/methods.py $flag "$m" >> "$LOG" 2>&1 || echo "!!! $mode $m упал" >> "$LOG" + done +done +echo "=== СЛИЯНИЕ $(date '+%H:%M:%S') ===" >> "$LOG" +.venv/bin/python tools/merge_rows.py >> "$LOG" 2>&1 +.venv/bin/python tools/merge_byyear.py >> "$LOG" 2>&1 +.venv/bin/python tools/make_report.py >> "$LOG" 2>&1 +echo "=== ГОТОВО $(date '+%H:%M:%S') ===" >> "$LOG" diff --git a/docs/REJECTED.md b/docs/REJECTED.md new file mode 100644 index 0000000..1489b35 --- /dev/null +++ b/docs/REJECTED.md @@ -0,0 +1,25 @@ +# Опровергнуто: методы, не прошедшие порог + +Прогон по 250 USDT-перпам Bybit (5м, 2024-10-01 … 2026-09-28). Издержки вычтены из каждой сделки. +Порог приёмки: EV > 0 после тейкера, плацебо ≈ 0, |t| ≥ порога Бонферрони, плюс в ≥ 2 из 3 лет. + +| Метод | сделок | EV, б.п. | EV OOS | t (бутстрап) | плацебо, б.п. | рандом, б.п. | EV мейкер | Причина отказа | +|---|---|---|---|---|---|---|---|---| +| prod_mr_5m | 105089 | -4.46 | -5.19 | -7.65 | 0.0 | 0.0 | 2.54 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| clv_flow | 257017 | -9.88 | -12.24 | -4.15 | 0.0 | 0.0 | -2.88 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| pairs_btc_eth | 1944 | -13.22 | -9.81 | -5.85 | 0.0 | 0.0 | -6.22 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| intraday_momentum | 125771 | -13.8 | -9.2 | -7.35 | 0.0 | 0.0 | -6.8 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| xs_reversal_15m | 873500 | -14.29 | -14.31 | -29.1 | -16.69 | -16.26 | -7.29 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| xs_reversal_costaware | 873400 | -14.29 | -14.31 | -29.1 | 0.0 | 0.0 | -7.29 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| hour_xs_reversal | 291200 | -14.7 | -14.77 | -35.34 | 0.0 | 0.0 | -7.7 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| ou_ema_costaware | 1047089 | -14.74 | -13.66 | -19.88 | 0.0 | 0.0 | -7.74 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| ou_ema_revert | 1123903 | -14.84 | -13.81 | -21.05 | 0.0 | 0.0 | -7.84 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| lead_lag_btc | 2311277 | -15.64 | -15.67 | -31.17 | 0.0 | 0.0 | -8.64 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| liquidity_sweep | 1166054 | -15.85 | -16.0 | -34.55 | 0.0 | 0.0 | -8.85 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| vwap_revert | 2655400 | -16.42 | -16.35 | -28.62 | 0.0 | 0.0 | -9.42 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| vol_breakout | 738494 | -18.51 | -19.43 | -24.03 | 0.0 | 0.0 | -11.51 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | +| xs_momentum_15m | 873500 | -18.87 | -19.04 | -38.48 | 0.0 | 0.0 | -11.87 | отрицательное матожидание после издержек; OOS тоже минус; плюс менее чем в 2 из 3 лет | + +Методы перечислены вместе с контролем: плацебо — то же правило на сдвинутом вперёд сигнале, +рандом — случайные входы той же частоты. Совпадение EV метода с плацебо означает, что +измерялся дрейф рынка, а не правило. diff --git a/reports/COSTS.md b/reports/COSTS.md new file mode 100644 index 0000000..3828b2f --- /dev/null +++ b/reports/COSTS.md @@ -0,0 +1,49 @@ +# Планка издержек на 5-минутных свечах + +Посчитано скриптом `tools/costs.py` по 249 парам 36 328 179 барам 5м (данные Bybit). + +## Что стоит торговля + +- Комиссия тейкера: 0.055% на ногу, **0.11% на круг** (вход+выход); +- Комиссия мейкера: 0.02% на ногу, **0.04% на круг**; +- Проскальзывание: 1 б.п. на ногу для топ-30 по обороту, 3 б.п. для остальных пар; +- Funding учитывается только при удержании через 00/08/16 UTC (0,006% за 8ч на BTC — на фоне комиссии незначимо, но в движке учтено). + +## Типичные движения (факт по данным) + +- Средний модуль доходности за 5м: **25.81 б.п.** (медиана по парам 23.33 б.п.); +- Средний модуль доходности за 1 час: **90.1 б.п.** + +Вывод: комиссия на круг (11 б.п.) сопоставима с типичным движением за 5 минут. Значит «скальпинг по одному 5м-бару» убыточен по построению, а осмысленны конструкции, где цель в 3–10 раз больше комиссии (сигнал на 5м, удержание десятки минут) либо мейкер-исполнение, либо кросс-секционные корзины. + +## Порог безубыточной точности p* = (Стоп + Издержки) / (Цель + Стоп) + +| Цель | Стоп | Тейкер (11 б.п.) | Мейкер (4 б.п.) | +|---|---|---|---| +| 0.1% | 0.1% | нужно выигрывать 105.0% | нужно выигрывать 70.0% | +| 0.2% | 0.2% | нужно выигрывать 77.5% | нужно выигрывать 60.0% | +| 0.3% | 0.2% | нужно выигрывать 62.0% | нужно выигрывать 48.0% | +| 0.5% | 0.3% | нужно выигрывать 51.2% | нужно выигрывать 42.5% | +| 1.0% | 0.5% | нужно выигрывать 40.7% | нужно выигрывать 36.0% | +| 1.0% | 1.0% | нужно выигрывать 55.5% | нужно выигрывать 52.0% | +| 2.0% | 1.0% | нужно выигрывать 37.0% | нужно выигрывать 34.7% | + +## Топ-15 пар по среднему движению 5м + +| Пара | баров | |r5м|, б.п. | σ 5м, б.п. | |r1ч|, б.п. | +|---|---|---|---|---| +| 4STOCKUSDT | 5596 | 98.97 | 156.51 | 345.70 | +| LONGXIAUSDT | 4181 | 83.76 | 138.74 | 294.47 | +| NIULAIUSDT | 9092 | 74.86 | 118.64 | 263.87 | +| MARSCOINUSDT | 7898 | 73.25 | 121.57 | 265.85 | +| BTWUSDT | 33271 | 71.21 | 126.09 | 245.68 | +| CASHCATUSDT | 15682 | 71.03 | 107.43 | 256.14 | +| PONSUSDT | 8201 | 62.01 | 89.07 | 215.86 | +| BEATUSDT | 89701 | 58.67 | 96.51 | 203.93 | +| LABUSDT | 96920 | 53.50 | 107.38 | 188.50 | +| STONKUSDT | 3036 | 52.80 | 75.32 | 243.38 | +| AKEUSDT | 104785 | 49.21 | 102.05 | 173.05 | +| RIVERUSDT | 98326 | 48.23 | 83.57 | 167.50 | +| HUSDT | 132627 | 46.78 | 89.22 | 160.55 | +| PIPPINUSDT | 178429 | 45.00 | 91.74 | 156.84 | +| CAPUSDT | 26421 | 44.47 | 78.81 | 154.40 | diff --git a/reports/PLACEBO.md b/reports/PLACEBO.md new file mode 100644 index 0000000..07a2779 --- /dev/null +++ b/reports/PLACEBO.md @@ -0,0 +1,22 @@ +# Плацебо- и рандом-контроль + +Плацебо — правило, применённое к сигналу, сдвинутому вперёд на горизонт удержания. +Рандом — случайные входы той же частоты и направления. Если EV метода близко к плацебо, +значит «эдж» был движением рынка, а не сигналом. + +| Метод | EV метода | плацебо | рандом | вердикт контроля | +|---|---|---|---|---| +| prod_mr_5m | -4.46 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| clv_flow | -9.88 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| pairs_btc_eth | -13.22 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| intraday_momentum | -13.8 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| xs_reversal_15m | -14.29 | -16.69 | -16.26 | метод не отличается от плацебо | +| xs_reversal_costaware | -14.29 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| hour_xs_reversal | -14.7 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| ou_ema_costaware | -14.74 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| ou_ema_revert | -14.84 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| lead_lag_btc | -15.64 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| liquidity_sweep | -15.85 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| vwap_revert | -16.42 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| vol_breakout | -18.51 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | +| xs_momentum_15m | -18.87 | 0.0 | 0.0 | контроль не проведён (не базовый метод) | diff --git a/reports/by_year.csv b/reports/by_year.csv new file mode 100644 index 0000000..1339dd1 --- /dev/null +++ b/reports/by_year.csv @@ -0,0 +1,85 @@ +method,year,ev_bp,n,n_trades +clv_flow,2024,-12.194,25421,25421 +clv_flow,2025,-7.451,114859,114859 +clv_flow,2026,-11.756,116737,116737 +clv_flow_maker,2024,-5.194,25421,25421 +clv_flow_maker,2025,-0.451,114859,114859 +clv_flow_maker,2026,-4.756,116737,116737 +hour_xs_reversal,2024,-13.138,36800,36800 +hour_xs_reversal,2025,-15.361,146000,146000 +hour_xs_reversal,2026,-14.353,108400,108400 +hour_xs_reversal_maker,2024,-6.138,36800,36800 +hour_xs_reversal_maker,2025,-8.361,146000,146000 +hour_xs_reversal_maker,2026,-7.353,108400,108400 +intraday_momentum,2024,-3.978,11098,11098 +intraday_momentum,2025,-21.422,56698,56698 +intraday_momentum,2026,-8.227,57975,57975 +intraday_momentum_maker,2024,3.022,11098,11098 +intraday_momentum_maker,2025,-14.422,56698,56698 +intraday_momentum_maker,2026,-1.227,57975,57975 +lead_lag_btc,2024,-16.295,203946,203946 +lead_lag_btc,2025,-15.415,1055623,1055623 +lead_lag_btc,2026,-15.744,1051708,1051708 +lead_lag_btc_maker,2024,-9.295,203946,203946 +lead_lag_btc_maker,2025,-8.415,1055623,1055623 +lead_lag_btc_maker,2026,-8.744,1051708,1051708 +liquidity_sweep,2024,-16.179,109657,109657 +liquidity_sweep,2025,-15.979,534572,534572 +liquidity_sweep,2026,-15.651,521825,521825 +liquidity_sweep_maker,2024,-9.179,109657,109657 +liquidity_sweep_maker,2025,-8.979,534572,534572 +liquidity_sweep_maker,2026,-8.651,521825,521825 +ou_ema_costaware,2024,-17.433,93868,93868 +ou_ema_costaware,2025,-15.616,477667,477667 +ou_ema_costaware,2026,-13.327,475554,475554 +ou_ema_costaware_maker,2024,-10.433,93868,93868 +ou_ema_costaware_maker,2025,-8.616,477667,477667 +ou_ema_costaware_maker,2026,-6.327,475554,475554 +ou_ema_revert,2024,-17.399,97940,97940 +ou_ema_revert,2025,-15.645,502849,502849 +ou_ema_revert,2026,-13.588,523114,523114 +ou_ema_revert_maker,2024,-10.399,97940,97940 +ou_ema_revert_maker,2025,-8.645,502849,502849 +ou_ema_revert_maker,2026,-6.588,523114,523114 +pairs_btc_eth,2024,-15.364,242,242 +pairs_btc_eth,2025,-14.429,990,990 +pairs_btc_eth,2026,-10.802,712,712 +pairs_btc_eth_maker,2024,-8.364,242,242 +pairs_btc_eth_maker,2025,-7.429,990,990 +pairs_btc_eth_maker,2026,-3.802,712,712 +prod_mr_5m,2024,-4.717,10186,10186 +prod_mr_5m,2025,-3.832,45902,45902 +prod_mr_5m,2026,-5.004,49001,49001 +prod_mr_5m_maker,2024,2.283,10186,10186 +prod_mr_5m_maker,2025,3.168,45902,45902 +prod_mr_5m_maker,2026,1.996,49001,49001 +vol_breakout,2024,-12.294,67189,67189 +vol_breakout,2025,-18.546,340933,340933 +vol_breakout,2026,-19.739,330372,330372 +vol_breakout_maker,2024,-5.294,67189,67189 +vol_breakout_maker,2025,-11.546,340933,340933 +vol_breakout_maker,2026,-12.739,330372,330372 +vwap_revert,2024,-16.574,196980,196980 +vwap_revert,2025,-16.542,1199981,1199981 +vwap_revert,2026,-16.273,1258439,1258439 +vwap_revert_maker,2024,-9.574,196980,196980 +vwap_revert_maker,2025,-9.542,1199981,1199981 +vwap_revert_maker,2026,-9.273,1258439,1258439 +xs_momentum_15m,2024,-18.51,110400,110400 +xs_momentum_15m,2025,-19.401,438000,438000 +xs_momentum_15m,2026,-18.289,325100,325100 +xs_momentum_15m_maker,2024,-11.51,110400,110400 +xs_momentum_15m_maker,2025,-12.401,438000,438000 +xs_momentum_15m_maker,2026,-11.289,325100,325100 +xs_reversal_15m,2024,-14.27,110400,110400 +xs_reversal_15m,2025,-13.727,438000,438000 +xs_reversal_15m,2026,-15.06,325100,325100 +xs_reversal_15m_maker,2024,-7.27,110400,110400 +xs_reversal_15m_maker,2025,-6.727,438000,438000 +xs_reversal_15m_maker,2026,-8.06,325100,325100 +xs_reversal_costaware,2024,-14.277,110300,110300 +xs_reversal_costaware,2025,-13.727,438000,438000 +xs_reversal_costaware,2026,-15.06,325100,325100 +xs_reversal_costaware_maker,2024,-7.277,110300,110300 +xs_reversal_costaware_maker,2025,-6.727,438000,438000 +xs_reversal_costaware_maker,2026,-8.06,325100,325100 diff --git a/reports/by_year_maker.csv b/reports/by_year_maker.csv new file mode 100644 index 0000000..5b7d938 --- /dev/null +++ b/reports/by_year_maker.csv @@ -0,0 +1,4 @@ +method,year,ev_bp,n +prod_mr_5m,2024,2.28,105089 +prod_mr_5m,2025,3.17,105089 +prod_mr_5m,2026,2.0,105089 diff --git a/reports/costs.json b/reports/costs.json new file mode 100644 index 0000000..a7785e1 --- /dev/null +++ b/reports/costs.json @@ -0,0 +1,2059 @@ +{ + "fees_pct_per_leg": { + "taker": 0.055, + "maker": 0.02 + }, + "fee_rt_pct": { + "taker": 0.11, + "maker": 0.04 + }, + "slippage_bp_per_leg": 1.0, + "slippage_bp_per_leg_illiquid": 3.0, + "mean_abs_r5m_bp": 25.81, + "mean_abs_r1h_bp": 90.1, + "aggregate": { + "pairs": 249, + "bars_total": 36328179, + "mean_abs_r5m_bp": 25.81, + "median_abs_r5m_bp": 23.33, + "mean_abs_r1h_bp": 90.1 + }, + "per_pair": [ + { + "symbol": "0GUSDT", + "bars": 108177, + "mean_abs_r5m_bp": 25.15922084804495, + "std_r5m_bp": 45.20265977909783, + "p95_abs_r5m_bp": 80.0066107179881, + "mean_abs_r1h_bp": 89.06802272984015 + }, + { + "symbol": "1000BONKUSDT", + "bars": 209631, + "mean_abs_r5m_bp": 25.6724522380972, + "std_r5m_bp": 43.0932934103404, + "p95_abs_r5m_bp": 77.63819119430737, + "mean_abs_r1h_bp": 88.26712566321937 + }, + { + "symbol": "1000FLOKIUSDT", + "bars": 209650, + "mean_abs_r5m_bp": 22.466372157626783, + "std_r5m_bp": 40.50072412698386, + "p95_abs_r5m_bp": 67.25296719752548, + "mean_abs_r1h_bp": 76.47628489409684 + }, + { + "symbol": "1000PEPEUSDT", + "bars": 208628, + "mean_abs_r5m_bp": 22.931072135103296, + "std_r5m_bp": 40.57606531538941, + "p95_abs_r5m_bp": 68.12007761845229, + "mean_abs_r1h_bp": 78.3879791646055 + }, + { + "symbol": "2ZUSDT", + "bars": 104060, + "mean_abs_r5m_bp": 21.977948343352644, + "std_r5m_bp": 39.55574185329813, + "p95_abs_r5m_bp": 66.99415680046276, + "mean_abs_r1h_bp": 77.09038630684879 + }, + { + "symbol": "4STOCKUSDT", + "bars": 5596, + "mean_abs_r5m_bp": 98.97043550513872, + "std_r5m_bp": 156.5100528565529, + "p95_abs_r5m_bp": 319.35735176351216, + "mean_abs_r1h_bp": 345.6968478665757 + }, + { + "symbol": "AAOIUSDT", + "bars": 32125, + "mean_abs_r5m_bp": 20.981125495910685, + "std_r5m_bp": 37.72673328980313, + "p95_abs_r5m_bp": 72.3160646071772, + "mean_abs_r1h_bp": 75.7895055979138 + }, + { + "symbol": "AAVEUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 20.326726213399272, + "std_r5m_bp": 34.72893730510991, + "p95_abs_r5m_bp": 59.71777104485376, + "mean_abs_r1h_bp": 69.84938509133237 + }, + { + "symbol": "ACEUSDT", + "bars": 209652, + "mean_abs_r5m_bp": 26.94442882876567, + "std_r5m_bp": 52.32230503475815, + "p95_abs_r5m_bp": 80.11114935675495, + "mean_abs_r1h_bp": 92.45705490191027 + }, + { + "symbol": "ADAUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 19.15042917724434, + "std_r5m_bp": 33.981456791651475, + "p95_abs_r5m_bp": 56.78693788680422, + "mean_abs_r1h_bp": 65.01350206747276 + }, + { + "symbol": "AEROUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 22.988751463723702, + "std_r5m_bp": 41.913207312666316, + "p95_abs_r5m_bp": 67.80202163109128, + "mean_abs_r1h_bp": 84.32235100650337 + }, + { + "symbol": "AGIUSDT", + "bars": 209641, + "mean_abs_r5m_bp": 23.788185409926257, + "std_r5m_bp": 41.35092786082496, + "p95_abs_r5m_bp": 75.47205635383136, + "mean_abs_r1h_bp": 80.6188848825379 + }, + { + "symbol": "AKEUSDT", + "bars": 104785, + "mean_abs_r5m_bp": 49.21314570025338, + "std_r5m_bp": 102.0535639230551, + "p95_abs_r5m_bp": 161.85324421018035, + "mean_abs_r1h_bp": 173.05333489863807 + }, + { + "symbol": "ALGOUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 20.7269981928512, + "std_r5m_bp": 33.1296335577068, + "p95_abs_r5m_bp": 62.3362451525091, + "mean_abs_r1h_bp": 71.16178333263595 + }, + { + "symbol": "ALLOUSDT", + "bars": 91705, + "mean_abs_r5m_bp": 33.04548853681781, + "std_r5m_bp": 57.32530797177805, + "p95_abs_r5m_bp": 105.80867284078714, + "mean_abs_r1h_bp": 115.24874294872009 + }, + { + "symbol": "AMDSTOCKUSDT", + "bars": 35577, + "mean_abs_r5m_bp": 11.715575370541822, + "std_r5m_bp": 22.341297971449084, + "p95_abs_r5m_bp": 43.672832826073815, + "mean_abs_r1h_bp": 44.97874413913174 + }, + { + "symbol": "APEUSDT", + "bars": 207635, + "mean_abs_r5m_bp": 21.921062516292174, + "std_r5m_bp": 41.460378962504635, + "p95_abs_r5m_bp": 63.70137337607058, + "mean_abs_r1h_bp": 75.85352520266767 + }, + { + "symbol": "APEXUSDT", + "bars": 105274, + "mean_abs_r5m_bp": 23.488766588032007, + "std_r5m_bp": 41.51910834772599, + "p95_abs_r5m_bp": 76.42824239451674, + "mean_abs_r1h_bp": 82.85251488047595 + }, + { + "symbol": "APPSTOCKUSDT", + "bars": 21464, + "mean_abs_r5m_bp": 10.864949541043595, + "std_r5m_bp": 29.424473239868554, + "p95_abs_r5m_bp": 38.43111173394485, + "mean_abs_r1h_bp": 33.99316333594129 + }, + { + "symbol": "APTUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 20.514650772174406, + "std_r5m_bp": 33.52274591477221, + "p95_abs_r5m_bp": 59.57036432803929, + "mean_abs_r1h_bp": 70.74339081873511 + }, + { + "symbol": "ARBUSDT", + "bars": 207628, + "mean_abs_r5m_bp": 21.489024853516664, + "std_r5m_bp": 39.82352101345332, + "p95_abs_r5m_bp": 62.85032302612237, + "mean_abs_r1h_bp": 74.2780093222745 + }, + { + "symbol": "ARKMUSDT", + "bars": 209653, + "mean_abs_r5m_bp": 24.376795759992532, + "std_r5m_bp": 43.15704980332897, + "p95_abs_r5m_bp": 71.39703188275196, + "mean_abs_r1h_bp": 83.93899901370382 + }, + { + "symbol": "ARKUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 20.390042460389658, + "std_r5m_bp": 36.5812583594449, + "p95_abs_r5m_bp": 64.56909218972125, + "mean_abs_r1h_bp": 69.47606960429431 + }, + { + "symbol": "ARUSDT", + "bars": 207635, + "mean_abs_r5m_bp": 23.84839649376678, + "std_r5m_bp": 46.14671184228223, + "p95_abs_r5m_bp": 69.80959001990338, + "mean_abs_r1h_bp": 82.15367837824957 + }, + { + "symbol": "ARXUSDT", + "bars": 28142, + "mean_abs_r5m_bp": 32.18580822952769, + "std_r5m_bp": 50.60412628474328, + "p95_abs_r5m_bp": 98.89772505181415, + "mean_abs_r1h_bp": 111.44931759250333 + }, + { + "symbol": "ASTERUSDT", + "bars": 107853, + "mean_abs_r5m_bp": 21.08954334849772, + "std_r5m_bp": 40.733646144480105, + "p95_abs_r5m_bp": 73.07606597116984, + "mean_abs_r1h_bp": 71.93310641031093 + }, + { + "symbol": "ATHUSDT", + "bars": 209646, + "mean_abs_r5m_bp": 23.759873237567945, + "std_r5m_bp": 38.70802819131798, + "p95_abs_r5m_bp": 71.47524641000646, + "mean_abs_r1h_bp": 83.20273800520039 + }, + { + "symbol": "ATOMUSDT", + "bars": 209630, + "mean_abs_r5m_bp": 17.314457439285416, + "std_r5m_bp": 30.30065437524329, + "p95_abs_r5m_bp": 50.54143669554899, + "mean_abs_r1h_bp": 58.90517433686419 + }, + { + "symbol": "AVAXUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 17.984356021364185, + "std_r5m_bp": 32.63607507657831, + "p95_abs_r5m_bp": 53.59273417132068, + "mean_abs_r1h_bp": 62.59993330202326 + }, + { + "symbol": "AVGOUSDT", + "bars": 40197, + "mean_abs_r5m_bp": 8.33711506285025, + "std_r5m_bp": 16.96650860651374, + "p95_abs_r5m_bp": 31.819668674670787, + "mean_abs_r1h_bp": 31.77582368504589 + }, + { + "symbol": "AVNTUSDT", + "bars": 110687, + "mean_abs_r5m_bp": 28.329402659002316, + "std_r5m_bp": 53.95747304807222, + "p95_abs_r5m_bp": 89.05161594958605, + "mean_abs_r1h_bp": 99.18108932939148 + }, + { + "symbol": "AXSUSDT", + "bars": 209642, + "mean_abs_r5m_bp": 20.471046291329234, + "std_r5m_bp": 34.8025837754036, + "p95_abs_r5m_bp": 60.7309144472068, + "mean_abs_r1h_bp": 70.44639802601736 + }, + { + "symbol": "AZTECUSDT", + "bars": 65825, + "mean_abs_r5m_bp": 22.96912505575546, + "std_r5m_bp": 40.486948193633054, + "p95_abs_r5m_bp": 70.58513214889746, + "mean_abs_r1h_bp": 80.39442020308687 + }, + { + "symbol": "B3USDT", + "bars": 170927, + "mean_abs_r5m_bp": 27.15301267156004, + "std_r5m_bp": 51.0939990743184, + "p95_abs_r5m_bp": 86.50365126877446, + "mean_abs_r1h_bp": 93.21660557240031 + }, + { + "symbol": "BANKUSDT", + "bars": 146469, + "mean_abs_r5m_bp": 32.26451819267088, + "std_r5m_bp": 66.73589928991531, + "p95_abs_r5m_bp": 103.41005636662908, + "mean_abs_r1h_bp": 113.82372725656116 + }, + { + "symbol": "BCHUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 15.462974188019153, + "std_r5m_bp": 24.7754168955133, + "p95_abs_r5m_bp": 46.80378297033092, + "mean_abs_r1h_bp": 53.382972001390456 + }, + { + "symbol": "BEAMUSDT", + "bars": 209654, + "mean_abs_r5m_bp": 23.097390443545663, + "std_r5m_bp": 37.15637033843373, + "p95_abs_r5m_bp": 68.66431198668896, + "mean_abs_r1h_bp": 80.721635551426 + }, + { + "symbol": "BEATUSDT", + "bars": 89701, + "mean_abs_r5m_bp": 58.669306663470955, + "std_r5m_bp": 96.50544517299227, + "p95_abs_r5m_bp": 184.88068707686833, + "mean_abs_r1h_bp": 203.92654258729587 + }, + { + "symbol": "BERAUSDT", + "bars": 172625, + "mean_abs_r5m_bp": 25.251510498293094, + "std_r5m_bp": 44.76219338494819, + "p95_abs_r5m_bp": 74.10963206211795, + "mean_abs_r1h_bp": 87.88234056613072 + }, + { + "symbol": "BEUSDT", + "bars": 35592, + "mean_abs_r5m_bp": 18.37970850861532, + "std_r5m_bp": 34.979841523874505, + "p95_abs_r5m_bp": 66.77715744071833, + "mean_abs_r1h_bp": 70.85371300110084 + }, + { + "symbol": "BILLUSDT", + "bars": 41945, + "mean_abs_r5m_bp": 38.7775761320244, + "std_r5m_bp": 64.31222777780891, + "p95_abs_r5m_bp": 126.67400909352165, + "mean_abs_r1h_bp": 134.4621616870197 + }, + { + "symbol": "BIOUSDT", + "bars": 185065, + "mean_abs_r5m_bp": 29.863975744842023, + "std_r5m_bp": 52.55672275253964, + "p95_abs_r5m_bp": 90.37650163630163, + "mean_abs_r1h_bp": 102.41785726918903 + }, + { + "symbol": "BNBUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 10.495905145243695, + "std_r5m_bp": 18.17483684475967, + "p95_abs_r5m_bp": 32.27133345008503, + "mean_abs_r1h_bp": 36.28858048022185 + }, + { + "symbol": "BRETTUSDT", + "bars": 209652, + "mean_abs_r5m_bp": 29.028464893680976, + "std_r5m_bp": 47.23381907541894, + "p95_abs_r5m_bp": 84.29832889283118, + "mean_abs_r1h_bp": 101.15111678104198 + }, + { + "symbol": "BROCCOLIUSDT", + "bars": 170326, + "mean_abs_r5m_bp": 34.183033048110914, + "std_r5m_bp": 73.01173550813331, + "p95_abs_r5m_bp": 107.47471027911875, + "mean_abs_r1h_bp": 116.35136322916213 + }, + { + "symbol": "BRUSDT", + "bars": 159511, + "mean_abs_r5m_bp": 31.362491156256308, + "std_r5m_bp": 68.66936907871747, + "p95_abs_r5m_bp": 104.1849124850965, + "mean_abs_r1h_bp": 110.10447933862501 + }, + { + "symbol": "BSVUSDT", + "bars": 209643, + "mean_abs_r5m_bp": 16.90832585352548, + "std_r5m_bp": 27.96549132260761, + "p95_abs_r5m_bp": 52.28721270309339, + "mean_abs_r1h_bp": 57.67095703030562 + }, + { + "symbol": "BTCUSDT", + "bars": 207626, + "mean_abs_r5m_bp": 9.095257468156106, + "std_r5m_bp": 14.351268429179699, + "p95_abs_r5m_bp": 28.080484133290888, + "mean_abs_r1h_bp": 31.008984187876106 + }, + { + "symbol": "BTWUSDT", + "bars": 33271, + "mean_abs_r5m_bp": 71.21424449150526, + "std_r5m_bp": 126.08842078557676, + "p95_abs_r5m_bp": 244.46429882268043, + "mean_abs_r1h_bp": 245.68120801034783 + }, + { + "symbol": "BXUSDT", + "bars": 21144, + "mean_abs_r5m_bp": 6.5738744207095605, + "std_r5m_bp": 14.073560311330262, + "p95_abs_r5m_bp": 28.424390838707307, + "mean_abs_r1h_bp": 25.63724800928537 + }, + { + "symbol": "CAKEUSDT", + "bars": 209635, + "mean_abs_r5m_bp": 17.967205858522096, + "std_r5m_bp": 31.10599468165697, + "p95_abs_r5m_bp": 54.89492284771469, + "mean_abs_r1h_bp": 62.584513474880566 + }, + { + "symbol": "CAPUSDT", + "bars": 26421, + "mean_abs_r5m_bp": 44.4690706833781, + "std_r5m_bp": 78.8083070264434, + "p95_abs_r5m_bp": 136.05960723476412, + "mean_abs_r1h_bp": 154.39566286886614 + }, + { + "symbol": "CASHCATUSDT", + "bars": 15682, + "mean_abs_r5m_bp": 71.02825359716066, + "std_r5m_bp": 107.43143975867066, + "p95_abs_r5m_bp": 204.7512227781212, + "mean_abs_r1h_bp": 256.1392364261782 + }, + { + "symbol": "CBRSUSDT", + "bars": 38177, + "mean_abs_r5m_bp": 18.149862504349574, + "std_r5m_bp": 34.64205345881344, + "p95_abs_r5m_bp": 63.96239907995094, + "mean_abs_r1h_bp": 66.54976740155497 + }, + { + "symbol": "CCUSDT", + "bars": 95698, + "mean_abs_r5m_bp": 22.041971050632483, + "std_r5m_bp": 38.46905325656179, + "p95_abs_r5m_bp": 74.93481317822723, + "mean_abs_r1h_bp": 77.33730037593384 + }, + { + "symbol": "CFGUSDT", + "bars": 56374, + "mean_abs_r5m_bp": 26.783865410656794, + "std_r5m_bp": 40.45422086332747, + "p95_abs_r5m_bp": 80.03459248002403, + "mean_abs_r1h_bp": 92.04950546616847 + }, + { + "symbol": "CHIPUSDT", + "bars": 47660, + "mean_abs_r5m_bp": 33.13324100893782, + "std_r5m_bp": 56.05877744987343, + "p95_abs_r5m_bp": 102.66768536487758, + "mean_abs_r1h_bp": 118.16993939547338 + }, + { + "symbol": "COMPUSDT", + "bars": 209653, + "mean_abs_r5m_bp": 18.857802279210404, + "std_r5m_bp": 33.0078415461869, + "p95_abs_r5m_bp": 56.00154671341205, + "mean_abs_r1h_bp": 64.61582626631031 + }, + { + "symbol": "COTIUSDT", + "bars": 209635, + "mean_abs_r5m_bp": 25.274224553486864, + "std_r5m_bp": 42.859143345874635, + "p95_abs_r5m_bp": 74.9034019013054, + "mean_abs_r1h_bp": 85.73443321007241 + }, + { + "symbol": "COWUSDT", + "bars": 198577, + "mean_abs_r5m_bp": 22.967873650050983, + "std_r5m_bp": 39.22376825167092, + "p95_abs_r5m_bp": 70.06964588511467, + "mean_abs_r1h_bp": 80.0551717658513 + }, + { + "symbol": "CPUSDT", + "bars": 7086, + "mean_abs_r5m_bp": 38.24247207630071, + "std_r5m_bp": 81.1237229354339, + "p95_abs_r5m_bp": 114.76242233275252, + "mean_abs_r1h_bp": 128.40435764827862 + }, + { + "symbol": "CROUSDT", + "bars": 207632, + "mean_abs_r5m_bp": 17.571263830144805, + "std_r5m_bp": 33.863000477842085, + "p95_abs_r5m_bp": 56.98021114637797, + "mean_abs_r1h_bp": 61.21147276766075 + }, + { + "symbol": "CRVUSDT", + "bars": 207629, + "mean_abs_r5m_bp": 23.633172241406168, + "std_r5m_bp": 41.4450423133466, + "p95_abs_r5m_bp": 71.15453348975458, + "mean_abs_r1h_bp": 81.57570042272106 + }, + { + "symbol": "CVXUSDT", + "bars": 209644, + "mean_abs_r5m_bp": 24.135967040913687, + "std_r5m_bp": 42.420911141882534, + "p95_abs_r5m_bp": 72.82994870110215, + "mean_abs_r1h_bp": 83.2717162529839 + }, + { + "symbol": "DASHUSDT", + "bars": 207629, + "mean_abs_r5m_bp": 23.445237776532064, + "std_r5m_bp": 39.660959298007505, + "p95_abs_r5m_bp": 73.28865976610489, + "mean_abs_r1h_bp": 79.68221869684683 + }, + { + "symbol": "DATAUSDT", + "bars": 23472, + "mean_abs_r5m_bp": 14.01273107007116, + "std_r5m_bp": 24.4326647271687, + "p95_abs_r5m_bp": 44.80628833899103, + "mean_abs_r1h_bp": 49.13082897174119 + }, + { + "symbol": "DEEPUSDT", + "bars": 204894, + "mean_abs_r5m_bp": 29.248519903279348, + "std_r5m_bp": 49.014223992260355, + "p95_abs_r5m_bp": 90.73355010390837, + "mean_abs_r1h_bp": 104.35715115717916 + }, + { + "symbol": "DGAIUSDT", + "bars": 7078, + "mean_abs_r5m_bp": 13.866162019593304, + "std_r5m_bp": 29.40812498685502, + "p95_abs_r5m_bp": 44.626740426447455, + "mean_abs_r1h_bp": 47.85724592783535 + }, + { + "symbol": "DOGEUSDT", + "bars": 206627, + "mean_abs_r5m_bp": 18.020024863429132, + "std_r5m_bp": 32.283639715297234, + "p95_abs_r5m_bp": 54.58300246754255, + "mean_abs_r1h_bp": 61.82485844598182 + }, + { + "symbol": "DOODUSDT", + "bars": 146130, + "mean_abs_r5m_bp": 25.497146117156227, + "std_r5m_bp": 48.279524530045585, + "p95_abs_r5m_bp": 79.89448614412184, + "mean_abs_r1h_bp": 91.61637633454193 + }, + { + "symbol": "DOTUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 18.34739683412528, + "std_r5m_bp": 33.96763880503077, + "p95_abs_r5m_bp": 53.956889919339744, + "mean_abs_r1h_bp": 62.704101526822946 + }, + { + "symbol": "DRAMUSDT", + "bars": 38154, + "mean_abs_r5m_bp": 16.843354466742582, + "std_r5m_bp": 27.480862374878367, + "p95_abs_r5m_bp": 56.95134198927045, + "mean_abs_r1h_bp": 60.457206035753146 + }, + { + "symbol": "DYDXUSDT", + "bars": 209654, + "mean_abs_r5m_bp": 23.06475067167828, + "std_r5m_bp": 49.62213259489168, + "p95_abs_r5m_bp": 67.24974907673986, + "mean_abs_r1h_bp": 80.01774533867273 + }, + { + "symbol": "EDGEUSDT", + "bars": 55508, + "mean_abs_r5m_bp": 34.173858815870446, + "std_r5m_bp": 60.715852591633, + "p95_abs_r5m_bp": 104.4557724147828, + "mean_abs_r1h_bp": 119.2970638502446 + }, + { + "symbol": "EGLDUSDT", + "bars": 209635, + "mean_abs_r5m_bp": 19.11497284008667, + "std_r5m_bp": 34.43746671899421, + "p95_abs_r5m_bp": 56.14447598132196, + "mean_abs_r1h_bp": 66.53732541594815 + }, + { + "symbol": "EIGENUSDT", + "bars": 209633, + "mean_abs_r5m_bp": 28.116993787686553, + "std_r5m_bp": 47.7804526774824, + "p95_abs_r5m_bp": 82.0286677453717, + "mean_abs_r1h_bp": 97.81969241894 + }, + { + "symbol": "ELSAUSDT", + "bars": 71897, + "mean_abs_r5m_bp": 21.65443112933327, + "std_r5m_bp": 37.53939756302066, + "p95_abs_r5m_bp": 68.58808293783491, + "mean_abs_r1h_bp": 76.4098570789178 + }, + { + "symbol": "ENAUSDT", + "bars": 205627, + "mean_abs_r5m_bp": 27.90270416989622, + "std_r5m_bp": 49.9006284316975, + "p95_abs_r5m_bp": 82.03023073472093, + "mean_abs_r1h_bp": 95.99560477625349 + }, + { + "symbol": "ENSUSDT", + "bars": 209644, + "mean_abs_r5m_bp": 20.215550175841148, + "std_r5m_bp": 35.47269124339342, + "p95_abs_r5m_bp": 60.17776545737584, + "mean_abs_r1h_bp": 68.935361322934 + }, + { + "symbol": "ESPORTSUSDT", + "bars": 122837, + "mean_abs_r5m_bp": 40.953579856571885, + "std_r5m_bp": 95.03448194294023, + "p95_abs_r5m_bp": 132.3995212515816, + "mean_abs_r1h_bp": 145.39466505475184 + }, + { + "symbol": "ETCUSDT", + "bars": 208631, + "mean_abs_r5m_bp": 15.869517658009054, + "std_r5m_bp": 26.282203897441267, + "p95_abs_r5m_bp": 47.76756842277872, + "mean_abs_r1h_bp": 54.21092323881845 + }, + { + "symbol": "ETHFIUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 26.051517025095514, + "std_r5m_bp": 51.370525445215335, + "p95_abs_r5m_bp": 75.64734183204553, + "mean_abs_r1h_bp": 89.35274025919443 + }, + { + "symbol": "ETHUSDT", + "bars": 208626, + "mean_abs_r5m_bp": 13.34559520716272, + "std_r5m_bp": 20.99190645966559, + "p95_abs_r5m_bp": 40.681642265727895, + "mean_abs_r1h_bp": 45.691493152841424 + }, + { + "symbol": "EULUSDT", + "bars": 100681, + "mean_abs_r5m_bp": 26.181562618079052, + "std_r5m_bp": 44.38263532968191, + "p95_abs_r5m_bp": 80.9540759257752, + "mean_abs_r1h_bp": 91.21421743101793 + }, + { + "symbol": "EWYUSDT", + "bars": 41623, + "mean_abs_r5m_bp": 12.618125739851369, + "std_r5m_bp": 20.940892369847422, + "p95_abs_r5m_bp": 44.1114289031894, + "mean_abs_r1h_bp": 44.827967637582894 + }, + { + "symbol": "FARTCOINUSDT", + "bars": 186477, + "mean_abs_r5m_bp": 37.99303708584084, + "std_r5m_bp": 66.39176737951871, + "p95_abs_r5m_bp": 116.44042966535679, + "mean_abs_r1h_bp": 130.81313048331694 + }, + { + "symbol": "FFUSDT", + "bars": 104914, + "mean_abs_r5m_bp": 21.939239790361217, + "std_r5m_bp": 45.18463065407809, + "p95_abs_r5m_bp": 72.61604563652166, + "mean_abs_r1h_bp": 78.75634233647733 + }, + { + "symbol": "FILUSDT", + "bars": 209630, + "mean_abs_r5m_bp": 19.824136261690878, + "std_r5m_bp": 38.10235149910635, + "p95_abs_r5m_bp": 58.619527359273555, + "mean_abs_r1h_bp": 67.81323144531325 + }, + { + "symbol": "FLOCKUSDT", + "bars": 180710, + "mean_abs_r5m_bp": 33.21077980004627, + "std_r5m_bp": 60.566989881109265, + "p95_abs_r5m_bp": 107.3355564310896, + "mean_abs_r1h_bp": 114.04870180307714 + }, + { + "symbol": "FOGOUSDT", + "bars": 75062, + "mean_abs_r5m_bp": 23.368499820105832, + "std_r5m_bp": 38.139678325001455, + "p95_abs_r5m_bp": 70.17572658646554, + "mean_abs_r1h_bp": 82.6643809738643 + }, + { + "symbol": "GALAUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 22.373027652644115, + "std_r5m_bp": 44.21520256167105, + "p95_abs_r5m_bp": 65.4309858893587, + "mean_abs_r1h_bp": 76.13929116668623 + }, + { + "symbol": "GIGGLEUSDT", + "bars": 102077, + "mean_abs_r5m_bp": 31.998596305407414, + "std_r5m_bp": 64.46738923840094, + "p95_abs_r5m_bp": 101.86630906574456, + "mean_abs_r1h_bp": 111.31349747689269 + }, + { + "symbol": "GMTUSDT", + "bars": 209653, + "mean_abs_r5m_bp": 20.500632238973033, + "std_r5m_bp": 36.7591658771615, + "p95_abs_r5m_bp": 59.42653048928334, + "mean_abs_r1h_bp": 70.58392443146174 + }, + { + "symbol": "GRAMUSDT", + "bars": 27353, + "mean_abs_r5m_bp": 13.848121628609869, + "std_r5m_bp": 23.24195975950333, + "p95_abs_r5m_bp": 42.52309738530358, + "mean_abs_r1h_bp": 45.9797126494079 + }, + { + "symbol": "GRASSUSDT", + "bars": 204902, + "mean_abs_r5m_bp": 32.83101280990287, + "std_r5m_bp": 58.11432111043032, + "p95_abs_r5m_bp": 97.50321001269936, + "mean_abs_r1h_bp": 112.35583549851924 + }, + { + "symbol": "GRTUSDT", + "bars": 207629, + "mean_abs_r5m_bp": 19.633919110555098, + "std_r5m_bp": 32.17864547297628, + "p95_abs_r5m_bp": 57.54347041420759, + "mean_abs_r1h_bp": 67.86683831794426 + }, + { + "symbol": "GRVTUSDT", + "bars": 17121, + "mean_abs_r5m_bp": 29.229845544880927, + "std_r5m_bp": 47.21821742017209, + "p95_abs_r5m_bp": 88.75396327055682, + "mean_abs_r1h_bp": 103.92505352449592 + }, + { + "symbol": "HBARUSDT", + "bars": 208626, + "mean_abs_r5m_bp": 19.763316510849958, + "std_r5m_bp": 34.00964768091087, + "p95_abs_r5m_bp": 61.21758248575748, + "mean_abs_r1h_bp": 67.64008164150876 + }, + { + "symbol": "HNTUSDT", + "bars": 209646, + "mean_abs_r5m_bp": 23.592996028347944, + "std_r5m_bp": 39.94952319577377, + "p95_abs_r5m_bp": 74.17121569621547, + "mean_abs_r1h_bp": 83.16698148067604 + }, + { + "symbol": "HUMAUSDT", + "bars": 141230, + "mean_abs_r5m_bp": 26.23380060321176, + "std_r5m_bp": 47.432023160817565, + "p95_abs_r5m_bp": 77.8000086905477, + "mean_abs_r1h_bp": 90.6832044934436 + }, + { + "symbol": "HUSDT", + "bars": 132627, + "mean_abs_r5m_bp": 46.783710733639836, + "std_r5m_bp": 89.2249903982729, + "p95_abs_r5m_bp": 149.25528616288318, + "mean_abs_r1h_bp": 160.55355293739916 + }, + { + "symbol": "HYPEUSDT", + "bars": 188751, + "mean_abs_r5m_bp": 26.77755352629678, + "std_r5m_bp": 41.92560320299126, + "p95_abs_r5m_bp": 80.93696634100573, + "mean_abs_r1h_bp": 88.14106655711508 + }, + { + "symbol": "ICPUSDT", + "bars": 208630, + "mean_abs_r5m_bp": 20.985225810746186, + "std_r5m_bp": 34.240999083046574, + "p95_abs_r5m_bp": 62.19451665641296, + "mean_abs_r1h_bp": 71.39381836791276 + }, + { + "symbol": "IMXUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 22.221383303215564, + "std_r5m_bp": 36.76254720078059, + "p95_abs_r5m_bp": 64.25155420054193, + "mean_abs_r1h_bp": 76.72642728741462 + }, + { + "symbol": "INITUSDT", + "bars": 153344, + "mean_abs_r5m_bp": 26.179848717819407, + "std_r5m_bp": 46.67940729015928, + "p95_abs_r5m_bp": 79.73689910700813, + "mean_abs_r1h_bp": 91.26377315621995 + }, + { + "symbol": "INJUSDT", + "bars": 203629, + "mean_abs_r5m_bp": 23.260027705208994, + "std_r5m_bp": 41.98842050377858, + "p95_abs_r5m_bp": 67.3938152818769, + "mean_abs_r1h_bp": 79.63140078729234 + }, + { + "symbol": "INXUSDT", + "bars": 69842, + "mean_abs_r5m_bp": 28.300063876939408, + "std_r5m_bp": 53.70250348246505, + "p95_abs_r5m_bp": 85.54372096658547, + "mean_abs_r1h_bp": 101.29553010715952 + }, + { + "symbol": "IOTAUSDT", + "bars": 209633, + "mean_abs_r5m_bp": 20.63087454593468, + "std_r5m_bp": 34.95267579674865, + "p95_abs_r5m_bp": 61.9270815225987, + "mean_abs_r1h_bp": 71.35620146742468 + }, + { + "symbol": "IRYSUSDT", + "bars": 88020, + "mean_abs_r5m_bp": 29.595644649304628, + "std_r5m_bp": 49.904066523191965, + "p95_abs_r5m_bp": 93.52560748716326, + "mean_abs_r1h_bp": 104.3194105856313 + }, + { + "symbol": "JASMYUSDT", + "bars": 207633, + "mean_abs_r5m_bp": 22.70533120495374, + "std_r5m_bp": 39.140372188804356, + "p95_abs_r5m_bp": 68.40881964430679, + "mean_abs_r1h_bp": 77.50418744135365 + }, + { + "symbol": "JSTUSDT", + "bars": 209654, + "mean_abs_r5m_bp": 11.435247417230922, + "std_r5m_bp": 20.436892658125444, + "p95_abs_r5m_bp": 37.05537140889969, + "mean_abs_r1h_bp": 42.1590762436415 + }, + { + "symbol": "JTOUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 24.959309887583665, + "std_r5m_bp": 43.81871351590994, + "p95_abs_r5m_bp": 72.89373567720192, + "mean_abs_r1h_bp": 87.04339867502827 + }, + { + "symbol": "JUPUSDT", + "bars": 208629, + "mean_abs_r5m_bp": 23.300164619804285, + "std_r5m_bp": 43.33063486212613, + "p95_abs_r5m_bp": 67.16256489129448, + "mean_abs_r1h_bp": 80.2692761351287 + }, + { + "symbol": "KAITOUSDT", + "bars": 168575, + "mean_abs_r5m_bp": 25.209805975946754, + "std_r5m_bp": 41.49876495784051, + "p95_abs_r5m_bp": 74.97521610888599, + "mean_abs_r1h_bp": 87.61473019757939 + }, + { + "symbol": "KASUSDT", + "bars": 206633, + "mean_abs_r5m_bp": 20.59510605806794, + "std_r5m_bp": 48.09275700207359, + "p95_abs_r5m_bp": 61.87754726087278, + "mean_abs_r1h_bp": 71.25205334850367 + }, + { + "symbol": "KITEUSDT", + "bars": 96289, + "mean_abs_r5m_bp": 29.201769432378274, + "std_r5m_bp": 47.350157774143625, + "p95_abs_r5m_bp": 89.93791617706063, + "mean_abs_r1h_bp": 101.77707493993225 + }, + { + "symbol": "KLACUSDT", + "bars": 27239, + "mean_abs_r5m_bp": 11.327154342605503, + "std_r5m_bp": 25.034184123611496, + "p95_abs_r5m_bp": 52.47523217519445, + "mean_abs_r1h_bp": 47.91251504004325 + }, + { + "symbol": "KMNOUSDT", + "bars": 207220, + "mean_abs_r5m_bp": 23.0297437598867, + "std_r5m_bp": 37.195074665533966, + "p95_abs_r5m_bp": 69.42815329808737, + "mean_abs_r1h_bp": 82.15903399453384 + }, + { + "symbol": "KORUUSDT", + "bars": 21725, + "mean_abs_r5m_bp": 31.832319507026536, + "std_r5m_bp": 53.425449742139996, + "p95_abs_r5m_bp": 112.20164600634122, + "mean_abs_r1h_bp": 111.77122128461112 + }, + { + "symbol": "LABUSDT", + "bars": 96920, + "mean_abs_r5m_bp": 53.49786421282302, + "std_r5m_bp": 107.37592623188479, + "p95_abs_r5m_bp": 176.05832883323797, + "mean_abs_r1h_bp": 188.4988253815272 + }, + { + "symbol": "LAUSDT", + "bars": 138441, + "mean_abs_r5m_bp": 26.109520141418308, + "std_r5m_bp": 47.192689332631055, + "p95_abs_r5m_bp": 79.01660409673444, + "mean_abs_r1h_bp": 90.8521358380923 + }, + { + "symbol": "LDOUSDT", + "bars": 209631, + "mean_abs_r5m_bp": 23.41813902912024, + "std_r5m_bp": 46.451652251262374, + "p95_abs_r5m_bp": 68.85332510344804, + "mean_abs_r1h_bp": 80.27699387762125 + }, + { + "symbol": "LINEAUSDT", + "bars": 113040, + "mean_abs_r5m_bp": 22.525364311086047, + "std_r5m_bp": 53.76869616254597, + "p95_abs_r5m_bp": 68.42522202446892, + "mean_abs_r1h_bp": 76.69218533971537 + }, + { + "symbol": "LINKUSDT", + "bars": 209627, + "mean_abs_r5m_bp": 18.351537506390855, + "std_r5m_bp": 30.247117126741692, + "p95_abs_r5m_bp": 54.85233135110623, + "mean_abs_r1h_bp": 62.44249066076185 + }, + { + "symbol": "LITUSDT", + "bars": 78424, + "mean_abs_r5m_bp": 31.813702994477595, + "std_r5m_bp": 46.570344578563684, + "p95_abs_r5m_bp": 94.99344090299525, + "mean_abs_r1h_bp": 109.10969386765723 + }, + { + "symbol": "LONGXIAUSDT", + "bars": 4181, + "mean_abs_r5m_bp": 83.76134220947799, + "std_r5m_bp": 138.73927217755002, + "p95_abs_r5m_bp": 270.12107086884106, + "mean_abs_r1h_bp": 294.46715880134445 + }, + { + "symbol": "LSKUSDT", + "bars": 209630, + "mean_abs_r5m_bp": 19.976844022719007, + "std_r5m_bp": 43.015146772760104, + "p95_abs_r5m_bp": 61.23061604352866, + "mean_abs_r1h_bp": 70.85303027899126 + }, + { + "symbol": "LTCUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 15.675498619854924, + "std_r5m_bp": 27.376583955448872, + "p95_abs_r5m_bp": 48.031897934447535, + "mean_abs_r1h_bp": 53.54584203658693 + }, + { + "symbol": "LYNUSDT", + "bars": 102976, + "mean_abs_r5m_bp": 38.8836310205459, + "std_r5m_bp": 73.81367046213533, + "p95_abs_r5m_bp": 126.98918645851674, + "mean_abs_r1h_bp": 133.51950709452785 + }, + { + "symbol": "MAGICUSDT", + "bars": 209648, + "mean_abs_r5m_bp": 23.352057755849216, + "std_r5m_bp": 43.29029720442573, + "p95_abs_r5m_bp": 70.5470297988997, + "mean_abs_r1h_bp": 80.4299363450048 + }, + { + "symbol": "MARSCOINUSDT", + "bars": 7898, + "mean_abs_r5m_bp": 73.2458894959522, + "std_r5m_bp": 121.56718121557155, + "p95_abs_r5m_bp": 221.5998467548904, + "mean_abs_r1h_bp": 265.8518249491968 + }, + { + "symbol": "MEGAUSDT", + "bars": 98031, + "mean_abs_r5m_bp": 32.44999938796589, + "std_r5m_bp": 64.6685556918381, + "p95_abs_r5m_bp": 121.7231206899099, + "mean_abs_r1h_bp": 104.0620982792037 + }, + { + "symbol": "METUSDT", + "bars": 101786, + "mean_abs_r5m_bp": 26.81167417664115, + "std_r5m_bp": 44.84046413802002, + "p95_abs_r5m_bp": 84.37206547457718, + "mean_abs_r1h_bp": 94.83381185473506 + }, + { + "symbol": "MINAUSDT", + "bars": 209644, + "mean_abs_r5m_bp": 21.00071003751233, + "std_r5m_bp": 39.190695150272, + "p95_abs_r5m_bp": 62.94959085477584, + "mean_abs_r1h_bp": 72.54926474972052 + }, + { + "symbol": "MMTUSDT", + "bars": 96900, + "mean_abs_r5m_bp": 29.58377519629142, + "std_r5m_bp": 74.91658540565876, + "p95_abs_r5m_bp": 89.07642833314893, + "mean_abs_r1h_bp": 101.55766276595254 + }, + { + "symbol": "MNTUSDT", + "bars": 208631, + "mean_abs_r5m_bp": 16.222798192952204, + "std_r5m_bp": 29.374156282140802, + "p95_abs_r5m_bp": 50.98335112245357, + "mean_abs_r1h_bp": 59.01555985998598 + }, + { + "symbol": "MONUSDT", + "bars": 88628, + "mean_abs_r5m_bp": 25.96308061978449, + "std_r5m_bp": 39.79045114060184, + "p95_abs_r5m_bp": 80.0694840717524, + "mean_abs_r1h_bp": 89.22867779991141 + }, + { + "symbol": "MOODENGUSDT", + "bars": 209544, + "mean_abs_r5m_bp": 33.13088840214432, + "std_r5m_bp": 58.31842660565095, + "p95_abs_r5m_bp": 104.45012101292181, + "mean_abs_r1h_bp": 113.30358202034493 + }, + { + "symbol": "MORPHOUSDT", + "bars": 194548, + "mean_abs_r5m_bp": 24.615841278928023, + "std_r5m_bp": 42.34946199923929, + "p95_abs_r5m_bp": 75.0794143687041, + "mean_abs_r1h_bp": 85.08053526804854 + }, + { + "symbol": "MRVLUSDT", + "bars": 39906, + "mean_abs_r5m_bp": 17.990863738972667, + "std_r5m_bp": 32.1292707115744, + "p95_abs_r5m_bp": 61.621400312702676, + "mean_abs_r1h_bp": 65.13934541474397 + }, + { + "symbol": "MUBARAKUSDT", + "bars": 160437, + "mean_abs_r5m_bp": 31.62468539096755, + "std_r5m_bp": 58.74938837228615, + "p95_abs_r5m_bp": 94.95768734900767, + "mean_abs_r1h_bp": 107.59630656987778 + }, + { + "symbol": "MUUSDT", + "bars": 41618, + "mean_abs_r5m_bp": 16.770653962763348, + "std_r5m_bp": 28.156937987591025, + "p95_abs_r5m_bp": 57.67266524098238, + "mean_abs_r1h_bp": 59.11590810105944 + }, + { + "symbol": "MUUUSDT", + "bars": 21459, + "mean_abs_r5m_bp": 26.486809465375096, + "std_r5m_bp": 47.1109979325785, + "p95_abs_r5m_bp": 96.31042681001098, + "mean_abs_r1h_bp": 96.04053460687686 + }, + { + "symbol": "MYXUSDT", + "bars": 120836, + "mean_abs_r5m_bp": 43.681447086612295, + "std_r5m_bp": 84.41409873114338, + "p95_abs_r5m_bp": 139.34783743684218, + "mean_abs_r1h_bp": 149.5623011941492 + }, + { + "symbol": "NBISUSDT", + "bars": 36152, + "mean_abs_r5m_bp": 20.290153642008477, + "std_r5m_bp": 35.38633402328555, + "p95_abs_r5m_bp": 70.24301550977441, + "mean_abs_r1h_bp": 74.86724897133081 + }, + { + "symbol": "NEARUSDT", + "bars": 205626, + "mean_abs_r5m_bp": 22.334752691643818, + "std_r5m_bp": 36.875776405707526, + "p95_abs_r5m_bp": 65.45978299261223, + "mean_abs_r1h_bp": 76.25079284411102 + }, + { + "symbol": "NIGHTUSDT", + "bars": 84523, + "mean_abs_r5m_bp": 23.614840365502438, + "std_r5m_bp": 41.430017373073404, + "p95_abs_r5m_bp": 73.40364784826758, + "mean_abs_r1h_bp": 82.57495154259911 + }, + { + "symbol": "NILUSDT", + "bars": 160510, + "mean_abs_r5m_bp": 29.574541693183438, + "std_r5m_bp": 50.31890205196862, + "p95_abs_r5m_bp": 89.9487915066777, + "mean_abs_r1h_bp": 104.02262238931111 + }, + { + "symbol": "NIULAIUSDT", + "bars": 9092, + "mean_abs_r5m_bp": 74.85797617692835, + "std_r5m_bp": 118.63937375173697, + "p95_abs_r5m_bp": 236.5764235627621, + "mean_abs_r1h_bp": 263.87057805299213 + }, + { + "symbol": "NMRUSDT", + "bars": 209631, + "mean_abs_r5m_bp": 20.849830117009326, + "std_r5m_bp": 41.09953506668882, + "p95_abs_r5m_bp": 62.77998800661953, + "mean_abs_r1h_bp": 70.65902675914421 + }, + { + "symbol": "NOMUSDT", + "bars": 104420, + "mean_abs_r5m_bp": 29.623332736735037, + "std_r5m_bp": 64.73089622994392, + "p95_abs_r5m_bp": 93.12752169751967, + "mean_abs_r1h_bp": 103.28084220669643 + }, + { + "symbol": "ONDOUSDT", + "bars": 209626, + "mean_abs_r5m_bp": 22.62710322626834, + "std_r5m_bp": 40.18648361033287, + "p95_abs_r5m_bp": 66.74159573350673, + "mean_abs_r1h_bp": 76.61087277165785 + }, + { + "symbol": "OPGUSDT", + "bars": 45703, + "mean_abs_r5m_bp": 30.183098457016317, + "std_r5m_bp": 53.79790754359615, + "p95_abs_r5m_bp": 90.73272685388876, + "mean_abs_r1h_bp": 105.97520363803748 + }, + { + "symbol": "OPNUSDT", + "bars": 66088, + "mean_abs_r5m_bp": 32.235348151215995, + "std_r5m_bp": 56.540632397946794, + "p95_abs_r5m_bp": 98.0858711253177, + "mean_abs_r1h_bp": 110.8126760189489 + }, + { + "symbol": "OPUSDT", + "bars": 208629, + "mean_abs_r5m_bp": 21.750089514710556, + "std_r5m_bp": 35.746984770755994, + "p95_abs_r5m_bp": 62.72340262975716, + "mean_abs_r1h_bp": 75.13005491615974 + }, + { + "symbol": "ORCAUSDT", + "bars": 209641, + "mean_abs_r5m_bp": 23.057476491311757, + "std_r5m_bp": 44.481056133363204, + "p95_abs_r5m_bp": 70.67259738161003, + "mean_abs_r1h_bp": 82.30055702060206 + }, + { + "symbol": "ORCLUSDT", + "bars": 41920, + "mean_abs_r5m_bp": 10.351342530843826, + "std_r5m_bp": 19.886755417714753, + "p95_abs_r5m_bp": 38.86840122381496, + "mean_abs_r1h_bp": 39.07533157333879 + }, + { + "symbol": "ORDIUSDT", + "bars": 209635, + "mean_abs_r5m_bp": 25.167124497801016, + "std_r5m_bp": 52.54658674715313, + "p95_abs_r5m_bp": 72.44840434213458, + "mean_abs_r1h_bp": 85.38807611038919 + }, + { + "symbol": "PAXGUSDT", + "bars": 208632, + "mean_abs_r5m_bp": 4.169678433105975, + "std_r5m_bp": 7.429967879099343, + "p95_abs_r5m_bp": 13.920508849372126, + "mean_abs_r1h_bp": 14.704849803648997 + }, + { + "symbol": "PEAQUSDT", + "bars": 195421, + "mean_abs_r5m_bp": 27.842584478764376, + "std_r5m_bp": 47.3117716247343, + "p95_abs_r5m_bp": 89.6662053791301, + "mean_abs_r1h_bp": 101.6505238473823 + }, + { + "symbol": "PENDLEUSDT", + "bars": 208632, + "mean_abs_r5m_bp": 23.334224551973588, + "std_r5m_bp": 38.34144823872225, + "p95_abs_r5m_bp": 66.73241944132214, + "mean_abs_r1h_bp": 81.80503706908607 + }, + { + "symbol": "PENGUUSDT", + "bars": 187278, + "mean_abs_r5m_bp": 28.321005457199924, + "std_r5m_bp": 50.63571991787544, + "p95_abs_r5m_bp": 84.86666643096713, + "mean_abs_r1h_bp": 98.49599474590642 + }, + { + "symbol": "PHAROSUSDT", + "bars": 39624, + "mean_abs_r5m_bp": 22.30009432041536, + "std_r5m_bp": 39.214369432699236, + "p95_abs_r5m_bp": 72.08817748465668, + "mean_abs_r1h_bp": 80.99423773929377 + }, + { + "symbol": "PHAUSDT", + "bars": 209633, + "mean_abs_r5m_bp": 26.75159702623474, + "std_r5m_bp": 47.25630059321534, + "p95_abs_r5m_bp": 80.28685852408569, + "mean_abs_r1h_bp": 92.23122891647832 + }, + { + "symbol": "PIEVERSEUSDT", + "bars": 91686, + "mean_abs_r5m_bp": 37.360018579094444, + "std_r5m_bp": 69.06790477966118, + "p95_abs_r5m_bp": 122.3703673313572, + "mean_abs_r1h_bp": 130.81311962316556 + }, + { + "symbol": "PIPPINUSDT", + "bars": 178429, + "mean_abs_r5m_bp": 44.99560015031408, + "std_r5m_bp": 91.73959078513018, + "p95_abs_r5m_bp": 144.72211741154837, + "mean_abs_r1h_bp": 156.84020976964533 + }, + { + "symbol": "PLUMEUSDT", + "bars": 176400, + "mean_abs_r5m_bp": 27.99243979468657, + "std_r5m_bp": 53.28507356428585, + "p95_abs_r5m_bp": 83.66299130913738, + "mean_abs_r1h_bp": 96.25499568387298 + }, + { + "symbol": "POLUSDT", + "bars": 208630, + "mean_abs_r5m_bp": 17.783197730755617, + "std_r5m_bp": 29.65613740143714, + "p95_abs_r5m_bp": 53.26244282674164, + "mean_abs_r1h_bp": 61.42797859986913 + }, + { + "symbol": "PONSUSDT", + "bars": 8201, + "mean_abs_r5m_bp": 62.01334437854188, + "std_r5m_bp": 89.07276051485573, + "p95_abs_r5m_bp": 183.45386254676012, + "mean_abs_r1h_bp": 215.86007346860396 + }, + { + "symbol": "POPCATUSDT", + "bars": 209643, + "mean_abs_r5m_bp": 30.756131673881953, + "std_r5m_bp": 50.718167036032796, + "p95_abs_r5m_bp": 92.90868937845698, + "mean_abs_r1h_bp": 104.58101394998883 + }, + { + "symbol": "PUMPFUNUSDT", + "bars": 128340, + "mean_abs_r5m_bp": 30.57972212409251, + "std_r5m_bp": 49.4726709140537, + "p95_abs_r5m_bp": 92.10371449337977, + "mean_abs_r1h_bp": 104.8524024006705 + }, + { + "symbol": "PYTHUSDT", + "bars": 209633, + "mean_abs_r5m_bp": 23.22617516592778, + "std_r5m_bp": 39.18114189537807, + "p95_abs_r5m_bp": 67.05808867191188, + "mean_abs_r1h_bp": 81.38628770775922 + }, + { + "symbol": "QNTUSDT", + "bars": 209626, + "mean_abs_r5m_bp": 18.368572951217935, + "std_r5m_bp": 30.170606057873382, + "p95_abs_r5m_bp": 55.52287969276643, + "mean_abs_r1h_bp": 62.84858265429264 + }, + { + "symbol": "QUSDT", + "bars": 106837, + "mean_abs_r5m_bp": 37.95459420408626, + "std_r5m_bp": 82.17868700483729, + "p95_abs_r5m_bp": 122.66985499061134, + "mean_abs_r1h_bp": 133.59215634143237 + }, + { + "symbol": "RAREUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 22.212167743296668, + "std_r5m_bp": 48.2269655981594, + "p95_abs_r5m_bp": 67.3403218134394, + "mean_abs_r1h_bp": 75.80929794317926 + }, + { + "symbol": "RAYDIUMUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 25.612682074655506, + "std_r5m_bp": 41.77834038471834, + "p95_abs_r5m_bp": 77.4907567362526, + "mean_abs_r1h_bp": 88.53648675402889 + }, + { + "symbol": "RENDERUSDT", + "bars": 208632, + "mean_abs_r5m_bp": 22.823283280537183, + "std_r5m_bp": 49.35592132474683, + "p95_abs_r5m_bp": 66.37126792196885, + "mean_abs_r1h_bp": 78.16168133373044 + }, + { + "symbol": "REUSDT", + "bars": 29475, + "mean_abs_r5m_bp": 34.41181245651747, + "std_r5m_bp": 58.002428524343635, + "p95_abs_r5m_bp": 107.28647791397853, + "mean_abs_r1h_bp": 119.35885252193061 + }, + { + "symbol": "RIVERUSDT", + "bars": 98326, + "mean_abs_r5m_bp": 48.22566230298559, + "std_r5m_bp": 83.57150853046227, + "p95_abs_r5m_bp": 164.94042005395212, + "mean_abs_r1h_bp": 167.49950002958963 + }, + { + "symbol": "RUNEUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 19.170027598800722, + "std_r5m_bp": 32.03562104630493, + "p95_abs_r5m_bp": 58.309203107931026, + "mean_abs_r1h_bp": 67.29368075011314 + }, + { + "symbol": "SAGAUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 28.40321773445242, + "std_r5m_bp": 49.72991614224732, + "p95_abs_r5m_bp": 82.78319878577634, + "mean_abs_r1h_bp": 97.18507920234964 + }, + { + "symbol": "SAHARAUSDT", + "bars": 134031, + "mean_abs_r5m_bp": 23.20601358673174, + "std_r5m_bp": 46.63751038678036, + "p95_abs_r5m_bp": 71.96801081782719, + "mean_abs_r1h_bp": 80.48407825989116 + }, + { + "symbol": "SAMSUNGUSDT", + "bars": 33618, + "mean_abs_r5m_bp": 17.136929305891506, + "std_r5m_bp": 29.187343723001486, + "p95_abs_r5m_bp": 59.49401423916512, + "mean_abs_r1h_bp": 62.33326038727968 + }, + { + "symbol": "SANDUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 19.576951071832166, + "std_r5m_bp": 34.840298534700736, + "p95_abs_r5m_bp": 57.99245984469747, + "mean_abs_r1h_bp": 67.55499260061269 + }, + { + "symbol": "SEIUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 20.8721983470605, + "std_r5m_bp": 34.52951407594023, + "p95_abs_r5m_bp": 61.45231123692005, + "mean_abs_r1h_bp": 72.4046510750826 + }, + { + "symbol": "SHIB1000USDT", + "bars": 209631, + "mean_abs_r5m_bp": 16.991837949797514, + "std_r5m_bp": 27.204948491336246, + "p95_abs_r5m_bp": 50.99122962913105, + "mean_abs_r1h_bp": 58.14221671403204 + }, + { + "symbol": "SIRENUSDT", + "bars": 160072, + "mean_abs_r5m_bp": 38.35613988364175, + "std_r5m_bp": 88.26036175809249, + "p95_abs_r5m_bp": 125.20515077257043, + "mean_abs_r1h_bp": 132.57429411442462 + }, + { + "symbol": "SKHYNIXUSDT", + "bars": 33620, + "mean_abs_r5m_bp": 19.86031399043102, + "std_r5m_bp": 32.94930242870882, + "p95_abs_r5m_bp": 69.79035079861262, + "mean_abs_r1h_bp": 70.40273825725362 + }, + { + "symbol": "SKHYUSDT", + "bars": 22970, + "mean_abs_r5m_bp": 16.98278116134976, + "std_r5m_bp": 29.886558287917453, + "p95_abs_r5m_bp": 61.47705355708054, + "mean_abs_r1h_bp": 60.558419381961926 + }, + { + "symbol": "SKRUSDT", + "bars": 72421, + "mean_abs_r5m_bp": 30.486971097076566, + "std_r5m_bp": 64.3227741404937, + "p95_abs_r5m_bp": 99.9638390344453, + "mean_abs_r1h_bp": 108.35179715800777 + }, + { + "symbol": "SKYUSDT", + "bars": 119095, + "mean_abs_r5m_bp": 16.126314166495536, + "std_r5m_bp": 28.381926482253238, + "p95_abs_r5m_bp": 48.357257988156526, + "mean_abs_r1h_bp": 56.84230217079085 + }, + { + "symbol": "SNDKUSDT", + "bars": 44213, + "mean_abs_r5m_bp": 19.14467090414691, + "std_r5m_bp": 33.10760682859791, + "p95_abs_r5m_bp": 66.2982684273916, + "mean_abs_r1h_bp": 68.32125718927561 + }, + { + "symbol": "SNXUSDT", + "bars": 209648, + "mean_abs_r5m_bp": 22.31651363235181, + "std_r5m_bp": 37.36715117420622, + "p95_abs_r5m_bp": 66.47534231617402, + "mean_abs_r1h_bp": 77.8546882285701 + }, + { + "symbol": "SNXXUSDT", + "bars": 22041, + "mean_abs_r5m_bp": 36.98902255289389, + "std_r5m_bp": 66.7876625963384, + "p95_abs_r5m_bp": 131.17979013538096, + "mean_abs_r1h_bp": 132.74086589784744 + }, + { + "symbol": "SOLUSDT", + "bars": 209626, + "mean_abs_r5m_bp": 16.20334512749723, + "std_r5m_bp": 25.272668585638687, + "p95_abs_r5m_bp": 47.976045299460424, + "mean_abs_r1h_bp": 55.33195480066128 + }, + { + "symbol": "SOONUSDT", + "bars": 142128, + "mean_abs_r5m_bp": 30.18815559670125, + "std_r5m_bp": 59.222489103752004, + "p95_abs_r5m_bp": 93.78117386161675, + "mean_abs_r1h_bp": 103.80350738108922 + }, + { + "symbol": "SOXSUSDT", + "bars": 21444, + "mean_abs_r5m_bp": 24.917419544915532, + "std_r5m_bp": 42.448952353459475, + "p95_abs_r5m_bp": 85.79673810684115, + "mean_abs_r1h_bp": 91.1404523263848 + }, + { + "symbol": "SPCXUSDT", + "bars": 30379, + "mean_abs_r5m_bp": 12.139478144143444, + "std_r5m_bp": 23.97262240924734, + "p95_abs_r5m_bp": 46.455881515390374, + "mean_abs_r1h_bp": 43.929233967351465 + }, + { + "symbol": "SPKUSDT", + "bars": 134947, + "mean_abs_r5m_bp": 23.964398663304074, + "std_r5m_bp": 44.755793106226555, + "p95_abs_r5m_bp": 73.99966495907573, + "mean_abs_r1h_bp": 83.69064250417428 + }, + { + "symbol": "SPXUSDT", + "bars": 201461, + "mean_abs_r5m_bp": 33.517936899567175, + "std_r5m_bp": 51.7845376948027, + "p95_abs_r5m_bp": 98.73202343552735, + "mean_abs_r1h_bp": 118.26847084528035 + }, + { + "symbol": "STABLEUSDT", + "bars": 93998, + "mean_abs_r5m_bp": 30.34668526776247, + "std_r5m_bp": 55.8673078182596, + "p95_abs_r5m_bp": 106.30460434958154, + "mean_abs_r1h_bp": 107.86416823192428 + }, + { + "symbol": "STGUSDT", + "bars": 208635, + "mean_abs_r5m_bp": 21.39202681086658, + "std_r5m_bp": 37.64684475730091, + "p95_abs_r5m_bp": 66.48665810306247, + "mean_abs_r1h_bp": 75.9673076736603 + }, + { + "symbol": "STONKUSDT", + "bars": 3036, + "mean_abs_r5m_bp": 52.800554419998775, + "std_r5m_bp": 75.31844106739985, + "p95_abs_r5m_bp": 158.56600069890388, + "mean_abs_r1h_bp": 243.37581827925422 + }, + { + "symbol": "STRKUSDT", + "bars": 209631, + "mean_abs_r5m_bp": 24.125418990428287, + "std_r5m_bp": 42.71689277546, + "p95_abs_r5m_bp": 70.6538254600117, + "mean_abs_r1h_bp": 82.8195014976737 + }, + { + "symbol": "STXUSDT", + "bars": 208634, + "mean_abs_r5m_bp": 20.48057602913211, + "std_r5m_bp": 34.58365607189213, + "p95_abs_r5m_bp": 60.76539459732664, + "mean_abs_r1h_bp": 70.48146650345994 + }, + { + "symbol": "SUIUSDT", + "bars": 209627, + "mean_abs_r5m_bp": 21.26497434039849, + "std_r5m_bp": 41.272744768578036, + "p95_abs_r5m_bp": 62.88405669666611, + "mean_abs_r1h_bp": 72.68523930057951 + }, + { + "symbol": "SUSDT", + "bars": 179010, + "mean_abs_r5m_bp": 22.15981427560085, + "std_r5m_bp": 40.78969039621073, + "p95_abs_r5m_bp": 65.46668226314445, + "mean_abs_r1h_bp": 78.0653888897253 + }, + { + "symbol": "SUSHIUSDT", + "bars": 209645, + "mean_abs_r5m_bp": 22.243822953131005, + "std_r5m_bp": 40.82927512029975, + "p95_abs_r5m_bp": 65.5118313189412, + "mean_abs_r1h_bp": 76.79683149473365 + }, + { + "symbol": "SYRUPUSDT", + "bars": 143307, + "mean_abs_r5m_bp": 22.553284861290603, + "std_r5m_bp": 34.11866245836622, + "p95_abs_r5m_bp": 67.53894843418928, + "mean_abs_r1h_bp": 78.47577531350436 + }, + { + "symbol": "TAIKOUSDT", + "bars": 209652, + "mean_abs_r5m_bp": 19.841715047892993, + "std_r5m_bp": 38.74064458110083, + "p95_abs_r5m_bp": 62.26237288432523, + "mean_abs_r1h_bp": 69.07070093009423 + }, + { + "symbol": "TAOUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 24.05466528237916, + "std_r5m_bp": 39.40863033878565, + "p95_abs_r5m_bp": 70.070719167302, + "mean_abs_r1h_bp": 81.1205669040307 + }, + { + "symbol": "THETAUSDT", + "bars": 209651, + "mean_abs_r5m_bp": 21.108784035927005, + "std_r5m_bp": 37.2819736544793, + "p95_abs_r5m_bp": 60.80812976879707, + "mean_abs_r1h_bp": 72.45145691328572 + }, + { + "symbol": "TIAUSDT", + "bars": 208630, + "mean_abs_r5m_bp": 24.035511019378912, + "std_r5m_bp": 44.81382929246078, + "p95_abs_r5m_bp": 68.91660839285309, + "mean_abs_r1h_bp": 82.20539253688852 + }, + { + "symbol": "TQQQUSDT", + "bars": 27811, + "mean_abs_r5m_bp": 10.742174176053515, + "std_r5m_bp": 18.050268153395127, + "p95_abs_r5m_bp": 37.49049525874913, + "mean_abs_r1h_bp": 38.93141745208117 + }, + { + "symbol": "TRBUSDT", + "bars": 209649, + "mean_abs_r5m_bp": 21.644788858985688, + "std_r5m_bp": 39.226957109779, + "p95_abs_r5m_bp": 63.02872915597721, + "mean_abs_r1h_bp": 72.60297866564517 + }, + { + "symbol": "TRUMPUSDT", + "bars": 178111, + "mean_abs_r5m_bp": 20.903348595400242, + "std_r5m_bp": 60.32409570050787, + "p95_abs_r5m_bp": 63.22465739487026, + "mean_abs_r1h_bp": 70.60007021402626 + }, + { + "symbol": "TRUSTUSDT", + "bars": 91710, + "mean_abs_r5m_bp": 22.060901536992823, + "std_r5m_bp": 43.45318222234013, + "p95_abs_r5m_bp": 69.05358485036571, + "mean_abs_r1h_bp": 76.44736257651654 + }, + { + "symbol": "TRXUSDT", + "bars": 206633, + "mean_abs_r5m_bp": 7.002744971441075, + "std_r5m_bp": 13.544884754672607, + "p95_abs_r5m_bp": 22.17232265193347, + "mean_abs_r1h_bp": 26.506621480297166 + }, + { + "symbol": "TSEMUSDT", + "bars": 21162, + "mean_abs_r5m_bp": 16.553438011054368, + "std_r5m_bp": 30.622944635372594, + "p95_abs_r5m_bp": 61.89071894620923, + "mean_abs_r1h_bp": 59.71129921275481 + }, + { + "symbol": "UAIUSDT", + "bars": 90861, + "mean_abs_r5m_bp": 40.38974252325904, + "std_r5m_bp": 71.0019251971544, + "p95_abs_r5m_bp": 124.87767975662051, + "mean_abs_r1h_bp": 143.87861786525818 + }, + { + "symbol": "UNITREEUSDT", + "bars": 16356, + "mean_abs_r5m_bp": 18.950759343755646, + "std_r5m_bp": 51.096939680221205, + "p95_abs_r5m_bp": 79.05526736770517, + "mean_abs_r1h_bp": 57.93967575466197 + }, + { + "symbol": "UNIUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 21.091464046816913, + "std_r5m_bp": 35.62704840026974, + "p95_abs_r5m_bp": 61.87520923368289, + "mean_abs_r1h_bp": 72.19846636777741 + }, + { + "symbol": "USELESSUSDT", + "bars": 122277, + "mean_abs_r5m_bp": 43.682935218943896, + "std_r5m_bp": 73.2518464059948, + "p95_abs_r5m_bp": 129.24354697451346, + "mean_abs_r1h_bp": 153.4891694198435 + }, + { + "symbol": "USUSDT", + "bars": 83639, + "mean_abs_r5m_bp": 39.47960013756108, + "std_r5m_bp": 73.11046082833481, + "p95_abs_r5m_bp": 125.72890765591357, + "mean_abs_r1h_bp": 137.28732750976638 + }, + { + "symbol": "VETUSDT", + "bars": 208635, + "mean_abs_r5m_bp": 19.08743087170387, + "std_r5m_bp": 30.894436217254437, + "p95_abs_r5m_bp": 57.587259688260424, + "mean_abs_r1h_bp": 65.92091733726514 + }, + { + "symbol": "VIRTUALUSDT", + "bars": 200586, + "mean_abs_r5m_bp": 29.366915569867245, + "std_r5m_bp": 50.33247122369621, + "p95_abs_r5m_bp": 88.13235357555445, + "mean_abs_r1h_bp": 102.81484687293654 + }, + { + "symbol": "VVVUSDT", + "bars": 174300, + "mean_abs_r5m_bp": 30.731584433801384, + "std_r5m_bp": 49.67332077427452, + "p95_abs_r5m_bp": 93.91882059664417, + "mean_abs_r1h_bp": 109.540074512167 + }, + { + "symbol": "WAVESUSDT", + "bars": 209643, + "mean_abs_r5m_bp": 18.836582932294974, + "std_r5m_bp": 35.82961511479868, + "p95_abs_r5m_bp": 62.01338620141225, + "mean_abs_r1h_bp": 62.065167072577694 + }, + { + "symbol": "WENSTOCKUSDT", + "bars": 20316, + "mean_abs_r5m_bp": 9.375792472003326, + "std_r5m_bp": 22.907863366233624, + "p95_abs_r5m_bp": 36.29413382346746, + "mean_abs_r1h_bp": 32.652086662243654 + }, + { + "symbol": "WIFUSDT", + "bars": 209631, + "mean_abs_r5m_bp": 26.483853747710675, + "std_r5m_bp": 46.98682717049948, + "p95_abs_r5m_bp": 78.18963350561381, + "mean_abs_r1h_bp": 90.38476366262408 + }, + { + "symbol": "WLDUSDT", + "bars": 209627, + "mean_abs_r5m_bp": 25.689903218640342, + "std_r5m_bp": 49.65017162912696, + "p95_abs_r5m_bp": 74.844205064547, + "mean_abs_r1h_bp": 87.05494146464514 + }, + { + "symbol": "WLFIUSDT", + "bars": 115614, + "mean_abs_r5m_bp": 20.864190323627387, + "std_r5m_bp": 41.05757096113327, + "p95_abs_r5m_bp": 65.66764622307673, + "mean_abs_r1h_bp": 71.49376994155962 + }, + { + "symbol": "WUSDT", + "bars": 209632, + "mean_abs_r5m_bp": 24.074918821050485, + "std_r5m_bp": 41.1240242882516, + "p95_abs_r5m_bp": 69.71770432650182, + "mean_abs_r1h_bp": 83.28985058842235 + }, + { + "symbol": "XAIUSDT", + "bars": 209651, + "mean_abs_r5m_bp": 24.532641595479426, + "std_r5m_bp": 44.50887670758337, + "p95_abs_r5m_bp": 71.79864268256819, + "mean_abs_r1h_bp": 83.36202799903765 + }, + { + "symbol": "XAUTUSDT", + "bars": 156497, + "mean_abs_r5m_bp": 4.386096580134171, + "std_r5m_bp": 7.736599442594967, + "p95_abs_r5m_bp": 14.784549225281829, + "mean_abs_r1h_bp": 15.682442386891289 + }, + { + "symbol": "XCNUSDT", + "bars": 209647, + "mean_abs_r5m_bp": 26.049577446810158, + "std_r5m_bp": 50.90700937728146, + "p95_abs_r5m_bp": 89.72886758661147, + "mean_abs_r1h_bp": 86.59552555276287 + }, + { + "symbol": "XDCUSDT", + "bars": 170498, + "mean_abs_r5m_bp": 12.447848629270398, + "std_r5m_bp": 21.581295283360333, + "p95_abs_r5m_bp": 40.35735144041645, + "mean_abs_r1h_bp": 46.017033776339915 + }, + { + "symbol": "XLMUSDT", + "bars": 209628, + "mean_abs_r5m_bp": 18.841963486940486, + "std_r5m_bp": 32.822275678656204, + "p95_abs_r5m_bp": 58.92442789774366, + "mean_abs_r1h_bp": 64.94606967844179 + }, + { + "symbol": "XMRUSDT", + "bars": 208629, + "mean_abs_r5m_bp": 18.199527312870174, + "std_r5m_bp": 28.446099132290804, + "p95_abs_r5m_bp": 54.6365396958417, + "mean_abs_r1h_bp": 62.51694212810207 + }, + { + "symbol": "XPLUSDT", + "bars": 115894, + "mean_abs_r5m_bp": 31.881607516801107, + "std_r5m_bp": 55.4669203637702, + "p95_abs_r5m_bp": 98.45313600910163, + "mean_abs_r1h_bp": 110.26819007051134 + }, + { + "symbol": "XRPUSDT", + "bars": 209626, + "mean_abs_r5m_bp": 16.25382364930991, + "std_r5m_bp": 28.97231602788928, + "p95_abs_r5m_bp": 50.85232967363712, + "mean_abs_r1h_bp": 55.21991345051859 + }, + { + "symbol": "XTZUSDT", + "bars": 209654, + "mean_abs_r5m_bp": 17.904546413617716, + "std_r5m_bp": 28.828539327157756, + "p95_abs_r5m_bp": 53.987861192495764, + "mean_abs_r1h_bp": 62.07875579553141 + }, + { + "symbol": "ZAMAUSDT", + "bars": 75586, + "mean_abs_r5m_bp": 28.741684471438948, + "std_r5m_bp": 48.058173102139776, + "p95_abs_r5m_bp": 91.60169627701633, + "mean_abs_r1h_bp": 100.81231279390194 + }, + { + "symbol": "ZECUSDT", + "bars": 209626, + "mean_abs_r5m_bp": 26.761347801758927, + "std_r5m_bp": 43.16852071263508, + "p95_abs_r5m_bp": 81.78140557669343, + "mean_abs_r1h_bp": 91.89582686250763 + }, + { + "symbol": "ZENUSDT", + "bars": 209634, + "mean_abs_r5m_bp": 25.599966414827847, + "std_r5m_bp": 43.504068864544394, + "p95_abs_r5m_bp": 78.48673787556226, + "mean_abs_r1h_bp": 87.9106130610706 + }, + { + "symbol": "ZESTUSDT", + "bars": 33289, + "mean_abs_r5m_bp": 27.14454638140972, + "std_r5m_bp": 51.592900893074464, + "p95_abs_r5m_bp": 93.70661404843068, + "mean_abs_r1h_bp": 92.71532192530057 + }, + { + "symbol": "ZHIPUUSDT", + "bars": 18016, + "mean_abs_r5m_bp": 17.94542424584587, + "std_r5m_bp": 37.45807892356559, + "p95_abs_r5m_bp": 68.83687671218293, + "mean_abs_r1h_bp": 68.00515014528754 + }, + { + "symbol": "ZKUSDT", + "bars": 209647, + "mean_abs_r5m_bp": 24.29876446391941, + "std_r5m_bp": 42.25817373616259, + "p95_abs_r5m_bp": 70.46038097108409, + "mean_abs_r1h_bp": 84.54675603443079 + }, + { + "symbol": "ZROUSDT", + "bars": 209629, + "mean_abs_r5m_bp": 24.941834279748957, + "std_r5m_bp": 39.238123702538545, + "p95_abs_r5m_bp": 72.91961411863478, + "mean_abs_r1h_bp": 86.50811667250854 + } + ], + "p_star": [ + { + "target_pct": 0.1, + "stop_pct": 0.1, + "taker": 105.0, + "maker": 70.0 + }, + { + "target_pct": 0.2, + "stop_pct": 0.2, + "taker": 77.5, + "maker": 60.0 + }, + { + "target_pct": 0.3, + "stop_pct": 0.2, + "taker": 62.0, + "maker": 48.0 + }, + { + "target_pct": 0.5, + "stop_pct": 0.3, + "taker": 51.2, + "maker": 42.5 + }, + { + "target_pct": 1.0, + "stop_pct": 0.5, + "taker": 40.7, + "maker": 36.0 + }, + { + "target_pct": 1.0, + "stop_pct": 1.0, + "taker": 55.5, + "maker": 52.0 + }, + { + "target_pct": 2.0, + "stop_pct": 1.0, + "taker": 37.0, + "maker": 34.7 + } + ] +} \ No newline at end of file diff --git a/reports/stand_reconcile.json b/reports/stand_reconcile.json new file mode 100644 index 0000000..93b9c5f --- /dev/null +++ b/reports/stand_reconcile.json @@ -0,0 +1,53 @@ +{ + "pairs": { + "BTCUSDT": { + "prod_n": 107, + "stand_n": 107, + "prod_pf": 0.4799, + "stand_pf": 0.4799, + "prod_sum_pnl": -0.2231, + "stand_sum_pnl": -0.2231, + "bars_30m": 34602 + }, + "ETHUSDT": { + "prod_n": 105, + "stand_n": 105, + "prod_pf": 0.3737, + "stand_pf": 0.3737, + "prod_sum_pnl": -0.3143, + "stand_sum_pnl": -0.3143, + "bars_30m": 34770 + }, + "SOLUSDT": { + "prod_n": 82, + "stand_n": 82, + "prod_pf": 0.3604, + "stand_pf": 0.3604, + "prod_sum_pnl": -0.2581, + "stand_sum_pnl": -0.2581, + "bars_30m": 34937 + }, + "XRPUSDT": { + "prod_n": 114, + "stand_n": 114, + "prod_pf": 0.6517, + "stand_pf": 0.6517, + "prod_sum_pnl": -0.1706, + "stand_sum_pnl": -0.1706, + "bars_30m": 34937 + }, + "ZECUSDT": { + "prod_n": 107, + "stand_n": 107, + "prod_pf": 0.9739, + "stand_pf": 0.9739, + "prod_sum_pnl": -0.0139, + "stand_sum_pnl": -0.0139, + "bars_30m": 34937 + } + }, + "fee_rt": 0.0011, + "engine": "app.trading.mean_reversion.run_mr_backtest", + "stand": "tools/mr_engine.stand_mr_backtest", + "source": "30м из ресемпла 5м данных" +} \ No newline at end of file diff --git a/reports/validate_5m.json b/reports/validate_5m.json new file mode 100644 index 0000000..40c8cf6 --- /dev/null +++ b/reports/validate_5m.json @@ -0,0 +1,1256 @@ +{ + "dup": 0, + "pairs": { + "BTCUSDT": { + "bars": 207626, + "coverage": 0.9905, + "dup": 0 + }, + "SOLUSDT": { + "bars": 209626, + "coverage": 1.0, + "dup": 0 + }, + "ETHUSDT": { + "bars": 208626, + "coverage": 0.9952, + "dup": 0 + }, + "XRPUSDT": { + "bars": 209626, + "coverage": 1.0, + "dup": 0 + }, + "ZECUSDT": { + "bars": 209626, + "coverage": 1.0, + "dup": 0 + }, + "QNTUSDT": { + "bars": 209626, + "coverage": 1.0, + "dup": 0 + }, + "NEARUSDT": { + "bars": 205626, + "coverage": 0.9809, + "dup": 0 + }, + "HBARUSDT": { + "bars": 208626, + "coverage": 0.9952, + "dup": 0 + }, + "ONDOUSDT": { + "bars": 209626, + "coverage": 1.0, + "dup": 0 + }, + "SUIUSDT": { + "bars": 209627, + "coverage": 1.0, + "dup": 0 + }, + "ENAUSDT": { + "bars": 205627, + "coverage": 0.9809, + "dup": 0 + }, + "LINKUSDT": { + "bars": 209627, + "coverage": 1.0, + "dup": 0 + }, + "DOGEUSDT": { + "bars": 206627, + "coverage": 0.9857, + "dup": 0 + }, + "WLDUSDT": { + "bars": 209627, + "coverage": 1.0, + "dup": 0 + }, + "ADAUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "XLMUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "TAOUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "UNIUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "HYPEUSDT": { + "bars": 188751, + "coverage": 0.9895, + "dup": 0 + }, + "1000PEPEUSDT": { + "bars": 208628, + "coverage": 0.9952, + "dup": 0 + }, + "LTCUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "ARBUSDT": { + "bars": 207628, + "coverage": 0.9905, + "dup": 0 + }, + "PUMPFUNUSDT": { + "bars": 128340, + "coverage": 1.0, + "dup": 0 + }, + "BTWUSDT": { + "bars": 33271, + "coverage": 1.0, + "dup": 0 + }, + "SNDKUSDT": { + "bars": 44213, + "coverage": 1.0, + "dup": 0 + }, + "XAUTUSDT": { + "bars": 156497, + "coverage": 1.0, + "dup": 0 + }, + "AVAXUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "CASHCATUSDT": { + "bars": 15682, + "coverage": 1.0, + "dup": 0 + }, + "MARSCOINUSDT": { + "bars": 7898, + "coverage": 1.0, + "dup": 0 + }, + "SPCXUSDT": { + "bars": 30379, + "coverage": 1.0, + "dup": 0 + }, + "XPLUSDT": { + "bars": 115894, + "coverage": 1.0, + "dup": 0 + }, + "SEIUSDT": { + "bars": 209628, + "coverage": 1.0, + "dup": 0 + }, + "AAVEUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "KORUUSDT": { + "bars": 21725, + "coverage": 1.0, + "dup": 0 + }, + "TRUMPUSDT": { + "bars": 178111, + "coverage": 1.0, + "dup": 0 + }, + "MONUSDT": { + "bars": 88628, + "coverage": 1.0, + "dup": 0 + }, + "SOONUSDT": { + "bars": 142128, + "coverage": 1.0, + "dup": 0 + }, + "ZROUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "BCHUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "BNBUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "PENGUUSDT": { + "bars": 187278, + "coverage": 1.0, + "dup": 0 + }, + "GRAMUSDT": { + "bars": 27353, + "coverage": 0.9647, + "dup": 0 + }, + "VIRTUALUSDT": { + "bars": 200586, + "coverage": 1.0, + "dup": 0 + }, + "PONSUSDT": { + "bars": 8201, + "coverage": 1.0, + "dup": 0 + }, + "ALGOUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "FARTCOINUSDT": { + "bars": 186477, + "coverage": 1.0, + "dup": 0 + }, + "GRASSUSDT": { + "bars": 204902, + "coverage": 1.0, + "dup": 0 + }, + "LITUSDT": { + "bars": 78424, + "coverage": 1.0, + "dup": 0 + }, + "USELESSUSDT": { + "bars": 122277, + "coverage": 0.9532, + "dup": 0 + }, + "INJUSDT": { + "bars": 203629, + "coverage": 0.9714, + "dup": 0 + }, + "QUSDT": { + "bars": 106837, + "coverage": 0.9727, + "dup": 0 + }, + "JUPUSDT": { + "bars": 208629, + "coverage": 0.9952, + "dup": 0 + }, + "SAMSUNGUSDT": { + "bars": 33618, + "coverage": 1.0, + "dup": 0 + }, + "DOTUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "GRTUSDT": { + "bars": 207629, + "coverage": 0.9905, + "dup": 0 + }, + "AKEUSDT": { + "bars": 104785, + "coverage": 0.9905, + "dup": 0 + }, + "APTUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "CRVUSDT": { + "bars": 207629, + "coverage": 0.9905, + "dup": 0 + }, + "MUUSDT": { + "bars": 41618, + "coverage": 1.0, + "dup": 0 + }, + "VVVUSDT": { + "bars": 174300, + "coverage": 0.9943, + "dup": 0 + }, + "SKHYUSDT": { + "bars": 22970, + "coverage": 1.0, + "dup": 0 + }, + "OPUSDT": { + "bars": 208629, + "coverage": 0.9952, + "dup": 0 + }, + "DASHUSDT": { + "bars": 207629, + "coverage": 0.9905, + "dup": 0 + }, + "XMRUSDT": { + "bars": 208629, + "coverage": 0.9952, + "dup": 0 + }, + "TIAUSDT": { + "bars": 208630, + "coverage": 0.9952, + "dup": 0 + }, + "AEROUSDT": { + "bars": 209629, + "coverage": 1.0, + "dup": 0 + }, + "SKHYNIXUSDT": { + "bars": 33620, + "coverage": 1.0, + "dup": 0 + }, + "MUBARAKUSDT": { + "bars": 160437, + "coverage": 0.9938, + "dup": 0 + }, + "FILUSDT": { + "bars": 209630, + "coverage": 1.0, + "dup": 0 + }, + "SOXSUSDT": { + "bars": 21444, + "coverage": 1.0, + "dup": 0 + }, + "DRAMUSDT": { + "bars": 38154, + "coverage": 1.0, + "dup": 0 + }, + "AZTECUSDT": { + "bars": 65825, + "coverage": 1.0, + "dup": 0 + }, + "ASTERUSDT": { + "bars": 107853, + "coverage": 1.0, + "dup": 0 + }, + "LSKUSDT": { + "bars": 209630, + "coverage": 1.0, + "dup": 0 + }, + "ATOMUSDT": { + "bars": 209630, + "coverage": 1.0, + "dup": 0 + }, + "ICPUSDT": { + "bars": 208630, + "coverage": 0.9952, + "dup": 0 + }, + "POLUSDT": { + "bars": 208630, + "coverage": 0.9952, + "dup": 0 + }, + "FLOCKUSDT": { + "bars": 180710, + "coverage": 1.0, + "dup": 0 + }, + "BXUSDT": { + "bars": 21144, + "coverage": 1.0, + "dup": 0 + }, + "SNXXUSDT": { + "bars": 22041, + "coverage": 1.0, + "dup": 0 + }, + "MNTUSDT": { + "bars": 208631, + "coverage": 0.9952, + "dup": 0 + }, + "1000BONKUSDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "WIFUSDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "LDOUSDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "ETCUSDT": { + "bars": 208631, + "coverage": 0.9952, + "dup": 0 + }, + "WLFIUSDT": { + "bars": 115614, + "coverage": 1.0, + "dup": 0 + }, + "NIULAIUSDT": { + "bars": 9092, + "coverage": 1.0, + "dup": 0 + }, + "CCUSDT": { + "bars": 95698, + "coverage": 1.0, + "dup": 0 + }, + "NILUSDT": { + "bars": 160510, + "coverage": 1.0, + "dup": 0 + }, + "NMRUSDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "USUSDT": { + "bars": 83639, + "coverage": 1.0, + "dup": 0 + }, + "STRKUSDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "PAXGUSDT": { + "bars": 208632, + "coverage": 0.9952, + "dup": 0 + }, + "SHIB1000USDT": { + "bars": 209631, + "coverage": 1.0, + "dup": 0 + }, + "SAGAUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "ETHFIUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "RENDERUSDT": { + "bars": 208632, + "coverage": 0.9952, + "dup": 0 + }, + "RAREUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "PENDLEUSDT": { + "bars": 208632, + "coverage": 0.9952, + "dup": 0 + }, + "RAYDIUMUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "METUSDT": { + "bars": 101786, + "coverage": 1.0, + "dup": 0 + }, + "SPXUSDT": { + "bars": 201461, + "coverage": 1.0, + "dup": 0 + }, + "WUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "CROUSDT": { + "bars": 207632, + "coverage": 0.9905, + "dup": 0 + }, + "ARXUSDT": { + "bars": 28142, + "coverage": 1.0, + "dup": 0 + }, + "TRXUSDT": { + "bars": 206633, + "coverage": 0.9857, + "dup": 0 + }, + "FFUSDT": { + "bars": 104914, + "coverage": 1.0, + "dup": 0 + }, + "EIGENUSDT": { + "bars": 209633, + "coverage": 1.0, + "dup": 0 + }, + "2ZUSDT": { + "bars": 104060, + "coverage": 1.0, + "dup": 0 + }, + "SKYUSDT": { + "bars": 119095, + "coverage": 1.0, + "dup": 0 + }, + "KASUSDT": { + "bars": 206633, + "coverage": 0.9857, + "dup": 0 + }, + "IOTAUSDT": { + "bars": 209633, + "coverage": 1.0, + "dup": 0 + }, + "JTOUSDT": { + "bars": 209632, + "coverage": 1.0, + "dup": 0 + }, + "PYTHUSDT": { + "bars": 209633, + "coverage": 1.0, + "dup": 0 + }, + "JASMYUSDT": { + "bars": 207633, + "coverage": 0.9905, + "dup": 0 + }, + "IRYSUSDT": { + "bars": 88020, + "coverage": 1.0, + "dup": 0 + }, + "XDCUSDT": { + "bars": 170498, + "coverage": 0.9942, + "dup": 0 + }, + "RUNEUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "CAPUSDT": { + "bars": 26421, + "coverage": 1.0, + "dup": 0 + }, + "PHAUSDT": { + "bars": 209633, + "coverage": 1.0, + "dup": 0 + }, + "IMXUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "GALAUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "MORPHOUSDT": { + "bars": 194548, + "coverage": 1.0, + "dup": 0 + }, + "LONGXIAUSDT": { + "bars": 4181, + "coverage": 1.0, + "dup": 0 + }, + "ARKUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "PLUMEUSDT": { + "bars": 176400, + "coverage": 1.0, + "dup": 0 + }, + "AMDSTOCKUSDT": { + "bars": 35577, + "coverage": 1.0, + "dup": 0 + }, + "KAITOUSDT": { + "bars": 168575, + "coverage": 1.0, + "dup": 0 + }, + "STXUSDT": { + "bars": 208634, + "coverage": 0.9952, + "dup": 0 + }, + "ZENUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "KITEUSDT": { + "bars": 96289, + "coverage": 1.0, + "dup": 0 + }, + "EWYUSDT": { + "bars": 41623, + "coverage": 1.0, + "dup": 0 + }, + "NBISUSDT": { + "bars": 36152, + "coverage": 1.0, + "dup": 0 + }, + "BRUSDT": { + "bars": 159511, + "coverage": 0.9938, + "dup": 0 + }, + "APEUSDT": { + "bars": 207635, + "coverage": 0.9905, + "dup": 0 + }, + "STONKUSDT": { + "bars": 3036, + "coverage": 1.0, + "dup": 0 + }, + "VETUSDT": { + "bars": 208635, + "coverage": 0.9952, + "dup": 0 + }, + "ARUSDT": { + "bars": 207635, + "coverage": 0.9905, + "dup": 0 + }, + "DGAIUSDT": { + "bars": 7078, + "coverage": 1.0, + "dup": 0 + }, + "TQQQUSDT": { + "bars": 27811, + "coverage": 1.0, + "dup": 0 + }, + "APEXUSDT": { + "bars": 105274, + "coverage": 1.0, + "dup": 0 + }, + "EGLDUSDT": { + "bars": 209635, + "coverage": 1.0, + "dup": 0 + }, + "LYNUSDT": { + "bars": 102976, + "coverage": 1.0, + "dup": 0 + }, + "CAKEUSDT": { + "bars": 209635, + "coverage": 1.0, + "dup": 0 + }, + "KIIUSDT": { + "bars": 144, + "coverage": 1.0, + "dup": 0 + }, + "STGUSDT": { + "bars": 208635, + "coverage": 0.9952, + "dup": 0 + }, + "ORDIUSDT": { + "bars": 209635, + "coverage": 1.0, + "dup": 0 + }, + "ZAMAUSDT": { + "bars": 75586, + "coverage": 1.0, + "dup": 0 + }, + "COTIUSDT": { + "bars": 209635, + "coverage": 1.0, + "dup": 0 + }, + "CHIPUSDT": { + "bars": 47660, + "coverage": 1.0, + "dup": 0 + }, + "SANDUSDT": { + "bars": 209634, + "coverage": 1.0, + "dup": 0 + }, + "INXUSDT": { + "bars": 69842, + "coverage": 1.0, + "dup": 0 + }, + "ORCAUSDT": { + "bars": 209641, + "coverage": 1.0, + "dup": 0 + }, + "AGIUSDT": { + "bars": 209641, + "coverage": 1.0, + "dup": 0 + }, + "AVNTUSDT": { + "bars": 110687, + "coverage": 1.0, + "dup": 0 + }, + "CFGUSDT": { + "bars": 56374, + "coverage": 1.0, + "dup": 0 + }, + "BEATUSDT": { + "bars": 89701, + "coverage": 1.0, + "dup": 0 + }, + "4STOCKUSDT": { + "bars": 5596, + "coverage": 1.0, + "dup": 0 + }, + "MEGAUSDT": { + "bars": 98031, + "coverage": 1.0, + "dup": 0 + }, + "AXSUSDT": { + "bars": 209642, + "coverage": 1.0, + "dup": 0 + }, + "SYRUPUSDT": { + "bars": 143307, + "coverage": 1.0, + "dup": 0 + }, + "EDGEUSDT": { + "bars": 55508, + "coverage": 1.0, + "dup": 0 + }, + "0GUSDT": { + "bars": 108177, + "coverage": 1.0, + "dup": 0 + }, + "HUSDT": { + "bars": 132627, + "coverage": 1.0, + "dup": 0 + }, + "PIEVERSEUSDT": { + "bars": 91686, + "coverage": 1.0, + "dup": 0 + }, + "POPCATUSDT": { + "bars": 209643, + "coverage": 1.0, + "dup": 0 + }, + "OPNUSDT": { + "bars": 66088, + "coverage": 1.0, + "dup": 0 + }, + "KLACUSDT": { + "bars": 27239, + "coverage": 1.0, + "dup": 0 + }, + "WAVESUSDT": { + "bars": 209643, + "coverage": 1.0, + "dup": 0 + }, + "BILLUSDT": { + "bars": 41945, + "coverage": 1.0, + "dup": 0 + }, + "MUUUSDT": { + "bars": 21459, + "coverage": 1.0, + "dup": 0 + }, + "DATAUSDT": { + "bars": 23472, + "coverage": 1.0, + "dup": 0 + }, + "BSVUSDT": { + "bars": 209643, + "coverage": 1.0, + "dup": 0 + }, + "UAIUSDT": { + "bars": 90861, + "coverage": 1.0, + "dup": 0 + }, + "MINAUSDT": { + "bars": 209644, + "coverage": 1.0, + "dup": 0 + }, + "HUMAUSDT": { + "bars": 141230, + "coverage": 1.0, + "dup": 0 + }, + "MRVLUSDT": { + "bars": 39906, + "coverage": 1.0, + "dup": 0 + }, + "KMNOUSDT": { + "bars": 207220, + "coverage": 1.0, + "dup": 0 + }, + "ENSUSDT": { + "bars": 209644, + "coverage": 1.0, + "dup": 0 + }, + "ORCLUSDT": { + "bars": 41920, + "coverage": 1.0, + "dup": 0 + }, + "CVXUSDT": { + "bars": 209644, + "coverage": 1.0, + "dup": 0 + }, + "SUSHIUSDT": { + "bars": 209645, + "coverage": 1.0, + "dup": 0 + }, + "NOMUSDT": { + "bars": 104420, + "coverage": 1.0, + "dup": 0 + }, + "MOODENGUSDT": { + "bars": 209544, + "coverage": 1.0, + "dup": 0 + }, + "MMTUSDT": { + "bars": 96900, + "coverage": 1.0, + "dup": 0 + }, + "UNITREEUSDT": { + "bars": 16356, + "coverage": 1.0, + "dup": 0 + }, + "BROCCOLIUSDT": { + "bars": 170326, + "coverage": 1.0, + "dup": 0 + }, + "AAOIUSDT": { + "bars": 32125, + "coverage": 1.0, + "dup": 0 + }, + "ZESTUSDT": { + "bars": 33289, + "coverage": 1.0, + "dup": 0 + }, + "HNTUSDT": { + "bars": 209646, + "coverage": 1.0, + "dup": 0 + }, + "DEEPUSDT": { + "bars": 204894, + "coverage": 1.0, + "dup": 0 + }, + "B3USDT": { + "bars": 170927, + "coverage": 1.0, + "dup": 0 + }, + "RIVERUSDT": { + "bars": 98326, + "coverage": 1.0, + "dup": 0 + }, + "ATHUSDT": { + "bars": 209646, + "coverage": 1.0, + "dup": 0 + }, + "INITUSDT": { + "bars": 153344, + "coverage": 1.0, + "dup": 0 + }, + "LABUSDT": { + "bars": 96920, + "coverage": 1.0, + "dup": 0 + }, + "XCNUSDT": { + "bars": 209647, + "coverage": 1.0, + "dup": 0 + }, + "ZKUSDT": { + "bars": 209647, + "coverage": 1.0, + "dup": 0 + }, + "LINEAUSDT": { + "bars": 113040, + "coverage": 1.0, + "dup": 0 + }, + "SUSDT": { + "bars": 179010, + "coverage": 1.0, + "dup": 0 + }, + "SAHARAUSDT": { + "bars": 134031, + "coverage": 1.0, + "dup": 0 + }, + "NIGHTUSDT": { + "bars": 84523, + "coverage": 1.0, + "dup": 0 + }, + "REUSDT": { + "bars": 29475, + "coverage": 1.0, + "dup": 0 + }, + "SNXUSDT": { + "bars": 209648, + "coverage": 1.0, + "dup": 0 + }, + "MAGICUSDT": { + "bars": 209648, + "coverage": 1.0, + "dup": 0 + }, + "AVGOUSDT": { + "bars": 40197, + "coverage": 1.0, + "dup": 0 + }, + "BEUSDT": { + "bars": 35592, + "coverage": 1.0, + "dup": 0 + }, + "TSEMUSDT": { + "bars": 21162, + "coverage": 1.0, + "dup": 0 + }, + "CPUSDT": { + "bars": 7086, + "coverage": 1.0, + "dup": 0 + }, + "LAUSDT": { + "bars": 138441, + "coverage": 1.0, + "dup": 0 + }, + "COWUSDT": { + "bars": 198577, + "coverage": 1.0, + "dup": 0 + }, + "PIPPINUSDT": { + "bars": 178429, + "coverage": 1.0, + "dup": 0 + }, + "TRBUSDT": { + "bars": 209649, + "coverage": 1.0, + "dup": 0 + }, + "GIGGLEUSDT": { + "bars": 102077, + "coverage": 1.0, + "dup": 0 + }, + "OPGUSDT": { + "bars": 45703, + "coverage": 1.0, + "dup": 0 + }, + "1000FLOKIUSDT": { + "bars": 209650, + "coverage": 1.0, + "dup": 0 + }, + "PEAQUSDT": { + "bars": 195421, + "coverage": 1.0, + "dup": 0 + }, + "ALLOUSDT": { + "bars": 91705, + "coverage": 1.0, + "dup": 0 + }, + "EULUSDT": { + "bars": 100681, + "coverage": 1.0, + "dup": 0 + }, + "SIRENUSDT": { + "bars": 160072, + "coverage": 1.0, + "dup": 0 + }, + "TRUSTUSDT": { + "bars": 91710, + "coverage": 1.0, + "dup": 0 + }, + "APPSTOCKUSDT": { + "bars": 21464, + "coverage": 1.0, + "dup": 0 + }, + "BANKUSDT": { + "bars": 146469, + "coverage": 1.0, + "dup": 0 + }, + "ESPORTSUSDT": { + "bars": 122837, + "coverage": 1.0, + "dup": 0 + }, + "SPKUSDT": { + "bars": 134947, + "coverage": 1.0, + "dup": 0 + }, + "THETAUSDT": { + "bars": 209651, + "coverage": 1.0, + "dup": 0 + }, + "XAIUSDT": { + "bars": 209651, + "coverage": 1.0, + "dup": 0 + }, + "GRVTUSDT": { + "bars": 17121, + "coverage": 1.0, + "dup": 0 + }, + "ZHIPUUSDT": { + "bars": 18016, + "coverage": 1.0, + "dup": 0 + }, + "ACEUSDT": { + "bars": 209652, + "coverage": 1.0, + "dup": 0 + }, + "BRETTUSDT": { + "bars": 209652, + "coverage": 1.0, + "dup": 0 + }, + "SKRUSDT": { + "bars": 72421, + "coverage": 1.0, + "dup": 0 + }, + "BIOUSDT": { + "bars": 185065, + "coverage": 1.0, + "dup": 0 + }, + "TAIKOUSDT": { + "bars": 209652, + "coverage": 1.0, + "dup": 0 + }, + "STABLEUSDT": { + "bars": 93998, + "coverage": 1.0, + "dup": 0 + }, + "BERAUSDT": { + "bars": 172625, + "coverage": 1.0, + "dup": 0 + }, + "ELSAUSDT": { + "bars": 71897, + "coverage": 1.0, + "dup": 0 + }, + "COMPUSDT": { + "bars": 209653, + "coverage": 1.0, + "dup": 0 + }, + "ARKMUSDT": { + "bars": 209653, + "coverage": 1.0, + "dup": 0 + }, + "GMTUSDT": { + "bars": 209653, + "coverage": 1.0, + "dup": 0 + }, + "FOGOUSDT": { + "bars": 75062, + "coverage": 1.0, + "dup": 0 + }, + "XTZUSDT": { + "bars": 209654, + "coverage": 1.0, + "dup": 0 + }, + "JSTUSDT": { + "bars": 209654, + "coverage": 1.0, + "dup": 0 + }, + "CBRSUSDT": { + "bars": 38177, + "coverage": 1.0, + "dup": 0 + }, + "DOODUSDT": { + "bars": 146130, + "coverage": 1.0, + "dup": 0 + }, + "BEAMUSDT": { + "bars": 209654, + "coverage": 1.0, + "dup": 0 + }, + "WENSTOCKUSDT": { + "bars": 20316, + "coverage": 1.0, + "dup": 0 + }, + "PHAROSUSDT": { + "bars": 39624, + "coverage": 1.0, + "dup": 0 + }, + "DYDXUSDT": { + "bars": 209654, + "coverage": 1.0, + "dup": 0 + }, + "MYXUSDT": { + "bars": 120836, + "coverage": 1.0, + "dup": 0 + } + }, + "checked_at": "2026-09-28T23:13:23.818885+00:00" +} \ No newline at end of file diff --git a/results/summary_5m.csv b/results/summary_5m.csv new file mode 100644 index 0000000..0e99128 --- /dev/null +++ b/results/summary_5m.csv @@ -0,0 +1,15 @@ +method,n_trades,ev_bp,ev_bp_oos,t_boot,placebo_ev_bp,rand_ev_bp,sharpe,max_dd_pct,by_year_ok +prod_mr_5m,105089,-4.46,-5.19,-7.65,0.0,0.0,-5.64,-4701.58,нет +clv_flow,257017,-9.88,-12.24,-4.15,0.0,0.0,-2.86,-25403.75,нет +pairs_btc_eth,1944,-13.22,-9.81,-5.85,0.0,0.0,-4.85,-256.08,нет +intraday_momentum,125771,-13.8,-9.2,-7.35,0.0,0.0,-4.8,-17786.84,нет +xs_reversal_15m,873500,-14.29,-14.31,-29.1,-16.69,-16.26,-20.44,-124648.41,нет +xs_reversal_costaware,873400,-14.29,-14.31,-29.1,0.0,0.0,-20.43,-124641.26,нет +hour_xs_reversal,291200,-14.7,-14.77,-35.34,0.0,0.0,-26.15,-42757.57,нет +ou_ema_costaware,1047089,-14.74,-13.66,-19.88,0.0,0.0,-13.43,-154108.02,нет +ou_ema_revert,1123903,-14.84,-13.81,-21.05,0.0,0.0,-14.46,-166544.66,нет +lead_lag_btc,2311277,-15.64,-15.67,-31.17,0.0,0.0,-23.18,-361023.26,нет +liquidity_sweep,1166054,-15.85,-16.0,-34.55,0.0,0.0,-25.57,-184485.18,нет +vwap_revert,2655400,-16.42,-16.35,-28.62,0.0,0.0,-23.3,-435944.62,нет +vol_breakout,738494,-18.51,-19.43,-24.03,0.0,0.0,-19.13,-136612.38,нет +xs_momentum_15m,873500,-18.87,-19.04,-38.48,0.0,0.0,-27.0,-164670.55,нет diff --git a/results/summary_5m.json b/results/summary_5m.json new file mode 100644 index 0000000..2906518 --- /dev/null +++ b/results/summary_5m.json @@ -0,0 +1,19 @@ +[ + { + "method": "prod_mr_5m", + "n_trades": 105089, + "ev_bp": -4.46, + "ev_bp_oos": -5.19, + "t_boot": -7.65, + "placebo_ev_bp": 0.0, + "rand_ev_bp": 0.0, + "sharpe": -5.64, + "max_dd_pct": -4701.58, + "by_year_ok": "нет", + "by_year": { + "2024": -4.72, + "2025": -3.83, + "2026": -5.0 + } + } +] \ No newline at end of file diff --git a/results/summary_5m_maker.csv b/results/summary_5m_maker.csv new file mode 100644 index 0000000..71bb3d8 --- /dev/null +++ b/results/summary_5m_maker.csv @@ -0,0 +1,15 @@ +method,n_trades,ev_bp,ev_bp_oos,t_boot,placebo_ev_bp,rand_ev_bp,sharpe,max_dd_pct,by_year_ok +prod_mr_5m,105089,2.54,1.81,4.31,0.0,0.0,3.21,-206.28,да +clv_flow,257017,-2.88,-5.24,-1.24,0.0,0.0,-0.83,-7515.49,нет +pairs_btc_eth,1944,-6.22,-2.81,-2.67,0.0,0.0,-2.25,-125.6,нет +intraday_momentum,125771,-6.8,-2.2,-3.64,0.0,0.0,-2.37,-9612.33,нет +xs_reversal_15m,873500,-7.29,-7.31,-14.85,-9.69,-9.26,-10.43,-63587.41,нет +xs_reversal_costaware,873400,-7.29,-7.31,-14.85,0.0,0.0,-10.43,-63587.26,нет +hour_xs_reversal,291200,-7.7,-7.77,-18.56,0.0,0.0,-13.7,-22437.45,нет +ou_ema_costaware,1047089,-7.74,-6.66,-10.64,0.0,0.0,-7.1,-81067.29,нет +ou_ema_revert,1123903,-7.84,-6.81,-11.5,0.0,0.0,-7.71,-88139.97,нет +lead_lag_btc,2311277,-8.64,-8.67,-19.41,0.0,0.0,-13.19,-199385.49,нет +liquidity_sweep,1166054,-8.85,-9.0,-21.45,0.0,0.0,-14.93,-102923.7,нет +vwap_revert,2655400,-9.42,-9.35,-18.64,0.0,0.0,-13.83,-250067.88,нет +vol_breakout,738494,-11.51,-12.43,-16.09,0.0,0.0,-12.11,-84977.02,нет +xs_momentum_15m,873500,-11.87,-12.04,-24.21,0.0,0.0,-16.99,-103609.55,нет diff --git a/results/summary_5m_maker.json b/results/summary_5m_maker.json new file mode 100644 index 0000000..63f8a51 --- /dev/null +++ b/results/summary_5m_maker.json @@ -0,0 +1,19 @@ +[ + { + "method": "prod_mr_5m", + "n_trades": 105089, + "ev_bp": 2.54, + "ev_bp_oos": 1.81, + "t_boot": 4.31, + "placebo_ev_bp": 0.0, + "rand_ev_bp": 0.0, + "sharpe": 3.21, + "max_dd_pct": -206.28, + "by_year_ok": "да", + "by_year": { + "2024": 2.28, + "2025": 3.17, + "2026": 2.0 + } + } +] \ No newline at end of file