English Русский Español 日本語 Português
preview
在MQL5中实现盈亏平衡机制(第一部分):基类与固定点数的盈亏平衡模式

在MQL5中实现盈亏平衡机制(第一部分):基类与固定点数的盈亏平衡模式

MetaTrader 5示例 |
46 0
Niquel Mendoza
Niquel Mendoza


引言

将止损移至盈亏平衡位,是一种保护持仓的交易技巧。核心逻辑是:当价格向盈利方向移动了一定点数后,就把止损调至开仓价。这不仅能有效保护头寸,还能在行情突然回调时减少损失

盈亏平衡机制的作用主要体现在两方面:

  • 第一,确保即使价格未触及止盈,交易也不会亏损离场
  • 第二,通过将止损设在入场价之外若干点来锁定利润。

本系列文章将开发三种不同形式的盈亏平衡机制。在第一部分中,我们将构建系统的基类,并实现第一种简易盈亏平衡模式,以此作为后续扩展的模板。


什么是盈亏平衡模式

在将盈亏平衡概念集成到系统之前,首要的是理解其基本运作机制。

简而言之,盈亏平衡模式是指当价格偏离开仓价一定距离后,将止损位移动固定的点数(即额外点数)


    be1

    图 1. 修改止损价格前的空头头寸

    图 1 显示,止损最初被设在 Order Blocks 指标信号所在位置(该指标是我们在另一系列文章中开发的)。

     be2

    图 2. 修改止损价格后的空头头寸

    图 2 展示了新 K 线出现后,系统自动将止损位调整至预设的 150 点位置。这一过程正是前述激活机制的具体体现。

    接下来,我们来看盈亏平衡模式激活之后发生了什么。

    be3


    图 3:在盈亏平衡位平仓后的空头头寸

    从图 3 可以看出,由于此前做了调整,头寸最终在盈亏平衡位平仓,略有盈利。在这个例子中,因为价格并没有运行到离入场点太远的位置,因此避免了触发止损而全额亏损。

    这就是盈亏平衡机制的优势之一:它有助于保留一定盈利空间,或帮助避免完全止损。
    但需注意,能否真正“锁定利润”,取决于盈亏平衡点的具体设置

    另一方面,这种方法也存在一些弊端。有时,若未提前移动止损,交易本有机会触及止盈目标。这在很大程度上取决于所采用的策略。

    在此,我们推荐结合 Order Blocks 信号使用盈亏平衡模式,因为订单块并不总能成功,因此对持仓进行保护就显得尤为关键。

    基于 ATR 的盈亏盈亏平衡模式

    另一种管理方式,是利用 ATR(Average True Range,平均真实波幅)指标动态地将止损移至盈亏平衡位。

    该方法与前一种类似,区别在于不使用固定点数,而是采用乘数参数。结合当前市场波动率,该乘数可计算出更合理的止损调整距离。对于黄金等高波动品种,这种方法显得更为灵活。

    例如,如果将固定盈亏平衡位设为 150 点(对黄金来说约相当于 1.50 美元),那么在重大经济数据发布前,金价很容易波动 3.00 美元甚至更多。在这种情况下,使用基于 ATR 的动态调整,可以更好地适应市场行为,避免因正常波动而导致交易过早被平仓。

    因此,基于 ATR 的方法并不采用固定点数,而是根据当前市场状况动态调整。

    基于风险回报比(RRR)的盈亏平衡模式

    最后,我们也可以依据风险回报比(Risk-Reward Ratio,RRR)将止损移至盈亏平衡位。RRR 反映的是一笔交易所承担的风险与预期收益之间的关系。其计算方式为止盈值除以止损值。

    理解 RRR 对于更好地管理仓位非常重要:

    • RRR 值越高,胜率通常越低,因为价格需运行更远才能达标;
    • RRR 值越低,触及止盈的概率虽高,但若市场反向运行,潜在亏损也更大。

      基于风险回报比(RRR)的盈亏平衡逻辑十分直观。例如,若开仓时止盈为止损的两倍,即 RRR 为 1:2。

      使用这种方法,可以设定:当交易相对于初始风险达到 1:1 时,将止损移至入场价。这意味着,一旦市场向有利方向走出足够弥补风险的幅度,交易就会自动受到保护。

      例如:

      • 达到 1:1 时,可以将止损移至盈亏平衡位以保护头寸;
      • 也可以配置在更大的比例触发,比如 1:2、1:3 等,具体取决于策略类型。

        这样一来,只要市场往有利方向走出足够距离,就能按设定的风险回报比保护仓位。


        所需的结构体与枚举

        在本节中,我们将开始编写盈亏平衡机制的基础代码。在构建主类之前,先定义必要的结构体与枚举。整个过程会清晰、简单、直观。我们不会把之前为风险管理创建的所有枚举都用上,实现盈亏平衡功能只需要其中一部分。让我们开始吧。

        我们将创建一个简易结构体,用于存储应用盈亏平衡模式所需的核心信息。这个结构体须包含:

        • trade ticket,ticket 编号;
        • price_to_beat,触发止损移动的突破价格,
        • breakeven_price,条件满足时的目标止损价
        • 操作类型,用于正确判断各个价位。

          该结构体的形式如下:

          struct position_be
           {
            ulong              ticket;           //Position Ticket
            double             breakeven_price;  //Be price
            double             price_to_beat;    //Price to exceed to reach break even
            ENUM_POSITION_TYPE type;             //Position type
           };

          盈亏平衡类型的枚举

          我们定义了一个枚举类型,用于选择前文所述的三种激活方式,以优化单笔交易的管理。

          enum ENUM_BREAKEVEN_TYPE
           {
            BREAKEVEN_TYPE_RR = 0,           //By RR
            BREAKEVEN_TYPE_FIXED_POINTS = 1, //By FixedPoints
            BREAKEVEN_TYPE_ATR = 2           //By Atr
           };

          盈亏平衡模式的枚举

          不同的交易策略运作方式各异。因此,我们创建了一个枚举,以便明确指定盈亏平衡模式如何应用于当前的持仓。

          enum ENUM_BREAKEVEN_MODE
           {
            BREAKEVEN_MODE_AUTOMATIC,
            BREAKEVEN_MODE_MANUAL
           };

          • BREAKEVEN_MODE_AUTOMATIC 模式通过 OnTradeTransaction 自动跟踪所有新开仓位。交易者无需进行额外操作。系统会即时检测、保护并管理仓位。
          • BREAKEVEN_MODE_MANUAL 模式提供了极高的配置灵活性。交易者可自主选择哪些订单受盈亏平衡机制保护,并决定何时开始检查条件。该模式非常适合需要人工筛选的策略,例如离散剥头皮交易或高精度的交易设置。


          创建基类 CBreakEvenBase

          在定义基类之前,我们首先引入之前文章中开发的风险管理系统。

          #include <Risk_Management.mqh>

          CBreakEvenBase类的受保护成员

          以下是该类及其内部变量的定义,每个变量均附有说明。

          //+------------------------------------------------------------------+
          //| Main class to apply break even                                   |
          //+------------------------------------------------------------------+
          class CBreakEvenBase
           {
          protected:
            CTrade             obj_trade;     //CTrade object
            MqlTick            tick;          //tick structure
            string             symbol;        //current symbol
            double             point_value;   //value of the set symbol point
            position_be        PositionsBe[];  //array of positions of type Positions
            ulong              magic;         //magic number of positions to make break even
            bool               pause;         //Boolean variable to activate the pause of the review, this is used to prevent the array from going out of range
            int                num_params;    //Number of parameters the class needs
            ENUM_BREAKEVEN_MODE breakeven_mode; //Break even mode, manual or automatic
            bool               allow_extra_logs;
             
             
           };

          发送和修改订单的变量。 

          CTrade             obj_trade;    //CTrade object

          MqlTickqlTick 结构体,用于存储最新 Tick 的信息。 

          MqlTick            tick;         //tick structure

          当前分析的品种名称。 

          string             symbol;       //current symbol

          所选品种的点值。 

          double             point_value;  //value of the set symbol point

          将应用盈亏平衡机制的持仓数组。 

          position_be        PositionsBe[]; //array of positions of type Positions

          交易的唯一标识符(magic数字)。 

          ulong              magic;        //magic number of positions to make break even

          用于暂停检查的标志位。 

          bool               pause;         //Boolean variable to activate the pause of the review, this is used to prevent the array from going out of range

          类所需的参数数量。

          int                num_params;    //Number of parameters the class needs

          盈亏平衡应用模式(手动或自动)。

          ENUM_BREAKEVEN_MODE breakeven_mode; //Break even mode, manual or automatic

          是否允许生成额外日志记录。

          bool               allow_extra_logs;

          CBreakEvenBase 函数

          本节我们将开始定义基类中的函数,这些函数将被后续实现的各个子类继承。

          构造函数

          构造函数是类的核心。在 CBreakEvenBase 中,其职责是初始化内部变量并赋值,以确保类正常运行。

          该构造函数接收三个参数:symbol(用于获取买价和卖价数据)、magic(用于识别特定订单)以及 mode(用于指明所选的盈亏平衡类型,可为自动或手动)。

          CBreakEvenBase(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_);

          构造函数定义

          构造函数执行时,pause 和 allow_extra_logs 变量会被初始化为 false。随后,它会检查传入的 mode 值是否有效。若参数无效,系统将打印错误信息并调用 ExpertRemove 将 EA 从图表移除,防止代码在错误配置下继续运行。

          //+------------------------------------------------------------------+
          //| Constructor                                                       |
          //+------------------------------------------------------------------+
          CBreakEvenBase::CBreakEvenBase(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_)
            : pause(false), allow_extra_logs(false)
           {
            if(magic_ != NOT_MAGIC_NUMBER)
              obj_trade.SetExpertMagicNumber(magic_);
          
            if(mode_ != BREAKEVEN_MODE_MANUAL && mode_ != BREAKEVEN_MODE_AUTOMATIC)
             {
              printf("%s:: Error critico el modo del break even %s, es invalido", __FUNCTION__, EnumToString(mode_));
              ExpertRemove();
             }
          
            this.symbol = symbol_;
            this.num_params = 0;
            this.magic = magic_;
            this.breakeven_mode = mode_;
            this.point_value = SymbolInfoDouble(symbol_, SYMBOL_POINT);
           }
          

          同时,接收到的参数会分别赋值给 symbol、magic 和 breakeven_mode。

          交易品种的点值存储在 point_value 变量中,参数数量初始化为 0。CBreakEvenBase 的每个子类都将在各自的构造函数中定义所需的参数数量。

          此外,若 magic_ 变量等于 NOT_MAGIC_NUMBER,我们将不执行 CTrade 类中用于设置魔术数字的成员函数。 

          注意: NOT_MAGIC_NUMBER 是在风险管理头文件中定义的一个宏。它用于用于表示不使用magic数字。若启用该定义,盈亏平衡机制将应用于所有交易(前提是使用自动模式)。

          析构函数

          析构函数用于释放存储被管理持仓的数组所占用的内存。当对象销毁时,该函数会自动执行。

          //+------------------------------------------------------------------+
          //| Destructor                                                       |
          //+------------------------------------------------------------------+
          CBreakEvenBase::~CBreakEvenBase()
           {
            ArrayFree(PositionsBe);
           }


          实现类的通用函数

          接下来,我们将定义 CBreakEvenBase 基类的函数。这些函数将被后续实现的派生类使用和扩展。

          首先,我们声明一个返回类所需参数数量的函数。它仅返回受保护变量 num_params 的值。此外,它被标记为 const 以防修改对象状态,并被标记为 final 以防止子类重写。

          virtual inline int GetNumParams() const final { return num_params; }

          接下来声明 Add 函数,用于向 PositionsBe 数组添加新的 position_be 结构体。该函数接收必要的交易数据,如订单号、开盘价、止损价和持仓类型。

          virtual bool       Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type) = 0;

          因为这是个纯虚函数,所以继承 CBreakEvenBase 的子类都必须实现自己的版本。

          该类的主要功能是确保实现盈亏平衡。它用于对 PositionsBe 数组中存储的所有持仓进行盈亏平衡调整。通过 for 循环遍历数组来实现这点。

          在应用调整前,会检查数组大小是否大于零且 pause 变量是否为 false。若条件未满足,函数将立即退出。

          由于 position_be 结构体已包含目标价位和触发调整的价格,因此只需检查市场条件是否满足:

          • 若为多头持仓,则检查 Ask 价格是否大于或等于 price_to_beat;
          • 若为空头持仓,则检查 Bid 价格是否小于或等于该值。

          确认条件满足后,通过相应的票据识别持仓,并计算当前的止盈值。随后,使用 CTrade 类的 PositionModify 方法执行修改。最后,标记该持仓,从数组中删除。

          //+------------------------------------------------------------------+
          //| Function to make break even                                      |
          //+------------------------------------------------------------------+
          void CBreakEvenBase::BreakEven(void)
           {
            if(this.PositionsBe.Size() < 1 || pause)
              return;
          
            SymbolInfoTick(this.symbol, tick);
          
            int indices_to_remove[];
          
            for(int i = 0 ; i < ArraySize(this.PositionsBe) ; i++)
             {
              if((this.PositionsBe[i].type == POSITION_TYPE_BUY && tick.ask >= this.PositionsBe[i].price_to_beat) ||
                 (this.PositionsBe[i].type == POSITION_TYPE_SELL && tick.bid <= this.PositionsBe[i].price_to_beat))
              {
                  if(!PositionSelectByTicket(this.PositionsBe[i].ticket))
                  {
                   printf("%s:: Error al seleccionar el ticket %I64u",__FUNCTION__,this.PositionsBe[i].ticket);
                   ExtraFunctions::AddArrayNoVerification(indices_to_remove, i);
                   continue;
                  }
                  
                  double position_tp = PositionGetDouble(POSITION_TP);
                  obj_trade.PositionModify(this.PositionsBe[i].ticket, this.PositionsBe[i].breakeven_price, position_tp);
                  ExtraFunctions::AddArrayNoVerification(indices_to_remove, i);
              }
             }
          
            ExtraFunctions::RemoveMultipleIndexes(this.PositionsBe, indices_to_remove);
           }

          为了在新仓开启时将其结构添加到 PositionsBe 数组中,我们定义了一个供OnTradeTransaction 事件调用的函数。该函数命名为 OnTradeTransactionEvent,每当发生交易事件时都会执行。

          virtual void       OnTradeTransactionEvent(const MqlTradeTransaction& trans) final;

          在将持仓加入 PositionsBe 数组之前,需先使用 HistoryDealSelect 函数选中对应的成交单。随后通过 entry 值判定交易的类型。

          若类型为开仓(即 DEAL_ENTRY_IN),系统会检查成交是否已完全开启、盈亏平衡模式是否为自动(BREAKEVEN_MODE_AUTOMATIC),以及成交单的magic数字是否与内部 magic 变量匹配(或者后者值为 NOT_MAGIC_NUMBER)。

          当所有条件满足时,通过 Add 函数将持仓添加至 PositionsBe 数组。若启用了 allow_extra_logs,则会输出持仓注册的提示信息。

          若类型为平仓(即 DEAL_ENTRY_OUT)且持仓已完全平仓,则从 PositionsBe 数组中移除对应的成交单。为避免移除过程中发生冲突,需暂时将 pause 变量设为 true。

          该函数的代码如下:

          //+------------------------------------------------------------------+
          //| OnTradeTransactionEvent                                          |
          //+------------------------------------------------------------------+
          void CBreakEvenBase::OnTradeTransactionEvent(const MqlTradeTransaction &trans)
           {
            if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
              return;
          
            HistoryDealSelect(trans.deal);
            ENUM_DEAL_ENTRY entry = (ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
            bool pos = PositionSelectByTicket(trans.position);
          
            if(breakeven_mode == BREAKEVEN_MODE_AUTOMATIC)
             {
              ulong position_magic = (ulong)HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
          
              if(entry == DEAL_ENTRY_IN && pos && (this.magic == position_magic || this.magic == NOT_MAGIC_NUMBER))
               {
                if(Add(trans.position, PositionGetDouble(POSITION_PRICE_OPEN), PositionGetDouble(POSITION_SL), (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE)))
                  if(this.allow_extra_logs)
                    printf("%s:: Se a añadido el ticket %I64u del array de posiciones", __FUNCTION__, trans.position);
                return;
               }
             }
          
            if(entry == DEAL_ENTRY_OUT && pos == false)
             {
              this.pause = true;
          
              if(ExtraFunctions::RemoveIndexFromAnArrayOfPositions(PositionsBe, trans.position))
                if(this.allow_extra_logs)
                  printf("%s:: Se a eliminado el ticket %I64u del array de posiciones", __FUNCTION__, trans.position);
          
              this.pause = false;
             }
           }

          有些派生类需要配置各种参数。比如,简单的调整需要两个整数值,而像基于 ATR 的方法则需要周期、时间框架和乘数等数据。

          由于无法在基类中定义参数不同的多个虚函数版本,我们建议使用 MqlParam 类型的数组来存储必要的配置。这样,每个子类都能在内部自行解析并赋予接收到的值。

          virtual void       Set(MqlParam &params[]) = 0;

          最后,我们添加一个函数用于开启或关闭额外的提示信息。在程序运行期间,当你需要记录交易的增删等操作时,这会很有用。

          virtual void       SetExtraLogs(bool allow_extra_logs_) final { this.allow_extra_logs = allow_extra_logs_; }


          开发首个类 CBreakEvenSimple

          内部变量

          基类准备就绪后,我们开始开发 CBreakEvenSimple 类。这个类通过固定点数来实现盈亏平衡功能。

          为此,声明了两个变量:

          int                extra_points_be, points_be;

          • 变量 extra_points_be 表示激活后盈亏平衡位将移动的额外距离;
          • 变量 points_be 则决定价格必须盈利多少点才会触发调整。

          构造函数

          构造函数中需设置初始值。调用基类构造函数来完成继承参数的赋值。在构造函数体内,内部变量初始化为 0,同时定义类所需的参数数量,此处为 2 个。

                               CBreakEvenSimple(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_)
          :                CBreakEvenBase(symbol_, magic_, mode_) { this.extra_points_be = 0; this.points_be = 0; this.num_params = 2;}

          参数设置函数

          为了给内部变量 extra_points_be 和 points_be 赋值,我们将使用 MqlParam 类型的数组。这是 MQL5 语言的标准结构,通常用于通过 ChartIndicatorAdd() 函数添加指标时传参。

          在这里,我们用它来向类传递参数。Set 函数将重写基类的虚函数。使用 "override" 关键字修饰。

          void               Set(MqlParam &params[]) override;

          函数内部会检查 params 数组的大小是否不小于 2。若条件不满足,将输出提示性错误信息;若满足,则调用 SetSimple 函数进行赋值。

          //+------------------------------------------------------------------+
          //| Set attributes of CBreakEvenSimple class with MqlParam array     |
          //+------------------------------------------------------------------+
          void CBreakEvenSimple::Set(MqlParam &params[])
           {
            if(params.Size() < 2)
             {
              printf("%s:: Error setting simple break-even, the size of the params array %I32u to less than 2", __FUNCTION__, params.Size());
              return;
             }
          
            SetSimple(int(params[0].integer_value), int(params[1].integer_value));
           }

          SetSimple 函数

          SetSimple 函数允许直接设置 points_be 和 extra_points_be 的值,无需借助参数数组。调用时直接传入整数值即可。

          //+------------------------------------------------------------------+
          //| Function to set member variables without using MalParams         |
          //+------------------------------------------------------------------+
          void CBreakEvenSimple::SetSimple(int points_be_, int extra_points_be_)
           {
            if(points_be_ <= 0)
             {
              printf("%s:: Error when setting the break even value for fixed points, be points %I32d are invalid.", __FUNCTION__, extra_points_be_);
              ExpertRemove();
              return;
             }
          
            if(extra_points_be_ < 0)
             {
              printf("%s:: Error when setting the break even value for fixed points, extra points %I32d are invalid.", __FUNCTION__, extra_points_be_);
              ExpertRemove();
              return;
             }
          
            if(extra_points_be_ >= points_be_)
             {
              printf("%s:: Warning: The break even points (breakeven_price) is greater than the breakeven points (price_to_beat)\nTherefore the value of the extra breakeven points will be modified 0.",
                     __FUNCTION__);
              this.points_be = points_be_; //0
              this.extra_points_be = 0; //1
              return;
             }
          
            this.points_be = points_be_; //0
            this.extra_points_be = extra_points_be_; //1
           }

          如果在代码中直接定义了值,或者在特定情况下需要避开 MqlParam 类型,这种方法会非常实用。

          向 PositionsBe 数组添加持仓数据的函数

          为了继续开发 CBreakEvenSimple 类,我们需要重写 Add 函数。该函数负责计算并声明 position_be 结构体的关键变量,这些数据随后将存入 PositionsBe 数组。

          open_price 参数是计算 breakeven_price 和 price_to_beat 这两个值的基础。前者定义止损移动的目标位,后者定义触发调整的价格。

          计算 breakeven_price 的公式如下:

          • 对于多头仓位:

          break_even_price = open_price + (point_value * extra_points_be)

          • 对于空头仓位:

          break_even_price = open_price - (point_value * extra_points_be)

          相应的,计算 price_to_beat 时使用相同的公式,只需将 extra_points_be 替换为 points_be。

          确定这些数值后,便用必要的数据填充 position_be 结构。接着,利用 AddArrayNoVerification 函数将该结构添加到 PositionsBe 数组中——该函数我们在之前的风险管理章节中已经使用过。

          实现代码如下:

          //+----------------------------------------------------------------------------------------------+
          //| Create a new structure and add it to the main array using the 'AddToArrayBe' function        |
          //+----------------------------------------------------------------------------------------------+
          bool CBreakEvenSimple::Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type)
           {
            position_be new_pos;
            new_pos.breakeven_price =  position_type == POSITION_TYPE_BUY ? open_price + (point_value * extra_points_be) : open_price - (point_value * extra_points_be);
            new_pos.type =  position_type;
            new_pos.price_to_beat = position_type == POSITION_TYPE_BUY ? open_price + (point_value * points_be) : open_price - (point_value * points_be) ;
            new_pos.ticket = post_ticket;
            ExtraFunctions::AddArrayNoVerification(this.PositionsBe,new_pos); 
            return true;
           }

          完整类代码:

          //+------------------------------------------------------------------+
          //| class CBreakEvenSimple                                           |
          //+------------------------------------------------------------------+
          class CBreakEvenSimple : public CBreakEvenBase
           {
          private:
            int                extra_points_be, points_be;
          
          public:
                               CBreakEvenSimple(string symbol_, ulong magic_, ENUM_BREAKEVEN_MODE mode_)
              :                CBreakEvenBase(symbol_, magic_, mode_) { this.extra_points_be = 0; this.points_be = 0; this.num_params = 2;}
          
          
            bool               Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type) override;
            void               Set(MqlParam &params[]) override;
            void               SetSimple(int points_be_, int extra_points_be_);
           };
          
          //+----------------------------------------------------------------------------------------------+
          //| Create a new structure and add it to the main array using the 'AddToArrayBe' function        |
          //+----------------------------------------------------------------------------------------------+
          bool CBreakEvenSimple::Add(ulong post_ticket, double open_price, double sl_price, ENUM_POSITION_TYPE position_type)
           {
            position_be new_pos;
            new_pos.breakeven_price =  position_type == POSITION_TYPE_BUY ? open_price + (point_value * extra_points_be) : open_price - (point_value * extra_points_be);
            new_pos.type =  position_type;
            new_pos.price_to_beat = position_type == POSITION_TYPE_BUY ? open_price + (point_value * points_be) : open_price - (point_value * points_be) ;
            new_pos.ticket = post_ticket;
            ExtraFunctions::AddArrayNoVerification(this.PositionsBe, new_pos);
            return true;
           }
          
          //+------------------------------------------------------------------+
          //| Set attributes of CBreakEvenSimple class with MqlParam array     |
          //+------------------------------------------------------------------+
          void CBreakEvenSimple::Set(MqlParam &params[])
           {
            if(params.Size() < 2)
             {
              printf("%s:: Error setting simple break-even, the size of the params array %I32u to less than 2", __FUNCTION__, params.Size());
              return;
             }
          
            SetSimple(int(params[0].integer_value), int(params[1].integer_value));
           }
          
          //+------------------------------------------------------------------+
          //| Function to set member variables without using MalParams         |
          //+------------------------------------------------------------------+
          void CBreakEvenSimple::SetSimple(int points_be_, int extra_points_be_)
           {
            if(points_be_ <= 0)
             {
              printf("%s:: Error when setting the break even value for fixed points, be points %I32d are invalid.", __FUNCTION__, extra_points_be_);
              ExpertRemove();
              return;
             }
          
            if(extra_points_be_ < 0)
             {
              printf("%s:: Error when setting the break even value for fixed points, extra points %I32d are invalid.", __FUNCTION__, extra_points_be_);
              ExpertRemove();
              return;
             }
          
            if(extra_points_be_ >= points_be_)
             {
              printf("%s:: Warning: The break even points (breakeven_price) is greater than the breakeven points (price_to_beat)\nTherefore the value of the extra breakeven points will be modified 0.",
                     __FUNCTION__);
              this.points_be = points_be_; //0
              this.extra_points_be = 0; //1
              return;
             }
          
            this.points_be = points_be_; //0
            this.extra_points_be = extra_points_be_; //1
           }
          //+------------------------------------------------------------------+

          至此,固定点数盈亏平衡方法的基本实现已完成。下一节我们将利用上文风险管理中开发的 Order Blocks EA,对该类的功能进行测试。


          测试盈亏平衡模式

          本节我们将改造 Order Blocks EA,把固定点数盈亏平衡模式集成到其中。我们目标是验证系统运行是否正常,并观察持仓在这种主动管理模式下的表现。

          首先,修改EA的参数,新增一个专门的分组:

          sinput group "-----| Break Even |----"

          把所有与盈亏平衡机制相关的参数放到这个分组下。由于目前只实现了固定点数这一个模式,我们先添加一个bool参数,用于启用或禁用该功能:

          input bool use_be = true;                         // Enable Break Even usage

          接下来,定义一个子分组,用于存放该方法的特定参数:

          sinput group "- BreakEven based on Fixed Points -"
          input int be_fixed_points_to_put_be = 200;         // Points traveled needed to activate Break Even
          input int be_fixed_points_extra = 100;             // Extra adjustment points when activating Break Even
          

          为了集成该功能,我们创建一个 CBreakEvenSimple 类型的对象。后续会用一个更灵活的管理器对象替代它,以便通过单一接口管理多种不同版本。

          CBreakEvenSimple break_even(_Symbol, Magic, BREAKEVEN_MODE_AUTOMATIC);

          在 OnInit 函数中,如果启用了 use_be,则通过 SetSimple 函数为对象配置参数。

          //---
            if(use_be)
              break_even.SetSimple(be_fixed_points_to_put_be, be_fixed_points_extra);
          

          要确保盈亏平衡机制正常工作,需要在 OnTradeTransaction 中调用 break_even 对象的 OnTradeTransactionEvent 函数。该函数用于在对应的数组中添加或移除成交记录。

          //+------------------------------------------------------------------+
          //| TradeTransaction function                                        |
          //+------------------------------------------------------------------+
          void OnTradeTransaction(const MqlTradeTransaction& trans,
                                  const MqlTradeRequest& request,
                                  const MqlTradeResult& result)
           {
            risk.OnTradeTransactionEvent(trans);
            break_even.OnTradeTransactionEvent(trans);
           }
          

          最后,在 OnTick 中,只有用户启用了盈亏平衡机制时,才调用此函数:

          if(use_be) break_even.BreakEven();   

          经过这些修改,EA 便能在指定条件满足时自动启动盈亏平衡机制。 

          回测

          我们对启用盈亏平衡模式的 Order Blocks EA 进行了回测,测试未设置资金管理限制或盈亏上限。持仓仅在触及止损或止盈时平仓。风险回报比采用 1:2,即止盈是止损的两倍。

          1. 常规设置:

              图 4. 回测的通用设置

          2. 未启用盈亏平衡机制时的回测曲线。

          图例 5. 未启用盈亏平衡机制时的回测曲线

          图 5 展示了未启用盈亏平衡机制时 EA 的表现。测试期间,初始资金从 15,000 美元跌至 8,700 美元,亏损约 7,000 美元。该结果将作为评估盈亏平衡功能效果的基准。

          3. 启用盈亏平衡机制时的回测曲线。

          图例 6. 启用盈亏平衡机制时的回测曲线

          第二次测试中,盈亏平衡模式处于激活状态。参数 be_fixed_points_extra 设为 100 点,be_fixed_points_to_put_be 设为 600 点。

          测试期间,资金曲线增长相对放缓。在连续亏损的交易序列中,资金回撤幅度更小。余额从 10900 美元降至 7900 美元,亏损 3000 美元。与第一种场景相比,这 3000 美元的差异表明,使用盈亏平衡机制有助于减轻连续亏损对资金的冲击。

          不过,也暴露出一些局限性。两次测试中,交易在 2024 年 3 月之前普遍处于亏损状态。自 2024-03-01 起,情况开始转变,出现一系列盈利交易。在第一次回测中,余额突破 16000 美元;而在第二次启用盈亏平衡机制的回测中,仅达到 10900 美元。

          一种可能的解释是,盈亏平衡模式导致了过早平仓。若建仓后价格回撤但未触及止损,在无盈亏平衡机制的情况下,价格随后可能按预期方向运行并盈利离场。反之,若启用盈亏平衡机制,持仓将在该位置平仓,从而错失盈利机会。

          这表明,在第一种情境下,许多交易在触及止盈前曾处于亏损状态。因此,当价格频繁回调时,固定点数盈亏平衡模式的效果往往有限。

          在高波动行情下,该设置虽能迅速触发从而保护本金,但也可能导致交易过早平仓。而在市场清淡期,它又可能根本不会被触发。因此,对于那些更看重资金安全而非单笔高收益的交易者来说,固定点数盈亏平衡模式更为合适。


          结论

          本文深入探讨了盈亏平衡的概念、其在 MQL5 中的具体实现,以及几种不同的模式。所有开发的功能均已集成到我们在上一期风险管理系列文章中构建的“Order Blocks”EA 中。

          最后,我们对比了启用与未启用盈亏平衡模式时的系统表现。观察发现,在连续盈利阶段,启用该模式可能会减缓资金增长速度。而在连续亏损阶段,该模式则能有效降低大幅回撤的风险。这种两面性揭示了风险与收益之间的平衡关系。但其实际效果,最终取决于底层策略在不受外部限制(如盈亏限额)时的统计表现。

          我们还得出结论:对于那些不追求激进风格,而是青睐稳健收益曲线且希望对离场时机有更强掌控力的交易者来说,盈亏平衡模式更为适用。

          本文使用/更新的文件:

          文件名 类型 说明 
           Risk_Management.mqh  .mqh (头文件) 包含风险管理系列上一篇文章中开发的风险管理类。
           Order_Block_Indicador_New_Part_2.mq5 .mq5 (指标) 包含 Order Block 指标的代码。
           Order Block EA MetaTrader 5.mq5  EA.mq5 (EA) 集成了盈亏平衡模式的 Order Block EA 代码。
           OB_SET_WITHOUT_BREAKEVEN.set .set (配置文件) 第一次回测的设置文件,未启用盈亏平衡机制。
           OB_SET_WITH_BREAKEVEN.set .set (配置文件) 第二次回测的设置文件,已启用盈亏平衡模式。
           PositionManagement.mqh .mqh (头文件) 包含盈亏平衡机制代码的头文件。


          本文由MetaQuotes Ltd译自西班牙语
          原文地址: https://www.mql5.com/es/articles/17957

          附加的文件 |
          MQL5.zip (210.17 KB)
          交易策略 交易策略
          各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
          骆驼算法(CA) 骆驼算法(CA)
          骆驼算法(CA)于 2016 年被提出,该算法模拟沙漠中骆驼的行为特征来求解优化问题,同时考量温度、供给储备和耐力三大因素。本文还提出了该算法的改进版本(CAm),核心改进包括:在解的生成过程中引入高斯分布,并对绿洲效应参数进行优化。
          新手在交易中的10个基本错误 新手在交易中的10个基本错误
          新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
          基于分形的算法(FBA) 基于分形的算法(FBA)
          本文提出了一种新型元启发式算法,该算法基于分形思想对搜索空间进行划分,以求解优化问题。该算法通过逐步识别并分离有前景的区域,构建出自相似的分形结构,从而将计算资源集中到最有前景的搜索区域。其独特的、面向更优解的变异机制,有助于在搜索空间的全局探索与局部开发之间取得良好的平衡,显著提升了算法效率。