rquote 0.4.4__tar.gz → 0.4.7__tar.gz

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.
Files changed (50) hide show
  1. {rquote-0.4.4 → rquote-0.4.7}/PKG-INFO +24 -2
  2. {rquote-0.4.4 → rquote-0.4.7}/README.md +23 -1
  3. {rquote-0.4.4 → rquote-0.4.7}/pyproject.toml +1 -1
  4. {rquote-0.4.4 → rquote-0.4.7}/rquote/__init__.py +3 -1
  5. {rquote-0.4.4 → rquote-0.4.7}/rquote/api/__init__.py +4 -2
  6. {rquote-0.4.4 → rquote-0.4.7}/rquote/api/lists.py +65 -1
  7. {rquote-0.4.4 → rquote-0.4.7}/rquote/config.py +2 -0
  8. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/base.py +117 -46
  9. rquote-0.4.7/rquote/markets/cn_stock.py +317 -0
  10. {rquote-0.4.4 → rquote-0.4.7}/rquote.egg-info/PKG-INFO +24 -2
  11. rquote-0.4.4/rquote/markets/cn_stock.py +0 -186
  12. {rquote-0.4.4 → rquote-0.4.7}/rquote/api/price.py +0 -0
  13. {rquote-0.4.4 → rquote-0.4.7}/rquote/api/stock_info.py +0 -0
  14. {rquote-0.4.4 → rquote-0.4.7}/rquote/api/tick.py +0 -0
  15. {rquote-0.4.4 → rquote-0.4.7}/rquote/cache/__init__.py +0 -0
  16. {rquote-0.4.4 → rquote-0.4.7}/rquote/cache/base.py +0 -0
  17. {rquote-0.4.4 → rquote-0.4.7}/rquote/cache/memory.py +0 -0
  18. {rquote-0.4.4 → rquote-0.4.7}/rquote/cache/persistent.py +0 -0
  19. {rquote-0.4.4 → rquote-0.4.7}/rquote/data_sources/__init__.py +0 -0
  20. {rquote-0.4.4 → rquote-0.4.7}/rquote/data_sources/base.py +0 -0
  21. {rquote-0.4.4 → rquote-0.4.7}/rquote/data_sources/sina.py +0 -0
  22. {rquote-0.4.4 → rquote-0.4.7}/rquote/data_sources/tencent.py +0 -0
  23. {rquote-0.4.4 → rquote-0.4.7}/rquote/exceptions.py +0 -0
  24. {rquote-0.4.4 → rquote-0.4.7}/rquote/factors/__init__.py +0 -0
  25. {rquote-0.4.4 → rquote-0.4.7}/rquote/factors/technical.py +0 -0
  26. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/__init__.py +0 -0
  27. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/factory.py +0 -0
  28. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/future.py +0 -0
  29. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/hk_stock.py +0 -0
  30. {rquote-0.4.4 → rquote-0.4.7}/rquote/markets/us_stock.py +0 -0
  31. {rquote-0.4.4 → rquote-0.4.7}/rquote/parsers/__init__.py +0 -0
  32. {rquote-0.4.4 → rquote-0.4.7}/rquote/parsers/kline.py +0 -0
  33. {rquote-0.4.4 → rquote-0.4.7}/rquote/plots.py +0 -0
  34. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/__init__.py +0 -0
  35. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/date.py +0 -0
  36. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/helpers.py +0 -0
  37. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/http.py +0 -0
  38. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/logging.py +0 -0
  39. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils/web.py +0 -0
  40. {rquote-0.4.4 → rquote-0.4.7}/rquote/utils.py +0 -0
  41. {rquote-0.4.4 → rquote-0.4.7}/rquote.egg-info/SOURCES.txt +0 -0
  42. {rquote-0.4.4 → rquote-0.4.7}/rquote.egg-info/dependency_links.txt +0 -0
  43. {rquote-0.4.4 → rquote-0.4.7}/rquote.egg-info/requires.txt +0 -0
  44. {rquote-0.4.4 → rquote-0.4.7}/rquote.egg-info/top_level.txt +0 -0
  45. {rquote-0.4.4 → rquote-0.4.7}/setup.cfg +0 -0
  46. {rquote-0.4.4 → rquote-0.4.7}/tests/test_api.py +0 -0
  47. {rquote-0.4.4 → rquote-0.4.7}/tests/test_cache.py +0 -0
  48. {rquote-0.4.4 → rquote-0.4.7}/tests/test_config.py +0 -0
  49. {rquote-0.4.4 → rquote-0.4.7}/tests/test_exceptions.py +0 -0
  50. {rquote-0.4.4 → rquote-0.4.7}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rquote
3
- Version: 0.4.4
3
+ Version: 0.4.7
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.4**
21
+ 当前版本:**0.4.7**
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()`
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 版本信息
6
6
 
7
- 当前版本:**0.4.4**
7
+ 当前版本:**0.4.7**
8
8
 
9
9
  ## 主要特性
10
10
 
@@ -205,6 +205,28 @@ us_stocks = get_us_stocks(k=100) # 获取前100只
205
205
  # 返回格式: [{name, symbol, market, mktcap, pe, ...}, ...]
206
206
  ```
207
207
 
