commit;
This commit is contained in:
parent
03450116ce
commit
ab27f46c87
729
src/app.py
729
src/app.py
|
@ -1,12 +1,12 @@
|
|||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta, time
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
import uuid
|
||||
import json
|
||||
from threading import Thread
|
||||
from sqlalchemy import create_engine, text
|
||||
from src.fundamentals_llm.fundamental_analysis_database import get_analysis_result, get_db
|
||||
from sqlalchemy import text
|
||||
from src.fundamentals_llm.fundamental_analysis_database import get_db
|
||||
|
||||
# 添加项目根目录到 Python 路径
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
@ -19,7 +19,7 @@ import logging
|
|||
from src.fundamentals_llm.enterprise_screener import EnterpriseScreener
|
||||
|
||||
# 导入股票回测器
|
||||
from src.stock_analysis_v2 import run_backtest, StockBacktester
|
||||
from src.stock_analysis_v2 import run_backtest
|
||||
|
||||
# 导入PE/PB估值分析器
|
||||
from src.valuation_analysis.pe_pb_analysis import ValuationAnalyzer
|
||||
|
@ -42,9 +42,6 @@ from src.valuation_analysis.index_analyzer import IndexAnalyzer
|
|||
# 导入股票日线数据采集器
|
||||
from src.scripts.stock_daily_data_collector import collect_stock_daily_data
|
||||
|
||||
from utils.distributed_lock import DistributedLock
|
||||
from valuation_analysis.industry_analysis import redis_client
|
||||
|
||||
from valuation_analysis.financial_analysis import FinancialAnalyzer
|
||||
from src.valuation_analysis.stock_price_collector import StockPriceCollector
|
||||
|
||||
|
@ -186,224 +183,159 @@ def run_backtest_task(task_id, stocks_buy_dates, end_date):
|
|||
backtest_tasks[task_id]['error'] = str(e)
|
||||
logger.error(f"回测任务 {task_id} 失败:{str(e)}")
|
||||
|
||||
def initialize_stock_price_schedule():
|
||||
"""
|
||||
初始化实时股价数据采集定时任务
|
||||
"""
|
||||
# 创建分布式锁
|
||||
price_lock = DistributedLock(redis_client, "stock_price_collector", expire_time=3600) # 1小时过期
|
||||
|
||||
# 尝试获取锁
|
||||
if not price_lock.acquire():
|
||||
logger.info("其他服务器正在运行实时股价数据采集任务,本服务器跳过")
|
||||
return None
|
||||
|
||||
@app.route('/scheduler/stockRealtimePrice/collection', methods=['GET'])
|
||||
def update_stock_realtime_price():
|
||||
"""更新实时股价数据 周内的9点半、10点半、11点半、2点、3点各更新一次"""
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# 创建定时任务调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
def is_trading_time():
|
||||
"""判断当前是否为交易时间"""
|
||||
now = datetime.now()
|
||||
current_time = now.time()
|
||||
|
||||
# 定义交易时间段
|
||||
morning_start = time(9, 25) # 上午开盘前5分钟
|
||||
morning_end = time(11, 30) # 上午收盘
|
||||
afternoon_start = time(13, 0) # 下午开盘
|
||||
afternoon_end = time(15, 0) # 下午收盘
|
||||
|
||||
# 判断是否为工作日
|
||||
if now.weekday() >= 5: # 5是周六,6是周日
|
||||
return False
|
||||
|
||||
# 判断是否在交易时间段内
|
||||
is_morning = morning_start <= current_time <= morning_end
|
||||
is_afternoon = afternoon_start <= current_time <= afternoon_end
|
||||
|
||||
return is_morning or is_afternoon
|
||||
|
||||
def update_stock_price():
|
||||
"""更新实时股价数据"""
|
||||
if not is_trading_time():
|
||||
return
|
||||
|
||||
try:
|
||||
collector = StockPriceCollector()
|
||||
collector.update_latest_data()
|
||||
except Exception as e:
|
||||
logger.error(f"更新实时股价数据失败: {e}")
|
||||
|
||||
# 添加定时任务
|
||||
scheduler.add_job(
|
||||
func=update_stock_price,
|
||||
trigger='interval',
|
||||
minutes=60,
|
||||
id='stock_price_update',
|
||||
name='实时股价数据采集',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
logger.info("实时股价数据采集定时任务已初始化,将在交易时间内每60分钟执行一次")
|
||||
return scheduler
|
||||
|
||||
collector = StockPriceCollector()
|
||||
collector.update_latest_data()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化实时股价数据采集定时任务失败: {str(e)}")
|
||||
price_lock.release()
|
||||
return None
|
||||
logger.error(f"更新实时股价数据失败: {e}")
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 200
|
||||
|
||||
def initialize_rzrq_collector_schedule():
|
||||
"""初始化融资融券数据采集定时任务"""
|
||||
# 创建分布式锁
|
||||
rzrq_lock = DistributedLock(redis_client, "em_rzrq_collector", expire_time=3600) # 1小时过期
|
||||
|
||||
# 尝试获取锁
|
||||
if not rzrq_lock.acquire():
|
||||
logger.info("其他服务器正在运行融资融券数据采集任务,本服务器跳过")
|
||||
return None
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# 创建定时任务调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# 添加每天下午5点执行的任务
|
||||
scheduler.add_job(
|
||||
func=run_rzrq_initial_collection,
|
||||
trigger=CronTrigger(hour=18, minute=40),
|
||||
id='rzrq_daily_update',
|
||||
name='每日更新融资融券数据',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
logger.info("融资融券数据采集定时任务已初始化,将在每天18:00执行")
|
||||
return scheduler
|
||||
except Exception as e:
|
||||
logger.error(f"初始化融资融券数据采集定时任务失败: {str(e)}")
|
||||
rzrq_lock.release()
|
||||
return None
|
||||
|
||||
def initialize_stock_daily_collector_schedule():
|
||||
"""初始化股票日线数据采集定时任务"""
|
||||
# 创建分布式锁
|
||||
stock_daily_lock = DistributedLock(redis_client, "stock_daily_collector", expire_time=3600) # 1小时过期
|
||||
|
||||
# 尝试获取锁
|
||||
if not stock_daily_lock.acquire():
|
||||
logger.info("其他服务器正在运行股票日线数据采集任务,本服务器跳过")
|
||||
return None
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# 创建定时任务调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# 添加每天下午5点执行的任务
|
||||
scheduler.add_job(
|
||||
func=run_stock_daily_collection,
|
||||
trigger=CronTrigger(hour=15, minute=40),
|
||||
id='stock_daily_update',
|
||||
name='每日更新股票日线数据',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
logger.info("股票日线数据采集定时任务已初始化,将在每天15:40执行")
|
||||
return scheduler
|
||||
except Exception as e:
|
||||
logger.error(f"初始化股票日线数据采集定时任务失败: {str(e)}")
|
||||
stock_daily_lock.release()
|
||||
return None
|
||||
|
||||
def run_stock_daily_collection():
|
||||
"""执行股票日线数据采集任务"""
|
||||
@app.route('/scheduler/stockDaily/collection', methods=['GET'])
|
||||
def run_stock_daily_collection1():
|
||||
"""执行股票日线数据采集任务 下午3点四十开始"""
|
||||
try:
|
||||
logger.info("开始执行股票日线数据采集")
|
||||
|
||||
# 获取当天日期
|
||||
today = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
|
||||
# 定义数据库连接地址
|
||||
db_url = 'mysql+pymysql://root:Chlry#$.8@192.168.18.199:3306/db_gp_cj'
|
||||
|
||||
# 在新线程中执行采集任务,避免阻塞主线程
|
||||
def collection_task():
|
||||
try:
|
||||
# 执行采集
|
||||
collect_stock_daily_data(db_url, today)
|
||||
logger.info(f"股票日线数据采集完成,日期: {today}")
|
||||
except Exception as e:
|
||||
logger.error(f"执行股票日线数据采集任务失败: {str(e)}")
|
||||
|
||||
# 创建并启动线程
|
||||
thread = Thread(target=collection_task)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return True
|
||||
collect_stock_daily_data(db_url, today)
|
||||
except Exception as e:
|
||||
logger.error(f"启动股票日线数据采集任务失败: {str(e)}")
|
||||
return False
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 200
|
||||
|
||||
def run_rzrq_initial_collection():
|
||||
"""执行融资融券数据更新采集"""
|
||||
|
||||
@app.route('/scheduler/rzrq/collection', methods=['GET'])
|
||||
def run_rzrq_initial_collection1():
|
||||
"""执行融资融券数据更新采集 下午7点开始"""
|
||||
try:
|
||||
logger.info("开始执行融资融券数据更新采集")
|
||||
|
||||
# 生成任务ID
|
||||
task_id = f"rzrq-{uuid.uuid4().hex[:16]}"
|
||||
|
||||
# 记录任务信息
|
||||
rzrq_tasks[task_id] = {
|
||||
'status': 'running',
|
||||
'created_at': datetime.now().isoformat(),
|
||||
'type': 'initial_collection',
|
||||
'message': '开始执行融资融券数据更新采集'
|
||||
}
|
||||
|
||||
# 在新线程中执行采集任务
|
||||
def collection_task():
|
||||
try:
|
||||
# 执行采集
|
||||
result = em_rzrq_collector.initial_data_collection()
|
||||
|
||||
if result:
|
||||
rzrq_tasks[task_id]['status'] = 'completed'
|
||||
rzrq_tasks[task_id]['message'] = '融资融券数据更新完成'
|
||||
logger.info(f"融资融券数据更新任务 {task_id} 完成")
|
||||
else:
|
||||
rzrq_tasks[task_id]['status'] = 'failed'
|
||||
rzrq_tasks[task_id]['message'] = '融资融券数据更新失败'
|
||||
logger.error(f"融资融券数据更新任务 {task_id} 失败")
|
||||
except Exception as e:
|
||||
rzrq_tasks[task_id]['status'] = 'failed'
|
||||
rzrq_tasks[task_id]['message'] = f'执行失败: {str(e)}'
|
||||
logger.error(f"执行融资融券数据更新线程中出错: {str(e)}")
|
||||
|
||||
# 创建并启动线程
|
||||
thread = Thread(target=collection_task)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
|
||||
return task_id
|
||||
# 执行采集
|
||||
em_rzrq_collector.initial_data_collection()
|
||||
except Exception as e:
|
||||
logger.error(f"启动融资融券数据更新任务失败: {str(e)}")
|
||||
if 'task_id' in locals():
|
||||
rzrq_tasks[task_id]['status'] = 'failed'
|
||||
rzrq_tasks[task_id]['message'] = f'启动失败: {str(e)}'
|
||||
return None
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 200
|
||||
|
||||
|
||||
@app.route('/scheduler/industry/crowding', methods=['GET'])
|
||||
def precalculate_industry_crowding1():
|
||||
"""预计算部分行业和概念板块的拥挤度指标 晚上10点开始"""
|
||||
try:
|
||||
from src.valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||
|
||||
analyzer = IndustryAnalyzer()
|
||||
# 固定行业和概念板块
|
||||
industries = ["IT设备", "消费电子", "半导体", "军工电子", "专用设备", "乘用车", "产业互联网", "元器件", "光学光电", "医疗器械", "医疗服务", "汽车零部件", "航天装备", "自动化设备"]
|
||||
concepts = ["先进封装", "芯片", "消费电子概念", "机器人概念"]
|
||||
|
||||
# 计算行业拥挤度
|
||||
for industry in industries:
|
||||
try:
|
||||
analyzer.get_industry_crowding_index(industry, use_cache=False)
|
||||
except Exception as e:
|
||||
logger.error(f"预计算行业 {industry} 的拥挤度指标时出错: {str(e)}")
|
||||
continue
|
||||
# 计算概念板块拥挤度
|
||||
for concept in concepts:
|
||||
try:
|
||||
analyzer.get_industry_crowding_index(concept, use_cache=False, is_concept=True)
|
||||
except Exception as e:
|
||||
logger.error(f"预计算概念板块 {concept} 的拥挤度指标时出错: {str(e)}")
|
||||
continue
|
||||
logger.info("指定行业和概念板块的拥挤度指标预计算完成")
|
||||
except Exception as e:
|
||||
logger.error(f"预计算行业拥挤度指标失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 200
|
||||
|
||||
|
||||
@app.route('/scheduler/financial/analysis', methods=['GET'])
|
||||
def scheduler_financial_analysis():
|
||||
"""预计算所有股票的财务分析数据 早晚各一次"""
|
||||
try:
|
||||
from src.valuation_analysis.financial_analysis import FinancialAnalyzer
|
||||
|
||||
analyzer = FinancialAnalyzer()
|
||||
analyzer.analyze_financial_data('601021.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('601021.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('601021.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('600483.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('600483.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('600483.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('688596.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688596.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688596.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('002747.SZ', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('002747.SZ', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('002747.SZ', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('688012.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688012.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688012.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('603658.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603658.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603658.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('002409.SZ', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('002409.SZ', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('002409.SZ', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('600584.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('600584.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('600584.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('603055.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603055.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603055.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('601138.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('601138.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('601138.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('603659.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603659.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603659.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('688072.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688072.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688072.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('688008.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688008.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('688008.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('300661.SZ', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('300661.SZ', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('300661.SZ', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('603986.SH', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603986.SH', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('603986.SH', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
analyzer.analyze_financial_data('000733.SZ', current_year = '2024-12-31', previous_year = '2023-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('000733.SZ', current_year = '2023-12-31', previous_year = '2022-12-31', force_update=True)
|
||||
analyzer.analyze_financial_data('000733.SZ', current_year = '2022-12-31', previous_year = '2021-12-31', force_update=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"预计算所有股票的财务分析数据失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "success"
|
||||
}), 200
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
|
@ -1551,7 +1483,7 @@ def valuation_analysis():
|
|||
估值分析接口 - 获取股票的PE/PB估值分析数据
|
||||
|
||||
参数:
|
||||
- stock_code: 股票代码(和stock_name二选一)
|
||||
- stock_code: 股票代码(支持两种格式:SH601021或601021.SH)
|
||||
- stock_name: 股票名称(和stock_code二选一)
|
||||
- start_date: 开始日期(可选,默认为2018-01-01)
|
||||
- industry_name: 行业名称(可选)
|
||||
|
@ -1582,15 +1514,19 @@ def valuation_analysis():
|
|||
"status": "error",
|
||||
"message": "请求格式错误: metric参数必须为'pe'或'pb'"
|
||||
}), 400
|
||||
|
||||
# 如果提供了stock_name但没有stock_code,则查询stock_code
|
||||
if not stock_code and stock_name:
|
||||
# 这里简化处理,实际项目中应该查询数据库获取股票代码
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "暂不支持通过股票名称查询,请提供股票代码"
|
||||
}), 400
|
||||
|
||||
# 处理股票代码格式
|
||||
if stock_code:
|
||||
if '.' in stock_code: # 处理601021.SH格式
|
||||
code_parts = stock_code.split('.')
|
||||
if len(code_parts) == 2:
|
||||
stock_code = f"{code_parts[1]}{code_parts[0]}"
|
||||
else:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "股票代码格式错误: 应为601021.SH格式"
|
||||
}), 400
|
||||
|
||||
# 验证日期格式
|
||||
try:
|
||||
datetime.strptime(start_date, '%Y-%m-%d')
|
||||
|
@ -1866,32 +1802,40 @@ def get_concept_list():
|
|||
@app.route('/api/industry/analysis', methods=['GET'])
|
||||
def industry_analysis():
|
||||
"""
|
||||
行业分析接口 - 获取行业的PE/PB/PS估值分析数据和拥挤度指标
|
||||
行业/概念板块分析接口 - 获取行业或概念板块的PE/PB/PS估值分析数据和拥挤度指标
|
||||
|
||||
参数:
|
||||
- industry_name: 行业名称
|
||||
- industry_name: 行业名称(与concept_name二选一)
|
||||
- concept_name: 概念板块名称(与industry_name二选一)
|
||||
- metric: 估值指标,可选值为'pe'、'pb'或'ps',默认为'pe'
|
||||
- start_date: 开始日期(可选,默认为3年前)
|
||||
|
||||
返回:
|
||||
用于构建ECharts图表的行业估值数据对象,包含估值指标和拥挤度
|
||||
用于构建ECharts图表的行业/概念板块估值数据对象,包含估值指标和拥挤度
|
||||
|
||||
注意:
|
||||
- 行业PE/PB/PS计算中已剔除负值和极端值(如PE>1000)
|
||||
- 所有百分位数据都是基于行业平均值计算的
|
||||
- 行业/概念板块PE/PB/PS计算中已剔除负值和极端值(如PE>1000)
|
||||
- 所有百分位数据都是基于行业/概念板块平均值计算的
|
||||
- 拥挤度数据固定使用最近3年的数据,不受start_date参数影响
|
||||
"""
|
||||
try:
|
||||
# 解析参数
|
||||
industry_name = request.args.get('industry_name')
|
||||
concept_name = request.args.get('concept_name')
|
||||
metric = request.args.get('metric', 'pe')
|
||||
start_date = request.args.get('start_date')
|
||||
|
||||
# 检查参数
|
||||
if not industry_name:
|
||||
if not industry_name and not concept_name:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请求格式错误: 需要提供industry_name参数"
|
||||
"message": "请求格式错误: 需要提供industry_name或concept_name参数"
|
||||
}), 400
|
||||
|
||||
if industry_name and concept_name:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "请求格式错误: industry_name和concept_name不能同时提供"
|
||||
}), 400
|
||||
|
||||
if metric not in ['pe', 'pb', 'ps']:
|
||||
|
@ -1900,8 +1844,13 @@ def industry_analysis():
|
|||
"message": "请求格式错误: metric参数必须为'pe'、'pb'或'ps'"
|
||||
}), 400
|
||||
|
||||
# 获取行业分析数据
|
||||
result = industry_analyzer.get_industry_analysis(industry_name, metric, start_date)
|
||||
# 获取分析数据
|
||||
if industry_name:
|
||||
result = industry_analyzer.get_industry_analysis(industry_name, metric, start_date)
|
||||
title_name = industry_name
|
||||
else:
|
||||
result = industry_analyzer.get_concept_analysis(concept_name, metric, start_date)
|
||||
title_name = concept_name
|
||||
|
||||
if not result.get('success', False):
|
||||
return jsonify({
|
||||
|
@ -1918,11 +1867,11 @@ def industry_analysis():
|
|||
|
||||
# 准备图例数据
|
||||
legend_data = [
|
||||
f"{industry_name}行业平均{metric_name}",
|
||||
f"行业平均{metric_name}历史最小值",
|
||||
f"行业平均{metric_name}历史最大值",
|
||||
f"行业平均{metric_name}历史Q1",
|
||||
f"行业平均{metric_name}历史Q3"
|
||||
f"{title_name}平均{metric_name}",
|
||||
f"平均{metric_name}历史最小值",
|
||||
f"平均{metric_name}历史最大值",
|
||||
f"平均{metric_name}历史Q1",
|
||||
f"平均{metric_name}历史Q3"
|
||||
]
|
||||
|
||||
# 构建结果
|
||||
|
@ -1930,7 +1879,7 @@ def industry_analysis():
|
|||
"status": "success",
|
||||
"data": {
|
||||
"title": {
|
||||
"text": f"{industry_name}行业历史{metric_name}分析",
|
||||
"text": f"{title_name}历史{metric_name}分析",
|
||||
"subtext": f"当前{metric_name}百分位: {percentiles['percentile']:.2f}%(剔除负值及极端值)"
|
||||
},
|
||||
"tooltip": {
|
||||
|
@ -1984,7 +1933,7 @@ def industry_analysis():
|
|||
],
|
||||
"series": [
|
||||
{
|
||||
"name": f"{industry_name}行业平均{metric_name}",
|
||||
"name": f"{title_name}平均{metric_name}",
|
||||
"type": "line",
|
||||
"data": valuation_data['avg_values'],
|
||||
"markLine": {
|
||||
|
@ -1998,27 +1947,27 @@ def industry_analysis():
|
|||
}
|
||||
},
|
||||
{
|
||||
"name": f"行业平均{metric_name}历史最小值",
|
||||
"name": f"平均{metric_name}历史最小值",
|
||||
"type": "line",
|
||||
"data": valuation_data['min_values'],
|
||||
"lineStyle": {"width": 1, "opacity": 0.4, "color": "#28a745"},
|
||||
"areaStyle": {"opacity": 0.1, "color": "#28a745"}
|
||||
},
|
||||
{
|
||||
"name": f"行业平均{metric_name}历史最大值",
|
||||
"name": f"平均{metric_name}历史最大值",
|
||||
"type": "line",
|
||||
"data": valuation_data['max_values'],
|
||||
"lineStyle": {"width": 1, "opacity": 0.4, "color": "#dc3545"},
|
||||
"areaStyle": {"opacity": 0.1, "color": "#dc3545"}
|
||||
},
|
||||
{
|
||||
"name": f"行业平均{metric_name}历史Q1",
|
||||
"name": f"平均{metric_name}历史Q1",
|
||||
"type": "line",
|
||||
"data": valuation_data['q1_values'],
|
||||
"lineStyle": {"width": 1, "opacity": 0.6, "color": "#28a745"}
|
||||
},
|
||||
{
|
||||
"name": f"行业平均{metric_name}历史Q3",
|
||||
"name": f"平均{metric_name}历史Q3",
|
||||
"type": "line",
|
||||
"data": valuation_data['q3_values'],
|
||||
"lineStyle": {"width": 1, "opacity": 0.6, "color": "#dc3545"}
|
||||
|
@ -2048,7 +1997,7 @@ def industry_analysis():
|
|||
}
|
||||
}
|
||||
|
||||
# 添加拥挤度指标(如果有)- 作为独立数据,不再添加到主图表series中
|
||||
# 添加拥挤度指标(如果有)
|
||||
if "crowding" in result:
|
||||
crowding_data = result["crowding"]
|
||||
current_crowding = crowding_data["current"]
|
||||
|
@ -2070,7 +2019,7 @@ def industry_analysis():
|
|||
return jsonify(response)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"行业分析请求失败: {str(e)}")
|
||||
logger.error(f"行业/概念板块分析请求失败: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"分析失败: {str(e)}"
|
||||
|
@ -2091,7 +2040,7 @@ def get_northbound_data():
|
|||
"""获取北向资金流向数据接口
|
||||
|
||||
参数:
|
||||
- start_time: 可选,开始时间戳(秒)
|
||||
- start_time: 可选 ,开始时间戳(秒)
|
||||
- end_time: 可选,结束时间戳(秒)
|
||||
|
||||
返回北向资金流向数据
|
||||
|
@ -2638,69 +2587,6 @@ def get_index_data():
|
|||
logger.error(f"获取指数数据失败: {str(e)}")
|
||||
return jsonify({"status": "error", "message": str(e)})
|
||||
|
||||
def initialize_industry_crowding_schedule():
|
||||
"""初始化行业拥挤度指标预计算定时任务"""
|
||||
# 创建分布式锁
|
||||
industry_crowding_lock = DistributedLock(redis_client, "industry_crowding_calculator", expire_time=3600) # 1小时过期
|
||||
|
||||
# 尝试获取锁
|
||||
if not industry_crowding_lock.acquire():
|
||||
logger.info("其他服务器正在运行行业拥挤度指标预计算任务,本服务器跳过")
|
||||
return None
|
||||
|
||||
try:
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
# 创建定时任务调度器
|
||||
scheduler = BackgroundScheduler()
|
||||
|
||||
# 添加每天晚上10点执行的任务
|
||||
scheduler.add_job(
|
||||
func=precalculate_industry_crowding,
|
||||
trigger=CronTrigger(hour=20, minute=30),
|
||||
id='industry_crowding_precalc',
|
||||
name='预计算行业拥挤度指标',
|
||||
replace_existing=True
|
||||
)
|
||||
|
||||
# 启动调度器
|
||||
scheduler.start()
|
||||
logger.info("行业拥挤度指标预计算定时任务已初始化,将在每天20:30执行")
|
||||
return scheduler
|
||||
except Exception as e:
|
||||
logger.error(f"初始化行业拥挤度指标预计算定时任务失败: {str(e)}")
|
||||
industry_crowding_lock.release()
|
||||
return None
|
||||
|
||||
def precalculate_industry_crowding():
|
||||
"""预计算所有行业的拥挤度指标"""
|
||||
try:
|
||||
from .valuation_analysis.industry_analysis import IndustryAnalyzer
|
||||
|
||||
analyzer = IndustryAnalyzer()
|
||||
industries = analyzer.get_all_industries()
|
||||
|
||||
for industry in industries:
|
||||
try:
|
||||
# 调用时设置 use_cache=False,强制重新计算
|
||||
df = analyzer.get_industry_crowding_index(industry, use_cache=False)
|
||||
if not df.empty:
|
||||
logger.info(f"成功预计算行业 {industry} 的拥挤度指标")
|
||||
else:
|
||||
logger.warning(f"行业 {industry} 的拥挤度指标计算失败")
|
||||
except Exception as e:
|
||||
logger.error(f"预计算行业 {industry} 的拥挤度指标时出错: {str(e)}")
|
||||
continue
|
||||
|
||||
logger.info("所有行业的拥挤度指标预计算完成")
|
||||
except Exception as e:
|
||||
logger.error(f"预计算行业拥挤度指标失败: {str(e)}")
|
||||
finally:
|
||||
# 释放分布式锁
|
||||
industry_crowding_lock = DistributedLock(redis_client, "industry_crowding_calculator")
|
||||
industry_crowding_lock.release()
|
||||
|
||||
@app.route('/api/financial/analysis', methods=['GET'])
|
||||
def financial_analysis():
|
||||
"""
|
||||
|
@ -2708,6 +2594,9 @@ def financial_analysis():
|
|||
|
||||
请求参数:
|
||||
stock_code: 股票代码
|
||||
force_update: 是否强制更新缓存(可选,默认为false)
|
||||
current_year: 当前年份,格式为'YYYY-12-31'(可选,默认为'2024-12-31')
|
||||
previous_year: 上一年份,格式为'YYYY-12-31'(可选,默认为'2023-12-31')
|
||||
|
||||
返回:
|
||||
分析结果JSON
|
||||
|
@ -2719,11 +2608,28 @@ def financial_analysis():
|
|||
'success': False,
|
||||
'message': '缺少必要参数:stock_code'
|
||||
}), 400
|
||||
|
||||
|
||||
analyzer = FinancialAnalyzer()
|
||||
result = analyzer.analyze_financial_data(stock_code)
|
||||
result2024 = analyzer.analyze_financial_data(stock_code, current_year = '2024-12-31', previous_year = '2023-12-31')
|
||||
result2023 = analyzer.analyze_financial_data(stock_code, current_year = '2023-12-31', previous_year = '2022-12-31')
|
||||
result2022 = analyzer.analyze_financial_data(stock_code, current_year = '2022-12-31', previous_year = '2021-12-31')
|
||||
|
||||
# 合并2023和2022的is_better、change_rate、change和avg_score到2024的result
|
||||
def merge_year_data(target, source, year):
|
||||
for block in ["financial_strength", "profitability", "growth", "value_rating", "liquidity"]:
|
||||
if block in target and block in source:
|
||||
# avg_score
|
||||
target[block][f"avg_score_{year}"] = source[block].get("avg_score")
|
||||
# indicators
|
||||
if "indicators" in target[block] and "indicators" in source[block]:
|
||||
for t_item, s_item in zip(target[block]["indicators"], source[block]["indicators"]):
|
||||
for k in ["is_better", "change_rate", "change"]:
|
||||
if k in s_item:
|
||||
t_item[f"{k}_{year}"] = s_item.get(k)
|
||||
merge_year_data(result2024, result2023, "2023")
|
||||
merge_year_data(result2024, result2022, "2022")
|
||||
|
||||
return jsonify(result)
|
||||
return jsonify(result2024)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"财务分析失败: {str(e)}")
|
||||
|
@ -2797,45 +2703,178 @@ def test_mongo_structure():
|
|||
'message': f'测试MongoDB结构失败: {str(e)}'
|
||||
}), 500
|
||||
|
||||
@app.route('/api/stock/real_time_price', methods=['GET'])
|
||||
def get_real_time_price():
|
||||
"""获取股票实时价格接口
|
||||
|
||||
参数:
|
||||
- stock_code: 股票代码(必填)
|
||||
|
||||
返回:
|
||||
{
|
||||
"status": "success",
|
||||
"data": {
|
||||
"stock_code": "600000",
|
||||
"stock_name": "浦发银行",
|
||||
"current_price": 10.5,
|
||||
"change_percent": 2.5,
|
||||
"change_amount": 0.25,
|
||||
"volume": 1234567,
|
||||
"amount": 12345678.9,
|
||||
"high": 10.8,
|
||||
"low": 10.2,
|
||||
"open": 10.3,
|
||||
"pre_close": 10.25,
|
||||
"update_time": "2024-01-20 14:30:00"
|
||||
}
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# 获取股票代码参数
|
||||
stock_code = request.args.get('stock_code')
|
||||
|
||||
# 验证参数
|
||||
if not stock_code:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": "缺少必要参数: stock_code"
|
||||
}), 400
|
||||
|
||||
# 导入股票价格采集器
|
||||
from src.valuation_analysis.stock_price_collector import StockPriceCollector
|
||||
|
||||
# 创建采集器实例
|
||||
collector = StockPriceCollector()
|
||||
|
||||
# 获取实时价格数据
|
||||
price_data = collector.get_stock_price_data(stock_code)
|
||||
|
||||
if not price_data:
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"获取股票 {stock_code} 的实时价格失败"
|
||||
}), 404
|
||||
|
||||
# 构建响应数据
|
||||
response_data = {
|
||||
"stock_code": stock_code,
|
||||
"stock_name": price_data.get('stock_name'),
|
||||
"current_price": price_data.get('current_price'),
|
||||
"change_percent": price_data.get('change_percent'),
|
||||
"change_amount": price_data.get('change_amount'),
|
||||
"volume": price_data.get('volume'),
|
||||
"amount": price_data.get('amount'),
|
||||
"high": price_data.get('high'),
|
||||
"low": price_data.get('low'),
|
||||
"open": price_data.get('open'),
|
||||
"pre_close": price_data.get('pre_close'),
|
||||
"update_time": price_data.get('update_time')
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"status": "success",
|
||||
"data": response_data
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取股票实时价格异常: {str(e)}")
|
||||
return jsonify({
|
||||
"status": "error",
|
||||
"message": f"服务器错误: {str(e)}"
|
||||
}), 500
|
||||
|
||||
@app.route('/bigscreen')
|
||||
def bigscreen_page():
|
||||
"""渲染大屏展示页面"""
|
||||
return render_template('bigscreen.html')
|
||||
|
||||
@app.route('/api/bigscreen_data', methods=['GET'])
|
||||
def bigscreen_data():
|
||||
"""聚合大屏所需的12张图数据,便于前端一次性加载"""
|
||||
try:
|
||||
# 资金流向
|
||||
north = hsgt_monitor.fetch_northbound_data()
|
||||
south = hsgt_monitor.fetch_southbound_data()
|
||||
# 融资融券
|
||||
rzrq = em_rzrq_collector.get_chart_data(limit_days=90)
|
||||
# 恐贪指数
|
||||
fear_greed = fear_greed_manager.get_index_data(limit=180)
|
||||
# 概念板块
|
||||
concepts = [
|
||||
("先进封装", "xjfz"),
|
||||
("芯片", "xp"),
|
||||
("消费电子概念", "xfdz"),
|
||||
("机器人概念", "jqr")
|
||||
]
|
||||
concept_data = {}
|
||||
for cname, key in concepts:
|
||||
res = industry_analyzer.get_concept_analysis(cname, 'pe', None)
|
||||
if res.get('success'):
|
||||
# PE主线
|
||||
pe = {
|
||||
'dates': res['valuation']['dates'],
|
||||
'values': res['valuation']['avg_values']
|
||||
}
|
||||
# 拥挤度
|
||||
crowding = res.get('crowding', {})
|
||||
crowding_obj = {
|
||||
'dates': crowding.get('dates', []),
|
||||
'values': crowding.get('percentiles', [])
|
||||
} if crowding else {'dates': [], 'values': []}
|
||||
concept_data[key] = {'pe': pe, 'crowding': crowding_obj}
|
||||
else:
|
||||
concept_data[key] = {'pe': {'dates': [], 'values': []}, 'crowding': {'dates': [], 'values': []}}
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'northbound': {
|
||||
'dates': north.get('times', []),
|
||||
'values': north.get('data', {}).get('total', [])
|
||||
} if north.get('success') else {'dates': [], 'values': []},
|
||||
'southbound': {
|
||||
'dates': south.get('times', []),
|
||||
'values': south.get('data', {}).get('total', [])
|
||||
} if south.get('success') else {'dates': [], 'values': []},
|
||||
'rzrq': {
|
||||
'dates': rzrq.get('dates', []),
|
||||
'values': rzrq.get('series', [{}])[0].get('data', [])
|
||||
} if rzrq.get('success') and rzrq.get('series') else {'dates': [], 'values': []},
|
||||
'fear_greed': {
|
||||
'dates': fear_greed.get('dates', []),
|
||||
'values': fear_greed.get('values', [])
|
||||
} if fear_greed.get('success') else {'dates': [], 'values': []},
|
||||
'concepts': concept_data
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"大屏数据聚合失败: {str(e)}")
|
||||
return jsonify({'status': 'error', 'message': str(e)})
|
||||
|
||||
@app.route('/api/pep_stock_info_by_shortname', methods=['GET'])
|
||||
def get_pep_stock_info_by_shortname():
|
||||
"""根据股票简称查询pep_stock_info集合中的全部字段"""
|
||||
short_name = request.args.get('short_name')
|
||||
if not short_name:
|
||||
return jsonify({'success': False, 'message': '缺少必要参数: short_name'}), 400
|
||||
try:
|
||||
analyzer = FinancialAnalyzer()
|
||||
result = analyzer.get_pep_stock_info_by_shortname(short_name)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/pep_stock_info_by_code', methods=['GET'])
|
||||
def get_pep_stock_info_by_code():
|
||||
"""根据股票简称查询pep_stock_info集合中的全部字段"""
|
||||
short_code = request.args.get('code')
|
||||
if not short_code:
|
||||
return jsonify({'success': False, 'message': '缺少必要参数: short_code'}), 400
|
||||
try:
|
||||
analyzer = FinancialAnalyzer()
|
||||
result = analyzer.get_pep_stock_info_by_code(short_code)
|
||||
return jsonify(result)
|
||||
except Exception as e:
|
||||
return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
"""
|
||||
# 手动释放锁的方法(需要时取消注释)
|
||||
# 创建锁实例
|
||||
rzrq_lock = DistributedLock(redis_client, "em_rzrq_collector")
|
||||
stock_daily_lock = DistributedLock(redis_client, "stock_daily_collector")
|
||||
industry_crowding_lock = DistributedLock(redis_client, "industry_crowding_calculator")
|
||||
|
||||
# 强制释放锁
|
||||
print("开始释放锁...")
|
||||
|
||||
if rzrq_lock.release():
|
||||
print("成功释放融资融券采集器锁")
|
||||
else:
|
||||
print("融资融券采集器锁释放失败或不存在")
|
||||
|
||||
if stock_daily_lock.release():
|
||||
print("成功释放股票日线采集器锁")
|
||||
else:
|
||||
print("股票日线采集器锁释放失败或不存在")
|
||||
if industry_crowding_lock.release():
|
||||
print("成功释放行业拥挤度锁")
|
||||
else:
|
||||
print("行业拥挤度锁释放失败或不存在")
|
||||
|
||||
print("锁释放操作完成")
|
||||
"""
|
||||
|
||||
# 初始化融资融券数据采集定时任务
|
||||
rzrq_scheduler = initialize_rzrq_collector_schedule()
|
||||
|
||||
# 初始化股票日线数据采集定时任务
|
||||
stock_daily_scheduler = initialize_stock_daily_collector_schedule()
|
||||
|
||||
# 初始化行业拥挤度指标预计算定时任务
|
||||
industry_crowding_scheduler = initialize_industry_crowding_schedule()
|
||||
|
||||
# 初始化实时股价数据采集定时任务
|
||||
initialize_stock_price_schedule()
|
||||
|
||||
# 启动Web服务器
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
|
|
|
@ -132,7 +132,6 @@ class ChatBot:
|
|||
summary = ref.get('summary', '')
|
||||
url = ref.get('url', '')
|
||||
publish_time = ref.get('publish_time', '')
|
||||
|
||||
formatted_ref = []
|
||||
if title:
|
||||
formatted_ref.append(f"标题:{title}")
|
||||
|
@ -150,12 +149,12 @@ class ChatBot:
|
|||
logger.error(f"格式化参考资料时出错: {str(e)}")
|
||||
return str(ref)
|
||||
|
||||
def chat(self, user_input: str, temperature: float = 1.0, top_p: float = 0.7, max_tokens: int = 4096, frequency_penalty: float = 0.0) -> Dict[str, Any]:
|
||||
def chat(self, user_input: str, temperature: float = 0.7, top_p: float = 0.7, max_tokens: int = 4096, frequency_penalty: float = 0.0) -> Dict[str, Any]:
|
||||
"""与AI进行对话
|
||||
|
||||
Args:
|
||||
user_input: 用户输入的问题
|
||||
temperature: 控制输出的随机性,范围0-2,默认1.0
|
||||
temperature: 控制输出的随机性,范围0-2,默认0.7
|
||||
top_p: 控制输出的多样性,范围0-1,默认0.7
|
||||
max_tokens: 控制输出的最大长度,默认4096
|
||||
frequency_penalty: 控制重复惩罚,范围-2到2,默认0.0
|
||||
|
|
|
@ -1464,6 +1464,11 @@ class FundamentalAnalyzer:
|
|||
if result:
|
||||
all_results[dimension] = result.ai_response
|
||||
|
||||
# 获取当前时间
|
||||
current_time = datetime.now()
|
||||
current_year = current_time.year
|
||||
current_month = current_time.month
|
||||
|
||||
# 构建提示词
|
||||
prompt = f"""请根据以下{stock_name}({stock_code})的各个维度分析结果,生成最终的投资建议,要求输出控制在300字以内,请严格按照以下格式输出:
|
||||
|
||||
|
@ -1477,12 +1482,7 @@ class FundamentalAnalyzer:
|
|||
- 长期持有:公司具备长期稳定的盈利能力、行业地位稳固、长期成长性好
|
||||
- 不建议投资:存在明显风险因素、基本面恶化、估值过高、行业前景不佳或者存在退市风险
|
||||
|
||||
请注意:
|
||||
1. 请完全基于提供的分析结果中的最新数据进行分析,不要使用任何历史数据或过时信息
|
||||
2. 如果分析结果中包含2024年或2025年的数据,请优先使用这些最新数据
|
||||
3. 避免使用"2023年"等历史时间点的数据,除非分析结果中明确提供了这些数据
|
||||
4. 重点关注公司最新的业务发展、财务表现和市场定位
|
||||
5. 在分析行业环境时,请使用最新的行业数据和竞争格局信息
|
||||
请注意:当前时间是{current_year}年{current_month}月,请基于这个时间点结合当前,分析未来的投资建议。
|
||||
|
||||
请提供专业、客观的分析,突出关键信息,避免冗长描述。重点关注投资价值和风险。在输出投资建议时,请明确指出是短期持有、中期持有、长期持有还是不建议投资。
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ XUEQIU_HEADERS = {
|
|||
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||
'Client-Version': 'v2.44.75',
|
||||
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; xq_a_token=660fb18cf1d15162da76deedc46b649370124dca; xqat=660fb18cf1d15162da76deedc46b649370124dca; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzQ5ODYxNjY5LCJjdG0iOjE3NDcyNjk2Njk0NDgsImNpZCI6ImQ5ZDBuNEFadXAifQ.jc_E9qvguLwBDASn1Z-KjGtU89pNJRwJq_hIaiR3r2re7-_xiXH8qhuhC3Se8rlfKGZ8sHsb3rSND_vnF7yMp90QQHdK_brSmlgd6_ltHmJfWSFNJvMk7F3s0yPjcpeMqeUTPFnZwKmoWwZVKEwdVBN8f25z6e9M2JjtSTZ2huADH_FdEn1rb9IU-H35z_MLWW1M7vB5xc2rh57yFIBnQoxu9OLfeETpeIpASP1UBeZXoQZ_v1gIWiFYItwuudIz0tPYzB-o2duRe31G0S_hNvEGl3HH4M5FjTyaPAq2PRuiZCyRF-25gHXBZnLcxyavZ1VAURfHng_377_IJNSXsw; xq_r_token=8a5dec9c93caf88d0e1f98f1d23ea1bb60eb6225; snbim_minify=true; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1747905410; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGTrlGiGbtOh01qDsqze4GzDiLPGhDBWAFdYCdqtsqfmmxXxyB+doh6odserKO5sg=EiqfqztqpiexCPGnD0=O77N4xYAEDBYD74G+DDeDiO3Dj4GmDGYd=eDFzjRQyl2edxDwDB=DmqG23grDm4DfDDL5xRD4zC2YDDtDAMWz5PDADA3ooDDlYGO44Lr4DYp52nXWdOaspxTXzeDMixGXzYlCgaCRo0TQy9LAN32TNPGuDG=H6e0ahrbicn0AP4KGGwQ0imPKY+5meOQDqixGYwQGGiGGetGe3qqjeKYw10G4ixqim2mpbK+h1iaIPeQAieNS1X5pXZP4rQ04Iv4zmQWvplG40P4Gw4CqRjwzlwGjPwlD3iho+qKlD4hi3YD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGTrlGiGbtOh0P4DWhYebouIdHtBItz/DboqtwisfWD',
|
||||
'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; acw_tc=0a27a9dd17489230816243798e0070441d5e7160c0ed179607143a953db903; xq_a_token=ef79e6da376751a4bf6c1538103e9894d44473e1; xqat=ef79e6da376751a4bf6c1538103e9894d44473e1; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzUxNTE1MDgxLCJjdG0iOjE3NDg5MjMwODE2NDQsImNpZCI6ImQ5ZDBuNEFadXAifQ.gQrIt4VI73JLUFGVSTKpXidhFIMwlusBKyrzYwClwCBszXCooQY3WnFqlbXqSX3SwnMapuveOFUM5sGIOoZ8oDF8cZYs3HDz5vezR-2nes9gfZr2nZcUfZzNRJ299wlX3Zis5NbnzNlfnisUhv9GUfEZjQ_Rs37B4qRbQZVC2kdN1Z0xB8j1MplSTOsYj4IliQntuaTo-8SBh-4zz5244dnF85xREBVxtFzzCtHUhn9B-mzxE81_42nwrDscvow-4_jtlJXlqbehiAFxld-dCWDXwmCju9lRWu_WzdoQe19n-c6jhCZZ1pU1JGsYyhIAsd1gV064jQ6FxfN38so1Eg; xq_r_token=30a80318ebcabffbe194e7deecb108b665e8c894; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1748923088; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=b+jlfRtg2lC80dGHk9izZ9Od1QBbaKrdx1aAMbruXo2ULyhkygsXnhJoa7lOWNgnQAphRKw3864D5K+U2pTL5g%3D%3D; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0shqDyji2YsBGdTYKYUxGXxN4xiNDAc40iDC3WLPeUpx5h5o5Gmxt3qU6P5b48r89Y4sKs=BkpxKFTG4SQW4odeGLDY=DCTKKSMiD4b3Dt4DIDAYDDxDWm4DLDYoDY3uexGPo2mTNpm2bD0YDzqDgD7jbmeDEDG3D0bbetGDo1Q4DGqDSWZHTxD3Dffb4DDN4zIG0GmDDbrR=qmcbC=7O9Wtox0tWDBL5YvysdVC441TXpw8w7WaaxBQD7d9Q5na7fCW13rWkYY0Yeoe7hx+BxYrKch4SbKOAYY7hq7hR0D3E5YD5QADW0D/hQ7Emh07hiY7xdUginMzSTblushiee2YKbK5nYO0t3Ede7d46DqEQMA557QODdNG4WG+slx5bhWiDD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0shqDyji2YsBGdTYKY4xDfiOYiiBq4YDj44KWGfmoD/8okGxAeG/0Dt6Q7D6cGudn3qfM5QntBLc5pp/FFY4hly50hUr5qB2v45io/FQi4eD',
|
||||
'Referer': 'https://weibo.com/u/7735765253',
|
||||
'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
|
@ -79,7 +79,7 @@ MODEL_CONFIGS = {
|
|||
"base_url": "http://192.168.16.174:1234/v1/",
|
||||
"api_key": "none",
|
||||
"models": {
|
||||
"glm-4": "glm-4-32b-0414-abliterated",
|
||||
"GLM": "glm-4-32b-0414-abliterated",
|
||||
"qwen3": "qwen3-235b-a22b",
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,269 @@
|
|||
$(function() {
|
||||
// 1. 资金流向/融资融券/恐贪指数
|
||||
function fetchNorth() {
|
||||
$.get('/api/hsgt/northbound', function(res) {
|
||||
if(res.status === 'success') {
|
||||
renderNorthChart(res.data);
|
||||
} else {
|
||||
$('#northChart').html('<div style="color:#888;text-align:center;padding-top:40px;">交易日9点20采集,暂无数据或数据未更新</div>');
|
||||
}
|
||||
}).fail(function() {
|
||||
$('#northChart').html('<div style="color:#888;text-align:center;padding-top:40px;">交易日9点20采集,暂无数据或数据未更新</div>');
|
||||
});
|
||||
}
|
||||
function fetchSouth() {
|
||||
$.get('/api/hsgt/southbound', function(res) {
|
||||
if(res.status === 'success') {
|
||||
renderSouthChart(res.data);
|
||||
} else {
|
||||
$('#southChart').html('<div style="color:#888;text-align:center;padding-top:40px;">交易日9点20采集,暂无数据或数据未更新</div>');
|
||||
}
|
||||
}).fail(function() {
|
||||
$('#southChart').html('<div style="color:#888;text-align:center;padding-top:40px;">交易日9点20采集,暂无数据或数据未更新</div>');
|
||||
});
|
||||
}
|
||||
fetchNorth();
|
||||
fetchSouth();
|
||||
setInterval(fetchNorth, 2 * 60 * 1000);
|
||||
setInterval(fetchSouth, 2 * 60 * 1000);
|
||||
|
||||
$.get('/api/rzrq/chart_data?days=90', function(res) {
|
||||
if(res.status === 'success') renderRzrqChart(res.data);
|
||||
});
|
||||
$.get('/api/fear_greed/data?limit=180', function(res) {
|
||||
if(res.status === 'success') renderFearGreedChart(res.data);
|
||||
});
|
||||
|
||||
// 2. 概念PE和拥挤度
|
||||
const concepts = [
|
||||
{name: "先进封装", peId: "peChart_xjfz", crowdId: "crowdChart_xjfz"},
|
||||
{name: "芯片", peId: "peChart_xp", crowdId: "crowdChart_xp"},
|
||||
{name: "消费电子概念", peId: "peChart_xfdz", crowdId: "crowdChart_xfdz"},
|
||||
{name: "机器人概念", peId: "peChart_jqr", crowdId: "crowdChart_jqr"}
|
||||
];
|
||||
concepts.forEach(c => {
|
||||
$.get(`/api/industry/analysis?concept_name=${encodeURIComponent(c.name)}&metric=pe`, function(res) {
|
||||
if(res.status === 'success') {
|
||||
renderPEChart(c.peId, res.data);
|
||||
renderCrowdChart(c.crowdId, res.data.crowding);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// --- 渲染函数 ---
|
||||
const chartInstances = {};
|
||||
function renderNorthChart(data) {
|
||||
if(!data || !data.data || !data.times) return;
|
||||
const dom = document.getElementById('northChart');
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances['northChart'] = chart;
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {data: ['北向资金','沪股通','深股通'], top: 5, textStyle: {color:'#333'}},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: data.times, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', name: '亿元', axisLabel: {color:'#666'}},
|
||||
series: [
|
||||
{name: '北向资金', type: 'line', data: data.data.total, symbol: 'none', lineStyle:{width:2}},
|
||||
{name: '沪股通', type: 'line', data: data.data.sh, symbol: 'none'},
|
||||
{name: '深股通', type: 'line', data: data.data.sz, symbol: 'none'}
|
||||
]
|
||||
});
|
||||
// 只绑定容器的原生点击事件
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== 'northChart' && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function renderSouthChart(data) {
|
||||
if(!data || !data.data || !data.times) return;
|
||||
const dom = document.getElementById('southChart');
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances['southChart'] = chart;
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {data: ['南向资金','沪市港股通','深市港股通'], top: 5, textStyle: {color:'#333'}},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: data.times, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', name: '亿元', axisLabel: {color:'#666'}},
|
||||
series: [
|
||||
{name: '南向资金', type: 'line', data: data.data.total, symbol: 'none', lineStyle:{width:2}},
|
||||
{name: '沪市港股通', type: 'line', data: data.data.hk_sh, symbol: 'none'},
|
||||
{name: '深市港股通', type: 'line', data: data.data.hk_sz, symbol: 'none'}
|
||||
]
|
||||
});
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== 'southChart' && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function renderRzrqChart(data) {
|
||||
if(!data || !data.dates || !data.series) return;
|
||||
const dom = document.getElementById('rzrqChart');
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances['rzrqChart'] = chart;
|
||||
const s = data.series[0];
|
||||
let min = Math.min(...s.data.filter(v => v !== null && v !== undefined));
|
||||
min = Math.floor(min * 0.98);
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {data: [s.name], top: 5, textStyle: {color:'#333'}},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: data.dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', name: s.unit, axisLabel: {color:'#666'}, min: min},
|
||||
series: [{name: s.name, type: 'line', data: s.data, symbol: 'none', lineStyle:{width:2}}]
|
||||
});
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== 'rzrqChart' && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function renderFearGreedChart(data) {
|
||||
if(!data || !data.dates || !data.values) return;
|
||||
const dom = document.getElementById('fearGreedChart');
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances['fearGreedChart'] = chart;
|
||||
let min = Math.min(...data.values.filter(v => v !== null && v !== undefined));
|
||||
min = Math.floor(min * 0.98);
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {data: ['恐贪指数'], top: 5, textStyle: {color:'#333'}},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: data.dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', min: min, max: 100, axisLabel: {color:'#666'}},
|
||||
series: [{name: '恐贪指数', type: 'line', data: data.values, symbol: 'none', lineStyle:{width:2, color:'#f0ad4e'}}]
|
||||
});
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== 'fearGreedChart' && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function renderPEChart(domId, data) {
|
||||
if(!data || !data.series || !data.xAxis || !data.xAxis[0] || !data.xAxis[0].data) return;
|
||||
const dom = document.getElementById(domId);
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances[domId] = chart;
|
||||
const mainSeries = data.series.filter(s => s.name.indexOf('平均PE') !== -1 || s.name.indexOf('PE') !== -1);
|
||||
mainSeries.forEach(s => { s.symbol = 'none'; });
|
||||
let allValues = [];
|
||||
mainSeries.forEach(s => allValues = allValues.concat(s.data.filter(v => v !== null && v !== undefined)));
|
||||
let min = Math.min(...allValues);
|
||||
min = Math.floor(min * 0.98);
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {show: false},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: data.xAxis[0].data, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', name: data.yAxis[0].name, axisLabel: {color:'#666'}, min: min},
|
||||
series: mainSeries,
|
||||
dataZoom: [
|
||||
{type: 'inside', start: 0, end: 100, zoomOnTouch: true, moveOnMouseWheel: true}
|
||||
]
|
||||
});
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== domId && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
function renderCrowdChart(domId, crowding) {
|
||||
if(!crowding || !crowding.dates || !crowding.percentiles) return;
|
||||
// 展示近一年(240天)数据
|
||||
let dates = crowding.dates;
|
||||
let percentiles = crowding.percentiles;
|
||||
if(dates.length > 240) {
|
||||
dates = dates.slice(-240);
|
||||
percentiles = percentiles.slice(-240);
|
||||
}
|
||||
const dom = document.getElementById(domId);
|
||||
const chart = echarts.init(dom);
|
||||
chartInstances[domId] = chart;
|
||||
let min = Math.min(...percentiles.filter(v => v !== null && v !== undefined));
|
||||
min = Math.floor(min * 0.98);
|
||||
// 检查最后一个点是否需要高亮
|
||||
let markPoint = undefined;
|
||||
const lastVal = percentiles[percentiles.length-1];
|
||||
if(lastVal !== undefined && (lastVal > 80 || lastVal < 20)) {
|
||||
markPoint = {
|
||||
data: [{
|
||||
coord: [dates[dates.length-1], lastVal],
|
||||
symbol: 'circle',
|
||||
symbolSize: 16,
|
||||
itemStyle: {
|
||||
color: lastVal > 80 ? '#ff3333' : '#33cc33',
|
||||
shadowBlur: 20,
|
||||
shadowColor: lastVal > 80 ? '#ff3333' : '#33cc33',
|
||||
opacity: 1
|
||||
},
|
||||
label: {show: false},
|
||||
animation: true,
|
||||
animationDuration: 500,
|
||||
animationEasing: 'bounceOut',
|
||||
animationDurationUpdate: 500,
|
||||
animationEasingUpdate: 'bounceOut',
|
||||
effect: {
|
||||
show: true,
|
||||
period: 1,
|
||||
scaleSize: 2,
|
||||
color: lastVal > 80 ? '#ff3333' : '#33cc33',
|
||||
shadowBlur: 10
|
||||
}
|
||||
}]
|
||||
};
|
||||
}
|
||||
chart.setOption({
|
||||
title: {text: '', show: false},
|
||||
tooltip: {trigger: 'axis'},
|
||||
legend: {data: ['拥挤度历史百分位'], top: 5, textStyle: {color:'#333'}},
|
||||
grid: {left: '5%', right: '5%', top: 30, bottom: 20, containLabel: true},
|
||||
xAxis: {type: 'category', data: dates, axisLabel: {rotate: 0, color:'#666', interval: 'auto'}},
|
||||
yAxis: {type: 'value', min: min, max: 100, name: '百分位(%)', axisLabel: {color:'#666'}},
|
||||
series: [{
|
||||
name: '拥挤度历史百分位',
|
||||
type: 'line',
|
||||
data: percentiles,
|
||||
symbol: 'none',
|
||||
lineStyle:{width:2, color:'#ff7f50'},
|
||||
markPoint: markPoint
|
||||
}],
|
||||
dataZoom: [
|
||||
{type: 'inside', start: 0, end: 100, zoomOnTouch: true, moveOnMouseWheel: true}
|
||||
]
|
||||
});
|
||||
dom.onclick = function() {
|
||||
for(const k in chartInstances) {
|
||||
if(k !== domId && chartInstances[k]) {
|
||||
chartInstances[k].dispatchAction({ type: 'hideTip' });
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 统一resize自适应
|
||||
$(window).on('resize', function() {
|
||||
for(const key in chartInstances) {
|
||||
if(chartInstances[key] && chartInstances[key].resize) {
|
||||
chartInstances[key].resize();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
File diff suppressed because one or more lines are too long
|
@ -230,7 +230,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
? data.data.message
|
||||
: (data.message || '北向资金数据格式错误');
|
||||
showError('北向资金数据获取失败: ' + errorMessage);
|
||||
|
||||
// 显示空数据状态
|
||||
renderEmptyNorthboundChart();
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// 获取DOM元素
|
||||
const industryForm = document.getElementById('industryForm');
|
||||
const industryNameSelect = document.getElementById('industryName');
|
||||
const conceptNameSelect = document.getElementById('conceptName');
|
||||
const startDateInput = document.getElementById('startDate');
|
||||
const metricSelect = document.getElementById('metric');
|
||||
const showCrowdingCheckbox = document.getElementById('showCrowding');
|
||||
|
@ -28,6 +29,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// 初始化 - 加载行业列表
|
||||
loadIndustryList();
|
||||
// 加载概念板块列表
|
||||
loadConceptList();
|
||||
|
||||
// 监听表单提交事件
|
||||
industryForm.addEventListener('submit', function(event) {
|
||||
|
@ -113,6 +116,88 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载概念板块列表
|
||||
*/
|
||||
function loadConceptList() {
|
||||
showLoading(true);
|
||||
|
||||
fetch('/api/concept/list')
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(data => {
|
||||
throw new Error(data.message || '请求失败');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
// 填充概念板块下拉列表
|
||||
populateConceptSelect(data.data);
|
||||
} else {
|
||||
showError(data.message || '获取概念板块列表失败');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError(error.message || '请求失败,请检查网络连接');
|
||||
// 加载失败时使用硬编码的常见概念作为备用
|
||||
loadFallbackConcepts();
|
||||
})
|
||||
.finally(() => {
|
||||
showLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充概念板块下拉列表
|
||||
*/
|
||||
function populateConceptSelect(concepts) {
|
||||
// 清空选项(保留第一个默认选项)
|
||||
conceptNameSelect.innerHTML = '<option value="" selected disabled>请选择概念板块</option>';
|
||||
|
||||
// 排序概念列表(按名称)
|
||||
concepts.sort((a, b) => a.name.localeCompare(b.name, 'zh'));
|
||||
|
||||
// 添加概念选项
|
||||
concepts.forEach(concept => {
|
||||
const option = document.createElement('option');
|
||||
option.value = concept.name;
|
||||
option.textContent = concept.name;
|
||||
conceptNameSelect.appendChild(option);
|
||||
});
|
||||
|
||||
// 如果存在Select2,刷新它
|
||||
if ($.fn.select2 && $(conceptNameSelect).data('select2')) {
|
||||
$(conceptNameSelect).trigger('change');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载备用的概念板块数据(硬编码)
|
||||
*/
|
||||
function loadFallbackConcepts() {
|
||||
const commonConcepts = [
|
||||
"人工智能", "大数据", "云计算", "物联网", "5G", "新能源", "新材料",
|
||||
"生物医药", "半导体", "芯片", "消费电子", "智能汽车", "区块链",
|
||||
"虚拟现实", "元宇宙", "工业互联网", "智能制造", "网络安全", "数字经济"
|
||||
];
|
||||
|
||||
// 先清空下拉框(保留第一个选项)
|
||||
while (conceptNameSelect.options.length > 1) {
|
||||
conceptNameSelect.remove(1);
|
||||
}
|
||||
|
||||
// 添加选项
|
||||
commonConcepts.forEach(concept => {
|
||||
const option = new Option(concept, concept);
|
||||
conceptNameSelect.add(option);
|
||||
});
|
||||
|
||||
// 刷新Select2
|
||||
$(conceptNameSelect).trigger('change');
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析行业估值
|
||||
*/
|
||||
|
@ -126,11 +211,27 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
|
||||
// 获取表单数据
|
||||
const industryName = industryNameSelect.value;
|
||||
const conceptName = conceptNameSelect.value;
|
||||
const startDate = startDateInput.value;
|
||||
const metric = metricSelect.value;
|
||||
|
||||
// 检查是否至少选择了一个
|
||||
if (!industryName && !conceptName) {
|
||||
showError('请选择行业或概念板块');
|
||||
showLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
let url = `/api/industry/analysis?industry_name=${encodeURIComponent(industryName)}&metric=${metric}`;
|
||||
let url = `/api/industry/analysis?metric=${metric}`;
|
||||
|
||||
if (industryName) {
|
||||
url += `&industry_name=${encodeURIComponent(industryName)}`;
|
||||
}
|
||||
|
||||
if (conceptName) {
|
||||
url += `&concept_name=${encodeURIComponent(conceptName)}`;
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
url += `&start_date=${startDate}`;
|
||||
|
@ -742,6 +843,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// 重置Select2
|
||||
if ($.fn.select2) {
|
||||
$(industryNameSelect).val('').trigger('change');
|
||||
$(conceptNameSelect).val('').trigger('change');
|
||||
}
|
||||
|
||||
// 隐藏结果和错误信息
|
||||
|
|
|
@ -0,0 +1,237 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>资金与行业估值大屏</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<link rel="stylesheet" href="/static/css/bootstrap.min.css">
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
body {
|
||||
background: #f7f7fa;
|
||||
color: #222;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.container-fluid {
|
||||
min-height: 100vh;
|
||||
padding: 0;
|
||||
}
|
||||
.row.d-flex {
|
||||
height: 33vh;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.row.d-flex2 {
|
||||
height: 64vh;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
.col-3.d-flex {
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
border: 1.5px solid #c7c6c6;
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.chart-box {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
padding: 4px 4px 2px 4px;
|
||||
box-shadow: 0 2px 8px #e0e0e0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.tight-box {
|
||||
margin: 0;
|
||||
padding: 2px 2px 1px 2px;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
text-align: center;
|
||||
}
|
||||
.small-title {
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 2px;
|
||||
margin-top: 2px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
}
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
background: #f9f9fb;
|
||||
}
|
||||
|
||||
/* 平板响应式 */
|
||||
@media (max-width: 1200px) {
|
||||
.row.d-flex { height: auto; }
|
||||
.chart-box { height: auto; min-height: 180px; }
|
||||
}
|
||||
|
||||
/* 移动端响应式 */
|
||||
@media (max-width: 768px) {
|
||||
html, body {
|
||||
padding: 2px;
|
||||
}
|
||||
.container-fluid {
|
||||
padding: 0;
|
||||
}
|
||||
.row.d-flex, .row.d-flex2 {
|
||||
flex-direction: column !important;
|
||||
height: auto !important;
|
||||
margin: 0;
|
||||
}
|
||||
.col-3.d-flex {
|
||||
width: 100% !important;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
margin-bottom: 8px;
|
||||
padding: 1px 0;
|
||||
}
|
||||
.chart-box, .tight-box {
|
||||
min-height: 200px;
|
||||
height: auto !important;
|
||||
padding: 2px;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
.small-title {
|
||||
font-size: 0.8rem;
|
||||
margin-bottom: 1px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.chart-container {
|
||||
min-height: 180px;
|
||||
}
|
||||
/* 调整概念卡片的布局 */
|
||||
.tight-box .chart-container {
|
||||
min-height: 150px;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
/* 优化图表间距 */
|
||||
.tight-box .chart-title {
|
||||
padding: 1px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 小屏手机响应式 */
|
||||
@media (max-width: 480px) {
|
||||
.chart-box, .tight-box {
|
||||
min-height: 180px;
|
||||
}
|
||||
.chart-container {
|
||||
min-height: 160px;
|
||||
}
|
||||
.tight-box .chart-container {
|
||||
min-height: 130px;
|
||||
}
|
||||
.chart-title {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.small-title {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row d-flex">
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box w-100">
|
||||
<button id="fullscreen-btn" style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;border:none;background:transparent;"></button>
|
||||
<div class="chart-title">北向资金流向(实时数据)</div>
|
||||
<div id="northChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box w-100">
|
||||
<div class="chart-title">南向资金流向(实时数据)</div>
|
||||
<div id="southChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box w-100">
|
||||
<div class="chart-title">融资融券数据监控</div>
|
||||
<div id="rzrqChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box w-100">
|
||||
<div class="chart-title">市场恐贪指数</div>
|
||||
<div id="fearGreedChart" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row d-flex2">
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box tight-box w-100">
|
||||
<div class="chart-title small-title">先进封装-历史PE分析</div>
|
||||
<div id="peChart_xjfz" class="chart-container"></div>
|
||||
<div class="chart-title small-title" style="margin-top:2px;">先进封装-拥挤度</div>
|
||||
<div id="crowdChart_xjfz" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box tight-box w-100">
|
||||
<div class="chart-title small-title">芯片-历史PE分析</div>
|
||||
<div id="peChart_xp" class="chart-container"></div>
|
||||
<div class="chart-title small-title" style="margin-top:2px;">芯片-拥挤度</div>
|
||||
<div id="crowdChart_xp" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box tight-box w-100">
|
||||
<div class="chart-title small-title">消费电子概念-历史PE分析</div>
|
||||
<div id="peChart_xfdz" class="chart-container"></div>
|
||||
<div class="chart-title small-title" style="margin-top:2px;">消费电子概念-拥挤度</div>
|
||||
<div id="crowdChart_xfdz" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-3 d-flex">
|
||||
<div class="chart-box tight-box w-100">
|
||||
<div class="chart-title small-title">机器人概念-历史PE分析</div>
|
||||
<div id="peChart_jqr" class="chart-container"></div>
|
||||
<div class="chart-title small-title" style="margin-top:2px;">机器人概念-拥挤度</div>
|
||||
<div id="crowdChart_jqr" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/js/echarts.min.js"></script>
|
||||
<script src="/static/js/jquery.min.js"></script>
|
||||
<script src="/static/js/bigscreen.js"></script>
|
||||
<script>
|
||||
// document.getElementById('fullscreen-btn').onclick = function() {
|
||||
// function launchFullScreen(element) {
|
||||
// if(element.requestFullscreen) {
|
||||
// element.requestFullscreen();
|
||||
// } else if(element.mozRequestFullScreen) {
|
||||
// element.mozRequestFullScreen();
|
||||
// } else if(element.webkitRequestFullscreen) {
|
||||
// element.webkitRequestFullscreen();
|
||||
// } else if(element.msRequestFullscreen) {
|
||||
// element.msRequestFullscreen();
|
||||
// }
|
||||
// }
|
||||
// launchFullScreen(document.documentElement);
|
||||
// this.style.display = 'none'; // 全屏后隐藏按钮
|
||||
// };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -71,6 +71,14 @@
|
|||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="conceptName" class="form-label">概念板块</label>
|
||||
<select class="form-select select2" id="conceptName">
|
||||
<option value="" selected disabled>请选择概念板块</option>
|
||||
<!-- 将通过API动态填充 -->
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<label for="metric" class="form-label">估值指标</label>
|
||||
<select class="form-select" id="metric" required>
|
||||
|
@ -195,9 +203,47 @@
|
|||
width: '100%'
|
||||
});
|
||||
|
||||
// 初始化概念板块下拉框为可搜索
|
||||
$('#conceptName').select2({
|
||||
placeholder: '请选择概念板块',
|
||||
allowClear: true,
|
||||
width: '100%'
|
||||
});
|
||||
|
||||
// 行业和概念板块互斥选择逻辑
|
||||
$('#industryName').on('change', function() {
|
||||
if ($(this).val()) {
|
||||
// 如果选择了行业,禁用概念板块
|
||||
$('#conceptName').prop('disabled', true);
|
||||
// 同时更新Select2的状态
|
||||
$('#conceptName').select2({disabled: true});
|
||||
} else {
|
||||
// 如果清空了行业,启用概念板块
|
||||
$('#conceptName').prop('disabled', false);
|
||||
// 同时更新Select2的状态
|
||||
$('#conceptName').select2({disabled: false});
|
||||
}
|
||||
});
|
||||
|
||||
// 概念板块选择变化时
|
||||
$('#conceptName').on('change', function() {
|
||||
if ($(this).val()) {
|
||||
// 如果选择了概念,禁用行业
|
||||
$('#industryName').prop('disabled', true);
|
||||
// 同时更新Select2的状态
|
||||
$('#industryName').select2({disabled: true});
|
||||
} else {
|
||||
// 如果清空了概念,启用行业
|
||||
$('#industryName').prop('disabled', false);
|
||||
// 同时更新Select2的状态
|
||||
$('#industryName').select2({disabled: false});
|
||||
}
|
||||
});
|
||||
|
||||
// 重置表单时也需要重置Select2
|
||||
$('#resetBtn').on('click', function() {
|
||||
$('#industryName').val('').trigger('change');
|
||||
$('#conceptName').val('').trigger('change');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -26,6 +26,15 @@ MONGO_CONFIG = {
|
|||
'password': 'wlkj2018',
|
||||
'collection': 'wind_financial_analysis'
|
||||
}
|
||||
# MongoDB配置
|
||||
MONGO_CONFIG2 = {
|
||||
'host': '192.168.20.110',
|
||||
'port': 27017,
|
||||
'db': 'judge',
|
||||
'username': 'root',
|
||||
'password': 'wlkj2018',
|
||||
'collection': 'wind_financial_analysis'
|
||||
}
|
||||
|
||||
# 项目根目录
|
||||
ROOT_DIR = Path(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||||
|
|
|
@ -431,15 +431,108 @@ class EastmoneyRzrqCollector:
|
|||
df = self.fetch_data(page=1)
|
||||
|
||||
if not df.empty:
|
||||
# 保存数据到数据库
|
||||
if self.save_to_database(df):
|
||||
logger.info(f"成功更新最新数据,日期:{df.iloc[0]['trade_date']}")
|
||||
else:
|
||||
logger.error("更新最新数据失败")
|
||||
# 确保数据表存在
|
||||
if not self._ensure_table_exists():
|
||||
return False
|
||||
|
||||
# 处理第一页的所有数据
|
||||
success_count = 0
|
||||
total_count = len(df)
|
||||
|
||||
with self.engine.connect() as conn:
|
||||
for _, row in df.iterrows():
|
||||
# 将Series转换为dict,并处理nan值
|
||||
row_dict = {k: (None if pd.isna(v) else v) for k, v in row.items()}
|
||||
|
||||
# 检查该日期的数据是否已存在
|
||||
check_query = text("""
|
||||
SELECT COUNT(*) FROM eastmoney_rzrq_data WHERE trade_date = :trade_date
|
||||
""")
|
||||
result = conn.execute(check_query, {"trade_date": row_dict['trade_date']}).scalar()
|
||||
|
||||
if result > 0: # 数据已存在,执行更新
|
||||
update_query = text("""
|
||||
UPDATE eastmoney_rzrq_data SET
|
||||
index_value = :index_value,
|
||||
change_percent = :change_percent,
|
||||
float_market_value = :float_market_value,
|
||||
change_percent_3d = :change_percent_3d,
|
||||
change_percent_5d = :change_percent_5d,
|
||||
change_percent_10d = :change_percent_10d,
|
||||
financing_balance = :financing_balance,
|
||||
financing_balance_ratio = :financing_balance_ratio,
|
||||
financing_buy_amount = :financing_buy_amount,
|
||||
financing_buy_amount_3d = :financing_buy_amount_3d,
|
||||
financing_buy_amount_5d = :financing_buy_amount_5d,
|
||||
financing_buy_amount_10d = :financing_buy_amount_10d,
|
||||
financing_repay_amount = :financing_repay_amount,
|
||||
financing_repay_amount_3d = :financing_repay_amount_3d,
|
||||
financing_repay_amount_5d = :financing_repay_amount_5d,
|
||||
financing_repay_amount_10d = :financing_repay_amount_10d,
|
||||
financing_net_amount = :financing_net_amount,
|
||||
financing_net_amount_3d = :financing_net_amount_3d,
|
||||
financing_net_amount_5d = :financing_net_amount_5d,
|
||||
financing_net_amount_10d = :financing_net_amount_10d,
|
||||
securities_balance = :securities_balance,
|
||||
securities_volume = :securities_volume,
|
||||
securities_repay_volume = :securities_repay_volume,
|
||||
securities_repay_volume_3d = :securities_repay_volume_3d,
|
||||
securities_repay_volume_5d = :securities_repay_volume_5d,
|
||||
securities_repay_volume_10d = :securities_repay_volume_10d,
|
||||
securities_sell_volume = :securities_sell_volume,
|
||||
securities_sell_volume_3d = :securities_sell_volume_3d,
|
||||
securities_sell_volume_5d = :securities_sell_volume_5d,
|
||||
securities_sell_volume_10d = :securities_sell_volume_10d,
|
||||
securities_net_volume = :securities_net_volume,
|
||||
securities_net_volume_3d = :securities_net_volume_3d,
|
||||
securities_net_volume_5d = :securities_net_volume_5d,
|
||||
securities_net_volume_10d = :securities_net_volume_10d,
|
||||
total_rzrq_balance = :total_rzrq_balance,
|
||||
total_rzrq_balance_cz = :total_rzrq_balance_cz
|
||||
WHERE trade_date = :trade_date
|
||||
""")
|
||||
conn.execute(update_query, row_dict)
|
||||
logger.info(f"更新数据成功,日期:{row_dict['trade_date']}")
|
||||
else: # 数据不存在,执行插入
|
||||
insert_query = text("""
|
||||
INSERT INTO eastmoney_rzrq_data (
|
||||
trade_date, index_value, change_percent, float_market_value,
|
||||
change_percent_3d, change_percent_5d, change_percent_10d,
|
||||
financing_balance, financing_balance_ratio,
|
||||
financing_buy_amount, financing_buy_amount_3d, financing_buy_amount_5d, financing_buy_amount_10d,
|
||||
financing_repay_amount, financing_repay_amount_3d, financing_repay_amount_5d, financing_repay_amount_10d,
|
||||
financing_net_amount, financing_net_amount_3d, financing_net_amount_5d, financing_net_amount_10d,
|
||||
securities_balance, securities_volume,
|
||||
securities_repay_volume, securities_repay_volume_3d, securities_repay_volume_5d, securities_repay_volume_10d,
|
||||
securities_sell_volume, securities_sell_volume_3d, securities_sell_volume_5d, securities_sell_volume_10d,
|
||||
securities_net_volume, securities_net_volume_3d, securities_net_volume_5d, securities_net_volume_10d,
|
||||
total_rzrq_balance, total_rzrq_balance_cz
|
||||
) VALUES (
|
||||
:trade_date, :index_value, :change_percent, :float_market_value,
|
||||
:change_percent_3d, :change_percent_5d, :change_percent_10d,
|
||||
:financing_balance, :financing_balance_ratio,
|
||||
:financing_buy_amount, :financing_buy_amount_3d, :financing_buy_amount_5d, :financing_buy_amount_10d,
|
||||
:financing_repay_amount, :financing_repay_amount_3d, :financing_repay_amount_5d, :financing_repay_amount_10d,
|
||||
:financing_net_amount, :financing_net_amount_3d, :financing_net_amount_5d, :financing_net_amount_10d,
|
||||
:securities_balance, :securities_volume,
|
||||
:securities_repay_volume, :securities_repay_volume_3d, :securities_repay_volume_5d, :securities_repay_volume_10d,
|
||||
:securities_sell_volume, :securities_sell_volume_3d, :securities_sell_volume_5d, :securities_sell_volume_10d,
|
||||
:securities_net_volume, :securities_net_volume_3d, :securities_net_volume_5d, :securities_net_volume_10d,
|
||||
:total_rzrq_balance, :total_rzrq_balance_cz
|
||||
)
|
||||
""")
|
||||
conn.execute(insert_query, row_dict)
|
||||
logger.info(f"插入新数据成功,日期:{row_dict['trade_date']}")
|
||||
|
||||
success_count += 1
|
||||
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"数据处理完成:成功处理 {success_count}/{total_count} 条记录")
|
||||
return success_count > 0
|
||||
else:
|
||||
logger.warning("未获取到最新数据")
|
||||
|
||||
return True
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"每日更新任务采集失败: {e}")
|
||||
|
|
|
@ -12,8 +12,9 @@ import logging
|
|||
from typing import Dict, List, Optional, Union, Tuple
|
||||
import json
|
||||
import requests
|
||||
import redis
|
||||
|
||||
from .config import DB_URL, MONGO_CONFIG, LOG_FILE
|
||||
from .config import DB_URL, MONGO_CONFIG,MONGO_CONFIG2, LOG_FILE
|
||||
from .stock_price_collector import StockPriceCollector
|
||||
from .industry_analysis import IndustryAnalyzer
|
||||
|
||||
|
@ -28,6 +29,16 @@ logging.basicConfig(
|
|||
)
|
||||
logger = logging.getLogger("financial_analysis")
|
||||
|
||||
# 初始化Redis连接
|
||||
redis_client = redis.Redis(
|
||||
host='192.168.18.208', # Redis服务器地址,根据实际情况调整
|
||||
port=6379,
|
||||
password='wlkj2018',
|
||||
db=13,
|
||||
socket_timeout=5,
|
||||
decode_responses=True
|
||||
)
|
||||
|
||||
class FinancialAnalyzer:
|
||||
"""财务分析器类"""
|
||||
|
||||
|
@ -48,6 +59,13 @@ class FinancialAnalyzer:
|
|||
username=MONGO_CONFIG['username'],
|
||||
password=MONGO_CONFIG['password']
|
||||
)
|
||||
# 初始化MongoDB连接
|
||||
self.mongo_client2 = MongoClient(
|
||||
host=MONGO_CONFIG2['host'],
|
||||
port=MONGO_CONFIG2['port'],
|
||||
username=MONGO_CONFIG2['username'],
|
||||
password=MONGO_CONFIG2['password']
|
||||
)
|
||||
self.mongo_db = self.mongo_client[MONGO_CONFIG['db']]
|
||||
self.mongo_collection = self.mongo_db[MONGO_CONFIG['collection']]
|
||||
self.wacc_collection = self.mongo_db['wind_stock_wacc_roic']
|
||||
|
@ -97,12 +115,14 @@ class FinancialAnalyzer:
|
|||
logger.error(f"计算增长变化率失败: {str(e)}")
|
||||
return None
|
||||
|
||||
def get_growth_change_indicators(self, stock_code: str) -> Dict:
|
||||
def get_growth_change_indicators(self, stock_code: str, current_year: str = '2024-12-31', previous_year: str = '2023-12-31') -> Dict:
|
||||
"""
|
||||
获取增长指标的变化率
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
current_year: 当前年份,格式为'YYYY-12-31',默认为'2024-12-31'
|
||||
previous_year: 上一年份,格式为'YYYY-12-31',默认为'2023-12-31'
|
||||
|
||||
Returns:
|
||||
包含增长指标变化率的字典
|
||||
|
@ -116,39 +136,39 @@ class FinancialAnalyzer:
|
|||
'message': f'未找到股票 {stock_code} 的财务数据'
|
||||
}
|
||||
|
||||
# 获取2023-12-31和2024-12-31的数据
|
||||
wind_data_2023 = None
|
||||
wind_data_2024 = None
|
||||
# 获取指定年份的数据
|
||||
wind_data_current = None
|
||||
wind_data_previous = None
|
||||
|
||||
for data in record['wind_data']:
|
||||
if data['time'] == '2023-12-31':
|
||||
wind_data_2023 = data
|
||||
elif data['time'] == '2024-12-31':
|
||||
wind_data_2024 = data
|
||||
if data['time'] == current_year:
|
||||
wind_data_current = data
|
||||
elif data['time'] == previous_year:
|
||||
wind_data_previous = data
|
||||
|
||||
if not wind_data_2023 or not wind_data_2024:
|
||||
if not wind_data_current or not wind_data_previous:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'未找到股票 {stock_code} 的2023或2024年财务数据'
|
||||
'message': f'未找到股票 {stock_code} 的{current_year}或{previous_year}年财务数据'
|
||||
}
|
||||
|
||||
# 获取两个时间点的指标
|
||||
indicators_2023 = self._get_growth_indicators(wind_data_2023)
|
||||
indicators_2024 = self._get_growth_indicators(wind_data_2024)
|
||||
indicators_current = self._get_growth_indicators(wind_data_current)
|
||||
indicators_previous = self._get_growth_indicators(wind_data_previous)
|
||||
|
||||
# 计算变化率
|
||||
growth_changes = {}
|
||||
for key in indicators_2023.keys():
|
||||
current_value = indicators_2024.get(key)
|
||||
previous_value = indicators_2023.get(key)
|
||||
for key in indicators_previous.keys():
|
||||
current_value = indicators_current.get(key)
|
||||
previous_value = indicators_previous.get(key)
|
||||
growth_changes[key] = self._calculate_growth_change(current_value, previous_value)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'stock_code': stock_code,
|
||||
'data': {
|
||||
'indicators_2023': indicators_2023,
|
||||
'indicators_2024': indicators_2024,
|
||||
f'indicators_{previous_year}': indicators_previous,
|
||||
f'indicators_{current_year}': indicators_current,
|
||||
'growth_changes': growth_changes
|
||||
}
|
||||
}
|
||||
|
@ -178,7 +198,7 @@ class FinancialAnalyzer:
|
|||
})
|
||||
|
||||
if not record:
|
||||
logger.warning(f"未找到股票 {stock_code} 的WACC数据")
|
||||
# logger.warning(f"未找到股票 {stock_code} 的WACC数据")
|
||||
return None
|
||||
|
||||
return record['wacc']
|
||||
|
@ -214,13 +234,13 @@ class FinancialAnalyzer:
|
|||
logger.error(f"计算盈利年数失败: {str(e)}")
|
||||
return 0
|
||||
|
||||
def extract_financial_indicators(self, stock_code: str) -> Dict:
|
||||
def extract_financial_indicators(self, stock_code: str, year: str = None) -> Dict:
|
||||
"""
|
||||
从MongoDB中提取指定的财务指标
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
year: 财报年份(可选,格式'YYYY-12-31'),如不传则取最新
|
||||
Returns:
|
||||
包含财务指标的字典
|
||||
"""
|
||||
|
@ -232,13 +252,18 @@ class FinancialAnalyzer:
|
|||
'success': False,
|
||||
'message': f'未找到股票 {stock_code} 的财务数据'
|
||||
}
|
||||
|
||||
# 获取最新的财务数据(按时间排序)
|
||||
wind_data = sorted(record['wind_data'], key=lambda x: x['time'], reverse=True)[0]
|
||||
|
||||
# 获取指定年份的财务数据(按时间排序)
|
||||
if year:
|
||||
wind_data = next((x for x in record['wind_data'] if x['time'] == year), None)
|
||||
if not wind_data:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'未找到股票 {stock_code} 的{year}年财务数据'
|
||||
}
|
||||
else:
|
||||
wind_data = sorted(record['wind_data'], key=lambda x: x['time'], reverse=True)[0]
|
||||
# 获取WACC数据
|
||||
wacc = self.get_wacc_data(stock_code)
|
||||
|
||||
# 计算近五年盈利年数
|
||||
profit_years = self._calculate_profit_years(record['wind_data'])
|
||||
|
||||
|
@ -458,17 +483,34 @@ class FinancialAnalyzer:
|
|||
'message': f'获取动量指标失败: {str(e)}'
|
||||
}
|
||||
|
||||
def analyze_financial_data(self, stock_code: str) -> Dict:
|
||||
def analyze_financial_data(self, stock_code: str, force_update: bool = False, current_year: str = '2024-12-31', previous_year: str = '2023-12-31') -> Dict:
|
||||
"""
|
||||
分析财务数据
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
force_update: 是否强制更新缓存,默认为False
|
||||
current_year: 当前年份,格式为'YYYY-12-31',默认为'2024-12-31'
|
||||
previous_year: 上一年份,格式为'YYYY-12-31',默认为'2023-12-31'
|
||||
|
||||
Returns:
|
||||
分析结果字典,包含所有财务指标及其排名得分
|
||||
"""
|
||||
try:
|
||||
cache_key = f"financial_analysis:{stock_code}:{current_year}"
|
||||
# 检查缓存
|
||||
if not force_update:
|
||||
|
||||
cached_data = redis_client.get(cache_key)
|
||||
if cached_data:
|
||||
try:
|
||||
# 尝试解析缓存的JSON数据
|
||||
result = json.loads(cached_data)
|
||||
logger.info(f"从缓存获取股票 {stock_code} 的财务分析数据")
|
||||
return result
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"解析缓存的财务分析数据失败,将重新查询: {cache_error}")
|
||||
|
||||
# 获取股票价格数据
|
||||
price_collector = StockPriceCollector()
|
||||
price_data = price_collector.get_stock_price_data(stock_code)
|
||||
|
@ -484,21 +526,45 @@ class FinancialAnalyzer:
|
|||
momentum_result = self.get_momentum_indicators(stock_code, industry_stocks)
|
||||
|
||||
# 获取基础财务指标
|
||||
base_result = self.extract_financial_indicators(stock_code)
|
||||
base_result = self.extract_financial_indicators(stock_code, current_year)
|
||||
if not base_result.get('success'):
|
||||
return base_result
|
||||
|
||||
# 获取增长指标变化
|
||||
growth_result = self.get_growth_change_indicators(stock_code)
|
||||
growth_result = self.get_growth_change_indicators(stock_code, current_year, previous_year)
|
||||
if not growth_result.get('success'):
|
||||
return growth_result
|
||||
|
||||
# 获取行业排名
|
||||
rank_result = self.calculate_industry_rankings(stock_code)
|
||||
rank_result = self.calculate_industry_rankings(stock_code, current_year, previous_year)
|
||||
if not rank_result.get('success'):
|
||||
return rank_result
|
||||
|
||||
# 定义指标说明映射
|
||||
# 获取指定年份的数据
|
||||
record = self.mongo_collection.find_one({'code': stock_code})
|
||||
if not record or 'wind_data' not in record:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'未找到股票 {stock_code} 的财务数据'
|
||||
}
|
||||
|
||||
# 获取指定年份的数据
|
||||
wind_data_current = None
|
||||
wind_data_previous = None
|
||||
|
||||
for data in record['wind_data']:
|
||||
if data['time'] == current_year:
|
||||
wind_data_current = data
|
||||
elif data['time'] == previous_year:
|
||||
wind_data_previous = data
|
||||
|
||||
if not wind_data_current or not wind_data_previous:
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'未找到股票 {stock_code} 的{current_year}或{previous_year}年财务数据'
|
||||
}
|
||||
|
||||
# 定义指标说明映射(前端显示用)
|
||||
indicator_descriptions = {
|
||||
# 财务实力指标
|
||||
'debt_equity_ratio': '债务股本比率',
|
||||
|
@ -545,10 +611,145 @@ class FinancialAnalyzer:
|
|||
'cash_ratio': '现金比率'
|
||||
}
|
||||
|
||||
# 定义MongoDB字段映射(用于从MongoDB获取数据)
|
||||
mongo_field_mapping = {
|
||||
# 偿债能力指标
|
||||
'debt_equity_ratio': ('solvency', '产权比率'),
|
||||
'debt_ebitda_ratio': ('solvency', '全部债务/EBITDA'),
|
||||
'interest_coverage_ratio': ('solvency', '已获利息倍数(EBIT/利息费用)'),
|
||||
'current_ratio': ('solvency', '流动比率'),
|
||||
'quick_ratio': ('solvency', '速动比率'),
|
||||
'cash_ratio': ('solvency', '现金比率'),
|
||||
'cash_to_debt_ratio': ('solvency', '经营活动产生的现金流量净额/负债合计'),
|
||||
|
||||
# 资本结构指标
|
||||
'equity_ratio': ('capitalStructure', '股东权益比'),
|
||||
|
||||
# 成长能力指标
|
||||
'diluted_eps_growth': ('grows', '稀释每股收益(同比增长率)'),
|
||||
'operating_cash_flow_per_share_growth': ('grows', '每股经营活动产生的现金流量净额(同比增长率)'),
|
||||
'revenue_growth': ('grows', '营业收入(同比增长率)'),
|
||||
'operating_profit_growth': ('grows', '营业利润(同比增长率)'),
|
||||
'net_profit_growth_excl_nonrecurring': ('grows', '归属母公司股东的净利润-扣除非经常性损益(同比增长率)'),
|
||||
'operating_cash_flow_growth': ('grows', '经营活动产生的现金流量净额(同比增长率)'),
|
||||
'rd_expense_growth': ('grows', '研发费用同比增长'),
|
||||
'roe_growth': ('grows', '净资产收益率(摊薄)(同比增长)'),
|
||||
|
||||
# Z值相关指标
|
||||
'working_capital_to_assets': ('ZValue', '营运资本/总资产'),
|
||||
'retained_earnings_to_assets': ('ZValue', '留存收益/总资产'),
|
||||
'ebit_to_assets': ('ZValue', '息税前利润(TTM)/总资产'),
|
||||
'market_value_to_liabilities': ('ZValue', '当日总市值/负债总计'),
|
||||
'equity_to_liabilities': ('ZValue', '股东权益合计(含少数)/负债总计'),
|
||||
'revenue_to_assets': ('ZValue', '营业收入/总资产'),
|
||||
'z_score': ('ZValue', 'Z值'),
|
||||
|
||||
# 营运能力指标
|
||||
'inventory_turnover_days': ('operatingCapacity', '存货周转天数'),
|
||||
'receivables_turnover_days': ('operatingCapacity', '应收账款周转天数'),
|
||||
'payables_turnover_days': ('operatingCapacity', '应付账款周转天数'),
|
||||
|
||||
# 盈利能力指标
|
||||
'gross_profit_margin': ('profitability', '销售毛利率'),
|
||||
'operating_profit_margin': ('profitability', '营业利润/营业总收入'),
|
||||
'net_profit_margin': ('profitability', '销售净利率'),
|
||||
'roe': ('profitability', '净资产收益率ROE(平均)'),
|
||||
'roa': ('profitability', '总资产净利率ROA'),
|
||||
'roic': ('profitability', '投入资本回报率ROIC')
|
||||
}
|
||||
|
||||
# 定义指标是否越高越好
|
||||
higher_better_indicators = {
|
||||
# 偿债能力指标
|
||||
'current_ratio': True,
|
||||
'quick_ratio': True,
|
||||
'cash_ratio': True,
|
||||
'interest_coverage_ratio': True,
|
||||
|
||||
# 资本结构指标
|
||||
'equity_ratio': True,
|
||||
|
||||
# 盈利能力指标
|
||||
'profit_years': True,
|
||||
|
||||
# 成长能力指标
|
||||
'diluted_eps_growth': True,
|
||||
'operating_cash_flow_per_share_growth': True,
|
||||
'revenue_growth': True,
|
||||
'operating_profit_growth': True,
|
||||
'net_profit_growth_excl_nonrecurring': True,
|
||||
'operating_cash_flow_growth': True,
|
||||
'rd_expense_growth': True,
|
||||
'roe_growth': True,
|
||||
|
||||
# Z值相关指标
|
||||
'working_capital_to_assets': True,
|
||||
'retained_earnings_to_assets': True,
|
||||
'ebit_to_assets': True,
|
||||
'market_value_to_liabilities': True,
|
||||
'equity_to_liabilities': True,
|
||||
'revenue_to_assets': True,
|
||||
'z_score': True,
|
||||
|
||||
# 营运能力指标
|
||||
'inventory_turnover_days': False,
|
||||
'receivables_turnover_days': False,
|
||||
'payables_turnover_days': False,
|
||||
|
||||
# 盈利能力指标
|
||||
'gross_profit_margin': True,
|
||||
'operating_profit_margin': True,
|
||||
'net_profit_margin': True,
|
||||
'roe': True,
|
||||
'roa': True,
|
||||
'roic': True,
|
||||
|
||||
# WACC数据
|
||||
'wacc': False
|
||||
}
|
||||
|
||||
# 构建基础指标数据
|
||||
base_indicators = base_result.get('indicators', {})
|
||||
rankings = rank_result.get('rankings', {})
|
||||
|
||||
def get_indicator_value(data: Dict, key: str) -> Optional[float]:
|
||||
"""获取指定指标的值"""
|
||||
if key not in mongo_field_mapping:
|
||||
return None
|
||||
|
||||
category, meaning = mongo_field_mapping[key]
|
||||
if category not in data:
|
||||
return None
|
||||
|
||||
for item in data[category]['list']:
|
||||
if item['meaning'] == meaning:
|
||||
return item['data']
|
||||
return None
|
||||
|
||||
def calculate_change(current: Optional[float], previous: Optional[float], is_higher_better: bool) -> Dict:
|
||||
"""计算指标变化情况"""
|
||||
if current is None or previous is None:
|
||||
return {
|
||||
'change': None,
|
||||
'change_rate': None,
|
||||
'is_better': None
|
||||
}
|
||||
|
||||
change = current - previous
|
||||
change_rate = (change / abs(previous)) * 100 if previous != 0 else None
|
||||
|
||||
# 判断是否变好
|
||||
if is_higher_better:
|
||||
is_better = change > 0
|
||||
else:
|
||||
is_better = change < 0
|
||||
|
||||
return {
|
||||
'change': round(change, 3),
|
||||
'change_rate': round(change_rate, 2) if change_rate is not None else None,
|
||||
'is_better': is_better
|
||||
}
|
||||
|
||||
# 定义各板块的指标列表
|
||||
financial_strength_indicators = [
|
||||
'debt_equity_ratio', 'debt_ebitda_ratio', 'interest_coverage_ratio',
|
||||
|
@ -578,7 +779,6 @@ class FinancialAnalyzer:
|
|||
'payables_turnover_days', 'current_ratio', 'quick_ratio', 'cash_ratio'
|
||||
]
|
||||
|
||||
# 处理各板块指标
|
||||
def process_indicators(indicator_list):
|
||||
result = []
|
||||
total_score = 0
|
||||
|
@ -586,6 +786,13 @@ class FinancialAnalyzer:
|
|||
|
||||
for key in indicator_list:
|
||||
if key in base_indicators:
|
||||
current_value = base_indicators[key]
|
||||
previous_value = get_indicator_value(wind_data_previous, key)
|
||||
|
||||
# 计算变化情况
|
||||
is_higher_better = higher_better_indicators.get(key, True)
|
||||
change_info = calculate_change(current_value, previous_value, is_higher_better)
|
||||
|
||||
rank_score = rankings.get(key, 0)
|
||||
if rank_score is not None:
|
||||
total_score += rank_score
|
||||
|
@ -594,7 +801,11 @@ class FinancialAnalyzer:
|
|||
result.append({
|
||||
'key': key,
|
||||
'desc': indicator_descriptions.get(key, key),
|
||||
'value': base_indicators[key],
|
||||
'value': current_value,
|
||||
'previous_value': previous_value,
|
||||
'change': change_info['change'],
|
||||
'change_rate': change_info['change_rate'],
|
||||
'is_better': change_info['is_better'],
|
||||
'rank_score': rank_score
|
||||
})
|
||||
|
||||
|
@ -606,6 +817,12 @@ class FinancialAnalyzer:
|
|||
'avg_score': avg_score
|
||||
}
|
||||
|
||||
# 处理各板块指标
|
||||
financial_strength = process_indicators(financial_strength_indicators)
|
||||
profitability = process_indicators(profitability_indicators)
|
||||
value_rating = process_indicators(value_rating_indicators)
|
||||
liquidity = process_indicators(liquidity_indicators)
|
||||
|
||||
# 处理增长指标变化
|
||||
growth_changes = growth_result.get('data', {}).get('growth_changes', {})
|
||||
growth_changes_list = []
|
||||
|
@ -639,20 +856,38 @@ class FinancialAnalyzer:
|
|||
'avg_score': growth_avg_score
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
industryList = industry_analyzer.get_stock_industry(stock_code)
|
||||
concepts += industryList
|
||||
|
||||
# 在返回结果之前,缓存数据
|
||||
result = {
|
||||
'success': True,
|
||||
'stock_code': stock_code,
|
||||
'data_time': base_indicators.get('data_time'),
|
||||
'financial_strength': process_indicators(financial_strength_indicators),
|
||||
'profitability': process_indicators(profitability_indicators),
|
||||
'financial_strength': financial_strength,
|
||||
'profitability': profitability,
|
||||
'growth': growth_data,
|
||||
'value_rating': process_indicators(value_rating_indicators),
|
||||
'liquidity': process_indicators(liquidity_indicators),
|
||||
'momentum': momentum_result.get('indicators', []), # 添加动量指标数据
|
||||
'concepts': concepts, # 添加概念板块数据
|
||||
'price_data': price_data # 添加实时股价数据
|
||||
'value_rating': value_rating,
|
||||
'liquidity': liquidity,
|
||||
'momentum': momentum_result.get('indicators', []),
|
||||
'concepts': concepts,
|
||||
'price_data': price_data
|
||||
}
|
||||
|
||||
# 缓存结果,有效期1天(86400秒)
|
||||
try:
|
||||
redis_client.set(
|
||||
cache_key,
|
||||
json.dumps(result, default=str), # 使用default=str处理日期等特殊类型
|
||||
ex=86400 # 1天的秒数
|
||||
)
|
||||
logger.info(f"已缓存股票 {stock_code} 的财务分析数据,有效期为1天")
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"缓存财务分析数据失败: {cache_error}")
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"分析财务数据失败: {str(e)}")
|
||||
return {
|
||||
|
@ -805,13 +1040,14 @@ class FinancialAnalyzer:
|
|||
logger.error(f"获取行业指标失败: {str(e)}")
|
||||
return {}
|
||||
|
||||
def calculate_industry_rankings(self, stock_code: str) -> Dict:
|
||||
def calculate_industry_rankings(self, stock_code: str, current_year: str = '2024-12-31', previous_year: str = '2023-12-31') -> Dict:
|
||||
"""
|
||||
计算公司在行业中的排名得分
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
current_year: 当前年份,格式为'YYYY-12-31',默认为'2024-12-31'
|
||||
previous_year: 上一年份,格式为'YYYY-12-31',默认为'2023-12-31'
|
||||
Returns:
|
||||
包含所有指标排名得分的字典
|
||||
"""
|
||||
|
@ -825,22 +1061,32 @@ class FinancialAnalyzer:
|
|||
}
|
||||
|
||||
# 获取当前公司的指标
|
||||
current_result = self.extract_financial_indicators(stock_code)
|
||||
current_result = self.extract_financial_indicators(stock_code, year=current_year)
|
||||
if not current_result.get('success'):
|
||||
return current_result
|
||||
|
||||
# 获取当前公司的增长指标变化
|
||||
current_growth_result = self.get_growth_change_indicators(stock_code)
|
||||
current_growth_result = self.get_growth_change_indicators(stock_code, current_year, previous_year)
|
||||
if not current_growth_result.get('success'):
|
||||
return current_growth_result
|
||||
|
||||
# 获取行业所有公司的指标
|
||||
industry_indicators = self._get_industry_indicators(stock_list)
|
||||
industry_indicators = {}
|
||||
for stock in stock_list:
|
||||
result = self.extract_financial_indicators(stock, year=current_year)
|
||||
if not result.get('success'):
|
||||
continue
|
||||
indicators = result.get('indicators', {})
|
||||
for key, value in indicators.items():
|
||||
if key != 'data_time':
|
||||
if key not in industry_indicators:
|
||||
industry_indicators[key] = []
|
||||
industry_indicators[key].append(value)
|
||||
|
||||
# 获取行业所有公司的增长指标变化
|
||||
industry_growth_indicators = {}
|
||||
for stock in stock_list:
|
||||
growth_result = self.get_growth_change_indicators(stock)
|
||||
growth_result = self.get_growth_change_indicators(stock, current_year, previous_year)
|
||||
if growth_result.get('success'):
|
||||
growth_changes = growth_result.get('data', {}).get('growth_changes', {})
|
||||
for key, value in growth_changes.items():
|
||||
|
@ -937,4 +1183,46 @@ class FinancialAnalyzer:
|
|||
return {
|
||||
'success': False,
|
||||
'message': f'计算行业排名失败: {str(e)}'
|
||||
}
|
||||
}
|
||||
|
||||
def get_pep_stock_info_by_shortname(self, short_name: str) -> dict:
|
||||
"""
|
||||
根据股票简称从pep_stock_info集合获取对应的全部字段数据
|
||||
Args:
|
||||
short_name: 股票简称
|
||||
Returns:
|
||||
查询到的文档(dict),未找到则返回{'success': False, 'message': ...}
|
||||
"""
|
||||
try:
|
||||
pep_collection = self.mongo_client[MONGO_CONFIG2['db']]['pep_stock_info']
|
||||
record = pep_collection.find_one({'shortName': short_name})
|
||||
if not record:
|
||||
return {'success': False, 'message': f'未找到简称为{short_name}的股票数据'}
|
||||
if '_id' in record:
|
||||
record['_id'] = str(record['_id']) # 转为字符串,便于JSON序列化
|
||||
record['success'] = True
|
||||
return record
|
||||
except Exception as e:
|
||||
logger.error(f"查询pep_stock_info失败: {str(e)}")
|
||||
return {'success': False, 'message': f'查询pep_stock_info失败: {str(e)}'}
|
||||
|
||||
def get_pep_stock_info_by_code(self, short_code: str) -> dict:
|
||||
"""
|
||||
根据股票简称从pep_stock_info集合获取对 应的全部字段数据
|
||||
Args:
|
||||
short_code: 股票简称
|
||||
Returns:
|
||||
查询到的文档(dict),未找到则返回{'success': False, 'message': ...}
|
||||
"""
|
||||
try:
|
||||
pep_collection = self.mongo_client2[MONGO_CONFIG2['db']]['pep_stock_info']
|
||||
record = pep_collection.find_one({'code': short_code})
|
||||
if not record:
|
||||
return {'success': False, 'message': f'未找到简称为{short_code}的股票数据'}
|
||||
if '_id' in record:
|
||||
record['_id'] = str(record['_id']) # 转为字符串,便于JSON序列化
|
||||
record['success'] = True
|
||||
return record
|
||||
except Exception as e:
|
||||
logger.error(f"查询pep_stock_info失败: {str(e)}")
|
||||
return {'success': False, 'message': f'查询pep_stock_info失败: {str(e)}'}
|
|
@ -274,56 +274,56 @@ class IndustryAnalyzer:
|
|||
logger.info(f"计算行业 {metric} 分位数完成: 当前{metric}={result['current']:.2f}, 百分位={result['percentile']:.2f}%")
|
||||
return result
|
||||
|
||||
def get_industry_crowding_index(self, industry_name: str, start_date: str = None, end_date: str = None, use_cache: bool = True) -> pd.DataFrame:
|
||||
def get_industry_crowding_index(self, name: str, start_date: str = None, end_date: str = None, use_cache: bool = True, is_concept: bool = False) -> pd.DataFrame:
|
||||
"""
|
||||
计算行业交易拥挤度指标,并使用Redis缓存结果
|
||||
计算行业/概念板块交易拥挤度指标,并使用Redis缓存结果
|
||||
|
||||
对于拥挤度指标,固定使用3年数据,不受start_date影响
|
||||
缓存时间为1天
|
||||
|
||||
Args:
|
||||
industry_name: 行业名称
|
||||
name: 行业/概念板块名称
|
||||
start_date: 不再使用此参数,保留是为了兼容性
|
||||
end_date: 结束日期(默认为当前日期)
|
||||
use_cache: 是否使用缓存,默认为True
|
||||
is_concept: 是否为概念板块,默认为False
|
||||
|
||||
Returns:
|
||||
包含行业拥挤度指标的DataFrame
|
||||
包含行业/概念板块拥挤度指标的DataFrame
|
||||
"""
|
||||
try:
|
||||
# 始终使用3年前作为开始日期
|
||||
three_years_ago = (datetime.datetime.now() - datetime.timedelta(days=3*365)).strftime('%Y-%m-%d')
|
||||
|
||||
cache_key = f"{'concept' if is_concept else 'industry'}_crowding:{name}"
|
||||
if end_date is None:
|
||||
end_date = datetime.datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
# 检查缓存
|
||||
if use_cache:
|
||||
cache_key = f"industry_crowding:{industry_name}"
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
try:
|
||||
# 尝试解析缓存的JSON数据
|
||||
cached_df_dict = json.loads(cached_data)
|
||||
logger.info(f"从缓存获取行业 {industry_name} 的拥挤度数据")
|
||||
|
||||
# 将缓存的字典转换回DataFrame
|
||||
df = pd.DataFrame(cached_df_dict)
|
||||
|
||||
# 确保trade_date列是日期类型
|
||||
df['trade_date'] = pd.to_datetime(df['trade_date'])
|
||||
|
||||
return df
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"解析缓存的拥挤度数据失败,将重新查询: {cache_error}")
|
||||
|
||||
# 获取行业所有股票
|
||||
stock_codes = self.get_industry_stocks(industry_name)
|
||||
cached_data = redis_client.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
try:
|
||||
# 尝试解析缓存的JSON数据
|
||||
cached_df_dict = json.loads(cached_data)
|
||||
logger.info(f"从缓存获取{'概念板块' if is_concept else '行业'} {name} 的拥挤度数据")
|
||||
|
||||
# 将缓存的字典转换回DataFrame
|
||||
df = pd.DataFrame(cached_df_dict)
|
||||
|
||||
# 确保trade_date列是日期类型
|
||||
df['trade_date'] = pd.to_datetime(df['trade_date'])
|
||||
|
||||
return df
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"解析缓存的拥挤度数据失败,将重新查询: {cache_error}")
|
||||
|
||||
# 获取行业/概念板块所有股票
|
||||
stock_codes = self.get_concept_stocks(name) if is_concept else self.get_industry_stocks(name)
|
||||
if not stock_codes:
|
||||
return pd.DataFrame()
|
||||
|
||||
# 优化方案:分别查询市场总成交额和行业成交额,然后在Python中计算比率
|
||||
# 优化方案:分别查询市场总成交额和行业/概念板块成交额,然后在Python中计算比率
|
||||
|
||||
# 查询1:获取每日总成交额
|
||||
query_total = text("""
|
||||
|
@ -340,11 +340,11 @@ class IndustryAnalyzer:
|
|||
`timestamp`
|
||||
""")
|
||||
|
||||
# 查询2:获取行业每日成交额
|
||||
query_industry = text("""
|
||||
# 查询2:获取行业/概念板块每日成交额
|
||||
query_sector = text("""
|
||||
SELECT
|
||||
`timestamp` AS trade_date,
|
||||
SUM(amount) AS industry_amount
|
||||
SUM(amount) AS sector_amount
|
||||
FROM
|
||||
gp_day_data
|
||||
WHERE
|
||||
|
@ -364,8 +364,8 @@ class IndustryAnalyzer:
|
|||
params={"start_date": three_years_ago, "end_date": end_date}
|
||||
)
|
||||
|
||||
df_industry = pd.read_sql(
|
||||
query_industry,
|
||||
df_sector = pd.read_sql(
|
||||
query_sector,
|
||||
conn,
|
||||
params={
|
||||
"stock_codes": tuple(stock_codes),
|
||||
|
@ -375,15 +375,15 @@ class IndustryAnalyzer:
|
|||
)
|
||||
|
||||
# 检查查询结果
|
||||
if df_total.empty or df_industry.empty:
|
||||
logger.warning(f"未找到行业 {industry_name} 的交易数据")
|
||||
if df_total.empty or df_sector.empty:
|
||||
logger.warning(f"未找到{'概念板块' if is_concept else '行业'} {name} 的交易数据")
|
||||
return pd.DataFrame()
|
||||
|
||||
# 在Python中合并数据并计算比率
|
||||
df = pd.merge(df_total, df_industry, on='trade_date', how='inner')
|
||||
df = pd.merge(df_total, df_sector, on='trade_date', how='inner')
|
||||
|
||||
# 计算行业成交额占比
|
||||
df['industry_amount_ratio'] = (df['industry_amount'] / df['total_market_amount']) * 100
|
||||
# 计算行业/概念板块成交额占比
|
||||
df['industry_amount_ratio'] = (df['sector_amount'] / df['total_market_amount']) * 100
|
||||
|
||||
# 在Python中计算百分位
|
||||
df['percentile'] = df['industry_amount_ratio'].rank(pct=True) * 100
|
||||
|
@ -399,22 +399,21 @@ class IndustryAnalyzer:
|
|||
df_dict = df.to_dict(orient='records')
|
||||
|
||||
# 缓存结果,有效期1天(86400秒)
|
||||
if use_cache:
|
||||
try:
|
||||
redis_client.set(
|
||||
cache_key,
|
||||
json.dumps(df_dict, default=str), # 使用default=str处理日期等特殊类型
|
||||
ex=86400 # 1天的秒数
|
||||
)
|
||||
logger.info(f"已缓存行业 {industry_name} 的拥挤度数据,有效期为1天")
|
||||
logger.info(f"已缓存{'概念板块' if is_concept else '行业'} {name} 的拥挤度数据,有效期为1天")
|
||||
except Exception as cache_error:
|
||||
logger.warning(f"缓存行业拥挤度数据失败: {cache_error}")
|
||||
|
||||
logger.info(f"成功计算行业 {industry_name} 的拥挤度指标,共 {len(df)} 条记录")
|
||||
logger.warning(f"缓存拥挤度数据失败: {cache_error}")
|
||||
|
||||
logger.info(f"成功计算{'概念板块' if is_concept else '行业'} {name} 的拥挤度指标,共 {len(df)} 条记录")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"计算行业拥挤度指标失败: {e}")
|
||||
logger.error(f"计算拥挤度指标失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_industry_analysis(self, industry_name: str, metric: str = 'pe', start_date: str = None) -> Dict:
|
||||
|
@ -508,11 +507,25 @@ class IndustryAnalyzer:
|
|||
}
|
||||
}
|
||||
|
||||
# ========== 新增:对所有数值四舍五入到两位小数 ==========
|
||||
def round_floats(obj):
|
||||
if isinstance(obj, float):
|
||||
return round(obj, 2)
|
||||
elif isinstance(obj, int):
|
||||
return obj # int不处理,保留原样(如有需要可加round)
|
||||
elif isinstance(obj, list):
|
||||
return [round_floats(x) for x in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return {k: round_floats(v) for k, v in obj.items()}
|
||||
else:
|
||||
return obj
|
||||
result = round_floats(result)
|
||||
# =====================================================
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取行业综合分析失败: {e}")
|
||||
return {"success": False, "message": f"获取行业综合分析失败: {e}"}
|
||||
return {"success": False, "message": f"获取行业综合分析失败: {e}"}
|
||||
|
||||
def get_stock_concepts(self, stock_code: str) -> List[str]:
|
||||
"""
|
||||
|
@ -547,6 +560,39 @@ class IndustryAnalyzer:
|
|||
logger.error(f"获取股票概念板块失败: {e}")
|
||||
return []
|
||||
|
||||
def get_stock_industry(self, stock_code: str) -> List[str]:
|
||||
"""
|
||||
获取指定股票所属的概念板块列表
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
概念板块名称列表
|
||||
"""
|
||||
try:
|
||||
# 转换股票代码格式
|
||||
formatted_code = self._convert_stock_code_format(stock_code)
|
||||
|
||||
query = text("""
|
||||
SELECT DISTINCT bk_name
|
||||
FROM gp_hybk
|
||||
WHERE gp_code = :stock_code
|
||||
""")
|
||||
|
||||
with self.engine.connect() as conn:
|
||||
result = conn.execute(query, {"stock_code": formatted_code}).fetchall()
|
||||
|
||||
if result:
|
||||
return [row[0] for row in result]
|
||||
else:
|
||||
logger.warning(f"未找到股票 {stock_code} 的行业板块数据")
|
||||
return []
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取股票行业板块失败: {e}")
|
||||
return []
|
||||
|
||||
def _convert_stock_code_format(self, stock_code: str) -> str:
|
||||
"""
|
||||
转换股票代码格式
|
||||
|
@ -562,4 +608,220 @@ class IndustryAnalyzer:
|
|||
return f"{market}{code}"
|
||||
except Exception as e:
|
||||
logger.error(f"转换股票代码格式失败: {str(e)}")
|
||||
return stock_code
|
||||
return stock_code
|
||||
|
||||
def get_concept_stocks(self, concept_name: str) -> List[str]:
|
||||
"""
|
||||
获取指定概念板块的所有股票代码
|
||||
|
||||
Args:
|
||||
concept_name: 概念板块名称
|
||||
|
||||
Returns:
|
||||
股票代码列表
|
||||
"""
|
||||
try:
|
||||
query = text("""
|
||||
SELECT DISTINCT gp_code
|
||||
FROM gp_gnbk
|
||||
WHERE bk_name = :concept_name
|
||||
""")
|
||||
|
||||
with self.engine.connect() as conn:
|
||||
result = conn.execute(query, {"concept_name": concept_name}).fetchall()
|
||||
|
||||
if result:
|
||||
return [row[0] for row in result]
|
||||
else:
|
||||
logger.warning(f"未找到概念板块 {concept_name} 的股票")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"获取概念板块股票失败: {e}")
|
||||
return []
|
||||
|
||||
def get_concept_valuation_data(self, concept_name: str, start_date: str, metric: str = 'pe') -> pd.DataFrame:
|
||||
"""
|
||||
获取概念板块估值数据,返回每日概念板块平均PE/PB/PS
|
||||
|
||||
说明:
|
||||
- 概念板块估值数据是指概念板块内所有股票的平均PE/PB/PS的历史数据
|
||||
- 在计算过程中会剔除负值和极端值(如PE>1000)
|
||||
|
||||
Args:
|
||||
concept_name: 概念板块名称
|
||||
start_date: 开始日期
|
||||
metric: 估值指标(pe、pb或ps)
|
||||
|
||||
Returns:
|
||||
包含概念板块估值数据的DataFrame,主要包含以下列:
|
||||
- timestamp: 日期
|
||||
- avg_{metric}: 概念板块平均值
|
||||
- stock_count: 参与计算的股票数量
|
||||
"""
|
||||
try:
|
||||
# 验证metric参数
|
||||
if metric not in ['pe', 'pb', 'ps']:
|
||||
logger.error(f"不支持的估值指标: {metric}")
|
||||
return pd.DataFrame()
|
||||
|
||||
# 获取概念板块所有股票
|
||||
stock_codes = self.get_concept_stocks(concept_name)
|
||||
if not stock_codes:
|
||||
return pd.DataFrame()
|
||||
|
||||
# 构建查询 - 只计算每天的概念板块平均值和参与计算的股票数量
|
||||
query = text(f"""
|
||||
WITH valid_data AS (
|
||||
SELECT
|
||||
`timestamp`,
|
||||
symbol,
|
||||
{metric}
|
||||
FROM
|
||||
gp_day_data
|
||||
WHERE
|
||||
symbol IN :stock_codes AND
|
||||
`timestamp` >= :start_date AND
|
||||
{metric} > 0 AND
|
||||
{metric} < 1000 -- 过滤掉极端异常值
|
||||
)
|
||||
SELECT
|
||||
`timestamp`,
|
||||
AVG({metric}) as avg_{metric},
|
||||
COUNT(*) as stock_count
|
||||
FROM
|
||||
valid_data
|
||||
GROUP BY
|
||||
`timestamp`
|
||||
ORDER BY
|
||||
`timestamp`
|
||||
""")
|
||||
|
||||
with self.engine.connect() as conn:
|
||||
# 获取汇总数据
|
||||
df = pd.read_sql(
|
||||
query,
|
||||
conn,
|
||||
params={"stock_codes": tuple(stock_codes), "start_date": start_date}
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
logger.warning(f"未找到概念板块 {concept_name} 的估值数据")
|
||||
return pd.DataFrame()
|
||||
|
||||
logger.info(f"成功获取概念板块 {concept_name} 的{metric.upper()}数据,共 {len(df)} 条记录")
|
||||
return df
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取概念板块估值数据失败: {e}")
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_concept_analysis(self, concept_name: str, metric: str = 'pe', start_date: str = None) -> Dict:
|
||||
"""
|
||||
获取概念板块综合分析结果
|
||||
|
||||
Args:
|
||||
concept_name: 概念板块名称
|
||||
metric: 估值指标(pe、pb或ps)
|
||||
start_date: 开始日期(默认为3年前)
|
||||
|
||||
Returns:
|
||||
概念板块分析结果字典,包含以下内容:
|
||||
- success: 是否成功
|
||||
- concept_name: 概念板块名称
|
||||
- metric: 估值指标
|
||||
- analysis_date: 分析日期
|
||||
- valuation: 估值数据,包含:
|
||||
- dates: 日期列表
|
||||
- avg_values: 概念板块平均值列表
|
||||
- stock_counts: 参与计算的股票数量列表
|
||||
- percentiles: 分位数信息,包含概念板块平均值的历史最大值、最小值、四分位数等
|
||||
- crowding(如有): 拥挤度数据,包含:
|
||||
- dates: 日期列表
|
||||
- ratios: 拥挤度比例列表
|
||||
- percentiles: 拥挤度百分位列表
|
||||
- current: 当前拥挤度信息
|
||||
"""
|
||||
try:
|
||||
# 默认查询近3年数据
|
||||
if start_date is None:
|
||||
start_date = (datetime.datetime.now() - datetime.timedelta(days=3*365)).strftime('%Y-%m-%d')
|
||||
|
||||
# 获取估值数据
|
||||
valuation_data = self.get_concept_valuation_data(concept_name, start_date, metric)
|
||||
if valuation_data.empty:
|
||||
return {"success": False, "message": f"无法获取概念板块 {concept_name} 的估值数据"}
|
||||
|
||||
# 计算估值分位数
|
||||
percentiles = self.calculate_industry_percentiles(valuation_data, metric)
|
||||
if not percentiles:
|
||||
return {"success": False, "message": f"无法计算概念板块 {concept_name} 的估值分位数"}
|
||||
|
||||
# 获取拥挤度指标(始终使用3年数据,不受start_date影响)
|
||||
crowding_data = self.get_industry_crowding_index(concept_name, is_concept=True)
|
||||
|
||||
# 为了兼容前端,准备一些概念板块平均值的历史统计数据
|
||||
avg_values = valuation_data[f'avg_{metric}'].tolist()
|
||||
|
||||
# 准备返回结果
|
||||
result = {
|
||||
"success": True,
|
||||
"concept_name": concept_name,
|
||||
"metric": metric.upper(),
|
||||
"analysis_date": datetime.datetime.now().strftime('%Y-%m-%d'),
|
||||
"valuation": {
|
||||
"dates": valuation_data['timestamp'].dt.strftime('%Y-%m-%d').tolist(),
|
||||
"avg_values": avg_values,
|
||||
# 填充概念板块平均值的历史统计线
|
||||
"min_values": [percentiles['min']] * len(avg_values), # 概念板块平均PE历史最小值
|
||||
"max_values": [percentiles['max']] * len(avg_values), # 概念板块平均PE历史最大值
|
||||
"q1_values": [percentiles['q1']] * len(avg_values), # 概念板块平均PE历史第一四分位数
|
||||
"q3_values": [percentiles['q3']] * len(avg_values), # 概念板块平均PE历史第三四分位数
|
||||
"median_values": [percentiles['median']] * len(avg_values), # 概念板块平均PE历史中位数
|
||||
"stock_counts": valuation_data['stock_count'].tolist(),
|
||||
"percentiles": percentiles
|
||||
}
|
||||
}
|
||||
|
||||
# 添加拥挤度数据(如果有)
|
||||
if not crowding_data.empty:
|
||||
current_crowding = crowding_data.iloc[-1]
|
||||
result["crowding"] = {
|
||||
"dates": crowding_data['trade_date'].dt.strftime('%Y-%m-%d').tolist(),
|
||||
"ratios": crowding_data['industry_amount_ratio'].tolist(),
|
||||
"percentiles": crowding_data['percentile'].tolist(),
|
||||
"current": {
|
||||
"date": current_crowding['trade_date'].strftime('%Y-%m-%d'),
|
||||
"ratio": float(current_crowding['industry_amount_ratio']),
|
||||
"percentile": float(current_crowding['percentile']),
|
||||
"level": current_crowding['crowding_level'],
|
||||
# 添加概念板块成交额比例的历史分位信息
|
||||
"ratio_stats": {
|
||||
"min": float(crowding_data['industry_amount_ratio'].min()),
|
||||
"max": float(crowding_data['industry_amount_ratio'].max()),
|
||||
"mean": float(crowding_data['industry_amount_ratio'].mean()),
|
||||
"median": float(crowding_data['industry_amount_ratio'].median()),
|
||||
"q1": float(crowding_data['industry_amount_ratio'].quantile(0.25)),
|
||||
"q3": float(crowding_data['industry_amount_ratio'].quantile(0.75)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ========== 新增:对所有数值四舍五入到两位小数 ==========
|
||||
def round_floats(obj):
|
||||
if isinstance(obj, float):
|
||||
return round(obj, 2)
|
||||
elif isinstance(obj, int):
|
||||
return obj # int不处理,保留原样(如有需要可加round)
|
||||
elif isinstance(obj, list):
|
||||
return [round_floats(x) for x in obj]
|
||||
elif isinstance(obj, dict):
|
||||
return {k: round_floats(v) for k, v in obj.items()}
|
||||
else:
|
||||
return obj
|
||||
result = round_floats(result)
|
||||
# =====================================================
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取概念板块综合分析失败: {e}")
|
||||
return {"success": False, "message": f"获取概念板块综合分析失败: {e}"}
|
Loading…
Reference in New Issue