跨平台的EA交易: 资金管理·进阶篇
⚙️

跨平台的EA交易: 资金管理·进阶篇

(2/3)· 固定手数到固定风险只是起点,让EA在MQL4/MQL5里自动切换算法才是真功夫

实战向 第 2/3 篇

很多跨平台EA写到手数计算就写死固定值,账户浮亏时仍重仓,回撤一下就爆。把资金管理方法硬编进主逻辑,换个策略就得重写一遍,维护成本极高。

◍ 手数怎么算:从固定到固定比例的五种路子

固定手数最直白,所有单子交易量一致,不随净值波动。CMoneyFixedLotBase 的构造函数直接吃一个 volume 参数写死 m_volume,想动态改就调 Volume() 改类成员,没有余额耦合。 固定分数(固定风险)按账户余额的某个百分比去承担最大止损亏损,公式 Volume = (balance * account_percentage / ticks) / tick_value。举例:美元账户余额 1000、风险 5%、5 位小数经纪商 tick_value 0.1、200 点止损,算出来就是 2.5 手。它强依赖非零止损,止损过小易算出超大手数,MT4 得手动拆单,MT5 可自动拆。外汇和贵金属杠杆高,这种算法在止损拉很大时手数会骤降,策略若无止损上限就不适合。 固定比例不要求止损,手数随余额阈值跳:Volume = base_volume + (balance / balance_increase) * volume_increment。账户 2500、每满 1000 加 0.1 手、base 为 0,得出 0.25 手;也可改成仅在特定余额水平更新,比如撑到 3000 才从 0.2 升 0.3。 每点固定风险把止损每点折成存款金额,比如每点 $2、200 点止损最大亏 $400,风险随止损变。固定保证金是它的特例:不论止损 200 点还是 100 点,最大风险锁死 $400,只是把每点公式再除以止损 tick 数。 下面这段是固定分数与固定比例更新手数的基类代码,注意前者用 m_risk/100 且必须传 sl,后者 sl 可缺省。

MQL5 / C++
CMoneyFixedLotBase::CMoneyFixedLotBase(class="type">class="kw">double volume)
  {
   Volume(volume);
  }
class="type">bool CMoneyFixedFractionalBase::UpdateLotSize(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double sl)
  {
   m_symbol=m_symbol_man.Get(symbol);
   class="type">class="kw">double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      class="type">class="kw">double balance=m_equity==class="kw">false?m_account.Balance():m_account.Equity();
      class="type">class="kw">double ticks=class="num">0;
      if(price==class="num">0.0)
        {
         if(type==ORDER_TYPE_BUY)
            ticks=MathAbs(m_symbol.Bid()-sl)/m_symbol.TickSize();
         else if(type==ORDER_TYPE_SELL)
            ticks=MathAbs(m_symbol.Ask()-sl)/m_symbol.TickSize();
        }
      else ticks=MathAbs(price-sl)/m_symbol.TickSize();
      m_volume=((balance*(m_risk/class="num">100))/ticks)/m_symbol.TickValue();
     }
   class="kw">return NormalizeDouble(last_volume-m_volume,class="num">2)==class="num">0;
  }
class="type">bool CMoneyFixedRatioBase::UpdateLotSize(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double sl=class="num">0)
  {
   m_symbol=m_symbol_man.Get(symbol);
   class="type">class="kw">double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {

「按账户权益与风险额动态算仓的三类实现」

下面三段 MQL5 代码分别对应三种仓位计算逻辑,都封装在 UpdateLotSize 里,返回值是「本次手数与上次的差异是否小于 0.01(NormalizeDouble 到 2 位后差为 0)」,用来判断要不要重算下单量。 第一段(无命名类片段)走的是阶梯加仓思路:以 balance 为基准,每跨过 m_balance_inc 的权益门槛就加 m_volume_inc 手,基础手数是 m_volume_base。注意 m_equity 开关决定用 Balance 还是 Equity,前者不含浮盈、后者含。 double balance=m_equity==false?m_account.Balance():m_account.Equity(); m_volume=m_volume_base+((int)(balance/m_balance_inc))*m_volume_inc; m_balance=balance; return NormalizeDouble(last_volume-m_volume,2)==0; 第二段 CMoneyFixedRiskPerPointBase 更简单粗暴:手数直接等于 m_risk 除以当前品种 TickValue,即「每点固定风险金额」模型,没用止损距离。外汇与贵金属杠杆高,TickValue 随合约规模和报价精度变,跑之前先在 MT5 用 SymbolInfoDouble(sym,SYMBOL_TRADE_TICK_VALUE) 打印核对。 第三段 CMoneyFixedRiskBase 才是带止损的:先算价格到 sl 的 tick 数(price 为 0 时自动取 Bid/Ask),再用 m_risk 除以「每 tick 价值 × tick 数」得出手数。黄金 XAUUSD 在常规账户下 TickSize 常是 0.01,若 sl 距入场 500 点就是 50000 ticks,手数会显著缩小,概率上更抗单笔回撤。 把这三段直接贴进你的 EA 资金类里,改 m_risk / m_balance_inc 等参数,就能在策略测试器里对比权益曲线陡峭度。

MQL5 / C++
class="type">class="kw">double balance=m_equity==class="kw">false?m_account.Balance():m_account.Equity();
   m_volume=m_volume_base+((class="type">int)(balance/m_balance_inc))*m_volume_inc;
   m_balance=balance;
   }
   class="kw">return NormalizeDouble(last_volume-m_volume,class="num">2)==class="num">0;
  }
class="type">bool CMoneyFixedRiskPerPointBase::UpdateLotSize(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double sl=class="num">0)
  {
   m_symbol=m_symbol_man.Get(symbol);
   class="type">class="kw">double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      class="type">class="kw">double balance=m_equity==class="kw">false?m_account.Balance():m_account.Equity();
      m_volume=(m_risk/m_symbol.TickValue());
     }
   class="kw">return NormalizeDouble(last_volume-m_volume,class="num">2)==class="num">0;
  }
class="type">bool CMoneyFixedRiskBase::UpdateLotSize(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double sl)
  {
   m_symbol=m_symbol_man.Get(symbol);
   class="type">class="kw">double last_volume=m_volume;
   if(CheckPointer(m_symbol))
     {
      class="type">class="kw">double balance=m_equity==class="kw">false?m_account.Balance():m_account.Equity();
      class="type">class="kw">double ticks=class="num">0;
      if(price==class="num">0.0)
        {
         if(type==ORDER_TYPE_BUY)
            ticks=MathAbs(m_symbol.Bid()-sl)/m_symbol.TickSize();
         else if(type==ORDER_TYPE_SELL)
            ticks=MathAbs(m_symbol.Ask()-sl)/m_symbol.TickSize();
        }
      else ticks=MathAbs(price-sl)/m_symbol.TickSize();
      m_volume=(m_risk/m_symbol.TickValue())/ticks;
     }
   class="kw">return NormalizeDouble(last_volume-m_volume,class="num">2)==class="num">0;
  }

让EA在多种仓位算法间热切换

做多策略EA时,固定一种仓位算法往往不够用:震荡市想用固定比例,趋势市想用波动率加权。把这些算法塞进一个容器,EA就能在运行时挑着用,而不是改代码重编译。 这个容器的基类叫 CMoneysBase,它直接继承 CArrayObj,也就是说本质是个对象数组,里面每一项都是一个具体的资金管理实例。中介层只暴露「选谁」和「算多少手」两件事给EA主逻辑,主循环完全不用关心背后是哪一种算法。 选择动作做了三次重载。整数参数版按数组索引直接定位;字符串参数版遍历 Total() 个元素,靠 Name() 比对命中后回写索引并返回 true,找不到就返回 false,所以给你的资金对象起非空名字不是可选项而是前提;无参版只吐回当前 m_selected 的值,方便日志里打印「我现在跑的是第几套」。 真正下单手数走 Volume 方法:先 At(m_selected) 拿指针,再调具体对象的 Volume。这里有个坑——如果数组里对应位置是空元素,指针校验没过就直接算会崩,EA挂实盘前务必在策略测试器里把索引越界和空指针两种情形都跑一遍。外汇与贵金属杠杆高,仓位算错可能瞬间吞掉本金,任何动态切换都要先在模拟盘验证。

