添加、替换和删除报价

自定义交易品种通过以下两个内置函数填充报价:CustomRatesUpdateCustomRatesReplace。除了交易品种的名称外,两者都期望输入一个 M1 时间范围的 MqlRates 结构体数组(更高的时间范围会自动从 M1 完成)。CustomRatesReplace 还有一对额外的参数(fromto),用于定义历史记录编辑受限的时间范围。

int CustomRatesUpdate(const string symbol, const MqlRates &rates[], uint count = WHOLE_ARRAY)

int CustomRatesReplace(const string symbol, datetime from, datetime to, const MqlRates &rates[], uint count = WHOLE_ARRAY)

CustomRatesUpdate 将缺失的柱线添加到历史记录中,并用数组中的数据替换现有的匹配柱线。

CustomRatesReplace 用数组中的数据完全替换指定时间间隔内的历史记录。

这些函数的区别在于预期应用场景的不同。下表更详细地列出了这些差异。

CustomRatesUpdate

CustomRatesReplace

将传递的 MqlRates 数组的元素应用于历史记录,无论其时间戳如何

仅应用传递的 MqlRates 数组中落在指定范围内的那些元素

保留历史记录中那些在函数调用之前已经存在且与数组中柱线时间不一致的 M1 柱线

范围之外的所有历史记录保持不变

当时间戳匹配时,用数组中的柱线替换现有的历史柱线

完全删除指定范围内的现有历史柱线

如果与旧柱线没有匹配项,则将数组中的元素作为“新”柱线插入

将数组中落在相关范围内的柱线插入到指定的历史记录范围中

rates 数组中的数据必须由有效的 OHLC 价格表示,并且柱线开盘时间不能包含秒。

fromto 之间的时间间隔是包含性的: from 等于要处理的第一个柱线的时间,to 等于最后一个柱线的时间。

下图更清楚地说明了这些规则。每个柱线的唯一时间戳都用其自己的拉丁字母表示。历史记录中可用的柱线用大写字母显示,而数组中的柱线用小写字母显示。字符 '-' 表示相应时间的历史记录或数组中的间隔符。

History                        ABC-EFGHIJKLMN-PQRST------    B
Array                          -------hijk--nopqrstuvwxyz    A
Result of CustomRatesUpdate    ABC-EFGhijkLMnopqrstuvwxyz    R
Result of CustomRatesReplace   ABC-E--hijk--nopqrstuvw---    S
                                    ^                ^
                                    |from          to|    TIME

可选参数 count 设置应使用的 rates 数组中元素的数量(其他元素将被忽略)。这允许你部分处理传递的数组。默认值 WHOLE_ARRAY 表示整个数组。

可以使用 CustomRatesDelete 函数完全或部分删除自定义交易品种的报价历史记录。

int CustomRatesDelete(const string symbol, datetime from, datetime to)

在这里,参数 fromto 也设置了被移除柱线的时间范围。要覆盖整个历史记录,请指定 0 和 LONG_MAX。

所有三个函数都返回已处理柱线的数量:更新或删除。如果发生错误,结果为 -1。

应该注意的是,自定义交易品种的报价不仅可以通过添加现成的柱线来形成,还可以通过分时报价数组甚至单个分时报价序列来形成。相关函数将在 下一节中介绍。当添加分时报价时,终端将自动基于它们计算柱线。这些方法之间的区别在于,自定义分时报价历史允许你在“真实”分时报价模式下测试 MQL 程序,而柱线历史记录只会迫使你要么将自己限制在 OHLC M1 或开盘价模式,要么依赖于测试器实现的分时报价模拟。

此外,通过逐个添加分时报价,你可以在自定义交易品种的图表上模拟标准的 OnTickOnCalculate 事件,这会使图表实现动画效果,类似于在线可用的工具,并在 MQL 程序绘制在图表上时启动相应的处理函数。但我们将在下一节讨论这个问题。

作为使用新函数的一个示例,我们考虑脚本 CustomSymbolRandomRates.mq5。它旨在按照“随机游走”或对现有报价进行“噪声处理”的原则生成随机报价。后者可用于评估 EA 交易的稳定性。

为了检查报价形成的正确性,我们还将支持一种模式,在该模式下创建原始金融工具的完整副本,并在其图表上启动脚本。

