3196 lines
102 KiB
MQL5
#include <Canvas\Canvas.mqh>
| |||
| |||
#property strict
| |||
#property indicator_separate_window
| |||
#property indicator_plots 0
| |||
#property version "3.00"
| |||
#property description "CodeLock TickGraphic / editable LEVEL grids / reversal analysis"
| |||
| |||
#define INVALID_GRID (-2147483647)
| |||
#define CODELOCK_KEY_ESCAPE 0x1B
| |||
| |||
// CODELOCK TICKGRAPHIC
| |||
// ============================================================================
| |||
// TickGraphic — графический компонент общей модели CodeLock.
| |||
// LEVEL — самостоятельная сетка, не фиксированный DISTANCE.
| |||
// Каждая строка LEVEL имеет собственную сетку, DIRECTION, REVERSAL AT и ALERT.
| |||
// HISTORY и LIVE используют одну модель CROSSING → DIRECTION → REVERSAL.
| |||
// REVERSAL подтверждается пересечением соседнего LEVEL:
| |||
// UP -> соседний LEVEL ниже текущего экстремума;
| |||
// DOWN -> соседний LEVEL выше текущего экстремума.
| |||
// Стрелка располагается на абсолютной цене LEVEL, подтвердившего REVERSAL.
| |||
// Для каждого направления хранится до ReversalArrowsHistory стрелок.
| |||
// Tick Graph, viewport и Canvas отображают модель, но не определяют её состояние.
| |||
// ============================================================================
| |||
| |||
input double StartPrice=1; // Начальная цена, от которой строится сетка уровней
| |||
input int BufferPoints=0; // Единый BUFFER подтверждения CROSSING в points
| |||
input int HistoryChunkSize=5000; // Размер блока исторических тиков для обработки
| |||
input int HistoryGraphTicks=200000; // Количество исторических тиков для первоначального графика
| |||
input int HistoryBootstrapTicks=20000; // Количество исторических тиков для восстановления состояния до запуска
| |||
input int MaxGraphSegments=200000; // Максимальное количество тиков, хранимых для графика
| |||
input int MaxModelHistoryTicks=200000; // Максимальное количество тиков, доступных для полного пересчета модели
| |||
input bool DrawReversalArrows=true; // Показывать стрелки подтверждённых разворотов
| |||
input int ReversalArrowsHistory=20; // Количество UP и отдельно DOWN стрелок в истории (всего 40 при значении 20)
| |||
input bool DrawRayObjects=true; // Показывать ценовые лучи
| |||
input int PriceLabelsCount=3; // Количество подписей ценовых уровней
| |||
input int InitialViewTicks=50000; // Количество тиков в первоначальном масштабе
| |||
input int MinViewTicks=5; // Минимальное количество тиков при максимальном приближении
| |||
input int BidLabelOffset=8; // Смещение подписи текущего Bid
| |||
// Вертикальный viewport работает как осциллограф: абсолютная цена фиксирована,
| |||
// BID движется внутри окна, а окно сдвигается только при достижении физического края.
| |||
// Уровни динамически появляются/исчезают по текущему диапазону High/Low BID.
| |||
input bool EnableAlert=true; // Разрешить экранные уведомления
| |||
input bool EnablePushNotification=true; // Разрешить Push-уведомления
| |||
input bool EnableEmailNotification=true; // Разрешить уведомления по Email
| |||
input bool EnableSoundNotification=true; // Разрешить звуковые уведомления
| |||
input string NotificationSound="alert.wav"; // Имя звукового файла уведомления
| |||
input color TickGraphColor=clrDodgerBlue; // Цвет тикового графика
| |||
input color RayColor=clrSilver; // Цвет обычных лучей сетки
| |||
input color CurrentRayColor=clrGold; // Цвет текущего уровня и Bid
| |||
input color UpCrossColor=clrGreen; // Цвет стрелок пересечения вверх
| |||
input color DownCrossColor=clrRed; // Цвет стрелок пересечения вниз
| |||
input color BuyColor=clrBlue; // Цвет стрелки BUY-разворота
| |||
input color SellColor=clrRed; // Цвет стрелки SELL-разворота
| |||
| |||
string Prefix="TPDR_";
| |||
string ShortName="CodeLock TickGraphic / LEVEL Table / Reversal Model";
| |||
| |||
// Рабочее состояние количества уровней. Inputs задают начальное значение,
| |||
// а кнопки изменяют именно это состояние без изменения input-параметров.
| |||
double PreviousBid=0.0;
| |||
bool HavePreviousBid=false;
| |||
bool LiveMode=false;
| |||
bool BootstrapComplete=false;
| |||
int SubWindow=-1;
| |||
| |||
| |||
int Direction=0;
| |||
int LastReversalDirection=0;
| |||
int LastCrossedRay=0;
| |||
int LastCrossingDirection=0;
| |||
int ConsecutiveCrossings=0;
| |||
int DirectionRay=INVALID_GRID;
| |||
double LastDirectionChangePrice=0.0;
| |||
double ReversalAtPrice=0.0;
| |||
| |||
// История подтверждённых REVERSAL.
| |||
// -1 = последний, -2 = предыдущий.
| |||
// Значения хранят абсолютные цены LEVEL, а не произвольные цены тика.
| |||
double LastDown1Price=0.0;
| |||
double LastDown2Price=0.0;
| |||
double LastUp1Price=0.0;
| |||
double LastUp2Price=0.0;
| |||
| |||
long AttachMsc=0;
| |||
long CursorMsc=1;
| |||
int CursorSameMsc=0;
| |||
int BootstrapAttempts=0;
| |||
| |||
CCanvas TickCanvas;
| |||
bool CanvasReady=false;
| |||
int CanvasWidth=0;
| |||
int CanvasHeight=0;
| |||
double ScaleMin=0.0;
| |||
double ScaleMax=0.0;
| |||
double GraphScaleMin=0.0;
| |||
double GraphScaleMax=0.0;
| |||
int ScaleCenterGrid=0;
| |||
double ViewportCenterPrice=0.0;
| |||
bool ScaleInitialized=false;
| |||
| |||
// Текущий LEVEL, определённый фактическим BID.
| |||
// Это состояние отображения LEVEL, а не центр вертикального viewport.
| |||
int CurrentLevelGrid=INVALID_GRID;
| |||
bool CurrentLevelGridInitialized=false;
| |||
int ViewSpan=0;
| |||
int ViewEndOffset=0;
| |||
bool DragActive=false;
| |||
int DragStartX=0;
| |||
int DragStartOffset=0;
| |||
| |||
// Вертикальный масштаб Tick Graph. После ручного движения мышью по правой
| |||
// области цен AutoScale не сбрасывает выбранный пользователем диапазон.
| |||
// В стабильном режиме ценовая шкала не центрируется по каждому текущему BID.
| |||
// BID движется внутри уже установленного вертикального окна.
| |||
bool VerticalScaleManual=false;
| |||
bool VerticalScaleDragActive=false;
| |||
bool VerticalPanDragActive=false;
| |||
bool PointerDragTracking=false;
| |||
int VerticalPanDragStartY=0;
| |||
double VerticalPanDragStartCenterPrice=0.0;
| |||
bool InitialArrowHistoryScalePending=false;
| |||
int VerticalScaleDragStartY=0;
| |||
double VerticalScaleDragStartHalfRange=0.0;
| |||
double VerticalScaleHalfRange=0.0;
| |||
| |||
const int VerticalScaleHandleWidth=18;
| |||
const double VerticalScaleDragSensitivity=0.008;
| |||
| |||
const int ZoomButtonWidth=36;
| |||
const int ResetButtonWidth=64;
| |||
const int ZoomButtonHeight=28;
| |||
const int ZoomButtonGap=6;
| |||
const int PriceScaleWidth=60;
| |||
const int LevelButtonWidth=52;
| |||
const int LevelButtonHeight=22;
| |||
const int LevelButtonGap=3;
| |||
const int ControlPanelTop=24;
| |||
const int DistanceButtonWidth=36;
| |||
const int DistanceButtonHeight=22;
| |||
const int DistanceButtonGap=3;
| |||
const int ControlPanelLabelWidth=52;
| |||
const int ControlPanelPadding=5;
| |||
const int ControlPanelRowGap=4;
| |||
const int ControlPanelHeight=520;
| |||
// Контекст последнего автоматического масштаба. Он изменяется только при
| |||
// изменении самого viewport/сеточного окна, а не при каждом новом BID.
| |||
bool AutoScaleInitialized=false;
| |||
int LastAutoScaleLevelsUp=-1;
| |||
int LastAutoScaleLevelsDown=-1;
| |||
| |||
bool ResetButtonHeld=false;
| |||
bool PreviousChartMouseScroll=true;
| |||
bool LegacyPriceObjectsCleaned=false;
| |||
bool MouseInTickSubwindow=false;
| |||
bool InitialViewLayoutActive=false;
| |||
| |||
// Сохранённый снимок расчётного состояния используется после REASON_CHARTCHANGE.
| |||
// Он не является отдельным источником истины: кэш — сериализованная копия
| |||
// ModelTickPrices[] и производного состояния.
| |||
| |||
double TickPrices[];
| |||
int TickCount=0;
| |||
int TickHead=0;
| |||
long TickSerial=0;
| |||
| |||
// Единственный источник истины для пересчета модели.
| |||
// Графический хвост TickPrices[] является только представлением этой истории.
| |||
double ModelTickPrices[];
| |||
int ModelTickCount=0;
| |||
int ModelTickHead=0;
| |||
| |||
struct SignalMark
| |||
{
| |||
long serial;
| |||
double price;
| |||
bool up;
| |||
bool reversal;
| |||
};
| |||
| |||
SignalMark SignalMarks[];
| |||
long RebuildArrowSerialOverride=-1;
| |||
| |||
// ============================================================================
| |||
// ЕДИНАЯ ТАБЛИЦА LEVEL
| |||
// LEVEL — самостоятельная сетка. DISTANCE как отдельное понятие отсутствует.
| |||
// ============================================================================
| |||
| |||
const int TickMinVisualRows=5;
| |||
const int TickMaxVisualRows=100;
| |||
const int TickVisualLevelMin=10;
| |||
const int TickVisualLevelMax=2000;
| |||
const int TickVisualLevelStep=10;
| |||
| |||
struct TickLevelRow
| |||
{
| |||
int level;
| |||
int direction;
| |||
int directionGrid;
| |||
double lastReversalPrice;
| |||
double reversalAtPrice;
| |||
double lastDown1Price;
| |||
double lastDown2Price;
| |||
double lastUp1Price;
| |||
double lastUp2Price;
| |||
bool alertEnabled;
| |||
};
| |||
| |||
TickLevelRow TickLevelRows[];
| |||
int TickLevelRowCount=0;
| |||
int TickEditingRow=-1;
| |||
| |||
string TickLevelStoragePrefix()
| |||
{
| |||
string symbol=_Symbol;
| |||
StringReplace(symbol,"\\","_");
| |||
StringReplace(symbol,"/","_");
| |||
StringReplace(symbol,":","_");
| |||
StringReplace(symbol," ","_");
| |||
string timeframe=IntegerToString((int)_Period);
| |||
string chart=IntegerToString((long)ChartID());
| |||
return "CodeLock_TickGraphic_LEVELS_"+symbol+"_"+timeframe+"_"+chart+"_";
| |||
}
| |||
| |||
bool IsValidTickVisualLevel(const int level)
| |||
{
| |||
return level>=TickVisualLevelMin &&
| |||
level<=TickVisualLevelMax &&
| |||
(level%TickVisualLevelStep)==0;
| |||
}
| |||
| |||
bool IsStrictTickVisualRows()
| |||
{
| |||
if(TickLevelRowCount<TickMinVisualRows ||
| |||
TickLevelRowCount>TickMaxVisualRows)
| |||
return false;
| |||
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
if(!IsValidTickVisualLevel(TickLevelRows[i].level))
| |||
return false;
| |||
| |||
if(i>0 && TickLevelRows[i].level<=TickLevelRows[i-1].level)
| |||
return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
void ResetTickLevelRowState(TickLevelRow &row)
| |||
{
| |||
row.direction=0;
| |||
row.directionGrid=INVALID_GRID;
| |||
row.lastReversalPrice=0.0;
| |||
row.reversalAtPrice=0.0;
| |||
row.lastDown1Price=0.0;
| |||
row.lastDown2Price=0.0;
| |||
row.lastUp1Price=0.0;
| |||
row.lastUp2Price=0.0;
| |||
row.alertEnabled=false;
| |||
}
| |||
| |||
void SaveTickVisualRows()
| |||
{
| |||
if(TickLevelRowCount<1)
| |||
return;
| |||
| |||
string prefix=TickLevelStoragePrefix();
| |||
GlobalVariableSet(prefix+"COUNT",(double)TickLevelRowCount);
| |||
| |||
for(int i=0;i<TickMaxVisualRows;i++)
| |||
{
| |||
string key=prefix+IntegerToString(i);
| |||
if(i<TickLevelRowCount)
| |||
GlobalVariableSet(key,(double)TickLevelRows[i].level);
| |||
else
| |||
GlobalVariableDel(key);
| |||
}
| |||
}
| |||
| |||
bool LoadTickVisualRows()
| |||
{
| |||
string prefix=TickLevelStoragePrefix();
| |||
| |||
if(!GlobalVariableCheck(prefix+"COUNT"))
| |||
return false;
| |||
| |||
int count=(int)GlobalVariableGet(prefix+"COUNT");
| |||
if(count<TickMinVisualRows || count>TickMaxVisualRows)
| |||
return false;
| |||
| |||
ArrayResize(TickLevelRows,count);
| |||
| |||
for(int i=0;i<count;i++)
| |||
{
| |||
string key=prefix+IntegerToString(i);
| |||
if(!GlobalVariableCheck(key))
| |||
return false;
| |||
| |||
TickLevelRows[i].level=(int)GlobalVariableGet(key);
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
TickLevelRowCount=count;
| |||
return IsStrictTickVisualRows();
| |||
}
| |||
| |||
void InitializeTickVisualRows()
| |||
{
| |||
ArrayResize(TickLevelRows,TickMinVisualRows);
| |||
| |||
for(int i=0;i<TickMinVisualRows;i++)
| |||
{
| |||
TickLevelRows[i].level=(i+1)*TickVisualLevelStep;
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
TickLevelRowCount=TickMinVisualRows;
| |||
SaveTickVisualRows();
| |||
}
| |||
| |||
void ResetAllTickVisualRowStates()
| |||
{
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
int FloorGridForStep(const double price,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return INVALID_GRID;
| |||
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathFloor(position);
| |||
}
| |||
| |||
int CeilGridForStep(const double price,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return INVALID_GRID;
| |||
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathCeil(position);
| |||
}
| |||
| |||
double RayPriceForStep(const int grid,const double step)
| |||
{
| |||
if(step<=0.0)
| |||
return 0.0;
| |||
| |||
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
int TickRowGrid(const int rowIndex,const double price)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return INVALID_GRID;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
return FloorGridForStep(price,step);
| |||
}
| |||
| |||
double TickRowPrice(const int rowIndex,const int grid)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return 0.0;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
return RayPriceForStep(grid,step);
| |||
}
| |||
| |||
void UpdateTickRowReversalAt(const int rowIndex)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return;
| |||
| |||
if(TickLevelRows[rowIndex].direction==0 || TickLevelRows[rowIndex].directionGrid==INVALID_GRID)
| |||
{
| |||
TickLevelRows[rowIndex].reversalAtPrice=0.0;
| |||
return;
| |||
}
| |||
| |||
TickLevelRows[rowIndex].reversalAtPrice=TickRowPrice(rowIndex,
| |||
TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction);
| |||
}
| |||
| |||
void RegisterTickRowCrossing(const int rowIndex,const int grid,
| |||
const int crossingDirection,const MqlTick &tick,
| |||
const bool emitSignals)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount)
| |||
return;
| |||
| |||
if(TickLevelRows[rowIndex].direction==0)
| |||
{
| |||
TickLevelRows[rowIndex].direction=crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
return;
| |||
}
| |||
| |||
if(crossingDirection==TickLevelRows[rowIndex].direction)
| |||
{
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
return;
| |||
}
| |||
| |||
int reversalGrid=TickLevelRows[rowIndex].directionGrid-TickLevelRows[rowIndex].direction;
| |||
if(grid!=reversalGrid)
| |||
return;
| |||
| |||
TickLevelRows[rowIndex].direction=crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid=grid;
| |||
TickLevelRows[rowIndex].lastReversalPrice=TickRowPrice(rowIndex,grid);
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
| |||
if(crossingDirection<0)
| |||
{
| |||
TickLevelRows[rowIndex].lastDown2Price=TickLevelRows[rowIndex].lastDown1Price;
| |||
TickLevelRows[rowIndex].lastDown1Price=TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
else
| |||
{
| |||
TickLevelRows[rowIndex].lastUp2Price=TickLevelRows[rowIndex].lastUp1Price;
| |||
TickLevelRows[rowIndex].lastUp1Price=TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
| |||
RecordReversalArrow(tick,TickLevelRows[rowIndex].lastReversalPrice,crossingDirection>0,emitSignals);
| |||
| |||
}
| |||
| |||
void ProcessTickRow(const int rowIndex,const double oldBid,const double bid,
| |||
const MqlTick &tick,const bool emitSignals)
| |||
{
| |||
if(rowIndex<0 || rowIndex>=TickLevelRowCount || bid==oldBid)
| |||
return;
| |||
| |||
double step=(double)TickLevelRows[rowIndex].level*_Point;
| |||
if(step<=0.0)
| |||
return;
| |||
| |||
double buffer=(double)MathMax(0,BufferPoints)*_Point;
| |||
| |||
if(bid>oldBid)
| |||
{
| |||
int first=FloorGridForStep(oldBid-buffer,step)-1;
| |||
int last=FloorGridForStep(bid-buffer,step)+1;
| |||
| |||
for(int grid=first;grid<=last;grid++)
| |||
{
| |||
double confirmationPrice=NormalizeDouble(
| |||
RayPriceForStep(grid,step)+buffer,_Digits);
| |||
| |||
if(confirmationPrice>oldBid && confirmationPrice<=bid)
| |||
{
| |||
RegisterTickRowCrossing(rowIndex,grid,1,tick,emitSignals);
| |||
if(TickLevelRows[rowIndex].alertEnabled && emitSignals)
| |||
{
| |||
NotifyEvent("LEVEL CROSSING","UP",
| |||
TickRowPrice(rowIndex,grid),tick.time_msc);
| |||
TickLevelRows[rowIndex].alertEnabled=false;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
else
| |||
{
| |||
int first=CeilGridForStep(oldBid+buffer,step)+1;
| |||
int last=CeilGridForStep(bid+buffer,step)-1;
| |||
| |||
for(int grid=first;grid>=last;grid--)
| |||
{
| |||
double confirmationPrice=NormalizeDouble(
| |||
RayPriceForStep(grid,step)-buffer,_Digits);
| |||
| |||
if(confirmationPrice<oldBid && confirmationPrice>=bid)
| |||
{
| |||
RegisterTickRowCrossing(rowIndex,grid,-1,tick,emitSignals);
| |||
if(TickLevelRows[rowIndex].alertEnabled && emitSignals)
| |||
{
| |||
NotifyEvent("LEVEL CROSSING","DOWN",
| |||
TickRowPrice(rowIndex,grid),tick.time_msc);
| |||
TickLevelRows[rowIndex].alertEnabled=false;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
void RebuildTickVisualRowsFromHistory()
| |||
{
| |||
ArrayResize(SignalMarks,0);
| |||
ResetAllTickVisualRowStates();
| |||
| |||
if(ModelTickCount<2)
| |||
return;
| |||
| |||
long savedTickSerial=TickSerial;
| |||
RebuildArrowSerialOverride=0;
| |||
| |||
for(int i=1;i<ModelTickCount;i++)
| |||
{
| |||
double oldBid=GetModelTickByIndex(i-1);
| |||
double bid=GetModelTickByIndex(i);
| |||
| |||
if(oldBid<=0.0 || bid<=0.0 || oldBid==bid)
| |||
continue;
| |||
| |||
MqlTick syntheticTick;
| |||
ZeroMemory(syntheticTick);
| |||
syntheticTick.bid=bid;
| |||
syntheticTick.time_msc=0;
| |||
RebuildArrowSerialOverride=MathMax(1,savedTickSerial-ModelTickCount+i+1);
| |||
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
ProcessTickRow(row,oldBid,bid,syntheticTick,false);
| |||
}
| |||
| |||
RebuildArrowSerialOverride=-1;
| |||
TickSerial=savedTickSerial;
| |||
}
| |||
| |||
bool ValidateTickEditedLevel(const int index,const int newLevel)
| |||
{
| |||
if(index<0 || index>=TickLevelRowCount ||
| |||
!IsValidTickVisualLevel(newLevel))
| |||
return false;
| |||
| |||
if(index>0 && newLevel<=TickLevelRows[index-1].level)
| |||
return false;
| |||
| |||
if(index+1<TickLevelRowCount &&
| |||
newLevel>=TickLevelRows[index+1].level)
| |||
return false;
| |||
| |||
return true;
| |||
}
| |||
| |||
void CancelTickLevelEdit()
| |||
{
| |||
if(TickEditingRow<0)
| |||
return;
| |||
| |||
ObjectDelete(0,Prefix+"LEVEL_EDIT_"+IntegerToString(TickEditingRow));
| |||
TickEditingRow=-1;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void BeginTickLevelEdit(const int index,const int x,const int y,
| |||
const int width,const int height)
| |||
{
| |||
if(index<0 || index>=TickLevelRowCount)
| |||
return;
| |||
| |||
if(TickEditingRow>=0 && TickEditingRow!=index)
| |||
ObjectDelete(0,Prefix+"LEVEL_EDIT_"+IntegerToString(TickEditingRow));
| |||
| |||
string name=Prefix+"LEVEL_EDIT_"+IntegerToString(index);
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
ResetLastError();
| |||
| |||
if(!ObjectCreate(0,name,OBJ_EDIT,0,0,0))
| |||
{
| |||
Print("TPDR: LEVEL edit creation failed. Error=",GetLastError());
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1500);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_ALIGN,ALIGN_CENTER);
| |||
ObjectSetInteger(0,name,OBJPROP_READONLY,false);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,width);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,height);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGold);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,IntegerToString(TickLevelRows[index].level));
| |||
| |||
TickEditingRow=index;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void CommitTickLevelEdit(const int index,const string textValue)
| |||
{
| |||
if(index<0 || index>=TickLevelRowCount)
| |||
{
| |||
CancelTickLevelEdit();
| |||
return;
| |||
}
| |||
| |||
if(StringLen(textValue)==0)
| |||
{
| |||
Print("TPDR: LEVEL edit rejected: empty value.");
| |||
CancelTickLevelEdit();
| |||
return;
| |||
}
| |||
| |||
for(int i=0;i<StringLen(textValue);i++)
| |||
{
| |||
ushort ch=StringGetCharacter(textValue,i);
| |||
if(ch<'0' || ch>'9')
| |||
{
| |||
Print("TPDR: LEVEL edit rejected: integer 10..2000, step 10 required.");
| |||
CancelTickLevelEdit();
| |||
return;
| |||
}
| |||
}
| |||
| |||
int newLevel=(int)StringToInteger(textValue);
| |||
| |||
if(!ValidateTickEditedLevel(index,newLevel))
| |||
{
| |||
Print("TPDR: LEVEL edit rejected: ",textValue,
| |||
". Allowed: 10..2000, step 10, strict order.");
| |||
CancelTickLevelEdit();
| |||
return;
| |||
}
| |||
| |||
TickLevelRows[index].level=newLevel;
| |||
SaveTickVisualRows();
| |||
CancelTickLevelEdit();
| |||
| |||
// Изменение LEVEL — изменение модели, поэтому производное состояние
| |||
// пересобирается из той же истории, а горизонтальный VIEW не сбрасывается.
| |||
RebuildTickVisualRowsFromHistory();
| |||
| |||
RefreshGraph();
| |||
}
| |||
| |||
bool AddTickLevelRow(const int index)
| |||
{
| |||
if(index<0 || index>=TickLevelRowCount ||
| |||
TickLevelRowCount>=TickMaxVisualRows)
| |||
return false;
| |||
| |||
int current=TickLevelRows[index].level;
| |||
int upper=(index+1<TickLevelRowCount ?
| |||
TickLevelRows[index+1].level : TickVisualLevelMax+TickVisualLevelStep);
| |||
| |||
int candidate=current+TickVisualLevelStep;
| |||
| |||
if(candidate>=upper || candidate>TickVisualLevelMax)
| |||
{
| |||
Print("TPDR: cannot add LEVEL above ",current,
| |||
" pt without breaking strict order.");
| |||
return false;
| |||
}
| |||
| |||
for(int i=TickLevelRowCount;i>index+1;i--)
| |||
TickLevelRows[i]=TickLevelRows[i-1];
| |||
| |||
TickLevelRows[index+1].level=candidate;
| |||
ResetTickLevelRowState(TickLevelRows[index+1]);
| |||
TickLevelRowCount++;
| |||
| |||
SaveTickVisualRows();
| |||
RebuildTickVisualRowsFromHistory();
| |||
RefreshGraph();
| |||
return true;
| |||
}
| |||
| |||
bool DeleteTickLevelRow(const int index)
| |||
{
| |||
if(index<0 || index>=TickLevelRowCount ||
| |||
TickLevelRowCount<=TickMinVisualRows)
| |||
return false;
| |||
| |||
int removed=TickLevelRows[index].level;
| |||
| |||
for(int i=index;i<TickLevelRowCount-1;i++)
| |||
TickLevelRows[i]=TickLevelRows[i+1];
| |||
| |||
TickLevelRowCount--;
| |||
| |||
SaveTickVisualRows();
| |||
RebuildTickVisualRowsFromHistory();
| |||
RefreshGraph();
| |||
| |||
Print("TPDR: deleted LEVEL ",removed," pt.");
| |||
return true;
| |||
}
| |||
| |||
| |||
void AppendModelTick(const double bid)
| |||
{
| |||
if(bid<=0.0 || MaxModelHistoryTicks<1)
| |||
return;
| |||
| |||
if(ArraySize(ModelTickPrices)<MaxModelHistoryTicks)
| |||
ArrayResize(ModelTickPrices,MaxModelHistoryTicks,MaxModelHistoryTicks);
| |||
| |||
if(ModelTickCount<MaxModelHistoryTicks)
| |||
{
| |||
int index=(ModelTickHead+ModelTickCount)%MaxModelHistoryTicks;
| |||
ModelTickPrices[index]=bid;
| |||
ModelTickCount++;
| |||
}
| |||
else
| |||
{
| |||
ModelTickPrices[ModelTickHead]=bid;
| |||
ModelTickHead=(ModelTickHead+1)%MaxModelHistoryTicks;
| |||
}
| |||
}
| |||
| |||
double GetModelTickByIndex(const int index)
| |||
{
| |||
if(index<0 || index>=ModelTickCount)
| |||
return 0.0;
| |||
| |||
return ModelTickPrices[(ModelTickHead+index)%MaxModelHistoryTicks];
| |||
}
| |||
| |||
void ResetSelectedCalculation()
| |||
{
| |||
Direction=0;
| |||
LastReversalDirection=0;
| |||
LastCrossedRay=0;
| |||
LastCrossingDirection=0;
| |||
ConsecutiveCrossings=0;
| |||
DirectionRay=INVALID_GRID;
| |||
LastDirectionChangePrice=0.0;
| |||
ReversalAtPrice=0.0;
| |||
LastDown1Price=0.0;
| |||
LastDown2Price=0.0;
| |||
LastUp1Price=0.0;
| |||
LastUp2Price=0.0;
| |||
PreviousBid=0.0;
| |||
HavePreviousBid=false;
| |||
}
| |||
| |||
void ClearDerivedState()
| |||
{
| |||
TickCount=0;
| |||
TickHead=0;
| |||
TickSerial=0;
| |||
ArrayResize(TickPrices,MathMax(100,MaxGraphSegments),
| |||
MathMax(100,MaxGraphSegments));
| |||
ArrayResize(SignalMarks,0);
| |||
AutoScaleInitialized=false;
| |||
LastAutoScaleLevelsUp=-1;
| |||
LastAutoScaleLevelsDown=-1;
| |||
ResetSelectedCalculation();
| |||
ScaleCenterGrid=0;
| |||
ScaleInitialized=false;
| |||
ResetAllTickVisualRowStates();
| |||
}
| |||
| |||
| |||
void ShiftRelativeWindowForCrossing(const int crossingGrid)
| |||
{
| |||
// CROSSING не изменяет viewport. Визуальное окно меняется только
| |||
// при достижении BID физического края ценового диапазона.
| |||
}
| |||
double GetViewportLevelStepPrice()
| |||
{
| |||
double minimum=0.0;
| |||
for(int i=0;i<TickLevelRowCount;i++)
| |||
{
| |||
double step=(double)TickLevelRows[i].level*_Point;
| |||
if(step<=0.0) continue;
| |||
if(minimum<=0.0 || step<minimum) minimum=step;
| |||
}
| |||
return minimum;
| |||
}
| |||
int FloorGridForViewport(const double price)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return INVALID_GRID;
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathFloor(position+1.0e-9);
| |||
}
| |||
int CeilGridForViewport(const double price)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return INVALID_GRID;
| |||
double position=(NormalizeDouble(price,_Digits)-NormalizeDouble(StartPrice,_Digits))/step;
| |||
return (int)MathCeil(position-1.0e-9);
| |||
}
| |||
double ViewportRayPrice(const int grid)
| |||
{
| |||
double step=GetViewportLevelStepPrice();
| |||
if(step<=0.0) return 0.0;
| |||
return NormalizeDouble(StartPrice+(double)grid*step,_Digits);
| |||
}
| |||
| |||
void DeleteObjectsByPrefix()
| |||
{
| |||
ObjectsDeleteAll(0,Prefix,-1,-1);
| |||
}
| |||
| |||
void NotifyEvent(const string eventName,const string directionName,const double price,const long timeMsc)
| |||
{
| |||
if(!LiveMode)
| |||
return;
| |||
| |||
string msg=StringFormat("TPDR %s | %s | %s | Price=%s | TimeMsc=%I64d",
| |||
_Symbol,eventName,directionName,DoubleToString(price,_Digits),timeMsc);
| |||
| |||
Print(msg);
| |||
| |||
if(EnableAlert)
| |||
Alert(msg);
| |||
| |||
if(EnableSoundNotification && StringLen(NotificationSound)>0)
| |||
PlaySound(NotificationSound);
| |||
| |||
if(EnablePushNotification)
| |||
{
| |||
ResetLastError();
| |||
if(!SendNotification(msg))
| |||
Print("TPDR SendNotification failed. Error=",GetLastError());
| |||
}
| |||
| |||
if(EnableEmailNotification)
| |||
{
| |||
ResetLastError();
| |||
if(!SendMail("TPDR "+eventName+" "+directionName,msg))
| |||
Print("TPDR SendMail failed. Error=",GetLastError());
| |||
}
| |||
}
| |||
| |||
void AddTickToGraph(const double bid)
| |||
{
| |||
if(bid<=0.0 || MaxGraphSegments<1)
| |||
return;
| |||
| |||
// TickPrices[] заранее резервируется целиком. На глубокой истории
| |||
// нельзя делать ArrayResize() на каждом тике: это создаёт лишние
| |||
// перераспределения памяти и заметно увеличивает стартовую загрузку.
| |||
if(ArraySize(TickPrices)<MaxGraphSegments)
| |||
ArrayResize(TickPrices,MaxGraphSegments,MaxGraphSegments);
| |||
| |||
if(TickCount<MaxGraphSegments)
| |||
{
| |||
int index=(TickHead+TickCount)%MaxGraphSegments;
| |||
TickPrices[index]=bid;
| |||
TickCount++;
| |||
}
| |||
else
| |||
{
| |||
TickPrices[TickHead]=bid;
| |||
TickHead=(TickHead+1)%MaxGraphSegments;
| |||
}
| |||
}
| |||
| |||
double GetTickPriceByVisibleIndex(const int index)
| |||
{
| |||
if(index<0 || index>=TickCount)
| |||
return 0.0;
| |||
| |||
int physical=(TickHead+index)%MaxGraphSegments;
| |||
return TickPrices[physical];
| |||
}
| |||
| |||
void AddSignalMark(const double price,const bool up,const bool reversal)
| |||
{
| |||
if(!reversal || ReversalArrowsHistory<=0)
| |||
return;
| |||
| |||
int size=ArraySize(SignalMarks);
| |||
ArrayResize(SignalMarks,size+1);
| |||
SignalMarks[size].serial=(RebuildArrowSerialOverride>=0 ? RebuildArrowSerialOverride : TickSerial);
| |||
SignalMarks[size].price=NormalizeDouble(price,_Digits);
| |||
SignalMarks[size].up=up;
| |||
SignalMarks[size].reversal=true;
| |||
| |||
int keepCount=MathMax(1,ReversalArrowsHistory);
| |||
int directionCount=0;
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
if(SignalMarks[i].up==up)
| |||
directionCount++;
| |||
| |||
if(directionCount<=keepCount)
| |||
return;
| |||
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
if(SignalMarks[i].up!=up)
| |||
continue;
| |||
for(int j=i;j<ArraySize(SignalMarks)-1;j++)
| |||
SignalMarks[j]=SignalMarks[j+1];
| |||
ArrayResize(SignalMarks,ArraySize(SignalMarks)-1);
| |||
break;
| |||
}
| |||
}
| |||
| |||
void RecordReversalArrow(const MqlTick &tick,const double price,const bool buy,
| |||
const bool notify)
| |||
{
| |||
if(DrawReversalArrows)
| |||
AddSignalMark(price,buy,true);
| |||
| |||
if(notify)
| |||
NotifyEvent(buy ? "BUY REVERSAL" : "SELL REVERSAL",
| |||
buy ? "BUY" : "SELL",price,tick.time_msc);
| |||
}
| |||
| |||
| |||
// ВЕРТИКАЛЬНЫЙ VIEWPORT
| |||
// Модель цены и отображение разделены.
| |||
// High/Low BID используются при создании/перестроении вертикального масштаба.
| |||
// Текущий LEVEL определяется отдельно и не является центром viewport.
| |||
// Новый тик не центрирует окно и не перемещает уже существующую историю.
| |||
void UpdateCurrentLevelGrid(const double bid)
| |||
{
| |||
if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
| |||
return;
| |||
| |||
int grid=FloorGridForViewport(bid);
| |||
if(grid==INVALID_GRID)
| |||
return;
| |||
| |||
CurrentLevelGrid=grid;
| |||
CurrentLevelGridInitialized=true;
| |||
}
| |||
| |||
// При достижении физического края переносится только viewport.
| |||
void AlignLevelWindowToCurrentBid(const double bid)
| |||
{
| |||
if(bid<=0.0 || GetViewportLevelStepPrice()<=0.0)
| |||
return;
| |||
| |||
if(!ScaleInitialized)
| |||
{
| |||
ScaleCenterGrid=FloorGridForViewport(bid);
| |||
ViewportCenterPrice=ViewportRayPrice(ScaleCenterGrid);
| |||
ScaleInitialized=true;
| |||
return;
| |||
}
| |||
| |||
if(GraphScaleMax<=GraphScaleMin)
| |||
return;
| |||
| |||
double range=GraphScaleMax-GraphScaleMin;
| |||
double center=(ViewportCenterPrice>0.0 ?
| |||
ViewportCenterPrice : ViewportRayPrice(ScaleCenterGrid));
| |||
| |||
// Осциллографический viewport:
| |||
// обычный BID, CROSSING и REVERSAL не двигают окно.
| |||
// Перенос выполняется только после входа BID в защитную зону края.
| |||
// Центр не привязывается к GRID: это исключает ступенчатый
| |||
// "трамплин" viewport при движении BID около границы.
| |||
double edgeMargin=range*0.20;
| |||
double upperEdge=GraphScaleMax-edgeMargin;
| |||
double lowerEdge=GraphScaleMin+edgeMargin;
| |||
// Перенос выполняется только после выхода BID в краевую зону.
| |||
// После переноса BID оказывается около центра окна, поэтому viewport
| |||
// не подтягивается за каждым тиком и не создаёт эффект "трамплина".
| |||
double targetOffset=range*0.05;
| |||
| |||
if(bid>upperEdge)
| |||
{
| |||
double targetCenter=bid-targetOffset;
| |||
if(targetCenter>center)
| |||
{
| |||
ViewportCenterPrice=NormalizeDouble(targetCenter,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
| |||
}
| |||
}
| |||
else
| |||
if(bid<lowerEdge)
| |||
{
| |||
double targetCenter=bid+targetOffset;
| |||
if(targetCenter<center)
| |||
{
| |||
ViewportCenterPrice=NormalizeDouble(targetCenter,_Digits);
| |||
ScaleCenterGrid=CeilGridForViewport(ViewportCenterPrice);
| |||
}
| |||
}
| |||
}
| |||
| |||
void UpdateSubwindowScale(const double bid)
| |||
{
| |||
if(SubWindow<0 || bid<=0.0 || TickLevelRowCount<=0)
| |||
return;
| |||
| |||
UpdateCurrentLevelGrid(bid);
| |||
| |||
if(!VerticalPanDragActive && !VerticalScaleManual)
| |||
AlignLevelWindowToCurrentBid(bid);
| |||
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
| |||
double visibleMin=bid;
| |||
double visibleMax=bid;
| |||
| |||
for(int i=0;i<viewCount;i++)
| |||
{
| |||
double price=GetTickPriceByVisibleIndex(viewStart+i);
| |||
if(price<=0.0)
| |||
continue;
| |||
| |||
visibleMin=MathMin(visibleMin,price);
| |||
visibleMax=MathMax(visibleMax,price);
| |||
}
| |||
| |||
if(InitialArrowHistoryScalePending)
| |||
{
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
visibleMin=MathMin(visibleMin,SignalMarks[i].price);
| |||
visibleMax=MathMax(visibleMax,SignalMarks[i].price);
| |||
}
| |||
}
| |||
| |||
double minimumStep=0.0;
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
{
| |||
double step=(double)TickLevelRows[row].level*_Point;
| |||
minimumStep=(minimumStep<=0.0 ? step : MathMin(minimumStep,step));
| |||
| |||
int grid=TickRowGrid(row,bid);
| |||
if(grid==INVALID_GRID)
| |||
continue;
| |||
| |||
double levelPrice=TickRowPrice(row,grid);
| |||
visibleMin=MathMin(visibleMin,levelPrice);
| |||
visibleMax=MathMax(visibleMax,levelPrice);
| |||
}
| |||
| |||
minimumStep=MathMax(minimumStep,_Point);
| |||
| |||
double autoHalfRange=MathMax(bid-visibleMin,visibleMax-bid);
| |||
double minimumHalfRange=minimumStep*3.0;
| |||
autoHalfRange=MathMax(autoHalfRange,minimumHalfRange);
| |||
| |||
double autoPadding=MathMax(autoHalfRange*0.05,minimumStep*0.10);
| |||
autoHalfRange+=autoPadding;
| |||
| |||
bool scaleContextChanged=!AutoScaleInitialized ||
| |||
LastAutoScaleLevelsUp!=TickLevelRowCount;
| |||
| |||
if(!VerticalScaleManual)
| |||
{
| |||
if(scaleContextChanged)
| |||
VerticalScaleHalfRange=autoHalfRange;
| |||
| |||
AutoScaleInitialized=true;
| |||
LastAutoScaleLevelsUp=TickLevelRowCount;
| |||
LastAutoScaleLevelsDown=0;
| |||
}
| |||
else
| |||
{
| |||
VerticalScaleHalfRange=MathMax(VerticalScaleHalfRange,
| |||
minimumHalfRange+minimumStep*0.10);
| |||
}
| |||
| |||
double scaleHalfRange=MathMax(VerticalScaleHalfRange,
| |||
minimumStep*0.20);
| |||
| |||
double viewportCenter=(ViewportCenterPrice>0.0 ?
| |||
ViewportCenterPrice : bid);
| |||
| |||
GraphScaleMin=NormalizeDouble(viewportCenter-scaleHalfRange,_Digits);
| |||
GraphScaleMax=NormalizeDouble(viewportCenter+scaleHalfRange,_Digits);
| |||
| |||
if(GraphScaleMax<=GraphScaleMin)
| |||
{
| |||
GraphScaleMin=viewportCenter-minimumStep;
| |||
GraphScaleMax=viewportCenter+minimumStep;
| |||
}
| |||
| |||
ScaleMin=GraphScaleMin;
| |||
ScaleMax=GraphScaleMax;
| |||
| |||
IndicatorSetDouble(INDICATOR_MINIMUM,GraphScaleMin);
| |||
IndicatorSetDouble(INDICATOR_MAXIMUM,GraphScaleMax);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,true);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,true);
| |||
| |||
if(CanvasReady)
| |||
CanvasHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
}
| |||
| |||
| |||
int GetViewStartIndex()
| |||
{
| |||
if(TickCount<=0)
| |||
return 0;
| |||
| |||
int span=ViewSpan;
| |||
if(span<=0)
| |||
span=TickCount;
| |||
| |||
span=MathMax(1,MathMin(span,TickCount));
| |||
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
int offset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
| |||
return TickCount-span-offset;
| |||
}
| |||
| |||
int GetViewCount()
| |||
{
| |||
if(TickCount<=0)
| |||
return 0;
| |||
| |||
int span=ViewSpan;
| |||
if(span<=0)
| |||
span=TickCount;
| |||
| |||
return MathMax(1,MathMin(span,TickCount));
| |||
}
| |||
| |||
// Первичная настройка горизонтального окна после загрузки истории.
| |||
// ВАЖНО: функция вызывается только при bootstrap/смене расстояния.
| |||
// Рендер больше не имеет права самовольно расширять ViewSpan.
| |||
// Поэтому после первоначального показа 40 стрелок пользователь может
| |||
// свободно приближать график колесом мыши.
| |||
void FitInitialArrowHistory()
| |||
{
| |||
if(TickCount<=0)
| |||
return;
| |||
| |||
const int targetPerDirection=10;
| |||
long firstGraphSerial=MathMax(1,TickSerial-TickCount+1);
| |||
long oldestSerial=-1;
| |||
long newestSerial=-1;
| |||
| |||
for(int direction=0;direction<2;direction++)
| |||
{
| |||
long cutoff=9223372036854775807;
| |||
for(int n=0;n<targetPerDirection;n++)
| |||
{
| |||
long best=-1;
| |||
for(int k=0;k<ArraySize(SignalMarks);k++)
| |||
{
| |||
bool wanted=(direction==1 ? SignalMarks[k].up : !SignalMarks[k].up);
| |||
if(!wanted)
| |||
continue;
| |||
| |||
long serial=SignalMarks[k].serial;
| |||
if(serial<firstGraphSerial || serial>TickSerial || serial>=cutoff)
| |||
continue;
| |||
| |||
if(serial>best)
| |||
best=serial;
| |||
}
| |||
| |||
if(best<0)
| |||
break;
| |||
| |||
oldestSerial=(oldestSerial<0 ? best : MathMin(oldestSerial,best));
| |||
newestSerial=(newestSerial<0 ? best : MathMax(newestSerial,best));
| |||
cutoff=best;
| |||
}
| |||
}
| |||
| |||
if(oldestSerial<0 || newestSerial<0)
| |||
{
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
return;
| |||
}
| |||
| |||
long spanLong=newestSerial-oldestSerial+1;
| |||
int requiredSpan=(int)MathMin((long)TickCount,spanLong);
| |||
requiredSpan=MathMax(MinViewTicks,requiredSpan);
| |||
requiredSpan=MathMin(requiredSpan,TickCount);
| |||
| |||
ViewSpan=requiredSpan;
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
}
| |||
| |||
| |||
// Сохраняем отдельную проверку состава истории: для стандартного значения
| |||
// ReversalArrowsHistory=20 ожидается максимум 20 UP + 20 DOWN.
| |||
void ValidateReversalArrowHistory()
| |||
{
| |||
if(ReversalArrowsHistory<=0)
| |||
return;
| |||
| |||
int upCount=0;
| |||
int downCount=0;
| |||
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
if(SignalMarks[i].up) upCount++;
| |||
else downCount++;
| |||
}
| |||
| |||
if(upCount>ReversalArrowsHistory || downCount>ReversalArrowsHistory)
| |||
Print("TPDR: reversal arrow history invariant violated. UP=",
| |||
upCount," DOWN=",downCount,
| |||
" Limit=",ReversalArrowsHistory);
| |||
}
| |||
| |||
int GraphPriceToY(const double price)
| |||
{
| |||
if(CanvasHeight<=1 || GraphScaleMax<=GraphScaleMin)
| |||
return CanvasHeight/2;
| |||
| |||
const int edgePadding=6;
| |||
int drawableHeight=MathMax(1,CanvasHeight-1-2*edgePadding);
| |||
| |||
double ratio=(GraphScaleMax-price)/(GraphScaleMax-GraphScaleMin);
| |||
ratio=MathMax(0.0,MathMin(1.0,ratio));
| |||
| |||
return edgePadding+(int)MathRound(ratio*(double)drawableHeight);
| |||
}
| |||
| |||
int PriceToY(const double price)
| |||
{
| |||
return GraphPriceToY(price);
| |||
}
| |||
| |||
bool IsGraphPriceVisible(const double price)
| |||
{
| |||
// Уровни больше не являются clipping-маской. Функция означает только
| |||
// принадлежность цены текущему AutoScale viewport.
| |||
return GraphScaleMax>GraphScaleMin &&
| |||
price>=GraphScaleMin &&
| |||
price<=GraphScaleMax;
| |||
}
| |||
| |||
void DrawDottedHorizontal(const int y,const uint clr)
| |||
{
| |||
if(!CanvasReady || CanvasWidth<=0)
| |||
return;
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
for(int x=0;x<graphWidth;x+=8)
| |||
{
| |||
int x2=MathMin(x+4,graphWidth-1);
| |||
TickCanvas.Line(x,y,x2,y,clr);
| |||
}
| |||
}
| |||
| |||
void EnsureLevelPriceLabel(const string name,const int x,const int y,
| |||
const color clr,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: level price label creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,20);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
void DrawActiveLevelPriceLabels(const double bid)
| |||
{
| |||
return;
| |||
}
| |||
| |||
void DrawRayLayer(const double bid)
| |||
{
| |||
if(!CanvasReady || !DrawRayObjects ||
| |||
GraphScaleMax<=GraphScaleMin || TickLevelRowCount<=0)
| |||
return;
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
{
| |||
int grid=TickRowGrid(row,bid);
| |||
if(grid==INVALID_GRID)
| |||
continue;
| |||
| |||
double price=TickRowPrice(row,grid);
| |||
if(!IsGraphPriceVisible(price))
| |||
continue;
| |||
| |||
int y=GraphPriceToY(price);
| |||
color rayColor=(row==TickLevelRowCount-1 ?
| |||
CurrentRayColor : RayColor);
| |||
| |||
if(row==TickLevelRowCount-1)
| |||
TickCanvas.LineHorizontal(0,graphWidth-1,y,
| |||
ColorToARGB(rayColor,230));
| |||
else
| |||
DrawDottedHorizontal(y,ColorToARGB(rayColor,210));
| |||
}
| |||
}
| |||
| |||
| |||
int GetGraphWidth()
| |||
{
| |||
// Canvas занимает только левую область тикового графика.
| |||
// Правая область полностью резервируется под цены и не рисуется Canvas-ом.
| |||
return MathMax(1,CanvasWidth);
| |||
}
| |||
| |||
void DrawTickLayer(const double currentBid)
| |||
{
| |||
if(!CanvasReady || TickCount<=0 || currentBid<=0.0)
| |||
return;
| |||
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
if(viewCount<=0)
| |||
return;
| |||
| |||
uint graphColor=ColorToARGB(TickGraphColor,255);
| |||
int graphWidth=GetGraphWidth();
| |||
int leftMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0);
| |||
int rightMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0);
| |||
int drawWidth=MathMax(1,graphWidth-leftMargin-rightMargin);
| |||
| |||
if(viewCount==1)
| |||
{
| |||
int y=GraphPriceToY(currentBid);
| |||
int x=leftMargin;
| |||
TickCanvas.LineThick(x,y,MathMin(graphWidth-1,x+4),y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
| |||
return;
| |||
}
| |||
| |||
bool havePrevious=false;
| |||
int previousX=0;
| |||
int previousY=0;
| |||
| |||
for(int i=0;i<viewCount;i++)
| |||
{
| |||
int x=leftMargin+(int)MathRound((double)i*(double)(drawWidth-1)/(double)(viewCount-1));
| |||
double price=GetTickPriceByVisibleIndex(viewStart+i);
| |||
| |||
// Последняя точка всегда является фактическим текущим Bid.
| |||
if(i==viewCount-1)
| |||
price=currentBid;
| |||
| |||
if(!IsGraphPriceVisible(price))
| |||
{
| |||
havePrevious=false;
| |||
continue;
| |||
}
| |||
| |||
int y=GraphPriceToY(price);
| |||
| |||
if(havePrevious)
| |||
TickCanvas.LineThick(previousX,previousY,x,y,graphColor,2,STYLE_SOLID,LINE_END_ROUND);
| |||
| |||
previousX=x;
| |||
previousY=y;
| |||
havePrevious=true;
| |||
}
| |||
}
| |||
| |||
void DrawArrowMark(const int x,const int y,const bool up,const bool reversal,const color clr)
| |||
{
| |||
if(!CanvasReady)
| |||
return;
| |||
| |||
uint c=ColorToARGB(clr,255);
| |||
int size=reversal ? 7 : 5;
| |||
| |||
// (x,y) — точка события, то есть ОСТРИЁ стрелки.
| |||
// Геометрия стрелки строится относительно этой точки и больше
| |||
// не добавляет скрытого смещения по цене.
| |||
if(up)
| |||
{
| |||
TickCanvas.Triangle(x,y,x-size,y+2*size,x+size,y+2*size,c);
| |||
}
| |||
else
| |||
{
| |||
TickCanvas.Triangle(x,y,x-size,y-2*size,x+size,y-2*size,c);
| |||
}
| |||
}
| |||
| |||
void DrawBidMarker(const double bid)
| |||
{
| |||
if(!CanvasReady || bid<=0.0)
| |||
return;
| |||
| |||
int y=GraphPriceToY(bid);
| |||
uint c=ColorToARGB(CurrentRayColor,255);
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
int rightX=(InitialViewLayoutActive ?
| |||
MathMax(0,graphWidth-MathMin(20,MathMax(0,graphWidth/2-1))) :
| |||
MathMax(0,graphWidth-1));
| |||
int markerWidth=MathMin(4,graphWidth);
| |||
int markerStart=MathMax(0,rightX-markerWidth+1);
| |||
TickCanvas.Line(markerStart,y,rightX,y,c);
| |||
TickCanvas.Triangle(MathMax(2,rightX-12),y-5,
| |||
MathMax(2,rightX-2),y,
| |||
MathMax(2,rightX-12),y+5,c);
| |||
}
| |||
| |||
void DrawCurrentTickMarker(const double currentBid)
| |||
{
| |||
if(!CanvasReady || currentBid<=0.0)
| |||
return;
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
int x=(InitialViewLayoutActive ?
| |||
MathMax(0,graphWidth-MathMin(20,MathMax(0,graphWidth/2-1))) :
| |||
MathMax(0,graphWidth-1));
| |||
int y=GraphPriceToY(currentBid);
| |||
| |||
TickCanvas.FillCircle(x,y,3,ColorToARGB(TickGraphColor,255));
| |||
}
| |||
| |||
void DrawSignalLayer()
| |||
{
| |||
if(!CanvasReady || TickCount<=0 || ArraySize(SignalMarks)==0)
| |||
return;
| |||
| |||
// Рендер не меняет горизонтальный масштаб. Пользовательский zoom
| |||
// полностью управляет ViewSpan/ViewEndOffset.
| |||
long firstSerial=TickSerial-TickCount+1;
| |||
int viewStart=GetViewStartIndex();
| |||
int viewCount=GetViewCount();
| |||
int viewEnd=viewStart+viewCount-1;
| |||
| |||
for(int i=0;i<ArraySize(SignalMarks);i++)
| |||
{
| |||
long serial=SignalMarks[i].serial;
| |||
| |||
if(serial<firstSerial || serial>TickSerial)
| |||
continue;
| |||
| |||
int index=(int)(serial-firstSerial);
| |||
if(index<viewStart || index>viewEnd)
| |||
continue;
| |||
| |||
int relative=index-viewStart;
| |||
int graphWidth=GetGraphWidth();
| |||
int leftMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0);
| |||
int rightMargin=(InitialViewLayoutActive ? MathMin(20,MathMax(0,graphWidth/2-1)) : 0);
| |||
int drawWidth=MathMax(1,graphWidth-leftMargin-rightMargin);
| |||
int x=(viewCount==1) ? leftMargin :
| |||
leftMargin+(int)MathRound((double)relative*(double)(drawWidth-1)/(double)(viewCount-1));
| |||
double signalPrice=SignalMarks[i].price;
| |||
if(!IsGraphPriceVisible(signalPrice))
| |||
continue;
| |||
| |||
int y=GraphPriceToY(signalPrice);
| |||
| |||
color clr=SignalMarks[i].reversal ?
| |||
(SignalMarks[i].up ? BuyColor : SellColor) :
| |||
(SignalMarks[i].up ? UpCrossColor : DownCrossColor);
| |||
| |||
// Точка привязки стрелки = абсолютная цена события.
| |||
// Никакого дополнительного вертикального offset здесь быть не должно:
| |||
// острие DrawArrowMark() совпадает с GraphPriceToY(price).
| |||
int arrowY=y;
| |||
| |||
DrawArrowMark(x,arrowY,SignalMarks[i].up,SignalMarks[i].reversal,clr);
| |||
}
| |||
}
| |||
| |||
void DeleteLegacyPriceObjects()
| |||
{
| |||
if(LegacyPriceObjectsCleaned)
| |||
return;
| |||
| |||
// Однократная миграционная очистка. Старые объекты больше не создаются,
| |||
// поэтому удалять их на каждом тике не имеет смысла и создаёт лишнюю
| |||
// нагрузку на графический поток.
| |||
for(int i=0;i<200;i++)
| |||
ObjectDelete(0,Prefix+"PRICE_DYNAMIC_"+IntegerToString(i));
| |||
| |||
for(int i=0;i<50;i++)
| |||
{
| |||
ObjectDelete(0,Prefix+"DIST_UP_"+IntegerToString(i));
| |||
ObjectDelete(0,Prefix+"DIST_DOWN_"+IntegerToString(i));
| |||
}
| |||
| |||
ObjectDelete(0,Prefix+"PRICE_BID");
| |||
ObjectDelete(0,Prefix+"CURRENT_DIRECTION_ARROW");
| |||
| |||
LegacyPriceObjectsCleaned=true;
| |||
}
| |||
| |||
void DeleteLegacyStatePanel()
| |||
{
| |||
string names[16]=
| |||
{
| |||
Prefix+"STATE_TITLE",
| |||
Prefix+"STATE_LEVEL_LABEL",Prefix+"STATE_LEVEL_VALUE",
| |||
Prefix+"STATE_DIRECTION_LABEL",Prefix+"STATE_DIRECTION_VALUE",
| |||
Prefix+"STATE_REVERSAL_AT_LABEL",Prefix+"STATE_REVERSAL_AT_VALUE",
| |||
Prefix+"STATE_LAST_DOWN_2",Prefix+"STATE_LAST_DOWN_2_VALUE",
| |||
Prefix+"STATE_LAST_DOWN_1",Prefix+"STATE_LAST_DOWN_1_VALUE",
| |||
Prefix+"STATE_LAST_UP_2",Prefix+"STATE_LAST_UP_2_VALUE",
| |||
Prefix+"STATE_LAST_UP_1",Prefix+"STATE_LAST_UP_1_VALUE"
| |||
};
| |||
for(int i=0;i<ArraySize(names);i++)
| |||
if(ObjectFind(0,names[i])>=0)
| |||
ObjectDelete(0,names[i]);
| |||
}
| |||
| |||
void EnsureCodeLockStateLabel(const string name,const int x,const int y,
| |||
const int fontSize,const color clr,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: CodeLock state label creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,10);
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,fontSize);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
color CodeLockDirectionColor(const int direction)
| |||
{
| |||
if(direction>0)
| |||
return clrLimeGreen;
| |||
if(direction<0)
| |||
return clrTomato;
| |||
return clrSilver;
| |||
}
| |||
| |||
string CodeLockDirectionText(const int direction)
| |||
{
| |||
if(direction>0)
| |||
return "UP";
| |||
if(direction<0)
| |||
return "DOWN";
| |||
return "WAIT";
| |||
}
| |||
| |||
void DrawCodeLockStatePanel(const double bid)
| |||
{
| |||
if(SubWindow<0 || bid<=0.0 || GetViewportLevelStepPrice()<=0.0 || CanvasWidth<=0)
| |||
return;
| |||
| |||
DeleteLegacyPriceObjects();
| |||
| |||
int panelX=CanvasWidth+100;
| |||
int panelY=8;
| |||
int labelGap=18;
| |||
int valueX=112;
| |||
int valueFont=9;
| |||
int headerFont=8;
| |||
| |||
// LEVEL — текущий абсолютный LEVEL, определённый общей моделью.
| |||
double levelPrice=0.0;
| |||
if(DirectionRay!=INVALID_GRID)
| |||
levelPrice=ViewportRayPrice(DirectionRay);
| |||
else
| |||
levelPrice=ViewportRayPrice(CurrentLevelGridInitialized ?
| |||
CurrentLevelGrid : FloorGridForViewport(bid));
| |||
| |||
string levelText=DoubleToString(levelPrice,_Digits);
| |||
string directionText=CodeLockDirectionText(Direction);
| |||
color directionColor=CodeLockDirectionColor(Direction);
| |||
| |||
string reversalAtText=(ReversalAtPrice>0.0 ?
| |||
DoubleToString(ReversalAtPrice,_Digits) : "—");
| |||
| |||
string lastDown2=(LastDown2Price>0.0 ?
| |||
DoubleToString(LastDown2Price,_Digits) : "—");
| |||
string lastDown1=(LastDown1Price>0.0 ?
| |||
DoubleToString(LastDown1Price,_Digits) : "—");
| |||
string lastUp2=(LastUp2Price>0.0 ?
| |||
DoubleToString(LastUp2Price,_Digits) : "—");
| |||
string lastUp1=(LastUp1Price>0.0 ?
| |||
DoubleToString(LastUp1Price,_Digits) : "—");
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_TITLE",
| |||
panelX,panelY,headerFont,clrSilver,
| |||
"CODELOCK STATE");
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LEVEL_LABEL",
| |||
panelX,panelY+labelGap,headerFont,clrSilver,
| |||
"LEVEL");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LEVEL_VALUE",
| |||
panelX+valueX,panelY+labelGap,valueFont,
| |||
directionColor,levelText);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_DIRECTION_LABEL",
| |||
panelX,panelY+2*labelGap,headerFont,clrSilver,
| |||
"DIRECTION");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_DIRECTION_VALUE",
| |||
panelX+valueX,panelY+2*labelGap,valueFont,
| |||
directionColor,directionText);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_REVERSAL_AT_LABEL",
| |||
panelX,panelY+3*labelGap,headerFont,clrSilver,
| |||
"REVERSAL AT");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_REVERSAL_AT_VALUE",
| |||
panelX+valueX,panelY+3*labelGap,valueFont,
| |||
directionColor,reversalAtText);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_2",
| |||
panelX,panelY+5*labelGap,headerFont,clrTomato,
| |||
"LAST-DOWN-2");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_2_VALUE",
| |||
panelX+valueX,panelY+5*labelGap,valueFont,
| |||
clrTomato,lastDown2);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_1",
| |||
panelX,panelY+6*labelGap,headerFont,clrTomato,
| |||
"LAST-DOWN-1");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_DOWN_1_VALUE",
| |||
panelX+valueX,panelY+6*labelGap,valueFont,
| |||
clrTomato,lastDown1);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_2",
| |||
panelX,panelY+7*labelGap,headerFont,clrLimeGreen,
| |||
"LAST-UP-2");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_2_VALUE",
| |||
panelX+valueX,panelY+7*labelGap,valueFont,
| |||
clrLimeGreen,lastUp2);
| |||
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_1",
| |||
panelX,panelY+8*labelGap,headerFont,clrLimeGreen,
| |||
"LAST-UP-1");
| |||
EnsureCodeLockStateLabel(Prefix+"STATE_LAST_UP_1_VALUE",
| |||
panelX+valueX,panelY+8*labelGap,valueFont,
| |||
clrLimeGreen,lastUp1);
| |||
}
| |||
| |||
| |||
| |||
bool EnsureCanvas()
| |||
{
| |||
if(SubWindow<0)
| |||
return false;
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int chartHeight=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
| |||
// Правая часть под цены физически не входит в Canvas, поэтому
| |||
// тиковая линия не может накладываться на ценовые подписи.
| |||
int wantedWidth=MathMax(50,chartWidth-PriceScaleWidth);
| |||
| |||
if(wantedWidth<50 || chartHeight<20)
| |||
return false;
| |||
| |||
if(CanvasReady && CanvasWidth==wantedWidth && CanvasHeight==chartHeight)
| |||
return true;
| |||
| |||
if(CanvasReady)
| |||
{
| |||
ResetLastError();
| |||
if(!TickCanvas.Resize(wantedWidth,chartHeight))
| |||
{
| |||
Print("TPDR: canvas resize failed. Error=",GetLastError());
| |||
return false;
| |||
}
| |||
CanvasWidth=wantedWidth;
| |||
CanvasHeight=chartHeight;
| |||
return true;
| |||
}
| |||
| |||
ResetLastError();
| |||
| |||
if(!TickCanvas.CreateBitmapLabel(0,SubWindow,Prefix+"PANEL",0,0,
| |||
wantedWidth,chartHeight,
| |||
COLOR_FORMAT_ARGB_NORMALIZE))
| |||
{
| |||
Print("TPDR: canvas creation failed. Error=",GetLastError());
| |||
return false;
| |||
}
| |||
| |||
CanvasWidth=wantedWidth;
| |||
CanvasHeight=chartHeight;
| |||
CanvasReady=true;
| |||
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_BACK,true);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,Prefix+"PANEL",OBJPROP_HIDDEN,true);
| |||
| |||
return true;
| |||
}
| |||
| |||
bool IsPointInsideObject(const string name,const int x,const int y)
| |||
{
| |||
if(ObjectFind(0,name)<0 || SubWindow<0)
| |||
return false;
| |||
| |||
int bx=(int)ObjectGetInteger(0,name,OBJPROP_XDISTANCE);
| |||
int by=(int)ObjectGetInteger(0,name,OBJPROP_YDISTANCE);
| |||
int bw=(int)ObjectGetInteger(0,name,OBJPROP_XSIZE);
| |||
int bh=(int)ObjectGetInteger(0,name,OBJPROP_YSIZE);
| |||
ENUM_BASE_CORNER corner=(ENUM_BASE_CORNER)ObjectGetInteger(
| |||
0,name,OBJPROP_CORNER);
| |||
| |||
// CHARTEVENT_MOUSE_MOVE передаёт Y относительно главного окна,
| |||
// графические объекты подокна — относительно самого подокна.
| |||
int subwindowTop=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
| |||
int localY=y-subwindowTop;
| |||
int localX=x;
| |||
| |||
int top=by;
| |||
int left=bx;
| |||
| |||
if(corner==CORNER_LEFT_LOWER)
| |||
top=CanvasHeight-by-bh;
| |||
else if(corner==CORNER_RIGHT_UPPER ||
| |||
corner==CORNER_RIGHT_LOWER)
| |||
left=CanvasWidth-bx-bw;
| |||
| |||
if(corner==CORNER_RIGHT_LOWER)
| |||
top=CanvasHeight-by-bh;
| |||
| |||
return localX>=left && localX<left+bw &&
| |||
localY>=top && localY<top+bh;
| |||
}
| |||
| |||
void EnsureResetButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: reset button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,ResetButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,10);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1000);
| |||
}
| |||
}
| |||
| |||
void EnsureZoomButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: zoom button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,12);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
| |||
}
| |||
}
| |||
| |||
void EnsureLevelCountButton(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_BUTTON,0,0,0))
| |||
{
| |||
Print("TPDR: level button creation error=",GetLastError(),
| |||
" Name=",name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,LevelButtonWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,LevelButtonHeight);
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,clrGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,120);
| |||
}
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
void SetLevelButtonVisual(const string name,const bool selected)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
return;
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_STATE,selected);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,selected ? clrGold : clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,selected ? clrBlack : clrWhite);
| |||
}
| |||
| |||
void EnsurePanelLabel(const string name,const string textValue)
| |||
{
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_LABEL,0,0,0))
| |||
{
| |||
Print("TPDR: panel label creation error=",GetLastError()," Name=",name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetString(0,name,OBJPROP_FONT,"Arial");
| |||
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,9);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrWhite);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,500);
| |||
}
| |||
ObjectSetString(0,name,OBJPROP_TEXT,textValue);
| |||
}
| |||
| |||
void EnsureControlPanelBackground()
| |||
{
| |||
string name=Prefix+"CONTROL_PANEL_BG";
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0))
| |||
{
| |||
Print("TPDR: control panel background creation error=",GetLastError());
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,5);
| |||
}
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int panelWidth=MathMin(520,MathMax(360,chartWidth-16));
| |||
int panelLeft=MathMax(8,chartWidth-panelWidth-8);
| |||
int tableHeight=ControlPanelTop+LevelButtonHeight+2+18+
| |||
TickLevelRowCount*(LevelButtonHeight+LevelButtonGap)+8;
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,panelLeft);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,8);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,panelWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,MathMax(60,tableHeight));
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrDimGray);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
}
| |||
| |||
void UpdateLevelTableLayout(const double bid)
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0 || bid<=0.0)
| |||
return;
| |||
| |||
EnsureControlPanelBackground();
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int panelWidth=MathMin(520,MathMax(360,chartWidth-16));
| |||
int panelLeft=MathMax(8,chartWidth-panelWidth-8);
| |||
| |||
// Таблица LEVEL находится в главном ценовом окне справа.
| |||
// График TickGraphic остаётся в отдельном подокне и занимает всю его ширину.
| |||
| |||
// Старые кнопки LEVELS/DISTANCE больше не являются частью модели.
| |||
string legacyNames[17]=
| |||
{
| |||
Prefix+"LEVEL_UP_3",Prefix+"LEVEL_UP_5",Prefix+"LEVEL_UP_10",
| |||
Prefix+"LEVEL_DOWN_3",Prefix+"LEVEL_DOWN_5",Prefix+"LEVEL_DOWN_10",
| |||
Prefix+"DISTANCE_10",Prefix+"DISTANCE_20",Prefix+"DISTANCE_30",
| |||
Prefix+"DISTANCE_40",Prefix+"DISTANCE_50",Prefix+"DISTANCE_100",
| |||
Prefix+"DISTANCE_150",Prefix+"DISTANCE_200",Prefix+"DISTANCE_300",
| |||
Prefix+"DISTANCE_350",Prefix+"DISTANCE_400"
| |||
};
| |||
| |||
for(int k=0;k<17;k++)
| |||
if(ObjectFind(0,legacyNames[k])>=0)
| |||
ObjectDelete(0,legacyNames[k]);
| |||
| |||
if(ObjectFind(0,Prefix+"LEVELS_LABEL")>=0)
| |||
ObjectDelete(0,Prefix+"LEVELS_LABEL");
| |||
if(ObjectFind(0,Prefix+"DISTANCE_LABEL")>=0)
| |||
ObjectDelete(0,Prefix+"DISTANCE_LABEL");
| |||
| |||
EnsurePanelLabel(Prefix+"LEVEL_TABLE_LABEL","LEVEL");
| |||
int x=panelLeft+ControlPanelLabelWidth+ControlPanelPadding;
| |||
int y=8+ControlPanelTop+LevelButtonHeight+2;
| |||
| |||
EnsurePanelLabel(Prefix+"LEVEL_TABLE_HEADER",
| |||
"DIRECTION REVERSAL AT ALERT D2 D1 U2 U1");
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap));
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_YDISTANCE,y-16);
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_HEADER",OBJPROP_ZORDER,1000);
| |||
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_XDISTANCE,
| |||
panelLeft+ControlPanelPadding);
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_YDISTANCE,y+3);
| |||
ObjectSetInteger(0,Prefix+"LEVEL_TABLE_LABEL",OBJPROP_ZORDER,1000);
| |||
| |||
// Показываем строки сверху вниз: самый высокий LEVEL первым.
| |||
for(int displayRow=0;displayRow<TickLevelRowCount;displayRow++)
| |||
{
| |||
int index=TickLevelRowCount-1-displayRow;
| |||
int rowY=y+displayRow*(LevelButtonHeight+LevelButtonGap);
| |||
| |||
string suffix=IntegerToString(index);
| |||
string minusName=Prefix+"LEVEL_MINUS_"+suffix;
| |||
string levelName=Prefix+"LEVEL_"+suffix;
| |||
string plusName=Prefix+"LEVEL_PLUS_"+suffix;
| |||
string dirName=Prefix+"LEVEL_DIR_"+suffix;
| |||
string revName=Prefix+"LEVEL_REV_"+suffix;
| |||
string alertName=Prefix+"LEVEL_ALERT_"+suffix;
| |||
string lastDown2Name=Prefix+"LEVEL_LAST_DOWN_2_"+suffix;
| |||
string lastDown1Name=Prefix+"LEVEL_LAST_DOWN_1_"+suffix;
| |||
string lastUp2Name=Prefix+"LEVEL_LAST_UP_2_"+suffix;
| |||
string lastUp1Name=Prefix+"LEVEL_LAST_UP_1_"+suffix;
| |||
| |||
EnsureLevelCountButton(minusName,"-");
| |||
EnsureLevelCountButton(plusName,"+");
| |||
EnsurePanelLabel(dirName,CodeLockDirectionText(TickLevelRows[index].direction));
| |||
EnsurePanelLabel(revName,
| |||
TickLevelRows[index].reversalAtPrice>0.0 ?
| |||
DoubleToString(TickLevelRows[index].reversalAtPrice,_Digits) : "—");
| |||
EnsureLevelCountButton(alertName,TickLevelRows[index].alertEnabled ? "ON" : "OFF");
| |||
EnsurePanelLabel(lastDown2Name,
| |||
TickLevelRows[index].lastDown2Price>0.0 ?
| |||
DoubleToString(TickLevelRows[index].lastDown2Price,_Digits) : "—");
| |||
EnsurePanelLabel(lastDown1Name,
| |||
TickLevelRows[index].lastDown1Price>0.0 ?
| |||
DoubleToString(TickLevelRows[index].lastDown1Price,_Digits) : "—");
| |||
EnsurePanelLabel(lastUp2Name,
| |||
TickLevelRows[index].lastUp2Price>0.0 ?
| |||
DoubleToString(TickLevelRows[index].lastUp2Price,_Digits) : "—");
| |||
EnsurePanelLabel(lastUp1Name,
| |||
TickLevelRows[index].lastUp1Price>0.0 ?
| |||
DoubleToString(TickLevelRows[index].lastUp1Price,_Digits) : "—");
| |||
| |||
// LEVEL — не кнопка выбора DISTANCE, а редактируемая ячейка.
| |||
EnsureLevelCountButton(levelName,IntegerToString(TickLevelRows[index].level));
| |||
| |||
ObjectSetInteger(0,minusName,OBJPROP_XDISTANCE,x);
| |||
ObjectSetInteger(0,minusName,OBJPROP_YDISTANCE,rowY);
| |||
ObjectSetInteger(0,plusName,OBJPROP_XDISTANCE,x+LevelButtonWidth+LevelButtonGap);
| |||
ObjectSetInteger(0,plusName,OBJPROP_YDISTANCE,rowY);
| |||
| |||
ObjectSetInteger(0,levelName,OBJPROP_XDISTANCE,
| |||
x+2*(LevelButtonWidth+LevelButtonGap));
| |||
ObjectSetInteger(0,levelName,OBJPROP_YDISTANCE,rowY);
| |||
ObjectSetInteger(0,levelName,OBJPROP_ZORDER,1100);
| |||
| |||
ObjectSetInteger(0,dirName,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+8);
| |||
ObjectSetInteger(0,dirName,OBJPROP_YDISTANCE,rowY+3);
| |||
| |||
ObjectSetInteger(0,revName,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+55);
| |||
ObjectSetInteger(0,revName,OBJPROP_YDISTANCE,rowY+3);
| |||
| |||
ObjectSetInteger(0,alertName,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+125);
| |||
ObjectSetInteger(0,alertName,OBJPROP_XSIZE,42);
| |||
ObjectSetInteger(0,alertName,OBJPROP_YSIZE,LevelButtonHeight);
| |||
SetLevelButtonVisual(alertName,TickLevelRows[index].alertEnabled);
| |||
ObjectSetInteger(0,alertName,OBJPROP_YDISTANCE,rowY+3);
| |||
ObjectSetInteger(0,lastDown2Name,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+175);
| |||
ObjectSetInteger(0,lastDown2Name,OBJPROP_YDISTANCE,rowY+3);
| |||
ObjectSetInteger(0,lastDown1Name,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+225);
| |||
ObjectSetInteger(0,lastDown1Name,OBJPROP_YDISTANCE,rowY+3);
| |||
ObjectSetInteger(0,lastUp2Name,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+275);
| |||
ObjectSetInteger(0,lastUp2Name,OBJPROP_YDISTANCE,rowY+3);
| |||
ObjectSetInteger(0,lastUp1Name,OBJPROP_XDISTANCE,
| |||
x+3*(LevelButtonWidth+LevelButtonGap)+325);
| |||
ObjectSetInteger(0,lastUp1Name,OBJPROP_YDISTANCE,rowY+3);
| |||
| |||
SetLevelButtonVisual(minusName,false);
| |||
SetLevelButtonVisual(plusName,false);
| |||
SetLevelButtonVisual(levelName,false);
| |||
| |||
ObjectSetInteger(0,dirName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,revName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,alertName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,lastDown2Name,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,lastDown1Name,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,lastUp2Name,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,lastUp1Name,OBJPROP_ZORDER,1000);
| |||
| |||
ObjectSetInteger(0,minusName,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,plusName,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,levelName,OBJPROP_HIDDEN,true);
| |||
}
| |||
| |||
// Удаляем строки, которые остались от предыдущего количества LEVEL.
| |||
for(int i=TickLevelRowCount;i<TickMaxVisualRows;i++)
| |||
{
| |||
string suffix=IntegerToString(i);
| |||
string names[10]=
| |||
{
| |||
Prefix+"LEVEL_MINUS_"+suffix,
| |||
Prefix+"LEVEL_"+suffix,
| |||
Prefix+"LEVEL_PLUS_"+suffix,
| |||
Prefix+"LEVEL_DIR_"+suffix,
| |||
Prefix+"LEVEL_REV_"+suffix,
| |||
Prefix+"LEVEL_ALERT_"+suffix,
| |||
Prefix+"LEVEL_LAST_DOWN_2_"+suffix,
| |||
Prefix+"LEVEL_LAST_DOWN_1_"+suffix,
| |||
Prefix+"LEVEL_LAST_UP_2_"+suffix,
| |||
Prefix+"LEVEL_LAST_UP_1_"+suffix
| |||
};
| |||
| |||
for(int k=0;k<10;k++)
| |||
if(ObjectFind(0,names[k])>=0)
| |||
ObjectDelete(0,names[k]);
| |||
}
| |||
}
| |||
| |||
| |||
void EnsureZoomControlBackground(const int x,const int y,const int totalWidth)
| |||
{
| |||
string name=Prefix+"ZOOM_CONTROL_BG";
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
return;
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,80);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
}
| |||
| |||
// Фон также принудительно привязываем к нижнему левому углу.
| |||
// Иначе старый объект мог остаться с прежним CORNER.
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_LOWER);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,80);
| |||
| |||
int padding=5;
| |||
int panelX=MathMax(0,x-padding);
| |||
int panelY=MathMax(0,y-padding);
| |||
int panelWidth=MathMin(CanvasWidth-panelX,totalWidth+2*padding);
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,panelX);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,panelY);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,MathMax(1,panelWidth));
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,ZoomButtonHeight+2*padding);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,clrBlack);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,clrDimGray);
| |||
}
| |||
| |||
void UpdateZoomButtonsLayout()
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return;
| |||
| |||
string minusName=Prefix+"ZOOM_MINUS";
| |||
string plusName=Prefix+"ZOOM_PLUS";
| |||
string resetName=Prefix+"RESET_CURRENT";
| |||
string labelName=Prefix+"ZOOM_LABEL";
| |||
| |||
// Старый отдельный фон больше не используется: кнопки входят
| |||
// непосредственно в общую таблицу LEVELS/Distance.
| |||
string oldBackground=Prefix+"ZOOM_CONTROL_BG";
| |||
if(ObjectFind(0,oldBackground)>=0)
| |||
ObjectDelete(0,oldBackground);
| |||
| |||
EnsureControlPanelBackground();
| |||
| |||
EnsureZoomButton(minusName,"-");
| |||
EnsureZoomButton(plusName,"+");
| |||
EnsureResetButton(resetName,"RESET");
| |||
EnsurePanelLabel(labelName,"ZOOM");
| |||
| |||
int x=ControlPanelLabelWidth+ControlPanelPadding;
| |||
// ZOOM располагается последней строкой — в самом низу общей таблицы,
| |||
// после обеих строк Distance.
| |||
int y=ControlPanelTop+4*(LevelButtonHeight+LevelButtonGap)
| |||
+2*(DistanceButtonHeight+DistanceButtonGap)+4;
| |||
int x1=x;
| |||
int x2=x+ZoomButtonWidth+ZoomButtonGap;
| |||
int x3=x+2*(ZoomButtonWidth+ZoomButtonGap);
| |||
| |||
ObjectSetInteger(0,minusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,minusName,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,minusName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,minusName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,minusName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,minusName,OBJPROP_XDISTANCE,x1);
| |||
ObjectSetInteger(0,minusName,OBJPROP_YDISTANCE,y);
| |||
| |||
ObjectSetInteger(0,plusName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,plusName,OBJPROP_XSIZE,ZoomButtonWidth);
| |||
ObjectSetInteger(0,plusName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,plusName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,plusName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,plusName,OBJPROP_XDISTANCE,x2);
| |||
ObjectSetInteger(0,plusName,OBJPROP_YDISTANCE,y);
| |||
| |||
ObjectSetInteger(0,resetName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,resetName,OBJPROP_XSIZE,ResetButtonWidth);
| |||
ObjectSetInteger(0,resetName,OBJPROP_YSIZE,ZoomButtonHeight);
| |||
ObjectSetInteger(0,resetName,OBJPROP_BACK,false);
| |||
ObjectSetInteger(0,resetName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,resetName,OBJPROP_XDISTANCE,x3);
| |||
ObjectSetInteger(0,resetName,OBJPROP_YDISTANCE,y);
| |||
ObjectSetInteger(0,resetName,OBJPROP_STATE,ResetButtonHeld);
| |||
| |||
ObjectSetInteger(0,labelName,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,labelName,OBJPROP_ZORDER,1000);
| |||
ObjectSetInteger(0,labelName,OBJPROP_XDISTANCE,ControlPanelPadding);
| |||
ObjectSetInteger(0,labelName,OBJPROP_YDISTANCE,y+4);
| |||
}
| |||
| |||
void ResetGraphToCurrentPrice()
| |||
{
| |||
if(TickCount<=0)
| |||
return;
| |||
| |||
// RESET возвращает только начальный VIEW: последние 10 UP + 10 DOWN + BID.
| |||
// История, LEVEL и runtime-модель RESET не изменяются.
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),TickCount);
| |||
ViewEndOffset=0;
| |||
InitialViewLayoutActive=true;
| |||
FitInitialArrowHistory();
| |||
| |||
VerticalScaleManual=false;
| |||
VerticalScaleDragActive=false;
| |||
VerticalPanDragActive=false;
| |||
VerticalScaleHalfRange=0.0;
| |||
| |||
AutoScaleInitialized=false;
| |||
LastAutoScaleLevelsUp=-1;
| |||
LastAutoScaleLevelsDown=-1;
| |||
InitialArrowHistoryScalePending=true;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0 && GetViewportLevelStepPrice()>0.0)
| |||
{
| |||
double bid=NormalizeDouble(current.bid,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(bid);
| |||
ViewportCenterPrice=ViewportRayPrice(ScaleCenterGrid);
| |||
ScaleInitialized=true;
| |||
UpdateCurrentLevelGrid(bid);
| |||
}
| |||
| |||
ResetButtonHeld=false;
| |||
}
| |||
| |||
void ApplyZoomButton(const int delta)
| |||
{
| |||
if(TickCount<=1 || GetGraphWidth()<=1)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
HandleHorizontalZoom(GetGraphWidth()/2,delta);
| |||
RefreshGraph();
| |||
}
| |||
| |||
void HandleZoomButtonClick(const string name)
| |||
{
| |||
if(name==Prefix+"ZOOM_MINUS")
| |||
{
| |||
InitialViewLayoutActive=false;
| |||
ApplyZoomButton(-1);
| |||
return;
| |||
}
| |||
| |||
if(name==Prefix+"ZOOM_PLUS")
| |||
{
| |||
InitialViewLayoutActive=false;
| |||
ApplyZoomButton(1);
| |||
return;
| |||
}
| |||
| |||
if(name==Prefix+"RESET_CURRENT")
| |||
{
| |||
ResetGraphToCurrentPrice();
| |||
ResetButtonHeld=false;
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
| |||
void EnsurePriceScaleArea()
| |||
{
| |||
if(SubWindow<0 || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return;
| |||
| |||
string name=Prefix+"PRICE_SCALE_AREA";
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
int priceAreaWidth=MathMax(0,chartWidth-CanvasWidth);
| |||
| |||
if(priceAreaWidth<=0)
| |||
{
| |||
if(ObjectFind(0,name)>=0)
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,0);
| |||
return;
| |||
}
| |||
| |||
if(ObjectFind(0,name)<0)
| |||
{
| |||
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,SubWindow,0,0))
| |||
{
| |||
Print("TPDR: price scale area creation error=",GetLastError());
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
| |||
ObjectSetInteger(0,name,OBJPROP_SELECTED,false);
| |||
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
| |||
ObjectSetInteger(0,name,OBJPROP_BACK,true);
| |||
ObjectSetInteger(0,name,OBJPROP_ZORDER,1);
| |||
}
| |||
| |||
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,CanvasWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,0);
| |||
ObjectSetInteger(0,name,OBJPROP_XSIZE,priceAreaWidth);
| |||
ObjectSetInteger(0,name,OBJPROP_YSIZE,CanvasHeight);
| |||
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_COLOR,background);
| |||
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
| |||
}
| |||
| |||
void RenderGraph(const double bid)
| |||
{
| |||
// Один источник текущего визуального LEVEL для всех render-слоёв.
| |||
UpdateCurrentLevelGrid(bid);
| |||
| |||
// До завершения bootstrap отображение запрещено. Это единый инвариант
| |||
// для OnTimer(), OnCalculate(), RefreshGraph() и обработчиков кнопок.
| |||
if(!BootstrapComplete)
| |||
return;
| |||
| |||
if(!EnsureCanvas())
| |||
return;
| |||
| |||
color background=(color)ChartGetInteger(0,CHART_COLOR_BACKGROUND,0);
| |||
| |||
TickCanvas.Erase(ColorToARGB(background,255));
| |||
| |||
DrawRayLayer(bid);
| |||
DrawTickLayer(bid);
| |||
DrawSignalLayer();
| |||
DrawBidMarker(bid);
| |||
DrawCurrentTickMarker(bid);
| |||
| |||
TickCanvas.Update(true);
| |||
| |||
// Таблица LEVEL выводится в главном ценовом окне.
| |||
UpdateZoomButtonsLayout();
| |||
UpdateLevelTableLayout(bid);
| |||
DeleteLegacyStatePanel();
| |||
}
| |||
| |||
void RefreshGraph()
| |||
{
| |||
if(SubWindow<0 || !BootstrapComplete)
| |||
return;
| |||
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
| |||
return;
| |||
| |||
// Масштаб зависит от текущего горизонтального viewport.
| |||
// Поэтому после wheel/drag сначала пересчитываем вертикальный диапазон,
| |||
// затем рисуем. Сам ViewSpan при этом НЕ изменяется.
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending=false;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void UpdateReversalAt()
| |||
{
| |||
if(Direction==0 || DirectionRay==INVALID_GRID)
| |||
{
| |||
ReversalAtPrice=0.0;
| |||
return;
| |||
}
| |||
ReversalAtPrice=ViewportRayPrice(DirectionRay-Direction);
| |||
}
| |||
| |||
void RegisterCrossing(const int grid,const int crossingDirection,
| |||
const MqlTick &tick,const bool emitSignals)
| |||
{
| |||
LastCrossedRay=grid;
| |||
LastCrossingDirection=crossingDirection;
| |||
| |||
if(Direction==0)
| |||
{
| |||
Direction=crossingDirection;
| |||
DirectionRay=grid;
| |||
ConsecutiveCrossings=1;
| |||
UpdateReversalAt();
| |||
return;
| |||
}
| |||
| |||
if(crossingDirection==Direction)
| |||
{
| |||
// CROSSING того же направления не изменяет DIRECTION.
| |||
// Последовательность 1.020 -> 1.040 -> 1.060 при UP
| |||
// только фиксирует CROSSING; DIRECTION остаётся определённым
| |||
// последним подтверждённым REVERSAL.
| |||
DirectionRay=grid;
| |||
ConsecutiveCrossings++;
| |||
UpdateReversalAt();
| |||
return;
| |||
}
| |||
| |||
// Противоположное CROSSING само по себе ещё не меняет DIRECTION.
| |||
// REVERSAL подтверждается только ожидаемым соседним LEVEL.
| |||
int reversalGrid=DirectionRay-Direction;
| |||
if(grid!=reversalGrid)
| |||
return;
| |||
| |||
Direction=crossingDirection;
| |||
DirectionRay=grid;
| |||
ConsecutiveCrossings=1;
| |||
LastReversalDirection=crossingDirection;
| |||
LastDirectionChangePrice=ViewportRayPrice(grid);
| |||
UpdateReversalAt();
| |||
| |||
// LAST изменяется только после подтверждённого REVERSAL.
| |||
// -1 = последний, -2 = предыдущий.
| |||
if(crossingDirection<0)
| |||
{
| |||
LastDown2Price=LastDown1Price;
| |||
LastDown1Price=LastDirectionChangePrice;
| |||
}
| |||
else
| |||
{
| |||
LastUp2Price=LastUp1Price;
| |||
LastUp1Price=LastDirectionChangePrice;
| |||
}
| |||
| |||
RecordReversalArrow(tick,LastDirectionChangePrice,crossingDirection>0,emitSignals);
| |||
}
| |||
| |||
void ProcessBidTick(const MqlTick &tick,const bool emitSignals,
| |||
const bool drawGraph,const bool storeHistory)
| |||
{
| |||
if(tick.bid<=0.0)
| |||
return;
| |||
| |||
double bid=NormalizeDouble(tick.bid,_Digits);
| |||
| |||
if(storeHistory)
| |||
{
| |||
AppendModelTick(bid);
| |||
}
| |||
| |||
TickSerial++;
| |||
| |||
if(!HavePreviousBid)
| |||
{
| |||
PreviousBid=bid;
| |||
HavePreviousBid=true;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
if(!ScaleInitialized)
| |||
{
| |||
ScaleCenterGrid=FloorGridForViewport(bid);
| |||
ScaleInitialized=true;
| |||
}
| |||
return;
| |||
}
| |||
| |||
double oldBid=PreviousBid;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
if(bid==oldBid)
| |||
return;
| |||
| |||
for(int row=0;row<TickLevelRowCount;row++)
| |||
ProcessTickRow(row,oldBid,bid,tick,emitSignals);
| |||
| |||
PreviousBid=bid;
| |||
}
| |||
| |||
void UpdateCursorFromArray(const MqlTick &ticks[],const int copied,const int startIndex)
| |||
{
| |||
if(copied<=0)
| |||
return;
| |||
| |||
long finalMsc=ticks[copied-1].time_msc;
| |||
| |||
// CopyTicks() работает включительно: запрос от CursorMsc может вернуть
| |||
// уже обработанные тики с той же миллисекундой. Поэтому храним точное количество
| |||
// обработанных тиков внутри последней временной отметки.
| |||
if(finalMsc==CursorMsc)
| |||
{
| |||
int processedAtFinal=0;
| |||
for(int i=0;i<copied;i++)
| |||
{
| |||
if(ticks[i].time_msc==finalMsc && i>=startIndex)
| |||
processedAtFinal++;
| |||
}
| |||
| |||
if(processedAtFinal>0)
| |||
CursorSameMsc+=processedAtFinal;
| |||
}
| |||
else
| |||
{
| |||
CursorMsc=finalMsc;
| |||
| |||
int finalCount=0;
| |||
for(int i=copied-1;i>=0 && ticks[i].time_msc==finalMsc;i--)
| |||
finalCount++;
| |||
| |||
CursorSameMsc=finalCount;
| |||
}
| |||
}
| |||
| |||
bool BootstrapHistory()
| |||
{
| |||
int targetTicks=MathMax(HistoryGraphTicks,HistoryBootstrapTicks);
| |||
targetTicks=MathMax(targetTicks,100);
| |||
targetTicks=MathMin(targetTicks,MathMax(100,MaxModelHistoryTicks));
| |||
| |||
MqlTick ticks[];
| |||
ArrayResize(ticks,targetTicks);
| |||
| |||
int copied=-1;
| |||
int lastError=0;
| |||
| |||
for(int attempt=0;attempt<3;attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,0,(uint)targetTicks);
| |||
lastError=GetLastError();
| |||
| |||
if(copied<0)
| |||
{
| |||
if(lastError==4004)
| |||
{
| |||
targetTicks=MathMax(100,targetTicks/2);
| |||
ArrayResize(ticks,targetTicks);
| |||
continue;
| |||
}
| |||
| |||
if(lastError==4401 || lastError==4403)
| |||
{
| |||
BootstrapAttempts++;
| |||
if(BootstrapAttempts<50)
| |||
return false;
| |||
Print("TPDR: tick history synchronization did not complete. Error=",lastError);
| |||
return false;
| |||
}
| |||
| |||
Print("TPDR: bootstrap CopyTicks failed. Error=",lastError);
| |||
return false;
| |||
}
| |||
break;
| |||
}
| |||
| |||
if(copied<=0)
| |||
{
| |||
BootstrapAttempts++;
| |||
return false;
| |||
}
| |||
| |||
int lastAttach=-1;
| |||
for(int i=0;i<copied;i++)
| |||
if(ticks[i].time_msc<=AttachMsc)
| |||
lastAttach=i;
| |||
| |||
if(lastAttach<0)
| |||
{
| |||
BootstrapAttempts++;
| |||
return false;
| |||
}
| |||
| |||
if(copied>=targetTicks && ticks[copied-1].time_msc<AttachMsc)
| |||
{
| |||
BootstrapAttempts++;
| |||
return false;
| |||
}
| |||
| |||
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),MathMax(100,MaxModelHistoryTicks));
| |||
ModelTickCount=0;
| |||
ModelTickHead=0;
| |||
ClearDerivedState();
| |||
| |||
// История и Live проходят один и тот же ProcessBidTick().
| |||
for(int i=0;i<=lastAttach;i++)
| |||
{
| |||
if(ticks[i].bid>0.0)
| |||
ProcessBidTick(ticks[i],false,true,true);
| |||
}
| |||
| |||
int attachCount=0;
| |||
for(int i=lastAttach;i>=0 && ticks[i].time_msc==AttachMsc;i--)
| |||
attachCount++;
| |||
| |||
CursorMsc=AttachMsc;
| |||
CursorSameMsc=attachCount;
| |||
BootstrapAttempts=0;
| |||
BootstrapComplete=true;
| |||
LiveMode=true;
| |||
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MathMax(1,TickCount));
| |||
ViewEndOffset=0;
| |||
| |||
// Стрелки уже формируются тем же проходом, что и основная модель.
| |||
// Второй полный проход по ModelTickPrices[] здесь не нужен.
| |||
FitInitialArrowHistory();
| |||
InitialArrowHistoryScalePending=true;
| |||
| |||
return true;
| |||
}
| |||
| |||
| |||
bool ProcessLiveTicks()
| |||
{
| |||
MqlTick ticks[5000];
| |||
int requestCount=MathMin(MathMax(HistoryChunkSize,100),5000);
| |||
| |||
for(int chunk=0;chunk<20;chunk++)
| |||
{
| |||
int copied=-1;
| |||
int requestTried=requestCount;
| |||
| |||
for(int attempt=0;attempt<3;attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied=CopyTicks(_Symbol,ticks,COPY_TICKS_INFO,
| |||
(ulong)MathMax(1,CursorMsc),(uint)requestTried);
| |||
int error=GetLastError();
| |||
| |||
if(copied>=0)
| |||
break;
| |||
| |||
if(error!=4004)
| |||
return false;
| |||
| |||
requestTried=MathMax(100,requestTried/2);
| |||
}
| |||
| |||
if(copied<0 || copied==0)
| |||
return copied==0;
| |||
| |||
int start=0;
| |||
if(ticks[0].time_msc==CursorMsc)
| |||
start=MathMin(CursorSameMsc,copied);
| |||
| |||
int processed=copied-start;
| |||
for(int i=start;i<copied;i++)
| |||
ProcessBidTick(ticks[i],true,true,true);
| |||
| |||
UpdateCursorFromArray(ticks,copied,start);
| |||
| |||
if(copied<requestTried || processed==0)
| |||
return true;
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
| |||
int OnInit()
| |||
{
| |||
if(StartPrice<=0.0 || BufferPoints<0 ||
| |||
ReversalArrowsHistory<0 || ReversalArrowsHistory>20 ||
| |||
MaxModelHistoryTicks<100 ||
| |||
HistoryChunkSize<100 || HistoryGraphTicks<100 || HistoryBootstrapTicks<100 ||
| |||
InitialViewTicks<1 || MinViewTicks<1 || MinViewTicks>InitialViewTicks ||
| |||
MinViewTicks>MaxGraphSegments)
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
| |||
| |||
IndicatorSetString(INDICATOR_SHORTNAME,ShortName);
| |||
if(!LoadTickVisualRows())
| |||
InitializeTickVisualRows();
| |||
if(!IsStrictTickVisualRows())
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
| |||
LegacyPriceObjectsCleaned=false;
| |||
| |||
// Высотой подокна управляет терминал/пользователь.
| |||
// Индикатор не фиксирует её; Canvas занимает актуальную высоту подокна.
| |||
//
| |||
// Вертикальный масштаб также не фиксируем средствами indicator window.
| |||
// Собственный Canvas имеет отдельный AutoScale, рассчитанный по видимому
| |||
// диапазону тиков и минимальному диапазону выбранных уровней.
| |||
// INDICATOR_HEIGHT здесь НЕ задаём.
| |||
// Это свойство фиксирует высоту подокна; пользователь должен иметь
| |||
// возможность менять высоту мышью, а Canvas должен следовать ей.
| |||
IndicatorSetInteger(INDICATOR_FIXED_MINIMUM,false);
| |||
IndicatorSetInteger(INDICATOR_FIXED_MAXIMUM,false);
| |||
| |||
MqlTick attachTick;
| |||
| |||
if(!SymbolInfoTick(_Symbol,attachTick) || attachTick.bid<=0.0)
| |||
return INIT_FAILED;
| |||
| |||
AttachMsc=attachTick.time_msc;
| |||
CursorMsc=AttachMsc;
| |||
CursorSameMsc=0;
| |||
| |||
ArrayResize(TickPrices,MathMax(100,MaxGraphSegments),
| |||
MathMax(100,MaxGraphSegments));
| |||
ArrayResize(SignalMarks,0);
| |||
ArrayResize(ModelTickPrices,MathMax(100,MaxModelHistoryTicks),
| |||
MathMax(100,MaxModelHistoryTicks));
| |||
ModelTickCount=0;
| |||
ModelTickHead=0;
| |||
TickCount=0;
| |||
TickHead=0;
| |||
TickSerial=0;
| |||
HavePreviousBid=false;
| |||
PreviousBid=0.0;
| |||
BootstrapComplete=false;
| |||
LiveMode=false;
| |||
| |||
LastReversalDirection=0;
| |||
ScaleCenterGrid=FloorGridForViewport(attachTick.bid);
| |||
ScaleInitialized=false;
| |||
AutoScaleInitialized=false;
| |||
LastAutoScaleLevelsUp=-1;
| |||
LastAutoScaleLevelsDown=-1;
| |||
ViewSpan=MathMin(MathMax(InitialViewTicks,MinViewTicks),MaxGraphSegments);
| |||
ViewEndOffset=0;
| |||
DragActive=false;
| |||
DragStartX=0;
| |||
DragStartOffset=0;
| |||
| |||
// Runtime-модель всегда восстанавливается из истории.
| |||
RebuildTickVisualRowsFromHistory();
| |||
FitInitialArrowHistory();
| |||
| |||
// Над тиковым подокном колесо и перетаскивание обслуживает наш график,
| |||
// а над основным свечным окном сохраняется штатное управление терминала.
| |||
PreviousChartMouseScroll=(bool)ChartGetInteger(0,CHART_MOUSE_SCROLL);
| |||
MouseInTickSubwindow=false;
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,true);
| |||
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
| |||
ChartSetInteger(0,CHART_EVENT_MOUSE_WHEEL,true);
| |||
EventSetMillisecondTimer(100);
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)PreviousChartMouseScroll);
| |||
| |||
if(CanvasReady)
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady=false;
| |||
}
| |||
| |||
EventKillTimer();
| |||
ResetButtonHeld=false;
| |||
DeleteObjectsByPrefix();
| |||
}
| |||
| |||
bool UpdateMarketState()
| |||
{
| |||
// Единый владелец состояния рынка.
| |||
// OnCalculate() и OnTimer() только запускают эту функцию, но сама обработка
| |||
// history/live выполняется здесь ровно одним и тем же кодом.
| |||
if(SubWindow<0)
| |||
return false;
| |||
| |||
if(!BootstrapComplete)
| |||
{
| |||
if(!BootstrapHistory())
| |||
return false;
| |||
}
| |||
else
| |||
{
| |||
if(!ProcessLiveTicks())
| |||
return false;
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
void OnTimer()
| |||
{
| |||
// Таймер также может довести bootstrap до готового состояния: при создании
| |||
// подокна OnCalculate иногда вызывается раньше, чем ChartWindowFind() готов.
| |||
SubWindow=ChartWindowFind(0,ShortName);
| |||
if(SubWindow<0)
| |||
return;
| |||
| |||
// Пока bootstrap не завершён, состояние и отображение не публикуются.
| |||
// OnTimer() не имеет права рисовать промежуточную историю.
| |||
if(!UpdateMarketState())
| |||
return;
| |||
| |||
// RESET выполняется только по событию нажатия кнопки.
| |||
// Таймер не должен повторять тяжёлый полный reset каждые 100 мс.
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol,current) || current.bid<=0.0)
| |||
return;
| |||
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending=false;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void HandleHorizontalZoom(const int xCursor,const int delta)
| |||
{
| |||
if(TickCount<=1 || GetGraphWidth()<=1 || delta==0)
| |||
return;
| |||
| |||
int oldSpan=GetViewCount();
| |||
int oldStart=GetViewStartIndex();
| |||
| |||
int newSpan=oldSpan;
| |||
if(delta>0)
| |||
newSpan=MathMax(MinViewTicks,oldSpan/2);
| |||
else
| |||
newSpan=MathMin(MathMin(MaxGraphSegments,TickCount),MathMax(MinViewTicks,oldSpan*2));
| |||
| |||
newSpan=MathMax(1,MathMin(newSpan,TickCount));
| |||
| |||
int graphWidth=GetGraphWidth();
| |||
double ratio=(double)MathMax(0,MathMin(xCursor,graphWidth-1))/
| |||
(double)MathMax(1,graphWidth-1);
| |||
| |||
int anchorOffset=(int)MathRound(ratio*(double)MathMax(0,oldSpan-1));
| |||
int anchorIndex=oldStart+anchorOffset;
| |||
| |||
ViewSpan=newSpan;
| |||
| |||
int newAnchorOffset=(int)MathRound(ratio*(double)MathMax(0,newSpan-1));
| |||
int newStart=anchorIndex-newAnchorOffset;
| |||
newStart=MathMax(0,MathMin(newStart,TickCount-newSpan));
| |||
| |||
ViewEndOffset=TickCount-newSpan-newStart;
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,TickCount-newSpan));
| |||
}
| |||
| |||
void HandleHorizontalDrag(const int x)
| |||
{
| |||
if(!DragActive || TickCount<=1 || GetGraphWidth()<=1)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
int span=GetViewCount();
| |||
int dx=x-DragStartX;
| |||
int shift=(int)MathRound((double)dx*(double)span/(double)GetGraphWidth());
| |||
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
ViewEndOffset=DragStartOffset+shift;
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
}
| |||
| |||
bool IsCursorInTickSubwindow(const int y)
| |||
{
| |||
if(SubWindow<=0)
| |||
return false;
| |||
| |||
int top=(int)ChartGetInteger(0,CHART_WINDOW_YDISTANCE,SubWindow);
| |||
int height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,SubWindow);
| |||
| |||
return height>0 && y>=top && y<top+height;
| |||
}
| |||
| |||
void UpdateMouseScrollTarget(const int y)
| |||
{
| |||
bool inTickWindow=IsCursorInTickSubwindow(y);
| |||
| |||
if(inTickWindow!=MouseInTickSubwindow)
| |||
{
| |||
MouseInTickSubwindow=inTickWindow;
| |||
| |||
// Штатное управление терминала остаётся включённым над свечным графиком.
| |||
// Над тиковым подокном оно отключается, чтобы колесо и перетаскивание
| |||
// обслуживались собственным интерактивным графиком.
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)(!inTickWindow));
| |||
}
| |||
}
| |||
| |||
bool IsCursorOnVerticalScaleHandle(const int x,const int y)
| |||
{
| |||
if(!MouseInTickSubwindow || CanvasWidth<=0 || CanvasHeight<=0)
| |||
return false;
| |||
| |||
int chartWidth=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0);
| |||
if(chartWidth<=CanvasWidth)
| |||
return false;
| |||
| |||
// Вертикальный zoom начинается именно в отдельной правой области цен.
| |||
// Ранее зона начиналась ещё на последних 18 px Canvas, из-за чего
| |||
// горизонтальный drag Tick Graph конкурировал с вертикальным drag.
| |||
// Теперь вся правая область является однозначной зоной вертикального
| |||
// масштабирования, а после начала drag масштаб сохраняется независимо
| |||
// от дальнейшего движения курсора по подокну.
| |||
int left=CanvasWidth;
| |||
int right=chartWidth;
| |||
return x>=left && x<right;
| |||
}
| |||
| |||
void BeginVerticalPanDrag(const int y)
| |||
{
| |||
VerticalPanDragActive=true;
| |||
VerticalScaleManual=true;
| |||
VerticalPanDragStartY=y;
| |||
VerticalPanDragStartCenterPrice=(ViewportCenterPrice>0.0 ?
| |||
ViewportCenterPrice :
| |||
ViewportRayPrice(ScaleCenterGrid));
| |||
}
| |||
| |||
void HandleVerticalPanDrag(const int y)
| |||
{
| |||
if(!VerticalPanDragActive || CanvasHeight<=1 ||
| |||
GraphScaleMax<=GraphScaleMin)
| |||
return;
| |||
| |||
int deltaY=y-VerticalPanDragStartY;
| |||
double pricePerPixel=(GraphScaleMax-GraphScaleMin)/
| |||
(double)MathMax(1,CanvasHeight-1);
| |||
// Мышь вверх -> график вверх; мышь вниз -> график вниз.
| |||
ViewportCenterPrice=NormalizeDouble(
| |||
VerticalPanDragStartCenterPrice+(double)deltaY*pricePerPixel,_Digits);
| |||
ScaleCenterGrid=FloorGridForViewport(ViewportCenterPrice);
| |||
VerticalPanDragStartY=y;
| |||
VerticalPanDragStartCenterPrice=ViewportCenterPrice;
| |||
VerticalPanDragActive=true;
| |||
RefreshGraph();
| |||
}
| |||
| |||
void EndVerticalPanDrag()
| |||
{
| |||
VerticalPanDragActive=false;
| |||
}
| |||
| |||
void BeginVerticalScaleDrag(const int y)
| |||
{
| |||
VerticalScaleDragActive=true;
| |||
VerticalScaleDragStartY=y;
| |||
VerticalScaleDragStartHalfRange=MathMax(VerticalScaleHalfRange,GetViewportLevelStepPrice()*0.20);
| |||
VerticalScaleManual=true;
| |||
}
| |||
| |||
void HandleVerticalScaleDrag(const int y)
| |||
{
| |||
if(!VerticalScaleDragActive)
| |||
return;
| |||
| |||
int deltaY=y-VerticalScaleDragStartY;
| |||
| |||
// Вверх = приближение по вертикали, вниз = отдаление.
| |||
// Масштаб рассчитывается относительно исходной точки drag, поэтому
| |||
// несколько mouse-move событий не накапливают округление.
| |||
double totalDeltaY=(double)(y-VerticalScaleDragStartY);
| |||
double factor=MathExp(totalDeltaY*VerticalScaleDragSensitivity);
| |||
double newHalfRange=VerticalScaleDragStartHalfRange*factor;
| |||
| |||
int upCount=MathMax(1,(TickLevelRowCount/2));
| |||
int downCount=MathMax(1,(TickLevelRowCount-(TickLevelRowCount/2)));
| |||
double minimumHalfRange=GetViewportLevelStepPrice()*MathMax(1,MathMax(upCount,downCount));
| |||
minimumHalfRange+=GetViewportLevelStepPrice()*0.10;
| |||
| |||
double maximumHalfRange=MathMax(minimumHalfRange*2.0,GetViewportLevelStepPrice()*2000.0);
| |||
| |||
VerticalScaleHalfRange=MathMax(minimumHalfRange,
| |||
MathMin(maximumHalfRange,newHalfRange));
| |||
| |||
RefreshGraph();
| |||
}
| |||
| |||
void EndVerticalScaleDrag()
| |||
{
| |||
VerticalScaleDragActive=false;
| |||
}
| |||
| |||
void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
| |||
{
| |||
if(id==CHARTEVENT_KEYDOWN && TickEditingRow>=0 && lparam==CODELOCK_KEY_ESCAPE)
| |||
{
| |||
CancelTickLevelEdit();
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_OBJECT_ENDEDIT)
| |||
{
| |||
string editPrefix=Prefix+"LEVEL_EDIT_";
| |||
if(StringFind(sparam,editPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(
| |||
StringSubstr(sparam,StringLen(editPrefix)));
| |||
| |||
string textValue="";
| |||
if(ObjectGetString(0,sparam,OBJPROP_TEXT,0,textValue))
| |||
CommitTickLevelEdit(index,textValue);
| |||
else
| |||
CancelTickLevelEdit();
| |||
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id==CHARTEVENT_OBJECT_CLICK)
| |||
{
| |||
string minusPrefix=Prefix+"LEVEL_MINUS_";
| |||
if(StringFind(sparam,minusPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(
| |||
StringSubstr(sparam,StringLen(minusPrefix)));
| |||
DeleteTickLevelRow(index);
| |||
return;
| |||
}
| |||
| |||
string plusPrefix=Prefix+"LEVEL_PLUS_";
| |||
if(StringFind(sparam,plusPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(
| |||
StringSubstr(sparam,StringLen(plusPrefix)));
| |||
AddTickLevelRow(index);
| |||
return;
| |||
}
| |||
| |||
string alertPrefix=Prefix+"LEVEL_ALERT_";
| |||
if(StringFind(sparam,alertPrefix)==0)
| |||
{
| |||
int index=(int)StringToInteger(
| |||
StringSubstr(sparam,StringLen(alertPrefix)));
| |||
| |||
if(index>=0 && index<TickLevelRowCount)
| |||
{
| |||
TickLevelRows[index].alertEnabled=!TickLevelRows[index].alertEnabled;
| |||
RefreshGraph();
| |||
}
| |||
return;
| |||
}
| |||
| |||
string levelPrefix=Prefix+"LEVEL_";
| |||
if(StringFind(sparam,levelPrefix)==0 &&
| |||
StringFind(sparam,Prefix+"LEVEL_EDIT_")!=0 &&
| |||
StringFind(sparam,Prefix+"LEVEL_DIR_")!=0 &&
| |||
StringFind(sparam,Prefix+"LEVEL_REV_")!=0 &&
| |||
StringFind(sparam,Prefix+"LEVEL_ALERT_")!=0)
| |||
{
| |||
int index=(int)StringToInteger(
| |||
StringSubstr(sparam,StringLen(levelPrefix)));
| |||
| |||
int x=(int)ObjectGetInteger(0,sparam,OBJPROP_XDISTANCE);
| |||
int y=(int)ObjectGetInteger(0,sparam,OBJPROP_YDISTANCE);
| |||
| |||
BeginTickLevelEdit(index,x,y,LevelButtonWidth,LevelButtonHeight);
| |||
return;
| |||
}
| |||
| |||
if(sparam==Prefix+"ZOOM_MINUS" ||
| |||
sparam==Prefix+"ZOOM_PLUS" ||
| |||
sparam==Prefix+"RESET_CURRENT")
| |||
{
| |||
HandleZoomButtonClick(sparam);
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id==CHARTEVENT_MOUSE_WHEEL)
| |||
{
| |||
int flags=(int)(lparam>>32);
| |||
int xCursor=(int)(short)lparam;
| |||
int yCursor=(int)(short)(lparam>>16);
| |||
| |||
UpdateMouseScrollTarget(yCursor);
| |||
| |||
if(!MouseInTickSubwindow)
| |||
return;
| |||
| |||
InitialViewLayoutActive=false;
| |||
| |||
if((flags&0x0004)!=0)
| |||
{
| |||
int span=GetViewCount();
| |||
int step=MathMax(1,span/10);
| |||
int maxOffset=MathMax(0,TickCount-span);
| |||
| |||
ViewEndOffset+=(dparam>0.0 ? -step : step);
| |||
ViewEndOffset=MathMax(0,MathMin(ViewEndOffset,maxOffset));
| |||
}
| |||
else
| |||
{
| |||
HandleHorizontalZoom(xCursor,(int)dparam);
| |||
ViewEndOffset=0;
| |||
}
| |||
| |||
RefreshGraph();
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_MOUSE_MOVE)
| |||
{
| |||
int x=(int)lparam;
| |||
int y=(int)dparam;
| |||
int state=(int)StringToInteger(sparam);
| |||
bool leftDown=((state&1)!=0);
| |||
| |||
UpdateMouseScrollTarget(y);
| |||
| |||
if(!MouseInTickSubwindow)
| |||
DragActive=false;
| |||
| |||
if(VerticalScaleDragActive)
| |||
{
| |||
if(leftDown)
| |||
{
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
| |||
HandleVerticalScaleDrag(y);
| |||
DragActive=false;
| |||
VerticalPanDragActive=false;
| |||
return;
| |||
}
| |||
| |||
EndVerticalScaleDrag();
| |||
UpdateMouseScrollTarget(y);
| |||
}
| |||
else if(VerticalPanDragActive)
| |||
{
| |||
if(leftDown)
| |||
{
| |||
ChartSetInteger(0,CHART_MOUSE_SCROLL,(long)false);
| |||
HandleVerticalPanDrag(y);
| |||
DragActive=false;
| |||
return;
| |||
}
| |||
| |||
EndVerticalPanDrag();
| |||
UpdateMouseScrollTarget(y);
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow &&
| |||
IsCursorOnVerticalScaleHandle(x,y))
| |||
{
| |||
BeginVerticalScaleDrag(y);
| |||
HandleVerticalScaleDrag(y);
| |||
DragActive=false;
| |||
VerticalPanDragActive=false;
| |||
return;
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow &&
| |||
IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
| |||
{
| |||
ResetButtonHeld=true;
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,true);
| |||
}
| |||
else if(!leftDown)
| |||
{
| |||
ResetButtonHeld=false;
| |||
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
| |||
}
| |||
else if(!IsPointInsideObject(Prefix+"RESET_CURRENT",x,y))
| |||
{
| |||
ResetButtonHeld=false;
| |||
if(ObjectFind(0,Prefix+"RESET_CURRENT")>=0)
| |||
ObjectSetInteger(0,Prefix+"RESET_CURRENT",OBJPROP_STATE,false);
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow && !ResetButtonHeld)
| |||
{
| |||
if(!DragActive && !VerticalPanDragActive)
| |||
{
| |||
if(!PointerDragTracking)
| |||
{
| |||
PointerDragTracking=true;
| |||
DragStartX=x;
| |||
DragStartOffset=ViewEndOffset;
| |||
VerticalPanDragStartY=y;
| |||
}
| |||
| |||
int dx=x-DragStartX;
| |||
int dy=y-VerticalPanDragStartY;
| |||
| |||
if(MathAbs((double)dy)>MathAbs((double)dx) && MathAbs(dy)>=4)
| |||
{
| |||
BeginVerticalPanDrag(y);
| |||
DragActive=false;
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(MathAbs(dx)>=4)
| |||
DragActive=true;
| |||
}
| |||
| |||
if(VerticalPanDragActive)
| |||
{
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(DragActive)
| |||
{
| |||
HandleHorizontalDrag(x);
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
else if(!leftDown || !MouseInTickSubwindow)
| |||
{
| |||
DragActive=false;
| |||
PointerDragTracking=false;
| |||
EndVerticalPanDrag();
| |||
}
| |||
| |||
return;
| |||
}
| |||
| |||
if(id==CHARTEVENT_CHART_CHANGE)
| |||
RefreshGraph();
| |||
}
| |||
| |||
int OnCalculate(const int rates_total,const int prev_calculated,const datetime &time[],
| |||
const double &open[],const double &high[],const double &low[],
| |||
const double &close[],const long &tick_volume[],const long &volume[],
| |||
const int &spread[])
| |||
{
| |||
SubWindow=ChartWindowFind(0,ShortName);
| |||
| |||
if(SubWindow<0)
| |||
return rates_total;
| |||
| |||
if(!UpdateMarketState())
| |||
return rates_total;
| |||
| |||
MqlTick current;
| |||
| |||
if(SymbolInfoTick(_Symbol,current) && current.bid>0.0)
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
}
| |||
| |||
ChartRedraw(0);
| |||
| |||
return rates_total;
| |||
}
|