This is my general approach for analysing repo specific bonds, or bonds that gone "special" on repo.
The Spot Price
Suppose firstly that we have 3 bonds, a 1y, 2y and 5y with different coupons and different YTMs currently. We can use this information to construct a bond curve that will value these bonds.
from rateslib import *
bond_curve = Curve( # create a curve defined by discount factors that we will solve
nodes={
dt(2023, 1, 1): 1.0,
dt(2024, 1, 1): 1.0,
dt(2025, 1, 1): 1.0,
dt(2028, 1, 1): 1.0,
},
)
b1y = FixedRateBond(dt(2023, 1, 1), "1Y", spec="ust", curves=bond_curve, fixed_rate=2.0)
b2y = FixedRateBond(dt(2023, 1, 1), "2Y", spec="ust", curves=bond_curve, fixed_rate=3.5)
b5y = FixedRateBond(dt(2023, 1, 1), "5Y", spec="ust", curves=bond_curve, fixed_rate=1.5)
Now we will solve the curve for YTM: 4.2%, 3.95% and 3.15%
solver = Solver(
curves=[bond_curve],
instruments=[
(b1y, (), {"metric": "ytm"}),
(b2y, (), {"metric": "ytm"}),
(b5y, (), {"metric": "ytm"}),
],
s=[4.2, 3.95, 3.15]
)
The bond_curve
now looks like this for overnight forwards:

We can use a curve or a price from ytm function to get the price of each bond:
b1y.rate(bond_curve) # 97.4295
b2y.rate(bond_curve) # 99.1448
b5y.rate(bond_curve) # 92.4295
Now suppose one of these bonds goes "special". We need to adjust the bond_curve
to reflect the new discounting regime for certain bonds cashflows.
We can create a curve just with the specialness. You can see the construction below (I use a cubic spline) and have plotted the forwards. In this case the specialness fades over the next 6 months initially worth about 1.5%.
specialness = Curve(
nodes={
dt(2023, 1, 1): 1.0,
dt(2023, 7, 1): 1.005,
dt(2023, 7, 2): 1.005,
dt(2028, 1, 1): 1.005,
},
t=[dt(2023, 1, 1), dt(2023, 1, 1), dt(2023, 1, 1), dt(2023, 1, 1),
dt(2023, 7, 1), dt(2023, 7, 2),
dt(2028, 1, 1), dt(2028, 1, 1), dt(2028, 1, 1), dt(2028, 1, 1)]
)
specialness.plot("1b")

OK now for the magic. We add the specialness
to the bond_curve
and reprice every bond:
composite = CompositeCurve([bond_curve, specialness])
composite.plot("1b")

b1y.rate(composite) # 98.3589
b2y.rate(composite) # 99.6301
b5y.rate(composite) # 92.8839
Notice how all bond prices have gone up to reflect the specialness.
You can quite easily make back of the envelope calculations to arrive at very similar prices by valuing the specialness and adding into the unadjusted bond price, but the analysis is here none-the-less to experiment.
The Forward Price
The interesting question is then what happens to the forward of the bond after the specialness?
In this example the "specialness" ceasaed as of 1st July 2023, i.e. after 6-months. The curves bond_curve and composite can be observed to converge to as of 1st July 2023.
If we calculate the forward prices as measured by each curve they are the same:
b5y.rate(composite, metric="fwd_clean_price", forward_settlement=dt(2023, 7, 1)) # 93.5867
b5y.rate(bond_curve, metric="fwd_clean_price", forward_settlement=dt(2023, 7, 1)) # 93.5868
It is also possible to observe that the forward prices derived from the adjusted spot according to the different repo rates are the same:
b5y.fwd_from_repo(92.8839, dt(2023, 1, 3), dt(2023, 7, 1), 3.131) # 93.5884
b5y.fwd_from_repo(92.429, dt(2023, 1, 3), dt(2023, 7, 1), 4.131) # 93.5861
So all is economically consistent.