rquote 0.4.3__py3-none-any.whl → 0.4.6__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.
rquote/__init__.py CHANGED
@@ -23,7 +23,8 @@ from .api import (
23
23
  get_us_stocks,
24
24
  get_cn_fund_list,
25
25
  get_tick,
26
- get_industry_stocks
26
+ get_industry_stocks,
27
+ get_cnindex_stocks
27
28
  )
28
29
 
29
30
  # 工具类
@@ -89,6 +90,7 @@ __all__ = [
89
90
  'get_cn_fund_list',
90
91
  'get_tick',
91
92
  'get_industry_stocks',
93
+ 'get_cnindex_stocks',
92
94
  # 工具类
93
95
  'WebUtils',
94
96
  'BasicFactors',
rquote/api/__init__.py CHANGED
@@ -10,7 +10,8 @@ from .lists import (
10
10
  get_cn_fund_list,
11
11
  get_cn_future_list,
12
12
  get_all_industries,
13
- get_industry_stocks
13
+ get_industry_stocks,
14
+ get_cnindex_stocks
14
15
  )
15
16
  from .tick import get_tick
16
17
  from .stock_info import get_stock_concepts, get_stock_industry
@@ -27,6 +28,7 @@ __all__ = [
27
28
  'get_stock_concepts',
28
29
  'get_stock_industry',
29
30
  'get_all_industries',
30
- 'get_industry_stocks'
31
+ 'get_industry_stocks',
32
+ 'get_cnindex_stocks'
31
33
  ]
32
34
 
rquote/api/lists.py CHANGED
@@ -6,6 +6,7 @@ import json
6
6
  import re
7
7
  import base64
8
8
  import time
9
+ from urllib.parse import urlencode
9
10
  from ..utils import hget, logger
10
11
  from ..exceptions import HTTPError
11
12
 
@@ -78,7 +79,7 @@ def get_us_stocks(k=100):
78
79
 
79
80
  def get_cn_fund_list():
80
81
  """
81
- 获取A股ETF基金列表
82
+ 获取A股ETF基金列表(sina)
82
83
 
83
84
  Returns:
84
85
  基金列表,格式: [code, name, change, amount, price]
@@ -173,3 +174,66 @@ def get_industry_stocks(node):
173
174
  data = json.loads(a.text)
174
175
  return data
175
176
 
177
+
178
+ def get_cnindex_stocks(index_type='hs300'):
179
+ """
180
+ 获取中国指数成分股列表
181
+
182
+ Args:
183
+ index_type: 指数类型,可选值: 'hs300', 'zz500', 'zz1000'
184
+ hs300: 沪深300 (TYPE=1)
185
+ zz500: 中证500 (TYPE=3)
186
+ zz1000: 中证1000 (TYPE=7)
187
+
188
+ Returns:
189
+ 股票列表,包含 SECUCODE, SECURITY_CODE, SECURITY_NAME_ABBR, CLOSE_PRICE 等字段
190
+ """
191
+ # 指数类型到 TYPE 值的映射
192
+ index_type_map = {
193
+ 'hs300': '1',
194
+ 'zz500': '3',
195
+ 'zz1000': '7'
196
+ }
197
+
198
+ if index_type not in index_type_map:
199
+ raise ValueError(f"不支持的指数类型: {index_type},支持的类型: {list(index_type_map.keys())}")
200
+
201
+ type_value = index_type_map[index_type]
202
+
203
+ # 构建 URL
204
+ base_url = 'https://datacenter-web.eastmoney.com/api/data/v1/get'
205
+ params = {
206
+ 'callback': 'jQuery112308471143523381743_1763517709888',
207
+ 'sortColumns': 'SECURITY_CODE',
208
+ 'sortTypes': '-1',
209
+ 'pageSize': '500',
210
+ 'pageNumber': '1',
211
+ 'reportName': 'RPT_INDEX_TS_COMPONENT',
212
+ 'columns': 'SECURITY_CODE,SECURITY_NAME_ABBR,INDUSTRY,WEIGHT,EPS,BPS,ROE,FREE_CAP',
213
+ 'quoteColumns': '',
214
+ 'quoteType': '0',
215
+ 'source': 'WEB',
216
+ 'client': 'WEB',
217
+ 'filter': f'(TYPE="{type_value}")'
218
+ }
219
+
220
+ # 构建完整 URL
221
+ url = f'{base_url}?{urlencode(params)}'
222
+
223
+ # 发送请求
224
+ a = hget(url)
225
+ if not a:
226
+ raise HTTPError(f'Failed to fetch {index_type} stocks from EastMoney')
227
+
228
+ # 解析 JSONP 格式的返回数据
229
+ # 格式: jQuery112308471143523381743_1763517709888({...})
230
+ json_str = a.text.split('(', 1)[1].rstrip(');')
231
+ data = json.loads(json_str)
232
+
233
+ # 返回 result.data 中的数据列表
234
+ if data.get('result') and data['result'].get('data'):
235
+ return data['result']['data']
236
+ else:
237
+ logger.warning(f'No data found in response for {index_type}')
238
+ return []
239
+
rquote/api/stock_info.py CHANGED
@@ -3,49 +3,101 @@
3
3
  股票信息相关API
4
4
  """
5
5
  import json
6
- from typing import List
6
+ from typing import List, Optional, Any
7
7
  from ..utils import hget
8
8
  from ..exceptions import HTTPError
9
+ from ..cache import Cache
10
+ from ..cache.memory import DictCache as DictCacheAdapter
9
11
 
10
12
 
11
- def get_stock_concepts(i: str) -> List[str]:
13
+ def _normalize_stock_code(code: str) -> str:
14
+ """标准化股票代码"""
15
+ return {'6': 'sh', '0': 'sz', '3': 'sz'}.get(code[0], '') + code if code[0] in ['6', '0', '3'] else code
16
+
17
+
18
+ def _get_cache_adapter(dd: Optional[Any]) -> Optional[Cache]:
19
+ """获取缓存适配器"""
20
+ if dd is None:
21
+ return None
22
+ if isinstance(dd, dict):
23
+ return DictCacheAdapter(dd)
24
+ elif isinstance(dd, Cache):
25
+ return dd
26
+ elif hasattr(dd, 'get') and hasattr(dd, 'put'):
27
+ return DictCacheAdapter(dd)
28
+ return None
29
+
30
+
31
+ def _fetch_stock_plate_data(normalized_code: str, data_key: str, error_msg: str) -> List[str]:
32
+ """获取股票板块数据的通用函数"""
33
+ url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/stockinfo/plateNew?code={normalized_code}&app=wzq&zdf=1'
34
+ a = hget(url)
35
+ if not a:
36
+ raise HTTPError(f'Failed to fetch {error_msg} from QQ Finance')
37
+ data = json.loads(a.text)
38
+ if data.get('code') != 0:
39
+ raise HTTPError('API returned error: {}'.format(data.get('msg', 'Unknown error')))
40
+ return data.get('data', {}).get(data_key, [])
41
+
42
+
43
+ def get_stock_concepts(i: str, dd: Optional[Any] = None) -> List[str]:
12
44
  """
13
45
  获取指定股票所属的概念板块
14
46
 
15
47
  Args:
16
48
  i: 股票代码
49
+ dd: data dictionary或Cache对象,任何有get/put方法的本地缓存
17
50
 
18
51
  Returns:
19
52
  概念代码列表
20
53
  """
21
- i = {'6': 'sh', '0': 'sz', '3': 'sz'}.get(i[0], '') + i if i[0] in ['6', '0', '3'] else i
22
- url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/stockinfo/plateNew?code={i}&app=wzq&zdf=1'
23
- a = hget(url)
24
- if not a:
25
- raise HTTPError('Failed to fetch concepts from QQ Finance')
26
- data = json.loads(a.text)
27
- if data.get('code') != 0:
28
- raise HTTPError('API returned error: {}'.format(data.get('msg', 'Unknown error')))
29
- return data.get('data', {}).get('concept', [])
54
+ cache = _get_cache_adapter(dd)
55
+ normalized_code = _normalize_stock_code(i)
56
+ cache_key = f'stock_concepts:{normalized_code}'
57
+
58
+ # 尝试从缓存获取
59
+ if cache:
60
+ cached_result = cache.get(cache_key)
61
+ if cached_result is not None:
62
+ return cached_result
63
+
64
+ # 缓存未命中,请求网络
65
+ result = _fetch_stock_plate_data(normalized_code, 'concept', 'concepts')
66
+
67
+ # 存入缓存
68
+ if cache:
69
+ cache.put(cache_key, result)
70
+
71
+ return result
30
72
 
31
73
 
32
- def get_stock_industry(i: str) -> List[str]:
74
+ def get_stock_industry(i: str, dd: Optional[Any] = None) -> List[str]:
33
75
  """
34
76
  获取指定股票所属的行业板块
35
77
 
36
78
  Args:
37
79
  i: 股票代码
80
+ dd: data dictionary或Cache对象,任何有get/put方法的本地缓存
38
81
 
39
82
  Returns:
40
83
  行业代码列表
41
84
  """
42
- i = {'6': 'sh', '0': 'sz', '3': 'sz'}.get(i[0], '') + i if i[0] in ['6', '0', '3'] else i
43
- url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/stockinfo/plateNew?code={i}&app=wzq&zdf=1'
44
- a = hget(url)
45
- if not a:
46
- raise HTTPError('Failed to fetch industry from QQ Finance')
47
- data = json.loads(a.text)
48
- if data.get('code') != 0:
49
- raise HTTPError('API returned error: {}'.format(data.get('msg', 'Unknown error')))
50
- return data.get('data', {}).get('plate', [])
85
+ cache = _get_cache_adapter(dd)
86
+ normalized_code = _normalize_stock_code(i)
87
+ cache_key = f'stock_industry:{normalized_code}'
88
+
89
+ # 尝试从缓存获取
90
+ if cache:
91
+ cached_result = cache.get(cache_key)
92
+ if cached_result is not None:
93
+ return cached_result
94
+
95
+ # 缓存未命中,请求网络
96
+ result = _fetch_stock_plate_data(normalized_code, 'plate', 'industry')
97
+
98
+ # 存入缓存
99
+ if cache:
100
+ cache.put(cache_key, result)
101
+
102
+ return result
51
103
 
rquote/markets/base.py CHANGED
@@ -98,10 +98,15 @@ class Market(ABC):
98
98
  if PersistentCache and isinstance(self.cache, PersistentCache):
99
99
  # 从完整 key 中提取 base_key
100
100
  parts = key.split(':')
101
- if len(parts) >= 3:
101
+ if len(parts) == 3:
102
+ # 已经是 base_key 格式:symbol:freq:fq
103
+ base_key = key
104
+ cached = self.cache.get(base_key, sdate=sdate, edate=edate)
105
+ elif len(parts) >= 6:
106
+ # 完整 key 格式:symbol:sdate:edate:freq:days:fq
102
107
  symbol = parts[0]
103
- freq = parts[3] if len(parts) > 3 else 'day'
104
- fq = parts[5] if len(parts) > 5 else 'qfq'
108
+ freq = parts[3]
109
+ fq = parts[5]
105
110
  base_key = f"{symbol}:{freq}:{fq}"
106
111
  cached = self.cache.get(base_key, sdate=sdate, edate=edate)
107
112
  else:
@@ -119,10 +124,15 @@ class Market(ABC):
119
124
  if PersistentCache and isinstance(self.cache, PersistentCache):
120
125
  # 从完整 key 中提取 base_key
121
126
  parts = key.split(':')
122
- if len(parts) >= 3:
127
+ if len(parts) == 3:
128
+ # 已经是 base_key 格式:symbol:freq:fq
129
+ base_key = key
130
+ self.cache.put(base_key, value)
131
+ elif len(parts) >= 6:
132
+ # 完整 key 格式:symbol:sdate:edate:freq:days:fq
123
133
  symbol = parts[0]
124
- freq = parts[3] if len(parts) > 3 else 'day'
125
- fq = parts[5] if len(parts) > 5 else 'qfq'
134
+ freq = parts[3]
135
+ fq = parts[5]
126
136
  base_key = f"{symbol}:{freq}:{fq}"
127
137
  self.cache.put(base_key, value)
128
138
  else:
@@ -94,6 +94,13 @@ class CNStockMarket(Market):
94
94
  def _get_pt_price(self, symbol: str, sdate: str, edate: str,
95
95
  freq: str, days: int, fq: str) -> Tuple[str, str, pd.DataFrame]:
96
96
  """获取PT代码价格"""
97
+ # 先检查缓存(使用base_key格式,日期通过参数传递)
98
+ base_key = f"{symbol}:{freq}:{fq}"
99
+ cached = self._get_cached(base_key, sdate=sdate, edate=edate)
100
+ if cached:
101
+ logger.info(f"[PT CACHE HIT] symbol={symbol}, 从缓存返回数据")
102
+ return cached
103
+
97
104
  try:
98
105
  url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/newfqkline/get?_var=kline_dayqfq&param={symbol},{freq},{sdate},{edate},{days},{fq}'
99
106
  response = hget(url)
@@ -117,7 +124,7 @@ class CNStockMarket(Market):
117
124
  parser = KlineParser()
118
125
  name, df = parser.parse_tencent_kline(data, symbol)
119
126
  result = (symbol, name, df)
120
- self._put_cache(f"{symbol}:{sdate}:{edate}:{freq}", result)
127
+ self._put_cache(base_key, result)
121
128
  return result
122
129
  except Exception as e:
123
130
  logger.warning(f'Failed to parse {symbol}, using fallback: {e}')
@@ -147,7 +154,7 @@ class CNStockMarket(Market):
147
154
  df[col] = pd.to_numeric(df[col], errors='coerce')
148
155
 
149
156
  result = (symbol, name, df)
150
- self._put_cache(f"{symbol}:{sdate}:{edate}:{freq}", result)
157
+ self._put_cache(base_key, result)
151
158
  return result
152
159
  except Exception as e:
153
160
  logger.warning(f'error fetching {symbol}, err: {e}')
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rquote
3
- Version: 0.4.3
3
+ Version: 0.4.6
4
4
  Summary: Mostly day quotes of cn/hk/us/fund/future markets, side with quote list fetch
5
5
  Requires-Python: >=3.9.0
6
6
  Description-Content-Type: text/markdown
@@ -18,7 +18,7 @@ Requires-Dist: duckdb>=0.9.0; extra == "persistent"
18
18
 
19
19
  ## 版本信息
20
20
 
21
- 当前版本:**0.4.3**
21
+ 当前版本:**0.4.6**
22
22
 
23
23
  ## 主要特性
24
24
 
@@ -219,6 +219,28 @@ us_stocks = get_us_stocks(k=100) # 获取前100只
219
219
  # 返回格式: [{name, symbol, market, mktcap, pe, ...}, ...]
220
220
  ```
221
221
 
222
+ #### `get_cnindex_stocks(index_type='hs300')`
223
+
224
+ 获取中国指数成分股列表
225
+
226
+ ```python
227
+ from rquote import get_cnindex_stocks
228
+
229
+ # 获取沪深300成分股
230
+ hs300_stocks = get_cnindex_stocks('hs300')
231
+ # 获取中证500成分股
232
+ zz500_stocks = get_cnindex_stocks('zz500')
233
+ # 获取中证1000成分股
234
+ zz1000_stocks = get_cnindex_stocks('zz1000')
235
+
236
+ # 返回格式: [{SECURITY_CODE, SECURITY_NAME_ABBR, INDUSTRY, WEIGHT, EPS, BPS, ROE, FREE_CAP, ...}, ...]
237
+ ```
238
+
239
+ 支持的指数类型:
240
+ - `'hs300'`: 沪深300
241
+ - `'zz500'`: 中证500
242
+ - `'zz1000'`: 中证1000
243
+
222
244
  ### 基金和期货
223
245
 
224
246
  #### `get_cn_fund_list()`
@@ -1,12 +1,12 @@
1
- rquote/__init__.py,sha256=HMXqZ_wfGoRqw1V3xm2MyBGYKB9ooGWIRnk60bisLZo,2370
1
+ rquote/__init__.py,sha256=VEZOnqv7Erc34qqllI17bG9cezoaf9uNoOtX9fmhtyw,2420
2
2
  rquote/config.py,sha256=noep_VzY_nJehnkPQb4mkwzpeYLwkU1riqofQJ6Hhw0,1108
3
3
  rquote/exceptions.py,sha256=lJH2GC5dDhMoW_OtlBc03wlUn684-7jNPyF1NjmfVIE,569
4
4
  rquote/plots.py,sha256=UQn4sjhIzVwagfhUDM738b2HHjKo4tRdU2UCs_1-FbY,2341
5
5
  rquote/utils.py,sha256=bH0ZFIo-ZelNztzPS6BXFShXE3yGA9USI_P9INN0Y-s,310
6
- rquote/api/__init__.py,sha256=ptizO--im80HaxlzxkJo9BKdJPEnbu00R9UDgcoA0mU,656
7
- rquote/api/lists.py,sha256=fRebS02Fi0qe6KpWBA-9W1UG0It6__DmRlNimtMa7L8,5331
6
+ rquote/api/__init__.py,sha256=17VgPOKsbQX8kKcuU8fKqn4qOHxMamV9GBTXTCcbdvw,706
7
+ rquote/api/lists.py,sha256=WGyliA9pkpPrN019xkhKL-5nzrmTGrsb8bnG8Zz4cRs,7395
8
8
  rquote/api/price.py,sha256=I5lZl6cUQRlE4AtzNbR-uGZt1ho9vgP1cgNFDjaigMA,3575
9
- rquote/api/stock_info.py,sha256=912ICdIBr8z2lKWDbq3gG0E94czTPvbx9aXsKUi-QkE,1537
9
+ rquote/api/stock_info.py,sha256=h_AbgsR8CLWz5zA2PtGsS3ROQ3qcw_hnRAtG3USeMos,2988
10
10
  rquote/api/tick.py,sha256=nEcjuAjtBHUaD8KPRLg643piVa21PhKDQvkVWNwvvME,1431
11
11
  rquote/cache/__init__.py,sha256=S393I5Wmp0QooaRka9n7bvDUdEbg3jUhm6u815T86rM,317
12
12
  rquote/cache/base.py,sha256=orzG4Yo-6gzVG027j1-LTZPT718JohnCdLDnOLoLUQ4,515
@@ -19,8 +19,8 @@ rquote/data_sources/tencent.py,sha256=ayt1O85pheLwzX3z5c6Qij1NrmUywcsz6YcSVzdDoM
19
19
  rquote/factors/__init__.py,sha256=_ZbH2XxYtXwCJpvRVdNvGncoPSpMqrtlYmf1_fMGIjM,116
20
20
  rquote/factors/technical.py,sha256=dPDs3pDEDRV9iQJBrSoKpGFLQMjOqyoBdN2rUntpOUU,4235
21
21
  rquote/markets/__init__.py,sha256=k4F8cZgb-phqemMqhZXFPdOKsR4P--DD3d5i21vKhbg,365
22
- rquote/markets/base.py,sha256=nHBMzQRkuDUrsx9GvB_QiMh2deMUjTiUZsIRYPJpB_8,11206
23
- rquote/markets/cn_stock.py,sha256=nu2ebTE4a6FAJkvpMN0FEPuqwom_hqTRjnUg96cQGKc,8320
22
+ rquote/markets/base.py,sha256=SyzghAMvSoQNL8DbHCXbWQ0bhu0pDlJrYmO16L_blOI,11646
23
+ rquote/markets/cn_stock.py,sha256=lATGlunbR5cV-D84S3iD-VKc3L71hz4dyYx8YBYiuQg,8596
24
24
  rquote/markets/factory.py,sha256=4Txpuok0LBOLT_vAiIU-NslwVnYF7sKHCdlacAboxpo,2875
25
25
  rquote/markets/future.py,sha256=yGMyUu9Fv75jbzPbvW6_36otEeebSij7vnzow_zyEn8,7358
26
26
  rquote/markets/hk_stock.py,sha256=AhRJpWp027ACew9ogxkVCJXbqbYQ1AkbFwDJccXbvAs,1183
@@ -33,7 +33,7 @@ rquote/utils/helpers.py,sha256=V07n9BtRS8bEJH023Kca78-unk7iD3B9hn2UjELetYs,354
33
33
  rquote/utils/http.py,sha256=X0Alhnu0CNqyQeOt6ivUWmh2XwrWxXd2lSpQOKDdnzw,3249
34
34
  rquote/utils/logging.py,sha256=fs2YF1Srux4LLTdk_Grjm5g1f4mzewI38VVSAI82goA,1471
35
35
  rquote/utils/web.py,sha256=I8_pcThW6VUvahuRHdtp32iZwr85hEt1hB6TgznMy_U,3854
36
- rquote-0.4.3.dist-info/METADATA,sha256=R9zS71G3lxp8C43qrY67DBn2Y2t_1P7rTtCztfj7Kq8,14344
37
- rquote-0.4.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
- rquote-0.4.3.dist-info/top_level.txt,sha256=CehAiaZx7Fo8HGoV2zd5GhILUW1jQEN8YS-cWMlrK9Y,7
39
- rquote-0.4.3.dist-info/RECORD,,
36
+ rquote-0.4.6.dist-info/METADATA,sha256=LRPiTig-kVH_yqrJpXu2h_8wlq4q9pjghHs3Fmv687w,14898
37
+ rquote-0.4.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
38
+ rquote-0.4.6.dist-info/top_level.txt,sha256=CehAiaZx7Fo8HGoV2zd5GhILUW1jQEN8YS-cWMlrK9Y,7
39
+ rquote-0.4.6.dist-info/RECORD,,
File without changes