303 lines
8.6 KiB
Python
303 lines
8.6 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"日期: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||
print(f"发件人: {EMAIL}")
|
||
print(f"收件人: {TO_EMAIL}")
|
||
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("请检查网络连接和API配置")
|
||
|
||
if __name__ == "__main__":
|
||
main()
|