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 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()
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/tickerapi.svg)](https://pypi.org/project/tickerapi/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/tickerapi.svg)](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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any