finbrain-python 0.1.1__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """FinBrain Python SDK."""
2
+
3
+ from importlib import metadata as _meta
4
+
5
+ try: # installed from a wheel / sdist
6
+ __version__: str = _meta.version(__name__)
7
+ except _meta.PackageNotFoundError: # running from a Git checkout
8
+ __version__ = "0.0.0.dev0"
9
+
10
+ from .client import FinBrainClient
11
+
12
+ __all__ = ["FinBrainClient", "__version__"]
File without changes
finbrain/aio/client.py ADDED
File without changes
finbrain/client.py ADDED
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from typing import Any, Dict, Optional
6
+ import requests
7
+ from urllib.parse import urljoin
8
+
9
+ from .exceptions import http_error_to_exception, InvalidResponse
10
+ from . import __version__
11
+
12
+ from .endpoints.available import AvailableAPI
13
+ from .endpoints.predictions import PredictionsAPI
14
+ from .endpoints.sentiments import SentimentsAPI
15
+ from .endpoints.app_ratings import AppRatingsAPI
16
+ from .endpoints.analyst_ratings import AnalystRatingsAPI
17
+ from .endpoints.house_trades import HouseTradesAPI
18
+ from .endpoints.insider_transactions import InsiderTransactionsAPI
19
+ from .endpoints.linkedin_data import LinkedInDataAPI
20
+ from .endpoints.options import OptionsAPI
21
+
22
+
23
+ # Which status codes merit a retry
24
+ _RETRYABLE_STATUS = {500}
25
+ # How long to wait between retries (2, 4, 8 … seconds)
26
+ _BACKOFF_BASE = 2
27
+
28
+
29
+ class FinBrainClient:
30
+ """
31
+ Thin wrapper around the FinBrain REST API.
32
+ """
33
+
34
+ DEFAULT_BASE_URL = "https://api.finbrain.tech/v1/"
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: Optional[str] = None,
39
+ base_url: str | None = None,
40
+ timeout: float = 10,
41
+ retries: int = 3,
42
+ ):
43
+ self.api_key = api_key or os.getenv("FINBRAIN_API_KEY")
44
+ if not self.api_key:
45
+ raise ValueError("FinBrain API key missing")
46
+ self.base_url = base_url or self.DEFAULT_BASE_URL
47
+
48
+ self.session = requests.Session()
49
+ self.session.headers["User-Agent"] = f"finbrain-python/{__version__}"
50
+ # optional: mount urllib3 Retry adapter here
51
+
52
+ self.timeout = timeout
53
+ self.retries = retries
54
+
55
+ # wire endpoint helpers
56
+ self.available = AvailableAPI(self)
57
+ self.predictions = PredictionsAPI(self)
58
+ self.sentiments = SentimentsAPI(self)
59
+ self.app_ratings = AppRatingsAPI(self)
60
+ self.analyst_ratings = AnalystRatingsAPI(self)
61
+ self.house_trades = HouseTradesAPI(self)
62
+ self.insider_transactions = InsiderTransactionsAPI(self)
63
+ self.linkedin_data = LinkedInDataAPI(self)
64
+ self.options = OptionsAPI(self)
65
+
66
+ # ---------- private helpers ----------
67
+ def _request(
68
+ self,
69
+ method: str,
70
+ path: str,
71
+ params: Optional[Dict[str, Any]] = None,
72
+ ) -> Any:
73
+ """Perform a single HTTP request with auth token and retries.
74
+
75
+ Raises
76
+ ------
77
+ FinBrainError
78
+ Mapped from HTTP status via ``http_error_to_exception``.
79
+ InvalidResponse
80
+ If the body is not valid JSON.
81
+ """
82
+ params = params.copy() if params else {}
83
+ params["token"] = self.api_key # FinBrain authentication
84
+ url = urljoin(self.base_url, path)
85
+
86
+ for attempt in range(self.retries + 1):
87
+ try:
88
+ resp = self.session.request(
89
+ method, url, params=params, timeout=self.timeout
90
+ )
91
+ except requests.RequestException as exc:
92
+ # Network problem → retry if budget allows, else wrap into FinBrainError
93
+ if attempt == self.retries:
94
+ raise InvalidResponse(f"Network error: {exc}") from exc
95
+ time.sleep(_BACKOFF_BASE**attempt)
96
+ continue
97
+
98
+ # ── Happy path ────────────────────────────────────
99
+ if resp.ok: # 2xx / 3xx
100
+ try:
101
+ return resp.json()
102
+ except ValueError as exc:
103
+ raise InvalidResponse("Response body is not valid JSON") from exc
104
+
105
+ # ── Error path ───────────────────────────────────
106
+ if resp.status_code in _RETRYABLE_STATUS and attempt < self.retries:
107
+ # 500 – exponential back-off then retry
108
+ time.sleep(_BACKOFF_BASE**attempt)
109
+ continue
110
+
111
+ # No more retries → raise the mapped FinBrainError
112
+ raise http_error_to_exception(resp)
File without changes
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ import datetime as _dt
4
+ from urllib.parse import quote
5
+ from typing import TYPE_CHECKING, Dict, Any, List
6
+
7
+ if TYPE_CHECKING: # imported only by type-checkers
8
+ from ..client import FinBrainClient
9
+
10
+
11
+ class AnalystRatingsAPI:
12
+ """
13
+ Endpoint: ``/analystratings/<MARKET>/<TICKER>``
14
+
15
+ Retrieve broker/analyst rating actions for a single ticker.
16
+ Market names may contain spaces (``"S&P 500"``, ``"HK Hang Seng"``...);
17
+ they are URL-encoded automatically.
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
+ market: str,
28
+ symbol: str,
29
+ *,
30
+ date_from: _dt.date | str | None = None,
31
+ date_to: _dt.date | str | None = None,
32
+ as_dataframe: bool = False,
33
+ ) -> Dict[str, Any] | pd.DataFrame:
34
+ """
35
+ Analyst ratings for *symbol* in *market*.
36
+
37
+ Parameters
38
+ ----------
39
+ market :
40
+ Market name **exactly as FinBrain lists it**
41
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
42
+ Spaces and special characters are accepted; they are URL-encoded
43
+ automatically.
44
+ symbol :
45
+ Ticker symbol (case-insensitive; converted to upper-case).
46
+ date_from, date_to :
47
+ Optional ISO dates ``YYYY-MM-DD`` limiting the range.
48
+ as_dataframe :
49
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
50
+ otherwise return the raw JSON dict.
51
+
52
+ Returns
53
+ -------
54
+ dict | pandas.DataFrame
55
+ """
56
+ params: Dict[str, str] = {}
57
+
58
+ if date_from:
59
+ params["dateFrom"] = _to_datestr(date_from)
60
+ if date_to:
61
+ params["dateTo"] = _to_datestr(date_to)
62
+
63
+ market_slug = quote(market, safe="")
64
+ path = f"analystratings/{market_slug}/{symbol.upper()}"
65
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
66
+
67
+ if as_dataframe:
68
+ rows: List[Dict[str, Any]] = data.get("analystRatings", [])
69
+ df = pd.DataFrame(rows)
70
+ if not df.empty and "date" in df.columns:
71
+ df["date"] = pd.to_datetime(df["date"])
72
+ df.set_index("date", inplace=True)
73
+ return df
74
+
75
+ return data
76
+
77
+
78
+ # ---------------------------------------------------------------------- #
79
+ # helper #
80
+ # ---------------------------------------------------------------------- #
81
+ def _to_datestr(value: _dt.date | str) -> str:
82
+ """Convert ``datetime.date`` → ``YYYY-MM-DD``; pass strings through untouched."""
83
+ return value.isoformat() if isinstance(value, _dt.date) else value
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime as _dt
4
+ import pandas as pd
5
+ from urllib.parse import quote
6
+ from typing import TYPE_CHECKING, Dict, Any, List
7
+
8
+ if TYPE_CHECKING: # imported only by static-type tools
9
+ from ..client import FinBrainClient
10
+
11
+
12
+ class AppRatingsAPI:
13
+ """
14
+ Mobile-app rating analytics for a single ticker.
15
+
16
+ Example
17
+ -------
18
+ >>> fb.app_ratings.ticker(
19
+ ... market="S&P 500",
20
+ ... symbol="AMZN",
21
+ ... date_from="2024-01-01",
22
+ ... date_to="2024-02-02",
23
+ ... )["appRatings"][:2]
24
+ [
25
+ {
26
+ "playStoreScore": 3.75,
27
+ "playStoreRatingsCount": 567996,
28
+ "appStoreScore": 4.07,
29
+ "appStoreRatingsCount": 88533,
30
+ "playStoreInstallCount": null,
31
+ "date": "2024-02-02"
32
+ },
33
+ ...
34
+ ]
35
+ """
36
+
37
+ # ------------------------------------------------------------------ #
38
+ def __init__(self, client: "FinBrainClient") -> None:
39
+ self._c = client
40
+
41
+ # ------------------------------------------------------------------ #
42
+ def ticker(
43
+ self,
44
+ market: str,
45
+ symbol: str,
46
+ *,
47
+ date_from: _dt.date | str | None = None,
48
+ date_to: _dt.date | str | None = None,
49
+ as_dataframe: bool = False,
50
+ ) -> Dict[str, Any] | pd.DataFrame:
51
+ """
52
+ Fetch mobile-app ratings for *symbol* in *market*.
53
+
54
+ Parameters
55
+ ----------
56
+ market :
57
+ Market name **exactly as FinBrain lists it**
58
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
59
+ Spaces and special characters are accepted; they are URL-encoded
60
+ automatically.
61
+ symbol :
62
+ Ticker symbol, upper-cased before the request.
63
+ date_from, date_to :
64
+ Optional ISO dates (``YYYY-MM-DD``) to bound the range.
65
+ as_dataframe :
66
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
67
+ otherwise return the raw JSON dict.
68
+
69
+ Returns
70
+ -------
71
+ dict | pandas.DataFrame
72
+ """
73
+ params: Dict[str, str] = {}
74
+
75
+ if date_from:
76
+ params["dateFrom"] = _to_datestr(date_from)
77
+ if date_to:
78
+ params["dateTo"] = _to_datestr(date_to)
79
+
80
+ market_slug = quote(market, safe="")
81
+ path = f"appratings/{market_slug}/{symbol.upper()}"
82
+ data = self._c._request("GET", path, params=params)
83
+
84
+ if as_dataframe:
85
+ rows: List[Dict[str, Any]] = data.get("appRatings", [])
86
+ df = pd.DataFrame(rows)
87
+ if not df.empty and "date" in df.columns:
88
+ df["date"] = pd.to_datetime(df["date"])
89
+ df.set_index("date", inplace=True)
90
+ return df
91
+
92
+ return data
93
+
94
+
95
+ # ---------------------------------------------------------------------- #
96
+ # helper #
97
+ # ---------------------------------------------------------------------- #
98
+ def _to_datestr(value: _dt.date | str) -> str:
99
+ """Convert :pyclass:`~datetime.date` → ``YYYY-MM-DD`` but pass strings."""
100
+ if isinstance(value, _dt.date):
101
+ return value.isoformat()
102
+ return value
@@ -0,0 +1,83 @@
1
+ # src/finbrain/endpoints/available.py
2
+ from __future__ import annotations
3
+ import pandas as pd
4
+ from typing import TYPE_CHECKING, Literal, List, Dict, Any
5
+
6
+ if TYPE_CHECKING: # imported only by type-checkers (mypy, pyright…)
7
+ from ..client import FinBrainClient
8
+
9
+ _PType = Literal["daily", "monthly"]
10
+ _ALLOWED: set[str] = {"daily", "monthly"}
11
+
12
+
13
+ class AvailableAPI:
14
+ """
15
+ Wrapper for FinBrain's **/available** endpoints
16
+ -----------------------------------------------
17
+
18
+ • ``/available/markets`` → list supported indices
19
+ • ``/available/tickers/<TYPE>`` → list tickers for that *TYPE*
20
+
21
+ The docs call the path segment “TYPE”; it might be a market name
22
+ (``sp500`` / ``nasdaq``) or something else. We don't guess—caller passes it.
23
+ """
24
+
25
+ # ------------------------------------------------------------
26
+ def __init__(self, client: "FinBrainClient") -> None:
27
+ self._c = client # reference to the parent client
28
+
29
+ # ------------------------------------------------------------
30
+ def markets(self) -> List[str]:
31
+ """
32
+ Return every market index string FinBrain supports.
33
+
34
+ Example
35
+ -------
36
+ >>> fb.available.markets()
37
+ ['S&P 500', 'NASDAQ', ...]
38
+ """
39
+ data = self._c._request("GET", "available/markets")
40
+
41
+ if isinstance(data, List):
42
+ return data
43
+
44
+ return data.get("availableMarkets", [])
45
+
46
+ # ------------------------------------------------------------
47
+ def tickers(
48
+ self,
49
+ prediction_type: _PType,
50
+ *,
51
+ as_dataframe: bool = False,
52
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
53
+ """
54
+ List all tickers for which **FinBrain has predictions** of the given type.
55
+
56
+ Parameters
57
+ ----------
58
+ prediction_type :
59
+ Either ``"daily"`` (10-day horizon predictions) or
60
+ ``"monthly"`` (12-month horizon predictions). Case-insensitive.
61
+ as_dataframe :
62
+ If *True*, return a ``pd.DataFrame``;
63
+ otherwise return the raw list of dicts.
64
+
65
+ Returns
66
+ -------
67
+ list[dict] | pandas.DataFrame
68
+ Each row / dict contains at least::
69
+
70
+ {
71
+ "ticker": "AAPL",
72
+ "name": "Apple Inc.",
73
+ "market": "S&P 500"
74
+ }
75
+ """
76
+ prediction_type = prediction_type.lower()
77
+ if prediction_type not in _ALLOWED:
78
+ raise ValueError("prediction_type must be 'daily' or 'monthly'")
79
+
80
+ path = f"available/tickers/{prediction_type}"
81
+ data: List[Dict[str, Any]] = self._c._request("GET", path)
82
+
83
+ return pd.DataFrame(data) if as_dataframe else data
@@ -0,0 +1,82 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ from urllib.parse import quote
4
+ import datetime as _dt
5
+ from typing import TYPE_CHECKING, Dict, Any
6
+
7
+ if TYPE_CHECKING: # imported only by type-checkers
8
+ from ..client import FinBrainClient
9
+
10
+
11
+ class HouseTradesAPI:
12
+ """
13
+ Endpoint
14
+ --------
15
+ ``/housetrades/<MARKET>/<TICKER>`` - trading activity of U.S. House
16
+ Representatives 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
+ market: str,
27
+ symbol: str,
28
+ *,
29
+ date_from: _dt.date | str | None = None,
30
+ date_to: _dt.date | str | None = None,
31
+ as_dataframe: bool = False,
32
+ ) -> Dict[str, Any] | pd.DataFrame:
33
+ """
34
+ Fetch House-member trades for *symbol* in *market*.
35
+
36
+ Parameters
37
+ ----------
38
+ market :
39
+ Market name **exactly as FinBrain lists it**
40
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
41
+ Spaces and special characters are accepted; they are URL-encoded
42
+ automatically.
43
+ symbol :
44
+ Ticker symbol; auto-upper-cased.
45
+ date_from, date_to :
46
+ Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows.
47
+ as_dataframe :
48
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
49
+ otherwise return the raw JSON dict.
50
+
51
+ Returns
52
+ -------
53
+ dict | pandas.DataFrame
54
+ """
55
+ params: Dict[str, str] = {}
56
+ if date_from:
57
+ params["dateFrom"] = _to_datestr(date_from)
58
+ if date_to:
59
+ params["dateTo"] = _to_datestr(date_to)
60
+
61
+ market_slug = quote(market, safe="")
62
+ path = f"housetrades/{market_slug}/{symbol.upper()}"
63
+
64
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
65
+
66
+ if as_dataframe:
67
+ rows = data.get("houseTrades", [])
68
+ df = pd.DataFrame(rows)
69
+ if not df.empty and "date" in df.columns:
70
+ df["date"] = pd.to_datetime(df["date"])
71
+ df.set_index("date", inplace=True)
72
+ return df
73
+
74
+ return data
75
+
76
+
77
+ # ---------------------------------------------------------------------- #
78
+ # helper #
79
+ # ---------------------------------------------------------------------- #
80
+ def _to_datestr(value: _dt.date | str) -> str:
81
+ """Convert ``datetime.date`` → ``YYYY-MM-DD``; leave strings untouched."""
82
+ return value.isoformat() if isinstance(value, _dt.date) else value
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ from urllib.parse import quote
4
+ from typing import TYPE_CHECKING, Dict, Any
5
+
6
+ if TYPE_CHECKING: # imported only by static-type tools
7
+ from ..client import FinBrainClient
8
+
9
+
10
+ class InsiderTransactionsAPI:
11
+ """
12
+ Endpoint
13
+ --------
14
+ ``/insidertransactions/<MARKET>/<TICKER>`` - recent Form-4 insider trades
15
+ for the requested ticker.
16
+ """
17
+
18
+ # ------------------------------------------------------------------ #
19
+ def __init__(self, client: "FinBrainClient") -> None:
20
+ self._c = client
21
+
22
+ # ------------------------------------------------------------------ #
23
+ def ticker(
24
+ self,
25
+ market: str,
26
+ symbol: str,
27
+ *,
28
+ as_dataframe: bool = False,
29
+ ) -> Dict[str, Any] | pd.DataFrame:
30
+ """
31
+ Insider transactions for *symbol* in *market*.
32
+
33
+ Parameters
34
+ ----------
35
+ market :
36
+ Market name **exactly as FinBrain lists it**
37
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
38
+ Spaces and special characters are accepted; they are URL-encoded
39
+ automatically.
40
+ symbol :
41
+ Ticker symbol; converted to upper-case.
42
+ as_dataframe :
43
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
44
+ otherwise return the raw JSON dict.
45
+
46
+ Returns
47
+ -------
48
+ dict | pandas.DataFrame
49
+ """
50
+ market_slug = quote(market, safe="")
51
+ path = f"insidertransactions/{market_slug}/{symbol.upper()}"
52
+ data: Dict[str, Any] = self._c._request("GET", path)
53
+
54
+ # --- DataFrame conversion ---
55
+ if as_dataframe:
56
+ rows = data.get("insiderTransactions", [])
57
+ df = pd.DataFrame(rows)
58
+ if not df.empty and "date" in df.columns:
59
+ # examples show dates like "Mar 08 '24" – let pandas parse flexibly
60
+ _fmt = "%b %d '%y" # e.g. Mar 08 '24
61
+ dt = pd.to_datetime(df["date"], format=_fmt, errors="coerce")
62
+ if dt.isna().any(): # fallback if format ever changes
63
+ dt = pd.to_datetime(df["date"], errors="coerce", dayfirst=False)
64
+ df["date"] = dt
65
+ df.set_index("date", inplace=True)
66
+ return df
67
+
68
+ return data
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+ import pandas as pd
3
+ from urllib.parse import quote
4
+ import datetime as _dt
5
+ from typing import TYPE_CHECKING, Dict, Any, List
6
+
7
+ if TYPE_CHECKING: # imported only by static type-checkers
8
+ from ..client import FinBrainClient
9
+
10
+
11
+ class LinkedInDataAPI:
12
+ """
13
+ Endpoint
14
+ --------
15
+ ``/linkedindata/<MARKET>/<TICKER>`` - LinkedIn follower / employee-count
16
+ metrics for a single 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
+ market: str,
27
+ symbol: str,
28
+ *,
29
+ date_from: _dt.date | str | None = None,
30
+ date_to: _dt.date | str | None = None,
31
+ as_dataframe: bool = False,
32
+ ) -> Dict[str, Any] | pd.DataFrame:
33
+ """
34
+ LinkedIn follower- and employee-count metrics for a single ticker.
35
+
36
+ Market names may contain spaces (“S&P 500”, “Germany DAX”, “HK Hang Seng”);
37
+ they are URL-encoded automatically.
38
+
39
+ Parameters
40
+ ----------
41
+ market :
42
+ Market name **exactly as FinBrain lists it**
43
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
44
+ Spaces and special characters are accepted; they are URL-encoded
45
+ automatically.
46
+ symbol :
47
+ Stock symbol; auto-upper-cased.
48
+ date_from, date_to :
49
+ Optional ``YYYY-MM-DD`` bounds.
50
+ as_dataframe :
51
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
52
+ otherwise return the raw JSON dict.
53
+
54
+ Returns
55
+ -------
56
+ dict | pandas.DataFrame
57
+ """
58
+ params: Dict[str, str] = {}
59
+ if date_from:
60
+ params["dateFrom"] = _to_datestr(date_from)
61
+ if date_to:
62
+ params["dateTo"] = _to_datestr(date_to)
63
+
64
+ market_slug = quote(market, safe="")
65
+ path = f"linkedindata/{market_slug}/{symbol.upper()}"
66
+
67
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
68
+
69
+ if as_dataframe:
70
+ rows: List[Dict[str, Any]] = data.get("linkedinData", [])
71
+ df = pd.DataFrame(rows)
72
+ if not df.empty and "date" in df.columns:
73
+ df["date"] = pd.to_datetime(df["date"])
74
+ df.set_index("date", inplace=True)
75
+ return df
76
+
77
+ return data
78
+
79
+
80
+ # ---------------------------------------------------------------------- #
81
+ # helper #
82
+ # ---------------------------------------------------------------------- #
83
+ def _to_datestr(value: _dt.date | str) -> str:
84
+ """Convert :class:`datetime.date` → ``YYYY-MM-DD``; leave strings untouched."""
85
+ return value.isoformat() if isinstance(value, _dt.date) else value