English 中文 Español Deutsch 日本語 Português
preview
Разработка системы репликации - Моделирование рынка (Часть 15): Появление СИМУЛЯТОРА (V) - СЛУЧАЙНОЕ БЛУЖДАНИЕ

Разработка системы репликации - Моделирование рынка (Часть 15): Появление СИМУЛЯТОРА (V) - СЛУЧАЙНОЕ БЛУЖДАНИЕ

MetaTrader 5Тестер | 2 октября 2023, 13:03
678 0
Daniel Jose
Daniel Jose

Введение

В нескольких последних статьях серии, посвященной разработке системы репликации рынка, мы видели как моделировать, а точнее, как генерировать некое виртуальное моделирование конкретного актива. Цель - получить движение, максимально приближенное к реальному. Несмотря на то, что мы значительно продвинулись от простой системы, очень похожей на ту, что используется в тестере стратегий, к очень интересной модели, нам пока не удалось создать модель, подходящую под любой набор данных. Нужно, чтобы она была динамичной и в то же время достаточно стабильной, чтобы можно было генерировать множество возможных сценариев с минимальными программными усилиями.

Здесь мы будем исправлять недостаток из статьи "Разработка системы репликации - Моделирование рынка (часть 14)": Появление СИМУЛЯТОРА (IV)". Несмотря на сформированный первый принцип работы СЛУЧАЙНОГО БЛУЖДАНИЯ, он не вполне адекватен при работе со значениями, заранее определенными в файле или базе данных. Наш случай особенный: в базе данных всегда будут указаны метрики, которые необходимо использовать и соблюдать. Хотя система СЛУЧАЙНОГО БЛУЖДАНИЯ при рассмотрении и разработке может генерировать движения, очень похожие на те, которые наблюдаются на реальном рынке, она не подходит для использования в симуляторе движения. Это объясняется тем, что он не может полностью охватить диапазон, который надо занимать во всех случаях. На самом деле в очень редких случаях мы имеем полный охват всего диапазона, исходя от цены открытия к максимуму или минимуму, полностью меняя направление при достижении одной из границ и уходя в другой экстремум. В конце концов, почти по волшебству, он найдет и остановится именно на той цене, которая была определена как закрытие бара.

Это может показаться невозможным, но иногда такое случается. Однако мы не можем полагаться на случай. Нужно, чтобы он был как можно более случайным и в рамках разрешенного. В то же время необходимо, чтобы он выполнял свою функцию - полный и всесторонный охват заданных точек на баре. Размышляя подобным образом и анализируя некоторые абстрактные математические понятия, мы можем сгенерировать относительно привлекательную форму управляемого СЛУЧАЙНОГО БЛУЖДАНИЯ. Хотя бы касаемо того, что все точки интереса и определенные точки будут достигнуты и соблюдены.

Сама идея несложная, хотя и довольно необычная, я не буду вдаваться в излишние математические подробности, чтобы не усложнять объяснение. Тем не менее, совместно с кодом мы рассмотрим концепцию, которая будет использоваться, чтобы вы тоже могли понять её, а тот, кто разбирается - даже разработать небольшие вариации того, что я представлю.


Разбор идеи и концепции

Если вы следили за этой серией статей о репликации/симуляторе, то, возможно, заметили, что мы начали с экспериментов и попыток создать систему, которая могла бы охватить все ценовые точки. Для этого использовалась методика, очень похожая на ту, что применяется в тестере стратегий. В основе этой техники лежит типичное зигзагообразное движение, которое очень распространено и широко известно среди тех, кто изучает рынок. Для тех, кто не знаком с ним или не знает, что это такое, на рис. 01 приводим его схематическое изображение.

Рисунок 01

Рисунок 01 - Типичное зигзагообразное движение

Дело в том, что, хотя такое моделирование хорошо подходит для тестера стратегий, оно далеко не всегда адекватно для системы репликации/моделирования. Причина проста: количество ордеров, генерируемых при таком моделировании, всегда значительно меньше реального, но это не означает, что данная модель несостоятельна, просто она не подходит для использования в системе, подобной той, которую мы хотим разработать. Даже если бы мы смогли найти способ, чтобы движение, изображенное на рис. 01, генерировало бы количество ордеров, эквивалентное реальной сделке, то само движение не было бы достаточно сложным. Это было бы просто адаптацией реального движения фигуры. Нам нужно реализовать это по-другому.

Необходимо найти способ, который был бы максимально случайным, но в то же время не усложняющим кода. Помните: если сложность кода растет слишком быстро, он в скором времени станет невозможным для поддержания и исправления. Мы всегда должны стремиться к тому, чтобы всё было как можно проще. Таким образом, вы можете подумать, что мы могли бы просто генерировать случайные значения цен и таким образом смоделировать достаточно высокий уровень сложности, чтобы движение было максимально приближено к реальному. Однако нельзя позволить себе принять любую цену или любое количество сгенерированных ордеров, мы должны всегда уважать базу данных. Всегда.

Просматривая базу данных, можно заметить, что в ней содержится достаточно много полезной информации, на самом деле, она существует потому, что она необходимая. На рисунке 02 показано типичное содержание файла баров. В нем выделены некоторые значения, которые представляют реальный интерес.


Рисунок 02

Рисунок 02 - Типичное содержимое файла 1-минутных баров

Если взять эти значения и сгенерировать совершенно случайные значения цен, и, подчеркиваю, не выходя из заданного диапазона, то получится совершенно случайный тип движения. В то же время сам этот шаг был бы далеко не удобным. Такое движение на реальном рынке происходит редко.

Чтобы просто не задаваться вопросом о том, что именно генерируется, мы можем перевести это в графический формат. Результат совершенно случайной генерации цен в заданном диапазоне будет выглядеть так:


Рисунок 03

Рисунок 03 - График результата полностью случайных значений

Такой график можно получить с помощью достаточно простого и в то же время очень эффективного метода. В прошлом мы уже видели, как это сделать в Excel, так что вы можете сгенерировать подобный график. Это позволит более точно анализировать строящееся движение, основываясь только на значениях в базе данных. Действительно, стоит научиться так делать, потому что это необходимо для более быстрого анализа совершенно случайных значений. Без этого ресурса мы почувствуем себя совершенно потерянными в огромном количестве данных.

Зная, как строить такие графики, можно будет более спокойно анализировать ситуацию. И вы поймете, что график, представленный на рисунке 03, не совсем подходит для нашей системы из-за высокой степени случайности, присутствующей в нем. Тогда нужно искать способ хотя бы сдержать эту случайность, чтобы создать нечто более похожее на реальное движение. Через некоторое время вы откроете для себя метод, который часто используется в старых видеоиграх, где персонаж, казалось бы, произвольно перемещается по игровому сценарию, но на самом деле всё происходит по довольно простым правилам. Данный метод также можно увидеть в очень популярных и забавных системах "перепутай и собери", таких как кубик Рубика (рис. 04), или даже в двумерных играх типа пятнашки, в которых раздвижные фигуры используются и для решения задачи, и для перемешивания деталей в игре (рис. 05).




Рисунок 05

Рисунок 04 - Кубик Рубика - пример движения СЛУЧАЙНОГО БЛУЖДАНИЯ.


Рисунок 05

Рисунок 05 - Пятнашки: простейшая система СЛУЧАЙНОГО БЛУЖДАНИЯ

Хотя на первый взгляд кажется, что эти «игрушки» не используют СЛУЧАЙНОГО БЛУЖДАНИЯ, на самом деле они его используют. Но не для их решения, хотя можно было бы разгадать их с использованием метода СЛУЧАЙНОГО БЛУЖДАНИЯ, однако это заняло бы гораздо больше времени, чем использование более подходящих методов. Для того, чтобы фигуры находились в позициях, кажущихся случайными, необходимо использовать формулировку, включающую СЛУЧАЙНОЕ БЛУЖДАНИЕ. После этого вы пытаетесь решить их по другой методике, как это делает ребенок, возвращая детали на правильное место. То же самое относится и к системе генерации ордеров с использованием СЛУЧАЙНОГО БЛУЖДАНИЯ. В действительности математика, лежащая в основе системы движения СЛУЧАЙНОГО БЛУЖДАНИЯ, практически одинакова во всех случаях, она не меняется.

По факту меняется только система направления, которую мы будем использовать. Попробуйте представить абсолютно бессмысленное движение, но это движение находится в трехмерном пространстве и не вписывается в такую математику. Однако, честно говоря, случаев, когда действительно такое происходит, очень мало. Во многих из них математика, задействованная при описании СЛУЧАЙНОГО БЛУЖДАНИЯ, может объяснить вариации, происходящие с течением времени. Таким образом, используя эту математику в системе «Цена х Время», мы получим график, аналогичный приведенному на рис. 06.


Рисунок 06

Рисунок 06 - СЛУЧАЙНОЕ БЛУЖДАНИЕ «Цена x Время»

Данный вид, построенный по тем же принципам, что и стохастические движения, очень похож на реальное движение, которое обычно демонстрирует рынок. Однако, не стоит обманываться. Приведенный график не соответствует реальному активу. Он получается с помощью генератора случайных чисел: к предыдущей цене прибавляется или отнимается одна единица, таким образом, формируется новая цена. Способ понимания того, прибавляем ли мы или вычитаем, зависит от используемого правила. Можно просто сказать, что любое случайно сгенерированное НЕЧЕТНОЕ число будет означать сложение, а любое ЧЕТНОЕ число - вычитание. Несмотря на свою простоту, данное правило позволяет добиться желаемого результата. Однако можно изменить правило так, чтобы любое случайно сгенерированное число, кратное 5, обозначало сложение, а в противном случае - вычитание. Простое изменение этого правила приведет к появлению совершенно другого графика. Даже значение, запускающее систему случайной генерации, несколько изменяет ситуацию.

Но хотя график на рис. 06 подходит для полностью свободной системы, мы не можем использовать его в системе моделирования. Это связано с тем, что мы должны соблюдать значения, указанные в базе данных, ведь это дает нам явные верхние и нижние границы для генерации СЛУЧАЙНОГО БЛУЖДАНИЯ. Таким образом, после внесения исправлений, обеспечивающих соблюдение этих ограничений, мы переходим от графика, показанного на рис. 06, к графику, показанному на рис. 07.


Рисунок 07

Рисунок 07 - СЛУЧАЙНОЕ БЛУЖДАНИЕ в пределах границ

Система, генерирующая график, как показано на рис. 07, гораздо больше подходит для использования в симуляторе. Здесь мы имеем базу данных, в которой указаны границы, внутри которых мы можем передвигаться. Фактически, используя столь же простую математику, нам удается создать систему, содержащую приемлемый уровень сложности, и в то же время она не является полностью предсказуемой, т.е. мы находимся на правильном пути. Обратите внимание, что на графиках, начиная с приведенного на рис. 01, до графика на рис. 07, единственное, что действительно изменилось - это уровень сложности. Вернее, случайность, присутствующая на последнем графике, более подходящая. Хотя система, представленная на рис. 01, достаточна для тестера стратегий, график, сгенерированный и представленный на рис. 07, содержит гораздо больше, чем необходимо для симулятора. Однако для симулятора данные вещи имеют первостепенное значение.

Но даже при таком увеличении уровня сложности, наблюдаемом на в графике 07, он всё еще не вполне достаточен для использования в симуляторе. Учитывая тот факт, что единственное, в чем мы можем быть уверены при использовании - это исходная точка. В данном случае речь идет о ЦЕНЕ ОТКРЫТИЯ. Невозможно гарантировать что какая-то из других точек, указанных в базе данных (CLOSE, MAXIMUM, MINIMUM) будет затронута. И это является проблемой, так как для того, чтобы симулятор был действительно жизнеспособным, все точки, указанные в базе данных, должны быть затронуты с абсолютной достоверностью.

Исходя из этого, нам необходимо как-то добиться того, чтобы график на рис. 07 превратился в график, подобный графику на рис. 08, где у нас есть абсолютная уверенность в том, что к этим точкам в определенный момент времени действительно осуществится доступ.


Рисунок 08

Рисунок 08 - Принудительное СЛУЧАЙНОЕ БЛУЖДАНИЕ


Этот вид изменения предполагает не более нескольких корректировок. Но если внимательно посмотреть на график, то можно отчетливо увидеть, что движение каким-то образом направляется. Даже в таком случае нельзя не заметить, что у нас есть СЛУЧАЙНОЕ БЛУЖДАНИЕ, правда не такое естественное, но всё еще случайное, как и следовало ожидать при стохастическом движении. Теперь я хочу обратить ваше внимание на следующий момент: Все рассмотренные выше графики используют одну и ту же базу данных. Она показана на рис. 02. Однако наилучшим решением является то, которое показано на рис. 08, где на самом деле мы имеем движение, не столь запутанное и не столь далекое от базы данных, как на рисунке 03. Но и в этом случае всё очень похоже на то, что могло произойти при построении бара.

Основываясь на показанных концепциях, я думаю, что убедил вас в том, что код, находящейся на той стадии, которая описывалась в предыдущей статье - это не совсем то, что нам нужно в симуляторе. Это связано с тем, что некоторые точки могут быть не затронуты в тот период, когда происходило случайное перемещение. Однако вместо того, чтобы заставлять эти точки появляться на графике, как вам идея сделать всё более естественным?

