Анализ торговли по HTML-отчетам

Dmitry Fedoseev | 4 января, 2019

Введение

Если трейдер пытается привлечь инвесторов, то очень вероятно, что они поинтересуются результатами его торговли. Значит, трейдер должен продемонстрировать их, то есть показать историю своей торговли. Терминал MetaTrader 5 позволят сохранять историю торговли в файл (окно "Инструменты" — вкладка "Торговля" — контекстное меню — Отчет). Отчет может сохраняться в файлах XLSX (для программы Microsoft Excel) и в файлах HTML, которые можно просматривать в любых браузерах. Очевидно, что второй вариант более популярен, поскольку программа Excel может быть установлена не на каждом компьютере, а вот браузер найдется у всех. Так что, потенциальному инвестору скорее попадется HTML-отчет о работе трейдера, а не какой-то другой вариант.

Ознакомившись со стандартными показателями приведенными в отчете, кому-то может потребоваться посчитать свои собственные показатели, используя данные о сделках, а для этого потребуется извлечь эти данные из отчета. Решением задачи извлечения данных из HTML-отчетов и займемся в данной статье. Сначала исследуем структуру отчетов, затем напишем функцию для их разбора. В функцию будет передаваться имя файла с отчетом, а возвращаться функцией будет структура со всеми данными. Благодаря этой структуре будет обеспечиваться прямой доступ к любому показателю из отчета. Используя эту структуру можно будет легко и быстро формировать свои собственные отчеты с какими угодно собственными показателями.

Кроме отчетов о торговле, терминал MetaTrader 5 позволяет сохранять отчеты о тестировании и оптимизации экспертов. Отчет о тестировании так же, как и история торговли, может быть сохранен в двух форматах: XLSX и HTML, а отчет об оптимизации сохраняется в формате XML.

Всего в этой статье будет рассмотрен разбор HTML-отчета после тестирования, XML-отчета после оптимизации и HTML-отчет с историей торговли.

HTML- и XML-файлы

HTML-файл, по сути, представляет собой текстовый файл, внутри которого находится текст (отображаемые данные) и теги, указывающие каким образом должны отображаться эти текстовые данные (рис. 1). Любой тег начинается с символа "<", заканчивается символом ">". Например, тег <br> означает, что текст идущий после него должен начинаться с новой строки, а тег <p> — с нового абзаца (не только с новой строки, но и еще нужно сделать пропуск в виде пустой строки). Внутри тегов могут располагаться дополнительные атрибуты, например, <p color="red"> означает, что текст, идущий после него, должен располагаться с нового абзаца и быть красного цвета. 

HTML файл в блокноте
Рис. 1. Фрагмент HTML-файла, открытого в блокноте

Для того чтобы отменить действие тега, используется закрывающий тег, он отличается от открывающего наличием знака "/" в начале, например, закрывающий тег абзаца — </p>. Некоторые теги не подразумевают использования закрывающих тегов, в частности <br>. Для некоторых тегов закрывающий тег может использоваться, но не является обязательным. Можно начать новый абзац не закрывая предыдущего. Если же, как показано выше в примере с тегом <p>, используется атрибут цвета, нужно использовать закрывающий тег, чтобы отметить окрашивание текста красным цветом. Есть и такие теги, которые обязательно должны закрываться, например тег таблицы <table>. Окончание таблицы всегда должно завершаться закрывающим тегом </table>. Внутри таблицы располагаются строки, обозначаемые открывающим тегом <tr> и закрывающим </tr>, а внутри строк располагаются ячейки обозначаемые тегами <td> и </td>. Иногда ячейки обозначаются тегом <th> (ячейка заголовка), в одной таблице могут существовать ячейки обозначенные как тегом <td>, так и тегом <th>. Закрывающие теги строк и ячеек являются обязательными.

В настоящее время большинство html-атрибутов практически не используются, вместо них используется один атрибут "style", а в нем описывается внешний вид элемента, например, <p style="color: red"> — абзац с текстом красного цвета, но и это тоже не очень распространенный способ. Чаще всего используется атрибут "class", в котором указывается только имя класса стиля, например, <p class="small">, а сам класс (описание стиля) располагается в начале html-документа или даже в отдельном CSS-файле  (Cascading Style Sheet — каскадная таблица стилей). 

XML-файлы (рис. 2) имеют много общего с HTML, основное отличие в том, что в HTML существует строго ограниченный набор тегов, в XML же набор тегов может расширяться, можно добавлять свои теги.

XML файл в блокноте
Рис. 2. Фрагмент XML файла открытого в блокноте

Основное назначение HTML — это отображение данных, поэтому набор тегов стандартен и HTML-документы отображаются относительно одинаково в различных браузерах. Основное назначение XML — это сохранение и передача данных, поэтому в нем имеет значение возможность произвольного структурирования данных, а уж тот, кто будет использовать этот файл, должен понимать зачем он это делает и какие данные из него извлекает.

Строковые функции или регулярные выражения

Обычно для извлечения каких либо данных из текста используются регулярные выражения. В CodeBase можно найти библиотеку RegularExpressions для работы с ними, есть статья по ее использованию: "Регулярные выражения для трейдеров". Безусловно, регулярные выражения это очень мощное и удобное средство по разбору текстовых данных. Если задачи по разбору данных приходится решать очень часто и они всегда разные, то сложно представить себе эту деятельность без использования регулярных выражений.

Однако парочка недостатков у регулярных выражений найдется: прежде чем ими пользоваться, их надо основательно изучить. Сама тема регулярных выражений довольно обширна, с ними не получится "заглянуть в справку когда надо", нужно изучить досконально всю теорию и иметь практические навыки. Сам образ мышления, нужный для использования регулярных выражений, значительно отличается от образа мышления необходимого для решения, скажем так, обычных задач программирования. Даже при умении пользоваться регулярными выражениями, может возникать небольшое затруднение при переключении на работу с регулярными выражениями и обратно. 

