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