基于预定义的风险和风险/回报比建立互动式半自动拖放EA 交易(基础篇)
◍ 用拖放方式在MT5里搭半自动风控EA
在 MT5 里做半自动交易,不一定非要写完整策略逻辑。一种务实思路是:先定好单笔风险金额和风报比,再用交互式拖放把止损、止盈挂到图表上,由 EA 负责按预设比例下单。 这类 EA 的典型交互是——交易者在图表拖出一条线段代表止损距离,EA 读取长度并结合账户净值算出手数;同时按风险/回报比自动推算止盈位。2013 年 12 月发布的一版示例,历史查看量约 2416 次、评论 7 条,说明手动拖放+自动算仓的需求一直有人用。 外汇与贵金属杠杆品种波动剧烈,拖放设定的止损若过窄,遇数据行情可能被瞬时扫损;建议先在策略测试器用历史数据跑一遍,确认手数计算和风报比映射符合预期再上模拟盘。
「为什么要在图表上动态算风险回报」
做混合交易的不少人卡在一个点上:指标给信号,但仓位和止损还得切到计算器里手敲。外汇和贵金属杠杆高,手动算一旦慢半拍,滑点就可能把原本计划好的风险/回报比直接打穿。 我自己属于「指标辅助+人工确认」那一类,核心诉求很直接——在 MT5 图表上拉一条止损线,EA 立刻告诉我这一笔最多亏多少净值、对应手数该开多大,而不是盘后复盘才发现风险超标。 这套思路落地的关键,是把「最大净值风险」和「风险/回报比」做成 EA 面板上的运行时参数。改一下数字,图表上的止盈止损参考线就重算,省掉切窗口的摩擦。 需要提醒的是,这种半自动方式只是把计算和执行链缩短,并不消除外汇/贵金属固有的高波动风险,实际成交价可能因流动性在极端时段偏离预设水平。
EA 该有的交互与风控底线
一个能用于实盘验证的仓位管理 EA,首先要在启动阶段就允许预设风险水平,并且能在图表运行时直接拖动改值,实时看到仓位大小怎么变。外汇和贵金属杠杆高,这种即时反馈能避免凭感觉下重手。 风险回报比也得能在运行时改,而不是写死在参数里。同一套止损距离,1:1 和 1:3 对应的手数和潜在回撤完全不同,交易者需要当场对比。 给定风险和止损后,EA 要能算出当前可开的最大手数,而不是等报错才知道超了。同时手数本身也能拖动,资产净值的风险暴露和回报会随之一并刷新。 最后,EA 必须能直接发买卖市价单,且支持在图上拖放止损线,同步显示预设风险回报对应的价格位置。这样从测算到下单形成闭环,省去切窗口的误差。
◍ 用图表对象类搭可交互面板
这个 EA 需要在行情跑着的时候改参数、看状态,所以直接走 CChartObject 体系,把 GUI 画在图表窗口上接管鼠标输入,而不是塞进 EA 属性框。界面里要有标签、按钮和编辑框三类基本件,交易者开图就能点。 最早想用 CChartObjectPanel 把控件归组,后来换思路:自己写一个类,把标签、编辑字段、按钮都垫在一张背景图上面。背景图是 GIMP 画的,MQL5 这边只负责生成红色实时标签、可输入编辑框和按钮对象。 具体落法是先把标签摆上图表、记死坐标,再写 CRRDialog 类统一算输出显示、读 CChartObjectEdit 的入参、记按钮状态。风险回报那两块彩色矩形是 CChartObjectRectangle,能拖的止损箭头是 CChartObjectBitmap 位图对象——你在 MT5 里点开对象列表,能看到这几类对象实打实建在图上。
「EA 面板类怎么搭起来」
做 MT5 面板型 EA,界面层最好单独封一个类来管。把背景图、编辑框、标签、按钮各自用图表对象类接住,逻辑和画图就不会缠在一起。 这个类里用了 CChartObjectBmpLabel 当背景,CChartObjectEdit 承接风控比例和手数输入,CChartObjectLabel 负责显示买卖价、净值、SL/TP 等只读字段,CChartObjectButton 管切换订单类型和下单退出。CreateCRRDialog() 按基准坐标 m_baseX、m_baseY 把上述对象逐一 Create() 定位,标签和编辑框顺手设 Font()、FontSize()、Description(),按钮则补 BackColor() 定底色。 Refresh() 是面板活起来的关键:它用 CRRDialog 内部变量加上实时 Ask/Bid、账户净值、净值风险值去重写所有标签和按钮描述。实盘里若发现面板数字卡住,先查 Refresh() 是否被主循环按帧调用,而不是怀疑对象创建失败。 下面这段类声明可直接贴进 MT5 编辑器看字段布局,private 区塞了 12 个 double 变量(RRRatio、riskPercent、orderLots、SL、TP 等)加坐标与字体配置,public 区暴露 Create / Refresh / Set / Get 接口,验证时重点看 m_maxAllowedLots 和 m_orderEquityRisk 有没有在 Refresh 里被赋值。
class CRRDialog { class="kw">private: class="type">int m_baseX; class="type">int m_baseY; class="type">int m_fontSize; class="type">class="kw">string m_font; class="type">class="kw">string m_dialogName; class="type">class="kw">string m_bgFileName; class="type">class="kw">double m_RRRatio; class="type">class="kw">double m_riskPercent; class="type">class="kw">double m_orderLots; class="type">class="kw">double m_SL; class="type">class="kw">double m_TP; class="type">class="kw">double m_maxAllowedLots; class="type">class="kw">double m_maxTicksLoss; class="type">class="kw">double m_orderEquityRisk; class="type">class="kw">double m_orderEquityReward; ENUM_ORDER_TYPE m_orderType; CChartObjectBmpLabel m_bgDialog; CChartObjectEdit m_riskRatioEdit; CChartObjectEdit m_riskValueEdit; CChartObjectEdit m_orderLotsEdit; CChartObjectLabel m_symbolNameLabel; CChartObjectLabel m_tickSizeLabel; CChartObjectLabel m_maxEquityLossLabel; CChartObjectLabel m_equityLabel; CChartObjectLabel m_profitValueLabel; CChartObjectLabel m_askLabel; CChartObjectLabel m_bidLabel; CChartObjectLabel m_tpLabel; CChartObjectLabel m_slLabel; CChartObjectLabel m_maxAllowedLotsLabel; CChartObjectLabel m_maxTicksLossLabel; CChartObjectLabel m_orderEquityRiskLabel; CChartObjectLabel m_orderEquityRewardLabel; CChartObjectLabel m_orderTypeLabel; CChartObjectButton m_switchOrderTypeButton; CChartObjectButton m_placeOrderButton; CChartObjectButton m_quitEAButton; class="kw">public: class="type">void CRRDialog(); class=class="str">"cmt">// CRRDialog 构造函数 class="type">void ~CRRDialog(); class=class="str">"cmt">// CRRDialog 析构函数 class="type">bool CreateCRRDialog(class="type">int topX,class="type">int leftY); class="type">int DeleteCRRDialog(); class="type">void Refresh(); class="type">void SetRRRatio(class="type">class="kw">double RRRatio); class="type">void SetRiskPercent(class="type">class="kw">double riskPercent); class="type">class="kw">double GetRiskPercent(); class="type">class="kw">double GetRRRRatio(); class="type">void SetSL(class="type">class="kw">double sl); class="type">void SetTP(class="type">class="kw">double tp); class="type">class="kw">double GetSL(); class="type">class="kw">double GetTP();
对话框里把风险敞口算给交易者看
上面这组方法暴露了面板类的核心接口:SetMaxAllowedLots 限定单笔最大手数,SetMaxTicksLoss 管住每单允许的 tick 亏损上限,SetOrderEquityRisk / SetOrderEquityReward 则把权益回撤与预期回报直接绑进按钮逻辑。SwitchOrderType 和 GetOrderType 配合,能在多空之间一键翻转,不用退出面板重设。 CreateCRRDialog 里能看到真实的布局坐标:symbolNameLabel 落在 baseX+120、baseY+40,tickSizeLabel 在 +57,riskRatioEdit 在 +72 且宽 35 高 15。也就是说整个风险面板竖向每行约占 15~18 像素,字体统一 Verdana 8 号,适合在 1280×720 副屏上常驻。 m_maxEquityLossLabel 的描述直接用 AccountInfoDouble(ACCOUNT_EQUITY)*m_riskPercent/100.0 算出金额,比如权益 10000、风险百分比 2.0 时显示 200.00。外汇与贵金属杠杆高,这个数值只是当前账户权益下的理论最大回撤,实际滑点可能让亏损超出。 把这段贴进 MT5 的面板类,改 m_baseX / m_baseY 就能挪动面板锚点;想让风险行更紧凑,把 +90、+107、+122 的 Y 偏移各减 5 即可,但别忘了同步调 askLabel 的 +145。
class="type">void SetMaxAllowedLots(class="type">class="kw">double lots); class="type">void SetMaxTicksLoss(class="type">class="kw">double ticks); class="type">void SetOrderType(ENUM_ORDER_TYPE); class="type">void SwitchOrderType(); class="type">void ResetButtons(); ENUM_ORDER_TYPE GetOrderType(); class="type">void SetOrderLots(class="type">class="kw">double orderLots); class="type">class="kw">double GetOrderLots(); class="type">void SetOrderEquityRisk(class="type">class="kw">double equityRisk); class="type">void SetOrderEquityReward(class="type">class="kw">double equityReward); }; class="type">bool CRRDialog::CreateCRRDialog(class="type">int topX,class="type">int leftY) { class="type">bool isCreated=false; class="type">MqlTick current_tick; SymbolInfoTick(Symbol(),current_tick); m_baseX = topX; m_baseY = leftY; m_bgDialog.Create(class="num">0, m_dialogName, class="num">0, topX, leftY); m_bgDialog.BmpFileOn(m_bgFileName); m_symbolNameLabel.Create(class="num">0, "symbolNameLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">40); m_symbolNameLabel.Font("Verdana"); m_symbolNameLabel.FontSize(class="num">8); m_symbolNameLabel.Description(Symbol()); m_tickSizeLabel.Create(class="num">0, "tickSizeLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">57); m_tickSizeLabel.Font("Verdana"); m_tickSizeLabel.FontSize(class="num">8); m_tickSizeLabel.Description(DoubleToString(SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE), Digits())); m_riskRatioEdit.Create(class="num">0, "riskRatioEdit", class="num">0, m_baseX + class="num">120, m_baseY + class="num">72, class="num">35, class="num">15); m_riskRatioEdit.Font("Verdana"); m_riskRatioEdit.FontSize(class="num">8); m_riskRatioEdit.Description(DoubleToString(m_RRRatio, class="num">2)); m_riskValueEdit.Create(class="num">0, "riskValueEdit", class="num">0, m_baseX + class="num">120, m_baseY + class="num">90, class="num">35, class="num">15); m_riskValueEdit.Font("Verdana"); m_riskValueEdit.FontSize(class="num">8); m_riskValueEdit.Description(DoubleToString(m_riskPercent, class="num">2)); m_equityLabel.Create(class="num">0, "equityLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">107); m_equityLabel.Font("Verdana"); m_equityLabel.FontSize(class="num">8); m_equityLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),class="num">2)); m_maxEquityLossLabel.Create(class="num">0, "maxEquityLossLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">122); m_maxEquityLossLabel.Font("Verdana"); m_maxEquityLossLabel.FontSize(class="num">8); m_maxEquityLossLabel.Description(DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY)*m_riskPercent/class="num">100.0,class="num">2)); m_askLabel.Create(class="num">0, "askLabel", class="num">0, m_baseX + class="num">120, m_baseY + class="num">145);