| //+------------------------------------------------------------------+
|
| //| 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);
|
| }
|
| }
|
| //+------------------------------------------------------------------+ |