在 MQL5 中创建每日回撤限制器 EA·进阶篇
🛑

在 MQL5 中创建每日回撤限制器 EA·进阶篇

(2/3)· 从回撤逻辑到 MQL5 信号控制,手把手把风控规则写进 EA 内部

新手友好 第 2/3 篇
很多手动交易者直到被 prop 公司取消资格才意识到每日回撤早已越线。把规则写进 EA,让图表上的交通信号灯替你盯盘,比事后看报表实在得多。

◍ 用随机入场和标签函数搭一个裸EA骨架

这段片段展示了一个极简 EA 的核心循环:先取当前 BID 价并归一化到品种小数位,再用 iBars 拿图表 K 线总数,若总数没变就直接 return,避免每 tick 重复跑逻辑。仓位判断也很粗暴——PositionsTotal() 大于 1 就退出,也就是说同时间最多只留 1 个持仓。 随机下单部分用 MathRand()%2 取 0 或 1,等概率做多或做空,各 0.1 手,止损止盈都挂在离场价 70 点之外。这种结构本身不预测方向,只是用来验证执行通道和滑点表现,外汇与贵金属杠杆高,实盘跑这类随机单可能很快吃掉本金。 createText 是一个画左上角文字标签的辅助函数:ObjectCreate 建 OBJ_LABEL,再依次设 X/Y 像素偏移、锚点左上、文本内容、颜色和字号,最后 ChartRedraw 刷新。若创建失败会打印错误码并返回 false,方便你排查图表对象权限问题。 checkDailyProfit 则按自然日切分统计:用 TimeToString 取当前日期字符串再转回时间戳作为当日起点,HistorySelect 拉成交历史,遍历 DEAL_ENTRY_OUT 的平仓单把 profit 和 commission 累加。dayTime 跨过当日终点时重置 dayBalance,相当于给每日盈亏画一条基准线。

MQL5 / C++
class="type">class="kw">double bid = NormalizeDouble(SymbolInfoDouble(_Symbol,SYMBOL_BID),_Digits);
class="type">int bars = iBars(_Symbol,_Period);
if (totalBars == bars) class="kw">return;
totalBars = bars;

if (PositionsTotal() > class="num">1) class="kw">return;
class="type">int number = MathRand()%
if (number == class="num">0){
    obj_Trade.Buy(class="num">0.1,_Symbol,ask,ask-class="num">70*_Point,ask+class="num">70*_Point);
}
else if (number == class="num">1){
    obj_Trade.Sell(class="num">0.1,_Symbol,bid,bid+class="num">70*_Point,bid-class="num">70*_Point);
}

class="type">bool createText(class="type">class="kw">string objName,class="type">class="kw">string text,class="type">int x, class="type">int y,class="type">class="kw">color clrTxt,class="type">int fontSize){
    ResetLastError();
    if (!ObjectCreate(class="num">0,objName,OBJ_LABEL,class="num">0,class="num">0,class="num">0)){
        Print(__FUNCTION__,": failed to create the Label! Error Code = ",GetLastError());
        class="kw">return (false);
    }
    ObjectSetInteger(class="num">0,objName,OBJPROP_XDISTANCE,x);
    ObjectSetInteger(class="num">0,objName,OBJPROP_YDISTANCE,y);
    ObjectSetInteger(class="num">0,objName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
    ObjectSetString(class="num">0,objName,OBJPROP_TEXT,text);
    ObjectSetInteger(class="num">0,objName,OBJPROP_COLOR,clrTxt);
    ObjectSetInteger(class="num">0,objName,OBJPROP_FONTSIZE,fontSize);

    ChartRedraw(class="num">0);
    class="kw">return (true);
}
class="type">void checkDailyProfit(){
    class="type">class="kw">double total_day_Profit = class="num">0;
    class="type">class="kw">datetime end = TimeCurrent();
    class="type">class="kw">string sdate = TimeToString(TimeCurrent(),TIME_DATE);
    class="type">class="kw">datetime start = StringToTime(sdate);
    class="type">class="kw">datetime to = start + (class="num">1*class="num">24*class="num">60*class="num">60);

