Forward-Simulation/Article-22323-Forward-Sim-V1/ForwardSimEngien.mq5

568 lines
22 KiB
MQL5
Raw Permalink Normal View History

2026-06-22 12:43:00 +02:00
//+------------------------------------------------------------------+
//| 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_");
}
//+------------------------------------------------------------------+