QuestDB 演算法交易實戰:改變遊戲規則的 SQL 擴充
免責聲明:本文內容僅供教育和參考目的,不構成任何財務、投資或交易建議。加密貨幣交易存在重大虧損風險。
歡迎來到 QuestDB 系列的第 2 篇。在第 1 篇中,我們介紹了三層儲存架構和模式設計原則。現在我們將深入探討真正令 QuestDB 與眾不同的特性集——那些讓 QuestDB 感覺像是由交易員為交易員設計的 SQL 擴充。
標準 SQL 誕生於 20 世紀 70 年代,為關係型資料而生。它對時間作為一等公民的概念一無所知。在 PostgreSQL 或 MySQL 中進行每一次時序操作都需要冗長的變通方案——視窗函數、側向連線、層層疊疊的 CTE。QuestDB 的擴充將這些多段式查詢壓縮為單一的、富有表現力的語句。
讓我們通過真實的交易示例逐一瞭解每個擴充。

SAMPLE BY:原生時間桶聚合
如果有一種查詢是每個交易系統執行最頻繁的,那就是 OHLCV 聚合——將原始成交資料轉化為 K 線資料。在標準 SQL 中,你需要這樣寫:
-- 标准 SQL:冗长且缓慢
SELECT
date_trunc('minute', timestamp) AS bucket,
symbol,
(array_agg(price ORDER BY timestamp))[1] AS open,
max(price) AS high,
min(price) AS low,
(array_agg(price ORDER BY timestamp DESC))[1] AS close,
sum(quantity) AS volume
FROM trades
WHERE timestamp >= now() - interval '1 hour'
GROUP BY bucket, symbol
ORDER BY bucket;
在 QuestDB 中,這變成了:
SELECT timestamp, symbol,
first(price) AS open,
max(price) AS high,
min(price) AS low,
last(price) AS close,
sum(quantity) AS volume
FROM trades
WHERE timestamp IN today()
SAMPLE BY 1m;
就這些。SAMPLE BY 1m 告訴 QuestDB 將資料分成 1 分鐘的區間,first() 和 last() 是原生聚合函數,在每個桶內尊重時間排序。無需 date_trunc,無需 array_agg 變通,無需顯式 GROUP BY。
可用的時間間隔非常靈活:1s、5s、15m、1h、1d、7d——任意時間單位的組合。對於永不停歇的加密市場,你可以通過時區感知對齊到日曆邊界:
SAMPLE BY 1d ALIGN TO CALENDAR TIME ZONE 'UTC';
FILL:處理資料缺口
真實市場存在缺口——低流動性的交易對可能數分鐘甚至數小時都沒有成交。SAMPLE BY 支援多種 FILL 策略:
-- 用前值填充(前向填充——金融领域的常规做法)
SAMPLE BY 15m FILL(PREV);
-- 用线性插值填充
SAMPLE BY 15m FILL(LINEAR);
-- 用常量填充
SAMPLE BY 15m FILL(0);
-- 不填充——返回 NULL(默认)
SAMPLE BY 15m FILL(NONE);
FILL(PREV) 是交易儀表板的標配——如果某個 15 分鐘桶內沒有成交,則沿用最後已知價格。FILL(LINEAR) 更適合資金費率或利率等連續型訊號。

