Files
server-configs/daily-stock-report.py

290 lines
9.9 KiB
Python
Raw Permalink Normal View History

2026-02-13 22:24:27 +08:00
#!/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()