English Deutsch
preview
MQL5でかぎ足をマスターする(第2回):かぎ足ベース自動売買の実装

MQL5でかぎ足をマスターする(第2回):かぎ足ベース自動売買の実装

MetaTrader 5トレーディング |
24 0
Chacha Ian Maroa
Chacha Ian Maroa

はじめに

本連載の第1回では、MQL5による完全なかぎ足エンジンを構築しました。価格データの取得方法、かぎ足構造の構築方法、および各ラインセグメントのチャート描画方法について学びました。第1回の終了時点で、新しいバーが形成されるたびに更新される完全なかぎ足のチャートが完成していました。

第2回では、チャート構築から実際の取引へと進みます。目標は、かぎ足チャートを市場構造の変化に反応するエキスパートアドバイザー(EA)へと変換することです。本記事では、反転シグナルの検出、取引の実行、リスク管理、ポジション管理といった新機能を導入します。また、トレーダーがシグナル発生のタイミングを明確に把握できるよう、視覚的なマーカーも追加します。

本パートは、かぎ足エンジンを基盤として構築されます。各機能は各機能はシンプルで分かりやすい形で追加されるため、読者はロジックを容易に追うことができます。本記事を読み終える頃には、MetaTrader 5上のあらゆる銘柄で利用可能な、かぎ足ベースの完全な自動売買システムが完成します。


取引モジュールの新機能

今回は、かぎ足を拡張し、完全な自動売買システムへと発展させます。コーディングを開始する前に、MetaEditor 5を開き、前回作成したソースファイルを読み込む必要があります。ファイル名はKagiTraderPart1.mq5で、本記事に添付されています。今回追加するすべてのロジックは、この基盤の上に実装されます。

本セクションでは、新機能を高レベルで順に説明し、後続の実装に必要となる準備コードも追加します。目的は、取引エンジンに何が追加され、なぜそれが重要なのかを読者が理解できるようにすることです。

売買シグナルを示す視覚的なマーカー

最初の機能は視覚的なマーカーの導入です。これらのマーカーは、買いまたは売りシグナルが発生した正確なタイミングを可視化します。トレーダーがEAのかぎ足の反転に対する反応を直感的に理解できるようにするためのものです。EAはローソク足の上または下に小さな矢印を表示し、ロングおよびショートシグナルを明確に示します。

取引を有効または無効にする機能

EAをインジケーターとしてのみ使用したいユーザーもいます。一方で、自動売買をおこないたいユーザーもいます。このため、取引が有効か無効かを制御する入力パラメータを導入します。このパラメータは既存の入力設定の下に配置されます。

input group "Trading"
input bool                     enableTrading  = true;

enableTradingtrueの場合、EAは新規ポジションを開きます。falseの場合、取引はおこなわず、かぎ足が更新されるだけです。

取引方向の制御

トレーダーにはそれぞれ異なるスタイルがあります。ロングのみを好む場合やショートのみを好む場合もありますが、多数は両方向の取引を好みます。この柔軟性を実現するために、列挙型を導入し、既存の列挙項目の下に配置します。

enum ENUM_TRADE_DIRECTION  
{ 
   ONLY_LONG, 
   ONLY_SHORT, 
   TRADE_BOTH 
};

定義した後は、入力パラメータを追加します。

input ENUM_TRADE_DIRECTION         direction  = TRADE_BOTH;

これにより、シンプルなスイッチが作成されます。ユーザーは3つのモードから1つを選択できます。EAは、新しい取引を開始する前にこの値を確認します。ONLY_LONGが選択された場合、EAはショートシグナルを無視します。ONLY_SHORTが選択された場合、EAはロングシグナルを無視します。TRADE_BOTHが選択された場合、EAは両方向で取引をおこないます。

陰から陽への反転時にロングポジションを建てる

買いシグナル

カギ足の大きな強みは、市場構造の変化に基づいた明確な反転を示すことができる点です。陰線から陽線への変化は、買い手が主導権を握ったことを示します。このためEAは、かぎ足の構造が陰から陽へ変化した際に買いポジションを開きます。これは上昇の可能性がある動きの初期を捉えるものです。後のセクションで、この反転をリアルタイムで検出するロジックを実装します。

陽から陰への反転時にショートポジションを建てる

売りシグナル

からへの反転は、売り手が主導権を握ったことを示します。これはしばしば下降トレンドの開始を意味します。EAは、かぎ足が陽から陰へ切り替わった際にショートポジションを開きます。これは買いロジックと対になるものであり、取引戦略のコアを形成します。実装パートでは、この転換を確実に検出する方法を示します。

手動および自動ロットサイズの選択

トレーダーはリスク管理の方法が異なります。固定ロットを好む場合もあれば、口座残高に基づいてロットサイズを計算したい場合もあります。この2つのスタイルに対応するために、簡単な列挙型を導入します。

enum ENUM_LOT_SIZE_INPUT_MODE 
{ 
   MODE_MANUAL, 
   MODE_AUTO 
};

この設定により、ユーザーはロットサイズの算出方法を選択できます。定義した後、以下の入力パラメータを追加します。

input ENUM_LOT_SIZE_INPUT_MODE   lotSizeMode  = MODE_AUTO;

MODE_AUTOが選択されている場合、ロットサイズはリスク割合に基づいて計算されます。そのため、リスク割合を定義する入力を追加します。

input double             riskPerTradePercent  = 1.0;

たとえば口座残高が10,000ドルでriskPerTradePercentが1.0の場合、1取引あたりの損失は100ドルになるようにポジションサイズが計算されます。これにより、リスクは予測可能かつ安定したものとなります。

MODE_MANUALが選択された場合は、このリスク割合は無視され、固定ロットサイズが使用されます。そのために、手動ロットサイズパラメータを追加しました。

input double                         lotSize  = 0.1;

これにより、ユーザーは各取引の取引量を完全に制御できます。両方の方法は一般的に使用されており、EAは両方をサポートします。

直近の局所極値に基づくストップロスの設定

カギ足は価格のスイング構造を明確に示します。各反転は局所的な高値または安値を形成します。これらの点は、ブレイクされた場合にトレンド転換を示唆する重要な構造です。このため、ロングポジションのストップロスは直近の局所安値に設定し、ショートポジションのストップロスは直近の局所高値に設定します。この方法によりリスクが制御され、かぎ足の構造と整合した取引が実現されます。

リスクリワード比によるテイクプロフィット

次の機能はリスクリワードシステムです。トレーダーはリスクとリワードの比率を固定することが多いため、そのための列挙型を導入します。

enum ENUM_RISK_REWARD_RATIO   
{ 
   ONE_TO_ONE, 
   ONE_TO_ONEandHALF, 
   ONE_TO_TWO, 
   ONE_TO_THREE, 
   ONE_TO_FOUR, 
   ONE_TO_FIVE, 
   ONE_TO_SIX 
};

