Финансовая лаборатория

Биржевая торговля и торговые роботы


Всем известна знаменитая торговая система Александра Элдера под названием "Три экрана". Смысл этой торговой стратегии заключается в том, что необходимо смотреть на рынок через призму трех разных временных измерений. Сначала на недельном графике определяется общая тенденция, на дневном графике определяются точки входа, а на внутридневном - моменты входа. Эта система является ярким примером работы с разными таймфреймами в одной стратегии.

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

Анализ разных таймфреймов

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

Например для дневных данных можно создать индикаторы на основе недельных или месячных графиков, а данные с внутридневных графиков могут быть сжаты как внутри дня (например с 5-ти минуток до часа) так и до дневных, недельных или даже месячных баров.

Анализ данных с учетом разных таймфреймов (мультитаймфреймовый анализ) позволяет Вам получить доступ к данным и индикаторам, которые базируются на таймфрейме более высокого порядка, но создавать трейда на графике базовой шкалы.

К примеру, предположим, что Вы хотели бы чтобы сделки в Вашей торговой системе осуществлялись в сторону лежащего в основе тренда, который измеряется с помощью скользящей средней на недельных барах. В программе Weatlh-Lab 6 Вы можете определить этот недельный тренд и торговать в его направлении на дневном (или даже внутридневном) интервале.

Как: с помощью программного кода определить шкалу графика

Для того, чтобы определить таймфрейм графика (баров), Вы можете использовать структуру BarDataScale для того, чтобы получить значения простого типа, которые содержит свойства Bars.Scale (Tick - тики, Second - секунды, Minute - минуты, Daily - дни, Weekly - недели, Monthly - месяцы, Quarterly - кварталы, Yearly - годы) и свойство Bars.Interval (20-tick, 30-second, 60-minute). При этом нужно иметь в виду что BarInterval возвращает нулевое значение для не внутридневных шкал.

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   // Получаем информацию об интервале баров графика и типе шкалы графика.

   BarDataScale ds = Bars.DataScale; //создаем переменную ds типа BarDataScale

   if( ds.BarInterval == 0 ) //если не интрадейные данные

      PrintDebug( ds.Scale );  //печатаем тип шкалы графика

   else

      PrintDebug( ds.BarInterval + "-" + ds.Scale ); //печатаем информацию об интервале графика и типе шкалы
}

В результате появится вот такое сообщение с информацией о шкале и интервале:

Сообщение о шкале и интервале

Как: получить доступ к данным с верхнего таймфрейма на внутридневной шкале (Intraday - Intraday)

Для получения доступа к данным со старшего таймфрейма на внутридневном графике нужно проделать следующие шаги:

  • Используйте метод SetCcaleCompressed() с требуемым периодом сжатия.
  • После того, как Вы закончите работать с данными старшего таймфрейма - вызовите метод RestoreScale().
  • Далее для получения нового синхронизированного с исходным таймфреймом ряда данных нужно вызвать метод Synchronize().

Этот пример демонстрирует как получить доступ к данным со старшей внутридневной шкалы (для целей иллюстрации рассмотрим 120-ти минутную шкалу) из шкалы с меньшим таймфреймом (в данном случае 15-ти минутной). Мы получим две разных скользящих средних с разными периодами на каждой из шкал и определим их пересечения (как снизу вверх, так и сверху вниз).

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   // Выясняем - является ли исходная шкала внутридневной (выполнение _стратегии требует внутридневных данных).
   if( Bars.BarInterval == 0 )

