根据指定的分布法则为自定义品种的时间序列建模·综合运用
📊

根据指定的分布法则为自定义品种的时间序列建模·综合运用

(3/3)·从建品种到模拟趋势与图表形态,一套脚本让无编程基础者也能跑通全流程

含代码示例实战向 第 3/3 篇
很多人以为自定义品种只是手动改几个参数凑出 K 线,结果生成的序列毫无统计意义,回测里好看实盘全废。不按指定分布法则建模,等于用假数据骗自己的策略。

删品种前先清掉它的图表

在 MT5 里用 SymbolSelect(SName,false) 把自定义品种从市场报价栏移除后,如果还有该品种的图表开着,品种数据并不会被彻底释放,后续再建同名品种可能触发缓存冲突。 上面这段逻辑先以 CurrChart=-1 为起点用 ChartNext 遍历全部图表,只要 ChartSymbol(CurrChart)==SName 就把图表 ID 塞进 ChartIDArray,数组每命中一次就 ArrayResize 扩 1 位、i_id 自增 1。 收集完所有相关图表后,用 for 循环从 0 跑到 i_id-1,逐个 ChartClose 关闭;任意一次关闭失败就 Print 错误码并直接 return,避免半清半留的状态。外汇与贵金属品种操作涉及高风险,这类清理脚本建议在策略测试器的无实时品种环境下先跑通。

MQL5 / C++
      }
class=class="str">"cmt">//一并结束 SymbolSelect
      } class=class="str">"cmt">// if(SymbolSelect(SName,false)) 则结束
      else
         Print("Error deleting symbol ",SName,". Error code: ",GetLastError());
      }
   else
      Print("Symbol ",SName," deleted successfully");
   }
   else
      Print("Symbol ",SName," does not exist");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
            class="kw">while(CurrChart!=-class="num">1)
            {
            class=class="str">"cmt">// 循环遍历图表列表并存储标识符,来打开品种 SName 的图表
            if(ChartSymbol(CurrChart)==SName)
               {
               ArrayResize(ChartIDArray,ArraySize(ChartIDArray)+class="num">1);
               ChartIDArray[i_id]=CurrChart;
               i_id++;
               }
            CurrChart=ChartNext(CurrChart);
            }
            for(i=class="num">0;i<i_id;i++)
            {
            if(!ChartClose(ChartIDArray[i]))
               {
               Print("Error closing chart of symbol ",SName,". Error code: ",GetLastError());
               class="kw">return;
               }
            }

「用统计分布伪造分钟K线与逐笔流」

想在 MT5 里给自定义品种灌入可控的历史数据,核心思路是先定分布、再算价格。脚本 GetCandle.mq5 用 Statistics 标准库里的多种概率分布(正态、均匀、泊松等)生成伪随机数,套进公式 P(i)=Base+step*DistValue(i) 来推开盘、最高、最低、收盘、成交量与点差。Base 和 step 由你手填,第一根 K 线开盘价若 CopyClose 取不到就用基准价顶上,其后每根开盘价直接等于前一根收盘价。 逐笔层面另有 GetTick.mq5:主循环里先判周末(FiveDayOfWeek=true 时周六日时间偏移 1~2 天跳过),再用 GetTick 函数改买价卖价。RandomTickValue=true 时每个参数以 0.5 概率扰动,RBid/RAsk/RVolume 三个均匀变量决定改哪几个;flags 按变动类型累加 2/4/8/16。ValHigh、ValLow 暂存极值,供后面分钟柱的高低价使用。 实测一次跑下来新建了 ExampleCurrency 品种,共生成 33121 根分钟柱。脚本还支持 BarForReplace 局部替换并释放 MRatesMin 数组缓冲区,避免内存堆积;DiffCande=true 时同概率出普通阳线、阴线、十字星、锤头、星线、maribozu。外汇与贵金属属高风险品种,这类合成数据只宜做EA逻辑压测,不能当真实行情概率依据。 把 MaxTickInMinute 设小一点,能逼出每秒多笔的密集逐笔流;RandomTickTime=false 则每根分钟柱严格等于该变量指定的笔数。换完分布类型重跑,你就能在策略测试器里比对不同随机结构下智能交易系统的回测稳定性。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                 GetCandle.mq5 |
class=class="str">"cmt">//|                                                                 Aleksey Zinovik |
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Aleksey Zinovik"
class="macro">#class="kw">property link      ""
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">property script_show_inputs
class="macro">#include </Math/Stat/Beta.mqh>
class="macro">#include </Math/Stat/Binomial.mqh>
class="macro">#include </Math/Stat/Cauchy.mqh>
class="macro">#include </Math/Stat/ChiSquare.mqh>
class="macro">#include </Math/Stat/Exponential.mqh>
class="macro">#include </Math/Stat/F.mqh>
class="macro">#include </Math/Stat/Gamma.mqh>
class="macro">#include </Math/Stat/Geometric.mqh>
class="macro">#include </Math/Stat/Hypergeometric.mqh>
class="macro">#include </Math/Stat/Logistic.mqh>
class="macro">#include </Math/Stat/Lognormal.mqh>
class="macro">#include </Math/Stat/NegativeBinomial.mqh>
class="macro">#include </Math/Stat/NoncentralBeta.mqh>
class="macro">#include </Math/Stat/NoncentralChiSquare.mqh>
class="macro">#include </Math/Stat/NoncentralF.mqh>
class="macro">#include </Math/Stat/NoncentralT.mqh>
class="macro">#include </Math/Stat/Normal.mqh>
class="macro">#include </Math/Stat/Poisson.mqh>
class="macro">#include </Math/Stat/T.mqh>
class="macro">#include </Math/Stat/Uniform.mqh>
class="macro">#include </Math/Stat/Weibull.mqh>
class=class="str">"cmt">//+------------------------------------------------------------------+

◍ 用枚举和输入参数搭一个合成行情骨架

做行情回放或压力测试时,经常需要把一段历史 K 线替换成按统计分布生成的假数据。下面这段 MT5 脚本的输入层,先把能控制的维度全摊开:价格、真实成交量、Tick 量、点差,各自配一个分布类型与基准/扰动因子。 Distribution 枚举列出了 21 种常见分布,从 Beta、Binomial 到 Weibull 全覆盖。实盘里外汇和贵金属波动常偏厚尾,Normal 只是默认假设,真要测极端行情建议把 DistOCHL 换成 Cauchy 或 Student-T 再跑。 输入块里 TBegin 设成 2018.01.01、TEnd 设成 2018.02.01,意味着默认只重造这一个月的 1000 根柱线;BaseOCHL=1 配合 dOCHL=0.001,表示价格围绕 1 浮动、单根扰动上限约 0.1%。BaseRealVol=10000 与 dRealVol=100 则说明成交量量级在万手级、逐根抖动百手级。 点差默认走 Uniform 分布、基准 0 加步长 1,适合模拟浮动点差环境。DiffCandle=false 先关掉异形蜡烛,调参时建议先跑通正态再开。外汇贵金属属高风险品种,合成数据仅用于验证策略鲁棒性,不代表任何真实收益可能。

MQL5 / C++
enum Distribution
  {
   Beta,
   Binomial,
   Cauchy,
   ChiSquare,
   Exponential,
   F,
   Gamma,
   Geometric,
   Hypergeometric,
   Logistic,
   Lognormal,
   NegativeBinomial,
   NoncentralBeta,
   NoncentralChiSquare,
   NoncentralF,
   NoncentralT,
   Normal,
   Poisson,
   T,
   Uniform,
   Weibull
  };
class="kw">input class="type">class="kw">string SName="ExampleCurrency";
class="kw">input class="type">class="kw">datetime TBegin=D&class="macro">#x27;class="num">2018.01.class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27;;
class="kw">input class="type">class="kw">datetime TEnd=D&class="macro">#x27;class="num">2018.02.class="num">01 class="num">00:class="num">00:class="num">00&class="macro">#x27;;
class="kw">input class="type">int BarForReplace=class="num">1000;
class="kw">input class="type">class="kw">double BaseOCHL=class="num">1;
class="kw">input class="type">class="kw">double dOCHL=class="num">0.001;
class="kw">input class="type">class="kw">ulong BaseRealVol=class="num">10000;
class="kw">input class="type">class="kw">ulong dRealVol=class="num">100;
class="kw">input class="type">class="kw">ulong BaseTickVol=class="num">100;
class="kw">input class="type">class="kw">ulong dTickVol=class="num">10;
class="kw">input class="type">class="kw">ulong BaseSpread=class="num">0;
class="kw">input class="type">class="kw">ulong dSpread=class="num">1;
class="kw">input Distribution DistOCHL=Normal;
class="kw">input Distribution DistRealVol = Normal;
class="kw">input Distribution DistTickVol = Normal;
class="kw">input Distribution DistSpread = Uniform;
class="kw">input class="type">bool DiffCandle=false;
class="kw">input class="type">class="kw">double DistOCHLParam1=class="num">0;
class="kw">input class="type">class="kw">double DistOCHLParam2=class="num">1;
class="kw">input class="type">class="kw">double DistOCHLParam3=class="num">0;

