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
rquote/__init__.py
CHANGED
|
@@ -7,9 +7,57 @@ Copyright (c) 2021 Roi ZHAO
|
|
|
7
7
|
|
|
8
8
|
'''
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
from .
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
# API函数
|
|
11
|
+
from .api import (
|
|
12
|
+
get_price,
|
|
13
|
+
get_price_longer,
|
|
14
|
+
get_all_industries,
|
|
15
|
+
get_stock_concepts,
|
|
16
|
+
get_stock_industry,
|
|
17
|
+
get_cn_stock_list,
|
|
18
|
+
get_hk_stocks_500,
|
|
19
|
+
get_cn_future_list,
|
|
20
|
+
get_us_stocks,
|
|
21
|
+
get_cn_fund_list,
|
|
22
|
+
get_tick,
|
|
23
|
+
get_industry_stocks
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# 工具类
|
|
27
|
+
from .utils import WebUtils, hget, logger
|
|
28
|
+
from .factors import BasicFactors
|
|
15
29
|
from .plots import PlotUtils
|
|
30
|
+
|
|
31
|
+
# 新增模块(可选使用)
|
|
32
|
+
from . import config
|
|
33
|
+
from . import exceptions
|
|
34
|
+
from .cache import MemoryCache, Cache
|
|
35
|
+
from .utils.http import HTTPClient
|
|
36
|
+
|
|
37
|
+
__version__ = '0.3.5'
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
# API函数
|
|
41
|
+
'get_price',
|
|
42
|
+
'get_price_longer',
|
|
43
|
+
'get_all_industries',
|
|
44
|
+
'get_stock_concepts',
|
|
45
|
+
'get_stock_industry',
|
|
46
|
+
'get_cn_stock_list',
|
|
47
|
+
'get_hk_stocks_500',
|
|
48
|
+
'get_cn_future_list',
|
|
49
|
+
'get_us_stocks',
|
|
50
|
+
'get_cn_fund_list',
|
|
51
|
+
'get_tick',
|
|
52
|
+
'get_industry_stocks',
|
|
53
|
+
# 工具类
|
|
54
|
+
'WebUtils',
|
|
55
|
+
'BasicFactors',
|
|
56
|
+
'PlotUtils',
|
|
57
|
+
# 新增模块
|
|
58
|
+
'config',
|
|
59
|
+
'exceptions',
|
|
60
|
+
'MemoryCache',
|
|
61
|
+
'Cache',
|
|
62
|
+
'HTTPClient',
|
|
63
|
+
]
|
rquote/api/__init__.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
API层
|
|
4
|
+
"""
|
|
5
|
+
from .price import get_price, get_price_longer
|
|
6
|
+
from .lists import (
|
|
7
|
+
get_cn_stock_list,
|
|
8
|
+
get_hk_stocks_500,
|
|
9
|
+
get_us_stocks,
|
|
10
|
+
get_cn_fund_list,
|
|
11
|
+
get_cn_future_list,
|
|
12
|
+
get_all_industries,
|
|
13
|
+
get_industry_stocks
|
|
14
|
+
)
|
|
15
|
+
from .tick import get_tick
|
|
16
|
+
from .stock_info import get_stock_concepts, get_stock_industry
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'get_price',
|
|
20
|
+
'get_price_longer',
|
|
21
|
+
'get_cn_stock_list',
|
|
22
|
+
'get_hk_stocks_500',
|
|
23
|
+
'get_us_stocks',
|
|
24
|
+
'get_cn_fund_list',
|
|
25
|
+
'get_cn_future_list',
|
|
26
|
+
'get_tick',
|
|
27
|
+
'get_stock_concepts',
|
|
28
|
+
'get_stock_industry',
|
|
29
|
+
'get_all_industries',
|
|
30
|
+
'get_industry_stocks'
|
|
31
|
+
]
|
|
32
|
+
|
rquote/api/lists.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
列表相关API
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import base64
|
|
8
|
+
import time
|
|
9
|
+
from ..utils import hget, logger
|
|
10
|
+
from ..exceptions import HTTPError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_cn_stock_list(money_min=2e8):
|
|
14
|
+
"""
|
|
15
|
+
获取A股股票列表
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
money_min: 最小成交额
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
股票列表
|
|
22
|
+
"""
|
|
23
|
+
offset = 0
|
|
24
|
+
count = 200 # max, or error
|
|
25
|
+
df = []
|
|
26
|
+
while not df or float(df[-1]['turnover'])*1e4 > money_min:
|
|
27
|
+
a = hget(
|
|
28
|
+
'https://proxy.finance.qq.com/cgi/cgi-bin/rank/hs/getBoardRankList?_appver=11.17.0'+
|
|
29
|
+
f'&board_code=aStock&sort_type=turnover&direct=down&offset={offset}&count={count}'
|
|
30
|
+
)
|
|
31
|
+
if a:
|
|
32
|
+
a = json.loads(a.text)
|
|
33
|
+
if a['data']['rank_list']:
|
|
34
|
+
df.extend(a['data']['rank_list'])
|
|
35
|
+
offset += count
|
|
36
|
+
else:
|
|
37
|
+
break
|
|
38
|
+
return df
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_hk_stocks_500():
|
|
42
|
+
"""
|
|
43
|
+
获取港股前500只股票列表
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
股票列表
|
|
47
|
+
"""
|
|
48
|
+
a = hget(
|
|
49
|
+
'https://stock.gtimg.cn/data/hk_rank.php?board=main_all&metric=amount&' +
|
|
50
|
+
'pageSize=500&reqPage=1&order=desc&var_name=list_data')
|
|
51
|
+
if a:
|
|
52
|
+
a = [i.split('~') for i in json.loads(a.text.split('list_data=')[1])['data']['page_data']]
|
|
53
|
+
return a
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def get_us_stocks(k=100):
|
|
57
|
+
"""
|
|
58
|
+
获取美股列表
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
k: 获取数量
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
股票列表
|
|
65
|
+
"""
|
|
66
|
+
uscands = []
|
|
67
|
+
page_n = k//20 + 1
|
|
68
|
+
for page in range(1, page_n+1):
|
|
69
|
+
a = hget(
|
|
70
|
+
"https://stock.finance.sina.com.cn/usstock/api/jsonp.php/IO.XSRV2."+
|
|
71
|
+
f"CallbackList['f0j3ltzVzdo2Fo4p']/US_CategoryService.getList?page={page}"+
|
|
72
|
+
"&num=20&sort=&asc=0&market=&id=")
|
|
73
|
+
if a:
|
|
74
|
+
uslist = json.loads(a.text.split('(',1)[1][:-2])['data']
|
|
75
|
+
uscands.extend(uslist)
|
|
76
|
+
return uscands
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_cn_fund_list():
|
|
80
|
+
"""
|
|
81
|
+
获取A股ETF基金列表
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
基金列表,格式: [code, name, change, amount, price]
|
|
85
|
+
"""
|
|
86
|
+
a = hget(base64.b64decode('aHR0cDovL3ZpcC5zdG9jay5maW5hbmNlLnNpbmEuY29tL'+
|
|
87
|
+
'mNuL3F1b3Rlc19zZXJ2aWNlL2FwaS9qc29ucC5waHAvSU8uWFNSVjIuQ2FsbGJhY2tMaX'+
|
|
88
|
+
'N0WydrMldhekswNk5Rd2xoeVh2J10vTWFya2V0X0NlbnRlci5nZXRIUU5vZGVEYXRhU2l'+
|
|
89
|
+
'tcGxlP3BhZ2U9MSZudW09MTAwMCZzb3J0PWFtb3VudCZhc2M9MCZub2RlPWV0Zl9ocV9m'+
|
|
90
|
+
'dW5kJiU1Qm9iamVjdCUyMEhUTUxEaXZFbGVtZW50JTVEPXhtNGkw').decode())
|
|
91
|
+
if a:
|
|
92
|
+
fundcands = [[i['symbol'], i['name'], i['changepercent'], i['amount'], i['trade']]
|
|
93
|
+
for i in json.loads(a.text.split('k2WazK06NQwlhyXv')[1][3:-2])]
|
|
94
|
+
return fundcands
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_cn_future_list():
|
|
98
|
+
"""
|
|
99
|
+
获取国内期货合约列表
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
期货代码列表,带fu前缀
|
|
103
|
+
"""
|
|
104
|
+
a = hget('https://finance.sina.com.cn/futuremarket/')
|
|
105
|
+
if a:
|
|
106
|
+
futurelist_active = [
|
|
107
|
+
'fu' + i for i in re.findall(r'quotes/(.*?\d+).shtml', a.text)]
|
|
108
|
+
return futurelist_active
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def get_all_industries():
|
|
112
|
+
"""
|
|
113
|
+
获取所有行业板块列表
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
行业列表,格式: [code, name, change, amount, price, sina_sw2_id]
|
|
117
|
+
"""
|
|
118
|
+
try:
|
|
119
|
+
url = 'https://proxy.finance.qq.com/cgi/cgi-bin/rank/pt/getRank?board_type=hy2&sort_type=price&direct=down&offset=0&count=200'
|
|
120
|
+
a = hget(url)
|
|
121
|
+
if not a:
|
|
122
|
+
raise HTTPError('Failed to fetch industries from QQ Finance')
|
|
123
|
+
data = json.loads(a.text)
|
|
124
|
+
if data.get('code') != 0:
|
|
125
|
+
logger.warning('API returned error: {}'.format(data.get('msg', 'Unknown error')))
|
|
126
|
+
return []
|
|
127
|
+
|
|
128
|
+
rank_list = data.get('data', {}).get('rank_list', [])
|
|
129
|
+
industries = [
|
|
130
|
+
[item['code'], item['name'], item.get('zdf', '0'),
|
|
131
|
+
item.get('zllr', '0'), item.get('zxj', '0')]
|
|
132
|
+
for item in rank_list
|
|
133
|
+
]
|
|
134
|
+
logger.debug('get industries {}'.format(len(industries)))
|
|
135
|
+
except Exception as e:
|
|
136
|
+
logger.warning('Error parsing industries data: {}'.format(e))
|
|
137
|
+
industries = []
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
url = 'https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/Market_Center.getHQNodes'
|
|
141
|
+
sina_industries = hget(url)
|
|
142
|
+
if not sina_industries:
|
|
143
|
+
raise HTTPError('Failed to fetch industries from Sina Finance')
|
|
144
|
+
data = json.loads(sina_industries.text)
|
|
145
|
+
sina_industries = data[1][0][1]
|
|
146
|
+
sina_sw2 = sina_industries[3][1]
|
|
147
|
+
sina_sw2_dict = {i[0]:i[2] for i in sina_sw2}
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.warning(f'Error parsing sina industries data: {e}')
|
|
150
|
+
sina_sw2_dict = {}
|
|
151
|
+
|
|
152
|
+
# 利用sina_sw2_dict给industries添加sina_sw2_id
|
|
153
|
+
for industry in industries:
|
|
154
|
+
industry.append(sina_sw2_dict.get(industry[1], ''))
|
|
155
|
+
return industries
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def get_industry_stocks(node):
|
|
159
|
+
"""
|
|
160
|
+
获取指定行业板块的股票列表
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
node: 行业节点代码
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
股票列表
|
|
167
|
+
"""
|
|
168
|
+
url = f'https://vip.stock.finance.sina.com.cn/quotes_service/api/json_v2.php/'+\
|
|
169
|
+
f'Market_Center.getHQNodeData?page=1&num=40&sort=symbol&asc=1&node={node}&symbol=&_s_r_a=init'
|
|
170
|
+
a = hget(url)
|
|
171
|
+
if not a:
|
|
172
|
+
raise HTTPError('Failed to fetch industry stocks from Sina Finance')
|
|
173
|
+
data = json.loads(a.text)
|
|
174
|
+
return data
|
|
175
|
+
|
rquote/api/price.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
价格相关API
|
|
4
|
+
"""
|
|
5
|
+
from typing import Tuple
|
|
6
|
+
import pandas as pd
|
|
7
|
+
from ..markets import MarketFactory
|
|
8
|
+
from ..cache import Cache
|
|
9
|
+
from ..cache.memory import DictCache as DictCacheAdapter
|
|
10
|
+
from ..utils.date import check_date_format
|
|
11
|
+
from ..exceptions import SymbolError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_price(i: str, sdate: str = '', edate: str = '', freq: str = 'day',
|
|
15
|
+
days: int = 320, fq: str = 'qfq', dd=None) -> Tuple[str, str, pd.DataFrame]:
|
|
16
|
+
'''
|
|
17
|
+
获取价格数据
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
i: 股票代码
|
|
21
|
+
sdate: 开始日期
|
|
22
|
+
edate: 结束日期
|
|
23
|
+
dd: data dictionary或Cache对象,任何有get/put方法的本地缓存
|
|
24
|
+
days: 获取天数,覆盖sdate
|
|
25
|
+
fq: 复权方式,qfq为前复权
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
(symbol, name, DataFrame)
|
|
29
|
+
'''
|
|
30
|
+
# 处理缓存
|
|
31
|
+
cache = None
|
|
32
|
+
if dd is not None:
|
|
33
|
+
if isinstance(dd, dict):
|
|
34
|
+
cache = DictCacheAdapter(dd)
|
|
35
|
+
elif isinstance(dd, Cache):
|
|
36
|
+
cache = dd
|
|
37
|
+
elif hasattr(dd, 'get'):
|
|
38
|
+
cache = DictCacheAdapter(dd)
|
|
39
|
+
|
|
40
|
+
# 检查日期格式
|
|
41
|
+
try:
|
|
42
|
+
sdate = check_date_format(sdate) if sdate else ''
|
|
43
|
+
edate = check_date_format(edate) if edate else ''
|
|
44
|
+
except ValueError as e:
|
|
45
|
+
raise SymbolError(f"Invalid date format: {e}")
|
|
46
|
+
|
|
47
|
+
# 使用市场工厂创建对应的市场实例
|
|
48
|
+
market = MarketFactory.create_from_symbol(i, cache=cache)
|
|
49
|
+
|
|
50
|
+
# 调用市场的get_price方法
|
|
51
|
+
return market.get_price(i, sdate=sdate, edate=edate, freq=freq, days=days, fq=fq)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_price_longer(i: str, l: int = 2, dd=None) -> Tuple[str, str, pd.DataFrame]:
|
|
55
|
+
"""
|
|
56
|
+
获取更长时间的历史数据
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
i: 股票代码
|
|
60
|
+
l: 年数
|
|
61
|
+
dd: 缓存对象
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
(symbol, name, DataFrame)
|
|
65
|
+
"""
|
|
66
|
+
_, name, a = get_price(i, dd=dd)
|
|
67
|
+
d1 = a.index.format()[0]
|
|
68
|
+
for y in range(1, l):
|
|
69
|
+
d0 = str(int(d1[:4]) - 1) + d1[4:]
|
|
70
|
+
a = pd.concat((get_price(i, d0, d1, dd=dd)[2], a), 0).drop_duplicates()
|
|
71
|
+
d1 = d0
|
|
72
|
+
return i, name, a
|
|
73
|
+
|
rquote/api/stock_info.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
股票信息相关API
|
|
4
|
+
"""
|
|
5
|
+
import json
|
|
6
|
+
from typing import List
|
|
7
|
+
from ..utils import hget
|
|
8
|
+
from ..exceptions import HTTPError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def get_stock_concepts(i: str) -> List[str]:
|
|
12
|
+
"""
|
|
13
|
+
获取指定股票所属的概念板块
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
i: 股票代码
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
概念代码列表
|
|
20
|
+
"""
|
|
21
|
+
url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/stockinfo/plateNew?code={i}&app=wzq&zdf=1'
|
|
22
|
+
a = hget(url)
|
|
23
|
+
if not a:
|
|
24
|
+
raise HTTPError('Failed to fetch concepts from QQ Finance')
|
|
25
|
+
data = json.loads(a.text)
|
|
26
|
+
if data.get('code') != 0:
|
|
27
|
+
raise HTTPError('API returned error: {}'.format(data.get('msg', 'Unknown error')))
|
|
28
|
+
return data.get('data', {}).get('concept', [])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def get_stock_industry(i: str) -> List[str]:
|
|
32
|
+
"""
|
|
33
|
+
获取指定股票所属的行业板块
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
i: 股票代码
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
行业代码列表
|
|
40
|
+
"""
|
|
41
|
+
url = f'https://proxy.finance.qq.com/ifzqgtimg/appstock/app/stockinfo/plateNew?code={i}&app=wzq&zdf=1'
|
|
42
|
+
a = hget(url)
|
|
43
|
+
if not a:
|
|
44
|
+
raise HTTPError('Failed to fetch industry from QQ Finance')
|
|
45
|
+
data = json.loads(a.text)
|
|
46
|
+
if data.get('code') != 0:
|
|
47
|
+
raise HTTPError('API returned error: {}'.format(data.get('msg', 'Unknown error')))
|
|
48
|
+
return data.get('data', {}).get('plate', [])
|
|
49
|
+
|
rquote/api/tick.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
实时行情相关API
|
|
4
|
+
"""
|
|
5
|
+
from typing import List, Union
|
|
6
|
+
from ..utils import hget
|
|
7
|
+
from ..exceptions import HTTPError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_tick(tgts: Union[List[str], str] = []):
|
|
11
|
+
"""
|
|
12
|
+
获取实时行情数据
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
tgts: 股票代码列表或单个代码(美股需要gb_前缀)
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
行情数据列表
|
|
19
|
+
"""
|
|
20
|
+
if not tgts:
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
sina_tick = 'https://hq.sinajs.cn/?list='
|
|
24
|
+
head_row = ['name', 'price', 'price_change_rate', 'timesec',
|
|
25
|
+
'price_change', '_', '_', '_', '_', '_', 'volume', '_', '_',
|
|
26
|
+
'_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_', '_',
|
|
27
|
+
'_', 'last_close', '_', '_', '_', 'turnover', '_', '_', '_', '_']
|
|
28
|
+
|
|
29
|
+
if isinstance(tgts, list):
|
|
30
|
+
tgts = ['gb_' + i.lower() for i in tgts]
|
|
31
|
+
elif isinstance(tgts, str):
|
|
32
|
+
tgts = ['gb_' + tgts]
|
|
33
|
+
else:
|
|
34
|
+
raise ValueError('tgt should be list or str, e.g. APPL,')
|
|
35
|
+
|
|
36
|
+
a = hget(sina_tick + ','.join(tgts))
|
|
37
|
+
if not a:
|
|
38
|
+
raise HTTPError('hget failed {}'.format(tgts))
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
dat = [i.split('"')[1].split(',') for i in a.text.split(';\n') if ',' in i]
|
|
42
|
+
dat_trim = [{k:i[j] for j,k in enumerate(head_row) if k!='_'} for i in dat]
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise HTTPError('data not complete, check tgt be code str or list without'+
|
|
45
|
+
' prefix, your given: {}'.format(tgts))
|
|
46
|
+
return dat_trim
|
|
47
|
+
|
rquote/cache/__init__.py
ADDED
rquote/cache/base.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
缓存基类
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Optional, Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Cache(ABC):
|
|
10
|
+
"""缓存基类"""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def get(self, key: str) -> Optional[Any]:
|
|
14
|
+
"""获取缓存"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def put(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
19
|
+
"""设置缓存"""
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
@abstractmethod
|
|
23
|
+
def delete(self, key: str) -> None:
|
|
24
|
+
"""删除缓存"""
|
|
25
|
+
pass
|
|
26
|
+
|
rquote/cache/memory.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
内存缓存实现
|
|
4
|
+
"""
|
|
5
|
+
import time
|
|
6
|
+
from typing import Optional, Any, Dict
|
|
7
|
+
from .base import Cache
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MemoryCache(Cache):
|
|
11
|
+
"""内存缓存实现"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, ttl: int = 3600):
|
|
14
|
+
"""
|
|
15
|
+
初始化内存缓存
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
ttl: 默认过期时间(秒)
|
|
19
|
+
"""
|
|
20
|
+
self.ttl = ttl
|
|
21
|
+
self._cache: Dict[str, tuple] = {} # {key: (value, expire_time)}
|
|
22
|
+
|
|
23
|
+
def get(self, key: str) -> Optional[Any]:
|
|
24
|
+
"""获取缓存"""
|
|
25
|
+
if key not in self._cache:
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
value, expire_time = self._cache[key]
|
|
29
|
+
if time.time() > expire_time:
|
|
30
|
+
del self._cache[key]
|
|
31
|
+
return None
|
|
32
|
+
|
|
33
|
+
return value
|
|
34
|
+
|
|
35
|
+
def put(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
36
|
+
"""设置缓存"""
|
|
37
|
+
ttl = ttl or self.ttl
|
|
38
|
+
expire_time = time.time() + ttl
|
|
39
|
+
self._cache[key] = (value, expire_time)
|
|
40
|
+
|
|
41
|
+
def delete(self, key: str) -> None:
|
|
42
|
+
"""删除缓存"""
|
|
43
|
+
self._cache.pop(key, None)
|
|
44
|
+
|
|
45
|
+
def clear(self) -> None:
|
|
46
|
+
"""清空缓存"""
|
|
47
|
+
self._cache.clear()
|
|
48
|
+
|
|
49
|
+
def size(self) -> int:
|
|
50
|
+
"""获取缓存大小"""
|
|
51
|
+
return len(self._cache)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class DictCache(Cache):
|
|
55
|
+
"""字典缓存适配器(用于向后兼容旧的dd参数)"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, cache_dict: dict):
|
|
58
|
+
"""
|
|
59
|
+
初始化字典缓存
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
cache_dict: 字典对象
|
|
63
|
+
"""
|
|
64
|
+
self._dict = cache_dict
|
|
65
|
+
|
|
66
|
+
def get(self, key: str) -> Optional[Any]:
|
|
67
|
+
"""获取缓存"""
|
|
68
|
+
return self._dict.get(key)
|
|
69
|
+
|
|
70
|
+
def put(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
|
|
71
|
+
"""设置缓存"""
|
|
72
|
+
self._dict[key] = value
|
|
73
|
+
|
|
74
|
+
def delete(self, key: str) -> None:
|
|
75
|
+
"""删除缓存"""
|
|
76
|
+
self._dict.pop(key, None)
|
|
77
|
+
|
rquote/config.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
配置管理模块
|
|
4
|
+
"""
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Config:
|
|
12
|
+
"""配置类"""
|
|
13
|
+
# HTTP配置
|
|
14
|
+
http_timeout: int = 10
|
|
15
|
+
http_retry_times: int = 3
|
|
16
|
+
http_retry_delay: float = 1.0
|
|
17
|
+
http_pool_size: int = 10
|
|
18
|
+
|
|
19
|
+
# 缓存配置
|
|
20
|
+
cache_enabled: bool = True
|
|
21
|
+
cache_ttl: int = 3600 # 秒
|
|
22
|
+
|
|
23
|
+
# 日志配置
|
|
24
|
+
log_level: str = "INFO"
|
|
25
|
+
log_file: Optional[str] = "/tmp/rquote.log"
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_env(cls) -> 'Config':
|
|
29
|
+
"""从环境变量创建配置"""
|
|
30
|
+
return cls(
|
|
31
|
+
http_timeout=int(os.getenv('RQUOTE_HTTP_TIMEOUT', '10')),
|
|
32
|
+
http_retry_times=int(os.getenv('RQUOTE_RETRY_TIMES', '3')),
|
|
33
|
+
cache_enabled=os.getenv('RQUOTE_CACHE_ENABLED', 'true').lower() == 'true',
|
|
34
|
+
cache_ttl=int(os.getenv('RQUOTE_CACHE_TTL', '3600')),
|
|
35
|
+
log_level=os.getenv('RQUOTE_LOG_LEVEL', 'INFO'),
|
|
36
|
+
log_file=os.getenv('RQUOTE_LOG_FILE', '/tmp/rquote.log')
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# 全局默认配置
|
|
41
|
+
default_config = Config.from_env()
|
|
42
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
数据源基类
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DataSource(ABC):
|
|
10
|
+
"""数据源基类"""
|
|
11
|
+
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def fetch_kline(self, symbol: str, **kwargs) -> Dict[str, Any]:
|
|
14
|
+
"""获取K线数据"""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def fetch_tick(self, symbols: List[str]) -> Dict[str, Any]:
|
|
19
|
+
"""获取实时行情"""
|
|
20
|
+
pass
|
|
21
|
+
|