Набросок модельки

17 ноября 2025, 11:45
Maxim Kuznetsov
0
55

Вслед за релизом ATcl и эскизом панельки, делаем набросок модельки

В реальной жизни пишется всё гораздо-гораздо проще и быстрее чем в местных оф.публикациях :-) Потому-что сам себе за lines-of-code и объём не заплатишь.

Чтобы наш GUI работал, он должен уметь получать данные бизнес-логики. А бизнес-логика должна уметь отдавать данные. Данные между ними это моделька. То есть данные надо просто представить в виде удобным и для работы как бизнес-логики (то есть программы на MQL) так и GUI на tcl.

При этом программа MQL не должна даже задумываться о том сколько, как, что и где рисуется на экране (или отдаётся браузером через http или транслируется). Не её это собачье дело. В модели никаких опций связанных с отображением, сортировкой или выборками нет.

В терминале наша бизнес-логика и все сведения о мире сосредоточены в базе инструментов (то что получаем через SymbolInfoXXX, PositionInfo и так далее), эти базы мы можем читать в основном только по опросу. Единственные оповещения об изменениях - OnTick,OnBookEvent,OnTrade и то "бабушка надвое" :-) То есть на уровне MQL придётся заводить свои массивы объектов и постоянно их синхронизовать с базами терминала.

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

Фигачим код, куда деваться :-)

#include <ATcl/ATcl.mqh>
#include <nektomk/Algo.mqh>

#resource "MajorTrade.tcl" as string MajorTrade_tcl;

ATcl *tcl=NULL;

class SymbolInfo {
public:
   SymbolInfo(string name);
   ~SymbolInfo();
   void FullUpdate();    // полностью обновить данные по инструменту
   bool CheckTick();
public:
   int id;        // индекс в таблице символов
   string name;
   string base;
   string quote;
   int digits;
   double point;
   double tickSize;
   double tickValue;
   double lotSize,minLot,maxLot,lotStep;
   
   datetime time;
   double bid,ask;
   MqlTick tick;
};

SymbolInfo::SymbolInfo(string _name)
{
   name=_name;
   id=-1;
   FullUpdate();   
}
SymbolInfo::~SymbolInfo() 
{
}
void SymbolInfo::FullUpdate() {
   base=SymbolInfoString(name,SYMBOL_CURRENCY_BASE);
   quote=SymbolInfoString(name,SYMBOL_CURRENCY_PROFIT);
   digits=(int)SymbolInfoInteger(name,SYMBOL_DIGITS);
   point=SymbolInfoDouble(name,SYMBOL_POINT);
   tickSize=SymbolInfoDouble(name,SYMBOL_TRADE_TICK_SIZE);
   tickValue=SymbolInfoDouble(name,SYMBOL_TRADE_TICK_VALUE);
   lotSize=SymbolInfoDouble(name,SYMBOL_TRADE_CONTRACT_SIZE);
   minLot=SymbolInfoDouble(name,SYMBOL_VOLUME_MIN);
   maxLot=SymbolInfoDouble(name,SYMBOL_VOLUME_MAX);
   lotStep=SymbolInfoDouble(name,SYMBOL_VOLUME_STEP);
   time=0;
   bid=0.0;
   ask=0.0;
   ZeroMemory(tick);
   PrintFormat("symbol %s, base %s quote %s",name,base,quote);
}
bool SymbolInfo::CheckTick(void) 
{
   MqlTick tmpTick;
   datetime tmpTime;
   if (SymbolInfoTick(name,tmpTick)) {
      if (tmpTick.ask!=tick.ask || tmpTick.bid!=tick.bid || tmpTick.time!=tick.time) {
         tick=tmpTick;
         bid=tick.bid;
         ask=tick.ask;
         time=tick.time;
         return true;
      }
   } else if ((tmpTime=(datetime)SymbolInfoInteger(name,SYMBOL_TIME))!=time) {
      double tmpBid=SymbolInfoDouble(name,SYMBOL_BID);
      double tmpAsk=SymbolInfoDouble(name,SYMBOL_ASK);
      if (tmpBid!=bid || tmpAsk!=ask) {
         ZeroMemory(tick);
         time=tick.time=tmpTime;
         bid=tick.bid=tmpBid;
         ask=tick.ask=tmpAsk;
         return true;
      }
   }
   return false;
}
class AssetInfo {
public:
   AssetInfo(string name,SymbolInfo *sym=NULL);
   ~AssetInfo();
public:
   int id;        // индекс в таблице активов
   string name;   // имя актива
   SymbolInfo *symbol; // инструмент к USD
};
AssetInfo::AssetInfo(string _name,SymbolInfo *_sym)
{
   id=-1;
   name=_name;
   symbol=_sym;
}
AssetInfo::~AssetInfo()
{
}

