构建和测试 Aroon 交易系统·进阶篇
📈

构建和测试 Aroon 交易系统·进阶篇

(2/3)· 从指标定义到策略测试器代码,手把手把 Aroon 做成能跑的 EA 而非纸上信号

含代码示例实战向 第 2/3 篇

不少交易者把 Aroon 上穿下穿当成了开仓闹钟,却从没在策略测试器里跑过一次完整样本。手工翻图表验证几个信号就以为系统靠谱,往往忽略了参数敏感性和滑点侵蚀。本篇接上篇基础概念,直接进代码层和回测层。

「Aroon 指标的缓冲区与画线属性初始化」

在 MT5 里写自定义 Aroon 指标,第一步是把两条输出线(上破线、下破线)的视觉属性钉死。下面这段代码把第二条线设成红色虚线、线宽 2、标签 "Down",第一条线默认走另一套配色,读者改 clrRed 为 clrDodgerBlue 就能换风格。 同时用 indicator_level1=10.0 与 indicator_level2=50.0 画了两条银色点线参考位,这对应 Aroon 里常用的 10/50 阈值区;价格行为上,Aroon 上破 50 倾向多头占优,下破 10 可能空头加速,但外汇与贵金属波动大,仅作概率参考。 输入参数 periodInp=25、shiftInp=0 控制周期与水平偏移。OnInit 中 SetIndexBuffer 把 upBuffer/downBuffer 绑到绘图槽,PlotIndexSetInteger 设偏移和起绘根数,ArraySetAsSeries(...,true) 让数组按时间倒序,StringFormat 拼出短名"Aroon Indicator (25,0) - "。 别把 25 周期当万能参数:EURUSD 在 M15 上用 25 周期 Aroon 过滤假突破,胜率倾向高于裸 K,但遇美联储决议前后跳空,信号可能失效,实盘前先用策略测试器跑半年 tick 数据。

MQL5 / C++
class="macro">#class="kw">property indicator_color2 clrRed         class=class="str">"cmt">// down line class="type">class="kw">color
class="macro">#class="kw">property indicator_style2 STYLE_DASH     class=class="str">"cmt">// down line style
class="macro">#class="kw">property indicator_width2 class="num">2              class=class="str">"cmt">// down line width
class="macro">#class="kw">property indicator_label2 "Down"         class=class="str">"cmt">// down line label
class=class="str">"cmt">// drawing some levels to be used later class="num">10 and class="num">50
class="macro">#class="kw">property indicator_level1 class="num">10.0
class="macro">#class="kw">property indicator_level2 class="num">50.0
class="macro">#class="kw">property indicator_levelcolor clrSilver
class="macro">#class="kw">property indicator_levelstyle STYLE_DOT
class=class="str">"cmt">//inputs
input class="type">int                periodInp = class="num">25; class=class="str">"cmt">// Period
input class="type">int                shiftInp  = class="num">0;  class=class="str">"cmt">// horizontal shift
class=class="str">"cmt">//buffers of the indicator
class="type">class="kw">double                   upBuffer[];
class="type">class="kw">double                   downBuffer[];
   SetIndexBuffer(class="num">0, upBuffer, INDICATOR_DATA);
   SetIndexBuffer(class="num">1, downBuffer, INDICATOR_DATA);
   PlotIndexSetInteger(class="num">0, PLOT_SHIFT, shiftInp);
   PlotIndexSetInteger(class="num">1, PLOT_SHIFT, shiftInp);
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, periodInp);
   PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, periodInp);
   ArraySetAsSeries(upBuffer, true);
   ArraySetAsSeries(downBuffer, true);
   class="type">class="kw">string indicatorName = StringFormat("Aroon Indicator(%i,%i) - ", periodInp, shiftInp);
   IndicatorSetString(INDICATOR_SHORTNAME, indicatorName);
   IndicatorSetInteger(INDICATOR_DIGITS, class="num">0);
class="kw">return INIT_SUCCEEDED;
class="type">int OnCalculate(const class="type">int     rates_total,
                const class="type">int     prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double  &open[],
                const class="type">class="kw">double  &high[],
                const class="type">class="kw">double  &low[],
                const class="type">class="kw">double  &close[],
                const class="type">long    &tick_volume[],
                const class="type">long    &volume[],

◍ Aroon 指标的缓冲区与计算循环拆解

这段 MT5 自定义指标代码把 Aroon 的上下轨塞进独立子窗口,用两个 buffer 分别画绿红虚线,默认周期参数 periodInp 取 25。 先看 OnCalculate 的边界处理:若总 BAR 数 rates_total 小于 periodInp-1 直接返回 0,避免数组越界;随后用 rates_total 减 prev_calculated 得到本次需计算的 count,若已有历史计算则补 1,并对 count 做上限钳制,保证不超出 (rates_total - periodInp + 1) 的范围。 核心循环从 count-1 倒序到 0:iHighest 取 i 位置往前 periodInp 根内的最高价 BAR 索引,iLowest 取最低价索引,upBuffer[i] = (periodInp - (highestVal - i)) * 100 / periodInp,downBuffer 同理。也就是说,最高价越靠近当前 BAR,Up 值越接近 100,倾向提示多头动能占优;外汇与贵金属杠杆高,该数值仅作概率参考。 指标属性段里 indicator_level1=10.0、level2=50.0 用银色点线标出,实战中 Up 跌破 10 可能意味着近期无新高,趋势衰竭概率上升。开 MT5 把这段直接贴进 Aroon.mq5 编译,调 periodInp 到 14 看黄金 M15 的灵敏度变化。

MQL5 / C++
const class="type">int& spread[])
  if(rates_total < periodInp - class="num">1)
    class="kw">return (class="num">0);
class="type">int count = rates_total - prev_calculated;
  if(prev_calculated > class="num">0)
    count++;
  if(count > (rates_total - periodInp + class="num">1))
    count = (rates_total - periodInp + class="num">1);
  for(class="type">int i = count - class="num">1; i >= class="num">0; i--)
   {
    class="type">int highestVal   = iHighest(Symbol(), Period(), MODE_HIGH, periodInp, i);
    class="type">int lowestVal    = iLowest(Symbol(), Period(), MODE_LOW, periodInp, i);
    upBuffer[i]   = (periodInp - (highestVal - i)) * class="num">100 / periodInp;
    downBuffer[i] = (periodInp - (lowestVal - i)) * class="num">100 / periodInp;
   }
class="kw">return (rates_total);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                Aroon.mq5 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property indicator_separate_window class=class="str">"cmt">// the place of the indicator
class="macro">#class="kw">property indicator_buffers class="num">2 class=class="str">"cmt">// number of buffers
class="macro">#class="kw">property indicator_plots class="num">2 class=class="str">"cmt">// number of plots
class="macro">#class="kw">property indicator_type1   DRAW_LINE       class=class="str">"cmt">// type of the up values to be drawn is a line
class="macro">#class="kw">property indicator_color1 clrGreen         class=class="str">"cmt">// up line class="type">class="kw">color
class="macro">#class="kw">property indicator_style1 STYLE_DASH       class=class="str">"cmt">// up line style
class="macro">#class="kw">property indicator_width1 class="num">2                class=class="str">"cmt">// up line width
class="macro">#class="kw">property indicator_label1 "Up"             class=class="str">"cmt">// up line label
class="macro">#class="kw">property indicator_type2   DRAW_LINE       class=class="str">"cmt">// type of the down values to be drawn is a line
class="macro">#class="kw">property indicator_color2 clrRed           class=class="str">"cmt">// down line class="type">class="kw">color
class="macro">#class="kw">property indicator_style2 STYLE_DASH       class=class="str">"cmt">// down line style
class="macro">#class="kw">property indicator_width2 class="num">2                class=class="str">"cmt">// down line width
class="macro">#class="kw">property indicator_label2 "Down"           class=class="str">"cmt">// down line label
class="macro">#class="kw">property indicator_level1 class="num">10.0
class="macro">#class="kw">property indicator_level2 class="num">50.0
class="macro">#class="kw">property indicator_levelcolor clrSilver
class="macro">#class="kw">property indicator_levelstyle STYLE_DOT
input class="type">int periodInp = class="num">25; class=class="str">"cmt">// Period

