#include #property strict #property indicator_separate_window #property indicator_plots 0 #property version "3.00" #property description "CodeLock TickGraphic / editable LEVEL grids / reversal analysis" #define INVALID_GRID (-2147483647) #define CODELOCK_KEY_ESCAPE 0x1B // CODELOCK TICKGRAPHIC // ============================================================================ // TickGraphic — графический компонент общей модели CodeLock. // LEVEL — самостоятельная сетка, не фиксированный DISTANCE. // Каждая строка LEVEL имеет собственную сетку, DIRECTION, REVERSAL AT и ALERT. // HISTORY и LIVE используют одну модель CROSSING → DIRECTION → REVERSAL. // REVERSAL подтверждается пересечением соседнего LEVEL: // UP -> соседний LEVEL ниже текущего экстремума; // DOWN -> соседний LEVEL выше текущего экстремума. // Стрелка располагается на абсолютной цене LEVEL, подтвердившего REVERSAL. // Для каждого направления хранится до ReversalArrowsHistory стрелок. // Tick Graph, viewport и Canvas отображают модель, но не определяют её состояние. // ============================================================================ input double StartPrice=1; // Начальная цена, от которой строится сетка уровней input int BufferPoints=0; // Единый BUFFER подтверждения CROSSING в points input int HistoryChunkSize=5000; // Размер блока исторических тиков для обработки input int HistoryGraphTicks=200000; // Количество исторических тиков для первоначального графика input int HistoryBootstrapTicks=20000; // Количество исторических тиков для восстановления состояния до запуска input int MaxGraphSegments=200000; // Максимальное количество тиков, хранимых для графика input int MaxModelHistoryTicks=200000; // Максимальное количество тиков, доступных для полного пересчета модели input bool DrawReversalArrows=true; // Показывать стрелки подтверждённых разворотов input int ReversalArrowsHistory=20; // Количество UP и отдельно DOWN стрелок в истории (всего 40 при значении 20) input bool DrawRayObjects=true; // Показывать ценовые лучи input int PriceLabelsCount=3; // Количество подписей ценовых уровней input int InitialViewTicks=50000; // Количество тиков в первоначальном масштабе input int MinViewTicks=5; // Минимальное количество тиков при максимальном приближении input int BidLabelOffset=8; // Смещение подписи текущего Bid // Вертикальный viewport работает как осциллограф: абсолютная цена фиксирована, // BID движется внутри окна, а окно сдвигается только при достижении физического края. // Уровни динамически появляются/исчезают по текущему диапазону High/Low 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; // Цвет стрелки BUY-разворота input color SellColor=clrRed; // Цвет стрелки SELL-разворота string Prefix="TPDR_"; string ShortName="CodeLock TickGraphic / LEVEL Table / Reversal Model"; // Рабочее состояние количества уровней. Inputs задают начальное значение, // а кнопки изменяют именно это состояние без изменения input-параметров. double PreviousBid=0.0; bool HavePreviousBid=false; bool LiveMode=false; bool BootstrapComplete=false; int SubWindow=-1; int Direction=0; int LastReversalDirection=0; int LastCrossedRay=0; int LastCrossingDirection=0; int ConsecutiveCrossings=0; int DirectionRay=INVALID_GRID; double LastDirectionChangePrice=0.0; double ReversalAtPrice=0.0; // История подтверждённых REVERSAL. // -1 = последний, -2 = предыдущий. // Значения хранят абсолютные цены LEVEL, а не произвольные цены тика. double LastDown1Price=0.0; double LastDown2Price=0.0; double LastUp1Price=0.0; double LastUp2Price=0.0; long AttachMsc=0; long CursorMsc=1; int CursorSameMsc=0; int BootstrapAttempts=0; CCanvas TickCanvas; bool CanvasReady=false; int CanvasWidth=0; int CanvasHeight=0; 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; // Вертикальный масштаб Tick Graph. После ручного движения мышью по правой // области цен AutoScale не сбрасывает выбранный пользователем диапазон. // В стабильном режиме ценовая шкала не центрируется по каждому текущему BID. // BID движется внутри уже установленного вертикального окна. 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=60; const int LevelButtonWidth=52; const int LevelButtonHeight=22; const int LevelButtonGap=3; 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; // Контекст последнего автоматического масштаба. Он изменяется только при // изменении самого viewport/сеточного окна, а не при каждом новом BID. bool AutoScaleInitialized=false; int LastAutoScaleLevelsUp=-1; int LastAutoScaleLevelsDown=-1; bool ResetButtonHeld=false; bool PreviousChartMouseScroll=true; bool LegacyPriceObjectsCleaned=false; bool MouseInTickSubwindow=false; bool InitialViewLayoutActive=false; // Сохранённый снимок расчётного состояния используется после REASON_CHARTCHANGE. // Он не является отдельным источником истины: кэш — сериализованная копия // ModelTickPrices[] и производного состояния. double TickPrices[]; int TickCount=0; int TickHead=0; long TickSerial=0; // Единственный источник истины для пересчета модели. // Графический хвост TickPrices[] является только представлением этой истории. double ModelTickPrices[]; int ModelTickCount=0; int ModelTickHead=0; struct SignalMark { long serial; double price; bool up; bool reversal; }; SignalMark SignalMarks[]; long RebuildArrowSerialOverride=-1; // ============================================================================ // ЕДИНАЯ ТАБЛИЦА LEVEL // LEVEL — самостоятельная сетка. DISTANCE как отдельное понятие отсутствует. // ============================================================================ const int TickMinVisualRows=5; const int TickMaxVisualRows=100; const int TickVisualLevelMin=10; const int TickVisualLevelMax=2000; const int TickVisualLevelStep=10; struct TickLevelRow { int level; int direction; int directionGrid; double lastReversalPrice; double reversalAtPrice; double lastDown1Price; double lastDown2Price; double lastUp1Price; double lastUp2Price; bool alertEnabled; }; TickLevelRow TickLevelRows[]; int TickLevelRowCount=0; int TickEditingRow=-1; string TickLevelStoragePrefix() { string symbol=_Symbol; StringReplace(symbol,"\\","_"); StringReplace(symbol,"/","_"); StringReplace(symbol,":","_"); StringReplace(symbol," ","_"); string timeframe=IntegerToString((int)_Period); string chart=IntegerToString((long)ChartID()); return "CodeLock_TickGraphic_LEVELS_"+symbol+"_"+timeframe+"_"+chart+"_"; } bool IsValidTickVisualLevel(const int level) { return level>=TickVisualLevelMin && level<=TickVisualLevelMax && (level%TickVisualLevelStep)==0; } bool IsStrictTickVisualRows() { if(TickLevelRowCountTickMaxVisualRows) return false; for(int i=0;i0 && TickLevelRows[i].level<=TickLevelRows[i-1].level) return false; } return true; } void ResetTickLevelRowState(TickLevelRow &row) { row.direction=0; row.directionGrid=INVALID_GRID; 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.alertEnabled=false; } void SaveTickVisualRows() { if(TickLevelRowCount<1) return; string prefix=TickLevelStoragePrefix(); GlobalVariableSet(prefix+"COUNT",(double)TickLevelRowCount); for(int i=0;iTickMaxVisualRows) return false; ArrayResize(TickLevelRows,count); 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); } 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); } void RegisterTickRowCrossing(const int rowIndex,const int grid, const int crossingDirection,const MqlTick &tick, const bool emitSignals) { if(rowIndex<0 || rowIndex>=TickLevelRowCount) return; if(TickLevelRows[rowIndex].direction==0) { TickLevelRows[rowIndex].direction=crossingDirection; TickLevelRows[rowIndex].directionGrid=grid; UpdateTickRowReversalAt(rowIndex); return; } if(crossingDirection==TickLevelRows[rowIndex].direction) { TickLevelRows[rowIndex].directionGrid=grid; UpdateTickRowReversalAt(rowIndex); return; } int reversalGrid=TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction; if(grid!=reversalGrid) return; TickLevelRows[rowIndex].direction=crossingDirection; TickLevelRows[rowIndex].directionGrid=grid; 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; } RecordReversalArrow(tick,TickLevelRows[rowIndex].lastReversalPrice,crossingDirection>0,emitSignals); } 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) { RegisterTickRowCrossing(rowIndex,grid,1,tick,emitSignals); if(TickLevelRows[rowIndex].alertEnabled && emitSignals) { NotifyEvent("LEVEL CROSSING","UP", TickRowPrice(rowIndex,grid),tick.time_msc); 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) { RegisterTickRowCrossing(rowIndex,grid,-1,tick,emitSignals); if(TickLevelRows[rowIndex].alertEnabled && emitSignals) { NotifyEvent("LEVEL CROSSING","DOWN", TickRowPrice(rowIndex,grid),tick.time_msc); TickLevelRows[rowIndex].alertEnabled=false; } } } } } void RebuildTickVisualRowsFromHistory() { ArrayResize(SignalMarks,0); ResetAllTickVisualRowStates(); if(ModelTickCount<2) return; long savedTickSerial=TickSerial; RebuildArrowSerialOverride=0; for(int i=1;i=TickLevelRowCount || !IsValidTickVisualLevel(newLevel)) return false; if(index>0 && newLevel<=TickLevelRows[index-1].level) return false; if(index+1=TickLevelRows[index+1].level) return false; return true; } void CancelTickLevelEdit() { if(TickEditingRow<0) return; ObjectDelete(0,Prefix+"LEVEL_EDIT_"+IntegerToString(TickEditingRow)); TickEditingRow=-1; ChartRedraw(0); } void BeginTickLevelEdit(const int index,const int x,const int y, const int width,const int height) { if(index<0 || index>=TickLevelRowCount) return; if(TickEditingRow>=0 && TickEditingRow!=index) ObjectDelete(0,Prefix+"LEVEL_EDIT_"+IntegerToString(TickEditingRow)); string name=Prefix+"LEVEL_EDIT_"+IntegerToString(index); if(ObjectFind(0,name)<0) { ResetLastError(); if(!ObjectCreate(0,name,OBJ_EDIT,0,0,0)) { Print("TPDR: 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,1500); ObjectSetString(0,name,OBJPROP_FONT,"Arial"); 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,9); ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite); ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack); ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGold); ObjectSetString(0,name,OBJPROP_TEXT,IntegerToString(TickLevelRows[index].level)); TickEditingRow=index; ChartRedraw(0); } void CommitTickLevelEdit(const int index,const string textValue) { if(index<0 || index>=TickLevelRowCount) { CancelTickLevelEdit(); return; } if(StringLen(textValue)==0) { Print("TPDR: LEVEL edit rejected: empty value."); CancelTickLevelEdit(); return; } for(int i=0;i'9') { Print("TPDR: LEVEL edit rejected: integer 10..2000, step 10 required."); CancelTickLevelEdit(); return; } } int newLevel=(int)StringToInteger(textValue); if(!ValidateTickEditedLevel(index,newLevel)) { Print("TPDR: LEVEL edit rejected: ",textValue, ". Allowed: 10..2000, step 10, strict order."); CancelTickLevelEdit(); return; } TickLevelRows[index].level=newLevel; SaveTickVisualRows(); CancelTickLevelEdit(); // Изменение LEVEL — изменение модели, поэтому производное состояние // пересобирается из той же истории, а горизонтальный VIEW не сбрасывается. RebuildTickVisualRowsFromHistory(); RefreshGraph(); } bool AddTickLevelRow(const int index) { if(index<0 || index>=TickLevelRowCount || TickLevelRowCount>=TickMaxVisualRows) return false; int current=TickLevelRows[index].level; int upper=(index+1=upper || candidate>TickVisualLevelMax) { Print("TPDR: cannot add LEVEL above ",current, " pt without breaking strict order."); return false; } for(int i=TickLevelRowCount;i>index+1;i--) TickLevelRows[i]=TickLevelRows[i-1]; TickLevelRows[index+1].level=candidate; ResetTickLevelRowState(TickLevelRows[index+1]); TickLevelRowCount++; SaveTickVisualRows(); RebuildTickVisualRowsFromHistory(); RefreshGraph(); return true; } bool DeleteTickLevelRow(const int index) { if(index<0 || index>=TickLevelRowCount || TickLevelRowCount<=TickMinVisualRows) return false; int removed=TickLevelRows[index].level; for(int i=index;i=ModelTickCount) return 0.0; return ModelTickPrices[(ModelTickHead+index)%MaxModelHistoryTicks]; } void ResetSelectedCalculation() { Direction=0; LastReversalDirection=0; LastCrossedRay=0; LastCrossingDirection=0; ConsecutiveCrossings=0; DirectionRay=INVALID_GRID; LastDirectionChangePrice=0.0; ReversalAtPrice=0.0; LastDown1Price=0.0; LastDown2Price=0.0; LastUp1Price=0.0; LastUp2Price=0.0; PreviousBid=0.0; HavePreviousBid=false; } void ClearDerivedState() { TickCount=0; TickHead=0; TickSerial=0; ArrayResize(TickPrices,MathMax(100,MaxGraphSegments), MathMax(100,MaxGraphSegments)); ArrayResize(SignalMarks,0); AutoScaleInitialized=false; LastAutoScaleLevelsUp=-1; LastAutoScaleLevelsDown=-1; ResetSelectedCalculation(); ScaleCenterGrid=0; ScaleInitialized=false; 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 || MaxGraphSegments<1) return; // TickPrices[] заранее резервируется целиком. На глубокой истории // нельзя делать ArrayResize() на каждом тике: это создаёт лишние // перераспределения памяти и заметно увеличивает стартовую загрузку. if(ArraySize(TickPrices)=TickCount) return 0.0; int physical=(TickHead+index)%MaxGraphSegments; return TickPrices[physical]; } void AddSignalMark(const double price,const bool up,const bool reversal) { if(!reversal || ReversalArrowsHistory<=0) return; int size=ArraySize(SignalMarks); ArrayResize(SignalMarks,size+1); SignalMarks[size].serial=(RebuildArrowSerialOverride>=0 ? RebuildArrowSerialOverride : TickSerial); SignalMarks[size].price=NormalizeDouble(price,_Digits); SignalMarks[size].up=up; SignalMarks[size].reversal=true; int keepCount=MathMax(1,ReversalArrowsHistory); int directionCount=0; for(int i=0;i0.0 ? ViewportCenterPrice : ViewportRayPrice(ScaleCenterGrid)); // Осциллографический viewport: // обычный BID, CROSSING и REVERSAL не двигают окно. // Перенос выполняется только после входа BID в защитную зону края. // Центр не привязывается к GRID: это исключает ступенчатый // "трамплин" viewport при движении BID около границы. double edgeMargin=range*0.20; double upperEdge=GraphScaleMax-edgeMargin; double lowerEdge=GraphScaleMin+edgeMargin; // Перенос выполняется только после выхода BID в краевую зону. // После переноса BID оказывается около центра окна, поэтому viewport // не подтягивается за каждым тиком и не создаёт эффект "трамплина". double targetOffset=range*0.05; if(bid>upperEdge) { double targetCenter=bid-targetOffset; if(targetCenter>center) { ViewportCenterPrice=NormalizeDouble(targetCenter,_Digits); ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice); } } else if(bid0.0 ? ViewportCenterPrice : bid); GraphScaleMin=NormalizeDouble(viewportCenter-scaleHalfRange,_Digits); GraphScaleMax=NormalizeDouble(viewportCenter+scaleHalfRange,_Digits); if(GraphScaleMax<=GraphScaleMin) { GraphScaleMin=viewportCenter-minimumStep; GraphScaleMax=viewportCenter+minimumStep; } 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)); } // Первичная настройка горизонтального окна после загрузки истории. // ВАЖНО: функция вызывается только при bootstrap/смене расстояния. // Рендер больше не имеет права самовольно расширять ViewSpan. // Поэтому после первоначального показа 40 стрелок пользователь может // свободно приближать график колесом мыши. void FitInitialArrowHistory() { if(TickCount<=0) return; const int targetPerDirection=10; 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; } // Сохраняем отдельную проверку состава истории: для стандартного значения // ReversalArrowsHistory=20 ожидается максимум 20 UP + 20 DOWN. void ValidateReversalArrowHistory() { if(ReversalArrowsHistory<=0) return; int upCount=0; int downCount=0; for(int i=0;iReversalArrowsHistory || downCount>ReversalArrowsHistory) Print("TPDR: reversal arrow history invariant violated. UP=", upCount," DOWN=",downCount, " Limit=",ReversalArrowsHistory); } int GraphPriceToY(const double price) { if(CanvasHeight<=1 || GraphScaleMax<=GraphScaleMin) return CanvasHeight/2; const int edgePadding=6; int drawableHeight=MathMax(1,CanvasHeight-1-2*edgePadding); double ratio=(GraphScaleMax-price)/(GraphScaleMax-GraphScaleMin); ratio=MathMax(0.0,MathMin(1.0,ratio)); return edgePadding+(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;xTickSerial) continue; int index=(int)(serial-firstSerial); if(indexviewEnd) continue; int relative=index-viewStart; int graphWidth=GetGraphWidth(); int leftMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0); int rightMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0); int drawWidth=MathMax(1,graphWidth-leftMargin-rightMargin); int x=(viewCount==1) ? leftMargin : leftMargin+(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].reversal ? (SignalMarks[i].up ? BuyColor : SellColor) : (SignalMarks[i].up ? UpCrossColor : DownCrossColor); // Точка привязки стрелки = абсолютная цена события. // Никакого дополнительного вертикального offset здесь быть не должно: // острие DrawArrowMark() совпадает с GraphPriceToY(price). int arrowY=y; DrawArrowMark(x,arrowY,SignalMarks[i].up,SignalMarks[i].reversal,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]); } void EnsureCodeLockStateLabel(const string name,const int x,const int y, const int fontSize,const color clr,const string textValue) { if(ObjectFind(0,name)<0) { if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0)) { Print("TPDR: CodeLock state label creation error=",GetLastError(), " Name=",name); return; } ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER); ObjectSetString(0,name,OBJPROP_FONT,"Arial"); ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER); 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,10); } ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x); ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y); ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize); ObjectSetInteger(0,name,OBJPROP_COLOR,clr); ObjectSetString(0,name,OBJPROP_TEXT,textValue); } color CodeLockDirectionColor(const int direction) { if(direction>0) return clrLimeGreen; if(direction<0) return clrTomato; return clrSilver; } string CodeLockDirectionText(const int direction) { if(direction>0) return "UP"; if(direction<0) return "DOWN"; return "WAIT"; } void DrawCodeLockStatePanel(const double bid) { if(SubWindow<0 || bid<=0.0 || GetViewportLevelStepPrice()<=0.0 || CanvasWidth<=0) return; DeleteLegacyPriceObjects(); int panelX=CanvasWidth+100; int panelY=8; int labelGap=18; int valueX=112; int valueFont=9; int headerFont=8; // LEVEL — текущий абсолютный LEVEL, определённый общей моделью. double levelPrice=0.0; if(DirectionRay!=INVALID_GRID) levelPrice=ViewportRayPrice(DirectionRay); else levelPrice=ViewportRayPrice(CurrentLevelGridInitialized ? CurrentLevelGrid : FloorGridForViewport(bid)); string levelText=DoubleToString(levelPrice,_Digits); string directionText=CodeLockDirectionText(Direction); color directionColor=CodeLockDirectionColor(Direction); string reversalAtText=(ReversalAtPrice>0.0 ? DoubleToString(ReversalAtPrice,_Digits) : "—"); string lastDown2=(LastDown2Price>0.0 ? DoubleToString(LastDown2Price,_Digits) : "—"); string lastDown1=(LastDown1Price>0.0 ? DoubleToString(LastDown1Price,_Digits) : "—"); string lastUp2=(LastUp2Price>0.0 ? DoubleToString(LastUp2Price,_Digits) : "—"); string lastUp1=(LastUp1Price>0.0 ? DoubleToString(LastUp1Price,_Digits) : "—"); EnsureCodeLockStateLabel(Prefix+"STATE_TITLE", panelX,panelY,headerFont,clrSilver, "CODELOCK STATE"); EnsureCodeLockStateLabel(Prefix+"STATE_LEVEL_LABEL", panelX,panelY+labelGap,headerFont,clrSilver, "LEVEL"); EnsureCodeLockStateLabel(Prefix+"STATE_LEVEL_VALUE", panelX+valueX,panelY+labelGap,valueFont, directionColor,levelText); EnsureCodeLockStateLabel(Prefix+"STATE_DIRECTION_LABEL", panelX,panelY+2*labelGap,headerFont,clrSilver, "DIRECTION"); EnsureCodeLockStateLabel(Prefix+"STATE_DIRECTION_VALUE", panelX+valueX,panelY+2*labelGap,valueFont, directionColor,directionText); EnsureCodeLockStateLabel(Prefix+"STATE_REVERSAL_AT_LABEL", panelX,panelY+3*labelGap,headerFont,clrSilver, "REVERSAL AT"); EnsureCodeLockStateLabel(Prefix+"STATE_REVERSAL_AT_VALUE", panelX+valueX,panelY+3*labelGap,valueFont, directionColor,reversalAtText); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_2", panelX,panelY+5*labelGap,headerFont,clrTomato, "LAST-DOWN-2"); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_2_VALUE", panelX+valueX,panelY+5*labelGap,valueFont, clrTomato,lastDown2); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_1", panelX,panelY+6*labelGap,headerFont,clrTomato, "LAST-DOWN-1"); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_1_VALUE", panelX+valueX,panelY+6*labelGap,valueFont, clrTomato,lastDown1); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_2", panelX,panelY+7*labelGap,headerFont,clrLimeGreen, "LAST-UP-2"); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_2_VALUE", panelX+valueX,panelY+7*labelGap,valueFont, clrLimeGreen,lastUp2); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_1", panelX,panelY+8*labelGap,headerFont,clrLimeGreen, "LAST-UP-1"); EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_1_VALUE", panelX+valueX,panelY+8*labelGap,valueFont, clrLimeGreen,lastUp1); } bool EnsureCanvas() { if(SubWindow<0) return false; 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; if(CanvasReady && CanvasWidth==wantedWidth && CanvasHeight==chartHeight) return true; if(CanvasReady) { ResetLastError(); if(!TickCanvas.Resize(wantedWidth,chartHeight)) { Print("TPDR: canvas resize failed. Error=",GetLastError()); return false; } 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; CanvasReady=true; 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) ObjectDelete(0,legacyNames[k]); if(ObjectFind(0,Prefix+"LEVELS_LABEL")>=0) ObjectDelete(0,Prefix+"LEVELS_LABEL"); if(ObjectFind(0,Prefix+"DISTANCE_LABEL")>=0) ObjectDelete(0,Prefix+"DISTANCE_LABEL"); EnsurePanelLabel(Prefix+"LEVEL_TABLE_LABEL","LEVEL"); int x=panelLeft+ControlPanelLabelWidth+ControlPanelPadding; int y=8+ControlPanelTop+LevelButtonHeight+2; EnsurePanelLabel(Prefix+"LEVEL_TABLE_HEADER", "DIRECTION REVERSAL AT ALERT D2 D1 U2 U1"); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_YDISTANCE,y-16); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_ZORDER,1000); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_XDISTANCE, panelLeft+ControlPanelPadding); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_YDISTANCE,y+3); ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_ZORDER,1000); // Показываем строки сверху вниз: самый высокий LEVEL первым. for(int displayRow=0;displayRow0.0 ? DoubleToString(TickLevelRows[index].reversalAtPrice,_Digits) : "—"); EnsureLevelCountButton(alertName,TickLevelRows[index].alertEnabled ? "ON" : "OFF"); EnsurePanelLabel(lastDown2Name, TickLevelRows[index].lastDown2Price>0.0 ? DoubleToString(TickLevelRows[index].lastDown2Price,_Digits) : "—"); EnsurePanelLabel(lastDown1Name, TickLevelRows[index].lastDown1Price>0.0 ? DoubleToString(TickLevelRows[index].lastDown1Price,_Digits) : "—"); EnsurePanelLabel(lastUp2Name, TickLevelRows[index].lastUp2Price>0.0 ? DoubleToString(TickLevelRows[index].lastUp2Price,_Digits) : "—"); EnsurePanelLabel(lastUp1Name, TickLevelRows[index].lastUp1Price>0.0 ? DoubleToString(TickLevelRows[index].lastUp1Price,_Digits) : "—"); // LEVEL — не кнопка выбора DISTANCE, а редактируемая ячейка. EnsureLevelCountButton(levelName,IntegerToString(TickLevelRows[index].level)); ObjectSetInteger(0,minusName,OBJPROP_XDISTANCE,x); ObjectSetInteger(0,minusName,OBJPROP_YDISTANCE,rowY); ObjectSetInteger(0,plusName,OBJPROP_XDISTANCE,x+LevelButtonWidth+LevelButtonGap); ObjectSetInteger(0,plusName,OBJPROP_YDISTANCE,rowY); ObjectSetInteger(0,levelName,OBJPROP_XDISTANCE, x+2*(LevelButtonWidth+LevelButtonGap)); ObjectSetInteger(0,levelName,OBJPROP_YDISTANCE,rowY); ObjectSetInteger(0,levelName,OBJPROP_ZORDER,1100); ObjectSetInteger(0,dirName,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+8); ObjectSetInteger(0,dirName,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,revName,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+55); ObjectSetInteger(0,revName,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,alertName,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+125); ObjectSetInteger(0,alertName,OBJPROP_XSIZE,42); ObjectSetInteger(0,alertName,OBJPROP_YSIZE,LevelButtonHeight); SetLevelButtonVisual(alertName,TickLevelRows[index].alertEnabled); ObjectSetInteger(0,alertName,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,lastDown2Name,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+175); ObjectSetInteger(0,lastDown2Name,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,lastDown1Name,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+225); ObjectSetInteger(0,lastDown1Name,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,lastUp2Name,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+275); ObjectSetInteger(0,lastUp2Name,OBJPROP_YDISTANCE,rowY+3); ObjectSetInteger(0,lastUp1Name,OBJPROP_XDISTANCE, x+3*(LevelButtonWidth+LevelButtonGap)+325); ObjectSetInteger(0,lastUp1Name,OBJPROP_YDISTANCE,rowY+3); SetLevelButtonVisual(minusName,false); SetLevelButtonVisual(plusName,false); SetLevelButtonVisual(levelName,false); ObjectSetInteger(0,dirName,OBJPROP_ZORDER,1000); ObjectSetInteger(0,revName,OBJPROP_ZORDER,1000); ObjectSetInteger(0,alertName,OBJPROP_ZORDER,1000); ObjectSetInteger(0,lastDown2Name,OBJPROP_ZORDER,1000); ObjectSetInteger(0,lastDown1Name,OBJPROP_ZORDER,1000); ObjectSetInteger(0,lastUp2Name,OBJPROP_ZORDER,1000); ObjectSetInteger(0,lastUp1Name,OBJPROP_ZORDER,1000); ObjectSetInteger(0,minusName,OBJPROP_HIDDEN,true); ObjectSetInteger(0,plusName,OBJPROP_HIDDEN,true); ObjectSetInteger(0,levelName,OBJPROP_HIDDEN,true); } // Удаляем строки, которые остались от предыдущего количества LEVEL. for(int i=TickLevelRowCount;i=0) ObjectDelete(0,names[k]); } } 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 UpdateZoomButtonsLayout() { if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0) return; 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); EnsureControlPanelBackground(); EnsureZoomButton(minusName,"-"); EnsureZoomButton(plusName,"+"); EnsureResetButton(resetName,"RESET"); EnsurePanelLabel(labelName,"ZOOM"); int x=ControlPanelLabelWidth+ControlPanelPadding; // ZOOM располагается последней строкой — в самом низу общей таблицы, // после обеих строк Distance. int y=ControlPanelTop+4*(LevelButtonHeight+LevelButtonGap) +2*(DistanceButtonHeight+DistanceButtonGap)+4; 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,ControlPanelPadding); ObjectSetInteger(0,labelName,OBJPROP_YDISTANCE,y+4); } void ResetGraphToCurrentPrice() { if(TickCount<=0) return; // RESET возвращает только начальный VIEW: последние 10 UP + 10 DOWN + 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; LastAutoScaleLevelsUp=-1; LastAutoScaleLevelsDown=-1; 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); DrawTickLayer(bid); DrawSignalLayer(); DrawBidMarker(bid); DrawCurrentTickMarker(bid); TickCanvas.Update(true); // Таблица LEVEL выводится в главном ценовом окне. UpdateZoomButtonsLayout(); UpdateLevelTableLayout(bid); DeleteLegacyStatePanel(); } void RefreshGraph() { if(SubWindow<0 || !BootstrapComplete) return; MqlTick current; if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0) return; // Масштаб зависит от текущего горизонтального viewport. // Поэтому после wheel/drag сначала пересчитываем вертикальный диапазон, // затем рисуем. Сам ViewSpan при этом НЕ изменяется. UpdateSubwindowScale(current.bid); RenderGraph(current.bid); InitialArrowHistoryScalePending=false; ChartRedraw(0); } void UpdateReversalAt() { if(Direction==0 || DirectionRay==INVALID_GRID) { ReversalAtPrice=0.0; return; } ReversalAtPrice=ViewportRayPrice(DirectionRay-Direction); } void RegisterCrossing(const int grid,const int crossingDirection, const MqlTick &tick,const bool emitSignals) { LastCrossedRay=grid; LastCrossingDirection=crossingDirection; if(Direction==0) { Direction=crossingDirection; DirectionRay=grid; ConsecutiveCrossings=1; UpdateReversalAt(); return; } if(crossingDirection==Direction) { // CROSSING того же направления не изменяет DIRECTION. // Последовательность 1.020 -> 1.040 -> 1.060 при UP // только фиксирует CROSSING; DIRECTION остаётся определённым // последним подтверждённым REVERSAL. DirectionRay=grid; ConsecutiveCrossings++; UpdateReversalAt(); return; } // Противоположное CROSSING само по себе ещё не меняет DIRECTION. // REVERSAL подтверждается только ожидаемым соседним LEVEL. int reversalGrid=DirectionRay-Direction; if(grid!=reversalGrid) return; Direction=crossingDirection; DirectionRay=grid; ConsecutiveCrossings=1; LastReversalDirection=crossingDirection; LastDirectionChangePrice=ViewportRayPrice(grid); UpdateReversalAt(); // LAST изменяется только после подтверждённого REVERSAL. // -1 = последний, -2 = предыдущий. if(crossingDirection<0) { LastDown2Price=LastDown1Price; LastDown1Price=LastDirectionChangePrice; } else { LastUp2Price=LastUp1Price; LastUp1Price=LastDirectionChangePrice; } RecordReversalArrow(tick,LastDirectionChangePrice,crossingDirection>0,emitSignals); } 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); } TickSerial++; if(!HavePreviousBid) { PreviousBid=bid; HavePreviousBid=true; if(drawGraph) AddTickToGraph(bid); if(!ScaleInitialized) { ScaleCenterGrid=FloorGridForViewport(bid); ScaleInitialized=true; } return; } double oldBid=PreviousBid; if(drawGraph) AddTickToGraph(bid); if(bid==oldBid) return; for(int row=0;row=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 BootstrapHistory() { int targetTicks=MathMax(HistoryGraphTicks,HistoryBootstrapTicks); targetTicks=MathMax(targetTicks,100); targetTicks=MathMin(targetTicks,MathMax(100,MaxModelHistoryTicks)); 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++; if(BootstrapAttempts<50) return false; Print("TPDR: tick history synchronization did not complete. Error=",lastError); return false; } Print("TPDR: bootstrap CopyTicks failed. Error=",lastError); return false; } break; } if(copied<=0) { BootstrapAttempts++; return false; } int lastAttach=-1; for(int i=0;i=targetTicks && ticks[copied-1].time_msc0.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; BootstrapAttempts=0; BootstrapComplete=true; LiveMode=true; ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount)); ViewEndOffset=0; // Стрелки уже формируются тем же проходом, что и основная модель. // Второй полный проход по ModelTickPrices[] здесь не нужен. FitInitialArrowHistory(); 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; for(int attempt=0;attempt<3;attempt++) { ResetLastError(); copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO, (ulong)MathMax(1,CursorMsc),(uint)requestTried); int error=GetLastError(); if(copied>=0) break; if(error!=4004) return false; requestTried=MathMax(100,requestTried/2); } if(copied<0 || copied==0) return copied==0; int start=0; if(ticks[0].time_msc==CursorMsc) start=MathMin(CursorSameMsc,copied); int processed=copied-start; for(int i=start;i20 || MaxModelHistoryTicks<100 || HistoryChunkSize<100 || HistoryGraphTicks<100 || HistoryBootstrapTicks<100 || InitialViewTicks<1 || MinViewTicks<1 || MinViewTicks>InitialViewTicks || MinViewTicks>MaxGraphSegments) return INIT_PARAMETERS_INCORRECT; IndicatorSetString(INDICATOR_SHORTNAME,ShortName); if(!LoadTickVisualRows()) InitializeTickVisualRows(); if(!IsStrictTickVisualRows()) return INIT_PARAMETERS_INCORRECT; LegacyPriceObjectsCleaned=false; // Высотой подокна управляет терминал/пользователь. // Индикатор не фиксирует её; Canvas занимает актуальную высоту подокна. // // Вертикальный масштаб также не фиксируем средствами indicator window. // Собственный Canvas имеет отдельный AutoScale, рассчитанный по видимому // диапазону тиков и минимальному диапазону выбранных уровней. // INDICATOR_HEIGHT здесь НЕ задаём. // Это свойство фиксирует высоту подокна; пользователь должен иметь // возможность менять высоту мышью, а 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; ArrayResize(TickPrices,MathMax(100,MaxGraphSegments), MathMax(100,MaxGraphSegments)); 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; LastReversalDirection=0; ScaleCenterGrid=FloorGridForViewport(attachTick.bid); ScaleInitialized=false; AutoScaleInitialized=false; LastAutoScaleLevelsUp=-1; LastAutoScaleLevelsDown=-1; ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MaxGraphSegments); ViewEndOffset=0; DragActive=false; DragStartX=0; DragStartOffset=0; // Runtime-модель всегда восстанавливается из истории. RebuildTickVisualRowsFromHistory(); FitInitialArrowHistory(); // Над тиковым подокном колесо и перетаскивание обслуживает наш график, // а над основным свечным окном сохраняется штатное управление терминала. PreviousChartMouseScroll=(bool)ChartGetInteger(0,CHART_MOUSE_SCROLL); MouseInTickSubwindow=false; ChartSetInteger(0,CHART_MOUSE_SCROLL,true); ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true); ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,true); EventSetMillisecondTimer(100); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll); if(CanvasReady) { TickCanvas.Destroy(); CanvasReady=false; } 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 OnTimer() { // Таймер также может довести 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; 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(MaxGraphSegments,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=left && x0.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 OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam) { if(id==CHARTEVENT_KEYDOWN && TickEditingRow>=0 && lparam==CODELOCK_KEY_ESCAPE) { CancelTickLevelEdit(); return; } if(id==CHARTEVENT_OBJECT_ENDEDIT) { string editPrefix=Prefix+"LEVEL_EDIT_"; if(StringFind(sparam,editPrefix)==0) { int index=(int)StringToInteger( StringSubstr(sparam,StringLen(editPrefix))); string textValue=""; if(ObjectGetString(0,sparam,OBJPROP_TEXT,0,textValue)) CommitTickLevelEdit(index,textValue); else CancelTickLevelEdit(); return; } } if(id==CHARTEVENT_OBJECT_CLICK) { string minusPrefix=Prefix+"LEVEL_MINUS_"; if(StringFind(sparam,minusPrefix)==0) { int index=(int)StringToInteger( StringSubstr(sparam,StringLen(minusPrefix))); DeleteTickLevelRow(index); return; } string plusPrefix=Prefix+"LEVEL_PLUS_"; if(StringFind(sparam,plusPrefix)==0) { int index=(int)StringToInteger( StringSubstr(sparam,StringLen(plusPrefix))); AddTickLevelRow(index); return; } string alertPrefix=Prefix+"LEVEL_ALERT_"; if(StringFind(sparam,alertPrefix)==0) { int index=(int)StringToInteger( StringSubstr(sparam,StringLen(alertPrefix))); if(index>=0 && index>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) RefreshGraph(); } int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[], const double &open[],const double &high[],const double &low[], const double &close[],const long &tick_volume[],const long &volume[], const int &spread[]) { 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) { UpdateSubwindowScale(current.bid); RenderGraph(current.bid); } ChartRedraw(0); return rates_total; }