2016年6月15日水曜日

[MT4プログラム]キャンバスを使ってポイントアンドフィギュアを描画しよう!

■キャンバスを使ったポイントアンドフィギュア

MQLには、いつぞかから、CCanvasクラスという関数を使ったビットマップ描画クラスが用意されています。

これを利用すると、MT4のオブジェクト配置機能だけでは表現不可能な画像をチャート上に生成できます。

サンプル例にポイントアンドフィギュアを描画してみました。

ソースコードが思ったより長くなってしまったため、かいつまんで説明します^^;;;;
CCanvasクラスはMQL5.comに日本語ドキュメントがありますので、そちらがとても参考になります。
https://www.mql5.com/ja/docs/standardlibrary/ccanvas
メソッド定義などは、こちらをご覧ください。

まず、CCanvasクラスを仕様するのに、次の一文が必要です。
#include <Canvas/Canvas.mqh>

今回はキャンバスをグローバルで一つ用意しました。
CCanvas m_canvas;


キャンバスに関する処理は、次の関数内にまとまっています。
void DrawCanvas

キャンバスの初期化は、CreateBitmapLabelメソッドを利用しています。これはOBJ_BITMAP_LABELタイプのオブジェクトベースにキャンバスを生成するという関数です。チャート上に固定のビットマップを表示します。今回はポイントアンドフィギュアを全面に出したいため、固定ビットマップをチャート一杯に貼り付ける形にしています。
canvas.CreateBitmapLabel(ChartID(), 0, OBJECT_NAME, 0, 0, chartWidth, chartHeight);


キャンバスクラスには、各描画メソッドが用意されています。今回は
FillRectangle : 矩形塗りつぶし
Line : 直線
TextOut : テキスト出力
Circle : 円描画

の描画関数を利用しています。

描画が終わったら、最後Updateメソッドをコールしないと反映されませんので注意しましょう。

実行してみるとポイントアンドフィギュアがチャート一杯に表示されます。チャートのサイズを変更す
ると自動追従はしませんが、一度クリックすると画面にフィットするようにしてあります。

ラインの線幅が指定できないなど、ちょっと困ったことはありましたが、とりえあずは、置いときました(汗)
描画に関するところはCanvas.mqh内に書かれているので自力修正は可能ですが、今回はそこまではしませんでした。

MT4のポイントアンドフィギュアインジケータは、たくさん公開されています。ただ、MT4のオブジェクト描画機能を強引に使っているため、縦横比率がおかしかったりしますよね。キャンバスを使うと、かなり綺麗に描画が出来ます。ただし、自力で描画が必要なため手間もかかります。

ちなみにインジケータとしては、本当に基本の描画しかないうえに、描画サンプルで作ってみたのでポイントアンドフィギュアとして正しいかはあまり検証していませんのでご注意ください^^;;;;;;

ポイントアンドフィギュアで形状検出まで作ったら買ってくれる人いるかなぁ?

・・・ちなみにチャートのウィンドウハンドルが取れるので、WindowsAPIを駆使して、ウィンドウ画面まるごと描画書き換えも不可能ではなかったりしますよねー。ネタがなくなったら試してみようかな?

「MT4でFXを勝ち抜く研究をするブログ」で公開している無料インジケータは、こちらの一覧から。
インジケータ一覧

Twitterもよろしくお願いします。
https://twitter.com/mt4program

ブログランキングにご協力よろしくお願いします。m(._.)m
にほんブログ村 為替ブログ MetaTraderへ
にほんブログ村

お約束ですが、本ブログは、投資に対する利益を約束する物ではありません。最終的には自己責任によるご判断よろしくお願いいたします。

プログラムはこちらから、長いので注意です。


//------------------------------------------------------------------
// キャンバスを使ったポイントアンドフィギュア

#property copyright "Copyright 2016,  Daisuke"
#property link      "http://mt4program.blogspot.jp/"
#property version   "1.00"
#property strict
#property indicator_chart_window

#include <Canvas/Canvas.mqh>
// オブジェクト名
#define OBJECT_NAME "OBJ_PAF_CANVAS"

// ポイントアンドフィギュアフラグ値
enum ENUM_PAF_BUYSELL
{
   PAF_NONE = 0 ,
   PAF_BUY,
   PAF_SELL,
};


