Path-dependent volatility in native MQL5: the Guyon-Lekeufack model as four exponential accumulators updated in O(1) per bar, splitting volatility into a trend component and an activity component. Includes the out-of-sample evidence against GARCH(1,1) and the ablation that prices what the sign of the path is worth.
Найти файл
Файлы репозитория (в начале последний коммит)
Имя файла Текст последнего коммита Дата последнего коммита
ayantrader ayantrader 5d1420e33b Purge the train/test boundary and align with the revised article
The fit now stops H bars before the split, so no training target reaches
into the test period, and every baseline in PDV_Evidence uses the same
purged rows. The determinant test in SolveBetas is relative, Sigma() and
Forecast() share one floor scaled by |b2|, and the comment boxes on the
target and ridge are corrected. README updated to match.
2026-10-18 06:09:02 +05:00
Include/PDV Purge the train/test boundary and align with the revised article 2026-10-18 06:09:02 +05:00
Indicators/PDV Path-dependent volatility: engine, calibration and evidence 2026-09-07 14:12:22 +05:00
Scripts/PDV Purge the train/test boundary and align with the revised article 2026-10-18 06:09:02 +05:00
README.md Purge the train/test boundary and align with the revised article 2026-10-18 06:09:02 +05:00

PDV

Path-dependent volatility in native MQL5: the Guyon-Lekeufack model written as four exponential accumulators, so the whole thing costs eight multiplications and four additions per bar and stores no history at all.

Companion code for the MQL5 article: https://www.mql5.com/en/articles/24607

What it does

Square a return and you throw away its sign. Every GARCH-family model does this in its first step, which is why a market that has fallen five percent and a market that has risen five percent look identical to it. The asymmetric extensions, GJR and TARCH and EGARCH, let the sign back in, and through the recursion it even accumulates, but only inside the variance's own memory: the direction of the path never gets a memory of its own.

Guyon and Lekeufack keep the whole path instead. Volatility becomes

sigma = b0 + b1 * R1 + b2 * sqrt(R2)

where R1 is a kernel-weighted sum of past signed returns and R2 the same construction on squared returns. R1 carries where the path has been going, R2 how hard it has been moving, and the claim is that those two numbers are most of what there is to know about today's volatility.

The reason this belongs in an indicator rather than a research report is the kernel. Both kernels are convex mixtures of two exponentials, and an exponential kernel has a recursive update, so a sum over unbounded history collapses to one running scalar. Two exponentials per feature and two features make four scalars. CPdvState is that arithmetic and nothing else: no lookback array, no matrix in the per-bar update, nothing that grows with the length of the history, and no knowledge of bars, so the same object serves a calibration sweep over ten years and an indicator updating on every tick.

Calibration splits nine parameters by how they are found rather than solving all nine the same way. Hold the kernels fixed and the model is a plain linear regression of realised volatility on two features, so the three betas come out of a closed form with no search. Only the six kernel parameters need an optimiser, and they get a Nelder-Mead simplex with the regression nested inside its objective. Six dimensions searched over three solved exactly is what makes this calibrate in seconds rather than minutes.

Two choices in CPdvModel are about honesty rather than accuracy. The features at bar i use returns up to and including bar i while the target is measured from bar i+1 onward, and the train/test cut is chronological, because a shuffled split on data this autocorrelated will manufacture an R-squared out of nothing. The fit also stops H bars short of the split, so no training target reaches into the test period. The evaluation window is also fixed before the search starts rather than derived per candidate: a long-memoried kernel needs more warm-up than a short one, so letting the window follow the candidate would score different models on different samples and quietly reward whichever got the easiest bars.

PDV_Evidence.mq5 is where the method has to justify itself. It scores the model out of sample against a constant, an EWMA volatility and GARCH(1,1), every baseline given the same intercept and slope the model gets and fitted on the same purged training rows, and then runs the ablation that matters: the same machinery with b1 pinned at zero. Same data, same target, same split, same two-exponential kernel on squared returns; each model is fitted at its own optimum, so the R-squared the trend term adds is the value of adding that term, including the readjustment of the activity side it permits.

