СМС-извещения о состоянии Советника

Дмитрий | 29 июля, 2009

Введение

Работа частенько отвлекает меня от того, чтобы быть рядом и наблюдать за Терминалом. Иногда это - 20-30 минут, иногда - сутки. Поэтому я решил организовать себе систему СМС-извещений о критических ситуациях, будь-то отключение света или если компьютер не смог "проснуться" после выходных. Уверен, что даже в том варианте готовности, в котором система представлена, она сможет помочь многим трейдерам, а те, кому её как таковой мало, смогут на базе этой идеи сформировать свой "шедевр".

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


План

  1. Возможности Google Calendar
  2. Немного об установке Google Data API
  3. Блок-схема работы системы
  4. Код Java приложения
  5. Код .bat файла
  6. Код в Советнике
  7. Слабые стороны
  8. Заключение


Возможности Google Calendar

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

Если вкратце объяснить принцип работы, то выйдет так: если до 9:59 (по скриншоту) ничто не удалит данное событие, то есть если Терминал не выйдет на связь с Сервером Google и не заменит это событие на следующее, то на телефон отправится СМС с "тревогой".


Немного об установке Google Data API

Для программной работы со своими сервисами Google предоставляет документацию API данных Google.
Документация Java-разработчику по работе с Календарем находится по следующей ссылке Google Calendar APIs and Tools.

Для программирования вам потребуется установить Google Data Java Client Library. На этой же страничке есть ссылка на документацию о настройке библиотеки для Eclipse, сам использовал именно этот способ, поэтому и вам рекомендую Using Eclipse with Google Data APIs.


Блок-схема работы системы

Считаю нужным сперва немного прокомментировать блок-схему:



Код Java-приложения

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

package Calendar;
/* INSTRUCTION: This is a command line application. 
    So please execute this template with the following arguments:

                arg[0] = username
                arg[1] = password
*/

import com.google.gdata.client.GoogleService;
import com.google.gdata.client.Query;
import com.google.gdata.client.calendar.CalendarService;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.PlainTextConstruct;
import com.google.gdata.data.calendar.CalendarEntry;
import com.google.gdata.data.calendar.CalendarEventEntry;
import com.google.gdata.data.calendar.CalendarEventFeed;
import com.google.gdata.data.calendar.CalendarFeed;
import com.google.gdata.data.extensions.Reminder;
import com.google.gdata.data.extensions.When;
import com.google.gdata.data.extensions.Reminder.Method;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

public class Calendar {