それぞれの値はストップロスに対するテイクプロフィットの倍率を定義します。たとえばONE_TO_THREEは、リワードがリスクの3倍であることを意味します。これはトレンドフォロー戦略で一般的なリスク管理手法です。列挙型を定義した後、入力パラメータを追加します。

input ENUM_RISK_REWARD_RATIO riskRewardRatio  = ONE_TO_THREE;

これにより、ユーザーは自身の取引スタイルに合った比率を選択できるようになります。

オプションのトレーリングストップ

最後の機能はトレーリングストップです。有効化された場合、価格がトレード方向に進むにつれてストップロスが追従します。これにより利益を保護できます。今のところは、主要な制御パラメータのみを追加します。

input bool                enableTrailingStop  = false;

今後、追加のパラメータが設定される可能性はありますが、現時点で開発を継続するには十分です。


陰から陽および陽から陰への転換の検出

取引を実行する前に、まずかぎ足が実際にいつ方向転換するのかを判定する必要があります。チャートでは、この方向転換は太さまたは色の変化として表現されます。

  • 陰から陽へ(買いシグナル)
  • 陽から陰へ(売りシグナル)

これらの反転はConstructKagiInRealTime関数の内部で発生しており、すでに3つの条件セットとして整理されています。それぞれの条件はkagiData.isYinおよびkagiData.isYangを更新するため、売買ロジックを追加するのに最適な箇所となっています。

1. 複雑な反転

これは典型的なかぎ足の反転です。価格が反対方向に十分に進んだ場合、トレンドは完全に転換し、ラインの太さが切り替わります。

陽から陰へ

該当箇所は以下です。

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...

      //--- Handle a complex reversal
      if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum)){
         if(overlayKagi){
            DrawBendTop (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor);
            DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMinimum, currentOpenTime, currentClosePrice, yinLineColor);
         }
         kagiData.localMaximum   = kagiData.referencePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.referenceTime  = currentOpenTime;
         kagiData.localMinimum   = currentClosePrice;
         kagiData.isDowntrend    = true;
         kagiData.isUptrend      = false;
         kagiData.isYang         = false;
         kagiData.isYin          = true;
      }
}

陰から陽へ

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...

      //--- Handle a complex reversal
      if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum)){
         if(overlayKagi){
            DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor);
            DrawYinLine   (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine  (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMaximum, currentOpenTime, currentClosePrice, yangLineColor);
         }
         kagiData.localMinimum   = kagiData.referencePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.referenceTime  = currentOpenTime;
         kagiData.localMaximum   = currentClosePrice;
         kagiData.isDowntrend    = false;
         kagiData.isUptrend      = true;
         kagiData.isYang         = true;
         kagiData.isYin          = false;
      }  
}

これらは最も強力なかぎ足シグナルタイプです。

2.反転後の複雑な継続

これは、反転がすでに発生した後に、価格が以前の局所的極値を超えて拡張する場合に発生します。このときかぎ足の太さは再び切り替わり、トレンドは新たな強さを伴って継続します。

陽から陰へ

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...
           
      //--- Handle a complex continuation after reversal
      if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum))){
         if(overlayKagi){
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor);
         }
         kagiData.localMinimum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.isYang         = false;
         kagiData.isYin          = true;
      }
      
}

陰から陽へ

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...
           
      //--- Handle a complex continuation after reversal
      if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum))){
         if(overlayKagi){
            DrawYinLine  (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor);
         }
         kagiData.localMaximum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.isYang         = true;
         kagiData.isYin          = false;
      }  
}

これらの変化は依然として、市場センチメントの重要な変化を表しています。

3. 例外的(奇妙な)シナリオ

このカテゴリは、通常とは異なる価格挙動であっても、有効な極性変化が発生するケースを扱います。頻度は低いものの、すべての有効な転換を取りこぼさないために重要です。

陰から陽へ

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...
           
      //--- Handle a weird scenario
      if(kagiData.isUptrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){
         if(overlayKagi){
            DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor);
         }
         kagiData.isYin  = false;
         kagiData.isYang = true;
         kagiData.localMaximum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
      }   
}

陽から陰へ

void ConstructKagiInRealTime(double bidPr, double askPr){
           
           ...
           
      //--- Handle a weird scenario
      if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){
         if(overlayKagi){
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor);
         }
         kagiData.isYang = false;
         kagiData.isYin  = true;
         kagiData.localMinimum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
      } 
}

各条件において、かぎ足は明確に一方から他方へと変化します。今回の戦略においては、これらの変化を検出できれば十分です。 次のセクションでは、これらの転換ポイントに直接売買ロジックを組み込みます。また、ユーザーの取引モード(ロングのみ、ショートのみ、または両方)を考慮し、選択されたロットサイズ方式を適用し、ストップロスおよびテイクプロフィットを設定する準備をおこないます。


かぎ足転換への売買ロジックの統合

カギ足の構造がすでに完成したので、次はチャートの転換(陰から陽、陽から陰)を実際の取引操作へ接続していきます。このセクションでは、取引環境の準備、小さな取引情報を保存するデータ構造の導入、そして買いや売りポジションを開く関数の解説をおこないます。各ステップは、そのまま実装できる形で説明します。

最初のステップとして、EAが注文送信できるようにします。そのためにTrade標準ライブラリをインクルードします。以下の行を#property宣言の直下に追加します。

//+------------------------------------------------------------------+
//| Standard Libraries                                               |
//+------------------------------------------------------------------+
#include <Trade\Trade.mqh>

これにより、注文送信処理をすべて扱うことができるCTradeクラスを利用できるようになります。

次に、直近の取引情報を保持するためのシンプルな構造体を定義します。この構造体はチケット番号、エントリー価格、ストップロス、テイクプロフィットなどの情報を保持します。既存の構造体定義の直下に以下を配置します。

struct MqlTradeInfo
{
   ulong orderTicket;                 
   ENUM_ORDER_TYPE type;
   ENUM_POSITION_TYPE posType;
   double entryPrice;
   double takeProfitLevel;
   double stopLossLevel;
   datetime openTime;
   double lotSize;   
};

この構造体は新しいポジションが開かれるたびに更新され、EAが直近の取引内容を正確に把握できるようになります。

構造体の直後に、次の2つのグローバルインスタンスを追加します。

//--- Instantiate the trade information data structure
MqlTradeInfo tradeInfo;

//--- Create a CTrade object to handle trading operations
CTrade Trade;

Tradeオブジェクトは注文を送信し、tradeInfoは直近の取引結果を保存するためのコンテナとして機能します。

