Skip to main content
Glama
by apetta
financial.py16.6 kB
"""Financial mathematics tools.""" import json import math from typing import Annotated, Any, Dict, List, Literal, Union, cast from pydantic import Field from mcp.types import ToolAnnotations import numpy_financial as npf from ..server import mcp from ..core import format_result @mcp.tool( name="financial_calcs", description="""Time Value of Money (TVM) calculations: solve for PV, FV, PMT, rate, IRR, or NPV. The TVM equation has 5 variables - know 4, solve for the 5th: PV = Present Value (lump sum now) FV = Future Value (lump sum at maturity) PMT = Payment (regular periodic cash flow) N = Number of periods I/Y = Interest rate per period Sign convention: negative = cash out (you pay), positive = cash in (you receive) Examples: ZERO-COUPON BOND: PV of £1000 in 10 years at 5% calculation="pv", rate=0.05, periods=10, future_value=1000 Result: £613.91 COUPON BOND: PV of £30 annual coupons + £1000 face value at 5% yield calculation="pv", rate=0.05, periods=10, payment=30, future_value=1000 Result: £845.57 RETIREMENT SAVINGS: FV with £500/month for 30 years at 7% calculation="fv", rate=0.07/12, periods=360, payment=-500, present_value=0 Result: £566,764 MORTGAGE PAYMENT: Monthly payment on £200k loan, 30 years, 4% APR calculation="pmt", rate=0.04/12, periods=360, present_value=-200000, future_value=0 Result: £954.83 INTEREST RATE: What rate grows £613.81 to £1000 in 10 years? calculation="rate", periods=10, present_value=-613.81, future_value=1000 Result: 0.05 (5%) GROWING ANNUITY: Salary stream with 3.5% raises, discounted at 12% calculation="pv", rate=0.12, periods=25, payment=-45000, growth_rate=0.035 Result: £402,586""", annotations=ToolAnnotations( title="Financial Calculations", readOnlyHint=True, idempotentHint=True, ), ) async def financial_calcs( calculation: Annotated[ Literal["pv", "fv", "pmt", "rate", "irr", "npv"], Field(description="What to solve for: pv, fv, pmt, rate, irr, or npv"), ], rate: Annotated[ float | None, Field(description="Interest/discount rate per period (e.g., 0.05 for 5% annual)"), ] = None, periods: Annotated[int | None, Field(description="Number of compounding periods", ge=1)] = None, payment: Annotated[ float | None, Field(description="Regular periodic payment (negative=pay out, positive=receive)"), ] = None, present_value: Annotated[ float | None, Field(description="Single lump sum at time 0 (negative=pay, positive=receive)"), ] = None, future_value: Annotated[ float | None, Field(description="Single lump sum at maturity (negative=owe, positive=receive)"), ] = None, cash_flows: Annotated[ Union[str, List[float], None], Field(description="Series of cash flows for IRR/NPV (e.g., [-100, 30, 30, 130])"), ] = None, when: Annotated[ Literal["end", "begin"], Field(description="Payment timing: 'end' (ordinary) or 'begin' (annuity due)"), ] = "end", growth_rate: Annotated[ float, Field(description="Payment growth rate per period (0.0 for level annuity)", ge=0) ] = 0.0, ) -> str: """Time Value of Money calculations.""" try: # Parse stringified JSON from XML serialization if isinstance(cash_flows, str): cash_flows = cast(List[float], json.loads(cash_flows)) if calculation == "pv": # Present Value: solve for PV given FV and/or PMT if rate is None: raise ValueError("PV calculation requires rate") if periods is None: raise ValueError("PV calculation requires periods") # Ensure at least one component was provided if future_value is None and payment is None: raise ValueError("PV: provide rate + periods + (future_value AND/OR payment)") # Handle growing annuity case if growth_rate != 0.0 and payment is not None and payment != 0: # Validate growth rate if growth_rate < 0: raise ValueError("Growth rate cannot be negative") # Calculate PV of growing annuity (formula works with positive values) payment_abs = abs(payment) if abs(rate - growth_rate) < 1e-10: # Special case: rate == growth_rate pv_annuity = payment_abs * periods / (1 + rate) else: # Standard growing annuity formula growth_factor = (1 + growth_rate) / (1 + rate) pv_annuity = payment_abs * (1 - growth_factor**periods) / (rate - growth_rate) # Adjust for annuity due if when == "begin": pv_annuity *= 1 + rate # Apply sign: payment < 0 (pay out) → PV > 0 (value received) pv_annuity = pv_annuity if payment < 0 else -pv_annuity # Add PV of lump sum if present if future_value is not None and future_value != 0: pv_lumpsum = abs(future_value) / ((1 + rate) ** periods) # future_value > 0 (receive) → PV < 0 (cost) pv_lumpsum = -pv_lumpsum if future_value > 0 else pv_lumpsum pv_annuity += pv_lumpsum result = pv_annuity else: # Use numpy-financial for standard (non-growing) calculation result = npf.pv( rate, periods, float(payment) if payment is not None else 0.0, float(future_value) if future_value is not None else 0.0, # type: ignore[arg-type] when=when, ) elif calculation == "rate": # Solve for interest rate if periods is None or present_value is None: raise ValueError("Rate calculation requires periods and present_value") # Ensure we have either future_value or payment if (future_value is None or future_value == 0) and (payment is None or payment == 0): raise ValueError("Rate calculation requires either future_value or payment") # Use numpy-financial for battle-tested calculation # Note: numpy-financial handles PV=0 correctly for annuity scenarios result = npf.rate( periods, float(payment) if payment is not None else 0.0, present_value, float(future_value) if future_value is not None else 0.0, when=when, ) elif calculation == "fv": # Future Value if rate is None: raise ValueError("FV calculation requires rate") if payment is None or periods is None: raise ValueError("FV calculation requires rate, periods, and payment") # Handle growing annuity case if growth_rate != 0.0 and payment != 0: # Validate growth rate if growth_rate < 0: raise ValueError("Growth rate cannot be negative") # Calculate FV of growing annuity (formula works with positive values) payment_abs = abs(payment) if abs(rate - growth_rate) < 1e-10: # Special case: rate == growth_rate fv_annuity = payment_abs * periods * ((1 + rate) ** (periods - 1)) else: # Standard growing annuity FV formula fv_annuity = payment_abs * ( ((1 + rate) ** periods - (1 + growth_rate) ** periods) / (rate - growth_rate) ) # Adjust for annuity due if when == "begin": fv_annuity *= 1 + rate # Apply sign: payment < 0 (pay) → FV > 0 (accumulate) fv_annuity = fv_annuity if payment < 0 else -fv_annuity # Add FV of present value if present if present_value is not None and present_value != 0: fv_pv = present_value * ((1 + rate) ** periods) fv_annuity += fv_pv result = fv_annuity else: # Use numpy-financial for standard (non-growing) calculation result = npf.fv( rate, periods, payment, float(present_value) if present_value is not None else 0.0, when=when, ) elif calculation == "pmt": # Payment if rate is None: raise ValueError("PMT calculation requires rate") if present_value is None or periods is None: raise ValueError("PMT calculation requires rate, periods, and present_value") # Use numpy-financial for battle-tested calculation result = npf.pmt( rate, periods, present_value, float(future_value) if future_value is not None else 0.0, # type: ignore[arg-type] when=when, ) elif calculation == "irr": # Internal Rate of Return if cash_flows is None or len(cash_flows) < 2: raise ValueError("IRR calculation requires cash_flows with at least 2 values") # Use numpy-financial for battle-tested calculation result = npf.irr(cash_flows) elif calculation == "npv": # Net Present Value if rate is None: raise ValueError("NPV calculation requires rate") if cash_flows is None: raise ValueError("NPV calculation requires cash_flows and rate") # Use numpy-financial for battle-tested calculation # Note: npf.npv treats first value as t=0 (present), matching our convention result = npf.npv(rate, cash_flows) else: raise ValueError(f"Unknown calculation type: {calculation}") # Build metadata with all provided parameters for better context management metadata: Dict[str, Any] = {"calculation": calculation} if rate is not None: metadata["rate"] = rate if periods is not None: metadata["periods"] = periods if payment is not None: metadata["payment"] = payment if present_value is not None: metadata["present_value"] = present_value if future_value is not None: metadata["future_value"] = future_value if cash_flows is not None: metadata["cash_flows"] = cash_flows if when != "end": metadata["when"] = when if growth_rate != 0.0: metadata["growth_rate"] = growth_rate return format_result(float(result), metadata) except Exception as e: raise ValueError(f"Financial calculation failed: {str(e)}") @mcp.tool( name="compound_interest", description="""Calculate compound interest with various compounding frequencies. Formulas: Discrete: A = P(1 + r/n)^(nt) Continuous: A = Pe^(rt) Examples: ANNUAL COMPOUNDING: £1000 at 5% for 10 years principal=1000, rate=0.05, time=10, frequency="annual" Result: £1628.89 MONTHLY COMPOUNDING: £1000 at 5% for 10 years principal=1000, rate=0.05, time=10, frequency="monthly" Result: £1647.01 CONTINUOUS COMPOUNDING: £1000 at 5% for 10 years principal=1000, rate=0.05, time=10, frequency="continuous" Result: £1648.72""", annotations=ToolAnnotations( title="Compound Interest", readOnlyHint=True, idempotentHint=True, ), ) async def compound_interest( principal: Annotated[float, Field(description="Initial principal amount (e.g., 1000)")], rate: Annotated[float, Field(description="Annual interest rate (e.g., 0.05 for 5%)")], time: Annotated[float, Field(description="Time period in years (e.g., 10)", ge=0)], frequency: Annotated[ Literal["annual", "semi-annual", "quarterly", "monthly", "daily", "continuous"], Field(description="Compounding frequency"), ] = "annual", ) -> str: """Calculate compound interest.""" try: freq_map = { "annual": 1, "semi-annual": 2, "quarterly": 4, "monthly": 12, "daily": 365, } if frequency == "continuous": # Continuous compounding: A = Pe^(rt) final_amount = principal * math.exp(rate * time) else: # Discrete compounding: A = P(1 + r/n)^(nt) n = freq_map[frequency] final_amount = principal * (1 + rate / n) ** (n * time) interest_earned = final_amount - principal return format_result( float(final_amount), { "principal": principal, "rate": rate, "time": time, "frequency": frequency, "interest_earned": float(interest_earned), }, ) except Exception as e: raise ValueError(f"Compound interest calculation failed: {str(e)}") @mcp.tool( name="perpetuity", description="""Calculate present value of a perpetuity (infinite series of payments). A perpetuity is an annuity that continues forever. Common in: - Preferred stock dividends - Endowment funds - Real estate with infinite rental income - UK Consol bonds (historically) Formulas: Level Ordinary: PV = C / r Level Due: PV = C / r × (1 + r) Growing: PV = C / (r - g), where r > g Examples: LEVEL PERPETUITY: £1000 annual payment at 5% payment=1000, rate=0.05 Result: PV = £20,000 GROWING PERPETUITY: £1000 payment growing 3% annually at 8% discount payment=1000, rate=0.08, growth_rate=0.03 Result: PV = £20,000 PERPETUITY DUE: £1000 at period start at 5% payment=1000, rate=0.05, when='begin' Result: PV = £21,000""", annotations=ToolAnnotations( title="Perpetuity Calculations", readOnlyHint=True, idempotentHint=True, ), ) async def perpetuity( payment: Annotated[float, Field(description="Periodic payment amount (e.g., 1000)")], rate: Annotated[float, Field(description="Discount rate per period (e.g., 0.05)", gt=0)], growth_rate: Annotated[ float | None, Field( description="Payment growth rate (None or 0 for level, e.g., 0.03 for growing)", ge=0 ), ] = None, when: Annotated[ Literal["end", "begin"], Field(description="Payment timing: 'end' or 'begin'") ] = "end", ) -> str: """Calculate present value of perpetuity.""" try: # Validate inputs if rate <= 0: raise ValueError("Discount rate must be positive") if growth_rate is not None: if growth_rate < 0: raise ValueError("Growth rate cannot be negative") if growth_rate >= rate: raise ValueError( f"Growth rate ({growth_rate}) must be less than discount rate ({rate}) " "for perpetuity to have finite value" ) # Calculate present value based on type if growth_rate is not None and growth_rate > 0: # Growing perpetuity: PV = C / (r - g) pv = payment / (rate - growth_rate) perpetuity_type = "growing" elif when == "begin": # Perpetuity due (payments at beginning): PV = C/r × (1+r) pv = (payment / rate) * (1 + rate) perpetuity_type = "level_due" else: # Ordinary perpetuity (payments at end): PV = C / r pv = payment / rate perpetuity_type = "level_ordinary" # Build metadata metadata: Dict[str, Any] = { "type": perpetuity_type, "payment": payment, "rate": rate, } if growth_rate is not None: metadata["growth_rate"] = growth_rate if when != "end": metadata["when"] = when return format_result(float(pv), metadata) except Exception as e: raise ValueError(f"Perpetuity calculation failed: {str(e)}")

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/apetta/vibe-math-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server