diff --git a/QQE.cs b/QQE.cs new file mode 100644 index 0000000..73d440c --- /dev/null +++ b/QQE.cs @@ -0,0 +1,312 @@ +// ------------------------------------------------------------------------------- +// +// QQE - Qualitative Quantitative Estimation. +// Calculated as two indicators: +// 1) MA on RSI +// 2) Difference of MA on RSI and MA of MA of ATR of MA of RSI +// The signal for buy is when blue line crosses level 50 from below after crossing the yellow line from below. +// The signal for sell is when blue line crosses level 50 from above after crossing the yellow line from above. +// +// Version 1.03 +// Copyright 2010-2022, EarnForex.com +// https://www.earnforex.com/metatrader-indicators/QQE/ +// ------------------------------------------------------------------------------- +using System; +using cAlgo.API; +using cAlgo.API.Indicators; + +namespace cAlgo.Indicators +{ + [Levels(50)] + [Indicator(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)] + public class QQE : Indicator + { + public enum ENUM_CANDLE_TO_CHECK + { + Current, + Previous + } + + [Parameter(DefaultValue = 5, MinValue = 1)] + public int SF { get; set; } + + [Parameter(DefaultValue = false)] + public bool AlertOnCrossover { get; set; } + + [Parameter(DefaultValue = false)] + public bool AlertOnLevel { get; set; } + + [Parameter(DefaultValue = 50, MinValue = 1, MaxValue = 99)] + public int AlertLevel { get; set; } + + [Parameter(DefaultValue = true)] + public bool ArrowsOnCrossover { get; set; } + + [Parameter(DefaultValue = "Green")] + public string CrossoverUpArrow { get; set; } + + [Parameter(DefaultValue = "Red")] + public string CrossoverDnArrow { get; set; } + + [Parameter(DefaultValue = true)] + public bool ArrowsOnLevel { get; set; } + + [Parameter(DefaultValue = "Green")] + public string LevelUpArrow { get; set; } + + [Parameter(DefaultValue = "Red")] + public string LevelDnArrow { get; set; } + + [Parameter("Enable email alerts", DefaultValue = false)] + public bool EnableEmailAlerts { get; set; } + + [Parameter("AlertEmail: Email From", DefaultValue = "")] + public string AlertEmailFrom { get; set; } + + [Parameter("AlertEmail: Email To", DefaultValue = "")] + public string AlertEmailTo { get; set; } + + [Parameter("Upper timeframe")] + public TimeFrame UpperTimeframe { get; set; } + + [Parameter(DefaultValue = "QQE-")] + public string ObjectPrefix { get; set; } + + [Output("RSI MA", LineColor = "DodgerBlue", Thickness = 2)] + public IndicatorDataSeries RsiMa { get; set; } + + [Output("Smoothed", LineColor = "Yellow")] + public IndicatorDataSeries TrLevelSlow { get; set; } + + private IndicatorDataSeries AtrRsi; + private IndicatorDataSeries TrLevelSlow_; + private IndicatorDataSeries MaAtrRsi_, MaAtrRsi_Wilders_; + private RelativeStrengthIndex Rsi_; + private MovingAverage RsiMa_; + + private bool UseUpperTimeFrame; + + private Bars customBars; + + private const int RSI_Period = 14; + private int Wilders_Period; + private double Wilders_Multiplier; + + private int Is_Initialized_MaAtrRsi = -1; + private int Is_Initialized_MaAtrRsi_Wilders = -1; + + private int prev_index = -1; + private int CI_zero = 0; + + private DateTime LastAlertTimeCross, LastAlertTimeLevel, unix_epoch; + + protected override void Initialize() + { + Wilders_Period = RSI_Period * 2 - 1; + Wilders_Multiplier = 2.0 / (Wilders_Period + 1.0); + + if (UpperTimeframe <= TimeFrame) + { + Print("UpperTimeframe <= current timeframe. Ignored."); + UseUpperTimeFrame = false; + customBars = Bars; + } + else + { + UseUpperTimeFrame = true; + customBars = MarketData.GetBars(UpperTimeframe); + } + + Rsi_ = Indicators.RelativeStrengthIndex(customBars.ClosePrices, RSI_Period); + RsiMa_ = Indicators.MovingAverage(Rsi_.Result, SF, MovingAverageType.Exponential); + AtrRsi = CreateDataSeries(); + MaAtrRsi_ = CreateDataSeries(); + MaAtrRsi_Wilders_ = CreateDataSeries(); + TrLevelSlow_ = CreateDataSeries(); + + unix_epoch = new DateTime(1970, 1, 1, 0, 0, 0); + LastAlertTimeCross = unix_epoch; + LastAlertTimeLevel = unix_epoch; + } + + protected override void OnDestroy() + { + if ((ArrowsOnLevel) || (ArrowsOnCrossover)) + { + var icons = Chart.FindAllObjects(ChartObjectType.Icon); + for (int i = icons.Length - 1; i >= 0; i--) + { + if (icons[i].Name.StartsWith(ObjectPrefix)) + Chart.RemoveObject(icons[i].Name); + } + } + } + + public override void Calculate(int index) + { + int customIndex = index; + int cnt = 0; // How many bars of the current timeframe should be recalculated. + if (UseUpperTimeFrame) + { + customIndex = customBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index]); + if (index == 0) CI_zero = customIndex; // customIndex at zero. Because an upper timeframe index may start from non-zero value. + if (customIndex <= CI_zero + SF) return; // Too early to calculate anything. + // Find how many current timeframe bars should be recalculated: + while (customBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index - cnt]) == customIndex) + { + cnt++; + } + } + else + { + cnt = 1; // Non-MTF. + if (customIndex <= SF) return; // Too early to calculate anything. + } + + + RsiMa[index] = RsiMa_.Result[customIndex]; + + AtrRsi[customIndex] = Math.Abs(RsiMa_.Result[customIndex - 1] - RsiMa_.Result[customIndex]); + + if (customIndex <= CI_zero + SF + Wilders_Period + 1) return; // Too early to calculate MaAtrRsi. + + if (Is_Initialized_MaAtrRsi == -1) + { + // Simple average for the first value. + MaAtrRsi_[customIndex] = GetAverage(AtrRsi, customIndex, Wilders_Period); + Is_Initialized_MaAtrRsi = customIndex; + } + else if (Is_Initialized_MaAtrRsi < customIndex) // On next index. + { + // Fail-safe for NaN. + if (double.IsNaN(MaAtrRsi_[customIndex - 1])) MaAtrRsi_[customIndex] = GetAverage(AtrRsi, customIndex, Wilders_Period); + // Exponential average formula. + else MaAtrRsi_[customIndex] = (AtrRsi[customIndex] - MaAtrRsi_[customIndex - 1]) * Wilders_Multiplier + MaAtrRsi_[customIndex - 1]; + } + + if (customIndex <= CI_zero + SF + Wilders_Period + Wilders_Period) return; // Too early to calculate MaAtrRsi_Wilders. + + if (Is_Initialized_MaAtrRsi_Wilders == -1) + { + // Simple average for the first value. + MaAtrRsi_Wilders_[customIndex] = GetAverage(MaAtrRsi_, customIndex, Wilders_Period); + Is_Initialized_MaAtrRsi_Wilders = customIndex; + } + else if (Is_Initialized_MaAtrRsi_Wilders < customIndex) // On next index. + { + // Fail-safe for NaN. + if (double.IsNaN(MaAtrRsi_Wilders_[customIndex - 1])) MaAtrRsi_Wilders_[customIndex] = GetAverage(MaAtrRsi_, customIndex, Wilders_Period); + // Exponential average formula. + else MaAtrRsi_Wilders_[customIndex] = (MaAtrRsi_[customIndex] - MaAtrRsi_Wilders_[customIndex - 1]) * Wilders_Multiplier + MaAtrRsi_Wilders_[customIndex - 1]; + } + + double rsi1 = RsiMa_.Result[customIndex - 1]; + double rsi0 = RsiMa_.Result[customIndex]; + double dar = MaAtrRsi_Wilders_[customIndex] * 4.236; + + double tr = TrLevelSlow_[customIndex - 1]; + if (double.IsNaN(tr)) tr = 0; + double dv = tr; + + if (rsi0 < tr) + { + tr = rsi0 + dar; + if ((rsi1 < dv) && (tr > dv)) tr = dv; + } + else if (rsi0 > tr) + { + tr = rsi0 - dar; + if ((rsi1 > dv) && (tr < dv)) tr = dv; + } + TrLevelSlow_[customIndex] = tr; + + TrLevelSlow[index] = TrLevelSlow_[customIndex]; + + int cnt_prev = 2; + if (UseUpperTimeFrame) + { + for (int i = 1; i < cnt; i++) + { + TrLevelSlow[index - i] = TrLevelSlow[index]; + RsiMa[index - i] = RsiMa[index]; + } + + int ci_temp = customBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index - cnt]); // Latest finished bar. + // Find pre-latest finished bar. + cnt_prev = cnt + 1; + while (customBars.OpenTimes.GetIndexByTime(Bars.OpenTimes[index - cnt_prev]) == ci_temp) + { + cnt_prev++; + } + } + + // Arrows + if (ArrowsOnCrossover) + { + if ((RsiMa[index - cnt_prev] < TrLevelSlow[index - cnt_prev]) && (RsiMa[index - cnt] > TrLevelSlow[index - cnt])) // Cross up. + { + Chart.DrawIcon(ObjectPrefix + "C" + Bars.OpenTimes[index - cnt + 1].ToString(), ChartIconType.UpTriangle, index - cnt + 1, Bars.LowPrices[index - cnt + 1], Color.FromName(CrossoverUpArrow)); + } + else if ((RsiMa[index - cnt_prev] > TrLevelSlow[index - cnt_prev]) && (RsiMa[index - cnt] < TrLevelSlow[index - cnt])) // Cross down. + { + Chart.DrawIcon(ObjectPrefix + "C" + Bars.OpenTimes[index - cnt + 1].ToString(), ChartIconType.DownTriangle, index - cnt + 1, Bars.HighPrices[index - cnt + 1], Color.FromName(CrossoverDnArrow)); + } + } + if (ArrowsOnLevel) + { + if ((RsiMa[index - cnt_prev] < AlertLevel) && (RsiMa[index - cnt] > AlertLevel)) + { + Chart.DrawIcon(ObjectPrefix + "L" + Bars.OpenTimes[index - cnt + 1].ToString(), ChartIconType.UpArrow, index - cnt + 1, Bars.LowPrices[index - cnt + 1], Color.FromName(LevelUpArrow)); + } + else if ((RsiMa[index - cnt_prev] > AlertLevel) && (RsiMa[index - cnt] < AlertLevel)) + { + Chart.DrawIcon(ObjectPrefix + "L" + Bars.OpenTimes[index - cnt + 1].ToString(), ChartIconType.DownArrow, index - cnt + 1, Bars.LowPrices[index - cnt + 1], Color.FromName(LevelDnArrow)); + } + } + + // Alerts + if (!EnableEmailAlerts) return; // No need to go further. + if ((!AlertOnCrossover) && (!AlertOnLevel)) return; + + if (AlertOnLevel) + { + if ((LastAlertTimeLevel > unix_epoch) && (((RsiMa[index - cnt_prev] < AlertLevel) && (RsiMa[index - cnt] > AlertLevel)) || ((RsiMa[index - cnt_prev] > AlertLevel) && (RsiMa[index - cnt] < AlertLevel))) && (Bars.OpenTimes[index - cnt] > LastAlertTimeLevel)) + { + string Text = "QQE: " + Symbol.Name + " - " + TimeFrame.Name + " - Level Cross Up"; + if ((RsiMa[index - cnt_prev] > AlertLevel) && (RsiMa[index - cnt] < AlertLevel)) Text = "QQE: " + Symbol.Name + " - " + TimeFrame.Name + " - Level Cross Down"; + Notifications.SendEmail(AlertEmailFrom, AlertEmailTo, "QQE Alert - " + Symbol.Name + " @ " + TimeFrame.Name, Text); + LastAlertTimeLevel = Bars.OpenTimes[index - cnt]; + } + } + + if (AlertOnCrossover) + { + if ((LastAlertTimeCross > unix_epoch) && (((RsiMa[index - cnt_prev] < TrLevelSlow[index - cnt_prev]) && (RsiMa[index - cnt] > TrLevelSlow[index - cnt])) || ((RsiMa[index - cnt_prev] > TrLevelSlow[index - cnt_prev]) && (RsiMa[index - cnt] < TrLevelSlow[index - cnt]))) && (Bars.OpenTimes[index - cnt] > LastAlertTimeCross)) + { + string Text = "QQE: " + Symbol.Name + " - " + TimeFrame.Name + " - RSI MA crossed Smoothed Line from below."; + if ((RsiMa[index - cnt_prev] > TrLevelSlow[index - cnt_prev]) && (RsiMa[index - cnt] < TrLevelSlow[index - cnt])) Text = "QQE: " + Symbol.Name + " - " + TimeFrame.Name + " - RSI MA crossed Smoothed Line from above."; + Notifications.SendEmail(AlertEmailFrom, AlertEmailTo, "QQE Alert - " + Symbol.Name + " @ " + TimeFrame.Name, Text); + LastAlertTimeCross = Bars.OpenTimes[index - cnt]; + } + } + + if ((LastAlertTimeLevel == unix_epoch) && (prev_index == index)) LastAlertTimeLevel = Bars.OpenTimes.LastValue; + if ((LastAlertTimeCross == unix_epoch) && (prev_index == index)) LastAlertTimeCross = Bars.OpenTimes.LastValue; + prev_index = index; + } + + // Simple moving average to seed the first value of the exponential moving average. + private double GetAverage(DataSeries series, int index, int period) + { + var lastIndex = index - period; + double sum = 0; + + for (var i = index; i > lastIndex; i--) + { + sum += series[i]; + } + + return sum / period; + } + } +} \ No newline at end of file diff --git a/QQE.mq4 b/QQE.mq4 index 21bccae..4efa022 100644 --- a/QQE.mq4 +++ b/QQE.mq4 @@ -7,45 +7,58 @@ //+------------------------------------------------------------------+ #property copyright "www.EarnForex.com, 2010-2022" #property link "https://www.earnforex.com/metatrader-indicators/QQE/" -#property version "1.02" +#property version "1.03" #property strict #property description "QQE - Qualitative Quantitative Estimation." #property description "Calculated as two indicators:" #property description "1) MA on RSI" #property description "2) Difference of MA on RSI and MA of MA of ATR of MA of RSI" -#property description "The signal for buy is when blue line crosses level 50 from below" -#property description "after crossing the yellow line from below." -#property description "The signal for sell is when blue line crosses level 50 from above" -#property description "after crossing the yellow line from above." +#property description "The signal for buy is when blue line crosses level 50 from below after crossing the yellow line from below." +#property description "The signal for sell is when blue line crosses level 50 from above after crossing the yellow line from above." #property indicator_separate_window #property indicator_buffers 5 -#property indicator_plots 2 #property indicator_color1 clrDodgerBlue #property indicator_width1 2 #property indicator_label1 "RSI MA" #property indicator_color2 clrYellow #property indicator_style2 STYLE_DOT #property indicator_label2 "Smoothed" +#property indicator_type3 DRAW_NONE +#property indicator_type4 DRAW_NONE +#property indicator_type5 DRAW_NONE #property indicator_level1 50 #property indicator_levelcolor clrAqua #property indicator_levelstyle STYLE_DOT // Inputs input int SF = 5; // Smoothing Factor +input bool AlertOnCrossover = false; +input bool AlertOnLevel = false; input int AlertLevel = 50; +input bool ArrowsOnCrossover = true; +input color CrossoverUpArrow = clrGreen; +input color CrossoverDnArrow = clrRed; +input bool ArrowsOnLevel = true; +input color LevelUpArrow = clrGreen; +input color LevelDnArrow = clrRed; input bool NativeAlerts = false; input bool EmailAlerts = false; input bool NotificationAlerts = false; +input ENUM_TIMEFRAMES UpperTimeframe = PERIOD_CURRENT; +input string ObjectPrefix = "QQE-"; -// Global variables +// Global variables: int RSI_Period = 14; int Wilders_Period; int StartBar; -datetime LastAlertTime = D'1970.01.01'; +datetime LastAlertTimeCross, LastAlertTimeLevel; -// Buffers +// For MTF support: +string IndicatorFileName; + +// Buffers: double TrLevelSlow[]; double AtrRsi[]; double MaAtrRsi[]; @@ -54,6 +67,8 @@ double RsiMa[]; void OnInit() { + LastAlertTimeCross = 0; + LastAlertTimeLevel = 0; Wilders_Period = RSI_Period * 2 - 1; StartBar = MathMax(SF, Wilders_Period); @@ -66,6 +81,19 @@ void OnInit() SetIndexBuffer(4, Rsi); IndicatorShortName(StringConcatenate("QQE(", SF, ")")); IndicatorSetInteger(INDICATOR_DIGITS, 2); + + if (PeriodSeconds(UpperTimeframe) < PeriodSeconds()) + { + Print("UpperTimeframe should be above the current timeframe."); + IndicatorFileName = ""; + } + else if (PeriodSeconds(UpperTimeframe) > PeriodSeconds()) IndicatorFileName = WindowExpertName(); + else IndicatorFileName = ""; +} + +void OnDeinit(const int reason) +{ + ObjectsDeleteAll(ChartID(), ObjectPrefix); } int OnCalculate(const int rates_total, @@ -93,65 +121,196 @@ int OnCalculate(const int rates_total, RsiMa[i] = 0.0; } } - counted = Bars - counted - 1; + if ((counted > 0) && (IndicatorFileName != "")) counted -= PeriodSeconds(UpperTimeframe) / PeriodSeconds(); // Make the indicator redraw all current bars that constitute the upper timeframe bar. + else counted = Bars - counted - 1; if (counted > Bars - StartBar - 1) counted = Bars - StartBar - 1; - for (int i = counted; i >= 0; i--) + if (IndicatorFileName == "") { - Rsi[i] = iRSI(NULL, 0, RSI_Period, PRICE_CLOSE, i); + for (int i = counted; i >= 0; i--) + { + Rsi[i] = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE, i); + } } for (int i = counted; i >= 0; i--) { - RsiMa[i] = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); - AtrRsi[i] = MathAbs(RsiMa[i + 1] - RsiMa[i]); + if (IndicatorFileName == "") RsiMa[i] = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); + else + { + int shift = iBarShift(Symbol(), UpperTimeframe, Time[i]); // Get the upper timeframe shift based on the current timeframe bar's time. + RsiMa[i] = iCustom(Symbol(), UpperTimeframe, IndicatorFileName, SF, false, false, AlertLevel, false, LevelUpArrow, LevelDnArrow, false, CrossoverUpArrow, CrossoverDnArrow, false, false, false, UpperTimeframe, ObjectPrefix, 0, shift); + } + if (IndicatorFileName == "") AtrRsi[i] = MathAbs(RsiMa[i + 1] - RsiMa[i]); } - for (int i = counted; i >= 0; i--) + if (IndicatorFileName == "") { - MaAtrRsi[i] = iMAOnArray(AtrRsi, 0, Wilders_Period, 0, MODE_EMA, i); + for (int i = counted; i >= 0; i--) + { + MaAtrRsi[i] = iMAOnArray(AtrRsi, 0, Wilders_Period, 0, MODE_EMA, i); + } } int i = counted + 1; - double tr = TrLevelSlow[i]; - double rsi1 = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); - + double tr = 0, rsi1 = 0; + + if (IndicatorFileName == "") + { + tr = TrLevelSlow[i]; + rsi1 = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); + } + while (i > 0) { i--; - double rsi0 = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); - double dar = iMAOnArray(MaAtrRsi, 0, Wilders_Period, 0, MODE_EMA, i) * 4.236; - double dv = tr; - if (rsi0 < tr) + if (IndicatorFileName == "") { - tr = rsi0 + dar; - if ((rsi1 < dv) && (tr > dv)) tr = dv; + double rsi0 = iMAOnArray(Rsi, 0, SF, 0, MODE_EMA, i); + double dar = iMAOnArray(MaAtrRsi, 0, Wilders_Period, 0, MODE_EMA, i) * 4.236; + double dv = tr; + if (rsi0 < tr) + { + tr = rsi0 + dar; + if ((rsi1 < dv) && (tr > dv)) tr = dv; + } + else if (rsi0 > tr) + { + tr = rsi0 - dar; + if ((rsi1 > dv) && (tr < dv)) tr = dv; + } + rsi1 = rsi0; + TrLevelSlow[i] = tr; } - else if (rsi0 > tr) + else { - tr = rsi0 - dar; - if ((rsi1 > dv) && (tr < dv)) tr = dv; + int shift = iBarShift(Symbol(), UpperTimeframe, Time[i]); // Get the upper timeframe shift based on the current timeframe bar's time. + TrLevelSlow[i] = iCustom(Symbol(), UpperTimeframe, IndicatorFileName, SF, false, false, AlertLevel, false, LevelUpArrow, LevelDnArrow, false, CrossoverUpArrow, CrossoverDnArrow, false, false, false, UpperTimeframe, ObjectPrefix, 1, shift); + } + + // Arrows: + if ((i > 0) || (IndicatorFileName != "")) // In MTF mode, check as soon as possible. + { + // Prepare for multi-timeframe mode. + int cur_i = i; + int pre_i = i + 1; + // Actual MTF (to avoid non-existing signals): + if ((IndicatorFileName != "") && (i < PeriodSeconds(UpperTimeframe) / PeriodSeconds())) // Can safely skip this step if processing old bars. + { + // Find the bar that corresponds to the upper timeframe's latest finished bar. + int customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cur_i]); + cur_i++; + while (iBarShift(Symbol(), UpperTimeframe, Time[cur_i]) == customIndex) + { + cur_i++; + } + // Find the bar that corresponds to the upper timeframe's pre-latest finished bar. + customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cur_i]); + pre_i = cur_i + 1; + while (iBarShift(Symbol(), UpperTimeframe, Time[pre_i]) == customIndex) + { + pre_i++; + } + cur_i = pre_i - 1; // Use oldest lower timeframe bar inside that upper timeframe bar. + } + + if (ArrowsOnCrossover) + { + string name = ObjectPrefix + "CArrow" + TimeToString(Time[cur_i]); + if ((RsiMa[pre_i] < TrLevelSlow[pre_i]) && (RsiMa[cur_i] > TrLevelSlow[cur_i])) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_THUMB_UP, 0, Time[cur_i], Low[cur_i] - 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelUpArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_TOP); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + else if ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[cur_i] < TrLevelSlow[cur_i])) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_THUMB_DOWN, 0, Time[cur_i], High[cur_i] + 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelDnArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + } + if (ArrowsOnLevel) + { + string name = ObjectPrefix + "LArrow" + TimeToString(Time[cur_i]); + if ((RsiMa[pre_i] < AlertLevel) && (RsiMa[cur_i] > AlertLevel)) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_UP, 0, Time[cur_i], Low[cur_i] - 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelUpArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_TOP); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + else if ((RsiMa[pre_i] > AlertLevel) && (RsiMa[cur_i] < AlertLevel)) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_DOWN, 0, Time[cur_i], High[cur_i] + 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelDnArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + } } - TrLevelSlow[i] = tr; - rsi1 = rsi0; } if ((!NativeAlerts) && (!EmailAlerts) && (!NotificationAlerts)) return rates_total; - - if ((((RsiMa[i + 1] < AlertLevel) && (RsiMa[i] > AlertLevel)) || ((RsiMa[i + 1] > AlertLevel) && (RsiMa[i] < AlertLevel))) && (Time[0] > LastAlertTime)) + if ((!AlertOnCrossover) && (!AlertOnLevel)) return rates_total; + + // Prepare for multi-timeframe mode. + int pre_i = 2; + if (IndicatorFileName != "") { - string base = Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()); - string Subj = base + ", " + IntegerToString(AlertLevel) + " level Cross Up"; - if ((RsiMa[i + 1] > AlertLevel) && (RsiMa[i] < AlertLevel)) Subj = base + " " + IntegerToString(AlertLevel) + " level Cross Down"; - string Msg = Subj + " @ " + TimeToString(TimeLocal(), TIME_SECONDS); - DoAlerts(Msg, Subj); + // Find the bar that corresponds to the upper timeframe's latest finished bar. + int cnt = 1; + int customIndex = iBarShift(Symbol(), UpperTimeframe, Time[0]); + while (iBarShift(Symbol(), UpperTimeframe, Time[cnt]) == customIndex) + { + cnt++; + } + i = cnt; + // Find the bar that corresponds to the upper timeframe's pre-latest finished bar. + customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cnt]); + cnt++; + while (iBarShift(Symbol(), UpperTimeframe, Time[cnt]) == customIndex) + { + cnt++; + } + pre_i = cnt; } - + else i = 1; // Non-MTF. + + if (AlertOnLevel) + { + if ((LastAlertTimeLevel > 0) && (((RsiMa[pre_i] < AlertLevel) && (RsiMa[i] > AlertLevel)) || ((RsiMa[pre_i] > AlertLevel) && (RsiMa[i] < AlertLevel))) && (Time[i - 1] > LastAlertTimeLevel)) + { + string base = "QQE " + Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()); + string text = base + ", " + IntegerToString(AlertLevel) + " level Cross Up"; + if ((RsiMa[pre_i] > AlertLevel) && (RsiMa[i] < AlertLevel)) text = base + " " + IntegerToString(AlertLevel) + " level Cross Down"; + DoAlerts(text); + LastAlertTimeLevel = Time[i - 1]; + } + } + + if (AlertOnCrossover) + { + if ((LastAlertTimeCross > 0) && (((RsiMa[pre_i] < TrLevelSlow[pre_i]) && (RsiMa[i] > TrLevelSlow[i])) || ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[i] < TrLevelSlow[i]))) && (Time[i - 1] > LastAlertTimeCross)) + { + string base = "QQE " + Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()); + string text = base + ", RSI MA crossed Smoothed Line from below."; + if ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[i] < TrLevelSlow[i])) text = base + ", RSI MA crossed Smoothed Line from above."; + DoAlerts(text); + LastAlertTimeCross = Time[i - 1]; + } + } + + if (LastAlertTimeLevel == 0) LastAlertTimeLevel = Time[0]; + if (LastAlertTimeCross == 0) LastAlertTimeCross = Time[0]; + return rates_total; } -void DoAlerts(string msgText, string eMailSub) +void DoAlerts(const string msgText) { if (NativeAlerts) Alert(msgText); - if (EmailAlerts) SendMail(eMailSub, msgText); + if (EmailAlerts) SendMail(msgText, msgText); if (NotificationAlerts) SendNotification(msgText); } diff --git a/QQE.mq5 b/QQE.mq5 index 86235c0..240fac2 100644 --- a/QQE.mq5 +++ b/QQE.mq5 @@ -7,16 +7,14 @@ //+------------------------------------------------------------------+ #property copyright "www.EarnForex.com, 2010-2022" #property link "https://www.earnforex.com/metatrader-indicators/QQE/" -#property version "1.02" +#property version "1.03" #property description "QQE - Qualitative Quantitative Estimation." #property description "Calculated as two indicators:" #property description "1) MA on RSI" #property description "2) Difference of MA on RSI and MA of MA of ATR of MA of RSI" -#property description "The signal for buy is when blue line crosses level 50 from below" -#property description "after crossing the yellow line from below." -#property description "The signal for sell is when blue line crosses level 50 from above" -#property description "after crossing the yellow line from above." +#property description "The signal for buy is when blue line crosses level 50 from below after crossing the yellow line from below." +#property description "The signal for sell is when blue line crosses level 50 from above after crossing the yellow line from above." #property indicator_separate_window #property indicator_buffers 6 @@ -35,18 +33,31 @@ // Inputs input int SF = 5; // Smoothing Factor +input bool AlertOnCrossover = false; +input bool AlertOnLevel = false; input int AlertLevel = 50; +input bool ArrowsOnCrossover = true; +input color CrossoverUpArrow = clrGreen; +input color CrossoverDnArrow = clrRed; +input bool ArrowsOnLevel = true; +input color LevelUpArrow = clrGreen; +input color LevelDnArrow = clrRed; input bool NativeAlerts = false; input bool EmailAlerts = false; input bool NotificationAlerts = false; +input ENUM_TIMEFRAMES UpperTimeframe = PERIOD_CURRENT; +input string ObjectPrefix = "QQE-"; -// Global variables +// Global variables: int RSI_Period = 14; int Wilders_Period; int StartBar; -int LastAlertBars = 0; +datetime LastAlertTimeCross, LastAlertTimeLevel; -// Buffers +// For MTF support: +int QQE_handle; + +// Buffers: double TrLevelSlow[]; double AtrRsi[]; double MaAtrRsi[]; @@ -59,6 +70,8 @@ int myRSI; void OnInit() { + LastAlertTimeCross = 0; + LastAlertTimeLevel = 0; Wilders_Period = RSI_Period * 2 - 1; StartBar = MathMax(SF, Wilders_Period); @@ -82,23 +95,47 @@ void OnInit() PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, StartBar); PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, StartBar); - IndicatorSetString(INDICATOR_SHORTNAME, "QQE(" + IntegerToString(SF) + ")"); IndicatorSetInteger(INDICATOR_DIGITS, 2); - myRSI = iRSI(NULL, 0, RSI_Period, PRICE_CLOSE); + myRSI = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE); + + if (PeriodSeconds(UpperTimeframe) > PeriodSeconds()) + { + string IndicatorFileName = MQLInfoString(MQL_PROGRAM_NAME); + QQE_handle = iCustom(Symbol(), UpperTimeframe, IndicatorFileName, SF, false, false, AlertLevel, false, LevelUpArrow, LevelDnArrow, false, CrossoverUpArrow, CrossoverDnArrow, false, false, false, UpperTimeframe, ObjectPrefix); + } + else + { + QQE_handle = INVALID_HANDLE; + if (PeriodSeconds(UpperTimeframe) < PeriodSeconds()) + { + Print("UpperTimeframe should be above the current timeframe."); + } + } + + IndicatorSetString(INDICATOR_SHORTNAME, "QQE(" + IntegerToString(SF) + ")"); +} + +void OnDeinit(const int reason) +{ + ObjectsDeleteAll(ChartID(), ObjectPrefix); } int OnCalculate(const int rates_total, const int prev_calculated, - const datetime &time[], + const datetime &Time[], const double &open[], - const double &high[], - const double &low[], + const double &High[], + const double &Low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { + ArraySetAsSeries(Time, true); + ArraySetAsSeries(High, true); + ArraySetAsSeries(Low, true); + if (rates_total <= StartBar) return 0; int counted = prev_calculated - 1; @@ -115,69 +152,218 @@ int OnCalculate(const int rates_total, } } - counted = rates_total - counted - 1; - if (counted > rates_total - StartBar - 1) counted = rates_total - StartBar - 1; + bool rec_only_latest_upper_bar = false; // Recalculate only the latest upper timeframe bar. + if ((counted > 0) && (QQE_handle != INVALID_HANDLE)) + { + counted = prev_calculated - PeriodSeconds(UpperTimeframe) / PeriodSeconds(); // Make the indicator redraw all current bars that constitute the upper timeframe bar. + rec_only_latest_upper_bar = true; + } + else + { + counted = rates_total - counted - 1; + if (counted > rates_total - StartBar - 1) counted = rates_total - StartBar - 1; + } - if (CopyBuffer(myRSI, 0, 0, counted + 2, Rsi) != counted + 2) return 0; - - // Fills "counted" cells of RsiMA with EMA of Rsi. - CalculateEMA(counted + 1, SF, Rsi, RsiMa); + if (QQE_handle == INVALID_HANDLE) + { + if (CopyBuffer(myRSI, 0, 0, counted + 2, Rsi) != counted + 2) return 0; + + // Fills "counted" cells of RsiMA with EMA of Rsi. + CalculateEMA(counted + 1, SF, Rsi, RsiMa); - for (int i = counted; i >= 0; i--) + for (int i = counted; i >= 0; i--) + { + AtrRsi[i] = MathAbs(RsiMa[i + 1] - RsiMa[i]); + } + } + else { - AtrRsi[i] = MathAbs(RsiMa[i + 1] - RsiMa[i]); + for (int i = counted; i >= 0; i--) + { + double buf[1]; + if (rec_only_latest_upper_bar) + if (Time[i] < iTime(Symbol(), UpperTimeframe, 0)) continue; // Skip bars older than the upper current bar. + int n = CopyBuffer(QQE_handle, 0, Time[i], 1, buf); + if (n == 1) RsiMa[i] = buf[0]; + else return prev_calculated; + } } - // Fills "counted" cells of MaAtrRsi with EMA of AtrRsi. - CalculateEMA(counted, Wilders_Period, AtrRsi, MaAtrRsi); - int i = counted + 1; - double tr = TrLevelSlow[i]; - double rsi1 = RsiMa[i]; + double tr = 0, rsi1 = 0; + + if (QQE_handle == INVALID_HANDLE) + { + // Fills "counted" cells of MaAtrRsi with EMA of AtrRsi. + CalculateEMA(counted, Wilders_Period, AtrRsi, MaAtrRsi); + tr = TrLevelSlow[i]; + rsi1 = RsiMa[i]; + CalculateEMA(counted, Wilders_Period, MaAtrRsi, MaMaAtrRsi); + } - CalculateEMA(counted, Wilders_Period, MaAtrRsi, MaMaAtrRsi); while (i > 0) { i--; - double rsi0 = RsiMa[i]; - double dar = MaMaAtrRsi[i] * 4.236; - double dv = tr; - - if (rsi0 < tr) + if (QQE_handle == INVALID_HANDLE) { - tr = rsi0 + dar; - if ((rsi1 < dv) && (tr > dv)) tr = dv; + double rsi0 = RsiMa[i]; + double dar = MaMaAtrRsi[i] * 4.236; + double dv = tr; + + if (rsi0 < tr) + { + tr = rsi0 + dar; + if ((rsi1 < dv) && (tr > dv)) tr = dv; + } + else if (rsi0 > tr) + { + tr = rsi0 - dar; + if ((rsi1 > dv) && (tr < dv)) tr = dv; + } + + TrLevelSlow[i] = tr; + rsi1 = rsi0; } - else if (rsi0 > tr) + else { - tr = rsi0 - dar; - if ((rsi1 > dv) && (tr < dv)) tr = dv; + double buf[1]; + if (rec_only_latest_upper_bar) + if (Time[i] < iTime(Symbol(), UpperTimeframe, 0)) continue; // Skip bars older than the upper current bar. + int n = CopyBuffer(QQE_handle, 1, Time[i], 1, buf); + if (n == 1) TrLevelSlow[i] = buf[0]; + else return prev_calculated; } - TrLevelSlow[i] = tr; - rsi1 = rsi0; + // Arrows: + if ((i > 0) || (QQE_handle != INVALID_HANDLE)) // In MTF mode, check as soon as possible. + { + // Prepare for multi-timeframe mode. + int cur_i = i; + int pre_i = i + 1; + // Actual MTF (to avoid non-existing signals): + if ((QQE_handle != INVALID_HANDLE) && (i < PeriodSeconds(UpperTimeframe) / PeriodSeconds())) // Can safely skip this step if processing old bars. + { + // Find the bar that corresponds to the upper timeframe's latest finished bar. + int customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cur_i]); + cur_i++; + while ((cur_i < rates_total) && (iBarShift(Symbol(), UpperTimeframe, Time[cur_i]) == customIndex)) + { + cur_i++; + } + if (cur_i == rates_total) return prev_calculated; + // Find the bar that corresponds to the upper timeframe's pre-latest finished bar. + customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cur_i]); + pre_i = cur_i + 1; + while ((pre_i < rates_total) && (iBarShift(Symbol(), UpperTimeframe, Time[pre_i])) == customIndex) + { + pre_i++; + } + if (pre_i == rates_total) return prev_calculated; + cur_i = pre_i - 1; // Use oldest lower timeframe bar inside that upper timeframe bar. + } + + if (ArrowsOnCrossover) + { + string name = ObjectPrefix + "CArrow" + TimeToString(Time[cur_i]); + if ((RsiMa[pre_i] < TrLevelSlow[pre_i]) && (RsiMa[cur_i] > TrLevelSlow[cur_i])) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_THUMB_UP, 0, Time[cur_i], Low[cur_i] - 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelUpArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_TOP); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + else if ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[cur_i] < TrLevelSlow[cur_i])) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_THUMB_DOWN, 0, Time[cur_i], High[cur_i] + 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelDnArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + } + if (ArrowsOnLevel) + { + string name = ObjectPrefix + "LArrow" + TimeToString(Time[cur_i]); + if ((RsiMa[pre_i] < AlertLevel) && (RsiMa[cur_i] > AlertLevel)) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_UP, 0, Time[cur_i], Low[cur_i] - 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelUpArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_TOP); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + else if ((RsiMa[pre_i] > AlertLevel) && (RsiMa[cur_i] < AlertLevel)) + { + ObjectCreate(ChartID(), name, OBJ_ARROW_DOWN, 0, Time[cur_i], High[cur_i] + 1 * _Point); + ObjectSetInteger(ChartID(), name, OBJPROP_COLOR, LevelDnArrow); + ObjectSetInteger(ChartID(), name, OBJPROP_ANCHOR, ANCHOR_BOTTOM); + ObjectSetInteger(ChartID(), name, OBJPROP_WIDTH, 5); + } + } + } } if ((!NativeAlerts) && (!EmailAlerts) && (!NotificationAlerts)) return rates_total; + if ((!AlertOnCrossover) && (!AlertOnLevel)) return rates_total; - if ((((RsiMa[i + 1] < AlertLevel) && (RsiMa[i] > AlertLevel)) || ((RsiMa[i + 1] > AlertLevel) && (RsiMa[i] < AlertLevel))) && (LastAlertBars < rates_total)) + // Prepare for multi-timeframe mode. + int pre_i = 2; + if (QQE_handle != INVALID_HANDLE) { - string base = Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()); - string Subj = base + ", " + IntegerToString(AlertLevel) + " level Cross Up"; - if ((RsiMa[i + 1] > AlertLevel) && (RsiMa[i] < AlertLevel)) Subj = base + " " + IntegerToString(AlertLevel) + " level Cross Down"; - string Msg = Subj + " @ " + TimeToString(TimeLocal(), TIME_SECONDS); - DoAlerts(Msg, Subj); - LastAlertBars = rates_total; + // Find the bar that corresponds to the upper timeframe's latest finished bar. + int cnt = 1; + int customIndex = iBarShift(Symbol(), UpperTimeframe, Time[0]); + while ((cnt < rates_total) && (iBarShift(Symbol(), UpperTimeframe, Time[cnt]) == customIndex)) + { + cnt++; + } + i = cnt; + // Find the bar that corresponds to the upper timeframe's pre-latest finished bar. + customIndex = iBarShift(Symbol(), UpperTimeframe, Time[cnt]); + cnt++; + while ((cnt < rates_total) && (iBarShift(Symbol(), UpperTimeframe, Time[cnt]) == customIndex)) + { + cnt++; + } + pre_i = cnt; + } + else i = 1; // Non-MTF. + + if (AlertOnLevel) + { + if ((LastAlertTimeLevel > 0) && (((RsiMa[pre_i] < AlertLevel) && (RsiMa[i] > AlertLevel)) || ((RsiMa[pre_i] > AlertLevel) && (RsiMa[i] < AlertLevel))) && (Time[i - 1] > LastAlertTimeLevel)) + { + string TextNative = IntegerToString(AlertLevel) + " level Cross "; + if ((RsiMa[pre_i] > AlertLevel) && (RsiMa[i] < AlertLevel)) TextNative += "Down"; + else TextNative += "Up"; + string Text = "QQE " + Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()) + " " + TextNative; + DoAlerts(Text, TextNative); + LastAlertTimeLevel = Time[i - 1]; + } } + if (AlertOnCrossover) + { + if ((LastAlertTimeCross > 0) && (((RsiMa[pre_i] < TrLevelSlow[pre_i]) && (RsiMa[i] > TrLevelSlow[i])) || ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[i] < TrLevelSlow[i]))) && (Time[i - 1] > LastAlertTimeCross)) + { + string TextNative = "RSI MA crossed Smoothed Line from "; + if ((RsiMa[pre_i] > TrLevelSlow[pre_i]) && (RsiMa[i] < TrLevelSlow[i])) TextNative += "above."; + else TextNative += "below."; + string Text = "QQE " + Symbol() + ", TF: " + TimeframeToString((ENUM_TIMEFRAMES)Period()) + " " + TextNative; + DoAlerts(Text, TextNative); + LastAlertTimeCross = Time[i - 1]; + } + } + + if (LastAlertTimeLevel == 0) LastAlertTimeLevel = Time[0]; + if (LastAlertTimeCross == 0) LastAlertTimeCross = Time[0]; + return rates_total; } -void DoAlerts(string msgText, string eMailSub) +void DoAlerts(string Text, string TextNative) { - if (NativeAlerts) Alert(msgText); - if (EmailAlerts) SendMail(eMailSub, msgText); - if (NotificationAlerts) SendNotification(msgText); + if (NativeAlerts) Alert(TextNative); + if (EmailAlerts) SendMail(Text, Text); + if (NotificationAlerts) SendNotification(Text); } string TimeframeToString(ENUM_TIMEFRAMES P)