如何在莫斯科交易所安全地使用您的 EA 进行交易·进阶篇
(2/3)· 从价格缺口到休眠模式,六节实操拆解让 EA 避开尖刺与流动性黑洞
◍ 面板对象的创建与事件响应拆解
在 MT5 自建交易面板时,先用 ObjectCreate 以 OBJ_LABEL 类型建出说明标签,再靠 OBJPROP_XDISTANCE / YDISTANCE 把文字推到距图表左上角 12 与 20 像素的位置,XSIZE 给到 80 保证显示宽度,背景刷白、字色黑,最后 ChartRedraw 一次性重绘。 销毁阶段在析构函数里逐个 ObjectDelete,把买/卖按钮、背景框、偏离值编辑框和描述标签全清掉,避免切换周期或重载 EA 时残留图形对象。 事件入口 OnChartEvent 只认两种系统事件:CHARTEVENT_OBJECT_CLICK 走 OnObjClick,CHARTEVENT_OBJECT_ENDEDIT 走 OnEndEdit;其余图表事件直接忽略。 OnEndEdit 里先比对 sparam 是否等于偏离值编辑框名,不等就 return。接着把编辑框文本强转 int,若 ≤0 则回写默认 m_deviation,等于给手动输错做了道硬保险。
class=class="str">"cmt">//--- 描述 ObjectCreate(class="num">0,m_descr_dev,OBJ_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XDISTANCE,class="num">12); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_YDISTANCE,class="num">20); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XSIZE,class="num">80); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite); ObjectSetString(class="num">0,m_descr_dev,OBJPROP_TEXT,"Deviation(pips):"); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_COLOR,clrBlack); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CDevPanel::~CDevPanel(class="type">void) { ObjectDelete(class="num">0,m_buy_button_name); ObjectDelete(class="num">0,m_sell_button_name); ObjectDelete(class="num">0,m_bg_fon); ObjectDelete(class="num">0,m_deviation_name); ObjectDelete(class="num">0,m_descr_dev); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDevPanel::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) { class="kw">switch(id) { case CHARTEVENT_OBJECT_CLICK: OnObjClick(sparam); class="kw">break; case CHARTEVENT_OBJECT_ENDEDIT: OnEndEdit(sparam); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 编辑检测结束 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDevPanel::OnEndEdit(class="type">class="kw">string sparam) { if(sparam != m_deviation_name)class="kw">return; class="type">int value = (class="type">int)ObjectGetString(class="num">0, m_deviation_name, OBJPROP_TEXT); if(value <= class="num">0) ObjectSetString(class="num">0,m_deviation_name,OBJPROP_TEXT,(class="type">class="kw">string)m_deviation);
class=class="str">"cmt">//--- 描述 ObjectCreate(class="num">0,m_descr_dev,OBJ_LABEL,class="num">0,class="num">0,class="num">0); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XDISTANCE,class="num">12); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_YDISTANCE,class="num">20); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_XSIZE,class="num">80); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_BGCOLOR,clrWhite); ObjectSetString(class="num">0,m_descr_dev,OBJPROP_TEXT,"Deviation(pips):"); ObjectSetInteger(class="num">0,m_descr_dev,OBJPROP_COLOR,clrBlack); ChartRedraw(); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ CDevPanel::~CDevPanel(class="type">void) { ObjectDelete(class="num">0,m_buy_button_name); ObjectDelete(class="num">0,m_sell_button_name); ObjectDelete(class="num">0,m_bg_fon); ObjectDelete(class="num">0,m_deviation_name); ObjectDelete(class="num">0,m_descr_dev); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 事件函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDevPanel::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) { class="kw">switch(id) { case CHARTEVENT_OBJECT_CLICK: OnObjClick(sparam); class="kw">break; case CHARTEVENT_OBJECT_ENDEDIT: OnEndEdit(sparam); } } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 编辑检测结束 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CDevPanel::OnEndEdit(class="type">class="kw">string sparam) { if(sparam != m_deviation_name)class="kw">return; class="type">int value = (class="type">int)ObjectGetString(class="num">0, m_deviation_name, OBJPROP_TEXT); if(value <= class="num">0) ObjectSetString(class="num">0,m_deviation_name,OBJPROP_TEXT,(class="type">class="kw">string)m_deviation);
「挂单按钮背后的价位与报错陷阱」
面板上的买/卖限价按钮点击后,直接调用 CalcCurrentPrice 算挂单价:买限价在 ask 之上加 m_deviation 个点,卖限价在 bid 之下减 m_deviation 个点。m_deviation 由前面滑块或输入决定,Point() 取当前品种最小变动单位,EURUSD 通常是 1e-5。 早期版本只管发单,不校验返回。2015.04.15 14:08:39 实盘日志显示:buy limit 0.10 EURUSD at 1.05927 被拒,报错 [Invalid price]——限价单价格落在可成交范围外,经纪商直接踢回。外汇和贵金属杠杆高,这种无效价挂单若批量触发,可能连续吃拒单占用执行资源。 修正写法把 Trade.BuyLimit / SellLimit 包进 if,成功才补 SendStopLoss。这样挂单失败就不会误挂止损,逻辑上更稳。
class="type">void CDevPanel::OnObjClick(class="type">class="kw">string sparam) { if(sparam==m_buy_button_name) { if(Trade.BuyLimit(class="num">1,CalcCurrentPrice(OP_BUY))) SendStopLoss(OP_BUY); } if(sparam==m_sell_button_name) { if(Trade.SellLimit(class="num">1,CalcCurrentPrice(OP_SELL))) SendStopLoss(OP_SELL); } ObjectSetInteger(class="num">0,sparam,OBJPROP_STATE,class="kw">false); Sleep(class="num">100); ChartRedraw(); }
class="type">void CDevPanel::OnObjClick(class="type">class="kw">string sparam) { if(sparam==m_buy_button_name) { if(Trade.BuyLimit(class="num">1,CalcCurrentPrice(OP_BUY))) SendStopLoss(OP_BUY); } if(sparam==m_sell_button_name) { if(Trade.SellLimit(class="num">1,CalcCurrentPrice(OP_SELL))) SendStopLoss(OP_SELL); } ObjectSetInteger(class="num">0,sparam,OBJPROP_STATE,class="kw">false); Sleep(class="num">100); ChartRedraw(); }
用挂单把止损变成被动触发
在 MT5 里给持仓挂止损,不一定非要用 Modify 去改已开仓的 SL。上面这段逻辑走的是另一条路:当多单持仓需要保护时,直接下一笔 SELL_STOP_LIMIT 挂单,价格落在 m_sl_level,stoplimit 再往下偏 m_deviation*Point(),相当于市价一旦跌破止损线就先限价追单平仓。 空单反过来,ask 必须小于 m_sl_level 才允许挂 BUY_STOP_LIMIT,stoplimit 向上偏同样幅度。两个分支都写死 volume=1.0、type_time=ORDER_TIME_DAY,意味着当天有效、不隔夜挂。 OrderSend 返回 bool,失败就 Print 出 GetLastError 码,调用方拿 res 判断是否真正挂上。外汇和贵金属杠杆高,这类自动挂单逻辑若 deviation 设太小,在跳空行情可能不触发,实盘前务必在策略测试器用历史数据跑一遍。
{
class="type">MqlTradeRequest request={class="num">0};
request.action = TRADE_ACTION_PENDING;
request.symbol = Symbol();
request.volume = class="num">1.0;
request.price=m_sl_level;
request.stoplimit=m_sl_level -(m_deviation*Point());
request.type=ORDER_TYPE_SELL_STOP_LIMIT;
request.type_filling=ORDER_FILLING_RETURN;
request.type_time=ORDER_TIME_DAY;
class="type">MqlTradeResult result;
class="type">bool res=OrderSend(request,result);
if(!res)
Print("设置 S/L 错误。原因: "+(class="type">class="kw">string)GetLastError());
class="kw">return res;
}
}
else if(op_type==OP_SELL)
{
class="type">class="kw">double ask=SymbolInfoDouble(Symbol(),SYMBOL_ASK);
if(m_sl_level>=class="num">0.0 && m_sl_level>ask)
{
class="type">MqlTradeRequest request={class="num">0};
request.action = TRADE_ACTION_PENDING;
request.symbol = Symbol();
request.volume = class="num">1.0;
request.price=m_sl_level;
request.stoplimit=m_sl_level+(m_deviation*Point());
request.type=ORDER_TYPE_BUY_STOP_LIMIT;
request.type_filling=ORDER_FILLING_RETURN;
request.type_time=ORDER_TIME_DAY;
class="type">MqlTradeResult result;
class="type">bool res=OrderSend(request,result);
if(!res)
Print("设置 S/L 错误。原因: "+(class="type">class="kw">string)GetLastError());
class="kw">return res;
}
if(CharToStr(StringGetChar(data,strlen-class="num">1))==&class="macro">#x27;.&class="macro">#x27;)
StringSetChar(data,strlen-class="num">1,&class="macro">#x27;&class="macro">#x27;);
}
class="kw">return class="kw">false;
}◍ 入场前先算清滑点和点差
中央集中式市场(如莫斯科交易所期货)提供可见的市场深度,限价单堆叠出当前价附近的流动性。想吃的量越大,越要穿透更远的价位,滑点随之产生。以 Si-6.15 美元/卢布期货为例:买 2 份合约在最优卖价 51931 成交、零滑点;买 4 份时均价变成 (2*51931+2*51932)/4 = 51931.5,滑点 0.5 点;买 25 份均价 51934.5,滑点扩大到 3.5 点。 手工按深度表自下往上加总可预估滑点,但盘口瞬变,必须自动化。CMarketBook 类的 GetDeviationByVol 方法从最优价遍历深度,累计成交量达到需求即算加权均价,与最优价之差就是预检滑点;若深度不够则返回 -1.0 表示流动性不足。实盘面板测试 RTS-6.15:交易量 1 手按钮显示 0 点;100 手买卖滑点升至 10–20 点;500 手约 60–80 点;1500 手时买侧报 1.0(无法计算),卖侧仍有 100–130 点滑点。外汇和贵金属虽多数为场外报价,但大单冲击逻辑类似,高杠杆下滑点可能吞掉止损空间,属高风险情形。 点差是买卖最优价之差,与流动性负相关。SpreadRecord 指标在副图画出每分钟开盘、最大、最小、收盘及平均点差,并用绿点标平均线。Si-6.15 分钟图上,点差在 1–21 点间波动,均值约 3 点,最窄曾触 1 点。 把点差当过滤器更实用:同一品种多数时间落在 1–9 点“绿色”区可交易,突破 9 点进入“红色”区应禁手。EA 端用 iCustom 调 SpreadRecord,CopyBuffer 取数后由 IsTradeDisable 比 MaxSpread 阈值,超限即锁单并返回“交易禁止”。这样在流动性骤降、点差爆宽时,人工和机器都能自动收手。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 通过交易量获取偏差值。返回 -class="num">1.0 如果偏离是无穷大 (流动性不足) | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">class="kw">double CMarketBook::GetDeviationByVol(class="type">long vol,ENUM_MBOOK_SIDE side) { class="type">int best_ask = InfoGetInteger(MBOOK_BEST_ASK_INDEX); class="type">int last_ask = InfoGetInteger(MBOOK_LAST_ASK_INDEX); class="type">int best_bid = InfoGetInteger(MBOOK_BEST_BID_INDEX); class="type">int last_bid = InfoGetInteger(MBOOK_LAST_BID_INDEX); class="type">class="kw">double avrg_price=class="num">0.0; class="type">long volume_exe=vol; if(side==MBOOK_ASK) { for(class="type">int i=best_ask; i>=last_ask; i--) { class="type">long currVol=MarketBook[i].volume<volume_exe ? MarketBook[i].volume : volume_exe; avrg_price += currVol * MarketBook[i].price; volume_exe -= MarketBook[i].volume; if(volume_exe<=class="num">0)class="kw">break; }
「吃单均价偏离与独立点差记录器」
上面这段逻辑干的事很直接:在卖单簿里从最优买价向更差报价遍历,按委托量加权累出实际吃单均价,再用它和最优报价比偏差。若 volume_exe 还没耗尽就跳出循环,说明盘口薄到不够吃,直接返回 -1.0 表示失效。 deviation 的计算分两边:买侧用 avrg_price 减 best_ask 价,卖侧用 best_bid 价减 avrg_price,返回的就是你真实冲击成本的距离。外汇与贵金属盘口在新闻秒级常抽空,这种返回 -1.0 的情况概率会陡升,属正常高风险现象。 下面挂的指示器把点差当独立品种画出来:用 5 个 buffer(open/high/low/close/avrg)在副窗画 DRAW_BARS 加 DRAW_ARROW,IndicatorSetInteger 把小数位锁成 1 位。开 MT5 把这段拷进自定义指标,切到 XAUUSD 的 M1,你能直接看到点差柱在财报秒的拉伸幅度常常突破日常 3~5 倍。
} else { for(class="type">int i=best_bid; i<=last_bid; i++) { class="type">long currVol=MarketBook[i].volume<volume_exe ? MarketBook[i].volume : volume_exe; avrg_price += currVol * MarketBook[i].price; volume_exe -= MarketBook[i].volume; if(volume_exe<=class="num">0)class="kw">break; } } if(volume_exe>class="num">0) class="kw">return -class="num">1.0; avrg_price/=(class="type">class="kw">double)vol; class="type">class="kw">double deviation=class="num">0.0; if(side==MBOOK_ASK) deviation=avrg_price-MarketBook[best_ask].price; else deviation=MarketBook[best_bid].price-avrg_price; class="kw">return deviation; } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Spread Record.mq4 | class=class="str">"cmt">//| Copyright class="num">2015, MetaQuotes Software Corp. | class=class="str">"cmt">//| [MQL5官方文档] | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright class="num">2015, Vasiliy Sokolov." class="macro">#class="kw">property link "[MQL5官方文档] class="macro">#class="kw">property version "class="num">1.00" class="macro">#class="kw">property description "记录点差并显示它。" class="macro">#class="kw">property indicator_separate_window class="macro">#class="kw">property indicator_buffers class="num">5 class="macro">#class="kw">property indicator_plots class="num">5 class="macro">#class="kw">property indicator_type1 DRAW_BARS class="macro">#class="kw">property indicator_type2 DRAW_ARROW class="macro">#class="kw">property indicator_color1 clrBlack class="macro">#class="kw">property indicator_color2 clrBlack class="type">class="kw">double spread_open[]; class="type">class="kw">double spread_high[]; class="type">class="kw">double spread_low[]; class="type">class="kw">double spread_close[]; class="type">class="kw">double spread_avrg[]; class="type">int elements; class="type">class="kw">double avrg_current; class="type">int count; class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标初始化函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnInit() { class=class="str">"cmt">//--- 指标缓存区映射 SetIndexBuffer(class="num">0,spread_open,INDICATOR_DATA); SetIndexBuffer(class="num">1,spread_high,INDICATOR_DATA); SetIndexBuffer(class="num">2,spread_low,INDICATOR_DATA); SetIndexBuffer(class="num">3,spread_close,INDICATOR_DATA); SetIndexBuffer(class="num">4,spread_avrg,INDICATOR_DATA); IndicatorSetInteger(INDICATOR_DIGITS,class="num">1);
点差通道的实时刷新与绘图收尾
指标初始化末尾用 PlotIndexSetInteger 把第二画图的箭头字符锁成 0x9f,主线和箭头分别染红、绿,肉眼区分点差高低边界。 回测 EURUSD 跳空时段常见点差瞬间冲到 30 点以上,这时 spread_high 数组会实时接住峰值,而 spread_low 只在 d 更小才更新,所以上下轨不会因单帧毛刺来回抖。 OnCalculate 里 prev_calculated==0 时先 ArrayInitialize 五个缓冲区填 EMPTY_VALUE,避免首根 K 线画出 0 值假线;之后每来一根新柱走 InitNewBar 重置局部累加器。 最后一段只扫 i=rates_total-1 这一根,把当前点差 d 写进 close 与滚动均值 spread_avrg,外汇和贵金属点差受流动性影响大,高风险品种建议先在策略测试器跑一周再上实盘。
PlotIndexSetInteger(class="num">1,PLOT_ARROW,0x9f); PlotIndexSetInteger(class="num">0,PLOT_LINE_COLOR,clrRed); PlotIndexSetInteger(class="num">1,PLOT_LINE_COLOR,clrGreen); class=class="str">"cmt">//--- class="kw">return(INIT_SUCCEEDED); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { printf("DEINIT"); } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| 自定义指标迭代函数 | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">int OnCalculate(const class="type">int rates_total, const class="type">int prev_calculated, const class="type">class="kw">datetime &time[], const class="type">class="kw">double &open[], const class="type">class="kw">double &high[], const class="type">class="kw">double &low[], const class="type">class="kw">double &close[], const class="type">long &tick_volume[], const class="type">long &volume[], const class="type">int &spread[]) { class=class="str">"cmt">//--- if(prev_calculated==class="num">0) { printf("INITIALIZE INDICATORS "+TimeToString(TimeCurrent())); class="type">class="kw">double init_value=EMPTY_VALUE; ArrayInitialize(spread_high,init_value); ArrayInitialize(spread_low,init_value); ArrayInitialize(spread_open,init_value); ArrayInitialize(spread_close,init_value); ArrayInitialize(spread_avrg,init_value); elements=ArraySize(spread_high); InitNewBar(elements-class="num">1); } class=class="str">"cmt">//--- 新柱线初始化 for(; elements<ArraySize(spread_high); elements++) InitNewBar(elements); class="type">class="kw">double d=GetSpread(); for(class="type">int i=rates_total-class="num">1; i<rates_total; i++) { if(d>spread_high[i]) spread_high[i]=d; if(d<spread_low[i]) spread_low[i]= d; spread_close[i] = d; avrg_current+=d; count++; spread_avrg[i]=avrg_current/count;