ASOF JOIN:"心領神會"的行情資料對齊
這是 QuestDB 的鎮店之寶,如果你曾經處理過行情資料,你將立刻明白其價值所在。
根本問題在於:你在一張表裡有成交資料,在另一張表裡有報價資料(買賣盤)。你想知道每筆成交執行時市場上的現行報價是多少。在普通資料庫中,時間戳幾乎從不完全對齊——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 中,等價實現需要對每一行進行帶 ORDER BY 和 LIMIT 1 的 LATERAL JOIN,在大數據集上效能相差幾個數量級。
TOLERANCE:防止連線到過期資料
這是區分玩具實現與生產系統的細節。如果一個成交量稀少資產的報價在成交時已經過時 5 分鐘怎麼辦?在波動市場中,5 分鐘前的報價基本上是垃圾資料。預設的 ASOF JOIN 仍然會使用它——它找到最新的匹配,無論多麼陳舊。
QuestDB 的 TOLERANCE 子句解決了這個問題:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol)
TOLERANCE 1s;
現在,如果在成交時間 1 秒內不存在匹配報價,連線將返回 NULL 而非陳舊資料。這對於精確的 TCA 以及任何資料新鮮度至關重要的分析場景都是關鍵所在。
額外的好處是,TOLERANCE 還能顯著提升查詢效能。沒有它時,引擎可能會深入掃描報價表尋找匹配項。有了 TOLERANCE,一旦記錄太舊不符合條件,它就可以提前終止向後掃描。
LT JOIN 與 SPLICE JOIN
兩個值得了解的變體:LT JOIN 類似 ASOF JOIN,但匹配時間戳嚴格早於(而非等於)的記錄。在回測中需要避免前瞻偏差時非常有用——你需要的是成交之前存在的報價,而非同一微秒到達的報價。
SPLICE JOIN 是雙向的完整 ASOF:對左表的每條記錄,找到當時最新的右表記錄;對右表的每條記錄,也找到當時最新的左表記錄。結果是兩個資料來源交錯融合的統一時間線。這對於從多個數據流建立統一事件時間線特別有用。
HORIZON JOIN:單次查詢完成交易後分析
HORIZON JOIN 於 QuestDB 9.3.3 引入,專為標記分析(markout analysis)而生——這是執行質量評估和市場微觀結構研究的基石。
它回答的問題是:"成交執行後,價格在接下來的 N 秒內是如何演變的?"傳統上,這需要自連線、跨多個 ASOF 查詢的 UNION ALL,或者將邏輯推送至應用層程式碼。HORIZON JOIN 將這一切壓縮為單一查詢:
SELECT h.offset / 1000000 AS offset_sec,
avg(mid.price - fill.price) AS avg_markout
FROM fills
HORIZON JOIN mid_prices ON (symbol)
RANGE BETWEEN 0 AND 60s STEP 1s;
這在每筆成交後最多 60 秒、以 1 秒為間隔計算平均價格變動。引擎負責處理時間偏移計算、每個時間點的 ASOF 匹配以及聚合——全部在單次掃描中完成。
對於非均勻時間點,或者需要檢視事件之前的情況,可以使用 LIST 語法:
HORIZON JOIN mid_prices ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 60s);
這同時給出了交易前後的標記曲線。結合按交易場所、策略或訂單規模進行過濾,你可以在資料庫內部構建完整的執行質量分析框架。
QuestDB 的手冊中包含了五種基於 HORIZON JOIN 的交易後分析模式:滑點分析(執行價格 vs. 中間價)、標記曲線、實施差異(Perold 分解)、用於智慧訂單路由的交易場所評分,以及流量毒性檢測(VPIN)。每種模式都可以在其即時演示中用真實資料執行。
WINDOW JOIN:關聯事件與周圍資料
WINDOW JOIN 於 QuestDB 9.3 引入。它允許主表的每一行與另一張表的時間視窗內的行進行連線,並對匹配的行計算聚合。
考慮一個外匯交易場景,你想將每筆成交與成交後 10 秒內的平均買賣盤價格關聯起來:
SELECT
trades.timestamp,
trades.symbol,
trades.price,
avg(quotes.bid) AS avg_bid,
avg(quotes.ask) AS avg_ask
FROM trades
WINDOW JOIN quotes ON (symbol)
RANGE BETWEEN 0 AND 10s FOLLOWING
INCLUDE PREVAILING;
INCLUDE PREVAILING 子句確保即使視窗邊界處沒有精確匹配,你也能獲取最新價格。這消除了短週期分析通常需要的大量子查詢。
在交易中的應用場景:計算每次執行前後的平均市場狀況、檢測大額訂單前某時間視窗內的異常價格行為、將 IoT/基礎設施事件(網路延遲峰值)與執行質量相關聯。
LATEST ON:即時獲取當前狀態
這是一個看似簡單卻非常實用的擴充。LATEST ON 返回每個分割槽列值對應的最後一行:
SELECT * FROM trades
LATEST ON timestamp PARTITION BY symbol, side
ORDER BY timestamp DESC;
這給出了每個(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
WHERE timestamp IN today()
SAMPLE BY 1h;
TWAP 是演算法訂單的標準執行基準。將其作為支援並行 GROUP BY 和 SAMPLE BY(包含所有 FILL 模式)的原生聚合函數,意味著你不需要任何客戶端整合——計算完全在查詢引擎內部執行。
視窗函數:技術分析的基礎
QuestDB 支援標準 SQL 視窗函數,這是技術指標計算的骨幹:
-- 基于物化 OHLC 视图的布林带
WITH stats AS (
SELECT timestamp, close,
AVG(close) OVER (
ORDER BY timestamp
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS sma20,
AVG(close * close) OVER (
ORDER BY timestamp
ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
) AS avg_close_sq
FROM trades_OHLC_15m
WHERE timestamp BETWEEN dateadd('h', -24, now()) AND now()
AND symbol = 'BTC-USDT'
)
SELECT timestamp,
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:
SELECT timestamp, 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 (PARTITION BY symbol ORDER BY timestamp ROWS BETWEEN 99 PRECEDING AND CURRENT ROW);
查詢更簡潔,複製貼上更少,執行效能不變。
真實世界查詢模式
讓我展示一些在生產交易系統中頻繁出現的模式:
跨資產相關性
-- ETH 与另一资产之间的滚动小时和日相关性
WITH data AS (
SELECT ETHUSD.timestamp,
corr(ETHUSD.price, asset.price) AS corr
FROM ETHUSD
ASOF JOIN asset
SAMPLE BY 1m
)
SELECT timestamp,
avg(corr) OVER (ORDER BY timestamp
RANGE BETWEEN 1 HOUR PRECEDING AND CURRENT ROW) AS hourly_corr,
avg(corr) OVER (ORDER BY timestamp
RANGE BETWEEN 24 HOUR PRECEDING AND CURRENT ROW) AS daily_corr
FROM data;
全交易對 RSI
-- 所有 USDT 交易对的 14 日 RSI
WITH gains_losses AS (
SELECT timestamp, symbol,
CASE WHEN close > lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
THEN close - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
ELSE 0 END AS gain,
CASE WHEN close < lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
THEN lag(close) OVER (PARTITION BY symbol ORDER BY timestamp) - close
ELSE 0 END AS loss
FROM trades_latest_1d
WHERE symbol LIKE '%-USDT'
)
SELECT timestamp, symbol,
100 - 100 / (1 + avg_gain / NULLIF(avg_loss, 0)) AS rsi
FROM (
SELECT timestamp, symbol,
AVG(gain) OVER (PARTITION BY symbol ORDER BY timestamp
ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS avg_gain,
AVG(loss) OVER (PARTITION BY symbol ORDER BY timestamp
ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS avg_loss
FROM gains_losses
);
ATR(平均真實波幅)
-- 基于 15 分钟 OHLC K 线的 14 周期 ATR
WITH tr AS (
SELECT timestamp, symbol,
GREATEST(
high - low,
ABS(high - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)),
ABS(low - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp))
) AS true_range
FROM trades_OHLC_15m
)
SELECT timestamp, symbol,
AVG(true_range) OVER (PARTITION BY symbol ORDER BY timestamp
ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS atr_14
FROM tr;
SQL 相對於自定義程式碼的優勢
你可能會想:既然可以將原始資料拉取到 Python 中使用 ta-lib 或 pandas 處理,為什麼要在資料庫內部計算這些指標?
三個原因。第一,資料區域性性——將 TB 級的 tick 資料通過網路傳輸來計算滾動平均值是一種浪費。計算應該在資料所在的地方進行。第二,並行性——QuestDB 帶有 JIT 編譯的向量化引擎可以利用 SIMD 指令在多核上並行處理這些查詢,通常比單執行緒 Python 程式碼更快。第三,一致性——當多個儀表板、策略和監控系統都需要相同的指標時,在資料庫中維護單一資料來源可以消除同步錯誤。
話雖如此,QuestDB 並不試圖取代你的整個分析棧。Parquet 互操作性意味著對於批次工作負載,你仍然可以不經過資料庫直接將歷史資料讀取到機器學習管道中。
第 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.}
}
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.