Informes con .NET

En esta entrada se va a mostrar cómo personalizar listados/informes en una tabla. Si se automatiza un sistema, es muy útil poder mostrar los resultados en una tabla. Report

Funciones de la tabla:

  • Ordenación por una o varias columnas a la vez. Ejemplo: Se puede ordenar por beneficio y, después, por número de barras de la operación. Se puede ordenar por tipo de orden (compra/venta) y luego por beneficio para ver los beneficios en función del tipo de operación.
  • Tooltip para mostrar información de las celdas seleccionadas.
  • Posibilidad de colorear valores con mayor o menor intensidad en función de su valor. Ejemplo: Mostrar en rojo los resultados negativos y en azul los positivos, con mayor intensidad los valores extremos.
  • Posibilidad de copiar las celdas seleccionadas y pegarlas en Excel.
  • Posibilidad de cambiar el orden de las columnas en tiempo de ejecución. Ejemplo: Se pueden situar juntas columnas que contienen datos que queremos comparar.
  • Inmovilizar columnas. Ejemplo: Inmovilizar la columna de la fecha y hacer scroll para ver otras columnas sin que desaparezca la de la fecha.

Este ejemplo está centrado en una lista de trades pero las posibilidades son infinitas, lo que necesite cada uno a nivel particular.

GridViewBase

La clase GridViewBase adjunta, amplía el control DataGridView de .NET con las funciones antes comentadas. Algunas de las funciones son propias del control, no es necesario implementar nada para disponer de ellas.

Veamos el código por fragmentos para entender cómo funciona.

protected GridViewBase()
{
   AllowUserToAddRows = false;
   AllowUserToDeleteRows = false;
   AllowUserToOrderColumns = true;
   BackgroundColor = SystemColors.Window;
   ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize;

   EditMode = DataGridViewEditMode.EditProgrammatically;
   RowHeadersVisible = false;

   _sorter = new GridSorter();
}

En el constructor se asignan algunas propiedades de la tabla para impedir que se puedan añadir/eliminar filas desde la aplicación, permitir que se puedan mover las columnas (cambiar su orden), impedir que se pueda modificar el contenido de las celdas (de solo lectura) y algunos detalles estéticos.

Al final, se crea un objeto GridSorter, usado para ordenar las filas de la tabla cuando se hace clic en una cabecera.

public int MaxSortColumns
{
   get
   {
      return _sorter.MaxSortColumns;
   }

   set
   {
      _sorter.MaxSortColumns = value;
   }
}

Esta propiedad permite limitar el número de columnas máximo por las que se ordena. Cuando se ordena por una columna A, si luego se ordena por otra B, se mantiene la ordenación de A dentro de la ordenación de B, es decir, se ordena primero por B y luego (los valores iguales de B) por A. Un valor de 1 equivale a ordenar siempre solo por una columna. El valor 0 significa que se ordena por cualquier número de columnas, no hay límite.

Un valor de 2 o 3 suele ser lo más habitual. Por encima de esto, la ordenación ya empieza a perder significado aunque depende mucho de las columnas existentes.

protected override void OnColumnHeaderMouseClick(DataGridViewCellMouseEventArgs e)
{
   DataGridViewColumn column = Columns[e.ColumnIndex];

   bool ctrl = ((ModifierKeys & Keys.Control) == Keys.Control);
   _sorter.Apply(column, ctrl);

   Sort();

   column.SortMode = DataGridViewColumnSortMode.Programmatic;

   base.OnColumnHeaderMouseClick(e);
}

Cuando se hace clic en una columna, se ordena por ella. Se le indica al “ordenador” la columna y si la tecla Control está pulsada. La tecla control no cambia la prioridad de la columna por la que se ordena, solo cambia la dirección. Por ejemplo, si se hace clic en la columna A y luego en la B, se ordena primero por B y luego por A, de menor a mayor en ambos casos. Si deseamos ordenar los valores de A de mayor a menor pero queremos que se mantenga la prioridad (primero A y luego B) basta con hacer clic en A manteniendo la tecla control pulsada.

