Finalize EA architecture before strategy development
This commit is contained in:
+254
-79
@@ -1,6 +1,6 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| RiskManager.mqh - Risk and position sizing management |
|
||||
//| Calculates lot sizes, validates parameters, checks trading hours |
|
||||
//| Calculates trade entry levels, stop loss, take profit and lot sizes|
|
||||
//+------------------------------------------------------------------+
|
||||
|
||||
#ifndef __RISKMANAGER_MQH__
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Logger.mqh"
|
||||
#include "MarketData.mqh"
|
||||
#include "Utilities.mqh"
|
||||
#include "Signal.mqh"
|
||||
|
||||
class CRiskManager
|
||||
{
|
||||
@@ -18,75 +19,186 @@ private:
|
||||
CLogger *mp_logger;
|
||||
|
||||
public:
|
||||
// Constructor
|
||||
CRiskManager(CMarketData *market_data, CLogger *logger)
|
||||
{
|
||||
mp_market_data = market_data;
|
||||
mp_logger = logger;
|
||||
}
|
||||
|
||||
// Calculate lot size based on configuration
|
||||
double CalculateLotSize(int stop_loss_points)
|
||||
double CalculateLotByRisk(double risk_distance)
|
||||
{
|
||||
double lot = 0.0;
|
||||
string symbol = mp_market_data.GetSymbol();
|
||||
if(!IsSymbolValid(symbol))
|
||||
return 0.0;
|
||||
if(risk_distance <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Risk distance must be positive to calculate lot size");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if(g_lot_mode == LOT_MODE_FIXED)
|
||||
{
|
||||
lot = g_fixed_lot;
|
||||
}
|
||||
else if(g_lot_mode == LOT_MODE_RISK)
|
||||
{
|
||||
lot = CalculateLotByRisk(stop_loss_points);
|
||||
}
|
||||
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
||||
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
||||
if(tick_size <= 0.0 || tick_value <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Symbol tick size or tick value invalid for lot calculation");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return ValidateLotSize(lot);
|
||||
double account_equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
if(account_equity <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Account equity is invalid for lot calculation");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double effective_risk_percent = GetEffectiveRiskPercent();
|
||||
if(effective_risk_percent <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Effective risk percent is invalid");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double risk_amount = account_equity * (effective_risk_percent / 100.0);
|
||||
|
||||
// Loss per single lot = (risk_distance / tick_size) * tick_value
|
||||
double loss_per_lot = (risk_distance / tick_size) * tick_value;
|
||||
if(loss_per_lot <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Calculated monetary loss per lot is non-positive");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double raw_lot = risk_amount / loss_per_lot;
|
||||
double normalized_lot = CUtilities::NormalizeLot(symbol, raw_lot);
|
||||
|
||||
if(normalized_lot <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("No valid lot size: raw=%.6f normalized=%.6f (min/max/step constraints)", raw_lot, normalized_lot));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if(mp_logger && g_debug_mode)
|
||||
{
|
||||
mp_logger.Info(StringFormat("Tick size=%.8f tick value=%.8f risk%%=%.2f rawLot=%.6f finalLot=%.6f",
|
||||
tick_size, tick_value, effective_risk_percent, raw_lot, normalized_lot));
|
||||
}
|
||||
|
||||
return normalized_lot;
|
||||
}
|
||||
|
||||
// Calculate lot size based on risk percent
|
||||
double CalculateLotByRisk(int stop_loss_points)
|
||||
bool ValidateTradeSetup(const TradeSetup &setup, double &lot)
|
||||
{
|
||||
double account_balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
||||
double stop_loss_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), stop_loss_points);
|
||||
double contract_size = CUtilities::GetContractSize(mp_market_data.GetSymbol());
|
||||
|
||||
if(stop_loss_distance == 0 || contract_size == 0)
|
||||
return g_min_lot;
|
||||
string symbol = mp_market_data.GetSymbol();
|
||||
if(!IsSymbolValid(symbol))
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Invalid symbol for trade setup validation");
|
||||
return false;
|
||||
}
|
||||
if(setup.signal != SIGNAL_BUY && setup.signal != SIGNAL_SELL)
|
||||
{
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Info("Trade setup rejected: no valid signal");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Risk = Account Balance * Risk Percent / 100
|
||||
double risk_amount = account_balance * (g_risk_percent / 100.0);
|
||||
|
||||
// Lot = Risk Amount / (SL Distance * Contract Size * Point)
|
||||
double point = mp_market_data.GetPoint();
|
||||
double lot = risk_amount / (stop_loss_distance * contract_size);
|
||||
|
||||
return lot;
|
||||
}
|
||||
double execution_price = (setup.signal == SIGNAL_BUY) ? mp_market_data.GetAsk() : mp_market_data.GetBid();
|
||||
double setup_entry_price = setup.entryPrice;
|
||||
double setup_diff = 0.0;
|
||||
if(setup_entry_price > 0.0)
|
||||
{
|
||||
setup_diff = MathAbs(setup_entry_price - execution_price);
|
||||
}
|
||||
|
||||
// Validate and normalize lot size
|
||||
double ValidateLotSize(double lot)
|
||||
{
|
||||
// Apply global limits first
|
||||
if(lot < g_min_lot)
|
||||
lot = g_min_lot;
|
||||
if(lot > g_max_lot)
|
||||
lot = g_max_lot;
|
||||
if(setup.stopLoss <= 0.0 || setup.takeProfit <= 0.0 || setup.riskDistance <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("Trade setup rejected: valid initial SL and TP are required (TP-less trades need a future explicit allowNoTakeProfit field). setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f risk=%.5f",
|
||||
setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit, setup.riskDistance));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Normalize to broker's lot step
|
||||
lot = CUtilities::NormalizeLot(mp_market_data.GetSymbol(), lot);
|
||||
bool valid_side = true;
|
||||
if(setup.signal == SIGNAL_BUY)
|
||||
{
|
||||
valid_side = (setup.stopLoss < execution_price && setup.takeProfit > execution_price);
|
||||
}
|
||||
else if(setup.signal == SIGNAL_SELL)
|
||||
{
|
||||
valid_side = (setup.stopLoss > execution_price && setup.takeProfit < execution_price);
|
||||
}
|
||||
|
||||
if(!valid_side)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("Trade setup rejected: SL/TP invalid for current market entry price (setupEntry=%.5f execEntry=%.5f sl=%.5f tp=%.5f)",
|
||||
setup_entry_price, execution_price, setup.stopLoss, setup.takeProfit));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Broker minimal stop distance relative to current market execution price
|
||||
double point = CUtilities::GetPoint(symbol);
|
||||
long min_stop_points = (long)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL);
|
||||
if(min_stop_points < 0)
|
||||
min_stop_points = 0;
|
||||
double min_stop_distance = min_stop_points * point;
|
||||
double actual_risk_distance = MathAbs(execution_price - setup.stopLoss);
|
||||
if(actual_risk_distance <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("Trade setup rejected: non-positive risk distance after using execution price (execEntry=%.5f sl=%.5f)",
|
||||
execution_price, setup.stopLoss));
|
||||
return false;
|
||||
}
|
||||
if(actual_risk_distance < min_stop_distance)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("Trade setup rejected: SL too close to current entry price (distance %.5f < broker stop level %.5f)", actual_risk_distance, min_stop_distance));
|
||||
return false;
|
||||
}
|
||||
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Info(StringFormat("Lot size calculated: %.2f", lot));
|
||||
{
|
||||
mp_logger.Info(StringFormat("Trade setup details: setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f setupRisk=%.5f execRisk=%.5f reason=%s",
|
||||
setup_entry_price, execution_price, setup_diff, setup.stopLoss, setup.takeProfit, setup.riskDistance, actual_risk_distance, setup.reason));
|
||||
}
|
||||
|
||||
return lot;
|
||||
// Lot sizing: use actual execution entry-to-SL distance in price units
|
||||
lot = CalculateLotByRisk(actual_risk_distance);
|
||||
if(lot <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning("Trade setup rejected: lot sizing failed or below broker minimum after execution price adjustment");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(mp_logger && g_debug_mode)
|
||||
{
|
||||
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
||||
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
||||
mp_logger.Info(StringFormat("Trade setup validated: signal=%s setupEntry=%.5f execEntry=%.5f diff=%.5f SL=%.5f TP=%.5f execRisk=%.5f tickSize=%.8f tickValue=%.8f riskUsed=%.2f finalLot=%.6f reason=%s",
|
||||
setup.signal == SIGNAL_BUY ? "BUY" : "SELL",
|
||||
setup_entry_price, execution_price, setup_diff,
|
||||
setup.stopLoss, setup.takeProfit, actual_risk_distance,
|
||||
tick_size, tick_value, GetEffectiveRiskPercent(), lot, setup.reason));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if spread is acceptable
|
||||
bool IsSpreadAcceptable()
|
||||
{
|
||||
int max_spread = g_max_spread_points;
|
||||
string symbol = mp_market_data.GetSymbol();
|
||||
|
||||
// XAUUSD/gold normally trades with a much wider spread than forex
|
||||
if(StringFind(symbol, "XAU") >= 0 || StringFind(symbol, "GOLD") >= 0)
|
||||
{
|
||||
max_spread = MathMax(max_spread, 200);
|
||||
@@ -95,7 +207,6 @@ public:
|
||||
return mp_market_data.IsSpreadAcceptable(max_spread);
|
||||
}
|
||||
|
||||
// Check if trading is allowed by time filter
|
||||
bool IsTradingHourValid()
|
||||
{
|
||||
if(!g_use_trading_hours)
|
||||
@@ -107,23 +218,21 @@ public:
|
||||
|
||||
if(g_trade_start_hour <= g_trade_end_hour)
|
||||
{
|
||||
// Normal case: e.g., 8:00 to 20:00
|
||||
if(current_hour < g_trade_start_hour || current_hour >= g_trade_end_hour)
|
||||
{
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)",
|
||||
current_hour, g_trade_start_hour, g_trade_end_hour));
|
||||
mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)",
|
||||
current_hour, g_trade_start_hour, g_trade_end_hour));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overnight case: e.g., 20:00 to 8:00
|
||||
if(current_hour < g_trade_start_hour && current_hour >= g_trade_end_hour)
|
||||
{
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)",
|
||||
current_hour, g_trade_start_hour, g_trade_end_hour));
|
||||
mp_logger.Info(StringFormat("Outside trading hours: %d (allowed: %d-%d)",
|
||||
current_hour, g_trade_start_hour, g_trade_end_hour));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -131,37 +240,103 @@ public:
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calculate stop loss price in absolute terms
|
||||
double CalculateStopLossPrice(bool buy)
|
||||
{
|
||||
double bid = mp_market_data.GetBid();
|
||||
double ask = mp_market_data.GetAsk();
|
||||
double entry_price = buy ? ask : bid;
|
||||
double sl_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), g_stop_loss_points);
|
||||
|
||||
double sl_price = buy ? (entry_price - sl_distance) : (entry_price + sl_distance);
|
||||
|
||||
return CUtilities::NormalizePrice(mp_market_data.GetSymbol(), sl_price);
|
||||
}
|
||||
|
||||
// Calculate take profit price in absolute terms
|
||||
double CalculateTakeProfitPrice(bool buy)
|
||||
{
|
||||
double bid = mp_market_data.GetBid();
|
||||
double ask = mp_market_data.GetAsk();
|
||||
double entry_price = buy ? ask : bid;
|
||||
double tp_distance = CUtilities::PointsToPrice(mp_market_data.GetSymbol(), g_take_profit_points);
|
||||
|
||||
double tp_price = buy ? (entry_price + tp_distance) : (entry_price - tp_distance);
|
||||
|
||||
return CUtilities::NormalizePrice(mp_market_data.GetSymbol(), tp_price);
|
||||
}
|
||||
|
||||
// Get market data reference
|
||||
CMarketData* GetMarketData()
|
||||
{
|
||||
return mp_market_data;
|
||||
}
|
||||
|
||||
private:
|
||||
double GetEffectiveRiskPercent()
|
||||
{
|
||||
double risk_percent = g_risk_percent;
|
||||
if(risk_percent > g_max_risk_percent)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Warning(StringFormat("RiskPercent capped from %.2f%% to %.2f%% (MaxRiskPercent)",
|
||||
risk_percent, g_max_risk_percent));
|
||||
risk_percent = g_max_risk_percent;
|
||||
}
|
||||
return risk_percent;
|
||||
}
|
||||
|
||||
double GetAtrValue(int shift)
|
||||
{
|
||||
string symbol = mp_market_data.GetSymbol();
|
||||
int atr_handle = iATR(symbol, g_strategy_entry_timeframe, g_atr_period);
|
||||
if(atr_handle == INVALID_HANDLE)
|
||||
return 0.0;
|
||||
|
||||
double atr_value[];
|
||||
ArraySetAsSeries(atr_value, true);
|
||||
ArrayResize(atr_value, 1);
|
||||
if(CopyBuffer(atr_handle, 0, shift, 1, atr_value) <= 0)
|
||||
{
|
||||
IndicatorRelease(atr_handle);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double atr = atr_value[0];
|
||||
IndicatorRelease(atr_handle);
|
||||
return atr;
|
||||
}
|
||||
|
||||
double ValidateLotSize(double lot)
|
||||
{
|
||||
string symbol = mp_market_data.GetSymbol();
|
||||
double min_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
||||
double max_lot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
||||
double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
if(min_lot <= 0.0 || max_lot <= 0.0 || lot_step <= 0.0 || max_lot < min_lot)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Invalid volume step or limits for symbol");
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if(lot <= 0.0)
|
||||
return 0.0;
|
||||
|
||||
double normalized = MathFloor(lot / lot_step) * lot_step;
|
||||
if(normalized < min_lot || normalized > max_lot)
|
||||
{
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Warning(StringFormat("Normalized lot %.2f outside allowed range [%.2f, %.2f]", normalized, min_lot, max_lot));
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
normalized = NormalizeDouble(normalized, 2);
|
||||
if(mp_logger && g_debug_mode)
|
||||
mp_logger.Info(StringFormat("Lot size calculated: %.2f", normalized));
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
bool IsSymbolValid(const string symbol)
|
||||
{
|
||||
if(!mp_market_data.IsTradingAllowed())
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Symbol trading is not allowed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(mp_market_data.GetPoint() <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Symbol point size is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
if(CUtilities::GetContractSize(symbol) <= 0.0)
|
||||
{
|
||||
if(mp_logger)
|
||||
mp_logger.Error("Symbol contract size is invalid");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
#endif //__RISKMANAGER_MQH__
|
||||
|
||||
Reference in New Issue
Block a user