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