Торговый эксперт с графическим интерфейсом: Наполнение функционалом (Часть II)

Anatoli Kazharski | 15 мая, 2018

Содержание

Введение

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

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

Для начала нужно обеспечить получение данных по символам и индикаторам. Соберем в таблицу форекс-символы, опираясь на значения фильтра в поле ввода Symbols filter. За это отвечает метод CProgram::GetSymbols().

В начале метода обозначим в индикаторе выполнения, что сейчас идёт процесс получения символов. Изначально неизвестно, сколько будет символов. Поэтому полосу индикатора установим на 50 процентов. Далее освобождаем массив символов. Во время работы с приложением может понадобиться сформировать другой список символов, поэтому это нужно делать каждый раз при вызове метода CProgram::GetSymbols().

Фильтр в поле ввода Symbols filter будет использоваться, только если его чек-бокс включен и в поле ввода есть какие-то текстовые обозначения, введённые через запятую. Если эти условия соблюдены, то эти текстовые обозначения получаем в массив как отдельные элементы, чтобы затем можно было использовать их для поиска нужных символов. На всякий случай очищаем края каждого элемента от специальных символов.

Следующий этап — цикл сбора форекс-символов. Он будет проходить по полному списку всех символов, имеющихся на сервере. В начале каждой итерации получаем имя символа и убираем его из окна «Обзор рынка». Таким образом списки в графическом интерфейсе программы и в этом окне будут совпадать. Далее проверим, относится ли полученный символ к категории форекс-символов. Если для работы нужны все символы, то просто закомментируйте это условие или удалите его. В рамках этой статьи мы будем работать только с форекс-символами.

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

Если не найдено ни одного символа, то в массив будет добавлен только текущий символ главного графика. После этого все символы, добавленные в массив, делаются видимыми в окне «Обзор рынка». 

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Символы для торговли
   string            m_symbols[];
   //---
private:
   //--- Получает символы
   void              GetSymbols(void);
  };
//+------------------------------------------------------------------+
//| Получает символы                                                 |
//+------------------------------------------------------------------+
void CProgram::GetSymbols(void)
  {
   m_progress_bar.LabelText("Get symbols...");
   m_progress_bar.Update(1,2);
   ::Sleep(5);
//--- Освободить массив символов
   ::ArrayFree(m_symbols);
//--- Массив элементов строк
   string elements[];
//--- Фильтр названий символов
   if(m_symb_filter.IsPressed())
     {
      string text=m_symb_filter.GetValue();
      if(text!="")
        {
         ushort sep=::StringGetCharacter(",",0);
         ::StringSplit(text,sep,elements);
         //---
         int elements_total=::ArraySize(elements);
         for(int e=0; e<elements_total; e++)
           {
            //--- Чистка по краям
            ::StringTrimLeft(elements[e]);
            ::StringTrimRight(elements[e]);
           }
        }
     }
//--- Собираем массив форекс-символов
   int symbols_total=::SymbolsTotal(false);
   for(int i=0; i<symbols_total; i++)
     {
      //--- Получим имя символа
      string symbol_name=::SymbolName(i,false);
      //--- Скроем его в окне ‘Обзор рынка’
      ::SymbolSelect(symbol_name,false);
      //--- Если не форекс-символ, перейти к следующему
      if(::SymbolInfoInteger(symbol_name,SYMBOL_TRADE_CALC_MODE)!=SYMBOL_CALC_MODE_FOREX)
         continue;
      //--- Фильтр названий символов
      if(m_symb_filter.IsPressed())
        {
         bool check=false;
         int elements_total=::ArraySize(elements);
         for(int e=0; e<elements_total; e++)
           {
            //--- Ищем совпадение в названии символа
            if(::StringFind(symbol_name,elements[e])>-1)
              {
               check=true;
               break;
              }
           }
         //--- Перейти к следующему, если не пропускает фильтр
         if(!check)
            continue;
        }
      //--- Сохраним символ в массив
      int array_size=::ArraySize(m_symbols);
      ::ArrayResize(m_symbols,array_size+1);
      m_symbols[array_size]=symbol_name;
     }
//--- Если массив пустой, установим текущий символ по умолчанию
   int array_size=::ArraySize(m_symbols);
   if(array_size<1)
     {
      ::ArrayResize(m_symbols,array_size+1);
      m_symbols[array_size]=::Symbol();
     }
//--- Покажем в окне Обзор рынка
   int selected_symbols_total=::ArraySize(m_symbols);
   for(int i=0; i<selected_symbols_total; i++)
      ::SymbolSelect(m_symbols[i],true);
  }

Теперь рассмотрим получение хэндлов индикаторов по всем выбранным символам с помощью метода CProgram::GetHandles(). Сначала массиву хэндлов устанавливаем такой же размер, как и у массива символов. Хэндлы будем получать с таким же таймфреймом, как это указано в комбо-боксе Timeframes. Так как из комбо-бокса можно получить строковое значение, то затем его нужно преобразовать в соответствующий тип (ENUM_TIMEFRAMES). В цикле заполняем массив хэндлов. В данном случае это индикатор Stochastic со значениями по умолчанию. На каждой итерации обновляем индикатор прогресса. В конце метода запоминаем первый индекс хэндла графика, который будет отображаться.

class CProgram : public CWndEvents
  {
private:
   //--- Хэндлы индикатора
   int               m_handles[];
   //--- Индекс хэндла текущего графика
   int               m_current_handle_index;
   //---
private:
   //--- Получает хэндлы
   void              GetHandles(void);
  };
//+------------------------------------------------------------------+
//| Получает хэндлы индикатора для всех символов                     |
//+------------------------------------------------------------------+
void CProgram::GetHandles(void)
  {
//--- Установить размер массиву хэндлов
   int symbols_total=::ArraySize(m_symbols);
   ::ArrayResize(m_handles,symbols_total);
//--- Получим значение из выпадающего списка комбо-бокса
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Пройдёмся по списку символов
   for(int i=0; i<symbols_total; i++)
     {
      //--- Получим хэндл индикатора
      m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),5,3,3,MODE_SMA,STO_LOWHIGH);
      //--- Индикатор выполнения
      m_progress_bar.LabelText("Get handles: "+string(symbols_total)+"/"+string(i)+" ["+m_symbols[i]+"] "+((m_handles[i]!=WRONG_VALUE)? "ok" : "wrong")+"...");
      m_progress_bar.Update(i,symbols_total);
      ::Sleep(5);
     }
//--- Запомнить первый индекс хэндла графика
   m_current_handle_index=0;
  }

Значения индикаторов получаем с помощью метода CProgram::GetIndicatorValues(). Опишем его алгоритм. Сначала установим размер массива для значений индикатора, равный размеру массива хэндлов. В главном цикле проходимся по массиву хэндлов и на каждой итерации делаем пять попыток получить данные индикатора. На всякий случай проверяем валидность хэндла, и если он не был получен ранее, пробуем получить его снова. В конце главного цикла обновляем индикатор выполнения, чтобы видеть, на каком этапе сейчас находится программа.

class CProgram : public CWndEvents
  {
private:
   //--- Значения индикатора
   double            m_values[];
   //---
private:
   //--- Получает значения индикаторов на всех символах
   void              GetIndicatorValues(void);
  };
//+------------------------------------------------------------------+
//| Получает значения индикаторов на всех символах                   |
//+------------------------------------------------------------------+
void CProgram::GetIndicatorValues(void)
  {
//--- Установим размер
   int handles_total=::ArraySize(m_handles);
   ::ArrayResize(m_values,handles_total);
//--- Получим значение из выпадающего списка комбо-бокса
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Получаем данные индикаторов для всех символов в списке
   for(int i=0; i<handles_total; i++)
     {
      //--- Сделаем 5 попыток получить данные
      int attempts=0;
      int received=0;
      while(attempts<5)
        {
         //--- Если невалидный хендл, попробуем получить его ещё раз
         if(m_handles[i]==WRONG_VALUE)
           {
            //--- Получим хендл индикатора
            m_handles[i]=::iStochastic(m_symbols[i],StringToTimeframe(tf),5,3,3,MODE_SMA,STO_LOWHIGH);
            continue;
           }
         //--- Попробуем получить значения индикатора
         double values[1];
         received=::CopyBuffer(m_handles[i],1,0,1,values);
         if(received>0)
           {
            //--- Сохраним значение
            m_values[i]=values[0];
            break;
           }
         //--- Увеличим счётчик
         attempts++;
         ::Sleep(100);
        }
      //--- Индикатор выполнения
      m_progress_bar.LabelText("Get values: "+string(handles_total)+"/"+string(i)+" ["+m_symbols[i]+"] "+((received>0)? "ok" : "wrong")+"...");
      m_progress_bar.Update(i,handles_total);
      ::Sleep(5);
     }
  }

После того, как сформирован список символов и получены данные индикаторов, нужно добавить значения этих массивов в таблицу на вкладке Trade. Эту задачу решает метод CProgram::RebuildingTables(). Количество символов может измениться. Поэтому при каждом вызове этого метода таблица полностью перестраивается.

Сначала из нее удаляются все строки, кроме одной — резервной. Далее в таблицу снова добавляются строки по количеству символов. Затем проходимся по ним в цикле и добавляем значения, ранее полученные в отдельные массивы. Кроме самих значений, нужно ещё выделить текст цветом, чтобы видеть, какие сигналы уже сформировались по значениям индикатора. Значения ниже минимума индикатора Stochastic будем выделять синим цветом как сигналы на покупку, а значения выше максимума — красным цветом как сигналы на продажу. На каждой итерации, по мере работы программы, будет обновляться индикатор выполнения. В конце метода нужно обновить таблицу и полосы прокрутки.

//+------------------------------------------------------------------+
//| Перестраивает таблицу символов                                   |
//+------------------------------------------------------------------+
void CProgram::RebuildingTables(void)
  {
//--- Удалить все строки
   m_table_symb.DeleteAllRows();
//--- Установим количество строк по количеству символов
   int symbols_total=::ArraySize(m_symbols);
   for(int i=0; i<symbols_total-1; i++)
      m_table_symb.AddRow(i);
//--- Установим значения в первый столбец
   uint rows_total=m_table_symb.RowsTotal();
   for(uint r=0; r<(uint)rows_total; r++)
     {
      //--- Установим значения
      m_table_symb.SetValue(0,r,m_symbols[r]);
      m_table_symb.SetValue(1,r,::DoubleToString(m_values[r],2));
      //--- Установим цвета
      color clr=(m_values[r]>(double)m_up_level.GetValue())? clrRed :(m_values[r]<(double)m_down_level.GetValue())? C'85,170,255' : clrBlack;
      m_table_symb.TextColor(0,r,clr);
      m_table_symb.TextColor(1,r,clr);
      //--- Обновить индикатор выполнения
      m_progress_bar.LabelText("Initialize tables: "+string(rows_total)+"/"+string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(5);
     }
//--- Обновить таблицу
   m_table_symb.Update(true);
   m_table_symb.GetScrollVPointer().Update(true);
   m_table_symb.GetScrollHPointer().Update(true);
  }

Все описанные выше методы вызываются в методе CProgram::RequestData(). В него передаётся единственный аргумент, по значению которого проверяем идентификатор элемента управления — кнопка Request. После этой проверки временно скрываем таблицу и делаем видимым индикатор выполнения. Далее последовательно вызываются все вышеперечисленные методы для получения данных и их внесения в таблицу. Затем скрываем индикатор выполнения, устанавливаем таймфрейм из комбобокса на график и делаем видимыми последние изменения. 

//+------------------------------------------------------------------+
//| Запрос данных                                                    |
//+------------------------------------------------------------------+
bool CProgram::RequestData(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_request.Id())
      return(false);
//--- Скрыть таблицу
   m_table_symb.Hide();
//--- Показать прогресс
   m_progress_bar.Show();
   m_chart.Redraw();
//--- Инициализация графика и таблицы
   GetSymbols();
   GetHandles();
   GetIndicatorValues();
   RebuildingTables();
//--- Скрыть прогресс
   m_progress_bar.Hide();
//--- Получим значение из выпадающего списка комбобокса
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Получим указатель графика по индексу
   m_sub_chart1.GetSubChartPointer(0).Period(StringToTimeframe(tf));
   m_sub_chart1.ResetCharts();
//--- Показать таблицу
   m_table_symb.Show();
   m_chart.Redraw();
   return(true);
  }

Получение данных по открытым позициям

Когда эксперт загружен на график, то нужно сразу определить, есть ли открытые позиции, чтобы отобразить эту информацию в таблице на вкладке Positions. Список всех позиций можно увидеть в окне «Инструменты» на вкладке «Торговля». Чтобы закрыть только одну позицию по символу, нужно нажать на крестик в ячейке таблицы на столбце Profit. Если же позиций на символе несколько (на хеджинговом счете) и нужно закрыть все, то потребуются несколько шагов. В таблице позиций графического интерфейса сделаем так, чтобы в одной строке для каждого символа была совокупная информация по текущему результату, загрузке депозита и средней цене. Кроме этого, добавим возможность закрывать одним нажатием сразу все позиции на указанном символе. 

Сначала рассмотрим метод CProgram::GetPositionsSymbols() для получения списка символов по открытым позициям. В него передается пустой динамический массив, в который и будут получаться символы. Затем в цикле проходим по всем открытым позициям. На каждой итерации получаем название символа позиции и добавляем его в строковую переменную через разделитель «,». Перед тем, как добавить название символа, сначала проверяем, нет ли его уже в этой строке

После завершения цикла и формирования строки символов получим в переданный массив элементы этой строки и вернём количество полученных символов.

//+------------------------------------------------------------------+
//| Получает в массив символы открытых позиций                       |
//+------------------------------------------------------------------+
int CProgram::GetPositionsSymbols(string &symbols_name[])
  {
   string symbols="";
//--- Пройдемся первый раз в цикле и получим символы открытых позиций
   int positions_total=::PositionsTotal();
   for(int i=0; i<positions_total; i++)
     {
      //--- Выберем позицию и получим её символ
      string position_symbol=::PositionGetSymbol(i);
      //--- Если есть название символа
      if(position_symbol=="")
         continue;
      //--- Если такой строки ещё нет, добавим её
      if(::StringFind(symbols,position_symbol,0)==WRONG_VALUE)
         ::StringAdd(symbols,(symbols=="")? position_symbol : ","+position_symbol);
     }
//--- Получим элементы строки по разделителю
   ushort u_sep=::StringGetCharacter(",",0);
   int symbols_total=::StringSplit(symbols,u_sep,symbols_name);
//--- Вернём количество символов
   return(symbols_total);
  }

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

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

//+------------------------------------------------------------------+
//| Количество сделок позиции с указанными свойствами                |
//+------------------------------------------------------------------+
int CProgram::PositionsTotal(const string symbol)
  {
//--- Счетчик позиций
   int pos_counter=0;
//--- Проверим, есть ли позиция с указанными свойствами
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Если не удалось выбрать позицию, перейти к следующей
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Увеличить счётчик
      pos_counter++;
     }
//--- Вернём количество позиций
   return(pos_counter);
  }

Объём позиций можно получить с помощью метода CProgram::PositionsVolumeTotal(). Кроме символа, по которому нужно получить общий объём позиций, в метод также можно передать их тип. Но тип позиций — необязательный аргумент в этом методе. По умолчанию указано значение WRONG_VALUE. Если тип не указан, то эта проверка не используется, а метод вернёт общий объём по всем позициям. 

//+------------------------------------------------------------------+
//| Общий объём позиций с указанными свойствами                      |
//+------------------------------------------------------------------+
double CProgram::PositionsVolumeTotal(const string symbol,const ENUM_POSITION_TYPE type=WRONG_VALUE)
  {
//--- Счетчик объёма
   double volume_counter=0;
//--- Проверим, есть ли позиция с указанными свойствами
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Если не удалось выбрать позицию, перейти к следующей
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Если нужно проверить тип
      if(type!=WRONG_VALUE)
        {
         //--- Если тип не совпадает, перейти к следующей позиции
         if(type!=(ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE))
            continue;
        }
      //--- Суммируем объём
      volume_counter+=::PositionGetDouble(POSITION_VOLUME);
     }
//--- Вернём объём
   return(volume_counter);
  }

Метод CProgram::PositionsFloatingProfitTotal() позволяет получить общую плавающую прибыль позиций по указанному символу. При подсчёте учитывается накопленный своп по позициям. Здесь также можно указать в качестве дополнительного необязательного аргумента тип позиций, по которым нужно получить плавающую прибыль. Таким образом метод становится универсальным. 

//+------------------------------------------------------------------+
//| Общая плавающая прибыль позиций с указанными свойствами          |
//+------------------------------------------------------------------+
double CProgram::PositionsFloatingProfitTotal(const string symbol,const ENUM_POSITION_TYPE type=WRONG_VALUE)
  {
//--- Счетчик текущей прибыли
   double profit_counter=0.0;
//--- Проверим, есть ли позиция с указанными свойствами
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Если не удалось выбрать позицию, перейти к следующей
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Если нужно проверить тип
      if(type!=WRONG_VALUE)
        {
         //--- Если тип не совпадает, перейти к следующей позиции
         if(type!=(ENUM_POSITION_TYPE)::PositionGetInteger(POSITION_TYPE))
            continue;
        }
      //--- Суммируем текущую прибыль + накопленный своп
      profit_counter+=::PositionGetDouble(POSITION_PROFIT)+::PositionGetDouble(POSITION_SWAP);
     }
//--- Вернуть результат
   return(profit_counter);
  }

Среднюю цену рассчитываем методом CProgram::PositionAveragePrice(). В цикле по каждой позиции символа получаем цену и объём. Затем суммируем произведение этих показателей и отдельнообъём позиций. После завершения цикла, чтобы получить среднюю цену позиций указанного символа, нужно разделить сумму произведения цен и объёмов на сумму объёмов. Именно это значение возвращает представленный метод.

//+------------------------------------------------------------------+
//| Средняя цена позиции                                             |
//+------------------------------------------------------------------+
double CProgram::PositionAveragePrice(const string symbol)
  {
//--- Для расчёта средней цены
   double sum_mult    =0.0;
   double sum_volumes =0.0;
//--- Проверим, есть ли позиция с указанными свойствами
   int positions_total=::PositionsTotal();
   for(int i=positions_total-1; i>=0; i--)
     {
      //--- Если не удалось выбрать позицию, перейти к следующей
      if(symbol!=::PositionGetSymbol(i))
         continue;
      //--- Получим цену и объём позиции
      double pos_price  =::PositionGetDouble(POSITION_PRICE_OPEN);
      double pos_volume =::PositionGetDouble(POSITION_VOLUME);
      //--- Суммируем промежуточные показатели
      sum_mult+=(pos_price*pos_volume);
      sum_volumes+=pos_volume;
     }
//--- Предотвращение деления на ноль
   if(sum_volumes<=0)
      return(0.0);
//--- Вернуть среднюю цену
   return(::NormalizeDouble(sum_mult/sum_volumes,(int)::SymbolInfoInteger(symbol,SYMBOL_DIGITS)));
  }

Рассмотрим следующий показатель: загрузку депозита. Для его получения нужно вызвать универсальный метод CProgram::DepositLoad(). В зависимости от переданных аргументов можно получить значения в разных представлениях: в валюте счёта и в процентном выражении. Кроме этого, можно получить общую загрузку депозита по всем открытым позициям либо только по указанному символу. 

У метода четыре аргумента, три из которых необязательные. Если первый аргумент имеет значение false, то метод вернёт значение в валюте депозита. Если же передано значение true, то возвращается значение в процентном выражении относительно свободных средств

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

//+------------------------------------------------------------------+
//| Загрузка депозита                                                |
//+------------------------------------------------------------------+
double CProgram::DepositLoad(const bool percent_mode,const double price=0.0,const string symbol="",const double volume=0.0)
  {
//--- Рассчитаем текущее значение загрузки депозита
   double margin=0.0;
//--- Общая загрузка по счёту
   if(symbol=="" || volume==0.0)
      margin=::AccountInfoDouble(ACCOUNT_MARGIN);
//--- Загрузка по указанному символу
   else
     {
      //--- Получим данные для расчёта маржи
      double leverage         =((double)::AccountInfoInteger(ACCOUNT_LEVERAGE)==0)? 1 : (double)::AccountInfoInteger(ACCOUNT_LEVERAGE);
      double contract_size    =::SymbolInfoDouble(symbol,SYMBOL_TRADE_CONTRACT_SIZE);
      string account_currency =::AccountInfoString(ACCOUNT_CURRENCY);
      string base_currency    =::SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);
      //--- Если валюта торгового счёта такая же, как базовая валюта символа
      if(account_currency==base_currency)
         margin=(volume*contract_size)/leverage;
      else
         margin=(volume*contract_size)/leverage*price;
     }
