diff --git a/src/app.py b/src/app.py index 5e3f486..c84598a 100644 --- a/src/app.py +++ b/src/app.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys import os from datetime import datetime, timedelta @@ -44,6 +45,7 @@ from src.scripts.stock_daily_data_collector import collect_stock_daily_data from valuation_analysis.financial_analysis import FinancialAnalyzer from src.valuation_analysis.stock_price_collector import StockPriceCollector +from src.quantitative_analysis.batch_stock_price_collector import fetch_and_store_stock_data, get_stock_realtime_info_from_redis # 设置日志 logging.basicConfig( @@ -227,7 +229,7 @@ def run_rzrq_initial_collection1(): }), 200 -@app.route('/scheduler/industry/crowding', methods=['GET']) +@app.route('/scheduler/industry/crowding1/bak', methods=['GET']) def precalculate_industry_crowding1(): """预计算部分行业和概念板块的拥挤度指标 晚上10点开始""" try: @@ -337,6 +339,27 @@ def scheduler_financial_analysis(): "status": "success" }), 200 +@app.route('/scheduler/industry/crowding', methods=['GET']) +def precalculate_industry_crowding_batch(): + """批量预计算行业和概念板块的拥挤度指标""" + try: + from src.valuation_analysis.industry_analysis import IndustryAnalyzer + + analyzer = IndustryAnalyzer() + # 固定行业和概念板块 + industries = ["煤炭开采", "焦炭加工", "油气开采", "石油化工", "油服工程", "日用化工", "化纤", "化学原料", "化学制品", "塑料", "橡胶", "农用化工", "非金属材料", "冶钢原料", "普钢", "特钢", "工业金属", "贵金属", "能源金属", "稀有金属", "金属新材料", "水泥", "玻璃玻纤", "装饰建材", "种植业", "养殖业", "林业", "渔业", "饲料", "农产品加工", "动物保健", "酿酒", "饮料乳品", "调味品", "休闲食品", "食品加工", "纺织制造", "服装家纺", "饰品", "造纸", "包装印刷", "家居用品", "文娱用品", "白色家电", "黑色家电", "小家电", "厨卫电器", "家电零部件", "一般零售", "商业物业经营", "专业连锁", "贸易", "电子商务", "乘用车", "商用车", "汽车零部件", "汽车服务", "摩托车及其他", "化学制药", "生物制品", "中药", "医药商业", "医疗器械", "医疗服务", "医疗美容", "电机制造", "电池", "电网设备", "光伏设备", "风电设备", "其他发电设备", "地面兵装", "航空装备", "航天装备", "航海装备", "军工电子", "轨交设备", "通用设备", "专用设备", "工程机械", "自动化设备", "半导体", "消费电子", "光学光电", "元器件", "其他电子", "通信设备", "通信工程", "电信服务", "IT设备", "软件服务", "云服务", "产业互联网", "游戏", "广告营销", "影视院线", "数字媒体", "出版业", "广播电视", "全国性银行", "地方性银行", "证券", "保险", "多元金融", "房屋建设", "基础建设", "专业工程", "工程咨询服务", "装修装饰", "房地产开发", "房产服务", "体育", "教育培训", "酒店餐饮", "旅游", "专业服务", "公路铁路", "航空机场", "航运港口", "物流", "电力", "燃气", "水务", "环保设备", "环境治理", "环境监测", "综合类"] + concepts = ["通达信88", "海峡西岸", "海南自贸", "一带一路", "上海自贸", "雄安新区", "粤港澳", "ST板块", "次新股", "含H股", "含B股", "含GDR", "含可转债", "国防军工", "军民融合", "大飞机", "稀缺资源", "5G概念", "碳中和", "黄金概念", "物联网", "创投概念", "航运概念", "铁路基建", "高端装备", "核电核能", "光伏", "风电", "锂电池概念", "燃料电池", "HJT电池", "固态电池", "钠电池", "钒电池", "TOPCon电池", "钙钛矿电池", "BC电池", "氢能源", "稀土永磁", "盐湖提锂", "锂矿", "水利建设", "卫星导航", "可燃冰", "页岩气", "生物疫苗", "基因概念", "维生素", "仿制药", "创新药", "免疫治疗", "CXO概念", "节能环保", "食品安全", "白酒概念", "代糖概念", "猪肉", "鸡肉", "水产品", "碳纤维", "石墨烯", "3D打印", "苹果概念", "阿里概念", "腾讯概念", "小米概念", "百度概念", "华为鸿蒙", "华为海思", "华为汽车", "华为算力", "特斯拉概念", "消费电子概念", "汽车电子", "无线耳机", "生物质能", "地热能", "充电桩", "新能源车", "换电概念", "高压快充", "草甘膦", "安防服务", "垃圾分类", "核污染防治", "风沙治理", "乡村振兴", "土地流转", "体育概念", "博彩概念", "赛马概念", "分散染料", "聚氨酯", "云计算", "边缘计算", "网络游戏", "信息安全", "国产软件", "大数据", "数据中心", "芯片", "MCU芯片", "汽车芯片", "存储芯片", "互联金融", "婴童概念", "养老概念", "网红经济", "民营医院", "特高压", "智能电网", "智能穿戴", "智能交通", "智能家居", "智能医疗", "智慧城市", "智慧政务", "机器人概念", "机器视觉", "超导概念", "职业教育", "物业管理概念", "虚拟现实", "数字孪生", "钛金属", "钴金属", "镍金属", "氟概念", "磷概念", "无人机", "PPP概念", "新零售", "跨境电商", "量子科技", "无人驾驶", "ETC概念", "胎压监测", "OLED概念", "MiniLED", "MicroLED", "超清视频", "区块链", "数字货币", "人工智能", "租购同权", "工业互联", "知识产权", "工业大麻", "工业气体", "人造肉", "预制菜", "种业", "化肥概念", "操作系统", "光刻机", "第三代半导体", "远程办公", "口罩防护", "医废处理", "虫害防治", "超级电容", "C2M概念", "地摊经济", "冷链物流", "抖音概念", "降解塑料", "医美概念", "人脑工程", "烟草概念", "新型烟草", "有机硅概念", "新冠检测", "BIPV概念", "地下管网", "储能", "新材料", "工业母机", "一体压铸", "汽车热管理", "汽车拆解", "NMN概念", "国资云", "元宇宙概念", "NFT概念", "云游戏", "天然气", "绿色电力", "培育钻石", "信创", "幽门螺杆菌", "电子纸", "新冠药概念", "免税概念", "PVDF概念", "装配式建筑", "绿色建筑", "东数西算", "跨境支付CIPS", "中俄贸易", "电子身份证", "家庭医生", "辅助生殖", "肝炎概念", "新型城镇", "粮食概念", "超临界发电", "虚拟电厂", "动力电池回收", "PCB概念", "先进封装", "热泵概念", "EDA概念", "光热发电", "供销社", "Web3概念", "DRG-DIP", "AIGC概念", "复合铜箔", "数据确权", "数据要素", "POE胶膜", "血氧仪", "旅游概念", "中特估", "ChatGPT概念", "CPO概念", "数字水印", "毫米波雷达", "工业软件", "6G概念", "时空大数据", "可控核聚变", "知识付费", "算力租赁", "光通信", "混合现实", "英伟达概念", "减速器", "减肥药", "合成生物", "星闪概念", "液冷服务器", "新型工业化", "短剧游戏", "多模态AI", "PEEK材料", "小米汽车概念", "飞行汽车", "Sora概念", "人形机器人", "AI手机PC", "低空经济", "铜缆高速连接", "军工信息化", "玻璃基板", "商业航天", "车联网", "财税数字化", "折叠屏", "AI眼镜", "智谱AI", "IP经济", "宠物经济", "小红书概念", "AI智能体", "DeepSeek概念", "AI医疗概念", "海洋经济", "外骨骼机器人", "军贸概念"] + + # 批量计算行业和概念板块拥挤度 + analyzer.batch_calculate_industry_crowding(industries, concepts) + + logger.info("批量计算行业和概念板块的拥挤度指标完成") + except Exception as e: + logger.error(f"批量计算行业拥挤度指标失败: {str(e)}") + return jsonify({ + "status": "success" + }), 200 + @app.route('/') def index(): """渲染主页""" @@ -2863,17 +2886,53 @@ def get_pep_stock_info_by_shortname(): @app.route('/api/pep_stock_info_by_code', methods=['GET']) def get_pep_stock_info_by_code(): - """根据股票简称查询pep_stock_info集合中的全部字段""" + """根据股票代码查询Redis中的实时行情并返回指定结构""" short_code = request.args.get('code') if not short_code: return jsonify({'success': False, 'message': '缺少必要参数: short_code'}), 400 try: - analyzer = FinancialAnalyzer() - result = analyzer.get_pep_stock_info_by_code(short_code) - return jsonify(result) + # 兼容600001.SH/SH600001等格式 + 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'未找到股票 {short_code} 的实时行情'}), 404 except Exception as e: return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500 +@app.route('/api/industry/crowding/filter', methods=['GET']) +def filter_industry_crowding(): + """根据拥挤度百分位区间筛选行业和概念板块""" + try: + min_val = float(request.args.get('min', 0)) + max_val = float(request.args.get('max', 100)) + from src.valuation_analysis.industry_analysis import IndustryAnalyzer + analyzer = IndustryAnalyzer() + result = analyzer.filter_crowding_by_percentile(min_val, max_val) + return jsonify({ + 'status': 'success', + 'min': min_val, + 'max': max_val, + 'result': result + }) + except Exception as e: + logger.error(f"筛选行业/概念拥挤度接口异常: {str(e)}") + return jsonify({ + 'status': 'error', + 'message': str(e) + }), 500 + +@app.route('/scheduler/batch_stock_price/collection', methods=['GET']) +def run_batch_stock_price_collection(): + """批量采集A股行情并保存到数据库""" + try: + fetch_and_store_stock_data() + return jsonify({"status": "success", "message": "批量采集A股行情并保存到数据库成功"}) + except Exception as e: + logger.error(f"批量采集A股行情失败: {str(e)}") + return jsonify({"status": "error", "message": str(e)}) + if __name__ == '__main__': # 启动Web服务器 diff --git a/src/fundamentals_llm/reports/宁德时代_300750_analysis.pdf b/src/fundamentals_llm/reports/宁德时代_300750_analysis.pdf deleted file mode 100644 index b708318..0000000 Binary files a/src/fundamentals_llm/reports/宁德时代_300750_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/思科瑞_688053_analysis.pdf b/src/fundamentals_llm/reports/思科瑞_688053_analysis.pdf deleted file mode 100644 index 92af37a..0000000 Binary files a/src/fundamentals_llm/reports/思科瑞_688053_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/洲际油气_600759_analysis.docx b/src/fundamentals_llm/reports/洲际油气_600759_analysis.docx deleted file mode 100644 index 4154bb5..0000000 Binary files a/src/fundamentals_llm/reports/洲际油气_600759_analysis.docx and /dev/null differ diff --git a/src/fundamentals_llm/reports/洲际油气_600759_analysis.pdf b/src/fundamentals_llm/reports/洲际油气_600759_analysis.pdf deleted file mode 100644 index 74c077a..0000000 Binary files a/src/fundamentals_llm/reports/洲际油气_600759_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/海默科技_300084_analysis.pdf b/src/fundamentals_llm/reports/海默科技_300084_analysis.pdf deleted file mode 100644 index 0360c1b..0000000 Binary files a/src/fundamentals_llm/reports/海默科技_300084_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/至纯科技_603690_analysis.docx b/src/fundamentals_llm/reports/至纯科技_603690_analysis.docx deleted file mode 100644 index f390912..0000000 Binary files a/src/fundamentals_llm/reports/至纯科技_603690_analysis.docx and /dev/null differ diff --git a/src/fundamentals_llm/reports/至纯科技_603690_analysis.pdf b/src/fundamentals_llm/reports/至纯科技_603690_analysis.pdf deleted file mode 100644 index b526456..0000000 Binary files a/src/fundamentals_llm/reports/至纯科技_603690_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/菲林格尔_603226_analysis.pdf b/src/fundamentals_llm/reports/菲林格尔_603226_analysis.pdf deleted file mode 100644 index 04611f6..0000000 Binary files a/src/fundamentals_llm/reports/菲林格尔_603226_analysis.pdf and /dev/null differ diff --git a/src/fundamentals_llm/reports/震安科技_300767_analysis.docx b/src/fundamentals_llm/reports/震安科技_300767_analysis.docx deleted file mode 100644 index 75bd2fe..0000000 Binary files a/src/fundamentals_llm/reports/震安科技_300767_analysis.docx and /dev/null differ diff --git a/src/fundamentals_llm/reports/震安科技_300767_analysis.pdf b/src/fundamentals_llm/reports/震安科技_300767_analysis.pdf deleted file mode 100644 index 8516581..0000000 Binary files a/src/fundamentals_llm/reports/震安科技_300767_analysis.pdf and /dev/null differ diff --git a/src/quantitative_analysis/0020.HK.csv b/src/quantitative_analysis/0020.HK.csv new file mode 100644 index 0000000..dd43f0e --- /dev/null +++ b/src/quantitative_analysis/0020.HK.csv @@ -0,0 +1,724 @@ +代码,日期,开盘价(元),最高价(元),最低价(元),收盘价(元),涨跌幅 +0020.HK,2022-07-05,2.4,2.78,2.38,2.75,0.0827 +0020.HK,2022-07-06,2.77,2.79,2.53,2.6,-0.0545 +0020.HK,2022-07-07,2.56,2.68,2.56,2.6,0 +0020.HK,2022-07-08,2.64,2.69,2.54,2.58,-0.0077 +0020.HK,2022-07-11,2.6,2.6,2.52,2.54,-0.0155 +0020.HK,2022-07-12,2.5,2.52,2.4,2.43,-0.0433 +0020.HK,2022-07-13,2.45,2.48,2.38,2.39,-0.0165 +0020.HK,2022-07-14,2.36,2.38,2.25,2.29,-0.0418 +0020.HK,2022-07-15,2.28,2.36,2.15,2.16,-0.0568 +0020.HK,2022-07-18,2.13,2.23,2.11,2.14,-0.0093 +0020.HK,2022-07-19,2.14,2.17,2.04,2.1,-0.0187 +0020.HK,2022-07-20,2.18,2.42,2.17,2.33,0.1095 +0020.HK,2022-07-21,2.33,2.46,2.29,2.39,0.0258 +0020.HK,2022-07-22,2.41,2.49,2.39,2.43,0.0167 +0020.HK,2022-07-25,2.41,2.43,2.34,2.4,-0.0123 +0020.HK,2022-07-26,2.31,2.34,2.19,2.27,-0.0542 +0020.HK,2022-07-27,2.23,2.37,2.22,2.34,0.0308 +0020.HK,2022-07-28,2.38,2.5,2.37,2.47,0.0556 +0020.HK,2022-07-29,2.5,2.52,2.3,2.33,-0.0567 +0020.HK,2022-08-01,2.32,2.35,2.26,2.28,-0.0215 +0020.HK,2022-08-02,2.22,2.22,2.12,2.17,-0.0482 +0020.HK,2022-08-03,2.22,2.33,2.19,2.2,0.0138 +0020.HK,2022-08-04,2.24,2.25,2.18,2.2,0 +0020.HK,2022-08-05,2.23,2.24,2.18,2.23,0.0136 +0020.HK,2022-08-08,2.25,2.26,2.2,2.2,-0.0135 +0020.HK,2022-08-09,2.2,2.23,2.18,2.19,-0.0045 +0020.HK,2022-08-10,2.2,2.21,2.14,2.16,-0.0137 +0020.HK,2022-08-11,2.19,2.21,2.16,2.19,0.0139 +0020.HK,2022-08-12,2.19,2.29,2.19,2.23,0.0183 +0020.HK,2022-08-15,2.26,2.29,2.23,2.25,0.009 +0020.HK,2022-08-16,2.25,2.28,2.18,2.2,-0.0222 +0020.HK,2022-08-17,2.2,2.22,2.16,2.16,-0.0182 +0020.HK,2022-08-18,2.18,2.18,2.06,2.08,-0.037 +0020.HK,2022-08-19,2.09,2.13,2.06,2.1,0.0096 +0020.HK,2022-08-22,2.15,2.22,2.12,2.19,0.0429 +0020.HK,2022-08-23,2.18,2.23,2.16,2.2,0.0046 +0020.HK,2022-08-24,2.21,2.3,2.21,2.24,0.0182 +0020.HK,2022-08-25,2.25,2.35,2.24,2.35,0.0491 +0020.HK,2022-08-26,2.34,2.34,2.26,2.29,-0.0255 +0020.HK,2022-08-29,2.23,2.32,2.21,2.29,0 +0020.HK,2022-08-30,2.29,2.34,2.27,2.31,0.0087 +0020.HK,2022-08-31,2.28,2.49,2.26,2.47,0.0693 +0020.HK,2022-09-01,2.47,2.49,2.36,2.39,-0.0324 +0020.HK,2022-09-02,2.38,2.39,2.25,2.26,-0.0544 +0020.HK,2022-09-05,2.25,2.35,2.25,2.29,0.0133 +0020.HK,2022-09-06,2.29,2.35,2.29,2.32,0.0131 +0020.HK,2022-09-07,2.29,2.31,2.26,2.29,-0.0129 +0020.HK,2022-09-08,2.3,2.35,2.26,2.29,0 +0020.HK,2022-09-09,2.28,2.35,2.28,2.28,-0.0044 +0020.HK,2022-09-13,2.3,2.3,2.15,2.17,-0.0482 +0020.HK,2022-09-14,2.11,2.14,2.09,2.13,-0.0184 +0020.HK,2022-09-15,2.12,2.15,2.07,2.08,-0.0235 +0020.HK,2022-09-16,2.05,2.06,1.99,2,-0.0385 +0020.HK,2022-09-19,1.98,1.98,1.85,1.91,-0.045 +0020.HK,2022-09-20,1.94,1.98,1.92,1.94,0.0157 +0020.HK,2022-09-21,1.93,1.99,1.89,1.9,-0.0206 +0020.HK,2022-09-22,1.89,1.95,1.84,1.94,0.0211 +0020.HK,2022-09-23,1.94,1.99,1.91,1.94,0 +0020.HK,2022-09-26,1.91,1.96,1.84,1.87,-0.0361 +0020.HK,2022-09-27,1.86,1.87,1.81,1.84,-0.016 +0020.HK,2022-09-28,1.82,1.83,1.72,1.73,-0.0598 +0020.HK,2022-09-29,1.78,1.79,1.58,1.63,-0.0578 +0020.HK,2022-09-30,1.63,1.67,1.57,1.6,-0.0184 +0020.HK,2022-10-03,1.57,1.64,1.56,1.61,0.0063 +0020.HK,2022-10-05,1.67,1.77,1.67,1.72,0.0683 +0020.HK,2022-10-06,1.71,1.75,1.65,1.66,-0.0349 +0020.HK,2022-10-07,1.66,1.66,1.58,1.59,-0.0422 +0020.HK,2022-10-10,1.54,1.56,1.49,1.5,-0.0566 +0020.HK,2022-10-11,1.5,1.53,1.32,1.33,-0.1133 +0020.HK,2022-10-12,1.33,1.34,1.2,1.28,-0.0376 +0020.HK,2022-10-13,1.3,1.32,1.2,1.21,-0.0547 +0020.HK,2022-10-14,1.25,1.28,1.21,1.24,0.0248 +0020.HK,2022-10-17,1.21,1.29,1.2,1.27,0.0242 +0020.HK,2022-10-18,1.3,1.37,1.27,1.35,0.063 +0020.HK,2022-10-19,1.34,1.34,1.27,1.29,-0.0444 +0020.HK,2022-10-20,1.25,1.31,1.22,1.28,-0.0078 +0020.HK,2022-10-21,1.29,1.31,1.25,1.26,-0.0156 +0020.HK,2022-10-24,1.26,1.29,1.13,1.17,-0.0714 +0020.HK,2022-10-25,1.2,1.26,1.11,1.21,0.0342 +0020.HK,2022-10-26,1.21,1.3,1.2,1.29,0.0661 +0020.HK,2022-10-27,1.34,1.35,1.24,1.26,-0.0233 +0020.HK,2022-10-28,1.26,1.28,1.18,1.19,-0.0556 +0020.HK,2022-10-31,1.2,1.26,1.18,1.19,0 +0020.HK,2022-11-01,1.21,1.29,1.18,1.28,0.0756 +0020.HK,2022-11-02,1.28,1.33,1.25,1.28,0 +0020.HK,2022-11-03,1.25,1.3,1.23,1.24,-0.0313 +0020.HK,2022-11-04,1.26,1.38,1.25,1.33,0.0726 +0020.HK,2022-11-07,1.34,1.8,1.33,1.8,0.3534 +0020.HK,2022-11-08,1.84,2.04,1.72,1.75,-0.0278 +0020.HK,2022-11-09,1.78,1.89,1.65,1.69,-0.0343 +0020.HK,2022-11-10,1.64,1.65,1.53,1.57,-0.071 +0020.HK,2022-11-11,1.78,1.82,1.65,1.7,0.0828 +0020.HK,2022-11-14,1.76,1.87,1.71,1.75,0.0294 +0020.HK,2022-11-15,1.69,1.85,1.63,1.79,0.0229 +0020.HK,2022-11-16,1.78,2.1,1.76,2.02,0.1285 +0020.HK,2022-11-17,2,2.2,1.88,2.13,0.0545 +0020.HK,2022-11-18,2.18,2.24,2.01,2.01,-0.0563 +0020.HK,2022-11-21,1.98,1.98,1.89,1.94,-0.0348 +0020.HK,2022-11-22,1.94,2.02,1.87,1.87,-0.0361 +0020.HK,2022-11-23,1.9,1.96,1.81,1.84,-0.016 +0020.HK,2022-11-24,1.87,1.89,1.81,1.85,0.0054 +0020.HK,2022-11-25,1.86,1.86,1.74,1.77,-0.0432 +0020.HK,2022-11-28,1.69,1.75,1.65,1.73,-0.0226 +0020.HK,2022-11-29,1.78,1.85,1.75,1.83,0.0578 +0020.HK,2022-11-30,1.86,1.97,1.84,1.91,0.0437 +0020.HK,2022-12-01,2,2.02,1.93,1.94,0.0157 +0020.HK,2022-12-02,1.95,2.02,1.91,2.01,0.0361 +0020.HK,2022-12-05,2.08,2.17,2.05,2.13,0.0597 +0020.HK,2022-12-06,2.08,2.13,2.04,2.06,-0.0329 +0020.HK,2022-12-07,2.06,2.46,2.05,2.2,0.068 +0020.HK,2022-12-08,2.26,2.39,2.19,2.37,0.0773 +0020.HK,2022-12-09,2.38,2.47,2.28,2.4,0.0127 +0020.HK,2022-12-12,2.34,2.54,2.25,2.28,-0.05 +0020.HK,2022-12-13,2.31,2.37,2.26,2.3,0.0088 +0020.HK,2022-12-14,2.31,2.35,2.19,2.27,-0.013 +0020.HK,2022-12-15,2.27,2.27,2.11,2.14,-0.0573 +0020.HK,2022-12-16,2.12,2.16,2.05,2.12,-0.0093 +0020.HK,2022-12-19,2.13,2.2,2.1,2.13,0.0047 +0020.HK,2022-12-20,2.1,2.12,2.05,2.09,-0.0188 +0020.HK,2022-12-21,2.11,2.16,2.11,2.16,0.0335 +0020.HK,2022-12-22,2.23,2.24,2.17,2.21,0.0231 +0020.HK,2022-12-23,2.16,2.21,2.14,2.16,-0.0226 +0020.HK,2022-12-28,2.18,2.31,2.18,2.29,0.0602 +0020.HK,2022-12-29,2.23,2.29,2.17,2.18,-0.048 +0020.HK,2022-12-30,2.25,2.25,2.18,2.22,0.0183 +0020.HK,2023-01-03,2.19,2.22,2.08,2.19,-0.0135 +0020.HK,2023-01-04,2.21,2.3,2.2,2.3,0.0502 +0020.HK,2023-01-05,2.35,2.37,2.26,2.26,-0.0174 +0020.HK,2023-01-06,2.27,2.28,2.11,2.15,-0.0487 +0020.HK,2023-01-09,2.18,2.23,2.15,2.19,0.0186 +0020.HK,2023-01-10,2.19,2.21,2.14,2.21,0.0091 +0020.HK,2023-01-11,2.23,2.34,2.18,2.22,0.0045 +0020.HK,2023-01-12,2.25,2.25,2.14,2.16,-0.027 +0020.HK,2023-01-13,2.17,2.22,2.15,2.22,0.0278 +0020.HK,2023-01-16,2.23,2.25,2.17,2.18,-0.018 +0020.HK,2023-01-17,2.18,2.19,2.11,2.13,-0.0229 +0020.HK,2023-01-18,2.11,2.16,2.1,2.13,0 +0020.HK,2023-01-19,2.11,2.13,2.08,2.11,-0.0094 +0020.HK,2023-01-20,2.1,2.17,2.1,2.17,0.0284 +0020.HK,2023-01-26,2.2,2.27,2.19,2.26,0.0415 +0020.HK,2023-01-27,2.34,2.74,2.31,2.71,0.1991 +0020.HK,2023-01-30,2.72,2.85,2.59,2.75,0.0148 +0020.HK,2023-01-31,2.78,2.84,2.62,2.84,0.0327 +0020.HK,2023-02-01,2.89,3.1,2.86,2.94,0.0352 +0020.HK,2023-02-02,3,3.07,2.71,2.73,-0.0714 +0020.HK,2023-02-03,2.74,2.88,2.74,2.82,0.033 +0020.HK,2023-02-06,2.79,3.03,2.75,3,0.0638 +0020.HK,2023-02-07,2.99,3.06,2.86,2.88,-0.04 +0020.HK,2023-02-08,2.87,2.88,2.68,2.69,-0.066 +0020.HK,2023-02-09,2.65,2.8,2.61,2.78,0.0335 +0020.HK,2023-02-10,2.75,2.81,2.68,2.69,-0.0324 +0020.HK,2023-02-13,2.66,2.71,2.6,2.67,-0.0074 +0020.HK,2023-02-14,2.73,2.79,2.7,2.74,0.0262 +0020.HK,2023-02-15,2.78,2.86,2.69,2.73,-0.0036 +0020.HK,2023-02-16,2.76,2.82,2.63,2.65,-0.0293 +0020.HK,2023-02-17,2.66,2.7,2.48,2.5,-0.0566 +0020.HK,2023-02-20,2.5,2.56,2.46,2.48,-0.008 +0020.HK,2023-02-21,2.5,2.57,2.48,2.49,0.004 +0020.HK,2023-02-22,2.46,2.49,2.37,2.42,-0.0281 +0020.HK,2023-02-23,2.44,2.46,2.38,2.4,-0.0083 +0020.HK,2023-02-24,2.4,2.56,2.39,2.51,0.0458 +0020.HK,2023-02-27,2.48,2.58,2.46,2.54,0.012 +0020.HK,2023-02-28,2.58,2.61,2.46,2.52,-0.0079 +0020.HK,2023-03-01,2.54,2.73,2.54,2.71,0.0754 +0020.HK,2023-03-02,2.69,2.74,2.66,2.71,0 +0020.HK,2023-03-03,2.74,2.76,2.63,2.69,-0.0074 +0020.HK,2023-03-06,2.67,2.71,2.58,2.66,-0.0112 +0020.HK,2023-03-07,2.65,2.7,2.5,2.53,-0.0489 +0020.HK,2023-03-08,2.5,2.51,2.4,2.5,-0.0119 +0020.HK,2023-03-09,2.5,2.54,2.45,2.51,0.004 +0020.HK,2023-03-10,2.45,2.54,2.44,2.51,0 +0020.HK,2023-03-13,2.52,2.59,2.47,2.52,0.004 +0020.HK,2023-03-14,2.53,2.59,2.45,2.48,-0.0159 +0020.HK,2023-03-15,2.56,2.6,2.5,2.55,0.0282 +0020.HK,2023-03-16,2.52,2.58,2.44,2.47,-0.0314 +0020.HK,2023-03-17,2.54,2.73,2.52,2.73,0.1053 +0020.HK,2023-03-20,2.72,2.74,2.61,2.66,-0.0256 +0020.HK,2023-03-21,2.7,2.7,2.63,2.68,0.0075 +0020.HK,2023-03-22,2.7,2.74,2.67,2.68,0 +0020.HK,2023-03-23,2.7,2.89,2.68,2.88,0.0746 +0020.HK,2023-03-24,2.87,2.92,2.77,2.9,0.0069 +0020.HK,2023-03-27,2.95,2.98,2.8,2.81,-0.031 +0020.HK,2023-03-28,2.81,2.83,2.64,2.68,-0.0463 +0020.HK,2023-03-29,2.72,2.76,2.56,2.62,-0.0224 +0020.HK,2023-03-30,2.64,2.77,2.6,2.64,0.0076 +0020.HK,2023-03-31,2.68,2.68,2.58,2.66,0.0076 +0020.HK,2023-04-03,2.65,2.9,2.65,2.89,0.0865 +0020.HK,2023-04-04,3.12,3.4,3.06,3.26,0.128 +0020.HK,2023-04-06,3.13,3.5,3.11,3.33,0.0215 +0020.HK,2023-04-11,3.62,3.7,3.13,3.3,-0.009 +0020.HK,2023-04-12,3.34,3.52,3.1,3.16,-0.0424 +0020.HK,2023-04-13,3.11,3.17,2.9,2.92,-0.0759 +0020.HK,2023-04-14,2.97,2.99,2.8,2.93,0.0034 +0020.HK,2023-04-17,2.92,2.93,2.76,2.81,-0.041 +0020.HK,2023-04-18,2.79,2.82,2.7,2.77,-0.0142 +0020.HK,2023-04-19,2.76,2.83,2.71,2.73,-0.0144 +0020.HK,2023-04-20,2.73,2.8,2.71,2.73,0 +0020.HK,2023-04-21,2.72,2.74,2.38,2.42,-0.1136 +0020.HK,2023-04-24,2.42,2.54,2.4,2.48,0.0248 +0020.HK,2023-04-25,2.48,2.5,2.38,2.42,-0.0242 +0020.HK,2023-04-26,2.4,2.47,2.4,2.44,0.0083 +0020.HK,2023-04-27,2.45,2.5,2.4,2.48,0.0164 +0020.HK,2023-04-28,2.51,2.63,2.5,2.6,0.0484 +0020.HK,2023-05-02,2.61,2.67,2.54,2.57,-0.0115 +0020.HK,2023-05-03,2.52,2.59,2.49,2.57,0 +0020.HK,2023-05-04,2.58,2.64,2.56,2.6,0.0117 +0020.HK,2023-05-05,2.6,2.65,2.56,2.59,-0.0038 +0020.HK,2023-05-08,2.5,2.51,2.4,2.43,-0.0618 +0020.HK,2023-05-09,2.43,2.43,2.22,2.27,-0.0658 +0020.HK,2023-05-10,2.28,2.35,2.27,2.31,0.0176 +0020.HK,2023-05-11,2.31,2.32,2.26,2.28,-0.013 +0020.HK,2023-05-12,2.31,2.34,2.28,2.28,0 +0020.HK,2023-05-15,2.25,2.29,2.19,2.25,-0.0132 +0020.HK,2023-05-16,2.28,2.29,2.2,2.22,-0.0133 +0020.HK,2023-05-17,2.23,2.25,2.16,2.17,-0.0225 +0020.HK,2023-05-18,2.2,2.3,2.18,2.24,0.0323 +0020.HK,2023-05-19,2.24,2.25,2.19,2.21,-0.0134 +0020.HK,2023-05-22,2.21,2.26,2.21,2.23,0.009 +0020.HK,2023-05-23,2.25,2.26,2.19,2.19,-0.0179 +0020.HK,2023-05-24,2.18,2.18,2.07,2.08,-0.0502 +0020.HK,2023-05-25,2.07,2.13,2.04,2.1,0.0096 +0020.HK,2023-05-29,2.15,2.22,2.12,2.15,0.0238 +0020.HK,2023-05-30,2.15,2.2,2.1,2.19,0.0186 +0020.HK,2023-05-31,2.16,2.18,2.06,2.1,-0.0411 +0020.HK,2023-06-01,2.1,2.21,2.07,2.12,0.0095 +0020.HK,2023-06-02,2.2,2.23,2.16,2.2,0.0377 +0020.HK,2023-06-05,2.24,2.26,2.16,2.18,-0.0091 +0020.HK,2023-06-06,2.16,2.2,2.1,2.12,-0.0275 +0020.HK,2023-06-07,2.18,2.18,2.12,2.14,0.0094 +0020.HK,2023-06-08,2.12,2.14,2.08,2.11,-0.014 +0020.HK,2023-06-09,2.12,2.19,2.07,2.18,0.0332 +0020.HK,2023-06-12,2.18,2.2,2.15,2.17,-0.0046 +0020.HK,2023-06-13,2.16,2.34,2.15,2.33,0.0737 +0020.HK,2023-06-14,2.36,2.36,2.23,2.26,-0.03 +0020.HK,2023-06-15,2.29,2.32,2.24,2.28,0.0088 +0020.HK,2023-06-16,2.29,2.33,2.26,2.29,0.0044 +0020.HK,2023-06-19,2.27,2.36,2.22,2.28,-0.0044 +0020.HK,2023-06-20,2.28,2.3,2.21,2.25,-0.0132 +0020.HK,2023-06-21,2.22,2.24,2.1,2.11,-0.0622 +0020.HK,2023-06-23,2.14,2.14,2.08,2.1,-0.0047 +0020.HK,2023-06-26,2.1,2.14,2.05,2.13,0.0143 +0020.HK,2023-06-27,2.13,2.16,2.12,2.13,0 +0020.HK,2023-06-28,2.13,2.16,2.07,2.15,0.0094 +0020.HK,2023-06-29,2.15,2.16,2.09,2.11,-0.0186 +0020.HK,2023-06-30,2.11,2.12,2.07,2.07,-0.019 +0020.HK,2023-07-03,2.1,2.23,2.09,2.18,0.0531 +0020.HK,2023-07-04,2.16,2.16,1.97,1.99,-0.0872 +0020.HK,2023-07-05,1.99,2,1.87,1.9,-0.0452 +0020.HK,2023-07-06,1.9,1.94,1.86,1.89,-0.0053 +0020.HK,2023-07-07,1.89,1.92,1.86,1.87,-0.0106 +0020.HK,2023-07-10,1.91,1.93,1.87,1.87,0 +0020.HK,2023-07-11,1.89,1.89,1.78,1.79,-0.0428 +0020.HK,2023-07-12,1.8,1.85,1.78,1.79,0 +0020.HK,2023-07-13,1.83,1.94,1.81,1.93,0.0782 +0020.HK,2023-07-14,1.94,1.96,1.89,1.92,-0.0052 +0020.HK,2023-07-18,1.9,1.9,1.8,1.8,-0.0625 +0020.HK,2023-07-19,1.8,1.83,1.78,1.79,-0.0056 +0020.HK,2023-07-20,1.8,1.82,1.68,1.69,-0.0559 +0020.HK,2023-07-21,1.64,1.84,1.61,1.72,0.0178 +0020.HK,2023-07-24,1.72,1.75,1.67,1.68,-0.0233 +0020.HK,2023-07-25,1.73,1.79,1.71,1.77,0.0536 +0020.HK,2023-07-26,1.77,1.79,1.73,1.76,-0.0056 +0020.HK,2023-07-27,1.77,1.81,1.76,1.8,0.0227 +0020.HK,2023-07-28,1.76,1.84,1.75,1.82,0.0111 +0020.HK,2023-07-31,1.85,1.91,1.84,1.85,0.0165 +0020.HK,2023-08-01,1.87,1.88,1.8,1.82,-0.0162 +0020.HK,2023-08-02,1.8,1.83,1.75,1.76,-0.033 +0020.HK,2023-08-03,1.75,1.79,1.74,1.76,0 +0020.HK,2023-08-04,1.79,1.81,1.75,1.76,0 +0020.HK,2023-08-07,1.74,1.74,1.69,1.7,-0.0341 +0020.HK,2023-08-08,1.68,1.72,1.67,1.68,-0.0118 +0020.HK,2023-08-09,1.67,1.7,1.66,1.69,0.006 +0020.HK,2023-08-10,1.68,1.68,1.62,1.65,-0.0237 +0020.HK,2023-08-11,1.65,1.65,1.56,1.59,-0.0364 +0020.HK,2023-08-14,1.54,1.62,1.5,1.61,0.0126 +0020.HK,2023-08-15,1.61,1.62,1.55,1.57,-0.0248 +0020.HK,2023-08-16,1.55,1.57,1.51,1.52,-0.0318 +0020.HK,2023-08-17,1.5,1.56,1.5,1.55,0.0197 +0020.HK,2023-08-18,1.55,1.56,1.5,1.51,-0.0258 +0020.HK,2023-08-21,1.51,1.52,1.48,1.49,-0.0132 +0020.HK,2023-08-22,1.49,1.53,1.47,1.52,0.0201 +0020.HK,2023-08-23,1.52,1.53,1.48,1.5,-0.0132 +0020.HK,2023-08-24,1.51,1.58,1.5,1.57,0.0467 +0020.HK,2023-08-25,1.54,1.55,1.51,1.53,-0.0255 +0020.HK,2023-08-28,1.58,1.6,1.5,1.5,-0.0196 +0020.HK,2023-08-29,1.51,1.55,1.37,1.54,0.0267 +0020.HK,2023-08-30,1.57,1.58,1.48,1.51,-0.0195 +0020.HK,2023-08-31,1.56,1.62,1.53,1.56,0.0331 +0020.HK,2023-09-04,1.58,1.71,1.57,1.68,0.0769 +0020.HK,2023-09-05,1.68,1.68,1.55,1.6,-0.0476 +0020.HK,2023-09-06,1.59,1.61,1.54,1.59,-0.0063 +0020.HK,2023-09-07,1.58,1.58,1.5,1.51,-0.0503 +0020.HK,2023-09-11,1.48,1.55,1.45,1.55,0.0265 +0020.HK,2023-09-12,1.55,1.55,1.49,1.5,-0.0323 +0020.HK,2023-09-13,1.51,1.52,1.47,1.48,-0.0133 +0020.HK,2023-09-14,1.49,1.5,1.47,1.48,0 +0020.HK,2023-09-15,1.49,1.51,1.43,1.49,0.0068 +0020.HK,2023-09-18,1.47,1.49,1.45,1.46,-0.0201 +0020.HK,2023-09-19,1.45,1.47,1.43,1.44,-0.0137 +0020.HK,2023-09-20,1.43,1.43,1.4,1.41,-0.0208 +0020.HK,2023-09-21,1.4,1.42,1.37,1.38,-0.0213 +0020.HK,2023-09-22,1.37,1.45,1.36,1.45,0.0507 +0020.HK,2023-09-25,1.46,1.47,1.42,1.42,-0.0207 +0020.HK,2023-09-26,1.43,1.46,1.4,1.42,0 +0020.HK,2023-09-27,1.43,1.44,1.38,1.39,-0.0211 +0020.HK,2023-09-28,1.39,1.42,1.37,1.37,-0.0144 +0020.HK,2023-09-29,1.39,1.44,1.38,1.43,0.0438 +0020.HK,2023-10-03,1.41,1.42,1.35,1.37,-0.042 +0020.HK,2023-10-04,1.37,1.37,1.31,1.32,-0.0365 +0020.HK,2023-10-05,1.33,1.37,1.32,1.34,0.0152 +0020.HK,2023-10-06,1.36,1.43,1.35,1.43,0.0672 +0020.HK,2023-10-09,1.46,1.46,1.4,1.41,-0.014 +0020.HK,2023-10-10,1.41,1.45,1.41,1.42,0.0071 +0020.HK,2023-10-11,1.45,1.49,1.43,1.47,0.0352 +0020.HK,2023-10-12,1.5,1.5,1.46,1.48,0.0068 +0020.HK,2023-10-13,1.46,1.47,1.43,1.44,-0.027 +0020.HK,2023-10-16,1.44,1.45,1.4,1.43,-0.0069 +0020.HK,2023-10-17,1.45,1.45,1.4,1.43,0 +0020.HK,2023-10-18,1.42,1.43,1.39,1.41,-0.014 +0020.HK,2023-10-19,1.4,1.43,1.38,1.4,-0.0071 +0020.HK,2023-10-20,1.4,1.4,1.37,1.38,-0.0143 +0020.HK,2023-10-24,1.38,1.39,1.34,1.35,-0.0217 +0020.HK,2023-10-25,1.4,1.43,1.37,1.39,0.0296 +0020.HK,2023-10-26,1.38,1.41,1.36,1.38,-0.0072 +0020.HK,2023-10-27,1.39,1.42,1.37,1.4,0.0145 +0020.HK,2023-10-30,1.4,1.46,1.4,1.42,0.0143 +0020.HK,2023-10-31,1.42,1.43,1.38,1.4,-0.0141 +0020.HK,2023-11-01,1.4,1.41,1.38,1.4,0 +0020.HK,2023-11-02,1.41,1.43,1.38,1.39,-0.0071 +0020.HK,2023-11-03,1.41,1.46,1.39,1.42,0.0216 +0020.HK,2023-11-06,1.45,1.54,1.43,1.53,0.0775 +0020.HK,2023-11-07,1.51,1.56,1.5,1.54,0.0065 +0020.HK,2023-11-08,1.55,1.57,1.52,1.53,-0.0065 +0020.HK,2023-11-09,1.53,1.56,1.48,1.49,-0.0261 +0020.HK,2023-11-10,1.47,1.48,1.43,1.44,-0.0336 +0020.HK,2023-11-13,1.46,1.54,1.46,1.53,0.0625 +0020.HK,2023-11-14,1.53,1.55,1.5,1.54,0.0065 +0020.HK,2023-11-15,1.58,1.6,1.53,1.56,0.013 +0020.HK,2023-11-16,1.57,1.57,1.51,1.52,-0.0256 +0020.HK,2023-11-17,1.5,1.52,1.47,1.48,-0.0263 +0020.HK,2023-11-20,1.5,1.55,1.48,1.55,0.0473 +0020.HK,2023-11-21,1.57,1.57,1.48,1.5,-0.0323 +0020.HK,2023-11-22,1.49,1.5,1.47,1.47,-0.02 +0020.HK,2023-11-23,1.48,1.53,1.46,1.52,0.034 +0020.HK,2023-11-24,1.51,1.52,1.46,1.46,-0.0395 +0020.HK,2023-11-27,1.47,1.47,1.43,1.44,-0.0137 +0020.HK,2023-11-28,1.44,1.45,1.3,1.37,-0.0486 +0020.HK,2023-11-29,1.37,1.38,1.33,1.36,-0.0073 +0020.HK,2023-11-30,1.36,1.37,1.33,1.36,0 +0020.HK,2023-12-01,1.36,1.39,1.34,1.38,0.0147 +0020.HK,2023-12-04,1.39,1.4,1.34,1.36,-0.0145 +0020.HK,2023-12-05,1.35,1.36,1.31,1.32,-0.0294 +0020.HK,2023-12-06,1.32,1.34,1.25,1.29,-0.0227 +0020.HK,2023-12-07,1.28,1.3,1.25,1.28,-0.0078 +0020.HK,2023-12-08,1.3,1.33,1.27,1.29,0.0078 +0020.HK,2023-12-11,1.29,1.29,1.23,1.25,-0.031 +0020.HK,2023-12-12,1.26,1.27,1.23,1.24,-0.008 +0020.HK,2023-12-13,1.24,1.24,1.2,1.22,-0.0161 +0020.HK,2023-12-14,1.24,1.25,1.21,1.22,0 +0020.HK,2023-12-15,1.24,1.29,1.24,1.26,0.0328 +0020.HK,2023-12-18,1.08,1.15,1.03,1.12,-0.1111 +0020.HK,2023-12-19,1.12,1.17,1.11,1.12,0 +0020.HK,2023-12-20,1.13,1.18,1.13,1.15,0.0268 +0020.HK,2023-12-21,1.14,1.17,1.13,1.15,0 +0020.HK,2023-12-22,1.17,1.17,1.08,1.08,-0.0609 +0020.HK,2023-12-27,1.09,1.12,1.06,1.09,0.0093 +0020.HK,2023-12-28,1.1,1.17,1.09,1.15,0.055 +0020.HK,2023-12-29,1.15,1.17,1.14,1.16,0.0087 +0020.HK,2024-01-02,1.18,1.19,1.14,1.16,0 +0020.HK,2024-01-03,1.14,1.16,1.11,1.12,-0.0345 +0020.HK,2024-01-04,1.12,1.13,1.09,1.1,-0.0179 +0020.HK,2024-01-05,1.1,1.12,1.08,1.09,-0.0091 +0020.HK,2024-01-08,1.09,1.09,1.02,1.03,-0.055 +0020.HK,2024-01-09,1.04,1.07,1.03,1.05,0.0194 +0020.HK,2024-01-10,1.05,1.06,1.02,1.02,-0.0286 +0020.HK,2024-01-11,1.03,1.07,1.02,1.05,0.0294 +0020.HK,2024-01-12,1.05,1.07,1.04,1.05,0 +0020.HK,2024-01-15,1.05,1.06,1.03,1.06,0.0095 +0020.HK,2024-01-16,1.05,1.07,1.03,1.03,-0.0283 +0020.HK,2024-01-17,1.03,1.03,0.91,0.91,-0.1165 +0020.HK,2024-01-18,0.91,0.94,0.89,0.91,0 +0020.HK,2024-01-19,0.91,0.93,0.87,0.89,-0.022 +0020.HK,2024-01-22,0.89,0.9,0.82,0.83,-0.0674 +0020.HK,2024-01-23,0.84,0.93,0.83,0.91,0.0964 +0020.HK,2024-01-24,0.92,0.93,0.88,0.92,0.011 +0020.HK,2024-01-25,0.93,0.93,0.89,0.92,0 +0020.HK,2024-01-26,0.91,0.92,0.87,0.87,-0.0543 +0020.HK,2024-01-29,0.87,0.9,0.86,0.87,0 +0020.HK,2024-01-30,0.87,0.87,0.83,0.84,-0.0345 +0020.HK,2024-01-31,0.83,0.85,0.78,0.79,-0.0595 +0020.HK,2024-02-01,0.8,0.83,0.79,0.79,0 +0020.HK,2024-02-02,0.81,0.84,0.76,0.78,-0.0127 +0020.HK,2024-02-05,0.77,0.82,0.76,0.78,0 +0020.HK,2024-02-06,0.78,0.88,0.78,0.86,0.1026 +0020.HK,2024-02-07,0.88,0.88,0.82,0.83,-0.0349 +0020.HK,2024-02-08,0.83,0.88,0.83,0.86,0.0361 +0020.HK,2024-02-09,0.85,0.85,0.81,0.82,-0.0465 +0020.HK,2024-02-14,0.81,0.85,0.78,0.8,-0.0244 +0020.HK,2024-02-15,0.81,0.82,0.78,0.8,0 +0020.HK,2024-02-16,0.8,0.86,0.8,0.85,0.0625 +0020.HK,2024-02-19,0.88,0.91,0.85,0.86,0.0118 +0020.HK,2024-02-20,0.86,0.87,0.83,0.85,-0.0116 +0020.HK,2024-02-21,0.84,0.94,0.83,0.91,0.0706 +0020.HK,2024-02-22,0.92,0.93,0.9,0.93,0.022 +0020.HK,2024-02-23,0.93,0.96,0.91,0.93,0 +0020.HK,2024-02-26,0.93,0.96,0.91,0.92,-0.0108 +0020.HK,2024-02-27,0.92,0.97,0.88,0.96,0.0435 +0020.HK,2024-02-28,0.97,0.99,0.89,0.9,-0.0625 +0020.HK,2024-02-29,0.89,0.94,0.89,0.9,0 +0020.HK,2024-03-01,0.9,0.91,0.88,0.89,-0.0111 +0020.HK,2024-03-04,0.9,0.93,0.89,0.89,0 +0020.HK,2024-03-05,0.88,0.89,0.82,0.83,-0.0674 +0020.HK,2024-03-06,0.84,0.86,0.82,0.84,0.012 +0020.HK,2024-03-07,0.84,0.89,0.83,0.83,-0.0119 +0020.HK,2024-03-08,0.84,0.87,0.83,0.86,0.0361 +0020.HK,2024-03-11,0.87,0.91,0.86,0.91,0.0581 +0020.HK,2024-03-12,0.92,0.93,0.9,0.92,0.011 +0020.HK,2024-03-13,0.92,0.94,0.88,0.89,-0.0326 +0020.HK,2024-03-14,0.89,0.9,0.84,0.85,-0.0449 +0020.HK,2024-03-15,0.84,0.85,0.82,0.84,-0.0118 +0020.HK,2024-03-18,0.85,0.85,0.83,0.84,0 +0020.HK,2024-03-19,0.84,0.85,0.82,0.82,-0.0238 +0020.HK,2024-03-20,0.82,0.84,0.82,0.82,0 +0020.HK,2024-03-21,0.84,0.87,0.83,0.84,0.0244 +0020.HK,2024-03-22,0.84,0.85,0.8,0.8,-0.0476 +0020.HK,2024-03-25,0.81,0.81,0.78,0.78,-0.025 +0020.HK,2024-03-26,0.78,0.79,0.76,0.78,0 +0020.HK,2024-03-27,0.79,0.79,0.7,0.7,-0.1026 +0020.HK,2024-03-28,0.7,0.74,0.7,0.71,0.0143 +0020.HK,2024-04-02,0.71,0.74,0.71,0.73,0.0282 +0020.HK,2024-04-03,0.73,0.74,0.68,0.68,-0.0685 +0020.HK,2024-04-05,0.68,0.69,0.62,0.65,-0.0441 +0020.HK,2024-04-08,0.66,0.68,0.64,0.65,0 +0020.HK,2024-04-09,0.65,0.68,0.65,0.67,0.0308 +0020.HK,2024-04-10,0.68,0.7,0.67,0.68,0.0149 +0020.HK,2024-04-11,0.67,0.7,0.66,0.68,0 +0020.HK,2024-04-12,0.68,0.71,0.66,0.66,-0.0294 +0020.HK,2024-04-15,0.65,0.66,0.61,0.62,-0.0606 +0020.HK,2024-04-16,0.61,0.62,0.58,0.59,-0.0484 +0020.HK,2024-04-17,0.59,0.62,0.58,0.62,0.0508 +0020.HK,2024-04-18,0.62,0.63,0.6,0.61,-0.0161 +0020.HK,2024-04-19,0.61,0.61,0.58,0.58,-0.0492 +0020.HK,2024-04-22,0.58,0.61,0.58,0.6,0.0345 +0020.HK,2024-04-23,0.61,0.63,0.6,0.61,0.0167 +0020.HK,2024-04-24,0.63,0.83,0.62,0.8,0.3115 +0020.HK,2024-04-25,0.94,0.96,0.82,0.83,0.0375 +0020.HK,2024-04-26,0.84,1.23,0.84,1.19,0.4337 +0020.HK,2024-04-29,1.22,1.32,1.16,1.21,0.0168 +0020.HK,2024-04-30,1.25,1.28,1.11,1.22,0.0083 +0020.HK,2024-05-02,1.21,1.68,1.19,1.66,0.3607 +0020.HK,2024-05-03,1.76,1.76,1.54,1.6,-0.0361 +0020.HK,2024-05-06,1.58,1.73,1.51,1.68,0.05 +0020.HK,2024-05-07,1.7,1.77,1.63,1.65,-0.0179 +0020.HK,2024-05-08,1.68,1.74,1.38,1.41,-0.1455 +0020.HK,2024-05-09,1.42,1.49,1.4,1.45,0.0284 +0020.HK,2024-05-10,1.47,1.5,1.34,1.47,0.0138 +0020.HK,2024-05-13,1.48,1.55,1.42,1.46,-0.0068 +0020.HK,2024-05-14,1.5,1.54,1.42,1.45,-0.0068 +0020.HK,2024-05-16,1.47,1.48,1.37,1.38,-0.0483 +0020.HK,2024-05-17,1.39,1.44,1.36,1.4,0.0145 +0020.HK,2024-05-20,1.4,1.6,1.38,1.57,0.1214 +0020.HK,2024-05-21,1.56,1.59,1.5,1.5,-0.0446 +0020.HK,2024-05-22,1.52,1.53,1.46,1.48,-0.0133 +0020.HK,2024-05-23,1.5,1.55,1.47,1.48,0 +0020.HK,2024-05-24,1.49,1.52,1.37,1.4,-0.0541 +0020.HK,2024-05-27,1.4,1.42,1.29,1.37,-0.0214 +0020.HK,2024-05-28,1.39,1.4,1.3,1.31,-0.0438 +0020.HK,2024-05-29,1.31,1.46,1.27,1.37,0.0458 +0020.HK,2024-05-30,1.37,1.4,1.35,1.36,-0.0073 +0020.HK,2024-05-31,1.38,1.42,1.31,1.32,-0.0294 +0020.HK,2024-06-03,1.33,1.38,1.32,1.36,0.0303 +0020.HK,2024-06-04,1.36,1.42,1.35,1.39,0.0221 +0020.HK,2024-06-05,1.39,1.45,1.37,1.38,-0.0072 +0020.HK,2024-06-06,1.4,1.5,1.38,1.48,0.0725 +0020.HK,2024-06-07,1.49,1.51,1.44,1.45,-0.0203 +0020.HK,2024-06-11,1.44,1.45,1.38,1.4,-0.0345 +0020.HK,2024-06-12,1.39,1.43,1.37,1.4,0 +0020.HK,2024-06-13,1.42,1.43,1.37,1.38,-0.0143 +0020.HK,2024-06-14,1.37,1.39,1.33,1.35,-0.0217 +0020.HK,2024-06-17,1.34,1.36,1.32,1.33,-0.0148 +0020.HK,2024-06-18,1.34,1.38,1.32,1.34,0.0075 +0020.HK,2024-06-19,1.35,1.46,1.35,1.43,0.0672 +0020.HK,2024-06-20,1.44,1.45,1.32,1.32,-0.0769 +0020.HK,2024-06-21,1.26,1.37,1.23,1.36,0.0303 +0020.HK,2024-06-24,1.37,1.38,1.32,1.37,0.0074 +0020.HK,2024-06-25,1.37,1.39,1.36,1.37,0 +0020.HK,2024-06-26,1.37,1.44,1.33,1.4,0.0219 +0020.HK,2024-06-27,1.4,1.41,1.33,1.34,-0.0429 +0020.HK,2024-06-28,1.33,1.37,1.32,1.32,-0.0149 +0020.HK,2024-07-02,1.32,1.41,1.31,1.38,0.0455 +0020.HK,2024-07-03,1.41,1.62,1.41,1.62,0.1739 +0020.HK,2024-07-04,1.66,1.67,1.56,1.61,-0.0062 +0020.HK,2024-07-05,1.61,1.63,1.35,1.35,-0.1615 +0020.HK,2024-07-08,1.36,1.39,1.28,1.31,-0.0296 +0020.HK,2024-07-09,1.32,1.35,1.29,1.33,0.0153 +0020.HK,2024-07-10,1.34,1.37,1.33,1.33,0 +0020.HK,2024-07-11,1.34,1.36,1.34,1.35,0.015 +0020.HK,2024-07-12,1.36,1.39,1.35,1.38,0.0222 +0020.HK,2024-07-15,1.37,1.37,1.32,1.33,-0.0362 +0020.HK,2024-07-16,1.32,1.35,1.32,1.34,0.0075 +0020.HK,2024-07-17,1.34,1.36,1.33,1.34,0 +0020.HK,2024-07-18,1.34,1.35,1.29,1.31,-0.0224 +0020.HK,2024-07-19,1.29,1.31,1.28,1.3,-0.0076 +0020.HK,2024-07-22,1.3,1.31,1.19,1.26,-0.0308 +0020.HK,2024-07-23,1.26,1.26,1.19,1.2,-0.0476 +0020.HK,2024-07-24,1.21,1.23,1.15,1.16,-0.0333 +0020.HK,2024-07-25,1.15,1.18,1.13,1.16,0 +0020.HK,2024-07-26,1.17,1.22,1.15,1.17,0.0086 +0020.HK,2024-07-29,1.19,1.21,1.16,1.18,0.0085 +0020.HK,2024-07-30,1.18,1.18,1.13,1.14,-0.0339 +0020.HK,2024-07-31,1.14,1.22,1.13,1.21,0.0614 +0020.HK,2024-08-01,1.21,1.23,1.17,1.18,-0.0248 +0020.HK,2024-08-02,1.16,1.18,1.14,1.16,-0.0169 +0020.HK,2024-08-05,1.14,1.17,1.05,1.07,-0.0776 +0020.HK,2024-08-06,1.1,1.12,1.05,1.09,0.0187 +0020.HK,2024-08-07,1.1,1.12,1.08,1.09,0 +0020.HK,2024-08-08,1.08,1.1,1.05,1.08,-0.0092 +0020.HK,2024-08-09,1.1,1.12,1.09,1.11,0.0278 +0020.HK,2024-08-12,1.12,1.12,1.07,1.1,-0.009 +0020.HK,2024-08-13,1.11,1.11,1.08,1.1,0 +0020.HK,2024-08-14,1.11,1.12,1.07,1.07,-0.0273 +0020.HK,2024-08-15,1.07,1.15,1.06,1.11,0.0374 +0020.HK,2024-08-16,1.12,1.14,1.1,1.11,0 +0020.HK,2024-08-19,1.11,1.15,1.11,1.13,0.018 +0020.HK,2024-08-20,1.13,1.14,1.09,1.1,-0.0265 +0020.HK,2024-08-21,1.09,1.12,1.07,1.11,0.0091 +0020.HK,2024-08-22,1.11,1.13,1.08,1.09,-0.018 +0020.HK,2024-08-23,1.09,1.11,1.08,1.1,0.0092 +0020.HK,2024-08-26,1.11,1.17,1.1,1.17,0.0636 +0020.HK,2024-08-27,1.16,1.19,1.14,1.18,0.0085 +0020.HK,2024-08-28,1.16,1.17,1.1,1.12,-0.0508 +0020.HK,2024-08-29,1.1,1.18,1.08,1.16,0.0357 +0020.HK,2024-08-30,1.17,1.2,1.15,1.18,0.0172 +0020.HK,2024-09-02,1.17,1.18,1.13,1.14,-0.0339 +0020.HK,2024-09-03,1.14,1.16,1.13,1.14,0 +0020.HK,2024-09-04,1.13,1.13,1.09,1.11,-0.0263 +0020.HK,2024-09-05,1.11,1.14,1.11,1.13,0.018 +0020.HK,2024-09-09,1.12,1.13,1.08,1.09,-0.0354 +0020.HK,2024-09-10,1.1,1.11,1.07,1.09,0 +0020.HK,2024-09-11,1.08,1.09,1,1.03,-0.055 +0020.HK,2024-09-12,1.04,1.06,1.03,1.04,0.0097 +0020.HK,2024-09-13,1.04,1.08,1.04,1.04,0 +0020.HK,2024-09-16,1.05,1.07,1.03,1.07,0.0288 +0020.HK,2024-09-17,1.08,1.11,1.06,1.1,0.028 +0020.HK,2024-09-19,1.09,1.13,1.07,1.1,0 +0020.HK,2024-09-20,1.11,1.2,1.11,1.17,0.0636 +0020.HK,2024-09-23,1.17,1.21,1.16,1.19,0.0171 +0020.HK,2024-09-24,1.21,1.24,1.18,1.23,0.0336 +0020.HK,2024-09-25,1.26,1.33,1.24,1.25,0.0163 +0020.HK,2024-09-26,1.26,1.4,1.24,1.39,0.112 +0020.HK,2024-09-27,1.44,1.5,1.4,1.46,0.0504 +0020.HK,2024-09-30,1.51,1.75,1.51,1.72,0.1781 +0020.HK,2024-10-02,1.72,1.88,1.68,1.85,0.0756 +0020.HK,2024-10-03,1.87,1.9,1.63,1.77,-0.0432 +0020.HK,2024-10-04,1.74,2.15,1.73,2.12,0.1977 +0020.HK,2024-10-07,2.23,2.34,2.11,2.33,0.0991 +0020.HK,2024-10-08,2.33,2.35,1.82,1.83,-0.2146 +0020.HK,2024-10-09,1.89,1.99,1.65,1.74,-0.0492 +0020.HK,2024-10-10,1.82,1.83,1.68,1.73,-0.0057 +0020.HK,2024-10-14,1.7,1.71,1.55,1.62,-0.0636 +0020.HK,2024-10-15,1.62,1.68,1.5,1.53,-0.0556 +0020.HK,2024-10-16,1.49,1.55,1.47,1.5,-0.0196 +0020.HK,2024-10-17,1.52,1.61,1.5,1.52,0.0133 +0020.HK,2024-10-18,1.53,1.68,1.48,1.66,0.0921 +0020.HK,2024-10-21,1.68,1.69,1.59,1.59,-0.0422 +0020.HK,2024-10-22,1.6,1.68,1.58,1.64,0.0314 +0020.HK,2024-10-23,1.66,1.74,1.62,1.66,0.0122 +0020.HK,2024-10-24,1.63,1.66,1.58,1.59,-0.0422 +0020.HK,2024-10-25,1.59,1.65,1.58,1.6,0.0063 +0020.HK,2024-10-28,1.6,1.63,1.57,1.6,0 +0020.HK,2024-10-29,1.63,1.66,1.54,1.57,-0.0188 +0020.HK,2024-10-30,1.58,1.6,1.52,1.55,-0.0127 +0020.HK,2024-10-31,1.55,1.58,1.53,1.56,0.0065 +0020.HK,2024-11-01,1.56,1.57,1.48,1.52,-0.0256 +0020.HK,2024-11-04,1.52,1.55,1.51,1.53,0.0066 +0020.HK,2024-11-05,1.53,1.65,1.5,1.64,0.0719 +0020.HK,2024-11-06,1.64,1.69,1.59,1.62,-0.0122 +0020.HK,2024-11-07,1.62,1.74,1.59,1.74,0.0741 +0020.HK,2024-11-08,1.78,1.8,1.68,1.71,-0.0172 +0020.HK,2024-11-11,1.65,1.73,1.63,1.72,0.0058 +0020.HK,2024-11-12,1.74,1.75,1.6,1.61,-0.064 +0020.HK,2024-11-13,1.59,1.62,1.57,1.61,0 +0020.HK,2024-11-14,1.6,1.7,1.59,1.61,0 +0020.HK,2024-11-15,1.63,1.66,1.58,1.58,-0.0186 +0020.HK,2024-11-18,1.6,1.61,1.52,1.53,-0.0316 +0020.HK,2024-11-19,1.55,1.57,1.52,1.56,0.0196 +0020.HK,2024-11-20,1.56,1.6,1.54,1.58,0.0128 +0020.HK,2024-11-21,1.58,1.6,1.54,1.55,-0.019 +0020.HK,2024-11-22,1.55,1.59,1.42,1.43,-0.0774 +0020.HK,2024-11-25,1.44,1.46,1.39,1.44,0.007 +0020.HK,2024-11-26,1.44,1.48,1.42,1.42,-0.0139 +0020.HK,2024-11-27,1.43,1.5,1.4,1.48,0.0423 +0020.HK,2024-11-28,1.49,1.49,1.44,1.44,-0.027 +0020.HK,2024-11-29,1.45,1.52,1.45,1.49,0.0347 +0020.HK,2024-12-02,1.49,1.51,1.47,1.5,0.0067 +0020.HK,2024-12-03,1.5,1.51,1.46,1.5,0 +0020.HK,2024-12-04,1.51,1.53,1.47,1.49,-0.0067 +0020.HK,2024-12-05,1.49,1.52,1.48,1.49,0 +0020.HK,2024-12-06,1.49,1.74,1.47,1.71,0.1477 +0020.HK,2024-12-09,1.71,1.86,1.68,1.85,0.0819 +0020.HK,2024-12-10,1.91,1.92,1.6,1.6,-0.1351 +0020.HK,2024-12-11,1.58,1.64,1.54,1.58,-0.0125 +0020.HK,2024-12-12,1.59,1.62,1.56,1.56,-0.0127 +0020.HK,2024-12-13,1.55,1.56,1.49,1.55,-0.0064 +0020.HK,2024-12-16,1.54,1.54,1.48,1.49,-0.0387 +0020.HK,2024-12-17,1.48,1.5,1.46,1.48,-0.0067 +0020.HK,2024-12-18,1.49,1.53,1.49,1.51,0.0203 +0020.HK,2024-12-19,1.48,1.58,1.48,1.51,0 +0020.HK,2024-12-20,1.51,1.54,1.5,1.5,-0.0066 +0020.HK,2024-12-23,1.51,1.52,1.48,1.48,-0.0133 +0020.HK,2024-12-24,1.48,1.5,1.47,1.48,0 +0020.HK,2024-12-27,1.49,1.56,1.48,1.53,0.0338 +0020.HK,2024-12-30,1.54,1.55,1.51,1.51,-0.0131 +0020.HK,2024-12-31,1.51,1.52,1.49,1.49,-0.0132 +0020.HK,2025-01-02,1.49,1.49,1.41,1.41,-0.0537 +0020.HK,2025-01-03,1.42,1.43,1.33,1.33,-0.0567 +0020.HK,2025-01-06,1.34,1.38,1.32,1.33,0 +0020.HK,2025-01-07,1.31,1.34,1.26,1.33,0 +0020.HK,2025-01-08,1.33,1.34,1.28,1.31,-0.015 +0020.HK,2025-01-09,1.3,1.35,1.3,1.31,0 +0020.HK,2025-01-10,1.32,1.34,1.28,1.28,-0.0229 +0020.HK,2025-01-13,1.27,1.3,1.26,1.3,0.0156 +0020.HK,2025-01-14,1.3,1.37,1.3,1.34,0.0308 +0020.HK,2025-01-15,1.35,1.39,1.32,1.33,-0.0075 +0020.HK,2025-01-16,1.35,1.39,1.34,1.36,0.0226 +0020.HK,2025-01-17,1.36,1.4,1.36,1.37,0.0074 +0020.HK,2025-01-20,1.4,1.44,1.39,1.41,0.0292 +0020.HK,2025-01-21,1.42,1.45,1.4,1.44,0.0213 +0020.HK,2025-01-22,1.42,1.45,1.39,1.41,-0.0208 +0020.HK,2025-01-23,1.42,1.48,1.42,1.43,0.0142 +0020.HK,2025-01-24,1.44,1.54,1.43,1.52,0.0629 +0020.HK,2025-01-27,1.55,1.73,1.55,1.63,0.0724 +0020.HK,2025-01-28,1.64,1.66,1.59,1.61,-0.0123 +0020.HK,2025-02-03,1.61,1.7,1.55,1.69,0.0497 +0020.HK,2025-02-04,1.71,1.74,1.65,1.73,0.0237 +0020.HK,2025-02-05,1.71,1.71,1.59,1.65,-0.0462 +0020.HK,2025-02-06,1.64,1.72,1.63,1.72,0.0424 +0020.HK,2025-02-07,1.73,1.8,1.68,1.73,0.0058 +0020.HK,2025-02-10,1.76,1.85,1.75,1.77,0.0231 +0020.HK,2025-02-11,1.78,1.8,1.68,1.71,-0.0339 +0020.HK,2025-02-12,1.72,1.74,1.67,1.72,0.0058 +0020.HK,2025-02-13,1.74,1.82,1.67,1.69,-0.0174 +0020.HK,2025-02-14,1.72,1.83,1.71,1.82,0.0769 +0020.HK,2025-02-17,1.87,1.9,1.78,1.83,0.0055 +0020.HK,2025-02-18,1.84,1.92,1.77,1.82,-0.0055 +0020.HK,2025-02-19,1.84,1.84,1.77,1.83,0.0055 +0020.HK,2025-02-20,1.81,1.82,1.73,1.73,-0.0546 +0020.HK,2025-02-21,1.78,1.9,1.75,1.88,0.0867 +0020.HK,2025-02-24,1.94,1.98,1.89,1.92,0.0213 +0020.HK,2025-02-25,1.83,1.88,1.79,1.81,-0.0573 +0020.HK,2025-02-26,1.84,1.86,1.79,1.82,0.0055 +0020.HK,2025-02-27,1.83,1.87,1.74,1.78,-0.022 +0020.HK,2025-02-28,1.77,1.77,1.62,1.64,-0.0787 +0020.HK,2025-03-03,1.68,1.69,1.6,1.64,0 +0020.HK,2025-03-04,1.56,1.66,1.56,1.64,0 +0020.HK,2025-03-05,1.66,1.69,1.63,1.68,0.0244 +0020.HK,2025-03-06,1.72,1.81,1.72,1.78,0.0595 +0020.HK,2025-03-07,1.76,1.8,1.72,1.75,-0.0169 +0020.HK,2025-03-10,1.74,1.76,1.69,1.73,-0.0114 +0020.HK,2025-03-11,1.68,1.76,1.67,1.75,0.0116 +0020.HK,2025-03-12,1.76,1.78,1.69,1.71,-0.0229 +0020.HK,2025-03-13,1.72,1.74,1.66,1.69,-0.0117 +0020.HK,2025-03-14,1.71,1.73,1.67,1.71,0.0118 +0020.HK,2025-03-17,1.71,1.73,1.67,1.68,-0.0175 +0020.HK,2025-03-18,1.71,1.73,1.7,1.72,0.0238 +0020.HK,2025-03-19,1.72,1.8,1.68,1.74,0.0116 +0020.HK,2025-03-20,1.74,1.75,1.68,1.68,-0.0345 +0020.HK,2025-03-21,1.68,1.69,1.6,1.61,-0.0417 +0020.HK,2025-03-24,1.62,1.63,1.58,1.61,0 +0020.HK,2025-03-25,1.6,1.61,1.56,1.57,-0.0248 +0020.HK,2025-03-26,1.57,1.6,1.57,1.59,0.0127 +0020.HK,2025-03-27,1.55,1.55,1.45,1.49,-0.0629 +0020.HK,2025-03-28,1.51,1.53,1.47,1.5,0.0067 +0020.HK,2025-03-31,1.5,1.5,1.45,1.49,-0.0067 +0020.HK,2025-04-01,1.49,1.51,1.46,1.47,-0.0134 +0020.HK,2025-04-02,1.48,1.54,1.47,1.53,0.0408 +0020.HK,2025-04-03,1.5,1.6,1.49,1.55,0.0131 +0020.HK,2025-04-07,1.4,1.44,1.25,1.28,-0.1742 +0020.HK,2025-04-08,1.31,1.35,1.25,1.31,0.0234 +0020.HK,2025-04-09,1.27,1.38,1.24,1.36,0.0382 +0020.HK,2025-04-10,1.41,1.45,1.39,1.41,0.0368 +0020.HK,2025-04-11,1.41,1.44,1.39,1.42,0.0071 +0020.HK,2025-04-14,1.45,1.51,1.45,1.47,0.0352 +0020.HK,2025-04-15,1.48,1.49,1.43,1.45,-0.0136 +0020.HK,2025-04-16,1.44,1.44,1.39,1.4,-0.0345 +0020.HK,2025-04-17,1.4,1.42,1.39,1.4,0 +0020.HK,2025-04-22,1.41,1.43,1.39,1.43,0.0214 +0020.HK,2025-04-23,1.47,1.47,1.43,1.43,0 +0020.HK,2025-04-24,1.44,1.45,1.4,1.43,0 +0020.HK,2025-04-25,1.45,1.47,1.42,1.42,-0.007 +0020.HK,2025-04-28,1.43,1.45,1.4,1.45,0.0211 +0020.HK,2025-04-29,1.45,1.48,1.44,1.46,0.0069 +0020.HK,2025-04-30,1.49,1.53,1.47,1.5,0.0274 +0020.HK,2025-05-02,1.5,1.56,1.48,1.55,0.0333 +0020.HK,2025-05-06,1.55,1.56,1.51,1.53,-0.0129 +0020.HK,2025-05-07,1.56,1.58,1.52,1.52,-0.0065 +0020.HK,2025-05-08,1.52,1.55,1.51,1.52,0 +0020.HK,2025-05-09,1.52,1.52,1.48,1.49,-0.0197 +0020.HK,2025-05-12,1.52,1.58,1.5,1.56,0.047 +0020.HK,2025-05-13,1.57,1.58,1.5,1.51,-0.0321 +0020.HK,2025-05-14,1.53,1.53,1.48,1.49,-0.0132 +0020.HK,2025-05-15,1.49,1.51,1.45,1.46,-0.0201 +0020.HK,2025-05-16,1.44,1.46,1.44,1.45,-0.0068 +0020.HK,2025-05-19,1.44,1.45,1.42,1.43,-0.0138 +0020.HK,2025-05-20,1.44,1.45,1.41,1.42,-0.007 +0020.HK,2025-05-21,1.43,1.44,1.41,1.41,-0.007 +0020.HK,2025-05-22,1.41,1.42,1.4,1.4,-0.0071 +0020.HK,2025-05-23,1.4,1.42,1.39,1.4,0 +0020.HK,2025-05-26,1.4,1.42,1.37,1.4,0 +0020.HK,2025-05-27,1.4,1.41,1.38,1.39,-0.0071 +0020.HK,2025-05-28,1.39,1.4,1.38,1.38,-0.0072 +0020.HK,2025-05-29,1.38,1.43,1.37,1.42,0.029 +0020.HK,2025-05-30,1.41,1.42,1.39,1.4,-0.0141 +0020.HK,2025-06-02,1.39,1.39,1.33,1.38,-0.0143 +0020.HK,2025-06-03,1.38,1.41,1.36,1.37,-0.0072 +0020.HK,2025-06-04,1.37,1.39,1.36,1.36,-0.0073 +0020.HK,2025-06-05,1.37,1.42,1.37,1.4,0.0294 +0020.HK,2025-06-06,1.4,1.41,1.36,1.4,0 +0020.HK,2025-06-09,1.41,1.5,1.4,1.47,0.05 +0020.HK,2025-06-10,1.48,1.48,1.42,1.46,-0.0068 +0020.HK,2025-06-11,1.46,1.49,1.45,1.47,0.0068 +0020.HK,2025-06-12,1.46,1.48,1.43,1.47,0 +0020.HK,2025-06-13,1.46,1.47,1.4,1.4,-0.0476 \ No newline at end of file diff --git a/src/quantitative_analysis/README.md b/src/quantitative_analysis/README.md new file mode 100644 index 0000000..7712003 --- /dev/null +++ b/src/quantitative_analysis/README.md @@ -0,0 +1,158 @@ +# 量化分析工程模块 + +## 功能介绍 + +这个模块主要用于A股量化分析,包括: + +1. **财务数据采集** - 自动从东方财富网采集上市公司财务报表数据 +2. **数据存储** - 将数据结构化存储到MongoDB数据库 +3. **量化分析** - 基于财务数据进行量化分析和策略开发 + +## 目录结构 + +``` +quantitative_analysis/ +├── __init__.py # 模块初始化文件 +├── financial_data_collector.py # 财务数据采集器 +├── README.md # 使用说明 +└── examples/ # 使用示例 +``` + +## 财务数据采集器 + +### 功能特点 + +- **全面的财务数据**:采集资产负债表、利润表、现金流量表、杜邦分析等 +- **批量处理**:支持单只股票和批量股票数据采集 +- **数据完整性**:2019-2024年完整的季度和年度财务数据 +- **智能存储**:自动去重,使用股票代码+报告日期作为唯一标识 +- **错误处理**:完善的异常处理和日志记录 + +### 使用方法 + +#### 1. 单只股票采集 + +```python +from src.quantitative_analysis.financial_data_collector import FinancialDataCollector + +# 创建采集器实例 +collector = FinancialDataCollector() + +# 采集单只股票财务数据 +success = collector.collect_financial_data('300750.SZ') # 宁德时代 + +if success: + print("财务数据采集成功") +else: + print("财务数据采集失败") + +# 关闭连接 +collector.close_connection() +``` + +#### 2. 批量股票采集 + +```python +from src.quantitative_analysis.financial_data_collector import FinancialDataCollector + +# 创建采集器实例 +collector = FinancialDataCollector() + +# 股票代码列表 +stock_list = [ + '300750.SZ', # 宁德时代 + '000858.SZ', # 五粮液 + '002415.SZ', # 海康威视 + '000001.SZ', # 平安银行 + '600519.SH' # 贵州茅台 +] + +# 批量采集 +results = collector.batch_collect_financial_data(stock_list) +print(f"采集结果: {results}") + +# 关闭连接 +collector.close_connection() +``` + +### 数据结构 + +采集的数据会以以下结构存储到MongoDB: + +```json +{ + "stock_code": "300750.SZ", + "report_date": "2024-06-30", + "collect_time": "2024-07-15T10:30:00", + "dupont_analysis": { + "roe": 15.2, + "dupont_assetstoequity": 1.8, + "assetsturn": 0.9, + "dupont_np": 8.5, + "profittogr": 12.3 + }, + "balance_sheet": { + "property": { + "monetary_cap": 1000000000, + "tot_assets": 5000000000, + ... + }, + "liabilities": { + "tot_liab": 2000000000, + ... + }, + "owner_equity": { + "tot_equity": 3000000000, + ... + } + }, + "profit_statement": { + "tot_oper_rev": 800000000, + "net_profit_is": 100000000, + ... + }, + "cash_flow_statement": { + "net_cash_flows_oper_act": 150000000, + ... + } +} +``` + +### 配置说明 + +- 使用 `src/valuation_analysis/config.py` 中的 `MONGO_CONFIG2` 配置 +- MongoDB服务器:192.168.20.110:27017 +- 数据库:judge +- 集合:wind_financial_analysis + +### 注意事项 + +1. **请求频率控制**:代码中已添加请求间隔,避免被东方财富网限制 +2. **数据完整性**:会检查四类财务数据是否都获取成功 +3. **错误处理**:单个股票失败不会影响其他股票的采集 +4. **日志记录**:详细的日志记录,便于排查问题 + +### 运行测试 + +直接运行财务数据采集器进行测试: + +```bash +cd src/quantitative_analysis +python financial_data_collector.py +``` + +这将测试采集宁德时代(300750.SZ)的财务数据。 + +## 扩展开发 + +后续可以在此模块基础上开发: + +1. **财务指标计算** - 基于原始财务数据计算各类财务指标 +2. **估值模型** - DCF、PE、PB等估值模型 +3. **量化策略** - 基于财务数据的选股策略 +4. **风险分析** - 财务风险评估模型 +5. **回测框架** - 策略回测和绩效分析 + +## 联系方式 + +如有问题或建议,请联系开发团队。 \ No newline at end of file diff --git a/src/quantitative_analysis/__init__.py b/src/quantitative_analysis/__init__.py new file mode 100644 index 0000000..e4f0005 --- /dev/null +++ b/src/quantitative_analysis/__init__.py @@ -0,0 +1,10 @@ +""" +量化分析工程模块 + +主要功能: +1. 财务数据采集 +2. 数据处理和分析 +3. 量化策略开发 +""" + +__version__ = "1.0.0" \ No newline at end of file diff --git a/src/quantitative_analysis/batch_stock_price_collector.py b/src/quantitative_analysis/batch_stock_price_collector.py new file mode 100644 index 0000000..f3e025d --- /dev/null +++ b/src/quantitative_analysis/batch_stock_price_collector.py @@ -0,0 +1,183 @@ +import requests +import pandas as pd +from datetime import datetime +import sys +import os +import redis +import json + +# 添加项目根目录到路径,便于导入scripts.config +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(project_root) + +# 读取雪球headers和Redis配置 +try: + from src.scripts.config import XUEQIU_HEADERS + from src.valuation_analysis.config import REDIS_CONFIG +except ImportError: + XUEQIU_HEADERS = { + 'User-Agent': 'Mozilla/5.0', + 'Cookie': '', # 需要填写雪球cookie + } + REDIS_CONFIG = { + 'host': 'localhost', + 'port': 6379, + 'db': 0, + 'password': None + } + +REDIS_KEY = 'xq_stock_changes_latest' # 存放行情的主键 + + +def get_redis_conn(): + """获取Redis连接""" + pool = redis.ConnectionPool( + host=REDIS_CONFIG['host'], + 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) + + +def fetch_and_store_stock_data(page_size=90): + """ + 批量采集雪球A股(上证、深证、科创板)股票的最新行情数据,并保存到Redis。 + :param page_size: 每页采集数量 + """ + base_url = 'https://stock.xueqiu.com/v5/stock/screener/quote/list.json' + types = ['sha', 'sza', 'kcb'] # 上证、深证、科创板 + headers = XUEQIU_HEADERS + + all_data = [] + + for stock_type in types: + params = { + 'page': 1, + 'size': page_size, + 'order': 'desc', + 'order_by': 'percent', + 'market': 'CN', + 'type': stock_type + } + + # 初次请求以获取总页数 + response = requests.get(base_url, headers=headers, params=params) + 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 + + for page in range(1, total_pages + 1): + params['page'] = page + response = requests.get(base_url, headers=headers, params=params) + if response.status_code == 200: + data = response.json() + all_data.extend(data['data']['list']) + else: + print(f"请求 {stock_type} 数据第 {page} 页失败,状态码:{response.status_code}") + # 转换为 DataFrame + df = pd.DataFrame(all_data) + + if not df.empty: + df['fetch_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + # 存入Redis,使用hash结构,key为symbol,value为json字符串 + r = get_redis_conn() + pipe = r.pipeline() + # 先清空旧数据 + r.delete(REDIS_KEY) + for _, row in df.iterrows(): + symbol = row.get('symbol') + if not symbol: + continue + # 只保留必要字段,也可直接存row.to_dict() + value = row.to_dict() + pipe.hset(REDIS_KEY, symbol, json.dumps(value, ensure_ascii=False)) + pipe.execute() + print(f"成功将数据写入Redis哈希 {REDIS_KEY},共{len(df)}条记录。") + else: + print("未获取到任何数据。") + + +def format_stock_code(stock_code): + """ + 统一股票代码格式,支持600001.SH、SH600001、000001.SZ、SZ000001等 + 返回雪球格式(如SH600001、SZ000001)和Redis存储格式(如SZ000978) + """ + stock_code = stock_code.upper() + if '.' in stock_code: + code, market = stock_code.split('.') + if market == 'SH': + return f'SH{code}', f'{market}{code}' + elif market == 'SZ': + return f'SZ{code}', f'{market}{code}' + elif market == 'BJ': + return f'BJ{code}', f'{market}{code}' + else: + return stock_code, stock_code + elif stock_code.startswith(('SH', 'SZ', 'BJ')): + return stock_code, stock_code + else: + # 默认返回原始 + return stock_code, stock_code + + +def get_stock_realtime_info_from_redis(stock_code): + """ + 根据股票代码从Redis查询实时行情,并封装为指定结构。 + :param stock_code: 支持600001.SH、SH600001、000001.SZ、SZ000001等 + :return: dict or None + """ + _, redis_code = format_stock_code(stock_code) + r = get_redis_conn() + value = r.hget(REDIS_KEY, redis_code) + if not value: + return None + try: + data = json.loads(value) + except Exception: + return None + # 封装为指定结构 + result = { + "code": None, + "crawlDate": None, + "marketValue": None, + "maxPrice": None, + "minPrice": None, + "nowPrice": None, + "pbRate": None, + "rangeRiseAndFall": None, + "shortName": None, + "todayStartPrice": None, + "ttm": None, + "turnoverRate": None, + "yesterdayEndPrice": None + } + # 赋值映射 + result["code"] = data.get("symbol") + result["crawlDate"] = data.get("fetch_time") + result["marketValue"] = data.get("market_capital") + 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["nowPrice"] = data.get("current") + result["pbRate"] = data.get("pb") + result["rangeRiseAndFall"] = data.get("percent") + result["shortName"] = data.get("name") + result["todayStartPrice"] = data.get("open") + result["ttm"] = data.get("pe_ttm") + result["turnoverRate"] = data.get("turnover_rate") + result["yesterdayEndPrice"] = data.get("last_close") if "last_close" in data else data.get("pre_close") + # 兼容部分字段缺失 + if result["maxPrice"] is None and "high" in data: + result["maxPrice"] = data["high"] + if result["minPrice"] is None and "low" in data: + result["minPrice"] = data["low"] + return result + + +if __name__ == '__main__': + fetch_and_store_stock_data() \ No newline at end of file diff --git a/src/quantitative_analysis/composite_grid_backtest.py b/src/quantitative_analysis/composite_grid_backtest.py new file mode 100644 index 0000000..fbdbcc3 --- /dev/null +++ b/src/quantitative_analysis/composite_grid_backtest.py @@ -0,0 +1,220 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import os + +# 新增:设置matplotlib支持中文和负号 +import matplotlib +matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +matplotlib.rcParams['axes.unicode_minus'] = False + +# 新增:屏蔽UserWarning +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +# 回测参数 +INIT_CASH = 4000000 +FEE_RATE = 0.003 # 千分之3 +GRID_NUM = 15 # 网格数量 +PER_GRID_CASH = INIT_CASH / GRID_NUM # 每格买入金额 + +def generate_composite_grids(price_min, price_max): + """生成等差网格""" + step = (price_max - price_min) / GRID_NUM + return np.arange(price_min, price_max, step) + +def should_trade(macd, signal): + """MACD趋势过滤 + 金叉时做多,死叉时暂停 + """ + return macd > signal + +def composite_grid_backtest(prices): + cash = INIT_CASH + position = 0 + cost = 0 + history = [] # 记录每日资金、持仓 + trade_log = [] # 买卖点 + last_grid = None + + # 计算MACD + exp1 = prices['close'].ewm(span=12, adjust=False).mean() + exp2 = prices['close'].ewm(span=26, adjust=False).mean() + prices['MACD'] = exp1 - exp2 + prices['Signal'] = prices['MACD'].ewm(span=9, adjust=False).mean() + + # 生成固定网格 + price_min = prices['close'].min() + price_max = prices['close'].max() + grids = generate_composite_grids(price_min, price_max) + + def get_grid_index(price): + """获取价格所在的网格索引""" + grid_idx = np.searchsorted(grids, price, side='right') - 1 + if grid_idx < 0: + grid_idx = 0 + if grid_idx >= len(grids)-1: + grid_idx = len(grids)-2 + return grid_idx + + def execute_trade(price, trade_type, grid_idx, last_grid_idx, date, allow_trade): + """执行交易""" + nonlocal cash, position, cost + if not allow_trade: + return False + if trade_type == 'buy' and cash >= PER_GRID_CASH: + buy_price = price * (1 + FEE_RATE) + buy_amount = PER_GRID_CASH / buy_price + cash -= buy_amount * buy_price + position += buy_amount + cost += buy_amount * buy_price + trade_log.append({'date': date, 'price': price, 'type': 'buy'}) + return True + elif trade_type == 'sell' and position > 0: + sell_price = price * (1 - FEE_RATE) + sell_amount = PER_GRID_CASH / price + if sell_amount > position: + sell_amount = position + cash += sell_amount * sell_price + position -= sell_amount + cost -= sell_amount * price + trade_log.append({'date': date, 'price': price, 'type': 'sell'}) + return True + return False + + for i, row in prices.iterrows(): + date = row['date'] + open_price = row['open'] + close_price = row['close'] + + # 判断是否允许交易(MACD趋势过滤) + allow_trade = should_trade(row['MACD'], row['Signal']) + + # 获取开盘价和收盘价所在的网格位置 + open_grid_idx = get_grid_index(open_price) + close_grid_idx = get_grid_index(close_price) + + # 开盘价交易处理 + if last_grid is not None: + # 开盘价买入处理 + if open_grid_idx < last_grid: + for grid_idx in range(open_grid_idx, last_grid): + if not execute_trade(open_price, 'buy', grid_idx, last_grid, date, allow_trade): + break + # 开盘价卖出处理 + elif open_grid_idx > last_grid: + for grid_idx in range(last_grid + 1, open_grid_idx + 1): + if not execute_trade(open_price, 'sell', grid_idx, last_grid, date, allow_trade): + break + + # 收盘价交易处理 + if last_grid is not None: + # 收盘价买入处理 + if close_grid_idx < open_grid_idx: + for grid_idx in range(close_grid_idx, open_grid_idx): + if not execute_trade(close_price, 'buy', grid_idx, open_grid_idx, date, allow_trade): + break + # 收盘价卖出处理 + elif close_grid_idx > open_grid_idx: + for grid_idx in range(open_grid_idx + 1, close_grid_idx + 1): + if not execute_trade(close_price, 'sell', grid_idx, open_grid_idx, date, allow_trade): + break + + # 更新last_grid为收盘价所在网格 + last_grid = close_grid_idx + + # 记录每日净值 + total = cash + position * close_price + history.append({ + 'date': date, + 'net': total, + 'cash': cash, + 'position': position, + 'price': close_price, + 'macd': row['MACD'], + 'signal': row['Signal'], + 'open_grid': open_grid_idx, + 'close_grid': close_grid_idx + }) + + # 统计 + df_hist = pd.DataFrame(history) + max_drawdown = ((df_hist['net'].cummax() - df_hist['net']) / df_hist['net'].cummax()).max() + total_return = (df_hist['net'].iloc[-1] - INIT_CASH) / INIT_CASH + + # 胜率统计 + win, lose = 0, 0 + for i in range(1, len(trade_log)): + if trade_log[i-1]['type']=='buy' and trade_log[i]['type']=='sell': + if trade_log[i]['price'] > trade_log[i-1]['price']: + win += 1 + else: + lose += 1 + win_rate = win / (win+lose) if (win+lose)>0 else 0 + + return df_hist, trade_log, total_return, max_drawdown, len([t for t in trade_log if t['type']=='buy']), len([t for t in trade_log if t['type']=='sell']), win_rate + +def plot_results(df_hist, trade_log, grids=None): + """绘制回测结果""" + fig, (ax1, ax3) = plt.subplots(2, 1, figsize=(14,10), height_ratios=[3, 1]) + ax2 = ax1.twinx() + + # 绘制价格和净值 + ax1.plot(df_hist['date'], df_hist['price'], label='收盘价', color='gray', alpha=0.7) + relative_net = df_hist['net'] / INIT_CASH + ax2.plot(df_hist['date'], relative_net, label='净值', color='blue') + + # 绘制交易点 + for t in trade_log: + if t['type']=='buy': + ax1.scatter(t['date'], t['price'], marker='^', color='red', label='买入' if '买入' not in ax1.get_legend_handles_labels()[1] else "") + else: + ax1.scatter(t['date'], t['price'], marker='v', color='green', label='卖出' if '卖出' not in ax1.get_legend_handles_labels()[1] else "") + + # 绘制网格线 + if grids is not None: + for g in grids: + ax1.axhline(g, color='orange', linestyle='--', alpha=0.2) + + # 绘制MACD + ax3.plot(df_hist['date'], df_hist['macd'], label='MACD', color='blue') + ax3.plot(df_hist['date'], df_hist['signal'], label='Signal', color='red') + ax3.fill_between(df_hist['date'], df_hist['macd'] - df_hist['signal'], + 0, where=(df_hist['macd'] >= df_hist['signal']), + color='green', alpha=0.3, label='做多区域') + ax3.grid(True, alpha=0.3) + ax3.legend(loc='upper left') + + plt.title('复合策略网格回测') + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2) + plt.tight_layout() + plt.show() + +if __name__ == '__main__': + # 读取数据 + DATA_PATH = os.path.join(os.path.dirname(__file__), '0020.HK.csv') + # 处理文件头乱码和尾部注释 + raw = pd.read_csv(DATA_PATH, header=None, skiprows=1, names=['code','date','open','high','low','close','pct']) + raw = raw[raw['close'].apply(lambda x: str(x).replace('.','',1).isdigit())] # 去除空行和注释 + raw['date'] = pd.to_datetime(raw['date']) + raw['close'] = raw['close'].astype(float) + raw['open'] = raw['open'].astype(float) + raw = raw.sort_values('date').reset_index(drop=True) + + # 运行回测 + df_hist, trade_log, total_return, max_drawdown, buy_cnt, sell_cnt, win_rate = composite_grid_backtest(raw) + + # 打印统计结果 + print('\n==== 复合策略网格回测结果 ===') + print(f'总收益率: {total_return*100:.2f}%') + print(f'最大回撤: {max_drawdown*100:.2f}%') + print(f'买入次数: {buy_cnt},卖出次数: {sell_cnt}') + print(f'胜率: {win_rate*100:.2f}%') + + # 绘制结果 + price_min = raw['close'].min() + price_max = raw['close'].max() + grids = generate_composite_grids(price_min, price_max) + plot_results(df_hist, trade_log, grids) \ No newline at end of file diff --git a/src/quantitative_analysis/dynamic_grid_backtest.py b/src/quantitative_analysis/dynamic_grid_backtest.py new file mode 100644 index 0000000..0e51bf0 --- /dev/null +++ b/src/quantitative_analysis/dynamic_grid_backtest.py @@ -0,0 +1,232 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import os + +# 新增:设置matplotlib支持中文和负号 +import matplotlib +matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +matplotlib.rcParams['axes.unicode_minus'] = False + +# 新增:屏蔽UserWarning +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +# 回测参数 +INIT_CASH = 4000000 +FEE_RATE = 0.003 # 千分之3 +GRID_NUM = 15 # 网格数量 +PER_GRID_CASH = INIT_CASH / GRID_NUM # 每格买入金额 + +def generate_dynamic_grids(price, ma120): + """生成动态网格 + 根据价格与MA120的偏离程度动态调整网格密度 + 核心区(±8%):密集网格(1.5%-2%) + 缓冲区(±8%-20%):中等间距(3%-4%) + 极端区(超出±20%):疏松间距(5%-6%) + """ + deviation = (price - ma120) / ma120 # 计算价格相对MA120的偏离率 + + if abs(deviation) <= 0.08: # 核心区(±8%) + # 在当前价格上下8%范围内生成密集网格 + grid_min = price * 0.92 + grid_max = price * 1.08 + return np.linspace(grid_min, grid_max, GRID_NUM) # 约1.5-2%间距 + + elif abs(deviation) <= 0.20: # 缓冲区(±8%-20%) + # 扩大网格范围,增加间距 + if deviation > 0: # 价格在MA120上方 + grid_min = ma120 * 1.08 # 从MA120+8%开始 + grid_max = price * 1.15 # 到当前价格+15% + else: # 价格在MA120下方 + grid_min = price * 0.85 # 从当前价格-15%开始 + grid_max = ma120 * 0.92 # 到MA120-8% + return np.linspace(grid_min, grid_max, GRID_NUM) # 约3-4%间距 + + else: # 极端区(超出±20%) + # 在极端区域使用更大的网格间距 + if deviation > 0: # 价格远高于MA120 + grid_min = ma120 * 1.20 # 从MA120+20%开始 + grid_max = price * 1.10 # 到当前价格+10% + else: # 价格远低于MA120 + grid_min = price * 0.90 # 从当前价格-10%开始 + grid_max = ma120 * 0.80 # 到MA120-20% + return np.linspace(grid_min, grid_max, GRID_NUM) # 约5-6%间距 + +def dynamic_grid_backtest(prices): + cash = INIT_CASH + position = 0 + cost = 0 + history = [] # 记录每日资金、持仓 + trade_log = [] # 买卖点 + last_grid = None + + # 计算120日移动平均作为动态参考价 + prices['MA120'] = prices['close'].rolling(window=120).mean() + + def get_grid_index(price, current_grids): + """获取价格所在的网格索引""" + if len(current_grids) == 0: + return -1 + grid_idx = np.searchsorted(current_grids, price, side='right') - 1 + if grid_idx < 0: + grid_idx = 0 + if grid_idx >= len(current_grids)-1: + grid_idx = len(current_grids)-2 + return grid_idx + + def execute_trade(price, trade_type, grid_idx, last_grid_idx, date): + """执行交易""" + nonlocal cash, position, cost + if trade_type == 'buy' and cash >= PER_GRID_CASH: + buy_price = price * (1 + FEE_RATE) + buy_amount = PER_GRID_CASH / buy_price + cash -= buy_amount * buy_price + position += buy_amount + cost += buy_amount * buy_price + trade_log.append({'date': date, 'price': price, 'type': 'buy'}) + return True + elif trade_type == 'sell' and position > 0: + sell_price = price * (1 - FEE_RATE) + sell_amount = PER_GRID_CASH / price + if sell_amount > position: + sell_amount = position + cash += sell_amount * sell_price + position -= sell_amount + cost -= sell_amount * price + trade_log.append({'date': date, 'price': price, 'type': 'sell'}) + return True + return False + + for i, row in prices.iterrows(): + if pd.isna(row['MA120']): # 跳过MA120未形成的时期 + continue + + date = row['date'] + open_price = row['open'] + close_price = row['close'] + ma120 = row['MA120'] + + # 生成当前的动态网格 + open_grids = generate_dynamic_grids(open_price, ma120) + close_grids = generate_dynamic_grids(close_price, ma120) + + # 获取开盘价和收盘价所在的网格位置 + open_grid_idx = get_grid_index(open_price, open_grids) + close_grid_idx = get_grid_index(close_price, close_grids) + + # 开盘价交易处理 + if last_grid is not None and open_grid_idx >= 0: + # 开盘价买入处理 + if open_grid_idx < last_grid: + for grid_idx in range(open_grid_idx, last_grid): + if not execute_trade(open_price, 'buy', grid_idx, last_grid, date): + break + # 开盘价卖出处理 + elif open_grid_idx > last_grid: + for grid_idx in range(last_grid + 1, open_grid_idx + 1): + if not execute_trade(open_price, 'sell', grid_idx, last_grid, date): + break + + # 收盘价交易处理 + if last_grid is not None and close_grid_idx >= 0: + # 收盘价买入处理 + if close_grid_idx < open_grid_idx: + for grid_idx in range(close_grid_idx, open_grid_idx): + if not execute_trade(close_price, 'buy', grid_idx, open_grid_idx, date): + break + # 收盘价卖出处理 + elif close_grid_idx > open_grid_idx: + for grid_idx in range(open_grid_idx + 1, close_grid_idx + 1): + if not execute_trade(close_price, 'sell', grid_idx, open_grid_idx, date): + break + + # 更新last_grid为收盘价所在网格 + last_grid = close_grid_idx + + # 记录每日净值 + total = cash + position * close_price + history.append({ + 'date': date, + 'net': total, + 'cash': cash, + 'position': position, + 'price': close_price, + 'ma120': ma120, + 'open_grid': open_grid_idx, + 'close_grid': close_grid_idx + }) + + # 统计 + df_hist = pd.DataFrame(history) + max_drawdown = ((df_hist['net'].cummax() - df_hist['net']) / df_hist['net'].cummax()).max() + total_return = (df_hist['net'].iloc[-1] - INIT_CASH) / INIT_CASH + + # 胜率统计 + win, lose = 0, 0 + for i in range(1, len(trade_log)): + if trade_log[i-1]['type']=='buy' and trade_log[i]['type']=='sell': + if trade_log[i]['price'] > trade_log[i-1]['price']: + win += 1 + else: + lose += 1 + win_rate = win / (win+lose) if (win+lose)>0 else 0 + + return df_hist, trade_log, total_return, max_drawdown, len([t for t in trade_log if t['type']=='buy']), len([t for t in trade_log if t['type']=='sell']), win_rate + +def plot_results(df_hist, trade_log, grids=None): + """绘制回测结果""" + fig, ax1 = plt.subplots(figsize=(14,6)) + ax2 = ax1.twinx() + + # 绘制价格、MA120和净值 + ax1.plot(df_hist['date'], df_hist['price'], label='收盘价', color='gray', alpha=0.7) + ax1.plot(df_hist['date'], df_hist['ma120'], label='MA120', color='purple', alpha=0.5) + relative_net = df_hist['net'] / INIT_CASH + ax2.plot(df_hist['date'], relative_net, label='净值', color='blue') + + # 绘制交易点 + for t in trade_log: + if t['type']=='buy': + ax1.scatter(t['date'], t['price'], marker='^', color='red', label='买入' if '买入' not in ax1.get_legend_handles_labels()[1] else "") + else: + ax1.scatter(t['date'], t['price'], marker='v', color='green', label='卖出' if '卖出' not in ax1.get_legend_handles_labels()[1] else "") + + # 绘制最后一个时间点的网格线 + if grids is not None and len(grids) > 0: + for g in grids: + ax1.axhline(g, color='orange', linestyle='--', alpha=0.2) + + plt.title('动态网格回测') + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2) + plt.tight_layout() + plt.show() + +if __name__ == '__main__': + # 读取数据 + DATA_PATH = os.path.join(os.path.dirname(__file__), '0020.HK.csv') + # 处理文件头乱码和尾部注释 + raw = pd.read_csv(DATA_PATH, header=None, skiprows=1, names=['code','date','open','high','low','close','pct']) + raw = raw[raw['close'].apply(lambda x: str(x).replace('.','',1).isdigit())] # 去除空行和注释 + raw['date'] = pd.to_datetime(raw['date']) + raw['close'] = raw['close'].astype(float) + raw['open'] = raw['open'].astype(float) + raw = raw.sort_values('date').reset_index(drop=True) + + # 运行回测 + df_hist, trade_log, total_return, max_drawdown, buy_cnt, sell_cnt, win_rate = dynamic_grid_backtest(raw) + + # 打印统计结果 + print('\n==== 动态网格回测结果 ===') + print(f'总收益率: {total_return*100:.2f}%') + print(f'最大回撤: {max_drawdown*100:.2f}%') + print(f'买入次数: {buy_cnt},卖出次数: {sell_cnt}') + print(f'胜率: {win_rate*100:.2f}%') + + # 绘制结果 + last_price = df_hist['price'].iloc[-1] + last_ma120 = df_hist['ma120'].iloc[-1] + last_grids = generate_dynamic_grids(last_price, last_ma120) + plot_results(df_hist, trade_log, last_grids) \ No newline at end of file diff --git a/src/quantitative_analysis/financial_data_collector.py b/src/quantitative_analysis/financial_data_collector.py new file mode 100644 index 0000000..a621517 --- /dev/null +++ b/src/quantitative_analysis/financial_data_collector.py @@ -0,0 +1,603 @@ +# -*- coding: utf-8 -*- +""" +东方财富财务数据采集器 V2.0 +适配2025年新版接口 + +从东方财富网自动采集A股上市公司的财务报表数据,包括: +1. 资产负债表 +2. 利润表 +3. 现金流量表 + +数据存储到MongoDB数据库中,保留所有原始字段 +""" + +import requests +import pymongo +import logging +import datetime +import time +import json +import pandas as pd +from typing import List, Dict, Optional +import sys +import os +from sqlalchemy import create_engine, text + +# 添加项目根目录到路径 +project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.append(project_root) + +# 导入配置 +try: + from valuation_analysis.config import MONGO_CONFIG2, DB_URL +except ImportError: + # 如果上面的导入失败,尝试直接导入 + import importlib.util + config_path = os.path.join(project_root, 'valuation_analysis', 'config.py') + spec = importlib.util.spec_from_file_location("config", config_path) + config_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config_module) + MONGO_CONFIG2 = config_module.MONGO_CONFIG2 + DB_URL = config_module.DB_URL + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('financial_data_collector_v2.log', encoding='utf-8'), + logging.StreamHandler() + ] +) +logger = logging.getLogger(__name__) + +# 为StreamHandler也设置编码(如果可能) +try: + if hasattr(sys.stdout, 'reconfigure'): + sys.stdout.reconfigure(encoding='utf-8') + if hasattr(sys.stderr, 'reconfigure'): + sys.stderr.reconfigure(encoding='utf-8') +except: + pass # 如果设置失败就忽略 + + +class FinancialDataCollectorV2: + """财务数据采集器 V2.0 - 适配新版东方财富接口""" + + def __init__(self): + """初始化""" + self.mongo_client = None + self.db = None + # 使用新的集合名称存储新版本数据 + self.collection_name = 'eastmoney_financial_data_v2' + self.collection = None + + # 初始化MySQL连接 + self.mysql_engine = None + + self.connect_mongodb() + self.connect_mysql() + + def connect_mongodb(self): + """连接MongoDB数据库""" + try: + # 使用参数形式连接MongoDB,而不是连接字符串 + self.mongo_client = pymongo.MongoClient( + host=MONGO_CONFIG2['host'], + port=MONGO_CONFIG2['port'], + username=MONGO_CONFIG2['username'], + password=MONGO_CONFIG2['password'] + ) + self.db = self.mongo_client[MONGO_CONFIG2['db']] + self.collection = self.db[self.collection_name] + + # 测试连接 + self.mongo_client.admin.command('ping') + logger.info(f"MongoDB连接成功,使用集合: {self.collection_name}") + + except Exception as e: + logger.error(f"MongoDB连接失败: {str(e)}") + raise + + def connect_mysql(self): + """连接MySQL数据库""" + try: + self.mysql_engine = create_engine( + DB_URL, + pool_size=5, + max_overflow=10, + pool_recycle=3600 + ) + + # 测试连接 + with self.mysql_engine.connect() as conn: + conn.execute(text("SELECT 1")) + + logger.info("MySQL数据库连接成功") + + except Exception as e: + logger.error(f"MySQL数据库连接失败: {str(e)}") + raise + + def get_all_stock_codes(self) -> List[str]: + """ + 从数据库中获取所有股票代码 + + Returns: + List[str]: 股票代码列表 + """ + try: + query = "SELECT DISTINCT gp_code_two FROM gp_code_all WHERE gp_code_two IS NOT NULL AND gp_code_two != ''" + + with self.mysql_engine.connect() as conn: + df = pd.read_sql(text(query), conn) + + stock_codes = df['gp_code_two'].tolist() + logger.info(f"从数据库获取到 {len(stock_codes)} 只股票") + + return stock_codes + + except Exception as e: + logger.error(f"获取股票代码列表失败: {str(e)}") + return [] + + def build_date_filter(self, stock_code: str, periods: int = 21) -> str: + """ + 构建日期过滤条件 - 适配新版接口 + + Args: + stock_code: 股票代码,如'300750.SZ' + periods: 获取多少个报告期,默认21个季度 + + Returns: + str: 日期过滤字符串 + """ + # 计算需要的日期范围,从当前日期往前推 + current_date = datetime.datetime.now() + dates = [] + + # 生成最近21个季度的日期 + for i in range(periods): + # 从当前季度往前推 + year = current_date.year + month = current_date.month + + # 计算季度 + if month <= 3: + quarter_month = 3 + year_offset = i // 4 + quarter_offset = i % 4 + if quarter_offset > 0: + year_offset += 1 + quarter_month = [12, 9, 6, 3][quarter_offset - 1] + target_year = year - year_offset + elif month <= 6: + quarter_month = 6 + year_offset = i // 4 + quarter_offset = i % 4 + if quarter_offset >= 1: + if quarter_offset == 1: + quarter_month = 3 + elif quarter_offset == 2: + quarter_month = 12 + target_year = year - 1 + elif quarter_offset == 3: + quarter_month = 9 + target_year = year - 1 + else: + target_year = year + if quarter_offset >= 2: + year_offset += 1 + target_year = year - year_offset + elif month <= 9: + quarter_month = 9 + year_offset = i // 4 + quarter_offset = i % 4 + if quarter_offset >= 1: + if quarter_offset == 1: + quarter_month = 6 + elif quarter_offset == 2: + quarter_month = 3 + elif quarter_offset == 3: + quarter_month = 12 + target_year = year - 1 + else: + target_year = year + if quarter_offset >= 3: + year_offset += 1 + target_year = year - year_offset + else: + quarter_month = 12 + year_offset = i // 4 + quarter_offset = i % 4 + if quarter_offset >= 1: + if quarter_offset == 1: + quarter_month = 9 + elif quarter_offset == 2: + quarter_month = 6 + elif quarter_offset == 3: + quarter_month = 3 + else: + target_year = year + target_year = year - year_offset + + # 简化逻辑:直接从2025年开始往前推21个季度 + base_year = 2025 + base_quarters = [ + (2025, 3), (2024, 12), (2024, 9), (2024, 6), (2024, 3), + (2023, 12), (2023, 9), (2023, 6), (2023, 3), + (2022, 12), (2022, 9), (2022, 6), (2022, 3), + (2021, 12), (2021, 9), (2021, 6), (2021, 3), + (2020, 12), (2020, 9), (2020, 6), (2020, 3) + ] + + if i < len(base_quarters): + year, month = base_quarters[i] + if month == 3: + date_str = f'{year}-03-31' + elif month == 6: + date_str = f'{year}-06-30' + elif month == 9: + date_str = f'{year}-09-30' + else: + date_str = f'{year}-12-31' + dates.append(date_str) + + # 构建过滤字符串 + date_filter = f'(SECUCODE%3D%22{stock_code}%22)(REPORT_DATE%20in%20(' + date_filter += '%2C'.join([f'%27{date}%27' for date in dates]) + date_filter += '))' + + return date_filter + + def fetch_profit_statement(self, stock_code: str, periods: int = 21) -> List[Dict]: + """获取利润表数据""" + date_filter = self.build_date_filter(stock_code, periods) + url = f'https://datacenter.eastmoney.com/securities/api/data/get?type=RPT_F10_FINANCE_GINCOME&sty=APP_F10_GINCOME&filter={date_filter}&p=1&ps={periods}&sr=-1&st=REPORT_DATE&source=HSF10&client=PC' + + headers = {"Content-Type": "application/json"} + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + data = response.json() + + if 'result' in data and 'data' in data['result']: + logger.info(f"成功获取利润表数据,共 {len(data['result']['data'])} 个报告期") + return data['result']['data'] + else: + logger.warning("利润表数据格式异常") + return [] + + except Exception as e: + logger.error(f"获取利润表失败: {str(e)}") + return [] + + def fetch_balance_sheet(self, stock_code: str, periods: int = 21) -> List[Dict]: + """获取资产负债表数据""" + date_filter = self.build_date_filter(stock_code, periods) + url = f'https://datacenter.eastmoney.com/securities/api/data/get?type=RPT_F10_FINANCE_GBALANCE&sty=F10_FINANCE_GBALANCE&filter={date_filter}&p=1&ps={periods}&sr=-1&st=REPORT_DATE&source=HSF10&client=PC&v=012481899342117453' + + headers = {"Content-Type": "application/json"} + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + data = response.json() + + if 'result' in data and 'data' in data['result']: + logger.info(f"成功获取资产负债表数据,共 {len(data['result']['data'])} 个报告期") + return data['result']['data'] + else: + logger.warning("资产负债表数据格式异常") + return [] + + except Exception as e: + logger.error(f"获取资产负债表失败: {str(e)}") + return [] + + def fetch_cash_flow_statement(self, stock_code: str, periods: int = 21) -> List[Dict]: + """获取现金流量表数据""" + date_filter = self.build_date_filter(stock_code, periods) + url = f'https://datacenter.eastmoney.com/securities/api/data/get?type=RPT_F10_FINANCE_GCASHFLOW&sty=APP_F10_GCASHFLOW&filter={date_filter}&p=1&ps={periods}&sr=-1&st=REPORT_DATE&source=HSF10&client=PC&v=04664977872701077' + + headers = {"Content-Type": "application/json"} + + try: + response = requests.get(url, headers=headers, timeout=30) + response.raise_for_status() + data = response.json() + + if 'result' in data and 'data' in data['result']: + logger.info(f"成功获取现金流量表数据,共 {len(data['result']['data'])} 个报告期") + return data['result']['data'] + else: + logger.warning("现金流量表数据格式异常") + return [] + + except Exception as e: + logger.error(f"获取现金流量表失败: {str(e)}") + return [] + + def process_financial_data(self, stock_code: str, profit_data: List[Dict], + balance_data: List[Dict], cash_data: List[Dict]) -> List[Dict]: + """ + 处理财务数据,按报告期合并三张表的数据 + + Args: + stock_code: 股票代码 + profit_data: 利润表数据 + balance_data: 资产负债表数据 + cash_data: 现金流量表数据 + + Returns: + List[Dict]: 处理后的财务数据列表 + """ + financial_data_list = [] + + # 创建按报告日期索引的字典 + profit_dict = {item['REPORT_DATE']: item for item in profit_data} + balance_dict = {item['REPORT_DATE']: item for item in balance_data} + cash_dict = {item['REPORT_DATE']: item for item in cash_data} + + # 获取所有报告日期 + all_dates = set() + all_dates.update(profit_dict.keys()) + all_dates.update(balance_dict.keys()) + all_dates.update(cash_dict.keys()) + + # 按日期排序 + sorted_dates = sorted(all_dates, reverse=True) + + for report_date in sorted_dates: + try: + # 构建完整的财务数据记录 + financial_record = { + 'stock_code': stock_code, + 'report_date': report_date[:10] if report_date else None, # 只取日期部分 + 'collect_time': datetime.datetime.now(), + 'data_source': 'eastmoney_v2', + 'profit_statement': profit_dict.get(report_date, {}), + 'balance_sheet': balance_dict.get(report_date, {}), + 'cash_flow_statement': cash_dict.get(report_date, {}) + } + + # 只有当至少有一张表有数据时才添加记录 + if (financial_record['profit_statement'] or + financial_record['balance_sheet'] or + financial_record['cash_flow_statement']): + financial_data_list.append(financial_record) + + except Exception as e: + logger.error(f"处理报告期 {report_date} 数据时出错: {str(e)}") + continue + + logger.info(f"成功处理 {len(financial_data_list)} 个报告期的数据") + return financial_data_list + + def save_to_mongodb(self, financial_data_list: List[Dict]) -> bool: + """ + 保存数据到MongoDB,如果数据已存在则跳过,只新增不存在的数据 + + Args: + financial_data_list: 财务数据列表 + + Returns: + bool: 是否保存成功 + """ + try: + if not financial_data_list: + logger.warning("没有数据需要保存") + return False + + # 统计插入和跳过的数量 + inserted_count = 0 + skipped_count = 0 + + for data in financial_data_list: + # 使用股票代码和报告日期作为唯一标识 + filter_condition = { + 'stock_code': data['stock_code'], + 'report_date': data['report_date'] + } + + # 检查数据是否已存在 + existing_record = self.collection.find_one(filter_condition) + + if existing_record: + # 数据已存在,跳过 + skipped_count += 1 + logger.debug(f"跳过已存在的数据: {data['stock_code']} - {data['report_date']}") + else: + # 数据不存在,插入新数据 + self.collection.insert_one(data) + inserted_count += 1 + logger.debug(f"插入新数据: {data['stock_code']} - {data['report_date']}") + + logger.info(f"数据保存完成 - 新增: {inserted_count} 条,跳过: {skipped_count} 条") + return True + + except Exception as e: + logger.error(f"保存数据到MongoDB失败: {str(e)}") + return False + + def collect_financial_data(self, stock_code: str, periods: int = 21) -> bool: + """ + 采集单只股票的财务数据 + + Args: + stock_code: 股票代码,如'300750.SZ' + periods: 获取多少个报告期,默认21个季度 + + Returns: + bool: 是否采集成功 + """ + try: + logger.info(f"开始采集股票 {stock_code} 的财务数据({periods}个报告期)") + + # 获取三张财务报表数据 + profit_data = self.fetch_profit_statement(stock_code, periods) + time.sleep(1) # 避免请求过于频繁 + + balance_data = self.fetch_balance_sheet(stock_code, periods) + time.sleep(1) + + cash_data = self.fetch_cash_flow_statement(stock_code, periods) + time.sleep(1) + + # 检查至少有一张表有数据 + if not any([profit_data, balance_data, cash_data]): + logger.error(f"股票 {stock_code} 没有获取到任何财务数据") + return False + + # 处理财务数据 + financial_data_list = self.process_financial_data( + stock_code, profit_data, balance_data, cash_data + ) + + if not financial_data_list: + logger.error(f"股票 {stock_code} 的财务数据处理失败") + return False + + # 保存到MongoDB + success = self.save_to_mongodb(financial_data_list) + + if success: + logger.info(f"股票 {stock_code} 的财务数据采集完成") + + return success + + except Exception as e: + logger.error(f"采集股票 {stock_code} 的财务数据失败: {str(e)}") + return False + + def batch_collect_financial_data(self, stock_codes: List[str], periods: int = 21) -> Dict: + """ + 批量采集多只股票的财务数据 + + Args: + stock_codes: 股票代码列表 + periods: 获取多少个报告期,默认21个季度 + + Returns: + Dict: 采集结果统计 + """ + results = {'success': 0, 'failed': 0, 'failed_stocks': []} + total_stocks = len(stock_codes) + + logger.info(f"开始批量采集 {total_stocks} 只股票的财务数据") + + for index, stock_code in enumerate(stock_codes, 1): + try: + # 显示进度 + progress = (index / total_stocks) * 100 + logger.info(f"进度 [{index}/{total_stocks}] ({progress:.1f}%) - 正在处理: {stock_code}") + + success = self.collect_financial_data(stock_code, periods) + if success: + results['success'] += 1 + logger.info(f"SUCCESS [{index}/{total_stocks}] {stock_code} 采集成功") + else: + results['failed'] += 1 + results['failed_stocks'].append(stock_code) + logger.warning(f"FAILED [{index}/{total_stocks}] {stock_code} 采集失败") + + # 每只股票之间暂停一下,避免请求过于频繁 + time.sleep(2) + + # 每100只股票输出一次统计信息 + if index % 100 == 0: + current_success_rate = (results['success'] / index) * 100 + logger.info(f"阶段性统计: 已处理 {index}/{total_stocks} 只股票,成功率: {current_success_rate:.2f}%") + + except Exception as e: + logger.error(f"处理股票 {stock_code} 时出错: {str(e)}") + results['failed'] += 1 + results['failed_stocks'].append(stock_code) + + # 继续处理下一只股票,不中断整个流程 + continue + + success_rate = (results['success'] / total_stocks) * 100 + logger.info(f"批量采集完成: 成功{results['success']}只,失败{results['failed']}只,成功率: {success_rate:.2f}%") + + if results['failed_stocks']: + logger.info(f"失败的股票数量: {len(results['failed_stocks'])}") + # 记录前10个失败的股票到日志 + failed_sample = results['failed_stocks'][:10] + logger.info(f"失败股票示例: {', '.join(failed_sample)}") + + return results + + def close_connection(self): + """关闭数据库连接""" + if self.mongo_client: + self.mongo_client.close() + logger.info("MongoDB连接已关闭") + + if self.mysql_engine: + self.mysql_engine.dispose() + logger.info("MySQL连接已关闭") + + +def main(): + """主函数 - 批量采集所有股票的财务数据""" + collector = FinancialDataCollectorV2() + + try: + # 从数据库获取所有股票代码 + logger.info("正在从数据库获取股票列表...") + stock_codes = collector.get_all_stock_codes() + + if not stock_codes: + logger.error("未获取到任何股票代码,程序退出") + return + + logger.info(f"从数据库获取到 {len(stock_codes)} 只股票") + + # 可以选择采集所有股票或者部分股票进行测试 + # 如果要测试,可以取前几只股票 + # 测试模式:只采集前10只股票 + TEST_MODE = False # 设置为False将采集所有股票 + + if TEST_MODE: + test_count = min(10, len(stock_codes)) # 最多取10只股票测试 + stock_codes = stock_codes[:test_count] + logger.info(f"TEST MODE: 仅采集前 {test_count} 只股票") + else: + logger.info(f"PRODUCTION MODE: 将采集全部 {len(stock_codes)} 只股票") + + logger.info(f"开始批量采集 {len(stock_codes)} 只股票的财务数据") + + # 批量采集 + results = collector.batch_collect_financial_data(stock_codes, periods=21) + + # 输出最终结果 + print(f"\n{'='*50}") + print(f"批量采集完成统计") + print(f"{'='*50}") + print(f"SUCCESS 成功采集: {results['success']} 只股票") + print(f"FAILED 采集失败: {results['failed']} 只股票") + print(f"SUCCESS RATE 成功率: {(results['success'] / len(stock_codes) * 100):.2f}%") + + if results['failed_stocks']: + print(f"\n失败的股票列表:") + for i, stock in enumerate(results['failed_stocks'][:10], 1): # 只显示前10个 + print(f" {i}. {stock}") + if len(results['failed_stocks']) > 10: + print(f" ... 还有 {len(results['failed_stocks']) - 10} 只股票") + + logger.info("所有任务已完成") + + except KeyboardInterrupt: + logger.info("用户中断程序执行") + print("\n警告: 程序被用户中断") + except Exception as e: + logger.error(f"采集过程中出现错误: {str(e)}") + print(f"\n错误: 程序执行出错: {str(e)}") + finally: + collector.close_connection() + + +if __name__ == "__main__": + main() diff --git a/src/quantitative_analysis/grid_backtest_0020HK.py b/src/quantitative_analysis/grid_backtest_0020HK.py new file mode 100644 index 0000000..85ab72b --- /dev/null +++ b/src/quantitative_analysis/grid_backtest_0020HK.py @@ -0,0 +1,200 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import os + +# 新增:设置matplotlib支持中文和负号 +import matplotlib +matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +matplotlib.rcParams['axes.unicode_minus'] = False + +# 新增:屏蔽UserWarning +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +# 读取数据 +DATA_PATH = os.path.join(os.path.dirname(__file__), '0020.HK.csv') +# 处理文件头乱码和尾部注释 +raw = pd.read_csv(DATA_PATH, header=None, skiprows=1, names=['code','date','open','high','low','close','pct']) +raw = raw[raw['close'].apply(lambda x: str(x).replace('.','',1).isdigit())] # 去除空行和注释 +raw['date'] = pd.to_datetime(raw['date']) +raw['close'] = raw['close'].astype(float) +raw = raw.sort_values('date').reset_index(drop=True) + +# 回测参数 +INIT_CASH = 4000000 +FEE_RATE = 0.003 # 千分之3 +GRID_TYPES = ['等差网格', '等比网格', 'ATR网格'] +GRID_NUM = 15 # 网格数量 +PER_GRID_CASH = INIT_CASH / GRID_NUM # 每格买入金额 + +# 自动确定网格上下限 +price_min = raw['close'].min() +price_max = raw['close'].max() +price_mean = raw['close'].mean() +price_std = raw['close'].std() + +# 网格参数生成 +# 1. 等差网格 +step_linear = (price_max - price_min) / GRID_NUM +linear_grids = np.arange(price_min, price_max, step_linear) +# 2. 等比网格 +step_ratio = (price_max / price_min) ** (1/GRID_NUM) +ratio_grids = [price_min * (step_ratio ** i) for i in range(GRID_NUM+1)] +# 3. ATR网格 +atr_window = 20 +raw['tr'] = np.maximum(raw['high']-raw['low'], np.abs(raw['high']-raw['close'].shift(1)), np.abs(raw['low']-raw['close'].shift(1))) +raw['atr'] = raw['tr'].rolling(atr_window).mean() +avg_atr = raw['atr'].mean() +atr_step = avg_atr * 1.0 # 可调参数 +atr_grids = np.arange(price_min, price_max, atr_step) + +# 网格集合 +grid_dict = { + '等差网格': linear_grids, + '等比网格': ratio_grids, + 'ATR网格': atr_grids +} + +# 回测主函数 +def grid_backtest(prices, grids, grid_type): + cash = INIT_CASH + position = 0 + cost = 0 + history = [] # 记录每日资金、持仓 + trade_log = [] # 买卖点 + last_grid = None + + def get_grid_index(price): + """获取价格所在的网格索引""" + grid_idx = np.searchsorted(grids, price, side='right') - 1 + if grid_idx < 0: + grid_idx = 0 + if grid_idx >= len(grids)-1: + grid_idx = len(grids)-2 + return grid_idx + + def execute_trade(price, trade_type, grid_idx, last_grid_idx): + """执行交易""" + nonlocal cash, position, cost + if trade_type == 'buy' and cash >= PER_GRID_CASH: + buy_price = price * (1 + FEE_RATE) + buy_amount = PER_GRID_CASH / buy_price + cash -= buy_amount * buy_price + position += buy_amount + cost += buy_amount * buy_price + trade_log.append({'date': date, 'price': price, 'type': 'buy'}) + return True + elif trade_type == 'sell' and position > 0: + sell_price = price * (1 - FEE_RATE) + sell_amount = PER_GRID_CASH / price + if sell_amount > position: + sell_amount = position + cash += sell_amount * sell_price + position -= sell_amount + cost -= sell_amount * price + trade_log.append({'date': date, 'price': price, 'type': 'sell'}) + return True + return False + + for i, row in prices.iterrows(): + date = row['date'] + open_price = row['open'] + close_price = row['close'] + + # 获取开盘价和收盘价所在的网格位置 + open_grid_idx = get_grid_index(open_price) + close_grid_idx = get_grid_index(close_price) + + # 开盘价交易处理 + if last_grid is not None: + # 开盘价买入处理 + if open_grid_idx < last_grid: + # 处理多个网格的买入 + for grid_idx in range(open_grid_idx, last_grid): + if not execute_trade(open_price, 'buy', grid_idx, last_grid): + break + # 开盘价卖出处理 + elif open_grid_idx > last_grid: + # 处理多个网格的卖出 + for grid_idx in range(last_grid + 1, open_grid_idx + 1): + if not execute_trade(open_price, 'sell', grid_idx, last_grid): + break + + # 收盘价交易处理 + if last_grid is not None: + # 收盘价买入处理 + if close_grid_idx < open_grid_idx: + # 处理多个网格的买入 + for grid_idx in range(close_grid_idx, open_grid_idx): + if not execute_trade(close_price, 'buy', grid_idx, open_grid_idx): + break + # 收盘价卖出处理 + elif close_grid_idx > open_grid_idx: + # 处理多个网格的卖出 + for grid_idx in range(open_grid_idx + 1, close_grid_idx + 1): + if not execute_trade(close_price, 'sell', grid_idx, open_grid_idx): + break + + # 更新last_grid为收盘价所在网格 + last_grid = close_grid_idx + + # 记录每日净值 + total = cash + position * close_price + history.append({ + 'date': date, + 'net': total, + 'cash': cash, + 'position': position, + 'price': close_price, + 'open_grid': open_grid_idx, + 'close_grid': close_grid_idx + }) + + # 统计 + df_hist = pd.DataFrame(history) + max_drawdown = ((df_hist['net'].cummax() - df_hist['net']) / df_hist['net'].cummax()).max() + total_return = (df_hist['net'].iloc[-1] - INIT_CASH) / INIT_CASH + + # 胜率统计 + win, lose = 0, 0 + for i in range(1, len(trade_log)): + if trade_log[i-1]['type']=='buy' and trade_log[i]['type']=='sell': + if trade_log[i]['price'] > trade_log[i-1]['price']: + win += 1 + else: + lose += 1 + win_rate = win / (win+lose) if (win+lose)>0 else 0 + + return df_hist, trade_log, total_return, max_drawdown, len([t for t in trade_log if t['type']=='buy']), len([t for t in trade_log if t['type']=='sell']), win_rate + +# 主流程 +if __name__ == '__main__': + for grid_type in GRID_TYPES: + grids = grid_dict[grid_type] + df_hist, trade_log, total_return, max_drawdown, buy_cnt, sell_cnt, win_rate = grid_backtest(raw, grids, grid_type) + print(f'\n==== {grid_type} 回测结果 ===') + print(f'总收益率: {total_return*100:.2f}%') + print(f'最大回撤: {max_drawdown*100:.2f}%') + print(f'买入次数: {buy_cnt},卖出次数: {sell_cnt}') + print(f'胜率: {win_rate*100:.2f}%') + # 绘图 + fig, ax1 = plt.subplots(figsize=(14,6)) + ax2 = ax1.twinx() + ax1.plot(df_hist['date'], df_hist['price'], label='收盘价', color='gray', alpha=0.7) + # 将净值转换为相对净值(从1开始) + relative_net = df_hist['net'] / INIT_CASH + ax2.plot(df_hist['date'], relative_net, label='净值', color='blue') + for t in trade_log: + if t['type']=='buy': + ax1.scatter(t['date'], t['price'], marker='^', color='red', label='买入' if '买入' not in ax1.get_legend_handles_labels()[1] else "") + else: + ax1.scatter(t['date'], t['price'], marker='v', color='green', label='卖出' if '卖出' not in ax1.get_legend_handles_labels()[1] else "") + for g in grids: + ax1.axhline(g, color='orange', linestyle='--', alpha=0.2) + plt.title(f'{grid_type} 网格回测') + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2) + plt.tight_layout() + plt.show() \ No newline at end of file diff --git a/src/quantitative_analysis/trend_following_grid_backtest.py b/src/quantitative_analysis/trend_following_grid_backtest.py new file mode 100644 index 0000000..94bcd4f --- /dev/null +++ b/src/quantitative_analysis/trend_following_grid_backtest.py @@ -0,0 +1,214 @@ +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import os + +# 新增:设置matplotlib支持中文和负号 +import matplotlib +matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei'] +matplotlib.rcParams['axes.unicode_minus'] = False + +# 新增:屏蔽UserWarning +import warnings +warnings.filterwarnings("ignore", category=UserWarning) + +# 回测参数 +INIT_CASH = 4000000 +FEE_RATE = 0.003 # 千分之3 +GRID_NUM = 15 # 网格数量 +PER_GRID_CASH = INIT_CASH / GRID_NUM # 每格买入金额 + +def generate_trend_following_grids(price, ma20): + """生成趋势跟随网格 + 上升趋势: + - 买入:每跌1%加仓 + - 卖出:每涨3%减仓 + 下降趋势: + - 买入:每跌3%加仓 + - 卖出:每涨1%减仓 + """ + if price > ma20: # 上升趋势 + buy_steps = np.arange(price * 0.99, price * 0.85, -price * 0.01) # 每跌1%加仓 + sell_steps = np.arange(price * 1.03, price * 1.30, price * 0.03) # 每涨3%减仓 + else: # 下降趋势 + buy_steps = np.arange(price * 0.97, price * 0.70, -price * 0.03) # 每跌3%加仓 + sell_steps = np.arange(price * 1.01, price * 1.15, price * 0.01) # 每涨1%减仓 + return np.sort(np.concatenate([buy_steps, sell_steps])) + +def trend_following_grid_backtest(prices): + cash = INIT_CASH + position = 0 + cost = 0 + history = [] # 记录每日资金、持仓 + trade_log = [] # 买卖点 + last_grid = None + + # 计算MA20 + prices['MA20'] = prices['close'].rolling(window=20).mean() + + def get_grid_index(price, current_grids): + """获取价格所在的网格索引""" + if len(current_grids) == 0: + return -1 + grid_idx = np.searchsorted(current_grids, price, side='right') - 1 + if grid_idx < 0: + grid_idx = 0 + if grid_idx >= len(current_grids)-1: + grid_idx = len(current_grids)-2 + return grid_idx + + def execute_trade(price, trade_type, grid_idx, last_grid_idx, date): + """执行交易""" + nonlocal cash, position, cost + if trade_type == 'buy' and cash >= PER_GRID_CASH: + buy_price = price * (1 + FEE_RATE) + buy_amount = PER_GRID_CASH / buy_price + cash -= buy_amount * buy_price + position += buy_amount + cost += buy_amount * buy_price + trade_log.append({'date': date, 'price': price, 'type': 'buy'}) + return True + elif trade_type == 'sell' and position > 0: + sell_price = price * (1 - FEE_RATE) + sell_amount = PER_GRID_CASH / price + if sell_amount > position: + sell_amount = position + cash += sell_amount * sell_price + position -= sell_amount + cost -= sell_amount * price + trade_log.append({'date': date, 'price': price, 'type': 'sell'}) + return True + return False + + for i, row in prices.iterrows(): + if pd.isna(row['MA20']): # 跳过MA20未形成的时期 + continue + + date = row['date'] + open_price = row['open'] + close_price = row['close'] + ma20 = row['MA20'] + + # 生成当前的趋势跟随网格 + open_grids = generate_trend_following_grids(open_price, ma20) + close_grids = generate_trend_following_grids(close_price, ma20) + + # 获取开盘价和收盘价所在的网格位置 + open_grid_idx = get_grid_index(open_price, open_grids) + close_grid_idx = get_grid_index(close_price, close_grids) + + # 开盘价交易处理 + if last_grid is not None and open_grid_idx >= 0: + # 开盘价买入处理 + if open_grid_idx < last_grid: + for grid_idx in range(open_grid_idx, last_grid): + if not execute_trade(open_price, 'buy', grid_idx, last_grid, date): + break + # 开盘价卖出处理 + elif open_grid_idx > last_grid: + for grid_idx in range(last_grid + 1, open_grid_idx + 1): + if not execute_trade(open_price, 'sell', grid_idx, last_grid, date): + break + + # 收盘价交易处理 + if last_grid is not None and close_grid_idx >= 0: + # 收盘价买入处理 + if close_grid_idx < open_grid_idx: + for grid_idx in range(close_grid_idx, open_grid_idx): + if not execute_trade(close_price, 'buy', grid_idx, open_grid_idx, date): + break + # 收盘价卖出处理 + elif close_grid_idx > open_grid_idx: + for grid_idx in range(open_grid_idx + 1, close_grid_idx + 1): + if not execute_trade(close_price, 'sell', grid_idx, open_grid_idx, date): + break + + # 更新last_grid为收盘价所在网格 + last_grid = close_grid_idx + + # 记录每日净值 + total = cash + position * close_price + history.append({ + 'date': date, + 'net': total, + 'cash': cash, + 'position': position, + 'price': close_price, + 'ma20': ma20, + 'open_grid': open_grid_idx, + 'close_grid': close_grid_idx + }) + + # 统计 + df_hist = pd.DataFrame(history) + max_drawdown = ((df_hist['net'].cummax() - df_hist['net']) / df_hist['net'].cummax()).max() + total_return = (df_hist['net'].iloc[-1] - INIT_CASH) / INIT_CASH + + # 胜率统计 + win, lose = 0, 0 + for i in range(1, len(trade_log)): + if trade_log[i-1]['type']=='buy' and trade_log[i]['type']=='sell': + if trade_log[i]['price'] > trade_log[i-1]['price']: + win += 1 + else: + lose += 1 + win_rate = win / (win+lose) if (win+lose)>0 else 0 + + return df_hist, trade_log, total_return, max_drawdown, len([t for t in trade_log if t['type']=='buy']), len([t for t in trade_log if t['type']=='sell']), win_rate + +def plot_results(df_hist, trade_log, grids=None): + """绘制回测结果""" + fig, ax1 = plt.subplots(figsize=(14,6)) + ax2 = ax1.twinx() + + # 绘制价格、MA20和净值 + ax1.plot(df_hist['date'], df_hist['price'], label='收盘价', color='gray', alpha=0.7) + ax1.plot(df_hist['date'], df_hist['ma20'], label='MA20', color='purple', alpha=0.5) + relative_net = df_hist['net'] / INIT_CASH + ax2.plot(df_hist['date'], relative_net, label='净值', color='blue') + + # 绘制交易点 + for t in trade_log: + if t['type']=='buy': + ax1.scatter(t['date'], t['price'], marker='^', color='red', label='买入' if '买入' not in ax1.get_legend_handles_labels()[1] else "") + else: + ax1.scatter(t['date'], t['price'], marker='v', color='green', label='卖出' if '卖出' not in ax1.get_legend_handles_labels()[1] else "") + + # 绘制最后一个时间点的网格线 + if grids is not None and len(grids) > 0: + for g in grids: + ax1.axhline(g, color='orange', linestyle='--', alpha=0.2) + + plt.title('趋势跟随网格回测') + lines1, labels1 = ax1.get_legend_handles_labels() + lines2, labels2 = ax2.get_legend_handles_labels() + ax1.legend(lines1 + lines2, labels1 + labels2) + plt.tight_layout() + plt.show() + +if __name__ == '__main__': + # 读取数据 + DATA_PATH = os.path.join(os.path.dirname(__file__), '0020.HK.csv') + # 处理文件头乱码和尾部注释 + raw = pd.read_csv(DATA_PATH, header=None, skiprows=1, names=['code','date','open','high','low','close','pct']) + raw = raw[raw['close'].apply(lambda x: str(x).replace('.','',1).isdigit())] # 去除空行和注释 + raw['date'] = pd.to_datetime(raw['date']) + raw['close'] = raw['close'].astype(float) + raw['open'] = raw['open'].astype(float) + raw = raw.sort_values('date').reset_index(drop=True) + + # 运行回测 + df_hist, trade_log, total_return, max_drawdown, buy_cnt, sell_cnt, win_rate = trend_following_grid_backtest(raw) + + # 打印统计结果 + print('\n==== 趋势跟随网格回测结果 ===') + print(f'总收益率: {total_return*100:.2f}%') + print(f'最大回撤: {max_drawdown*100:.2f}%') + print(f'买入次数: {buy_cnt},卖出次数: {sell_cnt}') + print(f'胜率: {win_rate*100:.2f}%') + + # 绘制结果 + last_price = df_hist['price'].iloc[-1] + last_ma20 = df_hist['ma20'].iloc[-1] + last_grids = generate_trend_following_grids(last_price, last_ma20) + plot_results(df_hist, trade_log, last_grids) \ No newline at end of file diff --git a/src/scripts/config.py b/src/scripts/config.py index 7a576c1..0388266 100644 --- a/src/scripts/config.py +++ b/src/scripts/config.py @@ -11,7 +11,7 @@ XUEQIU_HEADERS = { 'Accept-Encoding': 'gzip, deflate, br, zstd', 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', 'Client-Version': 'v2.44.75', - 'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; Hm_lvt_1db88642e346389874251b5a1eded6e3=1746410725; __utma=1.434320573.1747189698.1747189698.1747189698.1; __utmc=1; __utmz=1.1747189698.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); snbim_minify=true; acw_tc=0a27a9dd17489230816243798e0070441d5e7160c0ed179607143a953db903; xq_a_token=ef79e6da376751a4bf6c1538103e9894d44473e1; xqat=ef79e6da376751a4bf6c1538103e9894d44473e1; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzUxNTE1MDgxLCJjdG0iOjE3NDg5MjMwODE2NDQsImNpZCI6ImQ5ZDBuNEFadXAifQ.gQrIt4VI73JLUFGVSTKpXidhFIMwlusBKyrzYwClwCBszXCooQY3WnFqlbXqSX3SwnMapuveOFUM5sGIOoZ8oDF8cZYs3HDz5vezR-2nes9gfZr2nZcUfZzNRJ299wlX3Zis5NbnzNlfnisUhv9GUfEZjQ_Rs37B4qRbQZVC2kdN1Z0xB8j1MplSTOsYj4IliQntuaTo-8SBh-4zz5244dnF85xREBVxtFzzCtHUhn9B-mzxE81_42nwrDscvow-4_jtlJXlqbehiAFxld-dCWDXwmCju9lRWu_WzdoQe19n-c6jhCZZ1pU1JGsYyhIAsd1gV064jQ6FxfN38so1Eg; xq_r_token=30a80318ebcabffbe194e7deecb108b665e8c894; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1748923088; .thumbcache_f24b8bbe5a5934237bbc0eda20c1b6e7=b+jlfRtg2lC80dGHk9izZ9Od1QBbaKrdx1aAMbruXo2ULyhkygsXnhJoa7lOWNgnQAphRKw3864D5K+U2pTL5g%3D%3D; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0shqDyji2YsBGdTYKYUxGXxN4xiNDAc40iDC3WLPeUpx5h5o5Gmxt3qU6P5b48r89Y4sKs=BkpxKFTG4SQW4odeGLDY=DCTKKSMiD4b3Dt4DIDAYDDxDWm4DLDYoDY3uexGPo2mTNpm2bD0YDzqDgD7jbmeDEDG3D0bbetGDo1Q4DGqDSWZHTxD3Dffb4DDN4zIG0GmDDbrR=qmcbC=7O9Wtox0tWDBL5YvysdVC441TXpw8w7WaaxBQD7d9Q5na7fCW13rWkYY0Yeoe7hx+BxYrKch4SbKOAYY7hq7hR0D3E5YD5QADW0D/hQ7Emh07hiY7xdUginMzSTblushiee2YKbK5nYO0t3Ede7d46DqEQMA557QODdNG4WG+slx5bhWiDD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q0shqDyji2YsBGdTYKY4xDfiOYiiBq4YDj44KWGfmoD/8okGxAeG/0Dt6Q7D6cGudn3qfM5QntBLc5pp/FFY4hly50hUr5qB2v45io/FQi4eD', + 'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; snbim_minify=true; xq_a_token=ef79e6da376751a4bf6c1538103e9894d44473e1; xqat=ef79e6da376751a4bf6c1538103e9894d44473e1; xq_id_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1aWQiOjg0OTM0MTE2MzQsImlzcyI6InVjIiwiZXhwIjoxNzUxNTE1MDgxLCJjdG0iOjE3NDg5MjMwODE2NDQsImNpZCI6ImQ5ZDBuNEFadXAifQ.gQrIt4VI73JLUFGVSTKpXidhFIMwlusBKyrzYwClwCBszXCooQY3WnFqlbXqSX3SwnMapuveOFUM5sGIOoZ8oDF8cZYs3HDz5vezR-2nes9gfZr2nZcUfZzNRJ299wlX3Zis5NbnzNlfnisUhv9GUfEZjQ_Rs37B4qRbQZVC2kdN1Z0xB8j1MplSTOsYj4IliQntuaTo-8SBh-4zz5244dnF85xREBVxtFzzCtHUhn9B-mzxE81_42nwrDscvow-4_jtlJXlqbehiAFxld-dCWDXwmCju9lRWu_WzdoQe19n-c6jhCZZ1pU1JGsYyhIAsd1gV064jQ6FxfN38so1Eg; xq_r_token=30a80318ebcabffbe194e7deecb108b665e8c894; Hm_lvt_1db88642e346389874251b5a1eded6e3=1749028611; is_overseas=0; Hm_lpvt_1db88642e346389874251b5a1eded6e3=1750034926; ssxmod_itna=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q07hqDyliiYwirG4tshiQBKD/KlYeDZDGFdDqx0Ei6FiBFKCezjCGbKBACQ5xC3o0aOyndbV3Ab3t8NXiK3y4xB3DExGkR0iYeK4DxrPD5xDTDWeDGDD3WxGaDmeDeho+D0bmHUOvrU7oD7eDXxGCDQFor4GWDiPD7Po45iim=KxD0xD1ESkEDDPDaroxDG5NlQ9weDi3rfgsLFbLDdwIqQimD753DlcqwXLXmktxGfoyzd=bdI8fDCKDjxdIx93QW33vimn4xqiDb37Yn4qe2qW+QKGxAiUmrKiirFBDFAQQDT7GN0xq3DPQGQBwOb7Y3FrjAPdmrRiYIvZRHm9HV4hY3Ybz777DbRGxi4T/48Bxt3GhUhtBhNfqtRDWq2qADPfYYsihWYPeD; ssxmod_itna2=eqGxBDnGKYuxcD4kDRgxYq7ueYKS8DBP01Dp2xQyP08D60DB40Q07hqDyliiYwirG4tshiQYeDA4pg2w+RYD7POqKepgqDlcb9wAqieP5+6V+KK5w6Q4dMdqCLomgLyxQn93CdLjc3pXEYq0/0h+jCdiOWudEmrIjmRf1+lLX0OGj02GXiiblBod5++dyGbWSnufTL+nxBWIQimWCI3ueZSne50WYT6afRSyCo79FGa6WEk2j30a5d9LFRZFb==8bO73cfarqe=kkkK09RmTUISi6qQwqZfChNd3Ktj6E3tj9GjXLWwV59vpUqOnFXIp9/rujWHt7v3KhIHMUrH70=mn1em1A7ujba3Y4jwqKyWRDR4q7/rCDFoyF7AiK4rNz018Ix0rYfYx+OYm2=nxNlxPGTKYStcOPuEDD', 'Referer': 'https://weibo.com/u/7735765253', 'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', 'Sec-Ch-Ua-Mobile': '?0', diff --git a/src/valuation_analysis/config.py b/src/valuation_analysis/config.py index 2335b28..f83e5e4 100644 --- a/src/valuation_analysis/config.py +++ b/src/valuation_analysis/config.py @@ -13,7 +13,14 @@ DB_CONFIG = { 'password': 'Chlry#$.8', 'database': 'db_gp_cj' } - +# redis配置 +REDIS_CONFIG = { + 'host': '192.168.18.208', + 'port': 6379, + 'password': 'wlkj2018', + 'db': 13, + 'socket_timeout': 5 +} # 创建数据库连接URL DB_URL = f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}" diff --git a/src/valuation_analysis/financial_analysis.py b/src/valuation_analysis/financial_analysis.py index c6a63b9..193bd41 100644 --- a/src/valuation_analysis/financial_analysis.py +++ b/src/valuation_analysis/financial_analysis.py @@ -877,11 +877,12 @@ class FinancialAnalyzer: # 缓存结果,有效期1天(86400秒) try: - redis_client.set( + kkk = redis_client.set( cache_key, json.dumps(result, default=str), # 使用default=str处理日期等特殊类型 ex=86400 # 1天的秒数 ) + print(kkk) logger.info(f"已缓存股票 {stock_code} 的财务分析数据,有效期为1天") except Exception as cache_error: logger.warning(f"缓存财务分析数据失败: {cache_error}") diff --git a/src/valuation_analysis/industry_analysis.py b/src/valuation_analysis/industry_analysis.py index 0ca871c..fc65389 100644 --- a/src/valuation_analysis/industry_analysis.py +++ b/src/valuation_analysis/industry_analysis.py @@ -824,4 +824,178 @@ class IndustryAnalyzer: except Exception as e: logger.error(f"获取概念板块综合分析失败: {e}") - return {"success": False, "message": f"获取概念板块综合分析失败: {e}"} \ No newline at end of file + return {"success": False, "message": f"获取概念板块综合分析失败: {e}"} + + def batch_calculate_industry_crowding(self, industries: List[str], concepts: List[str] = None) -> None: + """ + 批量计算多个行业和概念板块的拥挤度指标 + + Args: + industries: 行业列表 + concepts: 概念板块列表,默认为None + """ + try: + # 1. 获取3年数据的时间范围 + end_date = datetime.datetime.now().strftime('%Y-%m-%d') + start_date = (datetime.datetime.now() - datetime.timedelta(days=3*365)).strftime('%Y-%m-%d') + + # 2. 一次性获取全市场所有股票的交易数据 + query = text(""" + SELECT + symbol, + `timestamp` AS trade_date, + amount + FROM + gp_day_data + WHERE + `timestamp` BETWEEN :start_date AND :end_date + """) + + with self.engine.connect() as conn: + df_all = pd.read_sql(query, conn, params={"start_date": start_date, "end_date": end_date}) + + # 3. 计算每日市场总成交额 + df_total = df_all.groupby('trade_date')['amount'].sum().reset_index() + df_total.columns = ['trade_date', 'total_market_amount'] + + # 4. 获取所有行业和概念板块的股票映射 + industry_stocks = {} + for industry in industries: + stocks = self.get_industry_stocks(industry) + if stocks: + industry_stocks[industry] = stocks + + if concepts: + concept_stocks = {} + for concept in concepts: + stocks = self.get_concept_stocks(concept) + if stocks: + concept_stocks[concept] = stocks + + # 5. 批量计算行业拥挤度 + for industry, stocks in industry_stocks.items(): + try: + # 计算行业成交额 + df_industry = df_all[df_all['symbol'].isin(stocks)].groupby('trade_date')['amount'].sum().reset_index() + df_industry.columns = ['trade_date', 'sector_amount'] + + # 合并数据 + df = pd.merge(df_total, df_industry, on='trade_date', how='inner') + + # 计算指标 + df['industry_amount_ratio'] = (df['sector_amount'] / df['total_market_amount']) * 100 + df['percentile'] = df['industry_amount_ratio'].rank(pct=True) * 100 + df['crowding_level'] = pd.cut( + df['percentile'], + bins=[0, 20, 40, 60, 80, 100], + labels=['不拥挤', '较不拥挤', '中性', '较为拥挤', '极度拥挤'] + ) + + # 缓存结果 + cache_key = f"industry_crowding:{industry}" + redis_client.set( + cache_key, + json.dumps(df.to_dict(orient='records'), default=str), + ex=86400 + ) + + logger.info(f"成功计算行业 {industry} 的拥挤度指标,共 {len(df)} 条记录") + except Exception as e: + logger.error(f"计算行业 {industry} 的拥挤度指标时出错: {str(e)}") + continue + + # 6. 批量计算概念板块拥挤度 + if concepts: + for concept, stocks in concept_stocks.items(): + try: + # 计算概念板块成交额 + df_concept = df_all[df_all['symbol'].isin(stocks)].groupby('trade_date')['amount'].sum().reset_index() + df_concept.columns = ['trade_date', 'sector_amount'] + + # 合并数据 + df = pd.merge(df_total, df_concept, on='trade_date', how='inner') + + # 计算指标 + df['industry_amount_ratio'] = (df['sector_amount'] / df['total_market_amount']) * 100 + df['percentile'] = df['industry_amount_ratio'].rank(pct=True) * 100 + df['crowding_level'] = pd.cut( + df['percentile'], + bins=[0, 20, 40, 60, 80, 100], + labels=['不拥挤', '较不拥挤', '中性', '较为拥挤', '极度拥挤'] + ) + + # 缓存结果 + cache_key = f"concept_crowding:{concept}" + redis_client.set( + cache_key, + json.dumps(df.to_dict(orient='records'), default=str), + ex=86400 + ) + + logger.info(f"成功计算概念板块 {concept} 的拥挤度指标,共 {len(df)} 条记录") + except Exception as e: + logger.error(f"计算概念板块 {concept} 的拥挤度指标时出错: {str(e)}") + continue + + except Exception as e: + logger.error(f"批量计算行业拥挤度指标失败: {str(e)}") + + def filter_crowding_by_percentile(self, min_percentile: float, max_percentile: float) -> dict: + """ + 查询所有缓存中的行业和概念板块拥挤度,筛选最后一个交易日拥挤度百分位在[min, max]区间的行业/概念。 + 返回格式:{'industry': [...], 'concept': [...]},每个元素包含名称、最后日期、拥挤度百分位、拥挤度等级等。 + """ + result = {'industry': [], 'concept': []} + try: + # 获取所有行业和概念板块的缓存key + industry_keys = redis_client.keys('industry_crowding:*') + concept_keys = redis_client.keys('concept_crowding:*') + + # 行业 + for key in industry_keys: + try: + name = key.split(':', 1)[1] + cached_data = redis_client.get(key) + if not cached_data: + continue + df = pd.DataFrame(json.loads(cached_data)) + if df.empty: + continue + last_row = df.iloc[-1] + percentile = float(last_row['percentile']) + if min_percentile <= percentile <= max_percentile: + result['industry'].append({ + 'name': name, + 'last_date': last_row['trade_date'], + 'percentile': percentile, + 'crowding_level': last_row.get('crowding_level', None) + }) + except Exception as e: + logger.warning(f"处理行业缓存 {key} 时出错: {e}") + continue + + # 概念板块 + for key in concept_keys: + try: + name = key.split(':', 1)[1] + cached_data = redis_client.get(key) + if not cached_data: + continue + df = pd.DataFrame(json.loads(cached_data)) + if df.empty: + continue + last_row = df.iloc[-1] + percentile = float(last_row['percentile']) + if min_percentile <= percentile <= max_percentile: + result['concept'].append({ + 'name': name, + 'last_date': last_row['trade_date'], + 'percentile': percentile, + 'crowding_level': last_row.get('crowding_level', None) + }) + except Exception as e: + logger.warning(f"处理概念缓存 {key} 时出错: {e}") + continue + except Exception as e: + logger.error(f"筛选拥挤度缓存时出错: {e}") + return result \ No newline at end of file