Building Volatility Models in MQL5 (Part II): Implementing GJR-GARCH and TARCH in MQL5
Contents
- Introduction
- Why and when is asymmetry relevant?
- The GJR-GARCH Model
- The TARCH Model
- MQL5 Implementation and Some Examples
- GJR-GARCH vs. TARCH
- Conclusion
Introduction
In the first part of this series, we implemented an MQL5 volatility library. We built conditional mean processes such as AR and HAR and paired them with standard ARCH/GARCH volatility frameworks. While effective for clustering, these traditional models are held back by their symmetric nature. They treat market rallies and market crashes as having an identical impact on future risk.
In this installment, we address this limitation by augmenting our library with support for asymmetric volatility processes, specifically the GJR-GARCH and TARCH models. In addition to defining these models, we explain why asymmetry is necessary from both mathematical and behavioral perspectives and how these frameworks improve on ARCH/GARCH. Finally, the article will demonstrate how to implement these asymmetric processes within full volatility models in MQL5 using real-world data. Readers will obtain ready-to-use code that captures the panic factor in financial markets. This provides a more robust toolset for risk management and directional analysis.
Why and when is asymmetry relevant?
To understand the leap into asymmetry, we must first appreciate the linear foundations laid by early autoregressive frameworks. Before the advent of ARCH and GARCH models, economists generally assumed that variance was constant over time, an assumption that did not correspond with real-world events. The revolution began in 1982 with Robert Engle’s ARCH (Autoregressive Conditional Heteroskedasticity) model. Engle’s breakthrough was recognizing that while we cannot easily predict the direction of tomorrow's return, we can predict the intensity of its fluctuation. He proposed that today's variance is a weighted function of past squared shocks.
However, ARCH models had a problem: they required a long memory (many lags) to capture the persistence of volatility. In 1986, Tim Bollerslev solved this with GARCH (Generalized ARCH). Bollerslev’s GARCH(1,1) was elegant because it made volatility recursive. It suggested that tomorrow’s variance depended on two things.
- The most recent return.
- The most recent volatility estimate itself.
This allowed a simple model with just three parameters to capture the volatility clusters seen in market data. Despite its elegance, the GARCH(1,1) model still had its limits. The mathematical reason lies in the way it processes market shocks. In the standard GARCH equation:

The predicted variance is based on the square of the previous period's data. Because any number, positive or negative, becomes positive when squared, the model strips the sign from the shock. That is to say, positive shock movements and negative shock movements in price are treated the same. GARCH assumes that the market reacts with the same level of agitation to a windfall as it does to a wipeout. This is the symmetry constraint, and in the context of equity markets, it is a significant oversimplification. While GARCH was being refined, researchers noticed a recurring pattern in empirical data that the model could not explain: the leverage effect.
First noted by Fischer Black in 1976, the leverage effect describes the observation that volatility tends to rise much more sharply following a price drop than it does following a price increase of the same magnitude. It is thought that there are two main contrasting reasons why this happens.
- When a company's stock price drops, its debt-to-equity ratio rises because debt stays relatively fixed while equity shrinks. The firm becomes more leveraged. A more leveraged firm is fundamentally riskier for investors. This increased risk translates directly into higher future volatility for the stock. In this framework, the leverage effect is not just a name; it is a literal description of corporate finance. As the equity cushion thins, the stock becomes more sensitive to every bit of new information. While the leverage hypothesis appears logically sound, it often fails to explain the sheer magnitude of volatility spikes, especially in broad market indices where the leverage of individual firms should diversify away. This is where behavioral finance and the volatility feedback effect come in.
- Behavioral finance suggests that investors are loss-averse. A price drop triggers panic, margin calls, and forced liquidations, all of which create chaotic, high-volatility environments. A price rise, by contrast, is usually met with rational profit-taking or quiet accumulation. The core of this theory is rooted in loss aversion, the psychological reality that the pain of a loss is roughly twice as potent as the joy of an equivalent gain. When a market begins to dip, investors do not just rationally rebalance. They experience fear. This fear triggers a cascade of selling, often exacerbated by automated stop-loss orders and margin calls. Also, as volatility increases, risk-averse investors demand a higher risk premium to hold the asset. To provide this higher expected return, the current price of the asset must drop even further. This creates a self-reinforcing cycle: falling prices lead to higher volatility, which leads to higher risk premiums, which leads to further falling prices.
The leverage effect shows that volatility is not just about the impact of the news but the nature of the news. This realization set the stage for the GJR and TARCH models, which were designed specifically to put the sign back into the volatility equation.
The GJR-GARCH Model
If the standard GARCH model is a basic thermostat that reacts to any change in temperature, the GJR-GARCH model is a sophisticated climate control system that recognizes the difference between extreme temperature conditions. The GJR-GARCH model was introduced by Lawrence Glosten, Ravi Jagannathan, and David Runkle in their seminal 1993 paper, "On the Relation between the Expected Value and the Volatility of the Nominal Excess Return on Stocks." Their goal was simple but ambitious: to fix the sign-blindness of the original GARCH and provide a more accurate tool for pricing financial derivatives and managing portfolio risk.
The GJR-GARCH(1,1) model extends the classic variance equation by adding an extra term specifically designed to capture asymmetric shocks. The mathematical structure is as follows.