//--- Получим текущие средства
   double equity=(::AccountInfoDouble(ACCOUNT_EQUITY)==0)? 1 : ::AccountInfoDouble(ACCOUNT_EQUITY);
//--- Вернуть текущую загрузку депозита
   return((!percent_mode)? margin : (margin/equity)*100);
  }

Все методы для получения показателей добавляются в таблицу вызовом метода CProgram::SetValuesToPositionsTable(). В метод нужно передать массив интересующих нас символов. Сначала проверяем, чтобы переданный массив был не меньше количества строк в таблице. Затем в цикле проходимся по всем строкам таблицы, последовательно получая показатели и заполняя ими ячейки таблицы. Кроме самих значений, будем также устанавливать цвет текста: зеленый цвет для положительного результата, красный — для отрицательного, серый — для нулевых. Обратите внимание, что загрузка депозита будет отображаться по каждому символу через наклонную черту («/») в денежном и процентном выражении

//+------------------------------------------------------------------+
//| Установить значения в таблицу позиций                            |
//+------------------------------------------------------------------+
void CProgram::SetValuesToPositionsTable(string &symbols_name[])
  {
//--- Проверка на выход из диапазона
   uint symbols_total =::ArraySize(symbols_name);
   uint rows_total    =m_table_positions.RowsTotal();
   if(symbols_total<rows_total)
      return;
//--- Получим в таблицу показатели
   for(uint r=0; r<rows_total; r++)
     {
      int    positions_total =PositionsTotal(symbols_name[r]);
      double pos_volume      =PositionsVolumeTotal(symbols_name[r]);
      double buy_volume      =PositionsVolumeTotal(symbols_name[r],POSITION_TYPE_BUY);
      double sell_volume     =PositionsVolumeTotal(symbols_name[r],POSITION_TYPE_SELL);
      double pos_profit      =PositionsFloatingProfitTotal(symbols_name[r]);
      double buy_profit      =PositionsFloatingProfitTotal(symbols_name[r],POSITION_TYPE_BUY);
      double sell_profit     =PositionsFloatingProfitTotal(symbols_name[r],POSITION_TYPE_SELL);
      double average_price   =PositionAveragePrice(symbols_name[r]);
      string deposit_load    =::DoubleToString(DepositLoad(false,average_price,symbols_name[r],pos_volume),2)+"/"+
                              ::DoubleToString(DepositLoad(true,average_price,symbols_name[r],pos_volume),2)+"%";
      //--- Установим значения
      m_table_positions.SetValue(0,r,symbols_name[r]);
      m_table_positions.SetValue(1,r,(string)positions_total);
      m_table_positions.SetValue(2,r,::DoubleToString(pos_volume,2));
      m_table_positions.SetValue(3,r,::DoubleToString(buy_volume,2));
      m_table_positions.SetValue(4,r,::DoubleToString(sell_volume,2));
      m_table_positions.SetValue(5,r,::DoubleToString(pos_profit,2));
      m_table_positions.SetValue(6,r,::DoubleToString(buy_profit,2));
      m_table_positions.SetValue(7,r,::DoubleToString(sell_profit,2));
      m_table_positions.SetValue(8,r,deposit_load);
      m_table_positions.SetValue(9,r,::DoubleToString(average_price,(int)::SymbolInfoInteger(symbols_name[r],SYMBOL_DIGITS)));
      //--- Установим цвет
      m_table_positions.TextColor(3,r,(buy_volume>0)? clrBlack : clrLightGray);
      m_table_positions.TextColor(4,r,(sell_volume>0)? clrBlack : clrLightGray);
      m_table_positions.TextColor(5,r,(pos_profit!=0)? (pos_profit>0)? clrGreen : clrRed : clrLightGray);
      m_table_positions.TextColor(6,r,(buy_profit!=0)? (buy_profit>0)? clrGreen : clrRed : clrLightGray);
      m_table_positions.TextColor(7,r,(sell_profit!=0)?(sell_profit>0)? clrGreen : clrRed : clrLightGray);
     }
  }

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

//+------------------------------------------------------------------+
//| Обновляет таблицу позиций                                        |
//+------------------------------------------------------------------+
void CProgram::UpdatePositionsTable(void)
  {
//--- Обновить таблицу
   m_table_positions.Update(true);
   m_table_positions.GetScrollVPointer().Update(true);
   m_table_positions.GetScrollHPointer().Update(true);
  }

Таблицу позиций инициализируем в методе CProgram::InitializePositionsTable(). В нем вызываются все методы, рассмотренные в этом разделе. Сначала получаем в массив символы открытых позиций. Затем нужно подготовить таблицу, поэтому удаляем все строки и добавляем новые по количеству полученных в массив символов. Если открытые позиции есть, то нужно сначала обозначить ячейки первого столбца, как кнопки. Для этого нужно установить соответствующий тип (CELL_BUTTON) и добавить картинку. После этого в ячейки устанавливаются значения, и таблица обновляется.

//+------------------------------------------------------------------+
//| Инициализация таблицы позиций                                    |
//+------------------------------------------------------------------+
#resource "\\Images\\EasyAndFastGUI\\Controls\\close_black.bmp"
//---
void CProgram::InitializePositionsTable(void)
  {
//--- Получим символы открытых позиций
   string symbols_name[];
   int symbols_total=GetPositionsSymbols(symbols_name);
//--- Удалить все строки
   m_table_positions.DeleteAllRows();
//--- Установим количество строк по количеству символов
   for(int i=0; i<symbols_total-1; i++)
      m_table_positions.AddRow(i);
//--- Если есть позиции
   if(symbols_total>0)
     {
      //--- Массив картинок для кнопок
      string button_images[1]={"Images\\EasyAndFastGUI\\Controls\\close_black.bmp"};
      //--- Установим значения в третий столбец
      for(uint r=0; r<(uint)symbols_total; r++)
        {
         //--- Установим тип и картинки
         m_table_positions.CellType(0,r,CELL_BUTTON);
         m_table_positions.SetImages(0,r,button_images);
        }
      //--- Установим значения в таблицу
      SetValuesToPositionsTable(symbols_name);
     }
//--- Обновить таблицу
   UpdatePositionsTable();
  }

Инициализация таблиц данными

Инициализировать таблицу символов и таблицу позиций нужно сразу после создания графического интерфейса программы. Узнать об окончании его формирования можно по пользовательскому событию ON_END_CREATE_GUI в обработчике событий. Для инициализации таблицы символов нужно вызывать метод CProgram::RequestData(), который уже рассматривали ранее . Чтобы метод отработал успешно, нужно передать в него идентификатор элемента кнопки Request.

//+------------------------------------------------------------------+
//| Обработчик событий                                               |
//+------------------------------------------------------------------+
void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
//--- Событие создания GUI
   if(id==CHARTEVENT_CUSTOM+ON_END_CREATE_GUI)
     {
      //--- Запрос данных
      RequestData(m_request.Id());
      //--- Инициализация таблицы позиций
      InitializePositionsTable();
      return;
     }
  }

В итоге после загрузки программы на график таблица символов будет выглядеть так:

 Рис. 3 – Инициализированная таблица символов.

Рис. 1. Инициализированная таблица символов.

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

 Рис. 4 – Инициализированная таблица позиций.

Рис. 2. Инициализированная таблица позиций.

Обновление таблиц в реальном времени

Цена все время движется, поэтому во время торговой сессии данные в таблицах должны постоянно пересчитываться. Обновлять таблицу будем через определённые интервалы в таймере программы. Чтобы обеспечить  обновление элементов с разными интервалами, можно воспользоваться объектами типа CTimeCounter. Этот класс есть в сборке библиотеки EasyAndFast. Чтобы использовать его в проекте, достаточно подключить файл с его содержимым:

//+------------------------------------------------------------------+
//|                                                      Program.mqh |
//|                        Copyright 2018, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
...
#include <EasyAndFastGUI\TimeCounter.mqh>
...

В нашем эксперте нужны три временных счётчика для обновления данных в статусной строке и в таблицах. 

Чтобы установить счётчики, нужно просто объявить объекты типа CTimeCounter и в конструкторе установить их параметры (см. листинг ниже). Первый параметр — частота таймера, а второй временной интервал, после прохождения которого метод CTimeCounter::CheckTimeCounter() вернёт значение true. После этого счётчик обнуляется и начинает накапливаться заново.

//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
...
   //--- Временные счётчики
   CTimeCounter      m_counter1;
   CTimeCounter      m_counter2;
   CTimeCounter      m_counter3;
...
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
//--- Установка параметров для временных счётчиков
   m_counter1.SetParameters(16,500);
   m_counter2.SetParameters(16,5000);
   m_counter3.SetParameters(16,1000);
...
  }

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

//+------------------------------------------------------------------+
//| Таймер                                                           |
//+------------------------------------------------------------------+
void CProgram::OnTimerEvent(void)
  {
...
//--- Обновить пункты в статусной строке
   if(m_counter1.CheckTimeCounter())
     {
      //--- Установить значения
      m_status_bar.SetValue(1,"Deposit load: "+::DoubleToString(DepositLoad(false),2)+"/"+::DoubleToString(DepositLoad(true),2)+"%");
      m_status_bar.SetValue(2,::TimeToString(::TimeTradeServer(),TIME_DATE|TIME_SECONDS));
      //--- Обновить пункты
      m_status_bar.GetItemPointer(1).Update(true);
      m_status_bar.GetItemPointer(2).Update(true);
     }
...
  }

Чтобы ускорить обновление таблицы, где нужно заменить только значения индикаторов, будем использовать отдельный метод — CProgram::UpdateSymbolsTable(). Перед его вызовом нужно сначала обновить массив значений индикаторов. Затем вызывается метод CProgram::UpdateSymbolsTable(). Здесь на каждой итерации проверяется выход за пределы массива. Если проверка пройдена, то обновляем ячейки второго столбца таблицы и корректируем цвет текста. Процесс получения данных и инициализации таблиц показан в индикаторе выполнения.

//+------------------------------------------------------------------+
//| Обновляет таблицу символов                                       |
//+------------------------------------------------------------------+
void CProgram::UpdateSymbolsTable(void)
  {
   uint values_total=::ArraySize(m_values);
//--- Установим значения в таблицу символов
   uint rows_total=m_table_symb.RowsTotal();
   for(uint r=0; r<(uint)rows_total; r++)
     {
      //--- Остановить цикл, если выход за пределы массива
      if(r>values_total-1 || values_total<1)
         break;
      //--- Установим значения
      m_table_symb.SetValue(1,r,::DoubleToString(m_values[r],2));
      //--- Установим цвета
      color clr=(m_values[r]>(double)m_up_level.GetValue())? clrRed :(m_values[r]<(double)m_down_level.GetValue())? C'85,170,255' : clrBlack;
      m_table_symb.TextColor(0,r,clr,true);
      m_table_symb.TextColor(1,r,clr,true);
      //--- Обновить индикатор выполнения
      m_progress_bar.LabelText("Initialize tables: "+string(rows_total)+"/"+string(r)+"...");
      m_progress_bar.Update(r,rows_total);
      ::Sleep(5);
     }
//--- Обновить таблицу
   m_table_symb.Update();
  }

Блок второго временного счётчика для обновления таблицы символов показан ниже. Таким образом, каждые 5 секунд программа будет получать текущие значения индикаторов по всем символам и обновлять ими таблицу.

void CProgram::OnTimerEvent(void)
  {
...
//--- Обновить таблицу символов
   if(m_counter2.CheckTimeCounter())
     {
      //--- Показать прогресс
      m_progress_bar.Show();
      m_chart.Redraw();
      //--- Обновить значения в таблице
      GetIndicatorValues();
      UpdateSymbolsTable();
      //--- Скрыть прогресс
      m_progress_bar.Hide();
      m_chart.Redraw();
     }
...
  }

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

void CProgram::OnTimerEvent(void)
  {
...
//--- Обновить таблицу позиций
   if(m_counter3.CheckTimeCounter())
     {
      //--- Получим символы открытых позиций
      string symbols_name[];
      int symbols_total=GetPositionsSymbols(symbols_name);
      //--- Обновить значения в таблице
      SetValuesToPositionsTable(symbols_name);
      //--- Отсортировать, если это уже было сделано пользователем до обновления
      m_table_positions.SortData((uint)m_table_positions.IsSortedColumnIndex(),m_table_positions.IsSortDirection());
      //--- Обновить таблицу
      UpdatePositionsTable();
     }
  }

Обработка событий элементов управления

В этом разделе рассмотрим методы для обработки событий, которые генерируются при взаимодействии пользователя с графическим интерфейсом нашего эксперта. Мы уже рассмотрели метод CProgram::RequestData() для получения символов и данных по индикаторам. Если инициализация не первая, то метод вызывается при нажатии на кнопку Request в любой момент во время выполнения программы. При нажатии на кнопку генерируется пользовательское событие с идентификатором ON_CLICK_BUTTON.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Запрос данных
      if(RequestData(lparam))
         return;
      //---
      return;
     }
...
  }

На гифке ниже мы видим следующее. В таблице сформирован список с форекс-символами, содержащими USD. Затем мы быстро формируем список символов, содержащих EUR. Для этого просто в поле ввода Symbols filter вносим текст «EUR» и нажимаем на кнопку Request. Если же мы хотим увидеть все имеющиеся на сервере символы с валютами USD и EUR, то нужно ввести эти валюты через запятую: «USD,EUR».

 Рис. 5 – Формирование списка форекс-символов.

Рис. 3. Формирование списка форекс-символов.

Формирование списка форекс-символов и получение хэндлов индикаторов осуществляется по периоду, указанному в комбо-боксе Timeframes. Если же выбрать в выпадающем списке другой таймфрейм, то нужно получить новые хэндлы и обновить значения в таблице. Для этого нужен метод CProgram::ChangePeriod(). Если пришёл идентификатор комбо-бокса, то далее сначала обновляем таймфрейм в объекте-графике. Затем получаем хэндлы и данные индикаторов для всех символов в таблице, после чего она обновляется для отображения внесённых изменений. 

//+------------------------------------------------------------------+
//| Изменение таймфрейма                                             |
//+------------------------------------------------------------------+
bool CProgram::ChangePeriod(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_timeframes.Id())
      return(false);
//--- Получим значение из выпадающего списка комбобокса
   string tf=m_timeframes.GetListViewPointer().SelectedItemText();
//--- Получим указатель графика по индексу
   m_sub_chart1.GetSubChartPointer(0).Period(StringToTimeframe(tf));
   m_sub_chart1.ResetCharts();
//--- Показать прогресс
   m_progress_bar.Show();
   m_chart.Redraw();
//--- Получаем хендлы и данные индикаторов
   GetHandles();
   GetIndicatorValues();
//--- Обновляем таблицу
   UpdateSymbolsTable();
//--- Скрыть прогресс
   m_progress_bar.Hide();
   m_chart.Redraw();
   return(true);
  }

Когда мы выбираем пункт в выпадающем списке, то генерируется пользовательское событие с идентификатором ON_CLICK_COMBOBOX_ITEM:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Событие выбора пункта в комбо-боксе
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_COMBOBOX_ITEM)
     {
      //--- Изменение таймфрейма
      if(ChangePeriod(lparam))
         return;
      //---
      return;
     }
...
  }

Вот как выглядит изменение таймфрейма и получение новых данных:

 Рис. 6 – Изменение таймфрейма.

Рис. 4. Изменение таймфрейма.

Теперь рассмотрим, как быстро изменять символ на объекте-графике. В таблице символов в первом столбце уже есть названия символов. Поэтому можно переключаться между ними, просто выделяя строки в таблице. По клику на той или иной строке вызывается метод CProgram::ChangeSymbol(). Здесь сначала проверяется идентификатор таблицы символов. Затем нужна проверить, выделена ли строка в таблице, поскольку выделение со строк снимается повторным кликом. Если эти проверки пройдены, то далее нужно сохранить индекс выделенной строки, как индекс хэндла. По нему потом можно будет установить индикатор на график (см. далее в статье).

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

//+------------------------------------------------------------------+
//| Изменение символа                                                |
//+------------------------------------------------------------------+
bool CProgram::ChangeSymbol(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_table_symb.Id())
      return(false);
//--- Выйти, если строка не выделена
   if(m_table_symb.SelectedItem()==WRONG_VALUE)
     {
      //--- Показать полное описание символа в статустной строке
      m_status_bar.SetValue(0,"For Help, press F1");
      m_status_bar.GetItemPointer(0).Update(true);
      return(false);
     }
//--- Сохраним индекс хендла
   m_current_handle_index=m_table_symb.SelectedItem();
//--- Получим символ
   string symbol=m_table_symb.GetValue(0,m_current_handle_index);
//--- Обновить график
   m_sub_chart1.GetSubChartPointer(0).Symbol(symbol);
   m_sub_chart1.ResetCharts();
//--- Показать полное описание символа в статустной строке
   m_status_bar.SetValue(0,::SymbolInfoString(symbol,SYMBOL_DESCRIPTION));
   m_status_bar.GetItemPointer(0).Update(true);
   m_chart.Redraw();
   return(true);
  }

При выделении строки в таблице генерируется пользовательское событие с идентификатором ON_CLICK_LIST_ITEM. Можно переключать символы и клавишами «Up», «Down», «Home» и «End». В этом случае генерируется событие CHARTEVENT_KEYDOWN. Метод для его обработки рассматривался в предыдущей статье, поэтому не будем здесь на нём останавливаться.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- События выбора пункта в списке/таблице
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_LIST_ITEM)
     {
      //--- Изменение символа
      if(ChangeSymbol(lparam))
         return;
      //---
      return;
     }
//--- Нажатие на клавише
   if(id==CHARTEVENT_KEYDOWN)
     {
      //--- Выбор результатов с использованием клавиш
      if(SelectingResultsUsingKeys(lparam))
         return;
      //---
      return;
     }
...
  }

В результате обработки этих событий мы увидим примерно такую картину:

 Рис. 7 – Переключение символов.

Рис. 5. Переключение символов.

Иногда нужно видеть на графике индикатор, по которому мы получаем сигналы. Чтобы включить показ индикатора, предназначен чек-бокс Show indicator. За взаимодействие с ним отвечает метод CProgram::ShowIndicator(). Здесь также нужно пройти проверки на идентификатор элемента и выход за пределы диапазона массива хэндлов. Чтобы добавить или удалить индикатор на объект-график, понадобится идентификатор этого графика. Затем, если чек-бокс включен, то индикатор нужно добавить на график. Поскольку индикатор будет всегда один, то номер подокна указываем равным 1. Для более сложных случаев нужно определять количество индикаторов на графике. 

//+------------------------------------------------------------------+
//| Видимость индикатора                                             |
//+------------------------------------------------------------------+
bool CProgram::ShowIndicator(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_show_indicator.Id())
      return(false);
//--- Проверка выхода за пределы массива
   int handles_total=::ArraySize(m_handles);
   if(m_current_handle_index<0 || m_current_handle_index>handles_total-1)
      return(true);
//--- Получим идентификатор графика
   long sub_chart_id=m_sub_chart1.GetSubChartPointer(0).GetInteger(OBJPROP_CHART_ID);
//--- Номер подокна для индикатора
   int subwindow =1;
//--- Получим указатель графика по индексу
   if(m_show_indicator.IsPressed())
     {
      //--- Добавим индикатор на график
      ::ChartIndicatorAdd(sub_chart_id,subwindow,m_handles[m_current_handle_index]);
     }
   else
     {
      //--- Удалим индикатор с графика
      ::ChartIndicatorDelete(sub_chart_id,subwindow,ChartIndicatorName(sub_chart_id,subwindow,0));
     }
//--- Обновить график
   m_chart.Redraw();
   return(true);
  }

При взаимодействии с чек-боксом генерируется пользовательское событие ON_CLICK_CHECKBOX:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- Событие нажатия на элементе "Чекбокс"
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_CHECKBOX)
     {
      //--- Если нажали на чек-бокс "Show indicator"
      if(ShowIndicator(lparam))
         return;
      //---
      return;
     }
  }

Вот так это выглядит в работе:

 Рис. 8 – Показ индикатора.

Рис. 6. Показ индикатора.

К индикатору в графическом интерфейсе эксперта имеют отношение ещё два элемента управления. Это цифровые поля ввода уровней индикатора Stochastic: Up level и Down level. По умолчанию в них установлены значения 80 и 20. Если значения индикаторов на каждом символе превышают эти лимиты вверх и вниз, то текст в ячейках таблицы символов изменяется с чёрного на синий для верхнего уровня и на красный — для нижнего. Если изменить значения в этих полях ввода, то при следующем обновлении (через каждые пять секунд) изменится и цветовая индикация. 

