Files
server-configs/send-pnl-report.py
2026-02-13 22:24:27 +08:00

305 lines
8.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()