rquote 0.3.4__py3-none-any.whl → 0.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rquote/__init__.py +53 -5
- rquote/api/__init__.py +32 -0
- rquote/api/lists.py +175 -0
- rquote/api/price.py +73 -0
- rquote/api/stock_info.py +49 -0
- rquote/api/tick.py +47 -0
- rquote/cache/__init__.py +9 -0
- rquote/cache/base.py +26 -0
- rquote/cache/memory.py +77 -0
- rquote/config.py +42 -0
- rquote/data_sources/__init__.py +10 -0
- rquote/data_sources/base.py +21 -0
- rquote/data_sources/sina.py +92 -0
- rquote/data_sources/tencent.py +90 -0
- rquote/exceptions.py +40 -0
- rquote/factors/__init__.py +8 -0
- rquote/factors/technical.py +150 -0
- rquote/markets/__init__.py +14 -0
- rquote/markets/base.py +49 -0
- rquote/markets/cn_stock.py +186 -0
- rquote/markets/factory.py +82 -0
- rquote/markets/future.py +92 -0
- rquote/markets/hk_stock.py +46 -0
- rquote/markets/us_stock.py +69 -0
- rquote/parsers/__init__.py +8 -0
- rquote/parsers/kline.py +104 -0
- rquote/plots.py +1 -1
- rquote/utils/__init__.py +13 -0
- rquote/utils/date.py +40 -0
- rquote/utils/helpers.py +23 -0
- rquote/utils/http.py +112 -0
- rquote/utils/logging.py +25 -0
- rquote/utils/web.py +104 -0
- rquote/utils.py +9 -195
- rquote-0.3.5.dist-info/METADATA +486 -0
- rquote-0.3.5.dist-info/RECORD +38 -0
- rquote/main.py +0 -503
- rquote-0.3.4.dist-info/METADATA +0 -286
- rquote-0.3.4.dist-info/RECORD +0 -8
- {rquote-0.3.4.dist-info → rquote-0.3.5.dist-info}/WHEEL +0 -0
- {rquote-0.3.4.dist-info → rquote-0.3.5.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
新浪数据源
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
from .base import DataSource
|
|
8
|
+
from ..utils.http import HTTPClient
|
|
9
|
+
from ..exceptions import DataSourceError, ParseError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class SinaDataSource(DataSource):
|
|
13
|
+
"""新浪数据源"""
|
|
14
|
+
|
|
15
|
+
BASE_URL_FUTURE = "https://stock2.finance.sina.com.cn/futures/api/jsonp.php/"
|
|
16
|
+
BASE_URL_TICK = "https://hq.sinajs.cn/?list="
|
|
17
|
+
|
|
18
|
+
def __init__(self, http_client: HTTPClient = None):
|
|
19
|
+
"""
|
|
20
|
+
初始化新浪数据源
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
http_client: HTTP客户端
|
|
24
|
+
"""
|
|
25
|
+
self.http_client = http_client or HTTPClient()
|
|
26
|
+
|
|
27
|
+
def fetch_kline(self, symbol: str, **kwargs) -> Dict[str, Any]:
|
|
28
|
+
"""
|
|
29
|
+
从新浪获取K线数据(主要用于期货)
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
symbol: 期货代码(不含fu前缀)
|
|
33
|
+
**kwargs: 其他参数
|
|
34
|
+
"""
|
|
35
|
+
freq = kwargs.get('freq', 'day')
|
|
36
|
+
|
|
37
|
+
if freq in ('min', '1min', 'minute'):
|
|
38
|
+
url = f"{self.BASE_URL_FUTURE}var%20t1nf_{symbol}=/InnerFuturesNewService.getMinLine?symbol={symbol}"
|
|
39
|
+
else:
|
|
40
|
+
url = f"{self.BASE_URL_FUTURE}var%20t1nf_{symbol}=/InnerFuturesNewService.getDailyKLine?symbol={symbol}"
|
|
41
|
+
|
|
42
|
+
response = self.http_client.get(url)
|
|
43
|
+
if not response:
|
|
44
|
+
raise DataSourceError(f"Failed to fetch from Sina: {symbol}")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
# 解析JavaScript变量赋值格式
|
|
48
|
+
text = response.text
|
|
49
|
+
# 提取JSON部分
|
|
50
|
+
json_start = text.find('[')
|
|
51
|
+
if json_start == -1:
|
|
52
|
+
json_start = text.find('{')
|
|
53
|
+
if json_start == -1:
|
|
54
|
+
raise ParseError("Invalid response format")
|
|
55
|
+
|
|
56
|
+
data = json.loads(text[json_start:])
|
|
57
|
+
return {'data': data}
|
|
58
|
+
except json.JSONDecodeError as e:
|
|
59
|
+
raise ParseError(f"Parse error: {e}")
|
|
60
|
+
|
|
61
|
+
def fetch_tick(self, symbols: List[str]) -> Dict[str, Any]:
|
|
62
|
+
"""
|
|
63
|
+
获取实时行情
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
symbols: 股票代码列表(美股需要gb_前缀)
|
|
67
|
+
"""
|
|
68
|
+
# 美股需要gb_前缀
|
|
69
|
+
if isinstance(symbols, str):
|
|
70
|
+
symbols = [symbols]
|
|
71
|
+
|
|
72
|
+
tick_symbols = ['gb_' + s.lower() if not s.startswith('gb_') else s for s in symbols]
|
|
73
|
+
url = f"{self.BASE_URL_TICK}{','.join(tick_symbols)}"
|
|
74
|
+
|
|
75
|
+
response = self.http_client.get(url)
|
|
76
|
+
if not response:
|
|
77
|
+
raise DataSourceError("Failed to fetch tick data from Sina")
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# 解析响应
|
|
81
|
+
lines = response.text.split(';\n')
|
|
82
|
+
result = []
|
|
83
|
+
for line in lines:
|
|
84
|
+
if ',' in line and '=' in line:
|
|
85
|
+
parts = line.split('=')
|
|
86
|
+
if len(parts) == 2:
|
|
87
|
+
data_str = parts[1].strip('"')
|
|
88
|
+
result.append(data_str.split(','))
|
|
89
|
+
return {'data': result}
|
|
90
|
+
except Exception as e:
|
|
91
|
+
raise ParseError(f"Parse tick error: {e}")
|
|
92
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
腾讯数据源
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
from .base import DataSource
|
|
8
|
+
from ..utils.http import HTTPClient
|
|
9
|
+
from ..exceptions import DataSourceError, ParseError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TencentDataSource(DataSource):
|
|
13
|
+
"""腾讯数据源"""
|
|
14
|
+
|
|
15
|
+
BASE_URL = "http://web.ifzq.gtimg.cn/appstock/app/newfqkline/get"
|
|
16
|
+
BASE_URL_HK = "http://web.ifzq.gtimg.cn/appstock/app/hkfqkline/get"
|
|
17
|
+
BASE_URL_US = "http://web.ifzq.gtimg.cn/appstock/app/usfqkline/get"
|
|
18
|
+
|
|
19
|
+
def __init__(self, http_client: HTTPClient = None):
|
|
20
|
+
"""
|
|
21
|
+
初始化腾讯数据源
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
http_client: HTTP客户端,如果为None则创建新实例
|
|
25
|
+
"""
|
|
26
|
+
self.http_client = http_client or HTTPClient()
|
|
27
|
+
|
|
28
|
+
def fetch_kline(self, symbol: str, freq: str = 'day',
|
|
29
|
+
sdate: str = '', edate: str = '',
|
|
30
|
+
days: int = 320, fq: str = 'qfq') -> Dict[str, Any]:
|
|
31
|
+
"""
|
|
32
|
+
从腾讯获取K线数据
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
symbol: 股票代码
|
|
36
|
+
freq: 频率
|
|
37
|
+
sdate: 开始日期
|
|
38
|
+
edate: 结束日期
|
|
39
|
+
days: 天数
|
|
40
|
+
fq: 复权方式
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
数据字典
|
|
44
|
+
"""
|
|
45
|
+
# 根据市场选择URL
|
|
46
|
+
if symbol[:2] in ['sh', 'sz']:
|
|
47
|
+
url = f"{self.BASE_URL}?param={symbol},{freq},{sdate},{edate},{days},{fq}"
|
|
48
|
+
elif symbol[:2] == 'hk':
|
|
49
|
+
url = f"{self.BASE_URL_HK}?param={symbol},{freq},{sdate},{edate},{days},{fq}"
|
|
50
|
+
elif symbol[:2] == 'us':
|
|
51
|
+
url = f"{self.BASE_URL_US}?param={symbol},{freq},{sdate},{edate},{days},{fq}"
|
|
52
|
+
else:
|
|
53
|
+
raise DataSourceError(f"Unsupported symbol format: {symbol}")
|
|
54
|
+
|
|
55
|
+
response = self.http_client.get(url)
|
|
56
|
+
if not response:
|
|
57
|
+
raise DataSourceError(f"Failed to fetch from Tencent: {symbol}")
|
|
58
|
+
|
|
59
|
+
# 解析响应
|
|
60
|
+
try:
|
|
61
|
+
text = response.text
|
|
62
|
+
# 处理不同的响应格式
|
|
63
|
+
if text.startswith('{'):
|
|
64
|
+
# 直接是JSON
|
|
65
|
+
data = json.loads(text)
|
|
66
|
+
elif '=' in text and '{' in text:
|
|
67
|
+
# JavaScript变量赋值格式: var_name={...}
|
|
68
|
+
json_start = text.find('{')
|
|
69
|
+
if json_start == -1:
|
|
70
|
+
raise ParseError("Invalid response format")
|
|
71
|
+
data = json.loads(text[json_start:])
|
|
72
|
+
else:
|
|
73
|
+
# 尝试直接解析
|
|
74
|
+
json_start = text.find('{')
|
|
75
|
+
if json_start == -1:
|
|
76
|
+
raise ParseError("Invalid response format")
|
|
77
|
+
data = json.loads(text[json_start:])
|
|
78
|
+
|
|
79
|
+
# 检查API返回码
|
|
80
|
+
if isinstance(data, dict) and data.get('code') != 0:
|
|
81
|
+
raise DataSourceError(f"API error: {data.get('msg', 'Unknown error')}")
|
|
82
|
+
|
|
83
|
+
return data
|
|
84
|
+
except json.JSONDecodeError as e:
|
|
85
|
+
raise ParseError(f"Parse error: {e}")
|
|
86
|
+
|
|
87
|
+
def fetch_tick(self, symbols: List[str]) -> Dict[str, Any]:
|
|
88
|
+
"""获取实时行情(暂未实现)"""
|
|
89
|
+
raise NotImplementedError("Tencent tick data not implemented yet")
|
|
90
|
+
|
rquote/exceptions.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
异常定义模块
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RQuoteException(Exception):
|
|
8
|
+
"""基础异常类"""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class DataSourceError(RQuoteException):
|
|
13
|
+
"""数据源错误"""
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ParseError(RQuoteException):
|
|
18
|
+
"""解析错误"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SymbolError(RQuoteException):
|
|
23
|
+
"""股票代码错误"""
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class NetworkError(RQuoteException):
|
|
28
|
+
"""网络错误"""
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CacheError(RQuoteException):
|
|
33
|
+
"""缓存错误"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class HTTPError(RQuoteException):
|
|
38
|
+
"""HTTP错误(向后兼容)"""
|
|
39
|
+
pass
|
|
40
|
+
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
技术因子
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BasicFactors:
|
|
10
|
+
"""基础技术因子"""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def break_rise(d: pd.DataFrame) -> float:
|
|
14
|
+
"""
|
|
15
|
+
突破上涨
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
d: 价格数据DataFrame
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
突破上涨幅度
|
|
22
|
+
"""
|
|
23
|
+
if d.open[-1] / d.close[-2] > 1.002 and d.close[-1] > d.open[-1]:
|
|
24
|
+
return round((d.open[-1] - d.close[-2]) / d.close[-2], 2)
|
|
25
|
+
else:
|
|
26
|
+
return 0
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def min_resist(d: pd.DataFrame) -> float:
|
|
30
|
+
"""
|
|
31
|
+
最小阻力
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
d: 价格数据DataFrame
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
最小阻力值
|
|
38
|
+
"""
|
|
39
|
+
sup, pre, pcur = 0, 0, d.close[-1]
|
|
40
|
+
for i in d.iterrows():
|
|
41
|
+
p = (i[1].open + i[1].close) / 2
|
|
42
|
+
if p > pcur:
|
|
43
|
+
pre += i[1].vol
|
|
44
|
+
if p < pcur:
|
|
45
|
+
sup += i[1].vol
|
|
46
|
+
minres = (sup - pre) / (sup + pre)
|
|
47
|
+
if abs(minres - 1) < .01 and d.close[-2] < max(d.close[:-2]):
|
|
48
|
+
minres += .2
|
|
49
|
+
minres = round(minres, 2)
|
|
50
|
+
return minres
|
|
51
|
+
|
|
52
|
+
@staticmethod
|
|
53
|
+
def vol_extreme(d: pd.DataFrame):
|
|
54
|
+
"""
|
|
55
|
+
成交量极值
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
d: 价格数据DataFrame
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
成交量极值比率
|
|
62
|
+
"""
|
|
63
|
+
vol_series = d.vol
|
|
64
|
+
v60max = vol_series.rolling(60).max()
|
|
65
|
+
v60min = vol_series.rolling(60).min()
|
|
66
|
+
# any in last 3days
|
|
67
|
+
for i in range(1, 3):
|
|
68
|
+
if vol_series[-i] > v60max[-i - 1]:
|
|
69
|
+
return round(vol_series[-i] / v60max[-i - 1], 2)
|
|
70
|
+
if vol_series[-i] < v60min[-i - 1]:
|
|
71
|
+
return round(-vol_series[-i] / v60min[-i - 1], 2)
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def bias_rate_over_ma60(d: pd.DataFrame) -> float:
|
|
76
|
+
"""
|
|
77
|
+
偏离MA60的比率
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
d: 价格数据DataFrame
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
偏离比率
|
|
84
|
+
"""
|
|
85
|
+
r60 = d.close - d.close.rolling(60).mean()
|
|
86
|
+
if r60[-1] > 0:
|
|
87
|
+
return round(r60[-1] / r60.rolling(60).max()[-1], 2)
|
|
88
|
+
else:
|
|
89
|
+
return round(-r60[-1] / r60.rolling(60).min()[-1], 2)
|
|
90
|
+
|
|
91
|
+
@staticmethod
|
|
92
|
+
def op_ma(d: pd.DataFrame) -> Union[float, None]:
|
|
93
|
+
"""
|
|
94
|
+
MA评分
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
d: 价格数据DataFrame
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
MA评分
|
|
101
|
+
"""
|
|
102
|
+
if len(d) < 22:
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
d = d.copy()
|
|
106
|
+
d['mv5'] = d.close.rolling(5).mean()
|
|
107
|
+
d['mv10'] = d.close.rolling(10).mean()
|
|
108
|
+
d['mv20'] = d.close.rolling(20).mean()
|
|
109
|
+
d['mv60'] = d.close.rolling(60).mean()
|
|
110
|
+
|
|
111
|
+
def ma20(d):
|
|
112
|
+
ret = 0
|
|
113
|
+
# .2 for over ma60
|
|
114
|
+
if d.close[-1] > d.mv60[-1]:
|
|
115
|
+
ret += 0.2
|
|
116
|
+
# .2 for all upwards ma's
|
|
117
|
+
if (d.mv5[-1] > d.mv5[-2] and d.mv10[-1] >
|
|
118
|
+
d.mv10[-2] and d.mv20[-1] > d.mv20[-2]):
|
|
119
|
+
ret += 0.2
|
|
120
|
+
for j in range(1, 3):
|
|
121
|
+
if not (d.close[-j] > d.mv5[-j] and d.close[-j]
|
|
122
|
+
> d.mv10[-j] and d.close[-j] > d.mv20[-j]):
|
|
123
|
+
return ret
|
|
124
|
+
for j in range(3, 5):
|
|
125
|
+
if (d.close[-j] > d.mv5[-j] and d.close[-j] >
|
|
126
|
+
d.mv10[-j] and d.close[-j] > d.mv20[-j]):
|
|
127
|
+
return ret
|
|
128
|
+
# .2 for just rush over ma's (fresh score)
|
|
129
|
+
ret += 0.2
|
|
130
|
+
return ret
|
|
131
|
+
return ma20(d)
|
|
132
|
+
|
|
133
|
+
@staticmethod
|
|
134
|
+
def op_cnt(d: pd.DataFrame, cont_min: int = 3) -> int:
|
|
135
|
+
"""
|
|
136
|
+
连续上涨天数计数
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
d: 价格数据DataFrame
|
|
140
|
+
cont_min: 最小连续天数
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
连续上涨天数
|
|
144
|
+
"""
|
|
145
|
+
d.index = pd.DatetimeIndex(d.index)
|
|
146
|
+
td = (d.p_change_on_sh.rolling(cont_min).min() > 0).astype(int) * \
|
|
147
|
+
(d.p_change.rolling(cont_min).min() > 0).astype(int)
|
|
148
|
+
ret = 0 if td[-1] <= 0 else td[-1]
|
|
149
|
+
return ret
|
|
150
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
市场模块
|
|
4
|
+
"""
|
|
5
|
+
from .base import Market
|
|
6
|
+
from .cn_stock import CNStockMarket
|
|
7
|
+
from .hk_stock import HKStockMarket
|
|
8
|
+
from .us_stock import USStockMarket
|
|
9
|
+
from .future import FutureMarket
|
|
10
|
+
from .factory import MarketFactory
|
|
11
|
+
|
|
12
|
+
__all__ = ['Market', 'CNStockMarket', 'HKStockMarket', 'USStockMarket',
|
|
13
|
+
'FutureMarket', 'MarketFactory']
|
|
14
|
+
|
rquote/markets/base.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
市场基类
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Tuple, Optional
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from ..cache import Cache
|
|
9
|
+
from ..data_sources.base import DataSource
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Market(ABC):
|
|
13
|
+
"""市场基类"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, data_source: DataSource, cache: Optional[Cache] = None):
|
|
16
|
+
"""
|
|
17
|
+
初始化市场
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
data_source: 数据源
|
|
21
|
+
cache: 缓存对象
|
|
22
|
+
"""
|
|
23
|
+
self.data_source = data_source
|
|
24
|
+
self.cache = cache
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
def get_price(self, symbol: str, sdate: str = '', edate: str = '',
|
|
28
|
+
freq: str = 'day', days: int = 320, fq: str = 'qfq') -> Tuple[str, str, pd.DataFrame]:
|
|
29
|
+
"""获取价格数据"""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def normalize_symbol(self, symbol: str) -> str:
|
|
34
|
+
"""标准化股票代码"""
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def _get_cached(self, key: str) -> Optional[Tuple[str, str, pd.DataFrame]]:
|
|
38
|
+
"""从缓存获取数据"""
|
|
39
|
+
if self.cache:
|
|
40
|
+
cached = self.cache.get(key)
|
|
41
|
+
if cached:
|
|
42
|
+
return cached
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
def _put_cache(self, key: str, value: Tuple[str, str, pd.DataFrame]) -> None:
|
|
46
|
+
"""存入缓存"""
|
|
47
|
+
if self.cache:
|
|
48
|
+
self.cache.put(key, value)
|
|
49
|
+
|
|
@@ -0,0 +1,186 @@
|
|
|
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
|
+
# 检查缓存
|
|
31
|
+
cache_key = f"{symbol}:{sdate}:{edate}:{freq}:{days}:{fq}"
|
|
32
|
+
cached = self._get_cached(cache_key)
|
|
33
|
+
if cached:
|
|
34
|
+
return cached
|
|
35
|
+
|
|
36
|
+
# 特殊处理BK(板块)代码
|
|
37
|
+
if symbol[:2] == 'BK':
|
|
38
|
+
return self._get_bk_price(symbol)
|
|
39
|
+
|
|
40
|
+
# 特殊处理PT代码
|
|
41
|
+
if symbol[:2] == 'pt':
|
|
42
|
+
return self._get_pt_price(symbol, sdate, edate, freq, days, fq)
|
|
43
|
+
|
|
44
|
+
# 使用数据源获取数据
|
|
45
|
+
try:
|
|
46
|
+
raw_data = self.data_source.fetch_kline(
|
|
47
|
+
symbol, freq=freq, sdate=sdate, edate=edate, days=days, fq=fq
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# 使用解析器解析
|
|
51
|
+
parser = KlineParser()
|
|
52
|
+
name, df = parser.parse_tencent_kline(raw_data, symbol)
|
|
53
|
+
|
|
54
|
+
result = (symbol, name, df)
|
|
55
|
+
self._put_cache(cache_key, result)
|
|
56
|
+
return result
|
|
57
|
+
except (DataSourceError, ParseError) as e:
|
|
58
|
+
logger.warning(f'Failed to fetch {symbol} using new architecture: {e}')
|
|
59
|
+
# 降级到旧方法
|
|
60
|
+
return self._get_price_fallback(symbol, sdate, edate, freq, days, fq)
|
|
61
|
+
|
|
62
|
+
def _get_bk_price(self, symbol: str) -> Tuple[str, str, pd.DataFrame]:
|
|
63
|
+
"""获取板块价格(BK开头)"""
|
|
64
|
+
try:
|
|
65
|
+
url = base64.b64decode('aHR0cDovL3B1c2gyaGlzLmVhc3' +
|
|
66
|
+
'Rtb25leS5jb20vYXBpL3F0L3N0b2NrL2tsaW5lL2dldD9jYj1qUX' +
|
|
67
|
+
'VlcnkxMTI0MDIyNTY2NDQ1ODczNzY2OTcyXzE2MTc4NjQ1NjgxMz' +
|
|
68
|
+
'Emc2VjaWQ9OTAu').decode() + symbol + \
|
|
69
|
+
'&fields1=f1%2Cf2%2Cf3%2Cf4%2Cf5' + \
|
|
70
|
+
'&fields2=f51%2Cf52%2Cf53%2Cf54%2Cf55%2Cf56%2Cf57%2Cf58' + \
|
|
71
|
+
'&klt=101&fqt=0&beg=19900101&end=20990101&_=1'
|
|
72
|
+
response = hget(url)
|
|
73
|
+
if not response:
|
|
74
|
+
logger.warning(f'{symbol} hget failed')
|
|
75
|
+
return symbol, 'None', pd.DataFrame([])
|
|
76
|
+
|
|
77
|
+
data = json.loads(response.text.split('jQuery1124022566445873766972_1617864568131(')[1][:-2])
|
|
78
|
+
if not data.get('data'):
|
|
79
|
+
logger.warning(f'{symbol} data empty')
|
|
80
|
+
return symbol, 'None', pd.DataFrame([])
|
|
81
|
+
|
|
82
|
+
name = data['data']['name']
|
|
83
|
+
df = pd.DataFrame([i.split(',') for i in data['data']['klines']],
|
|
84
|
+
columns=['date', 'open', 'close', 'high', 'low', 'vol', 'money', 'p'])
|
|
85
|
+
df = df.set_index(['date']).astype(float)
|
|
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¶m={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'http://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
|
+
|