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

290 lines
9.9 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
"""
每日美股持仓盈亏报告
"""
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()