tickerapi 0.1.0__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.
- tickerapi/__init__.py +40 -0
- tickerapi/async_client.py +436 -0
- tickerapi/client.py +434 -0
- tickerapi/exceptions.py +53 -0
- tickerapi/py.typed +0 -0
- tickerapi/types.py +112 -0
- tickerapi-0.1.0.dist-info/METADATA +216 -0
- tickerapi-0.1.0.dist-info/RECORD +9 -0
- tickerapi-0.1.0.dist-info/WHEEL +4 -0
tickerapi/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""TickerAPI Python SDK - Financial data at your fingertips.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from tickerapi import TickerAPI
|
|
6
|
+
|
|
7
|
+
client = TickerAPI("your_api_key")
|
|
8
|
+
result = client.summary("AAPL")
|
|
9
|
+
|
|
10
|
+
For async usage::
|
|
11
|
+
|
|
12
|
+
from tickerapi import AsyncTickerAPI
|
|
13
|
+
|
|
14
|
+
async with AsyncTickerAPI("your_api_key") as client:
|
|
15
|
+
result = await client.summary("AAPL")
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from .async_client import AsyncTickerAPI
|
|
19
|
+
from .client import TickerAPI
|
|
20
|
+
from .exceptions import (
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
DataUnavailableError,
|
|
23
|
+
ForbiddenError,
|
|
24
|
+
NotFoundError,
|
|
25
|
+
RateLimitError,
|
|
26
|
+
TickerAPIError,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"TickerAPI",
|
|
31
|
+
"AsyncTickerAPI",
|
|
32
|
+
"TickerAPIError",
|
|
33
|
+
"AuthenticationError",
|
|
34
|
+
"ForbiddenError",
|
|
35
|
+
"NotFoundError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"DataUnavailableError",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
"""Asynchronous TickerAPI client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
DataUnavailableError,
|
|
10
|
+
ForbiddenError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
TickerAPIError,
|
|
14
|
+
)
|
|
15
|
+
from .types import RateLimits
|
|
16
|
+
|
|
17
|
+
_DEFAULT_BASE_URL = "https://api.tickerapi.ai/v1"
|
|
18
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_rate_limits(headers: httpx.Headers) -> RateLimits:
|
|
22
|
+
"""Extract rate-limit information from response headers."""
|
|
23
|
+
|
|
24
|
+
def _int_or_none(key: str) -> Optional[int]:
|
|
25
|
+
val = headers.get(key)
|
|
26
|
+
if val is None:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return int(val)
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return RateLimits(
|
|
34
|
+
request_limit=_int_or_none("X-Request-Limit"),
|
|
35
|
+
requests_used=_int_or_none("X-Requests-Used"),
|
|
36
|
+
requests_remaining=_int_or_none("X-Requests-Remaining"),
|
|
37
|
+
request_reset=headers.get("X-Request-Reset"),
|
|
38
|
+
hourly_request_limit=_int_or_none("X-Hourly-Request-Limit"),
|
|
39
|
+
hourly_requests_used=_int_or_none("X-Hourly-Requests-Used"),
|
|
40
|
+
hourly_requests_remaining=_int_or_none("X-Hourly-Requests-Remaining"),
|
|
41
|
+
hourly_request_reset=headers.get("X-Hourly-Request-Reset"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
46
|
+
"""Raise a typed TickerAPIError if the response indicates an error."""
|
|
47
|
+
if response.status_code < 400:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
body = response.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
raise TickerAPIError(
|
|
54
|
+
status_code=response.status_code,
|
|
55
|
+
error_type="unknown_error",
|
|
56
|
+
message=response.text or "Unknown error",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
error = body.get("error", {})
|
|
60
|
+
error_type = error.get("type", "unknown_error")
|
|
61
|
+
message = error.get("message", "Unknown error")
|
|
62
|
+
upgrade_url = error.get("upgrade_url")
|
|
63
|
+
reset = error.get("reset")
|
|
64
|
+
|
|
65
|
+
kwargs: Dict[str, Any] = dict(
|
|
66
|
+
status_code=response.status_code,
|
|
67
|
+
error_type=error_type,
|
|
68
|
+
message=message,
|
|
69
|
+
upgrade_url=upgrade_url,
|
|
70
|
+
reset=reset,
|
|
71
|
+
raw=body,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
error_cls = {
|
|
75
|
+
401: AuthenticationError,
|
|
76
|
+
403: ForbiddenError,
|
|
77
|
+
404: NotFoundError,
|
|
78
|
+
429: RateLimitError,
|
|
79
|
+
503: DataUnavailableError,
|
|
80
|
+
}.get(response.status_code, TickerAPIError)
|
|
81
|
+
|
|
82
|
+
raise error_cls(**kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AsyncTickerAPI:
|
|
86
|
+
"""Asynchronous client for the TickerAPI financial data API.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
api_key: Your TickerAPI bearer token.
|
|
90
|
+
base_url: Override the default API base URL.
|
|
91
|
+
timeout: Request timeout in seconds (default 30).
|
|
92
|
+
**httpx_kwargs: Additional keyword arguments forwarded to ``httpx.AsyncClient``.
|
|
93
|
+
|
|
94
|
+
Usage::
|
|
95
|
+
|
|
96
|
+
import asyncio
|
|
97
|
+
from tickerapi import AsyncTickerAPI
|
|
98
|
+
|
|
99
|
+
async def main():
|
|
100
|
+
async with AsyncTickerAPI("your_api_key") as client:
|
|
101
|
+
result = await client.summary("AAPL")
|
|
102
|
+
print(result["data"])
|
|
103
|
+
|
|
104
|
+
asyncio.run(main())
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
api_key: str,
|
|
110
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
111
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
112
|
+
**httpx_kwargs: Any,
|
|
113
|
+
) -> None:
|
|
114
|
+
self._base_url = base_url.rstrip("/")
|
|
115
|
+
self._client = httpx.AsyncClient(
|
|
116
|
+
headers={
|
|
117
|
+
"Authorization": f"Bearer {api_key}",
|
|
118
|
+
"Accept": "application/json",
|
|
119
|
+
"User-Agent": "tickerapi-python/0.1.0",
|
|
120
|
+
},
|
|
121
|
+
timeout=timeout,
|
|
122
|
+
**httpx_kwargs,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
# Internal helpers
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
async def _request(
|
|
130
|
+
self,
|
|
131
|
+
method: str,
|
|
132
|
+
path: str,
|
|
133
|
+
*,
|
|
134
|
+
params: Optional[Dict[str, Any]] = None,
|
|
135
|
+
json: Optional[Dict[str, Any]] = None,
|
|
136
|
+
) -> Dict[str, Any]:
|
|
137
|
+
"""Send a request and return parsed data + rate limits."""
|
|
138
|
+
url = f"{self._base_url}{path}"
|
|
139
|
+
|
|
140
|
+
if params:
|
|
141
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
142
|
+
|
|
143
|
+
response = await self._client.request(method, url, params=params, json=json)
|
|
144
|
+
_raise_for_status(response)
|
|
145
|
+
|
|
146
|
+
data = response.json()
|
|
147
|
+
rate_limits = _parse_rate_limits(response.headers)
|
|
148
|
+
|
|
149
|
+
return {"data": data, "rate_limits": rate_limits}
|
|
150
|
+
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
# Public API methods
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
async def summary(
|
|
156
|
+
self,
|
|
157
|
+
ticker: str,
|
|
158
|
+
*,
|
|
159
|
+
timeframe: Optional[str] = None,
|
|
160
|
+
date: Optional[str] = None,
|
|
161
|
+
) -> Dict[str, Any]:
|
|
162
|
+
"""Get a summary for a single ticker.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
ticker: Asset ticker symbol (e.g. ``"AAPL"``).
|
|
166
|
+
timeframe: ``"daily"`` or ``"weekly"`` (default ``"daily"``).
|
|
167
|
+
date: ISO 8601 date string (``YYYY-MM-DD``).
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
171
|
+
"""
|
|
172
|
+
return await self._request(
|
|
173
|
+
"GET",
|
|
174
|
+
f"/summary/{ticker}",
|
|
175
|
+
params={"timeframe": timeframe, "date": date},
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
async def compare(
|
|
179
|
+
self,
|
|
180
|
+
tickers: Union[List[str], str],
|
|
181
|
+
*,
|
|
182
|
+
timeframe: Optional[str] = None,
|
|
183
|
+
date: Optional[str] = None,
|
|
184
|
+
) -> Dict[str, Any]:
|
|
185
|
+
"""Compare multiple tickers side-by-side.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
tickers: List of ticker symbols or a comma-separated string.
|
|
189
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
190
|
+
date: ISO 8601 date string.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
194
|
+
"""
|
|
195
|
+
if isinstance(tickers, list):
|
|
196
|
+
tickers = ",".join(tickers)
|
|
197
|
+
return await self._request(
|
|
198
|
+
"GET",
|
|
199
|
+
"/compare",
|
|
200
|
+
params={"tickers": tickers, "timeframe": timeframe, "date": date},
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def watchlist(
|
|
204
|
+
self,
|
|
205
|
+
tickers: List[str],
|
|
206
|
+
*,
|
|
207
|
+
timeframe: Optional[str] = None,
|
|
208
|
+
) -> Dict[str, Any]:
|
|
209
|
+
"""Get watchlist data for multiple tickers.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
tickers: List of ticker symbols.
|
|
213
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
217
|
+
"""
|
|
218
|
+
body: Dict[str, Any] = {"tickers": tickers}
|
|
219
|
+
if timeframe is not None:
|
|
220
|
+
body["timeframe"] = timeframe
|
|
221
|
+
return await self._request("POST", "/watchlist", json=body)
|
|
222
|
+
|
|
223
|
+
async def assets(self) -> Dict[str, Any]:
|
|
224
|
+
"""List all available assets.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
228
|
+
"""
|
|
229
|
+
return await self._request("GET", "/assets")
|
|
230
|
+
|
|
231
|
+
async def scan_oversold(
|
|
232
|
+
self,
|
|
233
|
+
*,
|
|
234
|
+
timeframe: Optional[str] = None,
|
|
235
|
+
asset_class: Optional[str] = None,
|
|
236
|
+
sector: Optional[str] = None,
|
|
237
|
+
min_severity: Optional[str] = None,
|
|
238
|
+
sort_by: Optional[str] = None,
|
|
239
|
+
limit: Optional[int] = None,
|
|
240
|
+
date: Optional[str] = None,
|
|
241
|
+
) -> Dict[str, Any]:
|
|
242
|
+
"""Scan for oversold assets.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
246
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
247
|
+
sector: Filter by sector.
|
|
248
|
+
min_severity: ``"oversold"`` or ``"deep_oversold"``.
|
|
249
|
+
sort_by: ``"severity"``, ``"days_oversold"``, or ``"condition_percentile"``.
|
|
250
|
+
limit: Max results (1-50).
|
|
251
|
+
date: ISO 8601 date string.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
255
|
+
"""
|
|
256
|
+
return await self._request(
|
|
257
|
+
"GET",
|
|
258
|
+
"/scan/oversold",
|
|
259
|
+
params={
|
|
260
|
+
"timeframe": timeframe,
|
|
261
|
+
"asset_class": asset_class,
|
|
262
|
+
"sector": sector,
|
|
263
|
+
"min_severity": min_severity,
|
|
264
|
+
"sort_by": sort_by,
|
|
265
|
+
"limit": limit,
|
|
266
|
+
"date": date,
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
async def scan_breakouts(
|
|
271
|
+
self,
|
|
272
|
+
*,
|
|
273
|
+
timeframe: Optional[str] = None,
|
|
274
|
+
asset_class: Optional[str] = None,
|
|
275
|
+
sector: Optional[str] = None,
|
|
276
|
+
direction: Optional[str] = None,
|
|
277
|
+
sort_by: Optional[str] = None,
|
|
278
|
+
limit: Optional[int] = None,
|
|
279
|
+
date: Optional[str] = None,
|
|
280
|
+
) -> Dict[str, Any]:
|
|
281
|
+
"""Scan for breakout patterns.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
285
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
286
|
+
sector: Filter by sector.
|
|
287
|
+
direction: ``"bullish"``, ``"bearish"``, or ``"all"``.
|
|
288
|
+
sort_by: ``"volume_ratio"``, ``"level_strength"``, or ``"condition_percentile"``.
|
|
289
|
+
limit: Max results (1-50).
|
|
290
|
+
date: ISO 8601 date string.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
294
|
+
"""
|
|
295
|
+
return await self._request(
|
|
296
|
+
"GET",
|
|
297
|
+
"/scan/breakouts",
|
|
298
|
+
params={
|
|
299
|
+
"timeframe": timeframe,
|
|
300
|
+
"asset_class": asset_class,
|
|
301
|
+
"sector": sector,
|
|
302
|
+
"direction": direction,
|
|
303
|
+
"sort_by": sort_by,
|
|
304
|
+
"limit": limit,
|
|
305
|
+
"date": date,
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
async def scan_unusual_volume(
|
|
310
|
+
self,
|
|
311
|
+
*,
|
|
312
|
+
timeframe: Optional[str] = None,
|
|
313
|
+
asset_class: Optional[str] = None,
|
|
314
|
+
sector: Optional[str] = None,
|
|
315
|
+
min_ratio_band: Optional[str] = None,
|
|
316
|
+
sort_by: Optional[str] = None,
|
|
317
|
+
limit: Optional[int] = None,
|
|
318
|
+
date: Optional[str] = None,
|
|
319
|
+
) -> Dict[str, Any]:
|
|
320
|
+
"""Scan for unusual volume activity.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
324
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
325
|
+
sector: Filter by sector.
|
|
326
|
+
min_ratio_band: ``"extremely_low"``, ``"low"``, ``"normal"``,
|
|
327
|
+
``"elevated"``, ``"high"``, or ``"extremely_high"``.
|
|
328
|
+
sort_by: ``"volume_percentile"``.
|
|
329
|
+
limit: Max results (1-50).
|
|
330
|
+
date: ISO 8601 date string.
|
|
331
|
+
|
|
332
|
+
Returns:
|
|
333
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
334
|
+
"""
|
|
335
|
+
return await self._request(
|
|
336
|
+
"GET",
|
|
337
|
+
"/scan/unusual-volume",
|
|
338
|
+
params={
|
|
339
|
+
"timeframe": timeframe,
|
|
340
|
+
"asset_class": asset_class,
|
|
341
|
+
"sector": sector,
|
|
342
|
+
"min_ratio_band": min_ratio_band,
|
|
343
|
+
"sort_by": sort_by,
|
|
344
|
+
"limit": limit,
|
|
345
|
+
"date": date,
|
|
346
|
+
},
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
async def scan_valuation(
|
|
350
|
+
self,
|
|
351
|
+
*,
|
|
352
|
+
timeframe: Optional[str] = None,
|
|
353
|
+
sector: Optional[str] = None,
|
|
354
|
+
direction: Optional[str] = None,
|
|
355
|
+
min_severity: Optional[str] = None,
|
|
356
|
+
sort_by: Optional[str] = None,
|
|
357
|
+
limit: Optional[int] = None,
|
|
358
|
+
date: Optional[str] = None,
|
|
359
|
+
) -> Dict[str, Any]:
|
|
360
|
+
"""Scan for valuation outliers.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
364
|
+
sector: Filter by sector.
|
|
365
|
+
direction: ``"undervalued"``, ``"overvalued"``, or ``"all"``.
|
|
366
|
+
min_severity: ``"deep_value"`` or ``"extreme_premium"``.
|
|
367
|
+
sort_by: ``"valuation_percentile"`` or ``"pe_vs_history"``.
|
|
368
|
+
limit: Max results (1-50).
|
|
369
|
+
date: ISO 8601 date string.
|
|
370
|
+
|
|
371
|
+
Returns:
|
|
372
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
373
|
+
"""
|
|
374
|
+
return await self._request(
|
|
375
|
+
"GET",
|
|
376
|
+
"/scan/valuation",
|
|
377
|
+
params={
|
|
378
|
+
"timeframe": timeframe,
|
|
379
|
+
"sector": sector,
|
|
380
|
+
"direction": direction,
|
|
381
|
+
"min_severity": min_severity,
|
|
382
|
+
"sort_by": sort_by,
|
|
383
|
+
"limit": limit,
|
|
384
|
+
"date": date,
|
|
385
|
+
},
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
async def scan_insider_activity(
|
|
389
|
+
self,
|
|
390
|
+
*,
|
|
391
|
+
timeframe: Optional[str] = None,
|
|
392
|
+
sector: Optional[str] = None,
|
|
393
|
+
direction: Optional[str] = None,
|
|
394
|
+
sort_by: Optional[str] = None,
|
|
395
|
+
limit: Optional[int] = None,
|
|
396
|
+
date: Optional[str] = None,
|
|
397
|
+
) -> Dict[str, Any]:
|
|
398
|
+
"""Scan for notable insider trading activity.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
402
|
+
sector: Filter by sector.
|
|
403
|
+
direction: ``"buying"``, ``"selling"``, or ``"all"``.
|
|
404
|
+
sort_by: ``"zone_severity"``, ``"shares_volume"``, or ``"net_ratio"``.
|
|
405
|
+
limit: Max results (1-50).
|
|
406
|
+
date: ISO 8601 date string.
|
|
407
|
+
|
|
408
|
+
Returns:
|
|
409
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
410
|
+
"""
|
|
411
|
+
return await self._request(
|
|
412
|
+
"GET",
|
|
413
|
+
"/scan/insider-activity",
|
|
414
|
+
params={
|
|
415
|
+
"timeframe": timeframe,
|
|
416
|
+
"sector": sector,
|
|
417
|
+
"direction": direction,
|
|
418
|
+
"sort_by": sort_by,
|
|
419
|
+
"limit": limit,
|
|
420
|
+
"date": date,
|
|
421
|
+
},
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
# ------------------------------------------------------------------
|
|
425
|
+
# Context manager support
|
|
426
|
+
# ------------------------------------------------------------------
|
|
427
|
+
|
|
428
|
+
async def close(self) -> None:
|
|
429
|
+
"""Close the underlying HTTP client."""
|
|
430
|
+
await self._client.aclose()
|
|
431
|
+
|
|
432
|
+
async def __aenter__(self) -> "AsyncTickerAPI":
|
|
433
|
+
return self
|
|
434
|
+
|
|
435
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
436
|
+
await self.close()
|
tickerapi/client.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""Synchronous TickerAPI client."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, List, Optional, Union
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from .exceptions import (
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
DataUnavailableError,
|
|
10
|
+
ForbiddenError,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
RateLimitError,
|
|
13
|
+
TickerAPIError,
|
|
14
|
+
)
|
|
15
|
+
from .types import RateLimits
|
|
16
|
+
|
|
17
|
+
_DEFAULT_BASE_URL = "https://api.tickerapi.ai/v1"
|
|
18
|
+
_DEFAULT_TIMEOUT = 30.0
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _parse_rate_limits(headers: httpx.Headers) -> RateLimits:
|
|
22
|
+
"""Extract rate-limit information from response headers."""
|
|
23
|
+
|
|
24
|
+
def _int_or_none(key: str) -> Optional[int]:
|
|
25
|
+
val = headers.get(key)
|
|
26
|
+
if val is None:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
return int(val)
|
|
30
|
+
except (ValueError, TypeError):
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return RateLimits(
|
|
34
|
+
request_limit=_int_or_none("X-Request-Limit"),
|
|
35
|
+
requests_used=_int_or_none("X-Requests-Used"),
|
|
36
|
+
requests_remaining=_int_or_none("X-Requests-Remaining"),
|
|
37
|
+
request_reset=headers.get("X-Request-Reset"),
|
|
38
|
+
hourly_request_limit=_int_or_none("X-Hourly-Request-Limit"),
|
|
39
|
+
hourly_requests_used=_int_or_none("X-Hourly-Requests-Used"),
|
|
40
|
+
hourly_requests_remaining=_int_or_none("X-Hourly-Requests-Remaining"),
|
|
41
|
+
hourly_request_reset=headers.get("X-Hourly-Request-Reset"),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
46
|
+
"""Raise a typed TickerAPIError if the response indicates an error."""
|
|
47
|
+
if response.status_code < 400:
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
body = response.json()
|
|
52
|
+
except Exception:
|
|
53
|
+
raise TickerAPIError(
|
|
54
|
+
status_code=response.status_code,
|
|
55
|
+
error_type="unknown_error",
|
|
56
|
+
message=response.text or "Unknown error",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
error = body.get("error", {})
|
|
60
|
+
error_type = error.get("type", "unknown_error")
|
|
61
|
+
message = error.get("message", "Unknown error")
|
|
62
|
+
upgrade_url = error.get("upgrade_url")
|
|
63
|
+
reset = error.get("reset")
|
|
64
|
+
|
|
65
|
+
kwargs: Dict[str, Any] = dict(
|
|
66
|
+
status_code=response.status_code,
|
|
67
|
+
error_type=error_type,
|
|
68
|
+
message=message,
|
|
69
|
+
upgrade_url=upgrade_url,
|
|
70
|
+
reset=reset,
|
|
71
|
+
raw=body,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
error_cls = {
|
|
75
|
+
401: AuthenticationError,
|
|
76
|
+
403: ForbiddenError,
|
|
77
|
+
404: NotFoundError,
|
|
78
|
+
429: RateLimitError,
|
|
79
|
+
503: DataUnavailableError,
|
|
80
|
+
}.get(response.status_code, TickerAPIError)
|
|
81
|
+
|
|
82
|
+
raise error_cls(**kwargs)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class TickerAPI:
|
|
86
|
+
"""Synchronous client for the TickerAPI financial data API.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
api_key: Your TickerAPI bearer token.
|
|
90
|
+
base_url: Override the default API base URL.
|
|
91
|
+
timeout: Request timeout in seconds (default 30).
|
|
92
|
+
**httpx_kwargs: Additional keyword arguments forwarded to ``httpx.Client``.
|
|
93
|
+
|
|
94
|
+
Usage::
|
|
95
|
+
|
|
96
|
+
from tickerapi import TickerAPI
|
|
97
|
+
|
|
98
|
+
client = TickerAPI("your_api_key")
|
|
99
|
+
result = client.summary("AAPL")
|
|
100
|
+
print(result["data"])
|
|
101
|
+
print(result["rate_limits"])
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
api_key: str,
|
|
107
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
108
|
+
timeout: float = _DEFAULT_TIMEOUT,
|
|
109
|
+
**httpx_kwargs: Any,
|
|
110
|
+
) -> None:
|
|
111
|
+
self._base_url = base_url.rstrip("/")
|
|
112
|
+
self._client = httpx.Client(
|
|
113
|
+
headers={
|
|
114
|
+
"Authorization": f"Bearer {api_key}",
|
|
115
|
+
"Accept": "application/json",
|
|
116
|
+
"User-Agent": "tickerapi-python/0.1.0",
|
|
117
|
+
},
|
|
118
|
+
timeout=timeout,
|
|
119
|
+
**httpx_kwargs,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------------
|
|
123
|
+
# Internal helpers
|
|
124
|
+
# ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
def _request(
|
|
127
|
+
self,
|
|
128
|
+
method: str,
|
|
129
|
+
path: str,
|
|
130
|
+
*,
|
|
131
|
+
params: Optional[Dict[str, Any]] = None,
|
|
132
|
+
json: Optional[Dict[str, Any]] = None,
|
|
133
|
+
) -> Dict[str, Any]:
|
|
134
|
+
"""Send a request and return parsed data + rate limits."""
|
|
135
|
+
url = f"{self._base_url}{path}"
|
|
136
|
+
|
|
137
|
+
# Strip None values from params
|
|
138
|
+
if params:
|
|
139
|
+
params = {k: v for k, v in params.items() if v is not None}
|
|
140
|
+
|
|
141
|
+
response = self._client.request(method, url, params=params, json=json)
|
|
142
|
+
_raise_for_status(response)
|
|
143
|
+
|
|
144
|
+
data = response.json()
|
|
145
|
+
rate_limits = _parse_rate_limits(response.headers)
|
|
146
|
+
|
|
147
|
+
return {"data": data, "rate_limits": rate_limits}
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# Public API methods
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
def summary(
|
|
154
|
+
self,
|
|
155
|
+
ticker: str,
|
|
156
|
+
*,
|
|
157
|
+
timeframe: Optional[str] = None,
|
|
158
|
+
date: Optional[str] = None,
|
|
159
|
+
) -> Dict[str, Any]:
|
|
160
|
+
"""Get a summary for a single ticker.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
ticker: Asset ticker symbol (e.g. ``"AAPL"``).
|
|
164
|
+
timeframe: ``"daily"`` or ``"weekly"`` (default ``"daily"``).
|
|
165
|
+
date: ISO 8601 date string (``YYYY-MM-DD``).
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
169
|
+
"""
|
|
170
|
+
return self._request(
|
|
171
|
+
"GET",
|
|
172
|
+
f"/summary/{ticker}",
|
|
173
|
+
params={"timeframe": timeframe, "date": date},
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def compare(
|
|
177
|
+
self,
|
|
178
|
+
tickers: Union[List[str], str],
|
|
179
|
+
*,
|
|
180
|
+
timeframe: Optional[str] = None,
|
|
181
|
+
date: Optional[str] = None,
|
|
182
|
+
) -> Dict[str, Any]:
|
|
183
|
+
"""Compare multiple tickers side-by-side.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
tickers: List of ticker symbols or a comma-separated string.
|
|
187
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
188
|
+
date: ISO 8601 date string.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
192
|
+
"""
|
|
193
|
+
if isinstance(tickers, list):
|
|
194
|
+
tickers = ",".join(tickers)
|
|
195
|
+
return self._request(
|
|
196
|
+
"GET",
|
|
197
|
+
"/compare",
|
|
198
|
+
params={"tickers": tickers, "timeframe": timeframe, "date": date},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def watchlist(
|
|
202
|
+
self,
|
|
203
|
+
tickers: List[str],
|
|
204
|
+
*,
|
|
205
|
+
timeframe: Optional[str] = None,
|
|
206
|
+
) -> Dict[str, Any]:
|
|
207
|
+
"""Get watchlist data for multiple tickers.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
tickers: List of ticker symbols.
|
|
211
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
215
|
+
"""
|
|
216
|
+
body: Dict[str, Any] = {"tickers": tickers}
|
|
217
|
+
if timeframe is not None:
|
|
218
|
+
body["timeframe"] = timeframe
|
|
219
|
+
return self._request("POST", "/watchlist", json=body)
|
|
220
|
+
|
|
221
|
+
def assets(self) -> Dict[str, Any]:
|
|
222
|
+
"""List all available assets.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
226
|
+
"""
|
|
227
|
+
return self._request("GET", "/assets")
|
|
228
|
+
|
|
229
|
+
def scan_oversold(
|
|
230
|
+
self,
|
|
231
|
+
*,
|
|
232
|
+
timeframe: Optional[str] = None,
|
|
233
|
+
asset_class: Optional[str] = None,
|
|
234
|
+
sector: Optional[str] = None,
|
|
235
|
+
min_severity: Optional[str] = None,
|
|
236
|
+
sort_by: Optional[str] = None,
|
|
237
|
+
limit: Optional[int] = None,
|
|
238
|
+
date: Optional[str] = None,
|
|
239
|
+
) -> Dict[str, Any]:
|
|
240
|
+
"""Scan for oversold assets.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
244
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
245
|
+
sector: Filter by sector.
|
|
246
|
+
min_severity: ``"oversold"`` or ``"deep_oversold"``.
|
|
247
|
+
sort_by: ``"severity"``, ``"days_oversold"``, or ``"condition_percentile"``.
|
|
248
|
+
limit: Max results (1-50).
|
|
249
|
+
date: ISO 8601 date string.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
253
|
+
"""
|
|
254
|
+
return self._request(
|
|
255
|
+
"GET",
|
|
256
|
+
"/scan/oversold",
|
|
257
|
+
params={
|
|
258
|
+
"timeframe": timeframe,
|
|
259
|
+
"asset_class": asset_class,
|
|
260
|
+
"sector": sector,
|
|
261
|
+
"min_severity": min_severity,
|
|
262
|
+
"sort_by": sort_by,
|
|
263
|
+
"limit": limit,
|
|
264
|
+
"date": date,
|
|
265
|
+
},
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def scan_breakouts(
|
|
269
|
+
self,
|
|
270
|
+
*,
|
|
271
|
+
timeframe: Optional[str] = None,
|
|
272
|
+
asset_class: Optional[str] = None,
|
|
273
|
+
sector: Optional[str] = None,
|
|
274
|
+
direction: Optional[str] = None,
|
|
275
|
+
sort_by: Optional[str] = None,
|
|
276
|
+
limit: Optional[int] = None,
|
|
277
|
+
date: Optional[str] = None,
|
|
278
|
+
) -> Dict[str, Any]:
|
|
279
|
+
"""Scan for breakout patterns.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
283
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
284
|
+
sector: Filter by sector.
|
|
285
|
+
direction: ``"bullish"``, ``"bearish"``, or ``"all"``.
|
|
286
|
+
sort_by: ``"volume_ratio"``, ``"level_strength"``, or ``"condition_percentile"``.
|
|
287
|
+
limit: Max results (1-50).
|
|
288
|
+
date: ISO 8601 date string.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
292
|
+
"""
|
|
293
|
+
return self._request(
|
|
294
|
+
"GET",
|
|
295
|
+
"/scan/breakouts",
|
|
296
|
+
params={
|
|
297
|
+
"timeframe": timeframe,
|
|
298
|
+
"asset_class": asset_class,
|
|
299
|
+
"sector": sector,
|
|
300
|
+
"direction": direction,
|
|
301
|
+
"sort_by": sort_by,
|
|
302
|
+
"limit": limit,
|
|
303
|
+
"date": date,
|
|
304
|
+
},
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
def scan_unusual_volume(
|
|
308
|
+
self,
|
|
309
|
+
*,
|
|
310
|
+
timeframe: Optional[str] = None,
|
|
311
|
+
asset_class: Optional[str] = None,
|
|
312
|
+
sector: Optional[str] = None,
|
|
313
|
+
min_ratio_band: Optional[str] = None,
|
|
314
|
+
sort_by: Optional[str] = None,
|
|
315
|
+
limit: Optional[int] = None,
|
|
316
|
+
date: Optional[str] = None,
|
|
317
|
+
) -> Dict[str, Any]:
|
|
318
|
+
"""Scan for unusual volume activity.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
322
|
+
asset_class: ``"stock"``, ``"crypto"``, ``"etf"``, or ``"all"``.
|
|
323
|
+
sector: Filter by sector.
|
|
324
|
+
min_ratio_band: ``"extremely_low"``, ``"low"``, ``"normal"``,
|
|
325
|
+
``"elevated"``, ``"high"``, or ``"extremely_high"``.
|
|
326
|
+
sort_by: ``"volume_percentile"``.
|
|
327
|
+
limit: Max results (1-50).
|
|
328
|
+
date: ISO 8601 date string.
|
|
329
|
+
|
|
330
|
+
Returns:
|
|
331
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
332
|
+
"""
|
|
333
|
+
return self._request(
|
|
334
|
+
"GET",
|
|
335
|
+
"/scan/unusual-volume",
|
|
336
|
+
params={
|
|
337
|
+
"timeframe": timeframe,
|
|
338
|
+
"asset_class": asset_class,
|
|
339
|
+
"sector": sector,
|
|
340
|
+
"min_ratio_band": min_ratio_band,
|
|
341
|
+
"sort_by": sort_by,
|
|
342
|
+
"limit": limit,
|
|
343
|
+
"date": date,
|
|
344
|
+
},
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def scan_valuation(
|
|
348
|
+
self,
|
|
349
|
+
*,
|
|
350
|
+
timeframe: Optional[str] = None,
|
|
351
|
+
sector: Optional[str] = None,
|
|
352
|
+
direction: Optional[str] = None,
|
|
353
|
+
min_severity: Optional[str] = None,
|
|
354
|
+
sort_by: Optional[str] = None,
|
|
355
|
+
limit: Optional[int] = None,
|
|
356
|
+
date: Optional[str] = None,
|
|
357
|
+
) -> Dict[str, Any]:
|
|
358
|
+
"""Scan for valuation outliers.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
362
|
+
sector: Filter by sector.
|
|
363
|
+
direction: ``"undervalued"``, ``"overvalued"``, or ``"all"``.
|
|
364
|
+
min_severity: ``"deep_value"`` or ``"extreme_premium"``.
|
|
365
|
+
sort_by: ``"valuation_percentile"`` or ``"pe_vs_history"``.
|
|
366
|
+
limit: Max results (1-50).
|
|
367
|
+
date: ISO 8601 date string.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
371
|
+
"""
|
|
372
|
+
return self._request(
|
|
373
|
+
"GET",
|
|
374
|
+
"/scan/valuation",
|
|
375
|
+
params={
|
|
376
|
+
"timeframe": timeframe,
|
|
377
|
+
"sector": sector,
|
|
378
|
+
"direction": direction,
|
|
379
|
+
"min_severity": min_severity,
|
|
380
|
+
"sort_by": sort_by,
|
|
381
|
+
"limit": limit,
|
|
382
|
+
"date": date,
|
|
383
|
+
},
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def scan_insider_activity(
|
|
387
|
+
self,
|
|
388
|
+
*,
|
|
389
|
+
timeframe: Optional[str] = None,
|
|
390
|
+
sector: Optional[str] = None,
|
|
391
|
+
direction: Optional[str] = None,
|
|
392
|
+
sort_by: Optional[str] = None,
|
|
393
|
+
limit: Optional[int] = None,
|
|
394
|
+
date: Optional[str] = None,
|
|
395
|
+
) -> Dict[str, Any]:
|
|
396
|
+
"""Scan for notable insider trading activity.
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
timeframe: ``"daily"`` or ``"weekly"``.
|
|
400
|
+
sector: Filter by sector.
|
|
401
|
+
direction: ``"buying"``, ``"selling"``, or ``"all"``.
|
|
402
|
+
sort_by: ``"zone_severity"``, ``"shares_volume"``, or ``"net_ratio"``.
|
|
403
|
+
limit: Max results (1-50).
|
|
404
|
+
date: ISO 8601 date string.
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
Dict with ``data`` and ``rate_limits`` keys.
|
|
408
|
+
"""
|
|
409
|
+
return self._request(
|
|
410
|
+
"GET",
|
|
411
|
+
"/scan/insider-activity",
|
|
412
|
+
params={
|
|
413
|
+
"timeframe": timeframe,
|
|
414
|
+
"sector": sector,
|
|
415
|
+
"direction": direction,
|
|
416
|
+
"sort_by": sort_by,
|
|
417
|
+
"limit": limit,
|
|
418
|
+
"date": date,
|
|
419
|
+
},
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# ------------------------------------------------------------------
|
|
423
|
+
# Context manager support
|
|
424
|
+
# ------------------------------------------------------------------
|
|
425
|
+
|
|
426
|
+
def close(self) -> None:
|
|
427
|
+
"""Close the underlying HTTP client."""
|
|
428
|
+
self._client.close()
|
|
429
|
+
|
|
430
|
+
def __enter__(self) -> "TickerAPI":
|
|
431
|
+
return self
|
|
432
|
+
|
|
433
|
+
def __exit__(self, *args: Any) -> None:
|
|
434
|
+
self.close()
|
tickerapi/exceptions.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Exceptions for the TickerAPI SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TickerAPIError(Exception):
|
|
7
|
+
"""Raised when the TickerAPI returns an error response.
|
|
8
|
+
|
|
9
|
+
Attributes:
|
|
10
|
+
status_code: HTTP status code of the response.
|
|
11
|
+
error_type: Error type string from the API (e.g. "authentication_error").
|
|
12
|
+
message: Human-readable error message.
|
|
13
|
+
upgrade_url: URL to upgrade the API plan (present on 403/429 errors).
|
|
14
|
+
reset: Rate limit reset timestamp (present on 429 errors).
|
|
15
|
+
raw: The full parsed error body from the API.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
status_code: int,
|
|
21
|
+
error_type: str,
|
|
22
|
+
message: str,
|
|
23
|
+
upgrade_url: Optional[str] = None,
|
|
24
|
+
reset: Optional[str] = None,
|
|
25
|
+
raw: Optional[Dict[str, Any]] = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.status_code = status_code
|
|
28
|
+
self.error_type = error_type
|
|
29
|
+
self.message = message
|
|
30
|
+
self.upgrade_url = upgrade_url
|
|
31
|
+
self.reset = reset
|
|
32
|
+
self.raw = raw or {}
|
|
33
|
+
super().__init__(f"[{status_code}] {error_type}: {message}")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthenticationError(TickerAPIError):
|
|
37
|
+
"""Raised on 401 responses (invalid or missing API key)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ForbiddenError(TickerAPIError):
|
|
41
|
+
"""Raised on 403 responses (tier-restricted endpoint)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotFoundError(TickerAPIError):
|
|
45
|
+
"""Raised on 404 responses (asset not found)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RateLimitError(TickerAPIError):
|
|
49
|
+
"""Raised on 429 responses (rate limit exceeded)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class DataUnavailableError(TickerAPIError):
|
|
53
|
+
"""Raised on 503 responses (data temporarily unavailable)."""
|
tickerapi/py.typed
ADDED
|
File without changes
|
tickerapi/types.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""TypedDict definitions for TickerAPI SDK parameters and responses."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Any, Dict, List, Optional
|
|
5
|
+
|
|
6
|
+
if sys.version_info >= (3, 8):
|
|
7
|
+
from typing import Literal, TypedDict
|
|
8
|
+
else:
|
|
9
|
+
from typing_extensions import Literal, TypedDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ---------------------------------------------------------------------------
|
|
13
|
+
# Common types
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
Timeframe = Literal["daily", "weekly"]
|
|
17
|
+
AssetClass = Literal["stock", "crypto", "etf", "all"]
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Rate limit information returned with every response
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RateLimits(TypedDict, total=False):
|
|
25
|
+
"""Rate limit information parsed from response headers."""
|
|
26
|
+
|
|
27
|
+
request_limit: Optional[int]
|
|
28
|
+
requests_used: Optional[int]
|
|
29
|
+
requests_remaining: Optional[int]
|
|
30
|
+
request_reset: Optional[str]
|
|
31
|
+
hourly_request_limit: Optional[int]
|
|
32
|
+
hourly_requests_used: Optional[int]
|
|
33
|
+
hourly_requests_remaining: Optional[int]
|
|
34
|
+
hourly_request_reset: Optional[str]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
# Scan parameter TypedDicts (for IDE autocompletion)
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class OversoldParams(TypedDict, total=False):
|
|
43
|
+
"""Parameters for the oversold scan endpoint."""
|
|
44
|
+
|
|
45
|
+
timeframe: Timeframe
|
|
46
|
+
asset_class: AssetClass
|
|
47
|
+
sector: str
|
|
48
|
+
min_severity: Literal["oversold", "deep_oversold"]
|
|
49
|
+
sort_by: Literal["severity", "days_oversold", "condition_percentile"]
|
|
50
|
+
limit: int
|
|
51
|
+
date: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class BreakoutsParams(TypedDict, total=False):
|
|
55
|
+
"""Parameters for the breakouts scan endpoint."""
|
|
56
|
+
|
|
57
|
+
timeframe: Timeframe
|
|
58
|
+
asset_class: AssetClass
|
|
59
|
+
sector: str
|
|
60
|
+
direction: Literal["bullish", "bearish", "all"]
|
|
61
|
+
sort_by: Literal["volume_ratio", "level_strength", "condition_percentile"]
|
|
62
|
+
limit: int
|
|
63
|
+
date: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class UnusualVolumeParams(TypedDict, total=False):
|
|
67
|
+
"""Parameters for the unusual volume scan endpoint."""
|
|
68
|
+
|
|
69
|
+
timeframe: Timeframe
|
|
70
|
+
asset_class: AssetClass
|
|
71
|
+
sector: str
|
|
72
|
+
min_ratio_band: Literal[
|
|
73
|
+
"extremely_low", "low", "normal", "elevated", "high", "extremely_high"
|
|
74
|
+
]
|
|
75
|
+
sort_by: Literal["volume_percentile"]
|
|
76
|
+
limit: int
|
|
77
|
+
date: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ValuationParams(TypedDict, total=False):
|
|
81
|
+
"""Parameters for the valuation scan endpoint."""
|
|
82
|
+
|
|
83
|
+
timeframe: Timeframe
|
|
84
|
+
sector: str
|
|
85
|
+
direction: Literal["undervalued", "overvalued", "all"]
|
|
86
|
+
min_severity: Literal["deep_value", "extreme_premium"]
|
|
87
|
+
sort_by: Literal["valuation_percentile", "pe_vs_history"]
|
|
88
|
+
limit: int
|
|
89
|
+
date: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class InsiderActivityParams(TypedDict, total=False):
|
|
93
|
+
"""Parameters for the insider activity scan endpoint."""
|
|
94
|
+
|
|
95
|
+
timeframe: Timeframe
|
|
96
|
+
sector: str
|
|
97
|
+
direction: Literal["buying", "selling", "all"]
|
|
98
|
+
sort_by: Literal["zone_severity", "shares_volume", "net_ratio"]
|
|
99
|
+
limit: int
|
|
100
|
+
date: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# API response wrapper
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class APIResponse(TypedDict):
|
|
109
|
+
"""Every SDK method returns a dict with parsed JSON data and rate limits."""
|
|
110
|
+
|
|
111
|
+
data: Any
|
|
112
|
+
rate_limits: RateLimits
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tickerapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the TickerAPI financial data API
|
|
5
|
+
Project-URL: Homepage, https://tickerapi.ai
|
|
6
|
+
Project-URL: Documentation, https://tickerapi.ai/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/tickerapi/tickerapi-python
|
|
8
|
+
Project-URL: Issues, https://github.com/tickerapi/tickerapi-python/issues
|
|
9
|
+
Author-email: TickerAPI <support@tickerapi.ai>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
Keywords: api,crypto,etf,finance,market-data,stocks,ticker
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
24
|
+
Classifier: Typing :: Typed
|
|
25
|
+
Requires-Python: >=3.8
|
|
26
|
+
Requires-Dist: httpx>=0.24.0
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# TickerAPI Python SDK
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/tickerapi/)
|
|
32
|
+
[](https://pypi.org/project/tickerapi/)
|
|
33
|
+
|
|
34
|
+
The official Python SDK for [TickerAPI](https://tickerapi.ai) -- financial data and market intelligence API.
|
|
35
|
+
|
|
36
|
+
- Sync and async clients
|
|
37
|
+
- Full type hints for IDE autocompletion
|
|
38
|
+
- Typed exceptions for every error class
|
|
39
|
+
- Rate limit information on every response
|
|
40
|
+
|
|
41
|
+
**Full API documentation:** [https://tickerapi.ai/docs](https://tickerapi.ai/docs)
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install tickerapi
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
### Synchronous
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from tickerapi import TickerAPI
|
|
55
|
+
|
|
56
|
+
client = TickerAPI("your_api_key")
|
|
57
|
+
|
|
58
|
+
# Get a ticker summary
|
|
59
|
+
result = client.summary("AAPL")
|
|
60
|
+
print(result["data"])
|
|
61
|
+
|
|
62
|
+
# Rate limit info is included on every response
|
|
63
|
+
print(result["rate_limits"]["requests_remaining"])
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Asynchronous
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import asyncio
|
|
70
|
+
from tickerapi import AsyncTickerAPI
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
async with AsyncTickerAPI("your_api_key") as client:
|
|
74
|
+
result = await client.summary("AAPL")
|
|
75
|
+
print(result["data"])
|
|
76
|
+
|
|
77
|
+
asyncio.run(main())
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Endpoints
|
|
81
|
+
|
|
82
|
+
### Summary
|
|
83
|
+
|
|
84
|
+
Get a detailed summary for a single ticker.
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
result = client.summary("AAPL")
|
|
88
|
+
result = client.summary("AAPL", timeframe="weekly")
|
|
89
|
+
result = client.summary("AAPL", date="2025-01-15")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Compare
|
|
93
|
+
|
|
94
|
+
Compare multiple tickers side-by-side.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
result = client.compare(["AAPL", "MSFT", "GOOGL"])
|
|
98
|
+
result = client.compare("AAPL,MSFT,GOOGL", timeframe="weekly")
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Watchlist
|
|
102
|
+
|
|
103
|
+
Post a list of tickers to get watchlist data.
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
result = client.watchlist(["AAPL", "MSFT", "TSLA"])
|
|
107
|
+
result = client.watchlist(["AAPL", "MSFT"], timeframe="weekly")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Assets
|
|
111
|
+
|
|
112
|
+
List all available assets.
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
result = client.assets()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### Scan: Oversold
|
|
119
|
+
|
|
120
|
+
Find oversold assets across markets.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
result = client.scan_oversold()
|
|
124
|
+
result = client.scan_oversold(
|
|
125
|
+
asset_class="stock",
|
|
126
|
+
min_severity="deep_oversold",
|
|
127
|
+
sort_by="severity",
|
|
128
|
+
limit=10,
|
|
129
|
+
)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Scan: Breakouts
|
|
133
|
+
|
|
134
|
+
Detect breakout patterns.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
result = client.scan_breakouts(direction="bullish", asset_class="stock", limit=20)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Scan: Unusual Volume
|
|
141
|
+
|
|
142
|
+
Spot unusual volume activity.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
result = client.scan_unusual_volume(min_ratio_band="high", limit=10)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Scan: Valuation
|
|
149
|
+
|
|
150
|
+
Find valuation outliers.
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
result = client.scan_valuation(direction="undervalued", sort_by="pe_vs_history")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Scan: Insider Activity
|
|
157
|
+
|
|
158
|
+
Track notable insider trading activity.
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
result = client.scan_insider_activity(direction="buying", sort_by="net_ratio", limit=15)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Error Handling
|
|
165
|
+
|
|
166
|
+
The SDK raises typed exceptions for all API errors:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from tickerapi import TickerAPI, TickerAPIError, RateLimitError, NotFoundError
|
|
170
|
+
|
|
171
|
+
client = TickerAPI("your_api_key")
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result = client.summary("INVALID_TICKER")
|
|
175
|
+
except NotFoundError as e:
|
|
176
|
+
print(f"Ticker not found: {e.message}")
|
|
177
|
+
except RateLimitError as e:
|
|
178
|
+
print(f"Rate limited! Resets at: {e.reset}")
|
|
179
|
+
print(f"Upgrade: {e.upgrade_url}")
|
|
180
|
+
except TickerAPIError as e:
|
|
181
|
+
print(f"API error [{e.status_code}]: {e.message}")
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Exception Hierarchy
|
|
185
|
+
|
|
186
|
+
| Exception | Status Code | Description |
|
|
187
|
+
|---|---|---|
|
|
188
|
+
| `TickerAPIError` | any | Base exception for all API errors |
|
|
189
|
+
| `AuthenticationError` | 401 | Invalid or missing API key |
|
|
190
|
+
| `ForbiddenError` | 403 | Endpoint restricted to higher tier |
|
|
191
|
+
| `NotFoundError` | 404 | Asset not found |
|
|
192
|
+
| `RateLimitError` | 429 | Rate limit exceeded |
|
|
193
|
+
| `DataUnavailableError` | 503 | Data temporarily unavailable |
|
|
194
|
+
|
|
195
|
+
All exceptions include `status_code`, `error_type`, `message`, and optionally `upgrade_url` and `reset` attributes.
|
|
196
|
+
|
|
197
|
+
## Rate Limits
|
|
198
|
+
|
|
199
|
+
Every response includes a `rate_limits` dict parsed from the API headers:
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
result = client.summary("AAPL")
|
|
203
|
+
limits = result["rate_limits"]
|
|
204
|
+
|
|
205
|
+
print(limits["request_limit"]) # Total request limit
|
|
206
|
+
print(limits["requests_remaining"]) # Requests remaining
|
|
207
|
+
print(limits["request_reset"]) # Reset timestamp
|
|
208
|
+
print(limits["hourly_request_limit"]) # Hourly limit
|
|
209
|
+
print(limits["hourly_requests_remaining"]) # Hourly remaining
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Links
|
|
213
|
+
|
|
214
|
+
- [TickerAPI Website](https://tickerapi.ai)
|
|
215
|
+
- [API Documentation](https://tickerapi.ai/docs)
|
|
216
|
+
- [PyPI Package](https://pypi.org/project/tickerapi/)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
tickerapi/__init__.py,sha256=xOj6HVMqeSCo22I9GSEcMgV31YMF_UizdnQD5899SfE,801
|
|
2
|
+
tickerapi/async_client.py,sha256=CvTD9oR_2mhdOp-WbzRFGi87kM3t4xOwdolyNM4f_AM,13624
|
|
3
|
+
tickerapi/client.py,sha256=hhw-NmKih8S9W0KM8AmAu8HNkStR8-GdhhSkvYyjj-0,13399
|
|
4
|
+
tickerapi/exceptions.py,sha256=EM7Jdzx9TKO_l3D41unDaRZec0tBXRj60o5e_NR2I6M,1613
|
|
5
|
+
tickerapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
tickerapi/types.py,sha256=6w5vPc_BGmZrQeaYknNUQtZ0z-UnXtxxyI-oTshuXpo,3387
|
|
7
|
+
tickerapi-0.1.0.dist-info/METADATA,sha256=4S9xdlbF5rmttX-icT0flN-9hKpEEI6phhdK9p9aewg,5623
|
|
8
|
+
tickerapi-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
tickerapi-0.1.0.dist-info/RECORD,,
|