//+------------------------------------------------------------------+
//| 68_Daily_Line_Coverage_Auditor_v1_00.mq5                         |
//| Measures how much of a daily range equally spaced lines cover,   |
//| and how often confirmed swing points land inside those bands.    |
//+------------------------------------------------------------------+
#property copyright "FXおもしろラボ"
#property version   "1.00"
#property strict
#property description "Daily line coverage versus confirmed swing hit rate."

#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots   4

#property indicator_label1  "Swing high in band"
#property indicator_type1   DRAW_ARROW
#property indicator_color1  clrAqua
#property indicator_width1  2

#property indicator_label2  "Swing high off band"
#property indicator_type2   DRAW_ARROW
#property indicator_color2  clrOrangeRed
#property indicator_width2  2

#property indicator_label3  "Swing low in band"
#property indicator_type3   DRAW_ARROW
#property indicator_color3  clrAqua
#property indicator_width3  2

#property indicator_label4  "Swing low off band"
#property indicator_type4   DRAW_ARROW
#property indicator_color4  clrOrangeRed
#property indicator_width4  2

input int    InpDays          = 10;   // 検査する確定日数
input int    InpLineCount     = 5;    // 1日あたりのライン本数
input double InpTolerancePips = 5.0;  // ラインの許容幅(±pips)
input int    InpSwingBars     = 2;    // スイング判定の左右本数
input int    InpMaxScanBars   = 4000; // 走査するバー数の上限
input bool   InpDrawBands     = true; // 帯と中心線を描画する

const string OBJECT_PREFIX = "MQLP68_";

struct DayRange
{
   int    newest;   // 時系列インデックス（小さいほど新しい）
   int    oldest;
   double high;
   double low;
};

double g_high_hit[];
double g_high_off[];
double g_low_hit[];
double g_low_off[];

double PipSize()
{
   const int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
   const double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
   if(point <= 0.0)
      return 0.0;
   return (digits == 3 || digits == 5) ? point * 10.0 : point;
}

int DayKey(const datetime value)
{
   MqlDateTime parts;
   TimeToStruct(value, parts);
   return parts.year * 10000 + parts.mon * 100 + parts.day;
}

// 等間隔ラインの中心価格。日足レンジをライン本数で等分した各区間の中央に置く。
double LinePrice(const double low, const double range, const int index)
{
   return low + range * (index + 0.5) / InpLineCount;
}

// 帯の重なりとレンジ外をのぞいた実効カバー幅。
double MergedBandWidth(
   const double &band_low[],
   const double &band_high[],
   const double range_low,
   const double range_high)
{
   double total = 0.0;
   double open_low = 0.0;
   double open_high = 0.0;
   bool   open = false;
   const int count = ArraySize(band_low);

   for(int index = 0; index < count; ++index)
   {
      const double clipped_low = MathMax(band_low[index], range_low);
      const double clipped_high = MathMin(band_high[index], range_high);
      if(clipped_high <= clipped_low)
         continue;

      if(!open)
      {
         open_low = clipped_low;
         open_high = clipped_high;
         open = true;
         continue;
      }
      if(clipped_low <= open_high)
      {
         open_high = MathMax(open_high, clipped_high);
         continue;
      }
      total += open_high - open_low;
      open_low = clipped_low;
      open_high = clipped_high;
   }
   if(open)
      total += open_high - open_low;
   return total;
}

bool IsInsideBand(
   const double &band_low[],
   const double &band_high[],
   const double price)
{
   const int count = ArraySize(band_low);
   for(int index = 0; index < count; ++index)
   {
      if(price >= band_low[index] && price <= band_high[index])
         return true;
   }
   return false;
}

bool IsSwingHigh(const double &high[], const int index, const int total)
{
   if(index < InpSwingBars || index + InpSwingBars >= total)
      return false;
   for(int offset = 1; offset <= InpSwingBars; ++offset)
   {
      if(high[index] <= high[index - offset])
         return false;
      if(high[index] <= high[index + offset])
         return false;
   }
   return true;
}

bool IsSwingLow(const double &low[], const int index, const int total)
{
   if(index < InpSwingBars || index + InpSwingBars >= total)
      return false;
   for(int offset = 1; offset <= InpSwingBars; ++offset)
   {
      if(low[index] >= low[index - offset])
         return false;
      if(low[index] >= low[index + offset])
         return false;
   }
   return true;
}

