mql5/Indicators/CodeLock/CodeLock-Dashboard.mq5
2026-11-05 12:52:36 +03:00
1027 lines
35 KiB
MQL5
// ===============CodeLock-Dashboard===========================
//============================================================================
// НОРМАТИВНЫЙ ДОКУМЕНТ
// Полный стандарт системы: CodeLock-README.md (корень репозитория).
// Термины, FSM, события, HISTORY/LIVE, GUI, ALERT и требования проверки
// должны соответствовать CodeLock-README.md. При изменении логики сначала
// обновляется/проверяется стандарт, затем изменяется код и выполняется
// проверка потребителей и регрессий.
//
// АРХИТЕКТУРНАЯ РОЛЬ
// CodeLock-Dashboard отвечает за визуальное отображение состояний LEVEL/
// DIRECTION и управление ALERT. Торговую логику LOCK/SIGNAL он не исполняет.
// CODELOCK DASHBOARD — ЕДИНЫЙ СТАНДАРТ МОДЕЛИ НАПРАВЛЕНИЯ
//
// НАЗНАЧЕНИЕ
// Индикатор восстанавливает и отображает состояние BID относительно ценовой
// сетки. Для каждого LEVEL используется независимое состояние DIRECTION.
//
// ЕДИНЫЕ ТЕРМИНЫ
// POINT = _Point.
// LEVEL = расстояние сетки в points.
// GRID RAY = цена StartPrice + GRID * stepPrice.
// DIRECTION = 0 WAIT, +1 UP, -1 DOWN.
// CONTROL RAY = directionRay — последний луч, подтвердивший DIRECTION.
// LAST CROSS = lastCrossedRay / lastCrossingDirection.
// REVERSAL = противоположное пересечение только соседнего CONTROL RAY:
// UP на N -> DOWN только на N-1;
// DOWN на N -> UP только на N+1.
// RETOUCH = обратное пересечение любого другого луча; DIRECTION не меняется.
//
// ЕДИНЫЙ АЛГОРИТМ
// TICK -> ProcessBidTick() -> ProcessDistanceTick()
// -> RegisterCrossing() -> изменение DIRECTION -> ALERT/GUI.
//
// Первое пересечение LEVEL определяет DIRECTION.
// Пересечение в том же направлении подтверждает DIRECTION.
// Противоположное пересечение разрешает REVERSAL только через соседний
// CONTROL RAY. Поэтому случайное повторное касание уровня не является
// подтверждением REVERSAL.
//
// HISTORY / LIVE
// HISTORY восстанавливает состояние до момента AttachMsc.
// LIVE обрабатывает только новые тики после восстановленного курсора.
// Оба режима используют один маршрут ProcessBidTick() -> ProcessDistanceTick()
// -> RegisterCrossing(). Одно событие не должно обрабатываться повторно.
//
// ИСТОЧНИК ИСТИНЫ
// States[] содержит расчётное состояние. GUI только отображает его и управляет
// ALERT. Изменение внешнего вида не считается изменением модели.
//
// ALERT
// ALERT реагирует на подтверждённое пересечение, отключается при срабатывании
// и не изменяет правила определения DIRECTION.
//
// GUI
// Жизненный цикл объектов: CREATE -> CONFIGURE -> UPDATE -> RESIZE
// -> EVENT -> DELETE. GUI не является источником торговой логики.
// ============================================================================
#property strict
#property indicator_chart_window
#property indicator_plots 0
#property version "1.00"
#property description "Standalone 30-level UP/DOWN price dashboard"
#define DASHBOARD_ROWS 30
#define DEFAULT_VISIBLE_ROWS 5
#define PANEL_PREFIX "TPDRD_"
#define ROWS_STATE_PREFIX "TPDR_ROWS_"
input double StartPrice=1.0; // Начало сетки ценовых уровней
input string DashboardLevels="20,40,60,80,100,120,140,160,180,200,220,240,260,280,300,320,340,360,380,400,420,440,460,480,500,520,540,560,580,600"; // Уровни dashboard в points, каждый уровень задаётся отдельно
input int HistoryBootstrapTicks=100000; // Расширенная история для восстановления состояний
input int HistoryChunkSize=5000; // Размер блока тиков LiveMode
input int UpdateIntervalMs=50; // Быстрое обновление dashboard
input int PanelLeftMargin=12; // Отступ слева
input int PanelTopMargin=12; // Отступ сверху
input int RowHeight=25; // Высота строки
input int HeaderHeight=72; // Высота заголовка
input int DashboardFontSize=11; // Единый размер шрифта всей таблицы, включая заголовки, цифры и кнопки
input color AlertOnColor=clrOrange; // Цвет включенного ALERT, ожидающего следующего пересечения
input color AlertOffColor=C'55,62,74'; // Цвет выключенного ALERT
input color AlertActivityColor=clrLimeGreen; // Цвет краткой индикации фактического срабатывания ALERT
input int AlertActivityMs=500; // Длительность индикации фактического срабатывания ALERT в миллисекундах
input color AlertTextColor=clrWhite; // Цвет текста кнопки ALERT
input color PanelBackground=C'16,20,28'; // Фон панели
input color RowBackground=C'29,35,46'; // Фон строки
input color RowBackgroundAlt=C'24,30,40'; // Чередующийся фон
input color BorderColor=C'75,88,108'; // Граница
input color HeaderColor=clrDeepSkyBlue; // Акцент заголовка
input color TextColor=clrWhiteSmoke; // Основной текст
input color MutedTextColor=C'155,165,180'; // Вторичный текст
input color UpColor=clrLimeGreen; // UP
input color DownColor=clrTomato; // DOWN
input color WaitColor=clrSilver; // WAIT
struct DistanceState
{
int stepPoints;
double stepPrice;
int direction;
int lastCrossedRay;
int lastCrossingDirection;
int consecutiveCrossings;
int directionRay;
double lastDirectionChangePrice;
};
DistanceState States[DASHBOARD_ROWS];
bool AlertEnabled[DASHBOARD_ROWS];
ulong AlertActivityUntil[DASHBOARD_ROWS];
string Prefix=PANEL_PREFIX;
string ShortName="Tick Price Dashboard 30";
long AttachMsc=0;
long CursorMsc=1;
int CursorSameMsc=0;
bool BootstrapComplete=false;
bool HavePreviousBid=false;
double PreviousBid=0.0;
int LastTickCount=0;
int VisibleRows=DEFAULT_VISIBLE_ROWS;
string RowsStateKey="";
int ColLevelX=16;
int ColStatusX=0;
int ColTriggerX=0;
int ColAlertX=0;
int ColDistanceX=0;
int ColLastX=0;
int AutoPanelWidth=0;
int MeasureTextWidth(const string text)
{
uint width=0;
uint height=0;
TextSetFont("Segoe UI",-DashboardFontSize*10,FW_NORMAL);
if(!TextGetSize(text,width,height))
return MathMax(1,StringLen(text)*DashboardFontSize);
return (int)width;
}
void CalculateAutoLayout(const double bid)
{
int levelWidth=MeasureTextWidth("LEVEL");
int statusWidth=MeasureTextWidth("STATUS");
int triggerWidth=MeasureTextWidth("REVERSE AT");
int alertWidth=MeasureTextWidth("ALERT")+16;
int distanceWidth=MeasureTextWidth("DIST");
int lastWidth=MeasureTextWidth("LAST");
int levelCount=MathMin(GetDashboardLevelCount(),VisibleRows);
for(int i=0;i<levelCount;i++)
{
levelWidth=MathMax(levelWidth,MeasureTextWidth(IntegerToString(States[i].stepPoints)+" pt"));
statusWidth=MathMax(statusWidth,MeasureTextWidth(StatusText(States[i].direction)));
bool valid=false;
bool triggerUp=false;
int triggerGrid=GetTriggerGrid(States[i],valid,triggerUp);
string triggerText="—";
string distanceText="—";
if(valid)
{
double triggerPrice=RayPriceForStep(triggerGrid,States[i].stepPrice);
triggerText=(triggerUp ? "↑ " : "↓ ")+DoubleToString(triggerPrice,_Digits);
distanceText=DoubleToString(MathAbs(triggerPrice-bid)/_Point,0)+" pt";
}
triggerWidth=MathMax(triggerWidth,MeasureTextWidth(triggerText));
distanceWidth=MathMax(distanceWidth,MeasureTextWidth(distanceText));
string lastText=States[i].lastDirectionChangePrice>0.0 ?
DoubleToString(States[i].lastDirectionChangePrice,_Digits) : "—";
lastWidth=MathMax(lastWidth,MeasureTextWidth(lastText));
}
int gap=6;
int pad=8;
ColLevelX=16;
ColStatusX=ColLevelX+levelWidth+gap;
ColTriggerX=ColStatusX+statusWidth+gap;
ColAlertX=ColTriggerX+triggerWidth+gap;
ColDistanceX=ColAlertX+alertWidth+gap;
ColLastX=ColDistanceX+distanceWidth+gap;
AutoPanelWidth=ColLastX+lastWidth+16;
}
int GetDashboardLevelCount()
{
string parts[];
int count=StringSplit(DashboardLevels,',',parts);
return MathMin(MathMax(count,0),DASHBOARD_ROWS);
}
int GetDashboardLevelPoints(const int index)
{
if(index<0 || index>=DASHBOARD_ROWS)
return 0;
string parts[];
int count=StringSplit(DashboardLevels,',',parts);
if(index>=count)
return 0;
return (int)StringToInteger(parts[index]);
}
int FloorGridForStep(const double price,const double stepPrice)
{
if(stepPrice<=0.0)
return 0;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/stepPrice;
return (int)MathFloor(position+1.0e-9);
}
int CeilGridForStep(const double price,const double stepPrice)
{
if(stepPrice<=0.0)
return 0;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/stepPrice;
return (int)MathCeil(position-1.0e-9);
}
double RayPriceForStep(const int grid,const double stepPrice)
{
return NormalizeDouble(StartPrice+(double)grid*stepPrice,_Digits);
}
void RegisterCrossing(DistanceState &state,const int grid,const int direction)
{
state.lastCrossedRay=grid;
state.lastCrossingDirection=direction;
// Один принцип разворота с гистерезисом:
// UP после пересечения уровня N разворачивается только на N-1.
// DOWN после пересечения уровня N разворачивается только на N+1.
// Повторное касание/пересечение того же уровня в обратную сторону
// само по себе направление не меняет.
if(state.direction==0)
{
state.direction=direction;
state.directionRay=grid;
state.consecutiveCrossings=1;
state.lastDirectionChangePrice=RayPriceForStep(grid,state.stepPrice);
return;
}
if(direction==state.direction)
{
state.directionRay=grid;
state.consecutiveCrossings++;
return;
}
int reversalGrid=state.directionRay-state.direction;
if(grid!=reversalGrid)
return;
state.direction=direction;
state.directionRay=grid;
state.consecutiveCrossings=1;
state.lastDirectionChangePrice=RayPriceForStep(grid,state.stepPrice);
}
void ProcessDistanceTick(DistanceState &state,const int stateIndex,
const double oldBid,const double bid,const bool emitAlert)
{
if(state.stepPrice<=0.0 || oldBid<=0.0 || bid<=0.0 || bid==oldBid)
return;
if(bid>oldBid)
{
int first=FloorGridForStep(oldBid,state.stepPrice)+1;
int last=FloorGridForStep(bid,state.stepPrice);
for(int grid=first;grid<=last;grid++)
{
bool isAlertCrossing=false;
if(emitAlert && AlertEnabled[stateIndex] && state.direction<0 && state.directionRay!=0)
{
int alertGrid=state.directionRay+1;
isAlertCrossing=(grid==alertGrid);
}
RegisterCrossing(state,grid,1);
if(isAlertCrossing)
FireLevelAlert(stateIndex,grid,1);
}
}
else
{
int first=CeilGridForStep(oldBid,state.stepPrice)-1;
int last=CeilGridForStep(bid,state.stepPrice);
for(int grid=first;grid>=last;grid--)
{
bool isAlertCrossing=false;
if(emitAlert && AlertEnabled[stateIndex] && state.direction>0 && state.directionRay!=0)
{
int alertGrid=state.directionRay-1;
isAlertCrossing=(grid==alertGrid);
}
RegisterCrossing(state,grid,-1);
if(isAlertCrossing)
FireLevelAlert(stateIndex,grid,-1);
}
}
}
void FireLevelAlert(const int stateIndex,const int grid,const int direction)
{
if(stateIndex<0 || stateIndex>=DASHBOARD_ROWS)
return;
double alertPrice=RayPriceForStep(grid,States[stateIndex].stepPrice);
string message=StringFormat("%s: ALERT %s %d pt %s price=%s",
_Symbol,
(direction>0 ? "UP" : "DOWN"),
States[stateIndex].stepPoints,
"REVERSE AT",
DoubleToString(alertPrice,_Digits));
// ALERT одноразовый: после фактического пробоя он немедленно деактивируется.
// Перед выключением кратко показываем отдельное состояние ACTIVITY.
// Деактивация внутреннего состояния происходит ДО Alert().
AlertEnabled[stateIndex]=false;
AlertActivityUntil[stateIndex]=(ulong)GetTickCount()+(ulong)MathMax(0,AlertActivityMs);
SetAlertButtonState(stateIndex);
ChartRedraw(0);
Alert(message);
PlaySound("alert.wav");
}
bool IsAlertButtonName(const string name,int &stateIndex)
{
string prefix=Prefix+"ALERT_";
if(StringFind(name,prefix)!=0)
return false;
string suffix=StringSubstr(name,StringLen(prefix));
if(suffix=="")
return false;
int index=(int)StringToInteger(suffix);
if(index<0 || index>=DASHBOARD_ROWS)
return false;
stateIndex=index;
return true;
}
void SetAlertButtonState(const int index)
{
if(index<0 || index>=DASHBOARD_ROWS)
return;
string name=Prefix+"ALERT_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
return;
bool enabled=AlertEnabled[index];
ulong now=(ulong)GetTickCount();
bool activity=(AlertActivityUntil[index]>now);
color buttonColor=activity ? AlertActivityColor :
(enabled ? AlertOnColor : AlertOffColor);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,buttonColor);
ObjectSetInteger(0,name,OBJPROP_COLOR,AlertTextColor);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,
activity ? AlertActivityColor :
(enabled ? AlertOnColor : BorderColor));
ObjectSetString(0,name,OBJPROP_TEXT,"ALERT");
ObjectSetInteger(0,name,OBJPROP_STATE,enabled);
}
void UpdateAlertButton(const int index,const int x,const int y,const int width,const int height)
{
string name=Prefix+"ALERT_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0))
{
Print("TPDR Dashboard: alert button creation failed. Error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,140);
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
}
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,DashboardFontSize);
SetAlertButtonState(index);
}
void ToggleAlert(const int index)
{
if(index<0 || index>=DASHBOARD_ROWS)
return;
// Каждый клик заново устанавливает/снимает режим ожидания.
// После повторной активации следующий подходящий crossing снова обязан вызвать ALERT.
AlertEnabled[index]=!AlertEnabled[index];
AlertActivityUntil[index]=0;
string stateText=(AlertEnabled[index] ? "включен" : "выключен");
Print("TPDR Dashboard: ALERT ",States[index].stepPoints," pt ",stateText);
SetAlertButtonState(index);
ChartRedraw(0);
}
// Единый вход модели для одного BID-события.
// HISTORY и LIVE вызывают одну и ту же функцию.
// Порядок: входной BID -> сравнение с PreviousBid -> все LEVEL
// -> ProcessDistanceTick -> RegisterCrossing -> состояние -> ALERT/GUI.
void ProcessBidTick(const double bid,const bool emitAlert=false)
{
if(bid<=0.0)
return;
double price=NormalizeDouble(bid,_Digits);
if(!HavePreviousBid)
{
PreviousBid=price;
HavePreviousBid=true;
return;
}
if(price==PreviousBid)
return;
int levelCount=GetDashboardLevelCount();
for(int i=0;i<levelCount;i++)
ProcessDistanceTick(States[i],i,PreviousBid,price,emitAlert);
PreviousBid=price;
}
void ResetStates()
{
for(int i=0;i<DASHBOARD_ROWS;i++)
{
int levelCount=GetDashboardLevelCount();
int points=(i<levelCount ? GetDashboardLevelPoints(i) : 0);
States[i].stepPoints=points;
States[i].stepPrice=(double)points*_Point;
States[i].direction=0;
States[i].lastCrossedRay=0;
States[i].lastCrossingDirection=0;
States[i].consecutiveCrossings=0;
States[i].directionRay=0;
States[i].lastDirectionChangePrice=0.0;
}
HavePreviousBid=false;
PreviousBid=0.0;
CursorSameMsc=0;
}
bool BootstrapHistory()
{
MqlTick ticks[];
int request=MathMax(100,HistoryBootstrapTicks);
ArrayResize(ticks,request);
ResetLastError();
int copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,0,(uint)request);
int error=GetLastError();
if(copied<0)
{
Print("TPDR Dashboard: bootstrap CopyTicks failed. Error=",error);
return false;
}
if(copied==0)
return false;
if(error==4403)
return false;
int lastAttach=-1;
for(int i=0;i<copied;i++)
{
if(ticks[i].time_msc<=AttachMsc)
lastAttach=i;
}
if(lastAttach<0)
return false;
ResetStates();
for(int i=0;i<=lastAttach;i++)
{
if(ticks[i].bid>0.0)
ProcessBidTick(ticks[i].bid,false);
}
CursorMsc=AttachMsc;
int attachCount=0;
for(int i=lastAttach;i>=0 && ticks[i].time_msc==AttachMsc;i--)
attachCount++;
CursorSameMsc=attachCount;
BootstrapComplete=true;
return true;
}
bool ProcessLiveTicks()
{
MqlTick ticks[5000];
int request=MathMin(MathMax(HistoryChunkSize,100),5000);
for(int chunk=0;chunk<8;chunk++)
{
ResetLastError();
int copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,
(ulong)MathMax(1,CursorMsc),(uint)request);
int error=GetLastError();
if(copied<0)
{
Print("TPDR Dashboard: live CopyTicks failed. Error=",error);
return false;
}
if(copied==0)
return true;
int start=0;
if(ticks[0].time_msc==CursorMsc)
start=MathMin(CursorSameMsc,copied);
int processed=0;
for(int i=start;i<copied;i++)
{
if(ticks[i].bid>0.0)
{
ProcessBidTick(ticks[i].bid,true);
processed++;
}
}
long finalMsc=ticks[copied-1].time_msc;
if(finalMsc==CursorMsc)
{
int sameMsc=0;
for(int i=start;i<copied;i++)
{
if(ticks[i].time_msc==finalMsc)
sameMsc++;
}
CursorSameMsc+=sameMsc;
}
else
{
CursorMsc=finalMsc;
int finalCount=0;
for(int i=copied-1;i>=0 && ticks[i].time_msc==finalMsc;i--)
finalCount++;
CursorSameMsc=finalCount;
}
if(copied<request || processed==0)
return true;
}
return true;
}
int GetTriggerGrid(const DistanceState &state,bool &valid,bool &up)
{
valid=false;
up=false;
if(state.direction>0 && state.directionRay!=0)
{
// Для UP разворот только на один уровень ниже последнего
// достигнутого UP-уровня.
valid=true;
up=false;
return state.directionRay-1;
}
if(state.direction<0 && state.directionRay!=0)
{
// Для DOWN разворот только на один уровень выше последнего
// достигнутого DOWN-уровня.
valid=true;
up=true;
return state.directionRay+1;
}
return 0;
}
string StatusText(const int direction)
{
if(direction>0)
return "UP";
if(direction<0)
return "DOWN";
return "WAIT";
}
color StatusColor(const int direction)
{
if(direction>0)
return UpColor;
if(direction<0)
return DownColor;
return WaitColor;
}
string TriggerText(const DistanceState &state)
{
bool valid=false;
bool up=false;
int grid=GetTriggerGrid(state,valid,up);
if(!valid)
return "—";
double price=RayPriceForStep(grid,state.stepPrice);
return (up ? "↑ " : "↓ ")+DoubleToString(price,_Digits);
}
void DeleteDashboardObjects()
{
ObjectsDeleteAll(0,Prefix,-1,-1);
}
void EnsureRect(const string name,const int x,const int y,const int width,const int height,
const color background,const color border)
{
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0))
{
Print("TPDR Dashboard: object creation failed. Error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
}
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
ObjectSetInteger(0,name,OBJPROP_COLOR,border);
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(0,name,OBJPROP_ZORDER,100);
}
void EnsureLabel(const string name,const int x,const int y,const int fontSize,
const color textColor,const string textValue)
{
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_LABEL,0,0,0))
{
Print("TPDR Dashboard: label creation failed. Error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,110);
}
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize);
ObjectSetInteger(0,name,OBJPROP_COLOR,textColor);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
}
void CreateRowsControl(const int panelX,const int y,const int panelWidth)
{
int buttonHeight=RowHeight-3;
int minusWidth=28;
int valueWidth=92;
int plusWidth=28;
int totalWidth=minusWidth+valueWidth+plusWidth;
int startX=panelX+(panelWidth-totalWidth)/2;
string names[3]={Prefix+"ROWS_MINUS",Prefix+"ROWS_VALUE",Prefix+"ROWS_PLUS"};
int xs[3]={startX,startX+minusWidth,startX+minusWidth+valueWidth};
int widths[3]={minusWidth,valueWidth,plusWidth};
string texts[3]={"-","ROWS: "+IntegerToString(VisibleRows),"+"};
for(int i=0;i<3;i++)
{
if(ObjectFind(0,names[i])<0)
{
ResetLastError();
if(!ObjectCreate(0,names[i],OBJ_BUTTON,0,0,0))
{
Print("TPDR Dashboard: rows control creation failed. Error=",GetLastError(),
" Name=",names[i]);
continue;
}
ObjectSetInteger(0,names[i],OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,names[i],OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,names[i],OBJPROP_SELECTED,false);
ObjectSetInteger(0,names[i],OBJPROP_HIDDEN,true);
ObjectSetInteger(0,names[i],OBJPROP_ZORDER,150);
ObjectSetString(0,names[i],OBJPROP_FONT,"Segoe UI");
}
ObjectSetInteger(0,names[i],OBJPROP_XDISTANCE,xs[i]);
ObjectSetInteger(0,names[i],OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,names[i],OBJPROP_XSIZE,widths[i]);
ObjectSetInteger(0,names[i],OBJPROP_YSIZE,buttonHeight);
ObjectSetInteger(0,names[i],OBJPROP_FONTSIZE,DashboardFontSize);
ObjectSetInteger(0,names[i],OBJPROP_BGCOLOR,C'42,50,64');
ObjectSetInteger(0,names[i],OBJPROP_BORDER_COLOR,BorderColor);
ObjectSetInteger(0,names[i],OBJPROP_COLOR,TextColor);
ObjectSetString(0,names[i],OBJPROP_TEXT,texts[i]);
}
}
void DeleteHiddenDashboardRows(const int visibleCount)
{
for(int i=visibleCount;i<DASHBOARD_ROWS;i++)
{
ObjectDelete(0,Prefix+"ROW_"+IntegerToString(i));
ObjectDelete(0,Prefix+"STATEBAR_"+IntegerToString(i));
ObjectDelete(0,Prefix+"LEVEL_"+IntegerToString(i));
ObjectDelete(0,Prefix+"STATE_"+IntegerToString(i));
ObjectDelete(0,Prefix+"TRIGGER_"+IntegerToString(i));
ObjectDelete(0,Prefix+"ALERT_"+IntegerToString(i));
ObjectDelete(0,Prefix+"DIST_"+IntegerToString(i));
ObjectDelete(0,Prefix+"LAST_"+IntegerToString(i));
}
}
string BuildRowsStateKey()
{
// Chart ID сохраняет привязку количества строк к конкретному графику.
// При смене периода MT5 переинициализирует индикатор, поэтому
// значение должно храниться вне памяти экземпляра индикатора.
return ROWS_STATE_PREFIX+IntegerToString(ChartID())+"_"+_Symbol;
}
void SaveVisibleRows()
{
if(RowsStateKey=="")
RowsStateKey=BuildRowsStateKey();
GlobalVariableSet(RowsStateKey,(double)VisibleRows);
}
void LoadVisibleRows()
{
int configured=GetDashboardLevelCount();
if(RowsStateKey=="")
RowsStateKey=BuildRowsStateKey();
VisibleRows=DEFAULT_VISIBLE_ROWS;
if(GlobalVariableCheck(RowsStateKey))
{
int saved=(int)MathRound(GlobalVariableGet(RowsStateKey));
if(saved>=1 && saved<=configured)
VisibleRows=saved;
}
// Если конфигурация уровней изменилась и сохранённое значение больше
// доступного количества строк, используем максимально допустимое.
VisibleRows=MathMax(1,MathMin(VisibleRows,configured));
}
void SetVisibleRows(const int delta)
{
int configured=GetDashboardLevelCount();
int newRows=VisibleRows+delta;
if(newRows<1)
newRows=1;
if(newRows>configured)
newRows=configured;
if(newRows==VisibleRows)
return;
VisibleRows=newRows;
// Сохраняем выбор пользователя сразу после нажатия +/−.
// Это значение будет восстановлено после смены периода графика.
SaveVisibleRows();
RenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
Print("TPDR Dashboard: visual rows changed to ",VisibleRows,
" of ",configured);
}
bool IsRowsControlName(const string name)
{
return name==Prefix+"ROWS_MINUS" ||
name==Prefix+"ROWS_PLUS";
}
void RenderDashboard(const double bid)
{
int levelCount=MathMin(GetDashboardLevelCount(),VisibleRows);
CalculateAutoLayout(bid);
int controlsHeight=RowHeight+8;
// Управление количеством строк находится сверху таблицы,
// между заголовками и первой строкой.
int panelHeight=HeaderHeight+controlsHeight+levelCount*RowHeight+12;
int panelX=MathMax(4,PanelLeftMargin);
int panelY=MathMax(4,PanelTopMargin);
EnsureRect(Prefix+"BG",panelX,panelY,AutoPanelWidth,panelHeight,
PanelBackground,BorderColor);
EnsureRect(Prefix+"HEADER",panelX+2,panelY+2,AutoPanelWidth-4,HeaderHeight-4,
PanelBackground,PanelBackground);
EnsureLabel(Prefix+"TITLE",panelX+16,panelY+8,DashboardFontSize,TextColor,
"TICK PRICE • "+IntegerToString(levelCount)+" LEVELS");
int bidWidth=MeasureTextWidth("BID "+DoubleToString(bid,_Digits));
EnsureLabel(Prefix+"BID",panelX+AutoPanelWidth-bidWidth-16,panelY+8,DashboardFontSize,HeaderColor,
"BID "+DoubleToString(bid,_Digits));
int headerY=panelY+HeaderHeight-20;
EnsureLabel(Prefix+"COL_LEVEL",panelX+ColLevelX,headerY,DashboardFontSize,MutedTextColor,"LEVEL");
EnsureLabel(Prefix+"COL_STATUS",panelX+ColStatusX,headerY,DashboardFontSize,MutedTextColor,"STATUS");
EnsureLabel(Prefix+"COL_TRIGGER",panelX+ColTriggerX,headerY,DashboardFontSize,MutedTextColor,"REVERSE AT");
EnsureLabel(Prefix+"COL_ALERT",panelX+ColAlertX,headerY,DashboardFontSize,MutedTextColor,"ALERT");
EnsureLabel(Prefix+"COL_DISTANCE",panelX+ColDistanceX,headerY,DashboardFontSize,MutedTextColor,"DIST");
EnsureLabel(Prefix+"COL_LAST",panelX+ColLastX,headerY,DashboardFontSize,MutedTextColor,"LAST");
for(int row=0;row<levelCount;row++)
{
int index=levelCount-1-row;
int y=panelY+HeaderHeight+controlsHeight+row*RowHeight;
string suffix=IntegerToString(index);
int direction=States[index].direction;
EnsureRect(Prefix+"ROW_"+suffix,panelX+2,y,AutoPanelWidth-4,RowHeight-1,
(row%2==0 ? RowBackground : RowBackgroundAlt),
(row==0 || row==levelCount-1 ? BorderColor :
(row%5==0 ? BorderColor : RowBackgroundAlt)));
EnsureRect(Prefix+"STATEBAR_"+suffix,panelX+3,y+2,5,RowHeight-5,
StatusColor(direction),StatusColor(direction));
EnsureLabel(Prefix+"LEVEL_"+suffix,panelX+ColLevelX,y+3,DashboardFontSize,TextColor,
IntegerToString(States[index].stepPoints)+" pt");
EnsureLabel(Prefix+"STATE_"+suffix,panelX+ColStatusX,y+3,DashboardFontSize,
StatusColor(direction),StatusText(direction));
bool valid=false;
bool triggerUp=false;
int triggerGrid=GetTriggerGrid(States[index],valid,triggerUp);
string triggerText="—";
string distanceText="—";
if(valid)
{
double triggerPrice=RayPriceForStep(triggerGrid,States[index].stepPrice);
triggerText=(triggerUp ? "↑ " : "↓ ")+DoubleToString(triggerPrice,_Digits);
distanceText=DoubleToString(MathAbs(triggerPrice-bid)/_Point,0)+" pt";
}
EnsureLabel(Prefix+"TRIGGER_"+suffix,panelX+ColTriggerX,y+3,DashboardFontSize,
valid ? StatusColor(direction) : MutedTextColor,triggerText);
int alertButtonWidth=MeasureTextWidth("ALERT")+16;
UpdateAlertButton(index,panelX+ColAlertX,y+1,alertButtonWidth,RowHeight-3);
EnsureLabel(Prefix+"DIST_"+suffix,panelX+ColDistanceX,y+3,DashboardFontSize,MutedTextColor,distanceText);
string lastText=States[index].lastDirectionChangePrice>0.0 ?
DoubleToString(States[index].lastDirectionChangePrice,_Digits) : "—";
EnsureLabel(Prefix+"LAST_"+suffix,panelX+ColLastX,y+3,DashboardFontSize,MutedTextColor,lastText);
}
DeleteHiddenDashboardRows(levelCount);
// Кнопки изменения количества строк размещены сверху таблицы.
CreateRowsControl(panelX,panelY+HeaderHeight+2,AutoPanelWidth);
ChartRedraw(0);
}
int OnInit()
{
int levelCount=GetDashboardLevelCount();
if(levelCount<1 || HistoryBootstrapTicks<100 || HistoryChunkSize<100 ||
UpdateIntervalMs<10 || UpdateIntervalMs>2000 ||
RowHeight<20 || HeaderHeight<45 || DashboardFontSize<7 ||
DashboardFontSize>18 || PanelLeftMargin<0 || PanelTopMargin<0)
return INIT_PARAMETERS_INCORRECT;
if(AlertActivityMs<0 || AlertActivityMs>10000)
return INIT_PARAMETERS_INCORRECT;
for(int i=0;i<levelCount;i++)
{
if(GetDashboardLevelPoints(i)<=0 || (i>0 && GetDashboardLevelPoints(i)<=GetDashboardLevelPoints(i-1)))
return INIT_PARAMETERS_INCORRECT;
}
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
MqlTick attachTick;
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
return INIT_FAILED;
AttachMsc=attachTick.time_msc;
CursorMsc=AttachMsc;
for(int i=0;i<DASHBOARD_ROWS;i++)
{
AlertEnabled[i]=false;
AlertActivityUntil[i]=0;
}
// Количество строк — пользовательская настройка интерфейса.
// Она не должна возвращаться к DEFAULT_VISIBLE_ROWS при переинициализации
// индикатора, например при смене периода графика.
LoadVisibleRows();
ResetStates();
EventSetMillisecondTimer(UpdateIntervalMs);
return INIT_SUCCEEDED;
}
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id!=CHARTEVENT_OBJECT_CLICK)
return;
if(IsRowsControlName(sparam))
{
SetVisibleRows(sparam==Prefix+"ROWS_PLUS" ? 1 : -1);
return;
}
int index=-1;
if(!IsAlertButtonName(sparam,index))
return;
ToggleAlert(index);
}
void OnDeinit(const int reason)
{
EventKillTimer();
DeleteDashboardObjects();
}
void OnTimer()
{
if(!BootstrapComplete)
{
if(!BootstrapHistory())
return;
}
else
{
ProcessLiveTicks();
}
MqlTick current;
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
return;
RenderDashboard(current.bid);
}
int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[],
const double &open[],const double &high[],const double &low[],
const double &close[],const long &tick_volume[],const long &volume[],
const int &spread[])
{
return rates_total;
}