//プロット数を指定する。
input double WindowSizePips = 20;   //1枠サイズ(Pips)
input int ReverseSize = 3;          //転換サイズ(1以上)
input int ScanCount = 1000;         //P&F計算バー数
input int MaxPAFBarCount = 100;     //最大P&Fバー数

input color BuyColor = clrOrangeRed;   //買い色
input color SellColor = clrDodgerBlue; //売り色

input ENUM_TIMEFRAMES TimeFrame = PERIOD_CURRENT;  //対象タイムフレーム
input int FontSize = 12;             // フォントサイズ

//1Pips
double m_onePips = 0 ;

//1Pips Int換算
int m_oneIntPips = 0 ;

//桁数
int m_digit = 0 ;

//1枠サイズ 実値
double m_windowSizeValue = 0 ;

//整数化倍率
double m_toIntRate = 0;

//1枠サイズ(整数化値)
int m_windowSizeInt = 0 ;

// キャンバス
CCanvas m_canvas;

//------------------------------------------------------------------
//初期化
int OnInit()
{
   // 短縮名を設定
   string shortName = "P&F";
   IndicatorShortName(shortName);

   if( ReverseSize < 1 ) return INIT_PARAMETERS_INCORRECT; 
   if( iBars(NULL, 0) < ScanCount ) return  INIT_PARAMETERS_INCORRECT; 

   //Pips計算 小数点桁数が3or5の場合、Point()*10=1pips
   m_digit = (int)MarketInfo(NULL, MODE_DIGITS);
   m_onePips = (double)MarketInfo(NULL, MODE_POINT) * (m_digit == 3 || m_digit == 5 ? 10 : 1);

   //上限下限初期値を設定する。
   m_toIntRate = MathPow(10, m_digit);

   m_windowSizeValue = WindowSizePips * m_onePips;
   m_windowSizeInt = (int)(m_windowSizeValue * m_toIntRate);
   m_oneIntPips = (int)(m_onePips * m_toIntRate);

   return(INIT_SUCCEEDED);
}

//------------------------------------------------------------------
//終了処理
void OnDeinit( const int reason )
{
   m_canvas.Destroy();
}

//------------------------------------------------------------------
//チャート更新イベント
void OnChartEvent(const int id,         // Event ID 
                  const long& lparam,   // Parameter of type long event 
                  const double& dparam, // Parameter of type double event 
                  const string& sparam  // Parameter of type string events 
)
{
   if(id == CHARTEVENT_OBJECT_CLICK)
   {
      if(sparam == OBJECT_NAME)
      {
         //キャンバスのサイズとチャートのサイズが異なる場合は、再描画
         int chartWidth = (int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS);
         int chartHeight = (int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS);
         
         if( m_canvas.Width() != chartWidth || m_canvas.Height() != chartHeight )
         {
            int paf[];
            int resolution;
            int columnCount;
         
            CalclatePointAndFigure(paf, resolution, columnCount, TimeFrame, ScanCount, m_toIntRate, m_windowSizeInt, MaxPAFBarCount, ReverseSize);
            DrawCanvas(m_canvas, paf, resolution, columnCount, TimeFrame, ScanCount, m_toIntRate, m_windowSizeInt, m_digit, FontSize, BuyColor, SellColor);
         }
      }
   }
}

//------------------------------------------------------------------
//計算イベント
int OnCalculate(
   const int rates_total,          //各レート要素数
 const int prev_calculated,      //計算済み要素数
 const datetime &time[],         //要素ごとの時間配列
 const double &open[],           //オープン価格配列
 const double &high[],           //高値配列
 const double &low[],            //安値配列
 const double &close[],          //クローズ価格配列
 const long &tick_volume[],      //ティック数(要素の更新回数)
 const long &volume[],           //実ボリューム(?)
 const int &spread[])            //スプレット
{
   if( rates_total == prev_calculated ) return rates_total;

   int paf[];
   int resolution;
   int columnCount;

   CalclatePointAndFigure(paf, resolution, columnCount, TimeFrame, ScanCount, m_toIntRate, m_windowSizeInt, MaxPAFBarCount, ReverseSize);
   DrawCanvas(m_canvas, paf, resolution, columnCount, TimeFrame, ScanCount, m_toIntRate, m_windowSizeInt, m_digit, FontSize, BuyColor, SellColor);

   return rates_total;
}