Если задачи про разбору данных несложные, возникают не часто, то вполне можно пользоваться стандартными строковыми функциями. Кроме того, в руководствах по регулярным выражениям часто встречается мнение, что использование строковых функций работает быстрее. Конечно, сомнительное утверждение, скорость будет зависеть от типа решаемой задачи и условий применения регулярных выражений, тем не менее, строковые функции — определено достойный способ решения задач по разбору данных.

Из всего обилия строковых функция нам пригодятся всего лишь некоторые из них: StringFind(), StringSubstr(), StringReplace(). Еще, может быть, несколько совсем простых: StringLen(), StringTrimLeft(), StringTrimRight().

Отчет тестера

Наибольший объем информации включает в себя отчет из тестера, поэтому начнем с него, что бы ознакомиться с задачей в полной мере и предусмотреть все сложности, которые могут возникнуть при ее решении. При создании этой статьи использовался отчет о тестировании эксперта ExpertMACD с параметрами по умолчанию (отчет прилагается к статье). Открываем отчет в браузере, что бы видеть с чем имеем дело (рис. 3).

Разделы HTML отчета
Рис. 3. HTML отчет тестера в браузере. Красными линиями с синими надписями показаны основные разделы отчета

В отчете можно увидеть, что все данные разделяются на несколько секций: название отчета, параметры, результаты, графики, ордера, сделки, итог.  

В каждом браузере есть команда для просмотра кода страницы, если щелкнуть правой кнопкой мыши на странице, откроется контекстное меню в котором будет команда "Исходный текст страницы", "Исходный код страницы" или что-то подобное. 

В результате визуального исследования исходного кода отчета выясняется, что все данные отчета располагаются в таблицах (тег <table>). Всего в отчете тестера находится две таблицы. В первой таблице находятся общие данные: от названия отчета и до показателя "Среднее время удержания позиции", то есть все, что находится до секции "Ордера". Во второй таблице находятся данные об ордерах, сделках и данные о конечном состоянии депозита. Размещение разнородных данных в одной таблице достигается за счет использования атрибута "colspan", объединяющего несколько ячеек. Названия общих показателей и их значения располагаются в разных ячейках таблицы, иногда эти ячейки находятся  в одной строке иногда в разных.

Существенный момент, на который надо обратить внимание — наличие атрибутов "id" — уникальных идентификаторов тегов. Используя значения идентификаторов, можно было бы однозначно находить ячейки с необходимыми данными. Однако атрибуты "id" не используются, поэтому все показатели будем искать просто отсчитывая строки и ячейки. Например, название отчета "Отчет Тестера стратегий" находится в первой таблице, первой строке, первой ячейке. 

Количество ордеров и сделок, находящихся во второй таблице,  во всех отчетах будет отличаться, поэтому нужно определиться с признаком окончания ордеров и начала сделок. После сделок наблюдается строка только с одной ячейкой, затем строка с заголовками — значит, есть признак. Пока не нужно глубоко вдаваться в отсчет количества строк и ячеек, чуть позже у нас будет более удобный способ сделать это.

Значит, все данные располагаются в ячейках таблицы, поэтому все, что нам нужно на начальном этапе — извлечь таблицы, их строки и ячейки.

Посмотрим отчеты истории торговли и оптимизации. Отчет с историей очень похож на отчет тестера, только раздел с результатами расположен внизу и появился еще один раздел с итоговым состоянием депозита (рис. 4).

HTML отчет с историей в браузере

Рис. 4. HTML отчет с историей в браузере. Красными линиями с синими надписями показаны основные разделы отчета

При изучении HTML-кода истории торговли выясняется, что в нем для  обозначения ячеек используется не только тег <td>, но и <th> — ячейка-заголовок, а все данные располагаются только в одной таблице.

Отчет от оптимизации совсем прост, в нем только один раздел данных с одной таблицей. Изучение его кода показывает, что для создания таблиц в нем используется тег <Table>, строки отделяются тегом <Row>, а ячейки тегом <Cell> (все теги с соответствующими закрывающими тегами).

Загружаем файл

Прежде чем что-либо делать с данными отчета, их надо прочитать из файла. Воспользуемся следующей функций, возвращающей все содержимое файла в одной строковой переменной:

bool FileGetContent(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

Первым параметром в функцию передается имя файла с отчетом, вторым параметром по ссылке передается переменная, в которой будет находиться содержимое файла, а сама функция возвращает true/false в зависимости от успешности ее работы.

Извлекаем таблицы

Для размещения данных таблицы используем две структуры. Структура, содержащая одну строку таблицы:

struct Str{
   string td[];
};

В каждом элемента массива td[] будет располагаться содержимое одной ячейки.

Структура содержащая всю таблицу (все строки):

struct STable{
   Str tr[];
};

Процесс извлечения данных из отчета будет происходить следующим образом: сперва находим начало таблицы по открывающему тегу. Поскольку теги могут иметь атрибуты, будет выполняться поиск только начала тега: "<table". Найдя начало открывающего тега, найдем его конец — скобку ">". После этого ищем окончание таблицы, то есть закрывающий тег: "</table>". Данная задача упрощается тем, что в отчетах нет вложенных таблиц, то есть за каждым открывающим тегом следует только закрывающий тег.

Располагая содержимым таблиц, для каждой таблицы сделаем подобное, но со строками — будем искать начала строк "<tr" и их окончания "</tr", затем для каждой строки будем искать начала ячеек — "<td" и их окончания </td>. При извлечении строк задача немного усложняется, так как ячейка может обозначаться как тегом <td> так и тегом <th>, поэтому, вместо функции StringFind() будет использоваться собственная функцию  TagFind():

int TagFind(string aContent,string & aTags[],int aStart,string & aTag){
   int rp=-1;
   for(int i=0;i<ArraySize(aTags);i++){
      int p=StringFind(aContent,"<"+aTags[i],aStart);
      if(p!=-1){
         if(rp==-1){
            rp=p;
            aTag=aTags[i];
         }
         else{
            if(p<rp){
               rp=p;
               aTag=aTags[i];
            }
         }      
      }
   }
   return(rp);
}

Параметры функции:

  • string aContent — строка, в которой выполняется поиск;
  • string & aTags[] — массив с тегами;
  • int aStart — позиция с которой выполняется поиск;
  • string & aTag — по ссылке возвращается найденный тег.

В отличие от функции StringFind() в функцию TagFind() передается не одна искомая строка, а массив. Кроме того, к искомым строкам в функции выполняется добавление открывающих скобок "<".  Возвращает функция позицию наиболее близко расположенного тега.

Используя функцию TagFind(), будем последовательно искать открывающие и закрывающие теги, а все, что  будет находиться между ними, собирать в массив:

int TagsToArray(string aContent,string & aTags[],string & aArray[]){
   ArrayResize(aArray,0);
   int e,s=0;
   string tag;
   while((s=TagFind(aContent,aTags,s,tag))!=-1 && !IsStopped()){  
      s=StringFind(aContent,">",s);
      if(s==-1)break;
      s++; 
      e=StringFind(aContent,"</"+tag,s);   
      if(e==-1)break;  
      ArrayResize(aArray,ArraySize(aArray)+1);
      aArray[ArraySize(aArray)-1]=StringSubstr(aContent,s,e-s);  
   }
   return(ArraySize(aArray));
}

Параметры функции:

  • string aContent — строка, в которой выполняется поиск;
  • string & aTags[] — массив с тегами;
  • string aArray[] — по ссылке возвращается массив с содержимом всех найденных тегов.

В функцию TagsToArray() передается массив искомых тегов только при извлечении ячеек, поэтому, для удобства, при использовании ее в остальных случаях, используя возможности перегрузки, напишем ее аналог, но с обычным строковым параметром:

int TagsToArray(string aContent,string aTag,string & aArray[]){
   string Tags[1];
   Tags[0]=aTag;
   return(TagsToArray(aContent,Tags,aArray));
}

Параметры функции:

  • string aContent — строка, в которой выполняется поиск;
  • string & aTag — искомый тег;
  • string aArray[] — по ссылке возвращается массив с содержимом всех найденных тегов.

Теперь займемся непосредственно функцией для извлечения содержимого таблиц. В функцию передается строковый параметр aFileName с именем разбираемого файла. В функции будет использоваться локальная строковая переменная для содержимого файла и локальный массив структур STable:

STable tables[];
string FileContent;

Используя функцию FileGetContent, получаем все содержимое отчета:

if(!FileGetContent(aFileName,FileContent)){
   return(true);
}

Теперь вспомогательные переменные:

string tags[]={"td","th"};
string ttmp[],trtmp[],tdtmp[];
int tcnt,trcnt,tdcnt;

В массиве tags подготовлены возможные варианты тега ячеек. В строковых массивы ttmp[], trtmp[], tdtmp[] будет временно размещаться содержимое таблиц, строк и ячеек соответственно. 

Извлекаем таблицы:

tcnt=TagsToArray(FileContent,"table",ttmp);
ArrayResize(tables,tcnt);

Проходя в цикле по всем таблицам извлекаем строки, проходя по всем строкам извлекаем ячейки:

for(int i=0;i<tcnt;i++){
   trcnt=TagsToArray(ttmp[i],"tr",trtmp);
   ArrayResize(tables[i].tr,trcnt);      
   for(int j=0;j<trcnt;j++){         
      tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
      ArrayResize(tables[i].tr[j].td,tdcnt);
      for(int k=0;k<tdcnt;k++){  
         tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
      }
   }
} 

Таблицы, строки и ячейки сначала извлекаются во временные массивы, затем массив ячеек копируется в структуру. При копировании ячейки выполняется ее фильтрация функцией RemoveTags(). В ячейках таблиц могут находиться вложенные теги, функция RemoveTags() удаляет их и оставляет только полезные данные.

Функция RemoveTags():

string RemoveTags(string aStr){
   string rstr="";
   int e,s=0;
   while((e=StringFind(aStr,"<",s))!=-1){
      if(e>s){

         rstr=rstr+StringSubstr(aStr,s,e-s);
      }
      s=StringFind(aStr,">",e);
      if(s==-1)break;
      s++;
   }
   if(s!=-1){
      rstr=rstr+StringSubstr(aStr,s,StringLen(aStr)-s);
   }
   StringReplace(rstr,"&nbsp;"," ");
   while(StringReplace(rstr,"  "," ")>0);
   StringTrimLeft(rstr);
   StringTrimRight(rstr);
   return(rstr);
}

Рассмотрим функцию RemoveTags(). Переменная s используется для позиции начала полезных данных, сначала ее значение равно 0, потому, что данные могут начинать с самого начала строки. В цикле while выполняется поиск открывающей скобки "<", означающей начало тега. Когда начало тега найдено, все данные от позиции указанной в переменой s и до найденной позиции копируются в переменную rstr и выполняется поиск окончания тега, то есть нового начала полезных данных. После цикла, если значение переменной s не равно -1 (это означает, что строка заканчивается полезными данными, но они не были скопированы) выполняется их копирование в переменную rstr. В самом конце функции выполняется замена пробельного спецсимвола &nbsp; простым пробелом, удаление повторяющихся пробелов и удаление пробелов в начале и в конце строки.

На данном этапе имеем массив структур tables, заполненный чистыми данными таблицы. Сохраним этот массив в текстовый файл, при сохранении пронумеруем таблицы, строки и ячейки (сохранение выполняется в файл с именем 1.txt):

int h=FileOpen("1.txt",FILE_TXT|FILE_WRITE);

for(int i=0;i<ArraySize(tables);i++){
   FileWriteString(h,"table "+(string)i+"\n");
   for(int j=0;j<ArraySize(tables[i].tr);j++){      
      FileWriteString(h,"   tr "+(string)j+"\n");         
      for(int k=0;k<ArraySize(tables[i].tr[j].td);k++){
         FileWriteString(h,"      td "+(string)k+": "+tables[i].tr[j].td[k]+"\n");
      }
   }
}

FileClose(h);

Просматривая этот файл, будет значительно легче разобраться какие данные в какой ячейке находятся. Ниже приведен фрагмент файла:

table 0
   tr 0
      td 0: Отчет Тестера стратегий
   tr 1
      td 0: IMPACT-Demo (Build 1940)
   tr 2
      td 0: 
   tr 3
      td 0: Настройки
   tr 4
      td 0: Советник:
      td 1: ExpertMACD
   tr 5
      td 0: Символ:
      td 1: USDCHF
   tr 6
      td 0: Период:
      td 1: H1 (2018.11.01 - 2018.12.01)
   tr 7
      td 0: Параметры:
      td 1: Inp_Expert_Title=ExpertMACD
   tr 8
      td 0: 
      td 1: Inp_Signal_MACD_PeriodFast=12
   tr 9
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSlow=24
   tr 10
      td 0: 
      td 1: Inp_Signal_MACD_PeriodSignal=9
   tr 11

Структура для данных отчета

Осталось немного скучной рутиной работы: написать структуру, соответствующую данным отчета, и заполнить ее данными из массива tables. Отчет разделен на несколько секций, соответственно будет использоваться несколько структур, собранных в одну общую.

Структура для секции "Настройки":

struct SSettings{
   string Expert;
   string Symbol;
   string Period;
   string Inputs;
   string Broker;
   string Currency;
   string InitialDeposit;
   string Leverage;
};

Назначение полей структуры очевидно из их названия. Все поля структуры будут содержать данные точно в таком виде, как в отчете, а список параметров эксперта будет находиться в оной строковой переменной Inputs.

Структура для данных секции "Результаты":

struct SResults{
   string HistoryQuality;
   string Bars;
   string Ticks;
   string Symbols;
   string TotalNetProfit;
   string BalanceDrawdownAbsolute;
   string EquityDrawdownAbsolute;
   string GrossProfit;
   string BalanceDrawdownMaximal;
   string EquityDrawdownMaximal;
   string GrossLoss;
   string BalanceDrawdownRelative;
   string EquityDrawdownRelative;
   string ProfitFactor;
   string ExpectedPayoff;
   string MarginLevel;
   string RecoveryFactor;
   string SharpeRatio;
   string ZScore;
   string AHPR;
   string LRCorrelation;
   string OnTesterResult;
   string GHPR;
   string LRStandardError;
   string TotalTrades;
   string ShortTrades_won_pers;
   string LongTrades_won_perc;
   string TotalDeals;
   string ProfitTrades_perc_of_total;
   string LossTrades_perc_of_total;
   string LargestProfitTrade;
   string LargestLossTrade;
   string AverageProfitTrade;
   string AverageLossTrade;
   string MaximumConsecutiveWins_cur;
   string MaximumConsecutiveLosses_cur;
   string MaximalConsecutiveProfit_count;
   string MaximalConsecutiveLoss_count;
   string AverageConsecutiveWins;
   string AverageConsecutiveLosses;
   string Correlation_Profits_MFE;
   string Correlation_Profits_MAE;
   string Correlation_MFE_MAE;      
   string MinimalPositionHoldingTime;
   string MaximalPositionHoldingTime;
   string AveragePositionHoldingTime;
};

Структура для данных одного ордера:

struct SOrder{
   string OpenTime;
   string Order;
   string Symbol;
   string Type;
   string Volume;
   string Price;
   string SL;
   string TP;
   string Time;
   string State;
   string Comment;
};

Структура для данных одной сделки:

struct SDeal{
   string Time;
   string Deal;
   string Symbol;
   string Type;
   string Direction;
   string Volume;
   string Price;
   string Order;
   string Commission;
   string Swap;
   string Profit;
   string Balance;
   string Comment;
};

Структура для итогового состояния депозита:

struct SSummary{
   string Commission;
   string Swap;
   string Profit;
   string Balance;
};

Общая структура:

struct STestingReport{
   SSettings Settings;
   SResults Results;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;
};

Для ордеров и сделок используются массивы структур SOrder и SDeals.

Заполнение структуры данными

Осталось уже немного рутиной работы, но и всё ещё требующей внимания. Просматриваем полученный ранее текстовый файл с данными таблицы и заполняем структуру:

aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
aTestingReport.Settings.Period=tables[0].tr[6].td[1];
aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];

Обратите внимание, в последней строке вышеприведенного примера кода выполняется присвоение значения полю Inputs, но после этого в поле будут находятся данные только об одном параметре. Далее собираются остальные параметры, они расположены со строки 8, первая ячейка каждой строки с параметром пустая, поэтому цикл выполняется до тех пор, пока первая ячейка в строке пуста:

int i=8;
while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
   aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
   i++;
}

Ниже приведена полная функция разбора отчета тестирования:

bool TestingHTMLReportToStruct(string aFileName,STestingReport & aTestingReport){

   STable tables[];

   string FileContent;
   
   if(!FileGetContent(aFileName,FileContent)){
      return(true);
   }

   string tags[]={"td","th"};
   string ttmp[],trtmp[],tdtmp[];
   int tcnt,trcnt,tdcnt;
   
   tcnt=TagsToArray(FileContent,"table",ttmp);

   ArrayResize(tables,tcnt);
   
   for(int i=0;i<tcnt;i++){
      trcnt=TagsToArray(ttmp[i],"tr",trtmp);
      ArrayResize(tables[i].tr,trcnt);      
      for(int j=0;j<trcnt;j++){         
         tdcnt=TagsToArray(trtmp[j],tags,tdtmp);
         ArrayResize(tables[i].tr[j].td,tdcnt);
         for(int k=0;k<tdcnt;k++){  
            tables[i].tr[j].td[k]=RemoveTags(tdtmp[k]);
         }
      }
   }   
   
   // settings section
   
   aTestingReport.Settings.Expert=tables[0].tr[4].td[1];
   aTestingReport.Settings.Symbol=tables[0].tr[5].td[1];
   aTestingReport.Settings.Period=tables[0].tr[6].td[1];
   aTestingReport.Settings.Inputs=tables[0].tr[7].td[1];
   int i=8;
   while(i<ArraySize(tables[0].tr) && tables[0].tr[i].td[0]==""){
      aTestingReport.Settings.Inputs=aTestingReport.Settings.Inputs+", "+tables[0].tr[i].td[1];
      i++;
   }
   aTestingReport.Settings.Broker=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Currency=tables[0].tr[i++].td[1];  
   aTestingReport.Settings.InitialDeposit=tables[0].tr[i++].td[1];
   aTestingReport.Settings.Leverage=tables[0].tr[i++].td[1];   
   
   // results section
   
   i+=2;
   aTestingReport.Results.HistoryQuality=tables[0].tr[i++].td[1];
   aTestingReport.Results.Bars=tables[0].tr[i].td[1];
   aTestingReport.Results.Ticks=tables[0].tr[i].td[3];
   aTestingReport.Results.Symbols=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalNetProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownAbsolute=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownAbsolute=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossProfit=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownMaximal=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownMaximal=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GrossLoss=tables[0].tr[i].td[1];
   aTestingReport.Results.BalanceDrawdownRelative=tables[0].tr[i].td[3];
   aTestingReport.Results.EquityDrawdownRelative=tables[0].tr[i].td[5];
   i+=2;
   aTestingReport.Results.ProfitFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.ExpectedPayoff=tables[0].tr[i].td[3];
   aTestingReport.Results.MarginLevel=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.RecoveryFactor=tables[0].tr[i].td[1];
   aTestingReport.Results.SharpeRatio=tables[0].tr[i].td[3];
   aTestingReport.Results.ZScore=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.AHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRCorrelation=tables[0].tr[i].td[3];
   aTestingReport.Results.tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.GHPR=tables[0].tr[i].td[1];
   aTestingReport.Results.LRStandardError=tables[0].tr[i].td[3];
   i+=2;
   aTestingReport.Results.TotalTrades=tables[0].tr[i].td[1];
   aTestingReport.Results.ShortTrades_won_pers=tables[0].tr[i].td[3];
   aTestingReport.Results.LongTrades_won_perc=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.TotalDeals=tables[0].tr[i].td[1];
   aTestingReport.Results.ProfitTrades_perc_of_total=tables[0].tr[i].td[3];
   aTestingReport.Results.LossTrades_perc_of_total=tables[0].tr[i].td[5];
   i++;
   aTestingReport.Results.LargestProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.LargestLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageProfitTrade=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageLossTrade=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximumConsecutiveWins_cur=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximumConsecutiveLosses_cur=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.MaximalConsecutiveProfit_count=tables[0].tr[i].td[2];
   aTestingReport.Results.MaximalConsecutiveLoss_count=tables[0].tr[i].td[4];
   i++;
   aTestingReport.Results.AverageConsecutiveWins=tables[0].tr[i].td[2];
   aTestingReport.Results.AverageConsecutiveLosses=tables[0].tr[i].td[4];    
   i+=6;
   aTestingReport.Results.Correlation_Profits_MFE=tables[0].tr[i].td[1];
   aTestingReport.Results.Correlation_Profits_MAE=tables[0].tr[i].td[3];
   aTestingReport.Results.Correlation_MFE_MAE=tables[0].tr[i].td[5];    
   i+=3;
   aTestingReport.Results.MinimalPositionHoldingTime=tables[0].tr[i].td[1];
   aTestingReport.Results.MaximalPositionHoldingTime=tables[0].tr[i].td[3];
   aTestingReport.Results.AveragePositionHoldingTime=tables[0].tr[i].td[5];   
   
   // orders

   ArrayFree(aTestingReport.Orders);
   int ocnt=0;
   for(i=3;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)==1){
         break;
      }   
      ArrayResize(aTestingReport.Orders,ocnt+1);
      aTestingReport.Orders[ocnt].OpenTime=tables[1].tr[i].td[0];
      aTestingReport.Orders[ocnt].Order=tables[1].tr[i].td[1];
      aTestingReport.Orders[ocnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Orders[ocnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Orders[ocnt].Volume=tables[1].tr[i].td[4];
      aTestingReport.Orders[ocnt].Price=tables[1].tr[i].td[5];
      aTestingReport.Orders[ocnt].SL=tables[1].tr[i].td[6];
      aTestingReport.Orders[ocnt].TP=tables[1].tr[i].td[7];
      aTestingReport.Orders[ocnt].Time=tables[1].tr[i].td[8];
      aTestingReport.Orders[ocnt].State=tables[1].tr[i].td[9];
      aTestingReport.Orders[ocnt].Comment=tables[1].tr[i].td[10];      
      ocnt++;
   }
   
   // deals
   
   i+=3;
   ArrayFree(aTestingReport.Deals);
   int dcnt=0;
   for(;i<ArraySize(tables[1].tr);i++){
      if(ArraySize(tables[1].tr[i].td)!=13){
         if(ArraySize(tables[1].tr[i].td)==6){
            aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
            aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
            aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
            aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
         }
         break;
      }   
      ArrayResize(aTestingReport.Deals,dcnt+1);   
      aTestingReport.Deals[dcnt].Time=tables[1].tr[i].td[0];
      aTestingReport.Deals[dcnt].Deal=tables[1].tr[i].td[1];
      aTestingReport.Deals[dcnt].Symbol=tables[1].tr[i].td[2];
      aTestingReport.Deals[dcnt].Type=tables[1].tr[i].td[3];
      aTestingReport.Deals[dcnt].Direction=tables[1].tr[i].td[4];
      aTestingReport.Deals[dcnt].Volume=tables[1].tr[i].td[5];
      aTestingReport.Deals[dcnt].Price=tables[1].tr[i].td[6];
      aTestingReport.Deals[dcnt].Order=tables[1].tr[i].td[7];
      aTestingReport.Deals[dcnt].Commission=tables[1].tr[i].td[8];
      aTestingReport.Deals[dcnt].Swap=tables[1].tr[i].td[9];
      aTestingReport.Deals[dcnt].Profit=tables[1].tr[i].td[10];
      aTestingReport.Deals[dcnt].Balance=tables[1].tr[i].td[11];
      aTestingReport.Deals[dcnt].Comment=tables[1].tr[i].td[12];
      dcnt++;
   }
   return(true);
}

