summaryrefslogtreecommitdiff
path: root/backend/tests
diff options
context:
space:
mode:
Diffstat (limited to 'backend/tests')
-rw-r--r--backend/tests/test_dcf_advanced.py148
-rw-r--r--backend/tests/test_dcf_edge_cases.py155
-rw-r--r--backend/tests/test_dcf_math.py205
-rw-r--r--backend/tests/test_valuation_advanced_endpoint.py126
4 files changed, 634 insertions, 0 deletions
diff --git a/backend/tests/test_dcf_advanced.py b/backend/tests/test_dcf_advanced.py
new file mode 100644
index 0000000..20f6921
--- /dev/null
+++ b/backend/tests/test_dcf_advanced.py
@@ -0,0 +1,148 @@
+import pytest
+
+from app.services.data_service import _run_dcf_explicit_build
+
+
+_APPROX = pytest.approx
+
+
+def test_run_dcf_explicit_build_two_year_reference_scenario() -> None:
+ # Given
+ base_revenue = 1_000.0
+
+ # When
+ result = _run_dcf_explicit_build(
+ base_revenue=base_revenue,
+ revenue_growth=[0.10, 0.05],
+ ebitda_margin=[0.30, 0.32],
+ dna_pct_revenue=[0.05, 0.05],
+ capex_pct_revenue=[0.07, 0.07],
+ nwc_chg_pct_delta_rev=[0.02, 0.02],
+ tax_rate=[0.25, 0.25],
+ wacc=0.10,
+ terminal_growth=0.03,
+ shares_outstanding=100.0,
+ total_debt=50.0,
+ cash=10.0,
+ preferred_equity=5.0,
+ minority_interest=3.0,
+ projection_years=2,
+ )
+
+ # Then
+ assert result["available"] is True
+ assert result["base_fcf"] == _APPROX(184.25)
+ assert result["fcf_pv_sum"] == _APPROX(357.42928814344134)
+ assert result["terminal_value_pv"] == _APPROX(2_674.37743224556)
+ assert result["enterprise_value"] == _APPROX(3_031.8067203890014)
+ assert result["net_debt"] == 40.0
+ assert result["equity_value"] == _APPROX(2_983.8067203890014)
+ assert result["intrinsic_value_per_share"] == _APPROX(29.838067203890013)
+ assert result["wacc"] == 0.10
+ assert result["terminal_growth"] == 0.03
+ assert result["projection_years"] == 2
+
+
+def test_run_dcf_explicit_build_terminal_growth_at_wacc_returns_error() -> None:
+ # Given / When
+ result = _run_dcf_explicit_build(
+ base_revenue=1_000.0,
+ revenue_growth=[0.05],
+ ebitda_margin=[0.30],
+ dna_pct_revenue=[0.05],
+ capex_pct_revenue=[0.07],
+ nwc_chg_pct_delta_rev=[0.02],
+ tax_rate=[0.25],
+ wacc=0.10,
+ terminal_growth=0.10,
+ shares_outstanding=100.0,
+ total_debt=0.0,
+ cash=0.0,
+ preferred_equity=0.0,
+ minority_interest=0.0,
+ projection_years=1,
+ )
+
+ # Then
+ assert result["available"] is True
+ assert "Terminal growth" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_explicit_build_zero_wacc_returns_error() -> None:
+ # Given / When
+ result = _run_dcf_explicit_build(
+ base_revenue=1_000.0,
+ revenue_growth=[0.05],
+ ebitda_margin=[0.30],
+ dna_pct_revenue=[0.05],
+ capex_pct_revenue=[0.07],
+ nwc_chg_pct_delta_rev=[0.02],
+ tax_rate=[0.25],
+ wacc=0.0,
+ terminal_growth=0.03,
+ shares_outstanding=100.0,
+ total_debt=0.0,
+ cash=0.0,
+ preferred_equity=0.0,
+ minority_interest=0.0,
+ projection_years=1,
+ )
+
+ # Then
+ assert result["available"] is True
+ assert "WACC" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+@pytest.mark.parametrize("shares_outstanding", [0.0, -1.0])
+def test_run_dcf_explicit_build_nonpositive_shares_return_error(shares_outstanding: float) -> None:
+ # Given / When
+ result = _run_dcf_explicit_build(
+ base_revenue=1_000.0,
+ revenue_growth=[0.05],
+ ebitda_margin=[0.30],
+ dna_pct_revenue=[0.05],
+ capex_pct_revenue=[0.07],
+ nwc_chg_pct_delta_rev=[0.02],
+ tax_rate=[0.25],
+ wacc=0.10,
+ terminal_growth=0.03,
+ shares_outstanding=shares_outstanding,
+ total_debt=0.0,
+ cash=0.0,
+ preferred_equity=0.0,
+ minority_interest=0.0,
+ projection_years=1,
+ )
+
+ # Then
+ assert result["available"] is True
+ assert "Shares outstanding" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_explicit_build_zero_base_revenue_returns_error() -> None:
+ # Given / When
+ result = _run_dcf_explicit_build(
+ base_revenue=0.0,
+ revenue_growth=[0.05],
+ ebitda_margin=[0.30],
+ dna_pct_revenue=[0.05],
+ capex_pct_revenue=[0.07],
+ nwc_chg_pct_delta_rev=[0.02],
+ tax_rate=[0.25],
+ wacc=0.10,
+ terminal_growth=0.03,
+ shares_outstanding=100.0,
+ total_debt=0.0,
+ cash=0.0,
+ preferred_equity=0.0,
+ minority_interest=0.0,
+ projection_years=1,
+ )
+
+ # Then
+ assert result["available"] is True
+ assert "Base revenue" in result["error"]
+ assert "intrinsic_value_per_share" not in result
diff --git a/backend/tests/test_dcf_edge_cases.py b/backend/tests/test_dcf_edge_cases.py
new file mode 100644
index 0000000..94a960b
--- /dev/null
+++ b/backend/tests/test_dcf_edge_cases.py
@@ -0,0 +1,155 @@
+"""Boundary and invalid-input tests for the DCF engine."""
+
+import pandas as pd
+import pytest
+
+from app.services.data_service import _run_dcf
+
+
+def _fcf_series(base: float, growth: float) -> pd.Series:
+ """Return a two-point historical FCF series whose median YoY growth equals `growth`."""
+ prior = base / (1 + growth)
+ return pd.Series([prior, base], index=pd.to_datetime(["2023-09-30", "2024-09-30"]))
+
+
+def _expected_mid_year_enterprise_value(
+ base_fcf: float,
+ growth: float,
+ wacc: float,
+ terminal_growth: float,
+ projection_years: int,
+) -> float:
+ projected = [base_fcf * ((1 + growth) ** year) for year in range(1, projection_years + 1)]
+ discounted = [fcf / ((1 + wacc) ** (year - 0.5)) for year, fcf in enumerate(projected, start=1)]
+ terminal_fcf = projected[-1] * (1 + terminal_growth)
+ terminal_value = terminal_fcf / (wacc - terminal_growth)
+ terminal_value_pv = terminal_value / ((1 + wacc) ** (projection_years - 0.5))
+ return sum(discounted) + terminal_value_pv
+
+
+def test_run_dcf_two_year_horizon_uses_mid_year_discounting() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ expected_enterprise_value = _expected_mid_year_enterprise_value(
+ base_fcf=100.0,
+ growth=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=2,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=2,
+ )
+
+ assert result["enterprise_value"] == pytest.approx(expected_enterprise_value)
+
+
+def test_run_dcf_terminal_growth_equals_wacc_is_error() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.10,
+ projection_years=5,
+ )
+ assert "error" in result
+ assert "Terminal growth" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_terminal_growth_above_wacc_is_error() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.11,
+ projection_years=5,
+ )
+ assert "error" in result
+ assert "Terminal growth" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_zero_wacc_is_error() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.0,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert "error" in result
+ assert "WACC" in result["error"]
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_negative_base_fcf_is_error() -> None:
+ """The most recent FCF is negative, making the DCF not meaningful."""
+ fcf = pd.Series([100.0, -50.0], index=pd.to_datetime(["2023-09-30", "2024-09-30"]))
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert "error" in result
+ assert "negative" in result["error"].lower() or "zero" in result["error"].lower()
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_zero_base_fcf_is_error() -> None:
+ fcf = pd.Series([100.0, 0.0], index=pd.to_datetime(["2023-09-30", "2024-09-30"]))
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert "error" in result
+ assert "zero" in result["error"].lower()
+ assert "intrinsic_value_per_share" not in result
+
+
+def test_run_dcf_zero_shares_returns_empty() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=0.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert result == {}
+
+
+def test_run_dcf_negative_shares_returns_empty() -> None:
+ fcf = _fcf_series(100.0, 0.05)
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=-1.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert result == {}
+
+
+def test_run_dcf_insufficient_history_returns_empty() -> None:
+ fcf = pd.Series([100.0], index=pd.to_datetime(["2024-09-30"]))
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+ assert result == {}
diff --git a/backend/tests/test_dcf_math.py b/backend/tests/test_dcf_math.py
new file mode 100644
index 0000000..0418a02
--- /dev/null
+++ b/backend/tests/test_dcf_math.py
@@ -0,0 +1,205 @@
+"""Exact-math verification tests for the DCF engine.
+
+Each scenario independently recomputes the expected projection, discounting,
+terminal value, enterprise value, equity value, and per-share intrinsic value,
+then asserts the implementation matches. This makes the test itself auditable.
+"""
+
+import pandas as pd
+import pytest
+
+from app.services.data_service import _run_dcf
+
+
+def _fcf_series(prior: float, base: float) -> pd.Series:
+ """Return a two-point historical FCF series: prior -> base (most recent)."""
+ return pd.Series([prior, base], index=pd.to_datetime(["2023-09-30", "2024-09-30"]))
+
+
+def _expected_dcf(
+ base_fcf: float,
+ growth_rate: float,
+ wacc: float,
+ terminal_growth: float,
+ projection_years: int,
+ shares_outstanding: float,
+ total_debt: float = 0.0,
+ cash_and_equivalents: float = 0.0,
+ preferred_equity: float = 0.0,
+ minority_interest: float = 0.0,
+) -> dict:
+ """Independent reference implementation of the DCF math used by `_run_dcf`."""
+ projected = [base_fcf * ((1 + growth_rate) ** yr) for yr in range(1, projection_years + 1)]
+ discounted = [fcf / ((1 + wacc) ** (year - 0.5)) for year, fcf in enumerate(projected, start=1)]
+ fcf_pv_sum = sum(discounted)
+
+ terminal_fcf = projected[-1] * (1 + terminal_growth)
+ terminal_value = terminal_fcf / (wacc - terminal_growth)
+ terminal_value_pv = terminal_value / ((1 + wacc) ** (projection_years - 0.5))
+
+ enterprise_value = fcf_pv_sum + terminal_value_pv
+ 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 {
+ "fcf_pv_sum": fcf_pv_sum,
+ "terminal_value_pv": terminal_value_pv,
+ "enterprise_value": enterprise_value,
+ "net_debt": net_debt,
+ "equity_value": equity_value,
+ "intrinsic_value_per_share": intrinsic_value_per_share,
+ }
+
+
+_APPROX = pytest.approx
+
+
+def test_run_dcf_one_year_with_equity_bridge() -> None:
+ """1-year projection with debt, cash, preferred, and minority claims."""
+ fcf = _fcf_series(100.0, 105.0) # exact 5% growth
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=1,
+ shares_outstanding=10.0,
+ total_debt=20.0,
+ cash_and_equivalents=5.0,
+ preferred_equity=2.0,
+ minority_interest=3.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=1,
+ total_debt=20.0,
+ cash_and_equivalents=5.0,
+ preferred_equity=2.0,
+ minority_interest=3.0,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["growth_rate_used"] == _APPROX(0.05)
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["net_debt"] == expected["net_debt"]
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_three_years_no_claims() -> None:
+ """3-year projection with a clean capital structure."""
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=3,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=3,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+ assert result["net_debt"] == 0.0
+
+
+def test_run_dcf_five_years_default_horizon() -> None:
+ """5-year projection matching the API default horizon."""
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+
+ assert result["base_fcf"] == 105.0
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["equity_value"] == _APPROX(expected["equity_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_zero_growth_flat_fcf() -> None:
+ """Edge of the model: zero growth projects a flat FCF stream."""
+ fcf = _fcf_series(100.0, 100.0) # exact 0% growth
+ expected = _expected_dcf(
+ base_fcf=100.0,
+ growth_rate=0.0,
+ wacc=0.10,
+ terminal_growth=0.02,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.02,
+ projection_years=5,
+ )
+
+ assert result["growth_rate_used"] == _APPROX(0.0)
+ assert result["fcf_pv_sum"] == _APPROX(expected["fcf_pv_sum"])
+ assert result["terminal_value_pv"] == _APPROX(expected["terminal_value_pv"])
+ assert result["enterprise_value"] == _APPROX(expected["enterprise_value"])
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
+
+
+def test_run_dcf_uses_historical_growth_rate() -> None:
+ """End-to-end check that the growth-rate estimator feeds the projection.
+
+ A series rising exactly 5% per year should produce the same 5-year result as
+ the hand-computed 5-year scenario (within float tolerance).
+ """
+ fcf = _fcf_series(100.0, 105.0)
+ expected = _expected_dcf(
+ base_fcf=105.0,
+ growth_rate=0.05,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ shares_outstanding=10.0,
+ )
+
+ result = _run_dcf(
+ fcf_series=fcf,
+ shares_outstanding=10.0,
+ wacc=0.10,
+ terminal_growth=0.03,
+ projection_years=5,
+ )
+
+ assert result["growth_rate_used"] == _APPROX(0.05)
+ assert result["intrinsic_value_per_share"] == _APPROX(expected["intrinsic_value_per_share"])
diff --git a/backend/tests/test_valuation_advanced_endpoint.py b/backend/tests/test_valuation_advanced_endpoint.py
new file mode 100644
index 0000000..3cdb857
--- /dev/null
+++ b/backend/tests/test_valuation_advanced_endpoint.py
@@ -0,0 +1,126 @@
+import pandas as pd
+import pytest
+from fastapi.testclient import TestClient
+
+from app import main
+from app.services import data_service
+
+
+def annual_frame(rows: dict[str, list[float]]) -> pd.DataFrame:
+ columns = pd.to_datetime(["2024-09-30", "2023-09-30", "2022-09-30", "2021-09-30"])
+ return pd.DataFrame(rows, index=columns).T
+
+
+def quarterly_frame(rows: dict[str, list[float]]) -> pd.DataFrame:
+ columns = pd.to_datetime(["2025-12-31", "2025-09-30", "2025-06-30", "2025-03-31"])
+ return pd.DataFrame(rows, index=columns).T
+
+
+def clear_valuation_cache() -> None:
+ data_service.VALUATION_CACHE.clear()
+
+
+def advanced_payload() -> dict[str, float | int | list[float]]:
+ return {
+ "base_revenue": 1_000.0,
+ "revenue_growth": [0.10, 0.05],
+ "ebitda_margin": [0.30, 0.32],
+ "dna_pct_revenue": [0.05, 0.05],
+ "capex_pct_revenue": [0.07, 0.07],
+ "nwc_chg_pct_delta_rev": [0.02, 0.02],
+ "tax_rate": [0.25, 0.25],
+ "wacc": 0.10,
+ "terminal_growth": 0.03,
+ "projection_years": 2,
+ }
+
+
+def test_simple_valuation_includes_five_by_five_sensitivity_grid(monkeypatch: pytest.MonkeyPatch) -> None:
+ # Given: historical free cash flow data for the simple DCF endpoint.
+ clear_valuation_cache()
+ cash_flow = annual_frame({
+ "Operating Cash Flow": [100.0, 90.0, 80.0, 70.0],
+ "Capital Expenditure": [-10.0, -9.0, -8.0, -7.0],
+ })
+ monkeypatch.setattr(data_service, "get_cash_flow", lambda symbol, quarterly=False: pd.DataFrame() if quarterly else cash_flow)
+ monkeypatch.setattr(data_service, "get_income_statement", lambda symbol, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_balance_sheet", lambda symbol, quarterly=False: pd.DataFrame())
+ monkeypatch.setattr(data_service, "get_company_info", lambda symbol: {"currentPrice": 150.0})
+ monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 1_000_000_000.0)
+
+ # When: the simple valuation service is called.
+ result = data_service.get_valuation("ref")
+
+ # Then: the DCF result includes the mandated WACC x terminal-growth matrix.
+ dcf = result["dcf"]
+ sensitivity = dcf["sensitivity"]
+ assert result["currency"] == "USD"
+ assert result["currency_warning"] is None
+ assert sensitivity["wacc"] == [0.08, 0.09, 0.10, 0.11, 0.12]
+ assert sensitivity["terminal_growth"] == [0.015, 0.02, 0.03, 0.04, 0.045]
+ assert len(sensitivity["implied_prices"]) == 5
+ assert all(len(row) == 5 for row in sensitivity["implied_prices"])
+ assert sensitivity["implied_prices"][2][2] == pytest.approx(dcf["intrinsic_value_per_share"])
+
+
+def test_advanced_valuation_endpoint_returns_inputs_and_sensitivity(monkeypatch: pytest.MonkeyPatch) -> None:
+ # Given: symbol-level balance sheet inputs and a base valuation shell.
+ clear_valuation_cache()
+ monkeypatch.setattr(
+ data_service,
+ "get_valuation",
+ lambda symbol: {
+ "symbol": "REF",
+ "current_price": 42.0,
+ "shares_outstanding": 100.0,
+ "dcf": {"available": False, "wacc": 0.10, "terminal_growth": 0.03, "projection_years": 5},
+ "ev_ebitda": {"available": False},
+ "ev_revenue": {"available": False},
+ "price_to_book": {"available": False},
+ },
+ )
+ monkeypatch.setattr(data_service, "get_shares_outstanding", lambda symbol: 100.0)
+ monkeypatch.setattr(
+ data_service,
+ "get_balance_sheet",
+ lambda symbol, quarterly=False: quarterly_frame({
+ "Total Debt": [50.0, 0.0, 0.0, 0.0],
+ "Cash And Cash Equivalents": [10.0, 0.0, 0.0, 0.0],
+ "Preferred Stock": [5.0, 0.0, 0.0, 0.0],
+ "Minority Interest": [3.0, 0.0, 0.0, 0.0],
+ }),
+ )
+ client = TestClient(main.app)
+
+ # When: the advanced valuation endpoint receives an explicit build payload.
+ response = client.post("/api/tickers/REF/valuation/advanced", json=advanced_payload())
+
+ # Then: it returns a full valuation response with base-case-centered sensitivity.
+ assert response.status_code == 200
+ body = response.json()
+ dcf = body["dcf"]
+ sensitivity = dcf["sensitivity"]
+ assert body["symbol"] == "REF"
+ assert body["currency"] == "USD"
+ assert body["currency_warning"] is None
+ assert dcf["advanced_inputs"] == advanced_payload()
+ assert dcf["intrinsic_value_per_share"] == pytest.approx(29.838067203890013)
+ assert sensitivity["wacc"] == [0.08, 0.09, 0.10, 0.11, 0.12]
+ assert sensitivity["terminal_growth"] == [0.015, 0.02, 0.03, 0.04, 0.045]
+ assert len(sensitivity["implied_prices"]) == 5
+ assert all(len(row) == 5 for row in sensitivity["implied_prices"])
+ assert sensitivity["implied_prices"][2][2] == pytest.approx(dcf["intrinsic_value_per_share"])
+
+
+def test_advanced_valuation_endpoint_rejects_short_projection_arrays() -> None:
+ # Given: a payload whose per-year arrays do not cover projection_years.
+ payload = advanced_payload()
+ payload["revenue_growth"] = [0.10]
+ client = TestClient(main.app)
+
+ # When: the invalid payload is submitted.
+ response = client.post("/api/tickers/REF/valuation/advanced", json=payload)
+
+ # Then: FastAPI returns a validation response instead of an unhandled exception.
+ assert response.status_code == 422
+ assert "projection_years" in response.text