<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[VertoxQuant]]></title><description><![CDATA[Applied quantitative research on trading, risk, and systematic strategy design.]]></description><link>https://www.vertoxquant.com</link><image><url>https://substackcdn.com/image/fetch/$s_!ufaQ!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png</url><title>VertoxQuant</title><link>https://www.vertoxquant.com</link></image><generator>Substack</generator><lastBuildDate>Sun, 13 Sep 2026 10:22:54 GMT</lastBuildDate><atom:link href="https://www.vertoxquant.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Vertox]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[vertox@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[vertox@substack.com]]></itunes:email><itunes:name><![CDATA[Vertox]]></itunes:name></itunes:owner><itunes:author><![CDATA[Vertox]]></itunes:author><googleplay:owner><![CDATA[vertox@substack.com]]></googleplay:owner><googleplay:email><![CDATA[vertox@substack.com]]></googleplay:email><googleplay:author><![CDATA[Vertox]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Volatility is Rough. But Why?]]></title><description><![CDATA[Order flow, Hawkes processes, and the origin of H=0.1]]></description><link>https://www.vertoxquant.com/p/volatility-is-rough-but-why</link><guid isPermaLink="false">https://www.vertoxquant.com/p/volatility-is-rough-but-why</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Fri, 11 Sep 2026 23:58:05 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!iD4D!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F7f3fc265-d0d7-4e4b-821b-c15a7cfca64f_690x490.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the early days of quantitative finance, a main assumption of volatility models was that volatility is driven by Brownian motion. Since 2014, thanks to Jim Gatheral, Thibault Jaisson, and Mathieu Rosenbaum, the idea of rough volatility has become commonplace. The main idea is that volatility is far more jittery, or rough, than regular Brownian motion. </p><p>This matters quite a lot, as the previous models didn&#8217;t fit empirically observed patterns well, like sharply steepening ATM skew for short-dated options. But as you know from my articles, just having a model fit well and calling it a day is not enough for us. We want to know WHY a certain model works well or doesn&#8217;t from an economic perspective. This reasoning is what allows us to develop truly robust models. </p><p>So in this article we will:</p><ul><li><p>measure how much BTC volatility actually moves across different timescales and let the data tell us what kind of process it is,</p></li><li><p>ask what that process does to an option book, derive the short-dated skew it implies, and check the prediction against Deribit,</p></li><li><p>ask where the process comes from in the first place, build the mechanism up from three facts about how markets trade, and estimate it from Binance data.</p></li></ul><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practised:</p><p>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p><strong>EigenScore</strong> is the first rated contest platform for quants: timed rounds, Elo ratings, and problems across probability, pricing, optimization, and forecasting.</p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://eigenscore.com&quot;,&quot;text&quot;:&quot;Solve your first problem&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://eigenscore.com"><span>Solve your first problem</span></a></p>
      <p>
          <a href="https://www.vertoxquant.com/p/volatility-is-rough-but-why">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[A few hours until EigenScore Round 1]]></title><description><![CDATA[The first contest]]></description><link>https://www.vertoxquant.com/p/a-few-hours-until-eigenscore-round</link><guid isPermaLink="false">https://www.vertoxquant.com/p/a-few-hours-until-eigenscore-round</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Tue, 08 Sep 2026 10:25:19 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dNYv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have launched EigenScore last week; The first competitive problem solving platform for quantitative finance.</p><p>You can use it to practice your quant skills, prep for interviews, or prove your skills against other quants in the rated contests.</p><p>The first such rated contest is today in a few hours. Join before it&#8217;s too late:</p><p><a href="https://eigenscore.com/contests/round-1">https://eigenscore.com/contests/round-1</a></p><p></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dNYv!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dNYv!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dNYv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg" width="1080" height="580" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:&quot;normal&quot;,&quot;height&quot;:580,&quot;width&quot;:1080,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127652,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dNYv!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 424w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 848w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!dNYv!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb4e35217-a479-43c6-a2b7-5b2e96922cf8_1080x580.jpeg 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The problems will range from easy to hard with 3 types of problems:</p><ul><li><p>Standard: Ground-truth problems. You either solve them or not.</p></li><li><p>Prediction: Better out-of-sample forecast performance gets more points.</p></li><li><p>Optimization: Better final minimum/maximum out-of-sample gets more points.</p></li></ul><p>Problems come from all kinds of areas: probability theory, combinatorics, portfolio optimization, graphs, and much more.</p><p>So anyone can join, no matter your level of experience.</p><p>Join here: <a href="https://eigenscore.com/contests/round-1">https://eigenscore.com/contests/round-1</a></p>]]></content:encoded></item><item><title><![CDATA[EigenScore is Live: The First Rated Contest Platform for Quants]]></title><description><![CDATA[What I've been working on for the past months]]></description><link>https://www.vertoxquant.com/p/eigenscore</link><guid isPermaLink="false">https://www.vertoxquant.com/p/eigenscore</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Thu, 03 Sep 2026 03:12:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/cbea9225-201f-434f-8086-a1741ab0a2f6_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For the past months, almost every free hour I had went into one thing. Today it&#8217;s live.</p><p><a href="http://eigenscore.com">eigenscore.com</a></p><p>In quant, everyone claims to be good, and there's no arena to settle it. Competitive programming solved that twenty years ago with rated contests. I built the same thing for us.</p><h4>What it is</h4><p>EigenScore is a competitive platform for quants: a problem archive and rated contests with a persistent rating (Elo).</p><p>You get a problem archive of 50 problems at launch, across probability, combinatorics, martingales, stochastic calculus, option pricing, portfolio optimization, time series, games, graphs, and numerical methods. You write Python in the browser, hit Submit, and a judge tells you within seconds whether you were right.</p><p>But &#8220;right&#8221; means different things in quant, so there are three kinds of problems:</p><ul><li><p><strong>Standard</strong>: Those have an exact answer, like a probability or an option price, and the judge checks it against the reference within tolerance.</p></li><li><p><strong>Prediction</strong>: You&#8217;re given data and are asked for an estimate, like the next values of a process, a covariance matrix, or tail quantiles. There&#8217;s no correct output; you&#8217;re scored on how close your estimate gets to the best one that was theoretically possible from the data.</p></li><li><p><strong>Optimization</strong>: You&#8217;re asked to make a decision. Accept or reject an offer, allocate a portfolio, choose a stopping rule, and get scored by how well your decisions perform relative to the optimal policy. </p></li></ul><p>Then there are rated contests: two hours, five problems, everyone at once. When the round settles, every participant&#8217;s rating updates. You start as a Newbie, and then climb your ranks through Pupil, Apprentice, Specialist, Expert, Master, Grandmaster, all the way to Legend. </p><h4>What went into it</h4><p>Every problem is original. Written, solved, and tested by me, with reference solutions, hidden test sets, and tolerance checks. </p><p>Every submission runs inside its own hardware-isolated virtual machine that boots in milliseconds, runs your code with strict time and memory limits, and is destroyed afterwards. I built the judge, the worker fleet, the queuing, the scoring for all the problems, and the settlement engine.</p><p>And then everything else: accounts, contest scheduling, reminder emails, leaderboards, profiles with rating graphs, an announcement system, and the entire legal setup.</p><h4>What I need from you</h4><p>It&#8217;s day one. The archive is small and will grow every week. Things will break. If you hit a wrong verdict, an unclear statement, or anything that feels off, use the contact form or tell me directly. I read everything and most fixes ship within days.</p><p>If you know someone who&#8217;d enjoy this, like a colleague or a student, send them the link. A rating is only worth something when there are people to be rated against.</p><p>I&#8217;ll see you on Monday, 8 September, 20:00-22:00 CEST when the first rated contest goes live!</p><p>-Vertox</p>]]></content:encoded></item><item><title><![CDATA[How Firms Measure Tail Risk]]></title><description><![CDATA[A practitioner's guide to Extreme Value Theory]]></description><link>https://www.vertoxquant.com/p/how-firms-measure-tail-risk</link><guid isPermaLink="false">https://www.vertoxquant.com/p/how-firms-measure-tail-risk</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Sat, 22 Aug 2026 20:54:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!dX7R!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a919a7f-421b-4b4b-9547-13fda3ec8d4e_1089x470.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Chapter 1 - Why the Tail Resists Measurement</h2><p>The entire purpose of a risk system is to quantify potential losses that have yet to occur. We care about losses so extreme they have never (or very rarely) occurred. And that is precisely what makes tail-risk estimation so difficult: the number you are trying to estimate is the one with the least data.</p><p>Let&#8217;s put this in perspective. Let&#8217;s say you have hourly BTC returns from August 2017 to May 2026 (what we will use in this article), which gives us n = 76,908 observations. That sounds like plenty of data, and it is for your typical moves, since even at the 99% level you still have around 769 observations. But a desk does not blow up at a 99% event; let&#8217;s push this to a 1-in-10,000 event, and suddenly you only have around seven to eight data points you can use for estimation! And even worse, that&#8217;s assuming you use the ENTIRE dataset for estimation, which you probably don&#8217;t want to do since data from 2017 won&#8217;t be as relevant to you when trading in a 2026 environment.</p><p>And what if you care about a 1-in-100,000 event? You realistically have at most one data point to estimate that! Worry not, I will tell you how you can still quite reliably estimate those!</p><h3>The parametric approach: a distribution for everything</h3><p>The first instinct is to fit a distribution to the data and read the quantile off the fitted form. Because we are trying to estimate tail risk, let&#8217;s use a fat-tailed distribution like a Student-t distribution:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;L = \\mu + \\sigma T_\\nu, \\quad g_\\nu(t) = \\frac{\\Gamma\\!\\left(\\tfrac{\\nu+1}{2}\\right)}{\\sqrt{\\nu\\pi}\\,\\Gamma\\!\\left(\\tfrac{\\nu}{2}\\right)}\\left(1 + \\frac{t^{2}}{\\nu}\\right)^{-\\frac{\\nu+1}{2}},&quot;,&quot;id&quot;:&quot;SXTQOBDEUD&quot;}" data-component-name="LatexBlockToDOM"></div><p>so L itself has density</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\sigma^{-1}g_\\nu((x - \\mu)/\\sigma).&quot;,&quot;id&quot;:&quot;ZAHNUSIWEH&quot;}" data-component-name="LatexBlockToDOM"></div><p>The &#957; degrees of freedom govern the tail: as |t| &#8594; infty the density decays like |t|^(-v+1), a polynomial rather than an exponential, so the survival function is genuinely power-law,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;P(L>x) \\sim c x ^{-\\nu} \\quad (x \\to \\infty).&quot;,&quot;id&quot;:&quot;GWYMBNQYKD&quot;}" data-component-name="LatexBlockToDOM"></div><p>Writing</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\tau_\\alpha = t^{-1}_\\nu(\\alpha)&quot;,&quot;id&quot;:&quot;JCNZGWNUNY&quot;}" data-component-name="LatexBlockToDOM"></div><p>for the standard-t quantile, we get a clean closed form for VaR and CVaR:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathrm{VaR}_\\alpha = \\mu + \\sigma\\,\\tau_\\alpha, \\qquad \\mathrm{CVaR}_\\alpha = \\mu + \\sigma\\,\\frac{g_\\nu(\\tau_\\alpha)}{1-\\alpha}\\cdot\\frac{\\nu + \\tau_\\alpha^{2}}{\\nu - 1} \\qquad (\\nu > 1)&quot;,&quot;id&quot;:&quot;HMYQICRQJA&quot;}" data-component-name="LatexBlockToDOM"></div><p>where VaR and CVaR are defined as</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathrm{VaR}_\\alpha = \\inf\\{\\ell : P(L > \\ell) \\le 1-\\alpha\\}, \\qquad \\mathrm{CVaR}_\\alpha = \\mathbb{E}\\!\\left[\\,L \\mid L > \\mathrm{VaR}_\\alpha\\,\\right].&quot;,&quot;id&quot;:&quot;ROKOTREAOA&quot;}" data-component-name="LatexBlockToDOM"></div><p>The Student-t distributions appear to have done everything right. So where is the failure? Not in the shape of the tail, but in the data the fit uses to find it. We have one parameter, the degrees of freedom v, that governs both the body and tail of the distribution. We care about the v that is accurate for the tail, but when fitting the Student-t distribution, most of our samples will be in the body. Maximum likelihood will prioritize finding a v that fits the body well rather than the tails. Another problem is that the Student-t distribution is symmetric, while real loss distributions are asymmetric: the left tail is heavier than the right tail, and a single parameter can&#8217;t capture both.</p><h3>Historical simulation: no distribution at all</h3><p>The opposite instinct is to assume nothing. Sort the realized losses, take the empirical quantile, and compute the average loss beyond that quantile. Boom, you have VaR and CVaR. Many desks, in fact, run exactly this on a rolling basis. And true, it never lies about the shape of what has already happened, and here is exactly the limitation of this method: It can&#8217;t tell you about tail events that have never happened before (or have not happened in your rolling window).</p><p>What if I told you there is a principled way to model <em>just </em>the tail of a distribution, and, under mild assumptions, the tails of almost every distribution approach the same special one.</p><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why the loss big enough to blow up a book is the one you have the least data to measure, and why two standard answers both break in the tail.</p></li><li><p>How extreme value theory models the tail on its own terms, assuming nothing about the rest of the distribution, and lets you put a number on a loss larger than anything that ever happened in history.</p></li><li><p>Why a single VaR is only half the answer, and what expected shortfall tells you about how bad losses actually get once that line is crossed.</p></li><li><p>Why a tail fitted once over all your data is wrong on any given day, and how conditional EVT builds a tail that adapts to the market.</p></li><li><p>How to prove the final model performs better than the baseline models.</p></li><li><p>A full implementation!</p></li></ul><div><hr></div><h2>Chapter 2 - The Limit Laws of Extremes</h2>
      <p>
          <a href="https://www.vertoxquant.com/p/how-firms-measure-tail-risk">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Don't Predict the Mean. Predict the Entire Distribution]]></title><description><![CDATA[An introduction to Distributional Regression]]></description><link>https://www.vertoxquant.com/p/distributional-regression</link><guid isPermaLink="false">https://www.vertoxquant.com/p/distributional-regression</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Wed, 12 Aug 2026 18:27:44 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!pWSz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1>The Problem With Predicting the Mean</h1><p>Suppose you want to predict tomorrow&#8217;s return using today&#8217;s information. We collect some features (X_t) and fit a regression model:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{r}_{t+1}=f(X_t)&quot;,&quot;id&quot;:&quot;ZVUAZXFZHI&quot;}" data-component-name="LatexBlockToDOM"></div><p>This seems reasonable at first. If our model predicts a return of 0.5%, we can use that prediction to decide whether to trade, how much to trade, or which assets to prefer.</p><p>But there is a fundamental problem with this approach:</p><p><strong>It condenses all information into a single number.</strong></p><p>Distributions can look vastly different while having the same mean.<br>Here are 6 distributions, all with a mean of 0.5%:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!pWSz!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!pWSz!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 424w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 848w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 1272w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!pWSz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png" width="1189" height="886" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/b8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:886,&quot;width&quot;:1189,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:94194,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/210918541?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!pWSz!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 424w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 848w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 1272w, https://substackcdn.com/image/fetch/$s_!pWSz!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fb8ac6a6a-7591-48e9-ae95-a0c65e2d5858_1189x886.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Consider only the tight normal and high-vol normal. We would always wish for the tight normal, since we are way more confident about our predicted return.</p><p>Now you could go and layer on top something like conformal prediction (which we discussed in our previous article) to get a sense of uncertainty, but even that doesn&#8217;t tell you the whole distribution.</p><p>And here is another idea: If you are able to predict the entire distribution, you can back out numbers like:</p><ul><li><p>The probability of the trade being profitable.</p></li><li><p>The probability of a loss of a certain size.</p></li><li><p>Any arbitrary quantity of the distribution.</p></li></ul><p>This is what Distributional Regression gives you.</p><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why predicting the conditional mean throws away most of the information contained in a return distribution.</p></li><li><p>How distributional regression turns a conventional regression problem into one of predicting the entire conditional probability distribution.</p></li><li><p>How link functions map unconstrained predictors to valid distributional parameters, allowing us to model quantities such as scale or tail parameters while respecting their constraints.</p></li><li><p>How NGBoost combines gradient boosting with probabilistic prediction to learn these distributional parameters.</p></li><li><p>How distributional predictions can give you quantities such as volatility, quantiles, tail probabilities, and expected shortfall from a single model.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/distributional-regression">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Conformal Prediction in Quantitative Finance]]></title><description><![CDATA[Distribution-free prediction intervals, and what they actually guarantee on time series.]]></description><link>https://www.vertoxquant.com/p/conformal-prediction</link><guid isPermaLink="false">https://www.vertoxquant.com/p/conformal-prediction</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Sun, 02 Aug 2026 12:35:26 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Tnwf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In one of our previous articles, we built a neural-network-based volatility forecaster that beats baseline models in all volatility regimes:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;4bc8fa02-5715-4478-b4f8-8dcef98410f0&quot;,&quot;caption&quot;:&quot;Today, we are gonna look at something we&#8217;ve never done in any article before: Neural Networks.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;md&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Volatility Forecasting using Neural Networks&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Senior Quantitative Researcher&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-06-22T06:26:07.843Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!3r1m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef9e0208-b91a-4941-8103-036e724579b1_889x490.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/volatility-forecasting-using-neural&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:202958175,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:4,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>Now no need to be sceptical about the &#8220;neural network&#8221; part! We also break up all the common misconceptions people have about them in the article.</p><p>With that said, our forecast there is just a single point, and we have no notion of how certain the neural network is about that forecast. And that second number drives a lot of decisions in the real world: How wide to quote, how much size to put on, etc.</p><p>What we want is a range we believe realized volatility to land in with a certain probability! A naive way to get something like that is to assume a distribution: Fit the model, look at the residuals, assume they&#8217;re Gaussian, and then read off the bands you get. This fails immediately for volatility, whose residuals are skewed and heavy-tailed. Plus we&#8217;ve now bolted a distributional assumption onto a model we picked deliberately because it was flexible and assumption-light.</p><p>What we&#8217;d like is a distribution-free, finite-sample guarantee that the true value lands inside the interval with the probability we asked for, and being able to use any model. That is exactly what conformal prediction gives us.</p><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why a point forecast is only half of a volatility model, and what a proper prediction interval gives you that a single number can&#8217;t.</p></li><li><p>How conformal prediction wraps any model in a distribution-free coverage guarantee that holds in finite samples, without assuming anything about the shape of the errors.</p></li><li><p>Why the naive symmetric, constant-width interval is wrong for volatility, and how conformalized quantile regression (CQR) fixes both.</p></li><li><p>The assumptions the coverage guarantee rests on and why time series badly violate it.</p></li><li><p>How Adaptive Conformal Inference (ACI) restored a coverage guarantee under arbitrary distribution shift, and what it loses.</p></li><li><p>A full implementation on top of our neural-network-based volatility forecaster.</p></li></ul><div><hr></div><h1>Split Conformal Prediction</h1><p>Start with the model you&#8217;ve already trained and call it f^, which in our case is the neural-network-based volatility forecaster. Conformal prediction will never actually look inside the model and treat it as a complete black box. Instead, we set aside a chunk of data the model never saw during training, called the <strong>calibration set</strong>, which consists of n points (x_1, y_1), &#8230;, (x_n, y_n).</p><p>For each calibration point, we compute a <strong>nonconformity score</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;s_i = |y_i - \\hat{f}(x_i)|&quot;,&quot;id&quot;:&quot;PAGTNWYSQJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>For a target coverage of 1 - alpha (say 90%, so alpha = 0.1), we take an empirical quantile of the scores with a small finite-sample correction:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{q} = \\text{the} \\ \\lceil (n+1)(1-\\alpha) \\rceil \\text{-th smallest of } \\{s_1, ..., s_n\\}&quot;,&quot;id&quot;:&quot;GHODRXOHJZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>Finally, for any new point x_{n+1}, we form the interval by padding the point forecast with the above quantile:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{C}(x_{n+1}) = [\\hat{f}(x_{n+1} - \\hat{q}, \\hat{f}(x_{n+1})+\\hat{q}]&quot;,&quot;id&quot;:&quot;ORDDTUSXVY&quot;}" data-component-name="LatexBlockToDOM"></div><p>For a new point <em><strong>drawn from the same distribution, </strong></em>the interval covers the truth with at least the probability you asked for:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{P}(y_{n+1} \\in \\hat{C}(x_{n+1})) \\geq 1-\\alpha.&quot;,&quot;id&quot;:&quot;VCIPUCOZFS&quot;}" data-component-name="LatexBlockToDOM"></div><p>This holds for ANY model f^, no matter how bad. A really bad model will simply give you very wide intervals. Note that this holds in finite samples, and without any assumptions on the distribution of the data!</p><h3>Why it works, and the one assumption hiding inside</h3><p>Okay, maybe it&#8217;s not all sunshine and rainbows. The guarantee that the interval covers the truth of the new point with at least the probability you asked for rests on a single condition: the calibration scores and the new point&#8217;s score must be <strong>exchangeable, </strong>meaning that their joint distribution remains unchanged by any reordering. Exchangeability is the slightly weaker cousin of &#8220;i.i.d.&#8221;, since it doesn&#8217;t require independence, only that the ordering carries no information.</p><p>Our data is not that. A volatility series is ordered in time, and the ordering carries an enormous amount of information. Two more problems with this method for our volatility series are symmetry and constant-width. We apply the same padding up and down at a given point, which can even make the interval go into the negative for low-volatility periods (Which isn&#8217;t necessarily wrong, but we&#8217;d much rather have more accurate intervals). We also apply the same padding across different points in time, which is broken by heteroskedasticity in volatility.</p><div><hr></div><h1>Conformalized Quantile Regression</h1><p>Instead of producing a point forecast and padding it symmetrically, we will have the model predict its own lower and upper bounds directly. We give the network 2 output neurons that give us the quantiles </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{q}_{lo}(x) \\text{ and } \\hat{q}_{hi}(x),&quot;,&quot;id&quot;:&quot;RSJWSRWELW&quot;}" data-component-name="LatexBlockToDOM"></div><p>its own estimate of where the lower and upper ends of the outcome sit at that particular x. This already gives us good intervals, and conformal prediction will then come in and make the whole thing valid.</p><h3>Predicting Quantiles</h3><p>To get the low and high quantiles at levels alpha/2 and 1-alpha/2 (for 90% coverage, the 5th and 95th percentiles), we train the network with the pinball loss rather than a squared error. For a target quantile level tau, the pinball loss on the prediction q^and truth y is</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\rho_{\\tau}(y, \\hat{q}) = \\max (\\tau(y-\\hat{q}), (\\tau - 1)(y - \\hat{q})).&quot;,&quot;id&quot;:&quot;TJAUBNRAGE&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is an asymmetric loss that penalizes being below the truth and above the truth at different rates, and the rate is set by tau. Minimising it drives q^ toward the true conditional tau-quantile of y. In our neural network, we do this once for the tau=alpha/2 head and once for the tau=1-alpha/2 head, and sum the two losses together.</p><h3>Conformalizing the Quantiles</h3><p>Quantile regression on its own comes with no coverage guarantee at all. We still need conformal prediction to give us those nice guarantees. We define the new nonconformity score by how far the truth fell outside the predicted band</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;s_i = \\max (\\hat{q}_{lo}(x_i)-y_i, y_i - \\hat{q}_{hi}(x_i)).&quot;,&quot;id&quot;:&quot;SKKGNMQZUP&quot;}" data-component-name="LatexBlockToDOM"></div><p>We then take the same finite-sample-corrected quantile of these scores:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{q} = \\text{the} \\ \\lceil (n+1)(1-\\alpha) \\rceil \\text{-th smallest of } \\{s_1, ..., s_n\\}&quot;,&quot;id&quot;:&quot;BOBUGRVZEM&quot;}" data-component-name="LatexBlockToDOM"></div><p>and form the final interval for a new point by adjusting each predicted quantile:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{C}(x) = [\\hat{q}_{lo}(x_i) - \\hat{q}, \\hat{q}_{hi}(x_i) + \\hat{q}]&quot;,&quot;id&quot;:&quot;ZYDJNQJXYS&quot;}" data-component-name="LatexBlockToDOM"></div><p>There is still one thing to keep in mind, however. While CQR fixes the shape of the interval, it still carries along the problem with exchangeability. We will fix that next.</p><div><hr></div><h1>Adaptive Conformal Inference (ACI)</h1><p>The idea is to stop treating the miscoverage level as a fixed number. So far, alpha has been set once (0.1 for 90% coverage) and used as is forever. ACI makes it a variable that adjusts itself in response to how well we&#8217;ve actually been covering. If the intervals have been missing too often lately, we push the level to make them wider, and if they&#8217;ve been covering more than we asked for, we tighten them. Concretely, we track whether each interval covered its outcome</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{err}_t = 1\\{y_t \\notin \\hat{C}_t\\},&quot;,&quot;id&quot;:&quot;WWCKUQAPIA&quot;}" data-component-name="LatexBlockToDOM"></div><p>which is 1 when the interval missed and 0 when it covered. We then update the level using the following rule:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\alpha_{t+1} = \\alpha_t + \\gamma (\\alpha - \\text{err}_t).&quot;,&quot;id&quot;:&quot;IUCCRCXSBJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>Here alpha is still our target (0.1), while alpha_t is the target we actually feed to the conformal prediction to compute the quantiles</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{q}_t = \\text{the} \\ \\lceil (n+1)(1-\\alpha_t) \\rceil \\text{-th smallest of } \\{s_1, ..., s_n\\},&quot;,&quot;id&quot;:&quot;WTFDRZSPUN&quot;}" data-component-name="LatexBlockToDOM"></div><p>and gamma &gt; 0 is a step size that controls how quickly we adapt.</p><h3>What this gives us</h3><p>ACI requires no distributional assumptions, no exchangeability, no stationarity, and nothing about how the data is generated. Under arbitrary distribution shift, the realised long-run miscoverage converges to the target you asked for:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{1}{T} \\sum_{t=1}^T \\text{err}_t \\to \\alpha.&quot;,&quot;id&quot;:&quot;QUXIVHXPZZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>What you lose is the finite-sample guarantee, which is now replaced by this long-run average. On any single day, the interval can be wrong, and the way ACI achieves its average is by reacting after it was wrong. It also introduces a parameter gamma, which you need to tune. There is a variant called AgACI, which removes it by running several values of gamma in parallel and aggregating them, though a single well-chosen gamma is enough for our purpose.</p><div><hr></div><h1>Implementation and Testing on Real Data</h1><p>The rest of this article is built on top of the code from the following one:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;9fc234e6-1dc4-42ab-a308-de2f89cb9584&quot;,&quot;caption&quot;:&quot;Today, we are gonna look at something we&#8217;ve never done in any article before: Neural Networks.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;md&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Volatility Forecasting using Neural Networks&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Senior Quantitative Researcher&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-06-22T06:26:07.843Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!3r1m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef9e0208-b91a-4941-8103-036e724579b1_889x490.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/volatility-forecasting-using-neural&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:202958175,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:4,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>Step 1 is defining the constants we will use and adding a second output to the NARX model:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8a4e8d2a-7866-407d-aea5-cc5c05f6daa7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">ALPHA  = 0.10          # target miscoverage  -&gt;  90% intervals
