К списку статей
May 17, 2025
5 мин. чтения

Создаем алгоритм маркет-мейкинга для криптовалютных пар с использованием модели Авелланеда-Стоикова

маркет-мейкинг
криптовалюта
Авелланеда-Стоиков
алгоритмическая торговля
reinforcement learning
PPO
DeFi

Привет, друзья! Сегодня я расскажу о том, как создать алгоритм маркет-мейкинга для криптовалютных пар USD+/wETH и USD+/cbbtc. Мы будем использовать модель Авелланеда-Стоикова (A-S) и дополним её алгоритмом Reinforcement Learning (PPO) для динамической оптимизации спредов. Звучит сложно? Не волнуйтесь, я разобью всё на понятные шаги, чтобы даже начинающий разработчик смог разобраться.

Что такое маркет-мейкинг и зачем он нужен?

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

Представьте, что вы - продавец на рынке, который всегда готов купить товар чуть дешевле рыночной цены и продать чуть дороже. Ваша прибыль - разница между ценами покупки и продажи. Но есть нюанс: если цена резко пойдет в одну сторону, вы можете накопить слишком много товара или, наоборот, остаться с пустыми руками.

Модель Авелланеда-Стоикова: математика на службе трейдинга

Модель A-S - это математический подход к определению оптимальных цен для маркет-мейкинга. Её главное преимущество в том, что она учитывает не только текущую рыночную цену, но и размер вашей позиции (инвентаря), волатильность рынка и риск-аппетит.

Основные формулы модели:

δ_a = S_t + (1/γ) * ln(1 + γ/k) + q_t * σ² * T
δ_b = S_t - (1/γ) * ln(1 + γ/k) - q_t * σ² * T

где:

  • δ_a и δ_b - цены продажи и покупки
  • S_t - текущая рыночная цена
  • γ - параметр риска (чем выше, тем шире спред)
  • k - интенсивность ордеров на рынке
  • q_t - текущий инвентарь
  • σ - волатильность
  • T - временной горизонт

Особенности onchain-торговли

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

  1. Latency (задержка) - в блокчейне транзакции не мгновенны, и цена может измениться до исполнения ордера
  2. Gas costs - каждая транзакция требует оплаты комиссии сети
  3. Особенности AMM/PMM - механики пулов ликвидности отличаются от традиционных бирж

Давайте посмотрим, как учесть эти факторы в нашем алгоритме.

Шаг 1: Настраиваем окружение и собираем данные

Первым делом нам нужно настроить среду для получения рыночных данных. Для этого мы будем использовать Binance API для получения текущих цен и глубины книги ордеров.

std::tuple MarketMaker::get_binance_data(const std::string& pair) {
    // В реальном коде здесь будет запрос к API Binance
    // Возвращаем: mid_price, bid, ask, bid_volume, ask_volume
    double mid_price = 2000.0;
    double bid = mid_price - 1.0;
    double ask = mid_price + 1.0;
    double bid_volume = 10.0;
    double ask_volume = 8.0;
    return {mid_price, bid, ask, bid_volume, ask_volume};
}

Также нам понадобятся onchain-метрики, такие как стоимость газа и задержка сети:

std::pair MarketMaker::get_onchain_metrics() {
    // В реальном коде здесь будет запрос к Ethereum ноде
    // Возвращаем: gas_price (wei), latency (seconds)
    return {50e9, 12.0};
}

Шаг 2: Реализуем базовую модель A-S

Теперь реализуем расчет спредов по модели A-S:

std::pair MarketMaker::calculate_spreads(double S_t, double sigma, double k, double q_t) {
    // Формула Avellaneda-Stoikov
    double spread_term = (1.0 / gamma_) * log(1.0 + gamma_ / k);
    double inventory_term = q_t * sigma * sigma * T_;
    
    double delta_a = S_t + spread_term + inventory_term;  // Ask price
    double delta_b = S_t - spread_term - inventory_term;  // Bid price
    
    return {delta_a, delta_b};
}

