372 lines
13 KiB
Plaintext
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
|