Skip to content

Inconsistency in Day Count in Index Curve? #191

Closed

Description

@YimingZhang07

In swap_float_leg.py, we have the following logic to compute the forward rate for reference index.

dfStart = index_curve.df(startAccruedDt)
dfEnd = index_curve.df(endAccruedDt)
fwd_rate = (dfStart / dfEnd - 1.0) / index_alpha

index_alpha is a result of year fraction using a day counter initialized by the index day count type we specified. (for example, this is Thirty E 360 when constructing a IBOR curve)

(index_alpha, num, _) = index_day_counter.year_frac(startAccruedDt,
                                                    endAccruedDt)

However, dfStart and dfEnd, though using the same index curve, always discount by ACT_ACT_ISDA. This is because we didn't specify the day count type in its second argument.

def df(self,
       dt: (list, Date),
       day_count=DayCountTypes.ACT_ACT_ISDA):

I am unsure if this is a convention - but it seems odd to me as we are using two different day count types on a single curve for time fraction and discounting factors.

Just want to point out before I forget the details, take the time for this.

Thanks,

Activity

  1. nashquant commented on Jul 30, 2023

    @nashquant
    Contributor

    I think this is true only when a discount curve is instantiated as the base class, i.e. discount_curve.py, in which you define your own method for interpolation from interpolator.py. Although I agree that this can cause mistmaches between day count types, I think it's likely not affecting the vast majority of the use cases.

    Normally, you instantiate discount curves like:

    1. discount_curve_flat.py
    2. discount_curve_zeros.py
    3. discount_curve_poly.py

    In the example we're discussing some weeks ago (issue #188), when I put a breakpoint in index.df(endAccruedDt) and step into, I get the correct day_count convention for float_leg, because discount_curve_flat.py overrides the base self.df method to its own (see image below).

    image

    Of course, since I wasn't responsible for the design of the discount curves, I'd appreciate if @domokane gave his views on this.

    Best,
    Matheus

  2. YimingZhang07 commented on Aug 1, 2023

    @YimingZhang07
    Author

    Hi, thanks for look at this. I think we are both correct, it depends on whether the child classes override the implementation of df.

    I was looking at IborSingleCurve class which is initialized from basket of deposit and IRS, which is common and useful in use cases related to curve construction and CDS survival curve calibration. Examples are the following notebooks,

    1. FINCDS_ValuingCDSCompareToMarkit.ipynb
    2. FINIBORSINGLECURVE_BuildingASimpleIborCurve.ipynb

    With typical usage like

    dcType = DayCountTypes.ACT_360
    depo1 = IborDeposit(settlement_date, "1M", 0.017156, dcType)
    depo2 = IborDeposit(settlement_date, "2M", 0.018335, dcType)
    depo3 = IborDeposit(settlement_date, "3M", 0.018988, dcType)
    depo4 = IborDeposit(settlement_date, "6M", 0.018911, dcType)
    depo5 = IborDeposit(settlement_date, "12M", 0.019093, dcType)
    depos = [depo1,depo2,depo3,depo4,depo5]
    
    swapType = SwapTypes.PAY
    dcType = DayCountTypes.THIRTY_E_360_ISDA
    fixedFreq = FrequencyTypes.SEMI_ANNUAL
    swap1 = IborSwap(settlement_date,"2Y",swapType,0.015630,fixedFreq,dcType)
    swap2 = IborSwap(settlement_date,"3Y",swapType,0.015140,fixedFreq,dcType)
    swap3 = IborSwap(settlement_date,"4Y",swapType,0.015065,fixedFreq,dcType)
    swap4 = IborSwap(settlement_date,"5Y",swapType,0.015140,fixedFreq,dcType)
    swap5 = IborSwap(settlement_date,"6Y",swapType,0.015270,fixedFreq,dcType)
    swap6 = IborSwap(settlement_date,"7Y",swapType,0.015470,fixedFreq,dcType)
    swap7 = IborSwap(settlement_date,"8Y",swapType,0.015720,fixedFreq,dcType)
    swap8 = IborSwap(settlement_date,"9Y",swapType,0.016000,fixedFreq,dcType)
    swap9 = IborSwap(settlement_date,"10Y",swapType,0.016285,fixedFreq,dcType)
    swap10 = IborSwap(settlement_date,"12Y",swapType,0.01670,fixedFreq,dcType)
    swaps = [swap1,swap2,swap3,swap4,swap5,swap6,swap7,swap8,swap9,swap10]
    
    libor_curve = IborSingleCurve(valuation_date, depos, [], swaps)

    If we check IborSingleCurve we will see it has no alternative implementation of df.

    I believe the previous issue I raised questioning the mismatch against QuantLib implementation in FINIBORSINGLECURVE_ReplicatingQuantlibExample.ipynb is related to this issue. In this notebook, curves are constructed as IborSingeCurve and then were consumed in the valuation of the floating leg. Index curve are used correctly, as you have checked, but the problem is on discount curve. as I see we use a very plain dfValue = discount_curve.df(valuation_date) in the value function.

  3. gulls-on-parade commented on Dec 3, 2023

    @gulls-on-parade

    Agree with @YimingZhang07 here and I do think this is an issue @domokane .

    The same issue, I believe, is indirectly referenced here and was of course discussed here. While I don't think there are any methodological issues with the change to floating leg valuation - indeed the added flexibility for distinct conventions is probably a good thing - I believe the change to the DiscountCurve.df() method default parameter values could be problematic more broadly. Any reference to a DiscountCurve.df() (or inherited class Curve - so all of the swap curve objects that are calibrated with their embedded methods) that expects (but does not explicitly specify) a convention different from ACT_ACT_ISDA will experience a change in behavior from this.

    As the discount curve's day count attribute is set in swap curve calibration as part of input validation (e.g. here and here), and the discount curve sets its own default value on direct instantiation, I believe a better default parameter value in DiscountCurve.df() would be simply self._dc_type.

  4. domokane commented on Sep 19, 2026

    @domokane
    Owner

    Hi - I have been away. Can you let me know if this is still an issue.

  5. added a commit that references this issue on Oct 16, 2026
  6. huuan26 commented on Oct 19, 2026

    @huuan26
    Contributor

    Hi @domokane,

    To summarize the current status of this issue:

    1. DiscountCurve: Commit 310bab06 previously resolved the core issue on DiscountCurve.df(self, dt) by removing the hardcoded day count and consistently converting dates via imes_from_dates(self.anchor_dt, dt, self.curve_dc_type).
    2. CDSCurve remaining inconsistency & vectorization bug: In inancepy/market/curves/cds_curve.py, CDSCurve.df(self, dt) still hardcoded = (dt - self.anchor_dt) / G_DAYS_IN_YEAR (ACT/365), bypassing the underlying libor_curve.curve_dc_type. Furthermore, passing a list[Date] to CDSCurve.df or CDSCurve.survival_prob crashed due to
      p.array(dt) producing an object array of Date instances instead of floats.

    I have submitted PR #408 with a clean fix that:

    • Delegates CDSCurve.df(self, dt) directly to libor_curve.df(dt) for dates (preserving libor_curve.curve_dc_type and date list vectorization).
    • Updates CDSCurve.survival_prob(self, dt) to use imes_from_dates(self.anchor_dt, dt, self.dc_type) for robust scalar and vectorized date evaluation.
    • Re-enables and updates est_fin_cds_curve in ests/unit/test_FinCDSCurve.py covering scalar/vector evaluations and day-count consistency.
  7. added 2 commits that reference this issue on Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions