跨平台的EA交易: 资金管理(基础篇)
「跨平台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 把下面这段基类声明丢进头文件,就能看清成员分布。
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 确认钳制生效。
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; }