← 返回文章列表
March 4, 2026
5 分鐘閱讀

QuestDB 演算法交易實戰:改變遊戲規則的 SQL 擴充

QuestDB 演算法交易實戰:改變遊戲規則的 SQL 擴充
#QuestDB
#SQL
#ASOF JOIN
#SAMPLE BY
#time-series
#algorithmic trading
🗄️
Part 2 of 3 · Collection
QuestDB for Algorithmic Trading

第 2 篇,共 3 篇 — 也可閱讀 RU · EN

免責聲明:本文內容僅供教育和參考目的,不構成任何財務、投資或交易建議。加密貨幣交易存在重大虧損風險。


歡迎來到 QuestDB 系列的第 2 篇。在第 1 篇中,我們介紹了三層儲存架構和模式設計原則。現在我們將深入探討真正令 QuestDB 與眾不同的特性集——那些讓 QuestDB 感覺像是由交易員為交易員設計的 SQL 擴充。

標準 SQL 誕生於 20 世紀 70 年代,為關係型資料而生。它對時間作為一等公民的概念一無所知。在 PostgreSQL 或 MySQL 中進行每一次時序操作都需要冗長的變通方案——視窗函數、側向連線、層層疊疊的 CTE。QuestDB 的擴充將這些多段式查詢壓縮為單一的、富有表現力的語句。

讓我們通過真實的交易示例逐一瞭解每個擴充。

SAMPLE BY:原始交易聚合為OHLCV K線

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

可用的時間間隔非常靈活:1s5s15m1h1d7d——任意時間單位的組合。對於永不停歇的加密市場,你可以通過時區感知對齊到日曆邊界:

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:交易和報價的時間對齊

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

Eugen Soloviov
Eugen Soloviov

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.

Newsletter

緊跟市場步伐

訂閱我們的時事通訊,獲取獨家 AI 交易見解、市場分析和平台更新。

我們尊重您的隱私。您可以隨時退訂。