//+------------------------------------------------------------------+ //| 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 #include #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 //+------------------------------------------------------------------+