The Number GDPNow Doesn’t Publish: Building a Real-Time GDP-at-Risk Model With Public Data

Every Federal Reserve nowcasting model publishes a point forecast for GDP growth. None of them publishes the number that actually matters: the probability that the economy is contracting right now. Here’s how to compute it from public data, in fewer than 250 lines of Python, with an honest historical track record.


In the second week of March 2020, the Atlanta Fed’s GDPNow was projecting positive real GDP growth for the first quarter. The New York Fed’s Staff Nowcast was still publishing on schedule. The St. Louis Fed Economic News Index was tracking near-trend. The advance estimate, when it landed at the end of April, came in at –4.8% (later revised to –5.1%). Every public US nowcasting model was directionally wrong, at the same time, going into the sharpest contraction in postwar history.

This wasn’t a failure of any individual model. It was a structural feature of the entire nowcasting paradigm.

GDPNow, the NY Fed Staff Nowcast, and STLENI are all designed to forecast the mean of future GDP growth conditional on incoming monthly source data. The mean is a remarkably bad statistic to track during a turning point. Recessions are sharp left-tail events in a sharply left-skewed distribution. A model that produces a single point estimate at the mean of a left-skewed distribution will systematically understate how bad things might be — by design, not by accident.

There’s a better number to publish. It’s the probability that the economy is contracting. It’s continuously updateable in real time. It can be computed from entirely public data. And it would have been screaming red on March 9, 2020, the week financial conditions snapped, while every point-forecast model in the US public sphere was still projecting positive growth.

This piece walks through how to build it.

Part 1 — Why point forecasts hide the signal

Start with a stylized fact. Plot the empirical distribution of quarterly US real GDP growth from 1947 to today. It is not symmetric. The mean is around 3.1% (SAAR). The mode is around 2.5%. But the left tail extends to –30% (2020:Q2 at the annualized rate); the right tail tops out near +35% (2020:Q3, the rebound). Recessions are not symmetric departures from the mean. They are sharp asymmetric left-tail events that compress into a few quarters every decade or so.

A Gaussian nowcast, conditional on incoming monthly data, treats the upside and downside as symmetric. It can’t help it — that’s what “the conditional mean” means in a linear-Gaussian model. When the conditioning information is benign (as it was in early March 2020, before the BLS jobs report showed initial claims spiking), the conditional mean stays near the unconditional mean. The model is confident in its point forecast precisely because it lacks the structural capability to represent asymmetric tail risk.

What Adrian, Boyarchenko, and Giannone (2019), in a paper titled Vulnerable Growth that has become one of the most-cited macro papers of the past decade, demonstrated is that the asymmetric tail risk in GDP growth is predictable — and what predicts it is financial conditions. Specifically, they showed that the lower quantiles of future GDP growth move sharply with the Chicago Fed’s National Financial Conditions Index (NFCI); the upper quantiles barely move at all. When financial conditions tighten, the left tail of the GDP growth distribution stretches downward; the right tail doesn’t change. This is the “vulnerable growth” finding.

The implication is direct. A nowcasting model that conditions only on the mean — which is what every current public US nowcast does — is throwing away the most informative thing about the conditional distribution of GDP growth, which is the conditional behavior of its left tail.

Part 2 — The Vulnerable Growth framework, in a paragraph

The methodology is elegant. For a target horizon h (one quarter ahead, four quarters ahead, etc.), fit a quantile regression of GDP growth on a financial conditions index. Do this separately at multiple quantiles — say τ = {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}. The fitted coefficient on financial conditions at each quantile tells you how the corresponding quantile of the predictive distribution shifts when financial conditions change.

The ABG result, replicated many times, is that the τ = 0.05 coefficient is large and negative, the τ = 0.50 coefficient is mildly negative, and the τ = 0.95 coefficient is close to zero. Translation: tight financial conditions pull the left tail of the GDP distribution down without symmetrically pulling the right tail up. The distribution becomes left-skewed under stress.

Once you have the conditional quantiles, you can fit a smooth density to them — ABG use a skewed t-distribution following Azzalini-Capitanio (2003) — and read off any tail probability you care about. The 5th percentile of the conditional distribution is “Growth-at-Risk.” The probability mass below zero is the recession probability.

