698 lines
28 KiB
MQL5
698 lines
28 KiB
MQL5
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Forward Simulation_V2.mq5 |
|
||
|
|
//| Copyright 2025, MetaQuotes Ltd. |
|
||
|
|
//| https://www.mql5.com/en/users/johnhlomohang/ |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
#property copyright "Copyright 2025, MetaQuotes Ltd."
|
||
|
|
#property link "https://www.mql5.com/en/users/johnhlomohang/"
|
||
|
|
#property version "2.00"
|
||
|
|
#property indicator_chart_window
|
||
|
|
#property indicator_buffers 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 = 30; // 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
|
||
|
|
input double CandleGapFraction = 0.08; // Gap between candles as fraction of bar width (0.04–0.20)
|
||
|
|
input int CounterCandleFreq = 4; // Insert 1 counter-trend candle every N candles (min 3)
|
||
|
|
input int AvgLookback = 50; // Bars used to measure average candle size
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| STRUCTS |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
struct PredictedCandle
|
||
|
|
{
|
||
|
|
double open;
|
||
|
|
double high;
|
||
|
|
double low;
|
||
|
|
double close;
|
||
|
|
bool bullish;
|
||
|
|
};
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| GLOBALS |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
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
|
||
|
|
|
||
|
|
//--- Measured average candle metrics (refreshed on each new signal)
|
||
|
|
double g_AvgBody = 0.0; // average |close - open| over AvgLookback bars
|
||
|
|
double g_AvgUpperWick = 0.0; // average (high - max(open,close))
|
||
|
|
double g_AvgLowerWick = 0.0; // average (min(open,close) - low)
|
||
|
|
double g_AvgRange = 0.0; // average (high - low) – full candle height
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| 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,
|
||
|
|
" AvgLookback=", AvgLookback);
|
||
|
|
|
||
|
|
//--- Pre-compute average candle metrics from history
|
||
|
|
CalcAvgCandleMetrics();
|
||
|
|
|
||
|
|
EventSetTimer(3);
|
||
|
|
return INIT_SUCCEEDED;
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Expert deinitialization function |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void OnDeinit(const int reason)
|
||
|
|
{
|
||
|
|
EventKillTimer();
|
||
|
|
CleanAllObjects();
|
||
|
|
Print("ForwardSimEngine [DEINIT]: objects cleaned, reason=", reason);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+-----------------------------------------------------------------------+
|
||
|
|
//| ON CALCULATE |
|
||
|
|
//+-----------------------------------------------------------------------+
|
||
|
|
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
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| CALC AVERAGE CANDLE METRICS |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void CalcAvgCandleMetrics()
|
||
|
|
{
|
||
|
|
int lookback = (AvgLookback < 10) ? 10 : AvgLookback; // minimum 10 bars
|
||
|
|
|
||
|
|
double hiBuf[], loBuf[], opBuf[], clBuf[];
|
||
|
|
ArraySetAsSeries(hiBuf, true);
|
||
|
|
ArraySetAsSeries(loBuf, true);
|
||
|
|
ArraySetAsSeries(opBuf, true);
|
||
|
|
ArraySetAsSeries(clBuf, true);
|
||
|
|
|
||
|
|
//--- Copy starting at shift 1 (skip the live bar), going back lookback bars
|
||
|
|
int copiedH = CopyHigh(_Symbol, _Period, 1, lookback, hiBuf);
|
||
|
|
int copiedL = CopyLow(_Symbol, _Period, 1, lookback, loBuf);
|
||
|
|
int copiedO = CopyOpen(_Symbol, _Period, 1, lookback, opBuf);
|
||
|
|
int copiedC = CopyClose(_Symbol, _Period, 1, lookback, clBuf);
|
||
|
|
|
||
|
|
int n = MathMin(MathMin(copiedH, copiedL), MathMin(copiedO, copiedC));
|
||
|
|
if(n <= 0)
|
||
|
|
{
|
||
|
|
Print("ForwardSimEngine [AVG]: CopyPrice failed, keeping previous averages.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
double sumBody = 0, sumUWick = 0, sumLWick = 0, sumRange = 0;
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
double bodyTop = MathMax(opBuf[i], clBuf[i]);
|
||
|
|
double bodyBot = MathMin(opBuf[i], clBuf[i]);
|
||
|
|
sumBody += bodyTop - bodyBot;
|
||
|
|
sumUWick += hiBuf[i] - bodyTop;
|
||
|
|
sumLWick += bodyBot - loBuf[i];
|
||
|
|
sumRange += hiBuf[i] - loBuf[i];
|
||
|
|
}
|
||
|
|
|
||
|
|
double pip = _Point * 10.0; // 1 pip floor
|
||
|
|
|
||
|
|
g_AvgBody = MathMax(sumBody / n, pip);
|
||
|
|
g_AvgUpperWick = MathMax(sumUWick / n, pip * 0.5);
|
||
|
|
g_AvgLowerWick = MathMax(sumLWick / n, pip * 0.5);
|
||
|
|
g_AvgRange = MathMax(sumRange / n, pip * 2.0);
|
||
|
|
|
||
|
|
Print("ForwardSimEngine [AVG]: lookback=", n,
|
||
|
|
" AvgBody=", DoubleToString(g_AvgBody / _Point, 1), " pts",
|
||
|
|
" AvgUpperWick=", DoubleToString(g_AvgUpperWick / _Point, 1), " pts",
|
||
|
|
" AvgLowerWick=", DoubleToString(g_AvgLowerWick / _Point, 1), " pts",
|
||
|
|
" AvgRange=", DoubleToString(g_AvgRange / _Point, 1), " pts");
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| 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
|
||
|
|
|
||
|
|
/* Refresh measured candle averages at the moment of the signal
|
||
|
|
so the projection uses the most recent volatility context. */
|
||
|
|
CalcAvgCandleMetrics();
|
||
|
|
|
||
|
|
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[])
|
||
|
|
{
|
||
|
|
MathSrand((int)(TimeLocal() % 32767));
|
||
|
|
|
||
|
|
int n = ArraySize(out);
|
||
|
|
int sig = g_LastSignal; // +1 bull, -1 bear
|
||
|
|
double pip = _Point * 10.0;
|
||
|
|
|
||
|
|
//--- Baseline body size from measured history
|
||
|
|
//--- Guard: if CalcAvgCandleMetrics hasn't run yet use a pip floor
|
||
|
|
double avgBody = (g_AvgBody > pip) ? g_AvgBody : pip * 3.0;
|
||
|
|
double avgUWick = (g_AvgUpperWick > 0) ? g_AvgUpperWick : avgBody * 0.5;
|
||
|
|
double avgLWick = (g_AvgLowerWick > 0) ? g_AvgLowerWick : avgBody * 0.5;
|
||
|
|
|
||
|
|
//--- EMA-slope momentum scale: 0.5× (weak) … 1.5× (strong)
|
||
|
|
//--- Normalise slope against avgBody so it is always unit-consistent
|
||
|
|
double slopeAbs = MathAbs(emaSlope);
|
||
|
|
double slopeRatio = slopeAbs / avgBody; // 0 = flat, 1 = slope equals avg body
|
||
|
|
//--- Clamp to a ±0.5 band around 1.0
|
||
|
|
double momentumScale = 0.5 + MathMin(slopeRatio, 1.0); // 0.5 … 1.5
|
||
|
|
|
||
|
|
//--- baseStep = what one average trend-direction candle body should be
|
||
|
|
double baseStep = avgBody * momentumScale;
|
||
|
|
|
||
|
|
//--- Counter-candle frequency guard
|
||
|
|
int ccFreq = (CounterCandleFreq < 3) ? 3 : CounterCandleFreq;
|
||
|
|
|
||
|
|
double prevClose = startPrice;
|
||
|
|
|
||
|
|
for(int i = 0; i < n; i++)
|
||
|
|
{
|
||
|
|
//--- Sine envelope: body pulses between 40% and 130% of baseStep
|
||
|
|
double phase = (double)i / (double)n * 2.0 * M_PI;
|
||
|
|
double sinSq = MathSin(phase) * MathSin(phase); // 0 … 1
|
||
|
|
double envelope = 0.40 + 0.90 * sinSq; // 0.40 … 1.30
|
||
|
|
|
||
|
|
//--- Exponential decay: projection flattens toward the tail
|
||
|
|
double decay = MathPow(0.93, i);
|
||
|
|
|
||
|
|
//--- Body for this candle
|
||
|
|
double bodySize = baseStep * envelope * decay;
|
||
|
|
|
||
|
|
//--- ±8% random jitter
|
||
|
|
double jitter = 1.0 + ((double)(MathRand() % 160) - 80.0) * 0.001;
|
||
|
|
bodySize *= jitter;
|
||
|
|
|
||
|
|
//--- Hard floor: at least 0.5 × avgBody so candles are always visible
|
||
|
|
if(bodySize < avgBody * 0.5)
|
||
|
|
bodySize = avgBody * 0.5;
|
||
|
|
|
||
|
|
//--- Counter-trend injection every ccFreq bars
|
||
|
|
bool isCounter = (i > 0 && (i % ccFreq == 0));
|
||
|
|
double retracePct = 0.25 + (MathRand() % 21) * 0.01; // 0.25–0.45
|
||
|
|
double step;
|
||
|
|
if(isCounter)
|
||
|
|
step = -(double)sig * bodySize * retracePct;
|
||
|
|
else
|
||
|
|
step = (double)sig * bodySize;
|
||
|
|
|
||
|
|
//--- OHLC
|
||
|
|
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);
|
||
|
|
double bHeight = bodyTop - bodyBot;
|
||
|
|
if(bHeight < _Point)
|
||
|
|
bHeight = _Point;
|
||
|
|
|
||
|
|
//--- Wicks anchored to measured avg wick ratios
|
||
|
|
//--- Base ratio = avgWick / avgBody, then add ±25% random spread
|
||
|
|
double uRatioBase = avgUWick / avgBody;
|
||
|
|
double lRatioBase = avgLWick / avgBody;
|
||
|
|
|
||
|
|
double uJitter = 1.0 + ((double)(MathRand() % 50) - 25.0) * 0.01; // ±25%
|
||
|
|
double lJitter = 1.0 + ((double)(MathRand() % 50) - 25.0) * 0.01;
|
||
|
|
|
||
|
|
double upWick = bHeight * uRatioBase * uJitter;
|
||
|
|
double dnWick = bHeight * lRatioBase * lJitter;
|
||
|
|
|
||
|
|
//--- Trend bias: the shadow in the signal direction is slightly longer
|
||
|
|
if(out[i].bullish)
|
||
|
|
upWick *= 1.20;
|
||
|
|
else
|
||
|
|
dnWick *= 1.20;
|
||
|
|
|
||
|
|
//--- Minimum wick: 20% of body height
|
||
|
|
if(upWick < bHeight * 0.20)
|
||
|
|
upWick = bHeight * 0.20;
|
||
|
|
if(dnWick < bHeight * 0.20)
|
||
|
|
dnWick = bHeight * 0.20;
|
||
|
|
|
||
|
|
out[i].high = bodyTop + upWick;
|
||
|
|
out[i].low = bodyBot - dnWick;
|
||
|
|
|
||
|
|
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);
|
||
|
|
|
||
|
|
//--- Gap in seconds on each side of the body (max 45% each side)
|
||
|
|
double gapFrac = CandleGapFraction;
|
||
|
|
if(gapFrac < 0.01)
|
||
|
|
gapFrac = 0.01;
|
||
|
|
if(gapFrac > 0.45)
|
||
|
|
gapFrac = 0.45;
|
||
|
|
int gapSec = (int)(barSec * gapFrac);
|
||
|
|
|
||
|
|
for(int i = 0; i < total; i++)
|
||
|
|
{
|
||
|
|
//--- Slot boundaries (full bar width)
|
||
|
|
datetime slotStart = startTime + (datetime)((i + 1) * barSec);
|
||
|
|
datetime slotEnd = slotStart + (datetime)barSec;
|
||
|
|
|
||
|
|
//--- Body boundaries (inset by gap on each side)
|
||
|
|
datetime bodyStart = slotStart + (datetime)gapSec;
|
||
|
|
datetime bodyEnd = slotEnd - (datetime)gapSec;
|
||
|
|
datetime bodyMid = slotStart + (datetime)(barSec / 2); // wick centre
|
||
|
|
|
||
|
|
DrawSingleCandle(i, bodyStart, bodyEnd, bodyMid, candles[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| DRAW ONE CANDLE |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void DrawSingleCandle(int idx,
|
||
|
|
datetime t1, datetime t2, datetime tMid,
|
||
|
|
const PredictedCandle &c)
|
||
|
|
{
|
||
|
|
string pfx = "FutureCandle_" + IntegerToString(idx);
|
||
|
|
color bodyCol = c.bullish ? BullishColor : BearishColor;
|
||
|
|
|
||
|
|
//--- Doji guard
|
||
|
|
double bOpen = c.open;
|
||
|
|
double bClose = c.close;
|
||
|
|
if(MathAbs(bOpen - bClose) < _Point)
|
||
|
|
bClose = bOpen + (c.bullish ? _Point * 2 : -_Point * 2);
|
||
|
|
|
||
|
|
//--- 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
|
||
|
|
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 (centred on full slot, not the inset body)
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Update Anchor Line |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
void UpdateAnchorLine(datetime t)
|
||
|
|
{
|
||
|
|
if(ObjectFind(0, AnchorLineName) < 0)
|
||
|
|
PlaceAnchorLine(t);
|
||
|
|
else
|
||
|
|
ObjectSetInteger(0, AnchorLineName, OBJPROP_TIME, t);
|
||
|
|
}
|
||
|
|
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
//| Get Anchor Time |
|
||
|
|
//+------------------------------------------------------------------+
|
||
|
|
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_");
|
||
|
|
}
|
||
|
|
//+------------------------------------------------------------------+
|