{
    DrawLabel(PricePane, "Данная стратегия работает только с внутридневными данными", Color.Red);
    return;
}

 // Выясняем - достаточно ли данных для создания 60-ти барного 120-ти минутного индикатора?

   int barsRequired = 120 * (60 + 1) / Bars.BarInterval;

   if (Bars.Count < barsRequired)
{
      DrawLabel(PricePane, "Для выполнения стратегии необходимо минимум " + barsRequired.ToString() + " баров", Color.Red);
      return;
}

   // Получаем две скоьзящие средние с периодом 20 и 60 баров на исходной шкале

   DataSeries SMA20 = SMA.Series( Close, 20 );
   DataSeries SMA60 = SMA.Series( Close, 60 );

   //Меняем сжатие шкалы до 120-ти минут

   SetScaleCompressed( 120 );

   // Получаем две скоьзящие средние с периодом 20 и 60 баров на новой (120-ти минутной) шкале

   DataSeries SMA20_120 = SMA.Series( Close, 20 );
   DataSeries SMA60_120 = SMA.Series( Close, 60 );

   //Меняем шкалу назад к оригинальному таймфрейму
   RestoreScale();

   SMA20_120 = Synchronize( SMA20_120 ); //синхронизируем данные с исходной шкалой
   SMA60_120 = Synchronize( SMA60_120 ); //синхронизируем данные с исходной шкалой

   //Отображаем на графике скользящие средние

   PlotSeries( PricePane, SMA20, Color.DarkRed, WealthLab.LineStyle.Solid, 2 );
   PlotSeries( PricePane, SMA60, Color.DarkGreen, WealthLab.LineStyle.Solid, 2 );
   PlotSeries( PricePane, SMA20_120, Color.Red, WealthLab.LineStyle.Solid, 2 );
   PlotSeries( PricePane, SMA60_120, Color.Green, WealthLab.LineStyle.Solid, 2 );

   // Выделяем на графике пересечения (сверху вниз и снизу вверх)

   for(int bar = barsRequired; bar < Bars.Count; bar++)
   {

      if( CrossOver( bar, SMA20, SMA60 ) )
         AnnotateChart( PricePane, Bars.DataScale + "Исходная шкала: снизу вверх", bar, SMA20[bar], Color.Red );

      if( CrossOver( bar, SMA20_120, SMA60_120 ) )
         AnnotateChart( PricePane, "120 Min: снизу вверх", bar, SMA20_120[bar], Color.Green);

      if( CrossUnder( bar, SMA20, SMA60 ) )
         AnnotateChart( PricePane, Bars.DataScale + "Исходная шкала: сверху вниз", bar, SMA20[bar], Color.Red );

      if( CrossUnder( bar, SMA20_120, SMA60_120 ) )
         AnnotateChart( PricePane, "120 Min: сверху вниз", bar, SMA20_120[bar], Color.Green);
   }
}

Вот такой график получится в итоге:

Сжатие внутридневных данных

Пример сжатия таймфрейма внутри дня

Как: получить доступ к дневным данным с внутридневного графика (Intraday - Daily)

Пример демонстрирует как вычислить так называемые уровни Пивот ("Floor Trader Pivots"), известные также как дневные линии поддержки и сопротивления. Для расчета этих уровней используется сжатие внутридневных данных до дневной шкалы. Как и всегда при работе с несколькими таймфреймами отображение индикаторов и выполнение сделок должно осуществляться на искодной шкале с меньшим таймфреймом.

