diff --git a/src/app.py b/src/app.py index 328f446..98b8ef7 100644 --- a/src/app.py +++ b/src/app.py @@ -236,12 +236,12 @@ def initialize_stock_price_schedule(): collector.update_latest_data() except Exception as e: logger.error(f"更新实时股价数据失败: {e}") - + # 添加定时任务 scheduler.add_job( func=update_stock_price, trigger='interval', - minutes=5, + minutes=60, id='stock_price_update', name='实时股价数据采集', replace_existing=True @@ -249,7 +249,7 @@ def initialize_stock_price_schedule(): # 启动调度器 scheduler.start() - logger.info("实时股价数据采集定时任务已初始化,将在交易时间内每5分钟执行一次") + logger.info("实时股价数据采集定时任务已初始化,将在交易时间内每60分钟执行一次") return scheduler except Exception as e: @@ -1452,6 +1452,22 @@ def comprehensive_analysis(): if 'db_session2' in locals() and db_session2 is not None: # 确保 db_session 已定义 db_session2.close() # <--- 关闭会话 + # 获取企业市值信息 + market_value_results = {} + try: + from src.valuation_analysis.stock_price_collector import StockPriceCollector + price_collector = StockPriceCollector() + + for stock_code in input_stock_codes: + price_data = price_collector.get_stock_price_data(stock_code) + if price_data and 'total_market_value' in price_data: + market_value_results[stock_code] = price_data['total_market_value'] + else: + market_value_results[stock_code] = None + except Exception as e: + logger.error(f"获取企业市值信息失败: {str(e)}") + market_value_results = {} + db_session = next(get_db()) # 筛选出传入列表中符合条件的股票 for code, name in all_stocks: @@ -1475,7 +1491,8 @@ def comprehensive_analysis(): "industry_space": industry_space, # 行业发展空间(2:高速增长, 1:稳定经营, 0:不确定性大, -1:不利经营) "financial_report_level": financial_report_level, # 经营质量(2:优秀, 1:较好, 0:一般, -1:存在隐患,-2:较大隐患) "pe_industry": pe_industry, # 个股在行业的PE水平(-1:高于行业, 0:接近行业, 1:低于行业) - "tracks": track_results.get(code, []) # 添加赛道信息 + "tracks": track_results.get(code, []), # 添加赛道信息 + "market_value": market_value_results.get(code) # 添加企业市值信息 }) logger.info(f"筛选出 {len(filtered_stocks)} 个符合条件的股票") @@ -2675,7 +2692,7 @@ def precalculate_industry_crowding(): except Exception as e: logger.error(f"预计算行业 {industry} 的拥挤度指标时出错: {str(e)}") continue - + logger.info("所有行业的拥挤度指标预计算完成") except Exception as e: logger.error(f"预计算行业拥挤度指标失败: {str(e)}") @@ -2819,6 +2836,6 @@ if __name__ == '__main__': # 初始化实时股价数据采集定时任务 initialize_stock_price_schedule() - + # 启动Web服务器 app.run(host='0.0.0.0', port=5000, debug=True) diff --git a/src/fundamentals_llm/fundamental_analysis.py b/src/fundamentals_llm/fundamental_analysis.py index ae67ea8..495954a 100644 --- a/src/fundamentals_llm/fundamental_analysis.py +++ b/src/fundamentals_llm/fundamental_analysis.py @@ -1477,6 +1477,13 @@ class FundamentalAnalyzer: - 长期持有:公司具备长期稳定的盈利能力、行业地位稳固、长期成长性好 - 不建议投资:存在明显风险因素、基本面恶化、估值过高、行业前景不佳或者存在退市风险 + 请注意: + 1. 请完全基于提供的分析结果中的最新数据进行分析,不要使用任何历史数据或过时信息 + 2. 如果分析结果中包含2024年或2025年的数据,请优先使用这些最新数据 + 3. 避免使用"2023年"等历史时间点的数据,除非分析结果中明确提供了这些数据 + 4. 重点关注公司最新的业务发展、财务表现和市场定位 + 5. 在分析行业环境时,请使用最新的行业数据和竞争格局信息 + 请提供专业、客观的分析,突出关键信息,避免冗长描述。重点关注投资价值和风险。在输出投资建议时,请明确指出是短期持有、中期持有、长期持有还是不建议投资。 各维度分析结果: diff --git a/src/scripts/config.py b/src/scripts/config.py index 843a874..eea4f67 100644 --- a/src/scripts/config.py +++ b/src/scripts/config.py @@ -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; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1747356850; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGdVmpmghQehYtDDsqze4GzDiLPGhDBWAFdYCdqt4NKWooqCWKCwdUme9Ill25QAClcymm=0Iil4OAe8oGLDY=DCTKK420iDYAEDBYD74G+DDeDiO3Dj4GmDGY=aeDFIQutVCRKdxDwDB=DmqG23ObDm4DfDDLorBD4Il2YDDtDAkaGNPDADA3doDDlYD84edb4DYpogQ0FdgahphuXIeDMixGXzAlzx9CnoiWtV/LfNf2aHPGuDG=OcC0Hh2bmRT3f8hGxYBY5QeOhx+BxorKq0DW7HRYqexx=CD=WKK7oQ7YBGxPG4KiKy7hAQd5dpOodYYrcqsMkbZMshieygdyhxogYO2deGd46DAQ5MA5VBxiT5/h4WB++l=Eet4D; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40QuHhqDyGGdVmpmghQehY4Dfie4pCoTp35CT5NsKziGGtvkoYD', + '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', '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', diff --git a/src/valuation_analysis/financial_analysis.py b/src/valuation_analysis/financial_analysis.py index 2fec7f0..e733cc2 100644 --- a/src/valuation_analysis/financial_analysis.py +++ b/src/valuation_analysis/financial_analysis.py @@ -11,6 +11,7 @@ from pymongo import MongoClient import logging from typing import Dict, List, Optional, Union, Tuple import json +import requests from .config import DB_URL, MONGO_CONFIG, LOG_FILE from .stock_price_collector import StockPriceCollector @@ -394,6 +395,69 @@ class FinancialAnalyzer: 'message': f'查询失败: {str(e)}' } + def get_momentum_indicators(self, stock_code: str, industry_codes: List[str]) -> Dict: + """ + 获取动量指标数据 + + Args: + stock_code: 目标股票代码 + industry_codes: 行业股票代码列表 + + Returns: + 动量指标数据字典 + """ + try: + url = "http://192.168.18.42:5000/api/dify/getStockMomentumIndex" + payload = { + "code_list": industry_codes, + "target_code": stock_code + } + + response = requests.post(url, json=payload) + if response.status_code != 200: + return { + 'success': False, + 'message': f'获取动量指标失败: HTTP {response.status_code}' + } + + data = response.json() + + # 计算OBV和NATR的rank_score + obv_rank_score = round((1 - data['obv_rank'] / len(industry_codes)) * 10, 3) + natr_rank_score = round((1 - data['natr_rank'] / len(industry_codes)) * 10, 3) + + return { + 'success': True, + 'indicators': [ + { + 'key': 'obv', + 'desc': '能量', + 'value': round(data['OBV'], 3), + 'rank_score': obv_rank_score + }, + { + 'key': 'form', + 'desc': f'技术形态-{data["form"]}', + 'value': round(data['form_probability'], 3), + 'rank_score': round(data['form_probability'] * 10, 3) # 将概率转换为0-10的分数 + }, + { + 'key': 'natr', + 'desc': '阶段位置', + 'value': round(data['NATR'], 3), + 'rank_score': natr_rank_score + } + ], + 'avg_score': round((obv_rank_score + natr_rank_score + round(data['form_probability'] * 10, 3)) / 3, 3) + } + + except Exception as e: + logger.error(f"获取动量指标失败: {str(e)}") + return { + 'success': False, + 'message': f'获取动量指标失败: {str(e)}' + } + def analyze_financial_data(self, stock_code: str) -> Dict: """ 分析财务数据 @@ -413,6 +477,12 @@ class FinancialAnalyzer: industry_analyzer = IndustryAnalyzer() concepts = industry_analyzer.get_stock_concepts(stock_code) + # 获取同行业股票列表 + industry_stocks = self.get_industry_stocks(stock_code) + + # 获取动量指标数据 + momentum_result = self.get_momentum_indicators(stock_code, industry_stocks) + # 获取基础财务指标 base_result = self.extract_financial_indicators(stock_code) if not base_result.get('success'): @@ -578,6 +648,7 @@ class FinancialAnalyzer: '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 # 添加实时股价数据 } diff --git a/src/valuation_analysis/industry_analysis.py b/src/valuation_analysis/industry_analysis.py index bc100a7..2e5604c 100644 --- a/src/valuation_analysis/industry_analysis.py +++ b/src/valuation_analysis/industry_analysis.py @@ -299,25 +299,25 @@ class IndustryAnalyzer: # 检查缓存 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}") + 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) if not stock_codes: @@ -400,15 +400,15 @@ class IndustryAnalyzer: # 缓存结果,有效期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天") - except Exception as cache_error: - logger.warning(f"缓存行业拥挤度数据失败: {cache_error}") + try: + redis_client.set( + cache_key, + json.dumps(df_dict, default=str), # 使用default=str处理日期等特殊类型 + ex=86400 # 1天的秒数 + ) + logger.info(f"已缓存行业 {industry_name} 的拥挤度数据,有效期为1天") + except Exception as cache_error: + logger.warning(f"缓存行业拥挤度数据失败: {cache_error}") logger.info(f"成功计算行业 {industry_name} 的拥挤度指标,共 {len(df)} 条记录") return df @@ -512,7 +512,7 @@ class IndustryAnalyzer: 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]: """