|
|
|
|
@ -1,637 +0,0 @@
|
|
|
|
|
"""AKShare数据源适配器 - 使用新浪接口"""
|
|
|
|
|
import asyncio
|
|
|
|
|
import time
|
|
|
|
|
from datetime import datetime
|
|
|
|
|
from typing import List, Optional, Dict, Any
|
|
|
|
|
|
|
|
|
|
import akshare as ak
|
|
|
|
|
import pandas as pd
|
|
|
|
|
|
|
|
|
|
from app.adapters.base import (
|
|
|
|
|
DataSourceAdapter, TickData, KLineData, SymbolInfo,
|
|
|
|
|
TradeCalData, TickCallback
|
|
|
|
|
)
|
|
|
|
|
from app.core.logger import info, error, warning
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 缓存字典,用于存储股票基本信息和交易日历
|
|
|
|
|
_stock_info_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
_trade_calendar_cache: Optional[pd.DataFrame] = None
|
|
|
|
|
_inst_holding_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AKShareAdapter(DataSourceAdapter):
|
|
|
|
|
"""AKShare数据源适配器 - 使用新浪接口"""
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.config = {}
|
|
|
|
|
self._connected = False
|
|
|
|
|
self._max_retries = 3
|
|
|
|
|
self._retry_delay = 2 # 秒
|
|
|
|
|
|
|
|
|
|
# 实例缓存
|
|
|
|
|
self._stock_info_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
self._trade_calendar_cache: Optional[pd.DataFrame] = None
|
|
|
|
|
self._inst_holding_cache: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
|
|
|
|
def _get_stock_code_from_symbol(self, symbol: str) -> str:
|
|
|
|
|
"""从 symbol 中提取股票代码: 000001.SZ -> 000001"""
|
|
|
|
|
if "." in symbol:
|
|
|
|
|
return symbol.split(".")[0]
|
|
|
|
|
return symbol
|
|
|
|
|
|
|
|
|
|
async def _get_trade_calendar(self) -> pd.DataFrame:
|
|
|
|
|
"""获取交易日历(带缓存)"""
|
|
|
|
|
if self._trade_calendar_cache is None:
|
|
|
|
|
try:
|
|
|
|
|
df = await self._fetch_with_retry(ak.tool_trade_date_hist_sina)
|
|
|
|
|
if df is not None and not df.empty:
|
|
|
|
|
df['trade_date'] = pd.to_datetime(df['trade_date'])
|
|
|
|
|
self._trade_calendar_cache = df
|
|
|
|
|
info(f"Loaded trade calendar with {len(df)} trading days")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to load trade calendar: {e}")
|
|
|
|
|
return pd.DataFrame()
|
|
|
|
|
return self._trade_calendar_cache if self._trade_calendar_cache is not None else pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
async def _get_stock_info(self, stock_code: str) -> Dict[str, Any]:
|
|
|
|
|
"""获取股票基本信息(带缓存)"""
|
|
|
|
|
if stock_code not in self._stock_info_cache:
|
|
|
|
|
try:
|
|
|
|
|
# 使用东财接口获取个股信息
|
|
|
|
|
info_df = await self._fetch_with_retry(ak.stock_individual_info_em, symbol=stock_code)
|
|
|
|
|
if info_df is not None and not info_df.empty:
|
|
|
|
|
info_dict = dict(zip(info_df['item'], info_df['value']))
|
|
|
|
|
self._stock_info_cache[stock_code] = info_dict
|
|
|
|
|
info(f"Loaded stock info for {stock_code}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to get stock info for {stock_code}: {e}")
|
|
|
|
|
return {}
|
|
|
|
|
return self._stock_info_cache.get(stock_code, {})
|
|
|
|
|
|
|
|
|
|
async def _get_trading_days_count(self, stock_code: str, trade_date: datetime) -> int:
|
|
|
|
|
"""获取可交易日数(从上市至今)"""
|
|
|
|
|
try:
|
|
|
|
|
stock_info = await self._get_stock_info(stock_code)
|
|
|
|
|
listing_date_str = str(stock_info.get('上市时间', ''))
|
|
|
|
|
|
|
|
|
|
if not listing_date_str or listing_date_str == 'nan':
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
listing_date = datetime.strptime(listing_date_str, '%Y%m%d').date()
|
|
|
|
|
trade_calendar = await self._get_trade_calendar()
|
|
|
|
|
|
|
|
|
|
if trade_calendar.empty:
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
# 计算从上市到指定日期的交易日数
|
|
|
|
|
trade_calendar['trade_date'] = pd.to_datetime(trade_calendar['trade_date']).dt.date
|
|
|
|
|
trading_days = trade_calendar[
|
|
|
|
|
(trade_calendar['trade_date'] >= listing_date) &
|
|
|
|
|
(trade_calendar['trade_date'] <= trade_date.date())
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return len(trading_days)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to calculate trading days for {stock_code}: {e}")
|
|
|
|
|
return 0
|
|
|
|
|
|
|
|
|
|
async def _check_limit_up_down(self, stock_code: str, trade_date: str, close_price: float) -> tuple:
|
|
|
|
|
"""检查是否涨停或跌停"""
|
|
|
|
|
try:
|
|
|
|
|
# 获取涨停/跌停股票池
|
|
|
|
|
zt_df = await self._fetch_with_retry(ak.stock_zt_pool_em, date=trade_date)
|
|
|
|
|
dt_df = await self._fetch_with_retry(ak.stock_zt_pool_dtgc_em, date=trade_date)
|
|
|
|
|
|
|
|
|
|
is_limit_up = False
|
|
|
|
|
is_limit_down = False
|
|
|
|
|
|
|
|
|
|
if zt_df is not None and not zt_df.empty:
|
|
|
|
|
zt_list = zt_df['代码'].astype(str).tolist()
|
|
|
|
|
is_limit_up = stock_code in zt_list
|
|
|
|
|
|
|
|
|
|
if dt_df is not None and not dt_df.empty:
|
|
|
|
|
dt_list = dt_df['代码'].astype(str).tolist()
|
|
|
|
|
is_limit_down = stock_code in dt_list
|
|
|
|
|
|
|
|
|
|
return is_limit_up, is_limit_down
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 涨停跌停池接口可能不支持历史日期,失败时返回False
|
|
|
|
|
info(f"Could not get limit up/down info for {stock_code} on {trade_date}: {e}")
|
|
|
|
|
return False, False
|
|
|
|
|
|
|
|
|
|
async def _get_market_cap(self, stock_code: str) -> tuple:
|
|
|
|
|
"""获取总市值和流通市值"""
|
|
|
|
|
try:
|
|
|
|
|
stock_info = await self._get_stock_info(stock_code)
|
|
|
|
|
|
|
|
|
|
total_cap = stock_info.get('总市值', 0)
|
|
|
|
|
float_cap = stock_info.get('流通市值', 0)
|
|
|
|
|
|
|
|
|
|
# 转换为浮点数
|
|
|
|
|
total_cap = float(total_cap) if total_cap and str(total_cap) != 'nan' else 0.0
|
|
|
|
|
float_cap = float(float_cap) if float_cap and str(float_cap) != 'nan' else 0.0
|
|
|
|
|
|
|
|
|
|
return total_cap, float_cap
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to get market cap for {stock_code}: {e}")
|
|
|
|
|
return 0.0, 0.0
|
|
|
|
|
|
|
|
|
|
async def _get_inst_holding_ratio(self, stock_code: str) -> float:
|
|
|
|
|
"""获取机构持仓占比"""
|
|
|
|
|
# 缓存键使用季度标识(简化处理,实际应按财报季度)
|
|
|
|
|
cache_key = f"{stock_code}_latest"
|
|
|
|
|
|
|
|
|
|
if cache_key not in self._inst_holding_cache:
|
|
|
|
|
try:
|
|
|
|
|
# 获取基金持仓数据
|
|
|
|
|
fund_holder_df = await self._fetch_with_retry(ak.stock_fund_stock_holder, symbol=stock_code)
|
|
|
|
|
|
|
|
|
|
if fund_holder_df is not None and not fund_holder_df.empty:
|
|
|
|
|
# 获取最新季度的数据
|
|
|
|
|
latest_quarter = fund_holder_df['季度'].iloc[0]
|
|
|
|
|
latest_df = fund_holder_df[fund_holder_df['季度'] == latest_quarter]
|
|
|
|
|
|
|
|
|
|
# 计算机构持仓占比合计
|
|
|
|
|
total_ratio = 0.0
|
|
|
|
|
if '占总股本比例' in latest_df.columns:
|
|
|
|
|
ratios = pd.to_numeric(latest_df['占总股本比例'], errors='coerce').fillna(0)
|
|
|
|
|
total_ratio = ratios.sum()
|
|
|
|
|
|
|
|
|
|
self._inst_holding_cache[cache_key] = {
|
|
|
|
|
'quarter': latest_quarter,
|
|
|
|
|
'ratio': total_ratio
|
|
|
|
|
}
|
|
|
|
|
info(f"Loaded inst holding for {stock_code}: {total_ratio:.4f}% in {latest_quarter}")
|
|
|
|
|
else:
|
|
|
|
|
self._inst_holding_cache[cache_key] = {'quarter': '', 'ratio': 0.0}
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to get inst holding for {stock_code}: {e}")
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
return self._inst_holding_cache.get(cache_key, {}).get('ratio', 0.0)
|
|
|
|
|
|
|
|
|
|
async def connect(self, config: dict) -> None:
|
|
|
|
|
"""建立连接(AKShare无需认证)"""
|
|
|
|
|
self.config = config
|
|
|
|
|
self._connected = True
|
|
|
|
|
info("AKShare adapter connected (Sina API)")
|
|
|
|
|
|
|
|
|
|
async def subscribe_ticks(self, symbols: List[str], callback: TickCallback) -> None:
|
|
|
|
|
"""订阅实时Tick(AKShare不支持实时推送)"""
|
|
|
|
|
raise NotImplementedError("AKShare does not support real-time tick subscription")
|
|
|
|
|
|
|
|
|
|
async def fetch_klines(
|
|
|
|
|
self,
|
|
|
|
|
symbol: str,
|
|
|
|
|
start: str,
|
|
|
|
|
end: str,
|
|
|
|
|
freq: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
info(f"Fetching KLines from Sina for {symbol} [{freq}] from {start} to {end}")
|
|
|
|
|
"""拉取历史K线"""
|
|
|
|
|
# 判断是股票还是期货
|
|
|
|
|
if ".SH" in symbol or ".SZ" in symbol or ".BJ" in symbol:
|
|
|
|
|
return await self._fetch_stock_klines(symbol, start, end, freq)
|
|
|
|
|
elif "." in symbol: # 期货格式: CU2504.SHFE
|
|
|
|
|
return await self._fetch_futures_klines(symbol, start, end, freq)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unknown symbol format: {symbol}")
|
|
|
|
|
|
|
|
|
|
async def _fetch_stock_klines(
|
|
|
|
|
self,
|
|
|
|
|
symbol: str,
|
|
|
|
|
start: str,
|
|
|
|
|
end: str,
|
|
|
|
|
freq: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取股票K线 - 使用新浪接口"""
|
|
|
|
|
# 转换symbol格式: 000001.SZ -> sz000001
|
|
|
|
|
ts_code = self._normalize_stock_symbol(symbol)
|
|
|
|
|
|
|
|
|
|
if freq in ["1d", "day", "D", ""]:
|
|
|
|
|
return await self._fetch_stock_daily_sina(ts_code, symbol, start, end)
|
|
|
|
|
elif freq in ["1m", "5m", "15m", "30m", "60m"]:
|
|
|
|
|
return await self._fetch_stock_minute_sina(ts_code, symbol, start, end, freq)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unsupported frequency: {freq}")
|
|
|
|
|
|
|
|
|
|
def _normalize_stock_symbol(self, symbol: str) -> str:
|
|
|
|
|
"""转换股票代码格式: 000001.SZ -> sz000001"""
|
|
|
|
|
if "." in symbol:
|
|
|
|
|
code, exchange = symbol.split(".")
|
|
|
|
|
exchange_map = {
|
|
|
|
|
"SH": "sh",
|
|
|
|
|
"SZ": "sz",
|
|
|
|
|
"BJ": "bj"
|
|
|
|
|
}
|
|
|
|
|
return exchange_map.get(exchange, "sz") + code
|
|
|
|
|
return symbol
|
|
|
|
|
|
|
|
|
|
def _denormalize_stock_symbol(self, symbol: str) -> str:
|
|
|
|
|
"""还原股票代码格式: sz000001 -> 000001.SZ"""
|
|
|
|
|
if symbol.startswith("sh"):
|
|
|
|
|
return symbol[2:] + ".SH"
|
|
|
|
|
elif symbol.startswith("sz"):
|
|
|
|
|
return symbol[2:] + ".SZ"
|
|
|
|
|
elif symbol.startswith("bj"):
|
|
|
|
|
return symbol[2:] + ".BJ"
|
|
|
|
|
return symbol
|
|
|
|
|
|
|
|
|
|
async def _fetch_with_retry(self, func, *args, **kwargs):
|
|
|
|
|
"""带重试机制的调用"""
|
|
|
|
|
last_exception = None
|
|
|
|
|
|
|
|
|
|
for attempt in range(self._max_retries):
|
|
|
|
|
try:
|
|
|
|
|
loop = asyncio.get_event_loop()
|
|
|
|
|
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
last_exception = e
|
|
|
|
|
error_msg = str(e).lower()
|
|
|
|
|
|
|
|
|
|
# 检查是否是可重试的错误
|
|
|
|
|
if any(x in error_msg for x in ['connection', 'timeout', 'remote', 'reset', 'closed']):
|
|
|
|
|
if attempt < self._max_retries - 1:
|
|
|
|
|
warning(f"Sina API request failed (attempt {attempt + 1}/{self._max_retries}): {e}")
|
|
|
|
|
await asyncio.sleep(self._retry_delay * (attempt + 1)) # 指数退避
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 不可重试的错误,直接抛出
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
raise last_exception
|
|
|
|
|
|
|
|
|
|
async def _fetch_stock_daily_sina(
|
|
|
|
|
self,
|
|
|
|
|
ts_code: str,
|
|
|
|
|
original_symbol: str,
|
|
|
|
|
start_date: str,
|
|
|
|
|
end_date: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取股票日线 - 使用新浪接口(包含扩展字段)"""
|
|
|
|
|
try:
|
|
|
|
|
# 新浪接口获取历史数据
|
|
|
|
|
# 使用 stock_zh_a_daily 接口(新浪)
|
|
|
|
|
df = await self._fetch_with_retry(
|
|
|
|
|
ak.stock_zh_a_daily,
|
|
|
|
|
symbol=ts_code,
|
|
|
|
|
start_date=start_date,
|
|
|
|
|
end_date=end_date,
|
|
|
|
|
adjust="qfq" # 前复权
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
warning(f"No data returned from Sina for {original_symbol}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# 获取股票代码(不带交易所后缀)
|
|
|
|
|
stock_code = self._get_stock_code_from_symbol(original_symbol)
|
|
|
|
|
|
|
|
|
|
# 预获取市值和机构持仓数据(这些不随日期变化)
|
|
|
|
|
total_cap, float_cap = await self._get_market_cap(stock_code)
|
|
|
|
|
inst_ratio = await self._get_inst_holding_ratio(stock_code)
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
trade_date = datetime.strptime(str(row['date']), "%Y-%m-%d")
|
|
|
|
|
trade_date_str = trade_date.strftime("%Y%m%d")
|
|
|
|
|
close_price = float(row['close'])
|
|
|
|
|
|
|
|
|
|
# 获取可交易日数(只计算一次,以当前日期为准)
|
|
|
|
|
trading_days = await self._get_trading_days_count(stock_code, trade_date)
|
|
|
|
|
|
|
|
|
|
# 检查涨停跌停(注意:历史数据可能无法准确判断)
|
|
|
|
|
is_limit_up, is_limit_down = await self._check_limit_up_down(
|
|
|
|
|
stock_code, trade_date_str, close_price
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
results.append(KLineData(
|
|
|
|
|
symbol=original_symbol,
|
|
|
|
|
time=int(trade_date.timestamp()),
|
|
|
|
|
open=float(row['open']),
|
|
|
|
|
high=float(row['high']),
|
|
|
|
|
low=float(row['low']),
|
|
|
|
|
close=close_price,
|
|
|
|
|
volume=int(row['volume']),
|
|
|
|
|
amount=float(row.get('amount', 0)),
|
|
|
|
|
trade_date=trade_date.strftime('%Y-%m-%d'),
|
|
|
|
|
is_limit_up=is_limit_up,
|
|
|
|
|
is_limit_down=is_limit_down,
|
|
|
|
|
total_market_cap=total_cap,
|
|
|
|
|
float_market_cap=float_cap,
|
|
|
|
|
inst_holding_ratio=inst_ratio,
|
|
|
|
|
trading_days=trading_days
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
info(f"Fetched {len(results)} daily klines with extended fields from Sina for {original_symbol}")
|
|
|
|
|
return results
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch stock daily from Sina for {original_symbol}: {e}")
|
|
|
|
|
# 新浪接口失败时返回空列表
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def _fetch_stock_minute_sina(
|
|
|
|
|
self,
|
|
|
|
|
ts_code: str,
|
|
|
|
|
original_symbol: str,
|
|
|
|
|
start_date: str,
|
|
|
|
|
end_date: str,
|
|
|
|
|
freq: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取股票分钟线 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 新浪分钟线接口
|
|
|
|
|
# 使用 stock_zh_a_minute 接口
|
|
|
|
|
df = await self._fetch_with_retry(
|
|
|
|
|
ak.stock_zh_a_minute,
|
|
|
|
|
symbol=ts_code,
|
|
|
|
|
period=freq.replace("m", ""), # 1m -> 1
|
|
|
|
|
adjust="qfq"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# 过滤日期范围
|
|
|
|
|
df['date'] = pd.to_datetime(df['date'])
|
|
|
|
|
start_dt = datetime.strptime(start_date, "%Y%m%d")
|
|
|
|
|
end_dt = datetime.strptime(end_date, "%Y%m%d")
|
|
|
|
|
df = df[(df['date'] >= start_dt) & (df['date'] <= end_dt)]
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
trade_time = datetime.strptime(str(row['date']), "%Y-%m-%d %H:%M:%S")
|
|
|
|
|
results.append(KLineData(
|
|
|
|
|
symbol=original_symbol,
|
|
|
|
|
time=int(trade_time.timestamp()),
|
|
|
|
|
open=float(row['open']),
|
|
|
|
|
high=float(row['high']),
|
|
|
|
|
low=float(row['low']),
|
|
|
|
|
close=float(row['close']),
|
|
|
|
|
volume=int(row['volume']),
|
|
|
|
|
amount=float(row.get('amount', 0))
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch stock minute from Sina for {original_symbol}: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def _fetch_futures_klines(
|
|
|
|
|
self,
|
|
|
|
|
symbol: str,
|
|
|
|
|
start: str,
|
|
|
|
|
end: str,
|
|
|
|
|
freq: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取期货K线 - 使用新浪接口"""
|
|
|
|
|
if freq in ["1d", "day", "D", ""]:
|
|
|
|
|
return await self._fetch_futures_daily_sina(symbol, start, end)
|
|
|
|
|
elif freq in ["1m", "5m", "15m", "30m", "60m"]:
|
|
|
|
|
return await self._fetch_futures_minute_sina(symbol, start, end, freq)
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unsupported frequency: {freq}")
|
|
|
|
|
|
|
|
|
|
async def _fetch_futures_daily_sina(
|
|
|
|
|
self,
|
|
|
|
|
symbol: str,
|
|
|
|
|
start_date: str,
|
|
|
|
|
end_date: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取期货日线 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析合约代码: CU2504.SHFE -> cu2504
|
|
|
|
|
contract_code, exchange = symbol.split(".")
|
|
|
|
|
contract_code = contract_code.lower()
|
|
|
|
|
|
|
|
|
|
# 新浪期货历史行情接口
|
|
|
|
|
df = await self._fetch_with_retry(
|
|
|
|
|
ak.futures_zh_daily,
|
|
|
|
|
symbol=contract_code,
|
|
|
|
|
start_date=start_date,
|
|
|
|
|
end_date=end_date
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
trade_date = datetime.strptime(str(row['date']), "%Y-%m-%d")
|
|
|
|
|
results.append(KLineData(
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
time=int(trade_date.timestamp()),
|
|
|
|
|
open=float(row['open']),
|
|
|
|
|
high=float(row['high']),
|
|
|
|
|
low=float(row['low']),
|
|
|
|
|
close=float(row['close']),
|
|
|
|
|
volume=int(row['volume']),
|
|
|
|
|
amount=float(row.get('amount', 0)),
|
|
|
|
|
open_interest=int(row.get('hold', 0))
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch futures daily from Sina for {symbol}: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def _fetch_futures_minute_sina(
|
|
|
|
|
self,
|
|
|
|
|
symbol: str,
|
|
|
|
|
start_date: str,
|
|
|
|
|
end_date: str,
|
|
|
|
|
freq: str
|
|
|
|
|
) -> List[KLineData]:
|
|
|
|
|
"""获取期货分钟线 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析合约代码
|
|
|
|
|
contract_code, exchange = symbol.split(".")
|
|
|
|
|
contract_code = contract_code.lower()
|
|
|
|
|
|
|
|
|
|
# 新浪期货分钟线接口
|
|
|
|
|
df = await self._fetch_with_retry(
|
|
|
|
|
ak.futures_zh_minute_sina,
|
|
|
|
|
symbol=contract_code,
|
|
|
|
|
period=freq.replace("m", "")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# 过滤日期范围
|
|
|
|
|
df['datetime'] = pd.to_datetime(df['datetime'])
|
|
|
|
|
start_dt = datetime.strptime(start_date, "%Y%m%d")
|
|
|
|
|
end_dt = datetime.strptime(end_date, "%Y%m%d")
|
|
|
|
|
df = df[(df['datetime'] >= start_dt) & (df['datetime'] <= end_dt)]
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
trade_time = row['datetime']
|
|
|
|
|
results.append(KLineData(
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
time=int(trade_time.timestamp()),
|
|
|
|
|
open=float(row['open']),
|
|
|
|
|
high=float(row['high']),
|
|
|
|
|
low=float(row['low']),
|
|
|
|
|
close=float(row['close']),
|
|
|
|
|
volume=int(row['volume']),
|
|
|
|
|
amount=0,
|
|
|
|
|
open_interest=0
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch futures minute from Sina for {symbol}: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def fetch_symbols(self, asset_type: str) -> List[SymbolInfo]:
|
|
|
|
|
"""获取标的列表"""
|
|
|
|
|
if asset_type == "stock":
|
|
|
|
|
return await self._fetch_stock_symbols_sina()
|
|
|
|
|
elif asset_type == "futures":
|
|
|
|
|
return await self._fetch_futures_symbols_sina()
|
|
|
|
|
else:
|
|
|
|
|
raise ValueError(f"Unsupported asset type: {asset_type}")
|
|
|
|
|
|
|
|
|
|
async def _fetch_stock_symbols_sina(self) -> List[SymbolInfo]:
|
|
|
|
|
"""获取A股股票列表 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 新浪A股列表接口
|
|
|
|
|
df = await self._fetch_with_retry(ak.stock_zh_a_spot)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
# 新浪接口的代码格式
|
|
|
|
|
code = str(row['代码'])
|
|
|
|
|
if code.startswith('6') or code.startswith('5') or code.startswith('9'):
|
|
|
|
|
ts_code = f"{code}.SH"
|
|
|
|
|
exchange = "SH"
|
|
|
|
|
elif code.startswith('8') or code.startswith('4'):
|
|
|
|
|
ts_code = f"{code}.BJ"
|
|
|
|
|
exchange = "BJ"
|
|
|
|
|
else:
|
|
|
|
|
ts_code = f"{code}.SZ"
|
|
|
|
|
exchange = "SZ"
|
|
|
|
|
|
|
|
|
|
results.append(SymbolInfo(
|
|
|
|
|
symbol_id=ts_code,
|
|
|
|
|
name=str(row['名称']),
|
|
|
|
|
exchange=exchange
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
info(f"Fetched {len(results)} stock symbols from Sina")
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch stock symbols from Sina: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def _fetch_futures_symbols_sina(self) -> List[SymbolInfo]:
|
|
|
|
|
"""获取期货合约列表 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 新浪期货列表接口
|
|
|
|
|
df = await self._fetch_with_retry(ak.futures_zh_realtime, subscribe_list=["0", "1", "2", "3"])
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
symbol = str(row['symbol'])
|
|
|
|
|
underlying = ''.join([c for c in symbol if c.isalpha()]).upper()
|
|
|
|
|
contract_month = ''.join([c for c in symbol if c.isdigit()])
|
|
|
|
|
|
|
|
|
|
exchange = self._get_futures_exchange(underlying)
|
|
|
|
|
ts_code = f"{symbol.upper()}.{exchange}"
|
|
|
|
|
|
|
|
|
|
results.append(SymbolInfo(
|
|
|
|
|
symbol_id=ts_code,
|
|
|
|
|
name=str(row.get('name', symbol)),
|
|
|
|
|
exchange=exchange,
|
|
|
|
|
underlying=underlying,
|
|
|
|
|
contract_month=contract_month
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
info(f"Fetched {len(results)} futures symbols from Sina")
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch futures symbols from Sina: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
def _get_futures_exchange(self, underlying: str) -> str:
|
|
|
|
|
"""根据品种代码判断交易所"""
|
|
|
|
|
# 上海期货交易所
|
|
|
|
|
if underlying in ['CU', 'AL', 'ZN', 'PB', 'NI', 'SN', 'AU', 'AG', 'RB', 'HC',
|
|
|
|
|
'BU', 'RU', 'FU', 'SP', 'WR', 'SS', 'LU', 'NR']:
|
|
|
|
|
return 'SHFE'
|
|
|
|
|
# 大连商品交易所
|
|
|
|
|
elif underlying in ['A', 'B', 'M', 'Y', 'P', 'C', 'CS', 'JD', 'LH', 'JM',
|
|
|
|
|
'J', 'I', 'FB', 'BB', 'RR', 'PG', 'EB', 'EG', 'V', 'PP', 'L']:
|
|
|
|
|
return 'DCE'
|
|
|
|
|
# 郑州商品交易所
|
|
|
|
|
elif underlying in ['WH', 'PM', 'CF', 'SR', 'TA', 'OI', 'RI', 'MA', 'FG', 'RS',
|
|
|
|
|
'RM', 'JR', 'LR', 'SM', 'SF', 'CY', 'AP', 'CJ', 'UR', 'SA', 'PF', 'PK']:
|
|
|
|
|
return 'CZCE'
|
|
|
|
|
# 中国金融期货交易所
|
|
|
|
|
elif underlying in ['IF', 'IC', 'IH', 'T', 'TF', 'TS', 'IM']:
|
|
|
|
|
return 'CFFEX'
|
|
|
|
|
# 上海国际能源交易中心
|
|
|
|
|
elif underlying in ['SC', 'BC', 'EC']:
|
|
|
|
|
return 'INE'
|
|
|
|
|
else:
|
|
|
|
|
return 'SHFE' # 默认上海
|
|
|
|
|
|
|
|
|
|
async def fetch_trading_calendar(
|
|
|
|
|
self,
|
|
|
|
|
exchange: str,
|
|
|
|
|
start: str,
|
|
|
|
|
end: str
|
|
|
|
|
) -> List[TradeCalData]:
|
|
|
|
|
"""获取交易日历 - 使用新浪接口"""
|
|
|
|
|
try:
|
|
|
|
|
# 新浪交易日历接口
|
|
|
|
|
df = await self._fetch_with_retry(ak.tool_trade_date_hist_sina)
|
|
|
|
|
|
|
|
|
|
if df is None or df.empty:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
# 过滤日期范围
|
|
|
|
|
df['trade_date'] = pd.to_datetime(df['trade_date'])
|
|
|
|
|
start_dt = datetime.strptime(start, "%Y%m%d")
|
|
|
|
|
end_dt = datetime.strptime(end, "%Y%m%d")
|
|
|
|
|
df = df[(df['trade_date'] >= start_dt) & (df['trade_date'] <= end_dt)]
|
|
|
|
|
|
|
|
|
|
results = []
|
|
|
|
|
for _, row in df.iterrows():
|
|
|
|
|
cal_date = row['trade_date']
|
|
|
|
|
results.append(TradeCalData(
|
|
|
|
|
date=cal_date,
|
|
|
|
|
is_trading_day=True
|
|
|
|
|
))
|
|
|
|
|
|
|
|
|
|
return results
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Failed to fetch trading calendar from Sina: {e}")
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
async def health_check(self) -> bool:
|
|
|
|
|
"""健康检查"""
|
|
|
|
|
try:
|
|
|
|
|
if not self._connected:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# 尝试获取股票列表作为健康检查
|
|
|
|
|
df = await self._fetch_with_retry(ak.stock_zh_a_spot)
|
|
|
|
|
return df is not None and not df.empty
|
|
|
|
|
except Exception as e:
|
|
|
|
|
error(f"Health check failed: {e}")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
async def close(self) -> None:
|
|
|
|
|
"""关闭连接"""
|
|
|
|
|
self._connected = False
|
|
|
|
|
info("AKShare adapter closed (Sina API)")
|