Skip to main content
Glama
by apetta
test_financial.py34 kB
"""Tests for financial mathematics tools.""" import json import pytest @pytest.mark.asyncio async def test_financial_fv(mcp_client): """Test future value calculation.""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "fv", "rate": 0.05, "periods": 10, "payment": -100} ) data = json.loads(result.content[0].text) # FV of annuity: PMT * ((1+r)^n - 1) / r expected = 100 * (((1.05) ** 10 - 1) / 0.05) assert abs(data["result"] - expected) < 1 @pytest.mark.asyncio async def test_financial_pv(mcp_client): """Test present value calculation (annuity).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.05, "periods": 10, "payment": -100} ) data = json.loads(result.content[0].text) # PV calculation returns negative value representing outflow assert "result" in data @pytest.mark.asyncio async def test_financial_pv_lump_sum(mcp_client): """Test present value of a lump sum.""" # What is the present value of £10,000 received in 10 years at 5% interest? # PV = FV / (1 + r)^n = 10000 / (1.05^10) = 6139.13 result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.05, "periods": 10, "future_value": 10000} ) data = json.loads(result.content[0].text) expected = 10000 / (1.05 ** 10) # 6139.13 assert abs(data["result"] - (-expected)) < 0.01 # Negative because it's cash outflow @pytest.mark.asyncio async def test_financial_pv_lump_sum_zero_rate(mcp_client): """Test present value of lump sum with zero interest rate.""" # With 0% rate, PV = FV (money doesn't change value) result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.0, "periods": 10, "future_value": 10000} ) data = json.loads(result.content[0].text) assert abs(data["result"] - (-10000.0)) < 0.01 @pytest.mark.asyncio async def test_financial_pv_missing_both_params(mcp_client): """Test error when PV is calculated without future_value or payment.""" with pytest.raises(Exception) as exc_info: await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.05, "periods": 10} ) assert ("future_value" in str(exc_info.value).lower() and "payment" in str(exc_info.value).lower()) @pytest.mark.asyncio async def test_financial_pv_coupon_bond(mcp_client): """Test present value of a coupon bond (combined payment + future_value).""" # £1,000 bond paying £30 annual coupons for 10 years at 5% yield # PV = PV(face value) + PV(coupons) # PV(face value) = 1000 / (1.05^10) = £613.91 # PV(coupons) = 30 × [(1 - 1.05^-10) / 0.05] = £231.65 # Total = £845.56 result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.05, "periods": 10, "payment": 30, "future_value": 1000} ) data = json.loads(result.content[0].text) # PV of lump sum component pv_face_value = 1000 / (1.05 ** 10) # 613.91 # PV of annuity component pv_coupons = 30 * ((1 - (1.05 ** -10)) / 0.05) # 231.65 expected = -(pv_face_value + pv_coupons) # -845.56 (negative = outflow) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_financial_npv(mcp_client): """Test net present value calculation.""" cash_flows = [-1000, 300, 400, 500, 200] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "npv", "rate": 0.1, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) assert "result" in data @pytest.mark.asyncio async def test_compound_interest_annual(mcp_client): """Test compound interest with annual compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 1000, "rate": 0.05, "time": 10, "frequency": "annual"}, ) data = json.loads(result.content[0].text) assert "result" in data expected = 1000 * (1.05**10) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_compound_interest_continuous(mcp_client): """Test compound interest with continuous compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 1000, "rate": 0.05, "time": 10, "frequency": "continuous"}, ) data = json.loads(result.content[0].text) assert "result" in data # Continuous: A = Pe^(rt) import math expected = 1000 * math.exp(0.05 * 10) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_financial_pmt_basic(mcp_client): """Test payment (PMT) calculation for a loan.""" # Loan: $10,000 at 5% for 12 periods result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.05, "periods": 12, "present_value": -10000}, ) data = json.loads(result.content[0].text) # PMT should be positive (payment outflow) assert data["result"] > 0 # Expected PMT ≈ 1128.25 assert 1100 < data["result"] < 1150 @pytest.mark.asyncio async def test_financial_pmt_zero_rate(mcp_client): """Test PMT calculation with zero interest rate.""" # With 0% rate, PMT = PV / periods result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.0, "periods": 10, "present_value": -1000}, ) data = json.loads(result.content[0].text) # PMT = 1000 / 10 = 100 assert abs(data["result"] - 100.0) < 0.01 @pytest.mark.asyncio async def test_financial_pmt_missing_params(mcp_client): """Test error when required parameters are missing for PMT.""" with pytest.raises(Exception) as exc_info: await mcp_client.call_tool("financial_calcs", {"calculation": "pmt", "rate": 0.05}) assert "requires" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_financial_irr_simple_investment(mcp_client): """Test IRR for a simple investment.""" # Initial investment of -$1000, returns of $500, $400, $300, $200 cash_flows = [-1000, 500, 400, 300, 200] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "irr", "rate": 0.1, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) # IRR should be positive for profitable investment assert data["result"] > 0 # Expected IRR approximately 0.149 (14.9%) assert 0.12 < data["result"] < 0.18 @pytest.mark.asyncio async def test_financial_irr_complex_cash_flows(mcp_client): """Test IRR with more complex cash flow pattern.""" # Project with initial investment and varying returns cash_flows = [-5000, 1000, 1500, 2000, 2500, 1000] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "irr", "rate": 0.1, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) # IRR should be positive assert data["result"] > 0 assert data["result"] < 1.0 # Should be reasonable (< 100%) @pytest.mark.asyncio async def test_financial_irr_negative_return(mcp_client): """Test IRR for a losing investment.""" # Investment that loses money cash_flows = [-1000, 100, 150, 200, 100] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "irr", "rate": 0.1, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) # IRR should be negative for unprofitable investment assert data["result"] < 0 @pytest.mark.asyncio async def test_financial_irr_insufficient_cash_flows(mcp_client): """Test error when IRR has insufficient cash flows.""" with pytest.raises(Exception) as exc_info: await mcp_client.call_tool( "financial_calcs", {"calculation": "irr", "rate": 0.1, "cash_flows": [-1000]} ) assert "at least 2" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_financial_irr_convergence(mcp_client): """Test IRR convergence with standard cash flows.""" # Well-behaved cash flows that should converge quickly # Initial investment of $10,000, returns of $3,000 for 5 years cash_flows = [-10000, 3000, 3000, 3000, 3000, 3000] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "irr", "rate": 0.1, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) # Expected IRR: 15.24% (from audit and verified calculation) assert abs(data["result"] - 0.1524) < 0.0001 @pytest.mark.asyncio async def test_financial_fv_zero_rate(mcp_client): """Test future value calculation with zero interest rate.""" # With 0% rate, FV = PV + (PMT × periods) result = await mcp_client.call_tool( "financial_calcs", {"calculation": "fv", "rate": 0.0, "periods": 10, "payment": -100} ) data = json.loads(result.content[0].text) # FV = 100 × 10 = 1000 assert abs(data["result"] - 1000.0) < 0.01 @pytest.mark.asyncio async def test_financial_pv_zero_rate(mcp_client): """Test present value calculation with zero interest rate.""" # With 0% rate, PV = PMT × periods result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.0, "periods": 10, "payment": -100} ) data = json.loads(result.content[0].text) # PV = 100 × 10 = 1000 assert abs(data["result"] - 1000.0) < 0.01 @pytest.mark.asyncio async def test_financial_npv_missing_cash_flows(mcp_client): """Test error when NPV is calculated without cash flows.""" with pytest.raises(Exception) as exc_info: await mcp_client.call_tool("financial_calcs", {"calculation": "npv", "rate": 0.1}) assert "requires" in str(exc_info.value).lower() or "cash_flows" in str(exc_info.value).lower() @pytest.mark.asyncio async def test_compound_interest_semi_annual(mcp_client): """Test compound interest with semi-annual compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 1000, "rate": 0.06, "time": 5, "frequency": "semi-annual"}, ) data = json.loads(result.content[0].text) assert "result" in data # Semi-annual: n=2, A = 1000 * (1 + 0.06/2)^(2*5) expected = 1000 * (1 + 0.06 / 2) ** (2 * 5) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_compound_interest_quarterly(mcp_client): """Test compound interest with quarterly compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 1000, "rate": 0.08, "time": 3, "frequency": "quarterly"}, ) data = json.loads(result.content[0].text) assert "result" in data # Quarterly: n=4, A = 1000 * (1 + 0.08/4)^(4*3) expected = 1000 * (1 + 0.08 / 4) ** (4 * 3) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_compound_interest_monthly(mcp_client): """Test compound interest with monthly compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 5000, "rate": 0.05, "time": 2, "frequency": "monthly"}, ) data = json.loads(result.content[0].text) assert "result" in data # Monthly: n=12, A = 5000 * (1 + 0.05/12)^(12*2) expected = 5000 * (1 + 0.05 / 12) ** (12 * 2) assert abs(data["result"] - expected) < 0.01 @pytest.mark.asyncio async def test_compound_interest_daily(mcp_client): """Test compound interest with daily compounding.""" result = await mcp_client.call_tool( "compound_interest", {"principal": 2000, "rate": 0.04, "time": 1, "frequency": "daily"}, ) data = json.loads(result.content[0].text) assert "result" in data # Daily: n=365, A = 2000 * (1 + 0.04/365)^(365*1) expected = 2000 * (1 + 0.04 / 365) ** (365 * 1) assert abs(data["result"] - expected) < 0.01 # ============================================================================ # Comprehensive Real-World TVM Test Scenarios # ============================================================================ # Section 1: Basic Lump Sum Problems @pytest.mark.asyncio async def test_financial_fv_sales_growth(mcp_client): """Test FV: $100M at 8% for 10 years (Problem 1.1).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "fv", "rate": 0.08, "periods": 10, "present_value": -100, "payment": 0} ) data = json.loads(result.content[0].text) # Expected: $215.89 million assert abs(data["result"] - 215.89) < 0.01 @pytest.mark.asyncio async def test_financial_pv_government_bond(mcp_client): """Test PV: $1000 in 3 years at 4% (Problem 1.2).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.04, "periods": 3, "future_value": 1000, "payment": 0} ) data = json.loads(result.content[0].text) # Expected: -$889.00 (negative = cash outflow to purchase) assert abs(data["result"] - (-889.00)) < 0.01 @pytest.mark.asyncio async def test_financial_rate_treasury_bond(mcp_client): """Test rate solving: $613.81 → $1000 in 10 years (Problem 1.3).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "rate", "periods": 10, "present_value": -613.81, "future_value": 1000} ) data = json.loads(result.content[0].text) # Expected: 5.00% assert abs(data["result"] - 0.05) < 0.0001 # Section 2: Annuity Problems @pytest.mark.asyncio async def test_financial_pv_annuity_payment(mcp_client): """Test PV of annuity: $1000/year for 5 years at 6% (Problem 2.1).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.06, "periods": 5, "payment": -1000, "future_value": 0} ) data = json.loads(result.content[0].text) # Expected: 4212.36 (amount you'd pay to receive the annuity) assert abs(data["result"] - 4212.36) < 0.01 @pytest.mark.asyncio async def test_financial_pmt_withdrawal(mcp_client): """Test PMT: Withdraw from $200k at 6% over 15 years (Problem 2.2).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.06, "periods": 15, "present_value": -200000, "future_value": 0} ) data = json.loads(result.content[0].text) # Expected: $20,592.55 assert abs(data["result"] - 20592.55) < 0.01 @pytest.mark.asyncio async def test_financial_pmt_mortgage(mcp_client): """Test PMT: $190k mortgage at 7%/12 for 360 months (Problem 2.3).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.07/12, "periods": 360, "present_value": -190000, "future_value": 0} ) data = json.loads(result.content[0].text) # Expected: $1,264 (approximately) assert abs(data["result"] - 1264) < 1 # Section 3: Bond Pricing Problems @pytest.mark.asyncio async def test_financial_pv_zero_coupon_bond_100k(mcp_client): """Test PV: Zero-coupon bond $100k at 10% for 4 years (Problem 3.1).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.10, "periods": 4, "future_value": 100000, "payment": 0} ) data = json.loads(result.content[0].text) # Expected: -$68,301 assert abs(data["result"] - (-68301)) < 1 @pytest.mark.asyncio async def test_financial_pv_coupon_bond_annual(mcp_client): """Test PV: $100k bond, $7k coupons, 9% yield, 15 years (Problem 3.2).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.09, "periods": 15, "payment": 7000, "future_value": 100000} ) data = json.loads(result.content[0].text) # Expected: -$83,879 (bond trades at discount) assert abs(data["result"] - (-83879)) < 1 @pytest.mark.asyncio async def test_financial_pv_coupon_bond_semiannual(mcp_client): """Test PV: $100k bond, $4k semi-annual coupons, 3.5% rate, 10 periods (Problem 3.3).""" result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.07/2, "periods": 10, "payment": 4000, "future_value": 100000} ) data = json.loads(result.content[0].text) # Expected: -$104,158.30 (bond trades at premium) # Corrected from audit: Previous comment had wrong expected value of -$104,376 assert abs(data["result"] - (-104158.30)) < 1 @pytest.mark.asyncio async def test_financial_pv_bond_discount(mcp_client): """Test PV: $1000 bond, 5% coupon, 6% yield, semi-annual (Problem 3.4).""" # Semi-annual: $25 coupon, 20 periods, 3% per period result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.03, "periods": 20, "payment": 25, "future_value": 1000} ) data = json.loads(result.content[0].text) # Expected: -$925.61 (trades at discount) assert abs(data["result"] - (-925.61)) < 0.01 @pytest.mark.asyncio async def test_financial_pv_openstax_bond(mcp_client): """Test PV: OpenStax 4% coupon bond, 5% YTM, semi-annual (Problem 3.5).""" # Semi-annual: $20 coupon, 30 periods, 2.5% per period result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.025, "periods": 30, "payment": 20, "future_value": 1000} ) data = json.loads(result.content[0].text) # Expected: -$895.35 assert abs(data["result"] - (-895.35)) < 0.01 # Section 4: Uneven Cash Flow Problems @pytest.mark.asyncio async def test_financial_npv_uneven_cashflows(mcp_client): """Test NPV: 10-year uneven cash flow stream at 8% (Problem 4.1).""" cash_flows = [0, 10000, 10000, 10000, 12000, 12000, 12000, 12000, 15000, 15000, 15000] result = await mcp_client.call_tool( "financial_calcs", {"calculation": "npv", "rate": 0.08, "cash_flows": cash_flows} ) data = json.loads(result.content[0].text) # Expected: $79,877 assert abs(data["result"] - 79877) < 1 @pytest.mark.asyncio async def test_financial_rate_savings_from_zero(mcp_client): """Test rate: Savings from zero with quarterly contributions (Problem: PV=0 regression).""" # Start with £0, save £30k quarterly, reach £550k in 4 years (16 quarters) result = await mcp_client.call_tool( "financial_calcs", {"calculation": "rate", "periods": 16, "payment": -30000, "present_value": 0, "future_value": 550000} ) data = json.loads(result.content[0].text) # Expected: ~1.79% per quarter assert abs(data["result"] - 0.0179) < 0.001 # Verify PV was included in metadata assert data["present_value"] == 0 @pytest.mark.asyncio async def test_financial_rate_annuity_due(mcp_client): """Test rate: Annuity due with payments at beginning (when='begin').""" # SmartChoice laptop: $399 cash or $59.88/month in advance for 12 months result = await mcp_client.call_tool( "financial_calcs", {"calculation": "rate", "periods": 12, "payment": 59.88, "present_value": -399, "future_value": 0, "when": "begin"} ) data = json.loads(result.content[0].text) # Expected: ~13.1% monthly (~157% APR) for annuity due assert 0.12 < data["result"] < 0.14 # Between 12% and 14% monthly # Verify when parameter in metadata assert data["when"] == "begin" @pytest.mark.asyncio async def test_financial_pmt_annuity_due(mcp_client): """Test PMT: Payment calculation with annuity due (when='begin').""" # Lease with payments at start of month result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.005, "periods": 36, "present_value": -20000, "future_value": 0, "when": "begin"} ) data = json.loads(result.content[0].text) # Payment should be slightly less than ordinary annuity due to earlier compounding assert data["result"] > 0 # Annuity due payment should be less than ordinary annuity assert 600 < data["result"] < 610 # Ordinary: ~$608.44, Annuity due: ~$605.41 # ============================================================================ # CFA and Textbook Verified Test Scenarios (From Independent Audit) # ============================================================================ @pytest.mark.asyncio async def test_financial_deferred_annuity_cfa(mcp_client): """Test deferred annuity from CFA Level 1 problem set. Source: CFA Level 1 - SlideShare Problem A Problem: Calculate PV of instrument that pays $20,000 at end of each year for 4 years starting 3 years from now. Discount rate: 8% Solution: Step 1: Find PV at year 3 (when annuity starts) PV₃ = 20,000 × [(1 - 1.08⁻⁴) / 0.08] = $66,242.54 Step 2: Discount back to today PV₀ = 66,242.54 / 1.08³ = $52,585.46 """ # Step 1: PV at year 3 (when annuity starts) result_at_start = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.08, "periods": 4, "payment": -20000, "future_value": 0} ) data_at_start = json.loads(result_at_start.content[0].text) assert abs(data_at_start["result"] - 66242.54) < 0.01 # Step 2: Discount back to today result_today = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.08, "periods": 3, "future_value": -data_at_start["result"], "payment": 0} ) data_today = json.loads(result_today.content[0].text) assert abs(data_today["result"] - 52585.46) < 0.01 @pytest.mark.asyncio async def test_financial_negative_interest_rate_swiss_bond(mcp_client): """Test negative interest rate scenario from CFA curriculum. Source: AnalystPrep CFA Level 1 - Time Value of Money Problem: Swiss government 15-year bond at -0.08% yield. PV of CHF 100 face value at issuance? Solution: N = 15 years, I/Y = -0.08%, FV = 100, PMT = 0 (zero-coupon) PV = 100 / (1 - 0.0008)^15 = CHF 101.20 Note: PV > FV when rate is negative """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": -0.0008, "periods": 15, "future_value": 100, "payment": 0} ) data = json.loads(result.content[0].text) # PV should be greater than FV (negative result represents cost) assert data["result"] < -101.00 # Negative because it's cash outflow assert data["result"] > -102.00 # More precise check assert abs(data["result"] - (-101.20)) < 0.01 @pytest.mark.asyncio async def test_financial_annuity_due_cfa_problem(mcp_client): """Test annuity due from CFA Level 1 end-of-chapter question. Source: CFA Level 1 PDF - SlideShare Problem: Investment pays 300 Euros annually for 5 years, first payment today. PV at 4% discount rate is closest to: A) 1336 B) 1389 C) 1625 Calculator Solution: N = 5, I/YR = 4%, PMT = 300, FV = 0, BGN mode CPT PV = -1,388.968567 Answer: B) 1389 """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.04, "periods": 5, "payment": -300, "future_value": 0, "when": "begin"} ) data = json.loads(result.content[0].text) assert abs(data["result"] - 1388.97) < 0.01 # Verify when parameter in metadata assert data["when"] == "begin" @pytest.mark.asyncio async def test_financial_balloon_payment_mortgage(mcp_client): """Test balloon mortgage from Omnicalculator validation. Source: Omnicalculator - Balloon Payment Calculator Problem: Jack's $100,000 balloon mortgage: - Term: 5 years - Amortisation: 30 years - Interest rate: 7% - Monthly payment: $665.30 - Balloon payment (after 5 years): $94,131.59 Solution: Step 1: Calculate monthly payment based on 30-year amortisation N = 360, I/Y = 7/12, PV = -100,000, FV = 0 CPT PMT = $665.30 Step 2: Calculate remaining balance after 60 payments N = 60, I/Y = 7/12, PMT = $665.30, PV = -100,000 CPT FV = $94,131.59 """ # Step 1: Get monthly payment result_pmt = await mcp_client.call_tool( "financial_calcs", {"calculation": "pmt", "rate": 0.07/12, "periods": 360, "present_value": -100000, "future_value": 0} ) data_pmt = json.loads(result_pmt.content[0].text) assert abs(data_pmt["result"] - 665.30) < 0.01 # Step 2: Calculate balloon (remaining balance after 60 payments) result_balloon = await mcp_client.call_tool( "financial_calcs", {"calculation": "fv", "rate": 0.07/12, "periods": 60, "payment": 665.30, "present_value": -100000} ) data_balloon = json.loads(result_balloon.content[0].text) assert abs(data_balloon["result"] - 94131.59) < 1.00 @pytest.mark.asyncio async def test_financial_deferred_annuity_changing_rates(mcp_client): """Test deferred annuity with changing interest rates. Source: Vaia Corporate Finance Textbook Problem: 15-year annuity paying $750/year. First payment in year 6. Interest rate: 12% for years 1-5, then 15% thereafter. Solution: Step 1: PV at year 5 (start of payments - 1) using 15% rate PV₅ = 750 × [(1 - 1.15⁻¹⁵) / 0.15] Step 2: Discount back to today using 12% rate PV₀ = PV₅ / 1.12⁵ """ # Step 1: PV at year 5 (one year before first payment) using 15% rate result_at_yr5 = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.15, "periods": 15, "payment": -750, "future_value": 0} ) data_at_yr5 = json.loads(result_at_yr5.content[0].text) # PV at year 5 should be approximately $4,372.56 assert 4300 < data_at_yr5["result"] < 4400 # Step 2: Discount back 5 years at 12% result_today = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.12, "periods": 5, "future_value": -data_at_yr5["result"], "payment": 0} ) data_today = json.loads(result_today.content[0].text) # Final PV should be approximately $2,481 assert 2400 < data_today["result"] < 2550 @pytest.mark.asyncio async def test_financial_mixed_cashflows_cfa(mcp_client): """Test mixed cash flows from CFA problem set. Source: CFA Level 1 PDF - Problem B Problem: Calculate PV of instrument paying $20,000 at end of years 1, 2, 3, and $30,000 at end of year 4. Discount rate: 8% Solution Method 2 (Annuity + lump sum): PV = 20,000 × [(1 - 1.08⁻³) / 0.08] + 30,000 / 1.08⁴ PV = 51,542.13 + 22,050.70 = $73,592.84 """ # Annuity component (3 payments of 20,000 received) # Use positive payment since we RECEIVE these cash flows result_annuity = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.08, "periods": 3, "payment": 20000, "future_value": 0} ) data_annuity = json.loads(result_annuity.content[0].text) # Lump sum component (30,000 at year 4 received) result_lumpsum = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.08, "periods": 4, "future_value": 30000, "payment": 0} ) data_lumpsum = json.loads(result_lumpsum.content[0].text) # Total PV (take absolute values since we want the instrument's value) total_pv = abs(data_annuity["result"]) + abs(data_lumpsum["result"]) assert abs(total_pv - 73592.83) < 0.01 @pytest.mark.asyncio async def test_financial_bond_semiannual_cfa(mcp_client): """Test semi-annual coupon bond from CFA curriculum. Source: AnalystPrep CFA Level 1 - Example 2 Problem: 2-year bond, face value $1000, 6% annual coupon (paid semi-annually), market discount rate 5%. Price closest to? Calculator Solution: N = 4 (2 years × 2 periods) I/Y = 2.5 (5% / 2) PMT = 30 (6% × 1000 / 2) FV = 1000 CPT PV = -1,018.81 """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.025, "periods": 4, "payment": 30, "future_value": 1000} ) data = json.loads(result.content[0].text) assert abs(data["result"] - (-1018.81)) < 0.01 @pytest.mark.asyncio async def test_financial_bond_at_discount_cfa(mcp_client): """Test bond trading at discount from CFA curriculum. Source: AnalystPrep CFA Level 1 Problem: Calculate bond price: - Face value: 100 - Coupon rate: 7.8% paid semi-annually (3.9 per period) - Periods: 20 (10 years) - Yield: 8.5% annually (4.25% per period) Calculator Solution: N = 20, I/Y = 4.25, PMT = 3.9, FV = 100 CPT PV = -95.35 """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.0425, "periods": 20, "payment": 3.9, "future_value": 100} ) data = json.loads(result.content[0].text) assert abs(data["result"] - (-95.35)) < 0.01 @pytest.mark.asyncio async def test_financial_zero_coupon_fv_cfa(mcp_client): """Test zero-coupon FV calculation from CFA curriculum. Source: AnalystPrep CFA Level 1 - Example 1 Problem: What is the future value of $8,000 invested for 4 years at 8% interest? Calculator Solution: N = 4, I/Y = 8, PV = -8,000, PMT = 0 CPT FV = 10,883.91 """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "fv", "rate": 0.08, "periods": 4, "present_value": -8000, "payment": 0} ) data = json.loads(result.content[0].text) assert abs(data["result"] - 10883.91) < 0.01 @pytest.mark.asyncio async def test_financial_ordinary_annuity_cfa_validation(mcp_client): """Test validation: Given PV from CFA curriculum. Source: CFA Level 1 PDF - Given Fact Statement: "At 5% interest rate per year compounded annually, the Present Value of a 10-year ordinary annuity with annual payments of $2000 is $15443.47." This is a validation test to verify our calculations match CFA standards. """ result = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.05, "periods": 10, "payment": -2000, "future_value": 0, "when": "end"} ) data = json.loads(result.content[0].text) assert abs(data["result"] - 15443.47) < 0.01 # ============================================================================ # Perpetuity and Growing Annuity Tests (New Tools/Features) # ============================================================================ @pytest.mark.asyncio async def test_financial_perpetuity_monthly_cfa(mcp_client): """Test monthly perpetuity from CFA curriculum. Source: Soleadea CFA Level 1 - TVM Practical Problems Problem: How much is a promise of receiving perpetually a sum of USD 1,000 each month worth now if the stated annual interest rate is 6%? Solution: Monthly rate: 6% / 12 = 0.5% = 0.005 PV = Payment / rate = 1,000 / 0.005 = $200,000 """ result = await mcp_client.call_tool( "perpetuity", {"payment": 1000, "rate": 0.005} ) data = json.loads(result.content[0].text) assert "result" in data assert abs(data["result"] - 200000.00) < 0.01 # Verify metadata assert data["type"] == "level_ordinary" assert data["payment"] == 1000 assert data["rate"] == 0.005 @pytest.mark.asyncio async def test_financial_perpetuity_quarterly(mcp_client): """Test quarterly perpetuity from corporate finance textbook. Source: Vaia Corporate Finance Textbook Problem: A prestigious investment bank designed a new security that pays a quarterly dividend of $5 in perpetuity. The first dividend occurs one quarter from today. What is the price of the security if the stated annual interest rate is 7 percent, compounded quarterly? Solution: Quarterly rate: 7% / 4 = 1.75% = 0.0175 PV = 5 / 0.0175 = $285.71 """ result = await mcp_client.call_tool( "perpetuity", {"payment": 5, "rate": 0.0175} ) data = json.loads(result.content[0].text) assert "result" in data assert abs(data["result"] - 285.71) < 0.01 # Verify metadata assert data["type"] == "level_ordinary" @pytest.mark.asyncio async def test_financial_growing_annuity_salary(mcp_client): """Test growing annuity using extended math_financial_calcs tool. Source: Vaia Corporate Finance - Problem 53 (Tom Adams) Problem: Tom Adams has received a job offer: - Base salary: $45,000 - Initial bonus: $10,000 (paid immediately) - Salary growth rate: 3.5% per year - Annual bonus: 10% of salary - Working years: 25 - Discount rate: 12% What is the present value of Tom's job offer? Solution: 1. PV of salary: $45,000 growing at 3.5% for 25 years at 12% 2. PV of bonus: 10% of growing salary stream 3. Initial bonus: $10,000 (no discounting) """ # Salary component result_salary = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.12, "periods": 25, "payment": -45000, "growth_rate": 0.035} ) data_salary = json.loads(result_salary.content[0].text) # Bonus component (10% of salary) result_bonus = await mcp_client.call_tool( "financial_calcs", {"calculation": "pv", "rate": 0.12, "periods": 25, "payment": -4500, "growth_rate": 0.035} ) data_bonus = json.loads(result_bonus.content[0].text) # Total PV total_pv = data_salary["result"] + data_bonus["result"] + 10000 # Expected: Salary component ≈ $455,816 (verified calculation) # Formula: PV = C/(r-g) × [1 - (1+g)^n/(1+r)^n] # PV = 45000/(0.12-0.035) × [1 - 1.035^25/1.12^25] = $455,816 assert 454000 < data_salary["result"] < 457000 # Bonus component (10% of salary) assert 45400 < data_bonus["result"] < 45700 # Total: salary + bonus + $10k initial = ~$511,000 assert 509000 < total_pv < 513000 # Verify growth_rate in metadata assert data_salary["growth_rate"] == 0.035 assert data_bonus["growth_rate"] == 0.035

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