三角套利·进阶篇
(2/3)· 论坛里总说三角套利稳,但多数人在竞买竞卖换算这一步就搞反了方向
EA 启动前的参数与文件自检
在 MT5 里跑三角套利类 EA,OnInit() 最先要拦住的只有一件事:交易量不能为负。六个输入参数里,其余设置不影响开单逻辑,唯独 lot<0 会直接让 ExpertRemove() 把机器人踢出图表。 日志句柄用固定前缀加启动日期拼文件名,比如 FILELOG+日期+.csv,每次重启先 FileDelete 同名旧文件再写新文件。这样你翻记录时不用在同一个文件里翻来翻去,但代价是历史日志默认不保留。 合约大小别想当然当 10 万。代码里遍历 SymbolsTotal(true) 拿 SYMBOL_TRADE_CONTRACT_SIZE,不等于 100000 就弹 Alert。虽然绝大多数经纪商外汇对是 100000,但贵金属或稀有品种偶尔例外,启动时报出来由你判断重不重要。 账户 margins 模式也顺手存进 accounttype,对冲或净持后续平仓分支要用。整套检查走完,EA 才真正进入 OnTick 逻辑。
class="type">void fnWarning(class="type">int &accounttype, class="type">class="kw">double lot, class="type">int &fh) { class=class="str">"cmt">// 检查交易量, 不应该是负数 if (lot<class="num">0) { Alert("交易量 < class="num">0"); ExpertRemove(); } class=class="str">"cmt">// 如果为 class="num">0, 发出警告, 且机器人将使用尽可能低的交易量。 if (lot==class="num">0) Alert("始终使用相同的最小交易量"); class=class="str">"cmt">// 仅在未选择三角文件创建模式时才创建日志文件。 if(inMode!=CREATE_FILE) { class="type">class="kw">string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv"; FileDelete(name); fh=FILEOPENWRITE(name); if (fh==INVALID_HANDLE) Alert("日志文件未能创建"); } class=class="str">"cmt">// 通常, 货币对的经纪商合约大小= class="num">100000, 但有时也有例外。 class=class="str">"cmt">// 然而, 这非常罕见, 在启动时很容易检查这个数值, 如果不是 class="num">10 万, 则报告, class=class="str">"cmt">// 以便让用户自己决定重要与否。当三角中的货币对合约大小不同时 class=class="str">"cmt">// EA 的处理未带有描述的时刻. for(class="type">int i=SymbolsTotal(true)-class="num">1;i>=class="num">0;i--) { class="type">class="kw">string name=SymbolName(i,true); class=class="str">"cmt">// 在形成三角时也要检查品种的可用性。 class=class="str">"cmt">// 我们会在稍后研究 if(!fnSmbCheck(name)) class="kw">continue; class="type">class="kw">double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE); if(cs!=class="num">100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,class="num">0)); } class=class="str">"cmt">// 获取账户类型, 对冲或净持 accounttype=(class="type">int)AccountInfoInteger(ACCOUNT_MARGIN_MODE); }
「三角组合的筛选与排序逻辑」
三角套利只认货币对,且同一三角内三个品种的合约大小必须一致,否则后续中性仓位没法对齐。MT5 测试器里别把市场观察所有品种都塞进去,普通家用机扛不住;优先读预存品种文件,没有就退化到 EURUSD + GBPUSD + EURGBP 这套标准三角。 品种排列不是随便放的。以 EUR-USD-GBP 为例,理论有 6 种排法,其中第 4、6 行无法通过其余两个品种反推第一个,直接丢弃;第 3、5 行要用除法,比乘法慢,也丢掉。只剩第 1、2 行可用,实盘里选第 2 种(EURUSD = EURGBP × GBPUSD)最好理解。 套利要求中性仓位:买 0.7 手 EURUSD、抛 0.7 手 EURGBP,相当于买卖 €70 000,实际游离于市场之外。第三对 GBPUSD 的手数直接等于第二对的价格,这样前两对反向同量、第三对做调整,订单结构就固定了。 形成三角后要把所用货币对的最小/最大交易量、价格与手数舍入位数、Point 和 TickSize 一次性读进来,后面下单不再现查。外汇和贵金属杠杆高、滑点凶,三角套利在实盘可能因流动性断裂而失效,只建议在策略测试器里先验证逻辑。
class="type">void fnGetThreeFromMarketWatch(stThree &MxSmb[]) { class=class="str">"cmt">// 获取品种总数 class="type">int total=SymbolsTotal(true); class=class="str">"cmt">// 用来比较合约大小的变量 class="type">class="kw">double cs1=class="num">0,cs2=class="num">0; class=class="str">"cmt">// 使用第一次循环列表中的第一个品种 for(class="type">int i=class="num">0;i<total-class="num">2 && !IsStopped();i++) {class=class="str">"cmt">//class="num">1 class="type">class="kw">string sm1=SymbolName(i,true); class=class="str">"cmt">// 检查品种的各种限制 if(!fnSmbCheck(sm1)) class="kw">continue; class=class="str">"cmt">// 获取合约大小, 并将之常规化, 因为我们稍后会比较这个值 if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) class="kw">continue; cs1=NormalizeDouble(cs1,class="num">0); class=class="str">"cmt">// 获取基准货币和盈利货币, 因为它们要用来比较 (而非货币对名称) class="type">class="kw">string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE); class="type">class="kw">string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT); class=class="str">"cmt">// 从第二次循环列表中取下一个品种 for(class="type">int j=i+class="num">1;j<total-class="num">1 && !IsStopped();j++) {class=class="str">"cmt">//class="num">2 class="type">class="kw">string sm2=SymbolName(j,true); if(!fnSmbCheck(sm2)) class="kw">continue; if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) class="kw">continue; cs2=NormalizeDouble(cs2,class="num">0); class="type">class="kw">string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE); class="type">class="kw">string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT);
◍ 三角套利的品种匹配与合约校验
构建外汇三角套利组合时,前两个货币对必须共享一种货币,否则无法闭合三角。例如 EURUSD 与 EURUSD.xxx 这类组合在逻辑上不可能形成有效三角,代码里用 base/profit 币种的交叉比对直接跳过无效对。 合约规模必须一致,cs1!=cs2 就 continue,这是很多人忽略的点:即便币种能凑成环,一手标准手在不同品种上代表不同体量,价差计算会失真。MT5 里 SYMBOL_TRADE_CONTRACT_SIZE 取出来后 NormalizeDouble(...,0) 取整再比,能避开浮点误差误杀。 第三层循环从 j+1 起扫,要求第三个品种的一端匹配第一对中的币种、另一端匹配第二对中的币种,且合约规模仍相等。三层过滤全过才写进 MxSmb 数组并 break,平均扫描复杂度从 O(n³) 降到实际可跑的量级。 别把币种字符串当万能钥匙。base 和 profit 币种在交叉盘里可能同是 EUR,但合约大小不同,光看符号名会漏掉这层风险,贵金属与外汇混扫时尤其要手动确认 cs 相等。
if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else class="kw">continue; if (cs1!=cs2) class="kw">continue; for(class="type">int k=j+class="num">1;k<total && !IsStopped();k++) { class="type">class="kw">string sm3=SymbolName(k,true); if(!fnSmbCheck(sm3)) class="kw">continue; if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) class="kw">continue; cs1=NormalizeDouble(cs1,class="num">0); class="type">class="kw">string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE); class="type">class="kw">string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT); if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else class="kw">continue; if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else class="kw">continue; if (cs1!=cs2) class="kw">continue; class="type">int cnt=ArraySize(MxSmb); ArrayResize(MxSmb,cnt+class="num">1); MxSmb[cnt].smb1.name=sm1; MxSmb[cnt].smb2.name=sm2; MxSmb[cnt].smb3.name=sm3; class="kw">break; }
三角品种文件的读取与回退逻辑
| EA 在初始化三角套利组合时,优先尝试从 CSV 文件加载用户自定义的三币种组合。若文件以 FILE_UNICODE | FILE_READ | FILE_SHARE_READ | FILE_CSV 方式打开后返回 INVALID_HANDLE,说明文件缺失或无权读取,此时直接打印提示并调用 ExpertRemove() 终止 EA,避免后续空跑。 |
|---|
文件成功打开后,先用 FileSeek 将指针置零,再靠 while(!FileIsLineEnding(fh)) FileReadString(fh) 吃掉首行标题,保证后面读到的都是纯数据行。逐行读取时每次取三个字符串作为 smb1/smb2/smb3,只要其中一个品种名在 csmb.Name() 检查中不通不过,就跳过该行继续,不写进 MxSmb 数组。 在策略测试器内若没找到品种文件,代码会遍历 SymbolsTotal(false) 返回的全部可见品种,硬匹配 BASE=EUR 且 PROFIT=GBP 的直盘,再结合 GBPUSD、EURUSD 拼出默认三角。这种回退机制意味着:你在 MT5 回测里不准备 FILENAME 文件,机器人也会自动用 EURUSD+GBPUSD+EURGBP 跑,但实盘若文件丢失则直接停摆,两者行为不一致,部署前必须确认文件就位。外汇与贵金属三角套利价差极薄,杠杆与滑点会带来显著高风险,上述逻辑仅解决品种加载,不预示任何收益。
class="type">int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV); if(fh==INVALID_HANDLE) { Print("未能读到品种文件!"); ExpertRemove(); } class=class="str">"cmt">// 将指针移动到文件的开头 FileSeek(fh,class="num">0,SEEK_SET); class=class="str">"cmt">// 跳过标题行 (文件的第一行) class="kw">while(!FileIsLineEnding(fh)) FileReadString(fh); class="kw">while(!FileIsEnding(fh) && !IsStopped()) { class=class="str">"cmt">// 得到三角的三个品种。我们来进行数据可用性的基本检查 class=class="str">"cmt">// 机器人能够自动形成三角文件。如果一位用户 class=class="str">"cmt">// 未能正确修改它, 我们假定这是故意的 class="type">class="kw">string smb1=FileReadString(fh); class="type">class="kw">string smb2=FileReadString(fh); class="type">class="kw">string smb3=FileReadString(fh); class=class="str">"cmt">// 如果品种的数据可用, 在到达行尾后将它们写入我们的三角数组 if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {class="kw">while(!FileIsLineEnding(fh)) FileReadString(fh);class="kw">continue;} class="type">int cnt=ArraySize(MxSmb); ArrayResize(MxSmb,cnt+class="num">1); MxSmb[cnt].smb1.name=smb1; MxSmb[cnt].smb2.name=smb2; MxSmb[cnt].smb3.name=smb3; class="kw">while(!FileIsLineEnding(fh)) FileReadString(fh); } } class="type">void fnSetThree(stThree &MxSmb[],enMode mode) { class=class="str">"cmt">// 重置我们的三角数组 ArrayFree(MxSmb); class=class="str">"cmt">// 检查我们是否在测试器中 if((class="type">bool)MQLInfoInteger(MQL_TESTER)) { class=class="str">"cmt">// 如果是的话, 查找一个品种文件并从文件启动三角的上传 if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb); class=class="str">"cmt">// 如果未找到文件, 遍历所有可用品种查找其中默认的 EURUSD + GBPUSD + EURGBP 三角 else{ class="type">char cnt=class="num">0; for(class="type">int i=SymbolsTotal(false)-class="num">1;i>=class="num">0;i--) { class="type">class="kw">string smb=SymbolName(i,false); if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") ||
「三角套利前的品种过滤逻辑」
做三角套利前,必须先确认市场观察里的品种够格。下面这段筛选函数会跳过一切非外汇 calc 模式、非全交易模式、带起止时间的合约,以及不支持市价/限价/止损/止损限价四类订单的品种。 过滤核心在 fnSmbCheck:先卡 SYMBOL_TRADE_CALC_MODE 不等于 SYMBOL_CALC_MODE_FOREX 的直接 false,再卡 SYMBOL_TRADE_MODE 不是 FULL 的也 false。若品种带 START_TIME 或 EXPIRATION_TIME 非零,同样抛弃,因为货币三角不需要交割类参数。 订单模式检查用位与:SYMBOL_ORDER_MARKET、LIMIT、STOP、STOP_LIMIT 四个标志必须全开,否则返回 false。这样机器人只会在流动性完整的普通货币对上跑三角。 实际验证时,可在 MT5 里随便挑一个 XAUUSD 或带到期日的 CFD,调 fnSmbCheck 必返回 false;而 EURUSD、GBPUSD 这类全模式外汇对才过得去。外汇与贵金属波动剧烈,三角套利亦属高风险策略,参数误用可能快速回撤。
class="type">bool fnSmbCheck(class="type">class="kw">string smb) { class=class="str">"cmt">// 三角只能由货币对组成 if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) class="kw">return(false); class=class="str">"cmt">// 如果有交易限制, 跳过此品种 if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) class="kw">return(false); class=class="str">"cmt">// 如果是合约的开始或结束, 也跳过该品种, 因为在处理货币时不使用该参数 if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=class="num">0)class="kw">return(false); if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=class="num">0) class="kw">return(false); class=class="str">"cmt">// 可用的订单类型。虽然机器人只进行市价订单交易, 但不应有限制 class="type">int som=(class="type">int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE); if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else class="kw">return(false); if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else class="kw">return(false); if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else class="kw">return(false); if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else class="kw">return(false);
◍ 三角套利里的第三腿定位与实时校验
在三角套利的品种组合里,第三腿的判定逻辑很直接:哪一对的基准货币跟另外两对都不匹配,它就排第三。代码里先用 SymbolInfoString 抓出三对的基准货币,只要有一个取不到就直接把该组合作废,不浪费后续计算。 实盘和回测对价格可用性的要求不一样。SymbolInfoTick 可能在品种刚上线时返回 ask 或 bid 为 0,所以非测试环境下要显式拦掉 tk.ask<=0 或 tk.bid<=0 的组合;而 MQLInfoInteger(MQL_TESTER) 为真时这段跳过,因为测试器里价格可能稍后才来。 排位修正只发生在前两腿基准货币不同的情况。若第一腿与第三腿同基币,就把第二、第三腿名称互换;若第二腿与第三腿同基币,则第一、第三腿互换。这样能保证后续套利路径计算时,第三腿确实是那个‘异类’。 外汇与贵金属三角套利涉及多腿同时成交,滑点和流动性断裂会让理论价差迅速消失,实盘高风险,建议在 MT5 策略测试器先跑通上述名称交换与校验逻辑再上模拟盘。
if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else class="kw">return(false); if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else class="kw">return(false); class=class="str">"cmt">// 为了数据可用性检查标准库 if(!csmb.Name(smb)) class="kw">return(false); class=class="str">"cmt">// 以下检查仅在实际操作中需要, 因为在某些情况下, 出于某些原因 SymbolInfoTick 接收价格 class=class="str">"cmt">// 而竞卖价或竞买价依旧为 class="num">0。 class=class="str">"cmt">// 在测试器中禁用, 因为价格可能会在稍后出现。 if(!(class="type">bool)MQLInfoInteger(MQL_TESTER)) { class="type">MqlTick tk; if(!SymbolInfoTick(smb,tk)) class="kw">return(false); if(tk.ask<=class="num">0 || tk.bid<=class="num">0) class="kw">return(false); } class="kw">return(true); } class="type">void fnChangeThree(stThree &MxSmb[]) { class="type">int count=class="num">0; for(class="type">int i=ArraySize(MxSmb)-class="num">1;i>=class="num">0;i--) {class=class="str">"cmt">//for class=class="str">"cmt">// 首先, 我们来确定第三个位置。 class=class="str">"cmt">// 这一货币对的基准货币与其它两个基准货币不匹配 class="type">class="kw">string sm1base="",sm2base="",sm3base=""; class=class="str">"cmt">// 如果由于某种原因我们无法得到基准货币, 我们不会使用这个三角操作 if(!SymbolInfoString(MxSmb[i].smb1.name,SYMBOL_CURRENCY_BASE,sm1base) || !SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_BASE,sm2base) || !SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE,sm3base)) {MxSmb[i].smb1.name="";class="kw">continue;} class=class="str">"cmt">// 如果品种 class="num">1 和 class="num">2 的基准货币相同, 则跳过此步骤。否则, 交换货币对的位置 if(sm1base!=sm2base) { if(sm1base==sm3base) { class="type">class="kw">string temp=MxSmb[i].smb2.name; MxSmb[i].smb2.name=MxSmb[i].smb3.name; MxSmb[i].smb3.name=temp; } if(sm2base==sm3base) { class="type">class="kw">string temp=MxSmb[i].smb1.name; MxSmb[i].smb1.name=MxSmb[i].smb3.name; MxSmb[i].smb3.name=temp; } } class=class="str">"cmt">// 现在, 我们来定义第一个和第二个位置。