""" 生成包含内嵌数据的股票查看器HTML 用法: python scripts/generate_stock_viewer.py python scripts/generate_stock_viewer.py --date 20260120 python scripts/generate_stock_viewer.py --all-stocks # 显示所有108只股票 """ from __future__ import annotations import argparse import csv import os import json import sys import pickle import numpy as np import pandas as pd # 添加scoring模块路径 sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'scoring')) try: from scoring import ( normalize_all, calculate_strength, CONFIG_EQUAL, CONFIG_AGGRESSIVE, CONFIG_CONSERVATIVE, CONFIG_VOLUME_FOCUS, # 单维度测试模式 CONFIG_TEST_PRICE, CONFIG_TEST_CONVERGENCE, CONFIG_TEST_VOLUME, CONFIG_TEST_GEOMETRY, CONFIG_TEST_ACTIVITY, CONFIG_TEST_TILT, ) SCORING_AVAILABLE = True except ImportError as e: print(f"警告: 无法导入scoring模块: {e}") print("将使用原始强度分,不进行标准化") SCORING_AVAILABLE = False def load_all_stocks_list(data_dir: str) -> tuple: """从close.pkl加载所有股票列表""" # 创建假模块以加载pickle sys.modules['model'] = type('FakeModule', (), {'ndarray': np.ndarray, '__path__': []})() sys.modules['model.index_info'] = sys.modules['model'] close_path = os.path.join(data_dir, 'close.pkl') with open(close_path, 'rb') as f: data = pickle.load(f) return data['tkrs'], data['tkrs_name'] def load_stock_data(csv_path: str, target_date: int = None, all_stocks_mode: bool = False, data_dir: str = 'data') -> tuple: """从CSV加载股票数据并进行标准化处理""" stocks_map = {} max_date = 0 with open(csv_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: date = int(row.get('date', '0')) if date > max_date: max_date = date except: continue # 使用指定日期或最新日期 use_date = target_date if target_date else max_date # 从CSV读取有强度分的股票 rows_list = [] with open(csv_path, 'r', encoding='utf-8-sig') as f: reader = csv.DictReader(f) for row in reader: try: date = int(row.get('date', '0')) if date != use_date: continue rows_list.append(row) except: continue # 转换为DataFrame以进行标准化 if rows_list and SCORING_AVAILABLE: df = pd.DataFrame(rows_list) # 转换数值列 numeric_cols = ['breakout_strength_up', 'breakout_strength_down', 'price_score_up', 'price_score_down', 'convergence_score', 'volume_score', 'geometry_score', 'activity_score', 'tilt_score'] for col in numeric_cols: if col in df.columns: df[col] = pd.to_numeric(df[col], errors='coerce').fillna(0) # 执行标准化 df_norm = normalize_all(df) # 计算4种预设模式的强度分 (分别计算up和down) from dataclasses import replace # 等权模式 config_equal_up = replace(CONFIG_EQUAL, direction='up') config_equal_down = replace(CONFIG_EQUAL, direction='down') df_norm['strength_equal_up'] = calculate_strength(df_norm, config_equal_up) df_norm['strength_equal_down'] = calculate_strength(df_norm, config_equal_down) # 激进模式 config_agg_up = replace(CONFIG_AGGRESSIVE, direction='up') config_agg_down = replace(CONFIG_AGGRESSIVE, direction='down') df_norm['strength_aggressive_up'] = calculate_strength(df_norm, config_agg_up) df_norm['strength_aggressive_down'] = calculate_strength(df_norm, config_agg_down) # 保守模式 config_cons_up = replace(CONFIG_CONSERVATIVE, direction='up') config_cons_down = replace(CONFIG_CONSERVATIVE, direction='down') df_norm['strength_conservative_up'] = calculate_strength(df_norm, config_cons_up) df_norm['strength_conservative_down'] = calculate_strength(df_norm, config_cons_down) # 放量模式 config_vol_up = replace(CONFIG_VOLUME_FOCUS, direction='up') config_vol_down = replace(CONFIG_VOLUME_FOCUS, direction='down') df_norm['strength_volume_up'] = calculate_strength(df_norm, config_vol_up) df_norm['strength_volume_down'] = calculate_strength(df_norm, config_vol_down) # 单维度测试模式(50%主导) # 突破主导 config_test_price_up = replace(CONFIG_TEST_PRICE, direction='up') config_test_price_down = replace(CONFIG_TEST_PRICE, direction='down') df_norm['strength_test_price_up'] = calculate_strength(df_norm, config_test_price_up) df_norm['strength_test_price_down'] = calculate_strength(df_norm, config_test_price_down) # 收敛主导 config_test_conv_up = replace(CONFIG_TEST_CONVERGENCE, direction='up') config_test_conv_down = replace(CONFIG_TEST_CONVERGENCE, direction='down') df_norm['strength_test_convergence_up'] = calculate_strength(df_norm, config_test_conv_up) df_norm['strength_test_convergence_down'] = calculate_strength(df_norm, config_test_conv_down) # 成交量主导 config_test_vol_up = replace(CONFIG_TEST_VOLUME, direction='up') config_test_vol_down = replace(CONFIG_TEST_VOLUME, direction='down') df_norm['strength_test_volume_up'] = calculate_strength(df_norm, config_test_vol_up) df_norm['strength_test_volume_down'] = calculate_strength(df_norm, config_test_vol_down) # 形态主导 config_test_geo_up = replace(CONFIG_TEST_GEOMETRY, direction='up') config_test_geo_down = replace(CONFIG_TEST_GEOMETRY, direction='down') df_norm['strength_test_geometry_up'] = calculate_strength(df_norm, config_test_geo_up) df_norm['strength_test_geometry_down'] = calculate_strength(df_norm, config_test_geo_down) # 活跃主导 config_test_act_up = replace(CONFIG_TEST_ACTIVITY, direction='up') config_test_act_down = replace(CONFIG_TEST_ACTIVITY, direction='down') df_norm['strength_test_activity_up'] = calculate_strength(df_norm, config_test_act_up) df_norm['strength_test_activity_down'] = calculate_strength(df_norm, config_test_act_down) # 倾斜主导 config_test_tilt_up = replace(CONFIG_TEST_TILT, direction='up') config_test_tilt_down = replace(CONFIG_TEST_TILT, direction='down') df_norm['strength_test_tilt_up'] = calculate_strength(df_norm, config_test_tilt_up) df_norm['strength_test_tilt_down'] = calculate_strength(df_norm, config_test_tilt_down) else: df_norm = None # 构建stocks_map for idx, row in enumerate(rows_list): try: stock_code = row.get('stock_code', '') stock = { 'idx': int(row.get('stock_idx', '0')), 'code': stock_code, 'name': row.get('stock_name', ''), 'strengthUp': float(row.get('breakout_strength_up', '0')), 'strengthDown': float(row.get('breakout_strength_down', '0')), 'direction': row.get('breakout_dir', ''), 'widthRatio': float(row.get('width_ratio', '0')), 'touchesUpper': int(row.get('touches_upper', '0')), 'touchesLower': int(row.get('touches_lower', '0')), 'volumeConfirmed': row.get('volume_confirmed', ''), 'activityScore': float(row.get('activity_score', '0')), 'tiltScore': float(row.get('tilt_score', '0')), 'date': use_date, 'hasTriangle': True } # 添加标准化字段 if df_norm is not None: norm_row = df_norm.iloc[idx] stock['priceUpNorm'] = float(norm_row.get('price_score_up_norm', 0)) stock['priceDownNorm'] = float(norm_row.get('price_score_down_norm', 0)) stock['convergenceNorm'] = float(norm_row.get('convergence_score_norm', 0)) stock['volumeNorm'] = float(norm_row.get('volume_score_norm', 0)) stock['geometryNorm'] = float(norm_row.get('geometry_score_norm', 0)) stock['activityNorm'] = float(norm_row.get('activity_score_norm', 0)) stock['tiltNorm'] = float(norm_row.get('tilt_score_norm', 0)) # 添加预设模式强度分 stock['strengthEqualUp'] = float(norm_row.get('strength_equal_up', 0)) stock['strengthEqualDown'] = float(norm_row.get('strength_equal_down', 0)) stock['strengthAggressiveUp'] = float(norm_row.get('strength_aggressive_up', 0)) stock['strengthAggressiveDown'] = float(norm_row.get('strength_aggressive_down', 0)) stock['strengthConservativeUp'] = float(norm_row.get('strength_conservative_up', 0)) stock['strengthConservativeDown'] = float(norm_row.get('strength_conservative_down', 0)) stock['strengthVolumeUp'] = float(norm_row.get('strength_volume_up', 0)) stock['strengthVolumeDown'] = float(norm_row.get('strength_volume_down', 0)) # 添加单维度测试模式强度分 stock['strengthTestPriceUp'] = float(norm_row.get('strength_test_price_up', 0)) stock['strengthTestPriceDown'] = float(norm_row.get('strength_test_price_down', 0)) stock['strengthTestConvergenceUp'] = float(norm_row.get('strength_test_convergence_up', 0)) stock['strengthTestConvergenceDown'] = float(norm_row.get('strength_test_convergence_down', 0)) stock['strengthTestVolumeUp'] = float(norm_row.get('strength_test_volume_up', 0)) stock['strengthTestVolumeDown'] = float(norm_row.get('strength_test_volume_down', 0)) stock['strengthTestGeometryUp'] = float(norm_row.get('strength_test_geometry_up', 0)) stock['strengthTestGeometryDown'] = float(norm_row.get('strength_test_geometry_down', 0)) stock['strengthTestActivityUp'] = float(norm_row.get('strength_test_activity_up', 0)) stock['strengthTestActivityDown'] = float(norm_row.get('strength_test_activity_down', 0)) stock['strengthTestTiltUp'] = float(norm_row.get('strength_test_tilt_up', 0)) stock['strengthTestTiltDown'] = float(norm_row.get('strength_test_tilt_down', 0)) # 根据方向选择强度分 if stock['direction'] == 'up': stock['strengthEqual'] = stock['strengthEqualUp'] stock['strengthAggressive'] = stock['strengthAggressiveUp'] stock['strengthConservative'] = stock['strengthConservativeUp'] stock['strengthVolume'] = stock['strengthVolumeUp'] stock['strengthTestPrice'] = stock['strengthTestPriceUp'] stock['strengthTestConvergence'] = stock['strengthTestConvergenceUp'] stock['strengthTestVolume'] = stock['strengthTestVolumeUp'] stock['strengthTestGeometry'] = stock['strengthTestGeometryUp'] stock['strengthTestActivity'] = stock['strengthTestActivityUp'] stock['strengthTestTilt'] = stock['strengthTestTiltUp'] elif stock['direction'] == 'down': stock['strengthEqual'] = stock['strengthEqualDown'] stock['strengthAggressive'] = stock['strengthAggressiveDown'] stock['strengthConservative'] = stock['strengthConservativeDown'] stock['strengthVolume'] = stock['strengthVolumeDown'] stock['strengthTestPrice'] = stock['strengthTestPriceDown'] stock['strengthTestConvergence'] = stock['strengthTestConvergenceDown'] stock['strengthTestVolume'] = stock['strengthTestVolumeDown'] stock['strengthTestGeometry'] = stock['strengthTestGeometryDown'] stock['strengthTestActivity'] = stock['strengthTestActivityDown'] stock['strengthTestTilt'] = stock['strengthTestTiltDown'] else: # 无方向时取两者最大值 stock['strengthEqual'] = max(stock['strengthEqualUp'], stock['strengthEqualDown']) stock['strengthAggressive'] = max(stock['strengthAggressiveUp'], stock['strengthAggressiveDown']) stock['strengthConservative'] = max(stock['strengthConservativeUp'], stock['strengthConservativeDown']) stock['strengthVolume'] = max(stock['strengthVolumeUp'], stock['strengthVolumeDown']) stock['strengthTestPrice'] = max(stock['strengthTestPriceUp'], stock['strengthTestPriceDown']) stock['strengthTestConvergence'] = max(stock['strengthTestConvergenceUp'], stock['strengthTestConvergenceDown']) stock['strengthTestVolume'] = max(stock['strengthTestVolumeUp'], stock['strengthTestVolumeDown']) stock['strengthTestGeometry'] = max(stock['strengthTestGeometryUp'], stock['strengthTestGeometryDown']) stock['strengthTestActivity'] = max(stock['strengthTestActivityUp'], stock['strengthTestActivityDown']) stock['strengthTestTilt'] = max(stock['strengthTestTiltUp'], stock['strengthTestTiltDown']) else: # 如果标准化不可用,设置默认值 stock['priceUpNorm'] = 0 stock['priceDownNorm'] = 0 stock['convergenceNorm'] = 0 stock['volumeNorm'] = 0 stock['geometryNorm'] = 0 stock['activityNorm'] = 0 stock['tiltNorm'] = 0 stock['strengthEqual'] = stock['strengthUp'] if stock['direction'] == 'up' else stock['strengthDown'] stock['strengthAggressive'] = stock['strengthEqual'] stock['strengthConservative'] = stock['strengthEqual'] stock['strengthVolume'] = stock['strengthEqual'] stock['strengthTestPrice'] = stock['strengthEqual'] stock['strengthTestConvergence'] = stock['strengthEqual'] stock['strengthTestVolume'] = stock['strengthEqual'] stock['strengthTestGeometry'] = stock['strengthEqual'] stock['strengthTestActivity'] = stock['strengthEqual'] stock['strengthTestTilt'] = stock['strengthEqual'] stock['strength'] = max(stock['strengthUp'], stock['strengthDown']) # 清理文件名中的非法字符 clean_name = stock['name'].replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '') stock['chartPath'] = f"charts/{use_date}_{stock_code}_{clean_name}.png" stock['chartPathDetail'] = f"charts/{use_date}_{stock_code}_{clean_name}_detail.png" if stock_code not in stocks_map or stocks_map[stock_code]['strength'] < stock['strength']: stocks_map[stock_code] = stock except Exception as e: print(f"处理股票 {row.get('stock_code', 'unknown')} 时出错: {e}") continue # 如果是all_stocks模式,添加所有股票 if all_stocks_mode: all_codes, all_names = load_all_stocks_list(data_dir) for idx, (code, name) in enumerate(zip(all_codes, all_names)): if code not in stocks_map: # 没有三角形形态的股票,强度分为0 clean_name = name.replace('*', '').replace('?', '').replace('"', '').replace('<', '').replace('>', '').replace('|', '').replace(':', '').replace('/', '').replace('\\', '') stocks_map[code] = { 'idx': idx, 'code': code, 'name': name, 'strengthUp': 0.0, 'strengthDown': 0.0, 'strength': 0.0, 'direction': 'none', 'widthRatio': 0.0, 'touchesUpper': 0, 'touchesLower': 0, 'volumeConfirmed': '', 'activityScore': 0.0, 'tiltScore': 0.0, 'date': use_date, 'chartPath': f"charts/{use_date}_{code}_{clean_name}.png", 'chartPathDetail': f"charts/{use_date}_{code}_{clean_name}_detail.png", 'hasTriangle': False, # 标准化字段 'priceUpNorm': 0.0, 'priceDownNorm': 0.0, 'convergenceNorm': 0.0, 'volumeNorm': 0.0, 'geometryNorm': 0.0, 'activityNorm': 0.0, 'tiltNorm': 0.0, 'strengthEqual': 0.0, 'strengthAggressive': 0.0, 'strengthConservative': 0.0, 'strengthVolume': 0.0, # 单维度测试模式 'strengthTestPrice': 0.0, 'strengthTestConvergence': 0.0, 'strengthTestVolume': 0.0, 'strengthTestGeometry': 0.0, 'strengthTestActivity': 0.0, 'strengthTestTilt': 0.0, } stocks = list(stocks_map.values()) stocks.sort(key=lambda x: x['strength'], reverse=True) return stocks, use_date def generate_html(stocks: list, date: int, output_path: str): """生成包含数据的HTML""" html_template = ''' 收敛三角形强度分选股系统

