回撤幅度是资产净值从历史最高点回落的百分比,计算公式为(历史最高净值-当前净值)÷历史最高净值;必须用累积最大值而非LAG偏移,因回撤始终与截至当前的峰值比较,需通过MAX() OVER(ORDER BY trade_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)实现。

什么是回撤幅度,为什么不能直接用LAG
回撤幅度(Drawdown)是资产净值从前期高点回落的百分比,核心是「历史最高值」和「当前值」的差值除以前者。很多人第一反应是用 LAG 向前取上一行,但这是错的——回撤不是和上一时刻比,而是和「截至当前的所有历史最高点」比,必须用累积最大值(cumulative max),不是偏移量。
用MAX() OVER(ORDER BY)算滚动最高净值
关键在构造「到当前行为止的历史最高净值」,窗口函数 MAX() 配合有序窗口即可实现。注意必须显式声明 ORDER BY,且排序字段要是时间维度(比如 trade_date),否则窗口无序,结果不可靠。
-
MAX(nav) OVER (ORDER BY trade_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)是标准写法,ROWS子句虽可省略(默认行为相同),但显式写出更防误读 - 如果数据存在多条同日记录,需先按日期聚合(如取当日收盘
nav),否则同一窗口内重复计算会扭曲高点 - PostgreSQL 和 SQL Server 支持
RANGE,但对时间字段慎用——若某天无数据,RANGE可能跳过空档,导致高点“滞留”,ROWS更稳妥
回撤幅度的完整SQL表达式
回撤幅度定义为 (peak_nav - current_nav) / peak_nav,结果为正数(0~1之间)。注意处理分母为零或 NULL 的边界情况。
SELECT
trade_date,
nav,
MAX(nav) OVER (ORDER BY trade_date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS peak_nav,
CASE
WHEN MAX(nav) OVER (ORDER BY trade_date) = 0 THEN NULL
ELSE (MAX(nav) OVER (ORDER BY trade_date) - nav) / MAX(nav) OVER (ORDER BY trade_date)
END AS drawdown
FROM asset_nav_history;- 三个
MAX(nav) OVER (...)在逻辑上等价,但多数引擎(如 PostgreSQL、Spark SQL)会自动复用窗口计算,性能无显著开销 - MySQL 8.0+ 支持窗口函数,但旧版本(
- 若需只看「最大回撤」(即整个序列中 drawdown 的最大值),再套一层
MAX(drawdown)即可,但注意这是标量结果,别忘了加GROUP BY或用子查询隔离作用域
常见错误:没过滤初始期或忽略净值归一化
回撤计算对起始点敏感。如果原始 nav 是绝对金额(比如从100万开始),而你想看相对波动,通常应先归一化为基期=1(如 nav / FIRST_VALUE(nav) OVER (ORDER BY trade_date)),否则早期小数值会导致回撤被放大。
- 未归一化时,
nav=100 → 95回撤 5%,但nav=1000000 → 950000也是 5%——看起来一样,可一旦混入不同起始规模的策略对比,就失去可比性 - 起始几行的
peak_nav等于nav,导致回撤恒为 0,这是正常现象,不是 bug;但若业务要求「首日不参与回撤统计」,需用ROW_NUMBER()过滤掉row_num = 1 - 时间字段有空值?
ORDER BY trade_date会让NULL排最前或最后(依数据库而定),直接污染peak_nav,务必提前WHERE trade_date IS NOT NULL
回撤本身不难算,难点在于确认「峰值」是否真的代表投资者当时能看到的最高点——比如净值更新延迟、分红再投资时点偏差、或者测试数据里人为插入的异常高点,都会让窗口结果偏离真实体验。

















