Files
aiagents-stock/stock_data.py
T
2025-10-04 11:01:37 +08:00

548 lines
24 KiB
Python

import yfinance as yf
import akshare as ak
import pandas as pd
import numpy as np
import ta
from datetime import datetime, timedelta
import requests
import json
class StockDataFetcher:
"""股票数据获取类"""
def __init__(self):
self.data = None
self.info = None
self.financial_data = None
def get_stock_info(self, symbol):
"""获取股票基本信息"""
try:
# 处理中国股票代码
if self._is_chinese_stock(symbol):
return self._get_chinese_stock_info(symbol)
else:
return self._get_us_stock_info(symbol)
except Exception as e:
return {"error": f"获取股票信息失败: {str(e)}"}
def get_stock_data(self, symbol, period="1y", interval="1d"):
"""获取股票历史数据"""
try:
if self._is_chinese_stock(symbol):
return self._get_chinese_stock_data(symbol, period)
else:
return self._get_us_stock_data(symbol, period, interval)
except Exception as e:
return {"error": f"获取股票数据失败: {str(e)}"}
def _is_chinese_stock(self, symbol):
"""判断是否为中国股票"""
# 简单判断:包含数字且长度为6位的认为是中国股票
return symbol.isdigit() and len(symbol) == 6
def _get_chinese_stock_info(self, symbol):
"""获取中国股票基本信息"""
try:
# 初始化基本信息
info = {
"symbol": symbol,
"name": "未知",
"current_price": "N/A",
"change_percent": "N/A",
"pe_ratio": "N/A",
"pb_ratio": "N/A",
"market_cap": "N/A",
"market": "中国A股",
"exchange": "上海/深圳证券交易所"
}
# 方法1: 尝试获取个股详细信息
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"获取个股详细信息失败: {e}")
# 方法2: 尝试获取实时价格和涨跌幅(如果网络允许)
try:
# 使用更简单的接口获取实时价格
real_time_data = ak.stock_zh_a_spot_em()
if real_time_data is not None and not real_time_data.empty:
stock_real_time = real_time_data[real_time_data['代码'] == symbol]
if not stock_real_time.empty:
row = stock_real_time.iloc[0]
info['current_price'] = row.get('最新价', 'N/A')
info['change_percent'] = row.get('涨跌幅', 'N/A')
if info['name'] == '未知':
info['name'] = row.get('名称', '未知')
# 如果实时数据中有市盈率和市净率,优先使用
if '市盈率-动态' in row and info['pe_ratio'] == 'N/A':
try:
pe_val = row['市盈率-动态']
if pe_val and pe_val != '-':
pe_val = float(pe_val)
if 0 < pe_val <= 1000:
info['pe_ratio'] = pe_val
except:
pass
if '市净率' in row and info['pb_ratio'] == 'N/A':
try:
pb_val = row['市净率']
if pb_val and pb_val != '-':
pb_val = float(pb_val)
if 0 < pb_val <= 100:
info['pb_ratio'] = pb_val
except:
pass
except Exception as e:
print(f"获取实时数据失败: {e}")
# 如果实时数据获取失败,尝试使用历史数据获取价格
try:
hist_data = ak.stock_zh_a_hist(symbol=symbol, period="daily",
start_date=(datetime.now() - timedelta(days=5)).strftime('%Y%m%d'),
end_date=datetime.now().strftime('%Y%m%d'), adjust="qfq")
if hist_data is not None and not hist_data.empty:
latest = hist_data.iloc[-1]
info['current_price'] = latest['收盘']
# 计算涨跌幅
if len(hist_data) > 1:
prev_close = hist_data.iloc[-2]['收盘']
change_pct = ((latest['收盘'] - prev_close) / prev_close) * 100
info['change_percent'] = round(change_pct, 2)
except Exception as e2:
print(f"获取历史数据也失败: {e2}")
# 方法3: 使用百度估值数据获取市盈率和市净率
if info['pe_ratio'] == 'N/A':
try:
pe_data = ak.stock_zh_valuation_baidu(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 != '-':
pe_val = float(latest_pe)
if 0 < pe_val <= 1000:
info['pe_ratio'] = pe_val
except Exception as e:
print(f"获取市盈率失败: {e}")
if info['pb_ratio'] == 'N/A':
try:
pb_data = ak.stock_zh_valuation_baidu(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 != '-':
pb_val = float(latest_pb)
if 0 < pb_val <= 100:
info['pb_ratio'] = pb_val
except Exception as e:
print(f"获取市净率失败: {e}")
return info
except Exception as e:
print(f"获取中国股票信息完全失败: {e}")
# 返回基本信息,避免完全失败
return {
"symbol": symbol,
"name": f"股票{symbol}",
"current_price": "N/A",
"change_percent": "N/A",
"pe_ratio": "N/A",
"pb_ratio": "N/A",
"market_cap": "N/A",
"market": "中国A股",
"exchange": "上海/深圳证券交易所"
}
def _get_us_stock_info(self, symbol):
"""获取美股基本信息"""
import time
try:
# 添加延迟避免频率限制
time.sleep(1)
ticker = yf.Ticker(symbol)
# 先尝试获取历史数据(通常更稳定)
try:
hist = ticker.history(period="2d")
if not hist.empty:
current_price = hist['Close'].iloc[-1]
if len(hist) > 1:
prev_close = hist['Close'].iloc[-2]
change_percent = ((current_price - prev_close) / prev_close) * 100
else:
change_percent = 'N/A'
else:
current_price = 'N/A'
change_percent = 'N/A'
except:
current_price = 'N/A'
change_percent = 'N/A'
# 获取基本信息
try:
info = ticker.info
# 获取市盈率,优先使用trailing PE,其次forward PE
pe_ratio = info.get('trailingPE', info.get('forwardPE', 'N/A'))
if pe_ratio == 'N/A' or pe_ratio is None or (isinstance(pe_ratio, float) and np.isnan(pe_ratio)):
pe_ratio = 'N/A'
# 获取市净率
pb_ratio = info.get('priceToBook', 'N/A')
if pb_ratio == 'N/A' or pb_ratio is None or (isinstance(pb_ratio, float) and np.isnan(pb_ratio)):
pb_ratio = 'N/A'
# 如果历史数据没有获取到价格,尝试从info获取
if current_price == 'N/A':
current_price = info.get('currentPrice', info.get('regularMarketPrice', 'N/A'))
if change_percent == 'N/A':
change_percent = info.get('regularMarketChangePercent', 'N/A')
if change_percent != 'N/A' and change_percent is not None:
change_percent = change_percent * 100 # 转换为百分比
return {
"symbol": symbol,
"name": info.get('longName', info.get('shortName', 'N/A')),
"current_price": current_price,
"change_percent": change_percent,
"market_cap": info.get('marketCap', 'N/A'),
"pe_ratio": pe_ratio,
"pb_ratio": pb_ratio,
"dividend_yield": info.get('dividendYield', 'N/A'),
"beta": info.get('beta', 'N/A'),
"52_week_high": info.get('fiftyTwoWeekHigh', 'N/A'),
"52_week_low": info.get('fiftyTwoWeekLow', 'N/A'),
"sector": info.get('sector', 'N/A'),
"industry": info.get('industry', 'N/A'),
"market": "美股",
"exchange": info.get('exchange', 'N/A')
}
except Exception as e:
# 如果获取详细信息失败,返回基本价格信息
return {
"symbol": symbol,
"name": f"美股{symbol}",
"current_price": current_price,
"change_percent": change_percent,
"market_cap": 'N/A',
"pe_ratio": 'N/A',
"pb_ratio": 'N/A',
"dividend_yield": 'N/A',
"beta": 'N/A',
"52_week_high": 'N/A',
"52_week_low": 'N/A',
"sector": 'N/A',
"industry": 'N/A',
"market": "美股",
"exchange": 'N/A'
}
except Exception as e:
return {"error": f"获取美股信息失败: {str(e)}"}
def _get_chinese_stock_data(self, symbol, period="1y"):
"""获取中国股票历史数据"""
try:
# 计算日期范围
end_date = datetime.now().strftime('%Y%m%d')
if period == "1y":
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
elif period == "6mo":
start_date = (datetime.now() - timedelta(days=180)).strftime('%Y%m%d')
elif period == "3mo":
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
else:
start_date = (datetime.now() - timedelta(days=365)).strftime('%Y%m%d')
# 获取历史数据
df = ak.stock_zh_a_hist(symbol=symbol, period="daily",
start_date=start_date, end_date=end_date, adjust="qfq")
if df is not None and not df.empty:
# 重命名列以匹配标准格式
df = df.rename(columns={
'日期': 'Date',
'开盘': 'Open',
'收盘': 'Close',
'最高': 'High',
'最低': 'Low',
'成交量': 'Volume'
})
df['Date'] = pd.to_datetime(df['Date'])
df.set_index('Date', inplace=True)
return df
else:
return {"error": "无法获取历史数据"}
except Exception as e:
return {"error": f"获取中国股票数据失败: {str(e)}"}
def _get_us_stock_data(self, symbol, period="1y", interval="1d"):
"""获取美股历史数据"""
try:
ticker = yf.Ticker(symbol)
df = ticker.history(period=period, interval=interval)
if not df.empty:
return df
else:
return {"error": "无法获取历史数据"}
except Exception as e:
return {"error": f"获取美股数据失败: {str(e)}"}
def calculate_technical_indicators(self, df):
"""计算技术指标"""
try:
if isinstance(df, dict) and "error" in df:
return df
# 移动平均线
df['MA5'] = ta.trend.sma_indicator(df['Close'], window=5)
df['MA10'] = ta.trend.sma_indicator(df['Close'], window=10)
df['MA20'] = ta.trend.sma_indicator(df['Close'], window=20)
df['MA60'] = ta.trend.sma_indicator(df['Close'], window=60)
# RSI
df['RSI'] = ta.momentum.rsi(df['Close'], window=14)
# MACD
macd = ta.trend.MACD(df['Close'])
df['MACD'] = macd.macd()
df['MACD_signal'] = macd.macd_signal()
df['MACD_histogram'] = macd.macd_diff()
# 布林带
bollinger = ta.volatility.BollingerBands(df['Close'])
df['BB_upper'] = bollinger.bollinger_hband()
df['BB_middle'] = bollinger.bollinger_mavg()
df['BB_lower'] = bollinger.bollinger_lband()
# KDJ指标
df['K'] = ta.momentum.stoch(df['High'], df['Low'], df['Close'])
df['D'] = ta.momentum.stoch_signal(df['High'], df['Low'], df['Close'])
# 成交量指标
df['Volume_MA5'] = ta.trend.sma_indicator(df['Volume'], window=5)
df['Volume_ratio'] = df['Volume'] / df['Volume_MA5']
return df
except Exception as e:
return {"error": f"计算技术指标失败: {str(e)}"}
def get_latest_indicators(self, df):
"""获取最新的技术指标值"""
try:
if isinstance(df, dict) and "error" in df:
return df
latest = df.iloc[-1]
return {
"price": latest['Close'],
"ma5": latest['MA5'],
"ma10": latest['MA10'],
"ma20": latest['MA20'],
"ma60": latest['MA60'],
"rsi": latest['RSI'],
"macd": latest['MACD'],
"macd_signal": latest['MACD_signal'],
"bb_upper": latest['BB_upper'],
"bb_lower": latest['BB_lower'],
"k_value": latest['K'],
"d_value": latest['D'],
"volume_ratio": latest['Volume_ratio']
}
except Exception as e:
return {"error": f"获取最新指标失败: {str(e)}"}
def get_financial_data(self, symbol):
"""获取详细财务数据"""
try:
if self._is_chinese_stock(symbol):
return self._get_chinese_financial_data(symbol)
else:
return self._get_us_financial_data(symbol)
except Exception as e:
return {"error": f"获取财务数据失败: {str(e)}"}
def _get_chinese_financial_data(self, symbol):
"""获取中国股票财务数据"""
financial_data = {
"symbol": symbol,
"balance_sheet": None, # 资产负债表
"income_statement": None, # 利润表
"cash_flow": None, # 现金流量表
"financial_ratios": {}, # 财务比率
"quarter_data": None, # 季度数据
}
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}")
# 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}")
# 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}")
# 4. 获取主要财务指标
try:
financial_indicators = ak.stock_financial_analysis_indicator(symbol=symbol)
if financial_indicators is not None and not financial_indicators.empty:
latest_data = financial_indicators.iloc[0]
financial_data["financial_ratios"] = {
"报告期": latest_data.get('报告期', 'N/A'),
"净资产收益率ROE": latest_data.get('净资产收益率', 'N/A'),
"总资产收益率ROA": latest_data.get('总资产收益率', 'N/A'),
"销售毛利率": latest_data.get('销售毛利率', 'N/A'),
"销售净利率": latest_data.get('销售净利率', 'N/A'),
"资产负债率": latest_data.get('资产负债率', 'N/A'),
"流动比率": latest_data.get('流动比率', 'N/A'),
"速动比率": latest_data.get('速动比率', 'N/A'),
"存货周转率": latest_data.get('存货周转率', 'N/A'),
"应收账款周转率": latest_data.get('应收账款周转率', 'N/A'),
"总资产周转率": latest_data.get('总资产周转率', 'N/A'),
"营业收入同比增长": latest_data.get('营业收入同比增长', 'N/A'),
"净利润同比增长": latest_data.get('净利润同比增长', 'N/A'),
}
except Exception as e:
print(f"获取财务指标失败: {e}")
# 5. 获取季度业绩(尝试不同API)
try:
# 尝试获取业绩预告
quarter_data = ak.stock_profit_forecast_em(symbol=symbol)
if quarter_data is not None and not quarter_data.empty:
financial_data["quarter_data"] = quarter_data.head(4).to_dict('records')
except:
try:
# 备用方案:获取季度财报
quarter_data = ak.stock_financial_report_sina(stock=symbol, symbol="季报")
if quarter_data is not None and not quarter_data.empty:
financial_data["quarter_data"] = quarter_data.head(4).to_dict('records')
except Exception as e:
print(f"获取季度数据失败: {e}")
return financial_data
except Exception as e:
print(f"获取中国股票财务数据失败: {e}")
return financial_data
def _get_us_financial_data(self, symbol):
"""获取美股财务数据"""
financial_data = {
"symbol": symbol,
"balance_sheet": None,
"income_statement": None,
"cash_flow": None,
"financial_ratios": {},
"quarter_data": None,
}
try:
stock = yf.Ticker(symbol)
info = stock.info
# 1. 资产负债表
try:
balance_sheet = stock.balance_sheet
if balance_sheet is not None and not balance_sheet.empty:
financial_data["balance_sheet"] = balance_sheet.iloc[:, :4].to_dict('index')
except Exception as e:
print(f"获取资产负债表失败: {e}")
# 2. 利润表
try:
income_stmt = stock.income_stmt
if income_stmt is not None and not income_stmt.empty:
financial_data["income_statement"] = income_stmt.iloc[:, :4].to_dict('index')
except Exception as e:
print(f"获取利润表失败: {e}")
# 3. 现金流量表
try:
cash_flow = stock.cashflow
if cash_flow is not None and not cash_flow.empty:
financial_data["cash_flow"] = cash_flow.iloc[:, :4].to_dict('index')
except Exception as e:
print(f"获取现金流量表失败: {e}")
# 4. 财务比率(从info中提取)
financial_data["financial_ratios"] = {
"ROE": info.get('returnOnEquity', 'N/A'),
"ROA": info.get('returnOnAssets', 'N/A'),
"毛利率": info.get('grossMargins', 'N/A'),
"营业利润率": info.get('operatingMargins', 'N/A'),
"净利率": info.get('profitMargins', 'N/A'),
"资产负债率": info.get('debtToEquity', 'N/A'),
"流动比率": info.get('currentRatio', 'N/A'),
"速动比率": info.get('quickRatio', 'N/A'),
"EPS": info.get('trailingEps', 'N/A'),
"每股账面价值": info.get('bookValue', 'N/A'),
"股息率": info.get('dividendYield', 'N/A'),
"派息率": info.get('payoutRatio', 'N/A'),
"收入增长": info.get('revenueGrowth', 'N/A'),
"盈利增长": info.get('earningsGrowth', 'N/A'),
}
return financial_data
except Exception as e:
print(f"获取美股财务数据失败: {e}")
return financial_data