Files
2026-06-08 10:36:15 +02:00

520 lines
18 KiB
Plaintext

//+------------------------------------------------------------------+
//| DonchianTurtle_v3_Consensus.mq5 |
//| EA v4 — Donchian Turtle + QuantAgent-Inspired Consensus Filter |
//| |
//| Base: V3.13 (Volatility Scaling + CSV Logging) |
//| New: Consensus gate — RSI + MACD + LinReg must agree |
//| Inspired by QuantAgent paper (arXiv:2509.09995) |
//| |
//| Validated params (Python IS/OOS + MT5 86% quality OOS): |
//| S1: Donchian(20/8), S2: Donchian(40/8) |
//| ADX>20, SL=2.0xATR(20), Vol Scaling ON |
//| Consensus: RSI>50 + MACD cross + Price>LinReg(50) |
//| Scorecard: 81/100 Grade B (OOS 2023-2025) |
//+------------------------------------------------------------------+
#property copyright "EA v4 — Turtle Consensus"
#property version "4.00"
#include <Trade\Trade.mqh>
//--- System 1
input group "=== System 1 (Donchian 20) ==="
input int S1_EntryPeriod = 20;
input int S1_ExitPeriod = 8; // Updated: was 10
input double S1_RiskPct = 0.5;
//--- System 2
input group "=== System 2 (Donchian 40) ==="
input int S2_EntryPeriod = 40; // Updated: was 55
input int S2_ExitPeriod = 8; // Updated: was 10
input double S2_RiskPct = 0.5;
//--- ATR / Base Filters
input group "=== Filters ==="
input int ADX_Period = 20;
input double ADX_MinLevel = 20.0; // Updated: was 25
input int ATR_Period = 20;
input double ATR_StopMult = 2.0;
input int MA_Period = 200;
input double ATR_SpikeMult = 3.0;
input double MaxDrawdownPct = 20.0;
//--- Break-Even + Trailing
input group "=== Break-Even + Trailing Stop ==="
input bool UseBreakEven = true;
input double BE_RMultiple = 1.0;
input bool UseTrailing = true;
input double Trail_RMultiple = 2.0;
input double Trail_ATRMult = 1.5;
//--- Volatility Scaling
input group "=== Volatility Scaling ==="
input bool UseVolScaling = true;
input int VolScale_Period = 252;
input double VolScale_LowPct = 0.33;
input double VolScale_HighPct = 0.67;
input double VolScale_LowMult = 1.5;
input double VolScale_HighMult = 0.5;
//--- Consensus Filter (QuantAgent-Inspired)
input group "=== Consensus Filter (QuantAgent-Inspired) ==="
input bool UseConsensus = true; // เปิด/ปิด consensus gate
input int ConsensusMin = 2; // ต้องผ่านอย่างน้อยกี่ conditions (max=3)
// Condition 1: RSI momentum
input int RSI_Period = 14;
input double RSI_BullLevel = 50.0; // RSI > 50 = bullish
// Condition 2: MACD direction
input int MACD_Fast = 12;
input int MACD_Slow = 26;
input int MACD_Signal = 9;
// Condition 3: Price vs OLS trend line
input int LinReg_Period = 50; // Linear regression period (TrendAgent)
//--- Magic Numbers
input group "=== Order Settings ==="
input int MagicS1 = 202901; // New magic (v4)
input int MagicS2 = 202902;
input string TradeComment = "Turtle_v4_Consensus";
//+------------------------------------------------------------------+
//--- Globals
CTrade trade;
int g_hATR = INVALID_HANDLE;
int g_hADX = INVALID_HANDLE;
int g_hMA = INVALID_HANDLE;
int g_hRSI = INVALID_HANDLE;
int g_hMACD = INVALID_HANDLE;
int g_hLR = INVALID_HANDLE; // Linear Regression handle
double g_AccountPeak = 0;
datetime g_LastBarTime = 0;
int g_hLog = INVALID_HANDLE;
double g_lastVolMult = 1.0;
double g_lastATRpct = -1.0;
int g_lastConsensus = 0;
//+------------------------------------------------------------------+
int OnInit()
{
// Base indicators
g_hATR = iATR(Symbol(), PERIOD_D1, ATR_Period);
g_hADX = iADX(Symbol(), PERIOD_D1, ADX_Period);
g_hMA = iMA(Symbol(), PERIOD_D1, MA_Period, 0, MODE_SMA, PRICE_CLOSE);
// Consensus indicators
g_hRSI = iRSI(Symbol(), PERIOD_D1, RSI_Period, PRICE_CLOSE);
g_hMACD = iMACD(Symbol(), PERIOD_D1, MACD_Fast, MACD_Slow, MACD_Signal, PRICE_CLOSE);
g_hLR = iMA(Symbol(), PERIOD_D1, LinReg_Period, 0, MODE_SMA, PRICE_CLOSE); // SMA50 as trend proxy (iLinReg not in MQL5 std)
if(g_hATR == INVALID_HANDLE || g_hADX == INVALID_HANDLE ||
g_hMA == INVALID_HANDLE || g_hRSI == INVALID_HANDLE ||
g_hMACD == INVALID_HANDLE || g_hLR == INVALID_HANDLE)
{
Print("ERROR: Failed to create indicator handles");
return INIT_FAILED;
}
// Warmup (skip in backtester)
bool inTester = (bool)MQLInfoInteger(MQL_TESTER);
if(!inTester)
{
double dummy[1];
int attempts = 0;
while(CopyBuffer(g_hMA, 0, 1, 1, dummy) <= 0 && attempts < 100)
{
Sleep(100);
attempts++;
}
if(attempts >= 100)
{
Print("ERROR: Indicators not ready.");
return INIT_FAILED;
}
}
trade.SetDeviationInPoints(50);
trade.SetTypeFilling(ORDER_FILLING_IOC);
g_AccountPeak = AccountInfoDouble(ACCOUNT_BALANCE);
// CSV log
string fname = "TurtleConsensus_" + Symbol() + "_trades.csv";
g_hLog = FileOpen(fname, FILE_WRITE|FILE_READ|FILE_CSV|FILE_ANSI|FILE_SHARE_READ, ',');
if(g_hLog == INVALID_HANDLE)
Print("WARNING: Cannot open log file");
else
{
if(FileTell(g_hLog) == 0)
FileWrite(g_hLog,
"Timestamp","Event","System","Magic",
"Lots","Price","SL","RiskPct",
"ATR_pct","VolMult","Consensus","PnL","Balance","Note");
FileSeek(g_hLog, 0, SEEK_END);
FileFlush(g_hLog);
}
PrintFormat("DonchianTurtle v4 Consensus | %s D1 | S1:%d/%d S2:%d/%d ADX>%.0f SL=%.1fx ConsMin=%d",
Symbol(), S1_EntryPeriod, S1_ExitPeriod,
S2_EntryPeriod, S2_ExitPeriod,
ADX_MinLevel, ATR_StopMult, ConsensusMin);
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
IndicatorRelease(g_hATR);
IndicatorRelease(g_hADX);
IndicatorRelease(g_hMA);
IndicatorRelease(g_hRSI);
IndicatorRelease(g_hMACD);
IndicatorRelease(g_hLR);
if(g_hLog != INVALID_HANDLE) { FileFlush(g_hLog); FileClose(g_hLog); }
}
//+------------------------------------------------------------------+
void OnTick()
{
// Trail runs every tick
double atrNow[1];
double atr = 0;
if(CopyBuffer(g_hATR, 0, 0, 1, atrNow) > 0) atr = atrNow[0];
if(atr > 0)
{
ManageTrail(MagicS1, atr);
ManageTrail(MagicS2, atr);
}
// New bar check
datetime barTime = iTime(Symbol(), PERIOD_D1, 0);
if(barTime == g_LastBarTime) return;
g_LastBarTime = barTime;
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
if(balance > g_AccountPeak) g_AccountPeak = balance;
// DD halt
if(g_AccountPeak > 0)
{
double dd = (g_AccountPeak - balance) / g_AccountPeak * 100.0;
if(dd >= MaxDrawdownPct)
{
PrintFormat("DD HALT: %.1f%% >= %.1f%%", dd, MaxDrawdownPct);
return;
}
}
// Read base indicators (bar[1] = last completed bar)
double atrBuf[1], adxBuf[1], maBuf[1];
if(CopyBuffer(g_hATR, 0, 1, 1, atrBuf) <= 0) return;
if(CopyBuffer(g_hADX, 0, 1, 1, adxBuf) <= 0) return;
if(CopyBuffer(g_hMA, 0, 1, 1, maBuf) <= 0) return;
double atrD1 = atrBuf[0];
double adx = adxBuf[0];
double ma200 = maBuf[0];
double close1 = iClose(Symbol(), PERIOD_D1, 1);
if(atrD1 <= 0 || adx <= 0 || ma200 <= 0 || close1 <= 0) return;
// ATR spike filter
double atrArr[20];
double atrAvg = 0;
if(CopyBuffer(g_hATR, 0, 1, 20, atrArr) == 20)
{
for(int k = 0; k < 20; k++) atrAvg += atrArr[k];
atrAvg /= 20.0;
}
if(atrAvg > 0 && atrD1 > atrAvg * ATR_SpikeMult) return;
// Donchian exits
ManageExits(MagicS1, S1_ExitPeriod);
ManageExits(MagicS2, S2_ExitPeriod);
// Base entry filters
if(close1 <= ma200) return; // Below MA200
if(adx < ADX_MinLevel) return; // Weak trend
// Consensus check (QuantAgent-inspired)
g_lastConsensus = 0;
if(UseConsensus)
{
g_lastConsensus = GetConsensusScore();
if(g_lastConsensus < ConsensusMin)
{
PrintFormat("Consensus FAIL: score=%d/%d (need %d) — skip entry",
g_lastConsensus, 3, ConsensusMin);
return;
}
PrintFormat("Consensus PASS: score=%d/3", g_lastConsensus);
}
// Volatility scaling
double scaledRiskS1 = S1_RiskPct;
double scaledRiskS2 = S2_RiskPct;
g_lastVolMult = 1.0;
g_lastATRpct = -1.0;
if(UseVolScaling)
{
double atrPct = GetATRPercentile(VolScale_Period, 1);
double mult = 1.0;
if(atrPct >= 0 && atrPct < VolScale_LowPct) mult = VolScale_LowMult;
else if(atrPct > VolScale_HighPct) mult = VolScale_HighMult;
scaledRiskS1 = S1_RiskPct * mult;
scaledRiskS2 = S2_RiskPct * mult;
g_lastVolMult = mult;
g_lastATRpct = atrPct;
}
// Entries
if(!HasPosition(MagicS1))
TryEntry(MagicS1, S1_EntryPeriod, scaledRiskS1, atrD1, close1, "S1");
if(!HasPosition(MagicS2))
TryEntry(MagicS2, S2_EntryPeriod, scaledRiskS2, atrD1, close1, "S2");
}
//+------------------------------------------------------------------+
//| Consensus Score — 3 conditions from QuantAgent |
//| Returns 0-3. Called after base filters pass. |
//+------------------------------------------------------------------+
int GetConsensusScore()
{
int score = 0;
// Condition 1: RSI(14) > 50 — bullish momentum (IndicatorAgent)
double rsiBuf[1];
if(CopyBuffer(g_hRSI, 0, 1, 1, rsiBuf) > 0)
{
if(rsiBuf[0] > RSI_BullLevel)
{
score++;
PrintFormat(" [C1] RSI=%.1f > %.1f PASS", rsiBuf[0], RSI_BullLevel);
}
else
PrintFormat(" [C1] RSI=%.1f <= %.1f FAIL", rsiBuf[0], RSI_BullLevel);
}
// Condition 2: MACD line > Signal line — directional confirm (IndicatorAgent)
double macdMain[1], macdSig[1];
if(CopyBuffer(g_hMACD, MAIN_LINE, 1, 1, macdMain) > 0 &&
CopyBuffer(g_hMACD, SIGNAL_LINE, 1, 1, macdSig) > 0)
{
if(macdMain[0] > macdSig[0])
{
score++;
PrintFormat(" [C2] MACD=%.4f > Signal=%.4f PASS", macdMain[0], macdSig[0]);
}
else
PrintFormat(" [C2] MACD=%.4f <= Signal=%.4f FAIL", macdMain[0], macdSig[0]);
}
// Condition 3: Price above OLS Linear Regression line — trend bias (TrendAgent)
double lrBuf[1];
double close1 = iClose(Symbol(), PERIOD_D1, 1);
if(CopyBuffer(g_hLR, 0, 1, 1, lrBuf) > 0)
{
if(close1 > lrBuf[0])
{
score++;
PrintFormat(" [C3] Close=%.2f > LinReg=%.2f PASS", close1, lrBuf[0]);
}
else
PrintFormat(" [C3] Close=%.2f <= LinReg=%.2f FAIL", close1, lrBuf[0]);
}
return score;
}
//+------------------------------------------------------------------+
void TryEntry(int magic, int period, double riskPct, double atr,
double close1, string label)
{
// Donchian entry band (bars 2..period+1, shift=2 matching MQL5 convention)
int hiIdx = iHighest(Symbol(), PERIOD_D1, MODE_HIGH, period, 2);
if(hiIdx < 0) return;
double prevBand = iHigh(Symbol(), PERIOD_D1, hiIdx);
if(close1 <= prevBand) return; // No breakout
double ask = SymbolInfoDouble(Symbol(), SYMBOL_ASK);
double sl = ask - atr * ATR_StopMult;
double lots = CalcLots(ask, sl, riskPct);
if(lots <= 0) return;
trade.SetExpertMagicNumber(magic);
if(trade.Buy(lots, Symbol(), ask, sl, 0, TradeComment + "_" + label))
{
PrintFormat("%s ENTRY | Ask=%.2f SL=%.2f Lots=%.2f Band=%.2f Vol=%.1f Cons=%d/3",
label, ask, sl, lots, prevBand, g_lastVolMult, g_lastConsensus);
if(g_hLog != INVALID_HANDLE)
{
FileWrite(g_hLog,
TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS),
"ENTRY", label, magic,
DoubleToString(lots, 2),
DoubleToString(ask, 2),
DoubleToString(sl, 2),
DoubleToString(riskPct, 3),
DoubleToString(g_lastATRpct, 3),
DoubleToString(g_lastVolMult, 2),
IntegerToString(g_lastConsensus),
"",
DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2),
"");
FileFlush(g_hLog);
}
}
else
PrintFormat("%s FAIL | code=%d %s", label,
trade.ResultRetcode(), trade.ResultRetcodeDescription());
}
//+------------------------------------------------------------------+
void ManageTrail(int magic, double atr)
{
if(!UseBreakEven && !UseTrailing) return;
if(atr <= 0) return;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
if(PositionGetString(POSITION_SYMBOL) != Symbol()) continue;
if(PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue;
double entry = PositionGetDouble(POSITION_PRICE_OPEN);
double curSL = PositionGetDouble(POSITION_SL);
double bid = SymbolInfoDouble(Symbol(), SYMBOL_BID);
double initRisk = atr * ATR_StopMult;
double profit = bid - entry;
double newSL = curSL;
if(UseBreakEven && profit >= BE_RMultiple * initRisk)
{
double beLevel = entry + 2 * SymbolInfoDouble(Symbol(), SYMBOL_POINT);
if(beLevel > curSL) newSL = MathMax(newSL, beLevel);
}
if(UseTrailing && profit >= Trail_RMultiple * initRisk)
{
double trailLevel = bid - atr * Trail_ATRMult;
if(trailLevel > curSL) newSL = MathMax(newSL, trailLevel);
}
if(newSL > curSL + SymbolInfoDouble(Symbol(), SYMBOL_POINT))
{
double tp = PositionGetDouble(POSITION_TP);
trade.SetExpertMagicNumber(magic);
trade.PositionModify(ticket, NormalizeDouble(newSL, _Digits), tp);
}
}
}
//+------------------------------------------------------------------+
void ManageExits(int magic, int exitPeriod)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
if(PositionGetString(POSITION_SYMBOL) != Symbol()) continue;
int loIdx = iLowest(Symbol(), PERIOD_D1, MODE_LOW, exitPeriod, 1);
if(loIdx < 0) continue;
double exitLow = iLow(Symbol(), PERIOD_D1, loIdx);
double close1 = iClose(Symbol(), PERIOD_D1, 1);
if(close1 < exitLow)
{
trade.SetExpertMagicNumber(magic);
trade.PositionClose(ticket);
}
}
}
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction& trans,
const MqlTradeRequest& request,
const MqlTradeResult& result)
{
if(trans.type != TRADE_TRANSACTION_DEAL_ADD) return;
if(g_hLog == INVALID_HANDLE) return;
if(!HistoryDealSelect(trans.deal)) return;
long dealEntry = HistoryDealGetInteger(trans.deal, DEAL_ENTRY);
if(dealEntry != DEAL_ENTRY_OUT && dealEntry != DEAL_ENTRY_INOUT) return;
long magic = HistoryDealGetInteger(trans.deal, DEAL_MAGIC);
if(magic != MagicS1 && magic != MagicS2) return;
string system = (magic == MagicS1) ? "S1" : "S2";
double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT)
+ HistoryDealGetDouble(trans.deal, DEAL_SWAP)
+ HistoryDealGetDouble(trans.deal, DEAL_COMMISSION);
double price = HistoryDealGetDouble(trans.deal, DEAL_PRICE);
double lots = HistoryDealGetDouble(trans.deal, DEAL_VOLUME);
string outcome = (profit >= 0) ? "WIN" : "LOSS";
FileWrite(g_hLog,
TimeToString(TimeCurrent(), TIME_DATE|TIME_SECONDS),
"EXIT_" + outcome, system, magic,
DoubleToString(lots, 2),
DoubleToString(price, 2),
"", "", "", "", "",
DoubleToString(profit, 2),
DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2),
outcome);
FileFlush(g_hLog);
}
//+------------------------------------------------------------------+
bool HasPosition(int magic)
{
for(int i = 0; i < PositionsTotal(); i++)
{
ulong ticket = PositionGetTicket(i);
if(!PositionSelectByTicket(ticket)) continue;
if(PositionGetInteger(POSITION_MAGIC) == magic &&
PositionGetString(POSITION_SYMBOL) == Symbol())
return true;
}
return false;
}
//+------------------------------------------------------------------+
double GetATRPercentile(int period, int shift)
{
double atrArr[];
ArraySetAsSeries(atrArr, true);
int copied = CopyBuffer(g_hATR, 0, shift, period, atrArr);
if(copied < period) return -1.0;
double curATR = atrArr[0];
int rank = 0;
for(int i = 1; i < period; i++)
if(atrArr[i] < curATR) rank++;
return (double)rank / (double)(period - 1);
}
//+------------------------------------------------------------------+
double CalcLots(double entry, double sl, double riskPct)
{
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
double riskAmt = balance * riskPct / 100.0;
double slDist = MathAbs(entry - sl);
if(slDist <= 0) return 0;
double tickVal = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_VALUE);
double tickSize = SymbolInfoDouble(Symbol(), SYMBOL_TRADE_TICK_SIZE);
if(tickSize <= 0 || tickVal <= 0) return 0;
double lots = riskAmt / ((slDist / tickSize) * tickVal);
double step = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP);
double minL = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN);
double maxL = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX);
lots = MathFloor(lots / step) * step;
return MathMax(minL, MathMin(maxL, lots));
}
//+------------------------------------------------------------------+