    if (dayTime < to){
        dayTime = to;
        dayBalance = Acc_B();
    }

    HistorySelect(start,end);
    class="type">int TotalDeals = HistoryDealsTotal();
    for (class="type">int i=class="num">0; i<TotalDeals; i++){
        class="type">class="kw">ulong Ticket = HistoryDealGetTicket(i);
        if (HistoryDealGetInteger(Ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT){
            class="type">class="kw">double Latest_Day_Profit = (HistoryDealGetDouble(Ticket,DEAL_PROFIT)
                                        +HistoryDealGetDouble(Ticket,DEAL_COMMISSION)

「把当日盈亏算成百分比并画到图上」

这段逻辑的核心是先把当日所有平仓单的净盈亏(含库存费)累加进 total_day_Profit,再用账户余额减去它反推出起始余额 startingBalance,从而算出当天收益率。daily_profit_or_drawdown 用 NormalizeDouble 保留两位小数,乘以 100 再除以 startingBalance,得到的是带两位小数的百分比数值,比如某日净赚 50 美元、起始余额 5000,那结果就是 1.00。 画图部分靠 createText 把关键节点时间戳和权益变化标在图表固定坐标上:start/to/end 用黄白两色区分时段边界,Acc_E() 与 initialBalance 的大小比较决定用绿(盈利)、红(亏损)还是白(持平)显示权益和收益率。dayBalance 那一行还顺手标了 5% 的预警线(dayBalance*5/100 取负),方便肉眼看回撤是否越界。 外汇与贵金属杠杆高,这类当日收益率只是历史截面,后续行情可能反向,验证时请在 MT5 策略测试器或实盘模拟环境跑一遍,别直接套真金白银。

MQL5 / C++
      +HistoryDealGetDouble(Ticket,DEAL_SWAP));
      total_day_Profit += Latest_Day_Profit;
   }
  }
  class="type">class="kw">double startingBalance = class="num">0;
  startingBalance = AccountInfoDouble(ACCOUNT_BALANCE) - total_day_Profit;
  class="type">class="kw">double daily_profit_or_drawdown = NormalizeDouble((total_day_Profit*class="num">100/startingBalance),class="num">2);
  class="type">class="kw">string daily_profit_in_Text_Format = "";
  daily_profit_in_Text_Format = DoubleToString(daily_profit_or_drawdown,class="num">2)+" %";

  class=class="str">"cmt">//Print(total_day_Profit, " >>> ",daily_profit_in_Text_Format);

  createText("class="num">4",TimeToString(start),class="num">120,class="num">80,clrYellow,class="num">10);
  createText("class="num">6",TimeToString(to),class="num">120,class="num">95,clrYellow,class="num">10);
  createText("class="num">8",TimeToString(end),class="num">120,class="num">110,clrWhite,class="num">10);
  if (Acc_E() > initialBalance){
     createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrLime,class="num">10);
     createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrLime,class="num">10);
  }
  else if (Acc_E() < initialBalance){
     createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrRed,class="num">10);
     createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrRed,class="num">10);
  }
  if (Acc_E() == initialBalance){
     createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrWhite,class="num">10);
     createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrWhite,class="num">10);
  }

  createText("class="num">20",DoubleToString(dayBalance,class="num">2)+" "+Acc_S(),class="num">270,class="num">225,clrWhite,class="num">10);
  createText("class="num">24","-"+DoubleToString(dayBalance*class="num">5/class="num">100,class="num">2)+" "+Acc_S(),class="num">270,class="num">255,clrYellow,class="num">10);
  if (Acc_B() > dayBalance){
     createText("class="num">26",DoubleToString(total_day_Profit,class="num">2)+" "+Acc_S(),class="num">270,class="num">270,clrLime,class="num">10);
     createText("class="num">28",daily_profit_in_Text_Format,class="num">270,class="num">285,clrLime,class="num">10);
  }
  else if (Acc_B() < dayBalance){

用标签和硬阈值卡死当日交易权限

这段逻辑把「当日浮盈/回撤」和「账户权益回撤」两条线同时盯住,超过阈值就直接在图上写红字并锁死下单。daily_profit_or_drawdown 低于 -5.00,或者 (Acc_E()-initialBalance)/initialBalance*100 小于 -12.00,isTradeAllowed 会被置为 false,提示文字写的是「Maximum Threshold Hit, Can't Trade.」——外汇和贵金属杠杆高,这类硬止损阈值能避免单日崩掉本金,但参数要按自己账户规模重算。 createText 是个封装好的 OBJ_LABEL 创建函数:先 ObjectCreate 建标签,再用 OBJPROP_XDISTANCE / OBJPROP_YDISTANCE 定坐标(左上角为锚),最后 ChartRedraw 刷新。下面这段是它的完整骨架,逐行看一遍就能抄进自己的 EA。 checkDailyProfit 里用 TimeToString(TimeCurrent(),TIME_DATE) 取当天 0 点,再加 86400 秒(1*24*60*60)得到当日截止时间,逻辑清晰但没处理跨夏令时,实盘前最好自己加打印验证。

MQL5 / C++
class="type">bool createText(class="type">class="kw">string objName,class="type">class="kw">string text,class="type">int x, class="type">int y,class="type">class="kw">color clrTxt,class="type">int fontSize){
   ResetLastError();
   if (!ObjectCreate(class="num">0,objName,OBJ_LABEL,class="num">0,class="num">0,class="num">0)){
      Print(__FUNCTION__,": failed to create the Label! Error Code = ",GetLastError());
      class="kw">return (false);
   }
   ObjectSetInteger(class="num">0,objName,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(class="num">0,objName,OBJPROP_YDISTANCE,y);
   ObjectSetInteger(class="num">0,objName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
   ObjectSetString(class="num">0,objName,OBJPROP_TEXT,text);
   ObjectSetInteger(class="num">0,objName,OBJPROP_COLOR,clrTxt);
   ObjectSetInteger(class="num">0,objName,OBJPROP_FONTSIZE,fontSize);

   ChartRedraw(class="num">0);
   class="kw">return (true);
}

◍ 从成交历史反推当日盈亏百分比

这段逻辑干的事很直接:拉取指定时间窗内的平仓成交,把每笔的 profit、commission、swap 三项加总,得到当日净盈亏 total_day_Profit。用当前账户余额减去这个值,反推出周期起始时的 startingBalance,再算 (total_day_Profit*100/startingBalance) 并 NormalizeDouble 到两位小数,就是当日收益率或回撤率的近似值。 注意 HistorySelect(start,end) 必须先调用,否则 HistoryDealsTotal 拿不到数据;循环里只筛 DEAL_ENTRY_OUT 的票,说明只考虑平仓动作,未平仓浮盈不计入。外汇与贵金属杠杆高,这类按日统计的回撤率若超过 5% 往往意味着仓位偏激,需结合品种波动率看。 最后一段用 Acc_E() 与 initialBalance 比大小,分别用 clrLime / clrRed / clrWhite 在固定坐标写文字:盈利显绿、亏损显红、持平显白,并在 250,190 位置输出相对初始资金的百分比。你可以把 createText 的坐标和颜色直接改掉,套进自己的 HUD 面板。

MQL5 / C++
  dayBalance = Acc_B();
  }

  HistorySelect(start,end);
  class="type">int TotalDeals = HistoryDealsTotal();
  for (class="type">int i=class="num">0; i<TotalDeals; i++){
    class="type">class="kw">ulong Ticket = HistoryDealGetTicket(i);
    if (HistoryDealGetInteger(Ticket,DEAL_ENTRY)==DEAL_ENTRY_OUT){
      class="type">class="kw">double Latest_Day_Profit = (HistoryDealGetDouble(Ticket,DEAL_PROFIT)
                                +HistoryDealGetDouble(Ticket,DEAL_COMMISSION)
                                +HistoryDealGetDouble(Ticket,DEAL_SWAP));
      total_day_Profit += Latest_Day_Profit;
    }
  }
  class="type">class="kw">double startingBalance = class="num">0;
  startingBalance = AccountInfoDouble(ACCOUNT_BALANCE) - total_day_Profit;
  class="type">class="kw">double daily_profit_or_drawdown = NormalizeDouble((total_day_Profit*class="num">100/startingBalance),class="num">2);
  class="type">class="kw">string daily_profit_in_Text_Format = "";
  daily_profit_in_Text_Format = DoubleToString(daily_profit_or_drawdown,class="num">2)+" %";

  class=class="str">"cmt">//Print(total_day_Profit, " >>> ",daily_profit_in_Text_Format);

  createText("class="num">4",TimeToString(start),class="num">120,class="num">80,clrYellow,class="num">10);
  createText("class="num">6",TimeToString(to),class="num">120,class="num">95,clrYellow,class="num">10);
  createText("class="num">8",TimeToString(end),class="num">120,class="num">110,clrWhite,class="num">10);
  if (Acc_E() > initialBalance){
    createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrLime,class="num">10);
    createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrLime,class="num">10);
  }
  else if (Acc_E() < initialBalance){
    createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrRed,class="num">10);
    createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrRed,class="num">10);
  }
  if (Acc_E() == initialBalance){
    createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrWhite,class="num">10);
    createText("class="num">17",DoubleToString((Acc_E()-initialBalance)/initialBalance*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrWhite,class="num">10);
  }