分布参数与内部变量的初始化拆解

做历史逐笔报价重建时,先要把分布类输入参数和运行时变量理清,否则后面生成的 OCHL 会跑偏。下面这段声明里,实际交易量、逐笔报价交易量、点差三组分布各带 3 个参数,默认点差分布参数2设为 50,意味着基准点差量级落在 50 点附近,外汇与贵金属品种的高波动时段需警惕滑点放大风险。 DistRealParam1/2/3 默认 0/1/0,DistTickParam1/2/3 同样 0/1/0,说明这两类分布默认以均值为 0、尺度为 1 的形态生成;FiveDayOfWeek 置 true 时,脚本会跳过周末补 tick,避免伪造非交易时段流动性。 函数体里的 i_bar 作分钟柱线计数器,MRatesMin[] 存柱线,DistMass[4] 正好对应 O/C/H/L 四个生成值,BValue[1] 只拷最后收盘价。把这些变量含义对照 MT5 导航器里的脚本输入界面,改一个参数跑一次,就能看出分布形态怎么影响重绘 K 线。

MQL5 / C++
class="kw">input class="type">class="kw">double DistRealParam1=class="num">0;                 class=class="str">"cmt">// 实际交易量分布的参数1
class="kw">input class="type">class="kw">double DistRealParam2=class="num">1;                 class=class="str">"cmt">// 实际交易量分布的参数2
class="kw">input class="type">class="kw">double DistRealParam3=class="num">0;                 class=class="str">"cmt">// 实际交易量分布的参数3
class="kw">input class="type">class="kw">double DistTickParam1=class="num">0;                 class=class="str">"cmt">// 逐笔报价交易量分布的参数1
class="kw">input class="type">class="kw">double DistTickParam2=class="num">1;                 class=class="str">"cmt">// 逐笔报价交易量分布的参数2
class="kw">input class="type">class="kw">double DistTickParam3=class="num">0;                 class=class="str">"cmt">// 逐笔报价交易量分布的参数3
class="kw">input class="type">class="kw">double DistSpreadParam1=class="num">0;               class=class="str">"cmt">// 点差分布的参数1
class="kw">input class="type">class="kw">double DistSpreadParam2=class="num">50;              class=class="str">"cmt">// 点差分布的参数2
class="kw">input class="type">class="kw">double DistSpreadParam3=class="num">0;               class=class="str">"cmt">// 点差分布的参数3
class="kw">input class="type">bool FiveDayOfWeek=true;                 class=class="str">"cmt">// true - 不要在周末生成逐笔报价
class=class="str">"cmt">/*----输入参数----*/
class="type">int i_bar=class="num">0;                                            class=class="str">"cmt">// 分钟柱线的计数器
class="type">MqlRates MRatesMin[];                                  class=class="str">"cmt">// 存储柱线的数组
class="type">MqlDateTime  StructCTime;                              class=class="str">"cmt">// 数据处理的时间结构
class="type">int DistErr=class="num">0;                                          class=class="str">"cmt">// 错误编号
class="type">bool IsErr=false;                                       class=class="str">"cmt">// 错误
class="type">class="kw">double DistMass[class="num">4];                                     class=class="str">"cmt">// 存储生成的 OCHL 数值的数组
class="type">int ReplaceBar=class="num">0;                                       class=class="str">"cmt">// 替换柱线的数量
class="type">class="kw">double BValue[class="num">1];                                       class=class="str">"cmt">// 复制最后收盘价的数组
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnStart()
  {
   class="type">int i=class="num">0;                                               class=class="str">"cmt">// 循环计数器
   class="type">class="kw">double MaxVal,MinVal;                                 class=class="str">"cmt">// 最高价和最低价数值

「跳过周末的分钟级合成逻辑」

这段逻辑解决一个很实际的问题:做历史回测时,如果品种按五天交易制(FiveDayOfWeek 为真),就得把周六周日从时间循环里剔除,否则会在休市段生成无效 K 线。代码里用 TimeToStruct 取出 day_of_week,0 代表周日、6 代表周六,遇上周末就把 TCurrent 直接往前推 1~2 个自然日(86400 秒或 2*86400 秒)并对齐到零点。 若跳过后 TCurrent 已经超过回测终点 TEnd,且 ReplaceBar 为 0,就打印“No trades in the specified range”并直接 return,避免空跑。外汇与贵金属属高杠杆品种,这种时间边界处理错了,回测样本偏差会很大。 每个新分钟 bar 先用 ArrayResize 扩数组,open 价优先取上一根 close;首根若取不到前 1 分钟收盘价(CopyClose 返回 -1),就用 BaseOCHL 叠加随机扰动 GetDist 来近似。最高最低价的初值用了 double 极限值(约 2.225e-308 与 1.798e+308),再用 3 次循环填充 DistMass 候选价,这是典型的极值初始化套路,开 MT5 把这段塞进 EA 初始化函数就能验证。

MQL5 / C++
  class="type">int i_max,i_min;                                                                     class=class="str">"cmt">// DistMass 数组中最大值和最小值的索引
  class="type">class="kw">datetime TCurrent=TBegin;
  BValue[class="num">0]=BaseOCHL;
  if(SymbolInfoInteger(SName,SYMBOL_CUSTOM))class=class="str">"cmt">// 如果品种存在
    {
      class="kw">while(TCurrent<=TEnd)
        {
         if(FiveDayOfWeek)
           {
            TimeToStruct(TCurrent,StructCTime);
            if(!((StructCTime.day_of_week!=class="num">0) && (StructCTime.day_of_week!=class="num">6)))
              {
               if(StructCTime.day_of_week==class="num">0)
                  TCurrent=TCurrent+class="num">86400-(StructCTime.hour*class="num">3600+StructCTime.min*class="num">60+StructCTime.sec);
               else
                  TCurrent=TCurrent+class="num">2*class="num">86400-(StructCTime.hour*class="num">3600+StructCTime.min*class="num">60+StructCTime.sec);
               if(TCurrent>=TEnd)
                 {
                  if(ReplaceBar==class="num">0)
                     Print("No trades in the specified range");
                  class="kw">return;
                 }
              }
           }
         ArrayResize(MRatesMin,ArraySize(MRatesMin)+class="num">1);
         MRatesMin[i_bar].open=class="num">0;
         MRatesMin[i_bar].close=class="num">0;
         MRatesMin[i_bar].high=class="num">0;
         MRatesMin[i_bar].low=class="num">0;
         class=class="str">"cmt">// 填充开盘价      
         if(i_bar>class="num">0)
            MRatesMin[i_bar].open=MRatesMin[i_bar-class="num">1].close;
         else
           {
            if((CopyClose(SName,PERIOD_M1,TCurrent-class="num">60,class="num">1,BValue)==-class="num">1))
               MRatesMin[i_bar].open=NormalizeDouble(BaseOCHL+dOCHL*GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3),_Digits);
            else
               MRatesMin[i_bar].open=BValue[class="num">0];
           }
         class=class="str">"cmt">// 生成最高价, 最低价
         MaxVal=class="num">2.2250738585072014e-308;
         MinVal=class="num">1.7976931348623158e+308;
         i_max=class="num">0;
         i_min=class="num">0;
         for(i=class="num">0;i<class="num">3;i++)
           {
            DistMass[i]=NormalizeDouble(BaseOCHL+dOCHL*GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3),_Digits);

◍ 把分布值落进分钟K线的收线逻辑

这段逻辑干的事,是把随机分布算出来的价格样本塞进一根分钟 K 线的 OHLC 与量仓字段。先扫一遍 DistMass 数组找最大最小值,记下位置 i_max 和 i_min,这两个位置后面不能拿来当收盘价。 最高价取 MaxVal 与开盘价的较大者,最低价取 MinVal 与开盘价的较小者;若 MaxVal 比开盘还低,那 high 直接等于 open,说明这根棒子里价格全在开盘下方游走。外汇与贵金属分钟线常出现这种“开盘即极值”的毛刺,模拟时得保留。 收盘价从剩下那个既不是最高也不是最低的分布点里取,循环里一命中就 break,保证只填一次。real_volume、tick_volume、spread 三行各自套一层 GetDist 随机扰动,Base 打底、d 系数缩放,每次生成后都跑 IsErrCheck(DistErr) 拦截分布错误,任一步出错直接 return 不写盘。 若 DiffCandle 开关打开,用 MathRand()%5 抽 0~4 决定变形类型;抽中 0 就把 close 拉回 open 做十字星,抽中 1 且开盘高于收盘时把 high 压成 open 模拟锤头。这类随机形态注入能让回测样本更接近真实影线分布,但贵金属高杠杆下模拟≠实盘,验证时请先在 MT5 策略测试器跑小周期。

MQL5 / C++
if(IsErrCheck(DistErr)) class="kw">return;
if(MaxVal<DistMass[i])
  {
   MaxVal=DistMass[i];
   i_max=i;
  }
if(MinVal>DistMass[i])
  {
   MinVal=DistMass[i];
   i_min=i;
  }
 }
if(MaxVal<MRatesMin[i_bar].open)
   MRatesMin[i_bar].high=MRatesMin[i_bar].open;
else
   MRatesMin[i_bar].high=MaxVal;
if(MinVal>MRatesMin[i_bar].open)
   MRatesMin[i_bar].low=MRatesMin[i_bar].open;
else
   MRatesMin[i_bar].low=MinVal;
class=class="str">"cmt">// 填充收盘价
for(i=class="num">0;i<class="num">3;i++)
 if((i!=i_max) && (i!=i_min))
   {
    MRatesMin[i_bar].close=DistMass[i];
    class="kw">break;
   }
class=class="str">"cmt">// 生成交易量, 点差
MRatesMin[i_bar].real_volume=(class="type">long)(BaseRealVol+dRealVol*GetDist(DistRealVol,DistRealParam1,DistRealParam2,DistRealParam3));
if(IsErrCheck(DistErr)) class="kw">return;
MRatesMin[i_bar].tick_volume=(class="type">long)(BaseTickVol+dTickVol*GetDist(DistTickVol,DistTickParam1,DistTickParam2,DistTickParam3));
if(IsErrCheck(DistErr)) class="kw">return;
MRatesMin[i_bar].spread=(class="type">int)(BaseSpread+dSpread*GetDist(DistSpread,DistSpreadParam1,DistSpreadParam2,DistSpreadParam3));
if(IsErrCheck(DistErr)) class="kw">return;
class=class="str">"cmt">// 存储时间
MRatesMin[i_bar].time=TCurrent;
if(DiffCandle)
  {
   i=MathRand()%class="num">5;
   class="kw">switch(i)
     {
      case class="num">0:class=class="str">"cmt">// 十字星
       {
        MRatesMin[i_bar].close=MRatesMin[i_bar].open;
        class="kw">break;
       }
      case class="num">1:class=class="str">"cmt">// 锤头
       {
        if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
          MRatesMin[i_bar].high=MRatesMin[i_bar].open;
       }
     }
  }

K线重绘里的三种形态分支

这段逻辑跑在分钟级数组 MRatesMin 上,按形态编号对单根 bar 的 high/low 做强制重算。case 1 处理的是带上下影但实体偏多头的情形:若 open 低于 close,直接把 high 拉到 close,相当于抹掉上影线以上的无效高点。 case 2 标注为「星」,逻辑反过来——阴线时 low 落到 close,阳线时 low 落到 open,重心明显压向实体下沿。case 3 是 Maribozu(光头光脚),无论多空都把 high/low 钉在 open 与 close 两端,彻底清掉影线。 尾部有个硬条件:i_bar>=BarForReplace-1 才触发 ReplaceHistory,把重算后的区间从首根时间写回历史。外汇与贵金属波动跳空频繁,这种重绘会改写已收盘 bar 的极值,回测和实盘可能不一致,属高风险操作,上 MT5 前先把 BarForReplace 调小到 3~5 根验证偏移量。

MQL5 / C++
else
              {
               if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
                  MRatesMin[i_bar].high=MRatesMin[i_bar].close;
              }
              class="kw">break;
             }
             case class="num">2:class=class="str">"cmt">// 星
             {
               if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
                  MRatesMin[i_bar].low=MRatesMin[i_bar].close;
               else
               {
                if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
                  MRatesMin[i_bar].low=MRatesMin[i_bar].open;
               }
              class="kw">break;
             }
             case class="num">3:class=class="str">"cmt">// Maribozu
             {
               if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
               {
                MRatesMin[i_bar].high=MRatesMin[i_bar].open;
                MRatesMin[i_bar].low=MRatesMin[i_bar].close;
               }
               else
               {
                if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
                  {
                   MRatesMin[i_bar].high=MRatesMin[i_bar].close;
                   MRatesMin[i_bar].low=MRatesMin[i_bar].open;
                  }
               }
              class="kw">break;
             }
             class="kw">default: class="kw">break;
            }
         }
      class=class="str">"cmt">// 检查替换柱线的时机  
      if(i_bar>=BarForReplace-class="num">1)
       {
        ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar].time);

