trend_report_v2.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. 创业板50指数 - 实时交易报告系统 (含实时数据获取)
  5. 基于趋势跟踪策略,生成当前时点报告
  6. """
  7. import sys
  8. sys.path.insert(0, '/root/.openclaw/workspace/cat-fly')
  9. sys.path.insert(0, '/root/.openclaw/workspace/quant')
  10. import pandas as pd
  11. import numpy as np
  12. from datetime import datetime, timedelta
  13. import smtplib
  14. import ssl
  15. from email.mime.text import MIMEText
  16. from email.mime.multipart import MIMEMultipart
  17. from email.header import Header
  18. import warnings
  19. warnings.filterwarnings('ignore')
  20. # ==================== 邮件配置 ====================
  21. EMAIL_CONFIG = {
  22. "smtp_server": "localhost",
  23. "smtp_port": 25,
  24. "sender_email": "catfly@erwin.wang",
  25. "receiver_email": "380880504@qq.com"
  26. }
  27. # ==================== 实时数据获取 ====================
  28. def fetch_realtime_data():
  29. """获取实时数据,与历史数据合并"""
  30. try:
  31. # 使用东方财富或新浪财经接口获取最新数据
  32. import requests
  33. import json
  34. # 尝试东方财富接口
  35. url = "http://push2.eastmoney.com/api/qt/stock/get"
  36. params = {
  37. "secid": "0.399673", # 创业板50
  38. "fields": "f43,f44,f45,f46,f47,f48,f50,f51,f52,f57,f58,f60,f107,f108,f109,f110,f111,f112,f113,f114,f115,f116,f117,f118,f119,f120,f121,f122,f123,f124,f125,f126,f127,f128,f129,f130,f131,f132,f133,f134,f135,f136,f137,f138,f139,f140,f141,f142,f143,f144,f145,f146,f147,f148,f149,f150,f151,f152,f153,f154,f155,f156,f157,f158,f159,f160,f161,f162,f163,f164,f165,f166,f167,f168,f169,f170,f171,f172,f173,f174,f175,f176,f177,f178,f179,f180,f181,f182,f183,f184,f185,f186,f187,f188,f189,f190,f191,f192,f193,f194,f195,f196,f197,f198,f199,f200"
  39. }
  40. response = requests.get(url, params=params, timeout=10)
  41. data = response.json()
  42. if 'data' in data and data['data']:
  43. d = data['data']
  44. # 解析实时数据
  45. current_price = float(d.get('f43', 0)) / 100 # 当前价
  46. open_price = float(d.get('f46', 0)) / 100 # 开盘价
  47. high_price = float(d.get('f44', 0)) / 100 # 最高价
  48. low_price = float(d.get('f45', 0)) / 100 # 最低价
  49. volume = int(d.get('f47', 0)) # 成交量
  50. today = datetime.now().strftime('%Y-%m-%d')
  51. return {
  52. 'date': today,
  53. 'open': open_price,
  54. 'high': high_price,
  55. 'low': low_price,
  56. 'close': current_price,
  57. 'volume': volume
  58. }
  59. except Exception as e:
  60. print(f"实时数据获取失败: {e}")
  61. return None
  62. # ==================== 趋势跟踪策略 ====================
  63. class TrendTrackingStrategy:
  64. """趋势跟踪策略 - 实时计算版"""
  65. def __init__(self):
  66. self.data = None
  67. def load_and_merge_data(self, csv_file='cyb50_baostock.csv'):
  68. """加载历史数据并合并实时数据"""
  69. try:
  70. # 加载历史数据
  71. df = pd.read_csv(f'/root/.openclaw/workspace/quant/{csv_file}')
  72. df['date'] = pd.to_datetime(df['date'])
  73. df = df.set_index('date').sort_index()
  74. # 转换数据类型
  75. for col in ['open', 'high', 'low', 'close', 'volume']:
  76. df[col] = pd.to_numeric(df[col], errors='coerce')
  77. # 获取最新历史数据日期
  78. last_hist_date = df.index[-1]
  79. today = pd.Timestamp.now().normalize()
  80. print(f"历史数据最新日期: {last_hist_date.date()}")
  81. print(f"当前日期: {today.date()}")
  82. # 如果历史数据不是最新的,尝试获取实时数据
  83. if last_hist_date < today:
  84. print("历史数据不是最新,尝试获取实时数据...")
  85. realtime = fetch_realtime_data()
  86. if realtime and pd.Timestamp(realtime['date']) > last_hist_date:
  87. # 创建新数据行
  88. new_row = pd.DataFrame([realtime]).set_index('date')
  89. new_row.index = pd.to_datetime(new_row.index)
  90. # 合并数据
  91. df = pd.concat([df, new_row])
  92. print(f"✅ 已合并实时数据: {realtime['date']}")
  93. else:
  94. print("⚠️ 未获取到更新的实时数据,使用历史数据")
  95. else:
  96. print("✅ 历史数据已是最新")
  97. self.data = df
  98. print(f"数据范围: {df.index[0].date()} ~ {df.index[-1].date()}")
  99. return True
  100. except Exception as e:
  101. print(f"数据加载失败: {e}")
  102. return False
  103. def calculate_indicators(self):
  104. """计算技术指标"""
  105. df = self.data.copy()
  106. # 均线
  107. df['ma10'] = df['close'].rolling(window=10).mean()
  108. df['ma30'] = df['close'].rolling(window=30).mean()
  109. # 20日高低点
  110. df['high_20'] = df['high'].rolling(window=20).max()
  111. df['low_20'] = df['low'].rolling(window=20).min()
  112. # 10日涨幅
  113. df['ret_10'] = df['close'].pct_change(periods=10)
  114. # ATR
  115. tr1 = df['high'] - df['low']
  116. tr2 = abs(df['high'] - df['close'].shift(1))
  117. tr3 = abs(df['low'] - df['close'].shift(1))
  118. df['tr'] = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
  119. df['atr'] = df['tr'].rolling(window=20).mean()
  120. self.data = df
  121. return df
  122. def generate_signals(self):
  123. """生成交易信号"""
  124. df = self.data
  125. # 买入条件
  126. buy_cond = (
  127. (df['close'] > df['ma10']) &
  128. (df['ma10'] > df['ma30']) &
  129. (df['close'] >= df['high_20'] * 0.995) &
  130. (df['ret_10'] > 0.02)
  131. )
  132. # 卖出条件
  133. sell_cond = (
  134. (df['close'] < df['ma30']) |
  135. (df['close'] <= df['low_20'] * 1.005)
  136. )
  137. df['signal'] = 0
  138. df.loc[buy_cond, 'signal'] = 1
  139. df.loc[sell_cond, 'signal'] = -1
  140. return df
  141. def backtest(self, initial_capital=1000000):
  142. """回测计算"""
  143. df = self.generate_signals()
  144. position = 0
  145. entry_price = 0
  146. peak_price = 0
  147. capital = initial_capital
  148. trades = []
  149. for i in range(30, len(df)):
  150. date = df.index[i]
  151. price = df['close'].iloc[i]
  152. signal = df['signal'].iloc[i]
  153. # 移动止损检查
  154. if position > 0:
  155. if price > peak_price:
  156. peak_price = price
  157. if price < peak_price * 0.90: # 10%回撤止损
  158. signal = -1
  159. # 执行交易
  160. if signal == 1 and position == 0:
  161. position = 1
  162. entry_price = price
  163. peak_price = price
  164. trades.append({
  165. 'date': date,
  166. 'action': 'BUY',
  167. 'price': price,
  168. 'capital': capital
  169. })
  170. elif signal == -1 and position == 1:
  171. pnl = (price / entry_price - 1) * capital
  172. capital += pnl
  173. position = 0
  174. trades.append({
  175. 'date': date,
  176. 'action': 'SELL',
  177. 'price': price,
  178. 'capital': capital,
  179. 'pnl': pnl,
  180. 'return_pct': (price / entry_price - 1) * 100
  181. })
  182. # 计算当前持仓
  183. current_position = position
  184. current_price = df['close'].iloc[-1]
  185. if position == 1:
  186. unrealized_pnl = (current_price / entry_price - 1) * capital
  187. total_value = capital + unrealized_pnl
  188. else:
  189. total_value = capital
  190. total_return = (total_value / initial_capital - 1) * 100
  191. return {
  192. 'trades': trades,
  193. 'current_position': current_position,
  194. 'current_price': current_price,
  195. 'entry_price': entry_price if position == 1 else None,
  196. 'capital': capital,
  197. 'total_value': total_value,
  198. 'total_return': total_return,
  199. 'trade_count': len([t for t in trades if t['action'] == 'SELL']),
  200. 'data_end_date': df.index[-1].strftime('%Y-%m-%d')
  201. }
  202. def get_recent_indicators(self, days=20):
  203. """获取近N天指标详情"""
  204. df = self.data.tail(days).copy()
  205. indicators = []
  206. for date, row in df.iterrows():
  207. indicators.append({
  208. 'date': date.strftime('%Y-%m-%d'),
  209. 'close': round(row['close'], 2),
  210. 'ma10': round(row['ma10'], 2) if not pd.isna(row['ma10']) else '-',
  211. 'ma30': round(row['ma30'], 2) if not pd.isna(row['ma30']) else '-',
  212. 'high_20': round(row['high_20'], 2) if not pd.isna(row['high_20']) else '-',
  213. 'ret_10': f"{row['ret_10']*100:.2f}%" if not pd.isna(row['ret_10']) else '-',
  214. 'signal': '买入' if row['signal'] == 1 else ('卖出' if row['signal'] == -1 else '持有'),
  215. 'atr': round(row['atr'], 2) if not pd.isna(row['atr']) else '-'
  216. })
  217. return indicators
  218. def get_recent_trades(self, n=20):
  219. """获取近N次交易详情"""
  220. result = self.backtest()
  221. trades = result['trades'][-n:]
  222. return trades
  223. def generate_report():
  224. """生成完整报告"""
  225. strategy = TrendTrackingStrategy()
  226. if not strategy.load_and_merge_data():
  227. return None, None, None
  228. strategy.calculate_indicators()
  229. result = strategy.backtest()
  230. # 获取近期数据
  231. recent_indicators = strategy.get_recent_indicators(20)
  232. recent_trades = strategy.get_recent_trades(20)
  233. # 生成HTML报告
  234. html = f"""
  235. <html>
  236. <head>
  237. <meta charset="utf-8">
  238. <style>
  239. body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
  240. .container {{ max-width: 1200px; margin: 0 auto; background: white; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }}
  241. h1 {{ color: #333; border-bottom: 3px solid #007bff; padding-bottom: 10px; }}
  242. h2 {{ color: #555; margin-top: 30px; border-left: 4px solid #007bff; padding-left: 10px; }}
  243. .summary {{ background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 20px 0; }}
  244. .metric {{ display: inline-block; margin: 10px 20px 10px 0; }}
  245. .metric-label {{ color: #666; font-size: 12px; }}
  246. .metric-value {{ font-size: 24px; font-weight: bold; color: #333; }}
  247. .positive {{ color: #28a745; }}
  248. .negative {{ color: #dc3545; }}
  249. table {{ width: 100%; border-collapse: collapse; margin: 20px 0; font-size: 13px; }}
  250. th {{ background: #007bff; color: white; padding: 10px; text-align: left; }}
  251. td {{ padding: 8px 10px; border-bottom: 1px solid #ddd; }}
  252. tr:nth-child(even) {{ background: #f8f9fa; }}
  253. tr:hover {{ background: #e9ecef; }}
  254. .position-yes {{ color: #28a745; font-weight: bold; }}
  255. .position-no {{ color: #666; }}
  256. .buy {{ color: #28a745; font-weight: bold; }}
  257. .sell {{ color: #dc3545; font-weight: bold; }}
  258. .data-info {{ background: #e7f3ff; padding: 10px; border-radius: 5px; margin: 10px 0; color: #004085; }}
  259. </style>
  260. </head>
  261. <body>
  262. <div class="container">
  263. <h1>🚀 创业板50趋势跟踪策略报告</h1>
  264. <p style="color: #666;">生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
  265. <div class="data-info">
  266. <strong>数据更新:</strong> 已合并历史数据与实时数据,最新数据日期: {result['data_end_date']}
  267. </div>
  268. <div class="summary">
  269. <h2>📊 总体绩效</h2>
  270. <div class="metric">
  271. <div class="metric-label">当前持仓</div>
  272. <div class="metric-value {'position-yes' if result['current_position'] == 1 else 'position-no'}">
  273. {'持有中' if result['current_position'] == 1 else '空仓'}
  274. </div>
  275. </div>
  276. <div class="metric">
  277. <div class="metric-label">当前价格</div>
  278. <div class="metric-value">{result['current_price']:.2f}</div>
  279. </div>
  280. <div class="metric">
  281. <div class="metric-label">累计收益率</div>
  282. <div class="metric-value {'positive' if result['total_return'] >= 0 else 'negative'}">
  283. {result['total_return']:+.2f}%
  284. </div>
  285. </div>
  286. <div class="metric">
  287. <div class="metric-label">总资产</div>
  288. <div class="metric-value">{result['total_value']:,.0f}元</div>
  289. </div>
  290. <div class="metric">
  291. <div class="metric-label">交易次数</div>
  292. <div class="metric-value">{result['trade_count']}</div>
  293. </div>
  294. </div>
  295. """
  296. # 持仓详情
  297. if result['current_position'] == 1:
  298. unrealized = (result['current_price'] / result['entry_price'] - 1) * 100
  299. html += f"""
  300. <div class="summary" style="background: #e8f5e9;">
  301. <h2>📈 持仓详情</h2>
  302. <p><strong>入场价格:</strong> {result['entry_price']:.2f} 元</p>
  303. <p><strong>当前浮盈:</strong> <span class="{'positive' if unrealized >= 0 else 'negative'}">{unrealized:+.2f}%</span></p>
  304. </div>
  305. """
  306. # 近20天指标
  307. html += """
  308. <h2>📅 近20天指标详情</h2>
  309. <table>
  310. <tr>
  311. <th>日期</th>
  312. <th>收盘价</th>
  313. <th>MA10</th>
  314. <th>MA30</th>
  315. <th>20日高</th>
  316. <th>10日涨幅</th>
  317. <th>信号</th>
  318. <th>ATR</th>
  319. </tr>
  320. """
  321. for ind in recent_indicators:
  322. signal_class = 'buy' if ind['signal'] == '买入' else ('sell' if ind['signal'] == '卖出' else '')
  323. html += f"""
  324. <tr>
  325. <td>{ind['date']}</td>
  326. <td>{ind['close']}</td>
  327. <td>{ind['ma10']}</td>
  328. <td>{ind['ma30']}</td>
  329. <td>{ind['high_20']}</td>
  330. <td>{ind['ret_10']}</td>
  331. <td class="{signal_class}">{ind['signal']}</td>
  332. <td>{ind['atr']}</td>
  333. </tr>
  334. """
  335. html += """
  336. </table>
  337. <h2>💼 近20次交易详情</h2>
  338. <table>
  339. <tr>
  340. <th>日期</th>
  341. <th>操作</th>
  342. <th>价格</th>
  343. <th>盈亏金额</th>
  344. <th>盈亏比例</th>
  345. <th>总资产</th>
  346. </tr>
  347. """
  348. for trade in recent_trades:
  349. action_class = 'buy' if trade['action'] == 'BUY' else 'sell'
  350. pnl = trade.get('pnl', 0)
  351. ret = trade.get('return_pct', 0)
  352. html += f"""
  353. <tr>
  354. <td>{trade['date'].strftime('%Y-%m-%d') if isinstance(trade['date'], pd.Timestamp) else trade['date']}</td>
  355. <td class="{action_class}">{trade['action']}</td>
  356. <td>{trade['price']:.2f}</td>
  357. <td>{f'{pnl:+,.0f}元' if 'pnl' in trade else '-'}</td>
  358. <td class="{'positive' if ret >= 0 else 'negative'}">{f'{ret:+.2f}%' if 'return_pct' in trade else '-'}</td>
  359. <td>{trade['capital']:,.0f}元</td>
  360. </tr>
  361. """
  362. html += """
  363. </table>
  364. <div style="margin-top: 30px; padding: 15px; background: #fff3cd; border-radius: 5px; color: #856404;">
  365. <strong>策略说明:</strong><br>
  366. • 买入条件: 价格>MA10>MA30 且 突破20日高×0.995 且 10日涨幅>2%<br>
  367. • 卖出条件: 跌破MA30 或 创20日新低 或 回撤10%止损<br>
  368. • 数据更新: 已合并历史数据与实时市场数据
  369. </div>
  370. </div>
  371. </body>
  372. </html>
  373. """
  374. # 生成文本报告
  375. text = f"""
  376. 创业板50趋势跟踪策略报告
  377. 生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  378. 数据最新日期: {result['data_end_date']}
  379. 【总体绩效】
  380. 当前持仓: {'持有中' if result['current_position'] == 1 else '空仓'}
  381. 当前价格: {result['current_price']:.2f}元
  382. 累计收益率: {result['total_return']:+.2f}%
  383. 总资产: {result['total_value']:,.0f}元
  384. 交易次数: {result['trade_count']}
  385. 【近20天指标】
  386. 日期 收盘价 MA10 MA30 20日高 10日涨幅 信号 ATR
  387. """
  388. for ind in recent_indicators:
  389. text += f"{ind['date']} {ind['close']:>8} {ind['ma10']:>8} {ind['ma30']:>8} {ind['high_20']:>8} {ind['ret_10']:>8} {ind['signal']:>4} {ind['atr']:>6}\n"
  390. text += "\n【近20次交易】\n"
  391. text += "日期 操作 价格 盈亏金额 盈亏比例 总资产\n"
  392. for trade in recent_trades:
  393. pnl_str = f"{trade.get('pnl', 0):+,.0f}元" if 'pnl' in trade else '-'
  394. ret_str = f"{trade.get('return_pct', 0):+.2f}%" if 'return_pct' in trade else '-'
  395. date_str = trade['date'].strftime('%Y-%m-%d') if isinstance(trade['date'], pd.Timestamp) else trade['date']
  396. text += f"{date_str} {trade['action']:>6} {trade['price']:>8.2f} {pnl_str:>12} {ret_str:>10} {trade['capital']:>12,.0f}元\n"
  397. return html, text, result
  398. def send_email(subject, html_content, text_content):
  399. """发送邮件"""
  400. try:
  401. msg = MIMEMultipart('alternative')
  402. msg['Subject'] = Header(subject, 'utf-8')
  403. msg['From'] = EMAIL_CONFIG['sender_email']
  404. msg['To'] = EMAIL_CONFIG['receiver_email']
  405. text_part = MIMEText(text_content, 'plain', 'utf-8')
  406. msg.attach(text_part)
  407. html_part = MIMEText(html_content, 'html', 'utf-8')
  408. msg.attach(html_part)
  409. with smtplib.SMTP(EMAIL_CONFIG['smtp_server'], EMAIL_CONFIG['smtp_port']) as server:
  410. server.sendmail(
  411. EMAIL_CONFIG['sender_email'],
  412. EMAIL_CONFIG['receiver_email'],
  413. msg.as_string()
  414. )
  415. print(f"✅ 邮件发送成功: {subject}")
  416. return True
  417. except Exception as e:
  418. print(f"❌ 邮件发送失败: {e}")
  419. return False
  420. def main():
  421. """主程序"""
  422. print("="*60)
  423. print("🚀 创业板50趋势跟踪实时报告 (含实时数据)")
  424. print("="*60)
  425. print(f"执行时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
  426. # 生成报告
  427. print("\n📊 加载数据并生成报告中...")
  428. html, text, result = generate_report()
  429. if html is None:
  430. print("❌ 报告生成失败")
  431. return
  432. print(f"✅ 报告生成完成")
  433. print(f" 数据最新日期: {result['data_end_date']}")
  434. print(f" 当前持仓: {'持有中' if result['current_position'] == 1 else '空仓'}")
  435. print(f" 累计收益: {result['total_return']:+.2f}%")
  436. print(f" 交易次数: {result['trade_count']}")
  437. # 发送邮件
  438. print("\n📧 发送邮件...")
  439. position_status = "持仓" if result['current_position'] == 1 else "空仓"
  440. subject = f"🚀 创业板50趋势报告 {datetime.now().strftime('%m-%d %H:%M')} | {position_status} | 收益{result['total_return']:+.2f}%"
  441. send_email(subject, html, text)
  442. print("\n✅ 全部完成!")
  443. print("="*60)
  444. if __name__ == "__main__":
  445. main()