quantcli 0.1.0__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.
- quantcli/cli.py +996 -0
- quantcli/core/__init__.py +50 -0
- quantcli/core/backtest.py +534 -0
- quantcli/core/data.py +512 -0
- quantcli/core/factor.py +507 -0
- quantcli/datasources/__init__.py +83 -0
- quantcli/datasources/akshare.py +313 -0
- quantcli/datasources/baostock.py +478 -0
- quantcli/datasources/base.py +220 -0
- quantcli/datasources/cache.py +377 -0
- quantcli/datasources/mixed.py +174 -0
- quantcli/factors/__init__.py +29 -0
- quantcli/factors/base.py +163 -0
- quantcli/factors/compute.py +281 -0
- quantcli/factors/loader.py +293 -0
- quantcli/factors/pipeline.py +463 -0
- quantcli/factors/ranking.py +538 -0
- quantcli/factors/screening.py +138 -0
- quantcli/parser/__init__.py +70 -0
- quantcli/parser/constants.py +24 -0
- quantcli/parser/formula.py +397 -0
- quantcli/utils/__init__.py +163 -0
- quantcli/utils/logger.py +207 -0
- quantcli/utils/path.py +422 -0
- quantcli/utils/time.py +522 -0
- quantcli/utils/validate.py +491 -0
- quantcli-0.1.0.dist-info/METADATA +79 -0
- quantcli-0.1.0.dist-info/RECORD +31 -0
- quantcli-0.1.0.dist-info/WHEEL +5 -0
- quantcli-0.1.0.dist-info/entry_points.txt +2 -0
- quantcli-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
"""AKShare 数据源适配器
|
|
2
|
+
|
|
3
|
+
使用稳定的接口:
|
|
4
|
+
- 日线数据: stock_zh_a_hist_tx (腾讯)
|
|
5
|
+
- 股票列表: stock_info_a_code_name
|
|
6
|
+
- 交易日历: tool_trade_date_hist_sina (新浪)
|
|
7
|
+
- 指数日线: stock_zh_index_daily_tx (腾讯)
|
|
8
|
+
|
|
9
|
+
文档: https://akshare.xyz/
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import date
|
|
13
|
+
from typing import List, Optional, Dict, Any
|
|
14
|
+
import pandas as pd
|
|
15
|
+
|
|
16
|
+
from ..utils import get_logger, format_date
|
|
17
|
+
from .base import DataSource, DataSourceConfig, StockInfo
|
|
18
|
+
from .cache import PriceCache, StockListCache, TradingCalendarCache
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AkshareDataSource(DataSource):
|
|
24
|
+
"""AKShare 数据源实现(稳定版本)"""
|
|
25
|
+
|
|
26
|
+
name = "akshare"
|
|
27
|
+
|
|
28
|
+
def __init__(self, use_cache: bool = True):
|
|
29
|
+
"""初始化
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
use_cache: 是否使用缓存,默认 True
|
|
33
|
+
"""
|
|
34
|
+
super().__init__(DataSourceConfig(name=self.name, use_cache=use_cache))
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
import akshare as ak
|
|
38
|
+
self._ak = ak
|
|
39
|
+
except ImportError:
|
|
40
|
+
raise ImportError("akshare not installed. Run: pip install akshare")
|
|
41
|
+
|
|
42
|
+
# 缓存实例
|
|
43
|
+
self._price_cache = PriceCache(enabled=use_cache)
|
|
44
|
+
self._stocklist_cache = StockListCache(enabled=use_cache)
|
|
45
|
+
self._calendar_cache = TradingCalendarCache(enabled=use_cache)
|
|
46
|
+
|
|
47
|
+
# ==================== 工具方法 ====================
|
|
48
|
+
|
|
49
|
+
def _to_tx_symbol(self, symbol: str) -> str:
|
|
50
|
+
"""转换为腾讯股票代码格式"""
|
|
51
|
+
symbol = symbol.replace(".SH", "").replace(".SZ", "")
|
|
52
|
+
return f"sh{symbol}" if symbol.startswith(("6", "5", "9")) else f"sz{symbol}"
|
|
53
|
+
|
|
54
|
+
def _to_index_tx_symbol(self, symbol: str) -> str:
|
|
55
|
+
"""转换为腾讯指数代码格式"""
|
|
56
|
+
symbol = symbol.replace(".SH", "").replace(".SZ", "")
|
|
57
|
+
return f"sh{symbol}" if symbol.startswith(("6", "5", "9")) else f"sz{symbol}"
|
|
58
|
+
|
|
59
|
+
def _market_filter(
|
|
60
|
+
self,
|
|
61
|
+
df: pd.DataFrame,
|
|
62
|
+
market: str
|
|
63
|
+
) -> pd.DataFrame:
|
|
64
|
+
"""过滤股票市场"""
|
|
65
|
+
if market == "all":
|
|
66
|
+
return df
|
|
67
|
+
|
|
68
|
+
is_sh = df["symbol"].str.startswith(("6", "5", "9"))
|
|
69
|
+
if market == "上海":
|
|
70
|
+
return df[is_sh]
|
|
71
|
+
return df[~is_sh]
|
|
72
|
+
|
|
73
|
+
# ==================== 核心接口 ====================
|
|
74
|
+
|
|
75
|
+
def get_daily(
|
|
76
|
+
self,
|
|
77
|
+
symbol: str,
|
|
78
|
+
start_date,
|
|
79
|
+
end_date,
|
|
80
|
+
fields: Optional[List[str]] = None
|
|
81
|
+
) -> pd.DataFrame:
|
|
82
|
+
"""获取A股日线数据(腾讯接口)"""
|
|
83
|
+
start_str = format_date(start_date, "%Y-%m-%d")
|
|
84
|
+
end_str = format_date(end_date, "%Y-%m-%d")
|
|
85
|
+
cache_key = f"{symbol}_{start_str}_{end_str}"
|
|
86
|
+
|
|
87
|
+
# 1. 尝试缓存
|
|
88
|
+
if self.config.use_cache:
|
|
89
|
+
cached = self._price_cache.get(cache_key)
|
|
90
|
+
if cached is not None:
|
|
91
|
+
return self._filter_fields(cached, fields) if fields else cached
|
|
92
|
+
|
|
93
|
+
# 2. API 调用
|
|
94
|
+
tx_symbol = self._to_tx_symbol(symbol)
|
|
95
|
+
df = self._ak.stock_zh_a_hist_tx(
|
|
96
|
+
symbol=tx_symbol,
|
|
97
|
+
start_date=format_date(start_date, "%Y%m%d"),
|
|
98
|
+
end_date=format_date(end_date, "%Y%m%d")
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if df.empty:
|
|
102
|
+
return df
|
|
103
|
+
|
|
104
|
+
df["date"] = pd.to_datetime(df["date"]).dt.date
|
|
105
|
+
|
|
106
|
+
# 3. 写入缓存
|
|
107
|
+
if self.config.use_cache:
|
|
108
|
+
self._price_cache.set(cache_key, df)
|
|
109
|
+
|
|
110
|
+
return self._filter_fields(df, fields) if fields else df
|
|
111
|
+
|
|
112
|
+
def get_index_daily(
|
|
113
|
+
self,
|
|
114
|
+
symbol: str,
|
|
115
|
+
start_date,
|
|
116
|
+
end_date
|
|
117
|
+
) -> pd.DataFrame:
|
|
118
|
+
"""获取指数日线数据(腾讯接口)"""
|
|
119
|
+
start_str = format_date(start_date, "%Y-%m-%d")
|
|
120
|
+
end_str = format_date(end_date, "%Y-%m-%d")
|
|
121
|
+
cache_key = f"idx_{symbol}_{start_str}_{end_str}"
|
|
122
|
+
|
|
123
|
+
if self.config.use_cache:
|
|
124
|
+
cached = self._price_cache.get(cache_key)
|
|
125
|
+
if cached is not None:
|
|
126
|
+
return cached
|
|
127
|
+
|
|
128
|
+
tx_symbol = self._to_index_tx_symbol(symbol)
|
|
129
|
+
df = self._ak.stock_zh_index_daily_tx(symbol=tx_symbol)
|
|
130
|
+
|
|
131
|
+
if df.empty:
|
|
132
|
+
return df
|
|
133
|
+
|
|
134
|
+
df["date"] = pd.to_datetime(df["date"]).dt.date
|
|
135
|
+
|
|
136
|
+
if self.config.use_cache:
|
|
137
|
+
self._price_cache.set(cache_key, df)
|
|
138
|
+
|
|
139
|
+
return df
|
|
140
|
+
|
|
141
|
+
def get_intraday(
|
|
142
|
+
self,
|
|
143
|
+
symbol: str,
|
|
144
|
+
start_date: Optional[date] = None,
|
|
145
|
+
end_date: Optional[date] = None,
|
|
146
|
+
period: str = "5"
|
|
147
|
+
) -> pd.DataFrame:
|
|
148
|
+
"""获取分钟级数据
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
symbol: 股票代码,如 "600519"
|
|
152
|
+
start_date: 开始日期
|
|
153
|
+
end_date: 结束日期
|
|
154
|
+
period: 分钟周期,支持 "1", "5", "15", "30", "60"
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
DataFrame with columns: [date, open, high, low, close, volume]
|
|
158
|
+
"""
|
|
159
|
+
import akshare as ak
|
|
160
|
+
from datetime import datetime, timedelta
|
|
161
|
+
|
|
162
|
+
# 默认获取最近5个交易日
|
|
163
|
+
if end_date is None:
|
|
164
|
+
end_date = date.today()
|
|
165
|
+
if start_date is None:
|
|
166
|
+
start_date = end_date - timedelta(days=10)
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
# 使用 akshare 获取分钟数据
|
|
170
|
+
df = ak.stock_zh_a_hist_min_em(
|
|
171
|
+
symbol=symbol,
|
|
172
|
+
period=period,
|
|
173
|
+
start_date=start_date.strftime("%Y%m%d"),
|
|
174
|
+
end_date=end_date.strftime("%Y%m%d"),
|
|
175
|
+
adjust="qfq"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if df.empty:
|
|
179
|
+
return pd.DataFrame(columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
|
180
|
+
|
|
181
|
+
# 标准化列名
|
|
182
|
+
df = df.rename(columns={
|
|
183
|
+
"时间": "date",
|
|
184
|
+
"开盘": "open",
|
|
185
|
+
"最高": "high",
|
|
186
|
+
"最低": "low",
|
|
187
|
+
"收盘": "close",
|
|
188
|
+
"成交量": "volume",
|
|
189
|
+
"成交额": "amount"
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
# 转换日期格式
|
|
193
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
194
|
+
|
|
195
|
+
# 确保数值列是数值类型
|
|
196
|
+
numeric_cols = ['open', 'high', 'low', 'close', 'volume', 'amount']
|
|
197
|
+
for col in numeric_cols:
|
|
198
|
+
if col in df.columns:
|
|
199
|
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
|
200
|
+
|
|
201
|
+
return df
|
|
202
|
+
|
|
203
|
+
except Exception as e:
|
|
204
|
+
logger.warning(f"Failed to get intraday data for {symbol}: {e}")
|
|
205
|
+
return pd.DataFrame(columns=['date', 'open', 'high', 'low', 'close', 'volume'])
|
|
206
|
+
|
|
207
|
+
def get_stock_list(self, market: str = "all") -> pd.DataFrame:
|
|
208
|
+
"""获取A股股票列表
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
DataFrame(columns=['symbol', 'name', 'exchange', 'market'])
|
|
212
|
+
"""
|
|
213
|
+
if self.config.use_cache:
|
|
214
|
+
cached = self._stocklist_cache.get_list(market)
|
|
215
|
+
if cached is not None:
|
|
216
|
+
return cached
|
|
217
|
+
|
|
218
|
+
df = self._ak.stock_info_a_code_name()
|
|
219
|
+
if df.empty:
|
|
220
|
+
return pd.DataFrame(columns=['symbol', 'name', 'exchange', 'market'])
|
|
221
|
+
|
|
222
|
+
# 转换格式:akshare 可能返回中文列名
|
|
223
|
+
df = df.rename(columns={"代码": "symbol", "名称": "name"})
|
|
224
|
+
# 兼容旧版本可能返回英文列名
|
|
225
|
+
if "symbol" not in df.columns:
|
|
226
|
+
df = df.rename(columns={"code": "symbol", "name": "name"})
|
|
227
|
+
df["exchange"] = df["symbol"].apply(
|
|
228
|
+
lambda x: "SSE" if x.startswith(("6", "5", "9")) else "SZSE"
|
|
229
|
+
)
|
|
230
|
+
df["market"] = df["exchange"].apply(lambda x: "上海" if x == "SSE" else "深圳")
|
|
231
|
+
|
|
232
|
+
# 过滤
|
|
233
|
+
df = self._market_filter(df, market)
|
|
234
|
+
|
|
235
|
+
if self.config.use_cache:
|
|
236
|
+
self._stocklist_cache.set_list(market, df)
|
|
237
|
+
|
|
238
|
+
return df
|
|
239
|
+
|
|
240
|
+
def get_trading_calendar(self, exchange: str = "SSE") -> List[date]:
|
|
241
|
+
"""获取交易日历"""
|
|
242
|
+
if self.config.use_cache:
|
|
243
|
+
cached = self._calendar_cache.get_calendar(exchange)
|
|
244
|
+
if cached is not None:
|
|
245
|
+
return sorted(pd.to_datetime(cached["trade_date"]).dt.date.tolist())
|
|
246
|
+
|
|
247
|
+
df = self._ak.tool_trade_date_hist_sina()
|
|
248
|
+
if df.empty:
|
|
249
|
+
return []
|
|
250
|
+
|
|
251
|
+
trading_dates = pd.to_datetime(df["trade_date"]).dt.date.tolist()
|
|
252
|
+
|
|
253
|
+
if self.config.use_cache:
|
|
254
|
+
self._calendar_cache.set_calendar(exchange, df)
|
|
255
|
+
|
|
256
|
+
return sorted(trading_dates)
|
|
257
|
+
|
|
258
|
+
# ==================== 辅助方法 ====================
|
|
259
|
+
|
|
260
|
+
def _filter_fields(
|
|
261
|
+
self,
|
|
262
|
+
df: pd.DataFrame,
|
|
263
|
+
fields: Optional[List[str]]
|
|
264
|
+
) -> pd.DataFrame:
|
|
265
|
+
"""过滤字段"""
|
|
266
|
+
if not fields:
|
|
267
|
+
return df
|
|
268
|
+
available = ["date", "open", "high", "low", "close", "volume", "amount"]
|
|
269
|
+
return df[[f for f in fields if f in available]]
|
|
270
|
+
|
|
271
|
+
def health_check(self) -> Dict[str, Any]:
|
|
272
|
+
"""健康检查"""
|
|
273
|
+
try:
|
|
274
|
+
self._ak.tool_trade_date_hist_sina()
|
|
275
|
+
return {
|
|
276
|
+
"status": "ok",
|
|
277
|
+
"source": self.name,
|
|
278
|
+
"cache": self.config.use_cache
|
|
279
|
+
}
|
|
280
|
+
except Exception as e:
|
|
281
|
+
return {"status": "error", "source": self.name, "error": str(e)}
|
|
282
|
+
|
|
283
|
+
# ==================== 不支持的接口 ====================
|
|
284
|
+
|
|
285
|
+
def get_fundamental(
|
|
286
|
+
self,
|
|
287
|
+
symbols: List[str],
|
|
288
|
+
date,
|
|
289
|
+
indicators: Optional[List[str]] = None
|
|
290
|
+
) -> pd.DataFrame:
|
|
291
|
+
"""基本面数据请使用 baostock 数据源"""
|
|
292
|
+
raise NotImplementedError(
|
|
293
|
+
"Use baostock datasource for fundamental data: "
|
|
294
|
+
"create_datasource('baostock') or create_datasource('mixed')"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
def get_fina_indicator(
|
|
298
|
+
self,
|
|
299
|
+
symbols: List[str],
|
|
300
|
+
report_type: str = "latest"
|
|
301
|
+
) -> pd.DataFrame:
|
|
302
|
+
raise NotImplementedError(
|
|
303
|
+
"Use baostock datasource for financial indicators"
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
def get_valuation(
|
|
307
|
+
self,
|
|
308
|
+
symbols: List[str],
|
|
309
|
+
date
|
|
310
|
+
) -> pd.DataFrame:
|
|
311
|
+
raise NotImplementedError(
|
|
312
|
+
"Use baostock datasource for valuation data"
|
|
313
|
+
)
|