Именно этот момент мы рассмотрим в следующей теме.


Принудительное СЛУЧАЙНОЕ БЛУЖДАНИЕ

Назвать его «принудительным случайным блужданием» не означает, что мы будем накладывать на него условие. Однако мы ограничим движение определенным образом, чтобы оно выглядело как можно более естественным, сохраняя при этом свою случайную природу. Правда, с одной деталью: Мы направим движение к точке схождения. И именно эту точку необходимо посетить. Таким образом, мы получим комбинацию между фигурами 01 и 07, и это очень интригует, не правда ли? Но в реальности мы не будем использовать путь, который обычно создается, как показано на рис. 01. Мы используем несколько иной подход.

Чтобы действительно понять это, давайте рассмотрим код случайного блуждания, который находится в коде предыдущей статьи. Можно ознакомиться с ним ниже:

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long    il0, max;
                                double  v0, v1;
                                bool    bLowOk, bHighOk;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                bLowOk = bHighOk = false;
                                for (int c0 = 1; c0 < max; c0++)
                                {                               
                                        v0 = tick[c0 - 1].last + (m_PointsPerTick * ((rand() & 1) == 1 ? 1 : -1));
                                        if (v0 <= rate.high)
                                                v0 = tick[c0].last = (v0 >= rate.low ? v0 : tick[c0 - 1].last + m_PointsPerTick);
                                        else
                                                v0 = tick[c0].last = tick[c0 - 1].last - m_PointsPerTick;
                                        bLowOk = (v0 == rate.low ? true : bLowOk);
                                        bHighOk = (v0 == rate.high ? true : bHighOk);
                                }                                       
                                il0 = (long)(max * (0.3));
                                if (!bLowOk) tick[macroRandomLimits(il0, il0 * 2)].last = rate.low;
                                if (!bHighOk) tick[macroRandomLimits(max - il0, max)].last = rate.high;                         
                                for (int c0 = 0; c0 <= max; c0++)
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                }
#undef macroRandomLimits
                        }                       

На самом деле нам нужно как-то изменить этот код, выделенный выше. При правильном подходе мы можем поддерживать систему в соответствии с принципами СЛУЧАЙНОГО БЛУЖДАНИЯ, контролируя при этом процесс генерации пути. Таким образом, мы сможем посетить все точки, о которых сообщается. Помните, что точка открытия всегда будет затронута. В действительности нас волнуют другие 3 точки: максимум, минимум и закрытие. Поэтому сделаем следующее: сначала создадим базовую функцию, которая заменит выделенный выше код, но мы сделаем это гораздо более интересным способом.

Чтобы облегчить себе жизнь, мы сначала создадим новую функцию, которая будет отвечать за генерацию СЛУЧАЙНОГО БЛУЖДАНИЯ. Ниже можно увидеть его в первой версии.

inline long RandomWalk(long pIn, long pOut, const MqlRates &rate, MqlTick &tick[], int iMode)
                        {
                                double vStep, vNext, price, vHigh, vLow;
                                
                                vNext = vStep = (pOut - pIn) / ((rate.high - rate.low) / m_PointsPerTick);
                                vHigh = rate.high;
                                vLow = rate.low;
                                for (long c0 = pIn, c1 = 0, c2 = 0; c0 < pOut; c0++, c1++)
                                {
                                        price = tick[c0 - 1].last + (m_PointsPerTick * ((rand() & 1) == 1 ? -1 : 1));
                                        price = tick[c0].last = (price > vHigh ? price - m_PointsPerTick : (price < vLow ? price + m_PointsPerTick : price));
                                        switch (iMode)
                                        {
                                                case 0:
                                                        if (price == rate.close)
                                                                return c0;
                                                case 1:
                                                case 2:
                                                        break;
                                        }
                                        if ((int)floor(vNext) < c1)
                                        {
                                                if ((++c2) <= 3) continue;
                                                vNext += vStep;
                                                if (rate.close > vLow) vLow += m_PointsPerTick); else vHigh -= m_PointsPerTick;
                                        }
                                }
                                
                                return pOut;
                        }

