equity-aggregator 0.1.1__py3-none-any.whl → 0.1.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. equity_aggregator/README.md +40 -36
  2. equity_aggregator/adapters/__init__.py +13 -7
  3. equity_aggregator/adapters/data_sources/__init__.py +4 -6
  4. equity_aggregator/adapters/data_sources/_utils/_client.py +1 -1
  5. equity_aggregator/adapters/data_sources/{authoritative_feeds → _utils}/_record_types.py +1 -1
  6. equity_aggregator/adapters/data_sources/discovery_feeds/__init__.py +17 -0
  7. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/__init__.py +7 -0
  8. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/__init__.py +10 -0
  9. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/backoff.py +33 -0
  10. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/parser.py +107 -0
  11. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/intrinio.py +305 -0
  12. equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/session.py +197 -0
  13. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/__init__.py +7 -0
  14. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/__init__.py +9 -0
  15. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/backoff.py +33 -0
  16. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/parser.py +120 -0
  17. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/lseg.py +239 -0
  18. equity_aggregator/adapters/data_sources/discovery_feeds/lseg/session.py +162 -0
  19. equity_aggregator/adapters/data_sources/discovery_feeds/sec/__init__.py +7 -0
  20. equity_aggregator/adapters/data_sources/{authoritative_feeds → discovery_feeds/sec}/sec.py +4 -5
  21. equity_aggregator/adapters/data_sources/discovery_feeds/stock_analysis/__init__.py +7 -0
  22. equity_aggregator/adapters/data_sources/discovery_feeds/stock_analysis/stock_analysis.py +150 -0
  23. equity_aggregator/adapters/data_sources/discovery_feeds/tradingview/__init__.py +5 -0
  24. equity_aggregator/adapters/data_sources/discovery_feeds/tradingview/tradingview.py +275 -0
  25. equity_aggregator/adapters/data_sources/discovery_feeds/xetra/__init__.py +7 -0
  26. equity_aggregator/adapters/data_sources/{authoritative_feeds → discovery_feeds/xetra}/xetra.py +9 -12
  27. equity_aggregator/adapters/data_sources/enrichment_feeds/__init__.py +6 -1
  28. equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/__init__.py +5 -0
  29. equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/api.py +71 -0
  30. equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/download.py +109 -0
  31. equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/gleif.py +195 -0
  32. equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/parser.py +75 -0
  33. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/__init__.py +1 -1
  34. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/_utils/__init__.py +11 -0
  35. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/{utils → _utils}/backoff.py +1 -1
  36. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/{utils → _utils}/fuzzy.py +28 -26
  37. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/_utils/json.py +36 -0
  38. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/__init__.py +1 -1
  39. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/{summary.py → quote_summary.py} +44 -30
  40. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/search.py +10 -5
  41. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/auth.py +130 -0
  42. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/config.py +3 -3
  43. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/ranking.py +97 -0
  44. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/session.py +85 -218
  45. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/transport.py +191 -0
  46. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/yfinance.py +413 -0
  47. equity_aggregator/adapters/data_sources/reference_lookup/exchange_rate_api.py +6 -13
  48. equity_aggregator/adapters/data_sources/reference_lookup/openfigi.py +23 -7
  49. equity_aggregator/cli/dispatcher.py +11 -8
  50. equity_aggregator/cli/main.py +14 -5
  51. equity_aggregator/cli/parser.py +1 -1
  52. equity_aggregator/cli/signals.py +32 -0
  53. equity_aggregator/domain/_utils/__init__.py +2 -2
  54. equity_aggregator/domain/_utils/_load_converter.py +30 -21
  55. equity_aggregator/domain/_utils/_merge.py +221 -368
  56. equity_aggregator/domain/_utils/_merge_config.py +205 -0
  57. equity_aggregator/domain/_utils/_strategies.py +180 -0
  58. equity_aggregator/domain/pipeline/resolve.py +17 -11
  59. equity_aggregator/domain/pipeline/runner.py +4 -4
  60. equity_aggregator/domain/pipeline/seed.py +5 -1
  61. equity_aggregator/domain/pipeline/transforms/__init__.py +2 -2
  62. equity_aggregator/domain/pipeline/transforms/canonicalise.py +1 -1
  63. equity_aggregator/domain/pipeline/transforms/enrich.py +328 -285
  64. equity_aggregator/domain/pipeline/transforms/group.py +48 -0
  65. equity_aggregator/logging_config.py +4 -1
  66. equity_aggregator/schemas/__init__.py +11 -5
  67. equity_aggregator/schemas/canonical.py +11 -6
  68. equity_aggregator/schemas/feeds/__init__.py +11 -5
  69. equity_aggregator/schemas/feeds/gleif_feed_data.py +35 -0
  70. equity_aggregator/schemas/feeds/intrinio_feed_data.py +142 -0
  71. equity_aggregator/schemas/feeds/{lse_feed_data.py → lseg_feed_data.py} +85 -52
  72. equity_aggregator/schemas/feeds/sec_feed_data.py +36 -6
  73. equity_aggregator/schemas/feeds/stock_analysis_feed_data.py +107 -0
  74. equity_aggregator/schemas/feeds/tradingview_feed_data.py +144 -0
  75. equity_aggregator/schemas/feeds/xetra_feed_data.py +1 -1
  76. equity_aggregator/schemas/feeds/yfinance_feed_data.py +47 -35
  77. equity_aggregator/schemas/raw.py +5 -3
  78. equity_aggregator/schemas/types.py +7 -0
  79. equity_aggregator/schemas/validators.py +81 -27
  80. equity_aggregator/storage/data_store.py +5 -3
  81. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.4.dist-info}/METADATA +205 -115
  82. equity_aggregator-0.1.4.dist-info/RECORD +103 -0
  83. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.4.dist-info}/WHEEL +1 -1
  84. equity_aggregator/adapters/data_sources/authoritative_feeds/__init__.py +0 -13
  85. equity_aggregator/adapters/data_sources/authoritative_feeds/euronext.py +0 -420
  86. equity_aggregator/adapters/data_sources/authoritative_feeds/lse.py +0 -352
  87. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/feed.py +0 -350
  88. equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/utils/__init__.py +0 -9
  89. equity_aggregator/domain/pipeline/transforms/deduplicate.py +0 -54
  90. equity_aggregator/schemas/feeds/euronext_feed_data.py +0 -59
  91. equity_aggregator-0.1.1.dist-info/RECORD +0 -72
  92. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.4.dist-info}/entry_points.txt +0 -0
  93. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.4.dist-info}/licenses/LICENCE.txt +0 -0
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- The equity aggregator is a sophisticated financial data processing system that aggregates equity information from multiple authoritative sources (Euronext, LSE, SEC, XETRA) and enriches it with supplementary data from Yahoo Finance.
5
+ The equity aggregator is a sophisticated financial data processing system that aggregates equity information from multiple discovery sources (LSEG, SEC, Stock Analysis, TradingView, XETRA) and enriches it with supplementary data from Yahoo Finance and Intrinio.
6
6
 
7
7
  ## Architecture & Design
8
8
 
@@ -30,16 +30,16 @@ src/equity_aggregator/
30
30
  The system processes equity data through a six-stage async streaming pipeline:
31
31
 
32
32
  ```
33
- Raw Data Sources → Parse → Convert → Identify → Deduplicate → Enrich → Canonicalise → Storage
33
+ Raw Data Sources → Parse → Convert → Identify → Group → Enrich → Canonicalise → Storage
34
34
  ```
35
35
 
36
36
  ### Pipeline Stages
37
37
 
38
38
  #### 1. **Resolve**
39
39
 
40
- Orchestrates parallel data fetching from authoritative feeds:
40
+ Orchestrates parallel data fetching from discovery feeds:
41
41
 
42
- - Fetches data from Euronext, LSE, SEC, and XETRA concurrently
42
+ - Fetches data from LSEG, SEC, Stock Analysis, TradingView, and XETRA concurrently
43
43
  - Combines all feed data into a single stream for processing
44
44
  - Maintains feed source metadata for downstream processing
45
45
 
@@ -47,7 +47,7 @@ Orchestrates parallel data fetching from authoritative feeds:
47
47
 
48
48
  Validates and structures raw feed data:
49
49
 
50
- - Applies feed-specific schemas (`EuronextFeedData`, `LseFeedData`, etc.)
50
+ - Applies feed-specific schemas (`LsegFeedData`, `SecFeedData`, etc.)
51
51
  - Filters out invalid records early in the pipeline
52
52
  - Normalises data formats across different sources
53
53
 
@@ -67,21 +67,23 @@ Enriches records with global identification metadata:
67
67
  - Adds CUSIP, ISIN, and other standard identifiers
68
68
  - Creates globally unique equity identities
69
69
 
70
- #### 5. **Deduplicate**
70
+ #### 5. **Group**
71
71
 
72
- Merges duplicate equity records by FIGI identifier:
72
+ Groups equities by Share Class FIGI:
73
73
 
74
74
  - Groups records with identical share_class_figi values
75
- - Uses fuzzy matching to merge similar company names within groups
76
- - Preserves the most complete data when merging
75
+ - Preserves all discovery feed source data for later merging
76
+ - Each group represents the same equity from multiple discovery sources
77
+ - Yields groups as `list[RawEquity]` for enrichment processing
77
78
 
78
79
  #### 6. **Enrich**
79
80
 
80
- Adds supplementary data from enrichment feeds:
81
+ Fetches enrichment data and performs comprehensive single merge:
81
82
 
82
- - Fetches additional data from Yahoo Finance
83
- - Only fills missing fields from authoritative sources
84
- - Preserves data integrity and source hierarchy
83
+ - Extracts representative identifiers from discovery sources using merge algorithms
84
+ - Queries enrichment feeds (Yahoo Finance, Intrinio) with these identifiers
85
+ - Performs single merge of all sources (discovery + enrichment) for optimal data quality
86
+ - Uses same merge logic for identifiers and final merge (mode for IDs, fuzzy clustering for names, frequency for symbols)
85
87
  - Applies controlled concurrency to respect API limits
86
88
 
87
89
  #### 7. **Canonicalise**
@@ -100,7 +102,7 @@ The pipeline uses asynchronous operations to process thousands of equity records
100
102
 
101
103
  **Parallel Data Fetching**
102
104
 
103
- - All authoritative feeds (Euronext, LSE, SEC, XETRA) are fetched simultaneously
105
+ - All discovery feeds (LSEG, SEC, Stock Analysis, TradingView, XETRA) are fetched simultaneously
104
106
 
105
107
  **Streaming Pipeline**
106
108
 
@@ -108,7 +110,7 @@ The pipeline uses asynchronous operations to process thousands of equity records
108
110
 
109
111
  **Controlled Concurrency**
110
112
 
111
- - External API calls (OpenFIGI, Yahoo Finance) use semaphores to limit concurrent requests and respect rate limits
113
+ - External API calls (OpenFIGI, Yahoo Finance, Intrinio) use semaphores to limit concurrent requests and respect rate limits
112
114
 
113
115
  **Non-blocking Operations**
114
116
 
@@ -119,12 +121,12 @@ Illustration of Pipeline Flow:
119
121
 
120
122
  ```python
121
123
  async def aggregate_canonical_equities() -> list[CanonicalEquity]:
122
-
124
+
123
125
  # Resolve creates an async stream from multiple sources
124
126
  stream = resolve()
125
127
 
126
128
  # Each transform receives and returns an async iterator
127
- transforms = (parse, convert, identify, deduplicate, enrich, canonicalise)
129
+ transforms = (parse, convert, identify, group, enrich, canonicalise)
128
130
 
129
131
  # Pipe stream through each transform sequentially
130
132
  for stage in transforms:
@@ -144,11 +146,13 @@ schemas/
144
146
  ├── canonical.py # CanonicalEquity - final standardised format
145
147
  ├── types.py # Type definitions and validators
146
148
  └── feeds/ # Feed-specific data models
147
- ├── euronext_feed_data.py
148
- ├── lse_feed_data.py
149
+ ├── lseg_feed_data.py
149
150
  ├── sec_feed_data.py
151
+ ├── stock_analysis_feed_data.py
152
+ ├── tradingview_feed_data.py
150
153
  ├── xetra_feed_data.py
151
- └── yfinance_feed_data.py
154
+ ├── yfinance_feed_data.py
155
+ └── intrinio_feed_data.py
152
156
  ```
153
157
 
154
158
  ### Critical Role of Schemas
@@ -171,11 +175,11 @@ def parse(stream: AsyncIterable[FeedRecord]) -> AsyncIterator[RawEquity]:
171
175
 
172
176
  #### 3. **Field Mapping & Normalisation**
