在创建一个算法用于自动交易时,您不仅应能处理价格以形成交易信号,还应能获取大量有关在“EA 交易”操作上施加限制的辅助信息。本文将指导您:
要接收有关交易时段的信息,您应使用 SymbolInfoSessionTrade() 函数,而报价时段则要使用相应的 SymbolInfoSessionQuote() 函数。两个函数的工作方式相同:如果存在具有星期中指定日子的指定索引的时段(时段的索引从零开始),函数返回 true。时段的起始时间写入由链接传递的第四个和第五个参数。
//--- 检查是否有编号为session_index的报价时段 bool session_exist=SymbolInfoSessionQuote(symbol,day,session_index,start,finish);要找出所有指定日期的时段,在循环中调用此函数,直至其返回 false。
//+------------------------------------------------------------------+ //| 显示报价时段信息 | //+------------------------------------------------------------------+ void PrintInfoForQuoteSessions(string symbol,ENUM_DAY_OF_WEEK day) { //--- 时段起始和终止时间 datetime start,finish; uint session_index=0; bool session_exist=true; //--- go over all sessions of this day while(session_exist) { //--- 检查是否存在编号为session_index的报价时段 session_exist=SymbolInfoSessionQuote(symbol,day,session_index,start,finish); //--- 如果有这样的时段 if(session_exist) { //--- 显示它的星期,时段编号以及起始和终止时间 Print(DayToString(day),": 时段编号=",session_index," 起始时间=", TimeToString(start,TIME_MINUTES)," 终止时间=",TimeToString(finish-1,TIME_MINUTES|TIME_SECONDS)); } //--- 增加时段计数器 session_index++; } }
星期几通过接收 ENUM_DAY_OF_WEEK 枚举值作为参数的自定义函数以字符串格式显示。
//+------------------------------------------------------------------+ //| 取得星期几的字符串表现形式 | //+------------------------------------------------------------------+ string DayToString(ENUM_DAY_OF_WEEK day) { switch(day) { case SUNDAY: return "星期日"; case MONDAY: return "星期一"; case TUESDAY: return "星期二"; case WEDNESDAY: return "星期三"; case THURSDAY: return "星期四"; case FRIDAY: return "星期五"; case SATURDAY: return "星期六"; default: return "未知的日子"; } return ""; }
SymbolInfoSession.mq5 脚本的最终代码附于本文的底部。此处仅提供其主要部分。
void OnStart() { //--- 保存星期数据的数组 ENUM_DAY_OF_WEEK days[]={SUNDAY,MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY}; int size=ArraySize(days); //--- Print("报价时段"); //--- 遍历一星期中的每一天 for(int d=0;d<size;d++) { PrintInfoForQuoteSessions(Symbol(),days[d]); } //--- Print("交易时段"); //--- 遍历一星期的每一天 for(int d=0;d<size;d++) { PrintInfoForTradeSessions(Symbol(),days[d]); } }
要找出开仓或增大持仓所需的预付款量,您可以使用 OrderCalcMargin() 函数;传递给函数的第一个参数是来自 ENUM_ORDER_TYPE 枚举的值。对于买入操作,您应通过 ORDER_TYPE_BUY 参数进行调用;卖出使用 ORDER_TYPE_SELL 参数。函数返回的预付款量取决于手数和开盘价。
void OnStart() { //--- 取得预付款数值的变量 double margin; //--- 取得最后一个订单预付款数值的变量 MqlTick last_tick; //--- 尝试读取最后一笔订单的数值 if(SymbolInfoTick(Symbol(),last_tick)) { //--- 重设最后的错误代码 ResetLastError(); //--- 计算预付款数值 bool check=OrderCalcMargin(type,Symbol(),lots,last_tick.ask,margin); if(check) { PrintFormat("交易操作 %s %s %.2f lot at %G 需要的预付款等于 %.2f %s",OrderTypeToString(type), Symbol(),lots,last_tick.ask,margin,AccountInfoString(ACCOUNT_CURRENCY)); } } else { Print("SymbolInfoTick() 函数执行失败, 错误 ",GetLastError()); } }
应当注意的是,OrderCalcMargin() 函数不仅可以计算市价单的预付款值,还可以计算挂单的预付款值。您可以使用 Check_Money.mq5 脚本检查为所有类型订单返回的值。
由于在某些交易系统中挂单可能也同样需要资金支持,OrderCalcMargin() 函数用于计算挂单的预付款的大小。通常而言,挂单的预付款大小通过买入持仓和卖出持仓的预付款大小的系数计算。
标识符 |
说明 |
属性类型 |
SYMBOL_MARGIN_LONG |
买入持仓预付款收取率 |
双精度 |
SYMBOL_MARGIN_SHORT |
卖出持仓预付款收取率 |
双精度 |
SYMBOL_MARGIN_LIMIT |
限价订单预付款收取率 |
双精度 |
SYMBOL_MARGIN_STOP |
止损订单预付款收取率 |
双精度 |
SYMBOL_MARGIN_STOPLIMIT |
限价止损订单预付款收取率 |
双精度 |
您可以使用下述简单代码获取这些系数的值:
//--- 计算不同类型订单的预付款率 PrintFormat("即时买入订单所需要的预付款率等于 %G",SymbolInfoDouble(Symbol(),SYMBOL_MARGIN_LONG)); PrintFormat("即时卖出订单所需要的预付款率等于 %G",SymbolInfoDouble(Symbol(),SYMBOL_MARGIN_SHORT)); PrintFormat("限价订单所需要的预付款率等于 %G",SymbolInfoDouble(Symbol(),SYMBOL_MARGIN_LIMIT)); PrintFormat("止损订单所需要的预付款率等于 %G",SymbolInfoDouble(Symbol(),SYMBOL_MARGIN_STOP)); PrintFormat("止损限价订单所需要的预付款率等于 %G",SymbolInfoDouble(Symbol(),SYMBOL_MARGIN_STOPLIMIT));
对于 Forex 交易品种,挂单的预付款收取率通常等于 0,即对挂单没有预付款要求。
Check_Money.mq5 脚本执行结果。
取决于预付款收取的方式,如果挂单有预付款要求,资金管理系统以及交易系统自身可能存在某些限制。这就是这些参数同样还是“EA 交易”操作天然限制的原因。
当设置了止损位时,要做好其触发的准备。应将资金潜在损失的风险考虑在内;而 OrderCalcProfit() 正是基于此目的。该函数与我们之前讨论过的 OrderCalcMargin() 函数极为相似,但前者要求开盘价和收盘价均用于计算。
指定 ENUM_ORDER_TYPE 枚举的两个值的其中之一作为首个参数 - ORDER_TYPE_BUY 或 ORDER_TYPE_SELL;其他订单类型将会引发错误。在最后一个参数中,您应通过引用传递一个 OrderCalcProfit() 函数在成功执行后将会将盈利/损失值写入其中的变量。
通过指定的进入和退出位关闭买入持仓时,使用 CalculateProfitOneLot() 函数计算盈利或损失的示例:
//+------------------------------------------------------------------+ //| 计算买入一手的潜在利润/亏损 | //+------------------------------------------------------------------+ double CalculateProfitOneLot(double entry_price,double exit_price) { //--- 取得利润值的变量 double profit=0; if(!OrderCalcProfit(ORDER_TYPE_BUY,Symbol(),1.0,entry_price,exit_price,profit)) { Print(__FUNCTION__," 计算 OrderCalcProfit() 失败. 错误 ",GetLastError()); } //--- return(profit); }
该函数的计算结果如图所示。
使用 OrderCalcProfit() 函数计算和在图表上显示潜在损失的示例。
完整的代码请见随附的“EA 交易”CalculateProfit_EA.mq5。
许多交易系统的开发假设仅在出现新柱时计算交易信号;且所有的交易操作仅执行一次。MetaTrader 5 客户端中策略测试程序的“仅开盘价”模式对检查此类自动交易系统大有裨益。
在“仅开盘价”模式下,测试时“EA 交易”中指标的所有计算和 OnTick() 函数的调用仅执行一次/柱。这是最快的交易模式;并且通常是对轻微的价格波动最具容错性的交易系统创建方式。当然,“EA 交易”中使用的指标也应正确编写,并在新柱出现时不应扭曲指标的值。
“仅开盘价”模式的策略测试程序可消除您对“EA 交易”每柱仅启动一次的担忧;并且该测试程序使用极为方便。然而,如果在模拟或真实帐户上以实时模式工作时,交易人员应控制“EA 交易”的行为,使其针对每个接收到的信号仅执行一次交易操作。实现此目的最简单的方式是追踪当前未形成柱的开启。
要获取最后一个柱的开启时间,您应使用 SeriesInfoInteger() 函数及指定的信号名称、时间表和 SERIES_LASTBAR_DATE 属性。通过将当前柱的开启时间不断地与存储在变量中的柱进行比较,您很容易检测到新柱出现的时间。这允许您创建如下所示的自定义函数 isNewBar():
//+------------------------------------------------------------------+ //| 如果在交易品种/周期中出现新柱 | //+------------------------------------------------------------------+ bool isNewBar() { //--- 在静态变量中记录上一个柱的开启时间 static datetime last_time=0; //--- 当前时间 datetime lastbar_time=SeriesInfoInteger(Symbol(),Period(),SERIES_LASTBAR_DATE); //--- 如果是第一次调用此函数 if(last_time==0) { //--- 设置时间并退出 last_time=lastbar_time; return(false); } //--- 如果时间不同 if(last_time!=lastbar_time) { //--- 记录时间并返回true last_time=lastbar_time; return(true); } //--- 如果我们运行到这一行,说明柱不是新的,返回false return(false); }
函数的使用示例在随附的“EA 交易”CheckLastBar.mq5 中给出。
有关新柱在 M1 时间表中出现的 CheckLastBar“EA 交易”消息。
如果您需要限制在一个帐户中可同时出现的活动挂单的数量,您可以编写您自己的自定义函数来达到目的。我们将此函数命名为 IsNewOrderAllowed();它将检查是否允许下达挂单。编写此函数时请遵从“自动交易锦标赛”的规则。
//+------------------------------------------------------------------+ //| 检查是否允许新开订单 | //+------------------------------------------------------------------+ bool IsNewOrderAllowed() { //--- 得到一个账户允许的最大挂单数 int max_allowed_orders=(int)AccountInfoInteger(ACCOUNT_LIMIT_ORDERS); //--- 如果没有限制,返回true,您可以发送一个订单 if(max_allowed_orders==0) return(true); //--- 如果我们运行到这一行,说明有限制,检测已经有多少个活动订单 int orders=OrdersTotal(); //--- 返回比较结果 return(orders<max_allowed_orders); }
函数十分简单:获取允许的订单数量至 max_allowed_orders 变量;并且如果其值不等于零,则与 当前订单数量进行比较。然而,此函数未考虑另一可能的限制 - 具体交易品种允许的开仓和挂单的总量的限制。
要获得具体交易品种的开仓的大小,您应使用 PositionSelect() 函数选择一个持仓。仅在该步骤之后,您才可以使用 PositionGetDouble() 请求开仓的交易量;函数返回具有双精度型的选定持仓的各种属性。现在我们将编写 PostionVolume() 函数以获取指定交易品种的持仓量。
//+------------------------------------------------------------------+ //| 返回指定交易品种的持仓大小 | //+------------------------------------------------------------------+ double PositionVolume(string symbol) { //--- 选择交易品种的一个仓位 bool selected=PositionSelect(symbol); //--- 仓位存在 if(selected) //--- 返回仓位大小 return(PositionGetDouble(POSITION_VOLUME)); else { //--- 报告选择仓位失败的信息 Print(__FUNCTION__," 执行PositionSelect()失败, 交易品种 ", symbol," 错误 ",GetLastError()); return(-1); } }
在为某个交易品种下达挂单 发出交易请求前,应检查该交易品种的开仓和挂单的总量限制 - SYMBOL_VOLUME_LIMIT。如果没有限制,则挂单量不能超出可使用 SymbolInfoDouble() 量接收的最大允许量。
double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); if(max_volume==0) volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX);
然而,此方法未考虑指定交易品种的当前挂单量。让我们编写一个计算该值的函数:
//+------------------------------------------------------------------+ //| 返回指定交易品种的持仓大小 | //+------------------------------------------------------------------+ double PositionVolume(string symbol) { //--- 根据交易品种选择仓位 bool selected=PositionSelect(symbol); //--- 仓位存在 if(selected) //--- 返回仓位大小 return(PositionGetDouble(POSITION_VOLUME)); else { //--- 如果没有仓位则返回0 return(0); } }
在将开仓量和挂单量纳入考虑后,最终检查代码如下所示:
//+------------------------------------------------------------------+ //| 返回一个交易品种允许的最大新开订单交易量 | //+------------------------------------------------------------------+ double NewOrderAllowedVolume(string symbol) { double allowed_volume=0; //--- 取得订单最大交易量的限制 double symbol_max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); //--- 取得交易品种最大交易量限制 double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_LIMIT); //--- 取得交易品种的持仓量 double opened_volume=PositionVolume(symbol); if(opened_volume>=0) { //--- 如果我们已经用完了可用的订单量 if(max_volume-opened_volume<=0) return(0); //--- 持仓量没有超过 max_volume double orders_volume_on_symbol=PendingsVolume(symbol); allowed_volume=max_volume-opened_volume-orders_volume_on_symbol; if(allowed_volume>symbol_max_volume) allowed_volume=symbol_max_volume; } return(allowed_volume); }
包含在本节中述及的该函数的 Check_Order_And_Volume_Limits.mq5“EA 交易”的完整代码随附于本文。
“2010 自动交易锦标赛”某参赛者帐户使用 Check_Order_And_Volume_Limits“EA 交易”的检查示例。
交易机器人的一个重要部分是选择正确的交易量执行交易操作的能力。在这里,我们并非是要探讨资金管理系统和风险管理系统,而是讨论针对相应交易品种属性的正确交易量。
标识符 |
说明 |
属性类型 |
SYMBOL_VOLUME_MIN |
最小交易量 |
双精度 |
SYMBOL_VOLUME_MAX |
最大交易量 |
双精度 |
SYMBOL_VOLUME_STEP |
交易执行的最小量更改步骤 |
双精度 |
要进行该验证,我们可以编写自定义函数 CheckVolumeValue():
//+------------------------------------------------------------------+ //| 检查订单量的正确性 | //+------------------------------------------------------------------+ bool CheckVolumeValue(double volume,string &description) { //--- 交易操作中允许的最小订单量 double min_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MIN); if(volume<min_volume) { description=StringFormat("交易量小于允许的最小值 SYMBOL_VOLUME_MIN=%.2f",min_volume); return(false); } //--- 交易操作中允许的最大订单量 double max_volume=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_MAX); if(volume>max_volume) { description=StringFormat("交易量大于允许的最大值 SYMBOL_VOLUME_MAX=%.2f",max_volume); return(false); } //--- 取得交易量允许的最小改变步长 double volume_step=SymbolInfoDouble(Symbol(),SYMBOL_VOLUME_STEP); int ratio=(int)MathRound(volume/volume_step); if(MathAbs(ratio*volume_step-volume)>0.0000001) { description=StringFormat("交易量不是最小步长的倍数 SYMBOL_VOLUME_STEP=%.2f, 最接近的正确交易量是 %.2f", volume_step,ratio*volume_step); return(false); } description="正确的交易量 "; return(true); }
您可以使用本文随附的 CheckVolumeValue.mq5 脚本检查该函数的运行。
检查量是否正确的 CheckVolumeValue.mq5 的消息。
本文论述了对使用“EA 交易”的可能限制的基本验证,这些在创建自己的自动交易系统时可能会碰到。文中的示例并未涵盖在交易帐户上操作“EA 交易”的过程中应检查的所有可能情形。我希望这些示例可以帮助初学者理解如何以 MQL5 语言实施最常用的验证。
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程