//+------------------------------------------------------------------+
//|                                           AngleTrendLineKeep.mq5 |
//|                 Trend line by angle, on-screen angle preserved  |
//|                  when the chart timeframe or zoom is changed.   |
//+------------------------------------------------------------------+
#property copyright "© 2026, Ryan Lawrence Johnson"
#property link      "https://www.mql5.com/en/users/rjo"
#property version   "1.00"
#property description "Keeps the on-screen angle of a trend line across chart timeframe and zoom changes."
#property indicator_chart_window
#property indicator_buffers 1
#property indicator_plots   1
//--- plot ATL (invisible: the trend line itself is a chart object)
#property indicator_label1  "AngleTrendLine"
#property indicator_type1   DRAW_NONE

//--------------------------------------------------------------------
//  What this indicator does
//  ------------------------
//  Draws ONE OBJ_TREND line whose ON-SCREEN incline is specified by
//  an angle in degrees, and keeps that on-screen angle when the
//  user changes the chart timeframe or zooms the chart.
//
//  How the angle is preserved
//  --------------------------
//  Anchor 1 (the "anchor bar shift point") is a fixed (time, price)
//  point taken from the bar that is InpAnchorShift bars to the left
//  of the current bar when the indicator is attached. Anchor 1 never
//  moves as long as the indicator is attached.
//
//  Anchor 2 is recomputed every time the visible scale changes:
//     p2 = p1 + InpLengthBars * ppb * tan(angle) / ppp
//  where ppb is the current pixels-per-bar and ppp the current
//  pixels-per-price of the visible chart. Because the segment always
//  spans InpLengthBars bars of the current timeframe, applying this
//  formula on the CURRENT scale makes the line keep exactly the same
//  pixel angle, no matter how the chart is zoomed or which timeframe
//  is selected. The visual result is that the line stays "glued" to
//  the same physical incline on screen.
//
//  Recalculation triggers
//  ----------------------
//  - OnCalculate(): every new quote, and right after the indicator
//    re-initializes on a symbol/timeframe change.
//  - OnChartEvent()/CHARTEVENT_CHART_CHANGE: zoom, scroll, autoscale
//    and window resize, which do not generate new quotes.
//
//  Notes
//  -----
//  - Only one line is drawn: the OBJ_TREND chart object. The indicator
//    plot is invisible (DRAW_NONE); its buffer only mirrors the segment
//    so the line prices are available in the Data Window.
//  - When the chart timeframe is changed the terminal reloads the
//    indicator, so the anchor re-establishes at the same bar-shift
//    distance on the new timeframe; the angle is preserved either way.
//  - Zooming or scrolling never moves anchor 1.
//  - The object is owned by the indicator (default prefix differs from
//    the AngleTrendLineStatic script) and is deleted with it.
//--------------------------------------------------------------------

enum ENUM_ANCHOR_PRICE
  {
   ANCHOR_OPEN  = 0,   // Open of anchor bar
   ANCHOR_CLOSE = 1,   // Close of anchor bar
   ANCHOR_HIGH  = 2,   // High of anchor bar
   ANCHOR_LOW   = 3    // Low of anchor bar
  };

input double            InpAngleDeg    = 15.0;          // Angle deg, +up/-down. 180-deg span: -90..+90. 12-decimals
input int               InpLengthBars  = 100;           // Segment length in bars
input int               InpAnchorShift = 50;            // Anchor bar shift (0 = current bar)
input ENUM_ANCHOR_PRICE InpPriceMode   = ANCHOR_OPEN;   // Anchor price source
input color             InpColor       = clrDodgerBlue; // Line color
input ENUM_LINE_STYLE   InpStyle       = STYLE_SOLID;   // Line style
input int               InpWidth       = 2;             // Line width (1-5)
input bool              InpRayRight    = true;          // Extend ray to the right
input string            InpPrefix      = "AngLineKeep_"; // Object name prefix

//--- indicator buffers
double         BufferTA[];
//--- indicator state

