Как сократить код торгового эксперта, попутно упростив себе жизнь и уменьшив число возможных ошибок
Введение
Очень часто торговые системы, основанные на элементах технического анализа, будь то индикаторы или графические построения, обладают одним очень важным свойством. Речь идет о симметрии таких систем относительно направления торговли. Благодаря этому свойству торговые сигналы и механика размещения торговых приказов в таких системах может быть выражена обобщенно относительно их направления.
Рассмотренный далее несложный подход, позволяет эффективно использовать это свойство для заметного сокращения объема кода торговых экспертов, основанных на таких симметричных системах. Торговые эксперты, использующие этот подход, используют один и тот же код для обнаружения торговых сигналов и генерации торговых приказов - как для длинного, так и для короткого направления торговли.
Зачастую при разработке торговых экспертов по симметричным системам сначала разрабатывается код для генерации и отработки торговых сигналов в одном направлении, а затем копируется и дорабатывается для работы в противоположном направлении. При этом очень легко ошибиться, и очень непросто потом обнаружить такую ошибку. Поэтому дополнительным преимуществом рассматриваемого подхода является уменьшение числа потенциальных ошибок в логике торговых экспертов.
1. Основа экспертов, инвариантных по отношению к направлению торговли
В основе рассматриваемой концепции лежит понятие направление торговли. Это направление может быть либо длинным (сигналы и приказы на покупку), либо коротким (сигналы и приказы на продажу). Нашей целью является написание торговых экспертов таким образом, чтобы их код был инвариантным относительно текущего направления торговли. Чтобы избежать нагромождений в тексте, в рамках данной статьи будем называть такой код инвариантным, подразумевая при этом, что речь идет об инвариантности относительно направления торговли.
Для этого вводится функция или переменная, значение которой всегда будет отражать текущее направление торговли одним из двух возможных значений.
Очень важным аспектом является представление этой переменной в коде. Несмотря на то, что для этих целей, казалось бы, напрашивается тип bool, гораздо эффективней использовать несколько иное представление - целочисленное. При этом направление торговли кодируется следующим образом:
- длинное направление торговли: +1
- короткое направление торговли: -1
Преимуществом такого представления по сравнению с логическим представлением является возможность его эффективного применения для вычислений и всевозможных проверок в коде эксперта без использования условных ветвлений, присущих традиционному подходу.
2. Примеры перехода от традиционного кода к инвариантному
Проясним это утверждение на примерах, но для начала рассмотрим пару вспомогательных функций, которые нам еще не раз пригодятся:
int sign( double v ) { if( v < 0 ) return( -1 ); return( 1 ); } double iif( bool condition, double ifTrue, double ifFalse ) { if( condition ) return( ifTrue ); return( ifFalse ); } string iifStr( bool condition, string ifTrue, string ifFalse ) { if( condition ) return( ifTrue ); return( ifFalse ); } int orderDirection() { return( 1 - 2 * ( OrderType() % 2 ) ); }
Назначение функции sign() очевидно: она возвращает 1 для неотрицательных значений аргумента и -1 - для отрицательных.
Функция iif() является эквивалентом оператора языка С "condition ? ifTrue : ifFalse" и позволяет значительно упростить код инвариантных экспертов, делая его более компактным и легким для восприятия. Она принимает аргументы типа double, что позволяет использовать ее как со значениями этого типа, так и со значениями типа int и datetime. Для аналогичной работы со строками нам понадобится полностью аналогичная функция iifStr(), которая принимает аргументы типа string.
Функция orderDirection() возвращает направление текущего торгового приказа (т.е. выбранного функцией OrderSelect()) согласно принятым нами соглашениям о представлении направления торговли.
Теперь рассмотрим на примерах, как инвариантный подход с таким кодированием направления торговли позволяет упростить код экспертов:
2.1 Пример 1. Трансформируем реализацию трейлинг-стопа:
Типовой код:
if( OrderType() == OP_BUY ) { bool modified = OrderModify( OrderTicket(), OrderOpenPrice(), Bid - Point * TrailingStop, OrderTakeProfit(), OrderExpiration() ); int error = GetLastError(); if( !modified && error != ERR_NO_RESULT ) { Print( "Failed to modify order " + OrderTicket() + ", error code: " + error ); } } else { modified = OrderModify( OrderTicket(), OrderOpenPrice(), Ask + Point * TrailingStop, OrderTakeProfit(), OrderExpiration() ); error = GetLastError(); if( !modified && error != ERR_NO_RESULT ) { Print( "Failed to modify order " + OrderTicket() + ", error code: " + error ); } }
Инвариантный код:
double closePrice = iif( orderDirection() > 0, Bid, Ask ); bool modified = OrderModify( OrderTicket(), OrderOpenPrice(), closePrice - orderDirection() * Point * TrailingStop, OrderTakeProfit(), OrderExpiration() ); int error = GetLastError(); if( !modified && error != ERR_NO_RESULT ) { Print( "Failed to modify order " + OrderTicket() + ", error code: " + error ); }
Итак, что мы видим:
- нам удалось избавиться от громоздкого условного ветвления;
- обойтись всего одной строкой с вызовом функции OrderModify() вместо исходных двух;
- и, как следствие (2), сократить код для обработки ошибок.
Обратите внимание, что нам удалось обойтись одним вызовом OrderModify() благодаря тому, что мы используем направление торгового приказа непосредственно в арифметическом выражении для расчета уровня стопа. Если бы мы использовали логическое представление направления торговли, то это было бы невозможно.
В принципе, опытный разработчик экспертов, мог бы обойтись одним вызовом OrderModify() и при использовании традиционного подхода. Однако в нашем случае это происходит абсолютно естественно и не требует каких-либо дополнительных "телодвижений".
2.2 Пример 2. Трансформируем обнаружение торговых сигналов:
Рассмотрим в качестве примера обнаружение торговых сигналов в системе из двух скользящих средних:
double slowMA = iMA( Symbol(), Period(), SlowMovingPeriod, 0, MODE_SMA, PRICE_CLOSE, 0 ); double fastMA = iMA( Symbol(), Period(), FastMovingPeriod, 0, MODE_SMA, PRICE_CLOSE, 0 ); if( fastMA > slowMA + Threshold * Point ) { // открываем длинную позицию int ticket = OrderSend( Symbol(), OP_BUY, Lots, Ask, Slippage, 0, 0 ); if( ticket == -1 ) { Print( "Failed to open BUY order, error code: " + GetLastError() ); } } else if( fastMA < slowMA - Threshold * Point ) { // открываем короткую позицию ticket = OrderSend( Symbol(), OP_SELL, Lots, Bid, Slippage, 0, 0 ); if( ticket == -1 ) { Print( "Failed to open SELL order, error code: " + GetLastError() ); } }
Теперь сделаем код инвариантным относительно направления торговли:
double slowMA = iMA( Symbol(), Period(), SlowMovingPeriod, 0, MODE_SMA, PRICE_CLOSE, 0 ); double fastMA = iMA( Symbol(), Period(), FastMovingPeriod, 0, MODE_SMA, PRICE_CLOSE, 0 ); if( MathAbs( fastMA - slowMA ) > Threshold * Point ) { // открываем позицию int tradeDirection = sign( fastMA - slowMA ); int ticket = OrderSend( Symbol(), iif( tradeDirection > 0, OP_BUY, OP_SELL ), Lots, iif( tradeDirection > 0, Ask, Bid ), Slippage, 0, 0 ); if( ticket == -1 ) { Print( "Failed to open " + iifStr( tradeDirection > 0, "BUY", "SELL" ) + " order, error code: " + GetLastError() ); } }
Я думаю, совершенно очевидно, что код стал компактней. Опять же естественным образом две проверки ошибочных ситуаций превратились в одну.
Несмотря на то, что здесь были рассмотрены очень простые примеры, основные преимущества рассматриваемого подхода должны быть весьма очевидны. В более сложных случаях разница между традиционным и рассматриваемым подходами еще больше. Давайте убедимся в этом, на примере стандартного эксперта MACD Sample
3. Упрощаем MACD Sample
Чтобы без надобности не раздувать объем статьи, мы здесь не будем рассматривать код этого эксперта в полном объеме, а сосредоточим наше внимание только на тех участках, которые будут подвергнуты изменениям, согласно рассматриваемой концепции.
Полный код данного эксперта входит в поставку MetaTrader 4. Для удобства он был приложен к данной статье (файл MACD Sample.mq4) вместе с полной версией его упрощенного варианта (MACD Sample-2.mq4).
Займемся сначала блоком обнаружения торговых сигналов. Вот его первоначальный вариант:
// check for long position (BUY) possibility if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && MaCurrent>MaPrevious) { ticket=OrderSend(Symbol(),OP_BUY,Lots,Ask,3,0,Ask+TakeProfit*Point, "macd sample",16384,0,Green); if(ticket>0) { if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES)) Print("BUY order opened : ",OrderOpenPrice()); } else Print("Error opening BUY order : ",GetLastError()); return(0); } // check for short position (SELL) possibility if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDOpenLevel*Point) && MaCurrent<MaPrevious) { ticket=OrderSend(Symbol(),OP_SELL,Lots,Bid,3,0,Bid-TakeProfit*Point, "macd sample",16384,0,Red); if(ticket>0) { if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES)) Print("SELL order opened : ",OrderOpenPrice()); } else Print("Error opening SELL order : ",GetLastError()); return(0); }
Теперь, используя рассмотренные выше приемы, переделаем код таким образом, чтобы он был одним и тем же, как для сигналов на покупку, так и для сигналов на продажу:
int tradeDirection = -sign( MacdCurrent ); // check if we can enter the market if( MacdCurrent * tradeDirection < 0 && ( MacdCurrent - SignalCurrent ) * tradeDirection > 0 && ( MacdPrevious - SignalPrevious ) * tradeDirection < 0 && MathAbs(MacdCurrent)>(MACDOpenLevel*Point) && ( MaCurrent - MaPrevious ) * tradeDirection > 0 ) { int orderType = iif( tradeDirection > 0, OP_BUY, OP_SELL ); string orderTypeName = iifStr( tradeDirection > 0, "BUY", "SELL" ); double openPrice = iif( tradeDirection > 0, Ask, Bid ); color c = iif( tradeDirection > 0, Green, Red ); ticket = OrderSend( Symbol(), orderType, Lots, openPrice, 3 , 0, openPrice + tradeDirection * TakeProfit * Point, "macd sample", 16384, 0, c ); if(ticket>0) { if(OrderSelect(ticket,SELECT_BY_TICKET,MODE_TRADES)) Print( orderTypeName + " order opened : ", OrderOpenPrice() ); } else Print("Error opening " + orderTypeName + " order : ",GetLastError()); return(0); }
Перейдем к блоку, отвечающему за закрытие открытых позиций и отработку трейлинг-стопов. Как и прежде, изучим сначала его исходную версию:
if(OrderType()==OP_BUY) // long position is opened { // should it be closed? if(MacdCurrent>0 && MacdCurrent<SignalCurrent && MacdPrevious>SignalPrevious && MacdCurrent>(MACDCloseLevel*Point)) { OrderClose(OrderTicket(),OrderLots(),Bid,3,Violet); // close position return(0); // exit } // check for trailing stop if(TrailingStop>0) { if(Bid-OrderOpenPrice()>Point*TrailingStop) { if(OrderStopLoss()<Bid-Point*TrailingStop) { OrderModify(OrderTicket(),OrderOpenPrice(),Bid-Point*TrailingStop, OrderTakeProfit(),0,Green); return(0); } } } } else // go to short position { // should it be closed? if(MacdCurrent<0 && MacdCurrent>SignalCurrent && MacdPrevious<SignalPrevious && MathAbs(MacdCurrent)>(MACDCloseLevel*Point)) { OrderClose(OrderTicket(),OrderLots(),Ask,3,Violet); // close position return(0); // exit } // check for trailing stop if(TrailingStop>0) { if((OrderOpenPrice()-Ask)>(Point*TrailingStop)) { if((OrderStopLoss()>(Ask+Point*TrailingStop)) || (OrderStopLoss()==0)) { OrderModify(OrderTicket(),OrderOpenPrice(),Ask+Point*TrailingStop, OrderTakeProfit(),0,Red); return(0); } } } }
Трансформируем этот код в инвариантный по отношению к направлению торговли:
tradeDirection = orderDirection(); double closePrice = iif( tradeDirection > 0, Bid, Ask ); c = iif( tradeDirection > 0, Green, Red ); // should it be closed? if( MacdCurrent * tradeDirection > 0 && ( MacdCurrent - SignalCurrent ) * tradeDirection < 0 && ( MacdPrevious - SignalPrevious ) * tradeDirection > 0 && MathAbs( MacdCurrent ) > ( MACDCloseLevel * Point ) ) { OrderClose(OrderTicket(),OrderLots(), closePrice, 3,Violet); // close position return(0); // exit } // check for trailing stop if(TrailingStop>0) { if( ( closePrice - OrderOpenPrice() ) * tradeDirection > Point * TrailingStop ) { if( OrderStopLoss() == 0 || ( OrderStopLoss() - ( closePrice - tradeDirection * Point * TrailingStop ) ) * tradeDirection < 0 ) { OrderModify( OrderTicket(), OrderOpenPrice(), closePrice - tradeDirection * Point * TrailingStop, OrderTakeProfit(), 0, c ); return(0); } } }
Обратите внимание, что в исходной версии эксперта только для коротких позиций в отработке трейлинг-стопа проверяется условие OrderStopLoss() == 0. Это необходимо для отработки тех ситуаций, когда первоначальный стоп установить не удалось (например, из-за того, что он был слишком близко к рынку).
То, что это условие не проверяется для длинных позиций, можно считать ошибкой, очень характерной для написания таких симметричных экспертов методом copy-and-paste.
Заметьте, что в усовершенствованном коде эта ошибка автоматически исправлена для обоих направлений торговли. Следует отдельно отметить, что если бы эта ошибка была бы допущена при написании инвариантного кода, то она проявлялась бы как при отработке длинных позиций, так и для коротких. Совершенно очевидно, что это увеличило бы вероятность ее обнаружения в процессе тестирования.
Вот собственно и все. Если вы протестируете этих экспертов с одинаковыми настройками на одних и тех же данных, то сможете убедиться, что они полностью эквивалентны. При этом упрощенный вариант заметно компактней, а также более прост в сопровождении.
4. Рекомендации по написанию симметричных экспертов "с нуля"
До сих пор мы рассматривали варианты перехода от традиционного кода торговых экспертов к инвариантному. Однако еще более эффективным является изначальная разработка торгового эксперта с использованием изложенных выше принципов.
На первый взгляд, это может показаться не самым простым делом, так как требует определенной наработки и навыка в формулировании условий и выражений, инвариантных по отношению к направлению торговли. Однако после некоторой практики написание кода в таком стиле будет получаться довольно легко, как бы само собой.
Попробую дать несколько рекомендаций, которые могут помочь быстрее взять на вооружение более эффективный подход:
- При разработке того или иного участка кода, задайтесь сначала длинным направлением торговли - синтезировать инвариантный код будет в большинстве случаев несколько проще, так как это направление представляется значением +1 и требует минимума умственных усилий при написании и анализе инвариантных отношений.
- Если вы задались длинным направлением, попробуйте сначала написать условие без переменной/функции, отражающей направление торговли. Убедившись, в корректности выражения, добавьте в него направление торговли. По мере накопления опыта, можно будет отказаться от такого пошагового разделения.
- Не зацикливайтесь на длинном направлении торговли - иногда бывают ситуации, когда сначала проще выразить условие для короткого направления.
- Старайтесь избегать условных ветвлений и использования функции iif() там, где можно обойтись арифметическими вычислениями.
По последнему пункту добавлю, что бывают ситуации, когда без условного ветвления не обойтись. Однако нужно стараться обобщать такие ситуации и выделять их в отдельные вспомогательные функции от направления торговли, которые не зависят от конкретного эксперта. Эти функции, а также рассмотренные выше функции sign(), iif() и orderDirection(), можно выделить в отдельную библиотеку, которая в дальнейшем будет использоваться всеми вашими экспертами.
Для того, чтобы было понятней о чем идет речь, давайте рассмотрим следующую задачу:
при формировании торгового приказа, защитный стоп должен быть на уровне минимума предыдущего бара для длинной позиции и на уровне максимума предыдущего бара для короткой позиции.
В коде это можно выразить следующим образом:
double stopLevel = iif( tradeDirection > 0, Low[ 1 ], High[ 1 ] );
Казалось бы, и просто и понятно, однако даже такие простые конструкции можно и нужно обобщать в небольшие и полезные функции, которые будут часто использоваться повторно.
Избавимся от условного оператора, перенеся его во вспомогательную функцию более общего назначения:
double barPeakPrice( int barIndex, int peakDirection ) { return( iif( peakDirection > 0, High[ barIndex ], Low[ barIndex ] ) ); }
Теперь расчет уровня защитного стопа можно выразить следующим образом:
double stopLevel = barPeakPrice( 1, -tradeDirection );
Не поддавайтесь первому впечатлению, если вам кажется, что разница совсем незначительна. Такой вариант имеет серьезные преимущества:
- он явно выражает свою цель в инвариантной форме;
- стимулирует написание кода эксперта в правильном стиле;
- легче читается и упрощает дальнейшую доработку.
Это всего лишь один пример. На самом деле очень многие типовые элементы кода торговых экспертов можно выразить в подобной форме. Вы без труда сможете сделать это сами. И я настоятельно рекомендую анализировать и перерабатывать свой код в таком ключе.
5. Зачем все это?
Описанная концепция, на мой взгляд, обладает следующими серьезными преимуществами:
- сокращение исходного кода, без потери функциональности, и как следствие, сокращение временных затрат на разработку и отладку торговых систем;
- уменьшение числа потенциальных ошибок;
- увеличение вероятности обнаружения существующих ошибок;
- упрощение дальнейшей модификации торгового эксперта (изменения автоматически распространяются как на длинные, так и на короткие сигналы и позиции).
Мною был замечен только один недостаток: на начальном этапе данная концепция может вызвать небольшие затруднения в плане понимания и освоения. Однако этот недостаток с лихвой перевешивается отмеченными преимуществами. Более того - стоит немного "набить руку" и разработка инвариантного кода становится естественным и несложным процессом.
Заключение
Изложенный подход к написанию кода торговых экспертов на MQL основан на использовании и эффективном представлении понятия направление торговли. Он позволяет избежать дублирования практически идентичного кода, который обычно можно видеть в экспертах, написанных с использованием традиционного подхода. Результатом применения изложенного метода является существенное сокращение объема исходного кода со всеми следующими из этого преимуществами.
Рассмотренные примеры призваны помочь начинающим и более опытным разработчикам торговых систем переработать, при желании, свои существующие торговые системы. Дополненные рекомендациями автора они помогут изначально писать компактный код, который будет инвариантным по отношению к направлению торговли.
Для облегчения восприятия материала были рассмотрены довольно простые примеры. Однако описанный здесь подход успешно применялся для реализации гораздо более сложных торговых систем, использовавших такие инструменты технического анализа как линии тренда, каналы, вилы Эндрюса, волновой принцип Элиота, а также другие методы анализа рынка умеренной и повышенной сложности.
- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования