Sistema automático: Cruce de medias I

Este es el primero de varios posts con el que crearemos un sistema automático con STMT. STMT descarga la mayor parte de la complejidad del desarrollo de un sistema automático dejando al programador la función que le corresponde: programar el sistema. No obstante, todo lo incorporado en STMT usa conceptos que se han tratado anteriormente por lo que cualquier lector avezado en programación debiera poder desarrollar su propio marco de trabajo o implementar un sistema sin usar marco de trabajo alguno.

La interfaz y parte del código de la aplicación es muy similar a la de la entrada https://speakertrading.wordpress.com/2013/01/03/indicador-stmt-rsi/. Los parámetros del indicador RSI cambian por los del sistema.

Parte del resultado (el visual) se puede apreciar en la siguiente imagen:

CruceMedias

La aplicación muestra el gráfico junto a dos medias (una larga y otra corta), las del sistema. Aparte del resultado visual, en esta entrada veremos algo más relevante: el desarrollo de un sistema automático.

Como es mucho trabajo, otras tareas, como la parte de la operativa del sistema, las veremos en futuros posts, cuando STMT incluya soporte para las mismas.

Implementación de los indicadores que usará el sistema

Realmente no es necesario implementar un indicador (dentro del sistema puede codificarse directamente la lógica del mismo) pero será lo más usual. Usar indicadores tiene varias ventajas. Por ejemplo, el indicador puede usarse en otros sistemas, en otros indicadores y para mostrarse junto con el gráfico.

En el sistema, tan básico como conocido, del cruce de medias, poder ver los indicadores es muy útil pues en sus cruces es donde se opera:

  • Se compra cuando la media corta cruza al alza a la media larga.
  • Se vende cuando la media larga cruza al alza a la media corta.

Veamos la implementación del indicador de la media:

public class IndAverage : Indicator
{
   public IndAverage(Historic historic, int period = 30)
      : this(historic, UpdateType.BarToBar, period)
   {
   }

   public IndAverage(Historic historic, UpdateType updateType, int period = 30)
      : base(historic, "Average", updateType)
   {
      Name = "Media";
      Period = period;

      Lines.AddLine(Code, true);
   }

   [Parameter]
   public int Period { get; set; }

   public override void OnProcessBar(DateTime date)
   {
      int bar = Historic.BarIndex(date);
      if (bar + 1 >= Period)
      {
         double average = Historic.GetAverage(bar - Period + 1, bar);
         this.AddValue(date, average);

         base.OnProcessBar(date);
      }
   }
}

Es un indicador muy simple. Ya hemos implementado otros similares. Dispone de una propiedad, Period, que codifica el periodo de la media.

En el constructor podemos ver una de las novedades de STMT: UpdateType. Este valor define el modo en que queremos actualizar el histórico: barra a barra y/o tick a tick. En este ejemplo usaremos solo barra a barra: backtesting y real serán totalmente equivalentes usando este sistema. Lo más recomendable es implementar ambos tipos de actualización y dejar a quien use el sistema la selección de tipo que prefiera.

El método OnProcessBar del indicador se invoca para cada una de las barras del histórico. La media no tiene ningún secreto. Obtenemos el índice de la barra procesada y hasta que no se dispone de Period elementos, no se calcula ningún valor. Cuando se tienen suficientes datos, se calcula la media de los últimos Period valores y se añade un valor al indicador.

Es muy importante que, si se procesa una barra, se invoque a la clase base pues si no se hace, el evento ProcessBar no será lanzado.

Implementación del sistema

El sistema desarrollado, SysAverageCross, debe implementar la clase System de STMT.

Veamos su implementación:

public class SysAverageCross : STMT.System
{
   public SysAverageCross(Historic historic, int shortPeriod = 30, int longPeriod = 100)
      : this(historic, UpdateType.Default, shortPeriod, longPeriod)
   {
   }

   public SysAverageCross(Historic historic, UpdateType updateType, int shortPeriod = 30, int longPeriod = 100)
      : base(historic)
   {
      // Crear los indicadores de las medias y cargarlos
      ShortAverage = new IndAverage(historic, updateType, shortPeriod);
      ShortAverage.Name = "Media corta";

      LongAverage = new IndAverage(historic, updateType, longPeriod);
      LongAverage.Name = "Media larga";

      LongAverage.ProcessBar += _longAverage_ProcessBar;

      ShortAverage.Load();
      LongAverage.Load();

      // Indicar el final de la carga
      this.BackTesting = false;
   }

   public IndAverage ShortAverage { get; private set; }

   public IndAverage LongAverage { get; private set; }

   private void _longAverage_ProcessBar(object sender, BarEventArgs e)
   {
      // Se necesitan al menos dos barras para que haya un cruce
      int longBar = LongAverage.BarIndex(e.Date);
      if (longBar > 0)
      {
         int barDiff = ShortAverage.BarIndex(e.Date) - longBar;
         int shortBar = longBar + barDiff;
         CheckCross(e.Date, longBar, shortBar);
      }
   }

   private void CheckCross(DateTime date, int longBar, int shortBar)
   {
      double currentLong = LongAverage[longBar];
      double currentShort = ShortAverage[shortBar];
      double previousLong = LongAverage.GetPreviousDifferentValue(longBar);
      double previousShort = ShortAverage.GetPreviousDifferentValue(shortBar);

      bool greater1 = previousLong > previousShort;
      bool greater2 = currentLong > currentShort;
      if (greater1 != greater2)
      {
         if (greater1)
            ShortCrossLong(date);
         else
            LongCrossShort(date);
      }
   }

   private void ShortCrossLong(DateTime barDate)
   {
      // Media corta cruza al alza a la media larga
      SysManager.BuyMarketOrder(barDate, false);
   }

   private void LongCrossShort(DateTime barDate)
   {
      // Media larga cruza al alza a la media corta
      SysManager.SellMarketOrder(barDate, true);
   }
}

El sistema tiene dos propiedades, de tipo IndAverage, que corresponden a la media larga y corta.

Veamos el constructor:

// Crear los indicadores de las medias y cargarlos
ShortAverage = new IndAverage(historic, updateType, shortPeriod);
ShortAverage.Name = "Media corta";

LongAverage = new IndAverage(historic, updateType, longPeriod);
LongAverage.Name = "Media larga";

LongAverage.ProcessBar += _longAverage_ProcessBar;

ShortAverage.Load();
LongAverage.Load();

// Indicar el final de la carga
this.BackTesting = false;

Se crean los dos indicadores, con los valores recibidos en los parámetros. El sistema cambia el nombre de sus indicadores para poder distinguirlos. La propiedad Code es la misma en ambos (Average) pero Name permite distinguirlos.

A continuación indicamos que queremos recibir una notificación cada vez que se procese una barra en el indicador. Aquí solo interesa la media larga pues en el intervalo en el que solo hay línea corta no puede haber cruces. Con cada barra en la que haya media larga, comprobaremos los cruces.

Cargamos la media corta y luego la larga. El orden es importante pues al cargar la línea larga, la corta debe estar ya cargada para poder acceder a ella en el evento.

Cuando la carga finaliza, se indica que acaba el modo de backtesting.

private void _longAverage_ProcessBar(object sender, BarEventArgs e)
{
   // Se necesitan al menos dos barras para que haya un cruce
   int longBar = LongAverage.BarIndex(e.Date);
   if (longBar > 0)
   {
      int barDiff = ShortAverage.BarIndex(e.Date) - longBar;
      int shortBar = longBar + barDiff;
      CheckCross(e.Date, longBar, shortBar);
   }
}

En el procesamiento de las barras, se obtiene el índice de la barra. Si es cero, el indicador solo tiene un valor y, por tanto, no puede haber cruce. A partir de dos valores se buscan cruces.

La variable barDiff contiene la diferencia entre los periodos de los indicadores. Se podría usar ese valor, la diferencia de los periodos, en lugar del aquí expuesto pero por motivos didácticos, se usa este código. La misma fecha tiene distintos índices en cada indicador. El método CheckCross es quien comprueba el cruce. Se le indica la fecha y el índice de la barra a comprobar de cada indicador.

private void CheckCross(DateTime date, int longBar, int shortBar)
{
   double currentLong = LongAverage[longBar];
   double currentShort = ShortAverage[shortBar];
   double previousLong = LongAverage.GetPreviousDifferentValue(longBar);
   double previousShort = ShortAverage.GetPreviousDifferentValue(shortBar);

   bool greater1 = previousLong > previousShort;
   bool greater2 = currentLong > currentShort;
   if (greater1 != greater2)
   {
      if (greater1)
         ShortCrossLong(date);
      else
         LongCrossShort(date);
   }
}

Las dos primeras líneas obtienen el valor de la barra actual de ambas medias. En versiones anteriores de STMT la sintaxis tendría que ser LongAverage.MainLine[longBar] o LongAverage.Lines[0][longBar].

Para obtener el valor anterior y poder determinar si hay un cruce, se usa el método GetPreviousDifferentValue. Esto es así porque si el valor anterior es igual al actual, hay que seguir mirando valores anteriores hasta encontrar uno que cambie.

Para terminar, podemos ver la recomendación dada en el post de STMT 2.1: Crear funciones con la parte operativa del sistema. Tanto cuando se produce un cruce largo-corto como cuando se produce uno corto-largo, se llama a las funciones ShortCrossingLong/LongCrossingShort.

private void ShortCrossLong(DateTime barDate)
{
   // Media corta cruza al alza a la media larga
   SysManager.BuyMarketOrder(barDate, false);
}

En estos métodos simplemente se ejecuta la orden que corresponde. La parte operativa se canaliza a través de SysManager. Realmente se puede operar a través de _visualChart.Trader pero SysManager tiene en cuenta cuando el sistema está en backtesting para no lanzar orden en ese caso y, próximamente, se encargará también de tareas de interés relacionadas con la operativa: conocer el histórico de órdenes del sistema, si estamos alcistas o bajistas, con cuantos contratos y todo tipo de cálculos del sistema que puedan servir para tomar decisiones en el propio sistema.

