English Deutsch
preview
MQL5での取引戦略の自動化(第43回):適応型線形回帰チャネル戦略

MQL5での取引戦略の自動化(第43回):適応型線形回帰チャネル戦略

MetaTrader 5トレーディングシステム |
64 0
Allan Munene Mutiiria
Allan Munene Mutiiria

はじめに

前回の記事(第42回)では、MetaQuotes Language 5 (MQL5)において、カスタムのセッション開始時刻と分単位のオープニングレンジ期間を設定可能にし、選択した時間足で実際の高値・安値を自動判定し、ブレイクアウト方向にのみエントリーする、セッションベースのオープニングレンジブレイクアウト(ORB)システムを開発しました。第43回では、適応型線形回帰チャネル戦略を開発します。

このシステムは、ユーザー定義期間にわたって線形回帰ラインと標準偏差バンドを計算します。また、トレンド相場であることを保証するため、絶対傾きが最小閾値を超えた場合にのみ有効化されます。さらに、価格がチャネル幅の設定可能な割合を超えて乖離した場合にはチャネルを自動的に再生成し、チャネル内からの明確なブレイクアウト時にエントリーします。本記事では以下のトピックを扱います。

  1. 適応型線形回帰チャネルフレームワークの理解
  2. MQL5での実装
  3. バックテスト
  4. 結論

本記事の終わりまでに、塗りつぶされた偏差ゾーン、ブレイクアウト検出、中央ラインクロスによる決済、通常モードと逆モードを備えた、動的な回帰チャネルを維持する実用的なMQL5プログラムを手に入れることができます。それでは進めていきましょう。


適応型線形回帰チャネルフレームワークの理解

線形回帰チャネル戦略は、一定数のバーに対して最小二乗法による線形回帰直線を適用し、基礎となるトレンドの方向と強さを特定し、その後、回帰直線の上下に指定した標準偏差分だけ平行なバンドを追加して上限と下限の境界を形成します。これにより、トレンド相場における期待される価格範囲を表す動的な価格チャネルが生成されます。価格がチャネル内で振動している場合はトレンド継続を示し、境界へのタッチや軽微なブレイクアウトは押し目と戻りのエントリー機会を提供し、大きな乖離はトレンドの消耗や新しいデータに基づく回帰の再計算の必要性を示唆します。通常は、価格が下側チャネルの下で決済した場合に買い、上側チャネルの上で決済した場合に売りをおこないます。

本実装では、設定可能な期間にわたって回帰の傾き、切片、および標準偏差を計算し、方向性のある動きを確認するために絶対傾きが最小閾値を超えた場合にのみチャネルを作成します。チャネルは期間内の最も古いバーを起点として、その長さの一定割合だけ未来に延長されます。また、上半分(ピンク)と下半分(ライトグリーン)の2つの塗りつぶしゾーンと、上限、中間、下限の各境界に対する実線のトレンドラインで構成されます。新しいバーが作成されるたびに、価格がチャネル内に収まっている場合はチャネルを右方向に1バー延長し、価格がチャネル幅の定義された割合を超えて乖離した場合はチャネルを完全に再生成します。

取引は、チャネル内からの明確なブレイクアウト時にエントリーし、固定pipsのストップロス/テイクプロフィット、方向ごとの最大同時ポジション数、逆モードオプションを備えています。すべての同方向ポジションは、価格が中間ラインを横切った時点で即座に決済されます。上限、中間、下限チャネルのラベルは右端に追従し、すべてのエントリーには矢印が表示されます。逆モードでは売買ロジックが単純に入れ替わり、同一プログラムでトレンド継続ではなく平均回帰型のブレイクアウトを取引することが可能になります。これは、逆の戦略を取りたい場合に有用であると考えられます。要するに、以下は本システムの目的を視覚的に示したものです。

線形回帰チャネルフレームワーク


MQL5での実装

MQL5でプログラムを作成するには、まずMetaEditorを開き、ナビゲーターで[Experts]フォルダを探します。[新規]タブをクリックして指示に従い、ファイルを作成します。ファイルが作成されたら、コーディング環境で、まずプログラム全体で使用する入力パラメータグローバル変数をいくつか宣言する必要があります。

//+------------------------------------------------------------------+
//|                                 Linear Regression Channel EA.mq5 |
//|                           Copyright 2025, Allan Munene Mutiiria. |
//|                                   https://t.me/Forex_Algo_Trader |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, Allan Munene Mutiiria."
#property link      "https://t.me/Forex_Algo_Trader"
#property version   "1.00"

#include <Trade\Trade.mqh>

//+------------------------------------------------------------------+
//| Global Variables                                                 |
//+------------------------------------------------------------------+
CTrade obj_Trade;                                                 //--- Trade object

//+------------------------------------------------------------------+
//| Enums                                                            |
//+------------------------------------------------------------------+
enum TradeMode {                                                  // Define trade mode enum
   Normal,                                                        // Normal
   Inverse                                                        // Inverse
};

//+------------------------------------------------------------------+
//| Input Parameters                                                 |
//+------------------------------------------------------------------+
input int      RegressionPeriod       = 100;                      // Period for regression calculation
input double   Deviations             = 2.0;                      // Standard deviation multiplier for channel
input double   MinSlopeThreshold      = 0.00001;                  // Min absolute slope to identify clear trend (low for detection)
input int      UpdateThresholdPercent = 30;                       // Update threshold in percent of channel width (e.g., 30 for 30%)
input double   ExtensionPercent       = 50.0;                     // Initial extension percent of channel length to the right
input TradeMode TradeDirection        = Normal;                   // Trade Mode
input double   Lots                   = 0.01;                     // Lot size
input int      StopLossPips           = 100;                      // Stop loss in pips
input int      TakeProfitPips         = 100;                      // Take profit in pips
input int      MaxBuys                = 2;                        // Maximum open buy positions
input int      MaxSells               = 2;                        // Maximum open sell positions
input int      MagicNumber            = 123456;                   // Magic number for positions
input int      Slippage               = 3;                        // Slippage

実装は、#include <Trade\Trade.mqh>でtradeライブラリをインクルードすることから開始します。これは、注文の実行およびポジション管理をおこなうためのCTradeクラスを提供します。プログラム全体で注文送信やポジションの変更に使用するために、CTradeクラスからobj_Tradeオブジェクトをグローバルに宣言します。

