Page 1 of 2

Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 2:35 pm
by FXS London Scalper
Per-trade risk limits are standard: 0.5% or 1% per trade. But if you have three trades on the same idea, the actual risk is triple.

What's a "theme"?
- A USD-wide move: long EURUSD, long GBPUSD and short USDCHF are all short USD.
- Risk-off: long JPY, long gold, short AUD.
- A single event: positions in several pairs all depending on one data release.

A theme-based risk cap:
- Maximum 1% per trade.
- Maximum 1.5% per theme.
- Maximum 3% total open risk.

Before opening a new trade, ask: what theme does this belong to? Is there already exposure to that theme?

Implementation:
- Keep a simple list of open positions with their theme.
- If adding a position would exceed the theme cap, either skip it or reduce size.

This prevents the common situation where a trader thinks they're diversified but is actually making one big bet across several pairs.

Do you track exposure by theme? How do you define your themes?

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:21 pm
by FTtrader
FXS London Scalper wrote: Thu Oct 22, 2026 2:35 pm Per-trade risk limits are standard: 0.5% or 1% per trade. But if you have three trades on the same idea, the actual risk is triple.

What's a "theme"?
- A USD-wide move: long EURUSD, long GBPUSD and short USDCHF are all short USD.
- Risk-off: long JPY, long gold, short AUD.
- A single event: positions in several pairs all depending on one data release.

A theme-based risk cap:
- Maximum 1% per trade.
- Maximum 1.5% per theme.
- Maximum 3% total open risk.

Before opening a new trade, ask: what theme does this belong to? Is there already exposure to that theme?

Implementation:
- Keep a simple list of open positions with their theme.
- If adding a position would exceed the theme cap, either skip it or reduce size.

This prevents the common situation where a trader thinks they're diversified but is actually making one big bet across several pairs.

Do you track exposure by theme? How do you define your themes?
Hello,

The Illusion of Diversification: Why You Need Theme-Based Risk Limits

Per-trade risk limits (like 0.5% or 1% per trade) are a standard starting point for risk management, but they leave a massive blind spot. If you take three simultaneous trades based on the same underlying market driver, your actual risk is triple what you think it is. You aren't diversified; you are just splitting one large bet across different tickers.

A "theme" is the core market driver behind a setup:

A USD-wide move: Being long EURUSD, long GBPUSD, and short USDCHF are not three independent trades. They are all expressing a short-USD theme.

Risk-off: Going long JPY, long Gold, and short AUD all map to the same fundamental flight-to-safety theme.

A single event: Taking positions in multiple GBP and EUR crosses ahead of a major Bank of England rate decision clusters your risk around a single volatility catalyst.

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:22 pm
by FTtrader
To survive these correlated drawdowns, implement a theme-based risk cap. A standard professional framework looks like this:

Maximum 1% per trade.

Maximum 1.5% per theme.

Maximum 3% total open risk across all positions.

Before executing a new setup, run it through this logic: What theme does this belong to? Do I already have exposure to this theme? Keep a simple running list or code a script to track your active themes. If adding a new position exceeds the 1.5% theme cap, you either skip the trade or reduce the lot size to fit the remaining allowance.

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:22 pm
by FTtrader
Below is an MQL5 script demonstrating how to programmatically enforce these caps before allowing a new trade. It maps symbols to themes, calculates current open risk via stop-loss distances, and blocks execution if limits are breached.

Code: Select all

//+------------------------------------------------------------------+
//|                                              ThemeRiskLimits.mq5 |
//|                                      Copyright 2026, ThemeTrader |
//+------------------------------------------------------------------+
#property copyright "ThemeTrader"
#property version   "1.00"
#property strict

// Risk caps (Percentage of account balance)
input double MaxRiskPerTrade = 1.0;
input double MaxRiskPerTheme = 1.5;
input double MaxTotalOpenRisk = 3.0;

