Как связать Python и C++ через ZeroMQ для передачи сигналов

Как связать Python и C++ через ZeroMQ для передачи сигналов Python & API
Пошаговое руководство по созданию ультра-низкозадержечного моста для передачи торговых сигналов из Python-анализатора в C++ исполнитель с помощью ZeroMQ.
Суть: Использование ZeroMQ (ZMQ) с паттерном PUB/SUB позволяет построить ультра-низкозадержечный (sub-millisecond) мост между исследовательским Python-анализатором и высокопроизводительным C++ исполнителем ордеров без накладных расходов на брокер сообщений.

Исходный код

В современном алгоритмическом трейдинге разделение труда критично: Python идеально подходит для анализа рынка, машинного обучения и генерации сигналов, тогда как C++ незаменим для сверхбыстрого исполнения ордеров и контроля рисков. Чтобы связать эти две среды с минимальной задержкой, стандартные REST API или тяжелые брокеры сообщений (RabbitMQ, Kafka) не подходят из-за высокого оверхеда. Оптимальный выбор — библиотека ZeroMQ (ZMQ).

Для генерации качественных сигналов аналитики часто используют исторические данные, которые можно получить, изучив руководство Как извлечь исторические данные стакана L2 с Tardis.dev в Python pandas, или обрабатывают события в реальном времени, например, настраивая отслеживание ликвидаций на Binance Futures через WebSocket на Python.

Ниже представлен рабочий пример реализации связки: Python-скрипт отправляет торговый сигнал (PUB), а C++ приложение мгновенно принимает его (SUB) для последующего исполнения.

# python_publisher.py
import zmq
import time
import json

def main():
    # Инициализация контекста ZMQ
    context = zmq.Context()
    # Создаем сокет типа PUB (Publisher)
    publisher = context.socket(zmq.PUB)
    # Привязываем сокет к TCP-порту
    publisher.bind('tcp://127.0.0.1:5555')
    
    print('Python Publisher запущен на порту 5555...')
    
    while True:
        # Имитируем генерацию торгового сигнала
        signal = {
            'symbol': 'BTCUSDT',
            'side': 'BUY',
            'price': 65200.5,
            'qty': 0.1,
            'timestamp_ns': time.time_ns()
        }
        
        # Формируем сообщение с топиком для фильтрации на стороне подписчика
        message = 'TRADE_SIGNAL ' + json.dumps(signal)
        publisher.send_string(message)
        print(f'Отправлен сигнал: {message}')
        
        time.sleep(1)

if __name__ == '__main__':
    main()

А вот код высокопроизводительного подписчика на C++, использующего официальный заголовочный файл cppzmq:

// cpp_subscriber.cpp
#include 
#include 
#include 
#include 

int main() {
    // Инициализация контекста ZMQ (1 рабочий поток)
    zmq::context_t context(1);
    
    // Создаем сокет типа SUB (Subscriber)
    zmq::socket_t subscriber(context, zmq::socket_type::sub);
    
    // Подключаемся к адресу публикации
    subscriber.connect("tcp://127.0.0.1:5555");
    
    // Подписываемся только на сообщения с префиксом "TRADE_SIGNAL"
    subscriber.set(zmq::sockopt::subscribe, "TRADE_SIGNAL");
    
    std::cout << "C++ Subscriber запущен и ожидает сигналы..." << std::endl;
    
    while (true) {
        zmq::message_t message;
        // Блокирующее ожидание сообщения
        auto result = subscriber.recv(message, zmq::recv_flags::none);
        
        if (result) {
            std::string raw_msg(static_cast(message.data()), message.size());
            
            // Извлекаем топик и JSON-тело
            size_t space_pos = raw_msg.find(' ');
            if (space_pos != std::string::npos) {
                std::string topic = raw_msg.substr(0, space_pos);
                std::string payload = raw_msg.substr(space_pos + 1);
                
                auto now = std::chrono::high_resolution_clock::now();
                auto epoch = now.time_since_epoch();
                uint64_t receive_time = std::chrono::duration_cast(epoch).count();
                
                std::cout << "[" << topic << "] Получено тело: " << payload << std::endl;
                std::cout << "Время получения (наносекунды): " << receive_time << std::endl;
            }
        }
    }
    return 0;
}

Разбор параметров

  • zmq.PUB / zmq::socket_type::sub: Паттерн Издатель-Подписчик (PUB/SUB). Позволяет транслировать сигналы неограниченному числу исполнителей без блокировки отправителя.
  • tcp://127.0.0.1:5555: Протокол TCP обеспечивает гарантированную доставку пакетов. Для межпроцессного взаимодействия (IPC) на одной машине можно использовать ipc:///tmp/feeds/0, что снизит задержки еще сильнее.
  • zmq::sockopt::subscribe: Опция фильтрации сообщений на стороне подписчика. C++ исполнитель будет игнорировать любые сообщения, не начинающиеся с префикса TRADE_SIGNAL, экономя ресурсы CPU.
  • time.time_ns(): Использование наносекундных таймстампов позволяет точно измерять задержку (latency) передачи данных между Python и C++.

Как запустить

Для запуска решения вам понадобятся установленные библиотеки ZeroMQ для обоих языков.

1. Установите Python-зависимости:

pip install pyzmq

2. Установите C++ библиотеку libzmq и заголовочные файлы cppzmq. На Ubuntu/Debian это делается командой:

sudo apt-get install libzmq3-dev cppzmq-headers

3. Скомпилируйте C++ код:

g++ -O3 cpp_subscriber.cpp -o cpp_subscriber -lzmq

4. Запустите C++ исполнитель в одном терминале:

./cpp_subscriber

5. Запустите Python-анализатор во втором терминале:

python python_publisher.py

После запуска вы увидите мгновенную передачу сигналов. При интеграции в реальный продакшн не забудьте настроить систему риск-менеджмента на стороне C++ исполнителя, например, внедрив защитные механизмы, аналогичные описанным в статье Circuit Breaker на Python: Защита Торгового Бота от Flash Crash.

Оцените статью
FinFluct