void DrawDayBands(
   const int day_index,
   const DayRange &day,
   const datetime &time[],
   const double &band_low[],
   const double &band_high[])
{
   const datetime start = time[day.oldest];
   const datetime end = time[day.newest] + PeriodSeconds();
   const int count = ArraySize(band_low);

   for(int index = 0; index < count; ++index)
   {
      const string band_name =
         StringFormat("%sband_%d_%d", OBJECT_PREFIX, day_index, index);
      if(ObjectCreate(
            0,
            band_name,
            OBJ_RECTANGLE,
            0,
            start,
            band_low[index],
            end,
            band_high[index]))
      {
         ObjectSetInteger(0, band_name, OBJPROP_COLOR, clrDarkSlateGray);
         ObjectSetInteger(0, band_name, OBJPROP_FILL, true);
         ObjectSetInteger(0, band_name, OBJPROP_BACK, true);
         ObjectSetInteger(0, band_name, OBJPROP_SELECTABLE, false);
         ObjectSetInteger(0, band_name, OBJPROP_HIDDEN, true);
      }

      const double center = (band_low[index] + band_high[index]) * 0.5;
      const string line_name =
         StringFormat("%sline_%d_%d", OBJECT_PREFIX, day_index, index);
      if(ObjectCreate(
            0,
            line_name,
            OBJ_TREND,
            0,
            start,
            center,
            end,
            center))
      {
         ObjectSetInteger(0, line_name, OBJPROP_COLOR, clrSilver);
         ObjectSetInteger(0, line_name, OBJPROP_STYLE, STYLE_DOT);
         ObjectSetInteger(0, line_name, OBJPROP_RAY_RIGHT, false);
         ObjectSetInteger(0, line_name, OBJPROP_BACK, true);
         ObjectSetInteger(0, line_name, OBJPROP_SELECTABLE, false);
         ObjectSetInteger(0, line_name, OBJPROP_HIDDEN, true);
      }
   }
}

