【MQL4関数】OnCalculate関数とは?ロウソク足が動くたびに実行されるイベント関数

関数

OnCalculate関数とは?

OnCalculate()は、カスタムインジケータ専用のイベント関数で、チャートに新しい価格データ(ティック)が到着するたびに自動的に呼び出されます。ロウソク足の値動きが発生するたびに実行されるため、移動平均線やRSIなどのテクニカル指標の計算処理をこの関数の中に記述します。

EA(エキスパートアドバイザー)における OnTick() に相当する役割を持ちますが、OnCalculate()はインジケータバッファへの描画計算に特化した設計になっています。

OnCalculate関数の2つの書式

OnCalculate()には2つの書式があります。1つのインジケータで使えるのはどちらか一方のみです。

書式1:短縮形(シンプル版)

チャートの価格データ(始値・高値・安値・終値など)を直接受け取る形式です。最も一般的に使われます。

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[])         // スプレッド

書式2:OnInit()でのSetIndexBuffer連携版

他のインジケータの値を入力として使う場合に利用します。#property indicator_separate_windowを使うインジケータなどで使われます。

int OnCalculate(const int rates_total,      // 利用可能なデータ数
                const int prev_calculated,   // 前回呼び出し時の計算済みデータ数
                const int begin,             // 意味のあるデータの開始位置
                const double &price[])       // 計算対象の価格配列

引数の詳細解説

引数 説明
rates_total int チャート上の全バー数。Barsと同等の値
prev_calculated int 前回のOnCalculate()呼び出しで返した戻り値。初回は0
time[] datetime 各バーの開始時刻の配列
open[] double 各バーの始値の配列
high[] double 各バーの高値の配列
low[] double 各バーの安値の配列
close[] double 各バーの終値の配列
tick_volume[] long 各バーのティックボリューム
volume[] long 各バーの実ボリューム(対応サーバーのみ)
spread[] int 各バーのスプレッド(ポイント単位)
begin int (書式2のみ)有効データの開始インデックス
price[] double (書式2のみ)計算に使う価格データ配列

戻り値

OnCalculate()はint型の値を返します。この戻り値は、次回呼び出し時に prev_calculated 引数として渡されます

  • 通常は rates_total を返します(すべてのバーを計算済みという意味)
  • 0を返すと、次回の呼び出しで全バーを再計算します
  • 初回呼び出し時は prev_calculated が0になるため、全バーの計算が行われます

prev_calculatedの仕組み(重要)

prev_calculated は、OnCalculate()のパフォーマンス最適化の鍵となる引数です。毎回すべてのバーを計算し直すのではなく、新しいバーだけを計算することで処理を高速化します。

// prev_calculatedの動作イメージ
// 
// 1回目の呼び出し: prev_calculated = 0  → 全バーを計算
//   戻り値として rates_total(例:1000)を返す
//
// 2回目の呼び出し: prev_calculated = 1000
//   rates_total = 1000(新しいバーなし)→ 最後のバーだけ更新
//   rates_total = 1001(新しいバーあり)→ 新しいバーだけ計算

プログラム例1:最もシンプルな終値ラインインジケータ

まずは最も基本的な例として、終値をそのままラインとして描画するインジケータを作成します。OnCalculate()の基本構造とprev_calculatedの使い方を理解するのに最適です。

//+------------------------------------------------------------------+
//| 終値ラインインジケータ(OnCalculateの基本構造を学ぶ)             |
//+------------------------------------------------------------------+
#property indicator_chart_window        // メインチャートに表示
#property indicator_buffers 1           // バッファ数:1本
#property indicator_color1 clrDodgerBlue // ラインの色
#property indicator_width1 2            // ラインの太さ

// インジケータバッファ
double LineBuffer[];

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // バッファをインジケータラインとして割り当て
   SetIndexBuffer(0, LineBuffer);
   SetIndexStyle(0, DRAW_LINE);
   SetIndexLabel(0, "終値ライン");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - ティックごとに呼ばれるメイン関数                    |
