English Deutsch
preview
データサイエンスとML(第37回):ローソク足パターンとAIを活用して市場をリードする

データサイエンスとML(第37回):ローソク足パターンとAIを活用して市場をリードする

MetaTrader 5トレーディングシステム |
104 5
Omega J Msigwa
Omega J Msigwa

内容



はじめに

私が初めて使用した取引戦略は、ローソク足に基づくものでした。今振り返ると、それを“戦略”と呼ぶには不十分だったかもしれませんが、初めての取引は、友人に勧められて読んだ本間宗久の『The Candle Stick Trading Bible』に記載されていたローソク足パターンを用いたものでした。

金融チャートにおけるローソク足パターンは、過去のパターンに基づいて価格の動きを予測するためにトレーダーによって使われます。一見ランダムに見える価格の上下動から生成されますが、トレーダーはこれを活用して短期的な価格の方向性を予測しようとします。

このコンセプトは、18世紀に本間宗久という名の貿易商(トレーダー)によって発案されました。彼は歴史上最も成功したトレーダーの一人とされ、「相場の神様」と呼ばれていました。彼の発見は、現代価値で100億ドル以上の利益をもたらしたとも言われています。 

本間宗久は、米の価格が需給で決まる一方、市場は人間の感情にも大きく影響されることを見出しました。

この人間の感情はローソク足に反映され、価格の動きの大きさや色によって表現されます。例えば、黒いローソク足は弱気の動きを、白いローソク足は強気の動きを示すのが一般的です。ただし、現在ではこれらの色は取引プラットフォームにより自由に変更可能であり、色自体に意味がない場合もあります


ローソク足の基本

ローソク足の髭(ヒゲ)は、その日の高値と安値を示し、それが始値と終値とどのように比較されるかを表します。ローソク足の形は、OHLC(始値、高値、安値、終値)の関係によってさまざまに変化します。本間宗久によって紹介された多数のローソク足パターンに加えて、近年では多くのトレーダーたちによって新たなパターンも登場しています。

本記事では、これらのローソク足パターンを識別・収集し、機械学習モデルに適用することで、AIベースの取引モデルにどのような価値を加えられるのか、またそれによって金融市場で優位に立てるかどうかを検証していきます。


ローソク足パターン

ローソク足パターンは多数存在しますが、本記事では10個のパターンのみに絞って解説します。この10個を選んだ主な理由は、理解しやすく、かつ単一のローソク足(バー)のみで判断可能なためです。つまり、現在のバーだけで検出できるという特徴があります。

複数のローソク足を必要とするパターンはすべて除外します。

CTALibクラスの実装形式は、TALib (Technical Analysis Library)に着想を得ています。TALibは、C#、C++、Pythonで構築されたテクニカル分析ライブラリであり、ローソク足パターンを検出する多数の関数が備わっています。

陽線(白いローソク足)

これは、終値が始値よりも高い場合に形成される強気のローソク足です。この単純な陽線は、上昇の勢い(モメンタム)を示しますが、将来の価格動向を予測するものではありません。単に、そのローソク足の方向性を示すものです

これは以下のように実装できます。

bool CTALib::CDLWHITECANDLE(double open, double close)
 {
   return (close > open);
 }

陰線(黒いローソク足)

陽線とは逆に、始値が終値よりも高い場合に形成される弱気のローソク足です。この単純な陰線は、下降のモメンタムを示します。

陽線と同様に、将来の価格を予測するものではなく、あくまでローソク足の方向性を示すだけです。

bool CTALib::CDLBLACKCANDLE(double open,double close)
 {
   return (open > close);
 }

十字線

十字線とは、始値と終値がほぼ等しいローソク足のことを指します。その見た目は、十字、逆十字、またはプラス記号のように見えることがあります。このローソク足パターンは比較的まれであり、まとまって現れることが多い傾向にあります。十字は、一般的にトレンド転換のシグナルとされることがありますが、同時に、将来の価格に対する市場の迷いを示している可能性もあります。

また、このパターンには以下のようないくつかのバリエーションがあります(下図参照)。

十字線を検出する関数を実装するのは難しい場合があります。十字線の定義は「始値と終値が等しい」ことですが、今日の市場の取引量やボラティリティを考えると、始値と終値が完全に同じになるローソク足は非常に稀です。たとえば、始値が1.0000で終値が1.0001の場合、これらはコンピュータ上では等しくありません。

この問題に対処するために、許容誤差を設定する方法があります。始値と終値の差がこの許容誤差以内であれば、価格がほとんど同じと見なして、十字線と判断することができます。

bool CTALib::CDLDOJI(double open,double close, double torrelance=3)
 {
   return (fabs(open-close)<torrelance*_Point); //A torrelance in market points
 }

トンボ(蜻蛉)

トンボは、価格の反転の可能性を示唆します。特に、下降トレンドの途中でトンボが現れた場合は、価格が上昇に転じる兆候や、下降トレンドの終了を示す良いサインとされています。

トンボ

トンボは、単なる十字線でありながら、下髭が上髭よりも長いことが特徴です。そのため、コードで検出する際は、まずローソク足が十字線であることを確認し、さらに下髭の長さが上髭の長さの少なくとも2倍以上であることを明示的に確認する必要があります。