MQL5 / C++
class CMoneysBase : class="kw">public CArrayObj
  {
class="kw">protected:
   class="type">bool                 m_active;
   class="type">int                  m_selected;
   CEventAggregator *m_event_man;
   CObject             *m_container;
class="kw">public:
                     CMoneysBase(class="type">void);
                    ~CMoneysBase(class="type">void);
   class="kw">virtual class="type">int        Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_MONEYS;}
   class=class="str">"cmt">//--- 初始化
   class="kw">virtual class="type">bool       Init(CSymbolManager*,CAccountInfo*,CEventAggregator*);
   CObject            *GetContainer(class="type">void);
   class="type">void               SetContainer(CObject*);
   class="kw">virtual class="type">bool       Validate(class="type">void) class="kw">const;
   class=class="str">"cmt">//--- 读取和设置函数
   class="kw">virtual class="type">bool       Active(class="type">void) class="kw">const;
   class="kw">virtual class="type">void       Active(class="kw">const class="type">bool);
   class="kw">virtual class="type">int        Selected(class="type">void) class="kw">const;
   class="kw">virtual class="type">void       Selected(class="kw">const class="type">int);
   class="kw">virtual class="type">bool       Selected(class="kw">const class="type">class="kw">string);
   class=class="str">"cmt">//--- 交易量的计算
   class="kw">virtual class="type">class="kw">double     Volume(class="kw">const class="type">class="kw">string,class="kw">const class="type">class="kw">double,class="kw">const ENUM_ORDER_TYPE,class="kw">const class="type">class="kw">double);
   };
CMoneysBase::Selected(class="kw">const class="type">int value)
  {
   m_selected=value;
  }
class="type">bool CMoneysBase::Selected(class="kw">const class="type">class="kw">string select)
  {
   for(class="type">int i=class="num">0;i<Total();i++)
     {
      CMoney *money=At(i);
      if(!CheckPointer(money))
        class="kw">continue;
      if(StringCompare(money.Name(),select))
        {
         Selected(i);
         class="kw">return true;
        }
     }
   class="kw">return class="kw">false;
  }
class="type">int CMoneysBase::Selected(class="type">void) class="kw">const
  {
   class="kw">return m_selected;
  }

◍ 按选中风控对象算下单手数

CMoneysBase 的 Volume 方法把具体计算委托给当前选中的 CMoney 实例,自己只做指针校验和转发。调用方传入品种、价格、订单类型与止损价,方法返回该风控模块算出的手数;若指针无效直接回 0,避免空指针崩在交易前。 实际在 MT5 里跑这套,m_selected 指向的风控类必须已实现 Volume 重载,否则永远拿到 0 手。下面这段是类内转发的核心,逐行看逻辑很薄:先取实例,校验指针,通过就转发四个参数,否则返回 0。

MQL5 / C++
class="type">class="kw">double CMoneysBase::Volume(class="kw">const class="type">class="kw">string symbol,class="kw">const class="type">class="kw">double price,class="kw">const ENUM_ORDER_TYPE type,class="kw">const class="type">class="kw">double sl=class="num">0)
  {
   CMoney *money=At(m_selected);
   if(CheckPointer(money))
      class="kw">return money.Volume(symbol,price,type,sl);
   class="kw">return class="num">0;
  }
把重复劳动交给小布
这些诊断小布盯盘的AIGC已内置,打开对应品种页即可看到EA当前调用的资金管理模式与实时手数推导,你只管判断该不该切算法。

常见问题

它作为虚方法占位,子类重写后才能真正算量;基类返回true只是保证调用链不中断,实际差异全在派生类。
该方法依赖非零止损算最大亏损,止损为0时手数计算会失效或报错,概率上导致不下单或沿用旧值。
可以,小布的品种页会解析EA运行态并标注当前Volume方法来源,比如CMoneyFixedRisk还是固定手数,省去你翻日志。
文中容器设计兼容双平台,调用方通过同一基类指针取Volume,底层差异被封装,迁移时不必改业务代码。