Problem Description
Solution
To programmatically change the horizontal levels of a custom indicator in MQL4, you must use the IndicatorSetDouble function with the INDICATOR_LEVELVALUE property identifier.
It is important to note that Indicator Levels (the horizontal lines drawn across the subwindow) are global for the indicator instance. If you change a level based on the current volatility, the line will move up or down for the entire chart history, not just the current bar.
Below is a complete, compilable example of a Dynamic RSI indicator. It calculates the Standard Deviation of the RSI to measure volatility and adjusts the overbought/oversold levels in real-time.
MQL4 Code: Dynamic Levels Indicator
//+------------------------------------------------------------------+
//| DynamicVolatilityRSI.mq4 |
//| Copyright 2023, MetaQuotes |
//| https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2023, Quantitative AI"
#property link "https://www.mql5.com"
#property version "1.00"
#property strict
//--- Indicator Settings
#property indicator_separate_window
#property indicator_buffers 1
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
//--- Define initial levels (will be overwritten programmatically)
#property indicator_level1 30.0
#property indicator_level2 70.0
#property indicator_levelcolor clrGray
#property indicator_levelstyle STYLE_DOT
//--- Input Parameters
input int InpRsiPeriod = 14; // RSI Period
input int InpVolPeriod = 20; // Volatility Calculation Period
input double InpDeviations = 2.0; // Standard Deviations for Levels
input ENUM_APPLIED_PRICE InpPrice = PRICE_CLOSE; // Applied Price
//--- Indicator Buffers
double RsiBuffer[];
//+------------------------------------------------------------------+
//| Custom indicator initialization function |
//+------------------------------------------------------------------+
int OnInit()
{
//--- Indicator mapping
SetIndexBuffer(0, RsiBuffer);
SetIndexStyle(0, DRAW_LINE);
//--- Set name and digits
string short_name = "DynRSI(" + IntegerToString(InpRsiPeriod) + ")";
IndicatorShortName(short_name);
IndicatorDigits(2);
//--- Set the number of levels explicitly (0 and 1)
IndicatorSetInteger(INDICATOR_LEVELS, 2);
//--- Set descriptions for levels
IndicatorSetString(INDICATOR_LEVELTEXT, 0, "Oversold (Dyn)");
IndicatorSetString(INDICATOR_LEVELTEXT, 1, "Overbought (Dyn)");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Custom indicator iteration function |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
//--- Check for sufficient data
if(rates_total < InpRsiPeriod + InpVolPeriod)
return(0);
//--- Calculation loop
int limit = rates_total - prev_calculated;
if(prev_calculated > 0) limit++;
for(int i = 0; i < limit; i++)
{
// 1. Calculate RSI
RsiBuffer[i] = iRSI(NULL, 0, InpRsiPeriod, InpPrice, i);
}
//--- DYNAMIC LEVEL UPDATE LOGIC ---------------------------------+
// We calculate the volatility of the RSI itself (Standard Deviation)
// based on the most recent closed bar (index 0 or 1 depending on strategy).
// Here we use index 0 for real-time updates.
double sum = 0.0;
double sum_sq = 0.0;
// Calculate Mean and Standard Deviation of RSI over VolPeriod
for(int k = 0; k < InpVolPeriod; k++)
{
double val = iRSI(NULL, 0, InpRsiPeriod, InpPrice, k);
sum += val;
sum_sq += val * val;
}
double mean = sum / InpVolPeriod;
double variance = (sum_sq / InpVolPeriod) - (mean * mean);
double std_dev = MathSqrt(variance);
// Calculate new dynamic levels centered around 50 (RSI Midpoint)
// or centered around the moving average of RSI (mean).
// Here we center around 50 for classic RSI behavior.
double new_upper_level = 50.0 + (std_dev * InpDeviations);
double new_lower_level = 50.0 - (std_dev * InpDeviations);
// Clamp values to keep them within RSI bounds (0-100)
if(new_upper_level > 99) new_upper_level = 99;
if(new_lower_level < 1) new_lower_level = 1;
//--- UPDATE THE LEVELS PROGRAMMATICALLY -------------------------+
// Level index 0 is the first level defined (originally 30)
// Level index 1 is the second level defined (originally 70)
IndicatorSetDouble(INDICATOR_LEVELVALUE, 0, new_lower_level);
IndicatorSetDouble(INDICATOR_LEVELVALUE, 1, new_upper_level);
// Optional: Update text to show current value
IndicatorSetString(INDICATOR_LEVELTEXT, 0, "Low: " + DoubleToString(new_lower_level, 1));
IndicatorSetString(INDICATOR_LEVELTEXT, 1, "High: " + DoubleToString(new_upper_level, 1));
//----------------------------------------------------------------+
return(rates_total);
}
//+------------------------------------------------------------------+
Key Functions Explained
-
IndicatorSetInteger(INDICATOR_LEVELS, 2):
This ensures the indicator knows exactly how many horizontal lines to manage. This is usually done inOnInit. -
IndicatorSetDouble(INDICATOR_LEVELVALUE, index, value):
This is the core function used insideOnCalculate.INDICATOR_LEVELVALUE: The property identifier telling MQL4 you want to change the value of a level.index: The zero-based index of the level. If you defined#property indicator_level1, its index is 0.#property indicator_level2is index 1.value: The new double value (e.g., the calculated volatility-adjusted price).
-
Volatility Logic:
In the example above, I calculated the Standard Deviation of the RSI array itself over the lastInpVolPeriodbars.- If the market is volatile, the Standard Deviation increases, pushing the levels outward (e.g., to 80/20).
- If the market is flat, the Standard Deviation decreases, pulling the levels inward (e.g., to 60/40).
Important Limitation
As mentioned, IndicatorSetDouble moves the horizontal line for the entire chart. If you look back at history, the lines will be at the position calculated for the current moment, not where they were in the past.
If you need to see the historical progression of dynamic levels (to backtest visually), you should not use "Levels". Instead, you should use Indicator Buffers (DRAW_LINE) to draw the upper and lower bounds.
Q&A
Q: Can I add a new level dynamically if it wasn't defined in properties?
A: Yes. You can use IndicatorSetInteger(INDICATOR_LEVELS, n) in OnInit to define n levels, even if you didn't use #property indicator_levelN. However, you must then set the style, color, and initial values for all of them programmatically.
Q: Why do the levels jump around on the chart?
A: Because the calculation is happening on every tick (or every bar). If the volatility changes slightly, the function updates the global level position immediately. To prevent jitter, you might want to round the values or only update on a new bar event.
Q: How do I access these dynamic levels from an Expert Advisor?
A: You cannot access the "Levels" of a custom indicator directly via iCustom. An EA using iCustom only reads the Indicator Buffers (the data lines). If you need the EA to trade based on these dynamic levels, you must either:
- Replicate the volatility math inside the EA.
- Convert the Levels into Indicator Buffers (using
SetIndexBuffer) soiCustomcan read them asmode1 and 2.