//------------------------------------------------------------------
//キャンバスに描画する
void DrawCanvas(
   CCanvas &canvas
,  const int &paf[]           //結果配列
,  int resolution             //解像度
,  int columnCount            //生成列数
,  ENUM_TIMEFRAMES timeFrame  //対象タイムフレーム
,  int scanCount              //探査バー数
,  double toIntRate           //価格→Int化レート
,  int windowSizeInt          //1枠サイズ(INT値)
,  int digit                  //小数点桁数
,  int fontSize               //フォントサイズ
,  color buyColor             //買色
,  color sellColor            //売色
)
{
   int chartWidth = (int)ChartGetInteger(ChartID(), CHART_WIDTH_IN_PIXELS);
   int chartHeight = (int)ChartGetInteger(ChartID(), CHART_HEIGHT_IN_PIXELS);
   
   //固定ビットマップを生成する。
   canvas.CreateBitmapLabel(ChartID(), 0, OBJECT_NAME, 0, 0, chartWidth, chartHeight);

   //余白
   int graphLeft = 10;
   int graphRight = 40;
   int graphTop = 10;
   int graphBottom = 20;

   //背景
   m_canvas.FillRectangle(0, 0, chartWidth, chartHeight, COLOR2RGB(clrWhite));
   
   // グリッド 
   int oneWidth = (int)((chartWidth - (graphLeft + graphRight)) / columnCount);
   int oneHeight = (int)((chartHeight - (graphTop + graphBottom)) / resolution);
   
   int oneSize = oneWidth < oneHeight ? oneWidth : oneHeight;
   
   // グリッド縦線
   for(int x = chartWidth - graphRight - oneSize; x >= graphLeft; x -= oneSize )
   {
      canvas.Line(x, graphTop, x, chartHeight - graphBottom, COLOR2RGB(clrAliceBlue));
   }

   // グリッド横線
   for(int y = chartHeight - graphBottom - oneSize; y >= graphTop; y -= oneSize )
   {
      canvas.Line(graphLeft, y, chartWidth - graphRight, y, COLOR2RGB(clrAliceBlue));
   }
   
   //枠線の描画 外枠
   canvas.Line(graphLeft, chartHeight - graphBottom, chartWidth - graphRight, chartHeight - graphBottom, COLOR2RGB(clrBlack));
   canvas.Line(chartWidth - graphRight, graphTop, chartWidth - graphRight, chartHeight - graphBottom, COLOR2RGB(clrBlack));
   
   //最大値 最小値を計算する。
   int lowest = (int)(iClose(NULL, timeFrame, iLowest(NULL, 0, MODE_CLOSE, scanCount, 0)) * toIntRate);
   int chartLowest = (lowest - lowest % windowSizeInt);

   //価格ラベルの描画
   canvas.FontSizeSet(fontSize);
   int textHeight = canvas.TextHeight("0123");

   int labelStep = textHeight / oneSize + 1;
   int textOffset = (oneSize - textHeight) / 2;
   if( textOffset < 0 ) textOffset == 0;
   
   for( int i = 0; i < resolution; i += labelStep )
   {
      canvas.TextOut(
            chartWidth - graphRight + 1
         ,  chartHeight - graphBottom - (i + 1) * oneSize + textOffset
         ,  DoubleToStr((chartLowest + i * windowSizeInt) / toIntRate, digit), COLOR2RGB(clrBlack));
   }
   
   //ポイント&フィギュアの描画
   for( int i = 0; i < columnCount; i++ )
   {
      for(int j = 0; j < resolution; j++ )
      {
         int index = i * resolution + j;
         
         if( paf[index] != PAF_NONE )
         {
            //描画位置の計算
            int x = chartWidth - graphRight - (columnCount - i) * oneSize;
            int y = chartHeight - graphBottom - (j + 1) * oneSize;
            
            if( paf[index] == PAF_BUY )
            {
               canvas.Line(x + 2, y + 2, x + oneSize - 2, y + oneSize - 2, COLOR2RGB(buyColor));
               canvas.Line(x + 2, y + oneSize - 2, x + oneSize - 2, y + 2, COLOR2RGB(buyColor));
            }
            else
            {
               canvas.Circle(x + oneSize / 2, y + oneSize / 2, oneSize / 2 - 1, COLOR2RGB(sellColor));
            }
         }
      }
   }
   canvas.Update();
}

//------------------------------------------------------------------
//ポイントアンドフィギュアを計算する
void CalclatePointAndFigure
(
   int &paf[]                 //結果配列
,  int &resolution            //解像度
,  int &columnCount           //生成列数
,  ENUM_TIMEFRAMES timeFrame  //対象タイムフレーム
,  int scanCount              //探査バー数
,  double toIntRate           //価格→Int化レート
,  int windowSizeInt          //1枠サイズ(INT値)
,  int maxPAFBarCount         //最大P&Fバー数
,  int reverseSize            //転換バー数
)
{
   //最大値 最小値を計算する。
   int highest = (int)(iClose(NULL, timeFrame, iHighest(NULL, 0, MODE_CLOSE, scanCount, 0)) * toIntRate);
   int lowest = (int)(iClose(NULL, timeFrame, iLowest(NULL, 0, MODE_CLOSE, scanCount, 0)) * toIntRate);
  
   int chartHighest = (highest - highest % windowSizeInt + windowSizeInt );
   int chartLowest = (lowest - lowest % windowSizeInt);
   
   //縦幅解像度
   resolution = (int)((chartHighest - chartLowest) / windowSizeInt);

   // ポイントアンドフィギュア 
   ArrayResize(paf, resolution * maxPAFBarCount);
   ArrayInitialize(paf, PAF_NONE);
   
   //最初の1点
   int close0 = (int)(iClose(NULL, timeFrame, scanCount - 1) * toIntRate);
   int close1 = (int)(iClose(NULL, timeFrame, scanCount - 2) * toIntRate);
   
   int currentColumn = 0;
   int beforeIndex = (close1 - chartLowest) / windowSizeInt + currentColumn * resolution;
   int beforeFlag = close0 < close1 ? PAF_BUY : PAF_SELL;
   paf[beforeIndex] = beforeFlag;
   
   for( int i = scanCount - 3; i >= 1; i-- )
   {
      int point = (int)(iClose(NULL, timeFrame, i) * toIntRate);
      int index = (point - chartLowest) / windowSizeInt + currentColumn * resolution;
    
      if( beforeFlag == PAF_BUY )
      {
         if( beforeIndex < index )
         {
            // 同じ方向で価格が上昇した場合
            for( int j = beforeIndex + 1; j <= index; j++)
            {
               paf[j] = PAF_BUY;
            }
            beforeIndex = index;
         }
         else if( beforeIndex > index && beforeIndex - index >= reverseSize )
         {
            // 転換枠数を超えた場合
            // 次の列へ
            
            int localIndex = beforeIndex - currentColumn * resolution;
            
            currentColumn++;
            if( currentColumn >= maxPAFBarCount )
            {
               //列を一つ移動する。
               ArrayLeftShift(paf, resolution);
               currentColumn--;
            }
            
            index += resolution;
            for( int j = (localIndex - 1) + currentColumn * resolution; j >= index; j--)
            {
               paf[j] = PAF_SELL;
            }
            
            beforeFlag = PAF_SELL;
            beforeIndex = index;
         }
      }
      else
      {
         if( beforeIndex > index )
         {
            // 同じ方向で価格が上昇した場合
            for( int j = beforeIndex - 1; j >= index; j--)
            {
               paf[j] = PAF_SELL;
            }
            beforeIndex = index;
         }
         else if( beforeIndex < index && index - beforeIndex >= reverseSize )
         {
            // 転換枠数を超えた場合
            // 次の列へ
            
            int localIndex = beforeIndex - currentColumn * resolution;
            
            currentColumn++;
            if( currentColumn >= maxPAFBarCount )
            {
               //列を一つ移動する。
               ArrayLeftShift(paf, resolution);
               currentColumn--;
            }

            index += resolution;
            for( int j = (localIndex + 1) + currentColumn * resolution; j <= index; j++)
            {
               paf[j] = PAF_BUY;
            }
            
            beforeFlag = PAF_BUY;
            beforeIndex = index;
         }
      }
   }
   columnCount = currentColumn + 1;
}

//------------------------------------------------------------------
//配列要素を指定量左シフトする
void ArrayLeftShift(
   int &array[]     //配列
,  int shiftValue    //シフト量
)
{
   int size = ArraySize(array);
   
   for( int i = shiftValue; i < size; i++)
   {
      array[i - shiftValue] = array[i];
   }
}