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.
- equity_aggregator/README.md +49 -39
- equity_aggregator/adapters/__init__.py +13 -7
- equity_aggregator/adapters/data_sources/__init__.py +4 -6
- equity_aggregator/adapters/data_sources/_utils/_client.py +1 -1
- equity_aggregator/adapters/data_sources/{authoritative_feeds → _utils}/_record_types.py +1 -1
- equity_aggregator/adapters/data_sources/discovery_feeds/__init__.py +17 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/__init__.py +7 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/__init__.py +10 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/backoff.py +33 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/_utils/parser.py +107 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/intrinio.py +305 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/intrinio/session.py +197 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/__init__.py +7 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/__init__.py +9 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/backoff.py +33 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/_utils/parser.py +120 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/lseg.py +239 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/lseg/session.py +162 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/sec/__init__.py +7 -0
- equity_aggregator/adapters/data_sources/{authoritative_feeds → discovery_feeds/sec}/sec.py +4 -5
- equity_aggregator/adapters/data_sources/discovery_feeds/stock_analysis/__init__.py +7 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/stock_analysis/stock_analysis.py +150 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/tradingview/__init__.py +5 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/tradingview/tradingview.py +275 -0
- equity_aggregator/adapters/data_sources/discovery_feeds/xetra/__init__.py +7 -0
- equity_aggregator/adapters/data_sources/{authoritative_feeds → discovery_feeds/xetra}/xetra.py +9 -12
- equity_aggregator/adapters/data_sources/enrichment_feeds/__init__.py +6 -1
- equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/__init__.py +5 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/api.py +71 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/download.py +109 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/gleif.py +195 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/gleif/parser.py +75 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/__init__.py +1 -1
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/_utils/__init__.py +11 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/{utils → _utils}/backoff.py +1 -1
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/{utils → _utils}/fuzzy.py +28 -26
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/_utils/json.py +36 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/__init__.py +1 -1
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/{summary.py → quote_summary.py} +44 -30
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/api/search.py +10 -5
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/auth.py +130 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/config.py +3 -3
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/ranking.py +97 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/session.py +85 -218
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/transport.py +191 -0
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/yfinance.py +413 -0
- equity_aggregator/adapters/data_sources/reference_lookup/exchange_rate_api.py +6 -13
- equity_aggregator/adapters/data_sources/reference_lookup/openfigi.py +23 -7
- equity_aggregator/cli/dispatcher.py +11 -8
- equity_aggregator/cli/main.py +14 -5
- equity_aggregator/cli/parser.py +1 -1
- equity_aggregator/cli/signals.py +32 -0
- equity_aggregator/domain/_utils/__init__.py +2 -2
- equity_aggregator/domain/_utils/_load_converter.py +30 -21
- equity_aggregator/domain/_utils/_merge.py +221 -368
- equity_aggregator/domain/_utils/_merge_config.py +205 -0
- equity_aggregator/domain/_utils/_strategies.py +180 -0
- equity_aggregator/domain/pipeline/resolve.py +17 -11
- equity_aggregator/domain/pipeline/runner.py +4 -4
- equity_aggregator/domain/pipeline/seed.py +5 -1
- equity_aggregator/domain/pipeline/transforms/__init__.py +2 -2
- equity_aggregator/domain/pipeline/transforms/canonicalise.py +1 -1
- equity_aggregator/domain/pipeline/transforms/enrich.py +328 -285
- equity_aggregator/domain/pipeline/transforms/group.py +48 -0
- equity_aggregator/logging_config.py +4 -1
- equity_aggregator/schemas/__init__.py +11 -5
- equity_aggregator/schemas/canonical.py +11 -6
- equity_aggregator/schemas/feeds/__init__.py +11 -5
- equity_aggregator/schemas/feeds/gleif_feed_data.py +35 -0
- equity_aggregator/schemas/feeds/intrinio_feed_data.py +142 -0
- equity_aggregator/schemas/feeds/{lse_feed_data.py → lseg_feed_data.py} +85 -52
- equity_aggregator/schemas/feeds/sec_feed_data.py +36 -6
- equity_aggregator/schemas/feeds/stock_analysis_feed_data.py +107 -0
- equity_aggregator/schemas/feeds/tradingview_feed_data.py +144 -0
- equity_aggregator/schemas/feeds/xetra_feed_data.py +1 -1
- equity_aggregator/schemas/feeds/yfinance_feed_data.py +47 -35
- equity_aggregator/schemas/raw.py +5 -3
- equity_aggregator/schemas/types.py +7 -0
- equity_aggregator/schemas/validators.py +81 -27
- equity_aggregator/storage/data_store.py +5 -3
- {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.dist-info}/METADATA +205 -115
- equity_aggregator-0.1.5.dist-info/RECORD +103 -0
- {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.dist-info}/WHEEL +1 -1
- equity_aggregator/adapters/data_sources/authoritative_feeds/__init__.py +0 -13
- equity_aggregator/adapters/data_sources/authoritative_feeds/euronext.py +0 -420
- equity_aggregator/adapters/data_sources/authoritative_feeds/lse.py +0 -352
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/feed.py +0 -350
- equity_aggregator/adapters/data_sources/enrichment_feeds/yfinance/utils/__init__.py +0 -9
- equity_aggregator/domain/pipeline/transforms/deduplicate.py +0 -54
- equity_aggregator/schemas/feeds/euronext_feed_data.py +0 -59
- equity_aggregator-0.1.1.dist-info/RECORD +0 -72
- {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.dist-info}/entry_points.txt +0 -0
- {equity_aggregator-0.1.1.dist-info → equity_aggregator-0.1.5.dist-info}/licenses/LICENCE.txt +0 -0
|
@@ -82,7 +82,7 @@ async def fetch_equity_identification(
|
|
|
82
82
|
|
|
83
83
|
cached = load_cache(cache_key)
|
|
84
84
|
if cached is not None:
|
|
85
|
-
logger.
|
|
85
|
+
logger.info("Loaded %d OpenFIGI records from cache.", len(cached))
|
|
86
86
|
return cached
|
|
87
87
|
|
|
88
88
|
# resolve identities using the provided client or a default one
|
|
@@ -92,7 +92,7 @@ async def fetch_equity_identification(
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
save_cache(cache_key, identities)
|
|
95
|
-
logger.
|
|
95
|
+
logger.info("Saved %d OpenFIGI identification records to cache.", len(identities))
|
|
96
96
|
return identities
|
|
97
97
|
|
|
98
98
|
|
|
@@ -412,16 +412,32 @@ def _to_query_record(equity: RawEquity) -> dict[str, str]:
|
|
|
412
412
|
"""
|
|
413
413
|
Converts a RawEquity object into a dictionary for querying OpenFIGI.
|
|
414
414
|
|
|
415
|
+
Specifically requests "Common Stock" securities to avoid duplicates from
|
|
416
|
+
depositary receipts (DRs), American depositary receipts (ADRs), and other
|
|
417
|
+
equity-like instruments that represent the same underlying company.
|
|
418
|
+
|
|
415
419
|
Args:
|
|
416
420
|
equity (RawEquity): The equity containing ISIN, CUSIP, or symbol.
|
|
417
421
|
|
|
418
422
|
Returns:
|
|
419
|
-
dict[str, str]: A dict with idType, idValue, and
|
|
423
|
+
dict[str, str]: A dict with idType, idValue, and securityType.
|
|
420
424
|
"""
|
|
421
425
|
if equity.isin:
|
|
422
|
-
return {
|
|
426
|
+
return {
|
|
427
|
+
"idType": "ID_ISIN",
|
|
428
|
+
"idValue": equity.isin,
|
|
429
|
+
"securityType": "Common Stock",
|
|
430
|
+
}
|
|
423
431
|
|
|
424
432
|
if equity.cusip:
|
|
425
|
-
return {
|
|
426
|
-
|
|
427
|
-
|
|
433
|
+
return {
|
|
434
|
+
"idType": "ID_CUSIP",
|
|
435
|
+
"idValue": equity.cusip,
|
|
436
|
+
"securityType": "Common Stock",
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
"idType": "TICKER",
|
|
441
|
+
"idValue": equity.symbol,
|
|
442
|
+
"securityType": "Common Stock",
|
|
443
|
+
}
|
|
@@ -25,23 +25,26 @@ def run_command(fn: callable) -> None:
|
|
|
25
25
|
raise SystemExit(1) from None
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
def dispatch_command(args: Namespace) -> None:
|
|
28
|
+
def dispatch_command(args: Namespace, handlers: dict | None = None) -> None:
|
|
29
29
|
"""
|
|
30
30
|
Dispatch execution to the appropriate command handler.
|
|
31
31
|
|
|
32
32
|
Args:
|
|
33
33
|
args: Parsed command line arguments from argparse.
|
|
34
|
+
handlers: Optional dictionary mapping command names to handler functions.
|
|
35
|
+
If not provided, uses the default production handlers.
|
|
34
36
|
|
|
35
37
|
Raises:
|
|
36
38
|
ValueError: If the command is not recognised.
|
|
37
39
|
"""
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
if handlers is None:
|
|
41
|
+
handlers = {
|
|
42
|
+
"seed": lambda: run_command(seed),
|
|
43
|
+
"export": lambda: run_command(lambda: export(args.output_dir, download)),
|
|
44
|
+
"download": lambda: run_command(download),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
handler = handlers.get(args.cmd)
|
|
45
48
|
if handler:
|
|
46
49
|
handler()
|
|
47
50
|
else:
|
equity_aggregator/cli/main.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
# cli/main.py
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import signal
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from typing import Any
|
|
5
6
|
|
|
6
7
|
from equity_aggregator.logging_config import configure_logging
|
|
7
8
|
|
|
8
9
|
from .config import determine_log_level
|
|
9
10
|
from .dispatcher import dispatch_command
|
|
10
11
|
from .parser import create_parser
|
|
12
|
+
from .signals import handle_sigint
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
def main() -> None:
|
|
15
|
+
def main(dispatcher: Callable[[Any], None] | None = None) -> None:
|
|
14
16
|
"""
|
|
15
17
|
Entry point for the equity-aggregator CLI application.
|
|
16
18
|
|
|
@@ -26,11 +28,18 @@ def main() -> None:
|
|
|
26
28
|
4. Configures the application logging system
|
|
27
29
|
5. Dispatches to the selected command handler
|
|
28
30
|
|
|
31
|
+
Args:
|
|
32
|
+
dispatcher: Optional callable to dispatch commands. If not provided,
|
|
33
|
+
uses the default dispatch_command function.
|
|
34
|
+
|
|
29
35
|
Raises:
|
|
30
36
|
SystemExit: When command execution fails or invalid arguments provided.
|
|
31
37
|
"""
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
if dispatcher is None:
|
|
39
|
+
dispatcher = dispatch_command
|
|
40
|
+
|
|
41
|
+
# Install signal handler for clean Ctrl+C handling
|
|
42
|
+
signal.signal(signal.SIGINT, handle_sigint)
|
|
34
43
|
|
|
35
44
|
# Create the argument parser with all CLI options and subcommands
|
|
36
45
|
parser = create_parser()
|
|
@@ -45,4 +54,4 @@ def main() -> None:
|
|
|
45
54
|
configure_logging(log_level)
|
|
46
55
|
|
|
47
56
|
# Dispatch execution to the appropriate command handler
|
|
48
|
-
|
|
57
|
+
dispatcher(args)
|
equity_aggregator/cli/parser.py
CHANGED
|
@@ -68,7 +68,7 @@ def _add_subcommands(parser: argparse.ArgumentParser) -> None:
|
|
|
68
68
|
"seed",
|
|
69
69
|
help="aggregate enriched canonical equity data sourced from data feeds",
|
|
70
70
|
description="execute the full aggregation pipeline to collect equity "
|
|
71
|
-
"data from
|
|
71
|
+
"data from discovery feeds (LSEG, SEC, XETRA), enrich "
|
|
72
72
|
"it with data from enrichment feeds, and store as canonical equities",
|
|
73
73
|
)
|
|
74
74
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# cli/signals.py
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from types import FrameType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_sigint(signum: int, frame: FrameType | None) -> None: # pragma: no cover
|
|
9
|
+
"""
|
|
10
|
+
Handle SIGINT (Ctrl+C) by exiting immediately.
|
|
11
|
+
|
|
12
|
+
When the user presses Ctrl+C, print a clean message and exit immediately
|
|
13
|
+
with status code 130 (standard Unix convention for SIGINT). Uses os._exit()
|
|
14
|
+
for immediate termination and redirects stderr to /dev/null to suppress
|
|
15
|
+
any process cleanup errors from the parent process manager.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
signum: The signal number (SIGINT).
|
|
19
|
+
frame: The current stack frame.
|
|
20
|
+
"""
|
|
21
|
+
print("\nOperation cancelled by user", file=sys.stderr)
|
|
22
|
+
sys.stderr.flush()
|
|
23
|
+
|
|
24
|
+
# Redirect stderr to suppress further output during exit
|
|
25
|
+
try:
|
|
26
|
+
devnull = os.open(os.devnull, os.O_WRONLY)
|
|
27
|
+
os.dup2(devnull, sys.stderr.fileno())
|
|
28
|
+
os.close(devnull)
|
|
29
|
+
except OSError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
os._exit(130)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# _utils/__init__.py
|
|
2
2
|
|
|
3
3
|
from ._load_converter import get_usd_converter
|
|
4
|
-
from ._merge import merge
|
|
4
|
+
from ._merge import EquityIdentifiers, extract_identifiers, merge
|
|
5
5
|
|
|
6
|
-
__all__ = ["get_usd_converter", "merge"]
|
|
6
|
+
__all__ = ["get_usd_converter", "merge", "extract_identifiers", "EquityIdentifiers"]
|
|
@@ -11,6 +11,21 @@ logger = logging.getLogger(__name__)
|
|
|
11
11
|
|
|
12
12
|
type RawEquityConverter = Callable[[RawEquity], RawEquity]
|
|
13
13
|
|
|
14
|
+
# All monetary fields that should be converted to USD
|
|
15
|
+
_MONETARY_FIELDS = [
|
|
16
|
+
"last_price",
|
|
17
|
+
"market_cap",
|
|
18
|
+
"fifty_two_week_min",
|
|
19
|
+
"fifty_two_week_max",
|
|
20
|
+
"revenue_per_share",
|
|
21
|
+
"free_cash_flow",
|
|
22
|
+
"operating_cash_flow",
|
|
23
|
+
"total_debt",
|
|
24
|
+
"revenue",
|
|
25
|
+
"ebitda",
|
|
26
|
+
"trailing_eps",
|
|
27
|
+
]
|
|
28
|
+
|
|
14
29
|
|
|
15
30
|
def _build_usd_converter_loader() -> Callable[[], RawEquityConverter]:
|
|
16
31
|
"""
|
|
@@ -75,26 +90,21 @@ def _should_skip_conversion(equity: RawEquity) -> bool:
|
|
|
75
90
|
"""
|
|
76
91
|
Determines whether the conversion process should be skipped for a given equity.
|
|
77
92
|
|
|
78
|
-
The function
|
|
79
|
-
currency is
|
|
80
|
-
|
|
93
|
+
The function only skips conversion if the currency is not specified or if the
|
|
94
|
+
currency is already USD. This ensures that currency normalisation happens
|
|
95
|
+
even for records with no monetary values.
|
|
81
96
|
|
|
82
97
|
Args:
|
|
83
|
-
equity (RawEquity): The equity object containing
|
|
84
|
-
|
|
98
|
+
equity (RawEquity): The equity object containing price fields and currency
|
|
99
|
+
information.
|
|
85
100
|
|
|
86
101
|
Returns:
|
|
87
102
|
bool: True if conversion should be skipped, False otherwise.
|
|
88
103
|
"""
|
|
89
|
-
last_price = equity.last_price
|
|
90
|
-
market_cap = equity.market_cap
|
|
91
104
|
currency = equity.currency
|
|
92
105
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
or currency is None
|
|
96
|
-
or currency == "USD"
|
|
97
|
-
)
|
|
106
|
+
# Skip only if currency is missing or already USD
|
|
107
|
+
return currency is None or currency == "USD"
|
|
98
108
|
|
|
99
109
|
|
|
100
110
|
def _get_rate_for_currency(currency: str, rates: dict[str, Decimal]) -> Decimal:
|
|
@@ -115,7 +125,7 @@ def _get_rate_for_currency(currency: str, rates: dict[str, Decimal]) -> Decimal:
|
|
|
115
125
|
"""
|
|
116
126
|
rate = rates.get(currency)
|
|
117
127
|
if rate is None:
|
|
118
|
-
raise ValueError(f"Missing FX rate for currency {currency}")
|
|
128
|
+
raise ValueError(f"Missing FX rate for currency {currency}.")
|
|
119
129
|
return rate
|
|
120
130
|
|
|
121
131
|
|
|
@@ -134,11 +144,10 @@ def _build_field_updates(equity: RawEquity, rate: Decimal) -> dict[str, Decimal]
|
|
|
134
144
|
"""
|
|
135
145
|
updates: dict[str, Decimal] = {}
|
|
136
146
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
updates["market_cap"] = _convert_to_usd(equity.market_cap, rate)
|
|
147
|
+
for field_name in _MONETARY_FIELDS:
|
|
148
|
+
field_value = getattr(equity, field_name)
|
|
149
|
+
if field_value is not None:
|
|
150
|
+
updates[field_name] = _convert_to_usd(field_value, rate)
|
|
142
151
|
|
|
143
152
|
return updates
|
|
144
153
|
|
|
@@ -155,9 +164,9 @@ def _convert_to_usd(figure: Decimal, rate: Decimal) -> Decimal:
|
|
|
155
164
|
Decimal: The equivalent value in USD, rounded to two decimal places.
|
|
156
165
|
|
|
157
166
|
Raises:
|
|
158
|
-
ValueError: If the FX rate is zero.
|
|
167
|
+
ValueError: If the FX rate is zero or negative.
|
|
159
168
|
"""
|
|
160
|
-
if rate
|
|
161
|
-
raise ValueError("FX rate
|
|
169
|
+
if rate <= 0:
|
|
170
|
+
raise ValueError("FX rate must be positive")
|
|
162
171
|
|
|
163
172
|
return (figure / rate).quantize(Decimal("0.01"))
|