添加、替换和移除分时报价

MQL5 API 允许你不仅在柱线级别,而且在分时报价级别生成自定义交易品种的历史。因此,在测试和优化 EA 交易时可以实现更高的真实性,并模拟自定义交易品种图表的实时更新,向这些自定义交易品种广播你的分时报价。形成柱线时,系统会自动考虑传输到系统的分时报价集。换句话说,如果以分时报价的形式(即 MqlTick 结构体数组)提供了同一时期价格变化的更详细信息,则无需调用上一节中操作 MqlRates 结构体的函数。每柱线 MqlRates 报价的唯一优点是性能和内存效率。

有两个用于添加分时报价的函数:CustomTicksAddCustomTicksReplace。第一个函数添加到达 Market Watch 窗口的交互式分时报价(终端会自动将它们从该窗口传输到分时报价数据库),并在 MQL 程序中生成相应的事件。第二个函数直接将分时报价写入分时报价数据库。

int CustomTicksAdd(const string symbol, const MqlTick &ticks[], uint count = WHOLE_ARRAY)

CustomTicksAdd 函数将 ticks 数组中的数据添加到 symbol 中指定的自定义交易品种的价格历史中。默认情况下,如果 count 设置等于 WHOLE_ARRAY,则添加整个数组。如有必要,你可以指定一个较小的数字,仅下载一部分分时报价。

请注意,在调用函数时,自定义交易品种必须已在Market Watch窗口中被选中。对于未在Market Watch窗口中选择的交易品种,你需要使用 CustomTicksReplace 函数(见下文)。

分时报价数据数组必须按时间升序排序,即要求满足以下条件:对于所有 i < jticks[i].time_msc <= ticks[j].time_msc

该函数返回添加的分时报价数量,如果发生错误则返回 -1。

CustomTicksAdd 函数以与从经纪商服务器传来相同的方式将分时报价广播到图表。通常,该函数应用于一个或多个分时报价。在这种情况下,它们会在Market Watch窗口中“播放”,然后从那里保存到分时报价数据库中。

但是,当一次调用中传输大量数据时,该函数会更改其行为以节省资源。如果传输超过 256 项分时报价,它们将被分成两部分。第一部分包含大量数据,立即直接写入分时报价数据库(与 CustomTicksReplace 的行为相同)。第二部分由最后(最新的)128 项分时报价组成,传递到Market Watch窗口,然后由终端保存到数据库中。

MqlTick 结构体有两个带时间值的字段: time (以秒为单位的分时报价时间)和 time_msc(以毫秒为单位的分时报价时间)。两个值都从 1970 年 1 月 1 日开始计时。已填充(非空)的 time_msc 字段优先于 time。请注意,time 是根据公式 time_msc / 1000 重新计算的结果,以秒为单位填充。如果 time_msc 字段为零,则使用 time 字段的值,而 time_msc 字段又从公式 time * 1000 中获取以毫秒为单位的值。如果两个字段都等于零,则将当前服务器时间(精确到毫秒)放入分时报价中。

在描述交易量的两个字段中,volume_real 的优先级高于 volume

根据特定数组元素(结构体 MqlTick)中填充了哪些其他字段,系统会在 flags 字段中为保存的分时报价设置标志:

  • ticks[i].bid – TICK_FLAG_BID(分时报价更改了卖价)
  • ticks[i].ask – TICK_FLAG_ASK(分时报价更改了买价)
  • ticks[i].last – TICK_FLAG_LAST(分时报价更改了最后一笔交易的价格)
  • ticks[i].volume or ticks[i].volume_real – TICK_FLAG_VOLUME(分时报价更改了交易量)

如果某个字段的值小于或等于零,则相应的标志不会写入 flags 字段。

TICK_FLAG_BUY 和 TICK_FLAG_SELL 标志不会添加到自定义交易品种的历史中。

CustomTicksReplace 函数用所传递数组中的数据完全替换指定时间间隔内自定义交易品种的价格历史。

int CustomTicksReplace(const string symbol, long from_msc, long to_msc,
const MqlTick &ticks[], uint count = WHOLE_ARRAY)

时间间隔由 from_mscto_msc 参数设置,单位为自 1970 年 1 月 1 日以来的毫秒数。两个值都包含在时间间隔内。

ticks 数组必须按分时报价到达的时间顺序排列,这对应于时间的增加,或者更确切地说,是非递减,因为具有相同时间的分时报价在具有毫秒精度的流中经常连续出现。

count 参数可用于处理数组的一部分。

to_msc 中指定的时间之前,或者以分时报价顺序发生错误之前,分时报价会逐日顺序替换。首先处理指定范围内的第一天,然后是下一天,依此类推。一旦检测到分时报价时间与升序(非递减)顺序不符,分时报价替换过程将在当前日期停止。在这种情况下,前几天的分时报价将成功替换,而当前日期(发生错误分时报价时)和指定时间间隔内的所有剩余日期将保持不变。该函数将返回 -1,_LastError 中的错误代码为 0(“无错误”)。

如果 ticks 数组在 from_mscto_msc(含)之间的总时间间隔内没有某个时期的数据,则在执行该函数后,自定义交易品种的历史中将出现与缺失数据对应的间隙。

如果在指定的时间间隔内分时报价数据库中没有数据,CustomTicksReplace 将从 ticks 数组中向其添加分时报价。

CustomTicksDelete 函数可用于删除指定时间间隔内的所有分时报价。

int CustomTicksDelete(const string symbol, long from_msc, long to_msc)

正在编辑的自定义交易品种的名称在 symbol 参数中设置,要清除的时间间隔由参数 from_mscto_msc(含)设置,单位为毫秒。

该函数返回移除的分时报价数量,如果发生错误则返回 -1。

注意!使用 CustomTicksDelete 删除分时报价会导致自动移除相应的柱线!但是,调用 CustomRatesDelete,即移除柱线,并不会移除分时报价!

为了在实践中掌握这些材料,我们将使用新学习的函数解决几个应用问题。

首先,我们接触一个有趣的任务,即基于真实交易品种但降低分时报价密度来创建自定义交易品种。与基于真实分时报价的模式相比,这将加快测试和优化速度,并减少资源消耗(主要是 RAM),同时保持可接受的、接近理想的过程质量。

加快测试和优化速度
 
交易者经常寻求加快 EA 交易优化和测试过程的方法。在可能的解决方案中,有些是显而易见的,只需更改设置即可(如果允许),还有一些更耗时,需要调整 EA 交易或测试环境。
 
第一类解决方案包括:
 
通过消除某些参数或减少其步长来缩小优化空间;
缩短优化周期;
切换到质量较低的分时报价模拟模式(例如,从真实分时报价到 OHLC M1);
启用以点而不是资金计算利润;
升级计算机;
使用 MQL Cloud 或其他本地网络计算机。
 
第二类与开发相关的解决方案包括:
 
代码概要分析,在此基础上可以消除代码中的“瓶颈”;
如果可能,使用资源高效的指标计算,即不使用 #property tester_everytick_calculate 指令;
将指标算法(如果使用)直接移植到 EA 交易代码中:指标调用会产生一定的开销成本;
消除图形和对象;
缓存计算(如果可能);
减少同时未平仓仓位和平仓订单的数量(大量订单时,每项分时报价的计算可能会变得明显);
结算、订单、交易和仓位的完全虚拟化:内置的记账机制由于其通用性、多币种支持和其他特性,有其自身的开销,可以通过在 MQL5 代码中执行类似的操作来消除(尽管此选项最耗时)。
 
分时报价密度降低属于一种中间类型的解决方案:它需要以编程方式创建自定义交易品种,但不会影响 EA 交易的源代码。

具有更少分时报价的自定义交易品种将由脚本 CustomSymbolFilterTicks.mq5 生成。初始金融工具将是启动脚本的图表的工作交易品种。在输入参数中,你可以指定自定义交易品种的文件夹和历史处理的开始日期。默认情况下,如果未指定日期,则计算最近 120 天。

input string CustomPath = "MQL5Book\\Part7"// Custom Symbol Folder
input datetime _Start;                       // Start (default: 120 days back)

交易品种的名称由源金融工具的名称和 ".TckFltr" 后缀组成。稍后我们将向其添加分时报价减少方法的标记。

string CustomSymbol = _Symbol + ".TckFltr";
const uint DailySeconds = 60 * 60 * 24;
datetime Start = _Start == 0 ? TimeCurrent() - DailySeconds * 120 : _Start;

为方便起见,在 OnStart 处理程序中,如果先前副本已存在,则可以删除它。

void OnStart()
{
   bool custom = false;
   if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
   {
      if(IDYES == MessageBox(StringFormat("Delete existing custom symbol '%s'?"CustomSymbol),
         "Please, confirm"MB_YESNO))
      {
         SymbolSelect(CustomSymbolfalse);
         CustomRatesDelete(CustomSymbol0LONG_MAX);
         CustomTicksDelete(CustomSymbol0LONG_MAX);
         CustomSymbolDelete(CustomSymbol);
      }
      else
      {
         return;
      }
   }

接下来,在用户同意后,将创建一个交易品种。历史由辅助函数 GenerateTickData 中的分时报价数据填充。如果成功,脚本会将新交易品种添加到Market Watch并打开图表。

   if(IDYES == MessageBox(StringFormat("Create new custom symbol '%s'?"CustomSymbol),
      "Please, confirm"MB_YESNO))
   {
      if(PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         CustomSymbolSetString(CustomSymbolSYMBOL_DESCRIPTION"Prunned ticks by " + EnumToString(Mode));
         if(GenerateTickData())
         {
            SymbolSelect(CustomSymboltrue);
            ChartOpen(CustomSymbolPERIOD_H1);
         }
      }
   }
}

GenerateTickData 函数以每天为单位,分批循环处理分时报价。每天的分时报价通过调用 CopyTicksRange 来请求。然后需要以某种方式减少它们,这由我们将在下面展示的 TickFilter 类实现。最后,使用 CustomTicksReplace 将分时报价数组添加到自定义交易品种历史中。

bool GenerateTickData()
{
   bool result = true;
   datetime from = Start / DailySeconds * DailySeconds// round up to the beginning of the day
   ulong read = 0written = 0;
   uint day = 0;
   const uint total = (uint)((TimeCurrent() - from) / DailySeconds + 1);
   MqlTick array[];
   
   while(!IsStopped() && from < TimeCurrent())
   {
      Comment(TimeToString(fromTIME_DATE), " "day++, "/"total);
      
      const int r = CopyTicksRange(_SymbolarrayCOPY_TICKS_ALL,
         from * 1000L, (from + DailySeconds) * 1000L - 1);
      if(r < 0)
      {
         Alert("Error reading ticks at "TimeToString(fromTIME_DATE));
         result = false;
         break;
      }
      read += r;
      
      if(r > 0)
      {
         const int t = TickFilter::filter(Modearray);
         const int w = CustomTicksReplace(CustomSymbol,
            from * 1000L, (from + DailySeconds) * 1000L - 1array);
         if(w <= 0)
         {
            Alert("Error writing custom ticks at "TimeToString(fromTIME_DATE));
            result = false;
            break;
         }
         written += w;
      }
      from += DailySeconds;
   }
   
   if(read > 0)
   {
      PrintFormat("Done ticks - read: %lld, written: %lld, ratio: %.1f%%",
         readwrittenwritten * 100.0 / read);
   }
   Comment("");
   return result;
}

在所有阶段都实现了错误控制和已处理分时报价的计数。最后,我们将初始和剩余分时报价的数量以及“压缩”因子输出到日志中。

现在,我们直接转向分时报价缩减技术。显然,可以采用很多方法,每种方法都或多或少地适用于特定的交易策略。我们将提供 3 个基本版本,组合在 TickFilter 类 (TickFilter.mqh) 中。此外,为了完整起见,还支持不减少分时报价的复制模式。

因此,该类中实现了以下模式:

  • 无缩减
  • 跳过价格单调变化而没有反转的分时报价序列(类似于“之字形”)
  • 跳过点差内的价格波动
  • 仅当BidAsk在两个相邻分时报价之间表示极值时,才记录具有分形配置的分时报价

这些模式被描述为 FILTER_MODE 枚举的元素。

class TickFilter
{
public:
   enum FILTER_MODE
   {
      NONE,
      SEQUENCE,
      FLUTTER,
      FRACTALS,
   };
   ...

每种模式都由一个单独的静态方法实现,该方法接受一个需要精简的分时报价数组作为输入。数组编辑在原地执行(不分配新的输出数组)。

   static int filterBySequences(MqlTick &data[]);
   static int filterBySpreadFlutter(MqlTick &data[]);
   static int filterByFractals(MqlTick &data[]);

所有方法都返回剩余的分时报价数量(缩减后的数组大小)。

为了统一不同模式下过程的执行,提供了 filter 方法。对于 NONE 模式,data 数组保持不变。

   static int filter(FILTER_MODE modeMqlTick &data[])
   {
      switch(mode)
      {
      case SEQUENCEreturn filterBySequences(data);
      case FLUTTERreturn filterBySpreadFlutter(data);
      case FRACTALSreturn filterByFractals(data);
      }
      return ArraySize(data);
   }

例如,以下展示了如何在 filterBySequences 方法中通过单调分时报价序列进行筛选。

   static int filterBySequences(MqlTick &data[])
   {
      const int size = ArraySize(data);
      if(size < 3return size;
      
      int index = 2;
      bool dirUp = data[1].bid - data[0].bid + data[1].ask - data[0].ask > 0;
      
      for(int i = 2i < sizei++)
      {
         if(dirUp)
         {
            if(data[i].bid - data[i - 1].bid + data[i].ask - data[i - 1].ask < 0)
            {
               dirUp = false;
               data[index++] = data[i];
            }
         }
         else
         {
            if(data[i].bid - data[i - 1].bid + data[i].ask - data[i - 1].ask > 0)
            {
               dirUp = true;
               data[index++] = data[i];
            }
         }
      }
      return ArrayResize(dataindex);
   }

这是分形精简呈现的效果。

   static int filterByFractals(MqlTick &data[])
   {
      int index = 1;
      const int size = ArraySize(data);
      if(size < 3return size;
      
      for(int i = 1i < size - 2i++)
      {
         if((data[i].bid < data[i - 1].bid && data[i].bid < data[i + 1].bid)
         || (data[i].ask > data[i - 1].ask && data[i].ask > data[i + 1].ask))
         {
            data[index++] = data[i];
         }
      }
      
      return ArrayResize(dataindex);
   }

我们依次为 EURUSD 创建几种分时报价密度降低模式的自定义交易品种,并比较它们的性能,即“压缩”程度、测试速度以及EA 交易交易性能的变化。

例如,精简分时报价序列会产生以下结果(在 MQ Demo 上一年半的历史)。

   Create new custom symbol 'EURUSD.TckFltr-SE'?
   Fixing SYMBOL_TRADE_TICK_VALUE: 0.0 <<< 1.0
   true  SYMBOL_TRADE_TICK_VALUE 1.0 -> SUCCESS (0)
   Fixing SYMBOL_TRADE_TICK_SIZE: 0.0 <<< 1e-05
   true  SYMBOL_TRADE_TICK_SIZE 1e-05 -> SUCCESS (0)
   Number of found discrepancies: 2
   Fixed
   Done ticks - read: 31553509, written: 16927376, ratio: 53.6%

对于平滑波动和分形模式,指标是不同的:

   EURUSD.TckFltr-FL will be updated
   Done ticks - read: 31568782, written: 22205879, ratio: 70.3%
   ...   
   Create new custom symbol 'EURUSD.TckFltr-FR'?
   ...
   Done ticks - read: 31569519, written: 12732777, ratio: 40.3%

对于基于压缩分时报价的实际交易实验,我们需要 EA 交易。我们采用 BandOsMATicks.mq5 的改编版本,与 原始版本相比,其中启用了每项分时报价的交易(在 SimpleStrategy::trade 方法中禁用了 if(lastBar == iTime(_Symbol, _Period, 0)) return false; 行),并且信号指标的值取自柱线 0 和 1(以前只有已完成的柱线 1 和 2)。

我们使用从 2021 年初到 2022 年 6 月 1 日的日期范围运行 EA 交易。设置附加在 MQL5/Presets/MQL5Book/BandOsMAticks.set 文件中。所有模式下余额曲线的总体行为都非常相似。

不同模式下按分时报价划分的测试余额组合图表

不同模式下按分时报价划分的测试余额组合图表

不同曲线等效极值的水平偏移是由于标准报告图表使用交易数量而不是时间作为水平坐标,而交易数量由于不同分时报价基础上交易信号触发的准确性而有所不同。

性能指标的差异如下表所示(N - 交易数量,$ - 利润,PF - 盈利因子,RF - 恢复因子,DD - 回撤):

模式

分时报价

时间 
mm:ss.msec

内存

N

$

PF

RF

DD

真实

31002919

02:45.251

835 Mb

962

166.24

1.32

2.88

54.99

模拟

25808139

01:58.131

687 Mb

928

171.94

1.34

3.44

47.64

OHLC M1

2084820

00:11.094

224 Mb

856

193.52

1.39

3.97

46.55

序列

16310236

01:24.784

559 Mb

860

168.95

1.34

2.92

55.16

快速震荡

21362616

01:52.172

623 Mb

920

179.75

1.37

3.60

47.28

分形

12270854

01:04.756

430 Mb

866

142.19

1.27

2.47

54.80

我们将认为基于真实分时报价的测试是最可靠的,并通过其与此测试的接近程度来评估其余测试。显然,由于准确性的显著损失(未考虑开盘价模式),OHLC M1 模式显示出最高的速度和较低的资源成本。它表现出过于乐观的财务结果。

在三种人为压缩分时报价的模式中,“序列”模式在指标集方面最接近真实模式。“序列”模式在时间上比真实模式快 2 倍,在内存消耗方面效率高 1.5 倍。“快速震荡”模式似乎更好地保留了原始交易数量。速度最快且内存需求最低的分形模式,当然比 OHLC M1 模式花费更多的时间和资源,但它不会高估交易得分。

请记住,对于不同的交易策略、金融工具,甚至特定经纪商的分时报价历史,分时报价缩减算法可能会有不同的工作方式,或者相反,会产生较差的结果。请在你的工作环境中使用 EA 交易进行研究。

作为使用自定义交易品种的第二个示例,我们考虑一下使用 CustomTicksAdd 进行分时报价转换所提供的一个有趣功能。

许多交易者使用交易面板,即带有交互式控件的程序,用于手动执行任意交易操作。你必须主要在线练习使用它们,因为测试程序会施加一些限制。首先,测试程序不支持图表事件和对象。这会导致控件停止工作。此外,在测试程序中,你不能对对图形标记应用任意对象。

我们尝试解决这些问题。

我们可以基于历史分时报价以慢动作生成自定义交易品种。然后,此类交易品种的图表将成为可视化测试程序的类似物。

这种方法有几个优点:

  • 所有图表事件的标准行为
  • 指标的交互式应用和设置
  • 对象的交互式应用和调整
  • 动态切换时间范围
  • 测试历史数据直至当前时间,包括今天(标准测试器不允许测试今天)

关于最后一点,我们注意到 MetaTrader 5 的开发人员故意禁止检查最后(当前)一天的交易,尽管有时需要快速查找错误(在代码中或交易策略中)。

动态修改价格(例如增加点差)也具有潜在的趣味性。

基于此类自定义交易品种的图表,我们稍后可以基于历史数据实现手动交易模拟器。

交易品种生成器将是非交易性 EA 交易 CustomTester.mq5。在其输入参数中,我们将提供新自定义交易品种在交易品种层级结构中的位置指示、用于分时报价转换(和构建自定义交易品种行情)的过去开始日期,以及图表的时间周期,该图表将自动打开以进行可视化测试。

input string CustomPath = "MQL5Book\\Part7"// Custom Symbol Folder
input datetime _Start;                       // Start (120-day indent by default)
input ENUM_TIMEFRAMES Timeframe = PERIOD_H1;

新交易品种的名称由当前图表的交易品种名称和 ".Tester" 后缀构成。

string CustomSymbol = _Symbol + ".Tester";

如果参数中未指定开始日期,EA 交易将从当前日期向后缩进 120 天。

const uint DailySeconds = 60 * 60 * 24;
datetime Start = _Start == 0 ? TimeCurrent() - DailySeconds * 120 : _Start;

分时报价将从工作交易品种的真实分时报价历史中一次性读取一整天的数据。正在读取的日期的指针存储在 Cursor 变量中。

bool FirstCopy = true;
// additionally 1 day ago, because otherwise, the chart will not update immediately
datetime Cursor = (Start / DailySeconds - 1) * DailySeconds// round off at the border of the day

要重现的全天分时报价将在 Ticks 数组中请求,然后从那里以 step 大小小批量转换到自定义交易品种的图表。

MqlTick Ticks[];       // ticks for the "current" day in the past
int Index = 0;         // position in ticks within a day
int Step = 32;         // fast forward 32 ticks at a time (default)
int StepRestore = 0;   // remember the speed for the duration of the pause
long Chart = 0;        // created custom symbol chart
bool InitDone = false// sign of completed initialization

为了以恒定速率播放分时报价,我们在 OnInit 中启动计时器。

void OnInit()
{
   EventSetMillisecondTimer(100);
}
   
void OnTimer()
{
   if(!GenerateData())
   {
      EventKillTimer();
   }
}

分时报价将由 GenerateData 函数生成。启动后,当 InitDone 标志被重置时,我们将尝试创建一个新交易品种,或者如果自定义交易品种已存在,则清除旧的行情和分时报价。

bool GenerateData()
{
   if(!InitDone)
   {
      bool custom = false;
      if(PRTF(SymbolExist(CustomSymbolcustom)) && custom)
      {
         if(IDYES == MessageBox(StringFormat("Clean up existing custom symbol '%s'?",
            CustomSymbol), "Please, confirm"MB_YESNO))
         {
            PRTF(CustomRatesDelete(CustomSymbol0LONG_MAX));
            PRTF(CustomTicksDelete(CustomSymbol0LONG_MAX));
            Sleep(1000);
            MqlRates rates[1];
            MqlTick tcks[];
            if(PRTF(CopyRates(CustomSymbolPERIOD_M101rates)) == 1
            || PRTF(CopyTicks(CustomSymboltcks) > 0))
            {
               Alert("Can't delete rates and Ticks, internal error");
               ExpertRemove();
            }
         }
         else
         {
            return false;
         }
      }
      else
      if(!PRTF(CustomSymbolCreate(CustomSymbolCustomPath_Symbol)))
      {
         return false;
      }
      ... // (A)

此时,我们将在 (A) 处省略一些内容,稍后再回到这一点。

创建交易品种后,我们在Market Watch窗口中将其选中并为其打开一个图表。

 SymbolSelect(CustomSymboltrue);
      Chart = ChartOpen(CustomSymbolTimeframe);
      ... // (B)
      ChartSetString(ChartCHART_COMMENT"Custom Tester");
      ChartSetInteger(ChartCHART_SHOW_OBJECT_DESCRtrue);
      ChartRedraw(Chart);
      InitDone = true;
   }
   ...

这里也缺少几行 (B);它们与未来的改进有关,但目前还不是必需的。

如果交易品种已经创建,我们开始以 Step 个分时报价为一批次广播分时报价,但不能超过 256 个。此限制与 CustomTicksAdd 函数的某些细节有关。

   else
   {
      for(int i = 0i <= (Step - 1) / 256; ++i)
      if(Step > 0 && !GenerateTicks())
      {
         return false;
      }
   }
   return true;
}

辅助函数 GenerateTicksStep 个(但不能超过 256 个)分时报价为一批次广播分时报价,通过偏移量 Index 从每日数组 Ticks 中读取它们。当数组为空或我们已将其读到末尾时,我们通过调用 FillTickBuffer 请求第二天的分时报价。

bool GenerateTicks()
{
   if(Index >= ArraySize(Ticks)) // daily array is empty or read to the end
   {
      if(!FillTickBuffer()) return false// fill the array with ticks per day
   }
   
   const int m = ArraySize(Ticks);
   MqlTick array[];
   const int n = ArrayCopy(arrayTicks0Indexfmin(fmin(Step256), m));
   if(n <= 0return false;
   
   ResetLastError();
   if(CustomTicksAdd(CustomSymbolarray) != ArraySize(array) || _LastError != 0)
   {
      Print(_LastError); // in case of ERR_CUSTOM_TICKS_WRONG_ORDER (5310)
      ExpertRemove();
   }
   Comment("Speed: ", (string)Step" / "STR_TIME_MSC(array[n - 1].time_msc));
   Index += Step// move forward by 'Step' ticks
   return true;
}

FillTickBuffer 函数使用 CopyTicksRange 进行操作。

bool FillTickBuffer()
{
   int r;
   ArrayResize(Ticks0);
   do
   {
      r = PRTF(CopyTicksRange(_SymbolTicksCOPY_TICKS_ALLCursor * 1000L,
         (Cursor + DailySeconds) * 1000L - 1));
      if(r > 0 && FirstCopy)
      {
         // NB: this pre-call is only needed to display the chart
         // from "Waiting for update" state
         PRTF(CustomTicksReplace(CustomSymbolCursor * 1000L,
            (Cursor + DailySeconds) * 1000L - 1Ticks));
         FirstCopy = false;
         r = 0;
      }
      Cursor += DailySeconds;
   }
   while(r == 0 && Cursor < TimeCurrent()); // skip non-trading days
   Index = 0;
   return r > 0;
}

当 EA 交易停止时,我们还将关闭相关的图表(以便在下次启动时不会重复)。

void OnDeinit(const int)
{
   if(Chart != 0)
   {
      ChartClose(Chart);
   }
   Comment("");
}

至此,EA 交易可以被认为是完整的了,但是存在一个问题。问题在于,由于某种原因,自定义交易品种的特性不会从原始工作交易品种中“按原样”复制,至少在 MQL5 API 的当前实现中是这样。这甚至适用于非常重要的特性,例如 SYMBOL_TRADE_TICK_VALUE、SYMBOL_TRADE_TICK_SIZE。如果在调用 CustomSymbolCreate(CustomSymbol, CustomPath, _Symbol) 后立即打印这些特性的值,我们将在那里看到零。

为了组织特性的检查、比较以及必要时更正,我们编写了一个特殊的类 CustomSymbolMonitor (CustomSymbolMonitor.mqh),它派生自 SymbolMonitor。你可以自行研究其内部结构,而这里我们仅介绍公共接口。

构造函数允许你创建一个自定义交易品种监视器,指定一个示例工作交易品种(通过字符串中的名称,或从 SymbolMonitor 对象),该交易品种用作设置的来源。

class CustomSymbolMonitorpublic SymbolMonitor
{
public:
   CustomSymbolMonitor(); // sample - _Symbol
   CustomSymbolMonitor(const string sconst SymbolMonitor *m = NULL);
   CustomSymbolMonitor(const string sconst string other);
   
   //set/replace sample symbol   
   void inherit(const SymbolMonitor &m);
   
   // copy all properties from the sample symbol in forward or reverse order
   bool setAll(const bool reverseOrder = trueconst int limit = UCHAR_MAX);
   
   // check all properties against the sample, return the number of corrections
   int verifyAll(const int limit = UCHAR_MAX);
   
   // check the specified properties with the sample, return the number of corrections
   int verify(const int &properties[]);
   
   // copy the given properties from the sample, return true if they all applied
   bool set(const int &properties[]);
   
   // copy the specific property from the sample, return true if applied
   template<typename E>
   bool set(const E e);
   
   bool set(const ENUM_SYMBOL_INFO_INTEGER propertyconst long valueconst
   {
      return CustomSymbolSetInteger(namepropertyvalue);
   }
   
   bool set(const ENUM_SYMBOL_INFO_DOUBLE propertyconst double valueconst
   {
      return CustomSymbolSetDouble(namepropertyvalue);
   }
   
   bool set(const ENUM_SYMBOL_INFO_STRING propertyconst string valueconst
   {
      return CustomSymbolSetString(namepropertyvalue);
   }
};

由于自定义交易品种与标准交易品种不同,允许你设置自己的特性,因此向该类添加了三个 set 方法。特别是,它们用于批量传输示例的特性并在其他类方法中检查这些操作的成功性。

我们现在可以回到自定义交易品种生成器及其源代码片段,如前面注释 (A) 所示。

      // (A) check important properties and set them in "manual" mode
      SymbolMonitor sm// _Symbol
      CustomSymbolMonitor csm(CustomSymbol, &sm);
      int props[] = {SYMBOL_TRADE_TICK_VALUESYMBOL_TRADE_TICK_SIZE};
      const int d1 = csm.verify(props); // check and try to fix
      if(d1)
      {
         Print("Number of found discrepancies: "d1); // number of edits
         if(csm.verify(props)) // check again
         {
            Alert("Custom symbol can not be created, internal error!");
            return false// symbol cannot be used without successful edits
         }
         Print("Fixed");
      }

现在你可以运行 CustomTester.mq5 EA 交易,并观察报价如何在自动打开的图表中动态形成,以及分时报价如何从历史转发到Market Watch窗口。

但是,这是以每 0.1 秒 32 个分时报价的恒定速率完成的。理想情况下,应根据用户的请求动态更改播放速度,既可以加快也可以减慢。例如,可以通过键盘来实现这种控制。

因此,你需要添加 OnChartEvent 处理程序。我们知道,对于 CHARTEVENT_KEYDOWN 事件,程序在 lparam 参数中接收按下键的代码,我们将其传递给 CheckKeys 函数(见下文)。与 (B) 密切相关的片段 (C) 不得不暂时推迟,我们稍后会回到它。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   ... // (C)
   if(id == CHARTEVENT_KEYDOWN// these events only arrive while the chart is active!
   {
      CheckKeys(lparam);
   }
}

CheckKeys 函数中,我们正在处理“向上箭头”和“向下箭头”键以增加和减少播放速度。此外,“暂停”键允许你完全暂停“测试”(分时报价传输)的过程。再次按下“暂停”键将以相同的速度恢复工作。

void CheckKeys(const long key)
{
   if(key == VK_DOWN)
   {
      Step /= 2;
      if(Step > 0)
      {
         Print("Slow down: "Step);
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
      else
      {
         Print("Paused");
         ChartSetString(ChartCHART_COMMENT"Paused");
         ChartRedraw(Chart);
      }
   }
   else if(key == VK_UP)
   {
      if(Step == 0)
      {
         Step = 1;
         Print("Resumed");
         ChartSetString(ChartCHART_COMMENT"Resumed");
      }
      else
      {
         Step *= 2;
         Print("Speed up: "Step);
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
   }
   else if(key == VK_PAUSE)
   {
      if(Step > 0)
      {
         StepRestore = Step;
         Step = 0;
         Print("Paused");
         ChartSetString(ChartCHART_COMMENT"Paused");
         ChartRedraw(Chart);
      }
      else
      {
         Step = StepRestore;
         Print("Resumed");
         ChartSetString(ChartCHART_COMMENT"Speed: " + (string)Step);
      }
   }
}

在首先确保 EA 交易工作的图表处于活动状态后,可以实际测试新代码。回想一下,键盘事件仅发送到活动窗口。这是我们测试程序的另一个问题。

由于用户必须在自定义交易品种图表上执行交易操作,因此生成器窗口几乎总是位于后台。要想切换到生成器窗口以暂时停止分时报价流然后恢复,这种做法不切实际。因此,需要以某种方式直接从自定义交易品种窗口通过键盘组织交互式控制。

为此,适合使用一个特殊的指标,我们可以自动将其添加到打开的自定义交易品种窗口中。该指标将在其自己的窗口(带有自定义交易品种的窗口)中拦截键盘事件,并将这些事件发送到生成器窗口。

指标的源代码附在 KeyboardSpy.mq5 文件中。当然,该指标没有图表。一对输入参数专用于获取图表 ID HostID(应向其发送消息)和自定义事件代码 EventID(交互式事件将打包到其中)。

#property indicator_chart_window
#property indicator_plots 0
   
input long HostID;
input ushort EventID;

主要工作在 OnChartEvent 处理程序中完成。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      EventChartCustom(HostIDEventIDlparam,
         // this is always 0 when inside iCustom
         (double)(ushort)TerminalInfoInteger(TERMINAL_KEYSTATE_CONTROL),
         sparam);
   }
}

请注意,我们选择的所有“热键”都是简单的,也就是说,它们不使用带有键盘状态键(例如 CtrlShift)的快捷方式。这是强制执行的,因为在以编程方式创建的指标(具体而言是通过 iCustom)内部,无法读取键盘状态。换句话说,调用 TerminalInfoInteger(TERMINAL_KEYSTATE_XYZ) 总是返回 0。在上面的处理程序中,我们添加它只是出于演示目的,这样的话,如果你愿意,可以通过在“接收端”显示传入参数来验证此限制。

但是,单个箭头和暂停点击将正常传输到父图表,这对我们来说已经足够了。剩下的唯一事情就是将指标与 EA 交易集成。

在前面跳过的片段 (B) 中,在生成器初始化期间,我们将创建一个指标并将其添加到自定义交易品种图表中。

#define EVENT_KEY 0xDED // custom event
      ...
      // (B)
      const int handle = iCustom(CustomSymbolTimeframe"MQL5Book/p7/KeyboardSpy",
         ChartID(), EVENT_KEY);
      ChartIndicatorAdd(Chart0handle);

接下来,在片段 (C) 中,我们将确保从指标接收用户消息并将其传输到已知的 CheckKeys 函数。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   // (C)
   if(id == CHARTEVENT_CUSTOM + EVENT_KEY// notifications from the dependent chart when it is active
   {
      CheckKeys(lparam); // "remote" processing of key presses
   }
   else if(id == CHARTEVENT_KEYDOWN// these events are only fired while the chart is active!
   {
      CheckKeys(lparam); // standard processing
   }
}

因此,现在可以在带有 EA 交易的图表和由其生成的自定义交易品种的图表上控制播放速度。

使用新的工具包,你可以尝试与“过去活动”的图表进行交互式工作。图表上会显示带有当前播放速度或暂停标记的注释。

在带有 EA 交易的图表上,注释中会显示“当前”广播分时报价的时间。

一个重现真实交易品种分时报价(和行情)历史的 EA 交易

一个重现真实交易品种分时报价(和行情)历史的 EA 交易

用户在此窗口中基本没有可以执行的操作(除非删除 EA 交易并停止自定义交易品种生成)。分时报价转换过程本身在此处不可见。此外,由于 EA 交易会自动打开一个自定义交易品种图表(其中历史行情会更新),因此正是这个图表变为活动状态。为了获得上面的屏幕截图,我们特别需要短暂切换到原始图表。

因此,我们回到自定义交易品种的图表。它在过去平稳渐进地更新的方式已经很棒了,但是你无法在其上进行交易实验。例如,如果你在其上运行常用的交易面板,其控件虽然在形式上可以工作,但不会执行交易,因为自定义交易品种在服务器上不存在,因此你会收到错误。在任何未专门针对自定义交易品种进行调整的程序中都会观察到此特性。我们展示一个如何虚拟化自定义交易品种交易的示例。

为了简化示例(但不失一般性),我们将以最简单的 EA 交易 CustomOrderSend.mq5 为基础,而不是交易面板,该 EA 交易可以通过按键执行多种交易操作:

  • 'B' – 市价买入
  • 'S' – 市价卖出
  • 'U' – 下达限价买单
  • 'L' – 下达限价卖单
  • 'C' – 全部平仓
  • 'D' – 删除所有订单
  • 'R' – 将交易报告输出到日志

在 EA 交易输入参数中,我们将设置单笔交易的交易量(默认为最小手数)以及止损和止盈水平的点数距离。

input double Volume;           // Volume (0 = minimal lot)
input int Distance2SLTP = 0;   // Distance to SL/TP in points (0 = no)
   
const double Lot = Volume == 0 ? SymbolInfoDouble(_SymbolSYMBOL_VOLUME_MIN) : Volume;

如果 Distance2SLTP 等于零,则市价单中不设置保护水平,也不形成挂单。当 Distance2SLTP 具有非零值时,它用作下挂单时与当前价格的距离(向上或向下,取决于命令)。

考虑到先前从 MqlTradeSync.mqh中介绍的类,上述逻辑转换为以下源代码。

#include <MQL5Book/MqlTradeSync.mqh>
   
#define KEY_B 66
#define KEY_C 67
#define KEY_D 68
#define KEY_L 76
#define KEY_R 82
#define KEY_S 83
#define KEY_U 85
   
void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      MqlTradeRequestSync request;
      const double ask = SymbolInfoDouble(_SymbolSYMBOL_ASK);
      const double bid = SymbolInfoDouble(_SymbolSYMBOL_BID);
      const double point = SymbolInfoDouble(_SymbolSYMBOL_POINT);
   
      switch((int)lparam)
      {
      case KEY_B:
         request.buy(Lot0,
            Distance2SLTP ? ask - point * Distance2SLTP : Distance2SLTP,
            Distance2SLTP ? ask + point * Distance2SLTP : Distance2SLTP);
         break;
      case KEY_S:
         request.sell(Lot0,
            Distance2SLTP ? bid + point * Distance2SLTP : Distance2SLTP,
            Distance2SLTP ? bid - point * Distance2SLTP : Distance2SLTP);
         break;
      case KEY_U:
         if(Distance2SLTP)
         {
            request.buyLimit(Lot, ask - point * Distance2SLTP);
         }
         break;
      case KEY_L:
         if(Distance2SLTP)
         {
            request.sellLimit(Lot, bid + point * Distance2SLTP);
         }
         break;
      case KEY_C:
         for(int i = PositionsTotal() - 1i >= 0i--)
         {
            request.close(PositionGetTicket(i));
         }
         break;
      case KEY_D:
         for(int i = OrdersTotal() - 1i >= 0i--)
         {
            request.remove(OrderGetTicket(i));
         }
         break;
      case KEY_R:
 // there should be something here...
         break;
      }
   }
}

正如我们所见,这里同时使用了标准的交易 API 函数和 MqlTradeRequestSync 方法。后者间接地最终也会调用许多内置函数。我们需要让这个 EA 交易与自定义交易品种进行交易。

最简单但耗时的想法是用它们自己的类似功能替换所有标准函数,这些类似功能会在某些结构体中计算订单、交易、仓位和财务统计数据。当然,必须拥有需要调整的 EA 交易源代码才能实现。

该方法的实验性实现在附加文件 CustomTrade.mqh 中进行了演示。你可以自行熟悉完整的代码,因为在本书的框架内,我们仅列出要点。

首先,我们注意到许多计算是以简化形式进行的,不支持许多模式,并且未对数据的正确性进行完整检查。请将源代码用作你自己开发的起点。

整个代码都包含在 CustomTrade 命名空间中,以避免冲突。

订单、交易和仓位实体被形式化为相应的类 CustomOrderCustomDealCustomPosition。它们都是 MonitorInterface<I,D,S> ::TradeState类的继承者。回想一下,这个类已经自动支持为每种类型的对象及其特定的枚举三元组形成整数、实数和字符串特性的数组。例如,CustomOrder 看起来是这样的:

class CustomOrderpublic MonitorInterface<ENUM_ORDER_PROPERTY_INTEGER,
   ENUM_ORDER_PROPERTY_DOUBLE,ENUM_ORDER_PROPERTY_STRING>::TradeState
{
   static long ticket// order counter and ticket provider
   static int done;    // counter of executed (historical) orders
public:
   CustomOrder(const ENUM_ORDER_TYPE typeconst double volumeconst string symbol)
   {
      _set(ORDER_TYPEtype);
      _set(ORDER_TICKET, ++ticket);
      _set(ORDER_TIME_SETUPSymbolInfoInteger(symbolSYMBOL_TIME));
      _set(ORDER_TIME_SETUP_MSCSymbolInfoInteger(symbolSYMBOL_TIME_MSC));
      if(type <= ORDER_TYPE_SELL)
      {
         // TODO: no deferred execution yet
         setDone(ORDER_STATE_FILLED);
      }
      else
      {
         _set(ORDER_STATEORDER_STATE_PLACED);
      }
      
      _set(ORDER_VOLUME_INITIALvolume);
      _set(ORDER_VOLUME_CURRENTvolume);
      
      _set(ORDER_SYMBOLsymbol);
   }
   
   void setDone(const ENUM_ORDER_STATE state)
   {
      const string symbol = _get<string>(ORDER_SYMBOL);
      _set(ORDER_TIME_DONESymbolInfoInteger(symbolSYMBOL_TIME));
      _set(ORDER_TIME_DONE_MSCSymbolInfoInteger(symbolSYMBOL_TIME_MSC));
      _set(ORDER_STATEstate);
      ++done;
   }
   
   bool isActive() const
   {
      return _get<long>(ORDER_TIME_DONE) == 0;
   }
   
   static int getDoneCount()
   {
      return done;
   }
};

请注意,在旧的“当前”时间的虚拟环境中,你不能使用 TimeCurrent 函数,而是使用自定义交易品种的最后已知时间 SymbolInfoInteger(symbol, SYMBOL_TIME)

在虚拟交易期间,当前对象及其历史会累积在相应类的数组中。

AutoPtr<CustomOrderorders[];
CustomOrder *selectedOrders[];
CustomOrder *selectedOrder = NULL;
AutoPtr<CustomDealdeals[];
CustomDeal *selectedDeals[];
CustomDeal *selectedDeal = NULL;
AutoPtr<CustomPositionpositions[];
CustomPosition *selectedPosition = NULL;

选择订单、交易和仓位的隐喻是为了在内置函数中模拟类似的方法。对于它们,CustomTrade 命名空间中有重复项,这些重复项使用宏替换指令替换了原始项。

#define HistorySelect CustomTrade::MT5HistorySelect
#define HistorySelectByPosition CustomTrade::MT5HistorySelectByPosition
#define PositionGetInteger CustomTrade::MT5PositionGetInteger
#define PositionGetDouble CustomTrade::MT5PositionGetDouble
#define PositionGetString CustomTrade::MT5PositionGetString
#define PositionSelect CustomTrade::MT5PositionSelect
#define PositionSelectByTicket CustomTrade::MT5PositionSelectByTicket
#define PositionsTotal CustomTrade::MT5PositionsTotal
#define OrdersTotal CustomTrade::MT5OrdersTotal
#define PositionGetSymbol CustomTrade::MT5PositionGetSymbol
#define PositionGetTicket CustomTrade::MT5PositionGetTicket
#define HistoryDealsTotal CustomTrade::MT5HistoryDealsTotal
#define HistoryOrdersTotal CustomTrade::MT5HistoryOrdersTotal
#define HistoryDealGetTicket CustomTrade::MT5HistoryDealGetTicket
#define HistoryOrderGetTicket CustomTrade::MT5HistoryOrderGetTicket
#define HistoryDealGetInteger CustomTrade::MT5HistoryDealGetInteger
#define HistoryDealGetDouble CustomTrade::MT5HistoryDealGetDouble
#define HistoryDealGetString CustomTrade::MT5HistoryDealGetString
#define HistoryOrderGetDouble CustomTrade::MT5HistoryOrderGetDouble
#define HistoryOrderGetInteger CustomTrade::MT5HistoryOrderGetInteger
#define HistoryOrderGetString CustomTrade::MT5HistoryOrderGetString
#define OrderSend CustomTrade::MT5OrderSend
#define OrderSelect CustomTrade::MT5OrderSelect
#define HistoryOrderSelect CustomTrade::MT5HistoryOrderSelect
#define HistoryDealSelect CustomTrade::MT5HistoryDealSelect

例如,这是 MT5HistorySelectByPosition 函数的实现方式。

bool MT5HistorySelectByPosition(long id)
{
   ArrayResize(selectedOrders0);
   ArrayResize(selectedDeals0);
  
   for(int i = 0i < ArraySize(orders); i++)
   {
      CustomOrder *ptr = orders[i][];
      if(!ptr.isActive())
      {
         if(ptr._get<long>(ORDER_POSITION_ID) == id)
         {
            PUSH(selectedOrdersptr);
         }
      }
   }
   
   for(int i = 0i < ArraySize(deals); i++)
   {
      CustomDeal *ptr = deals[i][];
      if(ptr._get<long>(DEAL_POSITION_ID) == id)
      {
         PUSH(selectedDealsptr);
      }
   }
   return true;

可以看到,该组的所有函数都带有 MT5 前缀,因此它们的双重用途立即清晰可见,并且很容易将它们与第二组函数区分开来。

CustomTrade 命名空间中的第二组函数执行实用操作:检查和更新订单、交易和仓位的状态,根据情况创建新对象并删除旧对象。具体来说,它们包括 CheckPositionsCheckOrders 函数,这些函数可以在计时器上调用或响应用户操作。但是,如果你使用其他几个旨在显示虚拟交易账户当前和历史状态的函数,则可以不这样做:

  • string ReportTradeState() 返回一个多行文本,其中包含未平仓仓位和已下订单的列表
  • void PrintTradeHistory() 在日志中显示订单和交易的历史

这些函数独立调用 CheckPositionsCheckOrders 以向你提供最新信息。

此外,还有一个函数用于以对象形式在图表上可视化仓位和有效订单:DisplayTrades

头文件 CustomTrade.mqh 应包含在 EA 交易中,位于其他头文件之前,以便宏替换对所有后续的源代码行都有效。

#include <MQL5Book/CustomTrade.mqh>
#include <MQL5Book/MqlTradeSync.mqh>

现在,上述算法 CustomOrderSend.mq5 可以在基于当前自定义交易品种的虚拟环境中开始“交易”(不需要服务器或标准测试器),而无需任何额外更改。

为了快速显示状态,我们将启动第二个计时器并定期更改注释,以及显示图形对象。

int OnInit()
{
   EventSetTimer(1);
   return INIT_SUCCEEDED;
}
   
void OnTimer()
{
   Comment(CustomTrade::ReportTradeState());
   CustomTrade::DisplayTrades();
}

为了通过按“R”构建报告,我们添加 OnChartEvent 处理程序。

void OnChartEvent(const int idconst long &lparamconst double &dparamconst string &sparam)
{
   if(id == CHARTEVENT_KEYDOWN)
   {
      switch((int)lparam)
      {
      ...
      case KEY_R:
         CustomTrade::PrintTradeHistory();
         break;
      }
   }
}

最后,一切准备就绪,可以实际测试新的软件包了。

在 EURUSD 上运行自定义交易品种生成器 CustomTester.mq5。在打开的 "EURUSD.Tester" 图表上,运行 CustomOrderSend.mq5 并开始交易。以下是测试过程的图片。

自定义交易品种图表上的虚拟交易

自定义交易品种图表上的虚拟交易

在这里你可以看到两个未平仓的多头仓位(带有保护水平)和一个挂起的卖出限价订单。

一段时间后,其中一个仓位被平仓(下面用带箭头的蓝色虚线表示),一个挂起的卖出订单被触发(带箭头的红色实线),从而产生以下图片。

自定义交易品种图表上的虚拟交易

自定义交易品种图表上的虚拟交易

在平掉所有仓位(一些通过止盈,其余通过用户命令)后,通过按 'R' 发出生成报告的命令。

History Orders:

(1) #1 ORDER_TYPE_BUY 2022.02.15 01:20:50 -> 2022.02.15 01:20:50 L=0.01 @ 1.1306

(4) #2 ORDER_TYPE_SELL_LIMIT 2022.02.15 02:34:29 -> 2022.02.15 18:10:17 L=0.01 @ 1.13626 [sell limit]

(2) #3 ORDER_TYPE_BUY 2022.02.15 10:08:20 -> 2022.02.15 10:08:20 L=0.01 @ 1.13189

(3) #4 ORDER_TYPE_BUY 2022.02.15 15:01:26 -> 2022.02.15 15:01:26 L=0.01 @ 1.13442

(1) #5 ORDER_TYPE_SELL 2022.02.15 15:35:43 -> 2022.02.15 15:35:43 L=0.01 @ 1.13568

(2) #6 ORDER_TYPE_SELL 2022.02.16 09:39:17 -> 2022.02.16 09:39:17 L=0.01 @ 1.13724

(4) #7 ORDER_TYPE_BUY 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13748

(3) #8 ORDER_TYPE_SELL 2022.02.16 23:31:15 -> 2022.02.16 23:31:15 L=0.01 @ 1.13742

Deals:

(1) #1 [#1] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 01:20:50 L=0.01 @ 1.1306 = 0.00

(2) #2 [#3] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 10:08:20 L=0.01 @ 1.13189 = 0.00

(3) #3 [#4] DEAL_TYPE_BUY DEAL_ENTRY_IN 2022.02.15 15:01:26 L=0.01 @ 1.13442 = 0.00

(1) #4 [#5] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.15 15:35:43 L=0.01 @ 1.13568 = 5.08 [tp]

(4) #5 [#2] DEAL_TYPE_SELL DEAL_ENTRY_IN 2022.02.15 18:10:17 L=0.01 @ 1.13626 = 0.00

(2) #6 [#6] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 09:39:17 L=0.01 @ 1.13724 = 5.35 [tp]

(4) #7 [#7] DEAL_TYPE_BUY DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13748 = -1.22

(3) #8 [#8] DEAL_TYPE_SELL DEAL_ENTRY_OUT 2022.02.16 23:31:15 L=0.01 @ 1.13742 = 3.00

Total: 12.21, Trades: 4

圆括号表示仓位标识符,方括号表示相应交易的订单凭证(两种类型的订单号前都有一个井号 '#')。

这里不考虑掉期和佣金。可以添加对掉期和佣金的计算。

我们将在 自定义交易品种交易细节一节中探讨另一个示例,该示例使用自定义交易品种分时报价。我们将讨论如何创建等量图。