173
177
  ```python
174
- class EuronextFeedData(BaseModel):
175
- name: str = Field(..., description="Company name")
176
- symbol: str = Field(..., description="Trading symbol")
178
+ class LsegFeedData(BaseModel):
179
+ issuername: str = Field(..., description="Company name")
180
+ tidm: str = Field(..., description="Trading symbol")
177
181
  isin: str = Field(..., description="ISIN identifier")
178
- mics: list[str] = Field(..., description="Market identifiers")
182
+ mics: list[str] | None = Field(..., description="Market identifiers")
179
183
 
180
184
  # Automatic field mapping from raw feed data
181
185
  @field_validator('symbol')
@@ -191,31 +195,31 @@ class EuronextFeedData(BaseModel):
191
195
  4. **Enriched RawEquity** → Final canonicalisation
192
196
  5. **CanonicalEquity** → Database persistence
193
197
 
194
- ## Authoritative vs Enrichment Feeds
198
+ ## Discovery vs Enrichment Feeds
195
199
 
196
- ### Authoritative Feeds (Primary Sources)
200
+ ### Discovery Feeds (Primary Sources)
197
201
 
198
- - **Euronext**: Pan-European Börse Stock Exchange
199
- - **LSE**: London Stock Exchange
200
- - **XETRA**: Deutsche Börse Stock Exchange
202
+ - **LSEG**: London Stock Exchange Group trading platform
201
203
  - **SEC**: US Securities and Exchange Commission
204
+ - **Stock Analysis**: US equities with comprehensive financial metrics
205
+ - **TradingView**: US equities with comprehensive financial metrics
206
+ - **XETRA**: Deutsche Börse Stock Exchange
202
207
 
203
208
  **Characteristics**:
204
209
 
205
- - Provide core equity data (names, symbols, identifiers)
206
- - Considered source of truth for fundamental information
207
- - Data from these feeds is **never** overwritten by enrichment
210
+ - Provide core equity identifier data (names, symbols, codes)
211
+ - Multiple discovery sources for the same equity are merged with enrichment data
208
212
 
209
213
  ### Enrichment Feeds (Supplementary Sources)
210
214
 
211
215
  - **Yahoo Finance**: Market data and financial metrics
216
+ - **Intrinio**: Market data and financial metrics
212
217
 
213
218
  **Characteristics**
214
219
 
215
- - Only supplements missing data; never overwrites authoritative values
216
220
  - Provides additional financial metrics (market cap, analyst ratings, etc.)
217
- - Respects data hierarchy and source priority
218
- - Applied after authoritative data processing is complete
221
+ - Uses representative identifiers from discovery sources for lookup
222
+ - Applied after grouping but before final merge
219
223
 
220
224
  ## Equity Aggregator Components
221
225
 
@@ -235,7 +239,7 @@ class EuronextFeedData(BaseModel):
235
239
 
236
240
  ### Adapters Layer
237
241
 
238
- - **data_sources/authoritative_feeds/**: Primary data source integrations
242
+ - **data_sources/discovery_feeds/**: Primary data source integrations
239
243
  - **data_sources/enrichment_feeds/**: Supplementary data integrations
240
244
  - **data_sources/reference_lookup/**: External API services (OpenFIGI, exchange rates)
241
245
 
@@ -1,12 +1,15 @@
1
1
  # adapters/__init__.py
2
2
 
3
- from .data_sources.authoritative_feeds import (
4
- fetch_equity_records_euronext,
5
- fetch_equity_records_lse,
3
+ from .data_sources.discovery_feeds import (
4
+ fetch_equity_records_intrinio,
5
+ fetch_equity_records_lseg,
6
6
  fetch_equity_records_sec,
7
+ fetch_equity_records_stock_analysis,
8
+ fetch_equity_records_tradingview,
7
9
  fetch_equity_records_xetra,
8
10
  )
9
11
  from .data_sources.enrichment_feeds import (
12
+ open_gleif_feed,
10
13
  open_yfinance_feed,
11
14
  )
12
15
  from .data_sources.reference_lookup import (
@@ -15,12 +18,15 @@ from .data_sources.reference_lookup import (
15
18
  )
16
19
 
17
20
  __all__ = [
18
- # authoritative feeds
19
- "fetch_equity_records_euronext",
20
- "fetch_equity_records_lse",
21
- "fetch_equity_records_xetra",
21
+ # discovery feeds
22
+ "fetch_equity_records_intrinio",
23
+ "fetch_equity_records_lseg",
22
24
  "fetch_equity_records_sec",
25
+ "fetch_equity_records_stock_analysis",
26
+ "fetch_equity_records_tradingview",
27
+ "fetch_equity_records_xetra",
23
28
  # enrichment feeds
29
+ "open_gleif_feed",
24
30
  "open_yfinance_feed",
25
31
  # reference lookup
26
32
  "fetch_equity_identification",
@@ -1,8 +1,7 @@
1
1
  # data_sources/__init__.py
2
2
 
3
- from .authoritative_feeds import (
4
- fetch_equity_records_euronext,
5
- fetch_equity_records_lse,
3
+ from .discovery_feeds import (
4
+ fetch_equity_records_lseg,
6
5
  fetch_equity_records_sec,
7
6
  fetch_equity_records_xetra,
8
7
  )
@@ -15,9 +14,8 @@ from .reference_lookup import (
15
14
  )
16
15
 
17
16
  __all__ = [
18
- # authoritative feeds
19
- "fetch_equity_records_euronext",
20
- "fetch_equity_records_lse",
17
+ # discovery feeds
18
+ "fetch_equity_records_lseg",
21
19
  "fetch_equity_records_sec",
22
20
  "fetch_equity_records_xetra",
23
21
  # enrichment feeds
@@ -33,7 +33,7 @@ def make_client(**overrides: object) -> AsyncClient:
33
33
  # Set default timeouts for connections, reading, and writing
34
34
  timeout = Timeout(
35
35
  connect=3.0, # 3s to establish TLS
36
- read=None, # no read timeout
36
+ read=300.0, # up to 5 minutes to read a response
37
37
  write=5.0, # up to 5s to send a body
38
38
  pool=None, # no pool timeout
39
39
  )
@@ -1,4 +1,4 @@
1
- # authoritative_feeds/_record_types.py
1
+ # _utils/_record_types.py
2
2
 
3
3
  from collections.abc import AsyncIterator, Callable
4
4
 
@@ -0,0 +1,17 @@
1
+ # discovery_feeds/__init__.py
2
+
3
+ from .intrinio import fetch_equity_records as fetch_equity_records_intrinio
4
+ from .lseg import fetch_equity_records as fetch_equity_records_lseg
5
+ from .sec import fetch_equity_records as fetch_equity_records_sec
6
+ from .stock_analysis import fetch_equity_records as fetch_equity_records_stock_analysis
7
+ from .tradingview import fetch_equity_records as fetch_equity_records_tradingview
8
+ from .xetra import fetch_equity_records as fetch_equity_records_xetra
9
+
10
+ __all__ = [
11
+ "fetch_equity_records_intrinio",
12
+ "fetch_equity_records_lseg",
13
+ "fetch_equity_records_sec",
14
+ "fetch_equity_records_stock_analysis",
15
+ "fetch_equity_records_tradingview",
16
+ "fetch_equity_records_xetra",
17
+ ]
@@ -0,0 +1,7 @@
1
+ # intrinio/__init__.py
2
+
3
+ from .intrinio import fetch_equity_records
4
+
5
+ __all__ = [
6
+ "fetch_equity_records",
7
+ ]
@@ -0,0 +1,10 @@
1
+ # _utils/__init__.py
2
+
3
+ from .backoff import backoff_delays
4
+ from .parser import parse_companies_response, parse_securities_response
5
+
6
+ __all__ = [
7
+ "backoff_delays",
8
+ "parse_companies_response",
9
+ "parse_securities_response",
10
+ ]
@@ -0,0 +1,33 @@
1
+ # intrinio/_utils/backoff.py
2
+
3
+
4
+ import random
5
+ from collections.abc import Iterator
6
+
7
+
8
+ def backoff_delays(
9
+ *,
10
+ base: float = 5.0,
11
+ cap: float = 128.0,
12
+ jitter: float = 0.10,
13
+ attempts: int = 5,
14
+ ) -> Iterator[float]:
15
+ """
16
+ Yield an exponential backoff sequence with bounded jitter for retry delays.
17
+
18
+ Each delay is calculated as: delay * (1 ± jitter), doubling each time up to cap.
19
+
20
+ Args:
21
+ base (float): Initial delay in seconds.
22
+ cap (float): Maximum delay in seconds.
23
+ jitter (float): Fractional jitter (+/-) applied to each delay.
24
+ attempts (int): Number of delay values to yield.
25
+
26
+ Returns:
27
+ Iterator[float]: Sequence of delay values in seconds.
28
+ """
29
+ delay: float = base
30
+ for _ in range(attempts):
31
+ delta: float = delay * jitter * (2 * random.random() - 1)
32
+ yield max(0.0, min(delay + delta, cap))
33
+ delay = min(delay * 2, cap)
@@ -0,0 +1,107 @@
1
+ # _utils/parser.py
2
+
3
+ from equity_aggregator.adapters.data_sources._utils._record_types import (
4
+ EquityRecord,
5
+ )
6
+
7
+
8
+ def parse_companies_response(payload: dict) -> tuple[list[EquityRecord], str | None]:
9
+ """
10
+ Parse Intrinio companies API response.
11
+
12
+ Args:
13
+ payload (dict): The JSON response from Intrinio API.
14
+
15
+ Returns:
16
+ tuple[list[EquityRecord], str | None]: Tuple of (parsed records,
17
+ next_page token).
18
+ """
19
+ companies = payload.get("companies", [])
20
+ next_page = payload.get("next_page")
21
+ valid_companies = [c for c in companies if _is_valid_company(c)]
22
+ records = [_extract_company_record(company) for company in valid_companies]
23
+ return records, next_page
24
+
25
+
26
+ def parse_securities_response(payload: dict) -> list[EquityRecord]:
27
+ """
28
+ Parse Intrinio securities API response.
29
+
30
+ Uses the company data embedded in the response (which is authoritative
31
+ for these securities) rather than external company data, to avoid
32
+ ticker reassignment issues where stale company records have incorrect
33
+ identifiers.
34
+
35
+ Args:
36
+ payload (dict): The JSON response from Intrinio API.
37
+
38
+ Returns:
39
+ list[EquityRecord]: List of security records with company data merged.
40
+ """
41
+ securities = payload.get("securities", [])
42
+ company_data = payload.get("company", {})
43
+ company = _extract_company_record(company_data) if company_data else {}
44
+
45
+ return [
46
+ _extract_security_record(security, company)
47
+ for security in securities
48
+ if security.get("share_class_figi")
49
+ ]
50
+
51
+
52
+ def _is_valid_company(company: dict | None) -> bool:
53
+ """
54
+ Check if a company record has required fields.
55
+
56
+ Args:
57
+ company (dict | None): Raw company data from API.
58
+
59
+ Returns:
60
+ bool: True if company has ticker and name, False otherwise.
61
+ """
62
+ return bool(company and company.get("ticker") and company.get("name"))
63
+
64
+
65
+ def _extract_company_record(company: dict) -> EquityRecord:
66
+ """
67
+ Extract a company record from raw API data.
68
+
69
+ Args:
70
+ company (dict): Raw company data from Intrinio API.
71
+
72
+ Returns:
73
+ EquityRecord: Normalised company record.
74
+ """
75
+ return {
76
+ "company_id": company.get("id"),
77
+ "company_ticker": company.get("ticker"),
78
+ "name": company.get("name"),
79
+ "lei": company.get("lei"),
80
+ "cik": company.get("cik"),
81
+ }
82
+
83
+
84
+ def _extract_security_record(security: dict, company: EquityRecord) -> EquityRecord:
85
+ """
86
+ Extract a security record merged with company data.
87
+
88
+ Args:
89
+ security (dict): Raw security data from Intrinio API.
90
+ company (EquityRecord): Company record to merge with security data.
91
+
92
+ Returns:
93
+ EquityRecord: Security record with company data merged.
94
+ """
95
+ return {
96
+ # Company data
97
+ "name": company.get("name"),
98
+ "lei": company.get("lei"),
99
+ "cik": company.get("cik"),
100
+ # Security data
101
+ "ticker": security.get("ticker"),
102
+ "share_class_figi": security.get("share_class_figi"),
103
+ "figi": security.get("figi"),
104
+ "composite_figi": security.get("composite_figi"),
105
+ "currency": security.get("currency"),
106
+ "exchange_mic": security.get("exchange_mic"),
107
+ }