    public static void main(String[] args) {

        try {
            //--- ВНЕШНИЕ ПЕРЕМЕННЫЕ ----------------------------
            String alertTxt = args[2];
            String reanimationTxt = args[3];
            String stopSMS = args[4];
            int timePeriod = Integer.parseInt(args[5]); // период в минутах, с которым терминал 
                                                        // будет выходить на связь
            int mode = Integer.parseInt(args[6]);
            int startHour = Integer.parseInt(args[7]);
            int startMin = Integer.parseInt(args[8]);
            //---------------------------------------------------
            //--- ВНУТРЕНИЕ ПЕРЕМЕННЫЕ --------------------------
            URL feedUrl = new URL("http://www.google.com/calendar/feeds/default/private/full");
            //ссылка для работы с событиями
            URL calendarUrl = new URL("http://www.google.com/calendar/feeds/default/allcalendars/full");
            //ссылка для работы с календарями   
            //---------------------------------------------------
            System.out.println(">>----------  Start   ----------<<");

            // Создаем новый Calendar service, коннект к Google
            CalendarService myService = new CalendarService("My Application");
            myService.setUserCredentials(args[0], args[1]);    // ЛОГИН И ПАРОЛЬ В КАЧЕСТВЕ АРГУМЕНТОВ

            // ОБЪЯВЛЕНИЕ ПЕРЕМЕННЫХ ДЛЯ ПОИСКА СОБЫТИЯ
            Query myQuery = new Query(feedUrl);
            CalendarEventFeed myResultsFeed;
            CalendarEventEntry firstMatchEntry = null;
            String myEntryTitle;
            URL deleteUrl;
            When timeVar;
            SimpleDateFormat formater = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
            Date newDate = new Date();
            CalendarEventEntry myEntry;
            When eventTimes = new When();
            DateTime startTime = null;
            DateTime endTime = null;
            int reminderMinutes;
            Method methodType = Method.SMS;
            Reminder reminder = new Reminder();
            CalendarEventEntry insertedEntry;

            // ГОТОВИМ ПОИСКОВЫЙ ЗАПРОС
            myQuery.setFullTextQuery(stopSMS);            // СОБЫТИЕ С ТЕКСТОМ stopSMS
            myResultsFeed = myService.query(myQuery, CalendarEventFeed.class);
            if (myResultsFeed.getEntries().size() > 0) {
                System.out.println(">> STOP event found. \n>> Finish");
                return;
            }

            // ГОТОВИМ ПОИСКОВЫЙ ЗАПРОС
            myQuery.setFullTextQuery(reanimationTxt);    // СОБЫТИЕ С ТЕКСТОМ reanimationTxt
            myResultsFeed = myService.query(myQuery, CalendarEventFeed.class);
            if (myResultsFeed.getEntries().size() > 0) {    // ЕСЛИ УДАЛОСЬ НАЙТИ
                System.out.println(">> REANIMATION event found");
                // У ПЕРВОГО ПОПАВШЕГОСЯ СОБЫТИЯ СПРАШИВАЕМ ЗАГОЛОВОК
                firstMatchEntry = (CalendarEventEntry) myResultsFeed.getEntries().get(0);
                myEntryTitle = firstMatchEntry.getTitle().getPlainText();

                // ЕСЛИ НАШЛИ СОБЫТИЕ reanimationTxt ТО УДАЛЯЕМ ЕГО
                deleteUrl = new URL(firstMatchEntry.getEditLink().getHref());
                myService.getRequestFactory().setHeader("If-Match", "*");
                myService.delete(deleteUrl);
                System.out.println(">> ...deleting REANIMATION event");
            }

            // ГОТОВИМ ПОИСКОВЫЙ ЗАПРОС
            myQuery.setFullTextQuery(alertTxt);            // СОБЫТИЕ С ТЕКСТОМ alertTxt
            myResultsFeed = myService.query(myQuery, CalendarEventFeed.class);
            if (myResultsFeed.getEntries().size() > 0) {    // ЕСЛИ УДАЛОСЬ НАЙТИ
                System.out.println(">>  >> ALERT event found");
                // У ПЕРВОГО ПОПАВШЕГОСЯ СОБЫТИЯ СПРАШИВАЕМ ЗАГОЛОВОК
                firstMatchEntry = (CalendarEventEntry) myResultsFeed.getEntries().get(0);
                myEntryTitle = firstMatchEntry.getTitle().getPlainText();

                timeVar = firstMatchEntry.getTimes().get(0);
                System.out.println(">>  >> event start&stop time         : " 
                        + timeVar.getStartTime() + "\n>>        >>                               : " 
                        + timeVar.getEndTime());
                System.out.println(">>  >> event start     (milliseconds): " 
                        + timeVar.getStartTime().getValue());
                System.out.println(">>  >> new event start (milliseconds): " 
                        + (newDate.getTime() - (1000 * 60 * timePeriod)));

                if (timeVar.getStartTime().getValue() > newDate.getTime() - (1000 * 60 * 0)) {    
                    // ЕСЛИ У СТАРОГО СОБЫТИЯ ВРЕМЯ НАЧАЛА МЕНЬШЕ ЧЕМ timePeriod МИНУТ НАЗАД
                    // УДАЛЯЕМ ЕГО
                    deleteUrl = new URL(firstMatchEntry.getEditLink().getHref());
                    myService.getRequestFactory().setHeader("If-Match", "*");
                    myService.delete(deleteUrl);
                    System.out.println(">>              >> event start > new event start");
                    System.out.println(">>              >> ...deleting event");
                    System.out.println(">>              >>    #M###      ##M    #M#  ");
                    System.out.println(">>              >>   ########    ###   ###   ");
                    System.out.println(">>              >>   ###  ###    ###  ###    ");
                    System.out.println(">>              >>  ###    ###   ### ###     ");
                    System.out.println(">>              >>  M##    ###   ######M     ");
                    System.out.println(">>              >>  ###    ###   #### ###    ");
                    System.out.println(">>              >>  ###    ###   ###  ###    ");
                    System.out.println(">>              >>  ###    ###   #M#   ###   ");
                    System.out.println(">>              >>   ###  ###    ###   ###   ");
                    System.out.println(">>              >>   ########    ###    ###  ");
                    System.out.println(">>              >>     ####      ###     ### ");
                } else {    // ВРЕМЯ БОЛЬШЕ ЧЕМ timePeriod МИНУТ НАЗАД, А ЗНАЧИТ смс УЖЕ УШЛА
                    // УДАЛЯЕМ ЕГО
                    deleteUrl = new URL(firstMatchEntry.getEditLink().getHref());
                    myService.getRequestFactory().setHeader("If-Match", "*");
                    myService.delete(deleteUrl);
                    System.out.println(">>              >> SMS has gone already");
                    System.out.println(">>              >> event start < new event start");
                    System.out.println(">>              >> ...deleting event");
                    System.out.println(">>              >>   .M####   M##M   ###M   .#####   ");
                    System.out.println(">>              >>  ########  ####   ####  ########  ");
                    System.out.println(">>              >>  ###  ###  ####   ####  #M#  ###  ");
                    System.out.println(">>              >>  ###       ###M# #####  ###       ");
                    System.out.println(">>              >>  #M####    ##### #####  ######    ");
                    System.out.println(">>              >>   ######   ### # # @##   #M####   ");
                    System.out.println(">>              >>     #####  ### # # ###     #####  ");
                    System.out.println(">>              >>  #M#  ###  ### # # ###  M##  ###  ");
                    System.out.println(">>              >>  ###  ###  ### ### ###  ###  ###  ");
                    System.out.println(">>              >>   ######   ### ### ###   ######   ");
                    System.out.println(">>              >>    ####    ###  #  ###    ####    ");
                    // СОЗДАЕМ НОВОЕ СОБЫТИЕ
                    myEntry = new CalendarEventEntry();
                    myEntry.setTitle(new PlainTextConstruct(reanimationTxt));  // ЗАДАЕМ ЕГО ЗАГОЛОВОК

                    newDate.setTime(newDate.getTime() + 1000 * 60 * 1); // ТЕКУЩЕЕ ВРЕМЯ + 1 МИНУТa
                    startTime = DateTime.parseDateTime(formater.format(newDate));        
                    // ВСТАВЛЯЕТ СТРОКУ ПО ШАБЛОНУ "2009-06-30T20:55:00"

                    System.out.println(">>              >> creating REANIMATION event");
                    System.out.println(">>              >> event starttime: " + formater.format(newDate));

                    newDate.setTime(newDate.getTime() + 1000 * 60 * 0); // ПРИБАВИМ ЕЩЕ РАЗ ТЕКУЩЕЕ ВРЕМЯ 
                                                                        // + 1 МИНУТa
                    endTime = DateTime.parseDateTime(formater.format(newDate));

                    System.out.println(">>              >> event stoptime : " + formater.format(newDate));

                    eventTimes.setStartTime(startTime);
                    eventTimes.setEndTime(endTime);
                    myEntry.addTime(eventTimes);

                    reminderMinutes = 0;  // ИЗВЕСТИТЬ ЗА 1 МИНУТУ

                    reminder.setMinutes(reminderMinutes);
                    reminder.setMethod(methodType);
                    myEntry.getReminder().add(reminder);

                    // СОЗДАЕМ НОВЫЙ Calendar service, КОННЕКТ К Google
                    CalendarService tempService = new CalendarService("My Application");
                    tempService.setUserCredentials(args[0], args[1]);     // ЛОГИН И ПАРОЛЬ В КАЧЕСТВЕ АРГУМЕНТОВ
                    // СТАВИМ СОБЫТИЕ В ОЧЕРЕДЬ НА ИСПОЛЕНЕНИЕ
                    tempService.insert(feedUrl, myEntry);
                }
            }

            // СОЗДАЕМ НОВЫЙ Calendar service, КОННЕКТ К Google
            CalendarService basicService = new CalendarService("basic Application");
            basicService.setUserCredentials(args[0], args[1]);             // ЛОГИН И ПАРОЛЬ В КАЧЕСТВЕ АРГУМЕНТОВ

            // СОЗДАЕМ НОВОЕ СОБЫТИЕ
            CalendarEventEntry basicEntry = new CalendarEventEntry();
            basicEntry.setTitle(new PlainTextConstruct(alertTxt));        // ЗАДАЕМ ЕГО ЗАГОЛОВОК

            if (mode == 0) {    // ОБЫЧНАЯ ДНЕВНАЯ СИТУАЦИЯ
                newDate.setTime(newDate.getTime() + 1000 * 60 * timePeriod);  // ТЕКУЩЕЕ ВРЕМЯ + timePeriod МИНУТ
                startTime = DateTime.parseDateTime(formater.format(newDate));
                // ВСТАВЛЯЕТ СТРОКУ ПО ШАБЛОНУ "2009-06-30T20:55:00"

                newDate.setTime(newDate.getTime() + 1000 * 60 * 0); // ПРИБАВИМ ЕЩЕ РАЗ ТЕКУЩЕЕ ВРЕМЯ
                                                                    // + timePeriod МИНУТ
                endTime = DateTime.parseDateTime(formater.format(newDate));
            }
            if (mode == 1) {    // СИТУАЦИЯ НАЗНАЧЕНИЯ СОБЫТИЯ НА СЛЕДУЮЩЕЕ УТРО, ИЛИ УТРО ПОНЕДЕЛЬНИКА
                Date curDate = new Date();
                GregorianCalendar gDate = new GregorianCalendar();
                gDate.setTime(curDate);
                // УСТАНАВЛИВАЕМ ЧАСЫ И МИНУТЫ В НЕОБХОДИМЫЕ
                gDate.set(GregorianCalendar.HOUR_OF_DAY, startHour);
                gDate.set(GregorianCalendar.MINUTE, startMin);
                gDate.set(GregorianCalendar.SECOND, 0);

                if (gDate.get(GregorianCalendar.DAY_OF_WEEK) < 6) {    // ПН - ЧТ
                    gDate.set(GregorianCalendar.DAY_OF_MONTH, gDate.get(GregorianCalendar.DAY_OF_MONTH) + 1);
                } else {    // ПТ
                    gDate.set(GregorianCalendar.DAY_OF_MONTH, gDate.get(GregorianCalendar.DAY_OF_MONTH) + 3);
                }
                // System.out.println("gDate "+ gDate.getTime());
                startTime = DateTime.parseDateTime(formater.format(gDate.getTime()));
                gDate.set(GregorianCalendar.MINUTE, gDate.get(GregorianCalendar.MINUTE) + timePeriod);
                endTime = DateTime.parseDateTime(formater.format(gDate.getTime()));
                System.out.println(">> nextday event should be created");
            }
            //System.out.println("создаю событие УПАЛ!");
            System.out.println(">> creating ALERT event");
            System.out.println(">> event starttime: " + startTime.toString());
            System.out.println(">> event stoptime : " + endTime.toString());

            eventTimes.setStartTime(startTime);
            eventTimes.setEndTime(endTime);
            basicEntry.addTime(eventTimes);

            reminderMinutes = 0;  // ИЗВЕСТИТЬ ЗА 1 МИНУТУ

            reminder.setMinutes(reminderMinutes);
            reminder.setMethod(methodType);
            basicEntry.getReminder().add(reminder);

            // СТАВИМ СОБЫТИЕ В ОЧЕРЕДЬ НА ИСПОЛЕНЕНИЕ
            basicService.insert(feedUrl, basicEntry);

            System.out.println(">>----------  Finish  ----------<<");
        }
        catch (AuthenticationException e) {
            e.printStackTrace();
        }
        catch (MalformedURLException e) {
            e.printStackTrace();
        }
        catch (ServiceException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
    }
}


Код .bat файла

Так как создание бат-файлов в MQL4 пока невозможно, то на практике я использую 2 бат-файла, которые отличаются Режимом(Mode).

@rem +------------------------------------------------------------------+
@rem |         Прикладное приложение производит отправку СМСок          |
@rem |      ------------------------------------------------------      |
@rem | Правила:                                                         |
@rem |                                                                  |
@rem | USER   -   учетная запись отправителя                            |
@rem | PASS   -   пароль отправителя                                    |
@rem | ALER   -   Alert, тестовка предупредительного сообщения          |
@rem |              (использовать только латиницу)                  |
@rem | REAN   -   Reanimated, текст сообщения о восстановлении связи    |
@rem |                  (использовать только латиницу)                  |
@rem | STOP   -   STOP, заголовок события, которое запрещает выполнение |
@rem |                  действий программы                              |
@rem |                  (использовать только латиницу)                  |
@rem | PERI   -   Period, период-таймер на отправку сообщения Alert     |
@rem | MODE   -   Режим                                                 |
@rem |                  (0 - обычная работа,                            |
@rem |                   1 - отправка сообщения на завтрашнее утро или  |
@rem |                        утро понедельника)                        |
@rem | HOUR   -   Час "завтрашнего утра, или утра понедельника", на     |
@rem |                   который необходимо поставить сообщение.        |
@rem | MINU   -   Minutes, минуты "завтрашнего утра..."                 |
@rem +------------------------------------------------------------------+

@rem ECHO off

set USER=forex.myaccount
set PASS=mypassword
set ALER=Terminal_Alert
set REAN=Trading_Resolved
set STOP=STOP
set PERI=5
set MODE=0
set HOUR=8
set MINU=20

set ARGS= %USER% %PASS% %ALER% %REAN% %STOP% %PERI% %MODE% %HOUR% %MINU%
     
