sql不内置最大回撤函数,必须用max() over(order by timestamp rows unbounded preceding)计算滚动历史高点,再算(price - peak_so_far)/peak_so_far的最小值。

直接说结论:SQL 本身不内置“最大回撤”(Maximum Drawdown)计算函数,必须用窗口函数组合 MAX、MIN 和累计逻辑手动实现;硬套 MAX() 或 MIN() 单独用会完全错误。
为什么不能直接用 MAX() / MIN() 算最大回撤
最大回撤不是全表最高价和最低价的差值,而是“从任意历史高点到后续最低点的最大跌幅”,本质是**路径依赖的滚动极值差**。常见错误是写成:
SELECT MAX(price) - MIN(price) AS wrong_dd FROM trades;
这算的是全局振幅,不是回撤。比如价格序列为 [100, 120, 110, 90, 95],真实最大回撤是 (120 → 90) = 25%,但 MAX-MIN 得到的是 30(绝对值),且没归一化为百分比。
关键点:
-
MAX()和MIN()是静态聚合,只返回一个标量,无法表达“每个点往前看的历史峰值” - 回撤必须按时间顺序逐点计算:当前净值 / 历史最高净值 - 1
- 数据库需支持窗口函数(PostgreSQL、SQL Server 2005+、MySQL 8.0+、BigQuery 等),否则无法可靠实现
用 MAX() OVER() 构建滚动历史高点
核心是用 MAX(price) OVER (ORDER BY timestamp ROWS UNBOUNDED PRECEDING) 动态计算截止到当前行的历史最高价。这是实现回撤的第一步。
示例(假设表 trades 含字段 timestamp、price):
SELECT timestamp, price, MAX(price) OVER (ORDER BY timestamp ROWS UNBOUNDED PRECEDING) AS peak_so_far FROM trades ORDER BY timestamp;
注意:
- 必须显式
ORDER BY timestamp,否则窗口无意义 -
ROWS UNBOUNDED PRECEDING表示“从第一行到当前行”,不可省略或写成RANGE(在时间戳重复时可能出错) - 如果
timestamp有重复,需加id等唯一列做二级排序,避免非确定性结果
组合计算回撤率并取最小值
有了 peak_so_far,就能算每一点的回撤率:(price - peak_so_far) / peak_so_far,再用 MIN() 找出最深的一次(即最大回撤)。
完整 SQL(含别名和过滤):
WITH rolling_peak AS (
SELECT
price,
MAX(price) OVER (ORDER BY timestamp ROWS UNBOUNDED PRECEDING) AS peak_so_far
FROM trades
)
SELECT MIN((price - peak_so_far) / peak_so_far) AS max_drawdown
FROM rolling_peak;
说明:
- 结果是负数(如
-0.25表示 25% 回撤),符合金融惯例 - 若需绝对值形式,外层套
ABS(),但原始定义就是负值 - 该查询不处理空值:确保
price列非 NULL,否则MAX()窗口会跳过 NULL 行,但后续除法可能报错 - 性能上,窗口函数会产生临时排序,大数据量时注意
timestamp字段是否有索引
实际业务中容易被忽略的细节
真正上线时,这几个点常被跳过导致结果偏差:
- 起始点问题:第一个交易日的
peak_so_far = price,回撤为 0 —— 这正确,但有人误加WHERE row_num > 1排除首行,破坏逻辑 - 数据顺序必须严格按时间升序;如果用
ORDER BY id代替timestamp,而id并不严格反映交易先后,结果全错 - 未区分“净值”和“价格”:回撤应基于账户净值曲线,不是标的资产价格;若直接用
price计算,隐含了“满仓持有一单位”的假设,实际策略需先生成净值序列 - 某些引擎(如旧版 Hive)不支持
ROWS UNBOUNDED PRECEDING,只能退化为自连接,性能极差且易写错











