
Optimización móvil continua (Parte 2): Mecanismo de creación de informes de optimización para cualquier robot
Introducción
Continuamos con el ciclo de artículos sobre la creación de un optimizador automático para realizar la optimización móvil de estrategias comerciales. Si el anterior artículo de la serie estaba dedicado a la creación de la biblioteca DLL que utilizaremos en nuestro optimizador automático y en el robot, este estará completamente dedicado al lenguaje MQL5. Vamos a analizar los métodos de generación de los informes de optimización y las formas en que podemos utilizar esta funcionalidad en nuestros algoritmos.
Dado que el simulador de estrategias no ofrece acceso a sus indicadores desde el robot, y los datos descargados no son tan exhaustivos como querríamos, vamos a usar la funcionalidad para la descarga de informes de optimización que ya ha sido implementada en algunos artículos anteriores. Sin embargo, dado que ciertas partes de esta funcionalidad han sido mejoradas, mientras que otras no han sido claradas como es debido en los anteriores artículos, creemos que será adecuado describirlas de nuevo, pues nos encontramos ante una de las partes clave del programa creado. Vamos a comenzar precisamente por una de las novedades, más concretamente, por la adición de comisiones de usuario. Todas las clases y funciones descritas en este artículo se ubican en el directorio Include/History manager.
Implementando la comisión personalizada y el deslizamiento
El simulador de la plataforma MetaTrader 5 ofrece multitud de magníficas capacidades, sin embargo, a veces algunos brókeres no añaden a la historia la comisión por la transacción, o bien querríamos añadir a la descarga para la simulación adicional de estrategias la comisión adicional; con este objetivo, hemos añadido una clase que guarda la comisión para cada símbolo aparte. Más tarde, al llamar el método correspondiente, este nos dará la comisión y el deslizamiento especificado. La propia clase tiene los siguientes encabezados:
class CCCM { private: struct Keeper { string symbol; double comission; double shift; }; Keeper comission_data[]; public: void add(string symbol,double comission,double shift); double get(string symbol,double price,double volume); void remove(string symbol); };
Para este clase se ha creado la estructura Keeper, que contiene la comisión y el deslizamiento para el activo establecido. Además, se ha creado una matriz con los datos de las estructuras en las que se almacenan las comisiones y el deslizamiento. Asimismo, se han declarado 3 métodos que añaden y eliminan los datos. El método de adición de una activo tiene el aspecto que sigue:
void CCCM::add(string symbol,double comission,double shift) { int s=ArraySize(comission_data); for(int i=0;i<s;i++) { if(comission_data[i].symbol==symbol) return; } ArrayResize(comission_data,s+1,s+1); Keeper keeper; keeper.symbol=symbol; keeper.comission=MathAbs(comission); keeper.shift=MathAbs(shift); comission_data[s]=keeper; }
Este método implementa la adición de un nuevo activo a la colección, comprobando preliminarmente si el mismo activo ha sido añadido con anterioridad. Debemos destacar que el deslizamiento y la comisión se añaden al módulo, esto es necesario para que al sumar los gastos, el signo no afecte al cálculo. Asimismo, también hay que prestar atención a las unidades de medida al añadir los datos de las magnitudes.
- Comisión: dependiendo del tipo de activo, se añade en la divisa en la que se valora el beneficio, o como porcentaje del volumen comerciado,
- Deslizamiento: se añade siempre en puntos.
También debemos prestar atención a que los datos de la magnitud no se añaden para una posición completa (apertura + cierre), sino para una transacción, es decir, para una posición completa tendremos n*comission + n*shift, donde n será el número total de transacciones que abren y cierran la posición.
El método remove elimina el activo seleccionado. Como clave se usará el nombre del símbolo.
void CCCM::remove(string symbol) { int total=ArraySize(comission_data); int ind=-1; for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ind=i; break; } } if(ind!=-1) ArrayRemove(comission_data,ind,1); }
Si no se ha encontrado el símbolo correspondiente, el método se finalizará sin eliminar ningún activo.
Para obtener el desplazamiento y la comisión seleccionados, se usa el método get; su implementación se diferencia para diferentes tipos de activos.
double CCCM::get(string symbol,double price,double volume) { int total=ArraySize(comission_data); for(int i=0;i<total;i++) { if(comission_data[i].symbol==symbol) { ENUM_SYMBOL_CALC_MODE mode=(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol,SYMBOL_TRADE_CALC_MODE); double shift=comission_data[i].shift*SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_VALUE); double ans; switch(mode) { case SYMBOL_CALC_MODE_FOREX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFD : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDINDEX : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_CFDLEVERAGE : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_STOCKS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_FUTURES : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_FUTURES_FORTS : ans=(comission_data[i].comission+shift)*volume; break; case SYMBOL_CALC_MODE_EXCH_BONDS : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_STOCKS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_EXCH_BONDS_MOEX : { double trading_volume=price*volume*SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE); ans=trading_volume*comission_data[i].comission/100+shift*volume; } break; case SYMBOL_CALC_MODE_SERV_COLLATERAL : ans=(comission_data[i].comission+shift)*volume; break; default: ans=0; break; } if(ans!=0) return -ans; } } return 0; }
Desplazándonos por la matriz, buscamos por ella el símbolo indicado. Dado que para diferentes tipos de símbolos existe un método de cálculo distinto, su establecimiento también puede variar. Así, para las acciones y obligaciones, la comisión se establece en tanto por ciento de la rotación (turnover), donde la propia rotación se calcula como el producto del número de lotes por el número de contratos en el lote y por el precio al que se ha realizado la transacción.
Como resultado, obtenemos un equivalente monetario de la operación realizada. El resultado de la ejecución de este método siempre es la suma de la comisión y el deslizamiento en equivalente monetario. En este caso, además, el deslizamiento se calcula a partir del coste del tick. La clase descrita se usará posteriormente en la clase encargada de descargar los informes, que vamos a analizar ahora. Los parámetros de las comisiones para cada uno de los activos pueden ser incluidos en el código, o solicitados desde alguna base de datos, o bien transmitirse a un robot como parámetros de entrada; eso precisamente es lo que hemos hecho en nuestros algoritmos.
La novedad en la clase CDealHistoryGetter
Las clases analizadas en el presente artículo y en los siguientes de la serie han figurado anteriormente en nuestros artículos, por eso, algunas partes pueden ser analizadas superficialmente, mientras que otras se someten a un estudio más profundo, en especial aquellas que no se han tenido en cuenta en artículos anteriores. En cualquier caso, no podremos arreglárnoslas sin ver su descripción, ya que en el algoritmo de descarga del informe de transacciones resulta clave el algoritmo de creación del informe a descargar.
La primera de las clases mencionadas es CDealHistoryGetter, que llevamos usando con algunas modificaciones desde el primer artículo. Precisamente dicho artículo describía esta clase. En los archivos adjuntos se encuentra la última versión de esta clase, con algunos defectos subsanados. También contiene algunos añadidos en la forma descrita más arriba. Para acceder a un análisis más detallado del mecanismo de funcionamiento de la descarga del informe de transacciones en un aspecto cómodamente legible, lea el artículo indicado. En este artículo, vamos a analizar con menor detalle su funcionalidad, así como la forma en que se añade la comisión de usuario y el deslizamiento al informe descargado. De acuerdo con uno de los principios de la POO, que implica que un objeto debe cumplir el propósito específico indicado, este objeto se crea para recibir todos los tipos de resultados de los informes comerciales que nos pueden interesar, y contiene los siguientes métodos públicos, cada uno de los cuales cumple su papel:
- getHistory — este método ayuda a descargar la historia de transacciones agrupada por posiciones. Si descargamos en el ciclo la historia de transacciones sin ningún filtrado con los métodos estándar, obtendremos la descripción de las transacciones presentadas por la estructura DealData:
struct DealData { long ticket; // Deal ticket long order; // The number of the order that opened the position datetime DT; // Position open date long DT_msc; // Position open date in milliseconds ENUM_DEAL_TYPE type; // Open position type ENUM_DEAL_ENTRY entry; // Position entry type long magic; // Unique position number ENUM_DEAL_REASON reason; // Order placing reason long ID; // Position ID double volume; // Position volume (lots) double price; // Position entry price double comission; // Commission paid double swap; // Swap double profit; // Profit / loss string symbol; // Symbol string comment; // Comment specified when at opening string ID_external; // External ID };
En este caso, además, los datos serán clasificados según la hora de apertura de las posiciones, pero no se agruparán de ninguna forma. En el artículo indicado, se muestra con ejemplos lo complicado que es leer una descarga semejante, ya que al implementar las transacciones con varios algoritmos al mismo tiempo, siempre surge una confusión entre las transacciones. Sobre todo, si se usa un sistema de incremento de posiciones que compren/vendan adicionalmente un activo de acuerdo con algoritmos propios. Como resultado, obtendremos una serie de transacciones de entrada y de salida que se mostrarán de forma no agrupada, y que solo se estorbarán unas a otras y no mostrarán una panorámica completa.
El método analizado las agrupa por posiciones, lo que sin duda ayuda a relacionar las transacciones con una posición concreta. A pesar de que la situación de las órdenes sigue siendo confusa, por lo menos nos deshacemos de las transacciones sobrantes que no entran en la posición investigada. El resultado obtenido se almacena como una estructura que guarda una matriz de la estructura de transacción mostrada arriba.
struct DealKeeper { DealData deals[]; /* List of all deals for this position (or several positions in case of position reversal)*/ string symbol; // Symbol long ID; // ID of the position (s) datetime DT_min; // Open date (or the date of the very first position) datetime DT_max; // Close date };
Debemos considerar que esta clase no tiene en cuenta en sus grupos los números mágicos, dado que en la práctica, si se usan dos o más algoritmos en las transacciones de un solo símbolo, estos se cruzarán, no pudiendo así hacerse cargo de la posición. Como mínimo en la "Bolsa de Moscú", para la cual escribimos la mayoría de los algoritmos, esto resulta técnicamente imposible. Además, debemos considerar que la herramienta que ofrecemos fue diseñada para descargar los informes de las transacciones, o bien para descargar los resultados de las simulaciones/optimizaciones. Para la primera tarea, nos bastará con tener las estadísticas del símbolo seleccionado, que se ofrecen al completo. Para la segunda, el número mágico no es esencial, dado que, según la lógica de esta funcionalidad, un algoritmo debetener solo un número mágico, y el simulador solo aplica una algoritmo cada vez.
La implementación de este método ha permanecido inalterada desde que se escribió por primera vez, salvo por la adición del método para implementar la comisión de usuario. Para la tarea planteada, al constructor de clase se le transmite por enlace la clase CCCM analizada antes, y se guarda en el campo correspondiente. A continuación, al rellenar la estructura DealData (más concretamente, en el momento en que se rellena la comisión), se añade la comisión de usuario guardada en la clase CCCM transmitida.
#ifndef ONLY_CUSTOM_COMISSION if(data.comission==0 && comission_manager != NULL) { data.comission=comission_manager.get(data.symbol,data.price,data.volume); } #else data.comission=comission_manager.get(data.symbol,data.price,data.volume); #endif
En este caso, además, la comisión se añade tanto de forma directiva, como condicional. Si, antes de incluir el archivo condicionado con los datos de la clase en el robot, definimos el parámetro ONLY_CUSTOM_COMISSION , el campo de la comisión contendrá siempre la comisión que hemos transmitido, y no la que proporciona el bróker. Si no definimos el dicho parámetro, la comisión que transmitimos se añadirá de forma condicional, para ser más exactos, solo cuando el bróker no la ofrezca junto con las cotizaciones, de lo contrario, la comisión de usuario será ignorada.
- getIDArr — retorna una matriz con las IDs de las posiciones abiertas de todos los símbolos en el intervalo temporal solicitado. Preciamente con las IDs de las posiciones se hace posible combinar todas las transacciones en la posición en nuestro método. En esencia, se trata de una lista única del campo DealData.ID.
- getDealsDetales — básicamente, se trata de un método semejante al método getHistory, pero ofrece menos detalles. La tarea esencial de este método es ofrecer un recuadro de posiciones cómodamente legible, donde cada línea se corresponda con una transacción concreta. Cada una de las posiciones se describe con la siguiente estructura:
struct DealDetales { string symbol; // Symbol datetime DT_open; // Open date ENUM_DAY_OF_WEEK day_open; // Open day datetime DT_close; // Cloe date ENUM_DAY_OF_WEEK day_close; // Close day double volume; // Volume (lots) bool isLong; // Long/Short double price_in; // Position entry price double price_out; // Position exit price double pl_oneLot; // Profit / loss is trading one lot double pl_forDeal; // Real profit/loss taking into account commission string open_comment; // Comment at the time of opening string close_comment; // Comment at the time of closing };
En general, representan un recuadro de posiciones en el que la clasificación se realiza según la fecha de cierre de las mismas. Esa matriz de datos la vamos a utilizar para calcular los coeficientes en la siguiente clase analizada; por consiguiente, usando como base los datos ofrecidos, obtendremos un informe final sobre la prueba realizada. Dichos datos será también utilizados por el simulador (al finalizar las transacciones) para construir la línea azul del gráfico PL.
Por cierto, ya que hemos tocado el tema del simulador, también debemos destacar que en los siguientes cálculos, el factor de recuperación calculado en el terminal se distinguirá del valor calculado sobre la descarga obtenida. El motivo es que, aunque la descarga de datos es correcta, y las fórmulas sobre las que se calcula el coeficiente descrito son idénticas en el terminal y en la clase posterior, lo datos fuente se diferencian. El simulador calcula el factor de recuperación según la línea verde, es decir, según la descarga detallada, y nosotros vamos a realizar los cálculos según la azul, o sea, según los datos que no tienen encuenta las oscilaciones en el intervalo temporal desde el momento de apertura hasta el cierre.
- getBalance — este método ha sido creado para obtener los datos sobre el balance sin tener en cuenta las operaciones en la fecha indicada.
double CDealHistoryGetter::getBalance(datetime toDate) { if(HistorySelect(0,(toDate>0 ? toDate : TimeCurrent()))) { int total=HistoryDealsTotal(); // Получаем общее количество позиций double balance=0; for(int i=0; i<total; i++) { long ticket=(long)HistoryDealGetTicket(i); ENUM_DEAL_TYPE dealType=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE); if(dealType==DEAL_TYPE_BALANCE || dealType == DEAL_TYPE_CORRECTION || dealType == DEAL_TYPE_COMMISSION) { balance+=HistoryDealGetDouble(ticket,DEAL_PROFIT); if(toDate<=0) break; } } return balance; } else return 0; }
Para conseguir la tarea planteada, primero se solicita la historia de todas las transacciones desde el primer intervalo temporal hasta el indicado. A continuación, el balance se guarda en un ciclo, y después se añaden todos los ingresos y retiradas de fondos al balance inicial, teniendo en cuenta las comisiones y las correcciones recibidas del bróker. Si se ha transmitido como parámetro de entrada una fecha cero, significará que se ha solicitado solo el balance de la fecha al inicio
- getBalanceWithPL — este método es análogo al anterior. No obstante, además de los cambios en el balance, también tiene en cuenta el beneficio y/o pérdidas de las operaciones realizadas, a las que también se añade la comisión según el principio descrito anteriormente.
Clase para crear el informe de optimización: estructuras implicadas en los cálculos
El siguiente de los objetos explicados (ya mencionados en artículos anteriores) será la clase CReportCreator. Esta clase fue brevemente presantada en el artículo Las 100 mejores pasadas de optimización, en el apartado "Cálculos"; sin embargo, debido al exceso de información que incluimos en el artículo, no quedó espacio para analizar la clase. Vamos a subsanar este detalle, puesto que precisamente dicha clase calcula todos los coeficientes en los que se basan las decisiones del optimizador automático sobre la correspondencia de dicha combinación de parámetros del algoritmo con los criterios solicitados.
Durante el análisis de este objeto, debemos describir primero la idea principal incorporada en el enfoque que hemos usado para su implementación. En el primero de nuestros artículos ya implementamos una clase semejante, aunque con una funcionalidad más pobre. Sin embargo, resultaba demasiado lenta: para calcular un grupo de parámetros solicitados o un gráfico, debíamos descargar de nuevo la historia de transacciones e iterar por la misma en un ciclo, y así en cada solicitud del parámetro.
En ocasiones, si el volumen de datos resulta considerable, este enfoque puede ocupar varios segundos, lo cual resulta demasiado tiempo. Precisamente para acelerar los cálculos realizados, hemos utilizado el método de implementación de esta clase que veremos a continuación. Asimismo, la clase contiene muchos más datos, bastantes de los cuales no se ofrecen incluso en la descarga estándar de los informes de la optimización. Si estudiamos un poco el tema, veremos que para calcular muchos coeficientes, necesitamos datos homogéneos, tales como el beneficio/pérdidas máximos o el beneficio/pérdidas acumulados, y otros semejantes.
Por consiguiente, tras calcular estos coeficientes en un único ciclo y guardarlos en los campos de esta clase, podemos aplicar los datos obtenidos para calcular todos los parámetros en cuyos cálculos figuran aquellos. De esta forma, obtendremos una clase que, en una sola pasada en el ciclo por la historia descargada, calculará todos los parámetros necesarios y los guardará hasta el siguiente cálculo. En lo sucesivo, para calcular el parámetro requerido, no se realizará su recálculo, sino que solo se copiarán los valores ya guardados, lo cual simplificará significativamente el trabajo.
Ahora que hemos descrito la idea principal utilizada en su creación, vamos analizar cómo se dan precisamente los cálculos de los parámetros. Comenzaremos analizando los objetos que guardan los datos usados para los posteriores cálculos de los valores buscados. Estos objetos se crean como objetos de clase incorporados, declarados en el ámbito private. Esto se hace por dos motivos: primero, para que no sea posible utilizarlos en otras clases que usarán esta funcionalidad, ya que resulta muy fácil confundirse entre tanta estructura y clase declarada (cuáles de ellas son necesarias para los cálculos externos, y cuáles son solo técnicas o internas); y segundo, para enfatizar su carácter puramente teórico.
Estructura de PL_Keeper:
struct PL_keeper
{
PLChart_item PL_total[];
PLChart_item PL_oneLot[];
PLChart_item PL_Indicative[];
};
La presente estructura se ha creado para guardar todos los gráficos posibles de beneficio y pérdidas. Estos han sido descritos con detalle en nuestro primer artículo, cuyo enlace ya se ha adjuntado. A continuación, debajo de esta estructura, se crean sus ejemplares:
PL_keeper PL,PL_hist,BH,BH_hist;
Cada uno de los ejemplares almacena los 4 tipos de gráfico presentados, pero solo para distintos gráficos fuente. Los datos con el prefijo PL se calculan según la mencionada línea azul del gráfico PL del terminal, mientras que los gráficos con el prefijo BH se calculan según los datos del gráfico de beneficio y pérdidas obtenido de la estrategia Buy and Hold. Los datos con el postfijo hist se calculan según el histograma de beneficio y pérdidas.
Estructura de DailyPL_keeper:
// The structure of Daily PL graphs struct DailyPL_keeper { DailyPL avarage_open,avarage_close,absolute_open,absolute_close; };
Esta estructura contiene los 4 tipos de gráfico posibles de beneficio/pérdidas diarios, dicho de otra forma, el propio histograma en el informe de transacciones en el que se describe el beneficio/pérdidas de las transacciones por días. Los ejemplares de la estructura DailyPLmarcados con el prefijo average, se calculan según los datos promedio de beneficio/pérdidas; aquellos mismos que estén marcados con el prefijo absolute, se calcularán según los datos totales sumados de beneficio y pérdidas. Por consiguiente, la diferencia entre ellos es obvia: en el primer caso, se representa el beneficio medio diario durante todas las transacciones, mientras que en el segundo, se representa el beneficio sumado. Los datos con el prefijo open están clasificados por días usando las fechas de apertura, mientras que los datos con el postfijo close usan las fechas de cierre. El ejemplar de esta estructura, al igual que los ejemplares de las otras estructuras descritas, está declarado más abajo en el código, pero su declaración resulta trivial.
Estructura de RationTable_keeper:
// Table structure of extreme points struct RatioTable_keeper { ProfitDrawdown Total_max,Total_absolute,Total_percent; ProfitDrawdown OneLot_max,OneLot_absolute,OneLot_percent; };
Esta estructura consta de los ejemplares de la estructura ProfitDrawdown
struct ProfitDrawdown { double Profit; // In some cases Profit, in other Profit / Loss double Drawdown; // Drawdown };
Y contiene la relación entre el beneficio y las pérdidas según determinados criterios. Los datos con el prefijo Total se calculan según el gráfico de beneficio/pérdidas construido teniendo en cuenta los cambios en el lotaje durante el comercio de una posición a otra. Los datos con el perfijo OneLot se calculan como si todo el comercio se realizara todo el tiempo con un contrato. Podrá encontra más información sobre este registro no estándar de lotaje en el mencionado primer artículo. En pocas palabras, podemos decir que se ha creado para evaluar los resultados de la actividad del sistema comercial, para que sea posible valorar qué produce más resultados, la gestión del lotaje o la propia lógica del sistema. El postfijo max muestra que en este ejemplar se han introducido los datos sobre el valor máximo de beneficio y reducción en la historia de transacciones. El postfijo absolute muestra la existencia de los datos sumados del beneficio en la historia de transacciones y la reducción en la historia de transacciones. El postfijo percent muestra que los datos introducidos sobre el beneficio y la reducción han sido calculados como el porcentaje respecto al valor máximo en la curva de PL alcanzado en el intervalo investigado. La declaración de esta estructura también es trivial, por lo que no se adjunta.
El siguiente grupo de estructuras no se declara como campo de la clase, pero sí que se usa su declaración local en el método Create principal. Todas las estructuras descritas en esta parte del artículo se combinan en una, por eso vamos a mostrar su definición en un solo lugar, analizando posteriormente cada una ellas por separado.
// Structures for calculating consecutive profits and losses struct S_dealsCounter { int Profit,DD; }; struct S_dealsInARow : public S_dealsCounter { S_dealsCounter Counter; }; // Structures for calculating auxiliary data struct CalculationData_item { S_dealsInARow dealsCounter; int R_arr[]; double DD_percent; double Accomulated_DD,Accomulated_Profit; double PL; double Max_DD_forDeal,Max_Profit_forDeal; double Max_DD_byPL,Max_Profit_byPL; datetime DT_Max_DD_byPL,DT_Max_Profit_byPL; datetime DT_Max_DD_forDeal,DT_Max_Profit_forDeal; int Total_DD_numDeals,Total_Profit_numDeals; }; struct CalculationData { CalculationData_item total,oneLot; int num_deals; bool isNot_firstDeal; };
Las estructuras S_dealsCounter y S_dealsInARow son en esencia una sola unidad. Semejante combinación de asociaciones, así como la herencia simultánea en estas estructuras, se relaciona con el cálculo peculiar de sus parámetros. Para comenzar, debemos decir que la estructura S_dealsInARow ha sido creada para almacenar y calcular el número de transacciones (en realidad, de las posiciones, es decir, nos referimos al ciclo que va desde la apertura hasta el cierre de la posición) sucedidas consecutivamente, tanto positivas, como negativas. El ejemplar incorporado de la estructura S_dealsCounter se declara para almacenar los resultados intermedios de los cálculos, mientras que los campos heredados guardan los valores totales. Más tarde, volveremos a esta operación de cálculo de transacciones rentables/no rentables.
La estructura CalculationData_item contiene los campos necesarios para calcular los coeficientes necesarios.
- R_arr es la matriz de las secuencias de series de transacciones rentables/no rentables, representadas como 1 / 0 respectivamente. Esta matriz se usa para calcular la puntuación Z;
- DD_percent — valor porcentual de la reducción;
- Accomulated_DD, Accomulated_Profit — guardan el valor sumado de las pérdidas y el beneficio;
- PL — beneficio / pérdidas;
- Max_DD_forDeal, Max_Profit_forDeal — según su denominación, guardan el beneficio y pérdidas máximos entre las transacciones;
- Max_DD_byPL, Mаx_Profit_byPL — según su nombre, guardan el beneficio y pérdidas máximos calculados según el gráfico de PL;
- DT_Max_DD_byPL, DT_Max_Profit_byPL — guardan las fechas de las reducciones máximas de PL;
- DT_Max_DD_forDeal, DT_Max_Profit_forDeal — respectivamente, las fechas de las reducciones y beneficios para las transacciones correspondientes;
- Total_DD_numDeals, TotalProfit_numDeals — cantidad sumada de transacciones rentables y no rentables.
A partir de estos datos, se realizan los cálculos posteriores.
La estructura CalculationData es una estructura acumulativa donde se combinan todas las descripciones de las estructuras, en concreto, contiene todos los datos necesarios. En ella también se contiene el campo num_deals, que en esencia supone la suma de los campos CalculationData_item::Total_DD_numDeals y CalculationData_item::TotalProfit_numDeals, mientras que el campo sNot_firstDeal es una bandera técnica que indica que el cálculo en la iteración actual del ciclo no se realiza para la primera transacción.
Estructura CoefChart_keeper:
struct CoefChart_keeper
{
CoefChart_item OneLot_ShartRatio_chart[],Total_ShartRatio_chart[];
CoefChart_item OneLot_WinCoef_chart[],Total_WinCoef_chart[];
CoefChart_item OneLot_RecoveryFactor_chart[],Total_RecoveryFactor_chart[];
CoefChart_item OneLot_ProfitFactor_chart[],Total_ProfitFactor_chart[];
CoefChart_item OneLot_AltmanZScore_chart[],Total_AltmanZScore_chart[];
};
Se ha creado para guardar los gráficos de los coeficientes. Esta clase construye no solo los gráficos de beneficio y pérdidas, sino también los gráficos de algunos de los coeficientes, por ello, de manera análoga a la estructura que guarda los gráficos de beneficio y pérdidas, hemos creado esta estructura para los tipos de datos descritos. El prefijo OneLot indica que en este ejemplar del objeto se guardarán los datos obtenidos mediante el análisis del gráfico de beneficio/pérdidas de las transacciones, si comerciamos con un lote sin tener encuenta la gestión del lotaje. El prefijo Total indica que se ha evaluado el gráfico de transacciones con el registro de gestión del lotaje que ha sido utilizado. Si no se ha usado ningún sistema de gestión del lotaje, ambos gráficos serán idénticos.
La clase СHistoryComparer:
También se define la clase que participa en la clasificación de los datos. Como se puede leer en el artículo "Las 100 mejores pasadas de optimzación", hemos creado la clase CGenericSorter, que sabe clasificar datos de cualquier tipo por orden descendente. No obstante, para que funcione, debemos escribir una clase que pueda comparar los tipos transmitidos. Precisamente la clase СHisoryComparer es esa clase.
class CHistoryComparer : public ICustomComparer<DealDetales> { public: int Compare(DealDetales &x,DealDetales &y); };
La implementación de su método es bastante prosaica. Compara las fechas de cierre, dado que la clasificación se realiza precisamente según ellas:
int CReportCreator::CHistoryComparer::Compare(DealDetales &x,DealDetales &y) { return(x.DT_close == y.DT_close ? 0 : (x.DT_close > y.DT_close ? 1 : -1)); }
Asimismo, existe una clase que clasifica los gráficos de los coeficientes, que tiene una estructura similar. Ambas clases, así como la clase del clasificador, se instalan como campo global de la clase CReportCreator descrita. Asimismo, aparte de los objetos descritos, existen otros dos campos cuyos tipos están descritos como objetos aparte, sin incorporar:
PL_detales PL_detales_data; DistributionChart OneLot_PDF_chart,Total_PDF_chart;
La estructura PL_detales contiene información breve sobre las transacciones para las posiciones rentables y no rentables:
//+------------------------------------------------------------------+ struct PL_detales_PLDD { int orders; // Number of deals double orders_in_Percent; // Number of orders as % of total number of orders int dealsInARow; // Deals in a row double totalResult; // Total result in money double averageResult; // Average result in money }; //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ struct PL_detales_item { PL_detales_PLDD profit; // Information on profitable deals PL_detales_PLDD drawdown; // Information on losing deals }; //+-------------------------------------------------------------------+ //| A brief PL graph summary divided into 2 main blocks | //+-------------------------------------------------------------------+ struct PL_detales { PL_detales_item total,oneLot; };
Y la segunda estructura, DistributionChart, contiene una serie de indicadores VaR, así como el gráfico de distribución conforme al cual se han calculado estos coeficientes. La distribución se calcula como una distribución normal.
//+------------------------------------------------------------------+ //| Structure used for saving distribution charts | //+------------------------------------------------------------------+ struct Chart_item { double y; // y axis double x; // x axis }; //+------------------------------------------------------------------+ //| Structure contains the VaR value | //+------------------------------------------------------------------+ struct VAR { double VAR_90,VAR_95,VAR_99; double Mx,Std; }; //+------------------------------------------------------------------+ //| Structure - it is used to store distribution charts and | //| the VaR values | //+------------------------------------------------------------------+ struct Distribution_item { Chart_item distribution[]; // Distribution chart VAR VaR; // VaR }; //+------------------------------------------------------------------+ //| Structure - Stores distribution data. Divided into 2 blocks | //+------------------------------------------------------------------+ struct DistributionChart { Distribution_item absolute,growth; };
Los propios coeficientes VaR se calculan según la fórmula del más simple: la del VaR histórico, el cual, posiblemente, no dé el resultado más exacto. Sin embargo, para la presente implementación, resultará adecuado.
Métodos de cálculo de coeficientes que describen los resultados de las transacciones
Ahora que nos hemos analizado las estructuras que guardan los datos, ya podemos imaginar el volumen de las estadísticas que calcula esta clase. Vamos a analizar por turno los métodos concretos que se encargan del cálculo de los indicadores descritos, tal y como se nombran en la clase CReportCreator.
El método CalcPL ha sido creado para calcular el gráfico PL. Su implementación es la siguiente:
void CReportCreator::CalcPL(const DealDetales &deal,CalculationData &data,PLChart_item &pl_out[],CalcType type) { PLChart_item item; ZeroMemory(item); item.DT=deal.DT_close; // Saving the date if(type!=_Indicative) { item.Profit=(type==_Total ? data.total.PL : data.oneLot.PL); // Saving the profit item.Drawdown=(type==_Total ? data.total.DD_percent : data.oneLot.DD_percent); // Saving the drawdown } else // Calculating the indicative chart { if(data.isNot_firstDeal) { if(data.total.PL!=0) { if(data.total.PL > 0 && data.total.Max_DD_forDeal < 0) item.Profit=data.total.PL/MathAbs(data.total.Max_DD_forDeal); else if(data.total.PL<0 && data.total.Max_Profit_forDeal>0) item.Profit=data.total.PL/data.total.Max_Profit_forDeal; } } } // Adding data to array int s=ArraySize(pl_out); ArrayResize(pl_out,s+1,s+1); pl_out[s]=item; }
Como podemos ver por su implementación, todos sus cálculos se basan en los datos de las estructuras ya descritas, que se transmiten como parámetro de entrada.
Si tenemos que calcular un gráfico PL de tipo no indicativo, solo tenemos que copiar los datos que ya conocemos. En caso contrario, el cálculo solo se realizará de cumplirse dos condiciones: la primera iteración no se ha encontrado en el ciclo, y PL no es igual a cero. El propio cálculo se realiza según la siguiente lógica:
- Si PL es mayor a cero, y la reducción menor, dividiremos el valor actual de PL por el valor de la reducción. Con ello, obtendremos un coeficiente que indicará cuántas reducciones máximas seguidas se necesitarán para reducir el PL actual a cero.
- Si el PL es menor a cero, y el beneficio máximo alcanzado para todas las transacciones es mayor a cero, dividiremos el valor de PL (que en estos momentos sería la reducción) por el beneficio máximo alcanzado, obteniendo con ello un coeficiente que indicará cuántos beneficios máximos seguidos se necesitarán para reducir la reducción actual a cero.
El siguiente método, CalcPLHist, se basa en un mecanismo similar, pero usa en los cálculos otros campos de estructura, en concreto, data.oneLot.Accomulated_DD, data.total.Accomulated_DD y data.oneLot.Accomulated_Profit, data.total.Accomulated_Profit. Dado que ya hemos analizado su algoritmo de acción, no nos detendremos en este método, vamos a pasar a dos métodos más importantes.
Los métodos CalcData y CalcData_item:
Precisamente en estos métodos tiene lugar el cálculo de todos los coeficientes auxiliares y principales. Comenzaremos el análisis por el método CalcData_item, cuya tarea consiste en calcular los coeficientes auxiliares que analizamos anteriormente, y que sirven para calcular los coeficientes principales.
//+------------------------------------------------------------------+ //| Calculando los datos auxiliares | //+------------------------------------------------------------------+ void CReportCreator::CalcData_item(const DealDetales &deal,CalculationData_item &out, bool isOneLot) { double pl=(isOneLot ? deal.pl_oneLot : deal.pl_forDeal); //PL int n=0; // Кол-прибылей и убытков if(pl>=0) { out.Total_Profit_numDeals++; n=1; out.dealsCounter.Counter.DD=0; out.dealsCounter.Counter.Profit++; } else { out.Total_DD_numDeals++; out.dealsCounter.Counter.DD++; out.dealsCounter.Counter.Profit=0; } out.dealsCounter.DD=MathMax(out.dealsCounter.DD,out.dealsCounter.Counter.DD); out.dealsCounter.Profit=MathMax(out.dealsCounter.Profit,out.dealsCounter.Counter.Profit); // Serie de beneficios y pérdidas int s=ArraySize(out.R_arr); if(!(s>0 && out.R_arr[s-1]==n)) { ArrayResize(out.R_arr,s+1,s+1); out.R_arr[s]=n; } out.PL+=pl; //PL общий // Макс Profit / DD if(out.Max_DD_forDeal>pl) { out.Max_DD_forDeal=pl; out.DT_Max_DD_forDeal=deal.DT_close; } if(out.Max_Profit_forDeal<pl) { out.Max_Profit_forDeal=pl; out.DT_Max_Profit_forDeal=deal.DT_close; } // Profit / DD acumulado out.Accomulated_DD+=(pl>0 ? 0 : pl); out.Accomulated_Profit+=(pl>0 ? pl : 0); // Puntos de extremo según el beneficio double maxPL=MathMax(out.Max_Profit_byPL,out.PL); if(compareDouble(maxPL,out.Max_Profit_byPL)==1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación { out.DT_Max_Profit_byPL=deal.DT_close; out.Max_Profit_byPL=maxPL; } double maxDD=out.Max_DD_byPL; double DD=0; if(out.PL>0) DD=out.PL-maxPL; else DD=-(MathAbs(out.PL)+maxPL); maxDD=MathMin(maxDD,DD); if(compareDouble(maxDD,out.Max_DD_byPL)==-1/* || !isNot_firstDeal*/)// para guardar la fecha será necesaria otra comprobación { out.Max_DD_byPL=maxDD; out.DT_Max_DD_byPL=deal.DT_close; } out.DD_percent=(balance>0 ?(MathAbs(DD)/(maxPL>0 ? maxPL : balance)) :(maxPL>0 ?(MathAbs(DD)/maxPL) : 0)); }
En primer lugar, se realiza el cálculo de PL en la i-ésima iteración. A continuación, si ha habido beneficio en esta iteración, incrementamos el contador de transacciones rentables, y también ponemos a cero el contador de transacciones no rentables consecutivas. Además, asignamos a la variable n el valor 1, que indica que la transacción actual ha sido rentable. Si el PL ha sido menor a cero, incrementamos el contador de pérdidas y ponemos a cero las transacciones rentables consecutivas. Después de ello, asignamos el número máximo de series rentables y no rentables seguidas.
El siguiente paso es calcular las series de transacciones rentables y no rentables. Entendemos por serie un cierto número de transacciones positivas o negativas consecutivas. En esta matriz, tras el cero siempre irá la unidad, y tras la unidad, solo el cero: esta alternancia muestra la alternancia de transacciones positivas y negativas, mientras que en el sitio donde hay, digamos, una unidad, en realidad pueden suceder no una, sino multitud de transacciones positivas; lo mismo sucede con el cero. Esta matriz se usará al calcular la puntuación Z, que indica el nivel de aleatoriedad del comercio. El siguiente paso es la asignación de los valores de beneficio/pérdidas máximos y el cálculo del beneficio/pérdidas acumulados. Al final de este método tiene lugar el cálculo de los puntos extremos, es decir, se rellenan las estructuras con los valores de los beneficios y pérdidas máximos.
El método CalcData ya usa los datos intermedios obtenidos al calcular los coeficientes necesarios y actualiza los cálculos en cada iteración. Su implementación es la siguiente:
void CReportCreator::CalcData(const DealDetales &deal,CalculationData &out,bool isBH) { out.num_deals++; // Counting the number of deals CalcData_item(deal,out.oneLot,true); CalcData_item(deal,out.total,false); if(!isBH) { // Fill PL graphs CalcPL(deal,out,PL.PL_total,_Total); CalcPL(deal,out,PL.PL_oneLot,_OneLot); CalcPL(deal,out,PL.PL_Indicative,_Indicative); // Fill PL Histogram graphs CalcPLHist(deal,out,PL_hist.PL_total,_Total); CalcPLHist(deal,out,PL_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,PL_hist.PL_Indicative,_Indicative); // Fill PL graphs by days CalcDailyPL(DailyPL_data.absolute_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.absolute_open,CALC_FOR_OPEN,deal); CalcDailyPL(DailyPL_data.avarage_close,CALC_FOR_CLOSE,deal); CalcDailyPL(DailyPL_data.avarage_open,CALC_FOR_OPEN,deal); // Fill Profit Factor graphs ProfitFactor_chart_calc(CoefChart_data.OneLot_ProfitFactor_chart,out,deal,true); ProfitFactor_chart_calc(CoefChart_data.Total_ProfitFactor_chart,out,deal,false); // Fill Recovery Factor graphs RecoveryFactor_chart_calc(CoefChart_data.OneLot_RecoveryFactor_chart,out,deal,true); RecoveryFactor_chart_calc(CoefChart_data.Total_RecoveryFactor_chart,out,deal,false); // Fill winning coefficient graphs WinCoef_chart_calc(CoefChart_data.OneLot_WinCoef_chart,out,deal,true); WinCoef_chart_calc(CoefChart_data.Total_WinCoef_chart,out,deal,false); // Fill Sharpe Ration graphs ShartRatio_chart_calc(CoefChart_data.OneLot_ShartRatio_chart,PL.PL_oneLot,deal/*,out.isNot_firstDeal*/); ShartRatio_chart_calc(CoefChart_data.Total_ShartRatio_chart,PL.PL_total,deal/*,out.isNot_firstDeal*/); // Fill Z Score graphs AltmanZScore_chart_calc(CoefChart_data.OneLot_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.oneLot.R_arr),(double)out.oneLot.Total_Profit_numDeals, (double)out.oneLot.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); AltmanZScore_chart_calc(CoefChart_data.Total_AltmanZScore_chart,(double)out.num_deals, (double)ArraySize(out.total.R_arr),(double)out.total.Total_Profit_numDeals, (double)out.total.Total_DD_numDeals/*,out.isNot_firstDeal*/,deal); } else // Fill PL Buy and Hold graphs { CalcPL(deal,out,BH.PL_total,_Total); CalcPL(deal,out,BH.PL_oneLot,_OneLot); CalcPL(deal,out,BH.PL_Indicative,_Indicative); CalcPLHist(deal,out,BH_hist.PL_total,_Total); CalcPLHist(deal,out,BH_hist.PL_oneLot,_OneLot); CalcPLHist(deal,out,BH_hist.PL_Indicative,_Indicative); } if(!out.isNot_firstDeal) out.isNot_firstDeal=true; // Flag "It is NOT the first deal" }
En primer lugar, se calculan los coeficientes intermedios para comerciar con un lote, así como para las transacciones con los sistemas de gestión de lotaje mediante la llamada del método ya descrito para ambos tipos de datos. A continuación, los cálculos se dividen entre los coefientes para BH y para el tipo de datos opuesto. Dentro de cada uno de los bloques, se calculan los coeficientes interpretables. Para la estrategia Buy and Hold, se calculan solo los gráficos, por eso no llamamos los métodos que calculan los coeficientes.
El siguiente grupo de métodos calcula el beneficio/pérdidas por días:
//+------------------------------------------------------------------+ //| Create a structure of trading during a day | //+------------------------------------------------------------------+ void CReportCreator::CalcDailyPL(DailyPL &out,DailyPL_calcBy calcBy,const DealDetales &deal) { cmpDay(deal,MONDAY,out.Mn,calcBy); cmpDay(deal,TUESDAY,out.Tu,calcBy); cmpDay(deal,WEDNESDAY,out.We,calcBy); cmpDay(deal,THURSDAY,out.Th,calcBy); cmpDay(deal,FRIDAY,out.Fr,calcBy); } //+------------------------------------------------------------------+ //| Save resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::cmpDay(const DealDetales &deal,ENUM_DAY_OF_WEEK etalone,PLDrawdown &ans,DailyPL_calcBy calcBy) { ENUM_DAY_OF_WEEK day=(calcBy==CALC_FOR_CLOSE ? deal.day_close : deal.day_open); if(day==etalone) { if(deal.pl_forDeal>0) { ans.Profit+=deal.pl_forDeal; ans.numTrades_profit++; } else if(deal.pl_forDeal<0) { ans.Drawdown+=MathAbs(deal.pl_forDeal); ans.numTrades_drawdown++; } } } //+------------------------------------------------------------------+ //| Average resulting PL/DD for the day | //+------------------------------------------------------------------+ void CReportCreator::avarageDay(PLDrawdown &day) { if(day.numTrades_profit>0) day.Profit/=day.numTrades_profit; if(day.numTrades_drawdown > 0) day.Drawdown/=day.numTrades_drawdown; }
Como podemos ver por la implementación mostrada, el trabajo principal en cuanto a la división según el beneficio/pérdidas por días tiene lugar en el método cmpDay, que primero comprueba si el día se corresponde o no con el solicitado, y a continuación simplemente añade los valores de beneficio y pérdidas. No obstante las pérdidas son sumadas en módulo. CalcDailyPL es un método de agregación en el que se intenta añadir el PL actual transmitido a uno de los cinco días laborables. El método avarageDay es llamado para promediar el beneficio/pérdidas en el método principal Create. Este método no hace nada especial, solo convierte en valores medios los valores absolutos de beneficio/pérdidas calculados anteriormente.
Método que calcula el Factor de Beneficio
//+------------------------------------------------------------------+ //| Calculate Profit Factor | //+------------------------------------------------------------------+ void CReportCreator::ProfitFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); if(dd==0) item.coef=0; else item.coef=profit/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
En esencia, este método calcula un gráfico con el cambio del factor de beneficio durante las transacciones. La última de estas será precisamente el coeficiente que se representa en el informe de simulación. La fórmula es sencilla = beneficio acumulado / pérdidas acumuladas. Si la reducción es cero, el coeficiente será igual a cero, dado que en aritmética clásica, es imposible dividir por cero sin usar límites, y esta regla se aplica en el leguaje utilizado. Por lo tanto, nosotros vamos a realizar las comprobaciones correspondientes del divisor en todas las operaciones aritméticas.
El factor de recuperación también se calcula de forma análoga:
//+------------------------------------------------------------------+ //| Calculate Recovery Factor | //+------------------------------------------------------------------+ void CReportCreator::RecoveryFactor_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double pl=(isOneLot ? data.oneLot.PL : data.total.PL); double dd=MathAbs(isOneLot ? data.oneLot.Max_DD_byPL : data.total.Max_DD_byPL); if(dd==0) item.coef=0;//ideally it should be plus infinity else item.coef=pl/dd; int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
La fórmula de cálculo de este coeficiente es: beneficio por la i-ésima iteración / reducción por la i-ésima iteración. Notemos también que, como el beneficio en el momento del cálculo de este coeficiente puede ser cero o negativo, el propio coeficiente podrá ser cero o negativo.
Coeficiente de ganancia
//+------------------------------------------------------------------+ //| Calculate Win Rate | //+------------------------------------------------------------------+ void CReportCreator::WinCoef_chart_calc(CoefChart_item &out[],CalculationData &data,const DealDetales &deal,bool isOneLot) { CoefChart_item item; item.DT=deal.DT_close; double profit=(isOneLot ? data.oneLot.Accomulated_Profit : data.total.Accomulated_Profit); double dd=MathAbs(isOneLot ? data.oneLot.Accomulated_DD : data.total.Accomulated_DD); int n_profit=(isOneLot ? data.oneLot.Total_Profit_numDeals : data.total.Total_Profit_numDeals); int n_dd=(isOneLot ? data.oneLot.Total_DD_numDeals : data.total.Total_DD_numDeals); if(n_dd == 0 || n_profit == 0) item.coef = 0; else item.coef=(profit/n_profit)/(dd/n_dd); int s=ArraySize(out); ArrayResize(out,s+1,s+1); out[s]=item; }
Fórmula de cálculo del coeficiente de ganancia = (beneficio / número de transacciones rentables) / (reducción / número de transacciones no rentables). Este coeficiente también puede ser negativo si no hay beneficio en el momento del cálculo.
El coeficiente de Sharpe es un poco más complicado que los anteriormente descritos:
//+------------------------------------------------------------------+ //| Calculate Sharpe Ratio | //+------------------------------------------------------------------+ double CReportCreator::ShartRatio_calc(PLChart_item &data[]) { int total=ArraySize(data); double ans=0; if(total>=2) { double pl_r=0; int n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { pl_r+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } if(n>=2) pl_r/=(double)n; double std=0; n=0; for(int i=1; i<total; i++) { if(data[i-1].Profit!=0) { std+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-pl_r,2); n++; } } if(n>=2) std=MathSqrt(std/(double)(n-1)); ans=(std!=0 ?(pl_r-r)/std : 0); } return ans; }
En el primer ciclo, se calcula la rentabilidad media del gráfico PL, donde cada i-ésimo beneficio se calcula como la ratio del incremento sobre el anterior valor de PL respecto al anterior valor de PL. Como ejemplo se ha tomado el modo de normalización de la serie de precios usado para valorar las series temporales.
A continuación, en el siguiente ciclo, se calcula la volatilidad. Esta se calcula según la misma serie normalizada de beneficios
Finalmente, se calcula el propio coeficiente según la fórmula (beneficio medio - tasa de interés libre de riesgo) / volatilidad (desvío estándar de la rentabilidad).
Es posible que nos hayamos permitido ciertas libertades en este coeficiente en cuanto a la normalización de la serie, y también respecto a las propias fórmulas, pero desde el punto de vista de la lógica, todo parece bastante sólido. Si nos hemos equivocado en algo, o hemos incurrido en algún error intolerable, no dude en notificarlo en los comentarios.
El cálculo del VaR y el gráfico de distribución normal. Esta parte de los cálculos consta de tres métodos. Como siempre, dos de ellos se dedican al cálculo, mientras que el tercero agrega todos los cálculos. Vamos a verlos por orden.
//+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ void CReportCreator::NormalPDF_chart_calc(DistributionChart &out,PLChart_item &data[]) { double Mx_absolute=0,Mx_growth=0,Std_absolute=0,Std_growth=0; int total=ArraySize(data); ZeroMemory(out.absolute); ZeroMemory(out.growth); ZeroMemory(out.absolute.VaR); ZeroMemory(out.growth.VaR); ArrayFree(out.absolute.distribution); ArrayFree(out.growth.distribution); // Calculation of distribution parameters if(total>=2) { int n=0; for(int i=0; i<total; i++) { Mx_absolute+=data[i].Profit; if(i>0 && data[i-1].Profit!=0) { Mx_growth+=(data[i].Profit-data[i-1].Profit)/data[i-1].Profit; n++; } } Mx_absolute/=(double)total; if(n>=2) Mx_growth/=(double)n; n=0; for(int i=0; i<total; i++) { Std_absolute+=MathPow(data[i].Profit-Mx_absolute,2); if(i>0 && data[i-1].Profit!=0) { Std_growth+=MathPow((data[i].Profit-data[i-1].Profit)/data[i-1].Profit-Mx_growth,2); n++; } } Std_absolute=MathSqrt(Std_absolute/(double)(total-1)); if(n>=2) Std_growth=MathSqrt(Std_growth/(double)(n-1)); // Calculate VaR out.absolute.VaR.Mx=Mx_absolute; out.absolute.VaR.Std=Std_absolute; out.absolute.VaR.VAR_90=VaR(Q_90,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_95=VaR(Q_95,Mx_absolute,Std_absolute); out.absolute.VaR.VAR_99=VaR(Q_99,Mx_absolute,Std_absolute); out.growth.VaR.Mx=Mx_growth; out.growth.VaR.Std=Std_growth; out.growth.VaR.VAR_90=VaR(Q_90,Mx_growth,Std_growth); out.growth.VaR.VAR_95=VaR(Q_95,Mx_growth,Std_growth); out.growth.VaR.VAR_99=VaR(Q_99,Mx_growth,Std_growth); // Calculate distribution for(int i=0; i<total; i++) { Chart_item item_a,item_g; ZeroMemory(item_a); ZeroMemory(item_g); item_a.x=data[i].Profit; item_a.y=PDF_calc(Mx_absolute,Std_absolute,data[i].Profit); if(i>0) { item_g.x=(data[i-1].Profit != 0 ?(data[i].Profit-data[i-1].Profit)/data[i-1].Profit : 0); item_g.y=PDF_calc(Mx_growth,Std_growth,item_g.x); } int s=ArraySize(out.absolute.distribution); ArrayResize(out.absolute.distribution,s+1,s+1); out.absolute.distribution[s]=item_a; s=ArraySize(out.growth.distribution); ArrayResize(out.growth.distribution,s+1,s+1); out.growth.distribution[s]=item_g; } // Ascending sorter.Sort<Chart_item>(out.absolute.distribution,&chartComparer); sorter.Sort<Chart_item>(out.growth.distribution,&chartComparer); } } //+------------------------------------------------------------------+ //| Calculate VaR | //+------------------------------------------------------------------+ double CReportCreator::VaR(double quantile,double Mx,double Std) { return Mx-quantile*Std; } //+------------------------------------------------------------------+ //| Distribution calculation | //+------------------------------------------------------------------+ double CReportCreator::PDF_calc(double Mx,double Std,double x) { if(Std!=0) return MathExp(-0.5*MathPow((x-Mx)/Std,2))/(MathSqrt(2*M_PI)*Std); else return 0; }
El método de cálculo del VaR es el más sencillo: usa en sus cálculos el modelo del VaR histórico, como hemos mencionado anteriormente en el artículo.
El método de cálculo para la distribución normal, para mayor precisión, ha sido tomado por completo del paquete de análisis estadístico Matlab
El método para el cálculo de la distribución normal y la construcción de su gráfico es un método agregador en el que se aplican todos los métodos anteriormente descritos. En el primer ciclo, se calcula el valor medio del beneficio, mientras que en el segundo, se realiza el cálculo de la desviación media cuadrática de la rentabilidad. En este caso, además, la propia rentabilidad para el gráfico y el VaR (calculado según los incrementos), se calculanotra vez como una serie temporal normalizada. A continuación, después de rellenar los indicadores del VaR, se calcula el gráfico de distribución normal con ayuda del método mencionado. Como valores del eje x se usan las ya mencionadas rentabilidades para el gráfico calculado según los incrementos, así como los valores absolutos de beneficio para el gráfico calculado según los beneficios.
El cálculo de la puntuación Z también es bastante trivial: la fórmula se ha tomado de uno de los artículos escritos en este sitio web. Por ello, consideramos posible omitir el código con su implementación, pero usted podrá leerlo en los archivos adjuntos.
Como finalización de la descripción de esta clase, indicaremos que todos los cálculos comienzan por el método Calculate con la siguiente signatura de llamada
void CReportCreator::Create(DealDetales &history[],DealDetales &BH_history[],const double _balance,const string &Symb[],double _r);
Su implementación se ha analizado en el artículo "Las 100 mejores pasadas de optimización", por eso no la mostraremos en este. Los métodos públicos tampoco son de gran interés, ya que no realizan ningún trabajo lógico, solo sirven de getters que generan los datos solicitados de acuerdo con los parámetros de entrada que indican el tipo de información necesaria.
Conclusión
Tras analizar en el artículo anterior el proceso de escritura de una biblioteca en el lenguaje C#, hemos pasado a la siguiente etapa: la creación de un informe comercial que precisamente se descargará con la ayuda de la biblioteca DLL desarrollada y los mecanismos que veremos en el siguiente artículo. El propio mecanismo de generación de informes, como hemos mencionado, ha sido tomado de desarrollos anteriores, pero desde que fueron creados, han tenido una serie de mejoras. En el presente artículo, hemos mostrado las versiones más recientes de los desarrollos. Asimismo, la solución ofrecida se ha puesto a prueba durante varios meses de optimizaciones y descargas de informes.
En el fichero adjunto se encuentran dos carpetas, ambas deberán ser descomprimidas en el directorio MQL/Include.
El fichero contiene los siguientes archivos:
- CustomGeneric
- GenericSorter.mqh
- ICustomComparer.mqh
- History manager
- CustomComissionManager.mqh
- DealHistoryGetter.mqh
- ReportCreator.mqh
Traducción del ruso hecha por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/ru/articles/7452





- Aplicaciones de trading gratuitas
- 8 000+ señales para copiar
- Noticias económicas para analizar los mercados financieros
Usted acepta la política del sitio web y las condiciones de uso