Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QST: Building SOFR discounting curve combining IRS and SOFR futures #662

Closed
Hyoung-Mook opened this issue Jan 28, 2025 · 4 comments
Closed

Comments

@Hyoung-Mook
Copy link

Hyoung-Mook commented Jan 28, 2025

Hi @attack68 ,

I'm trying to build SOFR discounting curve combining SOFR IRS and SOFR futures.
Ultimate goal is making step function that pricing FED actions.

My initial code is as below but seems not working properly.
Can you please share your idea on this. Thank you.

from rateslib.curves import Curve
from rateslib.solver import Solver
from rateslib.instruments import IRS
from rateslib.instruments import STIRFuture
from rateslib import dt
from rateslib import get_imm

nodes={
        
        dt(2025, 1, 28): 1.0,
        dt(2025, 2, 28): 1.0,  # 1M node
        get_imm(code="H25"): 1.0, # 2025 MAR IMM
        dt(2025, 4, 30): 1.0,  # 3M node
        dt(2025, 6, 30): 1.0,  # 6M node
        get_imm(code="M25"): 1.0, # 2025 June IMM        
    }


sofr = Curve(
    nodes=dict(sorted(nodes.items())),
    id="sofr",
)

instruments = [
    IRS(dt(2025, 1, 28), "1M", "A", curves="sofr"),
    IRS(dt(2025, 4, 30), "3M", "A", curves="sofr"),
    IRS(dt(2025, 6, 30), "6M", "A", curves="sofr"),
    STIRFuture(effective=get_imm(code="H25"), termination= get_imm(code="M25"), spec="usd_stir", curves="sofr")
]

rates = [4.31, 4.29, 4.23, 95.785]

solver = Solver(
    curves=[sofr],
    instruments=instruments,
    s=rates,
)

sofr.plot("1d", labels=["example sofr o/n curve"])
@attack68
Copy link
Owner

attack68 commented Jan 29, 2025

Theres a couple of things going on here.

  1. You are mixing prices and rates.

It would be better to convert the futures price to a rate (100-price). This will give you more consistency when examining sensitivities, using delta.

However, against this advice you can still use price if you want to, but you have to specifically inform the Solver to deviate from the default rate metric by adding arguments to the rate call. Change the STIRFuture instrument to a tuple with additional keyword arguments:

# BEFORE
STIRFuture(effective=get_imm(code="H25"), termination= get_imm(code="M25"), spec="usd_stir", curves="sofr")

# AFTER
(STIRFuture(effective=get_imm(code="H25"), termination= get_imm(code="M25"), spec="usd_stir", curves="sofr"), (), {"metric": "price"})
  1. You have defined an underspecified system.

Have a read of this thread and its other hyperlinked thread: Quant Stack

Essentially you have 6 discount factors on your curve, the first will always be 1.0 so that leaves 5 degrees of freedom and you have only 4 controlling instruments. The safest approach, if all of your instruments have unique maturities, is to use those maturities as curve nodes (this is what traditional bootstrappers have to do), and nothing else.

This will be a good starting point before you do more work to add-in or remove features to enhance the curve.

  1. Errors

You have not defined the effective date on the 3m and 6m swaps correctly, you have input their maturities instead.
Also you should use the spec argument to make sure you capture all the defaults of a SOFR IRS.

# REFORMED CODE

nodes={
        dt(2025, 1, 28): 1.0,
        dt(2025, 2, 28): 1.0,  # 1M node
        dt(2025, 4, 30): 1.0,  # 3M node
        dt(2025, 6, 30): 1.0,  # 6M node
        get_imm(code="M25"): 1.0, # 2025 June IMM  
    }


sofr = Curve(
    nodes=dict(sorted(nodes.items())),
    id="sofr",
)

instruments = [
    IRS(dt(2025, 1, 28), "1M", "A", spec="usd_irs", curves="sofr"),
    IRS(dt(2025, 1, 28), "3M", "A", spec="usd_irs", curves="sofr"),
    IRS(dt(2025, 1, 28), "6M", "A", spec="usd_irs", curves="sofr"),
    STIRFuture(effective=get_imm(code="H25"), termination= get_imm(code="M25"), spec="usd_stir", curves="sofr"),
]

rates = [4.31, 4.29, 4.23, 4.215]

solver = Solver(
    curves=[sofr],
    instruments=instruments,
    s=rates,
)

sofr.plot("1d", labels=["example sofr o/n curve"])

This will solve exactly.

Image

@Hyoung-Mook
Copy link
Author

@attack68 Thank you so much. Problem solved.

@Hyoung-Mook
Copy link
Author

Hello @attack68

I'm also trying to use 1month SOFR futures for front-end curve building. However, STIRFuture(effective=dt(2025, 7, 1), termination=dt(2025, 8, 1), spec="usd_stir1", curves="sofr") doesn't work while,

STIRFuture(effective=dt(2025, 7, 1), termination=get_imm(code="Z25"), spec="usd_stir1", curves="sofr") works.

Can you please have quick help on this? Thank you.

@attack68
Copy link
Owner

attack68 commented Jan 30, 2025

this was a bug in 1.6.0. The spec had an imm roll. you should set roll as "som". see #629.

Overload it with:

STIRFuture(effective=dt(2025, 7, 1), termination=dt(2025, 8, 1), spec="usd_stir1", roll="som", curves="sofr") 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants