You're reading...
EA development, Money Management, MQL4

Drawdown Percent Close EA

This is a very simple trade management EA that will close all your Open Orders (or both Open and Pending Orders) when your drawdown against balance exceeds a set percentage limit.

E.g. Balance = $10,000 and “DrawdownPercent” is set at 2.0, if floating P&L is greater than -$200.00, all Orders will be closed.


//+------------------------------------------------------------------+
//|                                       Drawdown Percent Close.mq4 |
//|                         Copyright 2012-2018, www.raidenworks.com |
//|                                          contact@raidenworks.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2012-2018, www.raidenworks.com"
#property link      "contact@raidenworks.com"
#define  NL "\n"

extern string _1              = "Settings";
extern double DrawdownPercent = 2.0;     //e.g. for 2% drawdown
extern int    MaxSlippage     = 3;         //maximum pips slippage allowed (auto-adjusts for 4-digit or 5-digit brokers)
extern bool   ClosePendingToo = False;     //set to True to close Pending Orders too
extern string _2              = "Display";
extern int    TopPadding      = 0;         //to shift Display X number of lines down from topleft corner
extern int    LeftPadding     = 0;         //to shift Display X number of spaces right from topleft corner

int mt;

int init(){
   if(Digits==3||Digits==5)mt=10;
   else mt=1;
   return(0);
}

int start(){
   if((1-AccountEquity()/AccountBalance())*100>NormalizeDouble(DrawdownPercent,2)){
      if(ClosePendingToo){
         CloseOpenOrders();
         ClosePendingOrders();
      }
      else CloseOpenOrders();
   }
   Display();
   return(0);
}

void CloseOpenOrders(){
   bool result=false;
   for(int i=0;i<OrdersTotal();i++){
      OrderSelect(i,SELECT_BY_POS);
      if(OrderType()==OP_BUY||OrderType()==OP_SELL)result=OrderClose(OrderTicket(),OrderLots(),OrderClosePrice(),MaxSlippage*mt,Blue);
      if(!result)Print("CloseOpenOrders failed with error#",GetLastError());
   }
}

void ClosePendingOrders(){
   bool result=false;
   for(int i=0;i<OrdersTotal();i++){
      OrderSelect(i,SELECT_BY_POS);
      if(OrderType()==OP_BUYSTOP||OrderType()==OP_BUYLIMIT||OrderType()==OP_SELLSTOP||OrderType()==OP_SELLLIMIT)result=OrderDelete(OrderTicket());
      if(!result)Print("ClosePendingOrders failed with error#",GetLastError());
   }
}

void Display(){
   string TOP="";
   for(int line=0;line<TopPadding;line++)TOP=TOP+NL;
   string LEFT="";
   for(int space=0;space<LeftPadding;space++)LEFT=LEFT+" ";
   string A1=StringConcatenate(TOP,NL);
   string A2=StringConcatenate(LEFT,"Balance: $",DoubleToStr(AccountBalance(),2),NL);
   string A3=StringConcatenate(LEFT,"Equity: $",DoubleToStr(AccountEquity(),2),NL);
   string A4=StringConcatenate(LEFT,"DD%Close: ",DoubleToStr(DrawdownPercent,2),NL);
   string A5=StringConcatenate(LEFT,"DD%Current: ",DoubleToStr((1-AccountEquity()/AccountBalance())*100,2),NL);
   string Display=StringConcatenate(A1,A2,A3,A4,A5);
   Comment(Display);
   return (0);
}

A simple text display is included for some visual feedback.

Basic notes: It only needs to be attached to one chart to function. Ideally attach to the same currency pair as the one you trade on. If trading on multiple pairs, attach it to the one with the most frequent ticks.

Advanced notes: I’ve played around in the past with a constant regular refresh, i.e. regardless of whether ticks come in or not, the code still runs a pass every X milliseconds; however, too low a resolution and the EA will not capture a fast market properly, too high and it sucks memory and is redundant for quiet markets. Hence, I’ve not opted for it.

 


Update: 2021.06.25

Been meaning to update this for a while now as a potential issue is the way Positions are closed. We should always target to close the youngest Position first then go on to the next youngest, and all the way to the oldest.

That is, we should count down:

for(int i = OrdersTotal() - 1; i >= 0; i--)

Instead of count up:

for(int i = 0; i < OrdersTotal(); i++)

There’s a few good reasons why but I’m too lazy to explain.

Took the opportunity to write more robust code for multiple Position-closing and Pending Order-deletion attempts. It’s also optimised a little better and might actually be marginally faster.

Intentionally kept the display lean but added a bit more information. It shows the amount of Account Currency your set “Drawdown Trigger Percentage” is.

And obviously test it out on a demo account first to get an feel of it.