「用随机分布给历史补帧的代码骨架」

这段逻辑干的事,是把分钟级缓存按 60 秒步长往后推,凑齐一段自定义历史后再整体写回。TCurrent 每次累加 60,意味着时间轴以 1 分钟为粒度向前爬;当 i_bar 归零且数组释放完成,这一段合成行情就被交付给后续替换函数。 ReplaceHistory 里直接调 CustomRatesReplace,返回负值就打印错误码,否则用 PrintFormat 报出生成周期与创建、替换的 bar 数量——这里 i_bar+1 就是实际生成的 bar 数,跑完能在 MT5 Experts 日志里直接核对。 GetDist 是个分发器,按 Distribution 枚举把不同随机数发生器兜进去。Beta 分布吃两个形状参数 p1、p2;Binomial 要测试次数加单次成功概率;Cauchy 用位置与比例系数;ChiSquare 只认自由度 p1。外汇与贵金属合成数据仅用于回测环境验证,实盘接入属高风险操作,参数错配可能导致历史重构失真。 后面还有 Exponential(lambda)、F(双自由度)、Gamma(整数参数加比例)、Geometric(成功概率)等分支,调用约定都写在注释里,复制时别把 p1/p2/p3 的顺序弄反。

MQL5 / C++
TCurrent=TCurrent+class="num">60;
BValue[class="num">0]=MRatesMin[i_bar].close;
i_bar=class="num">0;
ArrayFree(MRatesMin);
 }
 else
  {
  i_bar++;
  TCurrent=TCurrent+class="num">60;
  }
 }
