bellwether-mcp 1.3.1__tar.gz

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 (59) hide show
  1. bellwether_mcp-1.3.1/LICENSE +20 -0
  2. bellwether_mcp-1.3.1/PKG-INFO +73 -0
  3. bellwether_mcp-1.3.1/README.md +50 -0
  4. bellwether_mcp-1.3.1/bellwether_mcp/__init__.py +22 -0
  5. bellwether_mcp-1.3.1/bellwether_mcp/__main__.py +8 -0
  6. bellwether_mcp-1.3.1/bellwether_mcp/_version.py +5 -0
  7. bellwether_mcp-1.3.1/bellwether_mcp/api/__init__.py +3 -0
  8. bellwether_mcp-1.3.1/bellwether_mcp/api/coinmarketcap.py +125 -0
  9. bellwether_mcp-1.3.1/bellwether_mcp/api/fomc_calendar.py +97 -0
  10. bellwether_mcp-1.3.1/bellwether_mcp/api/fred.py +149 -0
  11. bellwether_mcp-1.3.1/bellwether_mcp/config.py +123 -0
  12. bellwether_mcp-1.3.1/bellwether_mcp/connectors/__init__.py +3 -0
  13. bellwether_mcp-1.3.1/bellwether_mcp/connectors/adapters.py +81 -0
  14. bellwether_mcp-1.3.1/bellwether_mcp/connectors/fomc_meetings.json +40 -0
  15. bellwether_mcp-1.3.1/bellwether_mcp/connectors/mcp_source_presets.json +147 -0
  16. bellwether_mcp-1.3.1/bellwether_mcp/connectors/tool_schema_fingerprints.json +11 -0
  17. bellwether_mcp-1.3.1/bellwether_mcp/credentials.py +70 -0
  18. bellwether_mcp-1.3.1/bellwether_mcp/errors.py +13 -0
  19. bellwether_mcp-1.3.1/bellwether_mcp/http_clients.py +28 -0
  20. bellwether_mcp-1.3.1/bellwether_mcp/mcp/__init__.py +3 -0
  21. bellwether_mcp-1.3.1/bellwether_mcp/mcp/clients.py +67 -0
  22. bellwether_mcp-1.3.1/bellwether_mcp/mcp/server.py +57 -0
  23. bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_crypto_reference.py +71 -0
  24. bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_fomc_calendar.py +49 -0
  25. bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_macro_series.py +134 -0
  26. bellwether_mcp-1.3.1/bellwether_mcp/mcp_config.json +3 -0
  27. bellwether_mcp-1.3.1/bellwether_mcp/mcp_server.py +26 -0
  28. bellwether_mcp-1.3.1/bellwether_mcp/ontology/__init__.py +3 -0
  29. bellwether_mcp-1.3.1/bellwether_mcp/ontology/certification.json +36 -0
  30. bellwether_mcp-1.3.1/bellwether_mcp/ontology/fixtures/records.json +135 -0
  31. bellwether_mcp-1.3.1/bellwether_mcp/ontology/mappings/source.yaml +119 -0
  32. bellwether_mcp-1.3.1/bellwether_mcp/ontology/market_data.ttl +41 -0
  33. bellwether_mcp-1.3.1/bellwether_mcp/ontology/migrations/manifest.json +6 -0
  34. bellwether_mcp-1.3.1/bellwether_mcp/ontology/shapes/connector.shacl.ttl +24 -0
  35. bellwether_mcp-1.3.1/bellwether_mcp/prompts/__init__.py +3 -0
  36. bellwether_mcp-1.3.1/bellwether_mcp/prompts/main_agent.json +20 -0
  37. bellwether_mcp-1.3.1/bellwether_mcp/prompts/market_data_specialist.json +51 -0
  38. bellwether_mcp-1.3.1/bellwether_mcp/skills/__init__.py +3 -0
  39. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/PKG-INFO +73 -0
  40. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/SOURCES.txt +57 -0
  41. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/dependency_links.txt +1 -0
  42. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/entry_points.txt +14 -0
  43. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/requires.txt +8 -0
  44. bellwether_mcp-1.3.1/bellwether_mcp.egg-info/top_level.txt +9 -0
  45. bellwether_mcp-1.3.1/pyproject.toml +83 -0
  46. bellwether_mcp-1.3.1/scripts/compute_tool_fingerprints.py +75 -0
  47. bellwether_mcp-1.3.1/scripts/run_agent_utilities_gate.py +104 -0
  48. bellwether_mcp-1.3.1/scripts/security_sanitizer.py +316 -0
  49. bellwether_mcp-1.3.1/setup.cfg +4 -0
  50. bellwether_mcp-1.3.1/tests/conftest.py +34 -0
  51. bellwether_mcp-1.3.1/tests/test_api_coinmarketcap.py +75 -0
  52. bellwether_mcp-1.3.1/tests/test_api_fomc_calendar.py +80 -0
  53. bellwether_mcp-1.3.1/tests/test_api_fred.py +76 -0
  54. bellwether_mcp-1.3.1/tests/test_concept_parity.py +87 -0
  55. bellwether_mcp-1.3.1/tests/test_env_prefix_alias.py +30 -0
  56. bellwether_mcp-1.3.1/tests/test_init_dynamics.py +66 -0
  57. bellwether_mcp-1.3.1/tests/test_presets_and_adapters.py +76 -0
  58. bellwether_mcp-1.3.1/tests/test_sdk_rate_limiter.py +26 -0
  59. bellwether_mcp-1.3.1/tests/test_startup.py +24 -0
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012-2023 Project Maintainers
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: bellwether-mcp
3
+ Version: 1.3.1
4
+ Summary: Crypto discovery/caps (CoinMarketCap), Fed macro series with point-in-time vintages (FRED/ALFRED), and the FOMC decision calendar as an MCP server and agent-connector-sdk source adapters
5
+ Author-email: Project Maintainers <maintainers@users.noreply.github.com>
6
+ License: MIT
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Environment :: Console
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: agent-connector-sdk[mcp]<1.0.0,>=0.3.13
16
+ Requires-Dist: httpx>=0.28.1
17
+ Provides-Extra: test
18
+ Requires-Dist: pytest-xdist>=3.8.0; extra == "test"
19
+ Requires-Dist: pytest>=9.1.1; extra == "test"
20
+ Requires-Dist: pytest-asyncio>=1.4.0; extra == "test"
21
+ Requires-Dist: pytest-cov>=7.1.0; extra == "test"
22
+ Dynamic: license-file
23
+
24
+ # bellwether-mcp
25
+
26
+ *Version: 1.3.1*
27
+
28
+ > **Documentation** — Installation, deployment, and tool usage are maintained in the
29
+ > [official documentation](https://knuckles-team.github.io/bellwether-mcp/).
30
+
31
+ Crypto discovery/rankings/market caps (CoinMarketCap), Fed macro and liquidity
32
+ series with point-in-time vintages (FRED/ALFRED), and the FOMC decision
33
+ calendar (hike/cut/hold, sourced) — as an MCP tool surface and, through
34
+ `agent-connector-sdk`'s declarative `mcp_tool` source adapter, as bounded,
35
+ checkpointed ingestion streams intended for EG's `finance-v1` world-model
36
+ ontology (`feat/finance-core`, not yet published — see AGENTS.md).
37
+
38
+ **Not a venue-specific OHLCV feed.** CoinMarketCap's general historical OHLCV
39
+ endpoint documents hourly/daily coverage only, not minute-level exchange
40
+ candles — pair this package with an `emerald-exchange` backend for tradable
41
+ bars and charts.
42
+
43
+ ## Sources
44
+
45
+ | Ledger row | Sources | Target class (provisional) |
46
+ |---|---|---|
47
+ | EH-412 (crypto discovery/caps) | CoinMarketCap listings/quotes/global-metrics | `Instrument`, `MacroEvent` |
48
+ | EH-412 (Fed macro series) | FRED/ALFRED `series/observations`, `series/vintagedates` | `EconomicIndicator` |
49
+ | EH-412 (FOMC calendar) | Curated dataset, sourced to federalreserve.gov press releases | `MacroEvent` |
50
+ | EH-422 (macro/liquidity) | FRED named aliases: `WALCL`, `WDTGAL`, `M2SL`, `RRPONTSYD`, `WRESBAL`, `DGS10`, `DTWEXBGS` | `EconomicIndicator` |
51
+
52
+ `Instrument`, `EconomicIndicator` and `MacroEvent` are the design doc's own
53
+ proposed finance-v1 class names
54
+ (`plans/refactor/proposals/FINANCE-INTEGRATION-20260924.md` section 4/7).
55
+ EG's `feat/finance-core` branch, which owns finance-v1, has not published
56
+ anything yet as of this package's build — see AGENTS.md "Ontology mapping"
57
+ before trusting these as final.
58
+
59
+ ## Tools
60
+
61
+ Eight MCP tools, one FastMCP server (`bellwether-mcp` console script):
62
+ `cmc_listings`, `cmc_quotes`, `cmc_global_metrics`, `fred_macro_series_aliases`,
63
+ `fred_series_observations`, `fred_series_observations_asof`,
64
+ `fred_series_vintage_dates`, `fomc_calendar_list`.
65
+
66
+ Every tool is keyless except `cmc_*` and `fred_*`, whose credentials are
67
+ resolved server-side through `agent_connector_sdk.credentials`
68
+ (`BELLWETHER_CMC_API_KEY`, `BELLWETHER_FRED_API_KEY`) — never accepted as a
69
+ tool argument. `fomc_calendar_list` reads a packaged, curated, sourced dataset
70
+ and needs no credential and no network.
71
+
72
+ See [`AGENTS.md`](AGENTS.md) for architecture, the ontology-mapping
73
+ authority, the FOMC dataset's provenance, rate limits, and commands.
@@ -0,0 +1,50 @@
1
+ # bellwether-mcp
2
+
3
+ *Version: 1.3.1*
4
+
5
+ > **Documentation** — Installation, deployment, and tool usage are maintained in the
6
+ > [official documentation](https://knuckles-team.github.io/bellwether-mcp/).
7
+
8
+ Crypto discovery/rankings/market caps (CoinMarketCap), Fed macro and liquidity
9
+ series with point-in-time vintages (FRED/ALFRED), and the FOMC decision
10
+ calendar (hike/cut/hold, sourced) — as an MCP tool surface and, through
11
+ `agent-connector-sdk`'s declarative `mcp_tool` source adapter, as bounded,
12
+ checkpointed ingestion streams intended for EG's `finance-v1` world-model
13
+ ontology (`feat/finance-core`, not yet published — see AGENTS.md).
14
+
15
+ **Not a venue-specific OHLCV feed.** CoinMarketCap's general historical OHLCV
16
+ endpoint documents hourly/daily coverage only, not minute-level exchange
17
+ candles — pair this package with an `emerald-exchange` backend for tradable
18
+ bars and charts.
19
+
20
+ ## Sources
21
+
22
+ | Ledger row | Sources | Target class (provisional) |
23
+ |---|---|---|
24
+ | EH-412 (crypto discovery/caps) | CoinMarketCap listings/quotes/global-metrics | `Instrument`, `MacroEvent` |
25
+ | EH-412 (Fed macro series) | FRED/ALFRED `series/observations`, `series/vintagedates` | `EconomicIndicator` |
26
+ | EH-412 (FOMC calendar) | Curated dataset, sourced to federalreserve.gov press releases | `MacroEvent` |
27
+ | EH-422 (macro/liquidity) | FRED named aliases: `WALCL`, `WDTGAL`, `M2SL`, `RRPONTSYD`, `WRESBAL`, `DGS10`, `DTWEXBGS` | `EconomicIndicator` |
28
+
29
+ `Instrument`, `EconomicIndicator` and `MacroEvent` are the design doc's own
30
+ proposed finance-v1 class names
31
+ (`plans/refactor/proposals/FINANCE-INTEGRATION-20260924.md` section 4/7).
32
+ EG's `feat/finance-core` branch, which owns finance-v1, has not published
33
+ anything yet as of this package's build — see AGENTS.md "Ontology mapping"
34
+ before trusting these as final.
35
+
36
+ ## Tools
37
+
38
+ Eight MCP tools, one FastMCP server (`bellwether-mcp` console script):
39
+ `cmc_listings`, `cmc_quotes`, `cmc_global_metrics`, `fred_macro_series_aliases`,
40
+ `fred_series_observations`, `fred_series_observations_asof`,
41
+ `fred_series_vintage_dates`, `fomc_calendar_list`.
42
+
43
+ Every tool is keyless except `cmc_*` and `fred_*`, whose credentials are
44
+ resolved server-side through `agent_connector_sdk.credentials`
45
+ (`BELLWETHER_CMC_API_KEY`, `BELLWETHER_FRED_API_KEY`) — never accepted as a
46
+ tool argument. `fomc_calendar_list` reads a packaged, curated, sourced dataset
47
+ and needs no credential and no network.
48
+
49
+ See [`AGENTS.md`](AGENTS.md) for architecture, the ontology-mapping
50
+ authority, the FOMC dataset's provenance, rate limits, and commands.
@@ -0,0 +1,22 @@
1
+ """bellwether-mcp: crypto discovery, Fed macro series and the FOMC calendar.
2
+
3
+ Wraps CoinMarketCap (crypto discovery/rankings/market caps — not a
4
+ venue-specific OHLCV feed; see ``api/coinmarketcap.py``), FRED/ALFRED (Fed
5
+ macro and liquidity series with point-in-time vintages), and a curated,
6
+ sourced FOMC decision calendar as an MCP tool surface and, through
7
+ ``agent-connector-sdk``'s declarative ``mcp_tool`` source adapter, as
8
+ bounded, checkpointed ingestion streams intended for the ``finance-v1``
9
+ world-model ontology once EG's parallel ``feat/finance-core`` publishes its
10
+ class/property names (see ``bellwether_mcp/ontology/mappings/source.yaml``
11
+ for the current, provisional mapping and how to update it).
12
+
13
+ This package never calls epistemic-graph directly: extraction rides an
14
+ in-process MCP session over its own tool surface, and delivery to a live
15
+ graph is owned by ``agent_connector_sdk.runner``/``sinks``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from bellwether_mcp._version import __version__
21
+
22
+ __all__ = ["__version__"]
@@ -0,0 +1,8 @@
1
+ """``python -m bellwether_mcp`` runs the MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from bellwether_mcp.mcp_server import mcp_server
6
+
7
+ if __name__ == "__main__":
8
+ mcp_server()
@@ -0,0 +1,5 @@
1
+ """Single-source package version."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "1.3.1"
@@ -0,0 +1,3 @@
1
+ """Thin async vendor API clients — pure functions, no MCP/adapter concerns."""
2
+
3
+ from __future__ import annotations
@@ -0,0 +1,125 @@
1
+ """CoinMarketCap Pro API: crypto discovery, rankings, market caps (EH-412).
2
+
3
+ https://coinmarketcap.com/api/documentation/pro-api-reference/cryptocurrency.
4
+ CMC is a discovery/caps source, not a venue-specific OHLCV feed — its general
5
+ historical OHLCV endpoint documents hourly/daily coverage only and must not
6
+ be assumed to supply minute-level exchange candles (that role stays with
7
+ emerald-exchange's venue backends). This client therefore exposes listings,
8
+ per-symbol quotes and global market metrics only.
9
+
10
+ Every endpoint requires ``X-CMC_PRO_API_KEY`` — there is no keyless CMC
11
+ endpoint, including the free "Basic" plan.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import Any
17
+
18
+ import httpx
19
+
20
+ __all__ = ["get_global_metrics", "get_quotes", "list_listings"]
21
+
22
+
23
+ def _quote_usd(item: dict[str, Any]) -> dict[str, Any]:
24
+ usd = item.get("quote", {}).get("USD", {})
25
+ return {
26
+ "price_usd": usd.get("price"),
27
+ "market_cap_usd": usd.get("market_cap"),
28
+ "volume_24h_usd": usd.get("volume_24h"),
29
+ "percent_change_24h": usd.get("percent_change_24h"),
30
+ "percent_change_7d": usd.get("percent_change_7d"),
31
+ "last_updated": usd.get("last_updated"),
32
+ }
33
+
34
+
35
+ def _listing_record(item: dict[str, Any]) -> dict[str, Any]:
36
+ return {
37
+ "cmc_id": str(item["id"]),
38
+ "name": item.get("name", ""),
39
+ "symbol": item.get("symbol", ""),
40
+ "cmc_rank": item.get("cmc_rank"),
41
+ "circulating_supply": item.get("circulating_supply"),
42
+ "total_supply": item.get("total_supply"),
43
+ "max_supply": item.get("max_supply"),
44
+ **_quote_usd(item),
45
+ }
46
+
47
+
48
+ async def list_listings(
49
+ client: httpx.AsyncClient, *, api_key: str, start: int, limit: int
50
+ ) -> dict[str, Any]:
51
+ """One page of the latest cryptocurrency listings, ranked by market cap.
52
+
53
+ ``start`` is a 0-based offset (this package's own convention, matching
54
+ ``ToolPreset(pagination="offset")``'s zero-based semantics) — translated
55
+ to CMC's native 1-based ``start`` query parameter internally, so a caller
56
+ never has to know CMC's API is 1-indexed.
57
+
58
+ CMC's listings endpoint reports no reliable total count, so callers page
59
+ until a page shorter than ``limit`` comes back (``total`` is always
60
+ ``None`` here).
61
+ """
62
+ response = await client.get(
63
+ "/v1/cryptocurrency/listings/latest",
64
+ headers={"X-CMC_PRO_API_KEY": api_key},
65
+ params={"start": start + 1, "limit": limit, "convert": "USD"},
66
+ )
67
+ response.raise_for_status()
68
+ payload = response.json()
69
+ items = [_listing_record(item) for item in payload.get("data", [])]
70
+ return {"items": items, "offset": start, "limit": limit, "total": None}
71
+
72
+
73
+ async def get_quotes(
74
+ client: httpx.AsyncClient, *, api_key: str, symbols: str
75
+ ) -> dict[str, Any]:
76
+ """Latest quote(s) for one or more comma-separated symbols (e.g. ``"BTC,ETH"``)."""
77
+ response = await client.get(
78
+ "/v1/cryptocurrency/quotes/latest",
79
+ headers={"X-CMC_PRO_API_KEY": api_key},
80
+ params={"symbol": symbols, "convert": "USD"},
81
+ )
82
+ response.raise_for_status()
83
+ payload = response.json()
84
+ data = payload.get("data", {})
85
+ items = []
86
+ for symbol, entries in data.items():
87
+ # CMC returns either one object or a list when a symbol maps to
88
+ # several listed assets (rare ticker collisions).
89
+ for item in entries if isinstance(entries, list) else [entries]:
90
+ items.append({"symbol": symbol, **_listing_record(item)})
91
+ return {"items": items, "offset": 0, "limit": len(items), "total": len(items)}
92
+
93
+
94
+ async def get_global_metrics(
95
+ client: httpx.AsyncClient, *, api_key: str
96
+ ) -> dict[str, Any]:
97
+ """Aggregate market metrics: total market cap, BTC/ETH dominance, volume.
98
+
99
+ A macro/liquidity gauge for the crypto market as a whole (EH-422) —
100
+ distinct from any single asset's quote.
101
+ """
102
+ response = await client.get(
103
+ "/v1/global-metrics/quotes/latest",
104
+ headers={"X-CMC_PRO_API_KEY": api_key},
105
+ params={"convert": "USD"},
106
+ )
107
+ response.raise_for_status()
108
+ payload = response.json().get("data", {})
109
+ usd = payload.get("quote", {}).get("USD", {})
110
+ return {
111
+ "items": [
112
+ {
113
+ "id": "global",
114
+ "active_cryptocurrencies": payload.get("active_cryptocurrencies"),
115
+ "btc_dominance": payload.get("btc_dominance"),
116
+ "eth_dominance": payload.get("eth_dominance"),
117
+ "total_market_cap_usd": usd.get("total_market_cap"),
118
+ "total_volume_24h_usd": usd.get("total_volume_24h"),
119
+ "last_updated": usd.get("last_updated"),
120
+ }
121
+ ],
122
+ "offset": 0,
123
+ "limit": 1,
124
+ "total": 1,
125
+ }
@@ -0,0 +1,97 @@
1
+ """The FOMC decision calendar as sourced records (EH-412).
2
+
3
+ The Federal Reserve does not publish its meeting calendar or decisions as a
4
+ JSON API — only as HTML
5
+ (https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm) and one
6
+ press-release page per meeting. This module therefore reads a curated,
7
+ versioned dataset (``bellwether_mcp/connectors/fomc_meetings.json``) instead
8
+ of scraping HTML at call time. Every record carries the Federal Reserve's own
9
+ press-release URL as ``source_url`` — never asserted as one of this package's
10
+ own findings. See ``AGENTS.md``'s "FOMC dataset provenance" section for how
11
+ and when this dataset was verified, and how to extend it.
12
+
13
+ No network I/O happens here; loading is a pure, cached read of packaged data.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ from functools import lru_cache
20
+ from importlib import resources
21
+ from typing import Any
22
+
23
+ from bellwether_mcp.errors import UnknownFomcOutcomeFilterError
24
+
25
+ __all__ = ["VALID_OUTCOMES", "list_meetings", "load_meetings"]
26
+
27
+ VALID_OUTCOMES = frozenset({"hike", "cut", "hold"})
28
+
29
+
30
+ @lru_cache(maxsize=1)
31
+ def load_meetings() -> tuple[dict[str, Any], ...]:
32
+ """The full curated FOMC meeting dataset, oldest first, each record sourced."""
33
+ raw = json.loads(
34
+ resources.files("bellwether_mcp.connectors")
35
+ .joinpath("fomc_meetings.json")
36
+ .read_text()
37
+ )
38
+ meetings = tuple(raw["meetings"])
39
+ for meeting in meetings:
40
+ if meeting["outcome"] not in VALID_OUTCOMES:
41
+ raise ValueError(
42
+ f"fomc_meetings.json has an invalid outcome {meeting['outcome']!r} "
43
+ f"for {meeting['decision_date']!r} (expected one of {sorted(VALID_OUTCOMES)})"
44
+ )
45
+ return meetings
46
+
47
+
48
+ def _record(meeting: dict[str, Any]) -> dict[str, Any]:
49
+ return {
50
+ "id": meeting["decision_date"],
51
+ "meeting_start_date": meeting["meeting_start_date"],
52
+ "decision_date": meeting["decision_date"],
53
+ "outcome": meeting["outcome"],
54
+ "bps_change": meeting["bps_change"],
55
+ "target_range_low_pct": meeting["target_range_low_pct"],
56
+ "target_range_high_pct": meeting["target_range_high_pct"],
57
+ "source_url": meeting["source_url"],
58
+ }
59
+
60
+
61
+ def list_meetings(
62
+ *,
63
+ since: str = "",
64
+ until: str = "",
65
+ outcome: str = "",
66
+ offset: int = 0,
67
+ limit: int = 100,
68
+ ) -> dict[str, Any]:
69
+ """Sourced FOMC decision records, optionally filtered by date range/outcome.
70
+
71
+ Args:
72
+ since: inclusive ISO ``decision_date`` lower bound (``""`` = no bound).
73
+ until: inclusive ISO ``decision_date`` upper bound (``""`` = no bound).
74
+ outcome: one of ``"hike"``, ``"cut"``, ``"hold"``, or ``""`` for all.
75
+
76
+ Raises:
77
+ UnknownFomcOutcomeFilterError: ``outcome`` is not empty and not one of
78
+ :data:`VALID_OUTCOMES`.
79
+ """
80
+ if outcome and outcome not in VALID_OUTCOMES:
81
+ raise UnknownFomcOutcomeFilterError(
82
+ f"outcome {outcome!r} is not one of {sorted(VALID_OUTCOMES)}"
83
+ )
84
+ matches = [
85
+ m
86
+ for m in load_meetings()
87
+ if (not since or m["decision_date"] >= since)
88
+ and (not until or m["decision_date"] <= until)
89
+ and (not outcome or m["outcome"] == outcome)
90
+ ]
91
+ page = matches[offset : offset + limit]
92
+ return {
93
+ "items": [_record(m) for m in page],
94
+ "offset": offset,
95
+ "limit": limit,
96
+ "total": len(matches),
97
+ }
@@ -0,0 +1,149 @@
1
+ """FRED + ALFRED (point-in-time vintages): Fed macro and liquidity series (EH-412/EH-422).
2
+
3
+ https://fred.stlouisfed.org/docs/api/fred/series_observations.html
4
+ https://fred.stlouisfed.org/docs/api/fred/series_vintagedates.html
5
+
6
+ ALFRED is not a separate host or API — it is the *same* FRED REST API's
7
+ ``realtime_start``/``realtime_end`` parameters on ``series/observations``,
8
+ which select "the data as it was known/published as of this date" instead of
9
+ today's latest revision. This client exposes that explicitly
10
+ (:func:`get_observations_asof`) rather than only ever returning today's
11
+ revised values, so a caller can reconstruct what a point-in-time backtest
12
+ would actually have seen (no look-ahead into later data revisions).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any
18
+
19
+ import httpx
20
+
21
+ __all__ = ["get_observations", "get_observations_asof", "get_vintage_dates"]
22
+
23
+
24
+ def _parse_observation_value(raw: Any) -> float | None:
25
+ """FRED encodes a missing observation as the literal string ``"."``."""
26
+ if raw is None or raw == ".":
27
+ return None
28
+ return float(raw)
29
+
30
+
31
+ def _observation_record(series_id: str, obs: dict[str, Any]) -> dict[str, Any]:
32
+ realtime_start = obs.get("realtime_start", "")
33
+ date = obs.get("date", "")
34
+ return {
35
+ # Unique within one call's vintage window; a single series_id+date can
36
+ # recur across different realtime_start vintages, so the record
37
+ # identity folds in the vintage too.
38
+ "observation_id": f"{series_id}:{date}:{realtime_start}",
39
+ "series_id": series_id,
40
+ "date": date,
41
+ "value": _parse_observation_value(obs.get("value")),
42
+ "realtime_start": realtime_start,
43
+ "realtime_end": obs.get("realtime_end", ""),
44
+ }
45
+
46
+
47
+ async def get_observations(
48
+ client: httpx.AsyncClient,
49
+ *,
50
+ api_key: str,
51
+ series_id: str,
52
+ offset: int = 0,
53
+ limit: int = 1000,
54
+ realtime_start: str | None = None,
55
+ realtime_end: str | None = None,
56
+ ) -> dict[str, Any]:
57
+ """One page of a FRED series' observations, latest-revision by default.
58
+
59
+ Pass ``realtime_start``/``realtime_end`` (``YYYY-MM-DD``) to pin the
60
+ vintage instead of today's latest revision — see
61
+ :func:`get_observations_asof` for the common "as of one date" case.
62
+ """
63
+ params: dict[str, Any] = {
64
+ "series_id": series_id,
65
+ "api_key": api_key,
66
+ "file_type": "json",
67
+ "offset": offset,
68
+ "limit": limit,
69
+ "sort_order": "asc",
70
+ }
71
+ if realtime_start:
72
+ params["realtime_start"] = realtime_start
73
+ if realtime_end:
74
+ params["realtime_end"] = realtime_end
75
+ response = await client.get("/series/observations", params=params)
76
+ response.raise_for_status()
77
+ payload = response.json()
78
+ items = [
79
+ _observation_record(series_id, obs) for obs in payload.get("observations", [])
80
+ ]
81
+ return {
82
+ "items": items,
83
+ "offset": offset,
84
+ "limit": limit,
85
+ "total": payload.get("count"),
86
+ }
87
+
88
+
89
+ async def get_observations_asof(
90
+ client: httpx.AsyncClient,
91
+ *,
92
+ api_key: str,
93
+ series_id: str,
94
+ vintage_date: str,
95
+ offset: int = 0,
96
+ limit: int = 1000,
97
+ ) -> dict[str, Any]:
98
+ """Point-in-time (ALFRED) observations: the series exactly as known on ``vintage_date``.
99
+
100
+ A pure convenience over :func:`get_observations` pinning both
101
+ ``realtime_start`` and ``realtime_end`` to the same date.
102
+ """
103
+ return await get_observations(
104
+ client,
105
+ api_key=api_key,
106
+ series_id=series_id,
107
+ offset=offset,
108
+ limit=limit,
109
+ realtime_start=vintage_date,
110
+ realtime_end=vintage_date,
111
+ )
112
+
113
+
114
+ async def get_vintage_dates(
115
+ client: httpx.AsyncClient,
116
+ *,
117
+ api_key: str,
118
+ series_id: str,
119
+ offset: int = 0,
120
+ limit: int = 1000,
121
+ ) -> dict[str, Any]:
122
+ """Every date this series' data was revised/published — the ALFRED vintage list.
123
+
124
+ Feed any entry back into :func:`get_observations_asof` as ``vintage_date``
125
+ to reconstruct that exact point-in-time view.
126
+ """
127
+ response = await client.get(
128
+ "/series/vintagedates",
129
+ params={
130
+ "series_id": series_id,
131
+ "api_key": api_key,
132
+ "file_type": "json",
133
+ "offset": offset,
134
+ "limit": limit,
135
+ },
136
+ )
137
+ response.raise_for_status()
138
+ payload = response.json()
139
+ dates = payload.get("vintage_dates", [])
140
+ items = [
141
+ {"vintage_id": f"{series_id}:{d}", "series_id": series_id, "vintage_date": d}
142
+ for d in dates
143
+ ]
144
+ return {
145
+ "items": items,
146
+ "offset": offset,
147
+ "limit": limit,
148
+ "total": payload.get("count"),
149
+ }