次に、TradeMode列挙型を定義し、2つのオプションを用意します。Normalは標準的なチャネルへのプルバックトレード(上昇トレンドでは下側バンド下への押しで買い、下降トレンドでは上側バンド上への戻りで売り)を意味し、Inverseはロジックを反転させて平均回帰型のブレイクアウトトレードをおこなうためのものです。続いて、プログラムのプロパティから直接調整可能な入力パラメータを設定します。これには、線形回帰計算に使用するバー数を指定するRegressionPeriod、チャネル幅の標準偏差倍率(一般的には約95%の包含を想定して2.0が用いられます)であるDeviations、市場がトレンド状態にあると見なしてチャネルを作成するために必要な最小絶対傾きを示すMinSlopeThresholdが含まれます。そのほかのパラメータについても、理解しやすいようにコメントを追加しています。これらの入力により、プログラムを変更することなく、チャネルの挙動、リスク管理、取引スタイルを完全に制御することができます。ここで使用している値はデフォルトであり、適応のためにいつでも変更可能です。コンパイルすると、以下のウィンドウが表示されるはずです。

入力パラメータウィンドウ

入力の設定が完了したので、プログラム全体で使用するいくつかのグローバル変数の定義に進むことができます。

//--- Global variables
datetime lastBarTime    = 0;                                      //--- Last bar time
double   channelUpper, channelLower, channelMiddle;               //--- Channel levels
string   channelName    = "LRC_Channel";                          //--- Channel name
string   upperLabelName = "LRC_Upper_Label";                      //--- Upper label name
string   middleLabelName = "LRC_Middle_Label";                    //--- Middle label name
string   lowerLabelName = "LRC_Lower_Label";                      //--- Lower label name
bool     hasValidChannel = false;                                 //--- Valid channel flag
datetime fixedTimeOld   = 0;                                      //--- Fixed old time
double   slope_global   = 0, intercept_global = 0, stdDev_global = 0; //--- Global slope, intercept, stdDev
long     period_sec     = PeriodSeconds(_Period);                 //--- Period seconds
int      arrowCounter   = 0;                                      //--- Arrow counter
double   current_right_x = 0;                                     //--- Current right x

続いて、適応型チャネルのロジックと可視化をサポートするために、追加のグローバル変数を宣言します。lastBarTimeは、直近で処理されたバーのタイムスタンプを追跡するために使用します。現在のチャネルの予測レベルは、ブレイクアウト判定時に素早く参照できるよう、channelUpper、channelLower、channelMiddleに格納します。定数文字列ではオブジェクト名を定義します。channelNameはマルチパートのチャネルオブジェクトのベースとしてLRC_Channelを使用します。upperLabelName、middleLabelName、lowerLabelNameはそれぞれ各境界を識別する移動テキストラベルの名前です。

ブール値のhasValidChannelフラグは現在トレンド回帰チャネルが有効かどうかを示します。fixedTimeOldは回帰期間内で最も古いバーの日時を保持し、x座標計算の一貫性を確保します。計算された回帰パラメータはslope_global、intercept_global、stdDev_globalとしてグローバルに保持します。これにより毎ティックごとに再計算することなく将来のプロジェクションに再利用できます。period_secはPeriodSeconds(_Period)によって1バーあたりの秒数を取得し、時間からx座標への正確な変換に使います。arrowCounterはエントリー用の矢印に一意な名前を付けるための整数カウンタです。current_right_xはチャネル右端のx座標を追跡し、バーごとにチャネルを拡張できるようにします。これによりチャネルオブジェクト全体を再生成せずに1バー単位で正確に延長でき、描画のちらつきを防げます。以上で準備が整ったので、チャネル描画用のヘルパー関数の定義から実装を始めます。

//+-----------------------------------------------------------------------------------+
//| Create Linear Regression Channel using channels for fill and trendlines for lines |
//+-----------------------------------------------------------------------------------+
bool ChannelCreate(const long chart_ID, const string name, const int sub_window, datetime time1, datetime time2) {
   double price1_middle = intercept_global;                       //--- Price1 middle
   double price2_middle = intercept_global + slope_global * current_right_x; //--- Price2 middle
   double price1_upper = price1_middle + Deviations * stdDev_global; //--- Price1 upper
   double price2_upper = price2_middle + Deviations * stdDev_global; //--- Price2 upper
   double price1_lower = price1_middle - Deviations * stdDev_global; //--- Price1 lower
   double price2_lower = price2_middle - Deviations * stdDev_global; //--- Price2 lower
   // Upper-middle fill channel
   string um_name = name + "_um";                                 //--- UM name
   if (!ObjectCreate(chart_ID, um_name, OBJ_CHANNEL, sub_window, time1, price1_upper, time2, price2_upper, time1, price1_middle)) { //--- Create UM channel
      Print(__FUNCTION__, ": failed to create upper-middle channel! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, um_name, OBJPROP_COLOR, clrPink);   //--- Set color
   ObjectSetInteger(chart_ID, um_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, um_name, OBJPROP_WIDTH, 1);         //--- Set width
   ObjectSetInteger(chart_ID, um_name, OBJPROP_FILL, true);       //--- Set fill
   ObjectSetInteger(chart_ID, um_name, OBJPROP_BACK, true);       //--- Set back
   ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, um_name, OBJPROP_SELECTED, false);  //--- Set not selected
   ObjectSetInteger(chart_ID, um_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, um_name, OBJPROP_HIDDEN, false);    //--- Set not hidden
   ObjectSetInteger(chart_ID, um_name, OBJPROP_ZORDER, 0);        //--- Set zorder
   // Middle-lower fill channel
   string ml_name = name + "_ml";                                 //--- ML name
   if (!ObjectCreate(chart_ID, ml_name, OBJ_CHANNEL, sub_window, time1, price1_middle, time2, price2_middle, time1, price1_lower)) { //--- Create ML channel
      Print(__FUNCTION__, ": failed to create middle-lower channel! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_COLOR, clrLightGreen); //--- Set color
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_WIDTH, 1);         //--- Set width
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_FILL, true);       //--- Set fill
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_BACK, true);       //--- Set back
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_SELECTED, false);  //--- Set not selected
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_HIDDEN, false);    //--- Set not hidden
   ObjectSetInteger(chart_ID, ml_name, OBJPROP_ZORDER, 0);        //--- Set zorder
   // Upper trendline
   string upper_name = name + "_upper";                           //--- Upper name
   if (!ObjectCreate(chart_ID, upper_name, OBJ_TREND, sub_window, time1, price1_upper, time2, price2_upper)) { //--- Create upper trend
      Print(__FUNCTION__, ": failed to create upper trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_COLOR, clrRed); //--- Set color
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_WIDTH, 1);      //--- Set width
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_BACK, false);   //--- Set foreground
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, upper_name, OBJPROP_ZORDER, 1);     //--- Set zorder
   // Middle trendline
   string middle_name = name + "_middle";                         //--- Middle name
   if (!ObjectCreate(chart_ID, middle_name, OBJ_TREND, sub_window, time1, price1_middle, time2, price2_middle)) { //--- Create middle trend
      Print(__FUNCTION__, ": failed to create middle trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_COLOR, clrBlue); //--- Set color
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_WIDTH, 1);     //--- Set width
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_BACK, false);  //--- Set foreground
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, middle_name, OBJPROP_ZORDER, 1);    //--- Set zorder
   // Lower trendline
   string lower_name = name + "_lower";                           //--- Lower name
   if (!ObjectCreate(chart_ID, lower_name, OBJ_TREND, sub_window, time1, price1_lower, time2, price2_lower)) { //--- Create lower trend
      Print(__FUNCTION__, ": failed to create lower trendline! Error code = ", GetLastError()); //--- Log error
      return(false);                                              //--- Return failure
   }
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_COLOR, clrGreen); //--- Set color
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_WIDTH, 1);      //--- Set width
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_BACK, false);   //--- Set foreground
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTABLE, true); //--- Set selectable
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_SELECTED, false); //--- Set not selected
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_RAY_RIGHT, false); //--- Set no ray
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_HIDDEN, false); //--- Set not hidden
   ObjectSetInteger(chart_ID, lower_name, OBJPROP_ZORDER, 1);     //--- Set zorder
   return(true);                                                  //--- Return success
}

//+------------------------------------------------------------------+
//| Delete the channel                                               |
//+------------------------------------------------------------------+
bool ChannelDelete(const long chart_ID, const string name) {
   bool success = true;                                           //--- Init success
   if (!ObjectDelete(chart_ID, name + "_um")) {                   //--- Delete um
      Print(__FUNCTION__, ": failed to delete um! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_ml")) {                   //--- Delete ml
      Print(__FUNCTION__, ": failed to delete ml! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_upper")) {                //--- Delete upper
      Print(__FUNCTION__, ": failed to delete upper! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_middle")) {               //--- Delete middle
      Print(__FUNCTION__, ": failed to delete middle! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   if (!ObjectDelete(chart_ID, name + "_lower")) {                //--- Delete lower
      Print(__FUNCTION__, ": failed to delete lower! Error code = ", GetLastError()); //--- Log error
      success = false;                                            //--- Set failure
   }
   return(success);                                               //--- Return success
}

ここではChannelCreate関数を実装し、単一の組み込みチャネルオブジェクトに依存せず、塗りつぶしチャネルとトレンドラインを組み合わせて視覚的な線形回帰チャネルを構築します。これによりカラーゾーンと明確な境界ラインを実現します。ここまでで分かる通り、挙動を明確に再現するために可視化にも同じくらい注意を払っています。まず保存済みのグローバル回帰値を使ってチャネル両端の正確な価格座標を計算します。左側の中央価格はintercept_global、右側の中央は「intercept_global + slope_global * current_right_x」とし、そこから「Deviations * stdDev_global」を加減することで上下の価格を両端それぞれで求めます。

塗りつぶしゾーンの作成では、最初に上部と中央のセクションを構築します。ベース名に_umを付加して一意の名前を作成し、ObjectCreateOBJ_CHANNELを使って左上から右上、左中央の3点でチャネルを描画します。このチャネルはピンク色、実線、幅1、塗りつぶし有効、背景配置、選択可能だが未選択、右方向レイなし、非表示ではない、zオーダー0に設定します。同様の手順で中央と下部のセクションも_mlサフィックスで作成します。左中央から右中央、左下の3点で別のOBJ_CHANNELを作成し、こちらはライトグリーンで同じスタイル設定にすることで2トーンの塗り分けを実現します。

次に視認性を高めるため、3本の境界ラインを個別のトレンドラインとして描画します。「_upper」サフィックスで左上から右上のOBJ_TRENDを作成し、色は赤、実線、幅1、前面表示、選択可能、レイなし、zオーダー1に設定します。中央は_middleで青、下部は_lowerで緑として同様に作成します。塗りとラインを分離することで、価格がチャネル内にあっても色分けと境界の両方が明確に維持されます。いずれかのObjectCreateが失敗した場合はGetLastErrorでログ出力しfalseを返します。すべて成功した場合はtrueを返します。

あわせてChannelDelete関数も定義し、チャネルを再作成またはリセットする際に5つの構成要素をまとめて削除できるようにします。_um、_ml、_upper、_middle、_lowerの各オブジェクトをフルネームで削除し、失敗があればログを出しますが処理は継続します。最終的にすべて削除できればtrue、いずれかでエラーがあればfalseを返します。削除にはObjectDeleteを使用します。削除にはObjectDeleteを使用します。これらの関数により、価格の大きなブレイクアウトや新しいトレンド発生時にチャネル全体を再構築できるようになります。今後は矢印によるシグナル表示とチャネルラベルの更新も実装していきます。

//+------------------------------------------------------------------+
//| Draw arrow on chart for signal                                   |
//+------------------------------------------------------------------+
void DrawArrow(bool isBuy, datetime time, double price) {
   string name = "SignalArrow_" + IntegerToString(arrowCounter++);  //--- Arrow name
   ObjectCreate(0, name, OBJ_ARROW, 0, time, price);                //--- Create arrow
   ObjectSetInteger(0, name, OBJPROP_ARROWCODE, isBuy ? 233 : 234); //--- Set code
   ObjectSetInteger(0, name, OBJPROP_COLOR, isBuy ? clrGreen : clrRed); //--- Set color
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 2);                     //--- Set width
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); //--- Set anchor
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);            //--- Set not selectable
   ChartRedraw(0);                                                  //--- Redraw chart
}

//+------------------------------------------------------------------+
//| Update channel labels                                            |
//+------------------------------------------------------------------+
void UpdateLabels(datetime labelTime) {
   double label_x = (double)(labelTime - fixedTimeOld) / period_sec; //--- Calc label x
   double middlePrice = intercept_global + slope_global * label_x;   //--- Calc middle
   double upperPrice = middlePrice + Deviations * stdDev_global;     //--- Calc upper
   double lowerPrice = middlePrice - Deviations * stdDev_global;     //--- Calc lower
   // Upper label
   if (ObjectFind(0, upperLabelName) < 0) {                          //--- Check no upper label
      ObjectCreate(0, upperLabelName, OBJ_TEXT, 0, labelTime, upperPrice); //--- Create upper label
   } else {                                                          //--- Exists
      ObjectMove(0, upperLabelName, 0, labelTime, upperPrice);       //--- Move upper label
   }
   ObjectSetString(0, upperLabelName, OBJPROP_TEXT, "Upper Channel"); //--- Set text
   ObjectSetInteger(0, upperLabelName, OBJPROP_COLOR, clrRed);       //--- Set color
   ObjectSetInteger(0, upperLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER); //--- Set anchor
   ObjectSetInteger(0, upperLabelName, OBJPROP_SELECTABLE, false);   //--- Set not selectable
   // Middle label
   if (ObjectFind(0, middleLabelName) < 0) {                         //--- Check no middle label
      ObjectCreate(0, middleLabelName, OBJ_TEXT, 0, labelTime, middlePrice); //--- Create middle label
   } else {                                                          //--- Exists
      ObjectMove(0, middleLabelName, 0, labelTime, middlePrice);     //--- Move middle label
   }
   ObjectSetString(0, middleLabelName, OBJPROP_TEXT, "Middle Channel"); //--- Set text
   ObjectSetInteger(0, middleLabelName, OBJPROP_COLOR, clrBlue);     //--- Set color
   ObjectSetInteger(0, middleLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor
   ObjectSetInteger(0, middleLabelName, OBJPROP_SELECTABLE, false);  //--- Set not selectable
   // Lower label
   if (ObjectFind(0, lowerLabelName) < 0) {                          //--- Check no lower label
      ObjectCreate(0, lowerLabelName, OBJ_TEXT, 0, labelTime, lowerPrice); //--- Create lower label
   } else {                                                          //--- Exists
      ObjectMove(0, lowerLabelName, 0, labelTime, lowerPrice);       //--- Move lower label
   }
   ObjectSetString(0, lowerLabelName, OBJPROP_TEXT, "Lower Channel"); //--- Set text
   ObjectSetInteger(0, lowerLabelName, OBJPROP_COLOR, clrGreen);     //--- Set color
   ObjectSetInteger(0, lowerLabelName, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); //--- Set anchor
   ObjectSetInteger(0, lowerLabelName, OBJPROP_SELECTABLE, false);   //--- Set not selectable
   ChartRedraw(0);                                                   //--- Redraw chart
}

次にDrawArrow関数を実装し、トレードシグナルが発生した際にチャート上へ明確な視覚マーカーを配置します。オブジェクト名はSignalArrow_とインクリメントされるarrowCounterを組み合わせて一意に生成し、指定された時間と価格にOBJ_ARROW を作成します。買いシグナルにはwingdingsの233上向き矢印、売りシグナルには234下向き矢印を使用します。買いは緑、売りは赤で色付けし、視認性のため幅は2に設定します。矢印はローソク足から正しく指すように、買いは上側アンカー、売りは下側アンカーに設定します。誤操作を防ぐため選択不可にし、最後にチャートを即時再描画します。MQL5にはwingdingsコードが用意されているため、必要に応じて好みのものに変更できます。

MQL5 WINGDINGS

続いてUpdateLabels関数を実装し、チャネル右端に追従する説明用テキストラベルを常に正確な位置へ表示します。チャネルの延長や再生成に合わせて滑らかに移動させます。まずlabelTimeからfixedTimeOldを引いてperiod_secで割ることで対応するx座標を求め、そのx値とグローバル回帰パラメータを使って、その時点の中央、上部、下部の価格を正確に計算します。

3つのラベル上部中央下部それぞれについて、まずObjectFindで既存かどうかを確認します。存在しない場合はlabelTimeと計算済み価格にOBJ_TEXTを新規作成し、存在する場合はObjectMoveで新しい位置へ移動します。テキストはUpper Channel、Middle Channel、Lower Channelを設定し、色はそれぞれ赤青緑に対応させます。アンカーは左上、左中央、左下に設定し、ラインと重ならず横に整列するようにします。ラベルは選択不可に設定し、最後にチャートを再描画します。これでチャネルの識別と描画にこれらの関数を使用できるようになります。さらにコードをモジュール化するため、判定ロジックも関数として実装していきます。

//+------------------------------------------------------------------+
//| Create channel if clear trend                                    |
//+------------------------------------------------------------------+
void CreateChannelIfTrend() {
   if (Bars(_Symbol, _Period) < RegressionPeriod + 1) return;     //--- Return if insufficient bars
   double closeArray[];                                           //--- Close array
   ArraySetAsSeries(closeArray, true);                            //--- Set as series
   if (CopyClose(_Symbol, _Period, 1, RegressionPeriod, closeArray) != RegressionPeriod) return; //--- Copy close or return
   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;               //--- Init sums
   int n = RegressionPeriod;                                      //--- Set n
   for (int i = 0; i < n; i++) {                                  //--- Iterate
      double x = (double)(n - 1 - i);                             //--- Calc x
      double y = closeArray[i];                                   //--- Get y
      sumX += x;                                                  //--- Add x
      sumY += y;                                                  //--- Add y
      sumXY += x * y;                                             //--- Add xy
      sumX2 += x * x;                                             //--- Add x2
   }
   double slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX); //--- Calc slope
   // Check for clear trend
   if (MathAbs(slope) < MinSlopeThreshold) {                      //--- Check no trend
      hasValidChannel = false;                                    //--- Reset channel
      ChannelDelete(0, channelName);                              //--- Delete channel
      ObjectDelete(0, upperLabelName);                            //--- Delete upper label
      ObjectDelete(0, middleLabelName);                           //--- Delete middle label
      ObjectDelete(0, lowerLabelName);                            //--- Delete lower label
      return;                                                     //--- Return
   }
   double intercept = (sumY - slope * sumX) / n;                  //--- Calc intercept
   // Calculate stdDev
   double sumRes2 = 0;                                            //--- Init res2 sum
   for (int i = 0; i < n; i++) {                                  //--- Iterate
      double x = (double)(n - 1 - i);                             //--- Calc x
      double predicted = intercept + slope * x;                   //--- Calc predicted
      double res = closeArray[i] - predicted;                     //--- Calc res
      sumRes2 += res * res;                                       //--- Add res2
   }
   double variance = sumRes2 / (n - 2);                           //--- Calc variance
   double stdDev = MathSqrt(variance);                            //--- Calc stdDev
   // Store for projection
   slope_global = slope;                                          //--- Set global slope
   intercept_global = intercept;                                  //--- Set global intercept
   stdDev_global = stdDev;                                        //--- Set global stdDev
   // Fixed anchors with initial extension (future time2)
   fixedTimeOld = iTime(_Symbol, _Period, RegressionPeriod);      //--- Set old time
   datetime fixedTimeNew = iTime(_Symbol, _Period, 1);            //--- Set new time
   long channel_sec = fixedTimeNew - fixedTimeOld;                //--- Calc channel sec
   long extension_sec = (long)(channel_sec * (ExtensionPercent / 100.0)); //--- Calc extension
   datetime time_extended = fixedTimeNew + (datetime)extension_sec; //--- Calc extended time
   current_right_x = (double)(time_extended - fixedTimeOld) / period_sec; //--- Calc right x
   // Delete old and create new
   ChannelDelete(0, channelName);                                 //--- Delete channel
   if (!ChannelCreate(0, channelName, 0, fixedTimeOld, time_extended)) return; //--- Create channel or return
   hasValidChannel = true;                                        //--- Set valid channel
   double channelWidth = 2 * Deviations * stdDev_global;          //--- Calc width
   double channelWidthPoints = channelWidth / _Point;             //--- Calc width points
   Print("Channel created: slope=" + DoubleToString(slope, 8) + ", range=" + DoubleToString(channelWidth, _Digits) + " (" + DoubleToString(channelWidthPoints, 0) + " points), times: " + TimeToString(fixedTimeOld) + " to " + TimeToString(time_extended)); //--- Log channel
   UpdateLabels(time_extended);                                   //--- Update labels
   ChartRedraw(0);                                                //--- Redraw chart
}