Aroon 指标的缓冲区与逐根计算写法

把 Aroon 搬进 MT5,第一步是在 OnInit 里把上下两条线绑到独立缓冲区,并允许水平偏移。下面这段代码里 shiftInp 默认 0,意味着指标线不对齐 K 线右移;若设成 1,视觉上整体右移一根,方便叠加其他信号观察。 [CODE] input int shiftInp = 0; // horizontal shift double upBuffer[]; double downBuffer[]; int OnInit() { SetIndexBuffer(0, upBuffer, INDICATOR_DATA); SetIndexBuffer(1, downBuffer, INDICATOR_DATA); PlotIndexSetInteger(0, PLOT_SHIFT, shiftInp); PlotIndexSetInteger(1, PLOT_SHIFT, shiftInp); PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, periodInp); PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, periodInp); ArraySetAsSeries(upBuffer, true); ArraySetAsSeries(downBuffer, true); string indicatorName = StringFormat("Aroon Indicator (%i,%i) - ", periodInp, shiftInp); IndicatorSetString(INDICATOR_SHORTNAME, indicatorName); IndicatorSetInteger(INDICATOR_DIGITS, 0); return INIT_SUCCEEDED; } [/CODE] 逐行拆解:input int shiftInp=0 暴露一个外部参数供 EA 或手动改;两个 double 数组是上下轨载体。SetIndexBuffer 把数组 0/1 注册为绘图数据,PlotIndexSetInteger 的 PLOT_SHIFT 控制水平位移,PLOT_DRAW_BEGIN 设成 periodInp 保证前 periodInp 根不画——避免周期不足时的鬼线。ArraySetAsSeries 置 true 让索引 0 对应最新柱,StringFormat 拼出带参数的短名,INDICATOR_DIGITS 设 0 因 Aroon 输出是 0~100 整数。 计算核心在 OnCalculate。当 rates_total < periodInp-1 直接 return 0,数据不够不运算。count 处理增量刷新:首次跑满 periodInp 根,后续只补新柱,prev_calculated>0 时 count++ 修正边界。 [CODE] int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total < periodInp - 1) return (0); int count = rates_total - prev_calculated; if(prev_calculated > 0) count++; if(count > (rates_total - periodInp + 1)) count = (rates_total - periodInp + 1); for(int i = count - 1; i >= 0; i--) { int highestVal = iHighest(Symbol(), Period(), MODE_HIGH, periodInp, i); int lowestVal = iLowest(Symbol(), Period(), MODE_LOW, periodInp, i); upBuffer[i] = (periodInp - (highestVal - i)) * 100 / periodInp; downBuffer[i] = (periodInp - (lowestVal - i)) * 100 / periodInp; } return (rates_total); } [/CODE] 循环里 iHighest / iLowest 在当前柱 i 往前 periodInp 根内找极值位置。upBuffer[i] 公式:(periodInp - (最高价所在柱距i的偏移)) * 100 / periodInp,结果 100 表示最高价就在 i 这根,0 表示在周期最左端。外汇与贵金属波动随机性强,Aroon 读数仅反映历史 N 根的相对位置,倾向用于识别趋势萌芽概率,实盘须警惕假突破高风险。

