English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Работа с GSM-модемом из эксперта на MQL5

Работа с GSM-модемом из эксперта на MQL5

MetaTrader 5Интеграция | 10 декабря 2013, 14:31
6 253 11
Serhii Shevchuk
Serhii Shevchuk

Введение

На текущий момент существует достаточно средств для комфортного удалённого мониторинга торгового счёта: мобильные терминалы, push-уведомления, работа с ICQ. Но всё это требует обязательного наличия интернета. Данная статья описывает создание эксперта, который позволит вам находиться на связи с торговым терминалом даже в той ситуации, когда мобильный интернет будет недоступен, а именно - при помощи звонков и SMS-сообщений. Также, данный эксперт известит вас о разрыве и восстановлении связи с торговым сервером.

Для этих целей подойдёт практически любой GSM-модем, а также большинство телефонов, имеющих функцию модема. В качестве примера выбран модем Huawei E1550, так как он является одним из самых распространённых в своём роде устройств. Также, в конце статьи мы попробуем подключить вместо модема старый телефон Siemens M55 (год выпуска 2003) и посмотрим, что из этого получится.

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


1. Работа с COM-портом

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

Модем в диспетчере устройств

Рис. 1. Модем Huawei находится на порту COM3

Здесь на помощь приходит DLL библиотека TrComPort.dll, которая свободно распространяется в интернете вместе с исходниками. При помощи неё мы будем конфигурировать COM-порт, опрашивать его состояние, а также принимать и отправлять данные. Для этого будем использовать следующие функции:

#import "TrComPort.dll"
   int TrComPortOpen(int portnum);
   int TrComPortClose(int portid);   
   int TrComPortSetConfig(int portid, TrComPortParameters& parameters);
   int TrComPortGetConfig(int portid, TrComPortParameters& parameters);
   int TrComPortWriteArray(int portid, uchar& buffer[], uint length, int timeout);
   int TrComPortReadArray(int portid, uchar& buffer[], uint length, int timeout);   
   int TrComPortGetQueue(int portid, uint& input_queue, uint& output_queue);
#import

Типы передаваемых данных пришлось немного подредактировать для совместимости с MQL5.

Структура TrComPortParameters имеет вид:

struct TrComPortParameters
{
   uint   DesiredParams;
   int    BaudRate;           // Скорость обмена
   int    DefaultTimeout;     // Тайм-аут по умолчанию(в миллисекундах)
   uchar  ByteSize;           // Размер данных(4-8)
   uchar  StopBits;           // Количество стоп-бит
   uchar  CheckParity;        // Контроль четности(0-нет,1-да)
   uchar  Parity;             // Вид четности
   uchar  RtsControl;         // Начальное состояние RTS
   uchar  DtrControl;         // Начальное состояние DTR
};

Большинство устройств работают со следующими настройками: 8 бит данных, без контроля чётности, 1 стоп-бит. Поэтому, в параметры эксперта, из всех настроек COM-порта, есть смысл выносить только его номер и скорость обмена:

input ComPortList    inp_com_port_index=COM3;   // Выбор COM-порта
input BaudRateList   inp_com_baudrate=_9600bps; // Скорость обмена

Тогда функция инициализации COM-порта будет иметь вид:

//+------------------------------------------------------------------+
//| Инициализация COM-порта                                          |
//+------------------------------------------------------------------+
bool InitComPort()
  {
   rx_cnt=0;
   tx_cnt=0;
   tx_err=0;
//--- попытка открыть порт
   PortID=TrComPortOpen(inp_com_port_index);
   if(PortID!=inp_com_port_index)
     {
      Print("Ошибка открытия COM"+DoubleToString(inp_com_port_index+1,0));
      return(false);
     }
   else
     {
      Print("Порт COM"+DoubleToString(inp_com_port_index+1,0)+" открыт успешно");
      //--- запрашиваем все параметры, поэтому установим все флаги
      com_par.DesiredParams=tcpmpBaudRate|tcpmpDefaultTimeout|tcpmpByteSize|tcpmpStopBits|tcpmpCheckParity|tcpmpParity|tcpmpEnableRtsControl|tcpmpEnableDtrControl;
      //--- чтение текущих параметров 
      if(TrComPortGetConfig(PortID,com_par)==-1)
         return(false);//ошибка чтения
      //
      com_par.ByteSize=8;                //8 бит
      com_par.Parity=0;                  //без контроля чётности
      com_par.StopBits=0;                //1 стоп-бит
      com_par.DefaultTimeout=100;        //таймаут 100 мс
      com_par.BaudRate=inp_com_baudrate; //скорость - из параметров эксперта
      //---
      if(TrComPortSetConfig(PortID,com_par)==-1)
         return(false);//ошибка записи
     }
   return(true);
  }

В случае успешной инициализации переменная PortID будет хранить в себе идентификатор открытого COM-порта.

Здесь следует заметить, что идентификаторы нумеруются от нуля, поэтому, идентификатор порта COM3 будет равен 2. Порт открыт - можно обмениваться данными с модемом. К слову, не только с модемом. Доступ к COM-порту из эксперта даёт большие возможности для творчества тех, кто "дружит с паяльником": можно подключить к эксперту светодиодное табло или бегущую строку, куда выводить эквити или котировки интересующих валютных пар.

Чтобы получить информацию о данных в очереди приёмника и передатчика COM-порта, следует использовать функцию TrComPortGetQueue:

int TrComPortGetQueue(
   int   portid,           // Идентификатор COM-порта
   uint& input_queue,      // Количество байт во входящем буфере
   uint& output_queue      // Количество байт в исходящем буфере
   );

В случае ошибки вернёт отрицательное значение кода ошибки. Подробное описание кодов ошибок находится в архиве с исходниками библиотеки TrComPort.dll.

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

int TrComPortReadArray(
   int portid,             // Идентификатор порта
   uchar& buffer[],        // Указатель на буфер, куда читать
   uint length,            // Количество байт данных
   int timeout             // Таймаут выполнения (в миллисекундах)
   );

В случае ошибки вернёт отрицательное значение кода ошибки. Количество байт данных должно соответствовать величине, возвращённой функцией TrComPortGetQueue.

Чтобы использовать таймаут по умолчанию (установленный при инициализации COM-порта), следует передать значение -1.

Для отправки данных в COM-порт используется функция TrComPortWriteArray:

int TrComPortWriteArray(
   int portid,             // Идентификатор порта 
   uchar& buffer[],        // Указатель на исходный буфер
   uint length,            // Количество байт данных
   int timeout             // Таймаут выполнения (в миллисекундах)
   );

Пример использования. Ждём строку "Hello world!", в ответ должны отправить "Have a nice day!".

uchar rx_buf[1024];
uchar tx_buf[1024];
string rx_str;
int rxn, txn;
TrComPortGetQueue(PortID, rxn, txn);
if(rxn>0)
{  //--- поступили данные в приёмный буфер
   //--- выполним чтение
   TrComPortReadArray(PortID, rx_buf, rxn, -1);
   //--- выполним преобразование в строку
   rx_str = CharArrayToString(rx_buf,0,rxn,CP_ACP);
   //--- проверим, что пришло (ожидаем фразу "Hello world!"
   if(StringFind(rx_str,"Hello world!",0)!=-1)
   {//--- если совпало, подготовим ответ
      string tx_str = "Have a nice day!";
      int len = StringLen(tx_str);//получим длину в символах
      //--- выполним преобразование в буфер uchar
      StringToCharArray(tx_str, tx_buf, 0, len, CP_ACP);
      //--- отправим в порт
      if(TrComPortWriteArray(PortID, tx_buf, len, -1)<0) Print("Ошибка записи в порт");
   }
}

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

int TrComPortClose(
   int portid         // Идентификатор порта
   );  

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


2. AT-команды и работа с модемом

Работа с модемом производится посредством АТ-команд. Те, кто пользовался мобильным интернетом с компьютера, должны помнить так называемую "строку инициализации модема", которая выглядит примерно так: AT+CGDCONT=1,"IP","internet". Это и есть одна из АТ-команд. Практически все они начинаются с префикса AT и заканчиваются символом 0x0d (перевод каретки).

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

Ниже приведён список AT-команд, которые используются нашим обработчиком для работы с модемом:

 Команда Описание
  ATE1                                    
  Включить эхо
  AT+CGMI
  Получить имя производителя
  AT+CGMM
  Получить модель устройства
  AT^SCKS
  Получить состояние SIM-карты
  AT^SYSINFO
  Получить системную информацию
  AT+CREG
  Получить состояние регистрации в сети
  AT+COPS
  Получить имя мобильного оператора, к которому подключены в данный момент
  AT+CMGF
  Переключение режимов текст/PDU
  AT+CLIP
  Включить идентификацию вызывающей линии
  AT+CPAS
  Получить состояние модема
  AT+CSQ
  Получить качество сигнала
  AT+CUSD
  Оправить USSD-запрос
  AT+CALM
  Включить беззвучный режим (актуально для телефонов)
  AT+CBC
  Получить состояние батареи питания (актуально для телефонов)
  AT+CSCA
  Получить номер сервисного центра SMS-сообщений
  AT+CMGL
  Получить список SMS-сообщений
  AT+CPMS
  Выбрать память для SMS-сообщений
  AT+CMGD
  Удалить SMS-сообщение из памяти
  AT+CMGR
  Прочитать SMS-сообщение из памяти
  AT+CHUP
  Отклонить входящий вызов
  AT+CMGS
  Отправить SMS-сообщение


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


2.1. Функции

Инициализация COM-порта:

bool InitComPort();

Возвращаемое значение: в случае успешной инициализации - true, иначе - false. Вызывать из функции OnInit() перед инициализацией модема.

Деинициализация COM-порта:

void DeinitComPort();

Возвращаемое значение: нет. Вызывать из функции OnDeinit().

Инициализация модема:

void InitModem();

Возвращаемое значение: нет. Вызывать из функции OnInit() после успешной инициализации COM-порта.

Обработчик событий модема:

void ModemTimerProc();

Возвращаемое значение: нет. Вызывать из функции OnTimer() с периодом 1 секунда.

Чтение SMS-сообщения по индексу в памяти модема:

bool ReadSMSbyIndex(
   int index,             // Индекс SMS-сообщения в памяти модема
   INCOMING_SMS_STR& sms  // Указатель на структуру, куда будет помещено сообщение
   );

Возвращаемое значение: в случае успешного чтения - true, иначе - false.

Удаление SMS-сообщения по индексу в памяти модема:

bool DelSMSbyIndex(
   int index              // Индекс SMS-сообщения в памяти модема
   );

Возвращаемое значение: в случае успешного удаления - true, иначе - false.

Преобразование индекса качества связи в строку:

string rssi_to_str(
   int rssi               // Индекс качества связи, значения 0..31, 99
   );

Возвращаемое значение: строка, например "-55 dBm".

Отправка SMS-сообщения:

bool SendSMS(
   string da,      // Номер получателя в международном формате
   string text,    // Текст сообщения, латинские буквы и цифры, максимально 158 знаков
   bool flash      // Признак flash-сообщения
   );

Возвращаемое значение: при успешной отправке - true, иначе - false. Отправка SMS-сообщений допускается только латинскими буквами. Кириллица поддерживается только для входящих SMS-сообщений. При установке flash=true, будет отправлено flash-сообщение.


2.2. События (функции, которые вызываются обработчиком модема)

Обновление данных в структуре состояния модема:

void ModemChState();

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

Входящий вызов:

void IncomingCall(
   string number          // Номер вызывающего абонента
   );

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

Принято новое SMS-сообщение:

void IncomingSMS(
   INCOMING_SMS_STR& sms  // Структура SMS-сообщения
   );

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

Переполнение памяти SMS-сообщений:

void SMSMemoryFull(
   int n                  // Количество SMS-сообщений в памяти модема
   );

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


2.3. Структура состояния параметров модема

struct MODEM_STR
{
   bool     init_ok;          // Необходимый минимум инициализирован
   //
   string   manufacturer;     // Производитель
   string   device;           // Модель
   int      sim_stat;         // Статус SIM
   int      net_reg;          // Состояние регистрации в сети
   int      status;           // Состояние модема
   string   op;               // Оператор
   int      rssi;             // Качество сигнала
   string   sms_sca;          // Номер смс-центра
   int      bat_stat;         // Состояние батареи
   int      bat_charge;       // Заряд батареи в процентах (актуальность зависит от bat_stat)
   //
   double   bal;              // Баланс мобильного счёта
   string   exp_date;         // Дата истечения срока действия мобильного номера
   int      sms_free;         // Остаток пакетных смс
   int      sms_free_cnt;     // Счётчик использованных пакетных смс
   //
   int      sms_mem_size;     // Емкость памяти смс
   int      sms_mem_used;     // Емкость использованной памяти смс
   //
   string   incoming;         // Номер вызывающего абонента
};

MODEM_STR modem;

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

Ниже приведено описание элементов структуры:

 ЭлементОписание
  modem.init_ok
  Признак того, что модем успешно инициализирован.
  Начальное значение false, после окончания инициализации равно true.
  modem.manufacturer
  Производитель модема, например: "huawei".
  Начальное значение "n/a".
  modem.device
  Модель модема, например: "E1550"
  Начальное значение "n/a".
  modem.sim_stat
  Состояние SIM-карты. Может принимать следующие значения:
  -1 - не известно
   0 - карта отсутствует, заблокирована или неисправна
   1 - карта присутствует    
  modem.net_reg
  Состояние регистрации в сети. Может принимать следующие значения:
  -1 - не известно
   0 - не зарегистрирован
   1 - зарегистрирован
   2 - идёт поиск
   3 - запрещено
   4 - неопределённое состояние
   5 - зарегистрирован в роуминге
  modem.status
  Состояние модема. Может принимать следующие значения:
  -1 - инициализация
   0 - готов
   1 - ошибка
   2 - ошибка
   3 - входящий вызов
   4 - активный вызов
  modem.op
  Оператор, к которому подключены в данный момент.
  Может быть равно либо имени оператора (например, "MTS UKR"),
  либо международному код оператора (например, "25501").
  Начальное значение "n/a".
  modem.rssi
  Индекс качества сигнала. Может принимать следующие значения:
  -1 - не известно
   0 - сигнал -113 dBm или меньше
   1 - сигнал -111 dBm
   2...30 - сигнал -109...-53 dBm
  31 - сигнал -51 dBm или выше
  99 - не известно
  Для перевода в сроку использовать функцию rssi_to_str().
  modem.sms_sca
  Номер сервис-центра SMS-сообщений. Содержится в памяти SIM-карты.
  Необходим для формирования исходящего SMS-сообщения.
  В редких случаях, если номер не записан в памяти SIM-карты, вместо него
  будет использоваться номер, указанный во входных параметрах эксперта.
  modem.bat_stat
  Состояние батареи питания модема (актуально только  для телефонов).
  Может принимать следующие значения:
  -1 - не известно
   0 - устройство питается от батареи
   1 - батарея присутствует, но устройство не питается от неё
   2 - батарея отсутствует
   3 - ошибка
  modem.bat_charge
  Процент заряда батареи питания.
  Может принимать значения от 0 до 100.
  modem.bal
  Количество денежных средств на мобильном счету. Значение получается
  из ответа оператора на USSD-запрос.
  Начальное значение (до инициализации): -10000.
  modem.exp_date
  Дата истечения срока действия мобильного номера. Значение получается
  из ответа оператора на USSD-запрос.
  Начальное значение "n/a".
  modem.sms_free
  Количество доступных пакетных SMS. Рассчитывается, как разница между
  начальным количеством и счётчиком использованных пакетных SMS.
  modem.sms_free_cnt
  Счётчик использованных пакетных SMS. Значение получается
  из ответа оператора на USSD-запрос. Начальное значение -1.
  modem.sms_mem_size
  Размер памяти SMS-сообщений модема.
  modem.sms_mem_used
  Размер использованной памяти SMS-сообщений модема.
  modem.incoming
  Номер последнего вызывающего абонента.
  Начальное значение "n/a".


2.4. Структура SMS-сообщения

//+------------------------------------------------------------------+
//| Структура SMS-сообщения                                          |
//+------------------------------------------------------------------+
struct INCOMING_SMS_STR
{
   int index;                //индекс в памяти модема
   string sca;               //номер SMS-центра отправителя
   string sender;            //номер отправителя
   INCOMING_CTST_STR scts;   //метка времени SMS-центра
   string text;              //текст сообщения
};

Метка времени SMS-центра - это время, когда SMS-центр получил данное сообщение от отправителя. Структура метки времени имеет следующий вид:

//+------------------------------------------------------------------+
//| Структура метки времени                                          |
//+------------------------------------------------------------------+
struct INCOMING_CTST_STR
{
   datetime time;            // время
   int gmt;                  // часовой пояс
};

Часовой пояс выражен в 15-минутных интервалах. То есть значение 8 соответствует GMT+02:00.

Текст принятых SMS-сообщений может быть как латинским, так и кириллическим. Принимаются сообщения как в 7-битной кодировке, так и в UCS2. Склеивание длинных сообщений не реализовано (с расчетом на то, что приём предназначается для коротких команд).

Отправка SMS-сообщений возможна только латинскими буквами. Максимальная длина сообщения - 158 знаков. При попытке отправить более длинную строку, лишние знаки будут отброшены.


3. Приступаем к созданию эксперта

Для начала вам необходимо скопировать файл TrComPort.dll в папку Libraries, а файлы ComPort.mqh, modem.mqh и sms.mqh - в папку Include.

Затем, с помощью Мастера создаём новый эксперт, и добавляем необходимый минимум для работы с модемом. А именно:

Прикрепляем файл modem.mqh:

#include <modem.mqh>

Добавляем входные параметры:

input string         str00="Настройка COM-порта";
input ComPortList    inp_com_port_index=COM3;   // Выбор COM-порта
input BaudRateList   inp_com_baudrate=_9600bps; // Скорость обмена
//
input string         str01="Модем";
input int            inp_refr_period=3;         // Период опроса модема, сек
input int            inp_ussd_request_tout=20;  // Таймаут ожидания ответа на ussd-запрос, сек
input string         inp_sms_service_center=""; // Номер сервис-центра смс
//
input string         str02="Состояние баланса";
input int            inp_refr_bal_period=12;    // Период опроса, часы
input string         inp_ussd_get_balance="";   // USSD-запрос баланса
input string         inp_ussd_bal_suffix="";    // Суффикс баланса
input string         inp_ussd_exp_prefix="";    // Префикс даты истечения срока действия номера
//
input string         str03="Количество пакетных смс";
input int            inp_refr_smscnt_period=6;  // Период опроса, часы
input string         inp_ussd_get_sms_cnt="";   // USSD-запрос состояния пакетных услуг
input string         inp_ussd_sms_suffix="";    // Суффикс счётчика смс
input int            inp_free_sms_daily=0;      // Дневной лимит смс

Функции, которые вызываются обработчиком модема:

//+------------------------------------------------------------------+
//| Вызывается при получении нового смс-сообщения                    |
//+------------------------------------------------------------------+
void IncomingSMS(INCOMING_SMS_STR& sms)
{  
} 

//+------------------------------------------------------------------+
//| Память SMS переполнена                                           |
//+------------------------------------------------------------------+
void SMSMemoryFull(int n)
{
}

//+------------------------------------------------------------------+
//| Вызывается при поступлении входящего звонка                      |
//+------------------------------------------------------------------+
void IncomingCall(string number)
{ 
}  

//+------------------------------------------------------------------+
//| Вызывается после обновления данных в структуре состояния модема  |
//+------------------------------------------------------------------+
void ModemChState()
{
   static bool init_ok = false;
   if(modem.init_ok==true && init_ok==false)
   {
      Print("Инициализация модема прошла успешно");      
      init_ok = true;
   }
}

В функцию OnInit() должна быть добавлена инициализация COM-порта и модема, а также запущен таймер с периодом 1 секунда:

int OnInit()
{  //---инициализация COM-порта
   if(InitComPort()==false)
   {
      Print("Ошибка инициализации порта COM"+DoubleToString(inp_com_port_index+1,0));
      return(INIT_FAILED);
   }      
   //--- инициализация модема
   InitModem();
   //--- запуск таймера 
   EventSetTimer(1); //период 1 секунда
   //      
   return(INIT_SUCCEEDED);
}

В функции OnTimer() нужно вызывать обработчик модема:

void OnTimer()
{
//---
   ModemTimerProc();
}

В функции OnDeinit() обязательно вызывать деинициализацию COM-порта:

void OnDeinit(const int reason)
{
//--- destroy timer
   EventKillTimer();
   DeinitComPort();   
}

Компилируем и видим: 0 error(s).

Запустите эксперт. При этом, не забудьте разрешить импорт DLL и выберите тот COM-порт, на котором находится модем. Во вкладке "Эксперты" должны появиться следующие сообщения:

Первый запуск

Рис. 2. Сообщения эксперта при успешном запуске

Если у вас получилось то же самое, значит, ваш модем (телефон) подходит для работы с данным экспертом. В таком случае идём дальше.

Для визуализации параметров модема нарисуем табличку. Она будет расположена в левом верхнем углу окна терминала, под строкой OHLC. Шрифт текста в таблице выберем с одинаковой шириной символов, например, "Courier New".

//+------------------------------------------------------------------+
//| TextXY                                                           |
//+------------------------------------------------------------------+
void TextXY(string ObjName,string Text,int x,int y,color TextColor)
  {
//--- выводим текстовую строку
   ObjectDelete(0,ObjName);
   ObjectCreate(0,ObjName,OBJ_LABEL,0,0,0,0,0);
   ObjectSetInteger(0,ObjName,OBJPROP_XDISTANCE,x);
   ObjectSetInteger(0,ObjName,OBJPROP_YDISTANCE,y);
   ObjectSetInteger(0,ObjName,OBJPROP_COLOR,TextColor);
   ObjectSetInteger(0,ObjName,OBJPROP_FONTSIZE,9);
   ObjectSetString(0,ObjName,OBJPROP_FONT,"Courier New");
   ObjectSetString(0,ObjName,OBJPROP_TEXT,Text);
  }
//+------------------------------------------------------------------+
//| Рисует таблицу параметров модема                                 |
//+------------------------------------------------------------------+
void DrawTab()
  {
   int   x=20, //отступ по горизонтали 
   y = 20,     //отступ по вертикали
   dy = 15;    //шаг по вертикали
//--- рисуем фон
   ObjectDelete(0,"bgnd000");
   ObjectCreate(0,"bgnd000",OBJ_RECTANGLE_LABEL,0,0,0,0,0);
   ObjectSetInteger(0,"bgnd000",OBJPROP_XDISTANCE,x-10);
   ObjectSetInteger(0,"bgnd000",OBJPROP_YDISTANCE,y-5);
   ObjectSetInteger(0,"bgnd000",OBJPROP_XSIZE,270);
   ObjectSetInteger(0,"bgnd000",OBJPROP_YSIZE,420);
   ObjectSetInteger(0,"bgnd000",OBJPROP_BGCOLOR,clrBlack);
//--- параметры порта
   TextXY("str0",  "Port:            ", x, y, clrWhite); y+=dy;
   TextXY("str1",  "Speed:           ", x, y, clrWhite); y+=dy;
   TextXY("str2",  "Rx:              ", x, y, clrWhite); y+=dy;
   TextXY("str3",  "Tx:              ", x, y, clrWhite); y+=dy;
   TextXY("str4",  "Err:             ", x, y, clrWhite); y+=(dy*3)/2;
//--- параметры модема
   TextXY("str5",  "Modem:           ", x, y, clrWhite); y+=dy;
   TextXY("str6",  "SIM:             ", x, y, clrWhite); y+=dy;
   TextXY("str7",  "NET:             ", x, y, clrWhite); y+=dy;
   TextXY("str8",  "Operator:        ", x, y, clrWhite); y+=dy;
   TextXY("str9",  "SMSC:            ", x, y, clrWhite); y+=dy;
   TextXY("str10", "RSSI:            ", x, y, clrWhite); y+=dy;
   TextXY("str11", "Bat:             ", x, y, clrWhite); y+=dy;
   TextXY("str12", "Modem status:    ", x, y, clrWhite); y+=(dy*3)/2;
//--- состояние мобильного счёта
   TextXY("str13", "Balance:         ", x, y, clrWhite); y+=dy;
   TextXY("str14", "Expiration date: ", x, y, clrWhite); y+=dy;
   TextXY("str15", "Free SMS:        ", x, y, clrWhite); y+=(dy*3)/2;
//--- номер последнего входящего вызова
   TextXY("str16","Incoming:        ",x,y,clrWhite); y+=(dy*3)/2;
//--- параметры последнего принятого SMS-сообщения
   TextXY("str17", "SMS mem full:    ", x, y, clrWhite); y+=dy;
   TextXY("str18", "SMS number:      ", x, y, clrWhite); y+=dy;
   TextXY("str19", "SMS date/time:   ", x, y, clrWhite); y+=dy;
//--- текст последнего принятого SMS-сообщения
   TextXY("str20", "                 ", x, y, clrGray); y+=dy;
   TextXY("str21", "                 ", x, y, clrGray); y+=dy;
   TextXY("str22", "                 ", x, y, clrGray); y+=dy;
   TextXY("str23", "                 ", x, y, clrGray); y+=dy;
   TextXY("str24", "                 ", x, y, clrGray); y+=dy;
//---
   ChartRedraw(0);
  }

Для обновления данных в таблице будет использоваться функция RefreshTab():

//+------------------------------------------------------------------+
//| Обновляет значения в таблице                                     |
//+------------------------------------------------------------------+
void RefreshTab()
  {
   string str;
//--- индекс COM-порта:
   str="COM"+DoubleToString(PortID+1,0);
   ObjectSetString(0,"str0",OBJPROP_TEXT,"Port:            "+str);
//--- скорость обмена:
   str=DoubleToString(inp_com_baudrate,0)+" bps";
   ObjectSetString(0,"str1",OBJPROP_TEXT,"Speed:           "+str);
//--- количество принятых байт:
   str=DoubleToString(rx_cnt,0)+" bytes";
   ObjectSetString(0,"str2",OBJPROP_TEXT,"Rx:              "+str);
//--- количество переданных байт:
   str=DoubleToString(tx_cnt,0)+" bytes";
   ObjectSetString(0,"str3",OBJPROP_TEXT,"Tx:              "+str);
//--- количество ошибок обращения к порту:
   str=DoubleToString(tx_err,0);
   ObjectSetString(0,"str4",OBJPROP_TEXT,"Err:             "+str);
//--- производитель и модель модема:
   str=modem.manufacturer+" "+modem.device;
   ObjectSetString(0,"str5",OBJPROP_TEXT,"Modem:           "+str);
//--- состояние SIM-карты:
   string sim_stat_str[2]={"Error","Ok"};
   if(modem.sim_stat==-1)
      str="n/a";
   else
      str=sim_stat_str[modem.sim_stat];
   ObjectSetString(0,"str6",OBJPROP_TEXT,"SIM:             "+str);
//--- регистрация в сети:
   string net_reg_str[6]={"No","Ok","Search...","Restricted","Unknown","Roaming"};
   if(modem.net_reg==-1)
      str="n/a";
   else
      str=net_reg_str[modem.net_reg];
   ObjectSetString(0,"str7",OBJPROP_TEXT,"NET:             "+str);
//--- имя оператора мобильной связи:
   ObjectSetString(0,"str8",OBJPROP_TEXT,"Operator:        "+modem.op);
//--- номер сервис-центра SMS
   ObjectSetString(0,"str9",OBJPROP_TEXT,"SMSC:            "+modem.sms_sca);
//--- уровень сигнала:
   if(modem.rssi==-1)
      str="n/a";
   else
      str=rssi_to_str(modem.rssi);
   ObjectSetString(0,"str10",OBJPROP_TEXT,"RSSI:            "+str);
//--- состояние батарейки (актуально для телефонов):
   string bat_stats_str[4]={"Ok, ","Ok, ","No","Err"};
   if(modem.bat_stat==-1)
      str="n/a";
   else
      str=bat_stats_str[modem.bat_stat];
   if(modem.bat_stat==0 || modem.bat_stat==1)
      str+=DoubleToString(modem.bat_charge,0)+"%";
   ObjectSetString(0,"str11",OBJPROP_TEXT,"Bat:             "+str);
//--- состояние модема:
   string modem_stat_str[5]={"Ready","Err","Err","Incoming call","Active call"};
   if(modem.status==-1)
      str="init...";
   else
     {
      if(modem.status>4 || modem.status<0)
         Print("Unknown modem status: "+DoubleToString(modem.status,0));
      else
         str=modem_stat_str[modem.status];
     }
   ObjectSetString(0,"str12",OBJPROP_TEXT,"Modem status:    "+str);
//--- состояние мобильного счёта:
   if(modem.bal==-10000)
      str="n/a";
   else
      str=DoubleToString(modem.bal,2)+" "+inp_ussd_bal_suffix;
   ObjectSetString(0,"str13",OBJPROP_TEXT,"Balance:         "+str);
//--- срок годности мобильного номера:
   ObjectSetString(0,"str14",OBJPROP_TEXT,"Expiration date: "+modem.exp_date);
//--- остаток пакетных SMS-сообщений:
   if(modem.sms_free<0)
      str="n/a";
   else
      str=DoubleToString(modem.sms_free,0);
   ObjectSetString(0,"str15",OBJPROP_TEXT,"Free SMS:        "+str);
//--- переполнение памяти SMS-сообщений:
   if(sms_mem_full==true)
      str="Yes";
   else
      str="No";
   ObjectSetString(0,"str17",OBJPROP_TEXT,"SMS mem full:    "+str);
//---
   ChartRedraw(0);
  }

Для удаления таблицы - функция DelTab():

//+------------------------------------------------------------------+
//| Удаляет таблицу                                                  |
//+------------------------------------------------------------------+
void DelTab()
  {
   for(int i=0; i<25; i++)
      ObjectDelete(0,"str"+DoubleToString(i,0));
   ObjectDelete(0,"bgnd000");
  }

Добавим вызовы функций работы с таблицей в обработчики событий OnInit(), OnDeinit() и в функцию ModemChState():

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- инициализация COM-порта
   if(InitComPort()==false)
     {
      Print("Ошибка инициализации порта COM"+DoubleToString(inp_com_port_index+1,0));
      return(INIT_FAILED);
     }
//---
   DrawTab();
//--- инициализация модема
   InitModem();
//--- запуск таймера 
   EventSetTimer(1);//период 1 секунда
//---
   RefreshTab();
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//--- destroy timer
   EventKillTimer();
   DeinitComPort();
   DelTab();
  }
//+------------------------------------------------------------------+
//| ModemChState                                                     |
//+------------------------------------------------------------------+
void ModemChState()
  {
   static bool init_ok=false;
//Print("Изменилось состояние модема");
   if(modem.init_ok==true && init_ok==false)
     {
      Print("Инициализация модема прошла успешно");
      init_ok=true;
     }
//---
   RefreshTab();
  }

В функцию IncomingCall() добавим обновление строки таблицы "номер последнего вызова":

//+------------------------------------------------------------------+
//| Ввызывается при поступлении входящего звонка                     |
//+------------------------------------------------------------------+
void IncomingCall(string number)
{    
   //--- обновляем номер последнего входящего вызова:
   ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming:        "+number);   
} 

Компилируем, запускаем. В окне терминала должно появиться следующее:

Параметры состояния модема

Рис. 3. Параметры модема

Попробуйте позвонить на модем. Звонок будет сброшен, а в строке "Incoming" отобразится ваш номер.


4. Работа с USSD запросами

Не пополненный вовремя мобильный счёт может остановить работу эксперта в самый неподходящий момент. Поэтому, функция проверки остатка на счету является одной из самых важных функций. Для проверки состояния мобильного счёта обычно используются USSD запросы. Также, мы будем использовать USSD запросы для получения информации об остатке пакетных SMS-сообщений.

Данные для формирования запросов и обработки ответов находятся во входных параметрах:

input string         str02="=== Состояние баланса ======";
input int            inp_refr_bal_period=12;  //период опроса, часы
input string         inp_ussd_get_balance=""; //USSD-запрос баланса
input string         inp_ussd_bal_suffix="";  //суффикс баланса
input string         inp_ussd_exp_prefix="";  //префикс даты истечения срока действия номера
//
input string         str03="= Количество пакетных смс ==";
input int            inp_refr_smscnt_period=6;//период опроса, часы
input string         inp_ussd_get_sms_cnt=""; //USSD-запрос состояния пакетных услуг
input string         inp_ussd_sms_suffix="";  //суффикс счётчика смс
input int            inp_free_sms_daily=0;    //дневной лимит смс

Если номер запроса не задан, запрос не будет обрабатываться. В противном случае, запрос будет отправлен сразу после инициализации модема, а затем повторная отправка произойдёт через указанный интервал времени. Также, запрос не будет обработан, если ваш модем (телефон) не поддерживает соответствующую AT-команду (касается старых моделей телефонов). 

Допустим, на запрос остатка баланса ваш оператор присылает ответ в следующем виде:

7.13 UAH, dijsnyj do 22.05.2014. Taryf - Super MTS 3D Nul 25.

Тогда для того, чтобы обработчик правильно распознал ответ, суффикс баланса должен задан "UAH", а префикс истечения срока действия номера "dijsnyj do".

Так как наш эксперт будет довольно часто отправлять SMS-сообщения, есть смысл заказать у оператора SMS-пакет - услугу, которая предусматривает начисление определённого количества SMS-сообщений за небольшую абонплату. В таком случае полезно знать, сколько осталось доступных пакетных SMS. Это тоже можно сделать при помощи USSD запроса. Обычно оператор возвращает не остаток, а число использованных SMS.

Допустим, ваш оператор присылает ответ в следующем виде:

Zalyshok 69 hvylyn v regioni na siogodni. Vykorystano 0 SMS ta 0 MB na siogodni.

В таком случае, суффикс счётчика SMS должен быть задан "SMS", а дневной лимит - соответственно условиям услуги SMS-пакет. Например, если вам начисляют 30 сообщений в сутки, а в запросе вернули 10, значит, доступно 30-10=20. Именно это число будет помещено обработчиком в соответствующий элемент структуры состояния модема.

ВАЖНО! При заполнении номеров USSD запросов будьте очень внимательны! Отправка ошибочного запроса может иметь очень неприятные последствия, например, подключение какой-нибудь нежелательной платной услуги!

Для того, чтобы наш эксперт начал работать с USSD запросами, нужно просто заполнить соответствующие входные параметры.

Например, для оператора "МТС Украина" это будет выглядеть так:

Параметры запроса остатка на счету

Рис. 4. Параметры USSD запроса остатка на счету

Параметры запроса количества пакетных SMS

Рис. 5. Параметры USSD запроса количества пакетных SMS

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

Баланс

Рис. 6. Параметры, получаемые из ответов на USSD-запросы

На момент написания статьи мой мобильный оператор, вместо даты истечения срока действия номера, присылает предновогоднюю рекламу. Соответственно, обработчик не нашёл дату, и в строке "Expiration date" отображается "n/a". Обратите внимание, что все ответы оператора отображаются во вкладке "Эксперты".

Ответы оператора

Рис. 7. Ответы оператора во вкладке "Эксперты"


5. Отправка SMS-сообщений

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

Разумеется, что реагировать нужно только на номер администратора, поэтому у нас появится ещё один входной параметр:

input string         inp_admin_number="+XXXXXXXXXXXX";//телефонный номер администратора

Номер должен быть задан в международном формате, включая "+" перед номером.

В обработчик входящих звонков нужно добавить проверку номера, формирование текста SMS-сообщения, и его отправку:

//+------------------------------------------------------------------+
//| Вызывается при поступлении входящего звонка                      |
//+------------------------------------------------------------------+
void IncomingCall(string number)
{
   bool result;
   if(number==inp_admin_number)
   {
      Print("Номер администратора. Отправляю смс.");
      //
      string mob_bal="";
      if(modem.bal!=-10000)//остаток на мобильном счету
         mob_bal = "\n(m.bal="+DoubleToString(modem.bal,2)+")";
      result = SendSMS(inp_admin_number, "Account: "+DoubleToString(AccountInfoInteger(ACCOUNT_LOGIN),0)   
               +"\nProfit: "+DoubleToString(AccountInfoDouble(ACCOUNT_PROFIT),2)
               +"\nEquity: "+DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY),2)
               +"\nPositions: "+DoubleToString(PositionsTotal(),0)
               +mob_bal
               , false);
      if(result==true)
         Print("SMS отправлено успешно");
      else
         Print("Ошибка отправки SMS");               
   }   
   else
      Print("Посторонний номер ("+number+")"); 
   //--- обновляем номер последнего входящего вызова:
   ObjectSetString(0, "str16",OBJPROP_TEXT, "Incoming:        "+number);   
}  

Теперь при звонке на модем с номера inp_admin_number, в ответ будет отправлено информационное SMS-сообщение:

SMS-ответ на входящий вызов

Рис. 8. Информационное SMS-сообщение, которое отправляется экспертом в ответ на вызов с номера администратора

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


6. Мониторинг соединения с торговым севером

