finbrain-python 0.2.0__tar.gz → 0.2.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 (77) hide show
  1. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/CHANGELOG.md +10 -0
  2. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/PKG-INFO +15 -2
  3. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/README.md +14 -1
  4. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/client.py +2 -0
  5. finbrain_python-0.2.1/src/finbrain/aio/endpoints/corporate_lobbying.py +48 -0
  6. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/client.py +2 -0
  7. finbrain_python-0.2.1/src/finbrain/endpoints/corporate_lobbying.py +72 -0
  8. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/plotting.py +156 -0
  9. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain_python.egg-info/PKG-INFO +15 -2
  10. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain_python.egg-info/SOURCES.txt +3 -0
  11. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_async_client.py +1 -0
  12. finbrain_python-0.2.1/tests/test_corporate_lobbying.py +104 -0
  13. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_integration.py +27 -0
  14. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/.gitattributes +0 -0
  15. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/.github/workflows/ci.yml +0 -0
  16. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/.github/workflows/release.yml +0 -0
  17. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/.gitignore +0 -0
  18. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/LICENSE +0 -0
  19. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/RELEASE.md +0 -0
  20. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/examples/async_example.py +0 -0
  21. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/examples/transactions_plotting_example.py +0 -0
  22. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/pyproject.toml +0 -0
  23. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/setup.cfg +0 -0
  24. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/__init__.py +0 -0
  25. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/__init__.py +0 -0
  26. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/__init__.py +0 -0
  27. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/_utils.py +0 -0
  28. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
  29. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
  30. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/available.py +0 -0
  31. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/house_trades.py +0 -0
  32. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
  33. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
  34. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/news.py +0 -0
  35. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/options.py +0 -0
  36. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/predictions.py +0 -0
  37. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/recent.py +0 -0
  38. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/screener.py +0 -0
  39. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/senate_trades.py +0 -0
  40. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/aio/endpoints/sentiments.py +0 -0
  41. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/__init__.py +0 -0
  42. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/_utils.py +0 -0
  43. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/analyst_ratings.py +0 -0
  44. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/app_ratings.py +0 -0
  45. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/available.py +0 -0
  46. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/house_trades.py +0 -0
  47. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/insider_transactions.py +0 -0
  48. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/linkedin_data.py +0 -0
  49. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/news.py +0 -0
  50. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/options.py +0 -0
  51. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/predictions.py +0 -0
  52. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/recent.py +0 -0
  53. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/screener.py +0 -0
  54. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/senate_trades.py +0 -0
  55. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/endpoints/sentiments.py +0 -0
  56. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/exceptions.py +0 -0
  57. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain/py.typed +0 -0
  58. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
  59. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain_python.egg-info/requires.txt +0 -0
  60. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/src/finbrain_python.egg-info/top_level.txt +0 -0
  61. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/__init__.py +0 -0
  62. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/conftest.py +0 -0
  63. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_analyst_ratings.py +0 -0
  64. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_app_ratings.py +0 -0
  65. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_available.py +0 -0
  66. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_envelope.py +0 -0
  67. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_house_trades.py +0 -0
  68. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_insider_transactions.py +0 -0
  69. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_linkedin_data.py +0 -0
  70. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_news.py +0 -0
  71. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_options.py +0 -0
  72. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_plotting.py +0 -0
  73. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_predictions.py +0 -0
  74. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_recent.py +0 -0
  75. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_screener.py +0 -0
  76. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_senate_trades.py +0 -0
  77. {finbrain_python-0.2.0 → finbrain_python-0.2.1}/tests/test_sentiments.py +0 -0
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2026-03-12
9
+
10
+ ### Added
11
+
12
+ - **Corporate Lobbying Endpoint**: `fb.corporate_lobbying.ticker("AAPL")` — fetch corporate lobbying filings with income, expenses, registrant details, issue codes, and government entities (`/lobbying/{SYMBOL}`)
13
+ - **Corporate Lobbying Plotting**: `fb.plot.corporate_lobbying()` — visualize lobbying spend (income + expenses) as bars on a secondary y-axis overlaid on a price chart, with hover details showing registrant, quarter, income, and expenses
14
+ - **Async Corporate Lobbying**: Full async support via `AsyncCorporateLobbyingAPI`
15
+ - **Corporate Lobbying Tests**: Unit tests (`tests/test_corporate_lobbying.py`) and integration tests
16
+
8
17
  ## [0.2.0] - 2026-03-09
