10 — Annuities
Concept
Chapter 09 ended with a Technical Note observing that every Time Value of Money calculation involving a single lump sum has a closed-form solution, because a single cash flow involves only one power of (1+i). That stops being true the moment a calculation involves a sum of payments — which is what an annuity is. Annuities are the largest topic in introductory Financial Mathematics, and this chapter is correspondingly the most substantial in Part III.
An annuity is a series of periodic payments.
Mathematical Foundation
Level Annuity-Immediate
For a level annuity-immediate paying 1 at the end of each period for n periods, at effective interest rate i per period:
For a payment of P per period rather than 1, both present and accumulated values scale linearly: P·a₍n⌐ and P·s₍n⌐.
Level Annuity-Due
Level Perpetuities
m-thly and Continuous
Arithmetic (Increasing / Decreasing) Annuities
Geometric Annuities
Solving for an Unknown Rate — Newton–Raphson
Worked Examples
Example 1 — level annuities and a perpetuity
An investor considers a level annuity of $500 per year for 10 years at an effective annual rate of 6%.
Example 2 — non-level annuities
(a) At 5%, an arithmetic annuity-immediate paying $100, $200, …, $600 at the end of years 1–6:
(b) At 7%, a geometric annuity-immediate, first payment $1,000, growing 3%/year, over 10 years:
Example 3 — solving for the interest rate
A payment stream of $1,000 per year for 10 years has a present value of $7,500. Find the effective annual interest rate.
| k | i_k | g(i_k) |
|---|---|---|
| 0 | 0.080000 | −$789.92 |
| 1 | 0.053900 | $77.54 |
| 2 | 0.056028 | $0.60 |
| 3 | 0.056045 | ≈ 0 |
The iteration converges to i ≈ 5.6045% in three steps. The Python implementation below (solve_annuity_rate) reproduces this.
Python Implementation
Every tool below follows the same Input / Processing / Output / Validation contract introduced in Chapter 05, and is registered on an Agno agent exactly as written — nothing here is simplified for the page.
annuity_immediate_pv
from agno.exceptions import RetryAgentRun
def annuity_immediate_pv(payment: float, rate_per_period: float, n: int) -> float:
"""
Compute the present value of a level annuity-immediate.
Args:
payment (float): The level payment amount per period. Must be
non-negative.
rate_per_period (float): The effective interest rate per
payment period, as a decimal. Must be greater than -1.
n (int): The number of payment periods. Must be positive.
Returns:
float: The present value of the annuity.
"""
if payment < 0:
raise RetryAgentRun(
"payment must be non-negative. Re-read the request and "
"call annuity_immediate_pv again with a corrected value."
)
if n <= 0:
raise RetryAgentRun(
"n must be a positive number of periods. Re-read the "
"request and call annuity_immediate_pv again with a "
"corrected value for n."
)
if rate_per_period <= -1:
raise RetryAgentRun(
"rate_per_period must be greater than -100%. Re-read the "
"request and call annuity_immediate_pv again with a "
"corrected rate_per_period."
)
if rate_per_period == 0:
return payment * n
v = 1 / (1 + rate_per_period)
a_n = (1 - v ** n) / rate_per_period
return payment * a_nTool Contract — annuity_immediate_pv
Input — payment (float, ≥ 0), rate_per_period (float, > −1), n (int, > 0).
Processing — computes P·a₍n⌐ = P·(1 − v^n)/i, returning P·n when the rate is zero.
Output — a float: the present value one period before the first payment, in the same units as payment.
annuity_due_pv
def annuity_due_pv(payment: float, rate_per_period: float, n: int) -> float:
"""
Compute the present value of a level annuity-due, by composing
the annuity-immediate calculation.
Args:
payment (float): The level payment amount per period. Must be
non-negative.
rate_per_period (float): The effective interest rate per
payment period, as a decimal. Must be greater than -1.
n (int): The number of payment periods. Must be positive.
Returns:
float: The present value of the annuity-due.
"""
return (1 + rate_per_period) * annuity_immediate_pv(payment, rate_per_period, n)Key idea: annuity_due_pv performs no validation of its own and contains no summation logic. It calls annuity_immediate_pv and scales the result, mirroring the mathematical relationship above directly in code. Composed tools carry the validation of the tools they call automatically; there is no separate validation path to keep in sync. This pattern recurs throughout Part III.
perpetuity_immediate_pv
def perpetuity_immediate_pv(payment: float, rate_per_period: float) -> float:
"""
Compute the present value of a level perpetuity-immediate.
Args:
payment (float): The level payment amount per period. Must be
non-negative.
rate_per_period (float): The effective interest rate per
payment period, as a decimal. Must be strictly positive.
Returns:
float: The present value of the perpetuity.
"""
if payment < 0:
raise RetryAgentRun(
"payment must be non-negative. Re-check the request."
)
if rate_per_period <= 0:
raise RetryAgentRun(
"rate_per_period must be strictly positive for a perpetuity "
"to have a finite present value. Re-check the request."
)
return payment / rate_per_periodincreasing_annuity_immediate_pv
def increasing_annuity_immediate_pv(
first_payment: float, increment: float, rate_per_period: float, n: int
) -> float:
"""
Compute the present value of an arithmetic annuity-immediate,
with payment k equal to first_payment + (k-1) * increment for
k = 1, ..., n. A positive increment gives an increasing annuity;
a negative increment gives a decreasing annuity.
Args:
first_payment (float): The payment at the end of period 1.
increment (float): The constant change in payment each
subsequent period. Positive for increasing, negative for
decreasing.
rate_per_period (float): The effective interest rate per
payment period, as a decimal. Must be greater than -1.
n (int): The number of payment periods. Must be positive.
Returns:
float: The present value of the arithmetic annuity.
"""
if n <= 0:
raise RetryAgentRun(
"n must be a positive number of periods. Re-check the "
"request."
)
if rate_per_period <= -1:
raise RetryAgentRun(
"rate_per_period must be greater than -100%. Re-check the "
"request."
)
if rate_per_period == 0:
return sum(first_payment + k * increment for k in range(n))
v = 1 / (1 + rate_per_period)
a_n = (1 - v ** n) / rate_per_period
add_n = a_n * (1 + rate_per_period)
increasing_factor = (add_n - n * v ** n) / rate_per_period
return (first_payment - increment) * a_n + increment * increasing_factorgeometric_annuity_immediate_pv
def geometric_annuity_immediate_pv(
first_payment: float, growth_rate: float, rate_per_period: float, n: int
) -> float:
"""
Compute the present value of a geometric annuity-immediate, with
first payment first_payment growing by a factor of
(1 + growth_rate) each subsequent period, for n periods.
Args:
first_payment (float): The payment at the end of period 1.
Must be non-negative.
growth_rate (float): The per-period growth rate of the
payments, as a decimal. Must be greater than -1.
rate_per_period (float): The effective interest rate per
payment period, as a decimal. Must be greater than -1.
n (int): The number of payment periods. Must be positive.
Returns:
float: The present value of the geometric annuity.
"""
if first_payment < 0:
raise RetryAgentRun("first_payment must be non-negative. Re-check the request.")
if n <= 0:
raise RetryAgentRun("n must be a positive number of periods. Re-check the request.")
if rate_per_period <= -1 or growth_rate <= -1:
raise RetryAgentRun(
"Both rate_per_period and growth_rate must be greater than "
"-100%. Re-check the request."
)
if abs(rate_per_period - growth_rate) < 1e-12:
v = 1 / (1 + rate_per_period)
return first_payment * n * v
r = (1 + growth_rate) / (1 + rate_per_period)
return first_payment * (1 - r ** n) / (rate_per_period - growth_rate)solve_annuity_rate
from scipy.optimize import newton
def solve_annuity_rate(
payment: float, present_value: float, n: int, initial_guess: float = 0.05
) -> float:
"""
Solve for the effective interest rate per period implied by a
level annuity-immediate's payment, present value, and term, using
Newton-Raphson.
Args:
payment (float): The level payment amount per period. Must be
positive.
present_value (float): The annuity's present value. Must be
positive and less than payment * n.
n (int): The number of payment periods. Must be positive.
initial_guess (float): The starting point for the iteration.
Defaults to 0.05 (5%).
Returns:
float: The implied effective interest rate per period.
"""
if payment <= 0 or present_value <= 0:
raise RetryAgentRun(
"payment and present_value must both be positive. "
"Re-check the request."
)
if n <= 0:
raise RetryAgentRun(
"n must be a positive number of periods. Re-check the "
"request."
)
if present_value >= payment * n:
raise RetryAgentRun(
"present_value must be less than payment * n; no positive "
"interest rate produces a present value this large for "
"this payment and term. Re-check the request."
)
def g(i: float) -> float:
return annuity_immediate_pv(payment, i, n) - present_value
def g_prime(i: float) -> float:
v = 1 / (1 + i)
a_n = (1 - v ** n) / i
return payment * (n * v ** (n + 1) - a_n) / i
try:
return newton(g, initial_guess, fprime=g_prime)
except RuntimeError:
raise RetryAgentRun(
"The Newton-Raphson iteration did not converge from the "
"given starting point. Try again with a different "
"initial_guess, or re-check the inputs for consistency."
)Agent Implementation
All six tools register on a single Agno agent:
from agno.agent import Agent
from agno.models.google import Gemini
annuity_agent = Agent(
name="Annuity Agent",
role="Answers annuity questions: present value of level, "
"arithmetic, and geometric annuities and perpetuities, and "
"solves for an unknown interest rate given payment, term, "
"and present value.",
model=Gemini(id="gemini-3.5-flash", temperature=0.0),
tools=[
annuity_immediate_pv,
annuity_due_pv,
perpetuity_immediate_pv,
increasing_annuity_immediate_pv,
geometric_annuity_immediate_pv,
solve_annuity_rate,
],
instructions=[
"Always use one of the available tools to perform any "
"numerical calculation. Never state a computed numeric "
"result unless it came directly from a tool call.",
"If it is unclear whether payments begin immediately "
"(annuity-due) or at the end of the first period "
"(annuity-immediate), ask a brief clarifying question rather "
"than guessing.",
"If a request gives a payment, term, and present value and "
"asks for the interest rate, use solve_annuity_rate.",
],
markdown=True,
)
annuity_agent.print_response(
"A payment stream of $1,000 a year for 10 years, paid at the end "
"of each year, has a present value of $7,500. What interest "
"rate does that imply?"
)Given this request, the agent should recognise, from the third instruction, that solve_annuity_rate is the appropriate tool, call it with payment=1000, present_value=7500, n=10, and report the result — reproducing Example 3 above, with the Newton–Raphson iteration hidden from the conversation.
Where This Can Fail
Annuity-immediate versus annuity-due ambiguity. This is the chapter's most realistic failure mode, paralleling Chapter 09's nominal-versus-effective concern. The second agent instruction above asks the model to seek clarification rather than guess; whether it does so reliably is a question for Chapter 19's evaluation practices, not something the instruction's presence settles on its own.
Newton–Raphson non-convergence. solve_annuity_rate's validation rejects the one case with a provably impossible request (present_value ≥ payment × n), but plausible-looking inputs can still cause the iteration to fail from a poorly chosen initial_guess, particularly for large n or a present_value close to its boundary. The RetryAgentRun raised in that case gives the model a chance to retry with a different starting point.
Payments implied to go negative. increasing_annuity_immediate_pv with a large negative increment will happily value a stream whose later "payments" are negative. The formula is still correct for that stream, but it is unlikely to be what a user meant, and nothing in the tool detects it.
Tool-set growth. This agent's six tools are a small, well-differentiated set. Adding Chapter 11's loan tools to the same agent is the point at which Chapter 08's specialisation pattern starts to become worth considering.
Key Takeaways
- Annuities generalise Chapter 09's single lump sum to a stream of payments, distinguished by timing (immediate versus due), duration (finite versus perpetuity), frequency (annual, m-thly, or continuous), and payment pattern (level, arithmetic, or geometric); every formula in this chapter reduces to the geometric series sum at its core.
- Solving for an unknown interest rate given payment, term, and present value has no closed-form solution once payments form a sum of more than one term; this is where Newton–Raphson becomes necessary.
- Composing tools (
annuity_due_pvcallingannuity_immediate_pv) mirrors mathematical relationships directly in code and avoids duplicating validation logic. - Ambiguity between annuity-immediate and annuity-due timing is a realistic, recurring failure mode for a natural-language agent, analogous to the nominal-versus-effective ambiguity of Chapter 09.
Exercises
- Conceptual. Explain why annuity-due present value can be obtained by multiplying annuity-immediate present value by (1+i), using the timing of the payments rather than re-deriving the summation.
- Mathematical. A perpetuity-due pays $200 per year, first payment today, at an effective annual rate of 4%. Find its present value, and verify your answer is consistent with ä₍∞⌐ = 1/d.
- Mathematical. Using the general arithmetic annuity formula above, find the present value of a decreasing annuity-immediate paying $500, $400, $300, $200, $100 at the end of years 1 through 5, at an effective annual rate of 6%.
- Implementation.
solve_annuity_ratesolves for the interest rate given payment, present value, and term. Write the full tool contract for a new functionsolve_annuity_termthat instead solves for the number of periods n given payment, present value, and interest rate — and explain whether this new function needs Newton–Raphson, or has a closed-form solution (consider taking a logarithm of the rearranged annuity formula). - Tool and agent design. Combine this chapter's six annuity tools with Chapter 09's five Time Value of Money tools into a single agent, and test whether tool selection remains reliable. If you observe selection errors, redesign the system as a two-specialist team (Chapter 08) instead, and compare.
- Evaluation. Construct a request that is genuinely ambiguous between annuity-immediate and annuity-due. Run it through the agent above several times at temperature zero and note whether it asks a clarifying question consistently, occasionally, or never.
Answers to Exercises 2–4
2. d = i/(1+i) = 0.04/1.04 ≈ 0.038462, so ä₍∞⌐ = 1/d = 26 and PV = 200 × 26 = $5,200. Equivalently (1+i) × 200/i = 1.04 × 5,000 = 5,200.
3. With P = 500, Q = −100, n = 5, i = 0.06: a₍5⌐ ≈ 4.212364, (Ia)₍5⌐ ≈ 12.147, so PV = (500 + 100)(4.212364) − 100(12.147) ≈ $1,312.73. Check via (Da)₍5⌐ = (5 − a₍5⌐)/i ≈ 13.12727: 100 × 13.12727 = 1,312.73. increasing_annuity_immediate_pv(500, -100, 0.06, 5) returns 1312.73.
4. Rearranging PV = PMT · (1 − v^n)/i gives v^n = 1 − PV·i/PMT, so n = −ln(1 − PV·i/PMT) / ln(1+i): a closed form, no Newton–Raphson needed. Contract — Input: payment (positive), present_value (positive, less than payment/rate_per_period so the logarithm's argument is positive), rate_per_period (positive). Output: n as a float, which will generally not be an integer. Check: payment=1000, present_value=7500, rate_per_period=0.056045 returns 10.0.