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.
- alphafeed/__init__.py +53 -0
- alphafeed/__version__.py +3 -0
- alphafeed/_base_client.py +305 -0
- alphafeed/_batch.py +87 -0
- alphafeed/_cache.py +130 -0
- alphafeed/_exceptions.py +140 -0
- alphafeed/_types.py +55 -0
- alphafeed/client.py +140 -0
- alphafeed/models.py +101 -0
- alphafeed/resources/__init__.py +13 -0
- alphafeed/resources/_base.py +17 -0
- alphafeed/resources/depth.py +44 -0
- alphafeed/resources/instruments.py +99 -0
- alphafeed/resources/klines.py +724 -0
- alphafeed/resources/quotes.py +257 -0
- alphafeed/utils.py +53 -0
- alphafeed-0.1.0.dev0.dist-info/METADATA +175 -0
- alphafeed-0.1.0.dev0.dist-info/RECORD +20 -0
- alphafeed-0.1.0.dev0.dist-info/WHEEL +5 -0
- alphafeed-0.1.0.dev0.dist-info/top_level.txt +1 -0
alphafeed/_exceptions.py
ADDED
|
@@ -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,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"]
|