290 lines
9.9 KiB
Python
290 lines
9.9 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
每日美股持仓盈亏报告
|
||
"""
|
||
|
||
import requests
|
||
import smtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
from datetime import datetime
|
||
|
||
# Notion API 配置
|
||
NOTION_API_KEY = "ntn_c43902219395mirQBetIfYoww1qKCAF14GBRUQeDee29o2"
|
||
DATABASE_ID = "2fb105ad-7873-8175-bbbd-e5b87cf101d9"
|
||
|
||
# 邮件配置
|
||
SMTP_SERVER = "smtp.163.com"
|
||
SMTP_PORT = 465
|
||
EMAIL_USER = "work_fyx02@163.com"
|
||
EMAIL_PASSWORD = "QLrTpw7SDxrMuAzh"
|
||
RECIPIENT_EMAIL = "Yaxing_feng@dgmaorui.com"
|
||
|
||
# 指数代码
|
||
INDICES = {
|
||
"^GSPC": "标普500",
|
||
"^IXIC": "纳斯达克",
|
||
"^DJI": "道琼斯"
|
||
}
|
||
|
||
def get_current_price(symbol):
|
||
"""获取股票当前价格(从Yahoo Finance)"""
|
||
try:
|
||
url = f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}"
|
||
params = {"range": "1d", "interval": "1m"}
|
||
response = requests.get(url, params=params, timeout=10)
|
||
|
||
if response.status_code == 200:
|
||
data = response.json()
|
||
chart_result = data.get("chart", {}).get("result", [{}])
|
||
if chart_result:
|
||
meta = chart_result[0].get("meta", {})
|
||
return meta.get("regularMarketPrice")
|
||
except Exception as e:
|
||
print(f"获取价格失败 {symbol}: {e}")
|
||
|
||
return None
|
||
|
||
def get_index_prices():
|
||
"""获取指数价格"""
|
||
prices = {}
|
||
for symbol, name in INDICES.items():
|
||
price = get_current_price(symbol)
|
||
if price:
|
||
prices[symbol] = {"name": name, "price": price}
|
||
return prices
|
||
|
||
def get_all_positions():
|
||
"""从Notion获取所有持仓"""
|
||
url = "https://api.notion.com/v1/databases/" + DATABASE_ID + "/query"
|
||
|
||
headers = {
|
||
"Authorization": f"Bearer {NOTION_API_KEY}",
|
||
"Content-Type": "application/json",
|
||
"Notion-Version": "2022-06-28"
|
||
}
|
||
|
||
response = requests.post(url, headers=headers)
|
||
|
||
if response.status_code == 200:
|
||
results = response.json().get("results", [])
|
||
positions = []
|
||
|
||
for page in results:
|
||
props = page["properties"]
|
||
|
||
symbol = props.get("股票代码", {}).get("title", [{}])[0].get("text", {}).get("content", "")
|
||
shares = props.get("持仓数量", {}).get("number", 0)
|
||
cost = props.get("买入成本", {}).get("number", 0)
|
||
current_price = props.get("当前价格", {}).get("number", 0)
|
||
market_value = props.get("当前市值", {}).get("number", 0)
|
||
pnl_amount = props.get("盈亏金额", {}).get("number", 0)
|
||
pnl_percent = props.get("盈亏百分比", {}).get("number", 0)
|
||
|
||
if symbol:
|
||
positions.append({
|
||
"symbol": symbol,
|
||
"shares": shares,
|
||
"cost": cost,
|
||
"current_price": current_price,
|
||
"market_value": market_value,
|
||
"pnl_amount": pnl_amount,
|
||
"pnl_percent": pnl_percent
|
||
})
|
||
|
||
return positions
|
||
else:
|
||
print(f"获取持仓失败: {response.status_code}")
|
||
return []
|
||
|
||
def calculate_portfolio_summary(positions):
|
||
"""计算投资组合汇总"""
|
||
total_cost = sum(p.get("cost", 0) or 0 for p in positions)
|
||
total_market_value = sum(p.get("market_value", 0) or 0 for p in positions)
|
||
total_pnl = total_market_value - total_cost
|
||
total_pnl_percent = (total_pnl / total_cost * 100) if total_cost > 0 else 0
|
||
|
||
return {
|
||
"total_cost": total_cost,
|
||
"total_market_value": total_market_value,
|
||
"total_pnl": total_pnl,
|
||
"total_pnl_percent": total_pnl_percent
|
||
}
|
||
|
||
def generate_html_report(positions, summary, indices):
|
||
"""生成HTML报告"""
|
||
|
||
# 获取当前时间(UTC+8)
|
||
now = datetime.utcnow()
|
||
date_str = now.strftime("%Y-%m-%d %H:%M")
|
||
|
||
# 生成持仓明细HTML
|
||
positions_html = ""
|
||
for p in positions:
|
||
symbol = p.get("symbol", "")
|
||
shares = p.get("shares", 0) or 0
|
||
cost = p.get("cost", 0) or 0
|
||
current_price = p.get("current_price", 0) or 0
|
||
market_value = p.get("market_value", 0) or 0
|
||
pnl_amount = p.get("pnl_amount", 0) or 0
|
||
pnl_percent = p.get("pnl_percent", 0) or 0
|
||
pnl_color = "green" if pnl_amount >= 0 else "red"
|
||
positions_html += f"""
|
||
<tr>
|
||
<td style="padding: 8px; border: 1px solid #ddd;">{symbol}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right;">{shares}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right;">${cost:.2f}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right;">${current_price:.2f}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right;">${market_value:.2f}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right; color: {pnl_color};">${pnl_amount:.2f}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right; color: {pnl_color};">{pnl_percent:.2f}%</td>
|
||
</tr>
|
||
"""
|
||
|
||
# 生成指数HTML
|
||
indices_html = ""
|
||
for symbol, data in indices.items():
|
||
indices_html += f"""
|
||
<tr>
|
||
<td style="padding: 8px; border: 1px solid #ddd;">{data['name']}</td>
|
||
<td style="padding: 8px; border: 1px solid #ddd; text-align: right;">{data['price']:.2f}</td>
|
||
</tr>
|
||
"""
|
||
|
||
# 汇总颜色
|
||
summary_pnl_color = "green" if summary["total_pnl"] >= 0 else "red"
|
||
|
||
html = f"""
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<style>
|
||
body {{ font-family: Arial, sans-serif; margin: 20px; }}
|
||
h1 {{ color: #333; }}
|
||
h2 {{ color: #555; margin-top: 30px; }}
|
||
table {{ border-collapse: collapse; width: 100%; margin-bottom: 20px; }}
|
||
th {{ background-color: #f2f2f2; padding: 10px; border: 1px solid #ddd; text-align: left; }}
|
||
.summary {{ background-color: #e8f4f8; padding: 15px; border-radius: 5px; margin: 20px 0; }}
|
||
.summary-item {{ margin: 8px 0; font-size: 16px; }}
|
||
.positive {{ color: green; }}
|
||
.negative {{ color: red; }}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>📊 美股持仓盈亏日报</h1>
|
||
<p><strong>报告时间:</strong>{date_str} (北京时间)</p>
|
||
|
||
<div class="summary">
|
||
<h2>📈 投资组合汇总</h2>
|
||
<div class="summary-item"><strong>总成本:</strong>${summary['total_cost']:.2f}</div>
|
||
<div class="summary-item"><strong>当前市值:</strong>${summary['total_market_value']:.2f}</div>
|
||
<div class="summary-item"><strong>总盈亏:</strong>
|
||
<span class="{'positive' if summary['total_pnl'] >= 0 else 'negative'}">
|
||
<strong>${summary['total_pnl']:.2f} ({summary['total_pnl_percent']:.2f}%)</strong>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<h2>📋 持仓明细</h2>
|
||
<table>
|
||
<tr>
|
||
<th>股票代码</th>
|
||
<th>持仓数量</th>
|
||
<th>买入成本</th>
|
||
<th>当前价格</th>
|
||
<th>当前市值</th>
|
||
<th>盈亏金额</th>
|
||
<th>盈亏百分比</th>
|
||
</tr>
|
||
{positions_html}
|
||
</table>
|
||
|
||
<h2>📊 主要指数</h2>
|
||
<table>
|
||
<tr>
|
||
<th>指数名称</th>
|
||
<th>当前点位</th>
|
||
</tr>
|
||
{indices_html}
|
||
</table>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 30px;">
|
||
此报告由 OpenClaw 自动生成<br>
|
||
数据来源:Yahoo Finance API<br>
|
||
更新时间:每日早上 6:30 (北京时间)
|
||
</p>
|
||
</body>
|
||
</html>
|
||
"""
|
||
|
||
return html
|
||
|
||
def send_email(subject, html_content):
|
||
"""发送邮件"""
|
||
try:
|
||
# 创建邮件
|
||
msg = MIMEMultipart('alternative')
|
||
msg['Subject'] = subject
|
||
msg['From'] = EMAIL_USER
|
||
msg['To'] = RECIPIENT_EMAIL
|
||
|
||
# 添加HTML内容
|
||
html_part = MIMEText(html_content, 'html', 'utf-8')
|
||
msg.attach(html_part)
|
||
|
||
# 发送邮件
|
||
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as server:
|
||
server.login(EMAIL_USER, EMAIL_PASSWORD)
|
||
server.send_message(msg)
|
||
|
||
print(f"✅ 邮件发送成功: {subject}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"❌ 邮件发送失败: {e}")
|
||
return False
|
||
|
||
def main():
|
||
print("=" * 60)
|
||
print("开始生成每日美股持仓盈亏报告...")
|
||
print("=" * 60)
|
||
|
||
# 获取持仓
|
||
print("\n1. 获取持仓数据...")
|
||
positions = get_all_positions()
|
||
|
||
if not positions:
|
||
print("❌ 未找到持仓数据")
|
||
return
|
||
|
||
print(f"✅ 找到 {len(positions)} 个持仓")
|
||
|
||
# 获取指数价格
|
||
print("\n2. 获取指数价格...")
|
||
indices = get_index_prices()
|
||
print(f"✅ 获取到 {len(indices)} 个指数价格")
|
||
|
||
# 计算汇总
|
||
print("\n3. 计算投资组合汇总...")
|
||
summary = calculate_portfolio_summary(positions)
|
||
print(f" 总成本: ${summary['total_cost']:.2f}")
|
||
print(f" 当前市值: ${summary['total_market_value']:.2f}")
|
||
print(f" 总盈亏: ${summary['total_pnl']:.2f} ({summary['total_pnl_percent']:.2f}%)")
|
||
|
||
# 生成报告
|
||
print("\n4. 生成HTML报告...")
|
||
html_report = generate_html_report(positions, summary, indices)
|
||
print("✅ 报告生成完成")
|
||
|
||
# 发送邮件
|
||
print("\n5. 发送邮件...")
|
||
subject = f"📊 美股持仓盈亏日报 - {datetime.now().strftime('%Y-%m-%d')}"
|
||
send_email(subject, html_report)
|
||
|
||
print("\n" + "=" * 60)
|
||
print("报告生成和发送完成!")
|
||
print("=" * 60)
|
||
|
||
if __name__ == "__main__":
|
||
main()
|