auto_report.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 自动化交易报告系统 (独立版)
  5. 功能:
  6. 1. 获取近2个月数据
  7. 2. 运行策略回测
  8. 3. 生成详细报告
  9. 4. 发送邮件通知
  10. 执行频率:A股开盘时间每半小时(9:30-11:30, 13:00-15:00)
  11. """
  12. import pandas as pd
  13. import numpy as np
  14. import akshare as ak
  15. import warnings
  16. import os
  17. import smtplib
  18. import ssl
  19. from datetime import datetime, timedelta
  20. from email.mime.text import MIMEText
  21. from email.mime.multipart import MIMEMultipart
  22. from email.header import Header
  23. warnings.filterwarnings('ignore')
  24. # ==================== 邮件配置 ====================
  25. # 使用本地Postfix SMTP服务器发送
  26. EMAIL_CONFIG = {
  27. "smtp_server": "localhost", # 本地Postfix服务器
  28. "smtp_port": 25, # SMTP端口
  29. "sender_email": "catfly@openclaw.local", # 发件人邮箱
  30. "sender_password": "", # 本地SMTP无需密码
  31. "receiver_email": "380880504@qq.com" # 收件人邮箱
  32. }
  33. def send_email(subject, html_content, text_content=""):
  34. """发送邮件 - 使用本地Postfix"""
  35. try:
  36. msg = MIMEMultipart('alternative')
  37. msg['Subject'] = Header(subject, 'utf-8')
  38. msg['From'] = EMAIL_CONFIG['sender_email']
  39. msg['To'] = EMAIL_CONFIG['receiver_email']
  40. # 纯文本版本
  41. text_part = MIMEText(text_content, 'plain', 'utf-8')
  42. msg.attach(text_part)
  43. # HTML版本
  44. html_part = MIMEText(html_content, 'html', 'utf-8')
  45. msg.attach(html_part)
  46. # 发送邮件 - 本地Postfix无需SSL和认证
  47. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  48. server.sendmail(
  49. EMAIL_CONFIG['sender_email'],
  50. EMAIL_CONFIG['receiver_email'],
  51. msg.as_string()
  52. )
  53. print(f"✅ 邮件发送成功: {subject}")
  54. return True
  55. except Exception as e:
  56. print(f"❌ 邮件发送失败: {e}")
  57. print(f" 请检查EMAIL_CONFIG配置是否正确")
  58. return False
  59. # ==================== 数据获取 ====================
  60. class DataFetcher:
  61. """数据获取类"""
  62. @staticmethod
  63. def fetch_recent_2months():
  64. """获取近2个月数据 - 优先在线获取,失败则使用本地数据"""
  65. end_date = datetime.now()
  66. start_date = end_date - timedelta(days=70) # 2个月+10天缓冲
  67. print(f"获取数据: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}")
  68. # 首先尝试在线获取
  69. try:
  70. df = ak.index_zh_a_hist_min_em(
  71. symbol="399673",
  72. period="30",
  73. start_date=start_date.strftime('%Y%m%d%H%M%S'),
  74. end_date=end_date.strftime('%Y%m%d%H%M%S')
  75. )
  76. if df is not None and len(df) > 0:
  77. df = df.rename(columns={
  78. '时间': 'datetime',
  79. '开盘': 'open',
  80. '收盘': 'close',
  81. '最高': 'high',
  82. '最低': 'low',
  83. '成交量': 'volume'
  84. })
  85. df['datetime'] = pd.to_datetime(df['datetime'])
  86. df = df.set_index('datetime').sort_index()
  87. backtest_start = end_date - timedelta(days=60)
  88. df_backtest = df[df.index >= backtest_start]
  89. print(f"✅ 在线数据获取成功: 共{len(df_backtest)}条30分钟K线")
  90. print(f" 数据区间: {df_backtest.index[0]} 至 {df_backtest.index[-1]}")
  91. return df_backtest
  92. except Exception as e:
  93. print(f"⚠️ 在线数据获取失败: {e}")
  94. print(" 尝试使用本地数据...")
  95. # 在线获取失败,使用本地数据
  96. return DataFetcher._load_local_data(start_date, end_date)
  97. @staticmethod
  98. def _load_local_data(start_date, end_date):
  99. """从本地文件加载数据"""
  100. local_file = 'SZ#399673.txt'
  101. try:
  102. if not os.path.exists(local_file):
  103. print(f"❌ 本地文件不存在: {local_file}")
  104. return None
  105. print(f"正在从本地文件读取数据: {local_file}")
  106. # 读取文本格式数据
  107. data_list = []
  108. encodings = ['gbk', 'gb2312', 'utf-8', 'latin-1']
  109. lines = None
  110. for encoding in encodings:
  111. try:
  112. with open(local_file, 'r', encoding=encoding) as f:
  113. lines = f.readlines()
  114. print(f"✅ 成功使用编码: {encoding}")
  115. break
  116. except UnicodeDecodeError:
  117. continue
  118. if lines is None:
  119. raise ValueError("无法读取文件,尝试了多种编码格式都失败")
  120. # 跳过前两行(标题行)
  121. for line in lines[2:]:
  122. line = line.strip()
  123. if not line:
  124. continue
  125. parts = line.split()
  126. if len(parts) >= 7:
  127. try:
  128. # 格式: 日期 时间 开盘 最高 最低 收盘 成交量 成交额
  129. date_time_str = f"{parts[0]} {parts[1]}"
  130. datetime_obj = pd.to_datetime(date_time_str, format='%Y/%m/%d %H%M')
  131. data_list.append({
  132. 'datetime': datetime_obj,
  133. 'open': float(parts[2]),
  134. 'high': float(parts[3]),
  135. 'low': float(parts[4]),
  136. 'close': float(parts[5]),
  137. 'volume': float(parts[6])
  138. })
  139. except (ValueError, IndexError) as e:
  140. continue
  141. if not data_list:
  142. raise ValueError("文本文件中没有解析到有效数据")
  143. df = pd.DataFrame(data_list)
  144. df = df.set_index('datetime').sort_index()
  145. # 只保留最近2个月的数据
  146. backtest_start = end_date - timedelta(days=60)
  147. df_backtest = df[df.index >= backtest_start]
  148. print(f"✅ 本地数据加载成功: 共{len(df_backtest)}条30分钟K线")
  149. if len(df_backtest) > 0:
  150. print(f" 数据区间: {df_backtest.index[0]} 至 {df_backtest.index[-1]}")
  151. return df_backtest
  152. except Exception as e:
  153. print(f"❌ 本地数据加载失败: {e}")
  154. return None
  155. # ==================== 策略类 ====================
  156. class CatFlyStrategy:
  157. """cat-fly策略简化版 - 基于30分钟K线"""
  158. def __init__(self, config=None):
  159. self.config = config or {
  160. 'initial_capital': 1000000,
  161. 'position_size_pct': 1.0,
  162. 'stop_loss_pct': 0.008,
  163. 'take_profit_pct': 0.02,
  164. 'max_hold_bars': 16,
  165. 'min_signal_strength': 3
  166. }
  167. self.initial_capital = self.config['initial_capital']
  168. def calculate_indicators(self, df):
  169. """计算技术指标"""
  170. # RSI
  171. delta = df['close'].diff()
  172. gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
  173. loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
  174. rs = gain / loss
  175. df['RSI'] = 100 - (100 / (1 + rs))
  176. # 移动平均线
  177. df['MA5'] = df['close'].rolling(5).mean()
  178. df['MA20'] = df['close'].rolling(20).mean()
  179. df['MA60'] = df['close'].rolling(60).mean()
  180. # 布林带
  181. df['BB_middle'] = df['close'].rolling(20).mean()
  182. bb_std = df['close'].rolling(20).std()
  183. df['BB_upper'] = df['BB_middle'] + 2 * bb_std
  184. df['BB_lower'] = df['BB_middle'] - 2 * bb_std
  185. # MACD
  186. ema12 = df['close'].ewm(span=12).mean()
  187. ema26 = df['close'].ewm(span=26).mean()
  188. df['MACD'] = ema12 - ema26
  189. df['MACD_signal'] = df['MACD'].ewm(span=9).mean()
  190. return df
  191. def generate_signals(self, df):
  192. """生成交易信号"""
  193. df = self.calculate_indicators(df)
  194. df['signal'] = 0
  195. df['signal_strength'] = 0
  196. for i in range(60, len(df)):
  197. row = df.iloc[i]
  198. strength = 0
  199. # RSI超卖/超买
  200. if row['RSI'] < 30:
  201. strength += 1
  202. elif row['RSI'] > 70:
  203. strength -= 1
  204. # 均线多头排列/空头排列
  205. if row['close'] > row['MA5'] > row['MA20']:
  206. strength += 1
  207. elif row['close'] < row['MA5'] < row['MA20']:
  208. strength -= 1
  209. # 布林带
  210. if row['close'] < row['BB_lower']:
  211. strength += 1
  212. elif row['close'] > row['BB_upper']:
  213. strength -= 1
  214. # MACD金叉/死叉
  215. if i > 0:
  216. prev_macd = df['MACD'].iloc[i-1]
  217. prev_signal = df['MACD_signal'].iloc[i-1]
  218. curr_macd = row['MACD']
  219. curr_signal_line = row['MACD_signal']
  220. if prev_macd < prev_signal and curr_macd > curr_signal_line:
  221. strength += 1
  222. elif prev_macd > prev_signal and curr_macd < curr_signal_line:
  223. strength -= 1
  224. df.iloc[i, df.columns.get_loc('signal_strength')] = strength
  225. # 生成交易信号
  226. if strength >= self.config['min_signal_strength']:
  227. df.iloc[i, df.columns.get_loc('signal')] = 1 # 做多
  228. elif strength <= -self.config['min_signal_strength']:
  229. df.iloc[i, df.columns.get_loc('signal')] = -1 # 做空
  230. return df
  231. def backtest(self, df):
  232. """回测"""
  233. df = self.generate_signals(df)
  234. trades = []
  235. capital = self.initial_capital
  236. position = 0
  237. entry_price = 0
  238. entry_time = None
  239. holding_bars = 0
  240. for i in range(60, len(df)):
  241. current_bar = df.iloc[i]
  242. price = current_bar['close']
  243. current_time = current_bar.name
  244. # 无持仓时检查开仓信号
  245. if position == 0:
  246. if current_bar['signal'] == 1: # 做多
  247. position_size = int(capital * self.config['position_size_pct'] / price)
  248. if position_size > 0:
  249. position = position_size
  250. entry_price = price
  251. entry_time = current_time
  252. holding_bars = 0
  253. elif current_bar['signal'] == -1: # 做空
  254. position_size = int(capital * self.config['position_size_pct'] / price)
  255. if position_size > 0:
  256. position = -position_size
  257. entry_price = price
  258. entry_time = current_time
  259. holding_bars = 0
  260. # 有持仓时检查平仓
  261. else:
  262. holding_bars += 1
  263. exit_signal = False
  264. exit_reason = ""
  265. if position > 0: # 做多持仓
  266. if price <= entry_price * (1 - self.config['stop_loss_pct']):
  267. exit_signal = True
  268. exit_reason = "止损"
  269. elif price >= entry_price * (1 + self.config['take_profit_pct']):
  270. exit_signal = True
  271. exit_reason = "止盈"
  272. elif holding_bars >= self.config['max_hold_bars']:
  273. exit_signal = True
  274. exit_reason = "时间止损"
  275. elif current_bar['RSI'] > 70:
  276. exit_signal = True
  277. exit_reason = "信号消失(RSI超买)"
  278. else: # 做空持仓
  279. if price >= entry_price * (1 + self.config['stop_loss_pct']):
  280. exit_signal = True
  281. exit_reason = "止损"
  282. elif price <= entry_price * (1 - self.config['take_profit_pct']):
  283. exit_signal = True
  284. exit_reason = "止盈"
  285. elif holding_bars >= self.config['max_hold_bars']:
  286. exit_signal = True
  287. exit_reason = "时间止损"
  288. elif current_bar['RSI'] < 30:
  289. exit_signal = True
  290. exit_reason = "信号消失(RSI超卖)"
  291. # 执行平仓
  292. if exit_signal:
  293. if position > 0:
  294. pnl = (price - entry_price) * position
  295. pnl_pct = (price - entry_price) / entry_price * 100
  296. else:
  297. pnl = (entry_price - price) * abs(position)
  298. pnl_pct = (entry_price - price) / entry_price * 100
  299. capital += pnl
  300. trades.append({
  301. '方向': '做多' if position > 0 else '做空',
  302. '开仓时间': entry_time,
  303. '平仓时间': current_time,
  304. '开仓价': entry_price,
  305. '平仓价': price,
  306. '持仓数量': abs(position),
  307. '盈亏金额': pnl,
  308. '盈亏百分比': pnl_pct,
  309. '退出原因': exit_reason,
  310. '持仓周期': holding_bars,
  311. '平仓后资金': capital
  312. })
  313. position = 0
  314. entry_price = 0
  315. entry_time = None
  316. holding_bars = 0
  317. return df, pd.DataFrame(trades), capital
  318. # ==================== 报告生成 ====================
  319. def generate_report(trades_df, final_capital, initial_capital=1000000):
  320. """生成详细报告"""
  321. if len(trades_df) == 0:
  322. html = "<html><body><h1>创业板50交易报告</h1><p>近2个月无交易信号</p></body></html>"
  323. text = "近2个月无交易信号"
  324. return html, text
  325. total_return = (final_capital - initial_capital) / initial_capital * 100
  326. total_trades = len(trades_df)
  327. winning_trades = trades_df[trades_df['盈亏金额'] > 0]
  328. losing_trades = trades_df[trades_df['盈亏金额'] < 0]
  329. win_rate = len(winning_trades) / total_trades * 100 if total_trades > 0 else 0
  330. avg_profit = winning_trades['盈亏金额'].mean() if len(winning_trades) > 0 else 0
  331. avg_loss = losing_trades['盈亏金额'].mean() if len(losing_trades) > 0 else 0
  332. total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
  333. total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
  334. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  335. max_profit = trades_df['盈亏金额'].max()
  336. max_loss = trades_df['盈亏金额'].min()
  337. avg_hold_time = trades_df['持仓周期'].mean()
  338. long_trades = trades_df[trades_df['方向'] == '做多']
  339. short_trades = trades_df[trades_df['方向'] == '做空']
  340. exit_reasons = trades_df['退出原因'].value_counts()
  341. # 生成HTML报告
  342. html = f"""
  343. <html>
  344. <head>
  345. <style>
  346. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  347. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  348. h2 {{ color: #555; margin-top: 30px; }}
  349. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  350. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  351. th {{ background-color: #007bff; color: white; }}
  352. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  353. .positive {{ color: green; font-weight: bold; }}
  354. .negative {{ color: red; font-weight: bold; }}
  355. .summary {{ background-color: #f8f9fa; padding: 15px; border-radius: 5px; margin: 15px 0; }}
  356. </style>
  357. </head>
  358. <body>
  359. <h1>🚀 创业板50指数交易报告</h1>
  360. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  361. <p>数据区间: 近2个月</p>
  362. <div class="summary">
  363. <h2>📊 总体绩效</h2>
  364. <table>
  365. <tr><th>指标</th><th>数值</th></tr>
  366. <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
  367. <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
  368. <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
  369. <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
  370. <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
  371. <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
  372. <tr><td>平均持仓时间</td><td>{avg_hold_time:.1f}周期 ({avg_hold_time*0.5:.1f}小时)</td></tr>
  373. </table>
  374. </div>
  375. <h2>📈 盈亏统计</h2>
  376. <table>
  377. <tr><th>指标</th><th>数值</th></tr>
  378. <tr><td>总盈利</td><td class="positive">+{total_profit:,.0f}元</td></tr>
  379. <tr><td>总亏损</td><td class="negative">-{total_loss:,.0f}元</td></tr>
  380. <tr><td>平均盈利</td><td class="positive">+{avg_profit:,.0f}元</td></tr>
  381. <tr><td>平均亏损</td><td class="negative">{avg_loss:,.0f}元</td></tr>
  382. <tr><td>最大单笔盈利</td><td class="positive">+{max_profit:,.0f}元</td></tr>
  383. <tr><td>最大单笔亏损</td><td class="negative">{max_loss:,.0f}元</td></tr>
  384. </table>
  385. <h2>🔄 多空统计</h2>
  386. <table>
  387. <tr><th>方向</th><th>交易次数</th><th>胜率</th><th>总盈亏</th></tr>
  388. <tr>
  389. <td>做多</td>
  390. <td>{len(long_trades)}笔</td>
  391. <td>{(len(long_trades[long_trades['盈亏金额']>0])/len(long_trades)*100 if len(long_trades)>0 else 0):.1f}%</td>
  392. <td class="{'positive' if long_trades['盈亏金额'].sum() >= 0 else 'negative'}">{long_trades['盈亏金额'].sum():+,.0f}元</td>
  393. </tr>
  394. <tr>
  395. <td>做空</td>
  396. <td>{len(short_trades)}笔</td>
  397. <td>{(len(short_trades[short_trades['盈亏金额']>0])/len(short_trades)*100 if len(short_trades)>0 else 0):.1f}%</td>
  398. <td class="{'positive' if short_trades['盈亏金额'].sum() >= 0 else 'negative'}">{short_trades['盈亏金额'].sum():+,.0f}元</td>
  399. </tr>
  400. </table>
  401. <h2>🚪 退出原因分析</h2>
  402. <table>
  403. <tr><th>退出原因</th><th>次数</th><th>占比</th></tr>
  404. """
  405. for reason, count in exit_reasons.items():
  406. pct = count / total_trades * 100
  407. html += f"<tr><td>{reason}</td><td>{count}</td><td>{pct:.1f}%</td></tr>"
  408. html += """
  409. </table>
  410. <h2>📝 最近10笔交易明细</h2>
  411. <table>
  412. <tr>
  413. <th>方向</th>
  414. <th>开仓时间</th>
  415. <th>平仓时间</th>
  416. <th>开仓价</th>
  417. <th>平仓价</th>
  418. <th>盈亏金额</th>
  419. <th>盈亏%</th>
  420. <th>退出原因</th>
  421. </tr>
  422. """
  423. recent_trades = trades_df.tail(10)
  424. for _, trade in recent_trades.iterrows():
  425. pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
  426. html += f"""
  427. <tr>
  428. <td>{trade['方向']}</td>
  429. <td>{trade['开仓时间']}</td>
  430. <td>{trade['平仓时间']}</td>
  431. <td>{trade['开仓价']:.2f}</td>
  432. <td>{trade['平仓价']:.2f}</td>
  433. <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
  434. <td class="{pnl_class}">{trade['盈亏百分比']:+.2f}%</td>
  435. <td>{trade['退出原因']}</td>
  436. </tr>
  437. """
  438. html += """
  439. </table>
  440. <hr>
  441. <p style="color: #666; font-size: 12px;">
  442. 本报告由 cat-fly 自动交易系统生成 | 策略:30分钟K线多空双向<br>
  443. 风险提示:历史回测不代表未来表现,投资有风险,入市需谨慎。
  444. </p>
  445. </body>
  446. </html>
  447. """
  448. # 生成纯文本版本
  449. text = f"""
  450. 创业板50指数交易报告
  451. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  452. 数据区间: 近2个月
  453. 【总体绩效】
  454. 初始资金: {initial_capital:,.0f}元
  455. 最终资金: {final_capital:,.0f}元
  456. 总收益率: {total_return:+.2f}%
  457. 总交易次数: {total_trades}笔
  458. 胜率: {win_rate:.1f}%
  459. 盈亏比: {profit_factor:.2f}
  460. 平均持仓: {avg_hold_time*0.5:.1f}小时
  461. 【盈亏统计】
  462. 总盈利: +{total_profit:,.0f}元
  463. 总亏损: -{total_loss:,.0f}元
  464. 最大单笔盈利: +{max_profit:,.0f}元
  465. 最大单笔亏损: {max_loss:,.0f}元
  466. 【多空统计】
  467. 做多: {len(long_trades)}笔, 盈亏{long_trades['盈亏金额'].sum():+,.0f}元
  468. 做空: {len(short_trades)}笔, 盈亏{short_trades['盈亏金额'].sum():+,.0f}元
  469. 【退出原因】
  470. {exit_reasons.to_string()}
  471. 【最近5笔交易】
  472. {trades_df.tail(5)[['方向', '开仓时间', '平仓时间', '盈亏金额', '退出原因']].to_string(index=False)}
  473. """
  474. return html, text
  475. # ==================== 主程序 ====================
  476. def main():
  477. """主程序"""
  478. print("="*80)
  479. print("🚀 cat-fly 自动交易报告系统")
  480. print("="*80)
  481. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  482. # 检查是否在交易时间(可选)
  483. now = datetime.now()
  484. hour = now.hour
  485. minute = now.minute
  486. time_str = f"{hour:02d}:{minute:02d}"
  487. # A股交易时间检查
  488. is_trading_time = False
  489. if (9 <= hour <= 11) or (13 <= hour <= 15):
  490. if hour == 9 and minute < 30:
  491. is_trading_time = False
  492. elif hour == 11 and minute > 30:
  493. is_trading_time = False
  494. elif hour == 15 and minute > 0:
  495. is_trading_time = False
  496. else:
  497. is_trading_time = True
  498. print(f"当前时间: {time_str}")
  499. print(f"交易时间: {'是' if is_trading_time else '否(非交易时间也会执行)'}")
  500. # 1. 获取近2个月数据
  501. print("\n📊 步骤1: 获取近2个月数据...")
  502. df = DataFetcher.fetch_recent_2months()
  503. if df is None:
  504. print("❌ 数据获取失败,退出")
  505. return
  506. # 2. 运行策略
  507. print("\n📈 步骤2: 运行策略回测...")
  508. strategy = CatFlyStrategy()
  509. df, trades_df, final_capital = strategy.backtest(df)
  510. print(f"✅ 回测完成: 共{len(trades_df)}笔交易")
  511. print(f" 最终资金: {final_capital:,.0f}元")
  512. print(f" 收益率: {(final_capital/1000000-1)*100:+.2f}%")
  513. # 3. 生成报告
  514. print("\n📝 步骤3: 生成报告...")
  515. html_report, text_report = generate_report(trades_df, final_capital)
  516. # 4. 发送邮件
  517. print("\n📧 步骤4: 发送邮件...")
  518. subject = f"🚀 创业板50交易报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{(final_capital/1000000-1)*100:+.2f}%"
  519. send_email(subject, html_report, text_report)
  520. print("\n✅ 全部完成!")
  521. print("="*80)
  522. if __name__ == "__main__":
  523. main()