Добавим отправку отчётов при разрыве и восстановлении связи с торговым сервером. Для этого, один раз в 10 секунд будем проверять наличие подключения к торговому серверу при помощи TerminalInfoInteger() с идентификатором свойства TERMINAL_CONNECTED.

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

input int            inp_conn_hyst=6; //Гистерезис, х10 сек

Значение 6 означает, что решение об обрыве связи будет принято, если связь будет отсутствовать более 6*10=60 секунд. Аналогично, восстановлением связи будет считать наличие подключения более 60 секунд. Временем потери связи будет считаться локальное время первого зарегистрированного отсутствия подключения, временем восстановления - первое локальное время наличия подключения.

Для этого добавим в функцию OnTimer() следующий код:

   static int s10 = 0;//предделитель на 10 секунд
   static datetime conn_time;
   static datetime disconn_time;
   if(++s10>=10)
   {//--- раз в 10 секунд
      s10 = 0;
      //
      if((bool)TerminalInfoInteger(TERMINAL_CONNECTED)==true)
      {
         if(cm.conn_cnt==0)             //первый удачный опрос в последовательности
            conn_time = TimeLocal();    //запоминаем время
         if(cm.conn_cnt<inp_conn_hyst)
         {
            if(++cm.conn_cnt>=inp_conn_hyst)
            {//--- соединение стабилизировалось
               if(cm.connected == false)
               {//--- если перед этим был устойчивый разрыв связи
                  cm.connected = true;
                  cm.new_state = true;
                  cm.conn_time = conn_time;
               }
            }
         }
         cm.disconn_cnt = 0;
      }
      else
      {
         if(cm.disconn_cnt==0)          //первый неудачный опрос в последовательности
            disconn_time = TimeLocal(); //запоминаем время
         if(cm.disconn_cnt<inp_conn_hyst)
         {
            if(++cm.disconn_cnt>=inp_conn_hyst)
            {//--- устойчивый разрыв связи
               if(cm.connected == true)
               {//--- если перед этим было стабильное соединение 
                  cm.connected = false;
                  cm.new_state = true;
                  cm.disconn_time = disconn_time;
               }
            }
         }
         cm.conn_cnt = 0;
      }
   }
   //
   if(cm.new_state == true)
   {//--- изменилось состояние соединения
      if(cm.connected == true)
      {//--- есть соединение
         string str = "Connected "+TimeToString(cm.conn_time,TIME_DATE|TIME_SECONDS);
         if(cm.disconn_time!=0)
            str+= ", offline: "+dTimeToString((ulong)(cm.conn_time-cm.disconn_time));
         Print(str);
         SendSMS(inp_admin_number, str, false);//отправка сообщения
      }
      else
      {//--- нет соединения
         string str = "Disconnected "+TimeToString(cm.disconn_time,TIME_DATE|TIME_SECONDS);
         if(cm.conn_time!=0)
            str+= ", online: "+dTimeToString((ulong)(cm.disconn_time-cm.conn_time));
         Print(str);
         SendSMS(inp_admin_number, str, false);//отправка сообщения
      }
      cm.new_state = false;
   }

Структура cm имеет следующий вид:

//+------------------------------------------------------------------+
//| Структура мониторинга связи с терминалом                         |
//+------------------------------------------------------------------+
struct CONN_MON_STR
  {
   bool              new_state;    //флаг изменения состояния соединения
   bool              connected;    //состояние соединения
   int               conn_cnt;     //счётчик успешных опросов подключения
   int               disconn_cnt;  //счётчик неуспешных опросов подключения
   datetime          conn_time;    //время установки соединения
   datetime          disconn_time; //время разрыва соединения
  };

CONN_MON_STR cm;//структура мониторинга соединения 

В тексте SMS-сообщения мы укажем время обрыва (или установки) связи с торговым сервером, а также время отсутствия (или наличия) соединения, которое вычислим, как разницу времён установки и разрыва соединения. Для перевода разницы времени из секунд в вид dd hh:mm:ss добавим функцию dTimeToString():

string dTimeToString(ulong sec)
{
   string str;
   uint days = (uint)(sec/86400);
   if(days>0)
   {
      str+= DoubleToString(days,0)+" days, ";
      sec-= days*86400;
   }
   uint hour = (uint)(sec/3600);
   if(hour<10) str+= "0";
   str+= DoubleToString(hour,0)+":";
   sec-= hour*3600;
   uint min = (uint)(sec/60);
   if(min<10) str+= "0";
   str+= DoubleToString(min,0)+":";
   sec-= min*60;
   if(sec<10) str+= "0";
   str+= DoubleToString(sec,0);
   //
   return(str);
}

Чтобы при каждом перезапуске эксперта он не отправлял сообщение об установке связи, добавим функцию conn_mon_init(), которая установит значения элементов структуры cm таким образом, как будто установка связи уже произошла. При этом, временем установки связи будет локальное время запуска эксперта. Данная функция должна вызываться из функции OnInit().

void conn_mon_init()
{
   cm.connected = true;   
   cm.conn_cnt = inp_conn_hyst;
   cm.disconn_cnt = 0;
   cm.conn_time = TimeLocal();
   cm.new_state = false;
} 

Скомпилируйте и запустите эксперт. Затем попробуйте отключить компьютер от интернета. Через 60 (плюс-минус 10) секунд вам придёт сообщение о разрыве связи с сервером. Подключите интернет. Через 60 секунд вам придёт сообщение о восстановлении связи с указанием времени отсутствия соединения:

Уведомлени о разрыве соединенияУведомление о восстановлении соединения

Рис. 9. SMS-уведомления о разрыве и восстановлении связи с торговым сервером


7. Отправка отчётов об открытии и закрытии позиций

Для мониторинга открытия и закрытия позиций добавим в функцию OnTradeTransaction() следующий код:

void OnTradeTransaction(const MqlTradeTransaction& trans,
                        const MqlTradeRequest& request,
                        const MqlTradeResult& result)
{
//---
   if(trans.type==TRADE_TRANSACTION_DEAL_ADD)
   {
      if(trans.deal_type==DEAL_TYPE_BUY ||
         trans.deal_type==DEAL_TYPE_SELL)
      {
         int i;
         for(i=0;i<POS_BUF_LEN;i++)
         {
            if(ps[i].new_event==false)
               break;
         }   
         if(i<POS_BUF_LEN)
         {
            ps[i].new_event = true;
            ps[i].deal_type = trans.deal_type;
            ps[i].symbol = trans.symbol;
            ps[i].volume = trans.volume;
            ps[i].price = trans.price;
            ps[i].deal = trans.deal;
         }
      }
   }    
}

где ps - это буфер структур типа POS_STR:

struct POS_STR
{
   bool new_event;
   string symbol;
   ulong deal;
   ENUM_DEAL_TYPE deal_type; 
   double volume;
   double price;
};  

#define POS_BUF_LEN  3

POS_STR ps[POS_BUF_LEN];

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

