从头开始开发智能交易系统(第 19 部分):新订单系统 (II)·综合运用
「挂单箭头的清理与价格轴联动」
在 MT5 图表上做可视化交易辅助时,删对象前先关掉 CHART_EVENT_OBJECT_DELETE 事件,能避免误触发删除回调导致递归崩图;处理完再打开。下面这段就是典型的清理逻辑:先禁事件,按指标类型删对应 MountName 对象,IT_PRICE 要连带删 IT_TAKE、IT_STOP,最后恢复事件开关。 ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it != NULL) && (it != IT_PRICE)) ObjectDelete(Terminal.Get_ID(), MountName(ticket, it)); else { ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_PRICE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_TAKE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_STOP)); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); PositionAxlePrice 负责把价格线拖到指定价位,并依据买卖方向与杠杆重算止盈止损偏移。ad 用 AdjustToTrade 除以(杠杆×最小交易量)得出每点金额步长,IT_PRICE 变动时同步移动 TAKE/STOP 线,多单止盈加 ad、止损减 ad,空单反之。 inline void PositionAxlePrice(double price, ulong ticket, eIndicatorTrade it, int FinanceTake, int FinanceStop, int Leverange, bool isBuy) { double ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal()); ObjectMove(Terminal.Get_ID(), MountName(ticket, it), 0, 0, price); if (it == IT_PRICE) { ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_TAKE), 0, 0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad)))); ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_STOP), 0, 0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad))); } } GetDisplacement 用基础金融参数算某合约量下止盈或止损相对开仓价的位移:i0 取 FinanceTake 或 FinanceStop,i1 按买卖与类型给正负号,返回 AdjustPrice 后的价格差。InitBaseFinance 里三行绿底代码创建了三种指标线对象(价格/止盈/止损)且都不立即绘制,方便后续 MoveTo 动态拖拽。外汇与贵金属波动剧烈,这类辅助线仅作参考,实际滑点可能让成交价偏离轴线。
ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, false); if ((it != NULL) && (it != IT_PRICE)) ObjectDelete(Terminal.Get_ID(), MountName(ticket, it)); else { ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_PRICE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_TAKE)); ObjectDelete(Terminal.Get_ID(), MountName(ticket, IT_STOP)); } ChartSetInteger(Terminal.Get_ID(), CHART_EVENT_OBJECT_DELETE, true); class="kw">inline class="type">void PositionAxlePrice(class="type">class="kw">double price, class="type">ulong ticket, eIndicatorTrade it, class="type">int FinanceTake, class="type">int FinanceStop, class="type">int Leverange, class="type">bool isBuy) { class="type">class="kw">double ad = Terminal.GetAdjustToTrade() / (Leverange * Terminal.GetVolumeMinimal()); ObjectMove(Terminal.Get_ID(), MountName(ticket, it), class="num">0, class="num">0, price); if (it == IT_PRICE) { ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_TAKE), class="num">0, class="num">0, price + Terminal.AdjustPrice(FinanceTake * (isBuy ? ad : (-ad)))); ObjectMove(Terminal.Get_ID(), MountName(ticket, IT_STOP), class="num">0, class="num">0, price + Terminal.AdjustPrice(FinanceStop * (isBuy ? (-ad) : ad))); } } class="kw">inline class="type">class="kw">double GetDisplacement(const class="type">bool IsBuy, const class="type">class="kw">double Vol, eIndicatorTrade it) const { class="type">int i0 = (it == IT_TAKE ? m_BaseFinance.FinanceTake : m_BaseFinance.FinanceStop), i1 = (it == IT_TAKE ? (IsBuy ? class="num">1 : -class="num">1) : (IsBuy ? -class="num">1 : class="num">1)); class="kw">return (Terminal.AdjustPrice(i0 * (Vol / m_BaseFinance.Leverange) * Terminal.GetAdjustToTrade() / Vol) * i1); } class="type">void InitBaseFinance(class="type">int nContracts, class="type">int FinanceTake, class="type">int FinanceStop, class="type">bool b1) { SetBaseFinance(nContracts, FinanceTake, FinanceStop, b1); CreateIndicatorTrade(def_IndicatorTicket0, IT_PRICE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_TAKE, false); CreateIndicatorTrade(def_IndicatorTicket0, IT_STOP, false); } class="kw">inline class="type">void MoveTo(class="type">uint Key) { class="kw">static class="type">class="kw">double local = class="num">0; class="type">class="kw">datetime dt; class="type">bool bEClick, bKeyBuy, bKeySell, bCheck; class="type">class="kw">double take = class="num">0, stop = class="num">0, price; }
◍ 用位运算把鼠标和键盘捏成挂单触发器
这段逻辑核心在 Key 变量的位掩码:0x01 代表鼠标左键被点击,0x04 是 SHIFT 被按住,0x08 是 CTRL 被按住。三者独立提取后,bKeyBuy 与 bKeySell 互斥才可能进入挂单分支,否则只把鼠标光标显示出来。 Mouse.GetPositionDP 先抓到鼠标在图表上的时间+价格坐标,随后若 SHIFT/CTRL 有且只有一个生效,就隐藏鼠标并跑 CheckLimits(price) 校验价格是否越界。PositionAxlePrice 接着把这条潜在挂单的止盈止损画到轴上——若校验没过,TP/SL 传 0,等于不预设出场。 真正发单只看 bEClick && 异键状态 && local==0:local 在这里兼作“已挂单价格锁”,一旦 local 等于 price 就不再重复创建,避免同价位连点刷出一堆 pending。外汇与贵金属杠杆高,这种一键挂单若没限制重复触发,滑点和串单风险会被放大。 让小布替你跑这套 把上面掩码换成你自己的快捷键组合前,先在 MT5 策略测试器里用 Print(Key) 打几次按键值,确认 0x04/0x08 在你的终端映射没被改过,否则 SHIFT/CTRL 判断会整反。
bEClick = (Key & 0x01) == 0x01; class=class="str">"cmt">//Let mouse button click bKeyBuy = (Key & 0x04) == 0x04; class=class="str">"cmt">//Pressed SHIFT bKeySell = (Key & 0x08) == 0x08; class=class="str">"cmt">//Pressed CTRL Mouse.GetPositionDP(dt, price); if (bKeyBuy != bKeySell) { Mouse.Hide(); bCheck = CheckLimits(price); } else Mouse.Show(); PositionAxlePrice((bKeyBuy != bKeySell ? price : class="num">0), def_IndicatorTicket0, IT_PRICE, (bCheck ? class="num">0 : GetBaseFinanceTakeProfit()), (bCheck ? class="num">0 : GetBaseFinanceStopLoss()), GetBaseFinanceLeveRange(), bKeyBuy); if((bEClick) && (bKeyBuy != bKeySell) && (local == class="num">0)) CreateOrderPendent(bKeyBuy, local = price); local = (local != price ? class="num">0 : local); }
画得少,看得清
这一篇里做的几处改动,核心意图是把 EA 逻辑摊薄——不在单一时间框架里塞满判断,而是让不同周期各自暴露倾向。外汇与贵金属波动受杠杆和流动性挤压,这么做只是降低过度拟合的概率,不保证任何方向。 下一篇会直接用到这些精简后的结构。你能马上做的,是打开 MT5 把附带 EA.zip(约 11.7 MB)里的样本挂上回测,对比改前改后在 EURUSD 周线与 XAUUSD 日线上的信号密度差异。 留个余味:写系统的人如果只给你成品,你永远学不会拆。自己跑一遍,比看十篇通顺的译文值钱。