Credit Spreads vs Equity Cushion¶
© 2025 Marek Ozana
This notebook uses polars-bloomberg to study how bond spreads relate to a company’s equity cushion, (EV-NetDebt) / EV
We plot each issuer as a scatter point using average z-spread per year of duration versus equity cushion and highlight the typical “hockey-stick” pattern: spreads are fairly stable at healthy cushions, then widen rapidly when the cushion becomes thin.
Implementation detail: in Bloomberg BQL we map each bond to its issuer fundamentals via fundamentalTicker (mapby=LINEAGE), then group by ticker and rating to aggregate bond-level spread/duration and issuer-level fundamentals into one issuer datapoint.
Out[1]:
In [ ]:
Copied!
# Get average spread (spread / duration) and equity cusion for all tickers
from polars_bloomberg import BQuery
query = """
let(
#dur = avg(group(duration(duration_type=MODIFIED),
by=[ticker, bb_composite]));
#zspread = avg(group(spread(spread_type=Z),
by=[ticker, bb_composite]));
#ev = avg(group(value(curr_entp_val(fpt='BT', fill='PREV'),
fundamentalTicker,
mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#ndebt = avg(group(value(net_debt(fpt='BT', fill='PREV'),
fundamentalTicker,
mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#ndebt2ebitda = avg(group(
value(net_debt_to_ebitda(fpt='BT', fill='PREV'),
fundamentalTicker, mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#eq_cushion = dropna((#ev - #ndebt) / #ev);
#spread_per_year = dropna(#zspread / #dur);
)
get(#eq_cushion, #spread_per_year, #ndebt2ebitda)
for(members(['LG30TRUU Index']))
"""
with BQuery() as bq:
res = bq.bql(query)
df = res.combine().drop_nulls()
df.head(3)
# Get average spread (spread / duration) and equity cusion for all tickers
from polars_bloomberg import BQuery
query = """
let(
#dur = avg(group(duration(duration_type=MODIFIED),
by=[ticker, bb_composite]));
#zspread = avg(group(spread(spread_type=Z),
by=[ticker, bb_composite]));
#ev = avg(group(value(curr_entp_val(fpt='BT', fill='PREV'),
fundamentalTicker,
mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#ndebt = avg(group(value(net_debt(fpt='BT', fill='PREV'),
fundamentalTicker,
mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#ndebt2ebitda = avg(group(
value(net_debt_to_ebitda(fpt='BT', fill='PREV'),
fundamentalTicker, mapby=LINEAGE).value,
by=[ticker, bb_composite]));
#eq_cushion = dropna((#ev - #ndebt) / #ev);
#spread_per_year = dropna(#zspread / #dur);
)
get(#eq_cushion, #spread_per_year, #ndebt2ebitda)
for(members(['LG30TRUU Index']))
"""
with BQuery() as bq:
res = bq.bql(query)
df = res.combine().drop_nulls()
df.head(3)
Out[ ]:
shape: (3, 8)
| ID | #eq_cushion | ORIG_IDS | TICKER | BB_COMPOSITE | #spread_per_year | DATE | #ndebt2ebitda |
|---|---|---|---|---|---|---|---|
| str | f64 | str | str | str | f64 | date | f64 |
| "ABEGET:B-" | 0.332338 | "YJ291755 Corp" | "ABEGET" | "B-" | 756.857051 | 2025-12-19 | 5.402544 |
| "ACALTD:BB" | 0.785345 | "BM122057 Corp" | "ACALTD" | "BB" | -10837.804437 | 2025-12-19 | 1.572696 |
| "ACKAF:B" | 0.363201 | "ZI967285 Corp" | "ACKAF" | "B" | 130.281249 | 2025-12-19 | 4.563269 |