aboutsummaryrefslogtreecommitdiff
path: root/services/valuation_service.py
blob: 855984279d2a346902463b8dba0d1a840c1afdb5 (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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
"""Valuation engines for DCF and EV/EBITDA."""
import numpy as np
import pandas as pd


GROWTH_FLOOR = -0.50
GROWTH_CAP = 0.50
MIN_BASE_MAGNITUDE = 1e-9


def _cap_growth(value: float) -> float:
    return max(GROWTH_FLOOR, min(GROWTH_CAP, float(value)))


def compute_historical_growth_rate(fcf_series: pd.Series) -> float | None:
    """
    Return a capped median YoY FCF growth rate from historical data.

    Notes:
    - skips periods with near-zero prior FCF
    - skips sign-flip periods (negative to positive or vice versa), since the
      implied "growth rate" is usually not economically meaningful
    """
    historical = fcf_series.sort_index().dropna().astype(float).values
    if len(historical) < 2:
        return None

    growth_rates = []
    for i in range(1, len(historical)):
        previous = float(historical[i - 1])
        current = float(historical[i])

        if abs(previous) < MIN_BASE_MAGNITUDE:
            continue
        if previous <= 0 or current <= 0:
            continue

        growth_rates.append((current - previous) / previous)

    if not growth_rates:
        return None

    return _cap_growth(float(np.median(growth_rates)))


def run_dcf(
    fcf_series: pd.Series,
    shares_outstanding: float,
    wacc: float = 0.10,
    terminal_growth: float = 0.03,
    projection_years: int = 5,
    growth_rate_override: float | None = None,
    total_debt: float = 0.0,
    cash_and_equivalents: float = 0.0,
    preferred_equity: float = 0.0,
    minority_interest: float = 0.0,
) -> dict:
    """
    Run a simple FCFF-style DCF and bridge enterprise value to equity value.

    Returns an error payload when inputs are mathematically invalid.
    """
    if fcf_series.empty or shares_outstanding <= 0:
        return {}

    historical = fcf_series.sort_index().dropna().astype(float).values
    if len(historical) < 2:
        return {}

    if wacc <= 0:
        return {"error": "WACC must be greater than 0%."}
    if terminal_growth >= wacc:
        return {"error": "Terminal growth must be lower than WACC."}

    if growth_rate_override is not None:
        growth_rate = _cap_growth(growth_rate_override)
    else:
        historical_growth = compute_historical_growth_rate(fcf_series)
        growth_rate = historical_growth if historical_growth is not None else 0.05

    base_fcf = float(historical[-1])
    if base_fcf <= 0:
        return {
            "error": (
                "DCF is not meaningful with zero or negative base free cash flow. "
                "Use comps, EV/EBITDA, or adjust the model only after underwriting a credible FCF turnaround."
            )
        }

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

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

    fcf_pv_sum = float(sum(discounted_fcfs))

    terminal_fcf = float(projected_fcfs[-1]) * (1 + terminal_growth)
    terminal_value = terminal_fcf / (wacc - terminal_growth)
    terminal_value_pv = terminal_value / ((1 + wacc) ** projection_years)

    enterprise_value = fcf_pv_sum + terminal_value_pv

    total_debt = float(total_debt or 0.0)
    cash_and_equivalents = float(cash_and_equivalents or 0.0)
    preferred_equity = float(preferred_equity or 0.0)
    minority_interest = float(minority_interest or 0.0)

    net_debt = total_debt - cash_and_equivalents
    equity_value = enterprise_value - net_debt - preferred_equity - minority_interest
    intrinsic_value_per_share = equity_value / shares_outstanding

    return {
        "intrinsic_value_per_share": intrinsic_value_per_share,
        "enterprise_value": enterprise_value,
        "equity_value": equity_value,
        "net_debt": net_debt,
        "cash_and_equivalents": cash_and_equivalents,
        "total_debt": total_debt,
        "preferred_equity": preferred_equity,
        "minority_interest": minority_interest,
        "terminal_value": terminal_value,
        "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,
    }


def run_ev_ebitda(
    ebitda: float,
    total_debt: float,
    total_cash: float,
    shares_outstanding: float,
    target_multiple: float,
) -> dict:
    """Derive implied equity value per share from an EV/EBITDA multiple."""
    if not ebitda or ebitda <= 0:
        return {}
    if not shares_outstanding or shares_outstanding <= 0:
        return {}
    if not target_multiple or target_multiple <= 0:
        return {}

    implied_ev = ebitda * target_multiple
    net_debt = (total_debt or 0.0) - (total_cash or 0.0)
    equity_value = implied_ev - net_debt

    return {
        "implied_ev": implied_ev,
        "net_debt": net_debt,
        "equity_value": equity_value,
        "implied_price_per_share": equity_value / shares_outstanding,
        "target_multiple_used": target_multiple,
    }