所有模式都收集在 RANDOMIZATION 枚举中。

enum RANDOMIZATION
{
   ORIGINAL,
   RANDOM_WALK,
   FUZZY_WEAK,
   FUZZY_STRONG,
};

我们用两种强度级别来实现报价噪声:弱和强。

在输入参数中,除了模式之外,你还可以选择交易品种层级结构中的文件夹、日期范围以及用于初始化随机生成器的数字(以便能够重现结果)。

input string CustomPath = "MQL5Book\\Part7";    // Custom Symbol Folder
input RANDOMIZATION RandomFactor = RANDOM_WALK;
input datetime _From;                           // From (default: 120 days ago)
input datetime _To;                             // To (default: current time)
input uint RandomSeed = 0;

默认情况下,如果未指定日期,脚本会生成最近 120 天的报价。RandomSeed 参数中的值 0 表示随机初始化。

交易品种的名称是根据当前图表的交易品种和选定的设置生成的。

const string CustomSymbol = _Symbol + "." + EnumToString(RandomFactor)
   + (RandomSeed ? "_" + (string)RandomSeed : "");

OnStart 的开头,我们将准备并检查数据。

datetime From;
datetime To;
   
void OnStart()
{
   From = _From == 0 ? TimeCurrent() - 60 * 60 * 24 * 120 : _From;
   To = _To == 0 ? TimeCurrent() / 60 * 60 : _To;
   if(From > To)
   {
      Alert("Date range must include From <= To");
      return;
   }
   
   if(RandomSeed != 0MathSrand(RandomSeed);
   ...

由于脚本很可能需要运行多次,因此我们将提供删除先前创建的自定义交易品种的功能,并向用户请求二次确认。

   bool custom = false;
   if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
   {
      if(IDYES == MessageBox(StringFormat("Delete custom symbol '%s'?"CustomSymbol),
         "Please, confirm"MB_YESNO))
      {
         if(CloseChartsForSymbol(CustomSymbol))
         {
            Sleep(500); // wait for the changes to take effect (opportunistically)
            PRTF(CustomRatesDelete(CustomSymbol0LONG_MAX));
            PRTF(SymbolSelect(CustomSymbolfalse));
            PRTF(CustomSymbolDelete(CustomSymbol));
         }
      }
   }
   ...

辅助函数 CloseChartsForSymbol 此处未显示(有兴趣的可以查看附加的源代码):其目的是查看打开的图表列表并关闭那些工作交易品种是被删除的自定义交易品种的图表(否则,删除将无法进行)。

更重要的是,要注意调用具有完整日期范围的 CustomRatesDelete。如果不这样做,先前用户交易品种的数据将在历史数据库(文件夹 bases/Custom/history/<symbol-name>)的磁盘上保留一段时间。换句话说,上面最后一行显示的 CustomSymbolDelete 调用不足以实际从终端清除自定义交易品种。

如果用户决定立即再次创建同名交易品种(我们在下面的代码中提供了这样的机会),那么旧的报价可能会混入新的报价中。

此外,在用户确认后,将启动生成报价的过程。这是由 GenerateQuotes 函数完成的(见下文)。

   if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?"CustomSymbol),
      "Please, confirm"MB_YESNO))
   {
      if(PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         if(RandomFactor == RANDOM_WALK)
         {
            CustomSymbolSetInteger(CustomSymbolSYMBOL_DIGITS8);
         }
         
         CustomSymbolSetString(CustomSymbolSYMBOL_DESCRIPTION"Randomized quotes");
      
         const int n = GenerateQuotes();
         Print("Bars M1 generated: "n);
         if(n > 0)
         {
            SymbolSelect(CustomSymboltrue);
            ChartOpen(CustomSymbolPERIOD_M1);
         }
      }
   }

如果成功,新创建的交易品种将在Market Watch中被选中,并为其打开一个图表。在此过程中,演示了如何设置一对特性:SYMBOL_DIGITS 和 SYMBOL_DESCRIPTION。

GenerateQuotes 函数中,除了 RANDOM_WALK 模式外,都必须请求原始交易品种的报价。