public void Sort()
{
   Sort(_sorter);

   DataGridViewColumn column = _sorter.GetColumntSort();
   if (column != null)
   {
      foreach (DataGridViewColumn aux in Columns)
      {
         aux.HeaderCell.SortGlyphDirection = aux == column ?
            _sorter.GetColumnSortOrder(column) :
            SortOrder.None;
      }
   }
}

Esta función ordena los valores de la tabla usando el “ordenador” de la clase. También se pinta la flecha que indica la columna que está ordenando y en qué dirección se está haciendo.

   public string UpdateSelectionTooltip()

Esta función crea el tooltip de las celdas seleccionadas. Se agrupan los resultados por columnas. Por ejemplo, si en la columna del tipo de operación hay seleccionadas 3 compras y dos ventas, aparecerá una línea que indique que hay 3 compras y 2 ventas. Si las columnas son de resultados de negocios, se puede mostrar el mejor y peor resultado junto con el promedio o la suma total.

Como en esta clase es imposible conocer si se trata de negocios, órdenes… no es posible rellenar los valores. Por eso se invoca aquí a la función GetSelectionTooltip que permite a la clase que corresponda (la del grid de negocios, de órdenes…) confeccionar el texto a mostrar.

Se incluyen otras funciones de utilidad para confeccionar los tooltips. Por ejemplo, GetDoubleRange permite crear un texto con el valor mínimo y máximo del rango indicado incluyendo si se desea la media y la suma total de los valores. Se puede examinar el código de GetIntegerRange, GetDatesRange, GetDistinctValuesCount… e incluso crear funciones propias para confeccionar fácilmente el tooltip a mostrar.

   protected static Color GradientColor(Color minColor, Color maxColor, double percent)

Esta función se utiliza para colorear rangos de valores. Dados dos colores extremos, con los distintos valores de percent (entre 0 y 1) obtenemos colores más cercanos a cada extremo. El valor 0 devuelve minColor, el 1 maxColor y 0.5 el color intermedio de ambos.

Un ejemplo

A continuación se muestra un ejemplo de uso para ver cómo utilizar la clase anterior.

   public class ExampleGridView : GridViewBase

Hay que definir una clase que derive de GridViewBase. Nuestro ejemplo, tiene las columnas mostradas en la imagen inicial, típicas de un negocio.

public ExampleGridView()
{
   StatusColumn = CreateTextBoxColumn("Status", "statusColumn", 67, true, true);

   OperationColumn = CreateTextBoxColumn("Op.", "operationColumn", 38, true, true);

   StartDateColumn = CreateTextBoxColumn("Start Date", "startDateColumn", 112, true, true);
   StartDateColumn.DefaultCellStyle = CreateCellStyle("dd/MM/yyyy HH:mm");

   EndDateColumn = CreateTextBoxColumn("End Date", "endDateColumn", 112, true);
   EndDateColumn.DefaultCellStyle = CreateCellStyle("dd/MM/yyyy HH:mm");

   EntryBarColumn = CreateTextBoxColumn("Entry Bar", "entryBarColumn", 60, true);
   EntryBarColumn.DefaultCellStyle = CreateCellStyle(DataGridViewContentAlignment.MiddleRight);

   ExitBarColumn = CreateTextBoxColumn("Exit Bar", "exitBarColumn", 60, true);
   ExitBarColumn.DefaultCellStyle = CreateCellStyle(DataGridViewContentAlignment.MiddleRight);

   BarCountColumn = CreateTextBoxColumn("Bar Count", "barCountColumn", 60, true);
   BarCountColumn.DefaultCellStyle = CreateCellStyle(DataGridViewContentAlignment.MiddleRight);

   EntryPriceColumn = CreateTextBoxColumn("Entry Price", "entryPriceColumn", 70, true);
   EntryPriceColumn.DefaultCellStyle = CreateCellStyle("0.00##", DataGridViewContentAlignment.MiddleRight);

   ExitPriceColumn = CreateTextBoxColumn("Exit Price", "exitPriceColumn", 70, true);
   ExitPriceColumn.DefaultCellStyle = CreateCellStyle("0.00##", DataGridViewContentAlignment.MiddleRight);

   ProfitColumn = CreateTextBoxColumn("Profit", "profitColumn", 58, true);
   ProfitColumn.DefaultCellStyle = CreateCellStyle("0.00", DataGridViewContentAlignment.MiddleRight);

   TotalProfitColumn = CreateTextBoxColumn("Total Profit", "totalProfitColumn", 77, true);
   TotalProfitColumn.DefaultCellStyle = CreateCellStyle("0.00", DataGridViewContentAlignment.MiddleRight);

   ReasonColumn = CreateTextBoxColumn("Reason", "reasonColumn", 60, true);
   ReasonColumn.DefaultCellStyle = CreateCellStyle(DataGridViewContentAlignment.MiddleRight);
   ReasonColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

   Columns.AddRange(new DataGridViewColumn[]
   {
      StatusColumn,
      OperationColumn,
      StartDateColumn,
      EndDateColumn,
      EntryBarColumn,
      ExitBarColumn,
      BarCountColumn,
      EntryPriceColumn,
      ExitPriceColumn,
      ProfitColumn,
      TotalProfitColumn,
      ReasonColumn
   });

   _sorter.Apply(StartDateColumn, ListSortDirection.Ascending, false);
}

En el constructor se crean las columnas que tendrá la tabla. Aunque el código es extenso, es siempre lo mimo (añadir columnas) con algunos matices para definir propiedades de las columnas.

CreateTextBoxColumn es una función de GridViewBase que permite crear una columna con el texto indicado, el nombre que se indique, la anchura de la columna, si es o no de solo lectura y un último parámetro opcional para inmovilizar la columna.

Las tres primeras columnas están inmovilizadas, lo cual permite hacer scroll para ver el resto de columnas sin que estás dejen de verse, tal como se aprecia en esta imagen:

Frozen

Las columnas “End Date”, “Entry Bar” y parte de “Exit Bar” se meten “por debajo” de las columnas inmovilizadas.

Las columnas de fechas incluyen un formato propio que se define con un estilo de celda:

   StartDateColumn.DefaultCellStyle = CreateCellStyle("dd/MM/yyyy HH:mm");

Las columnas que contienen números, se alinean a la derecha así:

   EntryBarColumn.DefaultCellStyle = CreateCellStyle(DataGridViewContentAlignment.MiddleRight);

Las que tienen precios, pueden incluir también el formato del precio:

   EntryPriceColumn.DefaultCellStyle = CreateCellStyle("0.00##", DataGridViewContentAlignment.MiddleRight);

El formato “0.00##” significa que como mínimo se incluyen dos decimales y, si tiene más hasta 4 decimales. “0.00” significa que se mostrarán dos decimales, ni más ni menos. “0.##” significa que se mostrarán entre 0 y 2 decimales.

   ReasonColumn.AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;

La línea anterior indica que la última columna ocupará todo el ancho que quede disponible. Así, las cabeceras de la tabla llegan hasta el final.

Una vez creadas las columnas, se añaden a la tabla mediante “Columns.AddRange“ y, para ordenar inicialmente por fecha de inicio del negocio:

   _sorter.Apply(StartDateColumn, ListSortDirection.Ascending, false);

Coloreando la tabla

La tabla ya está creada. Veamos un ejemplo de cómo hacer que el beneficio de los negocios se muestre en rojo/azul y con mayor o menor intensidad en función de su valor absoluto.

protected override void OnCellFormatting(DataGridViewCellFormattingEventArgs e)
{
   base.OnCellFormatting(e);

   DataGridViewRow row = Rows[e.RowIndex];
   RowInfo rangeInfo = (RowInfo)row.Tag;

   Color zeroColor = Color.White;
   Color cellColor = zeroColor;
   if (rangeInfo.Profit != 0)
   {
      double min = double.MaxValue;
      double max = double.MinValue;
      foreach (DataGridViewRow gridRow in Rows)
      {
         RowInfo auxInfo = (RowInfo)gridRow.Tag;
         min = Math.Min(min, auxInfo.Profit);
         max = Math.Max(max, auxInfo.Profit);
      }

      double interval = Math.Max(Math.Abs(max), Math.Abs(min));
      if (rangeInfo.Profit > 0)
      {
         double percent = rangeInfo.Profit / interval;
         Color limitColor = Color.FromArgb(79, 129, 189);
         cellColor = GradientColor(zeroColor, limitColor, percent);
      }
      else
      {
         double percent = -rangeInfo.Profit / interval;
         Color limitColor = Color.FromArgb(248, 105, 107);
         cellColor = GradientColor(zeroColor, limitColor, percent);
      }
   }

   row.Cells[ProfitColumn.Name].Style.BackColor = cellColor;
   //row.DefaultCellStyle.BackColor = cellColor;

   Color auxColor = Color.FromArgb(220, 230, 241);
   row.Cells[StartDateColumn.Name].Style.BackColor = auxColor;
   row.Cells[EndDateColumn.Name].Style.BackColor = auxColor;
   row.Cells[EntryBarColumn.Name].Style.BackColor = auxColor;
   row.Cells[ExitBarColumn.Name].Style.BackColor = auxColor;

   auxColor = Color.White;
   if (rangeInfo.CloseReason == RowInfo.Reason_EndOfDay)
      auxColor = Color.FromArgb(218, 220, 221);
   else if (rangeInfo.CloseReason == RowInfo.Reason_Lose)
      auxColor = Color.FromArgb(248, 105, 107);
   else if (rangeInfo.CloseReason == RowInfo.Reason_NoTrend)
      auxColor = Color.FromArgb(253, 228, 229);

   row.Cells[ReasonColumn.Name].Style.BackColor = auxColor;
}

RowInfo es una clase que contiene los valores que se muestran en cada fila de la tabla. En otros ejemplos serian estructuras propias del usuario, consultas a bases de datos, etc.

Se utiliza la propiedad Tag de cada fila (que puede contener un objeto cualquiera) para asociar los datos a mostrar en la fila con valores del usuario. Por eso, lo primero que se hace es obtener los valores del usuario del Tag de la fila que se está formateando.

El blanco es el color elegido para un beneficio de 0 puntos. Si el beneficio no es cero, se recorren todas las columnas (esto es fácilmente optimizable) para obtener el valor máximo y mínimo de toda la tabla. Interesa el mayor de ambos valores en valor absoluto pues un beneficio máximo de 100 se corresponde con un azul intenso y, si la pérdida máxima es de 10, no debe ser un rojo intenso porque es pequeña en comparación con el máximo beneficio.

Obtenemos la intensidad del color (percent) dividiendo el beneficio entre el intervalo anterior y al color de máxima intensidad (limitColor) se le da más o menos intensidad en función de percent.

Después de asignar el color de la celda del beneficio, se le da un color de fondo a las celdas de fecha inicial, fecha final, barra de entrada y de salida. Esto se hace para crear franjas de colores y evitar que toda la tabla sea blanca. De este modo, se distinguen mejor las columnas.

Para terminar, se le asigna color a la celda “Reason”. Es un ejemplo para resaltar celdas en función de un valor concreto. Por ejemplo, negocios que se han cerrado por un stop de pérdidas pueden tener un fondo de color rojo. Si se han cerrado por finalización de la sesión, en gris, etc.

Tooltips

Para finalizar, la gestión del tooltip. Es opcional pero muy útil.

