跨平台的EA交易: 资金管理·进阶篇
(2/3)· 固定手数到固定风险只是起点,让EA在MQL4/MQL5里自动切换算法才是真功夫
很多跨平台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 可缺省。
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 等参数,就能在策略测试器里对比权益曲线陡峭度。
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挂实盘前务必在策略测试器里把索引越界和空指针两种情形都跑一遍。外汇与贵金属杠杆高,仓位算错可能瞬间吞掉本金,任何动态切换都要先在模拟盘验证。
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。
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; }