commit;
This commit is contained in:
		
							parent
							
								
									ab27f46c87
								
							
						
					
					
						commit
						a0de689b3b
					
				
							
								
								
									
										69
									
								
								src/app.py
								
								
								
								
							
							
						
						
									
										69
									
								
								src/app.py
								
								
								
								
							|  | @ -1,3 +1,4 @@ | ||||||
|  | # -*- coding: utf-8 -*- | ||||||
| import sys | import sys | ||||||
| import os | import os | ||||||
| from datetime import datetime, timedelta | 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 valuation_analysis.financial_analysis import FinancialAnalyzer | ||||||
| from src.valuation_analysis.stock_price_collector import StockPriceCollector | 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( | logging.basicConfig( | ||||||
|  | @ -227,7 +229,7 @@ def run_rzrq_initial_collection1(): | ||||||
|     }), 200 |     }), 200 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @app.route('/scheduler/industry/crowding', methods=['GET']) | @app.route('/scheduler/industry/crowding1/bak', methods=['GET']) | ||||||
| def precalculate_industry_crowding1(): | def precalculate_industry_crowding1(): | ||||||
|     """预计算部分行业和概念板块的拥挤度指标 晚上10点开始""" |     """预计算部分行业和概念板块的拥挤度指标 晚上10点开始""" | ||||||
|     try: |     try: | ||||||
|  | @ -337,6 +339,27 @@ def scheduler_financial_analysis(): | ||||||
|         "status": "success" |         "status": "success" | ||||||
|     }), 200 |     }), 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('/') | @app.route('/') | ||||||
| def index(): | def index(): | ||||||
|     """渲染主页""" |     """渲染主页""" | ||||||
|  | @ -2863,17 +2886,53 @@ def get_pep_stock_info_by_shortname(): | ||||||
| 
 | 
 | ||||||
| @app.route('/api/pep_stock_info_by_code', methods=['GET']) | @app.route('/api/pep_stock_info_by_code', methods=['GET']) | ||||||
| def get_pep_stock_info_by_code(): | def get_pep_stock_info_by_code(): | ||||||
|     """根据股票简称查询pep_stock_info集合中的全部字段""" |     """根据股票代码查询Redis中的实时行情并返回指定结构""" | ||||||
|     short_code = request.args.get('code') |     short_code = request.args.get('code') | ||||||
|     if not short_code: |     if not short_code: | ||||||
|         return jsonify({'success': False, 'message': '缺少必要参数: short_code'}), 400 |         return jsonify({'success': False, 'message': '缺少必要参数: short_code'}), 400 | ||||||
|     try: |     try: | ||||||
|         analyzer = FinancialAnalyzer() |         # 兼容600001.SH/SH600001等格式 | ||||||
|         result = analyzer.get_pep_stock_info_by_code(short_code) |         from src.quantitative_analysis.batch_stock_price_collector import get_stock_realtime_info_from_redis | ||||||
|         return jsonify(result) |         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: |     except Exception as e: | ||||||
|         return jsonify({'success': False, 'message': f'服务器错误: {str(e)}'}), 500 |         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__': | if __name__ == '__main__': | ||||||
| 
 | 
 | ||||||
|     # 启动Web服务器 |     # 启动Web服务器 | ||||||
|  |  | ||||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -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 | ||||||
| 
 | 
|  | @ -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. **回测框架** - 策略回测和绩效分析 | ||||||
|  | 
 | ||||||
|  | ## 联系方式 | ||||||
|  | 
 | ||||||
