File size: 4,625 Bytes
7880373
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""tests/test_valuation.py β€” unit tests for analytics/valuation.py.

Mocks yfinance so tests are hermetic and fast, following the same pattern
as tests/test_analyst.py.
"""
from __future__ import annotations

from unittest.mock import MagicMock, patch

import pytest

from analytics import valuation


@pytest.fixture(autouse=True)
def _no_cache(monkeypatch):
    monkeypatch.setattr("storage.earnings_cache.get", lambda *a, **k: None)
    monkeypatch.setattr("storage.earnings_cache.set", lambda *a, **k: None)


# ── fetch_multiples ────────────────────────────────────────────────────────────

def test_fetch_multiples_success():
    fake = MagicMock()
    fake.info = {
        "trailingPE": 45.2, "forwardPE": 32.1,
        "enterpriseToRevenue": 18.5, "enterpriseToEbitda": 25.0,
        "marketCap": 3_000_000_000_000,
    }
    with patch("yfinance.Ticker", return_value=fake):
        data, err = valuation.fetch_multiples("NVDA")

    assert err is None
    assert data["trailing_pe"] == 45.2
    assert data["forward_pe"] == 32.1
    assert data["ev_to_sales"] == 18.5
    assert data["ev_to_ebitda"] == 25.0


def test_fetch_multiples_fail_closed_when_nothing_available():
    fake = MagicMock()
    fake.info = {}
    with patch("yfinance.Ticker", return_value=fake):
        data, err = valuation.fetch_multiples("XYZ")

    assert data is None
    assert err is not None


def test_fetch_multiples_never_raises_on_yfinance_error():
    with patch("yfinance.Ticker", side_effect=RuntimeError("rate limit")):
        data, err = valuation.fetch_multiples("NVDA")
    assert data is None
    assert err is not None


def test_fetch_multiples_tolerates_nan_values():
    fake = MagicMock()
    fake.info = {"trailingPE": float("nan"), "forwardPE": 20.0}
    with patch("yfinance.Ticker", return_value=fake):
        data, err = valuation.fetch_multiples("NVDA")
    assert err is None
    assert data["trailing_pe"] is None
    assert data["forward_pe"] == 20.0


# ── fetch_peer_multiples ───────────────────────────────────────────────────────

def test_fetch_peer_multiples_includes_primary_and_peers():
    fake = MagicMock()
    fake.info = {"trailingPE": 40.0}
    with patch("yfinance.Ticker", return_value=fake):
        rows = valuation.fetch_peer_multiples("NVDA")

    tickers = [r["ticker"] for r in rows]
    assert "NVDA" in tickers
    assert "AMD" in tickers  # from the curated PEERS map
    assert "AVGO" in tickers


def test_fetch_peer_multiples_unknown_ticker_has_no_peers():
    fake = MagicMock()
    fake.info = {"trailingPE": 40.0}
    with patch("yfinance.Ticker", return_value=fake):
        rows = valuation.fetch_peer_multiples("SOMEOBSCURETICKER")
    assert len(rows) == 1
    assert rows[0]["ticker"] == "SOMEOBSCURETICKER"


def test_fetch_peer_multiples_omits_peer_whose_fetch_fails():
    def _side_effect(ticker):
        if ticker == "AMD":
            raise RuntimeError("no data")
        fake = MagicMock()
        fake.info = {"trailingPE": 40.0}
        return fake

    with patch("yfinance.Ticker", side_effect=_side_effect):
        rows = valuation.fetch_peer_multiples("NVDA")

    tickers = [r["ticker"] for r in rows]
    assert "AMD" not in tickers
    assert "NVDA" in tickers


# ── next_catalysts ─────────────────────────────────────────────────────────────

def test_next_catalysts_parses_dict_calendar():
    fake = MagicMock()
    fake.calendar = {"Earnings Date": ["2026-08-20"], "Ex-Dividend Date": "2026-07-01"}
    with patch("yfinance.Ticker", return_value=fake):
        result = valuation.next_catalysts("NVDA")
    assert result["next_earnings_date"] == "2026-08-20"
    assert result["next_ex_dividend_date"] == "2026-07-01"


def test_next_catalysts_never_raises_when_calendar_missing():
    fake = MagicMock()
    fake.calendar = None
    with patch("yfinance.Ticker", return_value=fake):
        result = valuation.next_catalysts("NVDA")
    assert result == {"next_earnings_date": None, "next_ex_dividend_date": None}


def test_next_catalysts_never_raises_on_yfinance_error():
    with patch("yfinance.Ticker", side_effect=RuntimeError("boom")):
        result = valuation.next_catalysts("NVDA")
    assert result == {"next_earnings_date": None, "next_ex_dividend_date": None}