синхронизация данных, сдвинутых по шкале времени вправо

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

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   const int NumTicks = 2;  // Количество тиков после пивот-уровня

    SetScaleDaily();

    DataSeries hPivot = AveragePriceC.Series(Bars);
    DataSeries hDayHigh = High;
    DataSeries hDayClose = Close;
    DataSeries hDayLow = Low;
    RestoreScale();

    hPivot = Synchronize( hPivot ) >> 1;
    hDayHigh = Synchronize( hDayHigh ) >> 1;
    hDayClose = Synchronize( hDayClose ) >> 1;
    hDayLow = Synchronize( hDayLow ) >> 1;

    hPivot.Description = "Pivot";

    DataSeries hR1 =  ( 2 * hPivot - hDayLow );
    hR1.Description = "R1";

    DataSeries hS1 = ( 2 * hPivot - hDayHigh );
    hS1.Description = "S1";

    DataSeries hR2 = ( hPivot - ( hS1 - hR1 ) );
    hR2.Description = "R2";

    DataSeries hS2 = ( hPivot - ( hR1 - hS1 ) );
    hS2.Description = "S2";

    HideVolume();

    PlotSeries( PricePane, hR2, Color.Maroon, LineStyle.Dotted, 1);
    PlotSeries( PricePane, hR1, Color.Olive, LineStyle.Solid, 1 );
    PlotSeries( PricePane, hPivot, Color.Blue, LineStyle.Solid, 1 );
    PlotSeries( PricePane, hS1, Color.Fuchsia, LineStyle.Dotted, 1 );
    PlotSeries( PricePane, hS2, Color.Red, LineStyle.Solid, 1 );

   // Значение прорыва уровня (будет добавлено и вычтено из уровней поддержки/сопротивления)

    double boVal = Bars.SymbolInfo.Tick * NumTicks; //в нашем случае - 2 минимальных шага цены (тика)

   // Find the StartBar of the 2nd day
   int StartBar = 0;
   int cnt = 0;

    for(int bar = 0; bar < Bars.Count; bar++)
    {
        if( Bars.IntradayBarNumber(bar) == 0 ) cnt++;
        if( cnt == 2 )
        {
            StartBar = bar;
            break;
        }
    }

    for(int bar = StartBar; bar < Bars.Count; bar++)
    {

        // Не торговать на первых двух барах в начале дня (на барах с номерами "0" и "1"

        if( Bars.IntradayBarNumber(bar) < 2 ) continue; //переходим к следующей итерации цикла

        if (IsLastPositionActive)
        {
            Position p = LastPosition;

            if( p.PositionType == PositionType.Long )
            {
            if( !SellAtStop(bar + 1, p, hS1[bar], "StopLoss" ) )

                SellAtLimit(bar + 1, p, hR2[bar], "PftTgt" );
            }

            else if( !CoverAtStop(bar + 1, p, hR1[bar], "StopLoss" ) )

            CoverAtLimit(bar + 1, p, hS2[bar], "PftTgt" );
        }

        else if( Bars.IsLastBarOfDay( bar ) == false ) //если не является последним баром дня

        {
            bool NewPositionEntered = false; //произведен ли вход в новую позицию

            if( Close[bar] < hR1[bar] )

            NewPositionEntered = BuyAtStop(bar + 1, hR1[bar] + boVal) != null;

            if( !NewPositionEntered )

            if( Close[bar] > hS1[bar] )

            ShortAtStop(bar + 1, hS1[bar] - boVal);
        }
    }
}

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

Разворотные уровни пивот

Пример использования сжатия данных до дневного таймфрейма

Как: наложить дневные свечи на внутридневной график

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

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   HideVolume();
   // Убедимся в том, что работаем с внутридневными данными:
   if ( !Bars.IsIntraday ) return;

   // Получаем дневные бары:

   SetScaleDaily();
   Bars iDay = Bars; //присваиваем переменной iDay типа Bars бары дневного графика
   RestoreScale();

   iDay = Synchronize( iDay );      // Стандартная (быстрая) синхронизация

   for(int bar = 1; bar < Bars.Count; bar++)
   {

      if ( Bars.IsLastBarOfDay(bar) || (bar == Bars.Count-1) ) //если текущий бар является последним баром дня или последним баром на графике

      {

         int o = bar - Bars.IntradayBarNumber( bar ); // рассчитываем номер первого бара текущего дня

         double p1 = Open[o]; //Цена открытия первого бара текущего дня
         double p2 = Close[bar]; //Цена закрытия текущего дня
         double p3 = iDay.High[bar]; //максимальная цена текущего дня
         double p4 = iDay.Low[bar]; //минимальная цена текущего дня

         double[] rectangle = { o, p1, o, p2, bar, p2, bar, p1 }; // создаем массив с координатами 4-х точек, рисующих прямоугольник тела свечи (против часовой стрелки)

         int avgBar = bar - Bars.IntradayBarNumber(bar)/2; //находим середину дня:

         if( p2 > p1 ) //если цена закрытия больше цены открытия (если свеча белая)
        {
            DrawPolygon( PricePane, Color.Silver, Color.Empty, WealthLab.LineStyle.Invisible, 2, true, rectangle ); //рисуем прямоугольник, используя заготовленный массив (тело свечи)
            DrawLine( PricePane, avgBar, p4, avgBar, p1, Color.Silver, WealthLab.LineStyle.Solid, 3 ); //рисуем линию от минимума до окткрытия (нижнюю тень свечи)
            DrawLine( PricePane, avgBar, p3, avgBar, p2, Color.Silver, WealthLab.LineStyle.Solid, 3 ); //рисуем линию от максимума до закрытия (верхнюю тень свечи)
         }

         else  //если цена закрытия меньше цены открытия (если свеча черная)
         {
            DrawPolygon( PricePane, Color.Silver, Color.Silver, WealthLab.LineStyle.Invisible, 2, true, rectangle ); //рисуем тело свечи (закрашивая его)
            DrawLine( PricePane, avgBar, p3, avgBar, p4, Color.Silver, WealthLab.LineStyle.Solid, 3 ); //рисуем линию от низа до верха (сразу две тени, т.к. тело свечи непрозрачно)
         }
      }
   }
}

Вот так будет выглядеть этот график.

Рисование дневных свечей на часовом графике

Так выглядят дневные свечи на часовом графике

Тут сразу видно и внутридневной график и свечи дневного графика. Это демонстрирует мощность работы программы Wealth-Lab.

Intraday - Weekly, Monthly

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

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

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

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   // Ищем недельные уровни поддержки - сопротивления на внутридневных графиках
   // Уровни поддержки / сопротивления это скользящие средние максимальных и минимальных цен недельных баров

   if( Bars.Scale != BarScale.Minute ) return; //если шкала не внутридневная - ничего не делаем

   SetScaleWeekly(); //сжимаем шкалу до недельной

   DataSeries WeeklySup = SMA.Series( Bars.Low, 5 );
   DataSeries WeeklyRes = SMA.Series( Bars.High, 5 );

   RestoreScale(); //восстанавливаем шкалу в первоначальное состояние

   WeeklySup = Synchronize( WeeklySup );
   WeeklySup.Description = "Недельный уровень поддержки";

   WeeklyRes = Synchronize( WeeklyRes );
   WeeklyRes.Description = "Недельный уровень сопротивления";

   HideVolume();

   for(int bar = 0; bar < Bars.Count; bar++)

      SetBackgroundColor( bar, Color.LightSkyBlue );

   PlotSeriesFillBand(PricePane, WeeklySup, WeeklyRes, Color.Black, Color.White, LineStyle.Solid, 2);  //рисуем на внутридневном графике уровни
}

График этой стратегии будет выглядеть так:

Недельные уровни поддержки-сопротивления

Рисование недельных уровней поддержки-сопротивления на часовом графике

Как: получить доступ к ряду данных индикатора, построенного по недельным барам из графика дневного таймфрейма (Daily - Weekly)

Для того, чтобы проиллюстрировать идею, следующая стратегия покупает когда дневная MACD пересекает нулевой уровень снизу вверх и продает когда недельный MACD пересекает нулевой уровень сверху вниз.

Пример (как выполнить пример приведенного кода)...


protected override void Execute()
{
   /*
   Открываем длинную позицию при пересечении дневным MACD нулевого уровня снизу вверх
   Позиция закрывается когда недельный MACD пересекает нулевой уровень сверху вниз
   */

   if( Bars.Scale != BarScale.Daily ) return; //если шкала не дневная, то ничего не делаем.

   SetScaleWeekly(); //сжимаем шкалу до недельного уровня

   DataSeries WeeklyMACD = MACD.Series( Bars.Close );

   RestoreScale(); //восстанавливаем шкалу в исходное (дневное) состояние

   WeeklyMACD = Synchronize( WeeklyMACD );
   WeeklyMACD.Description = "Weekly MACD";

   for(int bar = 60; bar < Bars.Count; bar++)
   {
      if (IsLastPositionActive)
      {
         if( CrossUnder( bar, WeeklyMACD, 0 ) )

            SellAtMarket( bar+1, LastPosition, "Недельный MACD спустился ниже 0" );
      }

      else

      {

         if( CrossOver( bar, MACD.Series( Bars.Close ), 0 ) )

            BuyAtMarket( bar+1, "Дневной MACD поднялся выше 0" );
      }
   }

   HideVolume();

   ChartPane WeeklyMACDPane = CreatePane( 30, false, true );
   DrawHorzLine( WeeklyMACDPane, 0, Color.Red, WealthLab.LineStyle.Solid, 2 );
   PlotSeries( WeeklyMACDPane, WeeklyMACD, Color.Blue, WealthLab.LineStyle.Histogram, 2 );
}