- Omega is the long-term baseline variance.
- Alpha is the impact of the previous shock (the symmetric part).
- Beta is the persistence of volatility (the memory of the model).
- Gamma is the asymmetry parameter, the heart of the model.
- The indicator function acts as the logic gate that decides when to apply the extra panic weight.
The centerpiece of the GJR-GARCH model lies in the indicator function. Think of this as a dummy variable or a binary switch that monitors the sign of the previous day's return. It follows a simple rule: when the market goes up, the indicator is 0. The gamma term effectively disappears, and the model behaves like a standard GARCH. However, when the market crashes, the indicator switches to 1. Suddenly, the shock is amplified by both alpha and gamma. Therefore, gamma acts as a quantitative measure of market fragility. If gamma > 0, it confirms the leverage effect. It tells us that a negative shock is significantly more destabilizing to the market than a positive shock.
In risk management, a high gamma is a warning: during downturns, volatility can spike sharply, increasing the risk of margin calls and liquidity stress. For the option trader, gamma explains the volatility seen in option pricing. This helps explain why out-of-the-money put options are often more expensive than equivalent call options. By quantifying the extra volatility caused by bad news, GJR-GARCH provides a much more realistic mirror of human psychology in the face of financial loss.The TARCH Model
Now that we have looked at the GJR-GARCH model, we should address its closest sibling: the TARCH model. While GJR-GARCH models the conditional variance, Threshold ARCH (TARCH) models the conditional standard deviation. This subtle change makes the model even more sensitive to shocks. While the GJR-GARCH model was gaining traction in the United States, Jean-Michel Zakoian (1994) was developing a slightly different approach to asymmetry in France. His model shared the same goal as GJR-GARCH, which is capturing the leverage effect, but it arrived there through a different mathematical lens. The most significant departure of the TARCH model from the GARCH lineage is what it actually tries to predict. Most GARCH-type models (including GJR-GARCH) model the conditional variance. Zakoian argued that it is often more intuitive and statistically robust to model the conditional standard deviation.

In this specification, the model reacts to the absolute value of the shocks. When modeling variance, we are effectively squaring the shocks. This gives extreme outliers a significant amount of influence on the final model. By modeling the standard deviation, TARCH responds linearly to shock magnitude. This can make it more stable and less sensitive to isolated outliers while preserving sensitivity to the shock's sign. The "threshold" in TARCH refers to the point where the model's behavior changes. If the return is above the threshold, the model follows one path of volatility. If the return falls below the threshold, the model switches to a different regime where the impact of the shock is amplified.
This creates a piecewise linear news impact curve. Unlike the parabolic shape of standard GARCH, TARCH produces an impact curve that is lopsided, with a steeper slope for negative returns than for positive ones. A News Impact Curve (NIC) is a diagnostic tool in econometrics used to visualize how a specific model translates shocks or unexpected price movements into future volatility. By plotting the current shock on the x-axis against the predicted volatility for the next period on the y-axis, the curve reveals whether a model is symmetric, asymmetric, or robust to outliers. Below is an illustration of the NICs for the GARCH, GJR-GARCH, and TARCH models.