Вот как это работает при изменении значений с 80/20 на 90/10 и обратно:

 Рис. 9 – Изменение сигнальных уровней индикатора.

Рис. 7. Изменение сигнальных уровней индикатора.

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

Методы обработки событий от чек-боксов Date scale и Price scale очень похожи. И в том и в другом, в зависимости от состояния чек-бокса, включается либо отключается соответствующее свойство графика. Метод CStandardChart::ResetCharts() сдвигает график в самый конец.

//+------------------------------------------------------------------+
//| Видимость временной шкалы                                        |
//+------------------------------------------------------------------+
bool CProgram::DateScale(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_date_scale.Id())
      return(false);
//--- Получим указатель графика по индексу
   m_sub_chart1.GetSubChartPointer(0).DateScale(m_date_scale.IsPressed());
   m_sub_chart1.ResetCharts();
//--- Обновить график
   m_chart.Redraw();
   return(true);
  }
//+------------------------------------------------------------------+
//| Видимость ценовой шкалы                                          |
//+------------------------------------------------------------------+
bool CProgram::PriceScale(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_price_scale.Id())
      return(false);
//--- Получим указатель графика по индексу
   m_sub_chart1.GetSubChartPointer(0).PriceScale(m_price_scale.IsPressed());
   m_sub_chart1.ResetCharts();
//--- Обновить график
   m_chart.Redraw();
   return(true);
  }

Для управления масштабом графика используется метод CProgram::ChartScale(). Здесь, если значение в поле ввода изменилось, то оно присваивается графику.

//+------------------------------------------------------------------+
//| Масштаб графика                                                  |
//+------------------------------------------------------------------+
bool CProgram::ChartScale(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_chart_scale.Id())
      return(false);
//--- Установить масштаб
   if((int)m_chart_scale.GetValue()!=m_sub_chart1.GetSubChartPointer(0).Scale())
      m_sub_chart1.GetSubChartPointer(0).Scale((int)m_chart_scale.GetValue());
//--- Обновить
   m_chart.Redraw();
   return(true);
  }

Изменение значения в поле ввода Chart scale обрабатывается по приходу пользовательских событий с идентификаторами ON_CLICK_BUTTON и ON_END_EDIT.

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      //--- Масштаб графика
      if(ChartScale(lparam))
         return;
      //---
      return;
     }
//--- События окончания изменения значения в поле ввода
   if(id==CHARTEVENT_CUSTOM+ON_END_EDIT)
     {
      //--- Масштаб графика
      if(ChartScale(lparam))
         return;
      //---
      return;
     }
  }

Код метода CProgram::ChartShift() для включения в графике отступа справа показан ниже. В нем после проверки на идентификатор элемента сначала получаем идентификатор графика, а затем, используя его, как ключ доступа, можно установить отступ (CHART_SHIFT).

//+------------------------------------------------------------------+
//| Сдвиг графика                                                    |
//+------------------------------------------------------------------+
bool CProgram::ChartShift(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_chart_shift.Id())
      return(false);
//--- Получим идентификатор графика
   long sub_chart_id=m_sub_chart1.GetSubChartPointer(0).GetInteger(OBJPROP_CHART_ID);
//--- Установим отступ в правой части графика
   ::ChartSetInteger(sub_chart_id,CHART_SHIFT,true);
   m_sub_chart1.ResetCharts();
   return(true);
  }

Вот как это выглядит:

 Рис. 10 – Управление свойствами графика.

Рис. 8.  Управление свойствами графика.

Методы для совершения торговых операций

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

Подключаем к проекту файл Trade.mqh с классом CTrade и объявляем экземпляр этого класса:

//--- Класс для торговых операций
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| Класс для создания приложения                                    |
//+------------------------------------------------------------------+
class CProgram : public CWndEvents
  {
private:
   //--- Торговые операции
   CTrade            m_trade;
  };

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

//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void)
  {
...
   m_trade.SetAsyncMode(true);
   m_trade.SetDeviationInPoints(INT_MAX);
  }

Нажатия на кнопках Buy и Sell будут обрабатываться похожими методами CProgram::OnBuy() и CProgram::OnSell(). Объём для сделки получаем из поля ввода Lot. Символ, на котором торгуем, будем брать в объекте-графике. Это необходимый минимум для совершения торговой операции. В классе CTrade есть методы CTrade::Buy() и CTrade::Sell(), при вызове которых можно передать только два этих аргумента. 

//+------------------------------------------------------------------+
//| Покупка                                                          |
//+------------------------------------------------------------------+
bool CProgram::OnBuy(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_buy.Id())
      return(false);
//--- Объём и символ для открытия позиции
   double lot    =::NormalizeDouble((double)m_lot.GetValue(),2);
   string symbol =m_sub_chart1.GetSubChartPointer(0).Symbol();
//--- Открыть позицию
   m_trade.Buy(lot,symbol);
   return(true);
  }
//+------------------------------------------------------------------+
//| Продажа                                                          |
//+------------------------------------------------------------------+
bool CProgram::OnSell(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_sell.Id())
      return(false);
//--- Объём и символ для открытия позиции
   double lot    =::NormalizeDouble((double)m_lot.GetValue(),2);
   string symbol =m_sub_chart1.GetSubChartPointer(0).Symbol();
//--- Открыть позицию
   m_trade.Sell(lot,symbol);
   return(true);
  }

Для закрытия сразу всех позиций или только по указанному символу нужно написать отдельный метод — в классе CTrade такого нет. Если в метод передан символ (необязательный параметр), то будут закрыты позиции только по этому символу. Если же символ не указан, то будут закрыты все позиции.

//+------------------------------------------------------------------+
//| Закрывает все позиции                                            |
//+------------------------------------------------------------------+
bool CProgram::CloseAllPosition(const string symbol="")
  {
//--- Проверим, есть ли позиция с указанными свойствами
   int total=::PositionsTotal();
   for(int i=total-1; i>=0; i--)
     {
      //--- Выберем позицию
      string pos_symbol=::PositionGetSymbol(i);
      //--- Если указано закрыть по символу
      if(symbol!="")
         if(symbol!=pos_symbol)
            continue;
      //--- Получим тикет
      ulong position_ticket=::PositionGetInteger(POSITION_TICKET);
      //--- Сброс последней ошибки
      ::ResetLastError();
      //--- Если позиция не закрылась, вывести сообщение об этом
      if(!m_trade.PositionClose(position_ticket))
         ::Print(__FUNCTION__,": > An error occurred when closing a position: ",::GetLastError());
     }
//---
   return(true);
  }

Закрытие всех позиций привязано к кнопке Close all positions. Ее нажатие будет обрабатываться в методе CProgram::OnCloseAllPositions(). Чтобы исключить случайные нажатия на кнопке, будет открываться диалоговое окно для подтверждения операции.

