Вступление: идея, цель, гипотеза

Финансовые рынки редко движутся изолированно. Криптовалюты реагируют на фондовые индексы, золото реагирует на макроэкономику, а внутри крипторынка движение биткоина задаёт направление для альткоинов.

Гипотеза проекта:

Если агрегировать данные по разным классам активов (крипто, акции, золото), измерить их волатильность, тренд и взаимную корреляцию, можно получить осмысленную вероятностную оценку того, каким будет рынок в ближайшие 24 часа: рост, падение или консолидация.

Цель скрипта — не предсказать точную цену, а оценить состояние рынка в целом, получить вероятностный прогноз и использовать его как основу для торговых стратегий и автоматизированной торговли.

Общая архитектура проекта

На логическом уровне скрипт состоит из пяти ключевых блоков:

Данные → Индикаторы → Агрегация → Корреляции → Вероятностный прогноз

Код выложен на github.

Источники данных

Используются разные рынки:

  • криптовалюты (Binance, через ccxt),

  • фондовые индексы (S&P 500, NASDAQ, Dow Jones),

  • золото (через yfinance).

Все данные приводятся к единому таймфрейму (1час), единой временной зоне UTC, ну и само собой равномерно распределяю исходя из времени свечи, чтобы не было сдвигов данных на разных активов.

Это критично: без этого любые корреляции и агрегации будут искажены.

Конфигурация и управляемость

Все ключевые параметры вынесены в один CONFIG:

CONFIG = {
    "timeframe": "1h",
    "lookback_days": 30,
    "vol_low": 0.01,
    "vol_high": 0.05,
    "trend_thr": 0.02,
    "corr_weight": 0.30,
}

Это позволить нам гибко использовать прогнозную модель в будущем, а также тестировать гипотезы и подгонять параметры для определенных торговых стратегий.

Получение данных

Получаем данные по криптовалютам

def _fetch_crypto(self, symbol: str, days: int) -> pd.DataFrame:
    since = int((datetime.now(timezone.utc) - timedelta(days=days)).timestamp() * 1000)
    raw = self.exchange.fetch_ohlcv(symbol, "1h", since=since)

    df = pd.DataFrame(raw, columns=[
        'ts', 'open', 'high', 'low', 'close', 'volume'
    ])
    df['ts'] = pd.to_datetime(df['ts'], unit='ms', utc=True)
    df.set_index('ts', inplace=True)

    df = df.resample('1H').ffill()
    return df

Что здесь происходит:

  • Загружаем OHLCV-данные с Binance через ccxt.

  • Переводим временные метки в UTC.

  • Приводим данные к равномерной часовой сетке.

Зачем это нужно:
Корреляции, волатильность и агрегаты работают корректно только при одинаковом таймфрейме.

На выходе:
DataFrame с чистыми часовыми свечами.

Получаем данные фондового рынка и золота (yFinance)

def _fetch_yf(self, symbol: str, days: int) -> pd.DataFrame:
    end = datetime.now(timezone.utc)
    start = end - timedelta(days=days)

    df = yf.download(symbol, start=start, end=end, interval="1h")
    df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
    df.columns = ['open', 'high', 'low', 'close', 'volume']

    df.index = pd.to_datetime(df.index, utc=True)
    df = df.resample('1H').ffill()
    return df

Что происходит:

  • Загружаем индексы и золото.

  • Приводим названия колонок к единому формату.

  • Ресемплируем данные.

Почему не Binance:
Индексы и золото удобнее и стабильнее получать через yfinance.

Расчёт индикаторов для одного актива

def _asset_indicators(self, df: pd.DataFrame) -> dict:
    close = df['close']

    # Momentum (24h ROC)
    roc = (close.iloc[-1] - close.iloc[-25]) / close.iloc[-25]

    # ATR %
    atr = self._atr(df)
    atr_pct = atr.iloc[-1] / close.iloc[-1]

    # EMA trend
    ema12 = close.ewm(span=12).mean()
    ema26 = close.ewm(span=26).mean()
    trend = (ema12.iloc[-1] - ema26.iloc[-1]) / ema26.iloc[-1]

    # Volume ratio
    vol_ratio = df['volume'].iloc[-1] / df['volume'].rolling(24).mean().iloc[-1]

    return {
        "roc_24h": roc,
        "atr_pct": atr_pct,
        "trend_strength": trend,
        "volume_ratio": vol_ratio
    }

1. Momentum (ROC 24 часа)

roc = (price_now - price_24h_ago) / price_24h_ago

Интерпретация:

> 0 — рынок растёт

< 0 — рынок падает

Используется как базовый индикатор направления.

2. Волатильность (ATR в процентах)

atr_pct = ATR / price

Почему в процентах: Абсолютный ATR не сопоставим между BTC и, например, ADA. Процентная форма решает эту проблему.

3. Тренд через EMA

trend = (EMA12 - EMA26) / EMA26

Простая и устойчивая оценка:

  • положительное значение — восходящий тренд;

  • отрицательное — нисходящий.

Также мы можем использовать модели через прочие скользящие средние. Это может быть использование tema, линий macd и прочих.

4. Объём

volume_ratio = current_volume / avg_24h_volume

Объём позволяет выявить всплески интереса, подтвердить тренд и выявить фразу распределение накопления.

Агрегация индикаторов по рынку

def aggregate(self) -> dict:
    rows = []

    for df in self.data.values():
        ind = self._asset_indicators(df)
        rows.append(ind)

    agg = pd.DataFrame(rows).mean().to_dict()
    return agg

Что делаем:

  • рассчитываем индикаторы для каждого актива;

  • усредняем значения.

Почему усреднение работает:
Мы оцениваем не конкретный актив, а состояние рынка в целом.

Корреляционный анализ

def correlations(self) -> dict:
    btc = self.data["BTC/USDT"]['close'].tail(24)

    result = {}
    for symbol, df in self.data.items():
        if symbol == "BTC/USDT":
            continue

        series = df['close'].tail(24)
        corr, _ = pearsonr(btc, series)
        result[f"{symbol}_vs_BTC"] = corr

    result["avg_corr"] = np.mean(list(result.values()))
    return result

Здесь нам необходимо получить данные о корреляциях.

Что здесь происходит:

  • берём BTC как якорный актив;

  • считаем корреляции с остальными рынками;

  • вычисляем среднюю корреляцию.

Зачем это нужно:

  • высокая корреляция → рынок движется синхронно;

  • низкая → повышенная неопределённость и флет.

Вероятностная модель прогноза

Ключевая особенность проекта — отказ от бинарных сигналов вида buy / sell.
Вместо этого используется вероятностная модель, оценивающая три сценария поведения рынка на ближайшие 24 часа:

  • рост (upward);

  • падение (downward);

  • консолидация (consolidation).

Такой подход лучше отражает реальность рынка: в каждый момент времени существует несколько возможных сценариев, а не один “правильный”.

Базовая инициализация вероятностей

up = down = cons = 1 / 3

Модель стартует из нейтрального состояния, в котором рынок с равной вероятности может падать, расти или флетиться. Ни один сценарий не имеет преимущества, но почти сразу после запуска расчитываются уже реальные вероятности, основываясь на представленной ранее модели.

Это принципиально важно — модель не имеет смещения и не “верит” в рост или падение заранее.

Учет импульса (Momentum / ROC)

roc = ind.get('roc_24h', 0.0)

if roc > CONFIG["trend_thr"]:
    up += 0.20
    down -= 0.10
    cons -= 0.10
elif roc < -CONFIG["trend_thr"]:
    down += 0.20
    up -= 0.10
    cons -= 0.10

Экономический смысл:

  • сильное движение за последние 24 часа редко полностью исчезает на следующий день;

  • импульс увеличивает вероятность продолжения движения, но не гарантирует его.

Что делает модель:

  • усиливает сценарий по направлению импульса;

  • снижает альтернативные сценарии, но не обнуляет их.

Учет волатильности (ATR %)

vol = ind.get('atr_pct', 0.0)

if vol < CONFIG["vol_low"]:
    cons += 0.20
    up -= 0.10
    down -= 0.10
elif vol > CONFIG["vol_high"]:
    if ind.get('trend_strength', 0.0) > 0:
        up += 0.15
    else:
        down += 0.15
    cons -= 0.15

Интерпретация:

  • низкая волатильность → рынок не готов к сильному движению;

  • высокая волатильность → рынок уже “раскачан” и движение вероятнее.

При этом направление усиливается только при наличии тренда, что снижает шум.

Учет объёма

vratio = ind.get('volume_ratio', 1.0)

if vratio > 1.5:
    if ind.get('trend_strength', 0.0) > 0:
        up += 0.10
    else:
        down += 0.10
    cons -= 0.10

Почему объём важен:

  • объём — это подтверждение участия капитала;

  • движение без объёма статистически менее устойчиво.

Модель использует объём не как триггер, а как усилитель уже существующего направления.

Учет межрыночных корреляций

avg_c = corr.get('avg_corr', 0.0)
c_adj = CONFIG["corr_weight"] * abs(avg_c)

if abs(avg_c) < 0.5:
    cons += c_adj
    up -= c_adj / 2
    down -= c_adj / 2
else:
    gold_roc = ind.get('gold_roc', 0.0)
    if gold_roc > 0 and avg_c > 0:
        up += c_adj / 2
    elif gold_roc < 0 and avg_c > 0:
        down += c_adj / 2

Логика:

  • низкая корреляция между рынками → высокая неопределённость;

  • высокая корреляция → рынок следует глобальному макро-направлению.

