commit;
This commit is contained in:
		
							parent
							
								
									c4c4e8622f
								
							
						
					
					
						commit
						f2400305e9
					
				|  | @ -11,7 +11,7 @@ show_help() { | ||||||
|     echo "  start [实例ID]        启动指定实例或所有实例" |     echo "  start [实例ID]        启动指定实例或所有实例" | ||||||
|     echo "  stop [实例ID]         停止指定实例或所有实例" |     echo "  stop [实例ID]         停止指定实例或所有实例" | ||||||
|     echo "  restart [实例ID]      重启指定实例或所有实例" |     echo "  restart [实例ID]      重启指定实例或所有实例" | ||||||
|     echo "  logs [实例ID]         查看指定实例的日志" |     echo "  logs [实例ID]         实时查看指定实例的日志 (Ctrl+C 退出)" | ||||||
|     echo "  status               显示实例状态概览" |     echo "  status               显示实例状态概览" | ||||||
|     echo "  remove [实例ID]       删除指定实例或所有实例" |     echo "  remove [实例ID]       删除指定实例或所有实例" | ||||||
|     echo "  rebuild [数量]        重新构建镜像并部署指定数量的实例" |     echo "  rebuild [数量]        重新构建镜像并部署指定数量的实例" | ||||||
|  | @ -68,8 +68,9 @@ restart_instance() { | ||||||
| 
 | 
 | ||||||
| # 函数:查看实例日志 | # 函数:查看实例日志 | ||||||
| view_logs() { | view_logs() { | ||||||
|     echo "实例 $1 的日志:" |     echo "正在实时显示实例 $1 的日志 (按 Ctrl+C 退出):" | ||||||
|     docker logs stock-app-$1 |     echo "----------------------------------------" | ||||||
|  |     docker logs -f stock-app-$1 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| # 函数:显示状态概览 | # 函数:显示状态概览 | ||||||
|  |  | ||||||
							
								
								
									
										63
									
								
								src/app.py
								
								
								
								
							
							
						
						
									
										63
									
								
								src/app.py
								
								
								
								
							|  | @ -2897,21 +2897,64 @@ def get_pep_stock_info_by_shortname(): | ||||||
| 
 | 
 | ||||||
| @app.route('/api/pep_stock_info_by_code', methods=['GET']) | @app.route('/api/pep_stock_info_by_code', methods=['GET']) | ||||||
| def get_pep_stock_info_by_code(): | def get_pep_stock_info_by_code(): | ||||||
|     """根据股票代码查询Redis中的实时行情并返回指定结构""" |     """根据股票代码查询Redis中的实时行情并返回指定结构,支持A股和港股""" | ||||||
|     short_code = request.args.get('code') |     short_code = request.args.get('code') | ||||||
|     if not short_code: |     if not short_code: | ||||||
|         return jsonify({'success': False, 'message': '缺少必要参数: short_code'}), 400 |         return jsonify({'success': False, 'message': '缺少必要参数: code'}), 400 | ||||||
|  |      | ||||||
|     try: |     try: | ||||||
|         # 兼容600001.SH/SH600001等格式 |         # 判断股票类型并调用相应的查询函数 | ||||||
|         from src.quantitative_analysis.batch_stock_price_collector import get_stock_realtime_info_from_redis |         if is_hk_stock_code(short_code): | ||||||
|         result = get_stock_realtime_info_from_redis(short_code) |             # 港股代码 | ||||||
|         if result: |             from src.quantitative_analysis.hk_stock_price_collector import get_hk_stock_realtime_info_from_redis | ||||||
|             return jsonify(result) |             result = get_hk_stock_realtime_info_from_redis(short_code) | ||||||
|  |             if result: | ||||||
|  |                 return jsonify(result) | ||||||
|  |             else: | ||||||
|  |                 return jsonify({'success': False, 'message': f'未找到港股 {short_code} 的实时行情'}), 404 | ||||||
|         else: |         else: | ||||||
|             return jsonify({'success': False, 'message': f'未找到股票 {short_code} 的实时行情'}), 404 |             # A股代码 | ||||||
|  |             from src.quantitative_analysis.batch_stock_price_collector import get_stock_realtime_info_from_redis | ||||||
|  |             result = get_stock_realtime_info_from_redis(short_code) | ||||||
|  |             if result: | ||||||
|  |                 return jsonify(result) | ||||||
|  |             else: | ||||||
|  |                 return jsonify({'success': False, 'message': f'未找到A股 {short_code} 的实时行情'}), 404 | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500 |         return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500 | ||||||
| 
 | 
 | ||||||
|  | def is_hk_stock_code(stock_code): | ||||||
|  |     """ | ||||||
|  |     判断是否为港股代码 | ||||||
|  |     支持格式:00700, 00700.HK, HK00700, 0700.HK等 | ||||||
|  |     """ | ||||||
|  |     if not stock_code: | ||||||
|  |         return False | ||||||
|  |      | ||||||
|  |     stock_code = stock_code.upper().strip() | ||||||
|  |      | ||||||
|  |     # 港股代码特征: | ||||||
|  |     # 1. 包含.HK后缀 | ||||||
|  |     if '.HK' in stock_code: | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  |     # 2. 以HK开头 | ||||||
|  |     if stock_code.startswith('HK'): | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  |     # 3. 纯数字且长度为4-5位(港股代码通常是4-5位数字) | ||||||
|  |     if stock_code.isdigit() and 4 <= len(stock_code) <= 5: | ||||||
|  |         # 进一步判断:港股代码通常以0、1、2、3、6、8、9开头 | ||||||
|  |         if stock_code[0] in ['0', '1', '2', '3', '6', '8', '9']: | ||||||
|  |             return True | ||||||
|  |      | ||||||
|  |     # 4. 特殊港股代码(如腾讯00700、阿里巴巴09988等) | ||||||
|  |     common_hk_codes = ['00700', '09988', '03690', '09888', '06618', '02318', '02020', '01810'] | ||||||
|  |     if stock_code in common_hk_codes: | ||||||
|  |         return True | ||||||
|  |      | ||||||
|  |     return False | ||||||
|  | 
 | ||||||
| @app.route('/api/industry/crowding/filter', methods=['GET']) | @app.route('/api/industry/crowding/filter', methods=['GET']) | ||||||
| def filter_industry_crowding(): | def filter_industry_crowding(): | ||||||
|     """根据拥挤度百分位区间筛选行业和概念板块""" |     """根据拥挤度百分位区间筛选行业和概念板块""" | ||||||
|  | @ -2991,7 +3034,7 @@ def get_momentum_by_plate(): | ||||||
| def run_batch_stock_price_collection(): | def run_batch_stock_price_collection(): | ||||||
|     """批量采集A股行情并保存到数据库""" |     """批量采集A股行情并保存到数据库""" | ||||||
|     try: |     try: | ||||||
|         fetch_and_store_stock_data() |         fetch_and_store_stock_data(use_proxy=True) | ||||||
|         return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) |         return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         logger.error(f"批量采集A股行情失败: {str(e)}") |         logger.error(f"批量采集A股行情失败: {str(e)}") | ||||||
|  | @ -3001,7 +3044,7 @@ def run_batch_stock_price_collection(): | ||||||
| def run_batch_hk_stock_price_collection(): | def run_batch_hk_stock_price_collection(): | ||||||
|     """批量采集港股行情并保存到数据库""" |     """批量采集港股行情并保存到数据库""" | ||||||
|     try: |     try: | ||||||
|         fetch_and_store_hk_stock_data() |         fetch_and_store_hk_stock_data(use_proxy=True) | ||||||
|         return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) |         return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         logger.error(f"批量采集A股行情失败: {str(e)}") |         logger.error(f"批量采集A股行情失败: {str(e)}") | ||||||
|  |  | ||||||
|  | @ -5,14 +5,14 @@ import sys | ||||||
| import os | import os | ||||||
| import redis | import redis | ||||||
| import json | import json | ||||||
|  | import threading | ||||||
|  | from concurrent.futures import ThreadPoolExecutor, as_completed | ||||||
|  | import time | ||||||
| 
 | 
 | ||||||
| # 添加项目根目录到路径,便于导入scripts.config | # 添加项目根目录到路径,便于导入scripts.config | ||||||
| project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||||
| sys.path.append(project_root) | sys.path.append(project_root) | ||||||
| 
 | 
 | ||||||
| # 导入代理管理器 |  | ||||||
| from src.scripts.ProxyIP import EnhancedProxyManager |  | ||||||
| 
 |  | ||||||
| # 读取雪球headers和Redis配置 | # 读取雪球headers和Redis配置 | ||||||
| try: | try: | ||||||
|     from src.scripts.config import XUEQIU_HEADERS |     from src.scripts.config import XUEQIU_HEADERS | ||||||
|  | @ -31,8 +31,13 @@ except ImportError: | ||||||
| 
 | 
 | ||||||
| REDIS_KEY = 'xq_stock_changes_latest'  # 存放行情的主键 | REDIS_KEY = 'xq_stock_changes_latest'  # 存放行情的主键 | ||||||
| 
 | 
 | ||||||
| # 创建全局代理管理器实例 | # 条件导入代理管理器 | ||||||
| proxy_manager = EnhancedProxyManager() | proxy_manager = None | ||||||
|  | try: | ||||||
|  |     from src.scripts.ProxyIP import EnhancedProxyManager | ||||||
|  |     proxy_manager = EnhancedProxyManager() | ||||||
|  | except ImportError: | ||||||
|  |     print("代理管理器导入失败,将使用直接请求模式") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_redis_conn(): | def get_redis_conn(): | ||||||
|  | @ -47,58 +52,130 @@ def get_redis_conn(): | ||||||
|     return redis.Redis(connection_pool=pool) |     return redis.Redis(connection_pool=pool) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def fetch_and_store_stock_data(page_size=90): | def fetch_and_store_stock_data(page_size=90, max_workers=10, use_proxy=False): | ||||||
|     """ |     """ | ||||||
|     批量采集雪球A股(上证、深证、科创板)股票的最新行情数据,并保存到Redis。 |     批量采集雪球A股(上证、深证、科创板)股票的最新行情数据,并保存到Redis。 | ||||||
|  |     使用线程池并行请求,提高采集效率。 | ||||||
|  |      | ||||||
|     :param page_size: 每页采集数量 |     :param page_size: 每页采集数量 | ||||||
|  |     :param max_workers: 线程池最大工作线程数 | ||||||
|  |     :param use_proxy: 是否使用代理(默认False) | ||||||
|     """ |     """ | ||||||
|     base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' |     base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' | ||||||
|     types = ['sha', 'sza', 'kcb']  # 上证、深证、科创板 |     types = ['sha', 'sza', 'kcb']  # 上证、深证、科创板 | ||||||
|     headers = XUEQIU_HEADERS |     headers = XUEQIU_HEADERS | ||||||
| 
 | 
 | ||||||
|     all_data = [] |     all_data = [] | ||||||
|  |     data_lock = threading.Lock()  # 线程安全锁 | ||||||
| 
 | 
 | ||||||
|     for stock_type in types: |     def fetch_page_data(stock_type, page): | ||||||
|  |         """获取单页数据的函数""" | ||||||
|         params = { |         params = { | ||||||
|             'page': 1, |             'page': page, | ||||||
|             'size': page_size, |             'size': page_size, | ||||||
|             'order': 'desc', |             'order': 'desc', | ||||||
|             'order_by': 'dividend_yield', |             'order_by': 'dividend_yield', | ||||||
|             'market': 'CN', |             'market': 'CN', | ||||||
|             'type': stock_type |             'type': stock_type | ||||||
|         } |         } | ||||||
| 
 |          | ||||||
|         # 初次请求以获取总页数,使用代理 |         try: | ||||||
|         response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) |             # 根据配置选择是否使用代理 | ||||||
|         # response = requests.get(base_url, headers=headers, params=params) |             if use_proxy and proxy_manager: | ||||||
|         if response.status_code != 200: |                 response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) | ||||||
|             print(f"请求 {stock_type} 数据失败,状态码:{response.status_code}") |             else: | ||||||
|             continue |                 response = requests.get(base_url, headers=headers, params=params, timeout=10) | ||||||
| 
 |                  | ||||||
