Warrior_EA/Money/MoneyFixedLot.mqh
AnimateDread 77e8080cfe fix: four risk-layer holes a funded account would eventually find
1. The expectancy stop was stone dead at shipped defaults. Its only feed -
   RecordTradeResult inside CTradeJournalManager::Update() - ran solely under
   UseDatabaseRanking, which ships false, so the da54639 halt was armed
   (ExpectancyMinTrades=40) and never received a single closed trade. A risk
   rule must not be a side effect of an analytics toggle: the journal gains
   InitTrackingOnly(), Update() runs unconditionally from OnTick and skips
   only the DB insert when no DB was initialized.

2. Below-minimum lots were silently bumped UP to SYMBOL_VOLUME_MIN by
   TCNormalizeVolume - correct for a user-entered fixed lot, but in the
   risk-sizing path it turned a budget-capped 0.05 into 0.10 on min-0.10/
   step-0.01 symbols: double the intended risk, after CapRiskAmount already
   clamped, exactly the routine-stop-out-breaches-the-daily-limit scenario
   the budget exists to close. CMoneyRiskBase now refuses the trade when the
   risk-derived lot is below the broker minimum.

3. All trading was async fire-and-forget (SetAsyncMode(true)) with no
   OnTradeTransaction handler and no retry: server retcodes were never
   observed. Fail-safe for entries, not for closes - a silently rejected
   close rode the position until the next bar (or next day for the timed
   close window). Now synchronous, matching the risk-budget flatten's own
   already-synchronous CTrade; on an H1 EA the latency is irrelevant.

4. FIXED_LOT bypassed the budget entirely (no CapRiskAmount, no
   OpenRiskAtStops) - pre-halt it could commit more than the remaining daily
   allowance. A fixed lot cannot be scaled, so the rule is binary: its
   loss-to-stop fits the remaining allowance whole or the trade is refused;
   unpriceable risk (no SL) is refused while the budget is enabled.

Compile: 0 errors, 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 18:14:26 -04:00

117 lines
6.2 KiB
MQL5

//+------------------------------------------------------------------+
//| MoneyFixedLot.mqh |
//| Copyright 2000-2023, MetaQuotes Ltd. |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#include "..\Expert\ExpertMoneyCustom.mqh"
#include "..\Variables\RiskBudget.mqh"
// wizard description start
//+------------------------------------------------------------------+
//| Description of the class |
//| Title=Trading with fixed trade volume |
//| Type=Money |
//| Name=FixLot |
//| Class=CMoneyFixedLot |
//| Page= |
//| Parameter=Percent,double,10.0,Percent |
//| Parameter=Lots,double,0.1,Fixed volume |
//+------------------------------------------------------------------+
// wizard description end
//+------------------------------------------------------------------+
//| Class CMoneyFixedLot. |
//| Purpose: Class of money management with fixed lot. |
//| Derives from class CExpertMoney. |
//+------------------------------------------------------------------+
class CMoneyFixedLot : public CExpertMoneyCustom
{
protected:
//--- input parameters
double m_lots;
//--- declared ahead of the inline CheckOpenLong/Short bodies below that call it
double CheckedLot(ENUM_ORDER_TYPE type, double price, double sl);
public:
//---
void Lots(double lots) { m_lots=lots; }
virtual bool ValidationSettings(void);
//--- The fixed lot from the input is a REQUEST, not a guarantee: it still has to clear the
//--- broker's volume grid, the per-symbol SYMBOL_VOLUME_LIMIT and free margin before it can be
//--- sent (article 2555 #2/#3/#5). Returning m_lots raw - as this class used to - meant a
//--- Money_FixLot_Lots that a small/loaded account cannot margin was handed straight to
//--- OrderSend() and rejected server-side with "not enough money" every single signal.
virtual double CheckOpenLong(double price,double sl) { return(CheckedLot(ORDER_TYPE_BUY, price, sl)); }
virtual double CheckOpenShort(double price,double sl) { return(CheckedLot(ORDER_TYPE_SELL, price, sl)); }
};
//+------------------------------------------------------------------+
//| Run the configured fixed lot through the shared pre-OrderSend |
//| volume gate; 0.0 means "no legal volume right now", which |
//| CExpert::OpenLong/OpenShort treat as "do not trade this tick". |
//+------------------------------------------------------------------+
double CMoneyFixedLot::CheckedLot(ENUM_ORDER_TYPE type, double price, double sl)
{
double lot = m_lots;
//--- THE RISK BUDGET GOVERNS FIXED LOTS TOO (2026-08-11). This class used to bypass CapRiskAmount()
//--- and OpenRiskAtStops() entirely, so pre-halt a fixed lot could commit more than the remaining
//--- daily allowance with nothing shrinking or refusing it - the "enforced in the sizing path"
//--- guarantee only held for FIXED_RISK/INTELLIGENT. A fixed lot cannot be scaled (it is fixed by
//--- request), so the rule is binary: its loss-to-stop either fits the remaining allowance WHOLE,
//--- or the trade is refused. CapRiskAmount() handles disabled (returns the amount unchanged),
//--- halted (0) and partial-allowance (reduced) states - any reduction at all means "does not fit".
if(g_riskBudget.Enabled())
{
if(sl == 0.0 || price == 0.0)
{
//--- No stop (or no quote) means the risk is unpriceable, and the budget cannot govern what
//--- it cannot price - refuse rather than open unbounded risk on a budget-governed account.
PrintFormat("%s: rejected - fixed lot %.2f has no priceable risk (price=%.5f sl=%.5f) while "
"the risk budget is enabled", __FUNCTION__, lot, price, sl);
return(0.0);
}
double loss = -m_account.OrderProfitCheck(MoneySymbol(), type, lot, price, sl);
if(loss > 0.0)
{
double allowed = g_riskBudget.CapRiskAmount(loss);
if(allowed < loss)
{
if(allowed > 0.0)
PrintFormat("%s: rejected - fixed lot %.2f risks %.2f but only %.2f of the budget "
"remains; a fixed lot is not scaled down, it stands aside",
__FUNCTION__, lot, loss, allowed);
return(0.0);
}
}
}
string description;
if(!ValidateLotForTrade(MoneySymbol(), lot, type, description))
return(0.0);
return(lot);
}
//+------------------------------------------------------------------+
//| Validation settings protected data. |
//+------------------------------------------------------------------+
bool CMoneyFixedLot::ValidationSettings(void)
{
if(!CExpertMoney::ValidationSettings())
return(false);
//--- initial data checks
if(m_lots<m_symbol.LotsMin() || m_lots>m_symbol.LotsMax())
{
printf(__FUNCTION__+": lots amount must be in the range from %f to %f",m_symbol.LotsMin(),m_symbol.LotsMax());
return(false);
}
//--- article 2555 #10: LotsStep() is 0 until the broker syncs SYMBOL_VOLUME_STEP, and the
//--- multiple-of-step test below divides by it twice
if(m_symbol.LotsStep()<=0.0)
{
printf(__FUNCTION__+": SYMBOL_VOLUME_STEP for %s is not available yet (%f)",m_symbol.Name(),m_symbol.LotsStep());
return(false);
}
if(MathAbs(m_lots/m_symbol.LotsStep()-MathRound(m_lots/m_symbol.LotsStep()))>1.0E-10)
{
printf(__FUNCTION__+": lots amount is not corresponding with lot step %f",m_symbol.LotsStep());
return(false);
}
//--- ok
return(true);
}
//+------------------------------------------------------------------+