alphafeed 0.1.0.dev0__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,140 @@
1
+ """Custom exceptions for the AlphaFeed SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+
8
+ class AlphaFeedError(Exception):
9
+ """Base exception for all AlphaFeed SDK errors."""
10
+
11
+ def __init__(self, message: str) -> None:
12
+ super().__init__(message)
13
+ self.message = message
14
+
15
+
16
+ class APIError(AlphaFeedError):
17
+ """Error returned by the AlphaFeed API.
18
+
19
+ Attributes
20
+ ----------
21
+ message : str
22
+ Human-readable error message.
23
+ code : str
24
+ Error code returned by the API (e.g., "INVALID_PERIOD", "SYMBOL_NOT_FOUND").
25
+ status_code : int
26
+ HTTP status code.
27
+ details : Any, optional
28
+ Additional error details for debugging.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ message: str,
34
+ *,
35
+ code: str,
36
+ status_code: int,
37
+ details: Optional[Any] = None,
38
+ ) -> None:
39
+ super().__init__(message)
40
+ self.code = code
41
+ self.status_code = status_code
42
+ self.details = details
43
+
44
+ def __repr__(self) -> str:
45
+ return (
46
+ f"{self.__class__.__name__}("
47
+ f"message={self.message!r}, "
48
+ f"code={self.code!r}, "
49
+ f"status_code={self.status_code})"
50
+ )
51
+
52
+
53
+ class AuthenticationError(APIError):
54
+ """Authentication failed (401)."""
55
+
56
+ pass
57
+
58
+
59
+ class PermissionError(APIError):
60
+ """Permission denied (403)."""
61
+
62
+ pass
63
+
64
+
65
+ class NotFoundError(APIError):
66
+ """Resource not found (404)."""
67
+
68
+ pass
69
+
70
+
71
+ class BadRequestError(APIError):
72
+ """Invalid request parameters (400)."""
73
+
74
+ pass
75
+
76
+
77
+ class RateLimitError(APIError):
78
+ """Rate limit exceeded (429)."""
79
+
80
+ pass
81
+
82
+
83
+ class InternalServerError(APIError):
84
+ """Server error (5xx)."""
85
+
86
+ pass
87
+
88
+
89
+ class ConnectionError(AlphaFeedError):
90
+ """Network connection error."""
91
+
92
+ pass
93
+
94
+
95
+ class TimeoutError(AlphaFeedError):
96
+ """Request timeout."""
97
+
98
+ pass
99
+
100
+
101
+ def raise_for_status(status_code: int, response_body: dict[str, Any]) -> None:
102
+ """Raise an appropriate exception based on status code and response body.
103
+
104
+ Parameters
105
+ ----------
106
+ status_code : int
107
+ HTTP status code.
108
+ response_body : dict
109
+ Parsed JSON response body.
110
+
111
+ Raises
112
+ ------
113
+ APIError
114
+ Appropriate subclass based on the status code.
115
+ """
116
+ if status_code < 400:
117
+ return
118
+
119
+ message = response_body.get("message", "Unknown error")
120
+ code = response_body.get("code", "UNKNOWN")
121
+ details = response_body.get("details")
122
+
123
+ error_cls: type[APIError]
124
+
125
+ if status_code == 400:
126
+ error_cls = BadRequestError
127
+ elif status_code == 401:
128
+ error_cls = AuthenticationError
129
+ elif status_code == 403:
130
+ error_cls = PermissionError
131
+ elif status_code == 404:
132
+ error_cls = NotFoundError
133
+ elif status_code == 429:
134
+ error_cls = RateLimitError
135
+ elif status_code >= 500:
136
+ error_cls = InternalServerError
137
+ else:
138
+ error_cls = APIError
139
+
140
+ raise error_cls(message, code=code, status_code=status_code, details=details)
alphafeed/_types.py ADDED
@@ -0,0 +1,55 @@
1
+ """Custom types and sentinel values for the AlphaFeed SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Mapping, TypeVar, Union
6
+
7
+ import pandas as pd
8
+
9
+ __all__ = [
10
+ "NOT_GIVEN",
11
+ "NotGiven",
12
+ "Headers",
13
+ "Query",
14
+ "Timeout",
15
+ ]
16
+
17
+
18
+ class _NotGiven:
19
+ """Sentinel class for distinguishing omitted arguments from None.
20
+
21
+ This allows us to differentiate between:
22
+ - `param=None` (explicitly passing None)
23
+ - `param` not provided (using the default)
24
+ """
25
+
26
+ __slots__ = ()
27
+
28
+ def __bool__(self) -> bool:
29
+ return False
30
+
31
+ def __repr__(self) -> str:
32
+ return "NOT_GIVEN"
33
+
34
+
35
+ NOT_GIVEN = _NotGiven()
36
+
37
+ NotGiven = _NotGiven
38
+
39
+ Headers = Mapping[str, str]
40
+ Query = Mapping[str, Union[str, int, bool, None]]
41
+ Timeout = Union[float, None]
42
+
43
+ T = TypeVar("T")
44
+
45
+ DataFrameType = TypeVar("DataFrameType", bound="pd.DataFrame")
46
+
47
+
48
+ def is_given(value: Any) -> bool:
49
+ """Check if a value was explicitly provided (not NOT_GIVEN)."""
50
+ return not isinstance(value, _NotGiven)
51
+
52
+
53
+ def strip_not_given(params: dict[str, Any]) -> dict[str, Any]:
54
+ """Remove NOT_GIVEN values from a dictionary."""
55
+ return {k: v for k, v in params.items() if is_given(v)}
alphafeed/client.py ADDED
@@ -0,0 +1,140 @@
1
+ """Main client class for AlphaFeed API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Optional
6
+
7
+ from ._base_client import DEFAULT_MAX_RETRIES, SyncAPIClient
8
+ from ._cache import InstrumentNameCache
9
+ from ._types import Headers, Timeout
10
+ from .resources import (
11
+ Depth,
12
+ Instruments,
13
+ Klines,
14
+ Quotes,
15
+ )
16
+
17
+ __all__ = ["AlphaFeed"]
18
+
19
+
20
+ class AlphaFeed:
21
+ """Synchronous client for AlphaFeed market data API.
22
+
23
+ Provides access to market data including K-lines, quotes, instruments,
24
+ and market depth.
25
+
26
+ Parameters
27
+ ----------
28
+ api_key : str, optional
29
+ API key for authentication. If not provided, reads from ALPHAFEED_API_KEY
30
+ environment variable.
31
+ base_url : str, optional
32
+ Base URL for the API. Defaults to https://api.alphafeed.org.
33
+ Can also be set via ALPHAFEED_BASE_URL environment variable.
34
+ timeout : float, optional
35
+ Request timeout in seconds. Defaults to 30.0.
36
+ max_retries : int, optional
37
+ Maximum number of retry attempts for failed requests. Defaults to 3.
38
+ Retries occur on connection errors, timeouts, and server errors (5xx).
39
+ default_headers : dict, optional
40
+ Default headers to include in all requests.
41
+
42
+ Attributes
43
+ ----------
44
+ klines : Klines
45
+ K-line (OHLCV) data endpoints, including adjustment factors.
46
+ Supports DataFrame conversion and forward/backward adjustment.
47
+ quotes : Quotes
48
+ Real-time quote endpoints.
49
+ depth : Depth
50
+ Market depth (5-level order book) endpoint.
51
+ instruments : Instruments
52
+ Instrument metadata endpoints.
53
+
54
+ Examples
55
+ --------
56
+ Basic usage:
57
+
58
+ >>> from alphafeed import AlphaFeed
59
+ >>>
60
+ >>> client = AlphaFeed(api_key="your-api-key")
61
+ >>>
62
+ >>> # Get forward-adjusted K-line data (default)
63
+ >>> df = client.klines.get("600519.SH", to_dataframe=True)
64
+ >>>
65
+ >>> # Get unadjusted K-line data
66
+ >>> df_raw = client.klines.get("600519.SH", adjust="none", to_dataframe=True)
67
+ >>>
68
+ >>> # Get adjustment factors
69
+ >>> factors = client.klines.ex_factors(["600519.SH"], to_dataframe=True)
70
+
71
+ Using context manager:
72
+
73
+ >>> with AlphaFeed(api_key="your-api-key") as client:
74
+ ... df = client.klines.get("AAPL.US", to_dataframe=True)
75
+ ... print(df.tail())
76
+
77
+ Using environment variable:
78
+
79
+ >>> import os
80
+ >>> os.environ["ALPHAFEED_API_KEY"] = "your-api-key"
81
+ >>> client = AlphaFeed() # Uses ALPHAFEED_API_KEY
82
+ """
83
+
84
+ klines: Klines
85
+ quotes: Quotes
86
+ depth: Depth
87
+ instruments: Instruments
88
+
89
+ def __init__(
90
+ self,
91
+ api_key: Optional[str] = None,
92
+ *,
93
+ base_url: Optional[str] = None,
94
+ timeout: Timeout = 30.0,
95
+ max_retries: int = DEFAULT_MAX_RETRIES,
96
+ default_headers: Optional[Headers] = None,
97
+ cache_dir: Optional[str] = None,
98
+ ) -> None:
99
+ self._client = SyncAPIClient(
100
+ api_key=api_key,
101
+ base_url=base_url,
102
+ timeout=timeout,
103
+ max_retries=max_retries,
104
+ default_headers=default_headers,
105
+ )
106
+ self._instrument_cache = InstrumentNameCache(cache_dir=cache_dir)
107
+
108
+ self.klines = Klines(self._client, instrument_cache=self._instrument_cache)
109
+ self.quotes = Quotes(self._client)
110
+ self.depth = Depth(self._client)
111
+ self.instruments = Instruments(self._client)
112
+
113
+ def __enter__(self) -> "AlphaFeed":
114
+ return self
115
+
116
+ def __exit__(self, *args: Any) -> None:
117
+ self.close()
118
+
119
+ def close(self) -> None:
120
+ """Close the underlying HTTP client.
121
+
122
+ This releases any network resources held by the client.
123
+ Called automatically when using the client as a context manager.
124
+ """
125
+ self._client.close()
126
+
127
+ @property
128
+ def instrument_cache(self) -> InstrumentNameCache:
129
+ """The shared instrument name cache."""
130
+ return self._instrument_cache
131
+
132
+ @property
133
+ def api_key(self) -> Optional[str]:
134
+ """The API key used for authentication."""
135
+ return self._client.api_key
136
+
137
+ @property
138
+ def base_url(self) -> str:
139
+ """The base URL for API requests."""
140
+ return self._client.base_url
alphafeed/models.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, List, Literal, TypedDict
4
+
5
+ from typing_extensions import NotRequired
6
+
7
+ Period = Literal["1m", "5m", "15m", "30m", "60m", "1d", "1w", "1M", "1Q", "1Y"]
8
+
9
+ AdjustType = Literal["none", "forward", "backward", "forward_additive", "backward_additive"]
10
+
11
+
12
+ class CompactKlineData(TypedDict):
13
+ amount: List[float]
14
+ close: List[float]
15
+ high: List[float]
16
+ low: List[float]
17
+ open: List[float]
18
+ prev_close: NotRequired[List[float]]
19
+ timestamp: List[int]
20
+ volume: List[int]
21
+
22
+
23
+ class ErrorResponse(TypedDict):
24
+ code: int
25
+ message: str
26
+
27
+
28
+ class ExFactorEntry(TypedDict):
29
+ ex_factor: float
30
+ timestamp: int
31
+
32
+
33
+ class ExFactorsResponse(TypedDict):
34
+ data: Dict[str, List[ExFactorEntry]]
35
+
36
+
37
+
38
+ class Instrument(TypedDict):
39
+ currency: NotRequired[str]
40
+ exchange: str
41
+ instrument_type: Literal[
42
+ "stock", "etf", "index", "bond", "fund", "futures", "options", "other"
43
+ ]
44
+ list_date: NotRequired[str]
45
+ name: str
46
+ region: Literal["CN", "US", "HK"]
47
+ symbol: str
48
+
49
+
50
+ class InstrumentsBatchRequest(TypedDict):
51
+ symbols: List[str]
52
+
53
+
54
+ class InstrumentsResponse(TypedDict):
55
+ data: List[Instrument]
56
+
57
+
58
+ class KlineBatchResponse(TypedDict):
59
+ data: Dict[str, CompactKlineData]
60
+
61
+
62
+ class KlineResponse(TypedDict):
63
+ data: CompactKlineData
64
+
65
+
66
+ class MarketDepth(TypedDict):
67
+ ask_prices: List[float]
68
+ ask_volumes: List[int]
69
+ bid_prices: List[float]
70
+ bid_volumes: List[int]
71
+ region: Literal["CN", "US", "HK"]
72
+ symbol: str
73
+ timestamp: int
74
+
75
+
76
+ class Quote(TypedDict):
77
+ amount: float
78
+ ext: NotRequired[Dict[str, Any]]
79
+ high: float
80
+ last_price: float
81
+ low: float
82
+ open: float
83
+ prev_close: float
84
+ region: Literal["CN", "US", "HK"]
85
+ symbol: str
86
+ timestamp: int
87
+ volume: int
88
+
89
+
90
+ class QuotesResponse(TypedDict):
91
+ data: List[Quote]
92
+
93
+
94
+ class RateLimitResponse(TypedDict):
95
+ code: int
96
+ message: str
97
+ retry_after_ms: int
98
+
99
+
100
+ class DepthResponse(TypedDict):
101
+ data: MarketDepth
@@ -0,0 +1,13 @@
1
+ """Resource modules for AlphaFeed API."""
2
+
3
+ from .depth import Depth
4
+ from .instruments import Instruments
5
+ from .klines import Klines
6
+ from .quotes import Quotes
7
+
8
+ __all__ = [
9
+ "Depth",
10
+ "Instruments",
11
+ "Klines",
12
+ "Quotes",
13
+ ]
@@ -0,0 +1,17 @@
1
+ """Base resource class for API resources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ if TYPE_CHECKING:
8
+ from .._base_client import SyncAPIClient
9
+
10
+
11
+ class SyncResource:
12
+ """Base class for synchronous API resources."""
13
+
14
+ _client: "SyncAPIClient"
15
+
16
+ def __init__(self, client: "SyncAPIClient") -> None:
17
+ self._client = client
@@ -0,0 +1,44 @@
1
+ """Market depth (order book) resources for AlphaFeed API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from ._base import SyncResource
8
+
9
+ if TYPE_CHECKING:
10
+ from ..models import MarketDepth
11
+
12
+
13
+ class Depth(SyncResource):
14
+ """Synchronous interface for market depth endpoint.
15
+
16
+ Examples
17
+ --------
18
+ >>> client = AlphaFeed(api_key="your-key")
19
+ >>> depth = client.depth.get("600000.SH")
20
+ >>> print(depth["bid_prices"], depth["ask_prices"])
21
+ """
22
+
23
+ def get(self, symbol: str) -> "MarketDepth":
24
+ """Get 5-level market depth for a single symbol.
25
+
26
+ Parameters
27
+ ----------
28
+ symbol : str
29
+ Symbol code (e.g. "600000.SH").
30
+
31
+ Returns
32
+ -------
33
+ MarketDepth
34
+ Market depth data with bid/ask prices and volumes.
35
+
36
+ Examples
37
+ --------
38
+ >>> depth = client.depth.get("600000.SH")
39
+ >>> for i in range(5):
40
+ ... print(f"Bid {i+1}: {depth['bid_prices'][i]} x {depth['bid_volumes'][i]}")
41
+ ... print(f"Ask {i+1}: {depth['ask_prices'][i]} x {depth['ask_volumes'][i]}")
42
+ """
43
+ response = self._client.get("/v1/depth", params={"symbol": symbol})
44
+ return response["data"]
@@ -0,0 +1,99 @@
1
+ """Instrument resources for AlphaFeed API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, List, Union, overload
6
+
7
+ from ._base import SyncResource
8
+
9
+ if TYPE_CHECKING:
10
+ from ..models import Instrument
11
+
12
+
13
+ class Instruments(SyncResource):
14
+ """Synchronous interface for instrument endpoints.
15
+
16
+ Examples
17
+ --------
18
+ >>> client = AlphaFeed(api_key="your-key")
19
+ >>> inst = client.instruments.get("600000.SH")
20
+ >>> print(f"{inst['symbol']}: {inst['name']}")
21
+ """
22
+
23
+ @overload
24
+ def get(self, symbol: str) -> "Instrument": ...
25
+
26
+ @overload
27
+ def get(self, symbol: List[str]) -> List["Instrument"]: ...
28
+
29
+ def get(
30
+ self, symbol: Union[str, List[str]]
31
+ ) -> Union["Instrument", List["Instrument"]]:
32
+ """Get metadata for one or more instruments.
33
+
34
+ Parameters
35
+ ----------
36
+ symbol : str or list of str
37
+ Instrument code(s). Can be a single symbol string or a list of symbols.
38
+
39
+ Returns
40
+ -------
41
+ Instrument or list of Instrument
42
+ If a single symbol is provided, returns a single Instrument dict.
43
+ If a list is provided, returns a list of Instrument dicts.
44
+
45
+ Each Instrument contains:
46
+ - symbol: Full symbol code (e.g., "600000.SH")
47
+ - code: Exchange-specific code (e.g., "600000")
48
+ - exchange: Exchange code (e.g., "SH")
49
+ - region: Region code (e.g., "CN")
50
+ - name: Instrument name
51
+ - instrument_type: Type (stock, etf, index, etc.)
52
+ - ext: Market-specific extension data
53
+
54
+ Examples
55
+ --------
56
+ >>> # Single instrument
57
+ >>> inst = client.instruments.get("600000.SH")
58
+ >>> print(inst['name'])
59
+
60
+ >>> # Multiple instruments
61
+ >>> insts = client.instruments.get(["600000.SH", "AAPL.US"])
62
+ >>> for i in insts:
63
+ ... print(f"{i['symbol']}: {i['name']}")
64
+ """
65
+ if isinstance(symbol, str):
66
+ response = self._client.get("/v1/instruments", params={"symbols": symbol})
67
+ data = response["data"]
68
+ return data[0] if data else {}
69
+ else:
70
+ response = self._client.post(
71
+ "/v1/instruments/batch", json={"symbols": symbol}
72
+ )
73
+ return response["data"]
74
+
75
+ def batch(self, symbols: List[str]) -> List["Instrument"]:
76
+ """Get metadata for multiple instruments.
77
+
78
+ This method uses POST to handle large batches without URL length limits.
79
+
80
+ Parameters
81
+ ----------
82
+ symbols : list of str
83
+ List of symbol codes (up to 1000).
84
+
85
+ Returns
86
+ -------
87
+ list of Instrument
88
+ List of instrument metadata dicts.
89
+
90
+ Examples
91
+ --------
92
+ >>> insts = client.instruments.batch(["600000.SH", "000001.SZ", "AAPL.US"])
93
+ >>> for i in insts:
94
+ ... print(f"{i['symbol']}: {i['name']}")
95
+ """
96
+ response = self._client.post(
97
+ "/v1/instruments/batch", json={"symbols": symbols}
98
+ )
99
+ return response["data"]