//+------------------------------------------------------------------+
//| 30_News_Reaction_Analyzer_v1_00.mq5                            |
//| Measures a completed M1 price window after a specified event.   |
//+------------------------------------------------------------------+
#property copyright "FXおもしろラボ"
#property version   "1.00"
#property strict
#property description "Measures 5-minute and 60-minute price reactions after a specified chart time."

#property indicator_chart_window
#property indicator_buffers 6
#property indicator_plots   6

#property indicator_label1 "Reference price"
#property indicator_type1  DRAW_NONE
#property indicator_label2 "Initial close move (pips)"
#property indicator_type2  DRAW_NONE
#property indicator_label3 "Maximum up excursion (pips)"
#property indicator_type3  DRAW_NONE
#property indicator_label4 "Maximum down excursion (pips)"
#property indicator_type4  DRAW_NONE
#property indicator_label5 "Window close move (pips)"
#property indicator_type5  DRAW_NONE
#property indicator_label6 "Returned to reference"
#property indicator_type6  DRAW_NONE

input datetime InpEventTime          = D'2026.07.20 14:00'; // 発表時刻（チャート時刻）
input int      InpInitialMinutes     = 5;                    // 初動を測る分数
input int      InpMeasureMinutes     = 60;                   // 全体を測る分数
input double   InpReturnTolerancePips = 0.5;                 // 戻り判定の許容幅
input bool     InpShowPanel          = true;                 // チャートに結果を表示

const string OBJECT_PREFIX = "MQL30_NewsReaction_";

double g_reference_buffer[];
double g_initial_move_buffer[];
double g_max_up_buffer[];
double g_max_down_buffer[];
double g_window_move_buffer[];
double g_returned_buffer[];

bool   g_ready = false;
double g_reference_price = EMPTY_VALUE;
double g_initial_move_pips = EMPTY_VALUE;
double g_max_up_pips = EMPTY_VALUE;
double g_max_down_pips = EMPTY_VALUE;
double g_window_move_pips = EMPTY_VALUE;
bool   g_returned = false;
MqlRates g_event_rates[];
int    g_max_up_index = -1;
int    g_max_down_index = -1;

double PipSize();
bool CalculateReaction();
void PublishBuffers();
void DrawAnalysis();
void UpdatePanel(const string status_text);
void SetLabel(const string suffix, const int y, const string text, const color text_color, const int font_size);
void DeleteObjects();

