見出し画像

【MQL4】サンプルEAのMoving Average.mq4をリファクタリングしてみよう

こんにちは。もるふぉと申します。外資系IT会社のプログラマです。
今まで、PHP, Ruby, Python, Java, C++, GoLang などを利用して自社、他社問わず様々な企業のソフトウェアを構築してきました。
EAは自分用のものを作成してきました。
エンジニアリングの観点から皆様のお力になれるのではないかと思いEAに関する情報発信していきます。

この記事の対象となる方は、

✅ MQL4初学者
✅ 他人にコードレビューをしてもらったことがない方
✅ よりリーダブルなMQL4コードを書きたい方

となっています。

サンプルEAのMoving Average.mq4を読みやすくしてみよう


以前の記事で読みやすいコードを細切れで紹介させていただきました。
今回はMT4をインストールした時にデフォルトでバンドルされているサンプルEAのMoving Average.mq4 はサンプルEAの割にはプログラム的にツッコミどころがいくつかあるので、これを教材としてリファクタリングしてみたいと思います。

※以前の記事はこちら↓

🗺️ リファクタリングの方針

  • アーリーリターンの導入:ネストを浅くし、条件を満たさない場合は早期にリターンすることで可読性を向上させる。

  • 不要なコメントの削除:不要なコメントを削除し、コードが簡潔になるようにする

  • ループと条件文の簡略化:ループ内の条件文を簡略化し、不要なネストを減らす。

とします。

📖 Moving Average.mq4全コード

//+------------------------------------------------------------------+
//|                                               Moving Average.mq4 |
//|                   Copyright 2005-2014, MetaQuotes Software Corp. |
//|                                              http://www.mql4.com |
//+------------------------------------------------------------------+
#property copyright   "2005-2014, MetaQuotes Software Corp."
#property link        "http://www.mql4.com"
#property description "Moving Average sample expert advisor"

#define MAGICMA  20131111
//--- Inputs
input double Lots          =0.1;
input double MaximumRisk   =0.02;
input double DecreaseFactor=3;
input int    MovingPeriod  =12;
input int    MovingShift   =6;
//+------------------------------------------------------------------+
//| Calculate open positions                                         |
//+------------------------------------------------------------------+
int CalculateCurrentOrders(string symbol)
  {
   int buys=0,sells=0;
//---
   for(int i=0;i<OrdersTotal();i++)
     {
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;
      if(OrderSymbol()==Symbol() && OrderMagicNumber()==MAGICMA)
        {
         if(OrderType()==OP_BUY)  buys++;
         if(OrderType()==OP_SELL) sells++;
        }
     }
//--- return orders volume
   if(buys>0) return(buys);
   else       return(-sells);
  }
//+------------------------------------------------------------------+
//| Calculate optimal lot size                                       |
//+------------------------------------------------------------------+
double LotsOptimized()
  {
   double lot=Lots;
   int    orders=HistoryTotal();     // history orders total
   int    losses=0;                  // number of losses orders without a break
//--- select lot size
   lot=NormalizeDouble(AccountFreeMargin()*MaximumRisk/1000.0,1);
//--- calcuulate number of losses orders without a break
   if(DecreaseFactor>0)
     {
      for(int i=orders-1;i>=0;i--)
        {
         if(OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)==false)
           {
            Print("Error in history!");
            break;
           }
         if(OrderSymbol()!=Symbol() || OrderType()>OP_SELL)
            continue;
         //---
         if(OrderProfit()>0) break;
         if(OrderProfit()<0) losses++;
        }
      if(losses>1)
         lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1);
     }
//--- return lot size
   if(lot<0.1) lot=0.1;
   return(lot);
  }
//+------------------------------------------------------------------+
//| Check for open order conditions                                  |
//+------------------------------------------------------------------+
void CheckForOpen()
  {
   double ma;
   int    res;
//--- go trading only for first tiks of new bar
   if(Volume[0]>1) return;
//--- get Moving Average 
   ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0);
//--- sell conditions
   if(Open[1]>ma && Close[1]<ma)
     {
      res=OrderSend(Symbol(),OP_SELL,LotsOptimized(),Bid,3,0,0,"",MAGICMA,0,Red);
      return;
     }
//--- buy conditions
   if(Open[1]<ma && Close[1]>ma)
     {
      res=OrderSend(Symbol(),OP_BUY,LotsOptimized(),Ask,3,0,0,"",MAGICMA,0,Blue);
      return;
     }
//---
  }
//+------------------------------------------------------------------+
//| Check for close order conditions                                 |
//+------------------------------------------------------------------+
void CheckForClose()
  {
   double ma;
//--- go trading only for first tiks of new bar
   if(Volume[0]>1) return;
//--- get Moving Average 
   ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0);
//---
   for(int i=0;i<OrdersTotal();i++)
     {
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;
      if(OrderMagicNumber()!=MAGICMA || OrderSymbol()!=Symbol()) continue;
      //--- check order type 
      if(OrderType()==OP_BUY)
        {
         if(Open[1]>ma && Close[1]<ma)
           {
            if(!OrderClose(OrderTicket(),OrderLots(),Bid,3,White))
               Print("OrderClose error ",GetLastError());
           }
         break;
        }
      if(OrderType()==OP_SELL)
        {
         if(Open[1]<ma && Close[1]>ma)
           {
            if(!OrderClose(OrderTicket(),OrderLots(),Ask,3,White))
               Print("OrderClose error ",GetLastError());
           }
         break;
        }
     }
//---
  }
//+------------------------------------------------------------------+
//| OnTick function                                                  |
//+------------------------------------------------------------------+
void OnTick()
  {
//--- check for history and trading
   if(Bars<100 || IsTradeAllowed()==false)
      return;
//--- calculate open orders by current symbol
   if(CalculateCurrentOrders(Symbol())==0) CheckForOpen();
   else                                    CheckForClose();
//---
  }
//+------------------------------------------------------------------+

それぞれ関数ごとにリファクタリングしていきましょう。

📚 int CalculateCurrentOrders(string symbol)

☑️ オリジナルコード

int CalculateCurrentOrders(string symbol)
  {
   int buys=0,sells=0;
//---
   for(int i=0;i<OrdersTotal();i++)
     {
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;
      if(OrderSymbol()==Symbol() && OrderMagicNumber()==MAGICMA)
        {
         if(OrderType()==OP_BUY)  buys++;
         if(OrderType()==OP_SELL) sells++;
        }
     }
//--- return orders volume
   if(buys>0) return(buys);
   else       return(-sells);
  }

✅ リファクタリングしたコード

int CalculateCurrentOrders(string symbol) {
    int buys = 0, sells = 0;
    for (int i = 0; i < OrdersTotal(); i++) {
        if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) break;
        if (OrderSymbol() != Symbol() || OrderMagicNumber() != MAGICMA) continue;
        if (OrderType() == OP_BUY)  buys++;
        if (OrderType() == OP_SELL) sells++;
    }
    return (buys > 0) ? buys : -sells;
}

アーリーリターンを使い不要なネストを削除しました。

📚 double LotsOptimized()

☑️ オリジナルコード

double LotsOptimized()
  {
   double lot=Lots;
   int    orders=HistoryTotal();     // history orders total
   int    losses=0;                  // number of losses orders without a break
//--- select lot size
   lot=NormalizeDouble(AccountFreeMargin()*MaximumRisk/1000.0,1);
//--- calcuulate number of losses orders without a break
   if(DecreaseFactor>0)
     {
      for(int i=orders-1;i>=0;i--)
        {
         if(OrderSelect(i,SELECT_BY_POS,MODE_HISTORY)==false)
           {
            Print("Error in history!");
            break;
           }
         if(OrderSymbol()!=Symbol() || OrderType()>OP_SELL)
            continue;
         //---
         if(OrderProfit()>0) break;
         if(OrderProfit()<0) losses++;
        }
      if(losses>1)
         lot=NormalizeDouble(lot-lot*losses/DecreaseFactor,1);
     }
//--- return lot size
   if(lot<0.1) lot=0.1;
   return(lot);
  }

✅ リファクタリングしたコード

double LotsOptimized() {
    double lot = NormalizeDouble(AccountFreeMargin() * MaximumRisk / 1000.0, 1);
    int losses = 0;
    
    if (DecreaseFactor > 0) {
        for (int i = HistoryTotal() - 1; i >= 0; i--) {
            if (!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) {
                Print("Error in history!");
                break;
            }
            if (OrderSymbol() != Symbol() || OrderType() > OP_SELL) continue;
            if (OrderProfit() > 0) break;
            if (OrderProfit() < 0) losses++;
        }
        if (losses > 1) lot = NormalizeDouble(lot - lot * losses / DecreaseFactor, 1);
    }
    return (lot < 0.1) ? 0.1 : lot;
}

不要な変数、ネストを削除しました。


📚 void CheckForOpen()

☑️ オリジナルコード

void CheckForOpen()
  {
   double ma;
   int    res;
//--- go trading only for first tiks of new bar
   if(Volume[0]>1) return;
//--- get Moving Average 
   ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0);
//--- sell conditions
   if(Open[1]>ma && Close[1]<ma)
     {
      res=OrderSend(Symbol(),OP_SELL,LotsOptimized(),Bid,3,0,0,"",MAGICMA,0,Red);
      return;
     }
//--- buy conditions
   if(Open[1]<ma && Close[1]>ma)
     {
      res=OrderSend(Symbol(),OP_BUY,LotsOptimized(),Ask,3,0,0,"",MAGICMA,0,Blue);
      return;
     }
//---
  }

✅ リファクタリングしたコード