「把当日盈亏与硬止损画在图表上」

这段逻辑负责把账户当天的核心数字直接渲染到 MT5 主图,避免交易者盯着净值曲线凭感觉下单。dayBalance 是当日初始余额基准,代码先以白色在 (270,225) 写出基准值,再用黄色在 (270,255) 标出 dayBalance*5/100 的预警线——也就是余额回撤 5% 的硬阈值。 createText("20",DoubleToString(dayBalance,2)+" "+Acc_S(),270,225,clrWhite,10); createText("24","-"+DoubleToString(dayBalance*5/100,2)+" "+Acc_S(),270,255,clrYellow,10); 接下来按当前余额与 dayBalance 的大小关系切换颜色:高于基准用 clrLime 写 total_day_Profit 与格式化文本,低于基准用 clrRed,相等则用 clrWhite,坐标固定在 270 列的 270 与 285 两行。 if (Acc_B() > dayBalance){ createText("26",DoubleToString(total_day_Profit,2)+" "+Acc_S(),270,270,clrLime,10); createText("28",daily_profit_in_Text_Format,270,285,clrLime,10); } else if (Acc_B() < dayBalance){ createText("26",DoubleToString(total_day_Profit,2)+" "+Acc_S(),270,270,clrRed,10); createText("28",daily_profit_in_Text_Format,270,285,clrRed,10); } else if (Acc_B() == dayBalance){ createText("26",DoubleToString(total_day_Profit,2)+" "+Acc_S(),270,270,clrWhite,10); createText("28",daily_profit_in_Text_Format,270,285,clrWhite,10); } 真正的锁仓判断在末尾:当日浮亏 daily_profit_or_drawdown 小于等于 -5.00,或权益相对 initialBalance 回撤超 -12.00%,就在 (70,300) 用红字提示阈值触发并置 isTradeAllowed=false;否则写“未触发可交易”。外汇与贵金属杠杆高,这类硬熔断逻辑可能帮你躲过单日腰斩,但参数需按自己账户规模重算。

if (daily_profit_or_drawdown <= -5.00((Acc_E()-initialBalance)/initialBalance*100) < -12.00){

createText("29",">>> Maximum Threshold Hit, Can't Trade.",70,300,clrRed,10); isTradeAllowed = false; } else { createText("29",">>> Maximum Threshold Not Hit, Can Trade.",70,300,clrRed); } 基础变量声明里,initialBalance、dayBalance、isTradeAllowed 都是全局双精度或布尔,Acc_B() 直接封装 AccountInfoDouble(ACCOUNT_BALANCE) 取余额,开 MT5 把这几行塞进 EA 初始化后面就能跑。 #include <Trade/Trade.mqh> CTrade obj_Trade; int totalBars = 0; double initialBalance = 0; double dayBalance = 0; datetime dayTime = 0; bool isTradeAllowed = true; double Acc_B() {return AccountInfoDouble(ACCOUNT_BALANCE);}

MQL5 / C++
createText("class="num">20",DoubleToString(dayBalance,class="num">2)+" "+Acc_S(),class="num">270,class="num">225,clrWhite,class="num">10);
createText("class="num">24","-"+DoubleToString(dayBalance*class="num">5/class="num">100,class="num">2)+" "+Acc_S(),class="num">270,class="num">255,clrYellow,class="num">10);
if (Acc_B() > dayBalance){
   createText("class="num">26",DoubleToString(total_day_Profit,class="num">2)+" "+Acc_S(),class="num">270,class="num">270,clrLime,class="num">10);
   createText("class="num">28",daily_profit_in_Text_Format,class="num">270,class="num">285,clrLime,class="num">10);
}
else if (Acc_B() < dayBalance){
   createText("class="num">26",DoubleToString(total_day_Profit,class="num">2)+" "+Acc_S(),class="num">270,class="num">270,clrRed,class="num">10);
   createText("class="num">28",daily_profit_in_Text_Format,class="num">270,class="num">285,clrRed,class="num">10);
}
else if (Acc_B() == dayBalance){
   createText("class="num">26",DoubleToString(total_day_Profit,class="num">2)+" "+Acc_S(),class="num">270,class="num">270,clrWhite,class="num">10);
   createText("class="num">28",daily_profit_in_Text_Format,class="num">270,class="num">285,clrWhite,class="num">10);
}

if (daily_profit_or_drawdown <= -class="num">5.00 || ((Acc_E()-initialBalance)/initialBalance*class="num">100) < -class="num">12.00){
   createText("class="num">29",">>> Maximum Threshold Hit, Can&class="macro">#x27;t Trade.",class="num">70,class="num">300,clrRed,class="num">10);
   isTradeAllowed = false;
}
else {
   createText("class="num">29",">>> Maximum Threshold Not Hit, Can Trade.",class="num">70,class="num">300,clrRed);
}

class="macro">#include <Trade/Trade.mqh>
CTrade obj_Trade;
class="type">int totalBars = class="num">0;
class="type">class="kw">double initialBalance = class="num">0;
class="type">class="kw">double dayBalance = class="num">0;
class="type">class="kw">datetime dayTime = class="num">0;
class="type">bool isTradeAllowed = true;
class="type">class="kw">double Acc_B() {class="kw">return AccountInfoDouble(ACCOUNT_BALANCE);}

面板初始化时把回撤阈值写死在屏幕上

这段初始化函数负责在 MT5 图表左上角画出每日回撤限制器的监控面板。它先抓取账户初始余额 initialBalance = Acc_B(),随后用 createText 批量生成 18 行文本对象,坐标从 (30,30) 到 (250,190) 像素逐行排开。 面板里有一行写死了「Tolerated DrawDown: 12.00 %」,意味着当前版本把容忍回撤硬性锁在 12%,想改阈值得直接动这行字符串,而不是从外部输入参数读入。外汇与贵金属杠杆高,12% 的账户回撤容忍在实际滑点行情中可能很快被触及,需按自己风控重新编译。 净值的实时显示靠 Acc_E() 取 ACCOUNT_EQUITY,余额变异用 (Acc_E()-Acc_B())/Acc_B()*100 算百分比,保留两位小数。把下面代码丢进 EA 的 OnInit,就能在每次加载时看到这块面板,验证自己账户币种和初始余额有没有被正确捕获。

