commit;
This commit is contained in:
		
							parent
							
								
									7f478d91f4
								
							
						
					
					
						commit
						5b9ae03000
					
				
							
								
								
									
										43
									
								
								data.sql
								
								
								
								
							
							
						
						
									
										43
									
								
								data.sql
								
								
								
								
							|  | @ -1,43 +0,0 @@ | ||||||
| 
 |  | ||||||
| CREATE TABLE `gp_day_data`  ( |  | ||||||
|   `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键', |  | ||||||
|   `symbol` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '个股代码', |  | ||||||
|   `timestamp` timestamp NULL DEFAULT NULL COMMENT '时间戳', |  | ||||||
|   `volume` bigint NULL DEFAULT NULL COMMENT '数量', |  | ||||||
|   `open` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '开始价', |  | ||||||
|   `high` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '最高价', |  | ||||||
|   `low` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '最低价', |  | ||||||
|   `close` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '结束价', |  | ||||||
|   `chg` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL COMMENT '变化数值', |  | ||||||
|   `percent` decimal(10, 2) NULL DEFAULT NULL COMMENT '变化百分比', |  | ||||||
|   `turnoverrate` decimal(10, 2) NULL DEFAULT NULL COMMENT '换手率', |  | ||||||
|   `amount` bigint NULL DEFAULT NULL COMMENT '成交金额', |  | ||||||
|   `pb` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PB', |  | ||||||
|   `pe` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PE', |  | ||||||
|   `ps` decimal(10, 2) NULL DEFAULT NULL COMMENT '当前PS', |  | ||||||
|   PRIMARY KEY (`id`) USING BTREE, |  | ||||||
|   INDEX `idx_symbol`(`symbol` ASC) USING BTREE, |  | ||||||
|   INDEX `idx_timestamp`(`timestamp` ASC) USING BTREE |  | ||||||
| ) ENGINE = InnoDB AUTO_INCREMENT = 20472590 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic; |  | ||||||
| 
 |  | ||||||
| CREATE TABLE `gp_gnbk`  ( |  | ||||||
|   `id` bigint NULL DEFAULT NULL, |  | ||||||
|   `bk_code` bigint NULL DEFAULT NULL, |  | ||||||
|   `bk_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, |  | ||||||
|   `gp_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, |  | ||||||
|   `gp_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL |  | ||||||
| ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; |  | ||||||
| 
 |  | ||||||
| -- ---------------------------- |  | ||||||
| -- Table structure for gp_hybk |  | ||||||
| -- ---------------------------- |  | ||||||
| DROP TABLE IF EXISTS `gp_hybk`; |  | ||||||
| CREATE TABLE `gp_hybk`  ( |  | ||||||
|   `id` bigint NULL DEFAULT NULL, |  | ||||||
|   `bk_code` bigint NULL DEFAULT NULL, |  | ||||||
|   `bk_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, |  | ||||||
|   `gp_code` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, |  | ||||||
|   `gp_name` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL |  | ||||||
| ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; |  | ||||||
| 
 |  | ||||||
| SET FOREIGN_KEY_CHECKS = 1; |  | ||||||
|  | @ -15,4 +15,5 @@ reportlab>=4.3.1 | ||||||
| markdown2>=2.5.3 | markdown2>=2.5.3 | ||||||
| google-genai | google-genai | ||||||
| redis==5.2.1 | redis==5.2.1 | ||||||
| pandas==2.2.3 | pandas==2.2.3 | ||||||
|  | apscheduler==3.11.0 | ||||||
|  | @ -980,3 +980,5 @@ curl -X POST http://localhost:5000/api/comprehensive_analysis \ | ||||||
|   }' |   }' | ||||||
| ``` | ``` | ||||||
| 
 | 
 | ||||||
|  | PE--top bottom   | ||||||
|  | PB--top bottom | ||||||
							
								
								
									
										723
									
								
								src/app.py
								
								
								
								
							
							
						
						
									
										723
									
								
								src/app.py
								
								
								
								
							|  | @ -30,6 +30,21 @@ from src.valuation_analysis.industry_analysis import IndustryAnalyzer | ||||||
| # 导入沪深港通监控器 | # 导入沪深港通监控器 | ||||||
| from src.valuation_analysis.hsgt_monitor import HSGTMonitor | from src.valuation_analysis.hsgt_monitor import HSGTMonitor | ||||||
| 
 | 
 | ||||||
|  | # 导入融资融券数据采集器 | ||||||
|  | from src.valuation_analysis.eastmoney_rzrq_collector import EastmoneyRzrqCollector | ||||||
|  | 
 | ||||||
|  | # 导入恐贪指数管理器 | ||||||
|  | from src.valuation_analysis.fear_greed_index import FearGreedIndexManager | ||||||
|  | 
 | ||||||
|  | # 导入指数分析器 | ||||||
|  | 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 | ||||||
|  | 
 | ||||||
| # 设置日志 | # 设置日志 | ||||||
| logging.basicConfig( | logging.basicConfig( | ||||||
|     level=logging.INFO, |     level=logging.INFO, | ||||||
|  | @ -65,6 +80,15 @@ industry_analyzer = IndustryAnalyzer() | ||||||
| # 创建监控器实例 | # 创建监控器实例 | ||||||
| hsgt_monitor = HSGTMonitor() | hsgt_monitor = HSGTMonitor() | ||||||
| 
 | 
 | ||||||
|  | # 创建融资融券数据采集器实例 | ||||||
|  | em_rzrq_collector = EastmoneyRzrqCollector() | ||||||
|  | 
 | ||||||
|  | # 创建恐贪指数管理器实例 | ||||||
|  | fear_greed_manager = FearGreedIndexManager() | ||||||
|  | 
 | ||||||
|  | # 创建指数分析器实例 | ||||||
|  | index_analyzer = IndexAnalyzer() | ||||||
|  | 
 | ||||||
| # 获取项目根目录 | # 获取项目根目录 | ||||||
| ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||||
| REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports') | REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports') | ||||||
|  | @ -76,6 +100,9 @@ logger.info(f"报告目录路径: {REPORTS_DIR}") | ||||||
| # 存储回测任务状态的字典 | # 存储回测任务状态的字典 | ||||||
| backtest_tasks = {} | backtest_tasks = {} | ||||||
| 
 | 
 | ||||||
|  | # 融资融券数据采集任务列表 | ||||||
|  | rzrq_tasks = {} | ||||||
|  | 
 | ||||||
| def run_backtest_task(task_id, stocks_buy_dates, end_date): | def run_backtest_task(task_id, stocks_buy_dates, end_date): | ||||||
|     """ |     """ | ||||||
|     在后台运行回测任务 |     在后台运行回测任务 | ||||||
|  | @ -156,6 +183,154 @@ def run_backtest_task(task_id, stocks_buy_dates, end_date): | ||||||
|         backtest_tasks[task_id]['error'] = str(e) |         backtest_tasks[task_id]['error'] = str(e) | ||||||
|         logger.error(f"回测任务 {task_id} 失败:{str(e)}") |         logger.error(f"回测任务 {task_id} 失败:{str(e)}") | ||||||
| 
 | 
 | ||||||
|  | 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=0), | ||||||
|  |             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(): | ||||||
|  |     """执行股票日线数据采集任务""" | ||||||
|  |     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 | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"启动股票日线数据采集任务失败: {str(e)}") | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  | def run_rzrq_initial_collection(): | ||||||
|  |     """执行融资融券数据初始全量采集""" | ||||||
|  |     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 | ||||||
|  |     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 | ||||||
|  | 
 | ||||||
| @app.route('/') | @app.route('/') | ||||||
| def index(): | def index(): | ||||||
|     """渲染主页""" |     """渲染主页""" | ||||||
|  | @ -1558,6 +1733,45 @@ def get_industry_list(): | ||||||
|             "message": f"获取行业列表失败: {str(e)}" |             "message": f"获取行业列表失败: {str(e)}" | ||||||
|         }), 500 |         }), 500 | ||||||
| 
 | 
 | ||||||
|  | @app.route('/api/concept/list', methods=['GET']) | ||||||
|  | def get_concept_list(): | ||||||
|  |     """ | ||||||
|  |     获取概念板块列表 | ||||||
|  |      | ||||||
|  |     返回: | ||||||
|  |     { | ||||||
|  |         "status": "success", | ||||||
|  |         "data": [ | ||||||
|  |             {"code": "200001", "name": "人工智能"}, | ||||||
|  |             {"code": "200002", "name": "大数据"}, | ||||||
|  |             ... | ||||||
|  |         ] | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # 使用IndustryAnalyzer获取概念板块列表 | ||||||
|  |         concepts = industry_analyzer.get_concept_list() | ||||||
|  |          | ||||||
|  |         if not concepts: | ||||||
|  |             logger.warning("未找到概念板块数据") | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "未找到概念板块数据" | ||||||
|  |             }), 404 | ||||||
|  |              | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "success", | ||||||
|  |             "count": len(concepts), | ||||||
|  |             "data": concepts | ||||||
|  |         }) | ||||||
|  |              | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"获取概念板块列表失败: {str(e)}") | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error",  | ||||||
|  |             "message": f"获取概念板块列表失败: {str(e)}" | ||||||
|  |         }), 500 | ||||||
|  | 
 | ||||||
| @app.route('/api/industry/analysis', methods=['GET']) | @app.route('/api/industry/analysis', methods=['GET']) | ||||||
| def industry_analysis(): | def industry_analysis(): | ||||||
|     """ |     """ | ||||||
|  | @ -1875,6 +2089,67 @@ def get_southbound_data(): | ||||||
|             "message": f"服务器错误: {str(e)}" |             "message": f"服务器错误: {str(e)}" | ||||||
|         }), 500 |         }), 500 | ||||||
| 
 | 
 | ||||||
|  | @app.route('/api/rzrq/chart_data', methods=['GET']) | ||||||
|  | def get_rzrq_chart_data(): | ||||||
|  |     """获取融资融券数据用于图表展示 | ||||||
|  |      | ||||||
|  |     参数: | ||||||
|  |     - days: 可选,获取最近多少天的数据(默认30天) | ||||||
|  |      | ||||||
|  |     返回内容: | ||||||
|  |     { | ||||||
|  |         "status": "success", | ||||||
|  |         "data": { | ||||||
|  |             "success": true, | ||||||
|  |             "dates": ["2023-01-01", "2023-01-02", ...], | ||||||
|  |             "series": [ | ||||||
|  |                 { | ||||||
|  |                     "name": "融资融券余额合计", | ||||||
|  |                     "data": [1234.56, 1235.67, ...], | ||||||
|  |                     "unit": "亿元" | ||||||
|  |                 }, | ||||||
|  |                 // 其他系列数据... | ||||||
|  |             ], | ||||||
|  |             "last_update": "2023-01-15 12:34:56" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # 获取天数参数 | ||||||
|  |         days = request.args.get('days', type=int, default=30) | ||||||
|  |          | ||||||
|  |         # 限制天数范围 | ||||||
|  |         if days <= 0: | ||||||
|  |             days = 30 | ||||||
|  |         elif days > 365: | ||||||
|  |             days = 365 | ||||||
|  |              | ||||||
|  |         # 调用数据获取方法 | ||||||
|  |         result = em_rzrq_collector.get_chart_data(limit_days=days) | ||||||
|  |          | ||||||
|  |         if result.get('success'): | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "success", | ||||||
|  |                 "data": result | ||||||
|  |             }) | ||||||
|  |         else: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": result.get('message', '获取融资融券数据失败') | ||||||
|  |             }), 500 | ||||||
|  |              | ||||||
|  |     except ValueError as e: | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"参数格式错误: {str(e)}" | ||||||
|  |         }), 400 | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"获取融资融券图表数据异常: {str(e)}") | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"服务器错误: {str(e)}" | ||||||
|  |         }), 500 | ||||||
|  | 
 | ||||||
| @app.route('/api/stock/tracks', methods=['GET']) | @app.route('/api/stock/tracks', methods=['GET']) | ||||||
| def get_stock_tracks(): | def get_stock_tracks(): | ||||||
|     """根据股票代码获取相关赛道信息 |     """根据股票代码获取相关赛道信息 | ||||||
|  | @ -1939,5 +2214,451 @@ def get_stock_tracks(): | ||||||
|             "message": f"服务器错误: {str(e)}" |             "message": f"服务器错误: {str(e)}" | ||||||
|         }), 500 |         }), 500 | ||||||
| 
 | 
 | ||||||
|  | @app.route('/api/stock/price_range', methods=['GET']) | ||||||
|  | def get_stock_price_range(): | ||||||
|  |     """根据股票估值分位计算理论价格区间 | ||||||
|  |      | ||||||
|  |     根据当前PE和PB的四分位数据,反向计算出对应的理论股价区间 | ||||||
|  |      | ||||||
|  |     参数: | ||||||
|  |     - stock_code: 必须,股票代码 | ||||||
|  |     - start_date: 可选,开始日期,默认为一年前 | ||||||
|  |      | ||||||
|  |     返回内容: | ||||||
|  |     { | ||||||
|  |         "status": "success", | ||||||
|  |         "data": { | ||||||
|  |             "stock_code": "600000", | ||||||
|  |             "stock_name": "浦发银行", | ||||||
|  |             "current_price": 10.5, | ||||||
|  |             "current_date": "2023-12-01", | ||||||
|  |             "pe": { | ||||||
|  |                 "current": 5.2, | ||||||
|  |                 "q1": 4.8, | ||||||
|  |                 "q3": 6.5, | ||||||
|  |                 "q1_price": 9.7,  // 对应PE为Q1时的理论股价 | ||||||
|  |                 "q3_price": 13.1   // 对应PE为Q3时的理论股价 | ||||||
|  |             }, | ||||||
|  |             "pb": { | ||||||
|  |                 "current": 0.65, | ||||||
|  |                 "q1": 0.6, | ||||||
|  |                 "q3": 0.8, | ||||||
|  |                 "q1_price": 9.7,   // 对应PB为Q1时的理论股价 | ||||||
|  |                 "q3_price": 12.9    // 对应PB为Q3时的理论股价 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # 获取股票代码参数 | ||||||
|  |         stock_code = request.args.get('stock_code') | ||||||
|  |          | ||||||
|  |         # 验证参数 | ||||||
|  |         if not stock_code: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "缺少必要参数: stock_code" | ||||||
|  |             }), 400 | ||||||
|  |          | ||||||
|  |         # 计算一年前的日期作为默认起始日期 | ||||||
|  |         default_start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d') | ||||||
|  |         start_date = request.args.get('start_date', default_start_date) | ||||||
|  |          | ||||||
|  |         # 通过复用现有API的逻辑获取PE和PB数据 | ||||||
|  |         # 首先获取PE数据 | ||||||
|  |         pe_data = valuation_analyzer.get_historical_data(stock_code, start_date) | ||||||
|  |         if pe_data.empty: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error",  | ||||||
|  |                 "message": f"未找到股票 {stock_code} 的历史数据" | ||||||
|  |             }), 404 | ||||||
|  |          | ||||||
|  |         # 计算PE分位数 | ||||||
|  |         pe_percentiles = valuation_analyzer.calculate_percentiles(pe_data, 'pe') | ||||||
|  |         if not pe_percentiles: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error",  | ||||||
|  |                 "message": f"无法计算股票 {stock_code} 的PE分位数" | ||||||
|  |             }), 500 | ||||||
|  |          | ||||||
|  |         # 计算PB分位数 | ||||||
|  |         pb_percentiles = valuation_analyzer.calculate_percentiles(pe_data, 'pb') | ||||||
|  |         if not pb_percentiles: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error",  | ||||||
|  |                 "message": f"无法计算股票 {stock_code} 的PB分位数" | ||||||
|  |             }), 500 | ||||||
|  |          | ||||||
|  |         # 获取当前股价 | ||||||
|  |         current_price = None | ||||||
|  |         current_date = None | ||||||
|  |         if not pe_data.empty: | ||||||
|  |             current_price = pe_data.iloc[-1].get('close') | ||||||
|  |             current_date = pe_data.iloc[-1].get('timestamp').strftime('%Y-%m-%d') if 'timestamp' in pe_data.columns else None | ||||||
|  |          | ||||||
|  |         if current_price is None: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error",  | ||||||
|  |                 "message": f"无法获取股票 {stock_code} 的当前股价" | ||||||
|  |             }), 500 | ||||||
|  |          | ||||||
|  |         # 获取当前PE和PB | ||||||
|  |         current_pe = pe_percentiles.get('current') | ||||||
|  |         current_pb = pb_percentiles.get('current') | ||||||
|  |          | ||||||
|  |         # 获取PE的Q1和Q3 | ||||||
|  |         pe_q1 = pe_percentiles.get('q1') | ||||||
|  |         pe_q3 = pe_percentiles.get('q3') | ||||||
|  |          | ||||||
|  |         # 获取PB的Q1和Q3 | ||||||
|  |         pb_q1 = pb_percentiles.get('q1') | ||||||
|  |         pb_q3 = pb_percentiles.get('q3') | ||||||
|  |          | ||||||
|  |         # 反向计算估值分位对应的股价 | ||||||
|  |         # 如果当前PE为X,股价为Y,则PE为Z时的理论股价 = Y * (X / Z) | ||||||
|  |          | ||||||
|  |         # 计算PE对应的理论股价 | ||||||
|  |         pe_q1_price = None | ||||||
|  |         pe_q3_price = None | ||||||
|  |         if current_pe and current_pe > 0 and pe_q1 and pe_q3: | ||||||
|  |             pe_q1_price = current_price * (pe_q1 / current_pe) | ||||||
|  |             pe_q3_price = current_price * (pe_q3 / current_pe) | ||||||
|  |          | ||||||
|  |         # 计算PB对应的理论股价 | ||||||
|  |         pb_q1_price = None | ||||||
|  |         pb_q3_price = None | ||||||
|  |         if current_pb and current_pb > 0 and pb_q1 and pb_q3: | ||||||
|  |             pb_q1_price = current_price * (pb_q1 / current_pb) | ||||||
|  |             pb_q3_price = current_price * (pb_q3 / current_pb) | ||||||
|  |          | ||||||
|  |         # 获取股票名称 | ||||||
|  |         stock_name = valuation_analyzer.get_stock_name(stock_code) | ||||||
|  |          | ||||||
|  |         # 构建响应 | ||||||
|  |         response = { | ||||||
|  |             "status": "success", | ||||||
|  |             "data": { | ||||||
|  |                 "stock_code": stock_code, | ||||||
|  |                 "stock_name": stock_name, | ||||||
|  |                 "current_price": current_price, | ||||||
|  |                 "current_date": current_date, | ||||||
|  |                 "pe": { | ||||||
|  |                     "current": current_pe, | ||||||
|  |                     "q1": pe_q1, | ||||||
|  |                     "q3": pe_q3, | ||||||
|  |                     "q1_price": round(pe_q1_price, 2) if pe_q1_price is not None else None, | ||||||
|  |                     "q3_price": round(pe_q3_price, 2) if pe_q3_price is not None else None | ||||||
|  |                 }, | ||||||
|  |                 "pb": { | ||||||
|  |                     "current": current_pb, | ||||||
|  |                     "q1": pb_q1, | ||||||
|  |                     "q3": pb_q3, | ||||||
|  |                     "q1_price": round(pb_q1_price, 2) if pb_q1_price is not None else None, | ||||||
|  |                     "q3_price": round(pb_q3_price, 2) if pb_q3_price is not None else None | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         return jsonify(response) | ||||||
|  |      | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"计算股票价格区间异常: {str(e)}") | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"服务器错误: {str(e)}" | ||||||
|  |         }), 500 | ||||||
|  | 
 | ||||||
|  | @app.route('/api/fear_greed/data', methods=['GET']) | ||||||
|  | def get_fear_greed_data(): | ||||||
|  |     """获取恐贪指数数据 | ||||||
|  |      | ||||||
|  |     参数: | ||||||
|  |     - start_date: 可选,开始日期(YYYY-MM-DD格式) | ||||||
|  |     - end_date: 可选,结束日期(YYYY-MM-DD格式) | ||||||
|  |     - limit: 可选,限制返回的记录数量,默认为730(约两年的交易日数量) | ||||||
|  |      | ||||||
|  |     返回内容: | ||||||
|  |     { | ||||||
|  |         "status": "success", | ||||||
|  |         "data": { | ||||||
|  |             "dates": ["2023-01-01", "2023-01-02", ...], | ||||||
|  |             "values": [45.67, 50.12, ...], | ||||||
|  |             "latest": { | ||||||
|  |                 "id": 123, | ||||||
|  |                 "index_value": 50.12, | ||||||
|  |                 "trading_date": "2023-01-02", | ||||||
|  |                 "update_time": "2023-01-02 15:30:00" | ||||||
|  |             }, | ||||||
|  |             "latest_status": "中性", | ||||||
|  |             "update_time": "2023-01-02 16:00:00" | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # 获取参数 | ||||||
|  |         start_date = request.args.get('start_date') | ||||||
|  |         end_date = request.args.get('end_date') | ||||||
|  |         limit = request.args.get('limit', type=int, default=730) | ||||||
|  |          | ||||||
|  |         # 调用数据获取方法 | ||||||
|  |         result = fear_greed_manager.get_index_data(start_date, end_date, limit) | ||||||
|  |          | ||||||
|  |         if result.get('success'): | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "success", | ||||||
|  |                 "data": result | ||||||
|  |             }) | ||||||
|  |         else: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": result.get('message', '获取恐贪指数数据失败') | ||||||
|  |             }), 500 | ||||||
|  |              | ||||||
|  |     except ValueError as e: | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"参数格式错误: {str(e)}" | ||||||
|  |         }), 400 | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"获取恐贪指数数据异常: {str(e)}") | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"服务器错误: {str(e)}" | ||||||
|  |         }), 500 | ||||||
|  | 
 | ||||||
|  | @app.route('/api/fear_greed/add', methods=['POST']) | ||||||
|  | def add_fear_greed_data(): | ||||||
|  |     """添加恐贪指数数据 | ||||||
|  |      | ||||||
|  |     请求体格式: | ||||||
|  |     { | ||||||
|  |         "index_value": 45.67,  // 恐贪指数值,0-100之间的数值 | ||||||
|  |         "trading_date": "2023-01-01"  // 交易日期,YYYY-MM-DD格式 | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     返回内容: | ||||||
|  |     { | ||||||
|  |         "status": "success", | ||||||
|  |         "message": "数据添加成功" | ||||||
|  |     } | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # 从请求体获取参数 | ||||||
|  |         data = request.get_json() | ||||||
|  |          | ||||||
|  |         if not data: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error",  | ||||||
|  |                 "message": "请求体为空" | ||||||
|  |             }), 400 | ||||||
|  |              | ||||||
|  |         index_value = data.get('index_value') | ||||||
|  |         trading_date = data.get('trading_date') | ||||||
|  |          | ||||||
|  |         # 验证参数 | ||||||
|  |         if index_value is None: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "缺少必要参数: index_value" | ||||||
|  |             }), 400 | ||||||
|  |          | ||||||
|  |         if trading_date is None: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "缺少必要参数: trading_date" | ||||||
|  |             }), 400 | ||||||
|  |              | ||||||
|  |         # 尝试转换为浮点数 | ||||||
|  |         try: | ||||||
|  |             index_value = float(index_value) | ||||||
|  |         except ValueError: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "index_value必须是数值" | ||||||
|  |             }), 400 | ||||||
|  |              | ||||||
|  |         # 调用添加方法 | ||||||
|  |         result = fear_greed_manager.add_index_data(index_value, trading_date) | ||||||
|  |          | ||||||
|  |         if result: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "success", | ||||||
|  |                 "message": "恐贪指数数据添加成功" | ||||||
|  |             }) | ||||||
|  |         else: | ||||||
|  |             return jsonify({ | ||||||
|  |                 "status": "error", | ||||||
|  |                 "message": "恐贪指数数据添加失败" | ||||||
|  |             }), 500 | ||||||
|  |              | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"添加恐贪指数数据异常: {str(e)}") | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "error", | ||||||
|  |             "message": f"服务器错误: {str(e)}" | ||||||
|  |         }), 500 | ||||||
|  | 
 | ||||||
|  | # 获取可用指数列表 | ||||||
|  | @app.route('/api/indices/list', methods=['GET']) | ||||||
|  | def get_indices_list(): | ||||||
|  |     """ | ||||||
|  |     获取可用指数列表 | ||||||
|  |      | ||||||
|  |     返回所有可用于叠加显示的指数列表 | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         indices = index_analyzer.get_indices_list() | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "success", | ||||||
|  |             "data": indices | ||||||
|  |         }) | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"获取指数列表失败: {str(e)}") | ||||||
|  |         return jsonify({"status": "error", "message": str(e)}) | ||||||
|  | 
 | ||||||
|  | # 获取指数历史数据 | ||||||
|  | @app.route('/api/indices/data', methods=['GET']) | ||||||
|  | def get_index_data(): | ||||||
|  |     """ | ||||||
|  |     获取指数历史数据 | ||||||
|  |      | ||||||
|  |     参数: | ||||||
|  |     - code: 指数代码 | ||||||
|  |     - start_date: 开始日期 (可选,默认为1年前) | ||||||
|  |     - end_date: 结束日期 (可选,默认为今天) | ||||||
|  |      | ||||||
|  |     返回指数历史收盘价数据 | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         index_code = request.args.get('code') | ||||||
|  |         start_date = request.args.get('start_date') | ||||||
|  |         end_date = request.args.get('end_date') | ||||||
|  |          | ||||||
|  |         if not index_code: | ||||||
|  |             return jsonify({"status": "error", "message": "缺少指数代码参数"}) | ||||||
|  |          | ||||||
|  |         index_data = index_analyzer.get_index_data(index_code, start_date, end_date) | ||||||
|  |          | ||||||
|  |         return jsonify({ | ||||||
|  |             "status": "success", | ||||||
|  |             "data": index_data | ||||||
|  |         }) | ||||||
|  |     except Exception as e: | ||||||
|  |         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=22, minute=0), | ||||||
|  |             id='industry_crowding_precalc', | ||||||
|  |             name='预计算行业拥挤度指标', | ||||||
|  |             replace_existing=True | ||||||
|  |         ) | ||||||
|  |          | ||||||
|  |         # 启动调度器 | ||||||
|  |         scheduler.start() | ||||||
|  |         logger.info("行业拥挤度指标预计算定时任务已初始化,将在每天22:00执行") | ||||||
|  |         return scheduler | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"初始化行业拥挤度指标预计算定时任务失败: {str(e)}") | ||||||
|  |         industry_crowding_lock.release() | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  | def precalculate_industry_crowding(): | ||||||
|  |     """预计算所有行业的拥挤度指标""" | ||||||
|  |     try: | ||||||
|  |         logger.info("开始预计算所有行业的拥挤度指标") | ||||||
|  |          | ||||||
|  |         # 获取所有行业列表 | ||||||
|  |         industries = industry_analyzer.get_industry_list() | ||||||
|  |         if not industries: | ||||||
|  |             logger.error("获取行业列表失败") | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         # 记录成功和失败的数量 | ||||||
|  |         success_count = 0 | ||||||
|  |         fail_count = 0 | ||||||
|  |          | ||||||
|  |         # 遍历所有行业 | ||||||
|  |         for industry in industries: | ||||||
|  |             try: | ||||||
|  |                 industry_name = industry['name'] | ||||||
|  |                 logger.info(f"正在计算行业 {industry_name} 的拥挤度指标") | ||||||
|  |                  | ||||||
|  |                 # 调用拥挤度计算方法 | ||||||
|  |                 df = industry_analyzer.get_industry_crowding_index(industry_name) | ||||||
|  |                  | ||||||
|  |                 if not df.empty: | ||||||
|  |                     success_count += 1 | ||||||
|  |                     logger.info(f"成功计算行业 {industry_name} 的拥挤度指标") | ||||||
|  |                 else: | ||||||
|  |                     fail_count += 1 | ||||||
|  |                     logger.warning(f"计算行业 {industry_name} 的拥挤度指标失败") | ||||||
|  |                      | ||||||
|  |             except Exception as e: | ||||||
|  |                 fail_count += 1 | ||||||
|  |                 logger.error(f"计算行业 {industry_name} 的拥挤度指标时出错: {str(e)}") | ||||||
|  |                 continue | ||||||
|  |          | ||||||
|  |         logger.info(f"行业拥挤度指标预计算完成,成功: {success_count},失败: {fail_count}") | ||||||
|  |          | ||||||
|  |     except Exception as e: | ||||||
|  |         logger.error(f"预计算行业拥挤度指标失败: {str(e)}") | ||||||
|  | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     app.run(host='0.0.0.0', port=5000, debug=True)  |     """ | ||||||
|  |     # 手动释放锁的方法(需要时取消注释) | ||||||
|  |     # 创建锁实例 | ||||||
|  |     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() | ||||||
|  |      | ||||||
|  |     # 启动Web服务器 | ||||||
|  |     app.run(host='0.0.0.0', port=5000, debug=True) | ||||||
|  |  | ||||||
|  | @ -11,7 +11,7 @@ XUEQIU_HEADERS = { | ||||||
|     'Accept-Encoding': 'gzip, deflate, br, zstd', |     'Accept-Encoding': 'gzip, deflate, br, zstd', | ||||||
|     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', |     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||||
|     'Client-Version': 'v2.44.75', |     'Client-Version': 'v2.44.75', | ||||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; xq_a_token=90d76a1c24a9d8fd1b868cd7b94fabcdd6cb2f0a; xqat=90d76a1c24a9d8fd1b868cd7b94fabcdd6cb2f0a; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzQ4NTI1NTA4LCJjdG0iOjE3NDU5MzM1MDg4NDcsImNpZCI6ImQ5ZDBuNEFadXAifQ.Xj00ujbYNYb3jt0wev1VZSj37wy3oRdTXohaOXp0xGoV6xOS055QcxaeXzbE6yaKQDgwUC4NVCEQLfJ49LvxWDSvWGEI7y2j-_ZzH-ZoHc6-RZ7pQdLLlTeRSM17Sg1JZZWG4xwk4yb_aHoWyUznjODTOgyg8EOnhDPO6-bI8SrXXXV8a-TE0ZpDw1EIimKYzhCQR0qwEnm2swEoN3YRfyiBvuMg5Cr2zqgnrKQAafquUZmwFvudIVlYG1HppoMnrbzXhQ4II0tP8duvcT-mzabQE_OaY0RM5u9mwthMfm5KPThEVb_o74s_SweMv6vHZDRMaxxzrnlM4MgW-4mmpg; xq_r_token=6a95ad5270dea5256d4b5d14683bf40cdabce730; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1746517072; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0QHhqDyGmZATmkDq3e4pEBDDl=BrYDSxD6FDK4GTh86DOICelaOowCWKGLkUpeBUlCR5QW/+Dp0KY0zzSQW0P40aDmKDUoqqhW7YDeWaDCeDQxirDD4DAWPDFxibDiWIo4Ddb+ovz3owpDGrDlKDRx07pYoDbxDaDGpmYCDxbS7eDD5DnESXI4DWDWapeDDzelQx+xoxDm+mk4YLpB80RjgDqYoD9h4DsZigl/LgATiAkS=BWvewff3DvxDk2IEGU4TpKavbec4xqiibt7Y34qe2qF+QKGxAGUmrKiiYiiqP+xmhx84qmx4RxCIT5MqbF7YQFYRxY=7K5iK4rZ0y/mWV/HYerYTBqiAbYEk4hNDRu44bQmtBhN4+QlbqY3PA0PogkWgieD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0QHhqDyGmZATmkDq3e4pEYDA4gYHYeqRD7Pzg+ReDsXYe6pj6SmpZ2qUqQe6DhjRtXa2S6bph7ZGARuppraeTqypju30Gj37fAmhhj8qrSzx6KdQfXAG4Zj3f5WLPMjTIV77RYy+TnziIlSLPEBg3M3ZuL41LKWTf6lS330QyxSLXCOYnxlCGLl46fKbFElPrcG4=C=IQgQ9tGaCLfmgxZQBQtoiIQprYcbYfuRcCYM1y5OH37aMWU4=yQYv/LnWnGq5OSclDIyYpvCnDYqv9aUBn4=mQR0pGcsjuHQvLm9F7iPmPHYH+CcLjIjGBntKepw870/+FKq52z9YXHYaq4fbH0v2GHseRe=WHIgD9HY=FQrnctq/GFA3EhBKctmx4wvim9+bWX4UI+2FP+b8F9P0lS7rWz3PU9m4NmqwK0Wux6+xjn4qPtcYUD8OKpAYFK42qAid5Dt9RqiiqEiaeQhEo+aQwP2BYIpfihOiY3bre4t9rNnxro0q8GI==I2hDD', |     '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', | ||||||
|     'Referer': 'https://weibo.com/u/7735765253', |     'Referer': 'https://weibo.com/u/7735765253', | ||||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', |     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||||
|     'Sec-Ch-Ua-Mobile': '?0', |     'Sec-Ch-Ua-Mobile': '?0', | ||||||
|  |  | ||||||
|  | @ -1,4 +1,3 @@ | ||||||
| 
 |  | ||||||
| # coding:utf-8 | # coding:utf-8 | ||||||
| 
 | 
 | ||||||
| import requests | import requests | ||||||
|  | @ -6,9 +5,8 @@ import pandas as pd | ||||||
| from sqlalchemy import create_engine, text | from sqlalchemy import create_engine, text | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from tqdm import tqdm | from tqdm import tqdm | ||||||
| from config import XUEQIU_HEADERS | from src.scripts.config import XUEQIU_HEADERS | ||||||
| import gc | import gc | ||||||
| import time |  | ||||||
| 
 | 
 | ||||||
| class StockDailyDataCollector: | class StockDailyDataCollector: | ||||||
|     """股票日线数据采集器类""" |     """股票日线数据采集器类""" | ||||||
|  | @ -23,9 +21,20 @@ class StockDailyDataCollector: | ||||||
|         self.headers = XUEQIU_HEADERS |         self.headers = XUEQIU_HEADERS | ||||||
| 
 | 
 | ||||||
|     def fetch_all_stock_codes(self): |     def fetch_all_stock_codes(self): | ||||||
|         query = "SELECT gp_code FROM gp_code_all" |         # 从gp_code_all获取股票代码 | ||||||
|         df = pd.read_sql(query, self.engine) |         query_all = "SELECT gp_code FROM gp_code_all" | ||||||
|         return df['gp_code'].tolist() |         df_all = pd.read_sql(query_all, self.engine) | ||||||
|  |         codes_all = df_all['gp_code'].tolist() | ||||||
|  |          | ||||||
|  |         # 从gp_code_zs获取股票代码 | ||||||
|  |         query_zs = "SELECT gp_code FROM gp_code_zs" | ||||||
|  |         df_zs = pd.read_sql(query_zs, self.engine) | ||||||
|  |         codes_zs = df_zs['gp_code'].tolist() | ||||||
|  |          | ||||||
|  |         # 合并去重 | ||||||
|  |         all_codes = list(set(codes_all + codes_zs)) | ||||||
|  |         print(f"获取到股票代码: {len(codes_all)}个来自gp_code_all, {len(codes_zs)}个来自gp_code_zs, 去重后共{len(all_codes)}个") | ||||||
|  |         return all_codes | ||||||
| 
 | 
 | ||||||
|     def fetch_daily_stock_data(self, symbol, begin): |     def fetch_daily_stock_data(self, symbol, begin): | ||||||
|         url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-1&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance" |         url = f"https://stock.xueqiu.com/v5/stock/chart/kline.json?symbol={symbol}&begin={begin}&period=day&type=before&count=-1&indicator=kline,pe,pb,ps,pcf,market_capital,agt,ggt,balance" | ||||||
|  |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -0,0 +1,67 @@ | ||||||
|  | .card { | ||||||
|  |     margin-bottom: 20px; | ||||||
|  |     box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); | ||||||
|  | } | ||||||
|  | .card-header { | ||||||
|  |     background-color: #f8f9fa; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | .money-inflow { | ||||||
|  |     color: #d9534f; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | .money-outflow { | ||||||
|  |     color: #5cb85c; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | .chart-container { | ||||||
|  |     height: 350px; | ||||||
|  |     margin-bottom: 20px; | ||||||
|  | } | ||||||
|  | .stat-card { | ||||||
|  |     text-align: center; | ||||||
|  |     padding: 10px; | ||||||
|  |     border-radius: 5px; | ||||||
|  |     margin-bottom: 10px; | ||||||
|  | } | ||||||
|  | .stat-value { | ||||||
|  |     font-size: 24px; | ||||||
|  |     font-weight: bold; | ||||||
|  | } | ||||||
|  | .stat-title { | ||||||
|  |     font-size: 14px; | ||||||
|  |     color: #666; | ||||||
|  | } | ||||||
|  | .refresh-btn { | ||||||
|  |     margin-left: 10px; | ||||||
|  | } | ||||||
|  | .update-time { | ||||||
|  |     font-size: 12px; | ||||||
|  |     color: #666; | ||||||
|  |     margin-top: 5px; | ||||||
|  | } | ||||||
|  | .flow-direction { | ||||||
|  |     font-size: 13px; | ||||||
|  |     color: #666; | ||||||
|  |     font-style: italic; | ||||||
|  |     margin-top: -5px; | ||||||
|  |     margin-bottom: 10px; | ||||||
|  | } | ||||||
|  | .summary-text { | ||||||
|  |     font-size: 1em; | ||||||
|  |     margin-bottom: 8px; | ||||||
|  | } | ||||||
|  | .percentage-value { | ||||||
|  |     font-weight: bold; | ||||||
|  |     font-size: 1.2em; /* 字体放大 */ | ||||||
|  |     padding: 0 3px; /* 微调间距 */ | ||||||
|  | } | ||||||
|  | .percentage-value.positive { | ||||||
|  |     color: #d9534f; /* 红色 */ | ||||||
|  | } | ||||||
|  | .percentage-value.negative { | ||||||
|  |     color: #5cb85c; /* 绿色 */ | ||||||
|  | } | ||||||
|  | .percentage-value.neutral { | ||||||
|  |     color: #337ab7; /* 蓝色 (中性色) */ | ||||||
|  | } | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -6,12 +6,23 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     // 初始化图表
 |     // 初始化图表
 | ||||||
|     let northChart = null; |     let northChart = null; | ||||||
|     let southChart = null; |     let southChart = null; | ||||||
|  |     let rzrqChart = null;  // 融资融券图表实例
 | ||||||
|  |      | ||||||
|  |     // 当前显示的融资融券数据系列
 | ||||||
|  |     let currentMetric = 'total_rzrq_balance'; | ||||||
|  |     // 融资融券数据
 | ||||||
|  |     let rzrqData = null; | ||||||
|  |      | ||||||
|  |     // 融资融券图表相关功能
 | ||||||
|  |     let rzrqIndexSelector = null; | ||||||
|  |     let rzrqChartData = null; // 用于存储融资融券图表的原始数据
 | ||||||
|      |      | ||||||
|     // 初始化图表函数,确保DOM元素存在
 |     // 初始化图表函数,确保DOM元素存在
 | ||||||
|     function initCharts() { |     function initCharts() { | ||||||
|         try { |         try { | ||||||
|             const northChartDom = document.getElementById('northChart'); |             const northChartDom = document.getElementById('northChart'); | ||||||
|             const southChartDom = document.getElementById('southChart'); |             const southChartDom = document.getElementById('southChart'); | ||||||
|  |             const rzrqChartDom = document.getElementById('rzrqChart'); | ||||||
|              |              | ||||||
|             if (northChartDom && !northChart) { |             if (northChartDom && !northChart) { | ||||||
|                 try { |                 try { | ||||||
|  | @ -31,7 +42,16 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             return northChart && southChart; |             if (rzrqChartDom && !rzrqChart) { | ||||||
|  |                 try { | ||||||
|  |                     rzrqChart = echarts.init(rzrqChartDom); | ||||||
|  |                     console.log('融资融券图表初始化成功'); | ||||||
|  |                 } catch (e) { | ||||||
|  |                     console.error('融资融券图表初始化失败:', e); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             return northChart && southChart && rzrqChart; | ||||||
|         } catch (e) { |         } catch (e) { | ||||||
|             console.error('图表初始化过程中发生错误:', e); |             console.error('图表初始化过程中发生错误:', e); | ||||||
|             return false; |             return false; | ||||||
|  | @ -48,10 +68,46 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|         // 开始加载数据
 |         // 开始加载数据
 | ||||||
|         loadData(); |         loadData(); | ||||||
|          |          | ||||||
|         // 设置自动刷新 (每分钟刷新一次)
 |         // 加载融资融券数据
 | ||||||
|         setInterval(loadData, 60000); |         initRzrqChart(); | ||||||
|  |          | ||||||
|  |         // 检查是否在交易时段,只有在交易时段才设置自动刷新
 | ||||||
|  |         if (isWithinTradingHours()) { | ||||||
|  |             console.log('当前处于交易时段,启用自动刷新'); | ||||||
|  |             // 设置自动刷新 (每分钟刷新一次)
 | ||||||
|  |             window.refreshInterval = setInterval(loadData, 60000); | ||||||
|  |         } else { | ||||||
|  |             console.log('当前不在交易时段,不启用自动刷新'); | ||||||
|  |         } | ||||||
|     }, 100); |     }, 100); | ||||||
|      |      | ||||||
|  |     /** | ||||||
|  |      * 判断当前时间是否在交易时段内 (9:20-16:00) | ||||||
|  |      */ | ||||||
|  |     function isWithinTradingHours() { | ||||||
|  |         const now = new Date(); | ||||||
|  |         const hours = now.getHours(); | ||||||
|  |         const minutes = now.getMinutes(); | ||||||
|  |         const dayOfWeek = now.getDay(); // 0是周日,6是周六
 | ||||||
|  |          | ||||||
|  |         // 如果是周末,不在交易时段
 | ||||||
|  |         if (dayOfWeek === 0 || dayOfWeek === 6) { | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 计算当前时间的分钟数
 | ||||||
|  |         const totalMinutes = hours * 60 + minutes; | ||||||
|  |          | ||||||
|  |         // 交易时段开始时间:9:20 (9*60 + 20 = 560分钟)
 | ||||||
|  |         const tradingStartMinutes = 9 * 60 + 20; | ||||||
|  |          | ||||||
|  |         // 交易时段结束时间:16:00 (16*60 = 960分钟)
 | ||||||
|  |         const tradingEndMinutes = 16 * 60; | ||||||
|  |          | ||||||
|  |         // 判断当前时间是否在交易时段内
 | ||||||
|  |         return totalMinutes >= tradingStartMinutes && totalMinutes <= tradingEndMinutes; | ||||||
|  |     } | ||||||
|  |      | ||||||
|     // 设置图表自适应
 |     // 设置图表自适应
 | ||||||
|     window.addEventListener('resize', function() { |     window.addEventListener('resize', function() { | ||||||
|         if (northChart) { |         if (northChart) { | ||||||
|  | @ -68,6 +124,13 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|                 console.error('南向资金图表调整大小失败:', e); |                 console.error('南向资金图表调整大小失败:', e); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         if (rzrqChart) { | ||||||
|  |             try { | ||||||
|  |                 rzrqChart.resize(); | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.error('融资融券图表调整大小失败:', e); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     }); |     }); | ||||||
|      |      | ||||||
|     // 刷新按钮事件
 |     // 刷新按钮事件
 | ||||||
|  | @ -75,9 +138,56 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     if (refreshBtn) { |     if (refreshBtn) { | ||||||
|         refreshBtn.addEventListener('click', function() { |         refreshBtn.addEventListener('click', function() { | ||||||
|             loadData(); |             loadData(); | ||||||
|  |              | ||||||
|  |             // 如果当前不在交易时段但点击了刷新按钮,显示提示信息
 | ||||||
|  |             if (!isWithinTradingHours()) { | ||||||
|  |                 showMessage('当前不在交易时段(9:20-16:00),数据可能不会更新'); | ||||||
|  |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     // 融资融券刷新按钮事件
 | ||||||
|  |     const rzrqRefreshBtn = document.getElementById('rzrqRefreshBtn'); | ||||||
|  |     if (rzrqRefreshBtn) { | ||||||
|  |         rzrqRefreshBtn.addEventListener('click', function() { | ||||||
|  |             if (rzrqChart) { | ||||||
|  |                 rzrqChart.showLoading(); | ||||||
|  |                 loadRzrqData(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     // 融资融券指标切换按钮点击事件
 | ||||||
|  |     const metricButtons = document.querySelectorAll('.btn-group button[data-metric]'); | ||||||
|  |     if (metricButtons && metricButtons.length > 0) { | ||||||
|  |         metricButtons.forEach(button => { | ||||||
|  |             button.addEventListener('click', function() { | ||||||
|  |                 metricButtons.forEach(btn => btn.classList.remove('active')); | ||||||
|  |                 this.classList.add('active'); | ||||||
|  |                  | ||||||
|  |                 currentMetric = this.getAttribute('data-metric'); | ||||||
|  |                 updateRzrqChart(); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 显示消息提示 | ||||||
|  |      */ | ||||||
|  |     function showMessage(message) { | ||||||
|  |         console.log(message); | ||||||
|  |         // 可以在这里添加Toast或其他UI提示
 | ||||||
|  |         // 例如:
 | ||||||
|  |         const updateTimeElem = document.getElementById('updateTime'); | ||||||
|  |         if (updateTimeElem) { | ||||||
|  |             const originalText = updateTimeElem.textContent; | ||||||
|  |             updateTimeElem.textContent = message; | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 updateTimeElem.textContent = originalText; | ||||||
|  |             }, 3000); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|     /** |     /** | ||||||
|      * 加载北向和南向资金数据 |      * 加载北向和南向资金数据 | ||||||
|      */ |      */ | ||||||
|  | @ -314,7 +424,14 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|             // 更新时间
 |             // 更新时间
 | ||||||
|             const updateTimeElem = document.getElementById('updateTime'); |             const updateTimeElem = document.getElementById('updateTime'); | ||||||
|             if (updateTimeElem) { |             if (updateTimeElem) { | ||||||
|                 updateTimeElem.textContent = '最后更新时间: ' + data.update_time; |                 let timeText = '最后更新时间: ' + data.update_time; | ||||||
|  |                  | ||||||
|  |                 // 如果不在交易时段,添加提示
 | ||||||
|  |                 if (!isWithinTradingHours()) { | ||||||
|  |                     timeText += ' (非交易时段)'; | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 updateTimeElem.textContent = timeText; | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             // 创建简单的图表配置
 |             // 创建简单的图表配置
 | ||||||
|  | @ -592,4 +709,603 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|             southChart.hideLoading(); |             southChart.hideLoading(); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |      | ||||||
|  |     // ============ 融资融券图表相关功能 ============
 | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 初始化融资融券图表 | ||||||
|  |      */ | ||||||
|  |     function initRzrqChart() { | ||||||
|  |         if (rzrqChart) { | ||||||
|  |             rzrqChart.dispose(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         rzrqChart = echarts.init(document.getElementById('rzrqChart')); | ||||||
|  |          | ||||||
|  |         // 设置图表加载中状态
 | ||||||
|  |         if (rzrqChart) { | ||||||
|  |             rzrqChart.showLoading(); | ||||||
|  |              | ||||||
|  |             // 加载数据
 | ||||||
|  |             loadRzrqData(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 加载融资融券数据 | ||||||
|  |      */ | ||||||
|  |     function loadRzrqData() { | ||||||
|  |         $.ajax({ | ||||||
|  |             url: '/api/rzrq/chart_data', | ||||||
|  |             type: 'GET', | ||||||
|  |             data: { | ||||||
|  |                 days: 90  // 默认加载90天数据
 | ||||||
|  |             }, | ||||||
|  |             dataType: 'json', | ||||||
|  |             success: function(response) { | ||||||
|  |                 if (response.status === 'success') { | ||||||
|  |                     rzrqData = response.data; | ||||||
|  |                     rzrqChartData = response.data; // 用于存储融资融券图表的原始数据
 | ||||||
|  |                     updateRzrqChart(); | ||||||
|  |                     $('#rzrqUpdateTime').text('数据更新时间: ' + rzrqData.last_update); | ||||||
|  |                      | ||||||
|  |                     // 初始化融资融券图表的索引选择器
 | ||||||
|  |                     if (!rzrqIndexSelector) { | ||||||
|  |                         initRzrqIndexSelector(); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     rzrqChart.hideLoading(); | ||||||
|  |                     rzrqChart.setOption({ | ||||||
|  |                         title: { | ||||||
|  |                             text: '数据加载失败', | ||||||
|  |                             textStyle: { | ||||||
|  |                                 color: '#999', | ||||||
|  |                                 fontSize: 14 | ||||||
|  |                             }, | ||||||
|  |                             left: 'center', | ||||||
|  |                             top: 'center' | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             error: function(xhr, status, error) { | ||||||
|  |                 rzrqChart.hideLoading(); | ||||||
|  |                 rzrqChart.setOption({ | ||||||
|  |                     title: { | ||||||
|  |                         text: '数据加载失败: ' + error, | ||||||
|  |                         textStyle: { | ||||||
|  |                             color: '#999', | ||||||
|  |                             fontSize: 14 | ||||||
|  |                         }, | ||||||
|  |                         left: 'center', | ||||||
|  |                         top: 'center' | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 初始化融资融券图表的索引选择器 | ||||||
|  |      */ | ||||||
|  |     function initRzrqIndexSelector() { | ||||||
|  |         rzrqIndexSelector = new IndexSelector('rzrqChart', { | ||||||
|  |             // 获取图表当前显示的日期范围
 | ||||||
|  |             getDateRange: function() { | ||||||
|  |                 // 如果有rzrq图表的日期数据,返回第一个和最后一个日期
 | ||||||
|  |                 if (rzrqChartData && rzrqChartData.dates && rzrqChartData.dates.length) { | ||||||
|  |                     return { | ||||||
|  |                         startDate: rzrqChartData.dates[0], | ||||||
|  |                         endDate: rzrqChartData.dates[rzrqChartData.dates.length - 1] | ||||||
|  |                     }; | ||||||
|  |                 } | ||||||
|  |                 return { startDate: null, endDate: null }; | ||||||
|  |             }, | ||||||
|  |             // 指数数据更新时的回调
 | ||||||
|  |             onChange: function(selectedIndices) { | ||||||
|  |                 updateRzrqChartWithIndices(selectedIndices); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 将数据对齐到日期范围 | ||||||
|  |      */ | ||||||
|  |     function alignDataToDateRange(sourceDates, sourceValues, targetDates) { | ||||||
|  |         const result = new Array(targetDates.length).fill(null); | ||||||
|  |         const dateMap = {}; | ||||||
|  |          | ||||||
|  |         // 创建源数据日期到值的映射
 | ||||||
|  |         sourceDates.forEach((date, index) => { | ||||||
|  |             dateMap[date] = sourceValues[index]; | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // 映射到目标日期
 | ||||||
|  |         targetDates.forEach((date, index) => { | ||||||
|  |             if (dateMap[date] !== undefined) { | ||||||
|  |                 result[index] = dateMap[date]; | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         return result; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 更新融资融券图表,添加指数数据 | ||||||
|  |      */ | ||||||
|  |     function updateRzrqChartWithIndices(indices) { | ||||||
|  |         if (!rzrqChart) return; | ||||||
|  |          | ||||||
|  |         // 获取当前图表配置
 | ||||||
|  |         const option = rzrqChart.getOption(); | ||||||
|  |          | ||||||
|  |         // 保留原始系列数据(融资融券数据)
 | ||||||
|  |         const originalSeries = option.series.filter(s => s.name.indexOf('指数') === -1); | ||||||
|  |          | ||||||
|  |         // 清除所有指数系列
 | ||||||
|  |         option.series = [...originalSeries]; | ||||||
|  |          | ||||||
|  |         // 如果没有选择指数,则移除右侧Y轴
 | ||||||
|  |         if (indices.length === 0) { | ||||||
|  |             option.yAxis = option.yAxis.filter(axis => axis.name !== '指数值'); | ||||||
|  |             rzrqChart.setOption(option, true); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 计算所有指数数据的最小值和最大值
 | ||||||
|  |         let allValues = []; | ||||||
|  |         indices.forEach(index => { | ||||||
|  |             if (index.data && index.data.values) { | ||||||
|  |                 // 过滤掉null和undefined值
 | ||||||
|  |                 const validValues = index.data.values.filter(v => v !== null && v !== undefined); | ||||||
|  |                 allValues = allValues.concat(validValues); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // 计算数据范围
 | ||||||
|  |         let minValue = Math.min(...allValues); | ||||||
|  |         let maxValue = Math.max(...allValues); | ||||||
|  |          | ||||||
|  |         // 增加一定的边距,使图表更美观
 | ||||||
|  |         const range = maxValue - minValue; | ||||||
|  |         const padding = range * 0.1; // 上下各留10%的边距
 | ||||||
|  |         minValue = minValue - padding; | ||||||
|  |         maxValue = maxValue + padding; | ||||||
|  |         minValue = Math.round(minValue * 10) / 10; | ||||||
|  |         maxValue = Math.round(maxValue * 10) / 10; | ||||||
|  |         // 添加指数系列
 | ||||||
|  |         indices.forEach(index => { | ||||||
|  |             if (!index.data || !index.data.dates) return; | ||||||
|  |              | ||||||
|  |             // 将指数数据对齐到融资融券数据的日期范围
 | ||||||
|  |             const alignedData = alignDataToDateRange(index.data.dates, index.data.values, option.xAxis[0].data); | ||||||
|  |              | ||||||
|  |             // 创建新的Y轴用于指数
 | ||||||
|  |             if (!option.yAxis.some(axis => axis.name === '指数值')) { | ||||||
|  |                 option.yAxis.push({ | ||||||
|  |                     name: '指数值', | ||||||
|  |                     type: 'value', | ||||||
|  |                     position: 'right', | ||||||
|  |                     min: minValue,  // 使用计算出的最小值
 | ||||||
|  |                     max: maxValue,  // 使用计算出的最大值
 | ||||||
|  |                     splitLine: { | ||||||
|  |                         show: false | ||||||
|  |                     }, | ||||||
|  |                     axisLabel: { | ||||||
|  |                         formatter: '{value}' | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             } else { | ||||||
|  |                 // 如果已存在指数Y轴,更新其范围
 | ||||||
|  |                 const indexAxis = option.yAxis.find(axis => axis.name === '指数值'); | ||||||
|  |                 if (indexAxis) { | ||||||
|  |                     indexAxis.min = minValue; | ||||||
|  |                     indexAxis.max = maxValue; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 添加指数系列
 | ||||||
|  |             option.series.push({ | ||||||
|  |                 name: `${index.name}`,  // 移除"指数"后缀,避免在tooltip中显示为"上证指数指数"
 | ||||||
|  |                 type: 'line', | ||||||
|  |                 yAxisIndex: 1, // 使用第二个Y轴
 | ||||||
|  |                 data: alignedData, | ||||||
|  |                 symbol: 'none', | ||||||
|  |                 smooth: true, | ||||||
|  |                 lineStyle: { | ||||||
|  |                     width: 2, | ||||||
|  |                     color: index.color | ||||||
|  |                 }, | ||||||
|  |                 itemStyle: { | ||||||
|  |                     color: index.color | ||||||
|  |                 }, | ||||||
|  |                 // 标记这是指数数据
 | ||||||
|  |                 isIndex: true | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // 更新图例
 | ||||||
|  |         option.legend = { | ||||||
|  |             data: [ | ||||||
|  |                 ...originalSeries.map(s => s.name), | ||||||
|  |                 ...indices.map(i => i.name)  // 使用原始指数名称
 | ||||||
|  |             ], | ||||||
|  |             selected: { | ||||||
|  |                 ...option.legend?.selected || {}, | ||||||
|  |                 ...indices.reduce((acc, index) => { | ||||||
|  |                     acc[index.name] = true;  // 使用原始指数名称
 | ||||||
|  |                     return acc; | ||||||
|  |                 }, {}) | ||||||
|  |             }, | ||||||
|  |             top: 40  // 将图例下移,避免与标题重叠
 | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         // 应用更新
 | ||||||
|  |         rzrqChart.setOption(option, true); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 更新融资融券图表 | ||||||
|  |      */ | ||||||
|  |     function updateRzrqChart() { | ||||||
|  |         if (!rzrqData || !rzrqData.success) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 查找当前指标的数据
 | ||||||
|  |         let currentSeries = rzrqData.series.find(s => s.name === getMetricName(currentMetric)); | ||||||
|  |          | ||||||
|  |         // 如果未找到,使用第一个系列
 | ||||||
|  |         if (!currentSeries && rzrqData.series.length > 0) { | ||||||
|  |             currentSeries = rzrqData.series[0]; | ||||||
|  |             currentMetric = getMetricKey(currentSeries.name); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         if (!currentSeries) { | ||||||
|  |             rzrqChart.hideLoading(); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 计算数据的最小值和最大值,用于设置Y轴范围
 | ||||||
|  |         const validData = currentSeries.data.filter(value => value !== null && value !== undefined); | ||||||
|  |         let min = Math.min(...validData); | ||||||
|  |         let max = Math.max(...validData); | ||||||
|  |          | ||||||
|  |         // 为了图表美观,给最小值和最大值增加一些间距
 | ||||||
|  |         const range = max - min; | ||||||
|  |         min = min - range * 0.05;  // 下方留5%的间距
 | ||||||
|  |         max = max + range * 0.05;  // 上方留5%的间距
 | ||||||
|  |          | ||||||
|  |         // 设置图表选项
 | ||||||
|  |         const option = { | ||||||
|  |             title: { | ||||||
|  |                 text: currentSeries.name + '走势', | ||||||
|  |                 left: 'center', | ||||||
|  |                 top: 10  // 固定标题位置在顶部
 | ||||||
|  |             }, | ||||||
|  |             tooltip: { | ||||||
|  |                 trigger: 'axis', | ||||||
|  |                 formatter: function(params) { | ||||||
|  |                     let tooltip = params[0].axisValue + '<br/>'; | ||||||
|  |                     params.forEach(param => { | ||||||
|  |                         // 判断是否为指数系列
 | ||||||
|  |                         const isIndexSeries = param.seriesIndex > 0 && param.seriesName.indexOf('融资') === -1 && param.seriesName.indexOf('融券') === -1; | ||||||
|  |                          | ||||||
|  |                         // 对于指数系列,不添加单位;对于融资融券系列,添加相应单位
 | ||||||
|  |                         tooltip += param.marker + ' ' + param.seriesName + ': ' +  | ||||||
|  |                                   param.value + (isIndexSeries ? '' : (' ' + currentSeries.unit)) + '<br/>'; | ||||||
|  |                     }); | ||||||
|  |                     return tooltip; | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             xAxis: { | ||||||
|  |                 type: 'category', | ||||||
|  |                 data: rzrqData.dates, | ||||||
|  |                 axisLabel: { | ||||||
|  |                     rotate: 45 | ||||||
|  |                 }, | ||||||
|  |                 axisLine: { | ||||||
|  |                     lineStyle: { | ||||||
|  |                         color: '#999' | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             yAxis: { | ||||||
|  |                 type: 'value', | ||||||
|  |                 name: currentSeries.unit, | ||||||
|  |                 min: min,  // 设置Y轴最小值为数据的最小值
 | ||||||
|  |                 max: max,  // 设置Y轴最大值为数据的最大值
 | ||||||
|  |                 nameTextStyle: { | ||||||
|  |                     padding: [0, 30, 0, 0] | ||||||
|  |                 }, | ||||||
|  |                 axisLine: { | ||||||
|  |                     show: true, | ||||||
|  |                     lineStyle: { | ||||||
|  |                         color: '#999' | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 splitLine: { | ||||||
|  |                     lineStyle: { | ||||||
|  |                         color: '#eee' | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }, | ||||||
|  |             dataZoom: [ | ||||||
|  |                 { | ||||||
|  |                     type: 'inside', | ||||||
|  |                     start: 0, | ||||||
|  |                     end: 100 | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     start: 0, | ||||||
|  |                     end: 100 | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             series: [ | ||||||
|  |                 { | ||||||
|  |                     name: currentSeries.name, | ||||||
|  |                     type: 'line', | ||||||
|  |                     data: currentSeries.data, | ||||||
|  |                     lineStyle: { | ||||||
|  |                         width: 3 | ||||||
|  |                     }, | ||||||
|  |                     itemStyle: { | ||||||
|  |                         color: '#1890ff' | ||||||
|  |                     }, | ||||||
|  |                     areaStyle: { | ||||||
|  |                         color: { | ||||||
|  |                             type: 'linear', | ||||||
|  |                             x: 0, | ||||||
|  |                             y: 0, | ||||||
|  |                             x2: 0, | ||||||
|  |                             y2: 1, | ||||||
|  |                             colorStops: [ | ||||||
|  |                                 { | ||||||
|  |                                     offset: 0, | ||||||
|  |                                     color: 'rgba(24, 144, 255, 0.3)' | ||||||
|  |                                 }, | ||||||
|  |                                 { | ||||||
|  |                                     offset: 1, | ||||||
|  |                                     color: 'rgba(24, 144, 255, 0.1)' | ||||||
|  |                                 } | ||||||
|  |                             ] | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     connectNulls: true | ||||||
|  |                 } | ||||||
|  |             ], | ||||||
|  |             grid: { | ||||||
|  |                 left: '3%', | ||||||
|  |                 right: '4%', | ||||||
|  |                 bottom: '15%', | ||||||
|  |                 top: '70px',  // 增加顶部空间,给标题和图例留出足够位置
 | ||||||
|  |                 containLabel: true | ||||||
|  |             }, | ||||||
|  |             legend: { | ||||||
|  |                 top: 40,  // 将图例放在标题下方
 | ||||||
|  |                 left: 'center' | ||||||
|  |             }, | ||||||
|  |             toolbox: { | ||||||
|  |                 feature: { | ||||||
|  |                     saveAsImage: {} | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  |          | ||||||
|  |         rzrqChart.hideLoading(); | ||||||
|  |         rzrqChart.setOption(option); | ||||||
|  |          | ||||||
|  |         // 更新风险指标显示
 | ||||||
|  |         updateRiskIndicators(); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 更新风险指标显示 | ||||||
|  |      */ | ||||||
|  |     function updateRiskIndicators() { | ||||||
|  |         // 检查是否有风险指标数据
 | ||||||
|  |         if (!rzrqData || !rzrqData.risk_indicators) { | ||||||
|  |             // 隐藏或显示无数据状态
 | ||||||
|  |             console.log('无风险指标数据'); | ||||||
|  |             // 清理可能存在的旧数据
 | ||||||
|  |             $('#summaryBalanceChangeDesc').text('--'); | ||||||
|  |             $('#summarySecuritiesChangeDesc').text('--'); | ||||||
|  |             $('#overallRiskLevel').text('N/A'); | ||||||
|  |             $('#overallRiskDesc').text('无法加载风险数据。'); | ||||||
|  |             $('#overallRiskAlert').removeClass('alert-danger alert-warning alert-success alert-info'); | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         const indicators = rzrqData.risk_indicators; | ||||||
|  |          | ||||||
|  |         // 辅助函数:格式化描述文本并设置
 | ||||||
|  |         function formatAndSetDescription(targetSelector, descriptionText, defaultText) { | ||||||
|  |             const descElement = $(targetSelector); | ||||||
|  |             if (!descriptionText) { | ||||||
|  |                 descElement.text(defaultText || '--'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 正则表达式匹配数字(包括正负号和小数点)和百分号
 | ||||||
|  |             const percentageRegex = /([-+]?\d*\.?\d+)%/; | ||||||
|  |             const match = descriptionText.match(percentageRegex); | ||||||
|  |              | ||||||
|  |             if (match && match[1]) { | ||||||
|  |                 const percentageValueStr = match[0]; // 例如 "+0.15%" 或 "-3.14%"
 | ||||||
|  |                 const numericValue = parseFloat(match[1]); // 例如 0.15 或 -3.14
 | ||||||
|  |                  | ||||||
|  |                 let colorClass = 'neutral'; | ||||||
|  |                 if (numericValue > 0) { | ||||||
|  |                     colorClass = 'positive'; | ||||||
|  |                 } else if (numericValue < 0) { | ||||||
|  |                     colorClass = 'negative'; | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 const styledDescription = descriptionText.replace( | ||||||
|  |                     percentageRegex, | ||||||
|  |                     `<span class="percentage-value ${colorClass}">${percentageValueStr}</span>` | ||||||
|  |                 ); | ||||||
|  |                 descElement.html(styledDescription); | ||||||
|  |             } else { | ||||||
|  |                 descElement.text(descriptionText); // 没有百分比则直接显示
 | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新综合风险评估
 | ||||||
|  |         if (indicators.overall_risk) { | ||||||
|  |             const overallRisk = indicators.overall_risk; | ||||||
|  |             const riskLevel = overallRisk.level; | ||||||
|  |              | ||||||
|  |             // 设置风险等级和描述
 | ||||||
|  |             $('#overallRiskLevel').text(riskLevel); | ||||||
|  |             $('#overallRiskDesc').text(overallRisk.description); | ||||||
|  |              | ||||||
|  |             // 根据风险等级设置颜色
 | ||||||
|  |             const alertElem = $('#overallRiskAlert'); | ||||||
|  |             alertElem.removeClass('alert-danger alert-warning alert-success alert-info'); | ||||||
|  |              | ||||||
|  |             if (riskLevel === '高') { | ||||||
|  |                 alertElem.addClass('alert-danger'); | ||||||
|  |             } else if (riskLevel === '中') { | ||||||
|  |                 alertElem.addClass('alert-warning'); | ||||||
|  |             } else if (riskLevel === '低') { | ||||||
|  |                 alertElem.addClass('alert-success'); | ||||||
|  |             } else { | ||||||
|  |                 alertElem.addClass('alert-info'); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             $('#overallRiskLevel').text('N/A'); | ||||||
|  |             $('#overallRiskDesc').text('综合风险数据缺失。'); | ||||||
|  |             $('#overallRiskAlert').removeClass('alert-danger alert-warning alert-success').addClass('alert-info'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新融资融券余额变化 - 新的汇总位置
 | ||||||
|  |         if (indicators.balance_risk && indicators.balance_risk.description) { | ||||||
|  |             formatAndSetDescription('#summaryBalanceChangeDesc', indicators.balance_risk.description); | ||||||
|  |         } else { | ||||||
|  |             $('#summaryBalanceChangeDesc').text('融资融券余额变化数据: --'); | ||||||
|  |         } | ||||||
|  |         // 更新卡片内的详细信息(不含移动的描述)
 | ||||||
|  |         if (indicators.recent_balance_change && indicators.balance_risk) { | ||||||
|  |             const balanceChange = indicators.recent_balance_change; | ||||||
|  |             const balanceRisk = indicators.balance_risk; | ||||||
|  |             let rateText = balanceChange.rate > 0 ? '+' : ''; | ||||||
|  |             rateText += balanceChange.rate + '%'; | ||||||
|  |             $('#balanceChangeRate').text(rateText).removeClass('text-success text-danger').addClass(balanceChange.rate > 0 ? 'text-danger' : 'text-success'); | ||||||
|  |             $('#balanceRiskLevel').text(balanceRisk.level); | ||||||
|  |             setRiskLevelColor('#balanceRiskLevel', balanceRisk.level); | ||||||
|  |             // $('#balanceRiskDesc').text(balanceRisk.description); // 这行被移动了
 | ||||||
|  |         } else { | ||||||
|  |             $('#balanceChangeRate').text('--'); | ||||||
|  |             $('#balanceRiskLevel').text('--'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新融券余额变化 - 新的汇总位置
 | ||||||
|  |         if (indicators.securities_risk && indicators.securities_risk.description) { | ||||||
|  |             formatAndSetDescription('#summarySecuritiesChangeDesc', indicators.securities_risk.description); | ||||||
|  |         } else { | ||||||
|  |             $('#summarySecuritiesChangeDesc').text('融券余额变化数据: --'); | ||||||
|  |         } | ||||||
|  |         // 更新卡片内的详细信息(不含移动的描述)
 | ||||||
|  |         if (indicators.securities_balance_change && indicators.securities_risk) { | ||||||
|  |             const securitiesChange = indicators.securities_balance_change; | ||||||
|  |             const securitiesRisk = indicators.securities_risk; | ||||||
|  |             let rateText = securitiesChange.rate > 0 ? '+' : ''; | ||||||
|  |             rateText += securitiesChange.rate + '%'; | ||||||
|  |             $('#securitiesChangeRate').text(rateText).removeClass('text-success text-danger').addClass(securitiesChange.rate > 0 ? 'text-danger' : 'text-success'); | ||||||
|  |             $('#securitiesRiskLevel').text(securitiesRisk.level); | ||||||
|  |             setRiskLevelColor('#securitiesRiskLevel', securitiesRisk.level); | ||||||
|  |             // $('#securitiesRiskDesc').text(securitiesRisk.description); // 这行被移动了
 | ||||||
|  |         } else { | ||||||
|  |             $('#securitiesChangeRate').text('--'); | ||||||
|  |             $('#securitiesRiskLevel').text('--'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新融资偿还比率 (这部分逻辑不变,仅为上下文)
 | ||||||
|  |         if (indicators.repay_buy_ratio && indicators.repay_risk) { | ||||||
|  |             const repayRatio = indicators.repay_buy_ratio; | ||||||
|  |             const repayRisk = indicators.repay_risk; | ||||||
|  |             $('#repayBuyRatio').text(repayRatio.value); | ||||||
|  |             if (repayRatio.value > 1.1) { | ||||||
|  |                 $('#repayBuyRatio').removeClass('text-success').addClass('text-danger'); | ||||||
|  |             } else if (repayRatio.value < 0.9) { | ||||||
|  |                 $('#repayBuyRatio').removeClass('text-danger').addClass('text-success'); | ||||||
|  |             } else { | ||||||
|  |                 $('#repayBuyRatio').removeClass('text-danger text-success'); | ||||||
|  |             } | ||||||
|  |             $('#repayRiskLevel').text(repayRisk.level); | ||||||
|  |             setRiskLevelColor('#repayRiskLevel', repayRisk.level); | ||||||
|  |             $('#repayRiskDesc').text(repayRisk.description); // 这个描述保留在原位
 | ||||||
|  |         } else { | ||||||
|  |             $('#repayBuyRatio').text('--'); | ||||||
|  |             $('#repayRiskLevel').text('--'); | ||||||
|  |             $('#repayRiskDesc').text('--'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新融资占比 (这部分逻辑不变,仅为上下文)
 | ||||||
|  |         if (indicators.financing_ratio) { | ||||||
|  |             $('#financingRatio').text(indicators.financing_ratio + '%'); | ||||||
|  |             if (indicators.financing_ratio_percentile !== undefined) { | ||||||
|  |                 $('#financingRatioPercentile').text(indicators.financing_ratio_percentile + '%'); | ||||||
|  |                 if (indicators.financing_ratio_percentile > 80) { | ||||||
|  |                     $('#financingRatioPercentile').removeClass('text-success text-warning').addClass('text-danger'); | ||||||
|  |                 } else if (indicators.financing_ratio_percentile > 50) { | ||||||
|  |                     $('#financingRatioPercentile').removeClass('text-success text-danger').addClass('text-warning'); | ||||||
|  |                 } else { | ||||||
|  |                     $('#financingRatioPercentile').removeClass('text-danger text-warning').addClass('text-success'); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 $('#financingRatioPercentile').text('数据不足'); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             $('#financingRatio').text('--'); | ||||||
|  |             $('#financingRatioPercentile').text('--'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 根据风险等级设置文本颜色 | ||||||
|  |      */ | ||||||
|  |     function setRiskLevelColor(selector, level) { | ||||||
|  |         const elem = $(selector); | ||||||
|  |         elem.removeClass('text-danger text-warning text-success text-info'); | ||||||
|  |          | ||||||
|  |         if (level === '高') { | ||||||
|  |             elem.addClass('text-danger'); | ||||||
|  |         } else if (level === '中') { | ||||||
|  |             elem.addClass('text-warning'); | ||||||
|  |         } else if (level === '低') { | ||||||
|  |             elem.addClass('text-success'); | ||||||
|  |         } else { | ||||||
|  |             elem.addClass('text-info'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 根据数据系列键名获取显示名称 | ||||||
|  |      */ | ||||||
|  |     function getMetricName(metricKey) { | ||||||
|  |         const metricMap = { | ||||||
|  |             'total_rzrq_balance': '融资融券余额合计', | ||||||
|  |             'total_financing_buy': '融资买入额合计', | ||||||
|  |             'total_financing_balance': '融资余额合计', | ||||||
|  |             'financing_repayment': '融资偿还', | ||||||
|  |             'securities_balance': '融券余额' | ||||||
|  |         }; | ||||||
|  |         return metricMap[metricKey] || metricKey; | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 根据显示名称获取数据系列键名 | ||||||
|  |      */ | ||||||
|  |     function getMetricKey(metricName) { | ||||||
|  |         const metricMap = { | ||||||
|  |             '融资融券余额合计': 'total_rzrq_balance', | ||||||
|  |             '融资买入额合计': 'total_financing_buy', | ||||||
|  |             '融资余额合计': 'total_financing_balance', | ||||||
|  |             '融资偿还': 'financing_repayment', | ||||||
|  |             '融券余额': 'securities_balance' | ||||||
|  |         }; | ||||||
|  |         return metricMap[metricName] || metricName; | ||||||
|  |     } | ||||||
| });  | });  | ||||||
|  | @ -106,6 +106,11 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|             option.textContent = industry.name; |             option.textContent = industry.name; | ||||||
|             industryNameSelect.appendChild(option); |             industryNameSelect.appendChild(option); | ||||||
|         }); |         }); | ||||||
|  |          | ||||||
|  |         // 如果存在Select2,刷新它
 | ||||||
|  |         if ($.fn.select2 && $(industryNameSelect).data('select2')) { | ||||||
|  |             $(industryNameSelect).trigger('change'); | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     /** |     /** | ||||||
|  | @ -734,6 +739,11 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     function resetForm() { |     function resetForm() { | ||||||
|         industryForm.reset(); |         industryForm.reset(); | ||||||
|          |          | ||||||
|  |         // 重置Select2
 | ||||||
|  |         if ($.fn.select2) { | ||||||
|  |             $(industryNameSelect).val('').trigger('change'); | ||||||
|  |         } | ||||||
|  |          | ||||||
|         // 隐藏结果和错误信息
 |         // 隐藏结果和错误信息
 | ||||||
|         resultCard.classList.add('d-none'); |         resultCard.classList.add('d-none'); | ||||||
|         errorAlert.classList.add('d-none'); |         errorAlert.classList.add('d-none'); | ||||||
|  |  | ||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -8,8 +8,8 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     const stockCodeInput = document.getElementById('stockCode'); |     const stockCodeInput = document.getElementById('stockCode'); | ||||||
|     const startDateInput = document.getElementById('startDate'); |     const startDateInput = document.getElementById('startDate'); | ||||||
|     const metricSelect = document.getElementById('metric'); |     const metricSelect = document.getElementById('metric'); | ||||||
|     const industryNameInput = document.getElementById('industryName'); |     const industryNameSelect = document.getElementById('industryName'); | ||||||
|     const conceptNameInput = document.getElementById('conceptName'); |     const conceptNameSelect = document.getElementById('conceptName'); | ||||||
|     const analyzeBtn = document.getElementById('analyzeBtn'); |     const analyzeBtn = document.getElementById('analyzeBtn'); | ||||||
|     const resetBtn = document.getElementById('resetBtn'); |     const resetBtn = document.getElementById('resetBtn'); | ||||||
|     const loadingSpinner = document.getElementById('loadingSpinner'); |     const loadingSpinner = document.getElementById('loadingSpinner'); | ||||||
|  | @ -26,6 +26,128 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     // 定义图表实例
 |     // 定义图表实例
 | ||||||
|     let myChart = null; |     let myChart = null; | ||||||
|      |      | ||||||
|  |     // 初始化Select2插件
 | ||||||
|  |     $(document).ready(function() { | ||||||
|  |         // 初始化行业下拉框
 | ||||||
|  |         $('#industryName').select2({ | ||||||
|  |             placeholder: '请选择行业', | ||||||
|  |             allowClear: true, | ||||||
|  |             width: '100%' | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // 初始化概念板块下拉框
 | ||||||
|  |         $('#conceptName').select2({ | ||||||
|  |             placeholder: '请选择概念板块', | ||||||
|  |             allowClear: true, | ||||||
|  |             width: '100%' | ||||||
|  |         }); | ||||||
|  |          | ||||||
|  |         // 加载行业数据
 | ||||||
|  |         loadIndustryData(); | ||||||
|  |          | ||||||
|  |         // 加载概念板块数据
 | ||||||
|  |         loadConceptData(); | ||||||
|  |     }); | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 加载行业数据 | ||||||
|  |      */ | ||||||
|  |     function loadIndustryData() { | ||||||
|  |         fetch('/api/industry/list') | ||||||
|  |             .then(response => { | ||||||
|  |                 if (!response.ok) { | ||||||
|  |                     throw new Error('获取行业列表失败'); | ||||||
|  |                 } | ||||||
|  |                 return response.json(); | ||||||
|  |             }) | ||||||
|  |             .then(data => { | ||||||
|  |                 if (data.status === 'success') { | ||||||
|  |                     // 先清空下拉框(保留第一个选项)
 | ||||||
|  |                     while (industryNameSelect.options.length > 1) { | ||||||
|  |                         industryNameSelect.remove(1); | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // 添加选项
 | ||||||
|  |                     data.data.forEach(industry => { | ||||||
|  |                         const option = new Option(industry.name, industry.name); | ||||||
|  |                         industryNameSelect.add(option); | ||||||
|  |                     }); | ||||||
|  |                      | ||||||
|  |                     // 刷新Select2
 | ||||||
|  |                     $(industryNameSelect).trigger('change'); | ||||||
|  |                 } else { | ||||||
|  |                     console.error('加载行业数据失败:', data.message); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .catch(error => { | ||||||
|  |                 console.error('获取行业列表时出错:', error); | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 加载概念板块数据 | ||||||
|  |      */ | ||||||
|  |     function loadConceptData() { | ||||||
|  |         fetch('/api/concept/list') | ||||||
|  |             .then(response => { | ||||||
|  |                 if (!response.ok) { | ||||||
|  |                     throw new Error('获取概念板块列表失败'); | ||||||
|  |                 } | ||||||
|  |                 return response.json(); | ||||||
|  |             }) | ||||||
|  |             .then(data => { | ||||||
|  |                 if (data.status === 'success') { | ||||||
|  |                     // 先清空下拉框(保留第一个选项)
 | ||||||
|  |                     while (conceptNameSelect.options.length > 1) { | ||||||
|  |                         conceptNameSelect.remove(1); | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     // 添加选项
 | ||||||
|  |                     data.data.forEach(concept => { | ||||||
|  |                         const option = new Option(concept.name, concept.name); | ||||||
|  |                         conceptNameSelect.add(option); | ||||||
|  |                     }); | ||||||
|  |                      | ||||||
|  |                     // 刷新Select2
 | ||||||
|  |                     $(conceptNameSelect).trigger('change'); | ||||||
|  |                 } else { | ||||||
|  |                     console.error('加载概念板块数据失败:', data.message); | ||||||
|  |                     // 加载失败时使用硬编码的常见概念作为备用
 | ||||||
|  |                     loadFallbackConcepts(); | ||||||
|  |                 } | ||||||
|  |             }) | ||||||
|  |             .catch(error => { | ||||||
|  |                 console.error('获取概念板块列表时出错:', error); | ||||||
|  |                 // 出错时使用硬编码的常见概念作为备用
 | ||||||
|  |                 loadFallbackConcepts(); | ||||||
|  |             }); | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     /** | ||||||
|  |      * 加载备用的概念板块数据(硬编码) | ||||||
|  |      */ | ||||||
|  |     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'); | ||||||
|  |     } | ||||||
|  |      | ||||||
|     // 监听表单提交事件
 |     // 监听表单提交事件
 | ||||||
|     valuationForm.addEventListener('submit', function(event) { |     valuationForm.addEventListener('submit', function(event) { | ||||||
|         event.preventDefault(); |         event.preventDefault(); | ||||||
|  | @ -52,8 +174,8 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|         const stockCode = stockCodeInput.value.trim(); |         const stockCode = stockCodeInput.value.trim(); | ||||||
|         const startDate = startDateInput.value; |         const startDate = startDateInput.value; | ||||||
|         const metric = metricSelect.value; |         const metric = metricSelect.value; | ||||||
|         const industryName = industryNameInput.value.trim(); |         const industryName = industryNameSelect.value.trim(); | ||||||
|         const conceptName = conceptNameInput.value.trim(); |         const conceptName = conceptNameSelect.value.trim(); | ||||||
|          |          | ||||||
|         // 构建请求URL
 |         // 构建请求URL
 | ||||||
|         let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`; |         let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`; | ||||||
|  | @ -452,6 +574,10 @@ document.addEventListener('DOMContentLoaded', function() { | ||||||
|     function resetForm() { |     function resetForm() { | ||||||
|         valuationForm.reset(); |         valuationForm.reset(); | ||||||
|          |          | ||||||
|  |         // 重置Select2下拉框
 | ||||||
|  |         $(industryNameSelect).val('').trigger('change'); | ||||||
|  |         $(conceptNameSelect).val('').trigger('change'); | ||||||
|  |          | ||||||
|         // 隐藏结果和错误信息
 |         // 隐藏结果和错误信息
 | ||||||
|         resultCard.classList.add('d-none'); |         resultCard.classList.add('d-none'); | ||||||
|         errorAlert.classList.add('d-none'); |         errorAlert.classList.add('d-none'); | ||||||
|  |  | ||||||
|  | @ -6,60 +6,33 @@ | ||||||
|     <title>沪深港通资金流向监控</title> |     <title>沪深港通资金流向监控</title> | ||||||
|     <!-- Bootstrap CSS --> |     <!-- Bootstrap CSS --> | ||||||
|     <link rel="stylesheet" href="../static/css/bootstrap.min.css"> |     <link rel="stylesheet" href="../static/css/bootstrap.min.css"> | ||||||
|  |     <link rel="stylesheet" href="../static/css/hsgt_monitor.css"> | ||||||
|     <!-- 自定义样式 --> |     <!-- 自定义样式 --> | ||||||
|     <style> | 
 | ||||||
|         .card { |  | ||||||
|             margin-bottom: 20px; |  | ||||||
|             box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |  | ||||||
|         } |  | ||||||
|         .card-header { |  | ||||||
|             background-color: #f8f9fa; |  | ||||||
|             font-weight: bold; |  | ||||||
|         } |  | ||||||
|         .money-inflow { |  | ||||||
|             color: #d9534f; |  | ||||||
|             font-weight: bold; |  | ||||||
|         } |  | ||||||
|         .money-outflow { |  | ||||||
|             color: #5cb85c; |  | ||||||
|             font-weight: bold; |  | ||||||
|         } |  | ||||||
|         .chart-container { |  | ||||||
|             height: 350px; |  | ||||||
|             margin-bottom: 20px; |  | ||||||
|         } |  | ||||||
|         .stat-card { |  | ||||||
|             text-align: center; |  | ||||||
|             padding: 10px; |  | ||||||
|             border-radius: 5px; |  | ||||||
|             margin-bottom: 10px; |  | ||||||
|         } |  | ||||||
|         .stat-value { |  | ||||||
|             font-size: 24px; |  | ||||||
|             font-weight: bold; |  | ||||||
|         } |  | ||||||
|         .stat-title { |  | ||||||
|             font-size: 14px; |  | ||||||
|             color: #666; |  | ||||||
|         } |  | ||||||
|         .refresh-btn { |  | ||||||
|             margin-left: 10px; |  | ||||||
|         } |  | ||||||
|         .update-time { |  | ||||||
|             font-size: 12px; |  | ||||||
|             color: #666; |  | ||||||
|             margin-top: 5px; |  | ||||||
|         } |  | ||||||
|         .flow-direction { |  | ||||||
|             font-size: 13px; |  | ||||||
|             color: #666; |  | ||||||
|             font-style: italic; |  | ||||||
|             margin-top: -5px; |  | ||||||
|             margin-bottom: 10px; |  | ||||||
|         } |  | ||||||
|     </style> |  | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|  |     <!-- 导航栏 --> | ||||||
|  |     <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> | ||||||
|  |         <div class="container"> | ||||||
|  |             <a class="navbar-brand" href="/">股票估值分析工具</a> | ||||||
|  |             <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"> | ||||||
|  |                 <span class="navbar-toggler-icon"></span> | ||||||
|  |             </button> | ||||||
|  |             <div class="collapse navbar-collapse" id="navbarNav"> | ||||||
|  |                 <ul class="navbar-nav"> | ||||||
|  |                     <li class="nav-item"> | ||||||
|  |                         <a class="nav-link" href="/">个股估值分析</a> | ||||||
|  |                     </li> | ||||||
|  |                     <li class="nav-item"> | ||||||
|  |                         <a class="nav-link" href="/industry">行业估值分析</a> | ||||||
|  |                     </li> | ||||||
|  |                     <li class="nav-item"> | ||||||
|  |                         <a class="nav-link active" href="/hsgt">资金情况</a> | ||||||
|  |                     </li> | ||||||
|  |                 </ul> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </nav> | ||||||
|     <div class="container-fluid py-4"> |     <div class="container-fluid py-4"> | ||||||
|         <div class="row mb-4"> |         <div class="row mb-4"> | ||||||
|             <div class="col-12"> |             <div class="col-12"> | ||||||
|  | @ -140,21 +113,202 @@ | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <!-- 说明信息 --> |         <!-- 融资融券数据汇总信息 --> | ||||||
|  |         <div class="row mt-3 mb-2"> | ||||||
|  |             <div class="col-12 text-center"> | ||||||
|  |                 <p class="summary-text" id="summaryBalanceChangeDesc">--</p> | ||||||
|  |                 <p class="summary-text" id="summarySecuritiesChangeDesc">--</p> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 融资融券数据展示 --> | ||||||
|         <div class="row mt-4"> |         <div class="row mt-4"> | ||||||
|             <div class="col-12"> |             <div class="col-12"> | ||||||
|                 <div class="card"> |                 <div class="card"> | ||||||
|                     <div class="card-header"> |                     <div class="card-header d-flex justify-content-between align-items-center"> | ||||||
|                         数据说明 |                         <span>融资融券数据监控 (单位:亿元)</span> | ||||||
|  |                         <div class="btn-group" role="group"> | ||||||
|  |                             <button type="button" class="btn btn-sm btn-outline-primary active" data-metric="total_rzrq_balance">融资融券余额合计</button> | ||||||
|  |                             <button type="button" class="btn btn-sm btn-outline-primary" data-metric="total_financing_buy">融资买入额合计</button> | ||||||
|  |                             <button type="button" class="btn btn-sm btn-outline-primary" data-metric="total_financing_balance">融资余额合计</button> | ||||||
|  |                             <button type="button" class="btn btn-sm btn-outline-primary" data-metric="financing_repayment">融资偿还</button> | ||||||
|  |                             <button type="button" class="btn btn-sm btn-outline-primary" data-metric="securities_balance">融券余额</button> | ||||||
|  |                             <button id="rzrqRefreshBtn" class="btn btn-sm btn-outline-secondary ms-2"> | ||||||
|  |                                 <i class="bi bi-arrow-clockwise"></i> 刷新 | ||||||
|  |                             </button> | ||||||
|  |                         </div> | ||||||
|                     </div> |                     </div> | ||||||
|                     <div class="card-body"> |                     <div class="card-body"> | ||||||
|                         <ul> |                         <div id="rzrqChart" class="chart-container"></div> | ||||||
|                             <li>数据来源:同花顺数据,每分钟更新</li> |                         <p class="update-time text-center" id="rzrqUpdateTime"></p> | ||||||
|                             <li><strong>北向资金</strong>:是指从<strong>香港</strong>流入<strong>A股</strong>的资金,通过沪股通和深股通进入</li> |                     </div> | ||||||
|                             <li><strong>南向资金</strong>:是指从<strong>内地</strong>流入<strong>港股</strong>的资金,通过沪市港股通和深市港股通进入</li> |                 </div> | ||||||
|                             <li>净流入为正表示买入大于卖出,资金流入(<span class="money-inflow">红色</span>);净流入为负表示卖出大于买入,资金流出(<span class="money-outflow">绿色</span>)</li> |             </div> | ||||||
|                             <li>交易时间:北向9:30-11:30, 13:00-15:00;南向9:30-12:00, 13:00-16:00</li> |         </div> | ||||||
|                         </ul> |           | ||||||
|  |         <!-- 融资融券风险分析 --> | ||||||
|  |         <!-- <div class="row mt-4"> | ||||||
|  |             <div class="col-12"> | ||||||
|  |                 <div class="card"> | ||||||
|  |                     <div class="card-header"> | ||||||
|  |                         融资融券市场风险分析 | ||||||
|  |                     </div> | ||||||
|  |                     <div class="card-body"> | ||||||
|  |                          | ||||||
|  |                         <div class="row mb-4"> | ||||||
|  |                             <div class="col-md-12"> | ||||||
|  |                                 <div class="alert" id="overallRiskAlert"> | ||||||
|  |                                     <h5 class="alert-heading">市场综合风险评估: <span id="overallRiskLevel">加载中...</span></h5> | ||||||
|  |                                     <p id="overallRiskDesc">正在分析融资融券数据,评估市场风险...</p> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                          | ||||||
|  |                         <div class="row"> | ||||||
|  |                              | ||||||
|  |                             <div class="col-md-6 mb-3"> | ||||||
|  |                                 <div class="card h-100"> | ||||||
|  |                                     <div class="card-header"> | ||||||
|  |                                         融资融券余额变化 | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="card-body"> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>变化率:</span> | ||||||
|  |                                             <span id="balanceChangeRate" class="fw-bold">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>风险等级:</span> | ||||||
|  |                                             <span id="balanceRiskLevel">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <p id="balanceRiskDesc" class="mt-2 small text-muted">详细分析如下。</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                              | ||||||
|  |                             <div class="col-md-6 mb-3"> | ||||||
|  |                                 <div class="card h-100"> | ||||||
|  |                                     <div class="card-header"> | ||||||
|  |                                         融资偿还与买入比率 | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="card-body"> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>平均比率:</span> | ||||||
|  |                                             <span id="repayBuyRatio" class="fw-bold">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>风险等级:</span> | ||||||
|  |                                             <span id="repayRiskLevel">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <p id="repayRiskDesc" class="mt-2 small">--</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                              | ||||||
|  |                             <div class="col-md-6 mb-3"> | ||||||
|  |                                 <div class="card h-100"> | ||||||
|  |                                     <div class="card-header"> | ||||||
|  |                                         融券余额变化 (空头力量) | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="card-body"> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>变化率:</span> | ||||||
|  |                                             <span id="securitiesChangeRate" class="fw-bold">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>风险等级:</span> | ||||||
|  |                                             <span id="securitiesRiskLevel">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <p id="securitiesRiskDesc" class="mt-2 small text-muted">详细分析如下。</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                              | ||||||
|  |                             <div class="col-md-6 mb-3"> | ||||||
|  |                                 <div class="card h-100"> | ||||||
|  |                                     <div class="card-header"> | ||||||
|  |                                         融资占比分析 | ||||||
|  |                                     </div> | ||||||
|  |                                     <div class="card-body"> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>融资占比:</span> | ||||||
|  |                                             <span id="financingRatio" class="fw-bold">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <div class="d-flex justify-content-between mb-2"> | ||||||
|  |                                             <span>历史百分位:</span> | ||||||
|  |                                             <span id="financingRatioPercentile">--</span> | ||||||
|  |                                         </div> | ||||||
|  |                                         <p class="mt-2 small text-muted">融资占比反映了市场中多头使用杠杆的程度。百分位数越高表示融资占比处于历史较高水平。</p> | ||||||
|  |                                     </div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> --> | ||||||
|  | 
 | ||||||
|  |         <!-- 恐贪指数展示 --> | ||||||
|  |         <div class="row mt-4"> | ||||||
|  |             <div class="col-12"> | ||||||
|  |                 <div class="card"> | ||||||
|  |                     <div class="card-header d-flex justify-content-between align-items-center"> | ||||||
|  |                         <span>市场恐贪指数 (Fear & Greed Index)</span> | ||||||
|  |                         <button id="addFearGreedBtn" class="btn btn-sm btn-primary"> | ||||||
|  |                             <i class="bi bi-plus-circle"></i> 新增数据 | ||||||
|  |                         </button> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="card-body"> | ||||||
|  |                         <div class="row mb-3"> | ||||||
|  |                             <div class="col-md-4"> | ||||||
|  |                                 <div class="stat-card" id="fearGreedValue"> | ||||||
|  |                                     <div class="stat-value">--</div> | ||||||
|  |                                     <div class="stat-title">最新恐贪指数</div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="col-md-4"> | ||||||
|  |                                 <div class="stat-card" id="fearGreedStatus"> | ||||||
|  |                                     <div class="stat-value">--</div> | ||||||
|  |                                     <div class="stat-title">市场情绪状态</div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="col-md-4"> | ||||||
|  |                                 <div class="stat-card" id="fearGreedDate"> | ||||||
|  |                                     <div class="stat-value">--</div> | ||||||
|  |                                     <div class="stat-title">更新日期</div> | ||||||
|  |                                 </div> | ||||||
|  |                             </div> | ||||||
|  |                         </div> | ||||||
|  |                         <div id="fearGreedChart" class="chart-container"></div> | ||||||
|  |                         <p class="update-time text-center" id="fearGreedUpdateTime"></p> | ||||||
|  |                     </div> | ||||||
|  |                 </div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 新增恐贪指数数据的模态框 --> | ||||||
|  |         <div class="modal fade" id="addFearGreedModal" tabindex="-1" aria-labelledby="addFearGreedModalLabel" aria-hidden="true"> | ||||||
|  |             <div class="modal-dialog"> | ||||||
|  |                 <div class="modal-content"> | ||||||
|  |                     <div class="modal-header"> | ||||||
|  |                         <h5 class="modal-title" id="addFearGreedModalLabel">新增恐贪指数数据</h5> | ||||||
|  |                         <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="modal-body"> | ||||||
|  |                         <form id="addFearGreedForm"> | ||||||
|  |                             <div class="mb-3"> | ||||||
|  |                                 <label for="indexValue" class="form-label">恐贪指数值 (0-100)</label> | ||||||
|  |                                 <input type="number" class="form-control" id="indexValue" min="0" max="100" step="0.01" required> | ||||||
|  |                                 <div class="form-text">输入0-100之间的数值,保留两位小数</div> | ||||||
|  |                             </div> | ||||||
|  |                             <div class="mb-3"> | ||||||
|  |                                 <label for="tradingDate" class="form-label">交易日期</label> | ||||||
|  |                                 <input type="date" class="form-control" id="tradingDate" required> | ||||||
|  |                             </div> | ||||||
|  |                         </form> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="modal-footer"> | ||||||
|  |                         <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> | ||||||
|  |                         <button type="button" class="btn btn-primary" id="submitFearGreed">提交</button> | ||||||
|                     </div> |                     </div> | ||||||
|                 </div> |                 </div> | ||||||
|             </div> |             </div> | ||||||
|  | @ -166,5 +320,544 @@ | ||||||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> |     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||||
|     <script src="../static/js/echarts.min.js"></script> |     <script src="../static/js/echarts.min.js"></script> | ||||||
|     <script src="../static/js/hsgt_monitor.js"></script> |     <script src="../static/js/hsgt_monitor.js"></script> | ||||||
|  | 
 | ||||||
|  |     <!-- 恐贪指数相关的JavaScript --> | ||||||
|  |     <script> | ||||||
|  |         // 初始化恐贪指数图表 | ||||||
|  |         let fearGreedChart = null; | ||||||
|  |         let fearGreedIndexSelector = null; | ||||||
|  |         let fearGreedChartData = null; // 存储恐贪指数数据 | ||||||
|  | 
 | ||||||
|  |         // 加载恐贪指数数据 | ||||||
|  |         function loadFearGreedData() { | ||||||
|  |             $.ajax({ | ||||||
|  |                 url: '/api/fear_greed/data', | ||||||
|  |                 type: 'GET', | ||||||
|  |                 dataType: 'json', | ||||||
|  |                 success: function(response) { | ||||||
|  |                     if (response.status === 'success') { | ||||||
|  |                         fearGreedChartData = response.data; // 存储恐贪指数数据 | ||||||
|  |                         updateFearGreedUI(response.data); | ||||||
|  |                          | ||||||
|  |                         // 初始化恐贪指数选择器,只在首次加载时初始化 | ||||||
|  |                         if (!fearGreedIndexSelector) { | ||||||
|  |                             initFearGreedIndexSelector(); | ||||||
|  |                         } | ||||||
|  |                     } else { | ||||||
|  |                         console.error('加载恐贪指数数据失败:', response.message); | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 error: function(xhr, status, error) { | ||||||
|  |                     console.error('请求恐贪指数数据失败:', error); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 更新恐贪指数UI | ||||||
|  |         function updateFearGreedUI(data) { | ||||||
|  |             // 更新统计卡片 | ||||||
|  |             if (data.latest) { | ||||||
|  |                 $('#fearGreedValue .stat-value').text(data.latest.index_value.toFixed(2)); | ||||||
|  |                 $('#fearGreedStatus .stat-value').text(data.latest_status); | ||||||
|  |                 $('#fearGreedDate .stat-value').text(data.latest.trading_date); | ||||||
|  |                  | ||||||
|  |                 // 根据状态设置颜色 | ||||||
|  |                 const value = data.latest.index_value; | ||||||
|  |                 let statusColor; | ||||||
|  |                  | ||||||
|  |                 if (value < 25) { | ||||||
|  |                     statusColor = '#d9534f'; // 红色,极度恐慌 | ||||||
|  |                 } else if (value < 40) { | ||||||
|  |                     statusColor = '#f0ad4e'; // 橙色,恐慌 | ||||||
|  |                 } else if (value < 50) { | ||||||
|  |                     statusColor = '#5bc0de'; // 浅蓝色,偏向恐慌 | ||||||
|  |                 } else if (value < 60) { | ||||||
|  |                     statusColor = '#5cb85c'; // 绿色,中性 | ||||||
|  |                 } else if (value < 75) { | ||||||
|  |                     statusColor = '#0275d8'; // 蓝色,偏向贪婪 | ||||||
|  |                 } else if (value < 90) { | ||||||
|  |                     statusColor = '#f0ad4e'; // 橙色,贪婪 | ||||||
|  |                 } else { | ||||||
|  |                     statusColor = '#d9534f'; // 红色,极度贪婪 | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 $('#fearGreedStatus .stat-value').css('color', statusColor); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 更新更新时间 - 显示最新数据的更新时间 | ||||||
|  |             $('#fearGreedUpdateTime').text('最后更新: ' + data.update_time); | ||||||
|  |              | ||||||
|  |             // 初始化/更新图表 | ||||||
|  |             initFearGreedChart(data.dates, data.values); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 初始化恐贪指数图表 | ||||||
|  |         function initFearGreedChart(dates, values) { | ||||||
|  |             if (!fearGreedChart) { | ||||||
|  |                 fearGreedChart = echarts.init(document.getElementById('fearGreedChart')); | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             const option = { | ||||||
|  |                 tooltip: { | ||||||
|  |                     trigger: 'axis', | ||||||
|  |                     formatter: function(params) { | ||||||
|  |                         const param = params[0]; | ||||||
|  |                         const value = param.value; | ||||||
|  |                         let status; | ||||||
|  |                          | ||||||
|  |                         if (value < 25) { | ||||||
|  |                             status = '极度恐慌'; | ||||||
|  |                         } else if (value < 40) { | ||||||
|  |                             status = '恐慌'; | ||||||
|  |                         } else if (value < 50) { | ||||||
|  |                             status = '偏向恐慌'; | ||||||
|  |                         } else if (value < 60) { | ||||||
|  |                             status = '中性'; | ||||||
|  |                         } else if (value < 75) { | ||||||
|  |                             status = '偏向贪婪'; | ||||||
|  |                         } else if (value < 90) { | ||||||
|  |                             status = '贪婪'; | ||||||
|  |                         } else { | ||||||
|  |                             status = '极度贪婪'; | ||||||
|  |                         } | ||||||
|  |                          | ||||||
|  |                         return `${param.axisValue}<br/>恐贪指数: ${value.toFixed(2)}<br/>状态: ${status}`; | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 grid: { | ||||||
|  |                     left: '3%', | ||||||
|  |                     right: '4%', | ||||||
|  |                     bottom: '3%', | ||||||
|  |                     containLabel: true | ||||||
|  |                 }, | ||||||
|  |                 xAxis: { | ||||||
|  |                     type: 'category', | ||||||
|  |                     boundaryGap: false, | ||||||
|  |                     data: dates | ||||||
|  |                 }, | ||||||
|  |                 yAxis: { | ||||||
|  |                     type: 'value', | ||||||
|  |                     min: 0, | ||||||
|  |                     max: 100, | ||||||
|  |                     axisLabel: { | ||||||
|  |                         formatter: '{value}' | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 visualMap: { | ||||||
|  |                     show: false, | ||||||
|  |                     dimension: 1, | ||||||
|  |                     pieces: [ | ||||||
|  |                         {gt: 0, lte: 25, color: '#d9534f'},  // 极度恐慌 | ||||||
|  |                         {gt: 25, lte: 40, color: '#f0ad4e'}, // 恐慌 | ||||||
|  |                         {gt: 40, lte: 50, color: '#5bc0de'}, // 偏向恐慌 | ||||||
|  |                         {gt: 50, lte: 60, color: '#5cb85c'}, // 中性 | ||||||
|  |                         {gt: 60, lte: 75, color: '#0275d8'}, // 偏向贪婪 | ||||||
|  |                         {gt: 75, lte: 90, color: '#f0ad4e'}, // 贪婪 | ||||||
|  |                         {gt: 90, lte: 100, color: '#d9534f'} // 极度贪婪 | ||||||
|  |                     ] | ||||||
|  |                 }, | ||||||
|  |                 series: [ | ||||||
|  |                     { | ||||||
|  |                         name: '恐贪指数', | ||||||
|  |                         type: 'line', | ||||||
|  |                         data: values, | ||||||
|  |                         markLine: { | ||||||
|  |                             silent: true, | ||||||
|  |                             lineStyle: { | ||||||
|  |                                 color: '#999' | ||||||
|  |                             }, | ||||||
|  |                             data: [ | ||||||
|  |                                 { | ||||||
|  |                                     yAxis: 25, | ||||||
|  |                                     label: { | ||||||
|  |                                         formatter: '极度恐慌' | ||||||
|  |                                     } | ||||||
|  |                                 }, | ||||||
|  |                                 { | ||||||
|  |                                     yAxis: 50, | ||||||
|  |                                     label: { | ||||||
|  |                                         formatter: '中性' | ||||||
|  |                                     } | ||||||
|  |                                 }, | ||||||
|  |                                 { | ||||||
|  |                                     yAxis: 75, | ||||||
|  |                                     label: { | ||||||
|  |                                         formatter: '贪婪' | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             ] | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ] | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             fearGreedChart.setOption(option); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 初始化恐贪指数选择器 | ||||||
|  |         function initFearGreedIndexSelector() { | ||||||
|  |             fearGreedIndexSelector = new IndexSelector('fearGreedChart', { | ||||||
|  |                 // 获取图表当前显示的日期范围 | ||||||
|  |                 getDateRange: function() { | ||||||
|  |                     // 获取恐贪指数图表的日期范围 | ||||||
|  |                     if (fearGreedChartData && fearGreedChartData.dates && fearGreedChartData.dates.length) { | ||||||
|  |                         return { | ||||||
|  |                             startDate: fearGreedChartData.dates[0], | ||||||
|  |                             endDate: fearGreedChartData.dates[fearGreedChartData.dates.length - 1] | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |                     return { startDate: null, endDate: null }; | ||||||
|  |                 }, | ||||||
|  |                 // 指数数据更新时的回调 | ||||||
|  |                 onChange: function(selectedIndices) { | ||||||
|  |                     updateFearGreedChartWithIndices(selectedIndices); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 将数据对齐到日期范围 | ||||||
|  |         function alignDataToDateRange(sourceDates, sourceValues, targetDates) { | ||||||
|  |             const result = new Array(targetDates.length).fill(null); | ||||||
|  |             const dateMap = {}; | ||||||
|  |              | ||||||
|  |             // 创建源数据日期到值的映射 | ||||||
|  |             sourceDates.forEach((date, index) => { | ||||||
|  |                 dateMap[date] = sourceValues[index]; | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 映射到目标日期 | ||||||
|  |             targetDates.forEach((date, index) => { | ||||||
|  |                 if (dateMap[date] !== undefined) { | ||||||
|  |                     result[index] = dateMap[date]; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             return result; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 更新图表时添加指数数据 | ||||||
|  |         function updateFearGreedChartWithIndices(indices) { | ||||||
|  |             if (!fearGreedChart) return; | ||||||
|  |              | ||||||
|  |             // 获取图表当前选项 | ||||||
|  |             const option = fearGreedChart.getOption(); | ||||||
|  |              | ||||||
|  |             // 保留原始恐贪指数系列 | ||||||
|  |             const originalSeries = option.series.filter(s => s.name === '恐贪指数'); | ||||||
|  |              | ||||||
|  |             // 清除所有系列,并重新添加原始恐贪指数系列 | ||||||
|  |             option.series = [...originalSeries]; | ||||||
|  |              | ||||||
|  |             // 添加指数数据 | ||||||
|  |             indices.forEach(index => { | ||||||
|  |                 if (!index.data || !index.data.dates) return; | ||||||
|  |                  | ||||||
|  |                 // 将指数数据对齐到日期范围 | ||||||
|  |                 const alignedData = alignDataToDateRange(index.data.dates, index.data.values, option.xAxis[0].data); | ||||||
|  |                  | ||||||
|  |                 // 如果没有第二Y轴,创建新的Y轴用于指数 | ||||||
|  |                 if (!option.yAxis.some(axis => axis.name === '指数值')) { | ||||||
|  |                     option.yAxis.push({ | ||||||
|  |                         name: '指数值', | ||||||
|  |                         type: 'value', | ||||||
|  |                         position: 'right', | ||||||
|  |                         min: 'dataMin', | ||||||
|  |                         max: 'dataMax', | ||||||
|  |                         splitLine: { | ||||||
|  |                             show: false | ||||||
|  |                         }, | ||||||
|  |                         axisLabel: { | ||||||
|  |                             formatter: '{value}' | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |                 // 添加新的指数系列 | ||||||
|  |                 option.series.push({ | ||||||
|  |                     name: `${index.name}指数`, | ||||||
|  |                     type: 'line', | ||||||
|  |                     yAxisIndex: 1, // 使用第二个Y轴 | ||||||
|  |                     data: alignedData, | ||||||
|  |                     symbol: 'none', | ||||||
|  |                     smooth: true, | ||||||
|  |                     lineStyle: { | ||||||
|  |                         width: 2, | ||||||
|  |                         color: index.color | ||||||
|  |                     }, | ||||||
|  |                     itemStyle: { | ||||||
|  |                         color: index.color | ||||||
|  |                     } | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 更新图表 | ||||||
|  |             option.legend = { | ||||||
|  |                 data: [ | ||||||
|  |                     '恐贪指数', | ||||||
|  |                     ...indices.map(i => `${i.name}指数`) | ||||||
|  |                 ], | ||||||
|  |                 selected: { | ||||||
|  |                     '恐贪指数': true, | ||||||
|  |                     ...indices.reduce((acc, index) => { | ||||||
|  |                         acc[`${index.name}指数`] = true; | ||||||
|  |                         return acc; | ||||||
|  |                     }, {}) | ||||||
|  |                 } | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             fearGreedChart.setOption(option, true); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 窗口大小改变时调整图表大小 | ||||||
|  |         window.addEventListener('resize', function() { | ||||||
|  |             if (fearGreedChart) { | ||||||
|  |                 fearGreedChart.resize(); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // 添加恐贪指数数据 | ||||||
|  |         function addFearGreedData() { | ||||||
|  |             const indexValue = parseFloat($('#indexValue').val()); | ||||||
|  |             const tradingDate = $('#tradingDate').val(); | ||||||
|  |              | ||||||
|  |             if (isNaN(indexValue) || indexValue < 0 || indexValue > 100) { | ||||||
|  |                 alert('请输入有效的恐贪指数值(0-100)'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if (!tradingDate) { | ||||||
|  |                 alert('请选择交易日期'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             $.ajax({ | ||||||
|  |                 url: '/api/fear_greed/add', | ||||||
|  |                 type: 'POST', | ||||||
|  |                 contentType: 'application/json', | ||||||
|  |                 data: JSON.stringify({ | ||||||
|  |                     index_value: indexValue, | ||||||
|  |                     trading_date: tradingDate | ||||||
|  |                 }), | ||||||
|  |                 success: function(response) { | ||||||
|  |                     if (response.status === 'success') { | ||||||
|  |                         // 关闭模态框 | ||||||
|  |                         $('#addFearGreedModal').modal('hide'); | ||||||
|  |                         // 重新加载数据 | ||||||
|  |                         loadFearGreedData(); | ||||||
|  |                         // 重置表单 | ||||||
|  |                         $('#addFearGreedForm')[0].reset(); | ||||||
|  |                          | ||||||
|  |                         // 显示成功消息 | ||||||
|  |                         // alert('恐贪指数数据添加成功!'); | ||||||
|  |                     } else { | ||||||
|  |                         alert('添加失败: ' + response.message); | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 error: function(xhr, status, error) { | ||||||
|  |                     console.error('添加恐贪指数数据失败:', error); | ||||||
|  |                     alert('添加失败: ' + error); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 事件绑定 | ||||||
|  |         $(document).ready(function() { | ||||||
|  |             // 加载恐贪指数数据 | ||||||
|  |             loadFearGreedData(); | ||||||
|  |              | ||||||
|  |             // 新增数据按钮点击事件 | ||||||
|  |             $('#addFearGreedBtn').click(function() { | ||||||
|  |                 // 设置默认日期为今天 | ||||||
|  |                 $('#tradingDate').val(new Date().toISOString().split('T')[0]); | ||||||
|  |                 // 显示模态框 | ||||||
|  |                 $('#addFearGreedModal').modal('show'); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 提交按钮点击事件 | ||||||
|  |             $('#submitFearGreed').click(function() { | ||||||
|  |                 addFearGreedData(); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 刷新按钮点击事件(与恐贪指数一起刷新) | ||||||
|  |             $('#refreshBtn').click(function() { | ||||||
|  |                 loadFearGreedData(); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     </script> | ||||||
|  | 
 | ||||||
|  |     <!-- 指数选择器通用组件 --> | ||||||
|  |     <script> | ||||||
|  |     // 通用指数选择器组件 | ||||||
|  |     class IndexSelector { | ||||||
|  |         constructor(targetChartId, options = {}) { | ||||||
|  |             this.targetChartId = targetChartId; | ||||||
|  |             this.targetChart = null; | ||||||
|  |             this.indices = []; | ||||||
|  |             this.selectedIndices = []; | ||||||
|  |             this.colors = options.colors || ['#8A2BE2', '#FF1493', '#FF7F50', '#00CED1', '#32CD32']; | ||||||
|  |             this.maxIndices = options.maxIndices || 3; | ||||||
|  |             this.containerClass = options.containerClass || 'index-selector'; | ||||||
|  |             this.onChange = options.onChange || (() => {}); | ||||||
|  |             this.options = options; | ||||||
|  |              | ||||||
|  |             // 创建选择器DOM元素 | ||||||
|  |             this.createSelectorDOM(); | ||||||
|  |             // 加载指数列表 | ||||||
|  |             this.loadIndicesList(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 创建选择器DOM | ||||||
|  |         createSelectorDOM() { | ||||||
|  |             const container = document.createElement('div'); | ||||||
|  |             container.className = this.containerClass; | ||||||
|  |             container.style.cssText = 'margin-left: 10px; display: inline-block;'; | ||||||
|  |              | ||||||
|  |             const select = document.createElement('select'); | ||||||
|  |             select.className = 'form-select form-select-sm'; | ||||||
|  |             select.id = `${this.targetChartId}-index-select`; | ||||||
|  |             select.innerHTML = '<option value="">添加指数叠加...</option>'; | ||||||
|  |              | ||||||
|  |             container.appendChild(select); | ||||||
|  |              | ||||||
|  |             // 添加到目标图表的header旁边 | ||||||
|  |             const chartHeader = document.querySelector(`#${this.targetChartId}`).closest('.card').querySelector('.card-header'); | ||||||
|  |             const btnGroup = chartHeader.querySelector('.btn-group') || chartHeader; | ||||||
|  |             btnGroup.appendChild(container); | ||||||
|  |              | ||||||
|  |             // 添加事件监听 | ||||||
|  |             select.addEventListener('change', (e) => this.handleSelectChange(e)); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 加载指数列表 | ||||||
|  |         loadIndicesList() { | ||||||
|  |             fetch('/api/indices/list') | ||||||
|  |                 .then(response => response.json()) | ||||||
|  |                 .then(data => { | ||||||
|  |                     if (data.status === 'success') { | ||||||
|  |                         this.indices = data.data; | ||||||
|  |                         this.updateSelectOptions(); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .catch(error => console.error('加载指数列表失败:', error)); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 更新下拉选项 | ||||||
|  |         updateSelectOptions() { | ||||||
|  |             const select = document.getElementById(`${this.targetChartId}-index-select`); | ||||||
|  |             // 保留第一个选项 | ||||||
|  |             select.innerHTML = '<option value="">添加指数叠加...</option>'; | ||||||
|  |              | ||||||
|  |             // 添加未选择的指数到下拉列表 | ||||||
|  |             this.indices.forEach(index => { | ||||||
|  |                 if (!this.selectedIndices.some(i => i.code === index.code)) { | ||||||
|  |                     const option = document.createElement('option'); | ||||||
|  |                     option.value = index.code; | ||||||
|  |                     option.textContent = index.name; | ||||||
|  |                     select.appendChild(option); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 处理选择变更 | ||||||
|  |         handleSelectChange(e) { | ||||||
|  |             const indexCode = e.target.value; | ||||||
|  |             if (!indexCode) return; | ||||||
|  |              | ||||||
|  |             const index = this.indices.find(i => i.code === indexCode); | ||||||
|  |             if (!index) return; | ||||||
|  |              | ||||||
|  |             // 检查选择的指数数量限制 | ||||||
|  |             if (this.selectedIndices.length >= this.maxIndices) { | ||||||
|  |                 alert(`最多只能叠加${this.maxIndices}个指数!`); | ||||||
|  |                 e.target.value = ''; | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             // 添加到已选择列表 | ||||||
|  |             const colorIndex = this.selectedIndices.length % this.colors.length; | ||||||
|  |             const selectedIndex = { | ||||||
|  |                 ...index, | ||||||
|  |                 color: this.colors[colorIndex], | ||||||
|  |                 visible: true | ||||||
|  |             }; | ||||||
|  |              | ||||||
|  |             this.selectedIndices.push(selectedIndex); | ||||||
|  |              | ||||||
|  |             // 添加指数标签 | ||||||
|  |             this.addIndexLabel(selectedIndex); | ||||||
|  |              | ||||||
|  |             // 加载并显示指数数据 | ||||||
|  |             this.loadIndexData(selectedIndex); | ||||||
|  |              | ||||||
|  |             // 重置选择框 | ||||||
|  |             e.target.value = ''; | ||||||
|  |             this.updateSelectOptions(); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 添加指数标签 | ||||||
|  |         addIndexLabel(index) { | ||||||
|  |             const container = document.querySelector(`#${this.targetChartId}`).closest('.card-body'); | ||||||
|  |             const labelsContainer = container.querySelector('.index-labels') || (() => { | ||||||
|  |                 const div = document.createElement('div'); | ||||||
|  |                 div.className = 'index-labels d-flex flex-wrap mt-2'; | ||||||
|  |                 container.insertBefore(div, container.firstChild); | ||||||
|  |                 return div; | ||||||
|  |             })(); | ||||||
|  |              | ||||||
|  |             const label = document.createElement('div'); | ||||||
|  |             label.className = 'badge bg-light text-dark me-2 mb-2 p-2 d-flex align-items-center'; | ||||||
|  |             label.style.borderLeft = `3px solid ${index.color}`; | ||||||
|  |             label.innerHTML = ` | ||||||
|  |                 <span class="me-2">${index.name}</span> | ||||||
|  |                 <button type="button" class="btn-close btn-close-sm" aria-label="移除"></button> | ||||||
|  |             `; | ||||||
|  |              | ||||||
|  |             label.querySelector('.btn-close').addEventListener('click', () => { | ||||||
|  |                 this.removeIndex(index.code); | ||||||
|  |                 labelsContainer.removeChild(label); | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             labelsContainer.appendChild(label); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 加载指数数据 | ||||||
|  |         loadIndexData(index) { | ||||||
|  |             // 获取图表当前日期范围 | ||||||
|  |             const chartDates = this.options.getDateRange ?  | ||||||
|  |                 this.options.getDateRange() :  | ||||||
|  |                 { startDate: null, endDate: null }; | ||||||
|  |              | ||||||
|  |             fetch(`/api/indices/data?code=${index.code}&start_date=${chartDates.startDate || ''}&end_date=${chartDates.endDate || ''}`) | ||||||
|  |                 .then(response => response.json()) | ||||||
|  |                 .then(data => { | ||||||
|  |                     if (data.status === 'success') { | ||||||
|  |                         // 更新指数数据 | ||||||
|  |                         index.data = data.data; | ||||||
|  |                          | ||||||
|  |                         // 调用回调函数更新图表 | ||||||
|  |                         this.onChange(this.selectedIndices); | ||||||
|  |                     } | ||||||
|  |                 }) | ||||||
|  |                 .catch(error => console.error(`加载指数 ${index.name} 数据失败:`, error)); | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 移除指数 | ||||||
|  |         removeIndex(indexCode) { | ||||||
|  |             const index = this.selectedIndices.findIndex(i => i.code === indexCode); | ||||||
|  |             if (index !== -1) { | ||||||
|  |                 this.selectedIndices.splice(index, 1); | ||||||
|  |                 this.updateSelectOptions(); | ||||||
|  |                  | ||||||
|  |                 // 调用回调函数更新图表 | ||||||
|  |                 this.onChange(this.selectedIndices); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         // 获取所有已选择的指数 | ||||||
|  |         getSelectedIndices() { | ||||||
|  |             return [...this.selectedIndices]; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     </script> | ||||||
| </body> | </body> | ||||||
| </html>  | </html>  | ||||||
|  | @ -10,8 +10,25 @@ | ||||||
|     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> |     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> | ||||||
|     <!-- 引入ECharts --> |     <!-- 引入ECharts --> | ||||||
|     <script src="../static/js/echarts.min.js"></script> |     <script src="../static/js/echarts.min.js"></script> | ||||||
|  |     <!-- 引入Select2 CSS 和 JS (用于可搜索的下拉框) --> | ||||||
|  |     <link href="../static/css/select2.min.css" rel="stylesheet" /> | ||||||
|  |     <script src="../static/js/select2.min.js"></script> | ||||||
|     <!-- 引入自定义CSS --> |     <!-- 引入自定义CSS --> | ||||||
|     <link href="/static/css/style.css" rel="stylesheet"> |     <link href="/static/css/style.css" rel="stylesheet"> | ||||||
|  |     <style> | ||||||
|  |         /* 自定义Select2样式使其与Bootstrap兼容 */ | ||||||
|  |         .select2-container .select2-selection--single { | ||||||
|  |             height: 38px; | ||||||
|  |             line-height: 38px; | ||||||
|  |             border: 1px solid #ced4da; | ||||||
|  |         } | ||||||
|  |         .select2-container--default .select2-selection--single .select2-selection__rendered { | ||||||
|  |             line-height: 38px; | ||||||
|  |         } | ||||||
|  |         .select2-container--default .select2-selection--single .select2-selection__arrow { | ||||||
|  |             height: 36px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|     <!-- 导航栏 --> |     <!-- 导航栏 --> | ||||||
|  | @ -29,6 +46,9 @@ | ||||||
|                     <li class="nav-item"> |                     <li class="nav-item"> | ||||||
|                         <a class="nav-link" href="/industry">行业估值分析</a> |                         <a class="nav-link" href="/industry">行业估值分析</a> | ||||||
|                     </li> |                     </li> | ||||||
|  |                     <li class="nav-item"> | ||||||
|  |                         <a class="nav-link" href="/hsgt">资金情况</a> | ||||||
|  |                     </li> | ||||||
|                 </ul> |                 </ul> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  | @ -64,12 +84,18 @@ | ||||||
|                      |                      | ||||||
|                     <div class="col-md-4"> |                     <div class="col-md-4"> | ||||||
|                         <label for="industryName" class="form-label">行业名称(可选)</label> |                         <label for="industryName" class="form-label">行业名称(可选)</label> | ||||||
|                         <input type="text" class="form-control" id="industryName" placeholder="例如: 半导体"> |                         <select class="form-control select2" id="industryName"> | ||||||
|  |                             <option value="">请选择行业</option> | ||||||
|  |                             <!-- 选项将通过JS动态加载 --> | ||||||
|  |                         </select> | ||||||
|                     </div> |                     </div> | ||||||
|                      |                      | ||||||
|                     <div class="col-md-4"> |                     <div class="col-md-4"> | ||||||
|                         <label for="conceptName" class="form-label">概念板块(可选)</label> |                         <label for="conceptName" class="form-label">概念板块(可选)</label> | ||||||
|                         <input type="text" class="form-control" id="conceptName" placeholder="例如: 人工智能"> |                         <select class="form-control select2" id="conceptName"> | ||||||
|  |                             <option value="">请选择概念板块</option> | ||||||
|  |                             <!-- 选项将通过JS动态加载 --> | ||||||
|  |                         </select> | ||||||
|                     </div> |                     </div> | ||||||
|                      |                      | ||||||
|                     <div class="col-12 text-center mt-4"> |                     <div class="col-12 text-center mt-4"> | ||||||
|  | @ -148,5 +174,39 @@ | ||||||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> |     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||||
|     <!-- 引入自定义JS --> |     <!-- 引入自定义JS --> | ||||||
|     <script src="/static/js/valuation.js"></script> |     <script src="/static/js/valuation.js"></script> | ||||||
|  |     <script> | ||||||
|  |         // 行业和概念板块互斥选择逻辑 | ||||||
|  |         $(document).ready(function() { | ||||||
|  |             // 行业选择变化时 | ||||||
|  |             $('#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}); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     </script> | ||||||
| </body> | </body> | ||||||
| </html>  | </html>  | ||||||
|  | @ -10,8 +10,25 @@ | ||||||
|     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> |     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> | ||||||
|     <!-- 引入ECharts --> |     <!-- 引入ECharts --> | ||||||
|     <script src="../static/js/echarts.min.js"></script> |     <script src="../static/js/echarts.min.js"></script> | ||||||
|  |     <!-- 引入Select2 CSS 和 JS (用于可搜索的下拉框) --> | ||||||
|  |     <link href="../static/css/select2.min.css" rel="stylesheet" /> | ||||||
|  |     <script src="../static/js/select2.min.js"></script> | ||||||
|     <!-- 引入自定义CSS --> |     <!-- 引入自定义CSS --> | ||||||
|     <link href="/static/css/style.css" rel="stylesheet"> |     <link href="/static/css/style.css" rel="stylesheet"> | ||||||
|  |     <style> | ||||||
|  |         /* 自定义Select2样式使其与Bootstrap兼容 */ | ||||||
|  |         .select2-container .select2-selection--single { | ||||||
|  |             height: 38px; | ||||||
|  |             line-height: 38px; | ||||||
|  |             border: 1px solid #ced4da; | ||||||
|  |         } | ||||||
|  |         .select2-container--default .select2-selection--single .select2-selection__rendered { | ||||||
|  |             line-height: 38px; | ||||||
|  |         } | ||||||
|  |         .select2-container--default .select2-selection--single .select2-selection__arrow { | ||||||
|  |             height: 36px; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
| </head> | </head> | ||||||
| <body> | <body> | ||||||
|     <!-- 导航栏 --> |     <!-- 导航栏 --> | ||||||
|  | @ -29,6 +46,9 @@ | ||||||
|                     <li class="nav-item"> |                     <li class="nav-item"> | ||||||
|                         <a class="nav-link active" href="/industry">行业估值分析</a> |                         <a class="nav-link active" href="/industry">行业估值分析</a> | ||||||
|                     </li> |                     </li> | ||||||
|  |                     <li class="nav-item"> | ||||||
|  |                         <a class="nav-link" href="/hsgt">资金情况</a> | ||||||
|  |                     </li> | ||||||
|                 </ul> |                 </ul> | ||||||
|             </div> |             </div> | ||||||
|         </div> |         </div> | ||||||
|  | @ -45,7 +65,7 @@ | ||||||
|                 <form id="industryForm" class="row g-3"> |                 <form id="industryForm" class="row g-3"> | ||||||
|                     <div class="col-md-6"> |                     <div class="col-md-6"> | ||||||
|                         <label for="industryName" class="form-label">行业名称</label> |                         <label for="industryName" class="form-label">行业名称</label> | ||||||
|                         <select class="form-select" id="industryName" required> |                         <select class="form-select select2" id="industryName" required> | ||||||
|                             <option value="" selected disabled>请选择行业</option> |                             <option value="" selected disabled>请选择行业</option> | ||||||
|                             <!-- 将通过API动态填充 --> |                             <!-- 将通过API动态填充 --> | ||||||
|                         </select> |                         </select> | ||||||
|  | @ -165,5 +185,21 @@ | ||||||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> |     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||||
|     <!-- 引入行业分析JS --> |     <!-- 引入行业分析JS --> | ||||||
|     <script src="/static/js/industry.js"></script> |     <script src="/static/js/industry.js"></script> | ||||||
|  |     <script> | ||||||
|  |         // 初始化Select2 | ||||||
|  |         $(document).ready(function() { | ||||||
|  |             // 初始化行业下拉框为可搜索 | ||||||
|  |             $('#industryName').select2({ | ||||||
|  |                 placeholder: '请选择行业', | ||||||
|  |                 allowClear: true, | ||||||
|  |                 width: '100%' | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             // 重置表单时也需要重置Select2 | ||||||
|  |             $('#resetBtn').on('click', function() { | ||||||
|  |                 $('#industryName').val('').trigger('change'); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  |     </script> | ||||||
| </body> | </body> | ||||||
| </html>  | </html>  | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | import redis | ||||||
|  | import time | ||||||
|  | import logging | ||||||
|  | from typing import Optional | ||||||
|  | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | class DistributedLock: | ||||||
|  |     def __init__(self, redis_client: redis.Redis, lock_name: str, expire_time: int = 60): | ||||||
|  |         """ | ||||||
|  |         初始化分布式锁 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             redis_client: Redis客户端实例 | ||||||
|  |             lock_name: 锁的名称 | ||||||
|  |             expire_time: 锁的过期时间(秒) | ||||||
|  |         """ | ||||||
|  |         self.redis_client = redis_client | ||||||
|  |         self.lock_name = f"lock:{lock_name}" | ||||||
|  |         self.expire_time = expire_time | ||||||
|  |          | ||||||
|  |     def acquire(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         尝试获取锁 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             bool: 是否成功获取锁 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 使用SETNX命令尝试获取锁 | ||||||
|  |             # 如果key不存在,则设置key的值为当前时间戳,并设置过期时间 | ||||||
|  |             current_time = int(time.time()) | ||||||
|  |             result = self.redis_client.set( | ||||||
|  |                 self.lock_name, | ||||||
|  |                 current_time, | ||||||
|  |                 ex=self.expire_time, | ||||||
|  |                 nx=True | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             if result: | ||||||
|  |                 logger.info(f"成功获取锁: {self.lock_name}") | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 logger.info(f"未能获取锁: {self.lock_name}") | ||||||
|  |                 return False | ||||||
|  |                  | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取锁时发生错误: {e}") | ||||||
|  |             return False | ||||||
|  |              | ||||||
|  |     def release(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         释放锁 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             bool: 是否成功释放锁 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 先检查锁是否存在 | ||||||
|  |             lock_value = self.redis_client.get(self.lock_name) | ||||||
|  |             if not lock_value: | ||||||
|  |                 logger.info(f"锁不存在: {self.lock_name}") | ||||||
|  |                 return True | ||||||
|  |                  | ||||||
|  |             # 删除锁 | ||||||
|  |             result = self.redis_client.delete(self.lock_name) | ||||||
|  |             if result: | ||||||
|  |                 logger.info(f"成功释放锁: {self.lock_name}, 原值: {lock_value}") | ||||||
|  |                 return True | ||||||
|  |             else: | ||||||
|  |                 logger.warning(f"释放锁失败: {self.lock_name}, 原值: {lock_value}") | ||||||
|  |                 return False | ||||||
|  |                  | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"释放锁时发生错误: {e}") | ||||||
|  |             return False  | ||||||
|  | @ -1,111 +1,185 @@ | ||||||
| """ | """ | ||||||
| PE/PB估值分析命令行工具 | 估值分析模块命令行工具 | ||||||
| 
 |  | ||||||
| 使用方法: |  | ||||||
|     python -m src.valuation_analysis.cli --stock 601138 |  | ||||||
| """ | """ | ||||||
| 
 | 
 | ||||||
| import argparse | import argparse | ||||||
| import sys |  | ||||||
| import logging | import logging | ||||||
|  | import datetime | ||||||
| import json | import json | ||||||
| from pathlib import Path | import sys | ||||||
| from typing import Optional, List, Dict | import os | ||||||
| 
 |  | ||||||
| from .pe_pb_analysis import ValuationAnalyzer, analyze_stock |  | ||||||
| from .config import DB_URL, OUTPUT_DIR |  | ||||||
| 
 |  | ||||||
| logger = logging.getLogger("valuation_analysis.cli") |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def parse_args(): |  | ||||||
|     """解析命令行参数""" |  | ||||||
|     parser = argparse.ArgumentParser(description="股票PE/PB估值分析工具") |  | ||||||
|      |  | ||||||
|     parser.add_argument('--stock', '-s', type=str, required=True,  |  | ||||||
|                         help='股票代码,例如:601138') |  | ||||||
|     parser.add_argument('--start-date', type=str, default='2018-01-01', |  | ||||||
|                         help='起始日期 (默认: 2018-01-01)') |  | ||||||
|     parser.add_argument('--metrics', type=str, default='pe,pb', |  | ||||||
|                         help='分析指标,用逗号分隔 (默认: pe,pb)') |  | ||||||
|     parser.add_argument('--output', '-o', type=str, default=None, |  | ||||||
|                         help='结果保存路径 (默认: results/valuation_analysis/)') |  | ||||||
|     parser.add_argument('--format', type=str, choices=['json', 'text'], default='text', |  | ||||||
|                         help='输出格式 (默认: text)') |  | ||||||
|      |  | ||||||
|     return parser.parse_args() |  | ||||||
| 
 | 
 | ||||||
|  | from . import pe_pb_analysis | ||||||
|  | from . import industry_analysis | ||||||
|  | from . import rzrq_collector | ||||||
|  | from .config import OUTPUT_DIR | ||||||
| 
 | 
 | ||||||
| def main(): | def main(): | ||||||
|     """主函数""" |     """命令行工具主函数""" | ||||||
|     args = parse_args() |  | ||||||
|      |      | ||||||
|     # 解析参数 |     parser = argparse.ArgumentParser(description='股票估值分析工具', formatter_class=argparse.RawTextHelpFormatter) | ||||||
|     stock_code = args.stock |     subparsers = parser.add_subparsers(dest='command', help='子命令') | ||||||
|     start_date = args.start_date |  | ||||||
|     metrics = args.metrics.split(',') |  | ||||||
|     output_format = args.format |  | ||||||
|      |      | ||||||
|     # 设置输出路径 |     # 设置PE/PB分析子命令 | ||||||
|     output_path = args.output |     pepb_parser = subparsers.add_parser('pepb', help='PE/PB分析') | ||||||
|     if output_path is None: |     pepb_parser.add_argument('--stock', '-s', required=True, help='股票代码') | ||||||
|         output_path = OUTPUT_DIR / f"{stock_code}_valuation_analysis.{output_format}" |     pepb_parser.add_argument('--days', '-d', type=int, default=1000, help='分析天数 (默认: 1000)') | ||||||
|     else: |     pepb_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',  | ||||||
|         output_path = Path(output_path) |                              help='输出格式 (默认: json)') | ||||||
|  |      | ||||||
|  |     # 设置ROE分析子命令 | ||||||
|  |     roe_parser = subparsers.add_parser('roe', help='ROE分析') | ||||||
|  |     roe_parser.add_argument('--stock', '-s', required=True, help='股票代码') | ||||||
|  |     roe_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json', | ||||||
|  |                            help='输出格式 (默认: json)') | ||||||
|  |      | ||||||
|  |     # 设置行业分析子命令 | ||||||
|  |     industry_parser = subparsers.add_parser('industry', help='行业估值分析') | ||||||
|  |     industry_parser.add_argument('--name', '-n', required=True, help='行业名称') | ||||||
|  |     industry_parser.add_argument('--metric', '-m', choices=['pe', 'pb', 'ps'], default='pe', | ||||||
|  |                                help='估值指标 (pe/pb/ps) (默认: pe)') | ||||||
|  |     industry_parser.add_argument('--days', '-d', type=int, default=1095,  | ||||||
|  |                                help='分析天数 (默认: 1095,约3年)') | ||||||
|  |     industry_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json', | ||||||
|  |                                help='输出格式 (默认: json)') | ||||||
|  |      | ||||||
|  |     # 设置行业列表子命令 | ||||||
|  |     industry_list_parser = subparsers.add_parser('industry-list', help='获取行业列表') | ||||||
|  |      | ||||||
|  |     # 设置融资融券数据采集子命令 | ||||||
|  |     rzrq_parser = subparsers.add_parser('rzrq', help='融资融券数据采集') | ||||||
|  |     rzrq_parser.add_argument('--action', '-a', choices=['init', 'update', 'run-scheduler'],  | ||||||
|  |                            required=True, help='操作类型: init-首次全量采集,update-更新最新数据,run-scheduler-运行定时器') | ||||||
|  |     rzrq_parser.add_argument('--output-sql', '-s', action='store_true',  | ||||||
|  |                            help='输出创建表的SQL语句,仅与init配合使用') | ||||||
|  |      | ||||||
|  |     # 解析命令行参数 | ||||||
|  |     args = parser.parse_args() | ||||||
|  |      | ||||||
|  |     # 如果没有提供子命令,显示帮助信息 | ||||||
|  |     if not args.command: | ||||||
|  |         parser.print_help() | ||||||
|  |         sys.exit(1) | ||||||
|  |      | ||||||
|  |     # 执行对应的子命令 | ||||||
|  |     if args.command == 'pepb': | ||||||
|  |         # PE/PB分析 | ||||||
|  |         start_date = (datetime.datetime.now() - datetime.timedelta(days=args.days)).strftime('%Y-%m-%d') | ||||||
|  |         end_date = datetime.datetime.now().strftime('%Y-%m-%d') | ||||||
|          |          | ||||||
|     # 运行分析 |         analyzer = pe_pb_analysis.StockValuationAnalyzer() | ||||||
|     analyzer = ValuationAnalyzer() |         result = analyzer.get_stock_pe_pb_analysis(args.stock, start_date, end_date) | ||||||
|     result = analyzer.analyze_stock_valuation(stock_code, start_date, metrics) |  | ||||||
|      |  | ||||||
|     # 输出结果 |  | ||||||
|     if not result['success']: |  | ||||||
|         print(f"分析失败: {result.get('message', '未知错误')}") |  | ||||||
|         return 1 |  | ||||||
|          |          | ||||||
|     # 打印分析结果 |         if result["success"]: | ||||||
|     stock_name = result['stock_name'] |             # 输出结果 | ||||||
|     analysis_date = result['analysis_date'] |             if args.output in ['json', 'all']: | ||||||
|  |                 output_file = os.path.join(OUTPUT_DIR, f"{args.stock}_pepb_analysis.json") | ||||||
|  |                 with open(output_file, 'w', encoding='utf-8') as f: | ||||||
|  |                     json.dump(result, f, ensure_ascii=False, indent=2) | ||||||
|  |                 print(f"结果已保存到: {output_file}") | ||||||
|  |                  | ||||||
|  |             if args.output in ['csv', 'all']: | ||||||
|  |                 # 这里可以添加CSV输出逻辑 | ||||||
|  |                 pass | ||||||
|  |                  | ||||||
|  |             # 打印简要结果 | ||||||
|  |             print(f"\n{args.stock} PE/PB分析结果:") | ||||||
|  |             print(f"当前PE: {result['pe']['current']:.2f}, 百分位: {result['pe']['percentile']:.2f}%") | ||||||
|  |             print(f"当前PB: {result['pb']['current']:.2f}, 百分位: {result['pb']['percentile']:.2f}%") | ||||||
|  |         else: | ||||||
|  |             print(f"分析失败: {result['message']}") | ||||||
|      |      | ||||||
|     if output_format == 'json': |     elif args.command == 'roe': | ||||||
|         # 将图表路径转换为相对路径字符串 |         # ROE分析 | ||||||
|         for metric in result['metrics']: |         analyzer = pe_pb_analysis.StockValuationAnalyzer() | ||||||
|             if 'chart_path' in result['metrics'][metric]: |         result = analyzer.get_stock_roe_analysis(args.stock) | ||||||
|                 result['metrics'][metric]['chart_path'] = str(result['metrics'][metric]['chart_path']) |  | ||||||
|          |          | ||||||
|         # 写入JSON文件 |         if result["success"]: | ||||||
|         with open(output_path, 'w', encoding='utf-8') as f: |             # 输出结果 | ||||||
|             json.dump(result, f, ensure_ascii=False, indent=2) |             if args.output in ['json', 'all']: | ||||||
|  |                 output_file = os.path.join(OUTPUT_DIR, f"{args.stock}_roe_analysis.json") | ||||||
|  |                 with open(output_file, 'w', encoding='utf-8') as f: | ||||||
|  |                     json.dump(result, f, ensure_ascii=False, indent=2) | ||||||
|  |                 print(f"结果已保存到: {output_file}") | ||||||
|  |                  | ||||||
|  |             # 打印简要结果 | ||||||
|  |             print(f"\n{args.stock} ROE分析结果:") | ||||||
|  |             print(f"最新ROE: {result['latest_roe']:.2f}%") | ||||||
|  |             print(f"5年平均ROE: {result['avg_5year_roe']:.2f}%") | ||||||
|  |         else: | ||||||
|  |             print(f"分析失败: {result['message']}") | ||||||
|  |      | ||||||
|  |     elif args.command == 'industry': | ||||||
|  |         # 行业估值分析 | ||||||
|  |         start_date = (datetime.datetime.now() - datetime.timedelta(days=args.days)).strftime('%Y-%m-%d') | ||||||
|  |          | ||||||
|  |         analyzer = industry_analysis.IndustryAnalyzer() | ||||||
|  |         result = analyzer.get_industry_analysis(args.name, args.metric, start_date) | ||||||
|  |          | ||||||
|  |         if result["success"]: | ||||||
|  |             # 输出结果 | ||||||
|  |             if args.output in ['json', 'all']: | ||||||
|  |                 output_file = os.path.join(OUTPUT_DIR, f"{args.name}_{args.metric}_analysis.json") | ||||||
|  |                 with open(output_file, 'w', encoding='utf-8') as f: | ||||||
|  |                     json.dump(result, f, ensure_ascii=False, indent=2) | ||||||
|  |                 print(f"结果已保存到: {output_file}") | ||||||
|  |                  | ||||||
|  |             # 打印简要结果 | ||||||
|  |             print(f"\n{args.name} {args.metric.upper()}分析结果:") | ||||||
|  |             current = result['valuation']['percentiles']['current'] | ||||||
|  |             percentile = result['valuation']['percentiles']['percentile'] | ||||||
|  |             print(f"当前{args.metric.upper()}: {current:.2f}, 百分位: {percentile:.2f}%") | ||||||
|  |             if "crowding" in result: | ||||||
|  |                 crowding_level = result['crowding']['current']['level'] | ||||||
|  |                 crowding_percentile = result['crowding']['current']['percentile'] | ||||||
|  |                 print(f"行业拥挤度: {crowding_level} ({crowding_percentile:.2f}%)") | ||||||
|  |         else: | ||||||
|  |             print(f"分析失败: {result['message']}") | ||||||
|  |      | ||||||
|  |     elif args.command == 'industry-list': | ||||||
|  |         # 获取行业列表 | ||||||
|  |         analyzer = industry_analysis.IndustryAnalyzer() | ||||||
|  |         industry_list = analyzer.get_industry_list() | ||||||
|  |          | ||||||
|  |         if industry_list: | ||||||
|  |             for i, industry in enumerate(industry_list, 1): | ||||||
|  |                 print(f"{i}. {industry['name']} ({industry['code']})") | ||||||
|  |         else: | ||||||
|  |             print("获取行业列表失败") | ||||||
|              |              | ||||||
|         print(f"分析结果已保存至: {output_path}") |     elif args.command == 'rzrq': | ||||||
|     else: |         # 融资融券数据采集 | ||||||
|         # 打印文本格式分析结果 |         collector = rzrq_collector.RzrqCollector() | ||||||
|         print("\n" + "="*50) |  | ||||||
|         print(f"股票代码: {stock_code}") |  | ||||||
|         print(f"股票名称: {stock_name}") |  | ||||||
|         print(f"分析日期: {analysis_date}") |  | ||||||
|         print("="*50) |  | ||||||
|          |          | ||||||
|         for metric in result['metrics']: |         if args.action == 'init': | ||||||
|             metric_data = result['metrics'][metric] |             # 输出建表SQL | ||||||
|             metric_name = "PE" if metric == "pe" else "PB" |             if args.output_sql: | ||||||
|  |                 print("创建融资融券数据表的SQL语句:") | ||||||
|  |                 print(rzrq_collector.get_create_table_sql()) | ||||||
|  |                 print("\n") | ||||||
|              |              | ||||||
|             print(f"\n{metric_name}分析结果:") |             # 首次全量采集 | ||||||
|             print("-"*30) |             print("开始首次全量采集融资融券数据...") | ||||||
|             print(f"当前{metric_name}: {metric_data['current']:.2f}") |             result = collector.initial_data_collection() | ||||||
|             print(f"{metric_name}百分位: {metric_data['percentile']:.2f}%") |             if result: | ||||||
|             print(f"历史最小值: {metric_data['min']:.2f}") |                 print("融资融券数据采集完成") | ||||||
|             print(f"历史最大值: {metric_data['max']:.2f}") |             else: | ||||||
|             print(f"历史均值: {metric_data['mean']:.2f}") |                 print("融资融券数据采集失败") | ||||||
|             print(f"历史中位数: {metric_data['median']:.2f}") |                  | ||||||
|             print(f"第一四分位数: {metric_data['q1']:.2f}") |         elif args.action == 'update': | ||||||
|             print(f"第三四分位数: {metric_data['q3']:.2f}") |             # 更新最新数据 | ||||||
|             print(f"估值曲线图: {metric_data['chart_path']}") |             print("开始更新最新融资融券数据...") | ||||||
|              |             result = collector.update_latest_data() | ||||||
|         print("\n" + "="*50) |             if result: | ||||||
|         print(f"分析完成,图表已保存") |                 print("融资融券数据更新完成") | ||||||
|          |             else: | ||||||
|     return 0 |                 print("融资融券数据更新失败") | ||||||
| 
 |                  | ||||||
|  |         elif args.action == 'run-scheduler': | ||||||
|  |             # 运行定时器 | ||||||
|  |             print("启动融资融券数据采集定时器,将在每天下午17:00自动更新...") | ||||||
|  |             print("按Ctrl+C终止") | ||||||
|  |             collector.schedule_daily_update() | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|     sys.exit(main())  |     main()  | ||||||
|  | @ -0,0 +1,863 @@ | ||||||
|  | """ | ||||||
|  | 东方财富融资融券数据采集模块 | ||||||
|  | 提供从东方财富网站采集融资融券数据并存储到数据库的功能 | ||||||
|  | 功能包括: | ||||||
|  | 1. 采集融资融券数据 | ||||||
|  | 2. 存储数据到数据库 | ||||||
|  | 3. 定时自动更新数据 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import requests | ||||||
|  | import pandas as pd | ||||||
|  | import datetime | ||||||
|  | import logging | ||||||
|  | import time | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | from pathlib import Path | ||||||
|  | from sqlalchemy import create_engine, text | ||||||
|  | 
 | ||||||
|  | # 添加项目根目录到Python路径 | ||||||
|  | current_file = Path(__file__) | ||||||
|  | project_root = current_file.parent.parent.parent | ||||||
|  | sys.path.append(str(project_root)) | ||||||
|  | 
 | ||||||
|  | from src.valuation_analysis.config import DB_URL, LOG_FILE | ||||||
|  | 
 | ||||||
|  | # 获取项目根目录 | ||||||
|  | ROOT_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||||
|  | 
 | ||||||
|  | # 确保日志目录存在 | ||||||
|  | os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) | ||||||
|  | 
 | ||||||
|  | # 配置日志 | ||||||
|  | logging.basicConfig( | ||||||
|  |     level=logging.INFO, | ||||||
|  |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||||
|  |     handlers=[ | ||||||
|  |         logging.FileHandler(LOG_FILE), | ||||||
|  |         logging.StreamHandler() | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | logger = logging.getLogger("eastmoney_rzrq_collector") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class EastmoneyRzrqCollector: | ||||||
|  |     """东方财富融资融券数据采集器类""" | ||||||
|  |      | ||||||
|  |     def __init__(self, db_url: str = DB_URL): | ||||||
|  |         """ | ||||||
|  |         初始化东方财富融资融券数据采集器 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             db_url: 数据库连接URL | ||||||
|  |         """ | ||||||
|  |         self.engine = create_engine( | ||||||
|  |             db_url, | ||||||
|  |             pool_size=5, | ||||||
|  |             max_overflow=10, | ||||||
|  |             pool_recycle=3600 | ||||||
|  |         ) | ||||||
|  |         self.base_url = "https://datacenter-web.eastmoney.com/api/data/v1/get" | ||||||
|  |         self.headers = { | ||||||
|  |             "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", | ||||||
|  |             "Accept": "application/json, text/plain, */*", | ||||||
|  |             "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", | ||||||
|  |             "Origin": "https://data.eastmoney.com", | ||||||
|  |             "Referer": "https://data.eastmoney.com/", | ||||||
|  |         } | ||||||
|  |         logger.info("东方财富融资融券数据采集器初始化完成") | ||||||
|  |      | ||||||
|  |     def _parse_date(self, date_str: str) -> datetime.date: | ||||||
|  |         """将日期字符串解析为日期对象""" | ||||||
|  |         if not date_str: | ||||||
|  |             return None | ||||||
|  |         try: | ||||||
|  |             return datetime.datetime.strptime(date_str.split()[0], "%Y-%m-%d").date() | ||||||
|  |         except ValueError: | ||||||
|  |             logger.error(f"日期解析失败: {date_str}") | ||||||
|  |             return None | ||||||
|  |      | ||||||
|  |     def _ensure_table_exists(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         确保数据表存在,如果不存在则创建 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功确保表存在 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             create_table_query = text(""" | ||||||
|  |             CREATE TABLE IF NOT EXISTS eastmoney_rzrq_data ( | ||||||
|  |                 trade_date DATE PRIMARY KEY, | ||||||
|  |                 index_value DECIMAL(10,4) COMMENT '指数', | ||||||
|  |                 change_percent DECIMAL(10,4) COMMENT '涨跌幅', | ||||||
|  |                 float_market_value DECIMAL(20,2) COMMENT '流通市值', | ||||||
|  |                 change_percent_3d DECIMAL(10,4) COMMENT '3日涨跌幅', | ||||||
|  |                 change_percent_5d DECIMAL(10,4) COMMENT '5日涨跌幅', | ||||||
|  |                 change_percent_10d DECIMAL(10,4) COMMENT '10日涨跌幅', | ||||||
|  |                 financing_balance DECIMAL(20,2) COMMENT '融资余额', | ||||||
|  |                 financing_balance_ratio DECIMAL(10,4) COMMENT '融资余额占比', | ||||||
|  |                 financing_buy_amount DECIMAL(20,2) COMMENT '融资买入额', | ||||||
|  |                 financing_buy_amount_3d DECIMAL(20,2) COMMENT '3日融资买入额', | ||||||
|  |                 financing_buy_amount_5d DECIMAL(20,2) COMMENT '5日融资买入额', | ||||||
|  |                 financing_buy_amount_10d DECIMAL(20,2) COMMENT '10日融资买入额', | ||||||
|  |                 financing_repay_amount DECIMAL(20,2) COMMENT '融资偿还额', | ||||||
|  |                 financing_repay_amount_3d DECIMAL(20,2) COMMENT '3日融资偿还额', | ||||||
|  |                 financing_repay_amount_5d DECIMAL(20,2) COMMENT '5日融资偿还额', | ||||||
|  |                 financing_repay_amount_10d DECIMAL(20,2) COMMENT '10日融资偿还额', | ||||||
|  |                 financing_net_amount DECIMAL(20,2) COMMENT '融资净额', | ||||||
|  |                 financing_net_amount_3d DECIMAL(20,2) COMMENT '3日融资净额', | ||||||
|  |                 financing_net_amount_5d DECIMAL(20,2) COMMENT '5日融资净额', | ||||||
|  |                 financing_net_amount_10d DECIMAL(20,2) COMMENT '10日融资净额', | ||||||
|  |                 securities_balance DECIMAL(20,2) COMMENT '融券余额', | ||||||
|  |                 securities_volume DECIMAL(20,2) COMMENT '融券余量', | ||||||
|  |                 securities_repay_volume DECIMAL(20,2) COMMENT '融券偿还量', | ||||||
|  |                 securities_repay_volume_3d DECIMAL(20,2) COMMENT '3日融券偿还量', | ||||||
|  |                 securities_repay_volume_5d DECIMAL(20,2) COMMENT '5日融券偿还量', | ||||||
|  |                 securities_repay_volume_10d DECIMAL(20,2) COMMENT '10日融券偿还量', | ||||||
|  |                 securities_sell_volume DECIMAL(20,2) COMMENT '融券卖出量', | ||||||
|  |                 securities_sell_volume_3d DECIMAL(20,2) COMMENT '3日融券卖出量', | ||||||
|  |                 securities_sell_volume_5d DECIMAL(20,2) COMMENT '5日融券卖出量', | ||||||
|  |                 securities_sell_volume_10d DECIMAL(20,2) COMMENT '10日融券卖出量', | ||||||
|  |                 securities_net_volume DECIMAL(20,2) COMMENT '融券净量', | ||||||
|  |                 securities_net_volume_3d DECIMAL(20,2) COMMENT '3日融券净量', | ||||||
|  |                 securities_net_volume_5d DECIMAL(20,2) COMMENT '5日融券净量', | ||||||
|  |                 securities_net_volume_10d DECIMAL(20,2) COMMENT '10日融券净量', | ||||||
|  |                 total_rzrq_balance DECIMAL(20,2) COMMENT '融资融券余额', | ||||||
|  |                 total_rzrq_balance_cz DECIMAL(20,2) COMMENT '融资融券余额差值', | ||||||
|  |                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |                 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  |             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='东方财富融资融券数据表'; | ||||||
|  |             """) | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 conn.execute(create_table_query) | ||||||
|  |                 conn.commit() | ||||||
|  |                  | ||||||
|  |             logger.info("东方财富融资融券数据表创建成功") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"确保数据表存在失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def fetch_data(self, page: int = 1, page_size: int = 50) -> pd.DataFrame: | ||||||
|  |         """ | ||||||
|  |         获取指定页码的融资融券数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             page: 页码 | ||||||
|  |             page_size: 每页数据量 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含融资融券数据的DataFrame | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             params = { | ||||||
|  |                 "reportName": "RPTA_RZRQ_LSHJ", | ||||||
|  |                 "columns": "ALL", | ||||||
|  |                 "source": "WEB", | ||||||
|  |                 "sortColumns": "dim_date", | ||||||
|  |                 "sortTypes": "-1", | ||||||
|  |                 "pageNumber": page, | ||||||
|  |                 "pageSize": page_size | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             logger.info(f"开始获取第 {page} 页数据") | ||||||
|  |              | ||||||
|  |             response = requests.get(self.base_url, params=params, headers=self.headers) | ||||||
|  |             if response.status_code != 200: | ||||||
|  |                 logger.error(f"获取第 {page} 页数据失败: HTTP {response.status_code}") | ||||||
|  |                 return pd.DataFrame() | ||||||
|  |              | ||||||
|  |             data = response.json() | ||||||
|  |             if not data.get("success"): | ||||||
|  |                 logger.error(f"获取数据失败: {data.get('message', '未知错误')}") | ||||||
|  |                 return pd.DataFrame() | ||||||
|  |              | ||||||
|  |             # 提取数据列表 | ||||||
|  |             items = data.get("result", {}).get("data", []) | ||||||
|  |             if not items: | ||||||
|  |                 logger.warning(f"第 {page} 页未找到有效数据") | ||||||
|  |                 return pd.DataFrame() | ||||||
|  |              | ||||||
|  |             # 转换为DataFrame | ||||||
|  |             df = pd.DataFrame(items) | ||||||
|  |              | ||||||
|  |             # 重命名列 | ||||||
|  |             column_mapping = { | ||||||
|  |                 "DIM_DATE": "trade_date", | ||||||
|  |                 "NEW": "index_value", | ||||||
|  |                 "ZDF": "change_percent", | ||||||
|  |                 "LTSZ": "float_market_value", | ||||||
|  |                 "ZDF3D": "change_percent_3d", | ||||||
|  |                 "ZDF5D": "change_percent_5d", | ||||||
|  |                 "ZDF10D": "change_percent_10d", | ||||||
|  |                 "RZYE": "financing_balance", | ||||||
|  |                 "RZYEZB": "financing_balance_ratio", | ||||||
|  |                 "RZMRE": "financing_buy_amount", | ||||||
|  |                 "RZMRE3D": "financing_buy_amount_3d", | ||||||
|  |                 "RZMRE5D": "financing_buy_amount_5d", | ||||||
|  |                 "RZMRE10D": "financing_buy_amount_10d", | ||||||
|  |                 "RZCHE": "financing_repay_amount", | ||||||
|  |                 "RZCHE3D": "financing_repay_amount_3d", | ||||||
|  |                 "RZCHE5D": "financing_repay_amount_5d", | ||||||
|  |                 "RZCHE10D": "financing_repay_amount_10d", | ||||||
|  |                 "RZJME": "financing_net_amount", | ||||||
|  |                 "RZJME3D": "financing_net_amount_3d", | ||||||
|  |                 "RZJME5D": "financing_net_amount_5d", | ||||||
|  |                 "RZJME10D": "financing_net_amount_10d", | ||||||
|  |                 "RQYE": "securities_balance", | ||||||
|  |                 "RQYL": "securities_volume", | ||||||
|  |                 "RQCHL": "securities_repay_volume", | ||||||
|  |                 "RQCHL3D": "securities_repay_volume_3d", | ||||||
|  |                 "RQCHL5D": "securities_repay_volume_5d", | ||||||
|  |                 "RQCHL10D": "securities_repay_volume_10d", | ||||||
|  |                 "RQMCL": "securities_sell_volume", | ||||||
|  |                 "RQMCL3D": "securities_sell_volume_3d", | ||||||
|  |                 "RQMCL5D": "securities_sell_volume_5d", | ||||||
|  |                 "RQMCL10D": "securities_sell_volume_10d", | ||||||
|  |                 "RQJMG": "securities_net_volume", | ||||||
|  |                 "RQJMG3D": "securities_net_volume_3d", | ||||||
|  |                 "RQJMG5D": "securities_net_volume_5d", | ||||||
|  |                 "RQJMG10D": "securities_net_volume_10d", | ||||||
|  |                 "RZRQYE": "total_rzrq_balance", | ||||||
|  |                 "RZRQYECZ": "total_rzrq_balance_cz" | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             df = df.rename(columns=column_mapping) | ||||||
|  |              | ||||||
|  |             # 转换日期格式 | ||||||
|  |             df['trade_date'] = df['trade_date'].apply(self._parse_date) | ||||||
|  |              | ||||||
|  |             logger.info(f"第 {page} 页数据获取成功,包含 {len(df)} 条记录") | ||||||
|  |             return df | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取第 {page} 页数据失败: {e}") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |      | ||||||
|  |     def fetch_all_data(self, max_pages: int = 368) -> pd.DataFrame: | ||||||
|  |         """ | ||||||
|  |         获取所有页的融资融券数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             max_pages: 最大页数 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含所有融资融券数据的DataFrame | ||||||
|  |         """ | ||||||
|  |         all_data = [] | ||||||
|  |          | ||||||
|  |         for page in range(1, max_pages + 1): | ||||||
|  |             page_data = self.fetch_data(page) | ||||||
|  |             if page_data.empty: | ||||||
|  |                 logger.info(f"第 {page} 页数据为空,停止采集") | ||||||
|  |                 break | ||||||
|  |             all_data.append(page_data) | ||||||
|  |             # 添加延迟,避免请求过于频繁 | ||||||
|  |             time.sleep(1) | ||||||
|  |          | ||||||
|  |         if all_data: | ||||||
|  |             combined_df = pd.concat(all_data, ignore_index=True) | ||||||
|  |             logger.info(f"数据采集完成,共采集 {len(combined_df)} 条记录") | ||||||
|  |             return combined_df | ||||||
|  |         else: | ||||||
|  |             logger.warning("未获取到任何有效数据") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |      | ||||||
|  |     def save_to_database(self, data: pd.DataFrame) -> bool: | ||||||
|  |         """ | ||||||
|  |         将数据保存到数据库 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             data: 要保存的数据DataFrame | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             是否成功保存数据 | ||||||
|  |         """ | ||||||
|  |         if data.empty: | ||||||
|  |             logger.warning("没有数据需要保存") | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             # 确保数据表存在 | ||||||
|  |             if not self._ensure_table_exists(): | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 将nan值转换为None(在SQL中会变成NULL) | ||||||
|  |             data = data.replace({pd.NA: None, pd.NaT: None}) | ||||||
|  |             data = data.where(pd.notnull(data), None) | ||||||
|  |              | ||||||
|  |             # 添加数据或更新已有数据 | ||||||
|  |             inserted_count = 0 | ||||||
|  |             updated_count = 0 | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 for _, row in data.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) | ||||||
|  |                         updated_count += 1 | ||||||
|  |                     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) | ||||||
|  |                         inserted_count += 1 | ||||||
|  |                  | ||||||
|  |                 conn.commit() | ||||||
|  |              | ||||||
|  |             logger.info(f"数据保存成功:新增 {inserted_count} 条记录,更新 {updated_count} 条记录") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"保存数据到数据库失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def update_latest_data(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         更新最新一天的融资融券数据 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功更新最新数据 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info("开始更新最新一天的融资融券数据") | ||||||
|  |              | ||||||
|  |             # 获取第一页数据 | ||||||
|  |             df = self.fetch_data(1) | ||||||
|  |             if df.empty: | ||||||
|  |                 logger.warning("未获取到最新数据") | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 只保留第一行(最新一天的数据) | ||||||
|  |             latest_data = df.iloc[:1] | ||||||
|  |              | ||||||
|  |             # 保存数据到数据库 | ||||||
|  |             result = self.save_to_database(latest_data) | ||||||
|  |              | ||||||
|  |             if result: | ||||||
|  |                 logger.info(f"最新数据({latest_data.iloc[0]['trade_date']})更新成功") | ||||||
|  |             else: | ||||||
|  |                 logger.warning("最新数据更新失败") | ||||||
|  |                  | ||||||
|  |             return result | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"更新最新数据失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def initial_data_collection(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         首次全量采集融资融券数据 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功采集所有数据 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info("开始获取最新融资融券数据...") | ||||||
|  |             df = collector.fetch_data(page=1) | ||||||
|  |              | ||||||
|  |             if not df.empty: | ||||||
|  |                 # 保存数据到数据库 | ||||||
|  |                 if collector.save_to_database(df): | ||||||
|  |                     logger.info(f"成功更新最新数据,日期:{df.iloc[0]['trade_date']}") | ||||||
|  |                 else: | ||||||
|  |                     logger.error("更新最新数据失败") | ||||||
|  |             else: | ||||||
|  |                 logger.warning("未获取到最新数据") | ||||||
|  | 
 | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"首次全量采集失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def get_chart_data(self, limit_days: int = 30) -> dict: | ||||||
|  |         """ | ||||||
|  |         获取融资融券数据用于图表展示 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             limit_days: 获取最近多少天的数据,默认30天 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             dict: 包含图表所需数据的字典 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info(f"获取最近 {limit_days} 天融资融券数据用于图表展示") | ||||||
|  |              | ||||||
|  |             # 构建SQL查询 | ||||||
|  |             query = text(""" | ||||||
|  |             SELECT  | ||||||
|  |                 trade_date, | ||||||
|  |                 financing_balance, | ||||||
|  |                 financing_buy_amount, | ||||||
|  |                 securities_balance, | ||||||
|  |                 total_rzrq_balance | ||||||
|  |             FROM eastmoney_rzrq_data | ||||||
|  |             ORDER BY trade_date DESC | ||||||
|  |             LIMIT :limit_days | ||||||
|  |             """) | ||||||
|  |              | ||||||
|  |             # 执行查询 | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 result = conn.execute(query, {"limit_days": limit_days}).fetchall() | ||||||
|  |                  | ||||||
|  |             if not result: | ||||||
|  |                 logger.warning("未找到融资融券数据") | ||||||
|  |                 return {"success": False, "message": "未找到融资融券数据"} | ||||||
|  |              | ||||||
|  |             # 将结果转换为列表并倒序,使日期按升序排列 | ||||||
|  |             rows = [] | ||||||
|  |             for row in result: | ||||||
|  |                 # 将每行结果转换为字典 | ||||||
|  |                 row_dict = { | ||||||
|  |                     'trade_date': row.trade_date, | ||||||
|  |                     'financing_balance': row.financing_balance, | ||||||
|  |                     'financing_buy_amount': row.financing_buy_amount,  | ||||||
|  |                     'securities_balance': row.securities_balance, | ||||||
|  |                     'total_rzrq_balance': row.total_rzrq_balance | ||||||
|  |                 } | ||||||
|  |                 rows.append(row_dict) | ||||||
|  |              | ||||||
|  |             # 反转列表使日期按升序排列 | ||||||
|  |             rows.reverse() | ||||||
|  |              | ||||||
|  |             # 准备数据 | ||||||
|  |             dates = [] | ||||||
|  |             total_rzrq_balance = []  # 融资融券余额合计 | ||||||
|  |             total_financing_buy = []  # 融资买入额合计 | ||||||
|  |             total_financing_balance = []  # 融资余额合计 | ||||||
|  |             financing_repayment = []  # 融资偿还 | ||||||
|  |             securities_balance = []  # 融券余额 | ||||||
|  |              | ||||||
|  |             prev_financing_balance = None  # 上一日融资余额 | ||||||
|  |              | ||||||
|  |             for i, row in enumerate(rows): | ||||||
|  |                 dates.append(row['trade_date'].strftime('%Y-%m-%d')) | ||||||
|  |                 # 将金额从元转换为亿元(除以1亿) | ||||||
|  |                 total_rzrq_balance.append(round(float(row['total_rzrq_balance']) / 100000000, 2)) | ||||||
|  |                 total_financing_buy.append(round(float(row['financing_buy_amount']) / 100000000, 2)) | ||||||
|  |                 total_financing_balance.append(round(float(row['financing_balance']) / 100000000, 2)) | ||||||
|  |                 securities_balance.append(round(float(row['securities_balance']) / 100000000, 2)) | ||||||
|  |                  | ||||||
|  |                 # 计算融资偿还 = 融资买入额 + 前一日融资余额 - 当日融资余额 | ||||||
|  |                 if i > 0 and prev_financing_balance is not None: | ||||||
|  |                     # 注意:这里不需要再除以1亿,因为前面的数据已经是亿元单位 | ||||||
|  |                     repayment = float(row['financing_buy_amount']) / 100000000 + prev_financing_balance - float(row['financing_balance']) / 100000000 | ||||||
|  |                     financing_repayment.append(round(repayment, 2)) | ||||||
|  |                 else: | ||||||
|  |                     financing_repayment.append(None)  # 第一天无法计算 | ||||||
|  |                  | ||||||
|  |                 prev_financing_balance = float(row['financing_balance']) / 100000000  # 转换为亿元单位 | ||||||
|  |              | ||||||
|  |             # 计算市场风险分析指标 | ||||||
|  |             risk_indicators = self.analyze_market_risk( | ||||||
|  |                 dates=dates, | ||||||
|  |                 total_rzrq_balance=total_rzrq_balance, | ||||||
|  |                 total_financing_balance=total_financing_balance, | ||||||
|  |                 total_financing_buy=total_financing_buy, | ||||||
|  |                 securities_balance=securities_balance | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             # 构建返回数据 | ||||||
|  |             chart_data = { | ||||||
|  |                 "success": True, | ||||||
|  |                 "dates": dates, | ||||||
|  |                 "series": [ | ||||||
|  |                     { | ||||||
|  |                         "name": "融资融券余额合计", | ||||||
|  |                         "data": total_rzrq_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资买入额合计", | ||||||
|  |                         "data": total_financing_buy, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资余额合计", | ||||||
|  |                         "data": total_financing_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资偿还", | ||||||
|  |                         "data": financing_repayment, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融券余额", | ||||||
|  |                         "data": securities_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 "risk_indicators": risk_indicators | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             # 添加更新时间 | ||||||
|  |             chart_data["last_update"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |              | ||||||
|  |             logger.info(f"成功获取 {len(dates)} 天的融资融券图表数据") | ||||||
|  |             return chart_data | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取融资融券图表数据失败: {e}") | ||||||
|  |             return {"success": False, "message": f"获取数据失败: {str(e)}"} | ||||||
|  |              | ||||||
|  |     def analyze_market_risk(self, dates, total_rzrq_balance, total_financing_balance, total_financing_buy, securities_balance): | ||||||
|  |         """ | ||||||
|  |         分析融资融券数据,计算市场风险指标 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             dates: 日期列表 | ||||||
|  |             total_rzrq_balance: 融资融券余额合计列表 | ||||||
|  |             total_financing_balance: 融资余额合计列表 | ||||||
|  |             total_financing_buy: 融资买入额合计列表 | ||||||
|  |             securities_balance: 融券余额列表 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             dict: 包含风险指标的字典 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 确保数据长度足够 | ||||||
|  |             if len(dates) < 5: | ||||||
|  |                 return {"warning": "数据量不足,无法进行完整分析"} | ||||||
|  |                  | ||||||
|  |             risk_indicators = {} | ||||||
|  |              | ||||||
|  |             # 1. 计算融资融券余额趋势 - 近期变化率 | ||||||
|  |             recent_days = 5  # 分析最近5天的变化 | ||||||
|  |             if len(total_rzrq_balance) >= recent_days: | ||||||
|  |                 latest_balance = total_rzrq_balance[-1] | ||||||
|  |                 prev_balance = total_rzrq_balance[-recent_days] | ||||||
|  |                  | ||||||
|  |                 if prev_balance > 0: | ||||||
|  |                     balance_change_rate = (latest_balance - prev_balance) / prev_balance * 100 | ||||||
|  |                     balance_change_direction = "上升" if balance_change_rate > 0 else "下降" | ||||||
|  |                      | ||||||
|  |                     risk_indicators["recent_balance_change"] = { | ||||||
|  |                         "rate": round(balance_change_rate, 2), | ||||||
|  |                         "direction": balance_change_direction, | ||||||
|  |                         "days": recent_days, | ||||||
|  |                         "start_date": dates[-recent_days], | ||||||
|  |                         "end_date": dates[-1], | ||||||
|  |                         "start_value": round(prev_balance, 2), | ||||||
|  |                         "end_value": round(latest_balance, 2) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 融资融券余额变化 | ||||||
|  |                     if balance_change_rate > 10: | ||||||
|  |                         balance_risk_level = "高" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额快速上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速提升,风险较高" | ||||||
|  |                     elif balance_change_rate > 5: | ||||||
|  |                         balance_risk_level = "中" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所提升" | ||||||
|  |                     elif balance_change_rate < -10: | ||||||
|  |                         balance_risk_level = "高" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额快速下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速下降,可能伴随抛压" | ||||||
|  |                     elif balance_change_rate < -5: | ||||||
|  |                         balance_risk_level = "中" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所下降" | ||||||
|  |                     else: | ||||||
|  |                         balance_risk_level = "低" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额变化{round(balance_change_rate, 2)}%,市场杠杆水平相对稳定" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["balance_risk"] = { | ||||||
|  |                         "level": balance_risk_level, | ||||||
|  |                         "description": balance_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 2. 计算融资偿还比例 - 衡量资金偿还压力 | ||||||
|  |             # 融资偿还比例 = 当日融资偿还 / 当日融资买入 | ||||||
|  |             recent_days_repay = min(5, len(total_financing_buy) - 1)  # 考虑数据长度 | ||||||
|  |             if recent_days_repay > 0: | ||||||
|  |                 # 计算近几天的平均偿还买入比 | ||||||
|  |                 repay_buy_ratios = [] | ||||||
|  |                 for i in range(1, recent_days_repay + 1): | ||||||
|  |                     if total_financing_buy[-i] > 0: | ||||||
|  |                         # 融资偿还 = 当日融资买入额 + 前一日融资余额 - 当日融资余额 | ||||||
|  |                         if i < len(total_financing_balance) and i+1 < len(total_financing_balance): | ||||||
|  |                             repayment = total_financing_buy[-i] + total_financing_balance[-(i+1)] - total_financing_balance[-i] | ||||||
|  |                             ratio = repayment / total_financing_buy[-i] | ||||||
|  |                             repay_buy_ratios.append(ratio) | ||||||
|  |                  | ||||||
|  |                 if repay_buy_ratios: | ||||||
|  |                     avg_repay_buy_ratio = sum(repay_buy_ratios) / len(repay_buy_ratios) | ||||||
|  |                      | ||||||
|  |                     risk_indicators["repay_buy_ratio"] = { | ||||||
|  |                         "value": round(avg_repay_buy_ratio, 2), | ||||||
|  |                         "days": recent_days_repay | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 偿还率 | ||||||
|  |                     if avg_repay_buy_ratio > 1.2: | ||||||
|  |                         repay_risk_level = "高" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还明显大于买入,市场存在较强的抛压" | ||||||
|  |                     elif avg_repay_buy_ratio > 1.05: | ||||||
|  |                         repay_risk_level = "中" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还略大于买入,市场抛压增加" | ||||||
|  |                     elif avg_repay_buy_ratio < 0.8: | ||||||
|  |                         repay_risk_level = "中" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入明显大于偿还,杠杆快速提升" | ||||||
|  |                     else: | ||||||
|  |                         repay_risk_level = "低" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入与偿还较为平衡" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["repay_risk"] = { | ||||||
|  |                         "level": repay_risk_level, | ||||||
|  |                         "description": repay_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 3. 融券余额变化 - 分析空头力量 | ||||||
|  |             if len(securities_balance) >= recent_days: | ||||||
|  |                 latest_securities = securities_balance[-1] | ||||||
|  |                 prev_securities = securities_balance[-recent_days] | ||||||
|  |                  | ||||||
|  |                 if prev_securities > 0: | ||||||
|  |                     securities_change_rate = (latest_securities - prev_securities) / prev_securities * 100 | ||||||
|  |                     securities_change_direction = "上升" if securities_change_rate > 0 else "下降" | ||||||
|  |                      | ||||||
|  |                     risk_indicators["securities_balance_change"] = { | ||||||
|  |                         "rate": round(securities_change_rate, 2), | ||||||
|  |                         "direction": securities_change_direction, | ||||||
|  |                         "days": recent_days, | ||||||
|  |                         "start_value": round(prev_securities, 2), | ||||||
|  |                         "end_value": round(latest_securities, 2) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 融券余额变化 | ||||||
|  |                     if securities_change_rate > 15: | ||||||
|  |                         securities_risk_level = "高" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额快速上升{abs(round(securities_change_rate, 2))}%,空头力量显著增强" | ||||||
|  |                     elif securities_change_rate > 8: | ||||||
|  |                         securities_risk_level = "中" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额上升{abs(round(securities_change_rate, 2))}%,空头力量有所增强" | ||||||
|  |                     elif securities_change_rate < -15: | ||||||
|  |                         securities_risk_level = "低" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额快速下降{abs(round(securities_change_rate, 2))}%,空头力量显著减弱" | ||||||
|  |                     elif securities_change_rate < -8: | ||||||
|  |                         securities_risk_level = "低" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额下降{abs(round(securities_change_rate, 2))}%,空头力量有所减弱" | ||||||
|  |                     else: | ||||||
|  |                         securities_risk_level = "正常" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额变化{round(securities_change_rate, 2)}%,空头力量相对稳定" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["securities_risk"] = { | ||||||
|  |                         "level": securities_risk_level, | ||||||
|  |                         "description": securities_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 4. 融资占比 - 融资余额占融资融券余额的比例 | ||||||
|  |             if total_rzrq_balance[-1] > 0: | ||||||
|  |                 financing_ratio = total_financing_balance[-1] / total_rzrq_balance[-1] | ||||||
|  |                 risk_indicators["financing_ratio"] = round(financing_ratio * 100, 2) | ||||||
|  |                  | ||||||
|  |                 # 融资占比的历史百分位数计算 | ||||||
|  |                 if len(total_rzrq_balance) > 20:  # 至少需要20天数据计算有意义的百分位数 | ||||||
|  |                     historical_ratios = [] | ||||||
|  |                     for i in range(len(total_rzrq_balance)): | ||||||
|  |                         if total_rzrq_balance[i] > 0: | ||||||
|  |                             ratio = total_financing_balance[i] / total_rzrq_balance[i] | ||||||
|  |                             historical_ratios.append(ratio) | ||||||
|  |                      | ||||||
|  |                     if historical_ratios: | ||||||
|  |                         # 计算融资占比的百分位数 | ||||||
|  |                         sorted_ratios = sorted(historical_ratios) | ||||||
|  |                         percentile_rank = sorted_ratios.index(financing_ratio) / len(sorted_ratios) * 100 if financing_ratio in sorted_ratios else 0 | ||||||
|  |                          | ||||||
|  |                         # 如果不在列表中,找到最接近的位置 | ||||||
|  |                         if percentile_rank == 0: | ||||||
|  |                             for i, ratio in enumerate(sorted_ratios): | ||||||
|  |                                 if ratio > financing_ratio: | ||||||
|  |                                     percentile_rank = i / len(sorted_ratios) * 100 | ||||||
|  |                                     break | ||||||
|  |                             if percentile_rank == 0 and financing_ratio > sorted_ratios[-1]: | ||||||
|  |                                 percentile_rank = 100 | ||||||
|  |                                  | ||||||
|  |                         risk_indicators["financing_ratio_percentile"] = round(percentile_rank, 2) | ||||||
|  |              | ||||||
|  |             # 5. 综合风险评估 | ||||||
|  |             risk_levels = [] | ||||||
|  |             if "balance_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["balance_risk"]["level"]) | ||||||
|  |             if "repay_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["repay_risk"]["level"]) | ||||||
|  |             if "securities_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["securities_risk"]["level"]) | ||||||
|  |                  | ||||||
|  |             # 计算综合风险等级 | ||||||
|  |             high_count = risk_levels.count("高") | ||||||
|  |             medium_count = risk_levels.count("中") | ||||||
|  |              | ||||||
|  |             if high_count >= 2: | ||||||
|  |                 overall_risk = "高" | ||||||
|  |                 overall_desc = "多个指标显示市场风险较高,融资融券数据反映市场杠杆或波动风险增加" | ||||||
|  |             elif high_count == 1 or medium_count >= 2: | ||||||
|  |                 overall_risk = "中" | ||||||
|  |                 overall_desc = "部分指标显示市场风险增加,建议关注融资融券数据变化" | ||||||
|  |             else: | ||||||
|  |                 overall_risk = "低" | ||||||
|  |                 overall_desc = "融资融券数据显示市场风险较低,杠杆水平相对稳定" | ||||||
|  |                  | ||||||
|  |             risk_indicators["overall_risk"] = { | ||||||
|  |                 "level": overall_risk, | ||||||
|  |                 "description": overall_desc | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             return risk_indicators | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"分析市场风险指标失败: {e}") | ||||||
|  |             return {"error": f"分析失败: {str(e)}"} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_create_table_sql() -> str: | ||||||
|  |     """ | ||||||
|  |     获取创建东方财富融资融券数据表的SQL语句 | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         创建表的SQL语句 | ||||||
|  |     """ | ||||||
|  |     return """ | ||||||
|  |     CREATE TABLE IF NOT EXISTS eastmoney_rzrq_data ( | ||||||
|  |         trade_date DATE PRIMARY KEY, | ||||||
|  |         index_value DECIMAL(10,4) COMMENT '指数', | ||||||
|  |         change_percent DECIMAL(10,4) COMMENT '涨跌幅', | ||||||
|  |         float_market_value DECIMAL(20,2) COMMENT '流通市值', | ||||||
|  |         change_percent_3d DECIMAL(10,4) COMMENT '3日涨跌幅', | ||||||
|  |         change_percent_5d DECIMAL(10,4) COMMENT '5日涨跌幅', | ||||||
|  |         change_percent_10d DECIMAL(10,4) COMMENT '10日涨跌幅', | ||||||
|  |         financing_balance DECIMAL(20,2) COMMENT '融资余额', | ||||||
|  |         financing_balance_ratio DECIMAL(10,4) COMMENT '融资余额占比', | ||||||
|  |         financing_buy_amount DECIMAL(20,2) COMMENT '融资买入额', | ||||||
|  |         financing_buy_amount_3d DECIMAL(20,2) COMMENT '3日融资买入额', | ||||||
|  |         financing_buy_amount_5d DECIMAL(20,2) COMMENT '5日融资买入额', | ||||||
|  |         financing_buy_amount_10d DECIMAL(20,2) COMMENT '10日融资买入额', | ||||||
|  |         financing_repay_amount DECIMAL(20,2) COMMENT '融资偿还额', | ||||||
|  |         financing_repay_amount_3d DECIMAL(20,2) COMMENT '3日融资偿还额', | ||||||
|  |         financing_repay_amount_5d DECIMAL(20,2) COMMENT '5日融资偿还额', | ||||||
|  |         financing_repay_amount_10d DECIMAL(20,2) COMMENT '10日融资偿还额', | ||||||
|  |         financing_net_amount DECIMAL(20,2) COMMENT '融资净额', | ||||||
|  |         financing_net_amount_3d DECIMAL(20,2) COMMENT '3日融资净额', | ||||||
|  |         financing_net_amount_5d DECIMAL(20,2) COMMENT '5日融资净额', | ||||||
|  |         financing_net_amount_10d DECIMAL(20,2) COMMENT '10日融资净额', | ||||||
|  |         securities_balance DECIMAL(20,2) COMMENT '融券余额', | ||||||
|  |         securities_volume DECIMAL(20,2) COMMENT '融券余量', | ||||||
|  |         securities_repay_volume DECIMAL(20,2) COMMENT '融券偿还量', | ||||||
|  |         securities_repay_volume_3d DECIMAL(20,2) COMMENT '3日融券偿还量', | ||||||
|  |         securities_repay_volume_5d DECIMAL(20,2) COMMENT '5日融券偿还量', | ||||||
|  |         securities_repay_volume_10d DECIMAL(20,2) COMMENT '10日融券偿还量', | ||||||
|  |         securities_sell_volume DECIMAL(20,2) COMMENT '融券卖出量', | ||||||
|  |         securities_sell_volume_3d DECIMAL(20,2) COMMENT '3日融券卖出量', | ||||||
|  |         securities_sell_volume_5d DECIMAL(20,2) COMMENT '5日融券卖出量', | ||||||
|  |         securities_sell_volume_10d DECIMAL(20,2) COMMENT '10日融券卖出量', | ||||||
|  |         securities_net_volume DECIMAL(20,2) COMMENT '融券净量', | ||||||
|  |         securities_net_volume_3d DECIMAL(20,2) COMMENT '3日融券净量', | ||||||
|  |         securities_net_volume_5d DECIMAL(20,2) COMMENT '5日融券净量', | ||||||
|  |         securities_net_volume_10d DECIMAL(20,2) COMMENT '10日融券净量', | ||||||
|  |         total_rzrq_balance DECIMAL(20,2) COMMENT '融资融券余额', | ||||||
|  |         total_rzrq_balance_cz DECIMAL(20,2) COMMENT '融资融券余额差值', | ||||||
|  |         created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |         updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  |     ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='东方财富融资融券数据表'; | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # 示例使用方式 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     # 创建东方财富融资融券数据采集器 | ||||||
|  |     collector = EastmoneyRzrqCollector() | ||||||
|  |      | ||||||
|  |     # 获取最新一页数据 | ||||||
|  |     logger.info("开始获取最新融资融券数据...") | ||||||
|  |     df = collector.fetch_data(page=1) | ||||||
|  |      | ||||||
|  |     if not df.empty: | ||||||
|  |         # 保存数据到数据库 | ||||||
|  |         if collector.save_to_database(df): | ||||||
|  |             logger.info(f"成功更新最新数据,日期:{df.iloc[0]['trade_date']}") | ||||||
|  |         else: | ||||||
|  |             logger.error("更新最新数据失败") | ||||||
|  |     else: | ||||||
|  |         logger.warning("未获取到最新数据") | ||||||
|  | 
 | ||||||
|  |     # 输出创建表的SQL语句 | ||||||
|  |     # print("创建表的SQL语句:") | ||||||
|  |     # print(get_create_table_sql()) | ||||||
|  |     # | ||||||
|  |     # # 首次全量采集数据 | ||||||
|  |     # print("\n开始首次全量采集数据...") | ||||||
|  |     # collector.initial_data_collection() | ||||||
|  | @ -0,0 +1,242 @@ | ||||||
|  | """ | ||||||
|  | 恐贪指数(Fear & Greed Index)数据管理模块 | ||||||
|  | 
 | ||||||
|  | 提供恐贪指数的数据库操作功能,包括: | ||||||
|  | 1. 创建恐贪指数数据表 | ||||||
|  | 2. 新增恐贪指数数据 | ||||||
|  | 3. 查询恐贪指数数据 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import pandas as pd | ||||||
|  | from sqlalchemy import create_engine, text | ||||||
|  | import datetime | ||||||
|  | import logging | ||||||
|  | from typing import Dict, List, Optional, Union | ||||||
|  | 
 | ||||||
|  | from .config import DB_URL, LOG_FILE | ||||||
|  | 
 | ||||||
|  | # 配置日志 | ||||||
|  | logging.basicConfig( | ||||||
|  |     level=logging.INFO, | ||||||
|  |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||||
|  |     handlers=[ | ||||||
|  |         logging.FileHandler(LOG_FILE), | ||||||
|  |         logging.StreamHandler() | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | logger = logging.getLogger("fear_greed_index") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FearGreedIndexManager: | ||||||
|  |     """恐贪指数数据管理器类""" | ||||||
|  |      | ||||||
|  |     def __init__(self, db_url: str = DB_URL): | ||||||
|  |         """ | ||||||
|  |         初始化恐贪指数数据管理器 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             db_url: 数据库连接URL | ||||||
|  |         """ | ||||||
|  |         self.engine = create_engine( | ||||||
|  |             db_url, | ||||||
|  |             pool_size=5, | ||||||
|  |             max_overflow=10, | ||||||
|  |             pool_recycle=3600 | ||||||
|  |         ) | ||||||
|  |         # 确保数据表存在 | ||||||
|  |         self._ensure_table_exists() | ||||||
|  |         logger.info("恐贪指数数据管理器初始化完成") | ||||||
|  |      | ||||||
|  |     def _ensure_table_exists(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         确保恐贪指数数据表存在,如果不存在则创建 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功确保表存在 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 创建恐贪指数表的SQL语句 | ||||||
|  |             create_table_sql = """ | ||||||
|  |             CREATE TABLE IF NOT EXISTS `fear_greed_index` ( | ||||||
|  |               `id` int(11) NOT NULL AUTO_INCREMENT, | ||||||
|  |               `index_value` decimal(5,2) NOT NULL COMMENT '恐贪指数值(0-100)', | ||||||
|  |               `trading_date` date NOT NULL COMMENT '交易日期', | ||||||
|  |               `update_time` datetime NOT NULL COMMENT '更新时间', | ||||||
|  |               PRIMARY KEY (`id`), | ||||||
|  |               UNIQUE KEY `uk_trading_date` (`trading_date`) | ||||||
|  |             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='市场恐贪指数数据'; | ||||||
|  |             """ | ||||||
|  |              | ||||||
|  |             # 执行建表语句 | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 conn.execute(text(create_table_sql)) | ||||||
|  |                 conn.commit() | ||||||
|  |                  | ||||||
|  |             logger.info("恐贪指数数据表创建成功或已存在") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"确保恐贪指数数据表存在失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def add_index_data(self, index_value: float, trading_date: str) -> bool: | ||||||
|  |         """ | ||||||
|  |         添加恐贪指数数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             index_value: 恐贪指数值,范围0-100,保留两位小数 | ||||||
|  |             trading_date: 交易日期,格式为 YYYY-MM-DD | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             是否成功添加数据 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 验证指数值范围 | ||||||
|  |             if not 0 <= index_value <= 100: | ||||||
|  |                 logger.error(f"恐贪指数值 {index_value} 超出范围(0-100)") | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 验证日期格式 | ||||||
|  |             try: | ||||||
|  |                 formatted_date = datetime.datetime.strptime(trading_date, "%Y-%m-%d").date() | ||||||
|  |             except ValueError: | ||||||
|  |                 logger.error(f"无效的日期格式: {trading_date},应为YYYY-MM-DD格式") | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 当前时间作为更新时间 | ||||||
|  |             update_time = datetime.datetime.now() | ||||||
|  |              | ||||||
|  |             # 构建插入SQL | ||||||
|  |             insert_sql = """ | ||||||
|  |             INSERT INTO fear_greed_index (index_value, trading_date, update_time) | ||||||
|  |             VALUES (:index_value, :trading_date, :update_time) | ||||||
|  |             ON DUPLICATE KEY UPDATE | ||||||
|  |             index_value = :index_value, | ||||||
|  |             update_time = :update_time | ||||||
|  |             """ | ||||||
|  |              | ||||||
|  |             # 执行插入操作 | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 conn.execute( | ||||||
|  |                     text(insert_sql), | ||||||
|  |                     { | ||||||
|  |                         "index_value": round(index_value, 2), | ||||||
|  |                         "trading_date": trading_date, | ||||||
|  |                         "update_time": update_time | ||||||
|  |                     } | ||||||
|  |                 ) | ||||||
|  |                 conn.commit() | ||||||
|  |                  | ||||||
|  |             logger.info(f"恐贪指数数据添加成功: 日期={trading_date}, 值={index_value}") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"添加恐贪指数数据失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def get_index_data(self, start_date: str = None, end_date: str = None, limit: int = 730) -> Dict: | ||||||
|  |         """ | ||||||
|  |         获取恐贪指数数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             start_date: 开始日期,格式为YYYY-MM-DD | ||||||
|  |             end_date: 结束日期,格式为YYYY-MM-DD | ||||||
|  |             limit: 限制返回的记录数量,默认为730条(约两年的交易日数据) | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含恐贪指数数据的字典 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 构建查询条件 | ||||||
|  |             conditions = [] | ||||||
|  |             params = {} | ||||||
|  |              | ||||||
|  |             if start_date: | ||||||
|  |                 conditions.append("trading_date >= :start_date") | ||||||
|  |                 params["start_date"] = start_date | ||||||
|  |                  | ||||||
|  |             if end_date: | ||||||
|  |                 conditions.append("trading_date <= :end_date") | ||||||
|  |                 params["end_date"] = end_date | ||||||
|  |                  | ||||||
|  |             # 构建查询SQL | ||||||
|  |             where_clause = " AND ".join(conditions) if conditions else "1=1" | ||||||
|  |             query = f""" | ||||||
|  |             SELECT id, index_value, trading_date, update_time | ||||||
|  |             FROM fear_greed_index | ||||||
|  |             WHERE {where_clause} | ||||||
|  |             ORDER BY trading_date DESC | ||||||
|  |             LIMIT :limit | ||||||
|  |             """ | ||||||
|  |              | ||||||
|  |             params["limit"] = limit | ||||||
|  |              | ||||||
|  |             # 执行查询 | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 result = conn.execute(text(query), params) | ||||||
|  |                 rows = result.fetchall() | ||||||
|  |                  | ||||||
|  |             if not rows: | ||||||
|  |                 logger.warning("未找到恐贪指数数据") | ||||||
|  |                 return { | ||||||
|  |                     "success": False, | ||||||
|  |                     "message": "未找到数据" | ||||||
|  |                 } | ||||||
|  |              | ||||||
|  |             # 处理查询结果 | ||||||
|  |             data = [] | ||||||
|  |             for row in rows: | ||||||
|  |                 data.append({ | ||||||
|  |                     "id": row[0], | ||||||
|  |                     "index_value": float(row[1]), | ||||||
|  |                     "trading_date": row[2].strftime("%Y-%m-%d"), | ||||||
|  |                     "update_time": row[3].strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |                 }) | ||||||
|  |              | ||||||
|  |             # 按日期升序排序,方便生成图表 | ||||||
|  |             data.reverse() | ||||||
|  |              | ||||||
|  |             # 提取日期和指数值列表 | ||||||
|  |             dates = [item["trading_date"] for item in data] | ||||||
|  |             values = [item["index_value"] for item in data] | ||||||
|  |              | ||||||
|  |             # 最新数据是最大日期的数据 | ||||||
|  |             data_by_date = sorted(data, key=lambda x: x["trading_date"], reverse=True) | ||||||
|  |             latest = data_by_date[0] if data_by_date else None | ||||||
|  |              | ||||||
|  |             # 计算恐贪指数状态 | ||||||
|  |             def get_index_status(value): | ||||||
|  |                 if 0 <= value < 25: | ||||||
|  |                     return "极度恐慌" | ||||||
|  |                 elif 25 <= value < 40: | ||||||
|  |                     return "恐慌" | ||||||
|  |                 elif 40 <= value < 50: | ||||||
|  |                     return "偏向恐慌" | ||||||
|  |                 elif 50 <= value < 60: | ||||||
|  |                     return "中性" | ||||||
|  |                 elif 60 <= value < 75: | ||||||
|  |                     return "偏向贪婪" | ||||||
|  |                 elif 75 <= value < 90: | ||||||
|  |                     return "贪婪" | ||||||
|  |                 else: | ||||||
|  |                     return "极度贪婪" | ||||||
|  |              | ||||||
|  |             # 计算最新状态 | ||||||
|  |             latest_status = get_index_status(latest["index_value"]) if latest else None | ||||||
|  |              | ||||||
|  |             return { | ||||||
|  |                 "success": True, | ||||||
|  |                 "dates": dates, | ||||||
|  |                 "values": values, | ||||||
|  |                 "data": data, | ||||||
|  |                 "latest": latest, | ||||||
|  |                 "latest_status": latest_status, | ||||||
|  |                 "update_time": latest["update_time"] if latest else datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取恐贪指数数据失败: {e}") | ||||||
|  |             return { | ||||||
|  |                 "success": False, | ||||||
|  |                 "message": f"获取数据失败: {str(e)}" | ||||||
|  |             }  | ||||||
|  | @ -5,8 +5,6 @@ import requests | ||||||
| import json | import json | ||||||
| from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||||
| import logging | import logging | ||||||
| import pandas as pd |  | ||||||
| import time |  | ||||||
| 
 | 
 | ||||||
| logger = logging.getLogger(__name__) | logger = logging.getLogger(__name__) | ||||||
| 
 | 
 | ||||||
|  | @ -146,7 +144,7 @@ class HSGTMonitor: | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|          |          | ||||||
|         logger.info(f"请求{flow_type}资金数据: start={start_timestamp}, end={end_timestamp}, index_id={index_id}") |         # logger.info(f"请求{flow_type}资金数据: start={start_timestamp}, end={end_timestamp}, index_id={index_id}") | ||||||
|          |          | ||||||
|         try: |         try: | ||||||
|             # 发送请求 |             # 发送请求 | ||||||
|  | @ -164,7 +162,7 @@ class HSGTMonitor: | ||||||
|                 # 提取各通道的数据 |                 # 提取各通道的数据 | ||||||
|                 data_dict = {} |                 data_dict = {} | ||||||
|                  |                  | ||||||
|                 # 检查是否有数据 |                 # 检查是否有数据  | ||||||
|                 if "data" in result["data"] and result["data"]["data"]: |                 if "data" in result["data"] and result["data"]["data"]: | ||||||
|                     for item in result["data"]["data"]: |                     for item in result["data"]["data"]: | ||||||
|                         code = item["code"] |                         code = item["code"] | ||||||
|  |  | ||||||
|  | @ -0,0 +1,124 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
|  | 
 | ||||||
|  | import logging | ||||||
|  | from datetime import datetime, timedelta | ||||||
|  | from sqlalchemy import create_engine, text | ||||||
|  | import pandas as pd | ||||||
|  | 
 | ||||||
|  | from src.valuation_analysis.config import DB_URL | ||||||
|  | 
 | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | class IndexAnalyzer: | ||||||
|  |     """指数数据分析工具类""" | ||||||
|  |      | ||||||
|  |     def __init__(self, db_url=None): | ||||||
|  |         # 初始化数据库连接 | ||||||
|  |         self.db_url = db_url or DB_URL | ||||||
|  |         self.engine = create_engine(self.db_url) | ||||||
|  |          | ||||||
|  |     def get_indices_list(self): | ||||||
|  |         """ | ||||||
|  |         获取可用指数列表 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             list: 包含指数信息的列表 [{"id": id, "name": name, "code": code}, ...] | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 query = text(""" | ||||||
|  |                     SELECT id, gp_name as name, gp_code as code  | ||||||
|  |                     FROM gp_code_zs  | ||||||
|  |                     ORDER BY gp_name | ||||||
|  |                 """) | ||||||
|  |                 result = conn.execute(query).fetchall() | ||||||
|  |                  | ||||||
|  |                 indices = [] | ||||||
|  |                 for row in result: | ||||||
|  |                     indices.append({ | ||||||
|  |                         "id": row[0], | ||||||
|  |                         "name": row[1], | ||||||
|  |                         "code": row[2] | ||||||
|  |                     }) | ||||||
|  |                  | ||||||
|  |                 logger.info(f"获取到 {len(indices)} 个指数") | ||||||
|  |                 return indices | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取指数列表失败: {str(e)}") | ||||||
|  |             return [] | ||||||
|  |      | ||||||
|  |     def get_index_data(self, index_code, start_date=None, end_date=None): | ||||||
|  |         """ | ||||||
|  |         获取指数历史数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             index_code: 指数代码 | ||||||
|  |             start_date: 开始日期 (可选,默认为1年前) | ||||||
|  |             end_date: 结束日期 (可选,默认为今天) | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             dict: 包含指数数据的字典 {"code": code, "dates": [...], "values": [...]} | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 处理日期参数 | ||||||
|  |             if end_date is None: | ||||||
|  |                 end_date = datetime.now().strftime('%Y-%m-%d') | ||||||
|  |                  | ||||||
|  |             if start_date is None: | ||||||
|  |                 start_date = (datetime.now() - timedelta(days=365)).strftime('%Y-%m-%d') | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 query = text(""" | ||||||
|  |                     SELECT timestamp, close | ||||||
|  |                     FROM gp_day_data | ||||||
|  |                     WHERE symbol = :symbol | ||||||
|  |                     AND timestamp BETWEEN :start_date AND :end_date | ||||||
|  |                     ORDER BY timestamp | ||||||
|  |                 """) | ||||||
|  |                  | ||||||
|  |                 result = conn.execute(query, { | ||||||
|  |                     "symbol": index_code, | ||||||
|  |                     "start_date": start_date, | ||||||
|  |                     "end_date": end_date | ||||||
|  |                 }).fetchall() | ||||||
|  |                  | ||||||
|  |                 dates = [] | ||||||
|  |                 values = [] | ||||||
|  |                  | ||||||
|  |                 for row in result: | ||||||
|  |                     dates.append(row[0].strftime('%Y-%m-%d')) | ||||||
|  |                     # close可能是字符串类型,转换为浮点数 | ||||||
|  |                     values.append(float(row[1]) if row[1] else None) | ||||||
|  |                  | ||||||
|  |                 logger.info(f"获取指数 {index_code} 数据: {len(dates)} 条记录") | ||||||
|  |                 return { | ||||||
|  |                     "code": index_code, | ||||||
|  |                     "dates": dates, | ||||||
|  |                     "values": values | ||||||
|  |                 } | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取指数 {index_code} 数据失败: {str(e)}") | ||||||
|  |             return { | ||||||
|  |                 "code": index_code, | ||||||
|  |                 "dates": [], | ||||||
|  |                 "values": [] | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | # 测试代码 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     analyzer = IndexAnalyzer() | ||||||
|  |      | ||||||
|  |     # 测试获取指数列表 | ||||||
|  |     indices = analyzer.get_indices_list() | ||||||
|  |     print(f"指数列表: {indices[:5]}...") | ||||||
|  |      | ||||||
|  |     # 测试获取指数数据 | ||||||
|  |     if indices: | ||||||
|  |         # 测试第一个指数的数据 | ||||||
|  |         first_index = indices[0] | ||||||
|  |         index_data = analyzer.get_index_data(first_index['code']) | ||||||
|  |         print(f"指数 {first_index['name']} 数据:") | ||||||
|  |         print(f"日期数量: {len(index_data['dates'])}") | ||||||
|  |         if index_data['dates']: | ||||||
|  |             print(f"第一个日期: {index_data['dates'][0]}, 值: {index_data['values'][0]}")  | ||||||
|  | @ -85,6 +85,32 @@ class IndustryAnalyzer: | ||||||
|             logger.error(f"获取行业列表失败: {e}") |             logger.error(f"获取行业列表失败: {e}") | ||||||
|             return [] |             return [] | ||||||
|      |      | ||||||
|  |     def get_concept_list(self) -> List[Dict]: | ||||||
|  |         """ | ||||||
|  |         获取所有概念板块列表 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             概念板块列表,每个概念板块为一个字典,包含code和name | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             query = text(""" | ||||||
|  |                 SELECT DISTINCT bk_code, bk_name | ||||||
|  |                 FROM gp_gnbk | ||||||
|  |                 ORDER BY bk_name | ||||||
|  |             """) | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 result = conn.execute(query).fetchall() | ||||||
|  |                  | ||||||
|  |             if result: | ||||||
|  |                 return [{"code": str(row[0]), "name": row[1]} for row in result] | ||||||
|  |             else: | ||||||
|  |                 logger.warning("未找到概念板块数据") | ||||||
|  |                 return [] | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取概念板块列表失败: {e}") | ||||||
|  |             return [] | ||||||
|  |      | ||||||
|     def get_industry_stocks(self, industry_name: str) -> List[str]: |     def get_industry_stocks(self, industry_name: str) -> List[str]: | ||||||
|         """ |         """ | ||||||
|         获取指定行业的所有股票代码 |         获取指定行业的所有股票代码 | ||||||
|  |  | ||||||
|  | @ -0,0 +1,783 @@ | ||||||
|  | """ | ||||||
|  | 融资融券数据采集模块-采集太麻烦了,已被废弃,用东方财富的非常好用 | ||||||
|  | python -m src.valuation_analysis.cli rzrq --action init --output-sql | ||||||
|  | 提供从同花顺网站采集融资融券数据并存储到数据库的功能 | ||||||
|  | 功能包括: | ||||||
|  | 1. 采集融资融券数据 | ||||||
|  | 2. 存储数据到数据库 | ||||||
|  | 3. 定时自动更新数据 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import requests | ||||||
|  | import pandas as pd | ||||||
|  | from bs4 import BeautifulSoup | ||||||
|  | import datetime | ||||||
|  | import logging | ||||||
|  | import time | ||||||
|  | import re | ||||||
|  | from sqlalchemy import create_engine, text | ||||||
|  | 
 | ||||||
|  | from .config import DB_URL, LOG_FILE | ||||||
|  | 
 | ||||||
|  | # 配置日志 | ||||||
|  | logging.basicConfig( | ||||||
|  |     level=logging.INFO, | ||||||
|  |     format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', | ||||||
|  |     handlers=[ | ||||||
|  |         logging.FileHandler(LOG_FILE), | ||||||
|  |         logging.StreamHandler() | ||||||
|  |     ] | ||||||
|  | ) | ||||||
|  | logger = logging.getLogger("rzrq_collector") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class RzrqCollector: | ||||||
|  |     """融资融券数据采集器类""" | ||||||
|  |      | ||||||
|  |     def __init__(self, db_url: str = DB_URL): | ||||||
|  |         """ | ||||||
|  |         初始化融资融券数据采集器 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             db_url: 数据库连接URL | ||||||
|  |         """ | ||||||
|  |         self.engine = create_engine( | ||||||
|  |             db_url, | ||||||
|  |             pool_size=5, | ||||||
|  |             max_overflow=10, | ||||||
|  |             pool_recycle=3600 | ||||||
|  |         ) | ||||||
|  |         self.base_url = "https://data.10jqka.com.cn/market/rzrq/board/getRzrqPage/page/{}/ajax/1/" | ||||||
|  |         self.headers = { | ||||||
|  |                             "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", | ||||||
|  |                             "Accept-Encoding": "gzip, deflate, br, zstd", | ||||||
|  |                             "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8", | ||||||
|  |                             "Cache-Control": "max-age=0", | ||||||
|  |                             "Connection": "keep-alive", | ||||||
|  |                             "Cookie": "Hm_lvt_722143063e4892925903024537075d0d=1746513000; HMACCOUNT=8B64A2E3C307C8C0; Hm_lvt_929f8b362150b1f77b477230541dbbc2=1746513000; Hm_lvt_78c58f01938e4d85eaf619eae71b4ed1=1744946910,1746513000; Hm_lvt_60bad21af9c824a4a0530d5dbf4357ca=1746513010; Hm_lvt_f79b64788a4e377c608617fba4c736e2=1746513010; Hm_lpvt_722143063e4892925903024537075d0d=1746513010; Hm_lpvt_929f8b362150b1f77b477230541dbbc2=1746513010; Hm_lpvt_60bad21af9c824a4a0530d5dbf4357ca=1747277468; Hm_lpvt_78c58f01938e4d85eaf619eae71b4ed1=1747277468; Hm_lpvt_f79b64788a4e377c608617fba4c736e2=1747277468; v=AyrTmSqbNs9Y6LqjfoQNiqCwe5vJm6-AoB8imrTj1n0I58QFnCv-BXCvlqGH", | ||||||
|  |                             "Host": "data.10jqka.com.cn", | ||||||
|  |                             "Sec-Fetch-Dest": "document", | ||||||
|  |                             "Sec-Fetch-Mode": "navigate", | ||||||
|  |                             "Sec-Fetch-Site": "none", | ||||||
|  |                             "Sec-Fetch-User": "?1", | ||||||
|  |                             "Upgrade-Insecure-Requests": "1", | ||||||
|  |                             "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36", | ||||||
|  |                             "sec-ch-ua": "\"Chromium\";v=\"134\", \"Not:A-Brand\";v=\"24\", \"Google Chrome\";v=\"134\"", | ||||||
|  |                             "sec-ch-ua-mobile": "?0", | ||||||
|  |                             "sec-ch-ua-platform": "\"Windows\"" | ||||||
|  |                         } | ||||||
|  |         logger.info("融资融券数据采集器初始化完成") | ||||||
|  |      | ||||||
|  |     def _parse_date(self, date_str: str) -> datetime.date: | ||||||
|  |         """将日期字符串解析为日期对象""" | ||||||
|  |         if not date_str: | ||||||
|  |             return None | ||||||
|  |         try: | ||||||
|  |             return datetime.datetime.strptime(date_str, "%Y-%m-%d").date() | ||||||
|  |         except ValueError: | ||||||
|  |             logger.error(f"日期解析失败: {date_str}") | ||||||
|  |             return None | ||||||
|  |      | ||||||
|  |     def _parse_amount(self, amount_str: str) -> float: | ||||||
|  |         """将金额字符串解析为浮点数""" | ||||||
|  |         if not amount_str: | ||||||
|  |             return 0.0 | ||||||
|  |         try: | ||||||
|  |             # 去除逗号和其他非数字字符(除了小数点) | ||||||
|  |             clean_str = re.sub(r'[^\d.]', '', amount_str) | ||||||
|  |             return float(clean_str) | ||||||
|  |         except ValueError: | ||||||
|  |             logger.error(f"金额解析失败: {amount_str}") | ||||||
|  |             return 0.0 | ||||||
|  |      | ||||||
|  |     def _extract_table_data(self, html_content: str) -> pd.DataFrame: | ||||||
|  |         """ | ||||||
|  |         从HTML内容中提取表格数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             html_content: HTML内容 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含融资融券数据的DataFrame | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             soup = BeautifulSoup(html_content, 'html.parser') | ||||||
|  |             table = soup.find('table', class_='m-table') | ||||||
|  |              | ||||||
|  |             if not table: | ||||||
|  |                 logger.error("未找到数据表格") | ||||||
|  |                 return pd.DataFrame() | ||||||
|  |              | ||||||
|  |             rows = table.find_all('tr') | ||||||
|  |             data = [] | ||||||
|  |              | ||||||
|  |             # 跳过表头行(前两行) | ||||||
|  |             for row in rows[2:]:  # 从第三行开始是数据行 | ||||||
|  |                 cells = row.find_all('td') | ||||||
|  |                 if len(cells) >= 17:  # 确保行中有足够的单元格 | ||||||
|  |                     row_data = { | ||||||
|  |                         'trade_date': self._parse_date(cells[0].text.strip()), | ||||||
|  |                         'sh_financing_balance': self._parse_amount(cells[1].text.strip()), | ||||||
|  |                         'sz_financing_balance': self._parse_amount(cells[2].text.strip()), | ||||||
|  |                         'bj_financing_balance': self._parse_amount(cells[3].text.strip()), | ||||||
|  |                         'total_financing_balance': self._parse_amount(cells[4].text.strip()), | ||||||
|  |                         'sh_financing_buy': self._parse_amount(cells[5].text.strip()), | ||||||
|  |                         'sz_financing_buy': self._parse_amount(cells[6].text.strip()), | ||||||
|  |                         'bj_financing_buy': self._parse_amount(cells[7].text.strip()), | ||||||
|  |                         'total_financing_buy': self._parse_amount(cells[8].text.strip()), | ||||||
|  |                         'sh_securities_balance': self._parse_amount(cells[9].text.strip()), | ||||||
|  |                         'sz_securities_balance': self._parse_amount(cells[10].text.strip()), | ||||||
|  |                         'bj_securities_balance': self._parse_amount(cells[11].text.strip()), | ||||||
|  |                         'total_securities_balance': self._parse_amount(cells[12].text.strip()), | ||||||
|  |                         'sh_rzrq_balance': self._parse_amount(cells[13].text.strip()), | ||||||
|  |                         'sz_rzrq_balance': self._parse_amount(cells[14].text.strip()), | ||||||
|  |                         'bj_rzrq_balance': self._parse_amount(cells[15].text.strip()), | ||||||
|  |                         'total_rzrq_balance': self._parse_amount(cells[16].text.strip()), | ||||||
|  |                     } | ||||||
|  |                     data.append(row_data) | ||||||
|  |              | ||||||
|  |             df = pd.DataFrame(data) | ||||||
|  |             return df | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"提取表格数据失败: {e}") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |      | ||||||
|  |     def fetch_page_data(self, page: int = 1) -> pd.DataFrame: | ||||||
|  |         """ | ||||||
|  |         获取指定页码的融资融券数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             page: 页码 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含该页融资融券数据的DataFrame | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             url = self.base_url.format(page) | ||||||
|  |             logger.info(f"开始获取第 {page} 页数据: {url}") | ||||||
|  |              | ||||||
|  |             response = requests.get(url, headers=self.headers) | ||||||
|  |             print(response.text) | ||||||
|  |             if response.status_code != 200: | ||||||
|  |                 logger.error(f"获取第 {page} 页数据失败: HTTP {response.status_code}") | ||||||
|  |                 return pd.DataFrame() | ||||||
|  |              | ||||||
|  |             df = self._extract_table_data(response.text) | ||||||
|  |              | ||||||
|  |             if df.empty: | ||||||
|  |                 logger.warning(f"第 {page} 页未找到有效数据") | ||||||
|  |             else: | ||||||
|  |                 logger.info(f"第 {page} 页数据获取成功,包含 {len(df)} 条记录") | ||||||
|  |                  | ||||||
|  |             return df | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取第 {page} 页数据失败: {e}") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |      | ||||||
|  |     def fetch_all_data(self, pages: int = 4) -> pd.DataFrame: | ||||||
|  |         """ | ||||||
|  |         获取所有页的融资融券数据 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             pages: 要获取的总页数 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             包含所有融资融券数据的DataFrame | ||||||
|  |         """ | ||||||
|  |         all_data = [] | ||||||
|  |          | ||||||
|  |         for page in range(1, pages + 1): | ||||||
|  |             page_data = self.fetch_page_data(page) | ||||||
|  |             if not page_data.empty: | ||||||
|  |                 all_data.append(page_data) | ||||||
|  |             # 添加延迟,避免请求过于频繁 | ||||||
|  |             time.sleep(60) | ||||||
|  |          | ||||||
|  |         if all_data: | ||||||
|  |             combined_df = pd.concat(all_data, ignore_index=True) | ||||||
|  |             logger.info(f"所有页数据获取完成,共 {len(combined_df)} 条记录") | ||||||
|  |             return combined_df | ||||||
|  |         else: | ||||||
|  |             logger.warning("未获取到任何有效数据") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |      | ||||||
|  |     def _ensure_table_exists(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         确保数据表存在,如果不存在则创建 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功确保表存在 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             create_table_query = text(""" | ||||||
|  |             CREATE TABLE IF NOT EXISTS rzrq_data ( | ||||||
|  |                 trade_date DATE PRIMARY KEY, | ||||||
|  |                 sh_financing_balance DECIMAL(12,2) COMMENT '上海融资余额(亿元)', | ||||||
|  |                 sz_financing_balance DECIMAL(12,2) COMMENT '深圳融资余额(亿元)', | ||||||
|  |                 bj_financing_balance DECIMAL(12,2) COMMENT '北京融资余额(亿元)', | ||||||
|  |                 total_financing_balance DECIMAL(12,2) COMMENT '融资余额合计(亿元)', | ||||||
|  |                 sh_financing_buy DECIMAL(12,2) COMMENT '上海融资买入额(亿元)', | ||||||
|  |                 sz_financing_buy DECIMAL(12,2) COMMENT '深圳融资买入额(亿元)', | ||||||
|  |                 bj_financing_buy DECIMAL(12,2) COMMENT '北京融资买入额(亿元)', | ||||||
|  |                 total_financing_buy DECIMAL(12,2) COMMENT '融资买入额合计(亿元)', | ||||||
|  |                 sh_securities_balance DECIMAL(12,2) COMMENT '上海融券余量余额(亿元)', | ||||||
|  |                 sz_securities_balance DECIMAL(12,2) COMMENT '深圳融券余量余额(亿元)', | ||||||
|  |                 bj_securities_balance DECIMAL(12,2) COMMENT '北京融券余量余额(亿元)', | ||||||
|  |                 total_securities_balance DECIMAL(12,2) COMMENT '融券余量余额合计(亿元)', | ||||||
|  |                 sh_rzrq_balance DECIMAL(12,2) COMMENT '上海融资融券余额(亿元)', | ||||||
|  |                 sz_rzrq_balance DECIMAL(12,2) COMMENT '深圳融资融券余额(亿元)', | ||||||
|  |                 bj_rzrq_balance DECIMAL(12,2) COMMENT '北京融资融券余额(亿元)', | ||||||
|  |                 total_rzrq_balance DECIMAL(12,2) COMMENT '融资融券余额合计(亿元)', | ||||||
|  |                 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |                 updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  |             ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='融资融券数据表'; | ||||||
|  |             """) | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 conn.execute(create_table_query) | ||||||
|  |                 conn.commit() | ||||||
|  |                  | ||||||
|  |             logger.info("融资融券数据表创建成功") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"确保数据表存在失败: {e}") | ||||||
|  |             return False | ||||||
|  |      | ||||||
|  |     def save_to_database(self, data: pd.DataFrame) -> bool: | ||||||
|  |         """ | ||||||
|  |         将数据保存到数据库 | ||||||
|  |          | ||||||
|  |         首次存储时,存储所有数据; | ||||||
|  |         后续更新时,如果数据已存在则更新,不存在则插入 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             data: 要保存的数据DataFrame | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             是否成功保存数据 | ||||||
|  |         """ | ||||||
|  |         if data.empty: | ||||||
|  |             logger.warning("没有数据需要保存") | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             # 确保数据表存在 | ||||||
|  |             if not self._ensure_table_exists(): | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 添加数据或更新已有数据 | ||||||
|  |             inserted_count = 0 | ||||||
|  |             updated_count = 0 | ||||||
|  |              | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 for _, row in data.iterrows(): | ||||||
|  |                     # 检查该日期的数据是否已存在 | ||||||
|  |                     check_query = text(""" | ||||||
|  |                     SELECT COUNT(*) FROM rzrq_data WHERE trade_date = :trade_date | ||||||
|  |                     """) | ||||||
|  |                     result = conn.execute(check_query, {"trade_date": row['trade_date']}).scalar() | ||||||
|  |                      | ||||||
|  |                     if result > 0:  # 数据已存在,执行更新 | ||||||
|  |                         update_query = text(""" | ||||||
|  |                         UPDATE rzrq_data SET | ||||||
|  |                             sh_financing_balance = :sh_financing_balance, | ||||||
|  |                             sz_financing_balance = :sz_financing_balance, | ||||||
|  |                             bj_financing_balance = :bj_financing_balance, | ||||||
|  |                             total_financing_balance = :total_financing_balance, | ||||||
|  |                             sh_financing_buy = :sh_financing_buy, | ||||||
|  |                             sz_financing_buy = :sz_financing_buy, | ||||||
|  |                             bj_financing_buy = :bj_financing_buy, | ||||||
|  |                             total_financing_buy = :total_financing_buy, | ||||||
|  |                             sh_securities_balance = :sh_securities_balance, | ||||||
|  |                             sz_securities_balance = :sz_securities_balance, | ||||||
|  |                             bj_securities_balance = :bj_securities_balance, | ||||||
|  |                             total_securities_balance = :total_securities_balance, | ||||||
|  |                             sh_rzrq_balance = :sh_rzrq_balance, | ||||||
|  |                             sz_rzrq_balance = :sz_rzrq_balance, | ||||||
|  |                             bj_rzrq_balance = :bj_rzrq_balance, | ||||||
|  |                             total_rzrq_balance = :total_rzrq_balance | ||||||
|  |                         WHERE trade_date = :trade_date | ||||||
|  |                         """) | ||||||
|  |                         conn.execute(update_query, row.to_dict()) | ||||||
|  |                         updated_count += 1 | ||||||
|  |                     else:  # 数据不存在,执行插入 | ||||||
|  |                         insert_query = text(""" | ||||||
|  |                         INSERT INTO rzrq_data ( | ||||||
|  |                             trade_date,  | ||||||
|  |                             sh_financing_balance, sz_financing_balance, bj_financing_balance, total_financing_balance, | ||||||
|  |                             sh_financing_buy, sz_financing_buy, bj_financing_buy, total_financing_buy, | ||||||
|  |                             sh_securities_balance, sz_securities_balance, bj_securities_balance, total_securities_balance, | ||||||
|  |                             sh_rzrq_balance, sz_rzrq_balance, bj_rzrq_balance, total_rzrq_balance | ||||||
|  |                         ) VALUES ( | ||||||
|  |                             :trade_date, | ||||||
|  |                             :sh_financing_balance, :sz_financing_balance, :bj_financing_balance, :total_financing_balance, | ||||||
|  |                             :sh_financing_buy, :sz_financing_buy, :bj_financing_buy, :total_financing_buy, | ||||||
|  |                             :sh_securities_balance, :sz_securities_balance, :bj_securities_balance, :total_securities_balance, | ||||||
|  |                             :sh_rzrq_balance, :sz_rzrq_balance, :bj_rzrq_balance, :total_rzrq_balance | ||||||
|  |                         ) | ||||||
|  |                         """) | ||||||
|  |                         conn.execute(insert_query, row.to_dict()) | ||||||
|  |                         inserted_count += 1 | ||||||
|  |                  | ||||||
|  |                 conn.commit() | ||||||
|  |              | ||||||
|  |             logger.info(f"数据保存成功:新增 {inserted_count} 条记录,更新 {updated_count} 条记录") | ||||||
|  |             return True | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"保存数据到数据库失败: {e}") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def update_latest_data(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         更新最新一天的融资融券数据 | ||||||
|  |          | ||||||
|  |         仅获取第一页的第一行数据,如果数据库中已存在,则更新;否则插入 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功更新最新数据 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info("开始更新最新一天的融资融券数据") | ||||||
|  |              | ||||||
|  |             # 获取第一页数据 | ||||||
|  |             df = self.fetch_page_data(1) | ||||||
|  |             if df.empty: | ||||||
|  |                 logger.warning("未获取到最新数据") | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 只保留第一行(最新一天的数据) | ||||||
|  |             latest_data = df.iloc[:1] | ||||||
|  |              | ||||||
|  |             # 保存数据到数据库 | ||||||
|  |             result = self.save_to_database(latest_data) | ||||||
|  |              | ||||||
|  |             if result: | ||||||
|  |                 logger.info(f"最新数据({latest_data.iloc[0]['trade_date']})更新成功") | ||||||
|  |             else: | ||||||
|  |                 logger.warning("最新数据更新失败") | ||||||
|  |                  | ||||||
|  |             return result | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"更新最新数据失败: {e}") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def initial_data_collection(self) -> bool: | ||||||
|  |         """ | ||||||
|  |         首次全量采集融资融券数据 | ||||||
|  |          | ||||||
|  |         采集所有页的数据并保存到数据库 | ||||||
|  |          | ||||||
|  |         Returns: | ||||||
|  |             是否成功采集所有数据 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info("开始首次全量采集融资融券数据") | ||||||
|  |              | ||||||
|  |             # 获取所有页的数据 | ||||||
|  |             df = self.fetch_all_data() | ||||||
|  |             if df.empty: | ||||||
|  |                 logger.warning("未获取到任何数据") | ||||||
|  |                 return False | ||||||
|  |              | ||||||
|  |             # 保存数据到数据库 | ||||||
|  |             result = self.save_to_database(df) | ||||||
|  |              | ||||||
|  |             if result: | ||||||
|  |                 logger.info(f"全量数据采集完成,共采集 {len(df)} 条记录") | ||||||
|  |             else: | ||||||
|  |                 logger.warning("全量数据采集失败") | ||||||
|  |                  | ||||||
|  |             return result | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"首次全量采集失败: {e}") | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |     def get_chart_data(self, limit_days: int = 30) -> dict: | ||||||
|  |         """ | ||||||
|  |         获取融资融券数据用于图表展示 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             limit_days: 获取最近多少天的数据,默认30天 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             dict: 包含图表所需数据的字典 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             logger.info(f"获取最近 {limit_days} 天融资融券数据用于图表展示") | ||||||
|  |              | ||||||
|  |             # 构建SQL查询 | ||||||
|  |             query = text(""" | ||||||
|  |             SELECT  | ||||||
|  |                 trade_date, | ||||||
|  |                 total_financing_balance, | ||||||
|  |                 total_financing_buy, | ||||||
|  |                 total_securities_balance, | ||||||
|  |                 total_rzrq_balance | ||||||
|  |             FROM rzrq_data | ||||||
|  |             ORDER BY trade_date DESC | ||||||
|  |             LIMIT :limit_days | ||||||
|  |             """) | ||||||
|  |              | ||||||
|  |             # 执行查询 | ||||||
|  |             with self.engine.connect() as conn: | ||||||
|  |                 result = conn.execute(query, {"limit_days": limit_days}).fetchall() | ||||||
|  |                  | ||||||
|  |             if not result: | ||||||
|  |                 logger.warning("未找到融资融券数据") | ||||||
|  |                 return {"success": False, "message": "未找到融资融券数据"} | ||||||
|  |              | ||||||
|  |             # 将结果转换为列表并倒序,使日期按升序排列 | ||||||
|  |             rows = [] | ||||||
|  |             for row in result: | ||||||
|  |                 # 将每行结果转换为字典 | ||||||
|  |                 row_dict = { | ||||||
|  |                     'trade_date': row.trade_date, | ||||||
|  |                     'total_financing_balance': row.total_financing_balance, | ||||||
|  |                     'total_financing_buy': row.total_financing_buy,  | ||||||
|  |                     'total_securities_balance': row.total_securities_balance, | ||||||
|  |                     'total_rzrq_balance': row.total_rzrq_balance | ||||||
|  |                 } | ||||||
|  |                 rows.append(row_dict) | ||||||
|  |              | ||||||
|  |             # 反转列表使日期按升序排列 | ||||||
|  |             rows.reverse() | ||||||
|  |              | ||||||
|  |             # 准备数据 | ||||||
|  |             dates = [] | ||||||
|  |             total_rzrq_balance = []  # 融资融券余额合计 | ||||||
|  |             total_financing_buy = []  # 融资买入额合计 | ||||||
|  |             total_financing_balance = []  # 融资余额合计 | ||||||
|  |             financing_repayment = []  # 融资偿还 | ||||||
|  |             securities_balance = []  # 融券余额 | ||||||
|  |              | ||||||
|  |             prev_financing_balance = None  # 上一日融资余额 | ||||||
|  |              | ||||||
|  |             for i, row in enumerate(rows): | ||||||
|  |                 dates.append(row['trade_date'].strftime('%Y-%m-%d')) | ||||||
|  |                 total_rzrq_balance.append(float(row['total_rzrq_balance'])) | ||||||
|  |                 total_financing_buy.append(float(row['total_financing_buy'])) | ||||||
|  |                 total_financing_balance.append(float(row['total_financing_balance'])) | ||||||
|  |                  | ||||||
|  |                 # 计算融券余额 = 融资融券余额合计 - 融资余额合计 | ||||||
|  |                 securities_bal = float(row['total_rzrq_balance']) - float(row['total_financing_balance']) | ||||||
|  |                 securities_balance.append(securities_bal) | ||||||
|  |                  | ||||||
|  |                 # 计算融资偿还 = 融资买入额 + 前一日融资余额 - 当日融资余额 | ||||||
|  |                 if i > 0 and prev_financing_balance is not None: | ||||||
|  |                     repayment = float(row['total_financing_buy']) + prev_financing_balance - float(row['total_financing_balance']) | ||||||
|  |                     financing_repayment.append(repayment) | ||||||
|  |                 else: | ||||||
|  |                     financing_repayment.append(None)  # 第一天无法计算 | ||||||
|  |                  | ||||||
|  |                 prev_financing_balance = float(row['total_financing_balance']) | ||||||
|  |              | ||||||
|  |             # 计算市场风险分析指标 | ||||||
|  |             risk_indicators = self.analyze_market_risk( | ||||||
|  |                 dates=dates, | ||||||
|  |                 total_rzrq_balance=total_rzrq_balance, | ||||||
|  |                 total_financing_balance=total_financing_balance, | ||||||
|  |                 total_financing_buy=total_financing_buy, | ||||||
|  |                 securities_balance=securities_balance | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             # 构建返回数据 | ||||||
|  |             chart_data = { | ||||||
|  |                 "success": True, | ||||||
|  |                 "dates": dates, | ||||||
|  |                 "series": [ | ||||||
|  |                     { | ||||||
|  |                         "name": "融资融券余额合计", | ||||||
|  |                         "data": total_rzrq_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资买入额合计", | ||||||
|  |                         "data": total_financing_buy, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资余额合计", | ||||||
|  |                         "data": total_financing_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融资偿还", | ||||||
|  |                         "data": financing_repayment, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     }, | ||||||
|  |                     { | ||||||
|  |                         "name": "融券余额", | ||||||
|  |                         "data": securities_balance, | ||||||
|  |                         "unit": "亿元" | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 "risk_indicators": risk_indicators | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             # 添加更新时间 | ||||||
|  |             chart_data["last_update"] = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||||||
|  |              | ||||||
|  |             logger.info(f"成功获取 {len(dates)} 天的融资融券图表数据") | ||||||
|  |             return chart_data | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"获取融资融券图表数据失败: {e}") | ||||||
|  |             return {"success": False, "message": f"获取数据失败: {str(e)}"} | ||||||
|  |              | ||||||
|  |     def analyze_market_risk(self, dates, total_rzrq_balance, total_financing_balance, total_financing_buy, securities_balance): | ||||||
|  |         """ | ||||||
|  |         分析融资融券数据,计算市场风险指标 | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             dates: 日期列表 | ||||||
|  |             total_rzrq_balance: 融资融券余额合计列表 | ||||||
|  |             total_financing_balance: 融资余额合计列表 | ||||||
|  |             total_financing_buy: 融资买入额合计列表 | ||||||
|  |             securities_balance: 融券余额列表 | ||||||
|  |              | ||||||
|  |         Returns: | ||||||
|  |             dict: 包含风险指标的字典 | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             # 确保数据长度足够 | ||||||
|  |             if len(dates) < 5: | ||||||
|  |                 return {"warning": "数据量不足,无法进行完整分析"} | ||||||
|  |                  | ||||||
|  |             risk_indicators = {} | ||||||
|  |              | ||||||
|  |             # 1. 计算融资融券余额趋势 - 近期变化率 | ||||||
|  |             recent_days = 5  # 分析最近5天的变化 | ||||||
|  |             if len(total_rzrq_balance) >= recent_days: | ||||||
|  |                 latest_balance = total_rzrq_balance[-1] | ||||||
|  |                 prev_balance = total_rzrq_balance[-recent_days] | ||||||
|  |                  | ||||||
|  |                 if prev_balance > 0: | ||||||
|  |                     balance_change_rate = (latest_balance - prev_balance) / prev_balance * 100 | ||||||
|  |                     balance_change_direction = "上升" if balance_change_rate > 0 else "下降" | ||||||
|  |                      | ||||||
|  |                     risk_indicators["recent_balance_change"] = { | ||||||
|  |                         "rate": round(balance_change_rate, 2), | ||||||
|  |                         "direction": balance_change_direction, | ||||||
|  |                         "days": recent_days, | ||||||
|  |                         "start_date": dates[-recent_days], | ||||||
|  |                         "end_date": dates[-1], | ||||||
|  |                         "start_value": round(prev_balance, 2), | ||||||
|  |                         "end_value": round(latest_balance, 2) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 融资融券余额变化 | ||||||
|  |                     if balance_change_rate > 10: | ||||||
|  |                         balance_risk_level = "高" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额快速上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速提升,风险较高" | ||||||
|  |                     elif balance_change_rate > 5: | ||||||
|  |                         balance_risk_level = "中" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额上升{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所提升" | ||||||
|  |                     elif balance_change_rate < -10: | ||||||
|  |                         balance_risk_level = "高" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额快速下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平快速下降,可能伴随抛压" | ||||||
|  |                     elif balance_change_rate < -5: | ||||||
|  |                         balance_risk_level = "中" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额下降{abs(round(balance_change_rate, 2))}%,市场杠杆水平有所下降" | ||||||
|  |                     else: | ||||||
|  |                         balance_risk_level = "低" | ||||||
|  |                         balance_risk_desc = f"近{recent_days}天融资融券余额变化{round(balance_change_rate, 2)}%,市场杠杆水平相对稳定" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["balance_risk"] = { | ||||||
|  |                         "level": balance_risk_level, | ||||||
|  |                         "description": balance_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 2. 计算融资偿还比例 - 衡量资金偿还压力 | ||||||
|  |             # 融资偿还比例 = 当日融资偿还 / 当日融资买入 | ||||||
|  |             recent_days_repay = min(5, len(total_financing_buy) - 1)  # 考虑数据长度 | ||||||
|  |             if recent_days_repay > 0: | ||||||
|  |                 # 计算近几天的平均偿还买入比 | ||||||
|  |                 repay_buy_ratios = [] | ||||||
|  |                 for i in range(1, recent_days_repay + 1): | ||||||
|  |                     if total_financing_buy[-i] > 0: | ||||||
|  |                         # 融资偿还 = 当日融资买入额 + 前一日融资余额 - 当日融资余额 | ||||||
|  |                         if i < len(total_financing_balance) and i+1 < len(total_financing_balance): | ||||||
|  |                             repayment = total_financing_buy[-i] + total_financing_balance[-(i+1)] - total_financing_balance[-i] | ||||||
|  |                             ratio = repayment / total_financing_buy[-i] | ||||||
|  |                             repay_buy_ratios.append(ratio) | ||||||
|  |                  | ||||||
|  |                 if repay_buy_ratios: | ||||||
|  |                     avg_repay_buy_ratio = sum(repay_buy_ratios) / len(repay_buy_ratios) | ||||||
|  |                      | ||||||
|  |                     risk_indicators["repay_buy_ratio"] = { | ||||||
|  |                         "value": round(avg_repay_buy_ratio, 2), | ||||||
|  |                         "days": recent_days_repay | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 偿还率 | ||||||
|  |                     if avg_repay_buy_ratio > 1.2: | ||||||
|  |                         repay_risk_level = "高" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还明显大于买入,市场存在较强的抛压" | ||||||
|  |                     elif avg_repay_buy_ratio > 1.05: | ||||||
|  |                         repay_risk_level = "中" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},偿还略大于买入,市场抛压增加" | ||||||
|  |                     elif avg_repay_buy_ratio < 0.8: | ||||||
|  |                         repay_risk_level = "中" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入明显大于偿还,杠杆快速提升" | ||||||
|  |                     else: | ||||||
|  |                         repay_risk_level = "低" | ||||||
|  |                         repay_risk_desc = f"近期融资偿还与买入比率为{round(avg_repay_buy_ratio, 2)},买入与偿还较为平衡" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["repay_risk"] = { | ||||||
|  |                         "level": repay_risk_level, | ||||||
|  |                         "description": repay_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 3. 融券余额变化 - 分析空头力量 | ||||||
|  |             if len(securities_balance) >= recent_days: | ||||||
|  |                 latest_securities = securities_balance[-1] | ||||||
|  |                 prev_securities = securities_balance[-recent_days] | ||||||
|  |                  | ||||||
|  |                 if prev_securities > 0: | ||||||
|  |                     securities_change_rate = (latest_securities - prev_securities) / prev_securities * 100 | ||||||
|  |                     securities_change_direction = "上升" if securities_change_rate > 0 else "下降" | ||||||
|  |                      | ||||||
|  |                     risk_indicators["securities_balance_change"] = { | ||||||
|  |                         "rate": round(securities_change_rate, 2), | ||||||
|  |                         "direction": securities_change_direction, | ||||||
|  |                         "days": recent_days, | ||||||
|  |                         "start_value": round(prev_securities, 2), | ||||||
|  |                         "end_value": round(latest_securities, 2) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     # 风险评级 - 融券余额变化 | ||||||
|  |                     if securities_change_rate > 15: | ||||||
|  |                         securities_risk_level = "高" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额快速上升{abs(round(securities_change_rate, 2))}%,空头力量显著增强" | ||||||
|  |                     elif securities_change_rate > 8: | ||||||
|  |                         securities_risk_level = "中" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额上升{abs(round(securities_change_rate, 2))}%,空头力量有所增强" | ||||||
|  |                     elif securities_change_rate < -15: | ||||||
|  |                         securities_risk_level = "低" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额快速下降{abs(round(securities_change_rate, 2))}%,空头力量显著减弱" | ||||||
|  |                     elif securities_change_rate < -8: | ||||||
|  |                         securities_risk_level = "低" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额下降{abs(round(securities_change_rate, 2))}%,空头力量有所减弱" | ||||||
|  |                     else: | ||||||
|  |                         securities_risk_level = "正常" | ||||||
|  |                         securities_risk_desc = f"近{recent_days}天融券余额变化{round(securities_change_rate, 2)}%,空头力量相对稳定" | ||||||
|  |                          | ||||||
|  |                     risk_indicators["securities_risk"] = { | ||||||
|  |                         "level": securities_risk_level, | ||||||
|  |                         "description": securities_risk_desc | ||||||
|  |                     } | ||||||
|  |              | ||||||
|  |             # 4. 融资占比 - 融资余额占融资融券余额的比例 | ||||||
|  |             if total_rzrq_balance[-1] > 0: | ||||||
|  |                 financing_ratio = total_financing_balance[-1] / total_rzrq_balance[-1] | ||||||
|  |                 risk_indicators["financing_ratio"] = round(financing_ratio * 100, 2) | ||||||
|  |                  | ||||||
|  |                 # 融资占比的历史百分位数计算 | ||||||
|  |                 if len(total_rzrq_balance) > 20:  # 至少需要20天数据计算有意义的百分位数 | ||||||
|  |                     historical_ratios = [] | ||||||
|  |                     for i in range(len(total_rzrq_balance)): | ||||||
|  |                         if total_rzrq_balance[i] > 0: | ||||||
|  |                             ratio = total_financing_balance[i] / total_rzrq_balance[i] | ||||||
|  |                             historical_ratios.append(ratio) | ||||||
|  |                      | ||||||
|  |                     if historical_ratios: | ||||||
|  |                         # 计算融资占比的百分位数 | ||||||
|  |                         sorted_ratios = sorted(historical_ratios) | ||||||
|  |                         percentile_rank = sorted_ratios.index(financing_ratio) / len(sorted_ratios) * 100 if financing_ratio in sorted_ratios else 0 | ||||||
|  |                          | ||||||
|  |                         # 如果不在列表中,找到最接近的位置 | ||||||
|  |                         if percentile_rank == 0: | ||||||
|  |                             for i, ratio in enumerate(sorted_ratios): | ||||||
|  |                                 if ratio > financing_ratio: | ||||||
|  |                                     percentile_rank = i / len(sorted_ratios) * 100 | ||||||
|  |                                     break | ||||||
|  |                             if percentile_rank == 0 and financing_ratio > sorted_ratios[-1]: | ||||||
|  |                                 percentile_rank = 100 | ||||||
|  |                                  | ||||||
|  |                         risk_indicators["financing_ratio_percentile"] = round(percentile_rank, 2) | ||||||
|  |              | ||||||
|  |             # 5. 综合风险评估 | ||||||
|  |             risk_levels = [] | ||||||
|  |             if "balance_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["balance_risk"]["level"]) | ||||||
|  |             if "repay_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["repay_risk"]["level"]) | ||||||
|  |             if "securities_risk" in risk_indicators: | ||||||
|  |                 risk_levels.append(risk_indicators["securities_risk"]["level"]) | ||||||
|  |                  | ||||||
|  |             # 计算综合风险等级 | ||||||
|  |             high_count = risk_levels.count("高") | ||||||
|  |             medium_count = risk_levels.count("中") | ||||||
|  |              | ||||||
|  |             if high_count >= 2: | ||||||
|  |                 overall_risk = "高" | ||||||
|  |                 overall_desc = "多个指标显示市场风险较高,融资融券数据反映市场杠杆或波动风险增加" | ||||||
|  |             elif high_count == 1 or medium_count >= 2: | ||||||
|  |                 overall_risk = "中" | ||||||
|  |                 overall_desc = "部分指标显示市场风险增加,建议关注融资融券数据变化" | ||||||
|  |             else: | ||||||
|  |                 overall_risk = "低" | ||||||
|  |                 overall_desc = "融资融券数据显示市场风险较低,杠杆水平相对稳定" | ||||||
|  |                  | ||||||
|  |             risk_indicators["overall_risk"] = { | ||||||
|  |                 "level": overall_risk, | ||||||
|  |                 "description": overall_desc | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             return risk_indicators | ||||||
|  |              | ||||||
|  |         except Exception as e: | ||||||
|  |             logger.error(f"分析市场风险指标失败: {e}") | ||||||
|  |             return {"error": f"分析失败: {str(e)}"} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_create_table_sql() -> str: | ||||||
|  |     """ | ||||||
|  |     获取创建融资融券数据表的SQL语句 | ||||||
|  |      | ||||||
|  |     Returns: | ||||||
|  |         创建表的SQL语句 | ||||||
|  |     """ | ||||||
|  |     return """ | ||||||
|  |     CREATE TABLE IF NOT EXISTS rzrq_data ( | ||||||
|  |         trade_date DATE PRIMARY KEY, | ||||||
|  |         sh_financing_balance DECIMAL(12,2) COMMENT '上海融资余额(亿元)', | ||||||
|  |         sz_financing_balance DECIMAL(12,2) COMMENT '深圳融资余额(亿元)', | ||||||
|  |         bj_financing_balance DECIMAL(12,2) COMMENT '北京融资余额(亿元)', | ||||||
|  |         total_financing_balance DECIMAL(12,2) COMMENT '融资余额合计(亿元)', | ||||||
|  |         sh_financing_buy DECIMAL(12,2) COMMENT '上海融资买入额(亿元)', | ||||||
|  |         sz_financing_buy DECIMAL(12,2) COMMENT '深圳融资买入额(亿元)', | ||||||
|  |         bj_financing_buy DECIMAL(12,2) COMMENT '北京融资买入额(亿元)', | ||||||
|  |         total_financing_buy DECIMAL(12,2) COMMENT '融资买入额合计(亿元)', | ||||||
|  |         sh_securities_balance DECIMAL(12,2) COMMENT '上海融券余量余额(亿元)', | ||||||
|  |         sz_securities_balance DECIMAL(12,2) COMMENT '深圳融券余量余额(亿元)', | ||||||
|  |         bj_securities_balance DECIMAL(12,2) COMMENT '北京融券余量余额(亿元)', | ||||||
|  |         total_securities_balance DECIMAL(12,2) COMMENT '融券余量余额合计(亿元)', | ||||||
|  |         sh_rzrq_balance DECIMAL(12,2) COMMENT '上海融资融券余额(亿元)', | ||||||
|  |         sz_rzrq_balance DECIMAL(12,2) COMMENT '深圳融资融券余额(亿元)', | ||||||
|  |         bj_rzrq_balance DECIMAL(12,2) COMMENT '北京融资融券余额(亿元)', | ||||||
|  |         total_rzrq_balance DECIMAL(12,2) COMMENT '融资融券余额合计(亿元)', | ||||||
|  |         created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, | ||||||
|  |         updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | ||||||
|  |     ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='融资融券数据表'; | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # 示例使用方式 | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     # 创建融资融券数据采集器 | ||||||
|  |     collector = RzrqCollector() | ||||||
|  |      | ||||||
|  |     # 输出创建表的SQL语句 | ||||||
|  |     print("创建表的SQL语句:") | ||||||
|  |     print(get_create_table_sql()) | ||||||
|  |      | ||||||
|  |     # 首次全量采集数据 | ||||||
|  |     print("\n开始首次全量采集数据...") | ||||||
|  |     collector.initial_data_collection() | ||||||
|  | @ -0,0 +1,91 @@ | ||||||
|  | 阶段一:数据准备与基础指标计算 | ||||||
|  | 
 | ||||||
|  | 数据加载:使用Pandas加载您提供的rzrq_data.csv融资融券数据。未来若引入市场指数数据(如上证综指、深证成指的收盘价、成交量等),也一并加载,并按日期对齐。 | ||||||
|  | 基础指标计算:根据上一轮讨论的8个指标,计算出每日的指标值。例如: | ||||||
|  | 融资余额净变动 = total_financing_balance.diff() | ||||||
|  | 融资买入活跃度 = total_financing_buy / total_financing_balance.shift(1) | ||||||
|  | 融券余量净变动 = total_securities_balance.diff() | ||||||
|  | 融资融券比 = total_financing_balance / total_securities_balance (注意处理分母为0的情况) | ||||||
|  | 融资买入额趋势:可通过计算total_financing_buy的短期均线与长期均线的关系(如金叉/死叉,或差值)。 | ||||||
|  | 市场总杠杆水平趋势:可通过观察total_rzrq_balance的变动或其均线趋势。 | ||||||
|  | 融资偿还额 = total_financing_balance.shift(1) + total_financing_buy - total_financing_balance | ||||||
|  | 阶段二:指标标准化与信号转换 | ||||||
|  | 
 | ||||||
|  | 对每个基础指标,我们需要将其转化为一个“信号分”,这个分数应能反映该指标当前状态所指示的“机会”或“风险”的强度。 | ||||||
|  | 
 | ||||||
|  | 标准化方法(选其一或组合): | ||||||
|  | 
 | ||||||
|  | Z-Score 标准化: (value - mean) / std_dev。计算指标在一定历史回溯期(如过去1年,约252个交易日)的Z-Score。Z-Score能表示当前值偏离历史均值的程度。 | ||||||
|  | Python实现: 可以用scipy.stats.zscore或手动计算滚动Z-score (series - series.rolling(window=N).mean()) / series.rolling(window=N).std()。 | ||||||
|  | 百分位排名 (Percentile Ranking): 计算当前值在历史回溯期数据中的百分位。例如,0%表示历史最低,100%表示历史最高。 | ||||||
|  | Python实现: scipy.stats.percentileofscore 或 series.rolling(window=N).apply(lambda x: pd.Series(x).rank(pct=True).iloc[-1])。 | ||||||
|  | Min-Max 标准化: 将数值缩放到特定区间,如 [-1, 1] 或 [0, 1]。 scaled = (value - min_val) / (max_val - min_val)。 | ||||||
|  | Python实现: sklearn.preprocessing.MinMaxScaler 或手动计算。 | ||||||
|  | 信号分转换逻辑(示例): | ||||||
|  | 假设我们将每个指标的信号分努力映射到 [-2, +2] 的范围,其中正数代表机会,负数代表风险,绝对值大小代表强度。 | ||||||
|  | 
 | ||||||
|  | 融资余额净变动 (fin_balance_change_z): | ||||||
|  | Z-Score本身可直接作为信号的初步强度。例如,直接使用Z-Score,然后用np.clip(fin_balance_change_z, -2, 2)将其限制在[-2, 2]内。 | ||||||
|  | 融资买入活跃度 (fin_buy_activity_z): | ||||||
|  | 通常活跃度高是正面信号,但极高可能过热。可以对其Z-Score进行评估。 | ||||||
|  | 信号分 score_fin_buy_activity = np.clip(fin_buy_activity_z, -2, 2)。 | ||||||
|  | 融券余量净变动 (sec_balance_change_z): | ||||||
|  | 融券增加是负面信号。 | ||||||
|  | 信号分 score_sec_balance_change = np.clip(-sec_balance_change_z, -2, 2) (注意取负号)。 | ||||||
|  | 融资融券比 (fin_to_sec_ratio_z): | ||||||
|  | 通常比率高且持续上升是正面信号。可以对其Z-Score或其变动的Z-Score进行评估。 | ||||||
|  | 信号分 score_fin_to_sec_ratio = np.clip(fin_to_sec_ratio_z, -2, 2)。 | ||||||
|  | 融资买入额趋势: | ||||||
|  | 例如,短期均线上穿长期均线视为+1,死叉为-1。或者计算(短期MA / 长期MA - 1)的Z-Score。 | ||||||
|  | 信号分 score_fin_buy_trend = np.clip(trend_z_score, -2, 2)。 | ||||||
|  | 市场总杠杆水平趋势 (total_rzrq_balance_change_z or total_rzrq_balance_level_z): | ||||||
|  | 杠杆水平过高可能是风险,但温和上涨可能代表市场信心。可以分析其变动率的Z-Score。如果绝对水平过高(如处于历史90%分位以上),可以额外增加风险权重。 | ||||||
|  | 信号分 score_total_leverage_trend = np.clip(total_rzrq_balance_change_z, -2, 2)。同时,如果total_rzrq_balance_level_z > 1.5 (例如),则可以额外扣分。 | ||||||
|  | 融资偿还额 (fin_repayment_z): | ||||||
|  | 偿还额大幅增加通常是负面信号。 | ||||||
|  | 信号分 score_fin_repayment = np.clip(-fin_repayment_z, -2, 2) (注意取负号)。 | ||||||
|  | (注意:以上转换逻辑和阈值是示例,需要根据实际数据分布和市场理解进行细化和调整。) | ||||||
|  | 
 | ||||||
|  | 阶段三:(可选) 引入市场数据指标 | ||||||
|  | 
 | ||||||
|  | 如果引入了市场指数数据(如上证综指),可以计算额外的市场指标: | ||||||
|  | 指数趋势: 如指数收盘价与其移动平均线(MA20, MA60)的关系,MACD指标等。 | ||||||
|  | 市场波动率: 如指数的实际波动率。 | ||||||
|  | 成交量趋势: 成交量与其移动平均线的关系。 | ||||||
|  | 这些市场指标也需要进行标准化和信号转换,赋予其相应的机会/风险评分。 | ||||||
|  | 阶段四:综合得分计算 | ||||||
|  | 
 | ||||||
|  | 权重设定 (weights): 为每个指标的信号分(score_indicator_X)分配权重。 | ||||||
|  | Python | ||||||
|  | 
 | ||||||
|  | weights = { | ||||||
|  |     'score_fin_balance_change': 1.0, | ||||||
|  |     'score_fin_buy_activity': 0.8, | ||||||
|  |     'score_sec_balance_change': 1.2, | ||||||
|  |     'score_fin_to_sec_ratio': 1.0, | ||||||
|  |     'score_fin_buy_trend': 1.0, | ||||||
|  |     'score_total_leverage_trend': 0.8, | ||||||
|  |     'score_fin_repayment': 1.2, | ||||||
|  |     # 'score_market_index_trend': 1.5, # 如果引入市场指标 | ||||||
|  | } | ||||||
|  | 加权求和: composite_score = Σ (score_indicator_X * weight_X) Python实现: | ||||||
|  | Python | ||||||
|  | 
 | ||||||
|  | df['composite_score'] = 0 | ||||||
|  | total_weight = 0 | ||||||
|  | for indicator_name, weight_value in weights.items(): | ||||||
|  |     df['composite_score'] += df[indicator_name] * weight_value | ||||||
|  |     total_weight += weight_value | ||||||
|  | # (可选) 如果希望将composite_score也标准化到一个固定范围,可以除以total_weight或者再次进行Z-Score或Min-Max | ||||||
|  | # df['composite_score_normalized'] = df['composite_score'] / total_weight | ||||||
|  | 阶段五:结果解读与分级 | ||||||
|  | 
 | ||||||
|  | 定义阈值: 根据历史composite_score的分布,定义不同市场状态的阈值。 例如,假设composite_score经过处理后大致分布在 -10 到 +10 之间: | ||||||
|  | > 7: 强机会 (Strong Opportunity) | ||||||
|  | 3 to 7: 中等机会 (Moderate Opportunity) | ||||||
|  | 0.5 to 3: 轻微机会 (Slight Opportunity) | ||||||
|  | -0.5 to 0.5: 中性 (Neutral) | ||||||
|  | -3 to -0.5: 轻微风险 (Slight Risk) | ||||||
|  | -7 to -3: 中等风险 (Moderate Risk) | ||||||
|  | < -7: 强风险 (Strong Risk) | ||||||
|  | 数值化程度: composite_score的绝对值可以作为“危险”或“机会”的数值化程度。或者,将其映射到一个百分比,例如,如果最大正向得分为10,当前得分7,则机会程度为70%。 | ||||||
		Loading…
	
		Reference in New Issue