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 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