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
+330 -110
View File
@@ -8,10 +8,46 @@ import requests
import json
import pywencai
from data_source_manager import data_source_manager
from http_timeout import call_with_timeout
class StockDataFetcher:
"""股票数据获取类"""
# tushare财务字段 -> 中文映射(供AI分析展示)
INCOME_TS_MAP = {
'报告期': 'end_date',
'营业总收入': 'total_revenue',
'营业收入': 'revenue',
'营业总成本': 'total_operate_cost',
'营业利润': 'operate_profit',
'利润总额': 'total_profit',
'净利润': 'n_income',
'归属于母公司所有者的净利润': 'n_income_attr_p',
'基本每股收益': 'basic_eps',
'稀释每股收益': 'diluted_eps',
'销售费用': 'sell_exp',
'管理费用': 'admin_exp',
'财务费用': 'fin_exp',
'研发费用': 'rd_exp',
}
BALANCE_TS_MAP = {
'报告期': 'end_date',
'资产总计': 'total_assets',
'流动资产合计': 'total_cur_assets',
'非流动资产合计': 'total_ncur_assets',
'负债合计': 'total_liab',
'流动负债合计': 'total_cur_liab',
'非流动负债合计': 'total_ncur_liab',
'所有者权益合计': 'total_hldr_eqy_inc_min_int',
'归属于母公司股东权益合计': 'total_hldr_eqy_exc_min_int',
}
CASHFLOW_TS_MAP = {
'报告期': 'end_date',
'经营活动产生的现金流量净额': 'n_cashflow_act',
'投资活动产生的现金流量净额': 'n_cashflow_inv_act',
'筹资活动产生的现金流量净额': 'n_cash_flows_fnc_act',
}
def __init__(self):
self.data = None
self.info = None
@@ -89,57 +125,70 @@ class StockDataFetcher:
if basic_info:
info.update(basic_info)
# 方法1: 尝试获取个股详细信息(akshare
try:
stock_info = ak.stock_individual_info_em(symbol=symbol)
if stock_info is not None and not stock_info.empty:
for _, row in stock_info.iterrows():
key = row['item']
value = row['value']
if key == '股票简称':
info['name'] = value
elif key == '总市值':
try:
if value and value != '-':
info['market_cap'] = float(value)
except:
pass
elif key == '市盈率-动态':
try:
if value and value != '-':
pe_value = float(value)
if 0 < pe_value <= 1000:
info['pe_ratio'] = pe_value
except:
pass
elif key == '市净率':
try:
if value and value != '-':
pb_value = float(value)
if 0 < pb_value <= 100:
info['pb_ratio'] = pb_value
except:
pass
except Exception as e:
print(f"[Akshare] 获取个股详细信息失败: {e}")
# 如果akshare失败,尝试从tushare获取
if self.data_source_manager.tushare_available and info['name'] == '未知':
print(f"[Tushare] 尝试获取基本信息(tushare...")
# 方法1: 获取详细估值信息(优先tushare,失败时回退akshare
if (info.get('name') == '未知' or info.get('pe_ratio') == 'N/A' or
info.get('pb_ratio') == 'N/A' or info.get('market_cap') == 'N/A'):
# 优先使用tushare daily_basic(一次获取PE/PB/市值)
if self.data_source_manager.tushare_available:
try:
print(f"[Tushare] 正在获取 {symbol} 的估值信息(主要数据源)...")
ts_code = self.data_source_manager._convert_to_ts_code(symbol)
df = self.data_source_manager.tushare_api.daily_basic(
ts_code=ts_code,
trade_date=datetime.now().strftime('%Y%m%d')
start_date=(datetime.now() - timedelta(days=10)).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]
info['pe_ratio'] = row.get('pe', 'N/A')
info['pb_ratio'] = row.get('pb', 'N/A')
info['market_cap'] = row.get('total_mv', 'N/A')
print(f"[Tushare] ✅ 成功获取部分信息")
except Exception as te:
print(f"[Tushare] ❌ 获取失败: {te}")
if info.get('pe_ratio') == 'N/A' and 'pe' in df.columns:
info['pe_ratio'] = row.get('pe', 'N/A')
if info.get('pb_ratio') == 'N/A' and 'pb' in df.columns:
info['pb_ratio'] = row.get('pb', 'N/A')
if info.get('market_cap') == 'N/A' and 'total_mv' in df.columns:
info['market_cap'] = row.get('total_mv', 'N/A')
print(f"[Tushare] ✅ 成功获取估值信息")
else:
print(f"[Tushare] ❌ 未获取到估值信息,尝试备用数据源")
except Exception as e:
print(f"[Tushare] ❌ 获取估值信息失败: {e}")
# tushare未获取到时,回退akshare
if (info.get('name') == '未知' or info.get('pe_ratio') == 'N/A' or
info.get('pb_ratio') == 'N/A' or info.get('market_cap') == 'N/A'):
try:
print(f"[Akshare] 正在获取 {symbol} 的详细信息(备用数据源)...")
stock_info = ak.stock_individual_info_em(symbol=symbol)
if stock_info is not None and not stock_info.empty:
for _, row in stock_info.iterrows():
key = row['item']
value = row['value']
if key == '股票简称':
info['name'] = value
elif key == '总市值':
try:
if value and value != '-':
info['market_cap'] = float(value)
except:
pass
elif key == '市盈率-动态':
try:
if value and value != '-':
pe_value = float(value)
if 0 < pe_value <= 1000:
info['pe_ratio'] = pe_value
except:
pass
elif key == '市净率':
try:
if value and value != '-':
pb_value = float(value)
if 0 < pb_value <= 100:
info['pb_ratio'] = pb_value
except:
pass
except Exception as e:
print(f"[Akshare] 获取个股详细信息失败: {e}")
# 方法2: 尝试获取历史价格和涨跌幅(如果网络允许)
# try:
@@ -204,7 +253,10 @@ class StockDataFetcher:
# 方法3: 使用百度估值数据获取市盈率和市净率
if info['pe_ratio'] == 'N/A':
try:
pe_data = ak.stock_zh_valuation_baidu(symbol=symbol, indicator="市盈率(TTM)")
pe_data = call_with_timeout(
ak.stock_zh_valuation_baidu, timeout=15,
symbol=symbol, indicator="市盈率(TTM)"
)
if pe_data is not None and not pe_data.empty:
latest_pe = pe_data.iloc[-1]['value']
if latest_pe and latest_pe != '-':
@@ -216,7 +268,10 @@ class StockDataFetcher:
if info['pb_ratio'] == 'N/A':
try:
pb_data = ak.stock_zh_valuation_baidu(symbol=symbol, indicator="市净率")
pb_data = call_with_timeout(
ak.stock_zh_valuation_baidu, timeout=15,
symbol=symbol, indicator="市净率"
)
if pb_data is not None and not pb_data.empty:
latest_pb = pb_data.iloc[-1]['value']
if latest_pb and latest_pb != '-':
@@ -262,6 +317,32 @@ class StockDataFetcher:
"exchange": "香港交易所"
}
# 优先使用tusharehk_basic + hk_daily
if self.data_source_manager.tushare_available:
try:
print(f"[Tushare] 正在获取港股信息(主要数据源)...")
ts_code = f"{hk_code}.HK"
bdf = self.data_source_manager.tushare_api.hk_basic(ts_code=ts_code)
if bdf is not None and not bdf.empty and 'name' in bdf.columns:
info['name'] = bdf.iloc[0].get('name', '未知')
end = datetime.now().strftime('%Y%m%d')
start = (datetime.now() - timedelta(days=10)).strftime('%Y%m%d')
hdf = self.data_source_manager.tushare_api.hk_daily(
ts_code=ts_code, start_date=start, end_date=end
)
if hdf is not None and not hdf.empty:
latest = hdf.iloc[0]
info['current_price'] = latest.get('close', 'N/A')
info['change_percent'] = latest.get('pct_chg', 'N/A')
if info['current_price'] != 'N/A':
print(f"[Tushare] ✅ 成功获取港股信息")
return info
print(f"[Tushare] ❌ 未获取到港股行情,尝试备用数据源")
except Exception as e:
print(f"[Tushare] 获取港股信息失败: {e}")
# 方法1: 获取港股实时行情
try:
# 使用akshare获取港股实时数据
@@ -331,12 +412,7 @@ class StockDataFetcher:
def _get_us_stock_info(self, symbol):
"""获取美股基本信息"""
import time
try:
# 添加延迟避免频率限制
time.sleep(1)
ticker = yf.Ticker(symbol)
# 先尝试获取历史数据(通常更稳定)
@@ -487,7 +563,36 @@ class StockDataFetcher:
else:
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
# 获取港股历史数据
# 优先使用tusharehk_daily
if self.data_source_manager.tushare_available:
try:
print(f"[Tushare] 正在获取港股历史数据(主要数据源)...")
ts_code = f"{hk_code}.HK"
df = self.data_source_manager.tushare_api.hk_daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date
)
if df is not None and not df.empty:
df = df.rename(columns={
'trade_date': 'Date',
'open': 'Open',
'high': 'High',
'low': 'Low',
'close': 'Close',
'vol': 'Volume'
})
df['Date'] = pd.to_datetime(df['Date'])
df = df.sort_values('Date')
df.set_index('Date', inplace=True)
print(f"[Tushare] ✅ 成功获取港股历史数据")
return df
else:
print(f"[Tushare] ❌ 未获取到港股历史数据,尝试备用数据源")
except Exception as e:
print(f"[Tushare] 获取港股历史数据失败: {e}")
# 回退akshare
df = ak.stock_hk_hist(symbol=hk_code, period="daily",
start_date=start_date, end_date=end_date, adjust="qfq")
@@ -600,6 +705,112 @@ class StockDataFetcher:
except Exception as e:
return {"error": f"获取财务数据失败: {str(e)}"}
def _convert_ts_financial_records(self, df, field_map, limit=8):
"""将tushare财务表转换为统一的记录列表"""
try:
data_list = []
for _, row in df.head(limit).iterrows():
item = {}
for ch_name, ts_name in field_map.items():
if ts_name in df.columns:
value = row.get(ts_name)
if value is None or (isinstance(value, float) and pd.isna(value)):
continue
try:
item[ch_name] = str(value)
except:
item[ch_name] = "N/A"
if item:
data_list.append(item)
return data_list
except Exception as e:
print(f"转换tushare财务数据异常: {e}")
return None
def _convert_ts_ratios(self, df):
"""将tushare财务指标转换为AI使用的比率字典"""
try:
df = df.sort_values('end_date', ascending=False)
if df.empty:
return {}
row = df.iloc[0]
mapping = {
'净资产收益率(ROE)': 'roe',
'总资产报酬率(ROA)': 'roa',
'销售毛利率': 'grossprofit_margin',
'销售净利率': 'netprofit_margin',
'资产负债率': 'debt_to_assets',
'流动比率': 'current_ratio',
'速动比率': 'quick_ratio',
'存货周转率': 'inventory_turnover',
'应收账款周转率': 'ar_turnover',
'总资产周转率': 'assets_turnover',
'营业收入同比增长': 'or_yoy',
'净利润同比增长': 'netprofit_yoy',
'EPS': 'eps',
}
ratios = {'报告期': str(row.get('end_date', 'N/A'))}
for ch_name, ts_name in mapping.items():
if ts_name in df.columns:
value = row.get(ts_name)
if value is None or (isinstance(value, float) and pd.isna(value)):
ratios[ch_name] = "N/A"
else:
try:
ratios[ch_name] = str(value)
except:
ratios[ch_name] = "N/A"
return ratios
except Exception as e:
print(f"转换tushare财务指标异常: {e}")
return {}
def _get_financial_data_from_tushare(self, symbol):
"""优先从tushare获取财务三表与财务指标"""
try:
if not self.data_source_manager.tushare_available:
return None
ts_code = self.data_source_manager._convert_to_ts_code(symbol)
result = {}
# 利润表
df = self.data_source_manager.tushare_api.income(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.INCOME_TS_MAP)
if records:
result["income_statement"] = records
# 资产负债表
df = self.data_source_manager.tushare_api.balancesheet(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.BALANCE_TS_MAP)
if records:
result["balance_sheet"] = records
# 现金流量表
df = self.data_source_manager.tushare_api.cashflow(ts_code=ts_code)
if df is not None and not df.empty:
df = df.drop_duplicates(subset=['end_date']).sort_values('end_date', ascending=False)
records = self._convert_ts_financial_records(df, self.CASHFLOW_TS_MAP)
if records:
result["cash_flow"] = records
# 财务指标
df = self.data_source_manager.tushare_api.fina_indicator(ts_code=ts_code)
if df is not None and not df.empty:
ratios = self._convert_ts_ratios(df)
if ratios:
result["financial_ratios"] = ratios
if result:
print(f"[Tushare] ✅ 成功获取财务数据(主要数据源)")
return result or None
except Exception as e:
print(f"[Tushare] ❌ 获取财务数据失败: {e}")
return None
def _get_chinese_financial_data(self, symbol):
"""获取中国股票财务数据"""
financial_data = {
@@ -612,68 +823,77 @@ class StockDataFetcher:
}
try:
# 1. 获取资产负债表
try:
balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表")
if balance_sheet is not None and not balance_sheet.empty:
financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
except Exception as e:
print(f"获取资产负债表失败: {e}")
# 0. 优先使用tushare获取财务三表与财务指标
ts_financial = self._get_financial_data_from_tushare(symbol)
if ts_financial:
financial_data.update(ts_financial)
# 2. 获取利润表
try:
income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表")
if income_statement is not None and not income_statement.empty:
financial_data["income_statement"] = income_statement.head(8).to_dict('records')
except Exception as e:
print(f"获取利润表失败: {e}")
# 1. 获取资产负债表(tushare未获取到时回退akshare
if financial_data["balance_sheet"] is None:
try:
balance_sheet = ak.stock_financial_abstract_ths(symbol=symbol, indicator="资产负债表")
if balance_sheet is not None and not balance_sheet.empty:
financial_data["balance_sheet"] = balance_sheet.head(8).to_dict('records')
except Exception as e:
print(f"获取资产负债表失败: {e}")
# 3. 获取现金流量表
try:
cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表")
if cash_flow is not None and not cash_flow.empty:
financial_data["cash_flow"] = cash_flow.head(8).to_dict('records')
except Exception as e:
print(f"获取现金流量表失败: {e}")
# 2. 获取利润表(tushare未获取到时回退akshare
if financial_data["income_statement"] is None:
try:
income_statement = ak.stock_financial_abstract_ths(symbol=symbol, indicator="利润表")
if income_statement is not None and not income_statement.empty:
financial_data["income_statement"] = income_statement.head(8).to_dict('records')
except Exception as e:
print(f"获取利润表失败: {e}")
# 4. 获取主要财务指标
try:
financial_abstract = ak.stock_financial_abstract(symbol=symbol)
if financial_abstract is not None and not financial_abstract.empty:
# 提取关键财务指标
key_indicators = [
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售毛利率', '销售净利率',
'资产负债率', '流动比率', '速动比率', '存货周转率', '应收账款周转率',
'总资产周转率', '营业收入同比增长', '净利润同比增长'
]
# 筛选出包含关键指标的行
indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)]
if not indicator_rows.empty:
# 获取最新的报告期数据(第一列日期)
date_columns = [col for col in financial_abstract.columns if col not in ['选项', '指标']]
if date_columns:
latest_date = date_columns[0] # 最新日期列
# 构建财务比率字典
financial_ratios = {"报告期": latest_date}
# 提取每个指标的最新值
for _, row in indicator_rows.iterrows():
indicator_name = row['指标']
value = row.get(latest_date, 'N/A')
if value is not None and not (isinstance(value, float) and pd.isna(value)):
try:
financial_ratios[indicator_name] = str(value)
except:
# 3. 获取现金流量表(tushare未获取到时回退akshare
if financial_data["cash_flow"] is None:
try:
cash_flow = ak.stock_financial_abstract_ths(symbol=symbol, indicator="现金流量表")
if cash_flow is not None and not cash_flow.empty:
financial_data["cash_flow"] = cash_flow.head(8).to_dict('records')
except Exception as e:
print(f"获取现金流量表失败: {e}")
# 4. 获取主要财务指标(tushare未获取到时回退akshare
if not financial_data["financial_ratios"]:
try:
financial_abstract = ak.stock_financial_abstract(symbol=symbol)
if financial_abstract is not None and not financial_abstract.empty:
# 提取关键财务指标
key_indicators = [
'净资产收益率(ROE)', '总资产报酬率(ROA)', '销售毛利率', '销售净利率',
'资产负债率', '流动比率', '速动比率', '存货周转率', '应收账款周转率',
'总资产周转率', '营业收入同比增长', '净利润同比增长'
]
# 筛选出包含关键指标的行
indicator_rows = financial_abstract[financial_abstract['指标'].isin(key_indicators)]
if not indicator_rows.empty:
# 获取最新的报告期数据(第一列日期)
date_columns = [col for col in financial_abstract.columns if col not in ['选项', '指标']]
if date_columns:
latest_date = date_columns[0] # 最新日期列
# 构建财务比率字典
financial_ratios = {"报告期": latest_date}
# 提取每个指标的最新值
for _, row in indicator_rows.iterrows():
indicator_name = row['指标']
value = row.get(latest_date, 'N/A')
if value is not None and not (isinstance(value, float) and pd.isna(value)):
try:
financial_ratios[indicator_name] = str(value)
except:
financial_ratios[indicator_name] = "N/A"
else:
financial_ratios[indicator_name] = "N/A"
else:
financial_ratios[indicator_name] = "N/A"
financial_data["financial_ratios"] = financial_ratios
except Exception as e:
print(f"获取财务指标失败: {e}")
financial_data["financial_ratios"] = financial_ratios
except Exception as e:
print(f"获取财务指标失败: {e}")
# 注意:季报数据现在由 quarterly_report_data.py 模块使用 akshare 获取(8期完整季报)
# 不再使用问财获取季报,避免重复