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.
@@ -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
+ )