Implementación de la interfaz de usuario

En la parte de la interfaz, vamos a trabajar con bastante código ya conocido. Nuestra ventana contiene estas variables:

private VisualChart _visualChart;

private HistoricSerie _historic;

private SysAverageCross _system;

Se ha agregado la variable del sistema. El resto son la siempre presente VisualChart y el histórico sobre el que se aplica el sistema.

El código asociado al botón de cargar comienza con la típica limpieza de valores previos y la obtención de los valores configurados por el usuario en los controles de la ventana (método GetGuiValues).

Estos valores se usan para crear el histórico y el sistema. Después, LoadSeriesInChart, se encarga de mostrar en el gráfico el histórico y los indicadores del sistema.

private void ButtonCargar_Click(object sender, EventArgs e)
{
   // Limpiar el gráfico
   ChartGrafico.Series.Clear();

   if (_historic != null)
   {
      _historic.Dispose();
      _historic = null;
   }

   string symbol;
   DateTime startDate, endDate;
   int compressionUnits, averageShort, averageLong;
   CompressionType compressionType;
   if (!GetGuiValues(out symbol, out compressionType, out compressionUnits, out startDate, out endDate, out averageShort, out averageLong))
      return;

   _historic = new HistoricSerie(_visualChart, symbol, compressionType, compressionUnits, startDate, endDate, UpdateType.BarToBar);

   _system = new SysAverageCross(_historic, averageShort, averageLong);

   LoadSeriesInChart(symbol);
}

Veamos la carga de los históricos:

private void LoadSeriesInChart(string symbol)
{
   Series symbolSerie = AddChartSerie(symbol);

   bool velas = ComboBoxTipoBarras.SelectedIndex == 1;
   symbolSerie.ChartType = velas ? SeriesChartType.Candlestick : SeriesChartType.Stock;

   int numeroBarras = _historic.Count;
   for (int i = 0; i < _historic.Count; i++)
   {
      DateTime date = _historic.GetDate(i);
      symbolSerie.Points.AddXY(date, _historic.Max[i]);

      DataPoint barraGrafico = symbolSerie.Points[symbolSerie.Points.Count - 1];
      barraGrafico.YValues[1] = _historic.Min[i];
      barraGrafico.YValues[2] = _historic.Open[i];
      barraGrafico.YValues[3] = _historic.Close[i];
   }

   Line longLine = _system.LongAverage;
   Line shortLine = _system.ShortAverage;

   Series longSerie = AddChartSerie(_system.LongAverage.Name);
   Series shortSerie = AddChartSerie(_system.ShortAverage.Name);

   foreach (DateTime date in _historic.Dates)
   {
      int index = longSerie.Points.AddXY(date, longLine[date]);
      if (double.IsNaN(longLine[date]))
         longSerie.Points[index].IsEmpty = true;

      index = shortSerie.Points.AddXY(date, shortLine[date]);
      if (double.IsNaN(shortLine[date]))
         shortSerie.Points[index].IsEmpty = true;
   }
}

Es todo código ya utilizado. Aquí vemos la posibilidad de crear funciones propias (quizá las añada en STMT) para cargar un histórico o indicador en un gráfico. Esas funciones se podrían usar para simplificar la carga de los indicadores, históricos y todo lo que se nos ocurra.

Quizá se echa en falta conocer donde se han producido los cruces. Dejo como tarea la creación de sendos eventos en SysAverageCross que puedan usarse en la ventana y mostrar los cruces en un control de lista.

Conclusiones

Como hemos visto, la nueva versión de STMT permite crear tanto un indicador como un sistema con muy pocas líneas de código y centrándose en lo importante, la estrategia del sistema en lugar de pelear con limitaciones impuestas por un marco de trabajo.

Queda mucho por hacer, como la parte operativa del sistema antes comentada, pues saber con cuantos contratos se está abierto en la posición actual es interesante, conocer el beneficio actual y un sinfín de cálculos que pueden aplicarse a los negocios del sistema.

Desde aquí os animo a comentar las sugerencias que tengáis pues a cálculos como GetAverage o GetSum se le pueden añadir muchos otros que os parezcan de interés. También que funciones de operativa de sistemas os parecen necesarias (GetCurrentContracts, GetMarketPosition…). Recordemos que no tenemos limitación alguna para desarrollar nuestro marco de trabajo.

¡Espero vuestros comentarios y ver pronto sistemas vuestros funcionando!

Archivo Zip Descargar C# CruceDeMedias.zip.

Archivo Zip Descargar VB .NET CruceDeMedias_vb.zip.

Si te ha gustado la entrada, considera hacer una donación Donar. ¿Por qué donar?

Esta entrada fue publicada en Código fuente, Sistemas automáticos, STMT y etiquetada , , , . Guarda el enlace permanente.

Deja un comentario