从头开始开发智能交易系统(第 30 部分):CHART TRADE 当作指标?·进阶篇
(2/3)· 当 EA 只留订单系统,市价党如何不丢图表交易?把 Chart Trade 重做成指标就对了
◍ 用全局变量搭起指标与EA的内部对话
想让 Chart Trade 指标真正能发单、平仓、回报结果,关键是在 MT5 进程内搭一套客户端-服务器式的数据通道。这里不靠 DLL,而是直接借用 MT5 的全局变量——它们最好导航、易维护,也方便按资产隔离。 具体分工上,把挂载在图表上的 EA 设为服务器更实用:EA 常驻,Chart Trade 只在需要时浮现。EA 负责建 5 个全局变量里的 4 个(杠杆、止盈、止损、当日结果),剩下 1 个由 Chart Trade 建,用来传“按了哪个按钮”的状态。 每种资产用 5 个变量,变量名拼了 _Symbol 前缀,所以同一图表能同时跑多品种。EA 建了杠杆变量后,指标初始化时若读不到它直接 INIT_FAILED;EA 撤掉该变量,指标也跟着被删。检查点有两个:指标初始化时,以及图表有任何事件时。 别把 OnTime 塞进指标里。所有指标共享同一条线程,一个指标里的定时事件可能拖慢其它指标。真担心性能损耗再考虑挪进去,但得清楚后果。 平仓回报这块有个细节:EA 每次平仓建一个 _Result 变量,Chart Trade 观测到同名变量立刻取值并删除,避免 EA 又发新更新把上一条冲掉。取值在删除前完成,所以不会丢。 绿色代码对应指标形态,它把按钮状态写进全局变量。点平仓就写 0x10;市价单则组合 0x04/0x08(买卖)加 0x01/0x02(日内或短线)。EA 靠这个值决定动作。 这个系统是自我覆盖的:卖出标志写出后、EA 未响应前又点买入,EA 最终只认买入;有持仓时点平仓再按取消,持仓也会被平掉。外汇和贵金属波动大、杠杆高,这种竞态在手动快点的场景里容易误触发,实盘前务必在策略测试器里复现。
class="macro">#class="kw">property copyright "Daniel Jose" class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_GlobalVariableLeverage(_Symbol + "_Leverage") class="macro">#define def_GlobalVariableTake(_Symbol + "_Take") class="macro">#define def_GlobalVariableStop(_Symbol + "_Stop") class="macro">#define def_GlobalVariableResult(_Symbol + "_Result") class="macro">#define def_GlobalVariableButton(_Symbol + "_ButtonState") class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_ButtonDTSelect 0x01 class="macro">#define def_ButtonSWSelect 0x02 class="macro">#define def_ButtonBuyMarket 0x04 class="macro">#define def_ButtonSellMarket 0x08 class="macro">#define def_ButtonClosePosition 0x10 class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#define def_SHORTNAME "CHART TRADE" class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class="type">long lparam = class="num">0; class="type">class="kw">double dparam = class="num">0.0; class="type">class="kw">string sparam = ""; IndicatorSetString(INDICATOR_SHORTNAME, def_SHORTNAME); if(!GlobalVariableGet(def_GlobalVariableLeverage, dparam)) class="kw">return INIT_FAILED; Terminal.Init();
「图表交互事件的派发与下单链路」
这段逻辑展示了 MT5 指标/EA 里如何把图表上的按钮点击转成实盘下单指令。核心在 OnChartEvent 收到 CHARTEVENT_OBJECT_CLICK 后,先过滤掉非自身前缀的对象名(def_HeaderMSG),避免误触其他图形元素。 过滤后截取第 9 个字符之后的子串并转大写,再判断是否是买/卖按钮。若 sparam 等于 szMsgIDE[eBTN_BUY] 或 szMsgIDE[eBTN_SELL],就调用 TradeView.ExecuteOrderInMarket,传入杠杆、止盈、止损、买卖方向和是否日内交易等参数。 初始化阶段也有坑:Chart.InitilizeChartTrade 用 dparam * Terminal.GetVolumeMinimal() 计算手数,止盈止损取自全局变量。若 GlobalVariableCheck(def_GlobalVariableLeverage) 不成立,直接 OnDeinit(REASON_INITFAILED),并打印『Unable to use Chart Trade…』提示 EA 不在该品种图表上。 外汇与贵金属杠杆交易风险极高,这段代码只解决『按钮到订单』的通道,不保证任何胜率;开 MT5 把 def_HeaderMSG 和 szMsgIDE 数组打印出来,就能确认你图表上的对象名是否对得上。
Chart.AddThese("IDE(,,class="num">170, class="num">215)"); Chart.InitilizeChartTrade(dparam * Terminal.GetVolumeMinimal(), GlobalVariableGet(def_GlobalVariableTake), GlobalVariableGet(def_GlobalVariableStop), true); OnChartEvent(CHARTEVENT_OBJECT_ENDEDIT, lparam, dparam, sparam); class="kw">return INIT_SUCCEEDED; } class="type">void OnChartEvent(const class="type">int id, const class="type">long &lparam, const class="type">class="kw">double &dparam, const class="type">class="kw">string &sparam) { if (!GlobalVariableCheck(def_GlobalVariableLeverage)) OnDeinit(REASON_INITFAILED); Chart.DispatchMessage(id, lparam, dparam, sparam); } class="type">void OnDeinit(const class="type">int reason) { if (reason == REASON_INITFAILED) { Print("Unable to use Chart Trade. The EA is not on the chart of this asset..."); if (!ChartIndicatorDelete(class="num">0, class="num">0, def_SHORTNAME)) Print("Unable to class="kw">delete Chart Trade from the chart."); } } class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">int begin, const class="type">class="kw">double &price[]) { class="type">class="kw">double value; if (GlobalVariableGet(def_GlobalVariableResult, value)) { GlobalVariableDel(def_GlobalVariableResult); Chart.DispatchMessage(CHARTEVENT_CHART_CHANGE, class="num">0, value, C_Chart_IDE::szMsgIDE[C_Chart_IDE::eROOF_DIARY]); } class="kw">return rates_total; } class=class="str">"cmt">// ... Previous code ... case CHARTEVENT_OBJECT_CLICK: if (StringSubstr(sparam, class="num">0, StringLen(def_HeaderMSG)) != def_HeaderMSG) { Resize(-class="num">1); class="kw">return; } sparam = StringSubstr(sparam, class="num">9, StringLen(sparam)); StringToUpper(sparam); class="macro">#ifdef def_INTEGRATION_CHART_TRADER if ((sparam == szMsgIDE[eBTN_SELL]) || (sparam == szMsgIDE[eBTN_BUY])) TradeView.ExecuteOrderInMarket(m_BaseFinance.Leverange, m_BaseFinance.FinanceTake, m_BaseFinance.FinanceStop, sparam == szMsgIDE[eBTN_BUY], m_BaseFinance.IsDayTrade); if (sparam == szMsgIDE[eBTN_CANCEL]) { TradeView.CloseAllsPosition();
按钮事件里的联合体打包技巧
在 MT5 面板里处理 BUY / SELL / CANCEL 按钮点击时,这段非 MQL5 编译分支用了一个 union 把 double 和 ulong 叠在同一块内存上,靠 GlobalVariableSet 把指令甩给 EA 主循环。
具体看,u_local.c 先清零;若 sparam 匹配买按钮,就按日内或波段标记(def_ButtonDTSelect / def_ButtonSWSelect)加上市价买常量;卖按钮同理换常量;取消按钮则写 def_ButtonClosePosition 并顺手把取消键的 OBJPROP_STATE 置 false,避免界面卡在按下态。
最后一行 if (u_local.Value > 0) GlobalVariableSet(def_GlobalVariableButton, u_local.Value); 是真正触发点——只有打包出非零值才写全局变量,EA 侧读这个双精度数再拆回 ulong 就能知道用户点了什么。外汇与贵金属杠杆高,这类面板误操作可能瞬间触发实盘下单,建议在策略测试器里先跑通事件链路再接真账户。
下面把核心片段逐行拆一下,方便你直接抄进自己的面板类:
union u00 { double Value; ulong c; } u_local; — 声明联合体,Value 和 c 共用 8 字节。
u_local.c = 0; — 指令码先清白。
if (sparam == szMsgIDE[eBTN_BUY]) u_local.c = (m_BaseFinance.IsDayTrade ? def_ButtonDTSelect : def_ButtonSWSelect) + def_ButtonBuyMarket; — 买按钮按交易周期拼出指令。
else if (sparam == szMsgIDE[eBTN_SELL]) u_local.c = ... + def_ButtonSellMarket; — 卖按钮同理。
else if (sparam == szMsgIDE[eBTN_CANCEL]) { u_local.c = def_ButtonClosePosition; ObjectSetInteger(... OBJPROP_STATE, false); } — 取消平仓并复位按钮。
if (u_local.Value > 0) GlobalVariableSet(def_GlobalVariableButton, u_local.Value); — 非零才广播,主 EA 轮询取值。
union u00
{
class="type">class="kw">double Value;
class="type">class="kw">ulong c;
}u_local;
u_local.c = class="num">0;
if (sparam == szMsgIDE[eBTN_BUY]) u_local.c = (m_BaseFinance.IsDayTrade ? def_ButtonDTSelect : def_ButtonSWSelect) + def_ButtonBuyMarket; else
if (sparam == szMsgIDE[eBTN_SELL]) u_local.c = (m_BaseFinance.IsDayTrade ? def_ButtonDTSelect : def_ButtonSWSelect) + def_ButtonSellMarket; else
if (sparam == szMsgIDE[eBTN_CANCEL])
{
u_local.c = def_ButtonClosePosition;
ObjectSetInteger(Terminal.Get_ID(), m_ArrObject[eBTN_CANCEL].szName, OBJPROP_STATE, false);
}
if (u_local.Value > class="num">0) GlobalVariableSet(def_GlobalVariableButton, u_local.Value);◍ 让 EA 接住 Chart Trade 的按钮信号
改 EA 接 Chart Trade 的消息,核心就一件事:在 EA 里建一套临时全局变量当信道,Chart Trade 点按钮写变量,EA 读变量执行。记住,EA 必须比 Chart Trade 先加载,否则指标初始化会失败;变量初值由 EA 给定,哪怕之前有历史交易,累积值也会重新灌进 Chart Trade。 变量要用 GlobalVariableTemp 声明成临时型。平台崩了或 EA 卸载,这些变量自动消失,不会在数据转储里留垃圾。但即便如此,OnDeinit 里仍要强制删一遍,确保 Chart Trade 脱离图表后不残留。 响应函数声明为 inline,编译器就地展开,跑得最快。放哪触发是个真问题:塞 OnTick 里,低波动品种跳价稀,点按钮可能半天才响应;塞 OnTime 里按频率跑才稳。OnTime 默认 1 秒一次,调用 EventSetMillisecondTimer(500) 可压到 500 毫秒,绝大多数场景够用。OnInit 加这行,OnDeinit 里关掉。 C_IndicatorTradeView 类补一行抓全局变量值的代码,Chart Trade 上的挂单参数就能进订单系统;所有挂单默认跟 EA 时间,想改直接图表上拖,不用走 Chart Trade。语音通知加进 C_Router,Chart Trade 手动开平也会响。外汇和贵金属杠杆高、滑点猛,这套通信若时序错乱可能漏单,实盘前务必在 MT5 策略测试器跑通再上。
class="kw">input class="type">int user20 = class="num">1; class=class="str">"cmt">//Leverage class="kw">input class="type">class="kw">double user21 = class="num">100; class=class="str">"cmt">//Take Profit class="kw">input class="type">class="kw">double user22 = class="num">81.74; class=class="str">"cmt">//Stop Loss class="kw">input class="type">bool EA_user23 = true; class=class="str">"cmt">//Day Trade ? class="type">int OnInit() { if (!TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) { Sound.PlayAlert(C_Sounds::TRADE_ALLOWED); class="kw">return INIT_FAILED; } Terminal.Init(); class="macro">#ifdef def_INTEGRATION_TAPE_READING VolumeAtPrice.Init(user32, user33, user30, user31); TimesAndTrade.Init(user41); EventSetTimer(class="num">1); class="macro">#endif Mouse.Init(user50, user51, user52); class="macro">#ifdef def_INTEGRATION_CHART_TRADER class="kw">static class="type">class="kw">string memSzUser01 = ""; if (memSzUser01 != user01) { Chart.ClearTemplateChart(); Chart.AddThese(memSzUser01 = user01); } Chart.InitilizeChartTrade(EA_user20 * Terminal.GetVolumeMinimal(), EA_user21, EA_user22, EA_user23); TradeView.Initilize(); OnTrade(); class="macro">#else GlobalVariableTemp(def_GlobalVariableLeverage);
「按钮触发与全局变量清理的收尾逻辑」
EA 在初始化末尾用 GlobalVariableTemp 先占位三个临时变量(Take、Stop、Result),再用 GlobalVariableSet 写入真实参数:杠杆 = user20 乘终端最小成交量,止盈止损分别取 user21、user22,最后把 TradeView.GetFinanceRoof() 的返回值塞进 Result 变量。 非图表交易集成模式下,OnDeinit 会逐个 GlobalVariableDel 清掉 Leverage、Take、Stop、Result、Button 五个全局变量,避免残留数据干扰下一次加载;若开了 TAPE_READING 宏则额外 EventKillTimer 停掉定时器。 ChartTrade_ClickButton 用 union 把 double 和 ulong 复用同一块内存,读取 Button 变量后判断低位标志:等于 def_ButtonClosePosition 就平掉所有仓位,否则按 BuyMarket / DTSelect 位域调用 ExecuteOrderInMarket 下市价单,下单后重新 Initilize 刷新状态。 直接拷这段到 MT5 的 EA 源码里,把 user20~user22 换成你的手数系数与点数,编译后点面板按钮即可验证下单与全局变量读写是否如预期;外汇与贵金属杠杆品种波动剧烈,实盘前务必先用策略测试器跑一遍。
GlobalVariableTemp(def_GlobalVariableTake); GlobalVariableTemp(def_GlobalVariableStop); GlobalVariableTemp(def_GlobalVariableResult); GlobalVariableSet(def_GlobalVariableLeverage, user20 * Terminal.GetVolumeMinimal()); GlobalVariableSet(def_GlobalVariableTake, user21); GlobalVariableSet(def_GlobalVariableStop, user22); TradeView.Initilize(); GlobalVariableSet(def_GlobalVariableResult, TradeView.GetFinanceRoof()); class="macro">#endif class="kw">return INIT_SUCCEEDED; } class="type">void OnDeinit(const class="type">int reason) { Mouse.Destroy(); TradeView.Finish(); class="macro">#ifndef def_INTEGRATION_CHART_TRADER GlobalVariableDel(def_GlobalVariableLeverage); GlobalVariableDel(def_GlobalVariableTake); GlobalVariableDel(def_GlobalVariableStop); GlobalVariableDel(def_GlobalVariableResult); GlobalVariableDel(def_GlobalVariableButton); class="macro">#endif class="macro">#ifdef def_INTEGRATION_TAPE_READING EventKillTimer(); class="macro">#endif } class="kw">inline class="type">void ChartTrade_ClickButton(class="type">void) { union u00 { class="type">class="kw">double Value; class="type">class="kw">ulong c; }u_local; if (GlobalVariableGet(def_GlobalVariableButton, u_local.Value)) { GlobalVariableDel(def_GlobalVariableButton); if (u_local.c == def_ButtonClosePosition) TradeView.CloseAllsPosition(); else TradeView.ExecuteOrderInMarket(GlobalVariableGet(def_GlobalVariableLeverage), GlobalVariableGet(def_GlobalVariableTake), GlobalVariableGet(def_GlobalVariableStop), ((u_local.c & def_ButtonBuyMarket) == def_ButtonBuyMarket), ((u_local.c & def_ButtonDTSelect) == def_ButtonDTSelect)); TradeView.Initilize(); } } class="macro">#else GlobalVariableTemp(def_GlobalVariableLeverage); GlobalVariableTemp(def_GlobalVariableTake); GlobalVariableTemp(def_GlobalVariableStop); GlobalVariableTemp(def_GlobalVariableResult); GlobalVariableSet(def_GlobalVariableLeverage, user20 * Terminal.GetVolumeMinimal()); GlobalVariableSet(def_GlobalVariableTake, user21);