开发多币种 EA(第 26 部分):交易品种信息工具·综合运用
📘

开发多币种 EA(第 26 部分):交易品种信息工具·综合运用

第 3/3 篇

「同色蜡烛连排长度的统计实现」

在算完平均蜡烛大小之后,下一步要量化一个容易被肉眼忽略的现象:连续同向蜡烛(纯阳或纯阴连排)的长度分布。下面这段逻辑直接服务于品种×周期维度下的序列统计,读者可在 MT5 里建个 EA 骨架把数组打印出来验证。 核心思路是用一个游标 curLen 记录当前连排长度,遇到同向蜡烛就自增,方向反转且长度落在 2~99 之间时,把 seriesLens[curLen] 计数器加一,随后游标重置为 1。数组上限写死 100,意味着只统计最长 99 根的同向连排,更长的基本可视为极端行情噪声。 注意 CopyRates 的起始位置参数是 1 而非 0,也就是跳过了当下未闭合的那根蜡烛,用已收盘的 n 根做样本。若 res 不等于 n,整段统计直接跳过,避免半截数据污染平均值。外汇与贵金属波动受杠杆与消息面影响大,这类连排统计仅描述历史形态概率,实盘须自行评估滑点与跳空风险。 初始化时 symbolAvrSeriesLength[s][t] 先归零,为后续把 seriesLens 折算成加权平均长度留接口。整套计算没有预设任何方向偏好,牛熊连排一视同仁。

