← 返回文章列表
March 4, 2026
5 分钟阅读

QuestDB 算法交易实战:改变游戏规则的 SQL 扩展

QuestDB 算法交易实战:改变游戏规则的 SQL 扩展
#QuestDB
#SQL
#ASOF JOIN
#SAMPLE BY
#time-series
#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.}
}
免责声明:本文提供的信息仅用于教育和参考目的,不构成财务、投资或交易建议。加密货币交易涉及重大损失风险。

MarketMaker.cc Team

量化研究与策略

在 Telegram 中讨论
Newsletter

紧跟市场步伐

订阅我们的时事通讯,获取独家 AI 交易见解、市场分析和平台更新。

我们尊重您的隐私。您可以随时退订。