This commit is contained in:
songzhuoyuan
2026-08-11 20:41:31 +08:00
parent 2ce13e5bae
commit befdc32aea
24 changed files with 1311 additions and 530 deletions
+159 -130
View File
@@ -11,6 +11,10 @@ from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 注入外部请求默认超时(覆盖akshare、tushare等基于requests的调用)
from http_timeout import install_default_requests_timeout
install_default_requests_timeout()
class DataSourceManager:
"""数据源管理器 - 实现akshare与tushare自动切换"""
@@ -25,7 +29,7 @@ class DataSourceManager:
try:
import tushare as ts
ts.set_token(self.tushare_token)
self.tushare_api = ts.pro_api()
self.tushare_api = ts.pro_api(timeout=float(os.getenv('TUSHARE_TIMEOUT', '15')))
self.tushare_available = True
print("✅ Tushare数据源初始化成功")
except Exception as e:
@@ -36,7 +40,7 @@ class DataSourceManager:
def get_stock_hist_data(self, symbol, start_date=None, end_date=None, adjust='qfq'):
"""
获取股票历史数据(优先akshare,失败时使用tushare
获取股票历史数据(优先tushare,失败时使用akshare
Args:
symbol: 股票代码(6位数字)
@@ -55,10 +59,63 @@ class DataSourceManager:
else:
end_date = datetime.now().strftime('%Y%m%d')
# 优先使用akshare
# 优先使用tushare
if self.tushare_available:
try:
import tushare as ts
print(f"[Tushare] 正在获取 {symbol} 的历史数据(主要数据源)...")
# 转换股票代码格式(添加市场后缀)
ts_code = self._convert_to_ts_code(symbol)
# 转换复权类型
adj_dict = {'qfq': 'qfq', 'hfq': 'hfq', '': None}
adj = adj_dict.get(adjust, 'qfq')
if adj is None:
# 不复权数据直接使用daily接口
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
else:
# 复权数据使用pro_bardaily接口不支持adj参数)
df = ts.pro_bar(
api=self.tushare_api,
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
adj=adj,
retry_count=1
)
if df is not None and not df.empty:
# 标准化列名和数据格式
df = df.rename(columns={
'trade_date': 'date',
'vol': 'volume',
'amount': 'amount'
})
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 转换成交量单位(tushare单位是手,转换为股)
df['volume'] = df['volume'] * 100
# 转换成交额单位(tushare单位是千元,转换为元)
df['amount'] = df['amount'] * 1000
print(f"[Tushare] ✅ 成功获取 {len(df)} 条数据")
return df
else:
print(f"[Tushare] ❌ 未获取到数据,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退到akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的历史数据...")
print(f"[Akshare] 正在获取 {symbol} 的历史数据(备用数据源)...")
df = ak.stock_zh_a_hist(
symbol=symbol,
@@ -86,60 +143,18 @@ class DataSourceManager:
df['date'] = pd.to_datetime(df['date'])
print(f"[Akshare] ✅ 成功获取 {len(df)} 条数据")
return df
else:
print(f"[Akshare] ❌ 未获取到数据")
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的历史数据(备用数据源)...")
# 转换股票代码格式(添加市场后缀)
ts_code = self._convert_to_ts_code(symbol)
# 转换复权类型
adj_dict = {'qfq': 'qfq', 'hfq': 'hfq', '': None}
adj = adj_dict.get(adjust, 'qfq')
# 格式化日期
start = f"{start_date[:4]}-{start_date[4:6]}-{start_date[6:]}" if start_date else None
end = f"{end_date[:4]}-{end_date[4:6]}-{end_date[6:]}" if end_date else None
# 获取数据
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
adj=adj
)
if df is not None and not df.empty:
# 标准化列名和数据格式
df = df.rename(columns={
'trade_date': 'date',
'vol': 'volume',
'amount': 'amount'
})
df['date'] = pd.to_datetime(df['date'])
df = df.sort_values('date')
# 转换成交量单位(tushare单位是手,转换为股)
df['volume'] = df['volume'] * 100
# 转换成交额单位(tushare单位是千元,转换为元)
df['amount'] = df['amount'] * 1000
print(f"[Tushare] ✅ 成功获取 {len(df)} 条数据")
return df
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# 两个数据源都失败
print("❌ 所有数据源均获取失败")
return None
def get_stock_basic_info(self, symbol):
"""
获取股票基本信息(优先akshare,失败时使用tushare
获取股票基本信息(优先tushare,失败时使用akshare
Args:
symbol: 股票代码
@@ -154,10 +169,34 @@ class DataSourceManager:
"market": "未知"
}
# 优先使用akshare
# 优先使用tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的基本信息(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.stock_basic(
ts_code=ts_code,
fields='ts_code,name,area,industry,market,list_date'
)
if df is not None and not df.empty:
info['name'] = df.iloc[0]['name']
info['industry'] = df.iloc[0]['industry']
info['market'] = df.iloc[0]['market']
info['list_date'] = df.iloc[0]['list_date']
print(f"[Tushare] ✅ 成功获取基本信息")
return info
else:
print(f"[Tushare] ❌ 未获取到基本信息,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的基本信息...")
print(f"[Akshare] 正在获取 {symbol} 的基本信息(备用数据源)...")
stock_info = ak.stock_individual_info_em(symbol=symbol)
if stock_info is not None and not stock_info.empty:
@@ -181,33 +220,11 @@ class DataSourceManager:
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的基本信息(备用数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.stock_basic(
ts_code=ts_code,
fields='ts_code,name,area,industry,market,list_date'
)
if df is not None and not df.empty:
info['name'] = df.iloc[0]['name']
info['industry'] = df.iloc[0]['industry']
info['market'] = df.iloc[0]['market']
info['list_date'] = df.iloc[0]['list_date']
print(f"[Tushare] ✅ 成功获取基本信息")
return info
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
return info
def get_realtime_quotes(self, symbol):
"""
获取实时行情数据(优先akshare,失败时使用tushare
获取实时行情数据(优先tushare,失败时使用akshare
Args:
symbol: 股票代码
@@ -217,10 +234,51 @@ class DataSourceManager:
"""
quotes = {}
# 优先使用akshare
# 优先使用tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的实时行情(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol)
today = datetime.now().strftime('%Y%m%d')
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=today,
end_date=today
)
if df is None or df.empty:
# 非交易日时,获取最近10个交易日的最新数据
start = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=start,
end_date=today
)
if df is not None and not df.empty:
row = df.iloc[0]
quotes = {
'symbol': symbol,
'price': row['close'],
'change_percent': row['pct_chg'],
'volume': row['vol'] * 100,
'amount': row['amount'] * 1000,
'high': row['high'],
'low': row['low'],
'open': row['open'],
'pre_close': row['pre_close']
}
print(f"[Tushare] ✅ 成功获取实时行情")
return quotes
else:
print(f"[Tushare] ❌ 未获取到实时行情,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的实时行情...")
print(f"[Akshare] 正在获取 {symbol} 的实时行情(备用数据源)...")
df = ak.stock_zh_a_spot_em()
stock_df = df[df['代码'] == symbol]
@@ -245,41 +303,11 @@ class DataSourceManager:
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的实时行情(备用数据源)...")
ts_code = self._convert_to_ts_code(symbol)
df = self.tushare_api.daily(
ts_code=ts_code,
start_date=datetime.now().strftime('%Y%m%d'),
end_date=datetime.now().strftime('%Y%m%d')
)
if df is not None and not df.empty:
row = df.iloc[0]
quotes = {
'symbol': symbol,
'price': row['close'],
'change_percent': row['pct_chg'],
'volume': row['vol'] * 100,
'amount': row['amount'] * 1000,
'high': row['high'],
'low': row['low'],
'open': row['open'],
'pre_close': row['pre_close']
}
print(f"[Tushare] ✅ 成功获取实时行情")
return quotes
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
return quotes
def get_financial_data(self, symbol, report_type='income'):
"""
获取财务数据(优先akshare,失败时使用tushare
获取财务数据(优先tushare,失败时使用akshare
Args:
symbol: 股票代码
@@ -288,30 +316,10 @@ class DataSourceManager:
Returns:
DataFrame: 财务数据
"""
# 优先使用akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的财务数据...")
if report_type == 'income':
df = ak.stock_financial_report_sina(stock=symbol, symbol="利润表")
elif report_type == 'balance':
df = ak.stock_financial_report_sina(stock=symbol, symbol="资产负债表")
elif report_type == 'cashflow':
df = ak.stock_financial_report_sina(stock=symbol, symbol="现金流量表")
else:
df = None
if df is not None and not df.empty:
print(f"[Akshare] ✅ 成功获取财务数据")
return df
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
# akshare失败,尝试tushare
# 优先使用tushare
if self.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的财务数据(备用数据源)...")
print(f"[Tushare] 正在获取 {symbol} 的财务数据(主要数据源)...")
ts_code = self._convert_to_ts_code(symbol)
@@ -327,9 +335,31 @@ class DataSourceManager:
if df is not None and not df.empty:
print(f"[Tushare] ✅ 成功获取财务数据")
return df
else:
print(f"[Tushare] ❌ 未获取到财务数据,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取失败: {e}")
# tushare失败,回退akshare
try:
import akshare as ak
print(f"[Akshare] 正在获取 {symbol} 的财务数据(备用数据源)...")
if report_type == 'income':
df = ak.stock_financial_report_sina(stock=symbol, symbol="利润表")
elif report_type == 'balance':
df = ak.stock_financial_report_sina(stock=symbol, symbol="资产负债表")
elif report_type == 'cashflow':
df = ak.stock_financial_report_sina(stock=symbol, symbol="现金流量表")
else:
df = None
if df is not None and not df.empty:
print(f"[Akshare] ✅ 成功获取财务数据")
return df
except Exception as e:
print(f"[Akshare] ❌ 获取失败: {e}")
return None
def _convert_to_ts_code(self, symbol):
@@ -376,4 +406,3 @@ class DataSourceManager:
# 全局数据源管理器实例
data_source_manager = DataSourceManager()