Tuesday, February 18, 2025

Generating Swap Cashflows with QuantLib

     


M 917 536 3378

maksim_kozyarchuk@yahoo.com








Accurate modeling of cashflows is a cornerstone of any fixed income portfolio valuation and cashflow management platform. One of the essential ingredients in cashflow modeling is the generation of cashflow dates, and QuantLib.Schedule() function provides a flexible way to do this. However, its interface isn’t exactly straightforward.




The Challenge with QuantLib.Schedule

According to the SWIG-generated documentation, the QuantLib.Schedule() constructor accepts the following ten parameters:

  1. effectiveDate (Date)

  2. terminationDate (Date)

  3. tenor (Period)

  4. calendar (Calendar)

  5. convention (BusinessDayConvention)

  6. terminationDateConvention (BusinessDayConvention)

  7. rule (DateGeneration::Rule)

  8. endOfMonth (bool)

  9. firstDate (Date, optional)

  10. nextToLastDate (Date, optional)

While this constructor is functional, the parameters are quite cumbersome to work with and do not follow industry-standards especially around defining stubs and roll mechanisms



My Take on a More Convenient Approach

While expanding portfolio valuation functionality in Kupala-Nich, I came up with a different interface for defining the schedule that provides a more intuitive way of defining cashflows and by extension swaps.  Before looking at the new interface, let’s review the existing interface field by field.

  • effectiveDate:
    Although necessary, it should be optional and default to "today" plus the index’s settlement days.

  • terminationDate:
    This should be required but flexible. It should allow specification either as an explicit date or as a tenor (for example, "10Y" would be added to the effective date).

  • tenor:
    A better name for this parameter would be roll_period, it should be optional and default to the index’s convention.

  • calendar:
    Should be optional, defaulting to the index’s calendar.

  • convention:
    A better name for this parameter would be roll_convention, this too should be optional and default to the index’s settings.

  • terminationDateConvention:
    A better name for this parameter would be termination_roll_convention, it should follow similar defaulting behavior.

  • rule :
    Drives the algorithm used for date generation, forward, backward, IMM, etc. 

  • endOfMonth:
    Boolean flag to indicate that roll dates should be pushed to month end, but only works if start/end dates are also month ends.

  • firstDate:
    Allows defining the first period as a long stub.

  • nextToLastDate:
    Allows defining the last period as a long stub.

The final four fields have a complex interconnection.  These fields are not very intuitive and don’t match industry conventions I propose an alternative: replacing them with just two fields:

  • Roll Type:
    Options such as Standard, EOM, and IMM should suffice for OIS swaps while other products (like CDSes) might benefit from additional date generation rules.

  • Stub Type:
    Options like Front Short, Front Long, End Short, End Long.

In fact, for the majority of swaps, there are only nine valid combinations of these values:

  • 4 combinations: Standard with any stub.

  • 2 combinations: EOM with a front stub (termination date must be on month-end).

  • 2 combinations: EOM with an end stub (effective date must be on month-end).

  • 1 combination: IMM (only supports Front Short Stub).

Here is the implementation of a function generates these four fields from rollType and stubType:

get_roll_fields( roll_type: str, stub_type: str, start_date: ql.Date, maturity_date: ql.Date, roll_period: ql.Period, calendar: ql.Calendar, roll_conv, term_roll_conv) :
   
    first_roll_date = last_roll_date = ql.Date()
    end_of_month = False
    if roll_type == "IMM":
        if stub_type != "Front Short":
            raise ValueError("IMM roll_type requires stub_type='Front Short'")
        date_rule = ql.DateGeneration.TwentiethIMM
    elif roll_type == "EOM":
        end_of_month = True
        if stub_type.startswith("Front"):
            if not ql.Date.isEndOfMonth(maturity_date):
                raise ValueError("EOM roll_type with Front stub requires EOM maturity_date")
            date_rule = ql.DateGeneration.Backward
        elif stub_type.startswith("End"):
            if not ql.Date.isEndOfMonth(start_date):
                raise ValueError("EOM roll_type with End stub requires EOM start_date")
            date_rule = ql.DateGeneration.Forward
        else:
            raise ValueError(f"Unsupported stub_type for EOM: {stub_type}")
    else# Standard roll types
        if stub_type.startswith("Front"):
            date_rule = ql.DateGeneration.Backward
        elif stub_type.startswith("End"):
            date_rule = ql.DateGeneration.Forward
        else:
            raise ValueError(f"Unsupported stub_type: {stub_type}")
    if "Long" in stub_type:
        s = list( ql.Schedule( start_date, maturity_date, roll_period, calendar, roll_conv, term_roll_conv, date_rule, end_of_month ) )
        if len(s) >= 3:
            if stub_type.startswith("Front"):
                first_roll_date = s[2]
            elif stub_type.startswith("End"):
                last_roll_date = s[-3]
        else:
            raise ValueError("Schedule has too few dates for Long stub")
    return date_rule, end_of_month, first_roll_date, last_roll_date




Schedule generation with Kupala-Nich

With Kupala-Nich, I need to generate swap cashflows and by extension schedule based on user input in csv format.  This format needs to be simple for basic usecaes but should scale to support all commonly traded swap flavors. To support this, the generate_schedule function has only two required parameters. template which defines the governing conventions for the swap being traded and maturityDate.   The function also accepts seven optional arguments that fine grained control over cashflow schedule. Here is the implementation of this function.

def generate_schedule(template, maturity_date, start_date = None, calendar = None, roll_period = None, roll_conv = None, term_roll_conv = None, roll_type = None, stub_type = None):
    start_date    = template.get_and_validate_start_date(start_date)
    calendar      = template.get_and_validate_calendar(calendar)
    roll_period   = template.get_and_validate_roll_period(roll_period)
    roll_conv     = template.get_and_validate_roll_conv(roll_conv)
    term_roll_conv= template.get_and_validate_term_roll_conv(term_roll_conv)
    roll_type     = template.get_and_validate_roll_type(roll_type)
    stub_type     = template.get_and_validate_stub_type(stub_type)
    date_rule, end_of_month, first_roll_date, last_roll_date = get_roll_fields(
                  roll_type, stub_type, start_date, maturity_date
                  roll_periodcalendar, roll_conv, term_roll_conv)
    return ql.Schedule(start_date, maturity_date, roll_period,  
                    calendar, roll_conv, term_roll_conv,
                    date_rule, end_of_month, first_roll_date, last_roll_date
 




Conclusion

Generating cashflow schedules in QuantLib can be daunting due to its numerous parameters and lack of detailed comprehensive documentation. By rethinking the interface—defaulting many parameters to index conventions and simplifying as well as aligning the interface for defining roll specifications with industry practice, Kupala-Nich is making swap valuation more user friendly. 

Feedback and suggestions are welcome!