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>
533 lines
18 KiB
Plaintext
533 lines
18 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| RSIScalpingAdaptiveOptimizer.mqh |
|
|
//| Walk-forward: backtest prior month, pick best params for next |
|
|
//+------------------------------------------------------------------+
|
|
|
|
//+------------------------------------------------------------------+
|
|
struct RSIAdaptiveParams
|
|
{
|
|
ENUM_TIMEFRAMES timeframe;
|
|
int rsi_period;
|
|
double rsi_overbought;
|
|
double rsi_oversold;
|
|
double rsi_target_buy;
|
|
double rsi_target_sell;
|
|
int bars_to_wait;
|
|
|
|
bool IsValid() const
|
|
{
|
|
return (rsi_target_buy > rsi_oversold &&
|
|
rsi_target_sell < rsi_overbought &&
|
|
rsi_period >= 2 &&
|
|
bars_to_wait >= 1);
|
|
}
|
|
|
|
string ToString() const
|
|
{
|
|
return StringFormat(
|
|
"TF=%s RSI=%d OB=%.1f OS=%.1f TB=%.1f TS=%.1f Wait=%d",
|
|
EnumToString(timeframe),
|
|
rsi_period,
|
|
rsi_overbought,
|
|
rsi_oversold,
|
|
rsi_target_buy,
|
|
rsi_target_sell,
|
|
bars_to_wait
|
|
);
|
|
}
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
struct RSIAdaptiveMetrics
|
|
{
|
|
double net_profit;
|
|
int total_trades;
|
|
double win_rate;
|
|
double profit_factor;
|
|
double sharpe;
|
|
double max_drawdown_pct;
|
|
double score;
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
struct RSIAdaptiveSearchConfig
|
|
{
|
|
ENUM_TIMEFRAMES timeframe;
|
|
int rsi_period_min;
|
|
int rsi_period_max;
|
|
int rsi_period_step;
|
|
double rsi_overbought_min;
|
|
double rsi_overbought_max;
|
|
double rsi_overbought_step;
|
|
double rsi_oversold_min;
|
|
double rsi_oversold_max;
|
|
double rsi_oversold_step;
|
|
double rsi_target_buy_min;
|
|
double rsi_target_buy_max;
|
|
double rsi_target_buy_step;
|
|
double rsi_target_sell_min;
|
|
double rsi_target_sell_max;
|
|
double rsi_target_sell_step;
|
|
int bars_to_wait_min;
|
|
int bars_to_wait_max;
|
|
int bars_to_wait_step;
|
|
int min_trades;
|
|
double lot_size;
|
|
double initial_balance;
|
|
int slippage_points;
|
|
double weight_sharpe;
|
|
double weight_net_profit;
|
|
double weight_profit_factor;
|
|
double weight_max_dd;
|
|
int max_combinations;
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
class CRSIAdaptiveOptimizer
|
|
{
|
|
private:
|
|
string m_symbol;
|
|
datetime m_opt_start;
|
|
datetime m_opt_end;
|
|
int m_combos_tested;
|
|
|
|
double FillBuy(const double mid, const double point, const double half_spread, const int slippage_pts) const
|
|
{
|
|
return mid + half_spread + slippage_pts * point;
|
|
}
|
|
|
|
double FillSell(const double mid, const double point, const double half_spread, const int slippage_pts) const
|
|
{
|
|
return mid - half_spread - slippage_pts * point;
|
|
}
|
|
|
|
double CalcTradeProfit(const ENUM_ORDER_TYPE order_type,
|
|
const double volume,
|
|
const double entry,
|
|
const double exit_px) const
|
|
{
|
|
double profit = 0.0;
|
|
if(!OrderCalcProfit(order_type, m_symbol, volume, entry, exit_px, profit))
|
|
return 0.0;
|
|
return profit;
|
|
}
|
|
|
|
int BarsPerYear(const ENUM_TIMEFRAMES tf) const
|
|
{
|
|
switch(tf)
|
|
{
|
|
case PERIOD_M1: return 252 * 24 * 60;
|
|
case PERIOD_M5: return 252 * 24 * 12;
|
|
case PERIOD_M15: return 252 * 24 * 4;
|
|
case PERIOD_M30: return 252 * 24 * 2;
|
|
case PERIOD_H1: return 252 * 24;
|
|
case PERIOD_H4: return 252 * 6;
|
|
case PERIOD_D1: return 252;
|
|
default: return 252 * 24;
|
|
}
|
|
}
|
|
|
|
double ComputeSharpe(const double &equity[], const int count, const ENUM_TIMEFRAMES tf) const
|
|
{
|
|
if(count < 12)
|
|
return 0.0;
|
|
|
|
double sum = 0.0;
|
|
double sum_sq = 0.0;
|
|
int n = 0;
|
|
|
|
for(int i = 1; i < count; i++)
|
|
{
|
|
if(equity[i - 1] <= 0.0)
|
|
continue;
|
|
double r = (equity[i] - equity[i - 1]) / equity[i - 1];
|
|
sum += r;
|
|
sum_sq += r * r;
|
|
n++;
|
|
}
|
|
|
|
if(n < 10)
|
|
return 0.0;
|
|
|
|
double mean = sum / n;
|
|
double var = sum_sq / n - mean * mean;
|
|
if(var <= 0.0)
|
|
return 0.0;
|
|
|
|
double std = MathSqrt(var);
|
|
double scale = MathSqrt((double)BarsPerYear(tf) / (double)n);
|
|
return mean / std * scale;
|
|
}
|
|
|
|
double ComputeScore(const RSIAdaptiveMetrics &m, const RSIAdaptiveSearchConfig &cfg) const
|
|
{
|
|
if(m.total_trades < cfg.min_trades || m.net_profit <= 0.0 || m.profit_factor < 1.05)
|
|
return -1.0e12;
|
|
|
|
double pf = MathMin(m.profit_factor, 4.0) / 4.0;
|
|
return m.sharpe * cfg.weight_sharpe
|
|
+ (m.net_profit / 2000.0) * cfg.weight_net_profit
|
|
+ pf * cfg.weight_profit_factor
|
|
- m.max_drawdown_pct * cfg.weight_max_dd;
|
|
}
|
|
|
|
bool BacktestParams(const RSIAdaptiveParams ¶ms,
|
|
const RSIAdaptiveSearchConfig &cfg,
|
|
RSIAdaptiveMetrics &out) const
|
|
{
|
|
out.net_profit = 0.0;
|
|
out.total_trades = 0;
|
|
out.win_rate = 0.0;
|
|
out.profit_factor = 0.0;
|
|
out.sharpe = 0.0;
|
|
out.max_drawdown_pct = 0.0;
|
|
out.score = -1.0e12;
|
|
|
|
if(!params.IsValid())
|
|
return false;
|
|
|
|
int bt_rsi_handle = iRSI(m_symbol, params.timeframe, params.rsi_period, PRICE_CLOSE);
|
|
if(bt_rsi_handle == INVALID_HANDLE)
|
|
return false;
|
|
|
|
int end_shift = iBarShift(m_symbol, params.timeframe, m_opt_end, false);
|
|
int start_shift = iBarShift(m_symbol, params.timeframe, m_opt_start, false);
|
|
if(end_shift < 0)
|
|
end_shift = 0;
|
|
if(start_shift < 0)
|
|
{
|
|
IndicatorRelease(bt_rsi_handle);
|
|
return false;
|
|
}
|
|
|
|
int bars_count = start_shift - end_shift + 1;
|
|
if(bars_count < params.rsi_period + 5)
|
|
{
|
|
IndicatorRelease(bt_rsi_handle);
|
|
return false;
|
|
}
|
|
|
|
double rsi[];
|
|
double opens[];
|
|
datetime times[];
|
|
ArraySetAsSeries(rsi, false);
|
|
ArraySetAsSeries(opens, false);
|
|
ArraySetAsSeries(times, false);
|
|
|
|
// Copy from oldest bar (start_shift): buffer[0]=oldest, buffer[n-1]=newest
|
|
if(CopyBuffer(bt_rsi_handle, 0, start_shift, bars_count, rsi) < bars_count ||
|
|
CopyOpen(m_symbol, params.timeframe, start_shift, bars_count, opens) < bars_count ||
|
|
CopyTime(m_symbol, params.timeframe, start_shift, bars_count, times) < bars_count)
|
|
{
|
|
IndicatorRelease(bt_rsi_handle);
|
|
return false;
|
|
}
|
|
|
|
IndicatorRelease(bt_rsi_handle);
|
|
|
|
const double point = SymbolInfoDouble(m_symbol, SYMBOL_POINT);
|
|
const long spread_pts = SymbolInfoInteger(m_symbol, SYMBOL_SPREAD);
|
|
const double half_spread = spread_pts * point / 2.0;
|
|
|
|
bool has_position = false;
|
|
ENUM_ORDER_TYPE pos_type = ORDER_TYPE_BUY;
|
|
double entry_px = 0.0;
|
|
bool rsi_against = false;
|
|
int bars_against = 0;
|
|
|
|
double balance = cfg.initial_balance;
|
|
double peak = balance;
|
|
double max_dd_pct = 0.0;
|
|
double gross_profit = 0.0;
|
|
double gross_loss = 0.0;
|
|
int wins = 0;
|
|
|
|
double equity[];
|
|
ArrayResize(equity, bars_count);
|
|
int equity_count = 0;
|
|
|
|
// Chronological loop: index 0 = oldest bar in window (matches Python run_backtest.py)
|
|
for(int i = params.rsi_period + 2; i < bars_count; i++)
|
|
{
|
|
const double sig = rsi[i - 1];
|
|
const double prev = rsi[i - 2];
|
|
const double two = rsi[i - 3];
|
|
const double mid = opens[i];
|
|
|
|
if(has_position)
|
|
{
|
|
if(pos_type == ORDER_TYPE_BUY)
|
|
{
|
|
if(sig < params.rsi_oversold)
|
|
{
|
|
if(!rsi_against)
|
|
{
|
|
rsi_against = true;
|
|
bars_against = 1;
|
|
}
|
|
else
|
|
bars_against++;
|
|
|
|
if(bars_against >= params.bars_to_wait)
|
|
{
|
|
const double exit_px = FillSell(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_BUY, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
has_position = false;
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
if(sig >= params.rsi_target_buy)
|
|
{
|
|
const double exit_px = FillSell(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_BUY, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
has_position = false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(sig > params.rsi_overbought)
|
|
{
|
|
if(!rsi_against)
|
|
{
|
|
rsi_against = true;
|
|
bars_against = 1;
|
|
}
|
|
else
|
|
bars_against++;
|
|
|
|
if(bars_against >= params.bars_to_wait)
|
|
{
|
|
const double exit_px = FillBuy(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_SELL, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
has_position = false;
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
if(sig <= params.rsi_target_sell)
|
|
{
|
|
const double exit_px = FillBuy(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_SELL, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
has_position = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!has_position)
|
|
{
|
|
if(two <= params.rsi_oversold && prev > params.rsi_oversold)
|
|
{
|
|
entry_px = FillBuy(mid, point, half_spread, cfg.slippage_points);
|
|
pos_type = ORDER_TYPE_BUY;
|
|
has_position = true;
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
}
|
|
else if(two >= params.rsi_overbought && prev < params.rsi_overbought)
|
|
{
|
|
entry_px = FillSell(mid, point, half_spread, cfg.slippage_points);
|
|
pos_type = ORDER_TYPE_SELL;
|
|
has_position = true;
|
|
rsi_against = false;
|
|
bars_against = 0;
|
|
}
|
|
}
|
|
|
|
double mark = balance;
|
|
if(has_position)
|
|
{
|
|
const double mark_mid = opens[i];
|
|
if(pos_type == ORDER_TYPE_BUY)
|
|
mark += CalcTradeProfit(ORDER_TYPE_BUY, cfg.lot_size, entry_px, FillSell(mark_mid, point, half_spread, 0));
|
|
else
|
|
mark += CalcTradeProfit(ORDER_TYPE_SELL, cfg.lot_size, entry_px, FillBuy(mark_mid, point, half_spread, 0));
|
|
}
|
|
|
|
if(equity_count < bars_count)
|
|
equity[equity_count++] = mark;
|
|
|
|
if(mark > peak)
|
|
peak = mark;
|
|
if(peak > 0.0)
|
|
{
|
|
const double dd = (peak - mark) / peak * 100.0;
|
|
if(dd > max_dd_pct)
|
|
max_dd_pct = dd;
|
|
}
|
|
}
|
|
|
|
if(has_position)
|
|
{
|
|
const double mid = opens[bars_count - 1];
|
|
if(pos_type == ORDER_TYPE_BUY)
|
|
{
|
|
const double exit_px = FillSell(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_BUY, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
}
|
|
else
|
|
{
|
|
const double exit_px = FillBuy(mid, point, half_spread, cfg.slippage_points);
|
|
const double pnl = CalcTradeProfit(ORDER_TYPE_SELL, cfg.lot_size, entry_px, exit_px);
|
|
balance += pnl;
|
|
out.total_trades++;
|
|
if(pnl >= 0.0) { gross_profit += pnl; wins++; } else gross_loss += MathAbs(pnl);
|
|
}
|
|
}
|
|
|
|
out.net_profit = balance - cfg.initial_balance;
|
|
out.max_drawdown_pct = max_dd_pct;
|
|
out.win_rate = (out.total_trades > 0) ? (100.0 * wins / out.total_trades) : 0.0;
|
|
out.profit_factor = (gross_loss > 0.0) ? (gross_profit / gross_loss) : (gross_profit > 0.0 ? 999.0 : 0.0);
|
|
out.sharpe = ComputeSharpe(equity, equity_count, params.timeframe);
|
|
out.score = ComputeScore(out, cfg);
|
|
return true;
|
|
}
|
|
|
|
public:
|
|
CRSIAdaptiveOptimizer() : m_combos_tested(0) {}
|
|
|
|
static void PreviousCalendarMonth(const datetime now, datetime &month_start, datetime &month_end)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(now, dt);
|
|
datetime this_month_start = StringToTime(StringFormat("%04d.%02d.01 00:00", dt.year, dt.mon));
|
|
month_end = this_month_start - 1;
|
|
|
|
TimeToStruct(month_end, dt);
|
|
month_start = StringToTime(StringFormat("%04d.%02d.01 00:00", dt.year, dt.mon));
|
|
}
|
|
|
|
static int MonthKey(const datetime t)
|
|
{
|
|
MqlDateTime dt;
|
|
TimeToStruct(t, dt);
|
|
return dt.year * 100 + dt.mon;
|
|
}
|
|
|
|
bool Optimize(const string symbol,
|
|
const datetime opt_start,
|
|
const datetime opt_end,
|
|
const RSIAdaptiveParams &fallback,
|
|
const RSIAdaptiveSearchConfig &cfg,
|
|
RSIAdaptiveParams &best_out,
|
|
RSIAdaptiveMetrics &best_metrics_out)
|
|
{
|
|
m_symbol = symbol;
|
|
m_opt_start = opt_start;
|
|
m_opt_end = opt_end;
|
|
m_combos_tested = 0;
|
|
|
|
best_out = fallback;
|
|
best_metrics_out.net_profit = 0.0;
|
|
best_metrics_out.total_trades = 0;
|
|
best_metrics_out.win_rate = 0.0;
|
|
best_metrics_out.profit_factor = 0.0;
|
|
best_metrics_out.sharpe = 0.0;
|
|
best_metrics_out.max_drawdown_pct = 0.0;
|
|
best_metrics_out.score = -1.0e12;
|
|
|
|
RSIAdaptiveMetrics fallback_metrics;
|
|
if(BacktestParams(fallback, cfg, fallback_metrics))
|
|
{
|
|
if(fallback_metrics.score > best_metrics_out.score)
|
|
{
|
|
best_out = fallback;
|
|
best_metrics_out = fallback_metrics;
|
|
}
|
|
m_combos_tested++;
|
|
}
|
|
|
|
bool stop_search = false;
|
|
for(int rp = cfg.rsi_period_min; rp <= cfg.rsi_period_max && !stop_search; rp += cfg.rsi_period_step)
|
|
{
|
|
for(double ob = cfg.rsi_overbought_min; ob <= cfg.rsi_overbought_max + 0.001 && !stop_search; ob += cfg.rsi_overbought_step)
|
|
{
|
|
for(double os = cfg.rsi_oversold_min; os <= cfg.rsi_oversold_max + 0.001 && !stop_search; os += cfg.rsi_oversold_step)
|
|
{
|
|
for(double tb = cfg.rsi_target_buy_min; tb <= cfg.rsi_target_buy_max + 0.001 && !stop_search; tb += cfg.rsi_target_buy_step)
|
|
{
|
|
for(double ts = cfg.rsi_target_sell_min; ts <= cfg.rsi_target_sell_max + 0.001 && !stop_search; ts += cfg.rsi_target_sell_step)
|
|
{
|
|
for(int bw = cfg.bars_to_wait_min; bw <= cfg.bars_to_wait_max && !stop_search; bw += cfg.bars_to_wait_step)
|
|
{
|
|
if(m_combos_tested >= cfg.max_combinations)
|
|
{
|
|
stop_search = true;
|
|
break;
|
|
}
|
|
|
|
RSIAdaptiveParams p;
|
|
p.timeframe = cfg.timeframe;
|
|
p.rsi_period = rp;
|
|
p.rsi_overbought = ob;
|
|
p.rsi_oversold = os;
|
|
p.rsi_target_buy = tb;
|
|
p.rsi_target_sell = ts;
|
|
p.bars_to_wait = bw;
|
|
|
|
if(!p.IsValid())
|
|
continue;
|
|
|
|
RSIAdaptiveMetrics m;
|
|
if(!BacktestParams(p, cfg, m))
|
|
continue;
|
|
|
|
m_combos_tested++;
|
|
if(m.score > best_metrics_out.score)
|
|
{
|
|
best_out = p;
|
|
best_metrics_out = m;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
PrintFormat("[Adaptive] %s tested %d combos | window %s -> %s",
|
|
symbol,
|
|
m_combos_tested,
|
|
TimeToString(opt_start, TIME_DATE),
|
|
TimeToString(opt_end, TIME_DATE));
|
|
PrintFormat("[Adaptive] Best score=%.4f net=$%.2f sharpe=%.2f PF=%.2f trades=%d DD=%.2f%% | %s",
|
|
best_metrics_out.score,
|
|
best_metrics_out.net_profit,
|
|
best_metrics_out.sharpe,
|
|
best_metrics_out.profit_factor,
|
|
best_metrics_out.total_trades,
|
|
best_metrics_out.max_drawdown_pct,
|
|
best_out.ToString());
|
|
|
|
return (best_metrics_out.score > -1.0e11);
|
|
}
|
|
|
|
int CombosTested() const { return m_combos_tested; }
|
|
};
|