Кроссплатформенный торговый советник: Классы CExpertAdvisor и CExpertAdvisors
Оглавление
- Введение
- Класс торгового советника
- Инициализация
- Определение нового бара
- Обработчик OnTick
- Контейнер торговых экспертов
- Сохранение данных
- Примеры
- Окончательные заметки
- Заключение
Введение
В примерах из предыдущих статей серии (1, 2, 3, 4, 5, 6, 7, 8, 9) компоненты торговых советников были хаотично разбросаны по основному заголовочному файлу советника. За это отвечали пользовательские функции. Завершим работу созданием классов CExpertAdvisor и CExpertsAdvisors. С их помощью организуется более гармоничное взаимодействие между отдельными компонентами эксперта. Также рассмотрим некоторые общие вопросы использования советника: загрузку и сохранение изменчивых данных и обнаружение новых баров.
Класс торгового советника
Класс CExpertAdvisorBase показан в коде ниже. До сих пор большинство различий между MQL4 и MQL5 сглаживались путем обработки другими объектами класса. Они были описаны в предыдущих статьях этой серии.
class CExpertAdvisorBase : public CObject { protected: //--- параметры торговли bool m_active; string m_name; int m_distance; double m_distance_factor_long; double m_distance_factor_short; bool m_on_tick_process; //--- параметры сигнала bool m_every_tick; bool m_one_trade_per_candle; datetime m_last_trade_time; string m_symbol_name; int m_period; bool m_position_reverse; //--- объекты сигналов CSignals *m_signals; //--- торговые объекты CAccountInfo m_account; CSymbolManager m_symbol_man; COrderManager m_order_man; //--- объекты времени торговли CTimes *m_times; //--- свеча CCandleManager m_candle_man; //--- события CEventAggregator *m_event_man; //--- контейнер CObject *m_container; public: CExpertAdvisorBase(void); ~CExpertAdvisorBase(void); virtual int Type(void) const {return CLASS_TYPE_EXPERT;} //--- инициализация bool AddEventAggregator(CEventAggregator*); bool AddMoneys(CMoneys*); bool AddSignal(CSignals*); bool AddStops(CStops*); bool AddSymbol(const string); bool AddTimes(CTimes*); virtual bool Init(const string,const int,const int,const bool,const bool,const bool); virtual bool InitAccount(void); virtual bool InitCandleManager(void); virtual bool InitEventAggregator(void); virtual bool InitComponents(void); virtual bool InitSignals(void); virtual bool InitTimes(void); virtual bool InitOrderManager(void); virtual bool Validate(void) const; //--- контейнер void SetContainer(CObject*); CObject *GetContainer(void); //--- активация и дезактивация bool Active(void) const; void Active(const bool); //--- методы установки и получения string Name(void) const; void Name(const string); int Distance(void) const; void Distance(const int); double DistanceFactorLong(void) const; void DistanceFactorLong(const double); double DistanceFactorShort(void) const; void DistanceFactorShort(const double); string SymbolName(void) const; void SymbolName(const string); //--- указатели на объекты CAccountInfo *AccountInfo(void); CStop *MainStop(void); CMoneys *Moneys(void); COrders *Orders(void); COrders *OrdersHistory(void); CStops *Stops(void); CSignals *Signals(void); CTimes *Times(void); //--- менеджер ордеров string Comment(void) const; void Comment(const string); bool EnableTrade(void) const; void EnableTrade(bool); bool EnableLong(void) const; void EnableLong(bool); bool EnableShort(void) const; void EnableShort(bool); int Expiration(void) const; void Expiration(const int); double LotSize(void) const; void LotSize(const double); int MaxOrdersHistory(void) const; void MaxOrdersHistory(const int); int Magic(void) const; void Magic(const int); uint MaxTrades(void) const; void MaxTrades(const int); int MaxOrders(void) const; void MaxOrders(const int); int OrdersTotal(void) const; int OrdersHistoryTotal(void) const; int TradesTotal(void) const; //--- менеджер сигналов int Period(void) const; void Period(const int); bool EveryTick(void) const; void EveryTick(const bool); bool OneTradePerCandle(void) const; void OneTradePerCandle(const bool); bool PositionReverse(void) const; void PositionReverse(const bool); //--- дополнительные свечи void AddCandle(const string,const int); //--- обнаружение нового бара void DetectNewBars(void); //-- события virtual bool OnTick(void); virtual void OnChartEvent(const int,const long&,const double&,const string&); virtual void OnTimer(void); virtual void OnTrade(void); virtual void OnDeinit(const int,const int); //--- восстановление virtual bool Save(const int); virtual bool Load(const int); protected: //--- свечной менеджер virtual bool IsNewBar(const string,const int); //--- менеджер ордеров virtual void ManageOrders(void); virtual void ManageOrdersHistory(void); virtual void OnTradeTransaction(COrder*) {} virtual datetime Time(const int); virtual bool TradeOpen(const string,const ENUM_ORDER_TYPE,double,bool); //--- менеджер символов virtual bool RefreshRates(void); //--- деинициализация void DeinitAccount(void); void DeinitCandle(void); void DeinitSignals(void); void DeinitSymbol(void); void DeinitTimes(void); };
Большинство методов, объявленных в этом классе, выполняют роль оберток методов его компонентов. Ключевые методы класса описаны ниже.
Инициализация
В фазе инициализации советника в первую очередь надо создать объекты, необходимые для работы торговой стратегии (мани-менеджмент, сигналы и т.д.) и интегрировать их с экземпляром СExpertAdvisor, который также нужно создать в OnInit. В итоге при запуске в советнике любой функции событий нам понадобится всего лишь одна строка кода, которая вызывает соответствующий обработчик или метод экземпляра CExpertAdvisor. Это очень похоже на способ использования CExpert из Стандартной библиотеки MQL5.
После создания экземпляра CExpertAdvisor следующим вызывается его метод Init. Вот код этого метода:
bool CExpertAdvisorBase::Init(string symbol,int period,int magic,bool every_tick=true,bool one_trade_per_candle=true,bool position_reverse=true) { m_symbol_name=symbol; CSymbolInfo *instrument; if((instrument=new CSymbolInfo)==NULL) return false; if(symbol==NULL) symbol=Symbol(); if(!instrument.Name(symbol)) return false; instrument.Refresh(); m_symbol_man.Add(instrument); m_symbol_man.SetPrimary(m_symbol_name); m_period=(ENUM_TIMEFRAMES)period; m_every_tick=every_tick; m_order_man.Magic(magic); m_position_reverse=position_reverse; m_one_trade_per_candle=one_trade_per_candle; CCandle *candle=new CCandle(); candle.Init(instrument,m_period); m_candle_man.Add(candle); Magic(magic); return false; }
Здесь мы создаем экземпляры большинства компонентов, которые часто встречаются в торговых стратегиях. Также сюда включены символ или инструмент, который будет использоваться (он должен быть переведен в тип объекта) и период/таймфрейм по умолчанию. Здесь же прописываются правила:
- должны ли ключевые задачи стратегии выполняться на каждым тике или только на первом тике каждой свечи;
- нужно ли устанавливать максимальный лимит — одна сделка на свечу (чтобы предотвратить многократные входы в течение одной свечи);
- должна ли позиция разворачиваться при получении противоположного сигнала (закрываться текущая сделка и открываться новая на основании нового сигнала).
В конце функции OnInit экземпляр CExpertAdvisor должен вызвать его метод InitComponents. Нижеследующий фрагмент кода демонстрирует указанный метод для CExpertBase:
bool CExpertAdvisorBase::InitComponents(void) { if(!InitSignals()) { Print(__FUNCTION__+": error in signal initialization"); return false; } if(!InitTimes()) { Print(__FUNCTION__+": error in time initialization"); return false; } if(!InitOrderManager()) { Print(__FUNCTION__+": error in order manager initialization"); return false; } if(!InitCandleManager()) { Print(__FUNCTION__+": error in candle manager initialization"); return false; } if(!InitEventAggregator()) { Print(__FUNCTION__+": error in event aggregator initialization"); return false; } return true; }
В этом методе вызываются методы Init всех компонентов экземпляра советника. Аналогичным способом в этом методе вызываются методы Validate для каждого компонента, чтобы проверить, пройдут ли валидацию их настройки.
Определение нового бара
Некоторые торговые стратегии работают только на первом тике новой свечи. Есть много способов реализовать эту функцию. Один из них — сравнивать время и цену открытия текущей свечи с их предыдущими состояниями. Этот метод реализован в классе CCandle. Нижеследующий фрагмент кода показывает объявление класса CCandleBase, базового для CCandle:
class CCandleBase : public CObject { protected: bool m_new; bool m_wait_for_new; bool m_trade_processed; int m_period; bool m_active; MqlRates m_last; CSymbolInfo *m_symbol; CEventAggregator *m_event_man; CObject *m_container; public: CCandleBase(void); ~CCandleBase(void); virtual int Type(void) const {return(CLASS_TYPE_CANDLE);} virtual bool Init(CSymbolInfo*,const int); virtual bool Init(CEventAggregator*); CObject *GetContainer(void); void SetContainer(CObject*); //--- методы установки и получения void Active(bool); bool Active(void) const; datetime LastTime(void) const; double LastOpen(void) const; double LastHigh(void) const; double LastLow(void) const; double LastClose(void) const; string SymbolName(void) const; int Timeframe(void) const; void WaitForNew(bool); bool WaitForNew(void) const; //--- обработка virtual bool TradeProcessed(void) const; virtual void TradeProcessed(bool); virtual void Check(void); virtual void IsNewCandle(bool); virtual bool IsNewCandle(void) const; virtual bool Compare(MqlRates &) const; //--- восстановление virtual bool Save(const int); virtual bool Load(const int); };
Проверка появления новой свечи на графике осуществляется методом Check:
CCandleBase::Check(void) { if(!Active()) return; IsNewCandle(false); MqlRates rates[]; if(CopyRates(m_symbol.Name(),(ENUM_TIMEFRAMES)m_period,1,1,rates)==-1) return; if(Compare(rates[0])) { IsNewCandle(true); TradeProcessed(false); m_last=rates[0]; } }
При проверке на новый бар экземпляр советника должен вызывать этот метод на каждом тике. Затем программист может расширить CCxpertAdvisor так, чтобы при появлении новой свечи на графике он мог выполнять дополнительные задачи.
Как показано в коде выше, фактическое сравнение времени открытия и цены открытия бара происходит методом Compare этого класса, как показано в коде ниже:
bool CCandleBase::Compare(MqlRates &rates) const { return (m_last.time!=rates.time || (m_last.open/m_symbol.TickSize())!=(rates.open/m_symbol.TickSize()) || (!m_wait_for_new && m_last.time==0)); }
Этот метод проверки на наличие нового бара зависит от трех условий. Удовлетворение хотя бы одного из них будет гарантией результата true, который говорит о том, что на графике появилась новая свеча:
- Последнее записанное время открытия не равно времени открытия текущего бара.
- Последняя записанная цена открытия не совпадает с ценой открытия текущего бара.
- Последнее записанное время открытия нулевое, и новый тик не должен быть первым тиком этой свечи.
Первые два условия подразумевают прямое сравнение значений текущего бара с предыдущей записью. Третье условие относится только к самому первому тику, который будет зарегистрирован экспертом. Как только советник загружен на график, он пока еще не имеет предыдущих записей (значений времени и цены открытия). Таким образом, последнее записанное время будет равно нулю. Некоторые трейдеры рассматривают этот бар как новый в их советниках. Другие предпочитают, чтобы их эксперт ждал, когда после инициализации советника на графике появится новый бар.
Подобно другим типам классов, которые мы описали ранее, класс CCandle также должен иметь свой контейнер, CCandleManager. В коде ниже показано объявление класса СCandleManagerBase:
class CCandleManagerBase : public CArrayObj { protected: bool m_active; CSymbolManager *m_symbol_man; CEventAggregator *m_event_man; CObject *m_container; public: CCandleManagerBase(void); ~CCandleManagerBase(void); virtual int Type(void) const {return(CLASS_TYPE_CANDLE_MANAGER);} virtual bool Init(CSymbolManager*,CEventAggregator*); virtual bool Add(const string,const int); CObject *GetContainer(void); void SetContainer(CObject *container); bool Active(void) const; void Active(bool active); virtual void Check(void) const; virtual bool IsNewCandle(const string,const int) const; virtual CCandle *Get(const string,const int) const; virtual bool TradeProcessed(const string,const int) const; virtual void TradeProcessed(const string,const int,const bool) const; //--- recovery virtual bool Save(const int); virtual bool Load(const int); };
Экземпляр класса СCandle создается на основе имени инструмента и таймфрейма. Наличие CCandleManager упрощает советнику отслеживание нескольких графиков по заданному инструменту — например, можно проверить появление новой свечи на EURUSD M15 и EURUSD H1 в одном и том же советнике. Экземпляры CCandle с одинаковыми символом и таймфреймом избыточны, и их желательно избегать. При поиске определенного экземпляра CCandle нужно просто вызвать соответствующий метод, находящийся в CCandleManager, и указать символ и таймфрейм. В свою очередь, CCandleManager, найдет соответствующий экземпляр CCandle и вызовет предназначенный для него метод.
Кроме проверки возникновения новой свечи, CCandle и CCandleManager выполняют еще одну функцию: проверяют, открыл ли советник сделку по данному символу и таймфрейму. Можно проверять недавнюю сделку и только по символу, не учитывая таймфрейм. Чтобы реализовать такое поведение, сам экземпляр CExpertAdvisor должен установить/сбросить флаг, отвечающий за это. Переключатель для обоих классов можно установить с использованием метода TradeProcessed.
При управлении свечами методы TradeProcessed (как установки, так и получения) работают только для нахождения запрошенного экземпляра CCandle и применения соответствующего значения:
bool CCandleManagerBase::TradeProcessed(const string symbol,const int timeframe) const { CCandle *candle=Get(symbol,timeframe); if(CheckPointer(candle)) return candle.TradeProcessed(); return false; }
CCandle присваивает новое значение одному из его членов класса, m_trade_processed. За это отвечают показанные ниже методы:
bool CCandleBase::TradeProcessed(void) const { return m_trade_processed; } CCandleBase::TradeProcessed(bool value) { m_trade_processed=value; }
Обработчик OnTick
Метод OnTick класса CExpertAdvisor чаще всего используется в этом классе. Именно он выполняет большую часть действий. Главная операция этого метода показана на диаграмме:
Процесс начинается с переключения тикового флага в советнике. Это предотвращает повторную обработку тика. Метод OnTick в идеале вызывается только внутри функции события OnTick, но может быть вызан также и другими средствами — например, OnChartEvent. В отсутствии этого флажка метод OnTick класса может быть вызван, когда обработка предыдущего тика еще не закончена. Тогда тик может быть обработан более одного раза, а если на нем генерируется сделка, то и она тоже может быть продублирована.
Обновление данных нужно, чтобы у советника был доступ к самым свежим данным и он не переключался бы на обработку предыдущего тика. Если советнику не удается обновить данные, он сбрасывает флаг обработки тика, завершает работу метода и начинает ждать нового тика.
Следующий шаг — поиск новых баров и проверка торговых сигналов. Такая проверка по умолчанию производится на каждом тике. Однако можно расширить этот метод так, чтобы он производил эту проверку, только когда зарегистрировано поступление нового сигнала (чтобы ускорить время обработки, особенно во время бэк-тестов и оптимизации).
Также в классе присутствует член класса, m_position_reverse, который отвечает за переворот позиции в направлении, противоположном полученному сигналу. Такой переворот выполняется здесь только для нейтрализации текущей позиции. В MetaTrader 4 и режиме хеджирования MetaTrader 5 член класса обрабатывает выход из сделок, противоположных полученному сигналу (сделки, направление которых соответствует направлению текущего сигнала, не закрываются). В неттинговом режиме MetaTrader 5 может быть только одна позиция. Таким образом, советник будет открывать новую позицию с объемом, аналогичным текущей сделке, но в противоположном направлении.
Торговый сигнал обрабатывается в основном с использованием m_signals, но и другие факторы — например, торговля только на новом баре или временные фильтры — также могут предотвратить открытие новой сделки торговым советником. Эксперт получает возможность открыть новую сделку только при удовлетворении всем условиям.
В конце обработки тика советник установит тиковый флаг на false, и после этого ему будет разрешено обработать другой тик.
Контейнер торговых экспертов
Аналогично другим объектам класса, описанным в предыдущих статьях, класс CExpertAdvisor также имеет свой контейнер — CExpertAdvisors. В нижеследующем фрагменте кода показано объявление его базового класса, CExpertAdvisorsBase:
class CExpertAdvisorsBase : public CArrayObj { protected: bool m_active; int m_uninit_reason; CObject *m_container; public: CExpertAdvisorsBase(void); ~CExpertAdvisorsBase(void); virtual int Type(void) const {return CLASS_TYPE_EXPERTS;} virtual int UninitializeReason(void) const {return m_uninit_reason;} //--- методы получения и установки void SetContainer(CObject *container); CObject *GetContainer(void); bool Active(void) const; void Active(const bool); int OrdersTotal(void) const; int OrdersHistoryTotal(void) const; int TradesTotal(void) const; //--- инициализация virtual bool Validate(void) const; virtual bool InitComponents(void) const; //--- события virtual void OnTick(void); virtual void OnChartEvent(const int,const long&,const double&,const string&); virtual void OnTimer(void); virtual void OnTrade(void); virtual void OnDeinit(const int,const int); //--- восстановление virtual bool CreateElement(const int); virtual bool Save(const int); virtual bool Load(const int); };
Этот контейнер в первую очередь отражает публичные методы класса. Пример этого — обработчик OnTick. Метод просто перебирает каждый экземпляр СExpertAdvisor для вызова его метода OnTick:
void CExpertAdvisorsBase::OnTick(void) { if(!Active()) return; for(int i=0;i<Total();i++) { CExpertAdvisor *e=At(i); e.OnTick(); } }
Наличие этого контейнера дает возможность хранить множественные экземпляры CExpertAdvisor. Это, возможно, единственный способ запустить несколько советников на одном графике. Просто инициализируйте несколько экземпляров CExpertAdvisor, сохраните их указатели в один контейнер CExpertAdvisors, а затем используйте метод OnTick контейнера, чтобы методы OnTick каждого экземпляра CExpertAdvisor срабатывали. То же самое можно сделать для каждого экземпляра класса CExpert из Стандартной библиотеки MQL5, используя класс CArrayObj или его наследники.
Сохранение данных
Некоторые данные, которые используются в экземплярах CExpertAdvisor, хранятся только в памяти компьютера. Как правило, необходимые данные сохраняются в платформе, а советник получает их из нее с помощью функции вызова. Однако для данных, которые создаются динамически, пока запущен советник, это не так. Когда эксперт запускает событие OnDeinit, советник удаляет все объекты и таким образом теряет их данные.
OnDeinit может быть запущен в нескольких случаях: при закрытии всей платформы (MetaTrader 4 или MetaTrader 5), при удалении эксперта с графика или когда происходит повторная компиляция советника после изменения его кода. Полный список вероятных событий, которые могут вызвать деинициализацию, можно увидеть при использовании функции UninitializeReason. Когда советник теряет доступ к этим данным, он с высокой вероятностью ведет себя так, как будто только что впервые был загружен на график.
Большая часть волатильных данных в классе CExpertAdvisor может находиться в одном из его членов, который является экземпляром COrderManager. Здесь создаются экземпляры COrder и COrderStop (и их потомки), в то время как эксперт выполняет свою обычную работу. Поскольку эти экземпляры создаются динамически с использованием OnTick, они не восстанавливаются при новой инициализации советника. Поэтому в советнике должен быть реализован метод сохранения и извлечения этих изменяющихся данных. Один способ имплементации этой деятельности — использовать наследник класса CFileBin, CExpertFile. В коде ниже показано объявление его базового класса, CExpertFileBase.
class CExpertFileBase : public CFileBin { public: CExpertFileBase(void); ~CExpertFileBase(void); void Handle(const int handle) { m_handle=handle; }; uint WriteBool(const bool value); bool ReadBool(bool &value); };
Здесь мы расширим CFileBin, чтобы явно объявлять методы записи и считывания данных типа bool.
В конце файла этого класса мы объявляем экземпляр класса CExpertFile. Этот экземпляр будет использоваться всё время работы советника, если он должен сохранять и загружать изменяющиеся данные. В качестве альтернативы, можно просто довериться методам Save и Load, унаследованным от CObject, и обрабатывать сохранение и загрузку привычным способом. Но это может быть очень скрупулезная работа. Множество сил и строк кода можно сэкономить, если использовать только CFile (или его наследники).
//CExpertFileBase class definition //+------------------------------------------------------------------+ #ifdef __MQL5__ #include "..\..\MQL5\File\ExpertFile.mqh" #else #include "..\..\MQL4\File\ExpertFile.mqh" #endif //+------------------------------------------------------------------+ CExpertFile file; //+------------------------------------------------------------------+
Менеджер ордеров сохраняет изменяющиеся данные методом Save:
bool COrderManagerBase::Save(const int handle) { if(handle==INVALID_HANDLE) return false; file.WriteDouble(m_lotsize); file.WriteString(m_comment); file.WriteInteger(m_expiration); file.WriteInteger(m_history_count); file.WriteInteger(m_max_orders_history); file.WriteBool(m_trade_allowed); file.WriteBool(m_long_allowed); file.WriteBool(m_short_allowed); file.WriteInteger(m_max_orders); file.WriteInteger(m_max_trades); file.WriteObject(GetPointer(m_orders)); file.WriteObject(GetPointer(m_orders_history)); return true; }
Большая часть этих данных — примитивы, кроме последних двух, которые являются контейнерами текущих и исторических ордеров. Для этих данных используется метод WriteObject класса CFileBin, который просто вызывает метод Save объекта, который нужно записать. В коде ниже показан метод Save класса COrderBase:
bool COrderBase::Save(const int handle) { if(handle==INVALID_HANDLE) return false; file.WriteBool(m_initialized); file.WriteBool(m_closed); file.WriteBool(m_suspend); file.WriteInteger(m_magic); file.WriteDouble(m_price); file.WriteLong(m_ticket); file.WriteEnum(m_type); file.WriteDouble(m_volume); file.WriteDouble(m_volume_initial); file.WriteString(m_symbol); file.WriteObject(GetPointer(m_order_stops)); return true; }
Как мы можем здесь видеть, процесс просто повторяется при сохранении объектов. Данные-примитивы сохраняются в файл, как обычно. Для данных сложного типа метод Save объекта вызывается через метод WriteObject класса CFileBin.
В случаях, когда перед нами несколько экземпляров CExpertAdvisor, способность сохранять данные имеет также контейнер CExpertAdvisors:
bool CExpertAdvisorsBase::Save(const int handle) { if(handle!=INVALID_HANDLE) { for(int i=0;i<Total();i++) { CExpertAdvisor *e=At(i); if(!e.Save(handle)) return false; } } return true; }
Методы Save вызываются для каждого экземпляра CExpertAdvisor. Единственный хэндл файла означает, что для каждого файла торгового эксперта должен быть только один файл сохранения. Каждый экземпляр CExpertAdvisor мог бы иметь свой собственный файл сохранения, но это было бы более сложным подходом.
Загрузка данных — более сложная часть. При сохранении значения некоторых членов класса просто записываются в файл. При загрузке этих данных экземпляры объекта должны быть восстановлены, и в идеале — в том же состоянии, что и до сохранения. Код ниже показывает метод Load менеджера ордеров.
bool COrderManagerBase::Load(const int handle) { if(handle==INVALID_HANDLE) return false; if(!file.ReadDouble(m_lotsize)) return false; if(!file.ReadString(m_comment)) return false; if(!file.ReadInteger(m_expiration)) return false; if(!file.ReadInteger(m_history_count)) return false; if(!file.ReadInteger(m_max_orders_history)) return false; if(!file.ReadBool(m_trade_allowed)) return false; if(!file.ReadBool(m_long_allowed)) return false; if(!file.ReadBool(m_short_allowed)) return false; if(!file.ReadInteger(m_max_orders)) return false; if(!file.ReadInteger(m_max_trades)) return false; if(!file.ReadObject(GetPointer(m_orders))) return false; if(!file.ReadObject(GetPointer(m_orders_history))) return false; for(int i=0;i<m_orders.Total();i++) { COrder *order=m_orders.At(i); if(!CheckPointer(order)) continue; COrderStops *orderstops=order.OrderStops(); if(!CheckPointer(orderstops)) continue; for(int j=0;j<orderstops.Total();j++) { COrderStop *orderstop=orderstops.At(j); if(!CheckPointer(orderstop)) continue; for(int k=0;k<m_stops.Total();k++) { CStop *stop=m_stops.At(k); if(!CheckPointer(stop)) continue; orderstop.Order(order); if(StringCompare(orderstop.StopName(),stop.Name())==0) { orderstop.Stop(stop); orderstop.Recreate(); } } } } return true; }
Как видим, код метода в COrderManager более сложный, в отличие от аналогичного метода Load в классе CExpertAdvisor. Причина в том, что, в отличие от менеджера ордеров, экземпляры CExpertAdvisor создаются через OnInit, так что контейнеру нужно просто вызвать методы Load для каждого экземпляра CExpertAdvisor, вместо того, чтобы использовать метод ReadObject класса CFileBin.
Экземпляры класса, не созданные во время выполнения OnInit, должны будут создаваться по мере перезагрузки советника. Это достигается расширением метода CreateElement класса CArrayObj. Объект не может создать сам себя, так что он должен создаваться его родительским объектом или контейнером, или даже из исходного кода заголовочного файла. Пример можно видеть в расширенном методе CreateElement, который находится в COrdersBase. В этом классе контейнером является COrders (наследник класса COrdersBase), а созданный объект имеет тип COrder:
bool COrdersBase::CreateElement(const int index) { COrder*order=new COrder(); if(!CheckPointer(order)) return(false); order.SetContainer(GetPointer(this)); if(!Reserve(1)) return(false); m_data[index]=order; m_sort_mode=-1; return CheckPointer(m_data[index]); }
Здесь, кроме создания элемента, мы также устанавливаем его родительский объект или контейнер, чтобы отличить, относится ли он к списку активных сделок (член m_orders класса COrderManagerBase) или исторических (член m_orders_history класса COrderManagerBase).
Примеры
Примеры №№ 1 — 4 из этой статьи — измененные версии четырех примеров из предыдущей статьи (см. Кроссплатформенный торговый советник: Пользовательские стопы, Трейлинг и Безубыток). Давайте взглянем на самый сложный пример expert_custom_trail_ha_ma.mqh, который является модифицированным вариантом custom_trail_ha_ma.mqh.
Перед функцией OnInit мы объявляем следующие экземпляры глобальных объектов:
COrderManager *order_manager; CSymbolManager *symbol_manager; CSymbolInfo *symbol_info; CSignals *signals; CMoneys *money_manager; CTimes *time_filters;
Заменяем его экземпляром CExpert. Некоторые из вышеперечисленных экземпляров находятся в самом CExpetAdvisor (например, COrderManager), остальные должны быть созданы методом OnInit (контейнеры классов):
CExpertAdvisors experts;
В начале метода создаем экземпляр CExpertAdvisor. Также вызываем метод Init с базовыми настройками:
int OnInit() { //--- CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),12345,true,true,true); //--- other code //--- return(INIT_SUCCEEDED); }
Больше не нужно создавать CSymbolInfo / CSymbolManager, поскольку экземпляры класса CExpertAdvisor сами могут создавать экземпляры этих классов.
Функция, определяемая пользователем, тоже должна быть удалена — наш экперт больше в ней не нуждается.
Удаляем из кода и глобальное объявление контейнеров, поскольку они должны быть объявлены из OnInit. Пример этого — контейнер временных фильтров (CTimeFilters), находящийся в функции OnInit:
CTimes *time_filters=new CTimes();
Указатели на контейнеры, добавлявшиеся ранее в менеджер ордеров, теперь вместо этого добавляются в экземпляр CExpertAdvisor. Все другие контейнеры, которые не добавлялись в менеджер ордеров, тоже должны быть добавлены к экземпляру CExpertAdvisor. Эти указатели хранит именно COrderManager. Экземпляр CExpertAdvisor создает только методы оболочки.
После этого добавляем экземпляр CExpertAdvisor к экземпляру CExpertAdvisors. Затем вызываем метод InitComponents экземпляра CExpertAdvisors. Это гарантирует инициализацию всех экземпляров CExpertAdvisor и его компонентов.
int OnInit() { //--- //--- другой код experts.Add(GetPointer(expert)); if(!experts.InitComponents()) return(INIT_FAILED); //--- другой код //--- return(INIT_SUCCEEDED); }
И, наконец, вставляем код, обеспечивающий перезагрузку эксперта, если его работа была прервана.
int OnInit() { //--- //--- другой код file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) return(INIT_FAILED); file.Close(); //--- return(INIT_SUCCEEDED); }
Если советник не загружается из файла, он возвращает INIT_FAILED. Однако, если отсутствует файл сохранения (и генерируется INVALID_HANDLE), ошибки инициализации советника не будет, поскольку методы Load классов CExpertAdvisors и CExpertAdvisor возвращают true после получения недействительного хэндла. Этот подход содержит определенный риск, но крайне маловероятно, что сохраненный файл будет открыт другой программой. Просто убедитесь, что каждый экземпляр советника, запущенный на графике, имеет отдельный файл сохранения (так же, как и с магическим номером ордера).
Пятый пример взят не из предыдущей статьи. Вместо этого, в нем все четыре советника из этой статьи скомбинированы в один эксперт. В нем используется слегка измененная версия функции OnInit для каждого из этих советников, которая объявляется как "определяемая пользователем". Ее возвращаемое значение имеет тип CExpertAdvisor*. Если создание эксперта не удалось, возвращается NULL вместо INIT_SUCCEEDED. Фрагмент кода ниже показывает обновленную функцию OnInit заголовочного файла комбинированного эксперта:
int OnInit() { //--- CExpertAdvisor *expert1=expert_breakeven_ha_ma(); CExpertAdvisor *expert2=expert_trail_ha_ma(); CExpertAdvisor *expert3=expert_custom_stop_ha_ma(); CExpertAdvisor *expert4=expert_custom_trail_ha_ma(); if (!CheckPointer(expert1)) return INIT_FAILED; if (!CheckPointer(expert2)) return INIT_FAILED; if (!CheckPointer(expert3)) return INIT_FAILED; if (!CheckPointer(expert4)) return INIT_FAILED; experts.Add(GetPointer(expert1)); experts.Add(GetPointer(expert2)); experts.Add(GetPointer(expert3)); experts.Add(GetPointer(expert4)); if(!experts.InitComponents()) return(INIT_FAILED); file.Open(savefile,FILE_READ); if(!experts.Load(file.Handle())) return(INIT_FAILED); file.Close(); //--- return(INIT_SUCCEEDED); }
Советник начинает работу с инициализации каждого экземпляра CExpertAdvisor. Затем он перейдет к проверке каждого из указателей на CExpertAdvisor. Если указатель нединамический, тогда функция возвращает INIT_FAILED, и инициализация не удастся. Если каждый из экземпляров проходит проверку указателей, эти указатели сохраняются в экземпляре CExpertAdvisors. Экземпляр CExpertAdvisors (контейнер, а не экземпляр советника) затем инициализирует его компоненты и при необходимости загружает предыдущие данные.
Советник использует функцию, определяемую пользователем, для создания экземпляра СExpertAdvisor. Код ниже показывает функцию, использованную для создания экземпляра четвертого советника:
CExpertAdvisor *expert_custom_trail_ha_ma() { CExpertAdvisor *expert=new CExpertAdvisor(); expert.Init(Symbol(),Period(),magic4,true,true,true); CMoneys *money_manager=new CMoneys(); CMoney *money_fixed=new CMoneyFixedLot(0.05); CMoney *money_ff=new CMoneyFixedFractional(5); CMoney *money_ratio=new CMoneyFixedRatio(0,0.1,1000); CMoney *money_riskperpoint=new CMoneyFixedRiskPerPoint(0.1); CMoney *money_risk=new CMoneyFixedRisk(100); money_manager.Add(money_fixed); money_manager.Add(money_ff); money_manager.Add(money_ratio); money_manager.Add(money_riskperpoint); money_manager.Add(money_risk); expert.AddMoneys(GetPointer(money_manager)); CTimes *time_filters=new CTimes(); if(time_range_enabled && time_range_end>0 && time_range_end>time_range_start) { CTimeRange *timerange=new CTimeRange(time_range_start,time_range_end); time_filters.Add(GetPointer(timerange)); } if(time_days_enabled) { CTimeDays *timedays=new CTimeDays(sunday_enabled,monday_enabled,tuesday_enabled,wednesday_enabled,thursday_enabled,friday_enabled,saturday_enabled); time_filters.Add(GetPointer(timedays)); } if(timer_enabled) { CTimer *timer=new CTimer(timer_minutes*60); timer.TimeStart(TimeCurrent()); time_filters.Add(GetPointer(timer)); } switch(time_intraday_set) { case INTRADAY_SET_1: { CTimeFilter *timefilter=new CTimeFilter(time_intraday_gmt,intraday1_hour_start,intraday1_hour_end,intraday1_minute_start,intraday1_minute_end); time_filters.Add(timefilter); break; } case INTRADAY_SET_2: { CTimeFilter *timefilter=new CTimeFilter(0,0,0); timefilter.Reverse(true); CTimeFilter *sub1 = new CTimeFilter(time_intraday_gmt,intraday2_hour1_start,intraday2_hour1_end,intraday2_minute1_start,intraday2_minute1_end); CTimeFilter *sub2 = new CTimeFilter(time_intraday_gmt,intraday2_hour2_start,intraday2_hour2_end,intraday2_minute2_start,intraday2_minute2_end); timefilter.AddFilter(sub1); timefilter.AddFilter(sub2); time_filters.Add(timefilter); break; } default: break; } expert.AddTimes(GetPointer(time_filters)); CStops *stops=new CStops(); CCustomStop *main=new CCustomStop("main"); main.StopType(stop_type_main); main.VolumeType(VOLUME_TYPE_PERCENT_TOTAL); main.Main(true); //main.StopLoss(stop_loss); //main.TakeProfit(take_profit); stops.Add(GetPointer(main)); CTrails *trails=new CTrails(); CCustomTrail *trail=new CCustomTrail(); trails.Add(trail); main.Add(trails); expert.AddStops(GetPointer(stops)); MqlParam params[1]; params[0].type=TYPE_STRING; #ifdef __MQL5__ params[0].string_value="Examples\\Heiken_Ashi"; #else params[0].string_value="Heiken Ashi"; #endif SignalHA *signal_ha=new SignalHA(Symbol(),0,1,params,signal_bar); SignalMA *signal_ma=new SignalMA(Symbol(),(ENUM_TIMEFRAMES) Period(),maperiod,0,mamethod,maapplied,signal_bar); CSignals *signals=new CSignals(); signals.Add(GetPointer(signal_ha)); signals.Add(GetPointer(signal_ma)); expert.AddSignal(GetPointer(signals)); //--- return expert; }
Как мы можем видеть, код выглядит очень похоже на функцию OnInit заголовочного файла оригинального советника (expert_custom_trail_ha_ma.mqh). Другие функции, определяемые пользователем, организованы таким же образом.
Окончательные заметки
Прежде чем завершить эту серию статей, расскажу читателям, которые захотят использовать библиотеку, о факторах, поспособствовавших ее развитию.
К настоящему моменту библиотека, представленная в приложении к этой статье, насчитывает более 10 000 строк кода (включая комментарии). Несмотря на это, она до сих пор не завершена. Чтобы в полной мере использовать возможности MQL4 и MQL5, нужно проделать большую работу.
Я начал работу над этим проектом еще до того, как в MetaTrader 5 стал доступен режим хеджирования. Это сильно повлияло на дальнейшую разработку библиотеки. В итоге она в большей степени склонна к обозначениям, принятым в MetaTrader 4, чем к MetaTrader 5. Кроме того, я столкнулся с рядом проблем по совместимости с некоторыми билдами, выпущенными за последние несколько лет, что привело к крупным и незначительным корректировкам кода (и вследствие этого — к задержке публикации некоторых частей этой серии). К моменту написания этих строк выходы обновлений для обеих платформ со временем стали более редкими и стабильными. Ожидается, что этот тренд усилится. Тем не менее, библиотеку надо будет дорабатывать под будущие обновления платформ, которые могут вызвать несовместимость.
Библиотека использует данные, сохраненные в памяти, для отслеживания собственных сделок. Это стало причиной того, что эксперт, созданный с ее использованием, сильно зависит от сохранения и загрузки данных, чтобы обеспечить восстановление работы после возможных перерывов и сбоев. Работа в дальнейшем над этой библиотекой (а также над любым кроссплатформенным продуктом) должна быть ориентирована на независимую (или почти независимую) реализацию — как и реализация Стандартной Библиотеки MQL5.
И последнее замечание: библиотека, созданная в этой сериии статей, не должна рассматриваться как постоянное решение. Ее можно использовать для более мягкого перехода с MetaTrader 4 на MetaTrader 5. Несовместимость между MQL4 и MQL5 может представлять серьезное препятствие трейдерам, которые собирались перейти на новую платформу. В результате исходный код на MQL4, написанный для их советников, должен быть переработан для совместимости с компилятором MQL5. Библиотека, созданная в этой статье, представляется в качестве средства развернуть советника на новой платформе с небольшими корректировками в исходном коде или вообще без них. Это может помочь трейдеру в его решении о том, продолжать использовать MetaTrader 4 или перейти на MetaTrader 5. В случае решения о переключении использование этой библиотеки может минимизировать количество изменений и позволит трейдеру использовать советник привычным способом. Но если решено пока использовать старую платформу, трейдер все равно получает возможность быстро переключиться на MetaTrader 5 в тот момент, когда разработчики прекратят поддержку MetaTrader 4.
Заключение
В этой статье разработаны объекты класса CExpertAdvisor и CExpertAdvisors, которые используются для интеграции всех компонентов кроссплатформенного торгового советника, обсуждавшихся в предыдущих статьях серии. Статья обсуждает, как создаются эти два класса и как они связываются с другими компонентами кроссплатформенного торгового советника. Также предоставлено несколько решений проблем, которые обычно возникают в работе с советниками: детекция нового бара, сохранение и загрузка изменчивых данных.
Программы, использованные в статье
# | Имя |
Тип |
Описание параметров |
---|---|---|---|
1. |
expert_breakeven_ha_ma.mqh |
Заголовочный файл |
Основной заголовочный файл, использованный в первом примере |
2. |
expert_breakeven_ha_ma.mq4 | Советник |
Основной исходный файл, используемый для версии MQL4 в первом примере |
3. |
expert_breakeven_ha_ma.mq5 | Советник | Основной исходный файл, используемый для версии MQL5 в первом примере |
4. |
expert_trail_ha_ma.mqh | Заголовочный файл | Основной заголовочный файл, использованный во втором примере |
5. |
expert_trail_ha_ma.mq4 | Советник | Основной исходный файл, используемый для версии MQL4 во втором примере |
6. |
expert_trail_ha_ma.mq5 | Советник | Основной исходный файл, используемый для версии MQL5 во втором примере |
7. |
expert_custom_stop_ha_ma.mqh | Заголовочный файл | Основной заголовочный файл, использованный в третьем примере |
8. |
expert_custom_stop_ha_ma.mq4 | Советник | Основной исходный файл, используемый для версии MQL4 в третьем примере |
9. |
expert_custom_stop_ha_ma.mq5 | Советник | Основной исходный файл, используемый для версии MQL5 в третьем примере |
10. |
expert_custom_trail_ha_ma.mqh | Заголовочный файл | Основной заголовочный файл, использованный в четвертом примере |
11. |
expert_custom_trail_ha_ma.mq4 | Советник | Основной исходный файл, используемый для версии MQL4 в четвертом примере |
12. |
expert_custom_trail_ha_ma.mq5 | Советник | Основной исходный файл, используемый для версии MQL5 в четвертом примере |
13. |
combined.mqh | Заголовочный файл | Основной заголовочный файл, использованный в пятом примере |
13. |
combined.mq4 | Советник | Основной исходный файл, используемый для версии MQL4 в пятом примере |
15. |
combined.mq5 | Советник | Основной исходный файл, используемый для версии MQL5 в пятом примере |
Файлы классов, созданных в статье
# |
Имя |
Тип |
Описание параметров |
---|---|---|---|
1. | MQLx\Base\Expert\ExperAdvisorsBase | Заголовочный файл |
CExpertAdvisors (контейнер CExpertAdvisor, базовый класс) |
2. |
MQLx\MQL4\Expert\ExperAdvisors | Заголовочный файл | CExpertAdvisors (версия MQL4) |
3. |
MQLx\MQL5\Expert\ExperAdvisors | Заголовочный файл |
CExpertAdvisors (версия MQL5) |
4. |
MQLx\Base\Expert\ExperAdvisorBase | Заголовочный файл |
CExpertAdvisor (базовый класс) |
5. |
MQLx\MQL4\Expert\ExperAdvisor | Заголовочный файл |
CExpertAdvisor (версия MQL4) |
6. |
MQLx\MQL5\Expert\ExperAdvisor | Заголовочный файл |
CExpertAdvisor (версия MQL5) |
7. |
MQLx\Base\Candle\CandleManagerBase | Заголовочный файл | CCandleManager (контейнер CCandle, базовый класс) |
8. |
MQLx\MQL4\Candle\CandleManager | Заголовочный файл | CCandleManager (версия MQL4) |
9. |
MQLx\MQL5\Candle\CandleManager | Заголовочный файл | CCandleManager (версия MQL5) |
10. |
MQLx\Base\Candle\CandleBase | Заголовочный файл | CCandle (базовый класс) |
11. |
MQLx\MQL4\Candle\Candle | Заголовочный файл | CCandle (версия MQL4) |
12. |
MQLx\MQL5\Candle\Candle | Заголовочный файл |
CCandle (версия MQL5) |
13. |
MQLx\Base\File\ExpertFileBase | Заголовочный файл | CExpertFile (базовый класс) |
13. |
MQLx\MQL4\File\ExpertFile | Заголовочный файл | CExpertFile(версия MQL4) |
15. |
MQLx\MQL5\File\ExpertFile | Заголовочный файл | CExpertFile(версия MQL5) |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/3622
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Опубликована статья Кроссплатформенный торговый советник: Классы CExpertAdvisor и CExpertAdvisors:
Автор: Enrico Lambino
Проделана большая работа.
Спасибо большое!