总体思路
开发可靠规律的三角套利话题经常出现在论坛上。那么它究竟是什么呢?
“套利” 意味着有些偏向行情的中立性。”三角” 是指投资组合由三个金融工具组成。
我们举一个最流行的例子: “欧元 — 英镑 — 美元” 三角。就货币对而言, 可以描述如下: EURUSD + GBPUSD + EURGBP。所需的中立性包括尝试同时买入和卖出相同的金融工具, 从而赚取利润。
这看起来如下。这个例子中的任何一个货币对都可通过另外两个货币对来表示:
EURUSD=GBPUSD*EURGBP,
或 GBPUSD=EURUSD/EURGBP,
或 EURGBP=EURUSD/GBPUSD。
所有这些变体是相同的, 下面会更详细地讨论它们中的所有选择。同时, 我们来研究第一个选项。
首先, 我们需要看出竞买价和竞卖价。流程如下:
- 买入 EURUSD, 即使用 竞卖 价。这意味着, 我们在余额中增加 EUR 占比, 并消减 USD。
- 我们来通过其它两个货币对评估 EURUSD。
- GBPUSD: 这里面没有 EUR。代之, 我们需要抛售这里面的 USD。为了抛售 GBPUSD 当中的 USD, 我们需要买入这个货币对。意即, 我们使用 竞卖价。当买入时, 我们在余额中增加 GBP 占比, 同时消减 USD。
- EURGBP: 我们需要买入 EUR, 抛售我们不需要的 GBP。买入 EURGBP, 使用 竞卖价。我们在余额中增加 EUR 占比, 并消减 GBP。
总计我们拥有: (竞买价) EURUSD = (竞买价) GBPUSD * (竞买价) EURGBP。我们已获得了必要的等价。为了令其盈利, 我们应该一边买入一边卖出。这里有两种可能的选项:
- 比我们抛售 EURUSD 更便宜地买入, 但以不同的方式展现: (竞卖价) EURUSD < (竞买价) GBPUSD * (竞买价) EURGBP
- 比我们买入 EURUSD 的更高价格抛售, 但以不同的方式展现: (竞买价) EURUSD > (竞卖价) GBPUSD * (竞卖价) EURGBP
现在, 我们所要做的就是检测这种情况, 并从中获利。
注意, 三角可以用另一种方式来移动, 这三个货币对在一个方向上移动, 并与 1 比较。所有变体都相同, 但我相信, 上面描述的变体更容易理解和解释。
通过形势跟踪, 我们可以寻找一个同时买入和卖出的时刻。在这种情况下, 会即时盈利, 但这样的时刻是罕见的。
更常见的情况是, 当我们能够更便宜地买入一方时, 却无法在抛售另一方时盈利。那么我们只得等待这种不平衡消失。交易对我们来说是安全的, 因为我们的持仓相互抵消近乎为零, 意即我们游离于市场之外。虽然, 此处请注意 “近乎” 这个词。为了交易量的完美程度, 我们所需的精确度并未得到。交易量往往四舍五入到小数点后两位, 对于我们的策略来说这太粗糙了。
现在我们已经研究了这个理论, 现在是编写 EA 的时候了。EA 是以面向过程的风格开发的, 所以新入行的程序员, 以及那些因为某种原因不喜欢 OOP 的人都可以理解。
简要的 EA 描述
首先, 我们创建所有可能的三角, 将它们正确放置, 并获得每个货币对的所有必要数据。
所有这些信息都存储在 MxThree 结构数组中。每个三角都有 status (状态) 字段。它的初始值是 0。如果需要三角开单, 状态设置为 1。确认三角完全开单后, 状态变为 2。如果三角形部分开单, 或者平单时间已到, 则状态变为 3。一旦三角成功平单, 状态将返回到 0。
三角开单和平单均被保存到一个日志文件, 令我们能够检查动作的正确性并重温历史。日志文件名称为 Three Point Arbitrage Control YYYY.DD.MM.csv。
为了执行测试, 请将所有必要的货币对载入到测试器。为此, 在运行测试器之前, 在 “创建品种文件” 模式中启动 EA。如果不存在这样的文件, EA 将在默认的 EUR + GBP + USD 三角上运行测试。
使用的变量
在我的开发过程中, 任何机器人的代码都是从包含头文件开始的。它会列出所有包含内容, 函数库, 等等。这个机器人也不例外: 说明模块之后紧随 #include “head.mqh” 等等:
#include <Trade/Trade.mqh> #include <Trade/SymbolInfo.mqh> #include <Trade/TerminalInfo.mqh> #include "var.mqh" #include "fnWarning.mqh" #include "fnSetThree.mqh" #include "fnSmbCheck.mqh" #include "fnChangeThree.mqh" #include "fnSmbLoad.mqh" #include "fnCalcDelta.mqh" #include "fnMagicGet.mqh" #include "fnOpenCheck.mqh" #include "fnCalcPL.mqh" #include "fnCreateFileSymbols.mqh" #include "fnControlFile.mqh" #include "fnCloseThree.mqh" #include "fnCloseCheck.mqh" #include "fnCmnt.mqh" #include "fnRestart.mqh" #include "fnOpen.mqh"
此列表目前对您来说也许无法完全理解, 但本文会遵循这些代码, 因此程序结构在此并未被违反。往下一切都将变得清晰。所有函数, 类和代码单元都放在单独的文件中, 以方便使用。就我而言, 除了标准库之外, 每个包含文件也以 #include “head.mqh” 开头。允许在包含文件中使用 IntelliSense (智能感知), 因此不必在内存中保存所有必要实体的名称。
之后, 为测试器连接文件。我们不能在任意地方进行这一步, 所以我们要在此声明。这个字符串是多币种测试器加载品种所需的:
#property tester_file FILENAME
接下来, 我们描述程序中使用的变量。描述可以在单独的 var.mqh 文件中找到:
// 宏定义 #define DEVIATION 3 // 最大可能的滑点 #define FILENAME "Three Point Arbitrage.csv" // 操作品种存储在这里 #define FILELOG "Three Point Arbitrage Control " // 日志文件名称部分 #define FILEOPENWRITE(nm) FileOpen(nm,FILE_UNICODE|FILE_WRITE|FILE_SHARE_READ|FILE_CSV) // 打开文件写入 #define FILEOPENREAD(nm) FileOpen(nm,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV) // 打开文件读取 #define CF 1.2 // 提高保证金比例 #define MAGIC 200 // 应用的魔幻数字范围 #define MAXTIMEWAIT 3 // 三角开单后的最长等待时间, 以秒为单位 // 货币对结构 struct stSmb { string name; // 货币对 int digits; // 报价中的小数位数 uchar digits_lot; // 手数的四舍五入小数位数 int Rpoint; // 1/point, 以便在方程中乘以 (而不是除以) 该值 double dev; // 可能的滑点。一次性转换成点数 double lot; // 货币对的交易量 double lot_min; // 最小交易量 double lot_max; // 最大交易量 double lot_step; // 手数增量 double contract; // 合约大小 double price; // 在三角中的货币对开单价。净持模式需要 ulong tkt; // 交易开单所用的订单票号。对冲账户所需 MqlTick tick; // 当前货币对价格 double tv; // 当前分笔报价 double mrg; // 当前用于开单的保证金 double sppoint; // 点差, 单位为点数的整数值 double spcost; // 当前开单的每手点差, 以资金为单位 stSmb(){price=0;tkt=0;mrg=0;} }; // 三角结构 struct stThree { stSmb smb1; stSmb smb2; stSmb smb3; double lot_min; // 整个三角的最小交易量 double lot_max; // 整个三角的最大交易量 ulong magic; // 三角的魔幻数字 uchar status; // 三角状态。0 - 未使用。1 - 发送开单。2 - 成功开单。3 - 发送平单 double pl; // 三角盈利 datetime timeopen; // 发送三角开单的时间 double PLBuy; // 买入三角时的潜在利润 double PLSell; // 抛售三角时的潜在利润 double spread; // 所有三个点差的总价 (含佣金!) stThree(){status=0;magic=0;} }; // EA 操作模式 enum enMode { STANDART_MODE = 0, /*Symbols from Market Watch*/ // 标准操作模式。市场观察品种 USE_FILE = 1, /*Symbols from file*/ // 使用品种文件 CREATE_FILE = 2, /*Create file with symbols*/ // 为测试器或操作创建文件 //END_ADN_CLOSE = 3, /*Not open, wait profit, close & exit*/ // 您的所有交易平单并结束操作 //CLOSE_ONLY = 4 /*Not open, not wait profit, close & exit*/ }; stThree MxThree[]; // 主数组存储正在操作的三角和所有必要的附加数据 CTrade ctrade; // 标准库的 CTrade 类 CSymbolInfo csmb; // 标准库的 CSymbolInfo 类 CTerminalInfo cterm; // 标准库的 CTerminalInfo 类 int glAccountsType=0; // 账户类型: 对冲或净持 int glFileLog=0; // 日志文件句柄 // 输入 sinput enMode inMode= 0; // 操作模式 input double inProfit= 0; // 佣金 input double inLot= 1; // 交易量 input ushort inMaxThree= 0; // 三角已开单 sinput ulong inMagic= 300; // EA 魔幻数字 sinput string inCmnt= "R "; // 注释
由于它们很简单并附有注释, 故先行定义。我相信, 它们很容易理解。
它们跟着两个结构 — stSmb 和 stThree。逻辑如下: 任何三角由三个货币对组成。因此, 一旦描述其一并使用三次之后, 我们得到一个三角。stSmb — 描述货币对的结构及其规格: 可能的交易量, _Digits 和 _Point 变量, 开单时的当前价格和一些其它值。在 stThree 结构当中, stSmb 使用了三次。这就是我们的三角的形成过程。此外, 还会添加一些与三角相关的属性 (当前利润, 魔幻数字, 开单时间等)。然后, 是我们将在稍后介绍的操作模式和输入变量。输入也在注释中说明了。我们要仔细看看其中两个:
inMaxThree 参数中存储了可同时开单的最大三角可能数量。0 — 未用。例如, 如果参数设置为 2, 则不能有两个以上的三角同时开单。
inProfit 参数包含佣金值, 如果有的话。
初始设置
在我们描述过包含文件和使用变量之后, 我们进入 OnInint() 模块。
在启动 EA 之前, 请务必检查输入参数的正确性, 并在必要时接收初始数据。如果一切顺利的话, 我们就开始吧。我通常在 EA 中设置尽可能少的输入量, 这个机器人也不例外。
六个输入中只有一个也许阻止 EA 操作, 这就是交易量。我们不能以负数交易量开单交易。所有其它设置不影响操作。这些检查在 OnInit() 模块函数中最先执行。
我们来看看它的代码。
void fnWarning(int &accounttype, double lot, int &fh) { // 检查交易量, 不应该是负数 if (lot<0) { Alert("交易量 < 0"); ExpertRemove(); } // 如果为 0, 发出警告, 且机器人将使用尽可能低的交易量。 if (lot==0) Alert("始终使用相同的最小交易量");
由于机器人是以面向过程风格编写的, 所以我们必须创建几个全局变量。其中之一是日志文件句柄。该名称由一个固定部分和机器人开始日期组成 – 这是为了便于控制, 因此您不必在同一个文件中搜索特定日志的起始位置。请注意, 名称在每次重新启动时都会变更, 并删除前一个同名文件 (如果有的话)。
EA 在其操作中使用两个文件: 含有检测到三角的文件 (由用户自行决定), 和记录三角开单和平单时间的日志文件, 开单价格和一些方便控制的附加数据。日志记录始终处于活动状态。
// 仅在未选择三角文件创建模式时才创建日志文件。 if(inMode!=CREATE_FILE) { string name=FILELOG+TimeToString(TimeCurrent(),TIME_DATE)+".csv"; FileDelete(name); fh=FILEOPENWRITE(name); if (fh==INVALID_HANDLE) Alert("日志文件未能创建"); } // 通常, 货币对的经纪商合约大小= 100000, 但有时也有例外。 // 然而, 这非常罕见, 在启动时很容易检查这个数值, 如果不是 10 万, 则报告, // 以便让用户自己决定重要与否。当三角中的货币对合约大小不同时 // EA 的处理未带有描述的时刻. for(int i=SymbolsTotal(true)-1;i>=0;i--) { string name=SymbolName(i,true); // 在形成三角时也要检查品种的可用性。 // 我们会在稍后研究 if(!fnSmbCheck(name)) continue; double cs=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE); if(cs!=100000) Alert("Attention: "+name+", contract size = "+DoubleToString(cs,0)); } // 获取账户类型, 对冲或净持 accounttype=(int)AccountInfoInteger(ACCOUNT_MARGIN_MODE); }
形成三角
为了形成三角, 我们需要考虑以下几个方面:
- 数据来自市场观察窗口或预先准备的文件。
- 我们是否在测试器中?如果是的话, 则将品种上传到市场观察。上传所有可能的品种是没有意义的, 因为普通的家用电脑无法承受负载。搜索预先准备的包含测试器品种的文件。否则, 在标准三角: EUR + USD + GBP 上测试策略。
- 为了简化代码, 引入一个限制: 所有的三角品种应有相同的合约大小。
- 不要忘记, 三角只能以货币对构成。
第一个必要的函数是利用来自市场观察的品种形成三角。
void fnGetThreeFromMarketWatch(stThree &MxSmb[]) { // 获取品种总数 int total=SymbolsTotal(true); // 用来比较合约大小的变量 double cs1=0,cs2=0; // 使用第一次循环列表中的第一个品种 for(int i=0;i<total-2 && !IsStopped();i++) {//1 string sm1=SymbolName(i,true); // 检查品种的各种限制 if(!fnSmbCheck(sm1)) continue; // 获取合约大小, 并将之常规化, 因为我们稍后会比较这个值 if (!SymbolInfoDouble(sm1,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; cs1=NormalizeDouble(cs1,0); // 获取基准货币和盈利货币, 因为它们要用来比较 (而非货币对名称) string sm1base=SymbolInfoString(sm1,SYMBOL_CURRENCY_BASE); string sm1prft=SymbolInfoString(sm1,SYMBOL_CURRENCY_PROFIT); // 从第二次循环列表中取下一个品种 for(int j=i+1;j<total-1 && !IsStopped();j++) {//2 string sm2=SymbolName(j,true); if(!fnSmbCheck(sm2)) continue; if (!SymbolInfoDouble(sm2,SYMBOL_TRADE_CONTRACT_SIZE,cs2)) continue; cs2=NormalizeDouble(cs2,0); string sm2base=SymbolInfoString(sm2,SYMBOL_CURRENCY_BASE); string sm2prft=SymbolInfoString(sm2,SYMBOL_CURRENCY_PROFIT); // 在第一个和第二个货币对中应该有一种货币相匹配。 // 否则, 它们不能形成一个三角。 // 进行全面的匹配测试没有意义。例如, 这是不可能的 // 形成 eurusd 和 eurusd.xxx 的三角. if(sm1base==sm2base || sm1base==sm2prft || sm1prft==sm2base || sm1prft==sm2prft); else continue; // 合约应有相似的大小 if (cs1!=cs2) continue; // 搜索第三次循环中的最后一个三角品种 for(int k=j+1;k<total && !IsStopped();k++) {//3 string sm3=SymbolName(k,true); if(!fnSmbCheck(sm3)) continue; if (!SymbolInfoDouble(sm3,SYMBOL_TRADE_CONTRACT_SIZE,cs1)) continue; cs1=NormalizeDouble(cs1,0); string sm3base=SymbolInfoString(sm3,SYMBOL_CURRENCY_BASE); string sm3prft=SymbolInfoString(sm3,SYMBOL_CURRENCY_PROFIT); // 我们知道第一个和第二个品种有一个共同的货币。若要形成一个三角, 我们应该找到 // 第三个货币对内应有一种货币与第一个品种中的货币相匹配, 且其第二个货币匹配 // 第二个品种中的货币如果没有匹配, 这个货币对不能用来形成一个三角。 if(sm3base==sm1base || sm3base==sm1prft || sm3base==sm2base || sm3base==sm2prft);else continue; if(sm3prft==sm1base || sm3prft==sm1prft || sm3prft==sm2base || sm3prft==sm2prft);else continue; if (cs1!=cs2) continue; // 到达这个阶段, 意味着所有的检查都已经通过了, 且三个已检测货币对适于形成一个三角 // 将其写入数组 int cnt=ArraySize(MxSmb); ArrayResize(MxSmb,cnt+1); MxSmb[cnt].smb1.name=sm1; MxSmb[cnt].smb2.name=sm2; MxSmb[cnt].smb3.name=sm3; break; }//3 }//2 }//1 }
第二个必要的函数是从文件中读取三角
void fnGetThreeFromFile(stThree &MxSmb[]) { // 如果没有找到含有品种的文件, 显示相应的消息并停止工作 int fh=FileOpen(FILENAME,FILE_UNICODE|FILE_READ|FILE_SHARE_READ|FILE_CSV); if(fh==INVALID_HANDLE) { Print("未能读到品种文件!"); ExpertRemove(); } // 将指针移动到文件的开头 FileSeek(fh,0,SEEK_SET); // 跳过标题行 (文件的第一行) while(!FileIsLineEnding(fh)) FileReadString(fh); while(!FileIsEnding(fh) && !IsStopped()) { // 得到三角的三个品种。我们来进行数据可用性的基本检查 // 机器人能够自动形成三角文件。如果一位用户 // 未能正确修改它, 我们假定这是故意的 string smb1=FileReadString(fh); string smb2=FileReadString(fh); string smb3=FileReadString(fh); // 如果品种的数据可用, 在到达行尾后将它们写入我们的三角数组 if (!csmb.Name(smb1) || !csmb.Name(smb2) || !csmb.Name(smb3)) {while(!FileIsLineEnding(fh)) FileReadString(fh);continue;} int cnt=ArraySize(MxSmb); ArrayResize(MxSmb,cnt+1); MxSmb[cnt].smb1.name=smb1; MxSmb[cnt].smb2.name=smb2; MxSmb[cnt].smb3.name=smb3; while(!FileIsLineEnding(fh)) FileReadString(fh); } }
本节所需的最后一个函数是前两个函数的包装。它负责根据 EA 输入来选择三角的来源。另外, 检查机器人的启动位置。如果在测试器当中, 无论用户选择什么, 都可以从文件中上传三角。如果没有文件, 下载默认的 EURUSD + GBPUSD + EURGBP 三角。
void fnSetThree(stThree &MxSmb[],enMode mode) { // 重置我们的三角数组 ArrayFree(MxSmb); // 检查我们是否在测试器中 if((bool)MQLInfoInteger(MQL_TESTER)) { // 如果是的话, 查找一个品种文件并从文件启动三角的上传 if(FileIsExist(FILENAME)) fnGetThreeFromFile(MxSmb); // 如果未找到文件, 遍历所有可用品种查找其中默认的 EURUSD + GBPUSD + EURGBP 三角 else{ char cnt=0; for(int i=SymbolsTotal(false)-1;i>=0;i--) { string smb=SymbolName(i,false); if ((SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="GBP") || (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="EUR" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD") || (SymbolInfoString(smb,SYMBOL_CURRENCY_BASE)=="GBP" && SymbolInfoString(smb,SYMBOL_CURRENCY_PROFIT)=="USD")) { if (SymbolSelect(smb,true)) cnt++; } else SymbolSelect(smb,false); if (cnt>=3) break; } // 在市场观察中上载默认的三角之后, 启动三角 fnGetThreeFromMarketWatch(MxSmb); } return; } // 如果我们不在测试器当中, 查看用户选择的模式: // 从市场观察或从文件中获取品种 if(mode==STANDART_MODE || mode==CREATE_FILE) fnGetThreeFromMarketWatch(MxSmb); if(mode==USE_FILE) fnGetThreeFromFile(MxSmb); }
此处我们使用一个辅助函数 — fnSmbCheck()。它检查所用品种是否有任何限制。若是, 则跳过。下面是它的代码。
bool fnSmbCheck(string smb) { // 三角只能由货币对组成 if(SymbolInfoInteger(smb,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX) return(false); // 如果有交易限制, 跳过此品种 if(SymbolInfoInteger(smb,SYMBOL_TRADE_MODE)!=SYMBOL_TRADE_MODE_FULL) return(false); // 如果是合约的开始或结束, 也跳过该品种, 因为在处理货币时不使用该参数 if(SymbolInfoInteger(smb,SYMBOL_START_TIME)!=0)return(false); if(SymbolInfoInteger(smb,SYMBOL_EXPIRATION_TIME)!=0) return(false); // 可用的订单类型。虽然机器人只进行市价订单交易, 但不应有限制 int som=(int)SymbolInfoInteger(smb,SYMBOL_ORDER_MODE); if((SYMBOL_ORDER_MARKET&som)==SYMBOL_ORDER_MARKET); else return(false); if((SYMBOL_ORDER_LIMIT&som)==SYMBOL_ORDER_LIMIT); else return(false); if((SYMBOL_ORDER_STOP&som)==SYMBOL_ORDER_STOP); else return(false); if((SYMBOL_ORDER_STOP_LIMIT&som)==SYMBOL_ORDER_STOP_LIMIT); else return(false); if((SYMBOL_ORDER_SL&som)==SYMBOL_ORDER_SL); else return(false); if((SYMBOL_ORDER_TP&som)==SYMBOL_ORDER_TP); else return(false); // 为了数据可用性检查标准库 if(!csmb.Name(smb)) return(false); // 以下检查仅在实际操作中需要, 因为在某些情况下, 出于某些原因 SymbolInfoTick 接收价格 // 而竞卖价或竞买价依旧为 0。 // 在测试器中禁用, 因为价格可能会在稍后出现。 if(!(bool)MQLInfoInteger(MQL_TESTER)) { MqlTick tk; if(!SymbolInfoTick(smb,tk)) return(false); if(tk.ask<=0 || tk.bid<=0) return(false); } return(true); }
所以, 三角就形成了。forming 函数置于 fnSetThree.mqh 包含文件中。检查品种限制的函数置于单独的 fnSmbCheck.mqh 文件中。
我们形成了所有可能的三角。它们当中的货币对可以按照任意顺序排列, 这会带来很多不便, 因为我们需要确定如何通过其它货币对来表示一个货币对。为了建立订单, 我们来研究使用 EUR-USD-GBP 所有可能的位置选项作为例子:
# | 品名 1 | 品名 2 | 品名 3 | |
---|---|---|---|---|
1 | EURUSD = | GBPUSD | х | EURGBP |
2 | EURUSD = | EURGBP | х | GBPUSD |
3 | GBPUSD = | EURUSD | / | EURGBP |
4 | GBPUSD = | EURGBP | 0 | EURUSD |
5 | EURGBP = | EURUSD | / | GBPUSD |
6 | EURGBP = | GBPUSD | 0 | EURUSD |
‘x’ = 乘以, ‘/’ = 除以。’0′ = 不可能动作
在上面的表格中, 我们可以看到, 三角可以用 6 种可能的方式来形成, 虽然其中的两个 — 第 4 行和第 6 行 — 不允许通过其余两个表示第一个品种。这意味着, 这些选项应该被丢弃。其余 4 个选项是相同的。无论我们想表达什么品种, 以及我们用什么品种来表达, 都无关紧要。唯一重要的是速度。除法比乘法慢, 因此选项 3 和 5 被丢弃。剩下的唯一选项是第 1 行和第 2 行。
我们来研究方案 2, 因为它易于理解。因此, 我们不必为第一, 第二和第三个品种引入额外的字段。这是不可能的, 因为我们交易所有可能的三角而非单一的三角。
我们选择的便利性: 既然我们进行套利交易, 这个策略意味着一个中性的仓位, 我们应该买卖相同的资产。例如: 买入 0.7 手 EURUSD 并 抛售 0.7 手 EURGBP — 我们买卖 €70 000。因此, 我们有一笔仓位, 实际上我们已经游离在市场之外, 因为在买卖中 (虽然表达方式不同) 出现同样的数量。我们需要交易 GBPUSD 来调整它们。换句话说, 我们马上知道品种 1 和 2 应该有相似的交易量, 但方向不同。预先也知道, 第三对的交易量等于第二对的价格。
在三角中正确排列货币对的函数:
void fnChangeThree(stThree &MxSmb[]) { int count=0; for(int i=ArraySize(MxSmb)-1;i>=0;i--) {//for // 首先, 我们来确定第三个位置。 // 这一货币对的基准货币与其它两个基准货币不匹配 string sm1base="",sm2base="",sm3base=""; // 如果由于某种原因我们无法得到基准货币, 我们不会使用这个三角操作 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="";continue;} // 如果品种 1 和 2 的基准货币相同, 则跳过此步骤。否则, 交换货币对的位置 if(sm1base!=sm2base) { if(sm1base==sm3base) { string temp=MxSmb[i].smb2.name; MxSmb[i].smb2.name=MxSmb[i].smb3.name; MxSmb[i].smb3.name=temp; } if(sm2base==sm3base) { string temp=MxSmb[i].smb1.name; MxSmb[i].smb1.name=MxSmb[i].smb3.name; MxSmb[i].smb3.name=temp; } } // 现在, 我们来定义第一个和第二个位置。 // 第二个位置是与第三个基准货币匹配的利润货币对。 // 在这种情况下, 我们总是使用乘法。 sm3base=SymbolInfoString(MxSmb[i].smb3.name,SYMBOL_CURRENCY_BASE); string sm2prft=SymbolInfoString(MxSmb[i].smb2.name,SYMBOL_CURRENCY_PROFIT); // 交换第一和第二对的位置。 if(sm3base!=sm2prft) { string temp=MxSmb[i].smb1.name; MxSmb[i].smb1.name=MxSmb[i].smb2.name; MxSmb[i].smb2.name=temp; } // 显示已处理三角的消息。 Print("使用三角: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name); count++; }// // 通知操作中使用的三角总数。 Print("全部使用的三角: "+(string)count); }
该函数整个放在单独的 fnChangeThree.mqh 文件中。
完成三角准备所需的最后一步: 立即上传所用货币对的所有数据, 以便之后不必再花时间申请。我们需要以下:
- 每个品种的最小和最大交易量;
- 价格和交易量舍入的字符数;
- Point 和 Ticksize 变量。我从未遇到过它们不同时的情况。无论如何, 我们得到所有的数据, 并在必要时使用它们。
void fnSmbLoad(double lot,stThree &MxSmb[]) { // 用来打印的简单宏定义 #define prnt(nm) {nm="";Print("不正确的上载: "+nm);continue;} // 循环遍历所有形成的三角。在此, 同一品种重复数据请求会过度消耗我们的时间 // 但由于这个操作只在加载机器人的时候执行, 所以为了减少代码, 我们仍然可以这样做。 // 使用标准库来获取数据。 for(int i=ArraySize(MxSmb)-1;i>=0;i--) { // 通过上传品种到 CSymbolInfo 类, 我们初始化了所有必要数据的集合 // 检查它们的可用性。如果出现问题, 三角标记为不可操作。 if (!csmb.Name(MxSmb[i].smb1.name)) prnt(MxSmb[i].smb1.name); // 获得每个品种的 _capacity MxSmb[i].smb1.digits=csmb.Digits(); // 将滑点从整数转换为小数点。我们将需要这种格式进行进一步的计算 MxSmb[i].smb1.dev=csmb.TickSize()*DEVIATION; // 为了将报价转换为点数, 通常我们必须将价格除以 _Point 值。 // 把这个值显示为 1/Point 是比较合理的, 这样我们就可以用乘法代替除法。 // 没有检查 csmb.Point() 是否为 0: 它不能等于 0, 但如果 // 由于某种原因没有收到参数, 三角按 if (!csmb.Name(MxSmb[i].smb1.name)) 这行排序。 MxSmb[i].smb1.Rpoint=int(NormalizeDouble(1/csmb.Point(),0)); // 我们要将手数舍入到小数位数。 MxSmb[i].smb1.digits_lot=csup.NumberCount(csmb.LotsStep()); // 交易量限制 (一次性常规化) MxSmb[i].smb1.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb1.digits_lot); MxSmb[i].smb1.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb1.digits_lot); MxSmb[i].smb1.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb1.digits_lot); // 合约大小 MxSmb[i].smb1.contract=csmb.ContractSize(); // 同上, 但取自品种 2 if (!csmb.Name(MxSmb[i].smb2.name)) prnt(MxSmb[i].smb2.name); MxSmb[i].smb2.digits=csmb.Digits(); MxSmb[i].smb2.dev=csmb.TickSize()*DEVIATION; MxSmb[i].smb2.Rpoint=int(NormalizeDouble(1/csmb.Point(),0)); MxSmb[i].smb2.digits_lot=csup.NumberCount(csmb.LotsStep()); MxSmb[i].smb2.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb2.digits_lot); MxSmb[i].smb2.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb2.digits_lot); MxSmb[i].smb2.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb2.digits_lot); MxSmb[i].smb2.contract=csmb.ContractSize(); // 同上, 但针对品种 3 if (!csmb.Name(MxSmb[i].smb3.name)) prnt(MxSmb[i].smb3.name); MxSmb[i].smb3.digits=csmb.Digits(); MxSmb[i].smb3.dev=csmb.TickSize()*DEVIATION; MxSmb[i].smb3.Rpoint=int(NormalizeDouble(1/csmb.Point(),0)); MxSmb[i].smb3.digits_lot=csup.NumberCount(csmb.LotsStep()); MxSmb[i].smb3.lot_min=NormalizeDouble(csmb.LotsMin(),MxSmb[i].smb3.digits_lot); MxSmb[i].smb3.lot_max=NormalizeDouble(csmb.LotsMax(),MxSmb[i].smb3.digits_lot); MxSmb[i].smb3.lot_step=NormalizeDouble(csmb.LotsStep(),MxSmb[i].smb3.digits_lot); MxSmb[i].smb3.contract=csmb.ContractSize(); // 取齐交易量。对于 货币对和整个三角都有限制。 // 货币对限制写在这里: MxSmb[i].smbN.lotN // 三角限制写在这里: MxSmb[i].lotN // 选择所有最低数值中的最高值。将它舍入到最大值。 // 整个代码块仅适用于交易量大致如下的情况: 0.01 + 0.01 + 0.1。 // 在这种情况下, 尽可能少的交易量被设置为 0.1 并四舍五入到小数点后 1 位。 double lt=MathMax(MxSmb[i].smb1.lot_min,MathMax(MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min)); MxSmb[i].lot_min=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot))); // 另外, 最低交易量从最高交易量中取出并立即舍入。 lt=MathMin(MxSmb[i].smb1.lot_max,MathMin(MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max)); MxSmb[i].lot_max=NormalizeDouble(lt,(int)MathMax(MxSmb[i].smb1.digits_lot,MathMax(MxSmb[i].smb2.digits_lot,MxSmb[i].smb3.digits_lot))); // 如果交易量输入参数为 0, 则使用尽可能少的交易量, 但并非取每对最少的, // 而是所有对中最少的一个。 if (lot==0) { MxSmb[i].smb1.lot=MxSmb[i].lot_min; MxSmb[i].smb2.lot=MxSmb[i].lot_min; MxSmb[i].smb3.lot=MxSmb[i].lot_min; } else { // 如果您需要取齐交易量, 那么您知道货币对 1 和 2 的值, 而第三个交易量是在输入之前计算的。 MxSmb[i].smb1.lot=lot; MxSmb[i].smb2.lot=lot; // 如果投入的交易量不在当前的限制范围内, 则三角不能在操作中使用。 // 使用警报通知这一点 if (lot<MxSmb[i].smb1.lot_min || lot>MxSmb[i].smb1.lot_max || lot<MxSmb[i].smb2.lot_min || lot>MxSmb[i].smb2.lot_max) { MxSmb[i].smb1.name=""; Alert("三角: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - 交易量不正确"); continue; } } } }
函数可以在单独的 fnSmbLoad.mqh 文件中找到。
这就是有关形成三角的全部内容。我们继续前进。
EA 操作模式
启动机器人时, 我们可以选择一种可用的操作模式:
- 来自市场观察的品名。
- 来自文件的品名。
- 用品名创建文件。
“来自市场观察的品名” 意味着我们在当前品种上启动机器人, 并从市场观察窗口形成操作的三角。这是主要的操作模式, 不需要额外的处理。
“来自文件的品名” 不同于第一个仅从三角获得来源 — 从以前准备好的文件。
“用品名创建文件” 创建一个三角文件, 以备将来在第二种操作模式或测试器中使用。这种模式只假定形成三角。之后, EA 操作完成。
我们来描述一下这个逻辑:
if(inMode==CREATE_FILE) { // 删除文件, 如果它存在。 FileDelete(FILENAME); int fh=FILEOPENWRITE(FILENAME); if (fh==INVALID_HANDLE) { Alert("品种文件未创建"); ExpertRemove(); } // 将三角和一些其它数据写入文件 fnCreateFileSymbols(MxThree,fh); Print("品种文件已创建"); // 关闭文件并完成 EA 操作 FileClose(fh); ExpertRemove(); }
将数据写入文件的函数很简单, 不需要额外的注释:
void fnCreateFileSymbols(stThree &MxSmb[], int filehandle) { // 在文件中定义头文件 FileWrite(filehandle,"品名 1","品名 2","品名 3","合约大小 1","合约大小 2","合约大小 3", "最小手数 1","最小手数 2","最小手数 3","最大手数 1","最大手数 2","最大手数 3","手数增量 1","手数增量 2","手数增量 3", "公用最小手数","公用最大手数","小数位 1","小数位 2","小数位 3"); // 根据上面指定的头文件填写文件 for(int i=ArraySize(MxSmb)-1;i>=0;i--) { FileWrite(filehandle,MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name, MxSmb[i].smb1.contract,MxSmb[i].smb2.contract,MxSmb[i].smb3.contract, MxSmb[i].smb1.lot_min,MxSmb[i].smb2.lot_min,MxSmb[i].smb3.lot_min, MxSmb[i].smb1.lot_max,MxSmb[i].smb2.lot_max,MxSmb[i].smb3.lot_max, MxSmb[i].smb1.lot_step,MxSmb[i].smb2.lot_step,MxSmb[i].smb3.lot_step, MxSmb[i].lot_min,MxSmb[i].lot_max, MxSmb[i].smb1.digits,MxSmb[i].smb2.digits,MxSmb[i].smb3.digits); } FileWrite(filehandle,""); // 在所有品名之后留下一个空字符串 // 操作完成后, 出于安全原因将所有数据写入磁盘 FileFlush(filehandle); }
除了三角之外, 我们还会写入额外的数据: 允许交易量, 合约大小, 报价单数量。我们只需要从文件中获取这些数据来直观地检查品种的属性。
该函数置于一个单独的 fnCreateFileSymbols.mqh 文件中。
重新启动机器人
我们已近乎完成了 EA 的初始设置。不过, 我们仍然有一个问题需要回答: 如何处理崩溃后的恢复?我们不必担心短时间的互联网连接断线。重新连接到网络后, 机器人恢复运行。但如果我们必须重新启动机器人, 那么我们需要记住当前位置, 并从此处继续操作。
下面是解决机器人重新启动问题的函数:
void fnRestart(stThree &MxSmb[],ulong magic,int accounttype) { string smb1,smb2,smb3; long tkt1,tkt2,tkt3; ulong mg; uchar count=0; // 还原三角的计数器 switch(accounttype) { // 在对冲账户中恢复仓位非常容易: 遍历所有未平仓位, 使用魔幻数字定义持仓 // 并将它们组合为三角 // 如果净持账户, 情况会变得更加复杂 - 首先, 我们需要参考保存的仓位数据库, 这些仓位是由机器人打单的。 // 搜索必要仓位并将其恢复为三角的算法已经以相当直接的方式实现了, 没有任何装饰和 // 优化。但是, 由于这个阶段是不经常需要的, 我们可能会忽略其性能 // 以便简化代码。 case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: // 遍历所有已开仓位, 并检测魔幻数字匹配。 // 记住第一个检测到的仓位的魔幻数字: 用它来检测另外两个。 for(int i=PositionsTotal()-1;i>=2;i--) {//for i smb1=PositionGetSymbol(i); mg=PositionGetInteger(POSITION_MAGIC); if (mg<magic || mg>(magic+MAGIC)) continue; // 记住票号, 以便更方便地访问这个仓位。 tkt1=PositionGetInteger(POSITION_TICKET); // 寻找具有相同魔幻数字的第二个仓位。 for(int j=i-1;j>=1;j--) {//for j smb2=PositionGetSymbol(j); if (mg!=PositionGetInteger(POSITION_MAGIC)) continue; tkt2=PositionGetInteger(POSITION_TICKET); // 查找最后的仓位。 for(int k=j-1;k>=0;k--) {//for k smb3=PositionGetSymbol(k); if (mg!=PositionGetInteger(POSITION_MAGIC)) continue; tkt3=PositionGetInteger(POSITION_TICKET); // 如果您到达这个阶段, 已经找齐了已开单三角。数据已下载。机器人在下一笔分笔报价时计算其余数据。 for(int m=ArraySize(MxSmb)-1;m>=0;m--) {//for m // 遍历三角数组, 忽略已开三角。 if (MxSmb[m].status!=0) continue; // "bluntly" 完成。起初, 我们似乎可以 // 多次参考相同 货币对若干次。但事实并非如此, 因为在检测到另一种货币对之后, // 我们从下一对继续我们的搜索, 而不是从搜索循环的开始if ( (MxSmb[m].smb1.name==smb1 || MxSmb[m].smb1.name==smb2 || MxSmb[m].smb1.name==smb3) && (MxSmb[m].smb2.name==smb1 || MxSmb[m].smb2.name==smb2 || MxSmb[m].smb2.name==smb3) && (MxSmb[m].smb3.name==smb1 || MxSmb[m].smb3.name==smb2 || MxSmb[m].smb3.name==smb3)); else continue; // 我们已经检测到这个三角。现在, 我们为其分配适当的状态 MxSmb[m].status=2; MxSmb[m].magic=magic; MxSmb[m].pl=0; // 按所需顺序排列单号。三角已经还原了。 if (MxSmb[m].smb1.name==smb1) MxSmb[m].smb1.tkt=tkt1; if (MxSmb[m].smb1.name==smb2) MxSmb[m].smb1.tkt=tkt2; if (MxSmb[m].smb1.name==smb3) MxSmb[m].smb1.tkt=tkt3; if (MxSmb[m].smb2.name==smb1) MxSmb[m].smb2.tkt=tkt1; if (MxSmb[m].smb2.name==smb2) MxSmb[m].smb2.tkt=tkt2; if (MxSmb[m].smb2.name==smb3) MxSmb[m].smb2.tkt=tkt3; if (MxSmb[m].smb3.name==smb1) MxSmb[m].smb3.tkt=tkt1; if (MxSmb[m].smb3.name==smb2) MxSmb[m].smb3.tkt=tkt2; if (MxSmb[m].smb3.name==smb3) MxSmb[m].smb3.tkt=tkt3; count++; break; }//for m }//for k }//for j }//for i break; default: break; } if (count>0) Print("Restore "+(string)count+" triangles"); }
和以前一样, 这个函数在一个单独的文件中: fnRestart.mqh
最后一步:
ctrade.SetDeviationInPoints(DEVIATION); ctrade.SetTypeFilling(ORDER_FILLING_FOK); ctrade.SetAsyncMode(true); ctrade.LogLevel(LOG_LEVEL_NO); EventSetTimer(1);
注意发送订单的异步模式。策略假定最大的操作行为, 所以我们使用这种安置模式。还有一些复杂的情况: 我们需要额外的代码来跟踪其是否成功开单。我们在下面研究这一切。
OnInit() 模块已经完成。是进入机器人实体的时候了。
OnTick
首先, 我们来看看设置中是否对最大允许的三角数量有限制。如果存在这样的限制, 并且我们已经达到了这个限制, 那么可以跳过此分笔报价时刻的大部分代码:
ushort OpenThree=0; // 开单的三角数量 for(int j=ArraySize(MxThree)-1;j>=0;j--) if (MxThree[j].status!=0) OpenThree++; // 未平单的也被考虑在内
检查很简单。我们声明了一个局部变量来计数已开单的三角, 并在一个循环中遍历我们的主要数组。如果三角状态不为 0, 那么它是激活的。
计算已开单三角后 (如果限制允许), 查看所有剩余的三角并跟踪其状态。fnCalcDelta() 函数负责此任务:
if (inMaxThree==0 || (inMaxThree>0 && inMaxThree>OpenThree)) fnCalcDelta(MxThree,inProfit,inCmnt,inMagic,inLot,inMaxThree,OpenThree); // 计算偏离并立即开单
我们来详细分析代码:
void fnCalcDelta(stThree &MxSmb[],double prft, string cmnt, ulong magic,double lot, ushort lcMaxThree, ushort &lcOpenThree) { double temp=0; string cmnt_pos=""; for(int i=ArraySize(MxSmb)-1;i>=0;i--) {//for i // 如果三角已激活, 则跳过它 if(MxSmb[i].status!=0) continue; // 重新检查所有三个货币对的可用性: 如果至少有一个不可用, // 那么计算整个三角就没有意义 if (!fnSmbCheck(MxSmb[i].smb1.name)) continue; if (!fnSmbCheck(MxSmb[i].smb2.name)) continue; // 其中一个货币对平单 if (!fnSmbCheck(MxSmb[i].smb3.name)) continue; // 在每次分笔报价伊始计算开单三角的数量, // 但是它们也可在分笔报价之内开单。所以, 要持续跟踪它们的数量 if (lcMaxThree>0) {if (lcMaxThree>lcOpenThree); else continue;} // 之后, 获取所有必要的数据进行计算。 // 获取分笔报价的价格。 if(!SymbolInfoDouble(MxSmb[i].smb1.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb1.tv)) continue; if(!SymbolInfoDouble(MxSmb[i].smb2.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb2.tv)) continue; if(!SymbolInfoDouble(MxSmb[i].smb3.name,SYMBOL_TRADE_TICK_VALUE,MxSmb[i].smb3.tv)) continue; // 获取当前价格。 if(!SymbolInfoTick(MxSmb[i].smb1.name,MxSmb[i].smb1.tick)) continue; if(!SymbolInfoTick(MxSmb[i].smb2.name,MxSmb[i].smb2.tick)) continue; if(!SymbolInfoTick(MxSmb[i].smb3.name,MxSmb[i].smb3.tick)) continue; // 检查竞买价和竞卖价是否为 0。 if(MxSmb[i].smb1.tick.ask<=0 || MxSmb[i].smb1.tick.bid<=0 || MxSmb[i].smb2.tick.ask<=0 || MxSmb[i].smb2.tick.bid<=0 || MxSmb[i].smb3.tick.ask<=0 || MxSmb[i].smb3.tick.bid<=0) continue; // 计算第三对的交易量。我们知道前两对的交易量 — 它相同且固定。 // 第三对的交易量总是在变化。但只在初始变量中手数不为 0 时才会计算。 // 如果手数为零, 则使用最小 (相似) 交易量。 // 交易量计算逻辑很简单。我们回到我们的三角: EURUSD=EURGBP*GBPUSD。买入或卖出 GBP 数量 // 直接取决于 EURGBP 的报价, 而在第三个货币对中, 这第三种货币先到。通过使用第二个货币对的价格作为一个交易量, // 我们消减了一些计算。我已经取得了竞卖价和竞买价之间的平均价格。 // 不要忘记调整输入交易量。 if (lot>0) MxSmb[i].smb3.lot=NormalizeDouble((MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.tick.bid)/2*MxSmb[i].smb1.lot,MxSmb[i].smb3.digits_lot); // 如果计算的交易量超过允许的边界, 通知用户。 // 三角标记为非操作 if (MxSmb[i].smb3.lot<MxSmb[i].smb3.lot_min || MxSmb[i].smb3.lot>MxSmb[i].smb3.lot_max) { Alert("计算 ",MxSmb[i].smb3.name," 超界。Min/Max/Calc: ", DoubleToString(MxSmb[i].smb3.lot_min,MxSmb[i].smb3.digits_lot),"/", DoubleToString(MxSmb[i].smb3.lot_max,MxSmb[i].smb3.digits_lot),"/", DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); Alert("三角: "+MxSmb[i].smb1.name+" "+MxSmb[i].smb2.name+" "+MxSmb[i].smb3.name+" - 禁用"); MxSmb[i].smb1.name=""; continue; } // 计算我们的成本, 即点差+佣金。pr = 点差的整数形式点数。 // 点差妨碍我们使用这种策略赚钱, 因此, 应随时考虑到这一点。 // 您可用以点数为单位的点差替代差价乘以相反的点数。 MxSmb[i].smb1.sppoint=NormalizeDouble(MxSmb[i].smb1.tick.ask-MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits)*MxSmb[i].smb1.Rpoint; MxSmb[i].smb2.sppoint=NormalizeDouble(MxSmb[i].smb2.tick.ask-MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits)*MxSmb[i].smb2.Rpoint; MxSmb[i].smb3.sppoint=NormalizeDouble(MxSmb[i].smb3.tick.ask-MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)*MxSmb[i].smb3.Rpoint; if (MxSmb[i].smb1.sppoint<=0 || MxSmb[i].smb2.sppoint<=0 || MxSmb[i].smb3.sppoint<=0) continue; // 现在, 我们来计算存款币种的点差。 // 在货币中, 1 个分笔报价的价格总是等于 SYMBOL_TRADE_TICK_VALUE。 // 另外, 不要忘记交易量 MxSmb[i].smb1.spcost=MxSmb[i].smb1.sppoint*MxSmb[i].smb1.tv*MxSmb[i].smb1.lot; MxSmb[i].smb2.spcost=MxSmb[i].smb2.sppoint*MxSmb[i].smb2.tv*MxSmb[i].smb2.lot; MxSmb[i].smb3.spcost=MxSmb[i].smb3.sppoint*MxSmb[i].smb3.tv*MxSmb[i].smb3.lot; // 那么, 这里是指定交易量加上用户指定佣金后的成本 MxSmb[i].spread=MxSmb[i].smb1.spcost+MxSmb[i].smb2.spcost+MxSmb[i].smb3.spcost+prft; // 我们可以跟踪投资组合的竞卖价<竞买价的情况, 但这种情况很少见 // 且可单独考虑。同时, 时间上的套利空间也能够处理这样的情况。 // 仓位是没有风险的, 这就是为什么: 假设您已经买入了 eurusd, // 并立即抛售 eurgbp 和 gbpusd。 // 换言之, 我们看到 eurusd 竞买价 < eurgbp 竞买价 * gbpusd 竞买价。这种情况很多, 但是这对于一个成功的入场是不够的。 // 计算点差成本。当竞卖价<竞买价时, 不要机械地入场, 而要等到它们之间的差值 // 超过点差成本。 // 我们同意买入意味着买入第一个品种, 然后卖出剩下的两个品种, // 而卖出意味着卖出第一个货币双并买入其余的两个货币双。 temp=MxSmb[i].smb1.tv*MxSmb[i].smb1.Rpoint*MxSmb[i].smb1.lot; // 我们仔细看看计算公式。 // 1. 在括号内, 每个价格都针对较差方向的滑点进行了调整: MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev // 2. 如上面等式所示, eurgbp 竞买价 * gbpusd 竞买价 - 乘以第二和第三个品种的价格: // (MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev) // 3. 然后, 计算竞卖价和竞买价之间的差值 // 4. 我们收到的以点数为单位的差价现在应该转换为币值: 乘以 // 点数价格和交易量。取第一个货币对的数值。 // 如果我们通过把所有的货币对放在一侧来构建一个三角, 并与 1 进行比较, 就会有更多的计算。 MxSmb[i].PLBuy=((MxSmb[i].smb2.tick.bid-MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.bid-MxSmb[i].smb3.dev)-(MxSmb[i].smb1.tick.ask+MxSmb[i].smb1.dev))*temp; MxSmb[i].PLSell=((MxSmb[i].smb1.tick.bid-MxSmb[i].smb1.dev)-(MxSmb[i].smb2.tick.ask+MxSmb[i].smb2.dev)*(MxSmb[i].smb3.tick.ask+MxSmb[i].smb3.dev))*temp; // 我们已得到买入或卖出三角后可盈利或亏损的汇总计算。 // 现在, 我们只需将其与成本进行比较, 以便做出是否入场交易的决策。我们将所有数值均常规化到小数点后两位。 MxSmb[i].PLBuy= NormalizeDouble(MxSmb[i].PLBuy,2); MxSmb[i].PLSell= NormalizeDouble(MxSmb[i].PLSell,2); MxSmb[i].spread= NormalizeDouble(MxSmb[i].spread,2); // 如果有潜在的利润, 检查资金是否足够开单。 if (MxSmb[i].PLBuy>MxSmb[i].spread || MxSmb[i].PLSell>MxSmb[i].spread) { // 我只简单计算了入场买入的保证金。由于它比卖出稍高, 我们不必考虑交易方向。 // 还要注意递增因子。如果保证金不足, 我们不能为三角开单。默认的递增因子是 20% if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb1.name,MxSmb[i].smb1.lot,MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.mrg)) if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb2.name,MxSmb[i].smb2.lot,MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.mrg)) if(OrderCalcMargin(ORDER_TYPE_BUY,MxSmb[i].smb3.name,MxSmb[i].smb3.lot,MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.mrg)) if(AccountInfoDouble(ACCOUNT_MARGIN_FREE)>((MxSmb[i].smb1.mrg+MxSmb[i].smb2.mrg+MxSmb[i].smb3.mrg)*CF)) //check the free margin { // 我们几乎已为开单做好了各种准备。现在只需从我们的范围内找到一个可用的魔幻数字。 // 初始的魔幻数字是在 inMagic 变量中指定的。默认值是 300。 // 魔幻数字的范围在 MAGIC 定义中指定, 默认值是 200。 MxSmb[i].magic=fnMagicGet(MxSmb,magic); if (MxSmb[i].magic<=0) { // 若为 0, 则所有的魔幻数字都被占用。通知此消息并退出。 Print("可用魔幻数字结束/n新三角不会开单"); break; } // 设置检测到的魔幻数字 ctrade.SetExpertMagicNumber(MxSmb[i].magic); // 写一个三角的注释 cmnt_pos=cmnt+(string)MxSmb[i].magic+" 开单"; // 开单, 同时记住三角开单的时间。 // 这是避免等待所必需的。 // 默认情况下, 完全开单的等待时间在 MAXTIMEWAIT 定义中设置为 3 秒。 // 如果在这段时间内我们未能完全开单, 则所有已开单均要平单。 MxSmb[i].timeopen=TimeCurrent(); if (MxSmb[i].PLBuy>MxSmb[i].spread) fnOpen(MxSmb,i,cmnt_pos,true,lcOpenThree); if (MxSmb[i].PLSell>MxSmb[i].spread) fnOpen(MxSmb,i,cmnt_pos,false,lcOpenThree); // 打印有关三角开单的消息。 if (MxSmb[i].status==1) Print("开单三角: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" 魔幻数字: "+(string)MxSmb[i].magic); } } }//for i }
该函数带有详细的注释和解释, 令一切都很清楚。有两件事情已经被遗忘了: 我已应用的可用魔幻数字选择机制和三角开单。
以下是我们如何选择可用魔幻数字:
ulong fnMagicGet(stThree &MxSmb[],ulong magic) { int mxsize=ArraySize(MxSmb); bool find; // 我们可以遍历数组中的所有开单三角。 // 但我已选择了另外一个选项 - 遍历魔幻数字的范围, // 然后沿数组移动选定的一个。 for(ulong i=magic;i<magic+MAGIC;i++) { find=false; // 魔幻数字 i。我们来看看它是否被分配到了任何一个已开单三角。 for(int j=0;j<mxsize;j++) if (MxSmb[j].status>0 && MxSmb[j].magic==i) { find=true; break; } // 如果不使用魔幻数字, 则不等待其完成即退出循环。 if (!find) return(i); } return(0); }
此处是我们如何为三角开单:
bool fnOpen(stThree &MxSmb[],int i,string cmnt,bool side, ushort &opt) { // 首个订单开单标志。 bool openflag=false; // 无授权则不能交易。 if (!cterm.IsTradeAllowed()) return(false); if (!cterm.IsConnected()) return(false); switch(side) { case true: // 如果在发送开单后返回 "假", 则发送剩余的两个货币对没有意义。 // 我们在下一次分笔报价时再次尝试。另外, 机器人不能部分进行三角开单。 // 如果某些部分在发送订单后未开单, 等待 // MAXTIMEWAIT 定义中设置的时间, 然后将部分开单的三角平单。 if(ctrade.Buy(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt)) { openflag=true; MxSmb[i].status=1; opt++; // 进一步的逻辑是相同的: 如果无法开单, 则该三角平单。 if(ctrade.Sell(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt)) ctrade.Sell(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt); } break; case false: if(ctrade.Sell(MxSmb[i].smb1.lot,MxSmb[i].smb1.name,0,0,0,cmnt)) { openflag=true; MxSmb[i].status=1; opt++; if(ctrade.Buy(MxSmb[i].smb2.lot,MxSmb[i].smb2.name,0,0,0,cmnt)) ctrade.Buy(MxSmb[i].smb3.lot,MxSmb[i].smb3.name,0,0,0,cmnt); } break; } return(openflag); }
像往常一样, 上面的函数位于单独的 fnCalcDelta.mqh, fnMagicGet.mqh 和 fnOpen.mqh 文件中。
所以, 我们已经找到了必要的三角, 并将其送出开单。在 MetaTrader 4 以及 MetaTrader 5 对冲账户中, 这实际上意味着 EA 操作的结束。但是我们仍然需要跟踪三角开单的结果。我不打算使用 OnTrade 和 OnTradeTransaction 事件, 因为它们不能保证获得成功。代之, 我要检查当前仓位的数量 — 一个 100% 的指标。
我们来看看开仓管理函数:
void fnOpenCheck(stThree &MxSmb[], int accounttype, int fh) { uchar cnt=0; // 三角中开仓计数器 ulong tkt=0; // 当前单号 string smb=""; // 当前品种 // 检查我们的三角阵列 for(int i=ArraySize(MxSmb)-1;i>=0;i--) { // 只考虑具有状态 1 的三角, 即被送出用于开单 if(MxSmb[i].status!=1) continue; if ((TimeCurrent()-MxSmb[i].timeopen)>MAXTIMEWAIT) { // 如果超出开单时间, 请将三角标记为准备平单 MxSmb[i].status=3; Print("未正确开单: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name); continue; } cnt=0; switch(accounttype) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: // 检查所有未结仓位。针对每个三角执行此检查。 for(int j=PositionsTotal()-1;j>=0;j--) if (PositionSelectByTicket(PositionGetTicket(j))) if (PositionGetInteger(POSITION_MAGIC)==MxSmb[i].magic) { // 获取品种并考虑仓位的单号。tkt=PositionGetInteger(POSITION_TICKET); smb=PositionGetString(POSITION_SYMBOL); // 检查在所考虑的三角中是否有我们需要的当前仓位。 // 如果是, 增加计数, 并记住单号和开单价格。 if (smb==MxSmb[i].smb1.name){ cnt++; MxSmb[i].smb1.tkt=tkt; MxSmb[i].smb1.price=PositionGetDouble(POSITION_PRICE_OPEN);} else if (smb==MxSmb[i].smb2.name){ cnt++; MxSmb[i].smb2.tkt=tkt; MxSmb[i].smb2.price=PositionGetDouble(POSITION_PRICE_OPEN);} else if (smb==MxSmb[i].smb3.name){ cnt++; MxSmb[i].smb3.tkt=tkt; MxSmb[i].smb3.price=PositionGetDouble(POSITION_PRICE_OPEN);} // 如果有三个必需的仓位, 我们的三角已经成功开单。将其状态更改为 2 (开单)。 // 将开单数据写入日志文件 if (cnt==3) { MxSmb[i].status=2; fnControlFile(MxSmb,i,fh); break; } } break; default: break; } } }
写日志文件的函数很简单:
void fnControlFile(stThree &MxSmb[],int i, int fh) { FileWrite(fh,"============"); FileWrite(fh,"开单:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name); FileWrite(fh,"单号:",MxSmb[i].smb1.tkt,MxSmb[i].smb2.tkt,MxSmb[i].smb3.tkt); FileWrite(fh,"手数",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); FileWrite(fh,"Margin",DoubleToString(MxSmb[i].smb1.mrg,2),DoubleToString(MxSmb[i].smb2.mrg,2),DoubleToString(MxSmb[i].smb3.mrg,2)); FileWrite(fh,"竞卖价",DoubleToString(MxSmb[i].smb1.tick.ask,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.ask,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.ask,MxSmb[i].smb3.digits)); FileWrite(fh,"竞买价",DoubleToString(MxSmb[i].smb1.tick.bid,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tick.bid,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tick.bid,MxSmb[i].smb3.digits)); FileWrite(fh,"开单价格",DoubleToString(MxSmb[i].smb1.price,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.price,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.price,MxSmb[i].smb3.digits)); FileWrite(fh,"点值",DoubleToString(MxSmb[i].smb1.tv,MxSmb[i].smb1.digits),DoubleToString(MxSmb[i].smb2.tv,MxSmb[i].smb2.digits),DoubleToString(MxSmb[i].smb3.tv,MxSmb[i].smb3.digits)); FileWrite(fh,"点差点数",DoubleToString(MxSmb[i].smb1.sppoint,0),DoubleToString(MxSmb[i].smb2.sppoint,0),DoubleToString(MxSmb[i].smb3.sppoint,0)); FileWrite(fh,"点差 $",DoubleToString(MxSmb[i].smb1.spcost,3),DoubleToString(MxSmb[i].smb2.spcost,3),DoubleToString(MxSmb[i].smb3.spcost,3)); FileWrite(fh,"所有点差",DoubleToString(MxSmb[i].spread,3)); FileWrite(fh,"买入盈亏",DoubleToString(MxSmb[i].PLBuy,3)); FileWrite(fh,"卖出盈亏",DoubleToString(MxSmb[i].PLSell,3)); FileWrite(fh,"魔幻数字",string(MxSmb[i].magic)); FileWrite(fh,"开单时间",TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_SECONDS)); FileWrite(fh,"当前时间",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); FileFlush(fh); }
所以, 我们找到了一个已入场并相应开仓的三角。现在, 我们需要计算我们赚了多少。
void fnCalcPL(stThree &MxSmb[], int accounttype, double prft) { // 再次遍历我们的三角数组。 // 开单和平单的速度是这一策略的重要组成部分。 // 因此, 只要我们发现已平单的三角, 立即将之平单。 bool flag=cterm.IsTradeAllowed() & cterm.IsConnected(); for(int i=ArraySize(MxSmb)-1;i>=0;i--) {//for // 我们只对状态为 2 或 3 的三角感兴趣。 // 如果三角只是部分开单, 我们会得到状态 3 (三角平单) if(MxSmb[i].status==2 || MxSmb[i].status==3); else continue; // 我们来计算三角形的盈利 if (MxSmb[i].status==2) { MxSmb[i].pl=0; // 盈利清零 switch(accounttype) {//switch case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: if (PositionSelectByTicket(MxSmb[i].smb1.tkt)) MxSmb[i].pl=PositionGetDouble(POSITION_PROFIT); if (PositionSelectByTicket(MxSmb[i].smb2.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT); if (PositionSelectByTicket(MxSmb[i].smb3.tkt)) MxSmb[i].pl+=PositionGetDouble(POSITION_PROFIT); break; default: break; }//switch // 四舍五入到小数点后两位 MxSmb[i].pl=NormalizeDouble(MxSmb[i].pl,2); // 我们来近距离查看平单。我使用以下逻辑: // 套利的情况不正常, 不应该发生。当它出现时, 我们可以期待返回 // 到没有套利的状态。我们可以赚钱吗?换句话说, 我们不知道, // 是否我们能够继续获得利润。因此, 我希望在点差和佣金被抹平之后立即平仓。 // 三角套利是以点数计算的。您不能在此依赖大走势。 // 尽管您可以等待输入中 Commission 变量的期望利润。 // 如果我们赚得比我们花费的多, 则将 "送出平单" 状态分配给该仓位。 if (flag && MxSmb[i].pl>prft) MxSmb[i].status=3; } // 仅在允许交易的情况下将三角平单。 if (flag && MxSmb[i].status==3) fnCloseThree(MxSmb,accounttype,i); }//for }
负责三角平单的函数很简单:
void fnCloseThree(stThree &MxSmb[], int accounttype, int i) { // 在平单之前, 检查三角中所有货币对的可用性。 // 裂解三角是错误的, 极端危险。在净持账户上操作时, // 这可能会导致以后的仓位混乱。 if(fnSmbCheck(MxSmb[i].smb1.name)) if(fnSmbCheck(MxSmb[i].smb2.name)) if(fnSmbCheck(MxSmb[i].smb3.name)) // 如果全部可用, 则使用标准库将全部三个仓位平仓。 // 平仓后, 检查操作是否成功。 switch(accounttype) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: ctrade.PositionClose(MxSmb[i].smb1.tkt); ctrade.PositionClose(MxSmb[i].smb2.tkt); ctrade.PositionClose(MxSmb[i].smb3.tkt); break; default: break; } }
我们的工作近乎完成。现在, 我们只需检查平仓是否成功, 并在屏幕上显示一条消息。如果机器人什么都不写, 好似它未工作。
以下是我们对成功平仓的检查。我们可以实现一个单独的函数, 简单地通过改变交易方向来开仓和平仓, 但是我不喜欢这个选项, 因为这两个操作之间存在轻微的程序差异。
检查是否平仓成功:
void fnCloseCheck(stThree &MxSmb[], int accounttype,int fh) { // 遍历三角数组。 for(int i=ArraySize(MxSmb)-1;i>=0;i--) { // 我们只对那些状态为 3 的感兴趣, 即已平仓或送出平仓的那些。 if(MxSmb[i].status!=3) continue; switch(accounttype) { case ACCOUNT_MARGIN_MODE_RETAIL_HEDGING: // 如果从三角中不能选择一个货币对, 则平仓成功。返回状态 0 if (!PositionSelectByTicket(MxSmb[i].smb1.tkt)) if (!PositionSelectByTicket(MxSmb[i].smb2.tkt)) if (!PositionSelectByTicket(MxSmb[i].smb3.tkt)) { // 意味着平仓已成功 MxSmb[i].status=0; Print("三角平仓: "+MxSmb[i].smb1.name+" + "+MxSmb[i].smb2.name+" + "+MxSmb[i].smb3.name+" 魔幻数字: "+(string)MxSmb[i].magic+" P/L: "+DoubleToString(MxSmb[i].pl,2)); // 将平仓数据写入日志文件。 if (fh!=INVALID_HANDLE) { FileWrite(fh,"============"); FileWrite(fh,"平单:",MxSmb[i].smb1.name,MxSmb[i].smb2.name,MxSmb[i].smb3.name); FileWrite(fh,"手数",DoubleToString(MxSmb[i].smb1.lot,MxSmb[i].smb1.digits_lot),DoubleToString(MxSmb[i].smb2.lot,MxSmb[i].smb2.digits_lot),DoubleToString(MxSmb[i].smb3.lot,MxSmb[i].smb3.digits_lot)); FileWrite(fh,"单号",string(MxSmb[i].smb1.tkt),string(MxSmb[i].smb2.tkt),string(MxSmb[i].smb3.tkt)); FileWrite(fh,"魔幻数字",string(MxSmb[i].magic)); FileWrite(fh,"盈利",DoubleToString(MxSmb[i].pl,3)); FileWrite(fh,"当前时间",TimeToString(TimeCurrent(),TIME_DATE|TIME_SECONDS)); FileFlush(fh); } } break; } } }
最后, 我们在屏幕上显示一条注释以供直观确认。我们显示以下内容:
- 跟踪的三角总数
- 开单三角
- 最近五个开单三角
- 开单三角, 如果有的话
以下是函数代码:
void fnCmnt(stThree &MxSmb[], ushort lcOpenThree) { int total=ArraySize(MxSmb); string line="=============================/n"; string txt=line+MQLInfoString(MQL_PROGRAM_NAME)+": ON/n"; txt=txt+"三角总计: "+(string)total+"/n"; txt=txt+"开单三角: "+(string)lcOpenThree+"/n"+line; // 屏幕上显示的最大三角数量 short max=5; max=(short)MathMin(total,max); // 显示最近五个开单三角 short index[]; // 索引数字 ArrayResize(index,max); ArrayInitialize(index,-1); // 未使用 short cnt=0,num=0; while(cnt<max && num<total) // 第一个最大平仓三角索引作为开始 { if(MxSmb[num].status!=0) {num++;continue;} index[cnt]=num; num++;cnt++; } // 只有当元素的数量超过了屏幕上可以显示的数量时, 才能进行排序和搜索。 if (total>max) for(short i=max;i<total;i++) { // 开单三角显示在下面。 if(MxSmb[i].status!=0) continue; for(short j=0;j<max;j++) { if (MxSmb[i].PLBuy>MxSmb[index[j]].PLBuy) {index[j]=i;break;} if (MxSmb[i].PLSell>MxSmb[index[j]].PLSell) {index[j]=i;break;} } } // 显示最近开单的三角。 bool flag=true; for(short i=0;i<max;i++) { cnt=index[i]; if (cnt<0) continue; if (flag) { txt=txt+"品种1 品种2 品种3 买入盈亏 卖出盈亏 点差/n"; flag=false; } txt=txt+MxSmb[cnt].smb1.name+" + "+MxSmb[cnt].smb2.name+" + "+MxSmb[cnt].smb3.name+":"; txt=txt+" "+DoubleToString(MxSmb[cnt].PLBuy,2)+" "+DoubleToString(MxSmb[cnt].PLSell,2)+" "+DoubleToString(MxSmb[cnt].spread,2)+"/n"; } // 显示开单三角。 txt=txt+line+"/n"; for(int i=total-1;i>=0;i--) if (MxSmb[i].status==2) { txt=txt+MxSmb[i].smb1.name+"+"+MxSmb[i].smb2.name+"+"+MxSmb[i].smb3.name+" P/L: "+DoubleToString(MxSmb[i].pl,2); txt=txt+" 开单时间: "+TimeToString(MxSmb[i].timeopen,TIME_DATE|TIME_MINUTES|TIME_SECONDS); txt=txt+"/n"; } Comment(txt); }
测试
有可能在分笔报价模拟模式下进行测试, 并与真实分笔报价测试进行比较。我们可以更进一步比较基于真实分笔报价的实际行动的测试结果, 并得出结论: 多元测试器距现实尚远。
结果表明, 您平均每周可以进行 3-4 次交易。大多数情况下, 在夜间开仓, 三角通常含有 TRY, NOK, SEK 等低流动性货币。机器人的利润取决于交易量。由于交易不频繁, EA 可以轻松处理大交易量, 并与其它机器人并行工作。
机器人的风险很容易计算: 3 个点差 * 开单三角的数量。
为了准备我们可以使用的货币对, 我建议首先显示所有的品种, 然后隐藏那些禁止交易和非货币对的品种。可以使用多货币策略粉丝所不可或缺的脚本来快速完成: https://www.mql5.com/zh/market/product/25256
我还应提醒您, 测试器的历史不会从经纪商的服务器上传 – 应该预先上传到客户终端。因此, 这应该在测试之前单独完成, 或者再次使用上述脚本。
发展前景
我们能改善结果吗? 当然可以。要做到这一点, 我们需要做流动性汇聚。这种方法的缺点是需要在多个经纪商开户。
我们也可以加速测试结果。这可以通过两种方式来完成。第一步是引入一个离散计算, 持续跟踪三角, 其入场概率非常高。第二种方法是使用 OpenCL, 对于这个机器人来说这非常合理。
文章中使用的文件
# | 文件名 | 描述 |
---|---|---|
1 | var.mqh | 描述所有应用的变量, 定义和输入。 |
2 | fnWarning.mqh | 检查 EA 正确操作的初始条件: 输入, 环境, 设置。 |
3 | fnSetThree.mqh | 形成货币对三角。货币对的来源也可以在这里选择—- 市场观察或预先准备的文件。 |
4 | fnSmbCheck.mqh | 检查品种的可用性和其它限制的函数。注意: 机器人不会检查交易和报价时段。 |
5 | fnChangeThree.mqh | 改变三角中的货币对位置, 以统一的方式形成它们。 |
6 | fnSmbLoad.mqh | 上传各品种, 价格, 点数, 交易量限制等数据。 |
7 | fnCalcDelta.mqh | 考虑三角中的所有分量, 以及所有的附加成本: 点差, 佣金, 滑点。 |
8 | fnMagicGet.mqh | 搜索可用于当前三角的魔幻数字。 |
9 | fnOpenCheck.mqh | 检查三角是否成功开单。 |
10 | fnCalcPL.mqh | 计算三角利润/亏损。 |
11 | fnCreateFileSymbols.mqh | 用三角创建交易文件的函数。该文件还含有其它数据 (更多信息)。 |
12 | fnControlFile.mqh | 维护日志文件的函数。它包含所有开单和平单的必要数据。 |
13 | fnCloseThree.mqh | 三角平仓。 |
14 | fnCloseCheck.mqh | 检查三角是否完全平仓。 |
15 | fnCmnt.mqh | 在屏幕上显示注释。 |
16 | fnRestart.mqh | 当启动机器人时, 检查是否有以前打开的三角。如果有的话, 恢复并跟踪它们。 |
17 | fnOpen.mqh | 三角开单。 |
18 | Support.mqh | 额外的支持类。它只有一个函数 — 计算分数的小数位数。 |
19 | head.mqh | 描述所有上述文件的头文件。 |
本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/3150
MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。