generated from LengKundee/UA6-9V_VL6-N9
1527 lines
No EOL
120 KiB
MQL5
1527 lines
No EOL
120 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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);
|
|
}
|
|
} |