rquote 0.4.0__tar.gz → 0.4.2__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 (49) hide show
  1. {rquote-0.4.0 → rquote-0.4.2}/PKG-INFO +4 -4
  2. {rquote-0.4.0 → rquote-0.4.2}/README.md +3 -3
  3. {rquote-0.4.0 → rquote-0.4.2}/pyproject.toml +1 -1
  4. {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/persistent.py +140 -53
  5. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/base.py +62 -16
  6. {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/PKG-INFO +4 -4
  7. {rquote-0.4.0 → rquote-0.4.2}/rquote/__init__.py +0 -0
  8. {rquote-0.4.0 → rquote-0.4.2}/rquote/api/__init__.py +0 -0
  9. {rquote-0.4.0 → rquote-0.4.2}/rquote/api/lists.py +0 -0
  10. {rquote-0.4.0 → rquote-0.4.2}/rquote/api/price.py +0 -0
  11. {rquote-0.4.0 → rquote-0.4.2}/rquote/api/stock_info.py +0 -0
  12. {rquote-0.4.0 → rquote-0.4.2}/rquote/api/tick.py +0 -0
  13. {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/__init__.py +0 -0
  14. {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/base.py +0 -0
  15. {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/memory.py +0 -0
  16. {rquote-0.4.0 → rquote-0.4.2}/rquote/config.py +0 -0
  17. {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/__init__.py +0 -0
  18. {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/base.py +0 -0
  19. {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/sina.py +0 -0
  20. {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/tencent.py +0 -0
  21. {rquote-0.4.0 → rquote-0.4.2}/rquote/exceptions.py +0 -0
  22. {rquote-0.4.0 → rquote-0.4.2}/rquote/factors/__init__.py +0 -0
  23. {rquote-0.4.0 → rquote-0.4.2}/rquote/factors/technical.py +0 -0
  24. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/__init__.py +0 -0
  25. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/cn_stock.py +0 -0
  26. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/factory.py +0 -0
  27. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/future.py +0 -0
  28. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/hk_stock.py +0 -0
  29. {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/us_stock.py +0 -0
  30. {rquote-0.4.0 → rquote-0.4.2}/rquote/parsers/__init__.py +0 -0
  31. {rquote-0.4.0 → rquote-0.4.2}/rquote/parsers/kline.py +0 -0
  32. {rquote-0.4.0 → rquote-0.4.2}/rquote/plots.py +0 -0
  33. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/__init__.py +0 -0
  34. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/date.py +0 -0
  35. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/helpers.py +0 -0
  36. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/http.py +0 -0
  37. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/logging.py +0 -0
  38. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/web.py +0 -0
  39. {rquote-0.4.0 → rquote-0.4.2}/rquote/utils.py +0 -0
  40. {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/SOURCES.txt +0 -0
  41. {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/dependency_links.txt +0 -0
  42. {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/requires.txt +0 -0
  43. {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/top_level.txt +0 -0
  44. {rquote-0.4.0 → rquote-0.4.2}/setup.cfg +0 -0
  45. {rquote-0.4.0 → rquote-0.4.2}/tests/test_api.py +0 -0
  46. {rquote-0.4.0 → rquote-0.4.2}/tests/test_cache.py +0 -0
  47. {rquote-0.4.0 → rquote-0.4.2}/tests/test_config.py +0 -0
  48. {rquote-0.4.0 → rquote-0.4.2}/tests/test_exceptions.py +0 -0
  49. {rquote-0.4.0 → rquote-0.4.2}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rquote
3
- Version: 0.4.0
3
+ Version: 0.4.2
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.3.5**
21
+ 当前版本:**0.4.2**
22
22
 
23
23
  ## 主要特性
24
24
 
@@ -199,13 +199,13 @@ stocks = get_cn_stock_list(money_min=5e8)
199
199
 
200
200
  #### `get_hk_stocks_500()`
201
201
 
202
- 获取港股前500只股票列表
202
+ 获取港股前500只股票列表(按当日成交额排序)
203
203
 
204
204
  ```python
205
205
  from rquote import get_hk_stocks_500
206
206
 
207
207
  stocks = get_hk_stocks_500()
208
- # 返回格式: [[code, name, price, turnover, ...], ...]
208
+ # 返回格式: [[code, name, price, -, -, -, -, volume, turnover, ...], ...]
209
209
  ```
210
210
 
211
211
  #### `get_us_stocks(k=100)`
@@ -4,7 +4,7 @@
4
4
 
5
5
  ## 版本信息
6
6
 
7
- 当前版本:**0.3.5**
7
+ 当前版本:**0.4.2**
8
8
 
9
9
  ## 主要特性
10
10
 
@@ -185,13 +185,13 @@ stocks = get_cn_stock_list(money_min=5e8)
185
185
 
186
186
  #### `get_hk_stocks_500()`
187
187
 
188
- 获取港股前500只股票列表
188
+ 获取港股前500只股票列表(按当日成交额排序)
189
189
 
190
190
  ```python
191
191
  from rquote import get_hk_stocks_500
192
192
 
193
193
  stocks = get_hk_stocks_500()
194
- # 返回格式: [[code, name, price, turnover, ...], ...]
194
+ # 返回格式: [[code, name, price, -, -, -, -, volume, turnover, ...], ...]
195
195
  ```
196
196
 
197
197
  #### `get_us_stocks(k=100)`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "rquote"
7
- version = "0.4.0"
7
+ version = "0.4.2"
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
@@ -9,6 +9,13 @@ from typing import Optional, Any, Tuple
9
9
  import pandas as pd
10
10
  from .base import Cache
11
11
 
12
+ # 导入日志
13
+ try:
14
+ from ..utils.logging import logger
15
+ except ImportError:
16
+ import logging
17
+ logger = logging.getLogger(__name__)
18
+
12
19
  # 尝试导入 duckdb(可选依赖)
13
20
  try:
14
21
  import duckdb
@@ -125,8 +132,20 @@ class PersistentCache(Cache):
125
132
 
126
133
  def _get_dataframe_date_range(self, df: pd.DataFrame) -> Tuple[Optional[pd.Timestamp], Optional[pd.Timestamp]]:
127
134
  """获取 DataFrame 的日期范围"""
128
- if df.empty or not isinstance(df.index, pd.DatetimeIndex):
135
+ if df.empty:
136
+ return None, None
137
+
138
+ # 如果索引不是 DatetimeIndex,尝试转换
139
+ if not isinstance(df.index, pd.DatetimeIndex):
140
+ try:
141
+ # 尝试转换为 DatetimeIndex
142
+ index = pd.to_datetime(df.index)
143
+ if len(index) > 0:
144
+ return index.min(), index.max()
145
+ except (ValueError, TypeError):
146
+ pass
129
147
  return None, None
148
+
130
149
  return df.index.min(), df.index.max()
131
150
 
132
151
  def _filter_dataframe_by_date(self, df: pd.DataFrame, sdate: Optional[str] = None,
@@ -164,29 +183,56 @@ class PersistentCache(Cache):
164
183
  combined = combined.sort_index()
165
184
  return combined
166
185
 
167
- def get(self, key: str) -> Optional[Any]:
186
+ def get(self, key: str, sdate: Optional[str] = None, edate: Optional[str] = None) -> Optional[Any]:
168
187
  """
169
188
  获取缓存数据
170
189
 
171
190
  Args:
172
- key: 缓存 key,格式如 "symbol:sdate:edate:freq:days:fq"
191
+ key: 缓存 key,可以是完整格式 "symbol:sdate:edate:freq:days:fq"
192
+ 或 base_key 格式 "symbol:freq:fq"
193
+ sdate: 开始日期(可选,如果 key 是 base_key 格式则必须提供)
194
+ edate: 结束日期(可选,如果 key 是 base_key 格式则必须提供)
173
195
 
174
196
  Returns:
175
197
  (symbol, name, DataFrame) 或 None
176
198
  """
177
- symbol, sdate, edate, freq, fq = self._extract_key_parts(key)
178
- base_key = self._get_base_key(symbol, freq, fq)
199
+ # 判断 key 格式:如果是 base_key 格式(只有3部分),使用参数中的日期
200
+ parts = key.split(':')
201
+ if len(parts) == 3:
202
+ # base_key 格式:symbol:freq:fq
203
+ symbol, freq, fq = parts
204
+ base_key = key
205
+ # 使用参数中的日期,如果没有则使用空字符串
206
+ sdate = sdate or ''
207
+ edate = edate or ''
208
+ else:
209
+ # 完整 key 格式:symbol:sdate:edate:freq:days:fq
210
+ symbol, sdate_from_key, edate_from_key, freq, fq = self._extract_key_parts(key)
211
+ base_key = self._get_base_key(symbol, freq, fq)
212
+ # 优先使用参数中的日期,如果没有则使用 key 中的日期
213
+ sdate = sdate if sdate is not None else sdate_from_key
214
+ edate = edate if edate is not None else edate_from_key
215
+
216
+ logger.info(f"[CACHE GET] key={key}, base_key={base_key}, sdate={sdate}, edate={edate}")
179
217
 
180
218
  if self.use_duckdb:
181
- return self._get_duckdb(base_key, symbol, sdate, edate, freq, fq)
219
+ result = self._get_duckdb(base_key, symbol, sdate, edate, freq, fq)
220
+ else:
221
+ result = self._get_pickle(base_key, symbol, sdate, edate, freq, fq)
222
+
223
+ if result:
224
+ _, _, df = result
225
+ logger.info(f"[CACHE HIT] key={key}, 返回数据行数={len(df)}, 日期范围={df.index.min()} 到 {df.index.max()}")
182
226
  else:
183
- return self._get_pickle(base_key, symbol, sdate, edate, freq, fq)
227
+ logger.info(f"[CACHE MISS] key={key}, 缓存中无数据")
228
+
229
+ return result
184
230
 
185
231
  def _get_duckdb(self, base_key: str, symbol: str, sdate: str, edate: str,
186
232
  freq: str, fq: str) -> Optional[Tuple[str, str, pd.DataFrame]]:
187
233
  """从 duckdb 获取数据"""
188
234
  result = self.conn.execute("""
189
- SELECT name, data, earliest_date, latest_date, expire_at
235
+ SELECT name, data, expire_at
190
236
  FROM cache_data
191
237
  WHERE cache_key = ?
192
238
  """, [base_key]).fetchone()
@@ -194,7 +240,7 @@ class PersistentCache(Cache):
194
240
  if not result:
195
241
  return None
196
242
 
197
- name, data_blob, earliest_date, latest_date, expire_at = result
243
+ name, data_blob, expire_at = result
198
244
 
199
245
  # 检查过期
200
246
  if self.ttl and expire_at:
@@ -207,33 +253,40 @@ class PersistentCache(Cache):
207
253
  import pickle
208
254
  df = pickle.loads(data_blob)
209
255
 
210
- # 获取缓存数据的日期范围
211
- cached_earliest = self._parse_date(earliest_date)
212
- cached_latest = self._parse_date(latest_date)
256
+ # 确保索引是 DatetimeIndex
257
+ if not isinstance(df.index, pd.DatetimeIndex):
258
+ try:
259
+ df.index = pd.to_datetime(df.index)
260
+ except (ValueError, TypeError):
261
+ return None
262
+
263
+ if df.empty:
264
+ return None
265
+
266
+ # 直接从 DataFrame 索引获取实际的日期范围
267
+ cached_earliest = df.index.min()
268
+ cached_latest = df.index.max()
213
269
 
214
- # 如果请求的日期范围完全在缓存范围内,直接返回过滤后的数据
270
+ # 解析请求的日期范围
215
271
  request_sdate = self._parse_date(sdate) if sdate else None
216
272
  request_edate = self._parse_date(edate) if edate else None
217
273
 
218
- # 检查是否有重叠
219
- if request_edate and cached_earliest and request_edate < cached_earliest:
274
+ # 检查是否有重叠:如果请求的日期范围与缓存数据有重叠,就返回过滤后的数据
275
+ # 注意:即使缓存中有部分数据,也应该返回(让上层决定是否需要扩展)
276
+ has_overlap = True
277
+ if request_edate and request_edate < cached_earliest:
220
278
  # 请求的结束日期早于缓存的最早日期,无重叠
221
- return None
222
- if request_sdate and cached_latest and request_sdate > cached_latest:
279
+ has_overlap = False
280
+ if request_sdate and request_sdate > cached_latest:
223
281
  # 请求的开始日期晚于缓存的最晚日期,无重叠
224
- return None
282
+ has_overlap = False
225
283
 
226
- # 有重叠,返回缓存中可用的部分数据
227
- # 计算实际可用的日期范围
228
- actual_sdate = max(request_sdate, cached_earliest) if request_sdate and cached_earliest else (request_sdate or cached_earliest)
229
- actual_edate = min(request_edate, cached_latest) if request_edate and cached_latest else (request_edate or cached_latest)
284
+ if not has_overlap:
285
+ return None
230
286
 
231
- # 过滤数据
232
- filtered_df = self._filter_dataframe_by_date(
233
- df,
234
- actual_sdate.strftime('%Y-%m-%d') if actual_sdate else None,
235
- actual_edate.strftime('%Y-%m-%d') if actual_edate else None
236
- )
287
+ # 按照请求的日期范围过滤数据(即使缓存中有更多数据,也只返回请求范围内的)
288
+ # 重要:必须按照 edate 截取,和从网络获取的行为一致
289
+ filtered_df = self._filter_dataframe_by_date(df, sdate, edate)
237
290
 
238
291
  if filtered_df.empty:
239
292
  return None
@@ -258,36 +311,41 @@ class PersistentCache(Cache):
258
311
 
259
312
  df = cache_entry['data']
260
313
  name = cache_entry.get('name', '')
261
- earliest_date = cache_entry.get('earliest_date')
262
- latest_date = cache_entry.get('latest_date')
263
314
 
264
- # 获取缓存数据的日期范围
265
- cached_earliest = self._parse_date(earliest_date)
266
- cached_latest = self._parse_date(latest_date)
315
+ # 确保索引是 DatetimeIndex
316
+ if not isinstance(df.index, pd.DatetimeIndex):
317
+ try:
318
+ df.index = pd.to_datetime(df.index)
319
+ except (ValueError, TypeError):
320
+ return None
321
+
322
+ if df.empty:
323
+ return None
324
+
325
+ # 直接从 DataFrame 索引获取实际的日期范围
326
+ cached_earliest = df.index.min()
327
+ cached_latest = df.index.max()
267
328
 
268
- # 如果请求的日期范围完全在缓存范围内,直接返回过滤后的数据
329
+ # 解析请求的日期范围
269
330
  request_sdate = self._parse_date(sdate) if sdate else None
270
331
  request_edate = self._parse_date(edate) if edate else None
271
332
 
272
- # 检查是否有重叠
273
- if request_edate and cached_earliest and request_edate < cached_earliest:
333
+ # 检查是否有重叠:如果请求的日期范围与缓存数据有重叠,就返回过滤后的数据
334
+ # 注意:即使缓存中有部分数据,也应该返回(让上层决定是否需要扩展)
335
+ has_overlap = True
336
+ if request_edate and request_edate < cached_earliest:
274
337
  # 请求的结束日期早于缓存的最早日期,无重叠
275
- return None
276
- if request_sdate and cached_latest and request_sdate > cached_latest:
338
+ has_overlap = False
339
+ if request_sdate and request_sdate > cached_latest:
277
340
  # 请求的开始日期晚于缓存的最晚日期,无重叠
278
- return None
341
+ has_overlap = False
279
342
 
280
- # 有重叠,返回缓存中可用的部分数据
281
- # 计算实际可用的日期范围
282
- actual_sdate = max(request_sdate, cached_earliest) if request_sdate and cached_earliest else (request_sdate or cached_earliest)
283
- actual_edate = min(request_edate, cached_latest) if request_edate and cached_latest else (request_edate or cached_latest)
343
+ if not has_overlap:
344
+ return None
284
345
 
285
- # 过滤数据
286
- filtered_df = self._filter_dataframe_by_date(
287
- df,
288
- actual_sdate.strftime('%Y-%m-%d') if actual_sdate else None,
289
- actual_edate.strftime('%Y-%m-%d') if actual_edate else None
290
- )
346
+ # 按照请求的日期范围过滤数据(即使缓存中有更多数据,也只返回请求范围内的)
347
+ # 重要:必须按照 edate 截取,和从网络获取的行为一致
348
+ filtered_df = self._filter_dataframe_by_date(df, sdate, edate)
291
349
 
292
350
  if filtered_df.empty:
293
351
  return None
@@ -299,7 +357,8 @@ class PersistentCache(Cache):
299
357
  存储缓存数据
300
358
 
301
359
  Args:
302
- key: 缓存 key
360
+ key: 缓存 key,可以是完整格式 "symbol:sdate:edate:freq:days:fq"
361
+ 或 base_key 格式 "symbol:freq:fq"(推荐使用 base_key)
303
362
  value: (symbol, name, DataFrame) 元组
304
363
  ttl: 过期时间(秒)
305
364
  """
@@ -310,8 +369,25 @@ class PersistentCache(Cache):
310
369
  if not isinstance(df, pd.DataFrame) or df.empty:
311
370
  return
312
371
 
313
- _, _, _, freq, fq = self._extract_key_parts(key)
314
- base_key = self._get_base_key(symbol, freq, fq)
372
+ logger.info(f"[CACHE PUT] key={key}, 数据行数={len(df)}, 日期范围={df.index.min() if not df.empty else 'N/A'} 到 {df.index.max() if not df.empty else 'N/A'}")
373
+
374
+ # 确保索引是 DatetimeIndex(用于正确获取日期范围)
375
+ if not isinstance(df.index, pd.DatetimeIndex):
376
+ try:
377
+ df.index = pd.to_datetime(df.index)
378
+ except (ValueError, TypeError):
379
+ pass # 如果转换失败,继续处理(_get_dataframe_date_range 会处理)
380
+
381
+ # 判断 key 格式:如果是 base_key 格式(只有3部分),直接使用
382
+ parts = key.split(':')
383
+ if len(parts) == 3:
384
+ # base_key 格式:symbol:freq:fq
385
+ base_key = key
386
+ freq, fq = parts[1], parts[2]
387
+ else:
388
+ # 完整 key 格式:symbol:sdate:edate:freq:days:fq
389
+ _, _, _, freq, fq = self._extract_key_parts(key)
390
+ base_key = self._get_base_key(symbol, freq, fq)
315
391
 
316
392
  # 尝试从基础 key 获取完整数据并合并
317
393
  existing = self._get_raw(base_key)
@@ -322,6 +398,12 @@ class PersistentCache(Cache):
322
398
  name = existing_name
323
399
  # 合并数据
324
400
  df = self._merge_dataframes(existing_df, df)
401
+ # 合并后再次确保索引是 DatetimeIndex
402
+ if not isinstance(df.index, pd.DatetimeIndex):
403
+ try:
404
+ df.index = pd.to_datetime(df.index)
405
+ except (ValueError, TypeError):
406
+ pass
325
407
 
326
408
  # 获取日期范围
327
409
  earliest_date, latest_date = self._get_dataframe_date_range(df)
@@ -338,6 +420,8 @@ class PersistentCache(Cache):
338
420
  self._put_duckdb(base_key, symbol, name, df, earliest_str, latest_str, freq, fq, expire_at)
339
421
  else:
340
422
  self._put_pickle(base_key, symbol, name, df, earliest_str, latest_str, freq, fq, expire_at)
423
+
424
+ logger.info(f"[CACHE PUT] 存储完成, base_key={base_key}, 日期范围={earliest_str} 到 {latest_str}")
341
425
 
342
426
  def _get_raw(self, base_key: str) -> Optional[Tuple[str, str, pd.DataFrame]]:
343
427
  """获取原始数据(不进行日期过滤)"""
@@ -363,7 +447,10 @@ class PersistentCache(Cache):
363
447
  def _put_duckdb(self, base_key: str, symbol: str, name: str, df: pd.DataFrame,
364
448
  earliest_date: Optional[str], latest_date: Optional[str],
365
449
  freq: str, fq: str, expire_at: Optional[pd.Timestamp]):
366
- """存储到 duckdb"""
450
+ """存储到 duckdb
451
+
452
+ 注意:earliest_date 和 latest_date 仅用于记录,实际查询时从 DataFrame 索引获取
453
+ """
367
454
  import pickle
368
455
  data_blob = pickle.dumps(df)
369
456
 
@@ -9,6 +9,13 @@ from datetime import datetime, timedelta
9
9
  from ..cache import Cache
10
10
  from ..data_sources.base import DataSource
11
11
 
12
+ # 导入日志
13
+ try:
14
+ from ..utils.logging import logger
15
+ except ImportError:
16
+ import logging
17
+ logger = logging.getLogger(__name__)
18
+
12
19
  # 尝试导入持久化缓存(可选依赖)
13
20
  try:
14
21
  from ..cache.persistent import PersistentCache
@@ -84,10 +91,23 @@ class Market(ABC):
84
91
  """标准化股票代码"""
85
92
  pass
86
93
 
87
- def _get_cached(self, key: str) -> Optional[Tuple[str, str, pd.DataFrame]]:
94
+ def _get_cached(self, key: str, sdate: str = '', edate: str = '') -> Optional[Tuple[str, str, pd.DataFrame]]:
88
95
  """从缓存获取数据"""
89
96
  if self.cache:
90
- cached = self.cache.get(key)
97
+ # 如果是 PersistentCache,使用 base_key + 日期参数的方式
98
+ if PersistentCache and isinstance(self.cache, PersistentCache):
99
+ # 从完整 key 中提取 base_key
100
+ parts = key.split(':')
101
+ if len(parts) >= 3:
102
+ symbol = parts[0]
103
+ freq = parts[3] if len(parts) > 3 else 'day'
104
+ fq = parts[5] if len(parts) > 5 else 'qfq'
105
+ base_key = f"{symbol}:{freq}:{fq}"
106
+ cached = self.cache.get(base_key, sdate=sdate, edate=edate)
107
+ else:
108
+ cached = self.cache.get(key)
109
+ else:
110
+ cached = self.cache.get(key)
91
111
  if cached:
92
112
  return cached
93
113
  return None
@@ -95,7 +115,20 @@ class Market(ABC):
95
115
  def _put_cache(self, key: str, value: Tuple[str, str, pd.DataFrame]) -> None:
96
116
  """存入缓存"""
97
117
  if self.cache:
98
- self.cache.put(key, value)
118
+ # 如果是 PersistentCache,使用 base_key 存储
119
+ if PersistentCache and isinstance(self.cache, PersistentCache):
120
+ # 从完整 key 中提取 base_key
121
+ parts = key.split(':')
122
+ if len(parts) >= 3:
123
+ symbol = parts[0]
124
+ freq = parts[3] if len(parts) > 3 else 'day'
125
+ fq = parts[5] if len(parts) > 5 else 'qfq'
126
+ base_key = f"{symbol}:{freq}:{fq}"
127
+ self.cache.put(base_key, value)
128
+ else:
129
+ self.cache.put(key, value)
130
+ else:
131
+ self.cache.put(key, value)
99
132
 
100
133
  def _get_price_with_persistent_cache(self, symbol: str, sdate: str, edate: str,
101
134
  freq: str, days: int, fq: str,
@@ -108,14 +141,18 @@ class Market(ABC):
108
141
  """
109
142
  cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
110
143
 
111
- # 尝试从缓存获取
112
- cached = self._get_cached(cache_key)
144
+ logger.info(f"[PRICE GET] symbol={symbol}, sdate={sdate}, edate={edate}, freq={freq}, cache_key={cache_key}")
145
+
146
+ # 尝试从缓存获取(传入日期参数,PersistentCache 会使用 base_key + 日期参数)
147
+ cached = self._get_cached(cache_key, sdate=sdate, edate=edate)
113
148
  if cached:
114
149
  _, name, cached_df = cached
150
+ logger.info(f"[PRICE CACHE HIT] symbol={symbol}, 缓存数据行数={len(cached_df)}, 日期范围={cached_df.index.min() if not cached_df.empty else 'N/A'} 到 {cached_df.index.max() if not cached_df.empty else 'N/A'}")
115
151
 
116
152
  # 检查是否需要扩展
117
153
  if cached_df.empty or not isinstance(cached_df.index, pd.DatetimeIndex):
118
154
  # 缓存为空或索引不是日期,直接获取新数据
155
+ logger.info(f"[PRICE FETCH] 缓存数据无效,从网络获取 symbol={symbol}, sdate={sdate}, edate={edate}")
119
156
  result = fetch_func(symbol, sdate, edate, freq, days, fq)
120
157
  self._put_cache(cache_key, result)
121
158
  return result
@@ -146,11 +183,25 @@ class Market(ABC):
146
183
 
147
184
  # 如果需要扩展,获取缺失的数据
148
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}")
149
187
  # 获取扩展的数据
150
188
  extended_result = fetch_func(symbol, extend_sdate, extend_edate, freq, days, fq)
151
189
  _, _, extended_df = extended_result
190
+ logger.info(f"[PRICE FETCH] 从网络获取扩展数据, 数据行数={len(extended_df)}")
152
191
 
153
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
+
154
205
  # 合并数据
155
206
  merged_df = pd.concat([cached_df, extended_df])
156
207
  merged_df = merged_df[~merged_df.index.duplicated(keep='last')]
@@ -172,22 +223,17 @@ class Market(ABC):
172
223
  return result
173
224
 
174
225
  # 不需要扩展,直接返回缓存的数据
175
- # 过滤到请求的日期范围
176
- if request_sdate or request_edate:
177
- if request_sdate and request_edate:
178
- mask = (cached_df.index >= request_sdate) & (cached_df.index <= request_edate)
179
- elif request_sdate:
180
- mask = cached_df.index >= request_sdate
181
- else:
182
- mask = cached_df.index <= request_edate
183
- filtered_df = cached_df[mask]
184
- return (symbol, name, filtered_df)
185
-
226
+ # 注意:PersistentCache.get() 已经根据请求的日期范围进行了过滤,
227
+ # 返回的数据已经是过滤后的,不需要再次过滤
228
+ logger.info(f"[PRICE RETURN] 直接返回缓存数据, symbol={symbol}, 数据行数={len(cached_df)}")
186
229
  return (symbol, name, cached_df)
187
230
 
188
231
  # 缓存未命中,直接获取
189
232
  if fetch_func:
233
+ logger.info(f"[PRICE FETCH] 缓存未命中,从网络获取 symbol={symbol}, sdate={sdate}, edate={edate}")
190
234
  result = fetch_func(symbol, sdate, edate, freq, days, fq)
235
+ _, _, df = result
236
+ logger.info(f"[PRICE FETCH] 网络获取完成, 数据行数={len(df)}, 准备存储到缓存")
191
237
  self._put_cache(cache_key, result)
192
238
  return result
193
239
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rquote
3
- Version: 0.4.0
3
+ Version: 0.4.2
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.3.5**
21
+ 当前版本:**0.4.2**
22
22
 
23
23
  ## 主要特性
24
24
 
@@ -199,13 +199,13 @@ stocks = get_cn_stock_list(money_min=5e8)
199
199
 
200
200
  #### `get_hk_stocks_500()`
201
201
 
202
- 获取港股前500只股票列表
202
+ 获取港股前500只股票列表(按当日成交额排序)
203
203
 
204
204
  ```python
205
205
  from rquote import get_hk_stocks_500
206
206
 
207
207
  stocks = get_hk_stocks_500()
208
- # 返回格式: [[code, name, price, turnover, ...], ...]
208
+ # 返回格式: [[code, name, price, -, -, -, -, volume, turnover, ...], ...]
209
209
  ```
210
210
 
211
211
  #### `get_us_stocks(k=100)`
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
File without changes
File without changes
File without changes
File without changes