Between the points
Worked examples
Read the claim, then run it and check the machine agrees. One at a time — nothing here is taken on trust.
import math
import QuantLib as ql
today = ql.Date(9, 7, 2026)
dc = ql.Actual365Fixed()
dates = [today] + [today + ql.Period(y, ql.Years) for y in (1, 2, 5)]
dfs = [1.0] + [math.exp(-z * dc.yearFraction(today, d)) for z, d in zip((0.048, 0.045, 0.042), dates[1:])]
curve = ql.DiscountCurve(dates, dfs, dc)
def inst_fwd(t, h=1e-6):
return (math.log(curve.discount(t)) - math.log(curve.discount(t + h))) / h
for t in (0.3, 0.7, 1.3, 1.7, 2.5, 3.5, 4.5):
print(f"instantaneous forward at t = {t}: {inst_fwd(t):.4%}")
Three flat shelves: 4.8000% everywhere before the 1y pillar, 4.2008% between 1y and 2y, 3.9997% from 2y out to 5y. Within a segment the forward doesn't budge in the sixth decimal; at each pillar it jumps. Log-linear df is piecewise-constant forwards — the same fact wearing its two dialects. Every instrument QuantLib prices off this curve inherits these shelves.
import math
import QuantLib as ql
today = ql.Date(9, 7, 2026)
dc = ql.Actual365Fixed()
d2, d5 = today + ql.Period(2, ql.Years), today + ql.Period(5, ql.Years)
dfs = {d2: math.exp(-0.045 * dc.yearFraction(today, d2)),
d5: math.exp(-0.042 * dc.yearFraction(today, d5))}
curve = ql.DiscountCurve([today, d2, d5], [1.0, dfs[d2], dfs[d5]], dc)
t_mid = 0.5 * (dc.yearFraction(today, d2) + dc.yearFraction(today, d5))
geometric_mean = math.sqrt(dfs[d2] * dfs[d5])
print(f"df at the segment midpoint: {curve.discount(t_mid):.15f}")
print(f"geometric mean of the ends: {geometric_mean:.15f}")
Identical to all 15 printed decimals. A straight line in log-space makes the midpoint the average of the logs — i.e., the geometric mean of the endpoint prices. If you ever need to know what interpolation a black-box curve uses, this one-point fingerprint test is faster than reading its documentation.
import math
import QuantLib as ql
today = ql.Date(9, 7, 2026)
dc = ql.Actual365Fixed()
dates = [today] + [today + ql.Period(y, ql.Years) for y in (1, 2, 3)]
ts = [dc.yearFraction(today, d) for d in dates[1:]]
for name, log_dfs in (("curve_a", (-0.030, -0.058, -0.084)),
("curve_b", (-0.030, -0.070, -0.066))):
curve = ql.DiscountCurve(dates, [1.0] + [math.exp(l) for l in log_dfs], dc)
dfs = [curve.discount(t) for t in ts]
zeros = [-math.log(df) / t for df, t in zip(dfs, ts)]
print(name, " dfs:", [round(x, 4) for x in dfs], " zeros:", [f"{z:.2%}" for z in zeros])
Both curves look plausible in the two views printed: every df sits between 0 and 1, every zero rate is positive. But one of them hides a negative forward — its df rises from the 2y node to the 3y node, a free lunch in plain sight once you look at the right view. The zeros won't tell you (3.49% → 2.20% just looks like inversion); only the forward view exposes it. Writing the detector is your challenge.
Check the concept
One question at a time. Unsure? Revisit the lecture, then answer.
The challenge
Pass the quiz to unlock the challenge — your code will still be waiting here.