aboutsummaryrefslogtreecommitdiff
path: root/services/valuation_service.py
blob: f876f78c63ff2e7175c28e23c76f2e551aabebb7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
"""DCF valuation engine — Gordon Growth Model."""
import numpy as np
import pandas as pd


def run_dcf(
    fcf_series: pd.Series,
    shares_outstanding: float,
    wacc: float = 0.10,
    terminal_growth: float = 0.03,
    projection_years: int = 5,
) -> dict:
    """
    Run a DCF model and return per-year breakdown plus intrinsic value per share.

    Args:
        fcf_series: Annual FCF values, most recent first (yfinance order).
        shares_outstanding: Diluted shares outstanding.
        wacc: Weighted average cost of capital (decimal, e.g. 0.10).
        terminal_growth: Perpetuity growth rate (decimal, e.g. 0.03).
        projection_years: Number of years to project FCFs.

    Returns:
        dict with keys:
            intrinsic_value_per_share, total_pv, terminal_value_pv,
            fcf_pv_sum, years, projected_fcfs, discounted_fcfs,
            growth_rate_used
    """
    if fcf_series.empty or shares_outstanding <= 0:
        return {}

    # Use last N years of FCF (sorted oldest → newest)
    historical = fcf_series.sort_index().dropna().values
    if len(historical) < 2:
        return {}

    # Compute average YoY growth rate from historical FCF
    growth_rates = []
    for i in range(1, len(historical)):
        if historical[i - 1] != 0:
            g = (historical[i] - historical[i - 1]) / abs(historical[i - 1])
            growth_rates.append(g)

    # Cap growth rate to reasonable bounds [-0.5, 0.5]
    raw_growth = float(np.median(growth_rates)) if growth_rates else 0.05
    growth_rate = max(-0.50, min(0.50, raw_growth))

    base_fcf = float(historical[-1])  # most recent FCF

    # Project FCFs
    projected_fcfs = []
    for year in range(1, projection_years + 1):
        fcf = base_fcf * ((1 + growth_rate) ** year)
        projected_fcfs.append(fcf)

    # Discount projected FCFs
    discounted_fcfs = []
    for i, fcf in enumerate(projected_fcfs, start=1):
        pv = fcf / ((1 + wacc) ** i)
        discounted_fcfs.append(pv)

    fcf_pv_sum = sum(discounted_fcfs)

    # Terminal value (Gordon Growth Model)
    terminal_fcf = projected_fcfs[-1] * (1 + terminal_growth)
    terminal_value = terminal_fcf / (wacc - terminal_growth)
    terminal_value_pv = terminal_value / ((1 + wacc) ** projection_years)

    total_pv = fcf_pv_sum + terminal_value_pv
    intrinsic_value_per_share = total_pv / shares_outstanding

    return {
        "intrinsic_value_per_share": intrinsic_value_per_share,
        "total_pv": total_pv,
        "terminal_value_pv": terminal_value_pv,
        "fcf_pv_sum": fcf_pv_sum,
        "years": list(range(1, projection_years + 1)),
        "projected_fcfs": projected_fcfs,
        "discounted_fcfs": discounted_fcfs,
        "growth_rate_used": growth_rate,
        "base_fcf": base_fcf,
    }