Дисклеймер: Информация в этой статье предоставлена исключительно в образовательных и ознакомительных целях и не является финансовым, инвестиционным или торговым советом. Торговля криптовалютами сопряжена с высоким риском убытков.
Дисклеймер: информация в этой статье предназначена исключительно для образовательных и информационных целей и не является финансовой, инвестиционной или торговой рекомендацией. Торговля криптовалютами сопряжена со значительным риском потерь.
Добро пожаловать во вторую часть нашей серии о QuestDB. В Части 1 мы рассмотрели трёхуровневую архитектуру хранения и принципы проектирования схем. Теперь переходим к по-настоящему отличительному набору возможностей — SQL-расширениям, благодаря которым QuestDB ощущается как база данных, созданная трейдерами для трейдеров.
Стандартный SQL был создан в 1970-х годах для реляционных данных. Он ничего не знает о времени как концепции первого класса. Любая операция над временны́ми рядами в PostgreSQL или MySQL требует громоздких обходных решений — оконных функций, LATERAL JOIN'ов, CTEs в три уровня вложенности. Расширения QuestDB сворачивают эти многострочные запросы в одиночные выразительные операторы.
Разберём каждый из них на реальных торговых примерах.
SAMPLE BY: нативная агрегация по временны́м интервалам
Если и есть один запрос, который каждая торговая система выполняет чаще любого другого, — это агрегация OHLCV, то есть превращение сырых сделок в данные для свечных графиков. В стандартном SQL это выглядело бы примерно так:
-- Стандартный SQL: многословно и медленноSELECT
date_trunc('minute', timestamp) AS bucket,
symbol,
(array_agg(price ORDERBYtimestamp))[1] ASopen,
max(price) AS high,
min(price) AS low,
(array_agg(price ORDERBYtimestampDESC))[1] ASclose,
sum(quantity) AS volume
FROM trades
WHEREtimestamp>= now() -interval'1 hour'GROUPBY bucket, symbol
ORDERBY bucket;
В QuestDB это превращается в:
SELECTtimestamp, symbol,
first(price) ASopen,
max(price) AS high,
min(price) AS low,
last(price) ASclose,
sum(quantity) AS volume
FROM trades
WHEREtimestampIN today()
SAMPLE BY1m;
Вот и всё. SAMPLE BY 1m говорит QuestDB разбить данные на одноминутные интервалы, а first() / last() — нативные агрегатные функции, учитывающие временной порядок внутри каждого интервала. Никакого date_trunc, никаких манипуляций с array_agg, никакого явного GROUP BY.
Доступные интервалы гибки: 1s, 5s, 15m, 1h, 1d, 7d — любая комбинация временны́х единиц. Для криптовалютных рынков, не знающих выходных, можно выровнять по календарным границам с учётом часового пояса:
SAMPLE BY1d ALIGN TO CALENDAR TIME ZONE 'UTC';
FILL: обработка пробелов в данных
На реальных рынках бывают пробелы — низколиквидные пары могут не торговаться минутами или часами. SAMPLE BY поддерживает несколько стратегий FILL:
-- Заполнение предыдущим значением (forward fill — стандарт в финансах)
SAMPLE BY15m FILL(PREV);
-- Заполнение линейной интерполяцией
SAMPLE BY15m FILL(LINEAR);
-- Заполнение константой
SAMPLE BY15m FILL(0);
-- Без заполнения — вернуть NULL (по умолчанию)
SAMPLE BY15m FILL(NONE);
FILL(PREV) — рабочая лошадка торговых дашбордов: если за 15-минутный интервал не было сделок, берём последнюю известную цену. FILL(LINEAR) больше подходит для непрерывных сигналов вроде ставок финансирования или процентных ставок.
ASOF JOIN: «делай, что я имею в виду» в выравнивании рыночных данных
Это жемчужина QuestDB, и если вы работали с рыночными данными, вы сразу поймёте почему.
Фундаментальная проблема: у вас есть сделки в одной таблице и котировки (bid/ask) в другой. Вы хотите знать, какой была преобладающая котировка в точный момент исполнения каждой сделки. В обычной базе данных временны́е метки почти никогда не совпадают идеально — сделка в 12:00:00.123 должна сопоставиться с котировкой в 12:00:00.098, а не в 12:00:00.201.
ASOF JOIN решает это в одну строку:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol);
Для каждой строки в trades QuestDB находит строку в quotes с последней временно́й меткой, меньшей или равной временно́й метке сделки, сопоставляя по столбцу symbol. Никаких коррелированных подзапросов. Никаких оконных функций. Никакой логики на стороне приложения.
Это фундамент анализа транзакционных издержек (TCA) — сравнения цены исполнения с преобладающим рынком на момент исполнения. В PostgreSQL эквивалентный запрос требует LATERAL JOIN с ORDER BY и LIMIT 1 для каждой строки, что на больших наборах данных медленнее на порядки.
TOLERANCE: защита от устаревших данных при JOIN'е
Вот нюанс, отделяющий игрушечные реализации от промышленных систем. Что если котировка для малоликвидного актива была сформирована 5 минут назад на момент сделки? На волатильном рынке 5-минутная котировка — по сути мусор. Стандартный ASOF JOIN всё равно её использует — он находит последнее совпадение, неважно насколько устаревшее.
Конструкция TOLERANCE в QuestDB исправляет это:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol)
TOLERANCE 1s;
Теперь, если в пределах 1 секунды от сделки нет подходящей котировки, JOIN вернёт NULL вместо устаревших данных. Это критически важно для точного TCA и любой аналитики, где важна свежесть данных.
В качестве бонуса TOLERANCE может существенно повысить производительность запросов. Без него движок может уйти далеко назад в таблице котировок в поиске совпадения. С TOLERANCE он может завершить обратное сканирование раньше, как только записи станут слишком старыми.
LT JOIN и SPLICE JOIN
Две вариации, которые стоит знать: LT JOIN работает как ASOF JOIN, но сопоставляет строго до временно́й метки (никогда не равную). Это полезно, когда нужно избежать предвидения в бэктестировании — вы хотите котировку, которая существовала до вашей сделки, а не ту, что поступила в ту же микросекунду.
SPLICE JOIN — полный ASOF в обоих направлениях: для каждой записи левой таблицы он находит преобладающую запись правой таблицы, и наоборот. Результат — слитая, чередующаяся хронология обоих источников данных. Особенно полезен для создания единых временны́х линий событий из нескольких потоков данных.
HORIZON JOIN: постторговый анализ в одном запросе
Introduced в QuestDB 9.3.3, HORIZON JOIN создан специально для маркаут-анализа — краеугольного камня оценки качества исполнения и исследования рыночной микроструктуры.
Вопрос, на который он отвечает: «После исполнения сделки как менялась цена в течение следующих N секунд?» Традиционно это требует самосоединений, UNION ALL из нескольких ASOF-запросов или выноса логики в код приложения. HORIZON JOIN сворачивает всё это:
SELECT h.offset /1000000AS offset_sec,
avg(mid.price - fill.price) AS avg_markout
FROM fills
HORIZON JOIN mid_prices ON (symbol)
RANGEBETWEEN0AND60s STEP 1s;
Это вычисляет среднее изменение цены с шагом 1 секунда до 60 секунд после каждого исполнения. Движок самостоятельно обрабатывает вычисление временного смещения, ASOF-сопоставление в каждой точке горизонта и агрегацию — всё за один проход.
Для неравномерных горизонтов — или чтобы посмотреть до события — используйте синтаксис LIST:
HORIZON JOIN mid_prices ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 60s);
Это даёт вам кривые маркаута как до, так и после сделки. В сочетании с фильтрацией по площадке, стратегии или размеру ордера вы можете построить полноценный фреймворк оценки качества исполнения внутри базы данных.
Кукбук QuestDB включает пять шаблонов постторгового анализа на основе HORIZON JOIN: анализ проскальзывания (исполнение vs. средняя цена), кривые маркаута, shortfall реализации (декомпозиция Перольда), оценка площадок для умной маршрутизации ордеров и обнаружение токсичности потока (VPIN). Каждый из них работает на живом демо с реальными данными.
WINDOW JOIN: корреляция событий с окружающими данными
WINDOW JOIN был представлен в QuestDB 9.3. Он позволяет каждой строке из основной таблицы соединяться с временны́м окном строк из другой таблицы, с агрегациями, вычисляемыми по совпадающим строкам.
Рассмотрим сценарий торговли на форексе, где нужно сопоставить каждую сделку со средними значениями bid и ask в течение 10 секунд, следующих за сделкой:
SELECT
trades.timestamp,
trades.symbol,
trades.price,
avg(quotes.bid) AS avg_bid,
avg(quotes.ask) AS avg_ask
FROM trades
WINDOWJOIN quotes ON (symbol)
RANGEBETWEEN0AND10s FOLLOWING
INCLUDE PREVAILING;
Предложение INCLUDE PREVAILING гарантирует получение наиболее актуальной цены даже при отсутствии точного совпадения на границе окна. Это устраняет страницы подзапросов, которые обычно требует краткосрочная аналитика.
Варианты использования в трейдинге: вычисление средних рыночных условий вокруг каждого исполнения, обнаружение нетипичного поведения цены в временно́м окне перед крупными ордерами, корреляция событий IoT/инфраструктуры (всплески сетевой задержки) с качеством исполнения.
LATEST ON: мгновенное текущее состояние
Обманчиво простое, но невероятно полезное расширение. LATEST ON возвращает последнюю строку для каждого значения столбца секционирования:
SELECT*FROM trades
LATEST ONtimestampPARTITIONBY symbol, side
ORDERBYtimestampDESC;
Это даёт последнюю сделку для каждой пары (symbol, side) — по сути снимок «текущего состояния» в реальном времени. В традиционной базе данных это потребовало бы коррелированного подзапроса или оконной функции с ROW_NUMBER().
Для торговых дашбордов, отображающих последнюю цену по сотням символов, LATEST ON выполняется мгновенно. В сочетании с материализованными представлениями (которые мы рассмотрим в Части 3) он становится фундаментом для субмиллисекундных снимков портфеля.
TWAP: нативное взвешенное по времени среднее
Агрегат twap(price, timestamp) был добавлен в QuestDB 9.3.3. В отличие от VWAP, взвешивающего по объёму, TWAP взвешивает по продолжительности — каждая цена действует до следующего наблюдения, а результат — площадь под ступенчатой функцией, делённая на общее время.
SELECT symbol,
twap(price, timestamp) AS twap_value,
vwap(price, quantity) AS vwap_value
FROM trades
WHEREtimestampIN today()
SAMPLE BY1h;
TWAP — стандартный бенчмарк исполнения для алгоритмических ордеров. Наличие его в виде нативного агрегата, поддерживающего параллельный GROUP BY и SAMPLE BY со всеми режимами FILL, означает, что никакой клиентской интеграции не нужно — вычисление выполняется полностью внутри движка запросов.
Оконные функции: фундамент технического анализа
QuestDB поддерживает стандартные SQL-оконные функции, составляющие основу вычисления технических индикаторов:
-- Полосы Боллинджера на основе материализованного OHLC-представленияWITH stats AS (
SELECTtimestamp, close,
AVG(close) OVER (
ORDERBYtimestampROWSBETWEEN19 PRECEDING ANDCURRENTROW
) AS sma20,
AVG(close*close) OVER (
ORDERBYtimestampROWSBETWEEN19 PRECEDING ANDCURRENTROW
) AS avg_close_sq
FROM trades_OHLC_15m
WHEREtimestampBETWEEN dateadd('h', -24, now()) AND now()
AND symbol ='BTC-USDT'
)
SELECTtimestamp,
sma20,
sma20 +2*sqrt(avg_close_sq - sma20 * sma20) AS upper_band,
sma20 -2*sqrt(avg_close_sq - sma20 * sma20) AS lower_band
FROM stats;
QuestDB 9.3.3 также представил SQL-стандартный синтаксис WINDOW — определите спецификацию окна один раз и ссылайтесь на неё по имени в нескольких функциях. Больше не нужно повторять одни и те же PARTITION BY и ORDER BY в каждом выражении:
SELECTtimestamp, symbol,
avg(price) OVER w AS avg_price,
stddev(price) OVER w AS std_price,
min(price) OVER w AS min_price,
max(price) OVER w AS max_price
FROM trades
WINDOW w AS (PARTITIONBY symbol ORDERBYtimestampROWSBETWEEN99 PRECEDING ANDCURRENTROW);
Более чистые запросы, меньше копипасты, та же производительность исполнения.
Реальные шаблоны запросов
Вот несколько паттернов, постоянно встречающихся в промышленных торговых системах:
Межактивная корреляция
-- Скользящая почасовая и дневная корреляция между ETH и другим активомWITH data AS (
SELECT ETHUSD.timestamp,
corr(ETHUSD.price, asset.price) AS corr
FROM ETHUSD
ASOF JOIN asset
SAMPLE BY1m
)
SELECTtimestamp,
avg(corr) OVER (ORDERBYtimestampRANGEBETWEEN1HOUR PRECEDING ANDCURRENTROW) AS hourly_corr,
avg(corr) OVER (ORDERBYtimestampRANGEBETWEEN24HOUR PRECEDING ANDCURRENTROW) AS daily_corr
FROM data;
RSI по всем символам
-- 14-дневный RSI для всех USDT-парWITH gains_losses AS (
SELECTtimestamp, symbol,
CASEWHENclose>lag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp)
THENclose-lag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp)
ELSE0ENDAS gain,
CASEWHENclose<lag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp)
THENlag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp) -closeELSE0ENDAS loss
FROM trades_latest_1d
WHERE symbol LIKE'%-USDT'
)
SELECTtimestamp, symbol,
100-100/ (1+ avg_gain /NULLIF(avg_loss, 0)) AS rsi
FROM (
SELECTtimestamp, symbol,
AVG(gain) OVER (PARTITIONBY symbol ORDERBYtimestampROWSBETWEEN13 PRECEDING ANDCURRENTROW) AS avg_gain,
AVG(loss) OVER (PARTITIONBY symbol ORDERBYtimestampROWSBETWEEN13 PRECEDING ANDCURRENTROW) AS avg_loss
FROM gains_losses
);
ATR (средний истинный диапазон)
-- 14-периодный ATR на 15-минутных OHLC-барахWITH tr AS (
SELECTtimestamp, symbol,
GREATEST(
high - low,
ABS(high -lag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp)),
ABS(low -lag(close) OVER (PARTITIONBY symbol ORDERBYtimestamp))
) AS true_range
FROM trades_OHLC_15m
)
SELECTtimestamp, symbol,
AVG(true_range) OVER (PARTITIONBY symbol ORDERBYtimestampROWSBETWEEN13 PRECEDING ANDCURRENTROW) AS atr_14
FROM tr;
Преимущество SQL перед кастомным кодом
Вы можете задаться вопросом: зачем вычислять эти индикаторы внутри базы данных, если можно выгрузить сырые данные в Python и использовать ta-lib или pandas?
Три причины. Первая — локальность данных: перемещение терабайт тиковых данных по сети для вычисления скользящего среднего расточительно. Вычисление должно находиться там, где живут данные. Вторая — параллелизм: векторизованный движок QuestDB с JIT-компиляцией может обрабатывать эти запросы на нескольких ядрах с SIMD-инструкциями, зачастую быстрее однопоточного кода Python. Третья — согласованность: когда нескольким дашбордам, стратегиям и системам мониторинга нужны одни и те же индикаторы, наличие единого источника истины в базе данных исключает ошибки синхронизации.
При этом QuestDB не пытается заменить весь ваш аналитический стек. Интероперабельность с Parquet означает, что вы по-прежнему можете выгружать исторические данные напрямую в ML-конвейер, минуя базу данных для пакетных нагрузок.
Что будет в Части 3
В заключительной части мы рассмотрим материализованные представления для OHLC в реальном времени (включая каскадные представления на нескольких таймфреймах), 2D-массивы для нативной аналитики стакана заявок и референсную архитектуру полноценной алготрейдинговой платформы на основе QuestDB.
Цитирование
@software{soloviov2025questdb_algotrading_p2,
author = {Soloviov, Eugen},
title = {QuestDB for Algorithmic Trading: SQL Extensions That Change the Game},
year = {2025},
url = {https://marketmaker.cc/en/blog/post/questdb-algotrading-sql},
version = {0.1.0},
description = {Deep dive into QuestDB's time-series SQL extensions: SAMPLE BY, ASOF JOIN, HORIZON JOIN, WINDOW JOIN, LATEST ON, and real-world trading query patterns.}
}