equity-aggregator 0.1.1__py3-none-any.whl → 0.1.5__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 +49 -39
  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.5.dist-info}/METADATA +205 -115
  82. equity_aggregator-0.1.5.dist-info/RECORD +103 -0
  83. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.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.5.dist-info}/entry_points.txt +0 -0
  93. {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.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 Intrinio) and enriches it with supplementary data from Yahoo Finance and the Global LEI Foundation.
6
6
 
7
7
  ## Architecture & Design
8
8
 
@@ -15,6 +15,7 @@ src/equity_aggregator/
15
15
  ├── cli/ # Presentation Layer - User Interface
16
16
  ├── domain/ # Business Logic Layer - Core Domain
17
17
  │ ├── pipeline/ # Aggregation pipeline orchestration
18
+ │ ├── retrieval/ # Canonical equity download and retrieval
18
19
  │ └── _utils/ # Domain-specific utilities
19
20
  ├── adapters/ # Infrastructure Layer - External Integrations
20
21
  │ └── data_sources/ # Data source adapters
@@ -30,16 +31,16 @@ src/equity_aggregator/
30
31
  The system processes equity data through a six-stage async streaming pipeline:
31
32
 
32
33
  ```
33
- Raw Data Sources → Parse → Convert → Identify → Deduplicate → Enrich → Canonicalise → Storage
34
+ Raw Data Sources → Parse → Convert → Identify → Group → Enrich → Canonicalise → Storage
34
35
  ```
35
36
 
36
37
  ### Pipeline Stages
37
38
 
38
39
  #### 1. **Resolve**
39
40
 
40
- Orchestrates parallel data fetching from authoritative feeds:
41
+ Orchestrates parallel data fetching from discovery feeds:
41
42
 
42
- - Fetches data from Euronext, LSE, SEC, and XETRA concurrently
43
+ - Fetches data from LSEG, SEC, Stock Analysis, TradingView, XETRA and Intrinio concurrently
43
44
  - Combines all feed data into a single stream for processing
44
45
  - Maintains feed source metadata for downstream processing
45
46
 
@@ -47,7 +48,7 @@ Orchestrates parallel data fetching from authoritative feeds:
47
48
 
48
49
  Validates and structures raw feed data:
49
50
 
50
- - Applies feed-specific schemas (`EuronextFeedData`, `LseFeedData`, etc.)
51
+ - Applies feed-specific schemas (`LsegFeedData`, `SecFeedData`, etc.)
51
52
  - Filters out invalid records early in the pipeline
52
53
  - Normalises data formats across different sources
53
54
 
@@ -64,25 +65,25 @@ Standardises financial data to USD reference currency:
64
65
  Enriches records with global identification metadata:
65
66
 
66
67
  - Queries OpenFIGI API for FIGI identifiers
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
- - 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
74
+ - Groups records with identical Share Class FIGI values
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
85
- - Applies controlled concurrency to respect API limits
83
+ - Fetches representative identifiers from discovery data sources
84
+ - Queries enrichment feeds (Yahoo Finance, Global LEI Foundation) using these identifiers
85
+ - Performs single merge of all sources (discovery + enrichment) for optimal data quality
86
+ - Applies controlled concurrency to enrichment feeds to respect API limits
86
87
 
87
88
  #### 7. **Canonicalise**
88
89
 
@@ -100,7 +101,7 @@ The pipeline uses asynchronous operations to process thousands of equity records
100
101
 
101
102
  **Parallel Data Fetching**
102
103
 
103
- - All authoritative feeds (Euronext, LSE, SEC, XETRA) are fetched simultaneously
104
+ - All discovery feeds (LSEG, SEC, Stock Analysis, TradingView, XETRA, Intrinio) are fetched simultaneously
104
105
 
105
106
  **Streaming Pipeline**
106
107
 
@@ -108,7 +109,9 @@ The pipeline uses asynchronous operations to process thousands of equity records
108
109
 
109
110
  **Controlled Concurrency**
110
111
 
111
- - External API calls (OpenFIGI, Yahoo Finance) use semaphores to limit concurrent requests and respect rate limits
112
+ - External API calls (OpenFIGI, Yahoo Finance, GLEIF) use semaphores to limit concurrent requests and respect rate limits
113
+ - Each enrichment feed has a configurable concurrency limit
114
+ - Fetch operations include timeout protection to prevent indefinite blocking
112
115
 
113
116
  **Non-blocking Operations**
114
117
 
@@ -119,12 +122,12 @@ Illustration of Pipeline Flow:
119
122
 
120
123
  ```python
121
124
  async def aggregate_canonical_equities() -> list[CanonicalEquity]:
122
-
125
+
123
126
  # Resolve creates an async stream from multiple sources
124
127
  stream = resolve()
125
128
 
126
129
  # Each transform receives and returns an async iterator
127
- transforms = (parse, convert, identify, deduplicate, enrich, canonicalise)
130
+ transforms = (parse, convert, identify, group, enrich, canonicalise)
128
131
 
129
132
  # Pipe stream through each transform sequentially
130
133
  for stage in transforms:
@@ -143,17 +146,23 @@ schemas/
143
146
  ├── raw.py # RawEquity - intermediate pipeline format
144
147
  ├── canonical.py # CanonicalEquity - final standardised format
145
148
  ├── types.py # Type definitions and validators
149
+ ├── validators.py # Reusable validators for identifiers and financial data
146
150
  └── feeds/ # Feed-specific data models
147
- ├── euronext_feed_data.py
148
- ├── lse_feed_data.py
151
+ ├── lseg_feed_data.py
149
152
  ├── sec_feed_data.py
153
+ ├── stock_analysis_feed_data.py
154
+ ├── tradingview_feed_data.py
155
+ ├── gleif_feed_data.py
156
+ ├── feed_validators.py
150
157
  ├── xetra_feed_data.py
151
- └── yfinance_feed_data.py
158
+ ├── yfinance_feed_data.py
159
+ └── intrinio_feed_data.py
152
160
  ```
153
161
 
154
162
  ### Critical Role of Schemas
155
163
 
156
164
  #### 1. **Data Validation at Boundaries**
165
+
157
166
  Each feed has a dedicated Pydantic schema that:
158
167
  - Validates incoming data structure and types
159
168
  - Normalises field names and formats
@@ -171,11 +180,11 @@ def parse(stream: AsyncIterable[FeedRecord]) -> AsyncIterator[RawEquity]:
171
180
 
172
181
  #### 3. **Field Mapping & Normalisation**
173
182
  ```python
174
- class EuronextFeedData(BaseModel):
175
- name: str = Field(..., description="Company name")
176
- symbol: str = Field(..., description="Trading symbol")
183
+ class LsegFeedData(BaseModel):
184
+ issuername: str = Field(..., description="Company name")
185
+ tidm: str = Field(..., description="Trading symbol")
177
186
  isin: str = Field(..., description="ISIN identifier")
178
- mics: list[str] = Field(..., description="Market identifiers")
187
+ mics: list[str] | None = Field(..., description="Market identifiers")
179
188
 
180
189
  # Automatic field mapping from raw feed data
181
190
  @field_validator('symbol')
@@ -191,31 +200,32 @@ class EuronextFeedData(BaseModel):
191
200
  4. **Enriched RawEquity** → Final canonicalisation
192
201
  5. **CanonicalEquity** → Database persistence
193
202
 
194
- ## Authoritative vs Enrichment Feeds
203
+ ## Discovery vs Enrichment Feeds
195
204
 
196
- ### Authoritative Feeds (Primary Sources)
205
+ ### Discovery Feeds (Primary Sources)
197
206
 
198
- - **Euronext**: Pan-European Börse Stock Exchange
199
- - **LSE**: London Stock Exchange
200
- - **XETRA**: Deutsche Börse Stock Exchange
207
+ - **LSEG**: London Stock Exchange Group trading platform
201
208
  - **SEC**: US Securities and Exchange Commission
209
+ - **Stock Analysis**: US equities with comprehensive financial metrics
210
+ - **TradingView**: US equities with comprehensive financial metrics
211
+ - **XETRA**: Deutsche Börse Stock Exchange
212
+ - **Intrinio**: US financial data API providing company, securities, and real-time quote data
202
213
 
203
214
  **Characteristics**:
204
215
 
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
216
+ - Provide core equity identifier data (names, symbols, codes)
217
+ - Multiple discovery sources for the same equity are merged with enrichment data
208
218
 
209
219
  ### Enrichment Feeds (Supplementary Sources)
210
220
 
211
221
  - **Yahoo Finance**: Market data and financial metrics
222
+ - **Global LEI Foundation**: ISIN->LEI mapping for Legal Entity Identifier enrichment
212
223
 
213
224
  **Characteristics**
214
225
 
215
- - Only supplements missing data; never overwrites authoritative values
216
226
  - Provides additional financial metrics (market cap, analyst ratings, etc.)
217
- - Respects data hierarchy and source priority
218
- - Applied after authoritative data processing is complete
227
+ - Uses representative identifiers from discovery sources for look-up
228
+ - Applied after grouping but before final merge
219
229
 
220
230
  ## Equity Aggregator Components
221
231
 
@@ -235,7 +245,7 @@ class EuronextFeedData(BaseModel):
235
245
 
236
246
  ### Adapters Layer
237
247
 
238
- - **data_sources/authoritative_feeds/**: Primary data source integrations
248
+ - **data_sources/discovery_feeds/**: Primary data source integrations
239
249
  - **data_sources/enrichment_feeds/**: Supplementary data integrations
240
250
  - **data_sources/reference_lookup/**: External API services (OpenFIGI, exchange rates)
241
251
 
@@ -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
+ }