diff --git a/eodhd/APIs/CongressionalTradesAPI.py b/eodhd/APIs/CongressionalTradesAPI.py new file mode 100644 index 0000000..c913543 --- /dev/null +++ b/eodhd/APIs/CongressionalTradesAPI.py @@ -0,0 +1,109 @@ +# APIs/CongressionalTradesAPI.py + +from .BaseAPI import BaseAPI + + +class CongressionalTradesAPI(BaseAPI): + """ + Wrapper for the Congressional Trades endpoint: + + GET /api/congressional-trades + + US Congress stock-trade disclosures filed under the STOCK Act, from the + official Senate EFD and House Clerk sources. Requires the All-in-One plan; + each request costs 10 API calls. + + Optional filters (flat query keys): + symbol, chamber (senate/house), bioguide_id, + transaction_type (purchase/sale/exchange, comma-separated for multiple), + transaction_date_from, transaction_date_to, + disclosure_date_from, disclosure_date_to + + Pagination: + page_offset -> page[offset] (default 0) + page_limit -> page[limit] (default 20, max 100) + """ + + _CHAMBERS = ("senate", "house") + _TRANSACTION_TYPES = ("purchase", "sale", "exchange") + + def get_congressional_trades( + self, + api_token: str, + symbol: str = None, + chamber: str = None, + bioguide_id: str = None, + transaction_type: str = None, + transaction_date_from: str = None, + transaction_date_to: str = None, + disclosure_date_from: str = None, + disclosure_date_to: str = None, + page_offset: int = None, + page_limit: int = None, + ): + """ + Parameters + ---------- + api_token : str + Your EODHD API token. + symbol : str, optional + Filter by a single ticker symbol, e.g. "AAPL". + chamber : str, optional + Filter by chamber: "senate" or "house". + bioguide_id : str, optional + Filter by a member's Bioguide ID, e.g. "S000250". + transaction_type : str, optional + One or more of "purchase", "sale", "exchange", comma-separated. + transaction_date_from, transaction_date_to : str, optional + Transaction date range, YYYY-MM-DD, inclusive. + disclosure_date_from, disclosure_date_to : str, optional + Disclosure date range, YYYY-MM-DD, inclusive. + page_offset : int, optional + Pagination offset (records to skip). Default 0. + page_limit : int, optional + Records per page. Default 20, maximum 100. + + Returns + ------- + dict + JSON response with keys: data (array), meta (total, page), links (next). + """ + if chamber is not None: + chamber = str(chamber).lower() + if chamber not in self._CHAMBERS: + raise ValueError("chamber must be 'senate' or 'house'.") + + if transaction_type is not None: + for value in str(transaction_type).split(","): + if value.strip().lower() not in self._TRANSACTION_TYPES: + raise ValueError( + "transaction_type values must be one of 'purchase', 'sale', 'exchange'." + ) + + querystring = "" + if symbol is not None: + querystring += f"&symbol={symbol}" + if chamber is not None: + querystring += f"&chamber={chamber}" + if bioguide_id is not None: + querystring += f"&bioguide_id={bioguide_id}" + if transaction_type is not None: + querystring += f"&transaction_type={transaction_type}" + if transaction_date_from is not None: + querystring += f"&transaction_date_from={transaction_date_from}" + if transaction_date_to is not None: + querystring += f"&transaction_date_to={transaction_date_to}" + if disclosure_date_from is not None: + querystring += f"&disclosure_date_from={disclosure_date_from}" + if disclosure_date_to is not None: + querystring += f"&disclosure_date_to={disclosure_date_to}" + if page_offset is not None: + querystring += f"&page[offset]={int(page_offset)}" + if page_limit is not None: + querystring += f"&page[limit]={int(page_limit)}" + + return self._rest_get_method( + api_key=api_token, + endpoint="congressional-trades", + querystring=querystring, + ) diff --git a/eodhd/APIs/__init__.py b/eodhd/APIs/__init__.py index 0798a7e..9cb28c8 100644 --- a/eodhd/APIs/__init__.py +++ b/eodhd/APIs/__init__.py @@ -34,6 +34,7 @@ from .BulkFundamentalsAPI import BulkFundamentalsAPI from .TreasuryAPI import TreasuryAPI from .ExchangeDetailsV2API import ExchangeDetailsV2API +from .CongressionalTradesAPI import CongressionalTradesAPI #Marketplace endpoints from .MPIndexComponentsAPI import MPIndexComponentsAPI diff --git a/eodhd/apiclient.py b/eodhd/apiclient.py index b380002..187426d 100644 --- a/eodhd/apiclient.py +++ b/eodhd/apiclient.py @@ -51,6 +51,7 @@ from eodhd.APIs import BulkFundamentalsAPI from eodhd.APIs import TreasuryAPI from eodhd.APIs import ExchangeDetailsV2API +from eodhd.APIs import CongressionalTradesAPI #Marketplace endpoints from eodhd.APIs import MPIndexComponentsAPI @@ -1586,6 +1587,51 @@ def get_treasury_real_yield_rates(self, from_date=None, to_date=None): api_call = TreasuryAPI(session=self._session, timeout=self._timeout) return api_call.get_treasury_real_yield_rates(api_token=self._api_key, from_date=from_date, to_date=to_date) + # ── Congressional Trades ────────────────────────────────────── + + def get_congressional_trades( + self, + symbol=None, + chamber=None, + bioguide_id=None, + transaction_type=None, + transaction_date_from=None, + transaction_date_to=None, + disclosure_date_from=None, + disclosure_date_to=None, + page_offset=None, + page_limit=None, + ): + """ + Congressional Trades API + Endpoint: GET /api/congressional-trades + + US Senate and House stock-trade disclosures filed under the STOCK Act. + Requires the All-in-One plan; each request costs 10 API calls. + + Optional filters: symbol, chamber ("senate"/"house"), bioguide_id, + transaction_type ("purchase"/"sale"/"exchange", comma-separated for + multiple), transaction_date_from/to, disclosure_date_from/to. + Pagination: page_offset (default 0), page_limit (default 20, max 100). + + Returns: + dict with keys: data, meta (total, page), links (next). + """ + api_call = CongressionalTradesAPI(session=self._session, timeout=self._timeout) + return api_call.get_congressional_trades( + api_token=self._api_key, + symbol=symbol, + chamber=chamber, + bioguide_id=bioguide_id, + transaction_type=transaction_type, + transaction_date_from=transaction_date_from, + transaction_date_to=transaction_date_to, + disclosure_date_from=disclosure_date_from, + disclosure_date_to=disclosure_date_to, + page_offset=page_offset, + page_limit=page_limit, + ) + # ── Phase 2: Marketplace ────────────────────────────────────── # --- InvestVerte ESG (6 methods) --- diff --git a/tests/test_congressional_trades.py b/tests/test_congressional_trades.py new file mode 100644 index 0000000..febd24d --- /dev/null +++ b/tests/test_congressional_trades.py @@ -0,0 +1,84 @@ +"""Tests for CongressionalTradesAPI.""" + +import pytest +from unittest.mock import MagicMock + +from eodhd.APIs.CongressionalTradesAPI import CongressionalTradesAPI + + +@pytest.fixture +def mock_session(): + return MagicMock() + + +def _make_api(session): + return CongressionalTradesAPI(session=session) + + +def _mock_response(session, data=None): + resp = MagicMock() + resp.status_code = 200 + resp.json.return_value = data or { + "data": [], + "meta": {"total": 0, "page": {"offset": 0, "limit": 20}}, + "links": {"next": None}, + } + session.get.return_value = resp + + +def test_basic_request(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + result = api.get_congressional_trades(api_token="test1234567890123456") + + call_url = mock_session.get.call_args[0][0] + assert "/congressional-trades" in call_url + assert "meta" in result + + +def test_filters(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_congressional_trades( + api_token="test1234567890123456", + chamber="senate", + transaction_type="purchase,sale", + symbol="AAPL", + bioguide_id="S000250", + ) + + call_url = mock_session.get.call_args[0][0] + assert "&chamber=senate" in call_url + assert "&transaction_type=purchase,sale" in call_url + assert "&symbol=AAPL" in call_url + assert "&bioguide_id=S000250" in call_url + + +def test_date_range_and_pagination(mock_session): + _mock_response(mock_session) + api = _make_api(mock_session) + api.get_congressional_trades( + api_token="test1234567890123456", + transaction_date_from="2024-01-01", + transaction_date_to="2024-12-31", + page_offset=0, + page_limit=50, + ) + + call_url = mock_session.get.call_args[0][0] + assert "&transaction_date_from=2024-01-01" in call_url + assert "&transaction_date_to=2024-12-31" in call_url + assert "&page[offset]=0" in call_url + assert "&page[limit]=50" in call_url + + +def test_invalid_chamber_raises(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_congressional_trades(api_token="test1234567890123456", chamber="duma") + + +def test_invalid_transaction_type_raises(mock_session): + api = _make_api(mock_session) + with pytest.raises(ValueError): + api.get_congressional_trades(api_token="test1234567890123456", transaction_type="bribe")