9
18
 
10
19
  ### BREAKING CHANGES
@@ -128,6 +137,7 @@ Previous releases...
128
137
 
129
138
  Previous releases...
130
139
 
140
+ [0.2.1]: https://github.com/ahmetsbilgin/finbrain-python/compare/v0.2.0...v0.2.1
131
141
  [0.2.0]: https://github.com/ahmetsbilgin/finbrain-python/compare/v0.1.8...v0.2.0
132
142
  [0.1.8]: https://github.com/ahmetsbilgin/finbrain-python/compare/v0.1.7...v0.1.8
133
143
  [0.1.7]: https://github.com/ahmetsbilgin/finbrain-python/compare/v0.1.6...v0.1.7
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finbrain-python
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Official Python client for the FinBrain API
5
5
  Author-email: Ahmet Salim Bilgin <ahmet@finbrain.tech>
6
6
  License-Expression: MIT
@@ -93,6 +93,12 @@ fb.senate_trades.ticker("META",
93
93
  date_to="2025-06-30",
94
94
  as_dataframe=True)
95
95
 
96
+ # ---------- corporate lobbying ----------
97
+ fb.corporate_lobbying.ticker("AAPL",
98
+ date_from="2024-01-01",
99
+ date_to="2025-06-30",
100
+ as_dataframe=True)
101
+
96
102
  # ---------- insider transactions ----------
97
103
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
98
104
 
@@ -205,7 +211,7 @@ fb.plot.sentiments("AMZN",
205
211
  date_from="2025-01-01",
206
212
  date_to="2025-06-30")
207
213
 
208
- # ---------- Insider Transactions, House & Senate Trades (requires user price data) ----------
214
+ # ---------- Insider Transactions, House & Senate Trades, Corporate Lobbying (requires user price data) ----------
209
215
  # These plots overlay transaction markers on a price chart.
210
216
  # Since FinBrain doesn't provide historical prices, you must provide your own:
211
217
 
@@ -232,6 +238,12 @@ fb.plot.senate_trades("META",
232
238
  price_data=price_df,
233
239
  date_from="2025-01-01",
234
240
  date_to="2025-06-30")
241
+
242
+ # Plot corporate lobbying spend on your price chart
243
+ fb.plot.corporate_lobbying("AAPL",
244
+ price_data=price_df,
245
+ date_from="2024-01-01",
246
+ date_to="2025-06-30")
235
247
  ```
236
248
 
237
249
  **Price Data Requirements:**
@@ -276,6 +288,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
276
288
  | Analyst ratings | `client.analyst_ratings.ticker()` | `/analyst-ratings/{SYMBOL}` |
277
289
  | House trades | `client.house_trades.ticker()` | `/congress/house/{SYMBOL}` |
278
290
  | Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
291
+ | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
279
292
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
280
293
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
281
294
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -67,6 +67,12 @@ fb.senate_trades.ticker("META",
67
67
  date_to="2025-06-30",
68
68
  as_dataframe=True)
69
69
 
70
+ # ---------- corporate lobbying ----------
71
+ fb.corporate_lobbying.ticker("AAPL",
72
+ date_from="2024-01-01",
73
+ date_to="2025-06-30",
74
+ as_dataframe=True)
75
+
70
76
  # ---------- insider transactions ----------
71
77
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
72
78
 
@@ -179,7 +185,7 @@ fb.plot.sentiments("AMZN",
179
185
  date_from="2025-01-01",
180
186
  date_to="2025-06-30")
181
187
 
182
- # ---------- Insider Transactions, House & Senate Trades (requires user price data) ----------
188
+ # ---------- Insider Transactions, House & Senate Trades, Corporate Lobbying (requires user price data) ----------
183
189
  # These plots overlay transaction markers on a price chart.
184
190
  # Since FinBrain doesn't provide historical prices, you must provide your own:
185
191
 
@@ -206,6 +212,12 @@ fb.plot.senate_trades("META",
206
212
  price_data=price_df,
207
213
  date_from="2025-01-01",
208
214
  date_to="2025-06-30")
215
+
216
+ # Plot corporate lobbying spend on your price chart
217
+ fb.plot.corporate_lobbying("AAPL",
218
+ price_data=price_df,
219
+ date_from="2024-01-01",
220
+ date_to="2025-06-30")
209
221
  ```
210
222
 
211
223
  **Price Data Requirements:**
@@ -250,6 +262,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
250
262
  | Analyst ratings | `client.analyst_ratings.ticker()` | `/analyst-ratings/{SYMBOL}` |
251
263
  | House trades | `client.house_trades.ticker()` | `/congress/house/{SYMBOL}` |
252
264
  | Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
265
+ | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
253
266
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
254
267
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
255
268
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -22,6 +22,7 @@ from .endpoints.options import AsyncOptionsAPI
22
22
  from .endpoints.news import AsyncNewsAPI
23
23
  from .endpoints.screener import AsyncScreenerAPI
24
24
  from .endpoints.recent import AsyncRecentAPI
25
+ from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
25
26
 
26
27
 
27
28
  # Which status codes merit a retry
@@ -80,6 +81,7 @@ class AsyncFinBrainClient:
80
81
  self.news = AsyncNewsAPI(self)
81
82
  self.screener = AsyncScreenerAPI(self)
82
83
  self.recent = AsyncRecentAPI(self)
84
+ self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
83
85
 
84
86
  async def __aenter__(self) -> "AsyncFinBrainClient":
85
87
  """Context manager entry."""
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import AsyncFinBrainClient
10
+
11
+
12
+ class AsyncCorporateLobbyingAPI:
13
+ """Async wrapper for /lobbying endpoints."""
14
+
15
+ def __init__(self, client: "AsyncFinBrainClient") -> None:
16
+ self._c = client
17
+
18
+ async def ticker(
19
+ self,
20
+ symbol: str,
21
+ *,
22
+ date_from: _dt.date | str | None = None,
23
+ date_to: _dt.date | str | None = None,
24
+ limit: int | None = None,
25
+ as_dataframe: bool = False,
26
+ ) -> Dict[str, Any] | pd.DataFrame:
27
+ """Fetch corporate lobbying filings for a symbol (async)."""
28
+ params: Dict[str, str] = {}
29
+ if date_from:
30
+ params["startDate"] = to_datestr(date_from)
31
+ if date_to:
32
+ params["endDate"] = to_datestr(date_to)
33
+ if limit is not None:
34
+ params["limit"] = str(limit)
35
+
36
+ path = f"lobbying/{symbol.upper()}"
37
+
38
+ data: Dict[str, Any] = await self._c._request("GET", path, params=params)
39
+
40
+ if as_dataframe:
41
+ rows = data.get("filings", [])
42
+ df = pd.DataFrame(rows)
43
+ if not df.empty and "date" in df.columns:
44
+ df["date"] = pd.to_datetime(df["date"])
45
+ df.set_index("date", inplace=True)
46
+ return df
47
+
48
+ return data
@@ -23,6 +23,7 @@ from .endpoints.options import OptionsAPI
23
23
  from .endpoints.news import NewsAPI
24
24
  from .endpoints.screener import ScreenerAPI
25
25
  from .endpoints.recent import RecentAPI
26
+ from .endpoints.corporate_lobbying import CorporateLobbyingAPI
26
27
 
27
28
 
28
29
  # Which status codes merit a retry
@@ -75,6 +76,7 @@ class FinBrainClient:
75
76
  self.news = NewsAPI(self)
76
77
  self.screener = ScreenerAPI(self)
77
78
  self.recent = RecentAPI(self)
79
+ self.corporate_lobbying = CorporateLobbyingAPI(self)
78
80
 
79
81
  # ---------- private helpers ----------
80
82
  def _request(
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from typing import TYPE_CHECKING, Dict, Any, List
5
+
6
+ from ._utils import to_datestr
7
+
8
+ if TYPE_CHECKING: # imported only by type-checkers
9
+ from ..client import FinBrainClient
10
+
11
+
12
+ class CorporateLobbyingAPI:
13
+ """
14
+ Endpoint
15
+ --------
16
+ ``/lobbying/<TICKER>`` - corporate lobbying filings for the selected ticker.
17
+ """
18
+
19
+ # ------------------------------------------------------------------ #
20
+ def __init__(self, client: "FinBrainClient") -> None:
21
+ self._c = client # reference to the parent client
22
+
23
+ # ------------------------------------------------------------------ #
24
+ def ticker(
25
+ self,
26
+ symbol: str,
27
+ *,
28
+ date_from: _dt.date | str | None = None,
29
+ date_to: _dt.date | str | None = None,
30
+ limit: int | None = None,
31
+ as_dataframe: bool = False,
32
+ ) -> Dict[str, Any] | pd.DataFrame:
33
+ """
34
+ Fetch corporate lobbying filings for *symbol*.
35
+
36
+ Parameters
37
+ ----------
38
+ symbol :
39
+ Ticker symbol; auto-upper-cased.
40
+ date_from, date_to :
41
+ Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows.
42
+ limit :
43
+ Maximum number of records to return.
44
+ as_dataframe :
45
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
46
+ otherwise return the raw JSON dict.
47
+
48
+ Returns
49
+ -------
50
+ dict | pandas.DataFrame
51
+ """
52
+ params: Dict[str, str] = {}
53
+ if date_from:
54
+ params["startDate"] = to_datestr(date_from)
55
+ if date_to:
56
+ params["endDate"] = to_datestr(date_to)
57
+ if limit is not None:
58
+ params["limit"] = str(limit)
59
+
60
+ path = f"lobbying/{symbol.upper()}"
61
+
62
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
63
+
64
+ if as_dataframe:
65
+ rows: List[Dict[str, Any]] = data.get("filings", [])
66
+ df = pd.DataFrame(rows)
67
+ if not df.empty and "date" in df.columns:
68
+ df["date"] = pd.to_datetime(df["date"])
69
+ df.set_index("date", inplace=True)
70
+ return df
71
+
72
+ return data
@@ -751,6 +751,162 @@ class _PlotNamespace:
751
751
  return None
752
752
  return fig.to_json() if as_json else fig
753
753
 
754
+ # --------------------------------------------------------------------- #
755
+ # Corporate Lobbying → bars on price chart #
756
+ # --------------------------------------------------------------------- #
757
+ def corporate_lobbying(
758
+ self,
759
+ ticker: str,
760
+ price_data: pd.DataFrame,
761
+ *,
762
+ date_from: str | None = None,
763
+ date_to: str | None = None,
764
+ as_json: bool = False,
765
+ show: bool = True,
766
+ template: str = "plotly_dark",
767
+ **kwargs,
768
+ ):
769
+ """
770
+ Plot corporate lobbying spend overlaid on a price chart.
771
+
772
+ This method requires user-provided historical price data, as FinBrain
773
+ does not currently offer a price history endpoint.
774
+
775
+ Parameters
776
+ ----------
777
+ ticker : str
778
+ Ticker symbol (e.g. ``"AAPL"``).
779
+ price_data : pandas.DataFrame
780
+ **User-provided** price history with a DatetimeIndex and a column
781
+ containing prices (e.g. ``"close"``, ``"Close"``, or ``"price"``).
782
+ The index must be timezone-naive or UTC.
783
+ date_from, date_to : str or None, optional
784
+ Date range for filings in ``YYYY-MM-DD`` format.
785
+ as_json : bool, default False
786
+ If ``True``, return JSON string instead of Figure object.
787
+ show : bool, default True
788
+ If ``True`` and ``as_json=False``, display the figure immediately.
789
+ template : str, default "plotly_dark"
790
+ Plotly template name.
791
+ **kwargs
792
+ Additional arguments passed to
793
+ :meth:`FinBrainClient.corporate_lobbying.ticker`.
794
+
795
+ Returns
796
+ -------
797
+ plotly.graph_objects.Figure or str or None
798
+ Figure object, JSON string, or None (when shown).
799
+
800
+ Raises
801
+ ------
802
+ ValueError
803
+ If ``price_data`` is empty or missing required price column.
804
+ """
805
+ # Validate price_data
806
+ if price_data.empty:
807
+ raise ValueError("price_data cannot be empty")
808
+
809
+ # Flatten MultiIndex columns if present (e.g., from yf.download())
810
+ if isinstance(price_data.columns, pd.MultiIndex):
811
+ price_data = price_data.copy()
812
+ price_data.columns = price_data.columns.get_level_values(0)
813
+
814
+ # Find price column (case-insensitive search)
815
+ price_col = None
816
+ for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
817
+ if col in price_data.columns:
818
+ price_col = col
819
+ break
820
+ if price_col is None:
821
+ raise ValueError(
822
+ f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
823
+ f"Found columns: {price_data.columns.tolist()}"
824
+ )
825
+
826
+ # Fetch lobbying filings
827
+ filings_df = self._fb.corporate_lobbying.ticker(
828
+ ticker,
829
+ date_from=date_from,
830
+ date_to=date_to,
831
+ as_dataframe=True,
832
+ **kwargs,
833
+ )
834
+
835
+ # Normalize timezones
836
+ price_data_normalized = price_data.copy()
837
+ if price_data_normalized.index.tz is not None:
838
+ price_data_normalized.index = price_data_normalized.index.tz_localize(None)
839
+
840
+ fig = go.Figure(
841
+ layout=dict(
842
+ template=template,
843
+ title=f"Corporate Lobbying · {ticker}",
844
+ xaxis_title="Date",
845
+ hovermode="x unified",
846
+ )
847
+ )
848
+
849
+ # Plot price line on primary y-axis
850
+ fig.add_scatter(
851
+ name="Price",
852
+ x=price_data_normalized.index,
853
+ y=price_data_normalized[price_col],
854
+ mode="lines",
855
+ line=dict(width=2, color="#02d2ff"),
856
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>Price: $%{y:.2f}<extra></extra>",
857
+ )
858
+
859
+ if not filings_df.empty:
860
+ filings_normalized = filings_df.copy()
861
+ if filings_normalized.index.tz is not None:
862
+ filings_normalized.index = filings_normalized.index.tz_localize(None)
863
+
864
+ # Compute total spend per filing (income + expenses)
865
+ spend = filings_normalized.get("income", 0) + filings_normalized.get(
866
+ "expenses", 0
867
+ )
868
+
869
+ hover_text = []
870
+ for _, row in filings_normalized.iterrows():
871
+ registrant = row.get("registrantName", "N/A")
872
+ quarter = row.get("quarter", "")
873
+ income = row.get("income", 0)
874
+ expenses = row.get("expenses", 0)
875
+ hover_text.append(
876
+ f"Registrant: {registrant}<br>"
877
+ f"Quarter: {quarter}<br>"
878
+ f"Income: ${income:,.0f}<br>"
879
+ f"Expenses: ${expenses:,.0f}"
880
+ )
881
+
882
+ # Plot lobbying spend as bars on secondary y-axis
883
+ fig.add_bar(
884
+ name="Lobbying Spend",
885
+ x=filings_normalized.index,
886
+ y=spend,
887
+ marker_color="rgba(249,200,14,0.6)",
888
+ yaxis="y2",
889
+ hovertext=hover_text,
890
+ hovertemplate="<b>%{x|%Y-%m-%d}</b><br>%{hovertext}<extra></extra>",
891
+ )
892
+
893
+ fig.update_layout(
894
+ yaxis=dict(title="Price", showgrid=True),
895
+ yaxis2=dict(
896
+ title="Lobbying Spend ($)",
897
+ overlaying="y",
898
+ side="right",
899
+ showgrid=False,
900
+ zeroline=False,
901
+ rangemode="tozero",
902
+ ),
903
+ )
904
+
905
+ if show and not as_json:
906
+ fig.show()
907
+ return None
908
+ return fig.to_json() if as_json else fig
909
+
754
910
  # --------------------------------------------------------------------- #
755
911
  # Helper methods #
756
912
  # --------------------------------------------------------------------- #
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: finbrain-python
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Official Python client for the FinBrain API
5
5
  Author-email: Ahmet Salim Bilgin <ahmet@finbrain.tech>
6
6
  License-Expression: MIT
@@ -93,6 +93,12 @@ fb.senate_trades.ticker("META",
93
93
  date_to="2025-06-30",
94
94
  as_dataframe=True)
95
95
 
96
+ # ---------- corporate lobbying ----------
97
+ fb.corporate_lobbying.ticker("AAPL",
98
+ date_from="2024-01-01",
99
+ date_to="2025-06-30",
100
+ as_dataframe=True)
101
+
96
102
  # ---------- insider transactions ----------
97
103
  fb.insider_transactions.ticker("AMZN", as_dataframe=True)
98
104
 
@@ -205,7 +211,7 @@ fb.plot.sentiments("AMZN",
205
211
  date_from="2025-01-01",
206
212
  date_to="2025-06-30")
207
213
 
208
- # ---------- Insider Transactions, House & Senate Trades (requires user price data) ----------
214
+ # ---------- Insider Transactions, House & Senate Trades, Corporate Lobbying (requires user price data) ----------
209
215
  # These plots overlay transaction markers on a price chart.
210
216
  # Since FinBrain doesn't provide historical prices, you must provide your own:
211
217
 
@@ -232,6 +238,12 @@ fb.plot.senate_trades("META",
232
238
  price_data=price_df,
233
239
  date_from="2025-01-01",
234
240
  date_to="2025-06-30")
241
+
242
+ # Plot corporate lobbying spend on your price chart
243
+ fb.plot.corporate_lobbying("AAPL",
244
+ price_data=price_df,
245
+ date_from="2024-01-01",
246
+ date_to="2025-06-30")
235
247
  ```
236
248
 
237
249
  **Price Data Requirements:**
@@ -276,6 +288,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
276
288
  | Analyst ratings | `client.analyst_ratings.ticker()` | `/analyst-ratings/{SYMBOL}` |
277
289
  | House trades | `client.house_trades.ticker()` | `/congress/house/{SYMBOL}` |
278
290
  | Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
291
+ | Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
279
292
  | Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
280
293
  | LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
281
294
  | Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
@@ -21,6 +21,7 @@ src/finbrain/aio/endpoints/_utils.py
21
21
  src/finbrain/aio/endpoints/analyst_ratings.py
22
22
  src/finbrain/aio/endpoints/app_ratings.py
23
23
  src/finbrain/aio/endpoints/available.py
24
+ src/finbrain/aio/endpoints/corporate_lobbying.py
24
25
  src/finbrain/aio/endpoints/house_trades.py
25
26
  src/finbrain/aio/endpoints/insider_transactions.py
26
27
  src/finbrain/aio/endpoints/linkedin_data.py
@@ -36,6 +37,7 @@ src/finbrain/endpoints/_utils.py
36
37
  src/finbrain/endpoints/analyst_ratings.py
37
38
  src/finbrain/endpoints/app_ratings.py
38
39
  src/finbrain/endpoints/available.py
40
+ src/finbrain/endpoints/corporate_lobbying.py
39
41
  src/finbrain/endpoints/house_trades.py
40
42
  src/finbrain/endpoints/insider_transactions.py
41
43
  src/finbrain/endpoints/linkedin_data.py
@@ -57,6 +59,7 @@ tests/test_analyst_ratings.py
57
59
  tests/test_app_ratings.py
58
60
  tests/test_async_client.py
59
61
  tests/test_available.py
62
+ tests/test_corporate_lobbying.py
60
63
  tests/test_envelope.py
61
64
  tests/test_house_trades.py
62
65
  tests/test_insider_transactions.py
@@ -71,3 +71,4 @@ async def test_async_endpoints_exist():
71
71
  assert hasattr(client, "insider_transactions")
72
72
  assert hasattr(client, "linkedin_data")
73
73
  assert hasattr(client, "options")
74
+ assert hasattr(client, "corporate_lobbying")
@@ -0,0 +1,104 @@
1
+ # tests/test_corporate_lobbying.py
2
+ import pandas as pd
3
+ import pytest
4
+
5
+ from .conftest import stub_json, wrap_v2
6
+ from finbrain.exceptions import BadRequest
7
+
8
+ TICKER = "AAPL"
9
+
10
+
11
+ # ─────────── raw JSON branch ────────────────────────────────────────────
12
+ def test_corporate_lobbying_raw_ok(client, _activate_responses):
13
+ path = f"lobbying/{TICKER}"
14
+ payload = wrap_v2({
15
+ "symbol": "AAPL",
16
+ "name": "Apple Inc.",
17
+ "filings": [
18
+ {
19
+ "date": "2024-03-15",
20
+ "filingUuid": "abc-123",
21
+ "filingYear": 2024,
22
+ "quarter": "Q1",
23
+ "clientName": "Apple Inc.",
24
+ "registrantName": "Lobbying Firm LLC",
25
+ "income": 50000,
26
+ "expenses": 75000,
27
+ "issueCodes": ["TAX", "ITC"],
28
+ "governmentEntities": ["Congress", "Senate"],
29
+ }
30
+ ],
31
+ })
32
+
33
+ stub_json(_activate_responses, "GET", path, payload)
34
+
35
+ data = client.corporate_lobbying.ticker(symbol=TICKER)
36
+ assert data["symbol"] == TICKER
37
+ assert isinstance(data["filings"], list)
38
+ assert data["filings"][0]["registrantName"] == "Lobbying Firm LLC"
39
+
40
+
41
+ # ─────────── DataFrame branch ───────────────────────────────────────────
42
+ def test_corporate_lobbying_dataframe_ok(client, _activate_responses):
43
+ path = f"lobbying/{TICKER}"
44
+ payload = wrap_v2({
45
+ "symbol": "AAPL",
46
+ "name": "Apple Inc.",
47
+ "filings": [
48
+ {
49
+ "date": "2024-03-15",
50
+ "filingUuid": "abc-123",
51
+ "filingYear": 2024,
52
+ "quarter": "Q1",
53
+ "clientName": "Apple Inc.",
54
+ "registrantName": "Lobbying Firm LLC",
55
+ "income": 50000,
56
+ "expenses": 75000,
57
+ "issueCodes": ["TAX", "ITC"],
58
+ "governmentEntities": ["Congress", "Senate"],
59
+ },
60
+ {
61
+ "date": "2024-06-20",
62
+ "filingUuid": "def-456",
63
+ "filingYear": 2024,
64
+ "quarter": "Q2",
65
+ "clientName": "Apple Inc.",
66
+ "registrantName": "Another Firm Inc.",
67
+ "income": 30000,
68
+ "expenses": 45000,
69
+ "issueCodes": ["ENV"],
70
+ "governmentEntities": ["Congress"],
71
+ },
72
+ ],
73
+ })
74
+
75
+ stub_json(_activate_responses, "GET", path, payload)
76
+
77
+ df = client.corporate_lobbying.ticker(symbol=TICKER, as_dataframe=True)
78
+
79
+ assert isinstance(df, pd.DataFrame)
80
+ assert len(df) == 2
81
+ assert "registrantName" in df.columns
82
+ assert "income" in df.columns
83
+ assert "expenses" in df.columns
84
+ assert df.index.name == "date"
85
+ assert pd.api.types.is_datetime64_any_dtype(df.index)
86
+ assert pd.Timestamp("2024-03-15") in df.index
87
+ assert pd.Timestamp("2024-06-20") in df.index
88
+ assert df.loc["2024-03-15", "registrantName"] == "Lobbying Firm LLC"
89
+ assert df.loc["2024-06-20", "income"] == 30000
90
+
91
+
92
+ # ─────────── error mapping ──────────────────────────────────────────────
93
+ def test_corporate_lobbying_bad_request(client, _activate_responses):
94
+ path = f"lobbying/{TICKER}"
95
+ stub_json(
96
+ _activate_responses,
97
+ "GET",
98
+ path,
99
+ {"success": False, "error": {"code": "VALIDATION_ERROR", "message": "bad"}},
100
+ status=400,
101
+ )
102
+
103
+ with pytest.raises(BadRequest):
104
+ client.corporate_lobbying.ticker(symbol=TICKER)
@@ -261,6 +261,33 @@ class TestSenateTrades:
261
261
  assert "politician" in df.columns
262
262
 
263
263
 
264
+ # =====================================================================
265
+ # Corporate Lobbying
266
+ # =====================================================================
267
+ class TestCorporateLobbying:
268
+ def test_raw(self, fb):
269
+ data = fb.corporate_lobbying.ticker(TICKER)
270
+ assert isinstance(data, dict)
271
+ assert "filings" in data
272
+ filings = data["filings"]
273
+ assert isinstance(filings, list)
274
+ if len(filings) > 0:
275
+ assert "date" in filings[0]
276
+ assert "registrantName" in filings[0]
277
+ assert "income" in filings[0]
278
+ assert "expenses" in filings[0]
279
+
280
+ def test_dataframe(self, fb):
281
+ df = fb.corporate_lobbying.ticker(TICKER, as_dataframe=True)
282
+ assert isinstance(df, pd.DataFrame)
283
+ if len(df) > 0:
284
+ assert df.index.name == "date"
285
+ assert pd.api.types.is_datetime64_any_dtype(df.index)
286
+ assert "registrantName" in df.columns
287
+ assert "income" in df.columns
288
+ assert "expenses" in df.columns
289
+
290
+
264
291
  # =====================================================================
265
292
  # LinkedIn Data
266
293
  # =====================================================================
File without changes