305 lines
8.7 KiB
Python
305 lines
8.7 KiB
Python
|
|
#!/usr/bin/env python3
|
|||
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
发送持仓盈亏报告邮件
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
import smtplib
|
|||
|
|
from email.mime.text import MIMEText
|
|||
|
|
from email.mime.multipart import MIMEMultipart
|
|||
|
|
from email.header import Header
|
|||
|
|
import requests
|
|||
|
|
import json
|
|||
|
|
from datetime import datetime
|
|||
|
|
|
|||
|
|
# SMTP 配置
|
|||
|
|
SMTP_SERVER = "smtp.163.com"
|
|||
|
|
SMTP_PORT = 465
|
|||
|
|
EMAIL = "work_fyx02@163.com"
|
|||
|
|
AUTH_CODE = "QLrTpw7SDxrMuAzh"
|
|||
|
|
|
|||
|
|
# 收件人
|
|||
|
|
TO_EMAIL = "Yaxing_feng@dgmaorui.com"
|
|||
|
|
|
|||
|
|
# Yahoo Finance API 配置
|
|||
|
|
YAHOO_API_URL = "https://query1.finance.yahoo.com/v8/finance/chart/"
|
|||
|
|
|
|||
|
|
# 指数配置
|
|||
|
|
INDEXES = {
|
|||
|
|
"^GSPC": "标普500",
|
|||
|
|
"^IXIC": "纳斯达克",
|
|||
|
|
"^DJI": "道琼斯"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 股票配置(从Notion数据库读取)
|
|||
|
|
STOCKS = [
|
|||
|
|
{"symbol": "MSFT", "name": "Microsoft Corporation", "quantity": 1, "cost_basis": 439.00}
|
|||
|
|
]
|
|||
|
|
|
|||
|
|
def get_stock_price(symbol):
|
|||
|
|
"""获取股票实时价格"""
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
url = f"{YAHOO_API_URL}{symbol}"
|
|||
|
|
params = {
|
|||
|
|
"interval": "1d",
|
|||
|
|
"range": "1d"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
headers = {
|
|||
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
response = requests.get(url, params=params, headers=headers, timeout=10)
|
|||
|
|
|
|||
|
|
if response.status_code == 200:
|
|||
|
|
data = response.json()
|
|||
|
|
|
|||
|
|
if "chart" in data and "result" in data["chart"]:
|
|||
|
|
result = data["chart"]["result"]
|
|||
|
|
if result and len(result) > 0:
|
|||
|
|
meta = result[0].get("meta", {})
|
|||
|
|
current_price = meta.get("regularMarketPrice")
|
|||
|
|
previous_close = meta.get("chartPreviousClose")
|
|||
|
|
|
|||
|
|
if current_price and previous_close:
|
|||
|
|
change = current_price - previous_close
|
|||
|
|
change_percent = (change / previous_close) * 100
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"symbol": symbol,
|
|||
|
|
"price": current_price,
|
|||
|
|
"previous_close": previous_close,
|
|||
|
|
"change": change,
|
|||
|
|
"change_percent": change_percent,
|
|||
|
|
"currency": meta.get("currency", "USD")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"获取 {symbol} 价格失败: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def get_index_prices():
|
|||
|
|
"""获取指数价格"""
|
|||
|
|
|
|||
|
|
indexes = {}
|
|||
|
|
|
|||
|
|
for symbol, name in INDEXES.items():
|
|||
|
|
price_data = get_stock_price(symbol)
|
|||
|
|
if price_data:
|
|||
|
|
indexes[symbol] = {
|
|||
|
|
"name": name,
|
|||
|
|
"price": price_data["price"],
|
|||
|
|
"change": price_data["change"],
|
|||
|
|
"change_percent": price_data["change_percent"]
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return indexes
|
|||
|
|
|
|||
|
|
def calculate_pnl():
|
|||
|
|
"""计算持仓盈亏"""
|
|||
|
|
|
|||
|
|
total_cost = 0
|
|||
|
|
total_market_value = 0
|
|||
|
|
total_pnl = 0
|
|||
|
|
|
|||
|
|
stock_details = []
|
|||
|
|
|
|||
|
|
for stock in STOCKS:
|
|||
|
|
symbol = stock["symbol"]
|
|||
|
|
name = stock["name"]
|
|||
|
|
quantity = stock["quantity"]
|
|||
|
|
cost_basis = stock["cost_basis"]
|
|||
|
|
|
|||
|
|
# 计算总成本
|
|||
|
|
cost = quantity * cost_basis
|
|||
|
|
total_cost += cost
|
|||
|
|
|
|||
|
|
# 获取当前价格
|
|||
|
|
price_data = get_stock_price(symbol)
|
|||
|
|
|
|||
|
|
if price_data:
|
|||
|
|
current_price = price_data["price"]
|
|||
|
|
change = price_data["change"]
|
|||
|
|
change_percent = price_data["change_percent"]
|
|||
|
|
|
|||
|
|
# 计算当前市值
|
|||
|
|
market_value = quantity * current_price
|
|||
|
|
total_market_value += market_value
|
|||
|
|
|
|||
|
|
# 计算盈亏
|
|||
|
|
pnl = market_value - cost
|
|||
|
|
total_pnl += pnl
|
|||
|
|
|
|||
|
|
# 计算盈亏百分比
|
|||
|
|
pnl_percent = (pnl / cost) * 100
|
|||
|
|
|
|||
|
|
stock_details.append({
|
|||
|
|
"symbol": symbol,
|
|||
|
|
"name": name,
|
|||
|
|
"quantity": quantity,
|
|||
|
|
"cost_basis": cost_basis,
|
|||
|
|
"current_price": current_price,
|
|||
|
|
"cost": cost,
|
|||
|
|
"market_value": market_value,
|
|||
|
|
"pnl": pnl,
|
|||
|
|
"pnl_percent": pnl_percent,
|
|||
|
|
"change": change,
|
|||
|
|
"change_percent": change_percent
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
# 计算总盈亏百分比
|
|||
|
|
total_pnl_percent = 0
|
|||
|
|
if total_cost > 0:
|
|||
|
|
total_pnl_percent = (total_pnl / total_cost) * 100
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
"date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|||
|
|
"total_cost": total_cost,
|
|||
|
|
"total_market_value": total_market_value,
|
|||
|
|
"total_pnl": total_pnl,
|
|||
|
|
"total_pnl_percent": total_pnl_percent,
|
|||
|
|
"stocks": stock_details
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
def send_pnl_report():
|
|||
|
|
"""发送盈亏报告邮件"""
|
|||
|
|
|
|||
|
|
print("=" * 60)
|
|||
|
|
print("发送持仓盈亏报告")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"发件人: {EMAIL}")
|
|||
|
|
print(f"收件人: {TO_EMAIL}")
|
|||
|
|
print(f"SMTP 服务器: {SMTP_SERVER}:{SMTP_PORT}")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 获取盈亏数据
|
|||
|
|
print("\n正在获取盈亏数据...")
|
|||
|
|
pnl_data = calculate_pnl()
|
|||
|
|
|
|||
|
|
# 获取指数数据
|
|||
|
|
print("正在获取指数数据...")
|
|||
|
|
indexes = get_index_prices()
|
|||
|
|
|
|||
|
|
# 格式化邮件内容
|
|||
|
|
date_str = datetime.now().strftime("%Y年%m月%d日")
|
|||
|
|
|
|||
|
|
body = f"""📧 **美股持仓盈亏日报**
|
|||
|
|
|
|||
|
|
**日期:** {date_str}
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 持仓概览
|
|||
|
|
|
|||
|
|
**总持仓市值:** ${pnl_data['total_market_value']:.2f}
|
|||
|
|
**总成本:** ${pnl_data['total_cost']:.2f}
|
|||
|
|
**总盈亏:** ${pnl_data['total_pnl']:+.2f} ({pnl_data['total_pnl_percent']:+.2f}%)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📈 个股表现
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 添加个股详情
|
|||
|
|
for stock in pnl_data["stocks"]:
|
|||
|
|
body += f"""
|
|||
|
|
### {stock['symbol']} - {stock['name']}
|
|||
|
|
- **持仓:** {stock['quantity']}股 @ ${stock['cost_basis']}
|
|||
|
|
- **当前价:** ${stock['current_price']:.2f}
|
|||
|
|
- **总成本:** ${stock['cost']:.2f}
|
|||
|
|
- **当前市值:** ${stock['market_value']:.2f}
|
|||
|
|
- **盈亏:** ${stock['pnl']:+.2f} ({stock['pnl_percent']:+.2f}%)
|
|||
|
|
- **日涨跌:** ${stock['change']:+.2f} ({stock['change_percent']:+.2f}%)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 添加指数数据
|
|||
|
|
body += """
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 大盘指数
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
for symbol, data in indexes.items():
|
|||
|
|
body += f"""
|
|||
|
|
- **{data['name']}:** {data['price']:.2f} ({data['change_percent']:+.2f}%)
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 添加总结
|
|||
|
|
body += f"""
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📝 总结
|
|||
|
|
|
|||
|
|
**今日盈亏:** ${pnl_data['total_pnl']:+.2f}
|
|||
|
|
**盈亏百分比:** ${pnl_data['total_pnl_percent']:+.2f}%
|
|||
|
|
**持仓股票数量:** {len(pnl_data['stocks'])} 只
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**此报告由 OpenClaw 自动发送**
|
|||
|
|
**每日定时任务:美东时间收盘后(北京时间早上6点后)**
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
# 创建邮件
|
|||
|
|
msg = MIMEMultipart()
|
|||
|
|
msg["From"] = Header(f"OpenClaw <{EMAIL}>", "utf-8")
|
|||
|
|
msg["To"] = Header(TO_EMAIL, "utf-8")
|
|||
|
|
msg["Subject"] = Header(f"美股持仓盈亏日报 - {date_str}", "utf-8")
|
|||
|
|
|
|||
|
|
msg.attach(MIMEText(body, "plain", "utf-8"))
|
|||
|
|
|
|||
|
|
# 连接 SMTP 服务器并发送邮件
|
|||
|
|
print("\n正在连接 SMTP 服务器...")
|
|||
|
|
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server:
|
|||
|
|
print(f"连接成功: {SMTP_SERVER}:{SMTP_PORT}")
|
|||
|
|
|
|||
|
|
print("正在登录...")
|
|||
|
|
server.login(EMAIL, AUTH_CODE)
|
|||
|
|
print(f"登录成功: {EMAIL}")
|
|||
|
|
|
|||
|
|
print("正在发送邮件...")
|
|||
|
|
server.send_message(msg)
|
|||
|
|
print("邮件发送成功!")
|
|||
|
|
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("✅ 盈亏报告发送完成")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"收件人: {TO_EMAIL}")
|
|||
|
|
print(f"总盈亏: ${pnl_data['total_pnl']:+.2f} ({pnl_data['total_pnl_percent']:+.2f}%)")
|
|||
|
|
print("=" * 60)
|
|||
|
|
|
|||
|
|
return True
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
print("\n" + "=" * 60)
|
|||
|
|
print("❌ 发送失败")
|
|||
|
|
print("=" * 60)
|
|||
|
|
print(f"错误信息: {e}")
|
|||
|
|
print("=" * 60)
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def main():
|
|||
|
|
"""主函数"""
|
|||
|
|
print("开始发送持仓盈亏报告...")
|
|||
|
|
print()
|
|||
|
|
|
|||
|
|
success = send_pnl_report()
|
|||
|
|
|
|||
|
|
if success:
|
|||
|
|
print("\n💡 提示:")
|
|||
|
|
print("1. 邮件已发送到目标邮箱")
|
|||
|
|
print("2. 请检查收件箱(包括垃圾邮件)")
|
|||
|
|
print("3. 每天定时发送盈亏报告")
|
|||
|
|
else:
|
|||
|
|
print("\n🔧 解决方案:")
|
|||
|
|
print("1. 检查 SMTP 配置")
|
|||
|
|
print("2. 检查网络连接")
|
|||
|
|
print("3. 检查股票数据获取")
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
main()
|