
Анализ стратегий внутридневной торговли на основе прорывов диапазона открытия
Введение
Стратегии прорыва диапазона открытия (Opening Range Breakout, ORB) основаны на идее о том, что начальный торговый диапазон, установленный вскоре после открытия рынка, отражает значимые уровни цен, когда покупатели и продавцы договариваются о стоимости. Выявляя прорывы определенного диапазона вверх или вниз, трейдеры могут извлекать выгоду из моментума, который часто возникает, когда направление рынка становится более отчетливым.
В этой статье рассмотрим три стратегии ORB, адаптированные из статей компании Concretum Group. Сначала рассмотрим предпосылки для проведения данного исследования, включая основные концепции и используемую методологию. Затем объясним принцип работы каждой стратегии, перечислим правила использования в ней сигналов и выполним статистический анализ ее эффективности. Наконец, изучим их с точки зрения использования портфеля, уделив особое внимание теме диверсификации.
В настоящей статье мы не будем углубляться в программирование, а напротив, сконцентрируемся на исследовательском процессе, в том числе — на воссоздании, анализе и тестировании стратегий из этих трех публикаций. Это будет полезно читателям, которые ищут потенциальные преимущества в трейдинге, или тем из них, кому интересно узнать, как были изучены и воспроизведены эти стратегии. Несмотря на вышесказанное, будет раскрыт весь MQL5-код для этих советников. Читатели могут самостоятельно расширять и дополнять эту структуру.
Предпосылки для исследования
В этом разделе рассматривается методология исследования, которую мы будем использовать для анализа стратегий, а также основные концепции, которые будут обсуждаться ниже в этой статье.
Concretum Group — одна из немногих групп, проводящих академические исследования в области разработки стратегий внутридневной торговли. В выбранных нами для адаптации исследованиях основное внимание уделяется стратегиям торговли в промежутке между открытием и закрытием рынка (с 9:30 до 16:00 по восточному времени). Поскольку наш брокер использует часовой пояс UTC+2/3, это соответствует серверному времени 18:30-24:00 — при тестировании обязательно убедитесь, что вы учитываете часовой пояс своего брокера.
В оригинальном исследовании торгуются QQQ — ETF, отслеживающий индекс Nasdaq-100. Важно отметить, что индекс Nasdaq-100 отражает эффективность 100 крупных технологических компаний на бирже Nasdaq. Сам по себе индекс на самом деле не торгуется, трейдинг осуществляется только с его производными. QQQ позволяет инвесторам получить доступ к этим компаниям посредством единственной акции. В наших тестах мы будем торговать USTEC (CFD на Nasdaq-100), что позволяет спекулировать на движениях цен без владения базовыми активами, часто используя кредитное плечо для увеличения прибылей или убытков.
В этой статье рассмотрим два ключевых показателя: альфу и бету. В трейдинге альфа представляет собой создаваемую инвестированием избыточную доходность по сравнению с таким эталоном, как рыночный индекс. Она показывает, превосходит ли инвестирование ожидания, и в значительной мере отражает преимущество. Бета измеряет чувствительность инвестирования к движениям рынка. Значение бета, равное 1, означает, что она отражает колебания рынка. Значение выше 1 свидетельствует о более высокой волатильности, а значение ниже 1 указывает на менее высокую волатильность. Эти показатели играют важную роль в понимании, насколько ваша стратегия опирается на рыночные тренды, а насколько — на собственные уникальные преимущества. Эти знания помогут вам оценить потенциальное смещение направления трендовых активов, таких как индексы или криптовалюты.
Альфа и бета рассчитываются следующим образом:
Ri — инвестиционный доход, Rf — безрисковая ставка, часто основанная на доходности казначейских облигаций или игнорируемая, а Rm — рыночная доходность. Ковариация и дисперсия обычно рассчитываются с помощью дневного оборота.
Основным индикатором, который будет далее использоваться в этой статье, является взвешенная по объему средняя цена (Volume Weighted Average Price, VWAP). Он рассчитывается следующим образом:
Идея VWAP заключается в измерении средней цены, по которой торгуется ценная бумага, взвешенной по объему, что отражает «истинную» стоимость торговли за определенный период. В отличие от простого среднего значения, этот индикатор придает больший вес ценам с более высокой торговой активностью, делая его более справедливым ориентиром.
Обычные области его применения в алгоритмическом трейдинге включают в себя:
- Использование его в качестве фильтра трендов.
- Использование его в качестве трейлинг-стопа.
- Использование его как генератора сигналов (например, вход при пересечении ценой линии VWAP).
Обычно мы начинаем расчет VWAP с первой свечи при открытии рынка. В приведенном выше уравнении Pi представляет собой цену i-й свечи, обычно это — цена закрытия, а Vi — объем торговли i-й свечи. Объем торговли может различаться у разных CFD-брокеров из-за отличающихся поставщиков ликвидности, но относительный вес, как правило, должен быть сопоставимым у всех брокеров.
В этой статье реализована модель оценки рисков кредитного плеча. Этот метод подразумевает риск в виде фиксированного процента от нашего баланса по каждой сделке и срабатывает при достижении уровня стоп-лосса. Диапазон стоп-лосса будет представлять собой фиксированный процент от цены актива, соответствующий его изменяющейся стоимости и волатильности. Риск по каждой сделке будет установлен с использованием круглых чисел, чтобы для простоты достичь максимальной просадки около 20%. Мы протестируем каждую стратегию за пятилетний период с 1 января 2020 года до 1 января 2025 года, чтобы собрать достаточное количество актуальных данных для оценки текущей прибыльности. Тщательный статистический анализ будет включать в себя сравнения с подходом «покупай и удерживай» на основе суммарной доли в процентах от прибыли и индивидуальных показателей эффективности.
Стратегия первая. Направление свечи открытия
Первая стратегия, которую мы рассмотрим, — это классическая стратегия прорыва уровня открытия, представленная в статье Can Day Trading Really Be Profitable? (Может ли дневная торговля действительно приносить прибыль?) компании Concretum Group. Актуальность правил генерации сигналов в стратегии заключается в улавливании краткосрочного ценового моментума и одновременном обеспечении баланса между практичностью и управлением рисками для внутридневных трейдеров. Авторы решили использовать подход ORB, чтобы использовать повышенную волатильность и направленный моментум, которые часто наблюдаются при открытии рынка. Этот период рассматривается как критическое окно, в пределах которого часто становится очевидной деятельность организаций, и розничные трейдеры могут использовать направление цены этого периода в качестве показателя для определения тренда на весь день.
После рассмотрения статьи мы выявили несколько способов усовершенствования первоначальной стратегии. Первоначальный подход использовал максимум или минимум первой пятиминутной свечи в качестве уровня стоп-лосса или уровня тейк-профита с риском 10R. Несмотря на прибыльность, этот метод оказался непрактичным для розничных трейдеров, торгующих в режиме реального времени. Жестко привязанный к первой пятиминутной свече стоп-лосс увеличивал относительные торговые издержки. Кроме того, необходимости в тейк-профите 10R не было, поскольку к концу дня мы закрываем все сделки и его уровень достигался редко. Наконец, в оригинале стратегии отсутствовал фильтр режимов, поэтому добавление скользящей средней, которая послужила бы таким фильтром, могло бы ее усовершенствовать.
Измененные нами правила генерации сигналов были следующими:
- Через пять минут после открытия рынка покупаем, если пятиминутная свеча при открытии является бычьей и закрывается выше 350-периодной скользящей средней.
- Через пять минут после открытия рынка продаем, если пятиминутная свеча при открытии является медвежьей и закрывается ниже 350-периодной скользящей средней.
- За пять минут до закрытия рынка закрываем имеющиеся позиции.
- Стоп-лосс устанавливается на уровне 1% цены от уровня входа.
- Риск 2% на каждую сделку.
Полный MQL5-код для советника:
//USTEC-M5 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 55; input double risk = 2.0; input double slp = 0.01; input int MaPeriods = 350; input int Magic = 0; int barsTotal = 0; int handleMa; double lastClose=0; double lastOpen = 0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpen()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1); if(lastClose<lastOpen&&lastClose<ma[0])executeSell(); if (lastClose>lastOpen&&lastClose>ma[0]) executeBuy(); } if(MarketClose()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpen() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour == startHour &¤tMinute==startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClose() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour == endHour && currentMinute == endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Типичная сделка будет выглядеть примерно так:
Результаты тестирования на исторических данных:
Без фильтра скользящей средней правила исходной стратегии генерировали бы одну сделку каждый торговый день. Фильтр сократил количество сделок вдвое. Поскольку средний период удержания позиции охватывает всю торговую сессию, результаты до некоторой степени отражают макроэкономическую тенденцию: длинные сделки имеют место чаще и имеют более высокий процент прибылей. В целом стратегия обеспечивает фактор прибыльности 1.23 и коэффициент Шарпа 2.81, что отражает мощную производительность. Эти простые правила менее подвержены переобучению, и это говорит о том, что надежные результаты тестирования на исторических данных, вероятнее всего, сохранятся и в реальной торговле.
Советник внушительным образом превосходит подход «покупай и держи» для USTEC за этот пятилетний период, сохраняя при этом максимальную просадку на уровне 18%, что вдвое меньше базового показателя. Кривая капитала остается плавной, за исключением только короткой стагнации в период с конца 2022 года по начало 2023 года, когда индекс USTEC столкнулся с более значительной просадкой.
Альфа: 1.6017 Бета: 0.0090
Бета в размере 0.9% показывает, что внутридневная доходность имеет корреляцию с базовым активом в размере всего лишь 0.9%, указывая на то, что преимущество стратегии обусловлено в первую очередь ее правилами, а не рыночными трендами. Просадки и доходность остаются стабильными, что свидетельствует об устойчивости к периодам экстремального режима, таким как кризис в 2020 году, вызванный пандемией COVID-19. Большинство месяцев оказались прибыльными, а месяцы просадок были умеренными, и худший случай среди них составляет 10.2%. В целом это вполне реализуемая и прибыльная стратегия.
Стратегия вторая. Следование за трендом VWAP
Вторая стратегия, которую мы рассмотрим, представляет собой стратегию следования за трендом при открытии рынка, представленную в статье Volume Weighted Average Price (VWAP) The Holy Grail for Day Trading Systems (Взвешенная по объему средняя цена — Святой Грааль для систем внутридневной торговли). Актуальность правил генерации сигналов в ней связана с использованием VWAP в качестве четкого, взвешенного по объему ориентира для определения внутридневных трендов. Длинная позиция срабатывает, когда цена приближается к уровню выше VWAP, а короткая — когда она приближается к уровню ниже уровня этого индекса, имея целью зафиксировать подтвержденный моментум и отфильтровать шум. Такая простота гарантирует внутридневным трейдерам действенные и воспроизводимые сигналы. Такой классический подход на основе следования за трендом эффективнее всего работает в условиях высокой волатильности, улавливая долгосрочные тренды и обеспечивая высокое значение отношения доходности к риску. В течение пяти часов работы фондового рынка после открытия индекс испытывает значительные колебания, обеспечивая отличную контролируемую по времени ликвидность и, соответственно, успех этой стратегии.
В исходной статье торговля велась на одноминутном таймфрейме, и авторы утверждали, что среди различных таймфреймов этот — наиболее эффективный. Однако проведенное мной лично тестирование показало, что для этой стратегии лучше подходит 15-минутный таймфрейм, вероятно, из-за более высоких торговых издержек на CFD по сравнению с ETF, что делает частые сделки менее целесообразными с практической точки зрения. Кроме того, в статье не указан уровень стоп-лосса. В нашем подходе мы включим один, поскольку используем таймфрейм более высокого уровня. Такое дополнение служит защитой от аварийных ситуаций и обеспечивает контрольный диапазон для расчета рисков. Наконец, мы добавили фильтр тренда для скользящей средней, как мы делали раньше.
Измененные нами правила генерации сигналов были следующими:
- После открытия рынка покупаем, если текущая позиция не открыта, а последнее 15-минутное закрытие находится выше VWAP и 300-периодной скользящей средней.
- После открытия рынка продаем, если нет текущих открытых позиций, а последнее 15-минутное закрытие находится ниже уровня VWAP и 300-периодной скользящей средней.
- Стоп-лосс устанавливается на уровне 0.8% цены от уровня входа.
- Риск 2% на каждую сделку.
Полный MQL5-код для советника:
//USTEC-M15 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 45; input double risk = 2.0; input double slp = 0.008; input int MaPeriods = 300; input int Magic = 0; int barsTotal = 0; int handleMa; double lastClose=0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; bool NotInPosition = true; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); int startIndex = getSessionStartIndex(); double vwap = getVWAP(startIndex); for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } if(lastClose<vwap&&NotInPosition&&lastClose<ma[0])executeSell(); if(lastClose>vwap&&NotInPosition&&lastClose>ma[0]) executeBuy(); } if(MarketClosed()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpened() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClosed() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= endHour && currentMinute >= endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Get VWAP function | //+------------------------------------------------------------------+ double getVWAP(int startCandle) { double sumPV = 0.0; // Sum of (price * volume) long sumV = 0.0; // Sum of volume // Loop from the starting candle index down to 1 (excluding current candle) for(int i = startCandle; i >= 1; i--) { // Calculate typical price: (High + Low + Close) / 3 double high = iHigh(_Symbol, PERIOD_CURRENT, i); double low = iLow(_Symbol, PERIOD_CURRENT, i); double close = iClose(_Symbol, PERIOD_CURRENT, i); double typicalPrice = (high + low + close) / 3.0; // Get volume and update sums long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } // Calculate VWAP or return 0 if no volume if(sumV == 0) return 0.0; double vwap = sumPV / sumV; // Plot the dot datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap); ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen); // Green dot ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); // Dot style ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); // Size of the dot return vwap; } //+------------------------------------------------------------------+ //| Find the index of the candle corresponding to the session open | //+------------------------------------------------------------------+ int getSessionStartIndex() { int sessionIndex = 1; // Loop over bars until we find the session open for(int i = 1; i <=1000; i++) { datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i); MqlDateTime dt; TimeToStruct(barTime, dt); if(dt.hour == startHour && dt.min == startMinute-5) { sessionIndex = i; break; } } return sessionIndex; } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Типичная сделка будет выглядеть примерно так:
Результаты тестирования на исторических данных:
по сравнению с первой стратегией прорыва при открытии, данная стратегия используется в торговле чаще, совершая в среднем более одной сделки в день. Такое увеличение обусловлено разрешение повторно входить в рынок всякий раз, когда цена снова пересекает уровень VWAP в течение часов работы рынка. Процент выигрышных сделок получается ниже — 42%, то есть ниже 50%, что характерно для подхода, основанного на следовании за трендом, с динамическим трейлинг-стопом. Такая конфигурация благоприятствует сделкам с более высоким значение отношения прибыли к риску, но повышает вероятность выхода из сделки. Коэффициент Шарпа и фактор прибыли исключительно высоки — 3.57 и 1.26 соответственно.
Эта стратегия значительно превосходит подход «купи и держи», достигая за пять лет доходности в размере 501%. При этом максимальная просадка составляет 16%, а худший период пришелся на конец 2021 года, что отличается от худшей фазы с индексом USTEC и указывает на отсутствие корреляции в показателях эффективности.
Альфа: 4.8714 Бета: 0.0985
Бета соответствует бете первой стратегии, что также указывает на низкую корреляцию с базовым активом. Примечательно, что альфа этой стратегии в три раза выше, чем у первой, при этом сохраняется аналогичная максимальная просадка. Это преимущество, вероятно, связано с более частой торговлей, более короткими периодами удержания и более значительной внутренней диверсификацией за счет как длинных, так и коротких позиций в течение одного дня. Таблица с данными за месяц подтверждает надежное функционирование: падения и прибыли распределены по месяцам равномерно и стабильно.
Стратегия третья. Прорыв полос Concretum (Concretum Bands Breakout)
Третья стратегия — стратегия прорыва шумового диапазона, с помощью которой осуществляется торговля во время открытия рынка. Впервые она была представлена в статье Beat the Market An Effective Intraday Momentum Strategy for S&P500 ETF (SPY) (Обыграйте рынок: эффективная внутридневная стратегия моментума для S&P500 ETF (SPY)), а позднее завирусилась на X/Twitter. Основные принципы, лежащие в основе правил генерирования сигналов в стратегии прорыва полос Concretum, происходят из цели выявления существенных движений цены, вызванных дисбалансом между спросом и предложением во внутридневной торговле. Стратегия использует полосы волатильности, рассчитанные на основе закрытия предыдущего дня или открытия текущего дня и скорректированные с помощью множителя волатильности, чтобы определить «Зону шума», где происходят случайные колебания цен. Правила направлены на фильтрацию рыночного шума, извлечение выгоды из высоковероятных сдвигов моментума и адаптацию к изменяющейся волатильности, гарантируя, что сделки будут соответствовать истинному началу тренда, а не мимолетным колебаниям.
Вот расчеты этих полос.
Поскольку правила генерирования сигналов в исходной статье хорошо продуманы, не будем вносить в них существенные изменения и в этой статье. Для простоты будем сохранять тот же торговый актив (USTEC) и придерживаться того же подхода к управлению рисками, что может привести к результатам, которые отличаются от подхода, изложенного в исходной статье. Правила генерации сигналов следующие:
- После открытия рынка покупаем, когда 1-минутный бар пересечет верхнюю полосу.
- После открытия рынка продаем, когда 1-минутный бар пересечет нижнюю полосу.
- Выходим из всех позиции при закрытии рынка.
- Стоп-лосс устанавливается на уровне 1% цены от уровня входа вместе с VWAP в качестве трейлинг-стопа.
- Риск 4% на каждую сделку.
Полный MQL5-код для советника:
//USTEC-M1 #include <Trade/Trade.mqh> CTrade trade; input int startHour = 18; input int startMinute = 35; input int endHour = 23; input int endMinute = 55; input double risk = 4.0; input double slp = 0.01; input int Magic = 0; input int maPeriod = 400; int barsTotal = 0; int handleMa; double lastClose=0; double lastOpen = 0; double lot = 0.1; //+------------------------------------------------------------------+ //|Initialization function | //+------------------------------------------------------------------+ int OnInit() { trade.SetExpertMagicNumber(Magic); handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,0,MODE_SMA,PRICE_CLOSE); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //|Deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { } //+------------------------------------------------------------------+ //|On tick function | //+------------------------------------------------------------------+ void OnTick() { int bars = iBars(_Symbol, PERIOD_CURRENT); if(barsTotal != bars){ barsTotal=bars; bool NotInPosition = true; double ma[]; CopyBuffer(handleMa,0,1,1,ma); if(MarketOpened()&&!MarketClosed()){ lastClose = iClose(_Symbol,PERIOD_CURRENT,1); lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1); int startIndex = getSessionStartIndex(); double vwap = getVWAP(startIndex); for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){ if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos); else NotInPosition=false; } } double lower = getLowerBand(); double upper = getUpperBand(); if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[0])executeSell(); if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[0]) executeBuy(); } if(MarketClosed()){ for(int i = PositionsTotal()-1; i>=0; i--){ ulong pos = PositionGetTicket(i); string symboll = PositionGetSymbol(i); if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos); } } } } //+------------------------------------------------------------------+ //| Detect if market is opened | //+------------------------------------------------------------------+ bool MarketOpened() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= startHour &¤tMinute>=startMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Detect if market is closed | //+------------------------------------------------------------------+ bool MarketClosed() { datetime currentTime = TimeTradeServer(); MqlDateTime timeStruct; TimeToStruct(currentTime, timeStruct); int currentHour = timeStruct.hour; int currentMinute = timeStruct.min; if (currentHour >= endHour && currentMinute >= endMinute)return true; else return false; } //+------------------------------------------------------------------+ //| Sell execution function | //+------------------------------------------------------------------+ void executeSell() { double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID); bid = NormalizeDouble(bid,_Digits); double sl = bid*(1+slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(bid*slp); trade.Sell(lot,_Symbol,bid,sl); } //+------------------------------------------------------------------+ //| Buy execution function | //+------------------------------------------------------------------+ void executeBuy() { double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK); ask = NormalizeDouble(ask,_Digits); double sl = ask*(1-slp); sl = NormalizeDouble(sl, _Digits); lot = calclots(ask*slp); trade.Buy(lot,_Symbol,ask,sl); } //+------------------------------------------------------------------+ //| Get VWAP function | //+------------------------------------------------------------------+ double getVWAP(int startCandle) { double sumPV = 0.0; // Sum of (price * volume) long sumV = 0.0; // Sum of volume // Loop from the starting candle index down to 1 (excluding current candle) for(int i = startCandle; i >= 1; i--) { // Calculate typical price: (High + Low + Close) / 3 double high = iHigh(_Symbol, PERIOD_CURRENT, i); double low = iLow(_Symbol, PERIOD_CURRENT, i); double close = iClose(_Symbol, PERIOD_CURRENT, i); double typicalPrice = (high + low + close) / 3.0; // Get volume and update sums long volume = iVolume(_Symbol, PERIOD_CURRENT, i); sumPV += typicalPrice * volume; sumV += volume; } // Calculate VWAP or return 0 if no volume if(sumV == 0) return 0.0; double vwap = sumPV / sumV; // Plot the dot datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0); string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES); ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap); ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen); // Green dot ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); // Dot style ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); // Size of the dot return vwap; } //+------------------------------------------------------------------+ //| Find the index of the candle corresponding to the session open | //+------------------------------------------------------------------+ int getSessionStartIndex() { int sessionIndex = 1; // Loop over bars until we find the session open for(int i = 1; i <=1000; i++) { datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i); MqlDateTime dt; TimeToStruct(barTime, dt); if(dt.hour == startHour && dt.min == 30) { sessionIndex = i; break; } } return sessionIndex; } //+------------------------------------------------------------------+ //| Get the number of bars from now to market open | //+------------------------------------------------------------------+ int getBarShiftForTime(datetime day_start, int hour, int minute) { MqlDateTime dt; TimeToStruct(day_start, dt); dt.hour = hour; dt.min = minute; dt.sec = 0; datetime target_time = StructToTime(dt); int shift = iBarShift(_Symbol, PERIOD_M1, target_time, true); return shift; } //+------------------------------------------------------------------+ //| Get the upper Concretum band value | //+------------------------------------------------------------------+ double getUpperBand() { // Get the time of the current bar datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0); MqlDateTime current_dt; TimeToStruct(current_time, current_dt); int current_hour = current_dt.hour; int current_min = current_dt.min; // Find today's opening price at 9:30 AM datetime today_start = iTime(_Symbol, PERIOD_D1, 0); int bar_at_930_today = getBarShiftForTime(today_start, 9, 30); if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == 0) return 0; // No valid price // Calculate sigma based on the past 14 days double sum_moves = 0; int valid_days = 0; for (int i = 1; i <= 14; i++) { datetime day_start = iTime(_Symbol, PERIOD_D1, i); int bar_at_930 = getBarShiftForTime(day_start, 9, 30); int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == 0) continue; // Skip if no valid opening price double move = MathAbs(close_HHMM / open_930 - 1); sum_moves += move; valid_days++; } if (valid_days == 0) return 0; // Return 0 if no valid data double sigma = sum_moves / valid_days; // Calculate the upper band double upper_band = open_930_today * (1 + sigma); // Plot a blue dot at the upper band level string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band); ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2); return upper_band; } //+------------------------------------------------------------------+ //| Get the lower Concretum band value | //+------------------------------------------------------------------+ double getLowerBand() { // Get the time of the current bar datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0); MqlDateTime current_dt; TimeToStruct(current_time, current_dt); int current_hour = current_dt.hour; int current_min = current_dt.min; // Find today's opening price at 9:30 AM datetime today_start = iTime(_Symbol, PERIOD_D1, 0); int bar_at_930_today = getBarShiftForTime(today_start, 9, 30); if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today); if (open_930_today == 0) return 0; // No valid price // Calculate sigma based on the past 14 days double sum_moves = 0; int valid_days = 0; for (int i = 1; i <= 14; i++) { datetime day_start = iTime(_Symbol, PERIOD_D1, i); int bar_at_930 = getBarShiftForTime(day_start, 9, 30); int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min); if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930); double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM); if (open_930 == 0) continue; // Skip if no valid opening price double move = MathAbs(close_HHMM / open_930 - 1); sum_moves += move; valid_days++; } if (valid_days == 0) return 0; // Return 0 if no valid data double sigma = sum_moves / valid_days; // Calculate the lower band double lower_band = open_930_today * (1 - sigma); // Plot a red dot at the lower band level string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS); ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band); ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed); ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2); return lower_band; } //+------------------------------------------------------------------+ //| Calculate lot size based on risk and stop loss range | //+------------------------------------------------------------------+ double calclots(double slpoints) { double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100; double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep; double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep; lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX)); lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN)); return lots; }
Типичная сделка будет выглядеть примерно так:
Результаты тестирования на исторических данных:
Стратегия осуществляет торговлю с такой же частотой, что и первая ORB-стратегия, в среднем около одной сделки каждые два торговых дня. Она не совершает ежедневную торговлю, поскольку движения цены иногда остаются в пределах шумового диапазона, то есть им не удается выйти за его пределы. Процент выигрышных сделок — менее 50%, что является результатом использования уровня VWAP в качестве динамического трейлинг-стопа. Профит-фактор (коэффициент прибыли), составляющий 1.3, и коэффициента Шарпа, составляющий 5.9, указывают на высокую доходность относительно просадки.
Эта стратегия немного превосходит подход «купи и держи», сохраняя при этом половину максимальной просадки. Однако в ней периоды существенной просадки наблюдались чаще, чем в предыдущей стратегии. Это говорит о том, что, несмотря на превосходную производительность, эта стратегия часто переживаем продолжительные спады, прежде чем достичь новых максимумов капитала.
Альфа: 1.6562 Бета: -0.1183
Бета данной стратегии составляет -11%, что указывает на небольшую отрицательную корреляцию с базовым активом. Это благоприятный результат для трейдеров, которые ищут преимущество в движении против рыночных трендов. По сравнению с двумя другими стратегиями, у этой оказывается больше месяцев просадки, около 50%, но она обеспечивает более высокую доходность в прибыльные месяцы. Эта модель предполагает, что трейдерам при реальной торговле следует готовиться к длительным периодам просадки и терпеливо ожидать фаз более высокой доходности. При значительном размере выборки в течение длительного периода времени эта стратегия остается прибыльной.
Рассуждения
В своей предыдущей статье мы исследовали построение модельных систем вместо отдельных стратегий. Ту же самую концепцию мы применили в этой статье. Все три стратегии основаны на прорывах временных диапазонов открытия фондового рынка с вариациями, которые доказали свою прибыльность. Кроме того, мы поделились идеями относительно поиска стратегического преимущества, адаптируя научные статьи с использованием собственных знаний и интуиции. Такой подход — отличный способ раскрыть надежные концепции трейдинга и расширить наши возможности понимания.
Располагая тремя выгодными стратегиями, мы теперь должны рассмотреть перспективы портфеля. Нам нужно изучить их совокупные результаты, корреляции и общую максимальную просадку, прежде чем торговать ими одновременно. В алгоритмической торговле диверсификация — это настоящий Святой Грааль. Это помогает компенсировать убытки от различных стратегий в разные периоды. В некоторой степени ваша максимальная доходность ограничивается просадкой, которую вы готовы терпеть. Сочетание различных стратегий позволяет увеличивать объем инвестиций, сохраняя при этом аналогичный уровень просадки, и это повышает доходность. Однако риск невозможно масштабировать бесконечно, поскольку минимальный риск всегда будет превышать риск отдельных сделок.
Некоторые распространенные способы достижения диверсификации включают в себя:
- Торговлю по одной и той же стратегической модели и ее распределение по разным некоррелированным активам.
- Торговлю разными моделями стратегии на одном и том же активе.
- Распределение капиталов по различным подходам к торговле, таким как опционы, арбитраж и выбор акций.
Важно понимать, что больше диверсификации не всегда к лучшему; значение имеет некоррелированная диверсификация. Например, применение одной и той же стратегии на всех крипторынках не является идеальным вариантом, поскольку большинство криптоактивов имеют высокую степень корреляции в более широком масштабе. Более того, также может быть ошибочным полагаться исключительно на диверсификацию, полученную с помощью тестирования на исторических данных, потому что корреляция зависит от периода времени, то есть, например, от дневной или месячной доходности. Вместе с тем, при серьезных изменениях рыночного режима корреляции стратегий могут неожиданно исказиться и перекоситься. Вот почему некоторые трейдеры предпочитают использовать корреляции реальных торговых результатов по сравнению с корреляциями результатов тестирования на исторических данных, чтобы оценить, снизилось ли преимущество их стратегий.
Учитывая эту информацию, представляем ниже статистику тестирования совокупной эффективности трех стратегий.
Кривые капитала и просадки наглядно демонстрируют, как различные стратегии компенсируют просадки друг друга в различные периоды. Теперь максимальная просадка составляет около 10%, что заметно ниже максимальных просадок отдельных стратегий, каждая из которых превышает 15%.
Просадки и доходность, равномерно распределенные по месяцам, что говорит об отсутствии периодов экстремального режима, которые бы оказывали непропорционально сильное или слабое влияние на результаты тестирования на исторических данных. Это имеет смысл, если учесть наличие более 3000 образцов и последовательное распределение рисков по каждой сделке.
Корреляция измеряет, насколько схожи кривые капитала, полученные при тестировании каждой стратегии на исторических данных, варьируясь от -1 для противоположного поведения до 1 для идентичного поведения, сравнивая обычно два объекта. Рассчитываем ее, используя x для временной оси кривой капитала и y для оси доходности.
Заключение
В этой статье рассмотрены три стратегии прорыва внутридневного диапазона открытия, представленные в научных публикациях компании Concretum Group. Мы начали с краткого изложения актуальности исследования, объяснив основные концепции и методологии, которые были в нем использованы. Затем мы изучили мотивы, лежащие в основе трех стратегий, выявили области для совершенствования, предоставили четкие правила генерирования сигналов, MQL5-код и статистический анализ тестирования на исторических данных. Наконец, мы провели некоторые рассуждения о процессе и ввели диверсификацию, проанализировав совокупные результаты.
Статья дает представление об истинной надежности разработки стратегии. Более глубокий статистический анализ дает более широкое представление об эффективности стратегии и ее роли в работе портфеля. Все усилия направлены на углубление понимания и укрепление уверенности перед началом реальной торговли. Читателям предлагается повторить процесс исследования и с помощью предоставленной здесь структуры разработать экспертные советники.
Таблица файлов
Название файла | Использование файла |
---|---|
ORB1.mq5. | MQL5-скрипт экспертного советника для первой стратегии |
ORB2.mq5 | MQL5-скрипт экспертного советника для второй стратегии |
ORB3.mq5 | MQL5-скрипт экспертного советника для третьей стратегии |
Перевод с английского произведен MetaQuotes Ltd.
Оригинальная статья: https://www.mql5.com/en/articles/17745
Предупреждение: все права на данные материалы принадлежат MetaQuotes Ltd. Полная или частичная перепечатка запрещена.
Данная статья написана пользователем сайта и отражает его личную точку зрения. Компания MetaQuotes Ltd не несет ответственности за достоверность представленной информации, а также за возможные последствия использования описанных решений, стратегий или рекомендаций.




- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Ps. Для ORB3 я жестко закодировал время открытия рынка на 9:30. Вы можете изменить его в этих функциях, чтобы соответствовать серверному времени открытия рынка в Нью-Йорке.
Изменение времени расчета может быть способом дальнейшей оптимизации стратегии :)
ОМГ, не могу поверить, что пропустил это. Так и должно быть:
Я искренне сожалею о небрежной ошибке. Спасибо, что так внимательно читали и указали на нее.
Не беспокойтесь, я уже объединил открытый и закрытый рынок в одну функцию.
И еще одно: вы используете данные OHLC брокера для бэктестинга, без задержки. Эти бэктесты кажутся немного оптимистичными по сравнению с бэктестами, сделанными на реальных тиковых данных со случайной задержкой на проскальзывание и реквоты.
Еще раз спасибо за ваши усилия!
Не беспокойтесь, я уже объединил открытый и закрытый рынок в одну функцию.
Еще один момент: вы используете данные OHLC брокера для бэктестинга, без задержки. Эти бэктесты кажутся немного оптимистичными по сравнению с бэктестами, сделанными на реальных тиковых данных со случайной задержкой на проскальзывание и реквоты.
Еще раз спасибо за ваши усилия!
Отличная работа над модификацией! Я обновил весь код на своем Github.
К вашему сведению, торговая логика происходит на каждом новом баре и не предполагает тикового движения. Кроме того, среднее время удержания составляет около нескольких часов, что, я думаю, не сделает проскальзывание существенной проблемой. Я бы сказал, что очень немногие брокеры предоставляют реальные тиковые данные за более чем 5 лет, и 1 мин OHLC вполне достаточно.