|         data = response.json() |  | ||||||
|         total_count = data['data']['count'] |  | ||||||
|         total_pages = (total_count // page_size) + 1 |  | ||||||
| 
 |  | ||||||
|         for page in range(1, total_pages + 1): |  | ||||||
|             params['page'] = page |  | ||||||
|             # response = requests.get(base_url, headers=headers, params=params) |  | ||||||
|             response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) |  | ||||||
|             if response.status_code == 200: |             if response.status_code == 200: | ||||||
|                 data = response.json() |                 data = response.json() | ||||||
|                 all_data.extend(data['data']['list']) |                 page_data = data['data']['list'] | ||||||
|                 print(f"成功采集第 {page}/{total_pages} 页数据") |                  | ||||||
|  |                 # 线程安全地添加数据 | ||||||
|  |                 with data_lock: | ||||||
|  |                     all_data.extend(page_data) | ||||||
|  |                  | ||||||
|  |                 print(f"成功采集 {stock_type} 第 {page} 页数据,获取 {len(page_data)} 条记录") | ||||||
|  |                 return len(page_data) | ||||||
|             else: |             else: | ||||||
|                 print(f"请求 {stock_type} 数据第 {page} 页失败,状态码:{response.status_code}") |                 print(f"请求 {stock_type} 数据第 {page} 页失败,状态码:{response.status_code}") | ||||||
|  |                 return 0 | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"请求 {stock_type} 数据第 {page} 页异常:{e}") | ||||||
|  |             return 0 | ||||||
|  | 
 | ||||||
