Функция OrderSend() в MQL-4 является , пожалуй, самая распространенная, так именно с ее помощью выставляются отложенные ордера и открываются ордера по рынку. Рассмотрим применение этой функции на примере скрипта, который будет разворачивать открытую позицию по конкретному инструменту. Предположим, у нас имеются одна или несколько открытых позиций по EURUSD в одну сторону, в покупку. Происходит резкое движение цены вверх, цена достигает целевого уровня (по нашим предположениям), на котором необходимо фиксировать прибыль (закрывать позиции), и еще желательно открыть позицию в том же объеме в обратном направлении, на продажу. В условиях быстрого рынка, когда цена меняется достаточно быстро, проделать такую операцию вручную быстро не получится. Нам требуется:
Закрыть последовательно одну или несколько ордеров в покупку.
Открыть один ордер в продажу с таким же суммарным объемом.
MQL-4 и MetaTrader4 позволяют все эти операции провести одним действием. Дело в том, что в терминале МТ4 разрешается закрывать одну открытую позицию противоположной открытой позицией, то есть ордера в покупку закрывать ордерами в продажу. При этом дополнительно получается экономия в размере спреда. Если у нас есть ордер EURUSD buy по цене 1.2000 объемом 0.1 лота и ордер EURUSD sell по цене 1.2500 объемом 0.1 лота( и больше открытых ордеров по EURUSD нет), то независимо от того, где находится в данный момент цена, общая текущая прибыль по этим двум ордерам будет составлять 500 пунктов (Point=0.001). Свопы, начисляемые по этим двум ордерам в итоге будут отрицательными, но для нашего случая это не имеет значения. В МТ4 разрешено закрыть ордер в покупку, открытый по цене 1.2000, ордером в продажу по цене продажи (1.2500). Разумеется, объемы взаимозакрываемых ордеров должны быть равны.
Допустим, в данный момент Bid по EURUSD равен 1.2600. Если закроем каждый из двух ордеров независимо, то получим: buy 1.2000 закрываем по 1.2600 (+600 пунктов) и sell 1.2500 кроем по 1.2603 (Bid + спред 3 пункта) – итого имеем минус 103 пункта. В сумме прибыль 600-103=497 пунктов. Предполагается, что цена между двумя закрытиями не менялась. Если бы мы закрывали один ордер другим, то получили бы 500 пунктов (1.2500-1.2000). Если объемы встречных ордеров не совпадают, то полностью закрывается ордер с меньшим объемом и остается один ордер с объемом равным разнице между взаимозакрываемыми ордерами. То есть, в случае 0.2 buy и 0.1 sell , после частичного закрытия остался бы ордер 0.1 buy по цене покупки. Вот это свойство мы и используем в разворотном скрипте Revers.mq4. Мы проверим наличие ордеров, открытых только в одну сторону, подсчитаем суммарный объем открытой позиции по этому инструменту и откроем удвоенный объем в противоположную сторону. Таким образом, после работы скрипта у нас будут ордер(а) с объемом V1 и ордер в противоположную сторону с объемом V2=2*V1. Затем мы сможем вручную закрыть необходимые ордера и оставить только один ордер с объемом V2-V1=2*V1-V1=V1.
Построим скрипт по принципу преодоления препятствий. Если на каком-то этапе условия не складываются – скрипт досрочно прекращает работу. Первым делом(как обычно) встраиваем защиту от реала:
Если скрипт окажется запущенным на реальном счете, то появится предупреждение и скрипт закончит работу. Далее проверяем наличие ордеров:
Нет ордеров ( OrdersTotal() равно нулю) – нет смысла продолжать работу, выход. Но даже, если ордера и есть, необходимо убедиться, что присутствуют ордера на нашем символе (график инструмента, на котором мы запускаем скрипт), и эти ордера являются открытыми, а не отложенными. Для этого пройдем в цикле по списку и посчитаем ордера по нашему инструменту. Заодно посчитаем и объемы ордеров в покупку и в продажу, по отдельности.
Видно, что если ордер открыт не по нашему символу (OrderSymbol()!=Symbol()), работа с ордером прекращается и происходит переход к следующему по списку ордеру (оператор continue;) . После прохода по списку ордеров, можно по очереди проверить отдельно наличие ордеров в покупку и ордеров в продажу, но можно проверить оба счетчика одной операцией – сложив счетчики. Если сумма равна нулю – значит открытых ордеров нет, есть только отложенные.
Такой вариант нам тоже не подходит, выходим. Следующая проверка – нам нужно, чтобы были только ордера в покупку, либо только в продажу. Мы хотим иметь однозначно определенную ситуацию. Я проверил это простым умножением счетчиков, если произведение равно нулю – значит имеем только ордера одного вида.
Если скрипт после всех этих проверок еще не завершил работу – значит наступило время открытия встречного ордера с удвоенным размером.
Получение удвоенного ордера сделано не напрямую, то есть, вместо того, чтобы написать reversLots=2*sellLots (для ордеров, открытых в покупку) , была использована сложная конструкция. Для чего это сделано? Размер позиции имеет тип double, и не всегда в компьютерных расчетах 2.0 умножит на 2.0 равно 4.0. Это связано с ошибками округления не целочисленных переменных в двоичной системе счисления. Хотя 2 умножить на 2 всегда равно 4. Поэтому, сначала мы умножаем buyLots на 10 (без точки) и удваиваем полученный целочисленный результат. Например, 0.2(лот)*10*2=4. Получили целочисленный результат четыре. Теперь нам необходимо получить размер reversLots(0.4) в 10 раз меньший с точностью до одного знака до запятой. Для этого мы делим intLots (целые лоты) типа int на 10.0 типа double (обратите внимание на точку) и нормализуем (усекаем) результат до точности в один знак после запятой. В итоге получаем значение 0.4 с необходимой точностью.
Если бы мы разделили 4 на 10 (4/10) , то, так как оба числа являются целыми(int), результат деления вычислялся бы по правилам целочисленной арифметики. Так как 4(целое) на 10 (целое) не делится, то ответ был бы ноль. Это одна из самых распространенных ошибок, нельзя забывать разницу между целочисленным делением и делением вещественных чисел в компьютерных вычислениях. Если вспомнить случай с Буратино, то его не сильно обманывали, когда говорили, что «5 на 3 не делится, вот тебе одна монета».
Итак, размер разворотного ордера вычислен, осталось только отправить торговую команду на сервер. Иногда цены двигаются настолько быстро, что скрипт выполнял вычисления, текущие цены уже совсем не такие как на момент запуска скрипта. Необходимо всегда иметь самые свежие цены, чтобы обезопасить себя от отказа сервера по причине устаревания цены. Для этой цели служит команда RefreshRates(), рекомендую почитать справку по ней. При выполнении этой команды не происходит обращения к торговому серверу брокера, поэтому ее можно использовать так часто, как вам это необходимо, не боясь нарваться на санкции брокера. Наконец, можно отправить торговый приказ OrderSend(). Хотя справка по этой функции содержит всю необходимую информацию, должен выделить две основные ошибки при ее использовании:
часто забывают указать символ, по которому открывается ордер. То есть, просто пропускают первый параметр этой функции (Symbol()), а ведь в MQL-4 можно открывать ордера по «чужому» инструменту (в тестере нельзя). Компилятор эту ошибку прощает и ничего не сообщает, потом пользователь удивляется тому, что ордера не открываются .
при открытии ордера типа OP_BUY(покупка по рынку) подставляют в качестве цены открытия Bid(цена продажи), для продажи – наоборот. Естественно, ордер вряд ли будет открыт.
Кроме того, желательно параметр Slippage (проскальзывание) не ставить равным нулю, так как повышается вероятность отказа из-за изменения цены - пока ваш торговый приказ дойдет до обработки торговым сервером, цена может измениться, нулевой параметр проскальзывания будет воспринят сервером как отказ от совершения сделки по несколько иной цене и ордер не пройдет.
На самом деле количество ошибок, возвращаемых терминалом или торговым сервером весьма велико, эти ошибки помогают перенаправить алгоритм в нужное русло. Для того, чтобы узнать код возвращенной ошибки, существует функция GetLastError(). Необходимо опрашивать этой функцией код ошибки каждый раз, когда мы получили ошибочную ситуацию. Так как, в случае неудачи OrderSend() возвращает значение -1, то мы выводим эту ошибку в том случае, если ticket меньше нуля (можно было проверить и ticket==-1). Если ордер открылся нормально, сообщение об ошибке мы не увидим. Скрипт готов, я запускаю его на графике USDCHF, по которому был открыт ордер в покупку в размере 0.1 лота.
Видно, что скрипт открыл противоположный ордер в двойном размере.
В комментарии ордера указано «revers order» - наша метка. Теперь мы можем в любой момент кликнуть на один из этих ордеров, выбрать тип «Закрыть перекрытые ордера»,осталось только нажать желтую кнопку.
На этом первое знакомство с функцией OrderSend() для ордеров, открываемых по рынку, можно считать законченным. Скрипт по статье можно скачать здесь .
У нас уже есть почти все необходимые знания для создания советника. Из торговых функций мы не затрагивали только OrderModify() и OrderCloseBy(). Но прежде чем приступать к написанию сложных советников, необходимо четко представлять себе как работает советник в режиме реального времени, будучи прикреплен к графику инструмента, и как моделируется его работа в тестере при бектесте на исторических данных.
Многие трейдеры пишут индикаторы и советники, не представляя разницы между теорией и практикой. Прогнав советник в тестере и получив заманчивые результаты, они вешают эти советники на онлайновый график, часто позже удивляются несовпадению результатов (иногда разительному) и значениям индикаторов, снятых в онлайне и при бектесте. Поэтому, мы для начала создадим советник, за который нам не придется краснеть, и который одинаково хорошо работает на любом инструменте и на любом тайм-фрейме. Этот советник не сливает, потому что он не будет торговать. Единственное его назначение — вести запись о состоянии счета, поэтому я его назвал BlackBox («Черный ящик»), по аналогии с аппаратурой для записи полета авиалайнера.
Трейдеры придумывают сложные алгоритмы для открытия позиций, еще более сложные алгоритмы для трейлинга(подтягивания) стоплоссов (Stop Loss), проверяют значения индикаторов на «родном» и «чужом» тайм-фрейме, считают количество прибыльных и убыточных сделок в последней серии, в общем, отслеживают самые разнообразные события, которые могут произойти с ценой и с торговым счетом. Но при этом иногда забывают про два самых важных события.
Первое событие — это приход тика, именно по приходу нового тика выполняется каждый раз функция start() советника и индикатора. Это событие очевидно. Второе событие — это закрытие старого бара и зарождение нового. Эти два события между собой не равнозначны с точки зрения написания советника. Очевидно, что значение индикатора меняется в незавершенном баре с каждым изменением цены, поэтому большинство стратегий используют значения с последнего завершенного бара, то есть с первого бара.
Необходимо определить момент, когда старый бар закрылся, и пришел первый тик нового (нулевого) бара. В этот момент мы производим оценку текущей ситуации согласно алгоритму советника и принимаем решение о проведении или непроведении торговых операций. Это событие (рождение нового бара) мы будем проверять пользовательской функцией isNewBar() булевого типа, она будет возвращать true, если зародился новый бар, и false в противном случае. Если новый бар не появился — проводятся рядовые действия, которые можно делать на каждом новом тике.
Создаем новый советник с помощью «Мастера создания Советника». Никаких параметров для своей работы он не требует, поэтому заполняем только имя советника.
В блоке start() пишем несколько строчек простого алгоритма:
Видно, что на каждом тике проверяется факт появления нового бара и в случае положительного значения вызывается функция EveryBar(). Эта функция вызывается только один раз в начале каждого бара (если правильно написана isNewBar()) и в течение всего развития бара больше не вызывается. Кроме того, на каждом тике вызывается функция EveryTick() — в ней могут выполняться любые действия. Как нам определить появление нового бара? Существуют два стандартных способа.
Каждый бар имеет такую характеристику как Time[] — время отсчета(открытия) нового бара , и если мы будем каждый раз спрашивать значение Time[0] и сравнивать его с тем, что было на предыдущем тике, то в момент появления нового бара эти значения не совпадут.
Второй вариант — запоминать каждый раз значение предопределенной переменной Bars. При появлении нового бара это значение также изменится.
Оба этих варианта равноправны, но я рассмотрю второй. Пишем функцию:
Обратите внимание, что переменная expertBars (в которой мы запоминаем значение Bars) является глобальной. Только переменные на глобальном уровне помнят свои значения между приходами каждого тика и запуска блока start() (из которого функция isNewBar() вызывается). Если выражение expertBars!=Bars истинно, значит родился новый бар, установим значение res равным true и не забудем запомнить значение Bars на данный момент — в следующий раз в будущем мы будем сравнивать Bars с текущим значением. Таким образом, возвращаемое функцией значение res будет либо true либо false.
Идем далее, сочиняем функцию EveryBar(), чтобы долго не думать, я решил каждый новый бар распечатывать в логе список ордеров, как в скрипте OrderList. mq4 (статья «Ордера в MetaTrader4» . ).
Для этого из функции EveryBar() вызывается новая функция — PrintAllOrders() (распечатать все ордера).
Осталось написать только функцию EveryTick(). Я решил в этой функции вычислять средний профит за все тики, пришедшие в течение бара. Для этого на каждом тике суммируется текущая прибыль по счету (AccountProfit()), подсчитывается количество тиков, и при появлении нового бара сумма профитов делится на количество поступивших тиков, получаем в результате усредненную прибыль за бар.
Немного изменим функцию EveryBar() для вычисления средней прибыли.
Теперь мы с появлением нового бара вычисляем среднюю прибыль и обнуляем переменные SumProfit и ticksBars для дальнейшего из использования с чистого листа на новом баре. Запускаем на графике EURUSD M15 и видим вывод в лог:
Теперь наш советник будет каждые 15 минут выводить данные об ордерах и среднюю прибыль за этот период.
Думаю, логика работы этого советника не вызывает вопросов. Взять код советника можно здесь .
Думаю, большинство видело тиковый график в «Обзоре рынка» в терминале МТ4. Также он доступен при ручном выставлении ордеров.
Многие трейдеры по достоинству оценили появление этого вида графика в МТ4 (в МТ3 его не было). Но к хорошему привыкаешь быстро, и уже многим не хватает этого, было много просьб добавить такой вид графика в отдельном окне. Мы попробуем создать тиковый график самостоятельно. Размещать такой график в окне цен не очень удобно, так как будет не совпадать масштаб, поэтому мы его создадим в подокне основного графика. Чтобы добиться внешнего сходства, выберем тип отображения в виде линии. Запускаем «Мастера создания Советника», называем Ticks, внешних параметров нет, на втором шаге ставим чек-бокс «Индикатор в отдельном окне», добавляем индекс типа Line ( цвет я поставил Navy ) , жмем «Готово».
Наша задача крайне проста: приходит новый тик — вызывается функция start(), в этой функции в нулевом баре ставим значение Bid. Пришло 100 тиков — имеем 100 заполненных значений. Единственная проблема — просто так у нас это не получится. Если мы запускаем индикатор на 5 минутном тайм-фрейме, то новый бар у нас будет появляться только каждые 5 минут, а нам необходимо, чтобы на каждый тик индикатор сдвигался влево.
Если для стандартных индикаторов(как встроенных, так и пользовательских) терминал сам производит нужный сдвиг, то для тикового графика нам самим придется продумать механизм сдвига. По сути, мы имеем индикаторный массив ExtMapBuffer1[], значения которого нам необходимо сдвигать вглубь, значение ExtMapBuffer1[0] переносим в ExtMapBuffer1[1], ExtMapBuffer1[1] переносим в ExtMapBuffer1[2], и так далее, в освободившийся индекс ExtMapBuffer1[0] записываем значение Bid.
Таким образом, нужна некая функция ShiftArray() (сдвинуть массив), после которой пишем по нулевому адресу Bid.
Осталось написать функцию ShiftArray() :
Нам пришлось ввести новую глобальную переменную tickCounter, чтобы знать на какую глубину сдвигать значения массива. Индикатор практически готов. Но мы забыли, что при появлении нового бара индикаторный массив сдвигается автоматически. Добавим обработку события «Новый бар» и изменим код:
Функция isNewBar() аналогична той, что используется в советнике BlackBox, просто скопируем ее. Правда, вместо глобальной переменной expertBars я написал myBars.
Теперь индикатор безупречен. Правда, мы не использовали возможности оптимизации, но как можно оптимизировать этот индикатор? Это редкий случай, когда нет никакой пользы от встроенной функции IndicatorCounted() . Но оптимизировать можно и его. Предположим, мы запустили индикатор минут 10 назад, за это время набежало 100 тиков, значит, на каждом новом тике мы сдвигаем 100 значений массива, и при этом количество сдвигаемых значений растет. А спустя сутки этих значений будет тысячи. Помножим это на количество графиков, на которые мы набросим этот индикатор. Возможно, наступит момент, когда все ресурсы компьютера будут уходить на бессмысленный сдвиг десятков тысяч значений в разных массивах. Чтобы этого избежать, я ввел внешний параметр MaxDrawTicks=500 (максимальное число рисуемых тиков) и ввел правило: как только счетчик тиков tickCounter в два раз превысит это значение — происходит «обрезка» массива, значения от MaxDrawTicks+1 до 2*MaxDrawTicks заполняются нулями. Дальше как обычно. Я исходил из того, что пятисот значений на графике обычно достаточно для оценки текущей ситуации, вы можете изменить это число или изменить правило «обрезки».
Индикатор написан без ошибок, оптимизация проведена, что еще нужно для счастья? В терминале, если нажать Ctrl+Y , появляются разделители периодов. Почему бы и нам не сделать подобное. И тогда мы увидим, что графики имеют свое собственное время, которое не совпадает с астрономическим. И по законам внутреннего времени также развиваются периоды тренда, которые сменяются рейнджем или консолидацией. Да и понять по разделителям будет проще- как себя вела цена внутри того или иного бара, как менялись тики в модели флаг или ложный пробой. Разделители рисовать мы сможем только с помощью объектов, это объект «Вертикальная линия». Теперь при наступлении события «Новый бар» мы рисуем вертикальную линию с уникальным именем, реализуем это функцией SetDelimeter() (установить разделитель). Блок start() окончательно выглядит так:
Функция SetDelimeter() будет создавать объект типа OBJ_VLINE, размещать его в окне индикатора, задавать цвет линии и стиль. Вы можете задать свои параметры цвета и стиля. Вопрос уникальности создаваемых имен решен просто — каждый новый разделитель имеет имя delimeterDate, которое содержит в себе текстовый формат начала нового бара. Таким образом, мы всегда можем подвести курсор к нашему разделителю, и понять — в какой период времени развивалась данная последовательность тиков.
Посмотрев справку в МЕ, вы поймете полностью все тонкости использованных функций для работы с объектом. Осталась последняя деталь — мы породили объекты , теперь нам нужно не забыть двигать их по той же причине, что и индикаторный массив. Немного расширим функцию ShiftArray(), теперь она не только сдвигает индикаторный массив, но и двигает все вертикальные линии, которые найдет на окне индикатора.
Изучив код этого индикатора, вы сможете легко создавать и менять свойства вертикальных линий. На этом можно поставить окончательную точку в написании этого индикатора. Хотя, при желании, вы можете его улучшить и добавить какую-то свою функциональность. Запускаем его на графике EURUSD M5 и начинаем наблюдать.
Я подвел курсор к разделителю и увидел имя вертикальной линии(которое нам дает Time[] этого отрезка времени ).
Видно, что на тиковом графике хорошо видны линии поддержки и сопротивления (1.2644).
Видим, как цена пробила линию поддержки и вновь вернулась вверх. Спустя 10 минут цена все-таки пробивает линию поддержки 1.2620 . Мы можем нанести горизонтальную линию на наш индикатор.
С помощью нашего индикатора мы можем изучить развитие цены в каждый момент времени, не находясь физически в этот момент перед монитором. Думаю, те, кто торгует внутри дня, по достоинству оценят этот индикатор. Скачать индикатор можно здесь .
« - А где же моя взятка на козырного туза?! - Расклад, батенька, расклад » Из разбора партии в преферанс.
В данный момент имеем:
советник BlackBox, который умеет отслеживать два события;
индикатор Ticks, который отражает тиковый график.
Хотелось бы — чтобы советник умел определять факт открытия и закрытия ордера по инструменту. Ордер может открываться вручную, может срабатывать отложенный ордер и может открываться каким-то другим советником, для нас это не имеет значения. Закрываться может также по достижению Take Profit, срабатыванию Stop Loss, просто закрытие по рынку вручную или советником. Хорошо бы все это записывать в лог-файл или отдельный файл. А сохранять скриншот (снимок экрана) таких моментов — было бы совсем здорово.
Год назад записать такие моменты можно было бы только находясь перед монитором, или создав сложную конструкцию из нескольких программ. Но даже и в этом случае успех был не гарантирован. Человек не может постоянно отслеживать несколько графиков одновременно, тем более 24 часа в сутки. А программы видео-грабберы могут работать только с одним монитором, а значит на каждый график потребовался бы один монитор.
Но теперь в МТ4 есть такая функция — ScreenShot(), которую я и назвал замечательной функцией.
Сколько раз любой трейдер открывал ордер с выставленным Stop Loss, отходил от терминала на какое-то время, а, вернувшись, обнаруживал, что цена коснулась уровня стопа и ушла в нужном направлении? Или наоборот, открытый ордер достигал какого-то уровня минимальной прибыли, после этого цена возвращалась к уровню открытия, и трейдер не мог решиться закрыть сделку. Чаще всего, в этом случае цена в дальнейшем развивалась в неблагоприятном направлении, но как отличать эти два момента? Или третий вариант — в онлайне торгует советник, алгоритм выходов которого хотелось бы улучшить после наблюдения сделок. В общем, во всех этих случаях хотелось бы сохранять видеоряд, галерею картинок, просматривать которые можно было бы попозже в режиме слайд-шоу.
Логично совместить работу советника BlackBox c записью таких картинок, да еще при этом отслеживать состояние нужных нам индикаторов. Сохраним советника с именем BlackBox-2 и начнем модификацию. В индикаторе Ticks был один недостаток — не отражалась линия Bid. Добавим ее с помощью нашего советника, заодно научимся создавать и перемещать горизонтальную линию, назовем ее bidLine (линия Bid? а). Естественно вставить этот код в функцию EveryTick():
Этот пример показывает взаимодействие советника и окна индикатора. Сначала мы ищем линию с именем bidLine, если находим — двигаем, не находим — создаем. Вы можете удалять эту линию, и советник будет создавать ее снова.
Далее, для записи скриншотов из терминала необходимо добавить параметры — начинать записывать серию снимков по фактам открытия и закрытия ордера, сколько снимков делать по каждому этому событию, через какое количество тиков делать снимки.
Кроме того, ввели новые глобальные переменные: orderShotCounter будет уменьшаться после каждого вызова ScreenShot(), а снимки будут делаться каждый раз, как только tickRateCounter будет достигать значения 0 (то есть, каждые 10 тиков в данном случае). Итого, будет записано NumberShots (100) снимков на каждое событие.
Логично, что запись будет вестись в той функции, которая вызывается на каждом тике. Этот блок будет работать только в том случае, если orderShotCounter станет больше нуля, поэтому запустить серию снимков очень просто- нам необходимо определить момент открытия или закрытия ордера и установить значение orderShotCounter равным нужному числу снимков. Перед снимком выводится время и номер снимка с помощью Comment().
Нам пришлось добавить три новые функции. Функция SymbolOrderTotals() простая, она подсчитывает количество открытых ордеров по нашему символу. Функции AddNewOrderLine() и DeleteClosedOrderLine() были добавлены для красоты, первая добавляет в окне индикатора Ticks горизонтальную линию по цене открытия нового ордера, а вторая эту линию удаляет.
Поиск нового ордера выполняется следующим образом: в цикле перебираем все ордера, для каждого ордера по нашему инструменту ищется горизонтальная линия с именем равным тикету ордера, если таковой линии не найдено — значит этот ордер и является нашим новым ордером. В этом случае создаем горизонтальную линию с именем lineName=DoubleToStr(OrderTicket(),0). Пришлось использовать функцию DoubleToStr() для преобразования номера тикета OrderTicket() в тип string.
Цвет линии зависит от типа ордера, ордера в покупку рисуются синим цветом, в продажу — красным цветом. Поиск последнего закрытого ордера производится в обратном порядке: на графике перебираются все горизонтальные линии, и для линий, имеющих описание «order» ищется ордер по тикету. Тут необходимо уточнить, что определить закрытый ордер можно только c помощью функции OrderCloseTime(), для открытых и отложенных ордеров она возвращает значение 0, то есть факта удаления отложенного ордера или закрытия рыночного ордера еще не было.
Фактически мы использовали набор горизонтальных линий как массив для запоминания списка открытых ордеров. У такого способа есть свой плюс: даже если терминал закрыть или сменить профиль — список открытых ордеров сохранится в виде набора объектов — горизонтальных линий.
Последний штрих — посчитаем количество ордеров по нашему инструменту и нанесем линии ордеров (если в этом есть необходимость) при первом запуске советника. Все лишние выводы в лог файл можете закомментировать сами, они сделаны для удобства отладки и пониманию программы. Логика советника не универсальна и он не сможет отслеживать сложные комбинации открытия/закрытия ордеров.
Для того, чтобы сделать снимки во время выхода данных по Non-farm Payrolls, я открыл ордер на продажу на демо-счете за несколько минут до выхода новости, советник обнаружил новый ордер и съемка началась. Серия из 100 снимков в формате 800в600 заняла немногим более 1Мб памяти, но во время сильных движений линия bidLine не успевает следовать за ценой, и терминал может зависать на несколько минут на не достаточно мощных компьютерах. Поэтому, сначала необходимо протестировать работу советника и индикатора в таких условиях, чтобы не возникло помех для торговли на реальном счету.
Массивы – это набор однотипных элементов, доступ к которым возможен через индекс. Часто в советниках или индикаторах приходится использовать массивы. Например, если в индикаторе для расчетов требуется использовать более восьми индикаторных буферов, то помогут массивы, определенные как ценовые серии. Или требуется логику индикатора по каким-то причинам поместить в советник, для расчета значений индикатора без обращений к функции iCustom().
В чем отличие обычного массива от ценовой серии? Эта разница появляется только потому, что в МТ4 введены дополнительные функции, которые могут рассчитывать значения некоторых технических индикаторов на массивах. Индикаторный буфер не требует от программиста дополнительных усилий, при появлении нового бара размер индикаторного буфера автоматически увеличивается на единицу и индекс всех значений также увеличивается на единицу. Бар, имевший индекс равный 6, теперь имеет индекс равный 7. Новые данные (цена закрытия Close, объем Volume и т.д.) начинают записываться в новый бар с нулевым индексом. Индикаторы на таких буферах рассчитываются от последнего индекса к нулевому. Такое поведение массива и является ценовой серией.
В отличие от ценовой серии обычный массив ведет себя противоположным образом, но только по отношению к функциям i…onArray():
iMAOnArray()
iMomentumOnArray()
iStdDevOnArray()
iEnvelopesOnArray()
iBandsOnArray()
iCCIOnArray()
iRSIOnArray()
Поэтому, пока вам не требуется использовать эти функции, вы можете и не делать различий между массивом и ценовой серией. Но если вы захотите рассчитать скользящую среднюю по массиву, то вы сразу почувствуете разницу. Это все равно, что на вокзале вы ожидаете прихода поезда, но нумерация вагонов почему-то идет с хвоста поезда. Для иллюстрации разницы написан скрипт ArraySample.mq4. Алгоритм простой: заполняется массив из 100 элементов и выводится скользящее среднее с периодом 4 от этого массива с помощью функции iMAOnArray(), сначала для обычного массива, а потом делается попытка превратить массив в ценовую серию и сделать вывод повторно.
Выражение sum+=Array[i] означает тоже самое , что и sum=sum+Array[i]. После выполнения скрипта открываем файл ArraySample.csv и видим, что объявление массива ценовой серией с помощью ArraySetAsSeries() не помогло, функция iMAOnArray() возвращает те же самые значения. Хотя среднее по массиву считается правильно, как и должно быть. Изменим немного скрипт, после объявления массива ценовой серией заново заполним его значениями и проверим.
Откроем полученный файл ArraySample2.csv в Excel. Теперь вывод для массива и для ценовой серии отличается. Значит, массив необходимо объявлять как ценовую серию до первого обращения к нему. Сделаем ручную проверку для ценовой серии. Сначала находим сумму четырех ячеек:
Затем найдем среднее для этих 4-х ячеек :
Далее заполняем этой формулой остальные ячейки:
Видно, что вычисление скользящей средней по нашему массиву с периодом 4 в скрипте совпадает ручным вычислением в Excel.
Функция iMAOnArray() хорошо подходит в тех случаях, когда требуется сгладить массив значений, допустим какой-нибудь индикатор. Например, наблюдая за EURUSD H1, мы замечаем, что в конце американской сессии индикатор ATR(8) начинает снижаться (8 – длительность сессии в часах). Чтобы получить более гладкие значения этого индикатора, мы его сглаживаем простой скользящей средней с периодом 5. Для этого мы перетаскиваем стандартный индикатор Moving Average на окно индикатора ATR и указываем в качестве исходных значений «Previous Indicator’s Data» (значения предыдущего индикатора).
Если мы не хотим каждый раз проделывать эти манипуляции, то мы можем написать пользовательский индикатор, в котором будут сглаживаться значения индикатора ATR. Например, код индикатора VolATR.mq4:
Использование этого пользовательского индикатора заменяет нам два стандартных индикатора, наложенных друг на друга:
Аналогично рассмотрим функцию iMomentumOnArray(). В ней возникает необходимость в тех случаях, когда в массиве нужно рассчитать отношение одних элементов массива к другим элементам через какой-то период.
Проверочный файл ArraySample3.xls с формулами прилагается. Примером использования этой функции является индикатор простейшего расчета индекса доллара SumLogSample.mq4.
Последней рассмотрим функцию iStdDevOnArray(). В МТ4 эта функция характеризует меру разброса данных вокруг скользящей средней от этих данных. Эта мера разброса в статистике называется стандартным отклонением. То есть, сначала на данных массива строится скользящая средняя (период и тип средней задается 3 и 4 параметром), а затем вычисляется разброс данных массива вокруг этой средней. Справку по стандартному отклонению я приведу из справочной системы MS Excel.
Сам скрипт написан так, чтобы было легче сделать ручную проверку в Excel.
В каких случаях может понадобиться эта функция? Предположим, нам необходимо из двух МТС выбрать одну, требуется критерий. Таким критерием может быть мера разброса результатов тестирования МТС, когда заполняется массив из прибылей и убытков, находится средняя сделка и стандартное отклонение. Логично, что там, где стандартное отклонение на массиве сделок будет меньше, там волатильность (и возможная будущая просадка) также ожидается меньшей. Или другой вариант. После проведения бек-теста, по результатам сделок строится аппроксимирующая линия (красного цвета) по методу наименьших квадратов. И опять ищется стандартное отклонение от этой линии. Эти вычисления несложно реализовать в блоке deinit() советника.
Функция iEnvelopesOnArray() вычисляет скользящую среднюю по массиву (средняя не выводится) и прибавляет (или отнимает) к этой средней некоторый процент. Получаем верхнюю или нижнюю границу процентного конверта. Поэтому, скрипт по ней делать сложности не составляет (можете написать сами для тренировки).
Функция iBandsOnArray() также вычисляет среднюю по массиву, а затем прибавляет( или отнимает от нее) некоторое количество стандартных отклонений. И опять получаем верхнюю или нижнюю полосу Боллинджера. Так как мы уже убедились, что вычисление средней и стандартного отклонения по массиву в МТ4 происходит без ошибок, то проверку из скрипта этих двух функций делать не имеет смысла. Они в некотором роде похожи.
Функции iCCIOnArray() и iRSIOnArray() более специфичны, реже используются в пользовательских кодах, и поэтому рассматривать я их не стал. Основные ошибки при использовании всех этих функций на массивах:
забывают объявлять тип double для массивов
забывают объявлять массивы как ценовые серии(индикаторные буферы этого не требуют).
Во всех приведенных примерах использовалось простое сглаживание, сделано это было только для простоты ручной проверки в Excel. Вид сглаживания для своих задач вы выбираете сами.
Коды скриптов, индикаторов и файлы Microsoft Excel можно взять здесь .
Есть две новости - плохая и хорошая. Плохая - рынок предсказать нельзя. Хорошая - чтобы зарабатывать деньги, этого и не нужно Брюс Бэбкок
Наверно, многие слышали эту поговорку в той или иной вариации. Есть противники и сторонники теории эффективного рынка. Одни считают, что цены случайны и заработать на рынках нельзя, другие – что цены обладают свойством прогнозируемости, и это позволяет извлекать из поведения рынка прибыль
В каждой шутке есть доля шутки. Но шутка продолжается. Периодически мы слышим, что можно зарабатывать на случайном блуждании цены, при этом нет необходимости в разработке торговой системы, а требуется только знание математики в пределах первых курсов ВУЗа. Выкладываются результаты работы торговых систем на случайных котировках, результаты тестирования подтверждают – на случайном рынке зарабатывать можно и случайным образом. Я создал таблицу в Excel, которая моделирует 100 сделок на ценах с приращением по закону нормального распределения, и построил 7 столбцов, каждый из которых показывает итоговую прибыль из 20 серий по 100 сделок от заданных StopLoss(SL) и TakeProfit(TP).
Видно, что прибыль при соотношении TP/SL=3 получилась максимальной (472), а при обратном отношении максимальным является убыток (-492). Вы можете провести свои серии испытаний, чтобы сравнить с тем, что получилось у меня. Для этого вы меняете настройки и 20 раз записываете значение «Итога» в нужные ячейки
Возможно, ваши результаты будут совсем не такими, как у меня, но если они окажутся близки хотя бы наполовину? Что делать в этом случае, сразу начинать торговать по принципу случайного открытия сделки с соотношением TP/SL=3 или сначала убедиться – являются ли цены рынка нормально распределенными?
Нормальные распределения считаются наиболее важными в методах математической статистики, рекомендую почитать книгу С.В. Булашева «Статистика для трейдера». Я попробую своими словами объяснить что такое нормальное распределение. Пусть у нас имеется два спортсмена-стрелка, каждый из которых производит по 100 выстрелов в мишень. Мы будем замерять на какое расстояние от центра («десятки») отклонилась пуля. Например, в сантиметрах. Подсчитав все 100 выстрелов, мы получим примерно такую таблицу.
Видно, что частота распределения попаданий имеет колоколообразную форму с пиком в районе 0. Для нормальных распределений центр совпадает с математическим ожиданием (арифметическое среднее). Кроме того, колокол может иметь пологие края или более крутые. Это характеризуется разбросом случайной величины вокруг среднего. Критерием этого разброса является стандартное отклонение (σ - сигма), которое мы уже знаем как находить. Из двух стрелков в общем случае лучшим будет тот, у которого стандартное отклонение меньше.
Одним из важных свойств нормального распределения является то, что в интервале 3 сигмы от математического ожидания находятся 99,73% значений, в интервале +- 2 сигмы - 95,45 %, и в интервале +- сигма – 68.27 % . Мы попробуем изучить распределение цен закрытия для любого инструмента с помощью скрипта.
Скрипт будет заполнять массив CloseArray[] значениями Close[i]-Close[i+N], где N я назвал смещением. То есть, мы будем смотреть, как будет меняться распределение приращений при различных N, при N от 1 до 10. Каждое такое заполнение массива я назвал выборкой, каждая выборка будет иметь такие характеристики, как среднее значение (матожидание), стандартное отклонение(сигма), максимальное и минимальное значение, а также процент попадания в одну, две и три сигмы. Эти характеристики будут выводиться в файл *.csv, чтобы потом по ним можно было построить диаграммы распределения.
Для записи основных характеристик служит массив Stat[8]:
Таким образом, мы можем обработать значения Close[i]-Close[i+1] на тайм-фрейме D1. Получается, что для обработки Close[i]-Close[i+2] нам нужно либо создать новый массив Stat2[] либо очистить значения массива Stat[] и заново заполнить. Такой способ не является универсальным, и для таких случаев служат многомерные массивы. Мы лучше объявим массив Stat[10][8], где будем хранить для каждого смещения от 1 до 10 свои 8 характеристик.
Теперь мы имеем двухмерный массив. Но остался еще один момент – возможно, нам потребуется получить статистику не только для дневных баров (D1), но и для других тайм-фреймов. Добавим универсальности с помощью еще одного измерения :
Массивы в MQL-4 могут быть не более, чем 4-мерными, что хватает для большинства задач. Теперь мы можем в цикле проходить по 7 тайм-фреймам (от PERIOD_M1 до PERIOD_D1) и 10 смещениям. Так как индексы по первому измерению могут принимать значения от 0 до 6, то я создал функцию, которая возвращает по номеру тайм-фрейма значение тайм-фрейма в минутах, чтобы это можно было использовать в стандартных функциях.
Сами формулы предельно просты, но код получился немного длинноват.
Обратите внимание на многочисленное использование конструкции
Дело в том, что при обработке массивов динамических размеров или не двух массивов с разными размерами немудрено сделать ошибку, которая заключается в выходе за пределы массива. Например, я объявил массив double Array[100] и попытался обратиться к элементу Array[101](или Array[100]). В этом случае будет сгенерирована ошибка с кодом 4002. Прекращение работы программы она не вызовет, но код этой ошибки будет храниться до тех пор, пока не произойдет вызов GetLastError() (тогда значение внутренней переменной, содержащей код ошибки станет равным нулю) или пока не произойдет другая ошибка. У меня в скрипте была именно эта ошибка, и чтобы точно найти то место, где она происходит, мне пришлось обложить этой ловушкой все циклы. Другого надежного способа локализовать место ошибки нет.
После запуска скрипта будет создан файл ИмяСимволаCloseDist.csv . Открываем его, выделяем необходимые данные по частотам распределения и жмем кнопку «Мастера диаграмм».
Видно, что проверка подтверждает правильность работы скрипта – сумма выделенных ячеек равна количеству значений при заданном тайм-фрейме и смещении.
Выбираем тип диаграммы и жмем «Далее».
Нас все устраивает, жмем опять «Далее».
Если есть желание – заполняем остальные поля, и опять жмем «Далее». Последнее диалоговое окно.
Жмем готово и размещаем окно диаграммы поудобнее.
Теперь мы получили график распределения приращения для EURUSD M15. Видно, что через 10 баров с вероятностью 79% цена окажется не далее 23 пункта(1 сигма ) от текущей, с вероятностью 94.4% не далее 45 пунктов(2 сигмы) от текущей, и с вероятностью 98.2% не далее 67 пунктов(3 сигмы). Сделать вывод о том – соответствует ли данный график графику нормального распределения – вы должны сделать сами.
Скрипт правильный, но мы можем его немного доработать, используя технические индикаторы на массивах. Назовем новый скрипт CloseDist2.mq4 . Кроме того я ввел новую функцию – ArrayCopySeries(). Обратите внимание, что уже не требуется задавать размер массива temp, в который якобы копируются цены закрытия.
На самом деле с помощью этой функции мы получаем доступ к тайм-серии цен закрытия (MODE_CLOSE), хотя и обращаемся к массиву temp. При обращении к данным другого тайм-фрейма или символа возможна ситуация, когда отсутствует история котировок, в этом случае генерируется ошибка 4066. Во втором скрипте добавлена обработка ошибки 4006, но нужно обратить внимание, что этот пример не правильный:
Дело в том, что ошибка 4066 генерируется только один раз при обновлении истории, второй раз она может появиться только на другом тайм-фрейме или символе.
Сравнительный анализ двух немного разных скриптов, выполняющих одну и ту же задачу, поможет лучше разобраться в использованных функциях. Сами учебные скрипты, надеюсь, тоже пригодятся. Скрипты и файлы Microsoft Excel можно взять здесь .
… очевидный феномен, который я наблюдал много раз на протяжении многих лет, но для которого у меня нет неопровержимых доказательств: большинство людей торгуют хуже, чем трейдер, полагающийся исключительно на случайный выбор. Уильям Экхардт (Джек Д. Швагер «Новые маги рынка»)
Мы знаем основные торговые функции(кроме OrderModify()), у нас имеются некоторые соображения о простейшей торговой системе на EURUSD, основанной на приращении цены в модели нормального процесса , осталось только проверить. Для этого мы напишем советника (Random Trade.mq4), который будет открывать сделки случайным образом в случайном направлении с жестко заданными параметрами StopLoss и TakeProfit.
Зададим внешний параметр int StDev, который будет означать подразумевать стандартное отклонение в пунктах. Наш StopLoss будет равен этому параметру с неким коэффициентом, который мы будем менять, назовем его SL_K. Соответственно, TakeProfit также будет вычислять как SL_K *StDev . Поэтому параметры SL_K и TP_K также будут внешними (изменяемыми на усмотрение пользователя).
Для моделирования случайной величины в MQL-4 служит функция MathRand(). Чтобы получить случайную величину в интервале от 0 до 1, нам придется разделить ее значение на 32767. Если результат больше 0.5 – мы покупаем по рыночным ценам, если меньше 0.5 – продаем. В принципе, советник уже готов.
Начинаем процесс создания советника с помощью «Мастера создания советника»
Пишем простую функцию GetOrder(), и советник готов. Обратите внимание, что уровни StopLoss(SL) и TakePofit(TP) для открываемого ордера предварительно нормализуется. Так как коэффициенты для вычисления этих уровней необязательно будут целыми, а значит и сами вычисляемые уровни также могут содержат количество знаков после запятой больше, чем Digits, то лучше нам самим округлить эти цены до нужного значения, иначе торговый сервер может просто отвергнуть приказ на открытие ордера. Нормализация всегда необходима, когда уровни цен вычисляются, и желательна, даже когда мы просто берем цены с графика. Также в ордере стоит ненулевой параметр Slippage , для тестирования он влияния не окажет, но при торговле в реальном времени лучше его задавать, чтобы на изменяющемся рынке при открытиях или закрытиях по рынку не получать отказ сервера из-за устаревания цены.
Ну и, конечно, никогда не забываем, что покупки совершаются по цене Ask, а продажи по цене Bid. В качестве комментария мы записываем случайное число Random, на основании которого было принято решение о направлении торговли. Отчасти это сделано для примера, отчасти для контроля правильности алгоритма. Компилируем советника, открываем в терминале панель тестера (Ctrl+R), выбираем нашего советника, нужный символ и нужный тайм-фрейм. Модель тестирования ставим «По ценам открытия(быстрый метод на сформировавшихся барах)». Может сложиться ситуация, что в разворачивающимся списке символов не окажется нужного нам. Тогда необходимо схватить мышкой нужный символ и бросить на панель тестера (модель “Drag & Drop”).
Правда, есть в нем некоторая искусственность, неправильность, если присмотреться. Если ордер открыт, то новый ордер не откроется, пока не произойдет закрытие с прибылью или с убытком, но как только ордер закрылся – тут же открывается новый случайный ордер. Жмем кнопку «Старт», после окончания тестирования открываем закладку «Результаты» и видим подтверждение:
В жизни обычно такого не происходит, хоть в ручной торговле, хоть при торговле советником. Всегда происходит какая-то задержка между закрытием одного и открытием другого ордера. Улучшим нашу модель случайной торговли – добавим запаздывание между этими двумя событиями. Если эта задержка будет жесткой, то мы не полностью реализуем случайную модель торговли, поэтому предлагаю сделать саму паузу между двумя ордерами также случайной. Представим, что при торговле на часовках, в среднем между закрытием одного ордера и открытием другого проходит не более 20 периодов.
Значит нам необходимо как то получать информацию о времени закрытия последнего ордера. Для этого введем две новые переменные: lastTicket (номер тикета последнего ордера) и lastCloseTime (время последнего закрытия). Чтобы не терять эти значения в перерывах между вызовами функции start(), объявим их на глобальном уровне. Также добавим внешний параметр periodTrade, его значение будет нам гарантировать максимальное время между двумя ордерами. Наш советник RandomTrade-2 выглядит теперь так:
В советнике специально добавлено множество закомментированных выводов в лог (Print()), при необходимости для лучшего понимания их можно раскомментировать. Теперь при удачном открытии ордера запоминается его тикет. Позже в функции GetOrder() промежуток времени между последним закрытием и текущим временем сравнивается с заданным периодом в барах periodTrade. Выражение (CurTime()-lastCloseTime)/( periodTrade*Period()*60.0) поначалу будет малым, но с каждым новым баром в тестере будет расти, и случайное число в интервале от 0 до 1 сначала будет иметь минимальную вероятность оказаться меньше этого выражения , но после periodTrade баров вероятность открытия нового ордера станет 100%. Запускаем новый вариант советника, и видим, что ситуация исправилась. Теперь появилась пауза между сделками.
Но если мы изменим модель «По ценам открытия» на модель «Все тики» или «Контрольные точки», то картина изменится , вот «Результаты» по «Контрольным точкам»
и вот по «Всем тикам»
Вы можете провести тестирования на своем компьютере, количество и порядок сделок у вас будут отличаться от моих, но общий вывод будет такой же – чем более подробное моделирование мы проводим, тем больше получается сделок. Мы получили наглядное доказательство неправильного строительства этого простого советника – от изменения режима моделирования результаты теста очень сильно меняются. Если ваш советник, написанный по какой-то стратегии (а не как у меня на случайности) будет себя вести также – значит у вас имеется серьезный изъян в алгоритме советника. Чтобы получать результаты, не сильно зависящие от режима моделирования, вам необходимо:
закачать как можно более подробную историю за тестируемый период начиная с минуток, и
в явном виде разделить события EveryBar и EveryTick, как в советнике BlackBox.mq4.
Пусть эти события(функции) будут у вас называться и определены иначе, чем у меня, но вы должны их четко различать.
Тут можно возразить- «Конечно, количество сделок на разных моделях будет различаться, так как у нас случайные сделки, и повторятся они не обязаны». На самом деле у нашего советника есть последний недостаток – он не является на самом деле случайным, хотя мы и использовали функцию MathRand(). Каждый раз при каждом прогоне в тестере генерируется одна и та же последовательность псевдослучайных чисел, и поэтому на каждом режиме моделировании мы получаем одну и ту же последовательность сделок. Проверьте сами.
Чтобы получать новую последовательность случайных чисел, необходимо генератор случайных чисел инициализировать с помощью функции MathSrand(). Если мы используем MathSrand(1), то последовательность случайных чисел будет одна, если MathSrand(2) – то другая. Но ведь нам необходимо, чтобы при каждом новом тестировании получать новый ряд сделок, последовательности сделок должны быть разными. Самый простой выход – использовать конструкцию MathSrand(GetTickCount()). Поместим ее в блок init() и задача решена, так как функция GetTickCount() при каждом новом вызове будет возвращать новое значение. Окончательный вариант мы назовем RandomTrade-3.mq4.
Теперь мы можем провести оптимизацию нашего советника, то есть поиск наилучших параметров для получения прибыли, или другими словами – подтверждается ли гипотеза, что при соотношении TP/SL=3 мы не только получим прибыль, но и эта прибыль будет максимальна. Нажимаем кнопку «Свойства эксперта» , выбираем закладку «Входные параметры» и отмечаем как на рисунке.
Не забудем убедиться, что на закладке «Тестирование» чек-бокс «Генетические алгоритмы» не отмечен – он нам сейчас не нужен. Начальный депозит оставим как есть - $10000 , разрешены покупки и продажи (Позиции: Long&Short).
Отмечаем на панели тестера чек-бокс «Оптимизация», модель «По ценам открытия» и получаем что-то подобное.
Прогоняем еще раз – получаем новый график оптимизации результатов.
Хорошо видно, что на одних и тех же параметрах каждый раз получается новая серия случайных сделок, то есть мы добились все таки случайности. Но возникает новый вопрос – как же нам определить – являются ли какие-то параметры более оптимальными, чем другие. Оптимизация советника нам в данном случае не дает однозначного ответа, как быть? Если бы провели при каждом параметре по 100 прогонов советника в тестере, то мы бы смогли как-нибудь сравнить между собой результаты тестирования, полученные при разных параметрах. Мы бы могли усреднять прибыль за 100 прогонов, или количество сделок, или прибыльность.
Результаты оптимизации легко сортируются по возрастанию или убыванию при нажатии на имя столбца. Если мы два раза кликнем на строку с нужными нам параметрами советника, то попадем опять на закладку «Настройки», при этом эти параметры будут использоваться при тестировании советника.
Прогоняем советник с этими параметрами, получаем график.
Видим, что при тестировании кривая Баланса испытывала и подъемы и спады, но прибыль в $3963.66 не обнаружена. Количество сделок и прочие характеристики тестирования тоже не сходятся.
Мы стремились получить случайные сделки – мы их получили. Но наша гипотеза о возможности зарабатывания на случайных входах при определенном соотношении TP/SL так и не проверена. У нас есть последний способ это сделать - добавим пустую переменную (я ее назвал Balk), от которой ничего в алгоритме советника не зависит и сделаем оптимизацию. Эту новую внешнюю переменную я поместил самой первой в секции внешних параметров, чтобы прогон в оптимизаторе начинался с перебора ее значений. Назовем этот вариант советника RandomTrade-4, скомпилируем и зададим параметры оптимизации.
Параметр SL_K я исключил для ускорения процесса (все-таки потребуется 3000 проходов). При тестировании на часовках у меня это заняло 13 минут.
Видим, что при малом значении StDev (а значит и StopLoss) наблюдается сильная и устойчивая убыточность советника, которая практически не зависит от величины TakeProfit. График оптимизации можно еще посмотреть в виде двумерной плоскости, для этого жмем правую кнопку мыши и задаем этот режим.
Задаем значения для оси X
и для Y
<#IMG21>
Таким образом, можно очень удобно визуально оценивать влияние параметров советника на результаты тестирования. На этом знакомство с тестером можно считать состоявшимся. Возможно, что результаты тестирования этого советника дадут вам новый взгляд на эпиграф к этой статье. Файлы экспертов находятся здесь .
Люди считают, что без особого труда можно делать кучу денег. Они считают, что вы можете делать 100% в год, ограничиваясь крошечными исследованиями по выходным. Это смешно. Монро Траут. Глава «Лучшая прибыль при минимальном риске» (Джек Д. Швагер «Новые маги рынка»)
После прогона советника на исторических данных тестер в МТ4 позволяет сохранить отчет о результатах тестирования в формате html, с включением графика Баланса и Эквити, а также списком проведенных торговых операций. Список содержит дату и время открытия и закрытия каждой позиции, объем, тип (покупка или продажа) и результат (прибыль или убыток) каждой операции. Кроме того, в отчете приводятся разнообразные рассчитанные параметры для оценки результатов тестирования эксперта. Для лучшего понимания работы экспертов рекомендуется прочитать статьи на сайте разработчиков:
Список торговых операций со вкладки «Результаты» можно скопировать в Microsoft Excel и провести дополнительный анализ(провести самостоятельный расчет своих показателей). Но даже и в этом случае у нас будет не полная статистика, к тому же невозможно предусмотреть в тестере варианты на все случаи жизни. Для примера, нам необходимо получать информацию о поле «Комментарий» в ордерах, время жизни позиции (между открытием и закрытием) в минутах.
Также выведем прибыль в пунктах и в валюте депозита из расчета на 0.1 лот (для устранения влияния Money Management). На всякий случай будем также выводить час и день недели открытия/закрытия ордеров. Дополнительно будем различать ордера закрытые по стопу, по тейк-профиту и по рынку. Как видите, список получился достаточно большой.
Где нам проводить обработку истории ордеров? Конечно же в той процедуре, которая выполняется самой последней, то есть в функции deinit(). После бектеста советника имеется история сделок, которую мы можем обработать в цикле. Используем для полигона наш советника RandomTrade4. Создадим два массива double trades[] и cancells[] (для отмененных ордеров), в которые будем записывать атрибуты каждого ордера. В массив string commentsTrades[] будем записывать строковые значения.
Я не стал заполнять массив отмененных ордеров, для них все аналогично:
Пояснения требуются только для использования функции StringSubstr(). Дело в том, что к комментарию ордера при закрытии по стопу тестер или торговый сервер добавляет «[sl]» , а при закрытии по тейк-профиту добавляется «[tp]». Если у вас был ордер с комментарием «проба», то при закрытии по стопу комментарий превратится в «проба[sl]». Таким образом в commentsTrades[cnt][0] записывается первоначальный комментарий, в индекс [1] пишем «[sl]», «[tp]» или «»(пустую строку), если ордер был закрыт по рынку. В индекс [2] пишем тип ордера – «buy» или «sell». Измененный эксперт назовем RandomTrade+Statistika.mq4. В массиве trades[] не обрабатываются индексы 9, 10 и 12. Они могут понадобиться нам в будущем.
Теперь осталось решить – что мы будем анализировать. Наш советник хоть и не несет особой смысловой нагрузки, но попробуем и из него выжать по максимуму. Обычно пишут советник по сигналам некоего индикатора и стараются его каким-либо образом улучшить (путем оптимизации) для увеличения прибыли при торговле на истории. Попробуем пойти обратным путем – через массив случайных сделок увидеть закономерность в показаниях индикатора. Возьмем классический индикатор MACD(12,26,9) и будем записывать в комментарий ордера единицу, если значение главной линии выше нуля, и минус один в противном случае.
Компилируем код советника, прогоняем на EIUUSD H1 с параметром StDev=50 и открываем в Excel csv-файл. Мы попробуем найти закономерность – связь между значением MACD и направлением позиции. После сортировки по типу «buy» и полю «Comment»=-1 видим, что эти сделки нам принесли прибыль в размере $1266.
Меняем тип ордера на «sell» и «Comment» на 1. Получаем опять убыток, но небольшой - $250.
Интересно, последний год с лишним (на этом интервале шло тестирование) идет падение EURUSD, а покупки при условии StopLoss=50 и TakeProfit=150 пунктов являются прибыльными. Может быть мы случайно выявили некую закономерность в поведении этой валютной пары? Чтобы проверить это, мы немного изменим алгоритм нашего советника. Время открытия позиций будет по-прежнему случайным, а направление открытия мы зададим жестко. Если MACD выше нуля – продаем, если ниже – покупаем. Вот такая простая контртрендовая система. Назовем этот вариант RandomTtade+Statistika2.mq4 .
Проверим наше предположение, сделаем оптимизацию, если она покажет, что случайные сделки при таких параметрах в среднем прибыльны – значит мы на правильном пути. Параметры оптимизации:
Balk – от 1 до 100 с шагом 1 (100 проходов для каждой комбинации значащих параметров)
TP_K – от 1 до 3.
StDev – от 10 до 100 с шагом 5.
Оптимизация заняла чуть меньше двух часов, «Результаты оптимизации» я скопировал и сохранил в файле Excel.
Но самое главное, на «Графике оптимизации» мы видим, что есть целая серия прибыльных сделок при StDev=65 и TP_K=2. Лучшие результаты получены не при StDev=50, но и не очень далеко от этого значения.
Самое интересное, что если TakeProfit больше стопа в 3 раза (TP_K=3) – прибыль становится меньше. Это еще одна неожиданность. Устанавливаем оптимальные параметры и прогоняем разок в бектесте. Смотрим график:
Делаем еще прогон, опять смотрим:
Еще разок, опять видим прибыль:
«Отчет» тестера для одного из прогонов в виде html-файла прилагается.
Неужели все так просто? Даже не верится. Внимательно просматриваем сделки и замечаем, что после закрытия одной сделки тут же открывается другая. Каким-то образом при таком изменении кода мы потеряли главное достоинство нашего эксперта – случайность сделок. Или нет? Сделаем простую проверку, сначала прогоним в тестере нашего эксперта с оптимальными параметрами в режиме LongOnly (Только покупки),
а затем в режиме ShortOnly (Только продажи):
Нас учили: от перестановки мест слагаемых сумма не меняется. А тут не только нет прибыли, но и количество сделок не совпадает – отдельные покупки и продажи дали примерно по 300 сделок, всего около 600, а в режиме Long&Short мы имели примерно 400 сделок. С большой долей уверенности мы можем констатировать, что была произведена скрытая подгонка советника. Чтобы это опровергнуть, вам как минимум необходимо изменить код таким образом, чтобы появилась случайная пауза между сделками, как максимум – научить эксперта различать направление уже открытой позиции.
Возможно, нам понадобится проводить такой же анализ для других советников. В этом случае мы можем в функцию deinit() нового эксперта вставлять кусок кода из этого советника. Но есть и другой путь – использование директивы #include. Директива #include(включить, добавить) имеет силу только при компиляции кода. Когда компилятор встречает эту команду, то он пытается найти указанный файл в папке "C:Program FilesMetaTrader 4expertsinclude" и вставить весь код из этого файла в код советника.
Покажем это на примере. Весь код в блоке deinit() сначала вынесем в отдельную функцию HistoryCalculate(). В блоке deinit() теперь присутствует только вызов этой функции:
Сохраняем это вариант под именем RandonTrade+Statistika3.mq4. Если компиляция проходит успешно – переходим к созданию заголовочного файла Statistika.mqh. Запускаем «Мастера по созданию советника», выбираем тип «Заголовочный файл»:
Задаем имя файла, имя автора и линк (ссылку):
Получили шаблон заголовочного файла, который содержит примеры вызова динамической библиотеки dll, скомпилированного файла mq4 – ex4 и просто определения константы(#define).
Копируем определение нашей функции HistoryCalculate(), при этом не забываем перенести объявления переменных StatBars и stat_FileHandle, а также имени файла статистики TradesFileName:
В нашем же советнике функцию HistoryCalculate() и переменные StatBars, stat_FileHandle, TradesFileName удаляем, добавляем строчку #include :
Сохраняем советника с именем RandonTrade+Statistika4.mq4 и заголовочный файл. Удобство этого подхода в том, что если вы отладили некий код, который хотели бы использовать многократно в своих разработках, то впоследствии легко добавляете его в свой новый код на стадии компиляции. Поэтому, если впоследствии вам будет необходимо изменить код заголовочного файла (*.mqh), то вы не должны забывать о необходимости перекомпиляции всех программ, использующих этот файл.
Напоследок вставим в файл Statistika.mqh определение ненужной функции Placebo(), вызывать которую мы не будем.
Сохраняем файл и компилируем заново наш советник. Получаем сообщение:
Компилятор определил, что эта функция не используется и удалил ее из исполняемого файла. Поэтому, один заголовочный файл может содержать большую библиотеку разнообразных функций на все случаи жизни, на размер конечного исполняемого файла это не влияет, будут использованы только необходимые функции. Файлы экспертов находятся здесь .