2025-04-02 13:52:34 +08:00
|
|
|
|
import sys
|
|
|
|
|
import os
|
2025-05-06 15:13:15 +08:00
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
import pandas as pd
|
|
|
|
|
import uuid
|
|
|
|
|
import json
|
|
|
|
|
from threading import Thread
|
2025-04-02 13:52:34 +08:00
|
|
|
|
|
2025-04-02 17:38:26 +08:00
|
|
|
|
from src.fundamentals_llm.fundamental_analysis_database import get_analysis_result, get_db
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
# 添加项目根目录到 Python 路径
|
|
|
|
|
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
from flask import Flask, jsonify, request, send_from_directory
|
2025-04-02 13:52:34 +08:00
|
|
|
|
from flask_cors import CORS
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
# 导入企业筛选器
|
|
|
|
|
from src.fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 导入股票回测器
|
|
|
|
|
from src.stock_analysis_v2 import run_backtest, StockBacktester
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
# 设置日志
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO,
|
2025-05-06 15:13:15 +08:00
|
|
|
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
|
|
|
handlers=[
|
|
|
|
|
logging.StreamHandler(), # 输出到控制台
|
|
|
|
|
logging.FileHandler(f'logs/app_{datetime.now().strftime("%Y%m%d")}.log', encoding='utf-8') # 输出到文件
|
|
|
|
|
]
|
2025-04-02 13:52:34 +08:00
|
|
|
|
)
|
2025-05-06 15:13:15 +08:00
|
|
|
|
|
|
|
|
|
# 确保logs和results目录存在
|
|
|
|
|
os.makedirs('logs', exist_ok=True)
|
|
|
|
|
os.makedirs('results', exist_ok=True)
|
|
|
|
|
os.makedirs('results/tasks', exist_ok=True)
|
|
|
|
|
os.makedirs('static/results', exist_ok=True)
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
logger = logging.getLogger(__name__)
|
2025-05-06 15:13:15 +08:00
|
|
|
|
logger.info("Flask应用启动")
|
2025-04-02 13:52:34 +08:00
|
|
|
|
|
|
|
|
|
# 创建 Flask 应用
|
2025-05-06 15:13:15 +08:00
|
|
|
|
app = Flask(__name__, static_folder='static')
|
2025-04-02 13:52:34 +08:00
|
|
|
|
CORS(app) # 启用跨域请求支持
|
|
|
|
|
|
|
|
|
|
# 创建企业筛选器实例
|
|
|
|
|
screener = EnterpriseScreener()
|
|
|
|
|
|
2025-04-02 17:38:26 +08:00
|
|
|
|
# 获取数据库连接
|
|
|
|
|
db = next(get_db())
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 获取项目根目录
|
|
|
|
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports')
|
|
|
|
|
|
|
|
|
|
# 确保reports目录存在
|
|
|
|
|
os.makedirs(REPORTS_DIR, exist_ok=True)
|
|
|
|
|
logger.info(f"报告目录路径: {REPORTS_DIR}")
|
|
|
|
|
|
|
|
|
|
# 存储回测任务状态的字典
|
|
|
|
|
backtest_tasks = {}
|
|
|
|
|
|
|
|
|
|
def run_backtest_task(task_id, stocks_buy_dates, end_date):
|
|
|
|
|
"""
|
|
|
|
|
在后台运行回测任务
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info(f"开始执行回测任务 {task_id}")
|
|
|
|
|
# 更新任务状态为进行中
|
|
|
|
|
backtest_tasks[task_id]['status'] = 'running'
|
|
|
|
|
|
|
|
|
|
# 运行回测
|
|
|
|
|
results, stats_list = run_backtest(stocks_buy_dates, end_date)
|
|
|
|
|
|
|
|
|
|
# 如果回测成功
|
|
|
|
|
if results and stats_list:
|
|
|
|
|
# 计算总体统计
|
|
|
|
|
stats_df = pd.DataFrame(stats_list)
|
|
|
|
|
total_profit = stats_df['final_profit'].sum()
|
|
|
|
|
avg_win_rate = stats_df['win_rate'].mean()
|
|
|
|
|
avg_holding_days = stats_df['avg_holding_days'].mean()
|
|
|
|
|
|
|
|
|
|
# 找出最佳止盈比例(假设为0.15,实际应从回测结果中分析得出)
|
|
|
|
|
best_take_profit_pct = 0.15
|
|
|
|
|
|
|
|
|
|
# 获取图表URL
|
|
|
|
|
chart_urls = {
|
|
|
|
|
"all_stocks": f"/static/results/{task_id}/all_stocks_analysis.png",
|
|
|
|
|
"profit_matrix": f"/static/results/{task_id}/profit_matrix_analysis.png"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 保存股票详细统计
|
|
|
|
|
stock_stats = []
|
|
|
|
|
for stat in stats_list:
|
|
|
|
|
stock_stats.append({
|
|
|
|
|
"symbol": stat['symbol'],
|
|
|
|
|
"total_trades": int(stat['total_trades']),
|
|
|
|
|
"profitable_trades": int(stat['profitable_trades']),
|
|
|
|
|
"loss_trades": int(stat['loss_trades']),
|
|
|
|
|
"win_rate": float(stat['win_rate']),
|
|
|
|
|
"avg_holding_days": float(stat['avg_holding_days']),
|
|
|
|
|
"final_profit": float(stat['final_profit']),
|
|
|
|
|
"entry_count": int(stat['entry_count'])
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 构建结果数据
|
|
|
|
|
result_data = {
|
|
|
|
|
"task_id": task_id,
|
|
|
|
|
"status": "completed",
|
|
|
|
|
"results": {
|
|
|
|
|
"total_profit": float(total_profit),
|
|
|
|
|
"win_rate": float(avg_win_rate),
|
|
|
|
|
"avg_holding_days": float(avg_holding_days),
|
|
|
|
|
"best_take_profit_pct": best_take_profit_pct,
|
|
|
|
|
"stock_stats": stock_stats,
|
|
|
|
|
"chart_urls": chart_urls
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 保存结果到文件
|
|
|
|
|
task_result_path = os.path.join('results', 'tasks', f"{task_id}.json")
|
|
|
|
|
with open(task_result_path, 'w', encoding='utf-8') as f:
|
|
|
|
|
json.dump(result_data, f, ensure_ascii=False, indent=2)
|
|
|
|
|
|
|
|
|
|
# 更新任务状态为已完成
|
|
|
|
|
backtest_tasks[task_id].update({
|
|
|
|
|
'status': 'completed',
|
|
|
|
|
'results': result_data['results']
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
logger.info(f"回测任务 {task_id} 已完成")
|
|
|
|
|
else:
|
|
|
|
|
# 更新任务状态为失败
|
|
|
|
|
backtest_tasks[task_id]['status'] = 'failed'
|
|
|
|
|
backtest_tasks[task_id]['error'] = "回测未产生有效结果"
|
|
|
|
|
logger.error(f"回测任务 {task_id} 失败:回测未产生有效结果")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 更新任务状态为失败
|
|
|
|
|
backtest_tasks[task_id]['status'] = 'failed'
|
|
|
|
|
backtest_tasks[task_id]['error'] = str(e)
|
|
|
|
|
logger.error(f"回测任务 {task_id} 失败:{str(e)}")
|
|
|
|
|
|
|
|
|
|
@app.route('/api/backtest/run', methods=['POST'])
|
|
|
|
|
def start_backtest():
|
|
|
|
|
"""启动回测任务
|
|
|
|
|
|
|
|
|
|
请求体格式:
|
|
|
|
|
{
|
|
|
|
|
"stocks_buy_dates": {
|
|
|
|
|
"SH600522": ["2022-05-10", "2022-06-10"], // 股票代码: [买入日期列表]
|
|
|
|
|
"SZ002340": ["2022-06-15"],
|
|
|
|
|
"SH601615": ["2022-07-20", "2022-08-01"]
|
|
|
|
|
},
|
|
|
|
|
"end_date": "2022-10-20" // 所有股票共同的结束日期
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
返回内容:
|
|
|
|
|
{
|
|
|
|
|
"task_id": "backtask-khkerhy4u237y489237489truiy8432"
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 从请求体获取参数
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供JSON数据"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
stocks_buy_dates = data.get('stocks_buy_dates')
|
|
|
|
|
end_date = data.get('end_date')
|
|
|
|
|
|
|
|
|
|
if not stocks_buy_dates or not isinstance(stocks_buy_dates, dict):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供stocks_buy_dates字典"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
if not end_date or not isinstance(end_date, str):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供有效的end_date"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 验证日期格式
|
|
|
|
|
try:
|
|
|
|
|
datetime.strptime(end_date, '%Y-%m-%d')
|
|
|
|
|
for stock_code, buy_dates in stocks_buy_dates.items():
|
|
|
|
|
if not isinstance(buy_dates, list):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"请求格式错误: 股票 {stock_code} 的买入日期必须是列表"
|
|
|
|
|
}), 400
|
|
|
|
|
for buy_date in buy_dates:
|
|
|
|
|
datetime.strptime(buy_date, '%Y-%m-%d')
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"日期格式错误: {str(e)}"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 生成任务ID
|
|
|
|
|
task_id = f"backtask-{uuid.uuid4().hex[:16]}"
|
|
|
|
|
|
|
|
|
|
# 创建任务目录
|
|
|
|
|
task_dir = os.path.join('static', 'results', task_id)
|
|
|
|
|
os.makedirs(task_dir, exist_ok=True)
|
|
|
|
|
|
|
|
|
|
# 记录任务信息
|
|
|
|
|
backtest_tasks[task_id] = {
|
|
|
|
|
'status': 'pending',
|
|
|
|
|
'created_at': datetime.now().isoformat(),
|
|
|
|
|
'stocks_buy_dates': stocks_buy_dates,
|
|
|
|
|
'end_date': end_date
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 创建线程运行回测
|
|
|
|
|
thread = Thread(target=run_backtest_task, args=(task_id, stocks_buy_dates, end_date))
|
|
|
|
|
thread.daemon = True
|
|
|
|
|
thread.start()
|
|
|
|
|
|
|
|
|
|
logger.info(f"已创建回测任务: {task_id}")
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"task_id": task_id
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"创建回测任务失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"创建回测任务失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/backtest/status', methods=['GET'])
|
|
|
|
|
def check_backtest_status():
|
|
|
|
|
"""查询回测任务状态
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
- task_id: 回测任务ID
|
|
|
|
|
|
|
|
|
|
返回内容:
|
|
|
|
|
{
|
|
|
|
|
"task_id": "backtask-khkerhy4u237y489237489truiy8432",
|
|
|
|
|
"status": "running" | "completed" | "failed",
|
|
|
|
|
"created_at": "2023-12-01T10:30:45",
|
|
|
|
|
"error": "错误信息(如有)"
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
task_id = request.args.get('task_id')
|
|
|
|
|
|
|
|
|
|
if not task_id:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供task_id参数"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 检查任务是否存在
|
|
|
|
|
if task_id not in backtest_tasks:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"任务不存在: {task_id}"
|
|
|
|
|
}), 404
|
|
|
|
|
|
|
|
|
|
# 获取任务信息
|
|
|
|
|
task_info = backtest_tasks[task_id]
|
|
|
|
|
|
|
|
|
|
# 构建响应
|
|
|
|
|
response = {
|
|
|
|
|
"task_id": task_id,
|
|
|
|
|
"status": task_info['status'],
|
|
|
|
|
"created_at": task_info['created_at']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 如果任务失败,添加错误信息
|
|
|
|
|
if task_info['status'] == 'failed' and 'error' in task_info:
|
|
|
|
|
response['error'] = task_info['error']
|
|
|
|
|
|
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"查询任务状态失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"查询任务状态失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/backtest/result', methods=['GET'])
|
|
|
|
|
def get_backtest_result():
|
|
|
|
|
"""获取回测任务结果
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
- task_id: 回测任务ID
|
|
|
|
|
|
|
|
|
|
返回内容:
|
|
|
|
|
{
|
|
|
|
|
"task_id": "backtask-2023121-001",
|
|
|
|
|
"status": "completed",
|
|
|
|
|
"results": {
|
|
|
|
|
"total_profit": 123456.78,
|
|
|
|
|
"win_rate": 75.5,
|
|
|
|
|
"avg_holding_days": 12.3,
|
|
|
|
|
"best_take_profit_pct": 0.15,
|
|
|
|
|
"stock_stats": [...],
|
|
|
|
|
"chart_urls": {
|
|
|
|
|
"all_stocks": "/static/results/backtask-2023121-001/all_stocks_analysis.png",
|
|
|
|
|
"profit_matrix": "/static/results/backtask-2023121-001/profit_matrix_analysis.png"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
task_id = request.args.get('task_id')
|
|
|
|
|
|
|
|
|
|
if not task_id:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供task_id参数"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 检查任务是否存在
|
|
|
|
|
if task_id not in backtest_tasks:
|
|
|
|
|
# 尝试从文件加载
|
|
|
|
|
task_result_path = os.path.join('results', 'tasks', f"{task_id}.json")
|
|
|
|
|
if os.path.exists(task_result_path):
|
|
|
|
|
with open(task_result_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
result_data = json.load(f)
|
|
|
|
|
return jsonify(result_data)
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"任务不存在: {task_id}"
|
|
|
|
|
}), 404
|
|
|
|
|
|
|
|
|
|
# 获取任务信息
|
|
|
|
|
task_info = backtest_tasks[task_id]
|
|
|
|
|
|
|
|
|
|
# 检查任务是否完成
|
|
|
|
|
if task_info['status'] != 'completed':
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"任务尚未完成或已失败: {task_info['status']}"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 构建响应
|
|
|
|
|
response = {
|
|
|
|
|
"task_id": task_id,
|
|
|
|
|
"status": "completed",
|
|
|
|
|
"results": task_info['results']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"获取任务结果失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"获取任务结果失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/reports/<path:filename>')
|
|
|
|
|
def serve_report(filename):
|
|
|
|
|
"""提供PDF报告的访问"""
|
|
|
|
|
try:
|
|
|
|
|
logger.info(f"请求文件: {filename}")
|
|
|
|
|
logger.info(f"从目录: {REPORTS_DIR} 提供文件")
|
|
|
|
|
return send_from_directory(REPORTS_DIR, filename, as_attachment=True)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"提供报告文件失败: {str(e)}")
|
|
|
|
|
return jsonify({"error": "文件不存在"}), 404
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
@app.route('/api/health', methods=['GET'])
|
|
|
|
|
def health_check():
|
|
|
|
|
"""健康检查接口"""
|
|
|
|
|
return jsonify({"status": "ok", "message": "Service is running"})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/stock_profiles', methods=['GET'])
|
|
|
|
|
def get_stock_profiles():
|
|
|
|
|
"""获取企业画像筛选结果列表"""
|
|
|
|
|
return jsonify({
|
|
|
|
|
"profiles": [
|
|
|
|
|
{"id": 1, "name": "高成长潜力企业", "method": "high_growth"},
|
|
|
|
|
{"id": 2, "name": "稳定型龙头企业", "method": "stable_leaders"},
|
|
|
|
|
{"id": 3, "name": "短期投资机会", "method": "short_term"},
|
|
|
|
|
{"id": 4, "name": "价值型投资标的", "method": "value_investment"},
|
|
|
|
|
{"id": 5, "name": "困境反转机会", "method": "turnaround"},
|
|
|
|
|
{"id": 6, "name": "风险规避标的", "method": "risk_averse"},
|
|
|
|
|
{"id": 7, "name": "创新驱动型企业", "method": "innovation_driven"},
|
|
|
|
|
{"id": 8, "name": "行业整合机会", "method": "industry_integration"},
|
|
|
|
|
{"id": 9, "name": "推荐投资企业", "method": "recommended"}
|
|
|
|
|
]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
@app.route('/api/screen/<profile_type>', methods=['GET'])
|
|
|
|
|
def screen_stocks(profile_type):
|
|
|
|
|
"""根据企业画像类型筛选股票
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
profile_type: 企业画像类型 (method 字段值)
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析limit参数
|
|
|
|
|
limit = request.args.get('limit', None, type=int)
|
|
|
|
|
|
|
|
|
|
# 处理别名
|
|
|
|
|
if profile_type == 'innovation_driven':
|
|
|
|
|
profile_type = 'innovation'
|
|
|
|
|
elif profile_type == 'industry_integration':
|
|
|
|
|
profile_type = 'integration'
|
|
|
|
|
|
|
|
|
|
# 映射画像类型到筛选方法
|
|
|
|
|
method_map = {
|
|
|
|
|
'high_growth': screener.screen_high_growth_stocks,
|
|
|
|
|
'stable_leaders': screener.screen_stable_leaders,
|
|
|
|
|
'short_term': screener.screen_short_term_opportunities,
|
|
|
|
|
'value_investment': screener.screen_value_investment_stocks,
|
|
|
|
|
'turnaround': screener.screen_turnaround_opportunities,
|
|
|
|
|
'risk_averse': screener.screen_risk_averse_stocks,
|
|
|
|
|
'innovation': screener.screen_innovation_driven_stocks,
|
|
|
|
|
'integration': screener.screen_industry_integration_stocks,
|
|
|
|
|
'recommended': screener.screen_multi_term_investment_stocks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 检查请求的画像类型是否有效
|
|
|
|
|
if profile_type not in method_map:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"Invalid profile type: {profile_type}"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 调用相应的筛选方法
|
|
|
|
|
stocks = method_map[profile_type](limit=limit)
|
|
|
|
|
|
|
|
|
|
# 转换结果格式
|
|
|
|
|
result = []
|
|
|
|
|
for code, name in stocks:
|
|
|
|
|
result.append({
|
|
|
|
|
"code": code,
|
|
|
|
|
"name": name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "success",
|
|
|
|
|
"profile_type": profile_type,
|
|
|
|
|
"count": len(result),
|
|
|
|
|
"stocks": result
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"筛选股票失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"Failed to screen stocks: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 条件筛选接口,接受自定义的筛选条件
|
|
|
|
|
@app.route('/api/screen/custom', methods=['POST'])
|
|
|
|
|
def screen_custom():
|
|
|
|
|
"""使用自定义条件筛选股票"""
|
|
|
|
|
try:
|
|
|
|
|
# 从请求体获取筛选条件
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'conditions' not in data:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "Missing conditions in request body"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 调用通用筛选方法
|
|
|
|
|
stocks = screener._screen_stocks_by_conditions(data['conditions'])
|
|
|
|
|
|
|
|
|
|
# 转换结果格式
|
|
|
|
|
result = []
|
|
|
|
|
for code, name in stocks:
|
|
|
|
|
result.append({
|
|
|
|
|
"code": code,
|
|
|
|
|
"name": name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "success",
|
|
|
|
|
"count": len(result),
|
|
|
|
|
"stocks": result
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"自定义筛选股票失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"Failed to screen stocks with custom conditions: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/generate_reports', methods=['POST'])
|
|
|
|
|
def generate_reports():
|
|
|
|
|
"""为指定的股票列表生成PDF投资报告
|
|
|
|
|
|
|
|
|
|
请求体格式:
|
|
|
|
|
{
|
|
|
|
|
"stocks": [
|
|
|
|
|
["603690", "至纯科技"],
|
|
|
|
|
["688053", "思科瑞"],
|
|
|
|
|
["300750", "宁德时代"]
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 从请求体获取股票列表
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'stocks' not in data or not isinstance(data['stocks'], list):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供股票列表"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 检查股票列表格式
|
|
|
|
|
stocks = data['stocks']
|
|
|
|
|
if not all(isinstance(item, list) and len(item) == 2 for item in stocks):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "股票列表格式错误: 每个股票应该是 [代码, 名称] 格式"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 导入 PDF 生成器模块
|
|
|
|
|
try:
|
2025-05-06 15:13:15 +08:00
|
|
|
|
from src.fundamentals_llm.pdf_generator import PDFGenerator
|
2025-04-02 13:52:34 +08:00
|
|
|
|
except ImportError:
|
|
|
|
|
try:
|
2025-05-06 15:13:15 +08:00
|
|
|
|
from fundamentals_llm.pdf_generator import PDFGenerator
|
2025-04-02 13:52:34 +08:00
|
|
|
|
except ImportError as e:
|
|
|
|
|
logger.error(f"无法导入 PDF 生成器模块: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: PDF 生成器模块不可用, 错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 生成报告
|
|
|
|
|
generated_reports = []
|
|
|
|
|
for stock_code, stock_name in stocks:
|
|
|
|
|
try:
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 创建 PDF 生成器实例
|
|
|
|
|
generator = PDFGenerator()
|
2025-04-02 13:52:34 +08:00
|
|
|
|
# 调用 PDF 生成器
|
2025-05-06 15:13:15 +08:00
|
|
|
|
report_path = generator.generate_pdf(
|
|
|
|
|
title=f"{stock_name}({stock_code}) 基本面分析报告",
|
|
|
|
|
content_dict={}, # 这里需要传入实际的内容字典
|
|
|
|
|
filename=f"{stock_name}_{stock_code}_analysis.pdf"
|
|
|
|
|
)
|
2025-04-02 13:52:34 +08:00
|
|
|
|
generated_reports.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"report_path": report_path,
|
|
|
|
|
"status": "success"
|
|
|
|
|
})
|
|
|
|
|
logger.info(f"成功生成 {stock_name}({stock_code}) 的投资报告: {report_path}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"生成 {stock_name}({stock_code}) 的投资报告失败: {str(e)}")
|
|
|
|
|
generated_reports.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": str(e)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 返回结果
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "success",
|
|
|
|
|
"message": f"处理了 {len(stocks)} 个股票的报告生成请求",
|
|
|
|
|
"reports": generated_reports
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"处理报告生成请求失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"Failed to process report generation request: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/recommended_stocks', methods=['GET'])
|
|
|
|
|
def recommended_stocks():
|
|
|
|
|
"""获取系统推荐的投资股票
|
|
|
|
|
|
|
|
|
|
该接口会返回系统基于基本面分析推荐的股票列表,具有较高投资价值
|
|
|
|
|
|
|
|
|
|
可以通过以下参数进行过滤:
|
|
|
|
|
- profile_type: 企业画像类型,例如 'high_growth', 'stable_leaders' 等
|
|
|
|
|
- limit: 限制返回的股票数量
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 解析请求参数
|
|
|
|
|
profile_type = request.args.get('profile_type', 'high_growth')
|
|
|
|
|
limit = request.args.get('limit', 10, type=int)
|
|
|
|
|
|
|
|
|
|
# 导入筛选器模块
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
logger.error(f"无法导入企业筛选器模块: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 企业筛选器模块不可用, 错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 创建筛选器实例
|
|
|
|
|
screener = EnterpriseScreener()
|
|
|
|
|
|
|
|
|
|
# 根据类型获取推荐股票
|
|
|
|
|
try:
|
|
|
|
|
if profile_type == 'recommended':
|
|
|
|
|
stocks = screener.screen_multi_term_investment_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'high_growth':
|
|
|
|
|
stocks = screener.screen_high_growth_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'stable_leaders':
|
|
|
|
|
stocks = screener.screen_stable_leaders(limit=limit)
|
|
|
|
|
elif profile_type == 'short_term':
|
|
|
|
|
stocks = screener.screen_short_term_opportunities(limit=limit)
|
|
|
|
|
elif profile_type == 'value_investment':
|
|
|
|
|
stocks = screener.screen_value_investment_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'turnaround':
|
|
|
|
|
stocks = screener.screen_turnaround_opportunities(limit=limit)
|
|
|
|
|
elif profile_type == 'risk_averse':
|
|
|
|
|
stocks = screener.screen_risk_averse_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'innovation' or profile_type == 'innovation_driven':
|
|
|
|
|
stocks = screener.screen_innovation_driven_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'integration' or profile_type == 'industry_integration':
|
|
|
|
|
stocks = screener.screen_industry_integration_stocks(limit=limit)
|
|
|
|
|
elif profile_type == 'custom' and 'conditions' in request.args:
|
|
|
|
|
# 解析自定义条件
|
|
|
|
|
try:
|
|
|
|
|
import json
|
|
|
|
|
conditions = json.loads(request.args.get('conditions'))
|
|
|
|
|
stocks = screener.screen_by_custom_conditions(conditions, limit=limit)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"解析自定义条件失败: {str(e)}"
|
|
|
|
|
}), 400
|
|
|
|
|
else:
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"不支持的企业画像类型: {profile_type}"
|
|
|
|
|
}), 400
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"筛选股票时出错: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"筛选股票时出错: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 格式化返回结果
|
|
|
|
|
formatted_stocks = []
|
|
|
|
|
for stock_code, stock_name in stocks:
|
|
|
|
|
formatted_stocks.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 返回结果
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "success",
|
|
|
|
|
"profile_type": profile_type,
|
|
|
|
|
"count": len(formatted_stocks),
|
|
|
|
|
"stocks": formatted_stocks
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"获取推荐股票失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"获取推荐股票失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
@app.route('/api/analyze_and_recommend', methods=['POST'])
|
|
|
|
|
def analyze_and_recommend():
|
|
|
|
|
"""分析指定企业列表,生成投资建议,并筛选推荐投资的企业
|
|
|
|
|
|
|
|
|
|
请求体格式:
|
|
|
|
|
{
|
|
|
|
|
"stocks": [
|
|
|
|
|
["603690", "至纯科技"],
|
|
|
|
|
["688053", "思科瑞"],
|
|
|
|
|
["300750", "宁德时代"]
|
|
|
|
|
],
|
|
|
|
|
"limit": 10 // 可选,限制返回推荐股票的数量
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 从请求体获取股票列表
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'stocks' not in data or not isinstance(data['stocks'], list):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供股票列表"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 检查股票列表格式
|
|
|
|
|
stocks = data['stocks']
|
|
|
|
|
if not all(isinstance(item, list) and len(item) == 2 for item in stocks):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "股票列表格式错误: 每个股票应该是 [代码, 名称] 格式"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 解析limit参数
|
|
|
|
|
limit = data.get('limit', 10)
|
|
|
|
|
if not isinstance(limit, int) or limit <= 0:
|
|
|
|
|
limit = 10
|
|
|
|
|
|
|
|
|
|
# 导入必要的聊天机器人模块
|
|
|
|
|
try:
|
|
|
|
|
# 首先尝试导入聊天机器人模块
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.chat_bot import ChatBot as OnlineChatBot
|
|
|
|
|
logger.info("成功从 src.fundamentals_llm.chat_bot 导入 ChatBot")
|
|
|
|
|
except ImportError as e1:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.chat_bot import ChatBot as OnlineChatBot
|
|
|
|
|
logger.info("成功从 fundamentals_llm.chat_bot 导入 ChatBot")
|
|
|
|
|
except ImportError as e2:
|
|
|
|
|
logger.error(f"无法导入在线聊天机器人模块: {str(e1)}, {str(e2)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 聊天机器人模块不可用,错误详情: {str(e2)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 然后尝试导入离线聊天机器人模块
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.chat_bot_with_offline import ChatBot as OfflineChatBot
|
|
|
|
|
logger.info("成功从 src.fundamentals_llm.chat_bot_with_offline 导入 ChatBot")
|
|
|
|
|
except ImportError as e1:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.chat_bot_with_offline import ChatBot as OfflineChatBot
|
|
|
|
|
logger.info("成功从 fundamentals_llm.chat_bot_with_offline 导入 ChatBot")
|
|
|
|
|
except ImportError as e2:
|
|
|
|
|
logger.warning(f"无法导入离线聊天机器人模块: {str(e1)}, {str(e2)}")
|
|
|
|
|
# 这里可以继续执行,因为某些功能可能不需要离线模型
|
|
|
|
|
|
|
|
|
|
# 最后导入基本面分析器
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
|
|
|
|
logger.info("成功从 src.fundamentals_llm.fundamental_analysis 导入 FundamentalAnalyzer")
|
|
|
|
|
except ImportError as e1:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
|
|
|
|
logger.info("成功从 fundamentals_llm.fundamental_analysis 导入 FundamentalAnalyzer")
|
|
|
|
|
except ImportError as e2:
|
|
|
|
|
logger.error(f"无法导入基本面分析模块: {str(e1)}, {str(e2)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 基本面分析模块不可用,错误详情: {str(e2)}"
|
|
|
|
|
}), 500
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"导入必要模块时出错: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 导入必要模块时出错,错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 创建基本面分析器实例
|
|
|
|
|
analyzer = FundamentalAnalyzer()
|
|
|
|
|
|
|
|
|
|
# 为每个股票生成投资建议
|
|
|
|
|
investment_advices = []
|
|
|
|
|
for stock_code, stock_name in stocks:
|
|
|
|
|
try:
|
|
|
|
|
# 生成投资建议
|
|
|
|
|
success, advice, reasoning, references = analyzer.query_analysis(
|
|
|
|
|
stock_code, stock_name, "investment_advice"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if success:
|
|
|
|
|
investment_advices.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"advice": advice,
|
|
|
|
|
"reasoning": reasoning,
|
|
|
|
|
"references": references,
|
|
|
|
|
"status": "success"
|
|
|
|
|
})
|
|
|
|
|
logger.info(f"成功生成 {stock_name}({stock_code}) 的投资建议")
|
|
|
|
|
else:
|
|
|
|
|
investment_advices.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": advice
|
|
|
|
|
})
|
|
|
|
|
logger.error(f"生成 {stock_name}({stock_code}) 的投资建议失败: {advice}")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"处理 {stock_name}({stock_code}) 时出错: {str(e)}")
|
|
|
|
|
investment_advices.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": str(e)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 导入企业筛选器
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
logger.error(f"无法导入企业筛选器模块: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 企业筛选器模块不可用,错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 创建筛选器实例
|
|
|
|
|
screener = EnterpriseScreener()
|
|
|
|
|
|
|
|
|
|
# 筛选符合推荐投资条件的股票
|
|
|
|
|
# 获取传入的所有股票代码
|
|
|
|
|
input_stock_codes = set(code for code, _ in stocks)
|
|
|
|
|
|
|
|
|
|
# 获取所有符合推荐投资条件的股票
|
|
|
|
|
recommended_stocks = screener.screen_multi_term_investment_stocks(limit=limit)
|
|
|
|
|
|
|
|
|
|
# 筛选出传入列表中符合推荐投资条件的股票
|
|
|
|
|
recommended_input_stocks = []
|
|
|
|
|
for code, name in recommended_stocks:
|
|
|
|
|
if code in input_stock_codes:
|
|
|
|
|
recommended_input_stocks.append({
|
|
|
|
|
"code": code,
|
|
|
|
|
"name": name
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
# 返回结果
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "success",
|
|
|
|
|
"total_input_stocks": len(stocks),
|
|
|
|
|
"investment_advices": investment_advices,
|
|
|
|
|
"recommended_stocks": {
|
|
|
|
|
"count": len(recommended_input_stocks),
|
|
|
|
|
"stocks": recommended_input_stocks
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"分析和推荐股票失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"分析和推荐股票失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
@app.route('/api/comprehensive_analysis', methods=['POST'])
|
|
|
|
|
def comprehensive_analysis():
|
2025-05-06 15:13:15 +08:00
|
|
|
|
"""综合分析接口 - 使用队列方式处理被锁定的股票
|
2025-04-02 13:52:34 +08:00
|
|
|
|
|
|
|
|
|
请求体格式:
|
|
|
|
|
{
|
|
|
|
|
"stocks": [
|
|
|
|
|
["603690", "至纯科技"],
|
|
|
|
|
["688053", "思科瑞"],
|
|
|
|
|
["300750", "宁德时代"]
|
|
|
|
|
],
|
|
|
|
|
"generate_pdf": true,
|
|
|
|
|
"limit": 10, // 可选,限制返回推荐股票的数量
|
|
|
|
|
"profile_filter": {
|
|
|
|
|
"type": "custom", // 可选值: 预定义的画像类型或 "custom"
|
|
|
|
|
"profile_name": "high_growth", // 当 type = 预定义画像类型时使用
|
|
|
|
|
"conditions": [ // 当 type = "custom" 时使用
|
|
|
|
|
{
|
|
|
|
|
"dimension": "financial_report",
|
|
|
|
|
"field": "financial_report_level",
|
|
|
|
|
"operator": ">=",
|
|
|
|
|
"value": 1
|
|
|
|
|
},
|
|
|
|
|
// 其他条件...
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
# 从请求体获取参数
|
|
|
|
|
data = request.get_json()
|
|
|
|
|
|
|
|
|
|
if not data or 'stocks' not in data or not isinstance(data['stocks'], list):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "请求格式错误: 需要提供股票列表"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 检查股票列表格式
|
|
|
|
|
stocks = data['stocks']
|
|
|
|
|
if not all(isinstance(item, list) and len(item) == 2 for item in stocks):
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": "股票列表格式错误: 每个股票应该是 [代码, 名称] 格式"
|
|
|
|
|
}), 400
|
|
|
|
|
|
|
|
|
|
# 解析其他参数
|
|
|
|
|
generate_pdf = data.get('generate_pdf', False)
|
|
|
|
|
profile_filter = data.get('profile_filter', None)
|
|
|
|
|
limit = data.get('limit', 10)
|
|
|
|
|
if not isinstance(limit, int) or limit <= 0:
|
|
|
|
|
limit = 10
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 导入必要的模块
|
2025-04-02 13:52:34 +08:00
|
|
|
|
try:
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 先导入基本面分析器
|
2025-04-02 13:52:34 +08:00
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
|
|
|
|
logger.info("成功从 src.fundamentals_llm.fundamental_analysis 导入 FundamentalAnalyzer")
|
|
|
|
|
except ImportError as e1:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.fundamental_analysis import FundamentalAnalyzer
|
|
|
|
|
logger.info("成功从 fundamentals_llm.fundamental_analysis 导入 FundamentalAnalyzer")
|
|
|
|
|
except ImportError as e2:
|
|
|
|
|
logger.error(f"无法导入基本面分析模块: {str(e1)}, {str(e2)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 基本面分析模块不可用,错误详情: {str(e2)}"
|
|
|
|
|
}), 500
|
2025-05-06 15:13:15 +08:00
|
|
|
|
|
|
|
|
|
# 再导入其他可能需要的模块
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.chat_bot import ChatBot as OnlineChatBot
|
|
|
|
|
from src.fundamentals_llm.chat_bot_with_offline import ChatBot as OfflineChatBot
|
|
|
|
|
except ImportError:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.chat_bot import ChatBot as OnlineChatBot
|
|
|
|
|
from fundamentals_llm.chat_bot_with_offline import ChatBot as OfflineChatBot
|
|
|
|
|
except ImportError:
|
|
|
|
|
# 这些模块不是必须的,所以继续执行
|
|
|
|
|
logger.warning("无法导入聊天机器人模块,但这不会影响基本功能")
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"导入必要模块时出错: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 导入必要模块时出错,错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 创建基本面分析器实例
|
|
|
|
|
analyzer = FundamentalAnalyzer()
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 准备结果容器
|
|
|
|
|
investment_advices = {} # 使用字典,股票代码作为键
|
|
|
|
|
processing_queue = list(stocks) # 初始处理队列
|
|
|
|
|
max_attempts = 5 # 最大重试次数
|
|
|
|
|
total_attempts = 0
|
|
|
|
|
|
|
|
|
|
# 导入数据库模块
|
|
|
|
|
from src.fundamentals_llm.fundamental_analysis_database import get_analysis_result, get_db
|
|
|
|
|
|
|
|
|
|
# 开始处理队列
|
|
|
|
|
while processing_queue and total_attempts < max_attempts:
|
|
|
|
|
total_attempts += 1
|
|
|
|
|
logger.info(f"开始第 {total_attempts} 轮处理,队列中有 {len(processing_queue)} 只股票")
|
|
|
|
|
|
|
|
|
|
# 暂存下一轮需要处理的股票
|
|
|
|
|
next_round_queue = []
|
|
|
|
|
|
|
|
|
|
# 处理当前队列中的所有股票
|
|
|
|
|
for stock_code, stock_name in processing_queue:
|
|
|
|
|
try:
|
|
|
|
|
# 检查是否已有分析结果
|
|
|
|
|
db = next(get_db())
|
|
|
|
|
existing_result = get_analysis_result(db, stock_code, "investment_advice")
|
|
|
|
|
|
|
|
|
|
# 如果已有近期结果,直接使用
|
|
|
|
|
if existing_result and existing_result.update_time > datetime.now() - timedelta(hours=12):
|
|
|
|
|
investment_advices[stock_code] = {
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"advice": existing_result.ai_response,
|
|
|
|
|
"reasoning": existing_result.reasoning_process,
|
|
|
|
|
"references": existing_result.references,
|
|
|
|
|
"status": "success",
|
|
|
|
|
"from_cache": True
|
|
|
|
|
}
|
|
|
|
|
logger.info(f"使用缓存的 {stock_name}({stock_code}) 分析结果")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 检查是否被锁定
|
|
|
|
|
if analyzer.is_stock_locked(stock_code, "investment_advice"):
|
|
|
|
|
# 已被锁定,放到下一轮队列
|
|
|
|
|
next_round_queue.append([stock_code, stock_name])
|
|
|
|
|
# 记录状态
|
|
|
|
|
if stock_code not in investment_advices:
|
|
|
|
|
investment_advices[stock_code] = {
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "pending",
|
|
|
|
|
"message": f"股票 {stock_code} 正在被其他请求分析中,已加入等待队列"
|
|
|
|
|
}
|
|
|
|
|
logger.info(f"股票 {stock_name}({stock_code}) 已被锁定,放入下一轮队列")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
# 尝试锁定并分析
|
|
|
|
|
analyzer.lock_stock(stock_code, "investment_advice")
|
|
|
|
|
try:
|
|
|
|
|
# 执行分析
|
|
|
|
|
success, advice, reasoning, references = analyzer.query_analysis(
|
|
|
|
|
stock_code, stock_name, "investment_advice"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 记录结果
|
|
|
|
|
if success:
|
|
|
|
|
investment_advices[stock_code] = {
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"advice": advice,
|
|
|
|
|
"reasoning": reasoning,
|
|
|
|
|
"references": references,
|
|
|
|
|
"status": "success"
|
|
|
|
|
}
|
|
|
|
|
logger.info(f"成功分析 {stock_name}({stock_code})")
|
|
|
|
|
else:
|
|
|
|
|
investment_advices[stock_code] = {
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": advice or "分析失败,无详细信息"
|
|
|
|
|
}
|
|
|
|
|
logger.error(f"分析 {stock_name}({stock_code}) 失败: {advice}")
|
|
|
|
|
finally:
|
|
|
|
|
# 确保释放锁
|
|
|
|
|
analyzer.unlock_stock(stock_code, "investment_advice")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 处理异常
|
|
|
|
|
investment_advices[stock_code] = {
|
2025-04-02 13:52:34 +08:00
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
2025-05-06 15:13:15 +08:00
|
|
|
|
"error": str(e)
|
|
|
|
|
}
|
|
|
|
|
logger.error(f"处理 {stock_name}({stock_code}) 时出错: {str(e)}")
|
|
|
|
|
# 确保释放锁
|
|
|
|
|
try:
|
|
|
|
|
analyzer.unlock_stock(stock_code, "investment_advice")
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
# 如果还有下一轮要处理的股票,等待一段时间后继续
|
|
|
|
|
if next_round_queue:
|
|
|
|
|
logger.info(f"本轮结束,还有 {len(next_round_queue)} 只股票等待下一轮处理")
|
|
|
|
|
# 等待30秒再处理下一轮
|
|
|
|
|
import time
|
|
|
|
|
time.sleep(30)
|
|
|
|
|
processing_queue = next_round_queue
|
|
|
|
|
else:
|
|
|
|
|
# 所有股票都已处理完,退出循环
|
|
|
|
|
logger.info("所有股票处理完毕")
|
|
|
|
|
processing_queue = []
|
|
|
|
|
|
|
|
|
|
# 处理仍在队列中的股票(达到最大重试次数但仍未处理的)
|
|
|
|
|
for stock_code, stock_name in processing_queue:
|
|
|
|
|
if stock_code in investment_advices and investment_advices[stock_code]["status"] == "pending":
|
|
|
|
|
investment_advices[stock_code].update({
|
|
|
|
|
"status": "timeout",
|
|
|
|
|
"message": f"等待超时,股票 {stock_code} 可能正在被长时间分析"
|
2025-04-02 13:52:34 +08:00
|
|
|
|
})
|
2025-05-06 15:13:15 +08:00
|
|
|
|
logger.warning(f"股票 {stock_name}({stock_code}) 分析超时")
|
|
|
|
|
|
|
|
|
|
# 将字典转换为列表
|
|
|
|
|
investment_advice_list = list(investment_advices.values())
|
2025-04-02 13:52:34 +08:00
|
|
|
|
|
|
|
|
|
# 生成PDF报告(如果需要)
|
|
|
|
|
pdf_results = []
|
|
|
|
|
if generate_pdf:
|
|
|
|
|
try:
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 针对已成功分析的股票生成PDF
|
|
|
|
|
for stock_info in investment_advice_list:
|
|
|
|
|
if stock_info["status"] == "success":
|
|
|
|
|
stock_code = stock_info["code"]
|
|
|
|
|
stock_name = stock_info["name"]
|
|
|
|
|
try:
|
|
|
|
|
report_path = analyzer.generate_pdf_report(stock_code, stock_name)
|
|
|
|
|
if report_path:
|
|
|
|
|
pdf_results.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"report_path": report_path,
|
|
|
|
|
"status": "success"
|
|
|
|
|
})
|
|
|
|
|
logger.info(f"成功生成 {stock_name}({stock_code}) 的投资报告: {report_path}")
|
|
|
|
|
else:
|
|
|
|
|
pdf_results.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": "生成报告失败"
|
|
|
|
|
})
|
|
|
|
|
logger.error(f"生成 {stock_name}({stock_code}) 的投资报告失败")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"生成 {stock_name}({stock_code}) 的投资报告失败: {str(e)}")
|
|
|
|
|
pdf_results.append({
|
|
|
|
|
"code": stock_code,
|
|
|
|
|
"name": stock_name,
|
|
|
|
|
"status": "error",
|
|
|
|
|
"error": str(e)
|
|
|
|
|
})
|
2025-04-02 13:52:34 +08:00
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"处理PDF生成请求失败: {str(e)}")
|
|
|
|
|
pdf_results = []
|
|
|
|
|
|
|
|
|
|
# 应用企业画像筛选
|
|
|
|
|
filtered_stocks = []
|
|
|
|
|
if profile_filter:
|
|
|
|
|
try:
|
|
|
|
|
# 导入企业筛选器
|
|
|
|
|
try:
|
|
|
|
|
from src.fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError:
|
|
|
|
|
try:
|
|
|
|
|
from fundamentals_llm.enterprise_screener import EnterpriseScreener
|
|
|
|
|
except ImportError as e:
|
|
|
|
|
logger.error(f"无法导入企业筛选器模块: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"服务器配置错误: 企业筛选器模块不可用,错误详情: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
# 创建筛选器实例
|
|
|
|
|
screener = EnterpriseScreener()
|
|
|
|
|
|
|
|
|
|
# 根据筛选类型获取推荐股票
|
|
|
|
|
filter_type = profile_filter.get('type', 'custom')
|
|
|
|
|
|
|
|
|
|
# 如果是自定义条件筛选
|
|
|
|
|
if filter_type == 'custom':
|
|
|
|
|
conditions = profile_filter.get('conditions', [])
|
|
|
|
|
if conditions:
|
|
|
|
|
print(f"使用自定义条件筛选: {len(conditions)} 个条件")
|
|
|
|
|
all_stocks = screener.screen_by_custom_conditions(conditions, limit=limit)
|
|
|
|
|
else:
|
|
|
|
|
# 如果没有提供条件,使用默认的high_growth类型
|
|
|
|
|
print("未提供自定义条件,使用默认的high_growth类型")
|
|
|
|
|
all_stocks = screener.screen_high_growth_stocks(limit=limit)
|
|
|
|
|
# 如果是预定义类型
|
|
|
|
|
else:
|
|
|
|
|
# 直接使用type作为画像类型,如果用户同时提供了profile_name也兼容处理
|
|
|
|
|
profile_name = profile_filter.get('profile_name', filter_type)
|
|
|
|
|
|
|
|
|
|
print(f"使用预定义画像类型: {profile_name}")
|
|
|
|
|
|
|
|
|
|
if profile_name == 'high_growth':
|
|
|
|
|
all_stocks = screener.screen_high_growth_stocks(limit=limit)
|
|
|
|
|
elif profile_name == 'stable_leaders':
|
|
|
|
|
all_stocks = screener.screen_stable_leaders(limit=limit)
|
|
|
|
|
elif profile_name == 'short_term':
|
|
|
|
|
all_stocks = screener.screen_short_term_opportunities(limit=limit)
|
|
|
|
|
elif profile_name == 'value_investment':
|
|
|
|
|
all_stocks = screener.screen_value_investment_stocks(limit=limit)
|
|
|
|
|
elif profile_name == 'turnaround':
|
|
|
|
|
all_stocks = screener.screen_turnaround_opportunities(limit=limit)
|
|
|
|
|
elif profile_name == 'risk_averse':
|
|
|
|
|
all_stocks = screener.screen_risk_averse_stocks(limit=limit)
|
|
|
|
|
elif profile_name == 'innovation' or profile_name == 'innovation_driven':
|
|
|
|
|
all_stocks = screener.screen_innovation_driven_stocks(limit=limit)
|
|
|
|
|
elif profile_name == 'integration' or profile_name == 'industry_integration':
|
|
|
|
|
all_stocks = screener.screen_industry_integration_stocks(limit=limit)
|
|
|
|
|
elif profile_name == 'recommended':
|
|
|
|
|
all_stocks = screener.screen_multi_term_investment_stocks(limit=limit)
|
|
|
|
|
else:
|
|
|
|
|
# 如果类型无效,使用默认的high_growth类型
|
|
|
|
|
logger.warning(f"无效的画像类型: {profile_name},使用默认的high_growth类型")
|
|
|
|
|
all_stocks = screener.screen_high_growth_stocks(limit=limit)
|
|
|
|
|
|
|
|
|
|
# 获取传入的所有股票代码
|
|
|
|
|
input_stock_codes = set(code for code, _ in stocks)
|
|
|
|
|
|
|
|
|
|
# 筛选出传入列表中符合条件的股票
|
|
|
|
|
for code, name in all_stocks:
|
|
|
|
|
if code in input_stock_codes:
|
2025-04-02 17:38:26 +08:00
|
|
|
|
# 获取各个维度的分析结果
|
|
|
|
|
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
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
filtered_stocks.append({
|
|
|
|
|
"code": code,
|
2025-04-02 17:38:26 +08:00
|
|
|
|
"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:低于行业)
|
2025-04-02 13:52:34 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
logger.info(f"筛选出 {len(filtered_stocks)} 个符合条件的股票")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"应用企业画像筛选失败: {str(e)}")
|
|
|
|
|
filtered_stocks = []
|
|
|
|
|
|
2025-05-06 15:13:15 +08:00
|
|
|
|
# 统计各种状态的股票数量
|
|
|
|
|
success_count = sum(1 for item in investment_advice_list if item["status"] == "success")
|
|
|
|
|
pending_count = sum(1 for item in investment_advice_list if item["status"] == "pending")
|
|
|
|
|
timeout_count = sum(1 for item in investment_advice_list if item["status"] == "timeout")
|
|
|
|
|
error_count = sum(1 for item in investment_advice_list if item["status"] == "error")
|
|
|
|
|
|
2025-04-02 13:52:34 +08:00
|
|
|
|
# 返回结果
|
|
|
|
|
response = {
|
2025-05-06 15:13:15 +08:00
|
|
|
|
"status": "success" if success_count > 0 else "partial_success" if success_count + pending_count > 0 else "failed",
|
2025-04-02 13:52:34 +08:00
|
|
|
|
"total_input_stocks": len(stocks),
|
2025-05-06 15:13:15 +08:00
|
|
|
|
"stats": {
|
|
|
|
|
"success": success_count,
|
|
|
|
|
"pending": pending_count,
|
|
|
|
|
"timeout": timeout_count,
|
|
|
|
|
"error": error_count
|
|
|
|
|
},
|
|
|
|
|
"rounds_attempted": total_attempts,
|
|
|
|
|
"investment_advices": investment_advice_list
|
2025-04-02 13:52:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if profile_filter:
|
|
|
|
|
response["filtered_stocks"] = {
|
|
|
|
|
"count": len(filtered_stocks),
|
|
|
|
|
"stocks": filtered_stocks
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if generate_pdf:
|
|
|
|
|
response["pdf_results"] = {
|
|
|
|
|
"count": len(pdf_results),
|
|
|
|
|
"reports": pdf_results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"综合分析失败: {str(e)}")
|
|
|
|
|
return jsonify({
|
|
|
|
|
"status": "error",
|
|
|
|
|
"message": f"综合分析失败: {str(e)}"
|
|
|
|
|
}), 500
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
app.run(host='0.0.0.0', port=5000, debug=True)
|