#include #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) 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;i0 ? 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) 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=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)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;i0) 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)=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;i0.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;nTickSerial || 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;rowkeepCount) 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=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;i0.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;jTickSerial) continue; int index=(int)(serial-firstSerial); if(indexviewEnd) 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=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=top && localY=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;row0 && 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=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)=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=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(DashVisualRowCountDASH_DASHBOARD_ROWS) return false; for(int i=0;i0 && DashVisualRows[i].level<=DashVisualRows[i-1].level) return false; } return true; } bool DashParseVisualRows(const string source) { string parts[]; int count=StringSplit(source,',',parts); if(countDASH_DASHBOARD_ROWS) return false; DashVisualRowCount=0; for(int i=0;i'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;i0 && 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) 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) return; int selectedLevel=DashVisualRows[index].level; if(selectedLevel=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=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=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'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+1current) { bool free=true; for(int i=0;iindex+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;i0) 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;i0.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=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=0 && index=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(DashVisualRowCountDASH_VISUAL_ROW_MAX) return; int selectedLevel=GraphLevelsPerSide; bool selectedExists=false; for(int i=0;i=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>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); }