run_and_email.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Kalman策略 - 运行并发送邮件报告 (修复版)
  5. """
  6. import sys
  7. sys.path.insert(0, '/root/.openclaw/workspace/kalman-filter')
  8. import subprocess
  9. import smtplib
  10. from email.mime.text import MIMEText
  11. from email.mime.multipart import MIMEMultipart
  12. from email.mime.base import MIMEBase
  13. from email.header import Header
  14. from email import encoders
  15. import os
  16. from datetime import datetime
  17. # 邮件配置
  18. EMAIL_CONFIG = {
  19. "smtp_server": "localhost",
  20. "smtp_port": 25,
  21. "sender_email": "kalman@erwin.wang",
  22. "receiver_email": "380880504@qq.com"
  23. }
  24. def run_kalman_strategy():
  25. """运行 Kalman 策略"""
  26. print("="*60)
  27. print("📈 运行 Kalman 策略...")
  28. print("="*60)
  29. result = subprocess.run(
  30. ['python3', 'cyb50_kalman_filter_daily.py'],
  31. cwd='/root/.openclaw/workspace/kalman-filter',
  32. capture_output=True,
  33. text=True,
  34. timeout=300
  35. )
  36. return result.stdout, result.stderr, result.returncode
  37. def parse_results(output):
  38. """解析策略输出结果"""
  39. lines = output.split('\n')
  40. summary = {
  41. 'total_return': 'N/A',
  42. 'max_drawdown': 'N/A',
  43. 'sharpe': 'N/A',
  44. 'num_trades': 'N/A',
  45. 'win_rate': 'N/A',
  46. 'final_value': 'N/A'
  47. }
  48. for line in lines:
  49. if '总收益率:' in line:
  50. summary['total_return'] = line.split(':')[1].strip()
  51. elif '最大回撤:' in line:
  52. summary['max_drawdown'] = line.split(':')[1].strip()
  53. elif '夏普比率:' in line:
  54. summary['sharpe'] = line.split(':')[1].strip()
  55. elif '总交易对数' in line:
  56. summary['num_trades'] = line.split(':')[1].strip()
  57. elif '胜率:' in line:
  58. summary['win_rate'] = line.split(':')[1].strip()
  59. elif '最终资产:' in line:
  60. summary['final_value'] = line.split(':')[1].strip()
  61. return summary
  62. def simplify_reason(reason):
  63. """简化退出原因"""
  64. if '早期止损' in reason:
  65. return '早期止损'
  66. elif '趋势衰减' in reason:
  67. return '趋势衰减'
  68. elif '追踪止损' in reason:
  69. return '追踪止损'
  70. elif '最大亏损' in reason:
  71. return '最大亏损止损'
  72. elif '趋势卖出' in reason:
  73. return '趋势卖出'
  74. else:
  75. return reason.split('(')[0] if '(' in reason else reason
  76. def extract_recent_trades(output):
  77. """提取最近20次交易(优先从逐笔交易分析中提取,补充所有交易信号)"""
  78. lines = output.split('\n')
  79. trades = []
  80. # 方法1: 从"逐笔交易盈亏分析"中提取(包含完整交易信息)
  81. in_trade_section = False
  82. for line in lines:
  83. if '逐笔交易盈亏分析' in line:
  84. in_trade_section = True
  85. continue
  86. if in_trade_section:
  87. stripped = line.strip()
  88. # 匹配格式: "21 2026-02-26 3500.75 2026-03-04 3310.59 4 1.0x -5.43% 早期止损(...)"
  89. if len(stripped) > 60:
  90. parts = stripped.split()
  91. if len(parts) >= 9:
  92. try:
  93. # 检查第一列是数字,第二列是日期
  94. if parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
  95. # 简化退出原因
  96. parts[-1] = simplify_reason(parts[-1])
  97. trade_line = ' '.join(parts[:9])
  98. trades.append(trade_line)
  99. except:
  100. pass
  101. if len(trades) >= 25: # 多取一些,确保包含最近的交易
  102. break
  103. if '退出原因统计' in line:
  104. break
  105. # 方法2: 如果逐笔交易不足,从"所有交易信号"中补充配对
  106. if len(trades) < 20:
  107. signals = []
  108. in_signal_section = False
  109. for line in lines:
  110. if '所有交易信号详情' in line:
  111. in_signal_section = True
  112. continue
  113. if in_signal_section:
  114. stripped = line.strip()
  115. if len(stripped) > 30:
  116. parts = stripped.split()
  117. if len(parts) >= 3 and parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
  118. signals.append({
  119. 'num': parts[0],
  120. 'date': parts[1],
  121. 'action': parts[2],
  122. 'price': parts[3] if len(parts) > 3 else '',
  123. })
  124. if '当前市场状态' in line:
  125. break
  126. # 配对买卖信号(取全部信号,不只是最后40个)
  127. for i in range(len(signals) - 1):
  128. if signals[i]['action'] == '买入' and signals[i+1]['action'] == '卖出':
  129. buy = signals[i]
  130. sell = signals[i+1]
  131. # 检查是否已经存在(通过日期判断)
  132. trade_date = f"{buy['date']}买入"
  133. if not any(trade_date in t for t in trades):
  134. trade_line = f"{buy['num']:<4} {buy['date']:<12} {buy['price']:<10} {sell['date']:<12} {sell['price']:<10} {'?':<7} {'1.0x':<6} {'?':<8} {'信号配对'}"
  135. trades.append(trade_line)
  136. # 按交易编号排序,取最近20条
  137. def get_trade_num(trade_line):
  138. try:
  139. return int(trade_line.split()[0])
  140. except:
  141. return 0
  142. trades.sort(key=get_trade_num)
  143. return trades[-20:]
  144. def extract_recent_signals(output):
  145. """提取最近20天信号详情"""
  146. lines = output.split('\n')
  147. signals = []
  148. in_signal_section = False
  149. for line in lines:
  150. if '最近' in line and '个交易日信号判断详情' in line:
  151. in_signal_section = True
  152. continue
  153. if in_signal_section:
  154. stripped = line.strip()
  155. if len(stripped) > 80 and stripped[0:4].isdigit():
  156. signals.append(stripped)
  157. if len(signals) >= 20:
  158. break
  159. if '分析完成' in line or '当前市场状态' in line:
  160. break
  161. return signals
  162. def extract_current_status(output):
  163. """提取当前市场状态"""
  164. lines = output.split('\n')
  165. status = []
  166. in_status = False
  167. for line in lines:
  168. if '当前市场状态' in line:
  169. in_status = True
  170. if in_status:
  171. status.append(line.strip())
  172. if len(status) >= 8:
  173. break
  174. return status
  175. def send_email(subject, html_content, text_content, attachments=None):
  176. """发送邮件"""
  177. try:
  178. msg = MIMEMultipart('alternative')
  179. msg['Subject'] = Header(subject, 'utf-8')
  180. msg['From'] = EMAIL_CONFIG['sender_email']
  181. msg['To'] = EMAIL_CONFIG['receiver_email']
  182. text_part = MIMEText(text_content, 'plain', 'utf-8')
  183. msg.attach(text_part)
  184. html_part = MIMEText(html_content, 'html', 'utf-8')
  185. msg.attach(html_part)
  186. if attachments:
  187. for filepath in attachments:
  188. if os.path.exists(filepath):
  189. with open(filepath, 'rb') as f:
  190. attachment = MIMEBase('application', 'octet-stream')
  191. attachment.set_payload(f.read())
  192. encoders.encode_base64(attachment)
  193. filename = os.path.basename(filepath)
  194. attachment.add_header('Content-Disposition', f'attachment; filename="{filename}"')
  195. msg.attach(attachment)
  196. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  197. server.sendmail(
  198. EMAIL_CONFIG['sender_email'],
  199. EMAIL_CONFIG['receiver_email'],
  200. msg.as_string()
  201. )
  202. print(f"✅ 邮件发送成功: {subject}")
  203. return True
  204. except Exception as e:
  205. print(f"❌ 邮件发送失败: {e}")
  206. return False
  207. def main():
  208. """主程序"""
  209. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  210. stdout, stderr, returncode = run_kalman_strategy()
  211. if returncode != 0:
  212. print(f"❌ 策略运行失败: {stderr}")
  213. return
  214. summary = parse_results(stdout)
  215. recent_trades = extract_recent_trades(stdout)
  216. recent_signals = extract_recent_signals(stdout)
  217. current_status = extract_current_status(stdout)
  218. # 构建文本邮件
  219. text = f"""
  220. Kalman策略报告
  221. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  222. 【绩效摘要】
  223. 总收益率: {summary['total_return']}
  224. 最大回撤: {summary['max_drawdown']}
  225. 夏普比率: {summary['sharpe']}
  226. 总交易次数: {summary['num_trades']}
  227. 胜率: {summary['win_rate']}
  228. 最终资产: {summary['final_value']}
  229. 【当前市场状态】
  230. """
  231. for line in current_status[:8]:
  232. text += line + '\n'
  233. # 最近20次交易
  234. text += "\n【最近20次交易】\n"
  235. for i, trade in enumerate(recent_trades, 1):
  236. text += f"{i:2d}. {trade}\n"
  237. # 最近20天信号
  238. text += "\n【最近20天信号详情】\n"
  239. for signal in recent_signals:
  240. text += signal + '\n'
  241. # 构建HTML
  242. trades_html = ""
  243. for trade in recent_trades:
  244. parts = trade.split()
  245. if len(parts) >= 9:
  246. trades_html += "<tr>"
  247. # 只取前8列(编号到收益率),第9列是退出原因需要简化
  248. for part in parts[:8]:
  249. trades_html += f"<td>{part}</td>"
  250. # 简化退出原因
  251. reason = parts[8] if len(parts) > 8 else ""
  252. if '早期止损' in reason:
  253. reason = '早期止损'
  254. elif '趋势衰减' in reason:
  255. reason = '趋势衰减'
  256. elif '追踪止损' in reason:
  257. reason = '追踪止损'
  258. elif '最大亏损' in reason:
  259. reason = '最大亏损止损'
  260. elif '趋势卖出' in reason:
  261. reason = '趋势卖出'
  262. elif '信号配对' in reason:
  263. reason = '信号配对'
  264. trades_html += f"<td>{reason}</td>"
  265. trades_html += "</tr>"
  266. elif len(parts) >= 8:
  267. # 如果没有第9列,补充一个占位符
  268. trades_html += "<tr>"
  269. for part in parts[:8]:
  270. trades_html += f"<td>{part}</td>"
  271. trades_html += "<td>-</td>"
  272. trades_html += "</tr>"
  273. signals_html = ""
  274. for signal in recent_signals:
  275. parts = signal.split()
  276. if len(parts) >= 11:
  277. signal_class = ""
  278. if '>>买入' in signal:
  279. signal_class = "buy"
  280. elif '>>卖出' in signal:
  281. signal_class = "sell"
  282. signals_html += f"<tr class='{signal_class}'>"
  283. for part in parts[:11]:
  284. signals_html += f"<td>{part}</td>"
  285. signals_html += "</tr>"
  286. html = f"""
  287. <html>
  288. <head>
  289. <meta charset="utf-8">
  290. <style>
  291. body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
  292. .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; }}
  293. h1 {{ color: #333; border-bottom: 3px solid #9c27b0; padding-bottom: 10px; }}
  294. h2 {{ color: #555; margin-top: 30px; border-left: 4px solid #9c27b0; padding-left: 10px; }}
  295. .metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
  296. .metric-label {{ color: #666; font-size: 12px; }}
  297. .metric-value {{ font-size: 24px; font-weight: bold; }}
  298. .positive {{ color: #28a745; }}
  299. .negative {{ color: #dc3545; }}
  300. .summary {{ background: #f3e5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
  301. table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
  302. th {{ background: #9c27b0; color: white; padding: 8px; text-align: left; }}
  303. td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; }}
  304. tr:nth-child(even) {{ background: #f8f9fa; }}
  305. .buy {{ background: #e8f5e9; color: #28a745; font-weight: bold; }}
  306. .sell {{ background: #ffebee; color: #dc3545; font-weight: bold; }}
  307. </style>
  308. </head>
  309. <body>
  310. <div class="container">
  311. <h1>📊 Kalman策略报告</h1>
  312. <p style="color: #666;">生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  313. <div class="summary">
  314. <h2>📈 绩效摘要</h2>
  315. <div class="metric">
  316. <div class="metric-label">总收益率</div>
  317. <div class="metric-value {'positive' if '+' in summary['total_return'] else 'negative'}">{summary['total_return']}</div>
  318. </div>
  319. <div class="metric">
  320. <div class="metric-label">最大回撤</div>
  321. <div class="metric-value">{summary['max_drawdown']}</div>
  322. </div>
  323. <div class="metric">
  324. <div class="metric-label">夏普比率</div>
  325. <div class="metric-value">{summary['sharpe']}</div>
  326. </div>
  327. <div class="metric">
  328. <div class="metric-label">交易次数</div>
  329. <div class="metric-value">{summary['num_trades']}</div>
  330. </div>
  331. <div class="metric">
  332. <div class="metric-label">胜率</div>
  333. <div class="metric-value">{summary['win_rate']}</div>
  334. </div>
  335. <div class="metric">
  336. <div class="metric-label">最终资产</div>
  337. <div class="metric-value">{summary['final_value']}</div>
  338. </div>
  339. </div>
  340. <h2>💼 最近20次交易</h2>
  341. <table>
  342. <tr><th>#</th><th>买入时间</th><th>买入价</th><th>卖出时间</th><th>卖出价</th><th>持仓天</th><th>仓位</th><th>收益率</th><th>退出原因</th></tr>
  343. {trades_html}
  344. </table>
  345. <h2>📅 最近20天信号详情</h2>
  346. <table>
  347. <tr><th>日期</th><th>收盘</th><th>Trend</th><th>加速度</th><th>波动率</th><th>K偏离</th><th>趋势</th><th>加速</th><th>波动</th><th>偏离</th><th>信号</th><th>说明</th></tr>
  348. {signals_html}
  349. </table>
  350. <div style="margin-top: 30px; padding: 15px; background: #e8f5e9; border-radius: 5px;">
  351. <strong>附件:</strong><br>
  352. • kalman_filter_analysis.png - 策略分析图表<br>
  353. • kalman_daily_signals.csv - 完整交易信号数据
  354. </div>
  355. </div>
  356. </body>
  357. </html>
  358. """
  359. subject = f"📊 Kalman策略 {datetime.now().strftime('%m-%d %H:%M')} | 收益{summary['total_return']}"
  360. attachments = [
  361. '/root/.openclaw/workspace/kalman-filter/kalman_filter_analysis.png',
  362. '/root/.openclaw/workspace/kalman-filter/kalman_daily_signals.csv'
  363. ]
  364. send_email(subject, html, text, attachments)
  365. print(f"\n📊 提取到 {len(recent_trades)} 条交易记录")
  366. print(f"📊 提取到 {len(recent_signals)} 条信号记录")
  367. print("\n✅ 全部完成!")
  368. if __name__ == "__main__":
  369. main()