mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-20 22:28:05 +00:00
Complete migration from Streamlit to Next.js 14 App Router + FastAPI backend. Frontend (Next.js 14): - 10 pages: Overview, Research, Valuation, Technical, Markets, Earnings, News, Portfolio, Filings, Settings - Terminal Noir dark theme with custom Tailwind config - TradingView Lightweight Charts for candlestick/volume - Valuation: DCF, Sensitivity Matrix, Monte Carlo, Tornado, Reverse DCF - Financial Statements table with YoY growth badges and margin rows - SEC EDGAR inline filing viewer with section tabs - News split-view with iframe article embedding - Technical Analysis with RSI, MACD, Bollinger, Fibonacci, Moving Averages - Earnings beat/miss visualization - AI Copilot chat panel with Gemini integration Backend (FastAPI): - 13 routers: market_data, financials, valuation, technical, earnings, insider, edgar, news, portfolio, analysis, chat, estimates, fx - Services: DCF engine, Monte Carlo simulation, sensitivity analysis, risk metrics, SEC parser, technical indicators - yfinance + yahooquery data sources with fallback pattern - SQLite caching layer Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
125 lines
3.8 KiB
Python
125 lines
3.8 KiB
Python
"""Monte Carlo simulation for DCF valuation.
|
||
|
||
Runs N random DCF scenarios by sampling WACC and FCF growth from
|
||
normal distributions, then reports distributional statistics.
|
||
"""
|
||
|
||
from typing import Dict, Any, List
|
||
|
||
import numpy as np
|
||
|
||
|
||
def run_monte_carlo_dcf(
|
||
fcf: float,
|
||
wacc_mean: float,
|
||
wacc_std: float,
|
||
growth_mean: float,
|
||
growth_std: float,
|
||
term_growth: float,
|
||
total_debt: float,
|
||
cash: float,
|
||
shares: float,
|
||
n_simulations: int = 5000,
|
||
current_price: float | None = None,
|
||
) -> Dict[str, Any]:
|
||
"""Run a Monte Carlo DCF simulation.
|
||
|
||
Parameters
|
||
----------
|
||
fcf : float
|
||
Base free cash flow.
|
||
wacc_mean / wacc_std : float
|
||
Mean and standard deviation for WACC sampling (decimal, e.g. 0.09).
|
||
growth_mean / growth_std : float
|
||
Mean and standard deviation for FCF growth sampling (decimal).
|
||
term_growth : float
|
||
Terminal growth rate (constant across simulations).
|
||
total_debt, cash, shares : float
|
||
Balance-sheet items for equity bridge.
|
||
n_simulations : int
|
||
Number of Monte Carlo iterations (default 5 000).
|
||
current_price : float | None
|
||
Current market price; used to compute prob_above_current.
|
||
|
||
Returns
|
||
-------
|
||
dict
|
||
values – list of per-share intrinsic values (sorted)
|
||
percentile_10 – 10th percentile
|
||
median – 50th percentile
|
||
percentile_90 – 90th percentile
|
||
mean – arithmetic mean
|
||
prob_above_current – probability the simulated value exceeds current_price
|
||
current_price – echo back
|
||
n_simulations – echo back
|
||
"""
|
||
if shares <= 0 or fcf <= 0:
|
||
return {
|
||
"values": [],
|
||
"percentile_10": None,
|
||
"median": None,
|
||
"percentile_90": None,
|
||
"mean": None,
|
||
"prob_above_current": None,
|
||
"current_price": current_price,
|
||
"n_simulations": n_simulations,
|
||
}
|
||
|
||
rng = np.random.default_rng()
|
||
|
||
# Sample WACC and growth; clip to sensible bounds
|
||
waccs = rng.normal(wacc_mean, max(wacc_std, 1e-6), n_simulations)
|
||
waccs = np.clip(waccs, 0.01, 0.40)
|
||
|
||
growths = rng.normal(growth_mean, max(growth_std, 1e-6), n_simulations)
|
||
growths = np.clip(growths, -0.30, 0.60)
|
||
|
||
projection_years = 10
|
||
values: List[float] = []
|
||
|
||
for w, g in zip(waccs, growths):
|
||
if w <= term_growth:
|
||
continue
|
||
# 10-year two-stage DCF (simplified: constant growth then terminal)
|
||
pv = 0.0
|
||
fcft = float(fcf)
|
||
for t in range(1, projection_years + 1):
|
||
fcft *= (1 + g)
|
||
pv += fcft / ((1 + w) ** t)
|
||
tv = fcft * (1 + term_growth) / (w - term_growth)
|
||
pv += tv / ((1 + w) ** projection_years)
|
||
equity = pv - total_debt + cash
|
||
per_share = equity / shares
|
||
if per_share > 0:
|
||
values.append(round(per_share, 2))
|
||
|
||
if not values:
|
||
return {
|
||
"values": [],
|
||
"percentile_10": None,
|
||
"median": None,
|
||
"percentile_90": None,
|
||
"mean": None,
|
||
"prob_above_current": None,
|
||
"current_price": current_price,
|
||
"n_simulations": n_simulations,
|
||
}
|
||
|
||
arr = np.array(values)
|
||
arr.sort()
|
||
|
||
prob_above = None
|
||
if current_price is not None and current_price > 0:
|
||
prob_above = round(float(np.mean(arr > current_price) * 100), 1)
|
||
|
||
return {
|
||
"values": arr.tolist(),
|
||
"percentile_10": round(float(np.percentile(arr, 10)), 2),
|
||
"median": round(float(np.median(arr)), 2),
|
||
"percentile_90": round(float(np.percentile(arr, 90)), 2),
|
||
"mean": round(float(np.mean(arr)), 2),
|
||
"prob_above_current": prob_above,
|
||
"current_price": current_price,
|
||
"n_simulations": n_simulations,
|
||
}
|