main
py 76 lines 5.87 KB
Raw
1 from datetime import datetime, timezone
2 from uuid import uuid4
3
4 import httpx
5 import pytest
6
7 from app.fact_precedence import FactSourceTier, FinancialFact, FinancialFactKey, merge_fact
8 from app.international_fundamentals import EodhdFundamentalProvider, SecEdgarFundamentalProvider, international_provider_for
9 from app.models import CompanyResearchProfile, ProvenancedValue, SourceMode
10 from app.settings import Settings
11 from app.structured_research import financial_result_history_from_facts, financial_statement_history_from_facts
12
13
14 def _profile(**changes):
15 values = dict(instrument_id=uuid4(), company_id=uuid4(), company_name="Microsoft Corporation", ticker="MSFT", exchange="NASDAQ", mic="XNAS", country="US", currency="USD")
16 values.update(changes)
17 return CompanyResearchProfile(**values)
18
19
20 def _client(responses):
21 def handler(request):
22 for suffix, payload in responses.items():
23 if request.url.path.endswith(suffix): return httpx.Response(200, json=payload)
24 return httpx.Response(404)
25 return httpx.AsyncClient(transport=httpx.MockTransport(handler))
26
27
28 @pytest.mark.asyncio
29 async def test_sec_resolves_cik_and_normalizes_explicit_companyfacts():
30 client = _client({"company_tickers.json": {"0": {"ticker": "MSFT", "cik_str": 789019}}, "CIK0000789019.json": {"facts": {"us-gaap": {
31 "RevenueFromContractWithCustomerExcludingAssessedTax": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 100, "frame": "CY2026Q1", "accn": "a", "filed": "2026-04-25"}]}},
32 "NetIncomeLoss": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 25, "frame": "CY2026Q1", "accn": "a"}]}},
33 "EarningsPerShareDiluted": {"units": {"USD/shares": [{"form": "10-Q", "end": "2026-03-31", "val": 3, "frame": "CY2026Q1", "accn": "a"}]}},
34 "Assets": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 500, "accn": "a"}]}},
35 "NetCashProvidedByUsedInOperatingActivities": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 40, "frame": "CY2026Q1", "accn": "a"}]}},
36 "PaymentsToAcquirePropertyPlantAndEquipment": {"units": {"USD": [{"form": "10-Q", "end": "2026-03-31", "val": 8, "frame": "CY2026Q1", "accn": "a"}]}},
37 }}}})
38 result = await SecEdgarFundamentalProvider(Settings(), client=client).collect(_profile())
39 assert result.verified_provider_ids == {"SEC_CIK": "0000789019"}
40 assert {(f.key.metric, f.key.period_type) for f in result.facts} >= {("revenue", "QUARTERLY"), ("pat", "QUARTERLY"), ("eps", "QUARTERLY"), ("total_assets", "AS_AT"), ("operating_cash_flow", "QUARTERLY"), ("capex", "QUARTERLY")}
41 assert all(f.source_tier == FactSourceTier.OFFICIAL_REGULATORY and f.source_mode == SourceMode.REAL for f in result.facts)
42 await client.aclose()
43
44
45 @pytest.mark.asyncio
46 async def test_eodhd_validates_isin_exchange_currency_and_reuses_symbol_mapping():
47 profile = _profile(company_name="AIXTRON SE", ticker="AIXA", exchange="XETRA", mic="XFRA", country="DE", currency="EUR", isin="DE000A0WMPJ6")
48 payload = {"General": {"ISIN": "DE000A0WMPJ6", "Exchange": "F", "CurrencyCode": "EUR"}, "Financials": {"Income_Statement": {"yearly": {"2025": {"date": "2025-12-31", "Revenue": "100", "NetIncome": "20"}}, "quarterly": {"2026Q1": {"date": "2026-03-31", "Revenue": "30"}}}, "Balance_Sheet": {"yearly": {"2025": {"date": "2025-12-31", "TotalAssets": "200"}}}, "Cash_Flow": {"yearly": {"2025": {"date": "2025-12-31", "CapitalExpenditures": "5"}}}}}
49 settings = Settings(eodhd_api_key="test", eodhd_base_url="https://eod.test")
50 result = await EodhdFundamentalProvider(settings, client=_client({"fundamentals/AIXA": payload})).collect(profile)
51 assert result.verified_provider_ids == {"EODHD": "AIXA"}
52 assert {f.key.metric for f in result.facts} >= {"revenue", "pat", "total_assets", "capex"}
53 assert any(f.key.metric == "revenue" and f.key.period_type == "QUARTERLY" for f in result.facts)
54 bad = {**payload, "General": {**payload["General"], "CurrencyCode": "USD"}}
55 assert not (await EodhdFundamentalProvider(settings, client=_client({"fundamentals/AIXA": bad})).collect(profile)).facts
56
57
58 def test_international_precedence_and_read_projection_keep_basis_and_series_limits():
59 profile = _profile(); now = datetime.now(timezone.utc)
60 def fact(metric, end, kind, tier, value, basis="CONSOLIDATED"):
61 return FinancialFact(FinancialFactKey(profile.instrument_id, metric, end, kind, basis), ProvenancedValue(value=value, source_url="https://source", source_name="source", source_type="TEST", retrieved_at=now), tier, "test", end, SourceMode.REAL)
62 sec = fact("revenue", "2026-03-31", "QUARTERLY", FactSourceTier.OFFICIAL_REGULATORY, 100)
63 assert merge_fact(sec, fact("revenue", "2026-03-31", "QUARTERLY", FactSourceTier.STRUCTURED_FUNDAMENTALS, 1)) is sec
64 facts = [fact("revenue", f"202{year}-12-31", "ANNUAL", FactSourceTier.OFFICIAL_REGULATORY, year) for year in range(1, 7)]
65 facts += [fact("revenue", f"2026-0{month}-28", "QUARTERLY", FactSourceTier.OFFICIAL_REGULATORY, month) for month in range(1, 6)]
66 history = financial_result_history_from_facts(facts)
67 assert len([x for x in history if x.period_type == "ANNUAL"]) == 4
68 assert len([x for x in history if x.period_type == "QUARTERLY"]) == 4
69 mixed = facts + [fact("total_assets", "2026-03-31", "AS_AT", FactSourceTier.OFFICIAL_REGULATORY, 10, "UNKNOWN")]
70 assert financial_statement_history_from_facts(mixed, period_type="AS_AT", metrics={"total_assets"})[0].reporting_basis == "UNKNOWN"
71
72
73 def test_provider_routing_keeps_india_outside_international_pipeline():
74 assert international_provider_for(_profile(country="IN", exchange="NSE", mic="XNSE"), Settings()) is None
75 assert isinstance(international_provider_for(_profile(), Settings()), SecEdgarFundamentalProvider)
76 assert isinstance(international_provider_for(_profile(country="DE", exchange="XETRA", mic="XFRA", currency="EUR"), Settings(eodhd_api_key="x")), EodhdFundamentalProvider)