|  |     # 使用线程池并行采集数据 | ||||||
|  |     with ThreadPoolExecutor(max_workers=max_workers) as executor: | ||||||
|  |         futures = [] | ||||||
|  |          | ||||||
|  |         for stock_type in types: | ||||||
|  |             # 先获取总页数 | ||||||
|  |             params = { | ||||||
|  |                 'page': 1, | ||||||
|  |                 'size': page_size, | ||||||
|  |                 'order': 'desc', | ||||||
|  |                 'order_by': 'dividend_yield', | ||||||
|  |                 'market': 'CN', | ||||||
|  |                 'type': stock_type | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             try: | ||||||
|  |                 # 根据配置选择是否使用代理 | ||||||
|  |                 if use_proxy and proxy_manager: | ||||||
|  |                     response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) | ||||||
|  |                 else: | ||||||
|  |                     response = requests.get(base_url, headers=headers, params=params, timeout=10) | ||||||
|  |                      | ||||||
|  |                 if response.status_code != 200: | ||||||
|  |                     print(f"请求 {stock_type} 数据失败,状态码:{response.status_code}") | ||||||
|  |                     continue | ||||||
|  |                  | ||||||
|  |                 data = response.json() | ||||||
|  |                 total_count = data['data']['count'] | ||||||
|  |                 total_pages = (total_count // page_size) + 1 | ||||||
|  |                  | ||||||
|  |                 print(f"开始采集 {stock_type} 数据,共 {total_pages} 页,总计 {total_count} 条记录") | ||||||
|  |                  | ||||||
|  |                 # 提交所有页面的采集任务 | ||||||
|  |                 for page in range(1, total_pages + 1): | ||||||
|  |                     future = executor.submit(fetch_page_data, stock_type, page) | ||||||
|  |                     futures.append(future) | ||||||
|  |                      | ||||||
|  |             except Exception as e: | ||||||
|  |                 print(f"获取 {stock_type} 总页数失败:{e}") | ||||||
|  |                 continue | ||||||
|  |          | ||||||
|  |         # 等待所有任务完成 | ||||||
|  |         print(f"正在并行采集数据,使用 {max_workers} 个线程...") | ||||||
|  |         start_time = time.time() | ||||||
|  |          | ||||||
|  |         completed_count = 0 | ||||||
|  |         for future in as_completed(futures): | ||||||
|  |             completed_count += 1 | ||||||
|  |             try: | ||||||
|  |                 result = future.result() | ||||||
|  |                 if result > 0: | ||||||
|  |                     print(f"进度: {completed_count}/{len(futures)} 页完成") | ||||||
|  |             except Exception as e: | ||||||
|  |                 print(f"采集任务异常:{e}") | ||||||
|  |          | ||||||
|  |         end_time = time.time() | ||||||
|  |         print(f"数据采集完成,耗时: {end_time - start_time:.2f} 秒") | ||||||
|  | 
 | ||||||
|     # 转换为 DataFrame |     # 转换为 DataFrame | ||||||
|     df = pd.DataFrame(all_data) |     df = pd.DataFrame(all_data) | ||||||
| 
 | 
 | ||||||
|     if not df.empty: |     if not df.empty: | ||||||
|         df['fetch_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |         df['fetch_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||||
|  |          | ||||||
|         # 存入Redis,使用hash结构,key为symbol,value为json字符串 |         # 存入Redis,使用hash结构,key为symbol,value为json字符串 | ||||||
|         r = get_redis_conn() |         r = get_redis_conn() | ||||||
|         pipe = r.pipeline() |         pipe = r.pipeline() | ||||||
|  |          | ||||||
|         # 先清空旧数据 |         # 先清空旧数据 | ||||||
|         r.delete(REDIS_KEY) |         r.delete(REDIS_KEY) | ||||||
|  |          | ||||||
|  |         print(f"正在将 {len(df)} 条记录写入Redis...") | ||||||
|  |          | ||||||
|         for _, row in df.iterrows(): |         for _, row in df.iterrows(): | ||||||
|             symbol = row.get('symbol') |             symbol = row.get('symbol') | ||||||
|             if not symbol: |             if not symbol: | ||||||
|  | @ -106,6 +183,7 @@ def fetch_and_store_stock_data(page_size=90): | ||||||
|             # 只保留必要字段,也可直接存row.to_dict() |             # 只保留必要字段,也可直接存row.to_dict() | ||||||
|             value = row.to_dict() |             value = row.to_dict() | ||||||
|             pipe.hset(REDIS_KEY, symbol, json.dumps(value, ensure_ascii=False)) |             pipe.hset(REDIS_KEY, symbol, json.dumps(value, ensure_ascii=False)) | ||||||
|  |          | ||||||
|         pipe.execute() |         pipe.execute() | ||||||
|         print(f"成功将数据写入Redis哈希 {REDIS_KEY},共{len(df)}条记录。") |         print(f"成功将数据写入Redis哈希 {REDIS_KEY},共{len(df)}条记录。") | ||||||
|          |          | ||||||
|  | @ -192,5 +270,35 @@ def get_stock_realtime_info_from_redis(stock_code): | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def fetch_and_store_stock_data_optimized(page_size=90, max_workers=15, use_proxy=False): | ||||||
|  |     """ | ||||||
|  |     优化版本的批量采集函数,支持更灵活的配置 | ||||||
|  |      | ||||||
|  |     :param page_size: 每页采集数量 | ||||||
|  |     :param max_workers: 线程池最大工作线程数(建议10-20之间) | ||||||
|  |     :param use_proxy: 是否使用代理(默认False) | ||||||
|  |     """ | ||||||
|  |     print(f"开始批量采集A股数据...") | ||||||
|  |     print(f"配置: 每页 {page_size} 条记录,最大线程数 {max_workers}") | ||||||
|  |     print(f"代理模式: {'启用' if use_proxy else '禁用'}") | ||||||
|  |     print(f"预计采集: 上证、深证、科创板所有股票数据") | ||||||
|  |     print("-" * 50) | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         result = fetch_and_store_stock_data(page_size, max_workers, use_proxy) | ||||||
|  |         if not result.empty: | ||||||
|  |             print(f"采集完成!共获取 {len(result)} 只股票的数据") | ||||||
|  |             print(f"数据已保存到Redis键: {REDIS_KEY}") | ||||||
|  |         else: | ||||||
|  |             print("采集完成,但未获取到数据") | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"采集过程中发生错误: {e}") | ||||||
|  |         return None | ||||||
|  |      | ||||||
|  |     return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     fetch_and_store_stock_data()  |     # 可以根据需要调整参数 | ||||||
|  |     # fetch_and_store_stock_data_optimized(page_size=100, max_workers=15, use_proxy=True) | ||||||
|  |     fetch_and_store_stock_data_optimized(use_proxy=False)  # 默认不使用代理  | ||||||
|  | @ -5,14 +5,14 @@ import sys | ||||||
| import os | import os | ||||||
| import redis | import redis | ||||||
| import json | import json | ||||||
|  | import threading | ||||||
|  | from concurrent.futures import ThreadPoolExecutor, as_completed | ||||||
|  | import time | ||||||
| 
 | 
 | ||||||
| # 添加项目根目录到路径,便于导入scripts.config | # 添加项目根目录到路径,便于导入scripts.config | ||||||
| project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||||
| sys.path.append(project_root) | sys.path.append(project_root) | ||||||
| 
 | 
 | ||||||
| # 导入代理管理器 |  | ||||||
| from src.scripts.ProxyIP import EnhancedProxyManager |  | ||||||
| 
 |  | ||||||
| # 读取雪球headers和Redis配置 | # 读取雪球headers和Redis配置 | ||||||
| try: | try: | ||||||
|     from src.scripts.config import XUEQIU_HEADERS |     from src.scripts.config import XUEQIU_HEADERS | ||||||
|  | @ -31,8 +31,13 @@ except ImportError: | ||||||
| 
 | 
 | ||||||
| REDIS_KEY = 'xq_hk_stock_changes_latest'  # 存放港股行情的主键 | REDIS_KEY = 'xq_hk_stock_changes_latest'  # 存放港股行情的主键 | ||||||
| 
 | 
 | ||||||
| # 创建全局代理管理器实例 | # 条件导入代理管理器 | ||||||
| proxy_manager = EnhancedProxyManager() | proxy_manager = None | ||||||
|  | try: | ||||||
|  |     from src.scripts.ProxyIP import EnhancedProxyManager | ||||||
|  |     proxy_manager = EnhancedProxyManager() | ||||||
|  | except ImportError: | ||||||
|  |     print("代理管理器导入失败,将使用直接请求模式") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_redis_conn(): | def get_redis_conn(): | ||||||
|  | @ -47,67 +52,128 @@ def get_redis_conn(): | ||||||
|     return redis.Redis(connection_pool=pool) |     return redis.Redis(connection_pool=pool) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def fetch_and_store_hk_stock_data(page_size=90): | def fetch_and_store_hk_stock_data(page_size=90, max_workers=10, use_proxy=False): | ||||||
|     """ |     """ | ||||||
|     批量采集雪球港股所有股票的最新行情数据,并保存到Redis。 |     批量采集雪球港股所有股票的最新行情数据,并保存到Redis。 | ||||||
|  |     使用线程池并行请求,提高采集效率。 | ||||||
|  |      | ||||||
|     :param page_size: 每页采集数量 |     :param page_size: 每页采集数量 | ||||||
|  |     :param max_workers: 线程池最大工作线程数 | ||||||
|  |     :param use_proxy: 是否使用代理(默认False) | ||||||
|     """ |     """ | ||||||
|     base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' |     base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' | ||||||
|     headers = XUEQIU_HEADERS |     headers = XUEQIU_HEADERS | ||||||
| 
 | 
 | ||||||
|     all_data = [] |     all_data = [] | ||||||
|  |     data_lock = threading.Lock()  # 线程安全锁 | ||||||
| 
 | 
 | ||||||
|     # 使用港股API参数 |     def fetch_page_data(page): | ||||||
|  |         """获取单页数据的函数""" | ||||||
|  |         params = { | ||||||
|  |             'page': page, | ||||||
|  |             'size': page_size, | ||||||
|  |             'order': 'desc', | ||||||
|  |             'order_by': 'dividend_yield', | ||||||
|  |             'market': 'HK',  # 港股市场 | ||||||
|  |             'type': 'hk'     # 港股类型 | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             # 根据配置选择是否使用代理 | ||||||
|  |             if use_proxy and proxy_manager: | ||||||
|  |                 response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) | ||||||
|  |             else: | ||||||
|  |                 response = requests.get(base_url, headers=headers, params=params, timeout=10) | ||||||
|  |                  | ||||||
|  |             if response.status_code == 200: | ||||||
|  |                 data = response.json() | ||||||
|  |                 page_data = data['data']['list'] | ||||||
|  |                  | ||||||
|  |                 # 线程安全地添加数据 | ||||||
|  |                 with data_lock: | ||||||
|  |                     all_data.extend(page_data) | ||||||
|  |                  | ||||||
|  |                 print(f"成功采集港股第 {page} 页数据,获取 {len(page_data)} 条记录") | ||||||
|  |                 return len(page_data) | ||||||
|  |             else: | ||||||
|  |                 print(f"请求港股数据第 {page} 页失败,状态码:{response.status_code}") | ||||||
|  |                 return 0 | ||||||
|  |         except Exception as e: | ||||||
|  |             print(f"请求港股数据第 {page} 页异常:{e}") | ||||||
|  |             return 0 | ||||||
|  | 
 | ||||||
|  |     # 先获取总页数 | ||||||
|     params = { |     params = { | ||||||
|         'page': 1, |         'page': 1, | ||||||
|         'size': page_size, |         'size': page_size, | ||||||
|         'order': 'desc', |         'order': 'desc', | ||||||
|         'order_by': 'dividend_yield', |         'order_by': 'dividend_yield', | ||||||
|         'market': 'HK',  # 港股市场 |         'market': 'HK', | ||||||
|         'type': 'hk'     # 港股类型 |         'type': 'hk' | ||||||
|     } |     } | ||||||
| 
 |      | ||||||
|     # 初次请求以获取总页数,使用代理 |  | ||||||
|     try: |     try: | ||||||
|         response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) |         # 根据配置选择是否使用代理 | ||||||
|  |         if use_proxy and proxy_manager: | ||||||
|  |             response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) | ||||||
|  |         else: | ||||||
|  |             response = requests.get(base_url, headers=headers, params=params, timeout=10) | ||||||
|  |              | ||||||
|         if response.status_code != 200: |         if response.status_code != 200: | ||||||
|             print(f"请求港股数据失败,状态码:{response.status_code}") |             print(f"请求港股数据失败,状态码:{response.status_code}") | ||||||
|             return |             return pd.DataFrame() | ||||||
|  |          | ||||||
|  |         data = response.json() | ||||||
|  |         total_count = data['data']['count'] | ||||||
|  |         total_pages = (total_count // page_size) + 1 | ||||||
|  |          | ||||||
|  |         print(f"开始采集港股数据,共 {total_pages} 页,总计 {total_count} 条记录") | ||||||
|  |          | ||||||
|  |         # 使用线程池并行采集数据 | ||||||
|  |         with ThreadPoolExecutor(max_workers=max_workers) as executor: | ||||||
|  |             futures = [] | ||||||
|  |              | ||||||
|  |             # 提交所有页面的采集任务 | ||||||
|  |             for page in range(1, total_pages + 1): | ||||||
|  |                 future = executor.submit(fetch_page_data, page) | ||||||
|  |                 futures.append(future) | ||||||
|  |              | ||||||
|  |             # 等待所有任务完成 | ||||||
|  |             print(f"正在并行采集港股数据,使用 {max_workers} 个线程...") | ||||||
|  |             start_time = time.time() | ||||||
|  |              | ||||||
|  |             completed_count = 0 | ||||||
|  |             for future in as_completed(futures): | ||||||
|  |                 completed_count += 1 | ||||||
|  |                 try: | ||||||
|  |                     result = future.result() | ||||||
|  |                     if result > 0: | ||||||
|  |                         print(f"进度: {completed_count}/{len(futures)} 页完成") | ||||||
|  |                 except Exception as e: | ||||||
|  |                     print(f"采集任务异常:{e}") | ||||||
|  |              | ||||||
|  |             end_time = time.time() | ||||||
|  |             print(f"港股数据采集完成,耗时: {end_time - start_time:.2f} 秒") | ||||||
|  |              | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         print(f"请求港股数据时发生异常:{e}") |         print(f"获取港股总页数失败:{e}") | ||||||
|         return |         return pd.DataFrame() | ||||||
| 
 | 
 | ||||||
|     data = response.json() |  | ||||||
|     total_count = data['data']['count'] |  | ||||||
|     total_pages = (total_count // page_size) + 1 |  | ||||||
| 
 |  | ||||||
|     print(f"开始采集港股数据,共 {total_pages} 页,{total_count} 条记录") |  | ||||||
| 
 |  | ||||||
|     # 循环获取所有页面的数据 |  | ||||||
|     for page in range(1, total_pages + 1): |  | ||||||
|         params['page'] = page |  | ||||||
|         try: |  | ||||||
|             response = proxy_manager.request_with_proxy('get', base_url, headers=headers, params=params) |  | ||||||
|             if response.status_code == 200: |  | ||||||
|                 data = response.json() |  | ||||||
|                 all_data.extend(data['data']['list']) |  | ||||||
|                 print(f"成功采集港股第 {page}/{total_pages} 页数据") |  | ||||||
|             else: |  | ||||||
|                 print(f"请求港股数据第 {page} 页失败,状态码:{response.status_code}") |  | ||||||
|         except Exception as e: |  | ||||||
|             print(f"请求港股数据第 {page} 页时发生异常:{e}") |  | ||||||
|             continue |  | ||||||
|      |  | ||||||
|     # 转换为 DataFrame |     # 转换为 DataFrame | ||||||
|     df = pd.DataFrame(all_data) |     df = pd.DataFrame(all_data) | ||||||
| 
 | 
 | ||||||
|     if not df.empty: |     if not df.empty: | ||||||
|         df['fetch_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |         df['fetch_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') | ||||||
|  |          | ||||||
|         # 存入Redis,使用hash结构,key为symbol,value为json字符串 |         # 存入Redis,使用hash结构,key为symbol,value为json字符串 | ||||||
|         r = get_redis_conn() |         r = get_redis_conn() | ||||||
|         pipe = r.pipeline() |         pipe = r.pipeline() | ||||||
|  |          | ||||||
|         # 先清空旧数据 |         # 先清空旧数据 | ||||||
|         r.delete(REDIS_KEY) |         r.delete(REDIS_KEY) | ||||||
|  |          | ||||||
|  |         print(f"正在将 {len(df)} 条港股记录写入Redis...") | ||||||
|  |          | ||||||
|         for _, row in df.iterrows(): |         for _, row in df.iterrows(): | ||||||
|             symbol = row.get('symbol') |             symbol = row.get('symbol') | ||||||
|             if not symbol: |             if not symbol: | ||||||
|  | @ -115,10 +181,15 @@ def fetch_and_store_hk_stock_data(page_size=90): | ||||||
|             # 只保留必要字段,也可直接存row.to_dict() |             # 只保留必要字段,也可直接存row.to_dict() | ||||||
|             value = row.to_dict() |             value = row.to_dict() | ||||||
|             pipe.hset(REDIS_KEY, symbol, json.dumps(value, ensure_ascii=False)) |             pipe.hset(REDIS_KEY, symbol, json.dumps(value, ensure_ascii=False)) | ||||||
|  |          | ||||||
|         pipe.execute() |         pipe.execute() | ||||||
|         print(f"成功将港股数据写入Redis哈希 {REDIS_KEY},共{len(df)}条记录。") |         print(f"成功将港股数据写入Redis哈希 {REDIS_KEY},共{len(df)}条记录。") | ||||||
|  |          | ||||||
|  |         # 返回DataFrame供其他脚本使用 | ||||||
|  |         return df | ||||||
|     else: |     else: | ||||||
|         print("未获取到任何港股数据。") |         print("未获取到任何港股数据。") | ||||||
|  |         return pd.DataFrame() | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def format_hk_stock_code(stock_code): | def format_hk_stock_code(stock_code): | ||||||
|  | @ -175,7 +246,7 @@ def get_hk_stock_realtime_info_from_redis(stock_code): | ||||||
|     result["crawlDate"] = data.get("fetch_time") |     result["crawlDate"] = data.get("fetch_time") | ||||||
|     result["marketValue"] = data.get("market_capital") |     result["marketValue"] = data.get("market_capital") | ||||||
|     result["maxPrice"] = data.get("high") if "high" in data else data.get("high52w") |     result["maxPrice"] = data.get("high") if "high" in data else data.get("high52w") | ||||||
|     result["minPrice"] = data.get("low") if "low" in data else data.get("low52w") |     result["minPrice"] = data.get("low") if "low" in data else data.get("high52w") | ||||||
|     result["nowPrice"] = data.get("current") |     result["nowPrice"] = data.get("current") | ||||||
|     result["pbRate"] = data.get("pb") |     result["pbRate"] = data.get("pb") | ||||||
|     result["rangeRiseAndFall"] = data.get("percent") |     result["rangeRiseAndFall"] = data.get("percent") | ||||||
|  | @ -193,5 +264,35 @@ def get_hk_stock_realtime_info_from_redis(stock_code): | ||||||
|     return result |     return result | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def fetch_and_store_hk_stock_data_optimized(page_size=90, max_workers=15, use_proxy=False): | ||||||
|  |     """ | ||||||
|  |     优化版本的港股批量采集函数,支持更灵活的配置 | ||||||
|  |      | ||||||
|  |     :param page_size: 每页采集数量 | ||||||
|  |     :param max_workers: 线程池最大工作线程数(建议10-20之间) | ||||||
|  |     :param use_proxy: 是否使用代理(默认False) | ||||||
|  |     """ | ||||||
|  |     print(f"开始批量采集港股数据...") | ||||||
|  |     print(f"配置: 每页 {page_size} 条记录,最大线程数 {max_workers}") | ||||||
|  |     print(f"代理模式: {'启用' if use_proxy else '禁用'}") | ||||||
|  |     print(f"预计采集: 港股所有股票数据") | ||||||
|  |     print("-" * 50) | ||||||
|  |      | ||||||
|  |     try: | ||||||
|  |         result = fetch_and_store_hk_stock_data(page_size, max_workers, use_proxy) | ||||||
|  |         if not result.empty: | ||||||
|  |             print(f"港股采集完成!共获取 {len(result)} 只股票的数据") | ||||||
|  |             print(f"数据已保存到Redis键: {REDIS_KEY}") | ||||||
|  |         else: | ||||||
|  |             print("港股采集完成,但未获取到数据") | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"港股采集过程中发生错误: {e}") | ||||||
|  |         return None | ||||||
|  |      | ||||||
|  |     return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| if __name__ == '__main__': | if __name__ == '__main__': | ||||||
|     fetch_and_store_hk_stock_data()  |     # 可以根据需要调整参数 | ||||||
|  |     # fetch_and_store_hk_stock_data_optimized(page_size=100, max_workers=15, use_proxy=True) | ||||||
|  |     fetch_and_store_hk_stock_data_optimized(use_proxy=False)  # 默认不使用代理  | ||||||
|  | @ -339,4 +339,37 @@ class EnhancedProxyManager: | ||||||
|             'manual_proxies': self.redis_conn.hlen(manual_key), |             'manual_proxies': self.redis_conn.hlen(manual_key), | ||||||
|             'auto_refresh': self.auto_refresh, |             'auto_refresh': self.auto_refresh, | ||||||
|             'last_update': datetime.now().strftime("%Y-%m-%d %H:%M:%S") |             'last_update': datetime.now().strftime("%Y-%m-%d %H:%M:%S") | ||||||
|         } |         } | ||||||
|  |      | ||||||
|  |     def get_multiple_proxies(self, count: int = 5) -> List[Dict]: | ||||||
|  |         """ | ||||||
|  |         获取多个代理,用于并发请求 | ||||||
|  |          | ||||||
|  |         :param count: 需要的代理数量 | ||||||
|  |         :return: 代理列表 | ||||||
|  |         """ | ||||||
|  |         proxies = [] | ||||||
|  |         manual_key = self._get_redis_key('manual') | ||||||
|  |          | ||||||
|  |         # 获取手动代理池中的所有代理 | ||||||
|  |         manual_proxies = self.redis_conn.hgetall(manual_key) | ||||||
|  |         active_proxies = [] | ||||||
|  |          | ||||||
|  |         for proxy_json in manual_proxies.values(): | ||||||
|  |             proxy = json.loads(proxy_json) | ||||||
|  |             if proxy.get('status') == 'active': | ||||||
|  |                 active_proxies.append(proxy) | ||||||
|  |          | ||||||
|  |         if not active_proxies: | ||||||
|  |             return [] | ||||||
|  |          | ||||||
|  |         # 随机选择指定数量的代理 | ||||||
|  |         selected_count = min(count, len(active_proxies)) | ||||||
|  |         selected_proxies = random.sample(active_proxies, selected_count) | ||||||
|  |          | ||||||
|  |         # 为每个代理添加Redis键信息 | ||||||
|  |         for proxy in selected_proxies: | ||||||
|  |             proxy['_redis_key'] = self._get_redis_key(proxy['source']) | ||||||
|  |             proxies.append(proxy) | ||||||
|  |          | ||||||
|  |         return proxies | ||||||
|  | @ -11,7 +11,7 @@ XUEQIU_HEADERS = { | ||||||
|     'Accept-Encoding': 'gzip, deflate, br, zstd', |     'Accept-Encoding': 'gzip, deflate, br, zstd', | ||||||
|     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', |     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||||
|     'Client-Version': 'v2.44.75', |     'Client-Version': 'v2.44.75', | ||||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; xq_a_token=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xqat=ada154d4707b8d3f8aa521ff0c960aa7f81cbf9e; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzU2MDAyNjgyLCJjdG0iOjE3NTM0MTA2ODI0MTQsImNpZCI6ImQ5ZDBuNEFadXAifQ.AlnzQSY7oGKGABfaQcFLg0lAJsDdvBMiwUbgpCMCBlbx6VZPKhzERxWiylQb4dFIyyECvRRJ73SbO9cD46fAqgzOgTxArNHtTKD4lQapTnyb11diDADnpb_nzzaRr4k_BYQRKXWtcJxdUMzde2WLy-eAkSf76QkXmKrwS3kvRm5gfqhdye44whw5XMEGoZ_lXHzGLWGz_PludHZp6W3v-wwZc_0wLU6cTb_KdrwWUWT_8jw5JHXnJEmuZmQI8QWf60DtiHIYCYXarxv8XtyHK7lLKhIAa3C2QmGWw5wv2HGz4I5DPqm2uMPKumgkQxycfAk56-RWviLZ8LAPF-XcbA; xq_r_token=92527e51353f90ba14d5fd16581e5a7a2780baa2; acw_tc=1a0c655917546366986673411e68d25d3c69c1719d6d1d6283c7271cc1529f; is_overseas=0; Hm_lvt_1db88642e346389874251b5a1eded6e3=1754636834; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1754636837; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=Hvg6Ac+qmPnDgzOvFuCePWwm7reK8TPoE9ayL8cyLnFg+Jhg1RJO2WnkeH2T8Q18+iV9bDh+UAq222GxdelHBg%3D%3D; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuqbKOOQYMxPsMKjqDsqze4GzDiLPGhDBWAFdYjdqN4NCtAoqzWWF2ruqe8bOZqKKFS96SM6sXUGQKhexGLDY=DCuXiieGGU4GwDGoD34DiDDpLD03Db4D_nWrD7ORQMluokjeDQ4GyDiUk3ObDm4DfDDLorA6osQ4DGqDSFcyTxD3DfRb4DDN4CIDu_mDDbObt5jcbUx7OBCGxIeDMixGXzGC4InyRNvDrgjMXvzEKH1aDtqD9_au4XxKdr3NEAEP4KGGpC0inpge_5neOQDqix1oeee4eQvxQ5O7Gv0DOGDz0G4ix_jwP_RUWjiihW9PeGAShXZ=E/ZND6q3mi40weUmXjmvYIzSQzWDW9wsemhYedCrwihQYbKYvWRD3YD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuqbKOOQYMxPsMKe4DWhzmxhTKRDjR_xWs_DDs6KmhfHjRKnZkBxNA3TIO4Arip5wU2kO0SwUfkEzryfSk6Rzud3ARD49fiKFd344obYvCv1lxYhY3qdzQe3vWD', |     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; _c_WBKFRo=dsWgHR8i8KGPbIyhFlN51PHOzVuuNytvUAFppfkD; _nb_ioWEgULi=; Hm_lvt_1db88642e346389874251b5a1eded6e3=1754636834; xq_a_token=4ea8af8f9cb5850af2ba654c5255cbf6bf797b39; xqat=4ea8af8f9cb5850af2ba654c5255cbf6bf797b39; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzU3NjM3OTA4LCJjdG0iOjE3NTUwNDU5MDg0ODksImNpZCI6ImQ5ZDBuNEFadXAifQ.jAeKlW2r1xRuyoZ3cuy2rTgfSoW_79wGJeuup7I7sZMSH5QeRDJrGx5JWXO4373YRpNW0qnAXR51Ygd8Plmko1u99iN8LifGzyMtblXDPgs17aS0zyHr6cMAsURU984wCXkmZxdvRMCHdevc8XWNHnuqeGfQNSgBSdO6Zv7Xc5-t965TJba96UOsNBpv2GghV9B2mcrUQyW3edi9kRAN_Fxmx5M1Iri4Yfppcaj-VSZYkdZtUpizrN5BbVYujcnQjj4kceUYYAl3Ccs273KVNSMFKpHMIOJcMJATY6PRgLvbEu8_ttIfBnbG4mmZ71bU7RXigleXIj1qhcDL2rDzQQ; xq_r_token=2b5db3e3897cb3e46b8fa2fa384471b334ec59cb; acw_tc=ac11000117550489614555169ef3ec63ec008e1bfba0fe5321bc8a30c2deb8; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1755050072; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=kE/XuROkIJ4APDhfxUYOb9lRiDFNJT8KxiXYwJAuoCeNlkaxlcytBSuiCXGjqxhydALLguC/FB4qIXfLut408Q%3D%3D; ssxmod_itna=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuuqxGeR0qxPehAjqDsqze4GzDiLPGhDBWAFdYjw7ivq8RG5pxMBrGHuhLMEzoiqds=iCpxusTh1K/Zjw=KmoeK4xGLDY=DCTKq1QeD4S3Dt4DIDAYDDxDWU4DLDYoDY3nYxGP=xpWTcmRbD0YDzqDgUEe=xi3DA4DjnehqYiTdwDDBDGtO=9aDG4GfSmDD0wDLoGQQoDGWnCneE6mkiFIr6TTDjqPD/Shc59791vGW56CM9zo3paFDtqD90aAFn=GrvFaE_n93e4F4qibH7GYziTmrzt4xmrKi44mBDmAQQ0TKe4Bxq3DPQDhtTH7OY8FYS_Qqx4Gn/lHDcnDd7YerPCYC70bYbC42q3i8WTx3e8/rijIosDPRGrE0WdoqzGh_YPeD; ssxmod_itna2=1-eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0P6Dw1PtDCuuqxGeR0qxPehAe4DWhYeRonANsR7vNG4g2BxpQTxTiD', | ||||||
|     'Referer': 'https://weibo.com/u/7735765253', |     'Referer': 'https://weibo.com/u/7735765253', | ||||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', |     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||||
|     'Sec-Ch-Ua-Mobile': '?0', |     'Sec-Ch-Ua-Mobile': '?0', | ||||||
|  |  | ||||||
|  | @ -184,55 +184,6 @@ def analyze_price_changes(db_url): | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         print("分析数据时发生错误: {}".format(str(e))) |         print("分析数据时发生错误: {}".format(str(e))) | ||||||
| 
 | 
 | ||||||
| def get_previous_sector_ranks(engine): |  | ||||||
|     """获取上一次的板块排名""" |  | ||||||
|     try: |  | ||||||
|         query = text(""" |  | ||||||
|             SELECT sector_name, rank_num |  | ||||||
|             FROM sector_performance |  | ||||||
|             WHERE DATE(add_time) = CURDATE() |  | ||||||
|             ORDER BY add_time DESC |  | ||||||
|             LIMIT 1 |  | ||||||
|         """) |  | ||||||
|          |  | ||||||
|         result = pd.read_sql_query(query, engine) |  | ||||||
|         if result.empty: |  | ||||||
|             return {} |  | ||||||
|              |  | ||||||
|         return dict(zip(result['sector_name'], result['rank_num'])) |  | ||||||
|     except Exception as e: |  | ||||||
|         print("获取上一次排名数据时发生错误: {}".format(str(e))) |  | ||||||
|         return {} |  | ||||||
| 
 |  | ||||||
| def calculate_rank_change(row, previous_ranks): |  | ||||||
|     """计算排名变化""" |  | ||||||
|     previous_rank = previous_ranks.get(row['sector_name']) |  | ||||||
|     if previous_rank is None: |  | ||||||
|         return 0 |  | ||||||
|     return previous_rank - row['rank_num'] |  | ||||||
| 
 |  | ||||||
| def get_cache_mark(): |  | ||||||
|     """获取当前时间对应的缓存标记""" |  | ||||||
|     current_minute = datetime.now().minute |  | ||||||
|     mark = (current_minute % 10) // 2 * 2 |  | ||||||
|     return "{}m".format(mark) |  | ||||||
| 
 |  | ||||||
| def save_sector_cache(engine, df_result, cache_mark): |  | ||||||
|     """保存板块数据到缓存表""" |  | ||||||
|     try: |  | ||||||
|         df_cache = df_result.copy() |  | ||||||
|         df_cache['cache_mark'] = cache_mark |  | ||||||
|          |  | ||||||
|         with engine.connect() as conn: |  | ||||||
|             delete_query = text("DELETE FROM sector_performance_cache WHERE cache_mark = :cache_mark") |  | ||||||
|             conn.execute(delete_query, {'cache_mark': cache_mark}) |  | ||||||
|             conn.commit() |  | ||||||
|          |  | ||||||
|         df_cache.to_sql('sector_performance_cache', con=engine, if_exists='append', index=False) |  | ||||||
|         print(f"缓存数据已保存,标记: {cache_mark}") |  | ||||||
|     except Exception as e: |  | ||||||
|         print("保存缓存数据时发生错误: {}".format(str(e))) |  | ||||||
| 
 |  | ||||||
| def main(db_url): | def main(db_url): | ||||||
|     """主函数""" |     """主函数""" | ||||||
|     engine = create_engine(db_url) |     engine = create_engine(db_url) | ||||||
|  |  | ||||||
|  | @ -1,62 +1,74 @@ | ||||||
| # coding:utf-8 | # coding:utf-8 | ||||||
| 
 | 
 | ||||||
| import requests |  | ||||||
| import pandas as pd | import pandas as pd | ||||||
| from sqlalchemy import create_engine, text | from sqlalchemy import create_engine, text | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
| from config import XUEQIU_HEADERS | import redis | ||||||
|  | import json | ||||||
|  | import sys | ||||||
|  | import os | ||||||
| 
 | 
 | ||||||
| def fetch_and_store_stock_data(db_url, table_name='stock_changes', page_size=90): | # 添加项目根目录到路径,便于导入config | ||||||
|     """获取雪球数据并保存到数据库""" | project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | ||||||
|     base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' | sys.path.append(project_root) | ||||||
|     types = ['sha', 'sza', 'kcb']  # 数据类型 |  | ||||||
|     headers = XUEQIU_HEADERS |  | ||||||
| 
 | 
 | ||||||
|     all_data = [] | # 读取配置 | ||||||
|  | try: | ||||||
|  |     from src.valuation_analysis.config import REDIS_CONFIG | ||||||
|  | except ImportError: | ||||||
|  |     REDIS_CONFIG = { | ||||||
|  |         'host': 'localhost', | ||||||
|  |         'port': 6379, | ||||||
|  |         'db': 0, | ||||||
|  |         'password': None | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     for stock_type in types: | REDIS_KEY = 'xq_stock_changes_latest'  # Redis中存放行情的主键 | ||||||
|         params = { |  | ||||||
|             'page': 1, |  | ||||||
|             'size': page_size, |  | ||||||
|             'order': 'desc', |  | ||||||
|             'order_by': 'percent', |  | ||||||
|             'market': 'CN', |  | ||||||
|             'type': stock_type |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         # 初次请求以获取总页数 | def get_redis_conn(): | ||||||
|         response = requests.get(base_url, headers=headers, params=params) |     """获取Redis连接""" | ||||||
|         if response.status_code != 200: |     pool = redis.ConnectionPool( | ||||||
|             print(f"请求 {stock_type} 数据失败,状态码:{response.status_code}") |         host=REDIS_CONFIG['host'], | ||||||
|             continue |         port=REDIS_CONFIG['port'], | ||||||
|  |         db=REDIS_CONFIG.get('db', 0), | ||||||
|  |         password=REDIS_CONFIG.get('password', None), | ||||||
|  |         decode_responses=True | ||||||
|  |     ) | ||||||
|  |     return redis.Redis(connection_pool=pool) | ||||||
| 
 | 
 | ||||||
|         data = response.json() | def fetch_stock_data_from_redis(): | ||||||
|         total_count = data['data']['count'] |     """从Redis获取股票数据""" | ||||||
|         total_pages = (total_count // page_size) + 1 |     try: | ||||||
| 
 |         r = get_redis_conn() | ||||||
|         for page in range(1, total_pages + 1): |          | ||||||
|             params['page'] = page |         # 从Redis获取所有股票数据 | ||||||
|             response = requests.get(base_url, headers=headers, params=params) |         all_stock_data = r.hgetall(REDIS_KEY) | ||||||
|             if response.status_code == 200: |          | ||||||
|                 data = response.json() |         if not all_stock_data: | ||||||
|                 all_data.extend(data['data']['list']) |             print("Redis中没有找到股票数据") | ||||||
|             else: |             return pd.DataFrame() | ||||||
|                 print(f"请求 {stock_type} 数据第 {page} 页失败,状态码:{response.status_code}") |          | ||||||
|     # 转换为 DataFrame |         # 转换为DataFrame | ||||||
|     df = pd.DataFrame(all_data) |         stock_list = [] | ||||||
| 
 |         for symbol, value in all_stock_data.items(): | ||||||
|     if not df.empty: |             try: | ||||||
|         # 添加 id 列 |                 stock_data = json.loads(value) | ||||||
|         df['id'] = range(1, len(df) + 1) |                 stock_list.append(stock_data) | ||||||
| 
 |             except json.JSONDecodeError: | ||||||
|         # 创建数据库连接 |                 print(f"解析股票数据失败: {symbol}") | ||||||
|         engine = create_engine(db_url) |                 continue | ||||||
| 
 |          | ||||||
|         # 将数据写入数据库表 |         if stock_list: | ||||||
|         df.to_sql(table_name, con=engine, if_exists='replace', index=False) |             df = pd.DataFrame(stock_list) | ||||||
|         print(f"成功将数据写入数据库表 {table_name}。") |             print(f"从Redis成功获取 {len(df)} 条股票数据") | ||||||
|  |             return df | ||||||
|     else: |     else: | ||||||
|         print("未获取到任何数据。") |             print("Redis数据解析失败") | ||||||
|  |             return pd.DataFrame() | ||||||
|  |              | ||||||
|  |     except Exception as e: | ||||||
|  |         print(f"从Redis获取数据时发生错误: {str(e)}") | ||||||
|  |         return pd.DataFrame() | ||||||
| 
 | 
 | ||||||
| def get_cache_mark(): | def get_cache_mark(): | ||||||
|     """获取当前时间对应的缓存标记(0m, 2m, 4m, 6m, 8m)""" |     """获取当前时间对应的缓存标记(0m, 2m, 4m, 6m, 8m)""" | ||||||
|  | @ -164,21 +176,43 @@ def get_high_performance_stocks(db_url): | ||||||
|         # 确保缓存表存在 |         # 确保缓存表存在 | ||||||
|         ensure_cache_table_exists(engine) |         ensure_cache_table_exists(engine) | ||||||
| 
 | 
 | ||||||
|         # 先获取最新数据 |         # 从Redis获取股票数据 | ||||||
|         fetch_and_store_stock_data(db_url) |         print("从Redis获取股票数据...") | ||||||
|  |         stock_changes_df = fetch_stock_data_from_redis() | ||||||
|  |          | ||||||
|  |         if stock_changes_df.empty: | ||||||
|  |             print("Redis数据获取失败,无法进行分析") | ||||||
|  |             return [] | ||||||
| 
 | 
 | ||||||
|         # 读取数据 |         print(f"Redis数据获取成功,共 {len(stock_changes_df)} 条记录") | ||||||
|         stock_changes_df = pd.read_sql_table('stock_changes', con=engine) |          | ||||||
|  |         # 读取板块信息 | ||||||
|         gp_gnbk_df = pd.read_sql_table('gp_gnbk', con=engine) |         gp_gnbk_df = pd.read_sql_table('gp_gnbk', con=engine) | ||||||
| 
 | 
 | ||||||
|         # 去掉 symbol 字段的前两个字符 |         # 处理symbol字段 - 根据Redis数据格式,symbol已经是完整格式(如SZ002916) | ||||||
|         stock_changes_df['symbol'] = stock_changes_df['symbol'].str[2:] |         if 'symbol' in stock_changes_df.columns: | ||||||
|  |             # 保持原始格式用于关联板块表,因为板块表使用的是完整格式(如SZ000973) | ||||||
|  |             stock_changes_df['symbol_clean'] = stock_changes_df['symbol'] | ||||||
|  |         else: | ||||||
|  |             print("股票数据中缺少symbol字段") | ||||||
|  |             return [] | ||||||
| 
 | 
 | ||||||
|         # 筛选涨幅超过 1.5% 的股票 |         # 筛选涨幅超过 1.5% 的股票 | ||||||
|         high_performance_stocks = stock_changes_df[stock_changes_df['percent'] > 1.5] |         high_performance_stocks = stock_changes_df[stock_changes_df['percent'] > 1.5] | ||||||
|  |         print(f"筛选结果: {len(high_performance_stocks)} 只股票涨幅超过1.5%") | ||||||
|  |          | ||||||
|  |         if len(high_performance_stocks) == 0: | ||||||
|  |             print("没有股票涨幅超过1.5%,尝试降低阈值到0.5%...") | ||||||
|  |             high_performance_stocks = stock_changes_df[stock_changes_df['percent'] > 0.5] | ||||||
|  |             print(f"筛选结果: {len(high_performance_stocks)} 只股票涨幅超过0.5%") | ||||||
|  |              | ||||||
|  |             if len(high_performance_stocks) == 0: | ||||||
|  |                 print("仍然没有股票涨幅超过0.5%,尝试获取所有上涨股票...") | ||||||
|  |                 high_performance_stocks = stock_changes_df[stock_changes_df['percent'] > 0] | ||||||
|  |                 print(f"筛选结果: {len(high_performance_stocks)} 只股票上涨") | ||||||
| 
 | 
 | ||||||
|         # 关联两个表,获取 bk_name |         # 关联两个表,获取 bk_name | ||||||
|         merged_df = high_performance_stocks.merge(gp_gnbk_df, left_on='symbol', right_on='gp_code') |         merged_df = high_performance_stocks.merge(gp_gnbk_df, left_on='symbol_clean', right_on='gp_code') | ||||||
| 
 | 
 | ||||||
|         # 统计每个 bk_name 的数量 |         # 统计每个 bk_name 的数量 | ||||||
|         total_counts = gp_gnbk_df['bk_name'].value_counts() |         total_counts = gp_gnbk_df['bk_name'].value_counts() | ||||||
|  | @ -215,6 +249,7 @@ def get_high_performance_stocks(db_url): | ||||||
|         save_sector_cache(engine, df_result, cache_mark) |         save_sector_cache(engine, df_result, cache_mark) | ||||||
| 
 | 
 | ||||||
|         # 输出结果 |         # 输出结果 | ||||||
|  |         print("\n板块分析结果:") | ||||||
|         for _, row in df_result.iterrows(): |         for _, row in df_result.iterrows(): | ||||||
|             print("板块名称: {}, 上涨家数: {}, 总数: {}, 比重: {:.2%}, 排名: {}, 排名变化: {}".format( |             print("板块名称: {}, 上涨家数: {}, 总数: {}, 比重: {:.2%}, 排名: {}, 排名变化: {}".format( | ||||||
|                 row['sector_name'], row['up_count'], row['total_count'], |                 row['sector_name'], row['up_count'], row['total_count'], | ||||||
|  | @ -251,21 +286,31 @@ def get_top_industries_and_stocks(db_url, top_start=1, top_end=10): | ||||||
|         # 提取指定范围的行名称 |         # 提取指定范围的行名称 | ||||||
|         top_industry_names = [industry[0] for industry in top_industries[top_start-1:top_end]] |         top_industry_names = [industry[0] for industry in top_industries[top_start-1:top_end]] | ||||||
| 
 | 
 | ||||||
|         # 读取数据 |         # 从Redis获取股票数据 | ||||||
|         stock_changes_df = pd.read_sql_table('stock_changes', con=engine) |         stock_changes_df = fetch_stock_data_from_redis() | ||||||
|  |         if stock_changes_df.empty: | ||||||
|  |             print("无法获取股票数据") | ||||||
|  |             return | ||||||
|  |              | ||||||
|  |         # 读取板块信息 | ||||||
|         gp_gnbk_df = pd.read_sql_table('gp_gnbk', con=engine) |         gp_gnbk_df = pd.read_sql_table('gp_gnbk', con=engine) | ||||||
| 
 | 
 | ||||||
|         # 去掉 symbol 字段的前两个字符 |         # 处理symbol字段 | ||||||
|         stock_changes_df['symbol'] = stock_changes_df['symbol'].str[2:] |         if 'symbol' in stock_changes_df.columns: | ||||||
|  |             # 保持原始格式用于关联板块表 | ||||||
|  |             stock_changes_df['symbol_clean'] = stock_changes_df['symbol'] | ||||||
|  |         else: | ||||||
|  |             print("股票数据中缺少symbol字段") | ||||||
|  |             return | ||||||
| 
 | 
 | ||||||
|         # 关联两个表,获取 bk_name 和 gp_name |         # 关联两个表,获取 bk_name 和 gp_name | ||||||
|         merged_df = stock_changes_df.merge(gp_gnbk_df, left_on='symbol', right_on='gp_code') |         merged_df = stock_changes_df.merge(gp_gnbk_df, left_on='symbol_clean', right_on='gp_code') | ||||||
| 
 | 
 | ||||||
|         # 筛选指定范围的行业的股票 |         # 筛选指定范围的行业的股票 | ||||||
|         filtered_df = merged_df[merged_df['bk_name'].isin(top_industry_names)] |         filtered_df = merged_df[merged_df['bk_name'].isin(top_industry_names)] | ||||||
| 
 | 
 | ||||||
|         # 统计每只股票命中行业的数量 |         # 统计每只股票命中行业的数量 | ||||||
|         stock_industry_counts = filtered_df.groupby(['symbol', 'gp_name'])['bk_name'].nunique().sort_values(ascending=False).head(10) |         stock_industry_counts = filtered_df.groupby(['symbol', 'name'])['bk_name'].nunique().sort_values(ascending=False).head(10) | ||||||
| 
 | 
 | ||||||
|         # 获取每只股票的命中行业数量 |         # 获取每只股票的命中行业数量 | ||||||
|         stock_industry_list = stock_industry_counts.reset_index() |         stock_industry_list = stock_industry_counts.reset_index() | ||||||
|  | @ -332,5 +377,17 @@ if __name__ == "__main__": | ||||||
|     # 清理历史数据 |     # 清理历史数据 | ||||||
|     clean_historical_data(engine) |     clean_historical_data(engine) | ||||||
|      |      | ||||||
|     # 执行主要分析 |     # 执行主要分析 - 完全使用Redis数据源 | ||||||
|     get_top_industries_and_stocks(db_url, 1, 10) |     print("=" * 60) | ||||||
|  |     print("板块分析脚本启动 - Redis数据源版本") | ||||||
|  |     print("=" * 60) | ||||||
|  |     print("使用Redis作为数据源") | ||||||
|  |     print("确保batch_stock_price_collector.py脚本正在运行") | ||||||
|  |     print("-" * 60) | ||||||
|  |      | ||||||
|  |     # 执行分析 | ||||||
|  |     get_top_industries_and_stocks(db_url, 1, 10) | ||||||
|  |      | ||||||
|  |     print("=" * 60) | ||||||
|  |     print("板块分析完成") | ||||||
|  |     print("=" * 60) | ||||||
|  | @ -172,6 +172,13 @@ $(function() { | ||||||
|         // 隐藏重置按钮
 |         // 隐藏重置按钮
 | ||||||
|         $('#resetViewBtn').hide(); |         $('#resetViewBtn').hide(); | ||||||
|          |          | ||||||
|  |         // 静默请求接口,不传递行业参数
 | ||||||
|  |         $.get('https://spb.bmbs.tech/api/dify/webSelectStockIndustry') | ||||||
|  |             .fail(function(xhr, status, error) { | ||||||
|  |                 // 静默处理错误,不显示给用户
 | ||||||
|  |                 console.log('返回默认视图通知接口调用失败:', error); | ||||||
|  |             }); | ||||||
|  |          | ||||||
|         // 恢复第二行的原始布局
 |         // 恢复第二行的原始布局
 | ||||||
|         const rowContainer = document.querySelector('.row.d-flex2'); |         const rowContainer = document.querySelector('.row.d-flex2'); | ||||||
|         rowContainer.innerHTML = ` |         rowContainer.innerHTML = ` | ||||||
|  | @ -223,6 +230,9 @@ $(function() { | ||||||
|                 element.remove(); |                 element.remove(); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  |          | ||||||
|  |         // 重新绑定持仓点击事件
 | ||||||
|  |         setTimeout(bindIndustryHoldingsClick, 1000); // 延迟1秒绑定,确保数据加载完成
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     // 加载行业详情函数
 |     // 加载行业详情函数
 | ||||||
|  | @ -399,10 +409,10 @@ $(function() { | ||||||
|                      |                      | ||||||
|                     <!-- 持仓和因子的左右布局 --> |                     <!-- 持仓和因子的左右布局 --> | ||||||
|                     <div class="factor-holding-row"> |                     <div class="factor-holding-row"> | ||||||
|                         <!-- 持仓数据 --> |                     <!-- 持仓数据 --> | ||||||
|                         <div class="holding-section"> |                         <div class="holding-section"> | ||||||
|                             <h5>持仓情况</h5> |                         <h5>持仓情况</h5> | ||||||
|                             <div class="holding-details"> |                         <div class="holding-details"> | ||||||
|             `;
 |             `;
 | ||||||
|              |              | ||||||
|             if (stockHoldings.length > 0) { |             if (stockHoldings.length > 0) { | ||||||
|  | @ -431,9 +441,21 @@ $(function() { | ||||||
|                             <div class="factor-details"> |                             <div class="factor-details"> | ||||||
|             `;
 |             `;
 | ||||||
|              |              | ||||||
|             if (factorInfo && factorInfo.details) { |             if (factorInfo && factorInfo.factors_details) { | ||||||
|                 Object.entries(factorInfo.details).forEach(([factor, value]) => { |                 factorInfo.factors_details.forEach((factorDetail) => { | ||||||
|                     html += `<p><strong>${factor}:</strong> ${value}</p>`; |                     const factor = factorDetail.factor; | ||||||
|  |                     const detail = factorDetail.detail; | ||||||
|  |                     const signal = factorDetail.signal; | ||||||
|  |                      | ||||||
|  |                     // 根据signal显示箭头
 | ||||||
|  |                     let arrowIcon = ''; | ||||||
|  |                     if (signal === '买') { | ||||||
|  |                         arrowIcon = '<span class="factor-arrow buy">⬆</span>'; | ||||||
|  |                     } else if (signal === '卖') { | ||||||
|  |                         arrowIcon = '<span class="factor-arrow sell">⬇</span>'; | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     html += `<p>${arrowIcon}<strong>${factor}:</strong> ${detail}</p>`; | ||||||
|                 }); |                 }); | ||||||
|             } else { |             } else { | ||||||
|                 html += '<p style="color: #999;">暂无因子数据</p>'; |                 html += '<p style="color: #999;">暂无因子数据</p>'; | ||||||
|  | @ -485,10 +507,6 @@ $(function() { | ||||||
|         container.innerHTML = html; |         container.innerHTML = html; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     // 这个函数已经不再使用,删除
 |  | ||||||
|      |  | ||||||
|     // 这个函数已经不再使用,删除
 |  | ||||||
|      |  | ||||||
|     function convertStockCode(stockCode) { |     function convertStockCode(stockCode) { | ||||||
|         // 将 600584.SH 格式转换为 SH600584 格式
 |         // 将 600584.SH 格式转换为 SH600584 格式
 | ||||||
|         const parts = stockCode.split('.'); |         const parts = stockCode.split('.'); | ||||||
|  | @ -702,13 +720,16 @@ $(function() { | ||||||
|     function bindIndustryHoldingsClick() { |     function bindIndustryHoldingsClick() { | ||||||
|         // 为每个持仓容器添加点击事件
 |         // 为每个持仓容器添加点击事件
 | ||||||
|         const holdingsContainers = ['holdings_xjfz', 'holdings_xp', 'holdings_xfdz', 'holdings_jqr']; |         const holdingsContainers = ['holdings_xjfz', 'holdings_xp', 'holdings_xfdz', 'holdings_jqr']; | ||||||
|         const industries = ['半导体', '自动化设备', '军工电子', '消费电子']; // 默认行业,实际会根据持仓动态更新
 |  | ||||||
|          |          | ||||||
|         holdingsContainers.forEach((containerId, index) => { |         holdingsContainers.forEach((containerId, index) => { | ||||||
|             const container = document.getElementById(containerId); |             const container = document.getElementById(containerId); | ||||||
|             if (container) { |             if (container) { | ||||||
|                 container.style.cursor = 'pointer'; |                 // 移除之前的事件监听器,避免重复绑定
 | ||||||
|                 container.addEventListener('click', function() { |                 container.replaceWith(container.cloneNode(true)); | ||||||
|  |                 const newContainer = document.getElementById(containerId); | ||||||
|  |                  | ||||||
|  |                 newContainer.style.cursor = 'pointer'; | ||||||
|  |                 newContainer.addEventListener('click', function() { | ||||||
|                     // 获取当前显示的行业名称
 |                     // 获取当前显示的行业名称
 | ||||||
|                     const industryTitle = this.parentElement.querySelector('.chart-title'); |                     const industryTitle = this.parentElement.querySelector('.chart-title'); | ||||||
|                     if (industryTitle) { |                     if (industryTitle) { | ||||||
|  | @ -752,7 +773,7 @@ $(function() { | ||||||
|             html = '<div class="notice-item" style="text-align: center; color: #999; padding: 20px;">暂无重要提醒</div>'; |             html = '<div class="notice-item" style="text-align: center; color: #999; padding: 20px;">暂无重要提醒</div>'; | ||||||
|         } else { |         } else { | ||||||
|             // 只显示一次内容,不再重复
 |             // 只显示一次内容,不再重复
 | ||||||
|             notices.forEach(notice => { |             notices.forEach((notice, index) => { | ||||||
|                 html += `<div class="notice-item">${notice}</div>`; |                 html += `<div class="notice-item">${notice}</div>`; | ||||||
|             }); |             }); | ||||||
|         } |         } | ||||||
|  | @ -1120,34 +1141,34 @@ $(function() { | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| // 弹窗相关函数 - 移到全局作用域
 | // 弹窗相关函数 - 移到全局作用域
 | ||||||
| function showNoticeModal() { |     function showNoticeModal() { | ||||||
|     const modal = document.getElementById('noticeModal'); |         const modal = document.getElementById('noticeModal'); | ||||||
|     const modalContent = document.getElementById('modalNoticeContent'); |         const modalContent = document.getElementById('modalNoticeContent'); | ||||||
|      |          | ||||||
|     // 获取当前所有提醒数据
 |         // 获取当前所有提醒数据
 | ||||||
|     const notices = getCurrentNotices(); |         const notices = getCurrentNotices(); | ||||||
|      |          | ||||||
|     // 生成弹窗内容
 |         // 生成弹窗内容
 | ||||||
|     let html = ''; |         let html = ''; | ||||||
|     notices.forEach(notice => { |         notices.forEach(notice => { | ||||||
|         html += `<div class="modal-notice-item">${notice}</div>`; |             html += `<div class="modal-notice-item">${notice}</div>`; | ||||||
|     }); |         }); | ||||||
|     modalContent.innerHTML = html; |         modalContent.innerHTML = html; | ||||||
|      |          | ||||||
|     // 显示弹窗
 |         // 显示弹窗
 | ||||||
|     modal.style.display = 'flex'; |         modal.style.display = 'flex'; | ||||||
|      |          | ||||||
|     // 阻止背景滚动
 |         // 阻止背景滚动
 | ||||||
|     document.body.style.overflow = 'hidden'; |         document.body.style.overflow = 'hidden'; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function closeNoticeModal() { |     function closeNoticeModal() { | ||||||
|     const modal = document.getElementById('noticeModal'); |         const modal = document.getElementById('noticeModal'); | ||||||
|     modal.style.display = 'none'; |         modal.style.display = 'none'; | ||||||
|      |          | ||||||
|     // 恢复背景滚动
 |         // 恢复背景滚动
 | ||||||
|     document.body.style.overflow = 'auto'; |         document.body.style.overflow = 'auto'; | ||||||
| } |     } | ||||||
| 
 | 
 | ||||||
| function closeIndustryHoldingsModal() { | function closeIndustryHoldingsModal() { | ||||||
|     const modal = document.getElementById('industryHoldingsModal'); |     const modal = document.getElementById('industryHoldingsModal'); | ||||||
|  | @ -1157,14 +1178,14 @@ function closeIndustryHoldingsModal() { | ||||||
|     document.body.style.overflow = 'auto'; |     document.body.style.overflow = 'auto'; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| function getCurrentNotices() { |     function getCurrentNotices() { | ||||||
|     // 从当前显示的提醒框中获取数据,或者使用默认数据
 |         // 从当前显示的提醒框中获取数据,或者使用默认数据
 | ||||||
|     const noticeItems = document.querySelectorAll('.notice-item'); |         const noticeItems = document.querySelectorAll('.notice-item'); | ||||||
|     if (noticeItems.length > 0) { |         if (noticeItems.length > 0) { | ||||||
|         return Array.from(noticeItems).map(item => item.textContent.trim()); |             return Array.from(noticeItems).map(item => item.textContent.trim()); | ||||||
|  |         } | ||||||
|  |         return getDefaultNotices(); | ||||||
|     } |     } | ||||||
|     return getDefaultNotices(); |  | ||||||
| } |  | ||||||
| 
 | 
 | ||||||
| // 页面加载完成后绑定事件
 | // 页面加载完成后绑定事件
 | ||||||
| $(function() { | $(function() { | ||||||
|  |  | ||||||
|  | @ -103,6 +103,14 @@ | ||||||
|             border-left: 3px solid #5470c6; |             border-left: 3px solid #5470c6; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         .holding-item:nth-child(odd) { | ||||||
|  |             background-color: #f8f9fa; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .holding-item:nth-child(even) { | ||||||
|  |             background-color: #ffffff; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         .holding-name { |         .holding-name { | ||||||
|             font-weight: bold; |             font-weight: bold; | ||||||
|             color: #333; |             color: #333; | ||||||
|  | @ -162,6 +170,20 @@ | ||||||
|             line-height: 1.4; |             line-height: 1.4; | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|  |         .notice-item:nth-child(odd) { | ||||||
|  |             background-color: #f8f9fa; | ||||||
|  |             padding: 8px 10px; | ||||||
|  |             margin: 0 -10px; | ||||||
|  |             border-radius: 4px; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .notice-item:nth-child(even) { | ||||||
|  |             background-color: #ffffff; | ||||||
|  |             padding: 8px 10px; | ||||||
|  |             margin: 0 -10px; | ||||||
|  |             border-radius: 4px; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         .notice-item:last-child { |         .notice-item:last-child { | ||||||
|             border-bottom: none; |             border-bottom: none; | ||||||
|         } |         } | ||||||
|  | @ -566,6 +588,22 @@ | ||||||
|             color: #34495e; |             color: #34495e; | ||||||
|         } |         } | ||||||
|          |          | ||||||
|  |         /* 因子箭头样式 */ | ||||||
|  |         .factor-arrow { | ||||||
|  |             display: inline-block; | ||||||
|  |             font-weight: bold; | ||||||
|  |             margin-right: 8px; | ||||||
|  |             font-size: 14px; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .factor-arrow.buy { | ||||||
|  |             color: #ff4444; | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         .factor-arrow.sell { | ||||||
|  |             color: #44cc44; | ||||||
|  |         } | ||||||
|  |          | ||||||
|         .holding-detail-item { |         .holding-detail-item { | ||||||
|             margin-bottom: 3px; |             margin-bottom: 3px; | ||||||
|             padding: 3px; |             padding: 3px; | ||||||
|  |  | ||||||
|  | @ -0,0 +1,313 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <title>个股指数对比分析</title> | ||||||
|  |     <!-- 引入 ECharts --> | ||||||
|  |     <script src="../static/js/echarts.min.js"></script> | ||||||
|  |     <style> | ||||||
|  |         .container { | ||||||
|  |             padding: 20px; | ||||||
|  |             font-family: Arial, sans-serif; | ||||||
|  |         } | ||||||
|  |         .input-group { | ||||||
|  |             margin-bottom: 20px; | ||||||
|  |         } | ||||||
|  |         .input-group label { | ||||||
|  |             display: inline-block; | ||||||
|  |             width: 100px; | ||||||
|  |             margin-right: 10px; | ||||||
|  |         } | ||||||
|  |         .input-group input, .input-group select { | ||||||
|  |             padding: 5px; | ||||||
|  |             margin-right: 10px; | ||||||
|  |         } | ||||||
|  |         .result-panel { | ||||||
|  |             background-color: #f5f5f5; | ||||||
|  |             padding: 15px; | ||||||
|  |             border-radius: 5px; | ||||||
|  |             margin-bottom: 20px; | ||||||
|  |         } | ||||||
|  |         .strength-indicator { | ||||||
|  |             font-size: 24px; | ||||||
|  |             font-weight: bold; | ||||||
|  |             color: #ff4d4f; | ||||||
|  |         } | ||||||
|  |         .charts-container { | ||||||
|  |             display: flex; | ||||||
|  |             gap: 20px; | ||||||
|  |         } | ||||||
|  |         .chart-item { | ||||||
|  |             flex: 1; | ||||||
|  |         } | ||||||
|  |     </style> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <div class="container"> | ||||||
|  |         <h1>个股指数对比分析</h1> | ||||||
|  |          | ||||||
|  |         <!-- 输入控制区 --> | ||||||
|  |         <div class="input-group"> | ||||||
|  |             <label>个股代码:</label> | ||||||
|  |             <input type="text" id="stockCode" placeholder="输入股票代码"> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="input-group"> | ||||||
|  |             <label>对比指数:</label> | ||||||
|  |             <select id="indexSelect"> | ||||||
|  |                 <option value="000001|1">000001 - 上证指数</option> | ||||||
|  |                 <option value="399001|0">399001 - 深证成指</option> | ||||||
|  |                 <option value="399006|0">399006 - 创业板指</option> | ||||||
|  |                 <option value="000688|1">000688 - 科创50指数</option> | ||||||
|  |             </select> | ||||||
|  |         </div> | ||||||
|  |          | ||||||
|  |         <div class="input-group"> | ||||||
|  |             <button onclick="fetchAndAnalyze()" style="padding: 10px 20px; background-color: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer;">开始分析</button> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 结果展示区 --> | ||||||
|  |         <div class="result-panel" id="resultPanel" style="display: none;"> | ||||||
|  |             <h3>逆市强度分析结果</h3> | ||||||
|  |             <div> | ||||||
|  |                 <span>逆市强度: </span> | ||||||
|  |                 <span class="strength-indicator" id="strengthValue">-</span> | ||||||
|  |             </div> | ||||||
|  |             <div style="margin-top: 10px; font-size: 14px; color: #666;"> | ||||||
|  |                 指数弱势次数: <span id="indexWeakCount">-</span> |  | ||||||
|  |                 个股逆市次数: <span id="stockStrongCount">-</span> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <!-- 图表展示区 --> | ||||||
|  |         <div class="charts-container"> | ||||||
|  |             <div class="chart-item"> | ||||||
|  |                 <h3>个股买卖方能量</h3> | ||||||
|  |                 <div id="stockChart" style="width: 100%; height: 400px;"></div> | ||||||
|  |             </div> | ||||||
|  |             <div class="chart-item"> | ||||||
|  |                 <h3>指数买卖方能量</h3> | ||||||
|  |                 <div id="indexChart" style="width: 100%; height: 400px;"></div> | ||||||
|  |             </div> | ||||||
|  |         </div> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <script> | ||||||
|  |         // 初始化图表 | ||||||
|  |         var stockChart = echarts.init(document.getElementById('stockChart')); | ||||||
|  |         var indexChart = echarts.init(document.getElementById('indexChart')); | ||||||
|  | 
 | ||||||
|  |         // 获取分时数据 | ||||||
|  |         async function fetchStockData(code, market) { | ||||||
|  |             var baseUrl = "http://push2ex.eastmoney.com/getStockFenShi?pagesize=6000&ut=7eea3edcaed734bea9cbfc24409ed989&dpt=wzfscj&pageindex=0&id=3007792&sort=1&ft=1&code=REPLACE_CODE&market=REPLACE_MARKET"; | ||||||
|  |             var url = baseUrl.replace("REPLACE_CODE", code).replace("REPLACE_MARKET", market); | ||||||
|  |              | ||||||
|  |             try { | ||||||
|  |                 const response = await fetch(url); | ||||||
|  |                 const jsonData = await response.json(); | ||||||
|  |                 return jsonData.data.data; | ||||||
|  |             } catch (error) { | ||||||
|  |                 console.log('Request failed for ' + code, error); | ||||||
|  |                 return null; | ||||||
|  |             } | ||||||
|  |         }3 | ||||||
|  | 
 | ||||||
|  |         // 处理数据:按分钟聚合买卖方数据 | ||||||
|  |         function processTradeData(data) { | ||||||
|  |             var buyData = {}; | ||||||
|  |             var sellData = {}; | ||||||
|  |              | ||||||
|  |             if (!data) return { buyData, sellData }; | ||||||
|  |              | ||||||
|  |             data.forEach(function(item) { | ||||||
|  |                 var minute = Math.floor(item.t / 100); | ||||||
|  |                  | ||||||
|  |                 if (item.bs === 1) { | ||||||
|  |                     // 卖方 | ||||||
|  |                     if (!sellData[minute]) sellData[minute] = 0; | ||||||
|  |                     sellData[minute] += item.v; | ||||||
|  |                 } else if (item.bs === 2) { | ||||||
|  |                     // 买方 | ||||||
|  |                     if (!buyData[minute]) buyData[minute] = 0; | ||||||
|  |                     buyData[minute] += item.v; | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             return { buyData, sellData }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 计算逆市强度 | ||||||
|  |         function calculateCounterTrendStrength(stockBuyData, stockSellData, indexBuyData, indexSellData) { | ||||||
|  |             var indexWeakCount = 0;  // 指数弱势次数(卖方 > 买方) | ||||||
|  |             var stockStrongCount = 0; // 个股逆市强势次数 | ||||||
|  |              | ||||||
|  |             // 获取所有时间点 | ||||||
|  |             var allMinutes = new Set([ | ||||||
|  |                 ...Object.keys(stockBuyData),  | ||||||
|  |                 ...Object.keys(stockSellData), | ||||||
|  |                 ...Object.keys(indexBuyData),  | ||||||
|  |                 ...Object.keys(indexSellData) | ||||||
|  |             ]); | ||||||
|  |              | ||||||
|  |             allMinutes.forEach(function(minute) { | ||||||
|  |                 var stockBuy = stockBuyData[minute] || 0; | ||||||
|  |                 var stockSell = stockSellData[minute] || 0; | ||||||
|  |                 var indexBuy = indexBuyData[minute] || 0; | ||||||
|  |                 var indexSell = indexSellData[minute] || 0; | ||||||
|  |                  | ||||||
|  |                 // 指数弱势:卖方量 > 买方量 | ||||||
|  |                 if (indexSell > indexBuy) { | ||||||
|  |                     indexWeakCount++; | ||||||
|  |                      | ||||||
|  |                     // 个股强势:买方量 > 卖方量 | ||||||
|  |                     if (stockBuy > stockSell) { | ||||||
|  |                         stockStrongCount++; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |              | ||||||
|  |             var strength = indexWeakCount > 0 ? (stockStrongCount / indexWeakCount * 100) : 0; | ||||||
|  |              | ||||||
|  |             return { | ||||||
|  |                 strength: strength, | ||||||
|  |                 indexWeakCount: indexWeakCount, | ||||||
|  |                 stockStrongCount: stockStrongCount | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 生成图表配置 | ||||||
|  |         function generateChartOption(buyData, sellData, title) { | ||||||
|  |             var allMinutes = new Set([...Object.keys(buyData), ...Object.keys(sellData)]); | ||||||
|  |             var sortedMinutes = Array.from(allMinutes).map(Number).sort((a, b) => a - b); | ||||||
|  |              | ||||||
|  |             var categories = []; | ||||||
|  |             var buyValues = []; | ||||||
|  |             var sellValues = []; | ||||||
|  |              | ||||||
|  |             sortedMinutes.forEach(function(minute) { | ||||||
|  |                 var timeStr = minute.toString().padStart(6, '0'); | ||||||
|  |                 var formattedTime = timeStr.substring(0,2) + ':' + timeStr.substring(2,4) + ':' + timeStr.substring(4,6); | ||||||
|  |                 categories.push(formattedTime); | ||||||
|  |                 buyValues.push(buyData[minute] || 0); | ||||||
|  |                 sellValues.push(sellData[minute] || 0); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             return { | ||||||
|  |                 title: { | ||||||
|  |                     text: title | ||||||
|  |                 }, | ||||||
|  |                 tooltip: { | ||||||
|  |                     trigger: 'axis', | ||||||
|  |                     axisPointer: { | ||||||
|  |                         type: 'shadow' | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 legend: { | ||||||
|  |                     data: ['买方', '卖方'] | ||||||
|  |                 }, | ||||||
|  |                 xAxis: { | ||||||
|  |                     data: categories, | ||||||
|  |                     axisLabel: { | ||||||
|  |                         rotate: 45 | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 yAxis: { | ||||||
|  |                     type: 'value', | ||||||
|  |                     name: '交易量' | ||||||
|  |                 }, | ||||||
|  |                 series: [{ | ||||||
|  |                     name: '买方', | ||||||
|  |                     type: 'bar', | ||||||
|  |                     data: buyValues, | ||||||
|  |                     stack: '总量', | ||||||
|  |                     itemStyle: { | ||||||
|  |                         color: '#ff4d4f' | ||||||
|  |                     } | ||||||
|  |                 }, { | ||||||
|  |                     name: '卖方', | ||||||
|  |                     type: 'bar', | ||||||
|  |                     data: sellValues, | ||||||
|  |                     stack: '总量', | ||||||
|  |                     itemStyle: { | ||||||
|  |                         color: '#52c41a' | ||||||
|  |                     } | ||||||
|  |                 }], | ||||||
|  |                 dataZoom: [{ | ||||||
|  |                     type: 'inside', | ||||||
|  |                     start: 0, | ||||||
|  |                     end: 100 | ||||||
|  |                 }, { | ||||||
|  |                     type: 'slider', | ||||||
|  |                     start: 0, | ||||||
|  |                     end: 100 | ||||||
|  |                 }] | ||||||
|  |             }; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // 主分析函数 | ||||||
|  |         async function fetchAndAnalyze() { | ||||||
|  |             var stockCode = document.getElementById('stockCode').value; | ||||||
|  |             var indexInfo = document.getElementById('indexSelect').value.split('|'); | ||||||
|  |             var indexCode = indexInfo[0]; | ||||||
|  |             var indexMarket = indexInfo[1]; | ||||||
|  |              | ||||||
|  |             if (!stockCode) { | ||||||
|  |                 alert('请输入股票代码'); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // 确定个股市场 | ||||||
|  |             var stockMarket = stockCode.startsWith('6') ? '1' : '0'; | ||||||
|  |              | ||||||
|  |             console.log('开始获取数据...'); | ||||||
|  |             console.log('个股:', stockCode, '市场:', stockMarket); | ||||||
|  |             console.log('指数:', indexCode, '市场:', indexMarket); | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 // 并行获取个股和指数数据 | ||||||
|  |                 const [stockData, indexData] = await Promise.all([ | ||||||
|  |                     fetchStockData(stockCode, stockMarket), | ||||||
|  |                     fetchStockData(indexCode, indexMarket) | ||||||
|  |                 ]); | ||||||
|  | 
 | ||||||
|  |                 if (!stockData || !indexData) { | ||||||
|  |                     alert('数据获取失败,请检查代码是否正确'); | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // 处理数据 | ||||||
|  |                 var stockResult = processTradeData(stockData); | ||||||
|  |                 var indexResult = processTradeData(indexData); | ||||||
|  | 
 | ||||||
|  |                 // 计算逆市强度 | ||||||
|  |                 var analysis = calculateCounterTrendStrength( | ||||||
|  |                     stockResult.buyData,  | ||||||
|  |                     stockResult.sellData, | ||||||
|  |                     indexResult.buyData,  | ||||||
|  |                     indexResult.sellData | ||||||
|  |                 ); | ||||||
|  | 
 | ||||||
|  |                 // 显示结果 | ||||||
|  |                 document.getElementById('resultPanel').style.display = 'block'; | ||||||
|  |                 document.getElementById('strengthValue').textContent = analysis.strength.toFixed(2) + '%'; | ||||||
|  |                 document.getElementById('indexWeakCount').textContent = analysis.indexWeakCount; | ||||||
|  |                 document.getElementById('stockStrongCount').textContent = analysis.stockStrongCount; | ||||||
|  | 
 | ||||||
|  |                 // 生成图表 | ||||||
|  |                 var stockOption = generateChartOption(stockResult.buyData, stockResult.sellData, '个股: ' + stockCode); | ||||||
|  |                 var indexOption = generateChartOption(indexResult.buyData, indexResult.sellData, '指数: ' + indexCode); | ||||||
|  | 
 | ||||||
|  |                 stockChart.setOption(stockOption); | ||||||
|  |                 indexChart.setOption(indexOption); | ||||||
|  | 
 | ||||||
|  |                 console.log('分析完成'); | ||||||
|  |                 console.log('逆市强度:', analysis.strength.toFixed(2) + '%'); | ||||||
|  | 
 | ||||||
|  |             } catch (error) { | ||||||
|  |                 console.error('分析过程中出现错误:', error); | ||||||
|  |                 alert('分析过程中出现错误,请重试'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html>  | ||||||
|  | @ -0,0 +1,133 @@ | ||||||
|  | <!DOCTYPE html> | ||||||
|  | <html> | ||||||
|  | <head> | ||||||
|  |     <meta charset="UTF-8"> | ||||||
|  |     <title>ECharts 示例</title> | ||||||
|  |     <!-- 引入 ECharts --> | ||||||
|  |     <script src="../static/js/echarts.min.js"></script> | ||||||
|  | </head> | ||||||
|  | <body> | ||||||
|  |     <!-- 输入框和按钮 --> | ||||||
|  |     <div> | ||||||
|  |         <input type="text" id="stockCode" placeholder="输入股票代码"> | ||||||
|  |         <input type="text" id="jysl" value="0" placeholder="输入交易量阈值"> | ||||||
|  |         <button onclick="fetchData()">查询</button> | ||||||
|  |     </div> | ||||||
|  |     <!-- 为ECharts准备一个具备大小(宽高)的Dom --> | ||||||
|  |     <div id="main" style="width: 1000px;height:600px;"></div> | ||||||
|  |     <script> | ||||||
|  |         // ECharts图表初始化 | ||||||
|  |         var myChart = echarts.init(document.getElementById('main')); | ||||||
|  | 
 | ||||||
|  |         function fetchData() { | ||||||
|  | 
 | ||||||
|  |             let inputValue = document.getElementById('jysl').value; | ||||||
|  |             var stockCode = document.getElementById('stockCode').value; | ||||||
|  |             // 检查code是否以'6'开头,据此设置market值 | ||||||
|  |             var market = stockCode.startsWith('6') ? '1' : '0'; | ||||||
|  |             var baseUrl = "http://push2ex.eastmoney.com/getStockFenShi?pagesize=6000&ut=7eea3edcaed734bea9cbfc24409ed989&dpt=wzfscj&pageindex=0&id=3007792&sort=1&ft=1&code=REPLACE_CODE&market=REPLACE_MARKET"; | ||||||
|  |             var url = baseUrl.replace("REPLACE_CODE", stockCode).replace("REPLACE_MARKET", market); | ||||||
|  |              | ||||||
|  |             fetch(url) | ||||||
|  |                 .then(function(response) { | ||||||
|  |                     return response.json(); | ||||||
|  |                 }) | ||||||
|  |                 .then(function(jsonData) { | ||||||
|  |                     // 这里的jsonData就是你从服务器获取的数据 | ||||||
|  |                     // 下面是数据处理的代码,可能需要根据实际数据结构进行调整 | ||||||
|  | 
 | ||||||
|  |                     var data = jsonData.data.data; // 根据实际结构调整路径 | ||||||
|  | 
 | ||||||
|  |                     // 数据处理:按每分钟聚合 | ||||||
|  |                     var buyData = {};  // 买方数据 | ||||||
|  |                     var sellData = {}; // 卖方数据 | ||||||
|  |                     data.forEach(function(item) { | ||||||
|  |                         var minute = Math.floor(item.t / 100); // 取整到分钟 | ||||||
|  |                         if (item.v >= inputValue) { // 只有当v值大于或等于100时才进行处理 | ||||||
|  |                             if (item.bs === 1) { | ||||||
|  |                                 if (!sellData[minute]) sellData[minute] = 0; | ||||||
|  |                                 sellData[minute] += item.v; | ||||||
|  |                             } else if (item.bs === 2) { | ||||||
|  |                                 if (!buyData[minute]) buyData[minute] = 0; | ||||||
|  |                                 buyData[minute] += item.v; | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     // 准备ECharts数据 | ||||||
|  |                     var categories = []; | ||||||
|  |                     var buyValues = []; | ||||||
|  |                     var sellValues = []; | ||||||
|  |                      | ||||||
|  |                     // 获取所有时间点并按数字大小排序 | ||||||
|  |                     var allMinutes = new Set([...Object.keys(buyData), ...Object.keys(sellData)]); | ||||||
|  |                     var sortedMinutes = Array.from(allMinutes).map(Number).sort((a, b) => a - b); | ||||||
|  |                      | ||||||
|  |                     sortedMinutes.forEach(function(minute) { | ||||||
|  |                         // 格式化时间显示 (例如: 093000 -> 09:30:00) | ||||||
|  |                         var timeStr = minute.toString().padStart(6, '0'); | ||||||
|  |                         var formattedTime = timeStr.substring(0,2) + ':' + timeStr.substring(2,4) + ':' + timeStr.substring(4,6); | ||||||
|  |                         categories.push(formattedTime); | ||||||
|  |                         buyValues.push(buyData[minute] || 0); | ||||||
|  |                         sellValues.push(sellData[minute] || 0); | ||||||
|  |                     }); | ||||||
|  | 
 | ||||||
|  |                     // 设置ECharts选项 | ||||||
|  |                     var option = { | ||||||
|  |                         tooltip: { | ||||||
|  |                             trigger: 'axis', | ||||||
|  |                             axisPointer: { | ||||||
|  |                                 type: 'shadow' | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         legend: { | ||||||
|  |                             data:['买方', '卖方'] | ||||||
|  |                         }, | ||||||
|  |                         xAxis: { | ||||||
|  |                             data: categories, | ||||||
|  |                             axisLabel: { | ||||||
|  |                                 rotate: 45 // 旋转标签以避免重叠 | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         yAxis: { | ||||||
|  |                             type: 'value', | ||||||
|  |                             name: '交易量' | ||||||
|  |                         }, | ||||||
|  |                         series: [{ | ||||||
|  |                             name: '买方', | ||||||
|  |                             type: 'bar', | ||||||
|  |                             data: buyValues,  // 修复:买方显示买方数据 | ||||||
|  |                             stack: '总量', | ||||||
|  |                             itemStyle: { | ||||||
|  |                                 color: '#ff4d4f' // 红色表示买方 | ||||||
|  |                             } | ||||||
|  |                         }, { | ||||||
|  |                             name: '卖方', | ||||||
|  |                             type: 'bar', | ||||||
|  |                             data: sellValues, // 修复:卖方显示卖方数据 | ||||||
|  |                             stack: '总量', | ||||||
|  |                             itemStyle: { | ||||||
|  |                                 color: '#52c41a' // 绿色表示卖方 | ||||||
|  |                             } | ||||||
|  |                         }], | ||||||
|  |                         dataZoom: [{ | ||||||
|  |                             type: 'inside', // 内置于坐标系中,通过鼠标滚轮或触控板操作进行数据区域缩放 | ||||||
|  |                             start: 0,       // 数据窗口范围的起始百分比,表示数据窗口包含数据序列的起始部分 | ||||||
|  |                             end: 100        // 数据窗口范围的结束百分比,初始设置为100%,即展示全部数据 | ||||||
|  |                         }, { | ||||||
|  |                             type: 'slider', // 滑动条型数据区域缩放组件 | ||||||
|  |                             start: 0, | ||||||
|  |                             end: 100 | ||||||
|  |                         }], | ||||||
|  |                     }; | ||||||
|  | 
 | ||||||
|  |                     // 显示图表。 | ||||||
|  |                     myChart.setOption(option); | ||||||
|  |                 }) | ||||||
|  |                 .catch(function(error) { | ||||||
|  |                     console.log('Request failed', error); | ||||||
|  |                 }); | ||||||
|  |         } | ||||||
|  |     </script> | ||||||
|  | </body> | ||||||
|  | </html> | ||||||
|  | @ -43,7 +43,7 @@ class PortfolioAnalyzer: | ||||||
|             pool_recycle=3600 |             pool_recycle=3600 | ||||||
|         ) |         ) | ||||||
|         self.api_url = ("https://to.bmbs.tech/aim/app/v1/derivativeTrading/getTradingRecordList?" |         self.api_url = ("https://to.bmbs.tech/aim/app/v1/derivativeTrading/getTradingRecordList?" | ||||||
|                         "projectId=182AE38B8C254BC88C715D86C643A6DD,C9B533175D294648A2372CB2966BCC96") |                         "projectId=182AE38B8C254BC88C715D86C643A6DD,C9B533175D294648A2372CB2966BCC96&dataScope=all") | ||||||
|         logger.info("持仓分析器初始化完成") |         logger.info("持仓分析器初始化完成") | ||||||
|      |      | ||||||
|     def get_trading_records(self) -> Optional[Dict]: |     def get_trading_records(self) -> Optional[Dict]: | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue