StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //+------------------------------------------------------------------+
|
| | | //| AccountRisk.mqh|
|
| | | //| Copyright 2026, bandersoft |
|
| | | //| https://www.mql5.com/en/users/fats |
|
| | | //+------------------------------------------------------------------+
|
| | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Total money at risk across open positions. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
2026-10-16 21:48:01 +03:00 | | | //| Read-only: it inspects positions and calculates. |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| |
|
| | | //| The number worth having is not "how much am I risking per |
|
| | | //| trade" but "if every stop I have is hit, what do I lose". Those |
|
| | | //| differ once positions overlap, and the second one is the one |
|
| | | //| that ends accounts. |
|
| | | //| |
|
| | | //| Positions with no stop loss are the interesting case. Their |
|
| | | //| risk is unbounded, so they cannot be summed into a total |
|
| | | //| without making it a lie. They are counted and reported |
|
| | | //| separately, never silently skipped and never treated as zero. |
|
| | | //| |
|
| | | //+------------------------------------------------------------------+
|
| | | #ifndef STOPFIRST_ACCOUNTRISK_MQH
|
| | | #define STOPFIRST_ACCOUNTRISK_MQH
|
| | |
|
| | | #include <Trade\PositionInfo.mqh>
|
| | | #include <Trade\AccountInfo.mqh>
|
| | | #include "Defs.mqh"
|
| | | #include "Symbol.mqh"
|
| | | #include "Risk.mqh" // PipValuePerLot - this header did rely on includers for it
|
| | | #include "Positions.mqh" // PositionCommission
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| What one open position stands to lose |
|
| | | //+------------------------------------------------------------------+
|
| | | struct SPositionRisk
|
| | | {
|
| | | ulong ticket;
|
| | | string symbol;
|
| | | ENUM_POSITION_TYPE type;
|
| | | double volume;
|
| | | double openPrice;
|
| | | double stopLoss; // 0 when none is set
|
| | | double riskMoney; // account currency; 0 when unbounded
|
| | | double riskPips; // distance to the stop; 0 when unbounded
|
| | | bool bounded; // false when no stop loss is set
|
| | | double currentProfit; // profit + swap + commission
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| The account-level picture |
|
| | | //+------------------------------------------------------------------+
|
| | | struct SAccountRisk
|
| | | {
|
| | | int positions; // matching the filter
|
| | | int bounded; // with a stop loss
|
| | | int unbounded; // without one
|
| | | double riskMoney; // total loss if every stop is hit
|
| | | double riskPercent; // that loss as a share of balance
|
| | | double unboundedVolume; // lots with no stop behind them
|
| | | double openProfit; // current floating P/L of the same set
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Money a position loses if its stop is hit |
|
| | | //| |
|
| | | //| Uses the same tick-value route as the sizing functions, so the |
|
| | | //| number agrees with what CalcValueAtRisk said when the trade was |
|
| | | //| planned. |
|
| | | //| |
|
| | | //| Returns: the loss in account currency, or 0 when there is no stop |
|
| | | //| or the symbol reports no usable tick data. Check |
|
| | | //| `bounded` rather than testing the return for 0. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool PositionRisk(const ulong ticket, SPositionRisk &risk)
|
| | | {
|
| | | CPositionInfo position;
|
| | | if(!position.SelectByTicket(ticket)) return false;
|
| | |
|
| | | risk.ticket = ticket;
|
| | | risk.symbol = position.Symbol();
|
| | | risk.type = position.PositionType();
|
| | | risk.volume = position.Volume();
|
| | | risk.openPrice = position.PriceOpen();
|
| | | risk.stopLoss = position.StopLoss();
|
| | | risk.currentProfit = position.Profit() + position.Swap() + PositionCommission(ticket);
|
| | | risk.riskMoney = 0;
|
| | | risk.riskPips = 0;
|
| | | risk.bounded = (risk.stopLoss > 0);
|
| | |
|
| | | if(!risk.bounded) return true;
|
| | |
|
| | | double pipSize = GetPipSize(risk.symbol);
|
| | | if(pipSize <= 0)
|
| | | {
|
| | | risk.bounded = false; // cannot express the distance, so cannot bound it
|
| | | return true;
|
| | | }
|
| | |
|
| | | bool isBuy = (risk.type == POSITION_TYPE_BUY);
|
| | | double distance = isBuy ? (risk.openPrice - risk.stopLoss)
|
| | | : (risk.stopLoss - risk.openPrice);
|
| | |
|
| | | //--- a stop already past entry is locking profit in, not risking
|
| | | //--- money. Clamp at zero rather than reporting a negative risk.
|
| | | if(distance < 0) distance = 0;
|
| | |
|
| | | risk.riskPips = distance / pipSize;
|
| | |
|
| | | double pipValue = PipValuePerLot(risk.symbol);
|
| | | if(pipValue <= 0)
|
| | | {
|
| | | risk.bounded = false;
|
| | | risk.riskPips = 0;
|
| | | return true;
|
| | | }
|
| | |
|
| | | risk.riskMoney = NormalizeDouble(risk.volume * risk.riskPips * pipValue, 2);
|
| | |
|
| | | return true;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Total risk across matching open positions |
|
| | | //| |
|
| | | //| Parameters: |
|
2026-10-16 21:48:01 +03:00 | | | //| symbol - "" for every symbol |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| magic - STOPFIRST_ANY_MAGIC for any |
|
| | | //| |
|
| | | //| riskMoney sums only positions that HAVE a stop. Positions without |
|
| | | //| one are counted in `unbounded` and their volume in |
|
| | | //| `unboundedVolume`. Read both: a small riskMoney beside a non-zero |
|
| | | //| unbounded count is not a safe account, it is an unmeasured one. |
|
| | | //| |
|
| | | //| Returns: the filled summary. All zeros when nothing matches. |
|
| | | //+------------------------------------------------------------------+
|
| | | SAccountRisk TotalAccountRisk(const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC)
|
| | | {
|
| | | SAccountRisk total;
|
| | | total.positions = 0;
|
| | | total.bounded = 0;
|
| | | total.unbounded = 0;
|
| | | total.riskMoney = 0;
|
| | | total.riskPercent = 0;
|
| | | total.unboundedVolume = 0;
|
| | | total.openProfit = 0;
|
| | |
|
| | | CPositionInfo position;
|
| | |
|
| | | for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| | | {
|
| | | if(!position.SelectByIndex(i)) continue;
|
| | | if(symbol != "" && position.Symbol() != symbol) continue;
|
| | | if(magic != STOPFIRST_ANY_MAGIC && position.Magic() != magic) continue;
|
| | |
|
| | | SPositionRisk risk;
|
| | | if(!PositionRisk(position.Ticket(), risk)) continue;
|
| | |
|
| | | total.positions++;
|
| | | total.openProfit += risk.currentProfit;
|
| | |
|
| | | if(risk.bounded)
|
| | | {
|
| | | total.bounded++;
|
| | | total.riskMoney += risk.riskMoney;
|
| | | }
|
| | | else
|
| | | {
|
| | | total.unbounded++;
|
| | | total.unboundedVolume += risk.volume;
|
| | | }
|
| | | }
|
| | |
|
| | | total.riskMoney = NormalizeDouble(total.riskMoney, 2);
|
| | | total.openProfit = NormalizeDouble(total.openProfit, 2);
|
| | |
|
| | | CAccountInfo account;
|
| | | double balance = account.Balance();
|
| | | if(balance > 0)
|
| | | total.riskPercent = NormalizeDouble((total.riskMoney / balance) * 100.0, 2);
|
| | |
|
| | | return total;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Collect the per-position detail behind the total |
|
| | | //| |
|
| | | //| Parameters: |
|
| | | //| risks - receives one entry per matching position |
|
| | | //| |
|
| | | //| Returns: how many entries were written. |
|
| | | //+------------------------------------------------------------------+
|
| | | int CollectPositionRisks(SPositionRisk &risks[], const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC)
|
| | | {
|
| | | ArrayResize(risks, 0);
|
| | |
|
| | | CPositionInfo position;
|
| | | int count = 0;
|
| | |
|
| | | for(int i = PositionsTotal() - 1; i >= 0; i--)
|
| | | {
|
| | | if(!position.SelectByIndex(i)) continue;
|
| | | if(symbol != "" && position.Symbol() != symbol) continue;
|
| | | if(magic != STOPFIRST_ANY_MAGIC && position.Magic() != magic) continue;
|
| | |
|
| | | SPositionRisk risk;
|
| | | if(!PositionRisk(position.Ticket(), risk)) continue;
|
| | |
|
| | | ArrayResize(risks, count + 1);
|
| | | risks[count] = risk;
|
| | | count++;
|
| | | }
|
| | |
|
| | | return count;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Lots with no stop behind them |
|
| | | //| |
|
| | | //| A single number for a panel or an alert. Anything above zero |
|
| | | //| means the account's real exposure cannot be calculated. |
|
| | | //| |
|
| | | //| Returns: summed volume of matching positions with no stop loss. |
|
| | | //+------------------------------------------------------------------+
|
| | | double UnboundedVolume(const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC)
|
| | | {
|
| | | SAccountRisk total = TotalAccountRisk(symbol, magic);
|
| | |
|
| | | return total.unboundedVolume;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| What pending orders would risk if they all filled |
|
| | | //+------------------------------------------------------------------+
|
| | | struct SPendingRisk
|
| | | {
|
| | | int orders; // pending orders matching the filter
|
| | | int unbounded; // of them, without a stop loss
|
| | | double riskMoney; // loss at their stops, once filled
|
| | | double unboundedVolume; // lots that would fill with no stop
|
| | | };
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Risk carried by pending orders, as if every one of them filled |
|
| | | //| |
|
| | | //| A pending order with a stop is a position-in-waiting: ten of them |
|
| | | //| at 1% each are 10% the moment the market touches them. Counting |
|
2026-10-16 21:48:01 +03:00 | | | //| only open positions would let each one pass a 5% ceiling on its |
|
| | | //| own. Stops are measured from the order's |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| own price, the price it would fill at. |
|
| | | //| |
|
| | | //| Parameters: |
|
2026-10-16 21:48:01 +03:00 | | | //| symbol - "" for every symbol |
|
StopFirst 1.00, the read-only core
Fourteen headers, a header-only MQL5 library: lot size from risk, pips on
forex, gold and indices, broker checks, bars and candle patterns, cached ATR,
sessions in GMT, account risk, symbol suffixes, a calendar filter. Sends no
trade requests. Plus the LotByRisk example script.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-16 21:40:34 +03:00 | | | //| magic - STOPFIRST_ANY_MAGIC for any |
|
| | | //| |
|
| | | //| Returns: the summary. All zeros when nothing matches. |
|
| | | //+------------------------------------------------------------------+
|
| | | SPendingRisk PendingOrdersRisk(const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC)
|
| | | {
|
| | | SPendingRisk total;
|
| | | total.orders = 0;
|
| | | total.unbounded = 0;
|
| | | total.riskMoney = 0;
|
| | | total.unboundedVolume = 0;
|
| | |
|
| | | for(int i = OrdersTotal() - 1; i >= 0; i--)
|
| | | {
|
| | | ulong ticket = OrderGetTicket(i);
|
| | | if(ticket == 0) continue;
|
| | |
|
| | | string orderSymbol = OrderGetString(ORDER_SYMBOL);
|
| | | if(symbol != "" && orderSymbol != symbol) continue;
|
| | | if(magic != STOPFIRST_ANY_MAGIC && (ulong)OrderGetInteger(ORDER_MAGIC) != magic)
|
| | | continue;
|
| | |
|
| | | ENUM_ORDER_TYPE type = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
|
| | | if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_SELL) continue; // not pending
|
| | |
|
| | | double volume = OrderGetDouble(ORDER_VOLUME_CURRENT);
|
| | | double price = OrderGetDouble(ORDER_PRICE_OPEN);
|
| | | double stop = OrderGetDouble(ORDER_SL);
|
| | | total.orders++;
|
| | |
|
| | | double pipSize = GetPipSize(orderSymbol);
|
| | | double pipValue = PipValuePerLot(orderSymbol);
|
| | | if(stop <= 0 || pipSize <= 0 || pipValue <= 0)
|
| | | {
|
| | | total.unbounded++;
|
| | | total.unboundedVolume += volume;
|
| | | continue;
|
| | | }
|
| | |
|
| | | bool isBuy = IsBuySide(type);
|
| | | double distance = isBuy ? price - stop : stop - price;
|
| | | if(distance < 0) distance = 0;
|
| | |
|
| | | total.riskMoney += volume * (distance / pipSize) * pipValue;
|
| | | }
|
| | |
|
| | | total.riskMoney = NormalizeDouble(total.riskMoney, 2);
|
| | | return total;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Would opening this position breach a total-risk ceiling? |
|
| | | //| |
|
| | | //| Parameters: |
|
| | | //| maxRiskPercent - ceiling on combined risk, e.g. 5.0 |
|
| | | //| includePending - also count pending orders as if filled. Off |
|
| | | //| by default to keep existing callers' results; |
|
| | | //| a panel placing pending orders wants it on. |
|
| | | //| |
|
| | | //| Positions and orders with no stop are NOT counted, because their |
|
| | | //| risk is unknown rather than zero. Check UnboundedVolume |
|
| | | //| separately; this function cannot see what it cannot measure. |
|
| | | //| |
|
| | | //| Returns: true if the existing risk plus this trade stays at or |
|
| | | //| below the ceiling. false if it would exceed it, or if |
|
| | | //| the inputs are unusable. |
|
| | | //+------------------------------------------------------------------+
|
| | | bool RiskBudgetAllows(const double additionalRiskMoney, const double maxRiskPercent,
|
| | | const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC,
|
| | | const bool includePending = false)
|
| | | {
|
| | | if(additionalRiskMoney < 0 || maxRiskPercent <= 0) return false;
|
| | |
|
| | | CAccountInfo account;
|
| | | double balance = account.Balance();
|
| | | if(balance <= 0) return false;
|
| | |
|
| | | SAccountRisk total = TotalAccountRisk(symbol, magic);
|
| | | double combined = total.riskMoney + additionalRiskMoney;
|
| | |
|
| | | if(includePending)
|
| | | combined += PendingOrdersRisk(symbol, magic).riskMoney;
|
| | |
|
| | | return (combined / balance) * 100.0 <= maxRiskPercent;
|
| | | }
|
| | |
|
| | | //+------------------------------------------------------------------+
|
| | | //| Multi-line risk summary, for a log line or Comment() |
|
| | | //| Returns: the report. Never empty. |
|
| | | //+------------------------------------------------------------------+
|
| | | string AccountRiskReport(const string symbol = "",
|
| | | const ulong magic = STOPFIRST_ANY_MAGIC)
|
| | | {
|
| | | SAccountRisk total = TotalAccountRisk(symbol, magic);
|
| | | string currency = AccountInfoString(ACCOUNT_CURRENCY);
|
| | |
|
| | | string report = "=== Account risk ===\n";
|
| | | report += "Open positions: " + IntegerToString(total.positions) + "\n";
|
| | | report += "With a stop: " + IntegerToString(total.bounded) + "\n";
|
| | | report += "Risk if all hit: " + DoubleToString(total.riskMoney, 2) + " " +
|
| | | currency + " (" + DoubleToString(total.riskPercent, 2) + "%)\n";
|
| | | report += "Open P/L: " + DoubleToString(total.openProfit, 2) + " " +
|
| | | currency + "\n";
|
| | |
|
| | | if(total.unbounded > 0)
|
| | | report += "UNBOUNDED: " + IntegerToString(total.unbounded) +
|
| | | " position(s), " + DoubleToString(total.unboundedVolume, 2) +
|
| | | " lots with no stop - total risk above EXCLUDES these\n";
|
| | | else
|
| | | report += "Unbounded: none\n";
|
| | |
|
| | | return report;
|
| | | }
|
| | |
|
| | | #endif // STOPFIRST_ACCOUNTRISK_MQH
|
| | | //+------------------------------------------------------------------+
|