English Русский Español Deutsch 日本語 Português
如何基于HTML和CSV报表可视化多币种交易历史

如何基于HTML和CSV报表可视化多币种交易历史

MetaTrader 5指标 | 24 五月 2019, 08:57
2 684 0
Stanislav Korotky
Stanislav Korotky

自推出以来,MetaTrader 5提供了多货币测试选项,也许交易者经常使用这个功能。然而,这种功能并不是万能的。特别是,在运行测试之后,用户可以打开一个带有已执行交易操作的图表,但这只是策略测试人员设置中选择的一个交易品种的图表,测试后无法查看所有使用的交易品种的整个交易历史,而目测检查并不总是有效的,测试后一段时间可能需要进行额外的分析。此外,还可以由其他人提供报告。因此,一个基于HTML测试报告的可视化多个交易品种交易的工具将非常有用。

此任务与另一个类似的 MetaTrader 应用程序密切相关,MQL5.com 上的许多交易信号涉及多货币交易,将带有信号历史的 CSV 文件显示在图表中比较方便。

让我们开发一个能够执行上述功能的指标。

为了对多个工作交易品种进行并行分析,将在图表子窗口中创建多个指标实例(每个交易品种一个)。主要图形对象将是所选交易品种(通常与图表交易品种不同)的“报价”,与主窗口中的柱同步。与交易订单(头寸)相对应的趋势线将应用于这些“报价”。

还有一种替代方法:交易显示在主窗口中,但在这种情况下,图表上只分析一个交易品种。这种方法需要另一个没有缓冲区的指标,可以切换到报告中包含的任何交易品种。

之前的文章对基于 CSS 选择器的 HTML 解析器做了描述[1],解析器从HTML报告中提取交易列表,根据该列表我们可以进行交易(图形对象)。从信号部分解析 CSV 文件比较容易,而内置的 MQL 函数支持 MetaTrader 4(*.history.csv)和 MetaTrader 5(*.positions.csv)信号的文件格式。

SubChart 指标

实现的第一步是创建一个简单的指标,它在任何图表的子窗口中显示外部交易品种的“报价”,这将是 SubChart 指标。

为了使用 OHLC (开盘价,最高价,最低价,收盘价) 的数值显示数据, MQL 提供了多种显示样式,包括 DRAW_CANDLESDRAW_BARS,它们中的每一种都有四个指标缓冲区。我们将提供这两个选项的支持,而不是选择其中一种样式。将根据当前窗口设置动态选择样式。“图表设置”中的“常用”选项卡下有一组单选按钮:“条形图”、“日式烛形”和“折线图”。为了快速访问,可以使用与工具栏上的按钮相同的按钮。可以使用以下调用从 MQL 获取设置:

(ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE)

ENUM_CHART_MODE 枚举中包含了相同目标的元素: CHART_CANDLES, CHART_BARS, CHART_LINE.

最后一个 CHART_LINE 点的支持也将在指标中实现。这样,当用户界面中的显示模式切换时,指标视图将根据主窗口进行更改。DRAW_LINE 适合用于最后一种模式;它使用了一个缓冲区。

让我们开始实现. 声明指标缓冲区的数量和显示图形对象的类型:

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1

加上输入变量:

input string SubSymbol = ""; // 交易品种
input bool Exact = false;

使用 SubSymbol,可以为指标设置主窗口中当前交易品种以外的任何交易品种。

Exact 参数确定主窗口中的柱没有完全相同时间的另一个交易品种的匹配柱的情况下的操作。这个参数将在 iBarShift 函数调用中使用,其视觉效果如下:

  • 如果“Exact” 等于 “false”,则函数返回最接近指定时间的最接近合适的柱数,因此如果没有报价(由于假日或其他原因),则指标将显示上一个柱;
  • 如果 Exact = true, 函数返回 -1, 这样的画,指标图表在这个位置将是空白;

SubSymbol 参数默认等于空字符串,这表示报价与主窗口中显示的是一样的。在这种情况下,您应当编辑实际变量的值,把它设为 _Symbol. 视觉测试但是,由于“输入参数”是MQL中的只读变量,因此我们必须输入中间变量“Symbol”,并将其填充到OnInit处理程序中。

string Symbol;

