preview
Overcoming Accessibility Problems in MQL5 Trading Tools (Part VIII): Building a Floating Risk Management Calculator Module for Any EA

Overcoming Accessibility Problems in MQL5 Trading Tools (Part VIII): Building a Floating Risk Management Calculator Module for Any EA

MetaTrader 5 — Examples |
72 0
Clemence Benjamin
Clemence Benjamin

Monitoring several positions requires distinguishing selected losses and stop estimates from pressure on the whole account. In this article, we build a reusable MQL5 calculator that reports these measurements separately and identifies missing data before a host Expert Advisor considers new exposure.

In Part VII of this series, we explored natural-language assistance through the Model Context Protocol (MCP) in MetaTrader 5. Here, we develop the measurement layer for monitoring exposure within an existing Expert Advisor.

Contents

  1. Introduction
  2. Defining the measurements before assigning a warning
  3. Designing a reusable snapshot contract
  4. Collecting positions without hiding missing data
  5. Estimating two different stop-loss quantities
  6. Classifying risk and preserving its causes
  7. Building accessible output without repeated alarms
  8. Integrating with an existing Expert Advisor
  9. Operating and verifying the calculator
  10. Understanding the boundaries
  11. Conclusion
  12. Key lessons
  13. Attachments


Introduction

A trader monitoring several positions needs to distinguish current losses, estimates for existing stops, unprotected positions, and pressure on the account as a whole. Our calculator organizes these observations into separate selected-position and account warnings. It is a monitoring policy, not a calibrated measure of total economic risk or a forecast of maximum loss.

For example, a $200 winner and a $150 loser produce a positive $50 net result but still contain $150 of gross loss. Both amounts use position profit plus accrued swap. A net-only test would conceal the losing ticket.

FloatingRisk_Net_And_Gross.png

Fig. 1. A positive net result does not eliminate the loss carried by another position

Figure 1 uses synthetic amounts. On a $10,000 capital basis, the gross loss is 1.5%; on $5,000 it is 3%. Choosing the denominator is therefore part of the policy. The calculator supports a fixed selected-capital amount as well as the default account balance.

The presentation follows W3C guidance on use of color by pairing color with written states and reasons. We also use its contrast guidance as a visual design reference. These choices do not establish screen-reader support, keyboard accessibility, or WCAG conformance for the MetaTrader interface. You should be familiar with Expert Advisor (EA) callbacks, structures, and position properties.


Defining the measurements before assigning a warning

Let Pᵢ denote the selected position’s floating profit, Sᵢ its accrued swap, B the whole-account balance, E account equity, and M used account margin. The index i runs over positions accepted by the selected filter. C is the selected capital basis: use InpSelectedCapital when positive; otherwise use B. Amounts are in account currency; B and C must both be positive for percentage reporting.

Measurement Definition Scope and interpretation
Floating profit plus swap Σ(Pᵢ + Sᵢ) Selected positions; winners and losers offset.
Gross floating loss Σ max(0, −(Pᵢ + Sᵢ)) Selected positions; excludes separate commissions and fees.
Gross loss percentage 100 × gross loss/C Selected capital, not necessarily the full account balance.
Known stop-subset loss Σ max(0, −(Qᵢ + Sᵢ)) Only positions with stops; displayed as N/A if there are none.
Stop-subset giveback Σ max(0, −Rᵢ) Current-to-stop decline for positions with stops; N/A if none.
Stop and giveback percentages 100 × the corresponding amount/C Separate conditions; the two quantities are never added.
Account equity gap 100 × max(0, B − E) / B Whole-account shortfall against balance, not historical drawdown.
Account margin level 100 × E/M, when M > 0 Whole account; N/A if no margin is used.
Stop coverage 100 × positions with stops / selected positions Count statistic only; not money-weighted protection or security.

Qᵢ is the estimated entry-to-stop result; Rᵢ is the estimated current-to-stop result, each for the position’s volume and direction. Both use account currency. A fixed C represents capital you explicitly allocate for supervision; the calculator does not maintain allocations across strategies or adjust them for deposits and withdrawals.

Profit plus accrued swap is deliberately a limited accounting basis. Commissions, fees, externally booked financing, future swap, and execution deviations are excluded from selected-position totals. Consequently, reported loss can be below the full economic loss. Account equity is read as reported by the terminal and may already reflect account-level charges or credit; it is not forced to equal balance plus the selected position sum.


Designing a reusable snapshot contract

The architecture separates measurement, classification, and presentation. CFloatingRiskManager creates a snapshot; the host schedules refreshes and owns every trading response. Neither the calculator nor the demonstration sends orders or modifies positions.

FloatingRiskManager_Architecture.png

Fig. 2. The host owns scheduling and responses while the calculator owns measurements

SRiskSnapshot carries explicit account-read validity, selected quote freshness, stop-estimate availability, and separate selected_level/account_level values and reason masks. The level field is the maximum of the two domain levels. Read selected_level and account_level with their reasons to identify which measurements produced the warning. A valid result means that the implemented checks succeeded, not that the account state is atomic or every economic risk is measured.

struct SRiskSnapshot
  {
   datetime          measured_at;
   bool              valid;
   bool              account_valid;
   bool              quotes_valid;
   bool              stop_estimates_available;
   ENUM_RISK_LEVEL   selected_level;
   ENUM_RISK_LEVEL   account_level;
   uint              selected_reasons;
   uint              account_reasons;
   double            selected_basis;
   double            decline_percent;
   string            diagnostics;
   bool              percentages_valid;
   bool              margin_applicable;
   ENUM_RISK_LEVEL   level;
   uint              reasons;
   int               errors;
   string            first_error;
   string            currency;
   int               positions;
   int               winners;
   int               losers;
   int               flat;
   int               with_stop;
   int               without_stop;
   double            net_result;
   double            gross_loss;
   double            gross_loss_percent;
   double            stop_loss;
   double            stop_loss_percent;
   double            remaining_to_stops;
   double            balance;
   double            equity;
   double            margin;
   double            free_margin;
   double            margin_level;
   double            equity_gap_percent;
   double            coverage_percent;
   ulong             worst_ticket;
   string            worst_symbol;
   double            worst_result;
  };

A missing stop estimate has its own availability flag. Zero remains meaningful when a priced stop exists and the entry-to-stop outcome is nonnegative; N/A means no selected stop was available. The display suppresses complete totals and usable severity labels when valid is false. The host calls CanOpenNewExposure immediately after Calculate and checks its result before considering a new entry.

//+------------------------------------------------------------------+
//| Validate the reusable module even when no demo EA is present     |
//+------------------------------------------------------------------+
bool CFloatingRiskManager::Configure(const SRiskConfig &c)
  {
//--- failed reconfiguration invalidates the previous policy
   m_configured=false;
   if(c.scope<RISK_ACCOUNT || c.scope>RISK_SYMBOL_MAGIC)
      return(false);
   if((c.scope==RISK_SYMBOL || c.scope==RISK_SYMBOL_MAGIC) && c.symbol=="")
      return(false);
   if(!MathIsValidNumber(c.selected_capital) || c.selected_capital<0.0 ||
      c.max_quote_age_seconds<1 || c.missing_stop_level<RISK_SAFE ||
      c.missing_stop_level>RISK_CRITICAL)
      return(false);
   double values[]= {c.decline_caution,c.decline_danger,c.decline_critical,
                     c.loss_caution,c.loss_danger,c.loss_critical,
                     c.stop_caution,c.stop_danger,c.stop_critical,
                     c.margin_caution,c.margin_danger,c.margin_critical
                    };
   for(int i=0;i<ArraySize(values);i++)
     {
      if(!MathIsValidNumber(values[i]) || values[i]<=0.0)
         return(false);
     }
   if(c.decline_caution>=c.decline_danger || c.decline_danger>=c.decline_critical ||
      c.loss_caution>=c.loss_danger || c.loss_danger>=c.loss_critical ||
      c.stop_caution>=c.stop_danger || c.stop_danger>=c.stop_critical ||
      c.margin_critical>=c.margin_danger || c.margin_danger>=c.margin_caution)
      return(false);
   m_config=c;
   m_configured=true;
   return(true);
  }

Configure invalidates the previous configuration before checking a new one. It requires finite positive thresholds in order, a nonnegative capital input, a positive quote-age limit, and a symbol for symbol-based scopes. Failure therefore cannot leave an old policy active. RiskDefaults initializes the full structure before you customize it.

We initialize every configuration field in RiskDefaults before applying host-specific settings. The values below define the demonstration policy; they are starting values for verification, not measured limits for your strategy.

//+------------------------------------------------------------------+
//| Set explicit educational defaults; these are not broker limits   |
//+------------------------------------------------------------------+
void RiskDefaults(SRiskConfig &c)
  {
//--- initialize every field before a host customizes the policy
   ZeroMemory(c);
   c.scope=RISK_ACCOUNT;
   c.loss_caution=2.0;
   c.loss_danger=5.0;
   c.loss_critical=10.0;
   c.stop_caution=3.0;
   c.stop_danger=6.0;
   c.stop_critical=10.0;
   c.margin_caution=500.0;
   c.margin_danger=250.0;
   c.margin_critical=150.0;
   c.selected_capital=0.0;
   c.decline_caution=3.0;
   c.decline_danger=6.0;
   c.decline_critical=10.0;
   c.missing_stop_level=RISK_CAUTION;
   c.block_without_stop=true;
   c.max_quote_age_seconds=60;
  }


Collecting positions without hiding missing data

The scanner uses the Boolean overloads of PositionGetDouble and the corresponding integer and string accessors. The PositionSelectByTicket documentation describes cached selection data, so each captured ticket is reselected before its properties are read. Filter failures remain data failures rather than silent exclusions.

