2025-05-30 16:35:54 +02:00 | | | //+------------------------------------------------------------------+
|
| | | //| MoneyFixedLot.mqh |
|
| | | //| Copyright 2000-2023, MetaQuotes Ltd. |
|
| | | //| https://www.mql5.com |
|
| | | //+------------------------------------------------------------------+
|
| | | #include "..\Expert\ExpertMoneyCustom.mqh"
|
 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 | | | #include "..\Variables\RiskBudget.mqh"
|
2025-05-30 16:35:54 +02:00 | | | // 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;
|
2026-07-26 23:08:32 -04:00 | | | //--- declared ahead of the inline CheckOpenLong/Short bodies below that call it
|
 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 | | | double CheckedLot(ENUM_ORDER_TYPE type, double price, double sl);
|
2025-05-30 16:35:54 +02:00 | | |
|
| | | public:
|
| | | //---
|
| | | void Lots(double lots) { m_lots=lots; }
|
| | | virtual bool ValidationSettings(void);
|
2026-07-26 23:08:32 -04:00 | | | //--- 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.
|
 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 | | | 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)); }
|
2025-05-30 16:35:54 +02:00 | | | };
|
| | | //+------------------------------------------------------------------+
|
2026-07-26 23:08:32 -04:00 | | | //| 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". |
|
| | | //+------------------------------------------------------------------+
|
 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 | | | double CMoneyFixedLot::CheckedLot(ENUM_ORDER_TYPE type, double price, double sl)
|
2026-07-26 23:08:32 -04:00 | | | {
|
| | | double lot = m_lots;
|
 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 | | | //--- 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);
|
| | | }
|
| | | }
|
| | | }
|
2026-07-26 23:08:32 -04:00 | | | string description;
|
| | | if(!ValidateLotForTrade(MoneySymbol(), lot, type, description))
|
| | | return(0.0);
|
| | | return(lot);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|
2025-05-30 16:35:54 +02:00 | | | //| 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);
|
| | | }
|
2026-07-26 23:08:32 -04:00 | | | //--- 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);
|
| | | }
|
2025-05-30 16:35:54 +02:00 | | | 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);
|
| | | }
|
| | | //+------------------------------------------------------------------+
|