MQL5 / C++
input class="type">int shiftInp = class="num">0; class=class="str">"cmt">// horizontal shift
class="type">class="kw">double upBuffer[];
class="type">class="kw">double downBuffer[];
class="type">int OnInit()
  {
   SetIndexBuffer(class="num">0, upBuffer, INDICATOR_DATA);
   SetIndexBuffer(class="num">1, downBuffer, INDICATOR_DATA);
   PlotIndexSetInteger(class="num">0, PLOT_SHIFT, shiftInp);
   PlotIndexSetInteger(class="num">1, PLOT_SHIFT, shiftInp);
   PlotIndexSetInteger(class="num">0, PLOT_DRAW_BEGIN, periodInp);
   PlotIndexSetInteger(class="num">1, PLOT_DRAW_BEGIN, periodInp);
   ArraySetAsSeries(upBuffer, true);
   ArraySetAsSeries(downBuffer, true);
   class="type">class="kw">string indicatorName = StringFormat("Aroon Indicator(%i,%i) - ", periodInp, shiftInp);
   IndicatorSetString(INDICATOR_SHORTNAME, indicatorName);
   IndicatorSetInteger(INDICATOR_DIGITS, class="num">0);
   class="kw">return INIT_SUCCEEDED;
  }
class="type">int OnCalculate(const class="type">int rates_total,
                const class="type">int prev_calculated,
                const class="type">class="kw">datetime &time[],
                const class="type">class="kw">double &open[],
                const class="type">class="kw">double &high[],
                const class="type">class="kw">double &low[],
                const class="type">class="kw">double &close[],
                const class="type">long &tick_volume[],
                const class="type">long &volume[],
                const class="type">int &spread[])
  {
   if(rates_total < periodInp - class="num">1)
      class="kw">return (class="num">0);
   class="type">int count = rates_total - prev_calculated;
   if(prev_calculated > class="num">0)
      count++;
   if(count > (rates_total - periodInp + class="num">1))
      count = (rates_total - periodInp + class="num">1);
   for(class="type">int i = count - class="num">1; i >= class="num">0; i--)
     {
      class="type">int highestVal = iHighest(Symbol(), Period(), MODE_HIGH, periodInp, i);
      class="type">int lowestVal  = iLowest(Symbol(), Period(), MODE_LOW, periodInp, i);
      upBuffer[i]   = (periodInp - (highestVal - i)) * class="num">100 / periodInp;
      downBuffer[i] = (periodInp - (lowestVal - i)) * class="num">100 / periodInp;
     }
   class="kw">return (rates_total);
  }

「把 Aroon 双线接进 EA 实时读值」

想在 MT5 里用 Aroon 指标驱动自动交易,第一步是把自定义指标句柄拿到手。下面这段用 iCustom 加载名为 "Aroon" 的自定义指标,周期取当前图表、参数 periodInp=25、偏移 shiftInp=0,句柄存进 int 变量 aroon。 在 OnTick 里每次刷新都通过 CopyBuffer 取最近 3 根 K 线的上行线与下行线数据,再 ArraySetAsSeries 设成时间序列,让 [0] 永远是最新值。upBuffer[0] 与 downBuffer[0] 就是当前 Aroon 上/下值,用 Comment 直接打印到图表左上角,肉眼就能看。 若给 EA 加上交易动作,常见做法是引入 <trade/trade.mqh>,设 lotSize=1、slLvl=200 点、tpLvl=600 点,并用 iBars 取当前符号与周期的 BAR 总数。外汇与贵金属杠杆高,Aroon 穿越信号只代表趋势动能倾向,不代表胜率,实盘前务必在策略测试器跑历史数据。 别把指标句柄写进 OnTick 里反复创建 iCustom 只需在 OnInit 调用一次,反复建句柄会拖慢执行并可能触发资源告警。句柄建好留全局,OnTick 只管 CopyBuffer 读数就行。

