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.
@@ -0,0 +1,86 @@
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 type-checkers
8
+ from ..client import FinBrainClient
9
+
10
+
11
+ class OptionsAPI:
12
+ """
13
+ Options data endpoints
14
+ ----------------------
15
+
16
+ Currently implemented
17
+ ~~~~~~~~~~~~~~~~~~~~~
18
+ • **put_call** - ``/putcalldata/<MARKET>/<TICKER>``
19
+
20
+ Future additions (open interest, IV, strikes, …) can live in this same class.
21
+ """
22
+
23
+ # ────────────────────────────────────────────────────────────────────
24
+ def __init__(self, client: "FinBrainClient") -> None:
25
+ self._c = client # reference to the parent client
26
+
27
+ # ────────────────────────────────────────────────────────────────────
28
+ def put_call(
29
+ self,
30
+ market: str,
31
+ symbol: str,
32
+ *,
33
+ date_from: _dt.date | str | None = None,
34
+ date_to: _dt.date | str | None = None,
35
+ as_dataframe: bool = False,
36
+ ) -> Dict[str, Any] | pd.DataFrame:
37
+ """
38
+ Put/Call ratio data for *symbol* in *market*.
39
+
40
+ Parameters
41
+ ----------
42
+ market :
43
+ Market name **exactly as FinBrain lists it**
44
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
45
+ Spaces and special characters are accepted; they are URL-encoded
46
+ automatically.
47
+ symbol :
48
+ Ticker symbol; converted to upper-case.
49
+ date_from, date_to :
50
+ Optional ISO dates (``YYYY-MM-DD``) bounding the returned rows.
51
+ as_dataframe :
52
+ If *True*, return a **pandas.DataFrame** indexed by ``date``;
53
+ otherwise return the raw JSON dict.
54
+
55
+ Returns
56
+ -------
57
+ dict | pandas.DataFrame
58
+ """
59
+ params: Dict[str, str] = {}
60
+ if date_from:
61
+ params["dateFrom"] = _to_datestr(date_from)
62
+ if date_to:
63
+ params["dateTo"] = _to_datestr(date_to)
64
+
65
+ market_slug = quote(market, safe="")
66
+ path = f"putcalldata/{market_slug}/{symbol.upper()}"
67
+
68
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
69
+
70
+ if as_dataframe:
71
+ rows: List[Dict[str, Any]] = data.get("putCallData", [])
72
+ df = pd.DataFrame(rows)
73
+ if not df.empty and "date" in df.columns:
74
+ df["date"] = pd.to_datetime(df["date"])
75
+ df.set_index("date", inplace=True)
76
+ return df
77
+
78
+ return data
79
+
80
+
81
+ # ────────────────────────────────────────────────────────────────────────
82
+ # helper
83
+ # ────────────────────────────────────────────────────────────────────────
84
+ def _to_datestr(value: _dt.date | str) -> str:
85
+ """Convert :class:`datetime.date` → ``YYYY-MM-DD``; leave strings untouched."""
86
+ return value.isoformat() if isinstance(value, _dt.date) else value
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import pandas as pd
5
+ from urllib.parse import quote
6
+ from typing import TYPE_CHECKING, Literal, Dict, Any, List
7
+
8
+ if TYPE_CHECKING:
9
+ from ..client import FinBrainClient
10
+
11
+
12
+ # ------------------------------------------------------------------------- #
13
+ _PType = Literal["daily", "monthly"]
14
+ _ALLOWED: set[str] = {"daily", "monthly"}
15
+ _DATE_RE = re.compile(r"\d{4}-\d{2}-\d{2}")
16
+
17
+
18
+ class PredictionsAPI:
19
+ """
20
+ Price-prediction endpoints
21
+
22
+ • `/market/<MARKET>/predictions/<TYPE>`
23
+ • `/ticker/<TICKER>/predictions/<TYPE>`
24
+
25
+ where **TYPE** ∈ { `daily`, `monthly` }.
26
+ """
27
+
28
+ def __init__(self, client: "FinBrainClient") -> None:
29
+ self._c = client
30
+
31
+ # ------------------------------------------------------------------ #
32
+ def ticker(
33
+ self,
34
+ symbol: str,
35
+ *,
36
+ prediction_type: _PType = "daily",
37
+ as_dataframe: bool = False,
38
+ ) -> Dict[str, Any] | pd.DataFrame:
39
+ """
40
+ Single-ticker predictions.
41
+
42
+ Parameters
43
+ ----------
44
+ symbol :
45
+ Symbol such as ``AAPL`` (case-insensitive).
46
+ prediction_type :
47
+ ``"daily"`` (10-day horizon) or ``"monthly"`` (12-month horizon).
48
+ as_dataframe :
49
+ Return a **DataFrame** (index =`date`, cols =`main, lower, upper`)
50
+ instead of raw JSON.
51
+
52
+ Returns
53
+ -------
54
+ dict | pandas.DataFrame
55
+ """
56
+ _validate(prediction_type)
57
+ path = f"ticker/{symbol.upper()}/predictions/{prediction_type}"
58
+ data: Dict[str, Any] = self._c._request("GET", path)
59
+
60
+ if as_dataframe:
61
+ pred = data.get("prediction", {})
62
+ rows: list[dict[str, float]] = []
63
+ for k, v in pred.items():
64
+ if _DATE_RE.fullmatch(k):
65
+ main, low, high = map(float, v.split(","))
66
+ rows.append({"date": k, "main": main, "lower": low, "upper": high})
67
+ df = pd.DataFrame(rows).set_index(
68
+ pd.to_datetime(pd.Series([r["date"] for r in rows]))
69
+ )
70
+ df.index.name = "date"
71
+ df.drop(columns="date", inplace=True)
72
+ return df
73
+
74
+ return data
75
+
76
+ # ------------------------------------------------------------------ #
77
+ def market(
78
+ self,
79
+ market: str,
80
+ *,
81
+ prediction_type: _PType = "daily",
82
+ as_dataframe: bool = False,
83
+ ) -> List[Dict[str, Any]] | pd.DataFrame:
84
+ """
85
+ Predictions for **all** tickers in a market.
86
+
87
+ Parameters
88
+ ----------
89
+ market :
90
+ Market name **exactly as FinBrain lists it**
91
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``). Spaces/`&` are OK.
92
+ prediction_type :
93
+ ``"daily"`` or ``"monthly"``.
94
+ as_dataframe :
95
+ If *True* return a DataFrame (index =`ticker`) with
96
+ ``expectedShort``, ``expectedMid``, ``expectedLong``, and optional
97
+ ``sentimentScore``.
98
+
99
+ Returns
100
+ -------
101
+ list[dict] | pandas.DataFrame
102
+ """
103
+ _validate(prediction_type)
104
+ slug = quote(market, safe="")
105
+ path = f"market/{slug}/predictions/{prediction_type}"
106
+ data: List[Dict[str, Any]] = self._c._request("GET", path)
107
+
108
+ if as_dataframe:
109
+ rows: list[dict[str, Any]] = []
110
+ for rec in data:
111
+ p = rec.get("prediction", {})
112
+ rows.append(
113
+ {
114
+ "ticker": rec["ticker"],
115
+ "expectedShort": float(p["expectedShort"]),
116
+ "expectedMid": float(p["expectedMid"]),
117
+ "expectedLong": float(p["expectedLong"]),
118
+ "sentimentScore": float(rec.get("sentimentScore", "nan")),
119
+ }
120
+ )
121
+ df = pd.DataFrame(rows).set_index("ticker")
122
+ return df
123
+
124
+ return data
125
+
126
+
127
+ # ---------------------------------------------------------------------- #
128
+ def _validate(value: str) -> None:
129
+ if value not in _ALLOWED:
130
+ raise ValueError("prediction_type must be 'daily' or 'monthly'")
@@ -0,0 +1,108 @@
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
6
+
7
+ if TYPE_CHECKING: # imported only by static type-checkers
8
+ from ..client import FinBrainClient
9
+
10
+
11
+ class SentimentsAPI:
12
+ """
13
+ Wrapper for **/sentiments/<MARKET>/<TICKER>** endpoints.
14
+
15
+ Example
16
+ -------
17
+ >>> fb.sentiments.ticker(
18
+ ... market="S&P 500",
19
+ ... symbol="AMZN",
20
+ ... date_from="2024-01-01",
21
+ ... date_to="2024-02-02",
22
+ ... )
23
+ {
24
+ "ticker": "AMZN",
25
+ "name": "Amazon.com Inc.",
26
+ "sentimentAnalysis": {
27
+ "2024-01-15": "0.123",
28
+ ...
29
+ }
30
+ }
31
+ """
32
+
33
+ # --------------------------------------------------------------------- #
34
+ def __init__(self, client: "FinBrainClient") -> None:
35
+ self._c = client
36
+
37
+ # --------------------------------------------------------------------- #
38
+ def ticker(
39
+ self,
40
+ market: str,
41
+ symbol: str,
42
+ *,
43
+ date_from: _dt.date | str | None = None,
44
+ date_to: _dt.date | str | None = None,
45
+ days: int | None = None,
46
+ as_dataframe: bool = False,
47
+ ) -> Dict[str, Any] | pd.DataFrame:
48
+ """
49
+ Retrieve sentiment scores for a *single* ticker.
50
+
51
+ Parameters
52
+ ----------
53
+ market :
54
+ Market name **exactly as FinBrain lists it**
55
+ (e.g. ``"S&P 500"``, ``"Germany DAX"``, ``"HK Hang Seng"``).
56
+ Spaces and special characters are accepted; they are URL-encoded
57
+ automatically.
58
+ symbol :
59
+ Stock/crypto symbol (``AAPL``, ``AMZN`` …) *uppercase recommended*.
60
+ date_from, date_to :
61
+ Optional start / end dates (``YYYY-MM-DD``). If omitted, FinBrain
62
+ defaults to its internal window or to ``days``.
63
+ days :
64
+ Alternative to explicit dates - integer 1…120 for "past *n* days".
65
+ Ignored if either ``date_from`` or ``date_to`` is supplied.
66
+ as_dataframe :
67
+ If *True*, return a **DataFrame** with a ``date`` index and a single
68
+ ``sentiment`` column.
69
+
70
+ Returns
71
+ -------
72
+ dict | pandas.DataFrame
73
+ """
74
+ # Build query parameters
75
+ params: Dict[str, str] = {}
76
+
77
+ if date_from:
78
+ params["dateFrom"] = _to_datestr(date_from)
79
+ if date_to:
80
+ params["dateTo"] = _to_datestr(date_to)
81
+ if days is not None and "dateFrom" not in params and "dateTo" not in params:
82
+ params["days"] = str(days)
83
+
84
+ market_slug = quote(market, safe="")
85
+ path = f"sentiments/{market_slug}/{symbol.upper()}"
86
+
87
+ data: Dict[str, Any] = self._c._request("GET", path, params=params)
88
+
89
+ if as_dataframe:
90
+ sa: Dict[str, str] = data.get("sentimentAnalysis", {})
91
+ df = (
92
+ pd.Series(sa, name="sentiment")
93
+ .astype(float)
94
+ .rename_axis("date")
95
+ .to_frame()
96
+ )
97
+ df.index = pd.to_datetime(df.index)
98
+ return df
99
+
100
+ return data
101
+
102
+
103
+ # ------------------------------------------------------------------------- #
104
+ # Helpers #
105
+ # ------------------------------------------------------------------------- #
106
+ def _to_datestr(value: _dt.date | str) -> str:
107
+ """Convert ``datetime.date`` → 'YYYY-MM-DD' but pass strings through."""
108
+ return value.isoformat() if isinstance(value, _dt.date) else value
finbrain/exceptions.py ADDED
@@ -0,0 +1,140 @@
1
+ """
2
+ finbrain.exceptions
3
+ ~~~~~~~~~~~~~~~~~~~
4
+
5
+ Canonical exception hierarchy for the FinBrain Python SDK.
6
+ Every public error subclasses :class:`FinBrainError`.
7
+
8
+ Docs-based mapping
9
+ ------------------
10
+ 400 Bad Request → BadRequest
11
+ 401 Unauthorized → AuthenticationError
12
+ 403 Forbidden → PermissionDenied
13
+ 404 Not Found → NotFound
14
+ 405 Method Not Allowed → MethodNotAllowed
15
+ 500 Internal Server Error → ServerError
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from typing import Any, Dict, Union
21
+
22
+ __all__ = [
23
+ "FinBrainError",
24
+ "BadRequest",
25
+ "AuthenticationError",
26
+ "PermissionDenied",
27
+ "NotFound",
28
+ "MethodNotAllowed",
29
+ "ServerError",
30
+ #
31
+ "InvalidResponse",
32
+ "http_error_to_exception",
33
+ ]
34
+
35
+ # ─────────────────────────────────────────────────────────────
36
+ # Base class
37
+ # ─────────────────────────────────────────────────────────────
38
+
39
+
40
+ class FinBrainError(Exception):
41
+ """Root of the SDK's exception tree."""
42
+
43
+ def __init__(
44
+ self,
45
+ message: str,
46
+ *,
47
+ status_code: int | None = None,
48
+ payload: Any | None = None,
49
+ ):
50
+ super().__init__(message)
51
+ self.status_code: int | None = status_code
52
+ self.payload: Any | None = payload # raw JSON/text for debugging
53
+
54
+
55
+ # ─────────────────────────────────────────────────────────────
56
+ # 4xx family
57
+ # ─────────────────────────────────────────────────────────────
58
+
59
+
60
+ class BadRequest(FinBrainError):
61
+ """400 - The request is malformed or contains invalid parameters."""
62
+
63
+
64
+ class AuthenticationError(FinBrainError):
65
+ """401 - API key missing or invalid."""
66
+
67
+
68
+ class PermissionDenied(FinBrainError):
69
+ """403 - Authenticated, but not authorised to perform this action."""
70
+
71
+
72
+ class NotFound(FinBrainError):
73
+ """404 - Requested data or endpoint not found."""
74
+
75
+
76
+ class MethodNotAllowed(FinBrainError):
77
+ """405 - Endpoint exists, but the HTTP method is not supported."""
78
+
79
+
80
+ # ─────────────────────────────────────────────────────────────
81
+ # 5xx family
82
+ # ─────────────────────────────────────────────────────────────
83
+
84
+
85
+ class ServerError(FinBrainError):
86
+ """500 - Internal error on FinBrain's side. Retrying later may help."""
87
+
88
+
89
+ # ─────────────────────────────────────────────────────────────
90
+ # Transport / decoding guard
91
+ # ─────────────────────────────────────────────────────────────
92
+
93
+
94
+ class InvalidResponse(FinBrainError):
95
+ """Response couldn't be parsed as JSON or is missing required fields."""
96
+
97
+
98
+ # ─────────────────────────────────────────────────────────────
99
+ # Helper: map HTTP response ➜ exception
100
+ # ─────────────────────────────────────────────────────────────
101
+
102
+
103
+ def _extract_message(payload: Any, default: str) -> str:
104
+ if isinstance(payload, dict):
105
+ # FinBrain usually returns {"message": "..."}
106
+ return payload.get("message", default)
107
+ return default
108
+
109
+
110
+ def http_error_to_exception(resp) -> FinBrainError: # expects requests.Response
111
+ """
112
+ Convert a non-2xx ``requests.Response`` into a typed FinBrainError.
113
+
114
+ Usage
115
+ -----
116
+ >>> raise http_error_to_exception(resp)
117
+ """
118
+ status = resp.status_code
119
+ try:
120
+ payload: Union[Dict[str, Any], str] = resp.json()
121
+ except ValueError:
122
+ payload = resp.text
123
+
124
+ message = _extract_message(payload, f"{status} {resp.reason}")
125
+
126
+ if status == 400:
127
+ return BadRequest(message, status_code=status, payload=payload)
128
+ if status == 401:
129
+ return AuthenticationError(message, status_code=status, payload=payload)
130
+ if status == 403:
131
+ return PermissionDenied(message, status_code=status, payload=payload)
132
+ if status == 404:
133
+ return NotFound(message, status_code=status, payload=payload)
134
+ if status == 405:
135
+ return MethodNotAllowed(message, status_code=status, payload=payload)
136
+ if status == 500:
137
+ return ServerError(message, status_code=status, payload=payload)
138
+
139
+ # Fallback for undocumented codes (future-proofing)
140
+ return FinBrainError(message, status_code=status, payload=payload)