They referred to project documents that are not part of this repository. Comments only; PriceToPips is now documented as not rounding, which is what it does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
450 lines
20 KiB
MQL5
450 lines
20 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| Risk.mqh|
|
|
//| Copyright 2026, bandersoft |
|
|
//| https://www.mql5.com/en/users/fats |
|
|
//+------------------------------------------------------------------+
|
|
//| |
|
|
//| Position sizing and value at risk. |
|
|
//| |
|
|
//+------------------------------------------------------------------+
|
|
#ifndef STOPFIRST_RISK_MQH
|
|
#define STOPFIRST_RISK_MQH
|
|
|
|
#include <Trade\SymbolInfo.mqh>
|
|
#include <Trade\AccountInfo.mqh>
|
|
#include "Defs.mqh"
|
|
#include "Symbol.mqh" // GetPipSize - single definition of a pip
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Value of one unit of currency `from` in currency `to` |
|
|
//| |
|
|
//| Looks for a symbol quoting the pair either way round, first with |
|
|
//| the same suffix as `likeSymbol` ("EURUSD.m" -> ".m"), then bare. |
|
|
//| A symbol that exists but is not in Market Watch is selected, as |
|
|
//| the terminal's own OrderCalcProfit would have to. |
|
|
//| |
|
|
//| Returns: the rate, 1 when the currencies are the same, 0 when no |
|
|
//| symbol quotes the pair. |
|
|
//+------------------------------------------------------------------+
|
|
double CurrencyRate(const string from, const string to, const string likeSymbol = "")
|
|
{
|
|
if(from == "" || to == "") return 0;
|
|
if(from == to) return 1.0;
|
|
|
|
//--- the suffix of the symbol being priced, if it has one
|
|
string suffix = "";
|
|
if(likeSymbol != "")
|
|
{
|
|
string base = SymbolInfoString(likeSymbol, SYMBOL_CURRENCY_BASE) +
|
|
SymbolInfoString(likeSymbol, SYMBOL_CURRENCY_PROFIT);
|
|
if(StringLen(base) == 6 && StringFind(likeSymbol, base) == 0)
|
|
suffix = StringSubstr(likeSymbol, 6);
|
|
}
|
|
|
|
string tails[2];
|
|
tails[0] = suffix;
|
|
tails[1] = "";
|
|
|
|
for(int t = 0; t < 2; t++)
|
|
{
|
|
if(t == 1 && suffix == "") break;
|
|
|
|
for(int inverse = 0; inverse < 2; inverse++)
|
|
{
|
|
string name = (inverse == 0 ? from + to : to + from) + tails[t];
|
|
|
|
bool custom = false;
|
|
if(!SymbolExist(name, custom)) continue;
|
|
if(!SymbolInfoInteger(name, SYMBOL_SELECT)) SymbolSelect(name, true);
|
|
|
|
double quote = (inverse == 0) ? SymbolInfoDouble(name, SYMBOL_BID)
|
|
: SymbolInfoDouble(name, SYMBOL_ASK);
|
|
if(quote <= 0) continue;
|
|
|
|
return (inverse == 0) ? quote : 1.0 / quote;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Pip value from the contract specification |
|
|
//| |
|
|
//| The profit formulas MQL5 documents per ENUM_SYMBOL_CALC_MODE: for |
|
|
//| forex, CFD, CFD index, CFD leverage and exchange stocks a price |
|
|
//| move of d earns d * ContractSize per lot, in the symbol's profit |
|
|
//| currency. Converted to the account currency with CurrencyRate. |
|
|
//| |
|
|
//| Exists because OrderCalcProfit is not always available: inside |
|
|
//| an INDICATOR it fails with 4014 ERR_FUNCTION_NOT_ALLOWED |
|
|
//| (measured 2026-10-14). Without this, an indicator fell through |
|
|
//| to TICK_VALUE - the figure that is ten times wrong on gold. |
|
|
//| |
|
|
//| Returns: value of one pip per lot in account currency, or 0 for |
|
|
//| other calculation modes or when no conversion rate is |
|
|
//| available. |
|
|
//+------------------------------------------------------------------+
|
|
double PipValueByContract(string symbol = "")
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
double pipSize = GetPipSize(symbol);
|
|
double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
|
|
if(pipSize <= 0 || contract <= 0) return 0;
|
|
|
|
ENUM_SYMBOL_CALC_MODE mode =
|
|
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE);
|
|
|
|
switch(mode)
|
|
{
|
|
case SYMBOL_CALC_MODE_FOREX:
|
|
case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE:
|
|
case SYMBOL_CALC_MODE_CFD:
|
|
case SYMBOL_CALC_MODE_CFDINDEX:
|
|
case SYMBOL_CALC_MODE_CFDLEVERAGE:
|
|
case SYMBOL_CALC_MODE_EXCH_STOCKS:
|
|
break;
|
|
default:
|
|
return 0; // futures and bonds price through tick value
|
|
}
|
|
|
|
double rate = CurrencyRate(SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT),
|
|
AccountInfoString(ACCOUNT_CURRENCY), symbol);
|
|
if(rate <= 0) return 0;
|
|
|
|
return pipSize * contract * rate;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Money one pip is worth, per lot, in account currency |
|
|
//| |
|
|
//| In order: |
|
|
//| 1. OrderCalcProfit - the terminal's own settlement maths |
|
|
//| 2. the contract specification - PipValueByContract() |
|
|
//| 3. tick value over tick size |
|
|
//| A broker CAN report tick fields that disagree with how it |
|
|
//| actually settles - see below - so tick value is the last resort. |
|
|
//| |
|
|
//| Returns: value of one pip per lot, or 0 if the symbol does not |
|
|
//| report usable data by any route. |
|
|
//+------------------------------------------------------------------+
|
|
double PipValuePerLot(string symbol = "")
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
CSymbolInfo symbolInfo;
|
|
if(!symbolInfo.Name(symbol)) return 0.0;
|
|
|
|
double pipSize = GetPipSize(symbol);
|
|
if(pipSize <= 0) return 0.0;
|
|
|
|
//--- Preferred source: ask the terminal to price one pip of
|
|
//--- movement on one lot. OrderCalcProfit runs the same calculation
|
|
//--- a real trade settles with, so it is right by construction.
|
|
//---
|
|
//--- This is not theoretical. Measured on a live account
|
|
//--- 2026-09-24: XAUUSD reported TICK_VALUE 0.1 against TICK_SIZE
|
|
//--- 0.01 and CONTRACT_SIZE 100. The tick formula below therefore
|
|
//--- returned 0.10 per pip where OrderCalcProfit - and the actual
|
|
//--- profit and loss - said 1.00. A tenth of the true value makes
|
|
//--- CalcLotSizeByRisk size TEN TIMES too large. EURUSD agreed to
|
|
//--- the cent on the same broker, so the tick fields are reliable
|
|
//--- for forex and not for this CFD.
|
|
//---
|
|
//--- Measured over 1000 pips and divided, not over one. The terminal
|
|
//--- rounds OrderCalcProfit to the cent: on DE40, with a 0.01 pip,
|
|
//--- one pip came back as 0.01 where the true value is 0.0112 - 11%
|
|
//--- low, so 11% too many lots (measured 2026-10-14). Profit is
|
|
//--- linear in the move, so the larger move loses nothing.
|
|
const double MEASURE_PIPS = 1000.0;
|
|
|
|
symbolInfo.RefreshRates();
|
|
double price = symbolInfo.Ask();
|
|
double profit = 0;
|
|
|
|
if(price > 0 &&
|
|
OrderCalcProfit(ORDER_TYPE_BUY, symbol, 1.0, price,
|
|
price + MEASURE_PIPS * pipSize, profit) &&
|
|
profit > 0)
|
|
return profit / MEASURE_PIPS;
|
|
|
|
//--- Second: the contract specification. This is the route an
|
|
//--- indicator takes, since OrderCalcProfit is refused there (4014).
|
|
double byContract = PipValueByContract(symbol);
|
|
if(byContract > 0) return byContract;
|
|
|
|
//--- Last resort: the tick-value route. Correct for forex, and the
|
|
//--- best available guess otherwise.
|
|
double tickValue = symbolInfo.TickValueLoss();
|
|
if(tickValue <= 0) tickValue = symbolInfo.TickValue();
|
|
|
|
double tickSize = symbolInfo.TickSize();
|
|
|
|
//--- every divisor guarded
|
|
if(tickValue <= 0 || tickSize <= 0) return 0.0;
|
|
|
|
return tickValue * (pipSize / tickSize);
|
|
}
|
|
|
|
//--- CalcLotSizeByRisk caps itself against this, which is defined below
|
|
double CalcMaxLotSize(string symbol, double marginPercent);
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate lot size based on risk percentage and stop loss |
|
|
//| Parameters: |
|
|
//| riskPercent - Risk as percentage of account balance (e.g., 1.0)|
|
|
//| stopLossPips - Stop loss distance in pips |
|
|
//| symbol - Trading symbol (empty = current symbol) |
|
|
//| |
|
|
//| marginCapPercent - share of free margin the result may need, |
|
|
//| 1..100. Lower it to leave headroom. |
|
|
//| |
|
|
//| Returns: the lot size whose stop costs riskPercent of balance, |
|
|
//| floored to the volume step. |
|
|
//| 0 on failure, and 0 - not the broker minimum - when the |
|
|
//| risk budget will not cover one minimum lot. Rounding up |
|
|
//| there would silently risk more than was asked for. |
|
|
//| |
|
|
//| The result is capped THREE ways, all downwards, because reducing |
|
|
//| a position can only reduce risk: |
|
|
//| - the broker's maximum volume |
|
|
//| - what free margin supports (measured 2026-09-24: it returned |
|
|
//| 100 lots of XAUUSD on an account whose free margin |
|
|
//| covered 23.44 - the broker maximum was the only ceiling) |
|
|
//| - marginCapPercent of that |
|
|
//| A capped result risks LESS than requested, never more. |
|
|
//+------------------------------------------------------------------+
|
|
double CalcLotSizeByRisk(double riskPercent, double stopLossPips, string symbol = "",
|
|
double marginCapPercent = 100.0)
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
//--- guard before dividing: a zero stop would make the size infinite,
|
|
//--- and MathMin(maxLot, inf) would return the MAXIMUM lot size
|
|
if(riskPercent <= 0 || stopLossPips <= 0) return 0.0;
|
|
|
|
CSymbolInfo symbolInfo;
|
|
if(!symbolInfo.Name(symbol)) return 0.0;
|
|
|
|
CAccountInfo accountInfo;
|
|
double riskAmount = accountInfo.Balance() * (riskPercent / 100.0);
|
|
if(riskAmount <= 0) return 0.0;
|
|
|
|
double pipValue = PipValuePerLot(symbol);
|
|
if(pipValue <= 0) return 0.0;
|
|
|
|
double minLot = symbolInfo.LotsMin();
|
|
double maxLot = symbolInfo.LotsMax();
|
|
double lotStep = symbolInfo.LotsStep();
|
|
if(lotStep <= 0) return 0.0;
|
|
|
|
double lotSize = FloorToVolumeStep(riskAmount / (stopLossPips * pipValue), lotStep);
|
|
|
|
//--- capping down is safe, it can only reduce exposure
|
|
if(lotSize > maxLot) lotSize = FloorToVolumeStep(maxLot, lotStep);
|
|
|
|
//--- and the broker maximum is NOT the real ceiling: free margin
|
|
//--- usually runs out first. Without this a 1% risk on gold asked
|
|
//--- for 100 lots against margin that covered 23.
|
|
//---
|
|
//--- CalcMaxLotSize returns 0 both when margin is unknown and when it
|
|
//--- will not fund one minimum lot. Only the first may skip the cap:
|
|
//--- the second used to, and returned an unfundable size.
|
|
if(MarginPerLot(symbol, ORDER_TYPE_BUY) > 0)
|
|
{
|
|
double affordable = CalcMaxLotSize(symbol, marginCapPercent);
|
|
if(affordable <= 0) return 0.0;
|
|
if(lotSize > affordable) lotSize = FloorToVolumeStep(affordable, lotStep);
|
|
}
|
|
|
|
//--- capping up is not: refuse instead
|
|
if(lotSize < minLot - 1e-9) return 0.0;
|
|
|
|
return lotSize;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Margin one lot needs, in account currency |
|
|
//| |
|
|
//| OrderCalcMargin first, which honours the symbol's |
|
|
//| margin rate, calculation mode and hedged margin settings. |
|
|
//| |
|
|
//| Inside an indicator OrderCalcMargin fails with 4014 |
|
|
//| ERR_FUNCTION_NOT_ALLOWED (measured 2026-10-14), and so does |
|
|
//| CAccountInfo::MaxLotCheck, which is built on it - it also prints |
|
|
//| "margin calculation failed" on every call. The fallback is the |
|
|
//| margin formula MQL5 documents per ENUM_SYMBOL_CALC_MODE, with the |
|
|
//| symbol's initial margin rate, converted from the margin currency. |
|
|
//| It ignores hedged-margin reductions, so it can only overstate. |
|
|
//| |
|
|
//| Returns: margin for 1.0 lot, or 0 when it cannot be determined. |
|
|
//+------------------------------------------------------------------+
|
|
double MarginPerLotByFormula(string symbol = "", const ENUM_ORDER_TYPE type = ORDER_TYPE_BUY);
|
|
|
|
double MarginPerLot(string symbol = "", const ENUM_ORDER_TYPE type = ORDER_TYPE_BUY)
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
bool isBuy = (type == ORDER_TYPE_BUY);
|
|
double price = SymbolInfoDouble(symbol, isBuy ? SYMBOL_ASK : SYMBOL_BID);
|
|
if(price <= 0) return 0;
|
|
|
|
double margin = 0;
|
|
if(OrderCalcMargin(type, symbol, 1.0, price, margin) && margin > 0)
|
|
return margin;
|
|
|
|
return MarginPerLotByFormula(symbol, type);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Margin for one lot from the documented formula alone |
|
|
//| |
|
|
//| The fallback inside MarginPerLot, public so a test can compare it |
|
|
//| with OrderCalcMargin where both are available. |
|
|
//| |
|
|
//| Returns: margin for 1.0 lot in account currency, or 0 for other |
|
|
//| calculation modes or without a conversion rate. |
|
|
//+------------------------------------------------------------------+
|
|
double MarginPerLotByFormula(string symbol, const ENUM_ORDER_TYPE type)
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
bool isBuy = (type == ORDER_TYPE_BUY);
|
|
double price = SymbolInfoDouble(symbol, isBuy ? SYMBOL_ASK : SYMBOL_BID);
|
|
if(price <= 0) return 0;
|
|
|
|
//--- the documented formulas, in the margin currency
|
|
double contract = SymbolInfoDouble(symbol, SYMBOL_TRADE_CONTRACT_SIZE);
|
|
double leverage = (double)AccountInfoInteger(ACCOUNT_LEVERAGE);
|
|
double initialRate = 0, maintenanceRate = 0;
|
|
if(!SymbolInfoMarginRate(symbol, type, initialRate, maintenanceRate) || initialRate <= 0)
|
|
initialRate = 1.0;
|
|
if(contract <= 0) return 0;
|
|
|
|
ENUM_SYMBOL_CALC_MODE mode =
|
|
(ENUM_SYMBOL_CALC_MODE)SymbolInfoInteger(symbol, SYMBOL_TRADE_CALC_MODE);
|
|
double perLot = 0;
|
|
|
|
switch(mode)
|
|
{
|
|
case SYMBOL_CALC_MODE_FOREX:
|
|
if(leverage <= 0) return 0;
|
|
perLot = contract / leverage;
|
|
break;
|
|
case SYMBOL_CALC_MODE_FOREX_NO_LEVERAGE:
|
|
perLot = contract;
|
|
break;
|
|
case SYMBOL_CALC_MODE_CFD:
|
|
perLot = contract * price;
|
|
break;
|
|
case SYMBOL_CALC_MODE_CFDLEVERAGE:
|
|
if(leverage <= 0) return 0;
|
|
perLot = contract * price / leverage;
|
|
break;
|
|
case SYMBOL_CALC_MODE_CFDINDEX:
|
|
{
|
|
double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
if(tickValue <= 0 || tickSize <= 0) return 0;
|
|
perLot = contract * price * tickValue / tickSize;
|
|
break;
|
|
}
|
|
default:
|
|
return 0;
|
|
}
|
|
|
|
double rate = CurrencyRate(SymbolInfoString(symbol, SYMBOL_CURRENCY_MARGIN),
|
|
AccountInfoString(ACCOUNT_CURRENCY), symbol);
|
|
if(rate <= 0) return 0;
|
|
|
|
return perLot * initialRate * rate;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate maximum lot size based on free margin |
|
|
//| Parameters: |
|
|
//| symbol - Trading symbol (empty = current symbol) |
|
|
//| marginPercent - share of free margin to commit, 1..100 |
|
|
//| |
|
|
//| Free margin times the share, over MarginPerLot(). That is what |
|
|
//| CAccountInfo::MaxLotCheck computes, without its dependency on |
|
|
//| OrderCalcMargin alone - so it also works inside an indicator. |
|
|
//| |
|
|
//| Returns: the largest lot size free margin supports, floored to |
|
|
//| the volume step. 0 if margin cannot be calculated or |
|
|
//| will not cover one minimum lot - MarginPerLot() tells |
|
|
//| the two apart. |
|
|
//+------------------------------------------------------------------+
|
|
double CalcMaxLotSize(string symbol = "", double marginPercent = 100.0)
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
if(marginPercent < 1.0 || marginPercent > 100.0) return 0.0;
|
|
|
|
CSymbolInfo symbolInfo;
|
|
if(!symbolInfo.Name(symbol)) return 0.0;
|
|
|
|
double perLot = MarginPerLot(symbol, ORDER_TYPE_BUY);
|
|
if(perLot <= 0) return 0.0;
|
|
|
|
double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);
|
|
if(freeMargin <= 0) return 0.0;
|
|
|
|
double maxLot = freeMargin * (marginPercent / 100.0) / perLot;
|
|
|
|
double minLot = symbolInfo.LotsMin();
|
|
double maxAllowed = symbolInfo.LotsMax();
|
|
double lotStep = symbolInfo.LotsStep();
|
|
if(lotStep <= 0) return 0.0;
|
|
|
|
maxLot = FloorToVolumeStep(maxLot, lotStep);
|
|
if(maxLot > maxAllowed) maxLot = FloorToVolumeStep(maxAllowed, lotStep);
|
|
|
|
//--- do not claim a tradable size when margin will not cover one
|
|
if(maxLot < minLot - 1e-9) return 0.0;
|
|
|
|
return maxLot;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate risk-reward ratio |
|
|
//| |
|
|
//| Returns: reward distance divided by risk distance, 2 decimals. |
|
|
//| 0 when the stop equals the entry, which leaves the ratio |
|
|
//| undefined. |
|
|
//+------------------------------------------------------------------+
|
|
double CalcRiskRewardRatio(double entryPrice, double stopLoss, double takeProfit)
|
|
{
|
|
double risk = MathAbs(entryPrice - stopLoss);
|
|
double reward = MathAbs(takeProfit - entryPrice);
|
|
|
|
if(risk == 0) return 0;
|
|
return NormalizeDouble(reward / risk, 2);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate position value at risk in account currency |
|
|
//| Parameters: |
|
|
//| lotSize - position size |
|
|
//| stopLossPips - stop distance in pips |
|
|
//| symbol - Trading symbol (empty = current symbol) |
|
|
//| |
|
|
//| Returns: the money lost if the stop is hit, to 2 decimals. |
|
|
//| 0 if either argument is not positive, or the symbol |
|
|
//| reports no usable tick data. |
|
|
//+------------------------------------------------------------------+
|
|
double CalcValueAtRisk(double lotSize, double stopLossPips, string symbol = "")
|
|
{
|
|
if(symbol == "") symbol = _Symbol;
|
|
|
|
if(lotSize <= 0 || stopLossPips <= 0) return 0.0;
|
|
|
|
double pipValue = PipValuePerLot(symbol);
|
|
if(pipValue <= 0) return 0.0;
|
|
|
|
return NormalizeDouble(lotSize * stopLossPips * pipValue, 2);
|
|
}
|
|
|
|
#endif // STOPFIRST_RISK_MQH
|
|
//+------------------------------------------------------------------+
|