if(i_bar>class="num">0)
 {
  i_bar--;
  ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar].time);
 }
 }
 else
   Print("Symbol ",SName," does not exist");
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void ReplaceHistory(class="type">class="kw">datetime DBegin,class="type">class="kw">datetime DEnd)
  {
  ReplaceBar=CustomRatesReplace(SName,DBegin,DEnd,MRatesMin);
  if(ReplaceBar<class="num">0)
    Print("Error replacing bars. Error code: ",GetLastError());
  else
    PrintFormat("Price history for period: %s to %s generated successfully. Created %i bars, added(replaced) %i bars",TimeToString(DBegin),TimeToString(DEnd),i_bar+class="num">1,ReplaceBar);
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">double GetDist(Distribution d,class="type">class="kw">double p1,class="type">class="kw">double p2,class="type">class="kw">double p3)
  {
  class="type">class="kw">double res=class="num">0;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
  class="kw">switch(d)
   {
class=class="str">"cmt">/*Beta 分布*/
   class=class="str">"cmt">//p1,p2 - 第一和第二个参数 
   case Beta: {res=MathRandomBeta(p1,p2,DistErr); class="kw">break;}
class=class="str">"cmt">/*二项分布*/
   class=class="str">"cmt">//p1 - 测试次数, p2 - 每次测试成功的概率    
   case Binomial: {res=MathRandomBinomial(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*柯西分布*/
   class=class="str">"cmt">//p1 - 位置系数, p2 - 比例系数
   case Cauchy: {res=MathRandomCauchy(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*卡方分布*/
   class=class="str">"cmt">//p1 - 自由度
   case ChiSquare: {res=MathRandomChiSquare(p1,DistErr); class="kw">break;};
class=class="str">"cmt">/*指数分布*/
   class=class="str">"cmt">//p1 - 分布的参数(lambda) 
   case Exponential: {res=MathRandomExponential(p1,DistErr); class="kw">break;};
class=class="str">"cmt">/*费舍尔分布*/
   class=class="str">"cmt">//p1, p2 - 自由度
   case F: {res=MathRandomF(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*Gamma 分布*/
   class=class="str">"cmt">//p1 - 分布参数(整数), p2 - 比例系数
   case Gamma: {res=MathRandomGamma(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*几何分布*/
   class=class="str">"cmt">//p1 - 成功概率(在测试中事件发生)
   case Geometric: {res=MathRandomGeometric(p1,DistErr); class="kw">break;};
class=class="str">"cmt">/*超几何分布*/
   class=class="str">"cmt">//p1 - 对象总数, p2 - 具有所需特征的对象数量, p3 - 样本中的对象数量

◍ 离散与连续分布的随机分支实现

在 MT5 的随机函数封装里,不同概率分布通过 switch-case 分流到对应的 MathRandomXxx 接口,每个分布对参数含义有硬性约定。比如 Logistic 吃的是期望值 p1 与比例系数 p2,Lognormal 要的是预期值的对数和标准差的对数,NegativeBinomial 则要求传入成功测试次数与单次成功概率。 非中心族分布(Beta / ChiSquare / F / T)比普通版本多一个非中心参数 p3,用来刻画均值偏移,做蒙特卡洛压力测试时这类分布比标准正态更贴近跳空后的真实残差。Normal 只认期望与标准差,Poisson 仅吃期望值,Uniform 取上下限——写 EA 前务必核对注释里的 p1/p2 语义,错填会直接触发 DistErr 非零。 函数尾部用 if(DistErr!=0) return -1 把异常统一拦截;IsErrCheck 再把错误码翻译成弹窗:1=参数非实数,2=参数无效,4=除零。实盘跑分布采样前先挂这个检查,能少踩一半静默崩坑。外汇与贵金属波动带杠杆,任何采样偏差都可能放大成异常下单,验证代码时请在策略测试器小批量跑。

MQL5 / C++
case Hypergeometric: {res=MathRandomHypergeometric(p1,p2,p3,DistErr); class="kw">break;};
class=class="str">"cmt">/*逻辑分布*/
class=class="str">"cmt">//p1 - 期望值, p2 - 比例系数 
case Logistic: {res=MathRandomLogistic(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*对数正态分布*/
class=class="str">"cmt">//p1 - 预期值的对数, p2 - 标准差的对数
case Lognormal: {res=MathRandomLognormal(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*负二项分布*/
class=class="str">"cmt">//p1 - 成功测试的次数, p2 - 成功的概率  
case NegativeBinomial: {res=MathRandomNegativeBinomial(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*非中心 Beta 分布*/
class=class="str">"cmt">//p1,p2 - 第一和第二个参数, p3 - 非中心参数      
case NoncentralBeta: {res=MathRandomNoncentralBeta(p1,p2,p3,DistErr); class="kw">break;};
class=class="str">"cmt">/*非中心卡方分布*/
class=class="str">"cmt">//p1 - 自由度, p2 - 非中心参数
case NoncentralChiSquare: {res=MathRandomNoncentralChiSquare(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*非中心 F-分布*/
class=class="str">"cmt">//p1, p2 - 自由度, p3 - 非中心参数
case NoncentralF: {res=MathRandomNoncentralF(p1,p2,p3,DistErr); class="kw">break;};
class=class="str">"cmt">/*非中心 t-分布*/
class=class="str">"cmt">//p1 - 自由度, p2 - 非中心参数
case NoncentralT: {res=MathRandomNoncentralT(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*正态分布*/
class=class="str">"cmt">//p1 - 期望值, p2 - 标准差
case Normal: {res=MathRandomNormal(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*泊松分布*/
class=class="str">"cmt">//p1 - 期望值
case Poisson: {res=MathRandomPoisson(p1,DistErr); class="kw">break;};
class=class="str">"cmt">/*学生 t-分布*/
class=class="str">"cmt">//p1 - 自由度
case T: {res=MathRandomT(p1,DistErr); class="kw">break;};
class=class="str">"cmt">/*统一分布*/
class=class="str">"cmt">//p1 - 范围的下限, p2 - 范围的上限
case Uniform: {res=MathRandomUniform(p1,p2,DistErr); class="kw">break;};
class=class="str">"cmt">/*威布尔分布*/
class=class="str">"cmt">//p1 - 形状参数, p2 - 比例参数
case Weibull: {res=MathRandomWeibull(p1,p2,DistErr); class="kw">break;};
   }
   if(DistErr!=class="num">0)
      class="kw">return -class="num">1;
   else
      class="kw">return res;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool IsErrCheck(class="type">int Err)
  {
class=class="str">"cmt">// 生成伪随机数时检查错误
   class="kw">switch(DistErr)
     {
      case(class="num">1):
        {
         MessageBox("Specified distribution parameters are not real numbers","Input parameters error",MB_ICONWARNING);
         class="kw">return true;
        }
      case(class="num">2):
        {
         MessageBox("Specified distribution parameters are invalid","Input parameters error",MB_ICONWARNING);
         class="kw">return true;
        }
      case(class="num">4):
        {
         MessageBox("Zero divide error","Input parameters error",MB_ICONWARNING);
         class="kw">return true;
        }
     }
   class="kw">return false;
  }

合成分钟K线的开盘跳空与周末断点处理

在把 M1 数据重建成自定义分钟周期时,第一根 bar 的开盘价不能简单置零。若 i_bar>0,直接取前一根的 close 作衔接;若 i_bar==0 且取不到 60 秒前的 close(CopyClose 返回 -1),就用 BaseOCHL 加扰动函数 Normalized 到当前品种精度,否则用 BValue[0] 实值。 开启 FiveDayOfWeek 后,代码用 TimeToStruct 读星期:day_of_week 为 0(周日)或 6(周六)会被跳过。周日把时间推到下一交易日 00:00(加 86400 减当日已过秒数),周六则加 2*86400。若推进后超过 TEnd 且 ReplaceBar==0,会 Print("No trades in the specified range") 并 return——这段逻辑能避免在非交易时段伪造出连续阳线。 当 i_bar 累积到 BarForReplace,调用 ReplaceHistory 把 [MRatesMin[0].time, MRatesMin[i_bar-1].time] 区间写回历史,随后 i_bar 归零并 ArrayFree 释放数组,防止内存随回放长度线性膨胀。 DiffCandle 开启时,用 MathRand()%5 随机抽形态:case 0 把 close 强制等于 open 合成十字星,case 1 进入锤头分支。这种随机注入对做历史重放压力测试有用,但外汇/贵金属杠杆高、跳空频繁,实盘直接套用合成形态可能严重偏离真实滑点。

MQL5 / C++
if(i_bar>class="num">0)
      MRatesMin[i_bar].open=MRatesMin[i_bar-class="num">1].close;
   else
      {
      if((CopyClose(SName,PERIOD_M1,TCurrent-class="num">60,class="num">1,BValue)==-class="num">1))
         MRatesMin[i_bar].open=NormalizeDouble(BaseOCHL+dOCHL*GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3),_Digits);
      else
         MRatesMin[i_bar].open=BValue[class="num">0];
      }
if(FiveDayOfWeek)
      {
      TimeToStruct(TCurrent,StructCTime);
      if(!((StructCTime.day_of_week!=class="num">0) && (StructCTime.day_of_week!=class="num">6)))
         {
         if(StructCTime.day_of_week==class="num">0)
            TCurrent=TCurrent+class="num">86400-(StructCTime.hour*class="num">3600+StructCTime.min*class="num">60+StructCTime.sec);
         else
            TCurrent=TCurrent+class="num">2*class="num">86400-(StructCTime.hour*class="num">3600+StructCTime.min*class="num">60+StructCTime.sec);
         if(TCurrent>=TEnd)
            {
            if(ReplaceBar==class="num">0)
               Print("No trades in the specified range");
            class="kw">return;
            }
         }
      }
if(i_bar>=BarForReplace)
      {
      ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar-class="num">1].time);
      i_bar=class="num">0;
      ArrayFree(MRatesMin);
      }
if(DiffCandle)
      {
      i=MathRand()%class="num">5;
      class="kw">switch(i)
         {
         case class="num">0:class=class="str">"cmt">// 十字星
            {
            MRatesMin[i_bar].close=MRatesMin[i_bar].open;
            class="kw">break;
            }
         case class="num">1:class=class="str">"cmt">// 锤头
            {

「按形态重写K线极值」

这段分支逻辑干的事很直接:根据蜡烛分类代号,把 MRatesMin[i_bar] 的 high / low 强制改写成实体边界,相当于在分钟级数组里重画影线。 case 1 处理的是带上线影的阴线或阳线:若开盘高于收盘(阴线),high 被设为 open;若开盘低于收盘(阳线),high 被设为 close。也就是说,这根 K 线的上影线被抹平,最高价只认实体顶端。 case 2 是「星」形态,逻辑镜像:阴线时 low 取 close,阳线时 low 取 open,下影线被吃掉的。case 3 的 Maribozu(光头光脚)更狠,阴线直接 high=open、low=close,阳线则 high=close、low=open,上下影线全无。 在 MT5 里跑这段前,先确认 MRatesMin 已经用 CopyRates 填好且 i_bar 有效;改完极值后若拿去算 ATR 或画指标,原影线信息就丢了,外汇与贵金属波动大,这种重写会掩盖真实扫单路径,仅适合特定形态统计。

MQL5 / C++
if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
      MRatesMin[i_bar].high=MRatesMin[i_bar].open;
      else
      {
       if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
         MRatesMin[i_bar].high=MRatesMin[i_bar].close;
      }
      class="kw">break;
     }
     case class="num">2:class=class="str">"cmt">// 星
     {
      if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
       MRatesMin[i_bar].low=MRatesMin[i_bar].close;
      else
      {
       if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
         MRatesMin[i_bar].low=MRatesMin[i_bar].open;
      }
      class="kw">break;
     }
     case class="num">3:class=class="str">"cmt">// Maribozu
     {
      if(MRatesMin[i_bar].open>MRatesMin[i_bar].close)
      {
       MRatesMin[i_bar].high=MRatesMin[i_bar].open;
       MRatesMin[i_bar].low=MRatesMin[i_bar].close;
      }
      else
      {
       if(MRatesMin[i_bar].open<MRatesMin[i_bar].close)
         {
          MRatesMin[i_bar].high=MRatesMin[i_bar].close;
          MRatesMin[i_bar].low=MRatesMin[i_bar].open;
         }
      }
      class="kw">break;
     }
     class="kw">default: class="kw">break;
      }
     }
class="type">void OnStart()
  {

◍ 自定义品种跳空日的逐笔报价跳过逻辑

在生成自定义品种的逐笔报价时,若开启了 FiveDayOfWeek 开关,代码会先判断当前时间是否落在周日或周六。MT5 里 TimeToStruct 返回的 day_of_week 中,0 代表周日、6 代表周六,这两个值直接作为过滤条件。 遇到周日时,TCurrent 往后推 86400 秒(1 天),对应的毫秒计数 TBeginMSec 加 86400000;遇到周六则一次跳过 2 天(172800 秒 / 172800000 毫秒),因为周六之后紧接周一,中间无交易时段。跳过后若已越过结束时间 TEndMSec,直接 break 退出循环,避免无意义计算。 这段处理对外汇与贵金属自定义回测数据很关键:真实市场周末无报价,若不强跳,生成的 tick 会塞满非交易时段,扭曲点差与成交量分布。开 MT5 把 FiveDayOfWeek 设为 true 跑一遍,对比关闭时的 tick 数量,差异可能达到总时长 2/7 的量级。

MQL5 / C++
if(SymbolInfoInteger(SName,SYMBOL_CUSTOM))class=class="str">"cmt">// 如果品种存在
    {
      class="type">MqlDateTime  StructCTime;
      class="type">long TBeginMSec=(class="type">long)TBegin*class="num">1000;        class=class="str">"cmt">// 生成逐笔报价的开始时间,单位为毫秒
      class="type">long TEndMSec=(class="type">long)TEnd*class="num">1000;            class=class="str">"cmt">// 生成逐笔报价的结束时间,单位为毫秒
      class="type">int ValMsec=class="num">0;                            class=class="str">"cmt">// 生成随机时间偏移的变量,单位为毫秒
      class="type">int SumSec=class="num">0;                             class=class="str">"cmt">// 秒数计数器
      class="type">int SumMSec=class="num">0;                            class=class="str">"cmt">// 毫秒计数器
      class="type">int PrevTickCount=class="num">0;                      class=class="str">"cmt">// 保存一分钟内先前逐笔报价次数的变量
      class="type">class="kw">datetime TCurrent=TBegin;
      class="type">bool  NewMinute=false;
      class=class="str">"cmt">// 从逐笔报价生成开始时间复制价格数值
      if(CopyClose(SName,PERIOD_M1,TCurrent-class="num">60,class="num">1,BValue)==-class="num">1)
         BValue[class="num">0]=Base;
      class=class="str">"cmt">// 填充 LastTick 结构
      LastTick.ask=BValue[class="num">0];
      LastTick.bid=BValue[class="num">0];
      LastTick.last=BValue[class="num">0];
      LastTick.volume=baseVol;
      class="kw">while(TBeginMSec<=TEndMSec)
        {
         if(FiveDayOfWeek)
           {
            TimeToStruct(TCurrent,StructCTime);
            if((StructCTime.day_of_week==class="num">0) || (StructCTime.day_of_week==class="num">6))
              {
               if(StructCTime.day_of_week==class="num">0)
                 {
                  TCurrent=TCurrent+class="num">86400;
                  TBeginMSec=TBeginMSec+class="num">86400000;
                 }
               else
                 {
                  TCurrent=TCurrent+class="num">2*class="num">86400;
                  TBeginMSec=TBeginMSec+class="num">2*class="num">86400000;
                 }
               if(TBeginMSec>=TEndMSec)
                  class="kw">break;
              }
           }
         GetTick(TCurrent,TBeginMSec);
         if(IsErrCheck(DistErr)) class="kw">return;
         i_tick++;
        }

随机偏移下的逐笔报价时间推进

在回测引擎里模拟真实报价流,常需要在均匀节奏上叠加随机扰动。开启 RandomTickTime 后,每一笔的毫秒偏移由 MathRand()%30000 除以 (MaxTickInMinute*0.25) 再加 1 得到,意味着单笔抖动上限约 30000 毫秒,实际步长受每分钟最大 tick 数约束。 当累计 tick 数超过 MaxTickInMinute 时,代码把当前分钟秒数清零并跳到下一分钟整点:TCurrent 被重置为 StructToTime(StructCTime)+60,同时 TBeginMSec 补偿 60000-SumSec+ValMsec,SumSec 与 SumMSec 归零并置 NewMinute=true。这一分支保证跨分钟边界不被随机量拖垮。 若未跨分钟,则按 SumSec 是否过 60000 做模运算归零,并持续累加 TBeginMSec。SumMSec 每到 1000 就折算进 TCurrent 的秒数,避免毫秒计数器溢出。关闭 RandomTickTime 时走 else 分支,直接以 60000/MaxTickInMinute 为固定步长推进,无随机性。 把这段代码贴进 MT5 的 EA 回测预处理函数,调一下 MaxTickInMinute 从 30 到 200,能直观看到报价密度变化对时间轴的影响;外汇与贵金属杠杆品种跳空频繁,此类模拟仅降低拟合偏差,实盘仍属高风险。

MQL5 / C++
if(RandomTickTime)
  {
   class=class="str">"cmt">// 生成随机时间偏移
   
   ValMsec=(class="type">int)((MathRand()%class="num">30000)/(MaxTickInMinute*class="num">0.25)+class="num">1);
   SumSec=SumSec+ValMsec;
   SumMSec=SumMSec+ValMsec;
   if(i_tick-PrevTickCount>=MaxTickInMinute)
     {
      TimeToStruct(TCurrent,StructCTime);
      StructCTime.sec=class="num">0;
      TCurrent=StructToTime(StructCTime)+class="num">60;
      TBeginMSec=TBeginMSec+class="num">60000-SumSec+ValMsec;
      SumSec=class="num">0;
      SumMSec=class="num">0;
      NewMinute=true;
     }
   else
     {
      if(SumSec>=class="num">60000)
        {
         class=class="str">"cmt">// 将每分钟逐笔报价的计数器归零
         SumSec=SumSec-class="num">60000*(SumSec/class="num">60000);
         NewMinute=true;
        }
      class=class="str">"cmt">// 生成新的逐笔报价时间   
      TBeginMSec=TBeginMSec+ValMsec;
      if(SumMSec>=class="num">1000)
        {
         TCurrent=TCurrent+SumMSec/class="num">1000;
         SumMSec=SumMSec-class="num">1000*(SumMSec/class="num">1000);
        }
     }
  }
else
  {
   TBeginMSec=TBeginMSec+class="num">60000/MaxTickInMinute;
   SumSec=SumSec+class="num">60000/MaxTickInMinute;
   SumMSec=SumMSec+class="num">60000/MaxTickInMinute;
   if(SumMSec>=class="num">1000)
     {
      TCurrent=TCurrent+SumMSec/class="num">1000;
      SumMSec=SumMSec-class="num">1000*(SumMSec/class="num">1000);
     }
   if(SumSec>=class="num">60000)
     {
      SumSec=SumSec-class="num">60000*(SumSec/class="num">60000);

「逐笔合成分钟线的开盘与极值判定」

检测到 NewMinute 为真时,说明刚跨过一分钟边界,此时要把上一分钟聚合好的数据落进 MRatesMin 数组。数组先用 ArrayResize 扩 1 个单元,i_bar 指向当前待写入位置。 首根分钟线(数组长度为 1)的 open 直接取 LastTick.bid 并 NormalizeDouble 到 _Digits 精度,tick_volume 就是累计 tick 数 i_tick;非首根则用上一分钟末尾的 bid 做开盘,tick_volume 等于 i_tick 减去 PrevTickCount,也就是这一分钟内新增的 tick 量。 close 取本分钟最后一个 tick 的 bid。high 和 low 的判定很直白:若 ValHigh 大于 open 才采用 ValHigh,否则 high 回退为 open;ValLow 小于 open 才采用,否则 low 等于 open。这样能保证即使一分钟内没出新极值,OHL 也不会失真。 real_volume 取末 tick 的成交量,spread 用 abs(bid-ask) 除以 _Point 再四舍五入成 int 点差。时间方面把末 tick 的 time 拆结构后秒置 0,再拼回分钟起始时间。写完 i_bar 自增、PrevTickCount 更新、ValHigh/ValLow 复位到 double 极限值(约 2.225e-308 与 1.798e+308),NewMinute 置否。 当 i_bar 达到 BarForReplace 阈值,就调用 ReplaceHistory 把最早到最新的分钟区间写回历史,并把 LastTick 的 bid/ask 同步为最新 tick,避免下一轮引用到旧报价。外汇与贵金属品种点差跳变频繁,这种按 tick 重建的分钟线在高波动时段可能和券商聚合的 1 分钟 K 线出现几点偏差,开 MT5 用脚本打印两者 high/low 对比即可验证。

MQL5 / C++
NewMinute=true;
            }
          }
        if(NewMinute)
          {
           class=class="str">"cmt">// 将新柱线添加到数组中 
           ArrayResize(MRatesMin,ArraySize(MRatesMin)+class="num">1);
           if(ArraySize(MRatesMin)==class="num">1)class=class="str">"cmt">// 如果是第一分钟
             {
              MRatesMin[i_bar].open=NormalizeDouble(LastTick.bid,_Digits);
              MRatesMin[i_bar].tick_volume=(class="type">long)i_tick;
             }
           else
             {
              MRatesMin[i_bar].open=NormalizeDouble(MTick[PrevTickCount-class="num">1].bid,_Digits);
              MRatesMin[i_bar].tick_volume=(class="type">long)i_tick-PrevTickCount;
             }
           MRatesMin[i_bar].close=NormalizeDouble(MTick[i_tick-class="num">1].bid,_Digits);
           if(ValHigh>MRatesMin[i_bar].open)
             MRatesMin[i_bar].high=NormalizeDouble(ValHigh,_Digits);
           else
             MRatesMin[i_bar].high=MRatesMin[i_bar].open;
           if(ValLow<MRatesMin[i_bar].open)
             MRatesMin[i_bar].low=NormalizeDouble(ValLow,_Digits);
           else
             MRatesMin[i_bar].low=MRatesMin[i_bar].open;
           MRatesMin[i_bar].real_volume=(class="type">long)MTick[i_tick-class="num">1].volume;
           MRatesMin[i_bar].spread=(class="type">int)MathRound(MathAbs(MTick[i_tick-class="num">1].bid-MTick[i_tick-class="num">1].ask)/_Point);
           TimeToStruct(MTick[i_tick-class="num">1].time,StructCTime);
           StructCTime.sec=class="num">0;
           MRatesMin[i_bar].time=StructToTime(StructCTime);
           i_bar++;
           PrevTickCount=i_tick;
           ValHigh=class="num">2.2250738585072014e-308;
           ValLow=class="num">1.7976931348623158e+308;
           NewMinute=false;
           if(i_bar>=BarForReplace)
             {
              ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar-class="num">1].time);
              LastTick.bid=MTick[i_tick-class="num">1].bid;
              LastTick.ask=MTick[i_tick-class="num">1].ask;

◍ 逐笔报价回填与随机扰动的内部机制

这段逻辑干的事很直接:在合成历史 tick 时,先把上一根已处理 tick 的 last 与 volume 暂存进 LastTick,再把 i_tick、i_bar、PrevTickCount 归零,并释放 MRatesMin 与 MTick 两块动态数组,防止跨周期残留。 GetTick() 是真正写数组的地方。每次调用先 ArrayResize 把 MTick 扩 1,写入 time 与 time_msc;若数组长度大于 1,就用前一根 tick 的 ask/bid/volume/last 填当前帧,否则回退到 LastTick 里的全局暂存值。 当 RandomTickValue 开启,报价不再只是照搬前帧,而是用 MathRandomUniform(0,1) 抽三个独立随机数。RBid≥0.5 时才动 bid,并按 Base+GetDist(...)*dStep 重算,同时把 last 对齐 bid、flags 置 10;RAsk、RVolume 同理叠加 4 和 16 的标记位。 外汇与贵金属品种用这套随机合成做回测,价差与成交量分布可能偏离真实盘口,实盘前务必在 MT5 用已知样本比对一下分布,高风险。

MQL5 / C++
LastTick.last=MTick[i_tick-class="num">1].last;
LastTick.volume=MTick[i_tick-class="num">1].volume;
i_tick=class="num">0;
i_bar=class="num">0;
PrevTickCount=class="num">0;
ArrayFree(MRatesMin);
ArrayFree(MTick);
}
}
}class=class="str">"cmt">//循环结束
if(i_bar>class="num">0)
 ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar-class="num">1].time);
 }
 else
 Print("Symbol ",SName," does not exist");
}
class="kw">while(TBeginMSec<=TEndMSec)
{
...
}
if(FiveDayOfWeek)
{
...
}
GetTick(TCurrent,TBeginMSec);
if(IsErrCheck(DistErr)) class="kw">return;
i_tick++;
class="type">void GetTick(class="type">class="kw">datetime TDate,class="type">long TLong)
 {
 ArrayResize(MTick,ArraySize(MTick)+class="num">1);
class=class="str">"cmt">// 填充新时间  
 MTick[i_tick].time=TDate;
 MTick[i_tick].time_msc=TLong;
class=class="str">"cmt">// 用前一个数值填充当前逐笔报价
 if(ArraySize(MTick)>class="num">1)
   {
   MTick[i_tick].ask=MTick[i_tick-class="num">1].ask;
   MTick[i_tick].bid=MTick[i_tick-class="num">1].bid;
   MTick[i_tick].volume=MTick[i_tick-class="num">1].volume;
   MTick[i_tick].last=MTick[i_tick-class="num">1].last;
   }
  else
   {
   MTick[i_tick].ask=LastTick.ask;
   MTick[i_tick].bid=LastTick.bid;
   MTick[i_tick].last=LastTick.last;
   MTick[i_tick].volume=LastTick.volume;
   }
class=class="str">"cmt">// 填充当前逐笔报价  
  if(RandomTickValue)
   {
   class="type">class="kw">double RBid=MathRandomUniform(class="num">0,class="num">1,DistErr);
   class="type">class="kw">double RAsk=MathRandomUniform(class="num">0,class="num">1,DistErr);
   class="type">class="kw">double RVolume=MathRandomUniform(class="num">0,class="num">1,DistErr);
   if(RBid>=class="num">0.5)
     {
     if(i_tick>class="num">0)
       MTick[i_tick].bid=Base+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)*dStep;
     MTick[i_tick].last=MTick[i_tick].bid;
     MTick[i_tick].flags=class="num">10;
     if(RAsk>=class="num">0.5)
       {
       MTick[i_tick].ask=Base+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)*dStep;
       MTick[i_tick].flags=MTick[i_tick].flags+class="num">4;
       }
     if(RVolume>=class="num">0.5)
       {
       MTick[i_tick].volume=(class="type">class="kw">ulong)(baseVol+GetDist(DistVolume,DistVolumeParam1,DistVolumeParam2,DistVolumeParam3)*dStepVol);
       MTick[i_tick].flags=MTick[i_tick].flags+class="num">16;
       }
     }
   else
     {

随机扰动下的逐笔与分钟合成逻辑

这段逻辑展示了一个合成行情引擎在每根分钟线收尾时,如何处理买卖报价、成交量与极值。核心判断来自三个 [0,1) 均匀随机量:RBid、RAsk、RVolume,由 MathRandomUniform 在 DistErr 状态下生成。 当 RAsk 不低于 0.5 时,ask 价基于 Base 叠加 GetDist 算出的偏移量乘 dStep,flags 置为 4;若 RVolume 也 ≥0.5,volume 由 baseVol 加随机体积偏移,flags 再叠 16。否则走 else 分支,bid/ask/last 同价,flags 直接给 30,相当于非随机报价模式。 每笔 tick 都会更新 ValHigh 与 ValLow:仅当 bid 突破既有极值才记录,这意味着高低点严格取自 bid 侧,对贵金属与外汇模拟回测而言,这种单边取样可能低估真实振幅。 分钟柱收盘阶段,ValMsec 用 MathRand()%30000 除以 MaxTickInMinute*0.25 再加 1,控制毫秒抖动;ArrayResize 把 MRatesMin 扩容 1。首分钟 open 取 LastTick.bid,其后取上一 tick 的 bid,close 取末 tick bid,high/low 则与 open 比较后归一化到 _Digits。 开 MT5 把这段塞进自定义 synthesizer,调 RAsk/RVolume 的 0.5 阈值到 0.3,观察非随机模式触发频率变化,外汇与贵金属品种均属高风险,结果仅作概率参考。

MQL5 / C++
if(RAsk>=class="num">0.5)
  {
  MTick[i_tick].ask=Base+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)*dStep;
  MTick[i_tick].flags=class="num">4;
  if(RVolume>=class="num">0.5)
    {
     MTick[i_tick].volume=(class="type">class="kw">ulong)(baseVol+GetDist(DistVolume,DistVolumeParam1,DistVolumeParam2,DistVolumeParam3)*dStepVol);
     MTick[i_tick].flags=MTick[i_tick].flags+class="num">16;
    }
  }
 }
class=class="str">"cmt">// if(RandomTickValue) 则结束
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
  else
  {
   MTick[i_tick].bid=Base+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)*dStep;
   MTick[i_tick].ask=Base+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)*dStep;
   MTick[i_tick].last=MTick[i_tick].bid;
   MTick[i_tick].volume=(class="type">class="kw">ulong)(baseVol+GetDist(DistVolume,DistVolumeParam1,DistVolumeParam2,DistVolumeParam3)*dStepVol);
   MTick[i_tick].flags=class="num">30;
  }class=class="str">"cmt">// if(RandomTickValue) 则结束
  class=class="str">"cmt">// 存储所生成的分钟柱线最大值和最小值
  if(MTick[i_tick].bid>ValHigh)
   ValHigh=MTick[i_tick].bid;
  if(MTick[i_tick].bid<ValLow)
   ValLow=MTick[i_tick].bid;
 }class=class="str">"cmt">//结束 
      class="type">class="kw">double RBid=MathRandomUniform(class="num">0,class="num">1,DistErr);
      class="type">class="kw">double RAsk=MathRandomUniform(class="num">0,class="num">1,DistErr);
      class="type">class="kw">double RVolume=MathRandomUniform(class="num">0,class="num">1,DistErr);
ValMsec=(class="type">int)((MathRand()%class="num">30000)/(MaxTickInMinute*class="num">0.25)+class="num">1);
ArrayResize(MRatesMin,ArraySize(MRatesMin)+class="num">1);
          if(ArraySize(MRatesMin)==class="num">1)class=class="str">"cmt">// 如果是第一分钟
            {
             MRatesMin[i_bar].open=NormalizeDouble(LastTick.bid,_Digits);
             MRatesMin[i_bar].tick_volume=(class="type">long)i_tick;
            }
          else
            {
             MRatesMin[i_bar].open=NormalizeDouble(MTick[PrevTickCount-class="num">1].bid,_Digits);
             MRatesMin[i_bar].tick_volume=(class="type">long)i_tick-PrevTickCount;
            }
MRatesMin[i_bar].close=NormalizeDouble(MTick[i_tick-class="num">1].bid,_Digits);
 if(ValHigh>MRatesMin[i_bar].open)
   MRatesMin[i_bar].high=NormalizeDouble(ValHigh,_Digits);
 else
   MRatesMin[i_bar].high=MRatesMin[i_bar].open;
 if(ValLow<MRatesMin[i_bar].open)

「把逐笔tick压成分钟K线的收尾赋值」

这段逻辑跑在分钟聚合的末尾,负责把当前 bar 的低点、成交量、点差和 timestamps 一次性写进自定义数组 MRatesMin。若前面判断不是新低,就直接用开盘价填 low,避免无谓的 NormalizeDouble 调用。 low 用 NormalizeDouble(ValLow,_Digits) 对齐品种小数位;real_volume 直接把上一 tick 的 volume 强转 long 存进去,不另做累加。点差按 (bid-ask) 绝对值除以 _Point 后四舍五入成 int,黄金这种点值小的品种要注意 _Point 与报价的换算关系。 时间处理上,先 TimeToStruct 拆出上一 tick 的成交流,把秒清零再 StructToTime 回写,保证分钟 bar 的 time 落在整分起点。最后 i_bar>0 时调用 ReplaceHistory,用最新和上一 bar 的时间区间去修补历史,实盘里若掉线重连可能触发多次回写,建议在 MT5 用 Print 打出 MRatesMin[i_bar].time 验证覆盖边界。外汇和贵金属行情受杠杆与跳空影响大,这类自合成历史仅作分析参考,实盘风险自负。

MQL5 / C++
MRatesMin[i_bar].low=NormalizeDouble(ValLow,_Digits);
else
   MRatesMin[i_bar].low=MRatesMin[i_bar].open;
MRatesMin[i_bar].real_volume=(class="type">long)MTick[i_tick-class="num">1].volume;
MRatesMin[i_bar].spread=(class="type">int)MathRound(MathAbs(MTick[i_tick-class="num">1].bid-MTick[i_tick-class="num">1].ask)/_Point);
TimeToStruct(MTick[i_tick-class="num">1].time,StructCTime);
StructCTime.sec=class="num">0;
MRatesMin[i_bar].time=StructToTime(StructCTime);
   if(i_bar>class="num">0)
      ReplaceHistory(MRatesMin[class="num">0].time,MRatesMin[i_bar-class="num">1].time);

◍ 用脚本在 MT5 里捏出可控趋势行情

想在 MT5 里测 EA 或指标,光有平稳历史不够,得能生成带方向的走势。GetCandleTrend 和 GetTickTrend 两个脚本就是干这个的:前者直接造分钟柱线,后者先造逐笔报价再合成柱线,逻辑一致。 趋势模型有七种:线性、双曲线、指数、幂、抛物线、线性周期、线性随机。公式里的 T(i) 是当前趋势值,k1/k2/k3 控斜率与曲率,N(0,1) 是零均值单位方差的正态随机变量。 一个反直觉点:双曲线模型里选 TType=Increasing,图表反而向下走,因为双曲函数属反函数类。指数模型 Coeff1=1、Coeff2=0.1 时,柱线越大图表越呈指数拉升;幂模型 Coeff2=2 与之类似但更平滑;抛物线 Coeff2=Coeff3=0.05 则速率明显更高。 这些形态只在 M1 明显。切到 M15 及以上,它们看着都像直线。要在大周期看到转向,得用随机趋势:TType=Random,CountCandle=60 表示每 60 根分钟柱尝试变向。RandomTrendCoeff 调概率——设 0.7 倾向跌,设 0.3 倾向涨,H1 图表实测可验证。外汇与贵金属模拟仅作技术验证,实盘高风险,参数不代表任何收益预期。 GetTickTrend 功能与 GetCandleTrend 对齐,只是底层从报价出发,适合测对 tick 敏感的逻辑。

MQL5 / C++
class="kw">input TrendModel TModel = Linear;                    class=class="str">"cmt">// 趋势模型
class="kw">input TrendType TType =  Increasing;                  class=class="str">"cmt">// 趋势类型 (递增/递减,随机)
class="kw">input class="type">class="kw">double RandomTrendCoeff=class="num">0.5;                    class=class="str">"cmt">// 趋势系数 (如果 RandomTrendCoeff<class="num">0.5 下跌趋势占主导地位; 如果 RandomTrendCoeff>class="num">0.5 - 上涨)
class="kw">input class="type">class="kw">double Coeff1=class="num">0.1;                              class=class="str">"cmt">// 趋势模型系数 k1
class="kw">input class="type">class="kw">double Coeff2=class="num">0.1;                              class=class="str">"cmt">// 趋势模型系数 k2
class="kw">input class="type">class="kw">double Coeff3=class="num">0.1;                              class=class="str">"cmt">// 趋势模型系数 k3
class="kw">input class="type">int CountCandle=class="num">60;                             class=class="str">"cmt">// 趋势方向随机变化的间隔(以柱线为单位)
enum TrendModel
  {
   Linear,
   Hyperbolic,
   Exp,
   Power,
   SecondOrderPolynomial,
   LinearAndPeriodic,
   LinearAndStochastic
  };
enum TrendType
  {
   Increasing,
   Decreasing,
   Random
  };
class="type">class="kw">double ChooseTrend()
  {
   class="kw">switch(TType)
     {
      case class="num">0: class="kw">return NormalizeDouble(BValue[class="num">0]+dOCHL*(GetTrend()+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)),_Digits);
      case class="num">1: class="kw">return NormalizeDouble(BValue[class="num">0]+dOCHL*(-GetTrend()+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)),_Digits);
      case class="num">2:
        {

趋势模型的数学分支实现

这段逻辑把价格演化拆成两段:先用周期计数器 i_trend 决定是否重置基准价,再用 LastRand 与 RandomTrendCoeff 的比较来定多空偏移方向。当 i_trend 能被 CountCandle 整除且非零时,把上一根收盘价塞进 BValue[0],重新抽一个 [0,1) 均匀随机数,并把 i_trend 归零。 若 LastRand 大于 RandomTrendCoeff,返回 BValue[0] 加上正向趋势项 dOCHL*(GetModelTrend()+噪声);否则走负向分支。这里 NormalizeDouble(...,_Digits) 保证输出精度与品种小数点位数一致,外汇与贵金属点差敏感,位数不对会直接画出错位价位。 GetModelTrend() 是核心分支函数,支持 Linear、Hyperbolic、Exp、Power、二阶多项式、线性叠加周期、线性叠加随机游走共 7 种模型。比如 Hyperbolic 在 i_trend==0 时返回 Coeff1,否则返回 Coeff1+Coeff2/i_trend,意味着趋势强度随步数增加衰减;LinearAndStochastic 则用上一时刻 LastValue 做指数衰减递归,带 MathRandomNormal 正态扰动。 开 MT5 把这段代码贴进 EA 的 price 生成函数,改 TModel 枚举和 Coeff1~3,就能对比不同数学形态下模拟行情的发散速度。外汇和贵金属杠杆高,模拟参数不代表实盘概率,仅作模型验证用。

MQL5 / C++
if((i_trend%CountCandle==class="num">0) && (i_trend!=class="num">0))
  {
   if(i_bar!=class="num">0)
     BValue[class="num">0]=MRatesMin[i_bar-class="num">1].close;
   LastRand=MathRandomUniform(class="num">0,class="num">1,DistErr);
   i_trend=class="num">0;
  }
 if(LastRand>RandomTrendCoeff)
   class="kw">return NormalizeDouble(BValue[class="num">0]+dOCHL*(GetModelTrend()+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)),_Digits);
 else
   class="kw">return NormalizeDouble(BValue[class="num">0]+dOCHL*(-GetModelTrend()+GetDist(DistOCHL,DistOCHLParam1,DistOCHLParam2,DistOCHLParam3)),_Digits);
 }
 class="kw">default:class="kw">return class="num">0;
  }
 }
class="type">class="kw">double GetModelTrend()
  {
  class="kw">switch(TModel)
   {
    case Linear: class="kw">return Coeff1+Coeff2*i_trend;
    case Hyperbolic:
     {
      if(i_trend==class="num">0)
        class="kw">return Coeff1;
      else
        class="kw">return Coeff1+Coeff2/i_trend;
     }
    case Exp:
     {
      if(i_trend==class="num">0)
        class="kw">return Coeff1;
      else
        class="kw">return Coeff1+MathExp(Coeff2*i_trend);
     }
    case Power:class="kw">return Coeff1+MathPow((class="type">class="kw">double)i_trend,Coeff2);
    case SecondOrderPolynomial:class="kw">return Coeff1+Coeff2*i_trend+Coeff3*i_trend*i_trend;
    case LinearAndPeriodic: class="kw">return Coeff1*i_trend+sin(Coeff2*i_trend)+cos(Coeff3*i_trend);
    case LinearAndStochastic:
     {
      LastValue=Coeff1*i_trend+MathSqrt(Coeff2*(class="num">1-MathPow(exp(-Coeff3),class="num">2)))*MathRandomNormal(class="num">0,class="num">1,DistErr)+exp(-Coeff3)*LastValue;
      class="kw">return LastValue;
     }
    class="kw">default:
      class="kw">return -class="num">1;
   }
  }

「用脚本在MT5里捏出双顶双底」

想在MT5里做形态回测或教学演示,不一定非要等真实行情走出来。借助 GetCandleTrend 这类生成脚本,可以直接在指定周期合成“双顶”“双底”等结构,用来验证指标信号或训练肉眼识图。 以H1周期合成双顶为例,需要分四段跑脚本,每段覆盖不同的时间窗:t1=00:00 02.01.2018 到 t2=12:00 02.01.2018 为第一段,之后每段顺延至 t5=00:00 05.01.2018。第一次运行趋势系数设0.15,第二、四次设0.85,第三次回到0.15,其余参数(LinearAndStochastic模型、随机趋势类型、k1=0.15/k2=1/k3=1、方向随机变化区间60)保持默认。 双底只是把趋势系数反过来:首段0.85,随后0.15、0.85、0.15,时间窗与双顶完全一致。跑完四次后,图表上就会出现标准的两峰或两谷结构。 更细的形态建议在分钟图上生成,精度最高。若改到H4等其他周期,必须把“趋势方向随机变化区间”参数调成对应蜡烛分钟数——H1填60,H4填240,否则形态拉伸会失真。外汇与贵金属杠杆高,模拟形态仅作技术验证,实盘仍以小周期信号结合风控为准。

◍ 自定义品种脚本的实盘前验证路径

这套自定义品种脚本把原本要等真实行情才能跑的 EA 与指标测试,压缩到了本地可控环境。CreateSymbol.mq5 体积 28.79 KB,DeleteSymbol.mq5 仅 6.96 KB,前者负责基于已有品种或手填属性建自定义品种,后者会先关掉该品种所有图表再从市场观察移除,避免残留。 生成逐笔报价和柱线时,GetTick.mq5(39.14 KB)与 GetCandle.mq5(33.06 KB)能在指定间隔造分钟柱,甚至补周末非交易日的 K 线;蜡烛类型覆盖多头、空头、十字星、锤头、星与 maribozu。大数组下脚本支持部分柱线替换以省内存,这点对长周期回测很关键。 趋势与形态模拟分两层:GetTickTrend.mq5(46.66 KB)按线性、双曲线、指数、幂、抛物线、线性周期和线性随机七类模型造势;GetCandleTrend.mq5(40.66 KB)给了双顶双底形态的演示。读者开 MT5 加载这些文件,改一个模型参数就能看到历史重构的差异。 外汇与贵金属属高风险品类,自定义历史不等于真实撮合,任何回测优绩都只是概率倾向。直接拿脚本产物去跑实盘前,先确认你的 MT5 版本不会清掉自定义符号——有用户反馈重启丢失,作者回称更新版本后正常。

MQL5 / C++
class="macro">#include </Math/Stat/Beta.mqh>
class="macro">#include </Math/Stat/Binomial.mqh>
class="macro">#include </Math/Stat/Cauchy.mqh>
class="macro">#include </Math/Stat/ChiSquare.mqh>
class="macro">#include </Math/Stat/Exponential.mqh>
class="macro">#include </Math/Stat/F.mqh>
class="macro">#include </Math/Stat/Gamma.mqh>
class="macro">#include </Math/Stat/Geometric.mqh>
class="macro">#include </Math/Stat/Hypergeometric.mqh>
class="macro">#include </Math/Stat/Logistic.mqh>
class="macro">#include </Math/Stat/Lognormal.mqh>
class="macro">#include </Math/Stat/NegativeBinomial.mqh>
class="macro">#include </Math/Stat/NoncentralBeta.mqh>
class="macro">#include </Math/Stat/NoncentralChiSquare.mqh>
class="macro">#include </Math/Stat/NoncentralF.mqh>
class="macro">#include </Math/Stat/NoncentralT.mqh>
class="macro">#include </Math/Stat/Normal.mqh>
class="macro">#include </Math/Stat/Poisson.mqh>
class="macro">#include </Math/Stat/T.mqh>
class="macro">#include </Math/Stat/Uniform.mqh>
class="macro">#include </Math/Stat/Weibull.mqh>
class="macro">#include <Math\Stat\Stat.mqh>
把重复劳动交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到自定义序列的分布偏离度,你只需专注调参和决策。

常见问题

默认在 Custom/Forex 文件夹;若想在 Custom 下建子文件夹,需在函数第二个参数里写明组名。
脚本用 SetProperty 的三个重载实例分别处理字符串、整数、双精度,底层调用 CustomSymbolSetString/Integer/Double 并读取基准的 SymbolInfo 系列函数。
可以,品种页内置了序列分布诊断,上传或指定自定义品种后就能看偏度峰度是否贴合你设的法则。
倾向先生成基础逐笔报价与柱线,再叠加趋势偏移,最后嵌形态模板;乱序可能导致形态被趋势逻辑覆盖。
能,脚本只需填名称、简称、全称和可选基准名四个输入,编译后直接挂终端运行即可。