//+------------------------------------------------------------------+
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[])
{
   // 計算開始位置を決定
   // prev_calculated == 0 なら全バーを計算(初回 or 再計算)
   // それ以外なら、最後のバーから計算(効率化)
   int start;
   if(prev_calculated == 0)
      start = 0;                        // 初回:最初のバーから
   else
      start = prev_calculated - 1;      // 2回目以降:最後のバーから
   
   // 計算ループ
   for(int i = start; i < rates_total; i++)
   {
      LineBuffer[i] = close[i];         // 終値をそのままバッファに格納
   }
   
   // rates_totalを返すことで、次回のprev_calculatedに反映される
   return(rates_total);
}

このコードのポイントは、prev_calculatedを活用した計算開始位置の決定です。初回はすべてのバーを計算し、2回目以降は最後のバーだけを更新するため、処理が非常に軽くなります。

プログラム例2:単純移動平均(SMA)インジケータ

次に、実用的な単純移動平均線(SMA)を自作します。外部パラメータで期間を変更でき、prev_calculatedによる最適化も実装しています。

//+------------------------------------------------------------------+
//| 自作SMA(単純移動平均線)インジケータ                              |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_color1 clrRed
#property indicator_width1 2

// 外部パラメータ(ユーザーが変更可能)
input int    MaPeriod = 20;             // 移動平均の期間
input int    MaShift  = 0;              // 表示をシフトするバー数

// インジケータバッファ
double MaBuffer[];

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   // パラメータチェック
   if(MaPeriod < 1)
   {
      Print("エラー: 期間は1以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   SetIndexBuffer(0, MaBuffer);
   SetIndexStyle(0, DRAW_LINE);
   SetIndexShift(0, MaShift);           // ラインの水平シフト
   SetIndexLabel(0, "SMA(" + IntegerToString(MaPeriod) + ")");
   
   // 最初のMaPeriod-1本は描画しない(データ不足のため)
   SetIndexDrawBegin(0, MaPeriod);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - SMAの計算処理                                       |
//+------------------------------------------------------------------+
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 < MaPeriod)
      return(0);
   
   // 計算開始位置の決定
   int start;
   if(prev_calculated == 0)
      start = MaPeriod - 1;             // 初回:期間分のデータが揃った位置から
   else
      start = prev_calculated - 1;      // 2回目以降:最後のバーから
   
   // SMAの計算ループ
   for(int i = start; i < rates_total; i++)
   {
      double sum = 0.0;
      
      // 現在のバーからMaPeriod本分の終値を合計
      for(int j = 0; j < MaPeriod; j++)
      {
         sum += close[i - j];
      }
      
      // 平均値をバッファに格納
      MaBuffer[i] = sum / MaPeriod;
   }
   
   return(rates_total);
}

SetIndexDrawBegin() を使って、データが不足する最初の数本のバーでは描画を行わないようにしている点に注目してください。

プログラム例3:高値・安値バンドインジケータ(複数バッファ)

複数のインジケータバッファを使い、指定期間の高値ラインと安値ラインを同時に描画する例です。ドンチャンチャネルに似たバンド型インジケータを作成します。

//+------------------------------------------------------------------+
//| 高値・安値バンドインジケータ(ドンチャンチャネル風)               |
//+------------------------------------------------------------------+
#property indicator_chart_window
#property indicator_buffers 3           // バッファ3本(高値・安値・中間線)
#property indicator_color1 clrDeepSkyBlue   // 高値バンド
#property indicator_color2 clrOrangeRed     // 安値バンド
#property indicator_color3 clrGray          // 中間線
#property indicator_width1 2
#property indicator_width2 2
#property indicator_style3 STYLE_DOT    // 中間線は点線

// 外部パラメータ
input int BandPeriod = 20;              // バンドの期間

// インジケータバッファ
double HighBand[];                      // 高値バンド
double LowBand[];                       // 安値バンド
double MidLine[];                       // 中間線

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   if(BandPeriod < 2)
   {
      Print("エラー: 期間は2以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   // バッファの割り当て
   SetIndexBuffer(0, HighBand);
   SetIndexBuffer(1, LowBand);
   SetIndexBuffer(2, MidLine);
   
   // 描画スタイルの設定
   SetIndexStyle(0, DRAW_LINE);
   SetIndexStyle(1, DRAW_LINE);
   SetIndexStyle(2, DRAW_LINE);
   
   // ラベル設定
   SetIndexLabel(0, "高値バンド(" + IntegerToString(BandPeriod) + ")");
   SetIndexLabel(1, "安値バンド(" + IntegerToString(BandPeriod) + ")");
   SetIndexLabel(2, "中間線");
   
   // 描画開始位置
   SetIndexDrawBegin(0, BandPeriod);
   SetIndexDrawBegin(1, BandPeriod);
   SetIndexDrawBegin(2, BandPeriod);
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - 高値・安値バンドの計算                               |
//+------------------------------------------------------------------+
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 < BandPeriod)
      return(0);
   
   // 計算開始位置
   int start;
   if(prev_calculated == 0)
      start = BandPeriod - 1;
   else
      start = prev_calculated - 1;
   
   // バンドの計算
   for(int i = start; i < rates_total; i++)
   {
      double highestHigh = high[i];     // 最高値の初期値
      double lowestLow   = low[i];      // 最安値の初期値
      
      // 過去BandPeriod本のバーから最高値・最安値を探す
      for(int j = 0; j < BandPeriod; j++)
      {
         if(high[i - j] > highestHigh)
            highestHigh = high[i - j];
         if(low[i - j] < lowestLow)
            lowestLow = low[i - j];
      }
      
      // バッファに格納
      HighBand[i] = highestHigh;        // 期間内の最高値
      LowBand[i]  = lowestLow;          // 期間内の最安値
      MidLine[i]   = (highestHigh + lowestLow) / 2.0;  // 中間値
   }
   
   return(rates_total);
}

3本のバッファを同時に使うことで、チャート上にバンド(帯域)を視覚的に表示できます。SetIndexStyle()で各ラインの線種を変えることで、見分けやすくなります。

プログラム例4:書式2を使った価格モメンタムインジケータ(サブウインドウ)

書式2(price[]配列を使う形式)の例です。サブウインドウに表示するモメンタム系インジケータを作成します。ユーザーがチャート上で「適用価格」を選択でき、その値がprice[]として渡されます。

//+------------------------------------------------------------------+
//| モメンタムインジケータ(OnCalculate書式2を使用)                   |
//+------------------------------------------------------------------+
#property indicator_separate_window      // サブウインドウに表示
#property indicator_buffers 2
#property indicator_color1 clrLimeGreen  // モメンタムライン
#property indicator_color2 clrRed        // ゼロライン用(水平線)
#property indicator_width1 2
#property indicator_level1 0.0           // ゼロラインを自動描画
#property indicator_levelcolor clrSilver

// 外部パラメータ
input int MomentumPeriod = 14;          // モメンタム期間

// インジケータバッファ
double MomentumBuffer[];
double SignalBuffer[];                   // シグナル用(SMA平滑化)

//+------------------------------------------------------------------+
//| 初期化関数                                                        |
//+------------------------------------------------------------------+
int OnInit()
{
   if(MomentumPeriod < 1)
   {
      Print("エラー: 期間は1以上を指定してください");
      return(INIT_PARAMETERS_INCORRECT);
   }
   
   SetIndexBuffer(0, MomentumBuffer);
   SetIndexBuffer(1, SignalBuffer);
   
   SetIndexStyle(0, DRAW_LINE);
   SetIndexStyle(1, DRAW_LINE);
   
   SetIndexLabel(0, "Momentum(" + IntegerToString(MomentumPeriod) + ")");
   SetIndexLabel(1, "Signal(9)");
   
   SetIndexDrawBegin(0, MomentumPeriod);
   SetIndexDrawBegin(1, MomentumPeriod + 9);
   
   // インジケータの短縮名(サブウインドウのタイトル)
   IndicatorShortName("My Momentum(" + IntegerToString(MomentumPeriod) + ")");
   
   return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnCalculate - 書式2:price[]配列を使う形式                        |
//| ※ユーザーがインジケータの「適用価格」で選択した値がprice[]に入る   |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
                const int prev_calculated,
                const int begin,             // 有効データの開始位置
                const double &price[])       // 適用価格の配列
{
   // データ不足チェック
   if(rates_total < MomentumPeriod + 1)
      return(0);
   
   // ===== モメンタムの計算 =====
   int start;
   if(prev_calculated == 0)
      start = MomentumPeriod + begin;   // beginを考慮して開始位置を決定
   else
      start = prev_calculated - 1;
   
   // モメンタム = 現在価格 - N期間前の価格
   for(int i = start; i < rates_total; i++)
   {
      MomentumBuffer[i] = price[i] - price[i - MomentumPeriod];
   }
   
   // ===== シグナルライン(モメンタムの9期間SMA)=====
   int signalPeriod = 9;
   int signalStart;
   
   if(prev_calculated == 0)
      signalStart = MomentumPeriod + begin + signalPeriod - 1;
   else
      signalStart = prev_calculated - 1;
   
   // signalStartがデータ範囲内か確認
   if(signalStart < MomentumPeriod + begin + signalPeriod - 1)
      signalStart = MomentumPeriod + begin + signalPeriod - 1;
   
   for(int i = signalStart; i < rates_total; i++)
   {
      double sum = 0.0;
      for(int j = 0; j < signalPeriod; j++)
      {
         sum += MomentumBuffer[i - j];
      }
      SignalBuffer[i] = sum / signalPeriod;
   }
   
   return(rates_total);
}

書式2では、ユーザーがインジケータのプロパティで「適用価格」(終値、始値、高値、安値、中間値など)を選択でき、その値がprice[]として自動的に渡されます。beginパラメータは、他のインジケータの出力を入力にチェーンする場合に、有効なデータの開始位置を示します。

よくある使い方のポイント・注意事項

1. prev_calculatedの正しい活用

OnCalculate()で最も重要なのはprev_calculatedの活用です。毎回全バーを再計算すると、バー数が多い場合に処理が重くなります。

// ◎ 正しい使い方:差分計算
int start;
if(prev_calculated == 0)
   start = 0;              // 初回のみ全バー計算
else
   start = prev_calculated - 1;  // -1 は最後のバー(形成中)を再計算するため

// ✕ 悪い使い方:毎回全バー計算(非効率)
for(int i = 0; i < rates_total; i++)  // 毎回全部計算してしまう

2. 配列のインデックス方向に注意

OnCalculate()の引数で渡される配列(close[], high[]など)は、デフォルトで時系列ではない点に注意が必要です。

// OnCalculate内の配列のインデックス
// close[0]                → 最も古いバーの終値
// close[rates_total - 1]  → 最新(現在形成中の)バーの終値
//
// ※通常のClose[0]が最新バーを指すのとは逆!
//
// もし時系列順にしたい場合は以下を使う(ただし通常は不要)
// ArraySetAsSeries(close, true);
// ※ただし const 配列なので変更できない場合がある

3. 戻り値は必ずrates_totalを返す

// 正常な計算が完了した場合
return(rates_total);     // 次回のprev_calculatedにこの値が入る

// エラーや計算不能の場合
return(0);               // 次回は全バーを再計算する

4. OnCalculate()はEAやスクリプトでは使えない

OnCalculate()はカスタムインジケータ(.mq4ファイルの#property indicator_~ がある場合)でのみ使用できます。EAではOnTick()、スクリプトではOnStart()を使用してください。

5. rates_totalの変化を検知して新しいバーを判定

int OnCalculate(const int rates_total,
                const int prev_calculated,
                /* 省略 */)
{
   // 新しいバーが形成されたかどうかの判定
   if(rates_total > prev_calculated)
   {
      // 新しいバーが追加された(または初回呼び出し)
      Print("新しいバーが追加されました。バー総数: ", rates_total);
   }
   else
   {
      // 既存のバー(最新バー)のティック更新
      // 最新バーの終値だけが変わっている
   }
   
   return(rates_total);
}

6. EMPTY_VALUEで描画をスキップ

特定の条件でラインを描画しない場合は、バッファにEMPTY_VALUEを設定します。

// 条件を満たさない場合は描画しない
if(close[i] < open[i])
   LineBuffer[i] = EMPTY_VALUE;  // この位置はラインが途切れる
else
   LineBuffer[i] = close[i];

まとめ

  • OnCalculate()はカスタムインジケータ専用のイベント関数で、ティックごとに自動実行される
  • 2つの書式があり、用途に応じて使い分ける(書式1がメイン、書式2は他インジケータの値を入力にする場合)
  • prev_calculatedを活用して差分計算を行い、パフォーマンスを最適化するのが基本
  • 引数の配列は時系列ではない(インデックス0が最古、rates_total-1が最新)
  • 戻り値としてrates_totalを返すことで、次回のprev_calculatedに計算済みバー数を伝える
  • 描画をスキップしたい場合はEMPTY_VALUEを使用する