int OnInit()
{
   if(InpDays < 1 || InpLineCount < 1 || InpLineCount > 50 ||
      InpTolerancePips <= 0.0 || InpSwingBars < 1 || InpSwingBars > 50 ||
      InpMaxScanBars < 100)
      return INIT_PARAMETERS_INCORRECT;
   if(PipSize() <= 0.0)
      return INIT_FAILED;

   SetIndexBuffer(0, g_high_hit, INDICATOR_DATA);
   SetIndexBuffer(1, g_high_off, INDICATOR_DATA);
   SetIndexBuffer(2, g_low_hit, INDICATOR_DATA);
   SetIndexBuffer(3, g_low_off, INDICATOR_DATA);
   ArraySetAsSeries(g_high_hit, true);
   ArraySetAsSeries(g_high_off, true);
   ArraySetAsSeries(g_low_hit, true);
   ArraySetAsSeries(g_low_off, true);

   for(int plot = 0; plot < 4; ++plot)
   {
      PlotIndexSetInteger(plot, PLOT_ARROW, 159);
      PlotIndexSetDouble(plot, PLOT_EMPTY_VALUE, EMPTY_VALUE);
      PlotIndexSetInteger(plot, PLOT_ARROW_SHIFT, plot < 2 ? -12 : 12);
   }

   IndicatorSetInteger(INDICATOR_DIGITS, _Digits);
   IndicatorSetString(
      INDICATOR_SHORTNAME,
      StringFormat(
         "Line coverage auditor v1.00 (%d days, %d lines, +-%.1f pips)",
         InpDays,
         InpLineCount,
         InpTolerancePips));
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   ObjectsDeleteAll(0, OBJECT_PREFIX);
   Comment("");
}

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 < InpSwingBars * 2 + 10)
      return 0;
   // 集計はバー確定ごとに1回だけやり直す。
   if(prev_calculated == rates_total)
      return rates_total;

   ArraySetAsSeries(time, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);

   ArrayInitialize(g_high_hit, EMPTY_VALUE);
   ArrayInitialize(g_high_off, EMPTY_VALUE);
   ArrayInitialize(g_low_hit, EMPTY_VALUE);
   ArrayInitialize(g_low_off, EMPTY_VALUE);
   ObjectsDeleteAll(0, OBJECT_PREFIX);

   const int scan_limit = MathMin(rates_total, InpMaxScanBars);
   const int today = DayKey(time[0]);
   int index = 1;
   while(index < scan_limit && DayKey(time[index]) == today)
      ++index;

   DayRange days[];
   ArrayResize(days, 0);
   while(index < scan_limit && ArraySize(days) < InpDays)
   {
      const int key = DayKey(time[index]);
      DayRange day;
      day.newest = index;
      day.oldest = index;
      day.high = high[index];
      day.low = low[index];

      bool closed = false;
      while(index < scan_limit)
      {
         if(DayKey(time[index]) != key)
         {
            closed = true;
            break;
         }
         day.high = MathMax(day.high, high[index]);
         day.low = MathMin(day.low, low[index]);
         day.oldest = index;
         ++index;
      }
      // 走査上限で切れた日は不完全なので採用しない。
      if(!closed)
         break;

      const int size = ArraySize(days);
      ArrayResize(days, size + 1);
      days[size] = day;
   }

   const double tolerance = InpTolerancePips * PipSize();
   double band_low[];
   double band_high[];
   ArrayResize(band_low, InpLineCount);
   ArrayResize(band_high, InpLineCount);

   int    days_used = 0;
   double coverage_sum = 0.0;
   double range_sum = 0.0;
   int    high_total = 0;
   int    high_hit = 0;
   int    low_total = 0;
   int    low_hit = 0;
   const int day_count = ArraySize(days);

   for(int day_index = 0; day_index < day_count; ++day_index)
   {
      const double range = days[day_index].high - days[day_index].low;
      if(range <= 0.0)
         continue;

      for(int line = 0; line < InpLineCount; ++line)
      {
         const double center = LinePrice(days[day_index].low, range, line);
         band_low[line] = center - tolerance;
         band_high[line] = center + tolerance;
      }

      const double covered =
         MergedBandWidth(
            band_low,
            band_high,
            days[day_index].low,
            days[day_index].high);
      coverage_sum += covered / range;
      range_sum += range;
      ++days_used;

      for(int bar = days[day_index].oldest; bar >= days[day_index].newest; --bar)
      {
         if(IsSwingHigh(high, bar, rates_total))
         {
            ++high_total;
            if(IsInsideBand(band_low, band_high, high[bar]))
            {
               g_high_hit[bar] = high[bar];
               ++high_hit;
            }
            else
               g_high_off[bar] = high[bar];
         }
         if(IsSwingLow(low, bar, rates_total))
         {
            ++low_total;
            if(IsInsideBand(band_low, band_high, low[bar]))
            {
               g_low_hit[bar] = low[bar];
               ++low_hit;
            }
            else
               g_low_off[bar] = low[bar];
         }
      }

      if(InpDrawBands)
         DrawDayBands(day_index, days[day_index], time, band_low, band_high);
   }

   const int swing_total = high_total + low_total;
   const int swing_hit = high_hit + low_hit;
   const double coverage =
      days_used > 0 ? coverage_sum / days_used * 100.0 : 0.0;
   const double hit_rate =
      swing_total > 0 ? (double)swing_hit / swing_total * 100.0 : 0.0;
   const double lift = coverage > 0.0 ? hit_rate / coverage : 0.0;
   const double pip = PipSize();
   const double mean_range_pips =
      (days_used > 0 && pip > 0.0) ? range_sum / days_used / pip : 0.0;

   Comment(
      "#68 DAILY LINE COVERAGE AUDITOR\n",
      StringFormat("DAYS %d / LINES %d / TOL +-%.1f pips\n",
                   days_used, InpLineCount, InpTolerancePips),
      StringFormat("MEAN DAILY RANGE %.1f pips\n", mean_range_pips),
      StringFormat("BAND COVERAGE %.1f%% of range\n", coverage),
      StringFormat("SWINGS %d (high %d / low %d)\n",
                   swing_total, high_total, low_total),
      StringFormat("IN BAND %d -> HIT RATE %.1f%%\n", swing_hit, hit_rate),
      StringFormat("LIFT (hit / coverage) %.2fx\n", lift),
      "AQUA = IN BAND / RED = OFF BAND");

   PrintFormat(
      "line coverage auditor: days=%d lines=%d tolerance_pips=%.1f mean_range_pips=%.1f coverage_pct=%.2f swings=%d swing_high=%d swing_low=%d in_band=%d hit_rate_pct=%.2f lift=%.3f",
      days_used,
      InpLineCount,
      InpTolerancePips,
      mean_range_pips,
      coverage,
      swing_total,
      high_total,
      low_total,
      swing_hit,
      hit_rate,
      lift);

   ChartRedraw(0);
   return rates_total;
}
//+------------------------------------------------------------------+
