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 CHANGED
@@ -7,9 +7,57 @@ Copyright (c) 2021 Roi ZHAO
7
7
 
8
8
  '''
9
9
 
10
- from .main import get_price, get_stock_concepts, get_concept_stocks, get_bk_stocks
11
- from .main import get_all_concepts, get_all_industries
12
- from .main import get_cn_stock_list, get_hk_stocks_hsi, get_hk_stocks_ggt, get_hk_stocks_500
13
- from .main import get_cn_future_list, get_us_stocks, get_cn_fund_list
14
- from .utils import WebUtils, BasicFactors
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
+
@@ -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
+
@@ -0,0 +1,9 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 缓存模块
4
+ """
5
+ from .base import Cache
6
+ from .memory import MemoryCache
7
+
8
+ __all__ = ['Cache', 'MemoryCache']
9
+
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,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 数据源模块
4
+ """
5
+ from .base import DataSource
6
+ from .tencent import TencentDataSource
7
+ from .sina import SinaDataSource
8
+
9
+ __all__ = ['DataSource', 'TencentDataSource', 'SinaDataSource']
10
+
@@ -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
+