// +------------------------------------------------------------------+
// |                                       Drawdown Percent Close.mq4 |
// +------------------------------------------------------------------+
// MIT License
// Copyright © 2021 Michael Ng
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software
// and associated documentation files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

#property link        "www.raidenworks.com"
#property version     "21.06"
#property strict

//-------------------------------------------------------------------------------------------------

input string _1                          = "";    //_____Settings_____
input double e_d_DrawdownTrigger_Percent = 2.0;   //Drawdown Trigger Percentage
input int    e_i_MaxSlippage_Points      = 30;    //Maximum Slippage in Points
input bool   e_b_DeletePendingToo        = False; //Delete Pending Orders on trigger too
input string _2                          = "";    //_____Display_____
input int    e_i_TopPadding              = 0;     //Top Padding
input int    e_i_LeftPadding             = 0;     //Left Padding
 
//-------------------------------------------------------------------------------------------------

string gi_s_AccountCurrency = "";
double g_d_AccountEquity = 0.0;
double g_d_AccountBalance = 0.0;
double g_d_CurrentDrawdown = 0.0;
double g_d_CurrentDrawdown_Percent = 0.0;
double g_d_CurrentDrawdown_AccountCurrency = 0.0;

//-------------------------------------------------------------------------------------------------

int OnInit(){

   gi_s_AccountCurrency = AccountCurrency();

   return(INIT_SUCCEEDED);
}

//-------------------------------------------------------------------------------------------------

void OnTick(){

   g_d_AccountEquity = AccountEquity();
   g_d_AccountBalance = AccountBalance();
   g_d_CurrentDrawdown = g_d_AccountBalance - g_d_AccountEquity;
   g_d_CurrentDrawdown_Percent = (1 - g_d_AccountEquity / g_d_AccountBalance) * 100;
   g_d_CurrentDrawdown_AccountCurrency = AccountBalance() * e_d_DrawdownTrigger_Percent / 100;

   if(g_d_CurrentDrawdown_Percent > e_d_DrawdownTrigger_Percent){
      CloseAllPositions(e_i_MaxSlippage_Points);
      if(e_b_DeletePendingToo)DeleteAllPendingOrders();
   }

   Display();

}
 
//-------------------------------------------------------------------------------------------------

void Display(){

   string TOP = "";
   for(int line = 0; line < e_i_TopPadding; line++)TOP = TOP + "\n";
   string LEFT = "";
   for(int space = 0; space < e_i_LeftPadding; space++)LEFT = LEFT + " ";
   string A1 = StringConcatenate(TOP);
   string A2 = StringConcatenate(LEFT, "Balance: ",      DoubleToStr(g_d_AccountBalance, 2), " ", gi_s_AccountCurrency, "\n");
   string A3 = StringConcatenate(LEFT, "Equity: ",       DoubleToStr(g_d_AccountEquity, 2), " ", gi_s_AccountCurrency, "\n");
   string A4 = StringConcatenate(LEFT, "DD Trigger: ",   DoubleToStr(g_d_CurrentDrawdown_AccountCurrency, 2), " ", gi_s_AccountCurrency, "\n");
   string A5 = StringConcatenate(LEFT, "DD Current: ",   DoubleToStr(g_d_CurrentDrawdown, 2), " ", gi_s_AccountCurrency, "\n");
   string A6 = "\n";
   string A7 = StringConcatenate(LEFT, "DD Trigger %: ", DoubleToStr(e_d_DrawdownTrigger_Percent, 2), "\n");
   string A8 = StringConcatenate(LEFT, "DD Current %: ", DoubleToStr(g_d_CurrentDrawdown_Percent, 2));

   Comment(StringConcatenate(A1,A2,A3,A4,A5,A6,A7,A8));

}

//-------------------------------------------------------------------------------------------------

// Purpose: Return OrderTicket of YOUNGEST detected Position
// Usage:   CloseAllPositions()
// Outputs: function int OrderTicket()

int TicketOfYoungestPosition(){

   for(int i = OrdersTotal() - 1; i >= 0; i--){
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))return(0);
      if(OrderType() < 2)return(OrderTicket()); //OP_BUY = 0, OP_SELL = 1
   }

   return(0);
}

//-------------------------------------------------------------------------------------------------

// Purpose: Return OrderTicket of YOUNGEST detected Pending Order
// Usage:   DeleteAllPendingOrders()
// Outputs: function int OrderTicket()

int TicketOfYoungestPendingOrder(){

   for(int i = OrdersTotal() - 1; i >= 0; i--){
      if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES))return(0);
      if(OrderType() > 1)return(OrderTicket()); //OP_BUY = 0, OP_SELL = 1, OP_BUYLIMIT = 2, OP_SELLLIMIT = 3, OP_BUYSTOP = 4, OP_SELLSTOP = 5
   }

   return(0);
}

//-------------------------------------------------------------------------------------------------

// Purpose: Close all Positions
// Usage:   OnTick()
// Inputs:  function int    TicketOfYoungestPosition()
// Outputs: bool
//          function bool   OrderClose()
//          function string ErrorDescription()
// NB:      'while' loop continues as long as TicketOfYoungestPosition() > 0, which itself has a loop to get the OrderTickets of existing Positions

bool CloseAllPositions(const int &p_i_MaxSlippage_Points){

   int l_i_OrderTicket = 0, l_i_attempts = 3;
   bool l_b_result = false;

   while(l_i_attempts > 0){

      l_i_OrderTicket = TicketOfYoungestPosition();

      if(l_i_OrderTicket > 0){

         l_b_result = OrderClose(l_i_OrderTicket, OrderLots(), OrderClosePrice(), p_i_MaxSlippage_Points, clrBlue); //we can call OrderLots(), OrderClosePrice() directly here as latest OrderSelect() is in TicketOfYoungestPosition() immediately above

         if(!l_b_result){

            Print(TimeCurrent(), " CloseAllPositions() failed to close Position #", l_i_OrderTicket, " : Error #", GetLastError());

            l_i_attempts--;

            Sleep(2000);
            continue; //check this order again //do not need to OrderSelect(l_i_OrderTicket, SELECT_BY_TICKET, MODE_TRADES) to refresh OrderClosePrice() as TicketOfYoungestPosition() in next iteration of loop already does it

         }
         continue; //l_b_result = true; check next order

      }
      return(true); //return(true) as no Positions left

   }

   Print(TimeCurrent(), " Reached maximum attempts to close Positions");

   return(false);
}

//-------------------------------------------------------------------------------------------------

// Purpose: Delete all Pending Orders
// Usage:   OnTick()
// Inputs:  function int    TicketOfYoungestPendingOrder()
// Outputs: bool
//          function bool   OrderDelete()
//          function string ErrorDescription()
// NB:      'while' loop continues as long as TicketOfYoungestPendingOrder() > 0, which itself has a loop to get the OrderTickets of existing Pending Orders

bool DeleteAllPendingOrders(){

   int l_i_OrderTicket = 0, l_i_attempts = 3;
   bool l_b_result = false;

   while(l_i_attempts > 0){

      l_i_OrderTicket = TicketOfYoungestPendingOrder();

      if(l_i_OrderTicket > 0){

         l_b_result = OrderDelete(l_i_OrderTicket);

         if(!l_b_result){

            Print(TimeCurrent(), " DeleteAllPendingOrders() failed to delete Pending Order #", l_i_OrderTicket, " : Error #", GetLastError());

            l_i_attempts--;

            Sleep(2000);
            continue; //check this order again

         }
         continue; //l_b_result = true; check next order

      }
      return(true); //return(true) as no Pending Orders left

   }

   Print(TimeCurrent(), " Reached maximum attempts to close Pending Orders");

   return(false);
}

Discussion

3 thoughts on “Drawdown Percent Close EA

  1. Thanks for making this available, it’s very helpful!

    Posted by Ben Anderson | 2017-01-27, 6:22 AM
  2. This is really great. Thanks for sharing. Would it be possible for you to add the functionality to disable Auto Trading when the trigger is hit and also to generate push notification>

    Posted by Bas | 2021-07-11, 3:23 PM
    • For notifications, you can put a SendNotification() (https://docs.mql4.com/common/sendnotification) immediately after the Position-closing logic within the main OnTick() function.

      e.g.

      if(g_d_CurrentDrawdown_Percent > e_d_DrawdownTrigger_Percent){
      CloseAllPositions(e_i_MaxSlippage_Points);
      if(e_b_DeletePendingToo)DeleteAllPendingOrders();
      SendNotification(TimeToStr(TimeCurrent(), TIME_DATE|TIME_SECONDS) + ” Drawdown Percent Close triggered!”)
      }

      This notification gets sent to your mobile MT4 app and you’ll need to configure both applications accordingly.
      (https://www.metatrader4.com/en/trading-platform/help/setup/settings_notifications)

      For disabling Auto Trading, you can technically use the keybd_event() function of user32.dll to unpress the “AutoTrading” button, however, it is not advisable to use another EA to manage another EA’s logic.
      Each EA runs asynchronously (ever since MT4 build > ~640), as such you might have one EA constantly closing the freshly opened position of another EA, causing you to lose the spread every time this occurs. Which can be every tick!)

      Posted by Michael | 2021-07-11, 5:47 PM

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Archives

Visitors

Flag Counter
%d bloggers like this: