605faf5310
Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
489 lines
14 KiB
Plaintext
489 lines
14 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| RSIScalpingAdaptiveXAUUSD.mq5 |
|
|
//| RSI Scalping with monthly walk-forward parameter adaptation |
|
|
//| Backtests prior calendar month on each new month, applies best |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2025, MetaQuotes Ltd."
|
|
#property link "https://www.mql5.com"
|
|
#property version "2.00"
|
|
#property description "XAUUSD RSI Scalping — monthly walk-forward adaptive params"
|
|
|
|
#include <Trade\Trade.mqh>
|
|
#include "MagicNumberHelpers.mqh"
|
|
#include "RSIScalpingAdaptiveOptimizer.mqh"
|
|
|
|
//--- Fallback defaults (XAUUSD 123.set baseline)
|
|
input group "=== Fallback / seed parameters ==="
|
|
input ENUM_TIMEFRAMES TimeFrame = PERIOD_H1;
|
|
input int RSI_Period = 17;
|
|
input ENUM_APPLIED_PRICE RSI_Applied_Price = PRICE_CLOSE;
|
|
input double RSI_Overbought = 6.0;
|
|
input double RSI_Oversold = 74.0;
|
|
input double RSI_Target_Buy = 79.0;
|
|
input double RSI_Target_Sell = 24.0;
|
|
input int BarsToWait = 12;
|
|
|
|
input group "=== Execution ==="
|
|
input double LotSize = 0.1;
|
|
input int MagicNumber = 129102315;
|
|
input int Slippage = 3;
|
|
|
|
input group "=== Adaptive walk-forward ==="
|
|
input bool EnableAdaptive = true;
|
|
input int OptimizationCheckSeconds = 3600; // Timer interval for new-month check
|
|
input int MinTradesForSelection = 8;
|
|
input int MaxCombinations = 600;
|
|
input double BacktestInitialBalance = 10000.0;
|
|
input double ScoreWeightSharpe = 0.35;
|
|
input double ScoreWeightNetProfit = 0.25;
|
|
input double ScoreWeightProfitFactor = 0.15;
|
|
input double ScoreWeightMaxDD = 0.10;
|
|
|
|
input group "=== XAUUSD search ranges ==="
|
|
input int Search_RSI_Period_Min = 12;
|
|
input int Search_RSI_Period_Max = 18;
|
|
input int Search_RSI_Period_Step = 2;
|
|
input double Search_RSI_Overbought_Min = 65.0;
|
|
input double Search_RSI_Overbought_Max = 77.0;
|
|
input double Search_RSI_Overbought_Step = 3.0;
|
|
input double Search_RSI_Oversold_Min = 50.0;
|
|
input double Search_RSI_Oversold_Max = 63.0;
|
|
input double Search_RSI_Oversold_Step = 3.0;
|
|
input double Search_RSI_Target_Buy_Min = 75.0;
|
|
input double Search_RSI_Target_Buy_Max = 86.0;
|
|
input double Search_RSI_Target_Buy_Step = 3.0;
|
|
input double Search_RSI_Target_Sell_Min = 50.0;
|
|
input double Search_RSI_Target_Sell_Max = 63.0;
|
|
input double Search_RSI_Target_Sell_Step = 3.0;
|
|
input int Search_BarsToWait_Min = 1;
|
|
input int Search_BarsToWait_Max = 4;
|
|
input int Search_BarsToWait_Step = 1;
|
|
|
|
CTrade trade;
|
|
CRSIAdaptiveOptimizer g_optimizer;
|
|
|
|
int rsi_handle = INVALID_HANDLE;
|
|
double rsi_buffer[];
|
|
double rsi_prev, rsi_current, rsi_two_bars_ago;
|
|
|
|
bool position_open = false;
|
|
ulong position_ticket = 0;
|
|
ENUM_POSITION_TYPE current_position_type = POSITION_TYPE_BUY;
|
|
datetime last_bar_time = 0;
|
|
bool rsi_against_position = false;
|
|
int bars_against_count = 0;
|
|
|
|
RSIAdaptiveParams g_active;
|
|
RSIAdaptiveMetrics g_last_metrics;
|
|
int g_applied_month_key = 0;
|
|
bool g_optimization_done = false;
|
|
bool g_optimizing = false;
|
|
string g_status_line = "";
|
|
|
|
//+------------------------------------------------------------------+
|
|
void SyncOpenPosition()
|
|
{
|
|
if(!PositionExistsByMagic(_Symbol, MagicNumber))
|
|
{
|
|
position_open = false;
|
|
position_ticket = 0;
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
return;
|
|
}
|
|
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol)
|
|
continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicNumber)
|
|
continue;
|
|
|
|
position_ticket = ticket;
|
|
position_open = true;
|
|
current_position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool IsStrategyTester()
|
|
{
|
|
return (bool)MQLInfoInteger(MQL_TESTER);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
RSIAdaptiveParams BuildFallbackParams()
|
|
{
|
|
RSIAdaptiveParams p;
|
|
p.timeframe = TimeFrame;
|
|
p.rsi_period = RSI_Period;
|
|
p.rsi_overbought = RSI_Overbought;
|
|
p.rsi_oversold = RSI_Oversold;
|
|
p.rsi_target_buy = RSI_Target_Buy;
|
|
p.rsi_target_sell = RSI_Target_Sell;
|
|
p.bars_to_wait = BarsToWait;
|
|
return p;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
RSIAdaptiveSearchConfig BuildSearchConfig()
|
|
{
|
|
RSIAdaptiveSearchConfig cfg;
|
|
cfg.timeframe = TimeFrame;
|
|
cfg.rsi_period_min = Search_RSI_Period_Min;
|
|
cfg.rsi_period_max = Search_RSI_Period_Max;
|
|
cfg.rsi_period_step = MathMax(1, Search_RSI_Period_Step);
|
|
cfg.rsi_overbought_min = Search_RSI_Overbought_Min;
|
|
cfg.rsi_overbought_max = Search_RSI_Overbought_Max;
|
|
cfg.rsi_overbought_step = Search_RSI_Overbought_Step;
|
|
cfg.rsi_oversold_min = Search_RSI_Oversold_Min;
|
|
cfg.rsi_oversold_max = Search_RSI_Oversold_Max;
|
|
cfg.rsi_oversold_step = Search_RSI_Oversold_Step;
|
|
cfg.rsi_target_buy_min = Search_RSI_Target_Buy_Min;
|
|
cfg.rsi_target_buy_max = Search_RSI_Target_Buy_Max;
|
|
cfg.rsi_target_buy_step = Search_RSI_Target_Buy_Step;
|
|
cfg.rsi_target_sell_min = Search_RSI_Target_Sell_Min;
|
|
cfg.rsi_target_sell_max = Search_RSI_Target_Sell_Max;
|
|
cfg.rsi_target_sell_step = Search_RSI_Target_Sell_Step;
|
|
cfg.bars_to_wait_min = Search_BarsToWait_Min;
|
|
cfg.bars_to_wait_max = Search_BarsToWait_Max;
|
|
cfg.bars_to_wait_step = MathMax(1, Search_BarsToWait_Step);
|
|
cfg.min_trades = MinTradesForSelection;
|
|
cfg.lot_size = LotSize;
|
|
cfg.initial_balance = BacktestInitialBalance;
|
|
cfg.slippage_points = Slippage;
|
|
cfg.weight_sharpe = ScoreWeightSharpe;
|
|
cfg.weight_net_profit = ScoreWeightNetProfit;
|
|
cfg.weight_profit_factor = ScoreWeightProfitFactor;
|
|
cfg.weight_max_dd = ScoreWeightMaxDD;
|
|
cfg.max_combinations = MaxCombinations;
|
|
return cfg;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool RecreateRsiHandle()
|
|
{
|
|
if(rsi_handle != INVALID_HANDLE)
|
|
IndicatorRelease(rsi_handle);
|
|
|
|
rsi_handle = iRSI(_Symbol, g_active.timeframe, g_active.rsi_period, RSI_Applied_Price);
|
|
if(rsi_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: failed to create RSI handle for ", g_active.ToString());
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void UpdateStatusComment()
|
|
{
|
|
g_status_line = StringFormat(
|
|
"RSI Adaptive XAUUSD | month=%d | %s\n"
|
|
"BT: net=$%.0f sharpe=%.2f PF=%.2f trades=%d DD=%.1f%% | combos=%d",
|
|
g_applied_month_key,
|
|
g_active.ToString(),
|
|
g_last_metrics.net_profit,
|
|
g_last_metrics.sharpe,
|
|
g_last_metrics.profit_factor,
|
|
g_last_metrics.total_trades,
|
|
g_last_metrics.max_drawdown_pct,
|
|
g_optimizer.CombosTested()
|
|
);
|
|
Comment(g_status_line);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool RunMonthlyOptimization(const string reason)
|
|
{
|
|
if(g_optimizing)
|
|
return true;
|
|
|
|
g_optimizing = true;
|
|
RSIAdaptiveParams fallback = BuildFallbackParams();
|
|
RSIAdaptiveSearchConfig cfg = BuildSearchConfig();
|
|
|
|
datetime opt_start, opt_end;
|
|
CRSIAdaptiveOptimizer::PreviousCalendarMonth(TimeCurrent(), opt_start, opt_end);
|
|
|
|
PrintFormat("[Adaptive] %s — optimizing on prior month (%s to %s)",
|
|
reason,
|
|
TimeToString(opt_start, TIME_DATE),
|
|
TimeToString(opt_end, TIME_DATE));
|
|
|
|
RSIAdaptiveParams best;
|
|
RSIAdaptiveMetrics best_metrics;
|
|
const bool ok = g_optimizer.Optimize(_Symbol, opt_start, opt_end, fallback, cfg, best, best_metrics);
|
|
|
|
if(ok)
|
|
{
|
|
g_active = best;
|
|
g_last_metrics = best_metrics;
|
|
}
|
|
else
|
|
{
|
|
Print("[Adaptive] Optimization found no valid combo — keeping fallback params");
|
|
g_active = fallback;
|
|
g_last_metrics = best_metrics;
|
|
}
|
|
|
|
g_applied_month_key = CRSIAdaptiveOptimizer::MonthKey(TimeCurrent());
|
|
g_optimization_done = true;
|
|
|
|
if(!RecreateRsiHandle())
|
|
{
|
|
g_optimizing = false;
|
|
return false;
|
|
}
|
|
|
|
last_bar_time = 0;
|
|
g_optimizing = false;
|
|
UpdateStatusComment();
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void CheckMonthlyOptimizationSchedule(const string reason)
|
|
{
|
|
if(!EnableAdaptive || IsStrategyTester())
|
|
return;
|
|
|
|
const int month_key = CRSIAdaptiveOptimizer::MonthKey(TimeCurrent());
|
|
if(!g_optimization_done || month_key != g_applied_month_key)
|
|
RunMonthlyOptimization(reason);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
trade.SetExpertMagicNumber(MagicNumber);
|
|
trade.SetDeviationInPoints(Slippage);
|
|
trade.SetTypeFilling(ORDER_FILLING_FOK);
|
|
ArraySetAsSeries(rsi_buffer, true);
|
|
|
|
g_active = BuildFallbackParams();
|
|
if(!RecreateRsiHandle())
|
|
return INIT_FAILED;
|
|
|
|
EventSetTimer(OptimizationCheckSeconds);
|
|
|
|
// Strategy Tester / Optimization: use Inputs directly — no walk-forward grid search
|
|
if(IsStrategyTester() || !EnableAdaptive)
|
|
{
|
|
g_active = BuildFallbackParams();
|
|
g_optimization_done = true;
|
|
g_applied_month_key = CRSIAdaptiveOptimizer::MonthKey(TimeCurrent());
|
|
if(!RecreateRsiHandle())
|
|
return INIT_FAILED;
|
|
UpdateStatusComment();
|
|
SyncOpenPosition();
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
if(!RunMonthlyOptimization("OnInit"))
|
|
return INIT_FAILED;
|
|
|
|
SyncOpenPosition();
|
|
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
EventKillTimer();
|
|
if(rsi_handle != INVALID_HANDLE)
|
|
IndicatorRelease(rsi_handle);
|
|
Comment("");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void OnTimer()
|
|
{
|
|
CheckMonthlyOptimizationSchedule("OnTimer");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
if(Bars(_Symbol, g_active.timeframe) < g_active.rsi_period + 2)
|
|
return;
|
|
|
|
datetime current_bar_time = iTime(_Symbol, g_active.timeframe, 0);
|
|
if(current_bar_time == last_bar_time)
|
|
return;
|
|
|
|
last_bar_time = current_bar_time;
|
|
|
|
if(!UpdateRSI())
|
|
return;
|
|
|
|
CheckExistingPosition();
|
|
|
|
if(!position_open && !PositionExistsByMagic(_Symbol, MagicNumber))
|
|
CheckEntrySignals();
|
|
|
|
UpdateStatusComment();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
bool UpdateRSI()
|
|
{
|
|
if(CopyBuffer(rsi_handle, 0, 0, 3, rsi_buffer) < 3)
|
|
return false;
|
|
|
|
rsi_current = rsi_buffer[0];
|
|
rsi_prev = rsi_buffer[1];
|
|
rsi_two_bars_ago = rsi_buffer[2];
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void CheckExistingPosition()
|
|
{
|
|
if(!position_open)
|
|
SyncOpenPosition();
|
|
|
|
if(!position_open)
|
|
return;
|
|
|
|
if(!PositionSelectByTicketSymbolAndMagic(position_ticket, _Symbol, MagicNumber))
|
|
{
|
|
position_open = false;
|
|
position_ticket = 0;
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
return;
|
|
}
|
|
|
|
if(current_position_type == POSITION_TYPE_BUY)
|
|
{
|
|
if(rsi_current < g_active.rsi_oversold)
|
|
{
|
|
if(!rsi_against_position)
|
|
{
|
|
rsi_against_position = true;
|
|
bars_against_count = 1;
|
|
}
|
|
else
|
|
bars_against_count++;
|
|
|
|
if(bars_against_count >= g_active.bars_to_wait)
|
|
{
|
|
ClosePosition();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(rsi_against_position)
|
|
{
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
}
|
|
if(rsi_current >= g_active.rsi_target_buy)
|
|
ClosePosition();
|
|
}
|
|
}
|
|
else if(current_position_type == POSITION_TYPE_SELL)
|
|
{
|
|
if(rsi_current > g_active.rsi_overbought)
|
|
{
|
|
if(!rsi_against_position)
|
|
{
|
|
rsi_against_position = true;
|
|
bars_against_count = 1;
|
|
}
|
|
else
|
|
bars_against_count++;
|
|
|
|
if(bars_against_count >= g_active.bars_to_wait)
|
|
{
|
|
ClosePosition();
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(rsi_against_position)
|
|
{
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
}
|
|
if(rsi_current <= g_active.rsi_target_sell)
|
|
ClosePosition();
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void CheckEntrySignals()
|
|
{
|
|
if(rsi_two_bars_ago <= g_active.rsi_oversold && rsi_prev > g_active.rsi_oversold)
|
|
OpenBuyPosition();
|
|
|
|
if(rsi_two_bars_ago >= g_active.rsi_overbought && rsi_prev < g_active.rsi_overbought)
|
|
OpenSellPosition();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void OpenBuyPosition()
|
|
{
|
|
if(PositionExistsByMagic(_Symbol, MagicNumber))
|
|
return;
|
|
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
if(trade.Buy(LotSize, _Symbol, ask, 0, 0, "RSI Adaptive Buy"))
|
|
{
|
|
ulong new_ticket = trade.ResultOrder();
|
|
if(new_ticket > 0 && PositionSelectByTicketSymbolAndMagic(new_ticket, _Symbol, MagicNumber))
|
|
{
|
|
position_ticket = new_ticket;
|
|
position_open = true;
|
|
current_position_type = POSITION_TYPE_BUY;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void OpenSellPosition()
|
|
{
|
|
if(PositionExistsByMagic(_Symbol, MagicNumber))
|
|
return;
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
if(trade.Sell(LotSize, _Symbol, bid, 0, 0, "RSI Adaptive Sell"))
|
|
{
|
|
ulong new_ticket = trade.ResultOrder();
|
|
if(new_ticket > 0 && PositionSelectByTicketSymbolAndMagic(new_ticket, _Symbol, MagicNumber))
|
|
{
|
|
position_ticket = new_ticket;
|
|
position_open = true;
|
|
current_position_type = POSITION_TYPE_SELL;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
void ClosePosition()
|
|
{
|
|
if(ClosePositionByMagic(trade, _Symbol, MagicNumber))
|
|
{
|
|
position_open = false;
|
|
position_ticket = 0;
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
}
|
|
else
|
|
{
|
|
position_open = false;
|
|
position_ticket = 0;
|
|
rsi_against_position = false;
|
|
bars_against_count = 0;
|
|
}
|
|
}
|