    java -jar "d:\Documents\forex\Deltabank Trader 4\TerminalWatch\terminalWatcher.jar" %ARGS% 

@rem ECHO %ARGS%


Код в советнике

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

Ну а в конце торговой сессии я вызываю функцию для установки СМС на следующее утро.

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

//+------------------------------------------------------------------+
//| Отправляет статусное SMS                                         |
//+------------------------------------------------------------------+
int sendSMS()
{
      string destination = StringConcatenate(TerminalPath(),"\TerminalWatch\launch.lnk");
      ShellExecuteA(WindowHandle(Symbol(),0),"open", destination, NULL, NULL,1);
}
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//| Отправляет статусное SMS на следующее утро                       |
//+------------------------------------------------------------------+
int sendSMSnextDate()
{
      string destination = StringConcatenate(TerminalPath(),"\TerminalWatch\launch_next_date.lnk");
      ShellExecuteA(WindowHandle(Symbol(),0),"open", destination, NULL, NULL,1);
}
//+------------------------------------------------------------------+


Слабые стороны

Небольшая ложка дегтя - на практике очень трудно подстроится под синхронную работу часов на Терминале, на Сервер Котировок и на Сервере Google, поэтому в коде советника использую такую методику: каждые 5 минут устанавливаю смс с таймером через 8 минут. Такая тактика в 95% случаев работает хорошо, но пока я все еще сталкиваюсь с оставшимися 5% "дегтя".


Заключение

Всем, кому интересно, ну и кто не поленится заиметь себе аккаунт на Google, будет достаточно просто проверить, на что способна моя нехитрая система.

Повторюсь, многие детали Java-разработки мне не до конца понятны. Я использовал помощь моих товарищей девелоперов (Дима, большое спасибо!), так что не на все вопросы смогу ответить полноценно.

Большое спасибо за внимание!


Добавлено по результатом обсуждения статьи

В текущей версии учтено полезное замечание komposter по логике работы:

К статье прикреплен файл: TerminalWatch_03.rar