| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Kalman策略 - 运行并发送邮件报告 (修复版)
- """
- import sys
- sys.path.insert(0, '/root/.openclaw/workspace/kalman-filter')
- import subprocess
- import smtplib
- from email.mime.text import MIMEText
- from email.mime.multipart import MIMEMultipart
- from email.mime.base import MIMEBase
- from email.header import Header
- from email import encoders
- import os
- from datetime import datetime
- # 邮件配置
- EMAIL_CONFIG = {
- "smtp_server": "localhost",
- "smtp_port": 25,
- "sender_email": "kalman@erwin.wang",
- "receiver_email": "380880504@qq.com"
- }
- def run_kalman_strategy():
- """运行 Kalman 策略"""
- print("="*60)
- print("📈 运行 Kalman 策略...")
- print("="*60)
-
- result = subprocess.run(
- ['python3', 'cyb50_kalman_filter_daily.py'],
- cwd='/root/.openclaw/workspace/kalman-filter',
- capture_output=True,
- text=True,
- timeout=300
- )
-
- return result.stdout, result.stderr, result.returncode
- def parse_results(output):
- """解析策略输出结果"""
- lines = output.split('\n')
-
- summary = {
- 'total_return': 'N/A',
- 'max_drawdown': 'N/A',
- 'sharpe': 'N/A',
- 'num_trades': 'N/A',
- 'win_rate': 'N/A',
- 'final_value': 'N/A'
- }
-
- for line in lines:
- if '总收益率:' in line:
- summary['total_return'] = line.split(':')[1].strip()
- elif '最大回撤:' in line:
- summary['max_drawdown'] = line.split(':')[1].strip()
- elif '夏普比率:' in line:
- summary['sharpe'] = line.split(':')[1].strip()
- elif '总交易对数' in line:
- summary['num_trades'] = line.split(':')[1].strip()
- elif '胜率:' in line:
- summary['win_rate'] = line.split(':')[1].strip()
- elif '最终资产:' in line:
- summary['final_value'] = line.split(':')[1].strip()
-
- return summary
- def simplify_reason(reason):
- """简化退出原因"""
- if '早期止损' in reason:
- return '早期止损'
- elif '趋势衰减' in reason:
- return '趋势衰减'
- elif '追踪止损' in reason:
- return '追踪止损'
- elif '最大亏损' in reason:
- return '最大亏损止损'
- elif '趋势卖出' in reason:
- return '趋势卖出'
- else:
- return reason.split('(')[0] if '(' in reason else reason
- def extract_recent_trades(output):
- """提取最近20次交易(优先从逐笔交易分析中提取,补充所有交易信号)"""
- lines = output.split('\n')
- trades = []
-
- # 方法1: 从"逐笔交易盈亏分析"中提取(包含完整交易信息)
- in_trade_section = False
- for line in lines:
- if '逐笔交易盈亏分析' in line:
- in_trade_section = True
- continue
- if in_trade_section:
- stripped = line.strip()
- # 匹配格式: "21 2026-02-26 3500.75 2026-03-04 3310.59 4 1.0x -5.43% 早期止损(...)"
- if len(stripped) > 60:
- parts = stripped.split()
- if len(parts) >= 9:
- try:
- # 检查第一列是数字,第二列是日期
- if parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
- # 简化退出原因
- parts[-1] = simplify_reason(parts[-1])
- trade_line = ' '.join(parts[:9])
- trades.append(trade_line)
- except:
- pass
- if len(trades) >= 25: # 多取一些,确保包含最近的交易
- break
- if '退出原因统计' in line:
- break
-
- # 方法2: 如果逐笔交易不足,从"所有交易信号"中补充配对
- if len(trades) < 20:
- signals = []
- in_signal_section = False
- for line in lines:
- if '所有交易信号详情' in line:
- in_signal_section = True
- continue
- if in_signal_section:
- stripped = line.strip()
- if len(stripped) > 30:
- parts = stripped.split()
- if len(parts) >= 3 and parts[0].isdigit() and len(parts[1]) == 10 and parts[1][4] == '-':
- signals.append({
- 'num': parts[0],
- 'date': parts[1],
- 'action': parts[2],
- 'price': parts[3] if len(parts) > 3 else '',
- })
- if '当前市场状态' in line:
- break
-
- # 配对买卖信号(取全部信号,不只是最后40个)
- for i in range(len(signals) - 1):
- if signals[i]['action'] == '买入' and signals[i+1]['action'] == '卖出':
- buy = signals[i]
- sell = signals[i+1]
- # 检查是否已经存在(通过日期判断)
- trade_date = f"{buy['date']}买入"
- if not any(trade_date in t for t in trades):
- trade_line = f"{buy['num']:<4} {buy['date']:<12} {buy['price']:<10} {sell['date']:<12} {sell['price']:<10} {'?':<7} {'1.0x':<6} {'?':<8} {'信号配对'}"
- trades.append(trade_line)
-
- # 按交易编号排序,取最近20条
- def get_trade_num(trade_line):
- try:
- return int(trade_line.split()[0])
- except:
- return 0
-
- trades.sort(key=get_trade_num)
- return trades[-20:]
- def extract_recent_signals(output):
- """提取最近20天信号详情"""
- lines = output.split('\n')
- signals = []
- in_signal_section = False
-
- for line in lines:
- if '最近' in line and '个交易日信号判断详情' in line:
- in_signal_section = True
- continue
- if in_signal_section:
- stripped = line.strip()
- if len(stripped) > 80 and stripped[0:4].isdigit():
- signals.append(stripped)
- if len(signals) >= 20:
- break
- if '分析完成' in line or '当前市场状态' in line:
- break
-
- return signals
- def extract_current_status(output):
- """提取当前市场状态"""
- lines = output.split('\n')
- status = []
- in_status = False
-
- for line in lines:
- if '当前市场状态' in line:
- in_status = True
- if in_status:
- status.append(line.strip())
- if len(status) >= 8:
- break
-
- return status
- def send_email(subject, html_content, text_content, attachments=None):
- """发送邮件"""
- try:
- msg = MIMEMultipart('alternative')
- msg['Subject'] = Header(subject, 'utf-8')
- msg['From'] = EMAIL_CONFIG['sender_email']
- msg['To'] = EMAIL_CONFIG['receiver_email']
-
- text_part = MIMEText(text_content, 'plain', 'utf-8')
- msg.attach(text_part)
-
- html_part = MIMEText(html_content, 'html', 'utf-8')
- msg.attach(html_part)
-
- if attachments:
- for filepath in attachments:
- if os.path.exists(filepath):
- with open(filepath, 'rb') as f:
- attachment = MIMEBase('application', 'octet-stream')
- attachment.set_payload(f.read())
- encoders.encode_base64(attachment)
- filename = os.path.basename(filepath)
- attachment.add_header('Content-Disposition', f'attachment; filename="{filename}"')
- msg.attach(attachment)
-
- with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
- server.sendmail(
- EMAIL_CONFIG['sender_email'],
- EMAIL_CONFIG['receiver_email'],
- msg.as_string()
- )
- print(f"✅ 邮件发送成功: {subject}")
- return True
- except Exception as e:
- print(f"❌ 邮件发送失败: {e}")
- return False
- def main():
- """主程序"""
- print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
-
- stdout, stderr, returncode = run_kalman_strategy()
-
- if returncode != 0:
- print(f"❌ 策略运行失败: {stderr}")
- return
-
- summary = parse_results(stdout)
- recent_trades = extract_recent_trades(stdout)
- recent_signals = extract_recent_signals(stdout)
- current_status = extract_current_status(stdout)
-
- # 构建文本邮件
- text = f"""
- Kalman策略报告
- 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
- 【绩效摘要】
- 总收益率: {summary['total_return']}
- 最大回撤: {summary['max_drawdown']}
- 夏普比率: {summary['sharpe']}
- 总交易次数: {summary['num_trades']}
- 胜率: {summary['win_rate']}
- 最终资产: {summary['final_value']}
- 【当前市场状态】
- """
- for line in current_status[:8]:
- text += line + '\n'
-
- # 最近20次交易
- text += "\n【最近20次交易】\n"
- for i, trade in enumerate(recent_trades, 1):
- text += f"{i:2d}. {trade}\n"
-
- # 最近20天信号
- text += "\n【最近20天信号详情】\n"
- for signal in recent_signals:
- text += signal + '\n'
-
- # 构建HTML
- trades_html = ""
- for trade in recent_trades:
- parts = trade.split()
- if len(parts) >= 9:
- trades_html += "<tr>"
- # 只取前8列(编号到收益率),第9列是退出原因需要简化
- for part in parts[:8]:
- trades_html += f"<td>{part}</td>"
- # 简化退出原因
- reason = parts[8] if len(parts) > 8 else ""
- if '早期止损' in reason:
- reason = '早期止损'
- elif '趋势衰减' in reason:
- reason = '趋势衰减'
- elif '追踪止损' in reason:
- reason = '追踪止损'
- elif '最大亏损' in reason:
- reason = '最大亏损止损'
- elif '趋势卖出' in reason:
- reason = '趋势卖出'
- elif '信号配对' in reason:
- reason = '信号配对'
- trades_html += f"<td>{reason}</td>"
- trades_html += "</tr>"
- elif len(parts) >= 8:
- # 如果没有第9列,补充一个占位符
- trades_html += "<tr>"
- for part in parts[:8]:
- trades_html += f"<td>{part}</td>"
- trades_html += "<td>-</td>"
- trades_html += "</tr>"
-
- signals_html = ""
- for signal in recent_signals:
- parts = signal.split()
- if len(parts) >= 11:
- signal_class = ""
- if '>>买入' in signal:
- signal_class = "buy"
- elif '>>卖出' in signal:
- signal_class = "sell"
- signals_html += f"<tr class='{signal_class}'>"
- for part in parts[:11]:
- signals_html += f"<td>{part}</td>"
- signals_html += "</tr>"
-
- html = f"""
- <html>
- <head>
- <meta charset="utf-8">
- <style>
- body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
- .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; }}
- h1 {{ color: #333; border-bottom: 3px solid #9c27b0; padding-bottom: 10px; }}
- h2 {{ color: #555; margin-top: 30px; border-left: 4px solid #9c27b0; padding-left: 10px; }}
- .metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
- .metric-label {{ color: #666; font-size: 12px; }}
- .metric-value {{ font-size: 24px; font-weight: bold; }}
- .positive {{ color: #28a745; }}
- .negative {{ color: #dc3545; }}
- .summary {{ background: #f3e5f5; padding: 15px; border-radius: 5px; margin: 20px 0; }}
- table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 11px; }}
- th {{ background: #9c27b0; color: white; padding: 8px; text-align: left; }}
- td {{ padding: 6px 8px; border-bottom: 1px solid #ddd; }}
- tr:nth-child(even) {{ background: #f8f9fa; }}
- .buy {{ background: #e8f5e9; color: #28a745; font-weight: bold; }}
- .sell {{ background: #ffebee; color: #dc3545; font-weight: bold; }}
- </style>
- </head>
- <body>
- <div class="container">
- <h1>📊 Kalman策略报告</h1>
- <p style="color: #666;">生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
-
- <div class="summary">
- <h2>📈 绩效摘要</h2>
- <div class="metric">
- <div class="metric-label">总收益率</div>
- <div class="metric-value {'positive' if '+' in summary['total_return'] else 'negative'}">{summary['total_return']}</div>
- </div>
- <div class="metric">
- <div class="metric-label">最大回撤</div>
- <div class="metric-value">{summary['max_drawdown']}</div>
- </div>
- <div class="metric">
- <div class="metric-label">夏普比率</div>
- <div class="metric-value">{summary['sharpe']}</div>
- </div>
- <div class="metric">
- <div class="metric-label">交易次数</div>
- <div class="metric-value">{summary['num_trades']}</div>
- </div>
- <div class="metric">
- <div class="metric-label">胜率</div>
- <div class="metric-value">{summary['win_rate']}</div>
- </div>
- <div class="metric">
- <div class="metric-label">最终资产</div>
- <div class="metric-value">{summary['final_value']}</div>
- </div>
- </div>
-
- <h2>💼 最近20次交易</h2>
- <table>
- <tr><th>#</th><th>买入时间</th><th>买入价</th><th>卖出时间</th><th>卖出价</th><th>持仓天</th><th>仓位</th><th>收益率</th><th>退出原因</th></tr>
- {trades_html}
- </table>
-
- <h2>📅 最近20天信号详情</h2>
- <table>
- <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>
- {signals_html}
- </table>
-
- <div style="margin-top: 30px; padding: 15px; background: #e8f5e9; border-radius: 5px;">
- <strong>附件:</strong><br>
- • kalman_filter_analysis.png - 策略分析图表<br>
- • kalman_daily_signals.csv - 完整交易信号数据
- </div>
- </div>
- </body>
- </html>
- """
-
- subject = f"📊 Kalman策略 {datetime.now().strftime('%m-%d %H:%M')} | 收益{summary['total_return']}"
- attachments = [
- '/root/.openclaw/workspace/kalman-filter/kalman_filter_analysis.png',
- '/root/.openclaw/workspace/kalman-filter/kalman_daily_signals.csv'
- ]
-
- send_email(subject, html, text, attachments)
-
- print(f"\n📊 提取到 {len(recent_trades)} 条交易记录")
- print(f"📊 提取到 {len(recent_signals)} 条信号记录")
- print("\n✅ 全部完成!")
- if __name__ == "__main__":
- main()
|