mql5/Experts/mt5-fx-gold-ea/CascadeGreepEA.mq5
2025-08-14 19:25:09 -04:00

468 lines
16 KiB
MQL5

//+------------------------------------------------------------------+
//| CascadeGreepEA |
//| Copyright 2025 Cascade (AI) |
//| Purpose: ATR-risk FX/Gold mean-rev |
//+------------------------------------------------------------------+
#property copyright "Cascade AI"
#property link "https://windsurf.ai"
#property version "1.0"
#property strict
#include <Trade/Trade.mqh>
//==================== Inputs ====================
input group "Risk Management"
input double InpRiskPercent = 1.0; // per-trade risk % of equity (1.0 - 2.0)
input double InpKillSwitchDDPercent = 2.0; // stop trading if equity DD >= this % from start
input int InpMaxOpenPositions = 5; // cap across entire account
input bool InpAllowTradeIfBelowMinLot = true; // if true, will use min lot even if > risk (guarded by caps)
input bool InpCloseOnKillSwitch = true; // close EA positions when DD hit
input group "Signals"
input ENUM_TIMEFRAMES InpSignalTF = PERIOD_M15;
input int InpSMAPeriod = 200; // trend filter
input bool InpUseTrendFilter = true;
input int InpRSIPeriod = 2;
input int InpRSILow = 5; // buy when RSI <= low (with trend filter)
input int InpRSIHigh = 95; // sell when RSI >= high (with trend filter)
input bool InpAllowLong = true;
input bool InpAllowShort = true;
input group "Stops/Targets"
input int InpATRPeriod = 14;
input double InpATRMultiplierSL = 2.0; // SL = ATR * mult
input bool InpUseTrailing = true;
input double InpTrailATRMultiplier = 1.0; // trail distance in ATR
input double InpTP_ATR_Mult = 0.0; // 0 disables fixed TP; else TP = ATR * mult
input group "Execution Safety"
input int InpMaxSpreadPoints = 30; // max allowed spread (points); 0 disables
input double InpMaxATRPoints = 0.0; // max allowed ATR (points); 0 disables
input double InpDailyLossLimitPercent = 2.0; // block trading for the day if daily DD >= this %
input bool InpCloseOnDailyLoss = true; // close EA positions when daily loss hit
input int InpMinBarsBetweenTrades = 3; // cooldown per symbol/timeframe
input double InpMaxRiskPctWhenForcingMinLot = 5.0; // cap risk % when min lot is used
input double InpMaxRiskPctWhenForcingMinLot_XAU = 3.0; // stricter cap for XAU/GOLD
input group "Misc"
input long InpMagic = 420250810; // unique EA id
input int InpMaxSlippagePoints = 10; // allowed slippage in points for market orders
input group "Master Adjustments"
input bool InpUseMasterAdjust = true; // read dynamic factors from Master EA
input string InpGVNamespaceEA = "CGreep"; // must match Master EA namespace
//==================== Globals ====================
CTrade trade;
double g_start_equity = 0.0;
string g_symbol;
bool g_trading_disabled = false;
int hATR = INVALID_HANDLE;
int hRSI = INVALID_HANDLE;
int hSMA = INVALID_HANDLE;
datetime g_last_bar_time = 0;
datetime g_last_trade_bar_time = 0;
bool g_daily_blocked = false;
int g_day_key = 0;
double g_day_equity_start = 0.0;
//==================== Utility ====================
bool CopyOne(const int handle, const int buffer, const int shift, double &out)
{
double tmp[];
if(CopyBuffer(handle, buffer, shift, 1, tmp) != 1)
return false;
out = tmp[0];
return true;
}
bool GetATR(const int shift, double &atr)
{
if(hATR==INVALID_HANDLE) return false;
return CopyOne(hATR, 0, shift, atr);
}
bool GetRSI(const int shift, double &rsi)
{
if(hRSI==INVALID_HANDLE) return false;
return CopyOne(hRSI, 0, shift, rsi);
}
bool GetSMA(const int shift, double &sma)
{
if(hSMA==INVALID_HANDLE) return false;
return CopyOne(hSMA, 0, shift, sma);
}
bool SymbolIsXAU(const string sym)
{
return (StringFind(sym, "XAU", 0) >= 0 || StringFind(sym, "GOLD", 0) >= 0);
}
int TodayKey()
{
MqlDateTime dt; TimeToStruct(TimeCurrent(), dt);
return (dt.year*10000 + dt.mon*100 + dt.day);
}
void DailyResetIfNeeded()
{
int key = TodayKey();
if(key != g_day_key)
{
g_day_key = key;
g_day_equity_start = AccountInfoDouble(ACCOUNT_EQUITY);
g_daily_blocked = false;
Print("[RESET] New day baseline equity=", g_day_equity_start);
}
}
bool CheckDailyLossLimit()
{
if(g_daily_blocked) return true;
if(g_day_equity_start<=0) return false;
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
double ddpct = (g_day_equity_start - eq) / g_day_equity_start * 100.0;
if(ddpct >= InpDailyLossLimitPercent)
{
g_daily_blocked = true;
Print("[DAILY] Loss limit ", DoubleToString(ddpct,2), "% >= ", InpDailyLossLimitPercent, "%. Trading disabled for today.");
if(InpCloseOnDailyLoss) CloseEAPositions();
return true;
}
return false;
}
bool CheckSpreadAtrOK(const double atr)
{
MqlTick t; if(!SymbolInfoTick(g_symbol, t)) return false;
double spread_pts = (t.ask - t.bid) / _Point;
if(InpMaxSpreadPoints>0 && spread_pts > InpMaxSpreadPoints)
{
PrintFormat("[SKIP] Spread %.1f pts > max %d", spread_pts, InpMaxSpreadPoints);
return false;
}
if(InpMaxATRPoints>0.0)
{
double atr_pts = atr / _Point;
if(atr_pts > InpMaxATRPoints)
{
PrintFormat("[SKIP] ATR %.1f pts > max %.1f pts", atr_pts, InpMaxATRPoints);
return false;
}
}
return true;
}
int CountAllOpenPositions()
{
return (int)PositionsTotal();
}
bool HasAnyPositionForSymbol(const string sym)
{
for(int i=PositionsTotal()-1; i>=0; --i)
{
ulong ticket = PositionGetTicket(i);
if(ticket==0) continue;
string psym = PositionGetString(POSITION_SYMBOL);
if(psym==sym) return true;
}
return false;
}
int CountEAPositions()
{
int count=0;
for(int i=PositionsTotal()-1; i>=0; --i)
{
ulong ticket = PositionGetTicket(i);
if(ticket==0) continue;
long magic = (long)PositionGetInteger(POSITION_MAGIC);
if(magic==InpMagic) count++;
}
return count;
}
void CloseEAPositions()
{
for(int i=PositionsTotal()-1; i>=0; --i)
{
ulong ticket = PositionGetTicket(i);
if(ticket==0) continue;
string sym = PositionGetString(POSITION_SYMBOL);
long mgc = (long)PositionGetInteger(POSITION_MAGIC);
long type= (long)PositionGetInteger(POSITION_TYPE);
double vol = PositionGetDouble(POSITION_VOLUME);
if(mgc!=InpMagic) continue;
if(type==POSITION_TYPE_BUY)
trade.PositionClose(sym);
else if(type==POSITION_TYPE_SELL)
trade.PositionClose(sym);
}
}
bool CheckKillSwitch()
{
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
if(g_start_equity<=0) return false;
double ddpct = (g_start_equity - eq) / g_start_equity * 100.0;
if(ddpct >= InpKillSwitchDDPercent)
{
if(!g_trading_disabled)
{
g_trading_disabled = true;
Print("[KILL] Drawdown ", DoubleToString(ddpct,2), "% >= ", InpKillSwitchDDPercent, "%. Trading disabled.");
if(InpCloseOnKillSwitch) CloseEAPositions();
}
return true;
}
return false;
}
bool CalcLotsForRisk(const string sym, const double risk_money, const double entry_price, const double sl_price, double &lots, const double cap_scale=1.0)
{
lots = 0.0;
double ticksize = 0, tickvalue = 0, minlot=0, maxlot=0, lotstep=0;
if(!SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE, ticksize)) return false;
if(!SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE, tickvalue)) return false;
if(!SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN, minlot)) return false;
if(!SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX, maxlot)) return false;
if(!SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP, lotstep)) return false;
double delta = MathAbs(entry_price - sl_price);
if(delta<=0 || ticksize<=0 || tickvalue<=0) return false;
double ticks = delta / ticksize;
double risk_per_lot = ticks * tickvalue; // currency per 1.00 lot
if(risk_per_lot<=0) return false;
double raw_lots = risk_money / risk_per_lot;
if(raw_lots<=0) { lots=0; return true; }
// snap to step without exceeding target risk
if(lotstep>0)
raw_lots = MathFloor(raw_lots/lotstep) * lotstep;
lots = raw_lots;
// clamp
if(lots>maxlot) lots=maxlot;
if(lots<minlot)
{
if(InpAllowTradeIfBelowMinLot)
{
double eq = AccountInfoDouble(ACCOUNT_EQUITY);
double riskpct_minlot = (risk_per_lot * minlot) / eq * 100.0;
double cap = SymbolIsXAU(sym) ? InpMaxRiskPctWhenForcingMinLot_XAU : InpMaxRiskPctWhenForcingMinLot;
// scale cap downward only (never raise above baseline)
double scale = MathMin(1.0, cap_scale);
cap *= scale;
if(riskpct_minlot > cap)
{
PrintFormat("[SKIP] Min-lot risk %.2f%% > cap %.2f%% on %s", riskpct_minlot, cap, sym);
lots = 0.0; // too risky
}
else
{
lots = minlot; // accept min lot under cap
}
}
else
lots = 0.0; // skip trade
}
// normalize to lot step digits
int step_digits = 0;
if(lotstep>0) step_digits = (int)MathRound(MathLog10(1.0/lotstep));
lots = NormalizeDouble(lots, MathMax(0, step_digits));
return true;
}
void ManageTrailing()
{
if(!InpUseTrailing) return;
MqlTick t; if(!SymbolInfoTick(g_symbol,t)) return;
double atr; if(!GetATR(1, atr)) return;
// apply master adjustments to trailing distance (wider SL under stress)
double rf, slm, cdm; GetAdjustments(rf, slm, cdm);
for(int i=PositionsTotal()-1; i>=0; --i)
{
ulong ticket = PositionGetTicket(i);
if(ticket==0) continue;
string psym = PositionGetString(POSITION_SYMBOL);
if(psym!=g_symbol) continue; // only manage current chart symbol to avoid multi-symbol side effects
long magic = (long)PositionGetInteger(POSITION_MAGIC);
if(magic!=InpMagic) continue;
long type = (long)PositionGetInteger(POSITION_TYPE);
double sl = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
double desired_sl;
if(type==POSITION_TYPE_BUY)
{
desired_sl = t.bid - atr * InpTrailATRMultiplier * slm;
if(sl==0 || desired_sl>sl)
trade.PositionModify(psym, desired_sl, tp);
}
else if(type==POSITION_TYPE_SELL)
{
desired_sl = t.ask + atr * InpTrailATRMultiplier * slm;
if(sl==0 || desired_sl<sl)
trade.PositionModify(psym, desired_sl, tp);
}
}
}
bool NewBarOnSignalTF()
{
datetime bt = iTime(g_symbol, InpSignalTF, 0);
if(bt==0) return false;
if(bt!=g_last_bar_time)
{
g_last_bar_time = bt;
return true;
}
return false;
}
bool GetSignal(bool &buySignal, bool &sellSignal)
{
buySignal=false; sellSignal=false;
double rsi; if(!GetRSI(1, rsi)) return false;
double sma=0, close1=0;
if(InpUseTrendFilter)
{
if(!GetSMA(1, sma)) return false;
double tmp[]; if(CopyClose(g_symbol, InpSignalTF, 1, 1, tmp)!=1) return false; close1=tmp[0];
}
bool upTrend = !InpUseTrendFilter || (close1 > sma);
bool dnTrend = !InpUseTrendFilter || (close1 < sma);
if(InpAllowLong && upTrend && rsi <= InpRSILow) buySignal=true;
if(InpAllowShort && dnTrend && rsi >= InpRSIHigh) sellSignal=true;
return true;
}
void TryEnter()
{
if(g_trading_disabled) return;
if(g_daily_blocked) { Print("[INFO] Daily loss limit active; trading blocked."); return; }
if(CountAllOpenPositions() >= InpMaxOpenPositions)
{
Print("[INFO] Max open positions cap reached: ", InpMaxOpenPositions);
return;
}
if(HasAnyPositionForSymbol(g_symbol))
return; // only one position per symbol
bool buySig=false, sellSig=false;
if(!GetSignal(buySig, sellSig)) return;
if(!buySig && !sellSig) return;
MqlTick t; if(!SymbolInfoTick(g_symbol, t)) return;
double atr; if(!GetATR(1, atr)) return;
if(!CheckSpreadAtrOK(atr)) return; // skip on wide spreads or extreme ATR
// read master adjustments
double rf=1.0, slm=1.0, cdm=1.0; GetAdjustments(rf, slm, cdm);
double risk_money = AccountInfoDouble(ACCOUNT_EQUITY) * (InpRiskPercent/100.0);
risk_money *= rf; // scale risk dynamically (news/vol/equity)
// cooldown by bars since last trade (scaled by cooldown_mult)
int effCooldown = EffectiveCooldownBars();
if(effCooldown>0 && g_last_trade_bar_time>0)
{
int last_shift = iBarShift(g_symbol, InpSignalTF, g_last_trade_bar_time, false);
if(last_shift >= 0 && last_shift < effCooldown)
{
Print("[INFO] Cooldown active: waiting ", effCooldown - last_shift, " more bars before next trade.");
return;
}
}
if(buySig)
{
double sl = t.ask - atr*(InpATRMultiplierSL * slm);
double tp = 0.0; if(InpTP_ATR_Mult>0) tp = t.ask + atr*(InpTP_ATR_Mult * slm);
double lots; if(!CalcLotsForRisk(g_symbol, risk_money, t.ask, sl, lots, rf)) return;
if(lots<=0) { Print("[SKIP] Computed lots below min or invalid for BUY."); return; }
trade.SetExpertMagicNumber(InpMagic);
if(!trade.Buy(lots, g_symbol, t.ask, sl, tp, "CascadeGreepEA BUY"))
Print("[ERR] Buy failed: ", _LastError);
else
g_last_trade_bar_time = iTime(g_symbol, InpSignalTF, 0);
}
else if(sellSig)
{
double sl = t.bid + atr*(InpATRMultiplierSL * slm);
double tp = 0.0; if(InpTP_ATR_Mult>0) tp = t.bid - atr*(InpTP_ATR_Mult * slm);
double lots; if(!CalcLotsForRisk(g_symbol, risk_money, t.bid, sl, lots, rf)) return;
if(lots<=0) { Print("[SKIP] Computed lots below min or invalid for SELL."); return; }
trade.SetExpertMagicNumber(InpMagic);
if(!trade.Sell(lots, g_symbol, t.bid, sl, tp, "CascadeGreepEA SELL"))
Print("[ERR] Sell failed: ", _LastError);
else
g_last_trade_bar_time = iTime(g_symbol, InpSignalTF, 0);
}
}
//==================== Master Adjustments (Global Variables) ====================
double ReadGVDouble(const string name, const double defv)
{
if(!GlobalVariableCheck(name)) return defv;
return GlobalVariableGet(name);
}
void GetAdjustments(double &risk_mult, double &sl_mult, double &cooldown_mult)
{
risk_mult=1.0; sl_mult=1.0; cooldown_mult=1.0;
if(!InpUseMasterAdjust) return;
string base = InpGVNamespaceEA + "/";
string sym = g_symbol;
risk_mult = ReadGVDouble(base+"risk_mult/"+sym, 1.0);
sl_mult = ReadGVDouble(base+"sl_mult/"+sym, 1.0);
cooldown_mult = ReadGVDouble(base+"cooldown_mult/"+sym, 1.0);
}
int EffectiveCooldownBars()
{
double rf, sm, cm; GetAdjustments(rf, sm, cm);
int baseBars = InpMinBarsBetweenTrades;
int eff = (int)MathCeil(baseBars * cm);
if(eff<0) eff=0;
return eff;
}
//==================== MT5 Events ====================
int OnInit()
{
g_symbol = _Symbol;
g_start_equity = AccountInfoDouble(ACCOUNT_EQUITY);
g_day_key = TodayKey();
g_day_equity_start = g_start_equity;
g_daily_blocked = false;
hATR = iATR(g_symbol, InpSignalTF, InpATRPeriod);
if(hATR==INVALID_HANDLE) { Print("[FATAL] ATR handle failed."); return INIT_FAILED; }
hRSI = iRSI(g_symbol, InpSignalTF, InpRSIPeriod, PRICE_CLOSE);
if(hRSI==INVALID_HANDLE) { Print("[FATAL] RSI handle failed."); return INIT_FAILED; }
if(InpUseTrendFilter)
{
hSMA = iMA(g_symbol, InpSignalTF, InpSMAPeriod, 0, MODE_SMA, PRICE_CLOSE);
if(hSMA==INVALID_HANDLE) { Print("[FATAL] SMA handle failed."); return INIT_FAILED; }
}
trade.SetDeviationInPoints(InpMaxSlippagePoints);
Print("[INIT] CascadeGreepEA on ", g_symbol, ", start equity=", g_start_equity);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
if(hATR!=INVALID_HANDLE) IndicatorRelease(hATR);
if(hRSI!=INVALID_HANDLE) IndicatorRelease(hRSI);
if(hSMA!=INVALID_HANDLE) IndicatorRelease(hSMA);
}
void OnTick()
{
DailyResetIfNeeded();
if(CheckKillSwitch()) return;
if(CheckDailyLossLimit()) return;
ManageTrailing();
if(NewBarOnSignalTF())
TryEnter();
}
//+------------------------------------------------------------------+