bool CTALib::CDLDRAGONFLYDOJI(double open, 
                              double high, 
                              double low, 
                              double close, 
                              double body_torrelance = 3, 
                              double shadow_ratio = 2.0)
{
   double body_size    = MathAbs(open - close);
   double upper_shadow = upperShadowCalc(open, close, high);
   double lower_shadow = lowerShadowCalc(open, close, low);

   //--- Body is very small (like a Doji)
   if (CDLDOJI(open, close, body_torrelance))
   {
      //--- Lower shadow is significantly longer than upper shadow
      if (lower_shadow > upper_shadow * shadow_ratio)
         return true;
   }

   return false;
}

墓石

上髭が長い十字線です。これは弱気の反転サインであり、市場の方向性が変わる可能性を示しています。

このローソク足が上昇トレンドの途中で現れた場合、現在の上昇トレンドが終わり、下降トレンドに転じる可能性があることを示唆しています。

これは上髭が下髭より長い単なる十字線なので、コードでは、まずローソク足が十字線であることを確認でき、次に上髭が下髭より2倍長いことを明示的に確認する必要があります。

bool CTALib::CDLGRAVESTONEDOJI(double open, 
                               double high, 
                               double low, 
                               double close, 
                               double body_torrelance = 3, 
                               double shadow_ratio = 2.0)
{
   double body_size    = MathAbs(open - close);
   double upper_shadow = upperShadowCalc(open, close, high);
   double lower_shadow = lowerShadowCalc(open, close, low);

   //--- Body is very small (like a Doji)
   if (CDLDOJI(open, close, body_torrelance))
   {
      //--- Lower shadow is significantly longer than upper shadow
      if (upper_shadow > lower_shadow * shadow_ratio)
         return true;
   }

   return false;
}

ハンマー

ローソク足の上部に小さな実体があり、下部に長い下髭を持つローソク足です。下降トレンド中に出現すると、上昇への転換の可能性を示す強気の反転サインです。

ハンマー

ローソク足パターンは抽象的で分かりにくいことが多く、見方やそのときに注目している点によって、同じパターンでも異なるように見えることがあります。たとえば、ハンマーは、実体の大きさが違うだけで、トンボと混同されやすいです。両者は特徴が似ているため、区別が難しいことがあります。

このような問題は、ローソク足パターンを扱う際によく直面する課題の一つです。

この課題に対処するためには、明確なルールや閾値を設定することが有効です。これらの設定値は、銘柄ごとや用途に応じて調整できるようにすると、より正確なパターン判別が可能になります。

bool CTALib::CDLHAMMER(double open, 
                       double high, 
                       double low, 
                       double close, 
                       double min_body_percentage = 0.2,       // To avoid being a doji
                       double lower_shadow_ratio = 2.0,        // Lower shadow at least 2x the body
                       double upper_shadow_max_ratio = 0.3)    // Upper shadow must be small
{
   double body_size      = MathAbs(open - close);
   double total_range    = high - low + DBL_EPSILON;
   double upper_shadow   = upperShadowCalc(open, close, high);
   double lower_shadow   = lowerShadowCalc(open, close, low);
   double body_percentage = body_size / total_range;

   return (
      body_percentage >= min_body_percentage &&
      lower_shadow >= lower_shadow_ratio * body_size &&
      upper_shadow <= upper_shadow_max_ratio * body_size
   );
}

トンボとハンマーは、どちらも長い下髭、小さな実体、短い上髭という共通点を持っているため、区別が難しいことがあります。そのため、ハンマーであるかどうかを判断するために、実体の小ささをローソク足全体の値幅(High−Low)に対してどれくらいの割合であるかを確認するmin_body_percentageという基準を導入しました。また、デフォルトでは、下髭が上髭の2倍以上あるかどうかも同時にチェックしています。

逆ハンマー

これはハンマーに似たパターンですが、実体が下部に小さくあり、長い上髭と短い下髭を持つのが特徴です。

このパターンは下降トレンド中に出現し、市場が上昇へ転換する兆しとされています。

逆ハンマー

髭に少し変更を加えるだけで、ハンマーと同じように実装できます。

bool CTALib::CDLINVERTEDHAMMER(double open, 
                               double high, 
                               double low, 
                               double close, 
                               double min_body_percentage = 0.2,        // Avoid doji
                               double upper_shadow_ratio = 2.0,         // Upper shadow must be long
                               double lower_shadow_max_ratio = 0.3)     // Lower shadow should be small
{
   double body_size        = MathAbs(open - close);
   double total_range      = high - low + DBL_EPSILON;
   double upper_shadow     = upperShadowCalc(open, close, high);
   double lower_shadow     = lowerShadowCalc(open, close, low);
   double body_percentage  = body_size / total_range;

   return (
      body_percentage >= min_body_percentage &&
      upper_shadow >= upper_shadow_ratio * body_size &&
      lower_shadow <= lower_shadow_max_ratio * body_size
   );
}

コマ足

実体が中央に小さく、上下に長い髭を持つローソク足です。

コマ足

このローソク足が出現した場合、市場の迷いを示しており、買い手と売り手のどちらにも優勢がないことから、現在のトレンドが継続する可能性が高いとされています。

このローソク足は十字線に似ていますが、実体がやや大きい点が異なります。そのため、実体が十字線のように極端に小さくならないように明確な基準を設け、さらに長い髭の中央に実体が位置していることを確認する必要があります。