The standard GARCH impact curve is a perfectly symmetric parabola, a direct result of the model’s use of squared shocks. In contrast, the GJR-GARCH curve maintains a quadratic profile but exhibits a much steeper slope on the left side of the y-axis, highlighting its sensitivity to negative returns. The TARCH curve is uniquely characterized by two straight lines meeting at a central vertex of zero, forming a piecewise linear "V" shape. The graphic shown was generated by the Impact_Curves.ex5 script, which is provided as an attachment to this article.
MQL5 Implementation and Some Examples
The asymmetric volatility processes discussed are implemented as classes that inherit from the base GARCH class, CGarchProcess. Specifically, the CGjrGarchProcess class handles GJR-GARCH models, while TARCH models are managed by CTarchProcess.
//+------------------------------------------------------------------+ //| GJR-GARCH process | //+------------------------------------------------------------------+ class CGjrGarchProcess: public CGarchProcess { public: CGjrGarchProcess(void) { m_initialized = CGarchProcess::_initialize(VOL_GJR_GARCH,true,false,0,"GJR-GARCH"); } CGjrGarchProcess(ulong p, ulong o, ulong q, int seed = 0,ulong nboots=100) { m_initialized = CGarchProcess::_initialize(VOL_GJR_GARCH,true,false,0,"GJR-GARCH",seed,p,o,q,2.0,0,-1,nboots); } }; //+------------------------------------------------------------------+ //| TARCH process | //+------------------------------------------------------------------+ class CTarchProcess: public CGarchProcess { public: CTarchProcess(void) { m_initialized = CGarchProcess::_initialize(VOL_TARCH,true,false,0,"TARCH",0,1,1,1,1.0); } CTarchProcess(ulong p,ulong o,ulong q,int seed = 0,ulong nboots=100) { m_initialized = CGarchProcess::_initialize(VOL_TARCH,true,false,0,"TARCH",seed,p,o,q,1.0,0,-1,nboots); } };
To specify either model in practice, set the vol_model_type property of an ArchParameters instance to the corresponding ENUM_VOLATILITY_MODEL enumeration value.
ArchParameters tarch_spec; tarch_spec.vol_model_type = VOL_TARCH; ArchParameters gjr_spec; gjr_spec.vol_model_type = VOL_GJR_GARCH;
The GJR-GARCH model is primarily distinguished by its active "O" (asymmetry) parameter, which must be non-zero. Unless explicitly defined otherwise by the user, this parameter defaults to 1. In contrast, TARCH models are uniquely identified by their power parameter, which is strictly fixed at 1. The scripts GJR_Demo.ex5 and TARCH_Demo.ex5 demonstrate the configuration of GJR-GARCH and TARCH volatility processes, respectively. Notably, either process can be combined with any of the mean models currently implemented in the library. In the next section, we will apply the library to create a conditional volatility and standard deviation forecasting indicator.
//+------------------------------------------------------------------+ //| GJR_Demo.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #property script_show_inputs #include<VolatilityModels\Arch\Univariate\mean.mqh> //--- input parameters input string Symbol_="AUDUSD"; input ENUM_TIMEFRAMES TimeFrame=PERIOD_D1; input datetime StartDate=D'2025.01.01'; input ulong HistoryLen = 504; input double ScaleFactor=100.; input bool MeanConstant = true; input ulong _P_ = 1; input ulong _O_ = 1; input ulong _Q_ = 1; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { if(_O_<1) { Alert("Invalid setting: GJR-GARCH models need an asymmetry parameter of at least 1.\nOtherwise, it becomes a regular GARCH model."); return; } //---download data vector prices; if(!prices.CopyRates(Symbol_,TimeFrame,COPY_RATES_CLOSE,StartDate,HistoryLen)) { Print("Failed to get close prices for ", Symbol_,". Error ", GetLastError()); return; } //--- prices = log(prices); //--- vector returns = np::diff(prices); //-- specify the model parameters ArchParameters gjr_spec; gjr_spec.observations=ScaleFactor*returns; gjr_spec.include_constant = MeanConstant; gjr_spec.vol_model_type = VOL_GJR_GARCH; gjr_spec.garch_p = _P_; gjr_spec.garch_q = _Q_; gjr_spec.garch_o = _O_; //-- initialize the mean model ConstantMean gjr_model; //--- if(!gjr_model.initialize(gjr_spec)) return; //---get the model parameters vector empty; ArchModelResult gjr_params = gjr_model.fit(ScaleFactor); //--check the model is fitted if(!gjr_params.params.Size()) { Print("Convergence failed ", GetLastError()); return; } //--- Print("GJR-GARCH model parameters ", gjr_params.params); vector pv = gjr_params.pvalues(); Print("GJR-GARCH model pvalues: "); for(ulong i = 0; i<pv.Size(); ++i) Print("Pvalue for param at ", i, " :- ", pv[i]); //--- } //+------------------------------------------------------------------The indicator ConditionalVolatility_forecaster.ex5 applies the library to real-time forecasting. For each new bar, it calibrates the model on a sliding historical window (excluding the most recent bar) and produces a one-step-ahead forecast. Beyond the raw forecast, the indicator computes a volatility z-score.

This metric calculates how many standard deviations the current forecast sits from historical norms, effectively removing symbol-specific bias to better identify abnormal volatility spikes. Users can define a specific threshold for these deviations using the Num_Stds input parameter.
The indicator’s configuration is managed through adjustable input parameters.
- HistoryLen determines the size of the data sample used for calibration.
- BarsToDraw sets the total number of historical bars the indicator will calculate.
- Num_Stds defines the threshold above which volatility is considered relatively high.
//+------------------------------------------------------------------+ //| ConditionalVolatility_forecaster.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #include<VolatilityModels\Arch\Univariate\mean.mqh> #property indicator_separate_window #property indicator_buffers 2 #property indicator_plots 2 //--- plot Forecast #property indicator_label1 "Forecast" #property indicator_type1 DRAW_LINE #property indicator_color1 clrRed #property indicator_style1 STYLE_DOT #property indicator_width1 1 //--- plot Mean #property indicator_label2 "Vol_Zscore" #property indicator_type2 DRAW_LINE #property indicator_color2 clrLimeGreen #property indicator_style2 STYLE_SOLID #property indicator_width2 1 //--- input parameters input int BarsToDraw = 50; input int HistoryLen = 200; input double Num_Stds = 1.5; input double ScaleFactor=100.; input ENUM_MEAN_MODEL MeanModel = MEAN_CONSTANT; input bool MeanConstant = false; input string MeanLags =""; input ENUM_VOLATILITY_MODEL VolatilityModel = VOL_TARCH; input ulong _P_ = 1; input ulong _O_ = 1; input ulong _Q_ = 1; input int Volatility_Seed = 0; input ENUM_DISTRIBUTION_MODEL ErrorDistribution = DIST_NORMAL; input int Distribution_Seed = 0; //--- indicator buffers double ForecastBuffer[]; double ThresholdBuffer[]; double VolZscoreBuffer[]; vector returns = vector::Zeros(HistoryLen); ArchParameters spec; HARX* full_model; //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { if(HistoryLen<30) { Print("Invalid input value for HistoryLen"); return INIT_FAILED; } //--- indicator buffers mapping SetIndexBuffer(0,ForecastBuffer,INDICATOR_DATA); SetIndexBuffer(1,VolZscoreBuffer,INDICATOR_DATA); //--- PlotIndexSetInteger(0,PLOT_DRAW_BEGIN,BarsToDraw); PlotIndexSetInteger(1,PLOT_DRAW_BEGIN,BarsToDraw); //--- IndicatorSetInteger(INDICATOR_LEVELS,2); IndicatorSetDouble(INDICATOR_LEVELVALUE,0,Num_Stds); IndicatorSetDouble(INDICATOR_LEVELVALUE,1,-1.0); //--- spec.mean_model_type = MeanModel; if(StringLen(MeanLags)) { string lag_info[]; int nlags = StringSplit(MeanLags,StringGetCharacter(",",0),lag_info); if(nlags>0) { for(uint i = 0; i<uint(nlags); ++i) { if(StringLen(lag_info[i])>0) { if(spec.mean_lags.Resize(spec.mean_lags.Size()+1,3)) spec.mean_lags[spec.mean_lags.Size()-1] = StringToDouble(lag_info[i]); else { Print("Error ", GetLastError()); return INIT_FAILED; } } } } } spec.vol_rng_seed = Volatility_Seed; spec.garch_o = _O_; spec.garch_p = _P_; spec.garch_q = _Q_; spec.dist_type = ErrorDistribution; spec.dist_rng_seed = Distribution_Seed; switch(MeanModel) { case MEAN_CONSTANT: full_model = new ConstantMean(); break; case MEAN_ZERO: full_model = new ZeroMean(); break; case MEAN_AR: full_model = new AR(); break; default: full_model = new ConstantMean(); break; } if(CheckPointer(full_model)==POINTER_INVALID) return INIT_FAILED; switch(VolatilityModel) { case VOL_GJR_GARCH: spec.vol_model_type = VOL_GJR_GARCH; break; case VOL_TARCH: spec.vol_model_type = VOL_TARCH; break; default: spec.vol_model_type = VOL_GARCH; break; } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ //| Custom indicator deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { if(CheckPointer(full_model)==POINTER_DYNAMIC) delete full_model; } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int32_t rates_total, const int32_t 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 int32_t &spread[]) { //--- int32_t limit; static datetime last_time; if(time[rates_total-1]==last_time) return rates_total; else last_time = time[rates_total-1]; if(prev_calculated<=0) limit = rates_total - int32_t(fabs(BarsToDraw)); else limit = prev_calculated - 1; for(int32_t shift = limit; shift<rates_total; ++shift) { int from = shift - fabs(HistoryLen); for(int32_t i = from; i<(shift); ++i) returns[i-(from)] = log(close[i]/close[i-1]); returns*=fabs(ScaleFactor); spec.observations = returns; if(!full_model.initialize(spec)) { Print("Initialization error "); return 0; } ArchModelResult result = full_model.fit(ScaleFactor); ulong size = result.conditional_volatility.Size(); if(!size) { Print("Model fit error "); return 0; } ArchForecast forecast = full_model.forecast(); ForecastBuffer[shift] = (spec.vol_model_type!=VOL_TARCH)?sqrt(forecast.variance[0,0]):forecast.variance[0,0]; VolZscoreBuffer[shift] = (ForecastBuffer[shift] - result.conditional_volatility.Mean())/result.conditional_volatility.Std(); } //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
The chart shows two plots: a red line for the one-step-ahead volatility forecast and a lime-green line for the volatility z-score. Additionally, the chart displays the Num_Stds threshold and a default baseline at -1. Trading signals are generated by observing the z-score's position relative to these levels; for instance, if Num_Stds is set to 2.0 and the z-score exceeds this value, it indicates an abnormal volatility spike. The image below illustrates the indicator as it appears when applied to a chart in MetaTrader 5.

