322 lines
No EOL
8.6 KiB
MQL5
322 lines
No EOL
8.6 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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 |