TAU_LO = ALPHA / 2     # 0.05  lower quantile
TAU_HI = 1 - ALPHA / 2 # 0.95  upper quantile</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;885f3862-214f-4d2a-aa19-38195d7f3884&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">class QuantileForecastNet(nn.Module):
    def __init__(self, hparams):
        super().__init__()
        self.hparams = hparams
        self.n_d = hparams["n_d"]

        layers = []
        in_dim = hparams["input_dim"] + self.n_d
        for h in hparams["hidden_dims"]:
            layers.append(nn.Linear(in_dim, h))
            layers.append(nn.BatchNorm1d(h))
            layers.append(nn.ReLU())
            if hparams.get("dropout", 0.0) &gt; 0:
                layers.append(nn.Dropout(hparams["dropout"]))
            in_dim = h

        self.features = nn.Sequential(*layers)
        self.output = nn.Linear(in_dim, 2)          # [q_lo, q_hi]
        self.detection_head = nn.Linear(in_dim, 1)

        self.apply(self._init_weights)

    def _init_weights(self, module):
        if isinstance(module, nn.Linear):
            nn.init.kaiming_normal_(module.weight, nonlinearity='relu')
            nn.init.zeros_(module.bias)

    def forward(self, x, lags):
        z = self.features(torch.cat([x, lags], dim=-1))
        return self.output(z)                        # [B, 2]

    def forward_pretrain(self, x, lags_swapped):
        z = self.features(torch.cat([x, lags_swapped], dim=-1))
        return self.detection_head(z).squeeze(-1)</code></pre></div><p>Next, we need to be able to train the model using pinball loss:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;424c88ba-5c99-4064-98e3-1b9991e09422&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def pinball_loss(pred, target, tau):
    diff = target - pred
    return torch.mean(torch.maximum(tau * diff, (tau - 1.0) * diff))</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9e512983-1037-4d41-89fc-93d77edd7d2e&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def build_tensors(panel, feature_cols, hparams, device, with_target=True):
    panel = panel.copy()
    n_d = hparams["n_d"]
    lag_cols = [f"log_RV_lag{d}" for d in range(0, n_d)]
    for d, col in zip(range(0, n_d), lag_cols):
        panel[col] = panel["log_RV"].shift(d)
    panel = panel.dropna(subset=lag_cols)

    X = torch.tensor(panel[feature_cols].values, dtype=torch.float32).to(device)
    lags = torch.tensor(panel[lag_cols].values, dtype=torch.float32).to(device)
    y = None
    if with_target:
        y = torch.tensor(panel["target_log_RV_next"].values,
                         dtype=torch.float32).to(device)
    return X, lags, y</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3f0d24d0-b6da-4619-bff7-f990c2df388a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def train_model(model, train_panel, val_panel, feature_cols, hparams, device,
                name="default"):

    X_train, lags_train, y_train = build_tensors(train_panel, feature_cols, hparams, device)
    X_val,   lags_val,   y_val   = build_tensors(val_panel,   feature_cols, hparams, device)

    n_train = X_train.shape[0]
    batch_size = hparams.get("batch_size", 8192)
    n_batches = (n_train + batch_size - 1) // batch_size
    loss_cutoff = max(n_batches // 10, 1)

    model = model.to(device)
    optimizer = torch.optim.SGD(
        model.parameters(),
        lr=hparams["lr"],
        momentum=hparams["momentum"],
        weight_decay=hparams["weight_decay"],
        nesterov=True,
    )

    best_val_loss = float('inf')
    patience_counter = 0
    best_weights = None
    epoch = 0

    while True:

        model.train()
        train_losses = []
        perm = torch.randperm(n_train, device=device)

        pbar = tqdm(range(n_batches), desc=f"[{name}] epoch {epoch}", leave=False)
        for it in pbar:
            idx = perm[it * batch_size : (it + 1) * batch_size]
            xb, lagsb, yb = X_train[idx], lags_train[idx], y_train[idx]

            optimizer.zero_grad()
            q = model(xb, lagsb)                       # [B, 2]
            loss = (pinball_loss(q[:, 0], yb, TAU_LO)
                    + pinball_loss(q[:, 1], yb, TAU_HI))
            loss.backward()
            torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
            optimizer.step()

            train_losses.append(loss.item())
            train_losses = train_losses[-loss_cutoff:]
            pbar.set_postfix(train_loss=f"{np.mean(train_losses):.4f}")

        model.eval()
        with torch.no_grad():
            qv = model(X_val, lags_val)
            val_loss = (pinball_loss(qv[:, 0], y_val, TAU_LO)
                        + pinball_loss(qv[:, 1], y_val, TAU_HI)).item()

        #tqdm.write(f"[{name}] epoch {epoch}  train {np.mean(train_losses):.4f}  val {val_loss:.4f}")

        if val_loss &lt; best_val_loss:
            best_val_loss = val_loss
            best_weights = copy.deepcopy(model.state_dict())
            patience_counter = 0
        else:
            patience_counter += 1

        if patience_counter &gt;= hparams["patience"]:
            break
        epoch += 1

    if best_weights is not None:
        model.load_state_dict(best_weights)
    else:
        print("Warning: training never improved past initial val_loss (likely NaN/divergence)")
    return model</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9215d9ea-a610-4e7e-9c17-95910481b565&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def predict_quantiles(model, panel, feature_cols, hparams, device):
    X, lags, y = build_tensors(panel, feature_cols, hparams, device, with_target=True)
    model.eval()
    with torch.no_grad():
        q = model(X, lags).cpu().numpy()              # [N, 2]
    q_lo = np.minimum(q[:, 0], q[:, 1])               # enforce q_lo &lt;= q_hi
    q_hi = np.maximum(q[:, 0], q[:, 1])
    return q_lo, q_hi, y.cpu().numpy()</code></pre></div><p>And next, all of the conformal prediction machinery:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;aeeef1ec-0043-4eaa-8bb9-f1dcd552041a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def cqr_scores(q_lo, q_hi, y):
    return np.maximum(q_lo - y, y - q_hi)</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;da93f10a-e834-4e4c-8a79-70dab7b745a9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def cqr_quantile(sorted_scores, level):
    n = len(sorted_scores)
    k = int(np.ceil((n + 1) * level))
    if k &lt; 1:
        return -np.inf
    if k &gt; n:
        return np.inf
    return sorted_scores[k - 1]</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;e482fd97-a7e8-4c16-9ab2-3e5189301ef2&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def cqr_interval(cal_scores, q_lo_test, q_hi_test, alpha=ALPHA):
    s = np.sort(cal_scores)
    Q = cqr_quantile(s, 1 - alpha)
    return q_lo_test - Q, q_hi_test + Q</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5e37af6e-e529-46b2-9f7f-b1d795a542b9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def aci_cqr(cal_scores, q_lo_test, q_hi_test, y_test, alpha=ALPHA, gamma=0.01):
    s = np.sort(cal_scores)
    T = len(y_test)
    lo = np.empty(T)
    hi = np.empty(T)
    covered = np.empty(T, dtype=bool)
    alphas = np.empty(T)

    alpha_t = alpha
    for t in range(T):
        alphas[t] = alpha_t
        Q = cqr_quantile(s, 1 - alpha_t)
        lo[t] = q_lo_test[t] - Q
        hi[t] = q_hi_test[t] + Q

        err = 0.0 if (lo[t] &lt;= y_test[t] &lt;= hi[t]) else 1.0
        covered[t] = (err == 0.0)

        alpha_t = alpha_t + gamma * (alpha - err)
        alpha_t = min(max(alpha_t, 0.0), 1.0)         # keep the working level in [0, 1]

    return lo, hi, covered, alphas</code></pre></div><p>I will simply train this model using the same hyperparameters I picked for the single-point volatility forecast. If I were to run this in production, I would run full hyperparameter tuning again. For gamma, I just pick a standard value of 0.01, but you likely want to add it to the same hyperparameter tuning.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;6044b6b6-6f7c-4fe1-afee-9df852842126&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">hparams = {
    "input_dim": len(feature_cols),
    "hidden_dims": [370],
    "dropout": 0.25505051838919374,
    "lr": 0.01619079458212241,
    "momentum": 0.9,
    "weight_decay": 1e-05,
    "patience": 1000,
    "batch_size": 4096,
    "n_d": 30,
}

device = 'cuda:0'
model = QuantileForecastNet(hparams)

split = len(val_data) // 2
cal_data  = val_data.iloc[:split]
test_data = val_data.iloc[split:]

model = train_model(model, train_data, val_data, feature_cols, hparams, device)</code></pre></div><p>Now let&#8217;s finally test out our model:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3042d63c-db9b-49d1-a9d8-45cc923bfed4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">cal_lo, cal_hi, cal_y = predict_quantiles(model, cal_data, feature_cols, hparams, device)
cal_E = cqr_scores(cal_lo, cal_hi, cal_y)

# test-time quantiles
test_lo, test_hi, test_y = predict_quantiles(model, test_data, feature_cols, hparams, device)

# CQR + ACI (log space)
aci_lo, aci_hi, aci_covered, aci_alphas = aci_cqr(cal_E, test_lo, test_hi, test_y,
                                                  alpha=ALPHA, gamma=0.01)

print(f"CQR+ACI marginal coverage : {aci_covered.mean():.3f}")</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;76688229-d30f-49ca-b4ab-ac291c1cc8c8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">N = 300 

lo  = np.exp(aci_lo[:N])
hi  = np.exp(aci_hi[:N])
act = np.exp(test_y[:N])
mid = np.exp(0.5 * (aci_lo[:N] + aci_hi[:N]))

t = np.arange(N)

fig, ax = plt.subplots(figsize=(14, 5))
ax.fill_between(t, lo, hi, color="steelblue", alpha=0.25,
                label=f"{int((1-ALPHA)*100)}% CQR+ACI interval")
ax.plot(t, mid, color="steelblue", lw=1.0, label="predicted (band midpoint)")
ax.plot(t, act, color="black", lw=0.8, label="actual RV")

ax.set_xlabel("test step")
ax.set_ylabel("realized variance")
ax.set_title(f"CQR + ACI volatility intervals &#8212; first {N} test steps  "
             f"(overall coverage {aci_covered.mean():.1%}, target {1-ALPHA:.0%})")