收敛三角形选股系统

数据日期: DATA_DATE 监控股票: TOTAL_STOCKS
等权模式
激进模式
保守模式
放量模式
突破主导
收敛主导
成交量主导
形态主导
活跃主导
倾斜主导
排序
全部
↑ 向上
↓ 向下
— 无
全部
✓ 已确认
✗ 未确认
0.00
📊
Total
0
Showing
0
Average
0.00
''' # 替换数据 stocks_json = json.dumps(stocks, ensure_ascii=False, indent=2) html = html_template.replace('STOCK_DATA', stocks_json) html = html.replace('DATA_DATE', str(date)) html = html.replace('TOTAL_STOCKS', str(len(stocks))) with open(output_path, 'w', encoding='utf-8') as f: f.write(html) def main(): parser = argparse.ArgumentParser(description="生成包含内嵌数据的股票查看器HTML") parser.add_argument( "--date", type=int, default=None, help="指定日期(YYYYMMDD),默认为最新日期" ) parser.add_argument( "--input", default=os.path.join("outputs", "converging_triangles", "all_results.csv"), help="输入CSV路径" ) parser.add_argument( "--output", default=os.path.join("outputs", "converging_triangles", "stock_viewer.html"), help="输出HTML路径" ) parser.add_argument( "--all-stocks", action="store_true", help="显示所有108只股票(包括不满足条件的)" ) args = parser.parse_args() print("=" * 70) print("生成股票查看器HTML") print("=" * 70) if not os.path.exists(args.input): print(f"错误: CSV文件不存在: {args.input}") print("请先运行: python scripts/pipeline_converging_triangle.py") return print(f"读取数据: {args.input}") print(f"模式: {'所有股票' if args.all_stocks else '仅满足条件的股票'}") data_dir = "data" stocks, date = load_stock_data(args.input, args.date, args.all_stocks, data_dir) print(f"数据日期: {date}") print(f"股票数量: {len(stocks)}") if args.all_stocks: has_triangle = sum(1 for s in stocks if s.get('hasTriangle', False)) print(f" - 有三角形形态: {has_triangle}") print(f" - 无三角形形态: {len(stocks) - has_triangle}") print(f"生成HTML: {args.output}") generate_html(stocks, date, args.output) print("\n完成!") print(f"\n用浏览器打开: {args.output}") print("=" * 70) if __name__ == "__main__": main()