int OnInit()
{
  Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  ...

请注意,此交易品种也应添加到市场报价中,因为它可能不在市场报价列表中。

使用 'mode' 变量来控制当前的显示模式:

ENUM_CHART_MODE mode = 0;

将使用下面的四个指标缓冲区:

// OHLC
double open[];
double high[];
double low[];
double close[];

让我们添加一个小函数以通常的方式初始化缓冲区(使用“series”属性的设置):

void InitBuffer(int index, double &buffer[], ENUM_INDEXBUFFER_TYPE style)
{
  SetIndexBuffer(index, buffer, style);
  ArraySetAsSeries(buffer, true);
}

图形初始化也在一个辅助函数中执行(该指标只有一个图形结构;如果使用多个结构,该功能可以节省时间):

void InitPlot(int index, string name, int style, int width = -1, int colorx = -1)
{
  PlotIndexSetInteger(index, PLOT_DRAW_TYPE, style);
  PlotIndexSetDouble(index, PLOT_EMPTY_VALUE, 0);
  PlotIndexSetString(index, PLOT_LABEL, name);
  if(width != -1) PlotIndexSetInteger(index, PLOT_LINE_WIDTH, width);
  if(colorx != -1) PlotIndexSetInteger(index, PLOT_LINE_COLOR, colorx);
}

以下函数将图表显示模式切换为缓冲区样式:

int Mode2Style(/*global ENUM_CHART_MODE mode*/)
{
  switch(mode)
  {
    case CHART_CANDLES: return DRAW_CANDLES;
    case CHART_BARS: return DRAW_BARS;
    case CHART_LINE: return DRAW_LINE;
  }
  return 0;
}

函数使用前面提到的全局变量“mode”,该变量应在 OnInit 中填入相关值,并调用所有辅助函数。

  InitBuffer(0, open, INDICATOR_DATA);
  string title = "# Open;# High;# Low;# Close";
  StringReplace(title, "#", Symbol);
  mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
  InitPlot(0, title, Mode2Style());

  InitBuffer(1, high, INDICATOR_DATA);
  InitBuffer(2, low, INDICATOR_DATA);
  InitBuffer(3, close, INDICATOR_DATA);

这仍然不足以使指标正常工作。应根据当前模式(mode变量)更改线条颜色:颜色由图表设置提供。

void SetPlotColors()
{
  if(mode == CHART_CANDLES)
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 3);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 0, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));  // 长方形
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL)); // 向上
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 2, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR)); // 向下
  }
  else
  {
    PlotIndexSetInteger(0, PLOT_COLOR_INDEXES, 1);
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, (int)ChartGetInteger(0, CHART_COLOR_CHART_LINE));
  }
}

在 OnInit 中添加 SetPlotColors()调用并设置值的准确性,可以确保在启动后显示正确的指标。

  SetPlotColors();

  IndicatorSetString(INDICATOR_SHORTNAME, "SubChart (" + Symbol + ")");
  IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  
  return INIT_SUCCEEDED;
}

但是,如果用户在指标运行时更改图表模式,则需要跟踪此事件并修改缓冲区的属性。这是通过 OnChartEvent 处理函数完成的。

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    mode = (ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE);
    PlotIndexSetInteger(0, PLOT_DRAW_TYPE, Mode2Style());
    SetPlotColors();
    ChartRedraw();
  }
}

现在,我们需要编写最重要的指标函数,即OnCalculate 处理函数,此处理程序的特定功能是指标实际上使用第三方交易品种的报价而不是图表中的交易品种,因此,所有基于 rates_total 和 prev_calcuated 的标准编程技术(通常从内核传递)都不适用或仅部分适用。第三方交易品种的报价将被异步下载,因此新的一批柱形可能随时“到达”——这种情况需要重新计算。因此,让我们创建变量,它控制该交易品种(lastavailable)上的柱数和常量 prev_calculated 参数的可编辑“克隆”。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime& time[],
                const double& op[],
                const double& hi[],
                const double& lo[],
                const double& cl[],
                const long& tick_volume[],
                const long& volume[],
                const int& spread[])
{
  static int lastAvailable = 0;
  static bool initialized = false;

  int _prev_calculated = prev_calculated;

  if(iBars(Symbol, _Period) - lastAvailable > 1) // 填充柱的间隙
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
  }

  if(_prev_calculated == 0)
  {
    for(int i = 0; i < rates_total; ++i)
    {
      open[i] = 0;
      high[i] = 0;
      low[i] = 0;
      close[i] = 0;
    }
  }

如果指标交易品种与窗口交易品种不同,请使用iBarShift 函数找到同步的柱并复制它们的 OHLC 值。

  if(_Symbol != Symbol)
  {
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      datetime dt = iTime(_Symbol, _Period, i);
      int x = iBarShift(Symbol, _Period, dt, Exact);
      if(x != -1)
      {
        open[i] = iOpen(Symbol, _Period, x);
        high[i] = iHigh(Symbol, _Period, x);
        low[i] = iLow(Symbol, _Period, x);
        close[i] = iClose(Symbol, _Period, x);
      }
    }
  }

如果指标的交易品种与窗口交易品种匹配,就简单使用传入的参数数组:

  else
  {
    ArraySetAsSeries(op, true);
    ArraySetAsSeries(hi, true);
    ArraySetAsSeries(lo, true);
    ArraySetAsSeries(cl, true);
    for(int i = 0; i < MathMax(rates_total - _prev_calculated, 1); ++i)
    {
      open[i] = op[i];
      high[i] = hi[i];
      low[i] = lo[i];
      close[i] = cl[i];
    }
  }

最后,我们需要提供数据的上传,让我们使用来自 MetaQuotes 示例的 RefreshHistory 函数的实现(我们将此代码作为 Refresh.mqh 头文件包含在内)。

initialized”静态变量包含更新完成的交易品种,如果 RefreshHistory 返回成功的标志,或者第三方交易品种柱的数量保持不变且非零(如果所需柱数没有历史记录),则将其设置为true。

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
    }
    return rates_total;
  }

  if(!initialized)
  {
    if(_Symbol != Symbol)
    {
      Print("Updating ", Symbol, " ", lastAvailable, " -> ", iBars(Symbol, _Period), " bars up to ", (string)time[0], "... Please wait");
      int result = RefreshHistory(Symbol, time[0]);
      if(result >= 0 && result <= 2)
      {
        _prev_calculated = rates_total;
      }
      if(result >= 0)
      {
        initialized = true;
        ChartSetSymbolPeriod(0, _Symbol, _Period);
      }
    }
    else
    {
      initialized = true;
    }
  }
  
  lastAvailable = iBars(Symbol, _Period);
  
  return _Symbol != Symbol ? _prev_calculated : rates_total;
}