208
+ #### `get_cnindex_stocks(index_type='hs300')`
209
+
210
+ 获取中国指数成分股列表
211
+
212
+ ```python
213
+ from rquote import get_cnindex_stocks
214
+
215
+ # 获取沪深300成分股
216
+ hs300_stocks = get_cnindex_stocks('hs300')
217
+ # 获取中证500成分股
218
+ zz500_stocks = get_cnindex_stocks('zz500')
219
+ # 获取中证1000成分股
220
+ zz1000_stocks = get_cnindex_stocks('zz1000')
221
+
222
+ # 返回格式: [{SECURITY_CODE, SECURITY_NAME_ABBR, INDUSTRY, WEIGHT, EPS, BPS, ROE, FREE_CAP, ...}, ...]
223
+ ```
224
+
225
+ 支持的指数类型:
226
+ - `'hs300'`: 沪深300
227
+ - `'zz500'`: 中证500
228
+ - `'zz1000'`: 中证1000
229
+
208
230
  ### 基金和期货
209
231
 
210
232
  #### `get_cn_fund_list()`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rquote"
7
- version = "0.4.4"
7
+ version = "0.4.7"
8
8
  description = "Mostly day quotes of cn/hk/us/fund/future markets, side with quote list fetch"
9
9
  readme = "README.md"
10
10
  # requires-python = ">=3.6.1" # duckdb requires higher python version
@@ -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',
@@ -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
 
