| // ===============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;
|
| }
|