- 重构数据访问层:引入DAO模式,支持MySQL/SQLite双数据库 - 新增数据库架构:完整的股票数据、AI分析、自选股管理表结构 - 升级AI分析服务:集成豆包大模型,支持多维度分析 - 优化API路由:分离市场数据API,提供更清晰的接口设计 - 完善项目文档:添加数据库迁移指南、新功能指南等 - 清理冗余文件:删除旧的缓存文件和无用配置 - 新增调度器:支持定时任务和数据自动更新 - 改进前端模板:简化的股票展示页面 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
603 lines
27 KiB
Python
603 lines
27 KiB
Python
"""
|
||
基于数据库的股票服务
|
||
"""
|
||
import pandas as pd
|
||
from datetime import datetime, date
|
||
from app import pro
|
||
from app.dao import StockDAO, WatchlistDAO, ConfigDAO
|
||
from app.config import Config
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class StockServiceDB:
|
||
def __init__(self):
|
||
self.stock_dao = StockDAO()
|
||
self.watchlist_dao = WatchlistDAO()
|
||
self.config_dao = ConfigDAO()
|
||
self.logger = logging.getLogger(__name__)
|
||
|
||
def get_stock_info(self, stock_code: str, force_refresh: bool = False):
|
||
"""获取股票信息"""
|
||
try:
|
||
today = self.stock_dao.get_today_date()
|
||
|
||
# 检查缓存
|
||
if not force_refresh:
|
||
cached_data = self.stock_dao.get_stock_data(stock_code, today)
|
||
if cached_data:
|
||
self.logger.info(f"从数据库获取股票 {stock_code} 的数据")
|
||
return self._format_stock_data(cached_data, self.watchlist_dao.get_watchlist_item(stock_code))
|
||
|
||
# 从API获取数据
|
||
self.logger.info(f"从API获取股票 {stock_code} 的数据...")
|
||
api_data = self._fetch_stock_from_api(stock_code)
|
||
if 'error' in api_data:
|
||
return api_data
|
||
|
||
# 保存到数据库
|
||
stock_info = api_data['stock_info']
|
||
success = self.stock_dao.save_stock_data(stock_code, stock_info, today)
|
||
if not success:
|
||
self.logger.warning(f"保存股票数据失败: {stock_code}")
|
||
|
||
# 获取目标值
|
||
targets = self.watchlist_dao.get_watchlist_item(stock_code)
|
||
if targets:
|
||
api_data['targets'] = {
|
||
"target_market_value": {
|
||
"min": targets['target_market_value_min'],
|
||
"max": targets['target_market_value_max']
|
||
}
|
||
}
|
||
|
||
return api_data
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取股票信息失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"获取股票数据失败: {str(e)}"}
|
||
|
||
def _fetch_stock_from_api(self, stock_code: str):
|
||
"""从API获取股票数据"""
|
||
try:
|
||
# 验证股票代码格式
|
||
if len(stock_code) != 6:
|
||
return {"error": "股票代码格式错误"}
|
||
|
||
# 确定交易所
|
||
if stock_code.startswith('6'):
|
||
ts_code = f"{stock_code}.SH"
|
||
elif stock_code.startswith(('0', '3')):
|
||
ts_code = f"{stock_code}.SZ"
|
||
else:
|
||
return {"error": "不支持的股票代码"}
|
||
|
||
# 获取基本信息
|
||
basic_info = pro.daily_basic(ts_code=ts_code, fields='ts_code,total_mv', limit=1)
|
||
if basic_info.empty:
|
||
return {"error": "股票代码不存在"}
|
||
|
||
# 获取股票名称
|
||
stock_name = pro.stock_basic(ts_code=ts_code, fields='name').iloc[0]['name']
|
||
|
||
# 确保股票信息在数据库中
|
||
market = 'SH' if stock_code.startswith('6') else 'SZ'
|
||
self.stock_dao.add_or_update_stock(stock_code, stock_name, market)
|
||
|
||
# 获取最新财务指标
|
||
fina_indicator = pro.fina_indicator(
|
||
ts_code=ts_code,
|
||
period=datetime.now().strftime('%Y%m%d'),
|
||
fields='roe,grossprofit_margin,netprofit_margin,debt_to_assets,op_income_yoy,netprofit_yoy,bps,ocfps'
|
||
)
|
||
if fina_indicator.empty:
|
||
fina_indicator = pro.fina_indicator(ts_code=ts_code, limit=1)
|
||
|
||
# 获取实时行情
|
||
today = datetime.now().strftime('%Y%m%d')
|
||
daily_data = pro.daily(ts_code=basic_info['ts_code'].iloc[0], start_date=today, end_date=today)
|
||
if daily_data.empty:
|
||
daily_data = pro.daily(ts_code=basic_info['ts_code'].iloc[0], limit=1)
|
||
if daily_data.empty:
|
||
return {"error": "无法获取股票行情数据"}
|
||
|
||
# 获取估值指标
|
||
daily_basic = pro.daily_basic(
|
||
ts_code=basic_info['ts_code'].iloc[0],
|
||
fields='ts_code,trade_date,pe,pe_ttm,pb,ps,dv_ratio',
|
||
limit=1
|
||
)
|
||
|
||
if daily_basic.empty:
|
||
return {"error": "无法获取股票基础数据"}
|
||
|
||
latest_basic = daily_basic.iloc[0]
|
||
latest_fina = fina_indicator.iloc[0] if not fina_indicator.empty else pd.Series()
|
||
|
||
# 计算市值
|
||
current_price = float(daily_data['close'].iloc[0])
|
||
market_value = float(basic_info['total_mv'].iloc[0]) / 10000
|
||
|
||
# 处理各种指标
|
||
dv_ratio = float(latest_basic['dv_ratio']) if pd.notna(latest_basic['dv_ratio']) else 0
|
||
dividend_yield = round(dv_ratio / 100, 4)
|
||
|
||
stock_info = {
|
||
"code": stock_code,
|
||
"name": stock_name,
|
||
"market_value": round(market_value, 2),
|
||
"pe_ratio": round(float(latest_basic['pe']), 2) if pd.notna(latest_basic['pe']) else 0,
|
||
"pe_ttm": round(float(latest_basic['pe_ttm']), 2) if pd.notna(latest_basic.get('pe_ttm')) else 0,
|
||
"pb_ratio": round(float(latest_basic['pb']), 2) if pd.notna(latest_basic['pb']) else 0,
|
||
"ps_ratio": round(float(latest_basic['ps']), 2) if pd.notna(latest_basic['ps']) else 0,
|
||
"dividend_yield": dividend_yield,
|
||
"price": round(current_price, 2),
|
||
"change_percent": round(float(daily_data['pct_chg'].iloc[0]) / 100, 4),
|
||
# 财务指标
|
||
"roe": round(float(latest_fina['roe']) / 100, 4) if pd.notna(latest_fina.get('roe')) else 0,
|
||
"gross_profit_margin": round(float(latest_fina['grossprofit_margin']) / 100, 4) if pd.notna(latest_fina.get('grossprofit_margin')) else 0,
|
||
"net_profit_margin": round(float(latest_fina['netprofit_margin']) / 100, 4) if pd.notna(latest_fina.get('netprofit_margin')) else 0,
|
||
"debt_to_assets": round(float(latest_fina['debt_to_assets']) / 100, 4) if pd.notna(latest_fina.get('debt_to_assets')) else 0,
|
||
"revenue_yoy": round(float(latest_fina['op_income_yoy']) / 100, 4) if pd.notna(latest_fina.get('op_income_yoy')) else 0,
|
||
"net_profit_yoy": round(float(latest_fina['netprofit_yoy']) / 100, 4) if pd.notna(latest_fina.get('netprofit_yoy')) else 0,
|
||
"bps": round(float(latest_fina['bps']), 3) if pd.notna(latest_fina.get('bps')) else 0,
|
||
"ocfps": round(float(latest_fina['ocfps']), 3) if pd.notna(latest_fina.get('ocfps')) else 0,
|
||
"from_cache": False
|
||
}
|
||
|
||
return {"stock_info": stock_info, "targets": {}}
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"从API获取股票数据失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"获取股票数据失败: {str(e)}"}
|
||
|
||
def _format_stock_data(self, stock_data: dict, watchlist_item: dict = None):
|
||
"""格式化股票数据为API返回格式"""
|
||
stock_info = {
|
||
"code": stock_data['stock_code'],
|
||
"name": stock_data['stock_name'],
|
||
"market_value": stock_data['market_value'],
|
||
"pe_ratio": stock_data['pe_ratio'],
|
||
"pb_ratio": stock_data['pb_ratio'],
|
||
"ps_ratio": stock_data['ps_ratio'],
|
||
"dividend_yield": stock_data['dividend_yield'],
|
||
"price": stock_data['price'],
|
||
"change_percent": stock_data['change_percent'],
|
||
"roe": stock_data['roe'],
|
||
"gross_profit_margin": stock_data['gross_profit_margin'],
|
||
"net_profit_margin": stock_data['net_profit_margin'],
|
||
"debt_to_assets": stock_data['debt_to_assets'],
|
||
"revenue_yoy": stock_data['revenue_yoy'],
|
||
"net_profit_yoy": stock_data['net_profit_yoy'],
|
||
"bps": stock_data['bps'],
|
||
"ocfps": stock_data['ocfps'],
|
||
"from_cache": stock_data['from_cache']
|
||
}
|
||
|
||
targets = {}
|
||
if watchlist_item:
|
||
targets = {
|
||
"target_market_value": {
|
||
"min": watchlist_item['target_market_value_min'],
|
||
"max": watchlist_item['target_market_value_max']
|
||
}
|
||
}
|
||
|
||
return {"stock_info": stock_info, "targets": targets}
|
||
|
||
def get_watchlist(self):
|
||
"""获取监控列表"""
|
||
try:
|
||
watchlist_data = self.watchlist_dao.get_watchlist_with_data()
|
||
result = []
|
||
|
||
for item in watchlist_data:
|
||
# 处理股票信息
|
||
stock_info = {
|
||
"code": item['stock_code'],
|
||
"name": item['stock_name']
|
||
}
|
||
|
||
# 如果有股票数据,添加更多信息
|
||
if item.get('price') is not None:
|
||
stock_info.update({
|
||
"price": float(item['price']) if item['price'] else None,
|
||
"change_percent": float(item['change_percent']) if item['change_percent'] else None,
|
||
"market_value": float(item['current_market_value']) if item['current_market_value'] else None,
|
||
"pe_ratio": float(item['pe_ratio']) if item['pe_ratio'] else None,
|
||
"pb_ratio": float(item['pb_ratio']) if item['pb_ratio'] else None,
|
||
"from_cache": bool(item.get('from_cache', False))
|
||
})
|
||
|
||
# 处理目标市值
|
||
targets = {}
|
||
if item.get('target_market_value_min') is not None or item.get('target_market_value_max') is not None:
|
||
targets["target_market_value"] = {
|
||
"min": item.get('target_market_value_min'),
|
||
"max": item.get('target_market_value_max')
|
||
}
|
||
|
||
result.append({
|
||
"stock_info": stock_info,
|
||
"targets": targets
|
||
})
|
||
|
||
return result
|
||
except Exception as e:
|
||
self.logger.error(f"获取监控列表失败: {e}")
|
||
return []
|
||
|
||
def add_watch(self, stock_code: str, target_market_value_min: float = None, target_market_value_max: float = None):
|
||
"""添加股票到监控列表"""
|
||
try:
|
||
success = self.watchlist_dao.add_to_watchlist(
|
||
stock_code, target_market_value_min, target_market_value_max
|
||
)
|
||
return {"status": "success" if success else "failed"}
|
||
except Exception as e:
|
||
self.logger.error(f"添加监控股票失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"添加监控股票失败: {str(e)}"}
|
||
|
||
def remove_watch(self, stock_code: str):
|
||
"""从监控列表移除股票"""
|
||
try:
|
||
success = self.watchlist_dao.remove_from_watchlist(stock_code)
|
||
return {"status": "success" if success else "failed"}
|
||
except Exception as e:
|
||
self.logger.error(f"移除监控股票失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"移除监控股票失败: {str(e)}"}
|
||
|
||
def update_target(self, stock_code: str, target_market_value_min: float = None, target_market_value_max: float = None):
|
||
"""更新股票的目标市值"""
|
||
try:
|
||
success = self.watchlist_dao.update_watchlist_item(
|
||
stock_code, target_market_value_min, target_market_value_max
|
||
)
|
||
return {"status": "success" if success else "failed"}
|
||
except Exception as e:
|
||
self.logger.error(f"更新目标市值失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"更新目标市值失败: {str(e)}"}
|
||
|
||
def get_index_info(self):
|
||
"""获取主要指数数据(此功能保持不变,不需要数据库存储)"""
|
||
try:
|
||
index_codes = {
|
||
'000001.SH': '上证指数',
|
||
'399001.SZ': '深证成指',
|
||
'399006.SZ': '创业板指',
|
||
'000016.SH': '上证50',
|
||
'000300.SH': '沪深300',
|
||
'000905.SH': '中证500',
|
||
'000852.SH': '中证1000',
|
||
'899050.BJ': '北证50',
|
||
}
|
||
|
||
result = []
|
||
for ts_code, name in index_codes.items():
|
||
try:
|
||
df = pro.index_daily(ts_code=ts_code, limit=1)
|
||
if not df.empty:
|
||
data = df.iloc[0]
|
||
# 获取K线数据(最近20天)
|
||
kline_df = pro.index_daily(ts_code=ts_code, limit=20)
|
||
kline_data = []
|
||
if not kline_df.empty:
|
||
for _, row in kline_df.iterrows():
|
||
kline_data.append({
|
||
'date': row['trade_date'],
|
||
'open': float(row['open']),
|
||
'close': float(row['close']),
|
||
'high': float(row['high']),
|
||
'low': float(row['low']),
|
||
'vol': float(row['vol'])
|
||
})
|
||
|
||
result.append({
|
||
'code': ts_code,
|
||
'name': name,
|
||
'price': float(data['close']),
|
||
'change': float(data['pct_chg']),
|
||
'kline_data': kline_data
|
||
})
|
||
except Exception as e:
|
||
self.logger.error(f"获取指数 {ts_code} 数据失败: {str(e)}")
|
||
continue
|
||
|
||
return result
|
||
except Exception as e:
|
||
self.logger.error(f"获取指数数据失败: {str(e)}")
|
||
return []
|
||
|
||
def batch_update_watchlist_data(self):
|
||
"""批量更新监控列表的股票数据"""
|
||
try:
|
||
# 获取需要更新的股票
|
||
stocks_to_update = self.watchlist_dao.get_stocks_needing_update()
|
||
updated_count = 0
|
||
failed_count = 0
|
||
|
||
for stock_code in stocks_to_update:
|
||
try:
|
||
result = self.get_stock_info(stock_code, force_refresh=True)
|
||
if 'error' not in result:
|
||
updated_count += 1
|
||
else:
|
||
failed_count += 1
|
||
except Exception as e:
|
||
self.logger.error(f"更新股票数据失败: {stock_code}, 错误: {e}")
|
||
failed_count += 1
|
||
|
||
# 更新最后更新日期
|
||
self.config_dao.set_last_data_update_date(self.stock_dao.get_today_date())
|
||
|
||
return {
|
||
"total": len(stocks_to_update),
|
||
"updated": updated_count,
|
||
"failed": failed_count
|
||
}
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"批量更新监控列表数据失败: {e}")
|
||
return {"error": f"批量更新失败: {str(e)}"}
|
||
|
||
# 保持原有的其他方法不变,这些方法不需要数据库存储
|
||
def get_company_detail(self, stock_code: str):
|
||
"""获取公司详情(从API获取,实时数据)"""
|
||
try:
|
||
# 处理股票代码格式
|
||
if stock_code.startswith('6'):
|
||
ts_code = f"{stock_code}.SH"
|
||
elif stock_code.startswith(('0', '3')):
|
||
ts_code = f"{stock_code}.SZ"
|
||
else:
|
||
return {"error": "不支持的股票代码"}
|
||
|
||
# 获取公司基本信息
|
||
basic = pro.stock_basic(ts_code=ts_code, fields='name,industry,area,list_date')
|
||
if basic.empty:
|
||
return {"error": "无法获取公司信息"}
|
||
|
||
company_info = basic.iloc[0]
|
||
|
||
# 获取公司详细信息
|
||
try:
|
||
company_detail = pro.stock_company(ts_code=ts_code)
|
||
if not company_detail.empty:
|
||
detail_info = company_detail.iloc[0]
|
||
company_detail_dict = {
|
||
"com_name": str(detail_info.get('com_name', '')),
|
||
"chairman": str(detail_info.get('chairman', '')),
|
||
"manager": str(detail_info.get('manager', '')),
|
||
"secretary": str(detail_info.get('secretary', '')),
|
||
"reg_capital": float(detail_info.get('reg_capital', 0)) if pd.notna(detail_info.get('reg_capital')) else 0,
|
||
"setup_date": str(detail_info.get('setup_date', '')),
|
||
"province": str(detail_info.get('province', '')),
|
||
"city": str(detail_info.get('city', '')),
|
||
"introduction": str(detail_info.get('introduction', '')),
|
||
"website": f"http://{str(detail_info.get('website', '')).strip('http://').strip('https://')}" if detail_info.get('website') else "",
|
||
"email": str(detail_info.get('email', '')),
|
||
"office": str(detail_info.get('office', '')),
|
||
"employees": int(detail_info.get('employees', 0)) if pd.notna(detail_info.get('employees')) else 0,
|
||
"main_business": str(detail_info.get('main_business', '')),
|
||
"business_scope": str(detail_info.get('business_scope', ''))
|
||
}
|
||
else:
|
||
company_detail_dict = {
|
||
"com_name": "", "chairman": "", "manager": "", "secretary": "",
|
||
"reg_capital": 0, "setup_date": "", "province": "", "city": "",
|
||
"introduction": "", "website": "", "email": "", "office": "",
|
||
"employees": 0, "main_business": "", "business_scope": ""
|
||
}
|
||
except Exception as e:
|
||
self.logger.error(f"获取公司详细信息失败: {str(e)}")
|
||
company_detail_dict = {
|
||
"com_name": "", "chairman": "", "manager": "", "secretary": "",
|
||
"reg_capital": 0, "setup_date": "", "province": "", "city": "",
|
||
"introduction": "", "website": "", "email": "", "office": "",
|
||
"employees": 0, "main_business": "", "business_scope": ""
|
||
}
|
||
|
||
# 获取最新财务指标
|
||
try:
|
||
fina = pro.fina_indicator(ts_code=ts_code, period=datetime.now().strftime('%Y%m%d'))
|
||
if fina.empty:
|
||
fina = pro.fina_indicator(ts_code=ts_code, limit=1)
|
||
|
||
if fina.empty:
|
||
return {"error": "无法获取财务数据"}
|
||
|
||
fina_info = fina.iloc[0]
|
||
except Exception as e:
|
||
self.logger.error(f"获取财务指标失败: {str(e)}")
|
||
return {"error": "获取财务指标失败"}
|
||
|
||
# 获取市值信息(用于PE、PB等指标)
|
||
try:
|
||
daily_basic = pro.daily_basic(ts_code=ts_code, fields='pe,pb,ps,dv_ratio', limit=1)
|
||
if not daily_basic.empty:
|
||
latest_basic = daily_basic.iloc[0]
|
||
else:
|
||
latest_basic = pd.Series({'pe': 0, 'pb': 0, 'ps': 0, 'dv_ratio': 0})
|
||
except Exception as e:
|
||
self.logger.error(f"获取PE/PB失败: {str(e)}")
|
||
latest_basic = pd.Series({'pe': 0, 'pb': 0, 'ps': 0, 'dv_ratio': 0})
|
||
|
||
result = {
|
||
"basic_info": {
|
||
"name": str(company_info['name']),
|
||
"industry": str(company_info['industry']),
|
||
"list_date": str(company_info['list_date']),
|
||
"area": str(company_info['area']),
|
||
**company_detail_dict
|
||
},
|
||
"financial_info": {
|
||
# 估值指标
|
||
"pe_ratio": float(latest_basic['pe']) if pd.notna(latest_basic['pe']) else 0,
|
||
"pb_ratio": float(latest_basic['pb']) if pd.notna(latest_basic['pb']) else 0,
|
||
"ps_ratio": float(latest_basic['ps']) if pd.notna(latest_basic['ps']) else 0,
|
||
"dividend_yield": float(latest_basic['dv_ratio'])/100 if pd.notna(latest_basic['dv_ratio']) else 0,
|
||
|
||
# 盈利能力
|
||
"roe": float(fina_info['roe']) if pd.notna(fina_info.get('roe')) else 0,
|
||
"grossprofit_margin": float(fina_info['grossprofit_margin']) if pd.notna(fina_info.get('grossprofit_margin')) else 0,
|
||
"netprofit_margin": float(fina_info['netprofit_margin']) if pd.notna(fina_info.get('netprofit_margin')) else 0,
|
||
|
||
# 成长能力
|
||
"netprofit_yoy": float(fina_info['netprofit_yoy']) if pd.notna(fina_info.get('netprofit_yoy')) else 0,
|
||
"or_yoy": float(fina_info['or_yoy']) if pd.notna(fina_info.get('or_yoy')) else 0,
|
||
|
||
# 偿债能力
|
||
"debt_to_assets": float(fina_info['debt_to_assets']) if pd.notna(fina_info.get('debt_to_assets')) else 0,
|
||
|
||
# 每股指标
|
||
"eps": float(fina_info['eps']) if pd.notna(fina_info.get('eps')) else 0,
|
||
"bps": float(fina_info['bps']) if pd.notna(fina_info.get('bps')) else 0,
|
||
"ocfps": float(fina_info['ocfps']) if pd.notna(fina_info.get('ocfps')) else 0,
|
||
}
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取公司详情失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"获取公司详情失败: {str(e)}"}
|
||
|
||
def get_top_holders(self, stock_code: str):
|
||
"""获取前十大股东数据(从API获取,实时数据)"""
|
||
try:
|
||
# 处理股票代码格式
|
||
if stock_code.startswith('6'):
|
||
ts_code = f"{stock_code}.SH"
|
||
elif stock_code.startswith(('0', '3')):
|
||
ts_code = f"{stock_code}.SZ"
|
||
else:
|
||
return {"error": "不支持的股票代码"}
|
||
|
||
# 获取最新一期的股东数据
|
||
df = pro.top10_holders(ts_code=ts_code, limit=10)
|
||
if df.empty:
|
||
return {"error": "暂无股东数据"}
|
||
|
||
# 按持股比例降序排序
|
||
df = df.sort_values('hold_ratio', ascending=False)
|
||
|
||
# 获取最新的报告期
|
||
latest_end_date = df['end_date'].max()
|
||
latest_data = df[df['end_date'] == latest_end_date]
|
||
|
||
holders = []
|
||
for _, row in latest_data.iterrows():
|
||
holders.append({
|
||
"holder_name": str(row['holder_name']),
|
||
"hold_amount": float(row['hold_amount']) if pd.notna(row['hold_amount']) else 0,
|
||
"hold_ratio": float(row['hold_ratio']) if pd.notna(row['hold_ratio']) else 0,
|
||
"hold_change": float(row['hold_change']) if pd.notna(row['hold_change']) else 0,
|
||
"ann_date": str(row['ann_date']),
|
||
"end_date": str(row['end_date'])
|
||
})
|
||
|
||
result = {
|
||
"holders": holders,
|
||
"total_ratio": sum(holder['hold_ratio'] for holder in holders),
|
||
"report_date": str(latest_end_date)
|
||
}
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取股东数据失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"获取股东数据失败: {str(e)}"}
|
||
|
||
def get_value_analysis_data(self, stock_code: str):
|
||
"""获取价值投资分析数据(优先从数据库,如果没有则从API获取)"""
|
||
try:
|
||
# 先尝试从数据库获取今日数据
|
||
today = self.stock_dao.get_today_date()
|
||
cached_data = self.stock_dao.get_stock_data(stock_code, today)
|
||
|
||
if cached_data and not cached_data['from_cache']:
|
||
# 如果有今日的API数据(非缓存),直接使用
|
||
return self._format_value_analysis_data(cached_data)
|
||
|
||
# 否则从API获取
|
||
api_result = self.get_stock_info(stock_code, force_refresh=True)
|
||
if 'error' in api_result:
|
||
return api_result
|
||
|
||
stock_info = api_result['stock_info']
|
||
return self._format_value_analysis_data_from_info(stock_info)
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"获取价值投资分析数据失败: {stock_code}, 错误: {e}")
|
||
return {"error": f"获取价值投资分析数据失败: {str(e)}"}
|
||
|
||
def _format_value_analysis_data(self, stock_data: dict):
|
||
"""格式化价值投资分析数据"""
|
||
return {
|
||
"stock_info": {
|
||
"code": stock_data['stock_code'],
|
||
"name": stock_data['stock_name'],
|
||
"current_price": stock_data['price'],
|
||
"trade_date": stock_data.get('data_date', self.stock_dao.get_today_date())
|
||
},
|
||
"valuation": {
|
||
"pe_ratio": stock_data['pe_ratio'],
|
||
"pb_ratio": stock_data['pb_ratio'],
|
||
"ps_ratio": stock_data['ps_ratio'],
|
||
"dividend_yield": stock_data['dividend_yield'],
|
||
"total_market_value": stock_data['market_value']
|
||
},
|
||
"profitability": {
|
||
"roe": stock_data['roe'],
|
||
"gross_margin": stock_data['gross_profit_margin'],
|
||
"net_margin": stock_data['net_profit_margin']
|
||
},
|
||
"growth": {
|
||
"net_profit_growth": stock_data['net_profit_yoy'],
|
||
"revenue_growth": stock_data['revenue_yoy']
|
||
},
|
||
"solvency": {
|
||
"debt_to_assets": stock_data['debt_to_assets']
|
||
},
|
||
"per_share": {
|
||
"eps": stock_data.get('eps', 0), # 这个字段在基础数据中没有,需要计算
|
||
"bps": stock_data['bps'],
|
||
"ocfps": stock_data['ocfps']
|
||
}
|
||
}
|
||
|
||
def _format_value_analysis_data_from_info(self, stock_info: dict):
|
||
"""从股票信息格式化价值投资分析数据"""
|
||
return {
|
||
"stock_info": {
|
||
"code": stock_info['code'],
|
||
"name": stock_info['name'],
|
||
"current_price": stock_info['price'],
|
||
"trade_date": self.stock_dao.get_today_date()
|
||
},
|
||
"valuation": {
|
||
"pe_ratio": stock_info['pe_ratio'],
|
||
"pb_ratio": stock_info['pb_ratio'],
|
||
"ps_ratio": stock_info['ps_ratio'],
|
||
"dividend_yield": stock_info['dividend_yield'],
|
||
"total_market_value": stock_info['market_value']
|
||
},
|
||
"profitability": {
|
||
"roe": stock_info['roe'],
|
||
"gross_margin": stock_info['gross_profit_margin'],
|
||
"net_margin": stock_info['net_profit_margin']
|
||
},
|
||
"growth": {
|
||
"net_profit_growth": stock_info['net_profit_yoy'],
|
||
"revenue_growth": stock_info['revenue_yoy']
|
||
},
|
||
"solvency": {
|
||
"debt_to_assets": stock_info['debt_to_assets']
|
||
},
|
||
"per_share": {
|
||
"eps": stock_info.get('eps', 0),
|
||
"bps": stock_info['bps'],
|
||
"ocfps": stock_info['ocfps']
|
||
}
|
||
} |