|  | 如有问题或建议,请联系开发团队。  | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | """ | ||||||
|  | 量化分析工程模块 | ||||||
|  | 
 | ||||||
|  | 主要功能: | ||||||
|  | 1. 财务数据采集 | ||||||
|  | 2. 数据处理和分析 | ||||||
|  | 3. 量化策略开发 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | __version__ = "1.0.0"  | ||||||
|  | @ -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()  | ||||||
|  | @ -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)  | ||||||
|  | @ -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)  | ||||||
|  | @ -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() | ||||||
|  | @ -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()  | ||||||
|  | @ -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)  | ||||||
|  | @ -11,7 +11,7 @@ XUEQIU_HEADERS = { | ||||||
|     'Accept-Encoding': 'gzip, deflate, br, zstd', |     'Accept-Encoding': 'gzip, deflate, br, zstd', | ||||||
|     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', |     'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', | ||||||
|     'Client-Version': 'v2.44.75', |     'Client-Version': 'v2.44.75', | ||||||
|     'Cookie': 'cookiesu=811743062689927; device_id=33fa3c7fca4a65f8f4354e10ed6b7470; smidV2=20250327160437f244626e8b47ca2a7992f30f389e4e790074ae48656a22f10; HMACCOUNT=8B64A2E3C307C8C0; s=c611ttmqlj; xq_is_login=1; u=8493411634; bid=4065a77ca57a69c83405d6e591ab5449_m8r2nhs8; 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', |     'Referer': 'https://weibo.com/u/7735765253', | ||||||
|     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', |     'Sec-Ch-Ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"', | ||||||
|     'Sec-Ch-Ua-Mobile': '?0', |     'Sec-Ch-Ua-Mobile': '?0', | ||||||
|  |  | ||||||
|  | @ -13,7 +13,14 @@ DB_CONFIG = { | ||||||
|     'password': 'Chlry#$.8', |     'password': 'Chlry#$.8', | ||||||
|     'database': 'db_gp_cj' |     'database': 'db_gp_cj' | ||||||
| } | } | ||||||
| 
 | # redis配置 | ||||||
|  | REDIS_CONFIG = { | ||||||
|  |     'host': '192.168.18.208', | ||||||
|  |     'port': 6379, | ||||||
|  |     'password': 'wlkj2018', | ||||||
|  |     'db': 13, | ||||||
|  |     'socket_timeout': 5 | ||||||
|  | } | ||||||
| # 创建数据库连接URL | # 创建数据库连接URL | ||||||
| DB_URL = f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}" | DB_URL = f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -877,11 +877,12 @@ class FinancialAnalyzer: | ||||||
|              |              | ||||||
|             # 缓存结果,有效期1天(86400秒) |             # 缓存结果,有效期1天(86400秒) | ||||||
|             try: |             try: | ||||||
|                 redis_client.set( |                 kkk = redis_client.set( | ||||||
|                     cache_key, |                     cache_key, | ||||||
|                     json.dumps(result, default=str),  # 使用default=str处理日期等特殊类型 |                     json.dumps(result, default=str),  # 使用default=str处理日期等特殊类型 | ||||||
|                     ex=86400  # 1天的秒数 |                     ex=86400  # 1天的秒数 | ||||||
|                 ) |                 ) | ||||||
|  |                 print(kkk) | ||||||
|                 logger.info(f"已缓存股票 {stock_code} 的财务分析数据,有效期为1天") |                 logger.info(f"已缓存股票 {stock_code} 的财务分析数据,有效期为1天") | ||||||
|             except Exception as cache_error: |             except Exception as cache_error: | ||||||
|                 logger.warning(f"缓存财务分析数据失败: {cache_error}") |                 logger.warning(f"缓存财务分析数据失败: {cache_error}") | ||||||
|  |  | ||||||
|  | @ -824,4 +824,178 @@ class IndustryAnalyzer: | ||||||
|              |              | ||||||
|         except Exception as e: |         except Exception as e: | ||||||
|             logger.error(f"获取概念板块综合分析失败: {e}") |             logger.error(f"获取概念板块综合分析失败: {e}") | ||||||
|             return {"success": False, "message": f"获取概念板块综合分析失败: {e}"}  |             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  | ||||||
		Loading…
	
		Reference in New Issue