S&P 500 Valuation Scenarios¶
© 2025 Jim Domeij & Marek Ozana
This chart shows potential S&P 500 price levels and returns for the next 12 months. It uses a scenario matrix driven by two factors: Forward P/E ratios (vertical axis, from historical averages) and Forward EPS growth (horizontal axis, centered on consensus). Each cell displays the resulting index price and percentage return from the current price. This helps investors visualize risk and potential upside under different market conditions.
Out[1]:
In [2]:
Copied!
# BQL Code to fetch SPX valuation data
import polars as pl
from polars_bloomberg import BQuery
query = """
let(
#last_price = px_last();
#12m_trail_eps = headline_eps_market(fpt=LTM);
#12m_fwd_eps = headline_eps_market(fpt=BT, fpo=1);
#epsg_consensus = #12m_fwd_eps/#12m_trail_eps - 1;
#current_pe = headline_pe_ratio(fpt=BT, fpo=1);
#5y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-5Y, 0D, frq=W)));
#10y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-10Y, 0D, frq=W)));
#20y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-20Y, 0D, frq=W)));
#35y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-35Y, 0D, frq=W)));
#max_pe = max(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-35Y, 0D, frq=W)));
)
get(
#last_price,
#12m_trail_eps,
#12m_fwd_eps,
#epsg_consensus,
#current_pe,
#5y_avg_pe,
#10y_avg_pe,
#20y_avg_pe,
#35y_avg_pe,
#max_pe
)
preferences(dropCols=['CURRENCY', 'DATE', 'AS_OF_DATE', 'REVISION_DATE', 'PERIOD_END_DATE'])
for('SPX Index')
""" # noqa: E501
with BQuery() as bq:
res = bq.bql(query)
df = res.combine()
df
# BQL Code to fetch SPX valuation data
import polars as pl
from polars_bloomberg import BQuery
query = """
let(
#last_price = px_last();
#12m_trail_eps = headline_eps_market(fpt=LTM);
#12m_fwd_eps = headline_eps_market(fpt=BT, fpo=1);
#epsg_consensus = #12m_fwd_eps/#12m_trail_eps - 1;
#current_pe = headline_pe_ratio(fpt=BT, fpo=1);
#5y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-5Y, 0D, frq=W)));
#10y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-10Y, 0D, frq=W)));
#20y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-20Y, 0D, frq=W)));
#35y_avg_pe = avg(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-35Y, 0D, frq=W)));
#max_pe = max(headline_pe_ratio(fpt=BT, fpo=1, fill=PREV, dates=range(-35Y, 0D, frq=W)));
)
get(
#last_price,
#12m_trail_eps,
#12m_fwd_eps,
#epsg_consensus,
#current_pe,
#5y_avg_pe,
#10y_avg_pe,
#20y_avg_pe,
#35y_avg_pe,
#max_pe
)
preferences(dropCols=['CURRENCY', 'DATE', 'AS_OF_DATE', 'REVISION_DATE', 'PERIOD_END_DATE'])
for('SPX Index')
""" # noqa: E501
with BQuery() as bq:
res = bq.bql(query)
df = res.combine()
df
Out[2]:
shape: (1, 11)
| ID | #last_price | #12m_trail_eps | #12m_fwd_eps | #epsg_consensus | #current_pe | #5y_avg_pe | #10y_avg_pe | #20y_avg_pe | #35y_avg_pe | #max_pe |
|---|---|---|---|---|---|---|---|---|---|---|
| str | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 | f64 |
| "SPX Index" | 6753.61 | 269.207986 | 307.417324 | 0.141932 | 22.120614 | 20.041525 | 18.825588 | 16.331631 | 16.771892 | 25.532804 |