如果数据加载过程耗时过长,则需要手动刷新图表。

初始化完成后,将在资源节约模式下计算新的柱形图,即考虑到“prev_calcuated”和“rates_total”。如果最后可用的柱数更改超过1,则应重新绘制所有柱。为此,应将“initialized”重置为false。

将指标附在图表上,比如说 EURUSD,并在参数中输入另一个交易品种,例如 UKBRENT(布伦特CFD;这很有趣,因为当“Exact”为 true 时,白天时间范围内缺少夜间柱形是显而易见的)。单击“模式更改”按钮,确保指标绘图正确。

切换指标显示模式

切换指标显示模式

请注意,在线性显示模式中,指标使用第一个缓冲区(索引0)open,这与主图表不同,主图表基于close价格。这种方法可以避免在切换到线性表示法或从线性表示法切换到线性表示法时完成指标重绘:要显示基于收盘价的行,收盘价需要复制到第一个(唯一一个)缓冲区,而在烛台和柱形模式(使用4个缓冲区时)中,第一个缓冲区存储开盘价。当前的实现是基于这样一个事实,即open缓冲区是OHLC中的第一个缓冲区,因此样式的更改会立即导致表示形式的更改,而无需重新计算。线模式在历史分析中的应用并不多,因此这一特性并不重要。

以可视化测试模式运行指标。

可视化测试模式下的 SubChart 指标

可视化测试模式下的 SubChart 指标

现在我们可以根据这个指标继续进行交易历史可视化。

SubChartReporter 指标

让我们把新指标命名为 SubChartReporter,加上之前创建好的读取 HTML 和 CSV 报告的功能,用于分析的文件名将通过 ReportFile 输入变量传入。我们还将提供用于指定时间偏移的输入参数,以及在从其他用户(从不同的交易环境)接收报告时指定交易品种前缀和后缀的输入参数。

input string ReportFile = ""; // · ReportFile
input string Prefix = ""; // · Prefix
input string Suffix = ""; // · Suffix
input int  TimeShift = 0; // · TimeShift

SubchartReporter 指标中的特殊类将处理数据的接收和图形对象(趋势线)的生成。

由于我们想分析HTML和CSV文件,所以为所有报表类型(Processor)设计了一个通用的基本类。HTML和CSV的具体实现是从中继承的:ReportProcessor 和 HistoryProcessor。

Processor 中描述了以下变量:

class Processor
{
  protected:
    string symbol;
    string realsymbol;
    IndexMap *data;
    ulong timestamp;
    string prefix;
  • symbol — 报告中当前工作交易品种的名称;
  • realsymbol — 实际可用工作交易品种的名称,由于前缀和后缀的原因,可能与第三方报告中使用的交易品种不同;
  • data — 一个交易操作数组(indexmap类在文章[1]中已经很熟悉,它是从具有相同名称的头文件连接的);
  • timestamp and prefix — 辅助变量,用于将由指标生成的图表上图形对象的唯一命名;

为了能够在不同的报表交易品种之间进行切换,让我们添加界面按钮。当前按下的按钮ID(对应于所选交易品种)将存储在变量中:

    string pressed;

Processor 类中实现了以下用于对象生成的辅助方法:

    void createTrend(const long dealIn, const long dealOut, const int type, const datetime time1, const double price1, const datetime time2, const double price2, const string &description)
    void createButton(const int x, const int y, const int dx, const int dy, const string text, const bool selected)
    void controlPanel(const IndexMap &symbols)

createTrend 为单独的交易(趋势线和两个箭头)创建可视化表示,createButton 创建一个工作交易品种按钮,controlPanel 是报告中显示的所有符号的完整按钮集。按钮显示在子窗口的左下角。

Processor 类的公共接口包括两组方法:

  • 虚拟方法,受子类中特定实现的约束;
  • 非虚拟方法,提供单一标准功能;

    virtual IndexMap *load(const string file) = 0;
    virtual int getColumnCount() = 0;
    virtual int getSymbolColumn() = 0;
    virtual datetime getStart() = 0;
    virtual bool applyInit() { return true; }
    virtual void makeTrade(IndexMap *row) = 0;
    virtual int render() = 0;

    bool attach(const string file)
    bool apply(const string _s = NULL)
    bool isEmpty() const
    string findrealsymbol()
    string _symbol() const
    string _realsymbol() const
    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)

完整的源代码附在下面,这里我只提供一个简短的描述。

  • load — 上载指定的数据文件
  • getColumnCount — 返回数据表中的列数
  • getSymbolColumn — 返回具有交易操作交易品种名称的列的索引
  • getStart — 返回第一个交易操作的日期
  • applyInit — 处理前可选初始化内部数据结构的方法
  • makeTrade — 内部数据结构中报告中一条记录的登记方法
  • render — 基于内部数据结构中记录的图形对象生成方法

最后两种方法是单独存在的,因为有些报告格式,如MetaTrader 5中的HTML报告包含交易记录,但是我们需要显示头寸。因此,我们需要将一种类型的实体转换为另一种类型的实体,并需要查看一系列交易。