datetime       g_t1;                 // fixed anchor 1 time (anchor bar shift point)
double         g_p1;                 // fixed anchor 1 price
datetime       g_prevT2;             // last applied segment end time
double         g_prevP2;             // last applied segment end price
long           g_lastW   = -1;       // last seen chart width
long           g_lastH   = -1;       // last seen chart height
long           g_lastBars= -1;       // last seen visible bars
double         g_lastPmax= -1.0;     // last seen price max
double         g_lastPmin= -1.0;     // last seen price min
bool           g_ready   = false;    // anchor 1 has been resolved

//+------------------------------------------------------------------+
//| Custom indicator initialization function                         |
//+------------------------------------------------------------------+
int OnInit()
  {
//--- clear stale objects from a previous attach / timeframe, so only
//--- one line with this InpPrefix stays on the chart
   if(InpPrefix != "")
      ObjectsDeleteAll(0, InpPrefix);

//--- validate inputs
   if(InpLengthBars <= 0)
     {
      Print("InpLengthBars must be greater than 0.");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(InpAnchorShift < 0)
     {
      Print("InpAnchorShift must be >= 0.");
      return INIT_PARAMETERS_INCORRECT;
     }
   if(MathAbs(InpAngleDeg) >= 90.0)
     {
      Print("InpAngleDeg must be in (-90, +90) degrees: the full 180-degree span, exclusive at vertical.");
      return INIT_PARAMETERS_INCORRECT;
     }

//--- indicator buffers mapping
   SetIndexBuffer(0, BufferTA, INDICATOR_DATA);
//--- setting indicator parameters
   IndicatorSetString(INDICATOR_SHORTNAME, "AngleTrendLineKeep");

//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| Custom indicator deinitialization function                       |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
   if(InpPrefix != "")
      ObjectsDeleteAll(0, InpPrefix);
   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Custom indicator iteration function                              |
//+------------------------------------------------------------------+
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[])
  {
   UpdateLine(rates_total);
//--- return value of prev_calculated for next call
   return(rates_total);
  }
//+------------------------------------------------------------------+
//| Chart event handler: catches zoom / scroll / autoscale, which    |
//| do not generate new quotes and therefore miss OnCalculate.       |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
   if(id == CHARTEVENT_CHART_CHANGE)
      UpdateLine(Bars(_Symbol, _Period));
  }
//+------------------------------------------------------------------+
//| Recompute the segment end for the CURRENT chart scale so that    |
//| the on-screen angle stays equal to InpAngleDeg. No-op when the   |
//| visible scale (bars and price range) has not changed.            |
//+------------------------------------------------------------------+
void UpdateLine(const int rates_total)
  {
//--- current chart pixel scale
   const long   chartW    = ChartGetInteger(0, CHART_WIDTH_IN_PIXELS);
   const long   chartH    = ChartGetInteger(0, CHART_HEIGHT_IN_PIXELS);
   const long   widthBars = ChartGetInteger(0, CHART_WIDTH_IN_BARS);
   const double priceMax  = ChartGetDouble(0, CHART_PRICE_MAX);
   const double priceMin  = ChartGetDouble(0, CHART_PRICE_MIN);

   if(chartW <= 0 || chartH <= 0 || widthBars <= 0 || priceMax <= priceMin)
      return;

//--- scale/timeframe unchanged since last update: nothing to do
   if(g_ready &&
      g_lastW    == chartW    && g_lastH    == chartH    && g_lastBars == widthBars &&
      g_lastPmax == priceMax  && g_lastPmin == priceMin)
      return;

   g_lastW     = chartW;
   g_lastH     = chartH;
   g_lastBars  = widthBars;
   g_lastPmax  = priceMax;
   g_lastPmin  = priceMin;

//--- anchor 1: fixed at the anchor bar shift point (once per attach)
   if(!g_ready)
     {
      const datetime t1 = iTime(_Symbol, _Period, InpAnchorShift);
      if(t1 <= 0)
         return;
      double p1 = 0.0;
      switch(InpPriceMode)
        {
         case ANCHOR_OPEN:  p1 = iOpen (_Symbol, _Period, InpAnchorShift); break;
         case ANCHOR_CLOSE: p1 = iClose(_Symbol, _Period, InpAnchorShift); break;
         case ANCHOR_HIGH:  p1 = iHigh (_Symbol, _Period, InpAnchorShift); break;
         case ANCHOR_LOW:   p1 = iLow  (_Symbol, _Period, InpAnchorShift); break;
        }
      if(p1 <= 0.0)
         p1 = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(p1 <= 0.0)
         return;
      g_t1    = t1;
      g_p1    = p1;
      g_ready = true;
     }

//--- segment end time: InpLengthBars bars of the current timeframe
   const long periodSec = (long)PeriodSeconds(_Period);
   if(periodSec <= 0)
      return;
   const datetime t2 = g_t1 + (datetime)((long)InpLengthBars * periodSec);

//--- transform the requested angle into a price for the CURRENT scale
   const double ppb    = (double)chartW / (double)widthBars;      // pixels per one bar
   const double ppp    = (double)chartH / (priceMax - priceMin);  // pixels per one price unit
   const double dxPix  = (double)InpLengthBars * ppb;             // horizontal pixel span
   const double dyPix  = dxPix * MathTan(InpAngleDeg * M_PI / 180.0); // vertical pixel span
   double       p2     = g_p1 + dyPix / ppp;                      // positive angle => price rises
   if(p2 <= 0.0)
      return;

//--- near-vertical angles: keep anchor 2 inside a sane band so the chart auto-scale stays usable
   const double priceSpan = priceMax - priceMin;
   const double clampBand = 20.0 * priceSpan;
   const bool   clamped   = MathAbs(p2 - g_p1) > clampBand;
   if(clamped)
      p2 = (p2 > g_p1) ? g_p1 + clampBand : g_p1 - clampBand;

//--- object name: unique for this anchor (same anchor implies the same object)
   const string name = InpPrefix + _Symbol + "_" + IntegerToString(g_t1) + "_" + DoubleToString(g_p1, _Digits);

//--- geometry and object unchanged? nothing to do
   if(g_prevT2 == t2 && MathAbs(g_prevP2 - p2) < 0.0000000001 && ObjectFind(0, name) >= 0)
      return;

//--- create the trend line, or move its end to the recomputed point
   if(ObjectFind(0, name) < 0)
     {
      ResetLastError();
      if(!ObjectCreate(0, name, OBJ_TREND, 0, g_t1, g_p1, t2, p2))
        {
         PrintFormat("ObjectCreate failed with error %d.", GetLastError());
         return;
        }
      ObjectSetInteger(0, name, OBJPROP_COLOR,      InpColor);
      ObjectSetInteger(0, name, OBJPROP_STYLE,      InpStyle);
      ObjectSetInteger(0, name, OBJPROP_WIDTH,      InpWidth);
      ObjectSetInteger(0, name, OBJPROP_RAY_RIGHT,  InpRayRight);
      ObjectSetInteger(0, name, OBJPROP_BACK,       false);
      ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
      ObjectSetInteger(0, name, OBJPROP_HIDDEN,     false);
      if(clamped)
         Print("Note: angle is near vertical; anchor-2 price was clamped to keep the chart auto-scale usable.");
     }
   else
     {
      ObjectSetInteger(0, name, OBJPROP_TIME,  1, (long)t2);
      ObjectSetDouble (0, name, OBJPROP_PRICE, 1, p2);
     }

   g_prevT2 = t2;
   g_prevP2 = p2;

//--- refresh the invisible plot buffer (values visible in the Data Window)
   FillBuffer(rates_total, t2, p2);

   ChartRedraw();
  }
//+------------------------------------------------------------------+
//| Fill the indicator buffer with the segment prices between the    |
//| two anchors; EMPTY_VALUE outside the segment.                    |
//+------------------------------------------------------------------+
void FillBuffer(const int rates_total, const datetime t2, const double p2)
  {
   if(rates_total <= 0 || !g_ready)
      return;
   datetime time[];
   ArrayResize(time, rates_total);
   if(CopyTime(_Symbol, _Period, 0, rates_total, time) != rates_total)
      return;
   const double span = (double)(t2 - g_t1);
   if(span <= 0.0)
      return;
   for(int i = 0; i < rates_total; i++)
     {
      if(time[i] >= g_t1 && time[i] <= t2)
        {
         BufferTA[i] = g_p1 + (double)(time[i] - g_t1) * (p2 - g_p1) / span;
        }
      else
         BufferTA[i] = EMPTY_VALUE;
     }
  }
//+------------------------------------------------------------------+