👀 PairScan

Методология

Hurst exponent, ADF-test, walk-forward бэктест без lookahead — математика за каждым сигналом PairScan.

Обновлено:

Если ты пришёл с r/algotrading или Quantocracy — эта страница для тебя. Ниже — точная математика за каждым сигналом, который выдаёт PairScan. Никаких чёрных ящиков, никакого «proprietary AI» — только классическая статистика 50-х и 70-х, применённая к крипто- и tokenized-equity ratio. Reference-имплементация каждого фильтра — open-source под MIT (см. pairscan-rmrpip install pairscan-rmr).

Почему pair trading на ratio

Направленная сделка требует предсказывать цену. Ratio-сделка требует только распознать, когда отношение двух коррелированных активов вышло за свой исторический диапазон — это куда более слабая claim. Корреляция делает работу за тебя; ты просто продаёшь актив, который улетел вперёд, и покупаешь тот, что отстал.

Конкретный пример: предположим, ETH и BTC оба выросли на 50 % за год, но ETH вёл движение и оказался на отметке 0.075 BTC при средней за 18 месяцев 0.060. Направленная сделка — это «BTC до $90 k к Q4». Ratio-сделка — это «ETH/BTC = 0.075 это два стандартных отклонения выше скользящего диапазона; уменьшить ETH, добавить BTC». Тебе не нужно знать, куда пойдёт биткоин — нужно только чтобы ratio вернулся к 0.060. Если оба актива растут вместе — ratio дрейфует обратно; если оба падают — то же; ломает сделку только устойчивый сдвиг доминирования.

Чтобы это было прибыльным в реальном исполнении, три вещи должны выполняться одновременно:

  1. Ratio mean-revert'ит (не трендит).
  2. Границы достаточно широкие, чтобы покрыть round-trip slippage + fee.
  3. У пары достаточно ликвидности под твой размер.

Фильтры 1 и 2 — статистические. Фильтр 3 — операционный. Все три проверяются до того как пара попадёт в скрин.

Четыре фильтра

Каждая пара проходит четыре независимых теста на скользящем 540-дневном окне log(price_A / price_B). Пара должна пройти все четыре, чтобы попасть в скрин. Пороги ниже намеренно мягкие — см. секцию мягкие пороги.

Hurst exponent (R/S analysis)

Hurst exponent измеряет долгосрочную память серии. Для self-similar процесса rescaled range R/S на лаге n растёт как n^H. Мы оцениваем H посчитав R/S на нескольких лагах, рисуя log–log и читая наклон:

  • H < 0.5 — anti-persistent / mean-reverting (то что нам надо)
  • H ≈ 0.5 — random walk
  • H > 0.5 — persistent / trending

Считаем через R/S analysis на 540-дневном log-ratio. Reference-имплементация:

import numpy as np

def hurst_rs(series, max_lag=100):
    """Hurst exponent через R/S analysis."""
    lags = np.unique(np.geomspace(10, max_lag, 12).astype(int))
    rs_values, valid_lags = [], []
    for lag in lags:
        n_chunks = len(series) // lag
        if n_chunks < 2: continue
        chunks = series[:n_chunks * lag].reshape(n_chunks, lag)
        means = chunks.mean(axis=1, keepdims=True)
        cumdev = (chunks - means).cumsum(axis=1)
        ranges = cumdev.max(axis=1) - cumdev.min(axis=1)
        stds = chunks.std(axis=1, ddof=1)
        valid = stds > 0
        rs_values.append(np.mean(ranges[valid] / stds[valid]))
        valid_lags.append(lag)
    slope, _ = np.polyfit(np.log(valid_lags), np.log(rs_values), 1)
    return slope

Требуем H < 0.5 для включения. Это щедрый порог — многие quant-desks ставят H < 0.45 или даже < 0.4 — но мягкие пороги намеренные: лучше пропустим больше кандидатов и дадим walk-forward бэктесту отфильтровать дальше, чем срежем рано на шумном эстиматоре. У R/S статистики есть документированный small-lag bias (Lo, 1991), поэтому мы ограничиваем регрессию лагами ≥ 10 даже если пользователь передал меньший max_lag.

Тот же самый код живёт в pairscan-rmr на src/pairscan_rmr/filters.py.

Augmented Dickey-Fuller тест

ADF тестирует, стационарна ли серия (отклонение unit root) или нестационарна. Применяется к log(A/B) на 540 дней, длина лагов выбирается минимизацией AIC. Берём statsmodels напрямую:

from statsmodels.tsa.stattools import adfuller

def adf_pvalue(series, autolag="AIC"):
    """Возвращает только p-value; меньше = более стационарная."""
    result = adfuller(series, autolag=autolag)
    return float(result[1])

Требуем ADF p-value < 0.7 — снова щедрый порог. Строгая академия использует p < 0.05; мы — 0.7, потому что:

  • ADF имеет низкую statistical power на коротких выборках (а крипто-истории короткие),
  • У нас уже есть Hurst как параллельный фильтр — оба должны согласоваться,
  • Walk-forward бэктест — это и есть реальный decision-maker; ADF и Hurst это просто guards против явно неподходящих пар.

Пара которая показывает Hurst 0.45 и ADF 0.6 — пограничный кандидат; пара с Hurst 0.55 отлетает сразу. И Hurst-failing, и ADF-failing пары встречаются в нашем universe — мы исключаем порядка 30–40 % номинально cointegrated пар на этапе фильтров.

Ширина диапазона и чередующиеся касания

Пара может пройти Hurst и ADF и при этом быть бесполезной — если диапазон слишком узкий (slippage съедает edge) или если цена касалась только одной границы (нет реального колебания). Два операционных фильтра ловят это:

  • Ширина диапазона. Считаем (P95 − P5) от log-ratio на 540-дневном окне. Требуем минимум 0.4 log-юнита, что эквивалентно ~50 % размаху underlying ratio. Меньше — и 0.1 % swap fee плюс реалистичный slippage съедают больше половины per-cycle gain.
  • Чередующиеся касания. Идём по серии и считаем low-side касания (value ≤ P5) и high-side касания (value ≥ P95), но засчитываем только если предыдущее касание было на противоположной стороне. Монотонная серия, которая один раз посетила P5 и потом дрейфовала вверх — получает одно low-касание и одно high-касание, не пять каждого. Требуем ≥ 2 чередующихся касания на сторону, что значит пара реально колебалась четыре раза в lookback.

Alternating-constraint убивает «фейковых mean-reverter'ов» — пары, чей log-ratio выглядит ограниченным только потому что они обвалились в начале окна и с тех пор восстанавливаются.

Volume gate

Обе ноги должны иметь 24h спот-объём > $1 M хотя бы на одной из {Binance, Bybit, OKX, KuCoin}. Это не статистика — это операционный порог. Ниже $1 M/день slippage и спред съедают всю rebalancing-премию, и математика не работает в реальном исполнении. Gate non-negotiable; лучше пропустим красиво mean-reverting micro-cap чем рекомендуем пару, которую ты не можешь реально торговать в размере.

Walk-forward бэктест

Когда пара прошла все четыре фильтра — бэктестим 360 дней с той же логикой сигналов, что использует live-система. Критическая деталь: no lookahead.

Для каждого дня t в окне бэктеста мы считаем rolling P5 и P95 на последних rolling_window_days (default 180) днях — только тех данных, которые были бы доступны трейдеру на день t. Сегодняшние данные не используются в сегодняшнем сигнале. Это и есть разница между честным бэктестом и упражнением в подгонке.

import numpy as np

def walk_forward_backtest(price_a, price_b, lookback=540,
                          entry_low=0.2, entry_high=0.8, fee=0.001):
    log_ratio = np.log(price_a / price_b)
    a_qty, b_qty, holding_a = 100.0, 0.0, True
    trades = []
    for t in range(lookback, len(price_a)):
        # КРИТИЧНО: окно строго ДО t — никаких future данных.
        window = log_ratio[t - lookback:t]
        p_low, p_high = np.percentile(window, [5, 95])
        position = (log_ratio[t - 1] - p_low) / max(p_high - p_low, 1e-9)
        if position <= entry_low and not holding_a:
            a_qty = b_qty * price_b[t] / price_a[t] * (1 - fee)
            b_qty = 0.0; holding_a = True
            trades.append(("B→A", t))
        elif position >= entry_high and holding_a:
            b_qty = a_qty * price_a[t] / price_b[t] * (1 - fee)
            a_qty = 0.0; holding_a = False
            trades.append(("A→B", t))
    return a_qty, b_qty, trades

Наивная альтернатива — использовать np.percentile(log_ratio, [5, 95]) на полной серии включая future данные — типично завышает accumulation стратегии на 10–30 %. Этот класс багов мы ловим regression-тестом, который запускает бэктест дважды: один раз на чистых данных, второй — со всеми ценами после midpoint заменёнными на garbage. Сделки до midpoint должны быть byte-identical между запусками. Если future-dependent статистика когда-нибудь проникнет — тест фейлится сразу. Probe лежит в tests/test_no_lookahead.py.

Почему мы используем мягкие пороги

Стандартный quant-инстинкт — затягивать фильтры до тех пор, пока бэктесты не станут чистыми. Мы намеренно так не делаем. Мягкий Hurst (< 0.5), мягкий ADF (< 0.7), щедрая ширина диапазона (40 %) — результат: больше кандидатов попадает в скрин, а walk-forward бэктест делает финальную фильтрацию, показывая какие реально накапливают количество.

Это размен false-positive на false-negative. Лучше показать десять кандидатов и дать тебе выбрать три, которым ты доверяешь — чем показать одну «чистую» пару, которая случайно бэктестилась хорошо. Quant trading на маленьких выборках — это фундаментально high-noise среда; затягивание фильтров сужает воронку, но реально не улучшает signal-to-noise того что проходит.

С другой стороны: у каждого фильтра есть явный override в API pairscan-rmr (is_mean_reverting(price_a, price_b, hurst_threshold=0.45, ...)). Если у тебя сильный prior — institutional desk policy, более жёсткая risk tolerance — поставь threshold сам.

Чего мы явно не делаем

  • Не предсказываем цену. Никакого ML, sentiment, on-chain forensics.
  • Не используем плечо, не шортим. Капитал всегда на 100% в одной из двух ног.
  • Не auto-execute. Мы говорим, когда свопать; ордер ты ставишь сам на своей бирже.
  • Не обещаем. Mean-reversion стратегии ломаются в направленных режимах — см. Где стратегия не работает.

Ограничения

Это известные failure-режимы методологии. Никаких сюрпризов и ничего лёгкого в починке.

  • Hurst R/S — шумный эстиматор на коротких окнах. На 540-дневном окне standard error для H примерно ±0.05. Две пары с идентичными underlying процессами могут оказаться по разные стороны 0.5-cutoff'а по случайности. Митигируем щедрым порогом и требованием чтобы все четыре фильтра прошли — но пара с H = 0.49 не значимо отличается от пары с H = 0.51.
  • ADF предполагает stationary residuals. Структурные сломы (токен форкается, биржа делистит ногу, стейблкоин теряет peg) делают тест misleading. Pre-2022 LUNA выглядел идеально стационарным до того дня, когда не выглядел. Наш peg-check oracle layer ловит самые очевидные случаи для tokenized активов, но никакой статистический тест не детектит regime-changes которые ещё не случились.
  • Тесты descriptive, не predictive. Hurst, ADF и range-фильтры все описывают прошлые 540 дней. Past mean-reversion не гарантирует future mean-reversion — он только говорит, что пара была mean-reverting в lookback'е. Если underlying relationship сломается после конца lookback'а, скрин будет продолжать показывать пару как кандидата пока не накопится достаточно свежих данных чтобы перевернуть verdict.
  • Размер выборки имеет значение. Ниже 200 дней истории ни один из этих тестов не имеет meaningful power. Мы отказываемся скорить пары с менее 540 дней; даже там результаты слабее чем для пар с 5+ годами данных. Новые токены стартуют в нашем universe с verdict'ом «история слишком короткая», независимо от того как mean-reverting они выглядят intra-window.
  • Реальное исполнение добавляет slippage, налоги, exchange downtime — ничего не моделируется. Бэктесты предполагают, что ты можешь исполниться по close-цене дня по 0.1 % taker. На практике ты исполняешься по следующей доступной цене, в понедельник утром твоя биржа может быть недоступна час, а твоя юрисдикция может забрать 20 % любого realised gain. Стратегия всё ещё работает после этих frictions для ликвидных пар; перестаёт работать для менее ликвидных.
  • xStocks-specific риски. Tokenized equities младше 12 месяцев на момент написания. Mean-reversion claims на такой короткой истории — спекулятивные. Backed Finance в прошлом приостанавливал redemptions во время peg-events. Если твоя юрисдикция ограничивает tokenized securities, ничего из этого не применимо.

Open questions, над которыми мы работаем

Короткий список вещей, которые мы реально не знаем, в порядке частоты возникновения:

  1. Оптимальная длина lookback'а. Используем 540 дней потому что это примерно один BTC halving cycle. Короче окно — реагирует быстрее на regime shifts, но overfit'ится к недавнему шуму; длиннее — стабильнее, но привязано к истории, которая может уже не применяться (например, 2017 alt-season). Реально ли 540 правильный ответ для крипто-нативных пар vs. крипто-equity cross-asset пар — открытый вопрос.
  2. Hurst vs. variance-ratio тест. R/S Hurst — классический эстиматор, но variance-ratio (Lo & MacKinlay, 1988) и detrended fluctuation analysis (DFA, Peng et al. 1994) имеют меньший bias на коротких выборках. Сравниваем все три на production universe; пока результаты говорят что variance-ratio даёт номинально чище числа, но порядок пар по силе сигнала по сути тот же. Стоит дальнейшего исследования.
  3. Bayesian posterior на «эта пара ещё mean-revert'ит?». Сейчас verdict бинарный (прошла фильтры / не прошла). Posterior probability которая обновляется по мере прихода новых данных дала бы лучшее коммуникирование uncertainty и quantify regime-change risk. Открытый вопрос — стоит ли дополнительная сложность для retail-facing surfaces.
  4. Cross-asset peg integrity. Для tokenized equities cross-check'аем против Pyth oracles на каждом screening cycle. Для tokenized commodities (PAXG, XAUT) используем yfinance для underlying и допускаем большие drift bands. Правильное число для этих bands не well-established эмпирически; используем 1.5 % на основе observed drift distributions, но плausibly могло бы быть жёстче.
  5. Trade-cost моделирование. Допущение 0.1 % taker fee — консервативное для крупных CEX и агрессивное для тонких DEX-pool. Более честная модель скейлила бы fees по venue и pair liquidity. То же для slippage — сейчас игнорируем. Оба в roadmap; не сделано.

Если у тебя есть данные или pointer'ы по любому из этих вопросов — @pairscan в X или открой issue в pairscan-rmr репо.

Long-tail RWA — что подходит модели и что нет

Нас регулярно спрашивают, можно ли применить ratio screening к другим токенизированным real-world активам — фракционная недвижимость (RealT), yield-tokenized деривативы (Pendle PT/YT), токенизированные treasuries (USDY, OUSG, BUIDL, USDM). Честный ответ — "в основном нет", и стоит объяснить почему, чтобы скоуп был ясен:

Фракционная недвижимость (RealT, Lofty, etc.) — каждый property-токен уникален. Нет непрерывной live-цены; оценка приходит из квартальных off-chain appraisals. Вторичные рынки тонкие и эпизодические. Ratio между двумя RealT-токенами — это не market-discovered ratio, это соотношение двух устаревших оценок. Логика mean-reversion не применяется — нет high-frequency mean'а, к которому можно revert'иться. Мы их не скринем.

