Finnhub profile2 reports marketCapitalization in millions; storing it as dollars made mega-caps like SPCX show as micro (e.g. 1.8M). Normalize on ingest, add unit tests, and include a one-shot SQL backfill script.
93 lines
3.2 KiB
Python
93 lines
3.2 KiB
Python
"""Unit tests for FinnhubFundamentalProvider unit conversions."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import AsyncMock, patch
|
|
|
|
import httpx
|
|
import pytest
|
|
|
|
from app.providers.fundamentals_chain import FinnhubFundamentalProvider
|
|
|
|
|
|
def _mock_response(status_code: int, json_data: object = None) -> httpx.Response:
|
|
return httpx.Response(
|
|
status_code=status_code,
|
|
json=json_data if json_data is not None else {},
|
|
request=httpx.Request("GET", "https://example.com"),
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def provider() -> FinnhubFundamentalProvider:
|
|
return FinnhubFundamentalProvider(api_key="test-key")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finnhub_market_cap_converted_from_millions_to_dollars(provider):
|
|
"""Finnhub marketCapitalization is in millions — store absolute USD.
|
|
|
|
SPCX-scale example: ~$1.8T → Finnhub reports 1_800_000 (millions).
|
|
Without conversion the UI showed 1.8M / micro cap.
|
|
"""
|
|
profile = {"marketCapitalization": 1_800_000} # millions → $1.8T
|
|
metrics = {"metric": {"peTTM": 40.0, "revenueGrowthTTMYoy": 25.0}}
|
|
earnings = [{"surprisePercent": 2.5}]
|
|
calendar = {"earningsCalendar": []}
|
|
|
|
async def mock_get(url, params=None):
|
|
if "profile2" in url:
|
|
return _mock_response(200, profile)
|
|
if "stock/metric" in url:
|
|
return _mock_response(200, metrics)
|
|
if "stock/earnings" in url:
|
|
return _mock_response(200, earnings)
|
|
if "calendar/earnings" in url:
|
|
return _mock_response(200, calendar)
|
|
return _mock_response(200, {})
|
|
|
|
with patch("app.providers.fundamentals_chain.httpx.AsyncClient") as MockClient:
|
|
instance = AsyncMock()
|
|
instance.get.side_effect = mock_get
|
|
instance.__aenter__ = AsyncMock(return_value=instance)
|
|
instance.__aexit__ = AsyncMock(return_value=False)
|
|
MockClient.return_value = instance
|
|
|
|
result = await provider.fetch_fundamentals("SPCX")
|
|
|
|
assert result.market_cap == 1_800_000 * 1_000_000 # $1.8T
|
|
assert result.pe_ratio == 40.0
|
|
assert result.revenue_growth == 25.0
|
|
assert result.earnings_surprise == 2.5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_finnhub_market_cap_none_when_missing(provider):
|
|
profile: dict = {}
|
|
metrics = {"metric": {}}
|
|
earnings: list = []
|
|
calendar = {"earningsCalendar": []}
|
|
|
|
async def mock_get(url, params=None):
|
|
if "profile2" in url:
|
|
return _mock_response(200, profile)
|
|
if "stock/metric" in url:
|
|
return _mock_response(200, metrics)
|
|
if "stock/earnings" in url:
|
|
return _mock_response(200, earnings)
|
|
if "calendar/earnings" in url:
|
|
return _mock_response(200, calendar)
|
|
return _mock_response(200, {})
|
|
|
|
with patch("app.providers.fundamentals_chain.httpx.AsyncClient") as MockClient:
|
|
instance = AsyncMock()
|
|
instance.get.side_effect = mock_get
|
|
instance.__aenter__ = AsyncMock(return_value=instance)
|
|
instance.__aexit__ = AsyncMock(return_value=False)
|
|
MockClient.return_value = instance
|
|
|
|
result = await provider.fetch_fundamentals("XYZ")
|
|
|
|
assert result.market_cap is None
|
|
assert "market_cap" in result.unavailable_fields
|