PDV/README.md
ayantrader d301ee1690 Path-dependent volatility: engine, calibration and evidence
Guyon-Lekeufack path-dependent volatility ported to native MQL5.
sigma = b0 + b1*R1 + b2*sqrt(R2), where R1 and R2 are two-exponential
kernel sums over past signed and squared returns, implemented as four
running accumulators updated in O(1) per bar with no stored history.

CPdvModel calibrates nine parameters by splitting them: a Nelder-Mead
simplex over the six kernel parameters with the closed-form regression
for the three betas nested inside its objective. Features are causal,
the target is strictly forward, and the train/test cut is chronological.

PDV_Evidence scores the model out of sample against a constant, an EWMA
volatility and GARCH(1,1), and runs the b1-pinned-at-zero ablation that
isolates what the sign of the path is worth.
2026-09-07 14:12:22 +05:00

7.9 KiB

PDV

Path-dependent volatility in native MQL5: the Guyon-Lekeufack model written as four exponential accumulators, so the whole thing costs four multiplies and four adds 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 econometric patches, GJR and TARCH and EGARCH, add back one bit of that discarded information: the sign of yesterday's return, times a single fixed multiplier.

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, 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 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 then runs the ablation that matters: the identical machinery with b1 pinned at zero. Same data, same target, same split, same two-exponential kernel on squared returns, and the only difference is whether the sign of the path is allowed to count. Whatever R-squared the trend term adds over that is the entire measurable value of path dependence.

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, and that null is the useful part of the result rather than a failure: the leverage effect is an equity phenomenon, a currency pair has no issuer whose equity can be geared, and a b1 near zero there is the model reporting the truth. Gold's b1 comes out positive, which is a different market telling a different story about its own path.

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

A realised-volatility target caps the attainable R-squared by 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 while a forecaster that knew the true volatility scored 0.1114. The fit was not weak; the target was noisy. Against the true generating sigma the same fit correlated 0.976. Compute that ceiling before calling an R-squared disappointing.

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 script against another. Each rebuilds its own scored window, so a different warm-up starts the scored range at a different bar and the numbers legitimately differ: SPX500 comes out at 0.4480 under PDV_Calibrate at a 750-bar warm-up and 0.4464 inside the PDV_Evidence loop, and both are correct.

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 by construction. Test on your own data and broker conditions before drawing conclusions.