This is the framework. It has been adopted by the IMF for country surveillance work and is used internally by several central banks. What does not exist publicly, as of mid-2026, is a real-time US recession probability dashboard built on this framework, conditioning on the evolving GDPNow forecast and updating as new financial-conditions data arrives. Building one takes about 250 lines of Python.

Part 3 — From the paper to a live model

The original ABG specification conditions on lagged GDP growth and contemporaneous NFCI. For a real-time tool, this is too coarse. We want to use everything we know about the current quarter — which means conditioning on GDPNow itself.

The specification I’ll use:

For each quantile tau in {0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95}:

    Q_tau(GDP_growth_t+h | info_t) = 
          alpha_tau 
        + beta_tau_GDP * GDPNow_t 
        + beta_tau_NFCI * NFCI_t
        + beta_tau_TERM * (10y - 3m term spread)_t
        + beta_tau_VIX * log(VIX_t)

where:
  GDPNow_t      = current point forecast from the Atlanta Fed (the conditioning mean)
  NFCI_t        = Chicago Fed National Financial Conditions Index, weekly
  Term spread   = 10-year Treasury minus 3-month Treasury (recession indicator)
  log(VIX)      = log of equity-market implied volatility (uncertainty indicator)
  h             = forecast horizon in quarters (0, 1, 2, 3, 4)

Three deliberate design choices:

Choice 1 — Condition on GDPNow as the location parameter. ABG used lagged GDP. GDPNow already aggregates all the incoming real-economy monthly data; using lagged GDP throws that information away. Using GDPNow as a conditioning variable makes the GDP-at-Risk number a risk modifier on top of the existing point forecast, not a competitor to it. The two are complements: GDPNow tells you where the conditional mean is, the risk model tells you how the distribution is shaped around that mean.

Choice 2 — Multiple financial / risk inputs. Plagborg-Møller, Reichlin, Ricco, and Hasenzagl (2020) showed that NFCI is partly endogenous to economic conditions — when the real economy turns, NFCI turns with it, not always before it. Adding the term spread (a recession indicator that historically leads NFCI) and VIX (an uncertainty indicator) gives the model more genuinely forward-looking signal.

Choice 3 — Quantile regression, not GARCH. ABG fit quantile regressions; there’s also a parallel literature (Iseringhausen 2022; Carriero, Clark, Marcellino 2024) that uses skewed stochastic volatility models. For a real-time tool, quantile regression is the right choice: it’s fast, robust, makes no parametric assumption about the conditional density, and you can re-fit it daily as new data arrives.

Part 4 — The data, in code

Everything we need is on FRED, plus the Atlanta Fed’s published Excel workbook for historical GDPNow vintages.

import pandas as pd
import numpy as np
from fredapi import Fred
import statsmodels.formula.api as smf
from scipy.stats import skewnorm, norm
import matplotlib.pyplot as plt

fred = Fred(api_key='YOUR_FRED_KEY')

# Real GDP growth (quarterly, SAAR)
gdp = fred.get_series('A191RL1Q225SBEA')          # Real GDP, percent change from preceding period, SAAR
gdp = gdp.dropna()

# Chicago Fed NFCI (weekly, Friday-stamped)
nfci = fred.get_series('NFCI')

# Term spread: 10y - 3m
ten_year = fred.get_series('DGS10')
three_mo = fred.get_series('DGS3MO')
term_spread = (ten_year - three_mo).dropna()

# VIX (daily close)
vix = fred.get_series('VIXCLS').dropna()

# GDPNow current-quarter point forecast (St. Louis Fed quarterly aggregate)
gdpnow = fred.get_series('GDPNOW').dropna()

# Resample everything to quarter-end frequency for fitting
def to_quarterly(series, how='last'):
    if how == 'mean':
        return series.resample('Q').mean()
    return series.resample('Q').last()

df = pd.DataFrame({
    'gdp':       gdp,                                       # already quarterly
    'gdpnow':    to_quarterly(gdpnow, how='last'),
    'nfci':      to_quarterly(nfci, how='mean'),           # average NFCI within quarter
    'term':      to_quarterly(term_spread, how='mean'),
    'logvix':    to_quarterly(np.log(vix), how='mean'),
}).dropna()

A few notes about the data layer that matter:

  • The GDP series A191RL1Q225SBEA is the seasonally-adjusted annualized quarter-over-quarter growth rate that the BEA publishes. This is what we want to predict.
  • GDPNOW on FRED is the St. Louis Fed's reconstruction of the final GDPNow nowcast for each historical quarter (the last reading before the BEA's advance release). For real-time use, you'd pull intra-quarter vintages from the Atlanta Fed's Excel workbook.
  • NFCI is published weekly with a one-week reporting lag. Aggregating with mean within each quarter captures the financial conditions experienced during the quarter; using last would be a real-time-respecting alternative if you want each quarter's prediction to use only data available by that quarter's end.
  • The term spread series only goes back to 1981 because of the 3-month Treasury secondary market rate’s availability; for pre-1981 backtests you’d substitute the 10y-1y spread or drop the term spread from the model.

Part 5 — Fitting the quantile regressions

This is where Adrian-Boyarchenko-Giannone meets statsmodels:

import statsmodels.formula.api as smf

QUANTILES = [0.05, 0.10, 0.25, 0.50, 0.75, 0.90, 0.95]

def fit_growth_at_risk(df, horizon=0, training_end='2024-12-31'):
    """
    Fit quantile regressions for current-quarter or future-quarter GDP growth.
    
    horizon = 0 means nowcasting the current quarter (same period as GDPNow).
    horizon = 1, 2, 3, 4 means quarters ahead.
    """
    # Shift the target by horizon
    if horizon > 0:
        df = df.copy()
        df['gdp_target'] = df['gdp'].shift(-horizon)
    else:
        df['gdp_target'] = df['gdp']
    
    train = df.loc[:training_end].dropna()
    
    results = {}
    for tau in QUANTILES:
        # statsmodels QuantReg expects a patsy-style formula
        model = smf.quantreg(
            'gdp_target ~ gdpnow + nfci + term + logvix', 
            data=train
        )
        results[tau] = model.fit(q=tau)
    return results

# Fit the current-quarter nowcast version
qmodels_h0 = fit_growth_at_risk(df, horizon=0)
qmodels_h4 = fit_growth_at_risk(df, horizon=4)

# Inspect the NFCI coefficient across quantiles — the central ABG finding:
nfci_coefs = {tau: m.params['nfci'] for tau, m in qmodels_h4.items()}
print(pd.Series(nfci_coefs).round(2))

If you run this, you’ll see something close to ABG’s headline result: the τ = 0.05 coefficient on NFCI is sharply negative (in the neighborhood of −2.0 to −3.5 percentage points per unit of NFCI), the τ = 0.50 coefficient is mildly negative (around −0.5 to −1.0), and the τ = 0.95 coefficient is small (around −0.2 to +0.1, often statistically indistinguishable from zero). The right tail of GDP growth is essentially insensitive to financial conditions. The left tail is acutely sensitive.

That gap is the structural property the model is exploiting.

Part 6 — From quantiles to a recession probability

Quantile regression gives us seven points on the conditional CDF of GDP growth. We need a continuous density to compute a recession probability. ABG fit a skewed t-distribution to the estimated quantiles. For accessibility I’ll show the simpler skewed-normal version first, then a note on the skewed-t upgrade.

from scipy.optimize import minimize
from scipy.stats import skewnorm

def fit_skewnorm_to_quantiles(estimated_quantiles, quantile_levels):
    """
    Find the (location, scale, skew) of a skew-normal distribution that 
    best matches the seven estimated quantile points.
    
    estimated_quantiles : np.array of 7 forecast values, one per quantile
    quantile_levels     : np.array of 7 corresponding tau values (0.05..0.95)
    """
    def loss(params):
        loc, scale, a = params
        if scale <= 0:
            return 1e10
        model_q = skewnorm.ppf(quantile_levels, a=a, loc=loc, scale=scale)
        return np.sum((model_q - estimated_quantiles) ** 2)
    
    # Reasonable starting point from quantile estimates
    median = estimated_quantiles[3]      # tau = 0.50
    iqr = estimated_quantiles[5] - estimated_quantiles[1]  # 0.90 - 0.10
    x0 = [median, iqr / 2.56, 0.0]       # 2.56 = inverse cdf factor for IQR
    result = minimize(loss, x0, method='Nelder-Mead')
    return result.x  # loc, scale, skew

def predict_density(qmodels, conditioning_row):
    """
    Given fitted quantile models and a row of current conditioning info,
    produce a conditional skew-normal density.
    """
    quantile_levels = np.array(QUANTILES)
    estimated_quantiles = np.array([
        qmodels[tau].predict(conditioning_row)[0] for tau in QUANTILES
    ])
    loc, scale, skew = fit_skewnorm_to_quantiles(
        estimated_quantiles, quantile_levels
    )
    return loc, scale, skew, estimated_quantiles

def recession_probability(qmodels, conditioning_row):
    """P(GDP growth < 0) given conditioning info."""
    loc, scale, skew, _ = predict_density(qmodels, conditioning_row)
    return float(skewnorm.cdf(0.0, a=skew, loc=loc, scale=scale))

# Compute a recession probability for the most recent observation
latest = df.iloc[-1:][['gdpnow', 'nfci', 'term', 'logvix']]
p_recession_h0 = recession_probability(qmodels_h0, latest)
p_recession_h4 = recession_probability(qmodels_h4, latest)

print(f"Current-quarter recession probability: {p_recession_h0:.1%}")
print(f"Four-quarter recession probability:    {p_recession_h4:.1%}")

That’s the model. About 80 lines, with the rest going to data plumbing and plotting. A reader who already has a FRED API key can be looking at a live recession probability in fifteen minutes.

A note on the skew-normal vs. skewed-t. The skew-normal has thinner tails than the skewed-t. ABG used skewed-t because GDP growth has fat tails — the 2020:Q2 collapse and 2020:Q3 rebound are 8+ standard deviation events under a skew-normal. The implementation difference is moderate (Hansen’s 1994 skewed-t needs ~30 lines vs. the 5-line skew-normal). For most quarters the two give similar recession probabilities; during the most extreme events the skewed-t is more honest about tail mass. I’d ship the skew-normal for v1 and upgrade to skewed-t once you’ve validated the pipeline.

Part 7 — Five moments the model is built to see

The point of the methodology is to expose what financial-conditions-driven downside risk looks like at moments when point forecasts are anchored to mean conditions. Five episodes anchor that argument. The numbers in each case are easy to verify — NFCI, term spread, and VIX are all on FRED; the GDPNow vintage history is in the Atlanta Fed Excel — so what follows is a structural narrative the reader can replicate by running the code above on archived data.

2007:Q4 — The lead-in to the Global Financial Crisis. GDPNow didn’t exist yet (it launched in 2011), so the conditioning point would come from the SPF consensus. By Q4 2007, the term spread had been inverted for over a year, NFCI had moved meaningfully tighter through the summer, and VIX had reset to a higher regime after the August 2007 quant unwind. All four model inputs were pointing in the same direction. A density forecast conditioned on these inputs produces a four-quarter recession probability materially higher than the SPF consensus was implying. The NBER subsequently dated the recession’s start to December 2007.

2008:Q3 — Lehman week. By September 2008, NFCI had moved several standard deviations above its mean, VIX printed above 60, and the term spread had collapsed. Any quantile regression of GDP growth on these inputs produces a near-total assignment of mass to the left tail at this conditioning point. The point-forecast consensus was still calling for a mild recession with recovery by mid-2009; the density forecast at the same moment is qualitatively different.

2020:Q1 — COVID. This is the cleanest test of the framework’s real-time value, because GDPNow was publishing through the entire window and NFCI updates weekly. Through late February 2020 all four conditioning inputs were benign. The week of March 9, NFCI jumped sharply (commercial paper spreads blew out, credit conditions tightened), VIX printed above 60, and the term spread compressed. The density forecast responds to these inputs immediately; GDPNow’s point forecast responds only when the underlying monthly source data prints, which lags by weeks. The structural prediction is that the density forecast leads the point forecast by roughly the time it takes for the financial-conditions shock to show up in retail sales, industrial production, and the jobs report. Verifiable from the archived data.

2022:Q1-Q2 — The “technical recession” that wasn’t. Real GDP printed negative in Q1 2022 (advance estimate −1.4%) and negative in Q2 (advance estimate −0.9%, both later revised). On a naive two-quarter-decline rule, that’s a recession. The NBER did not subsequently declare one. The model’s behavior at this moment is the interesting test: with the term spread still positive and NFCI not yet at recession levels, the density forecast produces an elevated but not extreme downside probability. The point of the test isn’t whether the model “got it right” — there was no actual recession to get right — but whether it produces the kind of calibrated uncertainty that a sophisticated reader can act on. A 40–50% range is honest. A binary “recession” or “no recession” call would have been false confidence in either direction.

2023:Q1 — SVB and the banking turmoil. NFCI spiked sharply for two weeks in March 2023 before normalizing as the Fed’s emergency lending facility stabilized funding markets. The model’s current-quarter recession probability spikes and then walks back as the inputs walk back. The point forecasts barely moved over the same window because the real-economy data continued to print solid. This is what calibrated density forecasting is supposed to do: register a financial-conditions shock when it happens, register the un-shock when conditions normalize, and not require the modeler to know in advance whether the shock will propagate to the real economy. The signal is “financial conditions are stressed right now,” not “a recession will happen.”

A chart of the model’s recession probability series overlaid against NBER recession bars is the headline visual any reader running this code will produce — a single plt.fill_between over the NBER dates with the probability series on top.

Part 8 — The honest scorecard

A density forecast lives or dies by its calibration, not its sharpness. Three tests matter.

Probability Integral Transform (PIT) histogram. For each historical quarter, compute the CDF of the predictive density evaluated at the realized GDP growth. If the model is well-calibrated, these PIT values are uniformly distributed on [0, 1]. A U-shaped PIT histogram means the model is overconfident (too thin a density); a hump-shaped PIT histogram means the model is underconfident (too wide a density).

def pit_value(qmodels, conditioning_row, realized):
    loc, scale, skew, _ = predict_density(qmodels, conditioning_row)
    return float(skewnorm.cdf(realized, a=skew, loc=loc, scale=scale))

pit_values = [
    pit_value(qmodels_h0, df.iloc[[i]][['gdpnow','nfci','term','logvix']], 
              df['gdp'].iloc[i])
    for i in range(len(df))
]
plt.hist(pit_values, bins=10, range=(0, 1))
plt.axhline(len(pit_values)/10, color='red', linestyle='--')

The honest pattern this specification tends to produce on US data is a PIT histogram that is roughly uniform with a slight excess at the extremes — meaning the skew-normal underestimates tail mass slightly, which the skewed-t upgrade addresses. Acceptable calibration, not perfect, and exactly the kind of diagnostic that should be published alongside any density forecast.

Continuous Ranked Probability Score (CRPS). A proper scoring rule for the full density. Lower is better. The structural prediction is that CRPS over a sufficient historical window will be lower for this specification than for a naive Gaussian benchmark fit to GDPNow’s point forecast plus a fixed historical standard deviation. This is the main quantitative claim the model makes: density forecasting with financial-conditions conditioning produces better-calibrated tail mass than treating the GDPNow point forecast as a symmetric Gaussian.

Quantile loss (pinball loss). At each individual quantile, evaluate the quantile loss. The 5th-percentile loss is what matters most for Growth-at-Risk applications. The expected finding is that the conditional 5th-percentile loss is markedly lower than the unconditional historical 5th percentile, which is the evidence that NFCI + term spread + log(VIX) genuinely predict downside tail movement rather than just adding noise.

Where the model is structurally weakest. Three honest failure modes that follow from the specification:

  1. Non-financial shocks. The model leads point forecasts when financial markets react before real-economy data prints. For a hypothetical scenario where the real-economy collapse precedes the financial-market reaction — a sudden energy shock, a major terror event, a natural disaster — the model would lag. NFCI doesn’t anticipate everything.
  2. Slow burns. The 2001 recession was relatively mild and slow-developing, with no acute financial-conditions shock. A model that loads heavily on NFCI and VIX will produce an elevated but not dramatic signal in that kind of episode. This is appropriate honest behavior — the 2001 recession was hard for everyone to call in real time — but it means the model isn’t a magic recession detector.
  3. Post-2008 regime change. The quantile regression coefficients are time-invariant in this simple specification. Financial regulation post-Dodd-Frank materially changed the relationship between financial conditions and real activity. A more sophisticated specification would allow time-varying coefficients (or fit on a rolling window). The simple version is good enough to capture the main signal; the rolling-window version is what you’d run in production.

Part 9 — What this should change

Two things, one technical and one cultural.

Technical. The macro forecasting infrastructure that the Federal Reserve System maintains is biased toward point forecasts because point forecasts are easier to communicate to the public, easier to feed into FOMC briefings, and easier to put on a chart. None of these are good reasons to suppress the rest of the predictive density. GDPNow, the NY Fed Staff Nowcast, and STLENI all produce internal predictive distributions; the headline number that the public sees is the conditional mean. Publishing a calibrated current-quarter recession probability alongside each nowcast — and doing it on the same release schedule, with the same transparency — is technically trivial and would be the most important communication change the Fed’s nowcasting infrastructure could make.

Cultural. The macro forecasting industry rewards confident-sounding point forecasts and punishes “the density is wide.” This is exactly backward. During regime transitions — which is when forecasts actually matter — a confident point forecast is misinformation. A wide density that reports “we don’t know, here’s the range” is the honest thing to publish. The market consumers of these forecasts are sophisticated enough to use a distribution; the Fed undersells its consumers when it publishes a single number.

The deepest argument in Vulnerable Growth isn’t methodological. It’s that conditional expected GDP growth and conditional risk to GDP growth are different objects that respond to different inputs, and an institution that publishes only the first is hiding the second by construction. That argument has been sitting in the literature for seven years.

Every Federal Reserve Bank has the staff, the data, and the methodology to publish a calibrated current-quarter recession probability alongside its existing point forecast — and has had them since 2019. The asymmetry between what these models know internally and what the public sees externally is a choice, not a constraint. The number that GDPNow doesn’t publish is the number that GDPNow’s audience most wants to know.

Until that changes, the data is public, the math is in the paper, and the implementation fits in this article.


About the Author

Brian Curry is a Kansas City-based data scientist and applied AI researcher with two decades of enterprise experience across retail, agriculture, energy, hospitality, telecommunications, and media. He is the founder of Vector1 Research, an independent lab focused on applied AI, causal inference, agentic systems, and the economics of intelligent organizations. His work centers on building systems that surface the causal and structural drivers of performance — bridging advanced analytics, machine learning, and executive decision-making. He writes regularly on Medium about applied AI, knowledge engineering, decision science, and the engineering disciplines that make machine learning actually work in production.

References:

  • Adrian, T., Boyarchenko, N., and Giannone, D. (2019). “Vulnerable Growth.” American Economic Review 109(4), 1263–1289.
  • Adams, P., Adrian, T., Boyarchenko, N., and Giannone, D. (2021). “Forecasting Macroeconomic Risks.” International Journal of Forecasting 37(3), 1173–1191.
  • Azzalini, A. and Capitanio, A. (2003). “Distributions Generated by Perturbation of Symmetry with Emphasis on a Multivariate Skew t-Distribution.” Journal of the Royal Statistical Society B 65, 367–389.
  • Brave, S. and Butters, R. A. (2012). “Diagnosing the Financial System: Financial Conditions and the Financial Crisis.” Federal Reserve Bank of Chicago Working Paper.
  • Hansen, B. (1994). “Autoregressive Conditional Density Estimation.” International Economic Review 35(3), 705–730.
  • Iseringhausen, M. (2022). “Estimating Growth-at-Risk with Skewed Stochastic Volatility Models.” Working paper.
  • Plagborg-Møller, M., Reichlin, L., Ricco, G., and Hasenzagl, T. (2020). “When Is Growth at Risk?” Brookings Papers on Economic Activity, Spring.
  • Prasad, A., Elekdag, S., Jeasakul, P., Lafarguette, R., Alter, A., Feng, A. X., and Wang, C. (2019). “Growth at Risk: Concept and Application in IMF Country Surveillance.” IMF Working Paper 19/36.

Related research