Делаем отчёт в Word

22 января 2023, 21:44
Maxim Kuznetsov
1
89

Используя ATcl сделаем основу для торгового отчёта. Чтобы было просто красиво и современно - отчёт будем формировать сразу в Word, без промежуточных CSV,XML и потусторонних сервисов

начнём с постановки задачи и сценария использования :

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

набрасываем эскиз отчётика:

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

Читаем документацию

надо будет снимать скриншот, значит читаем про скриншоты https://www.mql5.com/ru/docs/chart_operations/chartscreenshot

надо формировать документ Word, значит читаем про Word https://www.tcl3d.org/cawt/download/CawtReference-Word.html CAWT - пакет для взаимодействия с Office через COM-интерфейс. Включен в состав ATcl

Пишем скрипт Tcl

Задействуем объектную ориентацию (хотя-бы чтобы вы с ней ознакомились). То есть сделаем класс с инстансами которого будем взаимодейстовать:

  • класс WordReport
  • создание инстанса без параметров
  • методами Set задаём разные опции, текст разделов, расположение скриншота и так далее
  • методы AddOpen, AddClose добавляют данные о сделках для таблиц
  • вызываем метод Publish который собственно и сделает публикацию в Word

минимальные опции исходя из макеты у нас следующие :

  • header - большой заголовок «Торговый отчёт EURUSD дата»
  • text - текст заполнитель, предполагается что пользователь потом в Word будет его править
  • screenshot - путь к снятому MetaTrader скриншоту

в каталоге Scripts будем писать два файла:  wordreport.tcl  с интерфесом класса и  wordreport_test.tcl  для его тестирования и отладки. Как только результат wordreport_test нас удовлетворит перейдём к MQL

в первом приближении файлы получаются такие:

# задействуем пакет cawt
package require cawt

# класс "отчётец в word"
oo::class create WordReport {
variable Options        ;# натраиваемые опции
 
constructor {} {
        array set Options {}
        set Options(openOrders) {}
        set Options(closeOrders) {}
        set Options(header) "Торговый отчёт"
        set Options(screenshot) ""
        set Options(text) "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
}
destructor {
}
method Set { name value } {
        set Options($name) $value
}
method AddOpen { args } {
        lappend Options(openOrders) $args
}
method AddClose { args } {
        lappend Options(closeOrders) $args
}
method Publish {} {
        if { ! [ info exists ::MetaTrader ] } {
                parray Options
        }
}
# делаем методы публичными
export Set AddOpen AddClose Publish
}; #/class WordReport

и

source -encoding utf-8 wordreport.tcl

# тестовый срипт расположен в mql5/Scripts
# а MetaTrader работает на два каталога выше,
# эмулируем - сменим текущий каталог
cd ../..
# создаём объект отчёта
set reporter [ WordReport new ]
# задаём ему разные опции
$reporter Set header "просто тестовый отчёт"
$reporter Set text "не забудьте выключить утюг!"
$reporter Set screenshot "screenshot.png"

# добавляем сделки
$reporter AddOpen [ clock format [ clock seconds ] -format "%T" ] "Buy" 1.00 1 "" "" 
$reporter AddOpen [ clock format [ clock seconds ] -format "%T" ] "Buy" 2.00 1 "" "" 

# добавляем сделки
$reporter AddClose [ clock format [ clock seconds ] -format "%T" ] "BuyLimit" 4.00 1 "" "" 
$reporter AddClose [ clock format [ clock seconds ] -format "%T" ] "SellLimit" 5.00 1 "" "" 

 
# публикуем
$reporter Publish

# удаляем объект
$reporter destroy
# (опционально) и переменную тоже
unset reporter

запускаем wordreport_test.tcl в командой строке  tclsh wordreport_test.tcl , видим распечатку массива. Значит методы Set AddOpen AddClose работают и можно браться за Publish

приручаем Word

Очень внимательно прочитав документацию Cawt делаем метод Publish в файле wordreport.tcl :

# детализуем метод Report
oo::define WordReport method Publish {} {
        # будем перехватывать ошибки.. можно было try {..} catch {..} finally , но я по старинке - просто catch
        if { [ catch {
                # открывaем Word
                set word [ Word::OpenNew true ]
                # создаём новый пустой документ и получаем его id
                set doc [ Word::AddDocument $word ]
                # пишем заголовок и после него начинаем параграф
                # -2 - непосредственное значение из WdBuiltinStyle WdStyleHeading1
                set text [ Word::AppendText $doc $Options(header) 1 -2 ]
                # по центру
                # wdAlignParagraphCenter 1
                Word::SetRangeHorizontalAlignment $text 1
                # добавляем изображение
                if { $Options(screenshot)!="" && [ file readable [ file join mql5 files $Options(screenshot) ] ] } {
                        # после нелишних проверок
                        # имя файла для передачи в ворд - абсолютный с правильными разделителями
                        set fileName [ file nativename [ file join [ pwd ] mql5 files $Options(screenshot) ] ]
                        puts $fileName
                        # берём указатель на конец текста
                        set insertPoint [ Word::GetEndRange $doc ] 
                        set imageId [ Word::InsertImage $insertPoint $fileName ]
                }
                # добавляем текст
                # -1 wdStyleNormal
                Word::AppendText $doc $Options(text) 1 -1
                # добавляем разрыв (аналог <br .>) 1.5 = полтора пункта; можно 1.5i - дюймы 1.5c - сантиметры
                Word::AppendParagraph $doc 1.5
                # добавляем таблицу с открытыми сделками
                if { [ llength $Options(openOrders) ] > 0 } {
                        # заголовок
                        # wdHeading2
                        Word::AppendText $doc "Открытые ордера" 1 -3
                        set insertPoint [ Word::GetEndRange $doc ] 
                        set rows [ llength $Options(openOrders) ]
                        set cols [ llength [ lindex $Options(openOrders) 0 ] ]
                        set table [ Word::AddTable $insertPoint $rows $cols 1.5 ]
                        # задаём рамки
                        # 1 wdLineStyleSingle
                        Word::SetTableBorderLineStyle $table 1
                        # внешние толстые (2) внутри тоньше
                        # 8 2 см WdLineWidth
                        Word::SetTableBorderLineWidth  $table 8 2
                        # пересылаем значения
                        Word::SetMatrixValues $table $Options(openOrders)
 
                }
                # аналогично добавляем таблицу с закрытыми сделками
                if { [ llength $Options(closeOrders) ] > 0 } {
                        # заголовок
                        # wdHeading2
                        Word::AppendText $doc "Закрытые за 24 часа" 1 -3
                        set insertPoint [ Word::GetEndRange $doc ] 
                        set rows [ llength $Options(closeOrders) ]
                        set cols [ llength [ lindex $Options(closeOrders) 0 ] ]
                        set table [ Word::AddTable $insertPoint $rows $cols 1.5 ]
                        Word::SetTableBorderLineStyle $table 1
                        Word::SetTableBorderLineWidth  $table 8 2
                        Word::SetMatrixValues $table $Options(closeOrders)
                }
        } err ] } {
                if { ! [ info exists ::MetaTrader ] } {
                        puts stderr "Cawt Error: $err"
                }
        }
        # завершили работу с Word, удаляем все COM объекты и связи
        Cawt::Destroy
}
# после переопределения метода, надо снова экспортировать
oo::define WordReport export Publish

Многократно запуская (и не забывая время от времени закрывать окошки Word) добились некоторого результата:

элементы из эскиза присутсвуют, но надо будет верстать. Пока чтобы времени не терять добавим себе ToDo:

  1. задать альбомную ориентацию листа
  2. задать аттрибут обтекания скриншота или сверстать его в таблицу с текстом
  3. добавить заголовки столбцам таблиц
  4. поменять заголовки таблиц на указанные в эскизе

Сочтём эти замечания не препятствующими к переходу непосредственно к MQL

Фигачим MQL

