| #include <Canvas\Canvas.mqh>
|
|
|
| #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(TickLevelRowCount<TickMinVisualRows ||
|
| TickLevelRowCount>TickMaxVisualRows)
|
| return false;
|
|
|
| for(int i=0;i<TickLevelRowCount;i++)
|
| {
|
| if(!IsValidTickVisualLevel(TickLevelRows[i].level))
|
| return false;
|
|
|
| if(i>0 && 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;i<TickMaxVisualRows;i++)
|
| {
|
| string key=prefix+IntegerToString(i);
|
| if(i<TickLevelRowCount)
|
| GlobalVariableSet(key,(double)TickLevelRows[i].level);
|
| else
|
| GlobalVariableDel(key);
|
| }
|
| }
|
|
|
| bool LoadTickVisualRows()
|
| {
|
| string prefix=TickLevelStoragePrefix();
|
|
|
| if(!GlobalVariableCheck(prefix+"COUNT"))
|
| return false;
|
|
|
| int count=(int)GlobalVariableGet(prefix+"COUNT");
|
| if(count<TickMinVisualRows || count>TickMaxVisualRows)
|
| return false;
|
|
|
| ArrayResize(TickLevelRows,count);
|
|
|
| for(int i=0;i<count;i++)
|
| {
|
| string key=prefix+IntegerToString(i);
|
| if(!GlobalVariableCheck(key))
|
| return false;
|
|
|
| TickLevelRows[i].level=(int)GlobalVariableGet(key);
|
| ResetTickLevelRowState(TickLevelRows[i]);
|
| }
|
|
|
| TickLevelRowCount=count;
|
| return IsStrictTickVisualRows();
|
| }
|
|
|
| void InitializeTickVisualRows()
|
| {
|
| ArrayResize(TickLevelRows,TickMinVisualRows);
|
|
|
| for(int i=0;i<TickMinVisualRows;i++)
|
| {
|
| TickLevelRows[i].level=(i+1)*TickVisualLevelStep;
|
| ResetTickLevelRowState(TickLevelRows[i]);
|
| }
|
|
|
| TickLevelRowCount=TickMinVisualRows;
|
| SaveTickVisualRows();
|
| }
|
|
|
| void ResetAllTickVisualRowStates()
|
| {
|
| for(int i=0;i<TickLevelRowCount;i++)
|
| ResetTickLevelRowState(TickLevelRows[i]);
|
| }
|
|
|
| int FloorGridForStep(const double price,const double step)
|
| {
|
| if(step<=0.0)
|
| return INVALID_GRID;
|
|
|
| double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
|
| return (int)MathFloor(position);
|
| }
|
|
|
| int CeilGridForStep(const double price,const double step)
|
| {
|
| if(step<=0.0)
|
| return INVALID_GRID;
|
|
|
| double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
|
| return (int)MathCeil(position);
|
| }
|
|
|
| double RayPriceForStep(const int grid,const double step)
|
| {
|
| if(step<=0.0)
|
| return 0.0;
|
|
|
| return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
|
| }
|
|
|
| int TickRowGrid(const int rowIndex,const double price)
|
| {
|
| if(rowIndex<0 || rowIndex>=TickLevelRowCount)
|
| return INVALID_GRID;
|
|
|
| double step=(double)TickLevelRows[rowIndex].level*_Point;
|
| return FloorGridForStep(price,step);
|
| }
|
|
|
| double TickRowPrice(const int rowIndex,const int grid)
|
| {
|
| if(rowIndex<0 || rowIndex>=TickLevelRowCount)
|
| return 0.0;
|
|
|
| double step=(double)TickLevelRows[rowIndex].level*_Point;
|
| return RayPriceForStep(grid,step);
|
| }
|
|
|
| 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<oldBid && 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<ModelTickCount;i++)
|
| {
|
| double oldBid=GetModelTickByIndex(i-1);
|
| double bid=GetModelTickByIndex(i);
|
|
|
| if(oldBid<=0.0 || bid<=0.0 || oldBid==bid)
|
| continue;
|
|
|
| MqlTick syntheticTick;
|
| ZeroMemory(syntheticTick);
|
| syntheticTick.bid=bid;
|
| syntheticTick.time_msc=0;
|
| RebuildArrowSerialOverride=MathMax(1,savedTickSerial-ModelTickCount+i+1);
|
|
|
| for(int row=0;row<TickLevelRowCount;row++)
|
| ProcessTickRow(row,oldBid,bid,syntheticTick,false);
|
| }
|
|
|
| RebuildArrowSerialOverride=-1;
|
| TickSerial=savedTickSerial;
|
| }
|
|
|
| bool ValidateTickEditedLevel(const int index,const int newLevel)
|
| {
|
| if(index<0 || index>=TickLevelRowCount ||
|
| !IsValidTickVisualLevel(newLevel))
|
| return false;
|
|
|
| if(index>0 && newLevel<=TickLevelRows[index-1].level)
|
| return false;
|
|
|
| if(index+1<TickLevelRowCount &&
|
| newLevel>=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<StringLen(textValue);i++)
|
| {
|
| ushort ch=StringGetCharacter(textValue,i);
|
| if(ch<'0' || ch>'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<TickLevelRowCount ?
|
| TickLevelRows[index+1].level : TickVisualLevelMax+TickVisualLevelStep);
|
|
|
| int candidate=current+TickVisualLevelStep;
|
|
|
| if(candidate>=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<TickLevelRowCount-1;i++)
|
| TickLevelRows[i]=TickLevelRows[i+1];
|
|
|
| TickLevelRowCount--;
|
|
|
| SaveTickVisualRows();
|
| RebuildTickVisualRowsFromHistory();
|
| RefreshGraph();
|
|
|
| Print("TPDR: deleted LEVEL ",removed," pt.");
|
| return true;
|
| }
|
|
|
|
|
| void AppendModelTick(const double bid)
|
| {
|
| if(bid<=0.0 || MaxModelHistoryTicks<1)
|
| return;
|
|
|
| if(ArraySize(ModelTickPrices)<MaxModelHistoryTicks)
|
| ArrayResize(ModelTickPrices,MaxModelHistoryTicks,MaxModelHistoryTicks);
|
|
|
| if(ModelTickCount<MaxModelHistoryTicks)
|
| {
|
| int index=(ModelTickHead+ModelTickCount)%MaxModelHistoryTicks;
|
| ModelTickPrices[index]=bid;
|
| ModelTickCount++;
|
| }
|
| else
|
| {
|
| ModelTickPrices[ModelTickHead]=bid;
|
| ModelTickHead=(ModelTickHead+1)%MaxModelHistoryTicks;
|
| }
|
| }
|
|
|
| double GetModelTickByIndex(const int index)
|
| {
|
| if(index<0 || index>=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;i<TickLevelRowCount;i++)
|
| {
|
| double step=(double)TickLevelRows[i].level*_Point;
|
| if(step<=0.0) continue;
|
| if(minimum<=0.0 || step<minimum) minimum=step;
|
| }
|
| return minimum;
|
| }
|
| int FloorGridForViewport(const double price)
|
| {
|
| double step=GetViewportLevelStepPrice();
|
| if(step<=0.0) return INVALID_GRID;
|
| double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
|
| return (int)MathFloor(position+1.0e-9);
|
| }
|
| int CeilGridForViewport(const double price)
|
| {
|
| double step=GetViewportLevelStepPrice();
|
| if(step<=0.0) return INVALID_GRID;
|
| double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
|
| return (int)MathCeil(position-1.0e-9);
|
| }
|
| double ViewportRayPrice(const int grid)
|
| {
|
| double step=GetViewportLevelStepPrice();
|
| if(step<=0.0) return 0.0;
|
| return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
|
| }
|
|
|
| void DeleteObjectsByPrefix()
|
| {
|
| ObjectsDeleteAll(0,Prefix,-1,-1);
|
| }
|
|
|
| void NotifyEvent(const string eventName,const string directionName,const double price,const long timeMsc)
|
| {
|
| if(!LiveMode)
|
| return;
|
|
|
| string msg=StringFormat("TPDR %s | %s | %s | Price=%s | TimeMsc=%I64d",
|
| _Symbol,eventName,directionName,DoubleToString(price,_Digits),timeMsc);
|
|
|
| Print(msg);
|
|
|
| if(EnableAlert)
|
| Alert(msg);
|
|
|
| if(EnableSoundNotification && StringLen(NotificationSound)>0)
|
| PlaySound(NotificationSound);
|
|
|
| if(EnablePushNotification)
|
| {
|
| ResetLastError();
|
| if(!SendNotification(msg))
|
| Print("TPDR SendNotification failed. Error=",GetLastError());
|
| }
|
|
|
| if(EnableEmailNotification)
|
| {
|
| ResetLastError();
|
| if(!SendMail("TPDR "+eventName+" "+directionName,msg))
|
| Print("TPDR SendMail failed. Error=",GetLastError());
|
| }
|
| }
|
|
|
| void AddTickToGraph(const double bid)
|
| {
|
| if(bid<=0.0 || MaxGraphSegments<1)
|
| return;
|
|
|
| // TickPrices[] заранее резервируется целиком. На глубокой истории
|
| // нельзя делать ArrayResize() на каждом тике: это создаёт лишние
|
| // перераспределения памяти и заметно увеличивает стартовую загрузку.
|
| if(ArraySize(TickPrices)<MaxGraphSegments)
|
| ArrayResize(TickPrices,MaxGraphSegments,MaxGraphSegments);
|
|
|
| if(TickCount<MaxGraphSegments)
|
| {
|
| int index=(TickHead+TickCount)%MaxGraphSegments;
|
| TickPrices[index]=bid;
|
| TickCount++;
|
| }
|
| else
|
| {
|
| TickPrices[TickHead]=bid;
|
| TickHead=(TickHead+1)%MaxGraphSegments;
|
| }
|
| }
|
|
|
| double GetTickPriceByVisibleIndex(const int index)
|
| {
|
| if(index<0 || index>=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;i<ArraySize(SignalMarks);i++)
|
| if(SignalMarks[i].up==up)
|
| directionCount++;
|
|
|
| if(directionCount<=keepCount)
|
| return;
|
|
|
| for(int i=0;i<ArraySize(SignalMarks);i++)
|
| {
|
| if(SignalMarks[i].up!=up)
|
| continue;
|
| for(int j=i;j<ArraySize(SignalMarks)-1;j++)
|
| SignalMarks[j]=SignalMarks[j+1];
|
| ArrayResize(SignalMarks,ArraySize(SignalMarks)-1);
|
| break;
|
| }
|
| }
|
|
|
| void RecordReversalArrow(const MqlTick &tick,const double price,const bool buy,
|
| const bool notify)
|
| {
|
| if(DrawReversalArrows)
|
| AddSignalMark(price,buy,true);
|
|
|
| if(notify)
|
| NotifyEvent(buy ? "BUY REVERSAL" : "SELL REVERSAL",
|
| buy ? "BUY" : "SELL",price,tick.time_msc);
|
| }
|
|
|
|
|
| // ВЕРТИКАЛЬНЫЙ VIEWPORT
|
| // Модель цены и отображение разделены.
|
| // High/Low BID используются при создании/перестроении вертикального масштаба.
|
| // Текущий LEVEL определяется отдельно и не является центром viewport.
|
| // Новый тик не центрирует окно и не перемещает уже существующую историю.
|
| void UpdateCurrentLevelGrid(const double bid)
|
| {
|
| if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
|
| return;
|
|
|
| int grid=FloorGridForViewport(bid);
|
| if(grid==INVALID_GRID)
|
| return;
|
|
|
| CurrentLevelGrid=grid;
|
| CurrentLevelGridInitialized=true;
|
| }
|
|
|
| // При достижении физического края переносится только viewport.
|
| void AlignLevelWindowToCurrentBid(const double bid)
|
| {
|
| if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
|
| return;
|
|
|
| if(!ScaleInitialized)
|
| {
|
| ScaleCenterGrid=FloorGridForViewport(bid);
|
| ViewportCenterPrice=ViewportRayPrice(ScaleCenterGrid);
|
| ScaleInitialized=true;
|
| return;
|
| }
|
|
|
| if(GraphScaleMax<=GraphScaleMin)
|
| return;
|
|
|
| double range=GraphScaleMax-GraphScaleMin;
|
| double center=(ViewportCenterPrice>0.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(bid<lowerEdge)
|
| {
|
| double targetCenter=bid+targetOffset;
|
| if(targetCenter<center)
|
| {
|
| ViewportCenterPrice=NormalizeDouble(targetCenter,_Digits);
|
| ScaleCenterGrid=CeilGridForViewport(ViewportCenterPrice);
|
| }
|
| }
|
| }
|
|
|
| void UpdateSubwindowScale(const double bid)
|
| {
|
| if(SubWindow<0 || bid<=0.0 || TickLevelRowCount<=0)
|
| return;
|
|
|
| UpdateCurrentLevelGrid(bid);
|
|
|
| if(!VerticalPanDragActive && !VerticalScaleManual)
|
| AlignLevelWindowToCurrentBid(bid);
|
|
|
| int viewStart=GetViewStartIndex();
|
| int viewCount=GetViewCount();
|
|
|
| double visibleMin=bid;
|
| double visibleMax=bid;
|
|
|
| for(int i=0;i<viewCount;i++)
|
| {
|
| double price=GetTickPriceByVisibleIndex(viewStart+i);
|
| if(price<=0.0)
|
| continue;
|
|
|
| visibleMin=MathMin(visibleMin,price);
|
| visibleMax=MathMax(visibleMax,price);
|
| }
|
|
|
| if(InitialArrowHistoryScalePending)
|
| {
|
| for(int i=0;i<ArraySize(SignalMarks);i++)
|
| {
|
| visibleMin=MathMin(visibleMin,SignalMarks[i].price);
|
| visibleMax=MathMax(visibleMax,SignalMarks[i].price);
|
| }
|
| }
|
|
|
| double minimumStep=0.0;
|
| for(int row=0;row<TickLevelRowCount;row++)
|
| {
|
| double step=(double)TickLevelRows[row].level*_Point;
|
| minimumStep=(minimumStep<=0.0 ? step : MathMin(minimumStep,step));
|
|
|
| int grid=TickRowGrid(row,bid);
|
| if(grid==INVALID_GRID)
|
| continue;
|
|
|
| double levelPrice=TickRowPrice(row,grid);
|
| visibleMin=MathMin(visibleMin,levelPrice);
|
| visibleMax=MathMax(visibleMax,levelPrice);
|
| }
|
|
|
| minimumStep=MathMax(minimumStep,_Point);
|
|
|
| double autoHalfRange=MathMax(bid-visibleMin,visibleMax-bid);
|
| double minimumHalfRange=minimumStep*3.0;
|
| autoHalfRange=MathMax(autoHalfRange,minimumHalfRange);
|
|
|
| double autoPadding=MathMax(autoHalfRange*0.05,minimumStep*0.10);
|
| autoHalfRange+=autoPadding;
|
|
|
| bool scaleContextChanged=!AutoScaleInitialized ||
|
| LastAutoScaleLevelsUp!=TickLevelRowCount;
|
|
|
| if(!VerticalScaleManual)
|
| {
|
| if(scaleContextChanged)
|
| VerticalScaleHalfRange=autoHalfRange;
|
|
|
| AutoScaleInitialized=true;
|
| LastAutoScaleLevelsUp=TickLevelRowCount;
|
| LastAutoScaleLevelsDown=0;
|
| }
|
| else
|
| {
|
| VerticalScaleHalfRange=MathMax(VerticalScaleHalfRange,
|
| minimumHalfRange+minimumStep*0.10);
|
| }
|
|
|
| double scaleHalfRange=MathMax(VerticalScaleHalfRange,
|
| minimumStep*0.20);
|
|
|
| double viewportCenter=(ViewportCenterPrice>0.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;n<targetPerDirection;n++)
|
| {
|
| long best=-1;
|
| for(int k=0;k<ArraySize(SignalMarks);k++)
|
| {
|
| bool wanted=(direction==1 ? SignalMarks[k].up : !SignalMarks[k].up);
|
| if(!wanted)
|
| continue;
|
|
|
| long serial=SignalMarks[k].serial;
|
| if(serial<firstGraphSerial || serial>TickSerial || serial>=cutoff)
|
| continue;
|
|
|
| if(serial>best)
|
| best=serial;
|
| }
|
|
|
| if(best<0)
|
| break;
|
|
|
| oldestSerial=(oldestSerial<0 ? best : MathMin(oldestSerial,best));
|
| newestSerial=(newestSerial<0 ? best : MathMax(newestSerial,best));
|
| cutoff=best;
|
| }
|
| }
|
|
|
| if(oldestSerial<0 || newestSerial<0)
|
| {
|
| ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
|
| ViewEndOffset=0;
|
| InitialViewLayoutActive=true;
|
| return;
|
| }
|
|
|
| long spanLong=newestSerial-oldestSerial+1;
|
| int requiredSpan=(int)MathMin((long)TickCount,spanLong);
|
| requiredSpan=MathMax(MinViewTicks,requiredSpan);
|
| requiredSpan=MathMin(requiredSpan,TickCount);
|
|
|
| ViewSpan=requiredSpan;
|
| ViewEndOffset=0;
|
| InitialViewLayoutActive=true;
|
| }
|
|
|
|
|
| // Сохраняем отдельную проверку состава истории: для стандартного значения
|
| // ReversalArrowsHistory=20 ожидается максимум 20 UP + 20 DOWN.
|
| void ValidateReversalArrowHistory()
|
| {
|
| if(ReversalArrowsHistory<=0)
|
| return;
|
|
|
| int upCount=0;
|
| int downCount=0;
|
|
|
| for(int i=0;i<ArraySize(SignalMarks);i++)
|
| {
|
| if(SignalMarks[i].up) upCount++;
|
| else downCount++;
|
| }
|
|
|
| if(upCount>ReversalArrowsHistory || 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;x<graphWidth;x+=8)
|
| {
|
| int x2=MathMin(x+4,graphWidth-1);
|
| TickCanvas.Line(x,y,x2,y,clr);
|
| }
|
| }
|
|
|
| void EnsureLevelPriceLabel(const string name,const int x,const int y,
|
| const color clr,const string textValue)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
|
| {
|
| Print("TPDR: level price label creation error=",GetLastError(),
|
| " Name=",name);
|
| return;
|
| }
|
|
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
|
| ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
|
| ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
|
| }
|
|
|
| ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
|
| ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
| ObjectSetString(0,name,OBJPROP_TEXT,textValue);
|
| }
|
|
|
| void DrawActiveLevelPriceLabels(const double bid)
|
| {
|
| return;
|
| }
|
|
|
| void DrawRayLayer(const double bid)
|
| {
|
| if(!CanvasReady || !DrawRayObjects ||
|
| GraphScaleMax<=GraphScaleMin || TickLevelRowCount<=0)
|
| return;
|
|
|
| int graphWidth=GetGraphWidth();
|
|
|
| for(int row=0;row<TickLevelRowCount;row++)
|
| {
|
| int grid=TickRowGrid(row,bid);
|
| if(grid==INVALID_GRID)
|
| continue;
|
|
|
| double price=TickRowPrice(row,grid);
|
| if(!IsGraphPriceVisible(price))
|
| continue;
|
|
|
| int y=GraphPriceToY(price);
|
| color rayColor=(row==TickLevelRowCount-1 ?
|
| CurrentRayColor : RayColor);
|
|
|
| if(row==TickLevelRowCount-1)
|
| TickCanvas.LineHorizontal(0,graphWidth-1,y,
|
| ColorToARGB(rayColor,230));
|
| else
|
| DrawDottedHorizontal(y,ColorToARGB(rayColor,210));
|
| }
|
| }
|
|
|
|
|
| int GetGraphWidth()
|
| {
|
| // Canvas занимает только левую область тикового графика.
|
| // Правая область полностью резервируется под цены и не рисуется Canvas-ом.
|
| return MathMax(1,CanvasWidth);
|
| }
|
|
|
| void DrawTickLayer(const double currentBid)
|
| {
|
| if(!CanvasReady || TickCount<=0 || currentBid<=0.0)
|
| return;
|
|
|
| int viewStart=GetViewStartIndex();
|
| int viewCount=GetViewCount();
|
| if(viewCount<=0)
|
| return;
|
|
|
| uint graphColor=ColorToARGB(TickGraphColor,255);
|
| int graphWidth=GetGraphWidth();
|
| int leftMargin=(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);
|
|
|
| if(viewCount==1)
|
| {
|
| int y=GraphPriceToY(currentBid);
|
| int x=leftMargin;
|
| TickCanvas.LineThick(x,y,MathMin(graphWidth-1,x+4),y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
|
| return;
|
| }
|
|
|
| bool havePrevious=false;
|
| int previousX=0;
|
| int previousY=0;
|
|
|
| for(int i=0;i<viewCount;i++)
|
| {
|
| int x=leftMargin+(int)MathRound((double)i*(double)(drawWidth-1)/(double)(viewCount-1));
|
| double price=GetTickPriceByVisibleIndex(viewStart+i);
|
|
|
| // Последняя точка всегда является фактическим текущим Bid.
|
| if(i==viewCount-1)
|
| price=currentBid;
|
|
|
| if(!IsGraphPriceVisible(price))
|
| {
|
| havePrevious=false;
|
| continue;
|
| }
|
|
|
| int y=GraphPriceToY(price);
|
|
|
| if(havePrevious)
|
| TickCanvas.LineThick(previousX,previousY,x,y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
|
|
|
| previousX=x;
|
| previousY=y;
|
| havePrevious=true;
|
| }
|
| }
|
|
|
| void DrawArrowMark(const int x,const int y,const bool up,const bool reversal,const color clr)
|
| {
|
| if(!CanvasReady)
|
| return;
|
|
|
| uint c=ColorToARGB(clr,255);
|
| int size=reversal ? 7 : 5;
|
|
|
| // (x,y) — точка события, то есть ОСТРИЁ стрелки.
|
| // Геометрия стрелки строится относительно этой точки и больше
|
| // не добавляет скрытого смещения по цене.
|
| if(up)
|
| {
|
| TickCanvas.Triangle(x,y,x-size,y+2*size,x+size,y+2*size,c);
|
| }
|
| else
|
| {
|
| TickCanvas.Triangle(x,y,x-size,y-2*size,x+size,y-2*size,c);
|
| }
|
| }
|
|
|
| void DrawBidMarker(const double bid)
|
| {
|
| if(!CanvasReady || bid<=0.0)
|
| return;
|
|
|
| int y=GraphPriceToY(bid);
|
| uint c=ColorToARGB(CurrentRayColor,255);
|
|
|
| int graphWidth=GetGraphWidth();
|
| int rightX=(InitialViewLayoutActive ?
|
| MathMax(0,graphWidth-MathMin(20,MathMax(0,graphWidth/2-1))) :
|
| MathMax(0,graphWidth-1));
|
| int markerWidth=MathMin(4,graphWidth);
|
| int markerStart=MathMax(0,rightX-markerWidth+1);
|
| TickCanvas.Line(markerStart,y,rightX,y,c);
|
| TickCanvas.Triangle(MathMax(2,rightX-12),y-5,
|
| MathMax(2,rightX-2),y,
|
| MathMax(2,rightX-12),y+5,c);
|
| }
|
|
|
| void DrawCurrentTickMarker(const double currentBid)
|
| {
|
| if(!CanvasReady || currentBid<=0.0)
|
| return;
|
|
|
| int graphWidth=GetGraphWidth();
|
| int x=(InitialViewLayoutActive ?
|
| MathMax(0,graphWidth-MathMin(20,MathMax(0,graphWidth/2-1))) :
|
| MathMax(0,graphWidth-1));
|
| int y=GraphPriceToY(currentBid);
|
|
|
| TickCanvas.FillCircle(x,y,3,ColorToARGB(TickGraphColor,255));
|
| }
|
|
|
| void DrawSignalLayer()
|
| {
|
| if(!CanvasReady || TickCount<=0 || ArraySize(SignalMarks)==0)
|
| return;
|
|
|
| // Рендер не меняет горизонтальный масштаб. Пользовательский zoom
|
| // полностью управляет ViewSpan/ViewEndOffset.
|
| long firstSerial=TickSerial-TickCount+1;
|
| int viewStart=GetViewStartIndex();
|
| int viewCount=GetViewCount();
|
| int viewEnd=viewStart+viewCount-1;
|
|
|
| for(int i=0;i<ArraySize(SignalMarks);i++)
|
| {
|
| long serial=SignalMarks[i].serial;
|
|
|
| if(serial<firstSerial || serial>TickSerial)
|
| continue;
|
|
|
| int index=(int)(serial-firstSerial);
|
| if(index<viewStart || index>viewEnd)
|
| continue;
|
|
|
| int relative=index-viewStart;
|
| int 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<ArraySize(names);i++)
|
| if(ObjectFind(0,names[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<left+bw &&
|
| localY>=top && localY<top+bh;
|
| }
|
|
|
| void EnsureResetButton(const string name,const string textValue)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
|
| {
|
| Print("TPDR: reset button creation error=",GetLastError(),
|
| " Name=",name);
|
| return;
|
| }
|
|
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
|
| ObjectSetInteger(0,name,OBJPROP_XSIZE,ResetButtonWidth);
|
| ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
|
| ObjectSetString(0,name,OBJPROP_TEXT,textValue);
|
| ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
|
| }
|
| }
|
|
|
| void EnsureZoomButton(const string name,const string textValue)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
|
| {
|
| Print("TPDR: zoom button creation error=",GetLastError(),
|
| " Name=",name);
|
| return;
|
| }
|
|
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
|
| ObjectSetInteger(0,name,OBJPROP_XSIZE,ZoomButtonWidth);
|
| ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
|
| ObjectSetString(0,name,OBJPROP_TEXT,textValue);
|
| ObjectSetString(0,name,OBJPROP_FONT,"Arial");
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,12);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
|
| }
|
| }
|
|
|
| void EnsureLevelCountButton(const string name,const string textValue)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0))
|
| {
|
| Print("TPDR: level 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,9);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
|
| }
|
| ObjectSetString(0,name,OBJPROP_TEXT,textValue);
|
| }
|
|
|
| void SetLevelButtonVisual(const string name,const bool selected)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| return;
|
|
|
| ObjectSetInteger(0,name,OBJPROP_STATE,selected);
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,selected ? clrGold : clrDimGray);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,selected ? clrBlack : clrWhite);
|
| }
|
|
|
| void EnsurePanelLabel(const string name,const string textValue)
|
| {
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_LABEL,0,0,0))
|
| {
|
| Print("TPDR: panel 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_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);
|
| }
|
| ObjectSetString(0,name,OBJPROP_TEXT,textValue);
|
| }
|
|
|
| void EnsureControlPanelBackground()
|
| {
|
| string name=Prefix+"CONTROL_PANEL_BG";
|
|
|
| if(ObjectFind(0,name)<0)
|
| {
|
| if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0))
|
| {
|
| Print("TPDR: control panel 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_ZORDER,5);
|
| }
|
|
|
| int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
|
| int panelWidth=MathMin(520,MathMax(360,chartWidth-16));
|
| int panelLeft=MathMax(8,chartWidth-panelWidth-8);
|
| int tableHeight=ControlPanelTop+LevelButtonHeight+2+18+
|
| TickLevelRowCount*(LevelButtonHeight+LevelButtonGap)+8;
|
|
|
| ObjectSetInteger(0,name,OBJPROP_XDISTANCE,panelLeft);
|
| ObjectSetInteger(0,name,OBJPROP_YDISTANCE,8);
|
| ObjectSetInteger(0,name,OBJPROP_XSIZE,panelWidth);
|
| ObjectSetInteger(0,name,OBJPROP_YSIZE,MathMax(60,tableHeight));
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clrDimGray);
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
|
| }
|
|
|
| void UpdateLevelTableLayout(const double bid)
|
| {
|
| if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0 || bid<=0.0)
|
| return;
|
|
|
| EnsureControlPanelBackground();
|
|
|
| int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
|
| int panelWidth=MathMin(520,MathMax(360,chartWidth-16));
|
| int panelLeft=MathMax(8,chartWidth-panelWidth-8);
|
|
|
| // Таблица LEVEL находится в главном ценовом окне справа.
|
| // График TickGraphic остаётся в отдельном подокне и занимает всю его ширину.
|
|
|
| // Старые кнопки LEVELS/DISTANCE больше не являются частью модели.
|
| string legacyNames[17]=
|
| {
|
| Prefix+"LEVEL_UP_3",Prefix+"LEVEL_UP_5",Prefix+"LEVEL_UP_10",
|
| Prefix+"LEVEL_DOWN_3",Prefix+"LEVEL_DOWN_5",Prefix+"LEVEL_DOWN_10",
|
| Prefix+"DISTANCE_10",Prefix+"DISTANCE_20",Prefix+"DISTANCE_30",
|
| Prefix+"DISTANCE_40",Prefix+"DISTANCE_50",Prefix+"DISTANCE_100",
|
| Prefix+"DISTANCE_150",Prefix+"DISTANCE_200",Prefix+"DISTANCE_300",
|
| Prefix+"DISTANCE_350",Prefix+"DISTANCE_400"
|
| };
|
|
|
| for(int k=0;k<17;k++)
|
| if(ObjectFind(0,legacyNames[k])>=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;displayRow<TickLevelRowCount;displayRow++)
|
| {
|
| int index=TickLevelRowCount-1-displayRow;
|
| int rowY=y+displayRow*(LevelButtonHeight+LevelButtonGap);
|
|
|
| string suffix=IntegerToString(index);
|
| string minusName=Prefix+"LEVEL_MINUS_"+suffix;
|
| string levelName=Prefix+"LEVEL_"+suffix;
|
| string plusName=Prefix+"LEVEL_PLUS_"+suffix;
|
| string dirName=Prefix+"LEVEL_DIR_"+suffix;
|
| string revName=Prefix+"LEVEL_REV_"+suffix;
|
| string alertName=Prefix+"LEVEL_ALERT_"+suffix;
|
| string lastDown2Name=Prefix+"LEVEL_LAST_DOWN_2_"+suffix;
|
| string lastDown1Name=Prefix+"LEVEL_LAST_DOWN_1_"+suffix;
|
| string lastUp2Name=Prefix+"LEVEL_LAST_UP_2_"+suffix;
|
| string lastUp1Name=Prefix+"LEVEL_LAST_UP_1_"+suffix;
|
|
|
| EnsureLevelCountButton(minusName,"-");
|
| EnsureLevelCountButton(plusName,"+");
|
| EnsurePanelLabel(dirName,CodeLockDirectionText(TickLevelRows[index].direction));
|
| EnsurePanelLabel(revName,
|
| TickLevelRows[index].reversalAtPrice>0.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<TickMaxVisualRows;i++)
|
| {
|
| string suffix=IntegerToString(i);
|
| string names[10]=
|
| {
|
| Prefix+"LEVEL_MINUS_"+suffix,
|
| Prefix+"LEVEL_"+suffix,
|
| Prefix+"LEVEL_PLUS_"+suffix,
|
| Prefix+"LEVEL_DIR_"+suffix,
|
| Prefix+"LEVEL_REV_"+suffix,
|
| Prefix+"LEVEL_ALERT_"+suffix,
|
| Prefix+"LEVEL_LAST_DOWN_2_"+suffix,
|
| Prefix+"LEVEL_LAST_DOWN_1_"+suffix,
|
| Prefix+"LEVEL_LAST_UP_2_"+suffix,
|
| Prefix+"LEVEL_LAST_UP_1_"+suffix
|
| };
|
|
|
| for(int k=0;k<10;k++)
|
| if(ObjectFind(0,names[k])>=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<TickLevelRowCount;row++)
|
| ProcessTickRow(row,oldBid,bid,tick,emitSignals);
|
|
|
| PreviousBid=bid;
|
| }
|
|
|
| void UpdateCursorFromArray(const MqlTick &ticks[],const int copied,const int startIndex)
|
| {
|
| if(copied<=0)
|
| return;
|
|
|
| long finalMsc=ticks[copied-1].time_msc;
|
|
|
| // CopyTicks() работает включительно: запрос от CursorMsc может вернуть
|
| // уже обработанные тики с той же миллисекундой. Поэтому храним точное количество
|
| // обработанных тиков внутри последней временной отметки.
|
| if(finalMsc==CursorMsc)
|
| {
|
| int processedAtFinal=0;
|
| for(int i=0;i<copied;i++)
|
| {
|
| if(ticks[i].time_msc==finalMsc && i>=startIndex)
|
| processedAtFinal++;
|
| }
|
|
|
| if(processedAtFinal>0)
|
| CursorSameMsc+=processedAtFinal;
|
| }
|
| else
|
| {
|
| CursorMsc=finalMsc;
|
|
|
| int finalCount=0;
|
| for(int i=copied-1;i>=0 && ticks[i].time_msc==finalMsc;i--)
|
| finalCount++;
|
|
|
| CursorSameMsc=finalCount;
|
| }
|
| }
|
|
|
| bool 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<copied;i++)
|
| if(ticks[i].time_msc<=AttachMsc)
|
| lastAttach=i;
|
|
|
| if(lastAttach<0)
|
| {
|
| BootstrapAttempts++;
|
| return false;
|
| }
|
|
|
| if(copied>=targetTicks && ticks[copied-1].time_msc<AttachMsc)
|
| {
|
| BootstrapAttempts++;
|
| return false;
|
| }
|
|
|
| ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),MathMax(100,MaxModelHistoryTicks));
|
| ModelTickCount=0;
|
| ModelTickHead=0;
|
| ClearDerivedState();
|
|
|
| // История и Live проходят один и тот же ProcessBidTick().
|
| for(int i=0;i<=lastAttach;i++)
|
| {
|
| if(ticks[i].bid>0.0)
|
| ProcessBidTick(ticks[i],false,true,true);
|
| }
|
|
|
| int attachCount=0;
|
| for(int i=lastAttach;i>=0 && ticks[i].time_msc==AttachMsc;i--)
|
| attachCount++;
|
|
|
| CursorMsc=AttachMsc;
|
| CursorSameMsc=attachCount;
|
| 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;i<copied;i++)
|
| ProcessBidTick(ticks[i],true,true,true);
|
|
|
| UpdateCursorFromArray(ticks,copied,start);
|
|
|
| if(copied<requestTried || processed==0)
|
| return true;
|
| }
|
|
|
| return true;
|
| }
|
|
|
|
|
| int OnInit()
|
| {
|
| 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>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<top+height;
|
| }
|
|
|
| void UpdateMouseScrollTarget(const int y)
|
| {
|
| bool inTickWindow=IsCursorInTickSubwindow(y);
|
|
|
| if(inTickWindow!=MouseInTickSubwindow)
|
| {
|
| MouseInTickSubwindow=inTickWindow;
|
|
|
| // Штатное управление терминала остаётся включённым над свечным графиком.
|
| // Над тиковым подокном оно отключается, чтобы колесо и перетаскивание
|
| // обслуживались собственным интерактивным графиком.
|
| ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)(!inTickWindow));
|
| }
|
| }
|
|
|
| bool IsCursorOnVerticalScaleHandle(const int x,const int y)
|
| {
|
| if(!MouseInTickSubwindow || CanvasWidth<=0 || CanvasHeight<=0)
|
| return false;
|
|
|
| int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
|
| if(chartWidth<=CanvasWidth)
|
| return false;
|
|
|
| // Вертикальный zoom начинается именно в отдельной правой области цен.
|
| // Ранее зона начиналась ещё на последних 18 px Canvas, из-за чего
|
| // горизонтальный drag Tick Graph конкурировал с вертикальным drag.
|
| // Теперь вся правая область является однозначной зоной вертикального
|
| // масштабирования, а после начала drag масштаб сохраняется независимо
|
| // от дальнейшего движения курсора по подокну.
|
| int left=CanvasWidth;
|
| int right=chartWidth;
|
| return x>=left && x<right;
|
| }
|
|
|
| void BeginVerticalPanDrag(const int y)
|
| {
|
| VerticalPanDragActive=true;
|
| VerticalScaleManual=true;
|
| VerticalPanDragStartY=y;
|
| VerticalPanDragStartCenterPrice=(ViewportCenterPrice>0.0 ?
|
| ViewportCenterPrice :
|
| ViewportRayPrice(ScaleCenterGrid));
|
| }
|
|
|
| void HandleVerticalPanDrag(const int y)
|
| {
|
| if(!VerticalPanDragActive || CanvasHeight<=1 ||
|
| GraphScaleMax<=GraphScaleMin)
|
| return;
|
|
|
| int deltaY=y-VerticalPanDragStartY;
|
| double pricePerPixel=(GraphScaleMax-GraphScaleMin)/
|
| (double)MathMax(1,CanvasHeight-1);
|
| // Мышь вверх -> график вверх; мышь вниз -> график вниз.
|
| ViewportCenterPrice=NormalizeDouble(
|
| VerticalPanDragStartCenterPrice+(double)deltaY*pricePerPixel,_Digits);
|
| ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
|
| VerticalPanDragStartY=y;
|
| VerticalPanDragStartCenterPrice=ViewportCenterPrice;
|
| VerticalPanDragActive=true;
|
| RefreshGraph();
|
| }
|
|
|
| void EndVerticalPanDrag()
|
| {
|
| VerticalPanDragActive=false;
|
| }
|
|
|
| void BeginVerticalScaleDrag(const int y)
|
| {
|
| VerticalScaleDragActive=true;
|
| VerticalScaleDragStartY=y;
|
| VerticalScaleDragStartHalfRange=MathMax(VerticalScaleHalfRange,GetViewportLevelStepPrice()*0.20);
|
| VerticalScaleManual=true;
|
| }
|
|
|
| void HandleVerticalScaleDrag(const int y)
|
| {
|
| if(!VerticalScaleDragActive)
|
| return;
|
|
|
| int deltaY=y-VerticalScaleDragStartY;
|
|
|
| // Вверх = приближение по вертикали, вниз = отдаление.
|
| // Масштаб рассчитывается относительно исходной точки drag, поэтому
|
| // несколько mouse-move событий не накапливают округление.
|
| double totalDeltaY=(double)(y-VerticalScaleDragStartY);
|
| double factor=MathExp(totalDeltaY*VerticalScaleDragSensitivity);
|
| double newHalfRange=VerticalScaleDragStartHalfRange*factor;
|
|
|
| int upCount=MathMax(1,(TickLevelRowCount/2));
|
| int downCount=MathMax(1,(TickLevelRowCount-(TickLevelRowCount/2)));
|
| double minimumHalfRange=GetViewportLevelStepPrice()*MathMax(1,MathMax(upCount,downCount));
|
| minimumHalfRange+=GetViewportLevelStepPrice()*0.10;
|
|
|
| double maximumHalfRange=MathMax(minimumHalfRange*2.0,GetViewportLevelStepPrice()*2000.0);
|
|
|
| VerticalScaleHalfRange=MathMax(minimumHalfRange,
|
| MathMin(maximumHalfRange,newHalfRange));
|
|
|
| RefreshGraph();
|
| }
|
|
|
| void EndVerticalScaleDrag()
|
| {
|
| VerticalScaleDragActive=false;
|
| }
|
|
|
| void 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<TickLevelRowCount)
|
| {
|
| TickLevelRows[index].alertEnabled=!TickLevelRows[index].alertEnabled;
|
| RefreshGraph();
|
| }
|
| return;
|
| }
|
|
|
| string levelPrefix=Prefix+"LEVEL_";
|
| if(StringFind(sparam,levelPrefix)==0 &&
|
| StringFind(sparam,Prefix+"LEVEL_EDIT_")!=0 &&
|
| StringFind(sparam,Prefix+"LEVEL_DIR_")!=0 &&
|
| StringFind(sparam,Prefix+"LEVEL_REV_")!=0 &&
|
| StringFind(sparam,Prefix+"LEVEL_ALERT_")!=0)
|
| {
|
| int index=(int)StringToInteger(
|
| StringSubstr(sparam,StringLen(levelPrefix)));
|
|
|
| int x=(int)ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE);
|
| int y=(int)ObjectGetInteger(0,sparam,OBJPROP_YDISTANCE);
|
|
|
| BeginTickLevelEdit(index,x,y,LevelButtonWidth,LevelButtonHeight);
|
| return;
|
| }
|
|
|
| if(sparam==Prefix+"ZOOM_MINUS" ||
|
| sparam==Prefix+"ZOOM_PLUS" ||
|
| sparam==Prefix+"RESET_CURRENT")
|
| {
|
| HandleZoomButtonClick(sparam);
|
| return;
|
| }
|
| }
|
|
|
| if(id==CHARTEVENT_MOUSE_WHEEL)
|
| {
|
| int flags=(int)(lparam>>32);
|
| int xCursor=(int)(short)lparam;
|
| int yCursor=(int)(short)(lparam>>16);
|
|
|
| UpdateMouseScrollTarget(yCursor);
|
|
|
| if(!MouseInTickSubwindow)
|
| return;
|
|
|
| InitialViewLayoutActive=false;
|
|
|
| if((flags&0x0004)!=0)
|
| {
|
| int span=GetViewCount();
|
| int step=MathMax(1,span/10);
|
| int maxOffset=MathMax(0,TickCount-span);
|
|
|
| ViewEndOffset+=(dparam>0.0 ? -step : step);
|
| ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
|
| }
|
| else
|
| {
|
| HandleHorizontalZoom(xCursor,(int)dparam);
|
| ViewEndOffset=0;
|
| }
|
|
|
| RefreshGraph();
|
| return;
|
| }
|
|
|
| if(id==CHARTEVENT_MOUSE_MOVE)
|
| {
|
| int x=(int)lparam;
|
| int y=(int)dparam;
|
| int state=(int)StringToInteger(sparam);
|
| bool leftDown=((state&1)!=0);
|
|
|
| UpdateMouseScrollTarget(y);
|
|
|
| if(!MouseInTickSubwindow)
|
| DragActive=false;
|
|
|
| if(VerticalScaleDragActive)
|
| {
|
| if(leftDown)
|
| {
|
| ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
|
| HandleVerticalScaleDrag(y);
|
| DragActive=false;
|
| VerticalPanDragActive=false;
|
| return;
|
| }
|
|
|
| EndVerticalScaleDrag();
|
| UpdateMouseScrollTarget(y);
|
| }
|
| else if(VerticalPanDragActive)
|
| {
|
| if(leftDown)
|
| {
|
| ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
|
| HandleVerticalPanDrag(y);
|
| DragActive=false;
|
| return;
|
| }
|
|
|
| EndVerticalPanDrag();
|
| UpdateMouseScrollTarget(y);
|
| }
|
|
|
| if(leftDown && MouseInTickSubwindow &&
|
| IsCursorOnVerticalScaleHandle(x,y))
|
| {
|
| BeginVerticalScaleDrag(y);
|
| HandleVerticalScaleDrag(y);
|
| DragActive=false;
|
| VerticalPanDragActive=false;
|
| return;
|
| }
|
|
|
| if(leftDown && MouseInTickSubwindow &&
|
| IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
|
| {
|
| ResetButtonHeld=true;
|
| ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,true);
|
| }
|
| else if(!leftDown)
|
| {
|
| ResetButtonHeld=false;
|
| if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
|
| ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
|
| }
|
| else if(!IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
|
| {
|
| ResetButtonHeld=false;
|
| if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
|
| ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
|
| }
|
|
|
| if(leftDown && MouseInTickSubwindow && !ResetButtonHeld)
|
| {
|
| if(!DragActive && !VerticalPanDragActive)
|
| {
|
| if(!PointerDragTracking)
|
| {
|
| PointerDragTracking=true;
|
| DragStartX=x;
|
| DragStartOffset=ViewEndOffset;
|
| VerticalPanDragStartY=y;
|
| }
|
|
|
| int dx=x-DragStartX;
|
| int dy=y-VerticalPanDragStartY;
|
|
|
| if(MathAbs((double)dy)>MathAbs((double)dx) && MathAbs(dy)>=4)
|
| {
|
| BeginVerticalPanDrag(y);
|
| DragActive=false;
|
| HandleVerticalPanDrag(y);
|
| return;
|
| }
|
|
|
| if(MathAbs(dx)>=4)
|
| DragActive=true;
|
| }
|
|
|
| if(VerticalPanDragActive)
|
| {
|
| HandleVerticalPanDrag(y);
|
| return;
|
| }
|
|
|
| if(DragActive)
|
| {
|
| HandleHorizontalDrag(x);
|
| RefreshGraph();
|
| }
|
| }
|
| else if(!leftDown || !MouseInTickSubwindow)
|
| {
|
| DragActive=false;
|
| PointerDragTracking=false;
|
| EndVerticalPanDrag();
|
| }
|
|
|
| return;
|
| }
|
|
|
| if(id==CHARTEVENT_CHART_CHANGE)
|
| 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;
|
| }
|