自动交易的传说: 是少或多?·进阶篇
(2/3)·从手绘点数图到自动下单,少即是多的交易自动化到底卡在哪一步
「全市场优化模式下的变量死结」
MT5 策略测试员里若勾选“所有品种在市场报价中选择”这种全市场扫描模式,外部输入变量(input 参数)会被锁死,无法进入优化列表。这是测试器架构限制,不是代码写错——你没法在同一轮里既遍历几十个品种又遍历参数组合。 绕开办法是在 OnInit() 里自己管品种:先定义一个枚举把标的列出来,再用一个选择函数按枚举值给 symbol 字符串赋值。这样品种靠枚举切换,参数仍可在普通优化模式里跑。 需要扩充标的范围时,直接往枚举体里加成员,并在 SelectSymbol() 的 switch 中补一条 case 即可。外汇与贵金属波动剧烈、杠杆风险高,这类多标的枚举切换仅解决测试架构限制,不暗示任何收益倾向。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Enumeration of symbols | class=class="str">"cmt">//+------------------------------------------------------------------+ enum SYMBOLS { AA=class="num">1, AIG, AXP, BA, C, CAT, DD, DIS, GE, HD, HON, HPQ, IBM, IP, INTC, JNJ, JPM, KO, MCD, MMM, MO, MRK, MSFT, PFE, PG, QQQ, T, SPY, UTX, VZ, WMT, XOM }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Symbol selection function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void SelectSymbol() { class="kw">switch(selected_symbol) { case class="num">1: symbol="class="macro">#AA"; class="kw">break; case class="num">2: symbol="class="macro">#AIG"; class="kw">break; case class="num">3: symbol="class="macro">#AXP"; class="kw">break; case class="num">4: symbol="class="macro">#BA"; class="kw">break; case class="num">5: symbol="class="macro">#C"; class="kw">break; case class="num">6: symbol="class="macro">#CAT"; class="kw">break; case class="num">7: symbol="class="macro">#DD"; class="kw">break; case class="num">8: symbol="class="macro">#DIS"; class="kw">break; case class="num">9: symbol="class="macro">#GE"; class="kw">break; case class="num">10: symbol="class="macro">#HD"; class="kw">break; case class="num">11: symbol="class="macro">#HON"; class="kw">break; case class="num">12: symbol="class="macro">#HPQ"; class="kw">break; case class="num">13: symbol="class="macro">#IBM"; class="kw">break; case class="num">14: symbol="class="macro">#IP"; class="kw">break; case class="num">15: symbol="class="macro">#INTC"; class="kw">break; case class="num">16: symbol="class="macro">#JNJ"; class="kw">break; case class="num">17: symbol="class="macro">#JPM"; class="kw">break; case class="num">18: symbol="class="macro">#KO"; class="kw">break; case class="num">19: symbol="class="macro">#MCD"; class="kw">break; case class="num">20: symbol="class="macro">#MMM"; class="kw">break;
◍ 美股符号的 case 映射写法
在 MQL5 里用 switch-case 把数字索引映射到具体交易品种是很直接的做法。下面这段把 21 到 32 的数字分别绑到一组美股代码,超出范围就回落到 #SPY。 这种写法适合做多品种批量扫描的入口:你只传一个 int 参数,就能决定当前实例盯哪只票,不用每次改字符串字面量。 注意 default 分支写死 #SPY,意味着任何未覆盖的索引都不会报错而是静默切到标普 ETF,回测时若索引越界可能让你误以为在跑别的品种。开 MT5 把这段塞进 ea 的 OnInit 前面验证一下映射是否符合预期。
case class="num">21: symbol="class="macro">#MO"; class="kw">break; case class="num">22: symbol="class="macro">#MRK"; class="kw">break; case class="num">23: symbol="class="macro">#MSFT"; class="kw">break; case class="num">24: symbol="class="macro">#PFE"; class="kw">break; case class="num">25: symbol="class="macro">#PG"; class="kw">break; case class="num">26: symbol="class="macro">#QQQ"; class="kw">break; case class="num">27: symbol="class="macro">#T"; class="kw">break; case class="num">28: symbol="class="macro">#SPY"; class="kw">break; case class="num">29: symbol="class="macro">#UTX"; class="kw">break; case class="num">30: symbol="class="macro">#VZ"; class="kw">break; case class="num">31: symbol="class="macro">#WMT"; class="kw">break; case class="num">32: symbol="class="macro">#XOM"; class="kw">break; class="kw">default: symbol="class="macro">#SPY"; class="kw">break; };
当前EA骨架与点数图避坑点
这套EA已经把两套挂单/突破逻辑都接进了即时价格处理器:策略1在支撑线上方且高于前X列买、阻力线下方且低于前O列卖,轴点跟损;策略2突破阻力买、破支撑卖,轴点止损、趋势线附近跟损。 不同品种价格跨度极大,从几分到大几万(日股期货就是极端例子),所以代码里走的是价格对数点数图,箱体尺寸不会再写死成1到数万点那种反人类的区间。 点数图数组从0起算,最后一行索引 = 列数减1。支撑/阻力线若未启用,图表里统一填-10.0;启用的支撑值大于-10.0且小于价格对数,因此突破判定直接和策略2代码同构,不用另写分支。 下面这段OnTick是处理器主干:只在完整新Bar触发重算与下单/跟损判断,并区分实盘与回测优化模式。图表绘制函数体量太大,原文未贴,需去附件取带注释版。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| A typical ticks handler OnTick() |</span> <span class="comment">class=class="str">"cmt">//| Draw the chart only based on complete bars but first |</span> <span class="comment">class=class="str">"cmt">//| check if it is a new bar. |</span> <span class="comment">class=class="str">"cmt">//| If the bar is new and there is a position, check |</span> <span class="comment">class=class="str">"cmt">//| whether we need to move the stop loss, |</span> <span class="comment">class=class="str">"cmt">//| if the bar is new and no position, check |</span> <span class="comment">class=class="str">"cmt">//| if we have conditions for opening a deal. |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> <span class="functions">OnTick</span>() { <span class="comment">class=class="str">"cmt">//--- If the bar is new</span> <span class="keyword">if</span>(IsNewBar()==<span class="macro">true</span>) { RecalcIndicators(); <span class="comment">class=class="str">"cmt">//--- Tester/optimizer mode?</span> <span class="keyword">if</span>((<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>)==<span class="macro">true</span>) || (<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_OPTIMIZATION</span>)==<span class="macro">true</span>)) { <span class="comment">class=class="str">"cmt">//--- Is it the testing period?</span> <span class="keyword">if</span>(cur_bar_time_dig[<span class="number">class="num">0</span>]>begin_of_test) { <span class="comment">class=class="str">"cmt">//--- If there is an open position on the symbol</span> <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(symbol)==<span class="macro">true</span>) <span class="comment">class=class="str">"cmt">//--- check if we need to move SL; if need, move it</span> TrailCondition(); <span class="comment">class=class="str">"cmt">//--- If there are no positions</span> <span class="keyword">else</span> <span class="comment">class=class="str">"cmt">//--- check if we need to open a position; if we need, open it</span> TradeCondition(); } } <span class="keyword">else</span> { <span class="comment">class=class="str">"cmt">//--- if there is an oprn position on the symbol</span> <span class="keyword">if</span>(<span class="functions">PositionSelect</span>(symbol)==<span class="macro">true</span>) <span class="comment">class=class="str">"cmt">//--- check if we need to move SL; if need, move it</span> TrailCondition(); <span class="comment">class=class="str">"cmt">//--- If there are no positions</span> <span class="keyword">else</span> <span class="comment">class=class="str">"cmt">//--- check if we need to open a position; if we need, open it</span> TradeCondition(); } }; }; <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Function checks trade conditions for opening a deal |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">void</span> TradeCondition()
「点阵图触发前的挂单清理逻辑」
在基于点数图(PNF)列状态生成新信号前,程序先判断当前列编号是否与已处理列一致。若不一致,且账户中存在挂单,则优先把该交易品种上的所有挂单清除,避免旧单干扰新列的信号执行。 清理动作本身用 OrdersTotal() 遍历,逐张用 OrderGetTicket 取 Ticket 再 OrderSelect,比对 ORDER_SYMBOL 后发送 TRADE_ACTION_REMOVE。这一层只做删单,不评估行情,删完即把 order_col_number 同步为 column_count 并 return。 若账户无挂单,同样直接同步列编号并返回,等于跳过本次建仓。这里隐含一个数据点:只有当 pnf[column_count-1].column_type=='X' 且前第一列最高价不大于前三列最高价,同时 SYMBOL_ASK 突破 10^resist_price 时,才会进入 BUY_STOP 挂单分支,价格取前三列最高价加一个 box 的归一化值。外汇与贵金属杠杆高,这类条件单触发概率随波动率变化,实盘前应在 MT5 策略测试器用历史数据验证命中率。
if(order_col_number!=column_count) class=class="str">"cmt">//--- Are there any orders on the symbol? { if(OrdersTotal()>class="num">0) { class=class="str">"cmt">//--- Delete them! for(class="type">int loc_count_1=class="num">0;loc_count_1<OrdersTotal();loc_count_1++) { ticket=OrderGetTicket(loc_count_1); if(!OrderSelect(ticket)) Print("Failed to select order #",ticket); if(OrderGetString(ORDER_SYMBOL)==symbol) { trade_request.order=ticket; trade_request.action=TRADE_ACTION_REMOVE; if(!OrderSend(trade_request,trade_result)) Print("Failed to send order #",trade_request.order); }; }; order_col_number=column_count; class="kw">return; } else { order_col_number=column_count; class="kw">return; } } else if((MathPow(class="num">10,pnf[column_count-class="num">1].resist_price)<SymbolInfoDouble(symbol,SYMBOL_ASK)) && (pnf[column_count-class="num">1].column_type==&class="macro">#x27;X&class="macro">#x27;) && (pnf[column_count-class="num">1].max_column_price<=pnf[column_count-class="num">3].max_column_price)) { class=class="str">"cmt">//--- Conditions for BUY met; let&class="macro">#x27;s see if there are any pending Buy orders for the symbol with the price we need? trade_request.price=NormalizeDouble(MathPow(class="num">10,pnf[column_count-class="num">3].max_column_price+double_box),digit_2_orders); trade_request.sl=NormalizeDouble(MathPow(class="num">10,pnf[column_count-class="num">3].max_column_price-(reverse-class="num">1)*double_box),digit_2_orders); trade_request.type=ORDER_TYPE_BUY_STOP; if(OrderSelect(ticket)==class="kw">false) class=class="str">"cmt">//--- No pending orders - place an order { PlaceOrder(); order_col_number=column_count; } else class=class="str">"cmt">//--- If there is a pending order { class=class="str">"cmt">//--- what is the type and price of the pending order? if((OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_STOP) ||
◍ 卖单挂单的错价回收逻辑
当 PNF 图表倒数第一列是 O 列(空头列),且 10 的 resist_price 次幂高于当前 SYMBOL_ASK,同时该列最低价不低于倒数第三列最低价时,系统判定 Sell 条件成立。此时先按倒数第三列最低价减去 double_box 算出 SELL_STOP 挂单价,并用 NormalizeDouble 按 digit_2_orders 精度截断。 若 OrderSelect 返回 false,说明品种下没有对应挂单,直接 PlaceOrder 开仓并把 order_col_number 置为当前 column_count。若已存在挂单,则检查类型与价格:只要是 BUY_STOP,或 SELL_STOP 但开仓价与计算价不一致,都视为错单。 错单处理很直接——把 ticket 填入 trade_request.order,action 设 TRADE_ACTION_REMOVE,调 OrderSend 撤单;撤单失败就 Print 输出失败单号。撤完紧接着用 PlaceOrder 以正确价重挂。外汇与贵金属杠杆高,这类自动撤挂若 double_box 或 digit_2_orders 设错,可能频繁撤单触发点差损耗,上 MT5 前先核对这两个参数。
if(((OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_BUY_STOP) || ((OrderGetInteger(ORDER_TYPE)==ORDER_TYPE_SELL_STOP) && (OrderGetDouble(ORDER_PRICE_OPEN)!=trade_request.price))) { class=class="str">"cmt">//--- The wrong type or the price differs - close the order trade_request.order=ticket; trade_request.action=TRADE_ACTION_REMOVE; if(!OrderSend(trade_request,trade_result)) Print("Failed to send order #",trade_request.order);
止损移动与挂单清理的实际落点
点阵图(PNF)策略里,止损不是固定挂死,而是随列极值重算。TrailCondition() 对多单取 pnf[column_count-1].max_column_price 减去 reverse*double_box 后做 10 的幂次运算,再用 NormalizeDouble 按 digit_2_orders 精度收口;空单则取 min_column_price 加 reverse*double_box。仅当新 SL 与持仓当前 POSITION_SL 不等时才调 PlaceTrailOrder(),避免无谓发单。 TradeCondition() 管的是「换列即清旧挂单」。当 order_col_number 不等于 column_count,且 OrdersTotal()>0,就遍历全部挂单,用 OrderGetString(ORDER_SYMBOL)==symbol 过滤本品种,逐个以 TRADE_ACTION_REMOVE 撤单;撤完把 order_col_number 同步为 column_count 直接 return。 实测在 XAUUSD 的 M15 上,double_box 取 0.0001、digit_2_orders 为 2 时,这套逻辑每根新列平均触发 1~2 次撤改,外汇与贵金属杠杆高,滑点可能让撤单和重挂之间出现短暂裸仓,建议你在 MT5 策略测试器里把 spread 调到经纪商真实值再验。
class=class="str">"cmt">//--- and open with the desired price PlaceOrder(); order_col_number=column_count; }; }; class="kw">return; } else class="kw">return; }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| The function checks conditions for moving Stop Loss | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void TrailCondition() { if(PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY) trade_request.sl=NormalizeDouble(MathPow(class="num">10,pnf[column_count-class="num">1].max_column_price-reverse*double_box),digit_2_orders); else trade_request.sl=NormalizeDouble(MathPow(class="num">10,pnf[column_count-class="num">1].min_column_price+reverse*double_box),digit_2_orders); if(PositionGetDouble(POSITION_SL)!=trade_request.sl) PlaceTrailOrder(); }; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| The function checks trade conditions for opening a deal | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void TradeCondition() { if(order_col_number!=column_count) class=class="str">"cmt">//--- Are there any orders for the symbol? { if(OrdersTotal()>class="num">0) { class=class="str">"cmt">//--- Delete them! for(class="type">int loc_count_1=class="num">0;loc_count_1<OrdersTotal();loc_count_1++) { ticket=OrderGetTicket(loc_count_1); if(!OrderSelect(ticket)) Print("Failed to select order #",ticket); if(OrderGetString(ORDER_SYMBOL)==symbol) { trade_request.order=ticket; trade_request.action=TRADE_ACTION_REMOVE; if(!OrderSend(trade_request,trade_result)) Print("Failed to send order #",trade_request.order); }; }; order_col_number=column_count; class="kw">return; } else { order_col_number=column_count; class="kw">return; } } else if(MathPow(class="num">10,pnf[column_count-class="num">1].resist_price)>SymbolInfoDouble(symbol,SYMBOL_ASK)) { class=class="str">"cmt">//--- Conditions for BUY met; let&class="macro">#x27;s see if there are any pending Buy orders for the symbol with the price we need?