Раз уж начали ОО , значит делаем класс который почти повторяет методы WordReport из tcl и собственно их вызывает. И пару-тройку функций для упрощения - прочесть открытые ордера, историю и сделать скриншот.

MQL гораздо более низкого уровня, поэтому объём кода получается побольше, хотя он и не делает ничего существенного :

// берём разработанный tcl как ресурс
#resource "wordreport.tcl" as string WordReport_tcl;
// подключаем библиотеку
#include <ATcl/ATcl.mqh>
// спасибо fxsaber за человеческую работу с ордерами
#include <MT4Orders.mqh>
// класс 
class WordReport {
public:
   WordReport();
   ~WordReport();
   int OnInit();  // проверить результат конструктора
   // задать опции отчёта
   bool Set(string option,string value);
   bool Set(string option,double value);
   bool Set(string option,long value);
   // добавить инфо про открытый ордер
   bool AddOpen(long ticket,datetime time,int type,double lots,double price,double stopLoss,double takeProfit,string comment);
   bool AddOpenSelected(); // добавить ордер выбранный по SELECT
   bool AddAllOpen(); // добавить все открытые ордера
   // добавить инфо про закрытый ордер
   bool AddClose(long ticket,datetime time,int type,double lots,double price,datetime closeTime,double closePrice,long points,double profit,string comment);
   bool AddCloseSelected(); // добавить ордер выбранный по SELECT
   bool AddAllClose(datetime fromTime); // добавить все закрытые ордера от заданного времени
   // снять скриншот
   bool Screenshot(string filename);
   // опубликовать
   bool Publish();
public:
   ATcl *interp;     // класс будет владеть собственным интерпретатором (их мржно много)
   Tcl_Obj instance; // инстанс класса WordReport в интерпретаторе  
   int digits;       // точность цен
};
WordReport::WordReport(void)
{
   // делаем себе интерпретатор
   interp = new ATcl();
   if (interp==NULL) return;
   // там исполняем скрипт с интерфейсом класса
   if (interp.Eval(WordReport_tcl)!=TCL_OK) {
      delete interp;
      interp=NULL;
      return;
   }
   // в нём объект класса WordReport
   instance=interp.ObjEval("set reporter [ WordReport new ]");
   if (instance==0) return;
   interp.Ref(instance);
   digits=_Digits;
}
WordReport::~WordReport()
{
   if (interp!=NULL) {
      if (instance!=0) {
         interp.Eval("$reporter destroy");
         interp.Unref(instance);
      }
      delete interp;
   }
}
int WordReport::OnInit(void)
{
   if (interp==NULL) return INIT_FAILED;
   if (instance==NULL) return INIT_FAILED;
   return INIT_SUCCEEDED;
}
bool WordReport::Set(string option,string value)
{
   if (interp.Call(instance,interp.Obj("Set"),interp.Obj(option),interp.Obj(value))!=TCL_OK) {
      return false;
   }
   return true;
}
bool WordReport::Set(string option,double value)
{
   if (interp.Call(instance,interp.Obj("Set"),interp.Obj(option),interp.Obj(value))!=TCL_OK) {
      return false;
   }
   return true;
}
bool WordReport::Set(string option,long value)
{
   if (interp.Call(instance,interp.Obj("Set"),interp.Obj(option),interp.Obj(value))!=TCL_OK) {
      return false;
   }
   return true;
}
bool WordReport::AddOpen(long ticket,datetime time,int type,double lots,double price,double stopLoss,double takeProfit,string comment)
{
   // для таблиц - double и datetime всё сформируем строками, чтобы не наступать в кривые double
   string strTime=TimeToString(time,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   string strLots=DoubleToString(lots,2);
   string strPrice=DoubleToString(price,digits);
   string strSL=""; if (stopLoss!=0) strSL=DoubleToString(stopLoss,digits);
   string strTP=""; if (takeProfit!=0) strSL=DoubleToString(takeProfit,digits);
   string strType=OrderTypeToString((ENUM_ORDER_TYPE)type);
   // вызываем AddOpen и передяём поля как аргументы interp.Obj(xx), 
   if (interp.Call(instance,interp.Obj("AddOpen"),
         interp.Obj(ticket),
         interp.Obj(strTime),
         interp.Obj(strType),
         interp.Obj(strLots),
         interp.Obj(strPrice),
         interp.Obj(strSL),
         interp.Obj(strTP),
         interp.Obj(comment)
      )!=TCL_OK) {
      return false;
   }
   return true;
}
bool WordReport::AddClose(long ticket,datetime time,int type,double lots,double price,datetime closeTime,double closePrice,long points,double profit,string comment)
{
   // для таблиц - double и datetime всё сформируем строками, чтобы не наступать в кривые double
   string strTime=TimeToString(time,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   string strLots=DoubleToString(lots,2);
   string strPrice=DoubleToString(price,digits);
   string strType=OrderTypeToString((ENUM_ORDER_TYPE)type);
   string strCloseTime=TimeToString(time,TIME_DATE|TIME_MINUTES|TIME_SECONDS);
   string strClosePrice=DoubleToString(closePrice,digits);
   string strProfit=DoubleToString(profit,2);
   // вызываем AddOpen и передяём поля как аргументы interp.Obj(xx), 
   if (interp.Call(instance,interp.Obj("AddOpen"),
         interp.Obj(ticket),
         interp.Obj(strTime),
         interp.Obj(strType),
         interp.Obj(strLots),
         interp.Obj(strPrice),
         interp.Obj(strCloseTime),
         interp.Obj(strClosePrice),
         interp.Obj(points),
         interp.Obj(strProfit),
         interp.Obj(comment)
      )!=TCL_OK) {
      return false;
   }
   return true;
}
bool WordReport::AddOpenSelected()
{
   return AddOpen(OrderTicket(),OrderOpenTime(),OrderType(),OrderLots(),OrderOpenPrice(),OrderStopLoss(),OrderTakeProfit(),OrderComment());
}
bool WordReport::AddAllOpen() 
{
   for(int pos=OrdersTotal()-1;pos>=0;pos--) {
      if (!OrderSelect(pos,SELECT_BY_POS,MODE_TRADES)) {
         Alert("OrderSelect failed, please repeate");
         ExpertRemove();
         return false;
      }
      if (OrderCloseTime()!=0) continue;
      if (OrderSymbol()!=_Symbol) continue;
      int type=OrderType();
      if (type!=OP_BUY && type!=OP_BUYLIMIT && type!=OP_BUYSTOP &&
         type!=OP_SELL && type!=OP_SELLLIMIT && type!=OP_SELLSTOP) {
         // балансовая операция или нечто новое
         continue;
      }
      AddOpenSelected();
   }
   return true;
}
bool WordReport::AddCloseSelected()
{
   long points=0;
   double profit=0.0;
   if (OrderType()==OP_BUY) {
      points=(int)MathRound((OrderClosePrice()-OrderOpenPrice())/_Point);
   } else if (OrderType()==OP_SELL) {
      points=(int)MathRound((OrderOpenPrice()-OrderClosePrice())/_Point);
   }
   profit=OrderProfit()+OrderSwap()+OrderCommission();
   return AddClose(OrderTicket(),OrderOpenTime(),OrderType(),OrderLots(),OrderOpenPrice(),OrderCloseTime(),OrderClosePrice(),points,profit,OrderComment());
}
bool WordReport::AddAllClose(datetime from) 
{
   for(int pos=OrdersHistoryTotal()-1;pos>=0;pos--) {
      if (!OrderSelect(pos,SELECT_BY_POS,MODE_HISTORY)) {
         Alert("history OrderSelect failed, please repeate");
         ExpertRemove();
         return false;
      }
      if (OrderSymbol()!=_Symbol) continue;
      if (OrderCloseTime()<from) continue;
      int type=OrderType();
      if (type!=OP_BUY && type!=OP_BUYLIMIT && type!=OP_BUYSTOP &&
         type!=OP_SELL && type!=OP_SELLLIMIT && type!=OP_SELLSTOP) {
         // балансовая операция или нечто новое
         continue;
      }
      AddOpenSelected();
   }
   return true;
}
bool WordReport::Screenshot(string filename)
{
   long chart=ChartID();
   int width=(int)ChartGetInteger(chart,CHART_WIDTH_IN_PIXELS);
   int height=(int)ChartGetInteger(chart,CHART_HEIGHT_IN_PIXELS);
   return ChartScreenShot(ChartID(),filename,width,height);
}
bool WordReport::Publish(void)
{
   if (interp.Call(instance,interp.Obj("Publish"))!=TCL_OK) {
      return false;
   }
   return true;
}
string OrderTypeToString(ENUM_ORDER_TYPE type)
{
   switch(type) {
      case OP_BUY: return "BUY";
      case OP_BUYLIMIT: return "BUYLIMIT";
      case OP_BUYSTOP: return "BUYSTOP";
      case OP_SELL: return "BUY";
      case OP_SELLLIMIT: return "SELLLIMIT";
      case OP_SELLSTOP: return "SELLSTOP";
   }
   return StringFormat("TYPE_%d",(int)type);
}
void OnStart()
{
   // инициализауем библиотеку
   if (ATcl_OnInit()!=INIT_SUCCEEDED) {
      Print("Ошибка при инициализации библиотеки");
      return;
   }
   WordReport *reporter=new WordReport();
   if (reporter==NULL) {
      Print("Такого небывает, исключений нет");
      return;
   }
   if (reporter.OnInit()!=INIT_SUCCEEDED) {
      Print("Ошибка при создании класса");
      delete reporter;
      return;
   }
   // задаём заголовок
   reporter.Set("header",StringFormat("Отчётец об %s от %s",_Symbol,TimeToString(TimeCurrent(),TIME_DATE)));
   // зададим текстовочку
   reporter.Set("text","Опишите что произошло за день, шёл ли снег, дождь ли.\nМожет чего наторогвалось полезного, или идеи какие");
   // определяем время 
   datetime from=TimeCurrent();
   MqlDateTime dt; TimeToStruct(TimeCurrent(),dt);
   if (dt.day_of_week==1 || dt.day_of_week==2) {
      from-=3*24*60*60;
   } else {
      from-=1*24*60*60;
   }
   // добавляем исторические ордера
   reporter.AddAllClose(from);
   // добавляем открытые ордера
   reporter.AddAllOpen();
   // добавляем скриншот
   if (!reporter.Screenshot("report.jpg")) {
      Alert("Ошибка при снятии скриншота");
      delete reporter;
      return;
   }
   reporter.Set("screenshot","report.jpg");
   // публикуем
   if (reporter.Publish()!=true) {
      Alert("Ошибка при публикации");
      delete reporter;
      return;
   }
   // ВСЁ
   delete reporter;
}

Собственно всё работает:

Осталось только дополнить ToDo:

  1. надо аккуратнее считать даты от которых берётся история (учитывать выходные и торговые сессии)
  2. так как лист белый, то стоит менять фон графика до скриншота
  3. скриншот надо масштабировать. или опциями ворда или до того

но в общем, на сегодня хватит   Указанные пометки вы и сами для себе можете подправить

Вместо заключения: Скрипты и отдельные от MetaTrader тестирование и отладка очень серьёзно сэкономили нам время. На всё про всё ушло 4 часа.К тому-же теперь можем отдельно заниматься оформлением отчёта, не затрагивая код программы mql. Софт который при деньгах, должен быть постоянно в стабильном состоянии.

Итоговые wordreport.tcl wordreport_test.tcl и wordreport.mq5 прикладываю

Установить библиотеку ATcl можно через инсталлятор или отсюда https://www.filefactory.com/file/6zj49q87vhkk/atclsetup.exe или следуя инструкциям в репозитарии проекта

или со страницы download эскизного сайта : http://atcl.unaux.com/download/


Файлы:
Поделитесь с друзьями: