English Русский 中文 Deutsch 日本語 Português
preview
Indicadores basados ​​en la clase CCanvas: Rellenando canales con transparencia

Indicadores basados ​​en la clase CCanvas: Rellenando canales con transparencia

MetaTrader 5Ejemplos | 6 julio 2023, 12:41
277 0
Samuel Manoel De Souza
Samuel Manoel De Souza

Introducción

En este artículo, analizaremos los métodos utilizados para crear indicadores personalizados que se dibujan con la ayuda de la clase CCanvas de la Biblioteca Estándar. Prestaremos especial atención a los indicadores que rellenan el área entre las dos líneas con un color sólido. Primero veremos las razones por las que usar la clase CCanvas podría ser una mejor opción que otras opciones para los indicadores de este tipo. Después de ello, echaremos un vistazo a algunas de las propiedades del gráfico necesarias para calcular las coordenadas y el proceso principal asociado con el trabajo con CCanvas.

El objetivo final es crear un indicador capaz de aplicar transparencia. Todo el trabajo se realizará solo considerando la ventana principal del gráfico. Una vez conseguido nuestro objetivo, podremos comenzar a trabajar con los indicadores de la subventana.

Los temas del artículo se enumeran a continuación:

Motivos para usar la clase CCanvas

Uno podría preguntarse por qué utilizar CCanvas cuando ya existe DRAW_FILLING para los indicadores personalizados. Vemos al menos dos razones:

  1. Los colores del indicador se mezclan con los colores de otros indicadores, velas japonesas y objetos gráficos.
  2. DRAW_FILLING no permite la transparencia

Gráfico con dos indicadores y un objeto

Propiedades de la ventana de gráfico

Antes de crear un gráfico personalizado, deberemos pensar en sus propiedades. Podrá encontrar todas las propiedades en la documentación. Para obtener los valores de propiedad, deberemos usar las funciones correspondientes ChartGetInteger y ChartGetDouble. También hay una función, ChartGetString, pero no trabajaremos aquí con ella.

Las propiedades que vamos a usar se enumeran a continuación con una breve descripción. Si necesitamos algo más, lo mencionaremos aparte.

  • CHART_WIDTH_IN_PIXELS — anchura de la ventana del gráfico sin escala de precios
  • CHART_HEIGTH _IN_PIXELS — altura de la subventana sin incluir la escala de fecha
  • CHART_PRICE_MIN — precio correspondiente a la parte superior de la subventana
  • CHART_PRICE_MAX — precio correspondiente a la parte inferior de la subventana
  • CHART_SCALE — distancia entre barras. Tras realizar algunas pruebas, hemos encontrado que es una potencia de dos pow(2, CHART_SCALE)
  • CHART_FISRT_VISIBLE_BAR: — primera barra visible en el gráfico de izquierda a derecha
  • CHART_VISIBLE_BARS — número de barras visibles en el gráfico

Descripción de las propiedades de la ventana de gráfico

Estas propiedades resultan claramente visibles en la siguiente figura.

Dibujando propiedades vinculadas con las coordenadas

Usaremos las propiedades CHART_WIDTH_IN_PIXELS y CHART_HEIGTH _IN_PIXELS para determinar el tamaño de lienzo necesario. Al cambiar el tamaño de la ventana y modificar las propiedades, deberemos ajustar el tamaño del lienzo.

Para lograr una mejor comprensión, crearemos un indicador simple que mostrará las propiedades y cómo cambian en función de los cambios de precio y la interacción con el usuario. También comenzaremos a usar el lienzo para familiarizarnos con el proceso de dibujado.


Indicador de visualización de las propiedades del gráfico

Este paso asume que ya sabemos cómo crear un indicador personalizado. Si desconoce el proceso, le sugiero que lea en primer lugar los artículos MQL5: Crea tu propio indicador y Explorando opciones para crear velas multicolores. Comencemos.

Hemos creado nuestro indicador en la ruta que indicamos a continuación. Para una mejor organización, le proponemos hacer lo mismo.

Cuando el esqueleto del indicador esté listo, deberemos añadir la biblioteca CCanvas al archivo. Podemos hacer esto con la directiva #include.

Luego crearemos un ejemplar de la clase CCanvas. Todo esto se coloca después de las directivas #property del indicador.

#property copyright "Copyright 2023, Samuel Manoel De Souza"
#property link      "https://www.mql5.com/en/users/samuelmnl"
#property version   "1.00"
#property indicator_chart_window

#include <Canvas/Canvas.mqh>
CCanvas Canvas;

Lo primero que deberemos hacer al trabajar con CCanvas es crear un OBJ_BITMAP_LABEL y añadirle un recurso. Deberemos hacer esto si deseamos añadirlo al gráfico, generalmente en el bloque de inicialización del indicador, utilizando el método CreateBitampLabel(...). Finalmente, OBJ_BITMAP_LABEL y el recurso adjunto deberán eliminarse. Deberemos hacer esto si deseamos eliminarlo del gráfico, generalmente en el bloque de desinicialización utilizando el método Destroy(void). Mientras tanto, efectuaremos el dibujado principal, que consiste en borrar los dibujos (borrar o establecer los valores de píxel predeterminados del recurso), crear los dibujos y actualizar el recurso. En el siguiente diagrama se muestra el ciclo de vida completo de un lienzo.

canvas_process

Para simplificar, almacenaremos Erase, Draw y Update en una función llamada Redraw. Tras escribir todo en el código, obtendremos la siguiente estructura.

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- indicator buffers mapping
   Canvas.CreateBitmapLabel(0, 0, "Canvas", 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE);
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   Canvas.Destroy();
  }
//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = ColorToARGB(clrBlack);
   uint text_color = ColorToARGB(clrWhite);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw

//--- add second draw

//--- add ... draw

//--- add last draw

//--- canvas update
   Canvas.Update();
  }

Para mostrar las propiedades, las escribiremos usando el método TextOut. Los valores de estas propiedades se almacenarán como una cadena en la variable struct

struct StrProperty
  {
   string name;
   string value;
  };
La estructura puede ser la siguiente. Luego podremos generarlos brevemente en un ciclo. Como aún no tenemos un array, transmitiremos el array como parámetro a la función Redraw. La función Redraw se ve así:
void Redraw(StrProperty &array[])
  {
   uint default_color = ColorToARGB(clrBlack);
   uint text_color = ColorToARGB(clrWhite);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   int total = ArraySize(array);
   for(int i=0;i<total;i++)
     {
      int padding = 2;
      int left = padding, right = Canvas.Width() - padding, y = i * 20 + padding;
      Canvas.TextOut(left, y, array[i].name, text_color, TA_LEFT);
      Canvas.TextOut(right, y, array[i].value, text_color, TA_RIGHT);
     }
//--- canvas update
   Canvas.Update();
  }
Finalmente, podremos obtener los valores de propiedad y generarlos. Si su código no tiene un manejador de función OnChartEvent, deberá añadirle uno. Allí comprobaremos el ID del evento CHARTEVENT_CHART_CHANGE. Cuando suceda un evento, declararemos algunas variables que tomarán los valores de propiedad y los transmitirán a un array de estructuras, y luego llamaremos a la función Redraw. Podemos compilar el indicador, añadirlo al gráfico y manipular el gráfico para ver la actualización del lienzo.
//+------------------------------------------------------------------+
//| Custom indicator chart event handler function                    |
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   int chart_width         = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   int chart_height        = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   int chart_scale         = (int)ChartGetInteger(0, CHART_SCALE);
   int chart_first_vis_bar = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   int chart_vis_bars      = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   double chart_prcmin     = ChartGetDouble(0, CHART_PRICE_MIN);
   double chart_prcmax     = ChartGetDouble(0, CHART_PRICE_MAX);
