//+------------------------------------------------------------------+
//| MA_Cross_Touch_Break_Alert.mq5                                   |
//| Product: 移動平均線クロス・タッチ・ブレイク複合アラート          |
//| Version: 1.000                                                   |
//+------------------------------------------------------------------+
// Disclaimer:
// This indicator notifies moving average cross, touch, and break events.
// These alerts are not trading signals.
// This indicator does not guarantee profit or trading performance.
// Users are responsible for their own trading decisions.
//
// 免責:
// 本インジケータは、移動平均線のクロス・タッチ・ブレイクを通知する補助ツールです。
// 表示される矢印やアラートは売買シグナルではありません。
// 本インジケータは利益や取引成果を保証するものではありません。
// 最終的な売買判断は利用者自身の責任で行ってください。
//+------------------------------------------------------------------+
#property strict
#property indicator_chart_window
#property indicator_buffers 3
#property indicator_plots   3
#property version   "1.000"

#property indicator_label1  "MA1"
#property indicator_type1   DRAW_LINE
#property indicator_color1  clrDodgerBlue
#property indicator_width1  1

#property indicator_label2  "MA2"
#property indicator_type2   DRAW_LINE
#property indicator_color2  clrOrange
#property indicator_width2  1

#property indicator_label3  "MA3"
#property indicator_type3   DRAW_LINE
#property indicator_color3  clrSilver
#property indicator_width3  1

input string InpInstanceId = ""; // インスタンスID
input int InpMA1Period = 20; // MA1期間
input int InpMA2Period = 75; // MA2期間
input int InpMA3Period = 200; // MA3期間

input ENUM_MA_METHOD InpMAMethod = MODE_SMA; // 移動平均の種類
input ENUM_APPLIED_PRICE InpAppliedPrice = PRICE_CLOSE; // 適用価格

input bool InpShowMA1 = true; // MA1を表示
input bool InpShowMA2 = true; // MA2を表示
input bool InpShowMA3 = true; // MA3を表示

input color InpMA1Color = clrDodgerBlue; // MA1色
input color InpMA2Color = clrOrange; // MA2色
input color InpMA3Color = clrSilver; // MA3色

input int InpMA1Width = 1; // MA1太さ
input int InpMA2Width = 1; // MA2太さ
input int InpMA3Width = 1; // MA3太さ

input bool InpEnableCrossAlert = true; // MAクロスアラートを有効化
input bool InpUseClosedBarOnly = true; // 確定足のみで判定
input bool InpShowCrossArrows = true; // クロス矢印を表示
input color InpGoldenCrossColor = clrLime; // ゴールデンクロス色
input color InpDeadCrossColor = clrTomato; // デッドクロス色
input int InpArrowOffsetPoints = 100; // 矢印オフセット

input bool InpEnableTouchAlert = true; // MAタッチアラートを有効化
input bool InpTouchMA1 = true; // MA1タッチを判定
input bool InpTouchMA2 = true; // MA2タッチを判定
input bool InpTouchMA3 = true; // MA3タッチを判定
input int InpTouchTolerancePoints = 0; // タッチ許容幅ポイント

input bool InpEnableBreakAlert = false; // MAブレイクアラートを有効化
input bool InpBreakMA1 = false; // MA1ブレイクを判定
input bool InpBreakMA2 = true; // MA2ブレイクを判定
input bool InpBreakMA3 = true; // MA3ブレイクを判定

input bool InpShowTouchMarkers = false; // タッチマーカーを表示
input bool InpShowBreakMarkers = false; // ブレイクマーカーを表示

input int InpMinAlertIntervalBars = 3; // 同種アラートの最小間隔バー数

input bool InpUsePopupAlert = true; // ポップアップ通知
input bool InpUseSoundAlert = true; // サウンド通知
input bool InpUsePushNotification = false; // プッシュ通知
input bool InpUseEmailNotification = false; // メール通知
input string InpSoundFile = "alert.wav"; // サウンドファイル

input bool InpShowDebugInfo = false; // デバッグ情報を表示

string PREFIX_BASE = "KDEV_MA_CTB_";

double g_ma1_buffer[];
double g_ma2_buffer[];
double g_ma3_buffer[];

int g_ma1_handle = INVALID_HANDLE;
int g_ma2_handle = INVALID_HANDLE;
int g_ma3_handle = INVALID_HANDLE;

datetime g_last_golden_cross_bar = 0;
datetime g_last_dead_cross_bar = 0;
datetime g_last_touch_bar[3] = {0, 0, 0};
datetime g_last_break_up_bar[3] = {0, 0, 0};
datetime g_last_break_down_bar[3] = {0, 0, 0};

//+------------------------------------------------------------------+
//| Helpers                                                          |
//+------------------------------------------------------------------+
void DebugLog(const string message)
  {
   if(InpShowDebugInfo)
      Print("[MA Alert] ", message);
  }

int SafeWidth(const int width)
  {
   return MathMax(1, MathMin(5, width));
  }

bool ValidateInputs()
  {
   if(InpMA1Period <= 0 || InpMA2Period <= 0 || InpMA3Period <= 0)
     {
      Print("MA periods must be greater than 0");
      return false;
     }

   if(InpMA1Width < 1 || InpMA1Width > 5 ||
      InpMA2Width < 1 || InpMA2Width > 5 ||
      InpMA3Width < 1 || InpMA3Width > 5)
     {
      Print("MA line widths must be between 1 and 5");
      return false;
     }

   if(InpMinAlertIntervalBars < 0)
     {
      Print("Minimum alert interval bars must be 0 or greater");
      return false;
     }

   if(InpTouchTolerancePoints < 0)
     {
      Print("Touch tolerance points must be 0 or greater");
      return false;
     }

   return true;
  }

void ReleaseMAHandles()
  {
   if(g_ma1_handle != INVALID_HANDLE)
     {
      IndicatorRelease(g_ma1_handle);
      g_ma1_handle = INVALID_HANDLE;
     }

   if(g_ma2_handle != INVALID_HANDLE)
     {
      IndicatorRelease(g_ma2_handle);
      g_ma2_handle = INVALID_HANDLE;
     }

   if(g_ma3_handle != INVALID_HANDLE)
     {
      IndicatorRelease(g_ma3_handle);
      g_ma3_handle = INVALID_HANDLE;
     }
  }

bool CreateMAHandles()
  {
   g_ma1_handle = iMA(_Symbol, _Period, InpMA1Period, 0, InpMAMethod, InpAppliedPrice);
   if(g_ma1_handle == INVALID_HANDLE)
     {
      Print("Failed to create MA1 handle. error=", GetLastError());
      return false;
     }

   g_ma2_handle = iMA(_Symbol, _Period, InpMA2Period, 0, InpMAMethod, InpAppliedPrice);
   if(g_ma2_handle == INVALID_HANDLE)
     {
      Print("Failed to create MA2 handle. error=", GetLastError());
      ReleaseMAHandles();
      return false;
     }

   g_ma3_handle = iMA(_Symbol, _Period, InpMA3Period, 0, InpMAMethod, InpAppliedPrice);
   if(g_ma3_handle == INVALID_HANDLE)
     {
      Print("Failed to create MA3 handle. error=", GetLastError());
      ReleaseMAHandles();
      return false;
     }

   return true;
  }

string TimeframeText()
  {
   switch((ENUM_TIMEFRAMES)_Period)
     {
      case PERIOD_M1:
         return "M1";
      case PERIOD_M2:
         return "M2";
      case PERIOD_M3:
         return "M3";
      case PERIOD_M4:
         return "M4";
      case PERIOD_M5:
         return "M5";
      case PERIOD_M6:
         return "M6";
      case PERIOD_M10:
         return "M10";
      case PERIOD_M12:
         return "M12";
      case PERIOD_M15:
         return "M15";
      case PERIOD_M20:
         return "M20";
      case PERIOD_M30:
         return "M30";
      case PERIOD_H1:
         return "H1";
      case PERIOD_H2:
         return "H2";
      case PERIOD_H3:
         return "H3";
      case PERIOD_H4:
         return "H4";
      case PERIOD_H6:
         return "H6";
      case PERIOD_H8:
         return "H8";
      case PERIOD_H12:
         return "H12";
      case PERIOD_D1:
         return "D1";
      case PERIOD_W1:
         return "W1";
      case PERIOD_MN1:
         return "MN1";
     }

   return EnumToString((ENUM_TIMEFRAMES)_Period);
  }

string TimeKey(const datetime value)
  {
   string key = TimeToString(value, TIME_DATE | TIME_MINUTES);
   StringReplace(key, ".", "");
   StringReplace(key, ":", "");
   StringReplace(key, " ", "_");
   return key;
  }

string SanitizeObjectToken(string value)
  {
   if(StringLen(value) == 0)
      value = "DEFAULT";

   StringReplace(value, " ", "_");
   StringReplace(value, ".", "_");
   StringReplace(value, ":", "_");
   StringReplace(value, "/", "_");
   StringReplace(value, "\\", "_");
   StringReplace(value, "-", "_");
   return value;
  }

string InstanceKey()
  {
   if(StringLen(InpInstanceId) > 0)
      return SanitizeObjectToken(InpInstanceId);

   return "MA" + IntegerToString(InpMA1Period) + "_" +
          IntegerToString(InpMA2Period) + "_" +
          IntegerToString(InpMA3Period) + "_" +
          IntegerToString((int)InpMAMethod);
  }

string ObjectPrefix()
  {
   return PREFIX_BASE + IntegerToString(ChartID()) + "_" +
          SanitizeObjectToken(_Symbol) + "_" +
          TimeframeText() + "_" + InstanceKey() + "_";
  }

bool IsValidMAValue(const double value)
  {
   return (value != EMPTY_VALUE && value != 0.0);
  }

bool CanAlert(const datetime last_alert_bar, const datetime event_bar)
  {
   if(event_bar <= 0)
      return false;

   if(last_alert_bar == 0)
      return true;

   if(last_alert_bar == event_bar)
      return false;

   if(InpMinAlertIntervalBars <= 0)
      return true;

   int last_shift = iBarShift(_Symbol, _Period, last_alert_bar, true);
   int event_shift = iBarShift(_Symbol, _Period, event_bar, true);

   if(last_shift < 0 || event_shift < 0)
      return true;

   return (MathAbs(last_shift - event_shift) >= InpMinAlertIntervalBars);
  }

void SendAlertMessage(const string message)
  {
   if(InpUsePopupAlert)
      Alert(message);

   if(InpUseSoundAlert)
      PlaySound(InpSoundFile);

   if(InpUsePushNotification)
      SendNotification(message);

   if(InpUseEmailNotification)
      SendMail("MA Cross Touch Alert", message);
  }

bool CreateArrowObject(const string name,
                       const datetime event_time,
                       const double price,
                       const color arrow_color,
                       const int arrow_code,
                       const int width)
  {
   if(ObjectFind(0, name) >= 0)
      return true;

   ResetLastError();
   if(!ObjectCreate(0, name, OBJ_ARROW, 0, event_time, price))
     {
      if(InpShowDebugInfo)
         Print("ObjectCreate failed name=", name, " error=", GetLastError());
      return false;
     }

   ObjectSetInteger(0, name, OBJPROP_ARROWCODE, arrow_code);
   ObjectSetInteger(0, name, OBJPROP_COLOR, arrow_color);
   ObjectSetInteger(0, name, OBJPROP_WIDTH, width);
   ObjectSetInteger(0, name, OBJPROP_SELECTABLE, false);
   ObjectSetInteger(0, name, OBJPROP_HIDDEN, true);

   return true;
  }

void RemoveAllObjectsByPrefix(const string prefix)
  {
   int windows = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL, 0);

   for(int w = windows - 1; w >= 0; w--)
     {
      int total = ObjectsTotal(0, w, -1);

      for(int i = total - 1; i >= 0; i--)
        {
         string name = ObjectName(0, i, w, -1);

         if(StringFind(name, prefix) == 0)
            ObjectDelete(0, name);
        }
     }
  }

color MAColor(const int ma_index)
  {
   if(ma_index == 0)
      return InpMA1Color;

   if(ma_index == 1)
      return InpMA2Color;

   return InpMA3Color;
  }

int MAPeriod(const int ma_index)
  {
   if(ma_index == 0)
      return InpMA1Period;

   if(ma_index == 1)
      return InpMA2Period;

   return InpMA3Period;
  }

string MALabel(const int ma_index)
  {
   return "MA" + IntegerToString(ma_index + 1);
  }

//+------------------------------------------------------------------+
//| Drawing and alert evaluation                                     |
//+------------------------------------------------------------------+
void ProcessCrossAlert(const datetime &time[],
                       const double &high[],
                       const double &low[],
                       const double &ma1[],
                       const double &ma2[],
                       const int current_shift,
                       const int previous_shift)
  {
   if(!InpEnableCrossAlert)
      return;

   if(!IsValidMAValue(ma1[current_shift]) || !IsValidMAValue(ma1[previous_shift]) ||
      !IsValidMAValue(ma2[current_shift]) || !IsValidMAValue(ma2[previous_shift]))
      return;

   datetime event_time = time[current_shift];
   double offset = InpArrowOffsetPoints * _Point;

   bool golden_cross = (ma1[previous_shift] <= ma2[previous_shift] &&
                        ma1[current_shift] > ma2[current_shift]);
   bool dead_cross = (ma1[previous_shift] >= ma2[previous_shift] &&
                      ma1[current_shift] < ma2[current_shift]);

   if(golden_cross && CanAlert(g_last_golden_cross_bar, event_time))
     {
      if(InpShowCrossArrows)
        {
         string name = ObjectPrefix() + "GC_" + TimeKey(event_time);
         CreateArrowObject(name, event_time, low[current_shift] - offset, InpGoldenCrossColor, 233, 2);
        }

      string message = StringFormat("[MA Alert] %s %s Golden Cross MA%d/MA%d",
                                    _Symbol,
                                    TimeframeText(),
                                    InpMA1Period,
                                    InpMA2Period);
      SendAlertMessage(message);
      g_last_golden_cross_bar = event_time;
      DebugLog(message);
     }

   if(dead_cross && CanAlert(g_last_dead_cross_bar, event_time))
     {
      if(InpShowCrossArrows)
        {
         string name = ObjectPrefix() + "DC_" + TimeKey(event_time);
         CreateArrowObject(name, event_time, high[current_shift] + offset, InpDeadCrossColor, 234, 2);
        }

      string message = StringFormat("[MA Alert] %s %s Dead Cross MA%d/MA%d",
                                    _Symbol,
                                    TimeframeText(),
                                    InpMA1Period,
                                    InpMA2Period);
      SendAlertMessage(message);
      g_last_dead_cross_bar = event_time;
      DebugLog(message);
     }
  }

void ProcessTouchForMA(const int ma_index,
                       const datetime &time[],
                       const double &high[],
                       const double &low[],
                       const double ma_value,
                       const int current_shift,
                       const double tolerance)
  {
   if(!IsValidMAValue(ma_value))
      return;

   datetime event_time = time[current_shift];
   bool touched = (low[current_shift] - tolerance <= ma_value &&
                   ma_value <= high[current_shift] + tolerance);

   if(!touched || !CanAlert(g_last_touch_bar[ma_index], event_time))
      return;

   if(InpShowTouchMarkers)
     {
      string name = ObjectPrefix() + "TOUCH_" + MALabel(ma_index) + "_" + TimeKey(event_time);
      CreateArrowObject(name, event_time, ma_value, MAColor(ma_index), 159, 1);
     }

   string message = StringFormat("[MA Alert] %s %s Price touched MA%d",
                                 _Symbol,
                                 TimeframeText(),
                                 MAPeriod(ma_index));
   SendAlertMessage(message);
   g_last_touch_bar[ma_index] = event_time;
   DebugLog(message);
  }

void ProcessTouchAlerts(const datetime &time[],
                        const double &high[],
                        const double &low[],
                        const double &ma1[],
                        const double &ma2[],
                        const double &ma3[],
                        const int current_shift)
  {
   if(!InpEnableTouchAlert)
      return;

   double tolerance = InpTouchTolerancePoints * _Point;

   if(InpTouchMA1)
      ProcessTouchForMA(0, time, high, low, ma1[current_shift], current_shift, tolerance);

   if(InpTouchMA2)
      ProcessTouchForMA(1, time, high, low, ma2[current_shift], current_shift, tolerance);

   if(InpTouchMA3)
      ProcessTouchForMA(2, time, high, low, ma3[current_shift], current_shift, tolerance);
  }