Как: получить доступ к ряду данных индикатора, построенного по месячным барам из графика дневного таймфрейма (Daily - Monthly)

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

  • вызовите методы SetScaleWeekly() и SetScaleMonthly() соответственно;
  • после того, как Вы закончите оперировать данными в старшем таймфрейме, вызовите метод RestoreScale();
  • следующим шагом станет вызов нового синхронизированного ряда данных (DataSeries) либо нового объекта баров (Bars), которые активируются при вызове метода Synchronize().

Здесь приводится код простой стратегии, которая использует в приказе сразу три таймфрейма (дневной, недельный и месячный) для того, чтобы принять решение.

Если и недельный и месячный индикатор CCI больше чем 100, а дневной CCI пересекает уровень 100 снизу вверх, то создается приказ на вход на следующем баре по рынку. Эта длинная позиция существует до тех пор, пока дневной CCI не пересечет уровень -200 сверху вниз.

Пример (как выполнить пример приведенного кода)...


using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using WealthLab;
using WealthLab.Indicators;

namespace WealthLab.Strategies
{
    public class CCItriplex : WealthScript
    {
        private StrategyParameter cciPeriod; // parameter for period

        public CCItriplex()
        {
            cciPeriod = CreateParameter("CCI Period", 10, 10, 60, 5);
        }

        protected override void Execute()
        {
            int Period = cciPeriod.ValueInt;

            if (Bars.Scale != BarScale.Daily) return; //если шкала не является дневной, то ничего не делаем

            DataSeries cciD = CCI.Series(Bars, Period); //дневной CCI

            SetScaleWeekly(); //сжимаем шкалу до недельной

            DataSeries cciW = CCI.Series(Bars, Period); //недельный CCI

            RestoreScale(); //приводим шкалу в первоначальный вид

            SetScaleMonthly(); //сжимаем шкалу до месячной

            DataSeries cciM = CCI.Series(Bars, Period); //месячный CCI

            RestoreScale(); //приводим шкалу в первоначальный вид

            cciW = Synchronize(cciW);
            cciW.Description = "Недельный CCI";

            cciM = Synchronize(cciM);
            cciM.Description = "Месячный CCI";

            ChartPane cciPane = CreatePane(50, true, true);

            PlotSeries(cciPane, cciD, Color.Green, WealthLab.LineStyle.Solid, 1);
            PlotSeries(cciPane, cciW, Color.Blue, WealthLab.LineStyle.Solid, 1);
            PlotSeries(cciPane, cciM, Color.Red, WealthLab.LineStyle.Solid, 1);

            for (int bar = Period; bar < Bars.Count; bar++)
            {
                if (IsLastPositionActive)
                {
                   if (CrossUnder(bar, cciD, -200))

                       SellAtMarket(bar + 1, LastPosition);

                }

                else if ((cciW[bar] >= 100) && (cciM[bar] >= 100) && CrossOver(bar, cciD, 100))

                    BuyAtMarket(bar + 1);
            }
        }
    }
}

Вот так будет выглядеть график:

Сегодня мы рассмотрели очень большую и интересную тему - анализ данных в разрезе разных таймфреймов в рамках одной торговой стратегии. Овладев этими несложными методами работы с разными таймфреймами и научившись сжимать шкалу графика до нужного Вам состояния Вы сможете создавать стратегии позволяющие Вам видеть одновременно несколько измерений рынка и ситуации на нем. Следующий раз мы рассмотрим довольно специфический класс WealthLab.Rules. Чтобы не пропустить новые статьи - подписывайтесь на обновления по RSS.

Комментариев: 2

  1. Максим пишет:

      Дмитрий, поясни, пожалуйста, зачем нужен при расчете уровней пивот этот цикл?
     // Find the StartBar of the 2nd day
       int StartBar = 0;
       int cnt = 0;
     
        for(int bar = 0; bar < Bars.Count; bar++)
        {
            if( Bars.IntradayBarNumber(bar) == 0 ) cnt++;
            if( cnt == 2 )
            {
                StartBar = bar;
                break;
            }
        }
     
    Ведь мы этого этого и так рассчитали все уровни.

Оставить комментарий