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

OISRateHelper uses index fixing calendar for date generation, should use paymentCalendar #1703

Open
trentmaetzold opened this issue Jun 15, 2023 · 5 comments

Comments

@trentmaetzold
Copy link
Contributor

OISRateHelper class does not appear to be using the calendar passed to the paymentCalendar parameter for calculating the start date of the helper. I'm constructing Chile Camara today and 6/19 is a holiday for trading, since local LATAM tends to observe a trading holiday when the US does. I'm passing JointCalendar(UnitedStates(UnitedStates.FederalReserve), Chile) for the paymentCalendar but the helper objects are using 6/19 for the earliestDate attribute. Seems like the helper should either use the paymentCalendar passed or accept a separate tradingCalendar parameter for cases like this.

@AND2797
Copy link
Contributor

AND2797 commented Jun 20, 2023

Any guidance on how I can reproduce this locally?

@trentmaetzold
Copy link
Contributor Author

trentmaetzold commented Jun 21, 2023

The below works on my machine using Python 3.11 and QuantLib 1.30, I can't guarantee that I don't have any code that is incompatible with prior Python versions.

The dates output by advancing the joint calendar are the dates I expect for the earliestDate attribute of the helpers.

from dataclasses import dataclass, field

import QuantLib as ql


@dataclass
class MarketData:
    prices: dict[str, float]
    _quotes: dict[str, ql.SimpleQuote] = field(init=False, default_factory=dict)

    def __post_init__(self) -> None:
        for ticker, price in self.prices.items():
            self._quotes[ticker] = ql.SimpleQuote(price)

    def get_quote(self, ticker: str) -> ql.SimpleQuote:
        if ticker not in self._quotes:
            self._quotes[ticker] = ql.SimpleQuote(0.0)
        return self._quotes[ticker]

    def get_derived_quote(self, ticker: str, divisor: int) -> ql.DerivedQuote:
        quote = self.get_quote(ticker)
        return ql.DerivedQuote(ql.QuoteHandle(quote), lambda x: x / divisor)


def main() -> None:
    prices = {
        "CHSWP20 Curncy": 5.145,
        "CHSWP10 Curncy": 5.015,
        "CHSWP9 Curncy": 5.01,
        "CHSWP8 Curncy": 5.045,
        "CHSWP7 Curncy": 5.085,
        "CHSWP6 Curncy": 5.155,
        "CHSWP5 Curncy": 5.267,
        "CHSWP12 Curncy": 5.055,
        "CHSWP4 Curncy": 5.545,
        "CHSWP2 Curncy": 6.88,
        "CHSWP1F Curncy": 7.84,
        "CHSWP1 Curncy": 9.028,
        "CHSWPI Curncy": 9.755,
        "CHSWPF Curncy": 10.44,
        "CHSWPC Curncy": 10.995,
        "CHSWP3 Curncy": 6.015,
        "CHSWP15 Curncy": 5.075,
    }
    for eval_date in [ql.Date(15, 6, 2023), ql.Date(16, 6, 2023), ql.Date(20, 6, 2023)]:
        ql.Settings.instance().evaluationDate = eval_date
        md = MarketData(prices=prices)

        handle = ql.RelinkableYieldTermStructureHandle()
        index = ql.OvernightIndex(
            "CLICP Index",
            2,
            ql.CLPCurrency(),
            ql.Chile(),
            ql.Actual360(),
            handle,
        )
        calendar = ql.JointCalendar(ql.UnitedStates(ql.UnitedStates.FederalReserve), ql.Chile())
        helpers = [
            ql.OISRateHelper(
                2,
                ql.Period(tenor),
                ql.QuoteHandle(md.get_derived_quote(ticker, divisor=100)),
                index,
                handle,
                telescopicValueDates=False,
                paymentConvention=ql.ModifiedFollowing,
                paymentFrequency=ql.Semiannual if ql.Period(tenor) > ql.Period("18M") else ql.Once,
                paymentCalendar=calendar,
                endOfMonth=False,
            )
            for tenor, ticker in [
                ("3M", "CHSWPC Curncy"),
                ("6M", "CHSWPF Curncy"),
                ("9M", "CHSWPI Curncy"),
                ("1Y", "CHSWP1 Curncy"),
                ("18M", "CHSWP1F Curncy"),
                ("2Y", "CHSWP2 Curncy"),
                ("3Y", "CHSWP3 Curncy"),
                ("4Y", "CHSWP4 Curncy"),
                ("5Y", "CHSWP5 Curncy"),
                ("6Y", "CHSWP6 Curncy"),
                ("7Y", "CHSWP7 Curncy"),
                ("8Y", "CHSWP8 Curncy"),
                ("9Y", "CHSWP9 Curncy"),
                ("10Y", "CHSWP10 Curncy"),
                ("12Y", "CHSWP12 Curncy"),
                ("15Y", "CHSWP15 Curncy"),
                ("20Y", "CHSWP20 Curncy"),
            ]
        ]
        curve = ql.PiecewiseLogCubicDiscount(0, calendar, helpers, ql.Actual360())
        curve.enableExtrapolation()

        print(f"Evaluation Date: {eval_date}")
        for date in set([helper.earliestDate() for helper in helpers]):
            print(" " * 4 + f"Helper Date: {date}")
        print(" " * 4 + f"Calendar Advanced Date: {calendar.advance(eval_date, ql.Period(2, ql.Days))}")


if __name__ == "__main__":
    main()

Output:

Evaluation Date: June 15th, 2023
    Helper Date: June 19th, 2023
    Calendar Advanced Date: June 20th, 2023
Evaluation Date: June 16th, 2023
    Helper Date: June 20th, 2023
    Calendar Advanced Date: June 22nd, 2023
Evaluation Date: June 20th, 2023
    Helper Date: June 23rd, 2023
    Calendar Advanced Date: June 23rd, 2023

@PaulXiCao
Copy link
Contributor

PaulXiCao commented Nov 5, 2023

Explanation
The helper's earliestDate() (as well as others, e.g. maturityDate()) are wrt to the given OvernightIndex index own calender (internally called fixingCalendar) dates.
Thus, you correctly mentioned that earliestDate() is independent of the given payment calender.

Idea
Does it make sense for you to set both calenders to the joint calender? E.g.

index = ql.OvernightIndex(
            "CLICP Index",
            2,
            ql.CLPCurrency(),
            calender,                  # <--- changed
            ql.Actual360(),
            handle,
        )

@trentmaetzold
Copy link
Contributor Author

I would say probably not, because the central bank will print fixings on that day and I'd think you'd end up with missing fixings. I'll try to whip up an example.

@PaulXiCao
Copy link
Contributor

PaulXiCao commented Nov 8, 2023

@trentmaetzold Is the following assessment correct?

  • The underlying index, e.g. ql.OvernightIndex, should be updateable with new fixings according to the passed calender, e.g. ql.Chile().
  • The BootstrapHelper, e.g. ql.OISRateHelper, should be able to specify payment dates as via the paymentCalendar=calendar argument.
  • The BootstrapHelper, e.g. ql.OISRateHelper, should be able to specify tradeable days as via the proposed tradingCalender.
  • All three calenders need not be be the same.

The proposed tradingCalender option seems reasonable. Though I am unsure where this should be placed as the OISRateHelper is probably not the only class with this issue.

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

4 participants