ここではCreateChannelIfTrend関数を実装し、線形回帰の完全な計算をおこなうと同時にチャネルを構築または更新するかを判断します。これにより明確なトレンド時のみチャネルを表示します。まず十分なヒストリカルバーが存在するかを確認します。少なくとも「RegressionPeriod + 1」が必要で、満たさない場合はエラー回避のため即座にreturnします。次に終値用の動的配列を宣言し、ArraySetAsSeriesでシリーズとして設定してインデックス0が最新バーになるようにします。その後CopyCloseでシフト1からRegressionPeriod本分の終値を取得し、失敗した場合は早期returnします。

最小二乗法のための合計変数を初期化し、期間内でループ処理をおこないます。各バーiについてxはn-1-iとし、最も古いバーが「x = n-1」、最新が「x = 0」となるようにします。yは終値とし、sumX、sumY、sumXY、sumX2を順に加算します。これはトレンド計算に必須です。その後標準的な式で傾きを計算します。傾きの絶対値がMinSlopeThreshold未満の場合はレンジと判断し、hasValidChannelをfalseに設定してChannelDeleteとObjectDeleteで既存のチャネルとラベルを削除してreturnします。これによりレンジ相場で意味のない水平チャネルが描画されるのを防ぎます。ただし、レンジ相場で取引したい場合はこのチェックは省略可能です。

傾きが十分な場合はinterceptを「sumY - slope × sumX」をnで割って求めます。続いて標準偏差を計算します。2回目のループで各xに対する予測価格を求め、実際の終値との差の二乗を計算して合計し、n-2で割って分散を求め、その平方根を取って標準偏差とします。傾き、切片、偏差は後のプロジェクション用にグローバル変数へ保存します。チャネルの左端は最も古いバーの時間をiTimeのシフトRegressionPeriodで取得してfixedTimeOldに設定します。右側の基準は直近確定バーシフト1とし、チャネルの時間長を秒で計算します。その後ExtensionPercent分だけ右へ延長し、その延長先の正確なx座標をcurrent_right_xとして計算します。

既存チャネルはChannelDeleteで削除し、新しいマルチパートチャネルをfixedTimeOldから延長時間までChannelCreateで構築します。成功した場合はhasValidChannelをtrueに設定し、傾き、価格幅とポイント幅、時間範囲などの情報をログ出力します。続いてUpdateLabelsで右端へラベルを更新し、チャートを再描画します。これで十分なデータがある場合、OnInitイベントハンドラからこの関数を呼び出して最初のチャネルを描画できます。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit() {
   obj_Trade.SetExpertMagicNumber(MagicNumber);                   //--- Set magic number
   CreateChannelIfTrend();                                        //--- Initial try
   return(INIT_SUCCEEDED);                                        //--- Return success
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
   ChannelDelete(0, channelName);                                 //--- Delete channel
   ObjectDelete(0, upperLabelName);                               //--- Delete upper label
   ObjectDelete(0, middleLabelName);                              //--- Delete middle label
   ObjectDelete(0, lowerLabelName);                               //--- Delete lower label
}

OnInitイベントハンドラでは、まずobj_TradeオブジェクトにSetExpertMagicNumberでMagicNumberを設定します。これにより、すべての発注にこの識別子が付与され、適切な管理が可能になります。次にCreateChannelIfTrendを呼び出し、起動時点で利用可能な最新データを使って回帰チャネルの構築を試みます。これにより、市場に十分なトレンドがある場合は初期チャネルが生成されます。最後にINIT_SUCCEEDEDIを返して初期化完了を示します。

OnDeinit関数ではクリーンアップをおこないます。ChannelDeleteを呼び出して回帰チャネルの5つの構成要素(塗りつぶし2つとトレンドライン3本)を削除し、その後upperLabelName、middleLabelName、lowerLabelNameを使ってObjectDeleteで3つの移動ラベルを個別に削除します。これによりプログラム停止後に不要なオブジェクトがチャートに残ることを防ぎます。コンパイルすると、次の結果が得られます。

初期線形回帰チャネル

画像から分かるように、トレンドが存在し、かつ計算に十分なバーがある場合にチャネルが初期化されます。ここからはティック関数内でバーの増加に応じて管理および更新をおこないます。ただし不要な負荷を避けるため、この処理は新しいバーのときのみ実行する必要があります。そのために以下の関数を定義します。

//+------------------------------------------------------------------+
//| Check if new bar has opened                                      |
//+------------------------------------------------------------------+
bool IsNewBar() {
   datetime currentBarTime = iTime(_Symbol, _Period, 0);          //--- Get current time
   if (currentBarTime != lastBarTime) {                           //--- Check new
      lastBarTime = currentBarTime;                               //--- Update last
      return true;                                                //--- Return true
   }
   return false;                                                  //--- Return false
}

IsNewBar関数では、現在の時間足で新しいバーが完全に形成されたかどうかを判定します。iTimeを使ってシフト0のバー開始時刻を取得しcurrentBarTimeに格納します。この値をグローバル変数lastBarTimeと比較し、異なっていれば新しいバーと判断します。その場合lastBarTimeを更新してtrueを返し、処理継続のシグナルとします。一致している場合はfalseを返し、そのティックでは重い処理をスキップします。一致している場合はfalseを返し、そのティックでは重い処理をスキップします。これで、新しいバーが生成されたときのOnTickイベントハンドラでこれを使用できるようになりました。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick() {
   // Check for new bar
   if (!IsNewBar()) return;                                       //--- Return if not new bar
   // Scan every bar if no channel
   if (!hasValidChannel) {                                        //--- Check no channel
      CreateChannelIfTrend();                                     //--- Create if trend
      if (!hasValidChannel) return;                               //--- Return if no channel
   }
   // Project values manually for completed bar (shift 1)
   datetime previousTime = iTime(_Symbol, _Period, 1);            //--- Get previous time
   int x = Bars(_Symbol, _Period, fixedTimeOld, previousTime) - 1; //--- Calc x
   channelMiddle = intercept_global + slope_global * x;           //--- Calc middle
   channelUpper = channelMiddle + Deviations * stdDev_global;     //--- Calc upper
   channelLower = channelMiddle - Deviations * stdDev_global;     //--- Calc lower
   // Project for shift 2
   datetime time2 = iTime(_Symbol, _Period, 2);                   //--- Get time 2
   if (time2 <= fixedTimeOld) return;                             //--- Return if invalid
   int x2 = Bars(_Symbol, _Period, fixedTimeOld, time2) - 1;      //--- Calc x2
   double middle2 = intercept_global + slope_global * x2;         //--- Calc middle2
   double upper2 = middle2 + Deviations * stdDev_global;          //--- Calc upper2
   double lower2 = middle2 - Deviations * stdDev_global;          //--- Calc lower2
   // Get closes
   double closePrevious = iClose(_Symbol, _Period, 1);            //--- Get close previous
   double close2 = iClose(_Symbol, _Period, 2);                   //--- Get close 2
   if (closePrevious == 0 || close2 == 0) return;                 //--- Return if invalid
   // Check if beyond end
   datetime current_time2 = (datetime)ObjectGetInteger(0, channelName + "_middle", OBJPROP_TIME, 1); //--- Get current time2
   if (previousTime > current_time2) {                            //--- Check beyond end
      Print("Bars beyond channel end: previousTime=" + TimeToString(previousTime) + ", current_time2=" + TimeToString(current_time2) + " - recreating channel"); //--- Log recreate
      CreateChannelIfTrend();                                     //--- Recreate channel
      return;                                                     //--- Return
   }
}

OnTick関数では、まずIsNewBarを呼び出して新しいバーが形成されたかを確認します。新しいバーでなければ即座にreturnし、同一ローソク足内でロジックが複数回実行されるのを防ぎます。これにより処理は常に確定バーに同期されます。hasValidChannelがfalseの場合(現在有効な回帰チャネルが存在しない場合)はCreateChannelIfTrendを呼び出して最新データを再評価し、傾きが最小閾値を満たしていれば新しいチャネルを構築します。それでもhasValidChannelがfalseのままであればレンジ相場と判断し、そのバーでは処理を終了します。

有効なチャネルが存在する場合、直近確定バー(シフト1)に対して回帰値を手動でプロジェクションします。iTimeでpreviousTimeを取得し、fixedTimeOldからpreviousTimeまでのBars数から1を引いてx座標を算出し、そのxを使って中央と上下のチャネル値を計算します。これにより前バーの終値時点における正確なチャネル位置が得られます。同様にシフト2のバーについてもプロジェクションをおこないます。これらの値を使うことで、前バーがその一つ前のバーの時点でチャネル内からブレイクアウトしたかどうかを判定できます。次に実際の終値を取得します。closePreviousはシフト1、close2はシフト2から取得し、どちらかが0であれば無効として早期returnします。

最後に安全チェックをおこないます。ObjectGetInteger を使って「channelName + _middle」の右端の時間OBJPROP_TIMEのインデックス1を取得しcurrent_time2に格納します。previousTimeがこの値を超えている場合、価格がチャネルの描画終端をすでに超えていることを意味します。その場合はログを出力し、CreateChannelIfTrendを呼び出して即座にチャネルを再構築し、その後returnします。コンパイルすると、次のようになります。

線形回帰チャネルティック更新

ここまでは順調です。トレンドが確認されたときに新しいバーでチャネルが更新されることが分かります。次はブレイクアウトを検出し、チャネルを延長するか再描画するかのロジックに進みます。そのためのコードの断片を以下に実装します。

// Check if breakout (deviation > threshold * width) for recreation
double channelWidth = channelUpper - channelLower;             //--- Calc width
double channelWidthPoints = channelWidth / _Point;             //--- Calc width points
double updateThreshold = UpdateThresholdPercent / 100.0;       //--- Calc threshold
double deviation = MathMax(closePrevious - channelUpper, channelLower - closePrevious); //--- Calc deviation
double deviationPercent = (deviation / channelWidth) * 100;    //--- Calc percent
if (deviation > updateThreshold * channelWidth) {              //--- Check breakout
   Print("Breakout detected - deviation: " + DoubleToString(deviation, _Digits) + " (" + DoubleToString(deviationPercent, 2) + "%), threshold: " + DoubleToString(updateThreshold * channelWidth, _Digits) + " (" + IntegerToString(UpdateThresholdPercent) + "%) - recreating channel"); //--- Log breakout
   CreateChannelIfTrend();                                     //--- Recreate channel
   return;                                                     //--- Return
} else {                                                       //--- No breakout
   // Extend right by one bar if within
   datetime new_time2 = current_time2 + (datetime)period_sec;  //--- Calc new time2
   current_right_x += 1.0;                                     //--- Increment x
   double new_price_middle = intercept_global + slope_global * current_right_x; //--- Calc new middle
   double new_price_upper = new_price_middle + Deviations * stdDev_global; //--- Calc new upper
   double new_price_lower = new_price_middle - Deviations * stdDev_global; //--- Calc new lower
   // Move channels
   ObjectMove(0, channelName + "_um", 1, new_time2, new_price_upper); //--- Move um
   ObjectMove(0, channelName + "_ml", 1, new_time2, new_price_middle); //--- Move ml
   // Move trendlines
   ObjectMove(0, channelName + "_upper", 1, new_time2, new_price_upper); //--- Move upper
   ObjectMove(0, channelName + "_middle", 1, new_time2, new_price_middle); //--- Move middle
   ObjectMove(0, channelName + "_lower", 1, new_time2, new_price_lower); //--- Move lower
   UpdateLabels(new_time2);                                    //--- Update labels
   ChartRedraw(0);                                             //--- Redraw chart
}

