← 中文资源

简体中文版本

指数移动平均平滑器

用由周期推导的平滑系数增量计算指数移动平均,并与固定窗口的简单平均结果对照。

阅读英文原文

功能说明

指数移动平均对近期数据赋予更高权重,适合监控指标、行情曲线和传感器读数的去噪。与简单移动平均相比,它不需要保存整个窗口,只需保留上一期的平滑值,因此可以在流式管道里常数内存运行。

EmaTracker 在初始化时根据周期计算平滑系数:周期越小,系数越大,曲线越贴近原始值;周期越大,曲线越平滑但滞后越明显。首次更新直接采用样本值作为起点,避免从零开始导致曲线长期偏低。

关键代码

alpha 等于二除以周期加一,这个取值让指数平均的等效窗口接近给定周期。update 在 value 为 None 时赋初值,否则按 alpha 乘新样本加上一减 alpha 乘旧值递推。reset 清空状态,便于在数据源切换或重启后重新预热。simple_moving_average 在前 window 减一个位置填充 None,对齐时间轴,便于和 EMA 并排比较。

使用注意

首次回填会让曲线在前几期偏向第一个样本,若指标对启动偏差敏感,可以先用历史数据预热或丢弃前若干期。周期必须是正整数,零或负数会直接报错。缺失值不要用零填充,否则会把曲线拉低;应先跳过或插值。EMA 对异常值仍有一定敏感度,极端尖峰可以配合截断或中位数滤波。多个序列需要各自维护独立的 tracker 实例。

验证方式

对 10、12、11、15、14、16 六个数、周期为三时,alpha 为 0.5,EMA 依次为 10、11、11、13、13.5、14.75。简单平均在前两个位置输出 None,第三个位置输出 11.0,说明两个序列已按同一时间轴对齐。可以把手动计算的第一期与打印值比对,确认平滑系数正确。

代码示例

from collections.abc import Iterable
from dataclasses import dataclass, field


@dataclass
class EmaTracker:
    period: int
    value: float | None = None
    alpha: float = field(init=False, default=0.0)

    def __post_init__(self) -> None:
        if self.period < 1:
            raise ValueError("period must be at least 1")
        self.alpha = 2.0 / (self.period + 1)

    def update(self, sample: float) -> float:
        if self.value is None:
            self.value = sample
        else:
            self.value = self.alpha * sample + (1 - self.alpha) * self.value
        return self.value

    def reset(self) -> None:
        self.value = None


def exponential_moving_average(values: Iterable[float], period: int) -> list[float]:
    tracker = EmaTracker(period)
    return [tracker.update(value) for value in values]


def simple_moving_average(values: list[float], window: int) -> list[float | None]:
    if window < 1:
        raise ValueError("window must be at least 1")
    smoothed: list[float | None] = []
    for index in range(len(values)):
        if index + 1 < window:
            smoothed.append(None)
        else:
            chunk = values[index + 1 - window : index + 1]
            smoothed.append(sum(chunk) / window)
    return smoothed


def main() -> None:
    samples = [10.0, 12.0, 11.0, 15.0, 14.0, 16.0]
    print("ema:", [round(value, 3) for value in exponential_moving_average(samples, 3)])
    print("sma:", simple_moving_average(samples, 3))


if __name__ == "__main__":
    main()