//+------------------------------------------------------------------+
//| Match position properties without implying netting attribution   |
//+------------------------------------------------------------------+
bool CFloatingRiskManager::Matches(const string symbol,const long magic) const
  {
//--- account metrics are deliberately outside these filters
   if((m_config.scope==RISK_SYMBOL || m_config.scope==RISK_SYMBOL_MAGIC) &&
      symbol!=m_config.symbol)
      return(false);
   if((m_config.scope==RISK_MAGIC || m_config.scope==RISK_SYMBOL_MAGIC) &&
      magic!=m_config.magic)
      return(false);
   return(true);
  }

Account and symbol scopes work independently of strategy ownership. Magic zero is an exact match, not a wildcard. The runtime rejects magic-based scopes unless ACCOUNT_MARGIN_MODE identifies a retail hedging account. On netting accounts, use account or symbol scope; attributing a shared position to strategies requires a deal-level ledger that this module does not implement.

The class declaration in FloatingRiskManager.mqh supplies the default diagnostic code. A call with two arguments therefore uses code 0 for a semantic failure; calls that read GetLastError pass an explicit API code. Keep the default in the declaration only. The out-of-class definition below implements that same three-parameter method.

class CFloatingRiskManager
  {
private:
   SRiskConfig       m_config;
   bool              m_configured;
   bool              Matches(const string symbol,const long magic) const;
   void              Fail(SRiskSnapshot &s,const string message,const int code=0) const;
   int               UpperBand(const double value,const double caution,
                               const double danger,const double critical) const;
   void              Raise(SRiskSnapshot &s,const int level,const uint reason) const;
public:
                     CFloatingRiskManager(void);
   bool              Configure(const SRiskConfig &c);
   bool              Calculate(SRiskSnapshot &s);
   void              Evaluate(SRiskSnapshot &s) const;
   string            LevelText(const ENUM_RISK_LEVEL level) const;
   string            ScopeText(void) const;
   string            ReasonsText(const uint reasons) const;
   string            Summary(const SRiskSnapshot &s) const;
   bool              CanOpenNewExposure(const SRiskSnapshot &s) const;
   bool              QuoteFresh(const datetime now,const datetime quote_time) const;
  };
//+------------------------------------------------------------------+
//| Retain the first diagnostic and mark partial measurements invalid|
//+------------------------------------------------------------------+
void CFloatingRiskManager::Fail(SRiskSnapshot &s,const string message,const int code) const
  {
//--- zero is permitted for a semantic error rather than an API error
   s.valid=false;
   s.errors++;
   string diagnostic=StringFormat("%s (code %d)",message,code);
   if(s.first_error=="")
      s.first_error=diagnostic;
   if(StringFind(s.diagnostics,diagnostic)<0)
      s.diagnostics+=diagnostic+"; ";
  }

Fail marks the snapshot incomplete, retains the first diagnostic for the panel, and accumulates distinct diagnostic text for notifications. Final ticket-count and inventory comparisons detect common changes during a scan. They do not detect every same-ticket modification. They also do not lock position properties or make account and position reads simultaneous. Obtain a fresh calculation at the host decision point and retain broker-side protection; successful validation is not an atomic execution guarantee.

For every selected position, including those without stops, SymbolInfoTick supplies the most recent symbol tick time. QuoteFresh compares it with TimeTradeServer. The default age limit is 60 seconds; missing, future-dated, or older timestamps make the scan incomplete.

//+------------------------------------------------------------------+
//| Apply a conservative age bound to a selected symbol quote       |
//+------------------------------------------------------------------+
bool CFloatingRiskManager::QuoteFresh(const datetime now,const datetime quote_time) const
  {
//--- reject missing times and future timestamps rather than guessing freshness
   if(!m_configured || now<=0 || quote_time<=0 || quote_time>now)
      return(false);
   return(now-quote_time<=m_config.max_quote_age_seconds);
  }

This is a conservative freshness policy, not a universal market rule. TimeTradeServer is a client-calculated server-time estimate dependent on the computer clock; in the tester it follows simulated time. A quiet symbol or closed session may therefore block the gate. Choose a suitable positive limit for the supervised instruments. The check does not verify the ages of every conversion symbol used internally by valuation, nor refresh an existing snapshot after Calculate returns.

The complete scanner is CFloatingRiskManager::Calculate in the attached FloatingRiskManager.mqh. For direct comparison with the measurement table, inspect these blocks in that function: the ACCOUNT_MARGIN_MODE read and hedging check before scanning; account reads, selected_basis, percentages_valid, and margin_level before ticket collection; result=profit+swap with net_result and gross_loss inside the accepted-position loop; and the final gross_loss_percent, stop_loss_percent, decline_percent, equity_gap_percent, and coverage_percent assignments before Evaluate. These are implementation references to the attached function, not additional methods omitted from the class.


Estimating two different stop-loss quantities

The OrderCalcProfit function estimates a result in account currency under the current market environment. We call it separately from entry to stop and from the current closing-side price to stop. Accrued swap is included only in the entry-based result because it is already present in the current account state.

      ENUM_ORDER_TYPE order_type=(type==POSITION_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL);
      double at_stop=0.0,from_now=0.0;
      ResetLastError();
      if(!OrderCalcProfit(order_type,symbol,volume,entry,stop,at_stop) ||
         !MathIsValidNumber(at_stop))
         Fail(s,"Entry-to-stop estimate failed",GetLastError());
      else
         s.stop_loss+=MathMax(0.0,-(at_stop+swap));
      ResetLastError();
      if(!OrderCalcProfit(order_type,symbol,volume,current,stop,from_now) ||
         !MathIsValidNumber(from_now))
         Fail(s,"Current-to-stop estimate failed",GetLastError());
      else
         s.remaining_to_stops+=MathMax(0.0,-from_now);

Each call is checked independently; a failure marks the snapshot incomplete. A reached or crossed stop also triggers a diagnostic because execution may be pending. These estimates do not model slippage, gaps, partial fills, liquidity, future conversion changes, commissions, or the sequence in which stops execute.

FloatingRisk_Trailing_Stop.png

Fig. 3. A profitable trailing stop can still allow equity to decline from its current value

Figure 3 illustrates a 0.10-lot EURUSD buy with a 100,000-unit contract on a USD account, without costs. From entry 1.1000 to current price 1.1040, the position gains $40. A stop at 1.1020 retains $20: entry-to-stop loss is zero, but giveback is $20. Giveback has its own selected-policy thresholds so a large retreat in open profit can raise a warning even without a current loss.

Do not add gross floating loss to the entry-to-stop estimate, or add the two stop measures together: they describe different baselines. Summing individual negative stop outcomes without offsetting profitable stops is an arithmetic subtotal. It is neither a conservative bound on total portfolio loss nor a correlated scenario model. Unprotected positions and pending orders remain outside that subtotal.


Classifying risk and preserving its causes

The classifier keeps selected-position policy separate from account pressure. It retains all triggered reasons within each domain and uses the higher domain level only as a composite signal. Account warnings remain relevant to a host’s capacity to add exposure, even when their causes lie outside the selected filter.

Condition CAUTION DANGER CRITICAL
Selected gross loss/C ≥ 2% ≥ 5% ≥ 10%
Account equity gap/B ≥ 2% ≥ 5% ≥ 10%
Known stop-subset loss/C ≥ 3% ≥ 6% ≥ 10%
Known stop-subset giveback/C ≥ 3% ≥ 6% ≥ 10%
Account margin level when applicable ≤ 500% ≤ 250% ≤ 150%
Successfully read account equity — — ≤ 0
Missing stop Default display policy Configurable Configurable

These defaults are illustrative design choices, not empirically calibrated probabilities, broker margin-call levels, or recommended limits for every strategy. Equality enters the more severe band. Calibrate settings against your capital allocation, instrument specifications, strategy, and acceptable drawdown before practical use. SAFE means only that no severity threshold fired; it does not certify safety.

Calibrating a strategy-specific policy

For a worked configuration exercise, suppose we allocate $5,000 to a strategy within a $20,000 account. Set selected_capital to 5000 so a $150 selected gross loss is reported as 3%, while the account equity gap still uses the full balance. Choose the allocation from the capital the strategy is permitted to use, rather than selecting it to make a warning disappear.

Next, define the strategy's acceptable loss budget and operational response. For example, a hypothetical 4% selected-loss ceiling is $200 on that allocation. Candidate caution, danger, and critical bands of 1%, 2.5%, and 4% correspond to $50, $125, and $200. With the existing gate, new entries are blocked at DANGER, so blocking begins at $125 in this example. The same loss_caution, loss_danger, and loss_critical fields also classify the account equity gap against B; at a $20,000 balance, those bands correspond to $200, $500, and $800. Check that both budgets suit the strategy because this implementation does not expose separate account-gap thresholds. Configure stop-loss and giveback bands separately according to the strategy's stop and profit-retention rules. These candidate values illustrate the procedure and have not been validated for a trading strategy.

Read the intended account's ACCOUNT_MARGIN_SO_MODE, ACCOUNT_MARGIN_SO_CALL, and ACCOUNT_MARGIN_SO_SO, along with leverage and instrument margin specifications. A broker using monetary stop-out limits cannot be represented by percentage bands alone. Keep the module's percentage policy distinct and retain a host-level check appropriate to the broker's actual rules.

Replay representative normal and stressed periods on the intended symbols, including spread widening, swap, stale data, and unprotected positions. Compare when each candidate policy warns and blocks new exposure, how long it blocks, and whether that matches the chosen loss budget. Vary each threshold above and below its candidate value, then verify the selected settings on a separate period. Record the capital basis, account rules, data source, and configuration before adopting the policy.