//+------------------------------------------------------------------+
//| Закрыть все позиции                                              |
//+------------------------------------------------------------------+
bool CProgram::OnCloseAllPositions(const long id)
  {
//--- Проверка идентификатора элемента
   if(id!=m_close_all.Id())
      return(false);
//--- Диалоговое окно
   int mb_id=::MessageBox("Are you sure you want to close \nall positions?","Close positions",MB_YESNO|MB_ICONWARNING);
//--- Закрыть позиции
   if(mb_id==IDYES)
      CloseAllPosition();
//---
   return(true);
  }

Вот так это выглядит:

 Рис. 11 – Закрытие всех позиций.

Рис. 9. Закрытие всех позиций.

Закрыть позиций по указанному символу можно на вкладке Positions. В ячейках первого столбца таблицы позиций добавлены кнопки в виде крестиков. Ими можно сразу закрыть все позиции по символу, данные которого показаны в этой строке. Нажатие на кнопках в ячейках генерирует пользовательское событие с идентификатором ON_CLICK_BUTTON. Но в элементе типа CTable есть полосы прокрутки, кнопки которых генерируют такие же события и идентификатор элемента тоже совпадает. Поэтому нужно отслеживать строковой параметр (sparam) события, чтобы случайно не обработать нажатие на других кнопках элемента. В строковом параметре указывается тип элемента, на котором было нажатие. У полос прокрутки это значение «scroll». Если пришло событие с таким значением, то программа выходит из метода. После этого нужно проверить, есть ли еще открытые позиции.

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

//+------------------------------------------------------------------+
//| Закрыть все позиции по указанному символу                        |
//+------------------------------------------------------------------+
bool CProgram::OnCloseSymbolPositions(const long id,const string desc)
  {
//--- Проверка идентификатора элемента
   if(id!=m_table_positions.Id())
      return(false);
//--- Выйти, если это нажатие на кнопке полосы прокрутки
   if(::StringFind(desc,"scroll",0)!=WRONG_VALUE)
      return(false);
//--- Выйти, если нет позиций
   if(::PositionsTotal()<1)
      return(true);
//--- Извлечём данные из строки
   string str_elements[];
   ushort sep=::StringGetCharacter("_",0);
   ::StringSplit(desc,sep,str_elements);
//--- Получим индекс и символ
   int    row_index =(int)str_elements[1];
   string symbol    =m_table_positions.GetValue(0,row_index);
//--- Диалоговое окно
   int mb_id=::MessageBox("Are you sure you want to close \nall positions on symbol "+symbol+"?","Close positions",MB_YESNO|MB_ICONWARNING);
//--- Закроем все позиции на указанном символе
   if(mb_id==IDYES)
      CloseAllPosition(symbol);
//---
   return(true);
  }

Вот как это выглядит:

 Рис. 11 – Закрытие всех позиций по указанному символу.

Рис. 10. Закрытие всех позиций по указанному символу.

Все торговые операции, описанные выше, обрабатываются по приходу события ON_CLICK_BUTTON:

void CProgram::OnEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
  {
...
//--- События нажатия на кнопках
   if(id==CHARTEVENT_CUSTOM+ON_CLICK_BUTTON)
     {
      ...
      //--- Покупка
      if(OnBuy(lparam))
         return;
      //--- Продажа
      if(OnSell(lparam))

         return;
      //--- Закрыть все позиции
      if(OnCloseAllPositions(lparam))
         return;
      //--- Закрыть все позиции указанного символа
      if(OnCloseSymbolPositions(lparam,sparam))
         return;
      //---
      return;
     }
...
  }

Каждая торговая операция должна отражаться в таблице позиций. Для этого нужно отслеживать торговые события и историю сделок торгового счёта. Если количество сделок изменилось, то таблицу нужно сформировать заново. Для проверки, изменилась ли история, используется метод CProgram::IsLastDealTicket(). Каждый раз после проверки нужно сохранять время и тикет последней сделки. Время сохраняем, чтобы не запрашивать каждый раз всю историю сделок. По тикету проверяем, изменилось ли количество сделок в истории. Так как сделка инициирует несколько торговых событий, то такой способ вернёт true только один раз.

class CProgram : public CWndEvents
  {
private:
   //--- Время и тикет последней проверенной сделки
   datetime          m_last_deal_time;
   ulong             m_last_deal_ticket;
   //---
private:
   //--- Проверка новой сделки в истории
   bool              IsLastDealTicket(void);
  };
//+------------------------------------------------------------------+
//| Constructor                                                      |
//+------------------------------------------------------------------+
CProgram::CProgram(void) : m_last_deal_time(NULL),
                           m_last_deal_ticket(WRONG_VALUE)
  {
...
  }
//+------------------------------------------------------------------+
//| Проверка новой сделки в истории                                  |
//+------------------------------------------------------------------+
bool CProgram::IsLastDealTicket(void)
  {
//--- Выйти, если история не получена
   if(!::HistorySelect(m_last_deal_time,UINT_MAX))
      return(false);
//--- Получим количество сделок в полученном списке
   int total_deals=::HistoryDealsTotal();
//--- Пройдемся по всем сделкам в полученном списке от последней сделки к первой
   for(int i=total_deals-1; i>=0; i--)
     {
      //--- Получим тикет сделки
      ulong deal_ticket=::HistoryDealGetTicket(i);
      //--- Если тикеты равны, выйдем
      if(deal_ticket==m_last_deal_ticket)
         return(false);
      //--- Если тикеты не равны, сообщим об этом
      else
        {
         datetime deal_time=(datetime)::HistoryDealGetInteger(deal_ticket,DEAL_TIME);
         //--- Запомним время и тикет последней сделки
         m_last_deal_time   =deal_time;
         m_last_deal_ticket =deal_ticket;
         return(true);
        }
     }
//--- Тикеты другого символа
   return(false);
  }

Метод CProgram::IsLastDealTicket() вызываем в обработчике торговых событий. Если история изменилась, то таблица позиций будет сформирована заново:

//+------------------------------------------------------------------+
//| Событие торговой операции                                        |
//+------------------------------------------------------------------+
void CProgram::OnTradeEvent(void)
  {
//--- Если новая сделка
   if(IsLastDealTicket())
     {
      //--- Инициализация таблицы позиций
      InitializePositionsTable();
     }
  }

Вот как это выглядит:

 Рис. 12 – Формирование таблицы при закрытии позиций по символу.

Рис. 11. Формирование таблицы при закрытии позиций по символу.

Заключение

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

Для тех, кто не хочет разбираться в коде и компилировать программу самостоятельно, в маркете выложено уже готовое приложение Trading Exposure.

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

Наименование файла Комментарий
MQL5\Experts\TradePanel\TradePanel.mq5 Торговый эксперт для ручной торговли с графическим интерфейсом
MQL5\Experts\TradePanel\Program.mqh Файл с классом программы
MQL5\Experts\TradePanel\CreateGUI.mqh Файл с реализацией методов для создания графического интерфейса из класса программы в файле Program.mqh
MQL5\Include\EasyAndFastGUI\Controls\Table.mqh Обновлённый класс CTable
MQL5\Include\EasyAndFastGUI\Keys.mqh Обновлённый класс CKeys