//+------------------------------------------------------------------+ //| MoneyRiskBase.mqh | //| AnimateDread | //| https://www.mql5.com | //+------------------------------------------------------------------+ #include "..\Expert\ExpertMoneyCustom.mqh" //+------------------------------------------------------------------+ //| Class CMoneyRiskBase. | //| Shared risk-based lot-sizing core for every money-management | //| strategy that sizes a trade off a fixed account-risk percentage | //| (CMoneyFixedRisk, CMoneyIntelligent) - CalculatePotentialLoss()/ | //| CheckOpenLong()/CheckOpenShort()/CalculateLotSize() used to be | //| duplicated near-verbatim across both. AdjustRiskAmount()/ | //| AdjustLotSize() are the two points a subclass can diverge at | //| (CMoneyIntelligent overrides AdjustRiskAmount() for its Kelly- | //| criterion, confidence-and-reward:risk-based sizing) - override | //| only those, everything else here is shared as-is. | //+------------------------------------------------------------------+ 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); //--- Hook for CMoneyIntelligent's Kelly-criterion risk% scaling (see its own //--- AdjustRiskAmount() override for the rationale); default no-op keeps CMoneyFixedRisk's //--- behavior exactly as it was before this base class existed. virtual double AdjustRiskAmount(double riskAmount) { return riskAmount; } //--- 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) { if(m_symbol == NULL) return 0.0; double loss = CalculatePotentialLoss(ORDER_TYPE_BUY, 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, ORDER_TYPE_BUY, description)) return 0.0; return lot; } //+------------------------------------------------------------------+ //| Getting lot size for open short position. | //+------------------------------------------------------------------+ double CMoneyRiskBase::CheckOpenShort(double price, double sl) { if(m_symbol == NULL) return 0.0; double loss = CalculatePotentialLoss(ORDER_TYPE_SELL, price, sl); if(loss <= 0.0) { // See CheckOpenLong() for why this rejects instead of falling back to LotsMin(). 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; // See CheckOpenLong() above - same article 2555 #14/#3/#5/#2 gate. if(!ValidateLotForTrade(m_symbol.Name(), lot, ORDER_TYPE_SELL, 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) { double riskAmount = AdjustRiskAmount(m_account.Balance() * m_percent / 100.0); 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; } return MathFloor(riskAmount / loss / stepvol) * stepvol; } //+------------------------------------------------------------------+