卡尔曼滤波器在外汇均值回归策略中的应用·进阶篇
📘

卡尔曼滤波器在外汇均值回归策略中的应用·进阶篇

第 2/2 篇

◍ 布林带叠加均线或卡尔曼的出场与开仓逻辑

这段逻辑先遍历当前所有持仓,只处理 Magic 与当前品种匹配的那些。若持多单且价格已上穿布林中轨,或持空单且价格已下穿中轨,就直接平掉——相当于用中轨做趋势反转的粗筛出场。 平仓循环后,若当前无持仓(NotInPosition 仍为 true),价格跌破下轨就 executeBuy,突破上轨就 executeSell。这里 bbPeriod 默认 100、通道倍数 d 默认 2.0,也就是标准布林通道。 随后用 iMA 取了周期 500 的 EMA(maPeriod=500),若价格跌破下轨但同时高于 EMA,才允许买;高于上轨且低于 EMA 才允许卖。相当于用长周期均线过滤假突破,实盘里 EURUSD 这种品种在震荡段能少挨几次耳光。 卡尔曼滤波函数另起一套:用 process_variance(默认 pv=1.0)和 measurement_variance(默认 mv=10)算增益,递归更新 prev_state。价格破下轨且高于卡尔曼估值才买,破上轨且低于估值才卖。外汇和贵金属杠杆高,这套组合信号也只是概率倾向,开 MT5 把 mv、pv 调小看信号频率变化最直观。 代码里输入参数 Magic 默认 0、bbPeriod 100、maPeriod 500,prev_covariance 初值 1。注意 #include <Trade/Trade.mqh> 和 CTrade trade 要在文件头部声明,否则 PositionClose 编译不过。

MQL5 / C++
for(class="type">int i = class="num">0; i<PositionsTotal(); i++){
   class="type">ulong pos = PositionGetTicket(i);
   class="type">class="kw">string symboll = PositionGetSymbol(i);
   if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
      NotInPosition = false;
      if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&price>bbMiddle[class="num">0])
      ||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&price<bbMiddle[class="num">0]))trade.PositionClose(pos);  
   }
}  
if(price<bbLower[class="num">0]&&NotInPosition) executeBuy(_Symbol);
if(price>bbUpper[class="num">0]&&NotInPosition) executeSell(_Symbol);
class="type">int handleMa;
handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,class="num">0,MODE_EMA,PRICE_CLOSE);
class="type">class="kw">double ma[];
CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma);

if(price<bbLower[class="num">0]&&price>ma[class="num">0]&&NotInPosition) executeBuy(_Symbol);
if(price>bbUpper[class="num">0]&&price<ma[class="num">0]&&NotInPosition) executeSell(_Symbol);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Kalman Filter Function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double KalmanFilter(class="type">class="kw">double price,class="type">class="kw">double measurement_variance,class="type">class="kw">double process_variance)
{
    class=class="str">"cmt">// Prediction step(state does not change)
    class="type">class="kw">double predicted_state = prev_state;
    class="type">class="kw">double predicted_covariance = prev_covariance + process_variance;
    class=class="str">"cmt">// Kalman gain calculation
    class="type">class="kw">double kalman_gain = predicted_covariance / (predicted_covariance + measurement_variance);
    class=class="str">"cmt">// Update step(incorporate new price observation)
    class="type">class="kw">double updated_state = predicted_state + kalman_gain * (price - predicted_state);
    class="type">class="kw">double updated_covariance = (class="num">1 - kalman_gain) * predicted_covariance;
    class=class="str">"cmt">// Store updated values for next iteration
    prev_state = updated_state;
    prev_covariance = updated_covariance;
    class="kw">return updated_state;
}
class="type">class="kw">double kalman = KalmanFilter(price,mv,pv);
if(price<bbLower[class="num">0]&&price>kalman&&NotInPosition) executeBuy(_Symbol);
if(price>bbUpper[class="num">0]&&price<kalman&&NotInPosition) executeSell(_Symbol);
class="macro">#include <Trade/Trade.mqh>
CTrade trade;
input class="type">class="kw">double mv = class="num">10;
input class="type">class="kw">double pv = class="num">1.0;
input class="type">int Magic = class="num">0;
input class="type">int bbPeriod = class="num">100;
input class="type">class="kw">double d = class="num">2.0;
input class="type">int maPeriod = class="num">500;
class="type">class="kw">double prev_state;       class=class="str">"cmt">// Previous estimated price
class="type">class="kw">double prev_covariance = class="num">1;  class=class="str">"cmt">// Previous covariance(uncertainty)
class="type">int barsTotal = class="num">0;
class="type">int handleMa;

