This commit is contained in:
liao 2025-04-02 17:38:26 +08:00
parent ed2758532e
commit ae9b2f2fcf
2 changed files with 267 additions and 158 deletions

View File

@ -1,6 +1,8 @@
import sys
import os
from src.fundamentals_llm.fundamental_analysis_database import get_analysis_result, get_db
# 添加项目根目录到 Python 路径
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
@ -25,6 +27,9 @@ CORS(app) # 启用跨域请求支持
# 创建企业筛选器实例
screener = EnterpriseScreener()
# 获取数据库连接
db = next(get_db())
@app.route('/api/health', methods=['GET'])
def health_check():
"""健康检查接口"""
@ -752,9 +757,25 @@ def comprehensive_analysis():
# 筛选出传入列表中符合条件的股票
for code, name in all_stocks:
if code in input_stock_codes:
# 获取各个维度的分析结果
investment_advice_result = get_analysis_result(db, code, "investment_advice")
industry_competition_result = get_analysis_result(db, code, "industry_competition")
financial_report_result = get_analysis_result(db, code, "financial_report")
valuation_level_result = get_analysis_result(db, code, "valuation_level")
# 从ai_response和extra_info中提取所需的值
investment_advice = investment_advice_result.ai_response if investment_advice_result else None
industry_space = industry_competition_result.extra_info.get("industry_space") if industry_competition_result else 0
financial_report_level = financial_report_result.extra_info.get("financial_report_level") if financial_report_result else 0
pe_industry = valuation_level_result.extra_info.get("pe_industry") if valuation_level_result else 0
filtered_stocks.append({
"code": code,
"name": name
"name": name,
"investment_advice": investment_advice, # 投资建议从ai_response获取
"industry_space": industry_space, # 行业发展空间2:高速增长, 1:稳定经营, 0:不确定性大, -1:不利经营)
"financial_report_level": financial_report_level, # 经营质量2:优秀, 1:较好, 0:一般, -1:存在隐患,-2较大隐患
"pe_industry": pe_industry # 个股在行业的PE水平-1:高于行业, 0:接近行业, 1:低于行业)
})
logger.info(f"筛选出 {len(filtered_stocks)} 个符合条件的股票")

View File

@ -66,7 +66,7 @@ class FundamentalAnalyzer:
# 使用联网模型进行基本面分析
self.chat_bot = ChatBot(model_type="online_bot")
# 使用离线模型进行其他分析
self.offline_bot = OfflineChatBot(platform="volc", model_type="offline_model")
self.offline_bot = OfflineChatBot(platform="tl_private", model_type="ds-v1")
# 千问打杂
self.offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="qwq")
self.db = next(get_db())
@ -366,25 +366,25 @@ class FundamentalAnalyzer:
try:
prompt = f"""请对{stock_name}({stock_code})的财报情况进行简要分析严格要求最新财报情况200字以内最新业绩预告情况100字以内近三年变化趋势150字以内请严格按照以下格式输出
1. 最新财报情况
- 营业收入及同比变化
- 主要成本构成及变化
- 净利润及同比变化
- 毛利率和净利率变化
- 其他重要财务指标如ROE资产负债率等
2. 最新业绩预告情况没有就不要提
- 预告类型预增/预减/扭亏/续亏
- 预计业绩区间
- 变动原因
3. 近三年变化趋势
- 收入增长趋势
- 利润变化趋势
- 盈利能力变化
- 经营质量变化
请提供专业客观的分析突出关键信息避免冗长描述"""
1. 最新财报情况
- 营业收入及同比变化
- 主要成本构成及变化
- 净利润及同比变化
- 毛利率和净利率变化
- 其他重要财务指标如ROE资产负债率等
2. 最新业绩预告情况没有就不要提
- 预告类型预增/预减/扭亏/续亏
- 预计业绩区间
- 变动原因
3. 近三年变化趋势
- 收入增长趋势
- 利润变化趋势
- 盈利能力变化
- 经营质量变化
请提供专业客观的分析突出关键信息避免冗长描述"""
# 获取AI分析结果
result = self.chat_bot.chat(prompt)
@ -425,16 +425,16 @@ class FundamentalAnalyzer:
# 使用在线模型分析财报水平
prompt = f"""请对{stock_name}({stock_code})的财报水平进行专业分析,并返回对应的数值评级:
- 如果财报水平边际向好最新财报没有任何风险返回数值"2"
- 如果边际变化波动不高较为稳定并且风险很小返回数值"1"
- 如果波动不高较为稳定但其中存在一定财报隐患返回数值"0"
- 如果财报波动较大亏损或者盈利并且存在一定财报隐患返回数值"-1"
- 如果财报波动较大亏损或者盈利并且存在较大财报隐患返回数值"-2"
财报分析内容
{report_text}
请仅返回一个数值210-1-2不要包含任何解释或说明"""
- 如果财报水平边际向好最新财报没有任何风险返回数值"2"
- 如果边际变化波动不高较为稳定并且风险很小返回数值"1"
- 如果波动不高较为稳定但其中存在一定财报隐患返回数值"0"
- 如果财报波动较大亏损或者盈利并且存在一定财报隐患返回数值"-1"
- 如果财报波动较大亏损或者盈利并且存在较大财报隐患返回数值"-2"
财报分析内容
{report_text}
请仅返回一个数值210-1-2不要包含任何解释或说明"""
# 使用在线模型进行分析
response = self.chat_bot.chat(prompt)
@ -484,26 +484,26 @@ class FundamentalAnalyzer:
try:
prompt = f"""请对{stock_name}({stock_code})所在行业的发展趋势和竞争格局进行简要分析要求输出控制在400字以内请严格按照以下格式输出
1. 市场需求
- 主要下游应用领域
- 需求增长驱动因素
- 市场规模和增速
2. 竞争格局
- 主要竞争对手及特点
- 行业集中度
- 竞争壁垒
3. 行业环境
- 行业平均利润率
- 政策环境影响
- 技术发展趋势
- 市场阶段结论新兴市场成熟市场衰退市场
4. 小结
- 简要说明当前市场是否有利于企业经营
请提供专业客观的分析突出关键信息避免冗长描述"""
1. 市场需求
- 主要下游应用领域
- 需求增长驱动因素
- 市场规模和增速
2. 竞争格局
- 主要竞争对手及特点
- 行业集中度
- 竞争壁垒
3. 行业环境
- 行业平均利润率
- 政策环境影响
- 技术发展趋势
- 市场阶段结论新兴市场成熟市场衰退市场
4. 小结
- 简要说明当前市场是否有利于企业经营
请提供专业客观的分析突出关键信息避免冗长描述"""
# 获取AI分析结果
result = self.chat_bot.chat(prompt)
@ -543,15 +543,15 @@ class FundamentalAnalyzer:
try:
# 使用离线模型分析行业发展空间
prompt = f"""请分析以下{stock_name}({stock_code})的行业发展趋势和竞争格局文本,评估当前市场环境、阶段和竞争格局对企业未来的影响,并返回对应的数值:
- 如果当前市场环境阶段和竞争格局符合未来企业高速增长返回数值"2"
- 如果当前市场环境阶段和竞争格局符合未来企业稳定经营返回数值"1"
- 如果当前市场环境阶段和竞争格局存在较大不确定性返回数值"0"
- 如果当前市场环境阶段和竞争格局不利于企业正常经营返回数值"-1"
行业发展趋势和竞争格局文本
{industry_text}
请仅返回一个数值210-1不要包含任何解释或说明"""
- 如果当前市场环境阶段和竞争格局符合未来企业高速增长返回数值"2"
- 如果当前市场环境阶段和竞争格局符合未来企业稳定经营返回数值"1"
- 如果当前市场环境阶段和竞争格局存在较大不确定性返回数值"0"
- 如果当前市场环境阶段和竞争格局不利于企业正常经营返回数值"-1"
行业发展趋势和竞争格局文本
{industry_text}
请仅返回一个数值210-1不要包含任何解释或说明"""
self.offline_bot_tl_qw.clear_history()
# 使用离线模型进行分析
space_value_str = self.offline_bot_tl_qw.chat(prompt)
@ -702,17 +702,17 @@ class FundamentalAnalyzer:
try:
prompt = f"""请对{stock_name}({stock_code})的股吧讨论内容进行简要分析要求输出控制在300字以内请严格按照以下格式输出
1. 主要讨论话题150字左右
- 近期热点事件
- 投资者关注焦点
- 市场情绪倾向
2. 重要信息汇总150字左右
- 公司相关动态
- 行业政策变化
- 市场预期变化
请提供专业客观的分析突出关键信息避免冗长描述重点关注投资者普遍关注的话题和重要市场信息"""
1. 主要讨论话题150字左右
- 近期热点事件
- 投资者关注焦点
- 市场情绪倾向
2. 重要信息汇总150字左右
- 公司相关动态
- 行业政策变化
- 市场预期变化
请提供专业客观的分析突出关键信息避免冗长描述重点关注投资者普遍关注的话题和重要市场信息"""
# 获取AI分析结果
result = self.chat_bot.chat(prompt)
@ -752,14 +752,14 @@ class FundamentalAnalyzer:
try:
# 使用离线模型分析市场情绪
prompt = f"""请分析以下{stock_name}({stock_code})的股吧讨论内容分析,判断整体市场情绪倾向,并返回对应的数值:
- 如果股吧讨论情绪偏乐观返回数值"1"
- 如果股吧讨论情绪偏中性返回数值"0"
- 如果股吧讨论情绪偏悲观返回数值"-1"
股吧讨论内容分析
{discussion_text}
请仅返回一个数值10-1不要包含任何解释或说明"""
- 如果股吧讨论情绪偏乐观返回数值"1"
- 如果股吧讨论情绪偏中性返回数值"0"
- 如果股吧讨论情绪偏悲观返回数值"-1"
股吧讨论内容分析
{discussion_text}
请仅返回一个数值10-1不要包含任何解释或说明"""
self.offline_bot_tl_qw.clear_history()
# 使用离线模型进行分析
emotion_value_str = self.offline_bot_tl_qw.chat(prompt)
@ -807,17 +807,17 @@ class FundamentalAnalyzer:
try:
prompt = f"""请对{stock_name}({stock_code})最近半年内的产业链上下游合作动态进行简要分析要求输出控制在400字以内请严格按照以下格式输出
1. 重要客户合作200字左右
- 主要客户合作进展
- 产品供应情况
- 合作深度和规模
2. 产业链布局200字左右
- 上下游合作动态
- 新业务领域拓展
- 战略合作项目
请提供专业客观的分析突出关键信息避免冗长描述重点关注最近半年内的合作动态如果没有相关动态请直接说明"""
1. 重要客户合作200字左右
- 主要客户合作进展
- 产品供应情况
- 合作深度和规模
2. 产业链布局200字左右
- 上下游合作动态
- 新业务领域拓展
- 战略合作项目
请提供专业客观的分析突出关键信息避免冗长描述重点关注最近半年内的合作动态如果没有相关动态请直接说明"""
# 获取AI分析结果
result = self.chat_bot.chat(prompt)
@ -858,15 +858,15 @@ class FundamentalAnalyzer:
# 使用在线模型分析合作动态质量
prompt = f"""请评估{stock_name}({stock_code})的产业链上下游合作动态质量,并返回相应数值:
- 如果企业近期有较多且质量高的新合作动态具备新业务拓展能力以及可以体现在近一年财报中返回数值"2"
- 如果企业半年内合作动态频率低或质量一般在原有业务上合作关系的衍生对财报影响一般返回数值"1"
- 如果企业没有合作动态或质量低返回数值"0"
- 如果企业有负面合作关系解除合作或业务被其他厂商瓜分返回数值"-1"
以下是合作动态相关信息
{cooperation_text}
请仅返回一个数值210-1不要包含任何解释或说明"""
- 如果企业近期有较多且质量高的新合作动态具备新业务拓展能力以及可以体现在近一年财报中返回数值"2"
- 如果企业半年内合作动态频率低或质量一般在原有业务上合作关系的衍生对财报影响一般返回数值"1"
- 如果企业没有合作动态或质量低返回数值"0"
- 如果企业有负面合作关系解除合作或业务被其他厂商瓜分返回数值"-1"
以下是合作动态相关信息
{cooperation_text}
请仅返回一个数值210-1不要包含任何解释或说明"""
# 使用在线模型进行分析
response = self.chat_bot.chat(prompt)
@ -1084,15 +1084,13 @@ class FundamentalAnalyzer:
prompt = f"""请对{stock_name}({stock_code})的估值水平进行简要分析要求输出控制在300字以内请严格按照以下格式输出
1. 历史估值水平150字左右
- 当前PE和PB值
- PE在历史分位水平的位置高于/接近/低于历史平均分位
- PB在历史分位水平的位置高于/接近/低于历史平均分位
- 当前PE值及其在历史分位水平的位置高于/接近/低于历史平均分位
- 当前PB值及其在历史分位水平的位置高于/接近/低于历史平均分位
- 历史估值变化趋势简要分析
2. 行业估值对比150字左右
- 所在行业平均PE和PB
- PE与行业平均的比较高于/接近/低于行业平均
- PB与行业平均的比较高于/接近/低于行业平均
- 当前PE值与行业平均水平的比较高于/接近/低于行业平均
- 当前PB值与行业平均水平的比较高于/接近/低于行业平均
- 与可比公司估值的简要对比
请提供专业客观的分析突出关键信息避免冗长描述如果无法获取某项数据请直接说明"""
@ -1130,14 +1128,18 @@ class FundamentalAnalyzer:
stock_name: 股票名称
Returns:
Dict[str, int]: 包含历史估值和行业估值分类的字典
Dict[str, int]: 包含四个分类值的字典
- pe_historical: PE历史分位分类 (-1:高于历史, 0:接近历史, 1:低于历史)
- pb_historical: PB历史分位分类 (-1:高于历史, 0:接近历史, 1:低于历史)
- pe_industry: PE行业对比分类 (-1:高于行业, 0:接近行业, 1:低于行业)
- pb_industry: PB行业对比分类 (-1:高于行业, 0:接近行业, 1:低于行业)
"""
try:
# 提取历史估值分类
historical_classification = self._extract_historical_valuation(valuation_text)
# 提取行业估值分类
industry_classification = self._extract_industry_valuation(valuation_text)
# 直接提取四个分类值
pe_historical = self._extract_pe_historical(valuation_text)
pb_historical = self._extract_pb_historical(valuation_text)
pe_industry = self._extract_pe_industry(valuation_text)
pb_industry = self._extract_pb_industry(valuation_text)
# 更新数据库中的记录
result = get_analysis_result(self.db, stock_code, "valuation_level")
@ -1150,103 +1152,189 @@ class FundamentalAnalyzer:
reasoning_process=result.reasoning_process,
references=result.references,
extra_info={
"historical_valuation": historical_classification,
"industry_valuation": industry_classification
"pe_historical": pe_historical,
"pb_historical": pb_historical,
"pe_industry": pe_industry,
"pb_industry": pb_industry
}
)
logger.info(f"已更新估值分类到数据库: historical_valuation={historical_classification}, industry_valuation={industry_classification}")
logger.info(f"已更新估值分类到数据库: pe_historical={pe_historical}, pb_historical={pb_historical}, pe_industry={pe_industry}, pb_industry={pb_industry}")
return {"historical_valuation": historical_classification, "industry_valuation": industry_classification}
return {
"pe_historical": pe_historical,
"pb_historical": pb_historical,
"pe_industry": pe_industry,
"pb_industry": pb_industry
}
except Exception as e:
logger.error(f"提取估值分类失败: {str(e)}")
return {"historical_valuation": 0, "industry_valuation": 0}
return {
"pe_historical": 0,
"pb_historical": 0,
"pe_industry": 0,
"pb_industry": 0
}
def _extract_historical_valuation(self, valuation_text: str) -> int:
"""从估值水平分析中提取历史估值分类
def _extract_pe_historical(self, valuation_text: str) -> int:
"""从估值水平分析中提取PE历史分位分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: 历史估值分类值 (高于历史:-1, 接近历史:0, 低于历史:1)
int: PE历史分位分类值 (-1:高于历史, 0:接近历史, 1:低于历史)
"""
try:
# 使用在线模型提取历史估值分类
prompt = f"""请仔细分析以下估值水平文本判断当前PE和PB在历史分位的位置并返回对应的数值
- 如果当前估值明显高于历史平均水平高估返回数值"-1"
- 如果当前估值接近历史平均水平返回数值"0"
- 如果当前估值明显低于历史平均水平低估返回数值"1"
prompt = f"""请仔细分析以下估值水平文本判断当前PE在历史分位的位置并返回对应的数值
- 如果当前PE为负数返回数值"-1"
- 如果当前PE明显高于历史平均水平返回数值"-1"
- 如果当前PE接近历史平均水平返回数值"0"
- 如果当前PE明显低于历史平均水平返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
# 使用千问离线模型提取数值
self.offline_bot_tl_qw.clear_history()
response = self.offline_bot_tl_qw.chat(prompt)
# 清理模型输出
hist_val_str = self._clean_model_output(response)
# 尝试将响应转换为整数
pe_hist_str = self._clean_model_output(response)
try:
hist_val = int(hist_val_str)
# 确保值在有效范围内
if hist_val < -1 or hist_val > 1:
logger.warning(f"提取的历史估值分类值超出范围: {hist_val}设置为默认值0")
pe_hist = int(pe_hist_str)
if pe_hist < -1 or pe_hist > 1:
logger.warning(f"提取的PE历史分位分类值超出范围: {pe_hist}设置为默认值0")
return 0
return hist_val
return pe_hist
except ValueError:
logger.warning(f"无法将提取的历史估值分类值转换为整数: {hist_val_str}设置为默认值0")
logger.warning(f"无法将提取的PE历史分位分类值转换为整数: {pe_hist_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取历史估值分类失败: {str(e)}")
logger.error(f"提取PE历史分位分类失败: {str(e)}")
return 0
def _extract_industry_valuation(self, valuation_text: str) -> int:
"""从估值水平分析中提取行业估值对比分类
def _extract_pb_historical(self, valuation_text: str) -> int:
"""从估值水平分析中提取PB历史分位分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: 行业估值对比分类值 (高于行业:-1, 接近行业:0, 低于行业:1)
int: PB历史分位分类值 (-1:高于历史, 0:接近历史, 1:低于历史)
"""
try:
# 使用在线模型提取行业估值对比分类
prompt = f"""请仔细分析以下估值水平文本判断当前企业的PE和PB与行业平均水平的对比情况并返回对应的数值
- 如果当前企业估值明显高于行业平均水平返回数值"-1"
- 如果当前企业估值接近行业平均水平返回数值"0"
- 如果当前企业估值明显低于行业平均水平返回数值"1"
prompt = f"""请仔细分析以下估值水平文本判断当前PB在历史分位的位置并返回对应的数值
- 如果当前PB为负数返回数值"-1"
- 如果当前PB明显高于历史平均水平返回数值"-1"
- 如果当前PB接近历史平均水平返回数值"0"
- 如果当前PB明显低于历史平均水平返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
# 使用千问离线模型提取数值
response = self.offline_bot_tl_qw.chat(prompt)
# 清理模型输出
ind_val_str = self._clean_model_output(response)
# 尝试将响应转换为整数
self.offline_bot_tl_qw.clear_history()
response = self.offline_bot_tl_qw.chat(prompt)
pb_hist_str = self._clean_model_output(response)
try:
ind_val = int(ind_val_str)
# 确保值在有效范围内
if ind_val < -1 or ind_val > 1:
logger.warning(f"提取的行业估值对比分类值超出范围: {ind_val}设置为默认值0")
pb_hist = int(pb_hist_str)
if pb_hist < -1 or pb_hist > 1:
logger.warning(f"提取的PB历史分位分类值超出范围: {pb_hist}设置为默认值0")
return 0
return ind_val
return pb_hist
except ValueError:
logger.warning(f"无法将提取的行业估值对比分类值转换为整数: {ind_val_str}设置为默认值0")
logger.warning(f"无法将提取的PB历史分位分类值转换为整数: {pb_hist_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取行业估值对比分类失败: {str(e)}")
logger.error(f"提取PB历史分位分类失败: {str(e)}")
return 0
def _extract_pe_industry(self, valuation_text: str) -> int:
"""从估值水平分析中提取PE行业对比分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: PE行业对比分类值 (-1:高于行业, 0:接近行业, 1:低于行业)
"""
try:
prompt = f"""请仔细分析以下估值水平文本判断当前PE与行业平均水平的对比情况并返回对应的数值
- 如果当前PE为负数返回数值"-1"
- 如果当前PE明显高于行业平均水平返回数值"-1"
- 如果当前PE接近行业平均水平返回数值"0"
- 如果当前PE明显低于行业平均水平返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
self.offline_bot_tl_qw.clear_history()
response = self.offline_bot_tl_qw.chat(prompt)
pe_ind_str = self._clean_model_output(response)
try:
pe_ind = int(pe_ind_str)
if pe_ind < -1 or pe_ind > 1:
logger.warning(f"提取的PE行业对比分类值超出范围: {pe_ind}设置为默认值0")
return 0
return pe_ind
except ValueError:
logger.warning(f"无法将提取的PE行业对比分类值转换为整数: {pe_ind_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取PE行业对比分类失败: {str(e)}")
return 0
def _extract_pb_industry(self, valuation_text: str) -> int:
"""从估值水平分析中提取PB行业对比分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: PB行业对比分类值 (-1:高于行业, 0:接近行业, 1:低于行业)
"""
try:
prompt = f"""请仔细分析以下估值水平文本判断当前PB与行业平均水平的对比情况并返回对应的数值
- 如果当前PB为负数返回数值"-1"
- 如果当前PB明显高于行业平均水平返回数值"-1"
- 如果当前PB接近行业平均水平返回数值"0"
- 如果当前PB明显低于行业平均水平返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
self.offline_bot_tl_qw.clear_history()
response = self.offline_bot_tl_qw.chat(prompt)
pb_ind_str = self._clean_model_output(response)
try:
pb_ind = int(pb_ind_str)
if pb_ind < -1 or pb_ind > 1:
logger.warning(f"提取的PB行业对比分类值超出范围: {pb_ind}设置为默认值0")
return 0
return pb_ind
except ValueError:
logger.warning(f"无法将提取的PB行业对比分类值转换为整数: {pb_ind_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取PB行业对比分类失败: {str(e)}")
return 0
def generate_investment_advice(self, stock_code: str, stock_name: str) -> bool: