移动振幅是时序数据分析中常用的指标,用来衡量指定滑动窗口内数据的最大波动范围,本质是窗口内最大值与最小值的差值。在股票行情分析、设备运行状态监控、用户行为波动统计等场景中,都需要通过移动振幅判断数据的稳定性。比如分析某只股票近7个交易日的振幅,就能知道这段时间内股价的最高波动幅度,为投资决策提供参考。

移动振幅的计算逻辑与窗口函数适配性
移动振幅的计算需要满足两个核心条件:第一是确定滑动窗口的范围,比如向前取3行、向后取2行,或者基于时间范围取前后7天的数据;第二是在每个窗口内分别计算最大值和最小值,再将两个值相减得到差值。如果不用窗口函数,传统实现方式需要通过自连接,把每一行数据和窗口内的其他行做关联,再分组计算极值,这种方式当数据量较大时会产生大量的临时关联结果,执行效率极低。
窗口函数的特性刚好适配移动振幅的计算需求,它可以在不改变原表行数的前提下,对每一行数据按照指定的排序规则和窗口范围,计算对应的聚合结果。MAX和MIN作为常用的聚合窗口函数,支持定义ROWS或者RANGE类型的窗口,直接在每一行的窗口范围内计算极值,不需要额外的关联操作。两个窗口函数的计算结果可以直接在同一行做减法,得到该行的移动振幅,逻辑简洁且执行效率高。
需要注意的是,窗口函数的窗口范围定义要和移动振幅的业务需求完全匹配。比如计算近3个周期的移动振幅,窗口应该定义为当前行加前2行,而不是当前行加后2行,否则会出现未来数据参与计算的问题。另外如果数据中存在NULL值,MAX和MIN窗口函数会自动忽略NULL值计算,若需要把NULL值当作0参与计算,需要先通过COALESCE函数做处理,再传入窗口函数。
基于ROWS窗口的移动振幅计算实现
ROWS类型的窗口是基于行的物理位置来定义范围的,适合按固定行数计算移动振幅的场景,比如每3条数据计算一次振幅。假设我们有一张股票价格表stock_price,包含交易日期trade_date、股票代码stock_code、当日收盘价close_price三个字段,现在需要计算每只股票近3个交易日的移动振幅,也就是当前交易日加前2个交易日的收盘价最大值减去最小值。
首先需要通过PARTITION BY子句按股票代码分组,保证窗口计算只在同只股票的数据内进行,然后通过ORDER BY按交易日期升序排序,保证窗口内的行是按时间顺序排列的。窗口范围定义为ROWS BETWEEN 2 PRECEDING AND CURRENT ROW,也就是当前行和前2行,共3行数据。然后分别用MAX和MIN窗口函数计算这个窗口内的收盘价极值,最后相减得到移动振幅。
以下是具体的SQL实现代码:
-- 计算每只股票近3个交易日的移动振幅
SELECT
stock_code,
trade_date,
close_price,
-- 计算窗口内最大收盘价
MAX(close_price) OVER (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS window_max_price,
-- 计算窗口内最小收盘价
MIN(close_price) OVER (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS window_min_price,
-- 移动振幅 = 最大值 - 最小值
MAX(close_price) OVER (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) - MIN(close_price) OVER (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS moving_amplitude
FROM
stock_price
ORDER BY
stock_code,
trade_date;
上述代码中,同一个窗口定义写了两次,实际使用时可以通过子查询或者CTE简化,避免重复编写窗口逻辑。比如先计算出每行的窗口极值,再在外层做减法:
WITH price_window AS (
SELECT
stock_code,
trade_date,
close_price,
MAX(close_price) OVER w AS window_max,
MIN(close_price) OVER w AS window_min
FROM
stock_price
WINDOW w AS (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)
)
SELECT
stock_code,
trade_date,
close_price,
window_max,
window_min,
window_max - window_min AS moving_amplitude
FROM
price_window
ORDER BY
stock_code,
trade_date;
这种方式通过WINDOW子句定义了公共窗口,后续直接引用窗口名即可,减少了代码冗余,也降低了窗口定义写错的概率。如果表的第一行和第二行,窗口内的行数不足3行,窗口函数会自动取实际存在的行计算,比如第一行只有自身1行数据,移动振幅就是0,符合业务预期。
基于RANGE窗口的时间维度移动振幅计算
ROWS窗口是基于行数计算的,适合数据间隔均匀的场景,如果数据存在缺失,比如某只股票某天停牌没有交易数据,ROWS窗口还是按行数取,就会出现时间范围不符合预期的问题。这时候需要用RANGE类型的窗口,基于排序字段的实际值范围来定义窗口,比如取当前交易日期前7天内的所有数据,不管这7天内有多少条有效数据。
假设需要计算每只股票近7个自然日的移动振幅,不管中间是否有停牌数据,窗口范围应该是当前交易日期减去6天到当前交易日期的所有数据。这时候ORDER BY还是按交易日期排序,窗口类型改为RANGE,范围是RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW,不过需要注意不同数据库对RANGE窗口的时间间隔支持有差异,MySQL 8.0+、PostgreSQL、SQL Server等主流数据库都支持这种写法,部分旧版本数据库可能需要先对日期做数值转换再使用RANGE窗口。
以下是基于RANGE窗口计算近7日移动振幅的SQL代码:
-- 计算每只股票近7个自然日的移动振幅
WITH price_window AS (
SELECT
stock_code,
trade_date,
close_price,
MAX(close_price) OVER w AS window_max,
MIN(close_price) OVER w AS window_min
FROM
stock_price
WINDOW w AS (
PARTITION BY stock_code
ORDER BY trade_date
RANGE BETWEEN INTERVAL 6 DAY PRECEDING AND CURRENT ROW
)
)
SELECT
stock_code,
trade_date,
close_price,
window_max,
window_min,
window_max - window_min AS moving_amplitude_7d
FROM
price_window
ORDER BY
stock_code,
trade_date;
使用RANGE窗口时需要注意排序字段的类型,必须是可比较的范围类型,比如日期、数值类型,字符串类型如果不支持范围比较则无法使用RANGE窗口。另外如果同一个交易日期有多条数据,比如日内分时数据,RANGE窗口会把同一个日期的所有行都纳入窗口,而ROWS窗口只会按行数取,这也是两者的重要区别。如果业务需求是取近7个交易日(不管自然日),还是应该用ROWS窗口,根据实际的业务场景选择窗口类型。
在实际使用中,还可以结合CASE语句处理特殊情况,比如移动振幅为负数时(一般不会出现,除非数据异常)置为0,或者只保留窗口行数足够的情况,过滤掉前N行窗口不足的数据。比如计算近3日振幅时,只保留交易日期排序第三行及以后的数据,避免前2行的振幅因为窗口数据不足没有参考意义:
WITH price_window AS (
SELECT
stock_code,
trade_date,
close_price,
MAX(close_price) OVER w AS window_max,
MIN(close_price) OVER w AS window_min,
-- 计算窗口内的行数,用于过滤不足3行的情况
COUNT(close_price) OVER w AS window_row_count
FROM
stock_price
WINDOW w AS (
PARTITION BY stock_code
ORDER BY trade_date
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
)
)
SELECT
stock_code,
trade_date,
close_price,
window_max - window_min AS moving_amplitude
FROM
price_window
-- 过滤窗口行数不足3行的数据
WHERE
window_row_count = 3
ORDER BY
stock_code,
trade_date;
这种处理方式可以保证移动振幅的计算结果都基于完整的窗口范围,避免不完整的窗口导致指标失真。如果业务中需要保留所有行的数据,也可以把过滤逻辑去掉,根据需求调整即可。