# 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.