简介
任何交易系统的生存期都可简化为开仓和平仓。这一点毫无疑问。但如果涉及到算法的实现,则有多少编程人员就有多少种选择。每个人都会以自己的方式来解决同样的问题,但殊途同归。
经过多年的编程实践,各种构建“EA 交易”的逻辑和结构的方法已一一试遍。现在可以说已开发出用于所有代码的清晰的模式模板。
该方法不能说具有 100% 的普适性,但它可能改变您设计“EA 交易”的逻辑的方法。而且这不是关乎您想要使用“EA 交易”处理订单的能力。重点是创建交易模型的原则。
1. 设计交易系统的原则和事件源的类型
大多数人采用的设计算法的基本方法,是从开仓起追踪持仓直至平仓。这是一种线性方法。若您想要对代码进行更改 – 由于有大量的条件出现且代码累积带来新的分析方向,这通常会导致极其复杂的情况。
建立交易机器人模型的最佳解决方案是“服务于条件”。其基本原则是 – 不要分析该“EA 交易”条件和其持仓以及订单是如何发生的 – 而是分析我们现在要如何做。该基本原则从根本上改变了交易的管理并简化了代码的开发。
接下来考虑更多细节。
1.1. “服务于条件”原则
正如前文所述,“EA 交易”并不需要知道当前状态是如何达成的。它需要知道的是,根据环境现在要做什么(参数值、存储的订单属性等)。
该原则和“EA 交易”存在于从循环到循环(特别是从订单号到订单号)的事实直接相关,它无需关心订单在上一订单号发生的事情。因此,您必须使用事件驱动的方法管理订单。换言之,“EA 交易”在当前订单号保存自己的状态,作为下一订单号相关决策的出发点。
例如,您必须移除所有“EA 交易”的挂单,然后才能继续分析指标并下达新的订单。我们看到的大部分代码示例使用 “while (true) {try to remove}” 循环或稍稍不那么生硬的 “while (k < 1000) {try to remove; k++;}” 循环。我们将跳过该变体,它只对移除命令进行了一次调用而无错误分析。
该方法是线性的,它将占用“EA 交易”不确定的时长。
因此,不循环“EA 交易”而是存储删除订单的顺序才是正确之举,这样在每个新的订单号处尝试删除挂单时都会检查该订单。在这种情况下,“EA交易”通过读取状态参数就会知道,在这一刻它必须删除订单。然后它将尝试删除订单。如果发生交易错误,“EA 交易”将只会在下一个循环开始前阻止进一步的分析和工作。
1.2. 设计的第二个主要原则 – 尽可能地抽象化考虑的持仓方向(买入/卖出)、货币和图表。所有“EA 交易”的功能应以这样一种方式实施,即仅在确实无法避免的极少数情况下分析方向或交易品种(例如,在您考虑开仓的有利价格增长时,尽管存在避免细节的不同选项)。始终尽量避免此类“低水平”设计。这将减少代码和至少两次的函数编写过程。并且可以实现“独立于交易”。
该原则的实施是用宏函数取代订单类型、交易品种参数和依赖性计算参数的显式分析。在后续文章中我们将详细介绍该实施。
1.3. 第三个原则 – 将算法细分为逻辑语义(独立模块)
在实践中,我们可以说,最佳方法是将“EA 交易”操作划分为单独的函数。我想您一定会同意,在一个函数中编写“EA 交易”的整个算法是很困难的,而且这也使后续的分析和编辑复杂化。所以我们在 MQL5 中要避免这样做,尤其是 MQL5 现在几乎可提供对环境的完全控制。
因此,逻辑语义(如开仓订单、追踪订单、平仓订单)应根据对环境参数和事件的完全分析彼此单独实施。通过这一方法,“EA 交易”的设计变得灵活起来。您可以很容易地向其添加新的独立模块而不会影响现有模块,或是禁用现有模块而无需更改主代码。
这三项原则使得为所有“EA 交易”创建单一的原型成为可能,您可以很容易地修改该原型或使其适应任何给定任务。
“EA 交易”系统的事件源:
1. 指标。 一个例子是对指标线条值、它们的交叉和组合等的分析。同时,指标可以是:当前时间、从网络获得的数据等。在大多数情况下,指标事件用于以信号指示订单开仓和平仓。较少用于它们的调整(通常为指标的追踪止损订单或挂单)。
例如,指标的实际实施可以是调用一个“EA 交易”,该“EA 交易”分析快速和慢速 MA 与沿交叉方向的进一步开仓的交叉。
2. 现有订单、持仓及其状态。例如,当前损失或利润规模、存在/不存在持仓或挂单、已平仓持仓的利润等。由于相比指标事件它们的关系存在更多选项,这些事件的实际实施要远为宽泛和多样化。
仅基于交易事件的最简单的“EA 交易”示例是重新填入以平均现有持仓,然后将其输出至所需利润。换言之,可用持仓存在的损失将成为下达新平均订单的事件。
或,例如,追踪止损。在价格从上一止损以指定的点数移动至利润时,该函数检查事件。作为结果,“EA 交易”在价格后拉动止损。
3. 外部事件。 虽然此类事件通常不会出现在纯粹的“EA 交易”系统中,但通常而言,在作出决策时需要将其考虑在内。这包括调整订单、持仓,处理交易错误、图表事件(移动/创建/删除对象、按下按钮等)。一般来说,这些是无法在历史数据中验证且仅在“EA 交易”工作时出现的事件。
此类“EA 交易”的一个突出示例是带图形交易控制的交易信息系统。
各种“EA 交易”都是基于这三种事件源的组合。
2. CExpertAdvisor 基类 – “EA 交易”构造函数
“EA 交易”的工作是什么?MQL 程序交互的基本框架如下图所示。
图 1. MQL 程序元素交互的基本框架
如您在框架中所看到的,首先是工作循环的入口(这可以是订单号或计时器信号)。在这一阶段,该订单号可以在第一个程序块中过滤掉而不加以处理。这在“EA 交易”仅需处理新柱的订单号而无需处理所有订单号,或在不允许“EA 交易”工作的情况下发生。
然后程序执行第二个程序块 – 处理订单和持仓的模块,此时事件处理程序块从模块中调用。每个模块仅可查询与其相关的事件。
该序列可称之为带直接逻辑的框架,因其首先确定“EA 交易”将要做什么(使用哪些事件处理模块),然后它才实施如何以及为什么要做(获取事件信号)。
直接逻辑和我们对世界的认知以及泛逻辑一致。毕竟,我们首先是思考具体概念,接下来才能对其进行终结,然后是将它们分类以及识别它们的相互关系。
设计“EA 交易”在这方面也不例外。首先是声明“EA 交易”应该做什么(开仓和平仓、拉动保护止损),然后才是指定在哪个事件中以及如何去做。但在任何情形下反过来都不成立:接收信号然后思考在何处以及如何处理。这是反向逻辑,最好不要使用,否则您将会获得冗长的代码和大量的条件分支。
下面是一个关于反向逻辑和直接逻辑的示例。通过 RSI 信号开仓/平仓。
- 在反向逻辑中,“EA 交易”首先是获得指标值,然后检查信号方向以及您需要对持仓执行的操作:买入开仓和卖出平仓,或反之亦然 – 卖出开仓和买入平仓。也就是说,切入点为获取和分析信号。
- 在直接逻辑中,一切都是相反的。“EA 交易”具有开仓和平仓两个模块,它只需检查执行这些模块的条件。换言之,在进入开仓模块后,“EA 交易”接收指标值并检查其是否是开仓的信号。然后,在进入平仓模块后,“EA 交易”检查其是否是平仓的信号。也就是说,不存在切入点 – 系统状态分析具有独立的工作模块(设计的第一个原则)。
现在,如果您想要为“EA 交易”添加功能,使用第二个变体要比第一个变体容易很多。它足以用来创建事件处理的新模块。
并且在第一个变体中,您将需要修改信号处理的结构或将其作为单独的功能粘贴。
要更好地理解这一方法,下图给出了以四种不同的“EA 交易”为上下文的不同工作框架。
图 2.“EA 交易”实施示例
a).“EA 交易”,仅基于某些指标的信号。它能够在信号改变时开仓或平仓。示例 – MA“EA 交易”。
b).带图形交易控制的“EA 交易”。
c).基于指标的“EA 交易”,但添加了追踪止损和操作时间。示例 – 在开立持仓通过 MA 指标进入趋势时利用消息套利。
d).“EA 交易”无指标,带平均持仓。它仅在建立新柱时对持仓参数进行一次验证。示例 – 平均“EA 交易”。
从框架中我们可以看出,使用直接逻辑说明任何交易系统都是极其简单的。
3. “EA 交易”类的实施
使用上文提到的所有原则和要求创建一个类,这将成为未来所有“EA 交易”的基础。
CExpertAdvisor 类应具有的基本功能如下:
1. 初始化:
- 注册指标
- 设置参数的初始值
- 调整至所需交易品种和时间表
2. 获取信号的函数
- 允许的工作时间(交易间隔)
- 确定开仓/平仓或未结订单/已结订单的信号
- 确定过滤器(趋势、时间等)
- 开始/停止计时器
3. 服务函数
- 计算开盘价、止损和获利水平、订单量
- 发送交易请求(开仓、平仓、修改持仓)
4. 交易模块
- 处理信号和过滤器
- 控制持仓和订单
- 引进“EA 交易”函数:OnTrade()、OnTimer()、OnTester()、OnChartEvent()。
5. 取消初始化
- 输出消息、报告
- 清除图表、卸载指标
该类的所有函数可分为三组。嵌套函数的基本框架以及函数的说明如下所示。
图 3.“EA 交易”嵌套函数的框架
1. 宏函数
该小组函数是使用订单类型、交易品种参数和价格值设置订单(未结订单和止损订单)的基础。这些宏函数体现了第二个设计原则 – 抽象化。它们在“EA 交易”使用的交易品种上下文中工作。
转换类型的宏函数根据市场方向 – 买入或卖出 – 工作。因此,为了不创建您自己的常量,最好使用已有常量 – ORDER_TYPE_BUY 和 ORDER_TYPE_SELL。下面给出了宏的一些使用示例以及工作结果。
//--- 类型转换模块 long BaseType(long dir); // 返回指定方向订单的基本类型 long ReversType(long dir); // 返回指定方向订单的反向类型 long StopType(long dir); // 返回指定方向的停止订单类型 long LimitType(long dir); // 返回指定方向的限价订单类型 //--- Normalization macro double BasePrice(long dir); // 返回指定方向的卖/买价 double ReversPrice(long dir); // 返回反方向的卖/买价 long dir,newdir; dir=ORDER_TYPE_BUY; newdir=ReversType(dir); // newdir=ORDER_TYPE_SELL newdir=StopType(dir); // newdir=ORDER_TYPE_BUY_STOP newdir=LimitType(dir); // newdir=ORDER_TYPE_BUY_LIMIT newdir=BaseType(newdir); // newdir=ORDER_TYPE_BUY double price; price=BasePrice(dir); // price=Ask price=ReversPrice(dir); // price=Bid
在开发“EA 交易”时,宏允许您不指定处理方向,并帮助您开发更紧凑的代码。
2. 服务函数
这些函数设计用于处理订单和持仓。与宏函数一样,它们也属于低级函数。为方便起见,可将服务函数分为两个类别:信息函数和执行函数。它们都只执行一种操作,没有任何事件分析。它们执行来自高级“EA 交易”句柄的订单。
信息函数示例: 找出当前挂单的最大开盘价;找出持仓的平仓方式 – 获利或是亏损;获取“EA 交易”订单的订单号和订单号列表,等等。
执行函数示例: 结算指定订单;在指定持仓中修改止损,等等。
这是最大的一组。这是“EA 交易”的整个运行时工作所基于的功能性。可在论坛上找到这些函数的大量示例,网址 https://www.mql5.com/ru/forum/107476。除此之外,MQL5 的标准库中已经包含了一些承担部分下达订单和持仓工作的类,特别是 – CTrade 类。
但您的任何任务都将要求您创建新的实施或是对已有实施稍加修改。
3. 事件处理模块
该组函数是之前两组函数的高级上部结构。正如前文所述 – 这些函数随时可用,是您的“EA 交易”所构建的程序块。一般而言,它们包含于 MQL 程序的事件处理函数中:OnStart()、OnTick()、OnTimer()、OnTrade()、OnChartEvent()。该组模块数目不多,且这些模块的内容可根据任务作出调整。但是基本上没有什么变化。
模块中的一切都应该是抽象的(第二个设计原则),以便同一模块可以为买入和卖出调用。这是通过宏的帮助实现的。
所以,继续实施
1. 初始化、取消初始化
class CExpertAdvisor { protected: bool m_bInit; // 正确初始化的标识 ulong m_magic; // EA专家系统的编号 string m_smb; // EA运行于的交易品种 ENUM_TIMEFRAMES m_tf; // 时间框架 CSymbolInfo m_smbinf; // 交易品种参数 int m_timer; // 计时器时间 public: double m_pnt; // 考虑停止位的5/3个小数位报价 CTrade m_trade; // 执行交易订单的对象 string m_inf; // EA运行信息的备注字符串
这是“EA 交易”函数工作所需的最小参数集。
m_smb 和 m_tf 参数专门放置在“EA 交易”属性中,以便很容易地告知“EA 交易”处理的货币和时间周期。例如,如果您分配 m_smb = “USDJPY”,“EA 交易”将处理该交易品种而不管之前运行的是何交易品种。如果您设置 tf = PERIOD_H1,则指标的所有信号和分析都将发生在 H1 图表上。
还有类方法。前三个方法是“EA 交易”的初始化和取消初始化。
public: //--- 初始化 void CExpertAdvisor(); // 构造函数 void ~CExpertAdvisor(); // 析构函数 virtual bool Init(long magic,string smb,ENUM_TIMEFRAMES tf); // 初始化
基类中的构造函数和析构函数不进行任何操作。
Init() 方法通过交易品种、时间表和幻数初始化“EA 交易”参数。
//------------------------------------------------------------------ CExpertAdvisor void CExpertAdvisor::CExpertAdvisor() { m_bInit=false; } //------------------------------------------------------------------ ~CExpertAdvisor void CExpertAdvisor::~CExpertAdvisor() { } //------------------------------------------------------------------ Init bool CExpertAdvisor::Init(long magic,string smb,ENUM_TIMEFRAMES tf) { m_magic=magic; m_smb=smb; m_tf=tf; // 设置初始化参数 m_smbinf.Name(m_smb); // 初始化交易品种 m_pnt=m_smbinf.Point(); // 计算5/3个小数位报价的乘数 if(m_smbinf.Digits()==5 || m_smbinf.Digits()==3) m_pnt*=10; m_trade.SetExpertMagicNumber(m_magic); // 设置EA专家系统的编号 m_bInit=true; return(true); // 允许交易 }
2. 获取信号的函数
这些函数分析市场和指标。
bool CheckNewBar(); // 检查新柱 bool CheckTime(datetime start,datetime end); // 检查允许交易的时间 virtual long CheckSignal(bool bEntry); // 检查信号 virtual bool CheckFilter(long dir); // 检查方向过滤器
前两个函数具有相当具体的实施,并可进一步用于该类的子类中。
//------------------------------------------------------------------ CheckNewBar bool CExpertAdvisor::CheckNewBar() // 检查新柱的函数 { MqlRates rt[2]; if(CopyRates(m_smb,m_tf,0,2,rt)!=2) // 复制柱的信息 { Print("CopyRates of ",m_smb," failed, no history"); return(false); } if(rt[1].tick_volume>1) return(false); // 检查交易量 return(true); } //--------------------------------------------------------------- CheckTime bool CExpertAdvisor::CheckTime(datetime start,datetime end) { datetime dt=TimeCurrent(); // 当前时间 if(start<end) if(dt>=start && dt<end) return(true); // 检查是否在允许交易的时间范围内 if(start>=end) if(dt>=start|| dt<end) return(true); return(false); }
后两个函数始终依赖于您正在使用的指标。要设置这些函数使其适用于所有情形几乎是不可能的。
把握重点 – 重要的是了解 CheckSignal() 和 CheckFilter() 信号函数可分析任何指标及其组合!换言之,这些信号随后将包含在其内的交易模块独立于来源。
这允许您将之前编写的“EA 交易”作为基于相似原则工作的其他“EA 交易”的模板。只需更改分析的指标或添加新的过滤条件。
3. 服务函数
正如前文所述,该组函数的数目是最多的。对于本文中描述的实践任务,实施此四个函数即已足够:
double CountLotByRisk(int dist,double risk,double lot); // 根据风险大小计算交易量 ulong DealOpen(long dir,double lot,int SL,int TP); // 用指定参数执行交易 ulong GetDealByOrder(ulong order); // 根据订单号获取成交编号 double CountProfitByDeal(ulong ticket); // 根据成交编号计算利润 //------------------------------------------------------------------ CountLotByRisk double CExpertAdvisor::CountLotByRisk(int dist,double risk,double lot) // 根据风险大小计算交易量 { if(dist==0 || risk==0) return(lot); m_smbinf.Refresh(); return(NormalLot(AccountInfoDouble(ACCOUNT_BALANCE)*risk/(dist*10*m_smbinf.TickValue()))); } //------------------------------------------------------------------ DealOpen ulong CExpertAdvisor::DealOpen(long dir,double lot,int SL,int TP) { double op,sl,tp,apr,StopLvl; // 确定价格参数 m_smbinf.RefreshRates(); m_smbinf.Refresh(); StopLvl = m_smbinf.StopsLevel()*m_smbinf.Point(); // 记录停止水平 apr = ReversPrice(dir); op = BasePrice(dir); // 开盘价 sl = NormalSL(dir, op, apr, SL, StopLvl); // 止损 tp = NormalTP(dir, op, apr, TP, StopLvl); // 获利 // 开仓 m_trade.PositionOpen(m_smb,(ENUM_ORDER_TYPE)dir,lot,op,sl,tp); ulong order = m_trade.ResultOrder(); if(order<=0) return(0); // 订单编号 return(GetDealByOrder(order)); // 返回成交编号 } //------------------------------------------------------------------ GetDealByOrder ulong CExpertAdvisor::GetDealByOrder(ulong order) // 根据订单号获取成交编号 { PositionSelect(m_smb); HistorySelectByPosition(PositionGetInteger(POSITION_IDENTIFIER)); uint total=HistoryDealsTotal(); for(uint i=0; i<total; i++) { ulong deal=HistoryDealGetTicket(i); if(order==HistoryDealGetInteger(deal,DEAL_ORDER)) return(deal); // 记录成交编号 } return(0); } //------------------------------------------------------------------ CountProfit double CExpertAdvisor::CountProfitByDeal(ulong ticket) // 根据成交编号计算利润 { CDealInfo deal; deal.Ticket(ticket); // 成交编号 HistorySelect(deal.Time(),TimeCurrent()); // 在此之后选中所有成交 uint total = HistoryDealsTotal(); long pos_id = deal.PositionId(); // 获取持仓ID double prof = 0; for(uint i=0; i<total; i++) // 根据这个ID找到其下的所有成交 { ticket = HistoryDealGetTicket(i); if(HistoryDealGetInteger(ticket,DEAL_POSITION_ID)!=pos_id) continue; prof += HistoryDealGetDouble(ticket,DEAL_PROFIT); // 对利润求和 } return(prof); // 返回利润 }
4. 交易模块
最后,该组函数使用服务函数和宏将交易的全过程、信号和事件的处理结合在一起。交易操作的逻辑语义很少,它们取决于您的具体目标。然而,我们可以区分几乎存在于所有“EA 交易”中的通用概念。
virtual bool Main(); // 控制交易过程的主模块 virtual void OpenPosition(long dir); // 开仓模块 virtual void CheckPosition(long dir); // 检查持仓和额外开仓 virtual void ClosePosition(long dir); // 平仓 virtual void BEPosition(long dir,int BE); // 将止损移动到位盈亏平衡位 virtual void TrailingPosition(long dir,int TS); // 追踪止损 virtual void OpenPending(long dir); // 挂单模块 virtual void CheckPending(long dir); // 处理当前订单和额外下单 virtual void TrailingPending(long dir); // 移动挂单 virtual void DeletePending(long dir); // 删除挂单
我们将通过以下示例来讨论这些函数的具体实施。
添加新的函数并不困难,因为我们已选择正确的方法并构建了“EA 交易”结构。如果您将使用该框架,您的设计将花费您最少的时间和精力,代码哪怕是在一年后也具有极佳的可读性。
当然,您的“EA 交易”并不局限于该框架。在 CExpertAdvisor 类中,我们仅声明了最为必要的方法。您可以在子类中添加新的句柄、修改现有句柄、扩展您自己的模块,从而创建单一的库。有了这个库,开发“EA 交易”成套项目仅需半小时到两天不等。
4. CExpertAdvisor 类使用示例
4.1. 基于指标信号的工作示例
作为第一个示例,让我们从最简单任务开始 – 使用 CExpertAdvisor 类考虑移动平均线“EA 交易”(MetaTrader 5 基本示例)。让我们稍稍对其添枝加叶。
算法:
a) 开仓条件
- 如果价格自底向上穿过 MA ,则买入开仓。
- 如果价格自顶向下穿过 MA ,则卖出开仓。
- 设置 SL(止损)、TP(获利)。
- 持仓手数由 Risk 参数计算 – 止损触发时的入金损失。
b) 平仓条件
- 如果价格自底向上穿过 MA ,则卖出平仓。
- 如果价格自顶向下穿过 MA ,则买入平仓。
c) 限制
- 限制“EA 交易”的工作时间为每日的 HourStart 到 HourEnd。
- “EA 交易”仅针对新柱进行交易操作。
d) 持仓支撑
- 使用距离为 TS 的简单追踪止损。
对于我们的“EA 交易”,我们将需要七个 CExpertAdvisor 类函数:
- 信号函数 – CheckSignal()
- 订单号过滤器 – CheckNewBar()
- 时间过滤器 – CheckTime()
- 开仓的服务功能 – DealOpen()
- 三个工作模块 – OpenPosition()、ClosePosition()、TrailingPosition()
CheckSignal() 函数和模块必须在子类中定义,以专门解决其任务。我们还需要添加指标的初始化。
//+------------------------------------------------------------------+ //| Moving Averages.mq5 | //| Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #include "ExpertAdvisor.mqh" input double Risk = 0.1; // 风险 input int SL = 100; // 止损距离 input int TP = 100; // 获利距离 input int TS = 30; // 追踪止损距离 input int pMA = 12; // 移动平均周期 input int HourStart = 7; // 交易的开始时间,小时 input int HourEnd = 20; // 交易的结束时间,小时 //--- class CMyEA : public CExpertAdvisor { protected: double m_risk; // 风险大小 int m_sl; // 止损 int m_tp; // 获利 int m_ts; // 追踪止损 int m_pMA; // MA周期 int m_hourStart; // 交易的开始时间,小时 int m_hourEnd; // 交易的结束时间,小时 int m_hma; // MA指标 public: void CMyEA(); void ~CMyEA(); virtual bool Init(string smb,ENUM_TIMEFRAMES tf); // 初始化 virtual bool Main(); // 主函数 virtual void OpenPosition(long dir); // 开仓 virtual void ClosePosition(long dir); // 平仓 virtual long CheckSignal(bool bEntry); // 检查信号 }; //------------------------------------------------------------------ CMyEA void CMyEA::CMyEA() { } //----------------------------------------------------------------- ~CMyEA void CMyEA::~CMyEA() { IndicatorRelease(m_hma); // 删除MA指标 } //------------------------------------------------------------------ Init bool CMyEA::Init(string smb,ENUM_TIMEFRAMES tf) { if(!CExpertAdvisor::Init(0,smb,tf)) return(false); // 初始化父类 m_risk=Risk; m_tp=TP; m_sl=SL; m_ts=TS; m_pMA=pMA; // 复制参数 m_hourStart=HourStart; m_hourEnd=HourEnd; m_hma=iMA(m_smb,m_tf,m_pMA,0,MODE_SMA,PRICE_CLOSE); // 创建MA指标 if(m_hma==INVALID_HANDLE) return(false); // 如果报错,则退出 m_bInit=true; return(true); // 允许交易 } //------------------------------------------------------------------ Main bool CMyEA::Main() // 主函数 { if(!CExpertAdvisor::Main()) return(false); // 调用父类函数 if(Bars(m_smb,m_tf)<=m_pMA) return(false); // 如果柱形图数量不够 if(!CheckNewBar()) return(true); // 检查新的柱形图 // 检查每个开仓方向 long dir; dir=ORDER_TYPE_BUY; OpenPosition(dir); ClosePosition(dir); TrailingPosition(dir,m_ts); dir=ORDER_TYPE_SELL; OpenPosition(dir); ClosePosition(dir); TrailingPosition(dir,m_ts); return(true); } //------------------------------------------------------------------ OpenPos void CMyEA::OpenPosition(long dir) { if(PositionSelect(m_smb)) return; // 如果已经有一个订单了,退出 if(!CheckTime(StringToTime(IntegerToString(m_hourStart)+":00"), StringToTime(IntegerToString(m_hourEnd)+":00"))) return; if(dir!=CheckSignal(true)) return; // 如果没有当前方向的信号 double lot=CountLotByRisk(m_sl,m_risk,0); if(lot<=0) return; // 如果交易量没有定义则退出 DealOpen(dir,lot,m_sl,m_tp); // 开仓 } //------------------------------------------------------------------ ClosePos void CMyEA::ClosePosition(long dir) { if(!PositionSelect(m_smb)) return; // 如果没有持仓头寸,退出 if(!CheckTime(StringToTime(IntegerToString(m_hourStart)+":00"), StringToTime(IntegerToString(m_hourEnd)+":00"))) { m_trade.PositionClose(m_smb); return; } // 如果现在不是交易的时间,那么平仓 if(dir!=PositionGetInteger(POSITION_TYPE)) return; // 如果是未确认的开仓方向 if(dir!=CheckSignal(false)) return; // 如果最近的信号和当前持仓方向不一致 m_trade.PositionClose(m_smb,1); // 平仓 } //------------------------------------------------------------------ CheckSignal long CMyEA::CheckSignal(bool bEntry) { MqlRates rt[2]; if(CopyRates(m_smb,m_tf,0,2,rt)!=2) { Print("CopyRates ",m_smb," history is not loaded"); return(WRONG_VALUE); } double ma[1]; if(CopyBuffer(m_hma,0,0,1,ma)!=1) { Print("CopyBuffer MA - no data"); return(WRONG_VALUE); } if(rt[0].open<ma[0] && rt[0].close>ma[0]) return(bEntry ?ORDER_TYPE_BUY:ORDER_TYPE_SELL); // 买入的条件 if(rt[0].open>ma[0] && rt[0].close<ma[0]) return(bEntry ?ORDER_TYPE_SELL:ORDER_TYPE_BUY); // 卖出条件 return(WRONG_VALUE); // 如果没有信号 } CMyEA ea; // 类的实例 //------------------------------------------------------------------ OnInit int OnInit() { ea.Init(Symbol(),Period()); // 初始化EA return(0); } //------------------------------------------------------------------ OnDeinit void OnDeinit(const int reason) { } //------------------------------------------------------------------ OnTick void OnTick() { ea.Main(); // 处理到来的报价 }
让我们来解析 Main() 函数的结构。按照惯例,它分为两个部分。
在第一部分中父函数被调用。该函数处理可在全局层面影响“EA 交易”工作的可能参数。这些包括对“EA 交易”交易许可的检查和历史数据的验证。
在第二部分中,市场事件获得直接处理。
测试 CheckNewBar() 过滤器 – 检查新柱。且用于交易的两个方向的模块相继调用。
模块中的一切都相当抽象(第二个设计原则)。没有到交易品种属性的直接地址。且三个模块 – OpenPosition()、ClosePosition() 和 TrailingPosition() – 仅依赖于那些来自外部的参数。这允许您调用这些模块以验证买入订单和卖出订单。
4.2. CExpertAdvisor 使用示例 – 无指标的“EA 交易”,用于分析持仓状态和结果
我们以损失后仅交易与手数增加反向的持仓的系统(该类型“EA 交易”通常称为“鞅”(Martingale))来进行说明。
a) 下达初始订单
- “EA 交易”启动后建立首个带初始手数的买入持仓
b) 建立后续持仓
- 如果上一持仓获利平仓,则建立带初始手数的相同方向的持仓
- 如果上一持仓亏损平仓,则建立带较大手数的相反方向的持仓(使用因子)。
对于我们的“EA 交易”,我们将需要三个 CExpertAdvisor 类函数:
- 开仓 – DealOpen()
- 通过交易订单号获取已平仓持仓的利润值 – CountProfitByDeal()
- 工作模块 – OpenPosition()、CheckPosition()
由于该“EA 交易”不分析任何指标而只处理结果,我们将使用 OnTrade() 事件获得最优效率。也就是说,该“EA 交易”在下达首个初始买入订单后,只有在平仓后才会下达所有后续订单。因此,我们将在 OnTick() 中下达初始订单,并将在 OnTrade() 中进行所有后续工作。
Init() 函数照例只是使用“EA 交易”的外部参数初始化类的参数。
OpenPosition() 模块建立初始持仓并被 m_first 标志阻止。
CheckPosition() 模块控制进一步的反向开仓。
这些模块从“EA 交易”各自的函数调用:OnTick() 和 OnTrade()。
//+------------------------------------------------------------------+ //| eMarti.mq5 | //| Copyright Copyright 2010, MetaQuotes Software Corp. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #include "ExpertAdvisor.mqh" #include <Trade/DealInfo.mqh> input double Lots = 0.1; // 交易量 input double LotKoef = 2; // 亏损后的交易量乘数 input int Dist = 60; // 止损和获利之间的距离 //--- class CMartiEA : public CExpertAdvisor { protected: double m_lots; // 交易量 double m_lotkoef; // 亏损后的交易量乘数 int m_dist; // 止损和获利之间的距离 CDealInfo m_deal; // 最后一笔交易 bool m_first; // 第一笔开仓的标识 public: void CMartiEA() { } void ~CMartiEA() { } virtual bool Init(string smb,ENUM_TIMEFRAMES tf); // 初始化 virtual void OpenPosition(); virtual void CheckPosition(); }; //------------------------------------------------------------------ Init bool CMartiEA::Init(string smb,ENUM_TIMEFRAMES tf) { if(!CExpertAdvisor::Init(0,smb,tf)) return(false); // 初始化父类 m_lots=Lots; m_lotkoef=LotKoef; m_dist=Dist; // 复制参数 m_deal.Ticket(0); m_first=true; m_bInit=true; return(true); // 允许交易 } //------------------------------------------------------------------ OnTrade void CMartiEA::OpenPosition() { if(!CExpertAdvisor::Main()) return; // 调用父函数 if(!m_first) return; // 如果已经持有初始持仓 ulong deal=DealOpen(ORDER_TYPE_BUY,m_lots,m_dist,m_dist); // 建立初始持仓 if(deal>0) { m_deal.Ticket(deal); m_first=false; } // 如果持仓存在 } //------------------------------------------------------------------ OnTrade void CMartiEA::CheckPosition() { if(!CExpertAdvisor::Main()) return; // 调用父函数 if(m_first) return; // 如果还没建立初始持仓 if(PositionSelect(m_smb)) return; // 如果持仓存在 // 检查先前持仓的盈利情况 double lot=m_lots; // 初始交易量 long dir=m_deal.Type(); // 先前的持仓方向 if(CountProfitByDeal(m_deal.Ticket())<0) // 如果亏损 { lot=NormalLot(m_lotkoef*m_deal.Volume()); // 增加交易量 dir=ReversType(m_deal.Type()); // 反转持仓 } ulong deal=DealOpen(dir,lot,m_dist,m_dist); // 开仓 if(deal>0) m_deal.Ticket(deal); // 记住交易单号 } CMartiEA ea; // 类的实例 //------------------------------------------------------------------ OnInit int OnInit() { ea.Init(Symbol(),Period()); // 初始化EA return(0); } //------------------------------------------------------------------ OnDeinit void OnDeinit(const int reason) { } //------------------------------------------------------------------ OnTick void OnTick() { ea.OpenPosition(); // 处理每个价格变动 - 下第一笔订单 } //------------------------------------------------------------------ OnTrade void OnTrade() { ea.CheckPosition(); // 处理交易事件 }
5. 处理事件
在本文中您会看到处理 NewTick 和 Trade 两个事件的示例,这两个事件分别由 OnTick() 和 OnTrade() 函数所代表。在大多数情况下,这两个事件经常会用到。
对于“EA 交易”,有四个处理事件的函数:
- OnChartEvent 处理一大组事件:使用图形对象时,键盘、鼠标和自定义事件。例如,该函数用于创建交互式“EA 交易”或“EA 交易”,根据订单的图形管理原则建立。或只是用于创建 MQL 程序参数的主动控制(使用按钮和编辑字段)。总而言之,该函数用于处理“EA 交易”的外部事件。
-
OnTimer 在处理系统计时器事件时调用。该函数可用于多种情形:当 MQL 程序需要定期分析其环境时;用于计算指标值;需要该函数以持续参考外部信号源时,等等。大体而言,OnTimer() 函数是
while(true) { /* perform analysis */; Sleep(1000); } 的替代方法,甚至可以说是最佳替代。
换言之,“EA 交易”无需在一开始就陷入无限循环,只需将其函数的调用从 OnTick() 移至 OnTimer() 便已足够。 - OnBookEvent 处理在“市场深度”更改自身状态时生成的事件。该事件可归于外部并可根据任务执行其处理。
- OnTester 当“EA 交易”在给定的日期范围内测试后调用,在通过自定义最大参数使用遗传优化时,该函数针对测试生成的可能筛选在 OnDeinit() 函数之前调用。
不要忘记,将任何事件及其组合用于它们特定任务的解决方案始终是明智的。
总结
如您所见,如果拥有正确的框架,编写“EA 交易”并不需要花费多少时间。得益于 MQL5 中事件处理的新的可能性,我们有了更加灵活的结构用于管理交易过程。但所有这一切只有在您准备好合适的交易算法时才能成为真正强大的工具。
本文给出了创建算法的三个主要原则 – 重要性、抽象化、模块化。如果您的“EA 交易”基于这“三大支柱”,您的交易将更加容易。
本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/132
(6.32 KB)
(10.45 KB)
(31.89 KB)
MyFxtop迈投-靠谱的外汇跟单社区,免费跟随高手做交易!
免责声明:本文系转载自网络,如有侵犯,请联系我们立即删除,另:本文仅代表作者个人观点,与迈投财经无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
著作权归作者所有。
商业转载请联系作者获得授权,非商业转载请注明出处。