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