Принципы экономного пересчета индикаторов
Введение
Тема экономии каких бы то ни было ресурсов и в каком бы то ни было сегменте практической деятельности человека - это, пожалуй, одна из самых актуальных и животрепещущих тем на пути к развитию и прогрессу. Программирование на MQL5 в этом плане не является исключением. Конечно, если диапазон решаемых задач ограничивать только визуальным трейдингом, то многие огрехи программирования можно попросту и не заметить.
Но вот всё, что касается автоматического трейдинга, изначально требует максимально экономного написания программного кода, в противном случае процессы тестирования и оптимизации торговых роботов могут растянуться настолько, что дождаться их окончания будет практически невозможно. Затея создания чего-либо дельного в подобной ситуации оказывается весьма и весьма сомнительной.
Так что прежде чем приступать к реализации торговых стратегий, не мешало бы более обстоятельно разобраться с теми подробностями программирования, которые в значительной степени влияют на время оптимизации и тестирования экспертов, а поскольку большинство экспертов содержат в своем, программном коде вызовы к пользовательским индикаторам, то, надо полагать, что именно с них и стоит начинать эту тему.
Наиболее актуальных моментов, которые необходимо учитывать при построении индикаторов, в общем-то, совсем немного, так что наиболее логичным будет просто сделать их обзор по порядку.
Пересчет на каждом тике индикатора только еще непосчитанных, вновь появившихся баров в классических индикаторах
Сама суть таких классических индикаторов, как RSI, ADX, ATR, CCI и т.п., такова, что на закрытых барах расчет этих индикаторов можно сделать всего один раз, а после этого производить расчет только на вновь появившихся барах. Исключение составляет только текущий незакрытый бар, на котором вычисления производятся многократно на каждом тике, пока этот бар не будет закрыт.
Самый простой способ понять, насколько актуально делать расчет индикаторов только на еще непосчитанных барах - это сравнить в Тестере стратегий результаты прогона таких (оптимизированных) индикаторов с индикаторами, пересчитывающимися на всех барах всё время (неоптимизированными).
Делается это весьма просто. Создается эксперт с пустыми функциями OnInit() и OnTick(). Всё, что необходимо доделать - это дописать вызов в эксперте необходимого варианта оптимизированного или неоптимизированного индикатора и полюбоваться на результаты прогонов в тестере таких экспертов в обоих случаях. Я в качестве примера возьму индикатор SMA.mq5 из своей статьи "Пользовательские индикаторы в MQL5 для начинающих", в котором сделаю замену строк
if (prev_calculated == 0) // если это первый старт, то делаем пересчёт всех, имеющихся баров first = MAPeriod - 1 + begin; else first = prev_calculated - 1; // на всех последующих стартах делаем расчёт только на вновь появившихся барах
на
first = MAPeriod - 1 + begin; // На всех тиках идёт пересчёт всех баров
В итоге у меня получится неоптимизированный вариант программного кода (SMA!!!!!!.mq5), который в отличие от оригинала будет пересчитывать все свои значения на каждом тике. Собственно говоря, варианты экспертного кода в обоих случаях практически одинаковые, так что я приведу всего один из них (SMA_Test.mq5)
//+------------------------------------------------------------------+ //| SMA_Test.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" int Handle; //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //----+ //----+ Получение хендла индикатора Handle = iCustom(Symbol(), 0, "SMA"); if (Handle == INVALID_HANDLE) Print(" Не удалось получить хендл индикатора SMA"); //----+ return(0); } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //----+ //--- Освобождаем хэндл индикатора IndicatorRelease(Handle); //----+ } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { //----+ double SMA[1]; //----+ Используя хендл индикатора, копируем значения индикаторного // буфера в специально подготовленный для этого статический массив CopyBuffer(Handle, 0, 0, 1, SMA); //----+ } //+------------------------------------------------------------------+
Можно приступать к тестам. Следует обратить внимание, что во всех тестах в данной статье мы будем использовать максимально приближенный к действительности режим моделирования изменений бара - "Каждый тик"!
Вот результат прогона в тестере оптимизированного индикатора:
Красным цветом выделено время, за которое прошел тест. Не так чтобы и много! А вот завершения теста индикатора SMA!!!!!!.mq5 пришлось ждать очень и очень долго!
Фактически время прохождения теста в данном случае отличается от предыдущего теста более чем в пятьсот раз. И это при том, что был выбран достаточно небольшой период тестирования. Но если при тестировании экспертов с такими вычислительными затратами можно смириться, то про оптимизацию их параметров лучше вообще забыть!
А стало быть, это самое красноречивое свидетельство того, что экономное написание кода - совсем не праздная забава для профи в области программирования, а вполне актуальный подход к написанию собственного программного кода.
В интернете существует целый сайт Overclockers.ru, посвященный разгону персонального компьютера с целью максимального повышения его производительности. Основными средством в этом направлении оказывается применение значительно более дорогих комплектующих компьютера для увеличения тактовой частоты процессора и оперативной памяти.
После этого для такого разогнанного процессора применяют более дорогие системы водяного охлаждения или даже погружение процессора в жидкий азот. Результатом подобных деяний оказывается двукратное или максимум трехкратное приращение производительности персонального компьютера.
Грамотное, экономное написание программного кода позволяет зачастую достичь несоизмеримо большего и при этом существенно меньшими усилиями. Конечно, таким способом Celleron300A в Core 2 Quad Q6600, пожалуй, не превратишь, но вот выжать из бюджетной серии персонального компьютера то, на что, казалось бы, способна только топовая модель, в определенных ситуациях очень даже и реально!
Многократный пересчет текущего незакрытого бара в некоторых не совсем классических индикаторах
И всё бы было просто здорово, если бы такой способ оптимизации программного кода годился для всех индикаторов без разбора! Но, увы, это далеко не так. Существует целая группа индикаторов, которые при таком подходе начинают нормально рассчитываться только один раз при загрузке индикатора на уже имеющихся исторических данных.
А на всех барах, появившихся после загрузки индикатора, его значения оказываются абсолютно некорректными. Основная причина, по которой это происходит, это зависимость некоторых переменных из кода этих индикаторов от тех значений, которые имели эти же переменные после расчета индикатора на предыдущем баре. Формально это будет выглядеть следующим образом:
SomeVariable(bar) = Function(SomeVariable(bar - 1))
где:
- SomeVariable() — значение некоторой переменной для какого-то бара;
- bar — номер бара, на котором производится расчет.
По вполне понятным причинам в реальном коде такие зависимости имеют менее явный, функциональный вид. Но сама суть от этого не меняется, например, для мувинга T3 (неоптимизированный индикатор - T3!!!!!!.mq5) актуальная для нас часть кода в индикаторе имеет следующий вид
e1 = w1 * series + w2 * e1;
e2 = w1 * e1 + w2 * e2;
e3 = w1 * e2 + w2 * e3;
e4 = w1 * e3 + w2 * e4;
e5 = w1 * e4 + w2 * e5;
e6 = w1 * e5 + w2 * e6;
//----
T3 = c1 * e6 + c2 * e5 + c3 * e4 + c4 * e3;
Переменные e1, e2, e3, e4, e5, e6 обладают именно таковой, функциональной зависимостью, которая предполагает использование этого кода для расчета на каждом новом баре всего один раз! Но текущий бар сквозь подобный расчет будет пропущен многократно до тех пор, пока не будет закрыт.
И значения этих переменных на текущем баре будут всё время изменяться, хотя и для текущего бара до его смены они должны оставаться такими, какими они оказались после расчета на предыдущем баре!
А стало быть, значения этих переменных на предыдущем баре (по отношению к текущему бару) следует запомнить в статических переменных и доставать их для многократного использования до очередной смены бара, на которой сделать очередное запоминание предпоследних значений переменных e1, e2, e3, e4, e5, e6.
Сам дополнительный код, который проделывает подобные манипуляции со значениями, довольно прост. Прежде всего, следует объявить локальные статические переменные для запоминания значений внутри функции OnCalculate()
//---- объявления статических переменных для хранения действительных значений коэффициентов static double e1_, e2_, e3_, e4_, e5_, e6_;
После этого сделать запоминание значений переменных в цикле на текущем баре до каких-либо расчетов в момент, когда количество вновь появившихся баров больше нуля:
//---- запоминаем значения переменных перед прогонами на текущем баре if (rates_total != prev_calculated && bar == rates_total - 1) { e1_ = e1; e2_ = e2; e3_ = e3; e4_ = e4; e5_ = e5; e6_ = e6; }
И до блока оператора цикла восстанавливать значения переменных обратным преобразованием:
//---- восстанавливаем значения переменных
e1 = e1_;
e2 = e2_;
e3 = e3_;
e4 = e4_;
e5 = e5_;
e6 = e6_;
Вполне естественно, что стартовую инициализацию расчетных коэффициентов теперь следует делать всего один раз, на первом старте функции OnCalculate(), причем теперь делается инициализация не самих коэффициентов, а соответствующих им статических переменных.
//---- расчёт стартового номера first для цикла пересчёта баров if (prev_calculated == 0) // проверка на первый старт расчёта индикатора { first = begin; // стартовый номер для расчёта всех баров //---- стартовая инициализация расчётных коэффициентов e1_ = price[first]; e2_ = price[first]; e3_ = price[first]; e4_ = price[first]; e5_ = price[first]; e6_ = price[first]; }
В результате итоговый индикатор T3.mq5 стал делать расчеты максимально экономичным образом. Всё бы ничего, но далеко не всегда подобные, функциональные зависимости можно так легко выявить. На такой случай можно значения всех переменных индикатора запоминать в статических переменных и восстанавливать аналогичным образом.
А уже после этого разбираться, какие из переменных действительно необходимо восстанавливать, а для каких нет такой необходимости. Для этого надо повесить на график неоптимизированный и оптимизированный варианты индикатора и сверять их работу, постепенно удаляя из списка восстановления по одной переменной. В итоге останутся только те переменные, которые действительно необходимо восстанавливать.
Вполне естественно, что я привел этот вариант логики работы с программным кодом нормальных индикаторов, у которых происходит пересчет текущего бара и вновь появившихся баров. Для перерисовывающихся и подсматривающих в будущее индикаторов создать какую-то аналогичную, совсем стандартную и простую методику оптимизации кода не получится по причине уникальных особенностей этих индикаторов. Да и большой надобности в этом, в общем-то, для большинства серьезных экспертописателей не возникает. Так что на этом подробный анализ подобных индикаторов можно считать законченным.
Особенности вызовов индикаторов, которые могут сделать MQL5-код непомерно заторможенным
Казалось бы, дело сделано, есть оптимизированный индикатор, который максимально экономно пересчитывает бары, и теперь достаточно настрочить несколько строчек кода, вызвав этот индикатор в коде эксперта или индикатора для получения рассчитанных им значений из его индикаторного буфера.
Но всё оказывается не так просто, как оно могло бы показаться, ежели подходить к этому делу чисто формально, не удосужившись один раз внимательно разобраться, а какие, собственно говоря, операции стоят за этими самыми несколькими строчками кода.
Сама специфика получения значений из пользовательских и технических индикаторов, как впрочем, и из таймсерий в MQL5 такова, что это делается через копирование данных в пользовательские массивы переменных. И накопировать таким образом можно огромное количество абсолютно ненужных для текущих расчетов данных.
Проще всего всё это проверить на конкретном получении данных из какого-нибудь технического индикатора. В качестве примера можно взять мувинг iAMA и построить на базе этого технического индикатора пользовательский индикатор AMkA.
Для копирования данных будем использовать первый вариант вызова функции CopyBuffer() с обращением по начальной позиции и количеству требуемых элементов для копирования. В индикаторе AMkA приращение мувинга на текущем баре обрабатывается с помощью технического индикатора Standard Deviation, и затем для получения торговых сигналов это приращение сравнивается с итоговым обработанным, среднеквадратичным значением.
Так что в простейшем случае для реализации индикатора AMkA сперва следует сделать индикатор, в котором в индикаторном буфере присутствовало бы значение приращения мувинга (индикатор dAMA). А потом в другом индикаторе, используя хендл индикатора с приращениями AMA, получим итоговое значение, обработанное индикатором Standard Deviation.
Сам процесс создания аналогичных индикаторов уже достаточно подробно обсуждался в статьях по подобной тематике, так что я его опускаю и подвергну анализу только подробности доступа к индикаторным буферам вызываемого индикатора в коде другого индикатора.
На бескрайних просторах интернета уже появились MQL5-образцы народного творчества, в коих их авторы, не мудрствуя лукаво, на каждом тике индикатора копируют буквально всё содержимое буферов вызываемого индикатора в промежуточные динамические массивы. А после этого из таких промежуточных массивов все значения по одному с помощью оператора цикла перекладываются в итоговые индикаторные буферы.
Для решения нашей задачи подобный подход выглядит максималистки просто
if (CopyBuffer(AMA_Handle, 0, 0, rates_total, Array) <= 0) return(0); ArrayCopy(AMA_Buffer, Array, 0, 0, WHOLE_ARRAY);
(Индикатор dAMA!!!!!!.mq5) Или вот так
if (CopyBuffer(AMA_Handle, 0, 0, rates_total, Array) <= 0) return(0); for(bar = 0; bar < rates_total; bar++) { AMA_Buffer[bar] = Array[bar]; /* здесь код индикаторных вычислений */ }
Но вот какова цена за подобное незатейливое решение? Сперва не мешало бы немного разобраться, а как следовало бы поступить максимально рациональным способом. Во-первых, какой-то серьезно обоснованной необходимости для использования промежуточного массива Array[] нет, и данные лучше копировать напрямую в индикаторный буфер AMA[].
Во-вторых, на каждом тике индикатора необходимо копировать значения только в трех случаях:
- со вновь появившихся баров,
- с закрытых баров,
- с текущего незакрытого баров.
Остальные значения в индикаторном буфере и так уже есть в наличии и какого-то приращения логического смысла от их многократного переписывания не добавится нисколько!
//--- расчет необходимого количества копируемых данных int to_copy; if(prev_calculated > rates_total || prev_calculated <= 0)// проверка на первый старт расчёта индикатора to_copy = rates_total - begin; // расчётное количество всех баров else to_copy = rates_total - prev_calculated + 1; // расчетное количество только новых баров //--- копируем вновь появившиеся данные в индикаторный буфер AMA_Buffer[] if (CopyBuffer(AMA_Handle, 0, 0, to_copy, AMA_Buffer) <= 0) return(0);
Вполне естественно, что итоговый код в этом случае будет несколько сложнее (Индикатор dAMA.mq5) , но теперь мы можем по предложенной мной в начале статьи методике провести тесты в обоих случаях и сделать соответствующие выводы. В этот раз увеличим период тестирования до года
В итоге после прохождения теста в журнале тестера стратегий находим необходимый нам результат времени прохождения теста эксперта dAMA_Test
Результат времени прохождения теста в 43937 ms находится в пределах логики здравого смысла! Чего, к сожалению, нельзя сказать про аналогичный результат времени прохождения теста экспертом dAMA!!!!!!_Test
Время прохождения теста 960625 ms оказывается фактически в двадцать раз больше, чем в предыдущем случае. Вывод напрашивается сам собой. Программный код следует писать максимально экономным образом, чтобы он не делал ни одного лишнего вычисления!
Сам индикатор AMkA, построенный на вышеизложенных принципах с точки зрения того, что я изложил выше, ничего нового не имеет, так что я остановлюсь только немного на деталях копирования данных в данном случае.
//---- объявления локальных массивов double dAMA_Array[], StdDev_Array[]; //---- индексация элементов в массивах как в таймсериях ArraySetAsSeries(dAMA_Array, true); ArraySetAsSeries(StdDev_Array, true); //--- расчёт количества копируемых данных int to_copy; if(prev_calculated > rates_total || prev_calculated <= 0)// проверка на первый старт расчёта индикатора to_copy = rates_total - begin; // расчётное количество всех баров else to_copy = rates_total - prev_calculated + 1; // расчётное количество только новых баров //--- копируем вновь появившиеся данные в индикаторный буфер и локальные, динамические массивы if(CopyBuffer(dAMAHandle, 1, 0, to_copy, AMABuffer ) <= 0) return(0); if(CopyBuffer(dAMAHandle, 0, 0, to_copy, dAMA_Array ) <= 0) return(0); if(CopyBuffer(StdDevHandle, 0, 0, to_copy, StdDev_Array) <= 0) return(0);
Всё делается абсолютно аналогичным образом, разве что только теперь данные копируются в один индикаторный буфер и два локально объявленных динамических массива для промежуточных вычислений.
Реализация всех индикаторных расчетов внутри индикатора, как один из способов его оптимизации
Всё это, конечно, интересно, но уж больно подозрительно выглядит такая, многоэтажная конструкция из последовательных вызовов пользовательских и технических индикаторов. И ее тоже следовало бы как-нибудь измерить. Но для этого не мешало бы иметь код индикатора AMkA, который непосредственно весь находился бы внутри пользовательского индикатора и никаких вызовов других индикаторов бы не использовал.
Для программиста, досконально разобравшегося с написанием индикаторов на MQL5, эта задача особых затруднений не вызывает. Сперва пишется код пользовательского индикатора AMA.mq5, а после этого в него добавляются необходимые элементы кода для реализации индикатора AMkA_.mq5. Появляется еще один, второй, большой цикл расчета индикатора, в котором в пользовательский массив загружаются приращения индикатора AMA,
//---- основной цикл расчёта индикатора AMkA for(bar = first; bar < rates_total; bar++) { //---- загружаем приращения индикатора AMA в массив для промежуточных вычислений for(iii = 0; iii < ama_period; iii++) dAMA[iii] = AMABuffer[bar - iii - 0] - AMABuffer[bar - iii - 1];
затем с этим массивом проделывают операции аналогичные вычислению технического индикатора StDev на данных индикатора dAMA.mq5.
//---- находим простое среднее приращений AMA Sum = 0.0; for(iii = 0; iii < ama_period; iii++) Sum += dAMA[iii]; SMAdif = Sum / ama_period; //---- находим сумму квадратов разностей приращений и среднего Sum = 0.0; for(iii = 0; iii < ama_period; iii++) Sum += MathPow(dAMA[iii] - SMAdif, 2); //---- определяем итоговое значение среднеквадратичного отклонения StDev от приращения AMA StDev = MathSqrt(Sum / ama_period);
Остаток кода абсолютно аналогичен коду индикатора AMkA.mq5 и особого интереса не представляет. Теперь можно начинать тесты индикаторов AMkA_.mq5 и AMkA.mq5 с помощью экспертов AMkA__Test.mq5 и AMkA_Test.mq5.
С тестом индикатора AMkA_.mq5 никаких недоразумений не возникло, и время тестирования вполне укладывается в приемлемые рамки
А вот индикатор AMkA.mq5 к своему финишу приполз как черепаха!
Его результат оказался аж в семь раз хуже, чем его, в общем-то, молочного брата-близнеца с более интеллектуальной начинкой! Какие еще могут быть комментарии? Вывод напрашивается сам собой, что городить такие конструкции, состоящие из нескольких последовательных вызовов индикаторов друг из друга не очень-то благоразумно и годится только для предварительных тестов!
Разумеется, эти результаты получены пока на тестовой версии клиентского терминала, а как всё это будет выглядеть в дальнейшем, сейчас сказать трудно. Но вот что касается грядущего Чемпионата торговых роботов, можно утверждать вполне определенно - это актуальная и довольно злободневная тема!
Некоторые особенности вызова индикаторов из экспертов
Всё, что касается оптимизации доступа к данным пользовательских и технических индикаторов в программном коде индикаторов, абсолютно справедливо и для оптимизации доступа к данным пользовательских и технических индикаторов в программном коде экспертов. Помимо уже представленных ситуаций в экспертном коде имеется еще один момент, который может достаточно сильно повлиять на время тестирования и оптимизации торговых систем.
Дело в том, что огромное количество экспертов обычно обрабатывает данные индикаторов только на смене бара, и по этой причине в этих экспертах нет никакой необходимости дергать функцию CopyBuffer() на каждом тике.
В такой ситуации в экспертах возникает потребность доставать данные из индикаторных буферов только при смене бара. Таким образом, вызовы индикаторов должны располагаться в блоке за фигурными скобками, доступ внутрь которых разрешается только один раз на каждой смене бара, если все необходимые данные из индикаторных буферов при этом успешно копируются в массивы для промежуточных вычислений.
Лучше всего в качестве такого фильтра справляется пользовательская функция с возвратом логической единицы в момент времени, когда произошла смена текущего бара. В файле IsNewBar.mqh представлен мой достаточно универсальный вариант такой функции:
bool IsNewBar ( int Number, // Номер вызова в функции IsNewBar в программном коде эксперта string symbol, // Символ графика, на котором делается расчёт данных ENUM_TIMEFRAMES timeframe // Таймфрейм графика, на котором делается расчёт данных )
При использовании такой функции в экспертном коде это может выглядеть, например, вот так:
//---- объявление статической переменной - массива для хранения значений индикатора AMA static double AMA_Array[3]; //---- вызов индикатора AMA для копирования его значений в массив AMA_Array if (IsNewBar(0, Symbol(), 0)) { CopyBuffer(AMA_Handle, 0, 1, 3, AMA_Array); }
Но гораздо рациональнее в этом случае поступить немного иначе. Дело в том, что при вызове функции CopyBuffer() данные в массив AMA_Array[] могут быть не скопированы, и в подобной ситуации следует делать вызов этой функции на каждом тике до тех пор, пока не будет удачного варианта копирования данных, что реализуется некоторым усложнением этого фильтра
//---- объявление статической переменной - массива для хранения значений индикатора AMA static double AMA_Array[3]; //---- объявление статической переменной для хранения результата копирования данных из индикатора AMA static bool Recount; //---- вызов индикатора AMA для копирования его значений в массив AMA_Array if (IsNewBar(0, Symbol(), 0) || Recount) { if (CopyBuffer(AMA_Handle, 0, 1, 3, AMA_Array) < 0) { Recount = true; // попытка копирования данных закончилась неудачно return; // выход из функции OnTick() } //---- Все операции копирования из индикаторных буферов завершены успешно // можно не возвращаться в этот блок до очередной смены бара Recount = false; }
Теперь, когда детали рационального вызова функции копирования индикаторных значений в экспертном коде стали понятны, можно тестировать преимущества применения функции IsNewBar() в экспертах.
Итак, у нас в наличии два варианта эксперта для испытаний в тестере стратегий, первый - AMA_Test.ex5. Он копирует данные из индикаторного буфера на каждом тике.
Второй - IsNewBar_AMA_Test.mq5 копирует данные только на смене бара.
Да! Результаты тестов несколько обескураживающие. Получается так, что вызывать функцию IsNewBar() на каждом тике куда более накладно, чем делать копирование данных в три ячейки пользовательского массива!
Здесь мне бы хотелось обратить внимание еще на одну немаловажную, но с виду неприметную деталь работы индикатора. Всё дело в том, что если мы получили хендл индикатора в функции OnInit(), то независимо от того, копируем мы или вообще не копируем данные из этого индикатора внутри функции OnTick(), его расчет на еще непосчитанных и текущем баре все равно делается на каждом тике.
Так что если нам в эксперте не нужны посчитанные индикаторные значения с текущего незакрытого бара, то лучше в целях экономии времени расчет этих значений отключить. Это делается довольно просто - уменьшается на единицу правая граница основного цикла пересчета баров в индикаторе. До изменения этот цикл в индикаторе AMA.mq5 выглядел вот так
//---- основной цикл расчёта индикатора for(bar = first; bar < rates_total; bar++)
А после изменения он будет таким
//---- основной цикл расчёта индикатора for(bar = first; bar < rates_total - 1; bar++)
Индикатор AMA_Ex.mq5. Теперь можно протестировать и этот индикатор (эксперт AMA_Ex_Test.mq5)
Конечно, данный результат лучше теста индикатора AMA на 21%, что, вообще-то, неплохо, но по логике вещей этот результат мог бы быть и значительно лучше.
Заключение
В конечном итоге экономичность программного кода - это такой же достаточно объективный параметр, который можно измерить, подвергнуть логическому анализу и в определенных ситуациях значительно увеличить. Сами методы, посредством которых это делается, не так чтобы и очень сложны, так что дело за малым - просто иметь некоторое терпение и делать не только то, от чего напрямую зависят показатели прибыльности автоматизируемой торговой системы.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
К счастью, ошибка создания индикатора по вине терминала практически не происходит.
Это слишком сильное утверждение. Всегда надеться какой нибудь криворукий Привал, который по незнанию, так замутит, что мама не горюй :-).
вернёмся к баран
существует генетический алгоритм который изменяет значение скользящей средней
притом переменная изменятся в процессе работы индикатора.
как правильней инициализировать технический индикатор
и нельзя делать выкид из цикла он калькулейт должен завершится до конца
подскажите почему везде происходит вот такая проверка
интерисует условие prev_calculated > rates_total ( обработано баров на предыдущем вызове больше чем размер входной таймсерии)
действительно ли такое может быть (что это за ситуация) ? просто я на эти грабли еще не наступал