Неужели вы не понимаете, что я делаю? Не волнуйтесь. Сейчас всё объясню. Как уже было сказано, это первая версия. Сначала мы вычисляем шаг, который необходимо выполнить, чтобы запустить случайное блуждание так, чтобы оно достигло определенной точки. Мы храним максимальное и минимальное значения границы, на которой в действительности будет находиться случайное блуждание. Теперь начинается наша одиссея. Пройдемся случайным образом по некоторому количеству точек, и на каждом шаге мы будем корректировать ситуацию, не блокируя случайное блуждание, а направляя его в некий канал, из которого оно не сможет выйти. Оно может колебаться из стороны в сторону, но ни в коем случае не выходит из канала. Когда мы находимся в нулевом режиме, то при достижении целевой точки выполнение программы прекращается. Это важно для обеспечения большей рандомизации. Но не волнуйтесь, это станет понятнее позже.

Теперь надо что-то делать: помните, мы делали расчет, чтобы узнать, сколько времени потребуется на сужение канала? Теперь пришло время начать закрывать канал.  Это закрытие происходит постепенно, но с довольным интересным моментом. В действительности мы не закроем его полностью. Мы оставим небольшую узкую полосу, в которой цена может следовать своему случайному блужданию, подобно полосам Боллинджера. Но в конечном итоге она будет практически в той точке, которая указана в качестве конечной, т.е. в месте закрытия. Фактически это происходит за счет изменения границ канала. Сначала мы закрываем нижнюю часть, а когда достигаем точки выхода, начинаем закрывать с верхней части.

Так или иначе, последняя точка будет практически доступна, но если к ней не обращаться, то она будет очень и очень близка к идеалу. Но к чему же относится приведенная выше функция? Она заменит старый метод случайного блуждания. При этом мы будем совмещать то, что происходит на рис. 01, и то, что происходит на рис. 07. В результате получится изображение, очень похожее на рис. 08.

Посмотрите, как выглядит новая функция моделирования.

inline void Simulation(const MqlRates &rate, MqlTick &tick[])
                        {
#define macroRandomLimits(A, B) (int)(MathMin(A, B) + (((rand() & 32767) / 32767.0) * MathAbs(B - A)))

                                long    il0, max, i0, i1;
                                bool    b1 = ((rand() & 1) == 1);
                                double  v0, v1;
                                MqlRates rLocal;
                                
                                ArrayResize(m_Ticks.Rate, (m_Ticks.nRate > 0 ? m_Ticks.nRate + 3 : def_BarsDiary), def_BarsDiary);
                                m_Ticks.Rate[++m_Ticks.nRate] = rate;
                                max = rate.tick_volume - 1;     
                                v0 = 4.0;
                                v1 = (60000 - v0) / (max + 1.0);
                                for (int c0 = 0; c0 <= max; c0++, v0 += v1)
                                {
                                        tick[c0].last = 0;
                                        tick[c0].flags = 0;
                                        il0 = (long)v0;
                                        tick[c0].time = rate.time + (datetime) (il0 / 1000);
                                        tick[c0].time_msc = il0 % 1000;
                                        tick[c0].volume_real = 1.0;
                                }
                                tick[0].last = rate.open;
                                tick[max].last = rate.close;
                                for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--)
                                        tick[macroRandomLimits(0, max)].volume_real += 1.0;                                     
                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);
                                for (int c0 = 0; c0 <= max; c0++)
                                {
                                        ArrayResize(m_Ticks.Info, (m_Ticks.nTicks + 1), def_MaxSizeArray);
                                        m_Ticks.Info[m_Ticks.nTicks++] = tick[c0];
                                }
#undef macroRandomLimits
                        }                       

Выделенный фрагмент заменяет старый метод. Теперь возникает важный вопрос: что делает выделенный фрагмент? Можете ли ответить, просто посмотрев на его код?

Если сможете - отлично, мои искренние поздравления! Если нет, то давайте посмотрим на этот фрагмент.

                                i0 = (long)(MathMin(max / 3.0, max * 0.2));
                                i1 = max - i0;
                                rLocal = rate;  
                                rLocal.open = rate.open;
                                rLocal.close = (b1 ? rate.high : rate.low);
                                i0 = RandomWalk(1, i0, rLocal, tick, 0);
                                rLocal.open = tick[i0].last;
                                rLocal.close = (b1 ? rate.low : rate.high);
                                RandomWalk(i0, i1, rLocal, tick, 1);
                                rLocal.open = tick[i1].last;
                                rLocal.close = rate.close;
                                RandomWalk(i1, max, rLocal, tick, 2);

То, что реализуется - это БОЛЬШОЙ зигзаг, но как 🤔? Я его не вижу 🥺! Мы будем идти поэтапно. Первоначально мы рассчитываем предельную точку, чтобы первый участок зигзага мог закончиться.  Как только это сделано, сразу же определяем размер третьего сегмента зигзага. Одна деталь: первый сегмент, в отличие от третьего, не имеет фиксированного размера. Он может закончиться в любой момент. Этот момент определяется программой случайного блуждания. Но поскольку по окончании первого этапа мы должны начать второй, то в качестве начальной точки используется значение, возвращаемое функцией случайного блуждания. Здесь у нас новый вызов для построения второго случайного блуждания. Вы еще не запутались?

Всё хорошо, дойдем и до этого. Теперь определим границы, точки входа и выхода первого участка случайного блуждания. Вызывается функция создания случайного блуждания, которая возвращается, как только будет достигнута конечная точка или как можно ближе к ней. Затем мы снова корректируем точки входа и выхода следующего этапа.  Мы вызываем функцию создания случайного блуждания так, чтобы движение шло от одного конца к другому. Но даже если эти границы будут достигнуты, функция вернется только тогда, когда будет достигнуто заданное количество позиций.  После этого мы в третий и последний раз определяем точки входа и выхода из случайного блуждания. Мы осуществляем вызов, который построит последний сегмент. Другими словами, мы имеем большой зигзаг. Вместо того чтобы цена переходила из одной точки в другую, она будет колебаться в определенных нами пределах 😁.

При запуске описанной выше системы мы получим график, очень похожий на тот, что приведен на рис. 08. Что очень неплохо, учитывая простоту описанного выше метода, но мы можем всё это улучшить. Если внимательно присмотреться, то можно заметить, что на графике, приведенном на рис. 08, есть точки, в которых цена будто упирается в стену. Это, как ни странно, иногда происходит на реальном рынке. В частности, когда одна из сторон, покупатели или продавцы, не допускают движения цены дальше определенной точки, происходит знаменитая борьба ордеров в книге заявок.

Но только если хотим получить чуть более естественное движение в системе, нам придется изменить не саму функцию моделирования, а исключительно функцию исполнения, отвечающую за создание случайного блуждания. Но вот в чем загвоздка всей этой истории. Мы не должны, и я повторюсь, НЕ ДОЛЖНЫ пытаться создать новый метод случайного блуждания. Нужно только создать способ включения и выключения границ, чтобы движение происходило само по себе. Если всё сделано правильно, то конечный результат будет гораздо более естественным, как будто мы не направляли движение в любой момент времени.

Первая попытка сделать это - добавить проверку касания границ. Как только это произойдет, начнется нечто волшебное. Итак, ознакомьтесь с новым методом случайного блуждания; это не новый метод, а всего лишь адаптация оригинального. Помните: я объясняю только новые части 😉.

inline long RandomWalk(long pIn, long pOut, const MqlRates &rate, MqlTick &tick[], int iMode)
                        {
                                double vStep, vNext, price, vHigh, vLow;
                                
    char  i0 = 0;
                                
                                vNext = vStep = (pOut - pIn) / ((rate.high - rate.low) / m_PointsPerTick);
                                vHigh = rate.high;
                                vLow = rate.low;
                                for (long c0 = pIn, c1 = 0, c2 = 0; c0 < pOut; c0++, c1++)
                                {
                                        price = tick[c0 - 1].last + (m_PointsPerTick * ((rand() & 1) == 1 ? -1 : 1));
                                        price = tick[c0].last = (price > vHigh ? price - m_PointsPerTick : (price < vLow ? price + m_PointsPerTick : price));
                                        switch (iMode)
                                        {
                                                case 0:
                                                        if (price == rate.close)
                                                                return c0;
                                                        break;
                                                case 1:
                                                        i0 |= (price == rate.high ? 0x01 : 0);
                                                        i0 |= (price == rate.low ? 0x02 : 0);
                                                        vHigh = (i0 == 3 ? rate.high : vHigh);
                                                        vLow = (i0 ==3 ? rate.low : vLow);
                                                        break;
                                                case 2:
                                                        break;
                                        }
                                        if ((int)floor(vNext) < c1)
                                        {
                                                if ((++c2) <= 3) continue;
                                                vNext += vStep;
                                                if (rate.close > vLow) vLow = (i0 == 3 ? vLow : vLow + m_PointsPerTick); else vHigh = (i0 == 3 ? vHigh : vHigh - m_PointsPerTick);
                                        }
                                }
                                
                                return pOut;
                        }

Теперь у нас совершенно новая ситуация. Поэтому мы должны избегать пробития бывших границ, превышение которых ранее не представляло проблемы. Но есть очень тонкий момент. У нас есть новая переменная, которая начинается с нуля. В действительности это было бы похоже на булево множество. То, что делает эта скромная переменная, поразительно. Поэтому надо быть очень внимательным, чтобы понять это. Данный момент очень хорошо проработан и весьма интересен. Когда мы находимся во втором сегменте зигзага, в какой-то момент цена касается максимума, а затем минимума. Затем на каждом из этих касаний мы записываем в нашу переменную определенное значение. А что будет, если рассматривать переменную не как битовые единицы, а как единое целое? Мы получим вполне конкретное значение. Это значение может быть равно нулю, единице, двум или трем.

Но погодите-ка, как же может быть три 🤔? Суть в том, что когда у нас есть касание максимума, мы выполняем операцию логического OR, в котором делаем истинным наименее значимый бит. Когда у нас есть касание минимума, мы выполняем ту же операцию, но теперь уже над вторым менее значимым битом. То есть, если первый бит уже установлен в true, а затем мы устанавливаем в true второй бит, то сразу же переходим от значения, равного 1, к значению, равному 3, поэтому счет идет до значения 3.

Важно понимать этот факт, поскольку именно это значение мы будем проверять. Вместо того чтобы тестировать 2 разных булевых числа, мы проверим одно значение, которое представляет оба. Вы поняли логику работы системы 😁? Таким образом, если значение равно 3, то у нас расширение границ, которые могут быть использованы. Или, говоря более понятным языком, если бы мы не проводили данную проверку, то границы закрывались бы до тех пор, пока случайное блуждание не сжималось бы только до 3 возможных положений движения. Но, делая это, мы позволяем движению снова развиваться естественно. Поскольку границы достигнуты, нет смысла ограничивать движение дальше.

«Взрыв» происходит в этих точках. Тогда, когда функция попытается уменьшить границы, она уже не сможет этого сделать. Теперь случайное блуждание будет происходить по всей границе, что делает его гораздо более естественным. Если теперь запустить систему с той же базой данных, которую мы использовали с самого начала статьи, то получится график, очень похожий на тот, что показан на рис. 09 😊.

Рисунок 09

Рисунок 09 - Случайное блуждание со «взрывом»


Обратите внимание, что этот график выглядит гораздо более естественным, чем график на рис. 08. Однако даже в этом последнем графике, который кажется идеальным, всё же есть точка, которая не столь естественна. Она ощущается на последнем сегменте. Обратите внимание на конец графика: заметно, что это выглядит несколько вынужденно, учитывая границы возможного перемещения. Такие вещи заслуживают исправления, именно этим мы сейчас и займемся.

Для решения данной проблемы необходимо добавить несколько строк в функцию, отвечающую за случайное блуждание. Ниже представлен окончательный вариант системы:

inline long RandomWalk(long pIn, long pOut, const MqlRates &rate, MqlTick &tick[], int iMode)
                        {
                                double vStep, vNext, price, vHigh, vLow;
                                char i0 = 0;
                                
                                vNext = vStep = (pOut - pIn) / ((rate.high - rate.low) / m_PointsPerTick);
                                vHigh = rate.high;
                                vLow = rate.low;
                                for (long c0 = pIn, c1 = 0, c2 = 0; c0 < pOut; c0++, c1++)
                                {
                                        price = tick[c0 - 1].last + (m_PointsPerTick * ((rand() & 1) == 1 ? -1 : 1));
                                        price = tick[c0].last = (price > vHigh ? price - m_PointsPerTick : (price < vLow ? price + m_PointsPerTick : price));
                                        switch (iMode)
                                        {
                                                case 0:
                                                        if (price == rate.close)
                                                                return c0;
                                                        break;
                                                case 1:
                                                        i0 |= (price == rate.high ? 0x01 : 0);
                                                        i0 |= (price == rate.low ? 0x02 : 0);
                                                        vHigh = (i0 == 3 ? rate.high : vHigh);
                                                        vLow = (i0 ==3 ? rate.low : vLow);
                                                        break;
                                                case 2:
                                                        break;
                                        }
                                        if ((int)floor(vNext) < c1)
                                        {
                                                if ((++c2) <= 3) continue;
                                                vNext += vStep;
                                                if (iMode == 2)
                                                {
                                                        if ((c2 & 1) == 1)
                                                        {
                                                                if (rate.close > vLow) vLow += m_PointsPerTick; else vHigh -= m_PointsPerTick;
                                                        }else
                                                        {
                                                                if (rate.close < vHigh) vHigh -= m_PointsPerTick; else vLow += m_PointsPerTick;
                                                        }
                                                } else
                                                {
                                                        if (rate.close > vLow) vLow = (i0 == 3 ? vLow : vLow + m_PointsPerTick); else vHigh = (i0 == 3 ? vHigh : vHigh - m_PointsPerTick);
                                                }
                                        }
                                }
                                
                                return pOut;
                        }

Кажется несколько странным, но посмотрите на следующее: когда мы выполняем первый и второй сегмент, то код, который фактически будет выполнен, выглядит следующим образом. Когда мы перейдем к третьему сегменту, у нас будет немного другая фигура.

Чтобы получить фигуру, которую в графическом анализе называют СИММЕТРИЧЕСКИМ ТРЕУГОЛЬНИКОМ, необходимо постепенно уменьшать обе стороны. Но очень важно понимать: у нас может быть СИММЕТРИЧНЫЙ ТРЕУГОЛЬНИК или АСИММЕТРИЧНЫЙ. В зависимости от того, находится ли исходная точка в равноудаленном положении между крайними точками, у нас будет построение симметричного треугольника. Если точка закрытия или выхода находится очень близко к одной из границ, то треугольник будет асимметричным. Для этого мы будем использовать чередование сложения и вычитания. Это делается, в частности, на таких точках. Способ переключения данных точек реализуется проверкой актуального значения уменьшения.

Таким образом, мы получим итоговый график, очень похожий на график, приведенный на рис. 10. Он показан ниже:

Рисунок 10

Рисунок 10 - График случайных блужданий 1-минутного бара


Обратите внимание, что он выглядит гораздо более естественно, чем любой другой график, который мы видели раньше. Таким образом, мы закрываем базу случайного блуждания, и можем переходить к следующим этапам создания нашей системы репликации/моделирования,

чтобы вы понимали, как устроена система в настоящее время.

В приложении находится полный код в его текущем состоянии разработки.


Заключение

В действительности для объяснения или создания вещей не нужно использовать сложную математику. Когда изобреталось колесо, не использовали математику, чтобы определить, каким должен быть его радиус, исходя из величины, соответствующей PI. Всё, что сделали - добились желаемого движения. В этой статье я показал, что простая математика, используемая в детских игрушках, может объяснить и представить движения, которые многие считают невозможными. И они продолжают использовать причудливую математику, хотя решение простое. Поэтому он всегда стремитесь к простоте.


Перевод с португальского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/pt/articles/11071

Прикрепленные файлы |
Сделайте торговые графики лучше с интерактивным графическим интерфейсом на основе MQL5 (Часть II): Перемещаемый интерфейс (II) Сделайте торговые графики лучше с интерактивным графическим интерфейсом на основе MQL5 (Часть II): Перемещаемый интерфейс (II)
Раскройте потенциал динамического представления данных в своих торговых стратегиях и утилитах с помощью нашего подробного руководства по созданию перемещаемых графических интерфейсов в MQL5. Погрузитесь в фундаментальные принципы объектно-ориентированного программирования и узнайте, как легко и эффективно разрабатывать и использовать один или несколько перемещаемых графических интерфейсов на одном графике.
Популяционные алгоритмы оптимизации: Алгоритм эволюции разума (Mind Evolutionary Computation, MEC) Популяционные алгоритмы оптимизации: Алгоритм эволюции разума (Mind Evolutionary Computation, MEC)
В данной статье рассматривается алгоритм семейства MEC, называемый простым алгоритмом эволюции разума (Simple MEC, SMEC). Алгоритм отличается красотой заложенной идеи и простотой реализации.
Может ли Heiken Ashi давать хорошие сигналы в сочетании со скользящими средними? Может ли Heiken Ashi давать хорошие сигналы в сочетании со скользящими средними?
Комбинации стратегий могут повысить эффективность торговли. Мы можем комбинировать индикаторы и паттерны, чтобы получать дополнительные подтверждения. Скользящие средние помогают нам подтвердить тренд и следовать ему. Это самые известный технический индикатор, что объясняется его простотой и доказанной эффективностью анализа.
Разработка пользовательского индикатора Heiken Ashi с помощью MQL5 Разработка пользовательского индикатора Heiken Ashi с помощью MQL5
В этой статье мы узнаем, как создать собственный индикатор с использованием MQL5 на основе наших предпочтений, который будет использоваться в MetaTrader 5 для интерпретации графиков или применяться в составе советников.