// 1. Map symbols to underlying themes
string GetTheme(string symbol)
  {
   if(StringFind(symbol, "USD") >= 0) return "USD_Wide";
   if(StringFind(symbol, "JPY") >= 0 || symbol == "XAUUSD") return "Risk_Off";
   if(StringFind(symbol, "GBP") >= 0) return "UK_Rates";
   return "Uncorrelated";
  }

// 2. Calculate the exact risk % of an open position based on its Stop Loss
double GetPositionRiskPct(ulong ticket)
  {
   if(PositionSelectByTicket(ticket))
     {
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
      double slPrice = PositionGetDouble(POSITION_SL);
      double volume = PositionGetDouble(POSITION_VOLUME);
      string symbol = PositionGetString(POSITION_SYMBOL);

      if(slPrice == 0) return 0.0; // Warning: Uncapped risk

      double tickValue = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
      double tickSize = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);

      if(tickSize > 0)
        {
         double riskPoints = MathAbs(openPrice - slPrice) / tickSize;
         double riskMoney = riskPoints * tickValue * volume;
         double balance = AccountInfoDouble(ACCOUNT_BALANCE);

         if(balance > 0) return (riskMoney / balance) * 100.0;
        }
     }
   return 0.0;
  }

// 3. Pre-trade check against theme and total caps
bool CanOpenTrade(string newSymbol, double newTradeRiskPct)
  {
   string newTheme = GetTheme(newSymbol);
   double currentThemeRisk = 0.0;
   double currentTotalRisk = 0.0;

   // Aggregate risk across all open positions
   int totalPositions = PositionsTotal();
   for(int i = 0; i < totalPositions; i++)
     {
      ulong ticket = PositionGetTicket(i);
      if(ticket > 0)
        {
         string posSymbol = PositionGetString(POSITION_SYMBOL);
         string posTheme = GetTheme(posSymbol);
         double posRisk = GetPositionRiskPct(ticket);

         currentTotalRisk += posRisk;
         if(posTheme == newTheme)
           {
            currentThemeRisk += posRisk;
           }
        }
     }

   // Rule 1: Per-Trade Cap
   if(newTradeRiskPct > MaxRiskPerTrade)
     {
      PrintFormat("REJECTED: Proposed trade risk (%.2f%%) exceeds per-trade limit (%.2f%%).", newTradeRiskPct, MaxRiskPerTrade);
      return false;
     }

   // Rule 2: Theme Cap
   if((currentThemeRisk + newTradeRiskPct) > MaxRiskPerTheme)
     {
      PrintFormat("REJECTED: Theme '%s' risk would reach %.2f%%, exceeding limit (%.2f%%).", newTheme, currentThemeRisk + newTradeRiskPct, MaxRiskPerTheme);
      return false;
     }

   // Rule 3: Total Account Cap
   if((currentTotalRisk + newTradeRiskPct) > MaxTotalOpenRisk)
     {
      PrintFormat("REJECTED: Total open risk would reach %.2f%%, exceeding limit (%.2f%%).", currentTotalRisk + newTradeRiskPct, MaxTotalOpenRisk);
      return false;
     }

   PrintFormat("APPROVED: Trade meets all risk parameters. Theme: %s, Total Risk after trade: %.2f%%", newTheme, currentTotalRisk + newTradeRiskPct);
   return true;
  }

// 4. Test execution logic
void OnStart()
  {
   string targetSymbol = "EURUSD";
   double proposedTradeRisk = 0.8; // User intends to risk 0.8%

   Print("--- Theme Risk Integrity Check ---");
   CanOpenTrade(targetSymbol, proposedTradeRisk);
  }
//+------------------------------------------------------------------+

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:23 pm
by FTtrader
Hardcoding themes relies on static assumptions, which fail when cross-asset dynamics shift. A live correlation matrix constantly measures the statistical relationship between price movements over a rolling window, exposing hidden risk clusters—like an unexpected alignment between CAD and equity indices—before you size your trades.

The core engine of this calculation is the Pearson correlation coefficient, which quantifies the linear relationship between two price arrays from -1.0 (perfectly inversely correlated) to +1.0 (perfectly correlated).

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:23 pm
by FTtrader
To build this in MQL5, you need to pull historical close prices for a defined basket of symbols, run them through the Pearson formula, and output the matrix. Highly correlated pairs (usually $r > 0.75$ or $r < -0.75$) belong to the same active theme.

Code: Select all

//+------------------------------------------------------------------+
//|                                     LiveCorrelationMatrix.mq5    |
//+------------------------------------------------------------------+
#property strict

input int LookbackBars = 100;
input ENUM_TIMEFRAMES MatrixTimeframe = PERIOD_H4;

// Define your universe of symbols to track
string Symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD", "XAUUSD"};

// Calculates the Pearson correlation coefficient between two arrays
double PearsonCorrelation(const double &x[], const double &y[])
  {
   int n = ArraySize(x);
   if(n == 0 || n != ArraySize(y)) return 0.0;
   
   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
   
   for(int i = 0; i < n; i++)
     {
      sumX += x[i];
      sumY += y[i];
      sumXY += x[i] * y[i];
      sumX2 += x[i] * x[i];
      sumY2 += y[i] * y[i];
     }
     
   double numerator = (n * sumXY) - (sumX * sumY);
   double denominator = MathSqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
   
   if(denominator == 0) return 0.0;
   return numerator / denominator;
  }

void OnStart()
  {
   int symCount = ArraySize(Symbols);
   double priceData[][];
   ArrayResize(priceData, symCount);
   
   // Fetch price data for all symbols
   for(int i = 0; i < symCount; i++)
     {
      double closes[];
      int copied = CopyClose(Symbols[i], MatrixTimeframe, 0, LookbackBars, closes);
      
      if(copied < LookbackBars)
        {
         Print("Failed to download sufficient data for ", Symbols[i]);
         return;
        }
        
      ArrayResize(priceData[i], LookbackBars);
      ArrayCopy(priceData[i], closes);
     }

   // Generate and print the matrix
   Print("--- Live Correlation Matrix (", EnumToString(MatrixTimeframe), ", ", LookbackBars, " bars) ---");
   
   string headerRow = "Symbol";
   for(int i = 0; i < symCount; i++) headerRow += " | " + Symbols[i];
   Print(headerRow);

   for(int i = 0; i < symCount; i++)
     {
      string rowString = Symbols[i];
      
      for(int j = 0; j < symCount; j++)
        {
         if(i == j)
           {
            rowString += " |  1.00";
           }
         else
           {
            double r = PearsonCorrelation(priceData[i], priceData[j]);
            rowString += StringFormat(" | %5.2f", r);
           }
        }
      Print(rowString);
     }
  }
//+------------------------------------------------------------------+

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:23 pm
by FTtrader
To integrate this programmatically with risk management, replace the printed output with a logic check. Before an EA opens a trade on AUDUSD, it loops through your active positions, calls PearsonCorrelation() against the AUDUSD close prices, and if any existing position returns an absolute correlation above $0.80$, it flags them as the same "theme" and applies the 1.5% combined risk cap.

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:24 pm
by FTtrader
Standard correlation calculated on raw close prices is highly vulnerable to outliers. A single massive news candle can distort the calculation, making two loosely related pairs look highly correlated over a 100-bar lookback simply because they both spiked on the same day.

To fix this, institutional models use volatility-adjusted correlation. Instead of correlating the raw prices, you correlate the ATR-normalized returns.

Code: Select all

The formula becomes:$Normalized Return = \frac{Close_i - Close_{i-1}}{ATR_i}$
By converting price changes into "ATRs moved," a 100-pip move on a volatile pair (like GBPJPY) carries the same statistical weight as a 30-pip move on a quiet pair (like EURGBP). This prevents high-volatility pairs from hijacking the correlation matrix.

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:25 pm
by FTtrader
Here is the MQL5 implementation. It pulls the ATR indicator for each symbol, calculates the period-to-period price change, divides that change by the current bar's ATR, and runs the Pearson correlation on those normalized values.

Code: Select all

//+------------------------------------------------------------------+
//|                                VolatilityAdjustedCorrelation.mq5 |
//+------------------------------------------------------------------+
#property strict

input int LookbackBars = 100;
input int AtrPeriod = 14;
input ENUM_TIMEFRAMES MatrixTimeframe = PERIOD_H4;

// Universe of symbols to track
string Symbols[] = {"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD", "XAUUSD"};

// Calculates the Pearson correlation coefficient between two arrays
double PearsonCorrelation(const double &x[], const double &y[])
  {
   int n = ArraySize(x);
   if(n == 0 || n != ArraySize(y)) return 0.0;
   
   double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0, sumY2 = 0;
   
   for(int i = 0; i < n; i++)
     {
      sumX += x[i];
      sumY += y[i];
      sumXY += x[i] * y[i];
      sumX2 += x[i] * x[i];
      sumY2 += y[i] * y[i];
     }
     
   double numerator = (n * sumXY) - (sumX * sumY);
   double denominator = MathSqrt(((n * sumX2) - (sumX * sumX)) * ((n * sumY2) - (sumY * sumY)));
   
   if(denominator == 0) return 0.0;
   return numerator / denominator;
  }

void OnStart()
  {
   int symCount = ArraySize(Symbols);
   
   // 2D Array to store ATR-normalized returns for each symbol
   double adjReturns[][];
   ArrayResize(adjReturns, symCount);
   
   for(int i = 0; i < symCount; i++)
     {
      double closes[], atrs[];
      
      // Request 1 extra bar of close data to calculate the initial return
      if(CopyClose(Symbols[i], MatrixTimeframe, 0, LookbackBars + 1, closes) < LookbackBars + 1)
        {
         Print("Insufficient close data for ", Symbols[i]);
         return;
        }
        
      int atrHandle = iATR(Symbols[i], MatrixTimeframe, AtrPeriod);
      if(atrHandle == INVALID_HANDLE || CopyBuffer(atrHandle, 0, 0, LookbackBars, atrs) < LookbackBars)
        {
         Print("Insufficient ATR data for ", Symbols[i]);
         return;
        }
        
      ArrayResize(adjReturns[i], LookbackBars);
      
      for(int j = 0; j < LookbackBars; j++)
        {
         // closes[j+1] is the current close, closes[j] is the previous bar's close
         double priceChange = closes[j+1] - closes[j]; 
         
         // Normalize the raw price change by the current bar's ATR
         if(atrs[j] > 0)
            adjReturns[i][j] = priceChange / atrs[j];
         else
            adjReturns[i][j] = 0.0;
        }
        
      IndicatorRelease(atrHandle); // Free memory 
     }

   // Generate and print the normalized matrix
   Print("--- Volatility-Adjusted Correlation Matrix ---");
   string headerRow = "Symbol";
   for(int i = 0; i < symCount; i++) headerRow += " | " + Symbols[i];
   Print(headerRow);

   for(int i = 0; i < symCount; i++)
     {
      string rowString = Symbols[i];
      for(int j = 0; j < symCount; j++)
        {
         if(i == j)
           {
            rowString += " |  1.00";
           }
         else
           {
            double r = PearsonCorrelation(adjReturns[i], adjReturns[j]);
            rowString += StringFormat(" | %5.2f", r);
           }
        }
      Print(rowString);
     }
  }
//+------------------------------------------------------------------+

Re: Capping risk per theme, not just per trade

Posted: Thu Oct 22, 2026 7:25 pm
by FTtrader
To filter signals dynamically, you transition the logic from calculating a static visual matrix to a targeted pre-trade check. Instead of computing the entire universe of pairs on every tick, the Expert Advisor only calculates the correlation between the proposed new symbol and the symbols of currently open positions.

If the absolute correlation ($\vert{}r\vert{}$) exceeds your defined threshold (typically 0.75 to 0.80), the new signal is flagged as redundant risk and rejected.