bool CTALib::CDLSPINNINGTOP(double open,
                            double high,
                            double low,
                            double close, 
                            double body_percentage_threshold = 0.3, 
                            double shadow_ratio = 2.0,
                            double shadow_symmetry_tolerance = 0.3)
{
   double body_size      = MathAbs(open - close);
   double total_range    = high - low + DBL_EPSILON;
   double upper_shadow   = upperShadowCalc(open, close, high);
   double lower_shadow   = lowerShadowCalc(open, close, low);
   double body_percentage = body_size / total_range;

   //--- Calculate shadow symmetry ratio
   double shadow_diff = MathAbs(upper_shadow - lower_shadow);
   double shadow_sum = upper_shadow + lower_shadow + DBL_EPSILON;
   double symmetry_ratio = shadow_diff / shadow_sum; // Closer to 0 = more balanced

   return (
      body_percentage < body_percentage_threshold && // Body is small compared to candle size
      upper_shadow > body_size * shadow_ratio && // Both shadows are significantly larger than the body
      lower_shadow > body_size * shadow_ratio &&
      symmetry_ratio <= shadow_symmetry_tolerance //Shadows are roughly equal (symmetrical)
   );
}

陽の丸坊主

「丸坊主」という名称は「坊主頭」に由来し、髭のないローソク足を意味します。

陽の丸坊主は、上髭と下髭がほとんどない、またはまったくない陽線であり、強い上昇のモメンタムを示す非常に強気なシグナルとされています。

陽の丸坊主

始値および終値が高値や安値に非常に近いかどうかを判定する際に、許容値をポイント単位で設定することができます。

bool CTALib::CDLBULLISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2)
 {
   return (MathAbs(open - low) <= (tolerance*_Point) &&  MathAbs(close - high) <= (tolerance*_Point) && close > open);
 }

陰の丸坊主

陰の丸坊主は、上髭と下髭がほとんどない、またはまったくない陰線であり、強い下落のモメンタムを示す非常に弱気なシグナルとされています。

陰の丸坊主

陽の丸坊主と同様に、始値と終値が高値・安値に非常に近いかどうかを判定するために、ポイント単位の許容値を設定しています。

bool CTALib::CDLBEARISHMARUBOZU(double open, double high, double low, double close, double tolerance = 2)
 {
   return (MathAbs(open - high) <= (tolerance*_Point) && MathAbs(close - low) <= (tolerance*_Point) && close < open);
 }

現在はローソク足の見た目だけでパターンとシグナルの検出をおこなっていますが、私の情報源によると、ローソク足から正しくシグナルを抽出するにはトレンドの検出を含めることが必須です。たとえば、ハンマーが強気のシグナルとして成立するためには、必ず下降トレンドの中で出現している必要があります。

トレンドはシグナル判定において非常に重要な要素であり、このプロジェクトをさらに発展させたいのであれば、ぜひ検討すべきポイントです。


ローソク足パターン検出インジケーター

ローソク足パターンを、これまでパターンを導き出すために使ったコードで可視化しましょう。パターンは抽象的で分かりにくいことが多いため、また機械学習に使うデータとしてはデータの質が一番重要になるので、パターンの計算方法とその見た目を可視化してしっかり理解できるようにしたいからです。

パラメータの調整やコードの修正は自由におこなってください

人間が市場で目視できるパターンを、少なくとも今回書いたコードで検出できることを確認しましょう。

このローソク足ベースのインジケーターは、5つのバッファとメインチャート上に1つのプロットを持つ形にします。

ファイル名: Candlestick Identifier.mq5

#property indicator_chart_window
#property indicator_buffers 5
#property indicator_plots 1

#property indicator_type1 DRAW_COLOR_CANDLES
#property indicator_color1 clrDodgerBlue, clrOrange, clrRed
#property indicator_style1  STYLE_SOLID
#property indicator_width1  1

double OpenBuff[];
double HighBuff[];
double LowBuff[];
double CloseBuff[];
double ColorBuff[];

#include <ta-lib.mqh> //!important for candlestick patterns

