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 | ||||
| google-genai | ||||
| 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.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( | ||||
|     level=logging.INFO, | ||||
|  | @ -65,6 +80,15 @@ industry_analyzer = IndustryAnalyzer() | |||
| # 创建监控器实例 | ||||
| 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__))) | ||||
| REPORTS_DIR = os.path.join(ROOT_DIR, 'src', 'reports') | ||||
|  | @ -76,6 +100,9 @@ logger.info(f"报告目录路径: {REPORTS_DIR}") | |||
| # 存储回测任务状态的字典 | ||||
| backtest_tasks = {} | ||||
| 
 | ||||
| # 融资融券数据采集任务列表 | ||||
| rzrq_tasks = {} | ||||
| 
 | ||||
| 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) | ||||
|         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('/') | ||||
| def index(): | ||||
|     """渲染主页""" | ||||
|  | @ -1558,6 +1733,45 @@ def get_industry_list(): | |||
|             "message": f"获取行业列表失败: {str(e)}" | ||||
|         }), 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']) | ||||
| def industry_analysis(): | ||||
|     """ | ||||
|  | @ -1875,6 +2089,67 @@ def get_southbound_data(): | |||
|             "message": f"服务器错误: {str(e)}" | ||||
|         }), 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']) | ||||
| def get_stock_tracks(): | ||||
|     """根据股票代码获取相关赛道信息 | ||||
|  | @ -1939,5 +2214,451 @@ def get_stock_tracks(): | |||
|             "message": f"服务器错误: {str(e)}" | ||||
|         }), 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__': | ||||
|     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-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||
|     'Client-Version': 'v2.44.75', | ||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; 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', | ||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||
|     'Sec-Ch-Ua-Mobile': '?0', | ||||
|  |  | |||
|  | @ -1,4 +1,3 @@ | |||
| 
 | ||||
| # coding:utf-8 | ||||
| 
 | ||||
| import requests | ||||
|  | @ -6,9 +5,8 @@ import pandas as pd | |||
| from sqlalchemy import create_engine, text | ||||
| from datetime import datetime | ||||
| from tqdm import tqdm | ||||
| from config import XUEQIU_HEADERS | ||||
| from src.scripts.config import XUEQIU_HEADERS | ||||
| import gc | ||||
| import time | ||||
| 
 | ||||
| class StockDailyDataCollector: | ||||
|     """股票日线数据采集器类""" | ||||
|  | @ -23,9 +21,20 @@ class StockDailyDataCollector: | |||
|         self.headers = XUEQIU_HEADERS | ||||
| 
 | ||||
|     def fetch_all_stock_codes(self): | ||||
|         query = "SELECT gp_code FROM gp_code_all" | ||||
|         df = pd.read_sql(query, self.engine) | ||||
|         return df['gp_code'].tolist() | ||||
|         # 从gp_code_all获取股票代码 | ||||
|         query_all = "SELECT gp_code FROM gp_code_all" | ||||
|         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): | ||||
|         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 southChart = null; | ||||
|     let rzrqChart = null;  // 融资融券图表实例
 | ||||
|      | ||||
|     // 当前显示的融资融券数据系列
 | ||||
|     let currentMetric = 'total_rzrq_balance'; | ||||
|     // 融资融券数据
 | ||||
|     let rzrqData = null; | ||||
|      | ||||
|     // 融资融券图表相关功能
 | ||||
|     let rzrqIndexSelector = null; | ||||
|     let rzrqChartData = null; // 用于存储融资融券图表的原始数据
 | ||||
|      | ||||
|     // 初始化图表函数,确保DOM元素存在
 | ||||
|     function initCharts() { | ||||
|         try { | ||||
|             const northChartDom = document.getElementById('northChart'); | ||||
|             const southChartDom = document.getElementById('southChart'); | ||||
|             const rzrqChartDom = document.getElementById('rzrqChart'); | ||||
|              | ||||
|             if (northChartDom && !northChart) { | ||||
|                 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) { | ||||
|             console.error('图表初始化过程中发生错误:', e); | ||||
|             return false; | ||||
|  | @ -48,10 +68,46 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|         // 开始加载数据
 | ||||
|         loadData(); | ||||
|          | ||||
|         // 设置自动刷新 (每分钟刷新一次)
 | ||||
|         setInterval(loadData, 60000); | ||||
|         // 加载融资融券数据
 | ||||
|         initRzrqChart(); | ||||
|          | ||||
|         // 检查是否在交易时段,只有在交易时段才设置自动刷新
 | ||||
|         if (isWithinTradingHours()) { | ||||
|             console.log('当前处于交易时段,启用自动刷新'); | ||||
|             // 设置自动刷新 (每分钟刷新一次)
 | ||||
|             window.refreshInterval = setInterval(loadData, 60000); | ||||
|         } else { | ||||
|             console.log('当前不在交易时段,不启用自动刷新'); | ||||
|         } | ||||
|     }, 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() { | ||||
|         if (northChart) { | ||||
|  | @ -68,6 +124,13 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|                 console.error('南向资金图表调整大小失败:', e); | ||||
|             } | ||||
|         } | ||||
|         if (rzrqChart) { | ||||
|             try { | ||||
|                 rzrqChart.resize(); | ||||
|             } catch (e) { | ||||
|                 console.error('融资融券图表调整大小失败:', e); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|      | ||||
|     // 刷新按钮事件
 | ||||
|  | @ -75,9 +138,56 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|     if (refreshBtn) { | ||||
|         refreshBtn.addEventListener('click', function() { | ||||
|             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'); | ||||
|             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(); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // ============ 融资融券图表相关功能 ============
 | ||||
|      | ||||
|     /** | ||||
|      * 初始化融资融券图表 | ||||
|      */ | ||||
|     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; | ||||
|             industryNameSelect.appendChild(option); | ||||
|         }); | ||||
|          | ||||
|         // 如果存在Select2,刷新它
 | ||||
|         if ($.fn.select2 && $(industryNameSelect).data('select2')) { | ||||
|             $(industryNameSelect).trigger('change'); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|  | @ -734,6 +739,11 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|     function resetForm() { | ||||
|         industryForm.reset(); | ||||
|          | ||||
|         // 重置Select2
 | ||||
|         if ($.fn.select2) { | ||||
|             $(industryNameSelect).val('').trigger('change'); | ||||
|         } | ||||
|          | ||||
|         // 隐藏结果和错误信息
 | ||||
|         resultCard.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 startDateInput = document.getElementById('startDate'); | ||||
|     const metricSelect = document.getElementById('metric'); | ||||
|     const industryNameInput = document.getElementById('industryName'); | ||||
|     const conceptNameInput = document.getElementById('conceptName'); | ||||
|     const industryNameSelect = document.getElementById('industryName'); | ||||
|     const conceptNameSelect = document.getElementById('conceptName'); | ||||
|     const analyzeBtn = document.getElementById('analyzeBtn'); | ||||
|     const resetBtn = document.getElementById('resetBtn'); | ||||
|     const loadingSpinner = document.getElementById('loadingSpinner'); | ||||
|  | @ -26,6 +26,128 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|     // 定义图表实例
 | ||||
|     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) { | ||||
|         event.preventDefault(); | ||||
|  | @ -52,8 +174,8 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|         const stockCode = stockCodeInput.value.trim(); | ||||
|         const startDate = startDateInput.value; | ||||
|         const metric = metricSelect.value; | ||||
|         const industryName = industryNameInput.value.trim(); | ||||
|         const conceptName = conceptNameInput.value.trim(); | ||||
|         const industryName = industryNameSelect.value.trim(); | ||||
|         const conceptName = conceptNameSelect.value.trim(); | ||||
|          | ||||
|         // 构建请求URL
 | ||||
|         let url = `/api/valuation_analysis?stock_code=${stockCode}&start_date=${startDate}&metric=${metric}`; | ||||
|  | @ -452,6 +574,10 @@ document.addEventListener('DOMContentLoaded', function() { | |||
|     function resetForm() { | ||||
|         valuationForm.reset(); | ||||
|          | ||||
|         // 重置Select2下拉框
 | ||||
|         $(industryNameSelect).val('').trigger('change'); | ||||
|         $(conceptNameSelect).val('').trigger('change'); | ||||
|          | ||||
|         // 隐藏结果和错误信息
 | ||||
|         resultCard.classList.add('d-none'); | ||||
|         errorAlert.classList.add('d-none'); | ||||
|  |  | |||
|  | @ -6,60 +6,33 @@ | |||
|     <title>沪深港通资金流向监控</title> | ||||
|     <!-- Bootstrap 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> | ||||
| <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="row mb-4"> | ||||
|             <div class="col-12"> | ||||
|  | @ -140,21 +113,202 @@ | |||
|             </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="col-12"> | ||||
|                 <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 class="card-body"> | ||||
|                         <ul> | ||||
|                             <li>数据来源:同花顺数据,每分钟更新</li> | ||||
|                             <li><strong>北向资金</strong>:是指从<strong>香港</strong>流入<strong>A股</strong>的资金,通过沪股通和深股通进入</li> | ||||
|                             <li><strong>南向资金</strong>:是指从<strong>内地</strong>流入<strong>港股</strong>的资金,通过沪市港股通和深市港股通进入</li> | ||||
|                             <li>净流入为正表示买入大于卖出,资金流入(<span class="money-inflow">红色</span>);净流入为负表示卖出大于买入,资金流出(<span class="money-outflow">绿色</span>)</li> | ||||
|                             <li>交易时间:北向9:30-11:30, 13:00-15:00;南向9:30-12:00, 13:00-16:00</li> | ||||
|                         </ul> | ||||
|                         <div id="rzrqChart" class="chart-container"></div> | ||||
|                         <p class="update-time text-center" id="rzrqUpdateTime"></p> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|           | ||||
|         <!-- 融资融券风险分析 --> | ||||
|         <!-- <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> | ||||
|  | @ -166,5 +320,544 @@ | |||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||
|     <script src="../static/js/echarts.min.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> | ||||
| </html>  | ||||
|  | @ -10,8 +10,25 @@ | |||
|     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <!-- 引入ECharts --> | ||||
|     <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 --> | ||||
|     <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> | ||||
| <body> | ||||
|     <!-- 导航栏 --> | ||||
|  | @ -29,6 +46,9 @@ | |||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/industry">行业估值分析</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/hsgt">资金情况</a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|  | @ -64,12 +84,18 @@ | |||
|                      | ||||
|                     <div class="col-md-4"> | ||||
|                         <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 class="col-md-4"> | ||||
|                         <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 class="col-12 text-center mt-4"> | ||||
|  | @ -148,5 +174,39 @@ | |||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||
|     <!-- 引入自定义JS --> | ||||
|     <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> | ||||
| </html>  | ||||
|  | @ -10,8 +10,25 @@ | |||
|     <link href="../static/css/bootstrap.min.css" rel="stylesheet"> | ||||
|     <!-- 引入ECharts --> | ||||
|     <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 --> | ||||
|     <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> | ||||
| <body> | ||||
|     <!-- 导航栏 --> | ||||
|  | @ -29,6 +46,9 @@ | |||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link active" href="/industry">行业估值分析</a> | ||||
|                     </li> | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" href="/hsgt">资金情况</a> | ||||
|                     </li> | ||||
|                 </ul> | ||||
|             </div> | ||||
|         </div> | ||||
|  | @ -45,7 +65,7 @@ | |||
|                 <form id="industryForm" class="row g-3"> | ||||
|                     <div class="col-md-6"> | ||||
|                         <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> | ||||
|                             <!-- 将通过API动态填充 --> | ||||
|                         </select> | ||||
|  | @ -165,5 +185,21 @@ | |||
|     <script src="../static/js/bootstrap.bundle.min.js"></script> | ||||
|     <!-- 引入行业分析JS --> | ||||
|     <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> | ||||
| </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 sys | ||||
| import logging | ||||
| import datetime | ||||
| import json | ||||
| from pathlib import Path | ||||
| from typing import Optional, List, Dict | ||||
| 
 | ||||
| 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() | ||||
| import sys | ||||
| import os | ||||
| 
 | ||||
| from . import pe_pb_analysis | ||||
| from . import industry_analysis | ||||
| from . import rzrq_collector | ||||
| from .config import OUTPUT_DIR | ||||
| 
 | ||||
| def main(): | ||||
|     """主函数""" | ||||
|     args = parse_args() | ||||
|     """命令行工具主函数""" | ||||
|      | ||||
|     # 解析参数 | ||||
|     stock_code = args.stock | ||||
|     start_date = args.start_date | ||||
|     metrics = args.metrics.split(',') | ||||
|     output_format = args.format | ||||
|     parser = argparse.ArgumentParser(description='股票估值分析工具', formatter_class=argparse.RawTextHelpFormatter) | ||||
|     subparsers = parser.add_subparsers(dest='command', help='子命令') | ||||
|      | ||||
|     # 设置输出路径 | ||||
|     output_path = args.output | ||||
|     if output_path is None: | ||||
|         output_path = OUTPUT_DIR / f"{stock_code}_valuation_analysis.{output_format}" | ||||
|     else: | ||||
|         output_path = Path(output_path) | ||||
|     # 设置PE/PB分析子命令 | ||||
|     pepb_parser = subparsers.add_parser('pepb', help='PE/PB分析') | ||||
|     pepb_parser.add_argument('--stock', '-s', required=True, help='股票代码') | ||||
|     pepb_parser.add_argument('--days', '-d', type=int, default=1000, help='分析天数 (默认: 1000)') | ||||
|     pepb_parser.add_argument('--output', '-o', choices=['json', 'csv', 'all'], default='json',  | ||||
|                              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 = ValuationAnalyzer() | ||||
|     result = analyzer.analyze_stock_valuation(stock_code, start_date, metrics) | ||||
|      | ||||
|     # 输出结果 | ||||
|     if not result['success']: | ||||
|         print(f"分析失败: {result.get('message', '未知错误')}") | ||||
|         return 1 | ||||
|         analyzer = pe_pb_analysis.StockValuationAnalyzer() | ||||
|         result = analyzer.get_stock_pe_pb_analysis(args.stock, start_date, end_date) | ||||
|          | ||||
|     # 打印分析结果 | ||||
|     stock_name = result['stock_name'] | ||||
|     analysis_date = result['analysis_date'] | ||||
|         if result["success"]: | ||||
|             # 输出结果 | ||||
|             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': | ||||
|         # 将图表路径转换为相对路径字符串 | ||||
|         for metric in result['metrics']: | ||||
|             if 'chart_path' in result['metrics'][metric]: | ||||
|                 result['metrics'][metric]['chart_path'] = str(result['metrics'][metric]['chart_path']) | ||||
|     elif args.command == 'roe': | ||||
|         # ROE分析 | ||||
|         analyzer = pe_pb_analysis.StockValuationAnalyzer() | ||||
|         result = analyzer.get_stock_roe_analysis(args.stock) | ||||
|          | ||||
|         # 写入JSON文件 | ||||
|         with open(output_path, 'w', encoding='utf-8') as f: | ||||
|             json.dump(result, f, ensure_ascii=False, indent=2) | ||||
|         if result["success"]: | ||||
|             # 输出结果 | ||||
|             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}") | ||||
|     else: | ||||
|         # 打印文本格式分析结果 | ||||
|         print("\n" + "="*50) | ||||
|         print(f"股票代码: {stock_code}") | ||||
|         print(f"股票名称: {stock_name}") | ||||
|         print(f"分析日期: {analysis_date}") | ||||
|         print("="*50) | ||||
|     elif args.command == 'rzrq': | ||||
|         # 融资融券数据采集 | ||||
|         collector = rzrq_collector.RzrqCollector() | ||||
|          | ||||
|         for metric in result['metrics']: | ||||
|             metric_data = result['metrics'][metric] | ||||
|             metric_name = "PE" if metric == "pe" else "PB" | ||||
|         if args.action == 'init': | ||||
|             # 输出建表SQL | ||||
|             if args.output_sql: | ||||
|                 print("创建融资融券数据表的SQL语句:") | ||||
|                 print(rzrq_collector.get_create_table_sql()) | ||||
|                 print("\n") | ||||
|              | ||||
|             print(f"\n{metric_name}分析结果:") | ||||
|             print("-"*30) | ||||
|             print(f"当前{metric_name}: {metric_data['current']:.2f}") | ||||
|             print(f"{metric_name}百分位: {metric_data['percentile']:.2f}%") | ||||
|             print(f"历史最小值: {metric_data['min']:.2f}") | ||||
|             print(f"历史最大值: {metric_data['max']:.2f}") | ||||
|             print(f"历史均值: {metric_data['mean']:.2f}") | ||||
|             print(f"历史中位数: {metric_data['median']:.2f}") | ||||
|             print(f"第一四分位数: {metric_data['q1']:.2f}") | ||||
|             print(f"第三四分位数: {metric_data['q3']:.2f}") | ||||
|             print(f"估值曲线图: {metric_data['chart_path']}") | ||||
|              | ||||
|         print("\n" + "="*50) | ||||
|         print(f"分析完成,图表已保存") | ||||
|          | ||||
|     return 0 | ||||
| 
 | ||||
|             # 首次全量采集 | ||||
|             print("开始首次全量采集融资融券数据...") | ||||
|             result = collector.initial_data_collection() | ||||
|             if result: | ||||
|                 print("融资融券数据采集完成") | ||||
|             else: | ||||
|                 print("融资融券数据采集失败") | ||||
|                  | ||||
|         elif args.action == 'update': | ||||
|             # 更新最新数据 | ||||
|             print("开始更新最新融资融券数据...") | ||||
|             result = collector.update_latest_data() | ||||
|             if result: | ||||
|                 print("融资融券数据更新完成") | ||||
|             else: | ||||
|                 print("融资融券数据更新失败") | ||||
|                  | ||||
|         elif args.action == 'run-scheduler': | ||||
|             # 运行定时器 | ||||
|             print("启动融资融券数据采集定时器,将在每天下午17:00自动更新...") | ||||
|             print("按Ctrl+C终止") | ||||
|             collector.schedule_daily_update() | ||||
| 
 | ||||
| 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 | ||||
| from datetime import datetime, timedelta | ||||
| import logging | ||||
| import pandas as pd | ||||
| import time | ||||
| 
 | ||||
| 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: | ||||
|             # 发送请求 | ||||
|  | @ -164,7 +162,7 @@ class HSGTMonitor: | |||
|                 # 提取各通道的数据 | ||||
|                 data_dict = {} | ||||
|                  | ||||
|                 # 检查是否有数据 | ||||
|                 # 检查是否有数据  | ||||
|                 if "data" in result["data"] and result["data"]["data"]: | ||||
|                     for item in result["data"]["data"]: | ||||
|                         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}") | ||||
|             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]: | ||||
|         """ | ||||
|         获取指定行业的所有股票代码 | ||||
|  |  | |||
|  | @ -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