Чтобы обеспечить самооптимизацию советника для реализации любых требуемых возможностей и функциональностей, используется схема, представленная на рисунке 1.
На временной шкале "История" советник позиционируется в точке "время сейчас", где принимается решение об оптимизации. Советник "EA" вызывает "функцию менеджера", которая управляет процессом оптимизации, при этом советник передает этой функции настройки оптимизации, "параметры оптимизации".
В свою очередь, менеджер запрашивает набор параметров из алгоритма оптимизации, "алгоритм оптимизации" или "АО", который отныне будет называться "набор". Впоследствии менеджер передает набор в виртуальную торговую стратегию "EA Virt", которая является полным аналогом реальной стратегии, выполняющей торговые операции "EA".
"EA Virt" участвует в виртуальной торговле с момента "прошлого" в истории до момента "время сейчас". Менеджер инициирует выполнение "EA Virt" столько раз, сколько указано размером популяции в "параметрах оптимизации". Затем "EA Virt" возвращает исторические результаты запуска в виде "ff result".
"ff result" представляет собой результат функции пригодности или критерий оптимизации, который может быть любым по усмотрению пользователя. Это может быть, например, баланс, коэффициент прибыли, математическое ожидание или сложный критерий, интеграл или совокупный дифференциал, измеряемый в различные моменты времени на временной шкале "History". Таким образом, результат функции пригодности, или "ff result" - это то, что пользователь считает важным показателем качества торговой стратегии.
Впоследствии "ff result", оценка определенного набора, передается менеджером алгоритму оптимизации.
При выполнении условия остановки менеджер передает наилучший набор торговому советнику "EA", после чего советник продолжает свою работу (торговлю) с обновленными параметрами от точки "time now" до точки повторной оптимизации "reoptimiz", где проводится повторная оптимизация на заданную глубину в истории.
Точка повторной оптимизации может быть выбрана исходя из различных соображений; это может быть фиксированное количество исторических баров, как в примере, приведенном ниже, или конкретное условие, такое как снижение торговых показателей до критического уровня.
Figure 1.
Согласно схеме работы алгоритма оптимизации "optimization ALGO", его можно рассматривать как "black box", который функционирует автономно (действительно, для него все внешнее также является "black box"), независимо от конкретной торговой стратегии, менеджера и виртуальной стратегии. Менеджер запрашивает набор из алгоритма оптимизации и отправляет обратно оценку этого набора, которую алгоритм оптимизации использует для определения следующего набора. Этот цикл продолжается до тех пор, пока не будет найден наилучший набор параметров, удовлетворяющий требованиям пользователя. Таким образом, алгоритм оптимизации ищет оптимальные параметры, которые конкретно удовлетворяют потребностям пользователя, определенным с помощью функции пригодности в "EA Virt".
Виртуализация индикатора
Чтобы запустить советник на исторических данных, необходимо создать виртуальную копию торговой стратегии, которая будет выполнять те же торговые операции, что и при работе на торговом счете. Когда индикаторы не используются, виртуализация логических условий в советнике становится относительно простой; для этого требуется только описать логические действия в соответствии с моментом времени в ценовом ряду. В то время как использование индикаторов представляет собой более сложную задачу, и в большинстве случаев торговые стратегии основаны на использовании различных индикаторов.
Проблема возникает при поиске оптимальных параметров индикатора, поскольку для этого требуется создать дескрипторы индикатора с текущим набором на данной итерации. После работы с историческими данными эти дескрипторы необходимо удалить; в противном случае оперативная память компьютера может быстро заполниться, особенно при наличии большого количества потенциальных наборов параметров. Это не проблема, если данная процедура выполняется на графике символов, но в тестере удаление дескриптора не разрешено.
Чтобы решить эту проблему, нам нужно виртуализировать расчет индикатора в исполняющем советнике, чтобы избежать использования дескрипторов. Давайте возьмем индикатор Stochastic в качестве примера.
Вычислительная часть каждого индикатора содержит стандартную функцию под названием "OnCalculate". Эту функцию нужно переименовать, например, в "Вычислять" и оставить практически без изменений.
Индикатор должен быть структурирован как класс (структура также будет работать), давайте назовем его "C_Stochastic". В объявлении класса основные индикаторные буферы должны быть определены как открытые поля (дополнительные вычислительные буферы могут быть закрытыми), и должна быть объявлена функция инициализации "Init", куда необходимо передать параметры индикатора.
//—————————————————————————————————————————————————————————————————————————————— class C_iStochastic { public: void Init (const int InpKPeriod, // K period const int InpDPeriod, // D period const int InpSlowing) // Slowing { inpKPeriod = InpKPeriod; inpDPeriod = InpDPeriod; inpSlowing = InpSlowing; } public: int Calculate (const int rates_total, const int prev_calculated, const double &high [], const double &low [], const double &close []); //--- indicator buffers public: double ExtMainBuffer []; public: double ExtSignalBuffer []; private: double ExtHighesBuffer []; private: double ExtLowesBuffer []; private: int inpKPeriod; // K period private: int inpDPeriod; // D period private: int inpSlowing; // Slowing }; //——————————————————————————————————————————————————————————————————————————————
И, соответственно, фактический расчет индикатора в методе "Рассчитать". Расчет индикатора абсолютно ничем не отличается от индикатора в стандартной настройке терминала. Единственное отличие заключается в выделении размера для буферов индикатора и их инициализации.
Это очень простой пример для понимания принципа виртуализации индикатора. Расчет выполняется по всей глубине периодов, указанных в параметрах индикатора.
//—————————————————————————————————————————————————————————————————————————————— int C_iStochastic::Calculate (const int rates_total, const int prev_calculated, const double &high [], const double &low [], const double &close []) { if (rates_total <= inpKPeriod + inpDPeriod + inpSlowing) return (0); ArrayResize (ExtHighesBuffer, rates_total); ArrayResize (ExtLowesBuffer, rates_total); ArrayResize (ExtMainBuffer, rates_total); ArrayResize (ExtSignalBuffer, rates_total); ArrayInitialize (ExtHighesBuffer, 0.0); ArrayInitialize (ExtLowesBuffer, 0.0); ArrayInitialize (ExtMainBuffer, 0.0); ArrayInitialize (ExtSignalBuffer, 0.0); int i, k, start; start = inpKPeriod - 1; if (start + 1 < prev_calculated) { start = prev_calculated - 2; Print ("start ", start); } else { for (i = 0; i < start; i++) { ExtLowesBuffer [i] = 0.0; ExtHighesBuffer [i] = 0.0; } } //--- calculate HighesBuffer[] and ExtHighesBuffer[] for (i = start; i < rates_total && !IsStopped (); i++) { double dmin = 1000000.0; double dmax = -1000000.0; for (k = i - inpKPeriod + 1; k <= i; k++) { if (dmin > low [k]) dmin = low [k]; if (dmax < high [k]) dmax = high [k]; } ExtLowesBuffer [i] = dmin; ExtHighesBuffer [i] = dmax; } //--- %K start = inpKPeriod - 1 + inpSlowing - 1; if (start + 1 < prev_calculated) start = prev_calculated - 2; else { for (i = 0; i < start; i++) ExtMainBuffer [i] = 0.0; } //--- main cycle for (i = start; i < rates_total && !IsStopped (); i++) { double sum_low = 0.0; double sum_high = 0.0; for (k = (i - inpSlowing + 1); k <= i; k++) { sum_low += (close [k] - ExtLowesBuffer [k]); sum_high += (ExtHighesBuffer [k] - ExtLowesBuffer [k]); } if (sum_high == 0.0) ExtMainBuffer [i] = 100.0; else ExtMainBuffer [i] = sum_low / sum_high * 100; } //--- signal start = inpDPeriod - 1; if (start + 1 < prev_calculated) start = prev_calculated - 2; else { for (i = 0; i < start; i++) ExtSignalBuffer [i] = 0.0; } for (i = start; i < rates_total && !IsStopped (); i++) { double sum = 0.0; for (k = 0; k < inpDPeriod; k++) sum += ExtMainBuffer [i - k]; ExtSignalBuffer [i] = sum / inpDPeriod; } //--- OnCalculate done. Return new prev_calculated. return (rates_total); } //——————————————————————————————————————————————————————————————————————————————
Виртуализация стратегии
Обсудив виртуализацию индикатора в советнике, мы теперь переходим к рассмотрению виртуализации стратегии. В начале кода советника мы объявляем импорт библиотек, включая файлы из стандартной торговой библиотеки и файл virtual stochastic.
Далее идут "входные" параметры советника, среди которых мы выделяем "InpKPeriod_P" и "InpUpperLevel_P". Эти параметры необходимо оптимизировать, отображая период действия индикатора "Stochastic" и его уровни.
input string InpKPeriod_P = "18|9|3|24"; //STO K period: it is necessary to optimize input string InpUpperLevel_P = "96|88|2|98"; //STO upper level: it is necessary to optimize
Кроме того, стоит отметить, что параметры объявляются с типом string. Эти параметры являются составными и включают значения по умолчанию, начальное значение оптимизации, шаг и конечное значение оптимизации.
При инициализации советника в рамках функции "OnInit" мы установим размер массивов параметров в соответствии с количеством оптимизируемых параметров: "Set" - набор параметров, "Range_Min" - минимальные значения параметров (начальные значения), "Range_Step" - параметр шаги, а "Range_Max" - максимальные значения параметров. Мы извлекем соответствующие значения из строковых параметров и присвоим их массивам.
//—————————————————————————————————————————————————————————————————————————————— #import "\\Market\\AO Core.ex5" bool Init (int colonySize, double &range_min [], double &range_max [], double &range_step []); //------------------------------------------------------------------------------ void Preparation (); void GetVariantCalc (double &variant [], int pos); void SetFitness (double value, int pos); void Revision (); //------------------------------------------------------------------------------ void GetVariant (double &variant [], int pos); double GetFitness (int pos); #import //—————————————————————————————————————————————————————————————————————————————— #include <Trade\Trade.mqh>; #include "cStochastic.mqh" input group "==== GENERAL ===="; sinput long InpMagicNumber = 132516; //Magic Number sinput double InpLotSize = 0.01; //Lots input group "==== Trading ===="; input int InpStopLoss = 1450; //Stoploss input int InpTakeProfit = 1200; //Takeprofit input group "==== Stochastic ==|value|start|step|end|=="; input string InpKPeriod_P = "18|9|3|24"; //STO K period : it is necessary to optimize input string InpUpperLevel_P = "96|88|2|98"; //STO upper level: it is necessary to optimize input group "====Self-optimization===="; sinput bool SelfOptimization = true; sinput int InpBarsOptimize = 18000; //Number of bars in the history for optimization sinput int InpBarsReOptimize = 1440; //After how many bars, EA will reoptimize sinput int InpPopSize = 50; //Population size sinput int NumberFFlaunches = 10000; //Number of runs in the history during optimization sinput int Spread = 10; //Spread MqlTick Tick; CTrade Trade; C_iStochastic IStoch; double Set []; double Range_Min []; double Range_Step []; double Range_Max []; double TickSize = 0.0; /—————————————————————————————————————————————————————————————————————————————— int OnInit () { TickSize = SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE); ArrayResize (Set, 2); ArrayResize (Range_Min, 2); ArrayResize (Range_Step, 2); ArrayResize (Range_Max, 2); string result []; if (StringSplit (InpKPeriod_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED; Set [0] = (double)StringToInteger (result [0]); Range_Min [0] = (double)StringToInteger (result [1]); Range_Step [0] = (double)StringToInteger (result [2]); Range_Max [0] = (double)StringToInteger (result [3]); if (StringSplit (InpUpperLevel_P, StringGetCharacter ("|", 0), result) != 4) return INIT_FAILED; Set [1] = (double)StringToInteger (result [0]); Range_Min [1] = (double)StringToInteger (result [1]); Range_Step [1] = (double)StringToInteger (result [2]); Range_Max [1] = (double)StringToInteger (result [3]); IStoch.Init ((int)Set [0], 1, 3); // set magicnumber to trade object Trade.SetExpertMagicNumber (InpMagicNumber); //--- return (INIT_SUCCEEDED); } //——————————————————————————————————————————————————————————————————————————————Кроме того, в код советника внутри функции "OnTick" мы вставляем блок, вызывающий самооптимизацию - функцию "Optimize", которая действует как "менеджер" на диаграмме на рисунке 1, инициируя процесс оптимизации. Там, где предполагалось использовать внешние переменные, которые необходимо оптимизировать, мы используем значения из массива "Set".
//—————————————————————————————————————————————————————————————————————————————— void OnTick () { //---------------------------------------------------------------------------- if (!IsNewBar ()) { return; } //---------------------------------------------------------------------------- if (SelfOptimization) { //-------------------------------------------------------------------------- static datetime LastOptimizeTime = 0; datetime timeNow = iTime (_Symbol, PERIOD_CURRENT, 0); datetime timeReop = iTime (_Symbol, PERIOD_CURRENT, InpBarsReOptimize); if (LastOptimizeTime <= timeReop) { LastOptimizeTime = timeNow; Print ("-------------------Start of optimization----------------------"); Print ("Old set:"); ArrayPrint (Set); Optimize (Set, Range_Min, Range_Step, Range_Max, InpBarsOptimize, InpPopSize, NumberFFlaunches, Spread * SymbolInfoDouble (_Symbol, SYMBOL_TRADE_TICK_SIZE)); Print ("New set:"); ArrayPrint (Set); IStoch.Init ((int)Set [0], 1, 3); } } //---------------------------------------------------------------------------- if (!SymbolInfoTick (_Symbol, Tick)) { Print ("Failed to get current symbol tick"); return; } //data preparation------------------------------------------------------------ MqlRates rates []; int dataCount = CopyRates (_Symbol, PERIOD_CURRENT, 0, (int)Set [0] + 1 + 3 + 1, rates); if (dataCount == -1) { Print ("Data get error"); return; } double hi []; double lo []; double cl []; ArrayResize (hi, dataCount); ArrayResize (lo, dataCount); ArrayResize (cl, dataCount); for (int i = 0; i < dataCount; i++) { hi [i] = rates [i].high; lo [i] = rates [i].low; cl [i] = rates [i].close; } int calc = IStoch.Calculate (dataCount, 0, hi, lo, cl); if (calc <= 0) return; double buff0 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 2]; double buff1 = IStoch.ExtMainBuffer [ArraySize (IStoch.ExtMainBuffer) - 3]; //---------------------------------------------------------------------------- // count open positions int cntBuy, cntSell; if (!CountOpenPositions (cntBuy, cntSell)) { Print ("Failed to count open positions"); return; } //---------------------------------------------------------------------------- // check for buy if (cntBuy == 0 && buff1 <= (100 - (int)Set [1]) && buff0 > (100 - (int)Set [1])) { ClosePositions (2); double sl = NP (Tick.bid - InpStopLoss * TickSize); double tp = NP (Tick.bid + InpTakeProfit * TickSize); Trade.PositionOpen (_Symbol, ORDER_TYPE_BUY, InpLotSize, Tick.ask, sl, tp, "Stochastic EA"); } //---------------------------------------------------------------------------- // check for sell if (cntSell == 0 && buff1 >= (int)Set [1] && buff0 < (int)Set [1]) { ClosePositions (1); double sl = NP (Tick.ask + InpStopLoss * TickSize); double tp = NP (Tick.ask - InpTakeProfit * TickSize); Trade.PositionOpen (_Symbol, ORDER_TYPE_SELL, InpLotSize, Tick.bid, sl, tp, "Stochastic EA"); } } //——————————————————————————————————————————————————————————————————————————————Аналогично, в функции "Optimize" выполняются те же действия, которые обычно наблюдаются в сценариях тестирования алгоритмов оптимизации в серии статей "Алгоритмы оптимизации популяции":
1. Инициализация алгоритма оптимизации.
2.1. Подготовка популяции.
2.2. Получение набора параметров из алгоритма оптимизации.
2.3. Вычисление функции пригодности с переданными ей параметрами.
2.4. Обновление наилучшего решения.
2.5. Получение наилучшего решения из алгоритма.
//—————————————————————————————————————————————————————————————————————————————— void Optimize (double &set [], double &range_min [], double &range_step [], double &range_max [], const int inpBarsOptimize, const int inpPopSize, const int numberFFlaunches, const double spread) { //---------------------------------------------------------------------------- double parametersSet []; ArrayResize(parametersSet, ArraySize(set)); //---------------------------------------------------------------------------- int epochCount = numberFFlaunches / inpPopSize; Init(inpPopSize, range_min, range_max, range_step); // Optimization------------------------------------------------------------- for (int epochCNT = 1; epochCNT <= epochCount && !IsStopped (); epochCNT++) { Preparation (); for (int set = 0; set < inpPopSize; set++) { GetVariantCalc (parametersSet, set); SetFitness (VirtualStrategy (parametersSet, inpBarsOptimize, spread), set); } Revision (); } Print ("Fitness: ", GetFitness (0)); GetVariant (parametersSet, 0); ArrayCopy (set, parametersSet, 0, 0, WHOLE_ARRAY); } //——————————————————————————————————————————————————————————————————————————————
Кроме того, функция "VirtualStrategy" проводит тестирование стратегии на исторических данных (на рисунке 1 она называется "EA Virt"). Для этого требуется массив параметров "set", количество баров для оптимизации "barsOptimize" и значение "spread".
Сначала выполняется этап подготовки данных. Исторические данные загружаются в массив "rates". Затем создаются массивы "hi", "lo" и "cl", необходимые для вычисления стохастика.
Далее инициализируется индикатор Stochastic, и выполняется его расчет на основе исторических данных. Если расчет завершается неудачей, функция возвращает "-DBL_MAX" (наихудшее возможное значение функции пригодности).
Впоследствии стратегия тестируется на исторических данных, следуя логике, идентичной коду основного советника. Для хранения сделок создается объект "сделки". Цикл повторяет исторические данные, где условия для открытия и закрытия позиций проверяются для каждого бара на основе значения индикатора и уровней "upLevel" и "dnLevel". Если условия выполняются, позиции открываются или закрываются.
После перебора исторических данных функция проверяет количество совершенных сделок. Если сделок совершено не было, функция возвращает "-DBL_MAX". В противном случае она возвращает окончательный баланс.
Возвращаемое значение "VirtualStrategy" представляет значение функции пригодности. В данном случае это конечный баланс в пунктах (как упоминалось ранее, функцией пригодности может быть баланс, коэффициент прибыли или любой другой показатель, указывающий качество результатов торговли на исторических данных).
Важно отметить, что виртуальная стратегия должна точно соответствовать стратегии советника. В этом примере торговля основана на ценах открытия, соответствующих элементу управления открытием бара в основном советнике. Если логика торговой стратегии работает на каждом тике, пользователю необходимо убедиться в доступности тиковых данных во время виртуального тестирования и соответствующим образом настроить функцию "VirtualStrategy".
//—————————————————————————————————————————————————————————————————————————————— double VirtualStrategy (double &set [], int barsOptimize, double spread) { //data preparation------------------------------------------------------------ MqlRates rates []; int dataCount = CopyRates(_Symbol, PERIOD_CURRENT, 0, barsOptimize + 1, rates); if (dataCount == -1) { Print ("Data get error"); return -DBL_MAX; } double hi []; double lo []; double cl []; ArrayResize (hi, dataCount); ArrayResize (lo, dataCount); ArrayResize (cl, dataCount); for (int i = 0; i < dataCount; i++) { hi [i] = rates [i].high; lo [i] = rates [i].low; cl [i] = rates [i].close; } C_iStochastic iStoch; iStoch.Init ((int)set [0], 1, 3); int calc = iStoch.Calculate (dataCount, 0, hi, lo, cl); if (calc <= 0) return -DBL_MAX; //============================================================================ //test of strategy on history------------------------------------------------- S_Deals deals; double iStMain0 = 0.0; double iStMain1 = 0.0; double upLevel = set [1]; double dnLevel = 100.0 - set [1]; double balance = 0.0; //running through history----------------------------------------------------- for (int i = 2; i < dataCount; i++) { if (i >= dataCount) { deals.ClosPos (-1, rates [i].open, spread); deals.ClosPos (1, rates [i].open, spread); break; } iStMain0 = iStoch.ExtMainBuffer [i - 1]; iStMain1 = iStoch.ExtMainBuffer [i - 2]; if (iStMain0 == 0.0 || iStMain1 == 0.0) continue; //buy------------------------------- if (iStMain1 <= dnLevel && dnLevel < iStMain0) { deals.ClosPos (-1, rates [i].open, spread); if (deals.GetBuys () == 0) deals.OpenPos (1, rates [i].open, spread); } //sell------------------------------ if (iStMain1 >= upLevel && upLevel > iStMain0) { deals.ClosPos (1, rates [i].open, spread); if (deals.GetSels () == 0) deals.OpenPos (-1, rates [i].open, spread); } } //---------------------------------------------------------------------------- if (deals.histSelsCNT + deals.histBuysCNT <= 0) return -DBL_MAX; return deals.balance; } //——————————————————————————————————————————————————————————————————————————————