ax.legend(loc="upper right", framealpha=0.9)
ax.margins(x=0)
plt.tight_layout()
plt.show()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Tnwf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Tnwf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 424w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 848w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 1272w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Tnwf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png" width="1389" height="490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:490,&quot;width&quot;:1389,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:145155,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/209480237?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Tnwf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 424w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 848w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 1272w, https://substackcdn.com/image/fetch/$s_!Tnwf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff68f7c2a-2a09-4e54-b4ae-d9fd4ce1c8fa_1389x490.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Those forecasts look absolutely fantastic! Between 200 and 250 you can see very tight intervals where the model is sure volatility is low, and around 50-150 where there is a ton of volatility, the model is naturally uncertain and outputs large intervals.</p><div><hr></div><h1>Conclusion</h1><p>We&#8217;ve taken our NARX volatility forecaster that already beats baseline models, and made it into something even greater! From here, there are still many directions you can take: </p><ol><li><p>Worst-case risk analysis using the upper quantile.</p></li><li><p>Adding back a point forecast as a third head, rather than taking the mean of the two heads.</p></li><li><p>Properly tuning gamma or using a method like AgACI.</p></li><li><p>Improving the underlying model itself further.</p></li></ol><div><hr></div><h3><strong>Other Articles You Would Enjoy</strong></h3><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;61921373-c00f-4779-b6f5-8653f790e37d&quot;,&quot;caption&quot;:&quot;Imagine you have multiple models forecasting asset returns.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Optimally Combining Forecasts&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Senior Quantitative Researcher&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-05-19T22:51:10.194Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!kb4Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F232c763f-61b8-4d92-abb9-0509513b040e_1390x1190.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/optimally-combining-forecasts&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:198322476,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:9,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;ce5eecc6-04d7-4a64-a218-3155bbf523cb&quot;,&quot;caption&quot;:&quot;In the previous article, we began our online learning theory journey by introducing a model that can take multiple models&#8217; forecasts and spit out a combined forecast that is often superior to any given model and is mathematically guaranteed to perform similarly to the best model.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;How to Build a Model That Adapts in Real Time&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Senior Quantitative Researcher&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-05-22T22:59:32.962Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!oGAt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36f36af4-55ba-4ebc-af17-de674802d788_1389x1621.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/how-to-build-a-model-that-adapts&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:198893102,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:4,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;4ac37b91-7d5f-4154-857d-cf5a7e1dd0f3&quot;,&quot;caption&quot;:&quot;Imagine the following: You spend weeks working on a strategy, the backtest looks great, a Sharpe of 1.5. You decide to go live, and it slowly and quietly stops working.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Strategy Decay Detection: Building a Warning System for Alpha Erosion&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Senior Quantitative Researcher&quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-04-14T09:17:32.961Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!Yd5E!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f0da95-24ec-4bd1-9b6f-73a13887cf7a_1189x2390.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/strategy-decay-detection&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:194057338,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:9,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div><hr></div><h3><strong>Quant Corner</strong></h3><p><strong>Join Quant Corner</strong><span>, a community that actually </span><em>does things</em><span>. Tournaments, live Q&amp;As, open discussions, and the best place to shape what gets built next.</span></p><p><span>Join here: </span><a href="https://discord.gg/X7TsxKNbXg">https://discord.gg/X7TsxKNbXg</a></p>]]></content:encoded></item><item><title><![CDATA[Common Quant Research Mistakes and How to Avoid Them]]></title><description><![CDATA[Beyond your typical "Don't overfit"]]></description><link>https://www.vertoxquant.com/p/common-quant-research-mistakes</link><guid isPermaLink="false">https://www.vertoxquant.com/p/common-quant-research-mistakes</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Fri, 24 Jul 2026 17:10:56 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!VxxQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F604f4368-4162-43c9-8a34-2bbeb2ad1293_989x390.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Whenever anyone talks about beginner mistakes in quant, it&#8217;s usually stuff like &#8220;you shouldn&#8217;t overfit&#8221; or &#8220;be careful about look-ahead bias&#8221;. While this advice does have a place, we will give some less common advice here and talk about mistakes that are often made that aren&#8217;t talked about enough, how to diagnose if you&#8217;ve fallen victim to them, and how to mitigate them.</p><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Common mistakes people make in quantitative research, and why they happen.</p></li><li><p>How to diagnose if you&#8217;ve fallen victim to any of the mistakes.</p></li><li><p>How to concretely mitigate the mistakes.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/common-quant-research-mistakes">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Causality in Time Series]]></title><description><![CDATA[Bayesian networks, Granger causality, directed information, and the limits of inference under hidden confounding]]></description><link>https://www.vertoxquant.com/p/causality-in-time-series</link><guid isPermaLink="false">https://www.vertoxquant.com/p/causality-in-time-series</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Tue, 14 Jul 2026 20:50:20 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!U6SD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I&#8217;m sure all of you have heard the following sentence before: &#8220;Correlation is not Causation&#8221;. That much is clear, but what is Causation exactly then? And how do we measure which variable causes which other variable?</p><p>Two time series can move together for a lot of reasons that have nothing to do with one driving the other. Ice cream sales and drowning incidents both rise every summer without either one causing the other; they&#8217;re both just downstream of the same thing: hot weather. </p><p>We start with Bayesian networks and how they tell us which variables can influence which. From there, we move to structural equation models, which let us distinguish between observing a variable and actually intervening on it. We then bring in Granger causality, the classical way of asking whether one time series helps predict another, and its more general, information-theoretic counterpart, directed information. In the end, we take a look at the hardest and most practically relevant case: what happens when some of the variables actually driving the system were never observed at all, and how much of the true causal structure can still be recovered despite that.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!U6SD!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!U6SD!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 424w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 848w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 1272w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!U6SD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png" width="576" height="313" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:313,&quot;width&quot;:576,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:137692,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/207003374?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!U6SD!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 424w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 848w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 1272w, https://substackcdn.com/image/fetch/$s_!U6SD!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F68c39602-4757-48ff-a3eb-313a1ce703e7_576x313.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>What it actually means for one variable to cause another, and why a Bayesian network&#8217;s conditional independencies alone can&#8217;t answer that.</p></li><li><p>Why conditioning on the wrong variable can create a dependence between two variables that never existed.</p></li><li><p>How the intervention operator do(.) formally separates observing X=x from <em>forcing </em>X=x.</p></li><li><p>How Granger causality turns &#8220;does X cause Y&#8221; into a precise, testable statement about prediction, and what directed information has to do with it.</p></li><li><p>Why a naive causality test across many time series can show a relationship between two variables that don&#8217;t actually influence each other at all, and what conditioning on the rest of the system has to do with fixing it.</p></li><li><p>What happens when hidden variables enter the system, and under what conditons the true causal structure can still be recovered anyway.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/causality-in-time-series">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Generating Financial Data using GANs]]></title><description><![CDATA[Adversarial Training, and the TCN Architecture]]></description><link>https://www.vertoxquant.com/p/generating-financial-data-using-gans</link><guid isPermaLink="false">https://www.vertoxquant.com/p/generating-financial-data-using-gans</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Sun, 05 Jul 2026 11:34:43 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!AwUk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>When backtesting a strategy using historical data, you are limited to one sequence of events, and your backtest is essentially measuring how well your strategy fits that single path.</p><p>When pricing options using Monte Carlo methods, on the other hand, you often fit something like Brownian motion to the underlying as well as possible and take the average payoff of those simulated paths, knowing full well that they behave nothing like the real asset.</p><p>GAN's solve both of those issues by learning the data-generating process directly from real market data with no assumptions about normality, no hand-crafted volatility models, and no direct parameters. The generator learns to produce synthetic return paths that are statistically indistinguishable from the real thing, preserving heavy tails, volatility clustering, the leverage effect, etc. that are present in real markets and that parametric models consistently get wrong.</p><p>In this article, we build up the full framework from scratch. We start with the original GAN, work through the training instabilities that make it difficult to use in practice, and then introduce methods to fix this training instability. In the end, we will take a look at one specific architecture called Quant GANs that is specifically designed for financial time series.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!AwUk!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!AwUk!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 424w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 848w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 1272w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!AwUk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png" width="968" height="619" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:619,&quot;width&quot;:968,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:427383,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/204933686?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!AwUk!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 424w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 848w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 1272w, https://substackcdn.com/image/fetch/$s_!AwUk!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F3189b38c-5fce-44bb-916b-6cfe57d03fe4_968x619.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Some generated paths directly from the Quant GAN paper</figcaption></figure></div><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3>What you&#8217;ll learn</h3><ul><li><p>What GANs are and why they&#8217;re so hard to train in practice.</p></li><li><p>How the Wasserstein distance fixes the core mathematical failure of the original GAN objective.</p></li><li><p>How Mescheder&#8217;s gradient penalty stabilizes training.</p></li><li><p>How the Quant GAN architecture works and how it reproduces stylized facts of financial returns like heavy tails, volatility clustering, and the leverage effect.</p></li><li><p>How to evaluate a generative model for financial data, and why standard metrics like loss curves tell you almost nothing.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/generating-financial-data-using-gans">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Volatility Forecasting using Neural Networks]]></title><description><![CDATA[Separating the hype from what actually moves the needle]]></description><link>https://www.vertoxquant.com/p/volatility-forecasting-using-neural</link><guid isPermaLink="false">https://www.vertoxquant.com/p/volatility-forecasting-using-neural</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Mon, 22 Jun 2026 06:26:07 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!3r1m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef9e0208-b91a-4941-8103-036e724579b1_889x490.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Today, we are gonna look at something we&#8217;ve never done in any article before: Neural Networks.</p><p>Neural networks have a bad reputation in the quant finance community, and I constantly see people skip papers with titles like &#8220;Volatility Forecasting using Neural Networks&#8221; immediately. The reason is usually some version of: black-box, overfits, needs more data than any of us actually have, traditional models are more robust.</p><p>First things first, we will be clearing up some of the common misconceptions and giving them a chance, like proper researchers. Next, we will talk about the specific model architecture used in this article, why forecasting in log-space turns out to improve performance, and how we selected a final model without overfitting. Finally, we&#8217;ll put the model to the test against a simple traditional baseline model commonly used for volatility forecasting.</p><div><hr></div><p><span>I write about quantitative trading the way it&#8217;s actually practised:</span></p><p><span>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</span></p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why the usual complaints about neural networks in finance hold up far less than people assume, and where they&#8217;re actually right.</p></li><li><p>How a NARX architecture works, and how to apply it to volatility forecasting.</p></li><li><p>Why forecasting realized volatility in log-space improves training stability and accuracy, even when your loss function (QLIKE) is already designed to be scale-robust. </p></li><li><p>How to tune and select a model, including catching when a network is overfitting.</p></li><li><p>How to interpret a trained network&#8217;s predictions using SHAP, instead of treating it as an unreadable black box.</p></li><li><p>A full comparison against a tuned EWMA and persistence baseline.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!dJBj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!dJBj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 424w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 848w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 1272w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!dJBj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png" width="1389" height="490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5a233813-1541-47f2-bf20-a36539053a77_1389x490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:490,&quot;width&quot;:1389,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:106697,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/202958175?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!dJBj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 424w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 848w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 1272w, https://substackcdn.com/image/fetch/$s_!dJBj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5a233813-1541-47f2-bf20-a36539053a77_1389x490.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div>
      <p>
          <a href="https://www.vertoxquant.com/p/volatility-forecasting-using-neural">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Fast Option Pricing using Fourier Transform]]></title><description><![CDATA[When Monte-Carlo is too slow]]></description><link>https://www.vertoxquant.com/p/fast-option-pricing-using-fourier</link><guid isPermaLink="false">https://www.vertoxquant.com/p/fast-option-pricing-using-fourier</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Tue, 09 Jun 2026 22:27:48 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!1H6m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Monte-Carlo Simulation is the most straightforward way to price an option, and if you don&#8217;t care about speed, it&#8217;s a solid choice.</p><p>The moment you care about speed, like when quoting live, or when calibrating a pricing model where you need to reprice options thousands of times, Monte Carlo quickly becomes unusable.</p><p>This article presents three Fourier-based methods that solve this problem, reducing pricing from seconds to microseconds. We&#8217;ll use the Heston model as our running example throughout, and achieve a speedup of 30,000x against Monte Carlo!</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practised:<br><br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why Monte Carlo breaks down for live pricing and model calibration, and what makes it fundamentally unsuitable for production options trading.</p></li><li><p>How the Fourier Transform and FFT work and why they matter for options pricing.</p></li><li><p>What the characteristic function is and how it enables us to price options even when the risk-neutral density of the model has no known closed-form analytical solution.</p></li><li><p>How Carr-Madan transforms option pricing into an FFT problem.</p></li><li><p>How Lewis reformulates the same problem via contour integration, eliminating the dampening parameter that makes Carr-Madan brittle.</p></li><li><p>How the COS method takes a completely different approach, approximating the density via a cosine series, and why it converges exponentially fast.</p></li><li><p>Python implementations for all methods, together with speed comparisons.</p></li></ul><p>This article is free to read, so I would appreciate it if you shared it with someone who would also like it!</p><div><hr></div><h1>The Characteristic Function</h1><p>To price options using Fourier methods, we need one thing from our model: the characteristic function of log S_T where S_T is the price at expiry.</p><p>For a random variable X, the characteristic function is defined as:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi(u) = \\mathbb{E}[e^{iuX}]&quot;,&quot;id&quot;:&quot;NRZRNDHLYL&quot;}" data-component-name="LatexBlockToDOM"></div><p>For log S_T we write:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi_T(u) = \\mathbb{E}^\\mathbb{Q}[e^{iu \\log S_T}],&quot;,&quot;id&quot;:&quot;WHXRFBHFWZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>where Q is the risk-neutral measure.</p><p>Just like the probability density function, it completely characterises the distribution of X. But while the density q(x) often has no closed-form expression, the characteristic function often does and is much easier to find.</p><p>In fact, the characteristic function and the density are a Fourier transform pair. Given the characteristic function, you can recover the density via the inverse Fourier transform:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;q(x) = \\frac{1}{2\\pi}\\int_{-\\infty}^\\infty e^{-iux}\\phi_T(u)du&quot;,&quot;id&quot;:&quot;TXSKEDBQVZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is the core idea behind all Fourier pricing methods. You don&#8217;t need to know the distribution of log S_T explicitly; you just need the characteristic function.</p><h3>Why does the characteristic function always exist?</h3><p>For any model where log S_T is a well-defined random variable, the characteristic function is guaranteed to exist for all real u, since</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|e^{iu\\log S_T}|=1,&quot;,&quot;id&quot;:&quot;OVHFZELRCR&quot;}" data-component-name="LatexBlockToDOM"></div><p>and thus the expectation is always finite.</p><h3>Pricing in Fourier Space</h3><p>In theory, you could now go ahead and compute the distribution from the characteristic function numerically, and then integrate against the payoff to get the fair value of the option. In practice, Fourier pricing methods skip this step entirely and price directly in Fourier space, which is both faster and more numerically stable.</p><h3>Two Examples</h3><p>For Black-Scholes, log S_T is Gaussian, so the characteristic function is that of a Gaussian random variable, with appropriate mean and standard deviation plugged in:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi_T^{\\text{BS}}(u) = \\exp(iu(\\log S_0 + (r-\\frac{\\sigma^2}{2})T) - \\frac{u^2 \\sigma^2 T}{2})&quot;,&quot;id&quot;:&quot;BUAIBETWKQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>For Heston, log S_T has no nice closed-form density expression. But the characteristic function is still available in closed form:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi_T^\\text{Heston}(u) = \\exp(iu \\log S_0 + A(T, u) + B(T,u) v_0)&quot;,&quot;id&quot;:&quot;BQMCKPYUJF&quot;}" data-component-name="LatexBlockToDOM"></div><p>where:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{align}\nA(T,u) &amp;= iurT + \\frac{\\kappa \\theta}{\\xi^2}[(\\rho\\xi iu-\\kappa - d)T - 2 \\log \\frac{1-ge^{-dT}}{1-g}] \\\\\nB(T,u) &amp;= \\frac{(\\rho\\xi iu - \\kappa - d)(1-e^{-dT})}{\\xi^2(1-ge^{-dT})}\n\\end{align}&quot;,&quot;id&quot;:&quot;YJHWDCYDMU&quot;}" data-component-name="LatexBlockToDOM"></div><p>with</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{align}\nd &amp;= \\sqrt{(\\rho\\xi i u - \\kappa)^2 + \\xi^2(iu+u^2)} \\\\\ng &amp;= \\frac{\\rho \\xi i u - \\kappa - d}{\\rho \\xi i u - \\kappa + d}\n\\end{align}&quot;,&quot;id&quot;:&quot;DNKVAHASQY&quot;}" data-component-name="LatexBlockToDOM"></div><p>These are derived by applying It&#244;&#8217;s lemma to log S_t, exploiting the affine structure of the Heston dynamics, and solving the resulting Riccati ODEs in closed form.</p><p>How to properly derive this will be the subject of future articles.</p><div><hr></div><h1>Fourier Transform and FFT</h1><p>Before deriving any pricing formulas, we need to understand the Fourier Transform and the Fast Fourier Transform algorithm.</p><h3>The Fourier Transform</h3><p>For a real square integrable function f(x), the Fourier transform is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{f}(\\xi) = \\int_{-\\infty}^\\infty e^{i\\xi x}f(x)dx&quot;,&quot;id&quot;:&quot;UTWYBTBMIG&quot;}" data-component-name="LatexBlockToDOM"></div><p>This function lives in frequency space; It tells you how much of each frequency xi is present in f. You recover f from hat{f} via the inverse Fourier transform:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;f(x) = \\frac{1}{2\\pi}\\int_{-\\infty}^\\infty e^{-i\\xi x}\\hat{f}(\\xi)dx.&quot;,&quot;id&quot;:&quot;POKSGHEIEV&quot;}" data-component-name="LatexBlockToDOM"></div><p>Two real functions f and g that are both square integrable have a well-defined inner product. Parseval&#8217;s theorem says this inner product can also be computed in Fourier space:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_{-\\infty}^\\infty f(x)\\overline{g(x)}dx = \\frac{1}{2\\pi}\\int_{-\\infty}^\\infty \\hat{f}(\\xi)\\overline{\\hat{g}(\\xi)}d\\xi&quot;,&quot;id&quot;:&quot;CZJPYDZBMJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>This will be used later on in the Lewis method.</p><h3>The Discrete Fourier Transform</h3><p>In practice, you work with a finite grid of N points rather than a continuous function. The Discrete Fourier Transform (DFT) of a sequence x_0, x_1, &#8230;, x_{N-1} is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;X_k = \\sum_{j=0}^{N-1} x_j e^{-i2\\pi\\frac{k}{N}j}.&quot;,&quot;id&quot;:&quot;GKNREUVCLO&quot;}" data-component-name="LatexBlockToDOM"></div><p>Each output X_k is the inner product of the input sequence against a complex exponential at frequency k. Computing all N outputs naively requires evaluating N terms for each of the N outputs, a total of N^2 multiplications.</p><h3>The Fast Fourier Transform</h3><p>For N a power of 2, the Fast Fourier Transform, introduced by Cooley and Tukey, reduces the cost from O(N^2) to O(N log N) by exploiting the fact that the DFT of a sequence of length N can be written as the sum of two DFTs of length N/2.<br>Applying this decomposition recursively, splitting each half into its own two halves, reduces the total number of multiplications to O(N log N). For N = 4096, this is roughly a 340x speedup over the naive approach!</p><p>More formally, the DFT of a sequence of length N can be written as the sum of the DFT over the even-indexed elements and the DFT over the odd-indexed elements:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\begin{align}\nX_k &amp;= \\sum_{j=0}^{N/2-1} x_{2j}e^{-\\frac{2\\pi i}{N}(2j)k} + \\sum_{j=0}^{N/2-1} x_{2j+1}e^{-\\frac{2\\pi i}{N}(2j+1)k}\\\\\n&amp;= E_k + e^{-\\frac{2\\pi i}{N}k} O_k\n\\end{align}&quot;,&quot;id&quot;:&quot;MRVUJQPJJB&quot;}" data-component-name="LatexBlockToDOM"></div><p>where E_k and O_k are the DFTs of the even and odd subsequences.</p><div><hr></div><h1>Carr-Madan</h1><p>The Carr-Madan method (1999) is one of the most popular option pricing methods using the Fast Fourier Transform. </p><p>Let s = log S_T and k = log K. The risk-neutral density of s is q_T(s), with characteristic function</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi_T(u) = \\int_{-\\infty}^\\infty e^{ius}q_T(s)ds.&quot;,&quot;id&quot;:&quot;RVDNZUDIKR&quot;}" data-component-name="LatexBlockToDOM"></div><p>The call price as a function of log strike is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k) = e^{-rT} \\int_k^\\infty (e^s - e^k)q_T(s)ds&quot;,&quot;id&quot;:&quot;KWUCVIYOOK&quot;}" data-component-name="LatexBlockToDOM"></div><p>We want to Fourier transform C_T(k). The problem is that C_T(k) is not square-integrable, since as k&#8594; -infinity, C_T(k) &#8594; S_0 rather than decaying to zero. So its Fourier Transform doesn&#8217;t exist in the classical sense.</p><p>The fix is to multiply by an exponential dampening factor. Define the modified call price:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;c_T(k) = e^{\\alpha k }C_T(k), \\quad \\alpha > 0.&quot;,&quot;id&quot;:&quot;OREAOUDLEE&quot;}" data-component-name="LatexBlockToDOM"></div><p>For suitable alpha, c_T(k) is square integrable, since the e^{alpha k) term decays fast enough as k &#8594; -infty to kill the blow-up. Therefore, this has a well-defined Fourier transform:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v) = \\int_{-\\infty}^\\infty e^{ivk}c_T(k)dk&quot;,&quot;id&quot;:&quot;PGNXSSEZNB&quot;}" data-component-name="LatexBlockToDOM"></div><p>Our goal now is to write an analytical expression of Psi_T in terms of phi_T, and then obtain call prices numerically using the inverse transform.</p><h3>Deriving Psi_T</h3><p>Substitute the definitions of c_T(k) and C_T(k):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v) = \\int_{-\\infty}^\\infty e^{ivk}e^{\\alpha k} \\int_k^\\infty e^{-rT}(e^s - e^k)q_T(s)dsdk&quot;,&quot;id&quot;:&quot;KNOJJFVAMM&quot;}" data-component-name="LatexBlockToDOM"></div><p>The region of integration is</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\{(k,s):k\\in(-\\infty,\\infty), s\\in(k,\\infty)\\},&quot;,&quot;id&quot;:&quot;QDTKICICLW&quot;}" data-component-name="LatexBlockToDOM"></div><p>which is equivalent to:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\{(k,s): s\\in(-\\infty,\\infty), k\\in(-\\infty,s)\\}.&quot;,&quot;id&quot;:&quot;XZOIDFRDRJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>The integrand is non-negative, so we can apply Tonelli&#8217;s theorem and swap the order of integration:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v) = \\int_{-\\infty}^\\infty e^{-rT}q_T(s)\\int_{-\\infty}^s e^{\\alpha k}(e^s-e^k)e^{ivk}dkds&quot;,&quot;id&quot;:&quot;ISHVPXYWFS&quot;}" data-component-name="LatexBlockToDOM"></div><p>Expand the inner integrand:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{\\alpha k}(e^s - e^k)e^{ivk} = e^s e^{(a+iv)k} - e^{(1+\\alpha+iv)k}&quot;,&quot;id&quot;:&quot;HGQYHUQOPA&quot;}" data-component-name="LatexBlockToDOM"></div><p>For the first term:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^s \\int_{-\\infty}^{s} e^{(\\alpha+iv)k}\\,dk\n= e^s \\left[ \\frac{e^{(\\alpha+iv)k}}{\\alpha+iv} \\right]_{-\\infty}^{s}\n= \\frac{e^{(\\alpha+1+iv)s}}{\\alpha+iv}&quot;,&quot;id&quot;:&quot;HQWOKDWCGU&quot;}" data-component-name="LatexBlockToDOM"></div><p>For the second term:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_{-\\infty}^{s} e^{(1+\\alpha+iv)k}\\,dk\n=\n\\left[\n\\frac{e^{(1+\\alpha+iv)k}}{1+\\alpha+iv}\n\\right]_{-\\infty}^{s}\n=\n\\frac{e^{(1+\\alpha+iv)s}}{1+\\alpha+iv}\n\\&quot;,&quot;id&quot;:&quot;KQHGVVZBMS&quot;}" data-component-name="LatexBlockToDOM"></div><p>Substituting back:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v)\n=\ne^{-rT}\n\\int_{-\\infty}^{\\infty}\nq_T(s)\n\\left(\n\\frac{e^{(\\alpha+1+iv)s}}{\\alpha+iv}\n-\n\\frac{e^{(\\alpha+1+iv)s}}{\\alpha+1+iv}\n\\right)\nds&quot;,&quot;id&quot;:&quot;EJAQULMPLR&quot;}" data-component-name="LatexBlockToDOM"></div><p>Factor out </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{(\\alpha+1+iv)s}&quot;,&quot;id&quot;:&quot;WQCNKASDMR&quot;}" data-component-name="LatexBlockToDOM"></div><p>and recognise that since</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{(\\alpha+1+iv)s} = e^{i(v-(\\alpha+1)i)s},&quot;,&quot;id&quot;:&quot;JCITOQMOGO&quot;}" data-component-name="LatexBlockToDOM"></div><p>we have:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_{-\\infty}^{\\infty}\nq_T(s)e^{(\\alpha+1+iv)s}\\,ds\n=\n\\phi_T\\!\\bigl(v-(\\alpha+1)i\\bigr)&quot;,&quot;id&quot;:&quot;LXOBHBZZDJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>So:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v)\n=\ne^{-rT}\n\\phi_T\\!\\bigl(v-(\\alpha+1)i\\bigr)\n\\left(\n\\frac{1}{\\alpha+iv}\n-\n\\frac{1}{\\alpha+1+iv}\n\\right)&quot;,&quot;id&quot;:&quot;IHAYFWWHPH&quot;}" data-component-name="LatexBlockToDOM"></div><p>After simplifying, we finally get:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(v)\n=\n\\frac{\ne^{-rT}\\phi_T\\!\\bigl(v-(\\alpha+1)i\\bigr)\n}{\n\\alpha^2+\\alpha-v^2+i(2\\alpha+1)v\n}&quot;,&quot;id&quot;:&quot;KLHUNDEGUS&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is expressed entirely in terms of phi_T, the characteristic function of log S_T under our model of choice, which we said earlier we often have a closed-form solution for, so this whole thing is closed-form!</p><h3>Recovery Formula</h3><p>Since Psi_T is the Fourier transform of the dampened call price, standard Fourier inversion gives:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;c_T(k) = \\frac{1}{2\\pi}\\int_{-\\infty}^\\infty e^{-ivk}\\Psi_T(v)dv&quot;,&quot;id&quot;:&quot;ROBKRWKSMX&quot;}" data-component-name="LatexBlockToDOM"></div><p>Dividing both sides by e^{alpha k}:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k) = \\frac{e^{-\\alpha k}}{2\\pi} \\int_{-\\infty}^\\infty e^{-ivk}\\Psi_T(v)dv&quot;,&quot;id&quot;:&quot;JBHQINAYDB&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since C_T(k) is real, we have</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Psi_T(-v) = \\overline{\\Psi_T(v)},&quot;,&quot;id&quot;:&quot;CWCQVZHXMU&quot;}" data-component-name="LatexBlockToDOM"></div><p>so the integrand over (-infty, 0) is the complex conjugate of the integrand over (0,infty), and their sum is twice the real part:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k) = \\frac{e^{-\\alpha k}}{\\pi} \\int_0^\\infty \\Re[e^{-ivk} \\Psi_T(v)]dv&quot;,&quot;id&quot;:&quot;NFHKCCALWA&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Choice of alpha</h3><p>Two conditions constrain alpha. First, for Psi_T(0) to be finite, we need</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\phi_T(-(\\alpha + 1)i) = \\mathbb{E}^\\mathbb{Q}[S_T^{\\alpha + 1}]< \\infty.&quot;,&quot;id&quot;:&quot;ABXVPEFLIV&quot;}" data-component-name="LatexBlockToDOM"></div><p>Another condition is alpha not being equal to 0, to avoid a singularity in the denominator at v = 0. For Black-Scholes and Heston, all positive moments exist, so alpha is unconstrained. Carr and Madan suggest using one quarter of the upper bound implied by the moment condition, and find alpha = 1.5 works well in practice.</p><h3>Truncation bound</h3><p>The recovery integral runs from 0 to infinity. We need to verify it converges and can be safely truncated at some finite value a. For that, let&#8217;s first look at the numerator of |Psi_T(v)|.</p><p>It&#8217;s easy to see that</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|\\phi_T(v-(\\alpha + 1)i)| \\leq \\mathbb{E}^\\mathbb{Q}[S_T^{\\alpha+1}],&quot;,&quot;id&quot;:&quot;NSJWEXFBZI&quot;}" data-component-name="LatexBlockToDOM"></div><p>which is independent of v. </p><p>The denominator of Psi_T(v) grows like v^4 for large v, so:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|\\Psi_T(v)|^2 \\leq \\frac{\\mathbb{E}[S_T^{\\alpha+1}]}{(\\alpha^2+\\alpha-v^2)^2+(2\\alpha+1)^2v^2} \\leq \\frac{A}{v^4}&quot;,&quot;id&quot;:&quot;FCYSNOBXTY&quot;}" data-component-name="LatexBlockToDOM"></div><p>for some constant A. Therefore</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|\\Psi_T(v)| \\leq \\frac{\\sqrt{A}}{v^2}&quot;,&quot;id&quot;:&quot;UPQNPZZCAF&quot;}" data-component-name="LatexBlockToDOM"></div><p>and the tail integral from any truncation point a satisfies:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_a^\\infty|\\Psi_T(v)|dv \\leq \\frac{\\sqrt{A}}{a}&quot;,&quot;id&quot;:&quot;ADPTSEQJDM&quot;}" data-component-name="LatexBlockToDOM"></div><p>Thus, the truncation error is bounded by:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|C_T(k) - \\tilde{C}_T(k)| \\leq \\frac{e^{-\\alpha k}}{\\pi}  \\frac{\\sqrt{A}}{a}&quot;,&quot;id&quot;:&quot;LBXBYZITCJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>To achieve a target error epsilon, choose:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;a > \\frac{e^{-\\alpha k} \\sqrt{A}}{\\pi \\epsilon}&quot;,&quot;id&quot;:&quot;NMBVUQDQIF&quot;}" data-component-name="LatexBlockToDOM"></div><h3>FFT discretization</h3><p>Discretize the recovery integral with N points at spacing eta, setting</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;v_j = \\eta(j-1) \\quad \\text{for} \\ j=1,...,N&quot;,&quot;id&quot;:&quot;KYRCIAGAMN&quot;}" data-component-name="LatexBlockToDOM"></div><p>Applying the trapezoid rule:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k) \\approx \\frac{e^{-\\alpha k}}{\\pi} \\sum_{j=1}^N e^{-iv_jk}\\Psi_T(v_j)\\eta&quot;,&quot;id&quot;:&quot;BFPFDQYHZN&quot;}" data-component-name="LatexBlockToDOM"></div><p>The effective upper limit of integration is a = N*eta. We want to evaluate this at a grid of N log-strikes</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;k_u = -b + \\lambda(u-1) \\quad \\text{for} \\ u=1,..,N,&quot;,&quot;id&quot;:&quot;GTGJTCNQMM&quot;}" data-component-name="LatexBlockToDOM"></div><p>which gives us log strike levels ranging from -b to b, centred around ATM by setting </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;b = \\frac{1}{2}N \\lambda.&quot;,&quot;id&quot;:&quot;QFOYWNAYFF&quot;}" data-component-name="LatexBlockToDOM"></div><p>Substituting v_j and k_u:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{-iv_jk_u} = e^{ibv_j}e^{-i\\eta\\lambda(j-1)(u-1)}&quot;,&quot;id&quot;:&quot;LYUTYFVUNZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>For the sum to match the DFT structure</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{-i\\frac{2\\pi}{N}(j-1)(u-1)}&quot;,&quot;id&quot;:&quot;ELUALEGWCK&quot;}" data-component-name="LatexBlockToDOM"></div><p>we need</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\lambda\\eta = \\frac{2\\pi}{N}.&quot;,&quot;id&quot;:&quot;LVKFWEQOUE&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is the grid coupling constraint; Fine integration grid (eta small) forces coarse strike spacing (lambda large) and vice versa. The sum becomes:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k_u) \\approx \\frac{e^{-\\alpha k_u}}{\\pi} \\sum_{j=1}^N e^{-i\\frac{2\\pi}{N}(j-1)(u-1)}e^{ibv_j}\\Psi_T(v_j)\\eta&quot;,&quot;id&quot;:&quot;IYFYBKYWPY&quot;}" data-component-name="LatexBlockToDOM"></div><p>which is exactly the DFT of the sequence</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;e^{ibv_j}\\Psi_t(v_j)\\eta.&quot;,&quot;id&quot;:&quot;CQLGOVTYBY&quot;}" data-component-name="LatexBlockToDOM"></div><p>A single FFT call evaluates this for all N strikes simultaneously.</p><p>So you build the input vector</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;x_j = e^{ibv_j}\\Psi_T(v_j)\\eta&quot;,&quot;id&quot;:&quot;DTOBQMUDAS&quot;}" data-component-name="LatexBlockToDOM"></div><p>once and then call the FFT algorithm. Then multiply the u-th output by</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{e^{-\\alpha k_u}}{\\pi}&quot;,&quot;id&quot;:&quot;BBLOPFSCMQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>to get the final call prices for each strike.</p><h3>Simpson&#8217;s rule</h3><p>The trapezoid rule has errors O(eta^2). To get accurate integration with larger eta, which, via the grid coupling constraint, means finer strike spacing, we replace the uniform weight eta with Simpson&#8217;s rule weights:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;w_j = \\frac{\\eta}{3}[3+(-1)^j + \\delta_{j-1}]&quot;,&quot;id&quot;:&quot;QISRMAXNIT&quot;}" data-component-name="LatexBlockToDOM"></div><p>where delta_{j-1} is the Kronecker delta, equal to 1 only at j=1. The error improves from O(eta^2) to O(eta^4), and the FFT structure is preserved since replacing eta with w_j just multiplies each term by a scalar before applying the FFT. </p><p>The final formula is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C_T(k_u) \\approx \\frac{e^{-\\alpha k_u}}{\\pi} \\sum_{j=1}^N e^{-i\\frac{2\\pi}{N}(j-1)(u-1)}e^{ibv_j}\\Psi_T(v_j)w_j&quot;,&quot;id&quot;:&quot;WFJZUPAPJI&quot;}" data-component-name="LatexBlockToDOM"></div><h3>The weakness of Carr-Madan</h3><p>The grid coupling constraint is the fundamental limitation; you cannot independently choose integration accuracy and strike grid density. Additionally, alpha requires model-specific tuning. Both issues are addressed by the methods that follow.</p><h3>Python Implementation</h3><p>We will be working with Heston in this article. Let&#8217;s first implement Monte Carlo pricing using Euler-Maruyama as a ground truth (note that Monte Carlo will give you slightly wrong values since we did not account for the discretisation, but it&#8217;s a method that is easy to implement and thus <em>almost</em> true, very likely) and for speed comparison:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a66992d3-e5ef-4985-b813-befeef818b05&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np