MQL5 / C++
class="type">class="kw">double Acc_E() {class="kw">return AccountInfoDouble(ACCOUNT_EQUITY);}
class="type">class="kw">string Acc_S() {class="kw">return AccountInfoString(ACCOUNT_CURRENCY);}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert initialization function                                     |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int OnInit()
  {
   class=class="str">"cmt">// Initialize initial balance
   initialBalance = Acc_B();
   
   class=class="str">"cmt">// Create dashboard texts
   createText("class="num">0","*** Daily Drawdown Limiter ***",class="num">30,class="num">30,clrBlack,class="num">13);
   createText("class="num">00","______________________________________",class="num">30,class="num">30,clrBlack,class="num">13);
   createText("class="num">1","DrawDown Limiter is Active.",class="num">70,class="num">50,clrBlack,class="num">11);
   createText("class="num">2","Counters will be reset on Next Day Start.",class="num">70,class="num">65,clrBlack,class="num">10);
   createText("class="num">3","From: ",class="num">70,class="num">80,clrBlack,class="num">10);
   createText("class="num">4","Time Here",class="num">120,class="num">80,clrGray,class="num">10);
   createText("class="num">5","To: ",class="num">70,class="num">95,clrBlack,class="num">10);
   createText("class="num">6","Time Here",class="num">120,class="num">95,clrGray,class="num">10);
   createText("class="num">7","Current: ",class="num">70,class="num">110,clrBlack,class="num">10);
   createText("class="num">8","Time Here",class="num">120,class="num">110,clrGray,class="num">10);
   createText("class="num">9","ACCOUNT DRAWDOWN ============",class="num">70,class="num">130,clrPeru,class="num">11);
   createText("class="num">10","Account Initial Balance: ",class="num">70,class="num">145,clrBlack,class="num">10);
   createText("class="num">11",DoubleToString(initialBalance,class="num">2)+" "+Acc_S(),class="num">250,class="num">145,clrBlack,class="num">10);
   createText("class="num">12","Tolerated DrawDown: ",class="num">70,class="num">160,clrBlack,class="num">10);
   createText("class="num">13","class="num">12.00 %",class="num">250,class="num">160,clrBlack,class="num">10);
   createText("class="num">14","Current Account Equity: ",class="num">70,class="num">175,clrBlack,class="num">10);
   createText("class="num">15",DoubleToString(Acc_E(),class="num">2)+" "+Acc_S(),class="num">250,class="num">175,clrBlack,class="num">10);
   createText("class="num">16","Current Balance Variation: ",class="num">70,class="num">190,clrBlack,class="num">10);
   createText("class="num">17",DoubleToString((Acc_E()-Acc_B())/Acc_B()*class="num">100,class="num">2)+" %",class="num">250,class="num">190,clrGray,class="num">10);
交给小布盯盘看盘口
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到实时回撤与信号状态,你专注决策就好。

常见问题

止损针对单笔持仓,回撤限制器看账户整体每日净值下滑,越线后限制新开仓或强平,防的是系统性过度交易。
可以,点击一下就能收起详情页释放图表空间,颜色字体也都能自定义,不影响后台监控逻辑。
小布盯盘内置 AIGC 诊断,可展示同类回撤与信号状态,但 EA 文件需在 MT5 本地编译部署,两者互补使用。
通常比对每日起始余额与当前权益,差值超阈值就置信号灯红并阻断开仓函数,具体代码见后续小节。
CTrade 类封装了下单撤单等接口,建指针实例才能调用成员函数完成实际交易操作。