Three-bond butterfly trade using curve-fit residuals rather than raw yields.
Demeaning removes slow-moving cheapness/richness bias; DV01-neutral weights
make the spread a pure curvature signal.
Signal Construction
-- step 1: demeaned residual per bond --
raw_resid(bond, t) = market_YTM(t) - fitted_YTM(t) -- from ExpSpline
adj_resid(bond, t) = raw_resid(bond, t) - rolling_mean(raw_resid, 126d)
-- step 2: DV01-neutral fly spread --
w1, w2 = DV01-neutral weights using rolling_126d avg DV01
-- s.t. w1×DV01_w1 + w2×DV01_w2 = DV01_belly
fly_spread(t) = adj_resid(belly,t) - w1×adj_resid(wing1,t) - w2×adj_resid(wing2,t)
-- step 3: z-score entry signal --
z_fly(t) = (fly_spread(t) - mean(fly_spread, 126d))
/ std(fly_spread, 126d)
-- entry: |z_fly| > 2, confirmed by individual bond z-scores
PnL & Carry
PnL = -Δfly_spread × DV01_belly × notional
carry (v2) = (clean_price - forward_price) × 0.01 × 1e6 -- per £1mm face
-- forward_price funded at SONIA (overnight compounded)
Data Flow
gilt_curve_history.parquet
→
fitted_YTM per bond/date
→
residuals
→
z_fly signal
bond_analytics.py DV01
→
neutral weights w1, w2
UI (planned)
Bond selector: belly + two wings. Fly spread time series + z-score panel. Entry/exit markers. Cumulative PnL chart.
[ Fly spread + z-score chart — not yet built ]
Trades saved to localStorage (key: fly_trades).
Schema: { id, belly, wing1, wing2, w1, w2, entry_date, entry_z, notional, status }
parquet ✓
DV01 via bond_analytics ✓
fitted_YTM in cache ✓
residual series — needs API endpoint
carry calc — needs SONIA forward rate
Visual trade builder on the instantaneous forward curve. Bonds shown as
scatter dots; click to toggle long/short. Weights entered as notionals.
Net curve exposure visualised as a shaded area on the forward curve.
Interaction Model
Forward curve (ExpSpline flat-forwards) plotted for the selected date.
Each bond in the universe appears as a dot at its maturity with y = market YTM.
Click a dot: first click = long (green), second = short (red), third = deselect.
Notional input appears inline next to selected bonds.
Exposure & PnL
-- net DV01-weighted forward curve exposure --
exposure(tenor) = Σ sign_i × notional_i × DV01_i(tenor) -- shaded area
-- mark-to-market PnL --
PnL(t) = Σ w_i × (P_i(t) - P_i(entry)) × notional_i
-- P_i(t): clean price from parquet on date t
-- w_i: +1 long / -1 short
Data Flow
/api/curves (flat forwards)
→
Plotly forward curve
→
bond dots overlay
click interactions
→
position vector
→
exposure shading + daily PnL
UI (planned)
Date nav to step forward curve through history. Side panel: open positions,
entry prices, daily PnL, cumulative PnL. Export to CSV button.
[ Interactive forward curve + bond dots — not yet built ]
Trades saved to localStorage (key: fwd_trades).
Schema: { id, bonds:[{isin, side, notional, entry_px, entry_date}], status }
forward curve in cache ✓
clean prices in parquet ✓
DV01 via bond_analytics ✓
interactive Plotly click handlers — needs build
exposure shading layer — needs build
Compares each gilt's YTM to the SONIA futures strip implied rate at the
bond's mid-life tenor. Gives a credit/term premium spread above risk-free
compounded SONIA. Spread-of-spreads and roll-down identify relative value.
SONIA Mid-Rate Interpolation
-- find the two SR3 contracts straddling the bond's mid-tenor --
mid_tenor = (maturity - settle) / 2 -- years
T_near, T_far = SR3 contracts with expiry on either side of mid_tenor
w = (mid_tenor - T_near) / (T_far - T_near) -- linear weight
SONIA_mid(t) = (1-w) × (100 - SR3_near_settle(t))
+ w × (100 - SR3_far_settle(t))
-- bond/SONIA spread --
spread(bond, t) = bond_YTM(t) - SONIA_mid(t)
Roll-Down & Trade Signal
-- roll-down: spread change from tenor shortening by 1 day --
roll_down(bond) = spread(bond at mid_tenor) - spread(bond at mid_tenor - 1/365)
-- z-score entry signal (126d lookback) --
z_spread(bond, t) = (spread(t) - mean(spread, 126d)) / std(spread, 126d)
-- pair trade: wide vs tight spread --
-- long wide-spread bond / short SR3 pair
-- short tight-spread bond / long SR3 pair
Data Flow
SR3_settlements.csv
→
SONIA_mid per bond/date
→
spread series
→
z-score signal
yields_by_date
+
SONIA_mid
→
spread history
UI (planned)
Bond selector dropdown. Spread history chart (multi-line). Spread-of-spreads
heatmap. z-score bar chart across all bonds. Roll-down table.
[ Bond/SONIA spread chart — not yet built ]
Trades saved to localStorage (key: sonia_spread_trades).
Schema: { id, long_bond, short_bond, sr3_near, sr3_far, entry_date, entry_spread_diff, notional }
SR3_settlements.csv ✓
yields_by_date in cache ✓
mid-tenor SR3 interpolation — needs API endpoint
roll-down calc — needs build
Compares the short-end path implied by the fitted ExpSpline forward curve
against the path implied by the SR3 futures strip. Divergence between the
two is a signal that either the curve-fitting is distorted by supply/demand
in the short end, or the futures strip is mispricing the expected SONIA path.
Signal Construction
-- curve-implied path: read instantaneous forward rate at each SR3 expiry --
curve_fwd(T_i) = flat_curve.interpolate(T_i) -- from ExpSpline
-- futures-implied path: 100 - settle price --
futures_fwd(T_i) = 100 - SR3_settle(T_i)
-- divergence signal per expiry --
divergence(T_i, t)= curve_fwd(T_i, t) - futures_fwd(T_i, t)
-- aggregate signal: mean divergence across front 4 contracts --
signal(t) = mean(divergence(T_1..T_4, t))
z_signal(t) = (signal(t) - mean(signal, 63d)) / std(signal, 63d)
Interpretation
curve_fwd > futures_fwd (positive divergence):
gilt curve implies higher rates than futures — gilts rich relative to SONIA strip,
or futures strip too low (market pricing in more cuts than curve implies).
curve_fwd < futures_fwd (negative divergence):
curve implies lower short-end rates — gilts cheap to SONIA, or BOE pricing
in futures too hawkish versus bond market.
Data Flow
curves_by_date["flat"]
→
curve_fwd at SR3 expiries
→
divergence series
SR3_settlements.csv
→
futures_fwd at expiries
→
divergence series
divergence history
→
z_signal
→
entry/exit markers
UI (planned)
Dual-panel: top = forward curve overlaid with SR3 dots per date.
Bottom = divergence per contract through time (coloured by contract).
Aggregate z-score bar. Date navigation linked to top panel.
[ Curve vs futures divergence chart — not yet built ]
Dependency note: requires SR3_settlements.csv to be populated
daily via download_futures.py daily (Item 5 — ICE downloader).
GitHub Actions workflow futures_download.yml runs weekdays 18:00 UTC.
Backfill available via python download_futures.py backfill with ICE credentials.
forward curve in cache ✓
SR3_settlements.csv ✓
ICE daily downloader (Item 5) ✓
divergence API endpoint — needs build
dual-panel interactive chart — needs build