int OnInit()
{
   if(InpEventTime <= 0 || InpInitialMinutes < 1 ||
      InpMeasureMinutes <= InpInitialMinutes || InpMeasureMinutes > 1440 ||
      InpReturnTolerancePips < 0.0 || InpReturnTolerancePips > 100.0)
   {
      Print("News reaction analyzer: input parameters are invalid.");
      return INIT_PARAMETERS_INCORRECT;
   }

   bool buffers_ok = true;
   if(!SetIndexBuffer(0, g_reference_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!SetIndexBuffer(1, g_initial_move_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!SetIndexBuffer(2, g_max_up_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!SetIndexBuffer(3, g_max_down_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!SetIndexBuffer(4, g_window_move_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!SetIndexBuffer(5, g_returned_buffer, INDICATOR_DATA)) buffers_ok = false;
   if(!buffers_ok)
   {
      PrintFormat("News reaction analyzer: SetIndexBuffer failed, error=%d", GetLastError());
      return INIT_FAILED;
   }

   ArraySetAsSeries(g_reference_buffer, true);
   ArraySetAsSeries(g_initial_move_buffer, true);
   ArraySetAsSeries(g_max_up_buffer, true);
   ArraySetAsSeries(g_max_down_buffer, true);
   ArraySetAsSeries(g_window_move_buffer, true);
   ArraySetAsSeries(g_returned_buffer, true);
   for(int plot = 0; plot < 6; ++plot)
      PlotIndexSetDouble(plot, PLOT_EMPTY_VALUE, EMPTY_VALUE);

   IndicatorSetString(
      INDICATOR_SHORTNAME,
      "News Reaction Analyzer (" + TimeToString(InpEventTime, TIME_DATE | TIME_MINUTES) + ")"
   );
   EventSetTimer(1);
   if(!CalculateReaction() && InpShowPanel)
      UpdatePanel("WAITING FOR COMPLETE M1 DATA");
   return INIT_SUCCEEDED;
}

void OnDeinit(const int reason)
{
   EventKillTimer();
   DeleteObjects();
}

void OnTimer()
{
   if(!g_ready)
      CalculateReaction();
   PublishBuffers();
   if(InpShowPanel)
      UpdatePanel(g_ready ? "ANALYSIS COMPLETE" : "WAITING FOR COMPLETE M1 DATA");
   ChartRedraw(0);
}

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 < 1)
      return 0;
   if(prev_calculated == 0)
   {
      ArrayInitialize(g_reference_buffer, EMPTY_VALUE);
      ArrayInitialize(g_initial_move_buffer, EMPTY_VALUE);
      ArrayInitialize(g_max_up_buffer, EMPTY_VALUE);
      ArrayInitialize(g_max_down_buffer, EMPTY_VALUE);
      ArrayInitialize(g_window_move_buffer, EMPTY_VALUE);
      ArrayInitialize(g_returned_buffer, EMPTY_VALUE);
   }
   if(!g_ready)
      CalculateReaction();
   PublishBuffers();
   return rates_total;
}

double PipSize()
{
   return (_Digits == 3 || _Digits == 5) ? _Point * 10.0 : _Point;
}

bool CalculateReaction()
{
   const datetime last_bar_time = InpEventTime + (InpMeasureMinutes - 1) * 60;
   if(TimeCurrent() < last_bar_time + 60)
      return false;

   ResetLastError();
   const int event_shift = iBarShift(_Symbol, PERIOD_M1, InpEventTime, true);
   if(event_shift < 0)
      return false;

   MqlRates reference_rate[];
   ArraySetAsSeries(reference_rate, false);
   const datetime reference_time = InpEventTime - 60;
   ResetLastError();
   const int reference_copied = CopyRates(
      _Symbol,
      PERIOD_M1,
      reference_time,
      reference_time,
      reference_rate
   );
   if(reference_copied != 1 || ArraySize(reference_rate) != 1 ||
      reference_rate[0].time != reference_time)
      return false;

   ArraySetAsSeries(g_event_rates, false);
   ResetLastError();
   const int copied = CopyRates(
      _Symbol,
      PERIOD_M1,
      InpEventTime,
      last_bar_time,
      g_event_rates
   );
   if(copied != InpMeasureMinutes || ArraySize(g_event_rates) != InpMeasureMinutes)
      return false;
   if(g_event_rates[0].time != InpEventTime ||
      g_event_rates[InpMeasureMinutes - 1].time != last_bar_time)
      return false;
   for(int i = 1; i < InpMeasureMinutes; ++i)
   {
      if(g_event_rates[i].time != g_event_rates[i - 1].time + 60)
         return false;
   }

   const double pip = PipSize();
   if(pip <= 0.0 || reference_rate[0].close <= 0.0)
      return false;

   double max_up = -DBL_MAX;
   double max_down = -DBL_MAX;
   int max_up_index = -1;
   int max_down_index = -1;
   for(int i = 0; i < InpMeasureMinutes; ++i)
   {
      if(g_event_rates[i].high <= 0.0 || g_event_rates[i].low <= 0.0 ||
         g_event_rates[i].close <= 0.0)
         return false;
      const double up = (g_event_rates[i].high - reference_rate[0].close) / pip;
      const double down = (reference_rate[0].close - g_event_rates[i].low) / pip;
      if(up > max_up)
      {
         max_up = up;
         max_up_index = i;
      }
      if(down > max_down)
      {
         max_down = down;
         max_down_index = i;
      }
   }
   if(max_up_index < 0 || max_down_index < 0)
      return false;

   const int initial_index = InpInitialMinutes - 1;
   if(initial_index < 0 || initial_index >= InpMeasureMinutes)
      return false;

   bool returned = false;
   const double tolerance = InpReturnTolerancePips * pip;
   for(int i = InpInitialMinutes; i < InpMeasureMinutes; ++i)
   {
      if(g_event_rates[i].high >= reference_rate[0].close - tolerance &&
         g_event_rates[i].low <= reference_rate[0].close + tolerance)
      {
         returned = true;
         break;
      }
   }

   g_reference_price = reference_rate[0].close;
   g_initial_move_pips = (g_event_rates[initial_index].close - g_reference_price) / pip;
   g_max_up_pips = max_up;
   g_max_down_pips = max_down;
   g_window_move_pips = (g_event_rates[InpMeasureMinutes - 1].close - g_reference_price) / pip;
   g_returned = returned;
   g_max_up_index = max_up_index;
   g_max_down_index = max_down_index;
   g_ready = true;

   DrawAnalysis();
   PrintFormat(
      "News reaction analyzer: ready event=%s initial=%.1f max_up=%.1f max_down=%.1f window=%.1f returned=%s",
      TimeToString(InpEventTime, TIME_DATE | TIME_MINUTES),
      g_initial_move_pips,
      g_max_up_pips,
      g_max_down_pips,
      g_window_move_pips,
      g_returned ? "true" : "false"
   );
   return true;
}

void PublishBuffers()
{
   if(ArraySize(g_reference_buffer) < 1)
      return;
   if(!g_ready)
   {
      g_reference_buffer[0] = EMPTY_VALUE;
      g_initial_move_buffer[0] = EMPTY_VALUE;
      g_max_up_buffer[0] = EMPTY_VALUE;
      g_max_down_buffer[0] = EMPTY_VALUE;
      g_window_move_buffer[0] = EMPTY_VALUE;
      g_returned_buffer[0] = EMPTY_VALUE;
      return;
   }
   g_reference_buffer[0] = g_reference_price;
   g_initial_move_buffer[0] = g_initial_move_pips;
   g_max_up_buffer[0] = g_max_up_pips;
   g_max_down_buffer[0] = g_max_down_pips;
   g_window_move_buffer[0] = g_window_move_pips;
   g_returned_buffer[0] = g_returned ? 1.0 : 0.0;
}

void DrawAnalysis()
{
   DeleteObjects();
   const datetime end_time = InpEventTime + InpMeasureMinutes * 60;
   double window_high = g_event_rates[g_max_up_index].high;
   double window_low = g_event_rates[g_max_down_index].low;
   const double padding = MathMax((window_high - window_low) * 0.08, PipSize() * 2.0);

   const string zone_name = OBJECT_PREFIX + "Window";
   ObjectCreate(0, zone_name, OBJ_RECTANGLE, 0, InpEventTime, window_low - padding, end_time, window_high + padding);
   ObjectSetInteger(0, zone_name, OBJPROP_COLOR, C'18,45,58');
   ObjectSetInteger(0, zone_name, OBJPROP_FILL, true);
   ObjectSetInteger(0, zone_name, OBJPROP_BACK, true);
   ObjectSetInteger(0, zone_name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, zone_name, OBJPROP_HIDDEN, true);

   const string event_name = OBJECT_PREFIX + "Event";
   ObjectCreate(0, event_name, OBJ_VLINE, 0, InpEventTime, 0.0);
   ObjectSetInteger(0, event_name, OBJPROP_COLOR, clrGold);
   ObjectSetInteger(0, event_name, OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, event_name, OBJPROP_SELECTABLE, false);
   ObjectSetString(0, event_name, OBJPROP_TEXT, "EVENT");

   const string end_name = OBJECT_PREFIX + "End";
   ObjectCreate(0, end_name, OBJ_VLINE, 0, end_time, 0.0);
   ObjectSetInteger(0, end_name, OBJPROP_COLOR, clrDarkTurquoise);
   ObjectSetInteger(0, end_name, OBJPROP_STYLE, STYLE_DOT);
   ObjectSetInteger(0, end_name, OBJPROP_SELECTABLE, false);

   const string reference_name = OBJECT_PREFIX + "Reference";
   ObjectCreate(0, reference_name, OBJ_HLINE, 0, 0, g_reference_price);
   ObjectSetInteger(0, reference_name, OBJPROP_COLOR, clrWhite);
   ObjectSetInteger(0, reference_name, OBJPROP_STYLE, STYLE_DASH);
   ObjectSetInteger(0, reference_name, OBJPROP_SELECTABLE, false);
   ObjectSetString(0, reference_name, OBJPROP_TEXT, "PRE-EVENT CLOSE");

   const string high_name = OBJECT_PREFIX + "MaxUp";
   ObjectCreate(0, high_name, OBJ_ARROW_DOWN, 0, g_event_rates[g_max_up_index].time, window_high);
   ObjectSetInteger(0, high_name, OBJPROP_COLOR, clrLime);
   ObjectSetInteger(0, high_name, OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, high_name, OBJPROP_SELECTABLE, false);

   const string low_name = OBJECT_PREFIX + "MaxDown";
   ObjectCreate(0, low_name, OBJ_ARROW_UP, 0, g_event_rates[g_max_down_index].time, window_low);
   ObjectSetInteger(0, low_name, OBJPROP_COLOR, clrTomato);
   ObjectSetInteger(0, low_name, OBJPROP_WIDTH, 2);
   ObjectSetInteger(0, low_name, OBJPROP_SELECTABLE, false);

   if(InpShowPanel)
      UpdatePanel("ANALYSIS COMPLETE");
}

void UpdatePanel(const string status_text)
{
   const string panel = OBJECT_PREFIX + "Panel";
   if(ObjectFind(0, panel) < 0)
      ObjectCreate(0, panel, OBJ_RECTANGLE_LABEL, 0, 0, 0);
   ObjectSetInteger(0, panel, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, panel, OBJPROP_XDISTANCE, 18);
   ObjectSetInteger(0, panel, OBJPROP_YDISTANCE, 24);
   ObjectSetInteger(0, panel, OBJPROP_XSIZE, 535);
   ObjectSetInteger(0, panel, OBJPROP_YSIZE, 190);
   ObjectSetInteger(0, panel, OBJPROP_BGCOLOR, C'8,18,14');
   ObjectSetInteger(0, panel, OBJPROP_COLOR, clrDarkTurquoise);
   ObjectSetInteger(0, panel, OBJPROP_BORDER_TYPE, BORDER_FLAT);
   ObjectSetInteger(0, panel, OBJPROP_BACK, false);
   ObjectSetInteger(0, panel, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, panel, OBJPROP_HIDDEN, true);

   SetLabel("Title", 36, "NEWS REACTION ANALYZER", clrLime, 15);
   SetLabel("EventText", 65, "Event: " + TimeToString(InpEventTime, TIME_DATE | TIME_MINUTES) + " (chart time)", clrWhite, 10);
   SetLabel("Status", 88, status_text, g_ready ? clrAqua : clrGold, 11);
   if(!g_ready)
   {
      SetLabel("Initial", 113, "Waiting for exact M1 bars...", clrSilver, 10);
      SetLabel("Excursion", 138, "", clrSilver, 10);
      SetLabel("WindowMove", 163, "", clrSilver, 10);
      SetLabel("Returned", 188, "", clrSilver, 10);
      return;
   }
   SetLabel("Initial", 113, IntegerToString(InpInitialMinutes) + "m close: " + DoubleToString(g_initial_move_pips, 1) + " pips", clrWhite, 11);
   SetLabel("Excursion", 138, "Max up: +" + DoubleToString(g_max_up_pips, 1) + "   Max down: -" + DoubleToString(g_max_down_pips, 1) + " pips", clrWhite, 11);
   SetLabel("WindowMove", 163, IntegerToString(InpMeasureMinutes) + "m close: " + DoubleToString(g_window_move_pips, 1) + " pips", clrWhite, 11);
   SetLabel("Returned", 188, "Returned after initial window: " + (g_returned ? "YES" : "NO"), g_returned ? clrLime : clrTomato, 11);
}

void SetLabel(
   const string suffix,
   const int y,
   const string text,
   const color text_color,
   const int font_size
)
{
   const string name = OBJECT_PREFIX + suffix;
   if(ObjectFind(0, name) < 0)
      ObjectCreate(0, name, OBJ_LABEL, 0, 0, 0);
   ObjectSetInteger(0, name, OBJPROP_CORNER, CORNER_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_XDISTANCE, 34);
   ObjectSetInteger(0, name, OBJPROP_YDISTANCE, y);
   ObjectSetInteger(0, name, OBJPROP_COLOR, text_color);
   ObjectSetInteger(0, name, OBJPROP_FONTSIZE, font_size);
   ObjectSetInteger(0, name, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);
   ObjectSetString(0, name, OBJPROP_FONT, "Consolas");
   ObjectSetString(0, name, OBJPROP_TEXT, text);
}

void DeleteObjects()
{
   ObjectsDeleteAll(0, OBJECT_PREFIX);
}