void ProcessBreakForMA(const int ma_index,
                       const datetime &time[],
                       const double &close[],
                       const double &ma[],
                       const int current_shift,
                       const int previous_shift)
  {
   if(!IsValidMAValue(ma[current_shift]) || !IsValidMAValue(ma[previous_shift]))
      return;

   datetime event_time = time[current_shift];
   bool broke_up = (close[previous_shift] <= ma[previous_shift] &&
                    close[current_shift] > ma[current_shift]);
   bool broke_down = (close[previous_shift] >= ma[previous_shift] &&
                      close[current_shift] < ma[current_shift]);

   if(broke_up && CanAlert(g_last_break_up_bar[ma_index], event_time))
     {
      if(InpShowBreakMarkers)
        {
         string name = ObjectPrefix() + "BREAK_UP_" + MALabel(ma_index) + "_" + TimeKey(event_time);
         CreateArrowObject(name, event_time, ma[current_shift], InpGoldenCrossColor, 233, 1);
        }

      string message = StringFormat("[MA Alert] %s %s Close broke above MA%d",
                                    _Symbol,
                                    TimeframeText(),
                                    MAPeriod(ma_index));
      SendAlertMessage(message);
      g_last_break_up_bar[ma_index] = event_time;
      DebugLog(message);
     }

   if(broke_down && CanAlert(g_last_break_down_bar[ma_index], event_time))
     {
      if(InpShowBreakMarkers)
        {
         string name = ObjectPrefix() + "BREAK_DOWN_" + MALabel(ma_index) + "_" + TimeKey(event_time);
         CreateArrowObject(name, event_time, ma[current_shift], InpDeadCrossColor, 234, 1);
        }

      string message = StringFormat("[MA Alert] %s %s Close broke below MA%d",
                                    _Symbol,
                                    TimeframeText(),
                                    MAPeriod(ma_index));
      SendAlertMessage(message);
      g_last_break_down_bar[ma_index] = event_time;
      DebugLog(message);
     }
  }

void ProcessBreakAlerts(const datetime &time[],
                        const double &close[],
                        const double &ma1[],
                        const double &ma2[],
                        const double &ma3[],
                        const int current_shift,
                        const int previous_shift)
  {
   if(!InpEnableBreakAlert)
      return;

   if(InpBreakMA1)
      ProcessBreakForMA(0, time, close, ma1, current_shift, previous_shift);

   if(InpBreakMA2)
      ProcessBreakForMA(1, time, close, ma2, current_shift, previous_shift);

   if(InpBreakMA3)
      ProcessBreakForMA(2, time, close, ma3, current_shift, previous_shift);
  }

//+------------------------------------------------------------------+
//| Indicator lifecycle                                              |
//+------------------------------------------------------------------+
int OnInit()
  {
   if(!ValidateInputs())
      return INIT_PARAMETERS_INCORRECT;

   SetIndexBuffer(0, g_ma1_buffer, INDICATOR_DATA);
   SetIndexBuffer(1, g_ma2_buffer, INDICATOR_DATA);
   SetIndexBuffer(2, g_ma3_buffer, INDICATOR_DATA);

   ArraySetAsSeries(g_ma1_buffer, true);
   ArraySetAsSeries(g_ma2_buffer, true);
   ArraySetAsSeries(g_ma3_buffer, true);

   PlotIndexSetInteger(0, PLOT_DRAW_TYPE, DRAW_LINE);
   PlotIndexSetInteger(1, PLOT_DRAW_TYPE, DRAW_LINE);
   PlotIndexSetInteger(2, PLOT_DRAW_TYPE, DRAW_LINE);

   PlotIndexSetInteger(0, PLOT_LINE_COLOR, InpMA1Color);
   PlotIndexSetInteger(1, PLOT_LINE_COLOR, InpMA2Color);
   PlotIndexSetInteger(2, PLOT_LINE_COLOR, InpMA3Color);

   PlotIndexSetInteger(0, PLOT_LINE_WIDTH, SafeWidth(InpMA1Width));
   PlotIndexSetInteger(1, PLOT_LINE_WIDTH, SafeWidth(InpMA2Width));
   PlotIndexSetInteger(2, PLOT_LINE_WIDTH, SafeWidth(InpMA3Width));

   PlotIndexSetDouble(0, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(1, PLOT_EMPTY_VALUE, EMPTY_VALUE);
   PlotIndexSetDouble(2, PLOT_EMPTY_VALUE, EMPTY_VALUE);

   PlotIndexSetString(0, PLOT_LABEL, "MA1(" + IntegerToString(InpMA1Period) + ")");
   PlotIndexSetString(1, PLOT_LABEL, "MA2(" + IntegerToString(InpMA2Period) + ")");
   PlotIndexSetString(2, PLOT_LABEL, "MA3(" + IntegerToString(InpMA3Period) + ")");

   IndicatorSetString(INDICATOR_SHORTNAME, "MA複合アラート");

   if(!CreateMAHandles())
      return INIT_FAILED;

   return INIT_SUCCEEDED;
  }

void OnDeinit(const int reason)
  {
   ReleaseMAHandles();
   RemoveAllObjectsByPrefix(ObjectPrefix());
   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[])
  {
   ArraySetAsSeries(time, true);
   ArraySetAsSeries(high, true);
   ArraySetAsSeries(low, true);
   ArraySetAsSeries(close, true);

   int required_bars = (InpUseClosedBarOnly ? 3 : 2);
   if(rates_total < required_bars)
      return prev_calculated;

   int calculated = MathMin(BarsCalculated(g_ma1_handle),
                            MathMin(BarsCalculated(g_ma2_handle), BarsCalculated(g_ma3_handle)));

   if(calculated <= 0)
      return prev_calculated;

   int copy_count = rates_total;
   if(prev_calculated > 0 && prev_calculated <= rates_total)
      copy_count = rates_total - prev_calculated + 3;

   copy_count = MathMax(required_bars, copy_count);
   copy_count = MathMin(copy_count, MathMin(rates_total, calculated));

   if(copy_count < required_bars)
      return prev_calculated;

   double ma1_values[];
   double ma2_values[];
   double ma3_values[];

   ArraySetAsSeries(ma1_values, true);
   ArraySetAsSeries(ma2_values, true);
   ArraySetAsSeries(ma3_values, true);

   if(CopyBuffer(g_ma1_handle, 0, 0, copy_count, ma1_values) <= 0)
      return prev_calculated;

   if(CopyBuffer(g_ma2_handle, 0, 0, copy_count, ma2_values) <= 0)
      return prev_calculated;

   if(CopyBuffer(g_ma3_handle, 0, 0, copy_count, ma3_values) <= 0)
      return prev_calculated;

   if(prev_calculated == 0)
     {
      for(int i = 0; i < rates_total; i++)
        {
         g_ma1_buffer[i] = EMPTY_VALUE;
         g_ma2_buffer[i] = EMPTY_VALUE;
         g_ma3_buffer[i] = EMPTY_VALUE;
        }
     }

   for(int i = 0; i < copy_count; i++)
     {
      g_ma1_buffer[i] = InpShowMA1 ? ma1_values[i] : EMPTY_VALUE;
      g_ma2_buffer[i] = InpShowMA2 ? ma2_values[i] : EMPTY_VALUE;
      g_ma3_buffer[i] = InpShowMA3 ? ma3_values[i] : EMPTY_VALUE;
     }

   int current_shift = InpUseClosedBarOnly ? 1 : 0;
   int previous_shift = InpUseClosedBarOnly ? 2 : 1;

   // 初回読み込みでは履歴を描画するだけで、過去のイベントを通知しない。
   if(prev_calculated > 0)
     {
      ProcessCrossAlert(time, high, low, ma1_values, ma2_values, current_shift, previous_shift);
      ProcessTouchAlerts(time, high, low, ma1_values, ma2_values, ma3_values, current_shift);
      ProcessBreakAlerts(time, close, ma1_values, ma2_values, ma3_values, current_shift, previous_shift);
     }

   return rates_total;
  }