В функцию передается имя файла с отчетом и по ссылке структура STestingReport, которая будет заполнена в функции.

Обратите внимания на участки кода начинающееся с комментариев Orders и Deals. Номер строки с началом списка ордеров определен однозначно, а вот окончание списка ордеров определяется по строке с одной ячейкой:

if(ArraySize(tables[1].tr[i].td)==1){
   break;
}  

После ордеров пропускается три строки:

// deals
   
i+=3;

Затем выполняется сбор данных о сделках. Окончание списка сделок определяется по строке с количеством ячеек 6, эта же строка содержит данные об итоговом состоянии депозита. Перед выходом из цикла выполняется заполнение структуры итогового состояния депозита:

if(ArraySize(tables[1].tr[i].td)!=13){
   if(ArraySize(tables[1].tr[i].td)==6){
      aTestingReport.Summary.Commission=tables[1].tr[i].td[1];
      aTestingReport.Summary.Swap=tables[1].tr[i].td[2];
      aTestingReport.Summary.Profit=tables[1].tr[i].td[3];
      aTestingReport.Summary.Balance=tables[1].tr[i].td[4];            
   }
   break;
} 

Структура с данными отчета полностью готова.

Отчет истории торговли

Разбор истории торговли почти ничем не отличается от разбора отчета тестера кроме отличий в итоговой структуре данных и структур входящих в ее состав:

struct SHistory{
   SHistoryInfo Info;
   SOrder Orders[];
   SDeal Deals[];
   SSummary Summary;  
   SDeposit Deposit;
   SHistoryResults Results;
};

В структуру SHistory входят следующие структуры:  SHistoryInfo — общая информация от счете, массивы структур с данными об ордерах и сделках, SSummary — результаты торговли, SDeposit — итоговое состояние депозита, SHistoryResults — структура с общими показателями (прибыль, количество сделок, просадка и т.п.).

Полный код функции для разбора отчетов торговли можно найти в приложении, имя функции HistoryHTMLReportToStruct(), в функцию передаются два параметра:

  • string FileName — имя файла с отчетом;
  • SHistory History — заполняемая структура данных отчета торговли.

Отчет оптимизации

Первое отличие в работе с отчетом оптимизации в типе файла, файл сохранен в файле ANSI, поэтому для чтения его содержимого используется другая функция:

bool FileGetContentAnsi(string aFileName,string & aContent){
   int h=FileOpen(aFileName,FILE_READ|FILE_TXT|FILE_ANSI);
   if(h==-1)return(false);
   aContent="";
   while(!FileIsEnding(h)){
      aContent=aContent+FileReadString(h);
   }
   FileClose(h);
   return(true);
}

Второе отличие в тегах. Вместо тегов <table>, <tr> и <td> используются теги <Table>, <Row> и <Cell>. Последнее, основное отличие — в структуре данных:

struct SOptimization{
   string ParameterName[];
   SPass Pass[];
};

В структуру входит строковый массив с именами оптимизируемых параметров (крайние справа колонки отчета) и массив структур SPass:

struct SPass{
   string Pass;
   string Result;
   string Profit;
   string ExpectedPayoff;
   string ProfitFactor;
   string RecoveryFactor;
   string SharpeRatio;
   string Custom;
   string EquityDD_perc;
   string Trades;
   string Parameters[];
};

В структуру входят те же поля, что и располагаются в колонках отчета, а последнее поле — строковый массив, в нем находятся значения оптимизируемых параметров в соответствии с их наименованиями в массиве ParameterName().

Полный код функции для разбора отчетов оптимизации можно найти в приложении, имя функции OptimizerXMLReportToStruct(), в функцию передаются два параметра:

  • string FileName — имя файла с отчетом;
  • SOptimization Optimization — заполняемая структура данных оптимизации.

Вспомогательные функции

Если все структуры и функции, созданные в этой статье, поместить в один включаемый файл, то вся работа по разбору отчетов будут состоять из трех строк кода. 

1. Подключить файл:

#include <HTMLReport.mqh>

2. Объявить структуру:

SHistory History;

3. Вызвать функцию:

HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

После этого все данные отчета будут располагаться в соответствующих полях структуры точно так, как в отчете. Хотя все поля имеют строковый тип, использование их для вычислений не составит труда, просто надо будет преобразовать строку в число. Однако в некоторых полях отчета, и соответственно структуры, находятся двойные значения, например данные об объеме ордера представлены двумя значениями типа "0.1 / 0.1", где первое значение - это объем в заявке, а второе - исполненный объем. Так же некоторые итоговые показатели имеют двойные значение, основное значение и дополнительное в скобках, например, данные о количестве трейдов: "11 (54.55%)", указано действительное количество и в скобках относительное в процентах. Поэтому напишем несколько вспомогательных функций для упрощения использования таких показателей.

Функции для извлечения отдельных значений объема:

string Volume1(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Volume2(string aStr){
   int p=StringFind(aStr,"/",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Функция Volume1() извлекает первое значение объема из строки типа "0.1 / 0.1", функция Volume2() - второе.

Функции для извлечения значений двойных показателей:

string Value1(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,0,p);
   }
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

string Value2(string aStr){
   int p=StringFind(aStr,"(",0);
   if(p!=-1){
      aStr=StringSubstr(aStr,p+1);
   }
   StringReplace(aStr,")","");
   StringReplace(aStr,"%","");
   StringTrimLeft(aStr);
   StringTrimRight(aStr);
   return(aStr);
}

Функция Value1() извлекает первое значение из строки типа "8.02 (0.08%)", функция Value2() - второе, удаляет закрывающую скобку и знак процента, если он есть.

Еще может пригодиться функция для преобразования данных о параметрах из структуры отчета тестирования. Для ее преобразования в более удобную форму потребуется структура:

struct SInput{
   string Name,
   string Value;
}

Поле Name используется для имени параметра, Value — для значения. Тип значения заранее неизвестен, поэтому поле Value имеет тип string.

Функция для преобразования строки с параметрами в массив структур SInputs:

void InputsToStruct(string aStr,SInput & aInputs[]){
   string tmp[];
   string tmp2[];
   StringSplit(aStr,',',tmp);
   int sz=ArraySize(tmp);
   ArrayResize(aInputs,sz);
   for(int i=0;i<sz;i++){
      StringSplit(tmp[i],'=',tmp2);
      StringTrimLeft(tmp2[0]);
      StringTrimRight(tmp2[0]);      
      aInputs[i].Name=tmp2[0];
      if(ArraySize(tmp2)>1){
         StringTrimLeft(tmp2[1]);
         StringTrimRight(tmp2[1]);       
         aInputs[i].Value=tmp2[1];
      }
      else{
         aInputs[i].Value="";
      }
   }
}

Строка с параметрами разбивается в массив по знаку ",", затем, каждый элемент полученного массива разбивается по знаку "=" на имя и значение.

На этом все готово для выполнения какого угодно анализа извлеченных данных и формирования собственных отчетов.

Формируем свой отчет

Теперь, имея легкий доступ к данным отчета, все зависит от того, что необходимо увидеть в этих данных. Вариантов множество. По результатам сделок можно рассчитывать какие-нибудь общие показатели типа коэффициента Шарпа, фактора восстановления и т.п. Формируя свои HTML-отчеты, открывается доступ ко всем возможностям HTML, CSS и JavaScript. Используя возможности HTML, отчеты можно каким угодно образом переформировать, например, создать общую таблицу ордеров и сделок или отдельные таблицы сделок на покупку и на продажу и т.п.

При помощи CSS, раскрашивая данные разными цветами, можно сделать отчет более наглядным и выразительным. Используя JavaScript, можно сделать отчет интерактивным, например, чтобы при наведении мыши на строку со сделкой, выделялась строка соответствующего ордера в таблице ордеров и многое другое. Можно формировать изображения и добавлять их в свой HTML-отчет или же, используя возможности JavaScript, рисовать прямо на элементe canvas, входящем в состав html5. В общем, все зависит от ваших потребностей, фантазии и степени владения данными инструментами.

Попробуем создать свой html-отчет о торговле. Соединим в одну таблицу ордера и сделки. Отмененные отложенные ордера будут раскрашиваться серым цветом, поскольку они не представляют интереса. Так же серым цветом будут раскрашиваться рыночные ордера, поскольку они в значительной степени дублируют информацию о расположенных после них сделках, а вот сделки будут раскрашены яркими цветами: синим и красным. Исполненные отложенные ордера тоже стоит раскрашивать заметными цветами, например розовым и голубым. 

Таблица формируемого отчета будет содержать заголовки из таблицы ордеров и из таблицы сделок, подготовим массив с ними:

   string TableHeader[]={  "Time",
                           "Order",
                           "Deal",
                           "Symbol",
                           "Type",
                           "Direction",
                           "Volume",
                           "Price",
                           "Order",
                           "S/L",
                           "T/P",
                           "Time",
                           "State",
                           "Commission",
                           "Swap",
                           "Profit",
                           "Balance",
                           "Comment"};

Получим структуру с данными отчета:

SHistory History;
HistoryHTMLReportToStruct("ReportHistory-555849.html",History);

Откроем файл для создаваемого отчета, запишем в него стандартное начало HTML страницы, формируемое функцией HTMLStart():

   int h=FileOpen("Report.htm",FILE_WRITE);
   if(h==-1)return;

   FileWriteString(h,HTMLStart("Report"));
   FileWriteString(h,"<table>\n");
   
   FileWriteString(h,"\t<tr>\n");   
   for(int i=0;i<ArraySize(TableHeader);i++){
      FileWriteString(h,"\t\t<th>"+TableHeader[i]+"</th>\n"); 
   }
   FileWriteString(h,"\t</tr>\n");     

Ниже приведен код функции HTMLStart():

string HTMLStart(string aTitle,string aCSSFile="style.css"){
   string str="<!DOCTYPE html>\n";
   str=str+"<html>\n";
   str=str+"<head>\n";
   str=str+"<link href=\""+aCSSFile+"\" rel=\"stylesheet\" type=\"text/css\">\n";
   str=str+"<title>"+aTitle+"</title>\n";
   str=str+"</head>\n";  
   str=str+"<body>\n";     
   return str;
}

В функцию передается строка с заголовком страницы для тега title и имя файла со стилями.

Проходя в цикле по все ордерам, определяем стиль отображения строки и формируем ее:

int j=0;
for(int i=0;i<ArraySize(History.Orders);i++){
   
   string sc="";
   
   if(History.Orders[i].State=="canceled"){
      sc="PendingCancelled";
   }
   else if(History.Orders[i].State=="filled"){
      if(History.Orders[i].Type=="buy"){
         sc="OrderMarketBuy";
      }
      else if(History.Orders[i].Type=="sell"){
         sc="OrderMarketSell";
      }
      if(History.Orders[i].Type=="buy limit" || History.Orders[i].Type=="buy stop"){
         sc="OrderPendingBuy";
      }
      else if(History.Orders[i].Type=="sell limit" || History.Orders[i].Type=="sell stop"){
         sc="OrderMarketSell";
      }         
   }

   FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
   FileWriteString(h,"\t\t<td>"+History.Orders[i].OpenTime+"</td>\n");  // Time
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Order+"</td>\n");     // Order 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Deal 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Symbol+"</td>\n");    // Symbol 
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Type+"</td>\n");      // Type 
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Direction       
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Volume+"</td>\n");    // Volume    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Price+"</td>\n");     // Price  
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Order        
   FileWriteString(h,"\t\t<td>"+History.Orders[i].SL+"</td>\n");        // SL
   FileWriteString(h,"\t\t<td>"+History.Orders[i].TP+"</td>\n");        // TP
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Time+"</td>\n");      // Time    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].State+"</td>\n");     // State
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Comission
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Swap
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Profit     
   FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");                    // Ballance    
   FileWriteString(h,"\t\t<td>"+History.Orders[i].Comment+"</td>\n");   // Comment     
   FileWriteString(h,"\t</tr>\n");   
   