MQL5 / C++
input class="type">int      periodInp = class="num">25; class=class="str">"cmt">// Period
input class="type">int      shiftInp  = class="num">0;  class=class="str">"cmt">// Shift
class="type">int aroon;
aroon = iCustom(_Symbol,PERIOD_CURRENT,"Aroon",periodInp,shiftInp);
class="kw">return(INIT_SUCCEEDED);
class="type">void OnDeinit(const class="type">int reason)
  {
   Print("EA is removed");
  }
class="type">class="kw">double upBuffer[], downBuffer[];
   CopyBuffer(aroon,class="num">0,class="num">0,class="num">3,upBuffer);
   CopyBuffer(aroon,class="num">1,class="num">0,class="num">3,downBuffer);
   ArraySetAsSeries(upBuffer,true);
   ArraySetAsSeries(downBuffer,true);
   class="type">class="kw">double upValue = upBuffer[class="num">0];
   class="type">class="kw">double downValue = downBuffer[class="num">0];
Comment("upValue: ",upValue,"\ndownValue: ",downValue);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                        AroonValuesEA.mq5 |
class=class="str">"cmt">//+------------------------------------------------------------------+
input class="type">int      periodInp = class="num">25; class=class="str">"cmt">// Period
input class="type">int      shiftInp  = class="num">0;  class=class="str">"cmt">// Shift
class="type">int aroon;
class="type">int OnInit()
  {
   aroon = iCustom(_Symbol,PERIOD_CURRENT,"Aroon",periodInp,shiftInp);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
   Print("EA is removed");
  }
class="type">void OnTick()
  {
   class="type">class="kw">double upBuffer[], downBuffer[];
   CopyBuffer(aroon,class="num">0,class="num">0,class="num">3,upBuffer);
   CopyBuffer(aroon,class="num">1,class="num">0,class="num">3,downBuffer);
   ArraySetAsSeries(upBuffer,true);
   ArraySetAsSeries(downBuffer,true);
   class="type">class="kw">double upValue = upBuffer[class="num">0];
   class="type">class="kw">double downValue = downBuffer[class="num">0];
   Comment("upValue: ",upValue,"\ndownValue: ",downValue);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <trade/trade.mqh>
input class="type">int      periodInp = class="num">25; class=class="str">"cmt">// Period
input class="type">int      shiftInp  = class="num">0;  class=class="str">"cmt">// Shift
input class="type">class="kw">double   lotSize=class="num">1;
input class="type">class="kw">double   slLvl=class="num">200;
input class="type">class="kw">double   tpLvl=class="num">600;
class="type">int aroon;
class="type">int barsTotal;
CTrade trade;
barsTotal=iBars(_Symbol,PERIOD_CURRENT);

◍ 把 Aroon 交叉写成可跑的 EA 骨架

下面这段 MT5 代码把 Aroon 指标的交叉直接接进了开仓逻辑。默认参数 periodInp=25、slLvl=200 点、tpLvl=600 点,黄金 1 标准手跑这套时止损约 200 点、止盈约 600 点,属于典型的宽止损窄止盈反交叉思路,外汇与贵金属波动剧烈,实盘前务必在策略测试器里用历史数据验证。 OnInit 里用 iCustom 挂上名为 "Aroon" 的自定义指标,OnTick 只在新 K 线出现时(barsTotal 变化)才读缓冲,避免每跳重复计算。CopyBuffer 取 0 号和 1 号缓冲各 3 根,ArraySetAsSeries 设成时间序列后,upBuffer[1] 即上一根 Aroon Up,upBuffer[0] 是当前根。 交叉判定很直白:上一根 Up 低于 Down、当前根 Up 高于 Down,视为多头反转,用 SymbolInfoDouble 取 SYMBOL_ASK 市价买;反向则取 SYMBOL_BID 卖。slVal/tpVal 用 _Point 乘输入档位,trade.Buy/Sell 把止损止盈一并发单。 让小布替你跑这套 把 slLvl、tpLvl 和 periodInp 丢进小布的参数扫描,看 25 周期在 EURUSD 与 XAUUSD 上哪边交叉假信号更少,再决定要不要上实盘。

MQL5 / C++
aroon = iCustom(_Symbol,PERIOD_CURRENT,"Aroon",periodInp,shiftInp);
Print("EA is removed");
class="type">int bars=iBars(_Symbol,PERIOD_CURRENT);
if(barsTotal != bars)
barsTotal=bars;
   class="type">class="kw">double upBuffer[], downBuffer[];
   CopyBuffer(aroon,class="num">0,class="num">0,class="num">3,upBuffer);
   CopyBuffer(aroon,class="num">1,class="num">0,class="num">3,downBuffer);
   ArraySetAsSeries(upBuffer,true);
   ArraySetAsSeries(downBuffer,true);
   class="type">class="kw">double prevUpValue = upBuffer[class="num">1];
   class="type">class="kw">double prevDownValue = downBuffer[class="num">1];
   class="type">class="kw">double upValue = upBuffer[class="num">0];
   class="type">class="kw">double downValue = downBuffer[class="num">0];
if(prevUpValue<prevDownValue && upValue>downValue)
       class="type">class="kw">double ask = SymbolInfoDouble(_Symbol,SYMBOL_ASK);
       class="type">class="kw">double slVal=ask - slLvl*_Point;
       class="type">class="kw">double tpVal=ask + tpLvl*_Point;
       trade.Buy(lotSize,_Symbol,ask,slVal,tpVal);
if(prevUpValue>prevDownValue && upValue<downValue)
       class="type">class="kw">double bid = SymbolInfoDouble(_Symbol,SYMBOL_BID);
       class="type">class="kw">double slVal=bid + slLvl*_Point;
       class="type">class="kw">double tpVal=bid - tpLvl*_Point;
       trade.Sell(lotSize,_Symbol,bid,slVal,tpVal);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                      AroonCrossoverEA.mq5 |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#include <trade/trade.mqh>
input class="type">int      periodInp = class="num">25; class=class="str">"cmt">// Period
input class="type">int      shiftInp  = class="num">0; class=class="str">"cmt">// Shift
input class="type">class="kw">double   lotSize=class="num">1;
input class="type">class="kw">double   slLvl=class="num">200;
input class="type">class="kw">double   tpLvl=class="num">600;
class="type">int aroon;
class="type">int barsTotal;
CTrade trade;
class="type">int OnInit()
  {
   barsTotal=iBars(_Symbol,PERIOD_CURRENT);
   aroon = iCustom(_Symbol,PERIOD_CURRENT,"Aroon",periodInp,shiftInp);
   class="kw">return(INIT_SUCCEEDED);
  }
class="type">void OnDeinit(const class="type">int reason)
  {
   Print("EA is removed");
  }
class="type">void OnTick()
  {
   class="type">int bars=iBars(_Symbol,PERIOD_CURRENT);
   if(barsTotal != bars)
     {
      barsTotal=bars;
      class="type">class="kw">double upBuffer[], downBuffer[];
把回测流水线交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到 Aroon 强弱区与历史回测概览,把重复劳动交给小布,你专注决策。

常见问题

原文用默认 25 周期做基准,但回测显示对外汇贵金属这类高波动品种,14–30 区间都可能出现明显差异,建议用策略测试器的参数优化跑一遍而非拍脑袋定。
文中代码是策略测试框架,实盘需自行加风控与订单管理。外汇贵金属杠杆高、跳空频繁,任何系统都只倾向在严格仓位下小范围验证。
小布的品种页已聚合常见指标区与历史表现快照,虽未内嵌本篇具体 EA 源码,但可对照 Aroon 强弱带快速判断当前趋势阶段,省去手算。
测试器默认固定点差与无延迟,实盘滑点和重组行情会让震荡市信号失效概率上升,需在多品种多周期交叉验证。
OnTick 负责指标读取与信号判定,OnTrade 处理成交回执与仓位修正,分离逻辑能避免回测与实盘行为偏移。