//+------------------------------------------------------------------+ //| ForwardSimEngine.mq5 | //| Forward Simulation Engine – EMA Crossover | //| Visualized as Predicted Future Candles (M15) | //| Git, Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com/en/users/johnhlomohang/ | //+------------------------------------------------------------------+ #property copyright "Git, Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/en/users/johnhlomohang/" #property version "1.00" #property indicator_chart_window #property indicator_buffers 2 #property indicator_plots 2 #property indicator_label1 "Fast EMA" #property indicator_type1 DRAW_LINE #property indicator_color1 clrDodgerBlue #property indicator_style1 STYLE_SOLID #property indicator_width1 2 #property indicator_label2 "Slow EMA" #property indicator_type2 DRAW_LINE #property indicator_color2 clrOrangeRed #property indicator_style2 STYLE_SOLID #property indicator_width2 2 //+------------------------------------------------------------------+ //| INDICATOR BUFFERS | //+------------------------------------------------------------------+ double FastEMABuffer[]; double SlowEMABuffer[]; //+------------------------------------------------------------------+ //| INPUT PARAMETERS | //+------------------------------------------------------------------+ input int FastEMA_Period = 9; // Fast EMA period input int SlowEMA_Period = 21; // Slow EMA period input int FutureBars = 10; // Number of future candles to project input double SpreadMultiplier = 2.0; // Wick size multiplier input bool AutoAnchor = true; // Auto-move anchor to latest cross bar input string AnchorLineName = "FSE_Anchor"; // Anchor vertical line name input color BullishColor = clrDodgerBlue; // Bullish body color input color BearishColor = clrCrimson; // Bearish body color input color WickColor = clrDimGray; // Wick color input bool ShowZoneLabel = true; // Show projection label input bool ShowSeparatorLine = true; // Show dashed separator at anchor input int InvalidationPips = 10; // Pip distance to trigger invalidation //+------------------------------------------------------------------+ //| STRUCTS | //+------------------------------------------------------------------+ struct PredictedCandle { double open; double high; double low; double close; bool bullish; }; //+------------------------------------------------------------------+ //| GLOBAL VARS | //+------------------------------------------------------------------+ int g_FastHandle = INVALID_HANDLE; int g_SlowHandle = INVALID_HANDLE; int g_LastSignal = 0; // +1 bull, -1 bear, 0 none bool g_SignalActive = false; double g_SignalPrice = 0.0; datetime g_SignalBarTime = 0; datetime g_DrawnAnchor = 0; // anchor time of the last successful draw //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- Bind and configure the two visible EMA plot buffers SetIndexBuffer(0, FastEMABuffer, INDICATOR_DATA); SetIndexBuffer(1, SlowEMABuffer, INDICATOR_DATA); ArraySetAsSeries(FastEMABuffer, false); ArraySetAsSeries(SlowEMABuffer, false); PlotIndexSetInteger(0, PLOT_DRAW_BEGIN, FastEMA_Period); PlotIndexSetInteger(1, PLOT_DRAW_BEGIN, SlowEMA_Period); //--- Internal calculation handles g_FastHandle = iMA(_Symbol, _Period, FastEMA_Period, 0, MODE_EMA, PRICE_CLOSE); g_SlowHandle = iMA(_Symbol, _Period, SlowEMA_Period, 0, MODE_EMA, PRICE_CLOSE); if(g_FastHandle == INVALID_HANDLE || g_SlowHandle == INVALID_HANDLE) { Print("ForwardSimEngine [ERROR]: iMA handle creation failed. " "Symbol=", _Symbol, " TF=", EnumToString(_Period)); return INIT_FAILED; } Print("ForwardSimEngine [INIT]: OK FastEMA=", FastEMA_Period, " SlowEMA=", SlowEMA_Period, " FutureBars=", FutureBars, " InvalidationPips=", InvalidationPips); EventSetTimer(3); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); CleanAllObjects(); Print("ForwardSimEngine [DEINIT]: objects cleaned, reason=", reason); } //+-----------------------------------------------------------------------+ //| ON CALCULATE | //| NOTE: time[], close[] (and all price arrays) passed in here are | //| NOT set as series – index 0 = oldest bar, rates_total-1 = newest | //+-----------------------------------------------------------------------+ 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 < SlowEMA_Period + 5) { Print("ForwardSimEngine [WARN]: Not enough bars: ", rates_total); return 0; } //--- How many bars to (re)calculate int limit = (prev_calculated > 1) ? rates_total - prev_calculated + 1 : rates_total; //--- Start index in the buffer (oldest bar that needs updating) int startBar = rates_total - limit; //--- Local arrays for engine logic – we own these, safe to size freely double tmpFast[], tmpSlow[]; ArraySetAsSeries(tmpFast, false); ArraySetAsSeries(tmpSlow, false); ArrayResize(tmpFast, rates_total); ArrayResize(tmpSlow, rates_total); //--- Copy ALL bars into local arrays (needed by RunEngine for crossover) int copiedFast = CopyBuffer(g_FastHandle, 0, 0, rates_total, tmpFast); int copiedSlow = CopyBuffer(g_SlowHandle, 0, 0, rates_total, tmpSlow); if(copiedFast <= 0 || copiedSlow <= 0) { Print("ForwardSimEngine [WARN]: CopyBuffer returned <=0. fast=", copiedFast, " slow=", copiedSlow); return prev_calculated; } //--- Write into plot buffers ONLY within the confirmed copied range //--- and only within what MT5 has allocated (ArraySize guard). int bufSzFast = ArraySize(FastEMABuffer); int bufSzSlow = ArraySize(SlowEMABuffer); int safeFast = MathMin(copiedFast, bufSzFast); int safeSlow = MathMin(copiedSlow, bufSzSlow); for(int i = startBar; i < safeFast; i++) FastEMABuffer[i] = tmpFast[i]; for(int i = startBar; i < safeSlow; i++) SlowEMABuffer[i] = tmpSlow[i]; //--- Run the simulation engine RunEngine(rates_total, time, close, tmpFast, tmpSlow); return rates_total; } //+------------------------------------------------------------------+ //| ON TIMER | //+------------------------------------------------------------------+ void OnTimer() { //--- If user moved the anchor manually, force a redraw on next tick if(!AutoAnchor) { datetime curAnchor = GetAnchorTime(); if(curAnchor != g_DrawnAnchor && curAnchor != 0) { g_DrawnAnchor = 0; ChartRedraw(); } } } //+------------------------------------------------------------------+ //| CORE ENGINE | //| All arrays: index 0 = oldest, rates_total-1 = live forming bar | //| rates_total-2 = last fully closed bar (bar index 1) | //| rates_total-3 = bar before that (bar index 2) | //+------------------------------------------------------------------+ void RunEngine(const int rates_total, const datetime &time[], const double &close[], const double &fastBuf[], const double &slowBuf[]) { int barLive = rates_total - 1; // live/forming bar int barClosed = rates_total - 2; // last fully closed bar int barPrev = rates_total - 3; // bar before that if(barPrev < SlowEMA_Period) return; //--- 1. Crossover detection //--- We look at the two most recently CLOSED bars (barPrev and barClosed). //--- Cross occurs when the fast EMA crossed over the slow EMA between them. double fCur = fastBuf[barClosed]; double fPrev = fastBuf[barPrev]; double sCur = slowBuf[barClosed]; double sPrev = slowBuf[barPrev]; int newSignal = 0; if(fPrev <= sPrev && fCur > sCur) newSignal = 1; // bullish if(fPrev >= sPrev && fCur < sCur) newSignal = -1; // bearish //--- Heartbeat log (first 3 ticks + every new signal) static int s_ticks = 0; s_ticks++; if(s_ticks <= 3 || newSignal != 0) { Print("ForwardSimEngine [TICK #", s_ticks, "]", " fCur=", DoubleToString(fCur, _Digits), " sCur=", DoubleToString(sCur, _Digits), " gap=", DoubleToString(fCur - sCur, _Digits), " newSig=", newSignal, " active=", g_SignalActive, " lastSig=",g_LastSignal); } //--- 2. Latch new signal if(newSignal != 0) { g_LastSignal = newSignal; g_SignalActive = true; g_SignalPrice = close[barClosed]; g_SignalBarTime = time[barClosed]; g_DrawnAnchor = 0; // force full redraw Print("ForwardSimEngine [SIGNAL]: ", (newSignal == 1 ? ">>> BULLISH CROSS <<<" : ">>> BEARISH CROSS <<<"), " bar=", TimeToString(g_SignalBarTime), " px=", DoubleToString(g_SignalPrice, _Digits)); } //--- 3. Invalidation check if(g_SignalActive) { double livePx = close[barLive]; //--- 1 pip = 10 * _Point for a 5-digit broker (covers 3-digit too) double pipSize = _Point * 10.0; double thresh = InvalidationPips * pipSize; bool inv = false; if(g_LastSignal == 1 && livePx < g_SignalPrice - thresh) inv = true; if(g_LastSignal == -1 && livePx > g_SignalPrice + thresh) inv = true; if(inv) { Print("ForwardSimEngine [INVALIDATED]", " signalPx=", DoubleToString(g_SignalPrice, _Digits), " livePx=", DoubleToString(livePx, _Digits), " thresh=", DoubleToString(thresh, _Digits)); g_SignalActive = false; g_LastSignal = 0; g_DrawnAnchor = 0; CleanAllObjects(); DrawInvalidationLabel(g_SignalBarTime, g_SignalPrice); ChartRedraw(); return; } } //--- 4. Only draw/redraw when there is an active signal if(!g_SignalActive || g_LastSignal == 0) return; datetime anchorTime = (AutoAnchor) ? g_SignalBarTime : GetAnchorTime(); if(anchorTime == 0) anchorTime = g_SignalBarTime; //--- Skip if already drawn at this anchor if(anchorTime == g_DrawnAnchor) return; g_DrawnAnchor = anchorTime; //--- 5. Build synthetic candles double emaSlope = fCur - fPrev; PredictedCandle pred[]; ArrayResize(pred, FutureBars); GeneratePrediction(close[barClosed], emaSlope, pred); //--- 6. Render CleanAllObjects(); DrawAllCandles(anchorTime, pred); if(ShowSeparatorLine) DrawSeparator(anchorTime); if(ShowZoneLabel) DrawZoneLabel(anchorTime, close[barClosed]); UpdateAnchorLine(anchorTime); ChartRedraw(); Print("ForwardSimEngine [DRAWN]", " signal=", g_LastSignal, " anchor=", TimeToString(anchorTime), " bars=", FutureBars, " slope=", DoubleToString(emaSlope, _Digits)); } //+------------------------------------------------------------------+ //| PREDICTION ENGINE | //+------------------------------------------------------------------+ void GeneratePrediction(double startPrice, double emaSlope, PredictedCandle &out[]) { //--- Wick size in price: 1 pip × multiplier × 3 double pipVal = _Point * 10.0; double wickSz = pipVal * SpreadMultiplier * 3.0; if(wickSz < _Point * 5) wickSz = _Point * 5; //--- Step direction: preserve sign from signal, floor at 3 points minimum double direction = (g_LastSignal == 1) ? MathAbs(emaSlope) : -MathAbs(emaSlope); if(MathAbs(direction) < _Point * 3) direction = (g_LastSignal == 1 ? 1.0 : -1.0) * _Point * 3; double decayBase = 0.91; // each bar's move is 91% of the previous double prevClose = startPrice; MathSrand((int)(TimeLocal() % 32767)); for(int i = 0; i < FutureBars; i++) { double decay = MathPow(decayBase, i); double step = direction * decay; //--- ±15% noise relative to wickSz double noise = ((double)(MathRand() % 200) - 100.0) / 100.0 * wickSz * 0.15; step += noise; out[i].open = prevClose; out[i].close = prevClose + step; out[i].bullish = (out[i].close >= out[i].open); double bodyTop = MathMax(out[i].open, out[i].close); double bodyBot = MathMin(out[i].open, out[i].close); //--- Wick: 30%–120% of wickSz double upWk = wickSz * (0.3 + (double)(MathRand() % 90) * 0.01); double dnWk = wickSz * (0.3 + (double)(MathRand() % 90) * 0.01); out[i].high = bodyTop + upWk; out[i].low = bodyBot - dnWk; prevClose = out[i].close; } } //+------------------------------------------------------------------+ //| DRAW ALL CANDLES – project one bar-width ahead of anchor | //+------------------------------------------------------------------+ void DrawAllCandles(datetime startTime, PredictedCandle &candles[]) { int barSec = PeriodSeconds(); int total = ArraySize(candles); for(int i = 0; i < total; i++) { //--- +1 so first projected candle starts ONE bar after the anchor datetime t1 = startTime + (datetime)((i + 1) * barSec); DrawSingleCandle(i, t1, candles[i]); } } //+------------------------------------------------------------------+ //| DRAW ONE CANDLE | //+------------------------------------------------------------------+ void DrawSingleCandle(int idx, datetime t1, const PredictedCandle &c) { int barSec = PeriodSeconds(); string pfx = "FutureCandle_" + IntegerToString(idx); datetime t2 = t1 + (datetime)barSec; datetime tMid = t1 + (datetime)(barSec / 2); color bodyCol = c.bullish ? BullishColor : BearishColor; //--- Doji guard: ensure body has at least 1 point height double bOpen = c.open; double bClose = c.close; if(MathAbs(bOpen - bClose) < _Point) bClose = bOpen + (c.bullish ? _Point : -_Point); //--- Filled body string nm = pfx + "_body"; ObjectDelete(0, nm); if(ObjectCreate(0, nm, OBJ_RECTANGLE, 0, t1, bOpen, t2, bClose)) { ObjectSetInteger(0, nm, OBJPROP_COLOR, bodyCol); ObjectSetInteger(0, nm, OBJPROP_FILL, true); ObjectSetInteger(0, nm, OBJPROP_BACK, false); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } //--- Body outline (unfilled rectangle) nm = pfx + "_bord"; ObjectDelete(0, nm); if(ObjectCreate(0, nm, OBJ_RECTANGLE, 0, t1, bOpen, t2, bClose)) { ObjectSetInteger(0, nm, OBJPROP_COLOR, bodyCol); ObjectSetInteger(0, nm, OBJPROP_FILL, false); ObjectSetInteger(0, nm, OBJPROP_BACK, false); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } //--- Upper wick nm = pfx + "_wU"; ObjectDelete(0, nm); double wTop = MathMax(bOpen, bClose); if(ObjectCreate(0, nm, OBJ_TREND, 0, tMid, wTop, tMid, c.high)) { ObjectSetInteger(0, nm, OBJPROP_COLOR, WickColor); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } //--- Lower wick nm = pfx + "_wD"; ObjectDelete(0, nm); double wBot = MathMin(bOpen, bClose); if(ObjectCreate(0, nm, OBJ_TREND, 0, tMid, wBot, tMid, c.low)) { ObjectSetInteger(0, nm, OBJPROP_COLOR, WickColor); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_RAY_RIGHT, false); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } } //+------------------------------------------------------------------+ //| SEPARATOR | //+------------------------------------------------------------------+ void DrawSeparator(datetime t) { string nm = "FSE_Sep"; ObjectDelete(0, nm); if(ObjectCreate(0, nm, OBJ_VLINE, 0, t, 0)) { ObjectSetInteger(0, nm, OBJPROP_COLOR, clrSilver); ObjectSetInteger(0, nm, OBJPROP_STYLE, STYLE_DASH); ObjectSetInteger(0, nm, OBJPROP_WIDTH, 1); ObjectSetInteger(0, nm, OBJPROP_BACK, true); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } } //+------------------------------------------------------------------+ //| ZONE LABEL | //+------------------------------------------------------------------+ void DrawZoneLabel(datetime t, double price) { string nm = "FSE_Label"; string txt = (g_LastSignal == 1) ? "[ BULLISH PROJECTION ]" : "[ BEARISH PROJECTION ]"; color col = (g_LastSignal == 1) ? BullishColor : BearishColor; ObjectDelete(0, nm); if(ObjectCreate(0, nm, OBJ_TEXT, 0, t + (datetime)PeriodSeconds(), price)) { ObjectSetString(0, nm, OBJPROP_TEXT, txt); ObjectSetInteger(0, nm, OBJPROP_COLOR, col); ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 10); ObjectSetString(0, nm, OBJPROP_FONT, "Courier New"); ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } } //+------------------------------------------------------------------+ //| INVALIDATION LABEL | //+------------------------------------------------------------------+ void DrawInvalidationLabel(datetime t, double price) { string nm = "FSE_Invalid"; ObjectDelete(0, nm); if(ObjectCreate(0, nm, OBJ_TEXT, 0, t + (datetime)PeriodSeconds(), price)) { ObjectSetString(0, nm, OBJPROP_TEXT, "[ INVALIDATED - AWAITING NEXT CROSS ]"); ObjectSetInteger(0, nm, OBJPROP_COLOR, clrOrange); ObjectSetInteger(0, nm, OBJPROP_FONTSIZE, 9); ObjectSetString(0, nm, OBJPROP_FONT, "Courier New"); ObjectSetInteger(0, nm, OBJPROP_ANCHOR, ANCHOR_LEFT_LOWER); ObjectSetInteger(0, nm, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, nm, OBJPROP_HIDDEN, false); } } //+------------------------------------------------------------------+ //| ANCHOR LINE HELPERS | //+------------------------------------------------------------------+ void PlaceAnchorLine(datetime t) { ObjectDelete(0, AnchorLineName); if(ObjectCreate(0, AnchorLineName, OBJ_VLINE, 0, t, 0)) { ObjectSetInteger(0, AnchorLineName, OBJPROP_COLOR, clrGold); ObjectSetInteger(0, AnchorLineName, OBJPROP_STYLE, STYLE_DASHDOTDOT); ObjectSetInteger(0, AnchorLineName, OBJPROP_WIDTH, 2); ObjectSetInteger(0, AnchorLineName, OBJPROP_SELECTABLE, true); ObjectSetInteger(0, AnchorLineName, OBJPROP_HIDDEN, false); } } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void UpdateAnchorLine(datetime t) { if(ObjectFind(0, AnchorLineName) < 0) PlaceAnchorLine(t); else ObjectSetInteger(0, AnchorLineName, OBJPROP_TIME, t); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ datetime GetAnchorTime() { if(ObjectFind(0, AnchorLineName) >= 0) return (datetime)ObjectGetInteger(0, AnchorLineName, OBJPROP_TIME); return 0; } //+------------------------------------------------------------------+ //| CLEANUP | //+------------------------------------------------------------------+ void CleanAllObjects() { ObjectsDeleteAll(0, "FutureCandle_"); ObjectsDeleteAll(0, "FSE_"); } //+------------------------------------------------------------------+