3371 lines
106 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| CodeLock.mq5 |
| |||
//| CodeLock — модель уровней + Dashboard + TickGraphic. |
| |||
//+------------------------------------------------------------------+
| |||
#property strict
| |||
#property indicator_separate_window
| |||
#property indicator_plots 0
| |||
#property version "4.02"
| |||
#property description "CodeLock: independent LEVEL grids, DIRECTION, REVERSAL, ALERT"
| |||
| |||
#include <Canvas\Canvas.mqh>
| |||
| |||
//============================================================================
| |||
// SECTION 1: DEFINES
| |||
//============================================================================
| |||
#define INVALID_GRID (-2147483647)
| |||
#define KEY_ESCAPE_CODE 27
| |||
| |||
#define MIN_VISUAL_ROWS 5
| |||
#define MAX_VISUAL_ROWS 100
| |||
#define VISUAL_LEVEL_MIN 10
| |||
#define VISUAL_LEVEL_MAX 2000
| |||
#define VISUAL_LEVEL_STEP 10
| |||
| |||
#define DEFAULT_VISUAL_ROWS "5,10,20,30,40,50"
| |||
| |||
#define PANEL_PREFIX "TPDR_"
| |||
#define DASH_PREFIX "TPDRD_"
| |||
| |||
#define STORAGE_PREFIX "CodeLock_LEVELS_"
| |||
#define STORAGE_DASH_PREFIX "CodeLock_DASH_"
| |||
| |||
//============================================================================
| |||
// SECTION 2: STRUCTURES
| |||
//============================================================================
| |||
struct TickLevelRow
| |||
{
| |||
int level;
| |||
int direction;
| |||
int directionGrid;
| |||
int lastCrossedGrid;
| |||
int lastCrossingDirection;
| |||
int consecutiveCrossings;
| |||
double lastReversalPrice;
| |||
double reversalAtPrice;
| |||
double lastDown1Price;
| |||
double lastDown2Price;
| |||
double lastUp1Price;
| |||
double lastUp2Price;
| |||
double phaseMinBid;
| |||
double phaseMaxAsk;
| |||
bool alertEnabled;
| |||
};
| |||
| |||
struct SignalMark
| |||
{
| |||
long serial;
| |||
double price;
| |||
int level;
| |||
bool up;
| |||
bool reversal;
| |||
};
| |||
| |||
struct DashVisualRow
| |||
{
| |||
int level;
| |||
};
| |||
| |||
//============================================================================
| |||
// SECTION 3: INPUTS
| |||
//============================================================================
| |||
input double StartPrice = 1.0;
| |||
input int BufferPoints = 0;
| |||
input string InitialVisualRows = "5,10,20,30,40,50";
| |||
input int MaxModelHistoryTicks = 2000000;
| |||
input int HistoryGraphTicks = 200000;
| |||
input int HistoryBootstrapTicks = 500000;
| |||
input int HistoryChunkSize = 5000;
| |||
| |||
input int InpGraphLevelsPerSide = 10;
| |||
input int InpGraphDisplayedLevelCount = 5;
| |||
| |||
int GraphLevelsPerSide = 10;
| |||
int GraphDisplayedLevelCount = 5;
| |||
| |||
input bool DrawReversalArrows = true;
| |||
input int ReversalArrowsHistory = 20;
| |||
input bool DrawRayObjects = true;
| |||
input int InitialViewTicks = 50000;
| |||
input int MinViewTicks = 5;
| |||
input int PriceScaleWidth = 85;
| |||
| |||
input bool EnableAlert = true;
| |||
input bool EnablePushNotification = false;
| |||
input bool EnableEmailNotification = false;
| |||
input bool EnableSoundNotification = true;
| |||
input string NotificationSound = "alert.wav";
| |||
| |||
input int PanelLeftMargin = 12;
| |||
input int PanelTopMargin = 12;
| |||
input int RowHeight = 25;
| |||
input int HeaderHeight = 72;
| |||
input int DashboardFontSize = 11;
| |||
input int UpdateIntervalMs = 50;
| |||
input int AlertActivityMs = 500;
| |||
| |||
input color TickGraphColor = clrDodgerBlue;
| |||
input color RayColor = clrSilver;
| |||
input color CurrentRayColor = clrGold;
| |||
input color UpCrossColor = clrGreen;
| |||
input color DownCrossColor = clrRed;
| |||
input color BuyColor = clrBlue;
| |||
input color SellColor = clrRed;
| |||
| |||
input color PanelBackground = C'16,20,28';
| |||
input color RowBackground = C'29,35,46';
| |||
input color RowBackgroundAlt = C'24,30,40';
| |||
input color BorderColor = C'75,88,108';
| |||
input color HeaderColor = clrDeepSkyBlue;
| |||
input color TextColor = clrWhiteSmoke;
| |||
input color MutedTextColor = C'155,165,180';
| |||
input color UpColor = clrLimeGreen;
| |||
input color DownColor = clrTomato;
| |||
input color WaitColor = clrSilver;
| |||
input color AlertOnColor = clrOrange;
| |||
input color AlertOffColor = C'55,62,74';
| |||
input color AlertActivityColor = clrLimeGreen;
| |||
input color AlertTextColor = clrWhite;
| |||
| |||
//============================================================================
| |||
// SECTION 4: GLOBAL VARIABLES
| |||
//============================================================================
| |||
TickLevelRow TickLevelRows[];
| |||
int TickLevelRowCount = 0;
| |||
| |||
DashVisualRow DashVisualRows[MAX_VISUAL_ROWS];
| |||
int DashVisualRowCount = 0;
| |||
bool DashAlertEnabled[MAX_VISUAL_ROWS];
| |||
ulong DashAlertActivityUntil[MAX_VISUAL_ROWS];
| |||
| |||
double TickPrices[];
| |||
int TickCount = 0;
| |||
int TickHead = 0;
| |||
long TickSerial = 0;
| |||
| |||
double ModelTickPrices[];
| |||
double ModelTickAsks[];
| |||
int ModelTickCount = 0;
| |||
int ModelTickHead = 0;
| |||
| |||
long AttachMsc = 0;
| |||
long CursorMsc = 1;
| |||
int CursorSameMsc = 0;
| |||
int BootstrapAttempts = 0;
| |||
int HistoryBootstrapTargetTicks = 0;
| |||
| |||
bool BootstrapComplete = false;
| |||
bool LiveMode = false;
| |||
bool HavePreviousBid = false;
| |||
double PreviousBid = 0.0;
| |||
int SubWindow = -1;
| |||
| |||
SignalMark SignalMarks[];
| |||
long RebuildArrowSerialOverride = -1;
| |||
| |||
CCanvas TickCanvas;
| |||
bool CanvasReady = false;
| |||
int CanvasWidth = 0;
| |||
int CanvasHeight = 0;
| |||
int CanvasSubWindow = -1;
| |||
| |||
double GraphScaleMin = 0.0;
| |||
double GraphScaleMax = 0.0;
| |||
double ViewportCenterPrice = 0.0;
| |||
int ScaleCenterGrid = 0;
| |||
bool ScaleInitialized = false;
| |||
bool AutoScaleInitialized = false;
| |||
| |||
double VerticalScaleHalfRange = 0.0;
| |||
bool VerticalScaleManual = false;
| |||
bool VerticalScaleDragActive = false;
| |||
bool VerticalPanDragActive = false;
| |||
bool PointerDragTracking = false;
| |||
int VerticalPanDragStartY = 0;
| |||
double VerticalPanDragStartCenterPrice = 0.0;
| |||
int VerticalScaleDragStartY = 0;
| |||
double VerticalScaleDragStartHalfRange = 0.0;
| |||
double VerticalScaleDragSensitivity = 0.008;
| |||
| |||
bool InitialArrowHistoryScalePending = false;
| |||
| |||
int ViewSpan = 0;
| |||
int ViewEndOffset = 0;
| |||
bool DragActive = false;
| |||
int DragStartX = 0;
| |||
int DragStartOffset = 0;
| |||
bool MouseInTickSubwindow = false;
| |||
bool InitialViewLayoutActive = false;
| |||
bool PreviousChartMouseScroll = true;
| |||
| |||
bool ResetButtonHeld = false;
| |||
| |||
const int ZoomButtonWidth = 36;
| |||
const int ZoomButtonHeight = 28;
| |||
const int ZoomButtonGap = 6;
| |||
const int ResetButtonWidth = 64;
| |||
const int LevelButtonWidth = 32;
| |||
const int LevelButtonHeight = 22;
| |||
const int LevelButtonGap = 3;
| |||
| |||
int DashEditingRow = -1;
| |||
| |||
string DashRowsStateKey = "";
| |||
| |||
//============================================================================
| |||
// SECTION 4.5: FORWARD DECLARATIONS
| |||
//============================================================================
| |||
void AppendModelTick(const double bid, const double ask);
| |||
double GetModelTickByIndex(const int index);
| |||
double GetModelAskByIndex(const int index);
| |||
void AddTickToGraph(const double bid);
| |||
double GetTickPriceByVisibleIndex(const int index);
| |||
int ProcessTickRowForRebuild(const int rowIndex, const double oldBid,
| |||
const double bid, const MqlTick &tick);
| |||
void FitInitialArrowHistory();
| |||
void RefreshGraph();
| |||
void UpdateSubwindowScale(const double bid);
| |||
void RenderGraph(const double bid);
| |||
bool EnsureCanvas();
| |||
int GetViewStartIndex();
| |||
int GetViewCount();
| |||
void DashRenderDashboard(const double bid);
| |||
void SyncTickVisualRowsFromDashboard();
| |||
void HandleHorizontalZoom(const int xCursor, const int delta);
| |||
void HandleHorizontalDrag(const int x);
| |||
void BeginVerticalPanDrag(const int y);
| |||
void HandleVerticalPanDrag(const int y);
| |||
void EndVerticalPanDrag();
| |||
void BeginVerticalScaleDrag(const int y);
| |||
void HandleVerticalScaleDrag(const int y);
| |||
void EndVerticalScaleDrag();
| |||
void HandleZoomButtonClick(const string name);
| |||
void ResetGraphToCurrentPrice();
| |||
bool IsPointInsideObject(const string name, const int x, const int y);
| |||
void UpdateMouseScrollTarget(const int y);
| |||
void ResetAllTickLevelRowStates();
| |||
void ResetTickLevelRowState(TickLevelRow &row);
| |||
| |||
//============================================================================
| |||
// SECTION 5: MODEL
| |||
//============================================================================
| |||
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);
| |||
}
| |||
| |||
bool IsValidVisualLevel(const int level)
| |||
{
| |||
return (level == 5) ||
| |||
(level >= VISUAL_LEVEL_MIN &&
| |||
level <= VISUAL_LEVEL_MAX &&
| |||
(level % VISUAL_LEVEL_STEP) == 0);
| |||
}
| |||
| |||
bool IsStrictVisualRows()
| |||
{
| |||
if(TickLevelRowCount < MIN_VISUAL_ROWS || TickLevelRowCount > MAX_VISUAL_ROWS)
| |||
return false;
| |||
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
{
| |||
if(!IsValidVisualLevel(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.lastCrossedGrid = INVALID_GRID;
| |||
row.lastCrossingDirection = 0;
| |||
row.consecutiveCrossings = 0;
| |||
row.lastReversalPrice = 0.0;
| |||
row.reversalAtPrice = 0.0;
| |||
row.lastDown1Price = 0.0;
| |||
row.lastDown2Price = 0.0;
| |||
row.lastUp1Price = 0.0;
| |||
row.lastUp2Price = 0.0;
| |||
row.phaseMinBid = 0.0;
| |||
row.phaseMaxAsk = 0.0;
| |||
row.alertEnabled = false;
| |||
}
| |||
| |||
void ResetAllTickLevelRowStates()
| |||
{
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
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;
| |||
}
| |||
| |||
int reversalGrid = TickLevelRows[rowIndex].directionGrid -
| |||
TickLevelRows[rowIndex].direction;
| |||
TickLevelRows[rowIndex].reversalAtPrice = TickRowPrice(rowIndex, reversalGrid);
| |||
}
| |||
| |||
bool RegisterTickRowCrossing(const int rowIndex,
| |||
const int grid,
| |||
const int crossingDirection,
| |||
const MqlTick &tick,
| |||
const bool emitSignals)
| |||
{
| |||
if(rowIndex < 0 || rowIndex >= TickLevelRowCount) return false;
| |||
| |||
TickLevelRows[rowIndex].lastCrossedGrid = grid;
| |||
TickLevelRows[rowIndex].lastCrossingDirection = crossingDirection;
| |||
| |||
if(TickLevelRows[rowIndex].direction == 0)
| |||
{
| |||
TickLevelRows[rowIndex].direction = crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid = grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings = 1;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
| |||
if(crossingDirection < 0)
| |||
TickLevelRows[rowIndex].phaseMinBid = tick.bid;
| |||
else if(tick.ask > 0.0)
| |||
TickLevelRows[rowIndex].phaseMaxAsk = tick.ask;
| |||
| |||
return false;
| |||
}
| |||
| |||
if(crossingDirection == TickLevelRows[rowIndex].direction)
| |||
{
| |||
TickLevelRows[rowIndex].directionGrid = grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings++;
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
return false;
| |||
}
| |||
| |||
int reversalGrid = TickLevelRows[rowIndex].directionGrid -
| |||
TickLevelRows[rowIndex].direction;
| |||
if(grid != reversalGrid) return false;
| |||
| |||
int oldDirection = TickLevelRows[rowIndex].direction;
| |||
| |||
TickLevelRows[rowIndex].direction = crossingDirection;
| |||
TickLevelRows[rowIndex].directionGrid = grid;
| |||
TickLevelRows[rowIndex].consecutiveCrossings = 1;
| |||
TickLevelRows[rowIndex].lastReversalPrice = TickRowPrice(rowIndex, grid);
| |||
UpdateTickRowReversalAt(rowIndex);
| |||
| |||
if(crossingDirection < 0)
| |||
{
| |||
TickLevelRows[rowIndex].lastDown2Price = TickLevelRows[rowIndex].lastDown1Price;
| |||
TickLevelRows[rowIndex].lastDown1Price = TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
else
| |||
{
| |||
TickLevelRows[rowIndex].lastUp2Price = TickLevelRows[rowIndex].lastUp1Price;
| |||
TickLevelRows[rowIndex].lastUp1Price = TickLevelRows[rowIndex].lastReversalPrice;
| |||
}
| |||
| |||
if(oldDirection > 0)
| |||
TickLevelRows[rowIndex].phaseMaxAsk = 0.0;
| |||
else
| |||
TickLevelRows[rowIndex].phaseMinBid = 0.0;
| |||
| |||
//--- ALERT + уведомления
| |||
if(emitSignals && TickLevelRows[rowIndex].alertEnabled)
| |||
{
| |||
string dir = (crossingDirection > 0) ? "UP" : "DOWN";
| |||
string msg = StringFormat("[CodeLock] REVERSAL %s @ %s (%d pt)",
| |||
dir,
| |||
DoubleToString(TickLevelRows[rowIndex].lastReversalPrice, _Digits),
| |||
TickLevelRows[rowIndex].level);
| |||
| |||
if(EnableAlert) Alert(msg);
| |||
if(EnablePushNotification) SendNotification(msg);
| |||
if(EnableEmailNotification) SendMail("CodeLock Alert", msg);
| |||
if(EnableSoundNotification && NotificationSound != "")
| |||
PlaySound(NotificationSound);
| |||
| |||
for(int d = 0; d < DashVisualRowCount; d++)
| |||
{
| |||
if(DashVisualRows[d].level == TickLevelRows[rowIndex].level)
| |||
{
| |||
DashAlertActivityUntil[d] = (ulong)GetTickCount() + (ulong)AlertActivityMs;
| |||
break;
| |||
}
| |||
}
| |||
}
| |||
| |||
return true;
| |||
}
| |||
| |||
void ProcessTickRow(const int rowIndex,
| |||
const double oldBid,
| |||
const double bid,
| |||
const MqlTick &tick,
| |||
const bool emitSignals)
| |||
{
| |||
if(rowIndex < 0 || rowIndex >= TickLevelRowCount) return;
| |||
if(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 confirmPrice = NormalizeDouble(
| |||
RayPriceForStep(grid, step) + buffer, _Digits);
| |||
| |||
if(confirmPrice > oldBid && confirmPrice <= bid)
| |||
RegisterTickRowCrossing(rowIndex, grid, +1, tick, emitSignals);
| |||
}
| |||
}
| |||
else
| |||
{
| |||
int first = CeilGridForStep(oldBid + buffer, step) + 1;
| |||
int last = CeilGridForStep(bid + buffer, step) - 1;
| |||
| |||
for(int grid = first; grid >= last; grid--)
| |||
{
| |||
double confirmPrice = NormalizeDouble(
| |||
RayPriceForStep(grid, step) - buffer, _Digits);
| |||
| |||
if(confirmPrice < oldBid && confirmPrice >= bid)
| |||
RegisterTickRowCrossing(rowIndex, grid, -1, tick, emitSignals);
| |||
}
| |||
}
| |||
}
| |||
| |||
void UpdateTickRowPhaseExtremes(const MqlTick &tick)
| |||
{
| |||
if(tick.bid <= 0.0) return;
| |||
| |||
for(int row = 0; row < TickLevelRowCount; row++)
| |||
{
| |||
if(TickLevelRows[row].direction > 0 && tick.ask > 0.0)
| |||
{
| |||
if(TickLevelRows[row].phaseMaxAsk <= 0.0 ||
| |||
tick.ask > TickLevelRows[row].phaseMaxAsk)
| |||
TickLevelRows[row].phaseMaxAsk = tick.ask;
| |||
}
| |||
else if(TickLevelRows[row].direction < 0)
| |||
{
| |||
if(TickLevelRows[row].phaseMinBid <= 0.0 ||
| |||
tick.bid < TickLevelRows[row].phaseMinBid)
| |||
TickLevelRows[row].phaseMinBid = tick.bid;
| |||
}
| |||
}
| |||
}
| |||
| |||
void ProcessBidTick(const MqlTick &tick,
| |||
const bool emitSignals,
| |||
const bool drawGraph,
| |||
const bool storeHistory)
| |||
{
| |||
if(tick.bid <= 0.0) return;
| |||
| |||
double bid = NormalizeDouble(tick.bid, _Digits);
| |||
| |||
if(storeHistory)
| |||
AppendModelTick(bid, tick.ask);
| |||
| |||
TickSerial++;
| |||
| |||
if(!HavePreviousBid)
| |||
{
| |||
PreviousBid = bid;
| |||
HavePreviousBid = true;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
UpdateTickRowPhaseExtremes(tick);
| |||
return;
| |||
}
| |||
| |||
double oldBid = PreviousBid;
| |||
| |||
if(drawGraph)
| |||
AddTickToGraph(bid);
| |||
| |||
if(bid != oldBid)
| |||
{
| |||
for(int row = 0; row < TickLevelRowCount; row++)
| |||
ProcessTickRow(row, oldBid, bid, tick, emitSignals);
| |||
| |||
PreviousBid = bid;
| |||
}
| |||
| |||
UpdateTickRowPhaseExtremes(tick);
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 5.1: MODEL — tick arrays
| |||
//============================================================================
| |||
void AppendModelTick(const double bid, const double ask)
| |||
{
| |||
if(bid <= 0.0 || MaxModelHistoryTicks < 1) return;
| |||
| |||
if(ArraySize(ModelTickPrices) < MaxModelHistoryTicks)
| |||
ArrayResize(ModelTickPrices, MaxModelHistoryTicks, MaxModelHistoryTicks);
| |||
if(ArraySize(ModelTickAsks) < MaxModelHistoryTicks)
| |||
ArrayResize(ModelTickAsks, MaxModelHistoryTicks, MaxModelHistoryTicks);
| |||
| |||
if(ModelTickCount < MaxModelHistoryTicks)
| |||
{
| |||
int idx = (ModelTickHead + ModelTickCount) % MaxModelHistoryTicks;
| |||
ModelTickPrices[idx] = bid;
| |||
ModelTickAsks[idx] = (ask > 0.0 ? ask : 0.0);
| |||
ModelTickCount++;
| |||
}
| |||
else
| |||
{
| |||
ModelTickPrices[ModelTickHead] = bid;
| |||
ModelTickAsks[ModelTickHead] = (ask > 0.0 ? ask : 0.0);
| |||
ModelTickHead = (ModelTickHead + 1) % MaxModelHistoryTicks;
| |||
}
| |||
}
| |||
| |||
double GetModelTickByIndex(const int index)
| |||
{
| |||
if(index < 0 || index >= ModelTickCount) return 0.0;
| |||
return ModelTickPrices[(ModelTickHead + index) % MaxModelHistoryTicks];
| |||
}
| |||
| |||
double GetModelAskByIndex(const int index)
| |||
{
| |||
if(index < 0 || index >= ModelTickCount) return 0.0;
| |||
return ModelTickAsks[(ModelTickHead + index) % MaxModelHistoryTicks];
| |||
}
| |||
| |||
void AddTickToGraph(const double bid)
| |||
{
| |||
if(bid <= 0.0 || HistoryGraphTicks < 1) return;
| |||
| |||
if(ArraySize(TickPrices) < HistoryGraphTicks)
| |||
ArrayResize(TickPrices, HistoryGraphTicks, HistoryGraphTicks);
| |||
| |||
if(TickCount < HistoryGraphTicks)
| |||
{
| |||
int idx = (TickHead + TickCount) % HistoryGraphTicks;
| |||
TickPrices[idx] = bid;
| |||
TickCount++;
| |||
}
| |||
else
| |||
{
| |||
TickPrices[TickHead] = bid;
| |||
TickHead = (TickHead + 1) % HistoryGraphTicks;
| |||
}
| |||
}
| |||
| |||
double GetTickPriceByVisibleIndex(const int index)
| |||
{
| |||
if(index < 0 || index >= TickCount) return 0.0;
| |||
int physical = (TickHead + index) % HistoryGraphTicks;
| |||
return TickPrices[physical];
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 6: HISTORY
| |||
//============================================================================
| |||
void AddReversalSignalMark(const double price, const bool up, const int level)
| |||
{
| |||
if(!DrawReversalArrows || ReversalArrowsHistory <= 0) return;
| |||
if(price <= 0.0 || level <= 0) return;
| |||
| |||
int size = ArraySize(SignalMarks);
| |||
ArrayResize(SignalMarks, size + 1);
| |||
| |||
long serial = (RebuildArrowSerialOverride >= 0)
| |||
? RebuildArrowSerialOverride
| |||
: TickSerial;
| |||
| |||
SignalMarks[size].serial = serial;
| |||
SignalMarks[size].price = NormalizeDouble(price, _Digits);
| |||
SignalMarks[size].level = level;
| |||
SignalMarks[size].up = up;
| |||
SignalMarks[size].reversal = true;
| |||
| |||
int keepCount = MathMin(20, MathMax(1, ReversalArrowsHistory));
| |||
int count = 0;
| |||
for(int i = 0; i < ArraySize(SignalMarks); i++)
| |||
if(SignalMarks[i].reversal &&
| |||
SignalMarks[i].up == up &&
| |||
SignalMarks[i].level == level)
| |||
count++;
| |||
| |||
if(count <= keepCount) return;
| |||
| |||
for(int i = 0; i < ArraySize(SignalMarks); i++)
| |||
{
| |||
if(!SignalMarks[i].reversal ||
| |||
SignalMarks[i].up != up ||
| |||
SignalMarks[i].level != level)
| |||
continue;
| |||
| |||
for(int j = i; j < ArraySize(SignalMarks) - 1; j++)
| |||
SignalMarks[j] = SignalMarks[j + 1];
| |||
| |||
ArrayResize(SignalMarks, ArraySize(SignalMarks) - 1);
| |||
break;
| |||
}
| |||
}
| |||
| |||
void RecordReversalArrow(const double price, const bool buy, const int level)
| |||
{
| |||
AddReversalSignalMark(price, buy, level);
| |||
}
| |||
| |||
void FinalizeTickRowsFromHistory()
| |||
{
| |||
if(TickLevelRowCount <= 0 || ModelTickCount < 2) return;
| |||
| |||
double lastBid = GetModelTickByIndex(ModelTickCount - 1);
| |||
double previousBid = GetModelTickByIndex(ModelTickCount - 2);
| |||
if(lastBid <= 0.0 || previousBid <= 0.0 || lastBid == previousBid) return;
| |||
| |||
int fallbackDirection = (lastBid > previousBid) ? +1 : -1;
| |||
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
{
| |||
if(TickLevelRows[i].direction != 0) continue;
| |||
| |||
double step = (double)TickLevelRows[i].level * _Point;
| |||
if(step <= 0.0) continue;
| |||
| |||
int grid = (fallbackDirection > 0)
| |||
? CeilGridForStep(lastBid, step)
| |||
: FloorGridForStep(lastBid, step);
| |||
if(grid == INVALID_GRID) continue;
| |||
| |||
TickLevelRows[i].direction = fallbackDirection;
| |||
TickLevelRows[i].directionGrid = grid;
| |||
TickLevelRows[i].lastCrossedGrid = grid;
| |||
TickLevelRows[i].lastCrossingDirection = fallbackDirection;
| |||
TickLevelRows[i].consecutiveCrossings = 1;
| |||
UpdateTickRowReversalAt(i);
| |||
}
| |||
}
| |||
| |||
bool HistoryDerivedStateComplete()
| |||
{
| |||
if(TickLevelRowCount <= 0 || ModelTickCount < 2) return false;
| |||
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
{
| |||
if(TickLevelRows[i].direction == 0) return false;
| |||
if(TickLevelRows[i].reversalAtPrice <= 0.0) return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
void ResetSelectedCalculation()
| |||
{
| |||
PreviousBid = 0.0;
| |||
HavePreviousBid = false;
| |||
}
| |||
| |||
void ClearDerivedState()
| |||
{
| |||
TickCount = 0;
| |||
TickHead = 0;
| |||
TickSerial = 0;
| |||
| |||
ArrayResize(TickPrices, MathMax(100, HistoryGraphTicks),
| |||
MathMax(100, HistoryGraphTicks));
| |||
ArrayResize(SignalMarks, 0);
| |||
| |||
ResetSelectedCalculation();
| |||
ResetAllTickLevelRowStates();
| |||
}
| |||
| |||
void UpdateCursorFromArray(const MqlTick &ticks[], const int copied, const int startIndex)
| |||
{
| |||
if(copied <= 0) return;
| |||
| |||
long finalMsc = ticks[copied - 1].time_msc;
| |||
| |||
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;
| |||
}
| |||
}
| |||
| |||
int ProcessTickRowForRebuild(const int rowIndex,
| |||
const double oldBid,
| |||
const double bid,
| |||
const MqlTick &tick)
| |||
{
| |||
if(rowIndex < 0 || rowIndex >= TickLevelRowCount) return 0;
| |||
if(bid == oldBid) return 0;
| |||
| |||
double step = (double)TickLevelRows[rowIndex].level * _Point;
| |||
if(step <= 0.0) return 0;
| |||
| |||
double buffer = (double)MathMax(0, BufferPoints) * _Point;
| |||
int reversals = 0;
| |||
| |||
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 confirmPrice = NormalizeDouble(
| |||
RayPriceForStep(grid, step) + buffer, _Digits);
| |||
| |||
if(confirmPrice > oldBid && confirmPrice <= bid)
| |||
{
| |||
if(RegisterTickRowCrossing(rowIndex, grid, +1, tick, false))
| |||
{
| |||
AddReversalSignalMark(TickLevelRows[rowIndex].lastReversalPrice,
| |||
true, TickLevelRows[rowIndex].level);
| |||
reversals++;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
else
| |||
{
| |||
int first = CeilGridForStep(oldBid + buffer, step) + 1;
| |||
int last = CeilGridForStep(bid + buffer, step) - 1;
| |||
| |||
for(int grid = first; grid >= last; grid--)
| |||
{
| |||
double confirmPrice = NormalizeDouble(
| |||
RayPriceForStep(grid, step) - buffer, _Digits);
| |||
| |||
if(confirmPrice < oldBid && confirmPrice >= bid)
| |||
{
| |||
if(RegisterTickRowCrossing(rowIndex, grid, -1, tick, false))
| |||
{
| |||
AddReversalSignalMark(TickLevelRows[rowIndex].lastReversalPrice,
| |||
false, TickLevelRows[rowIndex].level);
| |||
reversals++;
| |||
}
| |||
}
| |||
}
| |||
}
| |||
| |||
return reversals;
| |||
}
| |||
| |||
bool BootstrapHistory()
| |||
{
| |||
int minimumTarget = MathMax(HistoryGraphTicks, HistoryBootstrapTicks);
| |||
minimumTarget = MathMax(minimumTarget, 100);
| |||
int maximumTarget = MathMax(minimumTarget, MathMax(100, MaxModelHistoryTicks));
| |||
| |||
if(HistoryBootstrapTargetTicks <= 0)
| |||
HistoryBootstrapTargetTicks = MathMin(minimumTarget, maximumTarget);
| |||
else
| |||
HistoryBootstrapTargetTicks = MathMax(minimumTarget,
| |||
MathMin(HistoryBootstrapTargetTicks, maximumTarget));
| |||
| |||
int targetTicks = HistoryBootstrapTargetTicks;
| |||
MqlTick ticks[];
| |||
ArrayResize(ticks, targetTicks);
| |||
| |||
int copied = -1;
| |||
int lastError = 0;
| |||
| |||
for(int attempt = 0; attempt < 3; attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied = CopyTicks(_Symbol, ticks, COPY_TICKS_INFO, 0, (uint)targetTicks);
| |||
lastError = GetLastError();
| |||
| |||
if(copied < 0)
| |||
{
| |||
if(lastError == 4004)
| |||
{
| |||
targetTicks = MathMax(100, targetTicks / 2);
| |||
ArrayResize(ticks, targetTicks);
| |||
continue;
| |||
}
| |||
| |||
if(lastError == 4401 || lastError == 4403)
| |||
{
| |||
BootstrapAttempts++;
| |||
if(BootstrapAttempts < 50) return false;
| |||
Print("[CodeLock] Bootstrap: tick history sync timeout. Error=", lastError);
| |||
return false;
| |||
}
| |||
| |||
Print("[CodeLock] 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++;
| |||
if(BootstrapAttempts >= 3)
| |||
{
| |||
Print("[CodeLock] Bootstrap: no ticks up to attach boundary. Using available history.");
| |||
BootstrapAttempts = 0;
| |||
return false;
| |||
}
| |||
return false;
| |||
}
| |||
| |||
ArrayResize(ModelTickPrices, MathMax(100, MaxModelHistoryTicks),
| |||
MathMax(100, MaxModelHistoryTicks));
| |||
ArrayResize(ModelTickAsks, MathMax(100, MaxModelHistoryTicks),
| |||
MathMax(100, MaxModelHistoryTicks));
| |||
| |||
ModelTickCount = 0;
| |||
ModelTickHead = 0;
| |||
ClearDerivedState();
| |||
| |||
for(int i = 0; i <= lastAttach; i++)
| |||
{
| |||
if(ticks[i].bid > 0.0)
| |||
ProcessBidTick(ticks[i], false, true, true);
| |||
}
| |||
| |||
int attachCount = 0;
| |||
for(int i = lastAttach; i >= 0 && ticks[i].time_msc == AttachMsc; i--)
| |||
attachCount++;
| |||
| |||
CursorMsc = AttachMsc;
| |||
CursorSameMsc = attachCount;
| |||
| |||
FinalizeTickRowsFromHistory();
| |||
| |||
if(copied < targetTicks && targetTicks < maximumTarget && BootstrapAttempts < 3)
| |||
{
| |||
HistoryBootstrapTargetTicks = MathMin(maximumTarget,
| |||
MathMax(targetTicks + 1, targetTicks * 2));
| |||
BootstrapAttempts++;
| |||
BootstrapComplete = false;
| |||
LiveMode = false;
| |||
Print("[CodeLock] Tick history partial. Requested=", targetTicks,
| |||
" Copied=", copied, " Attempt=", BootstrapAttempts,
| |||
" Next=", HistoryBootstrapTargetTicks);
| |||
return false;
| |||
}
| |||
| |||
bool derivedComplete = HistoryDerivedStateComplete();
| |||
if(!derivedComplete && targetTicks < maximumTarget && BootstrapAttempts < 3)
| |||
{
| |||
HistoryBootstrapTargetTicks = MathMin(maximumTarget,
| |||
MathMax(targetTicks + 1, targetTicks * 2));
| |||
BootstrapAttempts++;
| |||
BootstrapComplete = false;
| |||
LiveMode = false;
| |||
Print("[CodeLock] Expanding history for LAST values. Attempt=", BootstrapAttempts,
| |||
" Next=", HistoryBootstrapTargetTicks);
| |||
return false;
| |||
}
| |||
| |||
BootstrapAttempts = 0;
| |||
BootstrapComplete = true;
| |||
LiveMode = true;
| |||
| |||
ViewSpan = MathMin(MathMax(InitialViewTicks, MinViewTicks),
| |||
MathMax(1, TickCount));
| |||
ViewEndOffset = 0;
| |||
| |||
//--- Пересборка стрелок
| |||
if(ModelTickCount >= 2)
| |||
{
| |||
ArrayResize(SignalMarks, 0);
| |||
RebuildArrowSerialOverride = 0;
| |||
| |||
long savedSerial = TickSerial;
| |||
| |||
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.ask = GetModelAskByIndex(i);
| |||
syntheticTick.time_msc = 0;
| |||
| |||
RebuildArrowSerialOverride = savedSerial - ModelTickCount + i + 1;
| |||
| |||
for(int row = 0; row < TickLevelRowCount; row++)
| |||
ProcessTickRowForRebuild(row, oldBid, bid, syntheticTick);
| |||
}
| |||
| |||
RebuildArrowSerialOverride = -1;
| |||
TickSerial = savedSerial;
| |||
}
| |||
| |||
FitInitialArrowHistory();
| |||
InitialArrowHistoryScalePending = true;
| |||
| |||
Print("[CodeLock] Bootstrap complete. ModelTicks=", ModelTickCount,
| |||
" LevelRows=", TickLevelRowCount, " Arrows=", ArraySize(SignalMarks));
| |||
| |||
return true;
| |||
}
| |||
| |||
bool ProcessLiveTicks()
| |||
{
| |||
MqlTick ticks[5000];
| |||
int requestCount = MathMin(MathMax(HistoryChunkSize, 100), 5000);
| |||
| |||
for(int chunk = 0; chunk < 20; chunk++)
| |||
{
| |||
int copied = -1;
| |||
int requestTried = requestCount;
| |||
int lastError = 0;
| |||
| |||
for(int attempt = 0; attempt < 3; attempt++)
| |||
{
| |||
ResetLastError();
| |||
copied = CopyTicks(_Symbol, ticks, COPY_TICKS_INFO,
| |||
(ulong)MathMax(1, CursorMsc), (uint)requestTried);
| |||
lastError = GetLastError();
| |||
| |||
if(copied >= 0) break;
| |||
| |||
if(lastError != 4004)
| |||
{
| |||
Print("[CodeLock] Live CopyTicks failed. Error=", lastError);
| |||
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;
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 7A: RENDER — Canvas + viewport + scale
| |||
//============================================================================
| |||
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);
| |||
}
| |||
| |||
int GetGraphWidth()
| |||
{
| |||
return MathMax(1, CanvasWidth);
| |||
}
| |||
| |||
double GetGraphLevelStepPrice()
| |||
{
| |||
if(GraphLevelsPerSide < 5) return 0.0;
| |||
return (double)GraphLevelsPerSide * _Point;
| |||
}
| |||
| |||
int GraphPriceToY(const double price)
| |||
{
| |||
if(CanvasHeight <= 1 || GraphScaleMax <= GraphScaleMin)
| |||
return CanvasHeight / 2;
| |||
| |||
int top = 0;
| |||
int bottom = MathMax(0, CanvasHeight - 1);
| |||
int drawH = MathMax(1, bottom - top);
| |||
| |||
double ratio = (GraphScaleMax - price) / (GraphScaleMax - GraphScaleMin);
| |||
ratio = MathMax(0.0, MathMin(1.0, ratio));
| |||
| |||
return top + (int)MathRound(ratio * (double)drawH);
| |||
}
| |||
| |||
int PriceToY(const double price)
| |||
{
| |||
return GraphPriceToY(price);
| |||
}
| |||
| |||
bool IsGraphPriceVisible(const double price)
| |||
{
| |||
return GraphScaleMax > GraphScaleMin &&
| |||
price >= GraphScaleMin &&
| |||
price <= GraphScaleMax;
| |||
}
| |||
| |||
double GetGraphLevelSidePriceByOffset(const double bid, const int graphLevel,
| |||
const int side, const int offset)
| |||
{
| |||
if(bid <= 0.0 || graphLevel <= 0 || (side != 1 && side != -1) || offset < 1)
| |||
return 0.0;
| |||
| |||
double step = (double)graphLevel * _Point;
| |||
if(step <= 0.0) return 0.0;
| |||
| |||
double normalizedBid = NormalizeDouble(bid, _Digits);
| |||
double position = (normalizedBid - NormalizeDouble(StartPrice, _Digits)) / step;
| |||
| |||
int baseGrid = (side > 0)
| |||
? (int)MathCeil(position - 1.0e-9)
| |||
: (int)MathFloor(position + 1.0e-9);
| |||
| |||
double basePrice = NormalizeDouble(StartPrice + (double)baseGrid * step, _Digits);
| |||
| |||
while(side > 0 && basePrice <= normalizedBid)
| |||
{
| |||
baseGrid++;
| |||
basePrice = NormalizeDouble(StartPrice + (double)baseGrid * step, _Digits);
| |||
}
| |||
while(side < 0 && basePrice > normalizedBid)
| |||
{
| |||
baseGrid--;
| |||
basePrice = NormalizeDouble(StartPrice + (double)baseGrid * step, _Digits);
| |||
}
| |||
| |||
int grid = baseGrid + ((side > 0) ? (offset - 1) : -(offset - 1));
| |||
return NormalizeDouble(StartPrice + (double)grid * step, _Digits);
| |||
}
| |||
| |||
void UpdateCurrentLevelGrid(const double bid)
| |||
{
| |||
if(bid <= 0.0) return;
| |||
if(GetViewportLevelStepPrice() <= 0.0) return;
| |||
// Заглушка
| |||
}
| |||
| |||
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));
| |||
}
| |||
| |||
void UpdateSubwindowScale(const double bid)
| |||
{
| |||
if(SubWindow < 0 || bid <= 0.0 || TickLevelRowCount <= 0) return;
| |||
| |||
UpdateCurrentLevelGrid(bid);
| |||
| |||
int viewStart = GetViewStartIndex();
| |||
int viewCount = GetViewCount();
| |||
| |||
double visibleMin = bid;
| |||
double visibleMax = bid;
| |||
| |||
for(int i = 0; i < viewCount; i++)
| |||
{
| |||
double price = GetTickPriceByVisibleIndex(viewStart + i);
| |||
if(price <= 0.0) continue;
| |||
visibleMin = MathMin(visibleMin, price);
| |||
visibleMax = MathMax(visibleMax, price);
| |||
}
| |||
| |||
if(InitialArrowHistoryScalePending)
| |||
{
| |||
for(int i = 0; i < ArraySize(SignalMarks); i++)
| |||
{
| |||
if(!SignalMarks[i].reversal) continue;
| |||
if(SignalMarks[i].level != GraphLevelsPerSide) continue;
| |||
visibleMin = MathMin(visibleMin, SignalMarks[i].price);
| |||
visibleMax = MathMax(visibleMax, SignalMarks[i].price);
| |||
}
| |||
}
| |||
| |||
if(!ScaleInitialized)
| |||
{
| |||
double historyCenter = (visibleMin + visibleMax) * 0.5;
| |||
ViewportCenterPrice = NormalizeDouble(historyCenter, _Digits);
| |||
ScaleCenterGrid = FloorGridForViewport(ViewportCenterPrice);
| |||
ScaleInitialized = true;
| |||
}
| |||
| |||
double minimumStep = GetGraphLevelStepPrice();
| |||
minimumStep = MathMax(minimumStep, _Point);
| |||
| |||
int graphLevel = GraphLevelsPerSide;
| |||
int graphPerSide = MathMax(1, GraphDisplayedLevelCount);
| |||
| |||
for(int side = -1; side <= 1; side += 2)
| |||
{
| |||
double levelPrice = GetGraphLevelSidePriceByOffset(bid, graphLevel, side, graphPerSide);
| |||
if(levelPrice <= 0.0) continue;
| |||
visibleMin = MathMin(visibleMin, levelPrice);
| |||
visibleMax = MathMax(visibleMax, levelPrice);
| |||
}
| |||
| |||
double viewportCenter = (ViewportCenterPrice > 0.0) ? ViewportCenterPrice : bid;
| |||
double autoHalfRange = MathMax(MathAbs(viewportCenter - visibleMin),
| |||
MathAbs(visibleMax - viewportCenter));
| |||
double minimumHalfRange = minimumStep * 3.0;
| |||
autoHalfRange = MathMax(autoHalfRange, minimumHalfRange);
| |||
| |||
double autoPadding = MathMax(autoHalfRange * 0.05, minimumStep * 0.10);
| |||
autoHalfRange += autoPadding;
| |||
| |||
if(!VerticalScaleManual && !AutoScaleInitialized)
| |||
{
| |||
VerticalScaleHalfRange = autoHalfRange;
| |||
AutoScaleInitialized = true;
| |||
}
| |||
| |||
double scaleHalfRange = MathMax(VerticalScaleHalfRange, minimumStep * 0.20);
| |||
| |||
//--- Автосдвиг viewport при подходе к краю
| |||
if(!VerticalScaleManual && ScaleInitialized && AutoScaleInitialized &&
| |||
scaleHalfRange > minimumStep)
| |||
{
| |||
double bidOffset = bid - viewportCenter;
| |||
double followTrigger = scaleHalfRange * 0.60;
| |||
double followTarget = scaleHalfRange * 0.35;
| |||
| |||
if(bidOffset > followTrigger)
| |||
viewportCenter = bid - followTarget;
| |||
else if(bidOffset < -followTrigger)
| |||
viewportCenter = bid + followTarget;
| |||
| |||
viewportCenter = NormalizeDouble(viewportCenter, _Digits);
| |||
ViewportCenterPrice = viewportCenter;
| |||
ScaleCenterGrid = FloorGridForViewport(ViewportCenterPrice);
| |||
}
| |||
| |||
//--- FIX: гарантируем, что bid всегда внутри видимого диапазона
| |||
// (даже после zoom / drag)
| |||
if(bid < viewportCenter - scaleHalfRange)
| |||
{
| |||
viewportCenter = bid + scaleHalfRange * 0.5;
| |||
ViewportCenterPrice = NormalizeDouble(viewportCenter, _Digits);
| |||
ScaleCenterGrid = FloorGridForViewport(ViewportCenterPrice);
| |||
}
| |||
else if(bid > viewportCenter + scaleHalfRange)
| |||
{
| |||
viewportCenter = bid - scaleHalfRange * 0.5;
| |||
ViewportCenterPrice = NormalizeDouble(viewportCenter, _Digits);
| |||
ScaleCenterGrid = FloorGridForViewport(ViewportCenterPrice);
| |||
}
| |||
| |||
GraphScaleMin = NormalizeDouble(viewportCenter - scaleHalfRange, _Digits);
| |||
GraphScaleMax = NormalizeDouble(viewportCenter + scaleHalfRange, _Digits);
| |||
| |||
if(GraphScaleMax <= GraphScaleMin)
| |||
{
| |||
GraphScaleMin = viewportCenter - minimumStep;
| |||
GraphScaleMax = viewportCenter + minimumStep;
| |||
}
| |||
| |||
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);
| |||
}
| |||
| |||
void FitInitialArrowHistory()
| |||
{
| |||
if(TickCount <= 0) return;
| |||
| |||
const int targetPerDirection = MathMin(20, MathMax(1, ReversalArrowsHistory));
| |||
long firstGraphSerial = MathMax(1, TickSerial - TickCount + 1);
| |||
| |||
long oldestSerial = -1;
| |||
long newestSerial = -1;
| |||
| |||
for(int direction = 0; direction < 2; direction++)
| |||
{
| |||
long cutoff = 9223372036854775807;
| |||
| |||
for(int n = 0; n < targetPerDirection; n++)
| |||
{
| |||
long best = -1;
| |||
| |||
for(int k = 0; k < ArraySize(SignalMarks); k++)
| |||
{
| |||
if(!SignalMarks[k].reversal) continue;
| |||
if(SignalMarks[k].level != GraphLevelsPerSide) continue;
| |||
| |||
bool wanted = (direction == 1) ? SignalMarks[k].up : !SignalMarks[k].up;
| |||
if(!wanted) continue;
| |||
| |||
long serial = SignalMarks[k].serial;
| |||
if(serial < firstGraphSerial || serial > TickSerial || serial >= cutoff)
| |||
continue;
| |||
| |||
if(serial > best) best = serial;
| |||
}
| |||
| |||
if(best < 0) break;
| |||
| |||
oldestSerial = (oldestSerial < 0) ? best : MathMin(oldestSerial, best);
| |||
newestSerial = (newestSerial < 0) ? best : MathMax(newestSerial, best);
| |||
cutoff = best;
| |||
}
| |||
}
| |||
| |||
if(oldestSerial < 0 || newestSerial < 0)
| |||
{
| |||
ViewSpan = MathMin(MathMax(InitialViewTicks, MinViewTicks), TickCount);
| |||
ViewEndOffset = 0;
| |||
InitialViewLayoutActive = true;
| |||
return;
| |||
}
| |||
| |||
long spanLong = newestSerial - oldestSerial + 1;
| |||
int requiredSpan = (int)MathMin((long)TickCount, spanLong);
| |||
requiredSpan = MathMax(MinViewTicks, requiredSpan);
| |||
requiredSpan = MathMin(requiredSpan, TickCount);
| |||
| |||
ViewSpan = requiredSpan;
| |||
ViewEndOffset = 0;
| |||
InitialViewLayoutActive = true;
| |||
}
| |||
| |||
bool EnsureCanvas()
| |||
{
| |||
if(SubWindow < 0) return false;
| |||
| |||
if(CanvasReady && CanvasSubWindow != SubWindow)
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady = false;
| |||
CanvasWidth = 0;
| |||
CanvasHeight = 0;
| |||
CanvasSubWindow = -1;
| |||
}
| |||
| |||
int chartWidth = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS, 0);
| |||
int chartHeight = (int)ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS, SubWindow);
| |||
| |||
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))
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady = false;
| |||
CanvasWidth = 0;
| |||
CanvasHeight = 0;
| |||
CanvasSubWindow = -1;
| |||
ObjectDelete(0, PANEL_PREFIX + "PANEL");
| |||
}
| |||
else
| |||
{
| |||
CanvasWidth = wantedWidth;
| |||
CanvasHeight = chartHeight;
| |||
return true;
| |||
}
| |||
}
| |||
| |||
ResetLastError();
| |||
if(!TickCanvas.CreateBitmapLabel(0, SubWindow, PANEL_PREFIX + "PANEL",
| |||
0, 0, wantedWidth, chartHeight,
| |||
COLOR_FORMAT_ARGB_NORMALIZE))
| |||
{
| |||
Print("[CodeLock] Canvas creation failed. Error=", GetLastError());
| |||
return false;
| |||
}
| |||
| |||
CanvasWidth = wantedWidth;
| |||
CanvasHeight = chartHeight;
| |||
CanvasSubWindow = SubWindow;
| |||
CanvasReady = true;
| |||
| |||
ObjectSetInteger(0, PANEL_PREFIX + "PANEL", OBJPROP_BACK, true);
| |||
ObjectSetInteger(0, PANEL_PREFIX + "PANEL", OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, PANEL_PREFIX + "PANEL", OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, PANEL_PREFIX + "PANEL", OBJPROP_HIDDEN, true);
| |||
| |||
return true;
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 7B: RENDER — слои отрисовки
| |||
//============================================================================
| |||
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 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;
| |||
| |||
//--- UP — вершина выше основания; DOWN — вершина ниже основания
| |||
if(up)
| |||
TickCanvas.FillTriangle(x, y - 2 * size, x - size, y, x + size, y, c);
| |||
else
| |||
TickCanvas.FillTriangle(x, y + 2 * size, x - size, y, x + size, y, c);
| |||
}
| |||
| |||
void DrawRayLayer(const double bid)
| |||
{
| |||
if(!CanvasReady || !DrawRayObjects) return;
| |||
if(GraphScaleMax <= GraphScaleMin || TickLevelRowCount <= 0) return;
| |||
if(bid <= 0.0) return;
| |||
if(GetGraphLevelStepPrice() <= 0.0) return;
| |||
| |||
int perSide = MathMax(1, GraphDisplayedLevelCount);
| |||
int graphLevel = GraphLevelsPerSide;
| |||
| |||
for(int offset = 1; offset <= perSide; offset++)
| |||
{
| |||
for(int side = -1; side <= 1; side += 2)
| |||
{
| |||
double price = GetGraphLevelSidePriceByOffset(bid, graphLevel, side, offset);
| |||
if(price > 0.0 && IsGraphPriceVisible(price))
| |||
DrawDottedHorizontal(GraphPriceToY(price), ColorToARGB(RayColor, 185));
| |||
}
| |||
}
| |||
}
| |||
| |||
void DrawTickLayer(const double currentBid)
| |||
{
| |||
if(!CanvasReady || TickCount <= 0 || currentBid <= 0.0) return;
| |||
| |||
int viewStart = GetViewStartIndex();
| |||
int viewCount = GetViewCount();
| |||
if(viewCount <= 0) return;
| |||
| |||
uint graphColor = ColorToARGB(TickGraphColor, 255);
| |||
int graphWidth = GetGraphWidth();
| |||
int leftMargin = 0;
| |||
int drawWidth = MathMax(1, graphWidth);
| |||
| |||
if(viewCount == 1)
| |||
{
| |||
int y = GraphPriceToY(currentBid);
| |||
int x = leftMargin;
| |||
TickCanvas.LineThick(x, y, MathMin(graphWidth - 1, x + 4), y,
| |||
graphColor, 2, STYLE_SOLID, LINE_END_ROUND);
| |||
return;
| |||
}
| |||
| |||
bool havePrev = false;
| |||
int prevX = 0;
| |||
int prevY = 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);
| |||
| |||
if(i == viewCount - 1)
| |||
price = currentBid;
| |||
| |||
if(!IsGraphPriceVisible(price))
| |||
{
| |||
havePrev = false;
| |||
continue;
| |||
}
| |||
| |||
int y = GraphPriceToY(price);
| |||
| |||
if(havePrev)
| |||
TickCanvas.LineThick(prevX, prevY, x, y,
| |||
graphColor, 2, STYLE_SOLID, LINE_END_ROUND);
| |||
| |||
prevX = x;
| |||
prevY = y;
| |||
havePrev = true;
| |||
}
| |||
}
| |||
| |||
void DrawSignalLayer(const double bid)
| |||
{
| |||
if(!CanvasReady || TickCount <= 0 || ArraySize(SignalMarks) == 0) return;
| |||
if(bid <= 0.0) return;
| |||
if(!DrawReversalArrows) return;
| |||
| |||
long firstSerial = TickSerial - TickCount + 1;
| |||
int viewStart = GetViewStartIndex();
| |||
int viewCount = GetViewCount();
| |||
int viewEnd = viewStart + viewCount - 1;
| |||
| |||
for(int i = 0; i < ArraySize(SignalMarks); i++)
| |||
{
| |||
if(!SignalMarks[i].reversal) continue;
| |||
if(SignalMarks[i].level != GraphLevelsPerSide) continue;
| |||
| |||
long serial = SignalMarks[i].serial;
| |||
if(serial < firstSerial || serial > TickSerial) continue;
| |||
| |||
int index = (int)(serial - firstSerial);
| |||
if(index < viewStart || index > viewEnd) continue;
| |||
| |||
int relative = index - viewStart;
| |||
int drawWidth = MathMax(1, GetGraphWidth());
| |||
int x = (viewCount == 1) ? 0
| |||
: (int)MathRound((double)relative * (double)(drawWidth - 1) /
| |||
(double)(viewCount - 1));
| |||
| |||
double signalPrice = SignalMarks[i].price;
| |||
if(!IsGraphPriceVisible(signalPrice)) continue;
| |||
| |||
int y = GraphPriceToY(signalPrice);
| |||
color clr = (SignalMarks[i].up ? UpCrossColor : DownCrossColor);
| |||
DrawArrowMark(x, y, SignalMarks[i].up, true, clr);
| |||
}
| |||
}
| |||
| |||
void DrawBidMarker(const double bid)
| |||
{
| |||
if(!CanvasReady || bid <= 0.0) return;
| |||
if(GraphScaleMax <= GraphScaleMin) return;
| |||
| |||
int graphWidth = GetGraphWidth();
| |||
int rightX = MathMax(0, graphWidth - 1);
| |||
| |||
int y;
| |||
bool outOfRange = false;
| |||
| |||
if(bid > GraphScaleMax)
| |||
{
| |||
y = 0; // прижат к верхнему краю
| |||
outOfRange = true;
| |||
}
| |||
else if(bid < GraphScaleMin)
| |||
{
| |||
y = CanvasHeight - 1; // прижат к нижнему краю
| |||
outOfRange = true;
| |||
}
| |||
else
| |||
{
| |||
y = GraphPriceToY(bid);
| |||
}
| |||
| |||
//--- Цвет: обычный, если в кадре; оранжевый, если вне кадра
| |||
color markerColor = outOfRange ? clrOrange : CurrentRayColor;
| |||
uint c = ColorToARGB(markerColor, 255);
| |||
| |||
//--- Горизонтальная линия-указатель
| |||
int markerWidth = MathMin(6, graphWidth);
| |||
int markerStart = MathMax(0, rightX - markerWidth + 1);
| |||
TickCanvas.Line(markerStart, y, rightX, y, c);
| |||
| |||
//--- Треугольник (направление: вверх, если bid выше; вниз, если ниже)
| |||
if(outOfRange && bid > GraphScaleMax)
| |||
{
| |||
//--- Bid выше видимой области → стрелка вверх
| |||
TickCanvas.FillTriangle(MathMax(2, rightX - 12), y + 6,
| |||
MathMax(2, rightX - 2), y + 6,
| |||
MathMax(2, rightX - 7), y,
| |||
c);
| |||
}
| |||
else if(outOfRange && bid < GraphScaleMin)
| |||
{
| |||
//--- Bid ниже видимой области → стрелка вниз
| |||
TickCanvas.FillTriangle(MathMax(2, rightX - 12), y - 6,
| |||
MathMax(2, rightX - 2), y - 6,
| |||
MathMax(2, rightX - 7), y,
| |||
c);
| |||
}
| |||
else
| |||
{
| |||
//--- Bid в кадре → обычный треугольник влево
| |||
TickCanvas.FillTriangle(MathMax(2, rightX - 12), y - 5,
| |||
MathMax(2, rightX - 2), y,
| |||
MathMax(2, rightX - 12), y + 5, c);
| |||
}
| |||
| |||
//--- Подпись с ценой, если вне кадра
| |||
if(outOfRange)
| |||
{
| |||
string priceText = DoubleToString(bid, _Digits);
| |||
uint textWidth = 0, textHeight = 0;
| |||
TextSetFont("Segoe UI", -10 * 10, FW_BOLD);
| |||
TextGetSize(priceText, textWidth, textHeight);
| |||
| |||
int tx = MathMax(2, rightX - (int)textWidth - 6);
| |||
int ty = (bid > GraphScaleMax) ? (y + 8) : (y - (int)textHeight - 8);
| |||
| |||
TickCanvas.FontSet("Segoe UI", -10 * 10, FW_BOLD);
| |||
TickCanvas.TextOut(tx, ty, priceText, ColorToARGB(clrOrange, 255));
| |||
}
| |||
}
| |||
void DrawCurrentTickMarker(const double currentBid)
| |||
{
| |||
if(!CanvasReady || currentBid <= 0.0) return;
| |||
| |||
int graphWidth = GetGraphWidth();
| |||
int x = MathMax(0, graphWidth - 1);
| |||
int y = GraphPriceToY(currentBid);
| |||
| |||
TickCanvas.FillCircle(x, y, 3, ColorToARGB(TickGraphColor, 255));
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 7C: RENDER — RenderGraph, zoom, drag
| |||
//============================================================================
| |||
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);
| |||
| |||
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 EnsureZoomButton(const string name, const string textValue)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_BUTTON, SubWindow, 0, 0))
| |||
{
| |||
Print("[CodeLock] Zoom button creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
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 EnsureResetButton(const string name, const string textValue)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_BUTTON, SubWindow, 0, 0))
| |||
{
| |||
Print("[CodeLock] Reset button creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
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 EnsurePanelLabel(const string name, const string textValue)
| |||
{
| |||
if(ObjectFind(0, name) >= 0 && ObjectFind(0, name) != SubWindow)
| |||
ObjectDelete(0, name);
| |||
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_LABEL, SubWindow, 0, 0))
| |||
{
| |||
Print("[CodeLock] Panel label creation failed. 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);
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetString (0, name, OBJPROP_TEXT, textValue);
| |||
}
| |||
| |||
void UpdateZoomButtonsLayout()
| |||
{
| |||
if(SubWindow < 0 || CanvasWidth <= 0 || CanvasHeight <= 0) return;
| |||
| |||
string minusName = PANEL_PREFIX + "ZOOM_MINUS";
| |||
string plusName = PANEL_PREFIX + "ZOOM_PLUS";
| |||
string resetName = PANEL_PREFIX + "RESET_CURRENT";
| |||
string labelName = PANEL_PREFIX + "ZOOM_LABEL";
| |||
| |||
EnsureZoomButton(minusName, "-");
| |||
EnsureZoomButton(plusName, "+");
| |||
EnsureResetButton(resetName, "RESET");
| |||
EnsurePanelLabel(labelName, "ZOOM");
| |||
| |||
int x = 8;
| |||
int y = 34;
| |||
| |||
int x1 = x;
| |||
int x2 = x + ZoomButtonWidth + ZoomButtonGap;
| |||
int x3 = x + 2 * (ZoomButtonWidth + ZoomButtonGap);
| |||
| |||
ObjectSetInteger(0, minusName, OBJPROP_XDISTANCE, x1);
| |||
ObjectSetInteger(0, minusName, OBJPROP_YDISTANCE, y);
| |||
| |||
ObjectSetInteger(0, plusName, OBJPROP_XDISTANCE, x2);
| |||
ObjectSetInteger(0, plusName, OBJPROP_YDISTANCE, y);
| |||
| |||
ObjectSetInteger(0, resetName, OBJPROP_XDISTANCE, x3);
| |||
ObjectSetInteger(0, resetName, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, resetName, OBJPROP_STATE, ResetButtonHeld);
| |||
| |||
ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE,
| |||
x3 + ResetButtonWidth + ZoomButtonGap);
| |||
ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, y + 4);
| |||
}
| |||
| |||
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(HistoryGraphTicks, 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;
| |||
| |||
return x >= CanvasWidth && x < chartWidth;
| |||
}
| |||
| |||
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) return;
| |||
if(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;
| |||
| |||
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 HandleZoomButtonClick(const string name)
| |||
{
| |||
if(name == PANEL_PREFIX + "ZOOM_MINUS" ||
| |||
name == PANEL_PREFIX + "ZOOM_PLUS")
| |||
{
| |||
InitialViewLayoutActive = false;
| |||
| |||
int delta = (name == PANEL_PREFIX + "ZOOM_PLUS") ? 1 : -1;
| |||
HandleHorizontalZoom(GetGraphWidth() / 2, delta);
| |||
RefreshGraph();
| |||
return;
| |||
}
| |||
| |||
if(name == PANEL_PREFIX + "RESET_CURRENT")
| |||
{
| |||
ResetGraphToCurrentPrice();
| |||
ResetButtonHeld = false;
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
| |||
void ResetGraphToCurrentPrice()
| |||
{
| |||
if(TickCount <= 0) return;
| |||
| |||
ViewSpan = MathMin(MathMax(InitialViewTicks, MinViewTicks), TickCount);
| |||
ViewEndOffset = 0;
| |||
InitialViewLayoutActive = true;
| |||
FitInitialArrowHistory();
| |||
| |||
VerticalScaleManual = false;
| |||
VerticalScaleDragActive = false;
| |||
VerticalPanDragActive = false;
| |||
VerticalScaleHalfRange = 0.0;
| |||
| |||
AutoScaleInitialized = false;
| |||
InitialArrowHistoryScalePending = true;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol, current) && current.bid > 0.0 &&
| |||
GetViewportLevelStepPrice() > 0.0)
| |||
{
| |||
double bid = NormalizeDouble(current.bid, _Digits);
| |||
ScaleCenterGrid = FloorGridForViewport(bid);
| |||
ViewportCenterPrice = ViewportRayPrice(ScaleCenterGrid);
| |||
ScaleInitialized = true;
| |||
}
| |||
| |||
ResetButtonHeld = false;
| |||
}
| |||
| |||
void RenderGraph(const double bid)
| |||
{
| |||
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(bid);
| |||
DrawBidMarker(bid);
| |||
DrawCurrentTickMarker(bid);
| |||
| |||
TickCanvas.Update(true);
| |||
| |||
UpdateZoomButtonsLayout();
| |||
}
| |||
| |||
void RefreshGraph()
| |||
{
| |||
if(SubWindow < 0 || !BootstrapComplete) return;
| |||
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol, current) || current.bid <= 0.0) return;
| |||
if(!EnsureCanvas()) return;
| |||
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending = false;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 8A: DASHBOARD — persistence + Visual ROWS
| |||
//============================================================================
| |||
bool DashIsValidVisualLevel(const int level)
| |||
{
| |||
return (level == 5) ||
| |||
(level >= VISUAL_LEVEL_MIN &&
| |||
level <= VISUAL_LEVEL_MAX &&
| |||
(level % VISUAL_LEVEL_STEP) == 0);
| |||
}
| |||
| |||
bool DashIsStrictVisualRows()
| |||
{
| |||
if(DashVisualRowCount < MIN_VISUAL_ROWS ||
| |||
DashVisualRowCount > MAX_VISUAL_ROWS)
| |||
return false;
| |||
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
if(!DashIsValidVisualLevel(DashVisualRows[i].level)) return false;
| |||
if(i > 0 && DashVisualRows[i].level <= DashVisualRows[i-1].level) return false;
| |||
}
| |||
return true;
| |||
}
| |||
| |||
string DashVisualRowsKeyBase()
| |||
{
| |||
if(DashRowsStateKey == "")
| |||
DashRowsStateKey = STORAGE_DASH_PREFIX +
| |||
IntegerToString(ChartID()) + "_" + _Symbol;
| |||
return DashRowsStateKey;
| |||
}
| |||
| |||
void DashSaveVisualRows()
| |||
{
| |||
string base = DashVisualRowsKeyBase();
| |||
GlobalVariableSet(base + "_COUNT", (double)DashVisualRowCount);
| |||
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
GlobalVariableSet(base + "_R" + IntegerToString(i),
| |||
(double)DashVisualRows[i].level);
| |||
| |||
for(int i = DashVisualRowCount; i < MAX_VISUAL_ROWS; i++)
| |||
GlobalVariableDel(base + "_R" + IntegerToString(i));
| |||
| |||
//--- Сохраняем состояние ALERT (L2-fix)
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
GlobalVariableSet(base + "_A" + IntegerToString(i),
| |||
DashAlertEnabled[i] ? 1.0 : 0.0);
| |||
}
| |||
| |||
bool DashLoadVisualRowsFromKey(const string base)
| |||
{
| |||
if(!GlobalVariableCheck(base + "_COUNT")) return false;
| |||
| |||
int count = (int)MathRound(GlobalVariableGet(base + "_COUNT"));
| |||
if(count < 1 || count > MAX_VISUAL_ROWS) return false;
| |||
| |||
DashVisualRowCount = 0;
| |||
| |||
for(int i = 0; i < count; i++)
| |||
{
| |||
string key = base + "_R" + IntegerToString(i);
| |||
if(!GlobalVariableCheck(key)) return false;
| |||
| |||
int level = (int)MathRound(GlobalVariableGet(key));
| |||
if(!DashIsValidVisualLevel(level)) return false;
| |||
if(i > 0 && level <= DashVisualRows[i-1].level) return false;
| |||
| |||
DashVisualRows[i].level = level;
| |||
DashVisualRowCount++;
| |||
}
| |||
| |||
//--- Восстановление ALERT (L2-fix)
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
string alertKey = base + "_A" + IntegerToString(i);
| |||
DashAlertEnabled[i] = GlobalVariableCheck(alertKey)
| |||
? (GlobalVariableGet(alertKey) > 0.5)
| |||
: false;
| |||
}
| |||
| |||
return DashIsStrictVisualRows();
| |||
}
| |||
| |||
bool DashParseVisualRows(const string source)
| |||
{
| |||
string parts[];
| |||
int count = StringSplit(source, ',', parts);
| |||
if(count < MIN_VISUAL_ROWS || count > MAX_VISUAL_ROWS) return false;
| |||
| |||
DashVisualRowCount = 0;
| |||
| |||
for(int i = 0; i < count; i++)
| |||
{
| |||
string token = parts[i];
| |||
StringTrimLeft(token);
| |||
StringTrimRight(token);
| |||
| |||
if(token == "") return false;
| |||
| |||
for(int k = 0; k < StringLen(token); k++)
| |||
{
| |||
ushort ch = StringGetCharacter(token, k);
| |||
if(ch < '0' || ch > '9') return false;
| |||
}
| |||
| |||
int level = (int)StringToInteger(token);
| |||
if(!DashIsValidVisualLevel(level)) return false;
| |||
if(i > 0 && level <= DashVisualRows[i-1].level) return false;
| |||
| |||
DashVisualRows[DashVisualRowCount].level = level;
| |||
DashVisualRowCount++;
| |||
}
| |||
| |||
return DashIsStrictVisualRows();
| |||
}
| |||
| |||
bool DashEnsureFivePointVisualRow()
| |||
{
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
if(DashVisualRows[i].level == 5) return false;
| |||
| |||
if(DashVisualRowCount >= MAX_VISUAL_ROWS)
| |||
{
| |||
int removedLevel = DashVisualRows[DashVisualRowCount - 1].level;
| |||
DashVisualRowCount--;
| |||
Print("[CodeLock] Mandatory ROW 5pt added; highest ROW ", removedLevel,
| |||
" pt removed.");
| |||
}
| |||
| |||
for(int i = DashVisualRowCount; i > 0; i--)
| |||
DashVisualRows[i] = DashVisualRows[i-1];
| |||
| |||
DashVisualRows[0].level = 5;
| |||
DashVisualRowCount++;
| |||
return true;
| |||
}
| |||
| |||
void DashLoadVisualRows()
| |||
{
| |||
if(!DashLoadVisualRowsFromKey(DashVisualRowsKeyBase()))
| |||
{
| |||
if(!DashParseVisualRows(InitialVisualRows))
| |||
{
| |||
if(!DashParseVisualRows(DEFAULT_VISUAL_ROWS))
| |||
{
| |||
Print("[CodeLock] Failed to load Visual ROWS");
| |||
return;
| |||
}
| |||
Print("[CodeLock] InitialVisualRows invalid; using default");
| |||
}
| |||
}
| |||
| |||
if(DashEnsureFivePointVisualRow())
| |||
DashSaveVisualRows();
| |||
}
| |||
| |||
void SyncTickVisualRowsFromDashboard()
| |||
{
| |||
if(DashVisualRowCount < MIN_VISUAL_ROWS ||
| |||
DashVisualRowCount > MAX_VISUAL_ROWS)
| |||
return;
| |||
| |||
bool changed = (TickLevelRowCount != DashVisualRowCount);
| |||
| |||
if(!changed)
| |||
{
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
if(TickLevelRows[i].level != DashVisualRows[i].level)
| |||
{
| |||
changed = true;
| |||
break;
| |||
}
| |||
}
| |||
}
| |||
| |||
if(!changed) return;
| |||
| |||
bool savedAlert[MAX_VISUAL_ROWS];
| |||
int savedMax = MathMax(TickLevelRowCount, DashVisualRowCount);
| |||
for(int i = 0; i < savedMax && i < MAX_VISUAL_ROWS; i++)
| |||
savedAlert[i] = DashAlertEnabled[i];
| |||
| |||
ArrayResize(TickLevelRows, DashVisualRowCount);
| |||
TickLevelRowCount = DashVisualRowCount;
| |||
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
TickLevelRows[i].level = DashVisualRows[i].level;
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
}
| |||
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
{
| |||
if(i < savedMax)
| |||
{
| |||
DashAlertEnabled[i] = savedAlert[i];
| |||
TickLevelRows[i].alertEnabled = savedAlert[i];
| |||
}
| |||
}
| |||
| |||
if(ModelTickCount >= 2)
| |||
{
| |||
HistoryBootstrapTargetTicks = MathMax(HistoryBootstrapTargetTicks, ModelTickCount);
| |||
BootstrapComplete = false;
| |||
LiveMode = false;
| |||
}
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 8B: DASHBOARD — GUI таблицы
| |||
//============================================================================
| |||
int DashColLevelX = 12;
| |||
int DashColStatusX = 0;
| |||
int DashColTriggerX = 0;
| |||
int DashColAlertX = 0;
| |||
int DashColGraphicX = 0;
| |||
int DashColLastDown2X = 0;
| |||
int DashColLastDown1X = 0;
| |||
int DashColLastUp2X = 0;
| |||
int DashColLastUp1X = 0;
| |||
int DashAutoPanelWidth = 0;
| |||
int DashLevelButtonWidth = 72;
| |||
int DashRowsButtonWidth = 27;
| |||
int DashRowsValueWidth = 48;
| |||
| |||
int DashMeasureTextWidth(const string text)
| |||
{
| |||
static bool fontReady = false;
| |||
if(!fontReady)
| |||
{
| |||
TextSetFont("Segoe UI", -DashboardFontSize * 10, FW_NORMAL);
| |||
fontReady = true;
| |||
}
| |||
| |||
uint width = 0, height = 0;
| |||
if(!TextGetSize(text, width, height))
| |||
return MathMax(1, StringLen(text) * DashboardFontSize);
| |||
return (int)width;
| |||
}
| |||
| |||
void DashCalculateAutoLayout()
| |||
{
| |||
int levelWidth = DashMeasureTextWidth("100000 pt");
| |||
int statusWidth = DashMeasureTextWidth("DIRECTION");
| |||
int triggerWidth = DashMeasureTextWidth("REVERSAL AT");
| |||
int alertWidth = DashMeasureTextWidth("ALERT") + 16;
| |||
int graphicWidth = DashMeasureTextWidth("GRAPHIC") + 16;
| |||
| |||
int lastWidth = DashMeasureTextWidth("LAST-DOWN-2");
| |||
lastWidth = MathMax(lastWidth, DashMeasureTextWidth("00000.00000"));
| |||
lastWidth = MathMax(lastWidth, DashMeasureTextWidth("LAST-UP-1"));
| |||
| |||
int gap = 5;
| |||
| |||
DashColLevelX = 12;
| |||
DashLevelButtonWidth = MathMax(66, levelWidth + 12);
| |||
| |||
DashColStatusX = DashColLevelX + DashLevelButtonWidth + gap;
| |||
DashColTriggerX = DashColStatusX + statusWidth + gap;
| |||
DashColAlertX = DashColTriggerX + triggerWidth + gap;
| |||
DashColGraphicX = DashColAlertX + alertWidth + gap;
| |||
DashColLastDown2X = DashColGraphicX + graphicWidth + gap;
| |||
DashColLastDown1X = DashColLastDown2X + lastWidth + gap;
| |||
DashColLastUp2X = DashColLastDown1X + lastWidth + gap;
| |||
DashColLastUp1X = DashColLastUp2X + lastWidth + gap;
| |||
| |||
DashAutoPanelWidth = DashColLastUp1X + lastWidth + 24;
| |||
}
| |||
| |||
void DashEnsureRect(const string name, const int x, const int y,
| |||
const int width, const int height,
| |||
const color background, const color border)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_RECTANGLE_LABEL, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Dashboard rect creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_BACK, false);
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
| |||
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
| |||
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, border);
| |||
ObjectSetInteger(0, name, OBJPROP_BORDER_TYPE, BORDER_FLAT);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, 100);
| |||
}
| |||
| |||
void DashEnsureLabel(const string name, const int x, const int y,
| |||
const int fontSize, const color textColor,
| |||
const string textValue)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Dashboard label creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetString (0, name, OBJPROP_FONT, "Segoe UI");
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, 110);
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, fontSize);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
| |||
ObjectSetString (0, name, OBJPROP_TEXT, textValue);
| |||
}
| |||
| |||
void DashEnsureButton(const string name, const int x, const int y,
| |||
const int width, const int height,
| |||
const string textValue,
| |||
const color background, const color border,
| |||
const color textColor, const long zorder = 150)
| |||
{
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Dashboard button creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, zorder);
| |||
ObjectSetString (0, name, OBJPROP_FONT, "Segoe UI");
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
| |||
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, DashboardFontSize);
| |||
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, background);
| |||
ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, border);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, textColor);
| |||
ObjectSetString (0, name, OBJPROP_TEXT, textValue);
| |||
}
| |||
| |||
void DashDeleteHiddenRows(const int visibleCount)
| |||
{
| |||
for(int i = visibleCount; i < MAX_VISUAL_ROWS; i++)
| |||
{
| |||
string sfx = IntegerToString(i);
| |||
ObjectDelete(0, DASH_PREFIX + "ROW_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "STATEBAR_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "LEVEL_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "STATE_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "TRIGGER_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "ALERT_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "GRAPHIC_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "LAST_DOWN_2_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "LAST_DOWN_1_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "LAST_UP_2_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "LAST_UP_1_" + sfx);
| |||
ObjectDelete(0, DASH_PREFIX + "EDIT_" + sfx);
| |||
}
| |||
}
| |||
| |||
color DashStatusColor(const int direction)
| |||
{
| |||
if(direction > 0) return UpColor;
| |||
if(direction < 0) return DownColor;
| |||
return WaitColor;
| |||
}
| |||
| |||
string DashStatusText(const int direction)
| |||
{
| |||
if(direction > 0) return "UP";
| |||
if(direction < 0) return "DOWN";
| |||
return "WAIT";
| |||
}
| |||
| |||
void DashSetAlertButtonState(const int index)
| |||
{
| |||
if(index < 0 || index >= MAX_VISUAL_ROWS) return;
| |||
| |||
string name = DASH_PREFIX + "ALERT_" + IntegerToString(index);
| |||
if(ObjectFind(0, name) < 0) return;
| |||
| |||
bool enabled = DashAlertEnabled[index];
| |||
ulong now = (ulong)GetTickCount();
| |||
bool activity = (DashAlertActivityUntil[index] > now);
| |||
| |||
color buttonColor = activity ? AlertActivityColor
| |||
: (enabled ? AlertOnColor : AlertOffColor);
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, buttonColor);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, AlertTextColor);
| |||
ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR,
| |||
activity ? AlertActivityColor
| |||
: (enabled ? AlertOnColor : BorderColor));
| |||
ObjectSetString (0, name, OBJPROP_TEXT, "ALERT");
| |||
ObjectSetInteger(0, name, OBJPROP_STATE, enabled);
| |||
}
| |||
| |||
void DashUpdateAlertButton(const int index, const int x, const int y,
| |||
const int width, const int height)
| |||
{
| |||
string name = DASH_PREFIX + "ALERT_" + IntegerToString(index);
| |||
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Alert button creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, 140);
| |||
ObjectSetString (0, name, OBJPROP_FONT, "Segoe UI");
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
| |||
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, DashboardFontSize);
| |||
| |||
DashSetAlertButtonState(index);
| |||
}
| |||
| |||
void DashUpdateGraphicButton(const int index, const int x, const int y,
| |||
const int width, const int height)
| |||
{
| |||
string name = DASH_PREFIX + "GRAPHIC_" + IntegerToString(index);
| |||
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_BUTTON, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Graphic button creation failed. Error=", GetLastError(),
| |||
" Name=", name);
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTED, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, 140);
| |||
ObjectSetString (0, name, OBJPROP_FONT, "Segoe UI");
| |||
}
| |||
| |||
bool selected = (index < DashVisualRowCount &&
| |||
DashVisualRows[index].level == GraphLevelsPerSide);
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
| |||
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, DashboardFontSize);
| |||
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, selected ? HeaderColor : C'42,50,64');
| |||
ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, selected ? HeaderColor : BorderColor);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, TextColor);
| |||
ObjectSetString (0, name, OBJPROP_TEXT, "GRAPHIC");
| |||
ObjectSetInteger(0, name, OBJPROP_STATE, selected);
| |||
}
| |||
| |||
void DashRenderDashboard(const double bid)
| |||
{
| |||
if(DashVisualRowCount < 1) return;
| |||
| |||
int levelCount = DashVisualRowCount;
| |||
DashCalculateAutoLayout();
| |||
| |||
int panelHeight = HeaderHeight + 8 + levelCount * RowHeight + 12;
| |||
int panelX = MathMax(4, PanelLeftMargin);
| |||
int panelY = MathMax(4, PanelTopMargin);
| |||
| |||
DashEnsureRect(DASH_PREFIX + "BG", panelX, panelY,
| |||
DashAutoPanelWidth, panelHeight,
| |||
PanelBackground, BorderColor);
| |||
| |||
DashEnsureRect(DASH_PREFIX + "HEADER", panelX + 2, panelY + 2,
| |||
DashAutoPanelWidth - 4, HeaderHeight - 4,
| |||
PanelBackground, PanelBackground);
| |||
| |||
int titleWidth = DashMeasureTextWidth("DASHBOARD");
| |||
int titleX = panelX + (DashAutoPanelWidth - titleWidth) / 2;
| |||
DashEnsureLabel(DASH_PREFIX + "TITLE", titleX, panelY + 6,
| |||
DashboardFontSize, TextColor, "DASHBOARD");
| |||
| |||
int bidWidth = DashMeasureTextWidth("BID " + DoubleToString(bid, _Digits));
| |||
DashEnsureLabel(DASH_PREFIX + "BID",
| |||
panelX + DashAutoPanelWidth - bidWidth - 16, panelY + 8,
| |||
DashboardFontSize, HeaderColor,
| |||
"BID " + DoubleToString(bid, _Digits));
| |||
| |||
int headerY = panelY + HeaderHeight - 20;
| |||
| |||
DashEnsureLabel(DASH_PREFIX + "COL_LEVEL", panelX + DashColLevelX, headerY,
| |||
DashboardFontSize, MutedTextColor, "LEVEL");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_STATUS", panelX + DashColStatusX, headerY,
| |||
DashboardFontSize, MutedTextColor, "DIRECTION");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_TRIGGER", panelX + DashColTriggerX, headerY,
| |||
DashboardFontSize, MutedTextColor, "REVERSAL AT");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_ALERT", panelX + DashColAlertX, headerY,
| |||
DashboardFontSize, MutedTextColor, "ALERT");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_GRAPHIC", panelX + DashColGraphicX, headerY,
| |||
DashboardFontSize, MutedTextColor, "GRAPHIC");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_LAST_DOWN_2", panelX + DashColLastDown2X, headerY,
| |||
DashboardFontSize, MutedTextColor, "LAST-DOWN-2");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_LAST_DOWN_1", panelX + DashColLastDown1X, headerY,
| |||
DashboardFontSize, MutedTextColor, "LAST-DOWN-1");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_LAST_UP_2", panelX + DashColLastUp2X, headerY,
| |||
DashboardFontSize, MutedTextColor, "LAST-UP-2");
| |||
DashEnsureLabel(DASH_PREFIX + "COL_LAST_UP_1", panelX + DashColLastUp1X, headerY,
| |||
DashboardFontSize, MutedTextColor, "LAST-UP-1");
| |||
| |||
for(int row = 0; row < levelCount; row++)
| |||
{
| |||
int index = levelCount - 1 - row;
| |||
int y = panelY + HeaderHeight + 8 + row * RowHeight;
| |||
string sfx = IntegerToString(index);
| |||
| |||
int direction = TickLevelRows[index].direction;
| |||
| |||
DashEnsureRect(DASH_PREFIX + "ROW_" + sfx, panelX + 2, y,
| |||
DashAutoPanelWidth - 4, RowHeight - 1,
| |||
(row % 2 == 0) ? RowBackground : RowBackgroundAlt,
| |||
BorderColor);
| |||
| |||
DashEnsureRect(DASH_PREFIX + "STATEBAR_" + sfx, panelX + 3, y + 2,
| |||
5, RowHeight - 5,
| |||
DashStatusColor(direction), DashStatusColor(direction));
| |||
| |||
DashEnsureButton(DASH_PREFIX + "LEVEL_" + sfx,
| |||
panelX + DashColLevelX, y + 1,
| |||
DashLevelButtonWidth, RowHeight - 3,
| |||
IntegerToString(TickLevelRows[index].level) + " pt",
| |||
RowBackground, BorderColor,
| |||
DashStatusColor(direction), 1100);
| |||
| |||
DashEnsureLabel(DASH_PREFIX + "STATE_" + sfx,
| |||
panelX + DashColStatusX, y + 3,
| |||
DashboardFontSize, DashStatusColor(direction),
| |||
DashStatusText(direction));
| |||
| |||
string reversalAtText = (TickLevelRows[index].direction != 0 &&
| |||
TickLevelRows[index].reversalAtPrice > 0.0)
| |||
? DoubleToString(TickLevelRows[index].reversalAtPrice, _Digits)
| |||
: "";
| |||
int reversalDir = (direction != 0) ? -direction : 0;
| |||
DashEnsureLabel(DASH_PREFIX + "TRIGGER_" + sfx,
| |||
panelX + DashColTriggerX, y + 3,
| |||
DashboardFontSize, DashStatusColor(reversalDir),
| |||
reversalAtText);
| |||
| |||
DashUpdateAlertButton(index, panelX + DashColAlertX, y + 1,
| |||
DashMeasureTextWidth("ALERT") + 16, RowHeight - 3);
| |||
| |||
DashUpdateGraphicButton(index, panelX + DashColGraphicX, y + 1,
| |||
DashMeasureTextWidth("GRAPHIC") + 16, RowHeight - 3);
| |||
| |||
string ld2 = (TickLevelRows[index].lastDown2Price > 0.0)
| |||
? DoubleToString(TickLevelRows[index].lastDown2Price, _Digits) : "";
| |||
string ld1 = (TickLevelRows[index].lastDown1Price > 0.0)
| |||
? DoubleToString(TickLevelRows[index].lastDown1Price, _Digits) : "";
| |||
string lu2 = (TickLevelRows[index].lastUp2Price > 0.0)
| |||
? DoubleToString(TickLevelRows[index].lastUp2Price, _Digits) : "";
| |||
string lu1 = (TickLevelRows[index].lastUp1Price > 0.0)
| |||
? DoubleToString(TickLevelRows[index].lastUp1Price, _Digits) : "";
| |||
| |||
DashEnsureLabel(DASH_PREFIX + "LAST_DOWN_2_" + sfx,
| |||
panelX + DashColLastDown2X, y + 3,
| |||
DashboardFontSize, DownColor, ld2);
| |||
DashEnsureLabel(DASH_PREFIX + "LAST_DOWN_1_" + sfx,
| |||
panelX + DashColLastDown1X, y + 3,
| |||
DashboardFontSize, DownColor, ld1);
| |||
DashEnsureLabel(DASH_PREFIX + "LAST_UP_2_" + sfx,
| |||
panelX + DashColLastUp2X, y + 3,
| |||
DashboardFontSize, UpColor, lu2);
| |||
DashEnsureLabel(DASH_PREFIX + "LAST_UP_1_" + sfx,
| |||
panelX + DashColLastUp1X, y + 3,
| |||
DashboardFontSize, UpColor, lu1);
| |||
}
| |||
| |||
DashDeleteHiddenRows(levelCount);
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 8C: DASHBOARD — обработчики кликов
| |||
//============================================================================
| |||
bool DashIsAlertButtonName(const string name, int &stateIndex)
| |||
{
| |||
string prefix = DASH_PREFIX + "ALERT_";
| |||
if(StringFind(name, prefix) != 0) return false;
| |||
| |||
string sfx = StringSubstr(name, StringLen(prefix));
| |||
if(sfx == "") return false;
| |||
| |||
int index = (int)StringToInteger(sfx);
| |||
if(index < 0 || index >= MAX_VISUAL_ROWS) return false;
| |||
| |||
stateIndex = index;
| |||
return true;
| |||
}
| |||
| |||
bool DashIsGraphicButtonName(const string name, int &stateIndex)
| |||
{
| |||
string prefix = DASH_PREFIX + "GRAPHIC_";
| |||
if(StringFind(name, prefix) != 0) return false;
| |||
| |||
string sfx = StringSubstr(name, StringLen(prefix));
| |||
if(sfx == "") return false;
| |||
| |||
int index = (int)StringToInteger(sfx);
| |||
if(index < 0 || index >= MAX_VISUAL_ROWS) return false;
| |||
| |||
stateIndex = index;
| |||
return true;
| |||
}
| |||
| |||
void DashToggleAlert(const int index)
| |||
{
| |||
if(index < 0 || index >= MAX_VISUAL_ROWS) return;
| |||
| |||
DashAlertEnabled[index] = !DashAlertEnabled[index];
| |||
if(index < TickLevelRowCount)
| |||
TickLevelRows[index].alertEnabled = DashAlertEnabled[index];
| |||
DashAlertActivityUntil[index] = 0;
| |||
| |||
Print("[CodeLock] ALERT ", TickLevelRows[index].level, " pt ",
| |||
(DashAlertEnabled[index] ? "ON" : "OFF"));
| |||
| |||
//--- Сохраняем состояние ALERT
| |||
DashSaveVisualRows();
| |||
| |||
DashSetAlertButtonState(index);
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void DashSelectGraphicLevel(const int index)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount) return;
| |||
| |||
int selectedLevel = DashVisualRows[index].level;
| |||
if(selectedLevel < 5) return;
| |||
| |||
//--- FIX: используем точное значение selectedLevel
| |||
GraphLevelsPerSide = MathMax(5, selectedLevel);
| |||
| |||
//--- Добавляем уровень в DashVisualRows, если его там нет
| |||
bool exists = false;
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
if(DashVisualRows[i].level == GraphLevelsPerSide)
| |||
{
| |||
exists = true;
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(!exists && DashVisualRowCount < MAX_VISUAL_ROWS)
| |||
{
| |||
DashVisualRows[DashVisualRowCount].level = GraphLevelsPerSide;
| |||
DashVisualRowCount++;
| |||
//--- сортировка не нужна: GraphLevelsPerSide уже существует в таблице
| |||
// (индекс — это строка, по которой кликнули)
| |||
}
| |||
| |||
//--- Пересобираем TickLevelRows из DashVisualRows
| |||
ArrayResize(TickLevelRows, DashVisualRowCount);
| |||
TickLevelRowCount = DashVisualRowCount;
| |||
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
{
| |||
TickLevelRows[i].level = DashVisualRows[i].level;
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
if(i < MAX_VISUAL_ROWS)
| |||
TickLevelRows[i].alertEnabled = DashAlertEnabled[i];
| |||
}
| |||
| |||
if(ModelTickCount >= 2)
| |||
{
| |||
HistoryBootstrapTargetTicks = MathMax(HistoryBootstrapTargetTicks, ModelTickCount);
| |||
BootstrapComplete = false;
| |||
LiveMode = false;
| |||
}
| |||
| |||
ResetGraphToCurrentPrice();
| |||
RefreshGraph();
| |||
DashRenderDashboard(SymbolInfoDouble(_Symbol, SYMBOL_BID));
| |||
}
| |||
| |||
void DashBeginLevelEdit(const int index, const int x, const int y,
| |||
const int width, const int height)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount) return;
| |||
if(DashEditingRow >= 0 && DashEditingRow != index)
| |||
ObjectDelete(0, DASH_PREFIX + "EDIT_" + IntegerToString(DashEditingRow));
| |||
| |||
string name = DASH_PREFIX + "EDIT_" + IntegerToString(index);
| |||
if(ObjectFind(0, name) < 0)
| |||
{
| |||
if(!ObjectCreate(0, name, OBJ_EDIT, 0, 0, 0))
| |||
{
| |||
Print("[CodeLock] Level edit creation failed. Error=", GetLastError());
| |||
return;
| |||
}
| |||
ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
| |||
ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
| |||
ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
| |||
ObjectSetInteger(0, name, OBJPROP_ZORDER, 180);
| |||
ObjectSetString (0, name, OBJPROP_FONT, "Segoe UI");
| |||
ObjectSetInteger(0, name, OBJPROP_ALIGN, ALIGN_CENTER);
| |||
ObjectSetInteger(0, name, OBJPROP_READONLY, false);
| |||
}
| |||
| |||
ObjectSetInteger(0, name, OBJPROP_XDISTANCE, x);
| |||
ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
| |||
ObjectSetInteger(0, name, OBJPROP_XSIZE, width);
| |||
ObjectSetInteger(0, name, OBJPROP_YSIZE, height);
| |||
ObjectSetInteger(0, name, OBJPROP_FONTSIZE, DashboardFontSize);
| |||
ObjectSetInteger(0, name, OBJPROP_COLOR, TextColor);
| |||
ObjectSetInteger(0, name, OBJPROP_BGCOLOR, PanelBackground);
| |||
ObjectSetInteger(0, name, OBJPROP_BORDER_COLOR, HeaderColor);
| |||
ObjectSetString (0, name, OBJPROP_TEXT, IntegerToString(DashVisualRows[index].level));
| |||
| |||
DashEditingRow = index;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void DashCancelLevelEdit()
| |||
{
| |||
if(DashEditingRow < 0) return;
| |||
ObjectDelete(0, DASH_PREFIX + "EDIT_" + IntegerToString(DashEditingRow));
| |||
DashEditingRow = -1;
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
bool DashValidateEditedLevel(const int index, const int newLevel)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount) return false;
| |||
if(!DashIsValidVisualLevel(newLevel)) return false;
| |||
if(index > 0 && newLevel <= DashVisualRows[index-1].level) return false;
| |||
if(index + 1 < DashVisualRowCount && newLevel >= DashVisualRows[index+1].level) return false;
| |||
return true;
| |||
}
| |||
| |||
void DashCommitEditedLevel(const int index, const string text)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount)
| |||
{
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
if(text == "")
| |||
{
| |||
Print("[CodeLock] LEVEL edit rejected: empty value");
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
for(int k = 0; k < StringLen(text); k++)
| |||
{
| |||
ushort ch = StringGetCharacter(text, k);
| |||
if(ch < '0' || ch > '9')
| |||
{
| |||
Print("[CodeLock] LEVEL edit rejected: use 5 or 10..2000 step 10");
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
}
| |||
| |||
int newLevel = (int)StringToInteger(text);
| |||
if(!DashValidateEditedLevel(index, newLevel))
| |||
{
| |||
Print("[CodeLock] LEVEL edit rejected: ", text,
| |||
" (strict order violated)");
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
DashVisualRows[index].level = newLevel;
| |||
DashSaveVisualRows();
| |||
DashCancelLevelEdit();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("[CodeLock] Visual ROW ", index, " LEVEL = ", newLevel, " pt");
| |||
}
| |||
| |||
bool DashAddVisualRow(const int index)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount ||
| |||
DashVisualRowCount >= MAX_VISUAL_ROWS) return false;
| |||
| |||
int current = DashVisualRows[index].level;
| |||
int upper = (index + 1 < DashVisualRowCount)
| |||
? DashVisualRows[index + 1].level
| |||
: (VISUAL_LEVEL_MAX + VISUAL_LEVEL_STEP);
| |||
| |||
//--- FIX: шаг +10, выравнивание по VISUAL_LEVEL_STEP
| |||
int candidate = current + VISUAL_LEVEL_STEP;
| |||
if(candidate < VISUAL_LEVEL_MIN && current >= 5)
| |||
candidate = VISUAL_LEVEL_MIN;
| |||
if(candidate % VISUAL_LEVEL_STEP != 0)
| |||
candidate = ((candidate / VISUAL_LEVEL_STEP) + 1) * VISUAL_LEVEL_STEP;
| |||
| |||
while(candidate < upper && candidate > current)
| |||
{
| |||
bool free = true;
| |||
for(int i = 0; i < DashVisualRowCount; i++)
| |||
if(DashVisualRows[i].level == candidate) { free = false; break; }
| |||
| |||
if(free)
| |||
{
| |||
for(int i = DashVisualRowCount; i > index + 1; i--)
| |||
DashVisualRows[i] = DashVisualRows[i-1];
| |||
| |||
DashVisualRows[index + 1].level = candidate;
| |||
DashVisualRowCount++;
| |||
DashSaveVisualRows();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("[CodeLock] Added ROW ", candidate, " pt");
| |||
return true;
| |||
}
| |||
candidate += VISUAL_LEVEL_STEP;
| |||
}
| |||
| |||
Print("[CodeLock] Cannot add ROW after ", current, " pt");
| |||
return false;
| |||
}
| |||
| |||
bool DashDeleteVisualRow(const int index)
| |||
{
| |||
if(index < 0 || index >= DashVisualRowCount) return false;
| |||
| |||
if(DashVisualRows[index].level == 5)
| |||
{
| |||
Print("[CodeLock] Cannot delete mandatory ROW 5 pt");
| |||
return false;
| |||
}
| |||
| |||
if(DashVisualRowCount <= MIN_VISUAL_ROWS)
| |||
{
| |||
Print("[CodeLock] At least ", MIN_VISUAL_ROWS, " ROWS required");
| |||
return false;
| |||
}
| |||
| |||
int removed = DashVisualRows[index].level;
| |||
for(int i = index; i < DashVisualRowCount - 1; i++)
| |||
DashVisualRows[i] = DashVisualRows[i+1];
| |||
DashVisualRowCount--;
| |||
DashSaveVisualRows();
| |||
SyncTickVisualRowsFromDashboard();
| |||
Print("[CodeLock] Deleted ROW ", removed, " pt");
| |||
return true;
| |||
}
| |||
| |||
void DashChartEvent(const int id, const long &lparam, const double &dparam,
| |||
const string &sparam)
| |||
{
| |||
if(id == CHARTEVENT_KEYDOWN && DashEditingRow >= 0 && lparam == KEY_ESCAPE_CODE)
| |||
{
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
| |||
if(id == CHARTEVENT_OBJECT_ENDEDIT)
| |||
{
| |||
string editPrefix = DASH_PREFIX + "EDIT_";
| |||
if(StringFind(sparam, editPrefix) == 0)
| |||
{
| |||
int index = (int)StringToInteger(StringSubstr(sparam, StringLen(editPrefix)));
| |||
string textValue = "";
| |||
if(ObjectGetString(0, sparam, OBJPROP_TEXT, 0, textValue))
| |||
DashCommitEditedLevel(index, textValue);
| |||
else
| |||
DashCancelLevelEdit();
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id != CHARTEVENT_OBJECT_CLICK) return;
| |||
| |||
if(StringFind(sparam, DASH_PREFIX) != 0) return;
| |||
| |||
int graphicIndex = -1;
| |||
if(DashIsGraphicButtonName(sparam, graphicIndex))
| |||
{
| |||
DashSelectGraphicLevel(graphicIndex);
| |||
return;
| |||
}
| |||
| |||
int alertIndex = -1;
| |||
if(DashIsAlertButtonName(sparam, alertIndex))
| |||
{
| |||
DashToggleAlert(alertIndex);
| |||
return;
| |||
}
| |||
| |||
string levelPrefix = DASH_PREFIX + "LEVEL_";
| |||
if(StringFind(sparam, levelPrefix) == 0)
| |||
{
| |||
int index = (int)StringToInteger(StringSubstr(sparam, StringLen(levelPrefix)));
| |||
if(index >= 0 && index < DashVisualRowCount)
| |||
{
| |||
string rowName = DASH_PREFIX + "ROW_" + IntegerToString(index);
| |||
int y = (ObjectFind(0, rowName) >= 0)
| |||
? (int)ObjectGetInteger(0, rowName, OBJPROP_YDISTANCE)
| |||
: PanelTopMargin + HeaderHeight + 8;
| |||
DashBeginLevelEdit(index,
| |||
(int)ObjectGetInteger(0, sparam, OBJPROP_XDISTANCE),
| |||
y + 1, DashLevelButtonWidth, RowHeight - 3);
| |||
}
| |||
return;
| |||
}
| |||
}
| |||
| |||
//============================================================================
| |||
// SECTION 9: MAIN — OnInit / OnDeinit / OnCalculate / OnTimer / OnChartEvent
| |||
//============================================================================
| |||
int OnInit()
| |||
{
| |||
GraphLevelsPerSide = InpGraphLevelsPerSide;
| |||
GraphDisplayedLevelCount = InpGraphDisplayedLevelCount;
| |||
| |||
if(StartPrice <= 0.0 || BufferPoints < 0)
| |||
{
| |||
Print("[CodeLock] Invalid StartPrice or BufferPoints");
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
if(ReversalArrowsHistory < 0 || ReversalArrowsHistory > 20)
| |||
{
| |||
Print("[CodeLock] ReversalArrowsHistory 0..20");
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
if(MaxModelHistoryTicks < 100 || HistoryGraphTicks < 100 || HistoryBootstrapTicks < 100)
| |||
{
| |||
Print("[CodeLock] History parameters must be >= 100");
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
| |||
//--- FIX: инициализация SubWindow ДО любых операций с Canvas
| |||
SubWindow = ChartWindowFind(0, "CodeLock");
| |||
if(SubWindow < 0)
| |||
{
| |||
//--- fallback: используем 0 (главное окно), Canvas создастся в главном
| |||
SubWindow = 0;
| |||
Print("[CodeLock] Warning: subwindow not found, using main window");
| |||
}
| |||
| |||
//--- Загружаем Visual ROWS (Dashboard)
| |||
DashLoadVisualRows();
| |||
if(!DashIsStrictVisualRows())
| |||
{
| |||
Print("[CodeLock] Invalid DashVisualRows");
| |||
return INIT_PARAMETERS_INCORRECT;
| |||
}
| |||
| |||
//--- Синхронизируем TickLevelRows с DashVisualRows
| |||
TickLevelRowCount = DashVisualRowCount;
| |||
ArrayResize(TickLevelRows, TickLevelRowCount);
| |||
for(int i = 0; i < TickLevelRowCount; i++)
| |||
{
| |||
TickLevelRows[i].level = DashVisualRows[i].level;
| |||
ResetTickLevelRowState(TickLevelRows[i]);
| |||
TickLevelRows[i].alertEnabled = DashAlertEnabled[i];
| |||
}
| |||
| |||
//--- Прикрепляемся к рынку
| |||
MqlTick attachTick;
| |||
if(!SymbolInfoTick(_Symbol, attachTick) || attachTick.bid <= 0.0)
| |||
{
| |||
Print("[CodeLock] Cannot get initial tick");
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
AttachMsc = attachTick.time_msc;
| |||
CursorMsc = AttachMsc;
| |||
CursorSameMsc = 0;
| |||
| |||
//--- Подготовка массивов
| |||
ArrayResize(TickPrices, MathMax(100, HistoryGraphTicks), MathMax(100, HistoryGraphTicks));
| |||
ArrayResize(ModelTickPrices, MathMax(100, MaxModelHistoryTicks), MathMax(100, MaxModelHistoryTicks));
| |||
ArrayResize(ModelTickAsks, MathMax(100, MaxModelHistoryTicks), MathMax(100, MaxModelHistoryTicks));
| |||
ArrayResize(SignalMarks, 0);
| |||
| |||
ModelTickCount = 0;
| |||
ModelTickHead = 0;
| |||
TickCount = 0;
| |||
TickHead = 0;
| |||
TickSerial = 0;
| |||
| |||
HavePreviousBid = false;
| |||
PreviousBid = 0.0;
| |||
BootstrapComplete = false;
| |||
LiveMode = false;
| |||
| |||
//--- Viewport
| |||
ScaleInitialized = false;
| |||
AutoScaleInitialized = false;
| |||
VerticalScaleManual = false;
| |||
VerticalScaleHalfRange = 0.0;
| |||
ViewSpan = MathMin(MathMax(InitialViewTicks, MinViewTicks), HistoryGraphTicks);
| |||
ViewEndOffset = 0;
| |||
InitialViewLayoutActive = true;
| |||
InitialArrowHistoryScalePending = true;
| |||
| |||
DragActive = false;
| |||
DragStartX = 0;
| |||
DragStartOffset = 0;
| |||
MouseInTickSubwindow = false;
| |||
ResetButtonHeld = false;
| |||
| |||
//--- Мышь
| |||
PreviousChartMouseScroll = (bool)ChartGetInteger(0, CHART_MOUSE_SCROLL);
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, true);
| |||
ChartSetInteger(0, CHART_EVENT_MOUSE_MOVE, true);
| |||
ChartSetInteger(0, CHART_EVENT_MOUSE_WHEEL, true);
| |||
| |||
//--- Таймер
| |||
int timerMs = MathMax(10, MathMin(2000, UpdateIntervalMs));
| |||
if(!EventSetMillisecondTimer(timerMs))
| |||
{
| |||
Print("[CodeLock] EventSetMillisecondTimer failed. Error=", GetLastError());
| |||
return INIT_FAILED;
| |||
}
| |||
| |||
Print("[CodeLock] Init OK. Rows=", TickLevelRowCount,
| |||
" GraphLevel=", GraphLevelsPerSide,
| |||
" GraphPerSide=", GraphDisplayedLevelCount,
| |||
" SubWindow=", SubWindow);
| |||
| |||
return INIT_SUCCEEDED;
| |||
}
| |||
| |||
void OnDeinit(const int reason)
| |||
{
| |||
Print("[CodeLock] Deinit. Reason=", reason);
| |||
| |||
EventKillTimer();
| |||
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, (long)PreviousChartMouseScroll);
| |||
| |||
if(CanvasReady)
| |||
{
| |||
TickCanvas.Destroy();
| |||
CanvasReady = false;
| |||
}
| |||
| |||
ObjectsDeleteAll(0, DASH_PREFIX);
| |||
ObjectsDeleteAll(0, PANEL_PREFIX);
| |||
}
| |||
| |||
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[])
| |||
{
| |||
if(rates_total < 10) return rates_total;
| |||
| |||
if(!BootstrapComplete)
| |||
{
| |||
if(!BootstrapHistory())
| |||
return rates_total;
| |||
}
| |||
else
| |||
{
| |||
if(!ProcessLiveTicks())
| |||
return rates_total;
| |||
}
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol, current) && current.bid > 0.0)
| |||
{
| |||
if(EnsureCanvas())
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending = false;
| |||
}
| |||
}
| |||
| |||
ChartRedraw(0);
| |||
return rates_total;
| |||
}
| |||
| |||
void OnTimer()
| |||
{
| |||
if(SubWindow < 0)
| |||
{
| |||
SubWindow = ChartWindowFind(0, "CodeLock");
| |||
if(SubWindow < 0) return;
| |||
}
| |||
| |||
SyncTickVisualRowsFromDashboard();
| |||
| |||
if(!BootstrapComplete)
| |||
{
| |||
if(!BootstrapHistory())
| |||
return;
| |||
}
| |||
else
| |||
{
| |||
if(!ProcessLiveTicks())
| |||
return;
| |||
}
| |||
| |||
MqlTick current;
| |||
if(!SymbolInfoTick(_Symbol, current) || current.bid <= 0.0) return;
| |||
| |||
if(EnsureCanvas())
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending = false;
| |||
}
| |||
| |||
DashRenderDashboard(current.bid);
| |||
| |||
ChartRedraw(0);
| |||
}
| |||
| |||
void OnChartEvent(const int id,
| |||
const long &lparam,
| |||
const double &dparam,
| |||
const string &sparam)
| |||
{
| |||
DashChartEvent(id, lparam, dparam, sparam);
| |||
| |||
if(id == CHARTEVENT_OBJECT_CLICK)
| |||
{
| |||
if(sparam == PANEL_PREFIX + "ZOOM_MINUS" ||
| |||
sparam == PANEL_PREFIX + "ZOOM_PLUS" ||
| |||
sparam == PANEL_PREFIX + "RESET_CURRENT")
| |||
{
| |||
HandleZoomButtonClick(sparam);
| |||
return;
| |||
}
| |||
}
| |||
| |||
if(id == CHARTEVENT_MOUSE_WHEEL)
| |||
{
| |||
int flags = (int)(lparam >> 32);
| |||
int xCur = (int)(short)lparam;
| |||
int yCur = (int)(short)(lparam >> 16);
| |||
| |||
UpdateMouseScrollTarget(yCur);
| |||
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(xCur, (int)dparam);
| |||
}
| |||
| |||
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, false);
| |||
HandleVerticalScaleDrag(y);
| |||
DragActive = false;
| |||
VerticalPanDragActive = false;
| |||
return;
| |||
}
| |||
EndVerticalScaleDrag();
| |||
UpdateMouseScrollTarget(y);
| |||
}
| |||
else if(VerticalPanDragActive)
| |||
{
| |||
if(leftDown)
| |||
{
| |||
ChartSetInteger(0, CHART_MOUSE_SCROLL, 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(PANEL_PREFIX + "RESET_CURRENT", x, y))
| |||
{
| |||
ResetButtonHeld = true;
| |||
ObjectSetInteger(0, PANEL_PREFIX + "RESET_CURRENT", OBJPROP_STATE, true);
| |||
}
| |||
else if(!leftDown)
| |||
{
| |||
ResetButtonHeld = false;
| |||
if(ObjectFind(0, PANEL_PREFIX + "RESET_CURRENT") >= 0)
| |||
ObjectSetInteger(0, PANEL_PREFIX + "RESET_CURRENT", OBJPROP_STATE, false);
| |||
}
| |||
| |||
if(leftDown && MouseInTickSubwindow && !ResetButtonHeld)
| |||
{
| |||
if(!DragActive && !VerticalPanDragActive)
| |||
{
| |||
if(!PointerDragTracking)
| |||
{
| |||
PointerDragTracking = true;
| |||
DragStartX = x;
| |||
DragStartOffset = ViewEndOffset;
| |||
VerticalPanDragStartY = y;
| |||
}
| |||
| |||
int dx = x - DragStartX;
| |||
int dy = y - VerticalPanDragStartY;
| |||
| |||
if(MathAbs((double)dy) > MathAbs((double)dx) && MathAbs(dy) >= 4)
| |||
{
| |||
BeginVerticalPanDrag(y);
| |||
DragActive = false;
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(MathAbs(dx) >= 4)
| |||
DragActive = true;
| |||
}
| |||
| |||
if(VerticalPanDragActive)
| |||
{
| |||
HandleVerticalPanDrag(y);
| |||
return;
| |||
}
| |||
| |||
if(DragActive)
| |||
{
| |||
HandleHorizontalDrag(x);
| |||
RefreshGraph();
| |||
}
| |||
}
| |||
else if(!leftDown || !MouseInTickSubwindow)
| |||
{
| |||
DragActive = false;
| |||
PointerDragTracking = false;
| |||
EndVerticalPanDrag();
| |||
}
| |||
| |||
return;
| |||
}
| |||
| |||
if(id == CHARTEVENT_CHART_CHANGE)
| |||
{
| |||
static int lastPeriod = (int)_Period;
| |||
bool periodChanged = ((int)_Period != lastPeriod);
| |||
lastPeriod = (int)_Period;
| |||
| |||
int currentSubWindow = ChartWindowFind(0, "CodeLock");
| |||
if(currentSubWindow >= 0)
| |||
SubWindow = currentSubWindow;
| |||
else
| |||
return;
| |||
| |||
if(periodChanged)
| |||
{
| |||
ViewSpan = MathMin(MathMax(InitialViewTicks, MinViewTicks),
| |||
MathMax(1, TickCount));
| |||
ViewEndOffset = 0;
| |||
InitialViewLayoutActive = true;
| |||
FitInitialArrowHistory();
| |||
}
| |||
| |||
if(!BootstrapComplete) return;
| |||
| |||
MqlTick current;
| |||
if(SymbolInfoTick(_Symbol, current) && current.bid > 0.0)
| |||
{
| |||
if(EnsureCanvas())
| |||
{
| |||
UpdateSubwindowScale(current.bid);
| |||
RenderGraph(current.bid);
| |||
InitialArrowHistoryScalePending = false;
| |||
}
| |||
}
| |||
ChartRedraw(0);
| |||
}
| |||
}
| |||
//+------------------------------------------------------------------+
|