void CheckForOpen() {
    if (Volume[0] > 1) return;
    double ma = iMA(NULL, 0, MovingPeriod, MovingShift, MODE_SMA, PRICE_CLOSE, 0);
    int res;
    if (Open[1] > ma && Close[1] < ma) {
        res = OrderSend(Symbol(), OP_SELL, LotsOptimized(), Bid, 3, 0, 0, "", MAGICMA, 0, Red);
        if (res < 0) {
            Print("OrderSend OP_SELL failed with error #", GetLastError());
        }
    } else if (Open[1] < ma && Close[1] > ma) {
        res = OrderSend(Symbol(), OP_BUY, LotsOptimized(), Ask, 3, 0, 0, "", MAGICMA, 0, Blue);
        if (res < 0) {
            Print("OrderSend OP_BUY failed with error #", GetLastError());
        }
    }
}

不要な変数の削除しました。エラーハンドリングしました。


📚 void CheckForClose()

☑️ オリジナルコード

void CheckForClose()
  {
   double ma;
//--- go trading only for first tiks of new bar
   if(Volume[0]>1) return;
//--- get Moving Average 
   ma=iMA(NULL,0,MovingPeriod,MovingShift,MODE_SMA,PRICE_CLOSE,0);
//---
   for(int i=0;i<OrdersTotal();i++)
     {
      if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)==false) break;
      if(OrderMagicNumber()!=MAGICMA || OrderSymbol()!=Symbol()) continue;
      //--- check order type 
      if(OrderType()==OP_BUY)
        {
         if(Open[1]>ma && Close[1]<ma)
           {
            if(!OrderClose(OrderTicket(),OrderLots(),Bid,3,White))
               Print("OrderClose error ",GetLastError());
           }
         break;
        }
      if(OrderType()==OP_SELL)
        {
         if(Open[1]<ma && Close[1]>ma)
           {
            if(!OrderClose(OrderTicket(),OrderLots(),Ask,3,White))
               Print("OrderClose error ",GetLastError());
           }
         break;
        }
     }
//---
  }

✅ リファクタリングしたコード

void CheckForClose() {
    if (Volume[0] > 1) return;
    double ma = iMA(NULL, 0, MovingPeriod, MovingShift, MODE_SMA, PRICE_CLOSE, 0);

    for (int i = 0; i < OrdersTotal(); i++) {
        if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) break;
        if (OrderMagicNumber() != MAGICMA || OrderSymbol() != Symbol()) continue;

        if (OrderType() == OP_BUY && Open[1] > ma && Close[1] < ma) {
            if (!OrderClose(OrderTicket(), OrderLots(), Bid, 3, White)) {
                Print("OrderClose error ", GetLastError());
            }
            break;
        } else if (OrderType() == OP_SELL && Open[1] < ma && Close[1] > ma) {
            if (!OrderClose(OrderTicket(), OrderLots(), Ask, 3, White)) {
                Print("OrderClose error ", GetLastError());
            }
            break;
        }
    }
}

不要な変数を削除、ネストを削除しました。

👨‍🔧 Moving Average.mq4のバグを修正する

ここまでEAの挙動を変えないよう修正してきました。
実はMoving Average.mq4にバグが存在します。

if (Volume[0] > 1) return;

CheckForClose関数CheckForOpen関数で見かけるというコードは時間足の始値で実行するという意図で書かれていると思います。
しかし、これが高頻度のティックによるスパイクやラグを裁けない場合があります。
MQL4の入門書にも頻出する書き方ですが誤りです。注意してください。

🔧 バグの修正方法

void OnTick() {
    static datetime lastBarTime = 0;

    if (Bars < 100 || !IsTradeAllowed()) return;

    datetime currentBarTime = iTime(Symbol(), 0, 0);
    if (currentBarTime == lastBarTime) return;
    lastBarTime = currentBarTime;

    if (CalculateCurrentOrders(Symbol()) == 0) {
        CheckForOpen();
    } else {
        CheckForClose();
    }
}
  1. static 変数の導入:OnTick 関数内に static datetime lastBarTime を導入しました。これにより、lastBarTime の値は関数が呼び出されるたびに保持されます。

  2. バーの開始時間の確認:currentBarTime と lastBarTime を比較し、currentBarTime が lastBarTime と異なる場合のみ処理を続行します。

  3. CheckForClose関数CheckForOpen関数 からはif (Volume[0] > 1) return;を削除してください


🍜 まとめ

今回のリファクタリングでは、コードの可読性とメンテナンス性を向上させるためにいくつかの改善を行いました。
具体的には、ネストの削減、アーリーリターンの導入、変数のスコープの適切な制限、そしてエラーハンドリングの強化を実施しました。
これにより、コードの構造が明確になり、将来的な変更や拡張が容易になりました。
また、適切なエラーチェックを行うことで、運用時の予期せぬエラーを減少させ、システムの信頼性を高めることができました。
リファクタリングは単なるコードの整理ではなく、品質向上のための重要なステップです。

🍖おまけ

リファクタリングしたコード全文を500円で提供します。
といっても文中にでてきたコードをつなぎ合わせれば出来上がりますので実質投げ銭になります🙇‍♂️


ここから先は

4,689字

¥ 500

期間限定 PayPay支払いすると抽選でお得に!

この記事が気に入ったらサポートをしてみませんか?