Avaliação visual de resultados de otimização
Introdução
Os critérios de otimização personalizados são parâmetros muito convenientes e flexíveis para otimizar Expert Advisors, porém, para verificar vários critérios, precisamos executar várias otimizações, o que consome muito tempo. Eu gostaria de poder verificar vários critérios personalizados de uma só vez em uma otimização. E não apenas ficar por aí, senão que também ver imediatamente o gráfico de saldo e capital líquido.
Parafraseando o famoso escritor Sergei Mikhalkov, podemos dizer o seguinte: diferentes visualizações são necessárias, diferentes visualizações são importantes. E esta será uma afirmação verdadeira, já que mais de oitenta por cento das informações são recebidas pelo nosso cérebro através dos olhos. Por isso, a discussão neste artigo se concentrará em como construir gráficos que abarquem todas as otimizações e como selecionar o critério personalizado ideal.
Além disso, falaremos sobre como programarmos o que quisermos, simplesmente recorrendo a um conhecimento mínimo em MQL5, a uma grande vontade, ao uso dos artigos do site e aos comentários do fórum.
Definamos a tarefa
- Coletar dados de cada passagem de otimização.
- Plotar gráficos de saldo/capital líquido para cada passagem de otimização.
- Calcular vários critérios personalizados de otimização.
- Classificar os gráficos por critério de otimização personalizado crescente.
- Mostrar os melhores resultados de todos os critérios personalizados.
Etapas para resolver o problema
Como não podemos ficar sem fazer alterações no código do EA, tentaremos pelo menos minimizá-las.
- Por isso, todo o código destinado à coleta de dados será escrito no arquivo incluído SkrShotOpt.mqh, e o critério personalizado será calculado no arquivo CustomCriterion.mqh,
- O script ScreenShotOptimization.mq5 desenhará gráficos e salvará capturas de tela.
Assim, apenas algumas linhas de código precisarão ser adicionadas ao EA.
1. Coleta de dados. SkrShotOpt.mqh
A função OnTick() armazenará os valores de capital líquido máximo e mínimo.
double _Equity = AccountInfoDouble(ACCOUNT_EQUITY); if(tempEquityMax < _Equity) tempEquityMax = _Equity; if(tempEquityMin > _Equity) tempEquityMin = _Equity;
Para não verificar as alterações de posição a cada tick, rastrearemos as alterações de posição na função OnTradeTransaction()
void IsOnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request, const MqlTradeResult & result) { if(trans.type == TRADE_TRANSACTION_DEAL_ADD) if(HistoryDealSelect(trans.deal)) { if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) _deal_entry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY); if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL) if(_deal_entry == DEAL_ENTRY_IN || _deal_entry == DEAL_ENTRY_OUT || _deal_entry == DEAL_ENTRY_INOUT || _deal_entry == DEAL_ENTRY_OUT_BY) allowed = true; } }
Ao alterar o número de negócios abertos, preenchemos as matrizes de saldo e de capital líquido.
if(allowed) // if there was a trade { double accBalance = AccountInfoDouble(ACCOUNT_BALANCE); double accEquity = AccountInfoDouble(ACCOUNT_EQUITY); ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = accBalance; if(_deal_entry != DEAL_ENTRY_OUT && _deal_entry != DEAL_ENTRY_OUT_BY) // if a new position appeared equity[_size] = accEquity; else // if position closed { if(changesB < accBalance) equity[_size] = tempEquityMin; else switch(s_view) { case min_max_E: equity[_size] = tempEquityMax; break; default: equity[_size] = tempEquityMin; break; } tempEquityMax = accEquity; tempEquityMin = accEquity; } _size = _size + 1; changesPos = PositionsTotal(); changesB = accBalance; _deal_entry = -1; allowed = false; }
Como o arquivo que contém quadros não é de borracha aumentando para tamanhos não processáveis devido a inúmeros negócios, teremos de registrar o mínimo necessário de informações.
Ao abrir um negócio, registramos o valor do saldo e do capital líquido:
- se o negócio for fechado no vermelho, registramos o valor do capital líquido máximo
- já se o negócio for fechado no positivo, registramos o valor do capital líquido mínimo
Assim, quase todos os negócios possuem quatro valores armazenados em matrizes: saldo e capital líquido na abertura, saldo e capital líquido máximo/mínimo no fechamento.
Acontece que, quando em um tick uma posição é fechada e outra é aberta, será registrada apenas uma posição e não duas. Isso não afetará os cálculos e a visualização dos gráficos, mas as matrizes diminuirão.
Salvando os dados coletados em um arquivo
Faz sentido coletar apenas execuções de otimizações rentáveis. Este parâmetro é movido para as configurações, portanto, se desejarmos, podemos gravar as não lucrativas. Todas as passagens forward são registradas.
Os dados coletados serão salvos em um arquivo utilizando a função FrameAdd(), ao final de cada execução, após ocorrido o evento Tester, este evento será processado pela função OnTester().
bool FrameAdd( const string name, // public name/tag long id, // public id double value, // value const void& data[] // array of any type );
Para mais informações sobre como usar a função FrameAdd() consulte: https://www.mql5.com/ru/forum/11277/page4#comment_469771
Como FrameAdd() pode escrever apenas uma matriz e um valor numérico value, e, além do saldo e do capital líquido, quero transferir todos os valores da enumeração ENUM_STATISTICS para análise posterior, decidi escrever tudo em uma matriz sequencialmente e gravar o tamanho da mesma no valor numérico transmitido value.
if(id == 1) // if it is a backward pass { // if profit % and the number of trades exceed those specified in the settings, the pass is written into the file if(TesterStatistics(STAT_PROFIT) / TesterStatistics(STAT_INITIAL_DEPOSIT) * 100 > _profit && TesterStatistics(STAT_TRADES) >= trades) { double TeSt[42]; // total number of elements in the ENUM_STATISTICS enumeration is 41 IsRecordStat(TeSt); // writing testing statistics to the array IsCorrect(); // adjusting balance and equity arrays if(m_sort != none) { while((sort)size_sort != none) size_sort++; double LRB[], LRE[], coeff[]; Coeff = Criterion(balance, equity, LRB, LRE, TeSt, coeff, 3);// calculating custom criterion ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add array with ENUM_STATISTICS data to the resulting array FrameAdd(name, id, _size + 1, balance); // write the frame into the file } else { ArrayInsert(balance, equity, _size + 1, 0); // joining balance and equity arrays into one ArrayInsert(balance, TeSt, (_size + 1) * 2, 0); // add array with ENUM_STATISTICS data to the resulting array FrameAdd(name, id, _size + 1, balance); // write the frame into the file } } }
As passagens forward são processadas da mesma forma que as backward, mas na verdade elas são o resultado da otimização, portanto, apenas os valores de saldo e capital líquido serão salvos, já o valor de enumeração ENUM_STATISTICS não.
Acontece que, quando no final do teste há uma posição aberta, o testador irá fechá-la por conta própria.
Por isso, fechamos o negócio virtualmente (registramos o saldo atual e o capital líquido) se a variável que armazena o número de negócios abertos for diferente de zero no final do teste.
void IsCorrect() { if(changesPos > 0) // if there is an open position by the testing end time, it should be virtually closed as the tester will close such a position { _size++; ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); if(balance[_size - 2] > AccountInfoDouble(ACCOUNT_BALANCE)) { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); switch(s_view) { case min_max_E: equity[_size - 1] = tempEquityMax; break; default: equity[_size - 1] = tempEquityMin; break; } } else { balance[_size - 1] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size - 1] = tempEquityMin; } balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } else { ArrayResize(balance, _size + 1); ArrayResize(equity, _size + 1); balance[_size] = AccountInfoDouble(ACCOUNT_BALANCE); equity[_size] = AccountInfoDouble(ACCOUNT_EQUITY); } }
Isso completa o registro de dados.
Leitura de dados a partir de um arquivo. ScreenShotOptimization.mq5
Após a otimização, é criado um arquivo com quadros: C:\Users\nome de usuário\AppData\Roaming\MetaQuotes\Terminal\identificador do terminal\MQL5\Files\Tester com extensão: nome do EA.símbolo.timeframe.mqd. Após a otimização, perde-se o acesso a este arquivo, mas após reiniciar o terminal, ele pode ser acessado usando funções de arquivo comuns.
Encontramos o arquivo necessário com quadros, ele está na pasta C:\Users\nome de usuário\AppData\Roaming\MetaQuotes\Terminal\ID do terminal\MQL5\Files\Tester.
int count = 0; long search_handle = FileFindFirst("Tester\\*.mqd", FileName); do { if(FileName != "") count++; FileName = "Tester\\" + FileName; } while(FileFindNext(search_handle, FileName)); FileFindClose(search_handle);
Primeiro, a leitura entra na estrutura.
FRAME Frame = {0}; FileReadStruct(handle, Frame); struct FRAME { ulong Pass; long ID; short String[64]; double Value; int SizeOfArray; long Tmp[2]; void GetArrayB(int handle, Data & m_FB) { ArrayFree(m_FB.Balance); FileReadArray(handle, m_FB.Balance, 0, (int)Value); ArrayFree(m_FB.Equity); FileReadArray(handle, m_FB.Equity, 0, (int)Value); ArrayFree(m_FB.TeSt); FileReadArray(handle, m_FB.TeSt, 0, (SizeOfArray / sizeof(m_FB.TeSt[0]) - (int)Value * 2)); } void GetArrayF(int handle, Data & m_FB, int size) { FileReadArray(handle, m_FB.Balance, size, (int)Value); FileReadArray(handle, m_FB.Equity, size, (int)Value); } };
Nas funções da estrutura FRAME, são preenchidas as matrizes da estrutura Data que permitirão construir gráficos posteriormente.
struct Data { ulong Pass; long id; int size; double Balance[]; double Equity[]; double LRegressB[]; double LRegressE[]; double coeff[]; double TeSt[]; }; Data m_Data[];
Como desenhar vários milhares de capturas de tela é demorado, devemos especificar um parâmetro nas configurações do script para salvar capturas de tela só com rendimentos superiores a uma determinada porcentagem.
Todo o arquivo com quadros é processado em um loop.
Para desenhar um gráfico, é necessária uma matriz com dados, por isso, primeiro, são registradas todas as passagens backward que satisfaçam a porcentagem de lucro especificada nas configurações do script.
Em seguida, são percorridas todas as passagens backward, sendo as passagens forward selecionadas em conformidade pelo número da mesma, e a matriz de saldo da passagem forward é adicionada à matriz de saldo da passagem backward.
Também é previsto o desenho de dois tipos de gráficos - um é o mesmo do testador de estratégia, o que quer dizer que a execução forward começa com o depósito inicial.
A segunda variante da passagem forward começa com o depósito que encerrou a passagem backward. Nesse caso, o valor do saldo da passagem backward é somado aos valores de saldo e capital líquido da passagem forward, e também é adicionado ao final da matriz com a passagem backward.
Obviamente, isso é feito se a otimização foi realizada com uma forward.
int handle = FileOpen(FileName, FILE_READ | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_BIN); if(handle != INVALID_HANDLE) { FileSeek(handle, 260, SEEK_SET); while(Res && !IsStopped()) { FRAME Frame = {0}; // read from the file to the Frame structure Res = (FileReadStruct(handle, Frame) == sizeof(Frame)); if(Res) if(Frame.ID == 1) // if it is a Backward pass, write data to the m_Data structure { ArrayResize(m_Data, size + 1); m_Data[size].Pass = Frame.Pass; m_Data[size].id = Frame.ID; m_Data[size].size = (int)Frame.Value; Frame.GetArrayB(handle, m_Data[size]); // write data to the m_Data structure arrays // if profit of this pass corresponds to the input settings, immediately calculate optimization criteria if(m_Data[size].TeSt[STAT_PROFIT] / m_Data[size].TeSt[STAT_INITIAL_DEPOSIT] * 100 >= profitPersent) { Criterion(m_Data[size].Balance, m_Data[size].Equity, m_Data[size].LRegressB, m_Data[size].LRegressE, m_Data[size].TeSt, m_Data[size].coeff, m_lineR); size++; } } else // if it is a Forward pass, write to the end of the m_Data data structures if(m_Forward != BackOnly) // if drawing of only Backward passes is not selected in settings for(int i = 0; i < size; i++) { if(Frame.Pass == m_Data[i].Pass) // if Back and Forward pass numbers match { int m = 0; if(m_Forward == Back_Next_Forward) // if selected drawing of Forward graph as a continuation of Backward { Frame.GetArrayF(handle, m_Data[i], m_Data[i].size - 1); // write data at the end of the the m_Data structure array, with a one-trade shift for(int x = m_Data[i].size - 1; x < m_Data[i].size + (int)Frame.Value - 1; x++) { m_Data[i].Balance[x] = m_Data[i].Balance[x] + m_Data[i].TeSt[STAT_PROFIT]; // add profit of the Backward test to the Forward pass m_Data[i].Equity[x] = m_Data[i].Equity[x] + m_Data[i].TeSt[STAT_PROFIT]; } m = 1; } else Frame.GetArrayF(handle, m_Data[i], m_Data[i].size); // is drawing of a Forward pass from a starting balance is selected m_Data[i].coeff[Forward_Trade] = (int)(Frame.Value / 2); // number of forward trades (not exact)) m_Data[i].coeff[Profit_Forward] = m_Data[i].Balance[m_Data[i].size + (int)Frame.Value - m - 1] - m_Data[i].Balance[m_Data[i].size - m]; break; } if(i == size - 1) // if no Backward is found for this Forward pass, move the file pointer to the end of writing FileSeek(handle, Frame.SizeOfArray, SEEK_CUR); // of this frame as if we read array data from the file } } FileClose(handle); //---
Plotagem
Função de gráficos.
string _GraphPlot(double& y1[], double& y2[], double& LRegressB[], double& LRegressE[], double& coeff[], double& TeSt[], ulong pass) { CGraphic graphic; //--- create graphic bool res = false; if(ObjectFind(0, "Graphic") >= 0) res = graphic.Attach(0, "Graphic"); else res = graphic.Create(0, "Graphic", 0, 0, 0, _width, _height); if(!res) return(NULL); graphic.BackgroundMain(FolderName); // print the Expert Advisor name graphic.BackgroundMainSize(FontSet + 1); // font size for the Expert Advisor name graphic.IndentLeft(FontSet); graphic.HistoryNameSize(FontSet); // font size for the line names graphic.HistorySymbolSize(FontSet); graphic.XAxis().Name("pass " + IntegerToString(pass)); // show the pass number along the X axis graphic.XAxis().NameSize(FontSet + 1); graphic.XAxis().ValuesSize(12); // price font size graphic.YAxis().ValuesSize(12); //--- add curves CCurve *curve = graphic.CurveAdd(y1, ColorToARGB(clrBlue), CURVE_POINTS_AND_LINES, "Balance"); // plot the balance graph curve.LinesWidth(widthL); // graph line width curve.PointsSize(widthL + 1); // size of dots on the balance graph CCurve *curve1 = graphic.CurveAdd(y2, ColorToARGB(clrGreen), CURVE_LINES, "Equity"); // plot the equity graph curve1.LinesWidth(widthL); int size = 0; switch(m_lineR) // plot the regression line { case lineR_Balance: // balance regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); } break; case lineR_Equity: // equity regression line { size = ArraySize(LRegressE); CCurve *curve2 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; case lineR_BalanceEquity: // balance and equity regression line { size = ArraySize(LRegressB); CCurve *curve2 = graphic.CurveAdd(LRegressB, ColorToARGB(clrBlue), CURVE_LINES, "LineR_Balance"); curve2.LinesWidth(widthL); CCurve *curve3 = graphic.CurveAdd(LRegressE, ColorToARGB(clrRed), CURVE_LINES, "LineR_Equity"); curve2.LinesWidth(widthL); } break; default: break; } //--- plot curves graphic.CurvePlotAll(); // Important!!! All lines and captions must be created after creating the graph; otherwise, the graph will override them if(size == 0) { size = ArraySize(LRegressE); if(size == 0) size = ArraySize(LRegressB); } int x1 = graphic.ScaleX(size - 1); //Scales the value of the number of trades along the X axis graphic.LineAdd(x1, 30, x1, _height - 45, ColorToARGB(clrBlue), LINE_END_BUTT); // construct a vertical line which indicates the end of the Backward pass string txt = ""; int txt_x = 70;// text indent along the X axis int txt_y = 30;// text indent along the Y axis graphic.FontSet("Arial", FontSet);// Set current font parameters for(int i = 0; i < size_sort; i++) // Write all coefficients and criteria on the chart { if(coeff[i] == 0) continue; if(i == 1 || i == 3) txt = StringFormat("%s = %d", EnumToString((sort)i), (int)coeff[i]); else if(i == 0 || i == 2) txt = StringFormat("%s = %.2f", EnumToString((sort)i), coeff[i]); else txt = StringFormat("%s = %.4f", EnumToString((sort)i), coeff[i]); graphic.TextAdd(txt_x, txt_y + FontSet * i, txt, ColorToARGB(clrGreen)); } txt_y = txt_y + FontSet * (size_sort - 1); txt = StringFormat("Profitability = %.2f", TeSt[STAT_PROFIT_FACTOR]); graphic.TextAdd(txt_x, txt_y + FontSet, txt, ColorToARGB(clrGreen)); txt = StringFormat("Expected payoff = %.2f", TeSt[STAT_EXPECTED_PAYOFF]); graphic.TextAdd(txt_x, txt_y + FontSet * 2, txt, ColorToARGB(clrGreen)); graphic.Update(); //--- return resource name return graphic.ChartObjectName(); }
Para entender como trabalhar com CGraphic, podemos ler os artigos:
- Escrevemos um livro de ofertas de scalping com base na biblioteca gráfica CGraphic
- Visualize isto! Biblioteca gráfica em MQL5 como equivalente a plot de R
- Visualização dos resultados de otimização pelo critério selecionado
- Trabalhando com resultados de otimização por meio de uma interface gráfica
- Interfaces gráficas XI: Integração da Biblioteca Gráfica Padrão (build 16)
As capturas de tela do gráfico são salvas em uma pasta separada - Files. O nome da pasta contém: nome do EA.símbolo.timeframe.
bool BitmapObjectToFile(const string ObjName, const string _FileName, const bool FullImage = true) { if(ObjName == "") return(true); const ENUM_OBJECT Type = (ENUM_OBJECT)ObjectGetInteger(0, ObjName, OBJPROP_TYPE); bool Res = (Type == OBJ_BITMAP_LABEL) || (Type == OBJ_BITMAP); if(Res) { const string Name = __FUNCTION__ + (string)MathRand(); ObjectCreate(0, Name, OBJ_CHART, 0, 0, 0); ObjectSetInteger(0, Name, OBJPROP_XDISTANCE, -5e3); const long chart = ObjectGetInteger(0, Name, OBJPROP_CHART_ID); Res = ChartSetInteger(chart, CHART_SHOW, false) && ObjectCreate(chart, Name, OBJ_BITMAP_LABEL, 0, 0, 0) && ObjectSetString(chart, Name, OBJPROP_BMPFILE, ObjectGetString(0, ObjName, OBJPROP_BMPFILE)) && (FullImage || (ObjectSetInteger(chart, Name, OBJPROP_XSIZE, ObjectGetInteger(0, ObjName, OBJPROP_XSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_YSIZE, ObjectGetInteger(0, ObjName, OBJPROP_YSIZE)) && ObjectSetInteger(chart, Name, OBJPROP_XOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_XOFFSET)) && ObjectSetInteger(chart, Name, OBJPROP_YOFFSET, ObjectGetInteger(0, ObjName, OBJPROP_YOFFSET)))) && ChartScreenShot(chart, FolderName + "\\" + _FileName, (int)ObjectGetInteger(chart, Name, OBJPROP_XSIZE), (int)ObjectGetInteger(chart, Name, OBJPROP_YSIZE)); ObjectDelete(0, Name); } return(Res); }
Aqui estão os gráficos resultantes.
É assim que os gráficos ficam na pasta.
Se for selecionado imprimir todas as telas, os nomes das capturas de tela serão: o critério personalizado selecionado para classificação + lucro + número da passagem.
Se for selecionado imprimir apenas as melhores, os nomes das capturas de tela serão: critério personalizado + lucro.
O gráfico desenhado pelo script fica assim:
Bem, para comparação, o mesmo gráfico do testador de estratégia
Aqui mostrei os gráficos mais parecidos, mas na maioria dos casos os gráficos serão muito diferentes dos de teste. Em primeiro lugar, devido ao fato de que no gráfico do testador, os negócios no eixo X estão vinculados ao tempo e o script desenha gráficos nos quais o eixo X está vinculado ao número de negócios. Além disso, devido ao fato de termos que escrever um mínimo de informações no quadro para não inflar o arquivo, os valores de capital líquido claramente não são suficientes para analisar completamente os gráficos desenhados pelo script, mas essas informações são suficientes para obter uma avaliação inicial da eficácia de uma passagem de otimização específica e também para calcular um critério de otimização personalizado.
Após a otimização, devemos reiniciar o terminal antes de executar o script ScreenShotOptimization!
Inicialmente, queria ver todos os gráficos de otimização, mas após a implementação vi sete mil capturas de tela na pasta, e notei que era impossível trabalhar com tantas. É necessário escolher as melhores e definir critérios para isso.
Há muito tempo eu tenho notado que os algotraders se enquadram em duas categorias:
- Aqueles que acreditam que o EA precisa ser otimizado durante um período de tempo muito longo, de preferência várias décadas.
- Aqueles que pensam que o EA precisa ser reotimizado regularmente usando um curto período de tempo, como um mês de otimização + uma semana de negociação, três meses de otimização + um mês de negociação ou outro período de reotimização.
Eu pertenço ao segundo tipo.
Por isso, optou-se por buscar critérios de otimização personalizados que servirão de filtro para selecionar as melhores.
Criando critérios de otimização personalizados
Os cálculos de todos os critérios de otimização personalizados serão feitos pelo arquivo incluído CustomCriterion.mqh, pois todos eles serão usados tanto no trabalho do script para desenhar gráficos quanto no trabalho do Expert Advisor a ser otimizado.
Antes de criar meu próprio critério de otimização, li tudo o que encontrei sobre esse tópico no site.
R-quadrado como uma estimativa da qualidade da curva de saldo da estratégia
O artigo descreve detalhadamente o que é regressão linear e como calculá-la usando a biblioteca AlgLib, bem como o coeficiente de determinação R^2 e sua aplicação na avaliação de resultados de testes. Recomendo a sua leitura.
Função para calcular regressão linear, R^2, bem como ProfitStability:
void Coeff(double& Array[], double& LR[], double& coeff[], double& TeSt[], int total, int c) { //-- Fill the matrix: Y - Array value, X - ordinal number of the value CMatrixDouble xy(total, 2); for(int i = 0; i < total; i++) { xy[i].Set(0, i); xy[i].Set(1, Array[i]); } //-- Find coefficients a and b of the linear model y = a*x + b; int retcode = 0; double a, b; CLinReg::LRLine(xy, total, retcode, b, a); //-- Generate the linear regression values for each X; ArrayResize(LR, total); for(int x = 0; x < total; x++) LR[x] = x * a + b; if(m_calc == c) { //-- Find the coefficient of correlation of values with their linear regression corr = CAlglib::PearsonCorr2(Array, LR); //-- Find R^2 and its sign coeff[r2] = MathPow(corr, 2.0); int sign = 1; if(Array[0] > Array[total - 1]) sign = -1; coeff[r2] *= sign; //-- Find LR Standard Error if(total - 2 == 0) stand_err = 0; else { for(int i = 0; i < total; i++) { double delta = MathAbs(Array[i] - LR[i]); stand_err = stand_err + delta * delta; } stand_err = MathSqrt(stand_err / (total - 2)); } } //-- Find ProfitStability = Profit_LR/stand_err if(stand_err == 0) coeff[ProfitStability] = 0; else coeff[ProfitStability] = (LR[total - 1] - LR[0]) / stand_err; }
A partir deste artigo, fiz o cálculo do critério de otimização personalizado ProfitStability. É calculado de forma simples: primeiro, calculamos o LR Standard error, isto é, o desvio médio da linha de regressão em relação ao gráfico de saldo ou patrimônio líquido. Em seguida, subtraímos o valor inicial do valor final da linha de regressão, assim obtemos TrendProfit.
ProfitStability é calculado como a razão entre TrendProfit e LR Standard error.
O artigo descreve detalhadamente todos os prós e contras deste critério de otimização, e também mostra diversos testes para comparar ProfitStability com outros critérios de otimização.
Como a regressão linear pode ser calculada tanto a partir do saldo quanto do capital líquido e ProfitStability está vinculado ao cálculo da regressão linear, o cálculo de ProfitStability é movido para a função de cálculo da regressão linear.
Criando critérios de otimização próprios para parâmetros do EA
O artigo é bem antigo, de 2011, mas interessante e ainda relevante. A partir dele tirei o cálculo do coeficiente de segurança do sistema de negociação (CSSN)
CSSN = Avg.Win / Avg.Loss ((110% - %Win) / (%Win-10%) + 1)
if(TeSt[STAT_PROFIT_TRADES] == 0 || TeSt[STAT_LOSS_TRADES] == 0 || TeSt[STAT_TRADES] == 0) coeff[TSSF] = 0; else { double avg_win = TeSt[STAT_GROSS_PROFIT] / TeSt[STAT_PROFIT_TRADES]; double avg_loss = -TeSt[STAT_GROSS_LOSS] / TeSt[STAT_LOSS_TRADES]; double win_perc = 100.0 * TeSt[STAT_PROFIT_TRADES] / TeSt[STAT_TRADES]; // Calculate safe ratio for this percentage of profitable trades: if((win_perc - 10.0) + 1.0 == 0) coeff[TSSF] = 0; else { double teor = (110.0 - win_perc) / (win_perc - 10.0) + 1.0; // Calculate real ratio: double real = avg_win / avg_loss; if(teor != 0) coeff[TSSF] = real / teor; else coeff[TSSF] = 0; } }
Abordagem ideal para o desenvolvimento e análise de sistemas de negociação
A partir deste artigo, tirei o fator linear (LinearFactor), que é calculado da seguinte forma:
- LinearFactor = MaxDeviation/EndBalance
- MaxDeviaton = Max(MathAbs(Balance[i]-AverageLine))
- AverageLine=StartBalance+K*i
- K=(EndBalance-StartBalance)/n
- n - número de negócios no teste
Mais detalhes podem ser encontrados no artigo do autor. O artigo é muito interessante e, embora tenha sido criticado decentemente nos comentários (na minha humilde opinião, sem razão), você pode aprender muitas coisas úteis com ele.
Olhando um pouco adiante, direi que não encontrei um "critério de otimização personalizado" universal adequado para qualquer Expert Advisor. Para diferentes Expert Advisors, diversos critérios fornecem os melhores resultados.
LinearFactor em alguns Expert Advisors deu resultados maravilhosos.
double MaxDeviaton = 0; double K = (Balance[total - 1] - Balance[0]) / total; for(int i = 0; i < total; i++) { if(i == 0) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); else if(MathAbs(Balance[i] - (Balance[0] + K * i) > MaxDeviaton)) MaxDeviaton = MathAbs(Balance[i] - (Balance[0] + K * i)); } if(MaxDeviaton ==0 || Balance[0] == 0) coeff[LinearFactor] = 0; else coeff[LinearFactor] = 1 / (MaxDeviaton / Balance[0]);
Em correspondência pessoal, o autor disse que esse critério pode ser reforçado e explicou como, mas não consegui transformar essas dicas em código.
Assim, acabei por adicionar quatro critérios de otimização personalizados ao código.
- R^2 - coeficiente de determinação.
- ProfitStability.
- CSSN - coeficiente de segurança do sistema de negociação.
- LinearFactor.
Adicionamos todos esses critérios de otimização ao nosso projeto.
É uma pena que não tenha sido possível adicionar o "Máximo do critério complexo", mas não consegui descobrir como é calculado.
Meu critério de otimização
Agora, com base em todos esses artigos, você pode começar a criar seu próprio critério de otimização.
Qual gráfico de saldo você gostaria de ver? Bem, é claro, na forma de uma linha reta indo para o céu com o máximo ângulo. 😃 Sonhar não custa nada😁.
Vejamos um exemplo de gráfico com lucro.
Considerando que ao otimizar comparamos os resultados de um Expert Advisor, e não de vários Expert Advisors, decidi não levar em consideração o tempo durante o qual o Expert Advisor trabalhou.
Além disso, não levo em consideração os volumes, mas se o lote for calculado dinamicamente, os volumes devem ser incluídos no cálculo do critério personalizado (não feito).
E aí o número de negócios? Não me importa quantos negócios me tragam mil tugriques da Mongólia, por isso não vou contar seu número. Mas é necessário levar em consideração que, se houver poucos negócios, a regressão linear será calculada incorretamente.
O que é importante para mim neste gráfico? Antes de mais nada, o lucro. Resolvi levar em conta o lucro relativo - em relação ao saldo inicial.
Relative_Prof = TeSt[STAT_PROFIT] / TeSt[STAT_INITIAL_DEPOSIT];
Igualmente importante, e talvez até o parâmetro mais importante, é o rebaixamento.
Como o rebaixamento é calculado no testador? A quantidade máxima de fundos à esquerda é obtida e comparada com a quantidade mínima de fundos à direita.
Eu chamo os fundos acima do saldo de "estou bravo". Mas quando os fundos estão abaixo do saldo, já não estou mais bravo, senão que "dói".
Por isso, para mim é mais importante o rebaixamento máximo, aquele que está abaixo do saldo. Dito de outro modo, ignoramos "estou bravo", para que não exista muito "dói".
double equityDD(const double & Balance[], const double & Equity[], const double & TeSt[], const double & coeff[], const int total) { if(TeSt[STAT_INITIAL_DEPOSIT] == 0) return(0); double Balance_max = Balance[0]; double Equity_min = Equity[0]; difference_B_E = 0; double Max_Balance = 0; switch((int)TeSt[41]) { case 0: difference_B_E = TeSt[STAT_EQUITY_DD]; break; default: for(int i = 0; i < total - 1; i++) { if(Balance_max < Balance[i]) Balance_max = Balance[i]; if(Balance[i] == 10963) Sleep(1); if(Balance_max - Equity[i + 1] > difference_B_E) { Equity_min = Equity[i + 1]; difference_B_E = Balance_max - Equity_min; Max_Balance = Balance_max; } } break; } return(1 - difference_B_E / TeSt[STAT_INITIAL_DEPOSIT]); }
Como os valores do critério personalizado devem ser levados em consideração em ordem crescente, subtraí o rebaixamento da unidade. Resultou que quanto maior o valor, menor o rebaixamento.
Chamei o valor resultante de equity_rel, isto é, rebaixamento dos fundos em relação ao saldo inicial
Como se viu, para calcular o equity_rel corretamente, é impossível coletar valores de capital líquido, como fiz no início. Como alguns valores do capital líquido mínimo são perdidos, tivemos que fazer duas variantes para economizá-lo. A primeira variante preserva o capital líquido dos valores máximos ao fechar uma posição com prejuízo e os valores mínimos ao fechar uma posição no positivo, a segunda variante preserva apenas os valores mínimos do capital líquido.
Para que o script saiba como coletamos o capital líquido, tivemos que gravar essas variantes em uma matriz com estatísticas do testador TeSt[41] e calcular equity_rel e Difference_B_E na função EquityDD() com base na variante de coleta de capital líquido.
//---
Decidi apenas misturar tudo isso de diferentes formas e ver o resultado.
//---
Com equity_rel, podemos calcular o fator de recuperação alternativo.
difference_B_E é o rebaixamento máximo de fundos em dinheiro.
coeff[c_recovery_factor] = coeff[Profit_Bak] / difference_B_E;
Mas gostaria que o gráfico ficasse mais próximo de uma linha reta, por isso, adicionei R^2 ao segundo fator de recuperação alternativo
coeff[c_recovery_factor_r2] = coeff[Profit_Bak] / difference_B_E * coeff[r2];
Como nas configurações você pode optar por calcular a correlação do saldo ou do capital líquido, se este último foi registrado apenas pelos valores mínimos, R^2 se correlacionará com o rebaixamento.
A fórmula lucro relativo * R^2 pode dar resultados interessantes a nível de critério personalizado.
coeff[profit_r2] = relative_prof * coeff[r2];
A correlação é boa, mas se considerarmos o tamanho, não é inútil, portanto o próximo critério personalizado é este.
Lucro relativo *R^2 / Standard Error
if(stand_err == 0) coeff[profit_r2_Err] = 0; else coeff[profit_r2_Err] = relative_prof * coeff[r2] / stand_err;
Há um lucro relativo, há um rebaixamento de fundos em relação ao saldo inicial, há R^2, sugere-se uma fórmula que leva em consideração o lucro e o rebaixamento e a aproximação máxima do gráfico a uma linha reta
relative_prof + equity_rel + r2;
Mas se de repente você quiser tornar um desses parâmetros mais significativo? Para fazer isso, eu introduzi uma variável de peso, ratio
Resultaram mais três critérios de otimização personalizados.
coeff[profit_R_equity_r2] = relative_prof * ratio + coeff[equity_rel] + coeff[r2]; coeff[profit_equity_R_r2] = relative_prof + coeff[equity_rel] * ratio + coeff[r2]; coeff[profit_equity_r2_R] = relative_prof + coeff[equity_rel] + coeff[r2] * ratio;
No total, foram obtidos doze critérios de otimização personalizados.
1. R^2 - coeficiente de determinação.
2. ProfitStability.
3. CSSN - coeficiente de segurança do sistema de negociação.
4. LinearFactor
5. equity_rel
6. c_recovery_factor
7. c_recovery_factor_r2
8. profit_r2
9. profit_r2_Err
10. profit_R_equity_r2
11. profit_equity_R_r2
12. profit_equity_r2_R
Verificando o resultado
Para verificar, vamos escrever um Expert Advisor simples...
De acordo com o plano preliminar do artigo, deveria haver um código simples de Expert Advisor aqui, mas, infelizmente, dois Expert Advisors simples não mostraram os resultados que eu gostaria de mostrar.
Por isso, eu tive que pegar um dos EAs que encomendei, e já mostrar os resultados de tudo descrito aqui com ele (desfoquei o nome do EA).
Vamos imaginar que temos o final de abril e planejamos rodar o Expert Advisor em uma conta real, mas como podemos descobrir por qual critério de otimização o Expert Advisor precisa ser otimizado para que negocie com lucro?
Desse modo, começamos a otimização: três meses de otimização, um mês forward.
Após a otimização, reinicie o terminal.
Eu inicio o script, seleciono apenas os melhores resultados nas configurações. Eu recebo essas miniaturas na pasta.
Eu escolho visualmente qual dessas execuções eu gosto mais. Houve vários resultados idênticos, escolhi profit_equity_R_r2, pois ao otimizar de acordo com esse critério, é priorizado um rebaixamento menor.
No testador, a mesma execução fica assim:
Para comparação, o saldo máximo:
O máximo de testes complexos fica assim:
Como se pode ver, com as configurações da melhor passagem profit_equity_R_r2, há muito menos negócios no gráfico do que com o saldo máximo e o complexo máximo, o lucro é aproximadamente o mesmo, mas o gráfico em si é muito mais suave.
Sendo assim, decidimos por o critério personalizado profit_equity_R_r2, agora vamos ver o que aconteceria se tivéssemos realizado a otimização para os últimos três meses e com as melhores configurações de resultados dessa otimização, decidimos negociar em maio.
Começamos a otimização com uma passagem forward.
Configurações de otimização.
Nas configurações do EA, devemos definir o critério personalizado pelo qual otimizaremos.
Como resultado, vemos que, se otimizarmos o EA nos últimos três meses com um critério personalizado profit_equity_R_r2,
e, em seguida, negociarmos de 1 de abril a 1 de maio com essas configurações, o EA ganharia 750 MNT, com um rebaixamento de capital de 300 tugriques da Mongólia.
Bem, um tiro de controle na cabeça do EA — EA Validate feito por fxsaber!!!
Vamos verificar como o EA negociaria por quatro meses. Configurações Validate: três meses de otimização, mês de negociação.
Como se pode ver, o EA sobreviveu mesmo após o controle!!!
Para comparação, temos um gráfico que ficou com as mesmas configurações, mas otimizado de acordo com o "máximo do critério complexo".
Vemos que o EA também sobreviveu, mas...
Conclusão
Vantagens:
- Pode-se visualizar todos os gráficos de resultados de otimização de uma só vez.
- Dá para escolher o critério de otimização personalizado ideal para seu Expert Advisor.
Desvantagens:
Devido à quantidade limitada de dados registrados, os gráficos são menos informativos do que no testador de estratégia.
Com um grande número de negócios, o arquivo com quadros aumenta para um tamanho ilegível.
//---
Como os experimentos mostraram, não existe um critério de otimização super legal, para diferentes EAs, os melhores resultados são dados por diferentes critérios e precisam ser selecionados individualmente.
Mas já temos um conjunto completo de critérios e, se os membros do fórum apoiarem e compartilharem seus critérios de otimização, a escolha será ainda maior.
//---
Um dos testadores sugeriu descrever as configurações do script no artigo, para que as pessoas com preguiça de ler e entender pudessem simplesmente descobrir as configurações e usar esse código sem se aprofundar na essência.
Como usar isso
Para usar este código, é necessário baixar o arquivo anexado na parte inferior do artigo, descompactá-lo, copiá-lo para a pasta MQL5,
em seguida, no terminal, pressionamos -> arquivo -> abrir diretório de dados -> clique com o botão direito do mouse em um local vazio da pasta aberta e clique em "colar", uma janela pode aparecer perguntando "Substituir arquivos na pasta de destino" -> clique em substituir .
A seguir, execute o MetaEditor, abra seu Expert Advisor e adicione as seguintes alterações:
1. Na função OnTick() cole --> IsOnTick();
2. Cole este código na parte inferior do EA:
#include <SkrShotOpt.mqh> double OnTester() {return(IsOnTester());} void OnTradeTransaction(const MqlTradeTransaction & trans, const MqlTradeRequest & request,const MqlTradeResult & result) { IsOnTradeTransaction(trans, request, result); }Se o EA já tiver a função OnTradeTransaction(), basta inserir nela: IsOnTradeTransaction(trans, request, result);
3. Pressione o botão "Compilar".
Se de repente houver erros que correspondam aos nomes das variáveis, você terá que alterar os nomes; se não tiver certeza de suas ações, é aconselhável recorrer a alguém que saiba como fazer isso.
Opções
Depois de inserir o código nas configurações do EA na parte inferior, serão adicionadas mais algumas linhas com configurações.
Não marque as caixas para otimizar essas configurações!!! Essas configurações não afetam os resultados da otimização, por isso, não há necessidade de otimizá-las.
- Se houver mais negócios, registramos a passagem - se o seu Expert Advisor fizer muitas negociações, esse parâmetro pode ser aumentado para reduzir a quantidade de dados gravados no arquivo com quadros.
- Registramos a passagem se o lucro for maior que %, assim, as passagens não lucrativas serão filtradas por padrão. É possível alterar isso se não nos importarmos com o lucro inferior a uma certa porcentagem em relação ao saldo inicial.
- Seleção de valores de capita líquido - selecionamos "armazenar só min equity", se os seguintes critérios de usuário devem ser calculados corretamente: quity_rel, c_recovery_factor, c_recovery_factor_r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R
se você precisar de gráficos mais semelhantes aos gráficos do testador, precisará selecionar "salvar min e max equity"
- Critério personalizado - se definido como "none", os quadros são gravados no arquivo, o critério personalizado não é calculado (para não aumentar o tempo de otimização),
Assim, é impossível definir a otimização de acordo com um critério personalizado. Também os parâmetros abaixo não importam.
Você precisa selecionar algum critério personalizado para este parâmetro se quiser executar a otimização por um critério personalizado.
Vale a pena lembrar que o cálculo de critério personalizado depende da escolha do parâmetro "seleção de valores de capital líquido" e do parâmetro "cálculo de critério por"
- cálculo de critério por - seleção do cálculo de R^2 com base no saldo ou capital líquido, respectivamente todos os valores dos critérios personalizados cujo cálculo envolve R^2 serão alterados
//----
Configurações do script.
- Desenho da linha de regressão - seleção da linha de regressão a ser desenhada, saldo, capital líquido, saldo e capital líquido, não desenhar linha de regressão.
- Porcentagem de lucro maior - como imprimir milhares de capturas de tela demora muito, só podemos imprimir capturas de tela com lucro maior do que o especificado nesta configuração.
- Apenas melhores resultados - se true, salva as telas apenas com o melhor resultado de cada critério personalizado, caso contrário, salva todas as telas.
- Critério personalizado - se estiver selecionado imprimir todas as capturas de tela, com este parâmetro será possível definir um critério personalizado para classificar as capturas de tela na pasta.
- ratio - coeficiente de peso para calcular os critérios personalizados profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
- cálculo de critério por - seleção do cálculo de R^2 com base no saldo ou capital líquido, respectivamente todos os valores dos critérios personalizados cujo cálculo envolve R^2 serão alterados
r2, profit_r2, profit_r2_Err, profit_R_equity_r2, profit_equity_R_r2, profit_equity_r2_R.
- Gráfico - seleção entre, imprimir o gráfico como no testador "Back separadamente Forward", ou seja, forward começa a partir do saldo inicial,
ou "Back continuação Forward" neste caso, forward continuará a partir do último valor de saldo da passagem backward.
//---
Os artigos deste site me ajudaram muito a escrever o programa.
Expresso minha profunda gratidão a todos os autores dos artigos aqui listados!
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/9922
- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso