跨平台的EA交易: 资金管理(基础篇)
📘

跨平台的EA交易: 资金管理(基础篇)

第 1/3 篇

「跨平台EA里的资金仓位该怎么管」

在 MT5 上写跨平台 EA 时,资金管理不能散落在各个交易函数里,否则换平台移植时极易出 bug。2017 年 7 月那篇示例里,作者把仓位计算抽成了独立的资金管理类,被社区翻阅了 17616 次,说明这是实盘迁移的高频痛点。 核心思路是先定义资金管理的基类,再派生出具体类型(如固定手数、百分比风险、固定杠杆倍数等),用一个容器统一持有这些对象。这样 EA 主逻辑只调用容器接口,不关心背后是哪种算法。 外汇和贵金属杠杆高、滑点跳空频繁,这类品种上用百分比风险类时,单笔亏损上限建议压在账户净值 1%–2% 概率更稳,但具体比例得你在 MT5 策略测试器里按品种波动调。

◍ 让EA自己算该下多少手

资金管理不是EA的附属装饰,而是决定下一次进场手数怎么来的核心模块。把这部分抽成独立类,EA就能在不同策略间复用同一套仓位逻辑,而不必每次重写。 原文给出的几个资金管理类,目标很直接:跨 MQL4 / MQL5 两套语言都能跑,且允许EA在运行时从可用方法列表里动态挑一个用。这意味着你写一次类,MT4 和 MT5 的EA都能直接挂。 对外汇、贵金属这类高杠杆品种,手数算错一次就可能把回撤打到不可控。把仓位计算交给经过验证的类,比在EA主循环里手写 if 要稳得多,也方便你之后只改一个参数就切换成另一种风控口径。

资金管理类的共同骨架

所有资金管理类都从 CMoney 往下走,而 CMoney 的父类是 CMoneyBase。这个类本身不干重活,只是把账户、品种、事件聚合器这些指针接好,给后续派生类一个统一的壳。 CMoneyBase 里真正在实战中要看的就三个方法:UpdateLotSize、OnLotSizeUpdated、Volume。其中 UpdateLotSize 是各资金管理算法分叉的地方——基类版本是虚方法,什么都不算,直接 return true,把计算逻辑完全留给子类去覆盖。 有时候算完手数还得顺手更新点内部变量供下次用,这时候就靠 OnLotSizeUpdated,它在 UpdateLotSize 里自动被调,你不用手动触发。 EA 端取手数别去直接调前面两个,只管调 Volume。Volume 会在自己代码里把 UpdateLotSize 和 OnLotSizeUpdated 串起来跑,返回最终手数。开 MT5 把下面这段基类声明丢进头文件,就能看清成员分布。

MQL5 / C++
class CMoneyBase : class="kw">public CObject
{
class="kw">protected:
  class="type">bool              m_active;
  class="type">class="kw">double            m_volume;
  class="type">class="kw">double            m_balance;
  class="type">class="kw">double            m_balance_inc;
  class="type">int               m_period;
  class="type">bool              m_equity;
  class="type">class="kw">string            m_name;
  CSymbolManager   *m_symbol_man;
  CSymbolInfo      *m_symbol;
  CAccountInfo     *m_account;
  CEventAggregator *m_event_man;
  CObject          *m_container;
class="kw">public:
                     CMoneyBase(class="type">void);
                    ~CMoneyBase(class="type">void);
  class="kw">virtual class="type">int       Type(class="type">void) class="kw">const {class="kw">return CLASS_TYPE_MONEY;}
  class=class="str">"cmt">//--- 初始化
  class="kw">virtual class="type">bool      Init(CSymbolManager*,CAccountInfo*,CEventAggregator*);
  class="type">bool              InitAccount(CAccountInfo*);
  class="type">bool              InitSymbol(CSymbolManager*);
  CObject          *GetContainer(class="type">void);
  class="type">void              SetContainer(CObject*);
  class="kw">virtual class="type">bool      Validate(class="type">void);
  class=class="str">"cmt">//--- 取得和设置属性
  class="type">bool              Active(class="type">void) class="kw">const;
  class="type">void              Active(class="kw">const class="type">bool);
  class="type">void              Equity(class="kw">const class="type">bool);
  class="type">bool              Equity(class="type">void) class="kw">const;
  class="type">void              LastUpdate(class="kw">const class="type">class="kw">datetime);
  class="type">class="kw">datetime          LastUpdate(class="type">void) class="kw">const;
  class="type">void              Name(class="kw">const class="type">class="kw">string);
  class="type">class="kw">string            Name(class="type">void) class="kw">const;
  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);
  class="type">void              Volume(class="kw">const class="type">class="kw">double);

「仓位基类里的手数边界钳制」

CMoneyBase 把手数计算收口在 Volume() 这一个公开方法里,外部只传品种、报价、订单类型和止损价,类内部决定最终 m_volume。默认 UpdateLotSize() 直接返回 true,等于每次询价都允许重算,但什么都没改。 真正的边界保护在 OnLotSizeUpdated():先从管理对象拿当前 CSymbol 实例,再取 LotsMax() 与 LotsMin(),低于最小手数直接抬到最小,高于最大手数直接压到最大。MT5 里这两个值由券商下发,比如 XAUUSD 常见 minvol=0.01、maxvol=50,越界赋值会被交易服务器拒单,这里提前钳制能少踩坑。 Volume() 开头先查 Active(),不激活直接返 0,避免拿不到品种时算出脏手数。外汇与贵金属杠杆高、波动剧烈,手数越界可能触发保证金暴增,实盘前务必在策略测试器里打印 m_volume 确认钳制生效。

MQL5 / C++
class="type">class="kw">double                Volume(class="type">void) class="kw">const;
class="kw">protected:
  class="kw">virtual class="type">void     OnLotSizeUpdated(class="type">void);
  class="kw">virtual class="type">bool     UpdateLotSize(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);
  };
class="type">bool CMoneyBase::UpdateLotSize(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)
  {
   class="kw">return true;
  }
class="type">void CMoneyBase::OnLotSizeUpdated(class="type">void)
  {
   m_symbol=m_symbol_man.Get();
   class="type">class="kw">double maxvol=m_symbol.LotsMax();
   class="type">class="kw">double minvol=m_symbol.LotsMin();
   if(m_volume<minvol)
      m_volume=minvol;
   if(m_volume>maxvol)
      m_volume=maxvol;
  }
class="type">class="kw">double CMoneyBase::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)
  {
   if(!Active())
      class="kw">return class="num">0;
   if(UpdateLotSize(symbol,price,type,sl))
      OnLotSizeUpdated();
   class="kw">return m_volume;
  }

常见问题

把账户权益、单笔风险比例和止损点数做成输入参数,用风险金额除以每点价值得出手数,再交给仓位类做边界钳制。
至少要有计算手数、校验最小/最大手数限制、以及按品种合约大小换算风险敞口的几个方法,形成统一骨架。
可以,把EA代码或手数计算片段发给小布,它能指出边界钳制缺失、除零风险等常见坑,并给修正建议。
防止计算出负手数、低于经纪商最小手数或超出最大允许手数导致下单失败,属于必做的防御性代码。
读取合约大小、报价精度和当前汇率换算成账户币种点值,封装成函数供手数计算调用,避免硬编码。