""" CFA AI Agent - Finance Calculation Tools This module contains various financial calculation functions for CFA analysis. """ import numpy as np import pandas as pd from typing import List, Dict, Union, Optional import yfinance as yf from langchain.tools import tool @tool def calculate_dcf( cash_flows: List[float], terminal_value: float, discount_rate: float ) -> Dict[str, float]: """ Calculate Discounted Cash Flow (DCF) valuation. Args: cash_flows: List of projected free cash flows terminal_value: Terminal value at end of projection period discount_rate: Weighted average cost of capital (WACC) as decimal Returns: Dictionary with NPV, terminal value present value, and total enterprise value """ try: if not cash_flows: raise ValueError("Cash flows list cannot be empty") if discount_rate <= 0: raise ValueError("Discount rate must be positive") # Calculate present value of cash flows pv_cash_flows = [] for i, cf in enumerate(cash_flows, 1): pv = cf / ((1 + discount_rate) ** i) pv_cash_flows.append(pv) # Calculate present value of terminal value years = len(cash_flows) pv_terminal = terminal_value / ((1 + discount_rate) ** years) # Total enterprise value enterprise_value = sum(pv_cash_flows) + pv_terminal return { "pv_cash_flows": sum(pv_cash_flows), "pv_terminal_value": pv_terminal, "enterprise_value": enterprise_value, "cash_flow_details": pv_cash_flows } except Exception as e: return {"error": f"DCF calculation failed: {str(e)}"} @tool def calculate_sharpe_ratio( returns: List[float], risk_free_rate: float ) -> Dict[str, float]: """ Calculate Sharpe Ratio for risk-adjusted returns. Args: returns: List of periodic returns (as decimals) risk_free_rate: Risk-free rate (as decimal) Returns: Dictionary with Sharpe ratio, average return, and standard deviation """ try: if not returns: raise ValueError("Returns list cannot be empty") if len(returns) < 2: raise ValueError("Need at least 2 return observations") returns_array = np.array(returns) # Calculate metrics avg_return = np.mean(returns_array) std_dev = np.std(returns_array, ddof=1) # Sample standard deviation excess_return = avg_return - risk_free_rate if std_dev == 0: raise ValueError("Standard deviation cannot be zero") sharpe_ratio = excess_return / std_dev return { "sharpe_ratio": sharpe_ratio, "average_return": avg_return, "standard_deviation": std_dev, "excess_return": excess_return, "risk_free_rate": risk_free_rate } except Exception as e: return {"error": f"Sharpe ratio calculation failed: {str(e)}"} @tool def compare_pe_ratios(ticker1: str, ticker2: str) -> Dict[str, Union[float, str]]: """ Compare P/E ratios of two stocks using real-time data. Args: ticker1: First stock ticker symbol ticker2: Second stock ticker symbol Returns: Dictionary with P/E ratios and comparison analysis """ try: # Fetch stock data stock1 = yf.Ticker(ticker1.upper()) stock2 = yf.Ticker(ticker2.upper()) # Get info info1 = stock1.info info2 = stock2.info # Extract P/E ratios pe1 = info1.get('trailingPE') or info1.get('forwardPE') pe2 = info2.get('trailingPE') or info2.get('forwardPE') if pe1 is None or pe2 is None: return {"error": f"Could not retrieve P/E ratios for {ticker1} or {ticker2}"} # Calculate comparison metrics pe_difference = pe1 - pe2 pe_ratio = pe1 / pe2 if pe2 != 0 else None # Determine which is more expensive comparison = "equal" if pe1 > pe2: comparison = f"{ticker1} is more expensive" elif pe1 < pe2: comparison = f"{ticker2} is more expensive" return { f"{ticker1}_pe": pe1, f"{ticker2}_pe": pe2, "pe_difference": pe_difference, "pe_ratio": pe_ratio, "comparison": comparison, f"{ticker1}_name": info1.get('longName', ticker1), f"{ticker2}_name": info2.get('longName', ticker2) } except Exception as e: return {"error": f"P/E comparison failed: {str(e)}"} @tool def calculate_beta(ticker: str, market_ticker: str = "^GSPC", period: str = "2y") -> Dict[str, float]: """ Calculate beta coefficient for a stock relative to market. Args: ticker: Stock ticker symbol market_ticker: Market index ticker (default S&P 500) period: Time period for calculation Returns: Dictionary with beta, correlation, and other metrics """ try: # Fetch data stock = yf.Ticker(ticker.upper()) market = yf.Ticker(market_ticker) # Get historical data stock_data = stock.history(period=period) market_data = market.history(period=period) if stock_data.empty or market_data.empty: raise ValueError("Could not fetch historical data") # Calculate returns stock_returns = stock_data['Close'].pct_change().dropna() market_returns = market_data['Close'].pct_change().dropna() # Align data aligned_data = pd.concat([stock_returns, market_returns], axis=1, join='inner') aligned_data.columns = ['stock', 'market'] aligned_data = aligned_data.dropna() if len(aligned_data) < 20: raise ValueError("Insufficient data points for beta calculation") # Calculate beta covariance = np.cov(aligned_data['stock'], aligned_data['market'])[0][1] market_variance = np.var(aligned_data['market'], ddof=1) beta = covariance / market_variance # Calculate correlation correlation = np.corrcoef(aligned_data['stock'], aligned_data['market'])[0][1] return { "beta": beta, "correlation": correlation, "stock_volatility": np.std(aligned_data['stock'], ddof=1), "market_volatility": np.std(aligned_data['market'], ddof=1), "data_points": len(aligned_data), "period": period } except Exception as e: return {"error": f"Beta calculation failed: {str(e)}"} @tool def calculate_wacc( cost_of_equity: float, cost_of_debt: float, tax_rate: float, market_value_equity: float, market_value_debt: float ) -> Dict[str, float]: """ Calculate Weighted Average Cost of Capital (WACC). Args: cost_of_equity: Cost of equity as decimal cost_of_debt: Cost of debt as decimal tax_rate: Corporate tax rate as decimal market_value_equity: Market value of equity market_value_debt: Market value of debt Returns: Dictionary with WACC and component calculations """ try: total_value = market_value_equity + market_value_debt if total_value <= 0: raise ValueError("Total market value must be positive") # Calculate weights weight_equity = market_value_equity / total_value weight_debt = market_value_debt / total_value # Calculate after-tax cost of debt after_tax_cost_debt = cost_of_debt * (1 - tax_rate) # Calculate WACC wacc = (weight_equity * cost_of_equity) + (weight_debt * after_tax_cost_debt) return { "wacc": wacc, "weight_equity": weight_equity, "weight_debt": weight_debt, "after_tax_cost_debt": after_tax_cost_debt, "cost_of_equity": cost_of_equity, "cost_of_debt": cost_of_debt, "tax_rate": tax_rate } except Exception as e: return {"error": f"WACC calculation failed: {str(e)}"} @tool def financial_ratios_analysis(ticker: str) -> Dict[str, Union[float, str]]: """ Perform comprehensive financial ratios analysis for a stock. Args: ticker: Stock ticker symbol Returns: Dictionary with various financial ratios and metrics """ try: stock = yf.Ticker(ticker.upper()) info = stock.info # Extract key metrics ratios = { "ticker": ticker.upper(), "company_name": info.get('longName', 'N/A'), # Valuation ratios "pe_ratio": info.get('trailingPE'), "forward_pe": info.get('forwardPE'), "price_to_book": info.get('priceToBook'), "price_to_sales": info.get('priceToSalesTrailing12Months'), "peg_ratio": info.get('pegRatio'), # Profitability ratios "profit_margin": info.get('profitMargins'), "operating_margin": info.get('operatingMargins'), "roe": info.get('returnOnEquity'), "roa": info.get('returnOnAssets'), # Financial health "current_ratio": info.get('currentRatio'), "quick_ratio": info.get('quickRatio'), "debt_to_equity": info.get('debtToEquity'), "total_debt": info.get('totalDebt'), "total_cash": info.get('totalCash'), # Market data "market_cap": info.get('marketCap'), "enterprise_value": info.get('enterpriseValue'), "beta": info.get('beta'), "52_week_high": info.get('fiftyTwoWeekHigh'), "52_week_low": info.get('fiftyTwoWeekLow'), # Dividend info "dividend_yield": info.get('dividendYield'), "payout_ratio": info.get('payoutRatio'), # Growth metrics "earnings_growth": info.get('earningsGrowth'), "revenue_growth": info.get('revenueGrowth') } # Remove None values ratios = {k: v for k, v in ratios.items() if v is not None} return ratios except Exception as e: return {"error": f"Financial ratios analysis failed: {str(e)}"}