""" 基于数据库的股票服务 """ 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'] } }