//+------------------------------------------------------------------+ //| ORB.mq5 | //| Copyright 2025 © YourRealName | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "© 2025 YourRealName" #property link "https://www.mql5.com/en/users/yourprofile" #property version "2.10" #property description "Opening Range Breakout (ORB) Indicator with DST support" #property indicator_chart_window #property indicator_plots 0 //+------------------------------------------------------------------+ //| Enumeraciones | //+------------------------------------------------------------------+ enum ENUM_ORB_PERIOD { ORB_5MIN = 5, // 5 minutos ORB_15MIN = 15, // 15 minutos ORB_30MIN = 30, // 30 minutos ORB_CUSTOM = 0 // Personalizado }; enum ENUM_MARKET_SESSION { SESSION_CUSTOM = 0, // Personalizado SESSION_SYDNEY = 1, // Sydney (00:00-06:00 UTC) SESSION_TOKYO = 2, // Tokyo (00:00-06:00 UTC) SESSION_LONDON = 3, // London (08:00-16:30 UTC) SESSION_NEWYORK = 4, // New York (14:30-21:00 UTC) SESSION_FRANKFURT = 5 // Frankfurt (08:00-16:30 UTC) }; enum ENUM_TIMEZONE { GMT_MINUS_11_MIDWAY = 0, // (GMT -11:00) Midway Island GMT_MINUS_11_SAMOA = 1, // (GMT -11:00) Samoa GMT_MINUS_10_HAWAII = 2, // (GMT -10:00) Hawaii GMT_MINUS_8_ALASKA = 3, // (GMT -8:00) Alaska GMT_MINUS_7_ARIZONA = 4, // (GMT -7:00) Arizona GMT_MINUS_7_BAJA = 5, // (GMT -7:00) Baja California GMT_MINUS_7_PACIFIC = 6, // (GMT -7:00) Pacific Time (US & Canada) GMT_MINUS_6_CHIHUAHUA = 7, // (GMT -6:00) Chihuahua, La Paz, Mazatlan GMT_MINUS_6_GUADALAJARA = 8, // (GMT -6:00) Guadalajara, Mexico City, Monterrey GMT_MINUS_6_MOUNTAIN = 9, // (GMT -6:00) Mountain Time (US & Canada) GMT_MINUS_5_BOGOTA = 10, // (GMT -5:00) Bogota, Lima, Quito GMT_MINUS_5_CENTRAL_AMERICA = 11, // (GMT -5:00) Central America GMT_MINUS_5_CENTRAL_US = 12, // (GMT -5:00) Central Time (US & Canada) GMT_MINUS_5_SASKATCHEWAN = 13, // (GMT -5:00) Saskatchewan GMT_MINUS_4_ASUNCION = 14, // (GMT -4:00) Asuncion GMT_MINUS_4_CARACAS = 15, // (GMT -4:00) Caracas GMT_MINUS_4_CUIABA = 16, // (GMT -4:00) Cuiaba GMT_MINUS_4_EASTERN_US = 17, // (GMT -4:00) Eastern Time (US & Canada) GMT_MINUS_4_GEORGETOWN = 18, // (GMT -4:00) Georgetown, La Paz, Manaus, San Juan GMT_MINUS_4_INDIANA = 19, // (GMT -4:00) Indiana (East) GMT_MINUS_4_SANTIAGO = 20, // (GMT -4:00) Santiago GMT_MINUS_3_ATLANTIC = 21, // (GMT -3:00) Atlantic Time (Canada) GMT_MINUS_3_BRASILIA = 22, // (GMT -3:00) Brasilia GMT_MINUS_3_BUENOS_AIRES = 23, // (GMT -3:00) Buenos Aires GMT_MINUS_3_CAYENNE = 24, // (GMT -3:00) Cayenne, Fortaleza GMT_MINUS_3_MONTEVIDEO = 25, // (GMT -3:00) Montevideo GMT_MINUS_2_30_NEWFOUNDLAND = 26, // (GMT -2:30) Newfoundland GMT_MINUS_2_GREENLAND = 27, // (GMT -2:00) Greenland GMT_MINUS_1_CAPE_VERDE = 28, // (GMT -1:00) Cape Verde Is. GMT_0_AZORES = 29, // (GMT) Azores GMT_0_UTC = 30, // (GMT) Coordinated Universal Time GMT_0_MONROVIA = 31, // (GMT) Monrovia, Reykjavik GMT_PLUS_1_CASABLANCA = 32, // (GMT +1:00) Casablanca GMT_PLUS_1_DUBLIN = 33, // (GMT +1:00) Dublin, Edinburgh, Lisbon, London GMT_PLUS_1_LAGOS = 34, // (GMT +1:00) Lagos GMT_PLUS_1_WEST_AFRICA = 35, // (GMT +1:00) West Central Africa GMT_PLUS_2_AMSTERDAM = 36, // (GMT +2:00) Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna GMT_PLUS_2_BELGRADE = 37, // (GMT +2:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague GMT_PLUS_2_BRUSSELS = 38, // (GMT +2:00) Brussels, Copenhagen, Madrid, Paris GMT_PLUS_2_CAIRO = 39, // (GMT +2:00) Cairo GMT_PLUS_2_JOHANNESBURG = 40, // (GMT +02:00) Johannesburg GMT_PLUS_2_SARAJEVO = 41, // (GMT +2:00) Sarajevo, Skopje, Warsaw, Zagreb GMT_PLUS_2_WINDHOEK = 42, // (GMT +2:00) Windhoek GMT_PLUS_3_AMMAN = 43, // (GMT +3:00) Amman GMT_PLUS_3_BAGHDAD = 44, // (GMT +3:00) Baghdad GMT_PLUS_3_BEIRUT = 45, // (GMT +3:00) Beirut GMT_PLUS_3_DAMASCUS = 46, // (GMT +3:00) Damascus GMT_PLUS_3_HELSINKI = 47, // (GMT +3:00) Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius GMT_PLUS_3_ISTANBUL = 48, // (GMT +3:00) Istanbul GMT_PLUS_3_JERUSALEM = 49, // (GMT +3:00) Jerusalem GMT_PLUS_3_KUWAIT = 50, // (GMT +3:00) Kuwait, Riyadh GMT_PLUS_3_MOSCOW = 51, // (GMT +3:00) Moscow, St. Petersburg, Volgograd GMT_PLUS_3_NAIROBI = 52, // (GMT +3:00) Nairobi GMT_PLUS_3_30_TEHRAN = 53, // (GMT +3:30) Tehran GMT_PLUS_4_ABU_DHABI = 54, // (GMT +4:00) Abu Dhabi, Dubai, Muscat GMT_PLUS_4_BAKU = 55, // (GMT +4:00) Baku GMT_PLUS_4_30_KABUL = 56, // (GMT +4:30) Kabul GMT_PLUS_5_EKATERINBURG = 57, // (GMT +5:00) Ekaterinburg GMT_PLUS_5_KARACHI = 58, // (GMT +5:00) Karachi GMT_PLUS_5_30_CHENNAI = 59, // (GMT +5:30) Chennai, Kolkata, Mumbai, New Delhi GMT_PLUS_5_30_COLOMBO = 60, // (GMT +5:30) Colombo GMT_PLUS_5_45_KATHMANDU = 61, // (GMT +5:45) Kathmandu GMT_PLUS_6_DHAKA = 62, // (GMT +6:00) Dhaka GMT_PLUS_6_30_YANGON = 63, // (GMT +6:30) Yangon (Rangoon) GMT_PLUS_7_BANGKOK = 64, // (GMT +7:00) Bangkok, Hanoi, Jakarta GMT_PLUS_8_BEIJING = 65, // (GMT +8:00) Beijing, Chongqing, Hong Kong, Urumqi GMT_PLUS_8_MANILA = 66, // (GMT +08:00) Manila GMT_PLUS_8_SINGAPORE = 67, // (GMT +8:00) Singapore GMT_PLUS_9_OSAKA = 68, // (GMT +9:00) Osaka, Sapporo, Tokyo GMT_PLUS_9_SEOUL = 69, // (GMT +9:00) Seoul GMT_PLUS_9_30_ADELAIDE = 70, // (GMT +9:30) Adelaide GMT_PLUS_10_BRISBANE = 71, // (GMT +10:00) Brisbane GMT_PLUS_10_CANBERRA = 72, // (GMT +10:00) Canberra, Melbourne, Sydney GMT_PLUS_10_VLADIVOSTOK = 73, // (GMT +10:00) Vladivostok GMT_PLUS_11_SOLOMON = 74, // (GMT +11:00) Solomon Is., New Caledonia GMT_PLUS_12_AUCKLAND = 75, // (GMT +12:00) Auckland, Wellington GMT_PLUS_12_ENIWETOK = 76 // (GMT +12:00) Eniwetok, Kwajalein }; //+------------------------------------------------------------------+ //| Inputs del Indicador | //+------------------------------------------------------------------+ input group "=== Configuración ORB ===" input ENUM_ORB_PERIOD InpORBPeriod = ORB_5MIN; // ORB period input ENUM_MARKET_SESSION InpMarketSession = SESSION_NEWYORK; // Market session input string InpCustomTime = "08:00-08:05"; // Custom time (only if Custom) input ENUM_TIMEZONE InpTimeZone = GMT_MINUS_5_BOGOTA; // GMT time zone input bool InpAlertBreakoutsOnly = false; // Alerts only on confirmed breakouts input bool InpDebugMode = false; // DEBUG MODE input group "=== Visualización ===" input bool InpShowLabels = true; // Show ORB labels input bool InpShowPreviousDays = true; // Show previous days' ORBs input bool InpShowPriceTargets = true; // Show basic targets (50%, 100%) input bool InpShowExtendedTargets = false; // Show extended targets input bool InpShowFibTargets = false; // Show Fibonacci targets (27.2%, 61.8%) input bool InpShowMidPoint = false; // Show midpoint input bool InpShowShadedBox = true; // ENABLE/DISABLE shaded box input int InpBoxBorderSize = 2; // Box border thickness input int InpMaxDaysBack = 5; // Maximum days back input group "=== Breakout Detection ===" input bool InpUseVolumeConfirmation = false; // Use volume confirmation input int InpVolumePeriod = 20; // Period for volume SMA input group "=== Colores y Estilo ===" input color InpORBHighColor = clrLimeGreen; // ORB High line color input color InpORBLowColor = clrRed; // ORB Low line color input color InpShadeColor = clrTeal; // Shaded box color input color InpBorderColor = clrDarkSlateGray; // Box border color input color InpTarget50Color = clrMagenta; // 50% target color input color InpTarget100Color = clrBlue; // 100% target color input color InpTargetOtherColor = clrTeal; // Other targets color input color InpFibColor = clrOrange; // Fibonacci targets color input color InpMidPointColor = clrGray; // Midpoint color input int InpLineWidth = 2; // Line thickness input ENUM_LINE_STYLE InpLineStyle = STYLE_SOLID; // Line style //+------------------------------------------------------------------+ //| Estructura para datos ORB | //+------------------------------------------------------------------+ struct ORBData { datetime date; double high; double low; double range; datetime startTime; datetime endTime; datetime candleTime; bool calculated; int barsFound; bool isFirstCandleMode; // 🔹 NUEVOS CAMPOS PARA BREAKOUT TRACKING bool inBreakout; int breakoutDirection; // 1 = alcista, -1 = bajista, 0 = none datetime breakoutTime; double breakoutPrice; }; //+------------------------------------------------------------------+ //| Estructura para configuración de sesión | //+------------------------------------------------------------------+ struct SessionConfig { string name; string startTime; string endTime; double timezoneOffset; bool isDST; // 🔹 NUEVO: Indicador de horario de verano }; //+------------------------------------------------------------------+ //| Estructura para datos de breakout | //+------------------------------------------------------------------+ struct BreakoutData { bool highCross; bool lowCross; bool highCrossBO; bool lowCrossBO; bool isRetestOrbHigh; bool isRetestOrbLow; bool failedRetest; bool volumeSpike; }; //+------------------------------------------------------------------+ //| Variables Globales | //+------------------------------------------------------------------+ ORBData orbDataArray[]; string orbPrefix = "ORB_"; int orbPeriodMinutes = 15; int chartTimeframeMinutes = 0; SessionConfig currentSession; bool currentInBreakout = false; datetime lastAlertTime = 0; // 🔹 NUEVAS VARIABLES PARA TRACKING DE BREAKOUTS double volumeSMA[]; int volumePeriod = 20; //+------------------------------------------------------------------+ //| Función auxiliar para debug | //+------------------------------------------------------------------+ void DebugPrint(string message) { if(InpDebugMode) Print("🔍 [ORB DEBUG] ", message); } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: Detectar si una fecha está en horario de verano | //+------------------------------------------------------------------+ bool IsDaylightSavingTime(datetime date, ENUM_MARKET_SESSION session) { MqlDateTime dt; TimeToStruct(date, dt); switch(session) { case SESSION_NEWYORK: // Estados Unidos: segundo domingo de marzo hasta primer domingo de noviembre return IsUSADaylightSavingTime(dt); case SESSION_LONDON: case SESSION_FRANKFURT: // Europa: último domingo de marzo hasta último domingo de octubre return IsEuropeDaylightSavingTime(dt); case SESSION_SYDNEY: // Australia: primer domingo de octubre hasta primer domingo de abril (inverso) return IsAustraliaDaylightSavingTime(dt); case SESSION_TOKYO: // Japón no usa horario de verano return false; default: return false; } } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: DST para Estados Unidos | //+------------------------------------------------------------------+ bool IsUSADaylightSavingTime(const MqlDateTime &dt) { // Segundo domingo de marzo hasta primer domingo de noviembre // Antes de marzo = horario estándar if(dt.mon < 3) return false; // Después de noviembre = horario estándar if(dt.mon > 11) return false; // Diciembre = horario estándar if(dt.mon == 12) return false; // Entre abril y octubre = horario de verano if(dt.mon >= 4 && dt.mon <= 10) return true; // Marzo: segundo domingo if(dt.mon == 3) { int secondSunday = GetNthSunday(dt.year, 3, 2); return dt.day >= secondSunday; } // Noviembre: primer domingo if(dt.mon == 11) { int firstSunday = GetNthSunday(dt.year, 11, 1); return dt.day < firstSunday; } return false; } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: DST para Europa | //+------------------------------------------------------------------+ bool IsEuropeDaylightSavingTime(const MqlDateTime &dt) { // Último domingo de marzo hasta último domingo de octubre // Antes de marzo = horario estándar if(dt.mon < 3) return false; // Después de octubre = horario estándar if(dt.mon > 10) return false; // Entre abril y septiembre = horario de verano if(dt.mon >= 4 && dt.mon <= 9) return true; // Marzo: último domingo if(dt.mon == 3) { int lastSunday = GetLastSunday(dt.year, 3); return dt.day >= lastSunday; } // Octubre: último domingo if(dt.mon == 10) { int lastSunday = GetLastSunday(dt.year, 10); return dt.day < lastSunday; } return false; } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: DST para Australia | //+------------------------------------------------------------------+ bool IsAustraliaDaylightSavingTime(const MqlDateTime &dt) { // Primer domingo de octubre hasta primer domingo de abril (inverso) // Entre noviembre y marzo = horario de verano if(dt.mon >= 11 || dt.mon <= 3) { if(dt.mon >= 11) return true; // Nov-Dic if(dt.mon <= 2) return true; // Ene-Feb // Marzo: primer domingo if(dt.mon == 3) { int firstSunday = GetNthSunday(dt.year, 3, 1); return dt.day < firstSunday; } } // Octubre: primer domingo if(dt.mon == 10) { int firstSunday = GetNthSunday(dt.year, 10, 1); return dt.day >= firstSunday; } // Entre mayo y septiembre = horario estándar return false; } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: Obtener el N-ésimo domingo de un mes | //+------------------------------------------------------------------+ int GetNthSunday(int year, int month, int n) { datetime firstDay = StringToTime(StringFormat("%04d.%02d.01", year, month)); MqlDateTime dt; TimeToStruct(firstDay, dt); // Encontrar el primer domingo int daysToFirstSunday = (7 - dt.day_of_week) % 7; int firstSunday = 1 + daysToFirstSunday; // Calcular el N-ésimo domingo return firstSunday + (n - 1) * 7; } //+------------------------------------------------------------------+ //| 🔹 NUEVA FUNCIÓN: Obtener el último domingo de un mes | //+------------------------------------------------------------------+ int GetLastSunday(int year, int month) { // Último día del mes datetime lastDay = StringToTime(StringFormat("%04d.%02d.01", year, month + 1)) - 86400; MqlDateTime dt; TimeToStruct(lastDay, dt); // Retroceder hasta el domingo return dt.day - dt.day_of_week; } //+------------------------------------------------------------------+ //| Obtener offset de zona horaria | //+------------------------------------------------------------------+ double GetTimezoneOffset() { switch(InpTimeZone) { case GMT_MINUS_11_MIDWAY: return -11.0; case GMT_MINUS_11_SAMOA: return -11.0; case GMT_MINUS_10_HAWAII: return -10.0; case GMT_MINUS_8_ALASKA: return -8.0; case GMT_MINUS_7_ARIZONA: return -7.0; case GMT_MINUS_7_BAJA: return -7.0; case GMT_MINUS_7_PACIFIC: return -7.0; case GMT_MINUS_6_CHIHUAHUA: return -6.0; case GMT_MINUS_6_GUADALAJARA: return -6.0; case GMT_MINUS_6_MOUNTAIN: return -6.0; case GMT_MINUS_5_BOGOTA: return -5.0; case GMT_MINUS_5_CENTRAL_AMERICA: return -5.0; case GMT_MINUS_5_CENTRAL_US: return -5.0; case GMT_MINUS_5_SASKATCHEWAN: return -5.0; case GMT_MINUS_4_ASUNCION: return -4.0; case GMT_MINUS_4_CARACAS: return -4.0; case GMT_MINUS_4_CUIABA: return -4.0; case GMT_MINUS_4_EASTERN_US: return -4.0; case GMT_MINUS_4_GEORGETOWN: return -4.0; case GMT_MINUS_4_INDIANA: return -4.0; case GMT_MINUS_4_SANTIAGO: return -4.0; case GMT_MINUS_3_ATLANTIC: return -3.0; case GMT_MINUS_3_BRASILIA: return -3.0; case GMT_MINUS_3_BUENOS_AIRES: return -3.0; case GMT_MINUS_3_CAYENNE: return -3.0; case GMT_MINUS_3_MONTEVIDEO: return -3.0; case GMT_MINUS_2_30_NEWFOUNDLAND: return -2.5; case GMT_MINUS_2_GREENLAND: return -2.0; case GMT_MINUS_1_CAPE_VERDE: return -1.0; case GMT_0_AZORES: return 0.0; case GMT_0_UTC: return 0.0; case GMT_0_MONROVIA: return 0.0; case GMT_PLUS_1_CASABLANCA: return 1.0; case GMT_PLUS_1_DUBLIN: return 1.0; case GMT_PLUS_1_LAGOS: return 1.0; case GMT_PLUS_1_WEST_AFRICA: return 1.0; case GMT_PLUS_2_AMSTERDAM: return 2.0; case GMT_PLUS_2_BELGRADE: return 2.0; case GMT_PLUS_2_BRUSSELS: return 2.0; case GMT_PLUS_2_CAIRO: return 2.0; case GMT_PLUS_2_JOHANNESBURG: return 2.0; case GMT_PLUS_2_SARAJEVO: return 2.0; case GMT_PLUS_2_WINDHOEK: return 2.0; case GMT_PLUS_3_AMMAN: return 3.0; case GMT_PLUS_3_BAGHDAD: return 3.0; case GMT_PLUS_3_BEIRUT: return 3.0; case GMT_PLUS_3_DAMASCUS: return 3.0; case GMT_PLUS_3_HELSINKI: return 3.0; case GMT_PLUS_3_ISTANBUL: return 3.0; case GMT_PLUS_3_JERUSALEM: return 3.0; case GMT_PLUS_3_KUWAIT: return 3.0; case GMT_PLUS_3_MOSCOW: return 3.0; case GMT_PLUS_3_NAIROBI: return 3.0; case GMT_PLUS_3_30_TEHRAN: return 3.5; case GMT_PLUS_4_ABU_DHABI: return 4.0; case GMT_PLUS_4_BAKU: return 4.0; case GMT_PLUS_4_30_KABUL: return 4.5; case GMT_PLUS_5_EKATERINBURG: return 5.0; case GMT_PLUS_5_KARACHI: return 5.0; case GMT_PLUS_5_30_CHENNAI: return 5.5; case GMT_PLUS_5_30_COLOMBO: return 5.5; case GMT_PLUS_5_45_KATHMANDU: return 5.75; case GMT_PLUS_6_DHAKA: return 6.0; case GMT_PLUS_6_30_YANGON: return 6.5; case GMT_PLUS_7_BANGKOK: return 7.0; case GMT_PLUS_8_BEIJING: return 8.0; case GMT_PLUS_8_MANILA: return 8.0; case GMT_PLUS_8_SINGAPORE: return 8.0; case GMT_PLUS_9_OSAKA: return 9.0; case GMT_PLUS_9_SEOUL: return 9.0; case GMT_PLUS_9_30_ADELAIDE: return 9.5; case GMT_PLUS_10_BRISBANE: return 10.0; case GMT_PLUS_10_CANBERRA: return 10.0; case GMT_PLUS_10_VLADIVOSTOK: return 10.0; case GMT_PLUS_11_SOLOMON: return 11.0; case GMT_PLUS_12_AUCKLAND: return 12.0; case GMT_PLUS_12_ENIWETOK: return 12.0; default: return 0.0; } } //+------------------------------------------------------------------+ //| 🔹 FUNCIÓN CORREGIDA: Configurar sesión de mercado con DST | //+------------------------------------------------------------------+ void SetupMarketSession() { // Detectar si estamos en horario de verano para la fecha actual bool isDST = IsDaylightSavingTime(TimeCurrent(), InpMarketSession); switch(InpMarketSession) { case SESSION_SYDNEY: currentSession.name = "Sydney"; if(isDST) // AEDT (UTC+11) { currentSession.startTime = "23:00"; // 10:00 AEDT currentSession.endTime = "05:00"; // 16:00 AEDT } else // AEST (UTC+10) { currentSession.startTime = "00:00"; // 10:00 AEST currentSession.endTime = "06:00"; // 16:00 AEST } currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = isDST; break; case SESSION_TOKYO: currentSession.name = "Tokyo"; currentSession.startTime = "00:00"; // 09:00 JST currentSession.endTime = "06:00"; // 15:00 JST (sin lunch break para ORB) currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = false; // Japón no usa DST break; case SESSION_LONDON: currentSession.name = "London"; if(isDST) // BST (UTC+1) { currentSession.startTime = "07:00"; // 08:00 BST currentSession.endTime = "15:30"; // 16:30 BST } else // GMT (UTC+0) { currentSession.startTime = "08:00"; // 08:00 GMT currentSession.endTime = "16:30"; // 16:30 GMT } currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = isDST; break; case SESSION_NEWYORK: currentSession.name = "New York"; if(isDST) // EDT (UTC-4) { currentSession.startTime = "13:30"; // 09:30 EDT currentSession.endTime = "20:00"; // 16:00 EDT } else // EST (UTC-5) { currentSession.startTime = "14:30"; // 09:30 EST currentSession.endTime = "21:00"; // 16:00 EST } currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = isDST; break; case SESSION_FRANKFURT: currentSession.name = "Frankfurt"; if(isDST) // CEST (UTC+2) { currentSession.startTime = "07:00"; // 09:00 CEST currentSession.endTime = "15:30"; // 17:30 CEST } else // CET (UTC+1) { currentSession.startTime = "08:00"; // 09:00 CET currentSession.endTime = "16:30"; // 17:30 CET } currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = isDST; break; case SESSION_CUSTOM: default: currentSession.name = "Custom"; currentSession.startTime = ""; currentSession.endTime = ""; currentSession.timezoneOffset = GetTimezoneOffset(); currentSession.isDST = false; break; } DebugPrint(StringFormat("📍 Market Session: %s (%s - %s UTC%+.1f) %s", currentSession.name, currentSession.startTime, currentSession.endTime, currentSession.timezoneOffset, isDST ? "[DST]" : "[STD]")); } //+------------------------------------------------------------------+ //| Obtener timeframe del gráfico en minutos | //+------------------------------------------------------------------+ int GetChartTimeframeMinutes() { ENUM_TIMEFRAMES tf = (ENUM_TIMEFRAMES)Period(); switch(tf) { case PERIOD_M1: return 1; case PERIOD_M2: return 2; case PERIOD_M3: return 3; case PERIOD_M4: return 4; case PERIOD_M5: return 5; case PERIOD_M6: return 6; case PERIOD_M10: return 10; case PERIOD_M12: return 12; case PERIOD_M15: return 15; case PERIOD_M20: return 20; case PERIOD_M30: return 30; case PERIOD_H1: return 60; case PERIOD_H2: return 120; case PERIOD_H3: return 180; case PERIOD_H4: return 240; case PERIOD_H6: return 360; case PERIOD_H8: return 480; case PERIOD_H12: return 720; case PERIOD_D1: return 1440; case PERIOD_W1: return 10080; case PERIOD_MN1: return 43200; default: return 15; } } //+------------------------------------------------------------------+ //| Determinar si debe usar modo de primera vela | //+------------------------------------------------------------------+ bool ShouldUseFirstCandleMode() { if(orbPeriodMinutes == 0) return false; bool useFirstCandle = (chartTimeframeMinutes == orbPeriodMinutes); DebugPrint(StringFormat("📊 Chart TF: %d min, ORB Period: %d min → Mode: %s", chartTimeframeMinutes, orbPeriodMinutes, useFirstCandle ? "FIRST CANDLE" : "RANGE")); return useFirstCandle; } //+------------------------------------------------------------------+ //| Calcular SMA de volumen | //+------------------------------------------------------------------+ void CalculateVolumeSMA(const long &volume[], int rates_total) { ArrayResize(volumeSMA, rates_total); for(int i = 0; i < rates_total; i++) { if(i < volumePeriod) { volumeSMA[i] = 0; continue; } double sum = 0; for(int j = 0; j < volumePeriod; j++) { sum += (double)volume[i - j]; } volumeSMA[i] = sum / volumePeriod; } } //+------------------------------------------------------------------+ //| Detectar breakout avanzado con confirmación | //+------------------------------------------------------------------+ BreakoutData DetectAdvancedBreakout(const datetime &time[], const double &high[], const double &low[], const double &close[], const long &volume[], int index, const ORBData &orb) { BreakoutData breakout; ZeroMemory(breakout); if(index < 2) return breakout; // 🔹 LÓGICA DE BREAKOUT AVANZADA (como en Pine Script) // highCrossBO = (low[2] < orbHighPrice and close[2] > orbHighPrice and low[1] > orbHighPrice and close[1] > orbHighPrice and close > low[1] and low > orbHighPrice) breakout.highCrossBO = (low[index-2] < orb.high && close[index-2] > orb.high && low[index-1] > orb.high && close[index-1] > orb.high && close[index] > low[index-1] && low[index] > orb.high); // lowCrossBO = (high[2] > orbLowPrice and close[2] < orbLowPrice and high[1] < orbLowPrice and close[1] < orbLowPrice and close < high[1] and high < orbLowPrice) breakout.lowCrossBO = (high[index-2] > orb.low && close[index-2] < orb.low && high[index-1] < orb.low && close[index-1] < orb.low && close[index] < high[index-1] && high[index] < orb.low); // 🔹 CRUCES SIMPLES breakout.highCross = (close[index-1] <= orb.high && close[index] > orb.high); breakout.lowCross = (close[index-1] >= orb.low && close[index] < orb.low); // 🔹 DETECCIÓN DE RETESTS breakout.isRetestOrbHigh = (close[index-1] > orb.high && low[index] <= orb.high && close[index] >= orb.high); breakout.isRetestOrbLow = (close[index-1] < orb.low && high[index] >= orb.low && close[index] <= orb.low); // 🔹 CONFIRMACIÓN DE VOLUMEN (si está habilitada) if(InpUseVolumeConfirmation && index < ArraySize(volumeSMA)) { breakout.volumeSpike = ((double)volume[index-2] > volumeSMA[index-2] || (double)volume[index-1] > volumeSMA[index-1] || (double)volume[index] > volumeSMA[index]); } else { breakout.volumeSpike = true; // Si no usamos volumen, siempre es true } return breakout; } //+------------------------------------------------------------------+ //| Inicialización del Indicador | //+------------------------------------------------------------------+ int OnInit() { SetupMarketSession(); chartTimeframeMinutes = GetChartTimeframeMinutes(); volumePeriod = InpVolumePeriod; if(InpORBPeriod == ORB_CUSTOM) { orbPeriodMinutes = 0; DebugPrint("Using CUSTOM time period: " + InpCustomTime); } else { orbPeriodMinutes = (int)InpORBPeriod; DebugPrint("Using predefined period: " + IntegerToString(orbPeriodMinutes) + " minutes"); } bool firstCandleMode = ShouldUseFirstCandleMode(); string indicatorName = StringFormat("ORB%s_%s_%s%s%s%s", (orbPeriodMinutes > 0) ? IntegerToString(orbPeriodMinutes) : "Custom", currentSession.name, firstCandleMode ? "1C" : "RANGE", currentSession.isDST ? "_DST" : "_STD", InpShowFibTargets ? "_FIB" : "", InpUseVolumeConfirmation ? "_VOL" : ""); IndicatorSetString(INDICATOR_SHORTNAME, indicatorName); ArrayResize(orbDataArray, 0); Print("🚀 ORB Indicator initialized - Version 2.1 WITH DST SUPPORT"); Print("📊 Chart Timeframe: ", chartTimeframeMinutes, " minutes"); Print("📊 ORB Period: ", orbPeriodMinutes, " minutes"); Print("📍 Market Session: ", currentSession.name); Print("🎯 Mode: ", firstCandleMode ? "FIRST CANDLE" : "FULL RANGE"); Print("🌍 Timezone: ", EnumToString(InpTimeZone), " (UTC", currentSession.timezoneOffset, ")"); Print("⏰ DST Status: ", currentSession.isDST ? "DAYLIGHT SAVING TIME" : "STANDARD TIME"); Print("📦 Shaded Box: ", InpShowShadedBox ? "ENABLED" : "DISABLED"); Print("📐 Fibonacci Targets: ", InpShowFibTargets ? "ENABLED" : "DISABLED"); Print("📊 Volume Confirmation: ", InpUseVolumeConfirmation ? "ENABLED" : "DISABLED"); return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Desinicialización del Indicador | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { DeleteAllORBObjects(); Print("👋 ORB Indicator deinitialized"); } //+------------------------------------------------------------------+ //| Función principal de cálculo | //+------------------------------------------------------------------+ 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 < 100) return(0); // 🔹 CALCULAR SMA DE VOLUMEN SI ESTÁ HABILITADO if(InpUseVolumeConfirmation) { CalculateVolumeSMA(volume, rates_total); } CalculateAllORBs(time, high, low, rates_total); DrawAllORBElements(); // 🔹 DETECTAR BREAKOUTS EN TIEMPO REAL DetectRealtimeBreakouts(time, high, low, close, volume, rates_total); return(rates_total); } //+------------------------------------------------------------------+ //| Detectar breakouts en tiempo real | //+------------------------------------------------------------------+ void DetectRealtimeBreakouts(const datetime &time[], const double &high[], const double &low[], const double &close[], const long &volume[], int rates_total) { if(rates_total < 3) return; // Buscar ORB del día actual ORBData currentORB; bool foundCurrentORB = false; int arraySize = ArraySize(orbDataArray); for(int i = arraySize - 1; i >= 0; i--) { if(IsToday(orbDataArray[i].date) && orbDataArray[i].calculated) { currentORB = orbDataArray[i]; foundCurrentORB = true; break; } } if(!foundCurrentORB) return; // Detectar breakouts en la vela actual int currentIndex = rates_total - 1; BreakoutData breakout = DetectAdvancedBreakout(time, high, low, close, volume, currentIndex, currentORB); // 🔹 PROCESAR ALERTAS ProcessBreakoutAlerts(breakout, currentORB, time[currentIndex]); // 🔹 ACTUALIZAR ESTADO DE BREAKOUT UpdateBreakoutState(breakout, currentORB); } //+------------------------------------------------------------------+ //| Procesar alertas de breakout | //+------------------------------------------------------------------+ void ProcessBreakoutAlerts(const BreakoutData &breakout, const ORBData &orb, datetime currentTime) { // Evitar alertas duplicadas en la misma vela if(currentTime == lastAlertTime) return; string orbName = StringFormat("ORB%s_%s", (orbPeriodMinutes > 0) ? IntegerToString(orbPeriodMinutes) : "Custom", currentSession.name); if(!InpAlertBreakoutsOnly) { // Alertas para cruces simples if(breakout.highCross) { string message = StringFormat("%s: Price crossing HIGH level %.5f", orbName, orb.high); SendORBAlert(message); lastAlertTime = currentTime; } if(breakout.lowCross) { string message = StringFormat("%s: Price crossing LOW level %.5f", orbName, orb.low); SendORBAlert(message); lastAlertTime = currentTime; } } else { // Alertas solo para breakouts confirmados if(breakout.highCrossBO && breakout.volumeSpike) { string message = StringFormat("%s: BREAKOUT above HIGH %.5f - Look for Retest", orbName, orb.high); SendORBAlert(message); lastAlertTime = currentTime; } if(breakout.lowCrossBO && breakout.volumeSpike) { string message = StringFormat("%s: BREAKOUT below LOW %.5f - Look for Retest", orbName, orb.low); SendORBAlert(message); } } // 🔹 ALERTAS DE RETEST if(breakout.isRetestOrbHigh) { string message = StringFormat("%s: RETEST of HIGH level %.5f", orbName, orb.high); SendORBAlert(message); } if(breakout.isRetestOrbLow) { string message = StringFormat("%s: RETEST of LOW level %.5f", orbName, orb.low); SendORBAlert(message); } } //+------------------------------------------------------------------+ //| Actualizar estado de breakout | //+------------------------------------------------------------------+ void UpdateBreakoutState(const BreakoutData &breakout, const ORBData &orb) { // Lógica similar al Pine Script para tracking de breakouts if(breakout.highCrossBO || breakout.lowCrossBO) { currentInBreakout = true; } if(currentInBreakout && (breakout.isRetestOrbHigh || breakout.isRetestOrbLow || breakout.failedRetest)) { currentInBreakout = false; } } //+------------------------------------------------------------------+ //| Calcular ORBs para todos los días | //+------------------------------------------------------------------+ void CalculateAllORBs(const datetime &time[], const double &high[], const double &low[], int rates_total) { ArrayResize(orbDataArray, 0); datetime firstVisibleTime = time[MathMax(0, rates_total - 1000)]; datetime lastTime = time[rates_total - 1]; MqlDateTime dtFirst, dtLast; TimeToStruct(firstVisibleTime, dtFirst); TimeToStruct(lastTime, dtLast); DebugPrint("Processing dates from " + TimeToString(firstVisibleTime) + " to " + TimeToString(lastTime)); int daysProcessed = 0; for(datetime currentDate = StringToTime(StringFormat("%04d.%02d.%02d", dtFirst.year, dtFirst.mon, dtFirst.day)); currentDate <= StringToTime(StringFormat("%04d.%02d.%02d", dtLast.year, dtLast.mon, dtLast.day)); currentDate += 86400) { MqlDateTime dt; TimeToStruct(currentDate, dt); if(dt.day_of_week == 0 || dt.day_of_week == 6) continue; ORBData orb; if(CalculateORBForDate(currentDate, time, high, low, rates_total, orb)) { int size = ArraySize(orbDataArray); ArrayResize(orbDataArray, size + 1); orbDataArray[size] = orb; daysProcessed++; DebugPrint(StringFormat("✅ ORB calculated for %s: H=%.5f, L=%.5f, Range=%.5f, Bars=%d, Mode=%s", TimeToString(currentDate, TIME_DATE), orb.high, orb.low, orb.range, orb.barsFound, orb.isFirstCandleMode ? "FirstCandle" : "Range")); } } Print("📈 Calculated ORBs for ", daysProcessed, " days"); } //+------------------------------------------------------------------+ //| 🔹 FUNCIÓN MEJORADA: Calcular ORB para una fecha específica con DST | //+------------------------------------------------------------------+ bool CalculateORBForDate(datetime date, const datetime &time[], const double &high[], const double &low[], int rates_total, ORBData &orb) { datetime sessionStart, sessionEnd; GetORBSessionForDate(date, sessionStart, sessionEnd); orb.date = date; orb.startTime = sessionStart; orb.endTime = sessionEnd; orb.high = 0.0; orb.low = DBL_MAX; orb.calculated = false; orb.barsFound = 0; orb.candleTime = 0; orb.inBreakout = false; orb.breakoutDirection = 0; orb.breakoutTime = 0; orb.breakoutPrice = 0; bool useFirstCandle = ShouldUseFirstCandleMode(); orb.isFirstCandleMode = useFirstCandle; if(useFirstCandle) { DebugPrint("🕯️ Using FIRST CANDLE mode for " + TimeToString(date, TIME_DATE)); return FindFirstCandleORB(time, high, low, rates_total, sessionStart, orb); } else { DebugPrint("📊 Using RANGE mode for " + TimeToString(date, TIME_DATE)); return FindRangeORB(time, high, low, rates_total, sessionStart, sessionEnd, orb); } } //+------------------------------------------------------------------+ //| Buscar ORB de la primera vela solamente | //+------------------------------------------------------------------+ bool FindFirstCandleORB(const datetime &time[], const double &high[], const double &low[], int rates_total, datetime sessionStart, ORBData &orb) { int bestIndex = -1; int minTimeDiff = INT_MAX; for(int i = 0; i < rates_total; i++) { int timeDiff = (int)MathAbs(time[i] - sessionStart); if(timeDiff < minTimeDiff) { minTimeDiff = timeDiff; bestIndex = i; } if(timeDiff == 0) break; } if(bestIndex >= 0 && minTimeDiff <= 300) { orb.high = high[bestIndex]; orb.low = low[bestIndex]; orb.range = orb.high - orb.low; orb.calculated = true; orb.barsFound = 1; orb.candleTime = time[bestIndex]; DebugPrint(StringFormat("🎯 Found FIRST CANDLE at %s: H=%.5f, L=%.5f, TimeDiff=%d sec", TimeToString(time[bestIndex]), high[bestIndex], low[bestIndex], minTimeDiff)); return true; } DebugPrint("❌ No suitable first candle found near " + TimeToString(sessionStart)); return false; } //+------------------------------------------------------------------+ //| Buscar ORB en rango completo | //+------------------------------------------------------------------+ bool FindRangeORB(const datetime &time[], const double &high[], const double &low[], int rates_total, datetime sessionStart, datetime sessionEnd, ORBData &orb) { bool foundData = false; DebugPrint(StringFormat("🔍 Searching RANGE from %s to %s", TimeToString(sessionStart), TimeToString(sessionEnd))); for(int i = 0; i < rates_total; i++) { if(time[i] >= sessionStart && time[i] <= sessionEnd) { if(!foundData) { orb.high = high[i]; orb.low = low[i]; foundData = true; orb.candleTime = time[i]; DebugPrint(StringFormat("📍 First bar in range at %s: H=%.5f, L=%.5f", TimeToString(time[i]), high[i], low[i])); } else { if(high[i] > orb.high) { orb.high = high[i]; DebugPrint(StringFormat("⬆️ New HIGH: %.5f at %s", high[i], TimeToString(time[i]))); } if(low[i] < orb.low) { orb.low = low[i]; DebugPrint(StringFormat("⬇️ New LOW: %.5f at %s", low[i], TimeToString(time[i]))); } } orb.barsFound++; } } if(foundData && orb.barsFound > 0) { orb.range = orb.high - orb.low; orb.calculated = true; DebugPrint(StringFormat("✅ RANGE ORB complete: H=%.5f, L=%.5f, Range=%.5f, Bars=%d", orb.high, orb.low, orb.range, orb.barsFound)); return true; } DebugPrint("❌ No data found in range"); return false; } //+------------------------------------------------------------------+ //| 🔹 FUNCIÓN MEJORADA: Obtener horarios de sesión con DST | //+------------------------------------------------------------------+ void GetORBSessionForDate(datetime date, datetime &startTime, datetime &endTime) { MqlDateTime dt; TimeToStruct(date, dt); string sessionStart, sessionEnd; double timezoneOffset = currentSession.timezoneOffset; // 🔹 DETECTAR DST PARA LA FECHA ESPECÍFICA bool isDST = IsDaylightSavingTime(date, InpMarketSession); if(InpMarketSession == SESSION_CUSTOM) { if(orbPeriodMinutes == 0) { string times[]; if(StringSplit(InpCustomTime, '-', times) == 2) { sessionStart = times[0]; sessionEnd = times[1]; } else { sessionStart = "09:30"; sessionEnd = "09:45"; } } else { sessionStart = "09:30"; switch(orbPeriodMinutes) { case 5: sessionEnd = "09:35"; break; case 15: sessionEnd = "09:45"; break; case 30: sessionEnd = "10:00"; break; default: sessionEnd = "09:45"; break; } } } else { // 🔹 USAR HORARIOS AJUSTADOS POR DST switch(InpMarketSession) { case SESSION_SYDNEY: if(isDST) // AEDT { sessionStart = "23:00"; // 10:00 AEDT sessionEnd = "05:00"; // 16:00 AEDT } else // AEST { sessionStart = "00:00"; // 10:00 AEST sessionEnd = "06:00"; // 16:00 AEST } break; case SESSION_TOKYO: sessionStart = "00:00"; // 09:00 JST sessionEnd = "06:00"; // 15:00 JST break; case SESSION_LONDON: if(isDST) // BST { sessionStart = "07:00"; // 08:00 BST sessionEnd = "15:30"; // 16:30 BST } else // GMT { sessionStart = "08:00"; // 08:00 GMT sessionEnd = "16:30"; // 16:30 GMT } break; case SESSION_NEWYORK: if(isDST) // EDT { sessionStart = "13:30"; // 09:30 EDT sessionEnd = "20:00"; // 16:00 EDT } else // EST { sessionStart = "14:30"; // 09:30 EST sessionEnd = "21:00"; // 16:00 EST } break; case SESSION_FRANKFURT: if(isDST) // CEST { sessionStart = "07:00"; // 09:00 CEST sessionEnd = "15:30"; // 17:30 CEST } else // CET { sessionStart = "08:00"; // 09:00 CET sessionEnd = "16:30"; // 17:30 CET } break; } // 🔹 AJUSTAR SEGÚN PERÍODO ORB if(orbPeriodMinutes > 0) { string startParts[]; StringSplit(sessionStart, ':', startParts); int startHour = (int)StringToInteger(startParts[0]); int startMinute = (int)StringToInteger(startParts[1]); int totalStartMinutes = startHour * 60 + startMinute; int totalEndMinutes = totalStartMinutes + orbPeriodMinutes; int endHour = totalEndMinutes / 60; int endMinute = totalEndMinutes % 60; if(endHour >= 24) { endHour -= 24; } sessionEnd = StringFormat("%02d:%02d", endHour, endMinute); } } string dateStr = StringFormat("%04d.%02d.%02d", dt.year, dt.mon, dt.day); startTime = StringToTime(dateStr + " " + sessionStart) + (int)(timezoneOffset * 3600); // 🔹 MANEJAR CRUCE DE MEDIANOCHE PARA SYDNEY if(InpMarketSession == SESSION_SYDNEY && isDST && StringCompare(sessionEnd, "05:00") == 0) { datetime nextDay = date + 86400; MqlDateTime dtNext; TimeToStruct(nextDay, dtNext); string nextDayStr = StringFormat("%04d.%02d.%02d", dtNext.year, dtNext.mon, dtNext.day); endTime = StringToTime(nextDayStr + " " + sessionEnd) + (int)(timezoneOffset * 3600); } else { endTime = StringToTime(dateStr + " " + sessionEnd) + (int)(timezoneOffset * 3600); } DebugPrint(StringFormat("📅 Session for %s (%s): %s to %s (UTC%+.1f) %s", dateStr, currentSession.name, TimeToString(startTime), TimeToString(endTime), timezoneOffset, isDST ? "[DST]" : "[STD]")); } //+------------------------------------------------------------------+ //| Dibujar todos los elementos ORB | //+------------------------------------------------------------------+ void DrawAllORBElements() { DeleteAllORBObjects(); int arraySize = ArraySize(orbDataArray); if(arraySize == 0) { DebugPrint("⚠️ No ORB data to draw"); return; } for(int i = 0; i < arraySize; i++) { if(!orbDataArray[i].calculated) continue; bool isToday = IsToday(orbDataArray[i].date); if(!InpShowPreviousDays && !isToday) continue; DrawORBForDay(orbDataArray[i], i); } } //+------------------------------------------------------------------+ //| Dibujar ORB para un día específico | //+------------------------------------------------------------------+ void DrawORBForDay(const ORBData &orb, int dayIndex) { string suffix = "_" + IntegerToString(dayIndex); datetime lineEndTime = orb.endTime + 86400; datetime boxStartTime = orb.isFirstCandleMode ? orb.candleTime : orb.startTime; // 🔹 CAJA SOMBREADA if(InpShowShadedBox) { string boxName = orbPrefix + "Box" + suffix; ObjectDelete(0, boxName); if(ObjectCreate(0, boxName, OBJ_RECTANGLE, 0, boxStartTime, orb.high, lineEndTime, orb.low)) { ObjectSetInteger(0, boxName, OBJPROP_COLOR, InpShadeColor); ObjectSetInteger(0, boxName, OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0, boxName, OBJPROP_WIDTH, InpBoxBorderSize); ObjectSetInteger(0, boxName, OBJPROP_FILL, true); ObjectSetInteger(0, boxName, OBJPROP_BACK, true); ObjectSetInteger(0, boxName, OBJPROP_SELECTED, false); ObjectSetInteger(0, boxName, OBJPROP_SELECTABLE, false); } } // 🔹 LÍNEAS ORB DrawORBLine("High" + suffix, orb.endTime, lineEndTime, orb.high, InpORBHighColor); DrawORBLine("Low" + suffix, orb.endTime, lineEndTime, orb.low, InpORBLowColor); // 🔹 OBJETIVOS DE PRECIO if(InpShowPriceTargets) DrawPriceTargetsForDay(orb, suffix, lineEndTime); // 🔹 OBJETIVOS FIBONACCI if(InpShowFibTargets) DrawFibonacciTargetsForDay(orb, suffix, lineEndTime); // 🔹 PUNTO MEDIO if(InpShowMidPoint) DrawMidPointForDay(orb, suffix, lineEndTime); // 🔹 ETIQUETAS if(InpShowLabels && IsToday(orb.date)) DrawLabelsForDay(orb, suffix); } //+------------------------------------------------------------------+ //| Dibujar línea ORB | //+------------------------------------------------------------------+ void DrawORBLine(string name, datetime startTime, datetime endTime, double price, color lineColor) { string objName = orbPrefix + name; if(ObjectCreate(0, objName, OBJ_TREND, 0, startTime, price, endTime, price)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, lineColor); ObjectSetInteger(0, objName, OBJPROP_STYLE, InpLineStyle); ObjectSetInteger(0, objName, OBJPROP_WIDTH, InpLineWidth); ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); } } //+------------------------------------------------------------------+ //| Dibujar objetivos de precio | //+------------------------------------------------------------------+ void DrawPriceTargetsForDay(const ORBData &orb, string suffix, datetime endTime) { DrawTargetLineForDay("Target_High_50" + suffix, orb.high + (orb.range * 0.5), InpTarget50Color, endTime); DrawTargetLineForDay("Target_High_100" + suffix, orb.high + (orb.range * 1.0), InpTarget100Color, endTime); DrawTargetLineForDay("Target_Low_50" + suffix, orb.low - (orb.range * 0.5), InpTarget50Color, endTime); DrawTargetLineForDay("Target_Low_100" + suffix, orb.low - (orb.range * 1.0), InpTarget100Color, endTime); if(InpShowExtendedTargets) { double multipliers[] = {1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0}; for(int i = 0; i < ArraySize(multipliers); i++) { DrawTargetLineForDay("Target_High_" + DoubleToString(multipliers[i], 1) + suffix, orb.high + (orb.range * multipliers[i]), InpTargetOtherColor, endTime); DrawTargetLineForDay("Target_Low_" + DoubleToString(multipliers[i], 1) + suffix, orb.low - (orb.range * multipliers[i]), InpTargetOtherColor, endTime); } } } //+------------------------------------------------------------------+ //| Dibujar objetivos Fibonacci | //+------------------------------------------------------------------+ void DrawFibonacciTargetsForDay(const ORBData &orb, string suffix, datetime endTime) { // 🔹 NIVELES FIBONACCI: 27.2% y 61.8% DrawTargetLineForDay("Fib_High_272" + suffix, orb.high + (orb.range * 0.272), InpFibColor, endTime); DrawTargetLineForDay("Fib_High_618" + suffix, orb.high + (orb.range * 0.618), InpFibColor, endTime); DrawTargetLineForDay("Fib_Low_272" + suffix, orb.low - (orb.range * 0.272), InpFibColor, endTime); DrawTargetLineForDay("Fib_Low_618" + suffix, orb.low - (orb.range * 0.618), InpFibColor, endTime); // 🔹 ETIQUETAS FIBONACCI (solo para el día actual) if(IsToday(orb.date)) { DrawPriceTargetLabel(orb.high, 0.272, "FIB 27.2%", InpFibColor, suffix); DrawPriceTargetLabel(orb.high, 0.618, "FIB 61.8%", InpFibColor, suffix); DrawPriceTargetLabel(orb.low, -0.272, "FIB 27.2%", InpFibColor, suffix); DrawPriceTargetLabel(orb.low, -0.618, "FIB 61.8%", InpFibColor, suffix); } } //+------------------------------------------------------------------+ //| Dibujar línea de objetivo | //+------------------------------------------------------------------+ void DrawTargetLineForDay(string name, double price, color lineColor, datetime endTime) { string objName = orbPrefix + name; datetime startTime = endTime - 86400; if(ObjectCreate(0, objName, OBJ_TREND, 0, startTime, price, endTime, price)) { ObjectSetInteger(0, objName, OBJPROP_COLOR, lineColor); ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT); ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1); ObjectSetInteger(0, objName, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, objName, OBJPROP_SELECTED, false); ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); } } //+------------------------------------------------------------------+ //| Dibujar punto medio | //+------------------------------------------------------------------+ void DrawMidPointForDay(const ORBData &orb, string suffix, datetime endTime) { double midPrice = orb.low + (orb.range * 0.5); DrawTargetLineForDay("MidPoint" + suffix, midPrice, InpMidPointColor, endTime); } //+------------------------------------------------------------------+ //| Dibujar etiqueta de objetivo de precio | //+------------------------------------------------------------------+ void DrawPriceTargetLabel(double fromPrice, double level, string name, color labelColor, string suffix) { double targetPrice = fromPrice + (level * (ArraySize(orbDataArray) > 0 ? orbDataArray[ArraySize(orbDataArray)-1].range : 10.0)); string labelName = orbPrefix + "Label_" + name + suffix; datetime labelTime = TimeCurrent() + 3600; // 1 hora en el futuro if(ObjectCreate(0, labelName, OBJ_TEXT, 0, labelTime, targetPrice)) { string orbName = StringFormat("ORB%s_%s", (orbPeriodMinutes > 0) ? IntegerToString(orbPeriodMinutes) : "Custom", currentSession.name); ObjectSetString(0, labelName, OBJPROP_TEXT, orbName + " " + name); ObjectSetInteger(0, labelName, OBJPROP_COLOR, labelColor); ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, 8); ObjectSetString(0, labelName, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, labelName, OBJPROP_SELECTED, false); ObjectSetInteger(0, labelName, OBJPROP_SELECTABLE, false); } } //+------------------------------------------------------------------+ //| Dibujar etiquetas | //+------------------------------------------------------------------+ void DrawLabelsForDay(const ORBData &orb, string suffix) { string orbName = StringFormat("ORB%s_%s%s", (orbPeriodMinutes > 0) ? IntegerToString(orbPeriodMinutes) : "Custom", currentSession.name, orb.isFirstCandleMode ? "_1C" : ""); string labelHighName = orbPrefix + "Label_High" + suffix; if(ObjectCreate(0, labelHighName, OBJ_TEXT, 0, orb.endTime + 600, orb.high)) { ObjectSetString(0, labelHighName, OBJPROP_TEXT, orbName + " HIGH"); ObjectSetInteger(0, labelHighName, OBJPROP_COLOR, InpORBHighColor); ObjectSetInteger(0, labelHighName, OBJPROP_FONTSIZE, 9); ObjectSetString(0, labelHighName, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, labelHighName, OBJPROP_SELECTED, false); ObjectSetInteger(0, labelHighName, OBJPROP_SELECTABLE, false); } string labelLowName = orbPrefix + "Label_Low" + suffix; if(ObjectCreate(0, labelLowName, OBJ_TEXT, 0, orb.endTime + 600, orb.low)) { ObjectSetString(0, labelLowName, OBJPROP_TEXT, orbName + " LOW"); ObjectSetInteger(0, labelLowName, OBJPROP_COLOR, InpORBLowColor); ObjectSetInteger(0, labelLowName, OBJPROP_FONTSIZE, 9); ObjectSetString(0, labelLowName, OBJPROP_FONT, "Arial Bold"); ObjectSetInteger(0, labelLowName, OBJPROP_SELECTED, false); ObjectSetInteger(0, labelLowName, OBJPROP_SELECTABLE, false); } // 🔹 ETIQUETAS DE OBJETIVOS BÁSICOS if(InpShowPriceTargets) { DrawPriceTargetLabel(orb.high, 0.5, "PT 50%", InpTarget50Color, suffix); DrawPriceTargetLabel(orb.high, 1.0, "PT 100%", InpTarget100Color, suffix); DrawPriceTargetLabel(orb.low, -0.5, "PT 50%", InpTarget50Color, suffix); DrawPriceTargetLabel(orb.low, -1.0, "PT 100%", InpTarget100Color, suffix); if(InpShowExtendedTargets) { double multipliers[] = {1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0}; for(int i = 0; i < ArraySize(multipliers); i++) { string ptName = StringFormat("PT %.0f%%", multipliers[i] * 100); DrawPriceTargetLabel(orb.high, multipliers[i], ptName, InpTargetOtherColor, suffix); DrawPriceTargetLabel(orb.low, -multipliers[i], ptName, InpTargetOtherColor, suffix); } } } // 🔹 PUNTO MEDIO if(InpShowMidPoint) { DrawPriceTargetLabel(orb.low, 0.5, "MIDPOINT", InpMidPointColor, suffix); } } //+------------------------------------------------------------------+ //| Verificar si es hoy | //+------------------------------------------------------------------+ bool IsToday(datetime date) { datetime today = StringToTime(TimeToString(TimeCurrent(), TIME_DATE)); datetime checkDate = StringToTime(TimeToString(date, TIME_DATE)); return today == checkDate; } //+------------------------------------------------------------------+ //| Eliminar todos los objetos ORB | //+------------------------------------------------------------------+ void DeleteAllORBObjects() { for(int i = ObjectsTotal(0) - 1; i >= 0; i--) { string objName = ObjectName(0, i); if(StringFind(objName, orbPrefix) == 0) ObjectDelete(0, objName); } } //+------------------------------------------------------------------+ //| Enviar alerta ORB | //+------------------------------------------------------------------+ void SendORBAlert(string message) { Alert("🔔 ORB Alert: " + message); Print("🔔 ORB Alert: " + message); // 🔹 OPCIONAL: Enviar notificación push si está habilitado if(TerminalInfoInteger(TERMINAL_NOTIFICATIONS_ENABLED)) { SendNotification("ORB: " + message); } } //+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void OnTimer() { // Implementar lógica de timer si es necesario } //+------------------------------------------------------------------+ //| Eventos del gráfico | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if(id == CHARTEVENT_OBJECT_CLICK) { DebugPrint("Objeto clickeado: " + sparam); } }