"""Deterministic financial calculations: six functions of a dollar and friends.""" from __future__ import annotations import math from typing import Any, Literal from pydantic import BaseModel, Field from app.llm.schemas import ToolResult from app.tools.registry import ToolContext, registry Function = Literal[ "fv_lump", "pv_lump", "fv_annuity", "pv_annuity", "sinking_fund", "capital_recovery", "payment", "irr", "npv", "age_life", "unit_cost", "market_extraction", ] class FinancialArgs(BaseModel): function: Function params: dict[str, Any] = Field(default_factory=dict) def _rate_per_period(p: dict[str, Any]) -> tuple[float, int]: rate = float(p.get("rate", 0.0)) if rate > 1: # given as percent rate = rate / 100.0 periods_per_year = int(p.get("periods_per_year", 1)) n = int(p.get("periods", p.get("years", 0) * periods_per_year if "years" in p else 0)) return rate / periods_per_year, n def compute(function: str, p: dict[str, Any]) -> dict[str, Any]: if function == "age_life": ae = float(p["effective_age"]) dve = float(p["economic_life"]) cn = float(p.get("cost_new", 1.0)) ratio = ae / dve return {"formula": "D = (A_e / DVE) × C_N", "depreciation_ratio": ratio, "depreciation": ratio * cn, "depreciated_cost": cn - ratio * cn, "remaining_life": dve - ae} if function == "unit_cost": area = float(p["area"]) unit = str(p.get("unit", "m2")) cost_per_unit = float(p["cost_per_unit"]) factors = [float(x) for x in p.get("factors", [])] total = area * cost_per_unit for f in factors: total *= f return {"formula": "C_N = S × c_u × Π facteurs", "area": area, "unit": unit, "cost_new": total, "area_other_unit": area * 10.7639 if unit == "m2" else area / 10.7639} if function == "market_extraction": price = float(p["sale_price"]) land = float(p["land_value"]) cn = float(p["cost_new"]) age = float(p.get("age", 0)) or None dep = cn - (price - land) out = {"formula": "D = C_N − (Prix − V_T)", "depreciation": dep, "ratio": dep / cn} if age: out["annual_rate"] = dep / cn / age return out i, n = _rate_per_period(p) if function == "fv_lump": f = (1 + i) ** n return {"formula": "VF = VA × (1+i)^n", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function == "pv_lump": f = (1 + i) ** -n return {"formula": "VA = VF × (1+i)^-n", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function == "fv_annuity": f = n if i == 0 else ((1 + i) ** n - 1) / i return {"formula": "VF_annuité = PMT × [((1+i)^n − 1)/i]", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function == "pv_annuity": f = n if i == 0 else (1 - (1 + i) ** -n) / i return {"formula": "VA_annuité = PMT × [(1 − (1+i)^-n)/i]", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function == "sinking_fund": f = 1 / n if i == 0 else i / ((1 + i) ** n - 1) return {"formula": "FA = i / ((1+i)^n − 1)", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function in {"capital_recovery", "payment"}: f = 1 / n if i == 0 else i / (1 - (1 + i) ** -n) return {"formula": "FRC = i / (1 − (1+i)^-n)", "factor": f, "value": float(p.get("amount", 1.0)) * f} if function == "npv": flows = [float(x) for x in p["cash_flows"]] rate = float(p.get("rate", 0.0)) rate = rate / 100 if rate > 1 else rate npv = sum(cf / (1 + rate) ** t for t, cf in enumerate(flows)) return {"formula": "VAN = Σ CF_t / (1+r)^t", "value": npv} if function == "irr": flows = [float(x) for x in p["cash_flows"]] lo, hi = -0.99, 10.0 def f(r: float) -> float: return sum(cf / (1 + r) ** t for t, cf in enumerate(flows)) if f(lo) * f(hi) > 0: return {"formula": "TRI : VAN(r) = 0", "value": None, "note": "Pas de TRI unique dans l'intervalle."} for _ in range(200): mid = (lo + hi) / 2 if f(lo) * f(mid) <= 0: hi = mid else: lo = mid return {"formula": "TRI : VAN(r) = 0", "value": (lo + hi) / 2} raise ValueError(f"unknown function {function}") def _fmt(v: Any) -> str: if isinstance(v, float): if math.isnan(v): return "nan" return f"{v:,.6f}".rstrip("0").rstrip(".") if abs(v) < 1e6 else f"{v:,.2f}" return str(v) async def run(args: dict[str, Any], ctx: ToolContext) -> ToolResult: fn = args["function"] params = args["params"] try: out = compute(fn, params) except (KeyError, ValueError, ZeroDivisionError) as exc: return ToolResult(content=f"Paramètre manquant ou invalide pour {fn} : {exc}. " "Paramètres attendus : rate (décimal ou %), periods ou years, " "periods_per_year, amount ; ou effective_age/economic_life/cost_new ; " "ou area/cost_per_unit/unit/factors ; ou sale_price/land_value/" "cost_new/age ; ou cash_flows/rate.", error=True) lines = [f"{k} = {_fmt(v)}" for k, v in out.items()] content = f"Résultat {fn} (paramètres : {params}) :\n" + "\n".join(lines) return ToolResult(content=content, payload={"function": fn, "params": params, "result": out}, meta={"summary": f"{fn} : {out.get('formula', '')}"}) registry.register("financial_calc", run, FinancialArgs)