I am trying to model a term loan in QuantLib-Python
that makes quarterly interest payments at CME Term SOFR 3M + 10bps + 525bps paid in arrears with a 2 business day fixing.
The amortization schedule is custom in that it does not start until after the first interest payment date and only occurs on the last business day of March, June, September and December. Annual amortization is 5% of the initial principal amount.
My code successfully generates the principal and interest cashflows for an assumed SOFR rate of 5%. Rather than wrap these cashflows in ql.SimpleCashFlow
, I'd rather take advantage of the ql.AmortizingFloatingRateBond
class so I can pass a SOFR curve. What is the recommended approach to set up this instrument given the custom schedule?
import QuantLib as ql
def quarter_end_business_days(start_date, end_date, calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond), quarter_end_months = [3,6,9,12]):
# Initialize the current date to the start date
current_date = start_date
# List to store the last business days of the quarter-end months
last_business_days = []
# Loop to iterate through the months and find the last business day of the quarter-end months
while current_date <= end_date:
# Determine the end of the current month
eom_date = ql.Date.endOfMonth(current_date)
# Check if it is a quarter-end month
if eom_date.month() in quarter_end_months:
# Adjust to the last business day
last_business_day = calendar.adjust(eom_date, ql.Preceding)
last_business_days.append(last_business_day)
# Move to the first day of the next month, adjusting the year if necessary
if current_date.month() == 12:
current_date = ql.Date(1, 1, current_date.year() + 1)
else:
current_date = ql.Date(1, current_date.month() + 1, current_date.year())
# Return the list of last business days
return last_business_days
# Set evaluation date and basic parameters
today = ql.Date(9, 5, 2024)
ql.Settings.instance().evaluationDate = today
calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
# Day count convention
day_count = ql.Actual360()
# Loan schedule setup
effective_date = ql.Date(10, 5, 2022)
maturity_date = ql.Date(1, 2, 2027)
first_amortization_date = ql.Date(30,9,2022)
tenor = ql.Period(ql.Quarterly)
# Manually define the dates to ensure they are quarter ends
dates = [effective_date] + quarter_end_business_days(start_date=effective_date, end_date=maturity_date, calendar=calendar) + [maturity_date]
# Create the schedule directly with these dates
schedule = ql.Schedule(dates, calendar, ql.Unadjusted)
# Define daily SOFR index and setup the forward curve for 3-month SOFR
sofr_index = ql.Sofr()
dates = [schedule[i] for i in range(len(schedule))]
rates = [0.05 + 0.0010 + 0.0525] * len(dates) # Consistent rates for simplification; Base SOFR + 10 bps + 525 bps
day_count = ql.Actual360()
sofr_curve = ql.ZeroCurve(dates, rates, day_count, calendar)
sofr_curve_handle = ql.YieldTermStructureHandle(sofr_curve)
# Create an overnight index linked to the constructed yield curve
three_month_sofr = ql.OvernightIndex("3M SOFR", 0, ql.USDCurrency(), calendar, day_count, sofr_curve_handle)
# Setting up the bond (loan) mechanics
face_value = 100 # Initial principal
principal_payment = face_value * 0.05 / 4 # 5% annually, divided by 4 for quarterly payments
# Initialize loan cashflows
principal_remaining = face_value
cashflows = []
for i in range(1, len(schedule)):
date = schedule[i]
# Calculate effective interest rate with floor
floor_rate = 0.005 # 50 bps floor
three_month_rate = max(floor_rate, sofr_curve_handle.zeroRate(date, ql.Actual360(), ql.Continuous).rate())
interest_payment = principal_remaining * three_month_rate * day_count.yearFraction(schedule[i-1], schedule[i]) # Quarterly payments
current_principal_payment = principal_remaining if date == maturity_date else (principal_payment if date >= first_amortization_date else 0)
total_payment = current_principal_payment + interest_payment
principal_remaining -= current_principal_payment
principal_remaining = max(0, principal_remaining) # Ensure no negative principal
cashflows.append((date, total_payment, interest_payment, current_principal_payment, principal_remaining))
# Display the amortization schedule
for date, total, interest, principal, remaining in cashflows:
print(f"Date: {date.ISO()}, Total Payment: {total:.2f}, Interest: {interest:.2f}, Principal: {principal:.2f}, Remaining: {remaining:.2f}")