from scipy.stats import norm
import time
import matplotlib.pyplot as plt
from scipy.integrate import quad</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;c5d4d1d7-40cb-42f0-a021-1b34a9b301a1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def heston_mc(S0, K, r, q, T, kappa, theta, eta, rho, v0,
              n_steps=200, n_paths=1_000_000, seed=42):
    rng = np.random.default_rng(seed)
    dt  = T / n_steps
    S   = np.full(n_paths, float(S0))
    v   = np.full(n_paths, float(v0))

    for _ in range(n_steps):
        Z1    = rng.standard_normal(n_paths)
        Z2    = rho * Z1 + np.sqrt(1 - rho**2) * rng.standard_normal(n_paths)
        v_pos = np.maximum(v, 0)
        S    *= np.exp((r - q - 0.5 * v_pos) * dt + np.sqrt(v_pos * dt) * Z1)
        v    += kappa * (theta - v_pos) * dt + eta * np.sqrt(v_pos * dt) * Z2

    return np.exp(-r * T) * np.mean(np.maximum(S - K, 0))</code></pre></div><p>Next, we implement the Heston Characteristic Function:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;a4c5f2a6-2c36-4c5e-bde2-1f82221b8af8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def heston_cf(omega, S0, r, q, T, kappa, theta, eta, rho, v0):
    x  = np.log(S0)
    xi = kappa - 1j * rho * eta * omega
    d  = np.sqrt(xi**2 + eta**2 * (omega**2 + 1j * omega))
    g  = (xi - d) / (xi + d)

    C = ((r - q) * 1j * omega * T
         + (kappa * theta / eta**2) * (
             (xi - d) * T - 2 * np.log((1 - g * np.exp(-d * T)) / (1 - g))
         ))
    D = ((xi - d) / eta**2) * (1 - np.exp(-d * T)) / (1 - g * np.exp(-d * T))

    return np.exp(C + D * v0 + 1j * omega * x)</code></pre></div><p>and finally, the FFT implementation of Carr-Madan using Simpson weights:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;cb192ef7-db7c-4705-b8d0-4097d8086561&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def carr_madan_fft(S0, r, q, T, kappa, theta, eta, rho, v0,
                   alpha=1.5, N=4096, eta_grid=0.25):
    lam = 2 * np.pi / (N * eta_grid)
    b   = 0.5 * N * lam

    j   = np.arange(1, N + 1)
    v_j = eta_grid * (j - 1)

    cf  = lambda omega: heston_cf(omega, S0, r, q, T, kappa, theta, eta, rho, v0)

    psi = (np.exp(-r * T) * cf(v_j - (alpha + 1) * 1j)
           / (alpha**2 + alpha - v_j**2 + 1j * (2 * alpha + 1) * v_j))

    w   = (eta_grid / 3) * (3 + (-1)**j - (j == 1).astype(float))
    k_u = -b + lam * (np.arange(1, N + 1) - 1)

    x       = np.fft.fft(np.exp(1j * b * v_j) * psi * w)
    calls   = (np.exp(-alpha * k_u) / np.pi) * np.real(x)
    strikes = np.exp(k_u)

    return strikes, calls</code></pre></div><p>Let&#8217;s compare MC to Carr-Mada:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;195d5e69-4af9-4be8-89a2-e302adc71f73&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># &#9472;&#9472; parameters &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

params = dict(S0=100, r=0.05, q=0, T=1.0,
              kappa=2.0, theta=0.04, eta=0.3, rho=-0.7, v0=0.04)

strikes_to_price = np.arange(60, 141, 5, dtype=float)

# &#9472;&#9472; MC ground truth &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

t0 = time.perf_counter()
mc_prices = np.array([heston_mc(**params, K=K) for K in strikes_to_price])
mc_time   = time.perf_counter() - t0

# &#9472;&#9472; Carr-Madan &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

t0 = time.perf_counter()
cm_strikes, cm_calls = carr_madan_fft(**params)
cm_time = time.perf_counter() - t0
cm_prices = np.interp(strikes_to_price, cm_strikes, cm_calls)

# &#9472;&#9472; print results &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

print(f"{'Strike':&gt;8} {'MC':&gt;10} {'Carr-Madan':&gt;12} {'Error':&gt;10}")
print("-" * 44)
for K, mc, cm in zip(strikes_to_price, mc_prices, cm_prices):
    print(f"{K:&gt;8.0f} {mc:&gt;10.4f} {cm:&gt;12.4f} {abs(mc-cm):&gt;10.4f}")

print(f"\nMC time:          {mc_time:.2f}s  ({len(strikes_to_price)} strikes, 100k paths each)")
print(f"Carr-Madan time:  {cm_time*1000:.2f}ms (all strikes simultaneously)")

# &#9472;&#9472; plot &#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.plot(strikes_to_price, mc_prices, 'o', label='Monte Carlo', color='steelblue')
ax1.plot(strikes_to_price, cm_prices, '-', label='Carr-Madan', color='tomato', linewidth=2)
ax1.set_xlabel('Strike')
ax1.set_ylabel('Call Price')
ax1.set_title('Heston Call Prices')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.bar(['Monte Carlo\n(100k paths, 17 strikes)', 'Carr-Madan FFT\n(all strikes)'],
        [mc_time, cm_time],
        color=['steelblue', 'tomato'])
ax2.set_ylabel('Time (seconds)')
ax2.set_title('Computation Time')
ax2.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;5a9f0d3e-1532-4113-a0e6-9c1c66619ff0&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">  Strike         MC   Carr-Madan      Error
--------------------------------------------
      60    43.0926      43.0487     0.0439
      65    38.4447      38.4019     0.0428
      70    33.8703      33.8281     0.0423
      75    29.4033      29.3614     0.0420
      80    25.0861      25.0447     0.0415
      85    20.9701      20.9298     0.0403
      90    17.1172      17.0758     0.0415
      95    13.5838      13.5438     0.0401
     100    10.4286      10.3950     0.0336
     105     7.7081       7.6798     0.0283
     110     5.4500       5.4306     0.0194
     115     3.6676       3.6572     0.0104
     120     2.3451       2.3334     0.0117
     125     1.4146       1.4061     0.0085
     130     0.8051       0.8002     0.0049
     135     0.4343       0.4309     0.0034
     140     0.2238       0.2206     0.0031

MC time:          6.46s  (17 strikes, 100k paths each)
Carr-Madan time:  1.05ms (all strikes simultaneously)</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!ZkZV!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!ZkZV!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!ZkZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png" width="1189" height="390" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/eeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:390,&quot;width&quot;:1189,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:45447,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/200941555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!ZkZV!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!ZkZV!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Feeb4f2b6-52d3-4260-baf8-05767e585855_1189x390.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The results speak for themselves! Even at 100k paths, which isn&#8217;t all that much, and gives somewhat inaccurate results, running MC takes around 6.5 seconds, while Carr-Madan only takes a millisecond. But we can do even better!</p><div><hr></div><h1>Lewis</h1><p>Lewis (2001) reformulates option pricing as a complex contour integral. The result is a cleaner formula than Carr-Mada; no dampening parameter alpha to tune, and the strip condition is easily satisfied for essentially all models used in practice.</p><h3>The core idea</h3><p>The option price is a discounted expectation under Q (We are restating this since the paper has slightly different notation):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V(S_0) = e^{-rT}\\int_{-\\infty}^\\infty w(x) q_T(x)dx&quot;,&quot;id&quot;:&quot;OCTCTJPVDQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>where x = log S_T, w(x) is the payoff function, and q_T(x) is the risk-neutral density of log S_T. This is an inner product of w and q_T. Parseval&#8217;s theorem says inner products are preserved under Fourier transformation:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_{-\\infty}^\\infty w(x)\\overline{q_T(x)}dx = \\frac{1}{2\\pi}\\int_{-\\infty}^\\infty \\hat{w}(\\xi)\\overline{\\hat{q_T}(\\xi)}d\\xi&quot;,&quot;id&quot;:&quot;MGSDTRIGSP&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since q_T(x) is real, we have</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\overline{\\hat{q}_T(\\xi)} = \\hat{q}_T(-\\xi)=\\phi_T(-\\xi)&quot;,&quot;id&quot;:&quot;OXYMBWVFNT&quot;}" data-component-name="LatexBlockToDOM"></div><p>So:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V(S_0) = \\frac{e^{-rT}}{2\\pi}\\int_{-\\infty}^\\infty \\hat{w}(\\xi) \\phi_T(-\\xi)d\\xi&quot;,&quot;id&quot;:&quot;GHCWDMTPKJ&quot;}" data-component-name="LatexBlockToDOM"></div><h3>The problem</h3><p>That formula looks clean, but there&#8217;s a catch. For a call, the payoff is </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;w(x) = (e^x - K)^+,&quot;,&quot;id&quot;:&quot;HBFBTMAQEC&quot;}" data-component-name="LatexBlockToDOM"></div><p>which grows like e^x as x &#8594; infty. Its ordinary Fourier transform diverges!</p><p>Carr-Madan&#8217;s fix was to multiply the payoff by a dampening factor before transforming. Lewis takes a cleaner route: allow the transform variable to be complex.</p><h3>Generalized Fourier Transform</h3><p>Let z = u+iv be a complex number. Define the generalized Fourier transform of the payoff:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{w}(z) = \\int_{-\\infty}^\\infty e^{izx}w(x)dx&quot;,&quot;id&quot;:&quot;SYBKKUJWKU&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now e^{izx} = e^{iux-vx}, and the factor e^{-vx} provides the decay that kills the growth of w(x), as long as v is large enough. For the call:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{w}(z) = \\int_{\\ln K}^\\infty e^{izx}(e^x - K)dx&quot;,&quot;id&quot;:&quot;PQGUZQXMLF&quot;}" data-component-name="LatexBlockToDOM"></div><p>This does not converge unless Im(z) &gt; 1. With Im(z) &gt; 1 we have:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{w}(z) = -\\frac{K^{iz+1}}{z^2-iz}, \\quad \\Im(z)>1&quot;,&quot;id&quot;:&quot;AUMTGOBUAR&quot;}" data-component-name="LatexBlockToDOM"></div><p>We call this area the <strong>strip of regularity </strong>for the call payoff:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathcal{S}_w = \\{z:\\Im(z)>1\\}&quot;,&quot;id&quot;:&quot;WYILRLHIMI&quot;}" data-component-name="LatexBlockToDOM"></div><p>You can refer to the following table from the paper for other options:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!R0m2!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!R0m2!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 424w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 848w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 1272w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!R0m2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png" width="791" height="603" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f1620642-a7f6-4282-9626-ecdb41441984_791x603.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:603,&quot;width&quot;:791,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:111090,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/200941555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!R0m2!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 424w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 848w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 1272w, https://substackcdn.com/image/fetch/$s_!R0m2!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1620642-a7f6-4282-9626-ecdb41441984_791x603.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The characteristic function has its own regularity strip. For the characteristic function of X_T to exist, we need</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[e^{-vX_T}]<\\infty.&quot;,&quot;id&quot;:&quot;VUQIZMGIDA&quot;}" data-component-name="LatexBlockToDOM"></div><p>The set of valid complex v with this property forms the strip S_X. </p><p>Define the <strong>reflected strip</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathcal{S}_X^* = \\{z:-z\\in \\mathcal{S}_X\\}.&quot;,&quot;id&quot;:&quot;ZCRTHHDZPD&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is where phi_T(-z) is regular, which is what appears in the pricing formula.</p><h3>Shifting the contour</h3><p>By Cauchy&#8217;s theorem, the integration contour may be shifted to any horizontal line inside the <strong>common strip of regularity</strong>:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\int_{-\\infty}^\\infty \\hat{w}(\\xi)\\phi_T(-\\xi)d\\xi = \\int_{iv-\\infty}^{iv+\\infty} \\hat{w}(\\xi)\\phi_T(-\\xi)d\\xi, \\quad v \\in \\mathcal{S}_V = \\mathcal{S}_w \\cap \\mathcal{S}_X^*&quot;,&quot;id&quot;:&quot;UFCINKLFQP&quot;}" data-component-name="LatexBlockToDOM"></div><h3>The Lewis Formula</h3><p>The option price finally becomes:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V(S_0) = \\frac{e^{-rT}}{2\\pi}\\int_{iv-\\infty}^{iv+\\infty}e^{-izY}\\phi_T(-z)\\hat{w}(z)dz&quot;,&quot;id&quot;:&quot;NJFCFELLOC&quot;}" data-component-name="LatexBlockToDOM"></div><p>where v = Im(z) in S_V and Y = ln S_0 + (r-q)T is the log forward price, and phi denotes the characteristic function of the centred log-return X_T = log S_T - Y.</p><h3>Call Pricing Formula</h3><p>Substituting the call payoff transform hat{w} from above:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C(S_0) = \\frac{e^{-rT}}{2\\pi} \\int_{iv-\\infty}^{iv+\\infty} e^{-izY}\\phi_T(-z)\\cdot(-\\frac{K^{iz+1}}{z^2-iz})dz, \\quad v\\in(1,b)&quot;,&quot;id&quot;:&quot;EEGPHVVLDV&quot;}" data-component-name="LatexBlockToDOM"></div><p>where b is some model-dependent upper bound below which the characteristic function stays regular. Lewi&#8217;s preferred contour is v=1/2, obtained by shifting the contour from (1,b) down to (0,1) via the residue theorem and picking up the pole at z=i. That gives</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C(S_0) = S_0 e^{-qT}-\\frac{\\sqrt{S_0K}e^{-(r+q)T/2}}{\\pi} \\int_0^\\infty\\Re[e^{iuk}\\phi_T(u-\\frac{i}{2})]\\frac{du}{u^2+\\frac{1}{4}},&quot;,&quot;id&quot;:&quot;SLQGFZLAFW&quot;}" data-component-name="LatexBlockToDOM"></div><p>where k = ln(S_0/K) + (r-q)T is the log-moneyness. The contour v=1/2 sits symmetrically between the poles at z=0 and z=i, and requires only </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}^\\mathbb{Q}[S_T^{1/2}] < \\infty,&quot;,&quot;id&quot;:&quot;POPDPOKOXX&quot;}" data-component-name="LatexBlockToDOM"></div><p>satisfies by practically every standard model, and is a much weaker requirement than </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}^\\mathbb{Q}[S_T^{\\alpha+1}] < \\infty,&quot;,&quot;id&quot;:&quot;LKZJNRIBVV&quot;}" data-component-name="LatexBlockToDOM"></div><p>that Carr-Madan needs for alpha &gt; 0.</p><h3>Python Implementation</h3><p>Lewis uses the characteristic function of X_T:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5843724a-54d3-4880-9606-988cf7f593df&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def heston_cf_xt(omega, S0, r, q, T, kappa, theta, eta, rho, v0):
    """CF of X_T = log(S_T/S_0) - (r-q)T under Heston."""
    xi = kappa - 1j * rho * eta * omega
    d  = np.sqrt(xi**2 + eta**2 * (omega**2 + 1j * omega))
    g  = (xi - d) / (xi + d)

    C = (kappa * theta / eta**2) * (
            (xi - d) * T - 2 * np.log((1 - g * np.exp(-d * T)) / (1 - g))
        )
    D = ((xi - d) / eta**2) * (1 - np.exp(-d * T)) / (1 - g * np.exp(-d * T))

    # no (r-q) drift term since X_T is already centered
    return np.exp(C + D * v0 - 0.5 * 1j * omega * eta**2 * 0)</code></pre></div><p>Now since Lewis doesn&#8217;t use FFT by design, we have to compute strikes one by one using the contour integral, or we are smart and vectorize:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2c465e16-6775-499a-9414-a6d4bc8b6f50&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def lewis_calls_vectorized(S0, strikes, r, q, T, kappa, theta, eta, rho, v0,
                           N=4096, eta_grid=0.25):
    u_j = eta_grid * np.arange(0, N)
    
    # Simpson weights
    w = np.ones(N) * 2
    w[1::2] = 4
    w[0] = w[-1] = 1
    w *= eta_grid / 3

    phi = heston_cf_xt(u_j - 0.5j, S0, r, q, T, kappa, theta, eta, rho, v0)
    phi /= (u_j**2 + 0.25)

    # k for each strike: shape (n_strikes,)
    k = np.log(S0 / strikes) + (r - q) * T

    # outer product: shape (n_strikes, N)
    phase = np.exp(1j * np.outer(k, u_j))

    I = np.real(phase * phi[np.newaxis, :]) @ w / np.pi

    return S0 * np.exp(-q * T) - np.sqrt(S0 * strikes) * np.exp(-(r + q) * T / 2) * I</code></pre></div><p>And the comparison:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;0fa746e2-f90c-4d91-9beb-263da03eba3b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">t0 = time.perf_counter()