public override string GetSelectionTooltip(DataGridViewColumn column, List columns)
{
   Dictionary<DataGridViewColumn, string> titles = new Dictionary<DataGridViewColumn, string>
   {
      { OperationColumn, "Operaciones" },
      { StartDateColumn, "Fecha inicial" },
      { EndDateColumn, "Fecha final" },
      { EntryBarColumn, "Entry Bar" },
      { ExitBarColumn, "Exit Bar" },
      { BarCountColumn, "Bar Count" },
      { ProfitColumn, "Profit" },
      { TotalProfitColumn, "TotalProfit" },
      { ReasonColumn, "Reason" },
   };

   if (!titles.ContainsKey(column))
   {
      return string.Empty;
   }

   string title = titles[column];
   StringBuilder builder = new StringBuilder();
   if (column == OperationColumn || column == ReasonColumn)
      builder.AppendFormat(GetDistinctValuesCount(title, columns));
   else if (column == StartDateColumn || column == EndDateColumn)
      builder.AppendFormat(GetDatesRange(title, columns));
   else if (column == EntryBarColumn || column == ExitBarColumn)
      builder.AppendFormat(GetIntegerRange(title, columns));
   else if (column == BarCountColumn)
      builder.AppendFormat(GetIntegerRange(title, columns, true));
   else if (column == ProfitColumn)
      builder.AppendFormat(GetDoubleRange(title, columns, true, true));
   else if (column == TotalProfitColumn)
      builder.AppendFormat(GetDoubleRange(title, columns));

   return builder.ToString();
}

Su implementación es bastante automática. Cuando GridViewBase necesita el texto de las celdas seleccionadas de una columna invoca esta función. Aquí solo se obtiene el título de la columna y se usan las funciones ya comentadas para obtener los valores que interesen.

Para las columnas operación y razón, se muestra un texto que indica cuantas celdas de cada tipo hay (2 compras y 3 ventas…), para las fechas, la menor y mayor de las seleccionadas, para el número de barras, el menor y mayor junto al promedio, para el beneficio, además del promedio, la suma total, etc.

En la imagen inicial se muestra el tooltip de varias celdas seleccionadas.

Uso de la clase ExampleGridView

Una vez creada la clase, veamos cómo usarla. Añadir una variable al formulario:

   private ExampleGridView grid;

Añadirlo en la ventana:

   grid = new ExampleGridView() { Dock = DockStyle.Fill };

   Controls.Add(grid);

El código anterior crea el grid y hace que ocupe todo el espacio disponible de su contenedor. Su contenedor es la ventana, donde se añade. Se pueden usar otros contenedores como las pestañas de un TabControl o los paneles de un SplitContainer.

El resto de código relevante de la ventana está en la función:

private void AddOperationsToGrid(string status, IEnumerable operations)
{
   double totalProfit = 0.0;

   foreach (RowInfo info in operations)
   {
      totalProfit += info.Profit;

      int index = grid.Rows.Add(
         status,
         info.Operation > 0 ? "Buy" : "Sell",
         info.EntryDate,
         info.ExitDate,
         info.EntryBar,
         info.ExitBar,
         info.ExitBar - info.EntryBar,
         info.EntryPrice,
         info.ExitPrice,
         info.Profit,
         totalProfit,
         info.CloseReason);
      grid.Rows[index].Tag = info;
   }
}

Aquí se añaden filas a la tabla (grid.Rows.Add) y se les asocia la información que contiene cada fila (grid.Rows[index].Tag = info). Recordemos que esta información se obtiene luego para uso en ExampleGridView.

El resto de código se limita a crear negocios ficticios para mostrar algo en la tabla.

Resumen

Crear una tabla bonita y funcional a partir de GridViewBase es un ejercicio sencillo. Las posibilidades son enormes y es posible visualizar fácilmente nuestros negocios, resultados, variables, estudios de estrategias… todo lo que se nos pueda ocurrir.

Si se monta la operativa con las Trading Tools, se pueden mantener informes en tiempo real de lo que está sucediendo con nuestra estrategia.

Si cargamos series, indicadores… con las Trading Tools, es posible volcar resultados interesantes (pivots, soportes…) en la tabla. Incluso los resultados que se obtendrían si se operase en esos puntos, incluir las comisiones de nuestro broker, el deslizamiento medio que nuestros estudios nos provean, etc.

Archivo Zip Descargar C# Report.zip.

Archivo Zip Descargar VB .NET Report_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, Programación y etiquetada , . Guarda el enlace permanente.

Deja un comentario