//+------------------------------------------------------------------+
//| Evaluate measurements independently of terminal access           |
//+------------------------------------------------------------------+
void CFloatingRiskManager::Evaluate(SRiskSnapshot &s) const
  {
//--- an invalid configuration must never reuse old thresholds
   s.level=RISK_SAFE;
   s.selected_level=RISK_SAFE;
   s.account_level=RISK_SAFE;
   s.reasons=REASON_NONE;
   s.selected_reasons=REASON_NONE;
   s.account_reasons=REASON_NONE;
   if(!m_configured)
     {
      //--- return before any threshold or default-equity comparison
      Fail(s,"Module is not configured");
      s.reasons=REASON_DATA;
      return;
     }
   if(!s.account_valid)
      Fail(s,"Account measurements unavailable");
   if(!s.quotes_valid)
      Fail(s,"Selected quote freshness unavailable");
   if(!s.percentages_valid)
      Fail(s,"Percentage basis unavailable");
   if(s.percentages_valid)
     {
      //--- selected measurements use the selected capital basis
      Raise(s,UpperBand(s.gross_loss_percent,m_config.loss_caution,
                       m_config.loss_danger,m_config.loss_critical),REASON_GROSS_LOSS);
      if(s.stop_estimates_available)
        {
         //--- entry loss and giveback are distinct conditions, never added
         Raise(s,UpperBand(s.stop_loss_percent,m_config.stop_caution,
                          m_config.stop_danger,m_config.stop_critical),REASON_STOP_LOSS);
         Raise(s,UpperBand(s.decline_percent,m_config.decline_caution,
                          m_config.decline_danger,m_config.decline_critical),REASON_GIVEBACK);
        }
      if(s.account_valid)
         Raise(s,UpperBand(s.equity_gap_percent,m_config.loss_caution,
                          m_config.loss_danger,m_config.loss_critical),REASON_EQUITY_GAP);
     }
   if(s.without_stop>0)
     {
      //--- this is a configured protection policy, not a monetary estimate
      Raise(s,(int)m_config.missing_stop_level,REASON_NO_STOP);
      s.reasons|=REASON_NO_STOP;
      s.selected_reasons|=REASON_NO_STOP;
     }
   if(s.account_valid && s.equity<=0.0)
      Raise(s,RISK_CRITICAL,REASON_EQUITY);
   if(s.account_valid && s.margin_applicable)
     {
      //--- local margin bands do not reproduce broker liquidation rules
      if(s.margin_level<=m_config.margin_critical)
         Raise(s,RISK_CRITICAL,REASON_MARGIN);
      else if(s.margin_level<=m_config.margin_danger)
         Raise(s,RISK_DANGER,REASON_MARGIN);
      else if(s.margin_level<=m_config.margin_caution)
         Raise(s,RISK_CAUTION,REASON_MARGIN);
     }
   if(!s.valid)
      s.reasons|=REASON_DATA;
  }

Evaluate returns immediately if the calculator is unconfigured and evaluates nonpositive equity only when account_valid is true. Calculate sets that flag after successful finite account reads; the synthetic test producer sets it explicitly. Evaluate expects trusted, internally consistent snapshots and does not replace terminal validation.

A missing stop is a protection-policy observation, not a measured amount of downside. InpMissingStopLevel controls its displayed level; the default is CAUTION. Independently, InpBlockWithoutStop defaults to true and blocks new exposure whenever any selected position lacks a stop. Strategies that intentionally omit stops must explicitly choose their own policy. Counts do not equate a small position with a large one in monetary terms; their unmeasured downside remains unknown.

FloatingRiskManager_Concept.png

Fig. 4. Completeness and observed severity are evaluated independently before reporting

The diagram separates completeness from observed conditions. In the implementation, the maximum-severity step applies separately to selected measurements and account pressure. Diagnostic severity can remain in the snapshot after an incomplete scan, but the panel and summary present DATA INCOMPLETE without a usable level. The entry gate checks validity before considering either domain.


Building accessible output without repeated alarms

The panel uses a black background, white text, written domain labels, and supporting severity colors. InpFontSize ranges from 10 to 24 points, with 14 as the default. Lines wrap, and the background grows vertically; narrow windows may still require resizing. The display includes the capital basis, excluded costs, stop availability, and entry-gate status. N/A is shown for both stop fields when no selected stops exist.

Chart writes are checked, with a journal fallback on rendering failure. Each EA attachment owns a unique chart-object prefix and its own timer. Deinitialization deletes only that nonempty prefix. This implementation provides visual reporting and optional attention cues. Screen-reader support, keyboard-only operation, and assistive-technology compatibility have not been established.

//+------------------------------------------------------------------+
//| Coalesce changes; escalate immediately and throttle repeats      |
//+------------------------------------------------------------------+
void Notify(const SRiskSnapshot &s)
  {
//--- compare urgency with every observation, even when sending is deferred
   string key=StringFormat("%d|%d|%d|%u|",(int)s.valid,(int)s.selected_level,
                           (int)s.account_level,s.reasons)+s.diagnostics;
   bool urgent=g_observed && ((!s.valid && g_observed_valid) ||
               (int)s.selected_level>(int)g_observed_selected ||
               (int)s.account_level>(int)g_observed_account ||
               (s.reasons & ~g_observed_reasons)!=0 ||
               (!s.valid && s.diagnostics!=g_observed_diagnostics));
   g_observed=true;
   g_observed_valid=s.valid;
   g_observed_selected=s.selected_level;
   g_observed_account=s.account_level;
   g_observed_reasons=s.reasons;
   g_observed_diagnostics=s.diagnostics;
   if(key==g_last_key && !urgent)
      return;
   ulong now=GetTickCount64();
   if(g_last_key!="" && !urgent && now-g_last_notice<(ulong)InpAlertCooldownSeconds*1000)
      return;
   string report=g_manager.Summary(s);
   Print(report);
   if(!MQLInfoInteger(MQL_TESTER) && (!s.valid || s.level>RISK_SAFE || s.without_stop>0))
     {
      //--- output channels remain optional; report text is the primary cue
      if(InpPopup)
         Alert(report);
      if(InpSound && !PlaySound(InpSoundFile))
         PrintFormat("Sound unavailable: %s, error %d",InpSoundFile,GetLastError());
     }
   g_last_key=key;
   g_last_notice=now;
  }

The notification key contains validity, both domain levels, reasons, and diagnostic text. Urgency is compared with every observed state before the cooldown can suppress a message. A newly invalid state, a worsening domain, a newly added reason, or changed incomplete-data diagnostics is immediate. Recovery and other nonurgent changes wait for the default 30-second cooldown. A renewed escalation can be sent even if its key equals the last sent key.

Numeric changes within the same policy state do not produce notifications; the panel continues refreshing. This is transition reporting, not a complete audit stream of every price or monetary change. Urgent oscillations can generate repeated notices, and state resets on reattachment. Popup and sound default to false; PlaySound requires an available WAV file and does not run in the Strategy Tester.


Integrating with an existing Expert Advisor

Place FloatingRiskManager.mqh in MQL5/Include/FloatingRisk, the monitor in MQL5/Experts/Accessibility/FloatingRisk, and the policy script in MQL5/Scripts/Accessibility/FloatingRisk. Create a manager and snapshot, initialize SRiskConfig with RiskDefaults, and require Configure to succeed. After Calculate, call CanOpenNewExposure immediately for the decision being considered.

//+------------------------------------------------------------------+
//| Reject invalid data before considering either policy domain     |
//+------------------------------------------------------------------+
bool CFloatingRiskManager::CanOpenNewExposure(const SRiskSnapshot &s) const
  {
//--- this gate is necessary, not sufficient, for a host trading decision
   if(!m_configured || !s.valid || !s.account_valid || !s.quotes_valid || !s.percentages_valid)
      return(false);
   if(m_config.block_without_stop && s.without_stop>0)
      return(false);
   return(s.selected_level<RISK_DANGER && s.account_level<RISK_DANGER);
  }

A false result closes only the new-entry path. Existing stop management and exit logic must continue. A true result means the configured monitoring gate passed; the host still checks its signal, order size, proposed stop, trading permissions, and execution conditions. The gate does not estimate the risk of an order that has not yet been added, reserve margin, or keep a snapshot fresh over time.

Reuse the host’s timer if one already exists. The calculator itself creates no timer, chart object, or trade request. The demonstration marks its snapshot dirty in OnTradeTransaction, keeping the transaction callback short rather than assuming one event represents a complete trading operation.

//+------------------------------------------------------------------+
//| Keep the transaction callback short; never rebuild deal sequences|
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
                        const MqlTradeRequest &request,
                        const MqlTradeResult &result)
  {
//--- several transaction events may be coalesced into one refresh
   g_dirty=true;
  }

The next chart tick refreshes a dirty snapshot; the one-second default timer supplies a fallback. Several events may be coalesced. Neither this scheduling nor the ticket inventory check guarantees immediate or atomic observation.

//+------------------------------------------------------------------+
//| Service a transaction-dirty snapshot at the next chart tick      |
//+------------------------------------------------------------------+
void OnTick(void)
  {
//--- the timer remains the fallback when this symbol does not tick
   if(g_dirty)
      RefreshRisk();
  }


Operating and verifying the calculator

FloatingRiskPolicyTests defines 41 deterministic checks. We ran those cases and six notification-transition cases with a portable C++ compatibility harness: 47 passed and 0 failed. The harness extracts the production classification and notification code, excludes Calculate, and supplies replacements for terminal-dependent types and calls. It does not compile or execute the MQL5 programs in MetaTrader 5, and it does not test position scanning or broker valuation.

The portable run used Python 3 and g++ 13.3.0 on Linux, with -std=c++17 -Wall -Wextra. From the verification directory, launch python3 run/portable_check.py. The compatibility definitions, full generated harness, and complete output are included with the verification material. Its recorded summaries were:

POLICY CHECKS: 41 passed, 0 failed
PORTABLE TOTAL: 47 passed, 0 failed

We compiled FloatingRiskMonitorEA.mq5 with FloatingRiskManager.mqh in MetaEditor. The monitor compiler reported 0 errors and 0 warnings, with 1,591 ms elapsed. FloatingRiskPolicyTests.mq5 also compiled with FloatingRiskManager.mqh: 0 errors, 0 warnings, and 871 ms elapsed. We then launched FloatingRiskPolicyTests on an EURUSD H1 chart on October 31, 2026. The full Experts output contains 41 PASS entries and a summary of 41 passed and 0 failed:

2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Accept default policy
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: No exposure
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Below loss boundary
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Loss caution equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Loss danger equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Loss critical equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Stop danger equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Account gap affects filtered risk
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: No stop warning
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: No margin is N/A
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Margin caution equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Margin danger equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Margin critical equality
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Zero equity is critical
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Data gap preserves critical
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Multiple reasons survive
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Reject equal ascending thresholds
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Failed configuration cannot reuse stale policy
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Reject empty symbol filter
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Restore policy for regression cases
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Account pressure does not raise selected severity
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Account critical blocks entry
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Selected loss does not raise account severity
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Unread equity is not measured zero equity
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Unread account blocks entry
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Invalid SAFE snapshot blocks entry
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Missing stops block entry without inventing stop estimates
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Accept configured missing-stop severity
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Missing-stop severity is configurable
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Accept explicit informational stop policy
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Informational stop policy retains reason
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Giveback equality raises selected danger
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Valid no-trigger snapshot passes only risk gate
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Quote age equality accepted
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Expired quote rejected
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Future quote rejected
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Missing quote time rejected
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Reject disabled quote age check
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Invalid configuration exits without stale classification
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Reject negative selected capital
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) PASS: Reject equal giveback thresholds
2026.10.31 07:57:53.353 FloatingRiskPolicyTests (EURUSD,H1) POLICY CHECKS: 41 passed, 0 failed

These 41 checks ran in MQL5 and are separate from the 47 portable checks above. They exercise supplied snapshots rather than broker position prices. Both compiler logs include FloatingRiskManager.mqh and report no errors or warnings. MetaEditor and MetaTrader 5 are both Version 5.00 build 6230, dated September 25, 2026. The monitor tester agent also reported build 6230.

For the monitor demonstration, the tester agent reported build 6230. The configured chart was EURUSD H1, using the recorded demo-server history and generated Every tick mode, with an initial deposit of 10,000 USD and leverage of 1:100. The requested interval was January 1, 2024, through December 31, 2024. The log records startup and shutdown but provides no final tick/bar statistics or last simulated timestamp, so it does not establish completion of that interval. The verification log retains the server name and all recorded inputs.

The run used account scope, a balance-based selected capital amount, default threshold bands, missing-stop blocking, a 60-second maximum quote age, a one-second refresh timer, and a 30-second notification cooldown. Popup and sound output were disabled. The startup snapshot reported zero profit-plus-swap and gross loss, no selected stops, no used margin, both warning domains SAFE, and an eligible monitoring gate. This confirms the reported empty-account path; it does not test loss valuation, stop pricing, notification channels, or permission to execute an order.

Verifying a fixed EURUSD valuation example

Use the following as a specified arithmetic fixture to verify in a terminal, not as an observed market capture. Select EURUSD with a 100,000 EUR contract size per lot, USD profit currency, FOREX calculation mode, and a USD-denominated retail hedging demo account. Check that 0.10 lots is valid for the symbol's volume grid. Record the actual server, terminal and editor builds, date, leverage, margin rules, and symbol specifications; a suffix or different contract specification requires recalculation.

For one hypothetical BUY of 0.10 lots, use entry 1.10000, current Bid 1.09800, stop 1.09500, and accrued swap -1.50 USD. At these fixed prices, OrderCalcProfit should return -20.00 USD from entry to current, -50.00 USD from entry to stop, and -30.00 USD from current to stop. The account is already in the symbol's profit currency, so this example needs no currency conversion. These calls price hypothetical arguments and do not require placing an order.

The expected calculator fields for that one-position fixture are net_result=-21.50 USD, gross_loss=21.50 USD, stop_loss=51.50 USD, and remaining_to_stops=30.00 USD. With B=C=10,000 USD, expect gross_loss_percent=0.215%, stop_loss_percent=0.515%, decline_percent=0.300%, and coverage_percent=100%. If there are no other balance/equity effects, E=9,978.50 USD and equity_gap_percent=0.215%. Margin level must be checked against the actual read margin M using 100 × E/M; it is not inferred from these price inputs. Separate commissions and fees remain excluded from the selected totals.

First, compare the three fixed-price OrderCalcProfit results with this arithmetic. Next, run the monitor against a demo position and record its actual fill, current Bid, stop, profit, and swap at the comparison time. Use the actual values to recompute the same fields; do not expect a live quote to remain at the fixture price. This second step is needed to check Calculate's position reads and account metrics. In hedging mode, verify symbol/magic filtering with separately owned tickets. In netting mode, use account or symbol scope; requesting a magic-based scope must return an invalid snapshot and a blocked entry gate.

The fixed-price OrderCalcProfit comparison and the live-position Calculate comparison remain pending. Extend verification to mixed winners and losers, other account currencies and contract specifications, stale quotes, same-ticket changes, disconnection, notification transitions, and removal. Evaluate the visual interface with intended users. The portable checks support the tested policy paths; they do not establish terminal valuation or assistive-technology compatibility.


Understanding the boundaries

This calculator is a bounded supervision tool. Profit-plus-swap totals omit separate costs; stop subtotals exclude unprotected positions, pending orders, execution uncertainty, and correlation. Count coverage is descriptive only. The full account balance is a default denominator, not a universal strategy capital model. Local margin thresholds are separate from broker margin-call and stop-out rules.

The scan is not atomic. Quote timestamps bound age only for selected symbols and depend on the terminal’s time estimate; they do not prove fresh conversion quotes or stable position properties. Positive-price validation excludes instruments quoted at zero or negative prices. Magic-based filtering is unavailable on netting accounts, and no deal-level attribution ledger is provided.

Validity describes completion of the implemented checks. It does not establish full economic accounting, future execution quality, or accessibility for assistive technologies. You remain responsible for host-level controls and for testing the configured policy in the intended environment.


Conclusion

In this article, we built a reusable monitoring contract that separates selected-position warnings from account pressure, makes unavailable estimates explicit, and places validity ahead of entry decisions. We also defined configurable protection and giveback policies, a quote-age check, and notification transitions that retain diagnostic changes.

The MQL5 policy script passed all 41 checks, and the monitor compiled and initialized with an empty account. The portable harness also exercises notification transitions. Position valuation, notification channels, and assistive interaction still need direct testing. You can integrate the calculator into an EA while keeping order execution and economic accounting in the host.

Key lessons

Lesson
Separate measurement scopes
Check validity before policy
Distinguish missing from zero
Make policy choices explicit
Bound evidence to what ran
Keep costs and execution separate

Attachments

File Type Purpose
FloatingRiskManager.mqh MQL5 include Configuration, selected/account classification, snapshot collection, and entry gate.
FloatingRiskMonitorEA.mq5 Expert Advisor Read-only panel, scheduling, diagnostic notifications, and cleanup.
FloatingRiskPolicyTests.mq5 MQL5 script 41 deterministic configuration and policy assertions.


Attached files |
MQL5.zip (12.28 KB)
Estimate the Probability of Large-range Sessions (Part 1): Build and Check EURUSD Forecasts Estimate the Probability of Large-range Sessions (Part 1): Build and Check EURUSD Forecasts
We estimate the probability, at broker time 08:00, that EURUSD's 08:00–16:00 range exceeds 1.25 times the mean of the previous 20 complete, valid target ranges. A two-feature SPSS logistic regression model employs the completed 00:00–08:00 H1 window. Coefficients from 2020–2023 sessions are fixed for the 2024 holdout comparison against constant and 60-outcome rolling frequency baselines, using Brier scores and log losses. MQL5 scripts allow readers to extract broker data and recreate the probabilities in MetaTrader 5.
From Evidence to Execution (Part 2): Turning MetaTrader 5 AI Strategy Logs into Robust Rule Based EA From Evidence to Execution (Part 2): Turning MetaTrader 5 AI Strategy Logs into Robust Rule Based EA
This article turns the Part 1 evidence into a working MQL5 Expert Advisor. It formalizes entry conditions, designs direction-aware stop loss and take profit, and stress-tests the combined logic before coding, with every rule linked to measured features. Readers receive a reproducible implementation with configurable inputs and per-bar execution consistent with the study.
Features of Experts Advisors Features of Experts Advisors
Creation of expert advisors in the MetaTrader trading system has a number of features.
Measuring What Matters (Part 5): The Risk Concentration Indicator — Plotting Eigenvalue Dominance Live Measuring What Matters (Part 5): The Risk Concentration Indicator — Plotting Eigenvalue Dominance Live
This article presents a live MQL5 indicator for monitoring portfolio risk concentration through eigenvalue decomposition. The indicator calculates rolling covariance statistics for three instruments using persistent prefix sums, aligns price data by timestamp, handles missing observations, and plots the dominant eigenvalue as a percentage of total portfolio variance. The implementation provides a practical way to observe changes in portfolio risk concentration as the covariance structure evolves over time.