ここではチャネルの適応的な挙動を処理します。つまり、価格の動きに応じてバーごとに延長するか、あるいは現在の回帰から大きく乖離した場合に完全に再構築するかを判断します。まずチャネル全体の幅をchannelUpperとchannelLowerの差として計算し、参照用にポイントへ変換します。次にUpdateThresholdPercentを100で割って更新判定の閾値を求めます。その後、直近の終値が最も近い境界からどれだけ乖離しているかをMathMaxで求め、この乖離をチャネル全体幅に対するパーセンテージとして表現します。この乖離率が閾値(たとえばデフォルトで幅の30パーセント)を超えた場合、有意なブレイクアウトまたは現在の回帰の限界とみなします。この場合、価格での乖離量とパーセンテージ、および閾値をログ出力し、CreateChannelIfTrendを即座に呼び出して最新データで回帰を再計算し、新しいトレンドにより適合するチャネルを再構築します。その後はチャネルが再生成されているため早期returnします。この再構築ロジックは任意に変更可能であり、ここでは一例として採用しています。

一方で価格がチャネル内に収まっている場合(乖離が閾値以下の場合)は、連続性を保つため既存チャネルを1バー分右へ延長します。「current_time2 + period_sec」で右端時間を1バー進めてnew_time2とし、current_right_xを1.0増加させます。次に保存済みのグローバル値を使って新しい中央、上、下の価格をプロジェクションします。その後すべての構成要素について右側アンカーポイントインデックス1を新しい時間と価格へ移動します。対象は_umの上部中央フィル、_mlの中央下部フィル、および_upper、_middle、_lowerの3本のトレンドラインです。最後にUpdateLabelsを新しい右端時間で呼び出してテキストラベルを再配置し、チャートを再描画します。この延長処理により、トレンド中は不要な再計算をおこなわずにチャネルが滑らかに価格へ追従します。一方で価格が想定レンジを大きく外れた場合には、新しい回帰へ即座に切り替わります。ブレイクアウトが発生した場合は、ログ出力によってその状況を確認できます。

脱走検知ログ記録

チャネルを完全に管理できるようになったので、次は価格がチャネル自体からブレイクアウトしたときにシグナルを生成できます。加えて、中央ラインをクロスした場合には既存ポジションを決済する処理もおこないます。この中央ラインクロスによる決済は任意の管理ロジックであり、ポジションをストップロスやテイクプロフィットでのみ完全に決済したい場合は省略可能です。

