Schedule Class
QuantLib Class | QuantLib-Python | QuantLib | 2025.04.22
1️⃣ Introduction to the Schedule Class
A single bond can have dozens of coupon payment dates. A 3-year bond paying interest quarterly has 12 payment dates to calculate — and once you apply business day adjustment rules, manual calculation quickly becomes unmanageable. QuantLib's Schedule class automatically generates a sequence of cash flow dates from a start date, end date, tenor, calendar, and business day convention.
Think of it like a factory production schedule: the Schedule class systematically manages every date on which a financial instrument produces a cash flow. It integrates all four classes covered so far — Date, Period, Calendar, and DayCounter — into a single object, enabling production-grade financial date calculation.
2️⃣ Why Schedule Generation Matters in Finance
The cash flows of financial instruments are concentrated on specific future dates. Every analysis — pricing, risk measurement, accounting — starts with an accurate list of those dates.
Why accurate scheduling matters in finance
- Discounting cash flows: You must know exactly when each payment occurs to calculate its present value (PV)
- Liquidity planning: Knowing payment dates in advance allows you to hold sufficient cash reserves
- Accrual accounting: Revenue and expense recognition depends on which date each cash flow is attributed to
- Risk metrics: Duration and convexity calculations use cash flow dates directly
- Regulatory reporting: Financial regulators require precise payment date information
For example, two bonds with the same coupon rate but different payment frequencies (monthly vs. semi-annual) and different holiday adjustment rules will have different prices. An incorrect schedule leads to pricing errors, settlement failures, and regulatory violations.
3️⃣ Creating a Schedule Object
A Schedule object is created by combining several parameters, each of which draws on the classes you have already learned. There are many parameters at first glance, but each one is intuitive once understood individually.
✅ Basic Construction
The most common approach is to specify the seven required parameters directly:
import QuantLib as ql
# Basic Schedule construction
schedule = ql.Schedule(
ql.Date(1, 1, 2025), # effectiveDate
ql.Date(1, 1, 2027), # terminationDate
ql.Period(6, ql.Months), # tenor (semi-annual)
ql.UnitedStates(ql.UnitedStates.GovernmentBond), # calendar
ql.ModifiedFollowing, # convention
ql.ModifiedFollowing, # terminationDateConvention
ql.DateGeneration.Forward, # rule
False # endOfMonth
)
# Print generated dates
print(f"Total dates: {len(schedule)}")
for i, date in enumerate(schedule):
print(f" {i}: {date}")
# Total dates: 5
# 0: January 2nd, 2025 ← effective date (Jan 1 holiday → adjusted)
# 1: July 1st, 2025
# 2: January 2nd, 2026
# 3: July 1st, 2026
# 4: January 2nd, 2027 ← termination date (Jan 1 holiday → adjusted)✅ Understanding BusinessDayConvention
The business day convention determines which direction a date is shifted when it falls on a weekend or holiday:
import QuantLib as ql
# Adjust May 3, 2025 (Saturday) using each convention
test_date = ql.Date(3, 5, 2025) # Saturday
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
conventions = {
"Following": ql.Following,
"ModifiedFollowing": ql.ModifiedFollowing,
"Preceding": ql.Preceding,
"ModifiedPreceding": ql.ModifiedPreceding,
"Unadjusted": ql.Unadjusted,
}
print(f"Original date: {test_date} (Saturday)")
print()
for name, conv in conventions.items():
adjusted = calendar.adjust(test_date, conv)
print(f" {name:25s}: {adjusted}")
# Original date: May 3rd, 2025 (Saturday)
#
# Following : May 5th, 2025 ← next business day
# ModifiedFollowing : May 2nd, 2025 ← next BD, or previous if different month
# Preceding : May 2nd, 2025 ← previous business day
# ModifiedPreceding : May 5th, 2025 ← previous BD, or next if different month
# Unadjusted : May 3rd, 2025 ← no adjustment✅ Understanding DateGeneration Rules
This controls whether dates are generated forward from the start date or backward from the termination date:
import QuantLib as ql
start = ql.Date(15, 1, 2025)
end = ql.Date(15, 10, 2025)
tenor = ql.Period(3, ql.Months)
cal = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
conv = ql.ModifiedFollowing
# Forward: start → termination
fwd_schedule = ql.Schedule(
start, end, tenor, cal, conv, conv,
ql.DateGeneration.Forward, False
)
# Backward: termination → start (termination date is fixed; intermediates roll back)
bwd_schedule = ql.Schedule(
start, end, tenor, cal, conv, conv,
ql.DateGeneration.Backward, False
)
print("Forward rule:")
for d in fwd_schedule:
print(f" {d}")
print("\nBackward rule:")
for d in bwd_schedule:
print(f" {d}")
# Forward rule:
# January 15th, 2025
# April 15th, 2025
# July 15th, 2025
# October 15th, 2025
# Backward rule:
# January 15th, 2025
# April 15th, 2025
# July 15th, 2025
# October 15th, 2025
# ※ Results are identical here, but differ when an odd (short/long)
# first or last coupon period is present.4️⃣ Practical Example 1: Treasury Bond Coupon Schedule
U.S. Treasury notes and bonds pay interest semi-annually. Let's build a Schedule using real Treasury bond terms and calculate the coupon amount for each payment date.
import QuantLib as ql
# ────────────────────────────────────────────
# Bond terms
# ────────────────────────────────────────────
issue_date = ql.Date(15, 4, 2025) # issue date
maturity_date = ql.Date(15, 4, 2030) # maturity (5 years)
coupon_rate = 0.0425 # 4.25% coupon
face_value = 1_000_000 # $1,000,000 face value
tenor = ql.Period(6, ql.Months)
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
convention = ql.ModifiedFollowing
day_counter = ql.ActualActual(ql.ActualActual.ISMA)
# Build Schedule
schedule = ql.Schedule(
issue_date,
maturity_date,
tenor,
calendar,
convention,
convention,
ql.DateGeneration.Backward, # maturity-anchored (Treasury convention)
False
)
# ────────────────────────────────────────────
# Print coupon schedule
# ────────────────────────────────────────────
print(f"Treasury Bond Coupon Schedule (Face ${face_value:,}, Coupon {coupon_rate*100:.2f}%)")
print(f"{'No.':>4} {'Payment Date':20} {'Days':>6} {'Coupon Amount':>14}")
print("-" * 52)
prev_date = issue_date
total_coupon = 0.0
for i, date in enumerate(schedule):
if i == 0:
prev_date = date
continue
yf = day_counter.yearFraction(prev_date, date)
coupon = face_value * coupon_rate * yf
total_coupon += coupon
days = date - prev_date
print(f" {i:>2}. {str(date):20} {days:>6}d ${coupon:>13,.2f}")
prev_date = date
print("-" * 52)
print(f"{'Total Coupons':>34} ${total_coupon:>13,.2f}")
print(f"{'Principal':>34} ${face_value:>13,}")
print(f"{'Total Receipts':>34} ${total_coupon + face_value:>13,.2f}")
# Treasury Bond Coupon Schedule (Face $1,000,000, Coupon 4.25%)
# No. Payment Date Days Coupon Amount
# ----------------------------------------------------
# 1. October 15th, 2025 183d $21,291.67
# 2. April 15th, 2026 182d $21,175.00
# 3. October 15th, 2026 183d $21,291.67
# ...
# 10. April 15th, 2030 181d $21,058.33
# ----------------------------------------------------
# Total Coupons $212,500.00
# Principal $1,000,000
# Total Receipts $1,212,500.00As this example shows, even at the same coupon rate, the actual payment amount differs slightly between periods because the number of days in each period varies. Using Schedule together with DayCounter captures these differences precisely.
5️⃣ Practical Example 2: Interest Rate Swap Payment Schedule
An interest rate swap (IRS) exchanges fixed-rate payments for floating-rate payments, with the two legs typically paying on different schedules. Let's implement a common structure where the fixed leg pays semi-annually and the floating leg pays quarterly.
import QuantLib as ql
# ────────────────────────────────────────────
# Swap terms
# ────────────────────────────────────────────
effective_date = ql.Date(20, 6, 2025)
termination_date = ql.Date(20, 6, 2030)
notional = 100_000_000 # $100 million notional
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
convention = ql.ModifiedFollowing
def build_schedule(tenor_months):
"""Helper: build a Schedule with the given tenor in months."""
return ql.Schedule(
effective_date,
termination_date,
ql.Period(tenor_months, ql.Months),
calendar,
convention,
convention,
ql.DateGeneration.Forward,
False
)
# Fixed leg (semi-annual, 6 months)
fixed_schedule = build_schedule(6)
# Floating leg (quarterly, 3 months)
float_schedule = build_schedule(3)
print(f"Swap: {effective_date} to {termination_date}")
print(f"Notional: ${notional:,}\n")
# ────────────────────────────────────────────
# Fixed leg
# ────────────────────────────────────────────
fixed_rate = 0.0450 # 4.50% fixed
day_counter = ql.Thirty360(ql.Thirty360.BondBasis)
print(f"[Fixed Leg] {fixed_rate*100:.2f}% p.a., semi-annual — {len(fixed_schedule)-1} payments")
print(f"{'No.':>4} {'Payment Date':20} {'Fixed Payment':>16}")
print("-" * 46)
prev = effective_date
total_fixed = 0.0
for i, date in enumerate(fixed_schedule):
if i == 0:
prev = date
continue
yf = day_counter.yearFraction(prev, date)
payment = notional * fixed_rate * yf
total_fixed += payment
print(f" {i:>2}. {str(date):20} ${payment:>15,.2f}")
prev = date
print(f"{'Total':>28} ${total_fixed:>15,.2f}\n")
# ────────────────────────────────────────────
# Floating leg (rates unknown; show reset/payment dates)
# ────────────────────────────────────────────
print(f"[Floating Leg] 3M SOFR, quarterly — {len(float_schedule)-1} payments")
print(f"{'No.':>4} {'Reset Date (Start)':22} {'Payment Date (End)':20}")
print("-" * 52)
dates = list(float_schedule)
for i in range(1, len(dates)):
print(f" {i:>2}. {str(dates[i-1]):22} {str(dates[i]):20}")
# Swap: June 20th, 2025 to June 20th, 2030
# Notional: $100,000,000
#
# [Fixed Leg] 4.50% p.a., semi-annual — 10 payments
# No. Payment Date Fixed Payment
# ----------------------------------------------
# 1. December 22nd, 2025 $2,250,000.00
# 2. June 22nd, 2026 $2,250,000.00
# ...
# 10. June 20th, 2030 $2,250,000.00
# Total $22,500,000.00
#
# [Floating Leg] 3M SOFR, quarterly — 20 payments
# No. Reset Date (Start) Payment Date (End)
# ----------------------------------------------------
# 1. June 20th, 2025 September 22nd, 2025
# 2. September 22nd, 2025 December 22nd, 2025
# ...
# 20. March 20th, 2030 June 20th, 2030This example demonstrates how two legs with different payment frequencies share the same business day convention, ensuring consistency throughout the swap's life. In practice, this schedule is used to look up the floating rate for each period and compute the net payment.
6️⃣ Integration with Other Classes
The Schedule class absorbs all four previously covered classes and acts as the entry point for financial instrument modeling. Once a Schedule exists, it can be passed directly to higher-level instrument classes.
✅ Integration with FixedRateBond
Schedule is passed directly to QuantLib's bond classes:
import QuantLib as ql
# Build Schedule
schedule = ql.Schedule(
ql.Date(15, 4, 2025),
ql.Date(15, 4, 2028),
ql.Period(6, ql.Months),
ql.UnitedStates(ql.UnitedStates.GovernmentBond),
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Backward,
False
)
# Construct FixedRateBond — pass Schedule directly
bond = ql.FixedRateBond(
1, # settlement days (T+1)
100.0, # face value
schedule, # ← Schedule passed here
[0.0425], # coupon rate 4.25%
ql.ActualActual(ql.ActualActual.ISMA)
)
print("Bond cash flows:")
for cf in bond.cashflows():
print(f" {cf.date()} : {cf.amount():>10.4f}")
# Bond cash flows:
# October 15th, 2025 : 21.2917
# April 15th, 2026 : 21.1750
# ...
# April 15th, 2028 : 121.0583 ← principal included✅ Class Dependency Map
Date ──┐
Period ├──► Schedule ──► FixedRateBond
Calendar├──► ──► FloatingRateBond
Conv ─┘ ──► VanillaSwap
──► CapFloor
DayCounter ──────────────► coupon / year-fraction calculation- Date: provides start and end dates
- Period: provides the payment frequency
- Calendar: supplies the holiday list and business-day logic
- BusinessDayConvention: resolves holiday conflicts
- Schedule: combines the above into an ordered date array
- DayCounter: converts each Schedule interval into a year fraction
7️⃣ Caveats and Practical Tips
Schedule construction caveats
- Forward vs. Backward: Match the market convention. Government bonds and swaps typically use
Backward(termination date is fixed); short-dated instruments often useForward. - endOfMonth option: Setting this to
Truelocks every payment date to the last business day of the month. Use this only for EOM bonds; standard bonds should useFalse. - Separate termination date convention:
terminationDateConventioncan differ fromconvention. If the contract specifies the maturity date verbatim (regardless of holidays), useUnadjustedfor the termination date.
Common mistakes
- Calendar mismatch: In a swap, using different calendars for the fixed and floating legs can cause payment dates to diverge. Always verify the calendar clause in the term sheet.
- effectiveDate >= terminationDate: This produces an empty Schedule silently. Validate inputs before construction.
- Non-integer tenor: If the period from start to end is not an exact multiple of the tenor, a short or long stub period is created. This is normal behavior — just confirm it is intentional.
Practical tips
- Reuse Schedules: If multiple instruments share the same Schedule parameters, create it once and pass the reference. This matters for performance when pricing hundreds of bonds.
- Validate the schedule: Call
schedule.dates()to retrieve the full date list and confirm the first and last dates match your expectations before proceeding. - Keep calendars up to date: Public holidays change annually. Either add custom holidays with
calendar.addHoliday()or keep your QuantLib version current.
8️⃣ Summary and Next Steps
The Schedule class integrates Date, Period, and Calendar to automatically generate the cash flow date sequence for any financial instrument. Key takeaways from this section:
- Schedule construction: specify effective date, termination date, tenor, calendar, and business day convention
- BusinessDayConvention: Following, ModifiedFollowing, Preceding, Unadjusted
- DateGeneration rule: Forward (start → end) vs. Backward (end → start)
- Practical use: Treasury bond coupon schedule, interest rate swap payment schedule
- Upstream integration: passed directly to FixedRateBond, VanillaSwap, and other instrument classes
Next, we will cover the InterestRate class, which encapsulates a rate value together with its compounding convention and day count rule. Rather than passing raw floats around, using InterestRate objects makes discount factor calculation and yield conversion explicit, type-safe, and error-resistant.