Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- app.py +404 -0
- data_fetcher.py +232 -0
- requirements.txt +5 -0
- utils.py +792 -0
- valuation.py +88 -0
app.py
ADDED
|
@@ -0,0 +1,404 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import numpy as np
|
| 5 |
+
from data_fetcher import DataFetcher
|
| 6 |
+
from valuation import DCFValuation
|
| 7 |
+
from utils import (format_number, format_metrics_table, create_price_chart,
|
| 8 |
+
format_financial_statement, create_financial_chart, create_spider_chart,
|
| 9 |
+
prepare_financial_table, create_key_metrics_chart, create_growth_chart,
|
| 10 |
+
create_margin_chart, create_multi_year_growth_chart, create_ratio_chart)
|
| 11 |
+
|
| 12 |
+
# Initialize classes
|
| 13 |
+
data_fetcher = DataFetcher()
|
| 14 |
+
dcf_valuation = DCFValuation()
|
| 15 |
+
|
| 16 |
+
def analyze_stock(ticker, growth_rate, discount_rate, projection_years, format_type):
|
| 17 |
+
# Close any existing matplotlib figures to prevent memory issues
|
| 18 |
+
plt.close('all')
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
# Validate inputs
|
| 22 |
+
if not ticker:
|
| 23 |
+
return {"error": "Please enter a ticker symbol"}, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None
|
| 24 |
+
|
| 25 |
+
ticker = ticker.upper().strip()
|
| 26 |
+
|
| 27 |
+
# Fetch data
|
| 28 |
+
metrics = data_fetcher.get_key_metrics(ticker)
|
| 29 |
+
price_history = data_fetcher.get_price_history(ticker)
|
| 30 |
+
financial_statements = data_fetcher.get_financial_statements(ticker)
|
| 31 |
+
|
| 32 |
+
# Get currency symbol
|
| 33 |
+
currency_symbol = metrics.get('Currency Symbol', '$')
|
| 34 |
+
|
| 35 |
+
# Format metrics for display
|
| 36 |
+
formatted_metrics = format_metrics_table(metrics)
|
| 37 |
+
|
| 38 |
+
# Calculate DCF valuation
|
| 39 |
+
try:
|
| 40 |
+
fcf = data_fetcher.get_free_cash_flow(ticker)
|
| 41 |
+
shares_outstanding = metrics.get('Shares Outstanding', None)
|
| 42 |
+
|
| 43 |
+
if fcf is not None and shares_outstanding and shares_outstanding != 'N/A':
|
| 44 |
+
company_value = dcf_valuation.calculate_dcf(
|
| 45 |
+
fcf=fcf,
|
| 46 |
+
growth_rate=growth_rate/100, # Convert percentage to decimal
|
| 47 |
+
discount_rate=discount_rate/100, # Convert percentage to decimal
|
| 48 |
+
years=int(projection_years)
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
# Format valuation results
|
| 52 |
+
valuation_results = {
|
| 53 |
+
"Company Value": format_number(company_value, currency_symbol=currency_symbol, format_type=format_type),
|
| 54 |
+
"Current Market Cap": formatted_metrics.get("Market Cap", "N/A"),
|
| 55 |
+
"Free Cash Flow": format_number(fcf, currency_symbol=currency_symbol, format_type=format_type),
|
| 56 |
+
"Growth Rate": f"{growth_rate:.1f}%",
|
| 57 |
+
"Discount Rate": f"{discount_rate:.1f}%",
|
| 58 |
+
"Projection Years": int(projection_years)
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if shares_outstanding:
|
| 62 |
+
per_share_value = dcf_valuation.calculate_per_share_value(company_value, shares_outstanding)
|
| 63 |
+
current_price = metrics.get('Current Price', None)
|
| 64 |
+
|
| 65 |
+
valuation_results["Estimated Share Value"] = f"{currency_symbol}{per_share_value:.2f}"
|
| 66 |
+
valuation_results["Current Share Price"] = f"{currency_symbol}{current_price:.2f}" if current_price else "N/A"
|
| 67 |
+
|
| 68 |
+
if current_price:
|
| 69 |
+
upside = (per_share_value / current_price - 1) * 100
|
| 70 |
+
valuation_results["Potential Upside"] = f"{upside:.1f}%"
|
| 71 |
+
else:
|
| 72 |
+
valuation_results = {"error": f"Insufficient data for DCF valuation. FCF: {fcf}, Shares: {shares_outstanding}"}
|
| 73 |
+
except Exception as e:
|
| 74 |
+
valuation_results = {"error": f"Valuation error: {str(e)}"}
|
| 75 |
+
|
| 76 |
+
# Create price chart
|
| 77 |
+
price_fig = create_price_chart(price_history)
|
| 78 |
+
|
| 79 |
+
# Create spider chart with enhanced metrics
|
| 80 |
+
spider_fig = create_spider_chart(metrics, f"{ticker} Financial Metrics")
|
| 81 |
+
|
| 82 |
+
# Prepare financial statements for display - Annual
|
| 83 |
+
annual_income_table = prepare_financial_table(
|
| 84 |
+
financial_statements['income_stmt'],
|
| 85 |
+
currency_symbol=currency_symbol,
|
| 86 |
+
format_type=format_type
|
| 87 |
+
)
|
| 88 |
+
|
| 89 |
+
annual_balance_table = prepare_financial_table(
|
| 90 |
+
financial_statements['balance_sheet'],
|
| 91 |
+
currency_symbol=currency_symbol,
|
| 92 |
+
format_type=format_type
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
annual_cash_flow_table = prepare_financial_table(
|
| 96 |
+
financial_statements['cash_flow'],
|
| 97 |
+
currency_symbol=currency_symbol,
|
| 98 |
+
format_type=format_type
|
| 99 |
+
)
|
| 100 |
+
|
| 101 |
+
# Prepare financial statements for display - Quarterly
|
| 102 |
+
quarterly_income_table = prepare_financial_table(
|
| 103 |
+
financial_statements['quarterly_income_stmt'],
|
| 104 |
+
currency_symbol=currency_symbol,
|
| 105 |
+
format_type=format_type
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
quarterly_balance_table = prepare_financial_table(
|
| 109 |
+
financial_statements['quarterly_balance_sheet'],
|
| 110 |
+
currency_symbol=currency_symbol,
|
| 111 |
+
format_type=format_type
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
quarterly_cash_flow_table = prepare_financial_table(
|
| 115 |
+
financial_statements['quarterly_cash_flow'],
|
| 116 |
+
currency_symbol=currency_symbol,
|
| 117 |
+
format_type=format_type
|
| 118 |
+
)
|
| 119 |
+
|
| 120 |
+
# Create financial charts with error handling
|
| 121 |
+
try:
|
| 122 |
+
income_fig = create_financial_chart(financial_statements['income_stmt'],
|
| 123 |
+
f"{ticker} Income Statement", 'bar')
|
| 124 |
+
except Exception as e:
|
| 125 |
+
income_fig = plt.figure(figsize=(10, 6))
|
| 126 |
+
plt.text(0.5, 0.5, f"Error creating income statement chart: {str(e)}",
|
| 127 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 128 |
+
plt.axis('off')
|
| 129 |
+
|
| 130 |
+
try:
|
| 131 |
+
balance_fig = create_financial_chart(financial_statements['balance_sheet'],
|
| 132 |
+
f"{ticker} Balance Sheet", 'bar')
|
| 133 |
+
except Exception as e:
|
| 134 |
+
balance_fig = plt.figure(figsize=(10, 6))
|
| 135 |
+
plt.text(0.5, 0.5, f"Error creating balance sheet chart: {str(e)}",
|
| 136 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 137 |
+
plt.axis('off')
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
cash_flow_fig = create_financial_chart(financial_statements['cash_flow'],
|
| 141 |
+
f"{ticker} Cash Flow", 'bar')
|
| 142 |
+
except Exception as e:
|
| 143 |
+
cash_flow_fig = plt.figure(figsize=(10, 6))
|
| 144 |
+
plt.text(0.5, 0.5, f"Error creating cash flow chart: {str(e)}",
|
| 145 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 146 |
+
plt.axis('off')
|
| 147 |
+
|
| 148 |
+
# Create quarterly financial charts with error handling
|
| 149 |
+
try:
|
| 150 |
+
q_income_fig = create_financial_chart(financial_statements['quarterly_income_stmt'],
|
| 151 |
+
f"{ticker} Quarterly Income Statement", 'bar')
|
| 152 |
+
except Exception as e:
|
| 153 |
+
q_income_fig = plt.figure(figsize=(10, 6))
|
| 154 |
+
plt.text(0.5, 0.5, f"Quarterly income data not available",
|
| 155 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 156 |
+
plt.axis('off')
|
| 157 |
+
|
| 158 |
+
try:
|
| 159 |
+
q_balance_fig = create_financial_chart(financial_statements['quarterly_balance_sheet'],
|
| 160 |
+
f"{ticker} Quarterly Balance Sheet", 'bar')
|
| 161 |
+
except Exception as e:
|
| 162 |
+
q_balance_fig = plt.figure(figsize=(10, 6))
|
| 163 |
+
plt.text(0.5, 0.5, f"Quarterly balance sheet data not available",
|
| 164 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 165 |
+
plt.axis('off')
|
| 166 |
+
|
| 167 |
+
try:
|
| 168 |
+
q_cash_flow_fig = create_financial_chart(financial_statements['quarterly_cash_flow'],
|
| 169 |
+
f"{ticker} Quarterly Cash Flow", 'bar')
|
| 170 |
+
except Exception as e:
|
| 171 |
+
q_cash_flow_fig = plt.figure(figsize=(10, 6))
|
| 172 |
+
plt.text(0.5, 0.5, f"Quarterly cash flow data not available",
|
| 173 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 174 |
+
plt.axis('off')
|
| 175 |
+
|
| 176 |
+
# Create additional analysis charts with error handling
|
| 177 |
+
try:
|
| 178 |
+
revenue_growth_fig = create_growth_chart(
|
| 179 |
+
financial_statements['income_stmt'],
|
| 180 |
+
'Total Revenue',
|
| 181 |
+
f"{ticker} Revenue Growth"
|
| 182 |
+
)
|
| 183 |
+
except Exception as e:
|
| 184 |
+
revenue_growth_fig = plt.figure(figsize=(10, 6))
|
| 185 |
+
plt.text(0.5, 0.5, f"Revenue growth data not available",
|
| 186 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 187 |
+
plt.axis('off')
|
| 188 |
+
|
| 189 |
+
try:
|
| 190 |
+
margin_fig = create_margin_chart(
|
| 191 |
+
financial_statements['income_stmt'],
|
| 192 |
+
f"{ticker} Margin Analysis"
|
| 193 |
+
)
|
| 194 |
+
except Exception as e:
|
| 195 |
+
margin_fig = plt.figure(figsize=(10, 6))
|
| 196 |
+
plt.text(0.5, 0.5, f"Margin analysis data not available",
|
| 197 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 198 |
+
plt.axis('off')
|
| 199 |
+
|
| 200 |
+
# Create key metrics charts with error handling
|
| 201 |
+
try:
|
| 202 |
+
key_metrics_income = create_key_metrics_chart(
|
| 203 |
+
financial_statements['income_stmt'],
|
| 204 |
+
f"{ticker} Key Income Metrics",
|
| 205 |
+
['Total Revenue', 'Gross Profit', 'Operating Income', 'Net Income'],
|
| 206 |
+
currency_symbol
|
| 207 |
+
)
|
| 208 |
+
except Exception as e:
|
| 209 |
+
key_metrics_income = plt.figure(figsize=(10, 6))
|
| 210 |
+
plt.text(0.5, 0.5, f"Income metrics data not available",
|
| 211 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 212 |
+
plt.axis('off')
|
| 213 |
+
|
| 214 |
+
try:
|
| 215 |
+
key_metrics_balance = create_key_metrics_chart(
|
| 216 |
+
financial_statements['balance_sheet'],
|
| 217 |
+
f"{ticker} Key Balance Sheet Metrics",
|
| 218 |
+
['Total Assets', 'Total Liabilities Net Minority Interest', 'Total Equity Gross Minority Interest'],
|
| 219 |
+
currency_symbol
|
| 220 |
+
)
|
| 221 |
+
except Exception as e:
|
| 222 |
+
key_metrics_balance = plt.figure(figsize=(10, 6))
|
| 223 |
+
plt.text(0.5, 0.5, f"Balance sheet metrics data not available",
|
| 224 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 225 |
+
plt.axis('off')
|
| 226 |
+
|
| 227 |
+
try:
|
| 228 |
+
key_metrics_cash = create_key_metrics_chart(
|
| 229 |
+
financial_statements['cash_flow'],
|
| 230 |
+
f"{ticker} Key Cash Flow Metrics",
|
| 231 |
+
['Operating Cash Flow', 'Free Cash Flow', 'Capital Expenditures'],
|
| 232 |
+
currency_symbol
|
| 233 |
+
)
|
| 234 |
+
except Exception as e:
|
| 235 |
+
key_metrics_cash = plt.figure(figsize=(10, 6))
|
| 236 |
+
plt.text(0.5, 0.5, f"Cash flow metrics data not available",
|
| 237 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 238 |
+
plt.axis('off')
|
| 239 |
+
|
| 240 |
+
# Create ratio charts with error handling
|
| 241 |
+
try:
|
| 242 |
+
profitability_fig = create_ratio_chart(
|
| 243 |
+
financial_statements['income_stmt'],
|
| 244 |
+
f"{ticker} Profitability Ratios",
|
| 245 |
+
'profitability'
|
| 246 |
+
)
|
| 247 |
+
except Exception as e:
|
| 248 |
+
profitability_fig = plt.figure(figsize=(10, 6))
|
| 249 |
+
plt.text(0.5, 0.5, f"Profitability ratio data not available",
|
| 250 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 251 |
+
plt.axis('off')
|
| 252 |
+
|
| 253 |
+
try:
|
| 254 |
+
efficiency_fig = create_ratio_chart(
|
| 255 |
+
financial_statements['balance_sheet'],
|
| 256 |
+
f"{ticker} Efficiency Ratios",
|
| 257 |
+
'efficiency'
|
| 258 |
+
)
|
| 259 |
+
except Exception as e:
|
| 260 |
+
efficiency_fig = plt.figure(figsize=(10, 6))
|
| 261 |
+
plt.text(0.5, 0.5, f"Efficiency ratio data not available",
|
| 262 |
+
horizontalalignment='center', verticalalignment='center', fontsize=12)
|
| 263 |
+
plt.axis('off')
|
| 264 |
+
|
| 265 |
+
return (formatted_metrics, valuation_results, price_fig,
|
| 266 |
+
annual_income_table, annual_balance_table, annual_cash_flow_table,
|
| 267 |
+
quarterly_income_table, quarterly_balance_table, quarterly_cash_flow_table,
|
| 268 |
+
income_fig, balance_fig, cash_flow_fig,
|
| 269 |
+
q_income_fig, q_balance_fig, q_cash_flow_fig,
|
| 270 |
+
spider_fig, revenue_growth_fig, margin_fig,
|
| 271 |
+
key_metrics_income, key_metrics_balance, key_metrics_cash,
|
| 272 |
+
profitability_fig)
|
| 273 |
+
|
| 274 |
+
except Exception as e:
|
| 275 |
+
error_msg = {"error": f"Error: {str(e)}"}
|
| 276 |
+
# Create empty figures for all plots
|
| 277 |
+
empty_fig = plt.figure(figsize=(10, 6))
|
| 278 |
+
plt.text(0.5, 0.5, f"Error: {str(e)}",
|
| 279 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 280 |
+
plt.axis('off')
|
| 281 |
+
|
| 282 |
+
return (error_msg, {"error": str(e)}, empty_fig,
|
| 283 |
+
{"error": "Data unavailable"}, {"error": "Data unavailable"}, {"error": "Data unavailable"},
|
| 284 |
+
{"error": "Data unavailable"}, {"error": "Data unavailable"}, {"error": "Data unavailable"},
|
| 285 |
+
empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, empty_fig,
|
| 286 |
+
empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, empty_fig, empty_fig)
|
| 287 |
+
|
| 288 |
+
# Create Gradio interface
|
| 289 |
+
with gr.Blocks(title="Stock DCF Valuation Tool") as app:
|
| 290 |
+
gr.Markdown("# Stock DCF Valuation Tool")
|
| 291 |
+
gr.Markdown("Enter a stock ticker and DCF assumptions to get a valuation")
|
| 292 |
+
|
| 293 |
+
with gr.Row():
|
| 294 |
+
with gr.Column(scale=1):
|
| 295 |
+
ticker_input = gr.Textbox(label="Stock Ticker (e.g., AAPL, RELIANCE.NS)", placeholder="Enter ticker...")
|
| 296 |
+
|
| 297 |
+
with gr.Row():
|
| 298 |
+
growth_rate = gr.Slider(minimum=0, maximum=250, value=15, step=1, label="Growth Rate (%)")
|
| 299 |
+
discount_rate = gr.Slider(minimum=5, maximum=20, value=10, step=0.1, label="Discount Rate (%)")
|
| 300 |
+
|
| 301 |
+
projection_years = gr.Slider(minimum=1, maximum=10, value=5, step=1, label="Projection Years")
|
| 302 |
+
|
| 303 |
+
format_type = gr.Radio(
|
| 304 |
+
["auto", "comma", "millions", "billions"],
|
| 305 |
+
label="Number Format",
|
| 306 |
+
value="millions"
|
| 307 |
+
)
|
| 308 |
+
|
| 309 |
+
analyze_button = gr.Button("Analyze Stock", variant="primary")
|
| 310 |
+
|
| 311 |
+
with gr.Column(scale=2):
|
| 312 |
+
with gr.Tabs():
|
| 313 |
+
with gr.TabItem("Key Metrics"):
|
| 314 |
+
metrics_output = gr.JSON(label="Key Metrics")
|
| 315 |
+
spider_chart = gr.Plot(label="Financial Metrics Radar")
|
| 316 |
+
|
| 317 |
+
with gr.TabItem("DCF Valuation"):
|
| 318 |
+
valuation_output = gr.JSON(label="DCF Valuation Results")
|
| 319 |
+
price_chart = gr.Plot(label="Price History")
|
| 320 |
+
|
| 321 |
+
with gr.TabItem("Annual Financials"):
|
| 322 |
+
with gr.Tabs():
|
| 323 |
+
with gr.TabItem("Income Statement"):
|
| 324 |
+
income_chart = gr.Plot(label="Income Statement Chart")
|
| 325 |
+
annual_income_output = gr.JSON(label="Annual Income Statement")
|
| 326 |
+
|
| 327 |
+
with gr.TabItem("Balance Sheet"):
|
| 328 |
+
balance_chart = gr.Plot(label="Balance Sheet Chart")
|
| 329 |
+
annual_balance_output = gr.JSON(label="Annual Balance Sheet")
|
| 330 |
+
|
| 331 |
+
with gr.TabItem("Cash Flow"):
|
| 332 |
+
cash_flow_chart = gr.Plot(label="Cash Flow Chart")
|
| 333 |
+
annual_cash_flow_output = gr.JSON(label="Annual Cash Flow")
|
| 334 |
+
|
| 335 |
+
with gr.TabItem("Quarterly Financials"):
|
| 336 |
+
with gr.Tabs():
|
| 337 |
+
with gr.TabItem("Income Statement"):
|
| 338 |
+
q_income_chart = gr.Plot(label="Quarterly Income Statement Chart")
|
| 339 |
+
quarterly_income_output = gr.JSON(label="Quarterly Income Statement")
|
| 340 |
+
|
| 341 |
+
with gr.TabItem("Balance Sheet"):
|
| 342 |
+
q_balance_chart = gr.Plot(label="Quarterly Balance Sheet Chart")
|
| 343 |
+
quarterly_balance_output = gr.JSON(label="Quarterly Balance Sheet")
|
| 344 |
+
|
| 345 |
+
with gr.TabItem("Cash Flow"):
|
| 346 |
+
q_cash_flow_chart = gr.Plot(label="Quarterly Cash Flow Chart")
|
| 347 |
+
quarterly_cash_flow_output = gr.JSON(label="Quarterly Cash Flow")
|
| 348 |
+
|
| 349 |
+
with gr.TabItem("Financial Analysis"):
|
| 350 |
+
with gr.Tabs():
|
| 351 |
+
with gr.TabItem("Revenue & Growth"):
|
| 352 |
+
revenue_growth_chart = gr.Plot(label="Revenue Growth")
|
| 353 |
+
key_metrics_income_chart = gr.Plot(label="Key Income Metrics")
|
| 354 |
+
|
| 355 |
+
with gr.TabItem("Profitability"):
|
| 356 |
+
margin_chart = gr.Plot(label="Margin Analysis")
|
| 357 |
+
profitability_chart = gr.Plot(label="Profitability Ratios")
|
| 358 |
+
|
| 359 |
+
with gr.TabItem("Balance Sheet Analysis"):
|
| 360 |
+
key_metrics_balance_chart = gr.Plot(label="Key Balance Sheet Metrics")
|
| 361 |
+
efficiency_chart = gr.Plot(label="Efficiency Ratios")
|
| 362 |
+
|
| 363 |
+
with gr.TabItem("Cash Flow Analysis"):
|
| 364 |
+
key_metrics_cash_chart = gr.Plot(label="Key Cash Flow Metrics")
|
| 365 |
+
|
| 366 |
+
# Define function to clear figures when app is closed
|
| 367 |
+
def on_close():
|
| 368 |
+
plt.close('all')
|
| 369 |
+
|
| 370 |
+
# Register the function to be called when the app is closed
|
| 371 |
+
app.load(on_close)
|
| 372 |
+
|
| 373 |
+
analyze_button.click(
|
| 374 |
+
analyze_stock,
|
| 375 |
+
inputs=[ticker_input, growth_rate, discount_rate, projection_years, format_type],
|
| 376 |
+
outputs=[
|
| 377 |
+
metrics_output,
|
| 378 |
+
valuation_output,
|
| 379 |
+
price_chart,
|
| 380 |
+
annual_income_output,
|
| 381 |
+
annual_balance_output,
|
| 382 |
+
annual_cash_flow_output,
|
| 383 |
+
quarterly_income_output,
|
| 384 |
+
quarterly_balance_output,
|
| 385 |
+
quarterly_cash_flow_output,
|
| 386 |
+
income_chart,
|
| 387 |
+
balance_chart,
|
| 388 |
+
cash_flow_chart,
|
| 389 |
+
q_income_chart,
|
| 390 |
+
q_balance_chart,
|
| 391 |
+
q_cash_flow_chart,
|
| 392 |
+
spider_chart,
|
| 393 |
+
revenue_growth_chart,
|
| 394 |
+
margin_chart,
|
| 395 |
+
key_metrics_income_chart,
|
| 396 |
+
key_metrics_balance_chart,
|
| 397 |
+
key_metrics_cash_chart,
|
| 398 |
+
profitability_chart
|
| 399 |
+
]
|
| 400 |
+
)
|
| 401 |
+
|
| 402 |
+
# Launch the app
|
| 403 |
+
if __name__ == "__main__":
|
| 404 |
+
app.launch()
|
data_fetcher.py
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yfinance as yf
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from datetime import datetime, timedelta
|
| 5 |
+
|
| 6 |
+
class DataFetcher:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
# Cache to store fetched data
|
| 9 |
+
self.cache = {}
|
| 10 |
+
|
| 11 |
+
def get_ticker_info(self, ticker):
|
| 12 |
+
"""Get basic information about a ticker"""
|
| 13 |
+
cache_key = f"{ticker}_info"
|
| 14 |
+
if cache_key in self.cache:
|
| 15 |
+
return self.cache[cache_key]
|
| 16 |
+
|
| 17 |
+
try:
|
| 18 |
+
stock = yf.Ticker(ticker)
|
| 19 |
+
info = stock.info
|
| 20 |
+
self.cache[cache_key] = info
|
| 21 |
+
return info
|
| 22 |
+
except Exception as e:
|
| 23 |
+
print(f"Error fetching info for {ticker}: {str(e)}")
|
| 24 |
+
return {}
|
| 25 |
+
|
| 26 |
+
def get_price_history(self, ticker, period="1y"):
|
| 27 |
+
"""Get historical price data for a ticker"""
|
| 28 |
+
cache_key = f"{ticker}_price_{period}"
|
| 29 |
+
if cache_key in self.cache:
|
| 30 |
+
return self.cache[cache_key]
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
stock = yf.Ticker(ticker)
|
| 34 |
+
history = stock.history(period=period)
|
| 35 |
+
|
| 36 |
+
if history.empty:
|
| 37 |
+
# Try with a shorter period if 1y fails
|
| 38 |
+
history = stock.history(period="6mo")
|
| 39 |
+
|
| 40 |
+
if history.empty:
|
| 41 |
+
# Try with an even shorter period if 6mo fails
|
| 42 |
+
history = stock.history(period="3mo")
|
| 43 |
+
|
| 44 |
+
if not history.empty:
|
| 45 |
+
# Use Close prices
|
| 46 |
+
price_series = history['Close']
|
| 47 |
+
self.cache[cache_key] = price_series
|
| 48 |
+
return price_series
|
| 49 |
+
else:
|
| 50 |
+
print(f"No price history available for {ticker}")
|
| 51 |
+
return pd.Series()
|
| 52 |
+
except Exception as e:
|
| 53 |
+
print(f"Error fetching price history for {ticker}: {str(e)}")
|
| 54 |
+
return pd.Series()
|
| 55 |
+
|
| 56 |
+
def get_key_metrics(self, ticker):
|
| 57 |
+
"""Get key financial metrics for a ticker"""
|
| 58 |
+
cache_key = f"{ticker}_metrics"
|
| 59 |
+
if cache_key in self.cache:
|
| 60 |
+
return self.cache[cache_key]
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
stock = yf.Ticker(ticker)
|
| 64 |
+
info = stock.info
|
| 65 |
+
|
| 66 |
+
# Determine currency symbol based on country or exchange
|
| 67 |
+
currency_symbol = '$' # Default to USD
|
| 68 |
+
if 'currency' in info:
|
| 69 |
+
if info['currency'] == 'INR':
|
| 70 |
+
currency_symbol = '₹'
|
| 71 |
+
elif info['currency'] == 'EUR':
|
| 72 |
+
currency_symbol = '€'
|
| 73 |
+
elif info['currency'] == 'GBP':
|
| 74 |
+
currency_symbol = '£'
|
| 75 |
+
elif info['currency'] == 'JPY':
|
| 76 |
+
currency_symbol = '¥'
|
| 77 |
+
|
| 78 |
+
# Check if it's an Indian stock based on ticker suffix
|
| 79 |
+
if ticker.endswith('.NS') or ticker.endswith('.BO'):
|
| 80 |
+
currency_symbol = '₹'
|
| 81 |
+
|
| 82 |
+
# Extract key metrics
|
| 83 |
+
metrics = {
|
| 84 |
+
'Company Name': info.get('longName', 'N/A'),
|
| 85 |
+
'Sector': info.get('sector', 'N/A'),
|
| 86 |
+
'Industry': info.get('industry', 'N/A'),
|
| 87 |
+
'Country': info.get('country', 'N/A'),
|
| 88 |
+
'Currency Symbol': currency_symbol,
|
| 89 |
+
'Current Price': info.get('currentPrice', info.get('regularMarketPrice', 'N/A')),
|
| 90 |
+
'Market Cap': info.get('marketCap', 'N/A'),
|
| 91 |
+
'P/E Ratio': info.get('trailingPE', 'N/A'),
|
| 92 |
+
'Forward P/E': info.get('forwardPE', 'N/A'),
|
| 93 |
+
'P/B Ratio': info.get('priceToBook', 'N/A'),
|
| 94 |
+
'EV/EBITDA': info.get('enterpriseToEbitda', 'N/A'),
|
| 95 |
+
'EV/Revenue': info.get('enterpriseToRevenue', 'N/A'),
|
| 96 |
+
'PEG Ratio': info.get('pegRatio', 'N/A'),
|
| 97 |
+
'Dividend Yield (%)': info.get('dividendYield', 'N/A') * 100 if info.get('dividendYield') is not None else 'N/A',
|
| 98 |
+
'EPS': info.get('trailingEps', 'N/A'),
|
| 99 |
+
'Profit Margin': info.get('profitMargins', 'N/A') * 100 if info.get('profitMargins') is not None else 'N/A',
|
| 100 |
+
'Operating Margin': info.get('operatingMargins', 'N/A') * 100 if info.get('operatingMargins') is not None else 'N/A',
|
| 101 |
+
'ROE': info.get('returnOnEquity', 'N/A') * 100 if info.get('returnOnEquity') is not None else 'N/A',
|
| 102 |
+
'ROA': info.get('returnOnAssets', 'N/A') * 100 if info.get('returnOnAssets') is not None else 'N/A',
|
| 103 |
+
'Revenue Growth': info.get('revenueGrowth', 'N/A') * 100 if info.get('revenueGrowth') is not None else 'N/A',
|
| 104 |
+
'Earnings Growth': info.get('earningsGrowth', 'N/A') * 100 if info.get('earningsGrowth') is not None else 'N/A',
|
| 105 |
+
'Debt to Equity': info.get('debtToEquity', 'N/A') / 100 if info.get('debtToEquity') is not None else 'N/A',
|
| 106 |
+
'Current Ratio': info.get('currentRatio', 'N/A'),
|
| 107 |
+
'Quick Ratio': info.get('quickRatio', 'N/A'),
|
| 108 |
+
'Beta': info.get('beta', 'N/A'),
|
| 109 |
+
'52 Week High': info.get('fiftyTwoWeekHigh', 'N/A'),
|
| 110 |
+
'52 Week Low': info.get('fiftyTwoWeekLow', 'N/A'),
|
| 111 |
+
'50-Day MA': info.get('fiftyDayAverage', 'N/A'),
|
| 112 |
+
'200-Day MA': info.get('twoHundredDayAverage', 'N/A'),
|
| 113 |
+
'Shares Outstanding': info.get('sharesOutstanding', 'N/A'),
|
| 114 |
+
'Free Cash Flow': info.get('freeCashflow', 'N/A'),
|
| 115 |
+
'Operating Cash Flow': info.get('operatingCashflow', 'N/A'),
|
| 116 |
+
'Revenue Per Share': info.get('revenuePerShare', 'N/A'),
|
| 117 |
+
'Target Mean Price': info.get('targetMeanPrice', 'N/A'),
|
| 118 |
+
'Payout Ratio': info.get('payoutRatio', 'N/A') * 100 if info.get('payoutRatio') is not None else 'N/A',
|
| 119 |
+
'EBITDA Margins': info.get('ebitdaMargins', 'N/A') * 100 if info.get('ebitdaMargins') is not None else 'N/A',
|
| 120 |
+
'Gross Margins': info.get('grossMargins', 'N/A') * 100 if info.get('grossMargins') is not None else 'N/A'
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
self.cache[cache_key] = metrics
|
| 124 |
+
return metrics
|
| 125 |
+
except Exception as e:
|
| 126 |
+
print(f"Error fetching metrics for {ticker}: {str(e)}")
|
| 127 |
+
return {'error': str(e), 'Currency Symbol': '$'}
|
| 128 |
+
|
| 129 |
+
def get_financial_statements(self, ticker):
|
| 130 |
+
"""Get financial statements for a ticker"""
|
| 131 |
+
cache_key = f"{ticker}_financials"
|
| 132 |
+
if cache_key in self.cache:
|
| 133 |
+
return self.cache[cache_key]
|
| 134 |
+
|
| 135 |
+
try:
|
| 136 |
+
stock = yf.Ticker(ticker)
|
| 137 |
+
|
| 138 |
+
# Get annual financial statements
|
| 139 |
+
income_stmt = stock.income_stmt
|
| 140 |
+
balance_sheet = stock.balance_sheet
|
| 141 |
+
cash_flow = stock.cashflow
|
| 142 |
+
|
| 143 |
+
# Get quarterly financial statements with error handling
|
| 144 |
+
try:
|
| 145 |
+
quarterly_income_stmt = stock.quarterly_income_stmt
|
| 146 |
+
except Exception as e:
|
| 147 |
+
print(f"Error fetching quarterly income statement for {ticker}: {str(e)}")
|
| 148 |
+
quarterly_income_stmt = pd.DataFrame()
|
| 149 |
+
|
| 150 |
+
try:
|
| 151 |
+
quarterly_balance_sheet = stock.quarterly_balance_sheet
|
| 152 |
+
except Exception as e:
|
| 153 |
+
print(f"Error fetching quarterly balance sheet for {ticker}: {str(e)}")
|
| 154 |
+
quarterly_balance_sheet = pd.DataFrame()
|
| 155 |
+
|
| 156 |
+
try:
|
| 157 |
+
quarterly_cash_flow = stock.quarterly_cashflow
|
| 158 |
+
except Exception as e:
|
| 159 |
+
print(f"Error fetching quarterly cash flow for {ticker}: {str(e)}")
|
| 160 |
+
quarterly_cash_flow = pd.DataFrame()
|
| 161 |
+
|
| 162 |
+
# Package all statements
|
| 163 |
+
financial_statements = {
|
| 164 |
+
'income_stmt': income_stmt,
|
| 165 |
+
'balance_sheet': balance_sheet,
|
| 166 |
+
'cash_flow': cash_flow,
|
| 167 |
+
'quarterly_income_stmt': quarterly_income_stmt,
|
| 168 |
+
'quarterly_balance_sheet': quarterly_balance_sheet,
|
| 169 |
+
'quarterly_cash_flow': quarterly_cash_flow
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
self.cache[cache_key] = financial_statements
|
| 173 |
+
return financial_statements
|
| 174 |
+
except Exception as e:
|
| 175 |
+
print(f"Error fetching financial statements for {ticker}: {str(e)}")
|
| 176 |
+
# Return empty DataFrames for all statements
|
| 177 |
+
empty_df = pd.DataFrame()
|
| 178 |
+
return {
|
| 179 |
+
'income_stmt': empty_df,
|
| 180 |
+
'balance_sheet': empty_df,
|
| 181 |
+
'cash_flow': empty_df,
|
| 182 |
+
'quarterly_income_stmt': empty_df,
|
| 183 |
+
'quarterly_balance_sheet': empty_df,
|
| 184 |
+
'quarterly_cash_flow': empty_df
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
def get_free_cash_flow(self, ticker):
|
| 188 |
+
"""Get the most recent free cash flow value"""
|
| 189 |
+
try:
|
| 190 |
+
# First try to get FCF directly from info
|
| 191 |
+
metrics = self.get_key_metrics(ticker)
|
| 192 |
+
if metrics.get('Free Cash Flow', 'N/A') != 'N/A':
|
| 193 |
+
fcf = metrics.get('Free Cash Flow')
|
| 194 |
+
# Handle negative FCF by using 0 as the base
|
| 195 |
+
if fcf is not None and fcf < 0:
|
| 196 |
+
print(f"Warning: Negative FCF ({fcf}) for {ticker}, using 0 as base for DCF")
|
| 197 |
+
return 0
|
| 198 |
+
return fcf
|
| 199 |
+
|
| 200 |
+
# If not available, calculate from cash flow statement
|
| 201 |
+
financial_statements = self.get_financial_statements(ticker)
|
| 202 |
+
cash_flow = financial_statements['cash_flow']
|
| 203 |
+
|
| 204 |
+
if cash_flow.empty:
|
| 205 |
+
return None
|
| 206 |
+
|
| 207 |
+
# Check if 'Free Cash Flow' is directly available
|
| 208 |
+
if 'Free Cash Flow' in cash_flow.index:
|
| 209 |
+
fcf = cash_flow.loc['Free Cash Flow', cash_flow.columns[0]]
|
| 210 |
+
# Handle negative FCF by using 0 as the base
|
| 211 |
+
if fcf is not None and fcf < 0:
|
| 212 |
+
print(f"Warning: Negative FCF ({fcf}) for {ticker}, using 0 as base for DCF")
|
| 213 |
+
return 0
|
| 214 |
+
return fcf
|
| 215 |
+
|
| 216 |
+
# If not, try to calculate it from components
|
| 217 |
+
if 'Operating Cash Flow' in cash_flow.index and 'Capital Expenditure' in cash_flow.index:
|
| 218 |
+
operating_cf = cash_flow.loc['Operating Cash Flow', cash_flow.columns[0]]
|
| 219 |
+
capex = cash_flow.loc['Capital Expenditure', cash_flow.columns[0]]
|
| 220 |
+
|
| 221 |
+
if pd.notnull(operating_cf) and pd.notnull(capex):
|
| 222 |
+
fcf = operating_cf + capex # Note: capex is usually negative
|
| 223 |
+
# Handle negative FCF by using 0 as the base
|
| 224 |
+
if fcf is not None and fcf < 0:
|
| 225 |
+
print(f"Warning: Negative FCF ({fcf}) for {ticker}, using 0 as base for DCF")
|
| 226 |
+
return 0
|
| 227 |
+
return fcf
|
| 228 |
+
|
| 229 |
+
return None
|
| 230 |
+
except Exception as e:
|
| 231 |
+
print(f"Error calculating free cash flow for {ticker}: {str(e)}")
|
| 232 |
+
return None
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=3.50.2
|
| 2 |
+
yfinance>=0.2.28
|
| 3 |
+
pandas>=2.0.0
|
| 4 |
+
numpy>=1.24.0
|
| 5 |
+
matplotlib>=3.7.0
|
utils.py
ADDED
|
@@ -0,0 +1,792 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import matplotlib.dates as mdates
|
| 6 |
+
from matplotlib.patches import Circle, RegularPolygon
|
| 7 |
+
from matplotlib.path import Path
|
| 8 |
+
from matplotlib.projections.polar import PolarAxes
|
| 9 |
+
from matplotlib.projections import register_projection
|
| 10 |
+
from matplotlib.spines import Spine
|
| 11 |
+
from matplotlib.transforms import Affine2D
|
| 12 |
+
import locale
|
| 13 |
+
import matplotlib.ticker as mtick
|
| 14 |
+
|
| 15 |
+
# Set locale for number formatting
|
| 16 |
+
try:
|
| 17 |
+
locale.setlocale(locale.LC_ALL, '')
|
| 18 |
+
except:
|
| 19 |
+
pass # Fallback if locale setting fails
|
| 20 |
+
|
| 21 |
+
def format_number(number, precision=2, currency_symbol='$', format_type='auto'):
|
| 22 |
+
"""
|
| 23 |
+
Format large numbers with K, M, B, T suffixes or with commas
|
| 24 |
+
|
| 25 |
+
Parameters:
|
| 26 |
+
- number: The number to format
|
| 27 |
+
- precision: Decimal precision
|
| 28 |
+
- currency_symbol: Currency symbol to use
|
| 29 |
+
- format_type: 'auto', 'suffix', 'comma', 'millions', 'billions'
|
| 30 |
+
"""
|
| 31 |
+
if number is None or number == 'N/A':
|
| 32 |
+
return 'N/A'
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
number = float(number)
|
| 36 |
+
except:
|
| 37 |
+
return str(number)
|
| 38 |
+
|
| 39 |
+
# Handle negative numbers
|
| 40 |
+
is_negative = number < 0
|
| 41 |
+
abs_number = abs(number)
|
| 42 |
+
|
| 43 |
+
# Format based on type
|
| 44 |
+
if format_type == 'comma':
|
| 45 |
+
# Format with commas
|
| 46 |
+
try:
|
| 47 |
+
formatted = locale.format_string(f"%.{precision}f", abs_number, grouping=True)
|
| 48 |
+
except:
|
| 49 |
+
# Fallback if locale formatting fails
|
| 50 |
+
formatted = f"{abs_number:,.{precision}f}"
|
| 51 |
+
elif format_type == 'millions':
|
| 52 |
+
# Always format in millions
|
| 53 |
+
formatted = f"{abs_number / 1_000_000:.{precision}f}M"
|
| 54 |
+
elif format_type == 'billions':
|
| 55 |
+
# Always format in billions
|
| 56 |
+
formatted = f"{abs_number / 1_000_000_000:.{precision}f}B"
|
| 57 |
+
else: # 'auto' or 'suffix'
|
| 58 |
+
# Format with appropriate suffix based on magnitude
|
| 59 |
+
if abs_number >= 1_000_000_000_000:
|
| 60 |
+
formatted = f"{abs_number / 1_000_000_000_000:.{precision}f}T"
|
| 61 |
+
elif abs_number >= 1_000_000_000:
|
| 62 |
+
formatted = f"{abs_number / 1_000_000_000:.{precision}f}B"
|
| 63 |
+
elif abs_number >= 1_000_000:
|
| 64 |
+
formatted = f"{abs_number / 1_000_000:.{precision}f}M"
|
| 65 |
+
elif abs_number >= 1_000:
|
| 66 |
+
formatted = f"{abs_number / 1_000:.{precision}f}K"
|
| 67 |
+
else:
|
| 68 |
+
formatted = f"{abs_number:.{precision}f}"
|
| 69 |
+
|
| 70 |
+
# Add negative sign if needed
|
| 71 |
+
if is_negative:
|
| 72 |
+
return f"-{currency_symbol}{formatted}"
|
| 73 |
+
else:
|
| 74 |
+
return f"{currency_symbol}{formatted}"
|
| 75 |
+
|
| 76 |
+
def format_percentage(number, precision=2):
|
| 77 |
+
"""Format number as percentage"""
|
| 78 |
+
if number is None or number == 'N/A':
|
| 79 |
+
return 'N/A'
|
| 80 |
+
|
| 81 |
+
try:
|
| 82 |
+
number = float(number)
|
| 83 |
+
return f"{number:.{precision}f}%"
|
| 84 |
+
except:
|
| 85 |
+
return str(number)
|
| 86 |
+
|
| 87 |
+
def create_price_chart(price_history):
|
| 88 |
+
"""Create a price chart from historical data"""
|
| 89 |
+
# Close any existing figures to prevent memory issues
|
| 90 |
+
plt.close('all')
|
| 91 |
+
|
| 92 |
+
if price_history is None or len(price_history) == 0:
|
| 93 |
+
fig = plt.figure(figsize=(10, 6))
|
| 94 |
+
plt.text(0.5, 0.5, "No price history data available",
|
| 95 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 96 |
+
plt.axis('off')
|
| 97 |
+
return fig
|
| 98 |
+
|
| 99 |
+
fig = plt.figure(figsize=(10, 6))
|
| 100 |
+
|
| 101 |
+
# Calculate moving averages if enough data points
|
| 102 |
+
if len(price_history) > 50:
|
| 103 |
+
ma50 = price_history.rolling(window=50).mean()
|
| 104 |
+
ma200 = price_history.rolling(window=min(200, len(price_history))).mean()
|
| 105 |
+
|
| 106 |
+
plt.plot(price_history.index, price_history.values, label='Price')
|
| 107 |
+
plt.plot(ma50.index, ma50.values, label='50-Day MA', linestyle='--')
|
| 108 |
+
plt.plot(ma200.index, ma200.values, label='200-Day MA', linestyle='-.')
|
| 109 |
+
plt.legend()
|
| 110 |
+
else:
|
| 111 |
+
plt.plot(price_history.index, price_history.values)
|
| 112 |
+
|
| 113 |
+
# Format x-axis to show dates nicely
|
| 114 |
+
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
|
| 115 |
+
plt.gca().xaxis.set_major_locator(mdates.MonthLocator(interval=2))
|
| 116 |
+
plt.gcf().autofmt_xdate()
|
| 117 |
+
|
| 118 |
+
# Add grid and labels
|
| 119 |
+
plt.title('Stock Price History', fontsize=14)
|
| 120 |
+
plt.xlabel('Date')
|
| 121 |
+
plt.ylabel('Price')
|
| 122 |
+
plt.grid(True, alpha=0.3)
|
| 123 |
+
plt.tight_layout()
|
| 124 |
+
|
| 125 |
+
return fig
|
| 126 |
+
|
| 127 |
+
def format_metrics_table(metrics):
|
| 128 |
+
"""Format metrics for display in a table"""
|
| 129 |
+
formatted_metrics = {}
|
| 130 |
+
currency_symbol = metrics.get('Currency Symbol', '$')
|
| 131 |
+
|
| 132 |
+
for key, value in metrics.items():
|
| 133 |
+
if key == 'Market Cap':
|
| 134 |
+
formatted_metrics[key] = format_number(value, currency_symbol=currency_symbol)
|
| 135 |
+
elif key in ['Dividend Yield (%)', 'Profit Margin', 'Operating Margin', 'ROE', 'ROA', 'Revenue Growth',
|
| 136 |
+
'Payout Ratio', 'Earnings Growth', 'EBITDA Margins', 'Gross Margins'] or key.endswith('(%)'):
|
| 137 |
+
formatted_metrics[key] = format_percentage(value)
|
| 138 |
+
elif key in ['Current Price', 'EPS', '52 Week High', '52 Week Low', '50-Day MA', '200-Day MA',
|
| 139 |
+
'Revenue Per Share', 'Target Mean Price', 'Free Cash Flow', 'Operating Cash Flow']:
|
| 140 |
+
if value != 'N/A':
|
| 141 |
+
formatted_metrics[key] = f"{currency_symbol}{value:.2f}"
|
| 142 |
+
else:
|
| 143 |
+
formatted_metrics[key] = value
|
| 144 |
+
elif key in ['P/E Ratio', 'P/B Ratio', 'Forward P/E', 'PEG Ratio', 'Debt to Equity',
|
| 145 |
+
'Current Ratio', 'Quick Ratio', 'Beta', 'EV/EBITDA', 'EV/Revenue']:
|
| 146 |
+
if value != 'N/A':
|
| 147 |
+
formatted_metrics[key] = f"{value:.2f}"
|
| 148 |
+
else:
|
| 149 |
+
formatted_metrics[key] = value
|
| 150 |
+
else:
|
| 151 |
+
formatted_metrics[key] = value
|
| 152 |
+
|
| 153 |
+
return formatted_metrics
|
| 154 |
+
|
| 155 |
+
def format_financial_statement(statement, statement_type, currency_symbol='$', format_type='millions'):
|
| 156 |
+
"""
|
| 157 |
+
Format financial statement for display
|
| 158 |
+
|
| 159 |
+
Parameters:
|
| 160 |
+
- statement: The financial statement DataFrame
|
| 161 |
+
- statement_type: Type of statement (for title)
|
| 162 |
+
- currency_symbol: Currency symbol to use
|
| 163 |
+
- format_type: How to format numbers ('comma', 'millions', 'billions', 'auto')
|
| 164 |
+
"""
|
| 165 |
+
if statement is None or statement.empty:
|
| 166 |
+
return pd.DataFrame()
|
| 167 |
+
|
| 168 |
+
# Make a copy to avoid modifying the original
|
| 169 |
+
df = statement.copy()
|
| 170 |
+
|
| 171 |
+
# Format column names (dates)
|
| 172 |
+
df.columns = [col.strftime('%Y-%m-%d') if isinstance(col, datetime) else str(col) for col in df.columns]
|
| 173 |
+
|
| 174 |
+
# Format the values based on format_type
|
| 175 |
+
if format_type == 'comma':
|
| 176 |
+
# Format with commas
|
| 177 |
+
for col in df.columns:
|
| 178 |
+
df[col] = df[col].apply(lambda x: format_number(x, currency_symbol=currency_symbol, format_type='comma') if pd.notnull(x) else 'N/A')
|
| 179 |
+
elif format_type == 'millions':
|
| 180 |
+
# Convert to millions and format
|
| 181 |
+
df = df / 1_000_000
|
| 182 |
+
for col in df.columns:
|
| 183 |
+
df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}M" if pd.notnull(x) else 'N/A')
|
| 184 |
+
elif format_type == 'billions':
|
| 185 |
+
# Convert to billions and format
|
| 186 |
+
df = df / 1_000_000_000
|
| 187 |
+
for col in df.columns:
|
| 188 |
+
df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}B" if pd.notnull(x) else 'N/A')
|
| 189 |
+
else: # 'auto'
|
| 190 |
+
# Determine appropriate scale based on data magnitude
|
| 191 |
+
max_abs_val = abs(df.max().max())
|
| 192 |
+
if max_abs_val >= 1_000_000_000:
|
| 193 |
+
df = df / 1_000_000_000
|
| 194 |
+
suffix = 'B'
|
| 195 |
+
else:
|
| 196 |
+
df = df / 1_000_000
|
| 197 |
+
suffix = 'M'
|
| 198 |
+
|
| 199 |
+
for col in df.columns:
|
| 200 |
+
df[col] = df[col].apply(lambda x: f"{currency_symbol}{x:.2f}{suffix}" if pd.notnull(x) else 'N/A')
|
| 201 |
+
|
| 202 |
+
return df
|
| 203 |
+
|
| 204 |
+
def prepare_financial_table(statement, currency_symbol='$', format_type='millions'):
|
| 205 |
+
"""
|
| 206 |
+
Prepare financial statement for display in a table format
|
| 207 |
+
|
| 208 |
+
Returns a dictionary with formatted data and metadata
|
| 209 |
+
"""
|
| 210 |
+
if statement is None or statement.empty:
|
| 211 |
+
return {"error": "No data available"}
|
| 212 |
+
|
| 213 |
+
# Format the statement
|
| 214 |
+
formatted_df = format_financial_statement(statement, "", currency_symbol, format_type)
|
| 215 |
+
|
| 216 |
+
# Prepare data for display
|
| 217 |
+
result = {
|
| 218 |
+
"data": formatted_df.reset_index().to_dict('records'),
|
| 219 |
+
"columns": [{"name": "Metric", "id": "index"}] + [{"name": col, "id": col} for col in formatted_df.columns],
|
| 220 |
+
"format_type": format_type,
|
| 221 |
+
"currency_symbol": currency_symbol
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
return result
|
| 225 |
+
|
| 226 |
+
def create_financial_chart(statement, title, chart_type='bar'):
|
| 227 |
+
"""Create a chart from financial statement data"""
|
| 228 |
+
# Close any existing figures to prevent memory issues
|
| 229 |
+
plt.close('all')
|
| 230 |
+
|
| 231 |
+
if statement is None or statement.empty:
|
| 232 |
+
fig = plt.figure(figsize=(12, 6))
|
| 233 |
+
plt.text(0.5, 0.5, "No data available",
|
| 234 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 235 |
+
plt.axis('off')
|
| 236 |
+
return fig
|
| 237 |
+
|
| 238 |
+
# Select key metrics based on statement type
|
| 239 |
+
if 'Total Revenue' in statement.index: # Income Statement
|
| 240 |
+
metrics = ['Total Revenue', 'Gross Profit', 'Operating Income', 'Net Income']
|
| 241 |
+
elif 'Total Assets' in statement.index: # Balance Sheet
|
| 242 |
+
metrics = ['Total Assets', 'Total Liabilities Net Minority Interest', 'Total Equity Gross Minority Interest']
|
| 243 |
+
elif 'Operating Cash Flow' in statement.index: # Cash Flow
|
| 244 |
+
metrics = ['Operating Cash Flow', 'Free Cash Flow', 'Capital Expenditures']
|
| 245 |
+
else:
|
| 246 |
+
# Default to first 4 rows if specific metrics not found
|
| 247 |
+
metrics = statement.index[:4]
|
| 248 |
+
|
| 249 |
+
# Filter for selected metrics that exist in the statement
|
| 250 |
+
metrics = [m for m in metrics if m in statement.index]
|
| 251 |
+
|
| 252 |
+
if not metrics:
|
| 253 |
+
fig = plt.figure(figsize=(12, 6))
|
| 254 |
+
plt.text(0.5, 0.5, "No relevant metrics found",
|
| 255 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 256 |
+
plt.axis('off')
|
| 257 |
+
return fig
|
| 258 |
+
|
| 259 |
+
# Get data for the selected metrics
|
| 260 |
+
data = statement.loc[metrics]
|
| 261 |
+
|
| 262 |
+
# Convert to millions for better readability
|
| 263 |
+
data = data / 1_000_000
|
| 264 |
+
|
| 265 |
+
# Create the chart
|
| 266 |
+
fig = plt.figure(figsize=(12, 6))
|
| 267 |
+
|
| 268 |
+
if chart_type == 'bar':
|
| 269 |
+
ax = data.T.plot(kind='bar', ax=plt.gca(), width=0.8)
|
| 270 |
+
|
| 271 |
+
# Add value labels on top of bars
|
| 272 |
+
for container in ax.containers:
|
| 273 |
+
ax.bar_label(container, fmt='%.1fM', fontsize=8)
|
| 274 |
+
else: # line chart
|
| 275 |
+
ax = data.T.plot(kind='line', marker='o', ax=plt.gca())
|
| 276 |
+
|
| 277 |
+
# Add value labels at data points
|
| 278 |
+
for line, metric in zip(ax.get_lines(), metrics):
|
| 279 |
+
x_data, y_data = line.get_data()
|
| 280 |
+
for x, y in zip(x_data, y_data):
|
| 281 |
+
ax.annotate(f'{y:.1f}M', (x, y), textcoords="offset points",
|
| 282 |
+
xytext=(0,5), ha='center', fontsize=8)
|
| 283 |
+
|
| 284 |
+
plt.title(title, fontsize=14)
|
| 285 |
+
plt.ylabel('Millions ($)')
|
| 286 |
+
plt.grid(True, alpha=0.3)
|
| 287 |
+
plt.legend(loc='best')
|
| 288 |
+
plt.tight_layout()
|
| 289 |
+
|
| 290 |
+
return fig
|
| 291 |
+
|
| 292 |
+
def create_key_metrics_chart(statement, title, metrics_list, currency_symbol='$'):
|
| 293 |
+
"""Create a chart for specific key metrics from financial statements"""
|
| 294 |
+
# Close any existing figures to prevent memory issues
|
| 295 |
+
plt.close('all')
|
| 296 |
+
|
| 297 |
+
if statement is None or statement.empty:
|
| 298 |
+
fig = plt.figure(figsize=(12, 6))
|
| 299 |
+
plt.text(0.5, 0.5, "No data available",
|
| 300 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 301 |
+
plt.axis('off')
|
| 302 |
+
return fig
|
| 303 |
+
|
| 304 |
+
# Filter for selected metrics that exist in the statement
|
| 305 |
+
available_metrics = [m for m in metrics_list if m in statement.index]
|
| 306 |
+
|
| 307 |
+
if not available_metrics:
|
| 308 |
+
fig = plt.figure(figsize=(12, 6))
|
| 309 |
+
plt.text(0.5, 0.5, "No relevant metrics found",
|
| 310 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 311 |
+
plt.axis('off')
|
| 312 |
+
return fig
|
| 313 |
+
|
| 314 |
+
# Get data for the selected metrics
|
| 315 |
+
data = statement.loc[available_metrics]
|
| 316 |
+
|
| 317 |
+
# Convert to millions for better readability
|
| 318 |
+
data = data / 1_000_000
|
| 319 |
+
|
| 320 |
+
# Create the chart
|
| 321 |
+
fig = plt.figure(figsize=(12, 6))
|
| 322 |
+
|
| 323 |
+
# Create a bar chart
|
| 324 |
+
ax = data.T.plot(kind='bar', ax=plt.gca(), width=0.8)
|
| 325 |
+
|
| 326 |
+
# Add value labels on top of bars
|
| 327 |
+
for container in ax.containers:
|
| 328 |
+
ax.bar_label(container, fmt=f'%.1fM', fontsize=8)
|
| 329 |
+
|
| 330 |
+
plt.title(title, fontsize=14)
|
| 331 |
+
plt.ylabel(f'Millions ({currency_symbol})')
|
| 332 |
+
plt.grid(True, alpha=0.3)
|
| 333 |
+
plt.legend(loc='best')
|
| 334 |
+
plt.tight_layout()
|
| 335 |
+
|
| 336 |
+
return fig
|
| 337 |
+
|
| 338 |
+
def create_growth_chart(statement, metric_name, title):
|
| 339 |
+
"""Create a growth rate chart for a specific metric"""
|
| 340 |
+
# Close any existing figures to prevent memory issues
|
| 341 |
+
plt.close('all')
|
| 342 |
+
|
| 343 |
+
if statement is None or statement.empty or metric_name not in statement.index:
|
| 344 |
+
fig = plt.figure(figsize=(10, 6))
|
| 345 |
+
plt.text(0.5, 0.5, f"No data available for {metric_name}",
|
| 346 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 347 |
+
plt.axis('off')
|
| 348 |
+
return fig
|
| 349 |
+
|
| 350 |
+
# Get data for the selected metric
|
| 351 |
+
data = statement.loc[metric_name]
|
| 352 |
+
|
| 353 |
+
# Calculate year-over-year growth rates
|
| 354 |
+
growth_rates = data.pct_change(-1) * 100 # Multiply by -1 to get YoY since columns are in reverse chronological order
|
| 355 |
+
|
| 356 |
+
# Create the chart
|
| 357 |
+
fig = plt.figure(figsize=(10, 6))
|
| 358 |
+
|
| 359 |
+
# Plot the growth rates
|
| 360 |
+
ax = plt.gca()
|
| 361 |
+
bars = ax.bar(growth_rates.index, growth_rates.values, color='teal', alpha=0.7)
|
| 362 |
+
|
| 363 |
+
# Add value labels on top of bars
|
| 364 |
+
for bar in bars:
|
| 365 |
+
height = bar.get_height()
|
| 366 |
+
if not np.isnan(height):
|
| 367 |
+
ax.text(bar.get_x() + bar.get_width()/2., height + (1 if height >= 0 else -5),
|
| 368 |
+
f'{height:.1f}%', ha='center', va='bottom' if height >= 0 else 'top', fontsize=9)
|
| 369 |
+
|
| 370 |
+
# Format y-axis as percentage
|
| 371 |
+
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
|
| 372 |
+
|
| 373 |
+
# Add a horizontal line at y=0
|
| 374 |
+
plt.axhline(y=0, color='black', linestyle='-', alpha=0.3)
|
| 375 |
+
|
| 376 |
+
plt.title(title, fontsize=14)
|
| 377 |
+
plt.ylabel('Year-over-Year Growth (%)')
|
| 378 |
+
plt.grid(True, alpha=0.3)
|
| 379 |
+
plt.tight_layout()
|
| 380 |
+
|
| 381 |
+
return fig
|
| 382 |
+
|
| 383 |
+
def create_margin_chart(statement, title):
|
| 384 |
+
"""Create a chart showing margin trends"""
|
| 385 |
+
# Close any existing figures to prevent memory issues
|
| 386 |
+
plt.close('all')
|
| 387 |
+
|
| 388 |
+
# Check if we have the necessary data
|
| 389 |
+
required_metrics = ['Total Revenue', 'Gross Profit', 'Operating Income', 'Net Income']
|
| 390 |
+
if statement is None or statement.empty or not all(metric in statement.index for metric in required_metrics):
|
| 391 |
+
fig = plt.figure(figsize=(10, 6))
|
| 392 |
+
plt.text(0.5, 0.5, "Insufficient data for margin analysis",
|
| 393 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 394 |
+
plt.axis('off')
|
| 395 |
+
return fig
|
| 396 |
+
|
| 397 |
+
# Get data for the required metrics
|
| 398 |
+
revenue = statement.loc['Total Revenue']
|
| 399 |
+
gross_profit = statement.loc['Gross Profit']
|
| 400 |
+
operating_income = statement.loc['Operating Income']
|
| 401 |
+
net_income = statement.loc['Net Income']
|
| 402 |
+
|
| 403 |
+
# Calculate margins
|
| 404 |
+
gross_margin = (gross_profit / revenue) * 100
|
| 405 |
+
operating_margin = (operating_income / revenue) * 100
|
| 406 |
+
net_margin = (net_income / revenue) * 100
|
| 407 |
+
|
| 408 |
+
# Create DataFrame for plotting
|
| 409 |
+
margins_df = pd.DataFrame({
|
| 410 |
+
'Gross Margin': gross_margin,
|
| 411 |
+
'Operating Margin': operating_margin,
|
| 412 |
+
'Net Margin': net_margin
|
| 413 |
+
})
|
| 414 |
+
|
| 415 |
+
# Create the chart
|
| 416 |
+
fig = plt.figure(figsize=(10, 6))
|
| 417 |
+
|
| 418 |
+
# Plot margins
|
| 419 |
+
ax = margins_df.plot(kind='line', marker='o', ax=plt.gca())
|
| 420 |
+
|
| 421 |
+
# Format y-axis as percentage
|
| 422 |
+
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
|
| 423 |
+
|
| 424 |
+
# Add value labels at data points
|
| 425 |
+
for line, margin_type in zip(ax.get_lines(), margins_df.columns):
|
| 426 |
+
x_data, y_data = line.get_data()
|
| 427 |
+
for x, y in zip(x_data, y_data):
|
| 428 |
+
ax.annotate(f'{y:.1f}%', (x, y), textcoords="offset points",
|
| 429 |
+
xytext=(0,5), ha='center', fontsize=8)
|
| 430 |
+
|
| 431 |
+
plt.title(title, fontsize=14)
|
| 432 |
+
plt.ylabel('Margin (%)')
|
| 433 |
+
plt.grid(True, alpha=0.3)
|
| 434 |
+
plt.legend(loc='best')
|
| 435 |
+
plt.tight_layout()
|
| 436 |
+
|
| 437 |
+
return fig
|
| 438 |
+
|
| 439 |
+
def radar_factory(num_vars, frame='circle'):
|
| 440 |
+
"""Create a radar chart with `num_vars` axes."""
|
| 441 |
+
# Calculate evenly-spaced axis angles
|
| 442 |
+
theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False)
|
| 443 |
+
|
| 444 |
+
class RadarAxes(PolarAxes):
|
| 445 |
+
name = 'radar'
|
| 446 |
+
|
| 447 |
+
def __init__(self, *args, **kwargs):
|
| 448 |
+
super().__init__(*args, **kwargs)
|
| 449 |
+
# Rotate plot so that first axis is at the top
|
| 450 |
+
self.set_theta_zero_location('N')
|
| 451 |
+
|
| 452 |
+
def fill(self, *args, closed=True, **kwargs):
|
| 453 |
+
"""Override fill so that line is closed by default"""
|
| 454 |
+
return super().fill(closed=closed, *args, **kwargs)
|
| 455 |
+
|
| 456 |
+
def plot(self, *args, **kwargs):
|
| 457 |
+
"""Override plot so that line is closed by default"""
|
| 458 |
+
lines = super().plot(*args, **kwargs)
|
| 459 |
+
for line in lines:
|
| 460 |
+
self._close_line(line)
|
| 461 |
+
return lines
|
| 462 |
+
|
| 463 |
+
def _close_line(self, line):
|
| 464 |
+
x, y = line.get_data()
|
| 465 |
+
# FIXME: markers at x[0], y[0] get doubled-up
|
| 466 |
+
if x[0] != x[-1]:
|
| 467 |
+
x = np.append(x, x[0])
|
| 468 |
+
y = np.append(y, y[0])
|
| 469 |
+
line.set_data(x, y)
|
| 470 |
+
|
| 471 |
+
def set_varlabels(self, labels):
|
| 472 |
+
self.set_thetagrids(np.degrees(theta), labels)
|
| 473 |
+
|
| 474 |
+
def _gen_axes_patch(self):
|
| 475 |
+
# The Axes patch must be centered at (0.5, 0.5) and of radius 0.5
|
| 476 |
+
# in axes coordinates.
|
| 477 |
+
if frame == 'circle':
|
| 478 |
+
return Circle((0.5, 0.5), 0.5)
|
| 479 |
+
elif frame == 'polygon':
|
| 480 |
+
return RegularPolygon((0.5, 0.5), num_vars, radius=0.5, orientation=np.pi/2)
|
| 481 |
+
else:
|
| 482 |
+
raise ValueError("Unknown value for 'frame': %s" % frame)
|
| 483 |
+
|
| 484 |
+
def _gen_axes_spines(self):
|
| 485 |
+
if frame == 'circle':
|
| 486 |
+
return super()._gen_axes_spines()
|
| 487 |
+
elif frame == 'polygon':
|
| 488 |
+
# spine_type must be 'left'/'right'/'top'/'bottom'/'circle'.
|
| 489 |
+
spine = Spine(axes=self,
|
| 490 |
+
spine_type='circle',
|
| 491 |
+
path=Path.unit_regular_polygon(num_vars))
|
| 492 |
+
# unit_regular_polygon returns a polygon of radius 1 centered at
|
| 493 |
+
# (0, 0) but we want a polygon of radius 0.5 centered at (0.5,
|
| 494 |
+
# 0.5) in axes coordinates.
|
| 495 |
+
spine.set_transform(Affine2D().scale(.5).translate(.5, .5)
|
| 496 |
+
+ self.transAxes)
|
| 497 |
+
return {'polar': spine}
|
| 498 |
+
else:
|
| 499 |
+
raise ValueError("Unknown value for 'frame': %s" % frame)
|
| 500 |
+
|
| 501 |
+
# Register the projection with Matplotlib
|
| 502 |
+
register_projection(RadarAxes)
|
| 503 |
+
return theta
|
| 504 |
+
|
| 505 |
+
def create_spider_chart(metrics, title="Financial Metrics Comparison"):
|
| 506 |
+
"""Create a spider/radar chart for key financial metrics"""
|
| 507 |
+
# Close any existing figures to prevent memory issues
|
| 508 |
+
plt.close('all')
|
| 509 |
+
|
| 510 |
+
# Select metrics to display on the spider chart - expanded list
|
| 511 |
+
spider_metrics = {
|
| 512 |
+
'P/E Ratio': metrics.get('P/E Ratio', 'N/A'),
|
| 513 |
+
'P/B Ratio': metrics.get('P/B Ratio', 'N/A'),
|
| 514 |
+
'EV/EBITDA': metrics.get('EV/EBITDA', 'N/A'),
|
| 515 |
+
'PEG Ratio': metrics.get('PEG Ratio', 'N/A'),
|
| 516 |
+
'ROE (%)': metrics.get('ROE', 'N/A'),
|
| 517 |
+
'ROA (%)': metrics.get('ROA', 'N/A'),
|
| 518 |
+
'Profit Margin (%)': metrics.get('Profit Margin', 'N/A'),
|
| 519 |
+
'Operating Margin (%)': metrics.get('Operating Margin', 'N/A'),
|
| 520 |
+
'Debt to Equity': metrics.get('Debt to Equity', 'N/A'),
|
| 521 |
+
'Current Ratio': metrics.get('Current Ratio', 'N/A'),
|
| 522 |
+
'Dividend Yield (%)': metrics.get('Dividend Yield (%)', 'N/A'),
|
| 523 |
+
'Revenue Growth (%)': metrics.get('Revenue Growth', 'N/A')
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
# Filter out N/A values and prepare data
|
| 527 |
+
filtered_metrics = {k: v for k, v in spider_metrics.items() if v != 'N/A' and v is not None}
|
| 528 |
+
|
| 529 |
+
if len(filtered_metrics) < 3:
|
| 530 |
+
# Not enough metrics for a meaningful spider chart
|
| 531 |
+
fig = plt.figure(figsize=(10, 10))
|
| 532 |
+
plt.text(0.5, 0.5, "Insufficient data for spider chart",
|
| 533 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 534 |
+
plt.axis('off')
|
| 535 |
+
return fig
|
| 536 |
+
|
| 537 |
+
# Prepare data for radar chart
|
| 538 |
+
categories = list(filtered_metrics.keys())
|
| 539 |
+
N = len(categories)
|
| 540 |
+
|
| 541 |
+
# Create radar chart
|
| 542 |
+
theta = radar_factory(N, frame='polygon')
|
| 543 |
+
|
| 544 |
+
# Normalize values for better visualization
|
| 545 |
+
values = list(filtered_metrics.values())
|
| 546 |
+
|
| 547 |
+
# Define normalization parameters for each metric
|
| 548 |
+
normalization_params = {
|
| 549 |
+
'P/E Ratio': {'better': 'lower', 'max': 50, 'min': 0},
|
| 550 |
+
'P/B Ratio': {'better': 'lower', 'max': 10, 'min': 0},
|
| 551 |
+
'EV/EBITDA': {'better': 'lower', 'max': 20, 'min': 0},
|
| 552 |
+
'PEG Ratio': {'better': 'lower', 'max': 3, 'min': 0},
|
| 553 |
+
'ROE (%)': {'better': 'higher', 'max': 30, 'min': 0},
|
| 554 |
+
'ROA (%)': {'better': 'higher', 'max': 15, 'min': 0},
|
| 555 |
+
'Profit Margin (%)': {'better': 'higher', 'max': 30, 'min': 0},
|
| 556 |
+
'Operating Margin (%)': {'better': 'higher', 'max': 30, 'min': 0},
|
| 557 |
+
'Debt to Equity': {'better': 'lower', 'max': 3, 'min': 0},
|
| 558 |
+
'Current Ratio': {'better': 'higher', 'max': 3, 'min': 0},
|
| 559 |
+
'Dividend Yield (%)': {'better': 'higher', 'max': 10, 'min': 0},
|
| 560 |
+
'Revenue Growth (%)': {'better': 'higher', 'max': 30, 'min': 0}
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
# Normalize values
|
| 564 |
+
normalized = []
|
| 565 |
+
for i, (cat, val) in enumerate(zip(categories, values)):
|
| 566 |
+
params = normalization_params.get(cat, {'better': 'higher', 'max': 100, 'min': 0})
|
| 567 |
+
|
| 568 |
+
# Clip value to min/max range
|
| 569 |
+
val = max(min(val, params['max']), params['min'])
|
| 570 |
+
|
| 571 |
+
# Normalize to 0-1 scale
|
| 572 |
+
if params['better'] == 'lower':
|
| 573 |
+
# For metrics where lower is better, invert the scale
|
| 574 |
+
norm_val = 1 - ((val - params['min']) / (params['max'] - params['min']))
|
| 575 |
+
else:
|
| 576 |
+
# For metrics where higher is better
|
| 577 |
+
norm_val = (val - params['min']) / (params['max'] - params['min'])
|
| 578 |
+
|
| 579 |
+
normalized.append(norm_val)
|
| 580 |
+
|
| 581 |
+
# Create the figure
|
| 582 |
+
fig, ax = plt.subplots(figsize=(10, 10), subplot_kw=dict(projection='radar'))
|
| 583 |
+
|
| 584 |
+
# Plot the data
|
| 585 |
+
ax.plot(theta, normalized, 'o-', linewidth=2)
|
| 586 |
+
ax.fill(theta, normalized, alpha=0.25)
|
| 587 |
+
|
| 588 |
+
# Set labels
|
| 589 |
+
ax.set_varlabels(categories)
|
| 590 |
+
|
| 591 |
+
# Add values to the plot
|
| 592 |
+
for i, (angle, radius) in enumerate(zip(theta, normalized)):
|
| 593 |
+
ax.text(angle, radius + 0.1, f"{values[i]:.1f}",
|
| 594 |
+
horizontalalignment='center', verticalalignment='center')
|
| 595 |
+
|
| 596 |
+
# Add title
|
| 597 |
+
plt.title(title, position=(0.5, 1.1), size=15)
|
| 598 |
+
|
| 599 |
+
# Add a reference circle at 0.5
|
| 600 |
+
ax.plot(theta, [0.5]*N, '--', color='gray', alpha=0.75, linewidth=1)
|
| 601 |
+
|
| 602 |
+
return fig
|
| 603 |
+
|
| 604 |
+
def create_multi_year_growth_chart(statement, metrics, title, currency_symbol='$'):
|
| 605 |
+
"""Create a chart showing growth of multiple metrics over years"""
|
| 606 |
+
# Close any existing figures to prevent memory issues
|
| 607 |
+
plt.close('all')
|
| 608 |
+
|
| 609 |
+
if statement is None or statement.empty:
|
| 610 |
+
fig = plt.figure(figsize=(12, 6))
|
| 611 |
+
plt.text(0.5, 0.5, "No data available",
|
| 612 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 613 |
+
plt.axis('off')
|
| 614 |
+
return fig
|
| 615 |
+
|
| 616 |
+
# Filter for metrics that exist in the statement
|
| 617 |
+
available_metrics = [m for m in metrics if m in statement.index]
|
| 618 |
+
|
| 619 |
+
if not available_metrics:
|
| 620 |
+
fig = plt.figure(figsize=(12, 6))
|
| 621 |
+
plt.text(0.5, 0.5, "No relevant metrics found",
|
| 622 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 623 |
+
plt.axis('off')
|
| 624 |
+
return fig
|
| 625 |
+
|
| 626 |
+
# Get data for the selected metrics
|
| 627 |
+
data = statement.loc[available_metrics]
|
| 628 |
+
|
| 629 |
+
# Convert to billions for better readability
|
| 630 |
+
data = data / 1_000_000_000
|
| 631 |
+
|
| 632 |
+
# Create the chart
|
| 633 |
+
fig = plt.figure(figsize=(12, 6))
|
| 634 |
+
|
| 635 |
+
# Plot as line chart
|
| 636 |
+
ax = data.T.plot(kind='line', marker='o', ax=plt.gca())
|
| 637 |
+
|
| 638 |
+
# Add value labels at data points
|
| 639 |
+
for line, metric in zip(ax.get_lines(), available_metrics):
|
| 640 |
+
x_data, y_data = line.get_data()
|
| 641 |
+
for x, y in zip(x_data, y_data):
|
| 642 |
+
ax.annotate(f'{y:.1f}B', (x, y), textcoords="offset points",
|
| 643 |
+
xytext=(0,5), ha='center', fontsize=8)
|
| 644 |
+
|
| 645 |
+
plt.title(title, fontsize=14)
|
| 646 |
+
plt.ylabel(f'Billions ({currency_symbol})')
|
| 647 |
+
plt.grid(True, alpha=0.3)
|
| 648 |
+
plt.legend(loc='best')
|
| 649 |
+
plt.tight_layout()
|
| 650 |
+
|
| 651 |
+
return fig
|
| 652 |
+
|
| 653 |
+
def create_ratio_chart(statement, title, ratio_type='profitability'):
|
| 654 |
+
"""Create a chart showing financial ratios over time"""
|
| 655 |
+
# Close any existing figures to prevent memory issues
|
| 656 |
+
plt.close('all')
|
| 657 |
+
|
| 658 |
+
if statement is None or statement.empty:
|
| 659 |
+
fig = plt.figure(figsize=(10, 6))
|
| 660 |
+
plt.text(0.5, 0.5, "No data available",
|
| 661 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 662 |
+
plt.axis('off')
|
| 663 |
+
return fig
|
| 664 |
+
|
| 665 |
+
# Define metrics based on ratio type
|
| 666 |
+
if ratio_type == 'profitability':
|
| 667 |
+
if 'Total Revenue' in statement.index and 'Net Income' in statement.index:
|
| 668 |
+
revenue = statement.loc['Total Revenue']
|
| 669 |
+
net_income = statement.loc['Net Income']
|
| 670 |
+
net_margin = (net_income / revenue) * 100
|
| 671 |
+
|
| 672 |
+
if 'Gross Profit' in statement.index and 'Operating Income' in statement.index:
|
| 673 |
+
gross_profit = statement.loc['Gross Profit']
|
| 674 |
+
operating_income = statement.loc['Operating Income']
|
| 675 |
+
|
| 676 |
+
gross_margin = (gross_profit / revenue) * 100
|
| 677 |
+
operating_margin = (operating_income / revenue) * 100
|
| 678 |
+
|
| 679 |
+
# Create DataFrame for plotting
|
| 680 |
+
ratios_df = pd.DataFrame({
|
| 681 |
+
'Gross Margin': gross_margin,
|
| 682 |
+
'Operating Margin': operating_margin,
|
| 683 |
+
'Net Margin': net_margin
|
| 684 |
+
})
|
| 685 |
+
else:
|
| 686 |
+
# Only net margin available
|
| 687 |
+
ratios_df = pd.DataFrame({
|
| 688 |
+
'Net Margin': net_margin
|
| 689 |
+
})
|
| 690 |
+
else:
|
| 691 |
+
fig = plt.figure(figsize=(10, 6))
|
| 692 |
+
plt.text(0.5, 0.5, "Insufficient data for profitability ratios",
|
| 693 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 694 |
+
plt.axis('off')
|
| 695 |
+
return fig
|
| 696 |
+
|
| 697 |
+
elif ratio_type == 'efficiency':
|
| 698 |
+
if 'Total Assets' in statement.index and 'Net Income' in statement.index:
|
| 699 |
+
assets = statement.loc['Total Assets']
|
| 700 |
+
net_income = statement.loc['Net Income']
|
| 701 |
+
roa = (net_income / assets) * 100
|
| 702 |
+
|
| 703 |
+
if 'Total Equity Gross Minority Interest' in statement.index:
|
| 704 |
+
equity = statement.loc['Total Equity Gross Minority Interest']
|
| 705 |
+
roe = (net_income / equity) * 100
|
| 706 |
+
|
| 707 |
+
# Create DataFrame for plotting
|
| 708 |
+
ratios_df = pd.DataFrame({
|
| 709 |
+
'Return on Assets': roa,
|
| 710 |
+
'Return on Equity': roe
|
| 711 |
+
})
|
| 712 |
+
else:
|
| 713 |
+
# Only ROA available
|
| 714 |
+
ratios_df = pd.DataFrame({
|
| 715 |
+
'Return on Assets': roa
|
| 716 |
+
})
|
| 717 |
+
else:
|
| 718 |
+
fig = plt.figure(figsize=(10, 6))
|
| 719 |
+
plt.text(0.5, 0.5, "Insufficient data for efficiency ratios",
|
| 720 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 721 |
+
plt.axis('off')
|
| 722 |
+
return fig
|
| 723 |
+
|
| 724 |
+
elif ratio_type == 'liquidity':
|
| 725 |
+
if 'Current Assets' in statement.index and 'Current Liabilities' in statement.index:
|
| 726 |
+
current_assets = statement.loc['Current Assets']
|
| 727 |
+
current_liabilities = statement.loc['Current Liabilities']
|
| 728 |
+
current_ratio = current_assets / current_liabilities
|
| 729 |
+
|
| 730 |
+
if 'Inventory' in statement.index:
|
| 731 |
+
inventory = statement.loc['Inventory']
|
| 732 |
+
quick_ratio = (current_assets - inventory) / current_liabilities
|
| 733 |
+
|
| 734 |
+
# Create DataFrame for plotting
|
| 735 |
+
ratios_df = pd.DataFrame({
|
| 736 |
+
'Current Ratio': current_ratio,
|
| 737 |
+
'Quick Ratio': quick_ratio
|
| 738 |
+
})
|
| 739 |
+
else:
|
| 740 |
+
# Only current ratio available
|
| 741 |
+
ratios_df = pd.DataFrame({
|
| 742 |
+
'Current Ratio': current_ratio
|
| 743 |
+
})
|
| 744 |
+
else:
|
| 745 |
+
fig = plt.figure(figsize=(10, 6))
|
| 746 |
+
plt.text(0.5, 0.5, "Insufficient data for liquidity ratios",
|
| 747 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 748 |
+
plt.axis('off')
|
| 749 |
+
return fig
|
| 750 |
+
|
| 751 |
+
else: # Default case
|
| 752 |
+
fig = plt.figure(figsize=(10, 6))
|
| 753 |
+
plt.text(0.5, 0.5, f"Unknown ratio type: {ratio_type}",
|
| 754 |
+
horizontalalignment='center', verticalalignment='center', fontsize=14)
|
| 755 |
+
plt.axis('off')
|
| 756 |
+
return fig
|
| 757 |
+
|
| 758 |
+
# Create the chart
|
| 759 |
+
fig = plt.figure(figsize=(10, 6))
|
| 760 |
+
|
| 761 |
+
# Plot ratios
|
| 762 |
+
ax = ratios_df.plot(kind='line', marker='o', ax=plt.gca())
|
| 763 |
+
|
| 764 |
+
# Format y-axis as percentage for profitability and efficiency ratios
|
| 765 |
+
if ratio_type in ['profitability', 'efficiency']:
|
| 766 |
+
ax.yaxis.set_major_formatter(mtick.PercentFormatter())
|
| 767 |
+
|
| 768 |
+
# Add value labels at data points
|
| 769 |
+
for line, ratio_name in zip(ax.get_lines(), ratios_df.columns):
|
| 770 |
+
x_data, y_data = line.get_data()
|
| 771 |
+
for x, y in zip(x_data, y_data):
|
| 772 |
+
if ratio_type in ['profitability', 'efficiency']:
|
| 773 |
+
label = f'{y:.1f}%'
|
| 774 |
+
else:
|
| 775 |
+
label = f'{y:.2f}'
|
| 776 |
+
ax.annotate(label, (x, y), textcoords="offset points",
|
| 777 |
+
xytext=(0,5), ha='center', fontsize=8)
|
| 778 |
+
|
| 779 |
+
plt.title(title, fontsize=14)
|
| 780 |
+
|
| 781 |
+
if ratio_type == 'profitability':
|
| 782 |
+
plt.ylabel('Margin (%)')
|
| 783 |
+
elif ratio_type == 'efficiency':
|
| 784 |
+
plt.ylabel('Return (%)')
|
| 785 |
+
elif ratio_type == 'liquidity':
|
| 786 |
+
plt.ylabel('Ratio')
|
| 787 |
+
|
| 788 |
+
plt.grid(True, alpha=0.3)
|
| 789 |
+
plt.legend(loc='best')
|
| 790 |
+
plt.tight_layout()
|
| 791 |
+
|
| 792 |
+
return fig
|
valuation.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class DCFValuation:
|
| 2 |
+
def __init__(self):
|
| 3 |
+
pass
|
| 4 |
+
|
| 5 |
+
def calculate_dcf(self, fcf, growth_rate, discount_rate, years):
|
| 6 |
+
"""
|
| 7 |
+
Calculate the Discounted Cash Flow (DCF) valuation
|
| 8 |
+
|
| 9 |
+
Parameters:
|
| 10 |
+
- fcf: Free Cash Flow (most recent year)
|
| 11 |
+
- growth_rate: Expected annual growth rate (decimal)
|
| 12 |
+
- discount_rate: Discount rate (decimal)
|
| 13 |
+
- years: Number of years to project
|
| 14 |
+
|
| 15 |
+
Returns:
|
| 16 |
+
- Present value of future cash flows plus terminal value
|
| 17 |
+
"""
|
| 18 |
+
if fcf is None:
|
| 19 |
+
raise ValueError("Free Cash Flow data is not available")
|
| 20 |
+
|
| 21 |
+
if fcf <= 0:
|
| 22 |
+
# For companies with negative or zero FCF, we'll use a small positive value
|
| 23 |
+
# This is a simplification - in reality, you might want to use other valuation methods
|
| 24 |
+
fcf = 1000000 # Use a nominal value of $1M
|
| 25 |
+
print(f"Warning: Using nominal FCF value of $1M for DCF calculation due to non-positive actual FCF")
|
| 26 |
+
|
| 27 |
+
if growth_rate < 0 or growth_rate > 2.5:
|
| 28 |
+
raise ValueError("Growth rate should be between 0 and 2.5 (0% to 250%)")
|
| 29 |
+
|
| 30 |
+
if discount_rate <= 0 or discount_rate > 0.3:
|
| 31 |
+
raise ValueError("Discount rate should be between 0 and 0.3 (0% to 30%)")
|
| 32 |
+
|
| 33 |
+
if years <= 0:
|
| 34 |
+
raise ValueError("Projection years must be positive")
|
| 35 |
+
|
| 36 |
+
# Calculate present value of projected cash flows
|
| 37 |
+
pv_fcf = 0
|
| 38 |
+
for year in range(1, years + 1):
|
| 39 |
+
projected_fcf = fcf * (1 + growth_rate) ** year
|
| 40 |
+
pv_fcf += projected_fcf / (1 + discount_rate) ** year
|
| 41 |
+
|
| 42 |
+
# Calculate terminal value (Gordon Growth Model)
|
| 43 |
+
# Assume long-term growth rate is lower than initial growth rate
|
| 44 |
+
# For high growth companies, cap the terminal growth rate at a reasonable level
|
| 45 |
+
terminal_growth_rate = min(growth_rate, 0.04) # Cap at 4% for sustainability
|
| 46 |
+
|
| 47 |
+
# For very high growth rates, use a more aggressive reduction to terminal rate
|
| 48 |
+
if growth_rate > 0.5:
|
| 49 |
+
# For high growth companies, use a more gradual approach to terminal value
|
| 50 |
+
# This simulates a company with high initial growth that normalizes over time
|
| 51 |
+
terminal_value = 0
|
| 52 |
+
transition_years = min(5, years) # Use up to 5 transition years
|
| 53 |
+
|
| 54 |
+
# Last projected FCF
|
| 55 |
+
last_fcf = fcf * (1 + growth_rate) ** years
|
| 56 |
+
|
| 57 |
+
# Calculate terminal value with gradual growth reduction
|
| 58 |
+
terminal_value = last_fcf * (1 + terminal_growth_rate) / (discount_rate - terminal_growth_rate)
|
| 59 |
+
else:
|
| 60 |
+
# Standard terminal value calculation
|
| 61 |
+
terminal_value = fcf * (1 + growth_rate) ** years * (1 + terminal_growth_rate) / (discount_rate - terminal_growth_rate)
|
| 62 |
+
|
| 63 |
+
# Discount terminal value to present
|
| 64 |
+
pv_terminal_value = terminal_value / (1 + discount_rate) ** years
|
| 65 |
+
|
| 66 |
+
# Total company value
|
| 67 |
+
company_value = pv_fcf + pv_terminal_value
|
| 68 |
+
|
| 69 |
+
return company_value
|
| 70 |
+
|
| 71 |
+
def calculate_per_share_value(self, company_value, shares_outstanding):
|
| 72 |
+
"""
|
| 73 |
+
Calculate per share value
|
| 74 |
+
|
| 75 |
+
Parameters:
|
| 76 |
+
- company_value: Total company value from DCF
|
| 77 |
+
- shares_outstanding: Number of shares outstanding
|
| 78 |
+
|
| 79 |
+
Returns:
|
| 80 |
+
- Value per share
|
| 81 |
+
"""
|
| 82 |
+
if shares_outstanding is None or shares_outstanding == 'N/A':
|
| 83 |
+
raise ValueError("Shares outstanding data is not available")
|
| 84 |
+
|
| 85 |
+
if shares_outstanding <= 0:
|
| 86 |
+
raise ValueError("Shares outstanding must be positive")
|
| 87 |
+
|
| 88 |
+
return company_value / shares_outstanding
|