vikramlingam commited on
Commit
d98f7af
·
verified ·
1 Parent(s): c0015ce

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +404 -0
  2. data_fetcher.py +232 -0
  3. requirements.txt +5 -0
  4. utils.py +792 -0
  5. 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