//+------------------------------------------------------------------+ //| MoneyRiskBase.mqh | //| AnimateDread | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "..\Expert\ExpertMoneyCustom.mqh" #include "..\Variables\RiskBudget.mqh" //+------------------------------------------------------------------+ //| Class CMoneyRiskBase. | //| Shared risk-based lot-sizing core for every money-management | //| strategy that sizes a trade off a fixed account-risk percentage. | //| It was extracted when CMoneyFixedRisk and CMoneyIntelligent had | //| CalculatePotentialLoss()/CheckOpenLong()/CheckOpenShort()/ | //| CalculateLotSize() duplicated near-verbatim between them; | //| CMoneyIntelligent was removed 2026-08-25, so CMoneyFixedRisk is | //| currently the only subclass. Kept as a base anyway - what lives | //| here is the account-risk contract (including the RiskBudget | //| clamp), not one strategy's arithmetic. | //| AdjustLotSize() is the one remaining divergence point; the | //| AdjustRiskAmount() hook beside it went with its only overrider. | //+------------------------------------------------------------------+ class CMoneyRiskBase : public CExpertMoneyCustom { public: virtual double CheckOpenLong(double price, double sl); virtual double CheckOpenShort(double price, double sl); virtual double CheckClose(CPositionInfo *position) { return(0.0); } protected: double CalculatePotentialLoss(ENUM_ORDER_TYPE orderType, double price, double sl); double CalculateLotSize(double loss); private: //--- CheckOpenLong()/CheckOpenShort() share this whole body, differing only in the order type //--- passed through to CalculatePotentialLoss()/ValidateLotForTrade(); see CheckTrailingStop() //--- in Trailing\TrailingATR.mqh for the same isLong-parameter unification pattern. double CheckOpen(ENUM_ORDER_TYPE type, double price, double sl); //--- Reserved extension point for a future lot-size-level adjustment (e.g. equity-curve- //--- based scaling); no current subclass overrides this - default is a no-op. virtual double AdjustLotSize(double lot) { return lot; } }; //+------------------------------------------------------------------+ //| Getting lot size for open long position. | //+------------------------------------------------------------------+ double CMoneyRiskBase::CheckOpenLong(double price, double sl) { return CheckOpen(ORDER_TYPE_BUY, price, sl); } //+------------------------------------------------------------------+ //| Getting lot size for open short position. | //+------------------------------------------------------------------+ double CMoneyRiskBase::CheckOpenShort(double price, double sl) { return CheckOpen(ORDER_TYPE_SELL, price, sl); } //+------------------------------------------------------------------+ //| Shared CheckOpenLong()/CheckOpenShort() body - see class header. | //+------------------------------------------------------------------+ double CMoneyRiskBase::CheckOpen(ENUM_ORDER_TYPE type, double price, double sl) { if(m_symbol == NULL) return 0.0; double loss = CalculatePotentialLoss(type, price, sl); if(loss <= 0.0) { // loss<=0 means sl landed on the wrong side of price (upstream signal bug, stale/gapped // quote, or a mid-tick price move) - reject the trade instead of silently opening at // LotsMin(), which used to bypass Money_Risk_Percent entirely with no trace of why. PrintFormat("%s: rejected - non-positive potential loss (%.5f) for price=%.5f sl=%.5f on %s", __FUNCTION__, loss, price, sl, m_symbol.Name()); return 0.0; } double lot = AdjustLotSize(CalculateLotSize(loss)); if(lot <= 0.0 || !MathIsValidNumber(lot)) { PrintFormat("%s: rejected - invalid computed lot size (%.5f) for loss=%.5f on %s", __FUNCTION__, lot, loss, m_symbol.Name()); return 0.0; } string description; // Article 2555 #14/#3/#5/#2 in one gate: tradeable symbol, legal volume on the SYMBOL_VOLUME_STEP // grid, inside SYMBOL_VOLUME_LIMIT for this direction, and covered by ACCOUNT_MARGIN_FREE. See // CExpertMoneyCustom::ValidateLotForTrade() - it logs its own (throttled) rejection reason. if(!ValidateLotForTrade(m_symbol.Name(), lot, type, description)) return 0.0; return lot; } //+------------------------------------------------------------------+ //| Calculate potential loss | //+------------------------------------------------------------------+ double CMoneyRiskBase::CalculatePotentialLoss(ENUM_ORDER_TYPE orderType, double price, double sl) { if(price == 0.0) price = (orderType == ORDER_TYPE_BUY) ? m_symbol.Ask() : m_symbol.Bid(); if(price == 0.0) { // SymbolInfoDouble-backed Ask()/Bid() returns 0 when no quote is available yet // (disconnected/freshly-selected symbol) - a 0 price makes OrderProfitCheck's result // meaningless, so fail loudly instead of feeding it into the lot-size calculation. PrintFormat("%s: no valid price available for %s (Ask/Bid returned 0)", __FUNCTION__, m_symbol.Name()); return 0.0; } return -m_account.OrderProfitCheck(m_symbol.Name(), orderType, 1.0, price, sl); } //+------------------------------------------------------------------+ //| Calculate the lot size based on potential loss and account balance| //+------------------------------------------------------------------+ double CMoneyRiskBase::CalculateLotSize(double loss) { if(loss <= 0.0 || !MathIsValidNumber(loss)) { // Both current callers reject a non-positive loss before reaching here, but this is where the // division happens, so this is where the invariant belongs - CalculatePotentialLoss() signals // "no usable quote" by returning exactly 0.0, and any future caller that forwards that value // straight through would otherwise divide by it and hand back an inf lot size. PrintFormat("%s: rejected - potential loss must be positive and finite, got %.5f", __FUNCTION__, loss); return 0.0; } double riskAmount = m_account.Balance() * m_percent / 100.0; //--- ACCOUNT-LEVEL CLAMP. Money_Risk_Percent alone answers "what is my usual risk per trade", which //--- is a different question from "how much am I still ALLOWED to lose today". Sizing off balance //--- with no reference to the second is how a routine stop-out breaches a hard daily limit: at 3.2% //--- into a 4% day, a full-size trade is already over the line before it is placed. CapRiskAmount() //--- reduces this to a fraction of what genuinely remains after every open position's loss-to-stop, //--- and returns 0 when the trade must not be taken at all. See Variables\RiskBudget.mqh. double allowed = g_riskBudget.CapRiskAmount(riskAmount); if(allowed <= 0.0) return 0.0; // budget exhausted / halted - CapRiskAmount has logged the reason riskAmount = allowed; double stepvol = m_symbol.LotsStep(); if(stepvol <= 0.0) { // A 0 SYMBOL_VOLUME_STEP (not yet synced by the broker) would otherwise divide by zero and // propagate inf/NaN downstream into volume/margin checks with no diagnostic. PrintFormat("%s: rejected - LotsStep() returned %.5f for %s (broker volume data not ready?)", __FUNCTION__, stepvol, m_symbol.Name()); return 0.0; } double lot = MathFloor(riskAmount / loss / stepvol) * stepvol; //--- BELOW-MINIMUM MEANS NO TRADE, NOT A BIGGER TRADE. Downstream, TCNormalizeVolume() bumps any //--- sub-minimum volume UP to SYMBOL_VOLUME_MIN (correct for a user-entered fixed lot), which on a //--- symbol where min > step (indices/metals: min 0.10, step 0.01) would turn a risk-derived 0.05 //--- into 0.10 - double the intended risk, AFTER CapRiskAmount() already clamped it. That is the //--- exact "routine stop-out breaches the daily limit" path the budget exists to close, and the //--- opposite of RISK_BUDGET_MIN_SIZE_FRACTION's near-binary rule (see Variables\RiskBudget.mqh): //--- when the affordable size is not at least the broker's minimum, the trade is refused here, in //--- the risk-sizing path, before the normalizer can inflate it. (2026-08-11) double minvol = m_symbol.LotsMin(); if(minvol > 0.0 && lot < minvol) { PrintFormat("%s: rejected - risk-derived lot %.2f is below the broker minimum %.2f on %s; " "opening at the minimum would exceed the intended risk (budget-capped risk %.2f, " "1-lot loss %.2f)", __FUNCTION__, lot, minvol, m_symbol.Name(), riskAmount, loss); return 0.0; } return lot; } //+------------------------------------------------------------------+