ta-lib.mqhライブラリは静的クラスなので、クラスを初期化する必要がなく、OnCalculate関数内でローソク足パターンを検出する関数をすぐに呼び出すことができます。

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
//---
   
   if (rates_total<1)
     return rates_total;
   
    for(int i = prev_calculated; i < rates_total; i++)
     {
      OpenBuff[i]  = open[i];
      HighBuff[i]  = high[i];
      LowBuff[i]   = low[i];
      CloseBuff[i] = close[i];
      
      //---
      
      if (close[i]>open[i])
         ColorBuff[i] = 1.0; 
      else
         ColorBuff[i] = 0.0;
      
      //---
      
      double padding = MathAbs(high[i] - low[i]) * 0.2; // 20% padding
      
      if (CTALib::CDLDOJI(open[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding, "Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLDRAGONFLYDOJI(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"DragonFly Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLGRAVESTONEDOJI(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"GraveStone Doji", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLHAMMER(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Hammer", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLINVERTEDHAMMER(open[i], high[i], low[i], close[i]))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Inverted Hammer", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLSPINNINGTOP(open[i], high[i], low[i], close[i], 0.3, 2.0))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Spinning Top", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLBULLISHMARUBOZU(open[i], high[i], low[i], close[i], 2))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bullish Marubozu", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
        
      if (CTALib::CDLBEARISHMARUBOZU(open[i], high[i], low[i], close[i], 2))
        {
          TextCreate(string(i)+(string)time[i], time[i]-PeriodSeconds(), high[i]+padding,"Bearish Marubozu", clrBlack, 90.0);
          ColorBuff[i] = 2.0;
        }
     }
     
//--- return value of prev_calculated for next call
   return(rates_total);
  }

デフォルトでは、強気のローソク足はオレンジ色、弱気のローソク足は青色で表示されます。この記事で解説したパターンに該当するローソク足は、赤色でマーキングされ、ローソク足に対して90度回転したテキストで、赤くマークされたローソク足に、そのパターン名が表示されます。

ローソク足パターンインジケーター表示

上の画像でご覧のとおり、ローソク足検出インジケーターは、これらのローソク足パターンをかなり正確に識別できています。これで、コードに適用したロジックに自信を持つことができます。 

では次に、これらのパターンを収集してスクリプトでCSVファイルに保存し、そのデータを使って機械学習モデルのトレーニングに活用しましょう。


機械学習のためのローソク足パターンの収集

一部のローソク足パターンは市場であまり頻繁に現れないことがあります。特に、上位の時間枠では過去のバー数が非常に少ないためです。そこで、2005年1月1日から2023年1月1日までの期間を対象にデータを収集していきましょう。

この18年間という長期間であれば、日足のバー数も十分に確保でき、多数のパターンを機械学習モデルに観察させることが可能です。

#include <ta-lib.mqh> //Contains CTALib class for candlestick patterns detection
#include <MALE5\Pandas\pandas.mqh> //https://www.mql5.com/ja/articles/17030

input datetime start_date = D'2005.01.01';
input datetime end_date = D'2023.01.01';

input string symbol = "XAUUSD";
input ENUM_TIMEFRAMES timeframe = PERIOD_D1;
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//---
   
   vector open, high, low, close;
   
   open.CopyRates(symbol, timeframe, COPY_RATES_OPEN, start_date, end_date);
   high.CopyRates(symbol, timeframe, COPY_RATES_HIGH, start_date, end_date);
   low.CopyRates(symbol, timeframe, COPY_RATES_LOW, start_date, end_date);
   close.CopyRates(symbol, timeframe, COPY_RATES_CLOSE, start_date, end_date);
   
   CDataFrame df;
   
   vector cdl_patterns = {};
   cdl_patterns = CTALib::CDLWHITECANDLE(open, close);
   df.insert("White Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBLACKCANDLE(open, close);
   df.insert("Black Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLDOJI(open, close);
   df.insert("Doji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLDRAGONFLYDOJI(open, high, low, close);
   df.insert("Dragonflydoji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLGRAVESTONEDOJI(open, high, low, close);
   df.insert("Gravestonedoji Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLHAMMER(open, high, low, close);
   df.insert("Hammer Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLINVERTEDHAMMER(open, high, low, close);
   df.insert("Invertedhammer Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLSPINNINGTOP(open, high, low, close);
   df.insert("Spinningtop Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBULLISHMARUBOZU(open, high, low, close);
   df.insert("BullishMarubozu Candle", cdl_patterns);
   
   cdl_patterns = CTALib::CDLBEARISHMARUBOZU(open, high, low, close);
   df.insert("BearishMarubozu Candle", cdl_patterns);
   
   df.insert("Open", open);
   df.insert("High", high);
   df.insert("Low", low);
   df.insert("Close", close);
   
   df.to_csv(StringFormat("CandlestickPatterns.%s.%s.csv",symbol,EnumToString(timeframe)), true);
  }

また、目的変数の準備や、万が一必要になった場合に備えて、OHLCの値も併せて収集します。


ローソク足パターンに基づいて予測するAIモデルの学習

データセットができたので、次はPythonスクリプト(Jupyter Notebook)でこのデータを読み込みましょう。

import pandas as pd

symbol = "XAUUSD"
df = pd.read_csv(f"/kaggle/input/forex-candlestick-patterns/CandlestickPatterns.{symbol}.PERIOD_D1.csv")

df

出力

White Candle Black Candle Doji Candle Dragonflydoji Candle Gravestonedoji Candle Hammer Candle Invertedhammer Candle Spinningtop Candle BullishMarubozu Candle BearishMarubozu Candle Open High Low Close
0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 438.45 438.71 426.72 429.55
1 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 429.52 430.18 423.71 427.51
2 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 427.50 428.77 425.10 426.58
3 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 426.31 427.85 420.17 421.37
4 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 421.39 425.48 416.57 419.02
... ... ... ... ... ... ... ... ... ... ... ... ... ... ...


このローソク足データを扱う際に最も大きな課題は、目的変数の作成です。

金融市場に関する多くの分類問題では、通常lookaheadと呼ばれるパラメータを使い、将来の価格変動に基づいて目的変数を定義します。

このパラメータは、何本先(または何ステップ先)のデータを見るかを指定するもので、たとえばlookaheadが1の場合は、現在のバーの終値と次のバーの終値を比較します。

次のバーの終値が現在の終値より高ければ、上昇(強気)と判定しターゲットラベルを1とします。

そうでなければ、下降(弱気)と判定しターゲットラベルを0とします。

ここでも同様に、特徴量を使って先の情報を予測できるよう目的変数を作成できます。しかし、上記の表に示したように、多くの行でローソク足パターンが検出されておらず(特に、陽線や陰線は特別なパターンとして扱わないため)、ターゲットに0(パターンなし)が多く含まれています。

これはつまり、機械学習モデルにゼロ値を多く与えてしまい、モデルが意味のない情報からターゲットを予測しようとすることを意味します。結果的に、モデルは陽線・陰線に過度に依存するようになり、望ましくありません

この問題への対処法としては、まずゼロ値の行をすべて削除し、純粋にパターンが検出されたデータのみで学習をおこなう方法があります。ただし、この方法を採用すると、実際のモデル運用時にも同様の問題が起きないよう対策を講じる必要があります。

もう一つの方法として、すべてのローソク足パターンが0で、陽線や陰線のみの行に対しては「様子見」を示すクラスとして-1を割り当てるやり方があります。しかし、この場合はクラスの不均衡が非常に大きくなり、前回の記事で指摘した問題を解決するには至りません。

とりあえず、目的変数の準備を進めましょう。

lookahead = 1

new_df = df.copy()
new_df["future_close"] = new_df["Close"].shift(-lookahead)
new_df.dropna(inplace=True)  # Drop NaNs caused by the shift operation

signal = []
for i in range(len(new_df)):  # Iterate over rows, not columns
    if new_df["future_close"].iloc[i] > new_df["Close"].iloc[i]:
        signal.append(1)
    else:
        signal.append(0)

new_df["Signal"] = signal

次に、予測に使う特徴量を2次元配列Xに分割します。この際、OHLCの値や予測したい列(ターゲット)、そしてターゲット列を作成するために使用したfuture_close列は除外します。また、ターゲット列であるSignaly配列に割り当てます。

X = new_df.drop(columns=[
    "Signal",
    "Open",
    "High",
    "Low",
    "Close",
    "future_close"
])

y = new_df["Signal"]

# Split data into train and test sets
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, shuffle=False)

この問題にはCatBoostモデルを使うことにしました。理由は、カテゴリカルデータの列が多く含まれており、理論上CatBoost分類器がこれらの特徴量をうまく扱えるためです。

from catboost import CatBoostClassifier
from sklearn.utils.class_weight import compute_class_weight

# Automatically calculate class weights
classes = np.unique(y)
weights = compute_class_weight(class_weight='balanced', classes=classes, y=y)
class_weights = dict(zip(classes, weights))

# Define the base model
model = CatBoostClassifier(
    iterations=1000,
    learning_rate=0.01,
    depth=5,
    loss_function='Logloss',
    class_weights=class_weights,
    verbose=100
)

model.fit(X_train, y_train) # Training the classifier

出力

0:      learn: 0.6930586        total: 3.64ms   remaining: 3.64s
100:    learn: 0.6897625        total: 136ms    remaining: 1.21s
200:    learn: 0.6888030        total: 269ms    remaining: 1.07s
300:    learn: 0.6883559        total: 401ms    remaining: 931ms
400:    learn: 0.6881469        total: 532ms    remaining: 795ms
500:    learn: 0.6879966        total: 661ms    remaining: 658ms
600:    learn: 0.6879013        total: 789ms    remaining: 524ms
700:    learn: 0.6878311        total: 916ms    remaining: 391ms
800:    learn: 0.6877729        total: 1.04s    remaining: 260ms
900:    learn: 0.6877273        total: 1.17s    remaining: 129ms
999:    learn: 0.6876900        total: 1.3s     remaining: 0us
<catboost.core.CatBoostClassifier at 0x798cc6d08dd0>

このモデルを、まだ見たことのないデータ(テストサンプル)で評価してみましょう。

y_pred = model.predict(X_test)

print("\nClassification Report:\n", classification_report(y_test, y_pred))

出力

Classification Report:
               precision    recall  f1-score   support

           0       0.49      0.55      0.52       429
           1       0.58      0.52      0.55       511

    accuracy                           0.53       940
   macro avg       0.53      0.53      0.53       940
weighted avg       0.54      0.53      0.53       940

結果は、クラス1に対する適合率が0.58、クラス0に対しては0.49と、平均的なモデルであることを示しています。クラス1の予測には58%の確信度で頼ることができますが、クラス0の予測には信頼がおけず、むしろ推測したほうが良い状況です。

全体の正解率は53%で、これはこの取引分野では現実的な数値です。コイントスやランダムな推測(50%の勝率)よりはマシと言えます

では、次に特徴量重要度のプロットを確認して、このモデルに最も影響を与えた特徴量を見てみましょう。

import matplotlib.pyplot as plt

# Get feature importances
importances = model.get_feature_importance()
feature_names = X_train.columns if hasattr(X_train, 'columns') else [f'feature_{i}' for i in range(X_train.shape[1])]

# Create DataFrame for plotting
feat_imp_df = pd.DataFrame({
    'Feature': feature_names,
    'Importance': importances
}).sort_values(by='Importance', ascending=False)

# Plot
plt.figure(figsize=(7, 3))
plt.barh(feat_imp_df['Feature'], feat_imp_df['Importance'])
plt.gca().invert_yaxis()  # Highest importance on top
plt.title('Feature Importances')
plt.xlabel('Importance')
plt.ylabel('Feature')
plt.tight_layout()
plt.show()

出力

このモデルに最も影響を与えた特徴量はコマ足で、次いで十字線でした。反対に、陰の丸坊主は最も影響が小さい特徴量でした。

私が調べたところによると、ローソク足パターンの中には、ある程度長い期間や時間枠にわたってトレンドや市場の動きを示唆するものがあります。たとえば、上昇トレンドの頂点に出現したドージーは、しばらくの間下降トレンドが続く可能性を示す場合があります。

lookahead値「1」に基づいて目的変数を作成したので、これらのローソク足パターンを使用して、数本のバー先ではなく1本のバー先を示します。

ですので、lookahead値を1より大きく設定して試してみることをおすすめします。これにより、ローソク足パターンがより長期的な期間や異なる時間軸に対してどのような影響を持つのかを観察することができます。私自身の試行では、lookahead値を1にした場合が最も精度の高いモデルができました。

今のところはlookahead値1に固定して進めますが、この「予測期間の問題」は、最終的な自動売買ロボットの中で、ストップロスやテイクプロフィットの設定、あるいは一定本数のバー経過後に取引をクローズする仕組みを組み込むことで対応できると考えています。


自動売買ロボットへの最終実装

ローソク足パターンに基づいてモデルの学習が完了したので、実際のトレーディング環境でこのモデルをテストし、ローソク足パターンがAI(人工知能)の領域で有効に機能するかどうかを確認してみましょう。

まず、MQL5およびMetaTrader 5と互換性のあるONNX形式でモデルを保存する必要があります。

model_onnx = convert_sklearn(
    model,
    "catboost",
    [("input", FloatTensorType([None, X_train.shape[1]]))],
    target_opset={"": 12, "ai.onnx.ml": 2},
)

# And save.
with open(f"CatBoost.CDLPatterns.{symbol}.onnx", "wb") as f:
    f.write(model_onnx.SerializeToString())

このCatboostモデルの保存に関する詳細については、こちらを参照してください。

私たちのエキスパートアドバイザー(EA)は非常にシンプルです。

#include <Trade\Trade.mqh> //The trading module
#include <Trade\PositionInfo.mqh> //Position handling module
#include <ta-lib.mqh> //For candlestick patterns
#include <Catboost.mqh> //Has a class for deploying a catboost model

CTrade m_trade;
CPositionInfo m_position;

CCatboostClassifier catboost;

input int magic_number = 21042025;
input int slippage = 100;
input string symbol_ = "XAUUSD";
input ENUM_TIMEFRAMES timeframe_ = PERIOD_D1;
input int lookahead = 1;

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
      
   if (!MQLInfoInteger(MQL_TESTER))
     if (!ChartSetSymbolPeriod(0, symbol_, timeframe_))
       {
         printf("%s failed to set symbol %s and timeframe %s, Check these values. Err = %d",__FUNCTION__,symbol_,EnumToString(timeframe_),GetLastError());
         return INIT_FAILED;
       }
  
//---
   
   if (!catboost.Init(StringFormat("CatBoost.CDLPatterns.%s.onnx",symbol_), ONNX_COMMON_FOLDER)) //Initialize the catboost model
      return INIT_FAILED;
   
//---

   m_trade.SetExpertMagicNumber(magic_number);
   m_trade.SetDeviationInPoints(slippage);
   m_trade.SetMarginMode();
   m_trade.SetTypeFillingBySymbol(Symbol());
           
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   
  }
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
  {
//---
   
   double open = iOpen(Symbol(), Period(), 1),
          high = iHigh(Symbol(), Period(), 1),
          low  = iLow(Symbol(), Period(), 1), 
          close = iClose(Symbol(), Period(), 1);   
   
   vector x = {
               CTALib::CDLWHITECANDLE(open, close),
               CTALib::CDLBLACKCANDLE(open, close),
               CTALib::CDLDOJI(open, close),
               CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
               CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
               CTALib::CDLHAMMER(open, high, low, close),
               CTALib::CDLINVERTEDHAMMER(open, high, low, close),
               CTALib::CDLSPINNINGTOP(open, high, low, close),
               CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
               CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
              };
       
   long signal = catboost.predict(x).cls; //Predicted class

   MqlTick ticks;
   if (!SymbolInfoTick(Symbol(), ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);   
   
   if (signal == 1) 
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0)
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
    
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }

共通フォルダに保存されたONNX形式のCatboostモデルを初期化した後。

OnTick関数内では、直近でクローズしたバーのOHLCの値を取得し、それらをCTALibの関数に渡してローソク足パターンを検出します。その結果を使用して、xというベクトルに特徴量を構築し、予測をおこないます。 

このとき重要なのは、特徴量の内容と順序を、Pythonスクリプトで最終的にCatBoostモデルを学習させたときとまったく同じにする必要があるという点です。

X_train.columns

最終モデルでは、次のようになりました。

Index(['White Candle', 'Black Candle', 'Doji Candle', 'Dragonflydoji Candle',
       'Gravestonedoji Candle', 'Hammer Candle', 'Invertedhammer Candle',
       'Spinningtop Candle', 'BullishMarubozu Candle',
       'BearishMarubozu Candle'],
      dtype='object')

この順序はEA中で正しく保持されています。

現在のところ、ストップロスやテイクプロフィットの値は設定されていないため、オープンしているポジションは、指定したlookahead値の本数分のバーが経過した時点でクローズされるようにしています。

以下はテスターの構成です。

以下は結果です。

 

私が興味深く感じたのは、買い取引と売り取引の結果が非常に似通っていたことです。この2年間で成立した売り取引は257件で、買い取引の259件と比べてわずか2件少ないだけでした。

これは適切ではなく、モデルがすべてのローソク足パターンを考慮しているとはいえ、最も影響を与えているのは「陽線」と「陰線」であると考えられます。これらのローソク足はすべてのバーに現れるため、モデルがそれらに過剰に依存していると見られます。さらに、私たちは取引を1本先(lookahead値 = 1)でクローズする設定にしており、この問題の原因は目的変数の作り方にさかのぼることができます。訓練データには、多くの特徴量に0(False)が含まれていたためです。 

本来重視したい特別なローソク足パターンが尊重されるようにするためには、モデルによる検出結果において、すべての特別なローソク足パターン(ハンマー、十字線、丸坊主など)が0(未検出)である場合には、取引をおこなわないしように制御する必要があります。

陽線と陰線以外の、何らかの特別なローソク足パターンが検出されたときのみ取引をおこなう、という条件を設けます。

void OnTick()
  {
//---
   
   double open = iOpen(Symbol(), Period(), 1),
          high = iHigh(Symbol(), Period(), 1),
          low  = iLow(Symbol(), Period(), 1), 
          close = iClose(Symbol(), Period(), 1);
   
   
   vector x = {
               CTALib::CDLWHITECANDLE(open, close),
               CTALib::CDLBLACKCANDLE(open, close),
               CTALib::CDLDOJI(open, close),
               CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
               CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
               CTALib::CDLHAMMER(open, high, low, close),
               CTALib::CDLINVERTEDHAMMER(open, high, low, close),
               CTALib::CDLSPINNINGTOP(open, high, low, close),
               CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
               CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
              };
   
   vector patterns = {
                        CTALib::CDLDOJI(open, close),
                        CTALib::CDLDRAGONFLYDOJI(open, high, low, close),
                        CTALib::CDLGRAVESTONEDOJI(open, high, low, close),
                        CTALib::CDLHAMMER(open, high, low, close),
                        CTALib::CDLINVERTEDHAMMER(open, high, low, close),
                        CTALib::CDLSPINNINGTOP(open, high, low, close),
                        CTALib::CDLBULLISHMARUBOZU(open, high, low, close),
                        CTALib::CDLBEARISHMARUBOZU(open, high, low, close)
                     }; //Store all the special patterns 
    
   long signal = catboost.predict(x).cls; //Predicted class

   MqlTick ticks;
   if (!SymbolInfoTick(Symbol(), ticks))
      {
         printf("Failed to obtain ticks information, Error = %d",GetLastError());
         return;
      }
      
   double volume_ = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);   
   
   if (signal == 1 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_BUY) && !PosExists(POSITION_TYPE_SELL))  
            m_trade.Buy(volume_, Symbol(), ticks.ask,0,0);
     }
     
   if (signal == 0 && patterns.Sum()>0) //Check if there are is atleast a special pattern before opening a trade
     {        
        if (!PosExists(POSITION_TYPE_SELL) && !PosExists(POSITION_TYPE_BUY))  
            m_trade.Sell(volume_, Symbol(), ticks.bid,0,0);
     } 
    
    CloseTradeAfterTime((Timeframe2Minutes(Period())*lookahead)*60); //Close the trade after a certain lookahead and according the the trained timeframe
  }

以下はテスターの結果です。

現在の結果は以前よりもずっと良く見えます。これは、上位の時間足においてローソク足パターンの出現が少ないことを反映しており、日足でモデルを学習させたこととも一致しています。

利益の出た取引の割合は54.55%で、これは分類レポートで得られた全体の正解率53%に非常に近い数値です。この一致は、私たちのアプローチが正しい方向に進んでいることを示しています。


結論

ローソク足パターンを人工知能(AI)モデルと組み合わせて活用し、市場の予測に応用することは十分に可能です。しかし、インジケーターや数学的な計算といった一般的なデータとは異なり、ローソク足パターンには非常に多くの注意点と繊細な配慮が必要です。市場で観測されるローソク足から特徴量を抽出する際、わずかな解釈の違いが結果に大きく影響することがあります

「人は自分が見たいものを見る」とも言われるように、私たちの願望や信念が情報の解釈に影響を与えることがあります。

これはローソク足パターンを扱うときによく起こることだと私は思います。つまり、ハンマーを探していると、トンボがハンマーに見えることもある、その逆もまた然りです。

このように、ローソク足に基づいたデータを用いる場合には、最適なパフォーマンスを得るまでに多くの試行錯誤が必要だと感じています。

ご一読、誠にありがとうございました。 

今後の更新にもご注目ください。こちらのGitHubリポジトリで、MQL5言語向けの機械学習アルゴリズムの開発にぜひ貢献してください。


情報源と参考文献


添付ファイルの表

ファイル名 説明/用途
Experts\CandlestickPatterns AI-EA.mq5 ローソク足パターンに基づいて予測をおこなうcatboostモデルを導入するEA
Indicators\Candlestick Identifier.mq5 チャート上にローソク足パターンを表示するためのインジケーター
Scripts\Candlestick Patterns Collect.mq5 ローソク足パターンを収集し、この情報をCSVファイルに保存するためのスクリプト
Include\Catboost.mqh 市場での予測をおこなうためのcatboost分類器を読み込み、初期化し、デプロイするためのクラスを含むライブラリ
Include\pandas.mqh データの保存と操作のためのPythonのようなPandasモジュール
Include\ta-lib.mqh ローソク足パターンを検出するためのクラスを含むテクニカル分析ライブラリ
Common\Files\*.csv 機械学習で使用するためのローソク足データを含むCSVファイル
Common\Files\*.onnx ONNX形式の機械学習モデル
CandlestickMarket Prediction.ipynb CatboostモデルをトレーニングするためのPythonスクリプト(Jupyter noteboot)


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

添付されたファイル |
Attachments.zip (183.92 KB)
最後のコメント | ディスカッションに移動 (5)
Rajesh Kumar Nait
Rajesh Kumar Nait | 29 4月 2025 において 09:24
素晴らしい記事だ。
Zhuo Kai Chen
Zhuo Kai Chen | 1 5月 2025 において 05:37
この試みは素晴らしい!ローソク足のパターンをMLに表示するという非常に興味深いアプローチ。
Stanislav Korotky
Stanislav Korotky | 1 5月 2025 において 10:17
基礎となるプライスアクションの代わりにローソク足の構成を使用することは、トゥルーカラー画像の代わりに256色のインデックス画像を使用することに似ています - あなたは重要なことができるニュアンスの多くを失う。もっと言えば、ブローカーの時間オフセットによって、ローソク足のパターンは 引用符の同じ履歴で完全に変更することが できます。また、大きなタイムフレーム(D1(あなたの場合)やH4など)を使用していない場合でも、1時間内の時間(分)の単純な人為的なシフトは、ローソク足の全く異なるフォーメーションを生成します。信頼性が低く、何の価値も生まない。
Omega J Msigwa
Omega J Msigwa | 1 5月 2025 において 13:08
Stanislav Korotky ローソク足のパターンは 完全に同じ履歴の引用符で変更することができます 。また、大きなタイムフレーム(D1(あなたの場合)やH4など)を使用していない場合でも、1時間内の時間(分)の単純な人為的なシフトは、ローソク足の全く異なるフォーメーションを生成します。信頼性が低く、何の価値も生まない。

このことはすでに記事で説明した。

現在、私たちはローソク足のパターンとそのシグナルをその出現に基づいて検出することだけを考えていますが、私の情報源によれば、ローソク足からシグナルを抽出する正しい方法は、トレンド検出を含まなければなりません。

このプロジェクトをさらに進めたいのであれば、トレンドは重要な要素です。

プライスアクションが重要であることは否定しません。

Stanislav Korotky
Stanislav Korotky | 1 5月 2025 において 13:35
Omega J Msigwa #:

このことはすでに記事で説明した。

しかし、私の情報源によれば、ローソク足からシグナルを抽出する正しい方法は、トレンドの検出を含まなければならない。

トレンドは方程式の重要な部分であり、このプロジェクトをさらに進めたいのであれば、考慮すべきかもしれない。

プライスアクションが重要であることは否定しません。

あなたの引用は、私の指摘の主旨とは関係ありません。

MQL5での取引戦略の自動化(第16回):ミッドナイトレンジブレイクアウト+Break of Structure (BoS)のプライスアクション MQL5での取引戦略の自動化(第16回):ミッドナイトレンジブレイクアウト+Break of Structure (BoS)のプライスアクション
本記事では、MQL5を用いて「ミッドナイトレンジブレイクアウト + Break of Structure (BoS)」戦略を自動化し、ブレイクアウトの検出および取引実行のコードを詳細に解説します。エントリー、ストップ、利益確定のためのリスクパラメータを正確に定義し、実際の取引に活用できるようバックテストおよび最適化もおこないます。
MQL5での取引戦略の自動化(第15回):プライスアクションハーモニックCypherパターンの可視化 MQL5での取引戦略の自動化(第15回):プライスアクションハーモニックCypherパターンの可視化
この記事では、CypherハーモニックパターンのMQL5における自動化について探究し、その検出方法とMetaTrader 5チャート上での可視化を詳しく解説します。スイングポイントを特定し、フィボナッチに基づいたパターンを検証し、明確な視覚的注釈とともに取引を実行するエキスパートアドバイザー(EA)を実装します。記事の最後では、効果的な取引のためのバックテストおよび最適化方法についても説明します。
取引チャート上で双三次補間を用いたリソース駆動型画像スケーリングによる動的MQL5グラフィカルインターフェイスの作成 取引チャート上で双三次補間を用いたリソース駆動型画像スケーリングによる動的MQL5グラフィカルインターフェイスの作成
本記事では、取引チャート上で高品質な画像スケーリングを実現するために、双三次補間(バイキュービック補間)を使用した動的なMQL5グラフィカルインターフェイスについて解説します。カスタムオフセットによる動的な中央配置やコーナーアンカーなど、柔軟なポジショニングオプションも紹介します。
知っておくべきMQL5ウィザードのテクニック(第60回):移動平均とストキャスティクスパターンを用いた推論(ワッサースタインVAE) 知っておくべきMQL5ウィザードのテクニック(第60回):移動平均とストキャスティクスパターンを用いた推論(ワッサースタインVAE)
MA(移動平均)とストキャスティクスの補完的な組み合わせに着目し、教師あり学習および強化学習を経た後の段階において、推論が果たしうる役割を検証します。推論にはさまざまなアプローチが存在しますが、この記事では変分オートエンコーダ(VAE: Variational Auto-Encoder)を用いる方法を採用します。まずはPythonでこのアプローチを探求し、その後、訓練済みモデルをONNX形式でエクスポートし、MetaTraderのウィザードで構築したエキスパートアドバイザー(EA)で活用します。