4110 lines
134 KiB
MQL5
#include <Canvas\Canvas.mqh>
| |||
| |||
#property strict
| |||
#property indicator_separate_window
| |||
#property indicator_plots 0
| |||
#property version "3.00"
| |||
#property description "Unified CodeLock Dashboard + Tick Graphic"
| |||
| |||
#define INVALID_GRID (-2147483647)
| |||
#define CODELOCK_KEY_ESCAPE 0x1B
| |||
#define DASH_MIN_VISUAL_ROWS 5
| |||
#define DASH_VISUAL_ROW_STEP 10
| |||
#define DASH_VISUAL_LEVEL_MAX 2000
| |||
#define DASH_VISUAL_ROW_MAX 40
| |||
#define DASH_DASHBOARD_ROWS 40
| |||
| |||
// ============================================================================
| |||
// CODELOCK-DASHBOARD-TICKGRAPHIC
| |||
// ============================================================================
| |||
| |||
input double StartPrice=1; // Начальная цена, от которой строится сетка уровней
| |||
input int BufferPoints=0; // Единый BUFFER подтверждения CROSSING в points
| |||
input int HistoryChunkSize=5000; // Размер блока исторических тиков для обработки
| |||
input int HistoryGraphTicks=700000; // Количество исторических тиков для первоначального графика
| |||
input int HistoryBootstrapTicks=700000; // Количество исторических тиков для восстановления состояния до запуска
| |||
input int MaxGraphSegments=2000000; // Максимальное количество тиков графика; согласовано с лимитом расчётной истории
| |||
input int MaxModelHistoryTicks=2000000; // Максимальное количество тиков, доступных для полного пересчета модели
| |||
input bool DrawReversalArrows=true; // Показывать последние REVERSAL: N UP + N DOWN на каждый LEVEL
| |||
input int ReversalArrowsHistory=20; // Количество последних REVERSAL отдельно по направлению и LEVEL
| |||
input bool DrawRayObjects=true; // Показывать ценовые лучи
| |||
input int PriceLabelsCount=3; // Legacy input: currently unused; retained for saved parameter-set compatibility
| |||
input int InitialViewTicks=50000; // Количество тиков в первоначальном масштабе
| |||
input int MinViewTicks=5; // Минимальное количество тиков при максимальном приближении
| |||
input int BidLabelOffset=8; // Legacy input: currently unused; retained for saved parameter-set compatibility
| |||
// Вертикальный viewport фиксируется при первом построении: новые тики не сдвигают
| |||
// масштаб и исторические цены по вертикали. Цены вне диапазона не расширяют окно.
| |||
// Уровни динамически появляются/исчезают по текущему диапазону BID.
| |||
input bool EnableAlert=true; // Разрешить экранные уведомления
| |||
input bool EnablePushNotification=true; // Разрешить Push-уведомления
| |||
input bool EnableEmailNotification=true; // Разрешить уведомления по Email
| |||
input bool EnableSoundNotification=true; // Разрешить звуковые уведомления
| |||
input string NotificationSound="alert.wav"; // Имя звукового файла уведомления
| |||
input color TickGraphColor=clrDodgerBlue; // Цвет тикового графика
| |||
input color RayColor=clrSilver; // Цвет обычных лучей сетки
| |||
input color CurrentRayColor=clrGold; // Цвет текущего уровня и Bid
| |||
input color UpCrossColor=clrGreen; // Цвет стрелок пересечения вверх
| |||
input color DownCrossColor=clrRed; // Цвет стрелок пересечения вниз
| |||
input color BuyColor=clrBlue; // Legacy input: currently unused; retained for saved parameter-set compatibility
| |||
input color SellColor=clrRed; // Legacy input: currently unused; retained for saved parameter-set compatibility
| |||
| |||
string Prefix="TPDR_";
| |||
string ShortName="CodeLock-Dashboard-TickGraphic";
| |||
| |||
// Forward declarations: MQL5 requires a declaration before an earlier call.
| |||
void DashFireLevelAlert(const int stateIndex,const int grid,const int direction);
| |||
void DashRenderDashboard(const double bid);
| |||
void HandleHorizontalZoom(const int xCursor,const int delta);
| |||
| |||
void NotifyEvent(const string eventName,const string directionName,const int level,const double price,const long timeMsc);
| |||
double GetModelTickByIndex(const int index);
| |||
void RecordReversalArrow(const double price,const bool buy,const int level=0);
| |||
int GetViewStartIndex();
| |||
int GetViewCount();
| |||
int GetGraphWidth();
| |||
void EnsurePanelLabel(const string name,const string textValue);
| |||
void RefreshGraph();
| |||
void DashSetAlertButtonState(const int index);
| |||
void SyncTickVisualRowsFromDashboard();
| |||
bool HistoryDerivedStateComplete();
| |||
bool RestoreChartEventSettings();
| |||
| |||
// Рабочее состояние количества уровней. Inputs задают начальное значение,
| |||
// а кнопки изменяют именно это состояние без изменения input-параметров.
| |||
double PreviousBid=0.0;
| |||
bool HavePreviousBid=false;
| |||
bool LiveMode=false;
| |||
bool BootstrapComplete=false;
| |||
int SubWindow=-1;
| |||
| |||
| |||
long AttachMsc=0;
| |||
long CursorMsc=1;
| |||
int CursorSameMsc=0;
| |||
int BootstrapAttempts=0;
| |||
int HistoryBootstrapTargetTicks=0;
| |||
uint BootstrapLastAttemptTick=0;
| |||
bool BootstrapRetryPending=false;
| |||
int BootstrapLastLoggedError=0;
| |||
uint BootstrapLastErrorLogTick=0;
| |||
const uint BootstrapRetryDelayMs=1000;
| |||
const uint BootstrapErrorLogIntervalMs=60000;
| |||
| |||
CCanvas TickCanvas;
| |||
bool CanvasReady=false;
| |||
int CanvasWidth=0;
| |||
int CanvasHeight=0;
| |||
int CanvasSubWindow=-1;
| |||
double ScaleMin=0.0;
| |||
double ScaleMax=0.0;
| |||
double GraphScaleMin=0.0;
| |||
double GraphScaleMax=0.0;
| |||
int ScaleCenterGrid=0;
| |||
double ViewportCenterPrice=0.0;
| |||
bool ScaleInitialized=false;
| |||
| |||
// Текущий LEVEL, определённый фактическим BID.
| |||
// Это состояние отображения LEVEL, а не центр вертикального viewport.
| |||
int CurrentLevelGrid=INVALID_GRID;
| |||
bool CurrentLevelGridInitialized=false;
| |||
int ViewSpan=0;
| |||
int ViewEndOffset=0;
| |||
bool DragActive=false;
| |||
int DragStartX=0;
| |||
int DragStartOffset=0;
| |||
| |||
// После ручного масштабирования AutoScale сохраняет выбранный диапазон.
| |||
bool VerticalScaleManual=false;
| |||
bool VerticalScaleDragActive=false;
| |||
bool VerticalPanDragActive=false;
| |||
bool PointerDragTracking=false;
| |||
int VerticalPanDragStartY=0;
| |||
double VerticalPanDragStartCenterPrice=0.0;
| |||
bool InitialArrowHistoryScalePending=false;
| |||
int VerticalScaleDragStartY=0;
| |||
double VerticalScaleDragStartHalfRange=0.0;
| |||
double VerticalScaleHalfRange=0.0;
| |||
| |||
const int VerticalScaleHandleWidth=18;
| |||
const double VerticalScaleDragSensitivity=0.008;
| |||
| |||
const int ZoomButtonWidth=36;
| |||
const int ResetButtonWidth=64;
| |||
const int ZoomButtonHeight=28;
| |||
const int ZoomButtonGap=6;
| |||
const int PriceScaleWidth=85;
| |||
const int GraphTopReserved=0;
| |||
const int GraphBottomReserved=0;
| |||
const double GraphBidSafeLower=0.22;
| |||
const double GraphBidSafeUpper=0.78;
| |||
const double GraphBidTargetLower=0.32;
| |||
const double GraphBidTargetUpper=0.68;
| |||
const int LevelButtonWidth=32;
| |||
const int LevelButtonHeight=22;
| |||
const int LevelButtonGap=3;
| |||
const int GraphLevelsPerSideMin=5;
| |||
const int GraphLevelsPerSideMax=DASH_VISUAL_LEVEL_MAX;
| |||
int GraphLevelsPerSide=10;
| |||
const int GraphDisplayedLevelCountMin=1;
| |||
const int GraphDisplayedLevelCountMax=100;
| |||
int GraphDisplayedLevelCount=5;
| |||
string GraphDisplayedLevelCountKey="";
| |||
string LastGraphLabelNames[];
| |||
int LastGraphLabelNameCount=0;
| |||
bool LegacyGraphLabelsCleaned=false;
| |||
const int ControlPanelTop=24;
| |||
const int DistanceButtonWidth=36;
| |||
const int DistanceButtonHeight=22;
| |||
const int DistanceButtonGap=3;
| |||
const int ControlPanelLabelWidth=52;
| |||
const int ControlPanelPadding=5;
| |||
const int ControlPanelRowGap=4;
| |||
const int ControlPanelHeight=520;
| |||
// Истинно после однократного расчёта первоначального вертикального диапазона.
| |||
bool AutoScaleInitialized=false;
| |||
| |||
bool ResetButtonHeld=false;
| |||
bool PreviousChartMouseScroll=true;
| |||
bool PreviousChartEventMouseMove=false;
| |||
bool PreviousChartEventMouseWheel=false;
| |||
bool ChartEventSettingsCaptured=false;
| |||
bool MouseScrollSetErrorLogged=false;
| |||
bool DashboardHasPublishedState=false;
| |||
bool LegacyPriceObjectsCleaned=false;
| |||
bool MouseInTickSubwindow=false;
| |||
bool InitialViewLayoutActive=false;
| |||
| |||
// Сохранённый снимок расчётного состояния используется после REASON_CHARTCHANGE.
| |||
// Он не является отдельным источником истины: кэш — сериализованная копия
| |||
// ModelTickPrices[] и производного состояния.
| |||
| |||
double TickPrices[];
| |||
int TickCount=0;
| |||
int GraphHistoryCapacity=2000000;
| |||
int TickHead=0;
| |||
long TickSerial=0;
| |||
| |||
// Единственный источник истины для пересчета модели.
| |||
// Графический хвост TickPrices[] является только представлением этой истории.
| |||
double ModelTickPrices[];
| |||
double ModelTickAsks[];
| |||
int ModelTickCount=0;
| |||
int ModelTickHead=0;
| |||
| |||
struct SignalMark
| |||
{
| |||
long serial;
| |||
double price;
| |||
int level; // LEVEL, на сетке которого подтверждён REVERSAL
| |||
bool up;
| |||
bool reversal;
| |||
};
| |||
| |||
SignalMark SignalMarks[];
| |||
| |||
// LEVEL — независимая сетка; отдельной модели DISTANCE нет.
| |||
| |||
struct TickLevelRow
| |||
{
| |||
int level;
| |||
int direction;
| |||
int directionGrid;
| |||
int lastCrossedGrid;
| |||
int lastCrossingDirection;
| |||
int consecutiveCrossings;
| |||
double lastReversalPrice;
| |||
double reversalAtPrice;
| |||
double lastDown1Price;
| |||
double lastDown2Price;
| |||
double lastUp1Price;
| |||
double lastUp2Price;
| |||
double phaseMinBid;
| |||
double phaseMaxAsk;
| |||
bool alertEnabled;
| |||
};
| |||
| |||
TickLevelRow TickLevelRows[];
| |||
int TickLevelRowCount=0;
| |||
void ResetTickLevelRowState(TickLevelRow &row)
| |||
{
| |||
row.direction=0;
| |||
row.directionGrid=INVALID_GRID;
| |||
row.lastCrossedGrid=INVALID_GRID;
| |||
row.lastCrossingDirection=0;
| |||
row.consecutiveCrossings=0;
| |||
row.lastReversalPrice=0.0;
| |||
row.reversalAtPrice=0.0;
| |||
row.lastDown1Price=0.0;
| |||
row.lastDown2Price=0.0;
| |||
row.lastUp1Price=0.0;
| |||
row.lastUp2Price=0.0;
| |||
row.phaseMinBid=0.0;
| |||
row.phaseMaxAsk=0.0;
| |||
row.alertEnabled=false;
| |||
}
| |||
| |||
| |||
| |||
| |||
| |||
| |||
| |||
void ResetAllTickVisualRowStates()
| |||
{
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
int FloorGridForStep(const double price,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return INVALID_GRID;
| |||
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathFloor(position);
| |||
}
| |||
| |||
int CeilGridForStep(const double price,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return INVALID_GRID;
| |||
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathCeil(position);
| |||
}
| |||
| |||
double RayPriceForStep(const int grid,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return 0.0;
| |||
| |||
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
int TickRowGrid(const int rowIndex,const double price)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return INVALID_GRID;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
return FloorGridForStep(price,step);
| |||
}
| |||
| |||
double TickRowPrice(const int rowIndex,const int grid)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return 0.0;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
return RayPriceForStep(grid,step);
| |||
}
| |||
| |||
// REVERSAL AT — соседний уровень, обратное пересечение которого подтверждает REVERSAL.
| |||
void FinalizeTickRowsFromHistory()
| |||
{
| |||
if(TickLevelRowCount<=0 || ModelTickCount<2)
| |||
return;
| |||
| |||
// Если история не содержит CROSSING, начальное направление берётся из последнего изменения BID.
| |||
double lastBid=GetModelTickByIndex(ModelTickCount-1);
| |||
double previousBid=GetModelTickByIndex(ModelTickCount-2);
| |||
if(lastBid<=0.0 || previousBid<=0.0 || lastBid==previousBid)
| |||
return;
| |||
| |||
int fallbackDirection=(lastBid>previousBid ? 1 : -1);
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
if(TickLevelRows[i].direction!=0)
| |||
continue;
| |||
| |||
double step=(double)TickLevelRows[i].level*_Point;
| |||
if(step<=0.0)
| |||
continue;
| |||
| |||
int direction=fallbackDirection;
| |||
int grid=(direction>0 ? CeilGridForStep(lastBid,step) :
| |||
FloorGridForStep(lastBid,step));
| |||
if(grid==INVALID_GRID)
| |||
continue;
| |||
| |||
TickLevelRows[i].direction=direction;
| |||
TickLevelRows[i].directionGrid=grid;
| |||
TickLevelRows[i].lastCrossedGrid=grid;
| |||
TickLevelRows[i].lastCrossingDirection=direction;
| |||
TickLevelRows[i].consecutiveCrossings=0;
| |||
UpdateTickRowReversalAt(i);
| |||
}
| |||
}
| |||
| |||
bool HistoryDerivedStateComplete()
| |||
{
| |||
// Published Dashboard values must come from the calculated history model.
| |||
// Do not publish temporary dash placeholders while history is still loading.
| |||
if(TickLevelRowCount<=0 || ModelTickCount<2)
| |||
return false;
| |||
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
if(TickLevelRows[i].direction==0 || TickLevelRows[i].reversalAtPrice<=0.0)
| |||
return false;
| |||
if(TickLevelRows[i].lastDown1Price<=0.0 || TickLevelRows[i].lastDown2Price<=0.0)
| |||
return false;
| |||
if(TickLevelRows[i].lastUp1Price<=0.0 || TickLevelRows[i].lastUp2Price<=0.0)
| |||
return false;
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
void UpdateTickRowReversalAt(const int rowIndex)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return;
| |||
| |||
if(TickLevelRows[rowIndex].direction==0 || TickLevelRows[rowIndex].directionGrid==INVALID_GRID)
| |||
{
| |||
TickLevelRows[rowIndex].reversalAtPrice=0.0;
| |||
return;
| |||
}
| |||
| |||
TickLevelRows[rowIndex].reversalAtPrice=TickRowPrice(rowIndex,
| |||
TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction);
| |||
}
| |||
| |||
bool RegisterTickRowCrossing(const int rowIndex,const int grid,
| |||
const int crossingDirection,const MqlTick &tick,
| |||
const bool emitSignals)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return false;
| |||
| |||
TickLevelRows[rowIndex].lastCrossedGrid=grid;
| |||
TickLevelRows[rowIndex].lastCrossingDirection=crossingDirection;
| |||
| |||
if(TickLevelRows[rowIndex].direction==0)
| |||
{
| |||
TickLevelRows[rowIndex].direction=crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings=1;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
| |||
if(crossingDirection<0)
| |||
TickLevelRows[rowIndex].phaseMinBid=tick.bid;
| |||
else if(tick.ask>0.0)
| |||
TickLevelRows[rowIndex].phaseMaxAsk=tick.ask;
| |||
return false;
| |||
}
| |||
| |||
if(crossingDirection==TickLevelRows[rowIndex].direction)
| |||
{
| |||
// CROSSING того же направления обновляет направление.
| |||
// REVERSAL подтверждается только на соседнем grid в обратную сторону.
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings++;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
return false;
| |||
}
| |||
| |||
int reversalGrid=TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction;
| |||
if(grid!=reversalGrid)
| |||
return false;
| |||
| |||
int oldDirection=TickLevelRows[rowIndex].direction;
| |||
TickLevelRows[rowIndex].direction=crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings=1;
| |||
TickLevelRows[rowIndex].lastReversalPrice=TickRowPrice(rowIndex,grid);
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
| |||
if(crossingDirection<0)
| |||
{
| |||
TickLevelRows[rowIndex].lastDown2Price=TickLevelRows[rowIndex].lastDown1Price;
| |||
TickLevelRows[rowIndex].lastDown1Price=TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
else
| |||
{
| |||
TickLevelRows[rowIndex].lastUp2Price=TickLevelRows[rowIndex].lastUp1Price;
| |||
TickLevelRows[rowIndex].lastUp1Price=TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
| |||
if(oldDirection>0)
| |||
TickLevelRows[rowIndex].phaseMaxAsk=0.0;
| |||
else
| |||
TickLevelRows[rowIndex].phaseMinBid=0.0;
| |||
| |||
RecordReversalArrow(TickLevelRows[rowIndex].lastReversalPrice,
| |||
crossingDirection>0,TickLevelRows[rowIndex].level);
| |||
return true;
| |||
}
| |||
| |||
void ProcessTickRow(const int rowIndex,const double oldBid,const double bid,
| |||
const MqlTick &tick,const bool emitSignals)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount || bid==oldBid)
| |||
return;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
if(step<=0.0)
| |||
return;
| |||
| |||
double buffer=(double)MathMax(0,BufferPoints)*_Point;
| |||
| |||
if(bid>oldBid)
| |||
{
| |||
int first=FloorGridForStep(oldBid-buffer,step)-1;
| |||
int last=FloorGridForStep(bid-buffer,step)+1;
| |||
| |||
for(int grid=first;grid<=last;grid++)
| |||
{
| |||
double confirmationPrice=NormalizeDouble(
| |||
RayPriceForStep(grid,step)+buffer,_Digits);
| |||
| |||
if(confirmationPrice>oldBid && confirmationPrice<=bid)
| |||
{
| |||
bool reversal=RegisterTickRowCrossing(rowIndex,grid,1,tick,emitSignals);
| |||
if(reversal && TickLevelRows[rowIndex].alertEnabled && emitSignals)
| |||
{
| |||
NotifyEvent("REVERSAL","UP",
| |||
TickLevelRows[rowIndex].level,
| |||
TickRowPrice(rowIndex,grid),tick.time_msc);
| |||
DashFireLevelAlert(rowIndex,grid,1);
| |||
TickLevelRows[rowIndex].alertEnabled=false;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
else
| |||
{
| |||
int first=CeilGridForStep(oldBid+buffer,step)+1;
| |||
int last=CeilGridForStep(bid+buffer,step)-1;
| |||
| |||
for(int grid=first;grid>=last;grid--)
| |||
{
| |||
double confirmationPrice=NormalizeDouble(
| |||
RayPriceForStep(grid,step)-buffer,_Digits);
| |||
| |||
if(confirmationPrice<oldBid && confirmationPrice>=bid)
| |||
{
| |||
bool reversal=RegisterTickRowCrossing(rowIndex,grid,-1,tick,emitSignals);
| |||
if(reversal && TickLevelRows[rowIndex].alertEnabled && emitSignals)
| |||
{
| |||
NotifyEvent("REVERSAL","DOWN",
| |||
TickLevelRows[rowIndex].level,
| |||
TickRowPrice(rowIndex,grid),tick.time_msc);
| |||
DashFireLevelAlert(rowIndex,grid,-1);
| |||
TickLevelRows[rowIndex].alertEnabled=false;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
}
| |||
void AppendModelTick(const double bid,const double ask)
| |||
{
| |||
if(bid<=0.0 || MaxModelHistoryTicks<1)
| |||
return;
| |||
| |||
if(ArraySize(ModelTickPrices)<MaxModelHistoryTicks)
| |||
ArrayResize(ModelTickPrices,MaxModelHistoryTicks,MaxModelHistoryTicks);
| |||
if(ArraySize(ModelTickAsks)<MaxModelHistoryTicks)
| |||
ArrayResize(ModelTickAsks,MaxModelHistoryTicks,MaxModelHistoryTicks);
| |||
| |||
if(ModelTickCount<MaxModelHistoryTicks)
| |||
{
| |||
int index=(ModelTickHead+ModelTickCount)%MaxModelHistoryTicks;
| |||
ModelTickPrices[index]=bid;
| |||
ModelTickAsks[index]=(ask>0.0 ? ask : 0.0);
| |||
ModelTickCount++;
| |||
}
| |||
else
| |||
{
| |||
ModelTickPrices[ModelTickHead]=bid;
| |||
ModelTickAsks[ModelTickHead]=(ask>0.0 ? ask : 0.0);
| |||
ModelTickHead=(ModelTickHead+1)%MaxModelHistoryTicks;
| |||
}
| |||
}
| |||
| |||
double GetModelTickByIndex(const int index)
| |||
{
| |||
if(index<0 || index>=ModelTickCount)
| |||
return 0.0;
| |||
| |||
return ModelTickPrices[(ModelTickHead+index)%MaxModelHistoryTicks];
| |||
}
| |||
| |||
double GetModelAskByIndex(const int index)
| |||
{
| |||
if(index<0 || index>=ModelTickCount)
| |||
return 0.0;
| |||
| |||
return ModelTickAsks[(ModelTickHead+index)%MaxModelHistoryTicks];
| |||
}
| |||
| |||
void ResetSelectedCalculation()
| |||
{
| |||
PreviousBid=0.0;
| |||
HavePreviousBid=false;
| |||
}
| |||
| |||
void ClearDerivedState()
| |||
{
| |||
TickCount=0;
| |||
TickHead=0;
| |||
TickSerial=0;
| |||
ArrayResize(TickPrices,MathMax(100,GraphHistoryCapacity),
| |||
MathMax(100,GraphHistoryCapacity));
| |||
ArrayResize(SignalMarks,0);
| |||
// Не сбрасывать viewport при пересборке расчётной истории:
| |||
// он относится к отображению, а не к производной модели тиков.
| |||
ResetSelectedCalculation();
| |||
ResetAllTickVisualRowStates();
| |||
}
| |||
| |||
| |||
void ShiftRelativeWindowForCrossing(const int crossingGrid)
| |||
{
| |||
// CROSSING не изменяет viewport. Визуальное окно меняется только
| |||
// при достижении BID физического края ценового диапазона.
| |||
}
| |||
double GetViewportLevelStepPrice()
| |||
{
| |||
double minimum=0.0;
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
double step=(double)TickLevelRows[i].level*_Point;
| |||
if(step<=0.0) continue;
| |||
if(minimum<=0.0 || step<minimum) minimum=step;
| |||
}
| |||
return minimum;
| |||
}
| |||
int FloorGridForViewport(const double price)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return INVALID_GRID;
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathFloor(position+1.0e-9);
| |||
}
| |||
int CeilGridForViewport(const double price)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return INVALID_GRID;
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathCeil(position-1.0e-9);
| |||
}
| |||
double ViewportRayPrice(const int grid)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return 0.0;
| |||
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
void DeleteObjectsByPrefix()
| |||
{
| |||
ObjectsDeleteAll(0,Prefix,-1,-1);
| |||
}
| |||
| |||
void NotifyEvent(const string eventName,const string directionName,const int level,const double price,const long timeMsc)
| |||
{
| |||
if(!LiveMode)
| |||
return;
| |||
| |||
string directionArrow=(directionName=="UP" ? "↑" : "↓");
| |||
string msg=StringFormat("%s %s ···· Level: %d pt ···· Price Level: %s",
| |||
directionArrow,_Symbol,level,DoubleToString(price,_Digits));
| |||
| |||
Print(msg);
| |||
| |||
if(EnableAlert)
| |||
Alert(msg);
| |||
| |||
if(EnableSoundNotification && StringLen(NotificationSound)>0)
| |||
PlaySound(NotificationSound);
| |||
| |||
if(EnablePushNotification)
| |||
{
| |||
ResetLastError();
| |||
if(!SendNotification(msg))
| |||
Print("TPDR SendNotification failed. Error=",GetLastError());
| |||
}
| |||
| |||
if(EnableEmailNotification)
| |||
{
| |||
ResetLastError();
| |||
if(!SendMail("TPDR "+eventName+" "+directionName,msg))
| |||
Print("TPDR SendMail failed. Error=",GetLastError());
| |||
}
| |||
}
| |||
| |||
void AddTickToGraph(const double bid)
| |||
{
| |||
if(bid<=0.0 || GraphHistoryCapacity<1)
| |||
return;
| |||
| |||
// TickPrices[] заранее резервируется целиком. На глубокой истории
| |||
// нельзя делать ArrayResize() на каждом тике: это создаёт лишние
| |||
// перераспределения памяти и заметно увеличивает стартовую загрузку.
| |||
if(ArraySize(TickPrices)<GraphHistoryCapacity)
| |||
ArrayResize(TickPrices,GraphHistoryCapacity,GraphHistoryCapacity);
| |||
| |||
if(TickCount<GraphHistoryCapacity)
| |||
{
| |||
int index=(TickHead+TickCount)%GraphHistoryCapacity;
| |||
TickPrices[index]=bid;
| |||
TickCount++;
| |||
}
| |||
else
| |||
{
| |||
TickPrices[TickHead]=bid;
| |||
TickHead=(TickHead+1)%GraphHistoryCapacity;
| |||
}
| |||
}
| |||
| |||
double GetTickPriceByVisibleIndex(const int index)
| |||
{
| |||
if(index<0 || index>=TickCount)
| |||
return 0.0;
| |||
| |||
int physical=(TickHead+index)%GraphHistoryCapacity;
| |||
return TickPrices[physical];
| |||
}
| |||
| |||
void AddReversalSignalMark(const double price,const bool up,const int level)
| |||
{
| |||
if(!DrawReversalArrows || ReversalArrowsHistory<=0 ||
| |||
price<=0.0 || level<=0)
| |||
return;
| |||
| |||
int size=ArraySize(SignalMarks);
| |||
ArrayResize(SignalMarks,size+1);
| |||
SignalMarks[size].serial=TickSerial;
| |||
SignalMarks[size].price=NormalizeDouble(price,_Digits);
| |||
SignalMarks[size].level=level;
| |||
SignalMarks[size].up=up;
| |||
SignalMarks[size].reversal=true;
| |||
| |||
// Keep the last N UP and N DOWN reversals independently for each LEVEL.
| |||
// Otherwise activity on other dashboard levels can evict this level's arrows
| |||
// before DrawSignalLayer() applies its selected-level filter.
| |||
int keepCount=MathMin(20,MathMax(1,ReversalArrowsHistory));
| |||
int count=0;
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
if(SignalMarks[i].reversal &&
| |||
SignalMarks[i].up==up &&
| |||
SignalMarks[i].level==level)
| |||
count++;
| |||
| |||
if(count<=keepCount)
| |||
return;
| |||
| |||
// SignalMarks are appended chronologically; remove the oldest matching
| |||
// direction+LEVEL event, not the oldest event from another LEVEL.
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
if(!SignalMarks[i].reversal ||
| |||
SignalMarks[i].up!=up ||
| |||
SignalMarks[i].level!=level)
| |||
continue;
| |||
| |||
for(int j=i;j<ArraySize(SignalMarks)-1;j++)
| |||
SignalMarks[j]=SignalMarks[j+1];
| |||
ArrayResize(SignalMarks,ArraySize(SignalMarks)-1);
| |||
break;
| |||
}
| |||
}
| |||
| |||
void RecordReversalArrow(const double price,const bool buy,const int level)
| |||
{
| |||
// Стрелка фиксирует REVERSAL, но сама по себе не является ALERT.
| |||
// Уведомления отправляются только при пересечении строки с активной кнопкой ALERT.
| |||
AddReversalSignalMark(price,buy,level);
| |||
}
| |||
| |||
| |||
// VIEWPORT: масштаб и позиция окна отделены от состояния LEVEL. Новый тик не центрирует окно.
| |||
void UpdateCurrentLevelGrid(const double bid)
| |||
{
| |||
if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
| |||
return;
| |||
| |||
int grid=FloorGridForViewport(bid);
| |||
if(grid==INVALID_GRID)
| |||
return;
| |||
| |||
CurrentLevelGrid=grid;
| |||
CurrentLevelGridInitialized=true;
| |||
}
| |||
| |||
void UpdateSubwindowScale(const double bid)
| |||
{
| |||
if(SubWindow<0 || bid<=0.0 || TickLevelRowCount<=0)
| |||
return;
| |||
| |||
UpdateCurrentLevelGrid(bid);
| |||
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
| |||
double visibleMin=bid;
| |||
double visibleMax=bid;
| |||
| |||
for(int i=0;i<viewCount;i++)
| |||
{
| |||
double price=GetTickPriceByVisibleIndex(viewStart+i);
| |||
if(price<=0.0)
| |||
continue;
| |||
| |||
visibleMin=MathMin(visibleMin,price);
| |||
visibleMax=MathMax(visibleMax,price);
| |||
}
| |||
| |||
if(InitialArrowHistoryScalePending)
| |||
{
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
if(!SignalMarks[i].reversal ||
| |||
SignalMarks[i].level!=GraphLevelsPerSide)
| |||
continue;
| |||
visibleMin=MathMin(visibleMin,SignalMarks[i].price);
| |||
visibleMax=MathMax(visibleMax,SignalMarks[i].price);
| |||
}
| |||
}
| |||
| |||
// Центр первоначального окна определяется историческим диапазоном,
| |||
// а не текущим BID. Последующие тики не меняют центр viewport.
| |||
if(!ScaleInitialized)
| |||
{
| |||
double historyCenter=(visibleMin+visibleMax)*0.5;
| |||
ViewportCenterPrice=NormalizeDouble(historyCenter,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
| |||
ScaleInitialized=true;
| |||
}
| |||
| |||
double minimumStep=GetGraphLevelStepPrice();
| |||
minimumStep=MathMax(minimumStep,_Point);
| |||
| |||
int graphLevel=GraphLevelsPerSide;
| |||
int graphPerSide=MathMax(1,GraphDisplayedLevelCount);
| |||
for(int side=-1;side<=1;side+=2)
| |||
{
| |||
double levelPrice=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,graphPerSide);
| |||
if(levelPrice<=0.0)
| |||
continue;
| |||
| |||
visibleMin=MathMin(visibleMin,levelPrice);
| |||
visibleMax=MathMax(visibleMax,levelPrice);
| |||
}
| |||
| |||
double viewportCenter=(ViewportCenterPrice>0.0 ?
| |||
ViewportCenterPrice : bid);
| |||
double autoHalfRange=MathMax(MathAbs(viewportCenter-visibleMin),
| |||
MathAbs(visibleMax-viewportCenter));
| |||
double minimumHalfRange=minimumStep*3.0;
| |||
autoHalfRange=MathMax(autoHalfRange,minimumHalfRange);
| |||
| |||
double autoPadding=MathMax(autoHalfRange*0.05,minimumStep*0.10);
| |||
autoHalfRange+=autoPadding;
| |||
| |||
// Автомасштаб разрешён только при первом построении (или явном RESET).
| |||
// Изменение BID и числа отображаемых уровней не меняет масштаб истории.
| |||
if(!VerticalScaleManual && !AutoScaleInitialized)
| |||
{
| |||
VerticalScaleHalfRange=autoHalfRange;
| |||
AutoScaleInitialized=true;
| |||
}
| |||
| |||
double scaleHalfRange=MathMax(VerticalScaleHalfRange,
| |||
minimumStep*0.20);
| |||
| |||
// Автосдвиг viewport включается только когда BID подходит к краю.
| |||
// Цена остаётся не в центре: вверх — в верхней трети окна,
| |||
// вниз — в нижней трети. Масштаб не меняется, двигается только окно.
| |||
if(!VerticalScaleManual && ScaleInitialized && AutoScaleInitialized &&
| |||
scaleHalfRange>minimumStep)
| |||
{
| |||
double bidOffset=bid-viewportCenter;
| |||
double followTrigger=scaleHalfRange*0.60;
| |||
double followTarget=scaleHalfRange*0.35;
| |||
| |||
if(bidOffset>followTrigger)
| |||
viewportCenter=bid-followTarget;
| |||
else if(bidOffset < -followTrigger)
| |||
viewportCenter=bid+followTarget;
| |||
| |||
viewportCenter=NormalizeDouble(viewportCenter,_Digits);
| |||
ViewportCenterPrice=viewportCenter;
| |||
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
| |||
}
| |||
| |||
GraphScaleMin=NormalizeDouble(viewportCenter-scaleHalfRange,_Digits);
| |||
GraphScaleMax=NormalizeDouble(viewportCenter+scaleHalfRange,_Digits);
| |||
| |||
if(GraphScaleMax<=GraphScaleMin)
| |||
{
| |||
GraphScaleMin=viewportCenter-minimumStep;
| |||
GraphScaleMax=viewportCenter+minimumStep;
| |||
}
| |||
| |||
// Viewport moves only after BID crosses the follow threshold.
| |||
| |||
| |||
ScaleMin=GraphScaleMin;
| |||
ScaleMax=GraphScaleMax;
| |||
| |||
IndicatorSetDouble(INDICATOR_MINIMUM,GraphScaleMin);
| |||
IndicatorSetDouble(INDICATOR_MAXIMUM,GraphScaleMax);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,true);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,true);
| |||
| |||
if(CanvasReady)
| |||
CanvasHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
}
| |||
| |||
| |||
int GetViewStartIndex()
| |||
{
| |||
if(TickCount<=0)
| |||
return 0;
| |||
| |||
int span=ViewSpan;
| |||
if(span<=0)
| |||
span=TickCount;
| |||
| |||
span=MathMax(1,MathMin(span,TickCount));
| |||
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
int offset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
| |||
return TickCount-span-offset;
| |||
}
| |||
| |||
int GetViewCount()
| |||
{
| |||
if(TickCount<=0)
| |||
return 0;
| |||
| |||
int span=ViewSpan;
| |||
if(span<=0)
| |||
span=TickCount;
| |||
| |||
return MathMax(1,MathMin(span,TickCount));
| |||
}
| |||
| |||
// ViewSpan настраивается при bootstrap или смене диапазона, а не при каждом рендере.
| |||
void FitInitialArrowHistory()
| |||
{
| |||
if(TickCount<=0)
| |||
return;
| |||
| |||
const int targetPerDirection=MathMin(20,MathMax(1,ReversalArrowsHistory));
| |||
long firstGraphSerial=MathMax(1,TickSerial-TickCount+1);
| |||
long oldestSerial=-1;
| |||
long newestSerial=-1;
| |||
| |||
for(int direction=0;direction<2;direction++)
| |||
{
| |||
long cutoff=9223372036854775807;
| |||
for(int n=0;n<targetPerDirection;n++)
| |||
{
| |||
long best=-1;
| |||
for(int k=0;k<ArraySize(SignalMarks);k++)
| |||
{
| |||
if(!SignalMarks[k].reversal ||
| |||
SignalMarks[k].level!=GraphLevelsPerSide)
| |||
continue;
| |||
bool wanted=(direction==1 ? SignalMarks[k].up : !SignalMarks[k].up);
| |||
if(!wanted)
| |||
continue;
| |||
| |||
long serial=SignalMarks[k].serial;
| |||
if(serial<firstGraphSerial || serial>TickSerial || serial>=cutoff)
| |||
continue;
| |||
| |||
if(serial>best)
| |||
best=serial;
| |||
}
| |||
| |||
if(best<0)
| |||
break;
| |||
| |||
oldestSerial=(oldestSerial<0 ? best : MathMin(oldestSerial,best));
| |||
newestSerial=(newestSerial<0 ? best : MathMax(newestSerial,best));
| |||
cutoff=best;
| |||
}
| |||
}
| |||
| |||
if(oldestSerial<0 || newestSerial<0)
| |||
{
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
return;
| |||
}
| |||
| |||
long spanLong=newestSerial-oldestSerial+1;
| |||
int requiredSpan=(int)MathMin((long)TickCount,spanLong);
| |||
requiredSpan=MathMax(MinViewTicks,requiredSpan);
| |||
requiredSpan=MathMin(requiredSpan,TickCount);
| |||
| |||
ViewSpan=requiredSpan;
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
}
| |||
| |||
| |||
// Validate the retained arrow limit independently for each LEVEL and direction.
| |||
void ValidateReversalArrowHistory()
| |||
{
| |||
if(ReversalArrowsHistory<=0)
| |||
return;
| |||
| |||
int keepCount=MathMin(20,MathMax(1,ReversalArrowsHistory));
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
{
| |||
int level=TickLevelRows[row].level;
| |||
for(int direction=0;direction<2;direction++)
| |||
{
| |||
bool wantedUp=(direction==1);
| |||
int count=0;
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
if(SignalMarks[i].reversal &&
| |||
SignalMarks[i].up==wantedUp &&
| |||
SignalMarks[i].level==level)
| |||
count++;
| |||
| |||
if(count>keepCount)
| |||
Print("TPDR: reversal arrow history invariant violated. Level=",
| |||
level," Direction=",(wantedUp ? "UP" : "DOWN"),
| |||
" Count=",count," Limit=",keepCount);
| |||
}
| |||
}
| |||
}
| |||
| |||
int GraphPriceToY(const double price)
| |||
{
| |||
if(CanvasHeight<=1 || GraphScaleMax<=GraphScaleMin)
| |||
return CanvasHeight/2;
| |||
| |||
int top=0;
| |||
int bottom=MathMax(0,CanvasHeight-1);
| |||
int drawableHeight=MathMax(1,bottom-top);
| |||
| |||
double ratio=(GraphScaleMax-price)/(GraphScaleMax-GraphScaleMin);
| |||
ratio=MathMax(0.0,MathMin(1.0,ratio));
| |||
| |||
return top+(int)MathRound(ratio*(double)drawableHeight);
| |||
}
| |||
| |||
int PriceToY(const double price)
| |||
{
| |||
return GraphPriceToY(price);
| |||
}
| |||
| |||
bool IsGraphPriceVisible(const double price)
| |||
{
| |||
// Уровни больше не являются clipping-маской. Функция означает только
| |||
// принадлежность цены текущему AutoScale viewport.
| |||
return GraphScaleMax>GraphScaleMin &&
| |||
price>=GraphScaleMin &&
| |||
price<=GraphScaleMax;
| |||
}
| |||
| |||
void DrawDottedHorizontal(const int y,const uint clr)
| |||
{
| |||
if(!CanvasReady || CanvasWidth<=0)
| |||
return;
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
for(int x=0;x<graphWidth;x+=8)
| |||
{
| |||
int x2=MathMin(x+4,graphWidth-1);
| |||
TickCanvas.Line(x,y,x2,y,clr);
| |||
}
| |||
}
| |||
| |||
void EnsureLevelPriceLabel(const string name,const int x,const int y,
| |||
const color clr,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: level price label creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
double GetGraphLevelStepPrice()
| |||
{
| |||
return (GraphLevelsPerSide>=GraphLevelsPerSideMin ?
| |||
(double)GraphLevelsPerSide*_Point : 0.0);
| |||
}
| |||
| |||
double GetGraphLevelSidePrice(const double bid,const int graphLevel,const int side)
| |||
{
| |||
if(bid<=0.0 || graphLevel<=0 || (side!=1 && side!=-1))
| |||
return 0.0;
| |||
| |||
double step=(double)graphLevel*_Point;
| |||
if(step<=0.0)
| |||
return 0.0;
| |||
| |||
double normalizedBid=NormalizeDouble(bid,_Digits);
| |||
double position=(normalizedBid-NormalizeDouble(StartPrice,_Digits))/step;
| |||
int grid=(side>0 ? (int)MathCeil(position-1.0e-9) :
| |||
(int)MathFloor(position+1.0e-9));
| |||
double gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
| |||
// UP must be strictly above BID and DOWN strictly below BID after rounding.
| |||
while(side>0 && gridPrice<=normalizedBid)
| |||
{
| |||
grid++;
| |||
gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
while(side<0 && gridPrice>=normalizedBid)
| |||
{
| |||
grid--;
| |||
gridPrice=NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
return gridPrice;
| |||
}
| |||
| |||
double GetGraphLevelSidePriceByOffset(const double bid,const int graphLevel,
| |||
const int side,const int offset)
| |||
{
| |||
if(bid<=0.0 || graphLevel<=0 || (side!=1 && side!=-1) || offset<1)
| |||
return 0.0;
| |||
double step=(double)graphLevel*_Point;
| |||
if(step<=0.0)
| |||
return 0.0;
| |||
| |||
double normalizedBid=NormalizeDouble(bid,_Digits);
| |||
double position=(normalizedBid-NormalizeDouble(StartPrice,_Digits))/step;
| |||
int baseGrid=(side>0 ? (int)MathCeil(position-1.0e-9) :
| |||
(int)MathFloor(position+1.0e-9));
| |||
double basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
| |||
| |||
// Keep the nearest grid level visible when BID lands exactly on it.
| |||
// At equality the level belongs to the DOWN side; UP remains strictly above.
| |||
while(side>0 && basePrice<=normalizedBid)
| |||
{
| |||
baseGrid++;
| |||
basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
| |||
}
| |||
while(side<0 && basePrice>normalizedBid)
| |||
{
| |||
baseGrid--;
| |||
basePrice=NormalizeDouble(StartPrice+(double)baseGrid*step,_Digits);
| |||
}
| |||
| |||
int grid=baseGrid+(side>0 ? (offset-1) : -(offset-1));
| |||
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
int GetGraphRuntimeRowIndex(const int graphLevel)
| |||
{
| |||
if(graphLevel<=0)
| |||
return -1;
| |||
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
if(TickLevelRows[i].level==graphLevel)
| |||
return i;
| |||
}
| |||
| |||
return -1;
| |||
}
| |||
| |||
void DrawRayLayer(const double bid)
| |||
{
| |||
if(!CanvasReady || !DrawRayObjects || GraphScaleMax<=GraphScaleMin || TickLevelRowCount<=0 || bid<=0.0)
| |||
return;
| |||
if(GetGraphLevelStepPrice()<=0.0)
| |||
return;
| |||
int perSide=MathMax(1,GraphDisplayedLevelCount);
| |||
int graphLevel=GraphLevelsPerSide;
| |||
for(int offset=1;offset<=perSide;offset++)
| |||
for(int side=-1;side<=1;side+=2)
| |||
{
| |||
double price=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,offset);
| |||
if(price>0.0 && IsGraphPriceVisible(price))
| |||
DrawDottedHorizontal(GraphPriceToY(price),ColorToARGB(RayColor,185));
| |||
}
| |||
// The full-width central BID line is intentionally removed.
| |||
| |||
}
| |||
| |||
int GetGraphWidth()
| |||
{
| |||
// Canvas занимает только левую область тикового графика.
| |||
// Правая область полностью резервируется под цены и не рисуется Canvas-ом.
| |||
return MathMax(1,CanvasWidth);
| |||
}
| |||
| |||
void EnsureLevelRowsControlButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: LEVEL rows button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,LevelButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,LevelButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1100);
| |||
}
| |||
}
| |||
| |||
void UpdateLevelRowsControlsLayout()
| |||
{
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_LABEL");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_MINUS");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_VALUE");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_PLUS");
| |||
}
| |||
| |||
| |||
string GetGraphDisplayedLevelCountKey()
| |||
{
| |||
if(GraphDisplayedLevelCountKey=="")
| |||
GraphDisplayedLevelCountKey="TPDR_GL_"+IntegerToString(ChartID())+"_"+_Symbol;
| |||
return GraphDisplayedLevelCountKey;
| |||
}
| |||
| |||
void SaveGraphDisplayedLevelCount()
| |||
{
| |||
GlobalVariableSet(GetGraphDisplayedLevelCountKey(),(double)GraphDisplayedLevelCount);
| |||
}
| |||
| |||
void LoadGraphDisplayedLevelCount()
| |||
{
| |||
string key=GetGraphDisplayedLevelCountKey();
| |||
if(GlobalVariableCheck(key))
| |||
GraphDisplayedLevelCount=(int)MathRound(GlobalVariableGet(key));
| |||
| |||
GraphDisplayedLevelCount=MathMax(GraphDisplayedLevelCountMin,
| |||
MathMin(GraphDisplayedLevelCountMax,
| |||
GraphDisplayedLevelCount));
| |||
SaveGraphDisplayedLevelCount();
| |||
}
| |||
| |||
void HandleLevelRowsButtonClick(const string name)
| |||
{
| |||
// GRAPHIC LEVELS controls belong to the Dashboard namespace (DashPrefix),
| |||
// not the TickGraph object namespace (Prefix).
| |||
if(name==DashPrefix+"GRAPH_ROWS_MINUS")
| |||
{
| |||
GraphDisplayedLevelCount=MathMax(GraphDisplayedLevelCountMin,GraphDisplayedLevelCount-1);
| |||
SaveGraphDisplayedLevelCount();
| |||
ResetGraphToCurrentPrice();
| |||
RefreshGraph();
| |||
return;
| |||
}
| |||
if(name==DashPrefix+"GRAPH_ROWS_PLUS")
| |||
{
| |||
GraphDisplayedLevelCount=MathMin(GraphDisplayedLevelCountMax,GraphDisplayedLevelCount+1);
| |||
SaveGraphDisplayedLevelCount();
| |||
ResetGraphToCurrentPrice();
| |||
RefreshGraph();
| |||
return;
| |||
}
| |||
}
| |||
| |||
| |||
void DrawGraphLevelLabels(const double bid)
| |||
{
| |||
if(!CanvasReady || TickLevelRowCount<=0 || bid<=0.0)
| |||
return;
| |||
ObjectDelete(0,Prefix+"GRAPH_TITLE");
| |||
EnsurePriceScaleArea();
| |||
UpdateLevelRowsControlsLayout();
| |||
| |||
int graphLevel=GraphLevelsPerSide;
| |||
int perSide=MathMax(1,GraphDisplayedLevelCount);
| |||
color priceLabelColor=clrSilver;
| |||
double step=(double)graphLevel*_Point;
| |||
if(step<=0.0)
| |||
return;
| |||
| |||
// Remove legacy names once. Current labels are keyed by absolute grid index,
| |||
// not UP/DOWN + offset, so crossing a grid does not recreate the same label.
| |||
if(!LegacyGraphLabelsCleaned)
| |||
{
| |||
ObjectsDeleteAll(0,Prefix+"GRAPH_LEVEL_",-1,-1);
| |||
LegacyGraphLabelsCleaned=true;
| |||
LastGraphLabelNameCount=0;
| |||
ArrayResize(LastGraphLabelNames,0);
| |||
}
| |||
| |||
string currentNames[];
| |||
ArrayResize(currentNames,perSide*2);
| |||
int currentNameCount=0;
| |||
| |||
for(int offset=1;offset<=perSide;offset++)
| |||
for(int side=-1;side<=1;side+=2)
| |||
{
| |||
double price=GetGraphLevelSidePriceByOffset(bid,graphLevel,side,offset);
| |||
if(price<=0.0 || !IsGraphPriceVisible(price))
| |||
continue;
| |||
| |||
int grid=(int)MathRound((price-NormalizeDouble(StartPrice,_Digits))/step);
| |||
string name=Prefix+"GRAPH_LEVEL_"+IntegerToString(graphLevel)+"_"+
| |||
IntegerToString(grid);
| |||
| |||
// Keep one object identity for the same absolute LEVEL grid price.
| |||
EnsurePanelLabel(name,DoubleToString(price,_Digits));
| |||
int desiredY=GraphPriceToY(price)-7;
| |||
int labelTop=0;
| |||
int labelBottom=MathMax(labelTop,CanvasHeight-2);
| |||
desiredY=MathMax(labelTop,MathMin(labelBottom,desiredY));
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,CanvasWidth+8);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,desiredY);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,8);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,priceLabelColor);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
| |||
| |||
bool duplicate=false;
| |||
for(int j=0;j<currentNameCount;j++)
| |||
if(currentNames[j]==name)
| |||
{
| |||
duplicate=true;
| |||
break;
| |||
}
| |||
if(!duplicate && currentNameCount<ArraySize(currentNames))
| |||
currentNames[currentNameCount++]=name;
| |||
}
| |||
| |||
// Delete only labels that left the current visible set.
| |||
for(int i=0;i<LastGraphLabelNameCount;i++)
| |||
{
| |||
bool retained=false;
| |||
for(int j=0;j<currentNameCount;j++)
| |||
if(LastGraphLabelNames[i]==currentNames[j])
| |||
{
| |||
retained=true;
| |||
break;
| |||
}
| |||
if(!retained)
| |||
ObjectDelete(0,LastGraphLabelNames[i]);
| |||
}
| |||
| |||
ArrayResize(LastGraphLabelNames,currentNameCount);
| |||
for(int i=0;i<currentNameCount;i++)
| |||
LastGraphLabelNames[i]=currentNames[i];
| |||
LastGraphLabelNameCount=currentNameCount;
| |||
}
| |||
| |||
void DrawTickLayer(const double currentBid)
| |||
{
| |||
if(!CanvasReady || TickCount<=0 || currentBid<=0.0)
| |||
return;
| |||
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
if(viewCount<=0)
| |||
return;
| |||
| |||
uint graphColor=ColorToARGB(TickGraphColor,255);
| |||
int graphWidth=GetGraphWidth();
| |||
int leftMargin=0;
| |||
int rightMargin=0;
| |||
int drawWidth=MathMax(1,graphWidth);
| |||
| |||
if(viewCount==1)
| |||
{
| |||
int y=GraphPriceToY(currentBid);
| |||
int x=leftMargin;
| |||
TickCanvas.LineThick(x,y,MathMin(graphWidth-1,x+4),y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
| |||
return;
| |||
}
| |||
| |||
bool havePrevious=false;
| |||
int previousX=0;
| |||
int previousY=0;
| |||
| |||
for(int i=0;i<viewCount;i++)
| |||
{
| |||
int x=leftMargin+(int)MathRound((double)i*(double)(drawWidth-1)/(double)(viewCount-1));
| |||
double price=GetTickPriceByVisibleIndex(viewStart+i);
| |||
| |||
// Последняя точка всегда является фактическим текущим Bid.
| |||
if(i==viewCount-1)
| |||
price=currentBid;
| |||
| |||
if(!IsGraphPriceVisible(price))
| |||
{
| |||
havePrevious=false;
| |||
continue;
| |||
}
| |||
| |||
int y=GraphPriceToY(price);
| |||
| |||
if(havePrevious)
| |||
TickCanvas.LineThick(previousX,previousY,x,y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
| |||
| |||
previousX=x;
| |||
previousY=y;
| |||
havePrevious=true;
| |||
}
| |||
}
| |||
| |||
void DrawArrowMark(const int x,const int y,const bool up,const bool reversal,const color clr)
| |||
{
| |||
if(!CanvasReady)
| |||
return;
| |||
| |||
uint c=ColorToARGB(clr,255);
| |||
int size=reversal ? 7 : 5;
| |||
| |||
// (x,y) — точка события, то есть ОСТРИЁ стрелки.
| |||
// Геометрия стрелки строится относительно этой точки и больше
| |||
// не добавляет скрытого смещения по цене.
| |||
if(up)
| |||
{
| |||
TickCanvas.FillTriangle(x,y,x-size,y+2*size,x+size,y+2*size,c);
| |||
}
| |||
else
| |||
{
| |||
TickCanvas.FillTriangle(x,y,x-size,y-2*size,x+size,y-2*size,c);
| |||
}
| |||
}
| |||
| |||
void DrawBidMarker(const double bid)
| |||
{
| |||
if(!CanvasReady || bid<=0.0)
| |||
return;
| |||
| |||
int y=GraphPriceToY(bid);
| |||
uint c=ColorToARGB(CurrentRayColor,255);
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
int rightX=MathMax(0,graphWidth-1);
| |||
int markerWidth=MathMin(4,graphWidth);
| |||
int markerStart=MathMax(0,rightX-markerWidth+1);
| |||
TickCanvas.Line(markerStart,y,rightX,y,c);
| |||
TickCanvas.FillTriangle(MathMax(2,rightX-12),y-5,
| |||
MathMax(2,rightX-2),y,
| |||
MathMax(2,rightX-12),y+5,c);
| |||
}
| |||
| |||
void DrawCurrentTickMarker(const double currentBid)
| |||
{
| |||
if(!CanvasReady || currentBid<=0.0)
| |||
return;
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
int x=MathMax(0,graphWidth-1);
| |||
int y=GraphPriceToY(currentBid);
| |||
| |||
TickCanvas.FillCircle(x,y,3,ColorToARGB(TickGraphColor,255));
| |||
}
| |||
| |||
void DrawSignalLayer(const double bid)
| |||
{
| |||
if(!CanvasReady || TickCount<=0 || ArraySize(SignalMarks)==0 || bid<=0.0)
| |||
return;
| |||
| |||
long firstSerial=TickSerial-TickCount+1;
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
int viewEnd=viewStart+viewCount-1;
| |||
if(!DrawReversalArrows)
| |||
return;
| |||
| |||
// На графике отображаются именно REVERSAL, а не обычные CROSSING.
| |||
// Храним последние N UP + N DOWN отдельно для каждого LEVEL.
| |||
// Цена стрелки абсолютная и не пересчитывается от текущего BID.
| |||
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
if(!SignalMarks[i].reversal)
| |||
continue;
| |||
| |||
// Графический REVERSAL относится только к выбранному GRAPHIC LEVEL.
| |||
// Стрелки разворотов других dashboard LEVEL не являются событиями
| |||
// выбранного тикового графика и не должны отображаться на нём.
| |||
if(SignalMarks[i].level!=GraphLevelsPerSide)
| |||
continue;
| |||
| |||
long serial=SignalMarks[i].serial;
| |||
if(serial<firstSerial || serial>TickSerial)
| |||
continue;
| |||
int index=(int)(serial-firstSerial);
| |||
if(index<viewStart || index>viewEnd)
| |||
continue;
| |||
| |||
int relative=index-viewStart;
| |||
int drawWidth=MathMax(1,GetGraphWidth());
| |||
int x=(viewCount==1) ? 0 :
| |||
(int)MathRound((double)relative*(double)(drawWidth-1)/(double)(viewCount-1));
| |||
double signalPrice=SignalMarks[i].price;
| |||
if(!IsGraphPriceVisible(signalPrice))
| |||
continue;
| |||
| |||
int y=GraphPriceToY(signalPrice);
| |||
color clr=(SignalMarks[i].up ? UpCrossColor : DownCrossColor);
| |||
DrawArrowMark(x,y,SignalMarks[i].up,true,clr);
| |||
}
| |||
}
| |||
| |||
void DeleteLegacyPriceObjects()
| |||
{
| |||
if(LegacyPriceObjectsCleaned)
| |||
return;
| |||
| |||
// Однократная миграционная очистка. Старые объекты больше не создаются,
| |||
// поэтому удалять их на каждом тике не имеет смысла и создаёт лишнюю
| |||
// нагрузку на графический поток.
| |||
for(int i=0;i<200;i++)
| |||
ObjectDelete(0,Prefix+"PRICE_DYNAMIC_"+IntegerToString(i));
| |||
| |||
for(int i=0;i<50;i++)
| |||
{
| |||
ObjectDelete(0,Prefix+"DIST_UP_"+IntegerToString(i));
| |||
ObjectDelete(0,Prefix+"DIST_DOWN_"+IntegerToString(i));
| |||
}
| |||
| |||
ObjectDelete(0,Prefix+"PRICE_BID");
| |||
ObjectDelete(0,Prefix+"CURRENT_DIRECTION_ARROW");
| |||
| |||
LegacyPriceObjectsCleaned=true;
| |||
}
| |||
| |||
void DeleteLegacyStatePanel()
| |||
{
| |||
string names[16]=
| |||
{
| |||
Prefix+"STATE_TITLE",
| |||
Prefix+"STATE_LEVEL_LABEL",Prefix+"STATE_LEVEL_VALUE",
| |||
Prefix+"STATE_DIRECTION_LABEL",Prefix+"STATE_DIRECTION_VALUE",
| |||
Prefix+"STATE_REVERSAL_AT_LABEL",Prefix+"STATE_REVERSAL_AT_VALUE",
| |||
Prefix+"STATE_LAST_DOWN_2",Prefix+"STATE_LAST_DOWN_2_VALUE",
| |||
Prefix+"STATE_LAST_DOWN_1",Prefix+"STATE_LAST_DOWN_1_VALUE",
| |||
Prefix+"STATE_LAST_UP_2",Prefix+"STATE_LAST_UP_2_VALUE",
| |||
Prefix+"STATE_LAST_UP_1",Prefix+"STATE_LAST_UP_1_VALUE"
| |||
};
| |||
for(int i=0;i<ArraySize(names);i++)
| |||
if(ObjectFind(0,names[i])>=0)
| |||
ObjectDelete(0,names[i]);
| |||
}
| |||
| |||
bool EnsureCanvas()
| |||
{
| |||
if(SubWindow<0)
| |||
return false;
| |||
| |||
// A resized/reordered indicator window may receive a new subwindow index.
| |||
// Recreate the bitmap in the current window instead of leaving it attached
| |||
// to the old one.
| |||
if(CanvasReady && CanvasSubWindow!=SubWindow)
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady=false;
| |||
CanvasWidth=0;
| |||
CanvasHeight=0;
| |||
CanvasSubWindow=-1;
| |||
}
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int chartHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
| |||
// Правая часть под цены физически не входит в Canvas, поэтому
| |||
// тиковая линия не может накладываться на ценовые подписи.
| |||
int wantedWidth=MathMax(50,chartWidth-PriceScaleWidth);
| |||
| |||
if(wantedWidth<50 || chartHeight<20)
| |||
return false;
| |||
| |||
// Canvas всегда занимает ровно всё окно TickGraph по вертикали:
| |||
// верхняя граница = 0, нижняя граница = chartHeight-1.
| |||
// Мышь не должна иметь возможности физически сдвинуть график за эти границы.
| |||
if(CanvasReady)
| |||
{
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XDISTANCE,0);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YDISTANCE,0);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XSIZE,wantedWidth);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YSIZE,chartHeight);
| |||
}
| |||
| |||
if(CanvasReady && CanvasWidth==wantedWidth && CanvasHeight==chartHeight)
| |||
return true;
| |||
| |||
if(CanvasReady)
| |||
{
| |||
ResetLastError();
| |||
if(!TickCanvas.Resize(wantedWidth,chartHeight))
| |||
{
| |||
int resizeError=GetLastError();
| |||
Print("TPDR: canvas resize failed; recreating bitmap. Error=",resizeError);
| |||
TickCanvas.Destroy();
| |||
CanvasReady=false;
| |||
CanvasWidth=0;
| |||
CanvasHeight=0;
| |||
CanvasSubWindow=-1;
| |||
ObjectDelete(0,Prefix+"PANEL");
| |||
}
| |||
else
| |||
{
| |||
CanvasWidth=wantedWidth;
| |||
CanvasHeight=chartHeight;
| |||
return true;
| |||
}
| |||
}
| |||
| |||
ResetLastError();
| |||
| |||
if(!TickCanvas.CreateBitmapLabel(0,SubWindow,Prefix+"PANEL",0,0,
| |||
wantedWidth,chartHeight,
| |||
COLOR_FORMAT_ARGB_NORMALIZE))
| |||
{
| |||
Print("TPDR: canvas creation failed. Error=",GetLastError());
| |||
return false;
| |||
}
| |||
| |||
CanvasWidth=wantedWidth;
| |||
CanvasHeight=chartHeight;
| |||
CanvasSubWindow=SubWindow;
| |||
CanvasReady=true;
| |||
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XDISTANCE,0);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YDISTANCE,0);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_XSIZE,wantedWidth);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_YSIZE,chartHeight);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_BACK,true);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_HIDDEN,true);
| |||
| |||
return true;
| |||
}
| |||
| |||
bool IsPointInsideObject(const string name,const int x,const int y)
| |||
{
| |||
if(ObjectFind(0,name)<0 || SubWindow<0)
| |||
return false;
| |||
| |||
int bx=(int)ObjectGetInteger(0,name,OBJPROP_XDISTANCE);
| |||
int by=(int)ObjectGetInteger(0,name,OBJPROP_YDISTANCE);
| |||
int bw=(int)ObjectGetInteger(0,name,OBJPROP_XSIZE);
| |||
int bh=(int)ObjectGetInteger(0,name,OBJPROP_YSIZE);
| |||
ENUM_BASE_CORNER corner=(ENUM_BASE_CORNER)ObjectGetInteger(
| |||
0,name,OBJPROP_CORNER);
| |||
| |||
// CHARTEVENT_MOUSE_MOVE передаёт Y относительно главного окна,
| |||
// графические объекты подокна — относительно самого подокна.
| |||
int subwindowTop=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
| |||
int localY=y-subwindowTop;
| |||
int localX=x;
| |||
| |||
int top=by;
| |||
int left=bx;
| |||
| |||
if(corner==CORNER_LEFT_LOWER)
| |||
top=CanvasHeight-by-bh;
| |||
else if(corner==CORNER_RIGHT_UPPER ||
| |||
corner==CORNER_RIGHT_LOWER)
| |||
left=CanvasWidth-bx-bw;
| |||
| |||
if(corner==CORNER_RIGHT_LOWER)
| |||
top=CanvasHeight-by-bh;
| |||
| |||
return localX>=left && localX<left+bw &&
| |||
localY>=top && localY<top+bh;
| |||
}
| |||
| |||
void EnsureResetButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: reset button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,ResetButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
| |||
}
| |||
}
| |||
| |||
void EnsureZoomButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: zoom button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,12);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
| |||
}
| |||
}
| |||
| |||
| |||
| |||
| |||
| |||
void EnsurePanelLabel(const string name,const string textValue)
| |||
{
| |||
// GRAPH labels belong to the TickGraphic indicator subwindow.
| |||
// Recreate legacy labels that were created in the main chart window.
| |||
// ObjectFind() returns the subwindow index for an existing object.
| |||
// OBJPROP_WINDOW does not exist in MQL5 object properties.
| |||
if(ObjectFind(0,name)>=0 && ObjectFind(0,name)!=SubWindow)
| |||
ObjectDelete(0,name);
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: panel label creation error=",GetLastError(),
| |||
" Name=",name," SubWindow=",SubWindow);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,500);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
| |||
| |||
| |||
| |||
void EnsureZoomControlBackground(const int x,const int y,const int totalWidth)
| |||
{
| |||
string name=Prefix+"ZOOM_CONTROL_BG";
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
return;
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,80);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
}
| |||
| |||
// Фон также принудительно привязываем к нижнему левому углу.
| |||
// Иначе старый объект мог остаться с прежним CORNER.
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,80);
| |||
| |||
int padding=5;
| |||
int panelX=MathMax(0,x-padding);
| |||
int panelY=MathMax(0,y-padding);
| |||
int panelWidth=MathMin(CanvasWidth-panelX,totalWidth+2*padding);
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,panelX);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,panelY);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,MathMax(1,panelWidth));
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight+2*padding);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrDimGray);
| |||
}
| |||
| |||
void EnsureGraphHeaderBackground()
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return;
| |||
| |||
string name=Prefix+"GRAPH_HEADER_BG";
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: graph header background creation error=",GetLastError());
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,40);
| |||
}
| |||
| |||
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,0);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,CanvasWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,
| |||
MathMin(GraphTopReserved,CanvasHeight));
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,background);
| |||
}
| |||
| |||
void UpdateZoomButtonsLayout()
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return;
| |||
| |||
EnsureGraphHeaderBackground();
| |||
| |||
string minusName=Prefix+"ZOOM_MINUS";
| |||
string plusName=Prefix+"ZOOM_PLUS";
| |||
string resetName=Prefix+"RESET_CURRENT";
| |||
string labelName=Prefix+"ZOOM_LABEL";
| |||
| |||
// Старый отдельный фон больше не используется: кнопки входят
| |||
// непосредственно в общую таблицу LEVELS/Distance.
| |||
string oldBackground=Prefix+"ZOOM_CONTROL_BG";
| |||
if(ObjectFind(0,oldBackground)>=0)
| |||
ObjectDelete(0,oldBackground);
| |||
EnsureZoomButton(minusName,"-");
| |||
EnsureZoomButton(plusName,"+");
| |||
EnsureResetButton(resetName,"RESET");
| |||
EnsurePanelLabel(labelName,"ZOOM");
| |||
| |||
// Верхняя строка зарезервирована под название индикатора/графика.
| |||
// Кнопки управления опускаем ниже, чтобы они не перекрывали заголовок.
| |||
int y=34;
| |||
int controlsWidth=2*ZoomButtonWidth+ResetButtonWidth+2*ZoomButtonGap;
| |||
int x=MathMax(4,CanvasWidth-controlsWidth-8);
| |||
| |||
int x1=x;
| |||
int x2=x+ZoomButtonWidth+ZoomButtonGap;
| |||
int x3=x+2*(ZoomButtonWidth+ZoomButtonGap);
| |||
| |||
ObjectSetInteger(0,minusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,minusName,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,minusName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,minusName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,minusName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,minusName,OBJPROP_XDISTANCE,x1);
| |||
ObjectSetInteger(0,minusName,OBJPROP_YDISTANCE,y);
| |||
| |||
ObjectSetInteger(0,plusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,plusName,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,plusName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,plusName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,plusName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,plusName,OBJPROP_XDISTANCE,x2);
| |||
ObjectSetInteger(0,plusName,OBJPROP_YDISTANCE,y);
| |||
| |||
ObjectSetInteger(0,resetName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,resetName,OBJPROP_XSIZE,ResetButtonWidth);
| |||
ObjectSetInteger(0,resetName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,resetName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,resetName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,resetName,OBJPROP_XDISTANCE,x3);
| |||
ObjectSetInteger(0,resetName,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,resetName,OBJPROP_STATE,ResetButtonHeld);
| |||
| |||
ObjectSetInteger(0,labelName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,labelName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,labelName,OBJPROP_XDISTANCE,MathMax(4,x-48));
| |||
ObjectSetInteger(0,labelName,OBJPROP_YDISTANCE,y+4);
| |||
}
| |||
| |||
void ResetGraphToCurrentPrice()
| |||
{
| |||
if(TickCount<=0)
| |||
return;
| |||
| |||
// RESET возвращает только начальный VIEW, стараясь показать последние
| |||
// REVERSAL-стрелки по правилам FitInitialArrowHistory() и текущий BID.
| |||
// История, LEVEL и runtime-модель RESET не изменяет.
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
FitInitialArrowHistory();
| |||
| |||
VerticalScaleManual=false;
| |||
VerticalScaleDragActive=false;
| |||
VerticalPanDragActive=false;
| |||
VerticalScaleHalfRange=0.0;
| |||
| |||
AutoScaleInitialized=false;
| |||
InitialArrowHistoryScalePending=true;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0 && GetViewportLevelStepPrice()>0.0)
| |||
{
| |||
double bid=NormalizeDouble(current.bid,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(bid);
| |||
ViewportCenterPrice=ViewportRayPrice(ScaleCenterGrid);
| |||
ScaleInitialized=true;
| |||
UpdateCurrentLevelGrid(bid);
| |||
}
| |||
| |||
ResetButtonHeld=false;
| |||
}
| |||
| |||
void ApplyZoomButton(const int delta)
| |||
{
| |||
if(TickCount<=1 || GetGraphWidth()<=1)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
HandleHorizontalZoom(GetGraphWidth()/2,delta);
| |||
RefreshGraph();
| |||
}
| |||
| |||
void HandleZoomButtonClick(const string name)
| |||
{
| |||
if(name==Prefix+"ZOOM_MINUS")
| |||
{
| |||
InitialViewLayoutActive=false;
| |||
ApplyZoomButton(-1);
| |||
return;
| |||
}
| |||
| |||
if(name==Prefix+"ZOOM_PLUS")
| |||
{
| |||
InitialViewLayoutActive=false;
| |||
ApplyZoomButton(1);
| |||
return;
| |||
}
| |||
| |||
if(name==Prefix+"RESET_CURRENT")
| |||
{
| |||
ResetGraphToCurrentPrice();
| |||
ResetButtonHeld=false;
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
| |||
void EnsurePriceScaleArea()
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return;
| |||
| |||
string name=Prefix+"PRICE_SCALE_AREA";
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int priceAreaWidth=MathMax(0,chartWidth-CanvasWidth);
| |||
| |||
if(priceAreaWidth<=0)
| |||
{
| |||
if(ObjectFind(0,name)>=0)
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,0);
| |||
return;
| |||
}
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: price scale area creation error=",GetLastError());
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1);
| |||
}
| |||
| |||
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,CanvasWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,priceAreaWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,CanvasHeight);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
}
| |||
| |||
void RenderGraph(const double bid)
| |||
{
| |||
// Один источник текущего визуального LEVEL для всех render-слоёв.
| |||
UpdateCurrentLevelGrid(bid);
| |||
| |||
// До завершения bootstrap отображение запрещено. Это единый инвариант
| |||
// для OnTimer(), OnCalculate(), RefreshGraph() и обработчиков кнопок.
| |||
if(!BootstrapComplete)
| |||
return;
| |||
| |||
if(!EnsureCanvas())
| |||
return;
| |||
| |||
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
| |||
| |||
TickCanvas.Erase(ColorToARGB(background,255));
| |||
| |||
DrawRayLayer(bid);
| |||
DrawGraphLevelLabels(bid);
| |||
DrawTickLayer(bid);
| |||
DrawSignalLayer(bid);
| |||
DrawBidMarker(bid);
| |||
DrawCurrentTickMarker(bid);
| |||
| |||
TickCanvas.Update(true);
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_LABEL");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_MINUS");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_VALUE");
| |||
ObjectDelete(0,Prefix+"GRAPH_ROWS_PLUS");
| |||
UpdateZoomButtonsLayout();
| |||
DeleteLegacyStatePanel();
| |||
}
| |||
| |||
void RefreshGraph()
| |||
{
| |||
if(SubWindow<0 || !BootstrapComplete)
| |||
return;
| |||
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
| |||
return;
| |||
| |||
// После wheel/drag обновляется горизонтальный viewport и перерисовывается
| |||
// график. Вертикальный диапазон остаётся фиксированным; его можно изменить
| |||
// вручную или явной командой RESET. Сам ViewSpan при этом НЕ изменяется.
| |||
if(!EnsureCanvas())
| |||
return;
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending=false;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void UpdateTickRowPhaseExtremes(const MqlTick &tick)
| |||
{
| |||
if(tick.bid<=0.0)
| |||
return;
| |||
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
{
| |||
if(TickLevelRows[row].direction>0 && tick.ask>0.0)
| |||
{
| |||
if(TickLevelRows[row].phaseMaxAsk<=0.0 || tick.ask>TickLevelRows[row].phaseMaxAsk)
| |||
TickLevelRows[row].phaseMaxAsk=tick.ask;
| |||
}
| |||
else if(TickLevelRows[row].direction<0)
| |||
{
| |||
if(TickLevelRows[row].phaseMinBid<=0.0 || tick.bid<TickLevelRows[row].phaseMinBid)
| |||
TickLevelRows[row].phaseMinBid=tick.bid;
| |||
}
| |||
}
| |||
}
| |||
| |||
void ProcessBidTick(const MqlTick &tick,const bool emitSignals,
| |||
const bool drawGraph,const bool storeHistory)
| |||
{
| |||
if(tick.bid<=0.0)
| |||
return;
| |||
| |||
double bid=NormalizeDouble(tick.bid,_Digits);
| |||
| |||
if(storeHistory)
| |||
AppendModelTick(bid,tick.ask);
| |||
| |||
TickSerial++;
| |||
| |||
if(!HavePreviousBid)
| |||
{
| |||
PreviousBid=bid;
| |||
HavePreviousBid=true;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
if(!ScaleInitialized)
| |||
{
| |||
ScaleCenterGrid=FloorGridForViewport(bid);
| |||
ScaleInitialized=true;
| |||
}
| |||
| |||
UpdateTickRowPhaseExtremes(tick);
| |||
return;
| |||
}
| |||
| |||
double oldBid=PreviousBid;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
if(bid!=oldBid)
| |||
{
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
ProcessTickRow(row,oldBid,bid,tick,emitSignals);
| |||
| |||
PreviousBid=bid;
| |||
}
| |||
| |||
UpdateTickRowPhaseExtremes(tick);
| |||
}
| |||
| |||
void UpdateCursorFromArray(const MqlTick &ticks[],const int copied,const int startIndex)
| |||
{
| |||
if(copied<=0)
| |||
return;
| |||
| |||
long finalMsc=ticks[copied-1].time_msc;
| |||
| |||
// CopyTicks() работает включительно: запрос от CursorMsc может вернуть
| |||
// уже обработанные тики с той же миллисекундой. Поэтому храним точное количество
| |||
// обработанных тиков внутри последней временной отметки.
| |||
if(finalMsc==CursorMsc)
| |||
{
| |||
int processedAtFinal=0;
| |||
for(int i=0;i<copied;i++)
| |||
{
| |||
if(ticks[i].time_msc==finalMsc && i>=startIndex)
| |||
processedAtFinal++;
| |||
}
| |||
| |||
if(processedAtFinal>0)
| |||
CursorSameMsc+=processedAtFinal;
| |||
}
| |||
else
| |||
{
| |||
CursorMsc=finalMsc;
| |||
| |||
int finalCount=0;
| |||
for(int i=copied-1;i>=0 && ticks[i].time_msc==finalMsc;i--)
| |||
finalCount++;
| |||
| |||
CursorSameMsc=finalCount;
| |||
}
| |||
}
| |||
| |||
bool BootstrapShouldLogError(const int error)
| |||
{
| |||
uint now=GetTickCount();
| |||
if(error!=BootstrapLastLoggedError ||
| |||
(uint)(now-BootstrapLastErrorLogTick)>=BootstrapErrorLogIntervalMs)
| |||
{
| |||
BootstrapLastLoggedError=error;
| |||
BootstrapLastErrorLogTick=now;
| |||
return true;
| |||
}
| |||
return false;
| |||
}
| |||
| |||
bool BootstrapHistory()
| |||
{
| |||
uint now=GetTickCount();
| |||
if(BootstrapRetryPending &&
| |||
(uint)(now-BootstrapLastAttemptTick)<BootstrapRetryDelayMs)
| |||
return false;
| |||
| |||
BootstrapRetryPending=false;
| |||
BootstrapLastAttemptTick=now;
| |||
| |||
int minimumTarget=MathMax(HistoryGraphTicks,HistoryBootstrapTicks);
| |||
minimumTarget=MathMax(minimumTarget,100);
| |||
int maximumTarget=MathMax(minimumTarget,MathMax(100,MaxModelHistoryTicks));
| |||
| |||
if(HistoryBootstrapTargetTicks<=0)
| |||
HistoryBootstrapTargetTicks=MathMin(minimumTarget,maximumTarget);
| |||
else
| |||
HistoryBootstrapTargetTicks=MathMax(minimumTarget,
| |||
MathMin(HistoryBootstrapTargetTicks,maximumTarget));
| |||
| |||
int targetTicks=HistoryBootstrapTargetTicks;
| |||
MqlTick ticks[];
| |||
ArrayResize(ticks,targetTicks);
| |||
| |||
int copied=-1;
| |||
int lastError=0;
| |||
| |||
for(int attempt=0;attempt<3;attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,0,(uint)targetTicks);
| |||
lastError=GetLastError();
| |||
| |||
if(copied<0)
| |||
{
| |||
if(lastError==4004)
| |||
{
| |||
targetTicks=MathMax(100,targetTicks/2);
| |||
ArrayResize(ticks,targetTicks);
| |||
continue;
| |||
}
| |||
| |||
if(lastError==4401 || lastError==4403)
| |||
{
| |||
BootstrapAttempts++;
| |||
BootstrapRetryPending=true;
| |||
if(BootstrapShouldLogError(lastError))
| |||
Print("TPDR: tick history synchronization did not complete. Error=",
| |||
lastError," Attempts=",BootstrapAttempts,
| |||
" RetryDelayMs=",BootstrapRetryDelayMs);
| |||
return false;
| |||
}
| |||
| |||
BootstrapRetryPending=true;
| |||
if(BootstrapShouldLogError(lastError))
| |||
Print("TPDR: bootstrap CopyTicks failed. Error=",lastError,
| |||
" Attempts=",BootstrapAttempts+1);
| |||
BootstrapAttempts++;
| |||
return false;
| |||
}
| |||
break;
| |||
}
| |||
| |||
if(copied<=0)
| |||
{
| |||
BootstrapAttempts++;
| |||
BootstrapRetryPending=true;
| |||
return false;
| |||
}
| |||
| |||
int lastAttach=-1;
| |||
for(int i=0;i<copied;i++)
| |||
if(ticks[i].time_msc<=AttachMsc)
| |||
lastAttach=i;
| |||
| |||
if(lastAttach<0)
| |||
{
| |||
if(targetTicks<maximumTarget)
| |||
{
| |||
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
| |||
MathMax(targetTicks+1,targetTicks*2));
| |||
BootstrapRetryPending=true;
| |||
return false;
| |||
}
| |||
| |||
BootstrapAttempts++;
| |||
BootstrapRetryPending=true;
| |||
if(BootstrapShouldLogError(lastError))
| |||
Print("TPDR: no tick at or before attach time. AttachMsc=",AttachMsc,
| |||
" Requested=",targetTicks," Copied=",copied,
| |||
" Maximum=",maximumTarget);
| |||
return false;
| |||
}
| |||
| |||
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),MathMax(100,MaxModelHistoryTicks));
| |||
ModelTickCount=0;
| |||
ModelTickHead=0;
| |||
ClearDerivedState();
| |||
| |||
// HISTORY and LIVE use the same ProcessBidTick() state machine.
| |||
for(int i=0;i<=lastAttach;i++)
| |||
{
| |||
if(ticks[i].bid>0.0)
| |||
ProcessBidTick(ticks[i],false,true,true);
| |||
}
| |||
| |||
int attachCount=0;
| |||
for(int i=lastAttach;i>=0 && ticks[i].time_msc==AttachMsc;i--)
| |||
attachCount++;
| |||
| |||
CursorMsc=AttachMsc;
| |||
CursorSameMsc=attachCount;
| |||
FinalizeTickRowsFromHistory();
| |||
| |||
// Do not accept a partial bootstrap as the requested history volume.
| |||
// Increase the request and rebuild the model from the chronological tick stream.
| |||
// At the configured maximum, continue only with the available broker history.
| |||
if(copied<targetTicks && targetTicks<maximumTarget)
| |||
{
| |||
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
| |||
MathMax(targetTicks+1,targetTicks*2));
| |||
BootstrapComplete=false;
| |||
LiveMode=false;
| |||
BootstrapRetryPending=true;
| |||
Print("TPDR: tick history is partial. Requested=",targetTicks,
| |||
" Copied=",copied," Next=",HistoryBootstrapTargetTicks,
| |||
" Maximum=",maximumTarget," Error=",lastError);
| |||
return false;
| |||
}
| |||
| |||
// Large LEVEL values require deeper history to obtain real REVERSAL,
| |||
// LAST-DOWN and LAST-UP values. Expand the request and recalculate from the
| |||
// beginning; if the configured limit is reached, report missing history.
| |||
bool derivedStateComplete=HistoryDerivedStateComplete();
| |||
if(!derivedStateComplete && targetTicks<maximumTarget)
| |||
{
| |||
HistoryBootstrapTargetTicks=MathMin(maximumTarget,
| |||
MathMax(targetTicks+1,targetTicks*2));
| |||
BootstrapComplete=false;
| |||
LiveMode=false;
| |||
BootstrapRetryPending=true;
| |||
Print("TPDR: expanding tick history for missing LAST values. Requested=",
| |||
targetTicks," Next=",HistoryBootstrapTargetTicks,
| |||
" Maximum=",maximumTarget);
| |||
return false;
| |||
}
| |||
| |||
if(!derivedStateComplete)
| |||
{
| |||
int incompleteRows=0;
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
{
| |||
if(TickLevelRows[row].direction==0 ||
| |||
TickLevelRows[row].reversalAtPrice<=0.0 ||
| |||
TickLevelRows[row].lastDown1Price<=0.0 ||
| |||
TickLevelRows[row].lastDown2Price<=0.0 ||
| |||
TickLevelRows[row].lastUp1Price<=0.0 ||
| |||
TickLevelRows[row].lastUp2Price<=0.0)
| |||
incompleteRows++;
| |||
}
| |||
| |||
string oldestTickText=(copied>0 ?
| |||
TimeToString(ticks[0].time,TIME_DATE|TIME_SECONDS) : "UNKNOWN");
| |||
Print("TPDR WARNING: required reversal history incomplete. Requested=",targetTicks,
| |||
" Copied=",copied," ModelTicks=",ModelTickCount,
| |||
" OldestTick=",oldestTickText,
| |||
" MissingRows=",incompleteRows,
| |||
" MaxHistoryTicks=",maximumTarget,
| |||
". The broker may not provide older ticks, or required REVERSAL events may not exist in available history.");
| |||
}
| |||
| |||
BootstrapAttempts=0;
| |||
BootstrapRetryPending=false;
| |||
BootstrapLastLoggedError=0;
| |||
BootstrapComplete=true;
| |||
LiveMode=true;
| |||
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount));
| |||
ViewEndOffset=0;
| |||
| |||
// REVERSAL arrows are produced by the same chronological history pass.
| |||
FitInitialArrowHistory();
| |||
ValidateReversalArrowHistory();
| |||
InitialArrowHistoryScalePending=true;
| |||
| |||
return true;
| |||
}
| |||
| |||
bool ProcessLiveTicks()
| |||
{
| |||
MqlTick ticks[5000];
| |||
int requestCount=MathMin(MathMax(HistoryChunkSize,100),5000);
| |||
| |||
for(int chunk=0;chunk<20;chunk++)
| |||
{
| |||
int copied=-1;
| |||
int requestTried=requestCount;
| |||
int lastError=0;
| |||
| |||
for(int attempt=0;attempt<3;attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,
| |||
(ulong)MathMax(1,CursorMsc),(uint)requestTried);
| |||
lastError=GetLastError();
| |||
| |||
if(copied>=0)
| |||
break;
| |||
| |||
if(lastError!=4004)
| |||
{
| |||
Print("TPDR: live CopyTicks failed. Requested=",requestTried,
| |||
" CursorMsc=",CursorMsc," Error=",lastError);
| |||
return false;
| |||
}
| |||
| |||
requestTried=MathMax(100,requestTried/2);
| |||
}
| |||
| |||
if(copied<0)
| |||
{
| |||
Print("TPDR: live CopyTicks retries exhausted. Requested=",requestTried,
| |||
" CursorMsc=",CursorMsc," Error=",lastError);
| |||
return false;
| |||
}
| |||
| |||
if(copied==0)
| |||
return true;
| |||
| |||
int start=0;
| |||
if(ticks[0].time_msc==CursorMsc)
| |||
start=MathMin(CursorSameMsc,copied);
| |||
| |||
int processed=copied-start;
| |||
for(int i=start;i<copied;i++)
| |||
ProcessBidTick(ticks[i],true,true,true);
| |||
| |||
UpdateCursorFromArray(ticks,copied,start);
| |||
| |||
if(copied<requestTried || processed==0)
| |||
return true;
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
| |||
| |||
// DASHBOARD отображает TickLevelRows[]; отдельной расчётной модели у GUI нет.
| |||
// Таблица: LEVEL, DIRECTION, REVERSAL AT, ALERT, GRAPHIC и LAST.
| |||
| |||
| |||
#define DASH_INVALID_GRID (-2147483647)
| |||
// Единый контракт LEVEL: каждая строка задаёт собственную ценовую сетку.
| |||
// Допускается любое целое значение LEVEL от 1 до DASH_VISUAL_LEVEL_MAX.
| |||
#define DASH_DEFAULT_VISUAL_ROWS "5,10,20,30,40,50"
| |||
#define DASH_PANEL_PREFIX "TPDRD_"
| |||
#define DASH_ROWS_STATE_PREFIX "TPDR_VISUAL_"
| |||
#define DASH_KEY_ESCAPE 27
| |||
input string InitialVisualRows=DASH_DEFAULT_VISUAL_ROWS; // Начальные Visual ROWS при первом запуске; рабочие ROWS сохраняются отдельно
| |||
input int UpdateIntervalMs=50; // Быстрое обновление dashboard
| |||
input int PanelLeftMargin=12; // Отступ слева
| |||
input int PanelTopMargin=12; // Отступ сверху
| |||
input int RowHeight=25; // Высота строки
| |||
input int HeaderHeight=72; // Высота заголовка
| |||
input int DashboardFontSize=11; // Единый размер шрифта всей таблицы, включая заголовки, цифры и кнопки
| |||
input color AlertOnColor=clrOrange; // Цвет включенного ALERT, ожидающего следующего пересечения
| |||
input color AlertOffColor=C'55,62,74'; // Цвет выключенного ALERT
| |||
input color AlertActivityColor=clrLimeGreen; // Цвет краткой индикации фактического срабатывания ALERT
| |||
input int AlertActivityMs=500; // Длительность индикации фактического срабатывания ALERT в миллисекундах
| |||
input color AlertTextColor=clrWhite; // Цвет текста кнопки ALERT
| |||
input color PanelBackground=C'16,20,28'; // Фон панели
| |||
input color RowBackground=C'29,35,46'; // Фон строки
| |||
input color RowBackgroundAlt=C'24,30,40'; // Чередующийся фон
| |||
input color BorderColor=C'75,88,108'; // Граница
| |||
input color HeaderColor=clrDeepSkyBlue; // Акцент заголовка
| |||
input color TextColor=clrWhiteSmoke; // Основной текст
| |||
input color MutedTextColor=C'155,165,180'; // Вторичный текст
| |||
input color UpColor=clrLimeGreen; // UP
| |||
input color DownColor=clrTomato; // DOWN
| |||
input color WaitColor=clrSilver; // WAIT
| |||
| |||
struct DashVisualRow
| |||
{
| |||
int level;
| |||
};
| |||
| |||
// TickLevelRows[] уже объявлен как динамический массив выше и является единым runtime-состоянием.
| |||
DashVisualRow DashVisualRows[DASH_DASHBOARD_ROWS];
| |||
int DashVisualRowCount=0;
| |||
bool DashAlertEnabled[DASH_DASHBOARD_ROWS];
| |||
ulong DashAlertActivityUntil[DASH_DASHBOARD_ROWS];
| |||
| |||
string DashPrefix=DASH_PANEL_PREFIX;
| |||
string DashShortName="CodeLock Dashboard";
| |||
string DashRowsStateKey="";
| |||
bool DashVisualRowsLoaded=false;
| |||
| |||
int DashColLevelX=12;
| |||
int DashColStatusX=0;
| |||
int DashColTriggerX=0;
| |||
int DashColAlertX=0;
| |||
int DashColGraphicX=0;
| |||
int DashAutoPanelWidth=0;
| |||
int DashLevelButtonWidth=72;
| |||
int DashRowsButtonWidth=27;
| |||
int DashRowsValueWidth=48;
| |||
int DashEditingRow=-1;
| |||
| |||
int DashMeasureTextWidth(const string text)
| |||
{
| |||
uint width=0;
| |||
uint height=0;
| |||
TextSetFont("Segoe UI",-DashboardFontSize*10,FW_NORMAL);
| |||
if(!TextGetSize(text,width,height))
| |||
return MathMax(1,StringLen(text)*DashboardFontSize);
| |||
return (int)width;
| |||
}
| |||
| |||
void DashCalculateAutoLayout(const double bid)
| |||
{
| |||
int levelWidth=DashMeasureTextWidth("LEVEL");
| |||
int statusWidth=DashMeasureTextWidth("DIRECTION");
| |||
int triggerWidth=DashMeasureTextWidth("REVERSAL AT");
| |||
int alertWidth=DashMeasureTextWidth("ALERT")+16;
| |||
int graphicWidth=DashMeasureTextWidth("GRAPHIC")+16;
| |||
levelWidth=MathMax(levelWidth,DashMeasureTextWidth("100000 pt"));
| |||
| |||
int gap=5;
| |||
DashColLevelX=12;
| |||
DashLevelButtonWidth=MathMax(66,levelWidth+12);
| |||
DashColStatusX=DashColLevelX+DashLevelButtonWidth+gap;
| |||
DashColTriggerX=DashColStatusX+statusWidth+gap;
| |||
DashColAlertX=DashColTriggerX+triggerWidth+gap;
| |||
DashColGraphicX=DashColAlertX+alertWidth+gap;
| |||
DashAutoPanelWidth=DashColGraphicX+graphicWidth+24;
| |||
}
| |||
| |||
int DashGetLevelCount()
| |||
{
| |||
return DashVisualRowCount;
| |||
}
| |||
| |||
int DashGetLevel(const int index)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount) return 0;
| |||
return DashVisualRows[index].level;
| |||
}
| |||
| |||
bool DashIsValidVisualLevel(const int level)
| |||
{
| |||
// LEVEL задаётся пользователем целым числом points без обязательного шага 10.
| |||
return level>=1 && level<=DASH_VISUAL_LEVEL_MAX;
| |||
}
| |||
| |||
bool DashIsStrictVisualRows()
| |||
{
| |||
if(DashVisualRowCount<DASH_MIN_VISUAL_ROWS || DashVisualRowCount>DASH_DASHBOARD_ROWS) return false;
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
{
| |||
if(!DashIsValidVisualLevel(DashVisualRows[i].level)) return false;
| |||
if(i>0 && DashVisualRows[i].level<=DashVisualRows[i-1].level) return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
bool DashParseVisualRows(const string source)
| |||
{
| |||
string parts[];
| |||
int count=StringSplit(source,',',parts);
| |||
if(count<DASH_MIN_VISUAL_ROWS || count>DASH_DASHBOARD_ROWS) return false;
| |||
DashVisualRowCount=0;
| |||
for(int i=0;i<count;i++)
| |||
{
| |||
string token=parts[i];
| |||
if(token=="") return false;
| |||
for(int k=0;k<StringLen(token);k++)
| |||
{
| |||
ushort ch=StringGetCharacter(token,k);
| |||
if(ch<'0' || ch>'9') return false;
| |||
}
| |||
int level=(int)StringToInteger(token);
| |||
if(!DashIsValidVisualLevel(level)) return false;
| |||
if(i>0 && level<=DashVisualRows[i-1].level) return false;
| |||
DashVisualRows[DashVisualRowCount].level=level;
| |||
DashVisualRowCount++;
| |||
}
| |||
return DashIsStrictVisualRows();
| |||
}
| |||
| |||
string DashVisualRowsKeyBase()
| |||
{
| |||
// Visual ROWS belong to the chart, not to its timeframe. A timeframe
| |||
// change reinitializes the indicator, so the persisted key must not contain
| |||
// _Period; otherwise the dashboard silently returns to InitialVisualRows.
| |||
if(DashRowsStateKey=="")
| |||
DashRowsStateKey=DASH_ROWS_STATE_PREFIX+
| |||
IntegerToString(ChartID())+"_"+_Symbol;
| |||
return DashRowsStateKey;
| |||
}
| |||
| |||
string DashLegacyVisualRowsKeyBase()
| |||
{
| |||
return DASH_ROWS_STATE_PREFIX+
| |||
IntegerToString(ChartID())+"_"+_Symbol+
| |||
"_TF"+IntegerToString((int)_Period);
| |||
}
| |||
| |||
bool DashLoadVisualRowsFromKey(const string base)
| |||
{
| |||
if(!GlobalVariableCheck(base+"_COUNT"))
| |||
return false;
| |||
| |||
int count=(int)MathRound(GlobalVariableGet(base+"_COUNT"));
| |||
if(count<1 || count>DASH_DASHBOARD_ROWS)
| |||
return false;
| |||
| |||
DashVisualRowCount=0;
| |||
for(int i=0;i<count;i++)
| |||
{
| |||
string key=base+"_R"+IntegerToString(i);
| |||
if(!GlobalVariableCheck(key))
| |||
return false;
| |||
| |||
int level=(int)MathRound(GlobalVariableGet(key));
| |||
if(!DashIsValidVisualLevel(level))
| |||
return false;
| |||
if(i>0 && level<=DashVisualRows[i-1].level)
| |||
return false;
| |||
| |||
DashVisualRows[i].level=level;
| |||
DashVisualRowCount++;
| |||
}
| |||
| |||
return DashIsStrictVisualRows();
| |||
}
| |||
| |||
void DashSaveVisualRows()
| |||
{
| |||
string base=DashVisualRowsKeyBase();
| |||
GlobalVariableSet(base+"_COUNT",(double)DashVisualRowCount);
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
GlobalVariableSet(base+"_R"+IntegerToString(i),(double)DashVisualRows[i].level);
| |||
for(int i=DashVisualRowCount;i<DASH_DASHBOARD_ROWS;i++)
| |||
GlobalVariableDel(base+"_R"+IntegerToString(i));
| |||
}
| |||
| |||
bool DashLoadPersistedVisualRows()
| |||
{
| |||
string base=DashVisualRowsKeyBase();
| |||
| |||
// New storage is stable across timeframe changes.
| |||
if(DashLoadVisualRowsFromKey(base))
| |||
return true;
| |||
| |||
// One-time migration from the previous timeframe-specific storage.
| |||
// This prevents an already configured dashboard from losing its ROWS
| |||
// immediately after updating the indicator.
| |||
string legacyBase=DashLegacyVisualRowsKeyBase();
| |||
if(legacyBase==base)
| |||
return false;
| |||
| |||
if(!DashLoadVisualRowsFromKey(legacyBase))
| |||
return false;
| |||
| |||
DashSaveVisualRows();
| |||
return true;
| |||
}
| |||
| |||
| |||
void DashLoadVisualRows()
| |||
{
| |||
DashVisualRowsLoaded=false;
| |||
if(!DashLoadPersistedVisualRows())
| |||
{
| |||
if(!DashParseVisualRows(InitialVisualRows))
| |||
{
| |||
if(!DashParseVisualRows(DASH_DEFAULT_VISUAL_ROWS)) return;
| |||
Print("TPDR Dashboard: InitialVisualRows invalid; using default Visual ROWS.");
| |||
}
| |||
}
| |||
| |||
// Preserve the user's exact LEVEL list; do not force-add or rewrite a 5 pt row.
| |||
DashVisualRowsLoaded=true;
| |||
}
| |||
| |||
int DashFloorGrid(const double price,const double stepPrice)
| |||
{
| |||
if(stepPrice<=0.0)
| |||
return 0;
| |||
return (int)MathFloor((price-StartPrice)/stepPrice);
| |||
}
| |||
| |||
int DashCeilGrid(const double price,const double stepPrice)
| |||
{
| |||
if(stepPrice<=0.0)
| |||
return 0;
| |||
return (int)MathCeil((price-StartPrice)/stepPrice);
| |||
}
| |||
| |||
double DashGridPrice(const int stateIndex,const int grid)
| |||
{
| |||
return TickRowPrice(stateIndex,grid);
| |||
}
| |||
| |||
void DashFireLevelAlert(const int stateIndex,const int grid,const int direction)
| |||
{
| |||
if(stateIndex<0 || stateIndex>=DashVisualRowCount)
| |||
return;
| |||
| |||
double alertPrice=DashGridPrice(stateIndex,grid);
| |||
| |||
string message=StringFormat("%s: ALERT %s LEVEL=%d GRID=%d REVERSAL PRICE=%s",
| |||
_Symbol,
| |||
(direction>0 ? "UP" : "DOWN"),
| |||
TickLevelRows[stateIndex].level,
| |||
grid,
| |||
DoubleToString(alertPrice,_Digits));
| |||
| |||
DashAlertEnabled[stateIndex]=false;
| |||
DashAlertActivityUntil[stateIndex]=(ulong)GetTickCount()+(ulong)MathMax(0,AlertActivityMs);
| |||
DashSetAlertButtonState(stateIndex);
| |||
ChartRedraw(0);
| |||
| |||
// NotifyEvent() sends alerts; this function updates only button state and display.
| |||
}
| |||
| |||
bool DashIsAlertButtonName(const string name,int &stateIndex)
| |||
{
| |||
string prefix=DashPrefix+"ALERT_";
| |||
if(StringFind(name,prefix)!=0)
| |||
return false;
| |||
| |||
string suffix=StringSubstr(name,StringLen(prefix));
| |||
if(suffix=="")
| |||
return false;
| |||
| |||
int index=(int)StringToInteger(suffix);
| |||
if(index<0 || index>=DASH_DASHBOARD_ROWS)
| |||
return false;
| |||
| |||
stateIndex=index;
| |||
return true;
| |||
}
| |||
| |||
void DashSetAlertButtonState(const int index)
| |||
{
| |||
if(index<0 || index>=DASH_DASHBOARD_ROWS)
| |||
return;
| |||
| |||
string name=DashPrefix+"ALERT_"+IntegerToString(index);
| |||
if(ObjectFind(0,name)<0)
| |||
return;
| |||
| |||
bool enabled=DashAlertEnabled[index];
| |||
ulong now=(ulong)GetTickCount();
| |||
bool activity=(DashAlertActivityUntil[index]>now);
| |||
| |||
color buttonColor=activity ? AlertActivityColor :
| |||
(enabled ? AlertOnColor : AlertOffColor);
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,buttonColor);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,AlertTextColor);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,
| |||
activity ? AlertActivityColor :
| |||
(enabled ? AlertOnColor : BorderColor));
| |||
ObjectSetString(0,name,OBJPROP_TEXT,"ALERT");
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,enabled);
| |||
}
| |||
| |||
void DashUpdateAlertButton(const int index,const int x,const int y,const int width,const int height)
| |||
{
| |||
string name=DashPrefix+"ALERT_"+IntegerToString(index);
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: alert button creation failed. Error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,140);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,DashboardFontSize);
| |||
DashSetAlertButtonState(index);
| |||
}
| |||
| |||
void DashUpdateGraphicButton(const int index,const int x,const int y,const int width,const int height)
| |||
{
| |||
string name=DashPrefix+"GRAPHIC_"+IntegerToString(index);
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: graphic button creation failed. Error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,140);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
| |||
}
| |||
| |||
bool selected=(index<DashVisualRowCount &&
| |||
DashVisualRows[index].level==GraphLevelsPerSide);
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,DashboardFontSize);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,
| |||
selected ? HeaderColor : C'42,50,64');
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,
| |||
selected ? HeaderColor : BorderColor);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,TextColor);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,"GRAPHIC");
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,selected);
| |||
}
| |||
| |||
void DashSelectGraphicLevel(const int index)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount)
| |||
return;
| |||
| |||
int selectedLevel=DashVisualRows[index].level;
| |||
if(selectedLevel<GraphLevelsPerSideMin)
| |||
return;
| |||
| |||
GraphLevelsPerSide=MathMax(GraphLevelsPerSideMin,
| |||
MathMin(GraphLevelsPerSideMax,
| |||
(selectedLevel/10)*10));
| |||
SyncTickVisualRowsFromDashboard();
| |||
ResetGraphToCurrentPrice();
| |||
RefreshGraph();
| |||
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
| |||
}
| |||
| |||
bool DashIsGraphicButtonName(const string name,int &stateIndex)
| |||
{
| |||
string prefix=DashPrefix+"GRAPHIC_";
| |||
if(StringFind(name,prefix)!=0)
| |||
return false;
| |||
| |||
string suffix=StringSubstr(name,StringLen(prefix));
| |||
if(suffix=="")
| |||
return false;
| |||
| |||
int index=(int)StringToInteger(suffix);
| |||
if(index<0 || index>=DASH_DASHBOARD_ROWS)
| |||
return false;
| |||
| |||
stateIndex=index;
| |||
return true;
| |||
}
| |||
| |||
void DashToggleAlert(const int index)
| |||
{
| |||
if(index<0 || index>=DASH_DASHBOARD_ROWS)
| |||
return;
| |||
| |||
// Каждый клик заново устанавливает/снимает режим ожидания.
| |||
// После повторной активации следующий подходящий crossing снова обязан вызвать ALERT.
| |||
DashAlertEnabled[index]=!DashAlertEnabled[index];
| |||
if(index<TickLevelRowCount)
| |||
TickLevelRows[index].alertEnabled=DashAlertEnabled[index];
| |||
DashAlertActivityUntil[index]=0;
| |||
| |||
string stateText=(DashAlertEnabled[index] ? "включен" : "выключен");
| |||
Print("TPDR Dashboard: ALERT ",TickLevelRows[index].level," pt ",stateText);
| |||
| |||
DashSetAlertButtonState(index);
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void DashDeleteDashboardObjects()
| |||
{
| |||
ObjectsDeleteAll(0,DashPrefix,-1,-1);
| |||
}
| |||
| |||
void DashEnsureRect(const string name,const int x,const int y,const int width,const int height,
| |||
const color background,const color border)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: object creation failed. Error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,border);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,100);
| |||
}
| |||
| |||
void DashEnsureLabel(const string name,const int x,const int y,const int fontSize,
| |||
const color textColor,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: label creation failed. Error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,110);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,textColor);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
void DashDeleteHiddenDashboardRows(const int visibleCount)
| |||
{
| |||
for(int i=visibleCount;i<DASH_DASHBOARD_ROWS;i++)
| |||
{
| |||
ObjectDelete(0,DashPrefix+"ROW_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"STATEBAR_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"MINUS_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"LEVEL_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"STATE_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"TRIGGER_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"ALERT_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"GRAPHIC_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"LAST_DOWN_2_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"LAST_DOWN_1_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"LAST_UP_2_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"LAST_UP_1_"+IntegerToString(i));
| |||
}
| |||
}
| |||
| |||
void DashEnsureButton(const string name,const int x,const int y,const int width,const int height,
| |||
const string textValue,const color background,const color border,
| |||
const color textColor,const long zorder=150)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: button creation failed. Error=",GetLastError()," Name=",name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,zorder);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,DashboardFontSize);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,border);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,textColor);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
void DashBeginLevelEdit(const int index,const int x,const int y,const int width,const int height)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount) return;
| |||
if(DashEditingRow>=0 && DashEditingRow!=index)
| |||
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(DashEditingRow));
| |||
| |||
string name=DashPrefix+"EDIT_"+IntegerToString(index);
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
if(!ObjectCreate(0,name,OBJ_EDIT,SubWindow,0,0))
| |||
{
| |||
Print("TPDR Dashboard: level edit creation failed. Error=",GetLastError());
| |||
return;
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,180);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Segoe UI");
| |||
ObjectSetInteger(0,name,OBJPROP_ALIGN,ALIGN_CENTER);
| |||
ObjectSetInteger(0,name,OBJPROP_READONLY,false);
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,DashboardFontSize);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,TextColor);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,PanelBackground);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,HeaderColor);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,IntegerToString(DashVisualRows[index].level));
| |||
DashEditingRow=index;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void DashCancelLevelEdit()
| |||
{
| |||
if(DashEditingRow<0) return;
| |||
ObjectDelete(0,DashPrefix+"EDIT_"+IntegerToString(DashEditingRow));
| |||
DashEditingRow=-1;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
bool DashValidateEditedLevel(const int index,const int newLevel)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount || !DashIsValidVisualLevel(newLevel)) return false;
| |||
if(index>0 && newLevel<=DashVisualRows[index-1].level) return false;
| |||
if(index+1<DashVisualRowCount && newLevel>=DashVisualRows[index+1].level) return false;
| |||
return true;
| |||
}
| |||
| |||
void DashCommitEditedLevel(const int index,const string text)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount) { DashCancelLevelEdit(); return; }
| |||
if(text=="") { Print("TPDR Dashboard: LEVEL edit rejected: empty value."); DashCancelLevelEdit(); return; }
| |||
| |||
for(int k=0;k<StringLen(text);k++)
| |||
{
| |||
ushort ch=StringGetCharacter(text,k);
| |||
if(ch<'0' || ch>'9')
| |||
{
| |||
Print("TPDR Dashboard: LEVEL edit rejected: enter an integer from 1 to 2000 pt.");
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
}
| |||
| |||
int newLevel=(int)StringToInteger(text);
| |||
if(!DashValidateEditedLevel(index,newLevel))
| |||
{
| |||
Print("TPDR Dashboard: LEVEL edit rejected: ",text,
| |||
". Allowed: integer 1..2000 pt; levels must remain strictly increasing.");
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
DashVisualRows[index].level=newLevel;
| |||
DashSaveVisualRows();
| |||
DashCancelLevelEdit();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("TPDR Dashboard: Visual ROW ",index," LEVEL changed to ",newLevel," pt.");
| |||
}
| |||
| |||
bool DashAddVisualRow(const int index)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount || DashVisualRowCount>=DASH_DASHBOARD_ROWS) return false;
| |||
| |||
int current=DashVisualRows[index].level;
| |||
int upper=(index+1<DashVisualRowCount ? DashVisualRows[index+1].level :
| |||
DASH_VISUAL_LEVEL_MAX+1);
| |||
// Кнопка + строка добавляет следующий уровень с фиксированным шагом 50 pt.
| |||
// Базовая сетка остаётся 10/20/30/40/50; первый добавленный уровень = 100.
| |||
const int addStep=50;
| |||
int candidate=current+addStep;
| |||
| |||
while(candidate<upper && candidate<=DASH_VISUAL_LEVEL_MAX && candidate>current)
| |||
{
| |||
bool free=true;
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
if(DashVisualRows[i].level==candidate) { free=false; break; }
| |||
| |||
if(free)
| |||
{
| |||
for(int i=DashVisualRowCount;i>index+1;i--)
| |||
DashVisualRows[i]=DashVisualRows[i-1];
| |||
DashVisualRows[index+1].level=candidate;
| |||
DashVisualRowCount++;
| |||
DashSaveVisualRows();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("TPDR Dashboard: added Visual ROW ",candidate," pt after ",current," pt.");
| |||
return true;
| |||
}
| |||
candidate+=addStep;
| |||
}
| |||
| |||
Print("TPDR Dashboard: cannot add ROW after ",current,
| |||
" pt: no free valid level preserves strict order.");
| |||
return false;
| |||
}
| |||
| |||
bool DashDeleteVisualRow(const int index)
| |||
{
| |||
if(index<0 || index>=DashVisualRowCount) return false;
| |||
if(DashVisualRows[index].level==5)
| |||
{
| |||
Print("TPDR Dashboard: mandatory ROW 5 pt cannot be deleted.");
| |||
return false;
| |||
}
| |||
if(DashVisualRowCount<=DASH_MIN_VISUAL_ROWS)
| |||
{
| |||
Print("TPDR Dashboard: at least five Visual ROWS must remain.");
| |||
return false;
| |||
}
| |||
| |||
int removed=DashVisualRows[index].level;
| |||
for(int i=index;i<DashVisualRowCount-1;i++)
| |||
DashVisualRows[i]=DashVisualRows[i+1];
| |||
DashVisualRowCount--;
| |||
DashSaveVisualRows();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("TPDR Dashboard: deleted Visual ROW ",removed," pt.");
| |||
return true;
| |||
}
| |||
| |||
| |||
// --- Dashboard visual status helpers: presentation only.
| |||
color DashStatusColor(const int direction)
| |||
{
| |||
if(direction>0) return UpColor;
| |||
if(direction<0) return DownColor;
| |||
return WaitColor;
| |||
}
| |||
| |||
string DashStatusText(const int direction)
| |||
{
| |||
if(direction>0) return "UP";
| |||
if(direction<0) return "DOWN";
| |||
return "WAIT";
| |||
}
| |||
| |||
void DashRenderDashboard(const double bid)
| |||
{
| |||
// During a rebuild keep the last complete dashboard snapshot visible.
| |||
// Before the first completed bootstrap, never publish placeholder model values.
| |||
if(!BootstrapComplete && DashboardHasPublishedState)
| |||
return;
| |||
| |||
bool modelReady=BootstrapComplete;
| |||
int levelCount=DashVisualRowCount;
| |||
DashCalculateAutoLayout(bid);
| |||
ObjectDelete(0,DashPrefix+"TITLE");
| |||
| |||
int panelHeight=HeaderHeight+8+levelCount*RowHeight+12;
| |||
int panelX=MathMax(4,PanelLeftMargin);
| |||
int panelY=MathMax(4,PanelTopMargin);
| |||
| |||
DashEnsureRect(DashPrefix+"BG",panelX,panelY,DashAutoPanelWidth,panelHeight,PanelBackground,BorderColor);
| |||
DashEnsureRect(DashPrefix+"HEADER",panelX+2,panelY+2,DashAutoPanelWidth-4,HeaderHeight-4,PanelBackground,PanelBackground);
| |||
| |||
int dashboardTitleWidth=DashMeasureTextWidth("DASHBOARD");
| |||
int dashboardTitleX=panelX+(DashAutoPanelWidth-dashboardTitleWidth)/2;
| |||
DashEnsureLabel(DashPrefix+"DASHBOARD_TITLE",dashboardTitleX,panelY+6,
| |||
DashboardFontSize,TextColor,"DASHBOARD");
| |||
| |||
// ROWS +/- стоят у левого края панели; GRAPHIC LEVELS +/- — у правого.
| |||
int controlsY=panelY+34;
| |||
int levelHeaderY=controlsY;
| |||
int levelButtonWidth=24;
| |||
int levelValueWidth=36;
| |||
int levelLabelWidth=DashMeasureTextWidth("GRAPHIC LEVELS");
| |||
int levelControlsWidth=levelLabelWidth+6+levelButtonWidth+4+levelValueWidth+4+levelButtonWidth;
| |||
int levelHeaderX=panelX+DashAutoPanelWidth-levelControlsWidth-2;
| |||
int levelMinusX=levelHeaderX+levelLabelWidth+6;
| |||
int levelValueX=levelMinusX+levelButtonWidth+4;
| |||
int levelPlusX=levelValueX+levelValueWidth+4;
| |||
| |||
DashEnsureLabel(DashPrefix+"GRAPH_ROWS_LABEL",levelHeaderX,levelHeaderY,
| |||
DashboardFontSize,MutedTextColor,"GRAPHIC LEVELS");
| |||
DashEnsureButton(DashPrefix+"GRAPH_ROWS_MINUS",levelMinusX,levelHeaderY-3,
| |||
levelButtonWidth,22,"-",C'42,50,64',BorderColor,TextColor);
| |||
DashEnsureLabel(DashPrefix+"GRAPH_ROWS_VALUE",levelValueX+2,levelHeaderY,
| |||
DashboardFontSize,TextColor,IntegerToString(GraphDisplayedLevelCount));
| |||
DashEnsureButton(DashPrefix+"GRAPH_ROWS_PLUS",levelPlusX,levelHeaderY-3,
| |||
levelButtonWidth,22,"+",C'42,50,64',BorderColor,TextColor);
| |||
| |||
int bidWidth=DashMeasureTextWidth("BID "+DoubleToString(bid,_Digits));
| |||
DashEnsureLabel(DashPrefix+"BID",panelX+DashAutoPanelWidth-bidWidth-16,panelY+8,DashboardFontSize,HeaderColor,
| |||
"BID "+DoubleToString(bid,_Digits));
| |||
| |||
// Удаляем старые построчные +/- объекты, включая объекты от предыдущей версии.
| |||
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
| |||
{
| |||
ObjectDelete(0,DashPrefix+"MINUS_"+IntegerToString(i));
| |||
ObjectDelete(0,DashPrefix+"PLUS_"+IntegerToString(i));
| |||
}
| |||
| |||
int headerY=panelY+HeaderHeight-20;
| |||
| |||
int rowsControlsX=panelX+2;
| |||
int rowsControlsY=controlsY;
| |||
DashEnsureLabel(DashPrefix+"ROWS_LABEL",rowsControlsX,rowsControlsY,
| |||
DashboardFontSize,MutedTextColor,"ROWS");
| |||
int rowsX=rowsControlsX+DashMeasureTextWidth("ROWS")+6;
| |||
DashEnsureButton(DashPrefix+"ROWS_MINUS",rowsX,rowsControlsY-3,
| |||
DashRowsButtonWidth,22,"-",
| |||
C'42,50,64',BorderColor,TextColor);
| |||
DashEnsureLabel(DashPrefix+"ROWS_VALUE",
| |||
rowsX+DashRowsButtonWidth+4,rowsControlsY,
| |||
DashboardFontSize,TextColor,IntegerToString(levelCount));
| |||
DashEnsureButton(DashPrefix+"ROWS_PLUS",
| |||
rowsX+DashRowsButtonWidth+DashRowsValueWidth+8,
| |||
rowsControlsY-3,DashRowsButtonWidth,22,"+",
| |||
C'42,50,64',BorderColor,TextColor);
| |||
DashEnsureLabel(DashPrefix+"COL_LEVEL",panelX+DashColLevelX,headerY,
| |||
DashboardFontSize,MutedTextColor,"LEVEL");
| |||
DashEnsureLabel(DashPrefix+"COL_STATUS",panelX+DashColStatusX,headerY,DashboardFontSize,MutedTextColor,"DIRECTION");
| |||
DashEnsureLabel(DashPrefix+"COL_TRIGGER",panelX+DashColTriggerX,headerY,DashboardFontSize,MutedTextColor,"REVERSAL AT");
| |||
DashEnsureLabel(DashPrefix+"COL_ALERT",panelX+DashColAlertX,headerY,DashboardFontSize,MutedTextColor,"ALERT");
| |||
DashEnsureLabel(DashPrefix+"COL_GRAPHIC",panelX+DashColGraphicX,headerY,DashboardFontSize,MutedTextColor,"GRAPHIC");
| |||
| |||
for(int row=0;row<levelCount;row++)
| |||
{
| |||
int index=levelCount-1-row;
| |||
int y=panelY+HeaderHeight+8+row*RowHeight;
| |||
string suffix=IntegerToString(index);
| |||
int direction=(modelReady ? TickLevelRows[index].direction : 0);
| |||
| |||
DashEnsureRect(DashPrefix+"ROW_"+suffix,panelX+2,y,DashAutoPanelWidth-4,RowHeight-1,
| |||
(row%2==0 ? RowBackground : RowBackgroundAlt),
| |||
(row==0 || row==levelCount-1 ? BorderColor :
| |||
(row%5==0 ? BorderColor : RowBackgroundAlt)));
| |||
DashEnsureRect(DashPrefix+"STATEBAR_"+suffix,panelX+3,y+2,5,RowHeight-5,
| |||
DashStatusColor(direction),DashStatusColor(direction));
| |||
| |||
if(DashEditingRow!=index)
| |||
DashEnsureButton(DashPrefix+"LEVEL_"+suffix,panelX+DashColLevelX,y+1,DashLevelButtonWidth,RowHeight-3,
| |||
IntegerToString(DashVisualRows[index].level)+" pt",RowBackground,BorderColor,
| |||
DashStatusColor(direction));
| |||
else
| |||
ObjectDelete(0,DashPrefix+"LEVEL_"+suffix);
| |||
| |||
DashEnsureLabel(DashPrefix+"STATE_"+suffix,panelX+DashColStatusX,y+3,DashboardFontSize,
| |||
DashStatusColor(direction),(modelReady ? DashStatusText(direction) : "LOADING"));
| |||
| |||
string reversalAtText=(modelReady && TickLevelRows[index].direction!=0 &&
| |||
TickLevelRows[index].reversalAtPrice>0.0 ?
| |||
DoubleToString(TickLevelRows[index].reversalAtPrice,_Digits) : "");
| |||
// REVERSAL AT показывает цену разворота и окрашивается цветом
| |||
// будущего направления: UP -> DownColor, DOWN -> UpColor.
| |||
int reversalDirection=(direction!=0 ? -direction : 0);
| |||
color reversalAtColor=DashStatusColor(reversalDirection);
| |||
DashEnsureLabel(DashPrefix+"TRIGGER_"+suffix,panelX+DashColTriggerX,y+3,DashboardFontSize,
| |||
reversalAtColor,reversalAtText);
| |||
| |||
int alertButtonWidth=DashMeasureTextWidth("ALERT")+16;
| |||
DashUpdateAlertButton(index,panelX+DashColAlertX,y+1,alertButtonWidth,RowHeight-3);
| |||
| |||
int graphicButtonWidth=DashMeasureTextWidth("GRAPHIC")+16;
| |||
DashUpdateGraphicButton(index,panelX+DashColGraphicX,y+1,graphicButtonWidth,RowHeight-3);
| |||
| |||
// LAST-DOWN/LAST-UP columns are intentionally not displayed.
| |||
}
| |||
| |||
DashDeleteHiddenDashboardRows(levelCount);
| |||
ChartRedraw(0);
| |||
if(modelReady)
| |||
DashboardHasPublishedState=true;
| |||
}
| |||
| |||
int DashInit()
| |||
{
| |||
if(StartPrice<=0.0 || BufferPoints<0 || HistoryBootstrapTicks<100 || HistoryChunkSize<100 ||
| |||
UpdateIntervalMs<10 || UpdateIntervalMs>2000 ||
| |||
RowHeight<20 || HeaderHeight<45 || DashboardFontSize<7 ||
| |||
DashboardFontSize>18 || PanelLeftMargin<0 || PanelTopMargin<0 ||
| |||
AlertActivityMs<0 || AlertActivityMs>10000)
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
| |||
IndicatorSetString(INDICATOR_SHORTNAME,DashShortName);
| |||
| |||
MqlTick attachTick;
| |||
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
| |||
return INIT_FAILED;
| |||
| |||
DashLoadVisualRows();
| |||
if(!DashVisualRowsLoaded || !DashIsStrictVisualRows())
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
| |||
// Remove dashboard objects from older versions before recreating them
| |||
// in the indicator subwindow instead of the main chart.
| |||
DashDeleteDashboardObjects();
| |||
| |||
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
| |||
{
| |||
DashAlertEnabled[i]=false;
| |||
DashAlertActivityUntil[i]=0;
| |||
}
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void DashChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
| |||
{
| |||
if(id==CHARTEVENT_KEYDOWN && DashEditingRow>=0 && lparam==DASH_KEY_ESCAPE)
| |||
{
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_OBJECT_ENDEDIT)
| |||
{
| |||
string editPrefix=DashPrefix+"EDIT_";
| |||
if(StringFind(sparam,editPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(StringSubstr(sparam,StringLen(editPrefix)));
| |||
string textValue="";
| |||
if(ObjectGetString(0,sparam,OBJPROP_TEXT,0,textValue))
| |||
DashCommitEditedLevel(index,textValue);
| |||
else
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id!=CHARTEVENT_OBJECT_CLICK)
| |||
return;
| |||
| |||
if(sparam==DashPrefix+"GRAPH_ROWS_MINUS" ||
| |||
sparam==DashPrefix+"GRAPH_ROWS_PLUS")
| |||
{
| |||
HandleLevelRowsButtonClick(sparam);
| |||
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
| |||
return;
| |||
}
| |||
| |||
if(sparam==DashPrefix+"ROWS_MINUS")
| |||
{
| |||
// Удаляется только самый высокий добавленный LEVEL.
| |||
if(DashVisualRowCount>DASH_MIN_VISUAL_ROWS &&
| |||
DashDeleteVisualRow(DashVisualRowCount-1))
| |||
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
| |||
return;
| |||
}
| |||
| |||
if(sparam==DashPrefix+"ROWS_PLUS")
| |||
{
| |||
// Новый LEVEL добавляется только сверху с шагом 50 pt: 50 -> 100 -> 150 -> ...
| |||
if(DashVisualRowCount<DASH_DASHBOARD_ROWS &&
| |||
DashAddVisualRow(DashVisualRowCount-1))
| |||
DashRenderDashboard(SymbolInfoDouble(_Symbol,SYMBOL_BID));
| |||
return;
| |||
}
| |||
| |||
string levelPrefix=DashPrefix+"LEVEL_";
| |||
if(StringFind(sparam,levelPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(StringSubstr(sparam,StringLen(levelPrefix)));
| |||
if(index>=0 && index<DashVisualRowCount)
| |||
{
| |||
string rowName=DashPrefix+"ROW_"+IntegerToString(index);
| |||
int y=(ObjectFind(0,rowName)>=0 ?
| |||
(int)ObjectGetInteger(0,rowName,OBJPROP_YDISTANCE) :
| |||
PanelTopMargin+HeaderHeight+8);
| |||
DashBeginLevelEdit(index,
| |||
(int)ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE),
| |||
y+1,DashLevelButtonWidth,RowHeight-3);
| |||
}
| |||
return;
| |||
}
| |||
| |||
int graphicIndex=-1;
| |||
if(DashIsGraphicButtonName(sparam,graphicIndex))
| |||
{
| |||
DashSelectGraphicLevel(graphicIndex);
| |||
return;
| |||
}
| |||
| |||
int index=-1;
| |||
if(DashIsAlertButtonName(sparam,index))
| |||
{
| |||
DashToggleAlert(index);
| |||
return;
| |||
}
| |||
}
| |||
| |||
void DashDeinit(const int reason)
| |||
{
| |||
| |||
DashDeleteDashboardObjects();
| |||
}
| |||
| |||
void DashTimer()
| |||
{
| |||
if(!BootstrapComplete)
| |||
return;
| |||
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
| |||
return;
| |||
| |||
DashRenderDashboard(current.bid);
| |||
}
| |||
| |||
int DashCalculate(const int rates_total,const int prev_calculated)
| |||
{
| |||
return rates_total;
| |||
}
| |||
| |||
| |||
| |||
| |||
| |||
void SyncTickVisualRowsFromDashboard()
| |||
{
| |||
if(DashVisualRowCount<DASH_MIN_VISUAL_ROWS || DashVisualRowCount>DASH_VISUAL_ROW_MAX)
| |||
return;
| |||
| |||
int selectedLevel=GraphLevelsPerSide;
| |||
bool selectedExists=false;
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
{
| |||
if(DashVisualRows[i].level==selectedLevel)
| |||
{
| |||
selectedExists=true;
| |||
break;
| |||
}
| |||
}
| |||
| |||
int targetCount=DashVisualRowCount+(selectedExists ? 0 : 1);
| |||
bool changed=(TickLevelRowCount!=targetCount);
| |||
| |||
if(!changed)
| |||
{
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
{
| |||
if(TickLevelRows[i].level!=DashVisualRows[i].level)
| |||
{
| |||
changed=true;
| |||
break;
| |||
}
| |||
}
| |||
}
| |||
| |||
if(!changed && !selectedExists &&
| |||
(TickLevelRowCount<=DashVisualRowCount ||
| |||
TickLevelRows[DashVisualRowCount].level!=selectedLevel))
| |||
changed=true;
| |||
| |||
if(!changed)
| |||
return;
| |||
| |||
ArrayResize(TickLevelRows,targetCount);
| |||
TickLevelRowCount=targetCount;
| |||
| |||
for(int i=0;i<DASH_DASHBOARD_ROWS;i++)
| |||
{
| |||
DashAlertEnabled[i]=false;
| |||
DashAlertActivityUntil[i]=0;
| |||
}
| |||
| |||
for(int i=0;i<DashVisualRowCount;i++)
| |||
TickLevelRows[i].level=DashVisualRows[i].level;
| |||
| |||
if(!selectedExists)
| |||
TickLevelRows[DashVisualRowCount].level=selectedLevel;
| |||
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
| |||
if(ModelTickCount>=2)
| |||
{
| |||
// Новый LEVEL может потребовать дополнительной истории до публикации состояния.
| |||
HistoryBootstrapTargetTicks=MathMax(HistoryBootstrapTargetTicks,ModelTickCount);
| |||
BootstrapComplete=false;
| |||
LiveMode=false;
| |||
}
| |||
}
| |||
| |||
int TickInit()
| |||
{
| |||
if(StartPrice<=0.0 || BufferPoints<0 ||
| |||
ReversalArrowsHistory<0 || ReversalArrowsHistory>20 ||
| |||
MaxModelHistoryTicks<100 ||
| |||
HistoryChunkSize<100 || HistoryGraphTicks<100 || HistoryBootstrapTicks<100 ||
| |||
InitialViewTicks<1 || MinViewTicks<1 || MinViewTicks>InitialViewTicks ||
| |||
MinViewTicks>MathMax(MaxGraphSegments,MaxModelHistoryTicks))
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
| |||
| |||
ShortName=_Symbol;
| |||
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
| |||
LegacyPriceObjectsCleaned=false;
| |||
DeleteLegacyPriceObjects();
| |||
LegacyGraphLabelsCleaned=false;
| |||
LastGraphLabelNameCount=0;
| |||
ArrayResize(LastGraphLabelNames,0);
| |||
GraphLevelsPerSide=MathMax(GraphLevelsPerSideMin,
| |||
MathMin(GraphLevelsPerSideMax,
| |||
(GraphLevelsPerSide/10)*10));
| |||
if(GraphLevelsPerSide<GraphLevelsPerSideMin)
| |||
GraphLevelsPerSide=GraphLevelsPerSideMin;
| |||
LoadGraphDisplayedLevelCount();
| |||
GraphHistoryCapacity=MathMax(100,MathMax(MaxGraphSegments,MaxModelHistoryTicks));
| |||
SyncTickVisualRowsFromDashboard();
| |||
| |||
// Высоту подокна задаёт терминал; Canvas подстраивается под доступную область.
| |||
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,false);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,false);
| |||
| |||
MqlTick attachTick;
| |||
| |||
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
| |||
return INIT_FAILED;
| |||
| |||
AttachMsc=attachTick.time_msc;
| |||
CursorMsc=AttachMsc;
| |||
CursorSameMsc=0;
| |||
HistoryBootstrapTargetTicks=0;
| |||
| |||
ArrayResize(TickPrices,MathMax(100,GraphHistoryCapacity),
| |||
MathMax(100,GraphHistoryCapacity));
| |||
ArrayResize(SignalMarks,0);
| |||
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),
| |||
MathMax(100,MaxModelHistoryTicks));
| |||
ModelTickCount=0;
| |||
ModelTickHead=0;
| |||
TickCount=0;
| |||
TickHead=0;
| |||
TickSerial=0;
| |||
HavePreviousBid=false;
| |||
PreviousBid=0.0;
| |||
BootstrapComplete=false;
| |||
LiveMode=false;
| |||
| |||
ScaleCenterGrid=FloorGridForViewport(attachTick.bid);
| |||
ScaleInitialized=false;
| |||
AutoScaleInitialized=false;
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),GraphHistoryCapacity);
| |||
ViewEndOffset=0;
| |||
DragActive=false;
| |||
DragStartX=0;
| |||
DragStartOffset=0;
| |||
| |||
// Runtime-модель восстанавливается единым bootstrap-проходом.
| |||
FitInitialArrowHistory();
| |||
| |||
// Preserve chart-wide settings before enabling the events this indicator needs.
| |||
long previousMouseScroll=0;
| |||
long previousMouseMove=0;
| |||
long previousMouseWheel=0;
| |||
if(!ChartGetInteger(0,CHART_MOUSE_SCROLL,0,previousMouseScroll) ||
| |||
!ChartGetInteger(0,CHART_EVENT_MOUSE_MOVE,0,previousMouseMove) ||
| |||
!ChartGetInteger(0,CHART_EVENT_MOUSE_WHEEL,0,previousMouseWheel))
| |||
{
| |||
Print("TPDR: failed to read chart event settings. Error=",GetLastError());
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
PreviousChartMouseScroll=(previousMouseScroll!=0);
| |||
PreviousChartEventMouseMove=(previousMouseMove!=0);
| |||
PreviousChartEventMouseWheel=(previousMouseWheel!=0);
| |||
ChartEventSettingsCaptured=true;
| |||
MouseInTickSubwindow=false;
| |||
| |||
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true) ||
| |||
!ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,true) ||
| |||
!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll))
| |||
{
| |||
int error=GetLastError();
| |||
RestoreChartEventSettings();
| |||
Print("TPDR: failed to configure chart event settings. Error=",error);
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
bool RestoreChartEventSettings()
| |||
{
| |||
if(!ChartEventSettingsCaptured)
| |||
return true;
| |||
| |||
bool ok=true;
| |||
if(!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll))
| |||
ok=false;
| |||
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,(long)PreviousChartEventMouseMove))
| |||
ok=false;
| |||
if(!ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,(long)PreviousChartEventMouseWheel))
| |||
ok=false;
| |||
| |||
if(ok)
| |||
ChartEventSettingsCaptured=false;
| |||
else
| |||
Print("TPDR: failed to restore one or more chart event settings. Error=",GetLastError());
| |||
return ok;
| |||
}
| |||
| |||
void TickDeinit(const int reason)
| |||
{
| |||
RestoreChartEventSettings();
| |||
| |||
if(CanvasReady)
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady=false;
| |||
}
| |||
CanvasWidth=0;
| |||
CanvasHeight=0;
| |||
CanvasSubWindow=-1;
| |||
| |||
EventKillTimer();
| |||
ResetButtonHeld=false;
| |||
DeleteObjectsByPrefix();
| |||
}
| |||
| |||
bool UpdateMarketState()
| |||
{
| |||
// Единый владелец состояния рынка.
| |||
// OnCalculate() и OnTimer() только запускают эту функцию, но сама обработка
| |||
// history/live выполняется здесь ровно одним и тем же кодом.
| |||
if(SubWindow<0)
| |||
return false;
| |||
| |||
if(!BootstrapComplete)
| |||
{
| |||
if(!BootstrapHistory())
| |||
return false;
| |||
}
| |||
else
| |||
{
| |||
if(!ProcessLiveTicks())
| |||
return false;
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
void TickTimer()
| |||
{
| |||
// Таймер также может довести bootstrap до готового состояния: при создании
| |||
// подокна OnCalculate иногда вызывается раньше, чем ChartWindowFind() готов.
| |||
SubWindow=ChartWindowFind(0,ShortName);
| |||
if(SubWindow<0)
| |||
return;
| |||
| |||
// Пока bootstrap не завершён, состояние и отображение не публикуются.
| |||
// OnTimer() не имеет права рисовать промежуточную историю.
| |||
if(!UpdateMarketState())
| |||
return;
| |||
| |||
// RESET выполняется только по событию нажатия кнопки.
| |||
// Таймер не должен повторять тяжёлый полный reset каждые 100 мс.
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
| |||
return;
| |||
| |||
if(!EnsureCanvas())
| |||
return;
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending=false;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void HandleHorizontalZoom(const int xCursor,const int delta)
| |||
{
| |||
if(TickCount<=1 || GetGraphWidth()<=1 || delta==0)
| |||
return;
| |||
| |||
int oldSpan=GetViewCount();
| |||
int oldStart=GetViewStartIndex();
| |||
| |||
int newSpan=oldSpan;
| |||
if(delta>0)
| |||
newSpan=MathMax(MinViewTicks,oldSpan/2);
| |||
else
| |||
newSpan=MathMin(MathMin(GraphHistoryCapacity,TickCount),MathMax(MinViewTicks,oldSpan*2));
| |||
| |||
newSpan=MathMax(1,MathMin(newSpan,TickCount));
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
double ratio=(double)MathMax(0,MathMin(xCursor,graphWidth-1))/
| |||
(double)MathMax(1,graphWidth-1);
| |||
| |||
int anchorOffset=(int)MathRound(ratio*(double)MathMax(0,oldSpan-1));
| |||
int anchorIndex=oldStart+anchorOffset;
| |||
| |||
ViewSpan=newSpan;
| |||
| |||
int newAnchorOffset=(int)MathRound(ratio*(double)MathMax(0,newSpan-1));
| |||
int newStart=anchorIndex-newAnchorOffset;
| |||
newStart=MathMax(0,MathMin(newStart,TickCount-newSpan));
| |||
| |||
ViewEndOffset=TickCount-newSpan-newStart;
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,TickCount-newSpan));
| |||
}
| |||
| |||
void HandleHorizontalDrag(const int x)
| |||
{
| |||
if(!DragActive || TickCount<=1 || GetGraphWidth()<=1)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
int span=GetViewCount();
| |||
int dx=x-DragStartX;
| |||
int shift=(int)MathRound((double)dx*(double)span/(double)GetGraphWidth());
| |||
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
ViewEndOffset=DragStartOffset+shift;
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
}
| |||
| |||
bool IsCursorInTickSubwindow(const int y)
| |||
{
| |||
if(SubWindow<=0)
| |||
return false;
| |||
| |||
int top=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
| |||
int height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
| |||
return height>0 && y>=top && y<top+height;
| |||
}
| |||
| |||
void UpdateMouseScrollTarget(const int y)
| |||
{
| |||
MouseInTickSubwindow=IsCursorInTickSubwindow(y);
| |||
| |||
// Respect the user's original setting outside our subwindow, and keep the
| |||
// terminal scroll disabled while our vertical drag handlers own the pointer.
| |||
bool enableScroll=(PreviousChartMouseScroll && !MouseInTickSubwindow &&
| |||
!VerticalScaleDragActive && !VerticalPanDragActive);
| |||
bool currentScroll=(ChartGetInteger(0,CHART_MOUSE_SCROLL)!=0);
| |||
if(currentScroll!=enableScroll)
| |||
{
| |||
if(!ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)enableScroll))
| |||
{
| |||
if(!MouseScrollSetErrorLogged)
| |||
Print("TPDR: failed to update CHART_MOUSE_SCROLL. Error=",GetLastError());
| |||
MouseScrollSetErrorLogged=true;
| |||
}
| |||
else
| |||
MouseScrollSetErrorLogged=false;
| |||
}
| |||
}
| |||
| |||
bool IsCursorOnVerticalScaleHandle(const int x,const int y)
| |||
{
| |||
if(!MouseInTickSubwindow || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return false;
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
if(chartWidth<=CanvasWidth)
| |||
return false;
| |||
| |||
// Вертикальный zoom начинается в правой области цен, вне Canvas.
| |||
int left=CanvasWidth;
| |||
int right=chartWidth;
| |||
return x>=left && x<right;
| |||
}
| |||
| |||
void BeginVerticalPanDrag(const int y)
| |||
{
| |||
VerticalPanDragActive=true;
| |||
VerticalScaleManual=true;
| |||
VerticalPanDragStartY=y;
| |||
VerticalPanDragStartCenterPrice=(ViewportCenterPrice>0.0 ?
| |||
ViewportCenterPrice :
| |||
ViewportRayPrice(ScaleCenterGrid));
| |||
}
| |||
| |||
void HandleVerticalPanDrag(const int y)
| |||
{
| |||
if(!VerticalPanDragActive || CanvasHeight<=1 ||
| |||
GraphScaleMax<=GraphScaleMin)
| |||
return;
| |||
| |||
int deltaY=y-VerticalPanDragStartY;
| |||
double pricePerPixel=(GraphScaleMax-GraphScaleMin)/
| |||
(double)MathMax(1,CanvasHeight-1);
| |||
// Мышь вверх -> график вверх; мышь вниз -> график вниз.
| |||
ViewportCenterPrice=NormalizeDouble(
| |||
VerticalPanDragStartCenterPrice+(double)deltaY*pricePerPixel,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
| |||
VerticalPanDragStartY=y;
| |||
VerticalPanDragStartCenterPrice=ViewportCenterPrice;
| |||
VerticalPanDragActive=true;
| |||
RefreshGraph();
| |||
}
| |||
| |||
void EndVerticalPanDrag()
| |||
{
| |||
VerticalPanDragActive=false;
| |||
}
| |||
| |||
void BeginVerticalScaleDrag(const int y)
| |||
{
| |||
VerticalScaleDragActive=true;
| |||
VerticalScaleDragStartY=y;
| |||
VerticalScaleDragStartHalfRange=MathMax(VerticalScaleHalfRange,GetViewportLevelStepPrice()*0.20);
| |||
VerticalScaleManual=true;
| |||
}
| |||
| |||
void HandleVerticalScaleDrag(const int y)
| |||
{
| |||
if(!VerticalScaleDragActive)
| |||
return;
| |||
| |||
int deltaY=y-VerticalScaleDragStartY;
| |||
| |||
// Вверх = приближение по вертикали, вниз = отдаление.
| |||
// Масштаб рассчитывается относительно исходной точки drag, поэтому
| |||
// несколько mouse-move событий не накапливают округление.
| |||
double totalDeltaY=(double)(y-VerticalScaleDragStartY);
| |||
double factor=MathExp(totalDeltaY*VerticalScaleDragSensitivity);
| |||
double newHalfRange=VerticalScaleDragStartHalfRange*factor;
| |||
| |||
int upCount=MathMax(1,(TickLevelRowCount/2));
| |||
int downCount=MathMax(1,(TickLevelRowCount-(TickLevelRowCount/2)));
| |||
double minimumHalfRange=GetViewportLevelStepPrice()*MathMax(1,MathMax(upCount,downCount));
| |||
minimumHalfRange+=GetViewportLevelStepPrice()*0.10;
| |||
| |||
double maximumHalfRange=MathMax(minimumHalfRange*2.0,GetViewportLevelStepPrice()*2000.0);
| |||
| |||
VerticalScaleHalfRange=MathMax(minimumHalfRange,
| |||
MathMin(maximumHalfRange,newHalfRange));
| |||
| |||
RefreshGraph();
| |||
}
| |||
| |||
void EndVerticalScaleDrag()
| |||
{
| |||
VerticalScaleDragActive=false;
| |||
}
| |||
| |||
void TickChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
| |||
{
| |||
if(id==CHARTEVENT_OBJECT_CLICK)
| |||
{
| |||
if(sparam==Prefix+"ZOOM_MINUS" ||
| |||
sparam==Prefix+"ZOOM_PLUS" ||
| |||
sparam==Prefix+"RESET_CURRENT")
| |||
{
| |||
HandleZoomButtonClick(sparam);
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id==CHARTEVENT_MOUSE_WHEEL)
| |||
{
| |||
int flags=(int)(lparam>>32);
| |||
int xCursor=(int)(short)lparam;
| |||
int yCursor=(int)(short)(lparam>>16);
| |||
| |||
UpdateMouseScrollTarget(yCursor);
| |||
| |||
if(!MouseInTickSubwindow)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
if((flags&0x0004)!=0)
| |||
{
| |||
int span=GetViewCount();
| |||
int step=MathMax(1,span/10);
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
| |||
ViewEndOffset+=(dparam>0.0 ? -step : step);
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
}
| |||
else
| |||
{
| |||
HandleHorizontalZoom(xCursor,(int)dparam);
| |||
ViewEndOffset=0;
| |||
}
| |||
| |||
RefreshGraph();
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_MOUSE_MOVE)
| |||
{
| |||
int x=(int)lparam;
| |||
int y=(int)dparam;
| |||
int state=(int)StringToInteger(sparam);
| |||
bool leftDown=((state&1)!=0);
| |||
| |||
UpdateMouseScrollTarget(y);
| |||
| |||
if(!MouseInTickSubwindow)
| |||
DragActive=false;
| |||
| |||
if(VerticalScaleDragActive)
| |||
{
| |||
if(leftDown)
| |||
{
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
| |||
HandleVerticalScaleDrag(y);
| |||
DragActive=false;
| |||
VerticalPanDragActive=false;
| |||
return;
| |||
}
| |||
| |||
EndVerticalScaleDrag();
| |||
UpdateMouseScrollTarget(y);
| |||
}
| |||
else if(VerticalPanDragActive)
| |||
{
| |||
if(leftDown)
| |||
{
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
| |||
HandleVerticalPanDrag(y);
| |||
DragActive=false;
| |||
return;
| |||
}
| |||
| |||
EndVerticalPanDrag();
| |||
UpdateMouseScrollTarget(y);
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow &&
| |||
IsCursorOnVerticalScaleHandle(x,y))
| |||
{
| |||
BeginVerticalScaleDrag(y);
| |||
HandleVerticalScaleDrag(y);
| |||
DragActive=false;
| |||
VerticalPanDragActive=false;
| |||
return;
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow &&
| |||
IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
| |||
{
| |||
ResetButtonHeld=true;
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,true);
| |||
}
| |||
else if(!leftDown)
| |||
{
| |||
ResetButtonHeld=false;
| |||
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
| |||
}
| |||
else if(!IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
| |||
{
| |||
ResetButtonHeld=false;
| |||
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow && !ResetButtonHeld)
| |||
{
| |||
if(!DragActive && !VerticalPanDragActive)
| |||
{
| |||
if(!PointerDragTracking)
| |||
{
| |||
PointerDragTracking=true;
| |||
DragStartX=x;
| |||
DragStartOffset=ViewEndOffset;
| |||
VerticalPanDragStartY=y;
| |||
}
| |||
| |||
int dx=x-DragStartX;
| |||
int dy=y-VerticalPanDragStartY;
| |||
| |||
if(MathAbs((double)dy)>MathAbs((double)dx) && MathAbs(dy)>=4)
| |||
{
| |||
BeginVerticalPanDrag(y);
| |||
DragActive=false;
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(MathAbs(dx)>=4)
| |||
DragActive=true;
| |||
}
| |||
| |||
if(VerticalPanDragActive)
| |||
{
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(DragActive)
| |||
{
| |||
HandleHorizontalDrag(x);
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
else if(!leftDown || !MouseInTickSubwindow)
| |||
{
| |||
DragActive=false;
| |||
PointerDragTracking=false;
| |||
EndVerticalPanDrag();
| |||
}
| |||
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_CHART_CHANGE)
| |||
{
| |||
static int lastPeriod=(int)_Period;
| |||
bool periodChanged=((int)_Period!=lastPeriod);
| |||
lastPeriod=(int)_Period;
| |||
| |||
// Resolve the current indicator subwindow on every chart geometry
| |||
// event. Dragging the upper border changes window geometry and can
| |||
// invalidate the old bitmap's dimensions or owning subwindow.
| |||
int currentSubWindow=ChartWindowFind(0,ShortName);
| |||
if(currentSubWindow>=0)
| |||
SubWindow=currentSubWindow;
| |||
else
| |||
return;
| |||
| |||
if(periodChanged)
| |||
{
| |||
// Смена таймфрейма сбрасывает только горизонтальный просмотр.
| |||
// Вертикальный масштаб и абсолютные Y-координаты истории сохраняются.
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount));
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
FitInitialArrowHistory();
| |||
}
| |||
| |||
if(!BootstrapComplete)
| |||
return;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0)
| |||
{
| |||
if(EnsureCanvas())
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending=false;
| |||
}
| |||
}
| |||
ChartRedraw(0);
| |||
return;
| |||
}
| |||
}
| |||
| |||
int TickCalculate(const int rates_total,const int prev_calculated)
| |||
{
| |||
SubWindow=ChartWindowFind(0,ShortName);
| |||
| |||
if(SubWindow<0)
| |||
return rates_total;
| |||
| |||
if(!UpdateMarketState())
| |||
return rates_total;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0)
| |||
{
| |||
if(EnsureCanvas())
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
}
| |||
}
| |||
| |||
ChartRedraw(0);
| |||
return rates_total;
| |||
}
| |||
| |||
| |||
| |||
| |||
int OnInit()
| |||
{
| |||
int dashResult=DashInit();
| |||
if(dashResult!=INIT_SUCCEEDED)
| |||
return dashResult;
| |||
| |||
int tickResult=TickInit();
| |||
if(tickResult!=INIT_SUCCEEDED)
| |||
{
| |||
TickDeinit(REASON_INITFAILED);
| |||
DashDeinit(REASON_INITFAILED);
| |||
return tickResult;
| |||
}
| |||
| |||
IndicatorSetString(INDICATOR_SHORTNAME,_Symbol);
| |||
int timerMs=MathMax(10,MathMin(2000,UpdateIntervalMs));
| |||
if(!EventSetMillisecondTimer(timerMs))
| |||
{
| |||
Print("CodeLock-Dashboard-TickGraphic: EventSetMillisecondTimer failed. Error=",GetLastError());
| |||
TickDeinit(REASON_INITFAILED);
| |||
DashDeinit(REASON_INITFAILED);
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
EventKillTimer();
| |||
TickDeinit(reason);
| |||
DashDeinit(reason);
| |||
}
| |||
| |||
void OnTimer()
| |||
{
| |||
SyncTickVisualRowsFromDashboard();
| |||
TickTimer();
| |||
DashTimer();
| |||
}
| |||
| |||
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
| |||
{
| |||
DashChartEvent(id,lparam,dparam,sparam);
| |||
TickChartEvent(id,lparam,dparam,sparam);
| |||
}
| |||
| |||
int OnCalculate(const int rates_total,const int prev_calculated,
| |||
const int begin,const double &price[])
| |||
{
| |||
// Этот индикатор не использует price[] как источник рыночной модели.
| |||
// Begin сохраняется в стандартной сигнатуре Calculate event.
| |||
DashCalculate(rates_total,prev_calculated);
| |||
return TickCalculate(rates_total,prev_calculated);
| |||
}
|