2206 lines
87 KiB
Plaintext
2206 lines
87 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| SelfOptimizingStrategy.mq5 |
|
|
//| Copyright 2025, MetaQuotes Ltd. |
|
|
//| https://www.mql5.com |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2025, MetaQuotes Ltd."
|
|
#property link "https://www.mql5.com"
|
|
#property version "1.00"
|
|
#property description "Self-Backtesting and Self-Optimizing Strategy"
|
|
#property description "Dynamically adjusts parameters based on last 3 days performance"
|
|
#property description "Two concurrent strategies: RSI Reversion and MA Crossover"
|
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
//--- Input parameters
|
|
input group "=== General Settings ==="
|
|
input string TradingSymbol = "BTCUSD"; // Trading Symbol
|
|
input ENUM_TIMEFRAMES TimeFrame = PERIOD_M1; // Timeframe (1 minute)
|
|
input double LotSize = 0.01; // Lot Size
|
|
input int MagicNumberBase = 88001; // Magic Number Base
|
|
input int Slippage = 3; // Slippage
|
|
|
|
input group "=== Self-Optimization Settings ==="
|
|
input int OptimizationPeriodHours = 6; // Backtesting Period (Hours) - Use last N hours
|
|
input int OptimizationPeriodMinutes = 0; // Additional Minutes (0-59) - Adds to hours
|
|
input int OptimizationIntervalBars = 50; // Bars Between Optimizations (reduced for faster adaptation)
|
|
input int MinTradesForOptimization = 2; // Min Trades for Optimization (reduced for faster adaptation)
|
|
input bool EnableAutoOptimization = true; // Enable Auto Optimization
|
|
input double MinProfitabilityForKeep = 0.1; // Min Profitability % to Keep Parameters
|
|
input bool EnableRandomExploration = true; // Enable Random Parameter Exploration
|
|
input int ConsecutiveLossesToTrigger = 5; // Consecutive Losses to Trigger Random Mode
|
|
input double MinProfitabilityForRandom = -2.0; // Min Profitability % to Trigger Random Mode
|
|
input bool EnableForkSystem = true; // Enable Fork/Merge System
|
|
input int ForkTestBars = 200; // Bars to Test Fork Before Merge
|
|
input double ForkMinImprovement = 0.2; // Min Improvement % to Merge Fork
|
|
input double MaxLossPercent = 0.5; // Max Loss % Before Force Exit (Fork)
|
|
input double AdverseMoveThreshold = 0.15; // Adverse Move % to Trigger Exit (Fork)
|
|
input int ATR_Period = 14; // ATR Period for Volatility Stop
|
|
|
|
input group "=== Strategy 1: RSI Reversion ==="
|
|
input bool EnableStrategy1 = true; // Enable RSI Reversion
|
|
input int RSI_Period_Start = 7; // RSI Period (Start)
|
|
input int RSI_Period_End = 21; // RSI Period (End)
|
|
input double RSI_Oversold_Start = 25.0; // RSI Oversold (Start)
|
|
input double RSI_Oversold_End = 35.0; // RSI Oversold (End)
|
|
input double RSI_Overbought_Start = 65.0; // RSI Overbought (Start)
|
|
input double RSI_Overbought_End = 75.0; // RSI Overbought (End)
|
|
input int RSI_MaxBars = 30; // Max Bars in Trade
|
|
input int RSI_MinBars = 3; // Min Bars Before Exit
|
|
input bool RSI_ExitOnReversal = false; // Exit on Signal Reversal
|
|
|
|
input group "=== Strategy 2: MA Crossover ==="
|
|
input bool EnableStrategy2 = true; // Enable MA Crossover
|
|
input int MA_Fast_Start = 5; // Fast MA Period (Start)
|
|
input int MA_Fast_End = 15; // Fast MA Period (End)
|
|
input int MA_Slow_Start = 20; // Slow MA Period (Start)
|
|
input int MA_Slow_End = 50; // Slow MA Period (End)
|
|
input ENUM_MA_METHOD MA_Method = MODE_EMA; // MA Method
|
|
input int MA_MaxBars = 40; // Max Bars in Trade
|
|
input int MA_MinBars = 5; // Min Bars Before Exit
|
|
input bool MA_ExitOnReversal = false; // Exit on Signal Reversal
|
|
|
|
//--- Global variables
|
|
CTrade trade;
|
|
|
|
// Strategy 1: RSI Reversion
|
|
struct RSIStrategyParams
|
|
{
|
|
int rsi_period;
|
|
double rsi_oversold;
|
|
double rsi_overbought;
|
|
double trailing_stop_pips;
|
|
double profit_target_percent;
|
|
int max_bars;
|
|
int min_bars;
|
|
bool exit_on_reversal;
|
|
double profitability;
|
|
int total_trades;
|
|
int winning_trades;
|
|
double net_profit;
|
|
};
|
|
|
|
// Strategy 2: MA Crossover
|
|
struct MAStrategyParams
|
|
{
|
|
int ma_fast;
|
|
int ma_slow;
|
|
ENUM_MA_METHOD ma_method;
|
|
double trailing_stop_pips;
|
|
double profit_target_percent;
|
|
int max_bars;
|
|
int min_bars;
|
|
bool exit_on_reversal;
|
|
double profitability;
|
|
int total_trades;
|
|
int winning_trades;
|
|
double net_profit;
|
|
};
|
|
|
|
RSIStrategyParams s1_current_params;
|
|
MAStrategyParams s2_current_params;
|
|
|
|
// Fork system - parallel testing
|
|
RSIStrategyParams s1_fork_params;
|
|
MAStrategyParams s2_fork_params;
|
|
bool s1_fork_active = false;
|
|
bool s2_fork_active = false;
|
|
datetime s1_fork_start_time = 0;
|
|
datetime s2_fork_start_time = 0;
|
|
int s1_fork_start_bars = 0;
|
|
int s2_fork_start_bars = 0;
|
|
double s1_fork_start_profit = 0.0;
|
|
double s2_fork_start_profit = 0.0;
|
|
int s1_fork_magic = 0;
|
|
int s2_fork_magic = 0;
|
|
|
|
// Strategy handles
|
|
int s1_rsi_handle = INVALID_HANDLE;
|
|
int s1_fork_rsi_handle = INVALID_HANDLE;
|
|
int s2_ma_fast_handle = INVALID_HANDLE;
|
|
int s2_ma_slow_handle = INVALID_HANDLE;
|
|
int s2_fork_ma_fast_handle = INVALID_HANDLE;
|
|
int s2_fork_ma_slow_handle = INVALID_HANDLE;
|
|
int s1_fork_atr_handle = INVALID_HANDLE;
|
|
int s2_fork_atr_handle = INVALID_HANDLE;
|
|
double s1_fork_entry_price = 0.0;
|
|
double s2_fork_entry_price = 0.0;
|
|
|
|
// Position tracking
|
|
ulong s1_position_ticket = 0;
|
|
ulong s1_fork_position_ticket = 0;
|
|
ulong s2_position_ticket = 0;
|
|
ulong s2_fork_position_ticket = 0;
|
|
datetime s1_last_bar = 0;
|
|
datetime s1_fork_last_bar = 0;
|
|
datetime s2_last_bar = 0;
|
|
datetime s2_fork_last_bar = 0;
|
|
int s1_magic = 0;
|
|
int s2_magic = 0;
|
|
|
|
// Optimization tracking
|
|
int bars_since_optimization = 0;
|
|
datetime last_optimization_time = 0;
|
|
|
|
// Loss tracking for random exploration
|
|
int s1_consecutive_losses = 0;
|
|
int s2_consecutive_losses = 0;
|
|
double s1_last_profitability = 0.0;
|
|
double s2_last_profitability = 0.0;
|
|
bool s1_random_mode = false;
|
|
bool s2_random_mode = false;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert initialization function |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
// Initialize random seed
|
|
MathSrand((uint)TimeCurrent());
|
|
|
|
trade.SetDeviationInPoints(Slippage);
|
|
trade.SetTypeFilling(ORDER_FILLING_FOK);
|
|
|
|
// Set magic numbers for each strategy
|
|
s1_magic = MagicNumberBase;
|
|
s2_magic = MagicNumberBase + 1;
|
|
s1_fork_magic = MagicNumberBase + 10; // Fork uses different magic
|
|
s2_fork_magic = MagicNumberBase + 11;
|
|
|
|
// Initialize default parameters
|
|
s1_current_params.rsi_period = RSI_Period_Start;
|
|
s1_current_params.rsi_oversold = RSI_Oversold_Start;
|
|
s1_current_params.rsi_overbought = RSI_Overbought_Start;
|
|
s1_current_params.trailing_stop_pips = 0; // Not used
|
|
s1_current_params.profit_target_percent = 0; // Not used
|
|
s1_current_params.max_bars = RSI_MaxBars;
|
|
s1_current_params.min_bars = RSI_MinBars;
|
|
s1_current_params.exit_on_reversal = RSI_ExitOnReversal;
|
|
s1_current_params.profitability = 0.0;
|
|
s1_current_params.total_trades = 0;
|
|
s1_current_params.winning_trades = 0;
|
|
s1_current_params.net_profit = 0.0;
|
|
|
|
s2_current_params.ma_fast = MA_Fast_Start;
|
|
s2_current_params.ma_slow = MA_Slow_Start;
|
|
s2_current_params.ma_method = MA_Method;
|
|
s2_current_params.trailing_stop_pips = 0; // Not used
|
|
s2_current_params.profit_target_percent = 0; // Not used
|
|
s2_current_params.max_bars = MA_MaxBars;
|
|
s2_current_params.min_bars = MA_MinBars;
|
|
s2_current_params.exit_on_reversal = MA_ExitOnReversal;
|
|
s2_current_params.profitability = 0.0;
|
|
s2_current_params.total_trades = 0;
|
|
s2_current_params.winning_trades = 0;
|
|
s2_current_params.net_profit = 0.0;
|
|
|
|
// Create initial indicators
|
|
if(EnableStrategy1)
|
|
{
|
|
s1_rsi_handle = iRSI(TradingSymbol, TimeFrame, s1_current_params.rsi_period, PRICE_CLOSE);
|
|
if(s1_rsi_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to create RSI indicator");
|
|
return(INIT_FAILED);
|
|
}
|
|
}
|
|
|
|
if(EnableStrategy2)
|
|
{
|
|
s2_ma_fast_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_fast, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
s2_ma_slow_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_slow, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
if(s2_ma_fast_handle == INVALID_HANDLE || s2_ma_slow_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to create MA indicators");
|
|
return(INIT_FAILED);
|
|
}
|
|
}
|
|
|
|
// Perform initial optimization
|
|
if(EnableAutoOptimization)
|
|
{
|
|
Print("=== Initial Self-Optimization Starting ===");
|
|
OptimizeStrategies();
|
|
}
|
|
|
|
Print("Self-Optimizing Strategy initialized");
|
|
Print("Strategy 1 (RSI Reversion): Period=", s1_current_params.rsi_period,
|
|
" Oversold=", s1_current_params.rsi_oversold, " Overbought=", s1_current_params.rsi_overbought);
|
|
Print("Strategy 2 (MA Crossover): Fast=", s2_current_params.ma_fast,
|
|
" Slow=", s2_current_params.ma_slow, " Method=", EnumToString(s2_current_params.ma_method));
|
|
|
|
return(INIT_SUCCEEDED);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert deinitialization function |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
if(s1_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_rsi_handle);
|
|
if(s1_fork_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_rsi_handle);
|
|
if(s1_fork_atr_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_atr_handle);
|
|
if(s2_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_fast_handle);
|
|
if(s2_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_slow_handle);
|
|
if(s2_fork_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_fast_handle);
|
|
if(s2_fork_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_slow_handle);
|
|
if(s2_fork_atr_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_atr_handle);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Expert tick function |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// Check if it's time to optimize
|
|
bars_since_optimization++;
|
|
if(EnableAutoOptimization && bars_since_optimization >= OptimizationIntervalBars)
|
|
{
|
|
Print("=== Self-Optimization Triggered ===");
|
|
OptimizeStrategies();
|
|
bars_since_optimization = 0;
|
|
}
|
|
|
|
// Run Strategy 1: RSI Reversion
|
|
if(EnableStrategy1)
|
|
{
|
|
RunRSIStrategy();
|
|
|
|
// Run fork if active
|
|
if(EnableForkSystem && s1_fork_active)
|
|
{
|
|
RunRSIStrategyFork();
|
|
CheckForkMerge(1); // Check if fork should be merged
|
|
}
|
|
}
|
|
|
|
// Run Strategy 2: MA Crossover
|
|
if(EnableStrategy2)
|
|
{
|
|
RunMAStrategy();
|
|
|
|
// Run fork if active
|
|
if(EnableForkSystem && s2_fork_active)
|
|
{
|
|
RunMAStrategyFork();
|
|
CheckForkMerge(2); // Check if fork should be merged
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Optimize both strategies using backtesting |
|
|
//+------------------------------------------------------------------+
|
|
void OptimizeStrategies()
|
|
{
|
|
Print("=== Starting Self-Optimization ===");
|
|
|
|
// FIRST: Update current profitability from live trades
|
|
if(EnableStrategy1)
|
|
{
|
|
double live_profit = CalculateStrategyProfitability(s1_magic);
|
|
s1_current_params.profitability = live_profit;
|
|
Print("Strategy 1 - Current Live Profitability: ", DoubleToString(live_profit, 2), "%");
|
|
}
|
|
|
|
if(EnableStrategy2)
|
|
{
|
|
double live_profit = CalculateStrategyProfitability(s2_magic);
|
|
s2_current_params.profitability = live_profit;
|
|
Print("Strategy 2 - Current Live Profitability: ", DoubleToString(live_profit, 2), "%");
|
|
}
|
|
|
|
int total_minutes = OptimizationPeriodHours * 60 + OptimizationPeriodMinutes;
|
|
if(OptimizationPeriodMinutes > 0)
|
|
Print("Backtesting period: Last ", OptimizationPeriodHours, " hour(s) ", OptimizationPeriodMinutes, " minute(s) (", total_minutes, " minutes total)");
|
|
else
|
|
Print("Backtesting period: Last ", OptimizationPeriodHours, " hour(s) (", total_minutes, " minutes total)");
|
|
|
|
datetime end_time = TimeCurrent();
|
|
datetime start_time = end_time - (OptimizationPeriodHours * 3600 + OptimizationPeriodMinutes * 60); // Convert to seconds
|
|
|
|
// Ensure we have enough historical data
|
|
int total_bars = Bars(TradingSymbol, TimeFrame);
|
|
if(total_bars > 0)
|
|
{
|
|
datetime oldest_bar = iTime(TradingSymbol, TimeFrame, total_bars - 1);
|
|
if(start_time < oldest_bar)
|
|
{
|
|
Print("WARNING: Not enough historical data. Using available data from ", TimeToString(oldest_bar));
|
|
start_time = oldest_bar;
|
|
}
|
|
}
|
|
|
|
// Optimize Strategy 1: RSI Reversion
|
|
if(EnableStrategy1)
|
|
{
|
|
Print("--- Optimizing Strategy 1: RSI Reversion ---");
|
|
OptimizeRSIStrategy(start_time, end_time);
|
|
}
|
|
|
|
// Optimize Strategy 2: MA Crossover
|
|
if(EnableStrategy2)
|
|
{
|
|
Print("--- Optimizing Strategy 2: MA Crossover ---");
|
|
OptimizeMAStrategy(start_time, end_time);
|
|
}
|
|
|
|
// Check if we should trigger random exploration mode
|
|
if(EnableRandomExploration)
|
|
{
|
|
// Check Strategy 1
|
|
if(EnableStrategy1)
|
|
{
|
|
if(s1_current_params.profitability < MinProfitabilityForRandom || s1_consecutive_losses >= ConsecutiveLossesToTrigger)
|
|
{
|
|
if(!s1_random_mode)
|
|
{
|
|
Print("=== Strategy 1: Entering RANDOM EXPLORATION MODE ===");
|
|
Print("Reason: Profitability=", DoubleToString(s1_current_params.profitability, 2),
|
|
"% Consecutive Losses=", s1_consecutive_losses);
|
|
s1_random_mode = true;
|
|
}
|
|
}
|
|
else if(s1_current_params.profitability > 0.5 && s1_random_mode)
|
|
{
|
|
Print("=== Strategy 1: Exiting RANDOM EXPLORATION MODE ===");
|
|
s1_random_mode = false;
|
|
s1_consecutive_losses = 0;
|
|
}
|
|
}
|
|
|
|
// Check Strategy 2
|
|
if(EnableStrategy2)
|
|
{
|
|
if(s2_current_params.profitability < MinProfitabilityForRandom || s2_consecutive_losses >= ConsecutiveLossesToTrigger)
|
|
{
|
|
if(!s2_random_mode)
|
|
{
|
|
Print("=== Strategy 2: Entering RANDOM EXPLORATION MODE ===");
|
|
Print("Reason: Profitability=", DoubleToString(s2_current_params.profitability, 2),
|
|
"% Consecutive Losses=", s2_consecutive_losses);
|
|
s2_random_mode = true;
|
|
}
|
|
}
|
|
else if(s2_current_params.profitability > 0.5 && s2_random_mode)
|
|
{
|
|
Print("=== Strategy 2: Exiting RANDOM EXPLORATION MODE ===");
|
|
s2_random_mode = false;
|
|
s2_consecutive_losses = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
Print("=== Self-Optimization Complete ===");
|
|
Print("Strategy 1 - RSI Period: ", s1_current_params.rsi_period,
|
|
" Oversold: ", s1_current_params.rsi_oversold,
|
|
" Overbought: ", s1_current_params.rsi_overbought,
|
|
" Profitability: ", DoubleToString(s1_current_params.profitability, 2), "%",
|
|
s1_random_mode ? " [RANDOM MODE]" : "");
|
|
Print("Strategy 2 - Fast MA: ", s2_current_params.ma_fast,
|
|
" Slow MA: ", s2_current_params.ma_slow,
|
|
" Profitability: ", DoubleToString(s2_current_params.profitability, 2), "%",
|
|
s2_random_mode ? " [RANDOM MODE]" : "");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Optimize RSI Reversion Strategy |
|
|
//+------------------------------------------------------------------+
|
|
void OptimizeRSIStrategy(datetime start_time, datetime end_time)
|
|
{
|
|
Print("RSI Optimization: Testing period from ", TimeToString(start_time), " to ", TimeToString(end_time));
|
|
RSIStrategyParams best_params = s1_current_params;
|
|
double best_profitability = s1_current_params.profitability;
|
|
int tests_run = 0; // Track number of tests run
|
|
|
|
if(s1_random_mode)
|
|
{
|
|
// Random exploration mode - test random parameter combinations
|
|
Print("RSI Strategy: RANDOM EXPLORATION MODE - Testing random parameters");
|
|
int random_tests = 20; // Test 20 random combinations
|
|
|
|
for(int i = 0; i < random_tests; i++)
|
|
{
|
|
// Generate random parameters within ranges
|
|
int rsi_period = (int)(RSI_Period_Start + MathRand() % (RSI_Period_End - RSI_Period_Start + 1));
|
|
double oversold = RSI_Oversold_Start + (MathRand() % (int)((RSI_Oversold_End - RSI_Oversold_Start) * 10 + 1)) / 10.0;
|
|
double overbought = RSI_Overbought_Start + (MathRand() % (int)((RSI_Overbought_End - RSI_Overbought_Start) * 10 + 1)) / 10.0;
|
|
|
|
// Ensure valid range
|
|
if(oversold >= overbought) continue;
|
|
|
|
RSIStrategyParams test_params;
|
|
test_params.rsi_period = rsi_period;
|
|
test_params.rsi_oversold = oversold;
|
|
test_params.rsi_overbought = overbought;
|
|
test_params.trailing_stop_pips = s1_current_params.trailing_stop_pips;
|
|
test_params.profit_target_percent = s1_current_params.profit_target_percent;
|
|
test_params.max_bars = s1_current_params.max_bars;
|
|
test_params.min_bars = s1_current_params.min_bars;
|
|
test_params.exit_on_reversal = s1_current_params.exit_on_reversal;
|
|
|
|
// Backtest this parameter set
|
|
double profitability = BacktestRSIStrategy(test_params, start_time, end_time);
|
|
tests_run++;
|
|
|
|
if(i == 0 || i == random_tests - 1)
|
|
{
|
|
Print("RSI Random Test ", i+1, "/", random_tests, ": Period=", rsi_period,
|
|
" Oversold=", DoubleToString(oversold, 1), " Overbought=", DoubleToString(overbought, 1),
|
|
" Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
|
|
if(profitability > best_profitability)
|
|
{
|
|
best_profitability = profitability;
|
|
best_params = test_params;
|
|
best_params.profitability = profitability;
|
|
Print("RSI NEW BEST: Period=", rsi_period, " Oversold=", DoubleToString(oversold, 1),
|
|
" Overbought=", DoubleToString(overbought, 1), " Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Normal grid search mode
|
|
for(int rsi_period = RSI_Period_Start; rsi_period <= RSI_Period_End; rsi_period += 2)
|
|
{
|
|
for(double oversold = RSI_Oversold_Start; oversold <= RSI_Oversold_End; oversold += 2.5)
|
|
{
|
|
for(double overbought = RSI_Overbought_Start; overbought <= RSI_Overbought_End; overbought += 2.5)
|
|
{
|
|
if(oversold >= overbought) continue; // Skip invalid combinations
|
|
|
|
RSIStrategyParams test_params;
|
|
test_params.rsi_period = rsi_period;
|
|
test_params.rsi_oversold = oversold;
|
|
test_params.rsi_overbought = overbought;
|
|
test_params.trailing_stop_pips = s1_current_params.trailing_stop_pips;
|
|
test_params.profit_target_percent = s1_current_params.profit_target_percent;
|
|
test_params.max_bars = s1_current_params.max_bars;
|
|
test_params.min_bars = s1_current_params.min_bars;
|
|
test_params.exit_on_reversal = s1_current_params.exit_on_reversal;
|
|
|
|
// Backtest this parameter set
|
|
double profitability = BacktestRSIStrategy(test_params, start_time, end_time);
|
|
tests_run++;
|
|
|
|
if(profitability > best_profitability)
|
|
{
|
|
best_profitability = profitability;
|
|
best_params = test_params;
|
|
best_params.profitability = profitability;
|
|
Print("RSI Grid Search: NEW BEST Period=", rsi_period, " Oversold=", DoubleToString(oversold, 1),
|
|
" Overbought=", DoubleToString(overbought, 1), " Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update parameters if new ones are better
|
|
// Be more aggressive when losing money
|
|
bool should_update = false;
|
|
bool is_losing = s1_current_params.profitability < -0.5; // Losing more than 0.5%
|
|
|
|
Print("RSI Optimization Results: Tests Run=", tests_run, " Current=", DoubleToString(s1_current_params.profitability, 2),
|
|
"% Best Found=", DoubleToString(best_profitability, 2), "%");
|
|
|
|
if(s1_random_mode)
|
|
{
|
|
// In random mode, accept if it's better OR if current is very bad
|
|
should_update = (best_profitability > s1_current_params.profitability) ||
|
|
(best_profitability > -5.0 && s1_current_params.profitability < -10.0);
|
|
}
|
|
else if(is_losing)
|
|
{
|
|
// When losing, be more aggressive - accept any improvement or if backtest shows positive
|
|
should_update = (best_profitability > s1_current_params.profitability) ||
|
|
(best_profitability > 0.1); // Accept if backtest shows any positive result
|
|
Print("RSI Strategy: LOSING MODE - Accepting improvements more aggressively");
|
|
}
|
|
else
|
|
{
|
|
should_update = (best_profitability > s1_current_params.profitability + 0.1) ||
|
|
(best_profitability >= MinProfitabilityForKeep && s1_current_params.profitability < MinProfitabilityForKeep);
|
|
}
|
|
|
|
if(should_update)
|
|
{
|
|
if(EnableForkSystem && !s1_fork_active)
|
|
{
|
|
// Create fork instead of immediately updating
|
|
Print("RSI Strategy: Creating FORK with new parameters. Current Profit: ",
|
|
DoubleToString(s1_current_params.profitability, 2),
|
|
"% Fork Profit (backtest): ", DoubleToString(best_profitability, 2), "%");
|
|
|
|
s1_fork_params = best_params;
|
|
s1_fork_active = true;
|
|
s1_fork_start_time = TimeCurrent();
|
|
s1_fork_start_bars = Bars(TradingSymbol, TimeFrame);
|
|
s1_fork_start_profit = s1_current_params.profitability;
|
|
|
|
// Create fork indicators
|
|
s1_fork_rsi_handle = iRSI(TradingSymbol, TimeFrame, s1_fork_params.rsi_period, PRICE_CLOSE);
|
|
s1_fork_atr_handle = iATR(TradingSymbol, TimeFrame, ATR_Period);
|
|
if(s1_fork_rsi_handle == INVALID_HANDLE || s1_fork_atr_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to create fork indicators");
|
|
if(s1_fork_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_rsi_handle);
|
|
if(s1_fork_atr_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_atr_handle);
|
|
s1_fork_active = false;
|
|
}
|
|
else
|
|
{
|
|
Print("FORK CREATED: RSI Period=", s1_fork_params.rsi_period,
|
|
" Oversold=", s1_fork_params.rsi_oversold,
|
|
" Overbought=", s1_fork_params.rsi_overbought);
|
|
}
|
|
}
|
|
else if(!EnableForkSystem)
|
|
{
|
|
// Direct update if fork system disabled
|
|
Print("RSI Strategy: Updating parameters. Old Profit: ", DoubleToString(s1_current_params.profitability, 2),
|
|
"% New Profit: ", DoubleToString(best_profitability, 2), "%");
|
|
|
|
s1_current_params = best_params;
|
|
s1_consecutive_losses = 0; // Reset on improvement
|
|
|
|
// Recreate indicator with new parameters
|
|
if(s1_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_rsi_handle);
|
|
s1_rsi_handle = iRSI(TradingSymbol, TimeFrame, s1_current_params.rsi_period, PRICE_CLOSE);
|
|
|
|
if(s1_rsi_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to recreate RSI indicator with new parameters");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Print("RSI Strategy: Fork already active, skipping new fork creation");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Print("RSI Strategy: Keeping current parameters. Current Profit: ",
|
|
DoubleToString(s1_current_params.profitability, 2), "% Best Found: ",
|
|
DoubleToString(best_profitability, 2), "%");
|
|
|
|
// Track if we're still losing
|
|
if(s1_current_params.profitability < 0)
|
|
s1_consecutive_losses++;
|
|
else
|
|
s1_consecutive_losses = 0;
|
|
}
|
|
|
|
s1_last_profitability = s1_current_params.profitability;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Backtest RSI Strategy |
|
|
//+------------------------------------------------------------------+
|
|
double BacktestRSIStrategy(RSIStrategyParams ¶ms, datetime start_time, datetime end_time)
|
|
{
|
|
// Create temporary RSI indicator for backtesting
|
|
int temp_rsi = iRSI(TradingSymbol, TimeFrame, params.rsi_period, PRICE_CLOSE);
|
|
if(temp_rsi == INVALID_HANDLE) return -999999.0;
|
|
|
|
double total_profit = 0.0;
|
|
int total_trades = 0;
|
|
int winning_trades = 0;
|
|
ulong virtual_position = 0;
|
|
double virtual_entry = 0;
|
|
datetime virtual_entry_time = 0;
|
|
ENUM_POSITION_TYPE virtual_position_type = WRONG_VALUE;
|
|
|
|
// Calculate how many bars we need based on time period
|
|
int period_seconds = PeriodSeconds(TimeFrame);
|
|
int bars_needed = (int)((end_time - start_time) / period_seconds) + 20; // Add buffer for indicators
|
|
|
|
// Get bars from end_time going backwards
|
|
// end_bar should be 0 (current bar) or the bar at end_time
|
|
int end_bar = iBarShift(TradingSymbol, TimeFrame, end_time, false);
|
|
if(end_bar < 0) end_bar = 0; // Use current bar if not found
|
|
|
|
// Calculate start_bar by going backwards from end_bar
|
|
int start_bar = end_bar + bars_needed;
|
|
int max_bars = Bars(TradingSymbol, TimeFrame);
|
|
if(start_bar >= max_bars)
|
|
{
|
|
start_bar = max_bars - 1;
|
|
bars_needed = start_bar - end_bar; // Adjust to available bars
|
|
}
|
|
|
|
// Verify we have enough bars
|
|
int bars_to_test = start_bar - end_bar;
|
|
Print("RSI Backtest: Bars needed: ", bars_needed, " Bars available: ", bars_to_test,
|
|
" (start_bar=", start_bar, " end_bar=", end_bar, ") Period: ",
|
|
TimeToString(start_time), " to ", TimeToString(end_time));
|
|
|
|
if(bars_to_test < 5) // Reduced minimum for shorter periods
|
|
{
|
|
Print("RSI Backtest: Not enough bars (need 5, have ", bars_to_test, ")");
|
|
IndicatorRelease(temp_rsi);
|
|
return -999999.0;
|
|
}
|
|
|
|
// Get data arrays
|
|
double rsi_buffer[];
|
|
double close_buffer[];
|
|
datetime time_buffer[];
|
|
ArraySetAsSeries(rsi_buffer, true);
|
|
ArraySetAsSeries(close_buffer, true);
|
|
ArraySetAsSeries(time_buffer, true);
|
|
|
|
// Copy data from end_bar to start_bar (oldest to newest)
|
|
// With ArraySetAsSeries(true), index 0 = most recent, higher index = older
|
|
if(CopyBuffer(temp_rsi, 0, end_bar, bars_to_test, rsi_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_rsi);
|
|
return -999999.0;
|
|
}
|
|
if(CopyClose(TradingSymbol, TimeFrame, end_bar, bars_to_test, close_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_rsi);
|
|
return -999999.0;
|
|
}
|
|
if(CopyTime(TradingSymbol, TimeFrame, end_bar, bars_to_test, time_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_rsi);
|
|
return -999999.0;
|
|
}
|
|
if(CopyTime(TradingSymbol, TimeFrame, end_bar, bars_to_test, time_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_rsi);
|
|
return -999999.0;
|
|
}
|
|
|
|
double point = SymbolInfoDouble(TradingSymbol, SYMBOL_POINT);
|
|
double pip = (SymbolInfoInteger(TradingSymbol, SYMBOL_DIGITS) == 3 ||
|
|
SymbolInfoInteger(TradingSymbol, SYMBOL_DIGITS) == 5) ? point * 10 : point;
|
|
|
|
// Iterate through historical bars (from oldest to newest)
|
|
// With ArraySetAsSeries(true), index 0 = newest, higher index = older
|
|
// So we iterate from highest index (oldest) down to 1 (newest)
|
|
for(int i = bars_to_test - 1; i >= 1; i--) // Start from oldest, need previous bar
|
|
{
|
|
datetime bar_time = time_buffer[i];
|
|
double current_rsi = rsi_buffer[i];
|
|
double prev_rsi = rsi_buffer[i-1]; // i-1 is more recent than i
|
|
double current_price = close_buffer[i];
|
|
|
|
// Check existing virtual position
|
|
if(virtual_position > 0)
|
|
{
|
|
// Check exit conditions
|
|
int bars_held = (int)((bar_time - virtual_entry_time) / period_seconds);
|
|
if(bars_held >= params.max_bars)
|
|
{
|
|
// Time-based exit
|
|
double exit_price = current_price;
|
|
double profit = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit = (exit_price - virtual_entry) / virtual_entry;
|
|
else
|
|
profit = (virtual_entry - exit_price) / virtual_entry;
|
|
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
|
|
virtual_position = 0;
|
|
}
|
|
else
|
|
{
|
|
// Check profit target
|
|
double profit_pct = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit_pct = ((current_price - virtual_entry) / virtual_entry) * 100.0;
|
|
else
|
|
profit_pct = ((virtual_entry - current_price) / virtual_entry) * 100.0;
|
|
|
|
// Profit target removed - using other exit conditions only
|
|
if(false) // Disabled
|
|
{
|
|
double profit = profit_pct / 100.0;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
|
|
// Check RSI extreme exit
|
|
if(virtual_position_type == POSITION_TYPE_BUY && current_rsi >= params.rsi_overbought)
|
|
{
|
|
double profit = (current_price - virtual_entry) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
else if(virtual_position_type == POSITION_TYPE_SELL && current_rsi <= params.rsi_oversold)
|
|
{
|
|
double profit = (virtual_entry - current_price) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals
|
|
if(virtual_position == 0)
|
|
{
|
|
// RSI Reversion: Buy when oversold, Sell when overbought
|
|
if(current_rsi > params.rsi_oversold && prev_rsi <= params.rsi_oversold)
|
|
{
|
|
// RSI crossed above oversold - buy signal
|
|
virtual_position = 1;
|
|
virtual_entry = current_price;
|
|
virtual_entry_time = bar_time;
|
|
virtual_position_type = POSITION_TYPE_BUY;
|
|
}
|
|
else if(current_rsi < params.rsi_overbought && prev_rsi >= params.rsi_overbought)
|
|
{
|
|
// RSI crossed below overbought - sell signal
|
|
virtual_position = 2;
|
|
virtual_entry = current_price;
|
|
virtual_entry_time = bar_time;
|
|
virtual_position_type = POSITION_TYPE_SELL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Debug: Log signal detection
|
|
if(total_trades == 0 && bars_to_test > 20)
|
|
{
|
|
// Check if we're even getting RSI signals
|
|
int signal_count = 0;
|
|
for(int i = 1; i < bars_to_test && i < 20; i++)
|
|
{
|
|
if(rsi_buffer[i] > params.rsi_oversold && rsi_buffer[i-1] <= params.rsi_oversold) signal_count++;
|
|
if(rsi_buffer[i] < params.rsi_overbought && rsi_buffer[i-1] >= params.rsi_overbought) signal_count++;
|
|
}
|
|
Print("RSI Backtest Debug: First 20 bars - Signals detected: ", signal_count,
|
|
" RSI range: ", DoubleToString(rsi_buffer[0], 1), " to ", DoubleToString(rsi_buffer[MathMin(19, bars_to_test-1)], 1));
|
|
}
|
|
|
|
// Close any remaining position
|
|
if(virtual_position > 0)
|
|
{
|
|
double exit_price = close_buffer[0];
|
|
double profit = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit = (exit_price - virtual_entry) / virtual_entry;
|
|
else
|
|
profit = (virtual_entry - exit_price) / virtual_entry;
|
|
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
}
|
|
|
|
IndicatorRelease(temp_rsi);
|
|
|
|
// Calculate profitability percentage
|
|
if(total_trades >= MinTradesForOptimization)
|
|
{
|
|
params.total_trades = total_trades;
|
|
params.winning_trades = winning_trades;
|
|
params.net_profit = total_profit;
|
|
double profitability = total_profit * 100.0;
|
|
Print("RSI Backtest: Trades=", total_trades, " Wins=", winning_trades,
|
|
" Profit=", DoubleToString(profitability, 2), "%");
|
|
// Return profitability as percentage (total_profit is already a ratio)
|
|
return profitability;
|
|
}
|
|
|
|
// Return a very negative value if not enough trades
|
|
if(total_trades > 0)
|
|
{
|
|
// Some trades found but not enough - return a scaled negative value
|
|
Print("RSI Backtest: Only ", total_trades, " trades found (need ", MinTradesForOptimization, ")");
|
|
return -1000.0 - (MinTradesForOptimization - total_trades);
|
|
}
|
|
|
|
Print("RSI Backtest: NO TRADES FOUND - Period=", params.rsi_period,
|
|
" Oversold=", DoubleToString(params.rsi_oversold, 1),
|
|
" Overbought=", DoubleToString(params.rsi_overbought, 1));
|
|
return -999999.0; // No trades found
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Optimize MA Crossover Strategy |
|
|
//+------------------------------------------------------------------+
|
|
void OptimizeMAStrategy(datetime start_time, datetime end_time)
|
|
{
|
|
Print("MA Optimization: Testing period from ", TimeToString(start_time), " to ", TimeToString(end_time));
|
|
MAStrategyParams best_params = s2_current_params;
|
|
double best_profitability = s2_current_params.profitability;
|
|
int tests_run = 0; // Track number of tests run
|
|
|
|
if(s2_random_mode)
|
|
{
|
|
// Random exploration mode - test random parameter combinations
|
|
Print("MA Strategy: RANDOM EXPLORATION MODE - Testing random parameters");
|
|
int random_tests = 20; // Test 20 random combinations
|
|
|
|
for(int i = 0; i < random_tests; i++)
|
|
{
|
|
// Generate random parameters within ranges
|
|
int ma_fast = MA_Fast_Start + (MathRand() % (MA_Fast_End - MA_Fast_Start + 1));
|
|
int ma_slow = MA_Slow_Start + (MathRand() % (MA_Slow_End - MA_Slow_Start + 1));
|
|
|
|
// Ensure valid range (fast < slow)
|
|
if(ma_fast >= ma_slow) continue;
|
|
|
|
MAStrategyParams test_params;
|
|
test_params.ma_fast = ma_fast;
|
|
test_params.ma_slow = ma_slow;
|
|
test_params.ma_method = MA_Method;
|
|
test_params.trailing_stop_pips = s2_current_params.trailing_stop_pips;
|
|
test_params.profit_target_percent = s2_current_params.profit_target_percent;
|
|
test_params.max_bars = s2_current_params.max_bars;
|
|
test_params.min_bars = s2_current_params.min_bars;
|
|
test_params.exit_on_reversal = s2_current_params.exit_on_reversal;
|
|
|
|
// Backtest this parameter set
|
|
double profitability = BacktestMAStrategy(test_params, start_time, end_time);
|
|
tests_run++;
|
|
|
|
if(i == 0 || i == random_tests - 1)
|
|
{
|
|
Print("MA Random Test ", i+1, "/", random_tests, ": Fast=", ma_fast,
|
|
" Slow=", ma_slow, " Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
|
|
if(profitability > best_profitability)
|
|
{
|
|
best_profitability = profitability;
|
|
best_params = test_params;
|
|
best_params.profitability = profitability;
|
|
Print("MA NEW BEST: Fast=", ma_fast, " Slow=", ma_slow,
|
|
" Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Normal grid search mode
|
|
for(int ma_fast = MA_Fast_Start; ma_fast <= MA_Fast_End; ma_fast += 2)
|
|
{
|
|
for(int ma_slow = MA_Slow_Start; ma_slow <= MA_Slow_End; ma_slow += 5)
|
|
{
|
|
if(ma_fast >= ma_slow) continue; // Fast must be less than slow
|
|
|
|
MAStrategyParams test_params;
|
|
test_params.ma_fast = ma_fast;
|
|
test_params.ma_slow = ma_slow;
|
|
test_params.ma_method = MA_Method;
|
|
test_params.trailing_stop_pips = s2_current_params.trailing_stop_pips;
|
|
test_params.profit_target_percent = s2_current_params.profit_target_percent;
|
|
test_params.max_bars = s2_current_params.max_bars;
|
|
test_params.min_bars = s2_current_params.min_bars;
|
|
test_params.exit_on_reversal = s2_current_params.exit_on_reversal;
|
|
|
|
// Backtest this parameter set
|
|
double profitability = BacktestMAStrategy(test_params, start_time, end_time);
|
|
tests_run++;
|
|
|
|
if(profitability > best_profitability)
|
|
{
|
|
best_profitability = profitability;
|
|
best_params = test_params;
|
|
best_params.profitability = profitability;
|
|
Print("MA Grid Search: NEW BEST Fast=", ma_fast, " Slow=", ma_slow,
|
|
" Profit=", DoubleToString(profitability, 2), "%");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update parameters if new ones are better
|
|
// Be more aggressive when losing money
|
|
bool should_update = false;
|
|
bool is_losing = s2_current_params.profitability < -0.5; // Losing more than 0.5%
|
|
|
|
Print("MA Optimization Results: Tests Run=", tests_run, " Current=", DoubleToString(s2_current_params.profitability, 2),
|
|
"% Best Found=", DoubleToString(best_profitability, 2), "%");
|
|
|
|
if(s2_random_mode)
|
|
{
|
|
// In random mode, accept if it's better OR if current is very bad
|
|
should_update = (best_profitability > s2_current_params.profitability) ||
|
|
(best_profitability > -5.0 && s2_current_params.profitability < -10.0);
|
|
}
|
|
else if(is_losing)
|
|
{
|
|
// When losing, be more aggressive - accept any improvement or if backtest shows positive
|
|
should_update = (best_profitability > s2_current_params.profitability) ||
|
|
(best_profitability > 0.1); // Accept if backtest shows any positive result
|
|
Print("MA Strategy: LOSING MODE - Accepting improvements more aggressively");
|
|
}
|
|
else
|
|
{
|
|
should_update = (best_profitability > s2_current_params.profitability + 0.1) ||
|
|
(best_profitability >= MinProfitabilityForKeep && s2_current_params.profitability < MinProfitabilityForKeep);
|
|
}
|
|
|
|
if(should_update)
|
|
{
|
|
if(EnableForkSystem && !s2_fork_active)
|
|
{
|
|
// Create fork instead of immediately updating
|
|
Print("MA Strategy: Creating FORK with new parameters. Current Profit: ",
|
|
DoubleToString(s2_current_params.profitability, 2),
|
|
"% Fork Profit (backtest): ", DoubleToString(best_profitability, 2), "%");
|
|
|
|
s2_fork_params = best_params;
|
|
s2_fork_active = true;
|
|
s2_fork_start_time = TimeCurrent();
|
|
s2_fork_start_bars = Bars(TradingSymbol, TimeFrame);
|
|
s2_fork_start_profit = s2_current_params.profitability;
|
|
|
|
// Create fork indicators
|
|
s2_fork_ma_fast_handle = iMA(TradingSymbol, TimeFrame, s2_fork_params.ma_fast, 0, s2_fork_params.ma_method, PRICE_CLOSE);
|
|
s2_fork_ma_slow_handle = iMA(TradingSymbol, TimeFrame, s2_fork_params.ma_slow, 0, s2_fork_params.ma_method, PRICE_CLOSE);
|
|
s2_fork_atr_handle = iATR(TradingSymbol, TimeFrame, ATR_Period);
|
|
if(s2_fork_ma_fast_handle == INVALID_HANDLE || s2_fork_ma_slow_handle == INVALID_HANDLE || s2_fork_atr_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to create fork MA indicators");
|
|
if(s2_fork_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_fast_handle);
|
|
if(s2_fork_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_slow_handle);
|
|
if(s2_fork_atr_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_atr_handle);
|
|
s2_fork_active = false;
|
|
}
|
|
else
|
|
{
|
|
Print("FORK CREATED: Fast MA=", s2_fork_params.ma_fast,
|
|
" Slow MA=", s2_fork_params.ma_slow);
|
|
}
|
|
}
|
|
else if(!EnableForkSystem)
|
|
{
|
|
// Direct update if fork system disabled
|
|
Print("MA Strategy: Updating parameters. Old Profit: ", DoubleToString(s2_current_params.profitability, 2),
|
|
"% New Profit: ", DoubleToString(best_profitability, 2), "%");
|
|
|
|
s2_current_params = best_params;
|
|
s2_consecutive_losses = 0; // Reset on improvement
|
|
|
|
// Recreate indicators with new parameters
|
|
if(s2_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_fast_handle);
|
|
if(s2_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_slow_handle);
|
|
|
|
s2_ma_fast_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_fast, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
s2_ma_slow_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_slow, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
|
|
if(s2_ma_fast_handle == INVALID_HANDLE || s2_ma_slow_handle == INVALID_HANDLE)
|
|
{
|
|
Print("ERROR: Failed to recreate MA indicators with new parameters");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Print("MA Strategy: Fork already active, skipping new fork creation");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Print("MA Strategy: Keeping current parameters. Current Profit: ",
|
|
DoubleToString(s2_current_params.profitability, 2), "% Best Found: ",
|
|
DoubleToString(best_profitability, 2), "%");
|
|
|
|
// Track if we're still losing
|
|
if(s2_current_params.profitability < 0)
|
|
s2_consecutive_losses++;
|
|
else
|
|
s2_consecutive_losses = 0;
|
|
}
|
|
|
|
s2_last_profitability = s2_current_params.profitability;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Backtest MA Crossover Strategy |
|
|
//+------------------------------------------------------------------+
|
|
double BacktestMAStrategy(MAStrategyParams ¶ms, datetime start_time, datetime end_time)
|
|
{
|
|
// Create temporary MA indicators for backtesting
|
|
int temp_ma_fast = iMA(TradingSymbol, TimeFrame, params.ma_fast, 0, params.ma_method, PRICE_CLOSE);
|
|
int temp_ma_slow = iMA(TradingSymbol, TimeFrame, params.ma_slow, 0, params.ma_method, PRICE_CLOSE);
|
|
|
|
if(temp_ma_fast == INVALID_HANDLE || temp_ma_slow == INVALID_HANDLE)
|
|
{
|
|
if(temp_ma_fast != INVALID_HANDLE) IndicatorRelease(temp_ma_fast);
|
|
if(temp_ma_slow != INVALID_HANDLE) IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
|
|
double total_profit = 0.0;
|
|
int total_trades = 0;
|
|
int winning_trades = 0;
|
|
ulong virtual_position = 0;
|
|
double virtual_entry = 0;
|
|
datetime virtual_entry_time = 0;
|
|
ENUM_POSITION_TYPE virtual_position_type = WRONG_VALUE;
|
|
|
|
// Calculate how many bars we need based on time period
|
|
int period_seconds = PeriodSeconds(TimeFrame);
|
|
int bars_needed = (int)((end_time - start_time) / period_seconds) + 20; // Add buffer for indicators
|
|
|
|
// Get bars from end_time going backwards
|
|
// end_bar should be 0 (current bar) or close to it
|
|
int end_bar = iBarShift(TradingSymbol, TimeFrame, end_time, false);
|
|
if(end_bar < 0) end_bar = 0; // Use current bar if not found
|
|
|
|
// Calculate start_bar by going backwards from end_bar
|
|
int start_bar = end_bar + bars_needed;
|
|
int max_bars = Bars(TradingSymbol, TimeFrame);
|
|
if(start_bar >= max_bars)
|
|
{
|
|
start_bar = max_bars - 1;
|
|
bars_needed = start_bar - end_bar; // Adjust to available bars
|
|
}
|
|
|
|
// Verify we have enough bars
|
|
int bars_to_test = start_bar - end_bar;
|
|
Print("MA Backtest: Bars needed: ", bars_needed, " Bars available: ", bars_to_test,
|
|
" (start_bar=", start_bar, " end_bar=", end_bar, ") Period: ",
|
|
TimeToString(start_time), " to ", TimeToString(end_time));
|
|
|
|
if(bars_to_test < 5) // Reduced minimum for shorter periods
|
|
{
|
|
Print("MA Backtest: Not enough bars (need 5, have ", bars_to_test, ")");
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
|
|
// Get data arrays
|
|
double ma_fast_buffer[];
|
|
double ma_slow_buffer[];
|
|
double close_buffer[];
|
|
datetime time_buffer[];
|
|
ArraySetAsSeries(ma_fast_buffer, true);
|
|
ArraySetAsSeries(ma_slow_buffer, true);
|
|
ArraySetAsSeries(close_buffer, true);
|
|
ArraySetAsSeries(time_buffer, true);
|
|
|
|
// Copy data from end_bar to start_bar (oldest to newest)
|
|
// With ArraySetAsSeries(true), index 0 = most recent, higher index = older
|
|
if(CopyBuffer(temp_ma_fast, 0, end_bar, bars_to_test, ma_fast_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
if(CopyBuffer(temp_ma_slow, 0, end_bar, bars_to_test, ma_slow_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
if(CopyClose(TradingSymbol, TimeFrame, end_bar, bars_to_test, close_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
if(CopyTime(TradingSymbol, TimeFrame, end_bar, bars_to_test, time_buffer) < bars_to_test)
|
|
{
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
return -999999.0;
|
|
}
|
|
|
|
double point = SymbolInfoDouble(TradingSymbol, SYMBOL_POINT);
|
|
double pip = (SymbolInfoInteger(TradingSymbol, SYMBOL_DIGITS) == 3 ||
|
|
SymbolInfoInteger(TradingSymbol, SYMBOL_DIGITS) == 5) ? point * 10 : point;
|
|
// period_seconds already declared above
|
|
|
|
// Iterate through historical bars (from oldest to newest)
|
|
// With ArraySetAsSeries(true), index 0 = newest, higher index = older
|
|
// So we iterate from highest index (oldest) down to 1 (newest)
|
|
for(int i = bars_to_test - 1; i >= 1; i--) // Start from oldest, need previous bar
|
|
{
|
|
datetime bar_time = time_buffer[i];
|
|
double current_ma_fast = ma_fast_buffer[i];
|
|
double prev_ma_fast = ma_fast_buffer[i-1]; // i-1 is more recent than i
|
|
double current_ma_slow = ma_slow_buffer[i];
|
|
double prev_ma_slow = ma_slow_buffer[i-1]; // i-1 is more recent than i
|
|
double current_price = close_buffer[i];
|
|
|
|
// Check existing virtual position
|
|
if(virtual_position > 0)
|
|
{
|
|
// Check exit conditions
|
|
int bars_held = (int)((bar_time - virtual_entry_time) / period_seconds);
|
|
if(bars_held >= params.max_bars)
|
|
{
|
|
// Time-based exit
|
|
double exit_price = current_price;
|
|
double profit = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit = (exit_price - virtual_entry) / virtual_entry;
|
|
else
|
|
profit = (virtual_entry - exit_price) / virtual_entry;
|
|
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
|
|
virtual_position = 0;
|
|
}
|
|
else
|
|
{
|
|
// Check profit target
|
|
double profit_pct = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit_pct = ((current_price - virtual_entry) / virtual_entry) * 100.0;
|
|
else
|
|
profit_pct = ((virtual_entry - current_price) / virtual_entry) * 100.0;
|
|
|
|
// Profit target removed - using other exit conditions only
|
|
if(false) // Disabled
|
|
{
|
|
double profit = profit_pct / 100.0;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
|
|
// Check price crosses back over MA (trend reversal)
|
|
if(virtual_position_type == POSITION_TYPE_BUY && current_price < current_ma_slow)
|
|
{
|
|
double profit = (current_price - virtual_entry) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
else if(virtual_position_type == POSITION_TYPE_SELL && current_price > current_ma_slow)
|
|
{
|
|
double profit = (virtual_entry - current_price) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
|
|
// Check opposite crossover (signal reversal)
|
|
if(params.exit_on_reversal)
|
|
{
|
|
bool bearish_cross = (current_ma_fast < current_ma_slow && prev_ma_fast >= prev_ma_slow);
|
|
bool bullish_cross = (current_ma_fast > current_ma_slow && prev_ma_fast <= prev_ma_slow);
|
|
|
|
if(virtual_position_type == POSITION_TYPE_BUY && bearish_cross)
|
|
{
|
|
double profit = (current_price - virtual_entry) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
else if(virtual_position_type == POSITION_TYPE_SELL && bullish_cross)
|
|
{
|
|
double profit = (virtual_entry - current_price) / virtual_entry;
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
virtual_position = 0;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals (MA Crossover)
|
|
if(virtual_position == 0)
|
|
{
|
|
// Bullish crossover: Fast MA crosses above Slow MA
|
|
bool bullish_cross = (current_ma_fast > current_ma_slow && prev_ma_fast <= prev_ma_slow);
|
|
// Bearish crossover: Fast MA crosses below Slow MA
|
|
bool bearish_cross = (current_ma_fast < current_ma_slow && prev_ma_fast >= prev_ma_slow);
|
|
|
|
if(bullish_cross)
|
|
{
|
|
virtual_position = 1;
|
|
virtual_entry = current_price;
|
|
virtual_entry_time = bar_time;
|
|
virtual_position_type = POSITION_TYPE_BUY;
|
|
}
|
|
else if(bearish_cross)
|
|
{
|
|
virtual_position = 2;
|
|
virtual_entry = current_price;
|
|
virtual_entry_time = bar_time;
|
|
virtual_position_type = POSITION_TYPE_SELL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Close any remaining position
|
|
if(virtual_position > 0)
|
|
{
|
|
double exit_price = close_buffer[0];
|
|
double profit = 0;
|
|
if(virtual_position_type == POSITION_TYPE_BUY)
|
|
profit = (exit_price - virtual_entry) / virtual_entry;
|
|
else
|
|
profit = (virtual_entry - exit_price) / virtual_entry;
|
|
|
|
total_profit += profit;
|
|
total_trades++;
|
|
if(profit > 0) winning_trades++;
|
|
}
|
|
|
|
IndicatorRelease(temp_ma_fast);
|
|
IndicatorRelease(temp_ma_slow);
|
|
|
|
// Calculate profitability percentage
|
|
if(total_trades >= MinTradesForOptimization)
|
|
{
|
|
params.total_trades = total_trades;
|
|
params.winning_trades = winning_trades;
|
|
params.net_profit = total_profit;
|
|
double profitability = total_profit * 100.0;
|
|
Print("MA Backtest: Trades=", total_trades, " Wins=", winning_trades,
|
|
" Profit=", DoubleToString(profitability, 2), "%");
|
|
// Return profitability as percentage (total_profit is already a ratio)
|
|
return profitability;
|
|
}
|
|
|
|
// Return a very negative value if not enough trades
|
|
if(total_trades > 0)
|
|
{
|
|
// Some trades found but not enough - return a scaled negative value
|
|
Print("MA Backtest: Only ", total_trades, " trades found (need ", MinTradesForOptimization, ")");
|
|
return -1000.0 - (MinTradesForOptimization - total_trades);
|
|
}
|
|
|
|
Print("MA Backtest: NO TRADES FOUND");
|
|
return -999999.0; // No trades found
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Normalize Stop Loss and Take Profit levels |
|
|
//+------------------------------------------------------------------+
|
|
bool NormalizeStops(string symbol, double entry_price, ENUM_ORDER_TYPE order_type, double &sl, double &tp)
|
|
{
|
|
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
|
double pip = (digits == 3 || digits == 5) ? point * 10 : point;
|
|
|
|
// Get minimum stop level from broker
|
|
long min_stop_level = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
|
|
double min_stop_distance = min_stop_level * point;
|
|
|
|
// Calculate safe minimum distance (0.1% of price or 50 points, whichever is larger)
|
|
double safe_min_distance = MathMax(entry_price * 0.001, 50 * point);
|
|
double required_distance = MathMax(min_stop_distance, safe_min_distance);
|
|
|
|
// Add a small buffer to prevent rounding issues
|
|
double buffer = required_distance * 0.1;
|
|
required_distance += buffer;
|
|
|
|
// Normalize to correct number of digits
|
|
required_distance = NormalizeDouble(required_distance, digits);
|
|
|
|
// Adjust stop loss and take profit based on order type
|
|
if(order_type == ORDER_TYPE_BUY)
|
|
{
|
|
// For buy: SL below entry, TP above entry
|
|
if(sl > 0 && sl >= entry_price - required_distance)
|
|
sl = NormalizeDouble(entry_price - required_distance, digits);
|
|
|
|
if(tp > 0 && tp <= entry_price + required_distance)
|
|
tp = NormalizeDouble(entry_price + required_distance, digits);
|
|
|
|
// Validate SL is below entry and TP is above entry
|
|
if(sl > 0 && sl >= entry_price) return false;
|
|
if(tp > 0 && tp <= entry_price) return false;
|
|
|
|
// Ensure SL and TP are far enough apart
|
|
if(sl > 0 && tp > 0 && (tp - sl) < required_distance * 2) return false;
|
|
}
|
|
else // ORDER_TYPE_SELL
|
|
{
|
|
// For sell: SL above entry, TP below entry
|
|
if(sl > 0 && sl <= entry_price + required_distance)
|
|
sl = NormalizeDouble(entry_price + required_distance, digits);
|
|
|
|
if(tp > 0 && tp >= entry_price - required_distance)
|
|
tp = NormalizeDouble(entry_price - required_distance, digits);
|
|
|
|
// Validate SL is above entry and TP is below entry
|
|
if(sl > 0 && sl <= entry_price) return false;
|
|
if(tp > 0 && tp >= entry_price) return false;
|
|
|
|
// Ensure SL and TP are far enough apart
|
|
if(sl > 0 && tp > 0 && (sl - tp) < required_distance * 2) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Run RSI Reversion Strategy |
|
|
//+------------------------------------------------------------------+
|
|
void RunRSIStrategy()
|
|
{
|
|
trade.SetExpertMagicNumber(s1_magic);
|
|
|
|
datetime current_bar = iTime(TradingSymbol, TimeFrame, 0);
|
|
if(current_bar == s1_last_bar) return;
|
|
s1_last_bar = current_bar;
|
|
|
|
// Get RSI data
|
|
double rsi_buffer[];
|
|
ArraySetAsSeries(rsi_buffer, true);
|
|
if(CopyBuffer(s1_rsi_handle, 0, 0, 3, rsi_buffer) < 3) return;
|
|
|
|
// Check existing position for smart exits
|
|
if(s1_position_ticket > 0)
|
|
{
|
|
if(!PositionSelectByTicket(s1_position_ticket))
|
|
{
|
|
s1_position_ticket = 0;
|
|
}
|
|
else
|
|
{
|
|
// Get position details
|
|
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
datetime position_open_time = (datetime)PositionGetInteger(POSITION_TIME);
|
|
double current_price = (position_type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_BID) :
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
|
|
// Calculate bars held
|
|
int bars_held = (int)((current_bar - position_open_time) / PeriodSeconds(TimeFrame));
|
|
|
|
// 1. Time-based exit (max bars)
|
|
if(bars_held >= s1_current_params.max_bars)
|
|
{
|
|
trade.PositionClose(s1_position_ticket);
|
|
s1_position_ticket = 0;
|
|
return;
|
|
}
|
|
|
|
// Minimum hold time - don't exit too early
|
|
if(bars_held < s1_current_params.min_bars)
|
|
{
|
|
return; // Don't check other exit conditions if minimum bars not reached
|
|
}
|
|
|
|
// 2. Signal reversal exit (if enabled)
|
|
if(s1_current_params.exit_on_reversal)
|
|
{
|
|
if(position_type == POSITION_TYPE_BUY)
|
|
{
|
|
// Exit buy if RSI crosses below oversold (reversal signal)
|
|
if(rsi_buffer[0] < s1_current_params.rsi_oversold && rsi_buffer[1] >= s1_current_params.rsi_oversold)
|
|
{
|
|
trade.PositionClose(s1_position_ticket);
|
|
s1_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
else // SELL
|
|
{
|
|
// Exit sell if RSI crosses above overbought (reversal signal)
|
|
if(rsi_buffer[0] > s1_current_params.rsi_overbought && rsi_buffer[1] <= s1_current_params.rsi_overbought)
|
|
{
|
|
trade.PositionClose(s1_position_ticket);
|
|
s1_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. RSI extreme exit (exit when RSI reaches opposite extreme)
|
|
{
|
|
if(position_type == POSITION_TYPE_BUY && rsi_buffer[0] >= s1_current_params.rsi_overbought)
|
|
{
|
|
// Bought from oversold, exit when reaches overbought
|
|
trade.PositionClose(s1_position_ticket);
|
|
s1_position_ticket = 0;
|
|
return;
|
|
}
|
|
else if(position_type == POSITION_TYPE_SELL && rsi_buffer[0] <= s1_current_params.rsi_oversold)
|
|
{
|
|
// Sold from overbought, exit when reaches oversold
|
|
trade.PositionClose(s1_position_ticket);
|
|
s1_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals
|
|
if(s1_position_ticket == 0)
|
|
{
|
|
// Buy signal: RSI crosses above oversold
|
|
if(rsi_buffer[0] > s1_current_params.rsi_oversold && rsi_buffer[1] <= s1_current_params.rsi_oversold)
|
|
{
|
|
double ask = SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
if(trade.Buy(LotSize, TradingSymbol, 0, 0, 0, "S1: RSI Reversion"))
|
|
{
|
|
// Find the position ticket
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s1_magic)
|
|
{
|
|
s1_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Sell signal: RSI crosses below overbought
|
|
else if(rsi_buffer[0] < s1_current_params.rsi_overbought && rsi_buffer[1] >= s1_current_params.rsi_overbought)
|
|
{
|
|
double bid = SymbolInfoDouble(TradingSymbol, SYMBOL_BID);
|
|
if(trade.Sell(LotSize, TradingSymbol, 0, 0, 0, "S1: RSI Reversion"))
|
|
{
|
|
// Find the position ticket
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s1_magic)
|
|
{
|
|
s1_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Run MA Crossover Strategy |
|
|
//+------------------------------------------------------------------+
|
|
void RunMAStrategy()
|
|
{
|
|
trade.SetExpertMagicNumber(s2_magic);
|
|
|
|
datetime current_bar = iTime(TradingSymbol, TimeFrame, 0);
|
|
if(current_bar == s2_last_bar) return;
|
|
s2_last_bar = current_bar;
|
|
|
|
// Get MA data
|
|
double ma_fast_buffer[];
|
|
double ma_slow_buffer[];
|
|
ArraySetAsSeries(ma_fast_buffer, true);
|
|
ArraySetAsSeries(ma_slow_buffer, true);
|
|
if(CopyBuffer(s2_ma_fast_handle, 0, 0, 3, ma_fast_buffer) < 3) return;
|
|
if(CopyBuffer(s2_ma_slow_handle, 0, 0, 3, ma_slow_buffer) < 3) return;
|
|
|
|
// Check existing position for smart exits
|
|
if(s2_position_ticket > 0)
|
|
{
|
|
if(!PositionSelectByTicket(s2_position_ticket))
|
|
{
|
|
s2_position_ticket = 0;
|
|
}
|
|
else
|
|
{
|
|
// Get position details
|
|
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
datetime position_open_time = (datetime)PositionGetInteger(POSITION_TIME);
|
|
double current_price = (position_type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_BID) :
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
|
|
// Calculate bars held
|
|
int bars_held = (int)((current_bar - position_open_time) / PeriodSeconds(TimeFrame));
|
|
|
|
// 1. Time-based exit (max bars)
|
|
if(bars_held >= s2_current_params.max_bars)
|
|
{
|
|
trade.PositionClose(s2_position_ticket);
|
|
s2_position_ticket = 0;
|
|
return;
|
|
}
|
|
|
|
// Minimum hold time - don't exit too early
|
|
if(bars_held < s2_current_params.min_bars)
|
|
{
|
|
return; // Don't check other exit conditions if minimum bars not reached
|
|
}
|
|
|
|
// 2. Signal reversal exit (opposite crossover)
|
|
if(s2_current_params.exit_on_reversal)
|
|
{
|
|
bool bearish_cross = (ma_fast_buffer[0] < ma_slow_buffer[0] && ma_fast_buffer[1] >= ma_slow_buffer[1]);
|
|
bool bullish_cross = (ma_fast_buffer[0] > ma_slow_buffer[0] && ma_fast_buffer[1] <= ma_slow_buffer[1]);
|
|
|
|
if(position_type == POSITION_TYPE_BUY && bearish_cross)
|
|
{
|
|
// Exit buy on bearish crossover
|
|
trade.PositionClose(s2_position_ticket);
|
|
s2_position_ticket = 0;
|
|
return;
|
|
}
|
|
else if(position_type == POSITION_TYPE_SELL && bullish_cross)
|
|
{
|
|
// Exit sell on bullish crossover
|
|
trade.PositionClose(s2_position_ticket);
|
|
s2_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 3. Price crosses back over MA (trend reversal)
|
|
{
|
|
if(position_type == POSITION_TYPE_BUY)
|
|
{
|
|
// Exit if price crosses below slow MA (trend reversal)
|
|
if(current_price < ma_slow_buffer[0])
|
|
{
|
|
trade.PositionClose(s2_position_ticket);
|
|
s2_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
else // SELL
|
|
{
|
|
// Exit if price crosses above slow MA (trend reversal)
|
|
if(current_price > ma_slow_buffer[0])
|
|
{
|
|
trade.PositionClose(s2_position_ticket);
|
|
s2_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals
|
|
if(s2_position_ticket == 0)
|
|
{
|
|
// Bullish crossover: Fast MA crosses above Slow MA
|
|
bool bullish_cross = (ma_fast_buffer[0] > ma_slow_buffer[0] && ma_fast_buffer[1] <= ma_slow_buffer[1]);
|
|
// Bearish crossover: Fast MA crosses below Slow MA
|
|
bool bearish_cross = (ma_fast_buffer[0] < ma_slow_buffer[0] && ma_fast_buffer[1] >= ma_slow_buffer[1]);
|
|
|
|
if(bullish_cross)
|
|
{
|
|
double ask = SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
if(trade.Buy(LotSize, TradingSymbol, 0, 0, 0, "S2: MA Crossover"))
|
|
{
|
|
// Find the position ticket
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s2_magic)
|
|
{
|
|
s2_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if(bearish_cross)
|
|
{
|
|
double bid = SymbolInfoDouble(TradingSymbol, SYMBOL_BID);
|
|
if(trade.Sell(LotSize, TradingSymbol, 0, 0, 0, "S2: MA Crossover"))
|
|
{
|
|
// Find the position ticket
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s2_magic)
|
|
{
|
|
s2_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if fork should be merged |
|
|
//+------------------------------------------------------------------+
|
|
void CheckForkMerge(int strategy_num)
|
|
{
|
|
if(strategy_num == 1 && s1_fork_active)
|
|
{
|
|
int current_bars = Bars(TradingSymbol, TimeFrame);
|
|
int bars_tested = current_bars - s1_fork_start_bars;
|
|
|
|
if(bars_tested >= ForkTestBars)
|
|
{
|
|
// Calculate current profitability for both original and fork
|
|
double original_profit = CalculateStrategyProfitability(s1_magic);
|
|
double fork_profit = CalculateStrategyProfitability(s1_fork_magic);
|
|
|
|
double improvement = fork_profit - s1_fork_start_profit;
|
|
double original_change = original_profit - s1_fork_start_profit;
|
|
|
|
Print("=== Strategy 1 Fork Evaluation ===");
|
|
Print("Original: Start=", DoubleToString(s1_fork_start_profit, 2),
|
|
"% Current=", DoubleToString(original_profit, 2),
|
|
"% Change=", DoubleToString(original_change, 2), "%");
|
|
Print("Fork: Start=", DoubleToString(s1_fork_start_profit, 2),
|
|
"% Current=", DoubleToString(fork_profit, 2),
|
|
"% Change=", DoubleToString(improvement, 2), "%");
|
|
Print("Bars tested: ", bars_tested, " / ", ForkTestBars);
|
|
|
|
// Merge if fork is better by minimum improvement threshold
|
|
if(improvement > original_change + ForkMinImprovement)
|
|
{
|
|
Print("=== MERGING Strategy 1 Fork ===");
|
|
Print("Fork improvement (", DoubleToString(improvement, 2),
|
|
"%) exceeds original (", DoubleToString(original_change, 2),
|
|
"%) by ", DoubleToString(improvement - original_change, 2), "%");
|
|
|
|
// Close all fork positions
|
|
CloseAllPositions(s1_fork_magic);
|
|
|
|
// Switch to fork parameters
|
|
s1_current_params = s1_fork_params;
|
|
s1_current_params.profitability = fork_profit;
|
|
|
|
// Recreate indicators with fork parameters
|
|
if(s1_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_rsi_handle);
|
|
s1_rsi_handle = iRSI(TradingSymbol, TimeFrame, s1_current_params.rsi_period, PRICE_CLOSE);
|
|
|
|
// Clean up fork
|
|
if(s1_fork_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_rsi_handle);
|
|
s1_fork_active = false;
|
|
s1_fork_rsi_handle = INVALID_HANDLE;
|
|
s1_fork_position_ticket = 0;
|
|
|
|
Print("MERGE COMPLETE: Now using fork parameters");
|
|
}
|
|
else
|
|
{
|
|
Print("=== DISCARDING Strategy 1 Fork ===");
|
|
Print("Fork did not improve enough. Keeping original parameters.");
|
|
|
|
// Close all fork positions
|
|
CloseAllPositions(s1_fork_magic);
|
|
|
|
// Clean up fork
|
|
if(s1_fork_rsi_handle != INVALID_HANDLE) IndicatorRelease(s1_fork_rsi_handle);
|
|
s1_fork_active = false;
|
|
s1_fork_rsi_handle = INVALID_HANDLE;
|
|
s1_fork_position_ticket = 0;
|
|
}
|
|
}
|
|
}
|
|
else if(strategy_num == 2 && s2_fork_active)
|
|
{
|
|
int current_bars = Bars(TradingSymbol, TimeFrame);
|
|
int bars_tested = current_bars - s2_fork_start_bars;
|
|
|
|
if(bars_tested >= ForkTestBars)
|
|
{
|
|
// Calculate current profitability for both original and fork
|
|
double original_profit = CalculateStrategyProfitability(s2_magic);
|
|
double fork_profit = CalculateStrategyProfitability(s2_fork_magic);
|
|
|
|
double improvement = fork_profit - s2_fork_start_profit;
|
|
double original_change = original_profit - s2_fork_start_profit;
|
|
|
|
Print("=== Strategy 2 Fork Evaluation ===");
|
|
Print("Original: Start=", DoubleToString(s2_fork_start_profit, 2),
|
|
"% Current=", DoubleToString(original_profit, 2),
|
|
"% Change=", DoubleToString(original_change, 2), "%");
|
|
Print("Fork: Start=", DoubleToString(s2_fork_start_profit, 2),
|
|
"% Current=", DoubleToString(fork_profit, 2),
|
|
"% Change=", DoubleToString(improvement, 2), "%");
|
|
Print("Bars tested: ", bars_tested, " / ", ForkTestBars);
|
|
|
|
// Merge if fork is better by minimum improvement threshold
|
|
if(improvement > original_change + ForkMinImprovement)
|
|
{
|
|
Print("=== MERGING Strategy 2 Fork ===");
|
|
Print("Fork improvement (", DoubleToString(improvement, 2),
|
|
"%) exceeds original (", DoubleToString(original_change, 2),
|
|
"%) by ", DoubleToString(improvement - original_change, 2), "%");
|
|
|
|
// Close all fork positions
|
|
CloseAllPositions(s2_fork_magic);
|
|
|
|
// Switch to fork parameters
|
|
s2_current_params = s2_fork_params;
|
|
s2_current_params.profitability = fork_profit;
|
|
|
|
// Recreate indicators with fork parameters
|
|
if(s2_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_fast_handle);
|
|
if(s2_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_ma_slow_handle);
|
|
|
|
s2_ma_fast_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_fast, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
s2_ma_slow_handle = iMA(TradingSymbol, TimeFrame, s2_current_params.ma_slow, 0, s2_current_params.ma_method, PRICE_CLOSE);
|
|
|
|
// Clean up fork
|
|
if(s2_fork_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_fast_handle);
|
|
if(s2_fork_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_slow_handle);
|
|
s2_fork_active = false;
|
|
s2_fork_ma_fast_handle = INVALID_HANDLE;
|
|
s2_fork_ma_slow_handle = INVALID_HANDLE;
|
|
s2_fork_position_ticket = 0;
|
|
|
|
Print("MERGE COMPLETE: Now using fork parameters");
|
|
}
|
|
else
|
|
{
|
|
Print("=== DISCARDING Strategy 2 Fork ===");
|
|
Print("Fork did not improve enough. Keeping original parameters.");
|
|
|
|
// Close all fork positions
|
|
CloseAllPositions(s2_fork_magic);
|
|
|
|
// Clean up fork
|
|
if(s2_fork_ma_fast_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_fast_handle);
|
|
if(s2_fork_ma_slow_handle != INVALID_HANDLE) IndicatorRelease(s2_fork_ma_slow_handle);
|
|
s2_fork_active = false;
|
|
s2_fork_ma_fast_handle = INVALID_HANDLE;
|
|
s2_fork_ma_slow_handle = INVALID_HANDLE;
|
|
s2_fork_position_ticket = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate strategy profitability from positions |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateStrategyProfitability(int magic)
|
|
{
|
|
double total_profit = 0.0;
|
|
double account_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
// Get all positions for this magic number
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == magic)
|
|
{
|
|
total_profit += PositionGetDouble(POSITION_PROFIT);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also check closed deals (history)
|
|
HistorySelect(TimeCurrent() - 86400, TimeCurrent()); // Last 24 hours
|
|
int deals = HistoryDealsTotal();
|
|
for(int i = 0; i < deals; i++)
|
|
{
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) == TradingSymbol &&
|
|
HistoryDealGetInteger(ticket, DEAL_MAGIC) == magic)
|
|
{
|
|
total_profit += HistoryDealGetDouble(ticket, DEAL_PROFIT);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(account_balance > 0)
|
|
return (total_profit / account_balance) * 100.0;
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Close all positions for a magic number |
|
|
//+------------------------------------------------------------------+
|
|
void CloseAllPositions(int magic)
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == magic)
|
|
{
|
|
trade.PositionClose(ticket);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Run RSI Strategy Fork (parallel testing) |
|
|
//+------------------------------------------------------------------+
|
|
void RunRSIStrategyFork()
|
|
{
|
|
trade.SetExpertMagicNumber(s1_fork_magic);
|
|
|
|
datetime current_bar = iTime(TradingSymbol, TimeFrame, 0);
|
|
if(current_bar == s1_fork_last_bar) return;
|
|
s1_fork_last_bar = current_bar;
|
|
|
|
// Get RSI data
|
|
double rsi_buffer[];
|
|
ArraySetAsSeries(rsi_buffer, true);
|
|
if(CopyBuffer(s1_fork_rsi_handle, 0, 0, 3, rsi_buffer) < 3) return;
|
|
|
|
// Check existing fork position for smart exits (same logic as original)
|
|
if(s1_fork_position_ticket > 0)
|
|
{
|
|
if(!PositionSelectByTicket(s1_fork_position_ticket))
|
|
{
|
|
s1_fork_position_ticket = 0;
|
|
}
|
|
else
|
|
{
|
|
// Same exit logic as RunRSIStrategy but using fork params
|
|
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
datetime position_open_time = (datetime)PositionGetInteger(POSITION_TIME);
|
|
double current_price = (position_type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_BID) :
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
|
|
int bars_held = (int)((current_bar - position_open_time) / PeriodSeconds(TimeFrame));
|
|
if(bars_held >= s1_fork_params.max_bars)
|
|
{
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
|
|
if(bars_held < s1_fork_params.min_bars) return;
|
|
|
|
// Calculate current profit/loss
|
|
double profit_pct = 0;
|
|
if(position_type == POSITION_TYPE_BUY)
|
|
profit_pct = ((current_price - position_open_price) / position_open_price) * 100.0;
|
|
else
|
|
profit_pct = ((position_open_price - current_price) / position_open_price) * 100.0;
|
|
|
|
// LOSS PROTECTION: Maximum loss threshold
|
|
if(profit_pct <= -MaxLossPercent)
|
|
{
|
|
Print("FORK S1: Force exit - Max loss exceeded: ", DoubleToString(profit_pct, 2), "%");
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
s1_fork_entry_price = 0.0;
|
|
return;
|
|
}
|
|
|
|
// LOSS PROTECTION: Adverse move detection (strong move against position)
|
|
if(profit_pct < 0)
|
|
{
|
|
double adverse_move = MathAbs(profit_pct);
|
|
if(adverse_move >= AdverseMoveThreshold)
|
|
{
|
|
// Get ATR for volatility check
|
|
double atr_buffer[];
|
|
ArraySetAsSeries(atr_buffer, true);
|
|
if(CopyBuffer(s1_fork_atr_handle, 0, 0, 2, atr_buffer) >= 2)
|
|
{
|
|
double current_atr = atr_buffer[0];
|
|
double prev_atr = atr_buffer[1];
|
|
|
|
// Exit if adverse move exceeds threshold AND volatility is increasing
|
|
if(adverse_move >= AdverseMoveThreshold && current_atr > prev_atr * 1.2)
|
|
{
|
|
Print("FORK S1: Force exit - Adverse move detected: ", DoubleToString(profit_pct, 2),
|
|
"% | ATR increased: ", DoubleToString(prev_atr, 2), " -> ", DoubleToString(current_atr, 2));
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
s1_fork_entry_price = 0.0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(s1_fork_params.exit_on_reversal)
|
|
{
|
|
if(position_type == POSITION_TYPE_BUY)
|
|
{
|
|
if(rsi_buffer[0] < s1_fork_params.rsi_oversold && rsi_buffer[1] >= s1_fork_params.rsi_oversold)
|
|
{
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(rsi_buffer[0] > s1_fork_params.rsi_overbought && rsi_buffer[1] <= s1_fork_params.rsi_overbought)
|
|
{
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// RSI extreme exit (no profit requirement for fork)
|
|
if(position_type == POSITION_TYPE_BUY && rsi_buffer[0] >= s1_fork_params.rsi_overbought)
|
|
{
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
else if(position_type == POSITION_TYPE_SELL && rsi_buffer[0] <= s1_fork_params.rsi_oversold)
|
|
{
|
|
trade.PositionClose(s1_fork_position_ticket);
|
|
s1_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals (using fork params)
|
|
if(s1_fork_position_ticket == 0)
|
|
{
|
|
if(rsi_buffer[0] > s1_fork_params.rsi_oversold && rsi_buffer[1] <= s1_fork_params.rsi_oversold)
|
|
{
|
|
double ask = SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
if(trade.Buy(LotSize, TradingSymbol, 0, 0, 0, "S1 Fork: RSI Reversion"))
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s1_fork_magic)
|
|
{
|
|
s1_fork_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
s1_fork_entry_price = ask;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if(rsi_buffer[0] < s1_fork_params.rsi_overbought && rsi_buffer[1] >= s1_fork_params.rsi_overbought)
|
|
{
|
|
double bid = SymbolInfoDouble(TradingSymbol, SYMBOL_BID);
|
|
if(trade.Sell(LotSize, TradingSymbol, 0, 0, 0, "S1 Fork: RSI Reversion"))
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s1_fork_magic)
|
|
{
|
|
s1_fork_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
s1_fork_entry_price = bid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Run MA Strategy Fork (parallel testing) |
|
|
//+------------------------------------------------------------------+
|
|
void RunMAStrategyFork()
|
|
{
|
|
trade.SetExpertMagicNumber(s2_fork_magic);
|
|
|
|
datetime current_bar = iTime(TradingSymbol, TimeFrame, 0);
|
|
if(current_bar == s2_fork_last_bar) return;
|
|
s2_fork_last_bar = current_bar;
|
|
|
|
// Get MA data
|
|
double ma_fast_buffer[];
|
|
double ma_slow_buffer[];
|
|
ArraySetAsSeries(ma_fast_buffer, true);
|
|
ArraySetAsSeries(ma_slow_buffer, true);
|
|
if(CopyBuffer(s2_fork_ma_fast_handle, 0, 0, 3, ma_fast_buffer) < 3) return;
|
|
if(CopyBuffer(s2_fork_ma_slow_handle, 0, 0, 3, ma_slow_buffer) < 3) return;
|
|
|
|
// Check existing fork position for smart exits (same logic as original)
|
|
if(s2_fork_position_ticket > 0)
|
|
{
|
|
if(!PositionSelectByTicket(s2_fork_position_ticket))
|
|
{
|
|
s2_fork_position_ticket = 0;
|
|
}
|
|
else
|
|
{
|
|
// Same exit logic as RunMAStrategy but using fork params
|
|
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
datetime position_open_time = (datetime)PositionGetInteger(POSITION_TIME);
|
|
double current_price = (position_type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_BID) :
|
|
SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
|
|
int bars_held = (int)((current_bar - position_open_time) / PeriodSeconds(TimeFrame));
|
|
if(bars_held >= s2_fork_params.max_bars)
|
|
{
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
|
|
if(bars_held < s2_fork_params.min_bars) return;
|
|
|
|
// Calculate current profit/loss
|
|
double profit_pct = 0;
|
|
if(position_type == POSITION_TYPE_BUY)
|
|
profit_pct = ((current_price - position_open_price) / position_open_price) * 100.0;
|
|
else
|
|
profit_pct = ((position_open_price - current_price) / position_open_price) * 100.0;
|
|
|
|
// LOSS PROTECTION: Maximum loss threshold
|
|
if(profit_pct <= -MaxLossPercent)
|
|
{
|
|
Print("FORK S2: Force exit - Max loss exceeded: ", DoubleToString(profit_pct, 2), "%");
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
s2_fork_entry_price = 0.0;
|
|
return;
|
|
}
|
|
|
|
// LOSS PROTECTION: Adverse move detection (strong move against position)
|
|
if(profit_pct < 0)
|
|
{
|
|
double adverse_move = MathAbs(profit_pct);
|
|
if(adverse_move >= AdverseMoveThreshold)
|
|
{
|
|
// Get ATR for volatility check
|
|
double atr_buffer[];
|
|
ArraySetAsSeries(atr_buffer, true);
|
|
if(CopyBuffer(s2_fork_atr_handle, 0, 0, 2, atr_buffer) >= 2)
|
|
{
|
|
double current_atr = atr_buffer[0];
|
|
double prev_atr = atr_buffer[1];
|
|
|
|
// Exit if adverse move exceeds threshold AND volatility is increasing
|
|
if(adverse_move >= AdverseMoveThreshold && current_atr > prev_atr * 1.2)
|
|
{
|
|
Print("FORK S2: Force exit - Adverse move detected: ", DoubleToString(profit_pct, 2),
|
|
"% | ATR increased: ", DoubleToString(prev_atr, 2), " -> ", DoubleToString(current_atr, 2));
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
s2_fork_entry_price = 0.0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(s2_fork_params.exit_on_reversal)
|
|
{
|
|
bool bearish_cross = (ma_fast_buffer[0] < ma_slow_buffer[0] && ma_fast_buffer[1] >= ma_slow_buffer[1]);
|
|
bool bullish_cross = (ma_fast_buffer[0] > ma_slow_buffer[0] && ma_fast_buffer[1] <= ma_slow_buffer[1]);
|
|
|
|
if(position_type == POSITION_TYPE_BUY && bearish_cross)
|
|
{
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
else if(position_type == POSITION_TYPE_SELL && bullish_cross)
|
|
{
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Price/MA reversal exit (no profit requirement for fork)
|
|
if(position_type == POSITION_TYPE_BUY && current_price < ma_slow_buffer[0])
|
|
{
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
else if(position_type == POSITION_TYPE_SELL && current_price > ma_slow_buffer[0])
|
|
{
|
|
trade.PositionClose(s2_fork_position_ticket);
|
|
s2_fork_position_ticket = 0;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for new entry signals (using fork params)
|
|
if(s2_fork_position_ticket == 0)
|
|
{
|
|
bool bullish_cross = (ma_fast_buffer[0] > ma_slow_buffer[0] && ma_fast_buffer[1] <= ma_slow_buffer[1]);
|
|
bool bearish_cross = (ma_fast_buffer[0] < ma_slow_buffer[0] && ma_fast_buffer[1] >= ma_slow_buffer[1]);
|
|
|
|
if(bullish_cross)
|
|
{
|
|
double ask = SymbolInfoDouble(TradingSymbol, SYMBOL_ASK);
|
|
if(trade.Buy(LotSize, TradingSymbol, 0, 0, 0, "S2 Fork: MA Crossover"))
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s2_fork_magic)
|
|
{
|
|
s2_fork_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
s2_fork_entry_price = ask;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else if(bearish_cross)
|
|
{
|
|
double bid = SymbolInfoDouble(TradingSymbol, SYMBOL_BID);
|
|
if(trade.Sell(LotSize, TradingSymbol, 0, 0, 0, "S2 Fork: MA Crossover"))
|
|
{
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(PositionGetTicket(i) > 0)
|
|
{
|
|
if(PositionGetString(POSITION_SYMBOL) == TradingSymbol &&
|
|
PositionGetInteger(POSITION_MAGIC) == s2_fork_magic)
|
|
{
|
|
s2_fork_position_ticket = PositionGetInteger(POSITION_TICKET);
|
|
s2_fork_entry_price = bid;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|