Pendle PT/YT (Principal / Yield Tokens) — PT торгуется со скидкой к underlying yield-bearing токену (например PT-aUSDC) и monotonically растёт к face value к maturity. YT накапливает yield и распадается до нуля к maturity. У обоих предсказуемые детерминистические price paths, определяемые time-to-maturity и underlying yield rate. Это противоположность mean-reversion — известный monotone trend. Правильная модель для Pendle — yield-curve fitting, не pair screening.

Rebase-style токенизированные treasuries (USDY / USTB / MTBILL) — добавлены в PairScan начиная с Phase 5, но с оговорками. On-chain цена каждого токена равна текущему per-token NAV ($1 + накопленный yield), не плоско $1. Пара одного из них против USDC даёт медленный upward drift ~5%/год (это yield) — это trend, не stationary ratio. Единственные plausible mean-reverting пары — в одном классе: USDY/USTB, USDY/MTBILL. Но даже они зависят от mean-reversion'а yield-spread'а, который намного медленнее чем equity / crypto пары для которых заточен скрин. Мы добавили asset class, чтобы peg-check infrastructure существовала для юзеров мониторящих drift, но не рекомендуем торговать эти пары только по screen-сигналам — 90-дневное окно, которое мы override'им вниз, на грани undersampled, noise floor высокий.

OUSG, BUIDL, USDM, redemption-only токены — нет Pyth или Chainlink oracle feed на 2026-05. Без reference-цены не можем запустить peg-check, а без peg-check on-DEX цена может произвольно отклоняться на любом блоке и мы не поймаем. Эти токены сознательно не whitelist'им до появления oracle coverage.

Короткая версия: ratio mean-reversion — острый инструмент для узкого набора asset classes. Работает на stationary log-ratios со стабильной variance и >100 дней joint history. Большинство "long-tail RWA" этим условиям не удовлетворяет, и лучше прямо это сказать, чем шипить screener, который misclassify'ит known-trending series как mean-reverting candidates.

Open-source утилита

Ядро фильтров и backtest-движок опубликованы как Python-библиотека под MIT: github.com/pairscan/ratio-mean-reversion.

pip install pairscan-rmr
from pairscan_rmr import is_mean_reverting, walk_forward_backtest

result = is_mean_reverting(price_a, price_b)
if result.passed:
    backtest = walk_forward_backtest(price_a, price_b)
    print(f"{backtest.n_trades} сделок, max DD {backtest.max_drawdown:.1%}")

Тот же код, что работает в продакшене, работает в библиотеке. Если найдёшь баг — это тот же баг что у нас.

Источники

  • Hurst, H.E. (1951). «Long-Term Storage Capacity of Reservoirs». Transactions of the American Society of Civil Engineers, 116, 770–799.
  • Dickey, D.A. & Fuller, W.A. (1979). «Distribution of the Estimators for Autoregressive Time Series with a Unit Root». JASA, 74(366a), 427–431.
  • Lo, A.W. (1991). «Long-Term Memory in Stock Market Prices». Econometrica, 59(5), 1279–1313. — Стандартный reference на small-sample bias в R/S Hurst.
  • Lo, A.W. & MacKinlay, A.C. (1988). «Stock Market Prices Do Not Follow Random Walks». Review of Financial Studies, 1(1), 41–66. — Variance-ratio test, альтернатива Hurst.
  • Gatev, E., Goetzmann, W.N. & Rouwenhorst, K.G. (2006). «Pairs Trading: Performance of a Relative-Value Arbitrage Rule». Review of Financial Studies, 19(3), 797–827.
  • Peng, C.-K. et al. (1994). «Mosaic organization of DNA nucleotides». Physical Review E, 49(2), 1685. — Detrended fluctuation analysis (DFA), обсуждается под «open questions».