卡尔曼滤波器在外汇均值回归策略中的应用·进阶篇
◍ 布林带叠加均线或卡尔曼的出场与开仓逻辑
这段逻辑先遍历当前所有持仓,只处理 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 编译不过。
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 手在策略测试器回测验证。
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 策略测试器用历史数据跑一遍验证夹层触发频率。
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 读取。下面这段代码把三版平仓时间抽成集合画韦恩图,能直接看出重叠笔数。
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()
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 前,先拿策略测试器跑重叠交易对比,确认过滤掉的是噪音而不是趋势段。工具归工具,能不能用活,看你愿不愿意自己改阈值、做验证。
Low Q and moderate R yield stable predictions, while high Q and low R make the filter more reactive but noisier.