lw_prices = lewis_calls_vectorized(strikes=strikes_to_price, **params)
lw_time = time.perf_counter() - t0

print(f"{'Strike':&gt;8} {'MC':&gt;10} {'Carr-Madan':&gt;12} {'Lewis':&gt;10} "
      f"{'CM Err':&gt;10} {'LW Err':&gt;10}")
print("-" * 64)
for K, mc, cm, lw in zip(strikes_to_price, mc_prices, cm_prices, lw_prices):
    print(f"{K:&gt;8.0f} {mc:&gt;10.4f} {cm:&gt;12.4f} {lw:&gt;10.4f} "
          f"{abs(mc-cm):&gt;10.4f} {abs(mc-lw):&gt;10.4f}")

print(f"\nMC time:          {mc_time:.2f}s")
print(f"Carr-Madan time:  {cm_time*1000:.2f}ms")
print(f"Lewis time:       {lw_time*1000:.2f}ms")

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.plot(strikes_to_price, mc_prices, 'o', label='Monte Carlo', color='steelblue')
ax1.plot(strikes_to_price, cm_prices, '-', label='Carr-Madan', color='tomato', linewidth=2)
ax1.plot(strikes_to_price, lw_prices, '--', label='Lewis', color='seagreen', linewidth=2)
ax1.set_xlabel('Strike')
ax1.set_ylabel('Call Price')
ax1.set_title('Heston Call Prices')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.bar(['Carr-Madan\nFFT', 'Lewis\nVectorized'],
        [cm_time, lw_time],
        color=['tomato', 'seagreen'])
ax2.set_ylabel('Time (seconds)')
ax2.set_title('Computation Time (excl. Monte Carlo)')
ax2.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;08813a5c-958c-48c2-b4e9-0c9bce873e9d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">  Strike         MC   Carr-Madan      Lewis     CM Err     LW Err
----------------------------------------------------------------
      60    43.0926      43.0487    43.1459     0.0439     0.0533
      65    38.4447      38.4019    38.5020     0.0428     0.0573
      70    33.8703      33.8281    33.9311     0.0423     0.0608
      75    29.4033      29.3614    29.4673     0.0420     0.0640
      80    25.0861      25.0447    25.1536     0.0415     0.0674
      85    20.9701      20.9298    21.0417     0.0403     0.0716
      90    17.1172      17.0758    17.1902     0.0415     0.0730
      95    13.5838      13.5438    13.6612     0.0401     0.0774
     100    10.4286      10.3950    10.5150     0.0336     0.0864
     105     7.7081       7.6798     7.8025     0.0283     0.0944
     110     5.4500       5.4306     5.5570     0.0194     0.1070
     115     3.6676       3.6572     3.7858     0.0104     0.1182
     120     2.3451       2.3334     2.4652     0.0117     0.1201
     125     1.4146       1.4061     1.5412     0.0085     0.1266
     130     0.8051       0.8002     0.9381     0.0049     0.1329
     135     0.4343       0.4309     0.5717     0.0034     0.1375
     140     0.2238       0.2206     0.3646     0.0031     0.1409

MC time:          8.09s
Carr-Madan time:  1.18ms
Lewis time:       2.49ms</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!lyUP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!lyUP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!lyUP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png" width="1189" height="390" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f1284676-9619-4d5f-8883-7cc73998e485_1189x390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:390,&quot;width&quot;:1189,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:50854,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/200941555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!lyUP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!lyUP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff1284676-9619-4d5f-8883-7cc73998e485_1189x390.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>As you can see, when using the same N and eta as with Carr-Madan, Lewis starts diverging pretty quickly and is still over twice as slow. If we increase N and reduce eta, we are quickly multiple times slower than Carr-Madan when pricing many strikes at once. On the other hand, we get rid of the dampening parameter alpha, which can cause real problems.</p><p>Let&#8217;s finally look at our method of choice, which performs better than both Lewis and Carr-Madan.</p><div><hr></div><h1>The COS Method</h1><p>Both Carr-Madan and Lewis price options by evaluating a Fourier integral numerically. The integrand is oscillatory, which is expensive to integrate accurately with equally spaced quadrature.</p><p>The COS method (2008) takes a different route. Instead of integrating the characteristic function directly, it expands the risk-neutral density in a cosine series and reads the series coefficients directly off the characteristic function, with those coefficients often being available analytically.</p><h3>Fourier-Cosine Series Expansion</h3><p>Starting from risk-neutral pricing (once again, slightly different notation!):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;v(x, 0) = e^{-rT}\\int_{-\\infty}^\\infty v(y,T)f(y|x)dy&quot;,&quot;id&quot;:&quot;CLWHVYRKUS&quot;}" data-component-name="LatexBlockToDOM"></div><p>where x=ln(S_0/K), y = ln(S_T/K), v(y,T) is the payoff, and f(y|x) is the risk-neutral transition density of the log-return.</p><p>We truncate to a finite interval [a,b]:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;v(x, 0) \\approx e^{-rT}\\int_{a}^b v(y,T)f(y|x)dy&quot;,&quot;id&quot;:&quot;ACLKDNKSRH&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now expand f(y|x) in a cosine series on [a,b]:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;f(y|x) \\approx \\sum_{k=0}^{N-1}{}' A_k(x) \\cos (k\\pi \\frac{y-a}{b-a})&quot;,&quot;id&quot;:&quot;PFKUCGLBAR&quot;}" data-component-name="LatexBlockToDOM"></div><p>where the &#8216; in the sum means the k=0 term is weighted by 1/2, and the cosine coefficients are:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;A_k(x) = \\frac{2}{b-a}\\int_a^b f(y|x) \\cos(k\\pi \\frac{y-a}{b-a})dy&quot;,&quot;id&quot;:&quot;TTGKZJROZC&quot;}" data-component-name="LatexBlockToDOM"></div><p>Substituting into the pricing integral and swapping sum and integral (which we can do thanks to Fubini):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;v(x,0) \\approx \\frac{b-a}{2}e^{-rT}\\sum_{k=0}^{N-1}{}'A_k(x)V_k&quot;,&quot;id&quot;:&quot;PRFWDTHYJE&quot;}" data-component-name="LatexBlockToDOM"></div><p>where:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V_k = \\frac{2}{b-a}\\int_a^bv(y,T)\\cos(k\\pi \\frac{y-a}{b-a})dy&quot;,&quot;id&quot;:&quot;EWGJYUFJWI&quot;}" data-component-name="LatexBlockToDOM"></div><p>Note that the V_k are the cosine series coefficients of v(y,T) in y. Thus we have transformed the product of two real functions, f(y|x) and v(y,T), to that of their Fourier-cosine series coefficients.</p><h3>Coefficients from the characteristic function</h3><p>The density f(y|x) is unknown, that&#8217;s the whole reason we use Fourier methods in the first place. Comparing the definition of A_k(x) with the characteristic function phi_T(w), one finds that:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;A_k(x) = \\frac{2}{b-a}\\Re[\\phi_1(\\frac{k\\pi}{b-a})\\exp(-i\\frac{k\\pi a}{b-a})]&quot;,&quot;id&quot;:&quot;FMEGWWNYZP&quot;}" data-component-name="LatexBlockToDOM"></div><p>where phi_1 is the characteristic function computed over the truncated range [a,b]. Since [a,b] was chosen to capture essentially all the density mass, phi_1 is approx phi and so:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;A_k(x) \\approx F_k = \\frac{2}{b-a}\\Re[\\phi_T(\\frac{k\\pi}{b-a})\\exp(-i\\frac{k\\pi a}{b-a})]&quot;,&quot;id&quot;:&quot;IJMPNUCBYW&quot;}" data-component-name="LatexBlockToDOM"></div><h3>The COS Formula</h3><p>Finally, replacing A_k by F_k in the formula for V(x, t_0), we obtain:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;v(x, 0) \\approx e^{-rT}\\sum_{k=0}^{N-1}{}'\\Re[\\phi_T(\\frac{k\\pi}{b-a})e^{-ik\\pi a / (b-a)}]V_k&quot;,&quot;id&quot;:&quot;UTQLWZJVAU&quot;}" data-component-name="LatexBlockToDOM"></div><p>This is the COS formula. </p><h3>Payoff coefficients for calls and puts</h3><p>For a call, v(y,T) = K(e^y-1)^+, the integration domain reduces to [0,b] since the payoff is zero for y &lt; 0. The coefficients are:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V_k^\\text{call} = \\frac{2}{b-a}K(\\chi_k(0,b)-\\psi_k(0,b))&quot;,&quot;id&quot;:&quot;DNZEKFDVOK&quot;}" data-component-name="LatexBlockToDOM"></div><p>where chi and psi have closed forms:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!UMVO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!UMVO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 424w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 848w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 1272w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!UMVO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png" width="698" height="307" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:307,&quot;width&quot;:698,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:42993,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/200941555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!UMVO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 424w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 848w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 1272w, https://substackcdn.com/image/fetch/$s_!UMVO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fef887e43-ad0f-49d1-8734-eb7c19f18398_698x307.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">I couldn&#8217;t be bothered typing this out in LaTeX</figcaption></figure></div><p>For a put the integration domain is [a,0], and</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;V_k^\\text{put} = \\frac{2}{b-a}K(-\\chi_k(a,0)+\\psi_k(a,0))&quot;,&quot;id&quot;:&quot;ZSONBSLYYR&quot;}" data-component-name="LatexBlockToDOM"></div><p>In practice, you can price puts or calls, and recover the other via put-call parity</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;C = P + S_0e^{-qT}-Ke^{-rT}.&quot;,&quot;id&quot;:&quot;NGKHMRMGXD&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Truncation Range</h3><p>The interval [a,b] must contain enough of the density without being so wide that the cosine approximation needs excessive terms to resolve the tails. Fang &amp; Oosterlee propose using cumulants of ln(S_T/K):</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;[a,b] = [c_1-L\\sqrt{c_2+\\sqrt{c_4}}, c_1+L\\sqrt{c_2+\\sqrt{c_4}}], \\quad L=10&quot;,&quot;id&quot;:&quot;XGYUFZMPWO&quot;}" data-component-name="LatexBlockToDOM"></div><p>where c_1, c_2, c_4 are the first, second and fourth cumulants of ln(S_T/K). Including c_4 matters for short maturities and fat-tailed processes where the density is sharply peaked. </p><p>You can additionally also include a c_6 term as well for extremely short maturities, but the sixth cumulant can be difficult to derive for many models. You can also go the other direction and only include the c_1 and c_2 terms.</p><h3>Benefits of the COS method</h3><p>For Carr-Madan and Lewis, accuracy is limited by how well a trapezoidal or Simpson sum approximates a slowly decaying oscillatory integral. The COS method avoids this entirely. For smooth densities like GBM, Heston, and many L&#233;vy processes at moderate maturities, the density is C^{infty} on [a,b] and the cosine coefficients decay exponentially, yielding exponential convergence.</p><p>The computational complexity is O(N) for a single strike. For a vector of strikes, the F_k are the same across all strikes, and the V_k differ only through the x-dependent phase; pricing M strikes simultaneously costs O(MN) via matrix-vector multiplication. </p><h3>Python Implementation</h3><p>We need the first and second cumulants of log(S_T/K) under Heston:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d4215513-af76-4e8b-8076-70a5d9f3f265&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def heston_cumulants(r, q, T, kappa, theta, eta, rho, v0):
    """First and second cumulants of log(S_T/K) under Heston."""
    c1 = (r - q) * T + (1 - np.exp(-kappa * T)) * (theta - v0) / (2 * kappa) - 0.5 * theta * T

    c2 = (1 / (8 * kappa**3)) * (
        eta * T * kappa * np.exp(-kappa * T) * (v0 - theta) * (8 * kappa * rho - 4 * eta)
        + kappa * rho * eta * (1 - np.exp(-kappa * T)) * (16 * theta - 8 * v0)
        + 2 * theta * kappa * T * (-4 * kappa * rho * eta + eta**2 + 4 * kappa**2)
        + eta**2 * ((theta - 2 * v0) * np.exp(-2 * kappa * T)
                    + theta * (6 * np.exp(-kappa * T) - 7) + 2 * v0)
        + 8 * kappa**2 * (v0 - theta) * (1 - np.exp(-kappa * T))
    )

    return c1, c2</code></pre></div><p>Fast vectorized implementation of the COS method:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;dee4a909-ad20-4743-aec4-964bc9dffbcc&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">def cos_calls(S0, strikes, r, q, T, kappa, theta, eta, rho, v0, N=128, L=12):
    c1, c2 = heston_cumulants(r, q, T, kappa, theta, eta, rho, v0)
    a = c1 - L * np.sqrt(abs(c2))
    b = c1 + L * np.sqrt(abs(c2))

    k     = np.arange(N)
    omega = k * np.pi / (b - a)

    def varphi(omega):
        xi = kappa - 1j * rho * eta * omega
        d  = np.sqrt(xi**2 + eta**2 * (omega**2 + 1j * omega))
        g  = (xi - d) / (xi + d)
        C  = ((r - q) * 1j * omega * T
              + (kappa * theta / eta**2) * (
                  (xi - d) * T - 2 * np.log((1 - g * np.exp(-d * T)) / (1 - g))
              ))
        D  = ((xi - d) / eta**2) * (1 - np.exp(-d * T)) / (1 - g * np.exp(-d * T))
        return np.exp(C + D * v0)

    def chi(c, d):
        kpi = np.where(k != 0, omega, 1.0)
        val = (np.cos(kpi * (d - a)) * np.exp(d)
             - np.cos(kpi * (c - a)) * np.exp(c)
             + kpi * np.sin(kpi * (d - a)) * np.exp(d)
             - kpi * np.sin(kpi * (c - a)) * np.exp(c)) / (1 + kpi**2)
        val[0] = np.exp(d) - np.exp(c)
        return val

    def psi(c, d):
        val      = np.empty(N)
        val[0]   = d - c
        val[1:]  = (b - a) / (k[1:] * np.pi) * (
                       np.sin(omega[1:] * (d - a)) - np.sin(omega[1:] * (c - a))
                   )
        return val

    # precompute strike-independent quantities
    basis    = varphi(omega) * np.exp(-1j * omega * a)  # vphi * e^{-i*omega*a}
    A        = np.real(basis)
    B        = np.imag(basis)
    Uk       = (2 / (b - a)) * (-chi(a, 0) + psi(a, 0))

    # cache constants
    disc     = np.exp(-r * T)
    fwd_S0   = S0 * np.exp(-q * T)
    disc_K   = np.exp(-r * T)

    # vectorize over strikes
    x        = np.log(S0 / strikes)                    # shape (M,)
    theta_mx = np.outer(x, omega)                      # shape (M, N)
    F        = A * np.cos(theta_mx) - B * np.sin(theta_mx)  # shape (M, N)
    F[:, 0] *= 0.5

    puts  = disc * strikes * (F @ Uk)
    calls = puts + fwd_S0 - strikes * disc_K
    return calls</code></pre></div><p>And last but not least, the comparison code:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;68675c02-8d02-45ef-9af8-88b823d74399&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">t0 = time.perf_counter()
cos_prices = cos_calls(strikes=strikes_to_price, **params)
cos_time = time.perf_counter() - t0

print(f"{'Strike':&gt;8} {'MC':&gt;10} {'Carr-Madan':&gt;12} {'Lewis':&gt;10} {'COS':&gt;10} "
      f"{'CM Err':&gt;10} {'LW Err':&gt;10} {'COS Err':&gt;10}")
print("-" * 84)
for K, mc, cm, lw, cos in zip(strikes_to_price, mc_prices, cm_prices, lw_prices, cos_prices):
    print(f"{K:&gt;8.0f} {mc:&gt;10.4f} {cm:&gt;12.4f} {lw:&gt;10.4f} {cos:&gt;10.4f} "
          f"{abs(mc-cm):&gt;10.4f} {abs(mc-lw):&gt;10.4f} {abs(mc-cos):&gt;10.4f}")

print(f"\nMC time:          {mc_time:.2f}s")
print(f"Carr-Madan time:  {cm_time*1000:.2f}ms")
print(f"Lewis time:       {lw_time*1000:.2f}ms")
print(f"COS time:         {cos_time*1000:.2f}ms")

fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))

ax1.plot(strikes_to_price, mc_prices, 'o', label='Monte Carlo', color='steelblue')
ax1.plot(strikes_to_price, cm_prices, '-', label='Carr-Madan', color='tomato', linewidth=2)
ax1.plot(strikes_to_price, lw_prices, '--', label='Lewis', color='seagreen', linewidth=2)
ax1.plot(strikes_to_price, cos_prices, ':', label='COS', color='purple', linewidth=2)
ax1.set_xlabel('Strike')
ax1.set_ylabel('Call Price')
ax1.set_title('Heston Call Prices')
ax1.legend()
ax1.grid(True, alpha=0.3)

ax2.bar(['Carr-Madan\nFFT', 'Lewis\nVectorized', 'COS'],
        [cm_time, lw_time, cos_time],
        color=['tomato', 'seagreen', 'purple'])
ax2.set_ylabel('Time (seconds)')
ax2.set_title('Computation Time (excl. Monte Carlo)')
ax2.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.show()</code></pre></div><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;214a9648-9ab2-49f3-8201-a2d418c1b1de&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">  Strike         MC   Carr-Madan      Lewis        COS     CM Err     LW Err    COS Err
------------------------------------------------------------------------------------
      60    43.0926      43.0487    43.1459    43.0487     0.0439     0.0533     0.0439
      65    38.4447      38.4019    38.5020    38.4019     0.0428     0.0573     0.0428
      70    33.8703      33.8281    33.9311    33.8280     0.0423     0.0608     0.0424
      75    29.4033      29.3614    29.4673    29.3612     0.0420     0.0640     0.0421
      80    25.0861      25.0447    25.1536    25.0446     0.0415     0.0674     0.0416
      85    20.9701      20.9298    21.0417    20.9297     0.0403     0.0716     0.0403
      90    17.1172      17.0758    17.1902    17.0753     0.0415     0.0730     0.0419
      95    13.5838      13.5438    13.6612    13.5434     0.0401     0.0774     0.0404
     100    10.4286      10.3950    10.5150    10.3942     0.0336     0.0864     0.0344
     105     7.7081       7.6798     7.8025     7.6788     0.0283     0.0944     0.0293
     110     5.4500       5.4306     5.5570     5.4303     0.0194     0.1070     0.0197
     115     3.6676       3.6572     3.7858     3.6562     0.0104     0.1182     0.0114
     120     2.3451       2.3334     2.4652     2.3326     0.0117     0.1201     0.0125
     125     1.4146       1.4061     1.5412     1.4057     0.0085     0.1266     0.0089
     130     0.8051       0.8002     0.9381     0.7996     0.0049     0.1329     0.0055
     135     0.4343       0.4309     0.5717     0.4303     0.0034     0.1375     0.0039
     140     0.2238       0.2206     0.3646     0.2203     0.0031     0.1409     0.0035

MC time:          8.09s
Carr-Madan time:  1.18ms
Lewis time:       2.49ms
COS time:         0.34ms</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1H6m!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1H6m!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1H6m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png" width="1189" height="390" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:390,&quot;width&quot;:1189,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:52582,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/200941555?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!1H6m!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 424w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 848w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 1272w, https://substackcdn.com/image/fetch/$s_!1H6m!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F4db5601f-c43b-4d49-9c77-24bb132dc734_1189x390.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Awesome! We are more than 3 times faster than Carr-Madan, while getting rid of annoying parameters and gaining exponential convergence!</p><p>The method naturally vectorizes into dense linear algebra operations, meaning you could still make this way way faster if you optimized it properly.</p><div><hr></div><h1>Conclusion</h1><p>Apologies for the delay since the last article! This one took a little longer than I expected since I had to verify all the math, and turn 3 papers into one article instead of the usual one paper.</p><p>I hope you enjoyed this rather math-heavy article anyway! I plan on covering more options content soon.</p><div><hr></div><h3>Other Articles You Would Enjoy</h3><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;3c0c515b-2295-454b-94e2-955f74f55fd4&quot;,&quot;caption&quot;:&quot;The volatility smile is of great importance to all options traders. Market makers use it to figure out where to quote and volatility traders use it to figure out where volatility will go.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;How to Trade the Volatility Smile&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2025-06-29T21:34:11.716Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!qS_y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F080572fd-2897-477b-8e1b-35abe0dc2808_817x690.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/how-to-trade-the-volatility-smile&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:166974509,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:37,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;601e844f-3c5d-4c3d-bdf7-ac8f385f710b&quot;,&quot;caption&quot;:&quot;The crypto options market has been growing in popularity for years now, yet it&#8217;s still an unexplored territory for the vast majority of crypto traders.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;An Introduction to Option Market Making&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2025-09-19T19:51:26.703Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!s2Nn!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd4a2ee61-3f69-4af2-a59b-fcad47566910_1590x1181.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/introduction-to-option-market-making&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:171482530,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:24,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;15c1bcca-1815-4f14-90f2-5be1d8189dcf&quot;,&quot;caption&quot;:&quot;In the previous article, we talked about how you can figure out the fair price of a polymarket bet using the options market of the underlying.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Pricing Illiquid Options Using Proxy Volatility Surfaces&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2025-11-24T15:22:01.576Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!4QV3!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdafc2860-f4ed-4f90-8d4f-2e445bc10721_1189x790.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/pricing-illiquid-options-using-proxy&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:179804020,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:10,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><div><hr></div><h3><strong>Quant Corner</strong></h3><p><strong>Join Quant Corner</strong>, a community that actually <em>does things</em>. Tournaments, live Q&amp;As, open discussions, and the best place to shape what gets built next.</p><p>Join here: <a href="https://discord.gg/X7TsxKNbXg">https://discord.gg/X7TsxKNbXg</a></p>]]></content:encoded></item><item><title><![CDATA[Queue Position Estimation For Market Making]]></title><description><![CDATA[And testing it live]]></description><link>https://www.vertoxquant.com/p/queue-position-estimation</link><guid isPermaLink="false">https://www.vertoxquant.com/p/queue-position-estimation</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Fri, 29 May 2026 00:32:32 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8a330681-73ed-4b9d-9af0-4174b06d5a3f_1197x665.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Estimating queue position is one of those things everyone talks about, but there isn&#8217;t much good literature on it. Which makes sense, it&#8217;s mostly market maker knowledge that they&#8217;d want to keep to themself! </p><p>The problem is straightforward: When you post a limit order, you know your price and your size, but you don&#8217;t know where you actually sit in the queue and how much volume is ahead of you before you get filled. An order at the front of a 10 BTC queue has a completely different fill probability than one at the back of it.</p><p>There is one particular blog post by Rigtop (2013) that proposed splitting observed volume changes proportionally between ahead and behind. It works as a heuristic, but it has a problem: it treats cancellations as events where some volume gets cancelled ahead of you and some behind, but in reality, each cancellation is either fully ahead of you or fully behind you. This means it also only gives you a point estimate of where in the queue you are, with no measure of uncertainty.</p><p>We do something more powerful. We maintain a distribution over all possible queue positions using a particle filter, which gives us a whole distribution of where we likely are in the queue. </p><div class="native-video-embed" data-component-name="VideoPlaceholder" data-attrs="{&quot;mediaUploadId&quot;:&quot;79e71126-030d-4c83-aba9-7c776c5422e3&quot;,&quot;duration&quot;:null}"></div><p>We derive the framework from scratch, implement it live on Bybit, and show the particle distribution evolving live in real time.</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practised:<br><br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn</strong></h3><ul><li><p>Why market-by-price data makes queue position a latent variable, and why the naive proportional heuristic isn&#8217;t enough.</p></li><li><p>How to model queue position as a distribution rather than a point estimate, and why that matters for execution and market making.</p></li><li><p>How to derive a particle filter for queue position from scratch, handling trades, cancellations, and fills correctly.</p></li><li><p>How to implement it live with Bybit&#8217;s WebSocket feed in Python.</p></li><li><p>What the queue position distribution actually looks like in real life, and how it evolves as the market moves.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/queue-position-estimation">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[How to Build a Model That Adapts in Real Time]]></title><description><![CDATA[Why rolling retraining isn't enough, and what to do instead.]]></description><link>https://www.vertoxquant.com/p/how-to-build-a-model-that-adapts</link><guid isPermaLink="false">https://www.vertoxquant.com/p/how-to-build-a-model-that-adapts</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Fri, 22 May 2026 22:59:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!oGAt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F36f36af4-55ba-4ebc-af17-de674802d788_1389x1621.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the previous article, we began our online learning theory journey by introducing a model that can take multiple models&#8217; forecasts and spit out a combined forecast that is often superior to any given model and is mathematically guaranteed to perform similarly to the best model.</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;f651447e-58ea-4a76-b2c5-00969f02d0e5&quot;,&quot;caption&quot;:&quot;Imagine you have multiple models forecasting asset returns.&quot;,&quot;cta&quot;:null,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;md&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Optimally Combining Forecasts&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-05-19T22:51:10.194Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!kb4Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F232c763f-61b8-4d92-abb9-0509513b040e_1390x1190.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/optimally-combining-forecasts&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:198322476,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:5,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>The models we used to forecast returns were themself not &#8220;online&#8221; though. They were trained on a warm-up set and then kept frozen. The first improvement that comes to mind is simply retraining them periodically, but it turns out there is a more powerful method that lets you learn every single timestep and not miss out on crucial shifts.</p><p>That&#8217;s what we&#8217;ll be implementing in this article.</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3>What you&#8217;ll learn</h3><ul><li><p>Why rolling window retraining is fundamentally limited and what assumptions it silently makes about your data.</p></li><li><p>How AROWR works: a second-order Bayesian online regression algorithm that maintains a full posterior over model weights and updates every single timestep.</p></li><li><p>How ARCOR extends AROWR with a principled covariance reset mechanism that detects when the market has changed and restores the model's ability to adapt.</p></li><li><p>How to implement AROWR and ARCOR from scratch in Python and apply them to any regression problem that updates in real time.</p></li><li><p>How we applied ARCOR to BTC beta estimation across 355 crypto assets over 4 years, significantly improving performance over rolling OLS, especially during LUNA, FTX, and similar situations.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/how-to-build-a-model-that-adapts">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Optimally Combining Forecasts]]></title><description><![CDATA[Online Learning with provable performance guarantees]]></description><link>https://www.vertoxquant.com/p/optimally-combining-forecasts</link><guid isPermaLink="false">https://www.vertoxquant.com/p/optimally-combining-forecasts</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Tue, 19 May 2026 22:51:10 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!kb4Y!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F232c763f-61b8-4d92-abb9-0509513b040e_1390x1190.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine you have multiple models forecasting asset returns. </p><p>Maybe one is a linear autoregressive model that performs well in trending markets. Another is a mean-reversion model, and a third is a more complex machine learning model that captures nonlinear patterns. Each has its strengths and weaknesses, and most importantly, you don&#8217;t know in advance which one will be best tomorrow.</p><p>The naive approach is to pick the best model in-sample and deploy it. Another naive approach is equal weighting. But those two approaches ignore everything you learn as new data arrives.</p><p>What if there exists a principled way to combine your models that:</p><ul><li><p>Allocates a lot of weight to models that currently perform well and less weight to models that perform poorly.</p></li><li><p>Requires no retraining, no hyperparameter tuning, no rebalancing decisions.</p></li><li><p>Comes with a mathematical guarantee that you perform nearly as well as the best model.</p></li></ul><p>In this article, we present an algorithm that is able to achieve all of the above, demonstrate its behavior, and show how this model shines when there are different regimes where different models perform best.</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br><br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>Topics I write about include portfolio construction, market making, risk management, research methodology, and more.</p><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h3><strong>What you&#8217;ll learn:</strong></h3><ul><li><p>The theoretical framework of online learning and how it applies to forecast combination.</p></li><li><p>What regret is, why minimizing it is the right objective, and how it differs from standard in-sample loss minimization.</p></li><li><p>Why square loss has a special property called 2-mixability that enables a provably optimal combination algorithm.</p></li><li><p>How the aggregating forecaster works, why it is minimax optimal, and how to implement it from scratch in Python.</p></li><li><p>How to extend the algorithm with polynomial discounting to handle non-stationary markets with regime changes.</p></li><li><p>Why the optimal discount rate depends on regime persistence, and how to tune it empirically.</p></li></ul>
      <p>
          <a href="https://www.vertoxquant.com/p/optimally-combining-forecasts">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[The Effective Number of Tested Strategies]]></title><description><![CDATA[A correlation-aware correction for multiple testing in strategy research]]></description><link>https://www.vertoxquant.com/p/the-effective-number-of-tested-strategies</link><guid isPermaLink="false">https://www.vertoxquant.com/p/the-effective-number-of-tested-strategies</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Thu, 14 May 2026 22:47:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!TxFj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In one of my <a href="https://www.vertoxquant.com/p/backtests-lie">recent articles,</a> we looked at a paper that proposed a measure of how many strategies you effectively tested in-sample. I found the idea of such a measure really interesting and useful, so I went deeper into it, uncovered problems with existing measures, and ultimately came up with my own measure that has all the properties I desire from such a measure!</p><h4>What is the point of such a measure?</h4><p>Let&#8217;s say you are testing a moving-average crossover strategy, and you test 20 different combinations of moving-average lookbacks. Even for different parameter settings, the strategy returns will be correlated, so you didn&#8217;t <em>really</em> test 20 different strategies. </p><p>Let&#8217;s now dive into ways of measuring this and why it&#8217;s incredibly useful.</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br><br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>More broadly, I write about:</p><ul><li><p>Statistical and cross-sectional arbitrage</p></li><li><p>Managing multiple strategies and signals</p></li><li><p>Risk and capital allocation</p></li><li><p>Research tooling and methodology</p></li><li><p>In-depth model assumptions and derivations</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><p><strong>What you&#8217;ll learn:</strong></p><ul><li><p>Why the raw number of tested strategies dramatically overstates overfitting risk.</p></li><li><p>Why dependence geometry determines multiple-testing complexity.</p></li><li><p>How expected chi-squared maxima define a continuous effective number of tests.</p></li><li><p>How to compute K_eff numerically.</p></li></ul><div><hr></div><h1>The 5 Axioms</h1><p>If I think of a measure of how many strategies were effectively tested, I think of a few intuitive axioms that such a measure should follow:</p><ul><li><p><strong>1 &lt;= K_eff &lt;= K</strong>: You can&#8217;t test fewer than one strategy, and you can&#8217;t test more strategies than you&#8217;ve actually tested. </p></li><li><p><strong>K_eff = 1 iff all strategies are perfectly correlated/anticorrelated</strong>: If all your strategies are perfectly correlated/anticorrelated, then you&#8217;ve effectively only tested a single strategy.</p></li><li><p><strong>K_eff = K iff all strategies are uncorrelated</strong>: If all your strategies are perfectly uncorrelated (or independent, which is the same in the Gaussian world), then you can&#8217;t get more information; you really tested as many strategies as you got.</p></li><li><p><strong>K_eff is non-decreasing in K</strong>: If we test more strategies, our effective number of strategies that we tested can&#8217;t decrease; it should only increase, or stay constant if the new strategy is perfectly correlated/anticorrelated to a previously tested strategy.</p></li><li><p><strong>The expectancy of the maximum of the absolute Z-scores of your K strategies grows asymptotically like the square root of twice the logarithm of K_eff</strong>: Okay, this one may not be so intuitive&#8230; Let me explain! Let Z_i be the Z-score of strategy i. We assume the Z&#8217;s to be standard Gaussian random variables with covariance matrix Sigma. A standard result in extreme value theory is that:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1\\leq i \\leq K} |Z_i|] \\sim \\sqrt{2 \\log K}&quot;,&quot;id&quot;:&quot;YPMNRUUIRY&quot;}" data-component-name="LatexBlockToDOM"></div><p>We want K_eff to be defined so that your correlated portfolio of K strategies looks exactly like K_eff independent strategies in terms of expected best absolute Z-score.</p></li></ul><div><hr></div><h1>Why Popular Measures Fail</h1><p>I&#8217;m not the first one to come up with a notion of &#8220;effective number of strategies tested&#8221;, so let&#8217;s look at some of the existing definitions and where they ultimately break down.</p><h3>Spectral Participation Rate</h3><p>Spectral Participation Rate is the measure that was used in the following article:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;ad3ff4b7-4ce0-4681-a5c0-8335d93fbad1&quot;,&quot;caption&quot;:&quot;Look at this backtest I found online:&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Backtests Lie: Building a Stress-Test Framework for Trading Signals&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-04-22T21:57:16.195Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!NzDd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/backtests-lie&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:194942463,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:19,&quot;comment_count&quot;:8,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:true,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>It&#8217;s defined as follows:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = \\frac{(\\text{tr} \\ K)^2}{||\\Sigma||_F^2} = \\frac{(\\sum_{i=1}^K \\lambda_i)^2}{\\sum_{i=1}^K \\lambda_i^2} = \\frac{K^2}{||\\Sigma||^2_F}&quot;,&quot;id&quot;:&quot;OPGHWKCLED&quot;}" data-component-name="LatexBlockToDOM"></div><p>where &#955;_1, &#8230;, &#955;_K are the eigenvalues of Sigma, which is the correlation matrix of our Z-scores.</p><p>By construction, K_eff = K under independence and decreases toward 1 as configurations become collinear. And there you have your problem, K_eff decreases toward 1 as configurations become collinear. Let&#8217;s say you have strategies A and B, which are completely independent, so K_eff = 2. Now imagine you were to test strategy B over and over again. You would want K_eff to stay 2, but something else happens.</p><p>If you keep adding copies of strategy B, your correlation matrix becomes:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Sigma =\n\\begin{bmatrix}\n1 &amp; 0 &amp; 0 &amp; \\cdots \\\\\n0 &amp; 1 &amp; 1 &amp; \\cdots \\\\\n0 &amp; 1 &amp; 1 &amp; \\cdots \\\\\n\\vdots &amp; \\vdots &amp; \\vdots &amp; \\ddots\n\\end{bmatrix}&quot;,&quot;id&quot;:&quot;CGDWUQAUKU&quot;}" data-component-name="LatexBlockToDOM"></div><p>The eigenvalues of Sigma are:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\lambda_1 = K - 1, \\quad \\lambda_2 = 1, \\quad \\lambda_3 = ... = \\lambda_K = 0&quot;,&quot;id&quot;:&quot;YJQOWPPWCY&quot;}" data-component-name="LatexBlockToDOM"></div><p>So:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} \\frac{K^2}{\\sum_{i=1}^K \\lambda_i^2} = \\frac{K^2}{(K-1)^2 + 1}&quot;,&quot;id&quot;:&quot;GSQPAAVBNR&quot;}" data-component-name="LatexBlockToDOM"></div><p>And as K goes to infinity, K_eff converges towards 1! </p><p>While this is an extreme example, it happens in reality with adaptive search as well. In Bayesian optimization, you explore parameter areas that previously gave you good results more thoroughly, which in turn gives you a highly correlated strategy and causes K_eff to decrease.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!NzDd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!NzDd!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 424w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 848w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 1272w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!NzDd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png" width="790" height="490" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:490,&quot;width&quot;:790,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!NzDd!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 424w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 848w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 1272w, https://substackcdn.com/image/fetch/$s_!NzDd!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Axiom 4 is clearly violated, and thus, this is not usable for us.</p><h3>Effective Rank</h3><p>Effective rank is defined in <a href="https://www.eurasip.org/Proceedings/Eusipco/Eusipco2007/Papers/a5p-h05.pdf">Roy &amp; Vetterli (2007)</a>, and measures effective dimensionality by providing a real-valued extension to the rank of a matrix.</p><p>Consider the singular value decomposition (SVD) of Sigma:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Sigma = UDV&quot;,&quot;id&quot;:&quot;ISSJVYUAWX&quot;}" data-component-name="LatexBlockToDOM"></div><p>where U and V are unitary matrices, and D is a diagonal matrix containing the singular values</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\sigma_1 \\geq \\sigma_2 \\geq ... \\geq \\sigma_K \\geq 0&quot;,&quot;id&quot;:&quot;HYFTJVQTJS&quot;}" data-component-name="LatexBlockToDOM"></div><p>Let the singular value distribution be </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;p_k = \\frac{\\sigma_k}{||\\sigma||_1}, \\quad k=1,2,...,K&quot;,&quot;id&quot;:&quot;SAWFAAUNBB&quot;}" data-component-name="LatexBlockToDOM"></div><p>where sigma is the vector of singular values and ||.||_1 the l1-norm. </p><p>The effective rank of the matrix Sigma is defined as</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{erank}(\\Sigma) = \\exp( H(p_1, p_2, ..., p_K)),&quot;,&quot;id&quot;:&quot;SVRSYLBRNL&quot;}" data-component-name="LatexBlockToDOM"></div><p>where H is the Shannon entropy given by</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;H(p_1, p_2, ..., p_K) = - \\sum_{k=1}^K p_k \\log p_k&quot;,&quot;id&quot;:&quot;SSPKWLQUCQ&quot;}" data-component-name="LatexBlockToDOM"></div><p>We will use this effective rank as our K_eff.</p><p>Consider again the scenario of independent strategies A and B, and testing strategy B over and over again. Since Sigma is SPD, the eigenvalues and singular values are the same:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\sigma_1 = K-1, \\quad \\sigma_2 = 1, \\quad \\sigma_3 = ... = \\sigma_K = 0&quot;,&quot;id&quot;:&quot;KEHKJNJZMO&quot;}" data-component-name="LatexBlockToDOM"></div><p>The l1 norm is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;||\\sigma||_1 = K - 1 + 1 = K&quot;,&quot;id&quot;:&quot;KRMFVHNVEM&quot;}" data-component-name="LatexBlockToDOM"></div><p>And the singular value distribution is:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;p_1 = \\frac{K-1}{K}, \\quad p_2 = \\frac{1}{K}, \\quad p_3=...=p_K = 0&quot;,&quot;id&quot;:&quot;GMTZTGWGWP&quot;}" data-component-name="LatexBlockToDOM"></div><p>With this, erank(Sigma), or K_eff, becomes</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{erank}(\\Sigma) = \\exp(-\\frac{K-1}{K} \\log \\frac{K-1}{K} - \\frac{1}{K} \\log \\frac{1}{K}) \\to 1 \\quad \\text{as} \\ K \\to \\infty.&quot;,&quot;id&quot;:&quot;OAWXVIJCAI&quot;}" data-component-name="LatexBlockToDOM"></div><p>This suffers from the same problem as the spectral participation rate! The problem is that those two measures only look at the <em>shape</em> of the spectrum, not its <em>scale</em>. Adding more copies of B drowns out the contribution of A in the normalized distribution, pulling K_eff towards 1.</p><h3>Bailey &amp; L&#243;pez de Prado </h3><p>In the paper <a href="https://download.ssrn.com/19/05/30/ssrn_id2460551_code87814.pdf?response-content-disposition=inline&amp;X-Amz-Security-Token=IQoJb3JpZ2luX2VjEJn%2F%2F%2F%2F%2F%2F%2F%2F%2F%2FwEaCXVzLWVhc3QtMSJHMEUCIHWbneth2mI6u1rupjNkDmsC24r7YhYtKqwzBrCdrBu1AiEA0WU%2B4iCvwSVB5ONW7ALjmPrdNS6r5OrCqrozPtwM%2FQIqvQUIYRAEGgwzMDg0NzUzMDEyNTciDCm525KNaW%2F%2FvbobXiqaBbM%2F10wzVAYLOFWs%2FBdHXoSs6z5TXKBRa4bQHQy6BIHTYZVfS6SLO4RbFKq5vL90d4VompUznee4RwIw3OsdFEJI2pEHmfDfklheeL8yN1MV8f4DR%2FRVhgvD2%2BPW3PPDL5HVERXsgHm9zDsPg7kkNifeAJBJEq%2FAvLsXA5j0Rib7JO3LWuDD6p0hCGU80x9345MhtxoyeZqCp5xndHj9UPhQzihMHPCRTH3KF3BFCCCB9OU3NPk7LNRcKGFo79dmnBdYnwqG7vhmLKp79MSxpeCQ%2BwEHl%2BKWF5tSlsVUOcQ8BwpbXT%2BfV8JQ6AM1MWAwy8XBAQ26FyrzzyqRsMuztXukttYEqyoybKKfzlF3bsXFk9fwF%2BWIMXm7C7SGxjC6NlGGgOQlE%2BI3jrbtm3zWMhMOeZK8ECyHe693vpzCW3fBsDhgi4GV%2BZIdQQKd%2BCoUoEHYQazMy6wku2sj9gG8PkNwIuLSLLIkcLdzCE1cmk6xZlHSEGGHKPZSpw4vHsw5m4xKnZOiquQlJGyHeIvvrl9yIjLVWdfInTlvvIp%2BK5CBvgVeP4Cx%2FKTUL4NmDx%2FB8ho5hfl2So6vKLFCy6B56b1oVoDPcCV5tMnRMk8yjy591i84Ulf2C5zphuEs4IYOLbGuTI9eqnUD57ifgOxnb%2B305GtO0nv6giMHgywu5O6pPH6IYH6vdxLxOnbbU8pjOad10Q53Wb2jQ39%2B0MZhbaprdcfinrz2wBoZZC6CmWsZzsbj5apKFAkGVOXXrmB4tihcRCtGsDfkD5P%2BpYJeZX%2FjqWR4VyqN4tFHvtjmXKP6C%2BMzpt5aej2KSZBR7po%2BCFm4%2BxkUl4DcucaL3fMJN4bNj9SOCakqON8ObGeWgsStFK9c0eFuv2hSSzC355fQBjqxAQoG21%2Ba57fWqweZ7Kb8BR1WXwhe7bE20M0VSwGZL5HZdsLE0mio22kgEM5%2BxQbVyKClcKNBkL%2B5RuwOmF5jJsxmf4N09LHgKWodjBVIMD549%2BnebRGLp%2BxbQNtfg9kNKJO%2FzmJFGzyqU%2B07e0H7cvz%2Fl3mAsntZUcRyWCxYsOc0OS03J88Xj9%2FFrqVA3nWoCL9tftQpH6CjtMFLjBAoVp7UrXYJdpxhA49yGnCh5PyVEA%3D%3D&amp;X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Date=20260514T164544Z&amp;X-Amz-SignedHeaders=host&amp;X-Amz-Expires=300&amp;X-Amz-Credential=ASIAUPUUPRWES6RLE2DD%2F20260514%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Signature=83dcbd6f904a7443203ec090e39e9feccf55d87f98e12cc0830d61f80c432d91&amp;abstractId=2460551">"The Deflated Sharpe Ratio: Correcting for Selection Bias, Backtest Overfitting and Non-Normality"</a>, Bailey and L&#243;pez de Prado present another method of measuring the effective number of strategies tested.</p><p>They define</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = K(1 - \\hat{\\rho}(1-\\hat{\\rho})) + \\hat{\\rho},&quot;,&quot;id&quot;:&quot;IUCKHIGFOH&quot;}" data-component-name="LatexBlockToDOM"></div><p>where the equal-weighted average pairwise correlation is</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\hat{\\rho} = \\frac{\\sum_{i\\neq j}\\Sigma_{ij}}{K(K-1)}.&quot;,&quot;id&quot;:&quot;ZWBQRJXSSY&quot;}" data-component-name="LatexBlockToDOM"></div><p>Here is an example where this measure also breaks down:</p><p>Let A and B be two perfectly anti-correlated strategies. Then</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Sigma = \\begin{bmatrix}\n1 &amp; -1 \\\\\n-1 &amp; 1\n\\end{bmatrix}&quot;,&quot;id&quot;:&quot;AYTKIFYMIG&quot;}" data-component-name="LatexBlockToDOM"></div><p>and the average pairwise correlation is -1.</p><p>We get</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = 2(1 +(1+1)) -1 = 5.&quot;,&quot;id&quot;:&quot;JZSUKBKTYJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>With this definition, our effective number of strategies tested is 5, even though we only tested 2 strategies. It also violates a bunch of other axioms. </p><div><hr></div><h1>A Definition That Works</h1><p>Here is what I propose. Let M(x) be the expected maximum of x i.i.d. squared standard normal random variables:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M(x) = \\mathbb{E}[\\max_{1\\leq i \\leq x} \\tilde{Z}_i^2], \\quad \\tilde{Z}_1,...,\\tilde{Z}_x \\sim \\mathcal{N}(0,1)&quot;,&quot;id&quot;:&quot;AYIJCNVLSB&quot;}" data-component-name="LatexBlockToDOM"></div><p>Equivalently, since squaring a standard normal random variable yields a chi-squared random variable, this is the expected maximum of x i.i.d. chi-squared random variables. Using the PDF and CDF, we write the expected maximum as:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M(x) = \\int_0^\\infty t \\cdot x \\cdot f_{\\chi_1^2}(t) \\cdot F_{\\chi_1^2}(t)^{x-1}dt&quot;,&quot;id&quot;:&quot;JNAFUJAYFI&quot;}" data-component-name="LatexBlockToDOM"></div><p>M is strictly increasing and continuous, so it has a well-defined inverse</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M^{-1}: [0,\\infty) \\to [1, \\infty).&quot;,&quot;id&quot;:&quot;RGDYNAJOUT&quot;}" data-component-name="LatexBlockToDOM"></div><p>We then define the effective number of tested strategies as:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} := M^{-1}(\\mathbb{E}[\\max_{1\\leq i \\leq K}Z_i^2])&quot;,&quot;id&quot;:&quot;JASOIIYWAI&quot;}" data-component-name="LatexBlockToDOM"></div><p>where Z_1, &#8230;, Z_K are the actual strategy Z-scores with correlation matrix Sigma. In other words, K_eff is the number of independent strategies that would produce the same expected best squared Z-score as your actual correlated portfolio of K strategies.</p><p>Now, let&#8217;s prove that this definition actually follows the 5 axioms.</p><h3>Axiom 1</h3><p>Since M is strictly increasing, it suffices to show that</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M(1) \\leq \\mathbb{E}[\\max_{1\\leq i \\leq K} Z_i^2] \\leq M(K).&quot;,&quot;id&quot;:&quot;HOHMIDXDFI&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Lower bound:</strong></p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] \\geq \\mathbb{E}[Z_1^2] = 1 = M(1)&quot;,&quot;id&quot;:&quot;OCMYFQRGHQ&quot;}" data-component-name="LatexBlockToDOM"></div><p><strong>Upper bound:</strong></p><p>By &#352;id&#225;k&#8217;s inequality for centered Gaussian vectors,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;P(|Z_1| \\leq t, ..., |Z_K| \\leq t) \\geq \\prod_{i=1}^K P(|Z_i| \\leq t).&quot;,&quot;id&quot;:&quot;NHVTXLWKMJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since each Z_i is standard normal, we have</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\prod_{i=1}^K P(|Z_i| \\leq t) = P(\\max_{1 \\leq i \\leq K} |\\tilde{Z}_i|\\leq t)&quot;,&quot;id&quot;:&quot;MLXQNCLEOO&quot;}" data-component-name="LatexBlockToDOM"></div><p>and hence</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_{1 \\leq i \\leq K} Z_i^2 \\leq_{st} \\max_{1 \\leq i \\leq K} |\\tilde{Z}_i|,&quot;,&quot;id&quot;:&quot;CZFITWJFHM&quot;}" data-component-name="LatexBlockToDOM"></div><p>where st means stochastically dominated. And because x &#8594; x^2 is increasing,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_{1 \\leq i \\leq K} Z_i^2 \\leq_{st} \\max_{1 \\leq i \\leq K} \\tilde{Z}_i^2.&quot;,&quot;id&quot;:&quot;YJVRKXPEQG&quot;}" data-component-name="LatexBlockToDOM"></div><p>Taking expectations gives</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] \\leq \\mathbb{E}[\\max_{1 \\leq i \\leq K} \\tilde{Z}_i^2] = M(K).&quot;,&quot;id&quot;:&quot;JSVWMVMOWJ&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Axiom 2</h3><p>If all strategies are perfectly correlated or anticorrelated, then for every i,j,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;|\\Sigma_{ij}| = 1.&quot;,&quot;id&quot;:&quot;PZBWQXOPGD&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since each Z_i is standard Gaussian, this implies there exists a single standard normal random variable X and signs epsilon_i, either -1 or 1, such that</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_i = \\epsilon_i X \\quad \\forall i.&quot;,&quot;id&quot;:&quot;IASXXUCNUO&quot;}" data-component-name="LatexBlockToDOM"></div><p>Therefore,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_i^2 = X^2 \\quad \\forall i,&quot;,&quot;id&quot;:&quot;KQSCOMBDOB&quot;}" data-component-name="LatexBlockToDOM"></div><p>so</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_{1 \\leq i \\leq K} Z_i^2 = X^2.&quot;,&quot;id&quot;:&quot;HNFISJARZU&quot;}" data-component-name="LatexBlockToDOM"></div><p>Taking expectations,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = \\mathbb{E}[X^2] = M(1) = 1.&quot;,&quot;id&quot;:&quot;ZYMEGGOQRC&quot;}" data-component-name="LatexBlockToDOM"></div><p>Applying the inverse of M,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = M^{-1}(M(1)) = 1.&quot;,&quot;id&quot;:&quot;YQVRCVXFFN&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now the other direction. Assume K_eff = 1. Since M(1) = 1, </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = 1 \\iff \\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = 1.&quot;,&quot;id&quot;:&quot;IDPRLCQYTA&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since each Z_i is standard Gaussian, we have E[Z_i^2] = 1.</p><p>Since </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] \\geq \\mathbb{E}[Z_j^2] = 1,&quot;,&quot;id&quot;:&quot;JLMXTDWLRP&quot;}" data-component-name="LatexBlockToDOM"></div><p>the equality</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = 1&quot;,&quot;id&quot;:&quot;AANFFOICSY&quot;}" data-component-name="LatexBlockToDOM"></div><p>can occur only if</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_{1 \\leq i \\leq K} Z_i^2 = Z_j^2 \\quad \\text{a.s.} \\ \\forall j.&quot;,&quot;id&quot;:&quot;NZCTCHLRKJ&quot;}" data-component-name="LatexBlockToDOM"></div><p>Thus</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_1^2 = ... = Z_K^2 \\quad \\text{a.s.}&quot;,&quot;id&quot;:&quot;KCZAJFDLMI&quot;}" data-component-name="LatexBlockToDOM"></div><p>For jointly Gaussian variables, this implies</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Z_i = \\pm Z_j \\quad \\text{a.s.}&quot;,&quot;id&quot;:&quot;KPNYJDPLGI&quot;}" data-component-name="LatexBlockToDOM"></div><p>and therefore</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\text{Corr}(Z_i, Z_j) = \\pm 1.&quot;,&quot;id&quot;:&quot;OUUXZJNAIN&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Axiom 3</h3><p>If all strategies are uncorrelated, we have</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\Sigma = I_K,&quot;,&quot;id&quot;:&quot;WZLOXHABHI&quot;}" data-component-name="LatexBlockToDOM"></div><p>which implies that</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;(Z_1, ..., Z_K) \\overset{d}{=} (\\tilde{Z}_1, ..., \\tilde{Z}_K).&quot;,&quot;id&quot;:&quot;BNKVSSQYZD&quot;}" data-component-name="LatexBlockToDOM"></div><p>Therefore,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = \\mathbb{E}[\\max_{1 \\leq i \\leq K} \\tilde{Z}_i^2] = M(K).&quot;,&quot;id&quot;:&quot;ODDZHDUXOY&quot;}" data-component-name="LatexBlockToDOM"></div><p>Applying the inverse of M, we get</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff} = M^{-1}(M(K)) = K.&quot;,&quot;id&quot;:&quot;YDSGOCCMGK&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now the other direction. Assume K_eff = K. Since M is strictly increasing,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = M(K) = \\mathbb{E}[\\max_{1 \\leq i \\leq K} \\tilde{Z}_i^2].&quot;,&quot;id&quot;:&quot;OWXQOCRACX&quot;}" data-component-name="LatexBlockToDOM"></div><p>Now let </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;X := \\max_{1 \\leq i \\leq K} Z_i^2, \\quad Y := \\max_{1 \\leq i \\leq K} \\tilde{Z}_i^2.&quot;,&quot;id&quot;:&quot;STQJGRXHML&quot;}" data-component-name="LatexBlockToDOM"></div><p>From the proof of Axiom 1, we know that </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;X \\leq_{st} Y,&quot;,&quot;id&quot;:&quot;TQLUONNEZS&quot;}" data-component-name="LatexBlockToDOM"></div><p>i.e.</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;P(X > t) \\leq P(Y > t) \\quad \\forall t \\geq 0.&quot;,&quot;id&quot;:&quot;PULFEPLXYX&quot;}" data-component-name="LatexBlockToDOM"></div><p>Using the tail-integral formula,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[Y] - \\mathbb{E}[X] = \\int_0^\\infty (P(Y > t) - P(X > t))dt&quot;,&quot;id&quot;:&quot;SUETPCVNDC&quot;}" data-component-name="LatexBlockToDOM"></div><p>The integrand is nonnegative everywhere, and the left-hand side is zero since E[X] = E[Y]. Hence</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;P(X > t) = P(Y > t) \\quad \\forall t \\geq 0.&quot;,&quot;id&quot;:&quot;CQULXWILIM&quot;}" data-component-name="LatexBlockToDOM"></div><p>Thus, X and Y have the same distribution. Equivalently,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;P(|Z_1| \\leq t, ..., |Z_K| \\leq t) = \\prod_{i=1}^K P(|Z_i| \\leq t) \\quad \\forall t \\geq 0.&quot;,&quot;id&quot;:&quot;NWFPJRLMYW&quot;}" data-component-name="LatexBlockToDOM"></div><p>By the equality case of &#352;id&#225;k&#8217;s inequality, this implies that Z_1, &#8230;, Z_K are independent, which for Gaussian random variables is equivalent to being uncorrelated.</p><h3>Axiom 4:</h3><p>We have</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max(\\max_{1 \\leq i \\leq K}Z_i^2, Z_{K+1}^2) \\geq \\max_{1 \\leq i \\leq K} Z_i^2 \\quad \\text{a.s.}&quot;,&quot;id&quot;:&quot;ULHTTCGETR&quot;}" data-component-name="LatexBlockToDOM"></div><p>Taking expectation</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max(\\max_{1 \\leq i \\leq K}Z_i^2, Z_{K+1}^2)] \\geq \\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2],&quot;,&quot;id&quot;:&quot;ZJJFLAPMUT&quot;}" data-component-name="LatexBlockToDOM"></div><p>and applying the inverse of M, we get </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;K_\\text{eff}(K+1) \\geq K_\\text{eff}(K).&quot;,&quot;id&quot;:&quot;AUKSODZVHC&quot;}" data-component-name="LatexBlockToDOM"></div><h3>Axiom 5:</h3><p>Let</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;Y_k := \\max_{1 \\leq i \\leq k} |\\tilde{Z}_i|.&quot;,&quot;id&quot;:&quot;QFEGTTDNDT&quot;}" data-component-name="LatexBlockToDOM"></div><p>Then</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M(k) = \\mathbb{E}[Y_k^2].&quot;,&quot;id&quot;:&quot;JIZDEIQWWC&quot;}" data-component-name="LatexBlockToDOM"></div><p>A standard result from Gaussian extreme-value theory states that</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{Y_k}{\\sqrt{2 \\log k}} \\to 1 \\quad \\text{in probability}.&quot;,&quot;id&quot;:&quot;DENHZQGZJL&quot;}" data-component-name="LatexBlockToDOM"></div><p>Moreover, </p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{Y_k^2}{2 \\log k}&quot;,&quot;id&quot;:&quot;MKFYHDONDI&quot;}" data-component-name="LatexBlockToDOM"></div><p>is uniformly integrable. Hence, convergence in probability implies convergence of expectations:</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\frac{\\mathbb{E}[Y_k^2]}{2 \\log k} \\to 1.&quot;,&quot;id&quot;:&quot;VSIGVTSOYE&quot;}" data-component-name="LatexBlockToDOM"></div><p>Hence</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;M(k) = \\mathbb{E}[Y_k^2] \\sim 2 \\log k&quot;,&quot;id&quot;:&quot;AENIOOBGGZ&quot;}" data-component-name="LatexBlockToDOM"></div><p>Substituting k = K_eff yields</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} Z_i^2] = M(K_\\text{eff}) \\sim 2 \\log K_\\text{eff}.&quot;,&quot;id&quot;:&quot;ADFIPBQXDK&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\max_{1\\leq i \\leq K} Z_i^2 = (\\max_{1 \\leq i \\leq K}|Z_i|)^2,&quot;,&quot;id&quot;:&quot;WTVWACBJJO&quot;}" data-component-name="LatexBlockToDOM"></div><p>we obtain</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[(\\max_{1 \\leq i \\leq K} |Z_i|)^2] \\sim 2 \\log K_\\text{eff}.&quot;,&quot;id&quot;:&quot;FWDRKYWKBP&quot;}" data-component-name="LatexBlockToDOM"></div><p>Since Gaussian maxima concentrate sharply, the first and second moments are asymptotically equivalent up to square-root scaling. Therefore,</p><div class="latex-rendered" data-attrs="{&quot;persistentExpression&quot;:&quot;\\mathbb{E}[\\max_{1 \\leq i \\leq K} |Z_i|] \\sim \\sqrt{2 \\log K_\\text{eff}}.&quot;,&quot;id&quot;:&quot;RRYXOUMETZ&quot;}" data-component-name="LatexBlockToDOM"></div><div><hr></div><h1>Simulation</h1><p>Below is the code used for computing K_eff:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;189169c9-5192-4c6e-8b4f-3584a139ab1a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import numpy as np
from scipy.stats import chi2
from scipy.optimize import brentq


def M(x: float, n_quad: int = 4000) -&gt; float:
    """
    Compute M(x) = E[max of x i.i.d. chi^2_1 variables] via numerical
    quadrature, working in log space to avoid underflow.

    Parameters
    ----------
    x       : effective count (any real &gt;= 1)
    n_quad  : number of quadrature points

    Returns
    -------
    float : E[max_i Z_i^2] for x i.i.d. N(0,1) variables
    """
    # Adaptive upper limit: tail of chi^2_1 maximum decays beyond this
    hi = chi2.ppf(1 - 1e-10 / x, df=1) if x &gt; 1 else chi2.ppf(1 - 1e-8, df=1)
    t  = np.linspace(0, hi, n_quad + 1)[1:]  # avoid t=0 where pdf diverges

    log_F   = chi2.logcdf(t, df=1)
    log_f   = chi2.logpdf(t, df=1)
    log_ker = np.log(t) + np.log(x) + log_f + (x - 1) * log_F

    # Zero out terms that underflow (negligible contribution)
    mask       = log_ker &gt; -700
    integrand  = np.zeros_like(t)
    integrand[mask] = np.exp(log_ker[mask])

    return float(np.trapezoid(integrand, t))


def M_inv(val: float, tol: float = 1e-8) -&gt; float:
    """
    Compute M^{-1}(val) via Brent's method.

    Parameters
    ----------
    val : target value, must be &gt;= M(1) = 1
    tol : tolerance for root finding

    Returns
    -------
    float : x such that M(x) = val
    """
    if val &lt;= 1.0:
        return 1.0

    # Bracket: expand upper bound until M(hi) &gt; val
    hi = 2.0
    while M(hi) &lt; val:
        hi *= 2.0

    return brentq(lambda x: M(x) - val, 1.0, hi, xtol=tol)


def K_eff(Sigma: np.ndarray, n_sim: int = 200_000, seed: int = 42) -&gt; float:
    """
    Compute K_eff for a portfolio of K strategies with correlation matrix Sigma.

    Uses Monte Carlo to estimate E[max_i Z_i^2], then inverts M analytically.

    Parameters
    ----------
    Sigma : (K, K) correlation matrix
    n_sim : number of Monte Carlo samples
    seed  : random seed

    Returns
    -------
    float : K_eff in [1, K]
    """
    K   = Sigma.shape[0]
    rng = np.random.default_rng(seed)
    L   = np.linalg.cholesky(Sigma)
    Z   = (L @ rng.standard_normal((K, n_sim)))  # (K, n_sim)
    E_max_Z2 = float(np.mean(np.max(Z**2, axis=0)))
    return M_inv(E_max_Z2)</code></pre></div><p>And now let&#8217;s simulate how K_eff evolves as K grows for different families of strategies:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;8bda2bcd-10b6-4db6-93ff-a7cab552200d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">import matplotlib.pyplot as plt


# ============================================================
# Helper: build equicorrelation matrix
# ============================================================

def equicorr_matrix(K, rho):
    """
    Build K x K equicorrelation matrix with off-diagonal rho.
    """

    Sigma = np.full((K, K), rho, dtype=float)
    np.fill_diagonal(Sigma, 1.0)

    return Sigma


# ============================================================
# Sequential simulation
# ============================================================

def sequential_keff(mode, K_max=100):

    K_vals    = []
    Keff_vals = []

    # Persistent storage for sequential random strategies
    random_vectors = []

    rng = np.random.default_rng(123)

    for K in range(1, K_max + 1):

        # ----------------------------------------------------
        # Independent
        # ----------------------------------------------------
        if mode == "independent":

            Sigma = np.eye(K, dtype=float)

        # ----------------------------------------------------
        # Perfect dependence
        # ----------------------------------------------------
        elif mode == "dependent":

            Sigma = np.ones((K, K), dtype=float)

        # ----------------------------------------------------
        # rho = 0.5
        # ----------------------------------------------------
        elif mode == "rho_0.5":

            Sigma = equicorr_matrix(K, 0.5)

        # ----------------------------------------------------
        # Maximally negative equicorrelation
        # ----------------------------------------------------
        elif mode == "negative_boundary":

            if K == 1:
                rho = 0.0
            else:
                # PSD boundary:
                # rho &gt;= -1/(K-1)
                rho = -0.9 / (K - 1)

            Sigma = equicorr_matrix(K, rho)

        # ----------------------------------------------------
        # Sequential random correlated strategies
        # ----------------------------------------------------
        elif mode == "random":

            # Add ONE new latent-factor strategy
            v = rng.standard_normal(5)

            # Normalize
            v /= np.linalg.norm(v)

            random_vectors.append(v)

            X = np.stack(random_vectors)

            # Correlation matrix
            Sigma = X @ X.T

        else:
            raise ValueError(f"Unknown mode: {mode}")

        # ----------------------------------------------------
        # Numerical stabilization
        # ----------------------------------------------------

        Sigma = Sigma.astype(float)

        Sigma += 1e-10 * np.eye(K)

        # ----------------------------------------------------
        # Compute K_eff
        # ----------------------------------------------------

        keff = K_eff(Sigma)

        K_vals.append(K)
        Keff_vals.append(keff)

        print(
            f"{mode:20s} | "
            f"K = {K:3d} | "
            f"K_eff = {keff:8.3f}"
        )

    return np.array(K_vals), np.array(Keff_vals)


# ============================================================
# Run all simulations
# ============================================================

modes = [
    "independent",
    "dependent",
    "rho_0.5",
    "negative_boundary",
    "random"
]

results = {}

for mode in modes:

    K, Keff = sequential_keff(mode, K_max=100)

    results[mode] = (K, Keff)


# ============================================================
# Plot
# ============================================================

plt.figure(figsize=(12, 8))

for mode in modes:

    K, Keff = results[mode]

    plt.plot(K, Keff, linewidth=2, label=mode)

# Reference line
plt.plot(
    np.arange(1, 101),
    np.arange(1, 101),
    linestyle="--",
    linewidth=2,
    label="K_eff = K"
)

plt.xlabel("Raw Number of Tested Strategies (K)")
plt.ylabel("Effective Number of Strategies (K_eff)")

plt.title(
    "Sequential Growth of K_eff Under Different Correlation Structures"
)

plt.legend()
plt.grid(True)

plt.show()</code></pre></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!TxFj!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!TxFj!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 424w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 848w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 1272w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!TxFj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png" width="1005" height="701" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:701,&quot;width&quot;:1005,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:86377,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/197731389?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!TxFj!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 424w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 848w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 1272w, https://substackcdn.com/image/fetch/$s_!TxFj!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F8f2727c5-6948-4e5a-98ce-b6a7af94ee79_1005x701.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h1>Conclusion</h1><p>K_eff is not just theoretical; it has immediate practical applications.</p><p>The most direct one is improving the Deflated Sharpe Ratio presented by Bailey and De Prado. More broadly, K_eff can be used anywhere you need to correct for multiple testing across correlated strategies.</p><p>It can also directly guide you in determining when to stop searching while tuning hyperparameters.</p><p>I will explore those and many other things K_eff is capable of, and the math behind K_eff in more detail in future articles, which I then plan to all combine into a research paper!</p><p>Join Quant Corner: <a href="https://discord.gg/X7TsxKNbXg">https://discord.gg/X7TsxKNbXg</a></p><p>Quick reminder that I&#8217;m now open for consulting and short-term engagements. If your team needs help with anything in the quant space, like strategy research, portfolio construction, execution analysis, signal development, or just a second brain on a hard problem, I'm available. You can DM me on Discord or email me at vertoxquant@gmail.com.</p>]]></content:encoded></item><item><title><![CDATA[Going full-time on VertoxQuant]]></title><description><![CDATA[And I'm up for hire!]]></description><link>https://www.vertoxquant.com/p/going-full-time-on-vertoxquant</link><guid isPermaLink="false">https://www.vertoxquant.com/p/going-full-time-on-vertoxquant</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Mon, 11 May 2026 17:03:45 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Hey everyone,</p><p>I have some news to share. As of today, I'm going fully independent and dedicating all of my time to VertoxQuant. </p><p>For the past few years, I've been splitting my hours between my work as a quant researcher and the newsletter. That chapter is now closed, which means every hour I would have spent on industry work is now going straight into research, writing, and building things here. </p><p>In practice, that means more articles, deeper dives, and more time to explore the corners of quant finance I actually find interesting, not just the stuff that's immediately commercially useful. </p><p>For those newer here: my background is in quantitative research and systematic trading, with experience across strategy research, portfolio construction, execution analysis, and signal development. I also hold a degree in mathematics, though I tend to approach problems very practically while maintaining the right level of mathematical rigor. VertoxQuant started as a place to share ideas and research publicly, and it has grown into a community of people who care deeply about quantitative finance. </p><p>I'm also now open for consulting and short-term engagements. If your team needs help with anything in the quant space, like strategy research, portfolio construction, execution analysis, signal development, or just a second brain on a hard problem, I'm available. You can DM me on Discord or email me at vertoxquant@gmail.com.</p><p>I&#8217;m incredibly excited for what comes next. </p><p>-VertoxQuant</p>]]></content:encoded></item><item><title><![CDATA[Topological Risk Parity]]></title><description><![CDATA[Tree-Based Long/Short Portfolio Construction]]></description><link>https://www.vertoxquant.com/p/topological-risk-parity</link><guid isPermaLink="false">https://www.vertoxquant.com/p/topological-risk-parity</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Thu, 07 May 2026 10:18:28 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ox8k!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fd6d67bb9-0135-41b9-beba-4d38c255d804_2590x1990.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Portfolio construction is one of the most studied problems in quantitative finance, yet it&#8217;s still surprisingly hard to get it done in practice. Mean-variance optimization is notoriously unstable. We talk about how to make it stable here:</p><div class="digest-post-embed" data-attrs="{&quot;nodeId&quot;:&quot;42c92dbf-ba88-427c-aaaa-6e3a2ed3396a&quot;,&quot;caption&quot;:&quot;Introduction&quot;,&quot;cta&quot;:&quot;Read full story&quot;,&quot;showBylines&quot;:true,&quot;showDescription&quot;:true,&quot;showImage&quot;:true,&quot;size&quot;:&quot;sm&quot;,&quot;isEditorNode&quot;:true,&quot;title&quot;:&quot;Why Mean-Variance Optimization Breaks Down&quot;,&quot;publishedBylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100}],&quot;post_date&quot;:&quot;2026-02-03T23:01:54.857Z&quot;,&quot;cover_image&quot;:&quot;https://substackcdn.com/image/fetch/$s_!YSLy!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F89f1c92b-a219-4a6b-afc1-7a900bc204d7_925x675.png&quot;,&quot;cover_image_alt&quot;:null,&quot;canonical_url&quot;:&quot;https://www.vertoxquant.com/p/why-mean-variance-optimization-breaks&quot;,&quot;section_name&quot;:null,&quot;video_upload_id&quot;:null,&quot;id&quot;:186718680,&quot;type&quot;:&quot;newsletter&quot;,&quot;reaction_count&quot;:19,&quot;comment_count&quot;:0,&quot;publication_id&quot;:1726874,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;belowTheFold&quot;:false,&quot;youtube_url&quot;:null,&quot;show_links&quot;:null,&quot;feed_url&quot;:null}"></div><p>Rather than trying to fix a method, you can also go down the route of using specially designed portfolio optimization techniques that don&#8217;t suffer from instability as much. Hierarchical Risk Parity (HRP) is one of the most popular such techniques, replacing the covariance inverse with a recursive clustering procedure that is more robust by construction. But even HRP has its limitations; It&#8217;s designed to be long-only, and it takes no signal input whatsoever. It&#8217;s a pure risk allocation method. Any alpha you&#8217;ve worked so hard to generate is completely ignored by it.</p><p>This matters enormously for stat-arb and long-short strategies, where both the signal and the risk structure are crucial. What you want is a portfolio construction method that uses market structure to shape position sizes while preserving signal direction. That is exactly what Topological Risk Parity (TRP), introduced by Nayar, Ainasse, and Kulkarni (2026), is designed to do.</p><p>In this article, we implement TRP from scratch in Python on a crypto universe. We also implement the Semi-Supervised variant of TRP, which allows you to impose an economic prior on the hierarchy, like forcing BTC to be the market root and major L1s as the second layer. We then apply Random Matrix Theory (RMT) to further improve TRP across every performance metric!</p><div><hr></div><p><strong>What you&#8217;ll learn:</strong></p><ul><li><p>What Topological Risk Parity is and why it outperforms HRP for long-short strategies</p></li><li><p>How to build a Minimum Spanning Tree from a correlation matrix and what it reveals about market structure</p></li><li><p>How Random Matrix Theory separates genuine economic signals from statistical noise in correlation matrices</p></li><li><p>How to denoise a correlation matrix using Ledoit-Wolf shrinkage and hard thresholding</p></li><li><p>How to implement TRP from scratch in Python, including the Semi-Supervised variant with economic anchoring</p></li></ul><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br><br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>More broadly, I write about:</p><ul><li><p>Statistical and cross-sectional arbitrage</p></li><li><p>Managing multiple strategies and signals</p></li><li><p>Risk and capital allocation</p></li><li><p>Research tooling and methodology</p></li><li><p>In-depth model assumptions and derivations</p></li></ul><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h1>Mapping the Market with Correlation Networks</h1>
      <p>
          <a href="https://www.vertoxquant.com/p/topological-risk-parity">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Backtests Lie: Building a Stress-Test Framework for Trading Signals]]></title><description><![CDATA[Synthetic nulls, falsification audits, and backtest inflation diagnostics in Python.]]></description><link>https://www.vertoxquant.com/p/backtests-lie</link><guid isPermaLink="false">https://www.vertoxquant.com/p/backtests-lie</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Wed, 22 Apr 2026 21:57:16 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!NzDd!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6c5a0ac2-d023-4c25-a9e6-9b9450d9f31d_790x490.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Look at this backtest I found online:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!svQw!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!svQw!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 424w, https://substackcdn.com/image/fetch/$s_!svQw!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 848w, https://substackcdn.com/image/fetch/$s_!svQw!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 1272w, https://substackcdn.com/image/fetch/$s_!svQw!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!svQw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png" width="1456" height="646" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:646,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:90931,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.vertoxquant.com/i/194942463?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!svQw!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 424w, https://substackcdn.com/image/fetch/$s_!svQw!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 848w, https://substackcdn.com/image/fetch/$s_!svQw!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 1272w, https://substackcdn.com/image/fetch/$s_!svQw!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2047df2c-3000-48bc-9349-3f4214af42b7_1838x816.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>One of your first thoughts when looking at a stranger&#8217;s backtest is probably that it&#8217;s overfit, or that there is some look-ahead somewhere.</p><p>When you go a step further, you are probably constantly worried about overfitting your own backtests too!</p><p>In this article, we will introduce a framework that allows you to identify both! It&#8217;s a two-stage approach introduced in <a href="https://arxiv.org/pdf/2604.15531">D. Nikolopoulos (2026)</a>. We will introduce the method, develop the two-stage approach, test the methodology empirically on a crypto dataset, look at some limitations, and also add some improvements of my own!</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>More broadly, I write about:</p><ul><li><p>Statistical and cross-sectional arbitrage</p></li><li><p>Managing multiple strategies and signals</p></li><li><p>Risk and capital allocation</p></li><li><p>Research tooling and methodology</p></li><li><p>In-depth model assumptions and derivations</p></li></ul><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><div><hr></div><h1>How Backtests Lie</h1>
      <p>
          <a href="https://www.vertoxquant.com/p/backtests-lie">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[Strategy Decay Detection: Building a Warning System for Alpha Erosion]]></title><description><![CDATA[Knowing when to pull the plug]]></description><link>https://www.vertoxquant.com/p/strategy-decay-detection</link><guid isPermaLink="false">https://www.vertoxquant.com/p/strategy-decay-detection</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Tue, 14 Apr 2026 09:17:32 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!Yd5E!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa4f0da95-24ec-4bd1-9b6f-73a13887cf7a_1189x2390.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine the following: You spend weeks working on a strategy, the backtest looks great, a Sharpe of 1.5. You decide to go live, and it slowly and quietly stops working.<br>Did you overfit? Or maybe it&#8217;s alpha decay? </p><p>Everyone talks about alpha decay, but no one tells you how to actually quantify it. In this article, we&#8217;ll figure out how to do so and build a warning system that tells you when it&#8217;s time to pull the plug on a strategy.</p><p>We do this by implementing Minimum Regime Performance (MRP), a framework introduced by Alexander and Fabozzi (2026) that measures how a strategy holds up across structurally distinct market regimes. We will apply this method to a universe of crypto factors.</p><p>Beyond signal monitoring, MRP is also relevant in portfolio construction. Just as you would punish strategy tail risk like Value at Risk, you can also punish strategies that have a high tendency to decay. </p><p>The article will use the factors discussed in the following article:</p><div class="embedded-post-wrap" data-attrs="{&quot;id&quot;:185829391,&quot;url&quot;:&quot;https://www.vertoxquant.com/p/the-myth-of-factor-free-crypto&quot;,&quot;publication_id&quot;:1726874,&quot;embedding_publication_id&quot;:null,&quot;publication_name&quot;:&quot;VertoxQuant&quot;,&quot;publication_logo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;title&quot;:&quot;The Myth of Factor-Free Crypto&quot;,&quot;truncated_body_text&quot;:&quot;People keep telling you to take what works in equities and apply it to crypto.&quot;,&quot;date&quot;:&quot;2026-01-27T11:58:38.274Z&quot;,&quot;like_count&quot;:9,&quot;comment_count&quot;:0,&quot;bylines&quot;:[{&quot;id&quot;:128680675,&quot;name&quot;:&quot;Vertox&quot;,&quot;handle&quot;:&quot;vertox&quot;,&quot;previous_name&quot;:null,&quot;photo_url&quot;:&quot;https://substackcdn.com/image/fetch/$s_!HGUA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9bf3fd86-d46a-4caa-969a-d80059b72cb9_128x128.jpeg&quot;,&quot;bio&quot;:&quot;Quantitative Researcher in Digital Asset Markets | Market Making | Statistical Arbitrage | Options &quot;,&quot;profile_set_up_at&quot;:&quot;2023-06-12T09:38:30.926Z&quot;,&quot;reader_installed_at&quot;:&quot;2023-06-12T09:50:35.665Z&quot;,&quot;publicationUsers&quot;:[{&quot;id&quot;:1706583,&quot;user_id&quot;:128680675,&quot;publication_id&quot;:1726874,&quot;role&quot;:&quot;admin&quot;,&quot;public&quot;:true,&quot;is_primary&quot;:true,&quot;publication&quot;:{&quot;id&quot;:1726874,&quot;name&quot;:&quot;VertoxQuant&quot;,&quot;subdomain&quot;:&quot;vertox&quot;,&quot;custom_domain&quot;:&quot;www.vertoxquant.com&quot;,&quot;custom_domain_optional&quot;:false,&quot;hero_text&quot;:&quot;Applied quantitative research on trading, risk, and systematic strategy design.&quot;,&quot;logo_url&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png&quot;,&quot;author_id&quot;:128680675,&quot;primary_user_id&quot;:128680675,&quot;theme_var_background_pop&quot;:&quot;#9A6600&quot;,&quot;created_at&quot;:&quot;2023-06-12T09:38:54.325Z&quot;,&quot;email_from_name&quot;:null,&quot;copyright&quot;:&quot;Vertox&quot;,&quot;founding_plan_name&quot;:null,&quot;community_enabled&quot;:true,&quot;invite_only&quot;:false,&quot;payments_state&quot;:&quot;enabled&quot;,&quot;language&quot;:null,&quot;explicit&quot;:false,&quot;homepage_type&quot;:&quot;newspaper&quot;,&quot;is_personal_mode&quot;:false,&quot;logo_url_wide&quot;:null}}],&quot;is_guest&quot;:false,&quot;bestseller_tier&quot;:100,&quot;status&quot;:{&quot;bestsellerTier&quot;:100,&quot;subscriberTier&quot;:null,&quot;leaderboard&quot;:null,&quot;vip&quot;:false,&quot;badge&quot;:{&quot;type&quot;:&quot;bestseller&quot;,&quot;tier&quot;:100},&quot;paidPublicationIds&quot;:[],&quot;subscriber&quot;:null}}],&quot;utm_campaign&quot;:null,&quot;belowTheFold&quot;:false,&quot;type&quot;:&quot;newsletter&quot;,&quot;language&quot;:&quot;en&quot;,&quot;source&quot;:null}" data-component-name="EmbeddedPostToDOM"><a class="embedded-post" native="true" href="https://www.vertoxquant.com/p/the-myth-of-factor-free-crypto?utm_source=substack&amp;utm_campaign=post_embed&amp;utm_medium=web"><div class="embedded-post-header"><img class="embedded-post-publication-logo" src="https://substackcdn.com/image/fetch/$s_!ufaQ!,w_56,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png"><span class="embedded-post-publication-name">VertoxQuant</span></div><div class="embedded-post-title-wrapper"><div class="embedded-post-title">The Myth of Factor-Free Crypto</div></div><div class="embedded-post-body">People keep telling you to take what works in equities and apply it to crypto&#8230;</div><div class="embedded-post-cta-wrapper"><span class="embedded-post-cta">Read more</span></div><div class="embedded-post-meta">8 months ago &#183; 9 likes &#183; Vertox</div></a></div><p>Knowledge of regime switching models is NOT required.</p><div><hr></div><p>I write about quantitative trading the way it&#8217;s actually practiced:<br>Robust models and portfolios, combining signals and strategies, understanding the assumptions behind your models.</p><p>More broadly, I write about:</p><ul><li><p>Statistical and cross-sectional arbitrage</p></li><li><p>Managing multiple strategies and signals</p></li><li><p>Risk and capital allocation</p></li><li><p>Research tooling and methodology</p></li><li><p>In-depth model assumptions and derivations</p></li></ul><p>If this way of thinking resonates, you&#8217;ll probably like what I publish.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">VertoxQuant is a reader-supported publication. To receive new posts and support my work, consider becoming a free or paid subscriber.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>
      <p>
          <a href="https://www.vertoxquant.com/p/strategy-decay-detection">
              Read more
          </a>
      </p>
   ]]></content:encoded></item><item><title><![CDATA[If you share this, you get paid back]]></title><description><![CDATA[Referral rewards are now live]]></description><link>https://www.vertoxquant.com/p/if-you-share-this-you-get-paid-back</link><guid isPermaLink="false">https://www.vertoxquant.com/p/if-you-share-this-you-get-paid-back</guid><dc:creator><![CDATA[Vertox]]></dc:creator><pubDate>Wed, 08 Apr 2026 10:52:46 GMT</pubDate><enclosure url="https://substackcdn.com/image/fetch/$s_!ufaQ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fb77b39-424b-4665-b2a7-7db519ff9e11_128x128.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>The Substack grows almost entirely through word of mouth. So I&#8217;m rewarding people who share it: </p><ul><li><p>1 referral &#8594; 20% off</p></li><li><p>5 referrals &#8594; 1 month free</p></li><li><p>25 referrals &#8594; 6 months free</p></li></ul><p>If you&#8217;ve gotten value from it, help me grow by recommending it. It costs you nothing! </p><p>You can find your personal referral link at the bottom of this email or right here: </p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.vertoxquant.com/leaderboard&quot;,&quot;text&quot;:&quot;Leaderboard&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.vertoxquant.com/leaderboard"><span>Leaderboard</span></a></p><p>Thanks!</p><p>-Vertox</p>]]></content:encoded></item></channel></rss>