finbrain-python 0.2.3__tar.gz → 0.2.4__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.
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/CHANGELOG.md +10 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/PKG-INFO +11 -1
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/README.md +10 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/client.py +2 -0
- finbrain_python-0.2.4/src/finbrain/aio/endpoints/government_contracts.py +48 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/screener.py +12 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/client.py +2 -0
- finbrain_python-0.2.4/src/finbrain/endpoints/government_contracts.py +73 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/screener.py +12 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/plotting.py +154 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/PKG-INFO +11 -1
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/SOURCES.txt +3 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_async_client.py +2 -0
- finbrain_python-0.2.4/tests/test_government_contracts.py +214 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_integration.py +41 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_plotting.py +45 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_screener.py +36 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/.gitattributes +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/.github/workflows/ci.yml +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/.github/workflows/release.yml +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/.gitignore +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/LICENSE +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/RELEASE.md +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/examples/async_example.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/examples/transactions_plotting_example.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/pyproject.toml +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/setup.cfg +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/__init__.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/__init__.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/_utils.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/available.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/news.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/options.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/recent.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/__init__.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/_utils.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/analyst_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/app_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/available.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/corporate_lobbying.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/house_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/insider_transactions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/linkedin_data.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/news.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/options.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/predictions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/recent.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/reddit_mentions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/senate_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/sentiments.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/exceptions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/py.typed +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/dependency_links.txt +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/requires.txt +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/top_level.txt +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/__init__.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/conftest.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_analyst_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_app_ratings.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_available.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_corporate_lobbying.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_envelope.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_house_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_insider_transactions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_linkedin_data.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_news.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_options.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_predictions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_recent.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_reddit_mentions.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_senate_trades.py +0 -0
- {finbrain_python-0.2.3 → finbrain_python-0.2.4}/tests/test_sentiments.py +0 -0
|
@@ -5,6 +5,16 @@ 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.4] - 2026-03-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Government Contracts Endpoint**: `fb.government_contracts.ticker("LMT")` — fetch U.S. government contract awards with award amounts, agencies, NAICS codes, and descriptions (`/government-contracts/{SYMBOL}`)
|
|
13
|
+
- **Government Contracts Screener**: `fb.screener.government_contracts()` — cross-ticker government contracts with summary stats (`/screener/government-contracts`)
|
|
14
|
+
- **Government Contracts Plotting**: `fb.plot.government_contracts()` — visualize contract award amounts as bars on a secondary y-axis overlaid on a price chart, with hover details showing agency, NAICS description, and contract description
|
|
15
|
+
- **Async Government Contracts**: Full async support via `AsyncGovernmentContractsAPI`
|
|
16
|
+
- **Government Contracts Tests**: Unit tests (`tests/test_government_contracts.py`), screener tests, integration tests, and plotting tests
|
|
17
|
+
|
|
8
18
|
## [0.2.3] - 2026-03-17
|
|
9
19
|
|
|
10
20
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: finbrain-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -105,6 +105,13 @@ fb.reddit_mentions.ticker("TSLA",
|
|
|
105
105
|
date_to="2026-03-17",
|
|
106
106
|
as_dataframe=True)
|
|
107
107
|
|
|
108
|
+
# ---------- government contracts ----------
|
|
109
|
+
fb.government_contracts.ticker("LMT",
|
|
110
|
+
date_from="2025-01-01",
|
|
111
|
+
date_to="2025-12-31",
|
|
112
|
+
limit=50,
|
|
113
|
+
as_dataframe=True)
|
|
114
|
+
|
|
108
115
|
# ---------- insider transactions ----------
|
|
109
116
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
110
117
|
|
|
@@ -137,6 +144,7 @@ fb.screener.sentiment(market="S&P 500", as_dataframe=True)
|
|
|
137
144
|
fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
138
145
|
fb.screener.insider_trading(limit=50)
|
|
139
146
|
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
147
|
+
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
140
148
|
|
|
141
149
|
# ---------- recent data ----------
|
|
142
150
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -312,6 +320,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
312
320
|
| Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
|
|
313
321
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
314
322
|
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
323
|
+
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
315
324
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
316
325
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
317
326
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
@@ -319,6 +328,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
319
328
|
| | `client.screener.predictions_daily()` | `/screener/predictions/daily` |
|
|
320
329
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
321
330
|
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
331
|
+
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
322
332
|
| | ... and 8 more screener methods | |
|
|
323
333
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
324
334
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -79,6 +79,13 @@ fb.reddit_mentions.ticker("TSLA",
|
|
|
79
79
|
date_to="2026-03-17",
|
|
80
80
|
as_dataframe=True)
|
|
81
81
|
|
|
82
|
+
# ---------- government contracts ----------
|
|
83
|
+
fb.government_contracts.ticker("LMT",
|
|
84
|
+
date_from="2025-01-01",
|
|
85
|
+
date_to="2025-12-31",
|
|
86
|
+
limit=50,
|
|
87
|
+
as_dataframe=True)
|
|
88
|
+
|
|
82
89
|
# ---------- insider transactions ----------
|
|
83
90
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
84
91
|
|
|
@@ -111,6 +118,7 @@ fb.screener.sentiment(market="S&P 500", as_dataframe=True)
|
|
|
111
118
|
fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
112
119
|
fb.screener.insider_trading(limit=50)
|
|
113
120
|
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
121
|
+
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
114
122
|
|
|
115
123
|
# ---------- recent data ----------
|
|
116
124
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -286,6 +294,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
286
294
|
| Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
|
|
287
295
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
288
296
|
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
297
|
+
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
289
298
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
290
299
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
291
300
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
@@ -293,6 +302,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
293
302
|
| | `client.screener.predictions_daily()` | `/screener/predictions/daily` |
|
|
294
303
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
295
304
|
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
305
|
+
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
296
306
|
| | ... and 8 more screener methods | |
|
|
297
307
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
298
308
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -24,6 +24,7 @@ from .endpoints.screener import AsyncScreenerAPI
|
|
|
24
24
|
from .endpoints.recent import AsyncRecentAPI
|
|
25
25
|
from .endpoints.corporate_lobbying import AsyncCorporateLobbyingAPI
|
|
26
26
|
from .endpoints.reddit_mentions import AsyncRedditMentionsAPI
|
|
27
|
+
from .endpoints.government_contracts import AsyncGovernmentContractsAPI
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
# Which status codes merit a retry
|
|
@@ -84,6 +85,7 @@ class AsyncFinBrainClient:
|
|
|
84
85
|
self.recent = AsyncRecentAPI(self)
|
|
85
86
|
self.corporate_lobbying = AsyncCorporateLobbyingAPI(self)
|
|
86
87
|
self.reddit_mentions = AsyncRedditMentionsAPI(self)
|
|
88
|
+
self.government_contracts = AsyncGovernmentContractsAPI(self)
|
|
87
89
|
|
|
88
90
|
async def __aenter__(self) -> "AsyncFinBrainClient":
|
|
89
91
|
"""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, List
|
|
5
|
+
|
|
6
|
+
from ._utils import to_datestr
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from ..client import AsyncFinBrainClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AsyncGovernmentContractsAPI:
|
|
13
|
+
"""Async wrapper for /government-contracts and /screener/government-contracts 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 government contract awards 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"government-contracts/{symbol.upper()}"
|
|
37
|
+
|
|
38
|
+
data: Dict[str, Any] = await self._c._request("GET", path, params=params)
|
|
39
|
+
|
|
40
|
+
if as_dataframe:
|
|
41
|
+
rows: List[Dict[str, Any]] = data.get("contracts", [])
|
|
42
|
+
df = pd.DataFrame(rows)
|
|
43
|
+
if not df.empty and "startDate" in df.columns:
|
|
44
|
+
df["startDate"] = pd.to_datetime(df["startDate"])
|
|
45
|
+
df.set_index("startDate", inplace=True)
|
|
46
|
+
return df
|
|
47
|
+
|
|
48
|
+
return data
|
|
@@ -222,3 +222,15 @@ class AsyncScreenerAPI:
|
|
|
222
222
|
"""Screen Reddit mention counts across tickers (async)."""
|
|
223
223
|
params = self._build_params(limit=limit, market=market, region=region)
|
|
224
224
|
return await self._get("screener/reddit-mentions", params, as_dataframe)
|
|
225
|
+
|
|
226
|
+
# ── government contracts ──────────────────────────────────
|
|
227
|
+
|
|
228
|
+
async def government_contracts(
|
|
229
|
+
self,
|
|
230
|
+
*,
|
|
231
|
+
limit: int | None = None,
|
|
232
|
+
as_dataframe: bool = False,
|
|
233
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
234
|
+
"""Screen government contracts across all tickers (async)."""
|
|
235
|
+
params = self._build_params(limit=limit)
|
|
236
|
+
return await self._get("screener/government-contracts", params, as_dataframe)
|
|
@@ -25,6 +25,7 @@ from .endpoints.screener import ScreenerAPI
|
|
|
25
25
|
from .endpoints.recent import RecentAPI
|
|
26
26
|
from .endpoints.corporate_lobbying import CorporateLobbyingAPI
|
|
27
27
|
from .endpoints.reddit_mentions import RedditMentionsAPI
|
|
28
|
+
from .endpoints.government_contracts import GovernmentContractsAPI
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
# Which status codes merit a retry
|
|
@@ -79,6 +80,7 @@ class FinBrainClient:
|
|
|
79
80
|
self.recent = RecentAPI(self)
|
|
80
81
|
self.corporate_lobbying = CorporateLobbyingAPI(self)
|
|
81
82
|
self.reddit_mentions = RedditMentionsAPI(self)
|
|
83
|
+
self.government_contracts = GovernmentContractsAPI(self)
|
|
82
84
|
|
|
83
85
|
# ---------- private helpers ----------
|
|
84
86
|
def _request(
|
|
@@ -0,0 +1,73 @@
|
|
|
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 GovernmentContractsAPI:
|
|
13
|
+
"""
|
|
14
|
+
Endpoints
|
|
15
|
+
---------
|
|
16
|
+
``/government-contracts/<TICKER>`` - U.S. government contract awards.
|
|
17
|
+
``/screener/government-contracts`` - cross-ticker government contracts screener.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# ------------------------------------------------------------------ #
|
|
21
|
+
def __init__(self, client: "FinBrainClient") -> None:
|
|
22
|
+
self._c = client # reference to the parent client
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ #
|
|
25
|
+
def ticker(
|
|
26
|
+
self,
|
|
27
|
+
symbol: str,
|
|
28
|
+
*,
|
|
29
|
+
date_from: _dt.date | str | None = None,
|
|
30
|
+
date_to: _dt.date | str | None = None,
|
|
31
|
+
limit: int | None = None,
|
|
32
|
+
as_dataframe: bool = False,
|
|
33
|
+
) -> Dict[str, Any] | pd.DataFrame:
|
|
34
|
+
"""
|
|
35
|
+
Fetch government contract awards for *symbol*.
|
|
36
|
+
|
|
37
|
+
Parameters
|
|
38
|
+
----------
|
|
39
|
+
symbol :
|
|
40
|
+
Ticker symbol; auto-upper-cased.
|
|
41
|
+
date_from, date_to :
|
|
42
|
+
Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows.
|
|
43
|
+
limit :
|
|
44
|
+
Maximum number of records to return (1-500).
|
|
45
|
+
as_dataframe :
|
|
46
|
+
If *True*, return a **pandas.DataFrame** indexed by ``startDate``;
|
|
47
|
+
otherwise return the raw JSON dict.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
dict | pandas.DataFrame
|
|
52
|
+
"""
|
|
53
|
+
params: Dict[str, str] = {}
|
|
54
|
+
if date_from:
|
|
55
|
+
params["startDate"] = to_datestr(date_from)
|
|
56
|
+
if date_to:
|
|
57
|
+
params["endDate"] = to_datestr(date_to)
|
|
58
|
+
if limit is not None:
|
|
59
|
+
params["limit"] = str(limit)
|
|
60
|
+
|
|
61
|
+
path = f"government-contracts/{symbol.upper()}"
|
|
62
|
+
|
|
63
|
+
data: Dict[str, Any] = self._c._request("GET", path, params=params)
|
|
64
|
+
|
|
65
|
+
if as_dataframe:
|
|
66
|
+
rows: List[Dict[str, Any]] = data.get("contracts", [])
|
|
67
|
+
df = pd.DataFrame(rows)
|
|
68
|
+
if not df.empty and "startDate" in df.columns:
|
|
69
|
+
df["startDate"] = pd.to_datetime(df["startDate"])
|
|
70
|
+
df.set_index("startDate", inplace=True)
|
|
71
|
+
return df
|
|
72
|
+
|
|
73
|
+
return data
|
|
@@ -227,3 +227,15 @@ class ScreenerAPI:
|
|
|
227
227
|
"""Screen Reddit mention counts across tickers."""
|
|
228
228
|
params = self._build_params(limit=limit, market=market, region=region)
|
|
229
229
|
return self._get("screener/reddit-mentions", params, as_dataframe)
|
|
230
|
+
|
|
231
|
+
# ── government contracts ──────────────────────────────────
|
|
232
|
+
|
|
233
|
+
def government_contracts(
|
|
234
|
+
self,
|
|
235
|
+
*,
|
|
236
|
+
limit: int | None = None,
|
|
237
|
+
as_dataframe: bool = False,
|
|
238
|
+
) -> List[Dict[str, Any]] | pd.DataFrame:
|
|
239
|
+
"""Screen government contracts across all tickers."""
|
|
240
|
+
params = self._build_params(limit=limit)
|
|
241
|
+
return self._get("screener/government-contracts", params, as_dataframe)
|
|
@@ -1154,6 +1154,160 @@ class _PlotNamespace:
|
|
|
1154
1154
|
return None
|
|
1155
1155
|
return fig.to_json() if as_json else fig
|
|
1156
1156
|
|
|
1157
|
+
# --------------------------------------------------------------------- #
|
|
1158
|
+
# Government Contracts → bars on price chart #
|
|
1159
|
+
# --------------------------------------------------------------------- #
|
|
1160
|
+
def government_contracts(
|
|
1161
|
+
self,
|
|
1162
|
+
ticker: str,
|
|
1163
|
+
price_data: pd.DataFrame,
|
|
1164
|
+
*,
|
|
1165
|
+
date_from: str | None = None,
|
|
1166
|
+
date_to: str | None = None,
|
|
1167
|
+
as_json: bool = False,
|
|
1168
|
+
show: bool = True,
|
|
1169
|
+
template: str = "plotly_dark",
|
|
1170
|
+
**kwargs,
|
|
1171
|
+
):
|
|
1172
|
+
"""
|
|
1173
|
+
Plot government contract awards overlaid on a price chart.
|
|
1174
|
+
|
|
1175
|
+
This method requires user-provided historical price data, as FinBrain
|
|
1176
|
+
does not currently offer a price history endpoint.
|
|
1177
|
+
|
|
1178
|
+
Parameters
|
|
1179
|
+
----------
|
|
1180
|
+
ticker : str
|
|
1181
|
+
Ticker symbol (e.g. ``"LMT"``).
|
|
1182
|
+
price_data : pandas.DataFrame
|
|
1183
|
+
**User-provided** price history with a DatetimeIndex and a column
|
|
1184
|
+
containing prices (e.g. ``"close"``, ``"Close"``, or ``"price"``).
|
|
1185
|
+
The index must be timezone-naive or UTC.
|
|
1186
|
+
date_from, date_to : str or None, optional
|
|
1187
|
+
Date range for contracts in ``YYYY-MM-DD`` format.
|
|
1188
|
+
as_json : bool, default False
|
|
1189
|
+
If ``True``, return JSON string instead of Figure object.
|
|
1190
|
+
show : bool, default True
|
|
1191
|
+
If ``True`` and ``as_json=False``, display the figure immediately.
|
|
1192
|
+
template : str, default "plotly_dark"
|
|
1193
|
+
Plotly template name.
|
|
1194
|
+
**kwargs
|
|
1195
|
+
Additional arguments passed to
|
|
1196
|
+
:meth:`FinBrainClient.government_contracts.ticker`.
|
|
1197
|
+
|
|
1198
|
+
Returns
|
|
1199
|
+
-------
|
|
1200
|
+
plotly.graph_objects.Figure or str or None
|
|
1201
|
+
Figure object, JSON string, or None (when shown).
|
|
1202
|
+
|
|
1203
|
+
Raises
|
|
1204
|
+
------
|
|
1205
|
+
ValueError
|
|
1206
|
+
If ``price_data`` is empty or missing required price column.
|
|
1207
|
+
"""
|
|
1208
|
+
# Validate price_data
|
|
1209
|
+
if price_data.empty:
|
|
1210
|
+
raise ValueError("price_data cannot be empty")
|
|
1211
|
+
|
|
1212
|
+
# Flatten MultiIndex columns if present (e.g., from yf.download())
|
|
1213
|
+
if isinstance(price_data.columns, pd.MultiIndex):
|
|
1214
|
+
price_data = price_data.copy()
|
|
1215
|
+
price_data.columns = price_data.columns.get_level_values(0)
|
|
1216
|
+
|
|
1217
|
+
# Find price column (case-insensitive search)
|
|
1218
|
+
price_col = None
|
|
1219
|
+
for col in ["close", "Close", "price", "Price", "adj_close", "Adj Close"]:
|
|
1220
|
+
if col in price_data.columns:
|
|
1221
|
+
price_col = col
|
|
1222
|
+
break
|
|
1223
|
+
if price_col is None:
|
|
1224
|
+
raise ValueError(
|
|
1225
|
+
f"price_data must contain a price column (e.g. 'close', 'Close', 'price'). "
|
|
1226
|
+
f"Found columns: {price_data.columns.tolist()}"
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
# Fetch government contracts
|
|
1230
|
+
contracts_df = self._fb.government_contracts.ticker(
|
|
1231
|
+
ticker,
|
|
1232
|
+
date_from=date_from,
|
|
1233
|
+
date_to=date_to,
|
|
1234
|
+
as_dataframe=True,
|
|
1235
|
+
**kwargs,
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
# Normalize timezones
|
|
1239
|
+
price_data_normalized = price_data.copy()
|
|
1240
|
+
if price_data_normalized.index.tz is not None:
|
|
1241
|
+
price_data_normalized.index = price_data_normalized.index.tz_localize(None)
|
|
1242
|
+
|
|
1243
|
+
fig = go.Figure(
|
|
1244
|
+
layout=dict(
|
|
1245
|
+
template=template,
|
|
1246
|
+
title=f"Government Contracts · {ticker}",
|
|
1247
|
+
xaxis_title="Date",
|
|
1248
|
+
hovermode="x unified",
|
|
1249
|
+
)
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
# Plot price line on primary y-axis
|
|
1253
|
+
fig.add_scatter(
|
|
1254
|
+
name="Price",
|
|
1255
|
+
x=price_data_normalized.index,
|
|
1256
|
+
y=price_data_normalized[price_col],
|
|
1257
|
+
mode="lines",
|
|
1258
|
+
line=dict(width=2, color="#02d2ff"),
|
|
1259
|
+
hovertemplate="<b>%{x|%Y-%m-%d}</b><br>Price: $%{y:.2f}<extra></extra>",
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
if not contracts_df.empty:
|
|
1263
|
+
contracts_normalized = contracts_df.copy()
|
|
1264
|
+
if contracts_normalized.index.tz is not None:
|
|
1265
|
+
contracts_normalized.index = contracts_normalized.index.tz_localize(None)
|
|
1266
|
+
|
|
1267
|
+
amounts = contracts_normalized.get("awardAmount", pd.Series(dtype=float))
|
|
1268
|
+
|
|
1269
|
+
hover_text = []
|
|
1270
|
+
for _, row in contracts_normalized.iterrows():
|
|
1271
|
+
agency = row.get("awardingAgency", "N/A")
|
|
1272
|
+
desc = row.get("description", "")
|
|
1273
|
+
if len(str(desc)) > 80:
|
|
1274
|
+
desc = str(desc)[:80] + "…"
|
|
1275
|
+
naics = row.get("naicsDescription", "")
|
|
1276
|
+
amount = row.get("awardAmount", 0)
|
|
1277
|
+
hover_text.append(
|
|
1278
|
+
f"Agency: {agency}<br>"
|
|
1279
|
+
f"Amount: ${amount:,.0f}<br>"
|
|
1280
|
+
f"NAICS: {naics}<br>"
|
|
1281
|
+
f"Desc: {desc}"
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
fig.add_bar(
|
|
1285
|
+
name="Contract Award",
|
|
1286
|
+
x=contracts_normalized.index,
|
|
1287
|
+
y=amounts,
|
|
1288
|
+
marker_color="rgba(249,200,14,0.6)",
|
|
1289
|
+
yaxis="y2",
|
|
1290
|
+
hovertext=hover_text,
|
|
1291
|
+
hovertemplate="<b>%{x|%Y-%m-%d}</b><br>%{hovertext}<extra></extra>",
|
|
1292
|
+
)
|
|
1293
|
+
|
|
1294
|
+
fig.update_layout(
|
|
1295
|
+
yaxis=dict(title="Price", showgrid=True),
|
|
1296
|
+
yaxis2=dict(
|
|
1297
|
+
title="Award Amount ($)",
|
|
1298
|
+
overlaying="y",
|
|
1299
|
+
side="right",
|
|
1300
|
+
showgrid=False,
|
|
1301
|
+
zeroline=False,
|
|
1302
|
+
rangemode="tozero",
|
|
1303
|
+
),
|
|
1304
|
+
)
|
|
1305
|
+
|
|
1306
|
+
if show and not as_json:
|
|
1307
|
+
fig.show()
|
|
1308
|
+
return None
|
|
1309
|
+
return fig.to_json() if as_json else fig
|
|
1310
|
+
|
|
1157
1311
|
# --------------------------------------------------------------------- #
|
|
1158
1312
|
# Helper methods #
|
|
1159
1313
|
# --------------------------------------------------------------------- #
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: finbrain-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
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
|
|
@@ -105,6 +105,13 @@ fb.reddit_mentions.ticker("TSLA",
|
|
|
105
105
|
date_to="2026-03-17",
|
|
106
106
|
as_dataframe=True)
|
|
107
107
|
|
|
108
|
+
# ---------- government contracts ----------
|
|
109
|
+
fb.government_contracts.ticker("LMT",
|
|
110
|
+
date_from="2025-01-01",
|
|
111
|
+
date_to="2025-12-31",
|
|
112
|
+
limit=50,
|
|
113
|
+
as_dataframe=True)
|
|
114
|
+
|
|
108
115
|
# ---------- insider transactions ----------
|
|
109
116
|
fb.insider_transactions.ticker("AMZN", as_dataframe=True)
|
|
110
117
|
|
|
@@ -137,6 +144,7 @@ fb.screener.sentiment(market="S&P 500", as_dataframe=True)
|
|
|
137
144
|
fb.screener.predictions_daily(limit=100, as_dataframe=True)
|
|
138
145
|
fb.screener.insider_trading(limit=50)
|
|
139
146
|
fb.screener.reddit_mentions(limit=100, as_dataframe=True)
|
|
147
|
+
fb.screener.government_contracts(limit=100, as_dataframe=True)
|
|
140
148
|
|
|
141
149
|
# ---------- recent data ----------
|
|
142
150
|
fb.recent.news(limit=100, as_dataframe=True)
|
|
@@ -312,6 +320,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
312
320
|
| Senate trades | `client.senate_trades.ticker()` | `/congress/senate/{SYMBOL}` |
|
|
313
321
|
| Corporate lobbying | `client.corporate_lobbying.ticker()` | `/lobbying/{SYMBOL}` |
|
|
314
322
|
| Reddit mentions | `client.reddit_mentions.ticker()` | `/reddit-mentions/{SYMBOL}` |
|
|
323
|
+
| Gov. contracts | `client.government_contracts.ticker()` | `/government-contracts/{SYMBOL}` |
|
|
315
324
|
| Insider transactions | `client.insider_transactions.ticker()` | `/insider-trading/{SYMBOL}` |
|
|
316
325
|
| LinkedIn | `client.linkedin_data.ticker()` | `/linkedin/{SYMBOL}` |
|
|
317
326
|
| Options – Put/Call | `client.options.put_call()` | `/put-call-ratio/{SYMBOL}` |
|
|
@@ -319,6 +328,7 @@ fb = FinBrainClient() # reads from FINBRAIN_API_KEY env var
|
|
|
319
328
|
| | `client.screener.predictions_daily()` | `/screener/predictions/daily` |
|
|
320
329
|
| | `client.screener.insider_trading()` | `/screener/insider-trading` |
|
|
321
330
|
| | `client.screener.reddit_mentions()` | `/screener/reddit-mentions` |
|
|
331
|
+
| | `client.screener.government_contracts()` | `/screener/government-contracts` |
|
|
322
332
|
| | ... and 8 more screener methods | |
|
|
323
333
|
| Recent | `client.recent.news()` | `/recent/news` |
|
|
324
334
|
| | `client.recent.analyst_ratings()` | `/recent/analyst-ratings` |
|
|
@@ -22,6 +22,7 @@ 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
24
|
src/finbrain/aio/endpoints/corporate_lobbying.py
|
|
25
|
+
src/finbrain/aio/endpoints/government_contracts.py
|
|
25
26
|
src/finbrain/aio/endpoints/house_trades.py
|
|
26
27
|
src/finbrain/aio/endpoints/insider_transactions.py
|
|
27
28
|
src/finbrain/aio/endpoints/linkedin_data.py
|
|
@@ -39,6 +40,7 @@ src/finbrain/endpoints/analyst_ratings.py
|
|
|
39
40
|
src/finbrain/endpoints/app_ratings.py
|
|
40
41
|
src/finbrain/endpoints/available.py
|
|
41
42
|
src/finbrain/endpoints/corporate_lobbying.py
|
|
43
|
+
src/finbrain/endpoints/government_contracts.py
|
|
42
44
|
src/finbrain/endpoints/house_trades.py
|
|
43
45
|
src/finbrain/endpoints/insider_transactions.py
|
|
44
46
|
src/finbrain/endpoints/linkedin_data.py
|
|
@@ -63,6 +65,7 @@ tests/test_async_client.py
|
|
|
63
65
|
tests/test_available.py
|
|
64
66
|
tests/test_corporate_lobbying.py
|
|
65
67
|
tests/test_envelope.py
|
|
68
|
+
tests/test_government_contracts.py
|
|
66
69
|
tests/test_house_trades.py
|
|
67
70
|
tests/test_insider_transactions.py
|
|
68
71
|
tests/test_integration.py
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# tests/test_government_contracts.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 = "LMT"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ─────────── raw JSON branch ────────────────────────────────────────────
|
|
12
|
+
def test_government_contracts_raw_ok(client, _activate_responses):
|
|
13
|
+
path = f"government-contracts/{TICKER}"
|
|
14
|
+
payload = wrap_v2({
|
|
15
|
+
"symbol": "LMT",
|
|
16
|
+
"name": "Lockheed Martin Corporation",
|
|
17
|
+
"contracts": [
|
|
18
|
+
{
|
|
19
|
+
"awardId": "CONT_AWD_0001",
|
|
20
|
+
"awardAmount": 50000000,
|
|
21
|
+
"awardType": "",
|
|
22
|
+
"awardingAgency": "Department of Defense",
|
|
23
|
+
"awardingSubAgency": "Department of the Army",
|
|
24
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
25
|
+
"startDate": "2025-06-01",
|
|
26
|
+
"endDate": "2026-06-01",
|
|
27
|
+
"description": "Aircraft maintenance services",
|
|
28
|
+
"naicsCode": "336411",
|
|
29
|
+
"naicsDescription": "Aircraft Manufacturing",
|
|
30
|
+
"contractAwardType": "",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"awardId": "CONT_AWD_0002",
|
|
34
|
+
"awardAmount": 12000000,
|
|
35
|
+
"awardType": "",
|
|
36
|
+
"awardingAgency": "NASA",
|
|
37
|
+
"awardingSubAgency": "NASA Headquarters",
|
|
38
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
39
|
+
"startDate": "2025-03-15",
|
|
40
|
+
"endDate": "2027-03-15",
|
|
41
|
+
"description": "Space systems development",
|
|
42
|
+
"naicsCode": "336414",
|
|
43
|
+
"naicsDescription": "Guided Missile and Space Vehicle Manufacturing",
|
|
44
|
+
"contractAwardType": "",
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
stub_json(_activate_responses, "GET", path, payload)
|
|
50
|
+
|
|
51
|
+
data = client.government_contracts.ticker(symbol=TICKER)
|
|
52
|
+
assert data["symbol"] == TICKER
|
|
53
|
+
assert isinstance(data["contracts"], list)
|
|
54
|
+
assert len(data["contracts"]) == 2
|
|
55
|
+
assert data["contracts"][0]["awardingAgency"] == "Department of Defense"
|
|
56
|
+
assert data["contracts"][0]["awardAmount"] == 50000000
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ─────────── DataFrame branch ───────────────────────────────────────────
|
|
60
|
+
def test_government_contracts_dataframe_ok(client, _activate_responses):
|
|
61
|
+
path = f"government-contracts/{TICKER}"
|
|
62
|
+
payload = wrap_v2({
|
|
63
|
+
"symbol": "LMT",
|
|
64
|
+
"name": "Lockheed Martin Corporation",
|
|
65
|
+
"contracts": [
|
|
66
|
+
{
|
|
67
|
+
"awardId": "CONT_AWD_0001",
|
|
68
|
+
"awardAmount": 50000000,
|
|
69
|
+
"awardingAgency": "Department of Defense",
|
|
70
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
71
|
+
"startDate": "2025-06-01",
|
|
72
|
+
"endDate": "2026-06-01",
|
|
73
|
+
"description": "Aircraft maintenance services",
|
|
74
|
+
"naicsCode": "336411",
|
|
75
|
+
"naicsDescription": "Aircraft Manufacturing",
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"awardId": "CONT_AWD_0002",
|
|
79
|
+
"awardAmount": 12000000,
|
|
80
|
+
"awardingAgency": "NASA",
|
|
81
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
82
|
+
"startDate": "2025-03-15",
|
|
83
|
+
"endDate": "2027-03-15",
|
|
84
|
+
"description": "Space systems development",
|
|
85
|
+
"naicsCode": "336414",
|
|
86
|
+
"naicsDescription": "Guided Missile and Space Vehicle Manufacturing",
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
stub_json(_activate_responses, "GET", path, payload)
|
|
92
|
+
|
|
93
|
+
df = client.government_contracts.ticker(symbol=TICKER, as_dataframe=True)
|
|
94
|
+
|
|
95
|
+
assert isinstance(df, pd.DataFrame)
|
|
96
|
+
assert len(df) == 2
|
|
97
|
+
assert "awardAmount" in df.columns
|
|
98
|
+
assert "awardingAgency" in df.columns
|
|
99
|
+
assert df.index.name == "startDate"
|
|
100
|
+
assert pd.api.types.is_datetime64_any_dtype(df.index)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ─────────── with query params ──────────────────────────────────────────
|
|
104
|
+
def test_government_contracts_with_params(client, _activate_responses):
|
|
105
|
+
path = f"government-contracts/{TICKER}"
|
|
106
|
+
payload = wrap_v2({
|
|
107
|
+
"symbol": "LMT",
|
|
108
|
+
"name": "Lockheed Martin Corporation",
|
|
109
|
+
"contracts": [],
|
|
110
|
+
})
|
|
111
|
+
stub_json(
|
|
112
|
+
_activate_responses,
|
|
113
|
+
"GET",
|
|
114
|
+
path,
|
|
115
|
+
payload,
|
|
116
|
+
params={"startDate": "2025-01-01", "endDate": "2025-12-31", "limit": "10"},
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
data = client.government_contracts.ticker(
|
|
120
|
+
symbol=TICKER,
|
|
121
|
+
date_from="2025-01-01",
|
|
122
|
+
date_to="2025-12-31",
|
|
123
|
+
limit=10,
|
|
124
|
+
)
|
|
125
|
+
assert data["symbol"] == TICKER
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ─────────── error mapping ──────────────────────────────────────────────
|
|
129
|
+
def test_government_contracts_bad_request(client, _activate_responses):
|
|
130
|
+
path = f"government-contracts/{TICKER}"
|
|
131
|
+
stub_json(
|
|
132
|
+
_activate_responses,
|
|
133
|
+
"GET",
|
|
134
|
+
path,
|
|
135
|
+
{"success": False, "error": {"code": "VALIDATION_ERROR", "message": "bad"}},
|
|
136
|
+
status=400,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
with pytest.raises(BadRequest):
|
|
140
|
+
client.government_contracts.ticker(symbol=TICKER)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ─────────── screener raw JSON ──────────────────────────────────────────
|
|
144
|
+
def test_government_contracts_screener_raw_ok(client, _activate_responses):
|
|
145
|
+
path = "screener/government-contracts"
|
|
146
|
+
payload = wrap_v2({
|
|
147
|
+
"data": [
|
|
148
|
+
{
|
|
149
|
+
"symbol": "LMT",
|
|
150
|
+
"name": "Lockheed Martin Corporation",
|
|
151
|
+
"awardId": "CONT_AWD_0001",
|
|
152
|
+
"awardAmount": 50000000,
|
|
153
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
154
|
+
"startDate": "2025-06-01",
|
|
155
|
+
"awardingAgency": "Department of Defense",
|
|
156
|
+
"naicsDescription": "Aircraft Manufacturing",
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
"summary": {
|
|
160
|
+
"totalContracts": 1,
|
|
161
|
+
"totalTickers": 1,
|
|
162
|
+
"totalValue": 50000000,
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
stub_json(_activate_responses, "GET", path, payload)
|
|
167
|
+
|
|
168
|
+
data = client.screener.government_contracts()
|
|
169
|
+
assert isinstance(data, list)
|
|
170
|
+
assert data[0]["symbol"] == "LMT"
|
|
171
|
+
assert data[0]["awardAmount"] == 50000000
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ─────────── screener DataFrame ─────────────────────────────────────────
|
|
175
|
+
def test_government_contracts_screener_dataframe_ok(client, _activate_responses):
|
|
176
|
+
path = "screener/government-contracts"
|
|
177
|
+
payload = wrap_v2({
|
|
178
|
+
"data": [
|
|
179
|
+
{
|
|
180
|
+
"symbol": "LMT",
|
|
181
|
+
"name": "Lockheed Martin Corporation",
|
|
182
|
+
"awardId": "CONT_AWD_0001",
|
|
183
|
+
"awardAmount": 50000000,
|
|
184
|
+
"recipientName": "Lockheed Martin Corporation",
|
|
185
|
+
"startDate": "2025-06-01",
|
|
186
|
+
"awardingAgency": "Department of Defense",
|
|
187
|
+
"naicsDescription": "Aircraft Manufacturing",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"symbol": "BA",
|
|
191
|
+
"name": "The Boeing Company",
|
|
192
|
+
"awardId": "CONT_AWD_0003",
|
|
193
|
+
"awardAmount": 80000000,
|
|
194
|
+
"recipientName": "The Boeing Company",
|
|
195
|
+
"startDate": "2025-05-20",
|
|
196
|
+
"awardingAgency": "Department of Defense",
|
|
197
|
+
"naicsDescription": "Aircraft Manufacturing",
|
|
198
|
+
},
|
|
199
|
+
],
|
|
200
|
+
"summary": {
|
|
201
|
+
"totalContracts": 2,
|
|
202
|
+
"totalTickers": 2,
|
|
203
|
+
"totalValue": 130000000,
|
|
204
|
+
},
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
stub_json(_activate_responses, "GET", path, payload)
|
|
208
|
+
|
|
209
|
+
df = client.screener.government_contracts(as_dataframe=True)
|
|
210
|
+
|
|
211
|
+
assert isinstance(df, pd.DataFrame)
|
|
212
|
+
assert len(df) == 2
|
|
213
|
+
assert df.index.name == "symbol"
|
|
214
|
+
assert "awardAmount" in df.columns
|
|
@@ -333,6 +333,47 @@ class TestRedditMentions:
|
|
|
333
333
|
assert len(df) > 0
|
|
334
334
|
|
|
335
335
|
|
|
336
|
+
# =====================================================================
|
|
337
|
+
# Government Contracts
|
|
338
|
+
# =====================================================================
|
|
339
|
+
class TestGovernmentContracts:
|
|
340
|
+
def test_raw(self, fb):
|
|
341
|
+
data = fb.government_contracts.ticker("LMT")
|
|
342
|
+
assert isinstance(data, dict)
|
|
343
|
+
assert "contracts" in data
|
|
344
|
+
contracts = data["contracts"]
|
|
345
|
+
assert isinstance(contracts, list)
|
|
346
|
+
if len(contracts) > 0:
|
|
347
|
+
assert "startDate" in contracts[0]
|
|
348
|
+
assert "awardAmount" in contracts[0]
|
|
349
|
+
assert "awardingAgency" in contracts[0]
|
|
350
|
+
assert "recipientName" in contracts[0]
|
|
351
|
+
|
|
352
|
+
def test_dataframe(self, fb):
|
|
353
|
+
df = fb.government_contracts.ticker("LMT", as_dataframe=True)
|
|
354
|
+
assert isinstance(df, pd.DataFrame)
|
|
355
|
+
if len(df) > 0:
|
|
356
|
+
assert df.index.name == "startDate"
|
|
357
|
+
assert pd.api.types.is_datetime64_any_dtype(df.index)
|
|
358
|
+
assert "awardAmount" in df.columns
|
|
359
|
+
assert "awardingAgency" in df.columns
|
|
360
|
+
|
|
361
|
+
def test_screener_raw(self, fb):
|
|
362
|
+
data = fb.screener.government_contracts()
|
|
363
|
+
assert isinstance(data, list)
|
|
364
|
+
if len(data) > 0:
|
|
365
|
+
row = data[0]
|
|
366
|
+
assert "symbol" in row
|
|
367
|
+
assert "awardAmount" in row
|
|
368
|
+
assert "awardingAgency" in row
|
|
369
|
+
|
|
370
|
+
def test_screener_dataframe(self, fb):
|
|
371
|
+
df = fb.screener.government_contracts(as_dataframe=True)
|
|
372
|
+
assert isinstance(df, pd.DataFrame)
|
|
373
|
+
if len(df) > 0:
|
|
374
|
+
assert df.index.name == "symbol"
|
|
375
|
+
|
|
376
|
+
|
|
336
377
|
# =====================================================================
|
|
337
378
|
# LinkedIn Data
|
|
338
379
|
# =====================================================================
|
|
@@ -547,3 +547,48 @@ def test_reddit_mentions_top_latest_snapshot():
|
|
|
547
547
|
|
|
548
548
|
# Should use the 08:00 snapshot (120 mentions), not 04:00 (50)
|
|
549
549
|
assert fig.data[0].x == (85,) # wsb=85 from the latest snapshot
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
# ── government_contracts ──────────────────────────────────────────────
|
|
553
|
+
|
|
554
|
+
class MockGovernmentContractsClient:
|
|
555
|
+
class government_contracts:
|
|
556
|
+
@staticmethod
|
|
557
|
+
def ticker(*args, **kwargs):
|
|
558
|
+
df = pd.DataFrame({
|
|
559
|
+
"awardAmount": [50000000, 12000000],
|
|
560
|
+
"awardingAgency": ["Department of Defense", "NASA"],
|
|
561
|
+
"description": ["Aircraft maintenance", "Space systems"],
|
|
562
|
+
"naicsDescription": ["Aircraft Manufacturing", "Guided Missile Mfg"],
|
|
563
|
+
"startDate": pd.to_datetime(["2024-01-15", "2024-03-20"]),
|
|
564
|
+
}).set_index("startDate")
|
|
565
|
+
return df
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def test_government_contracts_plot():
|
|
569
|
+
plot = _PlotNamespace(MockGovernmentContractsClient())
|
|
570
|
+
fig = plot.government_contracts("LMT", price_data=PRICE_DF, show=False)
|
|
571
|
+
|
|
572
|
+
assert isinstance(fig, go.Figure)
|
|
573
|
+
# price line + contract award bars
|
|
574
|
+
assert len(fig.data) >= 2
|
|
575
|
+
assert "Government Contracts" in fig.layout.title.text
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def test_government_contracts_empty_price_data():
|
|
579
|
+
plot = _PlotNamespace(MockClient())
|
|
580
|
+
with pytest.raises(ValueError, match="price_data cannot be empty"):
|
|
581
|
+
plot.government_contracts("LMT", price_data=pd.DataFrame())
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def test_government_contracts_missing_price_column():
|
|
585
|
+
plot = _PlotNamespace(MockClient())
|
|
586
|
+
bad_df = pd.DataFrame({"volume": [100, 200]})
|
|
587
|
+
with pytest.raises(ValueError, match="price_data must contain a price column"):
|
|
588
|
+
plot.government_contracts("LMT", price_data=bad_df)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def test_government_contracts_as_json():
|
|
592
|
+
plot = _PlotNamespace(MockGovernmentContractsClient())
|
|
593
|
+
result = plot.government_contracts("LMT", price_data=PRICE_DF, show=False, as_json=True)
|
|
594
|
+
assert isinstance(result, str)
|
|
@@ -312,3 +312,39 @@ def test_screener_predictions_monthly_dataframe(client, _activate_responses):
|
|
|
312
312
|
assert set(df.columns) == {"name", "expectedLongTerm"}
|
|
313
313
|
assert df.loc["AAPL", "expectedLongTerm"] == 1.5
|
|
314
314
|
assert df.loc["TSLA", "expectedLongTerm"] == 2.1
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
# ── government contracts ─────────────────────────────────────────────
|
|
318
|
+
def test_screener_government_contracts(client, _activate_responses):
|
|
319
|
+
payload = wrap_v2({
|
|
320
|
+
"data": [
|
|
321
|
+
{"symbol": "LMT", "name": "Lockheed Martin", "awardAmount": 50000000,
|
|
322
|
+
"awardingAgency": "Department of Defense", "startDate": "2025-06-01"},
|
|
323
|
+
],
|
|
324
|
+
"summary": {"totalContracts": 1, "totalTickers": 1, "totalValue": 50000000},
|
|
325
|
+
})
|
|
326
|
+
stub_json(_activate_responses, "GET", "screener/government-contracts", payload, params={"limit": "5"})
|
|
327
|
+
data = client.screener.government_contracts(limit=5)
|
|
328
|
+
assert isinstance(data, list)
|
|
329
|
+
assert len(data) == 1
|
|
330
|
+
assert data[0]["symbol"] == "LMT"
|
|
331
|
+
assert data[0]["awardAmount"] == 50000000
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_screener_government_contracts_dataframe(client, _activate_responses):
|
|
335
|
+
payload = wrap_v2({
|
|
336
|
+
"data": [
|
|
337
|
+
{"symbol": "LMT", "name": "Lockheed Martin", "awardAmount": 50000000,
|
|
338
|
+
"awardingAgency": "Department of Defense", "startDate": "2025-06-01"},
|
|
339
|
+
{"symbol": "BA", "name": "Boeing", "awardAmount": 80000000,
|
|
340
|
+
"awardingAgency": "Department of Defense", "startDate": "2025-05-20"},
|
|
341
|
+
],
|
|
342
|
+
"summary": {"totalContracts": 2, "totalTickers": 2, "totalValue": 130000000},
|
|
343
|
+
})
|
|
344
|
+
stub_json(_activate_responses, "GET", "screener/government-contracts", payload, params={"limit": "5"})
|
|
345
|
+
df = client.screener.government_contracts(limit=5, as_dataframe=True)
|
|
346
|
+
assert isinstance(df, pd.DataFrame)
|
|
347
|
+
assert len(df) == 2
|
|
348
|
+
assert df.index.name == "symbol"
|
|
349
|
+
assert "awardAmount" in df.columns
|
|
350
|
+
assert df.loc["LMT", "awardAmount"] == 50000000
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/analyst_ratings.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/corporate_lobbying.py
RENAMED
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/insider_transactions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/aio/endpoints/reddit_mentions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/corporate_lobbying.py
RENAMED
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain/endpoints/insider_transactions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{finbrain_python-0.2.3 → finbrain_python-0.2.4}/src/finbrain_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|