改为自己的mt5api
This commit is contained in:
@@ -0,0 +1,597 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| BB_RSI_MeanReversion.mq5 |
|
||||
//| Version: 1.0 |
|
||||
//| Description: Mean reversion EA using Bollinger Bands + RSI |
|
||||
//| on M1 with optional M15 higher-timeframe context. |
|
||||
//| |
|
||||
//| RISK WARNING: This EA is for educational purposes only. |
|
||||
//| Live trading requires proper risk assessment, forward testing, |
|
||||
//| and understanding of all risks involved in Forex trading. |
|
||||
//| Past performance does not guarantee future results. |
|
||||
//+------------------------------------------------------------------+
|
||||
#property copyright "GENESIS Strategy B — Ares"
|
||||
#property version "1.00"
|
||||
#property strict
|
||||
|
||||
#include <Trade\Trade.mqh>
|
||||
#include <Trade\PositionInfo.mqh>
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| INPUT GROUPS |
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
// --- 1. Trade Filters ---
|
||||
input string Inp_TradeComment = "BB_RSI_M1"; // EA comment
|
||||
input bool Inp_AllowLong = true; // Allow long trades
|
||||
input bool Inp_AllowShort = true; // Allow short trades
|
||||
input int Inp_MagicNumber = 20250514; // EA magic number
|
||||
|
||||
// --- 2. Bollinger Bands ---
|
||||
input int Inp_BB_Period = 20; // BB period
|
||||
input double Inp_BB_Deviation = 2.0; // BB deviation
|
||||
input int Inp_BB_Shift = 0; // BB shift
|
||||
input ENUM_MA_METHOD Inp_BB_MA_Method = MODE_SMA; // BB MA method
|
||||
input ENUM_APPLIED_PRICE Inp_BB_Price = PRICE_CLOSE; // BB applied price
|
||||
|
||||
// --- 3. RSI ---
|
||||
input int Inp_RSI_Period = 14; // RSI period
|
||||
input double Inp_RSI_Oversold = 30.0; // RSI oversold level
|
||||
input double Inp_RSI_Overbought = 70.0; // RSI overbought level
|
||||
input ENUM_APPLIED_PRICE Inp_RSI_Price = PRICE_CLOSE; // RSI applied price
|
||||
|
||||
// --- 4. Higher Timeframe Context (M15) ---
|
||||
input bool Inp_UseM15Context = true; // Use M15 context
|
||||
input ENUM_TIMEFRAMES Inp_ContextTF = PERIOD_M15; // Context timeframe
|
||||
input int Inp_ContextMAPeriod = 50; // Context MA period
|
||||
input double Inp_ContextMATol = 0.0002; // Distance tolerance from MA
|
||||
|
||||
// --- 5. Entry Logic ---
|
||||
input bool Inp_RequireOutsideBand = true; // Price must close outside BB
|
||||
input bool Inp_RequireRSIFilter = true; // Require RSI filter
|
||||
input int Inp_CandlesSinceSignal = 1; // Candle index (1=last closed)
|
||||
|
||||
// --- 6. Risk & Money Management ---
|
||||
input double Inp_RiskPercent = 1.0; // % account risked per trade
|
||||
input bool Inp_UseFixedLot = false; // Use fixed lot
|
||||
input double Inp_FixedLot = 0.01; // Fixed lot size
|
||||
input int Inp_StopLossPips = 20; // SL in pips
|
||||
input int Inp_TakeProfitPips = 40; // TP in pips
|
||||
input bool Inp_UseTrailingStop = false; // Enable trailing stop
|
||||
input int Inp_TrailingStartPips = 15; // Profit pips to start trailing
|
||||
input int Inp_TrailingStepPips = 5; // Trailing step in pips
|
||||
|
||||
// --- 7. Time & Session Filters ---
|
||||
input bool Inp_UseTimeFilter = true; // Restrict trading hours
|
||||
input int Inp_StartHour = 5; // Start hour (GMT)
|
||||
input int Inp_StartMinute = 0; // Start minute
|
||||
input int Inp_EndHour = 17; // End hour (GMT)
|
||||
input int Inp_EndMinute = 0; // End minute
|
||||
input bool Inp_UseNewsFilter = true; // Avoid news events
|
||||
input string Inp_NewsFile = "news.txt"; // News timestamps file
|
||||
|
||||
// --- 8. Spread & Slippage ---
|
||||
input double Inp_MaxSpreadPips = 1.0; // Max allowed spread (pips)
|
||||
input int Inp_Slippage = 10; // Slippage tolerance (points)
|
||||
input int Inp_MaxRetries = 3; // Max order send retries
|
||||
|
||||
// --- 9. Drawdown Protection ---
|
||||
input bool Inp_UseDailyLossLimit = true; // Stop after daily loss
|
||||
input double Inp_DailyLossPercent = 6.0; // Max daily loss %
|
||||
input bool Inp_UseGlobalDDLimit = true; // Global drawdown halt
|
||||
input double Inp_GlobalDDPercent = 25.0; // Max total DD %
|
||||
input bool Inp_CloseAllOnDD = true; // Close all on DD breach
|
||||
|
||||
// --- 10. Execution ---
|
||||
input bool Inp_UseOnePositionPerDir = true; // One position per direction
|
||||
input int Inp_MinSecondsBetweenTrades = 30; // Cooldown seconds
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| GLOBAL VARIABLES |
|
||||
//+------------------------------------------------------------------+
|
||||
CTrade g_Trade;
|
||||
CPositionInfo g_Position;
|
||||
|
||||
int g_BB_Handle = INVALID_HANDLE;
|
||||
int g_RSI_Handle = INVALID_HANDLE;
|
||||
int g_MA_Handle = INVALID_HANDLE;
|
||||
|
||||
double g_PipSize = 0.0;
|
||||
double g_PeakEquity = 0.0;
|
||||
double g_DayStartBal = 0.0;
|
||||
datetime g_LastBarTime = 0;
|
||||
datetime g_LastTradeCloseTime = 0;
|
||||
bool g_TradingDisabled = false;
|
||||
datetime g_CurrentDayStart = 0;
|
||||
|
||||
datetime g_NewsTimes[];
|
||||
int g_NewsCount = 0;
|
||||
int g_NewsMinutes = 15; // minutes before/after to block
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| OnInit |
|
||||
//+------------------------------------------------------------------+
|
||||
int OnInit()
|
||||
{
|
||||
// Determine pip size (4-digit vs 5-digit broker)
|
||||
int digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
||||
g_PipSize = (digits == 3 || digits == 5) ? _Point * 10.0 : _Point;
|
||||
|
||||
// Create indicator handles
|
||||
g_BB_Handle = iBands(_Symbol, PERIOD_M1, Inp_BB_Period, Inp_BB_Shift,
|
||||
Inp_BB_Deviation, Inp_BB_Price);
|
||||
g_RSI_Handle = iRSI(_Symbol, PERIOD_M1, Inp_RSI_Period, Inp_RSI_Price);
|
||||
g_MA_Handle = iMA(_Symbol, Inp_ContextTF, Inp_ContextMAPeriod, 0,
|
||||
MODE_SMA, PRICE_CLOSE);
|
||||
|
||||
if(g_BB_Handle == INVALID_HANDLE ||
|
||||
g_RSI_Handle == INVALID_HANDLE ||
|
||||
g_MA_Handle == INVALID_HANDLE)
|
||||
{
|
||||
Print("ERROR: Failed to create indicator handles. EA stopping.");
|
||||
return INIT_FAILED;
|
||||
}
|
||||
|
||||
// Configure trade object
|
||||
g_Trade.SetExpertMagicNumber(Inp_MagicNumber);
|
||||
g_Trade.SetDeviationInPoints(Inp_Slippage);
|
||||
g_Trade.SetTypeFilling(ORDER_FILLING_FOK);
|
||||
|
||||
// Initialise equity tracking
|
||||
g_PeakEquity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
g_DayStartBal = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
g_CurrentDayStart = GetDayStart(TimeCurrent());
|
||||
|
||||
// Load news filter file
|
||||
if(Inp_UseNewsFilter) LoadNewsFile();
|
||||
|
||||
Print("BB_RSI_MeanReversion EA initialised. PipSize=", g_PipSize,
|
||||
" | Magic=", Inp_MagicNumber);
|
||||
return INIT_SUCCEEDED;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| OnDeinit |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnDeinit(const int reason)
|
||||
{
|
||||
if(g_BB_Handle != INVALID_HANDLE) IndicatorRelease(g_BB_Handle);
|
||||
if(g_RSI_Handle != INVALID_HANDLE) IndicatorRelease(g_RSI_Handle);
|
||||
if(g_MA_Handle != INVALID_HANDLE) IndicatorRelease(g_MA_Handle);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| OnTick |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTick()
|
||||
{
|
||||
// 0. If globally disabled, just manage trailing on existing positions
|
||||
if(g_TradingDisabled)
|
||||
{
|
||||
if(Inp_UseTrailingStop) ManageTrailingStop();
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Only act on new bar
|
||||
if(!IsNewBar()) return;
|
||||
|
||||
// 2. Update peak equity
|
||||
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
if(equity > g_PeakEquity) g_PeakEquity = equity;
|
||||
|
||||
// 3. Reset day tracking if new day
|
||||
datetime today = GetDayStart(TimeCurrent());
|
||||
if(today != g_CurrentDayStart)
|
||||
{
|
||||
g_CurrentDayStart = today;
|
||||
g_DayStartBal = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
Print("New trading day. Starting balance: ", g_DayStartBal);
|
||||
}
|
||||
|
||||
// 4. Global drawdown check
|
||||
if(Inp_UseGlobalDDLimit && g_PeakEquity > 0)
|
||||
{
|
||||
double ddPct = (g_PeakEquity - equity) / g_PeakEquity * 100.0;
|
||||
if(ddPct >= Inp_GlobalDDPercent)
|
||||
{
|
||||
Print("GLOBAL DRAWDOWN LIMIT HIT: ", DoubleToString(ddPct, 2),
|
||||
"% >= ", Inp_GlobalDDPercent, "%. Halting EA.");
|
||||
if(Inp_CloseAllOnDD) CloseAllPositions();
|
||||
g_TradingDisabled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Daily loss check
|
||||
if(Inp_UseDailyLossLimit && g_DayStartBal > 0)
|
||||
{
|
||||
double dayLossPct = (g_DayStartBal - AccountInfoDouble(ACCOUNT_BALANCE))
|
||||
/ g_DayStartBal * 100.0;
|
||||
if(dayLossPct >= Inp_DailyLossPercent)
|
||||
{
|
||||
Print("DAILY LOSS LIMIT HIT: ", DoubleToString(dayLossPct, 2),
|
||||
"% >= ", Inp_DailyLossPercent, "%. Skipping until tomorrow.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Time filter
|
||||
if(Inp_UseTimeFilter && !IsTradeTime()) return;
|
||||
|
||||
// 7. Spread filter
|
||||
double spreadPips = GetCurrentSpreadPips();
|
||||
if(spreadPips > Inp_MaxSpreadPips)
|
||||
{
|
||||
Print("Spread too high: ", DoubleToString(spreadPips, 2),
|
||||
" pips > max ", Inp_MaxSpreadPips);
|
||||
return;
|
||||
}
|
||||
|
||||
// 8. News filter
|
||||
if(Inp_UseNewsFilter && IsNewsTime()) return;
|
||||
|
||||
// 9. Cooldown check
|
||||
if((int)(TimeCurrent() - g_LastTradeCloseTime) < Inp_MinSecondsBetweenTrades)
|
||||
return;
|
||||
|
||||
// 10. Get indicator values
|
||||
double bbUpper[], bbLower[], bbMiddle[];
|
||||
double rsiVal[];
|
||||
ArraySetAsSeries(bbUpper, true);
|
||||
ArraySetAsSeries(bbLower, true);
|
||||
ArraySetAsSeries(bbMiddle, true);
|
||||
ArraySetAsSeries(rsiVal, true);
|
||||
|
||||
int idx = Inp_CandlesSinceSignal; // 1 = last closed candle
|
||||
int need = idx + 2;
|
||||
|
||||
if(CopyBuffer(g_BB_Handle, 1, 0, need, bbUpper) < need) return; // Upper
|
||||
if(CopyBuffer(g_BB_Handle, 2, 0, need, bbLower) < need) return; // Lower
|
||||
if(CopyBuffer(g_BB_Handle, 0, 0, need, bbMiddle) < need) return; // Middle
|
||||
if(CopyBuffer(g_RSI_Handle, 0, 0, need, rsiVal) < need) return;
|
||||
|
||||
double closePrice = iClose(_Symbol, PERIOD_M1, idx);
|
||||
double rsi = rsiVal[idx];
|
||||
double bbUp = bbUpper[idx];
|
||||
double bbLow = bbLower[idx];
|
||||
|
||||
// 11. M15 context
|
||||
double contextMA = 0.0;
|
||||
if(Inp_UseM15Context)
|
||||
{
|
||||
double maArr[];
|
||||
ArraySetAsSeries(maArr, true);
|
||||
if(CopyBuffer(g_MA_Handle, 0, 0, 2, maArr) < 2) return;
|
||||
contextMA = maArr[0];
|
||||
}
|
||||
|
||||
// 12. Signal generation
|
||||
bool longSignal = false;
|
||||
bool shortSignal = false;
|
||||
|
||||
// Long
|
||||
if(Inp_AllowLong)
|
||||
{
|
||||
bool bbOk = !Inp_RequireOutsideBand || (closePrice < bbLow);
|
||||
bool rsiOk = !Inp_RequireRSIFilter || (rsi < Inp_RSI_Oversold);
|
||||
bool ctxOk = !Inp_UseM15Context || (closePrice > contextMA - Inp_ContextMATol);
|
||||
longSignal = bbOk && rsiOk && ctxOk;
|
||||
}
|
||||
|
||||
// Short
|
||||
if(Inp_AllowShort)
|
||||
{
|
||||
bool bbOk = !Inp_RequireOutsideBand || (closePrice > bbUp);
|
||||
bool rsiOk = !Inp_RequireRSIFilter || (rsi > Inp_RSI_Overbought);
|
||||
bool ctxOk = !Inp_UseM15Context || (closePrice < contextMA + Inp_ContextMATol);
|
||||
shortSignal = bbOk && rsiOk && ctxOk;
|
||||
}
|
||||
|
||||
// 13. Position check
|
||||
if(longSignal && Inp_UseOnePositionPerDir && HasPositionInDirection(POSITION_TYPE_BUY))
|
||||
longSignal = false;
|
||||
if(shortSignal && Inp_UseOnePositionPerDir && HasPositionInDirection(POSITION_TYPE_SELL))
|
||||
shortSignal = false;
|
||||
|
||||
// 14. Execute
|
||||
if(longSignal)
|
||||
{
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
double sl = ask - Inp_StopLossPips * g_PipSize;
|
||||
double tp = ask + Inp_TakeProfitPips * g_PipSize;
|
||||
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
||||
tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
||||
double lot = CalculateLot(Inp_StopLossPips);
|
||||
OpenOrder(ORDER_TYPE_BUY, lot, ask, sl, tp);
|
||||
}
|
||||
else if(shortSignal)
|
||||
{
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
double sl = bid + Inp_StopLossPips * g_PipSize;
|
||||
double tp = bid - Inp_TakeProfitPips * g_PipSize;
|
||||
sl = NormalizeDouble(sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
||||
tp = NormalizeDouble(tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
||||
double lot = CalculateLot(Inp_StopLossPips);
|
||||
OpenOrder(ORDER_TYPE_SELL, lot, bid, sl, tp);
|
||||
}
|
||||
|
||||
// 15. Trailing stop management
|
||||
if(Inp_UseTrailingStop) ManageTrailingStop();
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| IsNewBar — returns true only once per M1 candle |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsNewBar()
|
||||
{
|
||||
datetime barTime = iTime(_Symbol, PERIOD_M1, 0);
|
||||
if(barTime == g_LastBarTime) return false;
|
||||
g_LastBarTime = barTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| IsTradeTime — returns true if current time is in session |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsTradeTime()
|
||||
{
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(TimeCurrent(), dt);
|
||||
int nowMins = dt.hour * 60 + dt.min;
|
||||
int startMin = Inp_StartHour * 60 + Inp_StartMinute;
|
||||
int endMin = Inp_EndHour * 60 + Inp_EndMinute;
|
||||
return (nowMins >= startMin && nowMins < endMin);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| GetCurrentSpreadPips |
|
||||
//+------------------------------------------------------------------+
|
||||
double GetCurrentSpreadPips()
|
||||
{
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
return (ask - bid) / g_PipSize;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| LoadNewsFile — parse news.txt (format: "YYYY.MM.DD HH:MM") |
|
||||
//+------------------------------------------------------------------+
|
||||
void LoadNewsFile()
|
||||
{
|
||||
int fh = FileOpen(Inp_NewsFile, FILE_READ | FILE_TXT | FILE_COMMON);
|
||||
if(fh == INVALID_HANDLE)
|
||||
{
|
||||
Print("News file '", Inp_NewsFile, "' not found — news filter skipped.");
|
||||
return;
|
||||
}
|
||||
g_NewsCount = 0;
|
||||
ArrayResize(g_NewsTimes, 0);
|
||||
while(!FileIsEnding(fh))
|
||||
{
|
||||
string line = FileReadString(fh);
|
||||
StringTrimRight(line);
|
||||
StringTrimLeft(line);
|
||||
if(StringLen(line) < 16) continue;
|
||||
datetime t = StringToTime(line);
|
||||
if(t > 0)
|
||||
{
|
||||
ArrayResize(g_NewsTimes, g_NewsCount + 1);
|
||||
g_NewsTimes[g_NewsCount++] = t;
|
||||
}
|
||||
}
|
||||
FileClose(fh);
|
||||
Print("News filter loaded: ", g_NewsCount, " events from ", Inp_NewsFile);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| IsNewsTime — returns true if within news window |
|
||||
//+------------------------------------------------------------------+
|
||||
bool IsNewsTime()
|
||||
{
|
||||
if(g_NewsCount == 0) return false;
|
||||
datetime now = TimeCurrent();
|
||||
int windowSec = g_NewsMinutes * 60;
|
||||
for(int i = 0; i < g_NewsCount; i++)
|
||||
{
|
||||
if(MathAbs((double)(now - g_NewsTimes[i])) <= windowSec)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| CalculateLot — risk-based or fixed |
|
||||
//+------------------------------------------------------------------+
|
||||
double CalculateLot(int slPips)
|
||||
{
|
||||
if(Inp_UseFixedLot) return NormaliseLot(Inp_FixedLot);
|
||||
|
||||
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
double riskAmt = balance * Inp_RiskPercent / 100.0;
|
||||
double tickVal = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
||||
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
||||
|
||||
// pip value per lot in account currency
|
||||
double pipValuePerLot = (g_PipSize / tickSize) * tickVal;
|
||||
if(pipValuePerLot <= 0) return NormaliseLot(Inp_FixedLot);
|
||||
|
||||
double rawLot = riskAmt / ((double)slPips * pipValuePerLot);
|
||||
return NormaliseLot(rawLot);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| NormaliseLot — round to lot step, clamp to min/max |
|
||||
//+------------------------------------------------------------------+
|
||||
double NormaliseLot(double lot)
|
||||
{
|
||||
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
||||
double lotMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
||||
double lotMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
||||
lot = MathFloor(lot / lotStep) * lotStep;
|
||||
lot = MathMax(lot, lotMin);
|
||||
lot = MathMin(lot, lotMax);
|
||||
return NormalizeDouble(lot, 2);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| OpenOrder — send with retry loop |
|
||||
//+------------------------------------------------------------------+
|
||||
void OpenOrder(ENUM_ORDER_TYPE type, double lot, double price,
|
||||
double sl, double tp)
|
||||
{
|
||||
for(int attempt = 1; attempt <= Inp_MaxRetries; attempt++)
|
||||
{
|
||||
bool sent = false;
|
||||
if(type == ORDER_TYPE_BUY)
|
||||
sent = g_Trade.Buy(lot, _Symbol, price, sl, tp, Inp_TradeComment);
|
||||
else
|
||||
sent = g_Trade.Sell(lot, _Symbol, price, sl, tp, Inp_TradeComment);
|
||||
|
||||
if(sent)
|
||||
{
|
||||
ulong ticket = g_Trade.ResultOrder();
|
||||
string dir = (type == ORDER_TYPE_BUY) ? "BUY" : "SELL";
|
||||
Print(TimeToString(TimeCurrent()), " | ORDER OPENED | ", dir,
|
||||
" | Ticket=", ticket,
|
||||
" | Lot=", DoubleToString(lot, 2),
|
||||
" | Price=", DoubleToString(price, _Digits),
|
||||
" | SL=", DoubleToString(sl, _Digits),
|
||||
" | TP=", DoubleToString(tp, _Digits));
|
||||
return;
|
||||
}
|
||||
|
||||
int err = GetLastError();
|
||||
Print("Order attempt ", attempt, " failed. Error=", err,
|
||||
" | Retcode=", g_Trade.ResultRetcode());
|
||||
|
||||
// Don't retry on hard errors
|
||||
if(err == ERR_MARKET_CLOSED || err == ERR_TRADE_DISABLED) break;
|
||||
Sleep(500);
|
||||
}
|
||||
Print("Order FAILED after ", Inp_MaxRetries, " retries.");
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| HasPositionInDirection |
|
||||
//+------------------------------------------------------------------+
|
||||
bool HasPositionInDirection(ENUM_POSITION_TYPE dir)
|
||||
{
|
||||
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
if(g_Position.SelectByIndex(i))
|
||||
{
|
||||
if(g_Position.Magic() == Inp_MagicNumber &&
|
||||
g_Position.Symbol() == _Symbol &&
|
||||
g_Position.PositionType() == dir)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| ManageTrailingStop |
|
||||
//+------------------------------------------------------------------+
|
||||
void ManageTrailingStop()
|
||||
{
|
||||
double trailStart = Inp_TrailingStartPips * g_PipSize;
|
||||
double trailStep = Inp_TrailingStepPips * g_PipSize;
|
||||
|
||||
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
if(!g_Position.SelectByIndex(i)) continue;
|
||||
if(g_Position.Magic() != Inp_MagicNumber) continue;
|
||||
if(g_Position.Symbol() != _Symbol) continue;
|
||||
|
||||
double sl = g_Position.StopLoss();
|
||||
double openPx = g_Position.PriceOpen();
|
||||
double digits = (double)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
|
||||
|
||||
if(g_Position.PositionType() == POSITION_TYPE_BUY)
|
||||
{
|
||||
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
||||
double profit = bid - openPx;
|
||||
if(profit >= trailStart)
|
||||
{
|
||||
double newSL = NormalizeDouble(bid - trailStep, (int)digits);
|
||||
if(newSL > sl + _Point)
|
||||
g_Trade.PositionModify(g_Position.Ticket(), newSL,
|
||||
g_Position.TakeProfit());
|
||||
}
|
||||
}
|
||||
else // SELL
|
||||
{
|
||||
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
||||
double profit = openPx - ask;
|
||||
if(profit >= trailStart)
|
||||
{
|
||||
double newSL = NormalizeDouble(ask + trailStep, (int)digits);
|
||||
if(newSL < sl - _Point || sl == 0)
|
||||
g_Trade.PositionModify(g_Position.Ticket(), newSL,
|
||||
g_Position.TakeProfit());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| CloseAllPositions |
|
||||
//+------------------------------------------------------------------+
|
||||
void CloseAllPositions()
|
||||
{
|
||||
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||||
{
|
||||
if(g_Position.SelectByIndex(i))
|
||||
{
|
||||
if(g_Position.Magic() == Inp_MagicNumber &&
|
||||
g_Position.Symbol() == _Symbol)
|
||||
{
|
||||
g_Trade.PositionClose(g_Position.Ticket());
|
||||
Print(TimeToString(TimeCurrent()),
|
||||
" | EMERGENCY CLOSE | Ticket=", g_Position.Ticket(),
|
||||
" | Reason: Drawdown limit");
|
||||
g_LastTradeCloseTime = TimeCurrent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| GetDayStart — midnight of given datetime |
|
||||
//+------------------------------------------------------------------+
|
||||
datetime GetDayStart(datetime t)
|
||||
{
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(t, dt);
|
||||
dt.hour = 0; dt.min = 0; dt.sec = 0;
|
||||
return StructToTime(dt);
|
||||
}
|
||||
|
||||
//+------------------------------------------------------------------+
|
||||
//| OnTradeTransaction — track close time for cooldown |
|
||||
//+------------------------------------------------------------------+
|
||||
void OnTradeTransaction(const MqlTradeTransaction &trans,
|
||||
const MqlTradeRequest &request,
|
||||
const MqlTradeResult &result)
|
||||
{
|
||||
if(trans.type == TRADE_TRANSACTION_DEAL_ADD)
|
||||
{
|
||||
if(trans.deal_type == DEAL_TYPE_BUY || trans.deal_type == DEAL_TYPE_SELL)
|
||||
{
|
||||
// Check if this deal closes a position
|
||||
if((ENUM_DEAL_ENTRY)HistoryDealGetInteger(trans.deal, DEAL_ENTRY)
|
||||
== DEAL_ENTRY_OUT)
|
||||
{
|
||||
if((long)HistoryDealGetInteger(trans.deal, DEAL_MAGIC)
|
||||
== Inp_MagicNumber)
|
||||
{
|
||||
double profit = HistoryDealGetDouble(trans.deal, DEAL_PROFIT);
|
||||
Print(TimeToString(TimeCurrent()),
|
||||
" | POSITION CLOSED | Deal=", trans.deal,
|
||||
" | Profit=", DoubleToString(profit, 2));
|
||||
g_LastTradeCloseTime = TimeCurrent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
//+------------------------------------------------------------------+
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
GENESIS Backtesting Engine v1
|
||||
Uses yfinance for 1-year H1 historical data.
|
||||
Runs the exact same EMA/RSI/ATR strategy as the live system.
|
||||
Outputs performance report + sends results to Telegram.
|
||||
"""
|
||||
import os, json, requests
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
TG_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
TG_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "")
|
||||
|
||||
# Symbol mapping: MT5 broker suffix → Yahoo Finance ticker
|
||||
SYMBOL_MAP = {
|
||||
"EURUSDxx": "EURUSD=X",
|
||||
"XAUUSDxx": "GC=F",
|
||||
"GBPUSDxx": "GBPUSD=X",
|
||||
"GBPJPYxx": "GBPJPY=X",
|
||||
"USDJPYxx": "USDJPY=X",
|
||||
"EURUSD": "EURUSD=X",
|
||||
"XAUUSD": "GC=F",
|
||||
"GBPUSD": "GBPUSD=X",
|
||||
"GBPJPY": "GBPJPY=X",
|
||||
"USDJPY": "USDJPY=X",
|
||||
}
|
||||
|
||||
def tg(msg):
|
||||
try:
|
||||
requests.post(f"https://api.telegram.org/bot{TG_TOKEN}/sendMessage",
|
||||
json={"chat_id": TG_CHAT_ID, "text": msg, "parse_mode": "Markdown"}, timeout=15)
|
||||
except: pass
|
||||
|
||||
def backtest_symbol(mt5_sym, yf_sym):
|
||||
import yfinance as yf
|
||||
import pandas as pd
|
||||
import ta
|
||||
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Backtesting: {mt5_sym} ({yf_sym})")
|
||||
|
||||
df = yf.download(yf_sym, period="1y", interval="1h", progress=False, auto_adjust=True)
|
||||
if df.empty or len(df) < 100:
|
||||
print(f" Insufficient data: {len(df)} bars")
|
||||
return None
|
||||
|
||||
# Flatten multi-index if present
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = df.columns.get_level_values(0)
|
||||
df.columns = [c.lower() for c in df.columns]
|
||||
df = df.rename(columns={"adj close": "close"})
|
||||
df = df.dropna()
|
||||
|
||||
# Calculate indicators using 'ta' instead of 'pandas-ta'
|
||||
df["ema20"] = ta.trend.ema_indicator(df["close"], window=20)
|
||||
df["ema50"] = ta.trend.ema_indicator(df["close"], window=50)
|
||||
df["rsi"] = ta.momentum.rsi(df["close"], window=14)
|
||||
df["atr"] = ta.volatility.average_true_range(df["high"], df["low"], df["close"], window=14)
|
||||
df = df.dropna()
|
||||
|
||||
print(f" Downloaded {len(df)} H1 bars | {df.index[0].date()} → {df.index[-1].date()}")
|
||||
|
||||
# Strategy: EMA20 > EMA50 + RSI < 45 → Buy | EMA20 < EMA50 + RSI > 55 → Sell
|
||||
# SL = 2x ATR below/above entry | TP = 4x ATR (2:1 R:R minimum)
|
||||
trades = []
|
||||
in_trade = False
|
||||
entry_price = sl = tp = direction = entry_idx = None
|
||||
|
||||
for i in range(1, len(df)):
|
||||
row = df.iloc[i]
|
||||
prev = df.iloc[i-1]
|
||||
spread_est = row["atr"] * 0.05 # rough spread estimate
|
||||
|
||||
if not in_trade:
|
||||
# Entry signals
|
||||
if row["ema20"] > row["ema50"] and prev["rsi"] < 45 and row["rsi"] > 45:
|
||||
direction = "Buy"
|
||||
entry_price = row["close"] + spread_est
|
||||
sl = round(entry_price - 2.0 * row["atr"], 5)
|
||||
tp = round(entry_price + 4.0 * row["atr"], 5)
|
||||
in_trade = True
|
||||
entry_idx = i
|
||||
elif row["ema20"] < row["ema50"] and prev["rsi"] > 55 and row["rsi"] < 55:
|
||||
direction = "Sell"
|
||||
entry_price = row["close"] - spread_est
|
||||
sl = round(entry_price + 2.0 * row["atr"], 5)
|
||||
tp = round(entry_price - 4.0 * row["atr"], 5)
|
||||
in_trade = True
|
||||
entry_idx = i
|
||||
else:
|
||||
# Check SL/TP hit
|
||||
high, low = row["high"], row["low"]
|
||||
result = None
|
||||
if direction == "Buy":
|
||||
if low <= sl:
|
||||
result = "loss"; exit_price = sl
|
||||
elif high >= tp:
|
||||
result = "win"; exit_price = tp
|
||||
else:
|
||||
if high >= sl:
|
||||
result = "loss"; exit_price = sl
|
||||
elif low <= tp:
|
||||
result = "win"; exit_price = tp
|
||||
|
||||
# Max hold: 48 bars (2 days)
|
||||
if result is None and (i - entry_idx) >= 48:
|
||||
result = "timeout"; exit_price = row["close"]
|
||||
|
||||
if result:
|
||||
diff = (exit_price - entry_price) if direction == "Buy" else (entry_price - exit_price)
|
||||
if "JPY" in mt5_sym:
|
||||
pips = round(diff * 100.0, 1)
|
||||
elif "XAU" in mt5_sym or "GC" in yf_sym:
|
||||
pips = round(diff, 2)
|
||||
else:
|
||||
pips = round(diff * 10000.0, 1)
|
||||
|
||||
trades.append({
|
||||
"direction": direction,
|
||||
"entry": entry_price,
|
||||
"exit": exit_price,
|
||||
"result": result,
|
||||
"pips": pips,
|
||||
"bars_held": i - entry_idx,
|
||||
"date": df.index[entry_idx].strftime("%Y-%m-%d"),
|
||||
})
|
||||
in_trade = False
|
||||
|
||||
if not trades:
|
||||
print(" No trades generated")
|
||||
return None
|
||||
|
||||
wins = [t for t in trades if t["result"] == "win"]
|
||||
losses = [t for t in trades if t["result"] == "loss"]
|
||||
timeouts= [t for t in trades if t["result"] == "timeout"]
|
||||
total_pips = sum(t["pips"] for t in trades)
|
||||
win_pips = sum(t["pips"] for t in wins)
|
||||
loss_pips = sum(t["pips"] for t in losses)
|
||||
winrate = len(wins) / len(trades) * 100
|
||||
|
||||
# Profit factor
|
||||
pf = round(abs(win_pips / loss_pips), 2) if loss_pips != 0 else float("inf")
|
||||
|
||||
# Max drawdown (running pip balance)
|
||||
running = 0; peak = 0; max_dd = 0
|
||||
for t in trades:
|
||||
running += t["pips"]
|
||||
if running > peak: peak = running
|
||||
dd = peak - running
|
||||
if dd > max_dd: max_dd = dd
|
||||
|
||||
result = {
|
||||
"symbol": mt5_sym,
|
||||
"yf": yf_sym,
|
||||
"total_trades": len(trades),
|
||||
"wins": len(wins),
|
||||
"losses": len(losses),
|
||||
"timeouts": len(timeouts),
|
||||
"win_rate": round(winrate, 1),
|
||||
"total_pips": round(total_pips, 1),
|
||||
"profit_factor": pf,
|
||||
"max_drawdown_pips": round(max_dd, 1),
|
||||
"avg_hold_bars": round(sum(t["bars_held"] for t in trades) / len(trades), 1),
|
||||
}
|
||||
|
||||
print(f" Trades: {result['total_trades']} | W:{result['wins']} L:{result['losses']} T:{result['timeouts']}")
|
||||
print(f" Win rate: {result['win_rate']}% | Total pips: {result['total_pips']}")
|
||||
print(f" Profit factor: {result['profit_factor']} | Max DD: {result['max_drawdown_pips']} pips")
|
||||
return result
|
||||
|
||||
def main():
|
||||
tg("🔬 *GENESIS Backtest Starting*\nRunning 1-year H1 backtest on 5 symbols using EMA20/50 + RSI + ATR strategy...\n_This will take ~60 seconds._")
|
||||
|
||||
results = []
|
||||
for mt5_sym, yf_sym in SYMBOL_MAP.items():
|
||||
try:
|
||||
r = backtest_symbol(mt5_sym, yf_sym)
|
||||
if r:
|
||||
results.append(r)
|
||||
except Exception as e:
|
||||
print(f" ERROR {mt5_sym}: {e}")
|
||||
|
||||
if not results:
|
||||
tg("❌ *Backtest Failed*: No results generated.")
|
||||
return
|
||||
|
||||
# Save results
|
||||
try:
|
||||
out_path = Path("/var/log/hermes/backtest_results.json")
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
out_path.write_text(json.dumps(results, indent=2))
|
||||
print(f"\nResults saved to {out_path}")
|
||||
except Exception as e:
|
||||
print(f"\nCould not write to /var/log/hermes/backtest_results.json ({e}). Falling back to local workspace.")
|
||||
out_path = Path("./backtest_results.json")
|
||||
out_path.write_text(json.dumps(results, indent=2))
|
||||
print(f"Results saved to {out_path.resolve()}")
|
||||
|
||||
# Build Telegram report
|
||||
report = "📊 *GENESIS Backtest Results* (1 Year H1)\n"
|
||||
report += "Strategy: EMA20/50 crossover + RSI + 2x ATR SL + 4x ATR TP\n\n"
|
||||
|
||||
overall_trades = sum(r["total_trades"] for r in results)
|
||||
overall_wins = sum(r["wins"] for r in results)
|
||||
overall_wr = round(overall_wins / overall_trades * 100, 1) if overall_trades else 0
|
||||
|
||||
for r in sorted(results, key=lambda x: x["win_rate"], reverse=True):
|
||||
emoji = "✅" if r["win_rate"] >= 50 and r["profit_factor"] >= 1.0 else "⚠️" if r["win_rate"] >= 45 else "❌"
|
||||
report += f"{emoji} *{r['symbol']}*\n"
|
||||
report += f" {r['wins']}W/{r['losses']}L | WR: {r['win_rate']}% | PF: {r['profit_factor']}\n"
|
||||
report += f" Pips: {r['total_pips']} | Max DD: {r['max_drawdown_pips']} pips\n\n"
|
||||
|
||||
report += f"📈 *Overall:* {overall_wins}/{overall_trades} trades won ({overall_wr}%)\n"
|
||||
|
||||
# Strategy verdict
|
||||
viable = [r for r in results if r["win_rate"] >= 50 and r["profit_factor"] >= 1.2]
|
||||
if viable:
|
||||
report += f"\n✅ *Viable symbols*: {', '.join(r['symbol'] for r in viable)}\n"
|
||||
report += "_These pairs have >50% win rate and >1.2 profit factor historically._"
|
||||
else:
|
||||
report += "\n⚠️ *No symbol meets viability criteria (>50% WR + >1.2 PF)*\n"
|
||||
report += "_Strategy needs tuning before live deployment._"
|
||||
|
||||
print("\n" + report)
|
||||
tg(report)
|
||||
|
||||
# Save markdown report
|
||||
md = f"# GENESIS Backtest Report\n*Generated: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M')} UTC*\n\n"
|
||||
md += report.replace("*", "**").replace("_", "*")
|
||||
try:
|
||||
report_path = Path("/var/log/hermes/backtest_report.md")
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
report_path.write_text(md)
|
||||
print(f"Report saved to {report_path}")
|
||||
except Exception as e:
|
||||
print(f"Could not write to /var/log/hermes/backtest_report.md ({e}). Falling back to local workspace.")
|
||||
report_path = Path("./backtest_report.md")
|
||||
report_path.write_text(md)
|
||||
print(f"Report saved to {report_path.resolve()}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user