見出し画像

Python連携で分布推定精度を高めたFX自動売買ツール(MLD-EA)の開発

前回の記事では、グリッドサーチによって確率分布のパラメータを推定し取引戦略を決定するEAであるGSD-EAのロジックやバックテスト、ソースコードをご紹介しました。

今回は、さらに精度の高いパラメータ推定を目指し、最尤法(MLE: Maximum Likelihood Estimation)を活用した新しいEA「MLD-EA(Maximum Likelihood Distribution-EA)」を開発します。最尤法は、観測データから直接最適なパラメータを計算する手法で、未知の市場や予測が難しい環境でも柔軟に対応可能です。この記事では、その実装手順とソースコードについて詳しく紹介します。


最尤法とグリッドサーチの違い

グリッドサーチは計算がシンプルで使いやすい一方、候補のパラメータがあらかじめ決まっているため、精度に限界があります。一方、最尤法は観測データに基づき最適なパラメータを直接推定するため、より精密な結果が得られる可能性があります。

グリッドサーチの特徴

  • 手法: 事前に定めたパラメータ範囲を細かく試行錯誤し、最適なパラメータを見つける方法。

  • メリット:

    • 実装が簡単で直感的。

    • パラメータ範囲を適切に設定すれば、比較的良好な結果を得られる可能性が高い。

  • デメリット:

    • パラメータ範囲や分割の粒度次第で計算コストが高くなる。

    • 設定したパラメータ範囲に適切な解が含まれていなければ、最適解を見逃す可能性がある。

最尤法の特徴

  • 手法: 観測データに基づき、確率モデル内で最も尤度が高くなるパラメータを計算する方法。パラメータ範囲を事前に指定する必要はなく、数値的に最適化を進める。

  • メリット:

    • パラメータ範囲の事前設定が不要で、柔軟かつ精密な推定が可能。

    • 高次元のパラメータ空間や複雑なモデルに対しても適用できる。

  • デメリット:

    • 実装が比較的複雑で、数値最適化アルゴリズムの知識が必要。

    • Pythonや外部ツールとの連携が必要になる場合が多く、計算コストが増えることがある。

損益シミュレーション結果比較

損益シミュレーション結果

上記の損益シミュレーション結果では、最終的にグリッドサーチの方が累計損益が上回っていますが、それまでの間は安定して最尤法の方が損益が上回っています。

グリッドサーチの結果が良好となった理由の一つとして、GOLDの価格変動の特性や適切なパラメータ範囲を事前にある程度把握し、それを基にグリッドサーチのパラメータ範囲設定を行ったことが挙げられます。つまり、あらかじめ市場特性に基づいて適切な範囲を選定しないと良好な結果が得られない可能性があります。

一方で、より汎用的な手法としては最尤法が有用です。最尤法は、事前にパラメータ範囲を設定する必要がなく、観測データに基づいて直接的に最適なパラメータを計算できるため、未知の市場や予測の難しい環境においても柔軟に対応できます。特に、多くの資産や異なる市場での活用を想定する場合、最尤法の方が使い勝手が良く、より幅広い応用が可能でしょう。

MQL5とPythonの連携

今回のMLD-EAでは、MQL5とPythonを連携させる必要があります。

なぜPythonを使うのか?

MQL5単体では、最尤法を効率的に実装するライブラリが不足しています。一方、Pythonにはscipy.statsやnumpyなどの高性能なライブラリが揃っており、最尤法を簡単に実現できます。

MQL5とPythonの連携方法

今回はMQL5とPythonの特徴を活用して、以下のように連携させます。

  • Pythonスクリプトでt分布のパラメータを推定。

  • MQL5からPythonスクリプトを呼び出し、推定結果をEAに反映。

  • 推定したパラメータを基にエントリー、利確、損切りポイントを計算。

具体的な連携フローを図示すると以下のようになります。

MQL5とPythonの連携

本記事では、これらの具体的な処理をコードも含めて詳しく解説していきます。

MQL5のメイン処理

今回のMLD-EAにおけるMQL5側のコードについて順番に解説します。

Input(グローバル変数)

MQL5内のグローバル変数として以下を定義します。
EAのユーザーが設定可能な初期設定です。

//--- 入力パラメータ
input double lot_size      = 0.01;    // ロット数
input ulong  slippage      = 10;      // スリッページ
input double spread_limit  = 30;     // 許容スプレッド(point)
input int    magic_number  = 10001;   // マジックナンバー
input int    trade_mode    = 1;       // 1:buy, 2:sell, 3:buy&sell

ロット数
売買注文を行う際のロット数です。ロット数を変動させることは想定していません。

スリッページ
EAが発注したレートと、実際にFX会社側が約定するレートに乖離が生じること(またはその価格差)です。許容スリッページはFX会社によって異なり、設定不可の場合はスリッページの指定ができませんので、この値を設定しても意味がないこともあります。

許容スプレッド(point)
このMLD-EAでは、スプレッドが拡大している時には余計な処理を行わないような対処をしています。
取引通貨ペアによって平常時の平均スプレッド等が変わってくると思います。それに合わせて設定します。

マジックナンバー
注文がどのEA・ロジックから実行されたかを識別するための番号です。このML-EAでは、複数のマジックナンバーを使い分けたりしませんので、任意の値で問題ありません。

取引モード(trade_mode)
このMLD-EAでは、t分布に従った取引戦略を実装していますが、buy注文、sell注文、どちらの方向の売買戦略にも対応できるようにしています。
trade_mode=1であればbuy戦略のみ、trade_mode=2であればsell戦略のみ、trade_mode=3であれば、buy戦略とsell戦略のどちらも稼働する設定になっています。

起動時の処理

//+------------------------------------------------------------------+
//| 起動時の処理
//+------------------------------------------------------------------+
void OnInit()
  {
// プロセス完了ファイルを作成
   CreateProcessDoneFile();

// エントリー時間の初期化
   g_ea.buy_en_time = TimeLocal();
   g_ea.sell_en_time = TimeLocal();

// ターゲット計算
   CheckTargets(g_ea);
   if(trade_mode != 2)
     {
      g_ea.buy_last_check = TimeLocal();
      buy_load_flg = true;
     }
   if(trade_mode != 1)
     {
      g_ea.sell_last_check = TimeLocal();
      sell_load_flg = true;
     }
  }
  • エントリー時間の初期化: 売買ポジションのエントリー(注文)時間を現在時刻で初期化します。

  • CheckTargets: ターゲット価格(利確価格、エントリー価格、ロスカット価格)を計算します。この関数の中で、Pythonスクリプトを呼び出すような処理が入っています。詳細は後述します。

ティック毎の処理

void OnTick()
  {

// 最新のティック情報
   MqlTick last_tick;
   if(!SymbolInfoTick(_Symbol, last_tick))
     {
      Print(__FUNCTION__, ": Failed to get tick data. err=", GetLastError());
      return;
     }
   double Ask = last_tick.ask;
   double Bid = last_tick.bid;
   double spread = MathRound((Ask - Bid) / Point()) * Point();
  • AskとBid: 現在の買い・売り価格を取得します。

  • spread: スプレッド(AskとBidの差)を計算します。

コメント表示

// コメント表示
   string message = " \n";

   message += "Ask: " + DoubleToString(Ask, 2)+"\n";
   message += "Bid: " + DoubleToString(Bid, 2)+"\n";
   message += "Spread: " + DoubleToString(spread, 2)+"\n";
   message +=  "\n";

   if(trade_mode != 2)
     {
      message +=  "buy_last_check: \n " + TimeToString(g_ea.buy_last_check, TIME_DATE|TIME_MINUTES)+"\n \n";

      message += " buy_tp: " + DoubleToString(g_ea.buy_tp_price, 2)+"\n";
      message += " buy_en: " + DoubleToString(g_ea.buy_en_price, 2)+"\n";
      message += " buy_sl: " + DoubleToString(g_ea.buy_sl_price, 2)+"\n";
      message +=  "\n";
     }

   if(trade_mode != 1)
     {
      message +=  "sell_last_check: \n " + TimeToString(g_ea.sell_last_check, TIME_DATE|TIME_MINUTES)+"\n \n";

      message += " sell_tp: " + DoubleToString(g_ea.sell_tp_price, 2)+"\n";
      message += " sell_en: " + DoubleToString(g_ea.sell_en_price, 2)+"\n";
      message += " sell_sl: " + DoubleToString(g_ea.sell_sl_price, 2)+"\n";
      message +=  "\n";
     }

   Comment(message);

MT5のチャート画面で、以下のようなコメントを表示するためのコードです。

コメント表示

現在値とターゲット価格をわかりやすくするためのもので、ロジックに直接影響するものではありません。

スキップ処理

// スプレッドが許容スプレッドを超える場合は以降の処理をスキップ
   if(spread > spread_limit * Point())
      return;

// process_done.txtがない場合は以降の処理をスキップ
   if(!CheckProcessDoneFile())
      return;

このコードでは、一定の条件を満たさない場合に処理をスキップする仕組みを実装しています。これにより、不適切な状況で無駄な計算や取引を防ぎ、EAの動作を安定させます。

スプレッドが許容範囲を超える場合のスキップ
スプレッドが設定した上限(spread_limit)を超える場合は、以降の処理を行いません。

プロセス完了ファイルの確認
process_done.txt」というのは、Pythonの実行処理が完成した際に作成されるファイルです。外部処理が完了していない状態でEAが動作しないようにすることで、MQL5とPythonが適切に連携できるように制御します。

ターゲット価格の取得

// ターゲット取得
   GetBuyTargets(g_ea);
   GetSellTargets(g_ea);

Pythonで計算したターゲット価格の結果を外部から取得してEAの内部で管理するための処理です。これにより、最新のターゲット価格に基づいた取引戦略を実行できるようにします。

関数の詳細は後述しますが、ここでは結果的に以下の3つの価格を取得します。(buy注文の場合の例)

  • buy_tp_price: 利確価格(Take Profit Price)。

  • buy_en_price: エントリー価格(Entry Price)。

  • buy_sl_price: ロスカット価格(Stop Loss Price)。

ポジションの確認

// 前回のポジション数を退避しておく
   g_ea.pre_buy_position = g_ea.buy_position;
   g_ea.pre_sell_position = g_ea.sell_position;

// 現在のポジション状況をチェック
   CheckPositions(g_ea);

ここでは保有ポジションの確認を行います。常にポジションの状態を確認して、ポジションが利確や損切された場合も検知できるようにします。

利確や損切によるクローズ確認

// 前回はbuyポジションがあったのに、今は0になった => TP/SLなどでクローズされた
   if(g_ea.pre_buy_position > 0 && g_ea.buy_position == 0 && (TimeLocal() - g_ea.buy_en_time) < CHECK_INTERVAL_SEC && trade_mode != 2)
     {
      Print("[BUY TP/SL Close] bid=", Bid, " tp=", g_ea.buy_tp_price,
            " sl=", g_ea.buy_sl_price);
      CheckTargets(g_ea);
      g_ea.buy_last_check = TimeLocal();
      buy_load_flg = true;
     }

// 前回はsellポジションがあったのに、今は0になった => TP/SLなどでクローズされた
   if(g_ea.pre_sell_position > 0 && g_ea.sell_position == 0 && (TimeLocal() - g_ea.sell_en_time) < CHECK_INTERVAL_SEC && trade_mode != 1)
     {
      Print("[SELL TP/SL Close] ask=", Ask, " tp=", g_ea.sell_tp_price,
            " sl=", g_ea.sell_sl_price);
      CheckTargets(g_ea);
      g_ea.sell_last_check = TimeLocal();
      sell_load_flg = true;
     }

このコードは、買い(buy)または売り(sell)のポジションが利確(Take Profit, TP)や損切(Stop Loss, SL)によって自動的に決済されたことを確認し、その後の処理を行う部分です。
以下、buyポジションを例に解説します。

1. 利確や損切によるbuyポジションのクローズ確認

  • 前回確認時にbuyポジションが存在していた。

  • 現在、buyポジションがなくなった。

  • ポジションがエントリーされてからまだ保有時間が経過していない。

この条件が満たされると、「buyポジションが利確または損切の基準価格に到達したことによってクローズされた」と判断します。

2. クローズ後の処理

  • 新しいターゲット価格(利確価格、エントリー価格、ロスカット価格)を再計算し、次の取引に進みます。

ターゲット価格の再計算

// 一定間隔経過 & ポジションが無いので再分析
   if(g_ea.buy_position == 0 && (TimeLocal() - g_ea.buy_last_check >= CHECK_INTERVAL_SEC) && trade_mode != 2)
     {
      CheckTargets(g_ea);
      g_ea.buy_last_check = TimeLocal();
      buy_load_flg = true;
     }

   if(g_ea.sell_position == 0 && (TimeLocal() - g_ea.sell_last_check >= CHECK_INTERVAL_SEC) && trade_mode != 1)
     {
      CheckTargets(g_ea);
      g_ea.sell_last_check = TimeLocal();
      sell_load_flg = true;
     }

4時間経過してもエントリーされなかった場合、改めて新しい取引戦略を実施するために、ターゲット価格(利確価格、エントリー価格、ロスカット価格)を再計算します。

1. 処理の実施条件

  • ポジションが無い場合

  • 前回の確認時間から4時間経過した場合

2. その後の処理

  • 新しいターゲット価格(利確価格、エントリー価格、ロスカット価格)を再計算し、次の取引に進みます。

エントリー条件判定

// エントリー条件判定
   g_ea.can_buy = false;
   if(Bid < g_ea.buy_en_price && trade_mode != 2)
     {
      g_ea.can_buy = true;
     }

   g_ea.can_sell = false;
   if(Bid > g_ea.sell_en_price && trade_mode != 1)
     {
      g_ea.can_sell = true;
     }

エントリー注文を行う条件を満たしていているかを判定しています。

  • buyエントリーの条件

    • 現在の価格(Bid値)がエントリー基準(buy_en_price)を下回っているか確認します。

    • 条件を満たした場合、buyエントリー判定をtrueに設定します。

  • sellエントリーの条件

    • 現在の価格(Bid値)がエントリー基準(sell_en_price)を上回っているか確認します。

    • 条件を満たした場合、sellエントリー判定をtrueに設定します。

buyでもsellでもBid値で判定しているのは、分布の推定からターゲット価格計算まで、全ての計算をBid値ベースで行なっているからです。

エントリー注文

// 新規buyエントリー
   if(g_ea.buy_position == 0 && g_ea.can_buy)
     {
      if(SendOrder(1, 0, g_ea.buy_sl_price, g_ea.buy_tp_price))
        {
         g_ea.buy_en_time   = TimeLocal();
         Print("[BUY Entry] ask=", Ask, " tp=", g_ea.buy_tp_price,
               " sl=", g_ea.buy_sl_price);
        }
     }

// 新規sellエントリー
   if(g_ea.sell_position == 0 && g_ea.can_sell)
     {
      if(SendOrder(2, 0, g_ea.sell_sl_price, g_ea.sell_tp_price))
        {
         g_ea.sell_en_time   = TimeLocal();
         Print("[SELL Entry] bid=", Bid, " tp=", g_ea.sell_tp_price,
               " sl=", g_ea.sell_sl_price);
        }
     }

条件を満たした場合に、エントリー注文を行う処理です。

buyエントリー条件

  • 現在buyポジションを持っていない

  • buyエントリー判定が満たされている

sellエントリー条件

  • 現在sellポジションを持っていない

  • sellエントリー判定が満たされている

以上の条件が整っていれば、実際にbuyエントリー注文やsellエントリー注文を行います。

時間経過によるクローズ処理

// エントリー後、さらに4時間経過した場合はポジションクローズ
   if(g_ea.buy_position > 0 && trade_mode != 2)
     {
      if((TimeLocal() - g_ea.buy_en_time) >= CHECK_INTERVAL_SEC)
        {
         if(CloseBuyPositions())
           {
            Print("[BUY Time Limit Close] bid=", Bid, " tp=", g_ea.buy_tp_price,
                  " sl=", g_ea.buy_sl_price);
            CheckTargets(g_ea);
            g_ea.buy_last_check = TimeLocal();
            buy_load_flg = true;
           }
        }
     }

   if(g_ea.sell_position > 0 && trade_mode != 1)
     {
      if((TimeLocal() - g_ea.sell_en_time) >= CHECK_INTERVAL_SEC)
        {
         if(CloseSellPositions())
           {
            Print("[SELL Time Limit Close] ask=", Ask, " tp=", g_ea.sell_tp_price,
                  " sl=", g_ea.sell_sl_price);
            CheckTargets(g_ea);
            g_ea.sell_last_check = TimeLocal();
            sell_load_flg = true;
           }
        }
     }

この部分では、取引ポジション(buyまたはsell)をエントリーしてから、一定の時間が経過した後に強制的にクローズする処理を実装しています。

buyポジションのクローズ条件

  • buyポジションを保有している

  • エントリー時刻から4時間以上経過している

sellポジションのクローズ条件

  • sellポジションを保有している

  • エントリー時刻から4時間以上経過している

MQL5とPythonの連携

ここからはMQL5がPythonと連携する仕組みについて解説します。
左側のMQL5と右側のPythonをつなぐ処理です。

MQL5とPythonの連携

まずは概要です。

MQL5からPythonへの連携

  • MQL5から取得した価格データをテキストファイル(close_prices.txt)で保存

  • MQL5からバッチファイル(get_target_prices.bat)を実行

  • バッチファイルがPythonスクリプト(get_target_prices.py)を実行

  • Pythonスクリプトは「close_prices.txt」をインプットとしてターゲット価格計算を行う

PythonからMQL5への連携

  • Pythonスクリプトの実行結果はテキストファイル(target_prices.txt)で保存

  • MQL5はテキストファイルを読み込むことで結果(target_prices)を取得

つまり、常にバッチファイルあるいはテキストファイルを介していて、MQL5とPythonで直接のやり取りはありません。

ShellExecuteW関数

MQL5からバッチファイル(.bat)を実行するために、ShellExecuteW関数を使用します。この関数はWindows APIの一部で、外部プログラムやドキュメントを開くために使用されます。
以下のようにMQL5内で"Shell32.dll"をインポートすることで使用可能です。

#import "Shell32.dll"
int ShellExecuteW(int hwnd, string lpOperation, string lpFile, string lpParameters, string lpDirectory, int nShowCmd);
#import

バッチファイルのパスを設定

// バッチファイルのパス
input string user_name = "ご自身のPC環境の[ユーザー名]"; // ユーザー名
string get_target_prices_bat = "C:\\Users\\"+user_name+"\\get_target_prices.bat";

MQL5からは、「get_target_prices.bat」というバッチファイルを介してPythonスクリプトを実行するので、そのパスを指定しておきます。ここでは”C:¥Users¥[ユーザー名]¥”の直下にファイルを置いている場合を想定していますが、任意の場所でも問題ありません。[ユーザー名]部分は、それぞれの環境に合わせてEAのインプットで指定する必要があります。

EAのインプット画面

バッチファイルの作成

バッチファイルの作成方法は、以下の内容をテキストファイルに記載した状態で.batという拡張子で保存するだけです。

get_target_prices.bat

@echo off
C:\Users\[ユーザー名]\anaconda3\python.exe C:\Users\[ユーザー名]\get_target_prices.py

ここでは、Python実行ファイル(python.exe)とPythonスクリプト(get_target_prices.py)のパスを同じラインに書いています。

この記載は、Anacondaを用いてPythonの環境設定を行なった場合の例です。
[ユーザー名]は、ご自身の環境のユーザー名に置き換える必要があります。
その他、各々の環境におけるPythonをインストールした際のパスを記載していただければ問題ないです。

get_target_prices.pyは、実際にそのファイルを保存している場所でOKです。ここでは”C:¥Users¥[ユーザー名]¥”の直下に置いている想定です。
[ユーザー名]は、ご自身の環境のユーザー名に置き換える必要があります。

Pythonスクリプトの作成

続いて、右側のPythonにおける処理です。

MQL5とPythonの連携

ターゲット価格の計算

「get_target_prices.py」というPythonスクリプトで以下の処理を実行します。

  • MQL5から出力された240本(4時間分)の1分足終値データから、t分布のパラメータを推定

  • 推定したt分布の分位点を基準としてターゲット価格を計算

  • 計算したターゲット価格をテキストファイル(target_prices.txt)で出力

最終的には、例えばbuy注文の場合は以下のようなターゲット価格を出力します。

t分布に従った取引戦略

例えば、推定されたt分布のパラメータが平均値 (μ): 2,000、自由度 (ν): 1.99、標準偏差 (σ): 0.000929だった場合は、上記のグラフのように、

  • エントリー価格(Entry Price):1996.49

  • 利確価格(Take Profit Price):1999.46

  • ロスカット価格(Stop Loss Price):1981.49

というターゲット価格が計算されます。

その他事前準備

Pythonスクリプト(get_target_prices.py)では、以下のPythonライブラリを使用します。

import pandas as pd
import numpy as np
from scipy.stats import t
from datetime import datetime

MLD-EAを実際に稼働させるためには、EAを稼働するPCあるいはVPSにおいて、各種ライブラリがインストールされている必要があります。

より詳しい事前準備については以下の記事も参考にしてみてください。

上記の記事では、初心者向けにAncondaをインストールしてPython環境を設定する方法もご紹介しています。

有料部分の内容

以上、MLD-EAで行う処理の流れを具体的に説明してきました。
有料部分では一連の処理を実行するための全コードを掲載した以下のファイルをダウンロード可能にしています。

  • MLD-EA_note.mq5(MQL5のメイン処理だけでなく全ての関数を掲載)

  • get_target_prices.bat(get_target_prices.pyを実行するためのバッチファイル)

  • get_target_prices.py(ターゲット価格を計算するためのPythonスクリプト)

上記のファイルを使えば、MT5用のEAをコンパイルしてFX自動売買ツールとしてそのまま利用することも可能です。

注意点

  • 当記事で掲載しているコードはPythonの環境設定含め、必要な準備が整っている上での実行を想定しています。環境設定に問題がある場合はご自身で解決していただかないと実際のプログラム実行まで辿り着けない可能性があります。

  • 記事執筆時点で稼働確認を行なっており、エラーが出ないことを確認しておりますが、その後の環境変化等で想定通りに稼働しない可能性はございます。動作保証等はいたしかねますのでご了承ください。

  • リアル口座にアクセスして取引を行うことも可能なコードになっておりますが、必ずデモ口座で事前に稼働確認をしていただくことを推奨いたします。

  • 当記事で解説しているロジック通りの動作を保証するものではございません。あくまでFX自動売買ツール開発のためのサンプルコードとしてご活用ください。

コード全文

ここからは、これまでに掲載していないコードについて掲載しつつ、簡単な解説を入れていきます。最後に、全てのコードを盛り込んだファイル一式をダウンロード可能にしていますので、解説が不要であればそちらにお進みください。

ここから先は

7,405字

¥ 5,000

期間限定!Amazon Payで支払うと抽選で
Amazonギフトカード5,000円分が当たる

よろしければ応援お願いします。いただいたチップは今後の記事の執筆に活用させていただきます。