Золото используется как макро-фильтр:

  • рост золота при высокой корреляции часто указывает на risk-off;

  • падение — на risk-on.

Нормализация вероятностей

total = up + down + cons

up /= total
down /= total
cons /= total

После всех корректировок вероятности:

  • могут выходить за диапазон;

  • могут не суммироваться в 1.

Нормализация решает эту проблему и возвращает корректное распределение.

Математика модели: приоритеты и характеристики движения рынка

Приоритетность факторов

Факторы в модели имеют иерархию, а не равные веса:

  1. Импульс (Momentum) — определяет базовое направление.

  2. Волатильность — определяет возможность движения.

  3. Тренд — подтверждает устойчивость.

  4. Объём — подтверждает участие капитала.

  5. Корреляции — корректируют сценарий на уровне всего рынка.

Это отражает реальную структуру рынка cначала появляется движение, затем волатильность и объём, и только потом рынок синхронизируется глобально.

Характеристики движения рынка

Модель различает три принципиально разных состояния рынка:

1. Трендовое движение

  • положительный ROC;

  • высокая волатильность;

  • подтверждённый EMA-тренд;

  • объём выше среднего.

2. Импульсное движение

  • высокий ROC;

  • скачок волатильности;

  • объём растёт, но тренд ещё нестабилен.

3. Консолидация

  • низкий ROC;

  • низкая ATR;

  • слабые корреляции;

  • отсутствие объёма.

Каждому состоянию соответствует разное распределение вероятностей, а не одинаковый сигнал.

Почему вероятности, а не сигналы

Использование вероятностей позволяет грамотно фильтровать входы, выбирать размер позиции, адаптировать стратегию под режимы рынка и избегать торговли в неопределенности. Поэтому такой софт полезен даже для ручной торговли.

Модель может указать нам на состояние рынка в текущей момент - что при наличии определенного нарратива может помочь нам принять грамотное решение.

Выход в консоль

Market forecast: {
  "timestamp_utc": "2025-12-22T09:30:23.229039+00:00",
  "probabilities": {
    "upward": 0.1333,
    "downward": 0.4333,
    "consolidation": 0.4333
  },
  "indicators": {
    "roc_24h": -0.02796597685998984,
    "atr_pct": 0.006690744224154546,
    "trend_strength": -0.005271170297204962,
    "volume_ratio": 0.394397730504669,
    "crypto_cnt": 10,
    "stock_cnt": 0,
    "gold_roc": 0.0
  },
  "correlations": {
    "ETH/USDT_vs_BTC": 0.9975288228710706,
    "SOL/USDT_vs_BTC": 0.9811768939562674,
    "ADA/USDT_vs_BTC": 0.9907696697577224,
    "DOT/USDT_vs_BTC": 0.823343645765213,
    "LINK/USDT_vs_BTC": 0.9543745729318979,
    "LTC/USDT_vs_BTC": 0.9508485363364672,
    "TRX/USDT_vs_BTC": 0.9222384210578161,
    "AVAX/USDT_vs_BTC": 0.9603413942455601,
    "DOGE/USDT_vs_BTC": 0.9765121574347357,
    "avg_corr": 0.9507926793729722
  }
}

Тут скрипт нам выводит основные коэффициенты корреляций, индикаторы и самое главное сверху - рыночные вероятности. В этом примере вероятность роста 13%, падения 43% и консолидации 43%. Исходя из этих данных - сегодняшний день неплохой для открытия шортовых позиций, что сочетается и с моим ict анализом.

Вывод: возможности модели и практическое применение

В рамках проекта была реализована вероятностная модель оценки рыночного состояния, которая опирается не на один индикатор или источник данных, а на совокупность факторов: импульс, волатильность, тренд, объём и межрыночные корреляции.

Ключевая особенность подхода заключается в том, что модель не пытается предсказать цену и не генерирует жёсткие торговые сигналы. Вместо этого она формирует контекст рынка, описывая его через распределение вероятностей между тремя базовыми сценариями: рост, падение и консолидация.

Что даёт такая модель на практике

  1. Фильтрация торговых стратегий
    Модель может использоваться как верхнеуровневый фильтр: отключать трендовые стратегии в фазе консолидации, снижать риск при неопределённости

  2. Адаптивное управление риском
    Вероятностный выход позволяет управлять размером позиции: регулировать плечо и маржу, а также изменять тейки и стопы под состояние рынка.

  3. Контекст для алгоритмической торговли
    Модель хорошо ложится в архитектуру торговых ботов как внешний аналитический сервис.

  4. Кросс-рыночный анализ
    За счёт использования индексов и золота модель позволяет учитывать макроэкономические сдвиги, корреляции рынков и включать "режим risk on/risk off".

    Это особенно важно для крипторынка, который всё чаще реагирует на внешние макрофакторы.