Для каждого ордера, если он исполнен, ищем сделку, так же определяем стиль ее отображения и формируем ее HTML-код:  

// найти сделку

if(History.Orders[i].State=="filled"){
   for(;j<ArraySize(History.Deals);j++){
      if(History.Deals[j].Order==History.Orders[i].Order){
         
         sc="";
         
         if(History.Deals[j].Type=="buy"){
            sc="DealBuy";
         }
         else if(History.Deals[j].Type=="sell"){
            sc="DealSell";
         }
      
         FileWriteString(h,"\t<tr class=\""+sc+"\">\n");   
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Time+"</td>\n");     // Time
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Order 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Deal+"</td>\n");     // Deal 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Symbol+"</td>\n");     // Symbol 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Type+"</td>\n");     // Type 
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Direction+"</td>\n");     // Direction       
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Volume+"</td>\n");     // Volume    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Price+"</td>\n");     // Price  
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Order+"</td>\n");     // Order        
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // SL
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // TP
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // Time    
         FileWriteString(h,"\t\t<td>"+"&nbsp;"+"</td>\n");     // State
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Commission+"</td>\n");                    // Comission
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Swap+"</td>\n");     // Swap
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Profit+"</td>\n");     // Profit     
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Balance+"</td>\n");     // Ballance    
         FileWriteString(h,"\t\t<td>"+History.Deals[j].Comment+"</td>\n");     // Comment     
         FileWriteString(h,"\t</tr>\n"); 
         break;
      }
   }
}

В конце закрываем таблицу и добавляем стандартное окончание HTML-страницы, формируемое функцией HTMLEnd():

FileWriteString(h,"</table>\n");   
FileWriteString(h,HTMLEnd("Report"));  

Код функции HTMLEnd():

string HTMLEnd(){
   string str="</body>\n";
   str=str+"</html>\n";
   return str;
}

Осталось написать файл стилей — style.css. Изучение css не является целью данной статьи и не будет подробно рассматриваться, все желающие могут посмотреть файл, он находится в приложении к статье. Так же и скрипт, создающий отчет, находится в приложении к статье, имя файла HTMLReportCreate.mq5.

Вот так выглядит готовый отчет:

Собственный HTML отчет
Рис. 5. Фрагмент собственного HTML-отчета

Заключение

Возможно, у кого-то возникнет вопрос — на сколько проще все это было бы сделано с использованием регулярных выражений? Общая структура и принцип остались бы прежними. Так же выполнялось бы отдельное получение массива с содержимым таблиц, потом строк,  ячеек, только вместо функции  TagsToArray() использовался бы вызов функции с регулярным выражением, все остальное, очень вероятно, было бы точно таким же как в этой статье.

Пример формирования собственного отчета, приведенный в этой статье, это всего лишь один из множества вариантов оформления отчетов и приводится только в качестве примера. Каждый сам определиться с тем, что и как для него удобней и понятней. Главное, что теперь обеспечивается очень легкий доступ ко всем данным отчетов.

Файлы приложения

  • Include/HTMLReport.mqh — включаемый файл с функциями для разбора отчетов.
  • Scripts/HTMLReportTest.mq5 — пример использования HTMLReport.mqh для разбора отчетов тестирования, оптимизации, истории.
  • Scripts/HTMLReportCreate.mq5 — пример с формированием собственного HTML отчета.
  • Files/ReportTester-555849.html — отчет тестера.
  • Files/ReportOptimizer-555849.xml — отчет оптимизатора.
  • Files/ReportHistory-555849.html — отчет с историей.
  • Files/Report.htm — файл отчета созданного скриптом HTMLReportCreate.
  • Files/style.css — файл стилей для файла Report.htm.