MQL5 向导技巧须知(第27部分):移动平均线与攻击角度·进阶篇
◍ 把时间轴重标定成价格单位
在三角关系里,已知直角边底和高,用底除以高的反正切就能解出角度(单位度)。关键是底和高的量纲必须一致:水平轴和垂直轴单位相同,反余弦或反正切算出来的θ才有意义。价格图表横轴是时间、纵轴是价格,两类量纲不同,直接量角度会失真。 把两个轴统一,只有两条路:都转时间,或都转价格。资金随价格反向变动而缩水,所以更合理的是把横纵轴都用价格单位标记。也就是把时间距离D,重标成前一段D内的价格区间长度。外汇和贵金属杠杆高、跳空频繁,这种重标定只是降低量纲错配,不预示方向。 具体实现用可变比例尺:任取两个相距D的价格点,D被换算成此前D区间的价格单位跨度。历史区间价格极差可能为0,代码里用 symbol.Point() 兜底避免除零。下面这段 Angle 函数就是照此思路写的。 [CODE] //+------------------------------------------------------------------+ //| Get Angle function //+------------------------------------------------------------------+ double CSignalAA::Angle(int Index) { double _angle = 0.0; double _price = DM(Index) - DM(Index+m_length_period); double _max = DM(Index+m_length_period+1); double _min = DM(Index+m_length_period+1); for(int i=Index+m_length_period+2;i<Index+(2*m_length_period);i++) { double _dm = DM(i); _max = fmax(_max, _dm); _min = fmin(_min, _dm); } double _time = fmax(m_symbol.Point(), _max - _min); _angle = (180.0 / M_PI) * MathArctan(fabs(_price) / _time); if(_price < 0.0) { _angle *= -1.0; } return(_angle); } [/CODE] 逐行看:函数收一个 Index,确定量角起点;_price 是当下DM值与 m_length_period 根前的差,即纵向价格跨度。_max/_min 先取回溯段起点DM,再循环扫完一个长度周期,找出该历史窗口内DM的极大极小。_time 用极差值和 Point() 取大,充当横轴价格化后的长度。 角度由 fabs(_price)/_time 的反正切乘 180/π 得到;价格为负时角度转负,保留方向。读者把 m_length_period 调大,攻击角度会倾向更平滑,可在MT5里改参数看不同品种的反应。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Get Angle function class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSignalAA::Angle(class="type">int Index) { class="type">class="kw">double _angle = class="num">0.0; class="type">class="kw">double _price = DM(Index) - DM(Index+m_length_period); class="type">class="kw">double _max = DM(Index+m_length_period+class="num">1); class="type">class="kw">double _min = DM(Index+m_length_period+class="num">1); for(class="type">int i=Index+m_length_period+class="num">2;i<Index+(class="num">2*m_length_period);i++) { class="type">class="kw">double _dm = DM(i); _max = fmax(_max, _dm); _min = fmin(_min, _dm); } class="type">class="kw">double _time = fmax(m_symbol.Point(), _max - _min); _angle = (class="num">180.0 / M_PI) * MathArctan(fabs(_price) / _time); if(_price < class="num">0.0) { _angle *= -class="num">1.0; } class="kw">return(_angle); }
衰减移动平均比EMA掉权重更狠
衰减移动平均(DMA)给历史价格挂的是指数衰减权重,下降斜率比传统 EMA 更陡——越往过去,权重以 2 的幂次被快速榨干。样本大小 n 和位置索引 i 决定单项权重,P 为对应时刻价格。这类线若手动盯盘可用自定义指标挂缓冲,给 EA 当辅助层;但做逻辑验证时,用函数法更干净,免得编译 EA 多一层依赖。 MQL5 里实现 DMA 有个暗坑:CopyRates 拉回来的向量不是时间序列(non-series),最新价在索引 0 而不是末尾。for 循环从 0 往上数,所以权重公式里的指数得反着写,用 m_length_period-i 当幂,否则最新一根 K 的权重会被算到最老的位置上。 下面这段函数默认 Mask=8(即取最近 8 根之外的上下文),循环内先累加「1/2^(长度-i) × 价格」,再累加同式权重做归一,最后除权得均值。外汇与贵金属波动剧烈,DMA 对近期突变更敏感,实盘使用前建议在 MT5 策略测试器跑一轮历史数据确认信号滞后表现。 代码逐行拆解: // 函数属于 CSignalAA 类,DM 接收 Index 与可选 Mask(默认 8) double CSignalAA::DM(int Index, int Mask = 8) { double _dm = 0.0; // 均值累加器初始化 vector _r; // 声明价格向量 if(_r.CopyRates(m_symbol.Name(), m_period, Mask, Index, m_length_period)) // 从 Mask 偏移处拷 m_length_period 根 rates { //vectors are not series 注意向量非时间序列 double _weight = 0.0; // 权重总和 for(int i = 0; i < m_length_period; i++) // 正向遍历 { _dm += (1.0/pow(2.0, m_length_period-i))*_r[i]; // 反置指数挂权重乘价格 _weight += (1.0/pow(2.0, m_length_period-i)); // 同步累加权重 } if(_weight != 0.0) // 防零除 { _dm /= _weight; // 归一化得 DMA } } return(_dm); // 返回结果 }
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Decaying Mean | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CSignalAA::DM(class="type">int Index, class="type">int Mask = class="num">8) { class="type">class="kw">double _dm = class="num">0.0; vector _r; if(_r.CopyRates(m_symbol.Name(), m_period, Mask, Index, m_length_period)) { class=class="str">"cmt">//vectors are not series class="type">class="kw">double _weight = class="num">0.0; for(class="type">int i = class="num">0; i < m_length_period; i++) { _dm += (class="num">1.0/pow(class="num">2.0, m_length_period-i))*_r[i]; _weight += (class="num">1.0/pow(class="num">2.0, m_length_period-i)); } if(_weight != class="num">0.0) { _dm /= _weight; } } class="kw">return(_dm); }
「把攻击角度塞进信号类」
把衰减均值和角度函数收进一个信号类实例后,多头与空头条件只做一件事:从输入距离量起的角度幅度,是否达到或超过输入阈值。角度函数本身返回正或负值,所以开盘前多空分支各自先判符号,再判大小,本质是顺趋势投票。 角度过陡时,这套趋势跟随逻辑有可能被反转,本文没展开,但你可以自己改阈值试,看是否更抗洗盘。因为角度函数吐出的值比传统算法更“一致”,幅度基本不会摸到甚至接近 90 度,这给了我们干净归一化的空间。 归一化直接拿角度绝对值除以 90,再映射到 0–100 当 result 返回。这种写法把“攻击角度值”本身的权重放到最大;换别的缩放方式也能跑,但权重分布会偏。外汇与贵金属波动剧烈,这类信号仅作概率参考,实盘前请在 MT5 用历史数据验证。 下面这段是 LongCondition 与 ShortCondition 的原文,注意多空都用 round(100.0 * 角度/90.0) 做缩放,空头取 fabs 保证正值:
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will grow. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalAA::LongCondition(class="type">void) { class="type">int result = class="num">0; class="type">class="kw">double _angle = Angle(StartIndex()); if(_angle >= m_threshold) { result = class="type">int(round(class="num">100.0 * ((_angle) / (class="num">90.0)))); } class="kw">return(result); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will fall. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalAA::ShortCondition(class="type">void) { class="type">int result = class="num">0; class="type">class="kw">double _angle = Angle(StartIndex()); if(_angle <= -m_threshold) { result = class="type">int(round(class="num">100.0 * (fabs(_angle) / (class="num">90.0)))); } class="kw">return(result); }
◍ GBPCHF四小时回测的一年样本
拿 GBPCHF 的 4 小时图跑 2023 全年(2023.1.1–2024.1.1),两种方法的测试报告确实显出一些可用潜力,但全年成交次数偏少,样本厚度不够。 这类结果只覆盖单一年度,且没开策略测试器的逐步前进(walk-forward)筛法,没法快速区分哪套参数真有效、哪套只是过拟合。 我们这版用衰减移动平均斜率替掉了价格斜率。因为这种均值对最近价格权重极高,信号抖动会明显放大,实战里容易频繁翻脸,外汇和贵金属本身杠杆高、滑点跳空多,直接跟单风险不小。
给衰减角度加一道带宽围栏
把空气动力学里的临界角概念搬进价格行为,核心改动只有一句话:原来只看衰减均线角度是否越过阈值,现在要求它落在一个上限区间内。思路上不再用单点阈值拍板,而是用 m_band 这个双精度参数把触发带撑开。 我们在自定义信号类里加了一个 m_band 字段,配合已有的 m_threshold,多头条件变成角度既要 ≥ 阈值、又要 ≤ 阈值加带宽。空头对称处理,角度 ≤ -阈值且 ≥ -(阈值+带宽)才投卖出票。 实测组装成 EA 跑下来,最直观的变化是交易频率明显下降——这是意料之中的,因为过滤变严了。利润因子、恢复因子、回撤百分比这些指标多数倾向改善,但样本仍偏短,外汇与贵金属品种波动突变频繁,这类信号在高杠杆下仍属高风险,结论只能说概率上更稳,不能当确定性依据。 别把正态当圣经 带宽不是越宽越好,m_band 设太大就退化回单阈值逻辑,设太小则几乎不出信号。建议先在 EURUSD 的 M15 上把 m_threshold 固定、只扫 m_band 做参数敏感性,再决定实盘窗口。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will grow. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalAA::LongCondition(class="type">void) { class="type">int result = class="num">0; class="type">class="kw">double _angle = Angle(StartIndex()); if(_angle >= m_threshold && _angle <= m_threshold+m_band) { result = class="type">int(round(class="num">100.0 * ((_angle) / (class="num">90.0)))); } class="kw">return(result); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| "Voting" that price will fall. | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int CSignalAA::ShortCondition(class="type">void) { class="type">int result = class="num">0; class="type">class="kw">double _angle = Angle(StartIndex()); if(_angle <= -m_threshold && _angle >= -(m_threshold+m_band)) { result = class="type">int(round(class="num">100.0 * (fabs(_angle) / (class="num">90.0)))); } class="kw">return(result); }
「画得少,看得清」
把时间轴直接换成价格单位后再算攻击角度,比死磕传统价格-时间斜率靠谱得多。原文作者跑过两套过滤:单用角度阈值筛进场,以及要求攻击角越过阈值还卡在临界区间里,后者在回测里更省算力、信号也更干净。 附件里 attack_angle.mq5 和 SignalWZ_27_.mqh 是可直接拖进 MT5 验证的源码,黄金和欧元兑美元这类品种波动大、滑点狠,用临界角区间代替裸阈值,能砍掉一批假突破。 外汇和贵金属高风险,参数没调好一样亏。开 MT5 把周期切到 M15,先只加载角度阈值版本看信号密度,再加临界区间对比,哪套更少画线但更准,一眼便知。