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.
@@ -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,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 因子计算模块
4
+ """
5
+ from .technical import BasicFactors
6
+
7
+ __all__ = ['BasicFactors']
8
+
@@ -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&param={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
+