Files
Bell-PriceActionWithEma-EA/Experts/RSIForceStateEA/StateMachine.mqh
T
2026-05-09 23:16:50 +07:00

372 lines
13 KiB
Plaintext

#ifndef RSI_FORCE_STATE_EA__STATE_MACHINE_MQH
#define RSI_FORCE_STATE_EA__STATE_MACHINE_MQH
// ============================================================
// State machine: NO_TRADE -> WATCHING -> PENDING_ORDER -> IN_TRADE
// ============================================================
// Anti-spam guard: at most one trade per pullback. Reset when:
// - direction-trend flips (UP <-> DOWN)
// - pending order is cancelled (no actual trade happened)
// - the open trade fully closes (TP/SL/manual)
bool g_HasTradedThisPullback = false;
int g_BarsInWatching = 0;
// Two trend trackers:
// g_LastTrend - exact value last seen (UP / DOWN / NONE), for context
// g_LastDirTrend - last DIRECTIONAL value (UP / DOWN). Used to detect a real
// directional flip even if the trend briefly went through NONE.
TrendDirection g_LastTrend = TREND_NONE;
TrendDirection g_LastDirTrend = TREND_NONE;
// ------------------------------------------------------------
// Misc helpers
// ------------------------------------------------------------
void TransitionTo(const EAState nextState)
{
if (g_State == nextState) return;
if (InpDebugLog)
PrintFormat("[STATE] %s -> %s", EnumToString(g_State), EnumToString(nextState));
g_State = nextState;
}
bool IsNewBar()
{
static datetime lastBarTime = 0;
if (g_Bars[0].time == 0) return false;
if (g_Bars[0].time == lastBarTime) return false;
lastBarTime = g_Bars[0].time;
return true;
}
// ------------------------------------------------------------
// Trend filter (EMA200 with optional dead-zone + flat guard)
// ------------------------------------------------------------
TrendDirection DetectTrend(const int signalShift)
{
const double closePrice = g_Bars[signalShift].close;
const double ema200 = g_EMA200[signalShift];
const double atr = g_ATR[signalShift];
if (ema200 <= 0.0) return TREND_NONE;
// Dead-zone around EMA200 to avoid noise.
double bufferPrice = ema200 * (InpTrendBufferPercent / 100.0);
if (InpUseATRTrendBuffer) bufferPrice = atr * InpTrendBufferATRMult;
if (bufferPrice <= 0.0) bufferPrice = 5.0 * _Point;
// Flat EMA200 guard: skip when EMA200 barely moves between two bars.
if (InpSkipFlatEMA200)
{
const double emaDelta = MathAbs(g_EMA200[signalShift] - g_EMA200[signalShift + 1]);
const double flatThreshold = MathMax(_Point, atr * InpFlatEMA_ATRMult);
if (emaDelta <= flatThreshold) return TREND_NONE;
}
if (closePrice > ema200 + bufferPrice) return TREND_UP;
if (closePrice < ema200 - bufferPrice) return TREND_DOWN;
return TREND_NONE;
}
// All N RSI values inside [low, high] band -> sideway.
bool IsRSISideway(const int signalShift)
{
if (!InpUseRSISidewayFilter) return false;
for (int i = signalShift; i < signalShift + InpSidewayLookbackBars; i++)
{
if (g_RSI[i] < InpSidewayRSILow || g_RSI[i] > InpSidewayRSIHigh)
return false;
}
return true;
}
// ------------------------------------------------------------
// Pullback (state NO_TRADE -> WATCHING) and trigger (WATCHING -> PENDING)
// ------------------------------------------------------------
bool IsPullbackInUptrend(const int signalShift)
{
// RSI < EMA9 < WMA45 AND all three buffers slope down (current pullback).
return (g_RSI[signalShift] < g_EMA9[signalShift]
&& g_EMA9[signalShift] < g_WMA45[signalShift]
&& IsBufferSlopingDown(g_RSI, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingDown(g_EMA9, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingDown(g_WMA45, signalShift, InpSlopeLookbackBars));
}
bool IsPullbackInDowntrend(const int signalShift)
{
// RSI > EMA9 > WMA45 AND all three buffers slope up.
return (g_RSI[signalShift] > g_EMA9[signalShift]
&& g_EMA9[signalShift] > g_WMA45[signalShift]
&& IsBufferSlopingUp(g_RSI, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingUp(g_EMA9, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingUp(g_WMA45, signalShift, InpSlopeLookbackBars));
}
// BUY trigger: RSI just crossed up WMA45 + EMA9 still below WMA45
// + RSI didn't cross WMA45 in the previous N bars.
bool IsBuyTriggerSignal(const int signalShift)
{
const bool rsiCrossedUp = (g_RSI[signalShift + 1] <= g_WMA45[signalShift + 1]
&& g_RSI[signalShift] > g_WMA45[signalShift]);
const bool ema9StillBelow = (g_EMA9[signalShift] < g_WMA45[signalShift]);
const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45,
signalShift + 2,
InpMinBarsBetweenCrosses);
return rsiCrossedUp && ema9StillBelow && wasIsolated;
}
// SELL trigger: mirror of BUY.
bool IsSellTriggerSignal(const int signalShift)
{
const bool rsiCrossedDown = (g_RSI[signalShift + 1] >= g_WMA45[signalShift + 1]
&& g_RSI[signalShift] < g_WMA45[signalShift]);
const bool ema9StillAbove = (g_EMA9[signalShift] > g_WMA45[signalShift]);
const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45,
signalShift + 2,
InpMinBarsBetweenCrosses);
return rsiCrossedDown && ema9StillAbove && wasIsolated;
}
// ------------------------------------------------------------
// Build a complete entry plan (entry / SL / TP / direction / risk)
// ------------------------------------------------------------
bool BuildSignalPlan(const int direction, const int signalShift, SignalSnapshot &outPlan)
{
// Entry = midpoint between signal close and the nearest swing extreme:
// BUY -> midpoint between close and nearest swing low.
// SELL -> midpoint between close and nearest swing high.
const double swingAnchor = FindNearestSwingForEntry(direction, signalShift);
if (swingAnchor <= 0.0) return false;
const double closePrice = g_Bars[signalShift].close;
const double entryPrice = (closePrice + swingAnchor) * 0.5;
// Sanity: BUY limit must sit below close, SELL limit above close.
if (direction > 0 && entryPrice >= closePrice) return false;
if (direction < 0 && entryPrice <= closePrice) return false;
const double slPrice = ComputeStopLossPrice(direction, signalShift,
entryPrice, g_ATR[signalShift]);
if (direction > 0 && slPrice >= entryPrice) return false;
if (direction < 0 && slPrice <= entryPrice) return false;
const double riskInPrice = MathAbs(entryPrice - slPrice);
if (riskInPrice <= (2.0 * _Point)) return false;
outPlan.signalBarTime = g_Bars[signalShift].time;
outPlan.direction = direction;
outPlan.entryPrice = NormalizePriceToTick(entryPrice);
outPlan.stopLossPrice = NormalizePriceToTick(slPrice);
outPlan.initialRiskPrice = riskInPrice;
outPlan.takeProfitPrice = NormalizePriceToTick((direction > 0)
? (entryPrice + InpRiskRewardRatio * riskInPrice)
: (entryPrice - InpRiskRewardRatio * riskInPrice));
return true;
}
// ------------------------------------------------------------
// Broker-state synchronization
// ------------------------------------------------------------
void ResetPullbackCycle()
{
g_HasTradedThisPullback = false;
g_BarsInWatching = 0;
}
// Reconcile internal state with the broker:
// PENDING_ORDER -> IN_TRADE if a position appears
// PENDING_ORDER -> NO_TRADE if order vanished without fill (cancel/reject/expire)
// IN_TRADE -> NO_TRADE if position no longer exists (TP/SL/manual)
// On both NO_TRADE transitions, also clear g_HasTradedThisPullback so the EA
// can take the next pullback opportunity in the same trend.
void SyncStateWithBroker()
{
ulong posTicket = 0;
const bool hasPos = HasOurOpenPosition(posTicket);
const bool hasPending = HasOurPendingOrder(g_Pending.orderTicket);
if (g_State == STATE_PENDING_ORDER)
{
if (hasPos)
{
g_OpenTrade.isActive = true;
g_OpenTrade.positionTicket = posTicket;
g_OpenTrade.plan = g_Pending.plan;
g_OpenTrade.partialClosedDone = false;
g_Pending.orderTicket = 0;
g_Pending.barsSincePlaced = 0;
TransitionTo(STATE_IN_TRADE);
return;
}
if (!hasPending)
{
// Pending was cancelled / expired / rejected externally.
g_Pending.orderTicket = 0;
g_Pending.barsSincePlaced = 0;
g_HasTradedThisPullback = false; // allow the next setup attempt
TransitionTo(STATE_NO_TRADE);
}
}
else if (g_State == STATE_IN_TRADE)
{
if (!hasPos)
{
g_OpenTrade.isActive = false;
g_OpenTrade.positionTicket = 0;
g_HasTradedThisPullback = false; // trade done -> allow next setup
TransitionTo(STATE_NO_TRADE);
}
}
}
// ------------------------------------------------------------
// Pending order lifecycle on every newly closed bar
// ------------------------------------------------------------
void TickPendingOrderLifecycle(const TrendDirection trendNow)
{
if (g_State != STATE_PENDING_ORDER || g_Pending.orderTicket == 0) return;
g_Pending.barsSincePlaced++;
const bool expired = (g_Pending.barsSincePlaced >= InpPendingMaxAliveBars);
bool invalidated = false;
if (InpInvalidateIfCrossBack)
{
if (g_Pending.plan.direction > 0)
invalidated = (trendNow != TREND_UP || g_RSI[1] < g_WMA45[1]);
else
invalidated = (trendNow != TREND_DOWN || g_RSI[1] > g_WMA45[1]);
}
if (!expired && !invalidated) return;
if (CancelPendingOrder(g_Pending))
{
if (InpDebugLog)
PrintFormat("[PENDING] cancelled (%s)", expired ? "expired" : "invalidated");
g_HasTradedThisPullback = false; // allow next pullback to retry
TransitionTo(STATE_NO_TRADE);
}
}
// ------------------------------------------------------------
// Per-state handlers
// ------------------------------------------------------------
void HandleStateNoTrade(const int signalShift, const TrendDirection trendNow)
{
if (g_HasTradedThisPullback) return; // anti-spam: wait until reset
if (IsRSISideway(signalShift)) return; // sideway filter only blocks new setups
if (trendNow == TREND_NONE) return; // need a real direction
if (trendNow == TREND_UP && IsPullbackInUptrend(signalShift))
{
g_BarsInWatching = 0;
TransitionTo(STATE_WATCHING);
}
else if (trendNow == TREND_DOWN && IsPullbackInDowntrend(signalShift))
{
g_BarsInWatching = 0;
TransitionTo(STATE_WATCHING);
}
}
void HandleStateWatching(const int signalShift, const TrendDirection trendNow)
{
g_BarsInWatching++;
// Trend lost while watching -> abandon setup.
if (trendNow == TREND_NONE)
{
TransitionTo(STATE_NO_TRADE);
return;
}
// Watching too long without a trigger -> abandon to avoid stale setups.
if (g_BarsInWatching > InpWatchingMaxBars)
{
if (InpDebugLog) Print("[WATCHING] timed out, back to NO_TRADE");
TransitionTo(STATE_NO_TRADE);
return;
}
// Try to trigger an entry on this bar.
SignalSnapshot plan;
ZeroMemory(plan);
bool hasSignal = false;
if (trendNow == TREND_UP && IsBuyTriggerSignal(signalShift))
hasSignal = BuildSignalPlan(+1, signalShift, plan);
else if (trendNow == TREND_DOWN && IsSellTriggerSignal(signalShift))
hasSignal = BuildSignalPlan(-1, signalShift, plan);
if (!hasSignal) return;
if (PlaceLimitOrderFromPlan(plan, g_Pending))
{
g_HasTradedThisPullback = true;
TransitionTo(STATE_PENDING_ORDER);
}
}
// ------------------------------------------------------------
// Top-level entry point: called once per closed bar
// ------------------------------------------------------------
void RunStateMachine()
{
const int signalShift = InpSignalBarShift;
const TrendDirection trendNow = DetectTrend(signalShift);
// Detect a TRUE directional flip (UP <-> DOWN).
// A short trip through TREND_NONE between two same-direction trends
// is NOT a flip and must NOT reset the cycle / abandon WATCHING.
bool dirFlipped = false;
if (trendNow != TREND_NONE)
{
if (g_LastDirTrend != TREND_NONE && g_LastDirTrend != trendNow)
dirFlipped = true;
g_LastDirTrend = trendNow;
}
if (dirFlipped)
{
if (InpDebugLog)
PrintFormat("[TREND] direction flipped %s -> %s",
EnumToString(g_LastTrend), EnumToString(trendNow));
ResetPullbackCycle();
if (g_State == STATE_PENDING_ORDER && g_Pending.orderTicket > 0)
{
if (CancelPendingOrder(g_Pending))
{
if (InpDebugLog) Print("[PENDING] cancelled by trend flip");
TransitionTo(STATE_NO_TRADE);
}
}
else if (g_State == STATE_WATCHING)
{
TransitionTo(STATE_NO_TRADE);
}
}
g_LastTrend = trendNow;
switch (g_State)
{
case STATE_NO_TRADE: HandleStateNoTrade(signalShift, trendNow); break;
case STATE_WATCHING: HandleStateWatching(signalShift, trendNow); break;
case STATE_PENDING_ORDER: TickPendingOrderLifecycle(trendNow); break;
case STATE_IN_TRADE: /* tick-level handler does the work */ break;
}
}
#endif