В функцию OnTimer() добавим следующий код, который будет отслеживать флаги new_event и формировать SMS-отчёты:

   //--- обработка открытия-закрытия позиций
   string posstr="";
   for(int i=0;i<POS_BUF_LEN;i++)
   {
      if(ps[i].new_event==true)
      {
         string str;
         if(ps[i].deal_type==DEAL_TYPE_BUY)
            str+= "Buy ";
         else if(ps[i].deal_type==DEAL_TYPE_SELL)
            str+= "Sell ";
         str+= DoubleToString(ps[i].volume,2)+" "+ps[i].symbol;     
         int digits = (int)SymbolInfoInteger(ps[i].symbol,SYMBOL_DIGITS);
         str+= ", price="+DoubleToString(ps[i].price,digits);
         //
         long deal_entry;
         HistorySelect(TimeCurrent()-3600,TimeCurrent());//поднимаем историю за последний час
         if(HistoryDealGetInteger(ps[i].deal,DEAL_ENTRY,deal_entry)==true)
         {
            if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_IN)
               str+= ", entry: in";
            else if(((ENUM_DEAL_ENTRY)deal_entry)==DEAL_ENTRY_OUT)
            {
               str+= ", entry: out";
               double profit;
               if(HistoryDealGetDouble(ps[i].deal,DEAL_PROFIT,profit)==true)
               {
                  str+= ", profit = "+DoubleToString(profit,2);
               }
            }
         }           
         posstr+= str+"\r\n";
         ps[i].new_event=false;
      }
   }    
   if(posstr!="")
   {
      Print(posstr+"pos: "+DoubleToString(PositionsTotal(),0));
      SendSMS(inp_admin_number, posstr+"pos: "+DoubleToString(PositionsTotal(),0), false);
   }

Компилируем и запускаем эксперт. Попробуем купить AUDCAD, размер лота 0.14. Эксперт отправит SMS-сообщение "Buy 0.14 AUDCAD, price=0.96538, entry: in". Затем через некоторое время закроем позицию, и получим SMS-сообщение о закрытии:

Сообщение об открытии позицииСообщение о закрытии позиции

Рис. 10. SMS-уведомления об открытии (entry: in) и закрытии позиции (entry: out)


8. Обработка входящих SMS-сообщений для управления открытыми позициями

До этого момента наш эксперт только отправлял информационные сообщения на номер администратора. Теперь научим его принимать и выполнять SMS-команды. Это может быть полезно, например, для закрытия всех или части открытых позиций. Ведь, как известно, нет ничего лучше вовремя закрытой позиции.

Но для начала нужно убедиться, что приём SMS-сообщений работает корректно. Для этого в функцию IncomingSMS() добавим вывод последнего принятого сообщения на экран:

//+------------------------------------------------------------------+
//| Вызывается при получении нового смс-сообщения                    |
//+------------------------------------------------------------------+
void IncomingSMS(INCOMING_SMS_STR& sms)
{
   string str, strtmp;
   //Номер отправителя последнего принятого SMS-сообщения:
   ObjectSetString(0, "str18", OBJPROP_TEXT, "SMS number:      "+sms.sender);
   //Дата и время отправки последнего принятого SMS-сообщения:
   str = TimeToString(sms.scts.time,TIME_DATE|TIME_SECONDS);
   ObjectSetString(0, "str19", OBJPROP_TEXT, "SMS date/time:   "+str);
   //Текст последнего принятого SMS-сообщения:
   strtmp = StringSubstr(sms.text, 0, 32); str = " ";
   if(strtmp!="") str = strtmp;    
   ObjectSetString(0, "str20", OBJPROP_TEXT, str);
   strtmp = StringSubstr(sms.text,32, 32); str = " ";
   if(strtmp!="") str = strtmp;    
   ObjectSetString(0, "str21", OBJPROP_TEXT, str);
   strtmp = StringSubstr(sms.text,64, 32); str = " ";
   if(strtmp!="") str = strtmp;    
   ObjectSetString(0, "str22", OBJPROP_TEXT, str);
   strtmp = StringSubstr(sms.text,96, 32); str = " ";
   if(strtmp!="") str = strtmp;    
   ObjectSetString(0, "str23", OBJPROP_TEXT, str);
   strtmp = StringSubstr(sms.text,128,32); str = " ";
   if(strtmp!="") str = strtmp;    
   ObjectSetString(0, "str24", OBJPROP_TEXT, str);  
}

Если теперь отправить на модем SMS-сообщение, оно отобразится в таблице:

Новое SMS-сообщение

Рис. 11. Отображение входящего SMS-сообщения в окне терминала

Обратите внимание, что во вкладке "Эксперты" отображаются все входящие SMS-сообщения в виде <индекс_в_памяти_модема>текст_сообщения:

SMS-сообщение во вкладке "Эксперты"

Рис. 12. Отображение входящего SMS-сообщения во вкладке "Эксперты"

В качестве команды для закрытия сделок будет слово "close". За ним через пробел должен следовать параметр - символ той позиции, которую нужно закрыть, или "all", если нужно закрыть все позиции. Регистр значения не имеет, так как перед обработкой текста сообщения мы используем функцию StringToUpper(). Перед разбором сообщения обязательно проверяем, что номер отправителя совпадает с заданным номером администратора.

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

input int            inp_sms_max_old=600; //Срок действия смс-команды, сек

Значение 600 говорит о том, что команды, на доставку которых ушло более 600 секунд (10 минут) будут проигнорированы. Обратите внимание, что используемый в примере способ проверки времени доставки сообщения подразумевает, что сервис-центр SMS и машина, на которой запущен эксперт, находятся в одном часовом поясе.

Для обработки команд в SMS-сообщениях допишем в функцию IncomingSMS() следующий код:

   if(sms.sender==inp_admin_number)
   {
      Print("Смс от администратора");
      datetime t = TimeLocal();
      //--- проверим, что сообщение не просрочено
      if(t-sms.scts.time<=inp_sms_max_old)
      {//--- проверим, что сообщение является командой
         string cmdstr = sms.text;
         StringToUpper(cmdstr);//переводим всё в верхний регистр
         int pos = StringFind(cmdstr, "CLOSE", 0);
         cmdstr = StringSubstr(cmdstr, pos+6, 6);
         if(pos>=0)
         {//--- команда. отправляем на обработку
            ClosePositions(cmdstr);            
         } 
      }
      else
         Print("смс-команда устарела");
   }   

Если SMS-сообщение пришло от администратора, оно не просрочено и является командой (содержит ключевое слово "Close"), отправляем его параметр на обработку, которую выполняет функция ClosePositions():

uint ClosePositions(string sstr)
{//--- закрыть указанные позиции
   bool all = false;
   if(StringFind(sstr, "ALL", 0)>=0)
      all = true;
   uint res = 0;
   for(int i=0;i<PositionsTotal();i++)
   {
      string symbol = PositionGetSymbol(i);
      if(all==true || sstr==symbol)
      {
         if(PositionSelect(symbol)==true)
         {
            long pos_type;
            double pos_vol;
            if(PositionGetInteger(POSITION_TYPE,pos_type)==true)
            {
               if(PositionGetDouble(POSITION_VOLUME,pos_vol)==true)
               {
                  if(OrderClose(symbol, (ENUM_POSITION_TYPE)pos_type, pos_vol)==true)
                     res|=0x01;
                  else
                     res|=0x02;   
               }
            }
         }
      }
   }
   return(res);
}

Данная функция проверяет, есть ли среди открытых позиций соответствующие пришедшему в команде параметру (символу). Позиции, прошедшие это условие, закрываются при помощи функции OrderClose():

bool OrderClose(string symbol, ENUM_POSITION_TYPE pos_type, double vol)
{
   MqlTick last_tick;
   MqlTradeRequest request;
   MqlTradeResult result;
   double price = 0;
   //
   ZeroMemory(request);
   ZeroMemory(result);
   //
   if(SymbolInfoTick(Symbol(),last_tick))
   {
      price = last_tick.bid;
   }
   else
   {
      Print("Ошибка получения текущих цен");
      return(false);
   }
   //   
   if(pos_type==POSITION_TYPE_BUY)
   {//--- закрытие позиции BUY - ПРОДАЖА(SELL)
      request.type = ORDER_TYPE_SELL;
   }
   else if(pos_type==POSITION_TYPE_SELL)
   {//--- закрытие позиции SELL - ПОКУПКА(BUY)
      request.type = ORDER_TYPE_BUY;
   }
   else
      return(false);
   //
   request.price = NormalizeDouble(price, _Digits);
   request.deviation = 20;
   request.action = TRADE_ACTION_DEAL;
   request.symbol = symbol;
   request.volume = NormalizeDouble(vol, 2);
   if(request.volume==0)
      return(false);
   request.type_filling = ORDER_FILLING_FOK;
   //
   if(OrderSend(request, result)==true)
   {
      if(result.retcode==TRADE_RETCODE_DONE || result.retcode==TRADE_RETCODE_DONE_PARTIAL)
      {
         Print("Ордер выполнен успешно");
         return(true);
      }
   }
   else
   {
      Print("Ошибка параметров ордера: ", GetLastError(),", Код возврата торгового сервера: ", result.retcode);     
      return(false);
   }      
   //
   return(false);
}

При успешной обработке ордеров функция мониторинга изменения позиций сформирует и отправит SMS-уведомление.


9. Удаление сообщений из памяти модема

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

//+------------------------------------------------------------------+
//| Память SMS переполнена                                           |
//+------------------------------------------------------------------+
void SMSMemoryFull(int n)
{
   sms_mem_full = true;
   for(int i=0; i<n; i++)
   {//удаляем все SMS-сообщения
      if(DelSMSbyIndex(i)==false)
         break;
      else
         sms_mem_full = false;   
   }
}

Также можно удалять SMS-сообщения сразу после их обработки. При вызове обработчиком модема функции IncomingSMS(), в составе структуры INCOMING_SMS_STR передаётся индекс сообщения в памяти модема, что позволяет удалить сообщение сразу после его обработки, при помощи функции DelSMSbyIndex():

//+------------------------------------------------------------------+
//| Вызывается при получении нового смс-сообщения                    |
//+------------------------------------------------------------------+
void IncomingSMS(INCOMING_SMS_STR& sms)
{//
   /*
   ... обработка сообщения ...
   */
   DelSMSbyIndex(sms.index);//удаляем сообщение
}


Заключение

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

В заключение давайте проверим, как будет вести себя наш эксперт с мобильным телефоном 10-летней давности. Аппарат - Siemens M55. Подключаем:

Параметры Siemens M55Siemens M55

Рис. 13. Подключение телефона Siemens M55

Siemens M55, вкладка "Эксперты"

Рис. 14. Успешная инициализация телефона Siemens M55, вкладка "Эксперты"

Как видно, все необходимые параметры получены, проблема только с данными, которые получаем из USSD-запросов. Дело в том, что Siemens M55 не поддерживает AT-команду, которая предназначена для работы с USSD-запросами. В остальном, по функциональности он не уступает современному модему, и может быть использован для работы с нашим экспертом.


Прикрепленные файлы |
trcomport.zip (31.11 KB)
gsminformer.mq5 (25.36 KB)
comport.mqh (5.25 KB)
modem.mqh (44.56 KB)
sms.mqh (9.44 KB)
Последние комментарии | Перейти к обсуждению на форуме трейдеров (11)
revers45
revers45 | 7 янв. 2014 в 18:18
decanium:
Совершенно верно. И дай бог, чтобы вышки оператора всегда работали исправно, и запаянная лапша никогда так и не пригодилась.
...и КЗ в мобилках, от кустарной пайки лапши, не мешало им коннектиться к исправно работающим вышкам.
Serhii Shevchuk
Serhii Shevchuk | 7 янв. 2014 в 18:43
revers45:
не мешало им коннектиться к исправно работающим вышкам.

Ключевой момент:

decanium:
позволит вам находиться на связи с торговым терминалом даже в той ситуации, когда мобильный интернет будет недоступен, а именно - при помощи звонков и SMS-сообщений. Также, данный эксперт известит вас о разрыве и восстановлении связи с торговым сервером.

Я лишь предложил альтернативу на крайний случай. Поверьте, интернет обычно пропадает именно тогда, когда он больше всего нужен.

Допустим, вы в поезде. Поезд народный, без Wi-Fi (да, такие ещё ездят). Покрытие GPRS/3G между большинством станций тоже оставляет желать лучшего. Вы получаете отчёты о работе вашего эксперта в виде SMS-уведомлений. Вы видите, что эксперт начинает по какой-то причине неожиданно сливать (фактор, по которому эксперт должен остановиться, тоже по какой-то причине не срабатывает). Вы отправляете эксперту SMS-команду и таким образом останавливаете его вручную, до выяснения.

Хорошо, если это не понадобится.

Serhii Shevchuk
Serhii Shevchuk | 7 янв. 2014 в 18:54
Поймите меня правильно. Я не призываю всех дружно втыкать модемы в терминал. И откатов от Huawei тоже не получаю. Я считаю, что данная технология имеет право на жизнь, и буду рад, если это хоть кому-то поможет.
Igor Makanu
Igor Makanu | 17 февр. 2014 в 11:38
decanium:Вы отправляете эксперту SMS-команду и таким образом останавливаете его вручную, до выяснения.

Вот за что всегда уважал настоящих программистов - так это за их нежелание искать схемно-техническое решение.

Купите "GSM Switch" и отключите ПК. ;)

За статью спасибо - познавательно!

Sergey Kozlov
Sergey Kozlov | 28 дек. 2015 в 09:33
Здравствуйте! Я не работаю с мт5, в связи с этим вопрос-есть ли советник для мт4?
Основы программирования на MQL5 - Списки Основы программирования на MQL5 - Списки
Новая версия языка программирования торговых стратегий - MQL [MQL5] - имеет более эффективный и мощный инструментарий по сравнению с предыдущей [MQL4]. И это преимущество прежде всего относится к средствам объектно-ориентированного программирования. В данной статье рассматривается возможность использования такого пользовательского типа данных, относящегося к сложному, как узлы и списки. Приводится пример использования списков при программировании практических задач в MQL5.
Создание мультивалютного мультисистемного советника Создание мультивалютного мультисистемного советника
В статье представлена схема создания советника, торгующего сразу по нескольким торговым системам на нескольких символах. Если для всех своих советников вы уже подобрали наилучшие входные параметры и тестирование на истории показало хорошие результаты отдельно по каждому из них, то задайтесь вопросом - а как бы выглядел суммарный результат одновременного тестирования всех советников, имея все стратегии в одном "флаконе".
Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5 Рецепты MQL5 - Разработка мультивалютного индикатора волатильности на MQL5
В этой статье рассмотрим разработку мультивалютного индикатора волатильности. Начинающие разработчики на MQL5 могут столкнуться с некоторыми сложностями при разработке мультивалютных индикаторов, но после прочтения этой статьи все станет намного проще. Основные вопросы при разработке мультивалютного индикатора относятся к синхронизации данных других символов по отношению к текущему символу, решению проблемы отсутствия части данных индикатора, определению начала "истинных" баров таймфрейма. Все это будет подробно рассматриваться в статье.
Повышаем эффективность линейных торговых систем Повышаем эффективность линейных торговых систем
В сегодняшней статье речь пойдет о том, как MQL5-программисты со средним уровнем подготовки могут повысить прибыльность своих линейных торговых систем, используя так называемую технику возведения в степень (technique of exponentiation). Данная техника названа именно так, потому что при ее использовании кривая средств приобретает геометрическую, или экспоненциальную, форму, становясь похожей на параболу. В частности, мы реализуем на языке MQL5 фиксированно-фракционный (Fixed Fractional) метод Ральфа Винса.