main
py 88 lines 2.89 KB
Raw
1 """Provider-neutral financial fact identity and precedence rules.
2
3 This is deliberately independent of document parsing. Every future financial
4 ingester must normalize into ``FinancialFact`` and call ``merge_fact`` at its
5 durable upsert boundary instead of making provider-specific replacement choices.
6 """
7 from __future__ import annotations
8
9 from dataclasses import dataclass
10 from enum import IntEnum
11 from uuid import UUID
12
13 from app.models import ProvenancedValue, SourceMode
14
15
16 class FactSourceTier(IntEnum):
17 SEARCH = 1
18 YAHOO = 2
19 # 3 is durable historical storage for official NSE facts. Never reuse it.
20 OFFICIAL_NSE = 3
21 STRUCTURED_FUNDAMENTALS = 4
22 OFFICIAL_REGULATORY = 5
23
24
25 # Provider-neutral list of tiers eligible for financial read projections.
26 SUPPORTED_FINANCIAL_SOURCE_TIERS = frozenset({
27 FactSourceTier.YAHOO,
28 FactSourceTier.STRUCTURED_FUNDAMENTALS,
29 FactSourceTier.OFFICIAL_NSE,
30 FactSourceTier.OFFICIAL_REGULATORY,
31 })
32
33
34 def fact_source_authority(tier: FactSourceTier) -> int:
35 """Return authority without changing durable source-tier identities."""
36 return {
37 FactSourceTier.SEARCH: 1,
38 FactSourceTier.YAHOO: 2,
39 FactSourceTier.STRUCTURED_FUNDAMENTALS: 3,
40 FactSourceTier.OFFICIAL_NSE: 4,
41 FactSourceTier.OFFICIAL_REGULATORY: 5,
42 }[tier]
43
44
45 @dataclass(frozen=True)
46 class FinancialFactKey:
47 instrument_id: UUID
48 metric: str
49 period_end: str | None
50 period_type: str
51 reporting_basis: str | None = None
52
53
54 @dataclass(frozen=True)
55 class FinancialFact:
56 key: FinancialFactKey
57 value: ProvenancedValue
58 source_tier: FactSourceTier
59 source_provider: str
60 source_identity: str
61 source_mode: SourceMode = SourceMode.REAL
62
63
64 def merge_fact(existing: FinancialFact | None, incoming: FinancialFact | None, *,
65 allow_same_tier_correction: bool = False) -> FinancialFact | None:
66 """Return the accepted fact without letting a fallback erase evidence.
67
68 A correction is intentionally opt-in and only valid for a same-tier official
69 fact with the same canonical fact identity. Retrieval time never controls
70 precedence.
71 """
72 if incoming is None or _missing(incoming.value.value):
73 return existing
74 if existing is None or _missing(existing.value.value):
75 return incoming
76 if existing.key != incoming.key:
77 raise ValueError("Financial facts with different canonical identities cannot be merged")
78 if fact_source_authority(incoming.source_tier) > fact_source_authority(existing.source_tier):
79 return incoming
80 if fact_source_authority(incoming.source_tier) < fact_source_authority(existing.source_tier):
81 return existing
82 if allow_same_tier_correction and incoming.source_tier == FactSourceTier.OFFICIAL_NSE:
83 return incoming
84 return existing
85
86
87 def _missing(value: object) -> bool:
88 return value is None or (isinstance(value, str) and not value.strip())