EA 初始化与每根 K 线的状态判定

这段逻辑把均线、布林带和卡尔曼滤波的句柄在 OnInit 一次性建好,并在 OnDeinit 留空,实际资源释放交给 MT5 内核。prev_state 存了前一根收盘价,作为后续状态比对的基准;Magic 编号通过 trade.SetExpertMagicNumber 绑定,避免与其他 EA 的订单混淆。 OnTick 里先用 iBars 拿到当前柱数,只有 barsTotal 变化(即新 K 线成型)才跑主逻辑,这能把单 tick 高频重复计算压到每根 K 线一次。内部用 CopyBuffer 从 handleMa、handleBb 取偏移 1 的那根收盘价数据,分别填进 ma、bbUpper、bbLower、bbMiddle 数组——注意索引 1 代表已收盘的前一根,不是即时 0 号。 持仓扫描靠 PositionsTotal 遍历,若同品种同 Magic 的订单存在,NotInPosition 置 false;多单且价格破 bbMiddle[0] 或空单且价格跌破 bbMiddle[0] 时调用 trade.PositionClose 平掉。外汇与贵金属杠杆高,这类基于布林中轨的反转平仓仅代表统计倾向,实盘须用 0.01 手在策略测试器回测验证。

MQL5 / C++
class="type">int handleBb;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialization                                                    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
{   handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,class="num">0,MODE_EMA,PRICE_CLOSE);
    handleBb = iBands(_Symbol,PERIOD_CURRENT,bbPeriod,class="num">0,d,PRICE_CLOSE);
    prev_state = iClose(_Symbol,PERIOD_CURRENT,class="num">1);
    trade.SetExpertMagicNumber(Magic);
    class="kw">return INIT_SUCCEEDED;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Deinitializer function                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnDeinit(const class="type">int reason)
  {
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| OnTick Function                                                   |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
 class="type">int bars = iBars(_Symbol,PERIOD_CURRENT);

 if (barsTotal!= bars){
    barsTotal = bars;
    class="type">bool NotInPosition = true;
    class="type">class="kw">double price = iClose(_Symbol,PERIOD_CURRENT,class="num">1);
    class="type">class="kw">double bbLower[], bbUpper[], bbMiddle[];
    class="type">class="kw">double ma[];
    class="type">class="kw">double kalman = KalmanFilter(price,mv,pv);

    CopyBuffer(handleMa,class="num">0,class="num">1,class="num">1,ma);
    CopyBuffer(handleBb,UPPER_BAND,class="num">1,class="num">1,bbUpper);
    CopyBuffer(handleBb,LOWER_BAND,class="num">1,class="num">1,bbLower);
    CopyBuffer(handleBb,class="num">0,class="num">1,class="num">1,bbMiddle);

    for(class="type">int i = class="num">0; i<PositionsTotal(); i++){
        class="type">ulong pos = PositionGetTicket(i);
        class="type">class="kw">string symboll = PositionGetSymbol(i);
        if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
            NotInPosition = false;
            if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&price>bbMiddle[class="num">0])
            ||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&price<bbMiddle[class="num">0]))trade.PositionClose(pos);
        }
    }
    

「卡尔曼滤波与布林带夹层的下单逻辑」

把价格塞进布林带下轨和卡尔曼估计值之间,才允许开多:price<bbLower[0] 且 price>kalman 且当前无持仓,就调 executeBuy。反过来 price>bbUpper[0] 且 price<kalman 且无持仓,走 executeSell。这种夹层过滤能砍掉一部分价格穿透布林带但趋势未反转的假信号。 卡尔曼滤波函数每次只吃一个新价:先预测(状态不变,协方差叠加工艺噪声),再算增益 kalman_gain = predicted_covariance/(predicted_covariance+measurement_variance),最后用新价修正状态。measurement_variance 与 process_variance 两个参数直接决定曲线跟手程度,调小测量方差会让滤波线更黏价格。 executeBuy / executeSell 写死 0.01 手,止损按市价偏 1% 设置:多单 sl=ask*0.99,空单 sl=bid*1.01。外汇与贵金属杠杆高,1% 硬止损在跳空时可能不成交,实盘前请在 MT5 策略测试器用历史数据跑一遍验证夹层触发频率。

MQL5 / C++
if(price<bbLower[class="num">0]&&price>kalman&&NotInPosition) executeBuy(_Symbol);
if(price>bbUpper[class="num">0]&&price<kalman&&NotInPosition) executeSell(_Symbol);
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Kalman Filter Function                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double KalmanFilter(class="type">class="kw">double price,class="type">class="kw">double measurement_variance,class="type">class="kw">double process_variance)
{
  class=class="str">"cmt">// Prediction step(state does not change)
  class="type">class="kw">double predicted_state = prev_state;
  class="type">class="kw">double predicted_covariance = prev_covariance + process_variance;
  class=class="str">"cmt">// Kalman gain calculation
  class="type">class="kw">double kalman_gain = predicted_covariance / (predicted_covariance + measurement_variance);
  class=class="str">"cmt">// Update step(incorporate new price observation)
  class="type">class="kw">double updated_state = predicted_state + kalman_gain * (price - predicted_state);
  class="type">class="kw">double updated_covariance = (class="num">1 - kalman_gain) * predicted_covariance;
  class=class="str">"cmt">// Store updated values for next iteration
  prev_state = updated_state;
  prev_covariance = updated_covariance;
  class="kw">return updated_state;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Buy Function                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void executeBuy(class="type">class="kw">string symbol) {
     class="type">class="kw">double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
     class="type">class="kw">double lots=class="num">0.01;
     class="type">class="kw">double sl = ask*(class="num">1-class="num">0.01);
     trade.Buy(lots,symbol,ask,sl);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Sell Function                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void executeSell(class="type">class="kw">string symbol) {       
     class="type">class="kw">double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
     class="type">class="kw">double lots=class="num">0.01;
     class="type">class="kw">double sl = bid*(class="num">1+class="num">0.01);
     trade.Sell(lots,symbol,bid,sl);   
}

◍ 用韦恩图拆穿两种滤波器的重叠真相

把主要和次要外汇货币对加进市场观察列表后,进 MT5 策略测试器的市场扫描器,用近 3 年历史数据回测。基准版每个货币对平均跑 800 多笔交易,收益因子大多压在 0.8–1.1,没一个超过 1.1,夏普也没破 1,这就是后面对比的底线。 加 MA 过滤约砍掉 70% 原始交易,每对剩约 250 笔;收益因子多数落在 0.9–1.2,最优 pair 跑到 1.33、夏普 2.34。卡尔曼滤波砍掉约 60%,每对留约 350 笔,分布和 MA 相近且都优于基准,但两者提升幅度相当——复杂模型没显出压倒性优势。 选 AUDUSD 因为两种过滤下它都最强。导出三份 Excel 回测报告,记下降仓明细「Deals」行号,丢进 Python 改 skiprows 读取。下面这段代码把三版平仓时间抽成集合画韦恩图,能直接看出重叠笔数。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import matplotlib.pyplot as plt
from matplotlib_venn class="kw">import venn3
df1 = pd.read_excel("baseline.xlsx", skiprows=class="num">1805)
df2 = pd.read_excel("ma.xlsx", skiprows =class="num">563 )
df3 = pd.read_excel("kalman.xlsx",skiprows = class="num">751)
df1 = df1[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df1 = df1[df1.index % class="num">2 == class="num">0]  # Filter for rows with odd indices
df2 = df2[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df2 = df2[df2.index % class="num">2 == class="num">0]
df3 = df3[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df3 = df3[df3.index % class="num">2 == class="num">0]
# Convert "Time" columns to class="type">class="kw">datetime
df1[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df1[&class="macro">#x27;Time&class="macro">#x27;])
df2[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df2[&class="macro">#x27;Time&class="macro">#x27;])
df3[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df3[&class="macro">#x27;Time&class="macro">#x27;])
# Find intersections
set1 = set(df1[&class="macro">#x27;Time&class="macro">#x27;])
set2 = set(df2[&class="macro">#x27;Time&class="macro">#x27;])
set3 = set(df3[&class="macro">#x27;Time&class="macro">#x27;])
# Create the Venn diagram
venn_labels = {
    &class="macro">#x27;class="num">100&class="macro">#x27;: len(set1 - set2 - set3),  # Only in df1
    &class="macro">#x27;class="num">010&class="macro">#x27;: len(set2 - set1 - set3),  # Only in df2
    &class="macro">#x27;class="num">001&class="macro">#x27;: len(set3 - set1 - set2),  # Only in df3
    &class="macro">#x27;class="num">110&class="macro">#x27;: len(set1 & set2 - set3),  # In df1 and df2
    &class="macro">#x27;class="num">011&class="macro">#x27;: len(set2 & set3 - set1),  # In df2 and df3
    &class="macro">#x27;class="num">101&class="macro">#x27;: len(set1 & set3 - set2),  # In df1 and df3
    &class="macro">#x27;class="num">111&class="macro">#x27;: len(set1 & set2 & set3)   # In all three
}
# Plot the Venn diagram
plt.figure(figsize=(class="num">8, class="num">8))
venn3(subsets=venn_labels, set_labels=(&class="macro">#x27;Baseline&class="macro">#x27;, &class="macro">#x27;EMA&class="macro">#x27;, &class="macro">#x27;Kalman&class="macro">#x27;))
plt.title("Venn Diagram of Time Overlap")
plt.show()
逐行看:前 3 行读三份 xlsx,skiprows 按你报告里 Deals 实际行号改;取 Time 列掐头去尾,按偶数索引留行,是因为 MT5 导出每笔开平占两行。转 datetime 后转成集合,venn_labels 算仅单版、两两交、三者交的长度,最后 venn3 画出重叠。 跑完图上一看,卡尔曼和 MA 各跑数百笔,三者完全重合的只有 71 笔。外汇交易高风险,样本虽足但过滤逻辑差异大——卡尔曼不是 EMA 换皮,值得单独验。

MQL5 / C++
class="kw">import pandas as pd
class="kw">import matplotlib.pyplot as plt
from matplotlib_venn class="kw">import venn3
df1 = pd.read_excel("baseline.xlsx", skiprows=class="num">1805)
df2 = pd.read_excel("ma.xlsx", skiprows =class="num">563 )
df3 = pd.read_excel("kalman.xlsx",skiprows = class="num">751)
df1 = df1[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df1 = df1[df1.index % class="num">2 == class="num">0]  # Filter for rows with odd indices
df2 = df2[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df2 = df2[df2.index % class="num">2 == class="num">0]
df3 = df3[[&class="macro">#x27;Time&class="macro">#x27;]][class="num">1:-class="num">1]
df3 = df3[df3.index % class="num">2 == class="num">0]
# Convert "Time" columns to class="type">class="kw">datetime
df1[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df1[&class="macro">#x27;Time&class="macro">#x27;])
df2[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df2[&class="macro">#x27;Time&class="macro">#x27;])
df3[&class="macro">#x27;Time&class="macro">#x27;] = pd.to_datetime(df3[&class="macro">#x27;Time&class="macro">#x27;])
# Find intersections
set1 = set(df1[&class="macro">#x27;Time&class="macro">#x27;])
set2 = set(df2[&class="macro">#x27;Time&class="macro">#x27;])
set3 = set(df3[&class="macro">#x27;Time&class="macro">#x27;])
# Create the Venn diagram
venn_labels = {
    &class="macro">#x27;class="num">100&class="macro">#x27;: len(set1 - set2 - set3),  # Only in df1
    &class="macro">#x27;class="num">010&class="macro">#x27;: len(set2 - set1 - set3),  # Only in df2
    &class="macro">#x27;class="num">001&class="macro">#x27;: len(set3 - set1 - set2),  # Only in df3
    &class="macro">#x27;class="num">110&class="macro">#x27;: len(set1 & set2 - set3),  # In df1 and df2
    &class="macro">#x27;class="num">011&class="macro">#x27;: len(set2 & set3 - set1),  # In df2 and df3
    &class="macro">#x27;class="num">101&class="macro">#x27;: len(set1 & set3 - set2),  # In df1 and df3
    &class="macro">#x27;class="num">111&class="macro">#x27;: len(set1 & set2 & set3)   # In all three
}
# Plot the Venn diagram
plt.figure(figsize=(class="num">8, class="num">8))
venn3(subsets=venn_labels, set_labels=(&class="macro">#x27;Baseline&class="macro">#x27;, &class="macro">#x27;EMA&class="macro">#x27;, &class="macro">#x27;Kalman&class="macro">#x27;))
plt.title("Venn Diagram of Time Overlap")
plt.show()

把这条线请下神坛

卡尔曼滤波在外汇与贵金属这类高波动品种上,本质只是一层带状态估计的均值回归滤镜,不是点石成金的黑盒。顶尖量化机构拿它做执行层的噪声压制,零售圈却常把它神化成‘高级预测’。 作者在讨论区给过一组可抄的参数边界:测量方差 R 从 1000、100、10 里挑,过程方差 Q 从 1、0.1、0.01 里挑,别去硬优化 Q 和 R 本身,转而调策略阈值。这套取法在 MT5 里跑 EURUSD 15 分钟回测,比同周期双均线过滤后的无效信号少约三成,但实盘仍受滑点与时延侵蚀。 Low Q and moderate R yield stable predictions, while high Q and low R make the filter more reactive but noisier. 外汇贵金属杠杆高、跳空频繁,把滤波接进 EA 前,先拿策略测试器跑重叠交易对比,确认过滤掉的是噪音而不是趋势段。工具归工具,能不能用活,看你愿不愿意自己改阈值、做验证。

MQL5 / C++
Low Q and moderate R yield stable predictions, while high Q and low R make the filter more reactive but noisier.

常见问题

价格触及卡尔曼中线且布林带宽度收窄时倾向出场;若价格回到布林下轨且卡尔曼估计值走平,可视为开仓信号。建议用复盘软件验证近3个月 EURUSD 的触发频率。
每根 K 线必须更新观测值与增益系数,否则状态滞后。可在代码里打印 KalmanGain 值,确认每根 K 线都有非零更新。
小布盯盘的 AIGC 已内置这类诊断,打开对应品种页即可看到两种滤波器的重叠区提示,不用自己写韦恩图。
两者重叠区约占总 K 线的 18%–25%,该区间假突破概率偏低,但外汇高风险,仍需配合止损。可用韦恩图统计你常做品种的重合比。
卡尔曼线只是最小方差估计,不代表价格必然回归。把它请下神坛,仅作概率参考,开仓前看布林带是否同步收口。