取引処理には正確なBid/Ask価格が必要となるため、以下のグローバル変数を定義します。

//--- Bid and Ask
double   askPrice;
double   bidPrice;

これらの価格は常に最新状態に保つため、MQL5のOnTick関数内で更新します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   //--- Scope variables
   askPrice      = SymbolInfoDouble (_Symbol, SYMBOL_ASK);
   bidPrice      = SymbolInfoDouble (_Symbol, SYMBOL_BID);
}

これらの価格は後で買い関数と売り関数に渡されます。

コード重複を避けるため、売買ロジックはOpenBuyOpenSelの2つのヘルパー関数に分離します。どちらの関数もほぼ同じ処理をおこないますが、方向が逆になっています。同様の説明の繰り返しを避けるため、ここでは片方の関数のロジックを分解して説明します。もう一方にも同じロジックが逆方向で適用されます。以下がOpenBuy関数です。ユーティリティ関数を配置している場所に追加してください。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ENUM_ORDER_TYPE action          = ORDER_TYPE_BUY;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY;
   datetime currentTime            = TimeCurrent();
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }

   double stopLevel    = NormalizeDouble(kagiData.localMinimum, Digits());
   double stopDistance = NormalizeDouble(askPr - stopLevel, Digits());
   double targetLevel  = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits());
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      Print("Error while opening a long position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   return false;
}

この関数が何をおこなっているのかを理解するために、主なステップを以下に分解して説明します。

まず、この関数では買いポジションを開くことを定義し、現在時刻を取得します。 

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ENUM_ORDER_TYPE action          = ORDER_TYPE_BUY;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_BUY;
   datetime currentTime            = TimeCurrent();
   
   ...

}

次に、契約規模と口座残高を取得します。これらの値は、lotSizeModeパラメータがMODE_AUTOの場合にロットサイズ計算に使用されます。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   
   ...

}

次のステップは、リスクリワード倍率を決定することです。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
   
   ...

}

switch文は、ユーザーが選択したriskToRewardRatioを数値に変換します。

次に、ストップロスとテイクプロフィットの水準を計算します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   double stopLevel    = NormalizeDouble(kagiData.localMinimum, Digits());
   double stopDistance = NormalizeDouble(askPr - stopLevel, Digits());
   double targetLevel  = NormalizeDouble(askPr + (rewardValue * stopDistance), Digits());
   
   ...

}

ストップロスの水準は、カギ足における直近の局所的安値に設定されます。次に、エントリーポイントからストップロスレベルまでの距離を測定し、その値を変数stopDistanceに代入します。テイクプロフィット水準は、リスクリワード倍率を用いて決定します。

次に、取引量を計算します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   ...

}

lotSizeModeユーザー入力パラメータでMODE_MANUALが選択された場合、lotSize入力パラメータに指定された値を使用します。AUTO_MODEが選択された場合、指定されたリスク率に基づいてロットサイズを計算します。

次のステップは、市場価格で即座に買いポジションを開く注文を出すことに関係しています。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      Print("Error while opening a long position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }
   
   ...

}

Trade.Buy()コマンドは注文を送信します。失敗した場合、関数はデバッグのためにエラーを出力します。取引が成功した場合、チケット番号、建値、ストップロス、テイクプロフィット、ロットサイズをtradeInfo構造体内に保存します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      
      ...
      
   }else{
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      
      return true;
   }
   
   ...

}

OpenSel関数は全く同じ流れをたどりますが、方向が逆になります。カギ足構造の局所最大値を利用し、ストップレベルとターゲットレベルを逆算して計算します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ENUM_ORDER_TYPE action          = ORDER_TYPE_SELL;
   ENUM_POSITION_TYPE positionType = POSITION_TYPE_SELL;
   datetime currentTime            = TimeCurrent();   
   double contractSize             = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);
   double accountBalance           = AccountInfoDouble(ACCOUNT_BALANCE);
   double rewardValue              = 1.0;
   
   switch(riskRewardRatio){
      case ONE_TO_ONE: 
         rewardValue = 1.0;
         break;
      case ONE_TO_ONEandHALF:
         rewardValue = 1.5;
         break;
      case ONE_TO_TWO: 
         rewardValue = 2.0;
         break;
      case ONE_TO_THREE: 
         rewardValue = 3.0;
         break;
      case ONE_TO_FOUR: 
         rewardValue = 4.0;
         break;
      case ONE_TO_FIVE: 
         rewardValue = 5.0;
         break;
      case ONE_TO_SIX: 
         rewardValue = 6.0;
         break;
      default:
         rewardValue = 1.0;
         break;
   }
      
   double stopLevel    = NormalizeDouble(kagiData.localMaximum, Digits());
   double stopDistance = NormalizeDouble(stopLevel - bidPr, Digits());
   double targetLevel  = NormalizeDouble(bidPr - (rewardValue * stopDistance), Digits());
   double volume       = NormalizeDouble(lotSize, 2);
   if(lotSizeMode == MODE_AUTO){
      double amountAtRisk = (riskPerTradePercent / 100.0) *  accountBalance;
      volume              = amountAtRisk / (contractSize * stopDistance);
      volume              = NormalizeDouble(volume, 2);
   }
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      Print("Error while opening a short position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{ 
      MqlTradeResult result = {};
      Trade.Result(result);
      tradeInfo.orderTicket                 = result.order;
      tradeInfo.type                        = action;
      tradeInfo.posType                     = positionType;
      tradeInfo.entryPrice                  = result.price;
      tradeInfo.takeProfitLevel             = targetLevel;
      tradeInfo.stopLossLevel               = stopLevel;
      tradeInfo.openTime                    = currentTime;
      tradeInfo.lotSize                     = result.volume;
      return true;
   }
   
   return false;
}

取引ロジックをかぎ足の転換に統合する前に、まずいくつかのヘルパー関数を準備する必要があります。これらの関数は、EAがすでに買いまたは売りポジションを保有しているかどうかを確認するために使用されます。また、このEAに属するすべての保有ポジションを簡単にクローズするための仕組みも提供します。これらの関数は、「EAは常に同時に1つのポジションのみを保有する」という非常に重要なルールを実装するために役立ちます。すでにポジションが存在すれば、現在のポジションがクローズされるまで、新しいポジションは開かれません。

これらの関数は、既存のユーティリティ関数の直下に配置します。1つずつ追加し、それぞれが何をおこなっているのかを理解しながら進めてください。これらは後の処理で重要な役割を果たします。

アクティブなロングポジションを確認するために、以下の関数を定義します。

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active buy position.  |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveBuyPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
            return true;
         }
      }
   }
   
   return false;
}

