HTMLとCSVレポートに基づいて多通貨トレードヒストリーを可視化する方法

Stanislav Korotky | 20 6月, 2019

このテーマのイントロダクションをしてから、MetaTrader5は多通貨テストオプションを提供するようになりました。 この可能性は、多くの場合、トレーダーにとって有用です。 しかし、この機能は完璧ではありません。 特に、テストを実行した後、ユーザーは実行されたトレード操作でチャートを開くことができます。 しかし、これはストラテジーテスター設定で選択された1つのトレードされたシンボルのチャートに過ぎません。 すべての使用済み銘柄のトレードヒストリー全体は、テスト後に表示することはできませんが、ビジュアルテストは必ずしも効率的であるとは限りません。 テスト後しばらくしてから、追加の分析が必要になる場合があります。 また、レポートは他の人が提供することもできます。 したがって、HTML テスト レポートに基づいて複数のタスクシンボルのトレードを視覚化するツールは便利です。

このタスクは、別の同様のMetaTraderアプリケーションと密接に関連しています。 MQL5.comで利用可能なトレードシグナルの多くは、多通貨トレードです。 シグナルヒストリーを持つCSVファイルをチャートに表示すると便利です。

前述の関数を実行できるインジケータを開発してみましょう。

複数のタスクシンボルの並列解析を有効にするために、チャートサブウィンドウに複数のインジケータインスタンス(シンボルごとに1つ)が作成されます。 主なグラフィカルオブジェクトは、選択したシンボルの"quotes"で、通常はチャートシンボルとは異なり、メインウィンドウバーと同期されます。 トレードオーダー(ポジション)に対応するトレンドラインは、"quotes"に適用されます。

また、別のアプローチがあります。つまり、レードはメインウィンドウに表示されますが、この場合、チャート上で分析されるシンボルは1つだけです。 このアプローチでは、バッファのない別のインジケータが必要で、レポートに含まれるシンボルのいずれかに切り替えることができます。

前回の記事では、CSS セレクタ [1]に基づく HTML パーサーの説明を提供しました。 このパーサーは、トレードできるトレード(グラフィカルオブジェクト)に基づいて、HTMLレポートからトレードのリストを抽出します。 シグナルセクションからのCSVファイルの解析は少し簡単ですが、MetaTrader4(*.history.csv)およびMetaTrader5(*.positions.csv)シグナルのファイル形式は組み込みのMQL関数でサポートされています。

サブチャートインジケータ

実装の最初のステップは、任意のチャートのサブウィンドウに外部シンボルの"quotes"を表示する簡単なインジケータを作成することです。 サブチャートインジケータになります。

OHLC (始値、高値、安値、終値) 値でデータを表示するために、MQL には DRAW_CANDLESDRAW_BARSなどの複数の表示スタイルが表示されます。 それぞれ4つのインジケータバッファを使用します。 どちらのスタイルも選択する代わりに、両方のオプションをサポートしています。 このスタイルは、現在のウィンドウ設定に基づいて動的に選択されます。 ラジオボタンのグループは、チャートの設定で、[共通]タブの下で使用できます。つまり、"足"、"日足のローソク足"、および"Line"です。 クイック アクセスの場合は、ツールバーのボタンと同じボタンを使用できます。 この設定は、次の呼び出しを使用して MQL から取得できます。

(ENUM_CHART_MODE)ChartGetInteger(0, CHART_MODE)

ENUM_CHART_MODE列挙体には、同様の目的を持つ要素が含まれています。つまり、CHART_CANDLES、CHART_BARS、CHART_LINEです。

直近のCHART_LINEポイントのサポートもインジケータに実装されます。 したがって、インジケータビューは、UIの表示モードが切り替わると、メインウィンドウに従って変更されます。 DRAW_LINEは直近のモードに適します。1 つのバッファを使用します。

さて、実装してみましょう。 インジケータ バッファの数と表示されるグラフィカル オブジェクトタイプを宣言します。

#property indicator_separate_window
#property indicator_buffers 4
#property indicator_plots   1

インプット変数の追加:

input string SubSymbol = ""; //シンボル
input bool Exact = false;

