etf_monthly_ma_analysis.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. # ETF月线级别均线分析工具
  2. # 功能:
  3. # 1. 获取50ETF、300ETF、创业板ETF的价格数据
  4. # 2. 计算月线级别的5、10、20日均线
  5. # 3. 计算当月收盘价与各均线的距离
  6. # 4. 生成可视化图表和数据导出
  7. import jqdata
  8. from jqdata import *
  9. import pandas as pd
  10. import numpy as np
  11. import matplotlib.pyplot as plt
  12. import datetime
  13. from typing import Dict, List, Tuple
  14. # 设置中文字体
  15. plt.rcParams['font.sans-serif'] = ['SimHei']
  16. plt.rcParams['axes.unicode_minus'] = False
  17. class ETFMonthlyMAAnalysis:
  18. """ETF月线级别均线分析器"""
  19. def __init__(self, start_date='2024-01-01', end_date='2024-12-31'):
  20. """
  21. 初始化分析器
  22. :param start_date: 开始日期
  23. :param end_date: 结束日期
  24. """
  25. self.start_date = start_date
  26. self.end_date = end_date
  27. # ETF配置 - 基于现有策略的符号定义
  28. self.etf_symbols = {
  29. '50ETF': '510050.XSHG', # 上证50ETF
  30. '300ETF': '510300.XSHG', # 沪深300ETF (用户提到的500ETF应该是指这个)
  31. '创业板ETF': '159915.XSHE' # 创业板ETF
  32. }
  33. # 移动平均线配置
  34. self.ma_periods = [5, 10, 20] # 5日、10日、20日均线
  35. # 存储数据
  36. self.price_data = {}
  37. self.monthly_data = {}
  38. self.analysis_results = {}
  39. print(f"初始化ETF月线分析器")
  40. print(f"分析时间范围: {start_date} 到 {end_date}")
  41. print(f"分析ETF: {list(self.etf_symbols.keys())}")
  42. print(f"均线周期: {self.ma_periods}日")
  43. def fetch_etf_prices(self) -> Dict[str, pd.DataFrame]:
  44. """
  45. 获取所有ETF的价格数据
  46. 返回: {etf_name: price_dataframe}
  47. """
  48. print(f"\n开始获取ETF价格数据...")
  49. for etf_name, symbol in self.etf_symbols.items():
  50. try:
  51. print(f" 获取{etf_name}({symbol})价格数据...")
  52. # 使用现有策略的get_price方法获取数据
  53. price_data = get_price(
  54. symbol,
  55. start_date=self.start_date,
  56. end_date=self.end_date,
  57. fields=['close', 'open', 'high', 'low', 'volume'],
  58. frequency='daily'
  59. )
  60. if price_data.empty:
  61. print(f" 警告: {etf_name}无价格数据")
  62. continue
  63. # 确保索引为日期格式
  64. if not isinstance(price_data.index, pd.DatetimeIndex):
  65. price_data.index = pd.to_datetime(price_data.index)
  66. self.price_data[etf_name] = price_data
  67. print(f" 成功获取{etf_name}数据: {len(price_data)}条记录")
  68. print(f" 数据范围: {price_data.index[0].strftime('%Y-%m-%d')} 到 {price_data.index[-1].strftime('%Y-%m-%d')}")
  69. print(f" 价格范围: {price_data['close'].min():.3f} - {price_data['close'].max():.3f}")
  70. except Exception as e:
  71. print(f" 错误: 获取{etf_name}价格数据失败 - {e}")
  72. continue
  73. print(f"\nETF价格数据获取完成,成功获取{len(self.price_data)}个ETF的数据")
  74. return self.price_data
  75. def calculate_monthly_ma(self) -> Dict[str, pd.DataFrame]:
  76. """
  77. 计算月线级别的移动平均线
  78. 返回: {etf_name: monthly_data_with_ma}
  79. """
  80. print(f"\n开始计算月线级别的移动平均线...")
  81. for etf_name, price_df in self.price_data.items():
  82. try:
  83. print(f" 处理{etf_name}的月线数据...")
  84. # 转换为月线数据 - 取每月最后一个交易日的数据
  85. monthly_df = price_df.resample('M').agg({
  86. 'open': 'first', # 月初开盘价
  87. 'high': 'max', # 月内最高价
  88. 'low': 'min', # 月内最低价
  89. 'close': 'last', # 月末收盘价
  90. 'volume': 'sum' # 月成交量
  91. })
  92. # 计算移动平均线
  93. for period in self.ma_periods:
  94. ma_column = f'MA{period}'
  95. monthly_df[ma_column] = monthly_df['close'].rolling(window=period).mean()
  96. print(f" 计算{period}日均线完成")
  97. # 计算当月收盘价与各均线的距离
  98. for period in self.ma_periods:
  99. ma_column = f'MA{period}'
  100. distance_column = f'Distance_MA{period}'
  101. monthly_df[distance_column] = monthly_df['close'] - monthly_df[ma_column]
  102. # 计算距离的百分比
  103. pct_distance_column = f'Distance_MA{period}_Pct'
  104. monthly_df[pct_distance_column] = (monthly_df['close'] - monthly_df[ma_column]) / monthly_df[ma_column] * 100
  105. # 添加月份标识
  106. monthly_df['年月'] = monthly_df.index.strftime('%Y-%m')
  107. self.monthly_data[etf_name] = monthly_df
  108. print(f" {etf_name}月线数据计算完成: {len(monthly_df)}个月")
  109. # 显示最近几个月的数据示例
  110. if len(monthly_df) > 0:
  111. latest_data = monthly_df.tail(3)
  112. print(f" 最近数据示例:")
  113. for idx, row in latest_data.iterrows():
  114. month_str = idx.strftime('%Y-%m')
  115. close_price = row['close']
  116. ma5_dist = row.get('Distance_MA5', np.nan)
  117. ma10_dist = row.get('Distance_MA10', np.nan)
  118. ma20_dist = row.get('Distance_MA20', np.nan)
  119. print(f" {month_str}: 收盘{close_price:.3f}, 距MA5={ma5_dist:.3f}, 距MA10={ma10_dist:.3f}, 距MA20={ma20_dist:.3f}")
  120. except Exception as e:
  121. print(f" 错误: 处理{etf_name}月线数据失败 - {e}")
  122. continue
  123. print(f"\n月线移动平均线计算完成,处理了{len(self.monthly_data)}个ETF")
  124. return self.monthly_data
  125. def analyze_distance_statistics(self) -> Dict[str, Dict]:
  126. """
  127. 分析距离统计信息
  128. 返回: {etf_name: statistics}
  129. """
  130. print(f"\n开始分析距离统计信息...")
  131. for etf_name, monthly_df in self.monthly_data.items():
  132. try:
  133. print(f" 分析{etf_name}的距离统计...")
  134. stats = {}
  135. # 对每个均线周期进行统计
  136. for period in self.ma_periods:
  137. distance_col = f'Distance_MA{period}'
  138. pct_distance_col = f'Distance_MA{period}_Pct'
  139. if distance_col in monthly_df.columns:
  140. distance_data = monthly_df[distance_col].dropna()
  141. pct_distance_data = monthly_df[pct_distance_col].dropna()
  142. if len(distance_data) > 0:
  143. stats[f'MA{period}'] = {
  144. '绝对距离_均值': distance_data.mean(),
  145. '绝对距离_标准差': distance_data.std(),
  146. '绝对距离_最大值': distance_data.max(),
  147. '绝对距离_最小值': distance_data.min(),
  148. '百分比距离_均值': pct_distance_data.mean(),
  149. '百分比距离_标准差': pct_distance_data.std(),
  150. '百分比距离_最大值': pct_distance_data.max(),
  151. '百分比距离_最小值': pct_distance_data.min(),
  152. '正向距离次数': (distance_data > 0).sum(),
  153. '负向距离次数': (distance_data < 0).sum(),
  154. '数据点总数': len(distance_data)
  155. }
  156. print(f" MA{period}统计: 均值={stats[f'MA{period}']['绝对距离_均值']:.3f}, "
  157. f"正向{stats[f'MA{period}']['正向距离次数']}次, "
  158. f"负向{stats[f'MA{period}']['负向距离次数']}次")
  159. self.analysis_results[etf_name] = stats
  160. except Exception as e:
  161. print(f" 错误: 分析{etf_name}距离统计失败 - {e}")
  162. continue
  163. print(f"\n距离统计分析完成")
  164. return self.analysis_results
  165. def plot_distance_chart(self, save_path: str = None):
  166. """
  167. 绘制距离图表
  168. :param save_path: 保存路径,如果为None则直接显示
  169. """
  170. print(f"\n开始绘制距离图表...")
  171. # 设置图表布局:每个ETF一个子图
  172. n_etfs = len(self.monthly_data)
  173. if n_etfs == 0:
  174. print("没有数据可以绘制")
  175. return
  176. fig, axes = plt.subplots(n_etfs, 1, figsize=(14, 6 * n_etfs))
  177. if n_etfs == 1:
  178. axes = [axes] # 确保axes是列表
  179. # 颜色配置
  180. colors = ['red', 'blue', 'green']
  181. ma_labels = [f'距MA{period}日线' for period in self.ma_periods]
  182. for idx, (etf_name, monthly_df) in enumerate(self.monthly_data.items()):
  183. ax = axes[idx]
  184. # 准备数据
  185. months = monthly_df.index
  186. month_labels = [dt.strftime('%Y-%m') for dt in months]
  187. # 绘制距离线
  188. for i, period in enumerate(self.ma_periods):
  189. distance_col = f'Distance_MA{period}'
  190. if distance_col in monthly_df.columns:
  191. distance_data = monthly_df[distance_col]
  192. ax.plot(months, distance_data,
  193. label=ma_labels[i],
  194. color=colors[i],
  195. linewidth=2,
  196. marker='o',
  197. markersize=4)
  198. # 添加零线
  199. ax.axhline(y=0, color='black', linestyle='--', alpha=0.5, linewidth=1)
  200. # 设置标题和标签
  201. ax.set_title(f'{etf_name} 月收盘价与移动平均线距离', fontsize=14, fontweight='bold')
  202. ax.set_xlabel('月份', fontsize=12)
  203. ax.set_ylabel('距离 (元)', fontsize=12)
  204. ax.legend(fontsize=10)
  205. ax.grid(True, alpha=0.3)
  206. # 设置x轴标签
  207. if len(months) > 12:
  208. # 如果月份太多,只显示部分标签
  209. step = max(1, len(months) // 12)
  210. ax.set_xticks(months[::step])
  211. ax.set_xticklabels([dt.strftime('%Y-%m') for dt in months[::step]], rotation=45)
  212. else:
  213. ax.set_xticklabels(month_labels, rotation=45)
  214. # 添加统计信息文本
  215. if etf_name in self.analysis_results:
  216. stats_text = []
  217. for period in self.ma_periods:
  218. ma_key = f'MA{period}'
  219. if ma_key in self.analysis_results[etf_name]:
  220. mean_dist = self.analysis_results[etf_name][ma_key]['绝对距离_均值']
  221. positive_count = self.analysis_results[etf_name][ma_key]['正向距离次数']
  222. negative_count = self.analysis_results[etf_name][ma_key]['负向距离次数']
  223. stats_text.append(f'MA{period}: 均值{mean_dist:.3f}, 正{positive_count}负{negative_count}')
  224. if stats_text:
  225. ax.text(0.02, 0.98, '\n'.join(stats_text),
  226. transform=ax.transAxes,
  227. fontsize=9,
  228. verticalalignment='top',
  229. bbox=dict(boxstyle='round,pad=0.3', facecolor='lightgray', alpha=0.8))
  230. plt.tight_layout()
  231. if save_path:
  232. plt.savefig(save_path, dpi=300, bbox_inches='tight')
  233. print(f"图表已保存到: {save_path}")
  234. else:
  235. plt.show()
  236. print("距离图表绘制完成")
  237. def export_results_to_csv(self, output_dir: str = "./"):
  238. """
  239. 导出结果到CSV文件
  240. :param output_dir: 输出目录
  241. """
  242. print(f"\n开始导出结果到CSV文件...")
  243. for etf_name, monthly_df in self.monthly_data.items():
  244. try:
  245. # 准备导出数据
  246. export_df = monthly_df.copy()
  247. # 重新排列列的顺序,把重要信息放在前面
  248. column_order = ['年月', 'close']
  249. # 添加均线列
  250. for period in self.ma_periods:
  251. column_order.append(f'MA{period}')
  252. # 添加距离列
  253. for period in self.ma_periods:
  254. column_order.append(f'Distance_MA{period}')
  255. column_order.append(f'Distance_MA{period}_Pct')
  256. # 添加其他列
  257. other_columns = [col for col in export_df.columns if col not in column_order]
  258. column_order.extend(other_columns)
  259. # 重新排序并导出
  260. export_df = export_df.reindex(columns=column_order)
  261. # 构建文件名
  262. filename = f"{output_dir}/ETF_月线分析_{etf_name}_{self.start_date}_{self.end_date}.csv"
  263. export_df.to_csv(filename, encoding='utf-8-sig', index=True)
  264. print(f" {etf_name}数据已导出到: {filename}")
  265. except Exception as e:
  266. print(f" 错误: 导出{etf_name}数据失败 - {e}")
  267. print("CSV导出完成")
  268. def run_complete_analysis(self, save_chart: bool = True, export_csv: bool = True):
  269. """
  270. 运行完整的分析流程
  271. :param save_chart: 是否保存图表
  272. :param export_csv: 是否导出CSV
  273. """
  274. print("="*60)
  275. print("开始ETF月线级别均线距离分析")
  276. print("="*60)
  277. try:
  278. # 1. 获取ETF价格数据
  279. print(f"\n步骤1: 获取ETF价格数据")
  280. self.fetch_etf_prices()
  281. if not self.price_data:
  282. print("错误: 没有成功获取任何ETF价格数据")
  283. return
  284. # 2. 计算月线均线
  285. print(f"\n步骤2: 计算月线级别移动平均线")
  286. self.calculate_monthly_ma()
  287. if not self.monthly_data:
  288. print("错误: 没有成功计算任何月线数据")
  289. return
  290. # 3. 分析距离统计
  291. print(f"\n步骤3: 分析距离统计信息")
  292. self.analyze_distance_statistics()
  293. # 4. 绘制图表
  294. print(f"\n步骤4: 绘制距离图表")
  295. if save_chart:
  296. chart_filename = f"ETF月线距离分析_{self.start_date}_{self.end_date}.png"
  297. self.plot_distance_chart(save_path=chart_filename)
  298. else:
  299. self.plot_distance_chart()
  300. # 5. 导出CSV
  301. if export_csv:
  302. print(f"\n步骤5: 导出分析结果")
  303. self.export_results_to_csv()
  304. # 6. 输出总结
  305. print(f"\n" + "="*60)
  306. print("分析总结")
  307. print("="*60)
  308. for etf_name, stats in self.analysis_results.items():
  309. print(f"\n{etf_name}分析结果:")
  310. for ma_period, stat_data in stats.items():
  311. print(f" {ma_period}:")
  312. print(f" 平均距离: {stat_data['绝对距离_均值']:.3f} 元")
  313. print(f" 平均距离百分比: {stat_data['百分比距离_均值']:.2f}%")
  314. print(f" 正向/负向比例: {stat_data['正向距离次数']}/{stat_data['负向距离次数']}")
  315. print(f"\n分析完成! 总共处理了{len(self.monthly_data)}个ETF,{sum(len(df) for df in self.monthly_data.values())}个月度数据点")
  316. except Exception as e:
  317. print(f"分析过程中发生错误: {e}")
  318. import traceback
  319. traceback.print_exc()
  320. # =============================================================================
  321. # 直接运行分析 - 可以修改下面的参数来自定义分析
  322. # =============================================================================
  323. print("="*60)
  324. print("ETF月线级别均线距离分析工具")
  325. print("="*60)
  326. print("功能:分析ETF价格与5、10、20日均线的距离关系")
  327. print("ETF标的:50ETF、300ETF、创业板ETF")
  328. print()
  329. # 分析参数配置(可根据需要修改)
  330. START_DATE = '2022-01-01' # 开始日期
  331. END_DATE = '2024-12-31' # 结束日期
  332. print(f"分析时间范围: {START_DATE} 到 {END_DATE}")
  333. print()
  334. # 创建分析器实例
  335. analyzer = ETFMonthlyMAAnalysis(
  336. start_date=START_DATE,
  337. end_date=END_DATE
  338. )
  339. # 运行完整分析
  340. analyzer.run_complete_analysis(
  341. save_chart=False, # 保存图表到文件
  342. export_csv=False # 导出CSV数据
  343. )