// Count existing positions
int buyCount = 0, sellCount = 0;                               //--- Init counts
int total = PositionsTotal();                                  //--- Get total positions
for (int i = total - 1; i >= 0; i--) {                         //--- Iterate reverse
   ulong ticket = PositionGetTicket(i);                        //--- Get ticket
   if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber) { //--- Check symbol magic
      if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
         buyCount++;                                              //--- Increment buy
      } else if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell
         sellCount++;                                             //--- Increment sell
      }
   }
}
// Close logic: Close all buys if crossed above middle, all sells if below
if (closePrevious > channelMiddle) {                           //--- Check close above middle
   for (int i = PositionsTotal() - 1; i >= 0; i--) {           //--- Iterate reverse
      ulong ticket = PositionGetTicket(i);                     //--- Get ticket
      if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy
         obj_Trade.PositionClose(ticket, Slippage);                 //--- Close position
         Print("Closing Buy: Price " + DoubleToString(closePrevious, _Digits) + " crossed above middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Log close
      }
   }
}
if (closePrevious < channelMiddle) {                           //--- Check close below middle
   for (int i = PositionsTotal() - 1; i >= 0; i--) {           //--- Iterate reverse
      ulong ticket = PositionGetTicket(i);                     //--- Get ticket
      if (PositionGetString(POSITION_SYMBOL) == _Symbol && PositionGetInteger(POSITION_MAGIC) == MagicNumber && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell
         obj_Trade.PositionClose(ticket, Slippage);                 //--- Close position
         Print("Closing Sell: Price " + DoubleToString(closePrevious, _Digits) + " crossed below middle channel " + DoubleToString(channelMiddle, _Digits)); //--- Log close
      }
   }
}
// Open on clear breakout if room (with inverse option)
bool buySignal = (close2 >= lower2) && (closePrevious < channelLower); //--- Buy signal
bool sellSignal = (close2 <= upper2) && (closePrevious > channelUpper); //--- Sell signal
if (TradeDirection == Inverse) {                               //--- Check inverse
   bool temp = buySignal;                                      //--- Temp buy
   buySignal = sellSignal;                                     //--- Swap buy
   sellSignal = temp;                                          //--- Swap sell
}
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);            //--- Get ask
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);            //--- Get bid
if (buySignal && buyCount < MaxBuys) {                         //--- Check buy signal
   // Buy
   double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(ask - StopLossPips * _Point, _Digits); //--- Calc SL
   double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(ask + TakeProfitPips * _Point, _Digits); //--- Calc TP
   if (obj_Trade.Buy(Lots, _Symbol, 0, sl, tp, "LRC Buy")) {      //--- Open buy
      Print("Buy signal: Price " + DoubleToString(closePrevious, _Digits) + " broke below lower channel from inside"); //--- Log signal
      DrawArrow(true, previousTime, closePrevious);            //--- Draw arrow
   } else {                                                    //--- Failed
      Print("Buy order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Log failure
   }
}
if (sellSignal && sellCount < MaxSells) {                      //--- Check sell signal
   // Sell
   double sl = (StopLossPips == 0) ? 0 : NormalizeDouble(bid + StopLossPips * _Point, _Digits); //--- Calc SL
   double tp = (TakeProfitPips == 0) ? 0 : NormalizeDouble(bid - TakeProfitPips * _Point, _Digits); //--- Calc TP
   if (obj_Trade.Sell(Lots, _Symbol, 0, sl, tp, "LRC Sell")) {    //--- Open sell
      Print("Sell signal: Price " + DoubleToString(closePrevious, _Digits) + " broke above upper channel from inside"); //--- Log signal
      DrawArrow(false, previousTime, closePrevious);           //--- Draw arrow
   } else {                                                    //--- Failed
      Print("Sell order failed: " + obj_Trade.ResultRetcodeDescription()); //--- Log failure
   }
}

次に、新しいバーごとのポジション管理と取引実行に移ります。まず、このプログラムに属する買いおよび売りポジションが現在いくつ開かれているかをカウントします。PositionsTotalを逆順にループし、PositionGetTicketで各チケットを取得し、銘柄とMagicNumberが一致するかを確認します。POSITION_TYPE_BUYの場合はbuyCountを、POSITION_TYPE_SELLの場合はsellCountをインクリメントすることで、新規エントリーをおこなう前の正確な制限を把握できます。次に、ミドルラインクロスエグジットロジックを実装します。前のバーの終値("closePrevio次に、ミドルラインクロスによる決済ロジックを実装します。前バーの終値(closePrevious)がchannelMiddleより上の場合、すべての買いポジションを直ちに決済します。再度ループしてタイプを確認し、obj_Trade.PositionCloseにチケットと許容Slippageを指定して決済し、その理由をログに記録します。同様に、前バーの終値がchannelMiddleより下の場合は、すべての売りポジションを同じ手順で決済し、ログに残します。この方法により、価格が回帰線自体を越えた瞬間に利益を確保するか、損失を切ることが可能です。偏差がどれだけ離れているかは関係ありません。

次に、チャネル内からのクリーンなブレイクアウトに基づくエントリーシグナルを定義します。買いシグナルは、2本前のバーの終値(close2)が当時の下限バンド(lower2)以上であり、かつ前バーの終値が現在の下限バンド(channelLower)より下回った場合に発生します。これは価格がチャネルから明確に下方に抜けたことを意味します。売りシグナルはその逆条件で発生します。TradeDirectionがInverseの場合は、2つのシグナルを入れ替えるだけで、トレンド継続のプルバック戦略から平均回帰のフェードトレードに即座に変換できます。

次に、現在の売り気配値と買い気配値を取得し、買いシグナルが出ておりMaxBuysの制限内である場合は、ストップロスとテイクプロフィットを計算して、obj_Trade.Buyで市場注文を送信します。ロット数は固定、価格指定なし(市場成行)、計算したSL/TP、コメントは「LRC Buy」です。成功した場合はシグナルの詳細をログに記録し、DrawArrowで前バーの時間と終値に緑色の上向き矢印を描画します。失敗した場合は、リターンコードの説明をログに残します。売りシグナルも同様の手順で処理します。コンパイルすると、次の結果が得られます。

線形回帰チャネルテストGIF

この可視化から、線形回帰チャネルを定義し、必要に応じて更新し、ブレイクアウトでポジションを開くことが確認できます。これにより、目的を達成しています。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。


バックテスト

徹底的なバックテストによって、次の結果が得られました。

バックテストグラフ

グラフ

バックテストレポート

レポート


結論

MQL5で適応型の線形回帰チャネルシステムを開発しました。このシステムは、ユーザーが設定した期間に基づいて回帰線と標準偏差バンドを計算します。システムは、傾きの絶対値が最小閾値を超えた場合にのみ作動します。価格がチャネル内にある限り、チャネルはバーごとに自動的に拡張されます。また、価格がチャネル幅の設定可能な割合を超えて移動した場合は、チャネルを完全に再作成します。このシステムは、通常(プルバック)モードと逆(フェード)モードの両方をサポートしています。チャネル内からのクリーンなブレイクアウト時にポジションを開きます。視覚的には、塗りつぶされた二色ゾーン、上限、中間、下限のソリッドトレンドライン、移動ラベル、およびエントリー矢印を表示します。

免責条項:本記事は教育目的のみを意図したものです。取引には重大な財務リスクが伴い、市場の変動によって損失が生じる可能性があります。本プログラムを実際の市場で運用する前に、十分なバックテストと慎重なリスク管理が不可欠です。

この適応型線形回帰チャネル戦略は、動的な更新、通常および逆モード、中間線クロスでの決済を提供します。チャネルに基づくシグナルでトレンド相場を取引する準備が整っています。戦略はさらに最適化して活用することが可能です。取引をお楽しみください。

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/20347

添付されたファイル |
MQL5におけるARIMA予測指標 MQL5におけるARIMA予測指標
本記事では、MQL5でARIMA予測インジケーターを実装する方法について説明します。ARIMAモデルがどのように予測を生成するのか、またそれが外国為替市場や株式市場全般にどのように適用できるのかを解説します。さらに、自己回帰(AR)とは何か、自己回帰モデルがどのように予測に利用されるのか、その仕組みについても説明します。
他言語の実用モジュールをMQL5で実装する(第04回):Pythonのtime、date、datetimeモジュール 他言語の実用モジュールをMQL5で実装する(第04回):Pythonのtime、date、datetimeモジュール
MQL5とは異なり、Pythonは、時間の扱いや操作において高い柔軟性と制御性を提供します。本記事では、Pythonのように日付や時刻をより扱いやすくするためのモジュールを、MQL5で実装していきます。
プライスアクション分析ツールキットの開発(第53回):サポート・レジスタンスゾーン発見のためのPattern Density Heatmap プライスアクション分析ツールキットの開発(第53回):サポート・レジスタンスゾーン発見のためのPattern Density Heatmap
本記事では、パターン密度ヒートマップ(Pattern Density Heatmap)を紹介します。これは、繰り返し出現するローソク足パターンの検出結果を、統計的に有意なサポート・レジスタンスゾーンに変換するプライスアクションマッピングツールです。単一のシグナルを個別に扱うのではなく、EAは検出結果を固定価格レンジに集約し、密度をスコア化(必要に応じて直近の重み付けも可能)し、高い時間軸のデータと照合してレベルを確認します。その結果として得られるヒートマップは、市場が過去にどの価格レベルで反応したかを可視化し、売買のタイミング、リスク管理、戦略への信頼性向上に活用できます。あらゆる取引スタイルに対応可能です。
古典的な戦略を再構築する(第13回):クロスオーバー戦略を新たな次元へ(その2) 古典的な戦略を再構築する(第13回):クロスオーバー戦略を新たな次元へ(その2)
本記事では、移動平均クロスオーバー戦略に対してさらなる改善を加え、ラグをより実用的で信頼性の高い水準まで低減する方法について検討します。データサイエンスの知見を活用しながら議論を進めます。一般に、データを高次元へ射影することで、機械学習モデルの性能が向上する場合があることはよく知られています。本記事では、この考え方がトレーダーにとって実際に何を意味するのかを示し、MetaTrader 5ターミナルを用いてどのように活用できるかを説明します。