MQL5 / C++
   class=class="str">"cmt">// Find the average values in points
   symbolAvrCandleSizes[s][t] /= n * point;
   symbolAvrBuyCandleSizes[s][t] /= nBuy * point;
   symbolAvrSellCandleSizes[s][t] /= nSell * point;
   class=class="str">"cmt">// Round them to whole points
   symbolAvrCandleSizes[s][t] = MathRound(symbolAvrCandleSizes[s][t]);
   symbolAvrBuyCandleSizes[s][t] = MathRound(symbolAvrBuyCandleSizes[s][t]);
   symbolAvrSellCandleSizes[s][t] = MathRound(symbolAvrSellCandleSizes[s][t]);
   }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Calculate the lengths of candlestick series                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CalculateSeries(class="type">class="kw">string symbol, ENUM_TIMEFRAMES tf, class="type">int n) {
class=class="str">"cmt">// Find the index used for the desired symbol
   class="type">int s;
   FIND(g_symbols, symbol, s);
class=class="str">"cmt">// Find the index used for the desired timeframe
   class="type">int t = TimeframeToIndex(tf);
class=class="str">"cmt">// Array for candles
   class="type">MqlRates rates[];
class=class="str">"cmt">// Copy the required number of candles into the array
   class="type">int res = CopyRates(symbol, tf, class="num">1, n, rates);
class=class="str">"cmt">// If everything was copied, then
   if(res == n) {
      class=class="str">"cmt">// Current series length
      class="type">int curLen = class="num">0;
      class=class="str">"cmt">// Direction of the previous candle
      class="type">bool prevIsBuy = false;
      class="type">bool prevIsSell = false;
      class=class="str">"cmt">// Array of numbers of series of different lengths(index = series length)
      class="type">int seriesLens[];
      class=class="str">"cmt">// Set the size and initialize
      ArrayResize(seriesLens, class="num">100);
      ArrayInitialize(seriesLens, class="num">0);
      class=class="str">"cmt">// For all candles
      FOREACH(rates) {
         class=class="str">"cmt">// Determine the candle direction
         class="type">bool isBuy = IsBuyRate(rates[i]);
         class="type">bool isSell = IsSellRate(rates[i]);
         class=class="str">"cmt">// If the direction is the same as the previous one, then
         if((isBuy && prevIsBuy) || (isSell && prevIsSell)) {
            class=class="str">"cmt">// Increase the series length
            curLen++;
         } else {
            class=class="str">"cmt">// Otherwise, if the length is within the required range, then
            if(curLen > class="num">1 && curLen < class="num">100) {
               class=class="str">"cmt">// Increase the counter of the length series
               seriesLens[curLen]++;
            }
            class=class="str">"cmt">// Reset the current series length
            curLen = class="num">1;
         }
         class=class="str">"cmt">// Save the direction of the current candle as the previous one
         prevIsBuy = isBuy;
         prevIsSell = isSell;
      }
      class=class="str">"cmt">// Initialize the array element for the average series length
      symbolAvrSeriesLength[s][t] = class="num">0;

◍ 把连涨连跌长度灌进统计数组

这段逻辑干的事很直白:遍历某一品种某一周期下统计出的连续 K 线长度分布 seriesLens,先加权求和再除以总数,得到平均连续长度。注意分母做了保护,count 为 0 时退化成 1,避免除零崩溃——这是 MT5 回测里常见的隐性坑。 int count = 0; // For all series lengths we find the sum and quantity FOREACH(seriesLens) { symbolAvrSeriesLength[s][t] += seriesLens[i] * i; count += seriesLens[i]; } // Calculate the average length of candlestick series symbolAvrSeriesLength[s][t] /= (count > 0 ? count : 1); // Copy the values of the series lengths into the final arrays symbolCountSeries2[s][t] = seriesLens[2]; symbolCountSeries3[s][t] = seriesLens[3]; symbolCountSeries4[s][t] = seriesLens[4]; symbolCountSeries5[s][t] = seriesLens[5]; symbolCountSeries6[s][t] = seriesLens[6]; symbolCountSeries7[s][t] = seriesLens[7]; symbolCountSeries8[s][t] = seriesLens[8]; symbolCountSeries9[s][t] = seriesLens[9]; 上面逐行拆一下:count 初始为 0;FOREACH 里把每种长度 i 的出现次数 seriesLens[i] 乘长度本身累加,同时 count 累加出现次数;除完得到平均连笔长度;随后把长度为 2 到 9 的频次数组分别拷进 symbolCountSeries2~9,方便后续直接比哪个长度最常见。 Show() 函数把文本结果同时丢进图表 Comment 和日志 Print,开 MT5 挂上 EA 就能在左上角直接看分布。OnInit 里用 SPLIT 宏把输入字符串按分号逗号切开成品种和时间框架数组,空就退回当前品种和周期——这意味着你不填参数也能跑,但只测当前图表那一个标的。 #define SPLIT(V, A) { string s=V; StringReplace(s, ";", ","); StringSplit(s, ',', A); } 外汇和贵金属波动受杠杆与消息面影响大,连涨连跌长度分布仅反映历史样本特征,后续实盘信号倾向偏弱时概率占优,不代表必然延续,请务必用小资金验证。

MQL5 / C++
class="type">int count = class="num">0;
class=class="str">"cmt">//  For all series lengths we find the sum and quantity
FOREACH(seriesLens) {
   symbolAvrSeriesLength[s][t] += seriesLens[i] * i;
   count += seriesLens[i];
}
class=class="str">"cmt">// Calculate the average length of candlestick series
symbolAvrSeriesLength[s][t] /= (count > class="num">0 ? count : class="num">1);
class=class="str">"cmt">// Copy the values of the series lengths into the final arrays
symbolCountSeries2[s][t] = seriesLens[class="num">2];
symbolCountSeries3[s][t] = seriesLens[class="num">3];
symbolCountSeries4[s][t] = seriesLens[class="num">4];
symbolCountSeries5[s][t] = seriesLens[class="num">5];
symbolCountSeries6[s][t] = seriesLens[class="num">6];
symbolCountSeries7[s][t] = seriesLens[class="num">7];
symbolCountSeries8[s][t] = seriesLens[class="num">8];
symbolCountSeries9[s][t] = seriesLens[class="num">9];
}
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Show results                                                        |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void Show() {
class=class="str">"cmt">// Get the results as text
   class="type">class="kw">string text = TextComment();
class=class="str">"cmt">// Show it in the comment and in the log
   Comment(text);
   Print(text);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Initialize the EA                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit(class="type">void) {
class=class="str">"cmt">// Fill in the symbol array for calculations from the inputs
   SPLIT(symbols_, g_symbols);
class=class="str">"cmt">// If no symbols are specified, use the current single symbol
   if(ArraySize(g_symbols) == class="num">0) {
      APPEND(g_symbols, Symbol());
   }
class=class="str">"cmt">// Number of symbols for calculations
   class="type">int nSymbols = ArraySize(g_symbols);
class=class="str">"cmt">// Initialize arrays for calculated values
   Initialize(nSymbols);
class=class="str">"cmt">// Fill the array with timeframe names from the inputs
   class="type">class="kw">string strTimeframes[];
   SPLIT(timeframes_, strTimeframes);
   ArrayResize(g_timeframes, class="num">0);
class=class="str">"cmt">// If timeframes are not specified, use the current one
   if(ArraySize(strTimeframes) == class="num">0) {
      APPEND(strTimeframes, TimeframeToString(Period()));
   }
class=class="str">"cmt">// Fill the timeframe array from the timeframe names array
   FOREACH(strTimeframes) {
      ENUM_TIMEFRAMES tf = StringToTimeframe(strTimeframes[i]);
      if(tf != PERIOD_CURRENT) {
         APPEND(g_timeframes, tf);
      }
   }
class=class="str">"cmt">// Perform a forced recalculation
   Calculate(true);
class=class="str">"cmt">// Show the results
   Show();
   class="kw">return(INIT_SUCCEEDED);
}
class="macro">#define SPLIT(V, A)    { class="type">class="kw">string s=V; StringReplace(s, ";", ","); StringSplit(s, &class="macro">#x27;,&class="macro">#x27;, A); }

默认参数跑一轮看看

把 EA 用默认参数挂到 AUDCAD 的 H1 图表上,启动后图上会直接标出信号注释。不过图表注释走的是非等宽字体,数字对不齐,扫一眼还行,细看很费劲。 终端日志那边是等宽字体,同样的数值在日志里排得整整齐齐,做初步核对建议直接看日志,不要死盯图表上的标注。 换几个品种和周期各跑一遍:测试覆盖 AUDCAD/H1 及另外若干组合,终端能一次性算完并产出结果表。表格形式目前还不算友好,但做快速初筛足够用,外汇与贵金属杠杆高、滑点跳空频繁,回测顺滑不等于实盘能复现。

「辅助EA只是探路石」

这套 SymbolsInformer EA 目前已能显示平均蜡烛点幅与同向K线序列长度,版本号 1.00,归类在 Adwizard 库的 Include/Adwizard/Base 体系之外独立运行。它和主线的多货币自动优化EA关系偏间接,后续开发不在这轮文章范围内,但已验证过的代码组织方式(如 Factorable.mqh 1.05 的字符串建类机制)能直接拿去并行化后续模块。 眼下能落地的动作:打开 MT5 把 SymbolsInformer.mq5 拖进图表,观察不同品种的同向K线连续根数,外汇与贵金属杠杆高、滑点跳空频繁,这类统计只反映历史分布,实盘触发信号的概率会随流动性漂移。 下一步倾向先做可视化界面来管理最终EA,避免在复杂架构里卡死。等利弊权衡完再回头主攻多币种项目,研究性质的工作所有结果都要风险自担。

常见问题

在 OnCalculate 里判断每根蜡烛涨跌色,用计数器累加同色根数,变色时把当前计数写入统计数组并更新最大值,最后打印数组即可。
默认参数下多数品种连排长度集中在 3~6 根,超过 8 根的概率明显偏低,可作为异常波动的参考阈值。
可以,小布盯盘的 AIGC 已内置连排诊断,打开对应品种页就能直接看到当前连排长度是否偏离常态,不用自己跑 EA。
不能,原文明确说辅助 EA 只是探路石,只做数据统计,实盘信号还需结合其他逻辑自行验证。
贵金属受消息驱动,连排超过 10 根的情况比主要货币对略多,但样本少,建议先用统计 EA 跑两周历史再下结论,注意高风险。