开发多币种 EA(第 26 部分):交易品种信息工具·进阶篇
📘

开发多币种 EA(第 26 部分):交易品种信息工具·进阶篇

第 2/3 篇

从零搭 SymbolsInformer 的工程目录

新建一个 SymbolsInformer 仓库,除 main 外再开 develop 分支。若系列多篇文章推进,每篇从 develop 拉出独立分支,就绪后合回 develop 与 main,避免改动互相踩踏。 在 MT5 终端目录里建 MQL5/Experts/Article/17606 作为项目根,把仓库克隆进去,并新建 Include 子目录承接第三方库。当前仅依赖 Adwizard 一个库,将其仓库克隆进 Include;后续若增依赖,仍克隆到同一 Include 内,不另起层级。 Adwizard 克隆完后切到 develop 分支——这是各篇文章的共用基线。若本项目不改动该库,就停在 develop 只做 pull 更新;一旦要修库内问题,再开新分支隔离。 最后从项目仓库切出本文专属分支动手写代码。这套目录约定能让 MT5 的 #include 路径稳定,读者现在就能在终端文件夹里照此建好结构,等下篇细化。

「先把 EA 的统计边界钉死」

做这种单向蜡烛序列统计,没必要上指标。指标要周期重算、常驻刷新,而我们要的只是一次性拉出某段历史的分布,所以用 EA 跑更干净。 统计得先圈时间段。最简单的做法是以当前日期为终点,往回数 N 根主周期蜡烛。比如主周期挂 D1、取 200 根,就是约 200 个交易日的样本;主周期挂 H1、取 500 根,则覆盖约 21 天。这段我们叫「主要时间周期」,后续所有计数都基于它。 品种和周期不能写死。EA 起码要支持逗号分隔的字符串列表:品种如 EURUSD,XAUUSD;周期如 M5,M15,H1——名称和 MT5 终端一致,避免映射错误。这样一次运行能横扫多品种多周期,不用手动切图表。 输入参数先列四样:主要时间周期、主要周期蜡烛数、交易品种列表、时间周期列表。后续要加字段也容易扩。 每个品种×周期下,EA 要算出:看涨烛(收≥开)平均尺寸、看跌烛(收≤开)平均尺寸、全部平均尺寸;同向连续≥2 根算一个序列,给出序列平均长度;再分别统计长度为 2、3 直到 9 的序列数量。长度上限先开到 9,列表留口子,以后想加 10+ 也行。外汇与贵金属波动随机性高,这些只是历史分布画像,不预示下一步方向。

◍ 先把最简版跑起来看数据

做第一版 EA 时别纠结输入校验和存储结构,核心目标是把值算出来、能看见,再判断哪些维度真有用。输入用字符串一次性塞多个品种和周期,所以得拆成数组分别存。 时间周期维度固定为 21 个标准周期,用宏 TFN (21) 撑起二维数组第二维。品种×周期的组合值全部进二维数组,第一索引挂品种、第二索引挂周期。 品种名、周期常量、索引三者互转靠三个辅助函数:StringToTimeframe 吃字符串吐常量,TimeframeToString 吐无 PERIOD_ 前缀的名称,TimeframeToIndex 拿索引。不写这些,嵌套循环里根本定位不到数组格子。 Calculate() 里跑双层循环扫所有品种×周期,只在「该周期出了新蜡烛」时才调计算函数;EA 启动时传 force 参数强制全量算,免得空屏等 K 线。 平均值和序列长度拆到两个辅助函数,各自只处理单品种单周期,外加一个蜡烛数量参数。CalculateAvrSizes 里开盘=收盘的棒线既算涨也算跌,结果四舍五入成整数。CalculateSeries 用 100 元素的 seriesLens 数序列长度分布,只对外暴露长度小于 10 的序列数,绝大多数序列不超过 100 根蜡烛这个假设先记下,后面验证。 Show() 第一版只干两件事:Print 进终端日志、ChartComment 贴图。TextComment 负责拼文本。初始化里用 SPLIT 宏(逗号或分号都能切)把输入串拆了、建好数组,直接调计算+显示。 外汇和贵金属波动大、杠杆高,这套统计只是描述历史形态,不代表后续概率倾斜,上 MT5 跑起来先看日志再谈过滤。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Inputs                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
input group "::: Calculation period"
sinput ENUM_TIMEFRAMES mainTimeframe_   = PERIOD_D1;    class=class="str">"cmt">// Main timeframe
input class="type">int            mainLength_        = class="num">30;          class=class="str">"cmt">// Number of candles on the main timeframe
input group "::: Symbols and timeframes "
sinput class="type">class="kw">string        symbols_           = "";          class=class="str">"cmt">// Symbols(comma separated)
sinput class="type">class="kw">string        timeframes_        = "";          class=class="str">"cmt">// Timeframes(e.g. M5, H1, H4)
class=class="str">"cmt">// Number of existing timeframes
class="macro">#define TFN(class="num">21)
class=class="str">"cmt">// Global variables
class="type">class="kw">string g_symbols[];                                    class=class="str">"cmt">// Array of all used symbols
ENUM_TIMEFRAMES g_timeframes[];                        class=class="str">"cmt">// Array of all used timeframes
class=class="str">"cmt">// Arrays of calculated values.
class=class="str">"cmt">// The first index is a symbol, the second index is a timeframe
class="type">class="kw">double symbolAvrCandleSizes[][TFN];                    class=class="str">"cmt">// Array of average sizes of all candles
class="type">class="kw">double symbolAvrBuyCandleSizes[][TFN];                 class=class="str">"cmt">// Array of average sizes of bullish candles 
class="type">class="kw">double symbolAvrSellCandleSizes[][TFN];                class=class="str">"cmt">// Array of average sizes of bearish candles
class="type">class="kw">double symbolAvrSeriesLength[][TFN];                   class=class="str">"cmt">// Array of average series lengths 
class="type">int symbolCountSeries2[][TFN];                         class=class="str">"cmt">// Array of the number of series of length class="num">2
class="type">int symbolCountSeries3[][TFN];                         class=class="str">"cmt">// Array of the number of series of length class="num">3
class="type">int symbolCountSeries4[][TFN];                         class=class="str">"cmt">// Array of the number of series of length class="num">4
class="type">int symbolCountSeries5[][TFN];                         class=class="str">"cmt">// Array of the number of series of length class="num">5
class="type">int symbolCountSeries6[][TFN];                         class=class="str">"cmt">// Array of the number of series of length class="num">6

跨周期序列计数与周期名互换的实现

做多周期连涨连跌统计时,先得把 21 个内置周期统一收进一个数组。下面这段把 M1 到 MN1 全列进 tfValues[],长度固定为 21,后续所有序列长度(7/8/9 根 K)的计数都按 [品种][周期索引] 二维落表。 symbolCountSeries7/8/9 三个二维数组就是按这个索引排的,第一维通常是品种号,第二维就是 tfValues 里的位置。这样在 EA 初始化时一次性开好内存,回测里扫 20 年日线,EURUSD 的 9 连阳出现次数就直接写进 symbolCountSeries9[0][18](D1 对应索引 18)。 周期名和枚举互转是两个高频小函数。StringToTimeframe 会先掐掉下划线前的前缀再转大写,比如传入 "XAUUSD_D1" 只留 "D1" 去匹配;TimeframeToString 则反过来用 EnumToString 取 PERIOD_D1 再截尾。 TimeframeToIndex 负责把具体周期换成数组下标,找不到就返 WRONG_VALUE。实盘跑前建议先在脚本里 Print(TimeframeToIndex(PERIOD_H4)),确认吐出的是 13 再接后面的统计逻辑,外汇和贵金属波动大,周期索引错一位统计就全偏了。

MQL5 / C++
class="type">int symbolCountSeries7[][TFN];              class=class="str">"cmt">// Array of the number of series of length class="num">7
class="type">int symbolCountSeries8[][TFN];              class=class="str">"cmt">// Array of the number of series of length class="num">8
class="type">int symbolCountSeries9[][TFN];              class=class="str">"cmt">// Array of the number of series of length class="num">9
class=class="str">"cmt">// Array of all timeframes
ENUM_TIMEFRAMES tfValues[] = {
   PERIOD_M1, PERIOD_M2, PERIOD_M3, PERIOD_M4, PERIOD_M5, PERIOD_M6,
   PERIOD_M10, PERIOD_M12, PERIOD_M15, PERIOD_M20, PERIOD_M30,
   PERIOD_H1, PERIOD_H2, PERIOD_H3, PERIOD_H4, PERIOD_H6,
   PERIOD_H8, PERIOD_H12, PERIOD_D1, PERIOD_W1, PERIOD_MN1
};
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert a class="type">class="kw">string name to a timeframe                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TIMEFRAMES  StringToTimeframe(class="type">class="kw">string s) {
class=class="str">"cmt">// If the class="type">class="kw">string contains the "_" symbol, leave only the characters that follow it
   class="type">int pos = StringFind(s, "_");
   if(pos != -class="num">1) {
      s = StringSubstr(s, pos + class="num">1);
   }
class=class="str">"cmt">// Convert to uppercase
   StringToUpper(s);
class=class="str">"cmt">// Arrays of corresponding class="type">class="kw">string names of timeframes
   class="type">class="kw">string keys[] = {"M1", "M2", "M3", "M4", "M5", "M6", "M10", "M12", "M15", "M20", "M30",
                   "H1", "H2", "H3", "H4", "H6", "H8", "H12", "D1", "W1", "MN1"
                   };
class=class="str">"cmt">// Search for a match and class="kw">return it if found
   FOREACH(keys) {
      if(keys[i] == s)
         class="kw">return tfValues[i];
   }
   class="kw">return PERIOD_CURRENT;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert a timeframe to a class="type">class="kw">string name                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string TimeframeToString(ENUM_TIMEFRAMES tf) {
class=class="str">"cmt">// Get the timeframe name of the &class="macro">#x27;PERIOD_*&class="macro">#x27; type
   class="type">class="kw">string s = EnumToString(tf);
class=class="str">"cmt">// Return the part of the name after the &class="macro">#x27;_&class="macro">#x27; symbol
   class="kw">return StringSubstr(s, StringFind(s, "_") + class="num">1);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Convert a timeframe to an index in an array of all timeframes    |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int   TimeframeToIndex(ENUM_TIMEFRAMES tf) {
class=class="str">"cmt">// Search for a match and class="kw">return it if found
   FOREACH(tfValues) {
      if(tfValues[i] == tf)
         class="kw">return i;
   }
   class="kw">return WRONG_VALUE;
}
class=class="str">"cmt">//+------------------------------------------------------------------+

「多周期均值计算落点」

这段逻辑把所有监控品种和周期串成一张网:只有某品种某周期走了新一根 K 线(或强制刷新)才重算,避免每 tick 空转。 计算窗口 n 由主周期秒数乘主周期长度、再除以当前周期秒数得出。例如主周期 H1、mainLength_=100,当前周期 M5 时,n = 3600×100÷300 = 1200 根,足够覆盖等价时间跨度。 CalculateAvrSizes 里先用 CopyRates 从 1 号偏移(跳过当前未成型阳线)拉 n 根,逐根按 high-low 累加总波幅,并分 bullish / bearish 分别记 nBuy、nSell 计数。 最后用 SymbolInfoDouble 取 SYMBOL_POINT,为后续把绝对价格差转成点数做准备。外汇与贵金属杠杆高,波幅统计仅作概率参考,实盘前务必在 MT5 策略测试器跑一遍验证。

MQL5 / C++
class="type">void Calculate(class="type">bool force = false) {
   class="type">class="kw">string symbol;
   ENUM_TIMEFRAMES tf;
   FOREACH_AS(g_symbols, symbol) {
      FOREACH_AS(g_timeframes, tf) {
         if(IsNewBar(symbol, tf) || force) {
            class="type">int n = PeriodSeconds(mainTimeframe_) * mainLength_ / PeriodSeconds(tf);
            CalculateAvrSizes(symbol, tf, n);
            CalculateSeries(symbol, tf, n);
         }
      }
   }
}

class="type">void CalculateAvrSizes(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, class="type">int n) {
   class="type">int s;
   FIND(g_symbols, symbol, s);
   class="type">int t = TimeframeToIndex(tf);
   class="type">MqlRates rates[];
   class="type">int res = CopyRates(symbol, tf, class="num">1, n, rates);
   if(res == n) {
      class="type">int nBuy = class="num">0, nSell = class="num">0;
      symbolAvrCandleSizes[s][t] = class="num">0;
      symbolAvrBuyCandleSizes[s][t] = class="num">0;
      symbolAvrSellCandleSizes[s][t] = class="num">0;
      FOREACH(rates) {
         class="type">class="kw">double size = rates[i].high - rates[i].low;
         symbolAvrCandleSizes[s][t] += size;
         if(IsBuyRate(rates[i])) {
            symbolAvrBuyCandleSizes[s][t] += size;
            nBuy++;
         }
         if(IsSellRate(rates[i])) {
            symbolAvrSellCandleSizes[s][t] += size;
            nSell++;
         }
      }
      class="type">class="kw">double point = SymbolInfoDouble(symbol, SYMBOL_POINT);

常见问题

按功能拆成信息获取、周期处理和统计输出三个模块,分别建文件存放,主EA只做调度,避免所有逻辑堆在一个文件里。
在初始化时写死要监控的品种列表和最大K线读取条数,用配置数组限定范围,后面跑数据就不会超出预期内存和算力。
可以,小布能按你给的品种和周期配置直接跑诊断,把异常边界和空数据标出来,你省去手动开平台逐个核对。
用映射函数做周期字符串与内部枚举的双向转换,集中写一个工具函数调用,别在业务代码里散着硬转。
放在数据就绪后的独立计算函数里,算完存结构体再交给展示层,不要边抓序列边算均值,容易因缺数据出错。