mql5/Indicators/CodeLock/CodeLock-TickGraphic.mq5
3196 lines
102 KiB
MQL5
rytikov0285
2026-11-05 12:52:36 +03:00
#include <Canvas\Canvas.mqh>
#property strict
#property indicator_separate_window
#property indicator_plots 0
#property version "3.00"
#property description "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;
}