//---
   StrProperty array[]
     {
        {"Width", (string)chart_width},
        {"Height", (string)chart_height},
        {"Scale", (string)chart_scale},
        {"First Vis. Bar", (string)chart_first_vis_bar},
        {"Visible Bars", (string)chart_vis_bars},
        {"Price Min", (string)chart_prcmin},
        {"Price Max", (string)chart_prcmax},
     };
   Redraw(array);
  }

Transformación de las coordenadas

En este punto, necesitaremos algunas funciones básicas para convertir la fecha y hora o el índice de la barra a «x» en píxeles, del precio a «y» en píxeles, de «x» al índice de la barra y de «y» al precio (no usaremos algunas de estas ahora, pero podemos hacerlas todas a la vez). En este sentido, trasladaremos las variables de propiedad del gráfico al ámbito global, mientras que en la función OnChartEvent solo actualizaremos los valores y llamaremos a la función Redraw si fuera necesario. La solución ideal sería encapsular las variables y las funciones de transformación en una clase o estructura, pero por ahora procederemos de forma sencilla. Le sugiero que comience a aprender sobre POO leyendo el artículo "Las bases de la programación orientada a objetos" y el tema relacionado de la documentación (Programación orientada a objetos), pues aprovecharemos esto a la próxima oportunidad.

Principalmente, las funciones se relacionan con relaciones proporcionales.

//+------------------------------------------------------------------+
//| Converts the chart scale property to bar width/spacing           |
//+------------------------------------------------------------------+
int BarWidth(int scale) {return (int)pow(2, scale);}
//+------------------------------------------------------------------+
//| Converts the bar index(as series) to x in pixels                 |
//+------------------------------------------------------------------+
int ShiftToX(int shift) {return (chart_first_vis_bar - shift) * BarWidth(chart_scale) - 1;}
//+------------------------------------------------------------------+
//| Converts the price to y in pixels                                |
//+------------------------------------------------------------------+
int PriceToY(double price)
  {
// avoid zero divider
   if(chart_prcmax - chart_prcmin == 0.)
      return 0.;
   return (int)round(chart_height * (chart_prcmax - price) / (chart_prcmax - chart_prcmin) - 1);
  }
//+------------------------------------------------------------------+
//| Converts x in pixels to bar index(as series)                     |
//+------------------------------------------------------------------+
int XToShift(int x)
  {
// avoid zero divider
   if(BarWidth(chart_scale) == 0)
      return 0;
   return chart_first_vis_bar - (x + BarWidth(chart_scale) / 2) / BarWidth(chart_scale);
  }
//+------------------------------------------------------------------+
//| Converts y in pixels to price                                    |
//+------------------------------------------------------------------+
double YToPrice(int y)
  {
// avoid zero divider
   if(chart_height == 0)
      return 0;
   return chart_prcmax - y * (chart_prcmax - chart_prcmin) / chart_height;
  }

DRAW_FILLING con transparencia

Ahora tenemos todo lo necesario para implementar nuestro DRAW_FILLING con CCanvas.

No vamos a perder el tiempo creando un nuevo indicador. En su lugar, tomaremos el ejemplo utilizado en la plataforma MetaTrader 5 y añadiremos un rellenado entre las dos líneas. Yo estoy utilizando el indicador Envelopes en \\MQL5\\Indicators\\Examples\\, ubicado en la carpeta de datos del terminal. Copiaremos Envelopes.mq5 en el mismo directorio donde creamos el indicador ChartPropertiesViwer. Usted podrá elegir cualquier indicador, pero le sugiero usar el mismo indicador siguiendo los pasos de este artículo.

Lo primero que deberemos hacer es copiar en Envelopes todo lo que hemos hecho en el indicador ChartPropertiesViewer.

Como hemos mencionado anteriormente, rellenaremos el canal entre las dos líneas. Para ello crearemos una función donde se transmitirán los arrays correspondientes a los valores de las líneas. En el indicador Envelopes, los arrays se especificarán usando las variables ExtUpBuffer y ExtMABuffer.