GJR-GARCH vs. TARCH
The next question is when to prefer one model over the other. Choosing between GJR-GARCH and TARCH often comes down to a trade-off between consistency and robustness. While both are asymmetric, they react differently to market extremes. The choice depends on the data's statistical properties, the asset class, and the modeling objective.
One of the most important statistical properties to consider is whether the data contains extreme values. The difference between squaring a shock and taking its absolute value is the deciding factor here. The TARCH model should be used if the dataset contains extreme, one-off spikes and the goal is to minimize the effects of these extremes. Because TARCH is linear, it treats an extreme drop as exactly twice as significant as an average drop. If, on the other hand, the purpose is to build a model that is sensitive to extreme price movements, then use the GJR-GARCH. The GJR-GARCH is the aggressive choice for modeling tail risk.
The underlying asset class matters because asymmetry is not a universal law of finance; it is a characteristic of corporate structure. The Financial Leverage Hypothesis (debt-to-equity ratios) is a variance-based phenomenon. GJR-GARCH is better suited to equity-related symbols, where the panic factor is consistently high and statistically significant. Therefore, it makes more sense to use the GJR-GARCH model for equities. Foreign exchange markets often show less leverage asymmetry because a crash in one currency of a pair is a rally in its counterpart. TARCH’s absolute-value approach often fits these more balanced markets better than the aggressive squaring found in GJR-GARCH.
This phenomenon can be practically demonstrated by comparing the asymmetric volatility profiles of different asset classes. This analysis is implemented in the script Equity_vs_Forex_Asymmetry_Significance_Test.ex5, which constructs volatility models for both forex and equity data. To ensure reproducibility and a consistent dataset, the equity data was sourced using Python’s yfinance module.
//+------------------------------------------------------------------+ //| Equity_vs_Forex_Asymmetry_Significance_Test.mq5 | //| Copyright 2025, MetaQuotes Ltd. | //| https://www.mql5.com | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com" #property version "1.00" #resource "\\Files\\VolatilityModels\\sp500.csv" as string equity_data #property script_show_inputs #include<VolatilityModels\Arch\Univariate\mean.mqh> //--- input parameters input string Symbol_="AUDUSD"; input ENUM_TIMEFRAMES TimeFrame=PERIOD_D1; input datetime StartDate=D'2025.01.01'; input ENUM_VOLATILITY_MODEL VolatilityModel = VOL_GJR_GARCH; input double ScaleFactor=100.; input ENUM_MEAN_MODEL MeanModel = MEAN_CONSTANT; input bool MeanConstant = true; input ulong _P_ = 1; input ulong _O_ = 1; input ulong _Q_ = 1; //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { if(VolatilityModel != VOL_GJR_GARCH && VolatilityModel!=VOL_TARCH) { Print("INVALID INPUT: choose either TARCH or GJR-GARCH"); return; } //--- matrix data = np::readcsv_from_string(equity_data,false,",",true,0); vector equity = data.Col(1); vector forex; if(!forex.CopyRates(Symbol_,TimeFrame,COPY_RATES_CLOSE,datetime(data[0,0]),datetime(data[data.Rows()-1,0]))) { Print("Failed to get close prices for ", Symbol_,". Error ", GetLastError()); return; } forex = log(forex); equity = log(equity); forex = np::diff(forex); equity = np::diff(equity); forex*=ScaleFactor; equity*=ScaleFactor; ArchParameters asym_vol_spec; asym_vol_spec.include_constant = MeanConstant; asym_vol_spec.mean_model_type = MeanModel; asym_vol_spec.vol_model_type = VolatilityModel; asym_vol_spec.garch_p = _P_; asym_vol_spec.garch_q = _Q_; asym_vol_spec.garch_o = _O_; asym_vol_spec.observations=equity; HARX* equity_model = NULL; HARX* forex_model = NULL; switch(MeanModel) { case MEAN_CONSTANT: equity_model = new ConstantMean(); forex_model = new ConstantMean(); break; case MEAN_ZERO: equity_model = new ZeroMean(); forex_model = new ZeroMean(); break; default: equity_model = new ConstantMean(); forex_model = new ConstantMean(); break; } if(!equity_model.initialize(asym_vol_spec)) { delete equity_model; return; } ArchModelResult equity_result = equity_model.fit(ScaleFactor); Print("Results for ", EnumToString(VolatilityModel)); if(!equity_result.params.Size()) { Print("Equity model fit failed "); delete equity_model; } else { Print("Equity: Pvalue of gamma parameter is ", equity_result.pvalues()[3]); delete equity_model; } asym_vol_spec.observations = forex; if(!forex_model.initialize(asym_vol_spec)) { delete forex_model; return; } ArchModelResult forex_result = forex_model.fit(ScaleFactor); if(!forex_result.params.Size()) { Print("Forex model fit failed "); delete forex_model; } else { Print("Forex: pvalue of gamma parameter is ", forex_result.pvalues()[3]); delete forex_model; } } //+------------------------------------------------------------------
By leveraging the library’s analytical capabilities, we can inspect the statistical significance of the model’s asymmetry parameter (γ). This information is accessed via the pvalues() property of the ArchModelResult struct. This property can be helpful in determining model parsimony—specifically, whether the added complexity of an asymmetric model is statistically justified for a given dataset. The results below are derived from running the script twice: first for GJR-GARCH, then for TARCH.

The disparity in p-values is stark. In the GJR-GARCH results, the equity model yields a statistically significant p-value for the gamma parameter, whereas the Forex model produces a high p-value, suggesting a lack of asymmetric effects. For the TARCH models, while both p-values appear significant, the equity model again maintains a substantially lower value. These empirical results reinforce the hypothesis that the "leverage effect" is a dominant characteristic of equity markets that is often absent or muted in currency markets.
The choice of volatility model should match the modeling objective. TARCH often produces more stable forecasts (lower MAE), while GJR-GARCH is better suited to tail-risk tasks such as derivatives pricing and Value-at-Risk (VaR). When uncertain, compare AIC/BIC across both models. It is feasible to fit both models and let the statistics decide. If the TARCH model yields a lower IC, it means the added complexity of the GJR-GARCH squared terms is not helping explain the data; it may just be adding noise.
Conclusion
This installment has demonstrated that volatility is not merely a measure of the magnitude of market movement, but it can also be sensitive to its direction. By moving beyond the symmetric constraints of standard ARCH and GARCH models, we have equipped our MQL5 library with the ability to account for the leverage effect—the empirical reality that bad news impacts market psychology more aggressively than good news. We explored the two primary paths to capturing this asymmetry.
- The GJR-GARCH model. A variance-based approach that uses a quadratic penalty to model the exponential nature of market panic, making it the premier choice for equity risk and tail-event analysis.
- The TARCH model. A robust, standard-deviation-based alternative that utilizes absolute values to provide stable forecasts, proving particularly effective for foreign exchange markets and datasets prone to extreme outliers.
Through the implementation of the CGjrGarchProcess and CTarchProcess classes, MQL5 developers now have a native framework for sophisticated risk modeling. By utilizing the provided ConditionalVolatility_forecaster.ex5 indicator and z-score logic, traders can strip away symbol-specific bias and identify true volatility regimes in real-time. While the choice between GJR-GARCH and TARCH depends on the specific asset class and the trader's goal—whether it be the robust forecasting of TARCH or the aggressive risk assessment of GJR-GARCH—the inclusion of the asymmetry parameter (γ) ensures that the "sign" of market shocks is no longer ignored.
Integrating asymmetric volatility models enhances the library's utility. It also moves the project closer to a native MQL5 toolset comparable to Python's arch module. Some challenges remain: parameter estimates diverge between the two platforms, likely due to differences in the optimizers.
This discrepancy will be the focal point of the next installment in this series. The source code for all programs referenced in the text is attached below.
| File or Folder | Description |
|---|---|
| MQL5/Include/VolatilityModels/np.mqh: | Header file of various vector and matrix utility functions. |
| MQL5/Include/VolatilityModels/Arch: | Folder of header files for the conditional volatility modeling library. |
| MQL5/Files/VolatilityModels/sp500.csv: | This file contains S&P 500 OHLC data used in the scripts. |
| MQL5/Scripts/VolatilityModels/Impact_Curves.mq5: | This script generates the News Impact Curves graphic mentioned in the article. |
| MQL5/Scripts/VolatilityModels/GJR_Demo.mq5: | This script demonstrates fitting a GJR-GARCH model to a dataset. |
| MQL5/Scripts/VolatilityModels/TARCH_Demo.mq5: | This script demonstrates fitting a TARCH model to a dataset. |
| MQL5/Scripts/VolatilityModels/Equity_vs_Forex_Asymmetry_Significance_Test.mq5: | The script demonstrates the difference in asymmetric volatility inherent in forex and equity data sets. |
| MQL5/Indicators/VolatilityModels/ConditionalVolatility_forecaster.mq5: | This indicator demonstrates real-time volatility forecasting. |
Warning: All rights to these materials are reserved by MetaQuotes Ltd. Copying or reprinting of these materials in whole or in part is prohibited.
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.
Price Action Analysis Toolkit Development (Part 67): Automating Support and Resistance Monitoring in MQL5
MQL5 Trading Tools (Part 29): Step-by-Step Butterfly Animation on Canvas
Developing a Multi-Currency Advisor (Part 27): Component for Displaying Multi-Line Text
How to connect AI agents to MetaTrader 5 via MCP
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use