| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- # ETF月线级别均线分析工具
- # 功能:
- # 1. 获取50ETF、300ETF、创业板ETF的价格数据
- # 2. 计算月线级别的5、10、20日均线
- # 3. 计算当月收盘价与各均线的距离
- # 4. 生成可视化图表和数据导出
- import jqdata
- from jqdata import *
- import pandas as pd
- import numpy as np
- import matplotlib.pyplot as plt
- import datetime
- from typing import Dict, List, Tuple
- # 设置中文字体
- plt.rcParams['font.sans-serif'] = ['SimHei']
- plt.rcParams['axes.unicode_minus'] = False
- class ETFMonthlyMAAnalysis:
- """ETF月线级别均线分析器"""
-
- def __init__(self, start_date='2024-01-01', end_date='2024-12-31'):
- """
- 初始化分析器
- :param start_date: 开始日期
- :param end_date: 结束日期
- """
- self.start_date = start_date
- self.end_date = end_date
-
- # ETF配置 - 基于现有策略的符号定义
- self.etf_symbols = {
- '50ETF': '510050.XSHG', # 上证50ETF
- '300ETF': '510300.XSHG', # 沪深300ETF (用户提到的500ETF应该是指这个)
- '创业板ETF': '159915.XSHE' # 创业板ETF
- }
-
- # 移动平均线配置
- self.ma_periods = [5, 10, 20] # 5日、10日、20日均线
-
- # 存储数据
- self.price_data = {}
- self.monthly_data = {}
- self.analysis_results = {}
-
- print(f"初始化ETF月线分析器")
- print(f"分析时间范围: {start_date} 到 {end_date}")
- print(f"分析ETF: {list(self.etf_symbols.keys())}")
- print(f"均线周期: {self.ma_periods}日")
-
- def fetch_etf_prices(self) -> Dict[str, pd.DataFrame]:
- """
- 获取所有ETF的价格数据
- 返回: {etf_name: price_dataframe}
- """
- print(f"\n开始获取ETF价格数据...")
-
- for etf_name, symbol in self.etf_symbols.items():
- try:
- print(f" 获取{etf_name}({symbol})价格数据...")
-
- # 使用现有策略的get_price方法获取数据
- price_data = get_price(
- symbol,
- start_date=self.start_date,
- end_date=self.end_date,
- fields=['close', 'open', 'high', 'low', 'volume'],
- frequency='daily'
- )
-
- if price_data.empty:
- print(f" 警告: {etf_name}无价格数据")
- continue
-
- # 确保索引为日期格式
- if not isinstance(price_data.index, pd.DatetimeIndex):
- price_data.index = pd.to_datetime(price_data.index)
-
- self.price_data[etf_name] = price_data
-
- print(f" 成功获取{etf_name}数据: {len(price_data)}条记录")
- print(f" 数据范围: {price_data.index[0].strftime('%Y-%m-%d')} 到 {price_data.index[-1].strftime('%Y-%m-%d')}")
- print(f" 价格范围: {price_data['close'].min():.3f} - {price_data['close'].max():.3f}")
-
- except Exception as e:
- print(f" 错误: 获取{etf_name}价格数据失败 - {e}")
- continue
-
- print(f"\nETF价格数据获取完成,成功获取{len(self.price_data)}个ETF的数据")
- return self.price_data
-
- def calculate_monthly_ma(self) -> Dict[str, pd.DataFrame]:
- """
- 计算月线级别的移动平均线
- 返回: {etf_name: monthly_data_with_ma}
- """
- print(f"\n开始计算月线级别的移动平均线...")
-
- for etf_name, price_df in self.price_data.items():
- try:
- print(f" 处理{etf_name}的月线数据...")
-
- # 转换为月线数据 - 取每月最后一个交易日的数据
- monthly_df = price_df.resample('M').agg({
- 'open': 'first', # 月初开盘价
- 'high': 'max', # 月内最高价
- 'low': 'min', # 月内最低价
- 'close': 'last', # 月末收盘价
- 'volume': 'sum' # 月成交量
- })
-
- # 计算移动平均线
- for period in self.ma_periods:
- ma_column = f'MA{period}'
- monthly_df[ma_column] = monthly_df['close'].rolling(window=period).mean()
- print(f" 计算{period}日均线完成")
-
- # 计算当月收盘价与各均线的距离
- for period in self.ma_periods:
- ma_column = f'MA{period}'
- distance_column = f'Distance_MA{period}'
- monthly_df[distance_column] = monthly_df['close'] - monthly_df[ma_column]
-
- # 计算距离的百分比
- pct_distance_column = f'Distance_MA{period}_Pct'
- monthly_df[pct_distance_column] = (monthly_df['close'] - monthly_df[ma_column]) / monthly_df[ma_column] * 100
-
- # 添加月份标识
- monthly_df['年月'] = monthly_df.index.strftime('%Y-%m')
-
- self.monthly_data[etf_name] = monthly_df
-
- print(f" {etf_name}月线数据计算完成: {len(monthly_df)}个月")
-
- # 显示最近几个月的数据示例
- if len(monthly_df) > 0:
- latest_data = monthly_df.tail(3)
- print(f" 最近数据示例:")
- for idx, row in latest_data.iterrows():
- month_str = idx.strftime('%Y-%m')
- close_price = row['close']
- ma5_dist = row.get('Distance_MA5', np.nan)
- ma10_dist = row.get('Distance_MA10', np.nan)
- ma20_dist = row.get('Distance_MA20', np.nan)
- print(f" {month_str}: 收盘{close_price:.3f}, 距MA5={ma5_dist:.3f}, 距MA10={ma10_dist:.3f}, 距MA20={ma20_dist:.3f}")
-
- except Exception as e:
- print(f" 错误: 处理{etf_name}月线数据失败 - {e}")
- continue
-
- print(f"\n月线移动平均线计算完成,处理了{len(self.monthly_data)}个ETF")
- return self.monthly_data
-
- def analyze_distance_statistics(self) -> Dict[str, Dict]:
- """
- 分析距离统计信息
- 返回: {etf_name: statistics}
- """
- print(f"\n开始分析距离统计信息...")
-
- for etf_name, monthly_df in self.monthly_data.items():
- try:
- print(f" 分析{etf_name}的距离统计...")
-
- stats = {}
-
- # 对每个均线周期进行统计
- for period in self.ma_periods:
- distance_col = f'Distance_MA{period}'
- pct_distance_col = f'Distance_MA{period}_Pct'
-
- if distance_col in monthly_df.columns:
- distance_data = monthly_df[distance_col].dropna()
- pct_distance_data = monthly_df[pct_distance_col].dropna()
-
- if len(distance_data) > 0:
- stats[f'MA{period}'] = {
- '绝对距离_均值': distance_data.mean(),
- '绝对距离_标准差': distance_data.std(),
- '绝对距离_最大值': distance_data.max(),
- '绝对距离_最小值': distance_data.min(),
- '百分比距离_均值': pct_distance_data.mean(),
- '百分比距离_标准差': pct_distance_data.std(),
- '百分比距离_最大值': pct_distance_data.max(),
- '百分比距离_最小值': pct_distance_data.min(),
- '正向距离次数': (distance_data > 0).sum(),
- '负向距离次数': (distance_data < 0).sum(),
- '数据点总数': len(distance_data)
- }
-
- print(f" MA{period}统计: 均值={stats[f'MA{period}']['绝对距离_均值']:.3f}, "
- f"正向{stats[f'MA{period}']['正向距离次数']}次, "
- f"负向{stats[f'MA{period}']['负向距离次数']}次")
-
- self.analysis_results[etf_name] = stats
-
- except Exception as e:
- print(f" 错误: 分析{etf_name}距离统计失败 - {e}")
- continue
-
- print(f"\n距离统计分析完成")
- return self.analysis_results
-
- def plot_distance_chart(self, save_path: str = None):
- """
- 绘制距离图表
- :param save_path: 保存路径,如果为None则直接显示
- """
- print(f"\n开始绘制距离图表...")
-
- # 设置图表布局:每个ETF一个子图
- n_etfs = len(self.monthly_data)
- if n_etfs == 0:
- print("没有数据可以绘制")
- return
-
- fig, axes = plt.subplots(n_etfs, 1, figsize=(14, 6 * n_etfs))
- if n_etfs == 1:
- axes = [axes] # 确保axes是列表
-
- # 颜色配置
- colors = ['red', 'blue', 'green']
- ma_labels = [f'距MA{period}日线' for period in self.ma_periods]
-
- for idx, (etf_name, monthly_df) in enumerate(self.monthly_data.items()):
- ax = axes[idx]
-
- # 准备数据
- months = monthly_df.index
- month_labels = [dt.strftime('%Y-%m') for dt in months]
-
- # 绘制距离线
- for i, period in enumerate(self.ma_periods):
- distance_col = f'Distance_MA{period}'
- if distance_col in monthly_df.columns:
- distance_data = monthly_df[distance_col]
- ax.plot(months, distance_data,
- label=ma_labels[i],
- color=colors[i],
- linewidth=2,
- marker='o',
- markersize=4)
-
- # 添加零线
- ax.axhline(y=0, color='black', linestyle='--', alpha=0.5, linewidth=1)
-
- # 设置标题和标签
- ax.set_title(f'{etf_name} 月收盘价与移动平均线距离', fontsize=14, fontweight='bold')
- ax.set_xlabel('月份', fontsize=12)
- ax.set_ylabel('距离 (元)', fontsize=12)
- ax.legend(fontsize=10)
- ax.grid(True, alpha=0.3)
-
- # 设置x轴标签
- if len(months) > 12:
- # 如果月份太多,只显示部分标签
- step = max(1, len(months) // 12)
- ax.set_xticks(months[::step])
- ax.set_xticklabels([dt.strftime('%Y-%m') for dt in months[::step]], rotation=45)
- else:
- ax.set_xticklabels(month_labels, rotation=45)
-
- # 添加统计信息文本
- if etf_name in self.analysis_results:
- stats_text = []
- for period in self.ma_periods:
- ma_key = f'MA{period}'
- if ma_key in self.analysis_results[etf_name]:
- mean_dist = self.analysis_results[etf_name][ma_key]['绝对距离_均值']
- positive_count = self.analysis_results[etf_name][ma_key]['正向距离次数']
- negative_count = self.analysis_results[etf_name][ma_key]['负向距离次数']
- stats_text.append(f'MA{period}: 均值{mean_dist:.3f}, 正{positive_count}负{negative_count}')
-
- if stats_text:
- ax.text(0.02, 0.98, '\n'.join(stats_text),
- transform=ax.transAxes,
- fontsize=9,
- verticalalignment='top',
- bbox=dict(boxstyle='round,pad=0.3', facecolor='lightgray', alpha=0.8))
-
- plt.tight_layout()
-
- if save_path:
- plt.savefig(save_path, dpi=300, bbox_inches='tight')
- print(f"图表已保存到: {save_path}")
- else:
- plt.show()
-
- print("距离图表绘制完成")
-
- def export_results_to_csv(self, output_dir: str = "./"):
- """
- 导出结果到CSV文件
- :param output_dir: 输出目录
- """
- print(f"\n开始导出结果到CSV文件...")
-
- for etf_name, monthly_df in self.monthly_data.items():
- try:
- # 准备导出数据
- export_df = monthly_df.copy()
-
- # 重新排列列的顺序,把重要信息放在前面
- column_order = ['年月', 'close']
-
- # 添加均线列
- for period in self.ma_periods:
- column_order.append(f'MA{period}')
-
- # 添加距离列
- for period in self.ma_periods:
- column_order.append(f'Distance_MA{period}')
- column_order.append(f'Distance_MA{period}_Pct')
-
- # 添加其他列
- other_columns = [col for col in export_df.columns if col not in column_order]
- column_order.extend(other_columns)
-
- # 重新排序并导出
- export_df = export_df.reindex(columns=column_order)
-
- # 构建文件名
- filename = f"{output_dir}/ETF_月线分析_{etf_name}_{self.start_date}_{self.end_date}.csv"
- export_df.to_csv(filename, encoding='utf-8-sig', index=True)
-
- print(f" {etf_name}数据已导出到: {filename}")
-
- except Exception as e:
- print(f" 错误: 导出{etf_name}数据失败 - {e}")
-
- print("CSV导出完成")
-
- def run_complete_analysis(self, save_chart: bool = True, export_csv: bool = True):
- """
- 运行完整的分析流程
- :param save_chart: 是否保存图表
- :param export_csv: 是否导出CSV
- """
- print("="*60)
- print("开始ETF月线级别均线距离分析")
- print("="*60)
-
- try:
- # 1. 获取ETF价格数据
- print(f"\n步骤1: 获取ETF价格数据")
- self.fetch_etf_prices()
-
- if not self.price_data:
- print("错误: 没有成功获取任何ETF价格数据")
- return
-
- # 2. 计算月线均线
- print(f"\n步骤2: 计算月线级别移动平均线")
- self.calculate_monthly_ma()
-
- if not self.monthly_data:
- print("错误: 没有成功计算任何月线数据")
- return
-
- # 3. 分析距离统计
- print(f"\n步骤3: 分析距离统计信息")
- self.analyze_distance_statistics()
-
- # 4. 绘制图表
- print(f"\n步骤4: 绘制距离图表")
- if save_chart:
- chart_filename = f"ETF月线距离分析_{self.start_date}_{self.end_date}.png"
- self.plot_distance_chart(save_path=chart_filename)
- else:
- self.plot_distance_chart()
-
- # 5. 导出CSV
- if export_csv:
- print(f"\n步骤5: 导出分析结果")
- self.export_results_to_csv()
-
- # 6. 输出总结
- print(f"\n" + "="*60)
- print("分析总结")
- print("="*60)
-
- for etf_name, stats in self.analysis_results.items():
- print(f"\n{etf_name}分析结果:")
- for ma_period, stat_data in stats.items():
- print(f" {ma_period}:")
- print(f" 平均距离: {stat_data['绝对距离_均值']:.3f} 元")
- print(f" 平均距离百分比: {stat_data['百分比距离_均值']:.2f}%")
- print(f" 正向/负向比例: {stat_data['正向距离次数']}/{stat_data['负向距离次数']}")
-
- print(f"\n分析完成! 总共处理了{len(self.monthly_data)}个ETF,{sum(len(df) for df in self.monthly_data.values())}个月度数据点")
-
- except Exception as e:
- print(f"分析过程中发生错误: {e}")
- import traceback
- traceback.print_exc()
- # =============================================================================
- # 直接运行分析 - 可以修改下面的参数来自定义分析
- # =============================================================================
- print("="*60)
- print("ETF月线级别均线距离分析工具")
- print("="*60)
- print("功能:分析ETF价格与5、10、20日均线的距离关系")
- print("ETF标的:50ETF、300ETF、创业板ETF")
- print()
- # 分析参数配置(可根据需要修改)
- START_DATE = '2022-01-01' # 开始日期
- END_DATE = '2024-12-31' # 结束日期
- print(f"分析时间范围: {START_DATE} 到 {END_DATE}")
- print()
- # 创建分析器实例
- analyzer = ETFMonthlyMAAnalysis(
- start_date=START_DATE,
- end_date=END_DATE
- )
- # 运行完整分析
- analyzer.run_complete_analysis(
- save_chart=False, # 保存图表到文件
- export_csv=False # 导出CSV数据
- )
|