zzshare 0.1.2__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.
- zzshare/__init__.py +81 -0
- zzshare/client.py +579 -0
- zzshare/client.pyi +265 -0
- zzshare/core.py +93 -0
- zzshare/utils.py +45 -0
- zzshare-0.1.2.dist-info/METADATA +464 -0
- zzshare-0.1.2.dist-info/RECORD +10 -0
- zzshare-0.1.2.dist-info/WHEEL +5 -0
- zzshare-0.1.2.dist-info/licenses/LICENSE +21 -0
- zzshare-0.1.2.dist-info/top_level.txt +1 -0
zzshare/__init__.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from typing import Any, Callable, Optional
|
|
3
|
+
|
|
4
|
+
from zzshare.client import DataApi
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_default_api: Optional[DataApi] = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pro_api(token: str = "", timeout: int = 10, http_url: str = "https://api.zizizaizai.com") -> DataApi:
|
|
11
|
+
return DataApi(token=token, timeout=timeout, http_url=http_url)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_default_api() -> DataApi:
|
|
15
|
+
global _default_api
|
|
16
|
+
if _default_api is None:
|
|
17
|
+
_default_api = DataApi()
|
|
18
|
+
return _default_api
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def daily(
|
|
22
|
+
ts_code: Optional[str] = None,
|
|
23
|
+
trade_date: Optional[str] = None,
|
|
24
|
+
start_date: Optional[str] = None,
|
|
25
|
+
end_date: Optional[str] = None,
|
|
26
|
+
offset: Optional[int] = None,
|
|
27
|
+
limit: Optional[int] = None,
|
|
28
|
+
fields: Optional[str] = None,
|
|
29
|
+
**kwargs: Any,
|
|
30
|
+
):
|
|
31
|
+
return _get_default_api().daily(
|
|
32
|
+
ts_code=ts_code,
|
|
33
|
+
trade_date=trade_date,
|
|
34
|
+
start_date=start_date,
|
|
35
|
+
end_date=end_date,
|
|
36
|
+
offset=offset,
|
|
37
|
+
limit=limit,
|
|
38
|
+
fields=fields,
|
|
39
|
+
**kwargs,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def stock_basic(
|
|
44
|
+
ts_code: Optional[str] = None,
|
|
45
|
+
exchange: Optional[str] = None,
|
|
46
|
+
list_status: str = "L",
|
|
47
|
+
is_hs: Optional[str] = None,
|
|
48
|
+
fields: Optional[str] = None,
|
|
49
|
+
name: Optional[str] = None,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
):
|
|
52
|
+
return _get_default_api().stock_basic(
|
|
53
|
+
ts_code=ts_code,
|
|
54
|
+
exchange=exchange,
|
|
55
|
+
list_status=list_status,
|
|
56
|
+
is_hs=is_hs,
|
|
57
|
+
fields=fields,
|
|
58
|
+
name=name,
|
|
59
|
+
**kwargs,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def query(api_name: str, params: Optional[dict] = None):
|
|
64
|
+
return _get_default_api().query(api_name, params=params)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _create_shortcut_proxy(name: str) -> Callable[..., Any]:
|
|
68
|
+
def _proxy(*args: Any, **kwargs: Any) -> Any:
|
|
69
|
+
return getattr(_get_default_api(), name)(*args, **kwargs)
|
|
70
|
+
|
|
71
|
+
_proxy.__name__ = name
|
|
72
|
+
return _proxy
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
for _shortcut_name in DataApi.SHORTCUTS:
|
|
76
|
+
if _shortcut_name in {"daily", "stock_basic"}:
|
|
77
|
+
continue
|
|
78
|
+
globals()[_shortcut_name] = _create_shortcut_proxy(_shortcut_name)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["DataApi", "pro_api", "daily", "stock_basic", "query", *[name for name in DataApi.SHORTCUTS if name != "daily"]]
|
zzshare/client.py
ADDED
|
@@ -0,0 +1,579 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from typing import Any, Optional, Dict, Callable, List, Union
|
|
3
|
+
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from zzshare.core import BaseDataApi
|
|
6
|
+
from zzshare.utils import kline_data_to_df
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataApi(BaseDataApi):
|
|
10
|
+
def __init__(self, token: str = '', timeout: int = 10, http_url: str = 'https://api.zizizaizai.com'):
|
|
11
|
+
super().__init__(token, timeout, http_url)
|
|
12
|
+
|
|
13
|
+
SHORTCUTS = {
|
|
14
|
+
# Daily
|
|
15
|
+
"uplimit_hot": (
|
|
16
|
+
"open/review/uplimit/hot",
|
|
17
|
+
["date1", "board"],
|
|
18
|
+
None
|
|
19
|
+
),
|
|
20
|
+
"uplimit_stocks": (
|
|
21
|
+
"open/review/uplimit/stocks/{date1}",
|
|
22
|
+
["date1"],
|
|
23
|
+
None
|
|
24
|
+
),
|
|
25
|
+
# Sentiment Data
|
|
26
|
+
"market_plate_stocks": (
|
|
27
|
+
"market/plates/17/{plate_code}/stocks/rank",
|
|
28
|
+
["plate_code", "date1", "is_real", "limit"],
|
|
29
|
+
None
|
|
30
|
+
),
|
|
31
|
+
"market_plate": (
|
|
32
|
+
"market/plates/17/rank",
|
|
33
|
+
["date1", "limit"],
|
|
34
|
+
None
|
|
35
|
+
),
|
|
36
|
+
"market_sentiment": (
|
|
37
|
+
"v2/api/sentiment/kline/day/0",
|
|
38
|
+
["date1", "date2"],
|
|
39
|
+
None
|
|
40
|
+
),
|
|
41
|
+
"market_hot_sentiment": (
|
|
42
|
+
"v2/api/sentiment/kline/day/20",
|
|
43
|
+
["date1", "date2"],
|
|
44
|
+
None
|
|
45
|
+
),
|
|
46
|
+
"market_style": (
|
|
47
|
+
"v2/api/timing/market/style",
|
|
48
|
+
["date1"],
|
|
49
|
+
None
|
|
50
|
+
),
|
|
51
|
+
"open_sentiment_data": (
|
|
52
|
+
"open/sentiment/data",
|
|
53
|
+
["date1", "date2"],
|
|
54
|
+
None
|
|
55
|
+
),
|
|
56
|
+
# Kline
|
|
57
|
+
"daily": (
|
|
58
|
+
"open/kline/d/{code}",
|
|
59
|
+
["code", "date1", "date2"],
|
|
60
|
+
kline_data_to_df
|
|
61
|
+
),
|
|
62
|
+
# Base Data
|
|
63
|
+
"trade_days": (
|
|
64
|
+
"market/trade/days",
|
|
65
|
+
["day_start", "day_end", "days"],
|
|
66
|
+
None
|
|
67
|
+
),
|
|
68
|
+
# Third Party
|
|
69
|
+
"ths_hot_top": (
|
|
70
|
+
"open/sentiment/media/ths2/top",
|
|
71
|
+
["date1", "top_n"],
|
|
72
|
+
None
|
|
73
|
+
),
|
|
74
|
+
"stock_ths_hot": (
|
|
75
|
+
"v2/api/sentiment/media/ths/symbol/{code}",
|
|
76
|
+
["code", "date1"],
|
|
77
|
+
None
|
|
78
|
+
),
|
|
79
|
+
"sentiment_market_hot_day": (
|
|
80
|
+
"v3/api/sentiment/market/hot/day",
|
|
81
|
+
["date"],
|
|
82
|
+
None
|
|
83
|
+
),
|
|
84
|
+
"sentiment_trend": (
|
|
85
|
+
"v3/api/sentiment/trend/{model}",
|
|
86
|
+
["model", "date1"],
|
|
87
|
+
None
|
|
88
|
+
),
|
|
89
|
+
"sentiment_trend_range": (
|
|
90
|
+
"v3/api/sentiment/trend/{model}/range",
|
|
91
|
+
["model", "date1", "date2"],
|
|
92
|
+
None
|
|
93
|
+
),
|
|
94
|
+
"review_uplimit_reason": (
|
|
95
|
+
"v3/api/review/uplimit/reason",
|
|
96
|
+
["date1", "group", "page", "page_size"],
|
|
97
|
+
None
|
|
98
|
+
),
|
|
99
|
+
"review_uplimit_hot_open": (
|
|
100
|
+
"v3/open/review/uplimit/hot",
|
|
101
|
+
["date1", "date2", "board", "limit"],
|
|
102
|
+
None
|
|
103
|
+
),
|
|
104
|
+
"stock_uplimit_reason": (
|
|
105
|
+
"v3/open/stock/uplimit/reason/{stock_code}",
|
|
106
|
+
["stock_code", "date"],
|
|
107
|
+
None
|
|
108
|
+
),
|
|
109
|
+
"stock_uplimit_reason_history": (
|
|
110
|
+
"v3/open/stock/uplimit/reason/history/{stock_code}", ["stock_code", "page", "pageSize"], None),
|
|
111
|
+
"review_uplimit_reason_open": (
|
|
112
|
+
"v3/open/review/uplimit/reason",
|
|
113
|
+
["date1"],
|
|
114
|
+
None
|
|
115
|
+
),
|
|
116
|
+
"stock_info": (
|
|
117
|
+
"v3/open/stock/info",
|
|
118
|
+
["stock_id", "info_type"],
|
|
119
|
+
None
|
|
120
|
+
),
|
|
121
|
+
# ================== 新增接口 ==================
|
|
122
|
+
# 龙虎榜数据
|
|
123
|
+
"lhb_list": (
|
|
124
|
+
"market/lhb/list",
|
|
125
|
+
["date1"],
|
|
126
|
+
None
|
|
127
|
+
),
|
|
128
|
+
"lhb_detail": (
|
|
129
|
+
"market/lhb/detail",
|
|
130
|
+
["date1", "stock_code"],
|
|
131
|
+
None
|
|
132
|
+
),
|
|
133
|
+
"lhb_stock_history": (
|
|
134
|
+
"market/lhb/stock/history",
|
|
135
|
+
["stock_code", "trader_name"],
|
|
136
|
+
None
|
|
137
|
+
),
|
|
138
|
+
"lhb_trader_history": (
|
|
139
|
+
"market/lhb/trader/history",
|
|
140
|
+
["trader_name", "trader_id", "stock_code", "page", "per_page"],
|
|
141
|
+
None
|
|
142
|
+
),
|
|
143
|
+
# 板块数据
|
|
144
|
+
"plates_list": (
|
|
145
|
+
"market/plates/{plate_type}",
|
|
146
|
+
["plate_type"],
|
|
147
|
+
None
|
|
148
|
+
),
|
|
149
|
+
"plates_rank": (
|
|
150
|
+
"market/plates/{plate_type}/rank",
|
|
151
|
+
["plate_type", "date1", "limit"],
|
|
152
|
+
None
|
|
153
|
+
),
|
|
154
|
+
"plates_trend": (
|
|
155
|
+
"market/plates/{plate_type}/trend",
|
|
156
|
+
["plate_type", "plate_code", "day_start", "day_end"],
|
|
157
|
+
None
|
|
158
|
+
),
|
|
159
|
+
"plates_stocks": (
|
|
160
|
+
"market/plates/{plate_type}/{plate_code}/stocks",
|
|
161
|
+
["plate_type", "plate_code", "date"],
|
|
162
|
+
None
|
|
163
|
+
),
|
|
164
|
+
# 涨跌分布与情绪
|
|
165
|
+
"updown_distribution": (
|
|
166
|
+
"open/sentiment/updown/disctribution",
|
|
167
|
+
["date1"],
|
|
168
|
+
None
|
|
169
|
+
),
|
|
170
|
+
"uplimit_trend": (
|
|
171
|
+
"open/sentiment/uplimit/trend",
|
|
172
|
+
["date1"],
|
|
173
|
+
None
|
|
174
|
+
),
|
|
175
|
+
"sentiment_hot_day": (
|
|
176
|
+
"open/sentiment/hot/day",
|
|
177
|
+
["index", "st"],
|
|
178
|
+
None
|
|
179
|
+
),
|
|
180
|
+
"sentiment_level": (
|
|
181
|
+
"open/sentiment/level",
|
|
182
|
+
["date"],
|
|
183
|
+
None
|
|
184
|
+
),
|
|
185
|
+
"sentiment_bull_data": (
|
|
186
|
+
"open/sentiment/bull/data",
|
|
187
|
+
["date1", "date2"],
|
|
188
|
+
None
|
|
189
|
+
),
|
|
190
|
+
# 行情实时数据
|
|
191
|
+
"market_real": (
|
|
192
|
+
"open/market/real",
|
|
193
|
+
["symbols"],
|
|
194
|
+
None
|
|
195
|
+
),
|
|
196
|
+
"stock_moneyflow": (
|
|
197
|
+
"open/stock/{stock_id}/moneyflow",
|
|
198
|
+
["stock_id", "m_type"],
|
|
199
|
+
None
|
|
200
|
+
),
|
|
201
|
+
"market_mf": (
|
|
202
|
+
"open/market/mf",
|
|
203
|
+
["stock", "date", "wm", "default_v"],
|
|
204
|
+
None
|
|
205
|
+
),
|
|
206
|
+
# 涨停市值统计
|
|
207
|
+
"uplimit_market_value": (
|
|
208
|
+
"v2/api/uplimit/market/value",
|
|
209
|
+
["date1", "date2"],
|
|
210
|
+
None
|
|
211
|
+
),
|
|
212
|
+
# 市场TopN情绪
|
|
213
|
+
"sentiment_market_top_n": (
|
|
214
|
+
"v2/api/sentiment/market/top/n",
|
|
215
|
+
["modal_id", "date1", "date2"],
|
|
216
|
+
None
|
|
217
|
+
),
|
|
218
|
+
# 异动数据
|
|
219
|
+
"movement_alerts": (
|
|
220
|
+
"market/movement/alerts",
|
|
221
|
+
["date1", "type", "limit", "is_real"],
|
|
222
|
+
None
|
|
223
|
+
),
|
|
224
|
+
# 监控数据
|
|
225
|
+
"zdjk_get": (
|
|
226
|
+
"open/zdjk/get",
|
|
227
|
+
["date1", "date2"],
|
|
228
|
+
None
|
|
229
|
+
),
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
def _register_shortcuts(self):
|
|
233
|
+
"""根据 SHORTCUTS 表动态生成方法"""
|
|
234
|
+
for name, (path_template, param_names, post_process) in self.SHORTCUTS.items():
|
|
235
|
+
if name == "daily":
|
|
236
|
+
continue
|
|
237
|
+
def make_method(
|
|
238
|
+
template: str = path_template,
|
|
239
|
+
params_list: List[str] = param_names,
|
|
240
|
+
processor: Optional[Callable[[Optional[Dict]], Any]] = post_process
|
|
241
|
+
):
|
|
242
|
+
def shortcut_method(**kwargs) -> Any:
|
|
243
|
+
path = template
|
|
244
|
+
# 先处理路径参数:从 kwargs 中 pop 并替换 {xxx}
|
|
245
|
+
for param in params_list:
|
|
246
|
+
placeholder = f"{{{param}}}"
|
|
247
|
+
if placeholder in path:
|
|
248
|
+
if param not in kwargs:
|
|
249
|
+
raise ValueError(f"缺少路径参数 '{param}' for {name}")
|
|
250
|
+
value = kwargs.pop(param)
|
|
251
|
+
path = path.replace(placeholder, str(value))
|
|
252
|
+
|
|
253
|
+
# 剩下的 kwargs 作为 query 参数
|
|
254
|
+
data = self._query(path.lstrip('/'), params=kwargs)
|
|
255
|
+
|
|
256
|
+
# 后处理
|
|
257
|
+
if processor:
|
|
258
|
+
return processor(data)
|
|
259
|
+
return data
|
|
260
|
+
|
|
261
|
+
# 绑定方法名和文档
|
|
262
|
+
shortcut_method.__name__ = name
|
|
263
|
+
shortcut_method.__doc__ = (
|
|
264
|
+
f"快捷调用:{template}\n"
|
|
265
|
+
f"参数:{', '.join(params_list)}(路径参数会自动替换)\n"
|
|
266
|
+
f"后处理:{processor.__name__ if processor else '无'}"
|
|
267
|
+
)
|
|
268
|
+
setattr(self, name, shortcut_method)
|
|
269
|
+
|
|
270
|
+
make_method()
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def _normalize_symbol(symbol: str) -> str:
|
|
274
|
+
return symbol.split(".")[0] if "." in symbol else symbol
|
|
275
|
+
|
|
276
|
+
@staticmethod
|
|
277
|
+
def _to_tushare_ts_code(symbol: str) -> str:
|
|
278
|
+
normalized = symbol.strip().upper()
|
|
279
|
+
if "." in normalized:
|
|
280
|
+
code, suffix = normalized.split(".", 1)
|
|
281
|
+
suffix_map = {
|
|
282
|
+
"SS": "SH",
|
|
283
|
+
"SH": "SH",
|
|
284
|
+
"XSHG": "SH",
|
|
285
|
+
"SZ": "SZ",
|
|
286
|
+
"XSHE": "SZ",
|
|
287
|
+
"BJ": "BJ",
|
|
288
|
+
"BSE": "BJ",
|
|
289
|
+
}
|
|
290
|
+
return f"{code}.{suffix_map.get(suffix, suffix)}"
|
|
291
|
+
if normalized.startswith(("6", "5")):
|
|
292
|
+
return f"{normalized}.SH"
|
|
293
|
+
if normalized.startswith(("0", "3")):
|
|
294
|
+
return f"{normalized}.SZ"
|
|
295
|
+
if normalized.startswith(("8", "4", "2", "9")):
|
|
296
|
+
return f"{normalized}.BJ"
|
|
297
|
+
return normalized
|
|
298
|
+
|
|
299
|
+
@staticmethod
|
|
300
|
+
def _to_tushare_exchange(symbol: str) -> str:
|
|
301
|
+
code = symbol.split(".")[0] if "." in symbol else symbol
|
|
302
|
+
if code.startswith(("6", "5")):
|
|
303
|
+
return "SSE"
|
|
304
|
+
if code.startswith(("0", "3")):
|
|
305
|
+
return "SZSE"
|
|
306
|
+
if code.startswith(("8", "4", "2", "9")):
|
|
307
|
+
return "BSE"
|
|
308
|
+
return ""
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def _to_backend_exchange(exchange: Optional[str]) -> Optional[str]:
|
|
312
|
+
if not exchange:
|
|
313
|
+
return None
|
|
314
|
+
mapping = {
|
|
315
|
+
"SSE": "SS",
|
|
316
|
+
"SZSE": "SZ",
|
|
317
|
+
"BSE": "BJ",
|
|
318
|
+
"SH": "SS",
|
|
319
|
+
"SZ": "SZ",
|
|
320
|
+
"BJ": "BJ",
|
|
321
|
+
"GEM": "GEM",
|
|
322
|
+
"KSH": "KSH",
|
|
323
|
+
"STAR": "KSH",
|
|
324
|
+
"SS": "SS",
|
|
325
|
+
"ALL": "ALL",
|
|
326
|
+
}
|
|
327
|
+
return mapping.get(exchange.upper())
|
|
328
|
+
|
|
329
|
+
def daily(
|
|
330
|
+
self,
|
|
331
|
+
ts_code: Optional[str] = None,
|
|
332
|
+
trade_date: Optional[str] = None,
|
|
333
|
+
start_date: Optional[str] = None,
|
|
334
|
+
end_date: Optional[str] = None,
|
|
335
|
+
offset: Optional[int] = None,
|
|
336
|
+
limit: Optional[int] = None,
|
|
337
|
+
fields: Optional[str] = None,
|
|
338
|
+
**kwargs: Any
|
|
339
|
+
):
|
|
340
|
+
use_ts_code = self._to_tushare_ts_code(ts_code) if ts_code else None
|
|
341
|
+
normalized_trade_date = trade_date.replace("-", "") if trade_date else None
|
|
342
|
+
normalized_start = start_date.replace("-", "") if start_date else None
|
|
343
|
+
normalized_end = end_date.replace("-", "") if end_date else None
|
|
344
|
+
params: Dict[str, Any] = {}
|
|
345
|
+
adj = str(kwargs.pop("adj", "")).lower()
|
|
346
|
+
candle_mode = kwargs.pop("candle_mode", None)
|
|
347
|
+
if candle_mode is None:
|
|
348
|
+
if adj == "qfq":
|
|
349
|
+
candle_mode = 1
|
|
350
|
+
elif adj == "hfq":
|
|
351
|
+
candle_mode = 2
|
|
352
|
+
else:
|
|
353
|
+
candle_mode = 0
|
|
354
|
+
params["candle_mode"] = candle_mode
|
|
355
|
+
|
|
356
|
+
if not use_ts_code:
|
|
357
|
+
if not normalized_trade_date:
|
|
358
|
+
raise ValueError("当 ts_code 为空时,trade_date 不能为空")
|
|
359
|
+
params["trade_date"] = normalized_trade_date
|
|
360
|
+
if offset is not None:
|
|
361
|
+
params["offset"] = offset
|
|
362
|
+
if limit is not None:
|
|
363
|
+
params["limit"] = limit
|
|
364
|
+
url = f"{self.http_url}/v3/market/kline/day"
|
|
365
|
+
else:
|
|
366
|
+
if normalized_trade_date:
|
|
367
|
+
params["get_type"] = "range"
|
|
368
|
+
params["start_date"] = normalized_trade_date
|
|
369
|
+
params["end_date"] = normalized_trade_date
|
|
370
|
+
else:
|
|
371
|
+
params["get_type"] = "range"
|
|
372
|
+
if normalized_start:
|
|
373
|
+
params["start_date"] = normalized_start
|
|
374
|
+
if normalized_end:
|
|
375
|
+
params["end_date"] = normalized_end
|
|
376
|
+
url = f"{self.http_url}/v3/market/kline/day/{use_ts_code}"
|
|
377
|
+
|
|
378
|
+
params.update(kwargs)
|
|
379
|
+
data: Optional[Union[Dict[str, Any], List[Any]]] = None
|
|
380
|
+
try:
|
|
381
|
+
res = requests.get(url, params=params, headers=self.headers, timeout=self.timeout)
|
|
382
|
+
if res.status_code == 200:
|
|
383
|
+
body = res.json()
|
|
384
|
+
if isinstance(body, dict):
|
|
385
|
+
if body.get("code") == 200:
|
|
386
|
+
data = body.get("data")
|
|
387
|
+
elif "data" in body:
|
|
388
|
+
data = body.get("data")
|
|
389
|
+
else:
|
|
390
|
+
data = body
|
|
391
|
+
except Exception:
|
|
392
|
+
data = None
|
|
393
|
+
records: List[Dict[str, Any]] = []
|
|
394
|
+
data_ts_code: Optional[str] = None
|
|
395
|
+
if isinstance(data, list):
|
|
396
|
+
records = [item for item in data if isinstance(item, dict)]
|
|
397
|
+
elif isinstance(data, dict):
|
|
398
|
+
data_ts_code = data.get("ts_code")
|
|
399
|
+
list_data = data.get("list")
|
|
400
|
+
if isinstance(list_data, list):
|
|
401
|
+
records = [item for item in list_data if isinstance(item, dict)]
|
|
402
|
+
|
|
403
|
+
if records:
|
|
404
|
+
normalized_rows: List[Dict[str, Any]] = []
|
|
405
|
+
for row in records:
|
|
406
|
+
row_ts_code = row.get("ts_code") or row.get("symbol") or row.get("code") or data_ts_code or use_ts_code
|
|
407
|
+
normalized_rows.append({
|
|
408
|
+
"ts_code": self._to_tushare_ts_code(str(row_ts_code)) if row_ts_code else None,
|
|
409
|
+
"trade_date": str(row.get("trade_date") or row.get("date") or row.get("day") or "").replace("-", ""),
|
|
410
|
+
"open": row.get("open", row.get("o")),
|
|
411
|
+
"high": row.get("high", row.get("h")),
|
|
412
|
+
"low": row.get("low", row.get("l")),
|
|
413
|
+
"close": row.get("close", row.get("c")),
|
|
414
|
+
"pre_close": row.get("pre_close", row.get("prev_close")),
|
|
415
|
+
"change": row.get("change"),
|
|
416
|
+
"pct_chg": row.get("pct_chg", row.get("pct_change", row.get("quote_rate"))),
|
|
417
|
+
"vol": row.get("vol", row.get("volume")),
|
|
418
|
+
"amount": row.get("amount", row.get("turnover")),
|
|
419
|
+
})
|
|
420
|
+
df = pd.DataFrame(normalized_rows)
|
|
421
|
+
else:
|
|
422
|
+
df = pd.DataFrame()
|
|
423
|
+
|
|
424
|
+
if df.empty:
|
|
425
|
+
default_columns = [
|
|
426
|
+
"ts_code", "trade_date", "open", "high", "low", "close",
|
|
427
|
+
"pre_close", "change", "pct_chg", "vol", "amount"
|
|
428
|
+
]
|
|
429
|
+
return df.reindex(columns=default_columns)
|
|
430
|
+
|
|
431
|
+
if "ts_code" not in df.columns:
|
|
432
|
+
df["ts_code"] = use_ts_code
|
|
433
|
+
else:
|
|
434
|
+
df["ts_code"] = df["ts_code"].apply(lambda x: self._to_tushare_ts_code(str(x)) if pd.notna(x) and str(x) else "")
|
|
435
|
+
if "trade_date" in df.columns:
|
|
436
|
+
df["trade_date"] = df["trade_date"].astype(str).str.replace("-", "", regex=False)
|
|
437
|
+
numeric_columns = ["open", "high", "low", "close", "pre_close", "change", "pct_chg", "vol", "amount"]
|
|
438
|
+
for col in numeric_columns:
|
|
439
|
+
if col in df.columns:
|
|
440
|
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
441
|
+
if "change" in df.columns and "pre_close" in df.columns and "close" in df.columns:
|
|
442
|
+
missing_change = df["change"].isna()
|
|
443
|
+
df.loc[missing_change, "change"] = df.loc[missing_change, "close"] - df.loc[missing_change, "pre_close"]
|
|
444
|
+
if "pct_chg" in df.columns and "change" in df.columns and "pre_close" in df.columns:
|
|
445
|
+
missing_pct = df["pct_chg"].isna()
|
|
446
|
+
valid_pre_close = df["pre_close"] != 0
|
|
447
|
+
fill_mask = missing_pct & valid_pre_close
|
|
448
|
+
df.loc[fill_mask, "pct_chg"] = (df.loc[fill_mask, "change"] / df.loc[fill_mask, "pre_close"]) * 100
|
|
449
|
+
|
|
450
|
+
ordered_columns = [
|
|
451
|
+
"ts_code", "trade_date", "open", "high", "low", "close",
|
|
452
|
+
"pre_close", "change", "pct_chg", "vol", "amount"
|
|
453
|
+
]
|
|
454
|
+
df = df.reindex(columns=ordered_columns)
|
|
455
|
+
df = df.sort_values(by="trade_date", ascending=False).reset_index(drop=True)
|
|
456
|
+
if use_ts_code:
|
|
457
|
+
if offset is not None:
|
|
458
|
+
df = df.iloc[offset:]
|
|
459
|
+
if limit is not None:
|
|
460
|
+
df = df.head(limit)
|
|
461
|
+
|
|
462
|
+
if fields:
|
|
463
|
+
requested_fields = [field.strip() for field in fields.split(",") if field.strip()]
|
|
464
|
+
selected_fields = [field for field in requested_fields if field in df.columns]
|
|
465
|
+
if selected_fields:
|
|
466
|
+
return df[selected_fields]
|
|
467
|
+
return df.iloc[:, 0:0]
|
|
468
|
+
|
|
469
|
+
return df
|
|
470
|
+
|
|
471
|
+
def stock_basic(
|
|
472
|
+
self,
|
|
473
|
+
ts_code: Optional[str] = None,
|
|
474
|
+
exchange: Optional[str] = None,
|
|
475
|
+
list_status: str = "L",
|
|
476
|
+
is_hs: Optional[str] = None,
|
|
477
|
+
fields: Optional[str] = None,
|
|
478
|
+
name: Optional[str] = None,
|
|
479
|
+
**kwargs: Any
|
|
480
|
+
):
|
|
481
|
+
requested_status = (list_status or "L").upper()
|
|
482
|
+
if requested_status not in {"L", "D", "P"}:
|
|
483
|
+
raise ValueError("list_status 仅支持 L/D/P")
|
|
484
|
+
|
|
485
|
+
backend_exchange = self._to_backend_exchange(exchange)
|
|
486
|
+
if exchange and backend_exchange is None:
|
|
487
|
+
raise ValueError("exchange 仅支持 SSE/SZSE/BSE/SH/SZ/BJ/GEM/KSH/STAR/SS/ALL")
|
|
488
|
+
|
|
489
|
+
if requested_status == "P":
|
|
490
|
+
df_empty = pd.DataFrame(columns=[
|
|
491
|
+
"ts_code", "symbol", "name", "area", "industry", "fullname", "enname",
|
|
492
|
+
"cnspell", "market", "exchange", "curr_type", "list_status",
|
|
493
|
+
"list_date", "delist_date", "is_hs"
|
|
494
|
+
])
|
|
495
|
+
if fields:
|
|
496
|
+
requested_fields = [field.strip() for field in fields.split(",") if field.strip()]
|
|
497
|
+
selected_fields = [field for field in requested_fields if field in df_empty.columns]
|
|
498
|
+
if selected_fields:
|
|
499
|
+
return df_empty[selected_fields]
|
|
500
|
+
return df_empty.iloc[:, 0:0]
|
|
501
|
+
return df_empty
|
|
502
|
+
|
|
503
|
+
query_status = requested_status if requested_status in {"L", "D"} else "ALL"
|
|
504
|
+
exchange_list = [backend_exchange] if backend_exchange else ["SS", "KSH", "SZ", "GEM", "BJ"]
|
|
505
|
+
rows: List[Dict[str, Any]] = []
|
|
506
|
+
for ex in exchange_list:
|
|
507
|
+
data = self._query(
|
|
508
|
+
"v3/open/stocks/list",
|
|
509
|
+
params={
|
|
510
|
+
"exchange": ex,
|
|
511
|
+
"list_status": query_status,
|
|
512
|
+
"format": "records"
|
|
513
|
+
}
|
|
514
|
+
)
|
|
515
|
+
if isinstance(data, dict):
|
|
516
|
+
batch = data.get("list") or []
|
|
517
|
+
if isinstance(batch, list):
|
|
518
|
+
rows.extend(batch)
|
|
519
|
+
|
|
520
|
+
normalized_codes: Optional[List[str]] = None
|
|
521
|
+
if ts_code:
|
|
522
|
+
normalized_codes = [self._normalize_symbol(item.strip()) for item in ts_code.split(",") if item.strip()]
|
|
523
|
+
|
|
524
|
+
result_rows: List[Dict[str, Any]] = []
|
|
525
|
+
for row in rows:
|
|
526
|
+
code = str(row.get("code", "")).strip()
|
|
527
|
+
if not code:
|
|
528
|
+
continue
|
|
529
|
+
if normalized_codes is not None and code not in normalized_codes:
|
|
530
|
+
continue
|
|
531
|
+
row_name = str(row.get("name", "")).strip()
|
|
532
|
+
if name and name not in row_name:
|
|
533
|
+
continue
|
|
534
|
+
|
|
535
|
+
ex_name = self._to_tushare_exchange(code)
|
|
536
|
+
type_code = str(row.get("type_code", "")).upper()
|
|
537
|
+
market_name = "创业板" if type_code == "GEM" else "科创板" if "KSH" in type_code else ""
|
|
538
|
+
result_rows.append({
|
|
539
|
+
"ts_code": self._to_tushare_ts_code(code),
|
|
540
|
+
"symbol": code,
|
|
541
|
+
"name": row_name,
|
|
542
|
+
"area": "",
|
|
543
|
+
"industry": "",
|
|
544
|
+
"fullname": row_name,
|
|
545
|
+
"enname": "",
|
|
546
|
+
"cnspell": "",
|
|
547
|
+
"market": market_name,
|
|
548
|
+
"exchange": ex_name,
|
|
549
|
+
"curr_type": "CNY",
|
|
550
|
+
"list_status": str(row.get("list_status", query_status)).upper(),
|
|
551
|
+
"list_date": "",
|
|
552
|
+
"delist_date": "",
|
|
553
|
+
"is_hs": "",
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
df = pd.DataFrame(result_rows)
|
|
557
|
+
ordered_columns = [
|
|
558
|
+
"ts_code", "symbol", "name", "area", "industry", "fullname", "enname",
|
|
559
|
+
"cnspell", "market", "exchange", "curr_type", "list_status",
|
|
560
|
+
"list_date", "delist_date", "is_hs"
|
|
561
|
+
]
|
|
562
|
+
if df.empty:
|
|
563
|
+
df = pd.DataFrame(columns=ordered_columns)
|
|
564
|
+
else:
|
|
565
|
+
df = df[ordered_columns].drop_duplicates(subset=["ts_code"]).reset_index(drop=True)
|
|
566
|
+
|
|
567
|
+
if is_hs:
|
|
568
|
+
flag = is_hs.upper()
|
|
569
|
+
if flag in {"H", "S"}:
|
|
570
|
+
df = df.iloc[0:0]
|
|
571
|
+
|
|
572
|
+
if fields:
|
|
573
|
+
requested_fields = [field.strip() for field in fields.split(",") if field.strip()]
|
|
574
|
+
selected_fields = [field for field in requested_fields if field in df.columns]
|
|
575
|
+
if selected_fields:
|
|
576
|
+
return df[selected_fields]
|
|
577
|
+
return df.iloc[:, 0:0]
|
|
578
|
+
|
|
579
|
+
return df
|