rquote 0.3.8__tar.gz → 0.3.9__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.3.8 → rquote-0.3.9}/PKG-INFO +1 -1
- {rquote-0.3.8 → rquote-0.3.9}/pyproject.toml +1 -1
- {rquote-0.3.8 → rquote-0.3.9}/rquote/api/price.py +43 -2
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/cn_stock.py +4 -1
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/future.py +90 -4
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/hk_stock.py +1 -1
- {rquote-0.3.8 → rquote-0.3.9}/rquote/parsers/kline.py +12 -4
- {rquote-0.3.8 → rquote-0.3.9}/rquote.egg-info/PKG-INFO +1 -1
- {rquote-0.3.8 → rquote-0.3.9}/README.md +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/api/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/api/lists.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/api/stock_info.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/api/tick.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/cache/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/cache/base.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/cache/memory.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/config.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/data_sources/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/data_sources/base.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/data_sources/sina.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/data_sources/tencent.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/exceptions.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/factors/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/factors/technical.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/base.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/factory.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/markets/us_stock.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/parsers/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/plots.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/__init__.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/date.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/helpers.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/http.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/logging.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils/web.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote/utils.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote.egg-info/SOURCES.txt +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote.egg-info/dependency_links.txt +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote.egg-info/requires.txt +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/rquote.egg-info/top_level.txt +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/setup.cfg +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/tests/test_api.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/tests/test_cache.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/tests/test_config.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/tests/test_exceptions.py +0 -0
- {rquote-0.3.8 → rquote-0.3.9}/tests/test_utils.py +0 -0
|
@@ -11,6 +11,33 @@ from ..utils.date import check_date_format
|
|
|
11
11
|
from ..exceptions import SymbolError
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _normalize_dataframe_index(df: pd.DataFrame) -> pd.DataFrame:
|
|
15
|
+
"""
|
|
16
|
+
统一处理 DataFrame 索引,转换为 DatetimeIndex
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
df: 输入的 DataFrame
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
处理后的 DataFrame,索引为 DatetimeIndex
|
|
23
|
+
"""
|
|
24
|
+
if df.empty:
|
|
25
|
+
return df
|
|
26
|
+
|
|
27
|
+
# 如果已经是 DatetimeIndex,直接返回
|
|
28
|
+
if isinstance(df.index, pd.DatetimeIndex):
|
|
29
|
+
return df
|
|
30
|
+
|
|
31
|
+
# 尝试转换为 DatetimeIndex
|
|
32
|
+
try:
|
|
33
|
+
df.index = pd.to_datetime(df.index)
|
|
34
|
+
except (ValueError, TypeError) as e:
|
|
35
|
+
# 如果转换失败,保持原样(可能是其他类型的索引)
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return df
|
|
39
|
+
|
|
40
|
+
|
|
14
41
|
def get_price(i: str, sdate: str = '', edate: str = '', freq: str = 'day',
|
|
15
42
|
days: int = 320, fq: str = 'qfq', dd=None) -> Tuple[str, str, pd.DataFrame]:
|
|
16
43
|
'''
|
|
@@ -49,7 +76,12 @@ def get_price(i: str, sdate: str = '', edate: str = '', freq: str = 'day',
|
|
|
49
76
|
market = MarketFactory.create_from_symbol(i, cache=cache)
|
|
50
77
|
|
|
51
78
|
# 调用市场的get_price方法
|
|
52
|
-
|
|
79
|
+
symbol, name, df = market.get_price(i, sdate=sdate, edate=edate, freq=freq, days=days, fq=fq)
|
|
80
|
+
|
|
81
|
+
# 统一后处理:转换索引为 DatetimeIndex
|
|
82
|
+
df = _normalize_dataframe_index(df)
|
|
83
|
+
|
|
84
|
+
return symbol, name, df
|
|
53
85
|
|
|
54
86
|
|
|
55
87
|
def get_price_longer(i: str, l: int = 2, dd=None) -> Tuple[str, str, pd.DataFrame]:
|
|
@@ -65,7 +97,16 @@ def get_price_longer(i: str, l: int = 2, dd=None) -> Tuple[str, str, pd.DataFram
|
|
|
65
97
|
(symbol, name, DataFrame)
|
|
66
98
|
"""
|
|
67
99
|
_, name, a = get_price(i, dd=dd)
|
|
68
|
-
|
|
100
|
+
# 使用 DatetimeIndex 的格式化方法(get_price 已统一转换为 DatetimeIndex)
|
|
101
|
+
if isinstance(a.index, pd.DatetimeIndex) and len(a.index) > 0:
|
|
102
|
+
d1 = a.index[0].strftime('%Y%m%d')
|
|
103
|
+
else:
|
|
104
|
+
# 降级处理:如果索引不是 DatetimeIndex(理论上不应该发生),尝试格式化
|
|
105
|
+
try:
|
|
106
|
+
d1 = str(a.index[0])[:8] if len(str(a.index[0])) >= 8 else str(a.index[0])
|
|
107
|
+
except:
|
|
108
|
+
d1 = a.index.format()[0] if hasattr(a.index, 'format') else str(a.index[0])
|
|
109
|
+
|
|
69
110
|
for y in range(1, l):
|
|
70
111
|
d0 = str(int(d1[:4]) - 1) + d1[4:]
|
|
71
112
|
a = pd.concat((get_price(i, d0, d1, dd=dd)[2], a), 0).drop_duplicates()
|
|
@@ -82,7 +82,10 @@ class CNStockMarket(Market):
|
|
|
82
82
|
name = data['data']['name']
|
|
83
83
|
df = pd.DataFrame([i.split(',') for i in data['data']['klines']],
|
|
84
84
|
columns=['date', 'open', 'close', 'high', 'low', 'vol', 'money', 'p'])
|
|
85
|
-
df = df.set_index(['date'])
|
|
85
|
+
df = df.set_index(['date'])
|
|
86
|
+
# 转换数值列
|
|
87
|
+
for col in ['open', 'close', 'high', 'low', 'vol', 'money', 'p']:
|
|
88
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
86
89
|
|
|
87
90
|
result = (symbol, name, df)
|
|
88
91
|
self._put_cache(symbol, result)
|
|
@@ -27,7 +27,10 @@ class FutureMarket(Market):
|
|
|
27
27
|
|
|
28
28
|
# 特殊处理BTC
|
|
29
29
|
if symbol[2:5].lower() == 'btc':
|
|
30
|
-
|
|
30
|
+
if freq in ('min', '1min', 'minute'):
|
|
31
|
+
return self._get_btc_minute_price(symbol)
|
|
32
|
+
else:
|
|
33
|
+
return self._get_btc_price(symbol)
|
|
31
34
|
|
|
32
35
|
cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}"
|
|
33
36
|
cached = self._get_cached(cache_key)
|
|
@@ -49,7 +52,7 @@ class FutureMarket(Market):
|
|
|
49
52
|
return self._get_price_fallback(symbol, future_code, freq)
|
|
50
53
|
|
|
51
54
|
def _get_btc_price(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
|
|
52
|
-
"""
|
|
55
|
+
"""获取比特币日线价格"""
|
|
53
56
|
url = 'https://quotes.sina.cn/fx/api/openapi.php/BtcService.getDayKLine?symbol=btcbtcusd'
|
|
54
57
|
response = hget(url)
|
|
55
58
|
if not response:
|
|
@@ -60,12 +63,95 @@ class FutureMarket(Market):
|
|
|
60
63
|
columns=['date', 'open', 'high', 'low', 'close', 'vol', 'amount'])
|
|
61
64
|
for col in ['open', 'high', 'low', 'close', 'vol', 'amount']:
|
|
62
65
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
63
|
-
df = df.set_index('date')
|
|
66
|
+
df = df.set_index('date')
|
|
64
67
|
|
|
65
68
|
result = (symbol, 'BTC', df)
|
|
66
69
|
self._put_cache(symbol, result)
|
|
67
70
|
return result
|
|
68
71
|
|
|
72
|
+
def _get_btc_minute_price(self, symbol: str, datalen: int = 1440) -> Tuple[str, str, pd.DataFrame]:
|
|
73
|
+
"""
|
|
74
|
+
获取比特币分钟级价格
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
symbol: 股票代码(如 'fuBTC')
|
|
78
|
+
datalen: 数据长度,默认1440(24小时,每分钟1条)
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
(symbol, name, DataFrame)
|
|
82
|
+
"""
|
|
83
|
+
cache_key = f"{symbol}:min:{datalen}"
|
|
84
|
+
cached = self._get_cached(cache_key)
|
|
85
|
+
if cached:
|
|
86
|
+
return cached
|
|
87
|
+
|
|
88
|
+
url = f'https://quotes.sina.cn/fx/api/openapi.php/BtcService.getMinKline?symbol=btcbtcusd&scale=1&datalen={datalen}&callback=var%20_btcbtcusd'
|
|
89
|
+
response = hget(url)
|
|
90
|
+
if not response:
|
|
91
|
+
raise DataSourceError("Failed to fetch BTC minute data")
|
|
92
|
+
|
|
93
|
+
# 解析 JavaScript callback 格式: var _btcbtcusd({...})
|
|
94
|
+
text = response.text
|
|
95
|
+
|
|
96
|
+
# 移除开头的注释和脚本标签
|
|
97
|
+
if '*/' in text:
|
|
98
|
+
text = text.split('*/', 1)[1]
|
|
99
|
+
text = text.strip()
|
|
100
|
+
|
|
101
|
+
# 查找 JSON 部分(从第一个 { 开始)
|
|
102
|
+
json_start = text.find('{')
|
|
103
|
+
if json_start == -1:
|
|
104
|
+
raise DataSourceError("Invalid BTC minute data format: no JSON found")
|
|
105
|
+
|
|
106
|
+
# 提取 JSON 部分,需要找到匹配的最后一个 }
|
|
107
|
+
# 格式: var _btcbtcusd({...}) 或 var _btcbtcusd({...});
|
|
108
|
+
json_str = text[json_start:]
|
|
109
|
+
# 移除末尾可能的 ); 或 )
|
|
110
|
+
json_str = json_str.rstrip(');').rstrip(')')
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
data = json.loads(json_str)
|
|
114
|
+
except json.JSONDecodeError as e:
|
|
115
|
+
raise DataSourceError(f"Failed to parse BTC minute data JSON: {e}")
|
|
116
|
+
|
|
117
|
+
# 检查返回状态
|
|
118
|
+
if data.get('result', {}).get('status', {}).get('code') != 0:
|
|
119
|
+
raise DataSourceError(f"BTC API error: {data.get('result', {}).get('status', {}).get('msg', 'Unknown error')}")
|
|
120
|
+
|
|
121
|
+
# 提取数据
|
|
122
|
+
kline_data = data.get('result', {}).get('data', [])
|
|
123
|
+
if not kline_data:
|
|
124
|
+
raise DataSourceError("No BTC minute data returned")
|
|
125
|
+
|
|
126
|
+
# 转换为 DataFrame
|
|
127
|
+
# 数据格式: {"d":"2025-11-16 15:35:00","o":"95835.37","h":"95919.90","l":"95835.37","c":"95919.89","v":"6","a":"551441.4297"}
|
|
128
|
+
records = []
|
|
129
|
+
for item in kline_data:
|
|
130
|
+
records.append({
|
|
131
|
+
'date': item.get('d', ''),
|
|
132
|
+
'open': item.get('o', '0'),
|
|
133
|
+
'high': item.get('h', '0'),
|
|
134
|
+
'low': item.get('l', '0'),
|
|
135
|
+
'close': item.get('c', '0'),
|
|
136
|
+
'vol': item.get('v', '0'),
|
|
137
|
+
'amount': item.get('a', '0')
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
df = pd.DataFrame(records)
|
|
141
|
+
if df.empty:
|
|
142
|
+
raise DataSourceError("Empty BTC minute data")
|
|
143
|
+
|
|
144
|
+
# 转换数据类型
|
|
145
|
+
for col in ['open', 'high', 'low', 'close', 'vol', 'amount']:
|
|
146
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
147
|
+
|
|
148
|
+
# 设置索引
|
|
149
|
+
df = df.set_index('date')
|
|
150
|
+
|
|
151
|
+
result = (symbol, 'BTC', df)
|
|
152
|
+
self._put_cache(cache_key, result)
|
|
153
|
+
return result
|
|
154
|
+
|
|
69
155
|
def _get_price_fallback(self, symbol: str, future_code: str, freq: str) -> Tuple[str, str, pd.DataFrame]:
|
|
70
156
|
"""降级方法"""
|
|
71
157
|
from ..utils.helpers import load_js_var_json
|
|
@@ -84,7 +170,7 @@ class FutureMarket(Market):
|
|
|
84
170
|
df.columns = ['date', 'open', 'high', 'low', 'close', 'vol', 'p', 's']
|
|
85
171
|
for col in ['open', 'high', 'low', 'close', 'vol', 'p', 's']:
|
|
86
172
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
87
|
-
df = df.set_index('date')
|
|
173
|
+
df = df.set_index('date')
|
|
88
174
|
result = (symbol, future_code, df)
|
|
89
175
|
|
|
90
176
|
self._put_cache(f"{symbol}:{freq}", result)
|
|
@@ -11,7 +11,7 @@ class KlineParser:
|
|
|
11
11
|
"""K线数据解析器"""
|
|
12
12
|
|
|
13
13
|
@staticmethod
|
|
14
|
-
def parse_tencent_kline(data: Dict[str, Any], symbol: str) -> Tuple[str, pd.DataFrame]:
|
|
14
|
+
def parse_tencent_kline(data: Dict[str, Any], symbol: str, fq: str = 'qfq') -> Tuple[str, pd.DataFrame]:
|
|
15
15
|
"""
|
|
16
16
|
解析腾讯K线数据
|
|
17
17
|
|
|
@@ -27,9 +27,17 @@ class KlineParser:
|
|
|
27
27
|
if not symbol_data:
|
|
28
28
|
raise ParseError(f"No data for symbol {symbol}")
|
|
29
29
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
# 查找时间键,优先使用与fq参数匹配的键
|
|
31
|
+
# 根据fq参数确定优先级:qfq -> qfqday优先,hfq -> hfqday优先,否则day优先
|
|
32
|
+
if fq == 'qfq':
|
|
33
|
+
time_keys = ['qfqday', 'day', 'hfqday', 'qfqweek', 'week', 'hfqweek',
|
|
34
|
+
'qfqmonth', 'month', 'hfqmonth']
|
|
35
|
+
elif fq == 'hfq':
|
|
36
|
+
time_keys = ['hfqday', 'day', 'qfqday', 'hfqweek', 'week', 'qfqweek',
|
|
37
|
+
'hfqmonth', 'month', 'qfqmonth']
|
|
38
|
+
else:
|
|
39
|
+
time_keys = ['day', 'qfqday', 'hfqday', 'week', 'qfqweek', 'hfqweek',
|
|
40
|
+
'month', 'qfqmonth', 'hfqmonth']
|
|
33
41
|
tk = None
|
|
34
42
|
for tkt in time_keys:
|
|
35
43
|
if tkt in symbol_data:
|
|
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
|