
Возможности SQLite в MQL5: Пример интерактивной панели с торговой статистикой в разрезе символов и магиков
Содержание
- Введение
- Ставим задачу
- Информационная панель
- Функции для работы с базой данных
- Собираем проект - информационную панель
- Заключение
Введение
MQL5.com — это ресурс, предлагающий пользователям широкий доступ к разнообразной справочной и учебной информации в сфере алготрейдинга. Несмотря на обширную базу данных и возможности, некоторые пользователи всё же испытывают затруднения с нахождением конкретных решений или адаптацией примеров под свои нужды. На такие случаи ресурс предоставляет возможность взаимодействия с сообществом через форум, где можно получить полезные советы или даже готовые решения.
Настоящая статья направлена на демонстрацию и решение одной из типичных задач — создания информационной панели для отображения торговой истории и статистики на аккаунте. Мы рассмотрим процесс разработки данной панели, опираясь исключительно на существующие материалы с mql5.com, что интересно не только практическим решением, но и как пример применения обучающих и справочных материалов в реальных условиях.
Ставим задачу
Требуется создать информационную панель, где по запросу можно будет отображать информацию о торговой истории на счёте и торговую статистику в разрезе символов и магиков (экспертов, торгующих на счёте). Также необходимо отображать и полную торговую статистику по счёту. Торговая история должна быть отсортирована по времени проведённых трейдов. Статистика торговли по символам и магикам должна быть отсортирована по чистой прибыли (Net Profit).
Тип программы сделаем — Индикатор. Никаких данных в буферы выводить не будем. То есть это будет безбуферный индикатор, основным рабочим пространством которого будет графическая панель. Панель будет визуально разделена на две области — слева будем выводить списки всех символов и магиков, на которых велась торговля на счёте, а справа будем выводить либо таблицы статистики по символам и магикам в виде списка, либо итоговую статистику по какому-либо одному выбранному символу или магику, либо полностью по торговле на счёте. Управлять выводом тех или иных списков будем при помощи кнопок на панели, либо по щелчку на строке со статистикой по символу/магику в таблице на правой стороне панели.
В статье "Делаем информационную панель для отображения данных в индикаторах и советниках" мы возьмем информационную панель.
Статья "SQLite: нативная работа с базами данных на SQL в MQL5" поможет нам получить статистику по счёту и в разрезе символов и магиков (торговых стратегий).
Информационная панель
С момента первой публикации статьи о создании информационной панели, её код претерпел некоторые изменения и доработки. Подробно расписывать все сделанные в коде изменения не входит в планы и задачи данной статьи. Поэтому просто сделаем краткий обзор изменений. А поглядеть, что и как именно было доработано, можно, скачав первую версию панели из указанной статьи и сравнив с кодом этой же панели, прилагаемым к статье.
Была проведена работа над ошибками, устранены ошибки позиционирования и отображения панели в некоторых ситуациях при переключении графиков. Теперь к панели можно прикрепить дочерние панели. В частности, из них можно делать кнопки, свернув дочернюю панель и оставив только её шапку, которая и будет служить кнопкой. Были сняты ограничения на позиционирование таблиц внутри панели. Изначально таблица не могла рисоваться за пределами панели — только в области видимости. В некоторых случаях таблицы, рисуемые на панели, должны позиционироваться за пределами видимой области панели. Это необходимо для прокрутки длинных или широких таблиц, списки которых по своим размерам больше размера панели. Таким образом, если разрешить позиционировать начальные координаты таблицы за пределами панели, мы сможем сделать прокрутку таблицы. Именно это мы и будем сегодня делать. Но изменять классы панели и её таблиц мы не будем (хотя так правильнее в смысле дальнейшего использования доработанного класса панели) для того, чтобы можно было наглядно показать, что, имея не совсем подходящие по функционалу примеры, при правильном подходе всегда можно довести функциональность до нужного результата.
Посмотрим вкратце, что было доработано в коде классов таблиц и панели.
В класс ячейки таблицы добавлена переменная для хранения текста ячейки и методы для работы с этой переменной:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ class CTableCell : public CObject { private: int m_row; // Строка int m_col; // Столбец int m_x; // Координата X int m_y; // Координата Y string m_text; // Текст в ячейке public: //--- Методы установки значений void SetRow(const uint row) { this.m_row=(int)row; } void SetColumn(const uint col) { this.m_col=(int)col; } void SetX(const uint x) { this.m_x=(int)x; } void SetY(const uint y) { this.m_y=(int)y; } void SetXY(const uint x,const uint y) { this.m_x=(int)x; this.m_y=(int)y; } void SetText(const string text) { this.m_text=text; } //--- Методы получения значений int Row(void) const { return this.m_row; } int Column(void) const { return this.m_col; } int X(void) const { return this.m_x; } int Y(void) const { return this.m_y; } string Text(void) const { return this.m_text; } //--- Виртуальный метод сравнения двух объектов virtual int Compare(const CObject *node,const int mode=0) const { const CTableCell *compared=node; return(this.Column()>compared.Column() ? 1 : this.Column()<compared.Column() ? -1 : 0); } //--- Конструктор/деструктор CTableCell(const int row,const int column) : m_row(row),m_col(column){} ~CTableCell(void){} };
В классе данных таблиц теперь идентификатор может иметь отрицательное значение, равно как и координаты таблицы могут быть отрицательными. Добавлен метод установки идентификатора таблицы:
//+------------------------------------------------------------------+ //| Класс данных таблиц | //+------------------------------------------------------------------+ class CTableData : public CObject { private: CArrayObj m_list_rows; // Список строк int m_id; // Идентификатор таблицы int m_x1; // Координата X1 int m_y1; // Координата Y1 int m_x2; // Координата X2 int m_y2; // Координата Y2 int m_w; // Ширина int m_h; // Высота string m_name; // Наименование таблицы public: //--- Устанавливает (1) идентификатор, (2) наименование таблицы void SetID(const int id) { this.m_id=id; } void SetName(const string name) { this.m_name=name; } //--- Возвращает (1) идентификатор, (2) наименование таблицы int ID(void) const { return this.m_id; } string Name(void) const { return this.m_name; } //--- Устанавливает координату (1) X1, (2) X2 void SetX1(const int x1) { this.m_x1=x1; } void SetX2(const int x2) { this.m_x2=x2; } //--- Устанавливает координату (1) Y1, (2) Y2 void SetY1(const int y1) { this.m_y1=y1; } void SetY2(const int y2) { this.m_y2=y2; } //--- Устанавливает координаты таблицы void SetCoords(const int x1,const int y1,const int x2,const int y2) { this.SetX1(x1); this.SetY1(y1); this.SetX2(x2); this.SetY2(y2); }
Добавлен конструктор по умолчанию:
//--- Конструктор/деструктор CTableData(void) : m_id(-1) { this.m_list_rows.Clear(); this.m_name=""; } CTableData(const uint id) : m_id((int)id) { this.m_list_rows.Clear(); this.m_name=""; } ~CTableData(void) { this.m_list_rows.Clear(); }
Конструктор по умолчанию позволит объявить объект класса без создания посредством оператора new, а значения координат с типом int позволят устанавливать начальные координаты таблиц за пределами окна панели.
В классе объекта панели объявлены новые переменные:
//+------------------------------------------------------------------+ //| Класс Dashboard | //+------------------------------------------------------------------+ class CDashboard : public CObject { private: CTableData m_table_tmp; // Объект-таблица для поиска CCanvas m_canvas; // Канвас CCanvas m_workspace; // Рабочая область CArrayObj m_list_table; // Список таблиц CArrayObj m_list_obj; // Список привязанных панелей ENUM_PROGRAM_TYPE m_program_type; // Тип программы ENUM_MOUSE_STATE m_mouse_state; // Состояние кнопок мышки uint m_id; // Идентификатор объекта long m_chart_id; // ChartID int m_chart_w; // Ширина графика int m_chart_h; // Высота графика int m_x; // Координата X int m_y; // Координата Y int m_w; // Ширина int m_h; // Высота int m_x_dock; // Координата X закреплённой свёрнутой панели int m_y_dock; // Координата Y закреплённой свёрнутой панели int m_diff_x; // Смещение локальной координаты X относительно родителя int m_diff_y; // Смещение локальной координаты Y относительно родителя bool m_header; // Флаг наличия заголовка bool m_butt_close; // Флаг наличия кнопки закрытия bool m_butt_minimize; // Флаг наличия кнопки сворачивания/разворачивания bool m_butt_pin; // Флаг наличия кнопки закрепления bool m_wider_wnd; // Флаг превышения горизонтального размера панели ширины окна bool m_higher_wnd; // Флаг превышения вертикального размера панели высоты окна bool m_movable; // Флаг перемещаемости панели int m_header_h; // Высота заголовка int m_wnd; // Номер подокна графика int m_title_x_shift; // Смещение текста заголовка по горизонтали int m_title_y_shift; // Смещение текста заголовка по вертикали uchar m_header_alpha; // Прозрачность заголовка uchar m_header_alpha_c; // Текущая прозрачность заголовка color m_header_back_color; // Цвет фона заголовка color m_header_back_color_c; // Текущий цвет фона заголовка color m_header_fore_color; // Цвет текста заголовка color m_header_fore_color_c; // Текущий цвет текста заголовка color m_header_border_color; // Цвет рамки заголовка color m_header_border_color_c; // Текущий цвет рамки заголовка color m_butt_close_back_color; // Цвет фона кнопки закрытия color m_butt_close_back_color_c; // Текущий цвет фона кнопки закрытия color m_butt_close_fore_color; // Цвет значка кнопки закрытия color m_butt_close_fore_color_c; // Текущий цвет значка кнопки закрытия color m_butt_min_back_color; // Цвет фона кнопки сворачивания/разворачивания color m_butt_min_back_color_c; // Текущий цвет фона кнопки сворачивания/разворачивания color m_butt_min_fore_color; // Цвет значка кнопки сворачивания/разворачивания color m_butt_min_fore_color_c; // Текущий цвет значка кнопки сворачивания/разворачивания color m_butt_pin_back_color; // Цвет фона кнопки закрепления color m_butt_pin_back_color_c; // Текущий цвет фона кнопки закрепления color m_butt_pin_fore_color; // Цвет значка кнопки закрепления color m_butt_pin_fore_color_c; // Текущий цвет значка кнопки закрепления uchar m_alpha; // Прозрачность панели uchar m_alpha_c; // Текущая прозрачность панели uchar m_fore_alpha; // Прозрачность текста uchar m_fore_alpha_c; // Текущая прозрачность текста color m_back_color; // Цвет фона color m_back_color_c; // Текущий цвет фона color m_fore_color; // Цвет текста color m_fore_color_c; // Текущий цвет текста color m_border_color; // Цвет рамки color m_border_color_c; // Текущий цвет рамки string m_title; // Текст заголовка string m_title_font; // Фонт заголовка int m_title_font_size; // Размер шрифта заголовка string m_font; // Фонт int m_font_size; // Размер шрифта bool m_minimized; // Флаг свёрнутого окна панели string m_program_name; // Имя программы string m_name_gv_x; // Наименование глобальной переменной терминала, хранящей координату X string m_name_gv_y; // Наименование глобальной переменной терминала, хранящей координату Y string m_name_gv_m; // Наименование глобальной переменной терминала, хранящей флаг свёрнутости панели string m_name_gv_u; // Наименование глобальной переменной терминала, хранящей флаг закреплённой панели string m_filename_bg; // Наименование файла для сохранения пикселей фона string m_filename_ws; // Наименование файла для сохранения пикселей рабочей области uint m_array_wpx[]; // Массив пикселей для сохранения/восстановления рабочей области uint m_array_ppx[]; // Массив пикселей для сохранения/восстановления фона панели int m_mouse_diff_x; // Смещение курсора относительно угла привязки по X int m_mouse_diff_y; // Смещение курсора относительно угла привязки по Y bool m_slave; // Признак привязанной (зависимой) панели string m_name; // Наименование панели
Так как теперь к родительской панели можно привязывать дочерние, то некоторые методы работы с панелью перенесены из защищённой секции в публичную — к этим методам необходим доступ извне, и добавлены новые методы:
public: //--- Возвращает (1) идентификатор графика, (2) номер подокна long ChartID(void) const { return this.m_chart_id; } int SubWindow(void) const { return this.m_wnd; } //--- (1) Сворачивает, (2) разворачивает панель void Collapse(void); void Expand(void); //--- (1) Скрывает, (2) показывает, (3) переносит на передний план панель void Hide(const bool redraw=false); void Show(const bool redraw=false); void BringToTop(void); //--- Возвращает флаг скрытого объекта bool IsHidden(void); //--- Устанавливает новые цвета заголовка void SetHeaderNewColors(const color new_bg_color=clrNONE, const color title_new_color=clrNONE, const ushort new_alpha=USHORT_MAX) { this.m_header_back_color=(new_bg_color==clrNONE ? this.m_header_back_color : new_bg_color); this.m_header_back_color_c=this.m_header_back_color; this.m_header_fore_color=(title_new_color==clrNONE ? this.m_header_fore_color : title_new_color); this.m_header_fore_color_c=this.m_header_fore_color; this.m_header_alpha=uchar(new_alpha==USHORT_MAX ? this.m_header_alpha : (new_alpha>255 ? 255 : new_alpha)); this.m_header_alpha_c=this.m_header_alpha; } //--- Устанавливает новые свойства заголовка void SetHeaderNewParams(const string title,const color new_bg_color, const color title_new_color, const ushort new_alpha=USHORT_MAX, const int title_x_shift=0,const int title_y_shift=0, const string font_name="Calibri",const int font_size=8,const uint font_flags=0) { this.SetHeaderFontParams(font_name, font_size, font_flags); this.SetTitleShift(title_x_shift,title_y_shift); this.SetHeaderNewColors(new_bg_color,title_new_color,new_alpha); this.RedrawHeaderArea(new_bg_color, title, title_new_color, new_alpha); } //--- Устанавливает (1) ширину, (2) высоту панели bool SetWidth(const int width,const bool redraw=false); bool SetHeight(const int height,const bool redraw=false); //--- Отображает панель void View(const string title) { this.Draw(title); } //--- Возвращает объект (1) CCanvas, (2) рабочую область, (3) идентификатор объекта CCanvas *Canvas(void) { return &this.m_canvas; } CCanvas *Workspace(void) { return &this.m_workspace; } uint ID(void) const { return this.m_id; } //--- Возвращает координату (1) X, (2) Y панели int CoordX(void) const { return this.m_x; } int CoordY(void) const { return this.m_y; } //--- Возвращает (1) ширину, (2) высоту панели int Width(void) const { return this.m_w; } int Height(void) const { return this.m_h; } //--- Возвращает смещение локальной координаты (1) X, (2) Y панели int CoordDiffX(void) const { return this.m_diff_x; } int CoordDiffY(void) const { return this.m_diff_y; } //--- Устанавливает смещение локальной координаты (1) X, (2) Y панели void SetCoordDiffX(const int diff_x) { this.m_diff_x=diff_x; } void SetCoordDiffY(const int diff_y) { this.m_diff_y=diff_y; } //--- Устанавливает смещения текста заголовка по (1) горизонтали, (2) вертикали, (3) оба void SetTitleXShift(const int shift) { this.m_title_x_shift=shift; } void SetTitleYShift(const int shift) { this.m_title_y_shift=shift; } void SetTitleShift(const int x_shift, const int y_shift) { if(this.m_title_x_shift!=x_shift) this.m_title_x_shift=x_shift; if(this.m_title_y_shift!=y_shift) this.m_title_y_shift=y_shift; } //--- Возвращает (1) ширину, (2) высоту, (3) размеры указанного текста int TextWidth(const string text) { return this.m_workspace.TextWidth(text); } int TextHeight(const string text) { return this.m_workspace.TextHeight(text); } void TextSize(const string text,int &width,int &height) { this.m_workspace.TextSize(text,width,height); } //--- Устанавливает флаг (1) наличия, (2) отсутствия заголовка панели void SetPanelHeaderOn(const bool redraw=false); void SetPanelHeaderOff(const bool redraw=false); //--- Устанавливает флаг (1) наличия, (2) отсутствия кнопки закрытия void SetButtonCloseOn(const bool redraw=false); void SetButtonCloseOff(const bool redraw=false); //--- Устанавливает флаг (1) наличия, (2) отсутствия кнопки сворачивания/разворачивания void SetButtonMinimizeOn(const bool redraw=false); void SetButtonMinimizeOff(const bool redraw=false); //--- Устанавливает флаг (1) наличия, (2) отсутствия кнопки закрепления/открепления void SetButtonPinOn(const bool redraw=false); void SetButtonPinOff(const bool redraw=false); //--- Устанавливает координаты панели bool SetCoords(const int x,const int y); //--- Устанавливает размеры панели bool SetSizes(const int w,const int h,const bool update=false); //--- Устанавливает координаты и размеры панели bool SetParams(const int x,const int y,const int w,const int h,const bool update=false); //--- Устанавливает прозрачность (1) заголовка, (2) рабочей области панели void SetHeaderTransparency(const uchar value); void SetTransparency(const uchar value); //--- Устанавливает параметры шрифта (1) панели, (2) заголовка по умолчанию void SetFontParams(const string name,const int size,const uint flags=0,const uint angle=0); void SetHeaderFontParams(const string name,const int size,const uint flags=0,const uint angle=0); //--- Возвращает установленные параметры шрифта (1) панели, (2) заголовка string FontParams(int &size,uint &flags,uint &angle); string FontHeaderParams(int &size,uint &flags,uint &angle); //--- Возвращает установленные (1) шрифт, (2) размер, (3) флаги шрифта панели string FontName(void) const { return this.m_workspace.FontNameGet(); } int FontSize(void) const { return this.m_workspace.FontSizeGet(); } uint FontFlags(void) const { return this.m_workspace.FontFlagsGet(); } //--- Возвращает установленные (1) шрифт, (2) размер, (3) флаги шрифта заголовка string FontHeaderName(void) const { return this.m_canvas.FontNameGet(); } int FontHeaderSize(void) const { return this.m_canvas.FontSizeGet(); } uint FontHeaderFlags(void) const { return this.m_canvas.FontFlagsGet(); } //--- (1) Устанавливает (2) возвращает цвет текста рабочей области панели void SetForeColor(const color clr) { this.m_fore_color=clr; } color ForeColor(void) const { return this.m_fore_color; } //--- Выводит (2) текстовое сообщение, (2) закрашенный прямоугольник в указанные координаты void DrawText(const string text,const int x,const int y,const color clr=clrNONE,const int width=WRONG_VALUE,const int height=WRONG_VALUE); void DrawRectangleFill(const int x,const int y,const int width,const int height,const color clr,const uchar alpha); //--- Создаёт новую таблицу bool CreateNewTable(const int id=WRONG_VALUE); //--- Возвращает объект табличных данных по (1) идентификатору, (2) наименованию, (3) количество таблиц в списке CTableData *GetTable(const uint id); CTableData *GetTable(const string name); int TableTotal(void) const { return this.m_list_table.Total(); } //--- Возвращает флаг наличия таблицы в списке по (1) идентификатору, (2) наименованию bool TableIsExist(const uint id); bool TableIsExist(const string name); //--- Рисует (1) фоновую сетку, (2) с автоматическим размером ячеек void DrawGrid(const uint table_id,const int x,const int y,const uint rows,const uint columns,const uint row_size,const uint col_size,const color line_color=clrNONE,bool alternating_color=true); void DrawGridAutoFill(const uint table_id,const uint border,const uint rows,const uint columns,const color line_color=clrNONE,bool alternating_color=true); //--- Стирает всё нарисованное на панели и восстанавливает первоначальный вид void Clear(void) { this.m_canvas.Erase(::ColorToARGB(this.m_back_color,this.m_alpha)); this.DrawFrame(); this.m_workspace.Erase(0x00FFFFFF); } //--- Распечатывает данные сетки (координаты пересечения линий) void GridPrint(const uint table_id,const uint tabulation=0) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); return; } table.Print(tabulation); } //--- Записывает в переменные значения координат X и Y указанной ячейки таблицы void CellXY(const uint table_id,const uint row,const uint column, int &x, int &y) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); return; } table.CellXY(row,column,x,y); } //--- Возвращает координату (1) X, (2) Y указанной ячейки таблицы int CellX(const uint table_id,const uint row,const uint column) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); return WRONG_VALUE; } return table.CellX(row,column); } int CellY(const uint table_id,const uint row,const uint column) { CTableData *table=this.GetTable(table_id); if(table==NULL) { ::PrintFormat("%s: Error. Failed to get table object with id %lu",__FUNCTION__,table_id); return WRONG_VALUE; } return table.CellY(row,column); } //--- Записывает в переменные значения координат X1 и Y1, X2 и Y2 указанной таблицы void TableCoords(const uint table_id,int &x1,int &y1,int &x2,int &y2) { x1=y1=x2=y2=WRONG_VALUE; CTableData *table=this.GetTable(table_id); if(table==NULL) return; x1=table.X1(); y1=table.Y1(); x2=table.X2(); y2=table.Y2(); } //--- Возвращает координату (1) X1, (2) Y1, (3) X2, (4) Y2 указанной таблицы int TableX1(const uint table_id) { CTableData *table=this.GetTable(table_id); return(table!=NULL ? table.X1() : WRONG_VALUE); } int TableY1(const uint table_id) { CTableData *table=this.GetTable(table_id); return(table!=NULL ? table.Y1() : WRONG_VALUE); } int TableX2(const uint table_id) { CTableData *table=this.GetTable(table_id); return(table!=NULL ? table.X2() : WRONG_VALUE); } int TableY2(const uint table_id) { CTableData *table=this.GetTable(table_id); return(table!=NULL ? table.Y2() : WRONG_VALUE); } //--- Сравнивает два объекта по идентификатору virtual int Compare(const CObject *node,const int mode=0) const { const CDashboard *obj=node; return(this.ID()>obj.ID() ? 1 : this.ID()<obj.ID() ? -1 : 0); } //--- Создаёт и привязывает новую панель CDashboard *InsertNewPanel(const uint id, const int x, const int y, const int w, const int h) { CDashboard *obj=new CDashboard(id, this.CoordX()+x, this.CoordY()+y, w, (h>20 ? h : 21)); if(obj==NULL) return NULL; int diff_x=obj.CoordX()-this.CoordX(); int diff_y=obj.CoordY()-this.CoordY(); this.m_list_obj.Sort(); if(this.m_list_obj.Search(obj)==0 || !this.m_list_obj.Add(obj)) { delete obj; return NULL; } obj.SetCoordDiffX(diff_x); obj.SetCoordDiffY(diff_y); obj.SetAsSlave(); return obj; } //--- Возвращает указатель на панель по (1) идентификатору, (2) наименованию CDashboard *GetPanel(const uint id) { for(int i=0;i<this.m_list_obj.Total();i++) { CDashboard *obj=this.m_list_obj.At(i); if(obj!=NULL && obj.ID()==id) return obj; } return NULL; } CDashboard *GetPanel(const string name) { for(int i=0;i<this.m_list_obj.Total();i++) { CDashboard *obj=this.m_list_obj.At(i); if(obj!=NULL && obj.Name()==name) return obj; } return NULL; } //--- (1) Устанавливает, (2) возвращает флаг ведомого объекта void SetAsSlave(void) { this.m_slave=true; this.m_movable=false; } bool IsSlave(void) const { return this.m_slave; } //--- Возвращает флаг того, что объект с указанным именем принадлежит панели (например, создан объектом панели) bool IsOwnObject(const string object_name) const { string bmp=::ObjectGetString(this.m_chart_id,object_name,OBJPROP_BMPFILE); return(::StringFind(bmp,this.m_program_name+".ex5::")>WRONG_VALUE); } //--- (1) Устанавливает, (2) возвращает наименование панели void SetName(const string name) { this.m_name=name; } string Name(void) const { return this.m_name; } //--- Возвращает текст заголовка панели string HeaderTitle(void) const { return this.m_title; } //--- Обработчик событий
Все новые методы и произведённые доработки позволяют теперь прикреплять к родительской панели дочерние и использовать их как самостоятельные объекты, но зависимые от своего родителя. Теперь можно создавать прокручиваемые таблицы в случае, если их размер больше размера самой панели. Раньше таблицы могли быть только размером, меньшим, чем сама панель. Функционала прокрутки таблиц в классе панели нет — его мы организуем прямо из главной программы. Впоследствии, при необходимости, такой функционал будет добавлен к классу панели и её таблиц. Но на данный момент такой потребности нет.
Естественно, здесь мы рассмотрели только малую часть внесённых изменений в класс панели и её таблиц — только объявленные методы. Доработки вносились постепенно в достаточно обширную часть кода на протяжении времени, прошедшего с момента первой публикации. При желании всегда можно скачать первую версию панели из статьи её описания и сравнить с версией, представленной в этой статье. Файл информационной панели должен быть расположен в каталоге этого проекта: \MQL5\Indicators\StatisticsBy\Dashboard\Dashboard.mqh.
Функции для работы с базой данных
С базами данных я работал совсем давно и совсем немного, при этом это был совместный проект для игровой индустрии на C#, где БД занимался другой человек, а я лишь использовал предоставленный коннектор для подключения БД к проекту. Поэтому здесь пришлось вооружиться справочными материалами и статьями на ресурсе mql5.com. Начав изучать статью "SQLite: нативная работа с базами данных на SQL в MQL5", сразу увидел отсылки с документации, в первую же очередь к функции DatabasePrepare(). А там есть пример, в котором создаётся таблица сделок, а на её основе — таблица трейдов. Это же одно из того, что нам нужно! Вооружаемся терпением и изучаем пример и его функции.
Сначала видим две структуры для хранения данных сделок и трейдов:
//--- структура для хранения сделки struct Deal { ulong ticket; // DEAL_TICKET long order_ticket; // DEAL_ORDER long position_ticket; // DEAL_POSITION_ID datetime time; // DEAL_TIME char type; // DEAL_TYPE char entry; // DEAL_ENTRY string symbol; // DEAL_SYMBOL double volume; // DEAL_VOLUME double price; // DEAL_PRICE double profit; // DEAL_PROFIT double swap; // DEAL_SWAP double commission; // DEAL_COMMISSION long magic; // DEAL_MAGIC char reason; // DEAL_REASON }; //--- структура для хранения трейда - порядок членов соответствует позиции в терминале struct Trade { datetime time_in; // время входа ulong ticket; // ID позиции char type; // покупка или продажа double volume; // объем string symbol; // символ double price_in; // цена входа datetime time_out; // время выхода double price_out; // цена выхода double commission; // комиссия за вход и выход double swap; // своп double profit; // прибыль или убыток };
Далее разбираем логику:
//+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- создадим имя файла string filename=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"_trades.sqlite"; //--- открываем/создаем базу данных в общей папке терминалов int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); return; } //--- создадим таблицу DEALS if(!CreateTableDeals(db)) { DatabaseClose(db); return; } //--- запросим всю торговую историю datetime from_date=0; datetime to_date=TimeCurrent(); //--- запросим историю сделок в указанном интервале HistorySelect(from_date, to_date); int deals_total=HistoryDealsTotal(); PrintFormat("Торговая история насчитывает сделок: %d ", deals_total); //--- внесем в таблицу сделки if(!InsertDeals(db)) return; //--- покажем первые 10 сделок Deal deals[], deal; ArrayResize(deals, 10); int request=DatabasePrepare(db, "SELECT * FROM DEALS"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } int i; for(i=0; DatabaseReadBind(request, deal); i++) { if(i>=10) break; deals[i].ticket=deal.ticket; deals[i].order_ticket=deal.order_ticket; deals[i].position_ticket=deal.position_ticket; deals[i].time=deal.time; deals[i].type=deal.type; deals[i].entry=deal.entry; deals[i].symbol=deal.symbol; deals[i].volume=deal.volume; deals[i].price=deal.price; deals[i].profit=deal.profit; deals[i].swap=deal.swap; deals[i].commission=deal.commission; deals[i].magic=deal.magic; deals[i].reason=deal.reason; } //--- выведем сделки на печать if(i>0) { ArrayResize(deals, i); PrintFormat("Первые %d сделок:", i); ArrayPrint(deals); } //--- удалим запрос после использования DatabaseFinalize(request); //--- проверим что на счете используется хеджинг для учета открытых позиций if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { //--- не можем преобразовать сделки в трейды простым способом через транзакции, поэтому завершаем работу DatabaseClose(db); return; } //--- теперь создадим таблицу TRADES на основе таблицы DEALS if(!CreateTableTrades(db)) { DatabaseClose(db); return; } //--- заполним через SQL-запрос таблицу TRADES на основе данных из DEALS ulong start=GetMicrosecondCount(); if(DatabaseTableExists(db, "DEALS")) //--- заполним таблицу TRADES if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) " "SELECT " " d1.time as time_in," " d1.position_id as ticket," " d1.type as type," " d1.volume as volume," " d1.symbol as symbol," " d1.price as price_in," " d2.time as time_out," " d2.price as price_out," " d1.commission+d2.commission as commission," " d2.swap as swap," " d2.profit as profit " "FROM DEALS d1 " "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id " "WHERE d1.entry=0 AND d2.entry=1")) { Print("DB: fillng the TRADES table failed with code ", GetLastError()); return; } ulong transaction_time=GetMicrosecondCount()-start; //--- покажем первые 10 сделок Trade trades[], trade; ArrayResize(trades, 10); request=DatabasePrepare(db, "SELECT * FROM TRADES"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } for(i=0; DatabaseReadBind(request, trade); i++) { if(i>=10) break; trades[i].time_in=trade.time_in; trades[i].ticket=trade.ticket; trades[i].type=trade.type; trades[i].volume=trade.volume; trades[i].symbol=trade.symbol; trades[i].price_in=trade.price_in; trades[i].time_out=trade.time_out; trades[i].price_out=trade.price_out; trades[i].commission=trade.commission; trades[i].swap=trade.swap; trades[i].profit=trade.profit; } //--- выведем трейды на печать if(i>0) { ArrayResize(trades, i); PrintFormat("\r\nПервые %d трейдов:", i); ArrayPrint(trades); PrintFormat("Заполнение таблицы TRADES заняло %.2f миллисекунд",double(transaction_time)/1000); } //--- удалим запрос после использования DatabaseFinalize(request); //--- закрываем базу данных DatabaseClose(db); }
- Создаётся база данных,
- создаётся в БД таблица сделок,
- запрашивается история сделок, и сделки вносятся в созданную таблицу,
- проверяется тип счёта. Должен быть хедж, так как для неттинга просто так, на основании одних сделок, торговую историю не создать,
- создаётся таблица трейдов на основе таблицы сделок, и в таблицу трейдов заносятся данные трейдов на основе данных таблицы сделок.
Представленный скрипт ещё выводит на печать первые десять сделок и первые десять трейдов из созданных таблиц. Нам это не нужно.
Исходя из логики, нам нужно создать несколько функций на основе тех, что представлены в примере, и из строк кода в теле скрипта примера:
//+------------------------------------------------------------------+ //| Создает таблицу DEALS | //+------------------------------------------------------------------+ bool CreateTableDeals(int database) { //--- если таблица DEALS уже есть, удалим её if(!DeleteTable(database, "DEALS")) { return(false); } //--- проверим наличие таблицы if(!DatabaseTableExists(database, "DEALS")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE DEALS(" "ID INT KEY NOT NULL," "ORDER_ID INT NOT NULL," "POSITION_ID INT NOT NULL," "TIME INT NOT NULL," "TYPE INT NOT NULL," "ENTRY INT NOT NULL," "SYMBOL CHAR(10)," "VOLUME REAL," "PRICE REAL," "PROFIT REAL," "SWAP REAL," "COMMISSION REAL," "MAGIC INT," "REASON INT );")) { Print("DB: create the DEALS table failed with code ", GetLastError()); return(false); } //--- таблица успешно создана return(true); } //+------------------------------------------------------------------+ //| Удаляет из базы таблицу с указанным именем | //+------------------------------------------------------------------+ bool DeleteTable(int database, string table_name) { if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name)) { Print("Failed to drop the DEALS table with code ", GetLastError()); return(false); } //--- таблица успешно удалена return(true); } //+------------------------------------------------------------------+ //| Вносит сделки в таблицу базы данных | //+------------------------------------------------------------------+ bool InsertDeals(int database) { //--- вспомогательные переменные ulong deal_ticket; // тикет сделки long order_ticket; // тикет ордера,по которому была совершена сделка long position_ticket; // ID позиции, к которой относится сделка datetime time; // время совершения сделки long type ; // тип сделки long entry ; // направление сделки string symbol; // по какому символу была сделка double volume; // объем операции double price; // цена double profit; // финансовый результат double swap; // своп double commission; // комиссия long magic; // Magic number (ID советника) long reason; // причина или источник проведения сделки //--- пройдем по всем сделкам и внесем их в базу данных bool failed=false; int deals=HistoryDealsTotal(); //--- заблокируем базу данных перед выполнением транзакций DatabaseTransactionBegin(database); for(int i=0; i<deals; i++) { deal_ticket= HistoryDealGetTicket(i); order_ticket= HistoryDealGetInteger(deal_ticket, DEAL_ORDER); position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); type= HistoryDealGetInteger(deal_ticket, DEAL_TYPE); entry= HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); symbol= HistoryDealGetString(deal_ticket, DEAL_SYMBOL); volume= HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); price= HistoryDealGetDouble(deal_ticket, DEAL_PRICE); profit= HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); swap= HistoryDealGetDouble(deal_ticket, DEAL_SWAP); commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); magic= HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); reason= HistoryDealGetInteger(deal_ticket, DEAL_REASON); //--- внесем в таблицу каждую сделку через запрос string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)" "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d)", deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason); if(!DatabaseExecute(database, request_text)) { PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError()); PrintFormat("i=%d: deal #%d %s", i, deal_ticket, symbol); failed=true; break; } } //--- проверим на наличие ошибок при выполнении транзакций if(failed) { //--- откатим все транзакции и разблокируем базу данных DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError()); return(false); } //--- все транзакции прошли успешно - зафиксируем изменения и разблокируем базу данных DatabaseTransactionCommit(database); return(true); } //+------------------------------------------------------------------+ //| Создает таблицу TRADES | //+------------------------------------------------------------------+ bool CreateTableTrades(int database) { //--- если таблица TRADES уже есть, удалим её if(!DeleteTable(database, "TRADES")) return(false); //--- проверим наличие таблицы if(!DatabaseTableExists(database, "TRADES")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE TRADES(" "TIME_IN INT NOT NULL," "TICKET INT NOT NULL," "TYPE INT NOT NULL," "VOLUME REAL," "SYMBOL CHAR(10)," "PRICE_IN REAL," "TIME_OUT INT NOT NULL," "PRICE_OUT REAL," "COMMISSION REAL," "SWAP REAL," "PROFIT REAL);")) { Print("DB: create the TRADES table failed with code ", GetLastError()); return(false); } //--- таблица успешно создана return(true); } //+------------------------------------------------------------------+
Кроме тех полей структур и таблиц, что представлены в примере, нам нужно ещё поле, в котором будет храниться номер счёта — это потребуется для создания таблицы статистики по торговле на счёте. Иными словами — полной статистики торговли.
А как же нам сделать таблицы статистики? Ладно, читаем далее статью и находим! Вот именно то, что нам и нужно:
Анализ портфеля в разрезе стратегий
В вышеприведенных результатах работы скрипта из DatabasePrepare видно, что торговля ведется на нескольких валютных парах. Но кроме того, там же мы видим в столбце [magic] значения от 100 до 600. Это значит, что на торговом счёте ведут торговлю несколько стратегий, каждая из которых имеет свой собственный Magic Number для идентификации своих сделок.
С помощью SQL-запроса мы можем сделать анализ торговли в разрезе значений magic:
//--- получим торговую статистику в разрезе советников по Magic Number request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT MAGIC," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY MAGIC" " ) as r");
Результат:
Trade statistics by Magic Number [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] 100 242 2584.80000 -2110.00000 -33.36000 -93.53000 474.80000 347.91000 143 99 1.96198 59.09091 40.90909 18.07552 -21.31313 1.22502 [1] 200 254 3021.92000 -2834.50000 -29.45000 -98.22000 187.42000 59.75000 140 114 0.73787 55.11811 44.88189 21.58514 -24.86404 1.06612 [2] 300 250 2489.08000 -2381.57000 -34.37000 -96.58000 107.51000 -23.44000 134 116 0.43004 53.60000 46.40000 18.57522 -20.53078 1.04514 [3] 400 224 1272.50000 -1283.00000 -24.43000 -64.80000 -10.50000 -99.73000 131 93 -0.04687 58.48214 41.51786 9.71374 -13.79570 0.99182 [4] 500 198 1141.23000 -1051.91000 -27.66000 -63.36000 89.32000 -1.70000 116 82 0.45111 58.58586 41.41414 9.83819 -12.82817 1.08491 [5] 600 214 1317.10000 -1396.03000 -34.12000 -68.48000 -78.93000 -181.53000 116 98 -0.36883 54.20561 45.79439 11.35431 -14.24520 0.94346
Мы видим, что 4 из 6 стратегий показали прибыль. И для каждой из стратегий мы получили статистические показатели:
- trades — количество трейдов по стратегии,
- gross_profit — общая прибыль по стратегии (сумма всех положительных значений profit),
- gross_loss — общий убыток по стратегии (сумма всех отрицательных значений profit),
- total_commission — сумма всех комиссий по трейдам стратегии,
- total_swap — сумма всех свопов по трейдам стратегии,
- total_profit — сумма gross_profit и gross_loss,
- net_profit — сумма (gross_profit + gross_loss + total_commission + total_swap),
- win_trades — количество трейдов, где profit>0,
- loss_trades— количество трейдов, где profit<0,
- expected_payoff — матожидание трейда без учета свопов и комисссий = net_profit/trades,
- win_percent — процент выигрышных трейдов,
- loss_percent — процент проигрышных трейдов,
- average_profit — средний выигрыш = gross_profit/win_trades,
- average_loss — средний проигрыш = gross_loss /loss_trades,
- profit_factor — профит фактор = gross_profit/gross_loss.
Данная статистика для вычисления прибыли и убытка не принимает во внимание свопы и комиссии, которые были начислены на позицию. Это позволяет увидеть данные издержки в чистом виде. Может оказаться, что сама стратегия дает небольшую прибыль, но за счет свопов и комиссий оказывается невыгодной.
Анализ сделок по символам
Мы можем провести анализ торговли в разрезе символов. Для этого сделаем такой запрос:
//--- получим торговую статистику в разрезе символов int request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT SYMBOL," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY SYMBOL" " ) as r");
Результат:
Trade statistics by Symbol [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] "AUDUSD" 112 503.20000 -568.00000 -8.83000 -24.64000 -64.80000 -98.27000 70 42 -0.57857 62.50000 37.50000 7.18857 -13.52381 0.88592 [1] "EURCHF" 125 607.71000 -956.85000 -11.77000 -45.02000 -349.14000 -405.93000 54 71 -2.79312 43.20000 56.80000 11.25389 -13.47676 0.63512 [2] "EURJPY" 127 1078.49000 -1057.83000 -10.61000 -45.76000 20.66000 -35.71000 64 63 0.16268 50.39370 49.60630 16.85141 -16.79095 1.01953 [3] "EURUSD" 233 1685.60000 -1386.80000 -41.00000 -83.76000 298.80000 174.04000 127 106 1.28240 54.50644 45.49356 13.27244 -13.08302 1.21546 [4] "GBPCHF" 125 1881.37000 -1424.72000 -22.60000 -51.56000 456.65000 382.49000 80 45 3.65320 64.00000 36.00000 23.51712 -31.66044 1.32052 [5] "GBPJPY" 127 1943.43000 -1776.67000 -18.84000 -52.46000 166.76000 95.46000 76 51 1.31307 59.84252 40.15748 25.57145 -34.83667 1.09386 [6] "GBPUSD" 121 1668.50000 -1438.20000 -7.96000 -49.93000 230.30000 172.41000 77 44 1.90331 63.63636 36.36364 21.66883 -32.68636 1.16013 [7] "USDCAD" 99 405.28000 -475.47000 -8.68000 -31.68000 -70.19000 -110.55000 51 48 -0.70899 51.51515 48.48485 7.94667 -9.90563 0.85238 [8] "USDCHF" 206 1588.32000 -1241.83000 -17.98000 -65.92000 346.49000 262.59000 131 75 1.68199 63.59223 36.40777 12.12458 -16.55773 1.27902 [9] "USDJPY" 107 464.73000 -730.64000 -35.12000 -34.24000 -265.91000 -335.27000 50 57 -2.48514 46.72897 53.27103 9.29460 -12.81825 0.63606
Статистика говорит нам, что только на 5 символах из 10 была получена чистая прибыль (net_profit>0), хотя на 6 из 10 профит фактор был положителен (profit_factor>1). Как раз тот случай, когда свопы и комиссии делают стратегию проигрышной на EURJPY.
Читаем статью далее и видим:
Чудесно! Идём по ссылке в справку и получаем полный код из примера к этой функции:
//--- статистика по символу struct Symbol_Stats { string name; // имя символа int trades; // количество трейдов по символу double gross_profit; // общая прибыль по символу double gross_loss; // общий убыток по символу double total_commission; // сумма комиссий по символу double total_swap; // сумма свопов по символу double total_profit; // общая прибыль без учета свопов и комиссий double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор }; //--- статистика по Magic Number struct Magic_Stats { long magic; // Magic Number советника int trades; // количество трейдов по символу double gross_profit; // общая прибыль по символу double gross_loss; // общий убыток по символу double total_commission; // сумма комиссий по символу double total_swap; // сумма свопов по символу double total_profit; // общая прибыль без учета свопов и комиссий double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор }; //--- статистика по часу входа struct Hour_Stats { char hour_in; // час входа в рынок int trades; // количество трейдов в этот час входа double volume; // объем трейдов в этот час входа double gross_profit; // общая прибыль в этот час входа double gross_loss; // общий убыток в этот час входа double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор }; int ExtDealsTotal=0;; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- создадим имя файла string filename=IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"_stats.sqlite"; //--- открываем/создаем базу данных в общей папке терминалов int db=DatabaseOpen(filename, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); if(db==INVALID_HANDLE) { Print("DB: ", filename, " open failed with code ", GetLastError()); return; } //--- создадим таблицу DEALS if(!CreateTableDeals(db)) { DatabaseClose(db); return; } PrintFormat("Торговая история насчитывает сделок: %d ", ExtDealsTotal); //--- получим торговую статистику в разрезе символов int request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT SYMBOL," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY SYMBOL" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } Symbol_Stats stats[], symbol_stats; ArrayResize(stats, ExtDealsTotal); int i=0; //--- получаем записи из результатов запроса for(; DatabaseReadBind(request, symbol_stats) ; i++) { stats[i].name=symbol_stats.name; stats[i].trades=symbol_stats.trades; stats[i].gross_profit=symbol_stats.gross_profit; stats[i].gross_loss=symbol_stats.gross_loss; stats[i].total_commission=symbol_stats.total_commission; stats[i].total_swap=symbol_stats.total_swap; stats[i].total_profit=symbol_stats.total_profit; stats[i].net_profit=symbol_stats.net_profit; stats[i].win_trades=symbol_stats.win_trades; stats[i].loss_trades=symbol_stats.loss_trades; stats[i].expected_payoff=symbol_stats.expected_payoff; stats[i].win_percent=symbol_stats.win_percent; stats[i].loss_percent=symbol_stats.loss_percent; stats[i].average_profit=symbol_stats.average_profit; stats[i].average_loss=symbol_stats.average_loss; stats[i].profit_factor=symbol_stats.profit_factor; } ArrayResize(stats, i); Print("Trade statistics by Symbol"); ArrayPrint(stats); Print(""); //--- удалим запрос DatabaseFinalize(request); //--- получим торговую статистику в разрезе советников по Magic Number request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT MAGIC," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY MAGIC" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } Magic_Stats EA_stats[], magic_stats; ArrayResize(EA_stats, ExtDealsTotal); i=0; //--- выводим записи for(; DatabaseReadBind(request, magic_stats) ; i++) { EA_stats[i].magic=magic_stats.magic; EA_stats[i].trades=magic_stats.trades; EA_stats[i].gross_profit=magic_stats.gross_profit; EA_stats[i].gross_loss=magic_stats.gross_loss; EA_stats[i].total_commission=magic_stats.total_commission; EA_stats[i].total_swap=magic_stats.total_swap; EA_stats[i].total_profit=magic_stats.total_profit; EA_stats[i].net_profit=magic_stats.net_profit; EA_stats[i].win_trades=magic_stats.win_trades; EA_stats[i].loss_trades=magic_stats.loss_trades; EA_stats[i].expected_payoff=magic_stats.expected_payoff; EA_stats[i].win_percent=magic_stats.win_percent; EA_stats[i].loss_percent=magic_stats.loss_percent; EA_stats[i].average_profit=magic_stats.average_profit; EA_stats[i].average_loss=magic_stats.average_loss; EA_stats[i].profit_factor=magic_stats.profit_factor; } ArrayResize(EA_stats, i); Print("Trade statistics by Magic Number"); ArrayPrint(EA_stats); Print(""); //--- удалим запрос DatabaseFinalize(request); //--- проверим что на счете используется хеджинг для учета открытых позиций if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { //--- не можем преобразовать сделки в трейды простым способом через транзакции, поэтому завершаем работу DatabaseClose(db); return; } //--- теперь создадим таблицу TRADES на основе таблицы DEALS if(!CreateTableTrades(db)) { DatabaseClose(db); return; } //--- заполним через SQL-запрос таблицу TRADES на основе данных из DEALS if(DatabaseTableExists(db, "DEALS")) //--- заполним таблицу TRADES if(!DatabaseExecute(db, "INSERT INTO TRADES(TIME_IN,HOUR_IN,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) " "SELECT " " d1.time as time_in," " d1.hour as hour_in," " d1.position_id as ticket," " d1.type as type," " d1.volume as volume," " d1.symbol as symbol," " d1.price as price_in," " d2.time as time_out," " d2.price as price_out," " d1.commission+d2.commission as commission," " d2.swap as swap," " d2.profit as profit " "FROM DEALS d1 " "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id " "WHERE d1.entry=0 AND d2.entry=1 ")) { Print("DB: fillng the table TRADES failed with code ", GetLastError()); return; } //--- получим торговую статистику в разрезе часа входа в рынок request=DatabasePrepare(db, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor " "FROM " " (" " SELECT HOUR_IN," " count() as trades," " sum(volume) as volume," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(profit) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades " " FROM TRADES " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY HOUR_IN" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", filename, " request failed with code ", GetLastError()); DatabaseClose(db); return; } Hour_Stats hours_stats[], h_stats; ArrayResize(hours_stats, ExtDealsTotal); i=0; //--- выводим записи for(; DatabaseReadBind(request, h_stats) ; i++) { hours_stats[i].hour_in=h_stats.hour_in; hours_stats[i].trades=h_stats.trades; hours_stats[i].volume=h_stats.volume; hours_stats[i].gross_profit=h_stats.gross_profit; hours_stats[i].gross_loss=h_stats.gross_loss; hours_stats[i].net_profit=h_stats.net_profit; hours_stats[i].win_trades=h_stats.win_trades; hours_stats[i].loss_trades=h_stats.loss_trades; hours_stats[i].expected_payoff=h_stats.expected_payoff; hours_stats[i].win_percent=h_stats.win_percent; hours_stats[i].loss_percent=h_stats.loss_percent; hours_stats[i].average_profit=h_stats.average_profit; hours_stats[i].average_loss=h_stats.average_loss; hours_stats[i].profit_factor=h_stats.profit_factor; } ArrayResize(hours_stats, i); Print("Trade statistics by entry hour"); ArrayPrint(hours_stats); Print(""); //--- удалим запрос DatabaseFinalize(request); //--- закроем базу данных DatabaseClose(db); return; } /* Торговая история насчитывает сделок: 2771 Trade statistics by Symbol [name] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] "AUDUSD" 112 503.20000 -568.00000 -8.83000 -24.64000 -64.80000 -98.27000 70 42 -0.57857 62.50000 37.50000 7.18857 -13.52381 0.88592 [1] "EURCHF" 125 607.71000 -956.85000 -11.77000 -45.02000 -349.14000 -405.93000 54 71 -2.79312 43.20000 56.80000 11.25389 -13.47676 0.63512 [2] "EURJPY" 127 1078.49000 -1057.83000 -10.61000 -45.76000 20.66000 -35.71000 64 63 0.16268 50.39370 49.60630 16.85141 -16.79095 1.01953 [3] "EURUSD" 233 1685.60000 -1386.80000 -41.00000 -83.76000 298.80000 174.04000 127 106 1.28240 54.50644 45.49356 13.27244 -13.08302 1.21546 [4] "GBPCHF" 125 1881.37000 -1424.72000 -22.60000 -51.56000 456.65000 382.49000 80 45 3.65320 64.00000 36.00000 23.51712 -31.66044 1.32052 [5] "GBPJPY" 127 1943.43000 -1776.67000 -18.84000 -52.46000 166.76000 95.46000 76 51 1.31307 59.84252 40.15748 25.57145 -34.83667 1.09386 [6] "GBPUSD" 121 1668.50000 -1438.20000 -7.96000 -49.93000 230.30000 172.41000 77 44 1.90331 63.63636 36.36364 21.66883 -32.68636 1.16013 [7] "USDCAD" 99 405.28000 -475.47000 -8.68000 -31.68000 -70.19000 -110.55000 51 48 -0.70899 51.51515 48.48485 7.94667 -9.90563 0.85238 [8] "USDCHF" 206 1588.32000 -1241.83000 -17.98000 -65.92000 346.49000 262.59000 131 75 1.68199 63.59223 36.40777 12.12458 -16.55773 1.27902 [9] "USDJPY" 107 464.73000 -730.64000 -35.12000 -34.24000 -265.91000 -335.27000 50 57 -2.48514 46.72897 53.27103 9.29460 -12.81825 0.63606 Trade statistics by Magic Number [magic] [trades] [gross_profit] [gross_loss] [total_commission] [total_swap] [total_profit] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [0] 100 242 2584.80000 -2110.00000 -33.36000 -93.53000 474.80000 347.91000 143 99 1.96198 59.09091 40.90909 18.07552 -21.31313 1.22502 [1] 200 254 3021.92000 -2834.50000 -29.45000 -98.22000 187.42000 59.75000 140 114 0.73787 55.11811 44.88189 21.58514 -24.86404 1.06612 [2] 300 250 2489.08000 -2381.57000 -34.37000 -96.58000 107.51000 -23.44000 134 116 0.43004 53.60000 46.40000 18.57522 -20.53078 1.04514 [3] 400 224 1272.50000 -1283.00000 -24.43000 -64.80000 -10.50000 -99.73000 131 93 -0.04687 58.48214 41.51786 9.71374 -13.79570 0.99182 [4] 500 198 1141.23000 -1051.91000 -27.66000 -63.36000 89.32000 -1.70000 116 82 0.45111 58.58586 41.41414 9.83819 -12.82817 1.08491 [5] 600 214 1317.10000 -1396.03000 -34.12000 -68.48000 -78.93000 -181.53000 116 98 -0.36883 54.20561 45.79439 11.35431 -14.24520 0.94346 Trade statistics by entry hour [hour_in] [trades] [volume] [gross_profit] [gross_loss] [net_profit] [win_trades] [loss_trades] [expected_payoff] [win_percent] [loss_percent] [average_profit] [average_loss] [profit_factor] [ 0] 0 50 5.00000 336.51000 -747.47000 -410.96000 21 29 -8.21920 42.00000 58.00000 16.02429 -25.77483 0.45020 [ 1] 1 20 2.00000 102.56000 -57.20000 45.36000 12 8 2.26800 60.00000 40.00000 8.54667 -7.15000 1.79301 [ 2] 2 6 0.60000 38.55000 -14.60000 23.95000 5 1 3.99167 83.33333 16.66667 7.71000 -14.60000 2.64041 [ 3] 3 38 3.80000 173.84000 -200.15000 -26.31000 22 16 -0.69237 57.89474 42.10526 7.90182 -12.50938 0.86855 [ 4] 4 60 6.00000 361.44000 -389.40000 -27.96000 27 33 -0.46600 45.00000 55.00000 13.38667 -11.80000 0.92820 [ 5] 5 32 3.20000 157.43000 -179.89000 -22.46000 20 12 -0.70187 62.50000 37.50000 7.87150 -14.99083 0.87515 [ 6] 6 18 1.80000 95.59000 -162.33000 -66.74000 11 7 -3.70778 61.11111 38.88889 8.69000 -23.19000 0.58886 [ 7] 7 14 1.40000 38.48000 -134.30000 -95.82000 9 5 -6.84429 64.28571 35.71429 4.27556 -26.86000 0.28652 [ 8] 8 42 4.20000 368.48000 -322.30000 46.18000 24 18 1.09952 57.14286 42.85714 15.35333 -17.90556 1.14328 [ 9] 9 118 11.80000 1121.62000 -875.21000 246.41000 72 46 2.08822 61.01695 38.98305 15.57806 -19.02630 1.28154 [10] 10 206 20.60000 2280.59000 -2021.80000 258.79000 115 91 1.25626 55.82524 44.17476 19.83122 -22.21758 1.12800 [11] 11 138 13.80000 1377.02000 -994.18000 382.84000 84 54 2.77420 60.86957 39.13043 16.39310 -18.41074 1.38508 [12] 12 152 15.20000 1247.56000 -1463.80000 -216.24000 84 68 -1.42263 55.26316 44.73684 14.85190 -21.52647 0.85227 [13] 13 64 6.40000 778.27000 -516.22000 262.05000 36 28 4.09453 56.25000 43.75000 21.61861 -18.43643 1.50763 [14] 14 62 6.20000 536.93000 -427.47000 109.46000 38 24 1.76548 61.29032 38.70968 14.12974 -17.81125 1.25606 [15] 15 50 5.00000 699.92000 -413.00000 286.92000 28 22 5.73840 56.00000 44.00000 24.99714 -18.77273 1.69472 [16] 16 88 8.80000 778.55000 -514.00000 264.55000 51 37 3.00625 57.95455 42.04545 15.26569 -13.89189 1.51469 [17] 17 76 7.60000 533.92000 -1019.46000 -485.54000 44 32 -6.38868 57.89474 42.10526 12.13455 -31.85813 0.52373 [18] 18 52 5.20000 237.17000 -246.78000 -9.61000 24 28 -0.18481 46.15385 53.84615 9.88208 -8.81357 0.96106 [19] 19 52 5.20000 407.67000 -150.36000 257.31000 30 22 4.94827 57.69231 42.30769 13.58900 -6.83455 2.71129 [20] 20 18 1.80000 65.92000 -89.09000 -23.17000 9 9 -1.28722 50.00000 50.00000 7.32444 -9.89889 0.73993 [21] 21 10 1.00000 41.86000 -32.38000 9.48000 7 3 0.94800 70.00000 30.00000 5.98000 -10.79333 1.29277 [22] 22 14 1.40000 45.55000 -83.72000 -38.17000 6 8 -2.72643 42.85714 57.14286 7.59167 -10.46500 0.54408 [23] 23 2 0.20000 1.20000 -1.90000 -0.70000 1 1 -0.35000 50.00000 50.00000 1.20000 -1.90000 0.63158 */ //+------------------------------------------------------------------+ //| Создает таблицу DEALS | //+------------------------------------------------------------------+ bool CreateTableDeals(int database) { //--- если таблица DEALS уже есть, удалим её if(!DeleteTable(database, "DEALS")) { return(false); } //--- проверим наличие таблицы if(!DatabaseTableExists(database, "DEALS")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE DEALS(" "ID INT KEY NOT NULL," "ORDER_ID INT NOT NULL," "POSITION_ID INT NOT NULL," "TIME INT NOT NULL," "TYPE INT NOT NULL," "ENTRY INT NOT NULL," "SYMBOL CHAR(10)," "VOLUME REAL," "PRICE REAL," "PROFIT REAL," "SWAP REAL," "COMMISSION REAL," "MAGIC INT," "HOUR INT," "REASON INT);")) { Print("DB: create the DEALS table failed with code ", GetLastError()); return(false); } //--- запросим всю торговую историю datetime from_date=0; datetime to_date=TimeCurrent(); //--- запросим историю сделок в указанном интервале HistorySelect(from_date, to_date); ExtDealsTotal=HistoryDealsTotal(); //--- внесем в таблицу сделки if(!InsertDeals(database)) return(false); //--- таблица успешно создана return(true); } //+------------------------------------------------------------------+ //| Удаляет из базы таблицу с указанным именем | //+------------------------------------------------------------------+ bool DeleteTable(int database, string table_name) { if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name)) { Print("Failed to drop the DEALS table with code ", GetLastError()); return(false); } //--- таблица успешно удалена return(true); } //+------------------------------------------------------------------+ //| Вносит сделки в таблицу базы данных | //+------------------------------------------------------------------+ bool InsertDeals(int database) { //--- вспомогательные переменные ulong deal_ticket; // тикет сделки long order_ticket; // тикет ордера,по которому была совершена сделка long position_ticket; // ID позиции, к которой относится сделка datetime time; // время совершения сделки long type ; // тип сделки long entry ; // направление сделки string symbol; // по какому символу была сделка double volume; // объем операции double price; // цена double profit; // финансовый результат double swap; // своп double commission; // комиссия long magic; // Magic number (ID советника) long reason; // причина или источник проведения сделки char hour; // час совершения сделки MqlDateTime time_strusture; //--- пройдем по всем сделкам и внесем их в базу данных bool failed=false; int deals=HistoryDealsTotal(); //--- заблокируем базу данных перед выполнением транзакций DatabaseTransactionBegin(database); for(int i=0; i<deals; i++) { deal_ticket= HistoryDealGetTicket(i); order_ticket= HistoryDealGetInteger(deal_ticket, DEAL_ORDER); position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); type= HistoryDealGetInteger(deal_ticket, DEAL_TYPE); entry= HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); symbol= HistoryDealGetString(deal_ticket, DEAL_SYMBOL); volume= HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); price= HistoryDealGetDouble(deal_ticket, DEAL_PRICE); profit= HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); swap= HistoryDealGetDouble(deal_ticket, DEAL_SWAP); commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); magic= HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); reason= HistoryDealGetInteger(deal_ticket, DEAL_REASON); TimeToStruct(time, time_strusture); hour= (char)time_strusture.hour; //--- внесем в таблицу каждую сделку через запрос string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON,HOUR)" "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d,%d)", deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason, hour); if(!DatabaseExecute(database, request_text)) { PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError()); PrintFormat("i=%d: deal #%d %s", i, deal_ticket, symbol); failed=true; break; } } //--- проверим на наличие ошибок при выполнении транзакций if(failed) { //--- откатим все транзакции и разблокируем базу данных DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code ", __FUNCTION__, GetLastError()); return(false); } //--- все транзакции прошли успешно - зафиксируем изменения и разблокируем базу данных DatabaseTransactionCommit(database); return(true); } //+------------------------------------------------------------------+ //| Создает таблицу TRADES | //+------------------------------------------------------------------+ bool CreateTableTrades(int database) { //--- если таблица TRADES уже есть, удалим её if(!DeleteTable(database, "TRADES")) return(false); //--- проверим наличие таблицы if(!DatabaseTableExists(database, "TRADES")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE TRADES(" "TIME_IN INT NOT NULL," "HOUR_IN INT NOT NULL," "TICKET INT NOT NULL," "TYPE INT NOT NULL," "VOLUME REAL," "SYMBOL CHAR(10)," "PRICE_IN REAL," "TIME_OUT INT NOT NULL," "PRICE_OUT REAL," "COMMISSION REAL," "SWAP REAL," "PROFIT REAL);")) { Print("DB: create the TRADES table failed with code ", GetLastError()); return(false); } //--- таблица успешно создана return(true); } //+------------------------------------------------------------------+
Его можно скопировать в редактор, скомпилировать и, запустив, поглядеть в журнале результат его работы.
Теперь у нас есть примеры того, как нужно работать с базой данных для получения требуемого нам результата. Потребуется лишь немного доработать представленные в справке коды. Например, нам нужно, чтобы данные были отсортированы по какому-либо полю, или получить только уникальные значения из таблиц. Для этого можно почитать справочную информацию по SQL. И на основании полученных знаний и примеров мы сможем сделать так, как необходимо нам для задуманного проекта.
Давайте создадим в каталоге терминала \MQL5\Indicators\ новую папку StaticticsBy\. В ней будут находиться все файлы данного проекта.
В созданной папке создадим новый файл SQLiteFunc.mqh и начнём его наполнять функциями для работы с базой данных.
В первую очередь напишем необходимые структуры:
//+------------------------------------------------------------------+ //| SQLiteFunc.mqh | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" //+------------------------------------------------------------------+ //| Структура для хранения сделки | //+------------------------------------------------------------------+ struct SDeal { long account; // ACCOUNT ulong ticket; // DEAL_TICKET long order_ticket; // DEAL_ORDER long position_ticket; // DEAL_POSITION_ID datetime time; // DEAL_TIME char type; // DEAL_TYPE char entry; // DEAL_ENTRY string symbol; // DEAL_SYMBOL double volume; // DEAL_VOLUME double price; // DEAL_PRICE double profit; // DEAL_PROFIT double swap; // DEAL_SWAP double commission; // DEAL_COMMISSION long magic; // DEAL_MAGIC char reason; // DEAL_REASON }; //+------------------------------------------------------------------+ //| Структура для хранения трейда: | //| порядок членов соответствует позиции в терминале | //+------------------------------------------------------------------+ struct STrade { long account; // номер счёта datetime time_in; // время входа ulong ticket; // ID позиции char type; // покупка или продажа double volume; // объем string symbol; // символ double price_in; // цена входа datetime time_out; // время выхода double price_out; // цена выхода double commission; // комиссия за вход и выход double swap; // своп double profit; // прибыль или убыток }; //+------------------------------------------------------------------+ //| Структура для хранения статистики по символу | //+------------------------------------------------------------------+ struct SSymbolStats { string name; // имя символа int trades; // количество трейдов по символу double gross_profit; // общая прибыль по символу double gross_loss; // общий убыток по символу double total_commission; // сумма комиссий по символу double total_swap; // сумма свопов по символу double total_profit; // общая прибыль без учета свопов и комиссий double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов long long_trades; // покупки long short_trades; // продажи double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор }; //+------------------------------------------------------------------+ //| Структура для хранения статистики по Magic Number | //+------------------------------------------------------------------+ struct SMagicStats { long magic; // Magic Number советника int trades; // количество трейдов по символу double gross_profit; // общая прибыль по символу double gross_loss; // общий убыток по символу double total_commission; // сумма комиссий по символу double total_swap; // сумма свопов по символу double total_profit; // общая прибыль без учета свопов и комиссий double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов long long_trades; // покупки long short_trades; // продажи double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор }; //+------------------------------------------------------------------+ //| Структура для хранения статистики по счёту | //+------------------------------------------------------------------+ struct SAccountStats { long account; // номер счёта int trades; // количество трейдов по символу double gross_profit; // общая прибыль по символу double gross_loss; // общий убыток по символу double total_commission; // сумма комиссий по символу double total_swap; // сумма свопов по символу double total_profit; // общая прибыль без учета свопов и комиссий double net_profit; // чистая прибыль с учетом свопов и комиссий int win_trades; // количество прибыльных трейдов int loss_trades; // количество убыточных трейдов long long_trades; // покупки long short_trades; // продажи double expected_payoff; // матожидание трейда без учета свопов и комиссии double win_percent; // процент выигрышных трейдов double loss_percent; // процент проигрышных трейдов double average_profit; // средняя прибыль double average_loss; // средний убыток double profit_factor; // профит-фактор };
Структуры скопированы из показанных выше примеров в Документации, но им даны иные наименования и добавлены поля с номером счёта и с количеством длинных и коротких позиций, по которым можно будет делать выборку из БД.
История сделок нам нужна для создания таблицы сделок в БД. Поэтому и функцию для получения истории сделок напишем в этом же файле:
//+------------------------------------------------------------------+ //| Запрашивает историю сделок за указанный период | //+------------------------------------------------------------------+ bool GetHistoryDeals(const datetime from_date, const datetime to_date) { ResetLastError(); if(HistorySelect(from_date, to_date)) return true; Print("HistorySelect() failed. Error ", GetLastError()); return false; }
Сюда же впишем функцию для удаляения из базы данных таблицы с указанным именем:
//+------------------------------------------------------------------+ //| Удаляет из базы таблицу с указанным именем | //+------------------------------------------------------------------+ bool DeleteTable(int database, string table_name) { ResetLastError(); if(!DatabaseExecute(database, "DROP TABLE IF EXISTS "+table_name)) { Print("Failed to drop the DEALS table with code ", GetLastError()); return(false); } //--- таблица успешно удалена return(true); }
Напишем функцию для создания таблицы сделок:
//+------------------------------------------------------------------+ //| Создает таблицу DEALS | //+------------------------------------------------------------------+ bool CreateTableDeals(int database) { //--- если таблица DEALS уже есть, удалим её if(!DeleteTable(database, "DEALS")) return(false); //--- проверим наличие таблицы ResetLastError(); if(!DatabaseTableExists(database, "DEALS")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE DEALS(" "ID INT KEY NOT NULL," "ACCOUNT INT NOT NULL," "ORDER_ID INT NOT NULL," "POSITION_ID INT NOT NULL," "TIME INT NOT NULL," "TYPE INT NOT NULL," "ENTRY INT NOT NULL," "SYMBOL CHAR(10)," "VOLUME REAL," "PRICE REAL," "PROFIT REAL," "SWAP REAL," "COMMISSION REAL," "MAGIC INT," "REASON INT );")) { Print("DB: create the DEALS table failed with code ", GetLastError()); return(false); } //--- таблица успешно создана return(true); }
Код функции скопирован из справки, но здесь добавлено ещё одно поле — номер счёта.
По аналогии напишем функцию для создания таблицы трейдов, где тоже добавлено поле с номером счёта:
//+------------------------------------------------------------------+ //| Создает таблицу TRADES | //+------------------------------------------------------------------+ bool CreateTableTrades(int database) { //--- если таблица TRADES уже есть, удалим её if(!DeleteTable(database, "TRADES")) return(false); //--- проверим наличие таблицы ResetLastError(); if(!DatabaseTableExists(database, "TRADES")) //--- создаем таблицу if(!DatabaseExecute(database, "CREATE TABLE TRADES(" "ACCOUNT INT NOT NULL," "TIME_IN INT NOT NULL," "TICKET INT NOT NULL," "TYPE INT NOT NULL," "VOLUME REAL," "SYMBOL CHAR(10)," "PRICE_IN REAL," "TIME_OUT INT NOT NULL," "PRICE_OUT REAL," "COMMISSION REAL," "SWAP REAL," "PROFIT REAL);")) { Print("DB: create the TRADES table failed with code ", GetLastError()); return(false); } //--- таблица успешно создана return(true); }
Напишем функцию для заполнения таблицы БД данными сделок:
//+------------------------------------------------------------------+ //| Вносит сделки в таблицу базы данных | //+------------------------------------------------------------------+ bool InsertDeals(int database) { //--- вспомогательные переменные long account_login=AccountInfoInteger(ACCOUNT_LOGIN); // номер счёта ulong deal_ticket; // тикет сделки long order_ticket; // тикет ордера,по которому была совершена сделка long position_ticket; // ID позиции, к которой относится сделка datetime time; // время совершения сделки long type ; // тип сделки long entry ; // направление сделки string symbol; // по какому символу была сделка double volume; // объем операции double price; // цена double profit; // финансовый результат double swap; // своп double commission; // комиссия long magic; // Magic number (ID советника) long reason; // причина или источник проведения сделки //--- пройдем по всем сделкам и внесем их в базу данных bool failed=false; int deals=HistoryDealsTotal(); //--- заблокируем базу данных перед выполнением транзакций DatabaseTransactionBegin(database); ResetLastError(); for(int i=0; i<deals; i++) { deal_ticket= HistoryDealGetTicket(i); order_ticket= HistoryDealGetInteger(deal_ticket, DEAL_ORDER); position_ticket=HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); time= (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); type= HistoryDealGetInteger(deal_ticket, DEAL_TYPE); entry= HistoryDealGetInteger(deal_ticket, DEAL_ENTRY); symbol= HistoryDealGetString(deal_ticket, DEAL_SYMBOL); volume= HistoryDealGetDouble(deal_ticket, DEAL_VOLUME); price= HistoryDealGetDouble(deal_ticket, DEAL_PRICE); profit= HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); swap= HistoryDealGetDouble(deal_ticket, DEAL_SWAP); commission= HistoryDealGetDouble(deal_ticket, DEAL_COMMISSION); magic= HistoryDealGetInteger(deal_ticket, DEAL_MAGIC); reason= HistoryDealGetInteger(deal_ticket, DEAL_REASON); //--- внесем в таблицу каждую сделку через запрос string request_text=StringFormat("INSERT INTO DEALS (ID,ACCOUNT,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON)" "VALUES (%I64d, %I64d, %I64d, %I64d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %I64d, %d)", deal_ticket, account_login, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason); if(!DatabaseExecute(database, request_text)) { PrintFormat("%s: failed to insert deal #%d with code %d", __FUNCTION__, deal_ticket, GetLastError()); PrintFormat("i=%d: deal #%d %s", i, deal_ticket, symbol); failed=true; break; } } //--- проверим на наличие ошибок при выполнении транзакций if(failed) { //--- откатим все транзакции и разблокируем базу данных DatabaseTransactionRollback(database); PrintFormat("%s: DatabaseExecute() failed with code %d", __FUNCTION__, GetLastError()); return(false); } //--- все транзакции прошли успешно - зафиксируем изменения и разблокируем базу данных DatabaseTransactionCommit(database); return(true); }
Код функции взят из примера в справке по функции DatabaseExecute(). Здесь дополнительно создана переменная для хранения номера счёта, и исправлен текст запроса, так как в справке скорее всего есть ошибка: при составлении текста строки запроса указаны типы данных как int для данных, имеющих тип long:
//--- внесем в таблицу каждую сделку через запрос string request_text=StringFormat("INSERT INTO DEALS (ID,ORDER_ID,POSITION_ID,TIME,TYPE,ENTRY,SYMBOL,VOLUME,PRICE,PROFIT,SWAP,COMMISSION,MAGIC,REASON,HOUR)" "VALUES (%d, %d, %d, %d, %d, %d, '%s', %G, %G, %G, %G, %G, %d, %d,%d)", deal_ticket, order_ticket, position_ticket, time, type, entry, symbol, volume, price, profit, swap, commission, magic, reason, hour);
Здесь мы это исправили, добавив номер счёта и удалив час входа в сделку, так как часы входа в сделку нам здесь не нужны.
Напишем функцию, для заполнения таблицы трейдов на основе таблицы сделок:
//+------------------------------------------------------------------+ //| Заполняет таблицу TRADES на основе таблицы DEALS | //+------------------------------------------------------------------+ bool FillTRADEStableBasedOnDEALStable(int database) { if(!DatabaseTableExists(database, "DEALS")) { PrintFormat("%s: Error. DEALS table is missing in the database", __FUNCTION__); return false; } //--- заполним таблицу TRADES if(!DatabaseExecute(database, "INSERT INTO TRADES(TIME_IN,ACCOUNT,TICKET,TYPE,VOLUME,SYMBOL,PRICE_IN,TIME_OUT,PRICE_OUT,COMMISSION,SWAP,PROFIT) " "SELECT " " d1.time as time_in," " d1.account as account," " d1.position_id as ticket," " d1.type as type," " d1.volume as volume," " d1.symbol as symbol," " d1.price as price_in," " d2.time as time_out," " d2.price as price_out," " d1.commission+d2.commission as commission," " d2.swap as swap," " d2.profit as profit " "FROM DEALS d1 " "INNER JOIN DEALS d2 ON d1.position_id=d2.position_id " "WHERE d1.entry=0 AND d2.entry=1")) { Print("DB: fillng the TRADES table failed with code ", GetLastError()); return false; } return true; }
Точно так же, как и в функциях выше, здесь добавлен номер счёта.
Напишем функцию, заполняющую из базы данных список всех трейдов:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех трейдов | //+------------------------------------------------------------------+ bool FillsListTradesFromDB(int database, string db_name, STrade &array[]) { STrade trade; ResetLastError(); //--- Запросим из БД список трейдов, отсортированный по убыванию времени входа в рынок int request=DatabasePrepare(database, "SELECT * FROM TRADES ORDER BY time_in DESC"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- Прочитаем в массив структур данные созданной таблицы трейдов for(int i=0; DatabaseReadBind(request, trade); i++) { ArrayResize(array, i+1); array[i].account=trade.account; array[i].time_in=trade.time_in; array[i].ticket=trade.ticket; array[i].type=trade.type; array[i].volume=trade.volume; array[i].symbol=trade.symbol; array[i].price_in=trade.price_in; array[i].time_out=trade.time_out; array[i].price_out=trade.price_out; array[i].commission=trade.commission; array[i].swap=trade.swap; array[i].profit=trade.profit; } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Здесь мы добавили сортировку списка трейдов по убыванию времени входа в рынок. Если этого не сделать, то в списке последний трейд будет в конце, соответственно и в отображаемой на панели таблице он будет в самом низу. Это неудобно. Сортировка по убыванию переместит последний трейд в самое начало таблицы — вверх на панели, и последние трейды будут сразу видимы без длительной прокрутки списка трейдов до них.
Напишем функцию, заполняющую из базы данных список всех символов, на которых велась торговля:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех символов | //+------------------------------------------------------------------+ bool FillsListSymbolsFromDB(int database, string db_name, string &array[]) { //--- Проверим наличие созданной таблицы трейдов в базе данных ResetLastError(); if(!DatabaseTableExists(database, "TRADES")) { //--- Если таблица ещё не создана - сообщим о том, как её создать if(GetLastError()==5126) Alert("First you need to get the trade history.\nClick the \"Get trade history\" button."); else Print("DatabaseTableExists() failed. Error ",GetLastError()); return false; } //--- запросим из БД список всех символов, на которых осуществлялась торговля. Список отсортирован по алфавиту int request=DatabasePrepare(database, "SELECT DISTINCT symbol FROM TRADES ORDER BY symbol ASC"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- Прочитаем в массив данные созданной таблицы символов for(int i=0; DatabaseRead(request); i++) { ArrayResize(array, i+1); DatabaseColumnText(request, 0, array[i]); } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Для того, чтобы получить список, в котором будут лежать только уникальные неповторяющиеся наименования символов, нужно использовать ключевое слово "DISTINCT", ну и получать список нам необходимо в алфавитном порядке.
По аналогии напишем функцию, заполняющую из базы данных список всех магиков в порядке возрастания:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех магиков | //+------------------------------------------------------------------+ bool FillsListMagicsFromDB(int database, string db_name, long &array[]) { //--- Проверим наличие созданной таблицы трейдов в базе данных ResetLastError(); if(!DatabaseTableExists(database, "DEALS")) { //--- Если таблица ещё не создана - сообщим о том, как её создать if(GetLastError()==5126) Alert("First you need to get the trade history.\nClick the \"Get trade history\" button."); else Print("DatabaseTableExists() failed. Error ",GetLastError()); return false; } //--- запросим из БД список всех магиков, на которых осуществлялась торговля. Список отсортирован по возрастанию int request=DatabasePrepare(database, "SELECT DISTINCT magic FROM DEALS ORDER BY magic ASC"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- Прочитаем в массив данные созданной таблицы магиков for(int i=0; DatabaseRead(request); i++) { ArrayResize(array, i+1); DatabaseColumnLong(request, 0, array[i]); } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Создадим функцию, которая получает из базы данных и сохраняет в массив статистику торговли по символам:
//+-------------------------------------------------------------------+ //|Получает из БД и сохраняет в массив статистику торговли по символам| //+-------------------------------------------------------------------+ bool GetTradingStatsBySymbols(int database, string db_name, SSymbolStats &array[]) { int request=DatabasePrepare(database, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor, " " r.long_trades as long_trades," " r.short_trades as short_trades " "FROM " " (" " SELECT SYMBOL," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades, " " sum(case when type = 0 AND entry = 0 then 1 else 0 end) as long_trades, " " sum(case when type = 1 AND entry = 0 then 1 else 0 end) as short_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY SYMBOL ORDER BY net_profit DESC" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- получаем записи из результатов запроса SSymbolStats symbol_stats; for(int i=0; DatabaseReadBind(request, symbol_stats) ; i++) { ArrayResize(array, i+1); array[i].name=symbol_stats.name; array[i].trades=symbol_stats.trades; array[i].long_trades=symbol_stats.long_trades; array[i].short_trades=symbol_stats.short_trades; array[i].gross_profit=symbol_stats.gross_profit; array[i].gross_loss=symbol_stats.gross_loss; array[i].total_commission=symbol_stats.total_commission; array[i].total_swap=symbol_stats.total_swap; array[i].total_profit=symbol_stats.total_profit; array[i].net_profit=symbol_stats.net_profit; array[i].win_trades=symbol_stats.win_trades; array[i].loss_trades=symbol_stats.loss_trades; array[i].expected_payoff=symbol_stats.expected_payoff; array[i].win_percent=symbol_stats.win_percent; array[i].loss_percent=symbol_stats.loss_percent; array[i].average_profit=symbol_stats.average_profit; array[i].average_loss=symbol_stats.average_loss; array[i].profit_factor=symbol_stats.profit_factor; } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Здесь добавлены строки для учёта длинных и коротких позиций, и получаем список, отсортированный по убыванию чистой прибыли — чтобы символы, по которым была получена большая прибыль, находились в начале таблицы.
Аналогично напишем функцию для получения из базы данных и сохранения в массив статистики торговли по магикам:
//+------------------------------------------------------------------+ //|Получает из БД и сохраняет в массив статистику торговли по магикам| //+------------------------------------------------------------------+ bool GetTradingStatsByMagics(int database, string db_name, SMagicStats &array[]) { int request=DatabasePrepare(database, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor, " " r.long_trades as long_trades," " r.short_trades as short_trades " "FROM " " (" " SELECT MAGIC," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades, " " sum(case when type = 0 AND entry = 0 then 1 else 0 end) as long_trades, " " sum(case when type = 1 AND entry = 0 then 1 else 0 end) as short_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY MAGIC ORDER BY net_profit DESC" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- получаем записи из результатов запроса SMagicStats magic_stats; for(int i=0; DatabaseReadBind(request, magic_stats) ; i++) { ArrayResize(array, i+1); array[i].magic=magic_stats.magic; array[i].trades=magic_stats.trades; array[i].long_trades=magic_stats.long_trades; array[i].short_trades=magic_stats.short_trades; array[i].gross_profit=magic_stats.gross_profit; array[i].gross_loss=magic_stats.gross_loss; array[i].total_commission=magic_stats.total_commission; array[i].total_swap=magic_stats.total_swap; array[i].total_profit=magic_stats.total_profit; array[i].net_profit=magic_stats.net_profit; array[i].win_trades=magic_stats.win_trades; array[i].loss_trades=magic_stats.loss_trades; array[i].expected_payoff=magic_stats.expected_payoff; array[i].win_percent=magic_stats.win_percent; array[i].loss_percent=magic_stats.loss_percent; array[i].average_profit=magic_stats.average_profit; array[i].average_loss=magic_stats.average_loss; array[i].profit_factor=magic_stats.profit_factor; } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Наконец, напишем аналогичную функцию, получающую из базы данных и сохраняющую в массив статистику торговли по счёту:
//+------------------------------------------------------------------+ //| Получает из БД и сохраняет в массив статистику торговли по счёту | //+------------------------------------------------------------------+ bool GetTradingStatsByAccount(int database, string db_name, SAccountStats &array[]) { int request=DatabasePrepare(database, "SELECT r.*," " (case when r.trades != 0 then (r.gross_profit+r.gross_loss)/r.trades else 0 end) as expected_payoff," " (case when r.trades != 0 then r.win_trades*100.0/r.trades else 0 end) as win_percent," " (case when r.trades != 0 then r.loss_trades*100.0/r.trades else 0 end) as loss_percent," " r.gross_profit/r.win_trades as average_profit," " r.gross_loss/r.loss_trades as average_loss," " (case when r.gross_loss!=0.0 then r.gross_profit/(-r.gross_loss) else 0 end) as profit_factor, " " r.long_trades as long_trades," " r.short_trades as short_trades " "FROM " " (" " SELECT ACCOUNT," " sum(case when entry =1 then 1 else 0 end) as trades," " sum(case when profit > 0 then profit else 0 end) as gross_profit," " sum(case when profit < 0 then profit else 0 end) as gross_loss," " sum(swap) as total_swap," " sum(commission) as total_commission," " sum(profit) as total_profit," " sum(profit+swap+commission) as net_profit," " sum(case when profit > 0 then 1 else 0 end) as win_trades," " sum(case when profit < 0 then 1 else 0 end) as loss_trades, " " sum(case when type = 0 AND entry = 0 then 1 else 0 end) as long_trades, " " sum(case when type = 1 AND entry = 0 then 1 else 0 end) as short_trades " " FROM DEALS " " WHERE SYMBOL <> '' and SYMBOL is not NULL " " GROUP BY ACCOUNT ORDER BY net_profit DESC" " ) as r"); if(request==INVALID_HANDLE) { Print("DB: ", db_name, " request failed with code ", GetLastError()); DatabaseClose(database); return false; } //--- получаем записи из результатов запроса SAccountStats account_stats; for(int i=0; DatabaseReadBind(request, account_stats) ; i++) { ArrayResize(array, i+1); array[i].account=account_stats.account; array[i].trades=account_stats.trades; array[i].long_trades=account_stats.long_trades; array[i].short_trades=account_stats.short_trades; array[i].gross_profit=account_stats.gross_profit; array[i].gross_loss=account_stats.gross_loss; array[i].total_commission=account_stats.total_commission; array[i].total_swap=account_stats.total_swap; array[i].total_profit=account_stats.total_profit; array[i].net_profit=account_stats.net_profit; array[i].win_trades=account_stats.win_trades; array[i].loss_trades=account_stats.loss_trades; array[i].expected_payoff=account_stats.expected_payoff; array[i].win_percent=account_stats.win_percent; array[i].loss_percent=account_stats.loss_percent; array[i].average_profit=account_stats.average_profit; array[i].average_loss=account_stats.average_loss; array[i].profit_factor=account_stats.profit_factor; } //--- удалим запрос после использования DatabaseFinalize(request); return true; }
Всё, мы подготовили базовые вещи для создания проекта — выбрали панель и создали функции для работы с базой данных по информации из документации. Нам осталось сделать только логику взаимодействия панели и её таблиц с базой данных посредством написанных функций. Вполне вероятно, что списки, размещаемые в таблицах на панели, будут достаточно обширными, и размеры таблиц будут выходить за размеры панели. В этом случае нам нужно будет прокручивать таблицы по вертикали и горизонтали. Такой функционал сделаем прямо в создаваемом индикаторе — таблицы будут прокручиваться по вертикали вращением колёсика мышки, а по горизонтали — колёсиком мышки с зажатой клавишей Shift.
Статистика по выбранному символу или магику будет выводиться при щелчке по строке статистики нужного символа или магика. Для этого сделаем отслеживание расположения курсора над строками таблицы и щелчка по строке. Правильно делать такой функционал в классе панели — чтобы он мог использоваться в иных проектах. Но здесь покажем, как можно сделать всё то же самое без доработки классов панели.
Собираем проект — информационную панель
В ранее созданной папке \MQL5\Indicators\StatisticsBy\ создадим файл нового индикатора с именем StatisticsBy.mq5.
Подключим файлы классов таблиц и панели, и файл функций для работы с БД, и укажем, что у индикатора нет рисуемых буферов:
//+------------------------------------------------------------------+ //| StatisticsBy.mq5 | //| Copyright 2024, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_buffers 0 #property indicator_plots 0 #include "Dashboard\Dashboard.mqh" #include "SQLiteFunc.mqh"
Далее впишем макроподстановки, массив расположения столбцов таблиц статистики, входные параметры и глобальные переменные:
#property copyright "Copyright 2024, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property indicator_chart_window #property indicator_buffers 0 #property indicator_plots 0 #include "Dashboard\Dashboard.mqh" #include "SQLiteFunc.mqh" #define PROGRAM_NAME (MQLInfoString(MQL_PROGRAM_NAME)) // Имя программы #define DB_NAME (PROGRAM_NAME+"_DB.sqlite") // Имя базы данных #define DATE_FROM 0 // Дата начала истории сделок #define DATE_TO (TimeCurrent()) // Дата окончания истории сделок //--- Ширина ячеек таблиц #define CELL_W_TRADES 94 // Ширина ячеек таблицы истории торговли #define CELL_W_SYMBOLS 62 // Ширина ячеек таблицы символов, используемых в торговле #define CELL_W_MAGICS 62 // Ширина ячеек таблицы магиков, используемых в торговле #define CELL_H 16 // Высота ячейки таблицы //--- Размеры итоговой таблицы статистики #define TABLE_STAT_ROWS 9 // Количество строк итоговой таблицы статистики #define TABLE_STAT_COLS 4 // Количество столбцов итоговой таблицы статистики //--- Таблицы #define TABLE_TRADES 1 // Идентификатор таблицы истории торговли #define TABLE_SYMBOLS 2 // Идентификатор таблицы символов, используемых в торговле #define TABLE_MAGICS 3 // Идентификатор таблицы магиков, используемых в торговле #define TABLE_ACCOUNT 4 // Идентификатор таблицы статистики счёта #define TABLE_STATS 5 // Идентификатор итоговой таблицы статистики выбранного символа или магика //--- Заголовки таблиц (полный/сокращённый) #define H_TRADES "Trades" #define H_TRADES_S "Trades" #define H_LONG "Long" #define H_LONG_S "Long" #define H_SHORT "Short" #define H_SHORT_S "Short" #define H_GROSS_PROFIT "Gross Profit" #define H_GROSS_PROFIT_S "Gross Profit" #define H_GROSS_LOSS "Gross Loss" #define H_GROSS_LOSS_S "Gross Loss" #define H_COMMISSIONS "Commission total" #define H_COMMISSIONS_S "Fees" #define H_SWAPS "Swap total" #define H_SWAPS_S "Swaps" #define H_PROFITS "Profit Loss" #define H_PROFITS_S "P/L" #define H_NET_PROFIT "Net Profit" #define H_NET_PROFIT_S "Net Profit" #define H_WINS "Win trades" #define H_WINS_S "Win" #define H_LOST "Loss trades" #define H_LOST_S "Lost" #define H_EXP_PAYOFF "Expected Payoff" #define H_EXP_PAYOFF_S "Avg $" #define H_WIN_PRC "Win percent" #define H_WIN_PRC_S "Win %" #define H_LOSS_PRC "Loss percent" #define H_LOSS_PRC_S "Loss %" #define H_AVG_PROFIT "Average Profit" #define H_AVG_PROFIT_S "Avg Profit" #define H_AVG_LOSS "Average Loss" #define H_AVG_LOSS_S "Avg Loss" #define H_PRF_FACTOR "Profit factor" #define H_PRF_FACTOR_S "PF" //--- Массив расположения столбцов таблиц статистики слева направо string ArrayDataName[18]= { "HEADER", H_NET_PROFIT_S, H_TRADES_S, H_GROSS_PROFIT_S, H_GROSS_LOSS_S, H_COMMISSIONS_S, H_SWAPS_S, H_PROFITS_S, H_LONG_S, H_SHORT_S, H_WINS_S, H_LOST_S, H_EXP_PAYOFF_S, H_WIN_PRC_S, H_LOSS_PRC_S, H_AVG_PROFIT_S, H_AVG_LOSS_S, H_PRF_FACTOR_S, }; //--- input parameters input int InpPanelX = 20; /* Dashboard X */ // Координата X панели input int InpPanelY = 20; /* Dashboard Y */ // Координата Y панели input int InpUniqID = 0; /* Unique ID */ // Уникальный идентификатор для объекта-панели //--- global variables int DBHandle; // Хэндл базы данных int LPanelTable; // Активная панель в левом поле int RPanelTable; // Активная панель в правом поле long ArrayMagics[]; // Массив магиков string ArraySymbols[]; // Массив символов STrade ArrayTrades[]; // Массив трейдов SSymbolStats ArraySymbolStats[]; // Массив статистики по символам SMagicStats ArrayMagicStats[]; // Массив статистики по магикам SAccountStats ArrayAccountStats[]; // Массив статистики по счёту CDashboard *dashboard=NULL; // Указатель на экземпляр панели
Для указания количества столбцов таблиц статистики и указания количества и расположения данных в таблице, удобно использовать массив, в котором расположены константы наименований заголовков таблицы и, соответственно, данных под этими заголовками. При необходимости изменения порядка следования различных данных в таблице, достаточно поменять порядок их объявления в этом массиве и перекомпилировать индикатор. Так же можно убрать ненужные данные, закомментировав их в этом массиве, либо добавить новые. Но при добавлении новых данных, нужно будет добавлять их в функции работы с базой данных и в иных функциях, где рассчитываются и отображаются данные таблиц.
Рассмотрим обработчик OnInit() индикатора, где создаётся база данных и панель с её графическим наполнением:
//+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Проверим, что на счете используется хеджинг для учета открытых позиций if((ENUM_ACCOUNT_MARGIN_MODE)AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING) { //--- Если счёт неттинговый, то мы не можем преобразовать сделки в трейды простым способом через транзакции, поэтому завершаем работу Print("For a Netting account, there is no way to convert deals into trades in a simple way."); return INIT_FAILED; } //--- Указываем путь расположения и создаём БД (\MQL5\Files\StatisticsBy\Database\) string path=PROGRAM_NAME+"\\Database\\"; DBHandle=DatabaseOpen(path+DB_NAME,DATABASE_OPEN_CREATE); if(DBHandle==INVALID_HANDLE) { Print("DatabaseOpen() failed. Error ", GetLastError()); return(INIT_FAILED); } PrintFormat("Database \"%s\" successfully created at MQL5\\Files\\%s", DB_NAME, path); //--- Создаём панель-окно программы dashboard = new CDashboard(InpUniqID, InpPanelX, InpPanelY, 601, 300); if(dashboard==NULL) { Print("Error. Failed to create dashboard object"); return INIT_FAILED; } //--- Отображаем панель с текстом названия программы в заголовке окна dashboard.SetFontParams("Calibri",8); dashboard.SetName("Main"); dashboard.View(PROGRAM_NAME); //--- Рисуем рабочее пространство //--- Кнопка для выбора символов CDashboard *panel1=dashboard.InsertNewPanel(dashboard.ID()+1, 3, 20, 49, 21); if(panel1!=NULL) { panel1.SetName("SymbolButton"); panel1.SetButtonCloseOff(); panel1.SetButtonMinimizeOff(); panel1.SetButtonPinOff(); panel1.View(""); panel1.Collapse(); panel1.SetHeaderNewParams("Symbol",clrLightGray,clrBlack,USHORT_MAX,5,-1); } //--- Кнопка для выбора магиков CDashboard *panel2=dashboard.InsertNewPanel(dashboard.ID()+2, 54, 20, 48, 21); if(panel2!=NULL) { panel2.SetName("MagicButton"); panel2.SetButtonCloseOff(); panel2.SetButtonMinimizeOff(); panel2.SetButtonPinOff(); panel2.View(""); panel2.Collapse(); panel2.SetHeaderNewParams("Magic",clrLightGray,clrBlack,USHORT_MAX,8,-1); } //--- Кнопка для создания списка трейдов CDashboard *panel3=dashboard.InsertNewPanel(dashboard.ID()+3, 105, 20, 106, 21); if(panel3!=NULL) { panel3.SetName("TradesButton"); panel3.SetButtonCloseOff(); panel3.SetButtonMinimizeOff(); panel3.SetButtonPinOff(); panel3.View(""); panel3.Collapse(); panel3.SetHeaderNewParams("Get trade history",clrLightGray,clrBlack,USHORT_MAX,10,-1); } //--- Панель слева для вывода таблицы символов/магиков CDashboard *panel4=dashboard.InsertNewPanel(dashboard.ID()+4, 2, 38, 101, dashboard.Height()-38-2); if(panel4!=NULL) { panel4.SetName("FieldL"); panel4.SetButtonCloseOff(); panel4.SetButtonMinimizeOff(); panel4.SetButtonPinOff(); panel4.View(""); panel4.SetPanelHeaderOff(true); panel4.SetFontParams("Calibri",8); } //--- Панель справа для вывода заголовков статистики по списку трейдов и выбранному символу/магику CDashboard *panel5=dashboard.InsertNewPanel(dashboard.ID()+5, 104, 38, dashboard.Width()-104-2, 20); if(panel5!=NULL) { panel5.SetName("FieldH"); panel5.SetButtonCloseOff(); panel5.SetButtonMinimizeOff(); panel5.SetButtonPinOff(); panel5.View(""); panel5.SetPanelHeaderOff(true); panel5.SetFontParams("Calibri",8,FW_EXTRABOLD); } //--- Панель справа для вывода статистики по списку трейдов и выбранному символу/магику CDashboard *panel6=dashboard.InsertNewPanel(dashboard.ID()+6, 104, 38+20, dashboard.Width()-104-2, dashboard.Height()-38-20-2); if(panel5!=NULL) { panel6.SetName("FieldR"); panel6.SetButtonCloseOff(); panel6.SetButtonMinimizeOff(); panel6.SetButtonPinOff(); panel6.View(""); panel6.SetPanelHeaderOff(true); panel6.SetFontParams("Calibri",8); } //--- Все таблицы на левой и правой панелях изначально не активны LPanelTable=WRONG_VALUE; RPanelTable=WRONG_VALUE; //--- Всё успешно return(INIT_SUCCEEDED); }
По сути, здесь сначала проверяется тип учёта позиций, и если это неттинг, то индикатор завершает работу, так как для такого учёта позиций невозможно простым способом, только по сделкам входа-выхода, создать таблицу трейдов.
Далее в каталоге данных терминала (TERMINAL_DATA_PATH + \MQL5\Files\) создаётся база данных в папке с наименованием программы в подкаталоге Database (\StatisticsBy\Database\). После успешного создания базы данных, создаётся панель и наполняется содержимым — кнопками управления и панелями для вывода таблиц:
Что интересно, здесь мы вместо кнопок используем прикреплённые к основному окну дочерние панели в свёрнутом виде — от них виден только заголовок панели. У него есть свои обработчики для взаимодействия с курсором мышки, и таким образом, мы из обычных панелей создали кнопки, которые интерактивно взаимодействуют с пользователем и отправляют в основную программу события взаимодействия с мышью.
В обработчике OnDeinit() закрываем базу данных и панель:
//+------------------------------------------------------------------+ //| Custom indicator deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { //--- Закрываем базу данных DatabaseClose(DBHandle); if(GetLastError()==ERR_DATABASE_INVALID_HANDLE) Print("Error. An invalid database handle was passed to the DatabaseClose() function"); //--- Если объект панели существует - удаляем if(dashboard!=NULL) { delete dashboard; ChartRedraw(); } }
Обработчик OnCalculate() индикатора оставим пустым (индикатор ничего не рассчитывает):
//+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { //--- //--- return value of prev_calculated for next call return(rates_total); }
Вся работа по интерактивному взаимодействию панели с пользователем производится в обработчике событий индикатора.
Рассмотрим обработчик событий OnChartEvent() целиком. Его код подробно прокомментирован. При внимательном изучении комментариев обработчика событий, вся логика взаимодействия панели с пользователем будет понятна:
//+------------------------------------------------------------------+ //| ChartEvent function | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { //--- Идентификатор активной панели int table_id=WRONG_VALUE; //--- Вызываем обработчик событий панели, которая в свою очередь отправляет сюда свои события dashboard.OnChartEvent(id,lparam,dparam,sparam); //--- Если получили пользовательское событие от панели-окна программы if(id>CHARTEVENT_CUSTOM) { //--- Нажата кнопка закрытия на панели if(id==1001) { //--- Здесь может быть обработка щелчка по кнопке закрытия } //--- Нажатия кнопок работы с БД - идентификатор всегда 1002, а уточнение - по значению в lparam if(id==1002) { //--- получение истории сделок с сервера и трейдов из БД (нажата кнопка Get trade history) if(lparam==3) { //--- Если история сделок не получена - уходим if(!GetHistoryDeals(DATE_FROM, DATE_TO)) return; //--- Если в истории нет сделок - сообщаем об этом и уходим int deals_total=HistoryDealsTotal(); if(deals_total==0) { Print("No deals in history"); return; } //--- создадим таблицу сделок (DEALS) в базе данных if(!CreateTableDeals(DBHandle)) return; //--- внесем в созданную таблицу сделки if(!InsertDeals(DBHandle)) return; //--- Создадим таблицу трейдов (TRADES) на основе таблицы DEALS if(!CreateTableTrades(DBHandle)) return; //--- Заполним через SQL-запрос таблицу TRADES на основе данных из таблицы DEALS if(!FillTRADEStableBasedOnDEALStable(DBHandle)) return; //--- Запросим из БД список всех трейдов if(!FillsListTradesFromDB(DBHandle, DB_NAME, ArrayTrades)) return; //--- Выведем на панель количество сделок и количество трейдов в истории dashboard.DrawText(" ",2,2,clrNONE,0,0); // стираем ранее написанное dashboard.DrawText("Total deals in history: "+(string)deals_total+", trades: "+(string)ArrayTrades.Size(),216,3); //--- Получаем указатель на панель заголовков CDashboard *panel_h=dashboard.GetPanel("FieldH"); if(panel_h==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода заголовка таблицы трейдов CTableData *table_h=NULL; if(!panel_h.TableIsExist(TABLE_TRADES) && !panel_h.CreateNewTable(TABLE_TRADES)) return; //--- Получаем указатель на объект-таблицу заголовков трейдов table_h=panel_h.GetTable(TABLE_TRADES); if(table_h==NULL) return; //--- Очищаем панель заголовка таблицы и выводим на неё таблицу заголовков panel_h.Clear(); panel_h.DrawGrid(TABLE_TRADES,2,2,1,11,CELL_H,CELL_W_TRADES,C'200,200,200',false); //--- Заполняем таблицу заголовков трейдов FillsHeaderTradeTable(panel_h,table_h); //--- Получаем указатель на правую панель CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода трейдов if(!panel_r.TableIsExist(TABLE_TRADES) && !panel_r.CreateNewTable(TABLE_TRADES)) return; //--- Получаем указатель на объект-таблицу трейдов CTableData *table_r=panel_r.GetTable(TABLE_TRADES); if(table_r==NULL) return; //--- Очищаем панель и выводим на неё таблицу трейдов panel_r.Clear(); panel_r.DrawGrid(TABLE_TRADES,2,2,ArrayTrades.Size(),11,CELL_H,CELL_W_TRADES,C'220,220,220'); //--- Заполняем таблицу данными трейдов и указываем, что справа активна таблица TABLE_TRADES FillsTradeTable(panel_r,table_r); RPanelTable=TABLE_TRADES; } //--- Если нажата кнопка отображения символов if(lparam==1) { //--- запросим из БД список всех символов, на которых осуществлялась торговля, и заполним массив символов if(!FillsListSymbolsFromDB(DBHandle, DB_NAME, ArraySymbols)) return; //--- Увеличим массив символов на 1 для записи в него пункта "Все символы" (ALL) int size=(int)ArraySymbols.Size(); if(ArrayResize(ArraySymbols, size+1)==size+1) ArraySymbols[size]="ALL"; //--- Получаем указатель на левую панель CDashboard *panel=dashboard.GetPanel("FieldL"); if(panel==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода списка символов CTableData *table=NULL; if(!panel.TableIsExist(TABLE_SYMBOLS) && !panel.CreateNewTable(TABLE_SYMBOLS)) return; //--- Получаем указатель на объект-таблицу table=panel.GetTable(TABLE_SYMBOLS); if(table==NULL) return; //--- Очищаем панель и рисуем на ней таблицу символов panel.Clear(); panel.DrawGrid(TABLE_SYMBOLS,2,2,ArraySymbols.Size(),1,CELL_H,panel.Width()-5,C'220,220,220'); //--- Заполняем таблицу наименованиями символов и указываем, что на левой панели активна таблица TABLE_SYMBOLS FillsSymbolTable(panel,table); LPanelTable=TABLE_SYMBOLS; //--- получим торговую статистику в разрезе символов if(!GetTradingStatsBySymbols(DBHandle, DB_NAME, ArraySymbolStats)) return; //--- Выведем на панель количество символов, использованных в торговле dashboard.DrawText(" ",2,2,clrNONE,0,0); // Стираем всё содержимое панели dashboard.DrawText("Total number of symbols used in trade: "+(string)ArraySymbols.Size(),216,3); //--- Получаем указатель на панель заголовков CDashboard *panel_h=dashboard.GetPanel("FieldH"); if(panel_h==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода заголовка таблицы статистики символов CTableData *table_h=NULL; if(!panel_h.TableIsExist(TABLE_SYMBOLS) && !panel_h.CreateNewTable(TABLE_SYMBOLS)) return; //--- Получаем указатель на объект-таблицу заголовков статистики символов table_h=panel_h.GetTable(TABLE_SYMBOLS); if(table_h==NULL) return; //--- Очищаем панель заголовка таблицы и выводим на неё таблицу RPanelTable=TABLE_SYMBOLS; panel_h.Clear(); panel_h.DrawGrid(TABLE_SYMBOLS,2,2,1,ArrayDataName.Size(),CELL_H,CELL_W_SYMBOLS,C'200,200,200',false); //--- Заполняем таблицу заголовка статистики символов FillsHeaderTradingStatsTable(panel_h,table_h); //--- Получаем указатель на правую панель CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода статистики символов if(!panel_r.TableIsExist(TABLE_SYMBOLS) && !panel_r.CreateNewTable(TABLE_SYMBOLS)) return; //--- Получаем указатель на объект-таблицу статистики символов CTableData *table_r=panel_r.GetTable(TABLE_SYMBOLS); if(table_r==NULL) return; //--- Очищаем панель и выводим на неё таблицу статистики символов panel_r.Clear(); panel_r.DrawGrid(TABLE_SYMBOLS,2,2,ArraySymbolStats.Size(),ArrayDataName.Size(),CELL_H,CELL_W_SYMBOLS,C'220,220,220'); //--- Заполняем таблицу данными статистики символов и указываем, что справа активна таблица TABLE_SYMBOLS FillsTradingStatsBySymbolsTable(panel_r,table_r); RPanelTable=TABLE_SYMBOLS; } //--- Если нажата кнопка отображения магиков if(lparam==2) { //--- Запросим из БД список всех магиков, на которых осуществлялась торговля, и заполним массив магиков if(!FillsListMagicsFromDB(DBHandle, DB_NAME, ArrayMagics)) return; //--- Увеличим массив магиков на 1 для записи в него пункта "Все магики" (значение LONG_MAX указывает на это) int size=(int)ArrayMagics.Size(); if(ArrayResize(ArrayMagics, size+1)==size+1) ArrayMagics[size]=LONG_MAX; //--- Получаем указатель на левую панель CDashboard *panel=dashboard.GetPanel("FieldL"); if(panel==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода магиков CTableData *table=NULL; if(!panel.TableIsExist(TABLE_MAGICS) && !panel.CreateNewTable(TABLE_MAGICS)) return; //--- Получаем указатель на объект-таблицу table=panel.GetTable(TABLE_MAGICS); if(table==NULL) return; //--- Очищаем панель и рисуем на ней таблицу магиков panel.Clear(); panel.DrawGrid(TABLE_MAGICS,2,2,ArrayMagics.Size(),1,CELL_H,panel.Width()-5,C'220,220,220'); //--- Заполняем таблицу значениями магиков и указываем, что на левой панели активна таблица TABLE_MAGICS FillsMagicTable(panel,table); LPanelTable=TABLE_MAGICS; //--- Получаем торговую статистику в разрезе магиков if(!GetTradingStatsByMagics(DBHandle, DB_NAME, ArrayMagicStats)) return; //--- Выводим на панель количество магиков, использованных в торговле dashboard.DrawText(" ",2,2,clrNONE,0,0); dashboard.DrawText("Total number of magics used in trade: "+(string)ArrayMagics.Size(),216,3); //--- Получаем указатель на панель заголовков CDashboard *panel_h=dashboard.GetPanel("FieldH"); if(panel_h==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода заголовка таблицы статистики магиков CTableData *table_h=NULL; if(!panel_h.TableIsExist(TABLE_MAGICS) && !panel_h.CreateNewTable(TABLE_MAGICS)) return; //--- Получаем указатель на объект-таблицу заголовков статистики магиков table_h=panel_h.GetTable(TABLE_MAGICS); if(table_h==NULL) return; //--- Очищаем панель заголовка таблицы и выводим на неё таблицу panel_h.Clear(); panel_h.DrawGrid(TABLE_MAGICS,2,2,1,ArrayDataName.Size(),CELL_H,CELL_W_MAGICS,C'200,200,200',false); //--- Заполняем таблицу заголовка статистики символов FillsHeaderTradingStatsTable(panel_h,table_h); //--- Получаем указатель на правую панель CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r==NULL) return; //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода статистики магиков if(!panel_r.TableIsExist(TABLE_MAGICS) && !panel_r.CreateNewTable(TABLE_MAGICS)) return; //--- Получаем указатель на объект-таблицу статистики магиков CTableData *table_r=panel_r.GetTable(TABLE_MAGICS); if(table_r==NULL) return; //--- Очищаем панель и выводим на неё таблицу статистики магиков panel_r.Clear(); panel_r.DrawGrid(TABLE_MAGICS,2,2,ArrayMagicStats.Size(),ArrayDataName.Size(),CELL_H,CELL_W_MAGICS,C'220,220,220'); //--- Заполняем таблицу данными статистики магиков и указываем, что активна таблица TABLE_MAGICS FillsTradingStatsByMagicsTable(panel_r,table_r); RPanelTable=TABLE_MAGICS; } } } //--- Если получили событие прокрутки колёсика мышки if(id==CHARTEVENT_MOUSE_WHEEL) { static int index_l_p=WRONG_VALUE; // Прошлый индекс строки под курсором в таблице на левой панели static int index_r_p=WRONG_VALUE; // Прошлый индекс строки под курсором в таблице на правой панели //--- разберем состояние кнопок и колесика мышки для этого события int flg_keys = (int)(lparam>>32); // флаг состояний клавиш Ctrl, Shift и кнопок мышки int x_cursor = (int)(short)lparam; // X-координата, в которой произошло событие колесика мышки int y_cursor = (int)(short)(lparam>>16); // Y-координата, в которой произошло событие колесика мышки int delta = (int)dparam; // суммарное значение прокрутки колесика, срабатывает при достижении +120 или -120 //--- Получаем указатель на левую панель и вызываем для неё обработчик прокрутки колёсика мышки int index_l=WRONG_VALUE; CDashboard *panel_l=dashboard.GetPanel("FieldL"); if(panel_l!=NULL) index_l=TableMouseWhellHandlerL(x_cursor,y_cursor,((flg_keys&0x0004)!=0),delta,panel_l,LPanelTable); //--- Получаем указатель на правую панель и вызываем для неё обработчик прокрутки колёсика мышки int index_r=WRONG_VALUE; CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r!=NULL) index_r=TableMouseWhellHandlerR(x_cursor,y_cursor,((flg_keys&0x0004)!=0),delta,panel_r,RPanelTable); //--- При необходимости можно обработать строку таблицы, над которой находится курсор. //--- Номер строки записан в index_l для левой панели и в index_r - для правой //--- Обновляем график после всех изменений, произошедших в обработчиках прокрутки колёсика мышки if(index_l_p!=index_l) { index_l_p=index_l; ChartRedraw(); } if(index_r_p!=index_r) { index_r_p=index_r; ChartRedraw(); } } //--- Если получили событие перемещения мышки if(id==CHARTEVENT_MOUSE_MOVE) { static int index_l_p=WRONG_VALUE; // Прошлый индекс строки под курсором в таблице на левой панели static int index_r_p=WRONG_VALUE; // Прошлый индекс строки под курсором в таблице на правой панели int x_cursor = (int)lparam; // X-координата курсора мышки int y_cursor = (int)dparam; // Y-координата курсора мышки //--- Получаем указатель на левую панель и вызываем для неё обработчик перемещения мышки int index_l=WRONG_VALUE; CDashboard *panel_l=dashboard.GetPanel("FieldL"); if(panel_l!=NULL) index_l=TableMouseMoveHandlerL(x_cursor,y_cursor,panel_l,LPanelTable); //--- Получаем указатель на правую панель и вызываем для неё обработчик перемещения мышки int index_r=WRONG_VALUE; CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r!=NULL) index_r=TableMouseMoveHandlerR(x_cursor,y_cursor,panel_r,RPanelTable); //--- При необходимости можно обработать строку таблицы, над которой находится курсор. //--- Номер строки записан в index_l для левой панели и в index_r - для правой //--- Обновляем график после всех изменений, произошедших в обработчиках перемещения мышки if(index_l_p!=index_l) { index_l_p=index_l; ChartRedraw(); } if(index_r_p!=index_r) { index_r_p=index_r; ChartRedraw(); } } //--- Если получили событие щелчка мышки if(id==CHARTEVENT_CLICK) { int x_cursor = (int)lparam; // X-координата курсора мышки int y_cursor = (int)dparam; // Y-координата курсора мышки //--- Получаем указатель на левую панель и вызываем обработчик щелчка мышки int index_l=WRONG_VALUE; CDashboard *panel_l=dashboard.GetPanel("FieldL"); if(panel_l!=NULL) index_l=TableMouseClickHandler(x_cursor,y_cursor,panel_l,LPanelTable); //--- Получаем указатель на правую панель и вызываем обработчик щелчка мышки int index_r=WRONG_VALUE; CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_r!=NULL) index_r=TableMouseClickHandler(x_cursor,y_cursor,panel_r,RPanelTable); //--- Обработаем строку таблицы, по которой был щелчок //--- Если в левой панели был щелчок по символу из списка if(LPanelTable==TABLE_SYMBOLS && index_l>WRONG_VALUE) { //--- Получим таблицу символов с левой панели CTableData *table=panel_l.GetTable(TABLE_SYMBOLS); if(table==NULL) return; //--- Получим единственную ячейку таблицы с номером строки index_l CTableCell *cell=table.GetCell(index_l,0); if(cell==NULL) return; //--- Если щелчок по последнему пункту (ALL) if(index_l==ArraySymbols.Size()-1) { //--- получим и отобразим торговую статистику аккаунта, и укажем, что справа активна панель TABLE_STATS if(!GetTradingStatsByAccount(DBHandle, DB_NAME, ArrayAccountStats)) return; if(ViewStatistic(TABLE_ACCOUNT,(string)AccountInfoInteger(ACCOUNT_LOGIN))) RPanelTable=TABLE_STATS; } //--- Щелчок по наименованию символа - отобразим статистику по символу и укажем, что справа активна панель TABLE_STATS else { if(ViewStatistic(TABLE_SYMBOLS,cell.Text())) RPanelTable=TABLE_STATS; } } //--- Если в левой панели был щелчок по магику из списка if(LPanelTable==TABLE_MAGICS && index_l>WRONG_VALUE) { //--- Получим таблицу магиков с левой панели CTableData *table=panel_l.GetTable(TABLE_MAGICS); if(table==NULL) return; //--- Получим единственную ячейку таблицы с номером строки index_l CTableCell *cell=table.GetCell(index_l,0); if(cell==NULL) return; //--- Если щелчок по последнему пункту (ALL) if(index_l==ArrayMagics.Size()-1) { //--- получим и отобразим торговую статистику аккаунта, и укажем, что справа активна панель TABLE_STATS if(!GetTradingStatsByAccount(DBHandle, DB_NAME, ArrayAccountStats)) return; if(ViewStatistic(TABLE_ACCOUNT,(string)AccountInfoInteger(ACCOUNT_LOGIN))) RPanelTable=TABLE_STATS; } //--- Щелчок по значению магика - отобразим статистику по магику и укажем, что справа активна панель TABLE_STATS else { if(ViewStatistic(TABLE_MAGICS,cell.Text())) RPanelTable=TABLE_STATS; } } //--- Если в правой панели был щелчок по символу из списка if(RPanelTable==TABLE_SYMBOLS && index_r>WRONG_VALUE) { //--- Получим таблицу статистики символов с правой панели CTableData *table=panel_r.GetTable(TABLE_SYMBOLS); if(table==NULL) return; //--- Получим нулевую ячейку таблицы с номером строки index_r CTableCell *cell=table.GetCell(index_r,0); if(cell==NULL) return; //--- Отобразим итоговую статистику по символу и укажем, что справа активна панель TABLE_STATS if(ViewStatistic(TABLE_SYMBOLS,cell.Text())) RPanelTable=TABLE_STATS; } //--- Если в правой панели был щелчок по магику из списка if(RPanelTable==TABLE_MAGICS && index_r>WRONG_VALUE) { //--- Получим таблицу статистики магиков с правой панели CTableData *table=panel_r.GetTable(TABLE_MAGICS); if(table==NULL) return; //--- Получим нулевую ячейку таблицы с номером строки index_r CTableCell *cell=table.GetCell(index_r,0); if(cell==NULL) return; //--- Отобразим итоговую статистику по магику и укажем, что справа активна панель TABLE_STATS if(ViewStatistic(TABLE_MAGICS,cell.Text())) RPanelTable=TABLE_STATS; } } }
Теперь рассмотрим остальные функции, которые вызываются из обработчика событий. Код каждой функции подробно прокомментирован, и не должен вызывать непонимания.
Функция, возвращающая индекс строки таблицы по координатам курсора:
//+------------------------------------------------------------------+ //| Возвращает индекс строки таблицы по координатам курсора | //+------------------------------------------------------------------+ int TableSelectRowByMouse(const int x_cursor, const int y_cursor, const int cell_h, CDashboard *panel, CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return WRONG_VALUE; int index=WRONG_VALUE; // Индекс строки таблицы, расположенной под курсором //--- В цикле по строкам таблицы int total=table.RowsTotal(); for(int i=0;i<total;i++) { //--- получаем очередную нулевую ячейку таблицы в строке с индексом цикла CTableCell *cell=table.GetCell(i,0); if(cell==NULL) continue; //--- Рассчитываем верхнюю и нижнюю координаты расположения строки таблицы по координате Y ячейки int y1=panel.CoordY()+cell.Y()+1; int y2=y1+cell_h; //--- Если курсор по вертикали находится внутри рассчитанных координат строки таблицы if(y_cursor>y1 && y_cursor<y2) { //--- Записываем индекс строки, рисуем прямоугольную область на всю ширину таблицы (выделение строки под курсором) и возвращаем индекс строки index=cell.Row(); panel.DrawRectangleFill(2,cell.Y()+1,panel.Width()-4,y2-y1-1,C'220,220,220',240); return index; } } //--- Ничего не нашли return WRONG_VALUE; }
Функция выполняет сразу две задачи: (1) возвращает номер строки таблицы, над которой находится курсор мышки, и (2) подсвечивает эту строку фоновым цветом.
Функция-обработчик прокрутки колёсика мышки внутри таблицы левой панели:
//+------------------------------------------------------------------+ //| Обработчик прокрутки колёсика мышки внутри таблицы левой панели | //+------------------------------------------------------------------+ int TableMouseWhellHandlerL(const int x_cursor,const int y_cursor,const bool shift_flag,const int delta,CDashboard *panel,const int table_id) { //--- Проверяем указатель на левую панель if(panel==NULL) return WRONG_VALUE; //--- Проверяем расположение курсора внутри панели if(x_cursor<panel.CoordX()+2 || x_cursor>panel.CoordX()+panel.Width() || y_cursor<panel.CoordY()+4 || y_cursor>panel.CoordY()+panel.Height()) return WRONG_VALUE; //--- Проверяем наличие таблицы на панели if(!panel.TableIsExist(table_id)) return WRONG_VALUE; //--- Получаем указатель на активную таблицу на панели CTableData *table=panel.GetTable(table_id); if(table==NULL) return WRONG_VALUE; //--- Рассчитываем смещение таблицы на половину высоты строки таблицы int shift=CELL_H/2*(delta<0 ? -1 : 1); //--- Рассчитываем координаты, в пределах которых смещается таблица int y=table.Y1()+shift; if(y>2) y=2; if(y+table.Height()<panel.Height()-2) y=panel.Height()-2-table.Height(); if(table.Height()<panel.Height()) return WRONG_VALUE; //--- Очищаем панель и выводим на неё активную таблицу int total=int(table_id==TABLE_SYMBOLS ? ArraySymbols.Size() : ArrayMagics.Size()); panel.Clear(); panel.DrawGrid(table_id,2,y,total,1,CELL_H,panel.Width()-5,C'220,220,220'); //--- Заполняем таблицу значениями if(table_id==TABLE_SYMBOLS) FillsSymbolTable(panel,table); else FillsMagicTable(panel,table); //--- Получаем номер строки таблицы, над которой расположен курсор int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table); return index; }
При прокрутке колёсика мышки, если курсор находится над таблицей на панели, таблицу необходимо тоже прокручивать, в случае, если её размер превышает размер панели. Это и делает этот обработчик — сдвигает таблицу по указанным начальным координатам. Кроме того, строка, находящаяся под курсором, подсвечивается при помощи функции TableSelectRowByMouse(), и возвращается индекс строки под курсором, возвращаемый этой функцией. В левую панель выводятся небольшие по размерам списки символов и магиков, поэтому здесь выполнена упрощенная прокрутка — сразу смещаем таблицу на рассчитанные координаты. Для правой панели будет немного сложнее.
Функция-обработчик смещения курсора мышки внутри таблицы левой панели:
//+------------------------------------------------------------------+ //| Обработчик смещения курсора мышки внутри таблицы левой панели | //+------------------------------------------------------------------+ int TableMouseMoveHandlerL(const int x_cursor,const int y_cursor,CDashboard *panel,const int table_id) { //--- Проверяем указатель на левую панель if(panel==NULL) return WRONG_VALUE; //--- Проверяем наличие таблицы на панели if(!panel.TableIsExist(table_id)) return WRONG_VALUE; //--- Получаем указатель на активную таблицу на панели CTableData *table=panel.GetTable(table_id); if(table==NULL) return WRONG_VALUE; //--- Проверяем расположение курсора внутри панели //--- Если курсор за пределами панели - рисуем активную таблицу и возвращаем -1 (чтобы убрать выделение строки, над которой был курсор) int total=int(table_id==TABLE_SYMBOLS ? ArraySymbols.Size() : ArrayMagics.Size()); if(x_cursor<panel.CoordX()+2 || x_cursor>panel.CoordX()+panel.Width() || y_cursor<panel.CoordY()+4 || y_cursor>panel.CoordY()+panel.Height()) { panel.Clear(); panel.DrawGrid(table_id,2,table.Y1(),total,1,CELL_H,panel.Width()-5,C'220,220,220'); return WRONG_VALUE; } //--- Очищаем панель и выводим на неё активную таблицу panel.Clear(); panel.DrawGrid(table_id,2,table.Y1(),total,1,CELL_H,panel.Width()-5,C'220,220,220'); //--- Заполняем таблицу значениями if(table_id==TABLE_SYMBOLS) FillsSymbolTable(panel,table); else FillsMagicTable(panel,table); //--- Получаем и возвращаем номер строки таблицы, над которой расположен курсор int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table); return index; }
Здесь также, как и в предыдущей функции, ищется и выделяется строка под курсором. Но таблица, естественно, не прокручивается.
Функция-обработчик прокрутки колёсика мышки внутри таблицы правой панели:
//+------------------------------------------------------------------+ //| Обработчик прокрутки колёсика мышки внутри таблицы правой панели | //+------------------------------------------------------------------+ int TableMouseWhellHandlerR(const int x_cursor,const int y_cursor,const bool shift_flag,const int delta,CDashboard *panel,const int table_id) { //--- Проверяем указатель на правую панель if(panel==NULL) return WRONG_VALUE; //--- Проверяем расположение курсора внутри панели if(x_cursor<panel.CoordX()+2 || x_cursor>panel.CoordX()+panel.Width() || y_cursor<panel.CoordY()+4 || y_cursor>panel.CoordY()+panel.Height()) return WRONG_VALUE; //--- Проверяем наличие таблицы на панели if(!panel.TableIsExist(table_id)) return WRONG_VALUE; //--- Получаем указатель на активную таблицу на панели CTableData *table=panel.GetTable(table_id); if(table==NULL) return WRONG_VALUE; //--- Рассчитываем вертикальное смещение таблицы на половину высоты строки таблицы int shift_y=CELL_H/2*(delta<0 ? -1 : 1); //--- Рассчитываем горизонтальное смещение таблицы на размер высоты строки таблицы int shift_x=(shift_flag ? CELL_H*(delta<0 ? -1 : 1) : 0); //--- Рассчитываем координаты, в пределах которых смещается таблица по Y int y=table.Y1()+shift_y; if(y>2) y=2; if(y+table.Height()<panel.Height()-2) y=panel.Height()-2-table.Height(); //--- Рассчитываем координаты, в пределах которых смещается таблица по X int x=0; if(shift_flag) { x=table.X1()+shift_x; if(x>2) x=2; if(x+table.Width()<panel.Width()-2) x=panel.Width()-2-table.Width(); } //--- Если вся таблица умещается в размеры панели - ничего прокручивать не нужно, возвращаем -1 if(table.Height()<panel.Height() && table.Width()<panel.Width()) return WRONG_VALUE; //--- Определяем размеры таблицы int total=0; // количество строк int columns=0; // количество столбцов int cell_w=0; // ширина ячейки таблицы (столбца) int cell_h=CELL_H; // высота ячейки таблицы (строки) switch(table_id) { case TABLE_TRADES : total=(int)ArrayTrades.Size(); columns=11; cell_w=CELL_W_TRADES; break; case TABLE_SYMBOLS: total=(int)ArraySymbolStats.Size(); columns=(int)ArrayDataName.Size(); cell_w=CELL_W_SYMBOLS; break; case TABLE_MAGICS : total=(int)ArrayMagicStats.Size(); columns=(int)ArrayDataName.Size(); cell_w=CELL_W_MAGICS; break; case TABLE_STATS : total=TABLE_STAT_ROWS; columns=TABLE_STAT_COLS; cell_w=(panel.Width()-4)/TABLE_STAT_COLS; cell_h=(panel.Height()-4)/total; break; default : break; } //--- Очищаем панель и выводим на неё активную таблицу panel.Clear(); panel.DrawGrid(table_id, (shift_flag ? x : table.X1()), (!shift_flag && table.Height()>panel.Height() ? y : table.Y1()), total,columns,cell_h,cell_w, (table_id!=TABLE_STATS ? C'220,220,220' : C'230,230,230'), (table_id!=TABLE_STATS)); //--- Заполняем таблицу значениями switch(table_id) { case TABLE_TRADES : FillsTradeTable(panel,table); break; case TABLE_SYMBOLS: FillsTradingStatsBySymbolsTable(panel,table); break; case TABLE_MAGICS : FillsTradingStatsByMagicsTable(panel,table); break; default : break; } //--- Получаем указатель на панель заголовка CDashboard *panel_h=dashboard.GetPanel("FieldH"); if(panel_h==NULL) return WRONG_VALUE; //--- Получаем указатель на таблицу заголовка CTableData *table_h=panel_h.GetTable(table_id); if(table_h==NULL) return WRONG_VALUE; //--- Очищаем панель заголовка таблицы и выводим на неё таблицу panel_h.Clear(); panel_h.DrawGrid(table_id,(shift_flag ? x : table_h.X1()),2,1,columns,cell_h,cell_w,C'200,200,200',false); //--- Заполняем таблицу заголовков switch(table_id) { case TABLE_TRADES : FillsHeaderTradeTable(panel_h,table_h); break; case TABLE_SYMBOLS: case TABLE_MAGICS : FillsHeaderTradingStatsTable(panel_h,table_h); break; default : break; } //--- Для таблицы итоговой статистики номер строки под курсором искать не нужно if(table.ID()==TABLE_STATS) return WRONG_VALUE; //--- Получаем номер строки таблицы, над которой расположен курсор int index=TableSelectRowByMouse(x_cursor,y_cursor,cell_h,panel,table); return index; }
Здесь в одной функции обрабатываются сразу три таблицы и заголовки этих же таблиц. Всё зависит от типа таблицы, переданного в функцию. При прокрутке больших таблиц, их необходимо прокручивать не только по высоте, но и по ширине. За прокрутку по ширине отвечает флаг shift_flag — это признак удерживаемой клавиши Shift при вращении колёсика мышки. При прокрутке самой таблицы, одновременно с ней прокручивается и заголовок, расположенный на другой панели.
Функция-обработчик смещения курсора мышки внутри таблицы правой панели:
//+------------------------------------------------------------------+ //| Обработчик смещения курсора мышки внутри таблицы правой панели | //+------------------------------------------------------------------+ int TableMouseMoveHandlerR(const int x_cursor,const int y_cursor,CDashboard *panel,const int table_id) { //--- Проверяем указатель на левую панель if(panel==NULL) return WRONG_VALUE; //--- Проверяем наличие таблицы на панели if(!panel.TableIsExist(table_id)) return WRONG_VALUE; //--- Получаем указатель на активную таблицу на панели CTableData *table=panel.GetTable(table_id); if(table==NULL) return WRONG_VALUE; //--- Определяем размеры таблицы int total=0; // количество строк int columns=0; // количество столбцов int cell_w=0; // ширина ячейки таблицы (столбца) int cell_h=CELL_H; // высота ячейки таблицы (строки) switch(table_id) { case TABLE_TRADES : total=(int)ArrayTrades.Size(); columns=11; cell_w=CELL_W_TRADES; break; case TABLE_SYMBOLS: total=(int)ArraySymbolStats.Size(); columns=(int)ArrayDataName.Size(); cell_w=CELL_W_SYMBOLS; break; case TABLE_MAGICS : total=(int)ArrayMagicStats.Size(); columns=(int)ArrayDataName.Size(); cell_w=CELL_W_MAGICS; break; case TABLE_STATS : total=TABLE_STAT_ROWS; columns=TABLE_STAT_COLS; cell_w=(panel.Width()-4)/TABLE_STAT_COLS; cell_h=(panel.Height()-4)/total; break; default : break; } //--- Проверяем расположение курсора внутри панели //--- Если курсор за пределами панели - рисуем активную таблицу и возвращаем -1 (чтобы убрать выделение строки, над которой был курсор) if(x_cursor<panel.CoordX()+2 || x_cursor>panel.CoordX()+panel.Width() || y_cursor<panel.CoordY()+4 || y_cursor>panel.CoordY()+panel.Height()) { panel.Clear(); panel.DrawGrid(table_id,table.X1(),table.Y1(),total,columns,cell_h,cell_w,(table_id!=TABLE_STATS ? C'220,220,220' : C'230,230,230'),(table_id!=TABLE_STATS)); return WRONG_VALUE; } //--- Очищаем панель и выводим на неё активную таблицу panel.Clear(); panel.DrawGrid(table_id,table.X1(),table.Y1(),total,columns,cell_h,cell_w,(table_id!=TABLE_STATS ? C'220,220,220' : C'230,230,230'),(table_id!=TABLE_STATS)); //--- Заполняем таблицу значениями switch(table_id) { case TABLE_TRADES : FillsTradeTable(panel,table); break; case TABLE_SYMBOLS: FillsTradingStatsBySymbolsTable(panel,table); break; case TABLE_MAGICS : FillsTradingStatsByMagicsTable(panel,table); break; default : break; } //--- Для таблицы итоговой статистики номер строки под курсором искать не нужно if(table.ID()==TABLE_STATS) return WRONG_VALUE; //--- Получаем номер строки таблицы, над которой расположен курсор int index=TableSelectRowByMouse(x_cursor,y_cursor,cell_h,panel,table); return index; }
Вообще, здесь (да и в функциях, рассмотренных выше) нам нужно только найти номер строки таблицы, над которой находится курсор. Всё остальное — это обслуживание "красивостей" для выделения строки под курсором, что в итоге приводит к повышенному потреблению ресурсов процессора. Ведь нам приходится постоянно перерисовывать всю видимую часть таблицы и, чем больше по размерам панель и таблица на ней, тем большее пространство приходится обрисовывать. Конечно же, здесь (и далее) ограничено физически рисуемое пространство размерами панели, но всё равно это не оптимально. Если бы мы делали такие выделения строк в классе панели, то было бы всё иначе — мы бы перерисовывали только прилегающие к курсору строки таблицы, запоминая цвет фона до- и после подсветки, потом его восстанавливая. Но здесь, так как только в качестве примера, мы делаем всё прямо в функциях программы, и не будем сильно всё усложнять.
Функция-обработчик щелчка мышки внутри таблицы на панели:
//+------------------------------------------------------------------+ //| Обработчик щелчка мышки внутри таблицы на панели | //+------------------------------------------------------------------+ int TableMouseClickHandler(const int x_cursor,const int y_cursor,CDashboard *panel,const int table_id) { //--- Проверяем указатель на левую панель if(panel==NULL) return WRONG_VALUE; //--- Проверяем расположение курсора внутри панели if(x_cursor<panel.CoordX()+2 || x_cursor>panel.CoordX()+panel.Width() || y_cursor<panel.CoordY()+4 || y_cursor>panel.CoordY()+panel.Height()) return WRONG_VALUE; //--- Проверяем наличие таблицы на панели if(!panel.TableIsExist(table_id)) return WRONG_VALUE; //--- Получаем указатель на активную таблицу на панели CTableData *table=panel.GetTable(table_id); if(table==NULL) return WRONG_VALUE; //--- Для таблицы итоговой статистики номер строки под курсором искать не нужно if(table.ID()==TABLE_STATS) return WRONG_VALUE; //--- Получаем номер строки таблицы, на которой был щелчок int index=TableSelectRowByMouse(x_cursor,y_cursor,CELL_H,panel,table); return index; }
При щелчке мышкой по строке таблицы, нам нужно найти и вернуть номер строки, внутри которой был щелчок, чтобы далее его обработать. Что, впрочем, уже и было показано в обработчике пользовательских событий в OnChartEvent().
Функция, заполняющая таблицу наименованиями символов:
//+------------------------------------------------------------------+ //| Заполняет таблицу наименованиями символов | //+------------------------------------------------------------------+ void FillsSymbolTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Рассчитаем индекс строки, с которой необходимо начать заполнение таблицы CTableCell *cell=table.GetCell(0,0); if(cell==NULL) return; int y=panel.CoordY()+cell.Y()-2; int diff=panel.CoordY()-y; int index=diff/CELL_H; //--- Заполняем таблицу значениями из массива, начиная со строки index for(int i=index;i<(int)ArraySymbols.Size();i++) { CTableCell *cell=table.GetCell(i,0); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- выводим данные из массива в ячейки таблицы cell.SetText(ArraySymbols[i]); panel.DrawText(cell.Text(),cell.X()+2,cell.Y()+1); } }
Здесь, в дополнение к тому, что области таблицы, выходящие за пределы панели, не рисуются, мы ещё ограничиваем начало цикла той строкой таблицы, которая видима сверху самой первой. Поясню: при прокрутке таблицы вверх, самая первая строка может уйти далеко за пределы панели сверху. И чтобы лишний раз не "крутить цикл" и не пытаться рисовать за пределами панели те строки таблицы, которые всё равно не будут рисоваться, нам нужно рассчитать индекс первой видимой сверху строки таблицы, и с него начать цикл. При достаточно больших таблицах такой подход даёт ощутимый результат, убирая сильные замедления при прокрутке таблицы с сотнями строк.
Функция, заполняющая таблицу значениями магиков:
//+------------------------------------------------------------------+ //| Заполняет таблицу значениями магиков | //+------------------------------------------------------------------+ void FillsMagicTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Рассчитаем индекс строки, с которой необходимо начать заполнение таблицы CTableCell *cell=table.GetCell(0,0); if(cell==NULL) return; int y=panel.CoordY()+cell.Y()-2; int diff=panel.CoordY()-y; int index=diff/CELL_H; //--- Заполняем таблицу значениями из массива, начиная со строки index for(int i=index;i<(int)ArrayMagics.Size();i++) { CTableCell *cell=table.GetCell(i,0); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- выводим данные из массива в ячейки таблицы string text=(i<(int)ArrayMagics.Size()-1 ? (string)ArrayMagics[i] : "ALL"); cell.SetText(text); panel.DrawText(cell.Text(),cell.X()+2,cell.Y()+1); } }
Функция, заполняющая таблицу заголовков трейдов:
//+------------------------------------------------------------------+ //| Заполняет таблицу заголовков трейдов | //+------------------------------------------------------------------+ void FillsHeaderTradeTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Заполняем таблицу значениями int total=11; // 11 столбцов таблицы CTableCell *cell=NULL; for(int i=0;i<total;i++) { //--- Получаем ячейку i таблицы из нулевой (и единственной) строки таблицы cell=table.GetCell(0,i); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- Записываем наименования заголовков в зависимости от индекса цикла string cell_text=""; switch(i) { case 0 : cell_text="Time Entry In"; break; // время входа case 1 : cell_text="Position ID"; break; // ID позиции case 2 : cell_text="Position Type"; break; // покупка или продажа case 3 : cell_text="Volume"; break; // объем case 4 : cell_text="Symbol"; break; // символ case 5 : cell_text="Price Entry In"; break; // цена входа case 6 : cell_text="Time Entry Out"; break; // время выхода case 7 : cell_text="Price Entry Out"; break; // цена выхода case 8 : cell_text="Commission"; break; // комиссия за вход и выход case 9 : cell_text="Swap"; break; // своп case 10 : cell_text="Profit"; break; // прибыль или убыток default : break; } //--- выводим записи в ячейки таблицы cell.SetText(cell_text); panel.DrawText(cell.Text(),cell.X()+6,cell.Y()+2); } }
Функция, заполняющая таблицу трейдов:
//+------------------------------------------------------------------+ //| Заполняет таблицу трейдов | //+------------------------------------------------------------------+ void FillsTradeTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Заполняем таблицу значениями из массива CTableCell *cell=NULL; int total=(int)ArrayTrades.Size(); if(total==0) { PrintFormat("%s: Error: Trades array is empty",__FUNCTION__); return; } //--- Рассчитаем индекс строки, с которой необходимо начать заполнение таблицы cell=table.GetCell(0,0); if(cell==NULL) return; int y=panel.CoordY()+cell.Y()-2; int diff=panel.CoordY()-y; int index=diff/CELL_H; //--- В цикле по количеству строк (размер массива трейдов), начиная со строки index for(int i=index;i<total;i++) { //--- в цикле по количеству столбцов (11 для данной таблицы) for(int j=0;j<11;j++) { //--- получаем очередную ячейку таблицы cell=table.GetCell(i,j); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- Получаем данные таблицы из массива трейдов string cell_text=""; int digits=(int)SymbolInfoInteger(ArrayTrades[i].symbol,SYMBOL_DIGITS); switch(j) { case 0 : cell_text=TimeToString(ArrayTrades[i].time_in); break; // время входа case 1 : cell_text=IntegerToString(ArrayTrades[i].ticket); break; // ID позиции case 2 : cell_text=(ArrayTrades[i].type==0 ? "Buy" : "Sell"); break; // покупка или продажа case 3 : cell_text=DoubleToString(ArrayTrades[i].volume,2); break; // объем case 4 : cell_text=ArrayTrades[i].symbol; break; // символ case 5 : cell_text=DoubleToString(ArrayTrades[i].price_in,digits); break; // цена входа case 6 : cell_text=TimeToString(ArrayTrades[i].time_out); break; // время выхода case 7 : cell_text=DoubleToString(ArrayTrades[i].price_out,digits); break; // цена выхода case 8 : cell_text=DoubleToString(ArrayTrades[i].commission,2); break; // комиссия за вход и выход case 9 : cell_text=DoubleToString(ArrayTrades[i].swap,2); break; // своп case 10 : cell_text=DoubleToString(ArrayTrades[i].profit,2); break; // прибыль или убыток default : break; } //--- выводим записи в ячейки таблицы cell.SetText(cell_text); panel.DrawText(cell.Text(),cell.X()+6,cell.Y()+1); } } }
Две функции, по сути, рисуют одну таблицу. Одна функция рисует заголовок таблицы со столбцами, озаглавленными в зависимости от индекса столбца, а вторая функция рисует под заголовком таблицу со значениями, соответствующими заголовкам столбцов. В общем — функции работают в паре.
Функция, заполняющая таблицу заголовков статистики торговли:
//+------------------------------------------------------------------+ //| Заполняет таблицу заголовков статистики торговли | //+------------------------------------------------------------------+ void FillsHeaderTradingStatsTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Заполняем таблицу значениями в цикле по количеству столбцов (по размеру данных в ArrayDataName) int total=(int)ArrayDataName.Size(); CTableCell *cell=NULL; for(int i=0;i<total;i++) { //--- получаем очередную ячейку таблицы cell=table.GetCell(0,i); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- Записываем наименования заголовков в зависимости от индекса цикла string cell_text=(i>0 ? ArrayDataName[i] : table.ID()==TABLE_SYMBOLS ? "Symbol" : "Magic"); //--- выводим записи в ячейки таблицы cell.SetText(cell_text); panel.DrawText(cell.Text(),cell.X()+6,cell.Y()+2); } }
При выводе статистической таблицы по символам или магикам, требуется нарисовать заголовок таблицы статистики. Но у нас есть две таблицы со статистическими данными: одна по символам, вторая — по магикам. Заголовки этих таблиц отличаются лишь первым столбцом, а все остальные одинаковые. Здесь проверяется идентификатор таблицы и, в зависимости от него, заголовок первого столбца подписывается либо символом, либо магиком.
Функция, заполняющая таблицу статистики торговли по символам:
//+------------------------------------------------------------------+ //| Заполняет таблицу статистики торговли по символам | //+------------------------------------------------------------------+ void FillsTradingStatsBySymbolsTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Заполняем таблицу значениями из массива CTableCell *cell=NULL; int total=(int)ArraySymbolStats.Size(); if(total==0) { PrintFormat("%s: Error: The array of trading statistics by symbols is empty",__FUNCTION__); return; } //--- Рассчитаем индекс строки, с которой необходимо начать заполнение таблицы cell=table.GetCell(0,0); if(cell==NULL) return; int y=panel.CoordY()+cell.Y()-2; int diff=panel.CoordY()-y; int index=diff/CELL_H; //--- В цикле по количеству строк (размер массива статистики символов), начиная со строки index for(int i=index;i<total;i++) { //--- в цикле по количеству столбцов статистики (массив расположения столбцов таблиц статистики слева-направо) for(int j=0;j<(int)ArrayDataName.Size();j++) { //--- получаем очередную ячейку таблицы cell=table.GetCell(i,j); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- Получаем данные таблицы из массива структур string cell_text=""; cell_text=GetDataStatsStr(TABLE_SYMBOLS, ArrayDataName[j],i); //--- выводим записи в ячейки таблицы cell.SetText(cell_text); panel.DrawText(cell.Text(),cell.X()+6,cell.Y()+1); } } }
Функция, заполняющая таблицу статистики торговли по магикам:
//+------------------------------------------------------------------+ //| Заполняет таблицу статистики торговли по магикам | //+------------------------------------------------------------------+ void FillsTradingStatsByMagicsTable(CDashboard *panel,CTableData *table) { //--- Проверяем указатели на панель и таблицу if(panel==NULL || table==NULL) return; //--- Заполняем таблицу значениями из массива CTableCell *cell=NULL; int total=(int)ArrayMagicStats.Size(); if(total==0) { PrintFormat("%s: Error: The array of trading statistics by magics is empty",__FUNCTION__); return; } //--- Рассчитаем индекс строки, с которой необходимо начать заполнение таблицы cell=table.GetCell(0,0); if(cell==NULL) return; int y=panel.CoordY()+cell.Y()-2; int diff=panel.CoordY()-y; int index=diff/CELL_H; //--- В цикле по количеству строк (размер массива статистики магиков), начиная со строки index for(int i=index;i<total;i++) { //--- в цикле по количеству столбцов статистики (массив расположения столбцов таблиц статистики слева-направо) for(int j=0;j<(int)ArrayDataName.Size();j++) { //--- получаем очередную ячейку таблицы cell=table.GetCell(i,j); if(cell==NULL) continue; //--- невидимые области таблицы не рисуем if(cell.X()>panel.CoordX()+panel.Width()) continue; if(cell.Y()>panel.CoordY()+panel.Height()) break; //--- Получаем данные таблицы из массива структур string cell_text=GetDataStatsStr(TABLE_MAGICS, ArrayDataName[j],i); //--- выводим записи в ячейки таблицы cell.SetText(cell_text); panel.DrawText(cell.Text(),cell.X()+6,cell.Y()+1); } } }
Две однотипные функции, выполняющие заполнение таблиц статистики по символам и по магикам.
Функция, возвращающая данные из структуры по типу заголовка:
//+------------------------------------------------------------------+ //| Возвращает данные из структуры по типу заголовка | //+------------------------------------------------------------------+ double GetDataStats(const int table_type, const string data_type, const int index) { //--- В зависимости от типа данных в таблице возвращаем данные из полей структуры data_type, по индексу массива index switch(table_type) { case TABLE_SYMBOLS : return ( data_type==H_TRADES_S ? ArraySymbolStats[index].trades : data_type==H_GROSS_PROFIT_S ? ArraySymbolStats[index].gross_profit : data_type==H_GROSS_LOSS_S ? ArraySymbolStats[index].gross_loss : data_type==H_COMMISSIONS_S ? ArraySymbolStats[index].total_commission : data_type==H_SWAPS_S ? ArraySymbolStats[index].total_swap : data_type==H_PROFITS_S ? ArraySymbolStats[index].total_profit : data_type==H_NET_PROFIT_S ? ArraySymbolStats[index].net_profit : data_type==H_WINS_S ? ArraySymbolStats[index].win_trades : data_type==H_LOST_S ? ArraySymbolStats[index].loss_trades : data_type==H_LONG_S ? ArraySymbolStats[index].long_trades : data_type==H_SHORT_S ? ArraySymbolStats[index].short_trades : data_type==H_EXP_PAYOFF_S ? ArraySymbolStats[index].expected_payoff : data_type==H_WIN_PRC_S ? ArraySymbolStats[index].win_percent : data_type==H_LOSS_PRC_S ? ArraySymbolStats[index].loss_percent : data_type==H_AVG_PROFIT_S ? ArraySymbolStats[index].average_profit : data_type==H_AVG_LOSS_S ? ArraySymbolStats[index].average_loss : data_type==H_PRF_FACTOR_S ? ArraySymbolStats[index].profit_factor : 0 ); case TABLE_MAGICS : return ( data_type==H_TRADES_S ? ArrayMagicStats[index].trades : data_type==H_GROSS_PROFIT_S ? ArrayMagicStats[index].gross_profit : data_type==H_GROSS_LOSS_S ? ArrayMagicStats[index].gross_loss : data_type==H_COMMISSIONS_S ? ArrayMagicStats[index].total_commission : data_type==H_SWAPS_S ? ArrayMagicStats[index].total_swap : data_type==H_PROFITS_S ? ArrayMagicStats[index].total_profit : data_type==H_NET_PROFIT_S ? ArrayMagicStats[index].net_profit : data_type==H_WINS_S ? ArrayMagicStats[index].win_trades : data_type==H_LOST_S ? ArrayMagicStats[index].loss_trades : data_type==H_LONG_S ? ArrayMagicStats[index].long_trades : data_type==H_SHORT_S ? ArrayMagicStats[index].short_trades : data_type==H_EXP_PAYOFF_S ? ArrayMagicStats[index].expected_payoff : data_type==H_WIN_PRC_S ? ArrayMagicStats[index].win_percent : data_type==H_LOSS_PRC_S ? ArrayMagicStats[index].loss_percent : data_type==H_AVG_PROFIT_S ? ArrayMagicStats[index].average_profit : data_type==H_AVG_LOSS_S ? ArrayMagicStats[index].average_loss : data_type==H_PRF_FACTOR_S ? ArrayMagicStats[index].profit_factor : 0 ); case TABLE_ACCOUNT : return ( data_type==H_TRADES_S ? ArrayAccountStats[index].trades : data_type==H_GROSS_PROFIT_S ? ArrayAccountStats[index].gross_profit : data_type==H_GROSS_LOSS_S ? ArrayAccountStats[index].gross_loss : data_type==H_COMMISSIONS_S ? ArrayAccountStats[index].total_commission : data_type==H_SWAPS_S ? ArrayAccountStats[index].total_swap : data_type==H_PROFITS_S ? ArrayAccountStats[index].total_profit : data_type==H_NET_PROFIT_S ? ArrayAccountStats[index].net_profit : data_type==H_WINS_S ? ArrayAccountStats[index].win_trades : data_type==H_LOST_S ? ArrayAccountStats[index].loss_trades : data_type==H_LONG_S ? ArrayAccountStats[index].long_trades : data_type==H_SHORT_S ? ArrayAccountStats[index].short_trades : data_type==H_EXP_PAYOFF_S ? ArrayAccountStats[index].expected_payoff : data_type==H_WIN_PRC_S ? ArrayAccountStats[index].win_percent : data_type==H_LOSS_PRC_S ? ArrayAccountStats[index].loss_percent : data_type==H_AVG_PROFIT_S ? ArrayAccountStats[index].average_profit : data_type==H_AVG_LOSS_S ? ArrayAccountStats[index].average_loss : data_type==H_PRF_FACTOR_S ? ArrayAccountStats[index].profit_factor : 0 ); default : return 0; } }
В функцию передаётся тип таблицы статистики (символ/магик/аккаунт), тип данных (значение заголовка столбца) и индекс данных в массиве структур. В зависимости от переданных в функцию данных, возвращается значение из соответствующего массива структур. Для упрощения, значение всегда возвращается с типом double, даже если в структуре оно целочисленное. В следующей функции это значение возвращается в виде строки с нужным количеством десятичных знаков.
Функция, возвращающая данные из структуры по типу заголовка в виде строки:
//+------------------------------------------------------------------+ //| Возвращает данные из структуры по типу заголовка в виде строки | //+------------------------------------------------------------------+ string GetDataStatsStr(const int table_type, const string data_type, const int index) { //--- В зависимости от типа данных, определяем количество знаков после запятой //--- (2 - для вещественного свойства и 0 - для целочисленного) int digits=(data_type==H_TRADES_S || data_type==H_WINS_S || data_type==H_LOST_S || data_type==H_LONG_S || data_type==H_SHORT_S ? 0 : 2); //--- Если тип данных "Заголовок" if(data_type=="HEADER") { //--- возвращаем наименование в зависимости от типа таблицы (символ, магик, аккаунт) switch(table_type) { case TABLE_SYMBOLS : return ArraySymbolStats[index].name; case TABLE_MAGICS : return (string)ArrayMagicStats[index].magic; case TABLE_ACCOUNT : return (string)ArrayAccountStats[index].account; default : return "Unknown:"+(string)table_type; } } //--- Для всех остальных типов данных возвращаем их строковое значение с ранее определённым количеством десятичных знаков return(DoubleToString(GetDataStats(table_type, data_type, index),digits)); }
Функции используются для вывода значений ячеек таблиц статистики в таблицы на панели.
Функция, возвращающая индекс символа в массиве статистики по символам:
//+------------------------------------------------------------------+ //| Возвращает индекс символа в массиве статистики по символам | //+------------------------------------------------------------------+ int GetIndexSymbol(const string symbol) { int total=(int)ArraySymbolStats.Size(); for(int i=0;i<total;i++) { if(ArraySymbolStats[i].name==symbol) return i; } return WRONG_VALUE; }
Функция, возвращающая индекс символа в массиве статистики по магикам:
//+------------------------------------------------------------------+ //| Возвращает индекс магика в массиве статистики по магикам | //+------------------------------------------------------------------+ int GetIndexMagic(const long magic) { int total=(int)ArrayMagicStats.Size(); for(int i=0;i<total;i++) { if(ArrayMagicStats[i].magic==magic) return i; } return WRONG_VALUE; }
Обе функции возвращают индекс искомого символа или магика в соответствующем массиве статистики символов или магиков.
Функция, отображающая на панели итоговую статистику по выбранному символу, магику или аккаунту:
//+------------------------------------------------------------------+ //| Отображает статистику по выбранному символу, магику или счёту | //+------------------------------------------------------------------+ bool ViewStatistic(const int table_type,const string cell_text) { //--- Получаем указатели на панель заголовков и правую панель для вывода статистики CDashboard *panel_h=dashboard.GetPanel("FieldH"); CDashboard *panel_r=dashboard.GetPanel("FieldR"); if(panel_h==NULL || panel_r==NULL) return false; //--- Определяем источник статистических данных (символ/магик/аккаунт) string source=(table_type==TABLE_SYMBOLS ? "symbol" : table_type==TABLE_MAGICS ? "magic" : "account"); int index=WRONG_VALUE; //--- В зависимости от текста в выбранной ячейке таблицы (cell_text), переданному в функцию, //--- получаем индекс, по которому содержатся данные в соответствующем массиве статистики switch(table_type) { case TABLE_SYMBOLS: index=GetIndexSymbol(cell_text); break; case TABLE_MAGICS : index=GetIndexMagic(StringToInteger(cell_text)); break; case TABLE_ACCOUNT: index=(ArrayAccountStats.Size()==1 ? 0 : -1); break; default : break; } //--- Если индекс получить не удалось, считаем, что соответствующий массив статистики пуст if(index==WRONG_VALUE) { PrintFormat("%s: Error. Empty array of %s statistics",__FUNCTION__,source); return false; } //--- Получим и сохраним установленные для панели заголовков свойства шрифта int f_size,f_flags,f_angle; string f_name=panel_h.FontParams(f_size,f_flags,f_angle); //--- Очищаем панель заголовков и выводим на неё описание выбранных данных шрифтом Tahoma размером 8 panel_h.Clear(); panel_h.SetFontParams("Tahoma",8,f_flags,f_angle); panel_h.DrawText(StringFormat("Trade statistics by %s %s",source,cell_text),8,3,C'150,150,150'); //--- Возвращаем шрифту панели заголовков прежние сохранённые свойства panel_h.SetFontParams(f_name,f_size,f_flags,f_angle); //--- Проверяем наличие и получаем или создаём объект-таблицу для вывода статистики на правой панели if(!panel_r.TableIsExist(TABLE_STATS) && !panel_r.CreateNewTable(TABLE_STATS)) return false; //--- Получаем указатель на созданную таблицу CTableData *table_r=panel_r.GetTable(TABLE_STATS); if(table_r==NULL) return false; //--- Очищаем правую панель и рисуем на ней таблицу panel_r.Clear(); panel_r.DrawGrid(TABLE_STATS,2,2,TABLE_STAT_ROWS,TABLE_STAT_COLS,(panel_r.Height()-4)/TABLE_STAT_ROWS,(panel_r.Width()-4)/TABLE_STAT_COLS,C'230,230,230',false); //--- Объявляем структуру для хранения данных статистики //--- (символ/магик/аккаунт) по ранее полученному индексу данных. //--- Все поля структур SSymbolStats, SMagicStats и SAccountStats одинаковы, //--- кроме первого поля с наименованием символа, значением магика или номером счёта. //--- Т.к. первое поле здесь не нужно, поэтому здесь достаточно любого типа структуры из трёх. //--- Заполняем объявленную структуру данными в зависимости от выбранного источника SSymbolStats stats={}; switch(table_type) { case TABLE_SYMBOLS: stats.trades = ArraySymbolStats[index].trades; stats.gross_profit = ArraySymbolStats[index].gross_profit; stats.gross_loss = ArraySymbolStats[index].gross_loss; stats.total_commission= ArraySymbolStats[index].total_commission; stats.total_swap = ArraySymbolStats[index].total_swap; stats.total_profit = ArraySymbolStats[index].total_profit; stats.net_profit = ArraySymbolStats[index].net_profit; stats.win_trades = ArraySymbolStats[index].win_trades; stats.loss_trades = ArraySymbolStats[index].loss_trades; stats.long_trades = ArraySymbolStats[index].long_trades; stats.short_trades = ArraySymbolStats[index].short_trades; stats.expected_payoff = ArraySymbolStats[index].expected_payoff; stats.win_percent = ArraySymbolStats[index].win_percent; stats.loss_percent = ArraySymbolStats[index].loss_percent; stats.average_profit = ArraySymbolStats[index].average_profit; stats.average_loss = ArraySymbolStats[index].average_loss; stats.profit_factor = ArraySymbolStats[index].profit_factor; break; case TABLE_MAGICS : stats.trades = ArrayMagicStats[index].trades; stats.gross_profit = ArrayMagicStats[index].gross_profit; stats.gross_loss = ArrayMagicStats[index].gross_loss; stats.total_commission= ArrayMagicStats[index].total_commission; stats.total_swap = ArrayMagicStats[index].total_swap; stats.total_profit = ArrayMagicStats[index].total_profit; stats.net_profit = ArrayMagicStats[index].net_profit; stats.win_trades = ArrayMagicStats[index].win_trades; stats.loss_trades = ArrayMagicStats[index].loss_trades; stats.long_trades = ArrayMagicStats[index].long_trades; stats.short_trades = ArrayMagicStats[index].short_trades; stats.expected_payoff = ArrayMagicStats[index].expected_payoff; stats.win_percent = ArrayMagicStats[index].win_percent; stats.loss_percent = ArrayMagicStats[index].loss_percent; stats.average_profit = ArrayMagicStats[index].average_profit; stats.average_loss = ArrayMagicStats[index].average_loss; stats.profit_factor = ArrayMagicStats[index].profit_factor; break; case TABLE_ACCOUNT: stats.trades = ArrayAccountStats[index].trades; stats.gross_profit = ArrayAccountStats[index].gross_profit; stats.gross_loss = ArrayAccountStats[index].gross_loss; stats.total_commission= ArrayAccountStats[index].total_commission; stats.total_swap = ArrayAccountStats[index].total_swap; stats.total_profit = ArrayAccountStats[index].total_profit; stats.net_profit = ArrayAccountStats[index].net_profit; stats.win_trades = ArrayAccountStats[index].win_trades; stats.loss_trades = ArrayAccountStats[index].loss_trades; stats.long_trades = ArrayAccountStats[index].long_trades; stats.short_trades = ArrayAccountStats[index].short_trades; stats.expected_payoff = ArrayAccountStats[index].expected_payoff; stats.win_percent = ArrayAccountStats[index].win_percent; stats.loss_percent = ArrayAccountStats[index].loss_percent; stats.average_profit = ArrayAccountStats[index].average_profit; stats.average_loss = ArrayAccountStats[index].average_loss; stats.profit_factor = ArrayAccountStats[index].profit_factor; break; default: break; } //--- Получим и сохраним установленные для правой панели свойства шрифта f_name=panel_r.FontParams(f_size,f_flags,f_angle); //--- Устанавливаем для правой панели новый шрифт Tahoma размером 8 panel_r.SetFontParams("Tahoma",8,FW_BLACK,f_angle); //--- Переменные для расчёта места расположения текста в ячейке таблицы CTableCell *cellH=NULL, *cellV=NULL; int cols=table_r.ColumnsInRow(0); // количество столбцов таблицы статистики int cw=table_r.Width()/cols; // ширина одного столбца таблицы int y_shift=6; // смещение текста по высоте int x_shift=21; // смещение текста по ширине int tw=0; // ширина текста string text=""; double value=0; //--- Левый столбец (данные -- значение) //--- Получаем ячейки 0,0 и 0,1 таблицы и выводим в их координаты Trades и его значение cellH=table_r.GetCell(0,0); cellV=table_r.GetCell(0,1); if(cellH==NULL || cellV==NULL) return false; text=(string)stats.trades; tw=panel_r.TextWidth(text); panel_r.DrawText(H_TRADES+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 1,0 и 1,1 таблицы и выводим в их координаты Long и его значение cellH=table_r.GetCell(1,0); cellV=table_r.GetCell(1,1); if(cellH==NULL || cellV==NULL) return false; text=(string)stats.long_trades; tw=panel_r.TextWidth(text); panel_r.DrawText(H_LONG+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 2,0 и 2,1 таблицы и выводим в их координаты Short и его значение cellH=table_r.GetCell(2,0); cellV=table_r.GetCell(2,1); if(cellH==NULL || cellV==NULL) return false; text=(string)stats.short_trades; tw=panel_r.TextWidth(text); panel_r.DrawText(H_SHORT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 3,0 и 3,1 таблицы и выводим в их координаты Net Profit и его значение cellH=table_r.GetCell(3,0); cellV=table_r.GetCell(3,1); if(cellH==NULL || cellV==NULL) return false; value=stats.net_profit; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_NET_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>0 ? C'86,119,204' : value<0 ? C'234,50,50' : C'150,150,150')); //--- Получаем ячейки 4,0 и 4,1 таблицы и выводим в их координаты Profit Loss и его значение cellH=table_r.GetCell(4,0); cellV=table_r.GetCell(4,1); if(cellH==NULL || cellV==NULL) return false; value=stats.total_profit; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_PROFITS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>0 ? C'86,119,204' : value<0 ? C'234,50,50' : C'150,150,150')); //--- Получаем ячейки 5,0 и 5,1 таблицы и выводим в их координаты Gross Profit и его значение cellH=table_r.GetCell(5,0); cellV=table_r.GetCell(5,1); if(cellH==NULL || cellV==NULL) return false; value=stats.gross_profit; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_GROSS_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>0 ? C'86,119,204' : C'150,150,150')); //--- Получаем ячейки 6,0 и 6,1 таблицы и выводим в их координаты Gross Loss и его значение cellH=table_r.GetCell(6,0); cellV=table_r.GetCell(6,1); if(cellH==NULL || cellV==NULL) return false; value=stats.gross_loss; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_GROSS_LOSS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<0 ? C'234,50,50' : C'150,150,150')); //--- Получаем ячейки 7,0 и 7,1 таблицы и выводим в их координаты Commission total и его значение cellH=table_r.GetCell(7,0); cellV=table_r.GetCell(7,1); if(cellH==NULL || cellV==NULL) return false; value=stats.total_commission; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_COMMISSIONS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<0 ? C'234,50,50' : C'150,150,150')); //--- Получаем ячейки 8,0 и 8,1 таблицы и выводим в их координаты Swap total и его значение cellH=table_r.GetCell(8,0); cellV=table_r.GetCell(8,1); if(cellH==NULL || cellV==NULL) return false; value=stats.total_swap; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_SWAPS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<0 ? C'234,50,50' : C'150,150,150')); //--- Правый столбец (данные -- значение) //--- Получаем ячейки 0,2 и 0,3 таблицы и выводим в их координаты Win trades и его значение cellH=table_r.GetCell(0,2); cellV=table_r.GetCell(0,3); if(cellH==NULL || cellV==NULL) return false; text=(string)stats.win_trades; tw=panel_r.TextWidth(text); panel_r.DrawText(H_WINS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 1,2 и 1,3 таблицы и выводим в их координаты Loss trades и его значение cellH=table_r.GetCell(1,2); cellV=table_r.GetCell(1,3); if(cellH==NULL || cellV==NULL) return false; text=(string)stats.loss_trades; tw=panel_r.TextWidth(text); panel_r.DrawText(H_LOST+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 2,2 и 2,3 таблицы и выводим в их координаты Expected Payoff и его значение cellH=table_r.GetCell(2,2); cellV=table_r.GetCell(2,3); if(cellH==NULL || cellV==NULL) return false; value=stats.expected_payoff; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_EXP_PAYOFF+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Получаем ячейки 3,2 и 3,3 таблицы и выводим в их координаты Win percent и его значение cellH=table_r.GetCell(3,2); cellV=table_r.GetCell(3,3); if(cellH==NULL || cellV==NULL) return false; value=stats.win_percent; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_WIN_PRC+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'86,119,204'); //--- Получаем ячейки 4,2 и 4,3 таблицы и выводим в их координаты Loss percent и его значение cellH=table_r.GetCell(4,2); cellV=table_r.GetCell(4,3); if(cellH==NULL || cellV==NULL) return false; value=stats.loss_percent; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_LOSS_PRC+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'234,50,50'); //--- Получаем ячейки 5,2 и 5,3 таблицы и выводим в их координаты Average Profit и его значение cellH=table_r.GetCell(5,2); cellV=table_r.GetCell(5,3); if(cellH==NULL || cellV==NULL) return false; value=stats.average_profit; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_AVG_PROFIT+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value>0 ? C'86,119,204' : C'150,150,150')); //--- Получаем ячейки 6,2 и 6,3 таблицы и выводим в их координаты Average Loss и его значение cellH=table_r.GetCell(6,2); cellV=table_r.GetCell(6,3); if(cellH==NULL || cellV==NULL) return false; value=stats.average_loss; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_AVG_LOSS+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,(value<0 ? C'234,50,50' : C'150,150,150')); //--- Получаем ячейки 7,2 и 7,3 таблицы и выводим в их координаты Profit factor и его значение cellH=table_r.GetCell(7,2); cellV=table_r.GetCell(7,3); if(cellH==NULL || cellV==NULL) return false; value=stats.profit_factor; text=DoubleToString(value,2); tw=panel_r.TextWidth(text); panel_r.DrawText(H_PRF_FACTOR+":",cellH.X()+x_shift,cellH.Y()+y_shift,C'150,150,150'); panel_r.DrawText(text,cellV.X()+cw-tw-x_shift,cellV.Y()+y_shift,C'150,150,150'); //--- Возвращаем шрифту правой панели прежние сохранённые свойства panel_r.SetFontParams(f_name,f_size,f_flags,f_angle); return true; }
Функция предназначена для рисования итоговой таблицы статистики по выбранному символу, магику, или полностью по счёту. В функцию передаётся тип таблицы статистики и наименования символа, или строковое значение магика, или номера счёта. По тексту определяется индекс символа, магика или аккаунта в соответствующем массиве структур статистических данных. Из требуемой структуры по полученному индексу получаем в структуру все статистические данные, а далее располагаем их в нарисованной таблице по координатам расположения её ячеек. При этом смещения выводимого текста по горизонтали рассчитываются таким образом, чтобы заголовок данных был привязан к левому краю ячейки таблицы, а текст значения этих данных был привязан к правому краю своей ячейки таблицы. Все данные выводятся в четыре столбца таким образом, чтобы визуально они были сгруппированы на панели в два столбца в виде "заголовок -- значение".
Давайте скомпилируем индикатор и посмотрим воочию что мы получили:
Ну что ж, видим, что весь заявленный функционал работает как предполагалось. Да, заметны небольшие "моргания" текста в таблицах при перемещении курсора и прокрутке таблиц. Но это результат неоптимальной схемы перерисовки — вся видимая часть таблицы постоянно перерисовывается. Этого можно избежать более сложной логикой обработки строк таблиц под курсором, что не входит в планы этой статьи.
Таблицы можно прокручивать вертикально вращением колёсика мышки, а горизонтально — вращением колёсика с зажатой клавишей Shift. При внимательном рассмотрении видео можно заметить, что при отображении статистики по магику со значением 0, количество длинных и коротких позиций указано нулевым. Это результат ошибки при определении трейда в запросе из БД. Таблица трейдов создаётся из таблицы сделок. Если позиция была открыта советником (в данном случае с магиком 600), а закрыта вручную, то у сделки открытия будет прописан магик, а у сделки закрытия он будет нулевым. Это видно при просмотре истории сделок:
Здесь для определения трейда берутся сделки закрытия, а у неё магик равен нулю. А по нулевому магику невозможно найти сделку открытия — её нет. Соответственно, для нулевого магика не находятся ни длинные, ни короткие позиции. Значит, при создании таблицы трейдов стоит учитывать такую вероятность, что позиция может быть открыта советником, а закрыта вручную и наоборот. Если это учесть, то таких ошибок уже быть не должно.
Заключение
Сегодня мы поставили себе задачу создать информационную панель, отображающую статистику торговли, а решать мы её должны были на примерах из статей и документации на данном ресурсе. И, как видим, что даже чего-то не зная, всегда несложно найти ответы на свои вопросы, воспользовавшись огромной базой знаний, предлагаемой ресурсом, и сделать вполне работоспособный продукт.
Изучайте предлагаемую сайтом информацию, читайте статьи и документацию, общайтесь с более опытными коллегами, дорабатывайте найденные примеры под свои задачи, и всё обязательно получится!
Все файлы рассмотренных классов, функций и индикатора приложены к статье. Также приложен архив, который можно распаковать в каталог данных терминала, и все нужные файлы будут размещены в папке\MQL5\Indicators\StatisticsBy, их можно сразу скомпилировать и запустить файл индикатора.





- Бесплатные приложения для трейдинга
- 8 000+ сигналов для копирования
- Экономические новости для анализа финансовых рынков
Вы принимаете политику сайта и условия использования
Не хватает инструментария, который позволял бы работать с большой историей торговли.
К сожалению, данный инструментарий просто зависает при запросе истории, как и многие другие.
Пять минут на получение истории. Далее что-либо делать с окном невозможно - полная нагрузка CPU.
Не хватает инструментария, который позволял бы работать с большой историей торговли.
К сожалению, данный инструментарий просто зависает при запросе истории, как и многие другие.
Пять минут на получение истории. Далее что-либо делать с окном невозможно - полная нагрузка CPU.
Можно в личку инвесторский доступ к счëту?
К сожалению, нет такой возможности. Но Вы можете сами создать нечто подобное: на демо-счете скриптом за час асинхронным OrderSend открыть/закрыть нужное количество позиций по разным символам/мэджикам.
На московской бирже работать не хочет
На московской бирже работать не хочет
Естественно. Всё, что связано с позицией, кроме суммарной, при неттинге бесполезно, если работает больше одного робота на одном символе (или робот плюс ручная торговля).