class ModelProvider {
public:
   ModelProvider();
   ~ModelProvider();
   int OnInit();
   void OnDeinit(const int reason);
   void OnTimer();
   void CheckTicks();
public:
   SymbolInfo *SYMBOLS[];
   AssetInfo *ASSETS[];
   void InitSymbols();
   SymbolInfo *AddSymbol(string symbol);
   AssetInfo  *AddAsset(string asset,SymbolInfo *sym);

   void SyncSymbol(SymbolInfo *sym);
   void SyncTick(SymbolInfo *sym);

   datetime time;
   ulong nextCheckTicks;
};
ModelProvider::ModelProvider(void) {
   time=0;
}
ModelProvider::~ModelProvider() {
   for(int pos=ArraySize(ASSETS)-1;pos>=0;pos--) {
      if (ASSETS[pos]!=NULL) {
         delete ASSETS[pos];
         ASSETS[pos]=NULL;
      }
   }
   ArrayResize(ASSETS,0);
   for(int pos=ArraySize(SYMBOLS)-1;pos>=0;pos--) {
      if (SYMBOLS[pos]!=NULL) {
         delete SYMBOLS[pos];
         SYMBOLS[pos]=NULL;
      }
   }
   ArrayResize(SYMBOLS,0);
};
int ModelProvider::OnInit(void) {
   InitSymbols();
   return INIT_SUCCEEDED;
}
void ModelProvider::OnDeinit(const int reason) {
}
void ModelProvider::OnTimer() {
   CheckTicks();
}
void ModelProvider::CheckTicks() {
   datetime tmpTime=TimeCurrent();
   if (tmpTime!=time) {
      for(int pos=ArraySize(SYMBOLS)-1;pos>=0;pos--) {
         SymbolInfo *sym=SYMBOLS[pos];
         if (sym==NULL) continue;
         if (sym.CheckTick()) {
            //PrintFormat("%s tick bid=%f ask=%f",sym.name,sym.bid,sym.ask);
            SyncTick(sym);
         }
      }
   }
}
// строим таблицы символов и активов
void ModelProvider::InitSymbols(void) {
   if (ArraySize(ASSETS)==0) {
      AddAsset("USD",NULL);
   }
   int total=SymbolsTotal(false);
   for(int pos=0;pos<total;pos++) {
      string symbol=SymbolName(pos,false);
      if (symbol==NULL || symbol=="") continue;
      string base=SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);
      string quote=SymbolInfoString(symbol,SYMBOL_CURRENCY_PROFIT);
      if (base==NULL || base=="" || base==symbol) continue;
      if (quote==NULL || quote=="" || quote==symbol || quote==base) continue;
      if (StringFind(symbol,base)<0) continue;
      if (StringFind(symbol,quote)<0) continue;
      SymbolInfo *sym=AddSymbol(symbol);
      if (sym==NULL) continue;
      if (base=="USD") {
         AddAsset(quote,sym);
      } else if (quote=="USD") {
         AddAsset(base,sym);
      }
      SyncSymbol(sym);
   }
}
void ModelProvider::SyncSymbol(SymbolInfo *sym) {
   if (tcl.Call(tcl.Obj("UpdateSymbol"),tcl.Obj(sym.name),
      tcl.Obj("name base quote digits point tickSize tickValue lotSize minLot maxLot lotStep"),
      tcl.Obj(sym.name),
      tcl.Obj(sym.base),
      tcl.Obj(sym.quote),
      tcl.Obj(sym.digits),
      tcl.Obj(sym.point),
      tcl.Obj(sym.tickSize),
      tcl.Obj(sym.tickValue),
      tcl.Obj(sym.lotSize),
      tcl.Obj(sym.minLot),
      tcl.Obj(sym.maxLot),
      tcl.Obj(sym.lotStep))!=TCL_OK) {
      
      PrintFormat("UpdateSymbol error %s",tcl.StringResult());
   }
   SyncTick(sym);  
      
}
void ModelProvider::SyncTick(SymbolInfo *sym) {
   if (tcl.Call(tcl.Obj("UpdateSymbol"),tcl.Obj(sym.name),
      tcl.Obj("time bid ask tickValue tickSize"),
      tcl.Obj(sym.name),
      tcl.Obj(sym.time),
      tcl.Obj(sym.bid),
      tcl.Obj(sym.ask),
      tcl.Obj(sym.tickValue),
      tcl.Obj(sym.tickSize))!=TCL_OK) {

      PrintFormat("SyncTick error %s",tcl.StringResult());
   }
}
SymbolInfo *ModelProvider::AddSymbol(string symbol) {
   SymbolInfo *sym=new SymbolInfo(symbol);
   if (sym==NULL) return NULL;
   int id=ArrayPush(SYMBOLS,sym);
   sym.id=id;
   return sym;
}

AssetInfo *ModelProvider::AddAsset(string name,SymbolInfo *sym) {
   AssetInfo *asset=new AssetInfo(name,sym);
   if (asset==NULL) return NULL;
   int id=ArrayPush(ASSETS,asset);
   asset.id=id;
   PrintFormat("Asset %s%s",asset.name,asset.symbol==NULL?"":" via "+asset.symbol.name);
   return asset;
}
ModelProvider *model=NULL;

void OnStart()
{
   ATcl_OnInit();
   tcl=new ATcl();
   tcl.OnInit();
   tcl.Eval(MajorTrade_tcl);
   
   model=new ModelProvider();
   model.OnInit();
   
   tcl.Eval("package require tkcon");
   tcl.Eval("tkcon show");
   while(!IsStopped()) {
      model.OnTimer();
      tcl.Update();
      Sleep(20);
   }   
   
   if (model!=NULL) {
      model.OnDeinit(REASON_PROGRAM);
      delete model;
      model=NULL;
   }
   if (tcl!=NULL) {
      //tcl.OnDeInit();
      delete tcl;
      tcl=NULL;
   }
   ATcl_OnDeinit(REASON_PROGRAM);
}

и маленький довесок tcl (процедуры обновляющие ассоциативные массивы)

array set SYMBOLS {}
array set ASSETS {}

proc Update { table key fields args } {
        upvar #0 $table data
        if { [ info exists data($key) ] } {
                set record $data($key)
        } else {
                set record {}
        }
        foreach field $fields value $args {
                dict set record $field $value
        }
        set data($key) $record
}
proc UpdateSymbol { symbol fields args } {
        tailcall Update SYMBOLS $symbol $fields {*}$args
}
proc UpdateAsset { asset fields args } {
        tailcall Update ASSETS $symbol $fields {*}$args
}

 Моделька начала работать. В целях отладки поставлен вызов tkcon - можно смотреть что там пишется/обновляется в массивах SYMBOLS ASSETS:

 

время выдастся, вечерком свяжу полученную эскизную панель с этой моделью и 80% сделано ;-) останется только исполнение