mql5/Indicators/CodeLock/CodeLock-Dashboard-TickGraphic.mq5
4110 lines
134 KiB
MQL5
rytikov0285
2026-11-05 12:52:36 +03:00
#include <Canvas\Canvas.mqh>
#property strict
#property indicator_separate_window
#property indicator_plots 0
#property version "3.00"
#property description "Unified CodeLock Dashboard + Tick Graphic"
#define INVALID_GRID (-2147483647)
#define CODELOCK_KEY_ESCAPE 0x1B
#define DASH_MIN_VISUAL_ROWS 5
#define DASH_VISUAL_ROW_STEP 10
#define DASH_VISUAL_LEVEL_MAX 2000
#define DASH_VISUAL_ROW_MAX 40
#define DASH_DASHBOARD_ROWS 40
// ============================================================================
// CODELOCK-DASHBOARD-TICKGRAPHIC
// ============================================================================
input double StartPrice=1; // Начальная цена, от которой строится сетка уровней
input int BufferPoints=0; // Единый BUFFER подтверждения CROSSING в points
input int HistoryChunkSize=5000; // Размер блока исторических тиков для обработки
input int HistoryGraphTicks=700000; // Количество исторических тиков для первоначального графика
input int HistoryBootstrapTicks=700000; // Количество исторических тиков для восстановления состояния до запуска
input int MaxGraphSegments=2000000; // Максимальное количество тиков графика; согласовано с лимитом расчётной истории
input int MaxModelHistoryTicks=2000000; // Максимальное количество тиков, доступных для полного пересчета модели
input bool DrawReversalArrows=true; // Показывать последние REVERSAL: N UP + N DOWN на каждый LEVEL
input int ReversalArrowsHistory=20; // Количество последних REVERSAL отдельно по направлению и LEVEL
input bool DrawRayObjects=true; // Показывать ценовые лучи
input int PriceLabelsCount=3; // Legacy input: currently unused; retained for saved parameter-set compatibility
input int InitialViewTicks=50000; // Количество тиков в первоначальном масштабе
input int MinViewTicks=5; // Минимальное количество тиков при максимальном приближении
input int BidLabelOffset=8; // Legacy input: currently unused; retained for saved parameter-set compatibility
// Вертикальный viewport фиксируется при первом построении: новые тики не сдвигают
// масштаб и исторические цены по вертикали. Цены вне диапазона не расширяют окно.
// Уровни динамически появляются/исчезают по текущему диапазону BID.
input bool EnableAlert=true; // Разрешить экранные уведомления
input bool EnablePushNotification=true; // Разрешить Push-уведомления
input bool EnableEmailNotification=true; // Разрешить уведомления по Email
input bool EnableSoundNotification=true; // Разрешить звуковые уведомления
input string NotificationSound="alert.wav"; // Имя звукового файла уведомления
input color TickGraphColor=clrDodgerBlue; // Цвет тикового графика
input color RayColor=clrSilver; // Цвет обычных лучей сетки
input color CurrentRayColor=clrGold; // Цвет текущего уровня и Bid
input color UpCrossColor=clrGreen; // Цвет стрелок пересечения вверх
input color DownCrossColor=clrRed; // Цвет стрелок пересечения вниз
input color BuyColor=clrBlue; // Legacy input: currently unused; retained for saved parameter-set compatibility
input color SellColor=clrRed; // Legacy input: currently unused; retained for saved parameter-set compatibility
string Prefix="TPDR_";
string ShortName="CodeLock-Dashboard-TickGraphic";
// Forward declarations: MQL5 requires a declaration before an earlier call.
void DashFireLevelAlert(const int stateIndex,const int grid,const int direction);
void DashRenderDashboard(const double bid);
void HandleHorizontalZoom(const int xCursor,const int delta);
void NotifyEvent(const string eventName,const string directionName,const int level,const double price,const long timeMsc);
double GetModelTickByIndex(const int index);
void RecordReversalArrow(const double price,const bool buy,const int level=0);
int GetViewStartIndex();
int GetViewCount();
int GetGraphWidth();
void EnsurePanelLabel(const string name,const string textValue);
void RefreshGraph();
void DashSetAlertButtonState(const int index);
void SyncTickVisualRowsFromDashboard();
bool HistoryDerivedStateComplete();
bool RestoreChartEventSettings();
// Рабочее состояние количества уровней. Inputs задают начальное значение,
// а кнопки изменяют именно это состояние без изменения input-параметров.
double PreviousBid=0.0;
bool HavePreviousBid=false;
bool LiveMode=false;
bool BootstrapComplete=false;
int SubWindow=-1;
long AttachMsc=0;
long CursorMsc=1;
int CursorSameMsc=0;
int BootstrapAttempts=0;
int HistoryBootstrapTargetTicks=0;
uint BootstrapLastAttemptTick=0;
bool BootstrapRetryPending=false;
int BootstrapLastLoggedError=0;
uint BootstrapLastErrorLogTick=0;
const uint BootstrapRetryDelayMs=1000;
const uint BootstrapErrorLogIntervalMs=60000;
CCanvas TickCanvas;
bool CanvasReady=false;
int CanvasWidth=0;
int CanvasHeight=0;
int CanvasSubWindow=-1;
double ScaleMin=0.0;
double ScaleMax=0.0;
double GraphScaleMin=0.0;
double GraphScaleMax=0.0;
int ScaleCenterGrid=0;
double ViewportCenterPrice=0.0;
bool ScaleInitialized=false;
// Текущий LEVEL, определённый фактическим BID.
// Это состояние отображения LEVEL, а не центр вертикального viewport.
int CurrentLevelGrid=INVALID_GRID;
bool CurrentLevelGridInitialized=false;
int ViewSpan=0;
int ViewEndOffset=0;
bool DragActive=false;
int DragStartX=0;
int DragStartOffset=0;
// После ручного масштабирования AutoScale сохраняет выбранный диапазон.
bool VerticalScaleManual=false;
bool VerticalScaleDragActive=false;
bool VerticalPanDragActive=false;
bool PointerDragTracking=false;
int VerticalPanDragStartY=0;
double VerticalPanDragStartCenterPrice=0.0;
bool InitialArrowHistoryScalePending=false;
int VerticalScaleDragStartY=0;
double VerticalScaleDragStartHalfRange=0.0;
double VerticalScaleHalfRange=0.0;
const int VerticalScaleHandleWidth=18;
const double VerticalScaleDragSensitivity=0.008;
const int ZoomButtonWidth=36;
const int ResetButtonWidth=64;
const int ZoomButtonHeight=28;
const int ZoomButtonGap=6;
const int PriceScaleWidth=85;
const int GraphTopReserved=0;
const int GraphBottomReserved=0;
const double GraphBidSafeLower=0.22;
const double GraphBidSafeUpper=0.78;
const double GraphBidTargetLower=0.32;
const double GraphBidTargetUpper=0.68;
const int LevelButtonWidth=32;
const int LevelButtonHeight=22;
const int LevelButtonGap=3;
const int GraphLevelsPerSideMin=5;
const int GraphLevelsPerSideMax=DASH_VISUAL_LEVEL_MAX;
int GraphLevelsPerSide=10;
const int GraphDisplayedLevelCountMin=1;
const int GraphDisplayedLevelCountMax=100;
int GraphDisplayedLevelCount=5;
string GraphDisplayedLevelCountKey="";
string LastGraphLabelNames[];
int LastGraphLabelNameCount=0;
bool LegacyGraphLabelsCleaned=false;
const int ControlPanelTop=24;
const int DistanceButtonWidth=36;
const int DistanceButtonHeight=22;
const int DistanceButtonGap=3;
const int ControlPanelLabelWidth=52;
const int ControlPanelPadding=5;
const int ControlPanelRowGap=4;
const int ControlPanelHeight=520;
// Истинно после однократного расчёта первоначального вертикального диапазона.
bool AutoScaleInitialized=false;
bool ResetButtonHeld=false;
bool PreviousChartMouseScroll=true;
bool PreviousChartEventMouseMove=false;
bool PreviousChartEventMouseWheel=false;
bool ChartEventSettingsCaptured=false;
bool MouseScrollSetErrorLogged=false;
bool DashboardHasPublishedState=false;
bool LegacyPriceObjectsCleaned=false;
bool MouseInTickSubwindow=false;
bool InitialViewLayoutActive=false;
// Сохранённый снимок расчётного состояния используется после REASON_CHARTCHANGE.
// Он не является отдельным источником истины: кэш — сериализованная копия
// ModelTickPrices[] и производного состояния.
double TickPrices[];
int TickCount=0;
int GraphHistoryCapacity=2000000;
int TickHead=0;
long TickSerial=0;
// Единственный источник истины для пересчета модели.
// Графический хвост TickPrices[] является только представлением этой истории.
double ModelTickPrices[];
double ModelTickAsks[];
int ModelTickCount=0;
int ModelTickHead=0;
struct SignalMark
{
long serial;
double price;
int level; // LEVEL, на сетке которого подтверждён REVERSAL
bool up;
bool reversal;
};
SignalMark SignalMarks[];
// LEVEL — независимая сетка; отдельной модели DISTANCE нет.
struct TickLevelRow
{
int level;
int direction;
int directionGrid;
int lastCrossedGrid;
int lastCrossingDirection;
int consecutiveCrossings;
double lastReversalPrice;
double reversalAtPrice;
double lastDown1Price;
double lastDown2Price;
double lastUp1Price;
double lastUp2Price;
double phaseMinBid;
double phaseMaxAsk;
bool alertEnabled;
};
TickLevelRow TickLevelRows[];
int TickLevelRowCount=0;
void ResetTickLevelRowState(TickLevelRow &row)
{
row.direction=0;
row.directionGrid=INVALID_GRID;
row.lastCrossedGrid=INVALID_GRID;
row.lastCrossingDirection=0;
row.consecutiveCrossings=0;
row.lastReversalPrice=0.0;
row.reversalAtPrice=0.0;
row.lastDown1Price=0.0;
row.lastDown2Price=0.0;
row.lastUp1Price=0.0;
row.lastUp2Price=0.0;
row.phaseMinBid=0.0;
row.phaseMaxAsk=0.0;
row.alertEnabled=false;
}
void ResetAllTickVisualRowStates()
{
for(int i=0;i<TickLevelRowCount;i++)
ResetTickLevelRowState(TickLevelRows[i]);
}
int FloorGridForStep(const double price,const double step)
{
if(step<=0.0)
return INVALID_GRID;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
return (int)MathFloor(position);
}
int CeilGridForStep(const double price,const double step)
{
if(step<=0.0)
return INVALID_GRID;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
return (int)MathCeil(position);
}
double RayPriceForStep(const int grid,const double step)
{
if(step<=0.0)
return 0.0;
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
}
int TickRowGrid(const int rowIndex,const double price)
{
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
return INVALID_GRID;
double step=(double)TickLevelRows[rowIndex].level*_Point;
return FloorGridForStep(price,step);
}
double TickRowPrice(const int rowIndex,const int grid)
{
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
return 0.0;
double step=(double)TickLevelRows[rowIndex].level*_Point;
return RayPriceForStep(grid,step);
}
// REVERSAL AT — соседний уровень, обратное пересечение которого подтверждает REVERSAL.
void FinalizeTickRowsFromHistory()
{
if(TickLevelRowCount<=0 || ModelTickCount<2)
return;
// Если история не содержит CROSSING, начальное направление берётся из последнего изменения BID.
double lastBid=GetModelTickByIndex(ModelTickCount-1);
double previousBid=GetModelTickByIndex(ModelTickCount-2);
if(lastBid<=0.0 || previousBid<=0.0 || lastBid==previousBid)
return;
int fallbackDirection=(lastBid>previousBid ? 1 : -1);
for(int i=0;i<TickLevelRowCount;i++)
{
if(TickLevelRows[i].direction!=0)
continue;
double step=(double)TickLevelRows[i].level*_Point;
if(step<=0.0)
continue;
int direction=fallbackDirection;
int grid=(direction>0 ? CeilGridForStep(lastBid,step) :
FloorGridForStep(lastBid,step));
if(grid==INVALID_GRID)
continue;
TickLevelRows[i].direction=direction;
TickLevelRows[i].directionGrid=grid;
TickLevelRows[i].lastCrossedGrid=grid;
TickLevelRows[i].lastCrossingDirection=direction;
TickLevelRows[i].consecutiveCrossings=0;
UpdateTickRowReversalAt(i);
}
}
bool HistoryDerivedStateComplete()
{
// Published Dashboard values must come from the calculated history model.
// Do not publish temporary dash placeholders while history is still loading.
if(TickLevelRowCount<=0 || ModelTickCount<2)
return false;
for(int i=0;i<TickLevelRowCount;i++)
{
if(TickLevelRows[i].direction==0 || TickLevelRows[i].reversalAtPrice<=0.0)
return false;
if(TickLevelRows[i].lastDown1Price<=0.0 || TickLevelRows[i].lastDown2Price<=0.0)
return false;
if(TickLevelRows[i].lastUp1Price<=0.0 || TickLevelRows[i].lastUp2Price<=0.0)
return false;
}
return true;
}
void UpdateTickRowReversalAt(const int rowIndex)
{
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
return;
if(TickLevelRows[rowIndex].direction==0 || TickLevelRows[rowIndex].directionGrid==INVALID_GRID)
{
TickLevelRows[rowIndex].reversalAtPrice=0.0;
return;
}
TickLevelRows[rowIndex].reversalAtPrice=TickRowPrice(rowIndex,
TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction);
}
bool RegisterTickRowCrossing(const int rowIndex,const int grid,
const int crossingDirection,const MqlTick &tick,
const bool emitSignals)
{
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
return false;
TickLevelRows[rowIndex].lastCrossedGrid=grid;
TickLevelRows[rowIndex].lastCrossingDirection=crossingDirection;
if(TickLevelRows[rowIndex].direction==0)
{
TickLevelRows[rowIndex].direction=crossingDirection;
TickLevelRows[rowIndex].directionGrid=grid;
TickLevelRows[rowIndex].consecutiveCrossings=1;
UpdateTickRowReversalAt(rowIndex);
if(crossingDirection<0)
TickLevelRows[rowIndex].phaseMinBid=tick.bid;
else if(tick.ask>0.0)
TickLevelRows[rowIndex].phaseMaxAsk=tick.ask;
return false;
}
if(crossingDirection==TickLevelRows[rowIndex].direction)
{
// CROSSING того же направления обновляет направление.
// REVERSAL подтверждается только на соседнем grid в обратную сторону.
TickLevelRows[rowIndex].directionGrid=grid;
TickLevelRows[rowIndex].consecutiveCrossings++;
UpdateTickRowReversalAt(rowIndex);
return false;
}
int reversalGrid=TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction;
if(grid!=reversalGrid)
return false;
int oldDirection=TickLevelRows[rowIndex].direction;
TickLevelRows[rowIndex].direction=crossingDirection;
TickLevelRows[rowIndex].directionGrid=grid;
TickLevelRows[rowIndex].consecutiveCrossings=1;
TickLevelRows[rowIndex].lastReversalPrice=TickRowPrice(rowIndex,grid);
UpdateTickRowReversalAt(rowIndex);
if(crossingDirection<0)
{
TickLevelRows[rowIndex].lastDown2Price=TickLevelRows[rowIndex].lastDown1Price;
TickLevelRows[rowIndex].lastDown1Price=TickLevelRows[rowIndex].lastReversalPrice;
}
else
{
TickLevelRows[rowIndex].lastUp2Price=TickLevelRows[rowIndex].lastUp1Price;
TickLevelRows[rowIndex].lastUp1Price=TickLevelRows[rowIndex].lastReversalPrice;
}
if(oldDirection>0)
TickLevelRows[rowIndex].phaseMaxAsk=0.0;
else
TickLevelRows[rowIndex].phaseMinBid=0.0;
RecordReversalArrow(TickLevelRows[rowIndex].lastReversalPrice,
crossingDirection>0,TickLevelRows[rowIndex].level);
return true;
}
void ProcessTickRow(const int rowIndex,const double oldBid,const double bid,
const MqlTick &tick,const bool emitSignals)
{
if(rowIndex<0 || rowIndex>=TickLevelRowCount || bid==oldBid)
return;
double step=(double)TickLevelRows[rowIndex].level*_Point;
if(step<=0.0)
return;
double buffer=(double)MathMax(0,BufferPoints)*_Point;
if(bid>oldBid)
{
int first=FloorGridForStep(oldBid-buffer,step)-1;
int last=FloorGridForStep(bid-buffer,step)+1;
for(int grid=first;grid<=last;grid++)
{
double confirmationPrice=NormalizeDouble(
RayPriceForStep(grid,step)+buffer,_Digits);
if(confirmationPrice>oldBid && confirmationPrice<=bid)
{
bool reversal=RegisterTickRowCrossing(rowIndex,grid,1,tick,emitSignals);
if(reversal && TickLevelRows[rowIndex].alertEnabled && emitSignals)
{
NotifyEvent("REVERSAL","UP",
TickLevelRows[rowIndex].level,
TickRowPrice(rowIndex,grid),tick.time_msc);
DashFireLevelAlert(rowIndex,grid,1);
TickLevelRows[rowIndex].alertEnabled=false;
}
}
}
}
else
{
int first=CeilGridForStep(oldBid+buffer,step)+1;
int last=CeilGridForStep(bid+buffer,step)-1;
for(int grid=first;grid>=last;grid--)
{
double confirmationPrice=NormalizeDouble(
RayPriceForStep(grid,step)-buffer,_Digits);
if(confirmationPrice<oldBid && confirmationPrice>=bid)
{
bool reversal=RegisterTickRowCrossing(rowIndex,grid,-1,tick,emitSignals);
if(reversal && TickLevelRows[rowIndex].alertEnabled && emitSignals)
{
NotifyEvent("REVERSAL","DOWN",
TickLevelRows[rowIndex].level,
TickRowPrice(rowIndex,grid),tick.time_msc);
DashFireLevelAlert(rowIndex,grid,-1);
TickLevelRows[rowIndex].alertEnabled=false;
}
}
}
}
}
void AppendModelTick(const double bid,const double ask)
{
if(bid<=0.0 || MaxModelHistoryTicks<1)
return;
if(ArraySize(ModelTickPrices)<MaxModelHistoryTicks)
ArrayResize(ModelTickPrices,MaxModelHistoryTicks,MaxModelHistoryTicks);
if(ArraySize(ModelTickAsks)<MaxModelHistoryTicks)
ArrayResize(ModelTickAsks,MaxModelHistoryTicks,MaxModelHistoryTicks);
if(ModelTickCount<MaxModelHistoryTicks)
{
int index=(ModelTickHead+ModelTickCount)%MaxModelHistoryTicks;
ModelTickPrices[index]=bid;
ModelTickAsks[index]=(ask>0.0 ? ask : 0.0);
ModelTickCount++;
}
else
{
ModelTickPrices[ModelTickHead]=bid;
ModelTickAsks[ModelTickHead]=(ask>0.0 ? ask : 0.0);
ModelTickHead=(ModelTickHead+1)%MaxModelHistoryTicks;
}
}
double GetModelTickByIndex(const int index)
{
if(index<0 || index>=ModelTickCount)
return 0.0;
return ModelTickPrices[(ModelTickHead+index)%MaxModelHistoryTicks];
}
double GetModelAskByIndex(const int index)
{
if(index<0 || index>=ModelTickCount)
return 0.0;
return ModelTickAsks[(ModelTickHead+index)%MaxModelHistoryTicks];
}
void ResetSelectedCalculation()
{
PreviousBid=0.0;
HavePreviousBid=false;
}
void ClearDerivedState()
{
TickCount=0;
TickHead=0;
TickSerial=0;
ArrayResize(TickPrices,MathMax(100,GraphHistoryCapacity),
MathMax(100,GraphHistoryCapacity));
ArrayResize(SignalMarks,0);
// Не сбрасывать viewport при пересборке расчётной истории:
// он относится к отображению, а не к производной модели тиков.
ResetSelectedCalculation();
ResetAllTickVisualRowStates();
}
void ShiftRelativeWindowForCrossing(const int crossingGrid)
{
// CROSSING не изменяет viewport. Визуальное окно меняется только
// при достижении BID физического края ценового диапазона.
}
double GetViewportLevelStepPrice()
{
double minimum=0.0;
for(int i=0;i<TickLevelRowCount;i++)
{
double step=(double)TickLevelRows[i].level*_Point;
if(step<=0.0) continue;
if(minimum<=0.0 || step<minimum) minimum=step;
}
return minimum;
}
int FloorGridForViewport(const double price)
{
double step=GetViewportLevelStepPrice();
if(step<=0.0) return INVALID_GRID;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
return (int)MathFloor(position+1.0e-9);
}
int CeilGridForViewport(const double price)
{
double step=GetViewportLevelStepPrice();
if(step<=0.0) return INVALID_GRID;
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
return (int)MathCeil(position-1.0e-9);
}
double ViewportRayPrice(const int grid)
{
double step=GetViewportLevelStepPrice();
if(step<=0.0) return 0.0;
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
}
void DeleteObjectsByPrefix()
{
ObjectsDeleteAll(0,Prefix,-1,-1);
}
void NotifyEvent(const string eventName,const string directionName,const int level,const double price,const long timeMsc)
{
if(!LiveMode)
return;
string directionArrow=(directionName=="UP" ? "↑" : "↓");
string msg=StringFormat("%s %s ···· Level: %d pt ···· Price Level: %s",
directionArrow,_Symbol,level,DoubleToString(price,_Digits));
Print(msg);
if(EnableAlert)
Alert(msg);
if(EnableSoundNotification && StringLen(NotificationSound)>0)
PlaySound(NotificationSound);
if(EnablePushNotification)
{
ResetLastError();
if(!SendNotification(msg))
Print("TPDR SendNotification failed. Error=",GetLastError());
}
if(EnableEmailNotification)
{
ResetLastError();
if(!SendMail("TPDR "+eventName+" "+directionName,msg))
Print("TPDR SendMail failed. Error=",GetLastError());
}
}
void AddTickToGraph(const double bid)
{
if(bid<=0.0 || GraphHistoryCapacity<1)
return;
// TickPrices[] заранее резервируется целиком. На глубокой истории
// нельзя делать ArrayResize() на каждом тике: это создаёт лишние
// перераспределения памяти и заметно увеличивает стартовую загрузку.
if(ArraySize(TickPrices)<GraphHistoryCapacity)
ArrayResize(TickPrices,GraphHistoryCapacity,GraphHistoryCapacity);
if(TickCount<GraphHistoryCapacity)
{
int index=(TickHead+TickCount)%GraphHistoryCapacity;
TickPrices[index]=bid;
TickCount++;
}
else
{
TickPrices[TickHead]=bid;
TickHead=(TickHead+1)%GraphHistoryCapacity;
}
}
double GetTickPriceByVisibleIndex(const int index)
{
if(index<0 || index>=TickCount)
return 0.0;
int physical=(TickHead+index)%GraphHistoryCapacity;
return TickPrices[physical];
}
void AddReversalSignalMark(const double price,const bool up,const int level)
{
if(!DrawReversalArrows || ReversalArrowsHistory<=0 ||
price<=0.0 || level<=0)
return;
int size=ArraySize(SignalMarks);
ArrayResize(SignalMarks,size+1);
SignalMarks[size].serial=TickSerial;
SignalMarks[size].price=NormalizeDouble(price,_Digits);
SignalMarks[size].level=level;
SignalMarks[size].up=up;
SignalMarks[size].reversal=true;
// Keep the last N UP and N DOWN reversals independently for each LEVEL.
// Otherwise activity on other dashboard levels can evict this level's arrows
// before DrawSignalLayer() applies its selected-level filter.
int keepCount=MathMin(20,MathMax(1,ReversalArrowsHistory));
int count=0;
for(int i=0;i<ArraySize(SignalMarks);i++)
if(SignalMarks[i].reversal &&
SignalMarks[i].up==up &&
SignalMarks[i].level==level)
count++;
if(count<=keepCount)
return;
// SignalMarks are appended chronologically; remove the oldest matching
// direction+LEVEL event, not the oldest event from another LEVEL.
for(int i=0;i<ArraySize(SignalMarks);i++)
{
if(!SignalMarks[i].reversal ||
SignalMarks[i].up!=up ||
SignalMarks[i].level!=level)
continue;
for(int j=i;j<ArraySize(SignalMarks)-1;j++)
SignalMarks[j]=SignalMarks[j+1];
ArrayResize(SignalMarks,ArraySize(SignalMarks)-1);
break;
}
}
void RecordReversalArrow(const double price,const bool buy,const int level)
{
// Стрелка фиксирует REVERSAL, но сама по себе не является ALERT.
// Уведомления отправляются только при пересечении строки с активной кнопкой ALERT.
AddReversalSignalMark(price,buy,level);
}
// VIEWPORT: масштаб и позиция окна отделены от состояния LEVEL. Новый тик не центрирует окно.
void UpdateCurrentLevelGrid(const double bid)
{
if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
return;
int grid=FloorGridForViewport(bid);
if(grid==INVALID_GRID)
return;
CurrentLevelGrid=grid;
CurrentLevelGridInitialized=true;
}
void UpdateSubwindowScale(const double bid)
{
if(SubWindow<0 || bid<=0.0 || TickLevelRowCount<=0)
return;
UpdateCurrentLevelGrid(bid);
int viewStart=GetViewStartIndex();
int viewCount=GetViewCount();
double visibleMin=bid;
double visibleMax=bid;
for(int i=0;i<viewCount;i++)
{
double price=GetTickPriceByVisibleIndex(viewStart+i);
if(price<=0.0)
continue;
visibleMin=MathMin(visibleMin,price);
visibleMax=MathMax(visibleMax,price);
}
if(InitialArrowHistoryScalePending)
{
for(int i=0;i<ArraySize(SignalMarks);i++)
{
if(!SignalMarks[i].reversal ||
SignalMarks[i].level!=GraphLevelsPerSide)
continue;
visibleMin=MathMin(visibleMin,SignalMarks[i].price);
visibleMax=MathMax(visibleMax,SignalMarks[i].price);
}
}
// Центр первоначального окна определяется историческим диапазоном,
// а не текущим BID. Последующие тики не меняют центр viewport.
if(!ScaleInitialized)
{
double historyCenter=(visibleMin+visibleMax)*0.5;
ViewportCenterPrice=NormalizeDouble(historyCenter,_Digits);
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
ScaleInitialized=true;
}
double minimumStep=GetGraphLevelStepPrice();
minimumStep=MathMax(minimumStep,_Point);
int graphLevel=GraphLevelsPerSide;
int graphPerSide=MathMax(1,GraphDisplayedLevelCount);
for(int side=-1;side<=1;side+=2)
{
double levelPrice=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,graphPerSide);
if(levelPrice<=0.0)
continue;
visibleMin=MathMin(visibleMin,levelPrice);
visibleMax=MathMax(visibleMax,levelPrice);
}
double viewportCenter=(ViewportCenterPrice>0.0 ?
ViewportCenterPrice : bid);
double autoHalfRange=MathMax(MathAbs(viewportCenter-visibleMin),
MathAbs(visibleMax-viewportCenter));
double minimumHalfRange=minimumStep*3.0;
autoHalfRange=MathMax(autoHalfRange,minimumHalfRange);
double autoPadding=MathMax(autoHalfRange*0.05,minimumStep*0.10);
autoHalfRange+=autoPadding;
// Автомасштаб разрешён только при первом построении (или явном RESET).
// Изменение BID и числа отображаемых уровней не меняет масштаб истории.
if(!VerticalScaleManual && !AutoScaleInitialized)
{
VerticalScaleHalfRange=autoHalfRange;
AutoScaleInitialized=true;
}
double scaleHalfRange=MathMax(VerticalScaleHalfRange,
minimumStep*0.20);
// Автосдвиг viewport включается только когда BID подходит к краю.
// Цена остаётся не в центре: вверх — в верхней трети окна,
// вниз — в нижней трети. Масштаб не меняется, двигается только окно.
if(!VerticalScaleManual && ScaleInitialized && AutoScaleInitialized &&
scaleHalfRange>minimumStep)
{
double bidOffset=bid-viewportCenter;
double followTrigger=scaleHalfRange*0.60;
double followTarget=scaleHalfRange*0.35;
if(bidOffset>followTrigger)
viewportCenter=bid-followTarget;
else if(bidOffset < -followTrigger)
viewportCenter=bid+followTarget;
viewportCenter=NormalizeDouble(viewportCenter,_Digits);
ViewportCenterPrice=viewportCenter;
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
}
GraphScaleMin=NormalizeDouble(viewportCenter-scaleHalfRange,_Digits);
GraphScaleMax=NormalizeDouble(viewportCenter+scaleHalfRange,_Digits);
if(GraphScaleMax<=GraphScaleMin)
{
GraphScaleMin=viewportCenter-minimumStep;
GraphScaleMax=viewportCenter+minimumStep;
}
// Viewport moves only after BID crosses the follow threshold.
ScaleMin=GraphScaleMin;
ScaleMax=GraphScaleMax;
IndicatorSetDouble(INDICATOR_MINIMUM,GraphScaleMin);
IndicatorSetDouble(INDICATOR_MAXIMUM,GraphScaleMax);
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,true);
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,true);
if(CanvasReady)
CanvasHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
}
int GetViewStartIndex()
{
if(TickCount<=0)
return 0;
int span=ViewSpan;
if(span<=0)
span=TickCount;
span=MathMax(1,MathMin(span,TickCount));
int maxOffset=MathMax(0,TickCount-span);
int offset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
return TickCount-span-offset;
}
int GetViewCount()
{
if(TickCount<=0)
return 0;
int span=ViewSpan;
if(span<=0)
span=TickCount;
return MathMax(1,MathMin(span,TickCount));
}
// ViewSpan настраивается при bootstrap или смене диапазона, а не при каждом рендере.
void FitInitialArrowHistory()
{
if(TickCount<=0)
return;
const int targetPerDirection=MathMin(20,MathMax(1,ReversalArrowsHistory));
long firstGraphSerial=MathMax(1,TickSerial-TickCount+1);
long oldestSerial=-1;
long newestSerial=-1;
for(int direction=0;direction<2;direction++)
{
long cutoff=9223372036854775807;
for(int n=0;n<targetPerDirection;n++)
{
long best=-1;
for(int k=0;k<ArraySize(SignalMarks);k++)
{
if(!SignalMarks[k].reversal ||
SignalMarks[k].level!=GraphLevelsPerSide)
continue;
bool wanted=(direction==1 ? SignalMarks[k].up : !SignalMarks[k].up);
if(!wanted)
continue;
long serial=SignalMarks[k].serial;
if(serial<firstGraphSerial || serial>TickSerial || serial>=cutoff)
continue;
if(serial>best)
best=serial;
}
if(best<0)
break;
oldestSerial=(oldestSerial<0 ? best : MathMin(oldestSerial,best));
newestSerial=(newestSerial<0 ? best : MathMax(newestSerial,best));
cutoff=best;
}
}
if(oldestSerial<0 || newestSerial<0)
{
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
ViewEndOffset=0;
InitialViewLayoutActive=true;
return;
}
long spanLong=newestSerial-oldestSerial+1;
int requiredSpan=(int)MathMin((long)TickCount,spanLong);
requiredSpan=MathMax(MinViewTicks,requiredSpan);
requiredSpan=MathMin(requiredSpan,TickCount);
ViewSpan=requiredSpan;
ViewEndOffset=0;
InitialViewLayoutActive=true;
}
// Validate the retained arrow limit independently for each LEVEL and direction.
void ValidateReversalArrowHistory()
{
if(ReversalArrowsHistory<=0)
return;
int keepCount=MathMin(20,MathMax(1,ReversalArrowsHistory));
for(int row=0;row<TickLevelRowCount;row++)
{
int level=TickLevelRows[row].level;
for(int direction=0;direction<2;direction++)
{
bool wantedUp=(direction==1);
int count=0;
for(int i=0;i<ArraySize(SignalMarks);i++)
if(SignalMarks[i].reversal &&
SignalMarks[i].up==wantedUp &&
SignalMarks[i].level==level)
count++;
if(count>keepCount)
Print("TPDR: reversal arrow history invariant violated. Level=",
level," Direction=",(wantedUp ? "UP" : "DOWN"),
" Count=",count," Limit=",keepCount);
}
}
}
int GraphPriceToY(const double price)
{
if(CanvasHeight<=1 || GraphScaleMax<=GraphScaleMin)
return CanvasHeight/2;
int top=0;
int bottom=MathMax(0,CanvasHeight-1);
int drawableHeight=MathMax(1,bottom-top);
double ratio=(GraphScaleMax-price)/(GraphScaleMax-GraphScaleMin);
ratio=MathMax(0.0,MathMin(1.0,ratio));
return top+(int)MathRound(ratio*(double)drawableHeight);
}
int PriceToY(const double price)
{
return GraphPriceToY(price);
}
bool IsGraphPriceVisible(const double price)
{
// Уровни больше не являются clipping-маской. Функция означает только
// принадлежность цены текущему AutoScale viewport.
return GraphScaleMax>GraphScaleMin &&
price>=GraphScaleMin &&
price<=GraphScaleMax;
}
void DrawDottedHorizontal(const int y,const uint clr)
{
if(!CanvasReady || CanvasWidth<=0)
return;
int graphWidth=GetGraphWidth();
for(int x=0;x<graphWidth;x+=8)
{
int x2=MathMin(x+4,graphWidth-1);
TickCanvas.Line(x,y,x2,y,clr);
}
}
void EnsureLevelPriceLabel(const string name,const int x,const int y,
const color clr,const string textValue)
{
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
{
Print("TPDR: level price label creation error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
}
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
}
double GetGraphLevelStepPrice()
{
return (GraphLevelsPerSide>=GraphLevelsPerSideMin ?
(double)GraphLevelsPerSide*_Point : 0.0);
}
double GetGraphLevelSidePrice(const double bid,const int graphLevel,const int side)
{
if(bid<=0.0 || graphLevel<=0 || (side!=1 && side!=-1))
return 0.0;
double step=(double)graphLevel*_Point;
if(step<=0.0)
return 0.0;
double normalizedBid=NormalizeDouble(bid,_Digits);
double position=(normalizedBid-NormalizeDouble(StartPrice,_Digits))/step;
int grid=(side>0 ? (int)MathCeil(position-1.0e-9) :
(int)MathFloor(position+1.0e-9));
double gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
// UP must be strictly above BID and DOWN strictly below BID after rounding.
while(side>0 && gridPrice<=normalizedBid)
{
grid++;
gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
}
while(side<0 && gridPrice>=normalizedBid)
{
grid--;
gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
}
return gridPrice;
}
double GetGraphLevelSidePriceByOffset(const double bid,const int graphLevel,
const int side,const int offset)
{
if(bid<=0.0 || graphLevel<=0 || (side!=1 && side!=-1) || offset<1)
return 0.0;
double step=(double)graphLevel*_Point;
if(step<=0.0)
return 0.0;
double normalizedBid=NormalizeDouble(bid,_Digits);
double position=(normalizedBid-NormalizeDouble(StartPrice,_Digits))/step;
int baseGrid=(side>0 ? (int)MathCeil(position-1.0e-9) :
(int)MathFloor(position+1.0e-9));
double basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
// Keep the nearest grid level visible when BID lands exactly on it.
// At equality the level belongs to the DOWN side; UP remains strictly above.
while(side>0 && basePrice<=normalizedBid)
{
baseGrid++;
basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
}
while(side<0 && basePrice>normalizedBid)
{
baseGrid--;
basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
}
int grid=baseGrid+(side>0 ? (offset-1) : -(offset-1));
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
}
int GetGraphRuntimeRowIndex(const int graphLevel)
{
if(graphLevel<=0)
return -1;
for(int i=0;i<TickLevelRowCount;i++)
{
if(TickLevelRows[i].level==graphLevel)
return i;
}
return -1;
}
void DrawRayLayer(const double bid)
{
if(!CanvasReady || !DrawRayObjects || GraphScaleMax<=GraphScaleMin || TickLevelRowCount<=0 || bid<=0.0)
return;
if(GetGraphLevelStepPrice()<=0.0)
return;
int perSide=MathMax(1,GraphDisplayedLevelCount);
int graphLevel=GraphLevelsPerSide;
for(int offset=1;offset<=perSide;offset++)
for(int side=-1;side<=1;side+=2)
{
double price=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,offset);
if(price>0.0 && IsGraphPriceVisible(price))
DrawDottedHorizontal(GraphPriceToY(price),ColorToARGB(RayColor,185));
}
// The full-width central BID line is intentionally removed.
}
int GetGraphWidth()
{
// Canvas занимает только левую область тикового графика.
// Правая область полностью резервируется под цены и не рисуется Canvas-ом.
return MathMax(1,CanvasWidth);
}
void EnsureLevelRowsControlButton(const string name,const string textValue)
{
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
{
Print("TPDR: LEVEL rows button creation error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_XSIZE,LevelButtonWidth);
ObjectSetInteger(0,name,OBJPROP_YSIZE,LevelButtonHeight);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,1100);
}
}
void UpdateLevelRowsControlsLayout()
{
ObjectDelete(0,Prefix+"GRAPH_ROWS_LABEL");
ObjectDelete(0,Prefix+"GRAPH_ROWS_MINUS");
ObjectDelete(0,Prefix+"GRAPH_ROWS_VALUE");
ObjectDelete(0,Prefix+"GRAPH_ROWS_PLUS");
}
string GetGraphDisplayedLevelCountKey()
{
if(GraphDisplayedLevelCountKey=="")
GraphDisplayedLevelCountKey="TPDR_GL_"+IntegerToString(ChartID())+"_"+_Symbol;
return GraphDisplayedLevelCountKey;
}
void SaveGraphDisplayedLevelCount()
{
GlobalVariableSet(GetGraphDisplayedLevelCountKey(),(double)GraphDisplayedLevelCount);
}
void LoadGraphDisplayedLevelCount()
{
string key=GetGraphDisplayedLevelCountKey();
if(GlobalVariableCheck(key))
GraphDisplayedLevelCount=(int)MathRound(GlobalVariableGet(key));
GraphDisplayedLevelCount=MathMax(GraphDisplayedLevelCountMin,
MathMin(GraphDisplayedLevelCountMax,
GraphDisplayedLevelCount));
SaveGraphDisplayedLevelCount();
}
void HandleLevelRowsButtonClick(const string name)
{
// GRAPHIC LEVELS controls belong to the Dashboard namespace (DashPrefix),
// not the TickGraph object namespace (Prefix).
if(name==DashPrefix+"GRAPH_ROWS_MINUS")
{
GraphDisplayedLevelCount=MathMax(GraphDisplayedLevelCountMin,GraphDisplayedLevelCount-1);
SaveGraphDisplayedLevelCount();
ResetGraphToCurrentPrice();
RefreshGraph();
return;
}
if(name==DashPrefix+"GRAPH_ROWS_PLUS")
{
GraphDisplayedLevelCount=MathMin(GraphDisplayedLevelCountMax,GraphDisplayedLevelCount+1);
SaveGraphDisplayedLevelCount();
ResetGraphToCurrentPrice();
RefreshGraph();
return;
}
}
void DrawGraphLevelLabels(const double bid)
{
if(!CanvasReady || TickLevelRowCount<=0 || bid<=0.0)
return;
ObjectDelete(0,Prefix+"GRAPH_TITLE");
EnsurePriceScaleArea();
UpdateLevelRowsControlsLayout();
int graphLevel=GraphLevelsPerSide;
int perSide=MathMax(1,GraphDisplayedLevelCount);
color priceLabelColor=clrSilver;
double step=(double)graphLevel*_Point;
if(step<=0.0)
return;
// Remove legacy names once. Current labels are keyed by absolute grid index,
// not UP/DOWN + offset, so crossing a grid does not recreate the same label.
if(!LegacyGraphLabelsCleaned)
{
ObjectsDeleteAll(0,Prefix+"GRAPH_LEVEL_",-1,-1);
LegacyGraphLabelsCleaned=true;
LastGraphLabelNameCount=0;
ArrayResize(LastGraphLabelNames,0);
}
string currentNames[];
ArrayResize(currentNames,perSide*2);
int currentNameCount=0;
for(int offset=1;offset<=perSide;offset++)
for(int side=-1;side<=1;side+=2)
{
double price=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,offset);
if(price<=0.0 || !IsGraphPriceVisible(price))
continue;
int grid=(int)MathRound((price-NormalizeDouble(StartPrice,_Digits))/step);
string name=Prefix+"GRAPH_LEVEL_"+IntegerToString(graphLevel)+"_"+
IntegerToString(grid);
// Keep one object identity for the same absolute LEVEL grid price.
EnsurePanelLabel(name,DoubleToString(price,_Digits));
int desiredY=GraphPriceToY(price)-7;
int labelTop=0;
int labelBottom=MathMax(labelTop,CanvasHeight-2);
desiredY=MathMax(labelTop,MathMin(labelBottom,desiredY));
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,CanvasWidth+8);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,desiredY);
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,8);
ObjectSetInteger(0,name,OBJPROP_COLOR,priceLabelColor);
ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
bool duplicate=false;
for(int j=0;j<currentNameCount;j++)
if(currentNames[j]==name)
{
duplicate=true;
break;
}
if(!duplicate && currentNameCount<ArraySize(currentNames))
currentNames[currentNameCount++]=name;
}
// Delete only labels that left the current visible set.
for(int i=0;i<LastGraphLabelNameCount;i++)
{
bool retained=false;
for(int j=0;j<currentNameCount;j++)
if(LastGraphLabelNames[i]==currentNames[j])
{
retained=true;
break;
}
if(!retained)
ObjectDelete(0,LastGraphLabelNames[i]);
}
ArrayResize(LastGraphLabelNames,currentNameCount);
for(int i=0;i<currentNameCount;i++)
LastGraphLabelNames[i]=currentNames[i];
LastGraphLabelNameCount=currentNameCount;
}
void DrawTickLayer(const double currentBid)
{
if(!CanvasReady || TickCount<=0 || currentBid<=0.0)
return;
int viewStart=GetViewStartIndex();
int viewCount=GetViewCount();
if(viewCount<=0)
return;
uint graphColor=ColorToARGB(TickGraphColor,255);
int graphWidth=GetGraphWidth();
int leftMargin=0;
int rightMargin=0;
int drawWidth=MathMax(1,graphWidth);
if(viewCount==1)
{
int y=GraphPriceToY(currentBid);
int x=leftMargin;
TickCanvas.LineThick(x,y,MathMin(graphWidth-1,x+4),y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
return;
}
bool havePrevious=false;
int previousX=0;
int previousY=0;
for(int i=0;i<viewCount;i++)
{
int x=leftMargin+(int)MathRound((double)i*(double)(drawWidth-1)/(double)(viewCount-1));
double price=GetTickPriceByVisibleIndex(viewStart+i);
// Последняя точка всегда является фактическим текущим Bid.
if(i==viewCount-1)
price=currentBid;
if(!IsGraphPriceVisible(price))
{
havePrevious=false;
continue;
}
int y=GraphPriceToY(price);
if(havePrevious)
TickCanvas.LineThick(previousX,previousY,x,y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
previousX=x;
previousY=y;
havePrevious=true;
}
}
void DrawArrowMark(const int x,const int y,const bool up,const bool reversal,const color clr)
{
if(!CanvasReady)
return;
uint c=ColorToARGB(clr,255);
int size=reversal ? 7 : 5;
// (x,y) — точка события, то есть ОСТРИЁ стрелки.
// Геометрия стрелки строится относительно этой точки и больше
// не добавляет скрытого смещения по цене.
if(up)
{
TickCanvas.FillTriangle(x,y,x-size,y+2*size,x+size,y+2*size,c);
}
else
{
TickCanvas.FillTriangle(x,y,x-size,y-2*size,x+size,y-2*size,c);
}
}
void DrawBidMarker(const double bid)
{
if(!CanvasReady || bid<=0.0)
return;
int y=GraphPriceToY(bid);
uint c=ColorToARGB(CurrentRayColor,255);
int graphWidth=GetGraphWidth();
int rightX=MathMax(0,graphWidth-1);
int markerWidth=MathMin(4,graphWidth);
int markerStart=MathMax(0,rightX-markerWidth+1);
TickCanvas.Line(markerStart,y,rightX,y,c);
TickCanvas.FillTriangle(MathMax(2,rightX-12),y-5,
MathMax(2,rightX-2),y,
MathMax(2,rightX-12),y+5,c);
}
void DrawCurrentTickMarker(const double currentBid)
{
if(!CanvasReady || currentBid<=0.0)
return;
int graphWidth=GetGraphWidth();
int x=MathMax(0,graphWidth-1);
int y=GraphPriceToY(currentBid);
TickCanvas.FillCircle(x,y,3,ColorToARGB(TickGraphColor,255));
}
void DrawSignalLayer(const double bid)
{
if(!CanvasReady || TickCount<=0 || ArraySize(SignalMarks)==0 || bid<=0.0)
return;
long firstSerial=TickSerial-TickCount+1;
int viewStart=GetViewStartIndex();
int viewCount=GetViewCount();
int viewEnd=viewStart+viewCount-1;
if(!DrawReversalArrows)
return;
// На графике отображаются именно REVERSAL, а не обычные CROSSING.
// Храним последние N UP + N DOWN отдельно для каждого LEVEL.
// Цена стрелки абсолютная и не пересчитывается от текущего BID.
for(int i=0;i<ArraySize(SignalMarks);i++)
{
if(!SignalMarks[i].reversal)
continue;
// Графический REVERSAL относится только к выбранному GRAPHIC LEVEL.
// Стрелки разворотов других dashboard LEVEL не являются событиями
// выбранного тикового графика и не должны отображаться на нём.
if(SignalMarks[i].level!=GraphLevelsPerSide)
continue;
long serial=SignalMarks[i].serial;
if(serial<firstSerial || serial>TickSerial)
continue;
int index=(int)(serial-firstSerial);
if(index<viewStart || index>viewEnd)
continue;
int relative=index-viewStart;
int drawWidth=MathMax(1,GetGraphWidth());
int x=(viewCount==1) ? 0 :
(int)MathRound((double)relative*(double)(drawWidth-1)/(double)(viewCount-1));
double signalPrice=SignalMarks[i].price;
if(!IsGraphPriceVisible(signalPrice))
continue;
int y=GraphPriceToY(signalPrice);
color clr=(SignalMarks[i].up ? UpCrossColor : DownCrossColor);
DrawArrowMark(x,y,SignalMarks[i].up,true,clr);
}
}
void DeleteLegacyPriceObjects()
{
if(LegacyPriceObjectsCleaned)
return;
// Однократная миграционная очистка. Старые объекты больше не создаются,
// поэтому удалять их на каждом тике не имеет смысла и создаёт лишнюю
// нагрузку на графический поток.
for(int i=0;i<200;i++)
ObjectDelete(0,Prefix+"PRICE_DYNAMIC_"+IntegerToString(i));
for(int i=0;i<50;i++)
{
ObjectDelete(0,Prefix+"DIST_UP_"+IntegerToString(i));
ObjectDelete(0,Prefix+"DIST_DOWN_"+IntegerToString(i));
}
ObjectDelete(0,Prefix+"PRICE_BID");
ObjectDelete(0,Prefix+"CURRENT_DIRECTION_ARROW");
LegacyPriceObjectsCleaned=true;
}
void DeleteLegacyStatePanel()
{
string names[16]=
{
Prefix+"STATE_TITLE",
Prefix+"STATE_LEVEL_LABEL",Prefix+"STATE_LEVEL_VALUE",
Prefix+"STATE_DIRECTION_LABEL",Prefix+"STATE_DIRECTION_VALUE",
Prefix+"STATE_REVERSAL_AT_LABEL",Prefix+"STATE_REVERSAL_AT_VALUE",
Prefix+"STATE_LAST_DOWN_2",Prefix+"STATE_LAST_DOWN_2_VALUE",
Prefix+"STATE_LAST_DOWN_1",Prefix+"STATE_LAST_DOWN_1_VALUE",
Prefix+"STATE_LAST_UP_2",Prefix+"STATE_LAST_UP_2_VALUE",
Prefix+"STATE_LAST_UP_1",Prefix+"STATE_LAST_UP_1_VALUE"
};
for(int i=0;i<ArraySize(names);i++)
if(ObjectFind(0,names[i])>=0)
ObjectDelete(0,names[i]);
}
bool EnsureCanvas()
{
if(SubWindow<0)
return false;
// A resized/reordered indicator window may receive a new subwindow index.
// Recreate the bitmap in the current window instead of leaving it attached
// to the old one.
if(CanvasReady && CanvasSubWindow!=SubWindow)
{
TickCanvas.Destroy();
CanvasReady=false;
CanvasWidth=0;
CanvasHeight=0;
CanvasSubWindow=-1;
}
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
int chartHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
// Правая часть под цены физически не входит в Canvas, поэтому
// тиковая линия не может накладываться на ценовые подписи.
int wantedWidth=MathMax(50,chartWidth-PriceScaleWidth);
if(wantedWidth<50 || chartHeight<20)
return false;
// Canvas всегда занимает ровно всё окно TickGraph по вертикали:
// верхняя граница = 0, нижняя граница = chartHeight-1.
// Мышь не должна иметь возможности физически сдвинуть график за эти границы.
if(CanvasReady)
{
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XDISTANCE,0);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YDISTANCE,0);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XSIZE,wantedWidth);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YSIZE,chartHeight);
}
if(CanvasReady && CanvasWidth==wantedWidth && CanvasHeight==chartHeight)
return true;
if(CanvasReady)
{
ResetLastError();
if(!TickCanvas.Resize(wantedWidth,chartHeight))
{
int resizeError=GetLastError();
Print("TPDR: canvas resize failed; recreating bitmap. Error=",resizeError);
TickCanvas.Destroy();
CanvasReady=false;
CanvasWidth=0;
CanvasHeight=0;
CanvasSubWindow=-1;
ObjectDelete(0,Prefix+"PANEL");
}
else
{
CanvasWidth=wantedWidth;
CanvasHeight=chartHeight;
return true;
}
}
ResetLastError();
if(!TickCanvas.CreateBitmapLabel(0,SubWindow,Prefix+"PANEL",0,0,
wantedWidth,chartHeight,
COLOR_FORMAT_ARGB_NORMALIZE))
{
Print("TPDR: canvas creation failed. Error=",GetLastError());
return false;
}
CanvasWidth=wantedWidth;
CanvasHeight=chartHeight;
CanvasSubWindow=SubWindow;
CanvasReady=true;
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XDISTANCE,0);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YDISTANCE,0);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XSIZE,wantedWidth);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YSIZE,chartHeight);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_BACK,true);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTED,false);
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_HIDDEN,true);
return true;
}
bool IsPointInsideObject(const string name,const int x,const int y)
{
if(ObjectFind(0,name)<0 || SubWindow<0)
return false;
int bx=(int)ObjectGetInteger(0,name,OBJPROP_XDISTANCE);
int by=(int)ObjectGetInteger(0,name,OBJPROP_YDISTANCE);
int bw=(int)ObjectGetInteger(0,name,OBJPROP_XSIZE);
int bh=(int)ObjectGetInteger(0,name,OBJPROP_YSIZE);
ENUM_BASE_CORNER corner=(ENUM_BASE_CORNER)ObjectGetInteger(
0,name,OBJPROP_CORNER);
// CHARTEVENT_MOUSE_MOVE передаёт Y относительно главного окна,
// графические объекты подокна — относительно самого подокна.
int subwindowTop=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
int localY=y-subwindowTop;
int localX=x;
int top=by;
int left=bx;
if(corner==CORNER_LEFT_LOWER)
top=CanvasHeight-by-bh;
else if(corner==CORNER_RIGHT_UPPER ||
corner==CORNER_RIGHT_LOWER)
left=CanvasWidth-bx-bw;
if(corner==CORNER_RIGHT_LOWER)
top=CanvasHeight-by-bh;
return localX>=left && localX<left+bw &&
localY>=top && localY<top+bh;
}
void EnsureResetButton(const string name,const string textValue)
{
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
{
Print("TPDR: reset button creation error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
ObjectSetInteger(0,name,OBJPROP_XSIZE,ResetButtonWidth);
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_STATE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
}
}
void EnsureZoomButton(const string name,const string textValue)
{
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
{
Print("TPDR: zoom button creation error=",GetLastError(),
" Name=",name);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
ObjectSetInteger(0,name,OBJPROP_XSIZE,ZoomButtonWidth);
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,12);
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_STATE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
}
}
void EnsurePanelLabel(const string name,const string textValue)
{
// GRAPH labels belong to the TickGraphic indicator subwindow.
// Recreate legacy labels that were created in the main chart window.
// ObjectFind() returns the subwindow index for an existing object.
// OBJPROP_WINDOW does not exist in MQL5 object properties.
if(ObjectFind(0,name)>=0 && ObjectFind(0,name)!=SubWindow)
ObjectDelete(0,name);
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
{
Print("TPDR: panel label creation error=",GetLastError(),
" Name=",name," SubWindow=",SubWindow);
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,500);
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
}
void EnsureZoomControlBackground(const int x,const int y,const int totalWidth)
{
string name=Prefix+"ZOOM_CONTROL_BG";
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
return;
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
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_ZORDER,80);
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
}
// Фон также принудительно привязываем к нижнему левому углу.
// Иначе старый объект мог остаться с прежним CORNER.
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_ZORDER,80);
int padding=5;
int panelX=MathMax(0,x-padding);
int panelY=MathMax(0,y-padding);
int panelWidth=MathMin(CanvasWidth-panelX,totalWidth+2*padding);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,panelX);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,panelY);
ObjectSetInteger(0,name,OBJPROP_XSIZE,MathMax(1,panelWidth));
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight+2*padding);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
ObjectSetInteger(0,name,OBJPROP_COLOR,clrDimGray);
}
void EnsureGraphHeaderBackground()
{
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
return;
string name=Prefix+"GRAPH_HEADER_BG";
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
{
Print("TPDR: graph header background creation error=",GetLastError());
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_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(0,name,OBJPROP_ZORDER,40);
}
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,0);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,0);
ObjectSetInteger(0,name,OBJPROP_XSIZE,CanvasWidth);
ObjectSetInteger(0,name,OBJPROP_YSIZE,
MathMin(GraphTopReserved,CanvasHeight));
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
ObjectSetInteger(0,name,OBJPROP_COLOR,background);
}
void UpdateZoomButtonsLayout()
{
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
return;
EnsureGraphHeaderBackground();
string minusName=Prefix+"ZOOM_MINUS";
string plusName=Prefix+"ZOOM_PLUS";
string resetName=Prefix+"RESET_CURRENT";
string labelName=Prefix+"ZOOM_LABEL";
// Старый отдельный фон больше не используется: кнопки входят
// непосредственно в общую таблицу LEVELS/Distance.
string oldBackground=Prefix+"ZOOM_CONTROL_BG";
if(ObjectFind(0,oldBackground)>=0)
ObjectDelete(0,oldBackground);
EnsureZoomButton(minusName,"-");
EnsureZoomButton(plusName,"+");
EnsureResetButton(resetName,"RESET");
EnsurePanelLabel(labelName,"ZOOM");
// Верхняя строка зарезервирована под название индикатора/графика.
// Кнопки управления опускаем ниже, чтобы они не перекрывали заголовок.
int y=34;
int controlsWidth=2*ZoomButtonWidth+ResetButtonWidth+2*ZoomButtonGap;
int x=MathMax(4,CanvasWidth-controlsWidth-8);
int x1=x;
int x2=x+ZoomButtonWidth+ZoomButtonGap;
int x3=x+2*(ZoomButtonWidth+ZoomButtonGap);
ObjectSetInteger(0,minusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,minusName,OBJPROP_XSIZE,ZoomButtonWidth);
ObjectSetInteger(0,minusName,OBJPROP_YSIZE,ZoomButtonHeight);
ObjectSetInteger(0,minusName,OBJPROP_BACK,false);
ObjectSetInteger(0,minusName,OBJPROP_ZORDER,1000);
ObjectSetInteger(0,minusName,OBJPROP_XDISTANCE,x1);
ObjectSetInteger(0,minusName,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,plusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,plusName,OBJPROP_XSIZE,ZoomButtonWidth);
ObjectSetInteger(0,plusName,OBJPROP_YSIZE,ZoomButtonHeight);
ObjectSetInteger(0,plusName,OBJPROP_BACK,false);
ObjectSetInteger(0,plusName,OBJPROP_ZORDER,1000);
ObjectSetInteger(0,plusName,OBJPROP_XDISTANCE,x2);
ObjectSetInteger(0,plusName,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,resetName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,resetName,OBJPROP_XSIZE,ResetButtonWidth);
ObjectSetInteger(0,resetName,OBJPROP_YSIZE,ZoomButtonHeight);
ObjectSetInteger(0,resetName,OBJPROP_BACK,false);
ObjectSetInteger(0,resetName,OBJPROP_ZORDER,1000);
ObjectSetInteger(0,resetName,OBJPROP_XDISTANCE,x3);
ObjectSetInteger(0,resetName,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,resetName,OBJPROP_STATE,ResetButtonHeld);
ObjectSetInteger(0,labelName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,labelName,OBJPROP_ZORDER,1000);
ObjectSetInteger(0,labelName,OBJPROP_XDISTANCE,MathMax(4,x-48));
ObjectSetInteger(0,labelName,OBJPROP_YDISTANCE,y+4);
}
void ResetGraphToCurrentPrice()
{
if(TickCount<=0)
return;
// RESET возвращает только начальный VIEW, стараясь показать последние
// REVERSAL-стрелки по правилам FitInitialArrowHistory() и текущий BID.
// История, LEVEL и runtime-модель RESET не изменяет.
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
ViewEndOffset=0;
InitialViewLayoutActive=true;
FitInitialArrowHistory();
VerticalScaleManual=false;
VerticalScaleDragActive=false;
VerticalPanDragActive=false;
VerticalScaleHalfRange=0.0;
AutoScaleInitialized=false;
InitialArrowHistoryScalePending=true;
MqlTick current;
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0 && GetViewportLevelStepPrice()>0.0)
{
double bid=NormalizeDouble(current.bid,_Digits);
ScaleCenterGrid=FloorGridForViewport(bid);
ViewportCenterPrice=ViewportRayPrice(ScaleCenterGrid);
ScaleInitialized=true;
UpdateCurrentLevelGrid(bid);
}
ResetButtonHeld=false;
}
void ApplyZoomButton(const int delta)
{
if(TickCount<=1 || GetGraphWidth()<=1)
return;
InitialViewLayoutActive=false;
HandleHorizontalZoom(GetGraphWidth()/2,delta);
RefreshGraph();
}
void HandleZoomButtonClick(const string name)
{
if(name==Prefix+"ZOOM_MINUS")
{
InitialViewLayoutActive=false;
ApplyZoomButton(-1);
return;
}
if(name==Prefix+"ZOOM_PLUS")
{
InitialViewLayoutActive=false;
ApplyZoomButton(1);
return;
}
if(name==Prefix+"RESET_CURRENT")
{
ResetGraphToCurrentPrice();
ResetButtonHeld=false;
RefreshGraph();
}
}
void EnsurePriceScaleArea()
{
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
return;
string name=Prefix+"PRICE_SCALE_AREA";
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
int priceAreaWidth=MathMax(0,chartWidth-CanvasWidth);
if(priceAreaWidth<=0)
{
if(ObjectFind(0,name)>=0)
ObjectSetInteger(0,name,OBJPROP_XSIZE,0);
return;
}
if(ObjectFind(0,name)<0)
{
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
{
Print("TPDR: price scale area creation error=",GetLastError());
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,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,1);
}
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,CanvasWidth);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,0);
ObjectSetInteger(0,name,OBJPROP_XSIZE,priceAreaWidth);
ObjectSetInteger(0,name,OBJPROP_YSIZE,CanvasHeight);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
ObjectSetInteger(0,name,OBJPROP_COLOR,background);
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
}
void RenderGraph(const double bid)
{
// Один источник текущего визуального LEVEL для всех render-слоёв.
UpdateCurrentLevelGrid(bid);
// До завершения bootstrap отображение запрещено. Это единый инвариант
// для OnTimer(), OnCalculate(), RefreshGraph() и обработчиков кнопок.
if(!BootstrapComplete)
return;
if(!EnsureCanvas())
return;
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
TickCanvas.Erase(ColorToARGB(background,255));
DrawRayLayer(bid);
DrawGraphLevelLabels(bid);
DrawTickLayer(bid);
DrawSignalLayer(bid);
DrawBidMarker(bid);
DrawCurrentTickMarker(bid);
TickCanvas.Update(true);
ObjectDelete(0,Prefix+"GRAPH_ROWS_LABEL");
ObjectDelete(0,Prefix+"GRAPH_ROWS_MINUS");
ObjectDelete(0,Prefix+"GRAPH_ROWS_VALUE");
ObjectDelete(0,Prefix+"GRAPH_ROWS_PLUS");
UpdateZoomButtonsLayout();
DeleteLegacyStatePanel();
}
void RefreshGraph()
{
if(SubWindow<0 || !BootstrapComplete)
return;
MqlTick current;
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
return;
// После wheel/drag обновляется горизонтальный viewport и перерисовывается
// график. Вертикальный диапазон остаётся фиксированным; его можно изменить
// вручную или явной командой RESET. Сам ViewSpan при этом НЕ изменяется.
if(!EnsureCanvas())
return;
UpdateSubwindowScale(current.bid);
RenderGraph(current.bid);
InitialArrowHistoryScalePending=false;
ChartRedraw(0);
}
void UpdateTickRowPhaseExtremes(const MqlTick &tick)
{
if(tick.bid<=0.0)
return;
for(int row=0;row<TickLevelRowCount;row++)
{
if(TickLevelRows[row].direction>0 && tick.ask>0.0)
{
if(TickLevelRows[row].phaseMaxAsk<=0.0 || tick.ask>TickLevelRows[row].phaseMaxAsk)
TickLevelRows[row].phaseMaxAsk=tick.ask;
}
else if(TickLevelRows[row].direction<0)
{
if(TickLevelRows[row].phaseMinBid<=0.0 || tick.bid<TickLevelRows[row].phaseMinBid)
TickLevelRows[row].phaseMinBid=tick.bid;
}
}
}
void ProcessBidTick(const MqlTick &tick,const bool emitSignals,
const bool drawGraph,const bool storeHistory)
{
if(tick.bid<=0.0)
return;
double bid=NormalizeDouble(tick.bid,_Digits);
if(storeHistory)
AppendModelTick(bid,tick.ask);
TickSerial++;
if(!HavePreviousBid)
{
PreviousBid=bid;
HavePreviousBid=true;
if(drawGraph)
AddTickToGraph(bid);
if(!ScaleInitialized)
{
ScaleCenterGrid=FloorGridForViewport(bid);
ScaleInitialized=true;
}
UpdateTickRowPhaseExtremes(tick);
return;
}
double oldBid=PreviousBid;
if(drawGraph)
AddTickToGraph(bid);
if(bid!=oldBid)
{
for(int row=0;row<TickLevelRowCount;row++)
ProcessTickRow(row,oldBid,bid,tick,emitSignals);
PreviousBid=bid;
}
UpdateTickRowPhaseExtremes(tick);
}
void UpdateCursorFromArray(const MqlTick &ticks[],const int copied,const int startIndex)
{
if(copied<=0)
return;
long finalMsc=ticks[copied-1].time_msc;
// CopyTicks() работает включительно: запрос от CursorMsc может вернуть
// уже обработанные тики с той же миллисекундой. Поэтому храним точное количество
// обработанных тиков внутри последней временной отметки.
if(finalMsc==CursorMsc)
{
int processedAtFinal=0;
for(int i=0;i<copied;i++)
{
if(ticks[i].time_msc==finalMsc && i>=startIndex)
processedAtFinal++;
}
if(processedAtFinal>0)
CursorSameMsc+=processedAtFinal;
}
else
{
CursorMsc=finalMsc;
int finalCount=0;
for(int i=copied-1;i>=0 && ticks[i].time_msc==finalMsc;i--)
finalCount++;
CursorSameMsc=finalCount;
}
}
bool BootstrapShouldLogError(const int error)
{
uint now=GetTickCount();
if(error!=BootstrapLastLoggedError ||
(uint)(now-BootstrapLastErrorLogTick)>=BootstrapErrorLogIntervalMs)
{
BootstrapLastLoggedError=error;
BootstrapLastErrorLogTick=now;
return true;
}
return false;
}
bool BootstrapHistory()
{
uint now=GetTickCount();
if(BootstrapRetryPending &&
(uint)(now-BootstrapLastAttemptTick)<BootstrapRetryDelayMs)
return false;
BootstrapRetryPending=false;
BootstrapLastAttemptTick=now;
int minimumTarget=MathMax(HistoryGraphTicks,HistoryBootstrapTicks);
minimumTarget=MathMax(minimumTarget,100);
int maximumTarget=MathMax(minimumTarget,MathMax(100,MaxModelHistoryTicks));
if(HistoryBootstrapTargetTicks<=0)
HistoryBootstrapTargetTicks=MathMin(minimumTarget,maximumTarget);
else
HistoryBootstrapTargetTicks=MathMax(minimumTarget,
MathMin(HistoryBootstrapTargetTicks,maximumTarget));
int targetTicks=HistoryBootstrapTargetTicks;
MqlTick ticks[];
ArrayResize(ticks,targetTicks);
int copied=-1;
int lastError=0;
for(int attempt=0;attempt<3;attempt++)
{
ResetLastError();
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,0,(uint)targetTicks);
lastError=GetLastError();
if(copied<0)
{
if(lastError==4004)
{
targetTicks=MathMax(100,targetTicks/2);
ArrayResize(ticks,targetTicks);
continue;
}
if(lastError==4401 || lastError==4403)
{
BootstrapAttempts++;
BootstrapRetryPending=true;
if(BootstrapShouldLogError(lastError))
Print("TPDR: tick history synchronization did not complete. Error=",
lastError," Attempts=",BootstrapAttempts,
" RetryDelayMs=",BootstrapRetryDelayMs);
return false;
}
BootstrapRetryPending=true;
if(BootstrapShouldLogError(lastError))
Print("TPDR: bootstrap CopyTicks failed. Error=",lastError,
" Attempts=",BootstrapAttempts+1);
BootstrapAttempts++;
return false;
}
break;
}
if(copied<=0)
{
BootstrapAttempts++;
BootstrapRetryPending=true;
return false;
}
int lastAttach=-1;
for(int i=0;i<copied;i++)
if(ticks[i].time_msc<=AttachMsc)
lastAttach=i;
if(lastAttach<0)
{
if(targetTicks<maximumTarget)
{
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
MathMax(targetTicks+1,targetTicks*2));
BootstrapRetryPending=true;
return false;
}
BootstrapAttempts++;
BootstrapRetryPending=true;
if(BootstrapShouldLogError(lastError))
Print("TPDR: no tick at or before attach time. AttachMsc=",AttachMsc,
" Requested=",targetTicks," Copied=",copied,
" Maximum=",maximumTarget);
return false;
}
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),MathMax(100,MaxModelHistoryTicks));
ModelTickCount=0;
ModelTickHead=0;
ClearDerivedState();
// HISTORY and LIVE use the same ProcessBidTick() state machine.
for(int i=0;i<=lastAttach;i++)
{
if(ticks[i].bid>0.0)
ProcessBidTick(ticks[i],false,true,true);
}
int attachCount=0;
for(int i=lastAttach;i>=0 && ticks[i].time_msc==AttachMsc;i--)
attachCount++;
CursorMsc=AttachMsc;
CursorSameMsc=attachCount;
FinalizeTickRowsFromHistory();
// Do not accept a partial bootstrap as the requested history volume.
// Increase the request and rebuild the model from the chronological tick stream.
// At the configured maximum, continue only with the available broker history.
if(copied<targetTicks && targetTicks<maximumTarget)
{
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
MathMax(targetTicks+1,targetTicks*2));
BootstrapComplete=false;
LiveMode=false;
BootstrapRetryPending=true;
Print("TPDR: tick history is partial. Requested=",targetTicks,
" Copied=",copied," Next=",HistoryBootstrapTargetTicks,
" Maximum=",maximumTarget," Error=",lastError);
return false;
}
// Large LEVEL values require deeper history to obtain real REVERSAL,
// LAST-DOWN and LAST-UP values. Expand the request and recalculate from the
// beginning; if the configured limit is reached, report missing history.
bool derivedStateComplete=HistoryDerivedStateComplete();
if(!derivedStateComplete && targetTicks<maximumTarget)
{
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
MathMax(targetTicks+1,targetTicks*2));
BootstrapComplete=false;
LiveMode=false;
BootstrapRetryPending=true;
Print("TPDR: expanding tick history for missing LAST values. Requested=",
targetTicks," Next=",HistoryBootstrapTargetTicks,
" Maximum=",maximumTarget);
return false;
}
if(!derivedStateComplete)
{
int incompleteRows=0;
for(int row=0;row<TickLevelRowCount;row++)
{
if(TickLevelRows[row].direction==0 ||
TickLevelRows[row].reversalAtPrice<=0.0 ||
TickLevelRows[row].lastDown1Price<=0.0 ||
TickLevelRows[row].lastDown2Price<=0.0 ||
TickLevelRows[row].lastUp1Price<=0.0 ||
TickLevelRows[row].lastUp2Price<=0.0)
incompleteRows++;
}
string oldestTickText=(copied>0 ?
TimeToString(ticks[0].time,TIME_DATE|TIME_SECONDS) : "UNKNOWN");
Print("TPDR WARNING: required reversal history incomplete. Requested=",targetTicks,
" Copied=",copied," ModelTicks=",ModelTickCount,
" OldestTick=",oldestTickText,
" MissingRows=",incompleteRows,
" MaxHistoryTicks=",maximumTarget,
". The broker may not provide older ticks, or required REVERSAL events may not exist in available history.");
}
BootstrapAttempts=0;
BootstrapRetryPending=false;
BootstrapLastLoggedError=0;
BootstrapComplete=true;
LiveMode=true;
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount));
ViewEndOffset=0;
// REVERSAL arrows are produced by the same chronological history pass.
FitInitialArrowHistory();
ValidateReversalArrowHistory();
InitialArrowHistoryScalePending=true;
return true;
}
bool ProcessLiveTicks()
{
MqlTick ticks[5000];
int requestCount=MathMin(MathMax(HistoryChunkSize,100),5000);
for(int chunk=0;chunk<20;chunk++)
{
int copied=-1;
int requestTried=requestCount;
int lastError=0;
for(int attempt=0;attempt<3;attempt++)
{
ResetLastError();
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,
(ulong)MathMax(1,CursorMsc),(uint)requestTried);
lastError=GetLastError();
if(copied>=0)
break;
if(lastError!=4004)
{
Print("TPDR: live CopyTicks failed. Requested=",requestTried,
" CursorMsc=",CursorMsc," Error=",lastError);
return false;
}
requestTried=MathMax(100,requestTried/2);
}
if(copied<0)
{
Print("TPDR: live CopyTicks retries exhausted. Requested=",requestTried,
" CursorMsc=",CursorMsc," Error=",lastError);
return false;
}
if(copied==0)
return true;
int start=0;
if(ticks[0].time_msc==CursorMsc)
start=MathMin(CursorSameMsc,copied);
int processed=copied-start;
for(int i=start;i<copied;i++)
ProcessBidTick(ticks[i],true,true,true);
UpdateCursorFromArray(ticks,copied,start);
if(copied<requestTried || processed==0)
return true;
}
return true;
}
// DASHBOARD отображает TickLevelRows[]; отдельной расчётной модели у GUI нет.
// Таблица: LEVEL, DIRECTION, REVERSAL AT, ALERT, GRAPHIC и LAST.
#define DASH_INVALID_GRID (-2147483647)
// Единый контракт LEVEL: каждая строка задаёт собственную ценовую сетку.
// Допускается любое целое значение LEVEL от 1 до DASH_VISUAL_LEVEL_MAX.
#define DASH_DEFAULT_VISUAL_ROWS "5,10,20,30,40,50"
#define DASH_PANEL_PREFIX "TPDRD_"
#define DASH_ROWS_STATE_PREFIX "TPDR_VISUAL_"
#define DASH_KEY_ESCAPE 27
input string InitialVisualRows=DASH_DEFAULT_VISUAL_ROWS; // Начальные Visual ROWS при первом запуске; рабочие ROWS сохраняются отдельно
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 DashVisualRow
{
int level;
};
// TickLevelRows[] уже объявлен как динамический массив выше и является единым runtime-состоянием.
DashVisualRow DashVisualRows[DASH_DASHBOARD_ROWS];
int DashVisualRowCount=0;
bool DashAlertEnabled[DASH_DASHBOARD_ROWS];
ulong DashAlertActivityUntil[DASH_DASHBOARD_ROWS];
string DashPrefix=DASH_PANEL_PREFIX;
string DashShortName="CodeLock Dashboard";
string DashRowsStateKey="";
bool DashVisualRowsLoaded=false;
int DashColLevelX=12;
int DashColStatusX=0;
int DashColTriggerX=0;
int DashColAlertX=0;
int DashColGraphicX=0;
int DashAutoPanelWidth=0;
int DashLevelButtonWidth=72;
int DashRowsButtonWidth=27;
int DashRowsValueWidth=48;
int DashEditingRow=-1;
int DashMeasureTextWidth(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 DashCalculateAutoLayout(const double bid)
{
int levelWidth=DashMeasureTextWidth("LEVEL");
int statusWidth=DashMeasureTextWidth("DIRECTION");
int triggerWidth=DashMeasureTextWidth("REVERSAL AT");
int alertWidth=DashMeasureTextWidth("ALERT")+16;
int graphicWidth=DashMeasureTextWidth("GRAPHIC")+16;
levelWidth=MathMax(levelWidth,DashMeasureTextWidth("100000 pt"));
int gap=5;
DashColLevelX=12;
DashLevelButtonWidth=MathMax(66,levelWidth+12);
DashColStatusX=DashColLevelX+DashLevelButtonWidth+gap;
DashColTriggerX=DashColStatusX+statusWidth+gap;
DashColAlertX=DashColTriggerX+triggerWidth+gap;
DashColGraphicX=DashColAlertX+alertWidth+gap;
DashAutoPanelWidth=DashColGraphicX+graphicWidth+24;
}
int DashGetLevelCount()
{
return DashVisualRowCount;
}
int DashGetLevel(const int index)
{
if(index<0 || index>=DashVisualRowCount) return 0;
return DashVisualRows[index].level;
}
bool DashIsValidVisualLevel(const int level)
{
// LEVEL задаётся пользователем целым числом points без обязательного шага 10.
return level>=1 && level<=DASH_VISUAL_LEVEL_MAX;
}
bool DashIsStrictVisualRows()
{
if(DashVisualRowCount<DASH_MIN_VISUAL_ROWS || DashVisualRowCount>DASH_DASHBOARD_ROWS) return false;
for(int i=0;i<DashVisualRowCount;i++)
{
if(!DashIsValidVisualLevel(DashVisualRows[i].level)) return false;
if(i>0 && DashVisualRows[i].level<=DashVisualRows[i-1].level) return false;
}
return true;
}
bool DashParseVisualRows(const string source)
{
string parts[];
int count=StringSplit(source,',',parts);
if(count<DASH_MIN_VISUAL_ROWS || count>DASH_DASHBOARD_ROWS) return false;
DashVisualRowCount=0;
for(int i=0;i<count;i++)
{
string token=parts[i];
if(token=="") return false;
for(int k=0;k<StringLen(token);k++)
{
ushort ch=StringGetCharacter(token,k);
if(ch<'0' || ch>'9') return false;
}
int level=(int)StringToInteger(token);
if(!DashIsValidVisualLevel(level)) return false;
if(i>0 && level<=DashVisualRows[i-1].level) return false;
DashVisualRows[DashVisualRowCount].level=level;
DashVisualRowCount++;
}
return DashIsStrictVisualRows();
}
string DashVisualRowsKeyBase()
{
// Visual ROWS belong to the chart, not to its timeframe. A timeframe
// change reinitializes the indicator, so the persisted key must not contain
// _Period; otherwise the dashboard silently returns to InitialVisualRows.
if(DashRowsStateKey=="")
DashRowsStateKey=DASH_ROWS_STATE_PREFIX+
IntegerToString(ChartID())+"_"+_Symbol;
return DashRowsStateKey;
}
string DashLegacyVisualRowsKeyBase()
{
return DASH_ROWS_STATE_PREFIX+
IntegerToString(ChartID())+"_"+_Symbol+
"_TF"+IntegerToString((int)_Period);
}
bool DashLoadVisualRowsFromKey(const string base)
{
if(!GlobalVariableCheck(base+"_COUNT"))
return false;
int count=(int)MathRound(GlobalVariableGet(base+"_COUNT"));
if(count<1 || count>DASH_DASHBOARD_ROWS)
return false;
DashVisualRowCount=0;
for(int i=0;i<count;i++)
{
string key=base+"_R"+IntegerToString(i);
if(!GlobalVariableCheck(key))
return false;
int level=(int)MathRound(GlobalVariableGet(key));
if(!DashIsValidVisualLevel(level))
return false;
if(i>0 && level<=DashVisualRows[i-1].level)
return false;
DashVisualRows[i].level=level;
DashVisualRowCount++;
}
return DashIsStrictVisualRows();
}
void DashSaveVisualRows()
{
string base=DashVisualRowsKeyBase();
GlobalVariableSet(base+"_COUNT",(double)DashVisualRowCount);
for(int i=0;i<DashVisualRowCount;i++)
GlobalVariableSet(base+"_R"+IntegerToString(i),(double)DashVisualRows[i].level);
for(int i=DashVisualRowCount;i<DASH_DASHBOARD_ROWS;i++)
GlobalVariableDel(base+"_R"+IntegerToString(i));
}
bool DashLoadPersistedVisualRows()
{
string base=DashVisualRowsKeyBase();
// New storage is stable across timeframe changes.
if(DashLoadVisualRowsFromKey(base))
return true;
// One-time migration from the previous timeframe-specific storage.
// This prevents an already configured dashboard from losing its ROWS
// immediately after updating the indicator.
string legacyBase=DashLegacyVisualRowsKeyBase();
if(legacyBase==base)
return false;
if(!DashLoadVisualRowsFromKey(legacyBase))
return false;
DashSaveVisualRows();
return true;
}
void DashLoadVisualRows()
{
DashVisualRowsLoaded=false;
if(!DashLoadPersistedVisualRows())
{
if(!DashParseVisualRows(InitialVisualRows))
{
if(!DashParseVisualRows(DASH_DEFAULT_VISUAL_ROWS)) return;
Print("TPDR Dashboard: InitialVisualRows invalid; using default Visual ROWS.");
}
}
// Preserve the user's exact LEVEL list; do not force-add or rewrite a 5 pt row.
DashVisualRowsLoaded=true;
}
int DashFloorGrid(const double price,const double stepPrice)
{
if(stepPrice<=0.0)
return 0;
return (int)MathFloor((price-StartPrice)/stepPrice);
}
int DashCeilGrid(const double price,const double stepPrice)
{
if(stepPrice<=0.0)
return 0;
return (int)MathCeil((price-StartPrice)/stepPrice);
}
double DashGridPrice(const int stateIndex,const int grid)
{
return TickRowPrice(stateIndex,grid);
}
void DashFireLevelAlert(const int stateIndex,const int grid,const int direction)
{
if(stateIndex<0 || stateIndex>=DashVisualRowCount)
return;
double alertPrice=DashGridPrice(stateIndex,grid);
string message=StringFormat("%s: ALERT %s LEVEL=%d GRID=%d REVERSAL PRICE=%s",
_Symbol,
(direction>0 ? "UP" : "DOWN"),
TickLevelRows[stateIndex].level,
grid,
DoubleToString(alertPrice,_Digits));
DashAlertEnabled[stateIndex]=false;
DashAlertActivityUntil[stateIndex]=(ulong)GetTickCount()+(ulong)MathMax(0,AlertActivityMs);
DashSetAlertButtonState(stateIndex);
ChartRedraw(0);
// NotifyEvent() sends alerts; this function updates only button state and display.
}
bool DashIsAlertButtonName(const string name,int &stateIndex)
{
string prefix=DashPrefix+"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>=DASH_DASHBOARD_ROWS)
return false;
stateIndex=index;
return true;
}
void DashSetAlertButtonState(const int index)
{
if(index<0 || index>=DASH_DASHBOARD_ROWS)
return;
string name=DashPrefix+"ALERT_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
return;
bool enabled=DashAlertEnabled[index];
ulong now=(ulong)GetTickCount();
bool activity=(DashAlertActivityUntil[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 DashUpdateAlertButton(const int index,const int x,const int y,const int width,const int height)
{
string name=DashPrefix+"ALERT_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,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);
DashSetAlertButtonState(index);
}
void DashUpdateGraphicButton(const int index,const int x,const int y,const int width,const int height)
{
string name=DashPrefix+"GRAPHIC_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
{
Print("TPDR Dashboard: graphic 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");
}
bool selected=(index<DashVisualRowCount &&
DashVisualRows[index].level==GraphLevelsPerSide);
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);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,
selected ? HeaderColor : C'42,50,64');
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,
selected ? HeaderColor : BorderColor);
ObjectSetInteger(0,name,OBJPROP_COLOR,TextColor);
ObjectSetString(0,name,OBJPROP_TEXT,"GRAPHIC");
ObjectSetInteger(0,name,OBJPROP_STATE,selected);
}
void DashSelectGraphicLevel(const int index)
{
if(index<0 || index>=DashVisualRowCount)
return;
int selectedLevel=DashVisualRows[index].level;
if(selectedLevel<GraphLevelsPerSideMin)
return;
GraphLevelsPerSide=MathMax(GraphLevelsPerSideMin,
MathMin(GraphLevelsPerSideMax,
(selectedLevel/10)*10));
SyncTickVisualRowsFromDashboard();
ResetGraphToCurrentPrice();
RefreshGraph();
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
}
bool DashIsGraphicButtonName(const string name,int &stateIndex)
{
string prefix=DashPrefix+"GRAPHIC_";
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>=DASH_DASHBOARD_ROWS)
return false;
stateIndex=index;
return true;
}
void DashToggleAlert(const int index)
{
if(index<0 || index>=DASH_DASHBOARD_ROWS)
return;
// Каждый клик заново устанавливает/снимает режим ожидания.
// После повторной активации следующий подходящий crossing снова обязан вызвать ALERT.
DashAlertEnabled[index]=!DashAlertEnabled[index];
if(index<TickLevelRowCount)
TickLevelRows[index].alertEnabled=DashAlertEnabled[index];
DashAlertActivityUntil[index]=0;
string stateText=(DashAlertEnabled[index] ? "включен" : "выключен");
Print("TPDR Dashboard: ALERT ",TickLevelRows[index].level," pt ",stateText);
DashSetAlertButtonState(index);
ChartRedraw(0);
}
void DashDeleteDashboardObjects()
{
ObjectsDeleteAll(0,DashPrefix,-1,-1);
}
void DashEnsureRect(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,SubWindow,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 DashEnsureLabel(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,SubWindow,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 DashDeleteHiddenDashboardRows(const int visibleCount)
{
for(int i=visibleCount;i<DASH_DASHBOARD_ROWS;i++)
{
ObjectDelete(0,DashPrefix+"ROW_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"STATEBAR_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"MINUS_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"LEVEL_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"STATE_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"TRIGGER_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"ALERT_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"GRAPHIC_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"LAST_DOWN_2_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"LAST_DOWN_1_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"LAST_UP_2_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"LAST_UP_1_"+IntegerToString(i));
}
}
void DashEnsureButton(const string name,const int x,const int y,const int width,const int height,
const string textValue,const color background,const color border,
const color textColor,const long zorder=150)
{
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
{
Print("TPDR Dashboard: 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,zorder);
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);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,border);
ObjectSetInteger(0,name,OBJPROP_COLOR,textColor);
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
}
void DashBeginLevelEdit(const int index,const int x,const int y,const int width,const int height)
{
if(index<0 || index>=DashVisualRowCount) return;
if(DashEditingRow>=0 && DashEditingRow!=index)
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(DashEditingRow));
string name=DashPrefix+"EDIT_"+IntegerToString(index);
if(ObjectFind(0,name)<0)
{
ResetLastError();
if(!ObjectCreate(0,name,OBJ_EDIT,SubWindow,0,0))
{
Print("TPDR Dashboard: level edit creation failed. Error=",GetLastError());
return;
}
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,180);
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
ObjectSetInteger(0,name,OBJPROP_ALIGN,ALIGN_CENTER);
ObjectSetInteger(0,name,OBJPROP_READONLY,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_FONTSIZE,DashboardFontSize);
ObjectSetInteger(0,name,OBJPROP_COLOR,TextColor);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,PanelBackground);
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,HeaderColor);
ObjectSetString(0,name,OBJPROP_TEXT,IntegerToString(DashVisualRows[index].level));
DashEditingRow=index;
ChartRedraw(0);
}
void DashCancelLevelEdit()
{
if(DashEditingRow<0) return;
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(DashEditingRow));
DashEditingRow=-1;
ChartRedraw(0);
}
bool DashValidateEditedLevel(const int index,const int newLevel)
{
if(index<0 || index>=DashVisualRowCount || !DashIsValidVisualLevel(newLevel)) return false;
if(index>0 && newLevel<=DashVisualRows[index-1].level) return false;
if(index+1<DashVisualRowCount && newLevel>=DashVisualRows[index+1].level) return false;
return true;
}
void DashCommitEditedLevel(const int index,const string text)
{
if(index<0 || index>=DashVisualRowCount) { DashCancelLevelEdit(); return; }
if(text=="") { Print("TPDR Dashboard: LEVEL edit rejected: empty value."); DashCancelLevelEdit(); return; }
for(int k=0;k<StringLen(text);k++)
{
ushort ch=StringGetCharacter(text,k);
if(ch<'0' || ch>'9')
{
Print("TPDR Dashboard: LEVEL edit rejected: enter an integer from 1 to 2000 pt.");
DashCancelLevelEdit();
return;
}
}
int newLevel=(int)StringToInteger(text);
if(!DashValidateEditedLevel(index,newLevel))
{
Print("TPDR Dashboard: LEVEL edit rejected: ",text,
". Allowed: integer 1..2000 pt; levels must remain strictly increasing.");
DashCancelLevelEdit();
return;
}
DashVisualRows[index].level=newLevel;
DashSaveVisualRows();
DashCancelLevelEdit();
SyncTickVisualRowsFromDashboard();
Print("TPDR Dashboard: Visual ROW ",index," LEVEL changed to ",newLevel," pt.");
}
bool DashAddVisualRow(const int index)
{
if(index<0 || index>=DashVisualRowCount || DashVisualRowCount>=DASH_DASHBOARD_ROWS) return false;
int current=DashVisualRows[index].level;
int upper=(index+1<DashVisualRowCount ? DashVisualRows[index+1].level :
DASH_VISUAL_LEVEL_MAX+1);
// Кнопка + строка добавляет следующий уровень с фиксированным шагом 50 pt.
// Базовая сетка остаётся 10/20/30/40/50; первый добавленный уровень = 100.
const int addStep=50;
int candidate=current+addStep;
while(candidate<upper && candidate<=DASH_VISUAL_LEVEL_MAX && candidate>current)
{
bool free=true;
for(int i=0;i<DashVisualRowCount;i++)
if(DashVisualRows[i].level==candidate) { free=false; break; }
if(free)
{
for(int i=DashVisualRowCount;i>index+1;i--)
DashVisualRows[i]=DashVisualRows[i-1];
DashVisualRows[index+1].level=candidate;
DashVisualRowCount++;
DashSaveVisualRows();
SyncTickVisualRowsFromDashboard();
Print("TPDR Dashboard: added Visual ROW ",candidate," pt after ",current," pt.");
return true;
}
candidate+=addStep;
}
Print("TPDR Dashboard: cannot add ROW after ",current,
" pt: no free valid level preserves strict order.");
return false;
}
bool DashDeleteVisualRow(const int index)
{
if(index<0 || index>=DashVisualRowCount) return false;
if(DashVisualRows[index].level==5)
{
Print("TPDR Dashboard: mandatory ROW 5 pt cannot be deleted.");
return false;
}
if(DashVisualRowCount<=DASH_MIN_VISUAL_ROWS)
{
Print("TPDR Dashboard: at least five Visual ROWS must remain.");
return false;
}
int removed=DashVisualRows[index].level;
for(int i=index;i<DashVisualRowCount-1;i++)
DashVisualRows[i]=DashVisualRows[i+1];
DashVisualRowCount--;
DashSaveVisualRows();
SyncTickVisualRowsFromDashboard();
Print("TPDR Dashboard: deleted Visual ROW ",removed," pt.");
return true;
}
// --- Dashboard visual status helpers: presentation only.
color DashStatusColor(const int direction)
{
if(direction>0) return UpColor;
if(direction<0) return DownColor;
return WaitColor;
}
string DashStatusText(const int direction)
{
if(direction>0) return "UP";
if(direction<0) return "DOWN";
return "WAIT";
}
void DashRenderDashboard(const double bid)
{
// During a rebuild keep the last complete dashboard snapshot visible.
// Before the first completed bootstrap, never publish placeholder model values.
if(!BootstrapComplete && DashboardHasPublishedState)
return;
bool modelReady=BootstrapComplete;
int levelCount=DashVisualRowCount;
DashCalculateAutoLayout(bid);
ObjectDelete(0,DashPrefix+"TITLE");
int panelHeight=HeaderHeight+8+levelCount*RowHeight+12;
int panelX=MathMax(4,PanelLeftMargin);
int panelY=MathMax(4,PanelTopMargin);
DashEnsureRect(DashPrefix+"BG",panelX,panelY,DashAutoPanelWidth,panelHeight,PanelBackground,BorderColor);
DashEnsureRect(DashPrefix+"HEADER",panelX+2,panelY+2,DashAutoPanelWidth-4,HeaderHeight-4,PanelBackground,PanelBackground);
int dashboardTitleWidth=DashMeasureTextWidth("DASHBOARD");
int dashboardTitleX=panelX+(DashAutoPanelWidth-dashboardTitleWidth)/2;
DashEnsureLabel(DashPrefix+"DASHBOARD_TITLE",dashboardTitleX,panelY+6,
DashboardFontSize,TextColor,"DASHBOARD");
// ROWS +/- стоят у левого края панели; GRAPHIC LEVELS +/- — у правого.
int controlsY=panelY+34;
int levelHeaderY=controlsY;
int levelButtonWidth=24;
int levelValueWidth=36;
int levelLabelWidth=DashMeasureTextWidth("GRAPHIC LEVELS");
int levelControlsWidth=levelLabelWidth+6+levelButtonWidth+4+levelValueWidth+4+levelButtonWidth;
int levelHeaderX=panelX+DashAutoPanelWidth-levelControlsWidth-2;
int levelMinusX=levelHeaderX+levelLabelWidth+6;
int levelValueX=levelMinusX+levelButtonWidth+4;
int levelPlusX=levelValueX+levelValueWidth+4;
DashEnsureLabel(DashPrefix+"GRAPH_ROWS_LABEL",levelHeaderX,levelHeaderY,
DashboardFontSize,MutedTextColor,"GRAPHIC LEVELS");
DashEnsureButton(DashPrefix+"GRAPH_ROWS_MINUS",levelMinusX,levelHeaderY-3,
levelButtonWidth,22,"-",C'42,50,64',BorderColor,TextColor);
DashEnsureLabel(DashPrefix+"GRAPH_ROWS_VALUE",levelValueX+2,levelHeaderY,
DashboardFontSize,TextColor,IntegerToString(GraphDisplayedLevelCount));
DashEnsureButton(DashPrefix+"GRAPH_ROWS_PLUS",levelPlusX,levelHeaderY-3,
levelButtonWidth,22,"+",C'42,50,64',BorderColor,TextColor);
int bidWidth=DashMeasureTextWidth("BID "+DoubleToString(bid,_Digits));
DashEnsureLabel(DashPrefix+"BID",panelX+DashAutoPanelWidth-bidWidth-16,panelY+8,DashboardFontSize,HeaderColor,
"BID "+DoubleToString(bid,_Digits));
// Удаляем старые построчные +/- объекты, включая объекты от предыдущей версии.
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
{
ObjectDelete(0,DashPrefix+"MINUS_"+IntegerToString(i));
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
}
int headerY=panelY+HeaderHeight-20;
int rowsControlsX=panelX+2;
int rowsControlsY=controlsY;
DashEnsureLabel(DashPrefix+"ROWS_LABEL",rowsControlsX,rowsControlsY,
DashboardFontSize,MutedTextColor,"ROWS");
int rowsX=rowsControlsX+DashMeasureTextWidth("ROWS")+6;
DashEnsureButton(DashPrefix+"ROWS_MINUS",rowsX,rowsControlsY-3,
DashRowsButtonWidth,22,"-",
C'42,50,64',BorderColor,TextColor);
DashEnsureLabel(DashPrefix+"ROWS_VALUE",
rowsX+DashRowsButtonWidth+4,rowsControlsY,
DashboardFontSize,TextColor,IntegerToString(levelCount));
DashEnsureButton(DashPrefix+"ROWS_PLUS",
rowsX+DashRowsButtonWidth+DashRowsValueWidth+8,
rowsControlsY-3,DashRowsButtonWidth,22,"+",
C'42,50,64',BorderColor,TextColor);
DashEnsureLabel(DashPrefix+"COL_LEVEL",panelX+DashColLevelX,headerY,
DashboardFontSize,MutedTextColor,"LEVEL");
DashEnsureLabel(DashPrefix+"COL_STATUS",panelX+DashColStatusX,headerY,DashboardFontSize,MutedTextColor,"DIRECTION");
DashEnsureLabel(DashPrefix+"COL_TRIGGER",panelX+DashColTriggerX,headerY,DashboardFontSize,MutedTextColor,"REVERSAL AT");
DashEnsureLabel(DashPrefix+"COL_ALERT",panelX+DashColAlertX,headerY,DashboardFontSize,MutedTextColor,"ALERT");
DashEnsureLabel(DashPrefix+"COL_GRAPHIC",panelX+DashColGraphicX,headerY,DashboardFontSize,MutedTextColor,"GRAPHIC");
for(int row=0;row<levelCount;row++)
{
int index=levelCount-1-row;
int y=panelY+HeaderHeight+8+row*RowHeight;
string suffix=IntegerToString(index);
int direction=(modelReady ? TickLevelRows[index].direction : 0);
DashEnsureRect(DashPrefix+"ROW_"+suffix,panelX+2,y,DashAutoPanelWidth-4,RowHeight-1,
(row%2==0 ? RowBackground : RowBackgroundAlt),
(row==0 || row==levelCount-1 ? BorderColor :
(row%5==0 ? BorderColor : RowBackgroundAlt)));
DashEnsureRect(DashPrefix+"STATEBAR_"+suffix,panelX+3,y+2,5,RowHeight-5,
DashStatusColor(direction),DashStatusColor(direction));
if(DashEditingRow!=index)
DashEnsureButton(DashPrefix+"LEVEL_"+suffix,panelX+DashColLevelX,y+1,DashLevelButtonWidth,RowHeight-3,
IntegerToString(DashVisualRows[index].level)+" pt",RowBackground,BorderColor,
DashStatusColor(direction));
else
ObjectDelete(0,DashPrefix+"LEVEL_"+suffix);
DashEnsureLabel(DashPrefix+"STATE_"+suffix,panelX+DashColStatusX,y+3,DashboardFontSize,
DashStatusColor(direction),(modelReady ? DashStatusText(direction) : "LOADING"));
string reversalAtText=(modelReady && TickLevelRows[index].direction!=0 &&
TickLevelRows[index].reversalAtPrice>0.0 ?
DoubleToString(TickLevelRows[index].reversalAtPrice,_Digits) : "");
// REVERSAL AT показывает цену разворота и окрашивается цветом
// будущего направления: UP -> DownColor, DOWN -> UpColor.
int reversalDirection=(direction!=0 ? -direction : 0);
color reversalAtColor=DashStatusColor(reversalDirection);
DashEnsureLabel(DashPrefix+"TRIGGER_"+suffix,panelX+DashColTriggerX,y+3,DashboardFontSize,
reversalAtColor,reversalAtText);
int alertButtonWidth=DashMeasureTextWidth("ALERT")+16;
DashUpdateAlertButton(index,panelX+DashColAlertX,y+1,alertButtonWidth,RowHeight-3);
int graphicButtonWidth=DashMeasureTextWidth("GRAPHIC")+16;
DashUpdateGraphicButton(index,panelX+DashColGraphicX,y+1,graphicButtonWidth,RowHeight-3);
// LAST-DOWN/LAST-UP columns are intentionally not displayed.
}
DashDeleteHiddenDashboardRows(levelCount);
ChartRedraw(0);
if(modelReady)
DashboardHasPublishedState=true;
}
int DashInit()
{
if(StartPrice<=0.0 || BufferPoints<0 || HistoryBootstrapTicks<100 || HistoryChunkSize<100 ||
UpdateIntervalMs<10 || UpdateIntervalMs>2000 ||
RowHeight<20 || HeaderHeight<45 || DashboardFontSize<7 ||
DashboardFontSize>18 || PanelLeftMargin<0 || PanelTopMargin<0 ||
AlertActivityMs<0 || AlertActivityMs>10000)
return INIT_PARAMETERS_INCORRECT;
IndicatorSetString(INDICATOR_SHORTNAME,DashShortName);
MqlTick attachTick;
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
return INIT_FAILED;
DashLoadVisualRows();
if(!DashVisualRowsLoaded || !DashIsStrictVisualRows())
return INIT_PARAMETERS_INCORRECT;
// Remove dashboard objects from older versions before recreating them
// in the indicator subwindow instead of the main chart.
DashDeleteDashboardObjects();
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
{
DashAlertEnabled[i]=false;
DashAlertActivityUntil[i]=0;
}
return INIT_SUCCEEDED;
}
void DashChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id==CHARTEVENT_KEYDOWN && DashEditingRow>=0 && lparam==DASH_KEY_ESCAPE)
{
DashCancelLevelEdit();
return;
}
if(id==CHARTEVENT_OBJECT_ENDEDIT)
{
string editPrefix=DashPrefix+"EDIT_";
if(StringFind(sparam,editPrefix)==0)
{
int index=(int)StringToInteger(StringSubstr(sparam,StringLen(editPrefix)));
string textValue="";
if(ObjectGetString(0,sparam,OBJPROP_TEXT,0,textValue))
DashCommitEditedLevel(index,textValue);
else
DashCancelLevelEdit();
return;
}
}
if(id!=CHARTEVENT_OBJECT_CLICK)
return;
if(sparam==DashPrefix+"GRAPH_ROWS_MINUS" ||
sparam==DashPrefix+"GRAPH_ROWS_PLUS")
{
HandleLevelRowsButtonClick(sparam);
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
return;
}
if(sparam==DashPrefix+"ROWS_MINUS")
{
// Удаляется только самый высокий добавленный LEVEL.
if(DashVisualRowCount>DASH_MIN_VISUAL_ROWS &&
DashDeleteVisualRow(DashVisualRowCount-1))
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
return;
}
if(sparam==DashPrefix+"ROWS_PLUS")
{
// Новый LEVEL добавляется только сверху с шагом 50 pt: 50 -> 100 -> 150 -> ...
if(DashVisualRowCount<DASH_DASHBOARD_ROWS &&
DashAddVisualRow(DashVisualRowCount-1))
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
return;
}
string levelPrefix=DashPrefix+"LEVEL_";
if(StringFind(sparam,levelPrefix)==0)
{
int index=(int)StringToInteger(StringSubstr(sparam,StringLen(levelPrefix)));
if(index>=0 && index<DashVisualRowCount)
{
string rowName=DashPrefix+"ROW_"+IntegerToString(index);
int y=(ObjectFind(0,rowName)>=0 ?
(int)ObjectGetInteger(0,rowName,OBJPROP_YDISTANCE) :
PanelTopMargin+HeaderHeight+8);
DashBeginLevelEdit(index,
(int)ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE),
y+1,DashLevelButtonWidth,RowHeight-3);
}
return;
}
int graphicIndex=-1;
if(DashIsGraphicButtonName(sparam,graphicIndex))
{
DashSelectGraphicLevel(graphicIndex);
return;
}
int index=-1;
if(DashIsAlertButtonName(sparam,index))
{
DashToggleAlert(index);
return;
}
}
void DashDeinit(const int reason)
{
DashDeleteDashboardObjects();
}
void DashTimer()
{
if(!BootstrapComplete)
return;
MqlTick current;
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
return;
DashRenderDashboard(current.bid);
}
int DashCalculate(const int rates_total,const int prev_calculated)
{
return rates_total;
}
void SyncTickVisualRowsFromDashboard()
{
if(DashVisualRowCount<DASH_MIN_VISUAL_ROWS || DashVisualRowCount>DASH_VISUAL_ROW_MAX)
return;
int selectedLevel=GraphLevelsPerSide;
bool selectedExists=false;
for(int i=0;i<DashVisualRowCount;i++)
{
if(DashVisualRows[i].level==selectedLevel)
{
selectedExists=true;
break;
}
}
int targetCount=DashVisualRowCount+(selectedExists ? 0 : 1);
bool changed=(TickLevelRowCount!=targetCount);
if(!changed)
{
for(int i=0;i<DashVisualRowCount;i++)
{
if(TickLevelRows[i].level!=DashVisualRows[i].level)
{
changed=true;
break;
}
}
}
if(!changed && !selectedExists &&
(TickLevelRowCount<=DashVisualRowCount ||
TickLevelRows[DashVisualRowCount].level!=selectedLevel))
changed=true;
if(!changed)
return;
ArrayResize(TickLevelRows,targetCount);
TickLevelRowCount=targetCount;
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
{
DashAlertEnabled[i]=false;
DashAlertActivityUntil[i]=0;
}
for(int i=0;i<DashVisualRowCount;i++)
TickLevelRows[i].level=DashVisualRows[i].level;
if(!selectedExists)
TickLevelRows[DashVisualRowCount].level=selectedLevel;
for(int i=0;i<TickLevelRowCount;i++)
ResetTickLevelRowState(TickLevelRows[i]);
if(ModelTickCount>=2)
{
// Новый LEVEL может потребовать дополнительной истории до публикации состояния.
HistoryBootstrapTargetTicks=MathMax(HistoryBootstrapTargetTicks,ModelTickCount);
BootstrapComplete=false;
LiveMode=false;
}
}
int TickInit()
{
if(StartPrice<=0.0 || BufferPoints<0 ||
ReversalArrowsHistory<0 || ReversalArrowsHistory>20 ||
MaxModelHistoryTicks<100 ||
HistoryChunkSize<100 || HistoryGraphTicks<100 || HistoryBootstrapTicks<100 ||
InitialViewTicks<1 || MinViewTicks<1 || MinViewTicks>InitialViewTicks ||
MinViewTicks>MathMax(MaxGraphSegments,MaxModelHistoryTicks))
return INIT_PARAMETERS_INCORRECT;
ShortName=_Symbol;
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
LegacyPriceObjectsCleaned=false;
DeleteLegacyPriceObjects();
LegacyGraphLabelsCleaned=false;
LastGraphLabelNameCount=0;
ArrayResize(LastGraphLabelNames,0);
GraphLevelsPerSide=MathMax(GraphLevelsPerSideMin,
MathMin(GraphLevelsPerSideMax,
(GraphLevelsPerSide/10)*10));
if(GraphLevelsPerSide<GraphLevelsPerSideMin)
GraphLevelsPerSide=GraphLevelsPerSideMin;
LoadGraphDisplayedLevelCount();
GraphHistoryCapacity=MathMax(100,MathMax(MaxGraphSegments,MaxModelHistoryTicks));
SyncTickVisualRowsFromDashboard();
// Высоту подокна задаёт терминал; Canvas подстраивается под доступную область.
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,false);
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,false);
MqlTick attachTick;
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
return INIT_FAILED;
AttachMsc=attachTick.time_msc;
CursorMsc=AttachMsc;
CursorSameMsc=0;
HistoryBootstrapTargetTicks=0;
ArrayResize(TickPrices,MathMax(100,GraphHistoryCapacity),
MathMax(100,GraphHistoryCapacity));
ArrayResize(SignalMarks,0);
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),
MathMax(100,MaxModelHistoryTicks));
ModelTickCount=0;
ModelTickHead=0;
TickCount=0;
TickHead=0;
TickSerial=0;
HavePreviousBid=false;
PreviousBid=0.0;
BootstrapComplete=false;
LiveMode=false;
ScaleCenterGrid=FloorGridForViewport(attachTick.bid);
ScaleInitialized=false;
AutoScaleInitialized=false;
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),GraphHistoryCapacity);
ViewEndOffset=0;
DragActive=false;
DragStartX=0;
DragStartOffset=0;
// Runtime-модель восстанавливается единым bootstrap-проходом.
FitInitialArrowHistory();
// Preserve chart-wide settings before enabling the events this indicator needs.
long previousMouseScroll=0;
long previousMouseMove=0;
long previousMouseWheel=0;
if(!ChartGetInteger(0,CHART_MOUSE_SCROLL,0,previousMouseScroll) ||
!ChartGetInteger(0,CHART_EVENT_MOUSE_MOVE,0,previousMouseMove) ||
!ChartGetInteger(0,CHART_EVENT_MOUSE_WHEEL,0,previousMouseWheel))
{
Print("TPDR: failed to read chart event settings. Error=",GetLastError());
return INIT_FAILED;
}
PreviousChartMouseScroll=(previousMouseScroll!=0);
PreviousChartEventMouseMove=(previousMouseMove!=0);
PreviousChartEventMouseWheel=(previousMouseWheel!=0);
ChartEventSettingsCaptured=true;
MouseInTickSubwindow=false;
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true) ||
!ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,true) ||
!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll))
{
int error=GetLastError();
RestoreChartEventSettings();
Print("TPDR: failed to configure chart event settings. Error=",error);
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
bool RestoreChartEventSettings()
{
if(!ChartEventSettingsCaptured)
return true;
bool ok=true;
if(!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll))
ok=false;
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,(long)PreviousChartEventMouseMove))
ok=false;
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,(long)PreviousChartEventMouseWheel))
ok=false;
if(ok)
ChartEventSettingsCaptured=false;
else
Print("TPDR: failed to restore one or more chart event settings. Error=",GetLastError());
return ok;
}
void TickDeinit(const int reason)
{
RestoreChartEventSettings();
if(CanvasReady)
{
TickCanvas.Destroy();
CanvasReady=false;
}
CanvasWidth=0;
CanvasHeight=0;
CanvasSubWindow=-1;
EventKillTimer();
ResetButtonHeld=false;
DeleteObjectsByPrefix();
}
bool UpdateMarketState()
{
// Единый владелец состояния рынка.
// OnCalculate() и OnTimer() только запускают эту функцию, но сама обработка
// history/live выполняется здесь ровно одним и тем же кодом.
if(SubWindow<0)
return false;
if(!BootstrapComplete)
{
if(!BootstrapHistory())
return false;
}
else
{
if(!ProcessLiveTicks())
return false;
}
return true;
}
void TickTimer()
{
// Таймер также может довести bootstrap до готового состояния: при создании
// подокна OnCalculate иногда вызывается раньше, чем ChartWindowFind() готов.
SubWindow=ChartWindowFind(0,ShortName);
if(SubWindow<0)
return;
// Пока bootstrap не завершён, состояние и отображение не публикуются.
// OnTimer() не имеет права рисовать промежуточную историю.
if(!UpdateMarketState())
return;
// RESET выполняется только по событию нажатия кнопки.
// Таймер не должен повторять тяжёлый полный reset каждые 100 мс.
MqlTick current;
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
return;
if(!EnsureCanvas())
return;
UpdateSubwindowScale(current.bid);
RenderGraph(current.bid);
InitialArrowHistoryScalePending=false;
ChartRedraw(0);
}
void HandleHorizontalZoom(const int xCursor,const int delta)
{
if(TickCount<=1 || GetGraphWidth()<=1 || delta==0)
return;
int oldSpan=GetViewCount();
int oldStart=GetViewStartIndex();
int newSpan=oldSpan;
if(delta>0)
newSpan=MathMax(MinViewTicks,oldSpan/2);
else
newSpan=MathMin(MathMin(GraphHistoryCapacity,TickCount),MathMax(MinViewTicks,oldSpan*2));
newSpan=MathMax(1,MathMin(newSpan,TickCount));
int graphWidth=GetGraphWidth();
double ratio=(double)MathMax(0,MathMin(xCursor,graphWidth-1))/
(double)MathMax(1,graphWidth-1);
int anchorOffset=(int)MathRound(ratio*(double)MathMax(0,oldSpan-1));
int anchorIndex=oldStart+anchorOffset;
ViewSpan=newSpan;
int newAnchorOffset=(int)MathRound(ratio*(double)MathMax(0,newSpan-1));
int newStart=anchorIndex-newAnchorOffset;
newStart=MathMax(0,MathMin(newStart,TickCount-newSpan));
ViewEndOffset=TickCount-newSpan-newStart;
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,TickCount-newSpan));
}
void HandleHorizontalDrag(const int x)
{
if(!DragActive || TickCount<=1 || GetGraphWidth()<=1)
return;
InitialViewLayoutActive=false;
int span=GetViewCount();
int dx=x-DragStartX;
int shift=(int)MathRound((double)dx*(double)span/(double)GetGraphWidth());
int maxOffset=MathMax(0,TickCount-span);
ViewEndOffset=DragStartOffset+shift;
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
}
bool IsCursorInTickSubwindow(const int y)
{
if(SubWindow<=0)
return false;
int top=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
int height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
return height>0 && y>=top && y<top+height;
}
void UpdateMouseScrollTarget(const int y)
{
MouseInTickSubwindow=IsCursorInTickSubwindow(y);
// Respect the user's original setting outside our subwindow, and keep the
// terminal scroll disabled while our vertical drag handlers own the pointer.
bool enableScroll=(PreviousChartMouseScroll && !MouseInTickSubwindow &&
!VerticalScaleDragActive && !VerticalPanDragActive);
bool currentScroll=(ChartGetInteger(0,CHART_MOUSE_SCROLL)!=0);
if(currentScroll!=enableScroll)
{
if(!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)enableScroll))
{
if(!MouseScrollSetErrorLogged)
Print("TPDR: failed to update CHART_MOUSE_SCROLL. Error=",GetLastError());
MouseScrollSetErrorLogged=true;
}
else
MouseScrollSetErrorLogged=false;
}
}
bool IsCursorOnVerticalScaleHandle(const int x,const int y)
{
if(!MouseInTickSubwindow || CanvasWidth<=0 || CanvasHeight<=0)
return false;
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
if(chartWidth<=CanvasWidth)
return false;
// Вертикальный zoom начинается в правой области цен, вне Canvas.
int left=CanvasWidth;
int right=chartWidth;
return x>=left && x<right;
}
void BeginVerticalPanDrag(const int y)
{
VerticalPanDragActive=true;
VerticalScaleManual=true;
VerticalPanDragStartY=y;
VerticalPanDragStartCenterPrice=(ViewportCenterPrice>0.0 ?
ViewportCenterPrice :
ViewportRayPrice(ScaleCenterGrid));
}
void HandleVerticalPanDrag(const int y)
{
if(!VerticalPanDragActive || CanvasHeight<=1 ||
GraphScaleMax<=GraphScaleMin)
return;
int deltaY=y-VerticalPanDragStartY;
double pricePerPixel=(GraphScaleMax-GraphScaleMin)/
(double)MathMax(1,CanvasHeight-1);
// Мышь вверх -> график вверх; мышь вниз -> график вниз.
ViewportCenterPrice=NormalizeDouble(
VerticalPanDragStartCenterPrice+(double)deltaY*pricePerPixel,_Digits);
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
VerticalPanDragStartY=y;
VerticalPanDragStartCenterPrice=ViewportCenterPrice;
VerticalPanDragActive=true;
RefreshGraph();
}
void EndVerticalPanDrag()
{
VerticalPanDragActive=false;
}
void BeginVerticalScaleDrag(const int y)
{
VerticalScaleDragActive=true;
VerticalScaleDragStartY=y;
VerticalScaleDragStartHalfRange=MathMax(VerticalScaleHalfRange,GetViewportLevelStepPrice()*0.20);
VerticalScaleManual=true;
}
void HandleVerticalScaleDrag(const int y)
{
if(!VerticalScaleDragActive)
return;
int deltaY=y-VerticalScaleDragStartY;
// Вверх = приближение по вертикали, вниз = отдаление.
// Масштаб рассчитывается относительно исходной точки drag, поэтому
// несколько mouse-move событий не накапливают округление.
double totalDeltaY=(double)(y-VerticalScaleDragStartY);
double factor=MathExp(totalDeltaY*VerticalScaleDragSensitivity);
double newHalfRange=VerticalScaleDragStartHalfRange*factor;
int upCount=MathMax(1,(TickLevelRowCount/2));
int downCount=MathMax(1,(TickLevelRowCount-(TickLevelRowCount/2)));
double minimumHalfRange=GetViewportLevelStepPrice()*MathMax(1,MathMax(upCount,downCount));
minimumHalfRange+=GetViewportLevelStepPrice()*0.10;
double maximumHalfRange=MathMax(minimumHalfRange*2.0,GetViewportLevelStepPrice()*2000.0);
VerticalScaleHalfRange=MathMax(minimumHalfRange,
MathMin(maximumHalfRange,newHalfRange));
RefreshGraph();
}
void EndVerticalScaleDrag()
{
VerticalScaleDragActive=false;
}
void TickChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
if(id==CHARTEVENT_OBJECT_CLICK)
{
if(sparam==Prefix+"ZOOM_MINUS" ||
sparam==Prefix+"ZOOM_PLUS" ||
sparam==Prefix+"RESET_CURRENT")
{
HandleZoomButtonClick(sparam);
return;
}
}
if(id==CHARTEVENT_MOUSE_WHEEL)
{
int flags=(int)(lparam>>32);
int xCursor=(int)(short)lparam;
int yCursor=(int)(short)(lparam>>16);
UpdateMouseScrollTarget(yCursor);
if(!MouseInTickSubwindow)
return;
InitialViewLayoutActive=false;
if((flags&0x0004)!=0)
{
int span=GetViewCount();
int step=MathMax(1,span/10);
int maxOffset=MathMax(0,TickCount-span);
ViewEndOffset+=(dparam>0.0 ? -step : step);
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
}
else
{
HandleHorizontalZoom(xCursor,(int)dparam);
ViewEndOffset=0;
}
RefreshGraph();
return;
}
if(id==CHARTEVENT_MOUSE_MOVE)
{
int x=(int)lparam;
int y=(int)dparam;
int state=(int)StringToInteger(sparam);
bool leftDown=((state&1)!=0);
UpdateMouseScrollTarget(y);
if(!MouseInTickSubwindow)
DragActive=false;
if(VerticalScaleDragActive)
{
if(leftDown)
{
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
HandleVerticalScaleDrag(y);
DragActive=false;
VerticalPanDragActive=false;
return;
}
EndVerticalScaleDrag();
UpdateMouseScrollTarget(y);
}
else if(VerticalPanDragActive)
{
if(leftDown)
{
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
HandleVerticalPanDrag(y);
DragActive=false;
return;
}
EndVerticalPanDrag();
UpdateMouseScrollTarget(y);
}
if(leftDown && MouseInTickSubwindow &&
IsCursorOnVerticalScaleHandle(x,y))
{
BeginVerticalScaleDrag(y);
HandleVerticalScaleDrag(y);
DragActive=false;
VerticalPanDragActive=false;
return;
}
if(leftDown && MouseInTickSubwindow &&
IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
{
ResetButtonHeld=true;
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,true);
}
else if(!leftDown)
{
ResetButtonHeld=false;
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
}
else if(!IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
{
ResetButtonHeld=false;
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
}
if(leftDown && MouseInTickSubwindow && !ResetButtonHeld)
{
if(!DragActive && !VerticalPanDragActive)
{
if(!PointerDragTracking)
{
PointerDragTracking=true;
DragStartX=x;
DragStartOffset=ViewEndOffset;
VerticalPanDragStartY=y;
}
int dx=x-DragStartX;
int dy=y-VerticalPanDragStartY;
if(MathAbs((double)dy)>MathAbs((double)dx) && MathAbs(dy)>=4)
{
BeginVerticalPanDrag(y);
DragActive=false;
HandleVerticalPanDrag(y);
return;
}
if(MathAbs(dx)>=4)
DragActive=true;
}
if(VerticalPanDragActive)
{
HandleVerticalPanDrag(y);
return;
}
if(DragActive)
{
HandleHorizontalDrag(x);
RefreshGraph();
}
}
else if(!leftDown || !MouseInTickSubwindow)
{
DragActive=false;
PointerDragTracking=false;
EndVerticalPanDrag();
}
return;
}
if(id==CHARTEVENT_CHART_CHANGE)
{
static int lastPeriod=(int)_Period;
bool periodChanged=((int)_Period!=lastPeriod);
lastPeriod=(int)_Period;
// Resolve the current indicator subwindow on every chart geometry
// event. Dragging the upper border changes window geometry and can
// invalidate the old bitmap's dimensions or owning subwindow.
int currentSubWindow=ChartWindowFind(0,ShortName);
if(currentSubWindow>=0)
SubWindow=currentSubWindow;
else
return;
if(periodChanged)
{
// Смена таймфрейма сбрасывает только горизонтальный просмотр.
// Вертикальный масштаб и абсолютные Y-координаты истории сохраняются.
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount));
ViewEndOffset=0;
InitialViewLayoutActive=true;
FitInitialArrowHistory();
}
if(!BootstrapComplete)
return;
MqlTick current;
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0)
{
if(EnsureCanvas())
{
UpdateSubwindowScale(current.bid);
RenderGraph(current.bid);
InitialArrowHistoryScalePending=false;
}
}
ChartRedraw(0);
return;
}
}
int TickCalculate(const int rates_total,const int prev_calculated)
{
SubWindow=ChartWindowFind(0,ShortName);
if(SubWindow<0)
return rates_total;
if(!UpdateMarketState())
return rates_total;
MqlTick current;
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0)
{
if(EnsureCanvas())
{
UpdateSubwindowScale(current.bid);
RenderGraph(current.bid);
}
}
ChartRedraw(0);
return rates_total;
}
int OnInit()
{
int dashResult=DashInit();
if(dashResult!=INIT_SUCCEEDED)
return dashResult;
int tickResult=TickInit();
if(tickResult!=INIT_SUCCEEDED)
{
TickDeinit(REASON_INITFAILED);
DashDeinit(REASON_INITFAILED);
return tickResult;
}
IndicatorSetString(INDICATOR_SHORTNAME,_Symbol);
int timerMs=MathMax(10,MathMin(2000,UpdateIntervalMs));
if(!EventSetMillisecondTimer(timerMs))
{
Print("CodeLock-Dashboard-TickGraphic: EventSetMillisecondTimer failed. Error=",GetLastError());
TickDeinit(REASON_INITFAILED);
DashDeinit(REASON_INITFAILED);
return INIT_FAILED;
}
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
TickDeinit(reason);
DashDeinit(reason);
}
void OnTimer()
{
SyncTickVisualRowsFromDashboard();
TickTimer();
DashTimer();
}
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
{
DashChartEvent(id,lparam,dparam,sparam);
TickChartEvent(id,lparam,dparam,sparam);
}
int OnCalculate(const int rates_total,const int prev_calculated,
const int begin,const double &price[])
{
// Этот индикатор не использует price[] как источник рыночной модели.
// Begin сохраняется в стандартной сигнатуре Calculate event.
DashCalculate(rates_total,prev_calculated);
return TickCalculate(rates_total,prev_calculated);
}