この関数は、現在保有しているすべてのポジションをスキャンし、このEAに属するアクティブな買いポジションが存在するかどうかを確認します。各ポジションのマジックナンバーと、このEAに割り当てられたマジックナンバーを比較することで判定をおこないます。一致する買いポジションが見つかった場合はtrueを返します。該当するポジションが存在しない場合はfalseを返します。この関数は後に、EAが同時に2つの買いポジションを開かないようにするために使用されます。

アクティブなショートポジションを確認するために、以下の関数を定義します。

//+------------------------------------------------------------------+
//| To verify whether this EA currently has an active sell position. |                                 |
//+------------------------------------------------------------------+
bool IsThereAnActiveSellPosition(ulong magic){
   
   for(int i = PositionsTotal() - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket == 0){
         Print("Error while fetching position ticket ", _LastError);
         continue;
      }else{
         if(PositionGetInteger(POSITION_MAGIC) == magic && PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
            return true;
         }
      }
   }
   
   return false;
}

この関数は前の関数と同様の仕組みで動作しますが、売りポジションを対象として確認をおこないます。すべての保有ポジションをループし、マジックナンバーとポジションタイプを取得します。そして、このEAに属する売りポジションが見つかった場合のみtrueを返します。この関数を取引判断時に使用することで、EAはすでに売りポジションが実行中の場合に新たな売りポジションを開くことを防ぎます。

また、EAによって開かれたすべてのアクティブポジションをクローズする方法も必要です。そのため、以下のユーティリティ関数を定義します。

//+------------------------------------------------------------------+
//| To close all position with a specified magic number              |   
//+------------------------------------------------------------------+
void ClosePositionsByMagic(ulong magic) {
    
    for (int i = PositionsTotal() - 1; i >= 0; i--) {
        ulong ticket = PositionGetTicket(i);
        if (PositionSelectByTicket(ticket)) {
            if (PositionGetInteger(POSITION_MAGIC) == magic) {
                ulong positionType = PositionGetInteger(POSITION_TYPE);
                double volume = PositionGetDouble(POSITION_VOLUME);
                if (positionType == POSITION_TYPE_BUY) {
                    Trade.PositionClose(ticket);
                } else if (positionType == POSITION_TYPE_SELL) {
                    Trade.PositionClose(ticket);
                }
            }
        }
    } 
}

この関数は、このEAによって開かれたすべてのポジションをクローズするためのシンプルで信頼性の高い方法を提供します。すべてのアクティブな取引を走査し、それぞれをチケット番号によって選択します。そして、マジックナンバーがこのEAに設定されたものと一致するポジションを見つけた場合、それが買いか売りかに関係なくそのポジションをクローズします。この関数は、ポジション管理を完全に制御したい場合、特にEAのリセット時や、ロングのみ、ショートのみ、両方といった取引モードを実装する際に非常に有用です。

ユーザーが自動売買を無効にした場合でも、EAはかぎ足シグナルをチャート上に表示し続ける必要があります。この機能により、トレーダーはかぎ足の動きを視覚的に追跡でき、どこで潜在的なエントリーが発生したかを理解できます。この目的のために、2つのシンプルなユーティリティ関数を作成します。1つは買いマーカーを描画し、もう1つは売りマーカーを描画します。各マーカーは、シグナルが発生した正確な時間と価格レベルに配置されます。

買いシグナルマーカーを描画するために、以下の関数をポジション管理ユーティリティ関数の直後に追加します。

//+------------------------------------------------------------------+
//| Draw a Buy Signal Marker                                         |
//+------------------------------------------------------------------+
void DrawBuySignalMarker(const datetime time, const double price){

   //--- Create a unique name for the object
   string name = "BuySignal_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(MathRand());

   //--- Create the buy arrow
   if(!ObjectCreate(0, name, OBJ_ARROW_UP, 0, time, price)){
      Print("Failed to create Buy Signal marker. Error: ", GetLastError());
      return;
   }

   //--- Styling (optional)
   ObjectSetInteger(0, name, OBJPROP_COLOR, yangLineColor);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 3);
}

この関数は買いシグナルマーカーをチャート上に描画します。まずオブジェクトの一意な名前を生成することから始まります。これは重要で、MetaTrader 5ではすべてのグラフィカルオブジェクトがそれぞれ固有の名前を持つ必要があるためです。その後、指定された時間と価格に上向きの矢印を作成します。オブジェクトが正常に作成された場合、マーカーが視認できるように、またかぎ足のテーマと一貫性を保つように色と幅が適用されます。このマーカーは取引には影響を与えません。その目的はあくまで、買いシグナルがどこで発生したかをトレーダーに示すことです。

売りシグナルマーカーを描画するために、以下の関数を定義します。

//+------------------------------------------------------------------+
//| Draw a Sell Signal Marker                                        |
//+------------------------------------------------------------------+
void DrawSellSignalMarker(const datetime time, const double price){

   //--- Create a unique name for the object
   string name = "SellSignal_" + IntegerToString(TimeCurrent()) + "_" + IntegerToString(MathRand());

   //--- Create the sell arrow
   if(!ObjectCreate(0, name, OBJ_ARROW_DOWN, 0, time, price)){
      Print("Failed to create Sell Signal marker. Error: ", GetLastError());
      return;
   }

   //--- Styling (optional)
   ObjectSetInteger(0, name, OBJPROP_COLOR, yinLineColor);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, 3);
}

この関数は買いマーカー関数と同様に動作しますが、代わりに下向きの矢印を配置します。オブジェクト名も動的に生成されるため、各シグナルは個別に保存されます。矢印が正しい時間と価格に作成された後、この関数はその色と幅を設定し、陰線のスタイルと一致するようにします。これらの視覚的なタグにより、実際の取引が無効になっている場合でも、トレーダーは弱気のかぎ足シグナルを追跡することができます。

シグナル描画ユーティリティが整ったので、次のステップはかぎ足シグナルを実際の取引処理に接続することです。ここでの目的はシンプルです。EAが買いまたは売りシグナルとして有効な、確定したかぎ足の反転または継続を検出した場合に、その対応するトレードブロックを実行し、その後チャート上にマーカーを配置します。このアプローチにより、トレードのワークフロー全体がConstructKagiInRealTime関数内に集約され、シグナルがどのようにアクションへ変換されるかを読者が理解しやすくなります。

統合をよりスムーズにするためには、かぎ足エンジンが市場構造に応じて異なるポイントでシグナルを生成することを理解することが重要です。明確な反転によって発生するシグナルもあれば、反転後の継続的な動きによって発生するシグナルもあります。どのような経路でシグナルが発生したとしても、EAは常に同じ3つのステップを実行する必要があります。

  1. 反対方向のアクティブポジションをクローズする
  2. (取引が有効な場合)シグナル方向に新しいポジションを開く
  3. シグナルを表示するために、チャート上に視覚的なマーカーを配置する

これらのステップは複数回登場するため、買いシグナルのコードブロックはすべて同様の構造になります。同じ一貫性は売りシグナルにも適用されます。以下では、このロジックがどのように関数内に組み込まれるかを、実際のかぎ足ブロックの例を用いて説明します。

買いシグナルの処理

カギ足構造が上昇フェーズへの移行を検出した場合、完全な反転であっても継続的な動きであっても、買いロジックがトリガーされます。各買いブロックは同じパターンに従います。

  1. アクティブな売りポジションをクローズする:ロングポジションを開く前に、売り注文が存在していないことを確認します。存在する場合は直ちにクローズされます。
  2. 取引が許可されている場合は、新しいロングポジションを開く:自動売買が有効であり、ユーザーがロングエントリーを許可している場合、EAは既に買いポジションが存在するかどうかを確認します。存在しない場合、現在のAsk価格で新規に開きます。
  3. チャート上に買いシグナルマーカーを配置する:自動売買が有効か無効かに関わらず、EAは常に視覚的なかぎ足買いマーカーを配置し、トレーダーがチャート上でシグナルを直接追跡できるようにします。

関数内の買いシグナルブロックは次のようになります。

// Close a short position if it exists
if(IsThereAnActiveSellPosition(magicNumber)){
   ClosePositionsByMagic(magicNumber);
   Sleep(50);
}

//--- Open a long position if allowed
if(enableTrading){
   if(direction == TRADE_BOTH || direction == ONLY_LONG){
      if(!IsThereAnActiveBuyPosition(magicNumber)){
         OpenBuy(askPrice);
      }
   }
}

//--- Render a buy signal (up) arrow
datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
double   lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice);

かぎ足ロジックが有効な上方向のブレイクアウトまたは反転を生成するすべての状況で、この同じブロックが繰り返し使用されることになります。

売りシグナルの処理

売りシグナルは買いシグナルと対称的な構造を持ちますが、逆方向に動作します。価格構造が下降方向の反転または継続を確認した場合、以下のステップを実行します。

  1. アクティブな買いポジションをクローズする:EAはまず買いポジションが開いているかどうかを確認します。存在する場合は、ポジションの競合を避けるためにそれをクローズします。
  2. 条件が許可されている場合、新しいショートポジションを開く:自動売買が有効であり、ユーザーが売り取引を許可している場合、EAは現在のBid価格を使用して売り注文を開きます。ただし、すでにアクティブな売りポジションが存在しない場合に限ります。
  3. チャートに売りマーカーを追加する:マーカーは最後に確定したバー上に描画され、かぎ足の転換または継続が発生した位置を正確に示します。

以下は、売りシグナルブロックのサンプルです。


// Close a long position if it exists
if(IsThereAnActiveBuyPosition(magicNumber)){
   ClosePositionsByMagic(magicNumber);
   Sleep(50);
}

//--- Open a short position if allowed
if(enableTrading){
   if(direction == TRADE_BOTH || direction == ONLY_SHORT){
      if(!IsThereAnActiveSellPosition(magicNumber)){
         OpenSel(bidPrice);
      }
   }
}

//--- Render a sell signal (down) arrow
datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
double   lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice);

買いブロックと同様に、この売りテンプレートも、下降方向の構造的ブレイクが検出されるかぎ足エンジンのあらゆる箇所で使用されます。

ConstructKagiInRealTime関数の内部では、新しいバーが形成されるたびにKagiアルゴリズムが状態を更新します。その更新の中で、複数の条件が現在のKagiラインをブレイクするのに十分な価格変動が発生しているか、あるいはトレンドの反転が起きているかをチェックします。これらの各条件は、それぞれ買いまたは売りのシグナルとなる可能性を持ちます。

以下にその例を示します。

  • 上昇トレンドにおいて陽線を下抜けた場合、売りシグナルが発生します。
  • 下降トレンドにおいて陰線を上抜けた場合、買いシグナルが発生します。
  • 反転後に価格が強い勢いで継続した場合、継続シグナルが発生する可能性があり、それは通常の反転シグナルと同様に扱われます。

これらすべてのポイントにおいて、対応する売買ブロック(買いまたは売り)を単純に挿入します。

//+------------------------------------------------------------------+
//| This function is used to construct Kagi in real time             |
//+------------------------------------------------------------------+
void ConstructKagiInRealTime(double bidPr, double askPr){
   if(IsNewBar(_Symbol, kagiTimeframe, kagiData.lastBarOpenTime)){
      
      ...
      
      //--- Handle a complex reversal
      if(kagiData.isUptrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum)){
         
         // Close a long position if it exists
         if(IsThereAnActiveBuyPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a short position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_SHORT){
               if(!IsThereAnActiveSellPosition(magicNumber)){
                  OpenSel(bidPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawBendTop (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yangLineColor);
            DrawYangLine(GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMinimum, currentOpenTime, currentClosePrice, yinLineColor);
         }
         kagiData.localMaximum   = kagiData.referencePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.referenceTime  = currentOpenTime;
         kagiData.localMinimum   = currentClosePrice;
         kagiData.isDowntrend    = true;
         kagiData.isUptrend      = false;
         kagiData.isYang         = false;
         kagiData.isYin          = true;
      }
      
      if(kagiData.isDowntrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum)){
         
         // Close a short position if it exists
         if(IsThereAnActiveSellPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a long position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_LONG){
               if(!IsThereAnActiveBuyPosition(magicNumber)){
                  OpenBuy(askPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawBendBottom(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, currentOpenTime, kagiData.referencePrice, yinLineColor);
            DrawYinLine   (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.referencePrice, currentOpenTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine  (GenerateUniqueName(TRENDLINE), currentOpenTime, kagiData.localMaximum, currentOpenTime, currentClosePrice, yangLineColor);
         }
         kagiData.localMinimum   = kagiData.referencePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.referenceTime  = currentOpenTime;
         kagiData.localMaximum   = currentClosePrice;
         kagiData.isDowntrend    = false;
         kagiData.isUptrend      = true;
         kagiData.isYang         = true;
         kagiData.isYin          = false;
      }
           
      ...
            
      //--- Handle a complex continuation after reversal
      if(kagiData.isDowntrend && kagiData.isYang && (currentClosePrice <= (kagiData.referencePrice - reversalAmount) && (currentClosePrice < kagiData.localMinimum))){
         
         // Close a long position if it exists
         if(IsThereAnActiveBuyPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a short position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_SHORT){
               if(!IsThereAnActiveSellPosition(magicNumber)){
                  OpenSel(bidPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor);
         }
         kagiData.localMinimum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.isYang         = false;
         kagiData.isYin          = true;
      }
      
      if(kagiData.isUptrend && kagiData.isYin && (currentClosePrice >= (kagiData.referencePrice + reversalAmount) && (currentClosePrice > kagiData.localMaximum))){
         
         // Close a short position if it exists
         if(IsThereAnActiveSellPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a long position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_LONG){
               if(!IsThereAnActiveBuyPosition(magicNumber)){
                  OpenBuy(askPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawYinLine  (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine (GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor);
         }
         kagiData.localMaximum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
         kagiData.isYang         = true;
         kagiData.isYin          = false;
      }
          
      ...      
      
      //--- Handle a weird scenario
      if(kagiData.isUptrend && kagiData.isYin && currentClosePrice >= (kagiData.referencePrice + reversalAmount) && currentClosePrice > kagiData.localMaximum){
         
         // Close a short position if it exists
         if(IsThereAnActiveSellPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a long position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_LONG){
               if(!IsThereAnActiveBuyPosition(magicNumber)){
                  OpenBuy(askPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawBuySignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMaximum, yinLineColor);
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMaximum, kagiData.referenceTime, currentClosePrice, yangLineColor);
         }
         kagiData.isYin  = false;
         kagiData.isYang = true;
         kagiData.localMaximum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
      }
      
      if(kagiData.isDowntrend && kagiData.isYang && currentClosePrice <= (kagiData.referencePrice - reversalAmount) && currentClosePrice < kagiData.localMinimum){
         
         // Close a long position if it exists
         if(IsThereAnActiveBuyPosition(magicNumber)){
            ClosePositionsByMagic(magicNumber);
            Sleep(50);
         }
         
         //--- Open a short position if allowed
         if(enableTrading){
            if(direction == TRADE_BOTH || direction == ONLY_SHORT){
               if(!IsThereAnActiveSellPosition(magicNumber)){
                  OpenSel(bidPrice);
               }
            }
         }
         
         //--- Render a sell signal(down) arrow
         datetime lastBarOpenTime = iTime(_Symbol, kagiTimeframe, 1);
         double lastBarClosePrice = iClose(_Symbol, kagiTimeframe, 1);
         DrawSellSignalMarker(lastBarOpenTime, lastBarClosePrice);
         
         if(overlayKagi){
            DrawYangLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.referencePrice, kagiData.referenceTime, kagiData.localMinimum, yangLineColor);
            DrawYinLine(GenerateUniqueName(TRENDLINE), kagiData.referenceTime, kagiData.localMinimum, kagiData.referenceTime, currentClosePrice, yinLineColor);
         }
         kagiData.isYang = false;
         kagiData.isYin  = true;
         kagiData.localMinimum   = currentClosePrice;
         kagiData.referencePrice = currentClosePrice;
      }
      
   }
}


トレーリングストップの設計と実装

トレーリングストップは、取引が有利な方向に進行した際に利益を保護するための仕組みです。このEAではトレーリングストップは任意機能であり、入力パラメータenableTrailingStopによって有効/無効を切り替えることができます。有効にした場合、EAは価格があらかじめ定義された複数の閾値に到達するたびに、3段階でストップロスを調整します。目的は、段階的に利益を固定しつつ、取引に十分な値動きの余地を残すことです。

トレーリングロジックでは、エントリー価格とテイクプロフィット価格の距離を基準として使用します。この距離を4分割し、その値をトレーリングステップとします。次に、このステップを用いてエントリー価格から閾値レベルを計算します。買いの場合、閾値は以下の通りです。

  1. 建値 + 1トレーリングステップ
  2. 建値 + 2トレーリングステップ
  3. 建値 + 3トレーリングステップ

価格が第1閾値を超えるとストップロスは1ステップ分引き上げられます。第2閾値を超えると再びストップロスが調整され、第3閾値でも同様の処理がおこなわれます。ショートの場合も同じ考え方ですが、方向は逆になります。

トレーリングの状態を整理して管理するために、これら3つのレベルと対応するストップロスレベルを保持する小さな構造体を使用します。また、それぞれのレベルで既に調整がおこなわれたかどうかを示す3つのブールフラグも保持します。これにより、価格が上下に戻るたびに同じ調整が繰り返されることを防ぎます。

この構造体は、他のデータ構造の近くに配置します。


struct MqlTrailingStop
{
   double level1;
   double level2;
   double level3;
   double stopLevel1;
   double stopLevel2;
   double stopLevel3;
   bool isLevel1Active;
   bool isLevel2Active;
   bool isLevel3Active;
};

グローバル変数としてインスタンス化します。

//--- Instantiate the trailing stop structure
MqlTrailingStop trailingStop;

また、直近の分足の終値を格納するための配列も必要になります。これは、レベルクロスオーバーを確実に検出するのに役立ちます。

//--- To store minutes data
double closePriceMinutesData [];

OnInit内でminutes配列をシリーズとして設定します。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit(){

   ...
   
   //--- Array Set As Series
   
   ...
   
   ArraySetAsSeries(closePriceMinutesData, true);
   
   ...
   
   return INIT_SUCCEEDED;
}

そして、ティックごとに補充します。OnTickで買値と売値を更新した後、コピーの呼び出しを追加します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
   
   //--- Get some minutes data
   if(CopyClose(_Symbol, PERIOD_M1, 0, 7, closePriceMinutesData) == -1){
      Print("Error while copying minutes datas ", GetLastError());
      return;
   }
   
   ...
   
}

直近の数本のバーを使用することで、単一ティックノイズによる誤作動を回避できます。

価格が閾値を超えたかどうかを検出するために、2つのヘルパー関数を追加します。分配列内の直近2つの終値を比較し、クロスが発生した場合にtrueを返します。

//+------------------------------------------------------------------+
//| To detect a crossover at a given price level                     |                               
//+------------------------------------------------------------------+
bool IsCrossOver(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] <= price && closePriceMinsData[0] > price){
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| To detect a crossunder at a given price level                    |                               
//+------------------------------------------------------------------+
bool IsCrossUnder(const double price, const double &closePriceMinsData[]){
   if(closePriceMinsData[1] >= price && closePriceMinsData[0] < price){
      return true;
   }
   return false;
}

これらの関数はシンプルで信頼性が高く、価格が分足の終値レベルの片側からもう一方へ移動したことを示します。

EAが取引を開始した際には、trailingStop構造体にトレーリングの閾値およびストップ目標を計算して格納します。この処理は注文が成功した直後におこないます。

ロングポジションの場合、以下を計算します。

  1. targetDistancetakeProfitからentryを引いた値
  2. trailingSteptargetDistanceを4で割った値
  3. level1は建値+trailingStep
  4. level2はlevel1+trailingStep
  5. level3はlevel2+trailingStep
  6. stopLevel1は元のストップ+trailingStep
  7. stopLevel2はstopLevel1とtrailingStepの合計
  8. stopLevel3はstopLevel2とtrailingStepの合計

3つのブールフラグはすべてfalseに設定し、各レベルがまだアクション可能な状態であることを示します。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenBuy(const double askPr){
   
   ...
   
   if(!Trade.Buy(volume, _Symbol, askPr, stopLevel, targetLevel)){
      Print("Error while opening a long position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{
      
      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = targetLevel - askPr;
      double trailingStep         = NormalizeDouble(targetDistance / 4,   Digits());
      trailingStop.level1         = NormalizeDouble(askPr + trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 + trailingStep, Digits());
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 + trailingStep, Digits());
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel + trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 + trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 + trailingStep, Digits());
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      return true;
   }
   
   return false;
}

売り取引の場合も同様の考え方ですが、エントリーレベルとストップレベルからステップ数を引きます。

//+------------------------------------------------------------------+
//| Function used to open a market buy order.                        |   
//+------------------------------------------------------------------+
bool OpenSel( const double bidPr){

   ...
   
   if(!Trade.Sell(volume, _Symbol, bidPr, stopLevel, targetLevel)){
      Print("Error while opening a short position, ", GetLastError());
      Print(Trade.ResultRetcode());
      Print(Trade.ResultComment());
      return false;
   }else{ 
      
      ...
      
      //--- Refill the trailing Stop struct
      double targetDistance       = bidPr - targetLevel;
      double trailingStep         = NormalizeDouble(targetDistance / 4,   Digits());
      trailingStop.level1         = NormalizeDouble(bidPr - trailingStep, Digits());
      trailingStop.level2         = NormalizeDouble(trailingStop.level1 - trailingStep, Digits());
      trailingStop.level3         = NormalizeDouble(trailingStop.level2 - trailingStep, Digits());
      trailingStop.stopLevel1     = NormalizeDouble(stopLevel - trailingStep, Digits());
      trailingStop.stopLevel2     = NormalizeDouble(trailingStop.stopLevel1 - trailingStep, Digits());
      trailingStop.stopLevel3     = NormalizeDouble(trailingStop.stopLevel2 - trailingStep, Digits());
      trailingStop.isLevel1Active = false;
      trailingStop.isLevel2Active = false;
      trailingStop.isLevel3Active = false;
      return true;
   }
   
   return false;
}

この初期化により、EAは価格の変動に応じてストップをいつ、どこに移動させるべきかを正確に把握できます。 

トレーリングレベルとヘルパー関数が準備できたので、次のステップはManageTrailingStop関数自体を追加することです。 この関数は、EAの本体部分、トレーリングヘルパーのすぐ下に配置する必要があります。

//+------------------------------------------------------------------+
//| To track price action and updates the trailing stop              |   
//+------------------------------------------------------------------+
void ManageTrailingStop(){

   int totalPositions = PositionsTotal();
   //--- Loop through all open positions
   for(int i = totalPositions - 1; i >= 0; i--){
      ulong ticket = PositionGetTicket(i);
      if(ticket != 0){
         // Get some useful position properties
         ENUM_POSITION_TYPE positionType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);  
         string symbol                   = PositionGetString (POSITION_SYMBOL);
         ulong magic                     = PositionGetInteger(POSITION_MAGIC);
         double targetLevel              = PositionGetDouble(POSITION_TP);
         if(positionType == POSITION_TYPE_BUY ){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossOver(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossOver(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }
               
            }
         }
         if(positionType == POSITION_TYPE_SELL){
            if(symbol == _Symbol && magic == magicNumber){
            
               if(IsCrossUnder(trailingStop.level1, closePriceMinutesData) && !trailingStop.isLevel1Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel1, targetLevel)){
                     Print("Error while trailing SL at level 1: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel1Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level2, closePriceMinutesData) && !trailingStop.isLevel2Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel2, targetLevel)){
                     Print("Error while trailing SL at level 2: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel2Active = true;
                  }
               }
               
               if(IsCrossUnder(trailingStop.level3, closePriceMinutesData) && !trailingStop.isLevel3Active){
                  if(!Trade.PositionModify(ticket, trailingStop.stopLevel3, targetLevel)){
                     Print("Error while trailing SL at level 3: ", GetLastError());
                     Print(Trade.ResultRetcodeDescription());
                     Print(Trade.ResultRetcode());
                  }else{
                     trailingStop.isLevel3Active = true;
                  }
               }     
            }
         }
      }
   }   
}

その役割はシンプルです。新しいティックが到着するたびに、現在保有しているポジションのいずれかがトレーリング閾値を超えているかどうかを確認し、必要に応じてストップロスを調整します。

この関数の内部では、EAはまず現在保有しているすべてのポジションをループ処理します。それぞれのポジションについて、チケット番号、銘柄、マジックナンバー、そして買いか売りかといった重要な情報を取得します。EAが管理するのは、チャートと同じ銘柄であり、かつEAのマジックナンバーで開かれたポジションのみです。これにより、手動で開かれたポジションや他のEAによるポジションが誤って変更されることを防ぎます。

ロングポジションの場合、EAはクロスオーバー判定ヘルパーを使用して、価格がレベル1、2、または3を上抜けたかどうかを確認します。価格がレベル1を超え、かつその調整がまだ一度も使用されていない場合、EAはTrade.PositionModifyを呼び出してストップロスをstopLevel1に移動し、同時にレベル1をアクティブとしてマークします。同様のロジックがレベル2およびレベル3にも適用されます。各レベルは一度だけトリガーされるため、トレーリングはクリーンで予測可能な形になります。

ショートポジションの場合も手順は同じですが、crossunderヘルパーを使用します。価格がレベル1、2、または3を下回った場合、EAはストップロスを段階的に更新し、それぞれのレベルを完了済みとしてマークします。この対称的な処理により、トレード方向に関係なくトレーリングロジックの一貫性が保たれます。

すべてのPositionModifyの呼び出しは成功判定されます。もしブローカーが変更を拒否した場合、EAは[エキスパート]タブにメッセージを出力し、最小ストップ距離や証拠金不足などの問題をユーザーが特定できるようにします。

このように構造化されたトレーリングロジックを設計することで、各調整は閾値を本当に超えた場合にのみ一度だけ発生するようになります。この機能は、分足終値データを利用することで、ノイズに反応することを回避し、制御された方法で取引を保護します。

トレーリング関数が実装されたら、あとはOnTick内でそれを有効化するだけです。クォート更新とかぎ足のリアルタイム構築の後、ユーザーが機能を有効にしている場合にトレーリングマネージャーを呼び出します。

//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick(){

   ...
      
   //--- Trigger the trailing stop functionality
   if(enableTrailingStop){
      ManageTrailingStop();
   }
}

すべての主要コンポーネントが揃ったことで、取引EAの完全なバージョンの構築が完了しました。次のセクションではシステムのテストに焦点を当て、その後、本番環境で使用するための準備に進みます。


EAのテスト

KagiTrader EAのすべてのコンポーネントが実装されたため、次のステップは実際の市場環境でシステムがどのように動作するかを検証することです。そのために、2024年1月から2024年12月までの期間を対象として、日経225(JPN225)指数でフルバックテストを実施しました。このテストにより、かぎ足構造における価格のライブな変化、方向転換、そしてトレーリングストップ調整に対してEAがどのように対応するかを確認できます。

このテストでは、カギ足の動的構築、ポジション管理ルール、オプションのトレーリングストップロスを含むEAのすべての機能を有効化しました。バックテストはMetaTrader 5のストラテジーテスターを使用し、標準設定で実行されています。

結果として得られたエクイティカーブは、控えめながらも安定した収益性を示しました。これは、主に構造変化と規律あるSL/TPロジックに依存するシステムとしては良い兆候です。EAは安定した動作を示し、トレードの適切なオープンやクローズ、トレーリング閾値の遵守、そして異なるボラティリティ局面における一貫性を維持しました。

以下は、通年テストによって得られたエクイティカーブです。

エクイティカーブ

テスト結果

この結果は、KagiTrader EAが正しく動作しており、トレンドおよびリトレースメントのサイクルを異常な挙動なく処理できていることを確認するものです。ここでのパフォーマンスは単一銘柄および単一時間軸に基づくものですが、さらなる市場への拡張やパラメータ最適化のための十分なベースラインを提供しています。

このテストを簡単に再現できるように、バックテストで使用した.setファイルを同梱しています。ストラテジーテスターに直接読み込むことで、この評価と同一の条件およびパラメータを再現できます。

バックテスト結果に加えて、チャート上でシグナルの実行も視覚的に検証しました。その証拠として2枚のスクリーンショットを掲載しています。最初の画像では、かぎ足構造が陰から陽へ移行した瞬間にEAがロングポジションを開く様子が確認でき、強気の反転が正しく検出されていることが分かります。

ロングポジション

2枚目のスクリーンショットでは、その逆のケースが示されています。すなわち、陽から陰へのかぎ足反転によってショートポジションが発生しており、ルールセット通りに動作していることが確認できます。

ショートポジション

これらのチャート例は、EAが設計通りにトレンド変化へ反応していること、そして売買ロジックがリアルタイム環境で正しく機能していることに対する信頼性を与えるものです。


結論

KagiTraderを単純なシグナル解釈ツールから、完全に機能するEAへと発展させました。視覚的なシグナルマーカー、柔軟なトレードモード、よりスマートなポジションサイズ計算、動的なストップロス配置、そして構造化された3段階トレーリングシステムを追加しました。各機能は段階的に導入されており、そのロジックを追いながら全体像の中でどのように機能しているかを理解できるようになっています。

すべてを統合した時点で、このEAはかぎ足の反転をリアルタイムで読み取り、責任ある形でポジションのオープンおよび管理をおこない、市場の変化に応じてストップを調整できるようになりました。日経指数に対するバックテストでは、システムが一貫して動作し、設計通りにロジックを実行していることが確認されました。

EAが完成しテストも成功したことで、フィルターの追加、マネーマネジメントの改善、あるいは代替チャート手法の検討など、さらなる改良、拡張、実験をおこなうための強固な基盤が得られました。本連載の目的は実践的な自動売買開発スキルを習得することであり、この時点に到達したことで、ゼロから実際に動作する取引ツールを構築したことになります。


この記事で使用されているすべてのソースコードは以下に記載されています。続く表では各ファイルとその役割を説明します。

ファイル名 説明
KagiTraderPart1.mq5 第1回で作成した元のコード(第2回で拡張・改善したベース)
KagiTrader.mq5 第2回のソースコード
KagiTrader.set 第2回2のバックテストを実行するために使用された.setファイル

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

添付されたファイル |
KagiTrader.mq5 (130.72 KB)
kagiTrader.set (1.46 KB)
機械学習の限界を克服する(第9回):自己教師あり学習を用いた金融における相関ベース特徴学習 機械学習の限界を克服する(第9回):自己教師あり学習を用いた金融における相関ベース特徴学習
自己教師あり学習は、観測値そのものから生成された教師信号を探索する統計学習の強力なパラダイムです。このアプローチは、教師なし学習における困難な問題を、より馴染みのある教師あり学習問題へと再定式化します。この技術は、アルゴリズムトレーダーコミュニティの目的に対して、見過ごされてきた応用可能性を持っています。したがって本記事の議論は、読者に対して自己教師あり学習という未開拓の研究領域への橋渡しを提供し、さらに小規模データセットへの過学習を回避しながら、金融市場の頑健で信頼性の高い統計モデルを提供する実践的応用を提示することを目的としています。
古典的な戦略を再構築する(第14回):移動平均クロスオーバーの徹底解説 古典的な戦略を再構築する(第14回):移動平均クロスオーバーの徹底解説
本記事では、古典的な移動平均クロスオーバー戦略を改めて取り上げ、ノイズが多く変動の激しい市場環境においてなぜこの戦略がうまく機能しないのかを検証します。そのうえで、シグナル品質を向上させ、弱いまたは収益性の低い取引を除外するための5つの代替フィルタリング手法を紹介します。また、統計モデルが人間の直感や従来のルールでは捉えきれない誤差をどのように学習し、補正できるかについても説明します。読者は、時代遅れの戦略をどのように現代化するか、また金融モデリングにおいてRMSEのような指標に過度に依存することの落とし穴について理解を深めることができます。
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法 エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
この記事では、MT4において複数のEAの衝突をさける方法を扱います。ターミナルの操作、MQL4の基本的な使い方がわかる人にとって、役に立つでしょう。
共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較 共和分株式による統計的裁定取引(第8回):ポートフォリオのリバランスのためのローリングウィンドウ固有ベクトル比較
本記事では、共和分関係にある株式を用いた平均回帰型統計裁定戦略において、早期の不均衡診断およびポートフォリオリバランスのために、ローリングウィンドウ固有ベクトル比較を用いる手法を提案します。この手法は、従来のインサンプル/アウトオブサンプルADF (IS/OOS ADF)検証と比較されており、固有ベクトルの変化が、IS/OOS ADFが依然としてスプレッドの定常性を示している場合であっても、リバランスの必要性を示唆し得ることを示します。本手法は主に実運用取引の監視を目的としていますが、結論として、固有ベクトル比較をスコアリングシステムに統合することも可能である一方で、その実際のパフォーマンスへの寄与については検証が必要であるとされています。