This commit is contained in:
liao 2025-04-02 16:16:35 +08:00
parent 6986259726
commit ed2758532e
17 changed files with 328 additions and 62 deletions

View File

@ -1,5 +1,7 @@
import logging
from typing import Dict, List, Optional, Callable, Tuple
import os
from datetime import datetime
from typing import Dict, List, Optional, Tuple, Callable
# 修改导入路径,使用相对导入
try:
# 尝试相对导入
@ -26,6 +28,36 @@ import re
# 设置日志记录
logger = logging.getLogger(__name__)
# 获取项目根目录的绝对路径
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 创建logs目录如果不存在
LOGS_DIR = os.path.join(ROOT_DIR, "logs")
os.makedirs(LOGS_DIR, exist_ok=True)
# 配置日志格式
log_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'
# 创建文件处理器
log_file = os.path.join(LOGS_DIR, f"fundamental_analysis_{datetime.now().strftime('%Y%m%d')}.log")
file_handler = logging.FileHandler(log_file, encoding='utf-8')
file_handler.setLevel(logging.INFO)
file_handler.setFormatter(logging.Formatter(log_format, date_format))
# 创建控制台处理器
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter(log_format, date_format))
# 配置根日志记录器
logging.basicConfig(
level=logging.INFO,
handlers=[file_handler, console_handler],
format=log_format,
datefmt=date_format
)
class FundamentalAnalyzer:
"""基本面分析器"""
@ -35,6 +67,8 @@ class FundamentalAnalyzer:
self.chat_bot = ChatBot(model_type="online_bot")
# 使用离线模型进行其他分析
self.offline_bot = OfflineChatBot(platform="volc", model_type="offline_model")
# 千问打杂
self.offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="qwq")
self.db = next(get_db())
# 定义维度映射
@ -47,6 +81,7 @@ class FundamentalAnalyzer:
"stock_discussion": self.analyze_stock_discussion,
"industry_cooperation": self.analyze_industry_cooperation,
"target_price": self.analyze_target_price,
"valuation_level": self.analyze_valuation_level,
"investment_advice": self.generate_investment_advice
}
@ -517,10 +552,10 @@ class FundamentalAnalyzer:
{industry_text}
请仅返回一个数值210-1不要包含任何解释或说明"""
self.offline_bot_tl_qw.clear_history()
# 使用离线模型进行分析
space_value_str = self.offline_bot.chat(prompt).strip()
space_value_str = self.offline_bot_tl_qw.chat(prompt)
space_value_str = self._clean_model_output(space_value_str)
# 提取数值
space_value = 0 # 默认值
@ -613,18 +648,19 @@ class FundamentalAnalyzer:
try:
# 使用离线模型分析项目进展情况
prompt = f"""请分析以下{stock_name}({stock_code})的重大订单和项目进展情况,并返回对应的数值:
- 如果项目进展顺利且订单交付/建厂等超预期返回数值"1"
- 如果进展顺利但没有超预期返回数值"0"
- 如果进展不顺利或者按照进度进行但仍然在验证中返回数值"-1"
重大订单和项目进展内容
{projects_text}
请仅返回一个数值10-1不要包含任何解释或说明"""
- 如果项目进展顺利且订单交付/建厂等超预期返回数值"1"
- 如果进展顺利但没有超预期返回数值"0"
- 如果进展不顺利或者按照进度进行但仍然在验证中返回数值"-1"
重大订单和项目进展内容
{projects_text}
请仅返回一个数值10-1不要包含任何解释或说明"""
self.offline_bot_tl_qw.clear_history()
# 使用离线模型进行分析
events_value_str = self.offline_bot.chat(prompt).strip()
events_value_str = self.offline_bot_tl_qw.chat(prompt)
# 数据清洗
events_value_str = self._clean_model_output(events_value_str)
# 提取数值
events_value = 0 # 默认值
@ -724,10 +760,12 @@ class FundamentalAnalyzer:
{discussion_text}
请仅返回一个数值10-1不要包含任何解释或说明"""
self.offline_bot_tl_qw.clear_history()
# 使用离线模型进行分析
emotion_value_str = self.offline_bot.chat(prompt).strip()
emotion_value_str = self.offline_bot_tl_qw.chat(prompt)
# 数据清洗
emotion_value_str = self._clean_model_output(emotion_value_str)
# 提取数值
emotion_value = 0 # 默认值
@ -965,30 +1003,33 @@ class FundamentalAnalyzer:
try:
# 使用离线模型提取评级
prompt = f"""请仔细分析以下目标股价文本,判断半年内券商评级的主要倾向,并返回对应的数值:
- 如果半年内券商评级以"买入"居多返回数值"2"
- 如果半年内券商评级以"增持"居多返回数值"1"
- 如果半年内券商评级以"中性"居多返回数值"0"
- 如果半年内券商评级以"减持"居多返回数值"-1"
- 如果半年内券商评级以"卖出"居多返回数值"-2"
- 如果半年内没有券商评级信息返回数值"0"
目标股价文本
{price_text}
只需要输出一个数值不要输出任何说明或解释只输出2,1,0,-1-2"""
- 如果半年内券商评级以"买入"居多返回数值"2"
- 如果半年内券商评级以"增持"居多返回数值"1"
- 如果半年内券商评级以"中性"居多返回数值"0"
- 如果半年内券商评级以"减持"居多返回数值"-1"
- 如果半年内券商评级以"卖出"居多返回数值"-2"
- 如果半年内没有券商评级信息返回数值"0"
目标股价文本
{price_text}
只需要输出一个数值不要输出任何说明或解释只输出2,1,0,-1-2"""
response = self.chat_bot.chat(prompt)
# 提取数值
rating_str = self._extract_numeric_value_from_response(response)
# 尝试将响应转换为整数
try:
rating = int(response)
rating = int(rating_str)
# 确保评级在有效范围内
if rating < -2 or rating > 2:
logger.warning(f"提取的券商评级值超出范围: {rating}设置为默认值0")
return 0
return rating
except ValueError:
logger.warning(f"无法将提取的券商评级值转换为整数: {response}设置为默认值0")
logger.warning(f"无法将提取的券商评级值转换为整数: {rating_str}设置为默认值0")
return 0
except Exception as e:
@ -1018,27 +1059,196 @@ class FundamentalAnalyzer:
response = self.chat_bot.chat(prompt)
# 检查是否是错误响应
if isinstance(response, str) and "抱歉,发生错误" in response:
logger.warning(f"获取上涨/下跌空间失败: {response}")
return 0
# 提取数值
odds_str = self._extract_numeric_value_from_response(response)
# 尝试将响应转换为整数
try:
odds = int(response)
odds = int(odds_str)
# 确保值在有效范围内
if odds < -1 or odds > 1:
logger.warning(f"提取的上涨/下跌空间值超出范围: {odds}设置为默认值0")
return 0
return odds
except ValueError:
logger.warning(f"无法将提取的上涨/下跌空间值转换为整数: {response}设置为默认值0")
logger.warning(f"无法将提取的上涨/下跌空间值转换为整数: {odds_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取上涨/下跌空间失败: {str(e)}")
return 0
def analyze_valuation_level(self, stock_code: str, stock_name: str) -> bool:
"""分析企业PE和PB在历史分位水平和行业平均水平的对比情况"""
try:
prompt = f"""请对{stock_name}({stock_code})的估值水平进行简要分析要求输出控制在300字以内请严格按照以下格式输出
1. 历史估值水平150字左右
- 当前PE和PB值
- PE在历史分位水平的位置高于/接近/低于历史平均分位
- PB在历史分位水平的位置高于/接近/低于历史平均分位
- 历史估值变化趋势简要分析
2. 行业估值对比150字左右
- 所在行业平均PE和PB
- PE与行业平均的比较高于/接近/低于行业平均
- PB与行业平均的比较高于/接近/低于行业平均
- 与可比公司估值的简要对比
请提供专业客观的分析突出关键信息避免冗长描述如果无法获取某项数据请直接说明"""
# 获取AI分析结果
result = self.chat_bot.chat(prompt)
# 保存到数据库
success = save_analysis_result(
self.db,
stock_code=stock_code,
stock_name=stock_name,
dimension="valuation_level",
ai_response=self._remove_references_from_response(result["response"]),
reasoning_process=result["reasoning_process"],
references=result["references"]
)
# 提取估值水平分类并更新结果
if success:
self.extract_valuation_classification(result["response"], stock_code, stock_name)
return True
return success
except Exception as e:
logger.error(f"分析估值水平失败: {str(e)}")
return False
def extract_valuation_classification(self, valuation_text: str, stock_code: str, stock_name: str) -> Dict[str, int]:
"""从估值水平分析中提取历史和行业估值分类并更新数据库
Args:
valuation_text: 完整的估值水平分析文本
stock_code: 股票代码
stock_name: 股票名称
Returns:
Dict[str, int]: 包含历史估值和行业估值分类的字典
"""
try:
# 提取历史估值分类
historical_classification = self._extract_historical_valuation(valuation_text)
# 提取行业估值分类
industry_classification = self._extract_industry_valuation(valuation_text)
# 更新数据库中的记录
result = get_analysis_result(self.db, stock_code, "valuation_level")
if result:
update_analysis_result(
self.db,
stock_code=stock_code,
dimension="valuation_level",
ai_response=result.ai_response,
reasoning_process=result.reasoning_process,
references=result.references,
extra_info={
"historical_valuation": historical_classification,
"industry_valuation": industry_classification
}
)
logger.info(f"已更新估值分类到数据库: historical_valuation={historical_classification}, industry_valuation={industry_classification}")
return {"historical_valuation": historical_classification, "industry_valuation": industry_classification}
except Exception as e:
logger.error(f"提取估值分类失败: {str(e)}")
return {"historical_valuation": 0, "industry_valuation": 0}
def _extract_historical_valuation(self, valuation_text: str) -> int:
"""从估值水平分析中提取历史估值分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: 历史估值分类值 (高于历史:-1, 接近历史:0, 低于历史:1)
"""
try:
# 使用在线模型提取历史估值分类
prompt = f"""请仔细分析以下估值水平文本判断当前PE和PB在历史分位的位置并返回对应的数值
- 如果当前估值明显高于历史平均水平高估返回数值"-1"
- 如果当前估值接近历史平均水平返回数值"0"
- 如果当前估值明显低于历史平均水平低估返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
# 使用千问离线模型提取数值
response = self.offline_bot_tl_qw.chat(prompt)
# 清理模型输出
hist_val_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")
return 0
return hist_val
except ValueError:
logger.warning(f"无法将提取的历史估值分类值转换为整数: {hist_val_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取历史估值分类失败: {str(e)}")
return 0
def _extract_industry_valuation(self, valuation_text: str) -> int:
"""从估值水平分析中提取行业估值对比分类
Args:
valuation_text: 完整的估值水平分析文本
Returns:
int: 行业估值对比分类值 (高于行业:-1, 接近行业:0, 低于行业:1)
"""
try:
# 使用在线模型提取行业估值对比分类
prompt = f"""请仔细分析以下估值水平文本判断当前企业的PE和PB与行业平均水平的对比情况并返回对应的数值
- 如果当前企业估值明显高于行业平均水平返回数值"-1"
- 如果当前企业估值接近行业平均水平返回数值"0"
- 如果当前企业估值明显低于行业平均水平返回数值"1"
- 如果文本中没有相关信息返回数值"0"
估值水平文本
{valuation_text}
只需要输出一个数值不要输出任何说明或解释只输出-10或1"""
# 使用千问离线模型提取数值
response = self.offline_bot_tl_qw.chat(prompt)
# 清理模型输出
ind_val_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")
return 0
return ind_val
except ValueError:
logger.warning(f"无法将提取的行业估值对比分类值转换为整数: {ind_val_str}设置为默认值0")
return 0
except Exception as e:
logger.error(f"提取行业估值对比分类失败: {str(e)}")
return 0
def generate_investment_advice(self, stock_code: str, stock_name: str) -> bool:
"""生成最终投资建议"""
try:
@ -1076,7 +1286,8 @@ class FundamentalAnalyzer:
self.offline_bot.clear_history()
# 使用离线模型生成建议
result = self.offline_bot.chat(prompt)
# 清理模型输出
result = self._clean_model_output(result)
# 保存到数据库
success = save_analysis_result(
self.db,
@ -1175,6 +1386,60 @@ class FundamentalAnalyzer:
logger.error(f"清理模型输出失败: {str(e)}")
return output.strip()
def _extract_numeric_value_from_response(self, response: str) -> str:
"""从模型响应中提取数值,移除参考资料和推理过程
Args:
response: 模型原始响应文本或响应对象
Returns:
str: 提取的数值字符串
"""
try:
# 处理响应对象包含response字段的字典
if isinstance(response, dict) and "response" in response:
response = response["response"]
# 确保响应是字符串
if not isinstance(response, str):
logger.warning(f"响应不是字符串类型: {type(response)}")
return "0"
# 移除推理过程部分
reasoning_start = response.find("推理过程:")
if reasoning_start != -1:
response = response[:reasoning_start].strip()
# 移除参考资料部分(通常以 [数字] 开头的行)
lines = response.split("\n")
cleaned_lines = []
for line in lines:
# 跳过参考资料行(通常以 [数字] 开头)
if re.match(r'\[\d+\]', line.strip()):
continue
cleaned_lines.append(line)
response = "\n".join(cleaned_lines).strip()
# 提取数值
# 先尝试直接将整个响应转换为数值
if response.strip() in ["-2", "-1", "0", "1", "2"]:
return response.strip()
# 如果整个响应不是数值,尝试匹配第一个数值
match = re.search(r'([-]?[0-9])', response)
if match:
return match.group(1)
# 如果没有找到数值,返回默认值
logger.warning(f"未能从响应中提取数值: {response}")
return "0"
except Exception as e:
logger.error(f"从响应中提取数值失败: {str(e)}")
return "0"
def _try_extract_advice_type(self, advice_text: str, max_attempts: int = 3) -> Optional[str]:
"""尝试多次从投资建议中提取建议类型
@ -1195,8 +1460,8 @@ class FundamentalAnalyzer:
prompt = self._get_extract_prompt_by_attempt(advice_text, attempt)
# 使用千问离线模型提取建议类型
offline_bot_tl_qw = OfflineChatBot(platform="tl_qw_private", model_type="qwq")
result = offline_bot_tl_qw.chat(prompt)
result = self.offline_bot_tl_qw.chat(prompt)
# 检查是否是错误响应
if isinstance(result, str) and "抱歉,发生错误" in result:
@ -1301,6 +1566,7 @@ class FundamentalAnalyzer:
"stock_discussion": "市场讨论",
"industry_cooperation": "产业合作",
"target_price": "目标股价",
"valuation_level": "估值水平",
"investment_advice": "投资建议"
}

View File

@ -14,10 +14,10 @@ ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 数据库配置
DB_CONFIG = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}

View File

@ -6,10 +6,10 @@ from sqlalchemy import create_engine, Table, Column, String, DECIMAL, MetaData,
# 数据库连接配置
DB_CONFIG = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}

View File

@ -17,10 +17,10 @@ MODEL = "doubao-1-5-pro-32k-250115" # 请填入火山引擎的模型名称
# 数据库配置
DB_CONFIG = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}

View File

@ -121,5 +121,5 @@ def fetch_convertible_bonds(db_url):
print(f"错误类型: {type(e).__name__}")
if __name__ == "__main__":
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
fetch_convertible_bonds(db_url)

View File

@ -41,5 +41,5 @@ def import_concept_sectors(file_path, db_url, table_name):
if __name__ == "__main__":
# 示例调用
file_path = "C:/Users/xy/Desktop/temp/概念板块.csv"
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
import_concept_sectors(file_path, db_url, "gp_gnbk")

View File

@ -109,5 +109,5 @@ def clean_historical_data(engine):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
fetch_hk_hot_stocks(db_url)

View File

@ -185,5 +185,5 @@ def analyze_limitup_stocks_main(db_url):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
analyze_limitup_stocks_main(db_url)

View File

@ -131,7 +131,7 @@ def collect_stock_daily_data(db_url, date=None):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
# 方法1使用快捷函数获取当天数据
collect_stock_daily_data(db_url)

View File

@ -125,7 +125,7 @@ def get_todays_filtered_stocks(db_url):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
# 方法1使用快捷函数获取今日数据
# today_stocks = get_todays_filtered_stocks(db_url)

View File

@ -124,7 +124,7 @@ def collect_stock_minute_data(db_url, date=None):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
# 方法1使用快捷函数获取当天数据
collect_stock_minute_data(db_url)

View File

@ -276,5 +276,5 @@ def main(db_url):
print("9:24:00数据失败")
if __name__ == "__main__":
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
main(db_url)

View File

@ -324,7 +324,7 @@ def clean_historical_data(engine):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
# 创建数据库连接
engine = create_engine(db_url)

View File

@ -68,6 +68,6 @@ def fetch_and_store_stock_data(db_url, table_name, page_size=100):
if __name__ == "__main__":
# 示例调用
db_url = 'mysql+pymysql://root:Chlry$%.8@192.168.1.82:3306/db_gp_cj'
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
table_name = 'stock_changes'
fetch_and_store_stock_data(db_url, table_name)

View File

@ -756,10 +756,10 @@ class StockAnalyzer:
def main():
# 数据库连接配置
db_config = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}

View File

@ -386,10 +386,10 @@ class StockAnalyzer:
def main():
# 数据库连接配置
db_config = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}

View File

@ -1413,10 +1413,10 @@ class StockAnalyzer:
def main():
# 数据库连接配置
db_config = {
'host': '192.168.1.82',
'host': '192.168.18.199',
'port': 3306,
'user': 'root',
'password': 'Chlry$%.8',
'password': 'Chlry#$.8',
'database': 'db_gp_cj'
}