Как хранить рыночные данные, не замедляя бота
Записывать всё — это то, что делает систему честной. Записывать синхронно — это то, что заставляет её пропускать рынок. Лечение скучное, и сделать его правильно стоит рано.
Инженеры Vizanix · об авторе
- РАЗДЕЛ
- Архитектура ботов
- ОПУБЛИКОВАНО
- 2026-08-31
- ГЛАВ
- 5
- ЧИТАТЬ ДАЛЬШЕ
- 3
- ЯЗЫК
- написано на русском
Смешивают две разные задачи. Хранение рыночных данных для исследования — задача пропускной способности. Хранение операционного состояния работающего бота — задача задержки. Им нужны разные решения, и одно решение на обе делает исследование медленным либо торговлю опоздавшей.
Три пути, три инструмента
| Путь | Требование | Разумный выбор |
|---|---|---|
| Горячий — торговый цикл | Микросекунды, без аллокаций | Кольцевые буферы в памяти. Без базы. |
| Тёплый — операционное состояние и телеметрия | Долговечность, запросы, секунды приемлемы | SQLite с WAL или Postgres при нескольких процессах |
| Холодный — исследовательский архив | Пропускная способность и сжатие, минуты приемлемы | Файлы Parquet с разбиением по символу и дате |
Наш микроструктурный движок держит признаки в преаллоцированных кольцевых буферах без pandas в реалтайм-цикле, а фаза сбора пишет сырые события в Parquet. Эти двое никогда не делят код, потому что требования у них противоположны.
Никогда не пишите с горячего пути
Сканер, блокирующийся на записи на диск, перестал быть сканером. Паттерн — ограниченная очередь и поток-писатель, и очередь обязана быть ограниченной, иначе медленный диск превращается в неограниченный рост памяти.
# Ограничена намеренно: потерять телеметрию лучше, чем застопорить цикл.
queue: asyncio.Queue = asyncio.Queue(maxsize=50_000)
def record(event):
try:
queue.put_nowait(event)
except asyncio.QueueFull:
metrics.increment("telemetry_dropped") # видимо, а не молча
async def writer():
while True:
batch = [await queue.get()]
while len(batch) < 1000 and not queue.empty():
batch.append(queue.get_nowait())
await asyncio.to_thread(flush, batch) # одна транзакция на пачкуСчётчик потерянных событий важен. Система, тихо отбрасывающая телеметрию под нагрузкой, выглядит здоровой ровно в тех условиях, данные о которых нужны больше всего.
Батчить по-настоящему
Одна вставка на событие — самая частая причина того, что слой хранения не справляется. Тысяча вставок в одной транзакции не в тысячу раз быстрее, но достаточно близко, чтобы менять границу возможного.
Наш сигнальный движок хранит 5,76 миллиона оценок за пятнадцать суток — каждую оценку со значениями факторов, а не только сработавшие. Это примерно 384 тысячи строк в сутки: ничем не примечательно при батчинге и невозможно по одной на пути оценки.
Индексируйте под те запросы, которые выполняются
Не под воображаемые. На практике для торговой системы это:
- Последние N строк по одному символу —
(symbol, id DESC) - Всё в диапазоне времени —
(ts) - Недоставленные или сбойные элементы — частичный индекс по колонке статуса
- Агрегация по дням для отчётности — обычно закрывается индексом по времени
Четыре индекса покрывают почти всё. Каждый дополнительный замедляет каждую запись, а на тёплом пути именно записи вам и не хватает.
Политика хранения, решённая заранее
Объём растёт быстрее ожидаемого, а решение удалять всегда принимается под давлением. Задайте политику до того, как это станет срочным:
- Сырые события стакана: дорого, полезно только для микроструктурного исследования. Держите заданное окно, если точно не нужно больше.
- Свечи: дёшево и полезно бессрочно. Держите.
- Оценки и решения: журнал аудита. Держите столько, сколько может понадобиться, чтобы объяснить сделку.
- Сделки и снимки эквити: держите вечно. Они малы и они и есть запись.
Наш конвейер свечей ведёт скользящий архив за семь суток с автоматическим доливом 1450 пар «монета — таймфрейм». Старые данные удаляются по расписанию, а не когда заполнится диск, — в этом разница между регламентной работой и инцидентом.
Статья описывает инженерную практику. Это не инвестиционная рекомендация. Vizanix разрабатывает программное обеспечение и не обещает торговую доходность.