@@ -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
+
@@ -30,6 +30,8 @@ class Config:
30
30
  return cls(
31
31
  http_timeout=int(os.getenv('RQUOTE_HTTP_TIMEOUT', '10')),
32
32
  http_retry_times=int(os.getenv('RQUOTE_RETRY_TIMES', '3')),
33
+ http_retry_delay=float(os.getenv('RQUOTE_HTTP_RETRY_DELAY', '1.0')),
34
+ http_pool_size=int(os.getenv('RQUOTE_HTTP_POOL_SIZE', '10')),
33
35
  cache_enabled=os.getenv('RQUOTE_CACHE_ENABLED', 'true').lower() == 'true',
34
36
  cache_ttl=int(os.getenv('RQUOTE_CACHE_TTL', '3600')),
35
37
  log_level=os.getenv('RQUOTE_LOG_LEVEL', 'INFO'),
@@ -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:
@@ -164,63 +174,124 @@ class Market(ABC):
164
174
 
165
175
  need_extend_forward = False # 需要向前扩展(更新日期)
166
176
  need_extend_backward = False # 需要向后扩展(更早日期)
177
+ need_extend_for_length = False # 需要扩展以满足长度要求(>=60行)
167
178
  extend_sdate = sdate
168
179
  extend_edate = edate
169
180
 
170
- # 检查是否需要向前扩展
181
+ # 逻辑1: 检查是否需要向前扩展(请求的 edate 晚于缓存的最新日期)
171
182
  if request_edate and request_edate > cached_latest:
172
183
  need_extend_forward = True
173
184
  # 从缓存的最新日期+1天开始,扩展到请求的 edate
174
185
  extend_sdate = (cached_latest + pd.Timedelta(days=1)).strftime('%Y-%m-%d')
175
186
  extend_edate = edate
176
187
 
177
- # 检查是否需要向后扩展
178
- if request_sdate and request_sdate < cached_earliest:
188
+ # 逻辑2: 如果从cache取的数据在edate前的长度小于等于60,则进行网络请求取数合并进cache
189
+ elif request_edate:
190
+ # 计算edate之前的数据行数
191
+ data_before_edate = cached_df[cached_df.index <= request_edate]
192
+ if len(data_before_edate) <= 60:
193
+ need_extend_for_length = True
194
+ # 从更早的日期开始获取,确保edate前有足够的数据(>=60行)
195
+ # 往前推约4个月(120天),确保有足够的交易日
196
+ target_sdate = request_edate - pd.Timedelta(days=120)
197
+ extend_sdate = target_sdate.strftime('%Y-%m-%d')
198
+ extend_edate = edate
199
+ logger.info(f"[PRICE EXTEND LENGTH] symbol={symbol}, edate前数据行数={len(data_before_edate)} <= 60, 从更早日期获取, extend_sdate={extend_sdate}")
200
+
201
+ # 逻辑3: 如果cache中有数据,但新的edate小于cache中数据最小值
202
+ elif request_edate and request_edate < cached_earliest:
203
+ need_extend_backward = True
204
+ # 从缓存最早日期开始往前获取,直到覆盖edate且edate前的长度大于60
205
+ # 先尝试从edate往前推足够的天数(约4个月)
206
+ target_sdate = request_edate - pd.Timedelta(days=120)
207
+ extend_sdate = target_sdate.strftime('%Y-%m-%d')
208
+ extend_edate = (cached_earliest - pd.Timedelta(days=1)).strftime('%Y-%m-%d')
209
+ logger.info(f"[PRICE EXTEND EARLY] symbol={symbol}, edate={request_edate} 早于缓存最早日期={cached_earliest}, 从更早日期获取, extend_sdate={extend_sdate}, extend_edate={extend_edate}")
210
+
211
+ # 检查是否需要向后扩展(请求的 sdate 早于缓存的最早日期,且不是情况3)
212
+ if request_sdate and request_sdate < cached_earliest and not need_extend_backward:
179
213
  need_extend_backward = True
180
214
  # 从请求的 sdate 开始,扩展到缓存的最早日期-1天
181
215
  extend_sdate = sdate
182
216
  extend_edate = (cached_earliest - pd.Timedelta(days=1)).strftime('%Y-%m-%d')
183
217
 
184
218
  # 如果需要扩展,获取缺失的数据
185
- if need_extend_forward or need_extend_backward:
186
- logger.info(f"[PRICE EXTEND] 需要扩展数据, symbol={symbol}, extend_sdate={extend_sdate}, extend_edate={extend_edate}, need_forward={need_extend_forward}, need_backward={need_extend_backward}")
187
- # 获取扩展的数据
188
- extended_result = fetch_func(symbol, extend_sdate, extend_edate, freq, days, fq)
189
- _, _, extended_df = extended_result
190
- logger.info(f"[PRICE FETCH] 从网络获取扩展数据, 数据行数={len(extended_df)}")
219
+ if need_extend_forward or need_extend_backward or need_extend_for_length:
220
+ logger.info(f"[PRICE EXTEND] 需要扩展数据, symbol={symbol}, extend_sdate={extend_sdate}, extend_edate={extend_edate}, need_forward={need_extend_forward}, need_backward={need_extend_backward}, need_length={need_extend_for_length}")
191
221
 
192
- if not extended_df.empty:
193
- # 确保两个 DataFrame 的索引都是 DatetimeIndex
194
- if not isinstance(cached_df.index, pd.DatetimeIndex):
195
- try:
196
- cached_df.index = pd.to_datetime(cached_df.index)
197
- except (ValueError, TypeError):
198
- pass
199
- if not isinstance(extended_df.index, pd.DatetimeIndex):
200
- try:
201
- extended_df.index = pd.to_datetime(extended_df.index)
202
- except (ValueError, TypeError):
203
- pass
204
-
205
- # 合并数据
206
- merged_df = pd.concat([cached_df, extended_df])
207
- merged_df = merged_df[~merged_df.index.duplicated(keep='last')]
208
- merged_df = merged_df.sort_index()
209
-
210
- # 过滤到请求的日期范围
211
- if request_sdate or request_edate:
212
- if request_sdate and request_edate:
213
- mask = (merged_df.index >= request_sdate) & (merged_df.index <= request_edate)
214
- elif request_sdate:
215
- mask = merged_df.index >= request_sdate
216
- else:
217
- mask = merged_df.index <= request_edate
218
- merged_df = merged_df[mask]
222
+ # 对于逻辑2和逻辑3,可能需要循环获取直到满足条件
223
+ max_iterations = 5 # 最多循环5次,避免无限循环
224
+ iteration = 0
225
+ current_merged_df = cached_df.copy()
226
+
227
+ while iteration < max_iterations:
228
+ iteration += 1
229
+ # 获取扩展的数据
230
+ extended_result = fetch_func(symbol, extend_sdate, extend_edate, freq, days, fq)
231
+ _, _, extended_df = extended_result
232
+ logger.info(f"[PRICE FETCH] 从网络获取扩展数据 (迭代{iteration}), 数据行数={len(extended_df)}")
219
233
 
220
- result = (symbol, name, merged_df)
221
- # 更新缓存(使用原始 key,PersistentCache 会智能合并)
222
- self._put_cache(cache_key, result)
223
- return result
234
+ if not extended_df.empty:
235
+ # 确保两个 DataFrame 的索引都是 DatetimeIndex
236
+ if not isinstance(current_merged_df.index, pd.DatetimeIndex):
237
+ try:
238
+ current_merged_df.index = pd.to_datetime(current_merged_df.index)
239
+ except (ValueError, TypeError):
240
+ pass
241
+ if not isinstance(extended_df.index, pd.DatetimeIndex):
242
+ try:
243
+ extended_df.index = pd.to_datetime(extended_df.index)
244
+ except (ValueError, TypeError):
245
+ pass
246
+
247
+ # 合并数据
248
+ current_merged_df = pd.concat([current_merged_df, extended_df])
249
+ current_merged_df = current_merged_df[~current_merged_df.index.duplicated(keep='last')]
250
+ current_merged_df = current_merged_df.sort_index()
251
+
252
+ # 检查是否满足条件(逻辑2和逻辑3需要检查长度)
253
+ if need_extend_for_length or need_extend_backward:
254
+ if request_edate:
255
+ data_before_edate = current_merged_df[current_merged_df.index <= request_edate]
256
+ if len(data_before_edate) > 60:
257
+ # 满足条件,退出循环
258
+ logger.info(f"[PRICE EXTEND] 已满足长度要求, edate前数据行数={len(data_before_edate)}")
259
+ break
260
+ else:
261
+ # 还需要继续获取更早的数据
262
+ current_earliest = current_merged_df.index.min()
263
+ if current_earliest <= pd.to_datetime(extend_sdate):
264
+ # 已经获取到最早的数据,无法再往前获取
265
+ logger.warning(f"[PRICE EXTEND] 已获取到最早数据,但edate前数据行数={len(data_before_edate)}仍不足60行")
266
+ break
267
+ # 继续往前推
268
+ extend_sdate_dt = pd.to_datetime(extend_sdate) - pd.Timedelta(days=120)
269
+ extend_sdate = extend_sdate_dt.strftime('%Y-%m-%d')
270
+ logger.info(f"[PRICE EXTEND] 继续获取更早数据, 新extend_sdate={extend_sdate}")
271
+ continue
272
+
273
+ # 对于逻辑1(向前扩展),不需要循环,直接退出
274
+ if need_extend_forward and not need_extend_for_length and not need_extend_backward:
275
+ break
276
+ else:
277
+ # 获取失败,退出循环
278
+ logger.warning(f"[PRICE EXTEND] 获取数据为空,退出循环")
279
+ break
280
+
281
+ # 过滤到请求的日期范围
282
+ if request_sdate or request_edate:
283
+ if request_sdate and request_edate:
284
+ mask = (current_merged_df.index >= request_sdate) & (current_merged_df.index <= request_edate)
285
+ elif request_sdate:
286
+ mask = current_merged_df.index >= request_sdate
287
+ else:
288
+ mask = current_merged_df.index <= request_edate
289
+ current_merged_df = current_merged_df[mask]
290
+
291
+ result = (symbol, name, current_merged_df)
292
+ # 更新缓存(使用原始 key,PersistentCache 会智能合并)
293
+ self._put_cache(cache_key, result)
294
+ return result
224
295
 
225
296
  # 不需要扩展,直接返回缓存的数据
226
297
  # 注意:PersistentCache.get() 已经根据请求的日期范围进行了过滤,
@@ -0,0 +1,317 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ A股市场
4
+ """
5
+ import json
6
+ import base64
7
+ import pandas as pd
8
+ from typing import Tuple
9
+ from .base import Market
10
+ from ..parsers import KlineParser
11
+ from ..exceptions import DataSourceError, ParseError
12
+ from ..utils import hget, logger
13
+
14
+
15
+ class CNStockMarket(Market):
16
+ """A股市场"""
17
+
18
+ def normalize_symbol(self, symbol: str) -> str:
19
+ """标准化A股代码"""
20
+ if symbol[0] in ['0', '1', '3', '5', '6']:
21
+ prefix = 'sh' if symbol[0] in ['5', '6'] else 'sz'
22
+ return prefix + symbol
23
+ return symbol
24
+
25
+ def get_price(self, symbol: str, sdate: str = '', edate: str = '',
26
+ freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
27
+ """获取A股价格数据"""
28
+ symbol = self.normalize_symbol(symbol)
29
+
30
+ # 特殊处理BK(板块)代码(不使用缓存)
31
+ if symbol[:2] == 'BK':
32
+ return self._get_bk_price(symbol)
33
+
34
+ # PT代码也使用基类的缓存逻辑(包含长度检查和扩展逻辑)
35
+ # 使用基类的缓存逻辑,所有市场都会应用这两个逻辑:
36
+ # 1. 如果从cache取的数据在edate前的长度小于等于60,则进行网络请求取数合并进cache
37
+ # 2. 如果cache中有数据,但新的edate小于cache中数据最小值,从更早日期开始取并合并
38
+ return super().get_price(symbol, sdate, edate, freq, days, fq)
39
+
40
+ def _fetch_price_data(self, symbol: str, sdate: str = '', edate: str = '',
41
+ freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
42
+ """从数据源获取A股价格数据"""
43
+ try:
44
+ raw_data = self.data_source.fetch_kline(
45
+ symbol, freq=freq, sdate=sdate, edate=edate, days=days, fq=fq
46
+ )
47
+
48
+ # 使用解析器解析
49
+ parser = KlineParser()
50
+ name, df = parser.parse_tencent_kline(raw_data, symbol)
51
+
52
+ return (symbol, name, df)
53
+ except (DataSourceError, ParseError) as e:
54
+ logger.warning(f'Failed to fetch {symbol} using new architecture: {e}')
55
+ # 降级到旧方法
56
+ return self._get_price_fallback(symbol, sdate, edate, freq, days, fq)
57
+
58
+ def _get_bk_price(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
59
+ """获取板块价格(BK开头)"""
60
+ try:
61
+ url = base64.b64decode('aHR0cDovL3B1c2gyaGlzLmVhc3' +
62
+ 'Rtb25leS5jb20vYXBpL3F0L3N0b2NrL2tsaW5lL2dldD9jYj1qUX' +
63
+ 'VlcnkxMTI0MDIyNTY2NDQ1ODczNzY2OTcyXzE2MTc4NjQ1NjgxMz' +
64
+ 'Emc2VjaWQ9OTAu').decode() + symbol + \
65
+ '&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5' + \
66
+ '&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58' + \
67
+ '&klt=101&fqt=0&beg=19900101&end=20990101&_=1'
68
+ response = hget(url)
69
+ if not response:
70
+ logger.warning(f'{symbol} hget failed')
71
+ return symbol, 'None', pd.DataFrame([])
72
+
73
+ data = json.loads(response.text.split('jQuery1124022566445873766972_1617864568131(')[1][:-2])
74
+ if not data.get('data'):
75
+ logger.warning(f'{symbol} data empty')
76
+ return symbol, 'None', pd.DataFrame([])
77
+
78
+ name = data['data']['name']
79
+ df = pd.DataFrame([i.split(',') for i in data['data']['klines']],
80
+ columns=['date', 'open', 'close', 'high', 'low', 'vol', 'money', 'p'])
81
+ df = df.set_index(['date'])
82
+ # 转换数值列
83
+ for col in ['open', 'close', 'high', 'low', 'vol', 'money', 'p']:
84
+ df[col] = pd.to_numeric(df[col], errors='coerce')
85
+
86
+ result = (symbol, name, df)
87
+ self._put_cache(symbol, result)
88
+ return result
89
+ except Exception as e:
90
+ logger.warning(f'error fetching {symbol}, err: {e}')
91
+ return symbol, 'None', pd.DataFrame([])
92
+
93
+ def _get_pt_price(self, symbol: str, sdate: str, edate: str,
94
+ freq: str, days: int, fq: str) -> Tuple[str, str, pd.DataFrame]:
95
+ """获取PT代码价格"""
96
+ # 先检查缓存(使用base_key格式,日期通过参数传递)
97
+ base_key = f"{symbol}:{freq}:{fq}"
98
+ cached = self._get_cached(base_key, sdate=sdate, edate=edate)
99
+ cached_df = None
100
+ need_fetch = False # 标记是否需要从网络获取数据
101
+ if cached:
102
+ _, name, cached_df = cached
103
+ # 检查缓存数据是否满足请求的 edate
104
+ if not cached_df.empty and isinstance(cached_df.index, pd.DatetimeIndex):
105
+ cached_earliest = cached_df.index.min()
106
+ cached_latest = cached_df.index.max()
107
+ request_edate = pd.to_datetime(edate) if edate else None
108
+ request_sdate = pd.to_datetime(sdate) if sdate else None
109
+
110
+ # 逻辑1: 如果请求的 edate 晚于缓存的最新日期,需要从网络获取新数据
111
+ if request_edate and request_edate > cached_latest:
112
+ logger.info(f"[PT CACHE INCOMPLETE] symbol={symbol}, 缓存最新日期={cached_latest}, 请求日期={request_edate}, 需要扩展数据")
113
+ need_fetch = True
114
+ # 逻辑2: 如果从cache取的数据在edate前的长度小于等于60,则进行网络请求取数合并进cache
115
+ elif request_edate:
116
+ # 计算edate之前的数据行数
117
+ data_before_edate = cached_df[cached_df.index <= request_edate]
118
+ if len(data_before_edate) <= 60:
119
+ logger.info(f"[PT CACHE INSUFFICIENT] symbol={symbol}, edate前数据行数={len(data_before_edate)} <= 60, 需要获取更多历史数据")
120
+ need_fetch = True
121
+ # 逻辑3: 如果cache中有数据,但新的edate小于cache中数据最小值,需要从更早的日期开始取
122
+ elif request_edate and request_edate < cached_earliest:
123
+ logger.info(f"[PT CACHE EARLY] symbol={symbol}, 请求edate={request_edate} 早于缓存最早日期={cached_earliest}, 需要从更早日期获取")
124
+ need_fetch = True
125
+ else:
126
+ logger.info(f"[PT CACHE HIT] symbol={symbol}, 从缓存返回数据, 日期范围={cached_df.index.min()} 到 {cached_df.index.max()}")
127
+ return cached
128
+ else:
129
+ logger.info(f"[PT CACHE HIT] symbol={symbol}, 从缓存返回数据")
130
+ return cached
131
+
132
+ try:
133
+ # 确定需要获取的日期范围
134
+ extend_sdate = sdate
135
+ extend_edate = edate
136
+ need_multiple_fetch = False # 是否需要多次获取以满足长度要求
137
+
138
+ if cached and cached_df is not None and not cached_df.empty and isinstance(cached_df.index, pd.DatetimeIndex):
139
+ cached_earliest = cached_df.index.min()
140
+ cached_latest = cached_df.index.max()
141
+ request_edate = pd.to_datetime(edate) if edate else None
142
+ request_sdate = pd.to_datetime(sdate) if sdate else None
143
+
144
+ # 情况1: 请求的 edate 晚于缓存的最新日期,从缓存的最新日期+1天开始获取
145
+ if request_edate and request_edate > cached_latest:
146
+ extend_sdate = (cached_latest + pd.Timedelta(days=1)).strftime('%Y-%m-%d')
147
+ logger.info(f"[PT FETCH] 从缓存最新日期后开始获取, extend_sdate={extend_sdate}, edate={edate}")
148
+ # 情况2: edate前的数据长度<=60,需要获取更多历史数据
149
+ elif request_edate:
150
+ data_before_edate = cached_df[cached_df.index <= request_edate]
151
+ if len(data_before_edate) <= 60:
152
+ # 计算需要获取多少天的数据才能达到60+行
153
+ # 假设每个交易日都有数据,需要大约60个交易日(约3个月)
154
+ # 从edate往前推,确保获取足够的数据
155
+ target_sdate = request_edate - pd.Timedelta(days=120) # 往前推约4个月,确保有足够交易日
156
+ extend_sdate = target_sdate.strftime('%Y-%m-%d')
157
+ extend_edate = edate
158
+ logger.info(f"[PT FETCH] edate前数据不足60行,从更早日期获取, extend_sdate={extend_sdate}, extend_edate={extend_edate}")
159
+ need_multiple_fetch = True # 可能需要多次获取
160
+ # 情况3: 请求的edate早于缓存的最早日期,从缓存最早日期开始往前获取
161
+ elif request_edate and request_edate < cached_earliest:
162
+ # 从缓存最早日期开始往前获取,直到覆盖edate且edate前的长度大于60
163
+ # 先尝试从edate往前推足够的天数
164
+ target_sdate = request_edate - pd.Timedelta(days=120) # 往前推约4个月
165
+ extend_sdate = target_sdate.strftime('%Y-%m-%d')
166
+ extend_edate = (cached_earliest - pd.Timedelta(days=1)).strftime('%Y-%m-%d')
167
+ logger.info(f"[PT FETCH] edate早于缓存最早日期,从更早日期获取, extend_sdate={extend_sdate}, extend_edate={extend_edate}")
168
+ need_multiple_fetch = True # 可能需要多次获取
169
+
170
+ url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/newfqkline/get?_var=kline_dayqfq&param={symbol},{freq},{extend_sdate},{extend_edate},{days},{fq}'
171
+ response = hget(url)
172
+ if not response:
173
+ logger.warning(f'{symbol} hget failed')
174
+ # 如果网络请求失败,但有缓存数据,返回缓存数据
175
+ if cached:
176
+ logger.info(f"[PT FALLBACK] 网络请求失败,返回缓存数据")
177
+ return cached
178
+ return symbol, 'None', pd.DataFrame([])
179
+
180
+ response_text = response.text
181
+ json_start = response_text.find('{')
182
+ if json_start == -1:
183
+ logger.warning(f'{symbol} invalid response format')
184
+ # 如果解析失败,但有缓存数据,返回缓存数据
185
+ if cached:
186
+ logger.info(f"[PT FALLBACK] 解析失败,返回缓存数据")
187
+ return cached
188
+ return symbol, 'None', pd.DataFrame([])
189
+
190
+ data = json.loads(response_text[json_start:])
191
+ if data.get('code') != 0:
192
+ logger.warning(f'{symbol} API returned error: {data.get("msg", "Unknown error")}')
193
+ # 如果API返回错误,但有缓存数据,返回缓存数据
194
+ if cached:
195
+ logger.info(f"[PT FALLBACK] API错误,返回缓存数据")
196
+ return cached
197
+ return symbol, 'None', pd.DataFrame([])
198
+
199
+ # 使用解析器
200
+ try:
201
+ parser = KlineParser()
202
+ name, df = parser.parse_tencent_kline(data, symbol)
203
+
204
+ # 如果有缓存数据,合并新旧数据
205
+ if cached and cached_df is not None and not cached_df.empty and isinstance(cached_df.index, pd.DatetimeIndex):
206
+ # 确保两个 DataFrame 的索引都是 DatetimeIndex
207
+ if not isinstance(df.index, pd.DatetimeIndex):
208
+ try:
209
+ df.index = pd.to_datetime(df.index)
210
+ except (ValueError, TypeError):
211
+ pass
212
+
213
+ # 合并数据
214
+ merged_df = pd.concat([cached_df, df])
215
+ merged_df = merged_df[~merged_df.index.duplicated(keep='last')]
216
+ merged_df = merged_df.sort_index()
217
+
218
+ # 过滤到请求的日期范围
219
+ if edate:
220
+ request_edate = pd.to_datetime(edate)
221
+ merged_df = merged_df[merged_df.index <= request_edate]
222
+
223
+ result = (symbol, name, merged_df)
224
+ logger.info(f"[PT MERGE] 合并缓存和新数据, 缓存行数={len(cached_df)}, 新数据行数={len(df)}, 合并后行数={len(merged_df)}")
225
+ else:
226
+ result = (symbol, name, df)
227
+
228
+ self._put_cache(base_key, result)
229
+ return result
230
+ except Exception as e:
231
+ logger.warning(f'Failed to parse {symbol}, using fallback: {e}')
232
+ # 降级处理
233
+ symbol_data = data.get('data', {}).get(symbol, {})
234
+ if not symbol_data:
235
+ return symbol, 'None', pd.DataFrame([])
236
+
237
+ tk = None
238
+ for tkt in ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
239
+ 'month', 'qfqmonth', 'hfqmonth']:
240
+ if tkt in symbol_data:
241
+ tk = tkt
242
+ break
243
+
244
+ if not tk:
245
+ return symbol, 'None', pd.DataFrame([])
246
+
247
+ name = ''
248
+ if 'qt' in symbol_data and symbol in symbol_data['qt']:
249
+ name = symbol_data['qt'][symbol][1] if len(symbol_data['qt'][symbol]) > 1 else ''
250
+
251
+ kline_data = symbol_data[tk]
252
+ df = pd.DataFrame([j[:6] for j in kline_data],
253
+ columns=['date', 'open', 'close', 'high', 'low', 'vol']).set_index('date')
254
+ for col in ['open', 'high', 'low', 'close', 'vol']:
255
+ df[col] = pd.to_numeric(df[col], errors='coerce')
256
+
257
+ # 如果有缓存数据,合并新旧数据
258
+ if cached and cached_df is not None and not cached_df.empty and isinstance(cached_df.index, pd.DatetimeIndex):
259
+ # 确保两个 DataFrame 的索引都是 DatetimeIndex
260
+ if not isinstance(df.index, pd.DatetimeIndex):
261
+ try:
262
+ df.index = pd.to_datetime(df.index)
263
+ except (ValueError, TypeError):
264
+ pass
265
+
266
+ # 合并数据
267
+ merged_df = pd.concat([cached_df, df])
268
+ merged_df = merged_df[~merged_df.index.duplicated(keep='last')]
269
+ merged_df = merged_df.sort_index()
270
+
271
+ # 过滤到请求的日期范围
272
+ if edate:
273
+ request_edate = pd.to_datetime(edate)
274
+ merged_df = merged_df[merged_df.index <= request_edate]
275
+
276
+ result = (symbol, name, merged_df)
277
+ logger.info(f"[PT MERGE FALLBACK] 合并缓存和新数据, 缓存行数={len(cached_df)}, 新数据行数={len(df)}, 合并后行数={len(merged_df)}")
278
+ else:
279
+ result = (symbol, name, df)
280
+
281
+ self._put_cache(base_key, result)
282
+ return result
283
+ except Exception as e:
284
+ logger.warning(f'error fetching {symbol}, err: {e}')
285
+ return symbol, 'None', pd.DataFrame([])
286
+
287
+ def _get_price_fallback(self, symbol: str, sdate: str, edate: str,
288
+ freq: str, days: int, fq: str) -> Tuple[str, str, pd.DataFrame]:
289
+ """降级方法(旧实现)"""
290
+ from ..utils import hget
291
+ import json
292
+
293
+ url = f'https://web.ifzq.gtimg.cn/appstock/app/newfqkline/get?param={symbol},{freq},{sdate},{edate},{days},{fq}'
294
+ response = hget(url)
295
+ if not response:
296
+ raise DataSourceError(f'Failed to fetch data for {symbol}')
297
+
298
+ data = json.loads(response.text)['data'][symbol]
299
+ name = ''
300
+ for tkt in ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
301
+ 'month', 'qfqmonth', 'hfqmonth']:
302
+ if tkt in data:
303
+ tk = tkt
304
+ break
305
+
306
+ df = pd.DataFrame([j[:6] for j in data[tk]],
307
+ columns=['date', 'open', 'close', 'high', 'low', 'vol']).set_index('date')
308
+ for col in ['open', 'high', 'low', 'close', 'vol']:
309
+ df[col] = pd.to_numeric(df[col], errors='coerce')
310
+ if 'qt' in data:
311
+ name = data['qt'][symbol][1]
312
+
313
+ result = (symbol, name, df)
314
+ cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
315
+ self._put_cache(cache_key, result)
316
+ return result
317
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rquote
3
- Version: 0.4.4
3
+ Version: 0.4.7
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.4**
21
+ 当前版本:**0.4.7**
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,186 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """
3
- A股市场
4
- """
5
- import json
6
- import base64
7
- import pandas as pd
8
- from typing import Tuple
9
- from .base import Market
10
- from ..parsers import KlineParser
11
- from ..exceptions import DataSourceError, ParseError
12
- from ..utils import hget, logger
13
-
14
-
15
- class CNStockMarket(Market):
16
- """A股市场"""
17
-
18
- def normalize_symbol(self, symbol: str) -> str:
19
- """标准化A股代码"""
20
- if symbol[0] in ['0', '1', '3', '5', '6']:
21
- prefix = 'sh' if symbol[0] in ['5', '6'] else 'sz'
22
- return prefix + symbol
23
- return symbol
24
-
25
- def get_price(self, symbol: str, sdate: str = '', edate: str = '',
26
- freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
27
- """获取A股价格数据"""
28
- symbol = self.normalize_symbol(symbol)
29
-
30
- # 特殊处理BK(板块)代码(不使用缓存)
31
- if symbol[:2] == 'BK':
32
- return self._get_bk_price(symbol)
33
-
34
- # 特殊处理PT代码(不使用缓存)
35
- if symbol[:2] == 'pt':
36
- return self._get_pt_price(symbol, sdate, edate, freq, days, fq)
37
-
38
- # 使用基类的缓存逻辑
39
- return super().get_price(symbol, sdate, edate, freq, days, fq)
40
-
41
- def _fetch_price_data(self, symbol: str, sdate: str = '', edate: str = '',
42
- freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
43
- """从数据源获取A股价格数据"""
44
- try:
45
- raw_data = self.data_source.fetch_kline(
46
- symbol, freq=freq, sdate=sdate, edate=edate, days=days, fq=fq
47
- )
48
-
49
- # 使用解析器解析
50
- parser = KlineParser()
51
- name, df = parser.parse_tencent_kline(raw_data, symbol)
52
-
53
- return (symbol, name, df)
54
- except (DataSourceError, ParseError) as e:
55
- logger.warning(f'Failed to fetch {symbol} using new architecture: {e}')
56
- # 降级到旧方法
57
- return self._get_price_fallback(symbol, sdate, edate, freq, days, fq)
58
-
59
- def _get_bk_price(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
60
- """获取板块价格(BK开头)"""
61
- try:
62
- url = base64.b64decode('aHR0cDovL3B1c2gyaGlzLmVhc3' +
63
- 'Rtb25leS5jb20vYXBpL3F0L3N0b2NrL2tsaW5lL2dldD9jYj1qUX' +
64
- 'VlcnkxMTI0MDIyNTY2NDQ1ODczNzY2OTcyXzE2MTc4NjQ1NjgxMz' +
65
- 'Emc2VjaWQ9OTAu').decode() + symbol + \
66
- '&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5' + \
67
- '&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58' + \
68
- '&klt=101&fqt=0&beg=19900101&end=20990101&_=1'
69
- response = hget(url)
70
- if not response:
71
- logger.warning(f'{symbol} hget failed')
72
- return symbol, 'None', pd.DataFrame([])
73
-
74
- data = json.loads(response.text.split('jQuery1124022566445873766972_1617864568131(')[1][:-2])
75
- if not data.get('data'):
76
- logger.warning(f'{symbol} data empty')
77
- return symbol, 'None', pd.DataFrame([])
78
-
79
- name = data['data']['name']
80
- df = pd.DataFrame([i.split(',') for i in data['data']['klines']],
81
- columns=['date', 'open', 'close', 'high', 'low', 'vol', 'money', 'p'])
82
- df = df.set_index(['date'])
83
- # 转换数值列
84
- for col in ['open', 'close', 'high', 'low', 'vol', 'money', 'p']:
85
- df[col] = pd.to_numeric(df[col], errors='coerce')
86
-
87
- result = (symbol, name, df)
88
- self._put_cache(symbol, result)
89
- return result
90
- except Exception as e:
91
- logger.warning(f'error fetching {symbol}, err: {e}')
92
- return symbol, 'None', pd.DataFrame([])
93
-
94
- def _get_pt_price(self, symbol: str, sdate: str, edate: str,
95
- freq: str, days: int, fq: str) -> Tuple[str, str, pd.DataFrame]:
96
- """获取PT代码价格"""
97
- try:
98
- url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/newfqkline/get?_var=kline_dayqfq&param={symbol},{freq},{sdate},{edate},{days},{fq}'
99
- response = hget(url)
100
- if not response:
101
- logger.warning(f'{symbol} hget failed')
102
- return symbol, 'None', pd.DataFrame([])
103
-
104
- response_text = response.text
105
- json_start = response_text.find('{')
106
- if json_start == -1:
107
- logger.warning(f'{symbol} invalid response format')
108
- return symbol, 'None', pd.DataFrame([])
109
-
110
- data = json.loads(response_text[json_start:])
111
- if data.get('code') != 0:
112
- logger.warning(f'{symbol} API returned error: {data.get("msg", "Unknown error")}')
113
- return symbol, 'None', pd.DataFrame([])
114
-
115
- # 使用解析器
116
- try:
117
- parser = KlineParser()
118
- name, df = parser.parse_tencent_kline(data, symbol)
119
- result = (symbol, name, df)
120
- self._put_cache(f"{symbol}:{sdate}:{edate}:{freq}", result)
121
- return result
122
- except Exception as e:
123
- logger.warning(f'Failed to parse {symbol}, using fallback: {e}')
124
- # 降级处理
125
- symbol_data = data.get('data', {}).get(symbol, {})
126
- if not symbol_data:
127
- return symbol, 'None', pd.DataFrame([])
128
-
129
- tk = None
130
- for tkt in ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
131
- 'month', 'qfqmonth', 'hfqmonth']:
132
- if tkt in symbol_data:
133
- tk = tkt
134
- break
135
-
136
- if not tk:
137
- return symbol, 'None', pd.DataFrame([])
138
-
139
- name = ''
140
- if 'qt' in symbol_data and symbol in symbol_data['qt']:
141
- name = symbol_data['qt'][symbol][1] if len(symbol_data['qt'][symbol]) > 1 else ''
142
-
143
- kline_data = symbol_data[tk]
144
- df = pd.DataFrame([j[:6] for j in kline_data],
145
- columns=['date', 'open', 'close', 'high', 'low', 'vol']).set_index('date')
146
- for col in ['open', 'high', 'low', 'close', 'vol']:
147
- df[col] = pd.to_numeric(df[col], errors='coerce')
148
-
149
- result = (symbol, name, df)
150
- self._put_cache(f"{symbol}:{sdate}:{edate}:{freq}", result)
151
- return result
152
- except Exception as e:
153
- logger.warning(f'error fetching {symbol}, err: {e}')
154
- return symbol, 'None', pd.DataFrame([])
155
-
156
- def _get_price_fallback(self, symbol: str, sdate: str, edate: str,
157
- freq: str, days: int, fq: str) -> Tuple[str, str, pd.DataFrame]:
158
- """降级方法(旧实现)"""
159
- from ..utils import hget
160
- import json
161
-
162
- url = f'https://web.ifzq.gtimg.cn/appstock/app/newfqkline/get?param={symbol},{freq},{sdate},{edate},{days},{fq}'
163
- response = hget(url)
164
- if not response:
165
- raise DataSourceError(f'Failed to fetch data for {symbol}')
166
-
167
- data = json.loads(response.text)['data'][symbol]
168
- name = ''
169
- for tkt in ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
170
- 'month', 'qfqmonth', 'hfqmonth']:
171
- if tkt in data:
172
- tk = tkt
173
- break
174
-
175
- df = pd.DataFrame([j[:6] for j in data[tk]],
176
- columns=['date', 'open', 'close', 'high', 'low', 'vol']).set_index('date')
177
- for col in ['open', 'high', 'low', 'close', 'vol']:
178
- df[col] = pd.to_numeric(df[col], errors='coerce')
179
- if 'qt' in data:
180
- name = data['qt'][symbol][1]
181
-
182
- result = (symbol, name, df)
183
- cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
184
- self._put_cache(cache_key, result)
185
- return result
186
-
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes