Smart-Lot-Calculator/Include/SmartLotCalculator.mqh

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