auto_report.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 自动化交易报告系统 (完整版)
  5. 基于原版 cyb50_30min_intraday_reversal.py 策略
  6. 功能:
  7. 1. 获取近2个月实时数据
  8. 2. 运行完整策略回测(做多翻转策略)
  9. 3. 生成详细报告
  10. 4. 发送邮件通知
  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. import requests
  20. import json
  21. from datetime import datetime, timedelta
  22. from email.mime.text import MIMEText
  23. from email.mime.multipart import MIMEMultipart
  24. from email.header import Header
  25. warnings.filterwarnings('ignore')
  26. # ==================== 邮件配置 ====================
  27. EMAIL_CONFIG = {
  28. "smtp_server": "localhost",
  29. "smtp_port": 25,
  30. "sender_email": "catfly@openclaw.local",
  31. "sender_password": "",
  32. "receiver_email": "380880504@qq.com"
  33. }
  34. def send_email(subject, html_content, text_content=""):
  35. """发送邮件"""
  36. try:
  37. msg = MIMEMultipart('alternative')
  38. msg['Subject'] = Header(subject, 'utf-8')
  39. msg['From'] = EMAIL_CONFIG['sender_email']
  40. msg['To'] = EMAIL_CONFIG['receiver_email']
  41. text_part = MIMEText(text_content, 'plain', 'utf-8')
  42. msg.attach(text_part)
  43. html_part = MIMEText(html_content, 'html', 'utf-8')
  44. msg.attach(html_part)
  45. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  46. server.sendmail(
  47. EMAIL_CONFIG['sender_email'],
  48. EMAIL_CONFIG['receiver_email'],
  49. msg.as_string()
  50. )
  51. print(f"✅ 邮件发送成功: {subject}")
  52. return True
  53. except Exception as e:
  54. print(f"❌ 邮件发送失败: {e}")
  55. return False
  56. # ==================== 数据获取 ====================
  57. class DataFetcher:
  58. """数据获取类 - 双数据源"""
  59. @staticmethod
  60. def fetch_recent_2months():
  61. """获取近2个月数据"""
  62. end_date = datetime.now()
  63. start_date = end_date - timedelta(days=70)
  64. print(f"获取数据: {start_date.strftime('%Y-%m-%d')} 至 {end_date.strftime('%Y-%m-%d')}")
  65. # 尝试东方财富
  66. df = DataFetcher._fetch_eastmoney_data(start_date, end_date)
  67. if df is not None:
  68. return df
  69. # 尝试新浪财经
  70. print("⚠️ 东方财富失败,尝试新浪财经...")
  71. df = DataFetcher._fetch_sina_data(start_date, end_date)
  72. if df is not None:
  73. return df
  74. raise Exception("无法获取实时数据,所有数据源均失败。")
  75. @staticmethod
  76. def _fetch_eastmoney_data(start_date, end_date):
  77. """东方财富数据源"""
  78. try:
  79. print("[数据源1] 东方财富30分钟K线...")
  80. df = ak.index_zh_a_hist_min_em(symbol="399673", period="30")
  81. if df is not None and not df.empty and len(df) >= 50:
  82. # 标准化列名(大写,与原版一致)
  83. df = df.rename(columns={
  84. '时间': 'DateTime',
  85. '开盘': 'Open',
  86. '收盘': 'Close',
  87. '最高': 'High',
  88. '最低': 'Low',
  89. '成交量': 'Volume'
  90. })
  91. df['DateTime'] = pd.to_datetime(df['DateTime'])
  92. df = df.set_index('DateTime').sort_index()
  93. # 计算基础指标
  94. df['Returns'] = df['Close'].pct_change()
  95. df['High_Low_Pct'] = (df['High'] - df['Low']) / df['Close'].shift(1)
  96. df['Close_Open_Pct'] = (df['Close'] - df['Open']) / df['Open']
  97. # 保留近2个月
  98. backtest_start = end_date - timedelta(days=60)
  99. df_backtest = df[df.index >= backtest_start].copy()
  100. print(f"✅ 东方财富: {len(df_backtest)}条K线 ({df_backtest.index[0]} ~ {df_backtest.index[-1]})")
  101. return df_backtest
  102. except Exception as e:
  103. print(f"❌ 东方财富: {e}")
  104. return None
  105. @staticmethod
  106. def _fetch_sina_data(start_date, end_date):
  107. """新浪财经数据源"""
  108. try:
  109. print("[数据源2] 新浪财经30分钟K线...")
  110. symbol = "sz399673"
  111. url = f"https://quotes.sina.cn/cn/api/jsonp_v2.php/var_{symbol}_30_/CN_MarketDataService.getKLineData?symbol={symbol}&scale=30&ma=no&datalen=1023"
  112. headers = {
  113. 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
  114. 'Referer': 'https://finance.sina.com.cn/'
  115. }
  116. response = requests.get(url, headers=headers, timeout=15)
  117. json_start = response.text.find('[')
  118. json_end = response.text.rfind(']') + 1
  119. json_str = response.text[json_start:json_end]
  120. data_dict = json.loads(json_str)
  121. if data_dict and isinstance(data_dict, list):
  122. data_list = []
  123. for item in data_dict:
  124. data_list.append({
  125. 'DateTime': item.get('day'),
  126. 'Open': float(item.get('open', 0)),
  127. 'High': float(item.get('high', 0)),
  128. 'Low': float(item.get('low', 0)),
  129. 'Close': float(item.get('close', 0)),
  130. 'Volume': float(item.get('volume', 0))
  131. })
  132. df = pd.DataFrame(data_list)
  133. df['DateTime'] = pd.to_datetime(df['DateTime'])
  134. df = df.set_index('DateTime').sort_index()
  135. # 计算基础指标
  136. df['Returns'] = df['Close'].pct_change()
  137. df['High_Low_Pct'] = (df['High'] - df['Low']) / df['Close'].shift(1)
  138. df['Close_Open_Pct'] = (df['Close'] - df['Open']) / df['Open']
  139. # 保留近2个月
  140. backtest_start = end_date - timedelta(days=60)
  141. df_backtest = df[df.index >= backtest_start].copy()
  142. print(f"✅ 新浪财经: {len(df_backtest)}条K线 ({df_backtest.index[0]} ~ {df_backtest.index[-1]})")
  143. return df_backtest
  144. except Exception as e:
  145. print(f"❌ 新浪财经: {e}")
  146. return None
  147. # ==================== 策略类(完整版) ====================
  148. class CatFlyStrategy:
  149. """cat-fly完整策略 - 日内翻转做多策略"""
  150. def __init__(self, config=None):
  151. self.config = config or {
  152. 'initial_capital': 1000000,
  153. 'position_size_pct': 1.0,
  154. 'stop_loss_pct': 0.008,
  155. 'take_profit_pct': 0.015,
  156. 'max_hold_bars': 16,
  157. 'min_reversal_score': 4 # 原版阈值是4
  158. }
  159. self.initial_capital = self.config['initial_capital']
  160. def calculate_indicators(self, df):
  161. """计算完整技术指标(与原版一致)"""
  162. print("计算技术指标...")
  163. # 短期移动平均线
  164. df['MA6'] = df['Close'].rolling(window=6).mean()
  165. df['MA12'] = df['Close'].rolling(window=12).mean()
  166. df['MA24'] = df['Close'].rolling(window=24).mean()
  167. # RSI
  168. delta = df['Close'].diff()
  169. gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
  170. loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
  171. rs = gain / loss
  172. df['RSI'] = 100 - (100 / (1 + rs))
  173. # 布林带
  174. df['BB_middle'] = df['Close'].rolling(window=20).mean()
  175. bb_std = df['Close'].rolling(window=20).std()
  176. df['BB_upper'] = df['BB_middle'] + (bb_std * 2)
  177. df['BB_lower'] = df['BB_middle'] - (bb_std * 2)
  178. df['BB_width'] = (df['BB_upper'] - df['BB_lower']) / df['BB_middle']
  179. # MACD
  180. exp1 = df['Close'].ewm(span=12, adjust=False).mean()
  181. exp2 = df['Close'].ewm(span=26, adjust=False).mean()
  182. df['MACD'] = exp1 - exp2
  183. df['MACD_signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
  184. df['MACD_hist'] = df['MACD'] - df['MACD_signal']
  185. # KDJ
  186. low_9 = df['Low'].rolling(window=9).min()
  187. high_9 = df['High'].rolling(window=9).max()
  188. rsv = (df['Close'] - low_9) / (high_9 - low_9) * 100
  189. df['K'] = rsv.ewm(com=2, adjust=False).mean()
  190. df['D'] = df['K'].ewm(com=2, adjust=False).mean()
  191. df['J'] = 3 * df['K'] - 2 * df['D']
  192. # ATR
  193. high_low = df['High'] - df['Low']
  194. high_close = abs(df['High'] - df['Close'].shift())
  195. low_close = abs(df['Low'] - df['Close'].shift())
  196. true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
  197. df['ATR'] = true_range.rolling(window=14).mean()
  198. df['ATR_Pct'] = df['ATR'] / df['Close']
  199. # 动量指标
  200. df['Momentum'] = df['Close'] / df['Close'].shift(4) - 1
  201. # 成交量变化
  202. df['Volume_MA'] = df['Volume'].rolling(window=12).mean()
  203. df['Volume_Ratio'] = df['Volume'] / df['Volume_MA']
  204. # 价格动量
  205. df['Price_Momentum'] = (df['Close'] - df['Close'].shift(6)) / df['Close'].shift(6)
  206. print("技术指标计算完成")
  207. return df
  208. def generate_signals(self, df):
  209. """生成日内翻转信号(完整版)"""
  210. print("生成交易信号...")
  211. df = self.calculate_indicators(df)
  212. signals = []
  213. signal_count = 0
  214. # 从第24个周期开始(需要足够历史数据)
  215. for i in range(24, len(df)):
  216. current_bar = df.iloc[i]
  217. current_time = df.index[i]
  218. # 跳过非交易时间
  219. if hasattr(current_time, 'hour'):
  220. hour = current_time.hour
  221. if hour < 9 or hour > 15:
  222. continue
  223. # 计算翻转信号分数
  224. reversal_score = 0
  225. reversal_signals = []
  226. # 1. RSI超卖翻转
  227. if current_bar['RSI'] < 30:
  228. reversal_score += 2
  229. reversal_signals.append("RSI超卖")
  230. elif current_bar['RSI'] < 35:
  231. reversal_score += 1
  232. reversal_signals.append("RSI偏弱")
  233. # 2. KDJ超卖翻转
  234. if current_bar['K'] < 20 and current_bar['D'] < 20:
  235. reversal_score += 2
  236. reversal_signals.append("KDJ超卖")
  237. elif current_bar['J'] < 0:
  238. reversal_score += 2
  239. reversal_signals.append("KDJ极端超卖")
  240. # 3. MACD金叉或改善
  241. if current_bar['MACD_hist'] > 0 and df.iloc[i-1]['MACD_hist'] <= 0:
  242. reversal_score += 2
  243. reversal_signals.append("MACD金叉")
  244. elif current_bar['MACD_hist'] > df.iloc[i-1]['MACD_hist']:
  245. reversal_score += 1
  246. reversal_signals.append("MACD改善")
  247. # 4. 价格触及布林带下轨
  248. if current_bar['Close'] <= current_bar['BB_lower'] * 1.005:
  249. reversal_score += 2
  250. reversal_signals.append("触及下轨")
  251. elif current_bar['Close'] <= current_bar['BB_lower'] * 1.01:
  252. reversal_score += 1
  253. reversal_signals.append("接近下轨")
  254. # 5. 连续下跌后的反转
  255. recent_returns = df.iloc[i-6:i]['Returns']
  256. if recent_returns.min() < -0.015:
  257. consecutive_decline = sum(recent_returns < 0)
  258. if consecutive_decline >= 4:
  259. reversal_score += 2
  260. reversal_signals.append("连续下跌反转")
  261. # 6. 价格动量超卖
  262. if current_bar['Price_Momentum'] < -0.02:
  263. reversal_score += 1
  264. reversal_signals.append("动量超卖")
  265. # 7. 成交量配合
  266. if current_bar['Volume_Ratio'] > 1.2:
  267. reversal_score += 1
  268. reversal_signals.append("放量配合")
  269. # 8. 日内低位
  270. try:
  271. daily_data = df[df.index.date == current_time.date()]
  272. if len(daily_data) > 0:
  273. daily_high = daily_data['High'].max()
  274. daily_low = daily_data['Low'].min()
  275. daily_range = daily_high - daily_low
  276. if daily_range > 0:
  277. position_in_day = (current_bar['Close'] - daily_low) / daily_range
  278. if position_in_day < 0.3:
  279. reversal_score += 1
  280. reversal_signals.append("日内低位")
  281. except:
  282. pass
  283. # 记录信号
  284. df.loc[df.index[i], 'Reversal_Score'] = reversal_score
  285. df.loc[df.index[i], 'Reversal_Signals'] = ', '.join(reversal_signals)
  286. # 生成买入信号(阈值4分)
  287. if reversal_score >= self.config['min_reversal_score']:
  288. df.loc[df.index[i], 'Signal'] = 1
  289. df.loc[df.index[i], 'Signal_Type'] = '做多翻转'
  290. signal_count += 1
  291. else:
  292. df.loc[df.index[i], 'Signal'] = 0
  293. df.loc[df.index[i], 'Signal_Type'] = ''
  294. print(f"信号生成完成: 共{signal_count}个翻转信号")
  295. return df
  296. def backtest(self, df):
  297. """回测执行"""
  298. df = self.generate_signals(df)
  299. trades = []
  300. capital = self.initial_capital
  301. position = 0
  302. entry_price = 0
  303. entry_time = None
  304. entry_signals = ""
  305. holding_bars = 0
  306. for i in range(24, len(df)):
  307. current_bar = df.iloc[i]
  308. price = current_bar['Close']
  309. current_time = df.index[i]
  310. # 检查是否在交易时间
  311. if hasattr(current_time, 'hour'):
  312. hour = current_time.hour
  313. minute = current_time.minute
  314. if hour == 11 and minute >= 30: # 午休前不新开仓
  315. pass
  316. if hour == 15: # 收盘前不新开仓
  317. pass
  318. # 无持仓时检查开仓
  319. if position == 0:
  320. if current_bar.get('Signal', 0) == 1:
  321. position_value = capital * self.config['position_size_pct']
  322. position_size = int(position_value / price)
  323. if position_size > 0:
  324. position = position_size
  325. entry_price = price
  326. entry_time = current_time
  327. entry_signals = current_bar.get('Reversal_Signals', '')
  328. holding_bars = 0
  329. # 有持仓时检查平仓
  330. elif position > 0:
  331. holding_bars += 1
  332. exit_signal = False
  333. exit_reason = ""
  334. # 止损
  335. stop_loss_price = entry_price * (1 - self.config['stop_loss_pct'])
  336. take_profit_price = entry_price * (1 + self.config['take_profit_pct'])
  337. if price <= stop_loss_price:
  338. exit_signal = True
  339. exit_reason = f"止损({self.config['stop_loss_pct']*100:.1f}%)"
  340. # 止盈
  341. elif price >= take_profit_price:
  342. exit_signal = True
  343. exit_reason = f"止盈({self.config['take_profit_pct']*100:.1f}%)"
  344. # 时间止损
  345. elif holding_bars >= self.config['max_hold_bars']:
  346. exit_signal = True
  347. exit_reason = f"时间止损({holding_bars}周期)"
  348. # RSI超买平仓
  349. elif current_bar['RSI'] > 70:
  350. exit_signal = True
  351. exit_reason = "RSI超买平仓"
  352. # 执行平仓
  353. if exit_signal:
  354. pnl = (price - entry_price) * position
  355. pnl_pct = (price - entry_price) / entry_price * 100
  356. capital += pnl
  357. trades.append({
  358. '方向': '做多',
  359. '开仓时间': entry_time,
  360. '平仓时间': current_time,
  361. '开仓价': entry_price,
  362. '平仓价': price,
  363. '持仓数量': position,
  364. '盈亏金额': pnl,
  365. '盈亏百分比': pnl_pct,
  366. '退出原因': exit_reason,
  367. '持仓周期': holding_bars,
  368. '信号详情': entry_signals,
  369. '平仓后资金': capital
  370. })
  371. position = 0
  372. entry_price = 0
  373. entry_time = None
  374. holding_bars = 0
  375. return df, pd.DataFrame(trades), capital
  376. # ==================== 报告生成 ====================
  377. def generate_report(trades_df, final_capital, initial_capital=1000000):
  378. """生成详细报告"""
  379. total_return = (final_capital - initial_capital) / initial_capital * 100
  380. total_trades = len(trades_df)
  381. if total_trades == 0:
  382. html = f"""
  383. <html><body>
  384. <h1>🚀 创业板50交易报告</h1>
  385. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  386. <p>数据区间: 近2个月</p>
  387. <p><b>近2个月无交易信号触发</b></p>
  388. <p>初始资金: {initial_capital:,.0f}元</p>
  389. <p>最终资金: {final_capital:,.0f}元</p>
  390. <p>收益率: {total_return:+.2f}%</p>
  391. <p style="color: #666;">说明:策略在指定期间内未找到符合条件的翻转信号(需RSI<30、KDJ超卖、MACD金叉等多重条件同时满足)</p>
  392. </body></html>
  393. """
  394. text = f"""
  395. 创业板50交易报告
  396. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  397. 数据区间: 近2个月
  398. 近2个月无交易信号触发
  399. 初始资金: {initial_capital:,.0f}元
  400. 最终资金: {final_capital:,.0f}元
  401. 收益率: {total_return:+.2f}%
  402. 说明:策略在指定期间内未找到符合条件的翻转信号
  403. """
  404. return html, text
  405. # 有交易数据时的报告...
  406. winning_trades = trades_df[trades_df['盈亏金额'] > 0]
  407. losing_trades = trades_df[trades_df['盈亏金额'] < 0]
  408. win_rate = len(winning_trades) / total_trades * 100
  409. total_profit = winning_trades['盈亏金额'].sum() if len(winning_trades) > 0 else 0
  410. total_loss = abs(losing_trades['盈亏金额'].sum()) if len(losing_trades) > 0 else 0
  411. profit_factor = total_profit / total_loss if total_loss > 0 else 0
  412. # HTML报告
  413. html = f"""
  414. <html><head><style>
  415. body {{ font-family: Arial, sans-serif; margin: 20px; }}
  416. h1 {{ color: #333; border-bottom: 2px solid #007bff; padding-bottom: 10px; }}
  417. h2 {{ color: #555; margin-top: 30px; }}
  418. table {{ border-collapse: collapse; width: 100%; margin: 15px 0; font-size: 14px; }}
  419. th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
  420. th {{ background-color: #007bff; color: white; }}
  421. tr:nth-child(even) {{ background-color: #f2f2f2; }}
  422. .positive {{ color: green; font-weight: bold; }}
  423. .negative {{ color: red; font-weight: bold; }}
  424. </style></head><body>
  425. <h1>🚀 创业板50交易报告</h1>
  426. <p>生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  427. <p>数据区间: 近2个月</p>
  428. <h2>📊 总体绩效</h2>
  429. <table>
  430. <tr><th>指标</th><th>数值</th></tr>
  431. <tr><td>初始资金</td><td>{initial_capital:,.0f}元</td></tr>
  432. <tr><td>最终资金</td><td>{final_capital:,.0f}元</td></tr>
  433. <tr><td>总收益率</td><td class="{'positive' if total_return >= 0 else 'negative'}">{total_return:+.2f}%</td></tr>
  434. <tr><td>总交易次数</td><td>{total_trades}笔</td></tr>
  435. <tr><td>胜率</td><td>{win_rate:.1f}%</td></tr>
  436. <tr><td>盈亏比</td><td>{profit_factor:.2f}</td></tr>
  437. </table>
  438. <h2>📝 最近10笔交易明细</h2>
  439. <table>
  440. <tr>
  441. <th>开仓时间</th><th>平仓时间</th><th>开仓价</th><th>平仓价</th>
  442. <th>盈亏</th><th>盈亏%</th><th>退出原因</th><th>信号</th>
  443. </tr>
  444. """
  445. for _, trade in trades_df.tail(10).iterrows():
  446. pnl_class = "positive" if trade['盈亏金额'] >= 0 else "negative"
  447. html += f"""
  448. <tr>
  449. <td>{trade['开仓时间']}</td><td>{trade['平仓时间']}</td>
  450. <td>{trade['开仓价']:.2f}</td><td>{trade['平仓价']:.2f}</td>
  451. <td class="{pnl_class}">{trade['盈亏金额']:+.0f}</td>
  452. <td class="{pnl_class}">{trade['盈亏百分比']:+.2f}%</td>
  453. <td>{trade['退出原因']}</td><td>{trade['信号详情'][:20]}...</td>
  454. </tr>
  455. """
  456. html += "</table></body></html>"
  457. # 纯文本报告
  458. text = f"""
  459. 创业板50交易报告
  460. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  461. 【总体绩效】
  462. 初始资金: {initial_capital:,.0f}元
  463. 最终资金: {final_capital:,.0f}元
  464. 总收益率: {total_return:+.2f}%
  465. 总交易次数: {total_trades}笔
  466. 胜率: {win_rate:.1f}%
  467. 盈亏比: {profit_factor:.2f}
  468. 【最近5笔交易】
  469. {trades_df.tail(5)[['开仓时间', '平仓时间', '盈亏金额', '退出原因']].to_string(index=False)}
  470. """
  471. return html, text
  472. # ==================== 主程序 ====================
  473. def main():
  474. """主程序"""
  475. print("="*80)
  476. print("🚀 cat-fly 自动交易报告系统 (完整版)")
  477. print("="*80)
  478. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  479. # 1. 获取数据
  480. print("\n📊 步骤1: 获取近2个月实时数据...")
  481. try:
  482. df = DataFetcher.fetch_recent_2months()
  483. except Exception as e:
  484. print(f"❌ 数据获取失败: {e}")
  485. return
  486. # 2. 运行策略
  487. print("\n📈 步骤2: 运行完整策略回测...")
  488. strategy = CatFlyStrategy()
  489. df, trades_df, final_capital = strategy.backtest(df)
  490. print(f"✅ 回测完成: 共{len(trades_df)}笔交易")
  491. print(f" 最终资金: {final_capital:,.0f}元")
  492. print(f" 收益率: {(final_capital/1000000-1)*100:+.2f}%")
  493. # 3. 生成报告
  494. print("\n📝 步骤3: 生成报告...")
  495. html_report, text_report = generate_report(trades_df, final_capital)
  496. # 4. 发送邮件
  497. print("\n📧 步骤4: 发送邮件...")
  498. subject = f"🚀 创业板50交易报告 {datetime.now().strftime('%m-%d %H:%M')} | 收益{(final_capital/1000000-1)*100:+.2f}% | {len(trades_df)}笔交易"
  499. send_email(subject, html_report, text_report)
  500. print("\n✅ 全部完成!")
  501. print("="*80)
  502. if __name__ == "__main__":
  503. main()