generate_last60_report.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """生成最近60天详细数据邮件"""
  4. import sys
  5. sys.path.insert(0, '/root/.openclaw/workspace/market-regime-identifier')
  6. import numpy as np
  7. import pandas as pd
  8. from cyb50_market_classifier import fetch_cyb50_data, calculate_features, define_market_regime
  9. from sklearn.ensemble import RandomForestClassifier
  10. import warnings
  11. warnings.filterwarnings('ignore')
  12. # 获取数据
  13. df = fetch_cyb50_data('2024-01-01', '2026-03-06')
  14. if df is None:
  15. exit(1)
  16. # 计算特征和标签
  17. features = calculate_features(df)
  18. labels = define_market_regime(df, lookback=10)
  19. # 训练模型
  20. valid_idx = ~np.isnan(labels)
  21. X = features[valid_idx]
  22. y = labels[valid_idx]
  23. clf = RandomForestClassifier(
  24. n_estimators=100, max_depth=10, min_samples_split=20,
  25. min_samples_leaf=10, random_state=42, class_weight='balanced'
  26. )
  27. clf.fit(X, y)
  28. # 预测所有数据
  29. states = clf.predict(X)
  30. probs = clf.predict_proba(X)
  31. # 对齐数据
  32. df_aligned = df.iloc[-len(states):].copy()
  33. df_aligned['state'] = states
  34. df_aligned['prob_ranging'] = probs[:, 0]
  35. df_aligned['prob_trend'] = probs[:, 1]
  36. df_aligned['prob_reversal'] = probs[:, 2]
  37. # 获取最近60天
  38. last_60 = df_aligned.tail(60).copy()
  39. last_60['date'] = last_60.index.strftime('%m-%d')
  40. last_60['change'] = last_60['close'].pct_change() * 100
  41. state_names = ['震荡', '趋势', '反转']
  42. colors = ['#2196F3', '#4CAF50', '#FF5722']
  43. # 生成HTML
  44. html_rows = ""
  45. for idx, row in last_60.iterrows():
  46. state = int(row['state'])
  47. state_name = state_names[state]
  48. color = colors[state]
  49. change = row['change'] if not pd.isna(row['change']) else 0
  50. change_str = f"{change:+.2f}%" if change != 0 else "-"
  51. change_color = "green" if change > 0 else "red" if change < 0 else "gray"
  52. html_rows += f"""
  53. <tr>
  54. <td>{idx.strftime('%Y-%m-%d')}</td>
  55. <td>{row['close']:.2f}</td>
  56. <td style="color: {color}; font-weight: bold;">{state_name}</td>
  57. <td>{row['prob_ranging']:.1%}</td>
  58. <td>{row['prob_trend']:.1%}</td>
  59. <td>{row['prob_reversal']:.1%}</td>
  60. <td style="color: {change_color};">{change_str}</td>
  61. </tr>
  62. """
  63. # 计算统计
  64. summary = f"""
  65. <div class="summary">
  66. <h2>📊 最近60天统计</h2>
  67. <p><strong>统计区间:</strong> {last_60.index[0].date()} ~ {last_60.index[-1].date()}</p>
  68. <p><strong>起始价格:</strong> {last_60['close'].iloc[0]:.2f}</p>
  69. <p><strong>结束价格:</strong> {last_60['close'].iloc[-1]:.2f}</p>
  70. <p><strong>区间涨跌:</strong> {(last_60['close'].iloc[-1]/last_60['close'].iloc[0]-1)*100:+.2f}%</p>
  71. <p><strong>最高价:</strong> {last_60['close'].max():.2f} ({last_60['close'].idxmax().strftime('%m-%d')})</p>
  72. <p><strong>最低价:</strong> {last_60['close'].min():.2f} ({last_60['close'].idxmin().strftime('%m-%d')})</p>
  73. <br>
  74. <p><strong>状态分布:</strong></p>
  75. <p>🟦 震荡: {(last_60['state']==0).sum()}天 ({(last_60['state']==0).sum()/60*100:.1f}%)</p>
  76. <p>🟩 趋势: {(last_60['state']==1).sum()}天 ({(last_60['state']==1).sum()/60*100:.1f}%)</p>
  77. <p>🟧 反转: {(last_60['state']==2).sum()}天 ({(last_60['state']==2).sum()/60*100:.1f}%)</p>
  78. </div>
  79. """
  80. html = f"""
  81. <html>
  82. <head>
  83. <meta charset="utf-8">
  84. <style>
  85. body {{ font-family: Arial, sans-serif; margin: 20px; font-size: 12px; }}
  86. h1 {{ color: #333; border-bottom: 3px solid #2196F3; padding-bottom: 10px; font-size: 18px; }}
  87. h2 {{ color: #555; margin-top: 20px; border-left: 4px solid #4CAF50; padding-left: 10px; font-size: 14px; }}
  88. .summary {{ background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
  89. .summary p {{ margin: 5px 0; }}
  90. table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
  91. th {{ background: #2196F3; color: white; padding: 8px; text-align: center; position: sticky; top: 0; }}
  92. td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: center; }}
  93. tr:nth-child(even) {{ background: #f8f9fa; }}
  94. tr:hover {{ background: #e3f2fd; }}
  95. .table-container {{ max-height: 500px; overflow-y: auto; }}
  96. </style>
  97. </head>
  98. <body>
  99. <h1>📊 创业板50最近60天详细数据 (2026-01-06 ~ 2026-03-06)</h1>
  100. {summary}
  101. <h2>📋 每日详细数据</h2>
  102. <div class="table-container">
  103. <table>
  104. <thead>
  105. <tr>
  106. <th>日期</th>
  107. <th>收盘价</th>
  108. <th>状态</th>
  109. <th>震荡概率</th>
  110. <th>趋势概率</th>
  111. <th>反转概率</th>
  112. <th>日涨跌</th>
  113. </tr>
  114. </thead>
  115. <tbody>
  116. {html_rows}
  117. </tbody>
  118. </table>
  119. </div>
  120. <hr>
  121. <p style="color: #666; font-size: 11px;">
  122. 生成时间: 2026-03-06 19:10<br>
  123. 数据更新至: 2026-03-06<br>
  124. 模型准确率: 72.10%
  125. </p>
  126. </body>
  127. </html>
  128. """
  129. # 保存HTML
  130. with open('/root/.openclaw/workspace/market-regime-identifier/last_60_days_report.html', 'w', encoding='utf-8') as f:
  131. f.write(html)
  132. print("✓ HTML报告已生成")
  133. print(f"最近60天: {last_60.index[0].date()} ~ {last_60.index[-1].date()}")
  134. print(f"\n状态分布:")
  135. print(f" 震荡: {(last_60['state']==0).sum()}天")
  136. print(f" 趋势: {(last_60['state']==1).sum()}天")
  137. print(f" 反转: {(last_60['state']==2).sum()}天")