double                   ExtUpBuffer[];
double                   ExtDownBuffer[];

Junto con los arrays, transmitiremos algunas variables más que nos permitirán usar dos colores, establecer el nivel de transparencia y mover el indicador hacia la izquierda o hacia la derecha del gráfico.

Parámetros Descripción para la variable
 serie1  Array de valores correspondientes a la primera línea
 serie2  Array de valores correspondientes a la segunda línea
 clr1  Color cuando serie1 >= serie2
 clr2  Color cuando serie1 < serie2
 alpha  Valor de transparencia del canal
 plot_shift  Indicador de desplazamiento a la derecha o a la izquierda del gráfico

La función que utiliza las variables existentes y los parámetros mencionados tiene el aspecto que sigue.

//+------------------------------------------------------------------+
//| Fill the area between two lines                                  |
//+------------------------------------------------------------------+
void DrawFilling(double &serie1[], double &serie2[], color clr1, color clr2, uchar alpha = 255, int plot_shift = 0)
  {
   int start  = chart_first_vis_bar;
   int total  = chart_vis_bars + plot_shift;
   uint argb1 = ColorToARGB(clr1, alpha);
   uint argb2 = ColorToARGB(clr2, alpha);
   int limit  = fmin(ArraySize(serie1), ArraySize(serie2));
   int px, py1, py2;
   for(int i = 0; i < total; i++)
     {
      int bar_position = start - i;
      int bar_shift = start - i + plot_shift;
      int bar_index = limit - 1 - bar_shift;
      if(serie1[bar_index] == EMPTY_VALUE || serie1[bar_index] == EMPTY_VALUE || bar_shift >= limit)
         continue;
      int x  = ShiftToX(bar_position);
      int y1 = PriceToY(serie1[bar_index]);
      int y2 = PriceToY(serie2[bar_index]);
      uint argb = serie1[bar_index] < serie2[bar_index] ? argb2 : argb1;
      if(i > 0 && serie1[bar_index - 1] != EMPTY_VALUE && serie2[bar_index - 1] != EMPTY_VALUE)
        {
         if(py1 != py2)
            Canvas.FillTriangle(px, py1, px, py2, x, y1, argb);
         if(y1 != y2)
            Canvas.FillTriangle(px, py2, x, y1, x, y2, argb);
        }
      px  = x;
      py1 = y1;
      py2 = y2;
     }
  }

Hasta este punto, hemos estado utilizando un lienzo de tamaño fijo. Sin embargo, los indicadores requieren un lienzo para rellenar el área completa del gráfico. Además, cada vez que cambiamos el tamaño de la ventana del gráfico al acercar, reducir, estirar hacia cualquier lado o añadir indicadores de subventana, deberemos asegurarnos de que el lienzo aún rellene el área del gráfico al completo. Para hacer esto, cambiaremos el tamaño del lienzo realizando un pequeño cambio en nuestra función OnChartEvent.

void OnChartEvent(const int id, const long& lparam, const double& dparam, const string& sparam)
  {
   if(id != CHARTEVENT_CHART_CHANGE)
      return;
   chart_width          = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   chart_height         = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   chart_scale          = (int)ChartGetInteger(0, CHART_SCALE);
   chart_first_vis_bar  = (int)ChartGetInteger(0, CHART_FIRST_VISIBLE_BAR);
   chart_vis_bars       = (int)ChartGetInteger(0, CHART_VISIBLE_BARS);
   chart_prcmin         = ChartGetDouble(0, CHART_PRICE_MIN, 0);
   chart_prcmax         = ChartGetDouble(0, CHART_PRICE_MAX, 0);
   if(chart_width != Canvas.Width() || chart_height != Canvas.Height())
      Canvas.Resize(chart_width, chart_height);

Ahora haremos unas pequeñas actualizaciones para que la función funcione.

  1. Actualizaremos nuestra función Redraw eliminando los parámetros añadidos en el indicador anterior y agregando la función DrawFilling.
  2. Añadiremos nuestra función Redraw a OnCalculation para actualizar el dibujo cuando cambien los valores del indicador.
  3. Cambiaremos el nombre del objeto transmitido como parámetro al llamar a CreateBitmapLabel.

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = 0;
   color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0);
   color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   DrawFilling(ExtUpBuffer, ExtDownBuffer,clrup, clrdn, 128, InpMAShift);
//--- canvas update
   Canvas.Update();
  }
//--- the main loop of calculations
   for(int i=start; i<rates_total && !IsStopped(); i++)
     {
      ExtUpBuffer[i]=(1+InpDeviation/100.0)*ExtMABuffer[i];
      ExtDownBuffer[i]=(1-InpDeviation/100.0)*ExtMABuffer[i];
     }
   Redraw();
//--- OnCalculate done. Return new prev_calculated.
   return(rates_total);
   Canvas.CreateBitmapLabel(0, 0, short_name, 0, 0, 200, 150, COLOR_FORMAT_ARGB_NORMALIZE);

Ahora veremos el aspecto del gráfico con dos envoltorios con diferentes periodos y un objeto rectangular.

Envoltorios con CCanvas con canal alfa = 128

Envoltorios con CCanvas con canal alfa = 255

Como podemos ver, el problema con los indicadores está resuelto, mientras que el problema con los objetos del gráfico sigue vigente, pero este es un tema para otro capítulo.


Extensión del método para trabajar en los indicadores de la subventana

Analicemos la siguiente figura. Aquí vemos un indicador de subventana con uso de la función DRAW_FILLING. Esta imagen la hemos tomado de la documentación de MQL. Haremos lo mismo, pero seguiremos proporcionando transparencia con CCanvas y, lo que es más importante, evitaremos problemas con las áreas superpuestas.

Indicador de subventana con uso de DRAW_FILLING

A continuación enumeramos los cambios necesarios:

  • Creamos una etiqueta de mapa de bits en la misma subventana donde se encuentra el indicador
  • Cambiamos el tamaño del lienzo según el tamaño de la subventana en lugar de la ventana del gráfico principal

Para crear una etiqueta de mapa de bits en la misma subventana y obtener el tamaño de la subventana, necesitaremos encontrar en qué subventana se halla el indicador. Podríamos pensar que esta es solo la última subventana del gráfico, pero el terminal nos permite colocar dos o más indicadores en una subventana, y no necesariamente en la última. También necesitaremos una función que retorne el número de la subventana donde se encuentra el indicador. Vamos a echar un vistazo a la siguiente función:

//+------------------------------------------------------------------+
//| return the number of the subwindow where the indicator is located|
//+------------------------------------------------------------------+
int ChartIndicatorFind(string shortname)
  {
   int subwin = ChartGetInteger(0, CHART_WINDOWS_TOTAL);
   while(subwin > 0)
     {
      subwin--;
      int total = ChartIndicatorsTotal(0, subwin);
      for(int i = 0; i < total; i++)
        {
         string name = ChartIndicatorName(0, subwin, i);
         if(name == shortname)
            return subwin;
        }
     }
   return -1;
  }

En el último indicador, usaremos el indicador Envelopes como ejemplo. Ahora usaremos el código de la documentación (DRAW_FILLING) como fuente para nuestro ejemplo. Podemos crear un nuevo indicador en el mismo directorio donde creamos dos indicadores anteriormente. Lo llamaremos SubwindowIndicator. Luego copiaremos el código de la documentación.

El indicador se construirá utilizando la función DRAW_FILLING. Como usaremos CCanvas para rellenar el canal, podemos reemplazar el tipo de dibujo con líneas. A continuación le mostramos los cambios en las propiedades del indicador.

#property indicator_plots   2
//--- plot Intersection
#property indicator_label1  "Fast"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrRed
#property indicator_width1  1
#property indicator_label2  "Slow"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrBlue
#property indicator_width2  1

Cambios en la función OnInit.

//--- indicator buffers mapping
   SetIndexBuffer(0,IntersectionBuffer1,INDICATOR_DATA);
   SetIndexBuffer(1,IntersectionBuffer2,INDICATOR_DATA);
//---
   PlotIndexSetInteger(0,PLOT_SHIFT,InpMAShift);
   PlotIndexSetInteger(1,PLOT_SHIFT,InpMAShift);

Además, no necesitaremos un indicador para cambiar el estilo de línea. Podemos comentar esta línea en la función OnCalculate.

//--- If a sufficient number of ticks has been accumulated
   if(ticks>=N)
     {
      //--- Change the line properties
      //ChangeLineAppearance();
      //--- Reset the counter of ticks to zero
      ticks=0;
     }

Ahora podremos añadir las funciones y variables de propiedad del gráfico creadas en este artículo. En este indicador, los arrays que necesitamos transmitir como parámetros a la función DrawFilling tendrán nombres distintos. Necesitaremos cambiarlo en la función Redraw

double         IntersectionBuffer1[];
double         IntersectionBuffer2[];

La función Redraw se verá así:

//+------------------------------------------------------------------+
//| Custom indicator redraw function                                 |
//+------------------------------------------------------------------+
void Redraw(void)
  {
   uint default_color = 0;
   color clrup = (color)PlotIndexGetInteger(0, PLOT_LINE_COLOR, 0);
   color clrdn = (color)PlotIndexGetInteger(1, PLOT_LINE_COLOR, 0);
//--- canvas erase
   Canvas.Erase(default_color);
//--- add first draw
   DrawFilling(IntersectionBuffer1, IntersectionBuffer2, clrup, clrdn, 128, InpMAShift);
//--- canvas update
   Canvas.Update();
  }

Tras compilar el código, obtendremos el resultado esperado.

Indicador de subventana con rellenado de canal transparente


Conclusión

En este artículo, hemos tratado el trabajo con CCanvas, algunas propiedades del dibujado y la obtención de sus valores, y también su uso para realizar algunas transformaciones de coordenadas básicas que resultan útiles y aplicables para muchos propósitos. Después de ello hemos desarrollado un indicador con transparencia y ampliado el método de trabajo con indicadores de subventana.

Más abajo, adjuntamos los archivos de los indicadores desarrollados en el artículo.


Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/12357

Archivos adjuntos |
Envelopes.mq5 (10.43 KB)
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 05): Vistas previas Desarrollo de un sistema de repetición — Simulación de mercado (Parte 05): Vistas previas
Hemos logrado desarrollar una forma de ejecutar la repetición de mercado de manera bastante realista y aceptable. Ahora, vamos a continuar con nuestro proyecto y agregar datos para mejorar el comportamiento de la repetición.
Aprendiendo a diseñar un sistema de trading con Fibonacci Aprendiendo a diseñar un sistema de trading con Fibonacci
El presente artículo supone la continuación de la serie dedicada a la construcción de sistemas comerciales basados ​​en los indicadores más populares. La próxima herramienta técnica que analizaremos será el indicador de Fibonacci. Hoy veremos cómo escribir un programa basado en las señales de este indicador.
Teoría de categorías en MQL5 (Parte 4): Intervalos, experimentos y composiciones Teoría de categorías en MQL5 (Parte 4): Intervalos, experimentos y composiciones
La teoría de categorías es una rama de las matemáticas diversa y en expansión, relativamente inexplorada aún en la comunidad MQL5. Esta serie de artículos tiene como objetivo describir algunos de sus conceptos para crear una biblioteca abierta y seguir utilizando esta maravillosa sección para crear estrategias comerciales.
Desarrollo de un sistema de repetición — Simulación de mercado (Parte 04): Haciendo ajustes (II) Desarrollo de un sistema de repetición — Simulación de mercado (Parte 04): Haciendo ajustes (II)
Vamos continuar con el desarrollo del sistema y el control. Sin una forma de controlar el servicio, se complica avanzar y mejorar el sistema.