
开发多币种 EA 交易(第 2 部分):过渡到交易策略的虚拟仓位
概述
在上一篇文章中,我们开始开发一种可同时使用多种交易策略的多币种 EA。在第一阶段,只有两种不同的策略。它们代表了相同交易理念的实现,在相同的交易工具(交易品种)和图表时段(时间框架)上运行。它们之间的区别仅在于参数的数值不同。
我们还根据所需的最大回撤水平(存款的 10%)确定了未平仓仓位的最佳大小。我们对每种策略都分别进行了这样的设置。当我们将这两种策略结合在一起时,我们不得不减少开仓头寸的大小,以维持给定的回撤水平。对于两个策略,减少的幅度还算小。但是,如果我们想将数十或数百个策略实例组合在一起呢?对于某些策略,我们很可能不得不将仓位减小到经纪商允许的最小敞口仓位大小以下。在这种情况下,这些策略根本无法参与交易。如何让它们发挥作用?
为此,我们将剥夺策略独立开仓和设置挂单的权利。策略只需进行虚拟交易,即记住在什么水平应该开立一定大小的头寸,并根据要求报告现在应该开立的交易量。只有在对所有策略进行调查并计算出所需的总交易量后,我们才会开立真实的市场头寸,同时考虑到要保持一定的回撤比例。
我们现在只想测试这种方法的适用性,而不是其实施效率。因此,在本文的框架内,我们将尝试至少开发出这种方法的一些可行实现方案,这将有助于我们日后从架构的角度构建出更美观的方案,因为我们已经掌握了如何避免错误的知识。
让我们试着实现这一点。
回顾过去的成果
我们开发了CAdvisor EA 类,用于存储交易策略实例数组(更准确地说,是实例指针)。这样就可以在主程序中创建一个 EA 交易实例,并为其添加多个策略类实例。由于该数组存储指向 CStrategy 基类对象的指针,因此它可以存储指向继承自CStrategy 的任何子类对象的指针。在我们的案例中,我们创建了一个子类CSimpleVolumesStrategy,有它的两个对象被添加到 EA 中的数组中。
让我们商定一个方便的名称,以便于表述:
- EA 是我们的最终 mq5 文件,编译后提供一个可执行的 ex5 文件,适合在测试器和终端中运行。
- EA 是程序中声明的 CAdvisor 类对象。我们将在一个程序中只使用一个 EA 实例。
- 策略是从策略基类 CStrategy 继承而来的子类对象。
我们还可以回顾一下,指向对象(策略或任何其他类)的指针是关于先前创建的对象(简化过的)在内存中的位置的信息。这样,当我们将对象传递给函数、分配新变量或数组元素时,就可以避免在另一个内存位置重新创建同一个对象。
因此,在 EA 交易中,我们将策略对象的指针存储在一个数组中,这样在填充数组时,就不会创建策略对象的副本。然后,在访问策略数组的元素时,我们将访问原始策略对象。
EA 交易的工作包括以下几个阶段:
- 在静态内存区域中创建 EA。
- 程序初始化时,在动态内存中创建了两个策略,并在 EA 中存储了指向这两个策略的指针。
- 程序运行时,EA 通过调用 CStrategy::Tick() 方法,连续调用每个策略执行必要的交易操作。
- 在去初始化程序时,EA 从动态内存中删除了策略对象。
在开始之前,我们先对 EA 和 EA 类做一些小的修正。在 EA 中,我们将在动态内存区域创建 EA 对象。
CAdvisor expert; // EA object CAdvisor *expert; // Pointer to the EA object int OnInit() { expert = new CAdvisor(); // Create EA object // The rest of the code from OnInit() ... }
在 EA 类中,我们将创建一个析构函数 - 当 EA 对象从内存中删除时自动调用的函数。析构函数负责从动态内存中删除 CAdvisor::Deinit() 方法的策略对象。我们现在不需要这个方法,删除它吧。我们还将删除存储策略数量的 m_strategiesCount 类变量。我们可以根据需要使用 ArraySize()。
class CAdvisor { protected: CStrategy *m_strategies[]; // Array of trading strategies public: ~CAdvisor(); // Destructor // ... }; //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ void CAdvisor::~CAdvisor() { // Delete all strategy objects for(int i = 0; i < ArraySize(m_strategies); i++) { delete m_strategies[i]; } }
在 OnDeinit() 函数中,用删除 EA 对象替换CAdvisor::Deinit()方法。
void OnDeinit(const int reason) { expert.Deinit(); delete expert; }
路线图
如果交易策略不再能够自行开启市场仓位,那么
- 我们需要能存储策略虚拟仓位信息的对象;
- 我们需要能将虚拟仓位信息转化为真实市场仓位的对象。
虚拟仓位的对象应该是策略的一个组成部分,而且应该有几个。因此,让我们调用第一个新类 CVirtualOrder,并将这些对象的数组添加到 CStrategy 策略类中。CStrategy 还会获得一个属性,用于存储未结虚拟仓位变化的指示,以及获取和设置其值的方法。该属性实际上决定了策略当前处于两种状态中的哪一种:
- 无变化 - 整个开启的虚拟交易量已发送到市场;
- 有变化 - 虚拟交易量与市场交易量不一致,因此有必要调整实际市场仓位的交易量。
由于现在将由其他人负责建立真实仓位,因此可以从策略基类中删除 m_magic 幻数属性。今后,我们将进一步清理最基本的策略类,但目前我们将仅限于部分清理。
这样,基本策略类就会变成这样。
#include "VirtualOrder.mqh" //+------------------------------------------------------------------+ //| Base class of the trading strategy | //+------------------------------------------------------------------+ class CStrategy { protected: string m_symbol; // Symbol (trading instrument) ENUM_TIMEFRAMES m_timeframe; // Chart period (timeframe) double m_fixedLot; // Size of opened positions (fixed) CVirtualOrder m_orders[]; // Array of virtual positions (orders) int m_ordersTotal; // Total number of open positions and orders double m_volumeTotal; // Total volume of open positions and orders bool m_isChanged; // Sign of changes in open virtual positions void CountOrders(); // Calculate the number and volumes of open positions and orders public: // Constructor CStrategy(string p_symbol = "", ENUM_TIMEFRAMES p_timeframe = PERIOD_CURRENT, double p_fixedLot = 0.01); virtual void Tick(); // Main method - handling OnTick events virtual double Volume(); // Total volume of virtual positions virtual string Symbol(); // Strategy symbol (only one for a single strategy so far) virtual bool IsChanged(); // Are there any changes in open virtual positions? virtual void ResetChanges(); // Reset the sign of changes in open virtual positions };
我们已经可以实现 Symbol()、IsChanged() 和 ResetChanges() 方法。
//+------------------------------------------------------------------+ //| Strategy symbol | //+------------------------------------------------------------------+ string CStrategy::Symbol() { return m_symbol; } //+------------------------------------------------------------------+ //| Are there any changes to open virtual positions? | //+------------------------------------------------------------------+ bool CStrategy::IsChanged() { return m_isChanged; } //+------------------------------------------------------------------+ //| Reset the flag for changes in virtual positions | //+------------------------------------------------------------------+ void CStrategy::ResetChanges() { m_isChanged = false; }
其余的方法( Tick()、Volume() 和 CountOrders() )我们将在基类的子类中或在类本身中实现。
第二个新类名为 CReceiver,其对象将参与将策略的虚拟仓位发布到市场。该对象应能访问所有 EA 策略,以便找到应使用哪些交易品种和交易量来建立真实仓位。一个这样的对象对一个 EA 来说就足够了。CReceiver 对象应该有一个幻数,它将被设置为已开立的市场仓位。
#include "Strategy.mqh" //+------------------------------------------------------------------+ //| Base class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CReceiver { protected: CStrategy *m_strategies[]; // Array of strategies ulong m_magic; // Magic public: CReceiver(ulong p_magic = 0); // Constructor virtual void Add(CStrategy *strategy); // Adding strategy virtual bool Correct(); // Adjustment of open volumes }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CReceiver::CReceiver(ulong p_magic) : m_magic(p_magic) { ArrayResize(m_strategies, 0, 128); } //+------------------------------------------------------------------+ //| Add strategy | //+------------------------------------------------------------------+ void CReceiver::Add(CStrategy *strategy) { APPEND(m_strategies, strategy); } //+------------------------------------------------------------------+ //| Adjust open volumes | //+------------------------------------------------------------------+ bool CReceiver::Correct() { return true; }
该基类不包含指定交易量调节机制的实现。因此,我们可以在该类的不同子类中对调整进行不同的实现。该类对象可以作为这些策略的基础,这些策略本身暂时不会打开市场仓位。我们需要它来调试调整机制:需要比较哪些仓位是由 EA 开立的,其中策略本身进行实际交易,哪些仓位是由 EA 开立的,其中策略只进行虚拟交易。
因此,我们将准备两个 EA,在其中使用前一篇文章中的策略进行真实交易。
在第一个 EA 中,策略将是一个实例。在其参数中,我们可以指定该单一策略实例的参数,以便对其进行优化。
第二个 EA 将包含几个交易策略实例,其预定义参数是第一个 EA 优化的结果。
优化策略参数的 EA
上次,我们使用策略实现而不是 CStrategy 类对象的形式优化了策略参数。但现在我们已经有了一个现成的类 CSimpleVolumesStrategy,所以让我们创建一个单独的程序,其中的 EA 将包含该策略的一个实例。为了强调该策略本身将打开市场仓位,我们将以稍有不同的方式调用该类:我们将使用 CSimpleVolumesMarketStrategy 而不是 CSimpleVolumes,并将其保存在当前文件夹的 SimpleVolumesMarketStrategy.mqh 文件中。
在 EA 文件中,我们将从 EA 输入变量中加载策略参数,并在 EA 对象中添加一个策略实例。我们将得到一个可以优化策略参数的 EA。
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" #include "VolumeReceiver.mqh" //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ input string symbol_ = "EURGBP"; // Trading instrument (symbol) input ENUM_TIMEFRAMES timeframe_ = PERIOD_H1; // Chart period input group "=== Opening signal parameters" input int signalPeriod_ = 130; // Number of candles for volume averaging input double signalDeviation_ = 0.9; // Relative deviation from the average to open the first order input double signaAddlDeviation_ = 1.4; // Relative deviation from the average for opening the second and subsequent orders input group "=== Pending order parameters" input int openDistance_ = 0; // Distance from price to pending order input double stopLevel_ = 2000; // Stop Loss (in points) input double takeLevel_ = 475; // Take Profit (in points) input int ordersExpiration_ = 6000; // Pending order expiration time (in minutes) input group "=== Money management parameters" input int maxCountOfOrders_ = 3; // Maximum number of simultaneously open orders input double fixedLot_ = 0.01; // Single order volume input group "=== EA parameters" input ulong magic_ = 27181; // Magic CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(); expert.Add(new CSimpleVolumesMarketStrategy( magic_, symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { expert.Tick(); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { delete expert; }
让我们将其保存在 SimpleVolumesMarketExpertSingle.mq5 文件的当前文件夹中。
现在,让我们把交易策略复杂化一点,以简化任务的执行。对于我们来说,将使用市场仓位而非挂单的策略来转移到虚拟交易中会更加容易。而当前版本的策略只适用于挂单。让我们为 openDistance_ 策略添加参数值分析。如果高于零,则策略将打开 BUY_STOP 和 SELL_STOP 挂单。如果低于零,则策略将打开 BUY_LIMIT 和 SELL_LIMIT 挂单。如果等于零,则将打开市场仓位。
为此,只需修改 CSimpleVolumesMarketStrategy::OpenBuyOrder() 和 CSimpleVolumesMarketStrategy::OpenSellOrder() 方法的代码即可。
void CSimpleVolumesMarketStrategy::OpenBuyOrder() { // Previous code in the method ... // Order volume double lot = m_fixedLot; // Set a pending order bool res = false; if(openDistance_ > 0) { res = trade.BuyStop(lot, ...); } else if(openDistance_ < 0) { res = trade.BuyLimit(lot, ...); } else { res = trade.Buy(lot, ...); } if(!res) { Print("Error opening order"); } }
需要对策略进行的另一项必要修改是将初始化代码从 Init() 方法移至策略构造函数。这是必要的,因为现在 EA 将不会调用策略初始化方法,而是假设其代码位于策略构造函数内。
让我们编译一个新的 EA,并将其设置为使用三个交易品种对 H1 进行优化:EURGBP, GBPUSD 和 EURUSD.
图 1.使用 [EURGBP, H1, 17, 1.7, 0.5, 0, 16500, 100, 52000, 3, 0.01] 参数的测试结果
让我们从优化结果中选择几个好的参数选项(例如,每个交易品种有三个选项),然后创建第二个 EA,在其中创建九个带有所选参数的策略实例。对于每个实例,我们将计算未平仓仓位的最佳大小,在该大小下,一种策略的回撤率不超过 10%。计算方法已在上一篇文章中介绍过。
为了展示 EA 性能的变化,我们将可以设置要包含的策略。为此,我们首先要将所有策略实例放入由九个元素组成的数组中。让我们添加 startIndex_ 输入参数,它用于设置策略数组中的初始索引。参数 totalStrategies_ 决定了从 startIndex_ 开始从数组中启动多少个连续策略。初始化结束时,将数组中的相应策略添加到 EA 对象中。
#include "Advisor.mqh" #include "SimpleVolumesMarketStrategy.mqh" input int startIndex_ = 0; // Starting index input int totalStrategies_ = 1; // Number of strategies input double depoPart_ = 1.0; // Part of the deposit for one strategy input ulong magic_ = 27182; // Magic CAdvisor *expert; // EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { // Check if the parameters are correct if(startIndex_ < 0 || startIndex_ + totalStrategies_ > 9) { return INIT_PARAMETERS_INCORRECT; } // Create and fill the array of strategy instances CStrategy *strategies[9]; strategies[0] = new CSimpleVolumesMarketStrategy( magic_ + 0, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 13, 0.3, 1.0, 0, 10500, 465, 1000, 3); strategies[1] = new CSimpleVolumesMarketStrategy( magic_ + 1, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 17, 1.7, 0.5, 0, 16500, 220, 1000, 3); strategies[2] = new CSimpleVolumesMarketStrategy( magic_ + 2, "EURGBP", PERIOD_H1, NormalizeDouble(0.01 / 0.16 * depoPart_, 2), 51, 0.5, 1.1, 0, 19500, 370, 22000, 3); strategies[3] = new CSimpleVolumesMarketStrategy( magic_ + 3, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.25 * depoPart_, 2), 80, 1.1, 0.2, 0, 6000, 1190, 1000, 3); strategies[4] = new CSimpleVolumesMarketStrategy( magic_ + 4, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.09 * depoPart_, 2), 128, 2.0, 0.9, 0, 2000, 1170, 1000, 3); strategies[5] = new CSimpleVolumesMarketStrategy( magic_ + 5, "GBPUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.14 * depoPart_, 2), 13, 1.5, 0.8, 0, 2500, 1375, 1000, 3); strategies[6] = new CSimpleVolumesMarketStrategy( magic_ + 6, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.23 * depoPart_, 2), 24, 0.1, 0.3, 0, 7500, 2400, 24000, 3); strategies[7] = new CSimpleVolumesMarketStrategy( magic_ + 7, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.20 * depoPart_, 2), 18, 0.2, 0.4, 0, 19500, 1480, 6000, 3); strategies[8] = new CSimpleVolumesMarketStrategy( magic_ + 8, "EURUSD", PERIOD_H1, NormalizeDouble(0.01 / 0.22 * depoPart_, 2), 128, 0.7, 0.3, 0, 3000, 170, 42000, 3); expert = new CAdvisor(); // Add the necessary strategies to the EA for(int i = startIndex_; i < startIndex_ + totalStrategies_; i++) { expert.Add(strategies[i]); } return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
因此,我们可以对策略数组中策略的初始索引进行优化,以获得每个策略实例的结果。让我们以 100,000 美元的初始存款启动它,结果如下。
图 2.九个策略实例的单次运行结果
很明显,回撤约为初始存款的 1%,即大约 1000 美元,这与我们在选择最佳开仓规模时所计划的一样。平均夏普比率为 1.3。
现在,让我们打开所有实例,并选择适当的depoPart_ 乘数,以维持 1000 美元的回撤。如果 depoPart_ = 0.38,回撤仍在可接受范围内。
图 3.九个策略实例同时运行的测试结果
比较单个策略副本的工作结果和所有副本同时工作的结果,我们可以看到,在相同的回撤情况下,我们获得的利润增加了约 3 倍,夏普比率也从 1.3 增加到 2.84。
现在,让我们把重点放在主要任务上。
虚拟仓位(订单)类
因此,让我们创建承诺的 CVirtualOrder 类,并为其添加字段,以存储开启仓位的所有属性。
class CVirtualOrder { private: //--- Order (position) properties ulong m_id; // Unique ID string m_symbol; // Symbol double m_lot; // Volume ENUM_ORDER_TYPE m_type; // Type double m_openPrice; // Open price double m_stopLoss; // StopLoss level double m_takeProfit; // TakeProfit level string m_comment; // Comment datetime m_openTime; // Open time //--- Closed order (position) properties double m_closePrice; // Close price datetime m_closeTime; // Close time string m_closeReason; // Closure reason double m_point; // Point value bool m_isStopLoss; // StopLoss activation property bool m_isTakeProfit;// TakeProfit activation property };
每个虚拟仓位都应有一个唯一的 ID。因此,让我们添加 s_count 类静态变量来计算程序中创建的所有仓位对象的数量。当创建一个新的仓位对象时,该计数器将递增 1,该值将成为唯一的仓位编号。将s_count 初始值设为 0。
我们还需要 CSymbolInfo 类对象来获取定价信息,让它也成为类的静态成员。
class CVirtualOrder { private: static int s_count; static CSymbolInfo s_symbolInfo; //--- Order (position) properties ... }; int CVirtualOrder::s_count = 0; CSymbolInfo CVirtualOrder::s_symbolInfo;
值得注意的是,创建虚拟仓位对象和 "打开" 虚拟仓位是不同的操作。可以提前创建仓位对象,等待策略想要打开虚拟仓位的那一刻。此时,仓位属性将填入交易品种、成交量、开盘价等当前值。当策略决定平仓时,该对象将存储平仓属性的值:价格、时间和平仓原因。在下一次打开虚拟仓位的操作中,我们可以使用同一个对象实例,清除其平仓属性,并重新填充交易品种、成交量、开盘价等新值。
让我们为这个类添加方法。我们需要用于打开和关闭虚拟仓位的公有方法以及一个构造函数。检查仓位状态(是否未平仓,方向如何?)及其最重要属性(交易量和当前利润)的方法也是需要的。
class CVirtualOrder { //--- Previous code... public: CVirtualOrder(); // Constructor //--- Methods for checking the order (position) status bool IsOpen(); // Is the order open? bool IsMarketOrder(); // Is this a market position? bool IsBuyOrder(); // Is this an open BUY position? bool IsSellOrder(); // Is this an open SELL position? //--- Methods for obtaining order (position) properties double Volume(); // Volume with direction double Profit(); // Current profit //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = true); // Opening an order (position) bool Close(); // Closing an order (position) };
其中一些方法的实现非常简单和简短,因此可以直接放在类声明中:
class CVirtualOrder : public CObject { // ... //--- Methods for checking the order (position) status bool IsOpen() { // Is the order open? return(this.m_openTime > 0 && this.m_closeTime == 0); }; bool IsMarketOrder() { // Is this a market position? return IsOpen() && (m_type == ORDER_TYPE_BUY || m_type == ORDER_TYPE_SELL); } // ... };
构造函数将通过初始化列表为虚拟仓位的所有属性分配空值(即明显无效),只有唯一 ID 属性除外。如前所述,在构造函数中,ID 将根据之前创建的类对象的计数器值赋值。在整个 EA 运行过程中,该值将保持不变。在赋值之前,我们将递增已创建对象的计数器。
//+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CVirtualOrder::CVirtualOrder() : // Initialization list m_id(++s_count), // New ID = object counter + 1 m_symbol(""), m_lot(0), m_type(-1), m_openPrice(0), m_stopLoss(0), m_takeProfit(0), m_openTime(0), m_comment(""), m_closePrice(0), m_closeTime(0), m_closeReason(""), m_point(0) { }
在进一步实现 CVirtualOrder 类方法之前,让我们稍微向前看一下,想想我们将如何使用该类。我们的策略对象现在可以打开真实的市场仓位。此外,我们还知道未平仓市场仓位的最大数量(在策略参数中设置)。我们希望转到虚拟仓位,那么它们的数量也将受到限制。这意味着我们可以在策略中创建一个虚拟仓位对象数组,在初始化策略时填充所需的虚拟仓位数量,然后只使用该数组。
当出现建立新仓位的条件时,我们将从数组中提取一个尚未建立的虚拟仓位,并将其转化为已建立的仓位。如果出现强制平仓的条件,则将其转换为平仓。
只要存在开启的虚拟仓位,任何策略都必须在每个分时以相同的方式处理这些对象:依次查看所有对象,检查是否已达到止损或止盈水平,如果是,则平仓。这种相同性允许我们将处理开启虚拟仓位的实现转移到它们自己的类中,而只调用策略中的相应方法。
CVirtualOrder 类接收 Tick() 方法,该方法将检查平仓条件,如果满足条件,仓位将转入平仓状态。如果仓位状态发生变化,该方法将返回 true。
我们还要添加 Tick() 静态方法,它可以同时处理多个虚拟仓位对象。它将接受一个指向此类对象数组的链接作为参数。每个数组对象都将调用 Tick() 方法。如果至少有一个虚拟仓位被关闭,则最终返回 "true"。
class CVirtualOrder { private: //... public: //... //--- Methods for handling orders (positions) bool Open(string symbol, ENUM_ORDER_TYPE type, double lot, double sl = 0, double tp = 0, string comment = "", bool inPoints = false ); // Open order (position) bool Tick(); // Handle tick for an order (position) bool Close(); // Close an order (position) static bool Tick(CVirtualOrder &orders[]); // Handle a tick for the array of virtual orders }; //... //+------------------------------------------------------------------+ //| Handle a tick of a single virtual order (position) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick() { if(IsMarketOrder()) { // If this is a market virtual position if(CheckClose()) { // Check if SL or TP levels have been reached Close(); // Close when reached return true; // Return the fact that there are changes in open positions } } return false; } //+------------------------------------------------------------------+ //| Handle a tick for the array of virtual orders (positions) | //+------------------------------------------------------------------+ bool CVirtualOrder::Tick(CVirtualOrder &orders[]) { bool isChanged = false; // We assume that there will be no changes for(int i = 0; i < ArraySize(orders); i++) { // For all orders (positions) isChanged |= orders[i].Tick(); // Check and close if necessary } return isChanged; } //+------------------------------------------------------------------+
让我们把这段代码保存到当前文件夹的 VirtualOrder.mqh 文件中。
改进简单交易策略类
现在,我们可以回到交易策略类,对其进行修改,以便使用虚拟仓位。正如我们已经同意的那样,在基类 CStrategy 类中,我们已经有了用于存储虚拟仓位对象的 m_orders[] 数组。因此,它也可以在 CSimpleVolumesStrategy 类中使用。该策略的 m_maxCountOfOrders 参数决定了同时开仓的最大数量。然后在构造函数中设置虚拟仓位数组的大小等于该参数。
接下来,我们只需将 OpenBuyOrder() 和 OpenSellOrder() 方法中的真实仓位开仓替换为虚拟仓位开仓。目前,我们还无法用任何操作来替代打开真实挂单的操作,因此我们将直接注释掉这些操作。
//+------------------------------------------------------------------+ //| Open BUY order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenBuyOrder() { // ... if(m_openDistance > 0) { /* // Set BUY STOP pending order res = trade.BuyStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set BUY LIMIT pending order res = trade.BuyLimit(lot, ...); */ } else { // Open a virtual BUY position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_BUY, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... } //+------------------------------------------------------------------+ //| Open SELL order | //+------------------------------------------------------------------+ void CSimpleVolumesStrategy::OpenSellOrder() { // ... if(m_openDistance > 0) { /* // Set SELL STOP pending order res = trade.SellStop(lot, ...); */ } else if(m_openDistance < 0) { /* // Set SELL LIMIT pending order res = trade.SellLimit(lot, ...); */ } else { // Open a virtual SELL position for(int i = 0; i < m_maxCountOfOrders; i++) { // Iterate through all virtual positions if(!m_orders[i].IsOpen()) { // If we find one that is not open, then open it res = m_orders[i].Open(m_symbol, ORDER_TYPE_SELL, m_fixedLot, NormalizeDouble(sl, digits), NormalizeDouble(tp, digits)); break; // and exit } } } ... }
将更改保存到当前文件夹的 SimpleVolumesStrategy.mqh 文件中。
创建将未平仓交易量转换为市场仓位的类
我们已经创建了一个基类,用于将未平仓交易量转换为市场仓位的对象。现在,我们需要编写一个派生类,其中包含在市场上建仓的具体实现。让我们创建 CVolumeReceiver 类。我们需要添加大量代码来实现 Correct() 方法。我们将把它分解成几个受保护的类方法。
#include "Receiver.mqh" //+------------------------------------------------------------------+ //| Class for converting open volumes into market positions | //+------------------------------------------------------------------+ class CVolumeReceiver : public CReceiver { protected: bool m_isNetting; // Is this a netting account? string m_symbols[]; // Array of used symbols double m_minMargin; // Minimum margin for opening CPositionInfo m_position; CSymbolInfo m_symbolInfo; CTrade m_trade; // Filling the array of open market volumes by symbols void FillSymbolVolumes(double &oldVolumes[]); // Correction of open volumes using the array of volumes virtual bool Correct(double &symbolVolumes[]); // Volume correction for this symbol bool CorrectPosition(string symbol, double oldVolume, double diffVolume); // Auxiliary methods bool ClearOpen(string symbol, double diffVolume); bool AddBuy(string symbol, double volume); bool AddSell(string symbol, double volume); bool CloseBuyPartial(string symbol, double volume); bool CloseSellPartial(string symbol, double volume); bool CloseHedgingPartial(string symbol, double volume, ENUM_POSITION_TYPE type); bool CloseFull(string symbol = ""); bool FreeMarginCheck(string symbol, double volume, ENUM_ORDER_TYPE type); public: CVolumeReceiver(ulong p_magic, double p_minMargin = 100); // Constructor virtual void Add(CStrategy *strategy) override; // Add strategy virtual bool Correct() override; // Adjustment of open volumes };
已开启交易量的修正方法的一般算法如下:
- 对于使用的每个交易品种,查看所有策略并计算每个交易品种的总持仓量。生成的 newVolumes 数组将传递给下一个重载的 Correct() 方法
//+------------------------------------------------------------------+ //| Adjustment of open volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct() { int symbolsTotal = ArraySize(m_symbols); double newVolumes[]; ArrayResize(newVolumes, symbolsTotal); ArrayInitialize(newVolumes, 0); for(int j = 0; j < symbolsTotal; j++) { // For each used symbol for(int i = 0; i < ArraySize(m_strategies); i++) { // Iterate through all strategies if(m_strategies[i].Symbol() == m_symbols[j]) { // If the strategy uses this symbol newVolumes[j] += m_strategies[i].Volume(); // Add its open volume } } } // Call correction of open volumes using the array of volumes return Correct(newVolumes); }
- 对于每个交易品种,定义该交易品种未平仓交易量的变化量。如有必要,调用该交易品种的交易量修正方法
//+------------------------------------------------------------------+ //| Adjusting open volumes using the array of volumes | //+------------------------------------------------------------------+ bool CVolumeReceiver::Correct(double &newVolumes[]) { // ... bool res = true; // For each symbol for(int j = 0; j < ArraySize(m_symbols); j++) { // ... // Define how much the volume of open positions for the symbol should be changed double oldVolume = oldVolumes[j]; double newVolume = newVolumes[j]; // ... double diffVolume = newVolume - oldVolume; // If there is a need to adjust the volume for a given symbol, then do that if(MathAbs(diffVolume) > 0.001) { res = res && CorrectPosition(m_symbols[j], oldVolume, diffVolume); } } return res; }
- 对于一个交易品种,根据之前开仓量的值和所需的变化,确定我们需要执行哪种类型的交易操作(添加、关闭和重新开仓),并调用相应的辅助方法:
//+------------------------------------------------------------------+ //| Adjust volume by the symbol | //+------------------------------------------------------------------+ bool CVolumeReceiver::CorrectPosition(string symbol, double oldVolume, double diffVolume) { bool res = false; // ... double volume = MathAbs(diffVolume); if(oldVolume > 0) { // Have BUY position if(diffVolume > 0) { // New BUY position res = AddBuy(symbol, volume); } else if(diffVolume < 0) { // New SELL position if(volume < oldVolume) { res = CloseBuyPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > oldVolume) { res = AddSell(symbol, volume - oldVolume); } } } } else if(oldVolume < 0) { // Have SELL position if(diffVolume < 0) { // New SELL position res = AddSell(symbol, volume); } else if(diffVolume > 0) { // New BUY position if(volume < -oldVolume) { res = CloseSellPartial(symbol, volume); } else { res = CloseFull(symbol); if(res && volume > -oldVolume) { res = AddBuy(symbol, volume + oldVolume); } } } } else { // No old position res = ClearOpen(symbol, diffVolume); } return res; }
将代码保存到当前文件夹的 VolumeReceiver.mqh 文件中。
具有单一策略和虚拟仓位的 EA
根据 SimpleVolumesMarketExpertSingle.mq5 文件创建一个 EA,该 EA 将使用虚拟仓位交易策略的一个实例。我们需要连接必要的文件,在调用 EA 构造函数时,将新的 CVolumeReceiver 类对象传递给它,并替换已创建策略的类。
#include "Advisor.mqh" #include "SimpleVolumesStrategy.mqh" #include "VolumeReceiver.mqh" // Input parameters... CAdvisor *expert; // Pointer to the EA object //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { expert = new CAdvisor(new CVolumeReceiver(magic_)); expert.Add(new CSimpleVolumesStrategy( symbol_, timeframe_, fixedLot_, signalPeriod_, signalDeviation_, signaAddlDeviation_, openDistance_, stopLevel_, takeLevel_, ordersExpiration_, maxCountOfOrders_) ); // Add one strategy instance return(INIT_SUCCEEDED); } void OnTick() { expert.Tick(); } void OnDeinit(const int reason) { delete expert; }
将此代码保存在当前文件夹的 SimpleVolumesExpertSingle.mq5 文件中。
真实交易与虚拟交易的比较
让我们在很短的时间间隔内使用相同的策略启动 EA,使用相同的策略参数,但采用不同的开仓方式 - 直接开仓和通过虚拟仓位开仓。将这些结果保存到报告中,然后查看两个 EA 的交易列表。
图 4.两个 EA 进行的交易(有虚拟仓位和无虚拟仓位)
为减少宽度,表格中删除了所有行中数值相同的列,如交易品种(始终为 EURGBP)、成交量(始终为 0.01)等。我们可以看到,在这两种情况下,仓位都是以相同的价格、在相同的时间点建立的。如果我们有一个 SELL(2018.03.02 15:46:47 sell in)未平仓头寸,并打开了 BUY(2018.03.06 13:56:04 buy in)头寸,通过虚拟仓位工作的 EA 只需关闭之前的 SELL(2018.03.06 13:56:04 buy out)头寸。由于第一个 EA 继续为不同方向的未平仓仓位支付库存费,而第二个 EA 则不然,因此总体结果有所改善。
具有多种策略和虚拟仓位的 EA
让我们对 SimpleVolumesMarketExpert.mq5 文件中的 EA 执行类似操作。我们将包含必要的文件,在调用 EA 构造函数时,我们将为其提供新的 CVolumeReceiver 类对象,并替换已创建的策略类。将结果保存到 SimpleVolumesExpert.mq5 文件中,然后查看结果。
图 5.九个策略实例和虚拟仓位的 EA 工作结果
将这些结果与未使用虚拟仓位类的 EA 的结果进行比较,我们可以发现某些指标有所改善:利润略有增加,回撤减少,夏普比率和利润因子也有所提高。
结论
我们朝着实现目标又迈进了一步。通过过渡到使用虚拟仓位策略,我们提高了大量交易策略协同工作而互不干扰的能力。与使用每个策略实例进行单独交易相比,这还能让我们使用更低的最低交易保证金。另一个很好的收获是有机会从事净额(Netting)账户的工作。
但仍有许多需要进一步采取的措施。例如,到目前为止,只实现了建立市场仓位的策略,而没有实现挂单策略。有关资金管理的问题也留待将来解决。目前,我们使用固定交易量进行交易,并手动选择最佳仓位大小。同时处理多个交易品种的策略(无法划分为更简单的单交易品种的策略)也无法使用这种操作结构。
敬请期待最新消息。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/14107
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


你说得对。我是这么做的。这是一个全局字符串变量,所有输入变量都会自动(并创建) 进入该变量。也就是说,无论创建什么对象,这个变量都是输入变量。
为了以防万一,我提醒你,优化程序会将输入的字符串删减 63 个字符。
为了以防万一,我提醒大家,优化程序会将字符串输入减少 63 个字符。
谢谢。这不是输入,所以长度不受限制。
关于交易、自动交易系统和测试交易策略的论坛
讨论文章 "开发多币种智能交易系统(第 2 部分):转向虚拟头寸交易策略"
fxsaber, 2024.02.14 11:36 AM
你是对的。是这样做的。 这是一个全局字符串变量,所有输入变量都会自动(并创建) 进入该变量。也就是说,无论什么对象没有创建,这个变量都会被输入。附上。
您好、
我正在学习 OOP。上一篇文章的后续文章对我帮助很大。
我还在努力学习。谢谢。