int GenerateQuotes()
{
   MqlRates rates[];
   MqlRates zero = {};
   datetime start;     // time of the current bar
   double price;       // last closing price
   
   if(RandomFactor != RANDOM_WALK)
   {
      if(PRTF(CopyRates(_SymbolPERIOD_M1FromTorates)) <= 0)
      {
         return 0// error
      }
      if(RandomFactor == ORIGINAL)
      {
         return PRTF(CustomRatesReplace(CustomSymbolFromTorates));
      }
      ...

务必要记住,CopyRates 受图表上柱线数量限制的影响,该限制在终端设置中设置。

在 ORIGINAL 模式下,我们只需将生成的数组 rates 转发到 CustomRatesReplace 函数中。对于噪声模式,我们将专门选择的 pricestart 变量设置为第一个柱线的初始价格和时间值。

      price = rates[0].open;
      start = rates[0].time;
   }
   ...

在随机游走模式下,不需要报价,因此我们只需为将来的随机 M1 柱线分配 rates 数组。

   else
   {
      ArrayResize(rates, (int)((To - From) / 60) + 1);
      price = 1.0;
      start = From;
   }
   ...

接下来,在遍历 rates 数组的循环中,将随机值添加到原始交易品种的噪声价格中或“按原样”添加。在 RANDOM_WALK 模式下,我们自己负责增加变量 start 中的时间。在其他模式下,时间已经包含在初始报价中。

   const int size = ArraySize(rates);
   
   double hlc[3]; // future High Low Close (in unknown order)
   for(int i = 0i < size; ++i)
   {
      if(RandomFactor == RANDOM_WALK)
      {
         rates[i] = zero;             // zeroing the structure
         rates[i].time = start += 60// plus a minute to the last bar
         rates[i].open = price;       // start from the last price
         hlc[0] = RandomWalk(price);
         hlc[1] = RandomWalk(price);
         hlc[2] = RandomWalk(price);
      }
      else
      {
         double delta = 0;
         if(i > 0)
         {
            delta = rates[i].open - price// cumulative correction
         }
         rates[i].open = price;
         hlc[0] = RandomWalk(rates[i].high - delta);
         hlc[1] = RandomWalk(rates[i].low - delta);
         hlc[2] = RandomWalk(rates[i].close - delta);
      }
      ArraySort(hlc);
      
      rates[i].high = fmax(hlc[2], rates[i].open);
      rates[i].low = fmin(hlc[0], rates[i].open);
      rates[i].close = price = hlc[1];
      rates[i].tick_volume = 4;
   }
   ...

基于最后一个柱线的收盘价,生成 3 个随机值(使用 RandomWalk 函数)。它们的最大值和最小值分别成为新柱线的HighLow价。平均值是Close价。

在循环结束时,我们将数组传递给 CustomRatesReplace

   return PRTF(CustomRatesReplace(CustomSymbolFromTorates));
}

RandomWalk 函数中,尝试模拟具有宽尾部分布的情况,这是真实报价的典型情况。

double RandomWalk(const double p)
{
   const static double factor[] = {0.00.10.010.05};
   const static double f = factor[RandomFactor] / 100;
   const double r = (rand() - 16383.0) / 16384.0// [-1,+1]
   const int sign = r >= 0 ? +1 : -1;
   if(r != 0)
   {
      return p + p * sign * f * sqrt(-log(sqrt(fabs(r))));
   }
   return p;
}

随机变量的离散系数取决于模式。例如,弱噪声增加(或减少)最多百分之一的价格,强噪声增加百分之五的价格。

运行时,脚本会输出如下所示的详细日志:

Create new custom symbol 'GBPUSD.RANDOM_WALK'?
CustomSymbolCreate(CustomSymbol,CustomPath,_Symbol)=true / ok
CustomRatesReplace(CustomSymbol,From,To,rates)=171416 / ok
Bars M1 generated: 171416

我们看看结果如何。

下图显示了随机游走的几种实现(视觉叠加是在图形编辑器中完成的,实际上,每个自定义交易品种一般都是在单独的窗口中打开)。

具有随机游走的自定义交易品种的报价选项

具有随机游走的自定义交易品种的报价选项

这是 GBPUSD 报价加噪后的样子(黑色为原始报价,彩色为加噪报价)。首先,是弱噪声版本。

弱噪声的 GBPUSD 报价

弱噪声的 GBPUSD 报价

然后是强噪声。

强噪声的 GBPUSD 报价

强噪声的 GBPUSD 报价

尽管保留了局部特征,但明显存在较大的差异。