Обратите внимание на inventory_term. Если у нас положительный инвентарь (много актива), то цена продажи снижается, а цена покупки ещё сильнее снижается, чтобы стимулировать продажи и ограничить покупки. И наоборот для отрицательного инвентаря.

Шаг 3: Адаптируем модель для onchain-торговли

Теперь нужно учесть особенности блокчейна. Начнем с задержки (latency):

double MarketMaker::adjust_price_with_latency(double S_t, double sigma, double latency) {
    // Моделируем случайное изменение цены из-за задержки
    double latency_adjustment = utils::normal_dist(0.0, sigma * std::sqrt(latency));
    return S_t + latency_adjustment;
}

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

Теперь учтем стоимость газа:

double MarketMaker::calculate_gas_cost(double gas_price, double trade_size) {
    const double GAS_LIMIT_PER_ORDER = 100000;  // Примерное значение для ордера
    return (gas_price * GAS_LIMIT_PER_ORDER * trade_size) / 1e18;  // Конвертируем wei в ETH
}

И наконец, адаптируем спреды под особенности PMM-пулов:

std::pair MarketMaker::adjust_spreads_for_pmm(double S_t, double delta_a, double delta_b, double pool_depth) {
    // Упрощенная модель PMM: корректируем спреды в зависимости от глубины пула
    const double MIN_POOL_DEPTH = 10.0;
    double depth_factor = std::max(pool_depth, MIN_POOL_DEPTH) / MIN_POOL_DEPTH;
    
    // Уменьшаем спреды при большей глубине пула
    double spread_reduction = 1.0 / std::sqrt(depth_factor);
    double mid_price = (delta_a + delta_b) / 2;
    double new_delta_a = mid_price + (delta_a - mid_price) * spread_reduction;
    double new_delta_b = mid_price - (mid_price - delta_b) * spread_reduction;
    
    return {new_delta_a, new_delta_b};
}

Шаг 4: Управление инвентарем

Для отслеживания и управления инвентарем создадим простой класс:

class InventoryManager {
public:
    InventoryManager() : inventory_(0.0) {}
    
    void update_inventory(double size, bool is_buy) {
        inventory_ += is_buy ? size : -size;
    }
    
    double get_inventory() const {
        return inventory_;
    }
    
private:
    double inventory_;
};

Шаг 5: Объединяем всё в один алгоритм

Теперь объединим все компоненты в единый алгоритм маркет-мейкинга:

void MarketMaker::step(double S_t, double sigma, double k, double latency, double gas_cost, double trade_size) {
    // Получаем текущий инвентарь
    double current_inventory = inventory_.get_inventory();
    
    // Рассчитываем спреды на основе текущих рыночных условий и инвентаря
    auto [delta_a, delta_b] = calculate_spreads(S_t, sigma, k, current_inventory);
    auto [adjusted_delta_a, adjusted_delta_b] = adjust_spreads_for_onchain(S_t, delta_a, delta_b, latency, sigma, gas_cost, trade_size);
    
    // Генерация независимой рыночной цены
    double market_price = S_t + utils::normal_dist(0.0, sigma);
    
    // Определяем, должны ли произойти сделки на основе рыночной цены и спредов
    bool is_buy = (market_price = adjusted_delta_a);
    
    // Выполняем сделки и обновляем инвентарь
    if (is_buy) {
        inventory_.update_inventory(trade_size, true);
        std::cout  reset();
    
    // Выполнить действие и получить новое состояние, награду и флаг завершения
    std::tuple, double, bool> step(const std::array& action);
    
private:
    // Получить текущее состояние среды
    std::vector get_state() const;
    
    MarketMaker& mm_;
    double current_inventory_;
    double current_profit_;
    int current_step_;
    int max_steps_;
    
    // Текущие параметры рынка
    double mid_price_;
    double sigma_;
    double latency_;
    double pool_depth_;
    
    std::mt19937 rng_;
};

Состояние нашей среды - это вектор из текущей цены, инвентаря, волатильности, задержки сети и глубины пула. Действие - это вектор из спредов и объемов для покупки и продажи.

Теперь реализуем функцию награды:

double reward = profit_term - inventory_risk - gas_cost;

Где:

  • profit_term - прибыль от сделок
  • inventory_risk - штраф за большой инвентарь (риск)
  • gas_cost - затраты на газ

Наконец, обучим PPO-агента:

void PPOTrainer::train(int episodes) {
    for (int ep = 0; ep  states;
        std::vector actions;
        std::vector rewards;
        
        while (true) {
            // Получаем действие из политики
            auto action_probs = policy_net_->forward(torch::tensor(state));
            auto action = action_probs.multinomial(1);
            
            // Выполняем шаг в среде
            auto [next_state, reward, done] = env_.step(action);
            
            // Сохраняем переход
            states.push_back(torch::tensor(state));
            actions.push_back(action);
            rewards.push_back(reward);
            
            if (done) break;
            state = next_state;
        }
        
        // Обновляем политику PPO
        update_policy(states, actions, rewards);
    }
}

Шаг 7: Тестирование и визуализация

Для тестирования нашего алгоритма создадим простую симуляцию:

int main() {
    // Используем T = 300 секунд, как указано в задаче
    MarketMaker mm(0.1, 300.0);

    // Симуляция исторических данных для волатильности
    std::vector prices = {2000.0};
    double S_t = 2000.0;
    double trade_size = 1.0;
    double initial_sigma = 0.05;  // 5% волатильность

    for (int i = 0; i < 300; ++i) {
        std::cout << "Step " << i + 1 << ": ";

        // Получение данных (заглушки)
        auto [mid_price, bid_ask] = mm.get_binance_data("USD+/wETH");
        auto [gas_cost, latency] = mm.get_onchain_metrics();
        
        // Добавляем случайное движение цены для симуляции реального рынка
        S_t = mid_price + utils::normal_dist(0.0, mid_price * 0.01);

        // Вычисление волатильности
        double sigma = mm.calculate_volatility(prices, 5);
        if (sigma < 0.01) sigma = initial_sigma;

        // Интенсивность ордеров (заглушка)
        double k = 5.0;

        mm.step(S_t, sigma, k, latency, gas_cost, trade_size);

        // Обновление цены для следующего шага
        S_t += utils::normal_dist(0.0, S_t * 0.02);
        prices.push_back(S_t);
    }

    return 0;
}

Что дальше?

Наш алгоритм маркет-мейкинга готов, но есть множество направлений для улучшения:

  1. Подключение к реальным API: заменить заглушки на реальные запросы к Binance API и Ethereum ноде
  2. Улучшение модели волатильности: использовать GARCH или другие продвинутые модели
  3. Расширение PPO: добавить больше параметров в состояние и действие
  4. Оптимизация газа: стратегии для минимизации затрат на газ
  5. Мультиактивная стратегия: расширение на несколько пар одновременно

Заключение

Мы создали алгоритм маркет-мейкинга, который учитывает особенности onchain-торговли и использует как классическую модель A-S, так и современные методы RL. Этот подход позволяет адаптироваться к изменяющимся рыночным условиям и максимизировать прибыль при контролируемом риске.

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

Надеюсь, эта статья помогла вам лучше понять принципы маркет-мейкинга и вдохновила на создание собственных алгоритмов. Удачи в торговле!

Цитирование

@software{soloviov2025marketmakingavellanedastoikov,
  author = {Soloviov, Eugen},
  title = {Создаем алгоритм маркет-мейкинга для криптовалютных пар с использованием модели Авелланеда-Стоикова},
  year = {2025},
  url = {https://marketmaker.cc/ru/blog/post/market-making-avellaneda-stoikov},
  version = {0.1.0},
  description = {Пошаговое руководство по созданию алгоритма маркет-мейкинга для криптовалютных пар USD+/wETH и USD+/cbbtc с использованием модели Авелланеда-Стоикова и PPO. Особенности onchain-торговли, управление инвентарём, RL-обучение.}
}

MarketMaker.cc Team

Количественные исследования и стратегии

Обсудить в Telegram