下面以简化形式介绍了非虚拟方法。attach 方法接收要分析的文件名,使用虚拟的“load”方法加载该文件,创建唯一交易品种列表,并为这些交易品种创建按钮的“面板”。

    bool attach(const string file)
    {
      data = load(file);
      
      IndexMap symbols;
      
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        // 收集唯一的交易品种
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        if(StringLen(s) > 0) symbols.set(s);
      }

      if(symbols.getSize() > 0)
      {
        controlPanel(symbols);
      }
      return true;
    }

“apply”方法在指标中激活,从报告中可用的工作交易品种中选择的工作交易品种。首先,从图表中删除旧对象(如果有),然后搜索匹配的真实交易品种(例如,您的经纪商提供的 EURUSD,而不是报告中指定的 EURUSD.m)。在这里,子类可以使用 applyinit 重置旧的内部数组。然后根据带有匹配交易品种(makeTrade调用)的表条目形成交易。图形对象是基于这些交易(渲染调用)绘制的,并且界面已更新(活动按钮选择、指标名称更改、调用ChartRedraw)。

                                                                                                                                          
    bool apply(const string _s = NULL)
    {
      ObjectsDeleteAll(0, "SCR", ChartWindowFind(), OBJ_TREND);

      if(_s != NULL && _s != "") symbol = _s;
      if(symbol == NULL)
      {
        Print("没有选择交易品种");
        return false;
      }
      
      string real = findrealsymbol();
      if(real == NULL)
      {
        Print("没有找到合适的交易品种");
        return false;
      }
      
      SymbolSelect(real, true);

      if(!applyInit()) return false;
      
      int selected = 0;
  
      for(int i = 0; i < data.getSize(); ++i)
      {
        IndexMap *row = data[i];
        
        string s = row[getSymbolColumn()].get<string>();
        StringTrimLeft(s);
        
        if(s == symbol)
        {
          selected++;
          makeTrade(row);
        }
      }
      
      pressed = prefix + "#" + symbol;
      ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGreen);
      
      int trends = render();
      Print(data.getSize(), " records in total");
      Print(selected, " trades for ", symbol);
      
      string title = CHART_REPORTER_TITLE + " (" + symbol + ", " + (string)selected + " records, " + (string)trends + " trades)";
      IndicatorSetString(INDICATOR_SHORTNAME, title);
      
      ChartRedraw();
      return true;
    }

findrealsymbol 方法使用多种方法来搜索匹配的交易品种,它检查交易品种的市场数据(投标价格)的可用性,如果找到数据,则认为该交易品种是真实的。如果没有市场数据,程序将尝试匹配后缀和/或前缀参数(如果指定了后缀或前缀),即从交易品种名称中添加或删除它们。如果修改后收到价格,则表示已找到交易品种的有效别名。

_symbol 和 _realsymbol 方法从报告中返回当前符号的名称,并为您的帐户返回此交易品种或其有效副本。在分析您自己的报告时,您将收到相同的交易品种名称,除非经纪商排除了您的交易品种。

onChartEvent 方法处理 OnChartEvent, 也就是按钮点击事件。上一个选择的按钮(如果有)将像所有其他按钮一样变成灰色。重要的部分是对虚拟“apply”方法的调用,将从按钮标识符中提取的新交易品种的名称传递给该方法。

    void onChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
    {
      if(id == CHARTEVENT_OBJECT_CLICK)
      {
        int x = StringFind(sparam, "_#");
        if(x != -1)
        {
          string s = StringSubstr(sparam, x + 2);
          Print(s, " ", sparam, " ", pressed);
          
          ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_STATE, false);
          ObjectSetInteger(0, pressed, OBJPROP_BGCOLOR, clrGray);
          pressed = "";
          
          if(apply(s)) // 将设置按下状态和其它属性
          {
            ChartSetSymbolPeriod(0, _Symbol, _Period);
          }
        }
      }
    }

现在,让我们继续在子类的虚拟方法中实现主要功能。让我们从 ReportProcessor 开始,

将使用文章[1]中的 WebDataExtractor 分析HTML页。解析器将用作 include 文件 WebDataExtractor.mqh。与原始源代码相比,此文件中的所有工作方法都包装在 HTMLConverter 类中,以避免丢弃全局上下文。我们首先对 HTMLConverter::convertReport2Map 方法感兴趣,它几乎完全符合[1]中讨论的“process”函数。报告文件中的 ReportFile 将输入到 convertReport2Map。函数输出是 IndexMap,其中包含与报表中的交易操作相对应的字符串。

HTML报表分析所需的所有设置(如 RowSelector 和 ColumnSettingsFile)都已在头文件中指定。但它们可以被编辑,因为它们被描述为输入参数。默认参数适用于MetaTrader 5报告,从中分析交易表,并根据这些交易计算进一步显示的头寸。每个交易都由特殊“Deal”类的一个实例描述。“Deal”构造函数从报表表接收一个包含在 IndexMap中的条目。以下字段存储在“Deal”中:交易时间和价格、交易类型和方向、交易量和其他属性。

class ReportProcessor: public Processor
{
  private:
    class Deal   //  如果 MQL5 可以支持类的私有访问设置,
    {            // 交易将无法从外部世界获得,因此
      public:    // 把栏位变为共有,就可以从 Processor 中直接访问了。
        datetime time;
        double price;
        int type;      // +1 - buy, -1 - sell
        int direction; // +1 - in, -1 - out, 0 - in/out
        double volume;
        double profit;
        long deal;
        long order;
        string comment;
        
      public:
        Deal(const IndexMap *row) // 这是 MT5 交易
        {
          time = StringToTime(row[COLUMN_TIME].get<string>()) + TimeShift;
          price = StringToDouble(row[COLUMN_PRICE].get<string>());
          string t = row[COLUMN_TYPE].get<string>();
          type = t == "buy" ? +1 : (t == "sell" ? -1 : 0);
          t = row[COLUMN_DIRECTION].get<string>();
          direction = 0;
          if(StringFind(t, "in") > -1) ++direction;
          if(StringFind(t, "out") > -1) --direction;
          volume = StringToDouble(row[COLUMN_VOLUME].get<string>());
          t = row[COLUMN_PROFIT].get<string>();
          StringReplace(t, " ", "");
          profit = StringToDouble(t);
          deal = StringToInteger(row[COLUMN_DEAL].get<string>());
          order = StringToInteger(row[COLUMN_ORDER].get<string>());
          comment = row[COLUMN_COMMENT].get<string>();
        }
    
        bool isIn() const
        {
          return direction >= 0;
        }
        
        bool isOut() const
        {
          return direction <= 0;
        }
        
        bool isOpposite(const Deal *t) const
        {
          return type * t.type < 0;
        }
        
        bool isActive() const
        {
          return volume > 0;
        }
    };

所有交易都加到 'array' 中了。在历史分析过程中,市场进入交易将被添加到队列中,并在找到对应的相反退出交易时从队列中删除。如果队列在经过整个历史记录之后是非空的,那么就有一个打开的仓位。

    RubbArray<Deal *> array;
    RubbArray<Deal *> queue;

RubbArray 类是动态数组的包装器,动态数组自动扩展以适应传入的数据。

一些虚拟方法的实现如下所示:

    virtual IndexMap *load(const string file) override
    {
      return HTMLConverter::convertReport2Map(file, true);
    }

    virtual int getColumnCount() override
    {
      return COLUMNS_COUNT;
    }

    virtual int getSymbolColumn() override
    {
      return COLUMN_SYMBOL;
    }

最后两种方法使用来自 MetaTrader 5 HTML报告的标准交易表的宏定义。

#define COLUMNS_COUNT 13
#define COLUMN_TIME 0
#define COLUMN_DEAL 1
#define COLUMN_SYMBOL 2
...

applyInit 方法清空 'array' 和 'queue' 数组。

    virtual bool applyInit() override
    {
      ((BaseArray<Deal *> *)&queue).clear();
      array.clear();
      return true;
    }

在 makeTrade 方法中创建 Deal 对象并将其添加到“array”。

    virtual void makeTrade(IndexMap *row) override
    {
      array << new Deal(row);
    }

最后,最有趣也是最困难的部分是对交易清单的分析以及基于它们的交易对象的生成。

    virtual int render() override
    {
      int count = 0;
      
      for(int i = 0; i < array.size(); ++i)
      {
        Deal *current = array[i];
        
        if(!current.isActive()) continue;
        
        if(current.isOut())
        {
          // 首先尝试完全匹配
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current) && queue[j].volume == current.volume)
            {
              string description;
              StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
              createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);
              current.volume = 0;
              queue >> j; // 从队列中删除
              ++count;
              break;
            }
          }

          if(!current.isActive()) continue;
          
          // second try to perform partial close
          for(int j = 0; j < queue.size(); ++j)
          {
            if(queue[j].isIn() && queue[j].isOpposite(current))
            {
              string description;
              if(current.volume >= queue[j].volume)
              {
                StringConcatenate(description, (float)queue[j].volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                current.volume -= queue[j].volume;
                queue[j].volume = 0;
                ++count;
              }
              else
              {
                StringConcatenate(description, (float)current.volume, "[", queue[j].deal, "/", queue[j].order, "-", current.deal, "/", current.order, "] ", (current.profit < 0 ? "-" : ""), current.profit, " ", current.comment);
                createTrend(queue[j].deal, current.deal, queue[j].type, queue[j].time, queue[j].price, current.time, current.price, description);

                queue[j].volume -= current.volume;
                current.volume = 0;
                ++count;
                break;
              }
            }
          }
          
          // 从队列中清除非活动的部分
          for(int j = queue.size() - 1; j >= 0; --j)
          {
            if(!queue[j].isActive())
            {
              queue >> j;
            }
          }
        }
        
        if(current.isActive()) // is _still_ active
        {
          if(current.isIn())
          {
            queue << current;
          }
        }
      }
      
      if(!isQueueEmpty())
      {
        Print("Warning: not all deals are processed (probably, open positions left).");
      }
      
      return count;
    }

该算法通过所有交易的列表前进,并将市场进入交易放入队列中。找到退出交易后,将在队列中搜索与匹配大小相对应的相应交易。如果没有找到与大小完全匹配的位置,则按FIFO规则一致选择进入交易的数量,以覆盖退出交易的数量。处理完全覆盖的交易量将从队列中删除。为每个进出交易组合创建趋势线(createTrend)。

采用 FIFO 规则是因为从算法的角度来看,它是最合理的规则。然而,其他选择也是可能的。一个特定的交易机器人不仅可以通过先进先出(FIFO)完成交易,还可以通过后进先出(LIFO)规则甚至以任意顺序完成交易。这同样适用于手工交易。因此,为了在套期保值模式下建立进出口交易的对应关系,有必要找到某种“权宜之计”,如利润分析或评论。两个价格点之间的利润计算示例可以在我的 博客 中找到,但这不包括汇兑差额的核算。此任务通常不太简单,因此本文不涉及此任务。 

这样,我们回顾了如何在所描述的类中处理HTML报告。对于 CSV 文件来说,HistoryProcessor类的实现要简单得多。从所附的源代码可以很容易地理解。注意,对于MT4和MT5, mql5.com CSV 文件的信号有不同的历史和列数。根据 MetaTrader 4的双扩展名:“.history.csv”和 MetaTrader5 的“.positions.csv”自动选择适当的格式。csv文件的唯一设置是分隔符(mql5.com信号文件中的默认字符是';')。

SubChartReporter的主要源代码继承自SubChart,我们使用它检查了子窗口中第三方报价的显示,所以让我们来谈谈新的片段。

类对象是在事件处理程序中创建和使用的。处理器创建于OnInit中,并在OnDeInit中销毁:

Processor *processor = NULL;

int OnInit()
{
  if(StringFind(ReportFile, ".htm") > 0)
  {
    processor = new ReportProcessor();
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    processor = new HistoryProcessor();
  }
  string Symbol = SubSymbol;
  if(Symbol == "") Symbol = _Symbol;
  else SymbolSelect(Symbol, true);
  processor.apply(Symbol);
  ...
}

void OnDeinit(const int reason)
{
  if(processor != NULL) delete processor;
}

图形对象事件处理被添加到 OnChartEvent 处理程序中:

void OnChartEvent(const int id,
                  const long& lparam,
                  const double& dparam,
                  const string& sparam)
{
  if(id == CHARTEVENT_CHART_CHANGE)
  {
    ... // 相同的代码
  }
  else
  {
    processor.onChartEvent(id, lparam, dparam, sparam);
  }
}

OnCalculate处理程序中,我们需要跟踪分析交易品i那种在响应用户操作时发生更改的情况,之后执行完整的重新计算:

  string Symbol = processor._realsymbol();
  if(Symbol == NULL) Symbol = _Symbol;
  if(lastSymbol != Symbol)
  {
    _prev_calculated = 0;
    lastAvailable = 0;
    initialized = false;
    IndicatorSetInteger(INDICATOR_DIGITS, (int)SymbolInfoInteger(Symbol, SYMBOL_DIGITS));
  }

当初始绘图完成且工作交易品种的可用条数稳定后,启动计时器加载报告数据。

  if(lastAvailable == iBars(Symbol, _Period) && lastAvailable != 0)
  {
    if(!initialized)
    {
      Print("Updated ", Symbol, " ", iBars(Symbol, _Period), " bars");
      initialized = true;
      if(ReportFile != "") //
      {                    //
        EventSetTimer(1);  //
      }                    //
    }
    
    return rates_total;
  }

在计时器处理程序中上载报告(如果尚未上载),并激活所选字符(通过OnInit中的参数或通过单击OnChartEvent中的按钮设置)。

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // 只载入一次文件
  {
    if(processor.attach(ReportFile))
    {
      processor.apply(/*keep already selected symbol*/);
    }
    else
    {
      Print("File loading failed: ", ReportFile);
    }
  }
}

现在让我们测试指示器的性能。打开 EURUSD 图表,附加指示器,并在ReportTester 参数中指定reporttester-example.html文件(附加)。初始化后,子窗口将包含 EURUSD 图表(因为符号参数为空)和多个按钮,其中包含报表中所有符号的名称。

没有选定工作交易品种的 SubChartReporter 指标

没有选定工作交易品种的 SubChartReporter 指标

因为报告中没有 EURUSD,所有按钮都是灰色的。单击任何按钮(例如,EURGBP),都会将此货币报价加载到子窗口中,并显示相应的交易。按纽就将变成绿色。

具有选定工作交易品种的 SubChartReporter 指标

具有选定工作交易品种的 SubChartReporter 指标

在当前的实现中,按钮的顺序由报表中的交易品种出现时间决定。如有必要,它可以按任何需要的顺序排序,例如,按字母顺序或按交易数量排序。

通过切换按钮,我们可以查看报告中的所有交易品种。但是这不是很方便,对于某些报告,最好一次查看所有交易品种,每个交易品种都在自己的子窗口中。为此,让我们编写 SubChartsBuilder脚本,它将创建子窗口,并在这些子窗口 SubChartsReporter 实例中为不同的交易品种启动。

SubChartsBuilder 脚本

该脚本与SubchartReporter指标具有相同的参数集。要提供单个启动,将参数添加到 MqlParam数组,然后从MQL API 调用 IndicatorCreate。这是在一个应用程序函数 createIndicator中完成的。

bool createIndicator(const string symbol)
{
  MqlParam params[18] =
  {
    {TYPE_STRING, 0, 0.0, "::Indicators\\SubChartReporter.ex5"},
    
    {TYPE_INT, 0, 0.0, NULL}, // 图表设置
    {TYPE_STRING, 0, 0.0, "XYZ"},
    {TYPE_BOOL, 1, 0.0, NULL},
    
    {TYPE_INT, 0, 0.0, NULL}, // 通用设置
    {TYPE_STRING, 0, 0.0, "HTMLCSV"},
    {TYPE_STRING, 0, 0.0, "PREFIX"},
    {TYPE_STRING, 0, 0.0, "SUFFIX"},
    {TYPE_INT, 0, 0.0, NULL}, // 时间偏移

    {TYPE_INT, 0, 0.0, NULL}, // html 设置
    {TYPE_STRING, 0, 0.0, "ROW"},
    {TYPE_STRING, 0, 0.0, "COLUMNS"},
    {TYPE_STRING, 0, 0.0, "SUBST"},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},
    {TYPE_BOOL, 0, 0.0, NULL},

    {TYPE_INT, 0, 0.0, NULL}, // csv 设置
    {TYPE_STRING, 0, 0.0, ";"}
  };
  
  params[2].string_value = symbol;
  params[5].string_value = ReportFile;
  params[6].string_value = Prefix;
  params[7].string_value = Suffix;
  params[8].integer_value = TimeShift;
  params[10].string_value = RowSelector;
  params[11].string_value = ColumnSettingsFile;
  params[12].string_value = SubstitutionSettingsFile;
  params[17].string_value = CSVDelimiter;
  
  int handle = IndicatorCreate(_Symbol, _Period, IND_CUSTOM, 18, params);
  if(handle == INVALID_HANDLE)
  {
    Print("无法创建 SubChartReporter  ", symbol, ": ", GetLastError());
    return false;
  }
  else
  {
    if(!ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle))
    {
      Print("无法附加 SubChartReporter  ", symbol, ": ", GetLastError());
      return false;
    }
  }
  return true;
}

请注意,指标是从源代码中预先注册的资源中获取的,说明如下:

#resource "\\Indicators\\SubChartReporter.ex5"

这样,脚本就是一个功能齐全的独立程序,它不依赖于特定用户的指标的存在。

这种方法还有另一个目的,当为报表中使用的所有交易品种生成指标实例时,不再需要控制按钮。不幸的是,MQL不提供功能来判断指标是由用户启动还是通过IndicatorCreate启动,无论它是由自身运行还是由更大程序的整体(依赖)部分运行。当指标放置在资源中时,可以根据指标路径显示或隐藏按钮:资源中的路径(即存在“:Indicators\\”字符串)表示应禁用按钮的显示。

要为每个报表交易品种调用 createIndicator 函数,我们需要在脚本中分析报表。

int OnStart()
{
  IndexMap *data = NULL;
  int columnsCount = 0, symbolColumn = 0;
  
  if(ReportFile == "")
  {
    Print("cleanUpChart");
    return cleanUpChart();
  }
  else if(StringFind(ReportFile, ".htm") > 0)
  {
    data = HTMLConverter::convertReport2Map(ReportFile, true);
    columnsCount = COLUMNS_COUNT;
    symbolColumn = COLUMN_SYMBOL;
  }
  else if(StringFind(ReportFile, ".csv") > 0)
  {
    data = CSVConverter::ReadCSV(ReportFile);
    if(data != NULL && data.getSize() > 0)
    {
      IndexMap *row = data[0];
      columnsCount = row.getSize();
      symbolColumn = CSV_COLUMN_SYMBOL;
    }
  }
  
  if(data != NULL)
  {
    IndexMap symbols;
    
    for(int i = 0; i < data.getSize(); ++i)
    {
      IndexMap *row = data[i];
      if(CheckPointer(row) == POINTER_INVALID || row.getSize() != columnsCount) break;
      
      string s = row[symbolColumn].get<string>();
      StringTrimLeft(s);
      if(StringLen(s) > 0) symbols.set(s);
    }
    
    for(int i = 0; i < symbols.getSize(); ++i)
    {
      createIndicator(symbols.getKey(i));
    }
    delete data;
  }

  return 0;
}

如果脚本以空的报告名称运行,它将从窗口中删除所有具有指标实例的子窗口(如果这些子窗口是以前创建的)。这是通过 cleanUpChart 函数完成的。

bool cleanUpChart()
{
  bool result = true;
  int n = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
  for(int i = n - 1; i > 0; --i)
  {
    string name = ChartIndicatorName(0, i, 0);
    if(StringFind(name, "SubChartReporter") == 0)
    {
      Print("Deleting ", name);
      result &= ChartIndicatorDelete(0, i, name);
    }
  }
  return result;
}

这是在完成报告分析后清除图表的有效功能,

为了测试脚本,我下载了带有信号历史记录的 CSV 文件。下面是它的外观(主图表最小化):

分析多货币交易时的多个SubChartReporter实例

分析多货币交易时的多个SubChartReporter实例

生成的对象将提供包含报告详细信息(交易编号、交易量、利润和评论)的描述。要显示详细信息,请在图表设置中启用“显示对象描述”。

如果工作交易品种的数量很大,则子窗口大小会减小,虽然这提供了一个总体情况,但研究细节可能很困难。当您需要分析每个交易时,请尽可能多地使用空间,包括主窗口。为此,让我们创建一个新版本的SubchartReporter指标,它将在主图表上显示交易,而不是使用子窗口。让我们称它为 MainChartReporter.

MainChartReporter 指标

由于该指标显示在价格图表上,因此不需要计算缓冲区并绘制趋势对象以外的任何其他对象。换句话说,这是一个无缓冲指标,它将更改当前图表的工作交易品种并设置分析的交易品种。从实现的角度来看,指标几乎准备就绪:新版本代码是一个大大简化的SubChartReporter。这里是代码的功能。

Processor、ReportProcessor 和 HistoryProcessor 这三个主要类的源代码将移动到头文件中,并包含在这两个指标中。特定于任何版本的差异将在条件编译的预处理器指令中使用。CHART_REPORTER_SUB 宏将为 SubChartReporter指标代码定义,而 CHART_REPORTER_MAIN 宏将为 MainChartReporter定义。

