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.
- {rquote-0.4.0 → rquote-0.4.2}/PKG-INFO +4 -4
- {rquote-0.4.0 → rquote-0.4.2}/README.md +3 -3
- {rquote-0.4.0 → rquote-0.4.2}/pyproject.toml +1 -1
- {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/persistent.py +140 -53
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/base.py +62 -16
- {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/PKG-INFO +4 -4
- {rquote-0.4.0 → rquote-0.4.2}/rquote/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/api/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/api/lists.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/api/price.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/api/stock_info.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/api/tick.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/base.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/cache/memory.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/config.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/base.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/sina.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/data_sources/tencent.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/exceptions.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/factors/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/factors/technical.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/cn_stock.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/factory.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/future.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/hk_stock.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/markets/us_stock.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/parsers/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/parsers/kline.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/plots.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/__init__.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/date.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/helpers.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/http.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/logging.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils/web.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote/utils.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/SOURCES.txt +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/dependency_links.txt +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/requires.txt +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/rquote.egg-info/top_level.txt +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/setup.cfg +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/tests/test_api.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/tests/test_cache.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/tests/test_config.py +0 -0
- {rquote-0.4.0 → rquote-0.4.2}/tests/test_exceptions.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
274
|
+
# 检查是否有重叠:如果请求的日期范围与缓存数据有重叠,就返回过滤后的数据
|
|
275
|
+
# 注意:即使缓存中有部分数据,也应该返回(让上层决定是否需要扩展)
|
|
276
|
+
has_overlap = True
|
|
277
|
+
if request_edate and request_edate < cached_earliest:
|
|
220
278
|
# 请求的结束日期早于缓存的最早日期,无重叠
|
|
221
|
-
|
|
222
|
-
if request_sdate and
|
|
279
|
+
has_overlap = False
|
|
280
|
+
if request_sdate and request_sdate > cached_latest:
|
|
223
281
|
# 请求的开始日期晚于缓存的最晚日期,无重叠
|
|
224
|
-
|
|
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
|
-
|
|
233
|
-
|
|
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
|
-
|
|
266
|
-
|
|
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
|
-
|
|
333
|
+
# 检查是否有重叠:如果请求的日期范围与缓存数据有重叠,就返回过滤后的数据
|
|
334
|
+
# 注意:即使缓存中有部分数据,也应该返回(让上层决定是否需要扩展)
|
|
335
|
+
has_overlap = True
|
|
336
|
+
if request_edate and request_edate < cached_earliest:
|
|
274
337
|
# 请求的结束日期早于缓存的最早日期,无重叠
|
|
275
|
-
|
|
276
|
-
if request_sdate and
|
|
338
|
+
has_overlap = False
|
|
339
|
+
if request_sdate and request_sdate > cached_latest:
|
|
277
340
|
# 请求的开始日期晚于缓存的最晚日期,无重叠
|
|
278
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|