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
@@ -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.debug("Loaded %d OpenFIGI records from cache.", len(cached))
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.debug("Saved %d OpenFIGI identification records to cache.", len(identities))
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 marketSecDes.
423
+ dict[str, str]: A dict with idType, idValue, and securityType.
420
424
  """
421
425
  if equity.isin:
422
- return {"idType": "ID_ISIN", "idValue": equity.isin, "marketSecDes": "Equity"}
426
+ return {
427
+ "idType": "ID_ISIN",
428
+ "idValue": equity.isin,
429
+ "securityType": "Common Stock",
430
+ }
423
431
 
424
432
  if equity.cusip:
425
- return {"idType": "ID_CUSIP", "idValue": equity.cusip, "marketSecDes": "Equity"}
426
-
427
- return {"idType": "TICKER", "idValue": equity.symbol, "marketSecDes": "Equity"}
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
- commands = {
39
- "seed": lambda: run_command(seed),
40
- "export": lambda: run_command(lambda: export(args.output_dir, download)),
41
- "download": lambda: run_command(download),
42
- }
43
-
44
- handler = commands.get(args.cmd)
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:
@@ -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
- # Immediate force exit on Ctrl+C
33
- signal.signal(signal.SIGINT, lambda s, f: os._exit(130))
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
- dispatch_command(args)
57
+ dispatcher(args)
@@ -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 authoritative feeds (Euronext, LSE, SEC, XETRA), enrich "
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 checks if both the last price and market cap are missing, if the
79
- currency is not specified, or if the currency is already USD. In any of these
80
- cases, conversion is deemed unnecessary and the function returns True.
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 last price, market cap,
84
- and currency information.
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
- return (
94
- (last_price is None and market_cap is None)
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
- if equity.last_price is not None:
138
- updates["last_price"] = _convert_to_usd(equity.last_price, rate)
139
-
140
- if equity.market_cap is not None:
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 == 0:
161
- raise ValueError("FX rate cannot be zero")
169
+ if rate <= 0:
170
+ raise ValueError("FX rate must be positive")
162
171
 
163
172
  return (figure / rate).quantize(Decimal("0.01"))