需要在MainChartReporter中添加两行。我们需要在“apply”方法中将图表切换为新的实际交易品种:

#ifdef CHART_REPORTER_MAIN
      ChartSetSymbolPeriod(0, real, _Period);
#endif

我们还需要用文本显示一条注释,在第一个指标版本中,它等于它的名称(INDICATOR_SHORTNAME)。

#ifdef CHART_REPORTER_MAIN
      Comment(title);
#endif

在onChartEvent处理程序方法中,我们更新了当前图表,因为第一个指标版本在不同于主窗口符号的符号上绘制数据。在新版本中,主交易品种报价按原样使用,因此无需更新窗口。因此,相关行仅添加到 SubChartReporter指标的条件编译中。

#ifdef CHART_REPORTER_SUB
            ChartSetSymbolPeriod(0, _Symbol, _Period);
#endif

从MainChartReporter指标源代码中,删除缓冲区(通过指定缓冲区的数字等于0以避免编译器警告)。

#property indicator_chart_window
#property indicator_buffers 0
#property indicator_plots   0

同时删除不再使用的子窗口设置组:

input GroupSettings Chart_Settings; // 子图表设置
input string SubSymbol = ""; // · 交易品种
input bool Exact = true; // · Exact

OnCalculate函数变为空,但它必须出现在指标中。从数据接收报告的计时器在OnInit中启动。

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) // 只载入一次文件
  {
    if(processor.attach(ReportFile))
    {
      processor.apply();
      datetime start = processor.getStart();
      if(start != 0)
      {
        ChartSetInteger(ChartID(), CHART_AUTOSCROLL, false);
        // FIXME: 这里没有正确工作
        ChartNavigate(ChartID(), CHART_END, -1 * (iBarShift(_Symbol, _Period, start)));
      }
    }
  }
}

这是通过调用ChartNavigate将图表滚动到第一笔交易的尝试。不幸的是,我无法使这个代码部分正常工作,所以图表从未被移动过。可能的解决方案是确定当前位置,并使用图表“CHART_CURRENT_POS”相对于当前位置进行导航。然而,这个解决方案似乎并不理想。

这就是图表上MainChartReporter指标的样子。

MainChartReporter 指标

MainChartReporter 指标

附加的文件

  • SubChart.mq5 — SubChart 指标
  • SubChartReporter.mq5 — SubChartReporter 指标
  • MainChartReporter.mq5 — MainChartReporter 指标
  • SubChartsBuilder.mq5 — 为报表中使用的所有交易品种创建一组SubchartReporter实例的脚本
  • ChartReporterCore.mqh — 指标的主要共用类
  • WebDataExtractor.mqh — HTML 解析器
  • CSVReader.mqh — CSV 解析器
  • HTMLcolumns.mqh — 确定 HTML 报告列
  • CSVcolumns.mqh — 确定 CSV 报告列
  • IndexMap.mqh — 辅助映射类
  • RubbArray.mqh — 辅助 rubber 数组类
  • StringUtils.mqh — 字符串操作的工具函数
  • empty_strings.h — HTML分析器的空标记列表
  • GroupSettings.mqh — 空参数组
  • Refresh.mqh — 请求交易品种报价
  • ReportTester-example.html — HTML 测试报告实例
  • ReportHistoryDeals.cfg.csv — 使用HTML分析器选择表列的CSS选择器设置

结论

我们已经查看了几个指标,这些指标将多个交易品种的报价和交易可视化。HTML格式的报告用作输入数据的源,通过使用通用的解析器,不仅可以解析标准报告(源代码中已经包含其设置),还可以解析其他报告类型。此外,我们还实现了对 CSV 格式的支持,其中通常提供 mql5.com 信号的交易历史记录。通过使用开放源码,任何人都可以根据您的需要调整应用程序。

本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/5913

附加的文件 |
report2chart.zip (34.55 KB)
轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件 轻松快捷开发 MetaTrader 程序的函数库(第四部分):交易事件
在之前的文章中,我们已着手创建一个大型跨平台函数库,简化 MetaTrader 5 和 MetaTrader 4 平台程序的开发。 我们已拥有历史订单和成交集合,在场订单和仓位的集合,以及便捷选择和订单排序的类。 在这一部分中,我们将继续开发基础对象,并教导引擎(Engine)函数库跟踪帐户上的交易事件。
研究烛条分析技术(第四部分):形态分析器的更新和补充 研究烛条分析技术(第四部分):形态分析器的更新和补充
本文论述了形态分析器(Pattern Analyzer)应用程序的新版本。 此版本修复了已发现错误并提供了一些新功能,还改进了用户界面。 在新版本的开发过程中参考了上一篇文章中的意见和建议。 最终的应用程序会在本文中进行说明。
利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中 利用 MQL5 和 MQL4 实现的选择和导航工具: 把数据添加到图表中
在本文中,我们将继续扩展实用程序的功能。这一次,我们将增加显示简化交易的数据的能力,特别是前一天的最高、最低价位,全年的最高、最低价位,开盘时间等。
开发一个跨平台网格 EA 开发一个跨平台网格 EA
在本文中,我们将学习如何创建在 MetaTrader 4 和 MetaTrader 5 中都能工作的 EA 交易。为此,我们将开发一个 EA 构建的订单网格,网格是指将多个限价订单置于当前价格之上,同时将相同数量的限价订单置于当前价格之下的 EA 交易。