On H1 equity indices it is worth a lot. The trend term adds +0.171 out-of-sample R-squared on SPX500 and +0.15 and +0.11 on US30 and NDX100. On FX it adds roughly nothing. That fits the usual accounts of the leverage effect, financial leverage and volatility feedback, which are both about equity, but it is not proof that FX volatility has no path dependence: GBPUSD's in-sample b1 is as large as GER30's, it just does not carry out of sample. Gold's b1 comes out positive, but its gain is small and moves between refits, so treat it as a hint rather than a finding. None of these differences carries a confidence interval; the large index gains are big against the sample, the FX ones are not distinguishable from noise.

Two things are worth knowing before reading any of those numbers.

A realised-volatility target keeps R-squared well below 1 through its own sampling noise, because the target is an estimate and not the thing itself. On synthetic data with a known generating process the fit scored 0.1156 against a close-to-close target, and a forecaster handed the true current volatility scored 0.1114. That is a benchmark rather than a strict ceiling, but it shows the fit was not weak; the target was noisy. Against the true generating sigma the same fit correlated 0.976. Read any R-squared against the baselines, not against 1.0.

The model is linear in R1 and b1 is negative wherever leverage exists, so a long enough rally can drive fitted volatility through zero. That is a property of the specification, which the paper is explicit about, not a bug in the port. Both values are exposed: Sigma() is floored at a share of the activity term and is what a stop distance should be built on, while SigmaRaw() is what tells you the model has been pushed outside the range it was fitted in. PDV_Calibrate.mq5 reports the count of negative raw bars for exactly this reason. A handful is the model working as specified; thousands means the fit is not usable.

The decomposition, not the R-squared, is the deliverable. Splitting volatility into a direction component and an activity component answers a question a single conditional-variance number cannot express, and it costs nothing extra to compute: a market falling steadily and a market thrashing sideways at the same volatility become distinguishable, before the fact, from the state alone.

Layout

Include/PDV/PdvTypes.mqh            structs, buffer map, half-life and decay conversions
Include/PDV/PdvState.mqh            CPdvState: the four accumulators, O(1) per bar
Include/PDV/PdvModel.mqh            CPdvModel: forward target, closed-form betas, Nelder-Mead, ablation
Include/PDV/PdvForecast.mqh         CPdvForecast: decomposition, floor, trend share, multi-step projection
Indicators/PDV/PDV_Decomposition.mq5  total volatility against its trend and activity components
Scripts/PDV/PDV_Calibrate.mq5       fits one symbol, reports parameters, split scores, decomposition summary
Scripts/PDV/PDV_Evidence.mq5        out-of-sample comparison against constant, EWMA and GARCH(1,1)

Run PDV_Calibrate.mq5 first on the symbol you care about. Its last line prints the nine fitted parameters in input order, which paste straight into PDV_Decomposition.mq5; the defaults shipped in the indicator are the SPX500 H1 fit. Then run PDV_Evidence.mq5 for the comparison table and the ablation. Pick the instrument deliberately, because it is load-bearing: an FX-only run will show a gain near zero and no story at all. Broker symbol names vary, and the indices are not always called what you expect.

Do not reconcile a figure from one run against another. Both scripts load the latest history the terminal holds, so a run on a different day scores a different window: the article's SPX500 figures are 0.4480 from PDV_Calibrate on history ending 7 September 2026 and 0.4464 from PDV_Evidence on history ending a month earlier, and both are correct for their windows.

Substituting your own target is a single edit. BuildTarget() in PdvModel.mqh is the only place that decides what the model is being asked to predict, and nothing downstream knows where the target came from. The two supplied forms are close-to-close and Parkinson, which are on the same scale and interchangeable; Parkinson reads the whole bar range and is several times more efficient, at the cost of ignoring gaps.

Disclaimer

Educational code. This is a volatility measurement and forecasting library, not a trading system: there is no Expert Advisor here and nothing in this repository demonstrates a trading edge. A better volatility forecast is an input to position sizing and stop placement, which is a different thing from money. The out-of-sample gains reported above are R-squared against a realised volatility target on H1 bars from one broker's history, and they are strongly instrument-dependent, close to zero on FX. Test on your own data and broker conditions before drawing conclusions.