
指数移动平均 EMA(Exponential Moving Average)算法详解
- 一、算法详解
-
- 1、引言
- [2、 什么是移动平均](#2、 什么是移动平均)
-
- [2.1 、简单移动平均(SMA)](#2.1 、简单移动平均(SMA))
- [2.2、 指数移动平均(EMA)](#2.2、 指数移动平均(EMA))
- [3、 EMA 的数学定义](#3、 EMA 的数学定义)
-
- [3.1、 平滑因子 α 与窗口 N 的关系](#3.1、 平滑因子 α 与窗口 N 的关系)
- [4、 递推公式的展开推导](#4、 递推公式的展开推导)
- [5、 EMA 与 SMA 的对比](#5、 EMA 与 SMA 的对比)
-
- [5.1 适用场景建议](#5.1 适用场景建议)
- 6、典型应用场景
-
- [6.1 、量化交易中的技术指标](#6.1 、量化交易中的技术指标)
- [6.2 、深度学习优化器中的参数平滑](#6.2 、深度学习优化器中的参数平滑)
- [6.3、 系统监控与异常检测](#6.3、 系统监控与异常检测)
- [7. 总结](#7. 总结)
- 二、代码示例
一、算法详解
1、引言
在时间序列分析、技术指标计算和深度学习优化器中,指数移动平均(Exponential Moving Average,简称 EMA) 是一种极其常用的统计方法。与简单移动平均(SMA)不同,EMA 对近期数据赋予更高的权重,因此能更快地反映数据的最新变化趋势。
本文将从数学原理出发,逐步推导 EMA 的递推公式,并通过代码示例展示其实现方式,最后介绍 EMA 在量化交易和深度学习中的典型应用场景。
2、 什么是移动平均
移动平均(Moving Average,MA)是一种用于平滑时间序列数据的技术,其核心思想是取一段时间窗口内数据的平均值,以消除短期波动、凸显长期趋势。
2.1 、简单移动平均(SMA)
简单移动平均对窗口内的所有数据点赋予相同权重,其计算公式为:
SMA_t = (x_t + x_{t-1} + ... + x_{t-N+1}) / N
其中 N 为窗口大小。SMA 的缺点是:窗口外的数据被完全丢弃,窗口内的数据权重相同,导致对最新变化的响应不够灵敏。
2.2、 指数移动平均(EMA)
EMA 则采用指数衰减的权重分配方式,越近的数据权重越大,越远的数据权重越小,且权重永远不会归零。这使得 EMA 既能平滑噪声,又能更快地跟踪趋势变化。
3、 EMA 的数学定义
EMA 在时刻 t 的取值 EMA_t 定义如下:
EMA_t = α * x_t + (1 - α) * EMA_{t-1}
其中:
x_t为当前时刻的观测值;EMA_{t-1}为上一时刻的 EMA 值;α为平滑因子(smoothing factor),取值范围为0 < α ≤ 1。
3.1、 平滑因子 α 与窗口 N 的关系
在实际应用中,α 通常由窗口长度 N 换算而来:
α = 2 / (N + 1)
例如:
- 当
N = 5时,α = 2 / 6 ≈ 0.333; - 当
N = 10时,α = 2 / 11 ≈ 0.182; - 当
N = 20时,α = 2 / 21 ≈ 0.095。
窗口 N 越大,α 越小,平滑效果越强,但对新数据的响应越慢;反之,N 越小,α 越大,对最新数据越敏感。
4、 递推公式的展开推导
将 EMA 的递推公式逐层展开,可以更直观地理解其权重分配机制。
假设从时刻 0 开始,初始值 EMA_0 = x_0,则:
EMA_1 = α * x_1 + (1 - α) * EMA_0
= α * x_1 + (1 - α) * x_0
EMA_2 = α * x_2 + (1 - α) * EMA_1
= α * x_2 + (1 - α) * [α * x_1 + (1 - α) * x_0]
= α * x_2 + α(1 - α) * x_1 + (1 - α)² * x_0
以此类推,可以得到通项公式:
EMA_t = α * x_t + α(1 - α) * x_{t-1} + α(1 - α)² * x_{t-2} + ... + (1 - α)^t * x_0
可以看出:
- 当前时刻数据
x_t的权重为α; - 前一个时刻数据
x_{t-1}的权重为α(1 - α); - 前两个时刻数据
x_{t-2}的权重为α(1 - α)²; - 权重按
(1 - α)的幂次指数衰减,这正是「指数移动平均」名称的由来。
所有权重之和收敛于 1:
α + α(1 - α) + α(1 - α)² + ... = 1
5、 EMA 与 SMA 的对比
| 特性 | SMA | EMA |
|---|---|---|
| 权重分配 | 窗口内等权 | 近期权重高,指数衰减 |
| 响应速度 | 较慢 | 较快 |
| 对异常值敏感度 | 较低 | 较高 |
| 计算复杂度 | O(N) | O(1) 递推 |
| 数据存储 | 需保留窗口内全部数据 | 只需保留上一时刻 EMA 值 |
5.1 适用场景建议
- SMA:适合数据噪声较大、希望获得更平滑曲线的场景;
- EMA:适合需要快速捕捉趋势变化、对实时性要求较高的场景。
6、典型应用场景
6.1 、量化交易中的技术指标
EMA 是技术分析中最常用的指标之一,常被用于:
- 趋势判断:当短期 EMA(如 EMA12)上穿长期 EMA(如 EMA26)时,形成「金叉」,视为买入信号;反之形成「死叉」,视为卖出信号。
- 动态支撑与压力位:价格在 EMA 上方运行时,EMA 常被视为支撑位;价格在 EMA 下方运行时,EMA 常被视为压力位。
6.2 、深度学习优化器中的参数平滑
在深度学习训练中,EMA 被广泛用于模型参数的滑动平均。具体做法是:在训练过程中,维护一份模型参数的 EMA 副本,训练结束后用 EMA 副本替代原始参数进行推理。
python
# 伪代码示例:训练过程中维护参数 EMA
ema_decay = 0.999
ema_params = {}
for step, batch in enumerate(train_loader):
# 常规梯度更新
optimizer.zero_grad()
loss = compute_loss(batch)
loss.backward()
optimizer.step()
# 更新 EMA 参数
for name, param in model.named_parameters():
if name not in ema_params:
ema_params[name] = param.data.clone()
else:
ema_params[name] = ema_decay * ema_params[name] + (1 - ema_decay) * param.data
这样做的好处是:
- 对参数更新过程中的噪声进行平滑,使模型更加稳定;
- 在训练后期,EMA 参数往往能取得比原始参数更好的泛化性能。
6.3、 系统监控与异常检测
在服务器监控场景中,EMA 常用于平滑 CPU 使用率、内存占用等指标,帮助运维人员识别长期趋势,同时过滤瞬时抖动造成的误报。
7. 总结
本文详细介绍了指数移动平均(EMA)的数学原理、递推公式推导、代码实现及典型应用场景。核心要点总结如下:
- EMA 通过指数衰减的权重分配,对近期数据赋予更高权重;
- 平滑因子
α = 2 / (N + 1)建立了与窗口大小的换算关系; - EMA 只需保存上一时刻的值即可递推计算,空间复杂度为 O(1);
- 相比 SMA,EMA 响应更快,更适合实时趋势跟踪场景;
- EMA 在量化交易、深度学习参数平滑、系统监控等领域有广泛应用。
理解 EMA 的数学本质,有助于在实际问题中灵活选择平滑策略,从而更准确地把握数据的变化规律。
二、代码示例
cpp
#include <iostream>
#include <Windows.h>
#include <iostream>
#include <cassert>
class EMA
{
public:
/// @brief 构造:标准EMA,alpha = 2/(N+1),K线算法,前N点SMA初始化
explicit EMA(int period)
: alpha_(0.0f),
period_(period),
count_(0),
sum_(0.0f),
ema_prev_(0.0f),
init_done_(false)
{
assert(period > 0);
alpha_ = 2.0f / (period + 1.0f);
}
/// @brief 构造:一阶指数平滑,自定义alpha,传感器滤波
explicit EMA(float custom_alpha)
: alpha_(custom_alpha),
period_(0),
count_(0),
sum_(0.0f),
ema_prev_(0.0f),
init_done_(true)
{
assert(custom_alpha > 0.0f && custom_alpha <= 1.0f);
}
// 送入新采样值,返回当前EMA结果
float update(float val)
{
if (!init_done_)
{
sum_ += val;
count_++;
if (count_ >= period_)
{
ema_prev_ = sum_ / period_;
init_done_ = true;
}
return ema_prev_;
}
// 一阶平滑:首次进来直接赋值
if (count_ == 0)
{
ema_prev_ = val;
count_++;
return ema_prev_;
}
ema_prev_ = val * alpha_ + ema_prev_ * (1.0f - alpha_);
count_++;
return ema_prev_;
}
// 重置滤波器状态
void reset()
{
count_ = 0;
sum_ = 0.0f;
ema_prev_ = 0.0f;
init_done_ = false;
}
// 获取当前EMA值
float get() const
{
return ema_prev_;
}
private:
float alpha_;
int period_;
int count_;
float sum_;
float ema_prev_;
bool init_done_;
};
int main()
{
SetConsoleOutputCP(CP_UTF8);
// 方式1:标准EMA5,alpha=2/(5+1)
EMA ema5(5);
float prices[] = { 10, 12, 11, 13, 14, 16, 15, 17,10,2,2,3,5,4,8,63,45,1000,1,2,2000 };
int len = sizeof(prices) / sizeof(prices[0]);
std::cout << "===== EMA5 (周期构造) =====\n";
for (int i = 0; i < len; i++)
{
float v = ema5.update(prices[i]);
std::cout << "price=" << prices[i] << ", ema=" << v << "\n";
}
// 方式2:传感器一阶平滑,直接指定alpha=0.2
EMA filter(0.2f);
std::cout << "\n===== 一阶平滑 alpha=0.2 =====\n";
for (int i = 0; i < len; i++)
{
float v = filter.update(prices[i]);
std::cout << "price=" << prices[i] << ", filter=" << v << "\n";
}
return 0;
}
运行结果:
cpp
===== EMA5 (周期构造) =====
price=10, ema=0
price=12, ema=0
price=11, ema=0
price=13, ema=0
price=14, ema=12
price=16, ema=13.3333
price=15, ema=13.8889
price=17, ema=14.9259
price=10, ema=13.2839
price=2, ema=9.52263
price=2, ema=7.01509
price=3, ema=5.67672
price=5, ema=5.45115
price=4, ema=4.96743
price=8, ema=5.97829
price=63, ema=24.9855
price=45, ema=31.657
price=1000, ema=354.438
price=1, ema=236.625
price=2, ema=158.417
price=2000, ema=772.278
===== 一阶平滑 alpha=0.2 =====
price=10, filter=10
price=12, filter=10.4
price=11, filter=10.52
price=13, filter=11.016
price=14, filter=11.6128
price=16, filter=12.4902
price=15, filter=12.9922
price=17, filter=13.7938
price=10, filter=13.035
price=2, filter=10.828
price=2, filter=9.0624
price=3, filter=7.84992
price=5, filter=7.27994
price=4, filter=6.62395
price=8, filter=6.89916
price=63, filter=18.1193
price=45, filter=23.4955
price=1000, filter=218.796
price=1, filter=175.237
price=2, filter=140.59
price=2000, filter=512.472
d:\user\01417804\桌面\CppProject\cppTest\x64\Debug\cppTest.exe (进程 3732)已退出,代码为 0 (0x0)。
要在调试停止时自动关闭控制台,请启用"工具"->"选项"->"调试"->"调试停止时自动关闭控制台"。
按任意键关闭此窗口. . .

