//+------------------------------------------------------------------+ //| SmartLotCalculator.mqh | //| Open-source position sizing library for MQL5 | //+------------------------------------------------------------------+ #ifndef SMART_LOT_CALCULATOR_MQH #define SMART_LOT_CALCULATOR_MQH // Account value used as the basis for percentage risk. enum ENUM_RISK_BASE { RISK_BASE_BALANCE = 0, RISK_BASE_EQUITY = 1, RISK_BASE_FREE_MARGIN = 2 }; //+------------------------------------------------------------------+ //| Smart lot calculator | //+------------------------------------------------------------------+ class CSmartLotCalculator { private: // Returns the selected account value. static double GetRiskBaseValue(const ENUM_RISK_BASE risk_base) { switch(risk_base) { case RISK_BASE_BALANCE: return AccountInfoDouble(ACCOUNT_BALANCE); case RISK_BASE_EQUITY: return AccountInfoDouble(ACCOUNT_EQUITY); case RISK_BASE_FREE_MARGIN: return AccountInfoDouble(ACCOUNT_MARGIN_FREE); } return 0.0; } // Determines the number of decimal places required for displaying volume. static int GetVolumeDigits(const double volume_step) { if(volume_step <= 0.0) return 2; int digits = 0; double normalized_step = volume_step; while(digits < 8 && MathAbs(normalized_step - MathRound(normalized_step)) > 1e-8) { normalized_step *= 10.0; digits++; } return digits; } // Validates basic calculation parameters. static bool ValidateParameters(const string symbol, const ENUM_ORDER_TYPE order_type, const double entry_price, const double stop_price, string &error) { error = ""; if(symbol == "") { error = "The symbol name is empty."; return false; } if(order_type != ORDER_TYPE_BUY && order_type != ORDER_TYPE_SELL) { error = "Only ORDER_TYPE_BUY and ORDER_TYPE_SELL are supported."; return false; } if(entry_price <= 0.0) { error = "The entry price must be greater than zero."; return false; } if(stop_price <= 0.0) { error = "The Stop Loss price must be greater than zero."; return false; } if(entry_price == stop_price) { error = "The entry price and Stop Loss price cannot be equal."; return false; } if(order_type == ORDER_TYPE_BUY && stop_price >= entry_price) { error = "For a buy calculation, the Stop Loss must be below the entry price."; return false; } if(order_type == ORDER_TYPE_SELL && stop_price <= entry_price) { error = "For a sell calculation, the Stop Loss must be above the entry price."; return false; } if(!SymbolSelect(symbol, true)) { error = "The symbol could not be selected in Market Watch."; return false; } return true; } public: // Normalizes volume to the broker's allowed volume step. // The volume is rounded down so the selected risk is not exceeded. static double NormalizeVolume(const string symbol, const double requested_volume, string &error) { error = ""; if(requested_volume <= 0.0) { error = "The requested volume must be greater than zero."; return 0.0; } double volume_min = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN); double volume_max = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX); double volume_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP); if(volume_min <= 0.0 || volume_max <= 0.0 || volume_step <= 0.0) { error = "Invalid volume specifications received from the broker."; return 0.0; } if(requested_volume < volume_min) { error = StringFormat( "Calculated volume %.8f is below the broker minimum of %.8f lots.", requested_volume, volume_min ); return 0.0; } double limited_volume = MathMin(requested_volume, volume_max); // Normalize relative to the minimum volume. This also handles cases // where the minimum volume and the volume step are different. double steps = MathFloor( ((limited_volume - volume_min) / volume_step) + 1e-10 ); double normalized_volume = volume_min + (steps * volume_step); normalized_volume = MathMax(normalized_volume, volume_min); normalized_volume = MathMin(normalized_volume, volume_max); int volume_digits = GetVolumeDigits(volume_step); return NormalizeDouble(normalized_volume, volume_digits); } // Calculates the potential loss for a given volume. static double CalculatePotentialLoss(const string symbol, const ENUM_ORDER_TYPE order_type, const double volume, const double entry_price, const double stop_price, string &error) { error = ""; if(!ValidateParameters( symbol, order_type, entry_price, stop_price, error )) { return 0.0; } if(volume <= 0.0) { error = "The volume must be greater than zero."; return 0.0; } double calculated_profit = 0.0; ResetLastError(); bool calculation_success = OrderCalcProfit( order_type, symbol, volume, entry_price, stop_price, calculated_profit ); if(!calculation_success) { error = StringFormat( "OrderCalcProfit failed. MetaTrader error code: %d.", GetLastError() ); return 0.0; } double potential_loss = MathAbs(calculated_profit); if(potential_loss <= 0.0) { error = "MetaTrader returned a zero potential loss."; return 0.0; } return potential_loss; } // Calculates volume from a fixed amount of account currency. static double CalculateByMoney(const string symbol, const ENUM_ORDER_TYPE order_type, const double entry_price, const double stop_price, const double risk_money, string &error) { error = ""; if(risk_money <= 0.0) { error = "Risk money must be greater than zero."; return 0.0; } if(!ValidateParameters( symbol, order_type, entry_price, stop_price, error )) { return 0.0; } // Calculate how much one complete lot would lose at the Stop Loss. double loss_for_one_lot = CalculatePotentialLoss( symbol, order_type, 1.0, entry_price, stop_price, error ); if(loss_for_one_lot <= 0.0) return 0.0; double raw_volume = risk_money / loss_for_one_lot; return NormalizeVolume(symbol, raw_volume, error); } // Calculates volume from a percentage of balance, equity or free margin. static double CalculateByRiskPercent(const string symbol, const ENUM_ORDER_TYPE order_type, const double entry_price, const double stop_price, const double risk_percent, const ENUM_RISK_BASE risk_base, string &error) { error = ""; if(risk_percent <= 0.0) { error = "Risk percentage must be greater than zero."; return 0.0; } double base_value = GetRiskBaseValue(risk_base); if(base_value <= 0.0) { error = "The selected account risk base is unavailable or zero."; return 0.0; } double risk_money = base_value * risk_percent / 100.0; return CalculateByMoney( symbol, order_type, entry_price, stop_price, risk_money, error ); } }; #endif