SubSymbol を使用すると、メイン ウィンドウの現在のシンボル以外のシンボルをインジケータに設定できます。

[Exact] パラメータは、メイン ウィンドウ内の足にまったく同時に別のシンボルの一致する足がない場合のアクションを決定します。 このパラメータは iBarShift 関数呼び出しで使用します。 その視覚効果は次のようになります。

サブシンボル パラメータは、デフォルトでは空の文字列と等しくなります。 つまり、クオートはメイン ウィンドウに表示されるものと同じです。 この場合、実際の変数値を編集し、_Symbol に設定する必要があります。 ただし、'input' は MQL の読み取り専用変数であるため、中間変数 'Symbol' をインプットし、 OnInit ハンドラにインプットする必要があります。

string Symbol;

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

このシンボルはマーケットウォッチリストに表示されない場合がありますので、マーケットウォッチにも追加する必要がありますのでご注意ください。

'mode' 変数を使用して、現在の表示モードを制御します。

ENUM_CHART_MODE mode = 0;

次の 4 つのインジケータ バッファを使用します。

// 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);
}

グラフィックスの初期化は、1 つの補助関数でも実行されます (このインジケータにはグラフィカルな構造が 1 つしかありません。;

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;
}

この関数は、OnInit で関連する値で満たされるべき前述のグローバル変数 'mode' を、すべての補助関数の呼び出しと共に使用します。

  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);

これは、適切なインジケータ操作には十分ではありません。 線の色は、現在のモード(モード変数)に応じて変更する必要があります。つまり、色は、チャートの設定によって提供されます。

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));  //4角 形
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 1, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BULL)); // up
    PlotIndexSetInteger(0, PLOT_LINE_COLOR, 2, (int)ChartGetInteger(0, CHART_COLOR_CANDLE_BEAR)); // down
  }
  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_calculated 値に基づくすべての標準的なプログラミング手法は、適切ではなく、部分的にしか適していません。 サードパーティのシンボルのクオートは非同期にダウンロードされるため、足の新しいバッチはいつでも "arrive"(到達) になる可能性があります - この状況では完全な再計算が必要です。 したがって、そのシンボル上の足の数 (lastAvailable) と、定数 prev_calculated 引数の編集可能 "clone" を制御する変数を作成してみましょう。

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) // bar gap filled
  {
    _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 ヘッダー ファイルとして含めます)。

'初期化された' 静的変数には、更新完了のサインがあります。 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;
}

データの読み込みに時間がかかりすぎる場合は、手動チャートの更新が必要になる場合があります。

初期化が完了すると、新しい足は省エネモードで計算されます。 lastAvailable 可能な足の数が 1 を超える場合は、すべての足をリペイントする必要があります。 この目的のため、'初期化'はfalseにリセットする必要があります。

チャートにインジケータを添付し、EURUSDと言って、例えばUKBrent(ブレントCFD;'Exact'がtrueの場合、日中の時間枠に夜の足の不在が明らかなので、興味深いです)、パラメータに別のシンボルをインプットしてみましょう。 モード変更ボタンをクリックし、インジケータの描画が正しいことを確認します。

インジケータ表示モードの切り替え

インジケータ表示モードの切り替え

リニア表示モードでは、インジケータは最初のバッファ (インデックス 0) を始値で使用します。 終値に基づくメインチャートとは異なります。 このアプローチは、線形表現に切り替えるとき、または線形表現から切り替えるときに完全なインジケータのリペイントを回避することができます。つまり、終値ベースの行を表示するには、終値は、ローソク足とバーモード(4バッファの場合)の間、最初の(唯一の)バッファにコピーする必要があります。 現在の実装は、 オープンバッファがOHLCの最初のものであるという事実に基づいているため、スタイルの変更はすぐに再計算せずに、見た目の変更につながります。 このライン モードはヒストリー解析ではほとんど使用されないため、この機能は重要ではありません。

ビジュアル テスト モードでインジケータを実行します。

ビジュアル テスト モードのサブチャート インジケータ

ビジュアル テスト モードのサブチャート インジケータ

このインジケータに基づいてトレードヒストリービジュアライゼーションに進むことができます。

サブチャートレポーターインジケータ

