Add regime/probability filters and fix stale-signal, restart, and cooldown gaps

OHLC EA improvements:
- Add ADX regime filter (InpADXMax, default 25): block mean-reversion
  entries in trending markets, restoring the documented ADX gate
- Require live Z-Score (>= 90% of threshold) alongside the closed-bar
  signal so entries no longer chase price after an intra-bar snap-back,
  and no instant re-entry fires after a Gravity Exit win
- Add loss cooldown (InpCooldownMins, default 10): pause entries after
  a losing trade instead of re-entering the same extended move
- Remove the soft 0.75x Z threshold that entered at weak extremes
  (~1.5 sigma) in stable markets; full threshold always applies
- Recover open position on restart (adopt ticket + rebuild trade record)
  and seed Z-Score at init so a recovered position is managed instead of
  being orphaned or instantly closed on a bogus Z=0
- Restore the absolute spread cap (InpMaxSpreadPts was declared but unused)
- Give the velocity filter a 10-second time window so slow drift across
  10 ticks in quiet markets no longer false-triggers the spike block
- Capture position ticket via DEAL_POSITION_ID with ordered fallbacks
- Extend news load window past midnight for early-next-day events
- Guard daily-loss check against zero starting balance

EveryTick EA: remove dead no-op confirmation block

https://claude.ai/code/session_01GPhWxikvDKmDNwQGhs6gFp
This commit is contained in:
Claude
2026-06-10 15:20:41 +00:00
parent d48c2e48d0
commit 7559945962
2 changed files with 147 additions and 46 deletions
+2 -9
View File
@@ -446,21 +446,14 @@ if(baseFilters && IsNewBar())
double maPrev[1], sdPrev[1];
if(CopyBuffer(handleMA,0,1,1,maPrev)<1 || CopyBuffer(handleSD,0,1,1,sdPrev)<1) return;
if(sdPrev[0] <= 0) return;
double closedZ = (bid - maPrev[0]) / sdPrev[0];
if(closedZ < -InpEntryZ && IsAlignedWithH1Trend(true))
ExecuteTrade(ORDER_TYPE_BUY, ask, atr[0], closedZ);
else if(closedZ > InpEntryZ && IsAlignedWithH1Trend(false))
ExecuteTrade(ORDER_TYPE_SELL, bid, atr[0], closedZ);
}
static double lastZ = 0;
if(MathAbs(zScore) > InpEntryZ && MathAbs(lastZ) > InpEntryZ * 0.8) // required two ticks confirmation
{
// then do the entry checks
}
lastZ = zScore;
}
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
+145 -37
View File
@@ -23,12 +23,15 @@ input int InpStartHour = 10; // Trade window start hour
input int InpEndHour = 20; // Trade window end hour (exclusive)
input int InpFridayCloseHour = 20; // Friday hour to close and stop
input int InpMaxHoldMinutes = 30; // Max trade duration in minutes
input int InpCooldownMins = 10; // Pause after a losing trade, minutes (0 = disabled)
input int InpMagic = 777333; // Magic number
//--- Inputs: Indicators
input int InpMAPeriod = 20; // MA / StdDev period
input int InpATRPeriod = 14; // ATR period
input int InpFilterPeriodH1 = 50; // H1 SMA period for trend filter (0 = disabled)
input int InpADXPeriod = 14; // ADX period
input double InpADXMax = 25.0; // Skip entries when ADX above this (0 = disabled)
//--- Inputs: Execution
input int InpSlippage = 30; // Max slippage in points
@@ -65,11 +68,14 @@ struct TradeRecord {
};
//--- Global Handles & State
int handleMA, handleSD, handleATR, handleMA_H1;
int handleMA, handleSD, handleATR, handleMA_H1, handleADX;
ulong glTicket = 0; // active position ticket (0 = no open trade)
string glPendingCloseReason = ""; // reason set by CloseAllOwnPositions, read on next tick
TradeRecord glTradeRecord; // snapshot of the open trade
int glTradeCount = 0; // cumulative trade counter (persists across sessions via file)
double glZScore = 0; // Z-Score of last closed bar
bool glZValid = false; // false until first successful Z computation (guards exits after restart)
datetime glLossCooldownUntil = 0; // no new entries until this time after a losing trade
double spreadBuffer[20];
int spreadIdx = 0;
bool spreadBufferFull = false;
@@ -97,7 +103,8 @@ int glEntryConfirmDir = 0; // 1 = buy signal, -1 = sell signal, 0 = no
//--- Price Velocity filter: block entry when price moves > 25% of M1 ATR within 10 ticks
#define VELOCITY_TICKS 10
double glVelBuf[VELOCITY_TICKS]; // circular buffer of last 10 bid prices
double glVelBuf[VELOCITY_TICKS]; // circular buffer of last 10 bid prices
datetime glVelTime[VELOCITY_TICKS]; // arrival time of each buffered tick
int glVelIdx = 0; // write index
bool glVelBufFull = false; // true once buffer has been filled once
datetime glVelBlockUntil = 0; // entry blocked until this server time
@@ -129,6 +136,11 @@ int OnInit() {
else
handleMA_H1 = INVALID_HANDLE;
if(InpADXMax > 0)
handleADX = iADX(TradeSymbol, _Period, InpADXPeriod);
else
handleADX = INVALID_HANDLE;
if(handleMA == INVALID_HANDLE || handleSD == INVALID_HANDLE ||
handleATR == INVALID_HANDLE) {
Print("Failed to create indicator handles");
@@ -138,6 +150,10 @@ int OnInit() {
Print("Failed to create H1 MA handle");
return(INIT_FAILED);
}
if(InpADXMax > 0 && handleADX == INVALID_HANDLE) {
Print("Failed to create ADX handle");
return(INIT_FAILED);
}
if(InpUseNewsFilter) LoadNewsEvents();
InitTradeLog();
@@ -159,19 +175,69 @@ int OnInit() {
// Reset price velocity filter on (re)init
ArrayInitialize(glVelBuf, 0.0);
for(int v = 0; v < VELOCITY_TICKS; v++) glVelTime[v] = 0;
glVelIdx = 0;
glVelBufFull = false;
glVelBlockUntil = 0;
// Seed Z-Score from the last closed bar so exits don't act on a bogus Z=0 after restart
glZValid = false;
UpdateZScore();
// Recover an open position after restart so it keeps being managed
// (without this, an orphaned position gets no trailing/exits and a
// fresh zScore of 0 would have instantly closed it as a false Gravity Exit)
glTicket = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--) {
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != TradeSymbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != (long)InpMagic) continue;
glTicket = ticket;
glTradeRecord.ticket = ticket;
glTradeRecord.openTime = (datetime)PositionGetInteger(POSITION_TIME);
glTradeRecord.openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
glTradeRecord.lots = PositionGetDouble(POSITION_VOLUME);
glTradeRecord.type = (int)PositionGetInteger(POSITION_TYPE);
glTradeRecord.openBalance = AccountInfoDouble(ACCOUNT_BALANCE);
glTradeRecord.openEquity = AccountInfoDouble(ACCOUNT_EQUITY);
glTradeRecord.zScore = 0;
glTradeRecord.atr = 0;
glTradeRecord.spreadAtEntry = 0;
glTradeRecord.riskPct = 0;
glTradeRecord.sl = PositionGetDouble(POSITION_SL);
glTradeRecord.tp = PositionGetDouble(POSITION_TP);
glTradeRecord.h1Status = "RECOVERED";
glTradeRecord.comment = PositionGetString(POSITION_COMMENT);
Print("Recovered open position after restart: ticket=", ticket);
break;
}
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
// UpdateZScore — Z-Score of the last closed bar (shift 1)
//+------------------------------------------------------------------+
void UpdateZScore() {
double ma1[1], sd1[1];
if(CopyBuffer(handleMA,0,1,1,ma1)>=1 && CopyBuffer(handleSD,0,1,1,sd1)>=1) {
double close1 = iClose(TradeSymbol, _Period, 1);
if(sd1[0] > 0.0) {
glZScore = (close1 - ma1[0]) / sd1[0];
glZValid = true;
}
}
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {
if(handleMA != INVALID_HANDLE) IndicatorRelease(handleMA);
if(handleSD != INVALID_HANDLE) IndicatorRelease(handleSD);
if(handleATR != INVALID_HANDLE) IndicatorRelease(handleATR);
if(handleMA_H1 != INVALID_HANDLE) IndicatorRelease(handleMA_H1);
if(handleADX != INVALID_HANDLE) IndicatorRelease(handleADX);
}
//+------------------------------------------------------------------+
@@ -190,6 +256,7 @@ void CheckDailyReset() {
bool IsDailyLossLimitHit() {
if(!InpUseDailyLossLimit) return false;
if(dailyLossHit) return true;
if(dailyStartBalance <= 0) return false;
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
double lossPercent = ((dailyStartBalance - equity) / dailyStartBalance) * 100.0;
@@ -212,7 +279,8 @@ void LoadNewsEvents() {
TimeToStruct(TimeCurrent(), dt);
datetime dayStart = TimeCurrent() - (dt.hour * 3600 + dt.min * 60 + dt.sec);
datetime dayEnd = dayStart + 86400;
// Extend past midnight so early-next-day events still trigger the pre-news pause
datetime dayEnd = dayStart + 86400 + InpNewsMinsBefore * 60;
MqlCalendarValue values[];
if(!CalendarValueHistory(values, dayStart, dayEnd)) return;
@@ -437,15 +505,19 @@ void OnTick() {
double spreadMA = (count > 0) ? (spreadSum / count) : currentSpread;
// --- PRICE VELOCITY FILTER: update 10-tick bid buffer ---
glVelBuf[glVelIdx] = bid;
glVelBuf[glVelIdx] = bid;
glVelTime[glVelIdx] = TimeCurrent();
glVelIdx++;
if(glVelIdx >= VELOCITY_TICKS) { glVelIdx = 0; glVelBufFull = true; }
if(glVelBufFull) {
// oldest value is at current write position (about to be overwritten next tick)
double oldest = glVelBuf[glVelIdx % VELOCITY_TICKS];
double newest = glVelBuf[(glVelIdx + VELOCITY_TICKS - 1) % VELOCITY_TICKS];
double velMove = MathAbs(newest - oldest);
if(velMove > 0.25 * atr[0])
int oldIdx = glVelIdx % VELOCITY_TICKS;
int newIdx = (glVelIdx + VELOCITY_TICKS - 1) % VELOCITY_TICKS;
double velMove = MathAbs(glVelBuf[newIdx] - glVelBuf[oldIdx]);
long velSpan = (long)(glVelTime[newIdx] - glVelTime[oldIdx]);
// Only a genuine spike counts: 10 ticks can span minutes in quiet markets,
// and slow drift of 25% ATR over that span is normal, not a knife
if(velSpan <= 10 && velMove > 0.25 * atr[0])
glVelBlockUntil = TimeCurrent() + 3; // block entries for 3 seconds
}
@@ -456,15 +528,14 @@ void OnTick() {
// --- Z-Score Calculation (Shift 1 for OHLC alignment) ---
// We only update Z-Score once per bar to match OHLC backtest signals
static double zScore = 0;
if(isNewBar) {
double ma1[1], sd1[1];
if(CopyBuffer(handleMA,0,1,1,ma1)>=1 && CopyBuffer(handleSD,0,1,1,sd1)>=1) {
double close1 = iClose(TradeSymbol, _Period, 1);
zScore = (sd1[0] > 0.0) ? (close1 - ma1[0]) / sd1[0] : 0.0;
}
}
if(isNewBar) UpdateZScore();
double zScore = glZScore;
// --- Live Z-Score (current bid vs current MA/SD) ---
// Guards against stale signals: the closed-bar Z stays extreme for the whole
// next bar even after price has already snapped back intra-bar
double liveZ = (sd[0] > 0.0) ? (bid - ma[0]) / sd[0] : 0.0;
// --- Volatility Ratio & Dynamic Z-Score Threshold ---
double atrBuf100[100];
double avgATR100 = 0;
@@ -476,22 +547,36 @@ void OnTick() {
avgATR100 = atr[0]; // fallback: ratio = 1.0 (no adjustment)
}
double volRatio = (avgATR100 > 0) ? (atr[0] / avgATR100) : 1.0;
double dynamicZ = InpEntryZ + (volRatio > 1.5 ? 0.5 : 0.0);
double softZ = InpEntryZ * 0.75;
// Stable market (volRatio 0.7–1.1): lower soft threshold captures more standard reversions
// Elevated volatility (volRatio > 1.2): demand full dynamicZ to avoid noise spikes
double entryZ = (volRatio >= 0.7 && volRatio <= 1.1) ? softZ : dynamicZ;
// Full threshold always; raised +0.5 in elevated volatility to avoid noise spikes.
// (The old "soft" 0.75x threshold entered at Z~1.5 — too weak an extreme for
// mean reversion, producing many low-edge trades that mostly paid spread.)
double entryZ = InpEntryZ + (volRatio > 1.5 ? 0.5 : 0.0);
bool nearNews = IsNearNews();
bool lossLimitHit = IsDailyLossLimitHit();
bool redNewsImminent = IsRedNewsImminent();
bool weekendRisk = IsWeekendRisk();
// --- Spread Check (ATR-relative only) ---
// --- Spread Check ---
double atrPts = atr[0] / point;
double maxAllowedSpread = atrPts * 0.12; // block entry when spread > 12% of current M1 ATR
bool spreadOk = (currentSpread <= maxAllowedSpread) && // ATR-relative gate
(currentSpread <= spreadMA * 1.5); // liquidity-gap guard: no entry if spread > 1.5x 20-tick average
(currentSpread <= spreadMA * 1.5) && // liquidity-gap guard: no entry if spread > 1.5x 20-tick average
(currentSpread <= InpMaxSpreadPts); // absolute cap
// --- ADX Regime Filter: mean reversion only works in ranging markets ---
bool adxOk = true;
double adxVal = 0;
if(InpADXMax > 0 && handleADX != INVALID_HANDLE) {
double adxBuf[1];
if(CopyBuffer(handleADX, 0, 0, 1, adxBuf) >= 1) {
adxVal = adxBuf[0];
adxOk = (adxVal <= InpADXMax);
}
}
// --- Loss Cooldown: no re-entry right after a losing trade ---
bool cooldownOk = (TimeCurrent() >= glLossCooldownUntil);
// --- Pre-compute filter states ---
bool inWindow = (dt.hour >= InpStartHour && dt.hour < InpEndHour);
@@ -545,10 +630,14 @@ void OnTick() {
CloseAllOwnPositions("Time Exit (" + tradeDurStr + ")");
} else {
// Z-Score TP: Gravity Exit — exit threshold decays toward 0 as trade ages
// glZValid guard: never act on the uninitialized Z=0 right after a restart,
// which would instantly close a recovered position as a false reversion
long posType = PositionGetInteger(POSITION_TYPE);
bool zRevert = false;
if(posType == POSITION_TYPE_BUY && zScore >= -gravityExitZ) zRevert = true;
if(posType == POSITION_TYPE_SELL && zScore <= gravityExitZ) zRevert = true;
if(glZValid) {
if(posType == POSITION_TYPE_BUY && zScore >= -gravityExitZ) zRevert = true;
if(posType == POSITION_TYPE_SELL && zScore <= gravityExitZ) zRevert = true;
}
if(zRevert) {
CloseAllOwnPositions("Gravity Exit (Z=" + DoubleToString(zScore, 2) + " tgt=" + DoubleToString(gravityExitZ, 2) + ")");
@@ -564,13 +653,15 @@ void OnTick() {
}
} else { // --- ENTRY LOGIC (every tick, gated by flash-spike filter) ---
if(CountOwnPositions() < InpMaxPositions) {
bool baseFilters = (inWindow && spreadOk && !nearNews);
bool baseFilters = (inWindow && spreadOk && !nearNews && adxOk && cooldownOk && glZValid);
// Determine current signal direction (0 = no signal)
// Determine current signal direction (0 = no signal).
// liveZ must still be stretched (90% of threshold): the closed-bar signal
// alone would chase entries after the intra-bar snap-back already happened
int signalDir = 0;
if(baseFilters) {
if(zScore < -entryZ && IsAlignedWithH1Trend(true)) signalDir = 1;
else if(zScore > entryZ && IsAlignedWithH1Trend(false)) signalDir = -1;
if(zScore < -entryZ && liveZ <= -entryZ * 0.9 && IsAlignedWithH1Trend(true)) signalDir = 1;
else if(zScore > entryZ && liveZ >= entryZ * 0.9 && IsAlignedWithH1Trend(false)) signalDir = -1;
}
// --- Confirmation gate: accumulate or reset ---
@@ -606,10 +697,12 @@ void OnTick() {
Comment("--- N30 GOLD REVERSION (SIMPLE) ---\n",
"Z-Score(1): ", DoubleToString(zScore, 2),
" | Z-Target: ", DoubleToString(entryZ, 2),
" (", (volRatio >= 0.7 && volRatio <= 1.1 ? "SOFT" : "FULL"), ")\n",
" | Live Z: ", DoubleToString(liveZ, 2),
" | Z-Target: ", DoubleToString(entryZ, 2), "\n",
"Volatility Ratio: ", DoubleToString(volRatio, 2),
(volRatio > 1.5 ? " [HIGH - Z raised +0.5]" : (volRatio >= 0.7 && volRatio <= 1.1 ? " [stable - soft threshold]" : " [normal]")), "\n",
(volRatio > 1.5 ? " [HIGH - Z raised +0.5]" : " [normal]"), "\n",
"ADX: ", DoubleToString(adxVal, 1), " / Max: ", DoubleToString(InpADXMax, 1),
" ", (adxOk ? "OK" : "BLOCKED [trending]"), "\n",
"H1 Trend: ", h1Status, "\n",
"Spread: ", DoubleToString(currentSpread, 1),
" / MaxAllowed: ", DoubleToString(maxAllowedSpread, 1),
@@ -617,6 +710,7 @@ void OnTick() {
"News: ", (nearNews ? "BLOCKED" : "clear"),
(redNewsImminent ? " [CLOSING NOW]" : ""), "\n",
"Velocity: ", (TimeCurrent() < glVelBlockUntil ? "BLOCKED (" + IntegerToString((int)(glVelBlockUntil - TimeCurrent())) + "s)" : "ok"), "\n",
"Loss Cooldown: ", (cooldownOk ? "none" : IntegerToString((int)((glLossCooldownUntil - TimeCurrent()) / 60)) + "m remaining"), "\n",
"Trade Duration: ", tradeDurStr);
}
@@ -775,14 +869,21 @@ void ExecuteTrade(ENUM_ORDER_TYPE type, double p, double a, double zScore) {
if(!OrderSend(req, res) || res.retcode != TRADE_RETCODE_DONE) {
Print("Entry failed: retcode=", res.retcode, " comment=", res.comment);
} else {
// Capture position ticket: res.deal is the position ticket on netting accounts;
// fall back to res.order for hedging accounts.
glTicket = res.deal;
bool posSelected = PositionSelectByTicket(glTicket);
// Capture position ticket: the deal's DEAL_POSITION_ID is the position
// ticket on both netting and hedging accounts; fall back to res.order
// then res.deal if the deal isn't in history yet.
glTicket = 0;
if(res.deal > 0 && HistoryDealSelect(res.deal))
glTicket = (ulong)HistoryDealGetInteger(res.deal, DEAL_POSITION_ID);
bool posSelected = (glTicket != 0) && PositionSelectByTicket(glTicket);
if(!posSelected) {
glTicket = res.order;
posSelected = PositionSelectByTicket(glTicket);
}
if(!posSelected) {
glTicket = res.deal;
posSelected = PositionSelectByTicket(glTicket);
}
// Snapshot open-trade details for the trade log
glTradeRecord.ticket = glTicket;
glTradeRecord.openTime = posSelected ? (datetime)PositionGetInteger(POSITION_TIME) : TimeCurrent();
@@ -897,6 +998,13 @@ void WriteTradeResult(datetime closeTime, double closePrice,
glTradeCount++;
double netPnL = grossProfit + swap + commission;
double closingBalance = AccountInfoDouble(ACCOUNT_BALANCE);
// Loss cooldown: the frozen closed-bar Z is often still extreme right after a
// stop-out/time-exit, so an immediate re-entry walks straight back into the move
if(netPnL < 0 && InpCooldownMins > 0) {
glLossCooldownUntil = TimeCurrent() + (long)InpCooldownMins * 60;
Print("Losing trade — entry cooldown until ", TimeToString(glLossCooldownUntil, TIME_DATE|TIME_MINUTES));
}
long durationSec = (long)closeTime - (long)glTradeRecord.openTime;
int durationMins = (int)(durationSec / 60);