
Recursos do SQLite em MQL5: Exemplo de painel interativo com estatísticas de trading por símbolo e magic
Conteúdo
- Introdução
- Definindo o objetivo
- Painel informativo
- Funções para trabalhar com banco de dados
- Montando o projeto – painel informativo
- Considerações finais
Introdução
A MQL5.com é uma plataforma que oferece aos usuários amplo acesso a diversas informações de referência e materiais educativos sobre algoritmos de negociação. Apesar de sua extensa base de dados e recursos, alguns usuários ainda enfrentam dificuldades para encontrar soluções específicas ou adaptar exemplos às suas necessidades. Para esses casos, a plataforma permite a interação com a comunidade por meio do fórum, onde é possível obter dicas úteis ou até mesmo soluções prontas.
Este artigo tem como objetivo demonstrar e resolver uma das tarefas mais comuns: a criação de um painel informativo para exibir o histórico e as estatísticas de trading de uma conta. Vamos analisar o processo de desenvolvimento deste painel usando exclusivamente os materiais existentes do mql5.com, o que é interessante não apenas pela solução prática, mas também como exemplo de aplicação de conteúdos didáticos e de referência em situações reais.
Definindo o objetivo
Precisamos criar um painel informativo no qual seja possível exibir, sob demanda, informações sobre o histórico de trading da conta e estatísticas de negociação por símbolo e magic (EAs que operam na conta). Também deve ser possível exibir as estatísticas completas de trading da conta. O histórico de operações deve ser ordenado pelo tempo de execução das operações. As estatísticas por símbolos e magics devem ser ordenadas pelo lucro líquido (Net Profit).
O tipo de programa será um indicador. Não exibiremos nenhum dado nos buffers. Ou seja, será um indicador sem buffers, cujo espaço de trabalho principal será um painel gráfico. O painel será visualmente dividido em duas áreas — à esquerda, serão exibidas listas de todos os símbolos e magics nos quais houve trading na conta, à direita, serão exibidas tabelas com estatísticas por símbolo e magic em formato de lista, estatísticas consolidadas de um único símbolo ou magic selecionado ou, ainda, de toda a conta. O controle do conteúdo exibido será feito por meio de botões no painel ou clicando sobre a linha da estatística do símbolo/magic na tabela à direita.
No artigo "Criamos um painel informativo para exibir dados em indicadores e EAs" utilizaremos o painel informativo.
O artigo "SQLite: uso nativo de bancos de dados com SQL em MQL5" nos ajudará a obter estatísticas da conta, bem como estatísticas por símbolo e magic (estratégias de negociação).
Painel informativo
Desde a primeira publicação do artigo sobre a criação do painel informativo, o código passou por algumas modificações e melhorias. Detalhar todas as alterações feitas no código não é o objetivo deste artigo. Por isso, faremos apenas uma breve visão geral dessas mudanças. Para ver exatamente o que foi ajustado, basta baixar a primeira versão do painel mencionada naquele artigo e compará-la com o código do painel incluído neste artigo.
Foram corrigidos erros de posicionamento e exibição do painel que ocorriam ao alternar gráficos em determinadas situações. Agora é possível anexar painéis filhos ao principal. Especificamente, esses painéis filhos podem ser transformados em botões ao serem minimizados, deixando visível apenas o cabeçalho, que funcionará como botão. As restrições de posicionamento das tabelas dentro do painel foram removidas. Originalmente, não era possível desenhar a tabela fora da área visível do painel. Em alguns casos, no entanto, as tabelas desenhadas no painel precisam ser posicionadas fora dessa área. Isso é necessário para permitir a rolagem de tabelas longas ou largas, cujas dimensões excedem o tamanho do painel. Assim, se permitirmos que as coordenadas iniciais da tabela fiquem fora do painel, será possível implementar a rolagem da tabela. É exatamente isso que faremos hoje. No entanto, não vamos alterar as classes do painel e de suas tabelas (embora isso fosse o mais correto do ponto de vista da reutilização do painel aprimorado), justamente para mostrar que, mesmo com exemplos cujo funcionamento não seja totalmente adequado, é possível atingir a funcionalidade desejada com a abordagem correta.
Vamos ver brevemente o que foi aprimorado no código das classes das tabelas e do painel.
Foi adicionada à classe da célula da tabela uma variável para armazenar o texto da célula e métodos para trabalhar com essa variável:
//+------------------------------------------------------------------+ //| Класс ячейки таблицы | //+------------------------------------------------------------------+ 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){} };
Na classe dos dados da tabela, agora o identificador pode ter um valor negativo, assim como as coordenadas da tabela também podem ser negativas. Foi adicionado um método para definir o identificador da tabela:
//+------------------------------------------------------------------+ //| Класс данных таблиц | //+------------------------------------------------------------------+ 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); }
Também foi incluído um construtor padrão:
//--- Конструктор/деструктор 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(); }
O construtor padrão permitirá declarar um objeto da classe sem a necessidade de criá-lo usando o operador new, e os valores de coordenadas do tipo int possibilitarão definir as coordenadas iniciais das tabelas fora da janela do painel.
Na classe do objeto painel foram declaradas novas variáveis<1>:
//+------------------------------------------------------------------+ //| Класс 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; // Наименование панели
Como agora é possível vincular painéis filhos ao painel principal, alguns métodos de manipulação do painel foram movidos da seção protegida para a seção pública — é necessário acessá-los externamente — e também foram adicionados novos métodos<2>:
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; } //--- Обработчик событий
Todos os novos métodos e melhorias permitem agora anexar painéis filhos ao painel principal e usá-los como objetos independentes, porém dependentes do seu pai. Agora é possível criar tabelas com rolagem, caso o tamanho delas exceda o do próprio painel. Anteriormente, as tabelas só podiam ter tamanho menor do que o do próprio painel. O recurso de rolagem de tabelas não está presente na classe do painel; vamos implementá-lo diretamente no programa principal. Posteriormente, se necessário, esse recurso poderá ser incorporado à classe do painel e às suas tabelas. Porém, nesse momento, isso não é necessário.
Naturalmente, aqui apresentamos apenas uma pequena parte das alterações feitas na classe do painel e suas tabelas — apenas os métodos declarados. As melhorias foram realizadas gradualmente em uma parte bastante extensa do código ao longo do tempo, desde a primeira publicação. Quem quiser pode baixar a primeira versão do painel no artigo original e compará-la com a versão apresentada neste artigo. O arquivo do painel informativo deve estar localizado no diretório do projeto: \MQL5\Indicators\StatisticsBy\Dashboard\Dashboard.mqh.
Funções para trabalhar com banco de dados
Já faz muito tempo que trabalho com bancos de dados, mas apenas superficialmente, em um projeto conjunto para a indústria de jogos com C#, no qual o banco de dados era responsabilidade de outra pessoa, e eu apenas usava o conector fornecido para integrá-lo ao projeto. Por isso, foi necessário recorrer a materiais de referência e artigos no site mql5.com. Ao começar a ler o artigo "SQLite: uso nativo de bancos de dados com SQL em MQL5", logo notei referências à documentação, especialmente à função DatabasePrepare(). Lá há um exemplo no qual é criada uma tabela de operações, e com base nela, uma tabela de trades. É exatamente o que precisamos! Com paciência, vamos estudar o exemplo e suas funções.
Primeiro, vemos duas estruturas para armazenar dados de operações e de trades:
//--- структура для хранения сделки 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; // прибыль или убыток };
Em seguida, analisamos a lógica:
//+------------------------------------------------------------------+ //| 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); }
- É criado um banco de dados,
- é criada uma tabela de operações no banco de dados,
- o histórico de operações é requisitado, e as operações são inseridas na tabela criada,
- o tipo de conta é verificado. Deve ser do tipo hedge, pois no modo netting, com base apenas nas operações, não é possível construir o histórico de negociação,
- é criada uma tabela de trades com base na tabela de operações, e os dados de trades são inseridos nela com base nas informações da tabela de operações.
O script apresentado também imprime no log as dez primeiras operações e os dez primeiros trades das tabelas criadas. Isso não é necessário para nós.
Com base na lógica, precisamos criar algumas funções inspiradas naquelas apresentadas no exemplo, bem como aproveitar trechos de código presentes no corpo do script exemplo:
//+------------------------------------------------------------------+ //| Создает таблицу 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); } //+------------------------------------------------------------------+
Além dos campos das estruturas e tabelas mostrados no exemplo, precisamos de um campo adicional que armazene o número da conta — isso será necessário para a criação da tabela de estatísticas de trading da conta. Em outras palavras, para a geração da estatística completa da conta.
Mas como vamos criar as tabelas de estatísticas? Tudo bem, continuamos lendo o artigo e encontramos! Aqui está exatamente o que precisamos:
Análise de portfólio por estratégia
Nos resultados apresentados pelo script de DatabasePrepare, vemos que a negociação é realizada em vários pares de moedas. Mas além disso, vemos também, na coluna [magic], valores que vão de 100 a 600. Isso indica que várias estratégias de negociação atuam na conta, cada uma com seu próprio Magic Number para identificar suas operações.
Com uma consulta SQL, podemos analisar a negociação com base nos valores de 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");
Resultado:
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
Vemos que 4 das 6 estratégias apresentaram lucro. E para cada estratégia obtivemos os seguintes indicadores estatísticos:
- trades — quantidade de trades por estratégia,
- gross_profit — lucro bruto por estratégia (soma de todos os valores positivos de profit),
- gross_loss — prejuízo bruto por estratégia (soma de todos os valores negativos de profit),
- total_commission — soma de todas as comissões dos trades da estratégia,
- total_swap — soma de todos os swaps dos trades da estratégia,
- total_profit — soma de gross_profit e gross_loss,
- net_profit — soma (gross_profit + gross_loss + total_commission + total_swap),
- win_trades — quantidade de trades com profit>0,
- loss_trades — quantidade de trades com profit<0,
- expected_payoff — expectativa matemática do trade, sem considerar swaps e comissões = net_profit/trades,
- win_percent — percentual de trades vencedores,
- loss_percent — percentual de trades perdedores,
- average_profit — lucro médio = gross_profit/win_trades,
- average_loss — prejuízo médio = gross_loss/loss_trades,
- profit_factor — fator de lucro = gross_profit/gross_loss.
Essas estatísticas não consideram os swaps e as comissões no cálculo do lucro e prejuízo, permitindo visualizar esses custos de forma isolada. Pode acontecer de uma estratégia ser lucrativa por si só,
mas acabar sendo desvantajosa devido aos swaps e comissões.
Podemos realizar uma análise de trading por símbolo. Para isso, fazemos a seguinte consulta:
//--- получим торговую статистику в разрезе символов 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");
Resultado:
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
As estatísticas mostram que apenas em 5 dos 10 símbolos foi obtido lucro líquido (net_profit>0), embora em 6 dos 10 o fator de lucro tenha sido positivo (profit_factor>1). Esse é justamente o caso em que swaps e comissões tornam a estratégia perdedora no par EURJPY.
Continuando a leitura do artigo, encontramos:
Excelente! Acessamos o link na documentação e obtemos o código completo do exemplo relacionado a essa função:
//--- статистика по символу 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); } //+------------------------------------------------------------------+
Ele pode ser copiado para o editor, compilado e, ao ser executado, permite visualizar o resultado no log.
Agora temos exemplos práticos de como trabalhar com o banco de dados para alcançar o resultado desejado. Será necessário apenas fazer alguns ajustes nos códigos fornecidos na documentação. Por exemplo, podemos querer que os dados sejam ordenados por algum campo específico ou obter apenas valores únicos das tabelas. Para isso, vale a pena consultar a documentação do SQL. Com o conhecimento adquirido e os exemplos em mãos, conseguiremos adaptar tudo conforme as necessidades do projeto.
Vamos criar no diretório do terminal \MQL5Indicators uma nova pasta StatisticsBy. Nela ficarão todos os arquivos deste projeto.
Na pasta criada, vamos criar um novo arquivo SQLiteFunc.mqh e começar a preenchê-lo com funções para trabalhar com o banco de dados.
Em primeiro lugar, vamos escrever as estruturas necessárias:
//+------------------------------------------------------------------+ //| 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; // профит-фактор };
As estruturas foram copiadas dos exemplos apresentados anteriormente na Documentação, mas receberam novos nomes e foram adicionados campos para armazenar o número da conta e a quantidade de posições compradas e vendidas, permitindo realizar filtros no banco de dados com base nesses dados.
Precisamos do histórico de operações para criar a tabela de operações no banco de dados. Por isso, a função que obtém esse histórico também será escrita nesse mesmo arquivo:
//+------------------------------------------------------------------+ //| Запрашивает историю сделок за указанный период | //+------------------------------------------------------------------+ 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; }
Neste arquivo também será incluída uma função para excluir do banco de dados uma tabela com o nome especificado:
//+------------------------------------------------------------------+ //| Удаляет из базы таблицу с указанным именем | //+------------------------------------------------------------------+ 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); }
Vamos escrever uma função para criar a tabela de operações:
//+------------------------------------------------------------------+ //| Создает таблицу 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); }
O código da função foi copiado da documentação, mas aqui foi adicionado um campo extra — o número da conta.
De forma análoga, escrevemos a função para criação da tabela de trades, também com o campo do número da conta incluído:
//+------------------------------------------------------------------+ //| Создает таблицу 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); }
Agora, vamos escrever a função para preencher a tabela de operações no banco de dados com os dados coletados:
//+------------------------------------------------------------------+ //| Вносит сделки в таблицу базы данных | //+------------------------------------------------------------------+ 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); }
O código da função foi retirado do exemplo na documentação da função DatabaseExecute(). Aqui foi criada uma variável adicional para armazenar o número da conta, e corrigido o texto da consulta, pois a documentação provavelmente contém um erro: ao compor a string da consulta, os tipos de dados foram declarados como int para campos que, na verdade, têm tipo 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);
Corrigimos isso aqui, adicionamos o campo do número da conta e removemos a hora de entrada da operação, pois neste caso a hora de entrada não é necessária.
Vamos escrever a função para preencher a tabela de trades com base na tabela de operações:
//+------------------------------------------------------------------+ //| Заполняет таблицу 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; }
Assim como nas funções anteriores, aqui também foi adicionado o campo do número da conta.
Agora, escrevemos a função que preenche a lista de todos os trades a partir do banco de dados:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех трейдов | //+------------------------------------------------------------------+ 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; }
Aqui adicionamos a ordenação da lista de trades por ordem decrescente do horário de entrada no mercado. Se não fizermos isso, o último trade da lista aparecerá no final, o que significa que ele será exibido na parte inferior da tabela do painel. Isso é inconveniente. Ao ordenar de forma decrescente, o último trade será colocado no topo da tabela — na parte superior do painel — e os trades mais recentes estarão visíveis de imediato, sem a necessidade de rolar toda a lista.
Vamos escrever a função que preenche, a partir do banco de dados, a lista de todos os símbolos nos quais houve trading:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех символов | //+------------------------------------------------------------------+ 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; }
Para obter uma lista contendo apenas os nomes de símbolos únicos e não repetidos, é necessário usar a palavra-chave "DISTINCT", e o ideal é que essa lista seja retornada em ordem alfabética.
De forma semelhante, escrevemos a função que preenche a partir do banco de dados a lista de todos os magics em ordem crescente:
//+------------------------------------------------------------------+ //| Заполняет из БД список всех магиков | //+------------------------------------------------------------------+ 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; }
Criamos agora uma função que obtém, do banco de dados, e armazena em um array as estatísticas de trading por símbolo:
//+-------------------------------------------------------------------+ //|Получает из БД и сохраняет в массив статистику торговли по символам| //+-------------------------------------------------------------------+ 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; }
Aqui foram adicionadas linhas para contabilizar posições compradas e vendidas, e obtemos uma lista ordenada por lucro líquido em ordem decrescente — ou seja, os símbolos mais lucrativos aparecem no início da tabela.
De forma análoga, escrevemos uma função para obter do banco de dados e armazenar em um array as estatísticas de trading por magic:
//+------------------------------------------------------------------+ //|Получает из БД и сохраняет в массив статистику торговли по магикам| //+------------------------------------------------------------------+ 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; }
Por fim, escrevemos uma função semelhante, que obtém do banco de dados e armazena em um array as estatísticas de trading da conta:
//+------------------------------------------------------------------+ //| Получает из БД и сохраняет в массив статистику торговли по счёту | //+------------------------------------------------------------------+ 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; }
Agora, temos todos os elementos básicos do projeto: selecionamos o painel e criamos funções para manipulação do banco de dados com base nas informações da documentação. Agora falta apenas implementar a lógica de interação entre o painel, suas tabelas e o banco de dados, usando as funções que escrevemos. É bem possível que as listas exibidas nas tabelas do painel sejam bastante longas, o que fará com que o tamanho delas ultrapasse os limites do painel. Nesses casos, será necessário implementar rolagem vertical e horizontal. Esse recurso será implementado diretamente no indicador que estamos criando: as tabelas serão roladas verticalmente com a rotação do scroll do mouse, e horizontalmente com o scroll do mouse enquanto a tecla Shift estiver pressionada.
A estatística de um símbolo ou magic selecionado será exibida ao clicar sobre a linha correspondente na tabela de estatísticas. Para isso, faremos o rastreamento da posição do cursor sobre as linhas da tabela e detectaremos os cliques nessas linhas. O ideal seria que esse recurso estivesse na própria classe do painel, permitindo sua reutilização em outros projetos. Mas aqui vamos mostrar como fazer tudo isso sem precisar modificar as classes do painel.
Montando o projeto — painel informativo
Na pasta anteriormente criada \MQL5\IndicatorsStatisticsBy, vamos criar o arquivo de um novo indicador com o nome StatisticsBy.mq5.
Incluímos os arquivos das classes de tabelas e do painel, além do arquivo de funções para manipulação do banco de dados, e especificamos que o indicador não possui buffers desenháveis:
//+------------------------------------------------------------------+ //| 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"
Em seguida, inserimos as macros, o array com a disposição das colunas das tabelas de estatísticas, os parâmetros de entrada e as variáveis globais:
#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; // Указатель на экземпляр панели
Para indicar a quantidade de colunas das tabelas de estatísticas e definir a disposição e quantidade de dados nas tabelas, é conveniente usar um array contendo constantes com os nomes dos cabeçalhos das tabelas e, por consequência, os dados sob esses cabeçalhos. Se for necessário alterar a ordem de exibição dos dados, basta reorganizar sua declaração nesse array e recompilar o indicador. Também é possível remover dados desnecessários comentando-os no array, ou adicionar novos dados. No entanto, ao adicionar novos dados, será necessário incluí-los nas funções de manipulação do banco de dados e em outras funções responsáveis por calcular e exibir os dados nas tabelas.
Vamos analisar o manipulador OnInit() do indicador, onde o banco de dados é criado e o painel com seu conteúdo gráfico é montado:
//+------------------------------------------------------------------+ //| 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); }
Basicamente, aqui primeiro é verificado o tipo de contabilização de posições e, se for netting, o indicador é encerrado, pois nesse tipo de conta não é possível criar uma tabela de trades de forma simples, utilizando apenas as operações de entrada e saída.
Depois, no diretório de dados do terminal (TERMINAL_DATA_PATH + \MQL5\Files), é criado o banco de dados dentro de uma subpasta com o nome do programa, no subdiretório Database (\StatisticsBy\Database). Após a criação bem-sucedida do banco de dados, o painel é criado e preenchido com conteúdo, como botões de controle e painéis para exibição das tabelas:
O interessante aqui é que, em vez de botões convencionais, usamos painéis filhos anexados à janela principal em estado minimizado — apenas o cabeçalho do painel fica visível. Esse cabeçalho tem seus próprios manipuladores para interagir com o cursor do mouse e, dessa forma, transformamos painéis comuns em botões que interagem de maneira interativa com o usuário e enviam eventos de interação com o mouse ao programa principal.
No manipulador OnDeinit(), fechamos o banco de dados e o painel:
//+------------------------------------------------------------------+ //| 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(); } }
O manipulador OnCalculate() do indicador permanecerá vazio (o indicador não realiza nenhum cálculo):
//+------------------------------------------------------------------+ //| 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); }
Toda a lógica de interação interativa entre o painel e o usuário será tratada no manipulador de eventos do indicador.
Vamos analisar o manipulador de eventos OnChartEvent() por completo. Seu código está comentado em detalhes. Com uma leitura atenta dos comentários dentro do manipulador, toda a lógica de interação entre o painel e o usuário se torna clara:
//+------------------------------------------------------------------+ //| 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; } } }
Agora vamos examinar as demais funções chamadas a partir do manipulador de eventos. O código de cada função está comentado de forma detalhada, de modo que não deve gerar dúvidas.
Função que retorna o índice da linha da tabela com base nas coordenadas do cursor:
//+------------------------------------------------------------------+ //| Возвращает индекс строки таблицы по координатам курсора | //+------------------------------------------------------------------+ 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; }
Essa função realiza duas tarefas ao mesmo tempo: (1) retorna o número da linha da tabela sobre a qual o cursor do mouse está posicionado e (2) destaca essa linha com uma cor de fundo.
Função manipuladora da rolagem do scroll do mouse dentro da tabela no painel da esquerda:
//+------------------------------------------------------------------+ //| Обработчик прокрутки колёсика мышки внутри таблицы левой панели | //+------------------------------------------------------------------+ 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; }
Ao girar o scroll do mouse, se o cursor estiver sobre uma tabela no painel, a tabela também deve ser rolada, caso seu tamanho exceda o tamanho do painel. Essa função faz exatamente isso: desloca a tabela com base nas coordenadas iniciais indicadas. Além disso, a linha sob o cursor é destacada com a função TableSelectRowByMouse(), que também retorna o índice da linha posicionada sob o cursor. Como no painel da esquerda são exibidas listas pequenas de símbolos e magics, a rolagem aqui é simplificada: a tabela é deslocada diretamente para as coordenadas calculadas. Já no painel da direita, a lógica será um pouco mais complexa.
Função manipuladora do movimento do cursor do mouse dentro da tabela do painel da esquerda:
//+------------------------------------------------------------------+ //| Обработчик смещения курсора мышки внутри таблицы левой панели | //+------------------------------------------------------------------+ 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; }
Assim como na função anterior, aqui também é localizada e destacada a linha que está sob o cursor. A tabela, naturalmente, não é rolada.
Função manipuladora da rolagem do scroll do mouse dentro da tabela no painel da direita:
//+------------------------------------------------------------------+ //| Обработчик прокрутки колёсика мышки внутри таблицы правой панели | //+------------------------------------------------------------------+ 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; }
Aqui, uma única função trata simultaneamente três tabelas diferentes, além dos respectivos cabeçalhos. Tudo depende do tipo de tabela passado como argumento. Ao rolar tabelas grandes, é necessário permitir a rolagem tanto na vertical quanto na horizontal. A rolagem horizontal é controlada por um sinalizador chamado shift_flag , que indica se a tecla Shift está pressionada durante a rolagem do scroll do mouse. Quando a tabela é rolada, o cabeçalho correspondente, que está em outro painel, também é rolado simultaneamente.
Função manipuladora do movimento do cursor do mouse dentro da tabela no painel da direita:
//+------------------------------------------------------------------+ //| Обработчик смещения курсора мышки внутри таблицы правой панели | //+------------------------------------------------------------------+ 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; }
Na verdade, aqui (e também nas funções analisadas anteriormente), tudo o que precisamos é identificar a linha da tabela sobre a qual o cursor está posicionado. Todo o restante está relacionado à parte visual do destaque da linha sob o cursor, o que aumenta o consumo de recursos do processador. Pois precisamos redesenhar constantemente toda a parte visível da tabela, e quanto maiores forem o painel e a tabela, maior será a área a ser redesenhada. Embora aqui (e mais adiante) o espaço desenhável esteja fisicamente limitado pelo tamanho do painel, esse processo não é ideal. Se estivéssemos implementando esse destaque de linha diretamente na classe do painel, seria diferente: apenas as linhas próximas ao cursor seriam redesenhadas, e a cor de fundo seria memorizada antes e depois do destaque, sendo restaurada em seguida. No entanto, como estamos apenas apresentando um exemplo, vamos implementar tudo diretamente nas funções do programa, sem complicar demais.
Função manipuladora do clique do mouse dentro da tabela no painel:
//+------------------------------------------------------------------+ //| Обработчик щелчка мышки внутри таблицы на панели | //+------------------------------------------------------------------+ 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; }
Ao clicar com o mouse sobre uma linha da tabela, é necessário localizar e retornar o número da linha onde ocorreu o clique, para que possamos tratá-lo posteriormente. Isso, aliás, já foi mostrado no manipulador de eventos do usuário dentro de OnChartEvent().
Função que preenche a tabela com os nomes dos símbolos:
//+------------------------------------------------------------------+ //| Заполняет таблицу наименованиями символов | //+------------------------------------------------------------------+ 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); } }
Aqui, além de garantir que as áreas da tabela que extrapolam os limites do painel não sejam desenhadas, limitamos também o início do laço à linha da tabela que está visível no topo. Explicando melhor: ao rolar a tabela para cima, a primeira linha pode acabar saindo completamente da área visível do painel. E, para evitar processar linhas que não serão exibidas de qualquer forma, calculamos o índice da primeira linha visível no topo da tabela e iniciamos o laço a partir daí. Com tabelas suficientemente grandes, esse método traz um ganho perceptível de desempenho, eliminando travamentos durante a rolagem de tabelas com centenas de linhas.
Função que preenche a tabela com os valores dos magics:
//+------------------------------------------------------------------+ //| Заполняет таблицу значениями магиков | //+------------------------------------------------------------------+ 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); } }
Função que preenche a tabela com os cabeçalhos dos trades:
//+------------------------------------------------------------------+ //| Заполняет таблицу заголовков трейдов | //+------------------------------------------------------------------+ 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); } }
Função que preenche a tabela com os dados dos trades:
//+------------------------------------------------------------------+ //| Заполняет таблицу трейдов | //+------------------------------------------------------------------+ 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); } } }
Na verdade, essas duas funções desenham uma única tabela. Uma delas desenha o cabeçalho da tabela com os nomes das colunas, dependendo do índice da coluna, enquanto a outra desenha, logo abaixo, a tabela com os valores correspondentes a esses cabeçalhos. Em resumo, são funções que atuam em conjunto.
Função que preenche a tabela com os cabeçalhos da estatística de trading:
//+------------------------------------------------------------------+ //| Заполняет таблицу заголовков статистики торговли | //+------------------------------------------------------------------+ 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); } }
Ao exibir a tabela estatística por símbolos ou por magics, é necessário desenhar o cabeçalho correspondente. Como temos duas tabelas de estatísticas — uma por símbolos, outra por magics — os cabeçalhos dessas tabelas diferem apenas na primeira coluna; todas as demais colunas são idênticas. Aqui é feita uma verificação do identificador da tabela e, com base nele, o cabeçalho da primeira coluna é preenchido com o nome do símbolo ou do magic, conforme apropriado.
Função que preenche a tabela com a estatística de trading por símbolos:
//+------------------------------------------------------------------+ //| Заполняет таблицу статистики торговли по символам | //+------------------------------------------------------------------+ 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); } } }
Função que preenche a tabela com a estatística de trading por magics:
//+------------------------------------------------------------------+ //| Заполняет таблицу статистики торговли по магикам | //+------------------------------------------------------------------+ 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); } } }
Duas funções semelhantes são responsáveis por preencher as tabelas de estatísticas por símbolos e por magics.
Função que retorna dados da estrutura com base no tipo de cabeçalho:
//+------------------------------------------------------------------+ //| Возвращает данные из структуры по типу заголовка | //+------------------------------------------------------------------+ 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; } }
A função recebe como parâmetros o tipo da tabela de estatísticas (símbolo, magic ou conta), o tipo de dado (correspondente ao cabeçalho da coluna) e o índice dos dados no array de estruturas. Com base nessas informações, é retornado o valor correspondente do array de estruturas. Para simplificar, o valor é sempre retornado como tipo double, mesmo que, na estrutura original, o tipo seja inteiro. Na função seguinte, esse valor será convertido em uma string com a quantidade necessária de casas decimais.
Função que retorna dados da estrutura com base no tipo de cabeçalho no formato string:
//+------------------------------------------------------------------+ //| Возвращает данные из структуры по типу заголовка в виде строки | //+------------------------------------------------------------------+ 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)); }
Essas funções são utilizadas para exibir os valores das células das tabelas de estatísticas nas tabelas visíveis no painel.
Função que retorna o índice de um símbolo no array de estatísticas por símbolos:
//+------------------------------------------------------------------+ //| Возвращает индекс символа в массиве статистики по символам | //+------------------------------------------------------------------+ 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; }
Função que retorna o índice de um símbolo no array de estatísticas por magics:
//+------------------------------------------------------------------+ //| Возвращает индекс магика в массиве статистики по магикам | //+------------------------------------------------------------------+ 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; }
Ambas as funções retornam o índice do símbolo ou do magic buscado dentro do respectivo array de estatísticas.
Função que exibe no painel a estatística consolidada para o símbolo, magic ou conta selecionado:
//+------------------------------------------------------------------+ //| Отображает статистику по выбранному символу, магику или счёту | //+------------------------------------------------------------------+ 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; }
Essa função é responsável por desenhar a tabela de estatísticas consolidadas para o símbolo, magic ou conta selecionado. Ela recebe como parâmetros o tipo de tabela de estatísticas e o nome do símbolo, o valor em string do magic ou o número da conta. A partir do texto fornecido, determina-se o índice correspondente no array de estruturas de dados estatísticos. Em seguida, todos os dados da estrutura referente a esse índice são obtidos e posicionados na tabela desenhada, de acordo com as coordenadas das células. Os deslocamentos horizontais dos textos exibidos são calculados de forma que o título do dado fique alinhado à esquerda da célula e o valor correspondente fique alinhado à direita da sua célula. Todos os dados são exibidos em quatro colunas, organizados visualmente no painel como dois pares “cabeçalho: valor”.
Vamos compilar o indicador e observar o que obtivemos:
E então, vemos que toda a funcionalidade prevista está operando conforme esperado. Sim, há pequenos “piscares” de texto nas tabelas ao mover o cursor e rolar a tela. No entanto, isso é resultado de um esquema de redesenho não otimizado: toda a parte visível da tabela é redesenhada continuamente. Esse efeito poderia ser evitado com uma lógica mais sofisticada para tratar apenas as linhas sob o cursor, o que, no entanto, foge ao escopo deste artigo.
As tabelas podem ser roladas verticalmente girando o scroll do mouse, e horizontalmente girando o scroll com a tecla Shift pressionada. Ao observar atentamente o vídeo, é possível notar que, ao exibir a estatística para o magic com valor 0, a quantidade de posições compradas e vendidas aparece como zero. Isso é resultado de um erro na definição do trade na consulta ao banco de dados. A tabela de trades é criada com base na tabela de operações. Se uma posição foi aberta por um EA (neste caso, com magic 600) e fechada manualmente, a operação de abertura terá o magic definido, mas a operação de fechamento aparecerá com magic igual a zero. Isso pode ser visto ao analisar o histórico de operações:
Nesse caso, o trade é determinado com base na operação de fechamento, e como ela tem o magic igual a zero, não é possível localizar a operação de abertura correspondente — ela simplesmente não será encontrada. Consequentemente, para o magic zero, não serão registradas posições compradas nem vendidas. Isso significa que, ao criar a tabela de trades, deve-se considerar a possibilidade de que uma posição tenha sido aberta por um EA e fechada manualmente, ou o contrário. Se essa situação for levada em conta, tais erros deixarão de ocorrer. Considerações finais
Considerações finais
E, como foi possível ver, mesmo sem saber tudo de antemão, é sempre fácil encontrar as respostas necessárias consultando a vasta base de conhecimento oferecida pela plataforma, e assim criar um produto totalmente funcional. Aprofunde-se no conteúdo oferecido pelo site, leia os artigos e a documentação, interaja com colegas mais experientes e adapte os exemplos encontrados às suas necessidades.
Com certeza você terá sucesso!
Todos os arquivos dos classes, funções e do indicador abordados estão anexados ao artigo. Também está incluído um arquivo compactado que pode ser extraído diretamente na pasta de dados do terminal, e todos os arquivos necessários serão colocados automaticamente na pasta \MQL5\Indicators\StatisticsBy, podendo ser compilados e executados de imediato.
Traduzido do russo pela MetaQuotes Ltd.
Artigo original: https://www.mql5.com/ru/articles/16233





- Aplicativos de negociação gratuitos
- 8 000+ sinais para cópia
- Notícias econômicas para análise dos mercados financeiros
Você concorda com a política do site e com os termos de uso