新しいインジケータサブチャートレポーターに名前を付けましょう。 以前に作成したコードに HTML および CSV レポートを読み取れるようにします。 分析用のファイルの名前は、ReportFile インプット変数に渡されます。 また、タイムシフトを指定するためのインプットパラメータや、別のユーザからレポートを受け取ったときにシンボルプレフィックスとサフィックスを指定するためのインプットパラメータも提供します(別のトレード環境から)。

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

SubChartReporterインジケータの特別なクラスは、データの受信とグラフィカルオブジェクト(近似曲線)の生成を処理します。

CSV ファイルと共に HTML を分析したいので、一般的な基本クラスは、すべてのレポート タイプであるプロセッサ用に設計されています。 HTML と CSV の特定の実装は、レポートプロセッサとヒストリープロセッサから継承されました。

以下の変数については、プロセッサで説明します。

class Processor
{
  protected:
    string symbol;
    string realsymbol;
    IndexMap *data;
    ulong timestamp;
    string 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は、別のトレード(近似曲線と2つの矢印)の視覚的表現を作成し、createButtonは、タスクシンボルボタンをクレートし、controlPanelは、レポートに表示されるすべてのシンボルのボタンの完全なセットです。 このボタンはサブウィンドウの左下隅に表示されます。

Processor クラスのパブリック インターフェイスには、次の 2 つのメソッド グループが含まれます。

    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)

完全なソースコードは以下に添付されています。 ここでは簡単な説明をします。

MetaTrader5 の HTML レポートなどの一部のレポート形式にはトレードの記録が含まれているため、直近の 2 つの方法は別々に存在しますが、ポジションを表示する必要があります。 したがって、ある種類のエンティティを別のエンティティに追加変換する必要があります。

非仮想メソッドは、簡略化された形式で以下に示します。 添付メソッドは、分析用にファイルの名前を受け取り、仮想 'load' メソッドを使用して読み込み、一意のシンボルのリストを作成し、シンボルのボタンの "panel" を作成します。

    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.mではなくブローカーが提供するのがEURUSDの場合)。 ここでは、子クラスは 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("No symbol selected");
        return false;
      }
      
      string real = findrealsymbol();
      if(real == NULL)
      {
        Print("No suitable symbol found");
        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 メソッドは、複数のアプローチを使用して一致するシンボルを詳しく見ることができます。 シンボルの相場データ (Bid価格) の可用性をチェックします。 データが見つかった場合、シンボルは実数と見なされます。 相場データがない場合、プログラムはサフィックスまたはプレフィックスパラメータ(サフィックスまたはプレフィックスが指定されている場合)、すなわちシンボル名からを追加または削除します。 変更後に価格を受け取った場合は、シンボルに対して有効なエイリアスが見つかったことを意味します。

メソッド _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);
          }
        }
      }
    }

次に、子クラスの仮想メソッドの主な関数の実装に移りましょう。 レポートプロセッサから始めましょう。

HTML ページは、記事 [1] の WebData Extractor を使用して解析されます。 このパーサーはインクルード ファイル WebDataExtractor.mqh として使用されます。 元のソース コードと比較して、このファイル内のすべてのタスクメソッドは、グローバル コンテキストを散らかさないために HTMLConverter クラスにラップされます。 まず、HTMLConverter::convertReport2Map メソッドに焦点を当て、[1] で説明した 'プロセス' 関数とほぼ完全に一致します。 ReportFile からのレポート ファイル名は、convertReport2Map にインプットされます。 関数出力は、レポートテーブルのトレード操作に対応する文字列を持つIndexMapです。

行セレクタや列設定ファイルなど、HTML レポートの解析に必要なすべての設定は、ヘッダ ファイルで既に指定されています。 ただし、インプットパラメータとして記述されているため、編集できます。 デフォルトパラメータはMetaTrader5レポートに適用され、そこからトレードのテーブルが解析され、さらに表示されたポジションがトレードに基づいて計算されます。 各トレードは、特別な「取引」クラスのインスタンスによって記述されます。 'Deal' コンストラクタは、IndexMap でラップされたレポート テーブルから 1 つのインプットを受け取ります。 次のフィールドは、トレード時間と価格、そのタイプと方向、ボリュームおよびその他のプロパティの中に格納されます。

class ReportProcessor: public Processor
{
  private:
    class Deal   //MQL5 がクラスのプライベート アクセス指定子をできる場合、
    {            //トレードは外から到達できないので、
      public:    //プロセッサからの直接アクセスのみ公開されたフィールド
        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) //MetaTrader5のトレード
        {
          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;
        }
    };

すべてのトレードは’配列’に追加されます。 ヒストリー分析の過程で、トレードはキューに追加され、対応する反対の決済トレードが見つかった場合は、そこから削除されます。 ヒストリー全体を通過した後にキューが空でない場合は、オープンポジションがあります。

    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;
    }

直近の 2 つのメソッドは、MetaTrader5 HTML レポートのトレードの標準テーブルにマクロ定義を使用します。

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

applyInit メソッドは、'配列' 配列と 'キュー' 配列をクリアします。

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

トレードオブジェクトが作成され、makeTrade メソッドの '配列' に追加されます。

    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())
        {
          // first try to find exact match
          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;
          
          //2 回目は部分的なクローズを実行します。
          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ルールによって、あるいは任意のオーダーでトレードをクローズすることができます。 手動トレードについても同じことが当てはまります。 したがって、ヘッジモードでエントリーと決済の対応を確立するためには、利益やコメントの分析など、特定の"回避策"を見つける必要があります。 2つの価格ポイント間の利益計算の例は、 blog postで利用可能ですが、為替差異の会計は含まれていません。 このタスクは一般的にシンプルではないため、この記事では説明しません。 

したがって、HTML レポートが説明されたクラスでどのように処理されるかを確認しました。 HistoryProcessor クラスの実装は、CSV ファイルの方がはるかに簡単です。 これは、添付されたソースコードから理解できます。 mql5.comシグナルヒストリーを持つ CSV ファイルの列数が MetaTrader4 と MetaTrader5 では異なることに注意してください。 適切な形式は、メタトレーダー 4 の ".history.csv" とメタトレーダー 5 の ".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 ハンドラでは、ユーザーの操作に応じて分析されたシンボルが変更された場合の状況を追跡し、その後完全な再計算が実行されます。

  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()) //ファイルを 1 回だけ読み込む
  {
    if(processor.attach(ReportFile))
    {
      processor.apply(/*keep already selected symbol*/);
    }
    else
    {
      Print("File loading failed: ", ReportFile);
    }
  }
}

今すぐインジケータのパフォーマンスをテストしてみましょう。 EURUSD チャートを開き、インジケーターを添付し、ReportFile パラメーターに ReportTester-example.html ファイル (添付) を指定します。 初期化後、サブウィンドウには EURUSD チャート (Symbol パラメータが空であるため) と、レポートに含まれるすべてのシンボルの名前を持つ多数のボタンがあります。

選択したタスクシンボルのないサブチャートレポーターインジケータ

選択したタスクシンボルのないサブチャートレポーターインジケータ

このレポートには EURUSD がないため、すべてのボタンが灰色になります。 EURGBP など、任意のボタンをクリックすると、この通貨の相場がサブウィンドウに読み込まれ、適切なトレードが表示されます。 このボタンが緑色に変わります。

選択したタスクシンボルを持つサブチャートレポーターインジケータ

選択したタスクシンボルを持つサブチャートレポーターインジケータ

現在の実装では、ボタンの順序は、レポート内のシンボル年表によって決定されます。 必要に応じて、アルファベット順やトレード数など、任意の任意のオーダーで並べ替えることができます。

ボタンを切り替えることで、レポートからすべてのシンボルを表示できます。 しかし、あまり便利ではありません。 一部のレポートでは、すべてのシンボルを一度に表示し、各シンボルを独自のサブウィンドウに表示することができます。 このために、サブウィンドウを作成し、異なるシンボルのサブウィンドウ SubChartReporter インスタンスで起動する SubChartsBuilder スクリプトを記述してみましょう。

サブチャートビルダースクリプト

このスクリプトには、サブチャートレポーターインジケータと同じパラメータセットがあります。 単一の開始を提供するために、パラメータが MqlParam 配列に追加され、その後、IndicatorCreate が MQL API から呼び出されます。 これは、1 つのアプリケーション関数 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("Can't create SubChartReporter for ", symbol, ": ", GetLastError());
    return false;
  }
  else
  {
    if(!ChartIndicatorAdd(0, (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL), handle))
    {
      Print("Can't attach SubChartReporter for ", symbol, ": ", GetLastError());
      return false;
    }
  }
  return true;
}

このインジケータは、ソース コードに事前に登録されているリソースから取得します。

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

したがって、このスクリプトは、特定のユーザーとインジケータに依存しない、完全に独立したプログラムです。

このアプローチには別の目的があります。 レポートで使用するすべてのシンボルに対してインジケータインスタンスが生成されると、コントロール ボタンは不要になります。 残念ながら、MQLは、インジケータがユーザによって起動するか、IndicatorCreateを介して起動されるか、単独で実行されるか、またはより大きなプログラムの不可欠な(依存)部分であるかを判断することができません。 このインジケータをリソースに配置すると、インジケータパスに応じてボタンを表示または非表示にできます。

各レポートシンボルの 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インジケータの新しいバージョンを作成してみましょう。 これをメインチャートレポーター(MainChartReporter)と呼びましょう。

メインチャートレポーターインジケータ

このインジケータは価格チャートに表示されるので、バッファを計算し、トレンドオブジェクト以外のものを描画する必要はありません。 言い換えれば、現在のチャートの動作シンボルを変更し、分析されたシンボルを設定するバッファレスインジケータです。 実装の観点で言えば、このインジケータはほぼ準備ができています。つまり、新しいバージョンコードは大幅に簡素化されたSubChartReporterです。 これがこのコードの特徴です。

プロセッサ、レポートプロセッサ、および HistoryProcessor の 3 つの主要なクラスのソース コードはヘッダ ファイルに移動され、両方のインジケータに含まれます。 いずれのバージョンにもある相違点として、条件付きコンパイル用のプリプロセッサオーダーで使用します。 CHART_REPORTER_SUB マクロはサブチャートレポーターインジケータコードに対して定義され、CHART_REPORTER_MAIN はメインチャートレポーターに対して定義されます。

メインチャートレポーターに二本線の線を追加する必要があります。 "適用"(apply)メソッドでチャートを新しい実数シンボルに切り替える必要があります。

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

また、最初のインジケータバージョンでその名前と等しかったテキストを含むコメントを表示する必要があります(INDICATOR_SHORTNAME)。

#ifdef CHART_REPORTER_MAIN
      Comment(title);
#endif

onChartEvent ハンドラメソッドでは、最初のインジケータバージョンがメインウィンドウシンボルとは異なるシンボルのデータを引き出したため、現在のチャートを更新しました。 新しいバージョンでは、メインシンボルクオートが"as is" で使用され、ウィンドウを更新する必要はありません。 したがって、関連するメインラインは、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; //S U B C H A R T T I N G S
input string SubSymbol = ""; // · Symbol
input bool Exact = true; // · Exact

OnCalculate 関数は空になりますが、インジケータにある必要があります。 データからレポートを受信するためのタイマーは OnInit で開始されます。

void OnTimer()
{
  EventKillTimer();
  
  if(processor.isEmpty()) //ファイルを 1 回だけ読み込む
  {
    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 を使用して対して相対的にナビゲートすることです。 しかし、このソリューションは最適ではないようです。

これは、メインチャートレポーターインジケータがチャート上で見えるということです。

メインチャートレポーターインジケータ

メインチャートレポーターインジケータ

添付ファイル

結論

複数のシンボルのクオートやトレードトレードを視覚化するインジケータを検討しました。 HTML 形式のレポートは、インプットデータのソースとして機能します。 ユニバーサル パーサーを使用すると、標準レポート (ソース コードに既に含まれている設定) だけでなく、他のレポートの種類も解析できます。 また、mql5.comシグナルのトレードヒストリーが通常提供されるCSV形式のサポートを実装します。 オープンソースを使用することで、誰でもニーズに合わせてアプリケーションを調整できます。