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.
- bellwether_mcp-1.3.1/LICENSE +20 -0
- bellwether_mcp-1.3.1/PKG-INFO +73 -0
- bellwether_mcp-1.3.1/README.md +50 -0
- bellwether_mcp-1.3.1/bellwether_mcp/__init__.py +22 -0
- bellwether_mcp-1.3.1/bellwether_mcp/__main__.py +8 -0
- bellwether_mcp-1.3.1/bellwether_mcp/_version.py +5 -0
- bellwether_mcp-1.3.1/bellwether_mcp/api/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/api/coinmarketcap.py +125 -0
- bellwether_mcp-1.3.1/bellwether_mcp/api/fomc_calendar.py +97 -0
- bellwether_mcp-1.3.1/bellwether_mcp/api/fred.py +149 -0
- bellwether_mcp-1.3.1/bellwether_mcp/config.py +123 -0
- bellwether_mcp-1.3.1/bellwether_mcp/connectors/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/connectors/adapters.py +81 -0
- bellwether_mcp-1.3.1/bellwether_mcp/connectors/fomc_meetings.json +40 -0
- bellwether_mcp-1.3.1/bellwether_mcp/connectors/mcp_source_presets.json +147 -0
- bellwether_mcp-1.3.1/bellwether_mcp/connectors/tool_schema_fingerprints.json +11 -0
- bellwether_mcp-1.3.1/bellwether_mcp/credentials.py +70 -0
- bellwether_mcp-1.3.1/bellwether_mcp/errors.py +13 -0
- bellwether_mcp-1.3.1/bellwether_mcp/http_clients.py +28 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/clients.py +67 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/server.py +57 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_crypto_reference.py +71 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_fomc_calendar.py +49 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp/tools_macro_series.py +134 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp_config.json +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/mcp_server.py +26 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/certification.json +36 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/fixtures/records.json +135 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/mappings/source.yaml +119 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/market_data.ttl +41 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/migrations/manifest.json +6 -0
- bellwether_mcp-1.3.1/bellwether_mcp/ontology/shapes/connector.shacl.ttl +24 -0
- bellwether_mcp-1.3.1/bellwether_mcp/prompts/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp/prompts/main_agent.json +20 -0
- bellwether_mcp-1.3.1/bellwether_mcp/prompts/market_data_specialist.json +51 -0
- bellwether_mcp-1.3.1/bellwether_mcp/skills/__init__.py +3 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/PKG-INFO +73 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/SOURCES.txt +57 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/dependency_links.txt +1 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/entry_points.txt +14 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/requires.txt +8 -0
- bellwether_mcp-1.3.1/bellwether_mcp.egg-info/top_level.txt +9 -0
- bellwether_mcp-1.3.1/pyproject.toml +83 -0
- bellwether_mcp-1.3.1/scripts/compute_tool_fingerprints.py +75 -0
- bellwether_mcp-1.3.1/scripts/run_agent_utilities_gate.py +104 -0
- bellwether_mcp-1.3.1/scripts/security_sanitizer.py +316 -0
- bellwether_mcp-1.3.1/setup.cfg +4 -0
- bellwether_mcp-1.3.1/tests/conftest.py +34 -0
- bellwether_mcp-1.3.1/tests/test_api_coinmarketcap.py +75 -0
- bellwether_mcp-1.3.1/tests/test_api_fomc_calendar.py +80 -0
- bellwether_mcp-1.3.1/tests/test_api_fred.py +76 -0
- bellwether_mcp-1.3.1/tests/test_concept_parity.py +87 -0
- bellwether_mcp-1.3.1/tests/test_env_prefix_alias.py +30 -0
- bellwether_mcp-1.3.1/tests/test_init_dynamics.py +66 -0
- bellwether_mcp-1.3.1/tests/test_presets_and_adapters.py +76 -0
- bellwether_mcp-1.3.1/tests/test_sdk_rate_limiter.py +26 -0
- 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,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
|
+
}
|