#!/usr/bin/env python3 """每日股票更新 - Yahoo Finance (yfinance)""" import requests import yfinance as yf import time from datetime import datetime USER_DB_ID = "2fb105ad78738175bbbde5b87cf101d9" AUTO_DB_ID = "300105ad-7873-812a-bbda-d3019703fed1" NOTION_TOKEN = "ntn_c43902219395mirQBetIfYoww1qKCAF14GBRUQeDee29o2" # 备用价格 BACKUP_PRICES = {"SGOV": 100.43, "MSFT": 401.14, "BND": 74.23, "VOO": 635.24} def get_user_holdings(): url = f"https://api.notion.com/v1/databases/{USER_DB_ID}/query" resp = requests.post(url, headers={ "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": "2022-06-28" }, json={}, timeout=30) if resp.status_code == 200: holdings = {} for item in resp.json().get("results", []): props = item.get("properties", {}) code = "" titles = props.get("股票代码", {}).get("title", []) if titles: code = titles[0].get("plain_text", "").upper() shares = props.get("持仓数量", {}).get("number", 0) or 0 cost_per = props.get("持仓成本", {}).get("number", 0) or 0 if code and shares > 0: holdings[code] = {"shares": shares, "cost_per": cost_per, "cost": round(shares * cost_per, 2)} return holdings return {} def get_price(code): """Yahoo Finance (yfinance)""" time.sleep(2) try: ticker = yf.Ticker(code) hist = ticker.history(period="1d") if len(hist) > 0: return round(hist['Close'].iloc[-1], 2), "Yahoo" except: pass if code in BACKUP_PRICES: return BACKUP_PRICES[code], "备用" return None, None def get_auto_pages(): url = f"https://api.notion.com/v1/databases/{AUTO_DB_ID}/query" resp = requests.post(url, headers={ "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": "2022-06-28" }, json={}, timeout=30) pages = {} for item in resp.json().get("results", []): props = item.get("properties", {}) code = "" texts = props.get("代码", {}).get("rich_text", []) if texts: code = texts[0].get("plain_text", "").upper() if code: pages[code] = item.get("id") return pages def update_auto_table(holdings, prices, auto_pages): updated = 0 for code, info in holdings.items(): price = prices.get(code) if not price: continue shares = info["shares"] cost = info["cost"] value = round(shares * price, 2) pnl = round(value - cost, 2) pnl_pct = round((pnl / cost * 100), 2) if cost > 0 else 0 data = { "properties": { "当前价格": {"number": price}, "持仓数量": {"number": shares}, "当前市值": {"number": value}, "成本": {"number": cost}, "盈亏金额": {"number": pnl}, "盈亏百分比": {"number": pnl_pct / 100}, "最后更新": {"date": {"start": datetime.now().strftime("%Y-%m-%d")}} } } if code in auto_pages: url = f"https://api.notion.com/v1/pages/{auto_pages[code]}" resp = requests.patch(url, headers={ "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": "2022-06-28" }, json=data, timeout=30) else: url = "https://api.notion.com/v1/pages" data["parent"] = {"database_id": AUTO_DB_ID} data["properties"]["名称"] = {"title": [{"text": {"content": code}}]} data["properties"]["代码"] = {"rich_text": [{"text": {"content": code}}]} resp = requests.post(url, headers={ "Authorization": f"Bearer {NOTION_TOKEN}", "Content-Type": "application/json", "Notion-Version": "2022-06-28" }, json=data, timeout=30) if resp.status_code in [200, 201]: updated += 1 return updated def generate_report(holdings, prices): lines = [] lines.append("="*65) lines.append(f"📊 每日投资报告") lines.append(f"更新时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}") lines.append("="*65) lines.append("") lines.append("【持仓盈亏】") lines.append(f"{'代码':<8} {'股数':<8} {'成本':<10} {'当前价':<10} {'市值':<12} {'盈亏':<15}") lines.append("-"*60) total_cost = 0 total_value = 0 for code, info in holdings.items(): shares = info["shares"] cost = info["cost"] price = prices.get(code, 0) value = round(shares * price, 2) if price else 0 pnl = round(value - cost, 2) pnl_pct = round((pnl / cost * 100), 2) if cost > 0 else 0 total_cost += cost total_value += value emoji = "🟢" if pnl >= 0 else "🔴" lines.append(f"{code:<8} {shares:<8.2f} ${cost:<9.2f} ${price:<9.2f} ${value:<11.2f} {emoji}${pnl:+.2f} ({pnl_pct:+.2f}%)") lines.append("-"*60) total_pnl = round(total_value - total_cost, 2) total_pnl_pct = round((total_pnl / total_cost * 100), 2) if total_cost > 0 else 0 total_emoji = "🟢" if total_pnl >= 0 else "🔴" lines.append(f"{'合计':<8} {'':<8} ${total_cost:<9.2f} {'':<10} ${total_value:<11.2f} {total_emoji}${total_pnl:+.2f} ({total_pnl_pct:+.2f}%)") lines.append("") lines.append("="*65) lines.append("📍 数据来源: Yahoo Finance (yfinance)") lines.append("="*65) return "\n".join(lines) def main(): print(f"\n{'='*65}") print("📊 每日股票更新") print(f"时间: {datetime.now()}") print(f"{'='*65}") holdings = get_user_holdings() if not holdings: print("无法获取用户数据") return print("\n[1] 获取股价 (Yahoo Finance)...") prices = {} for code in holdings.keys(): price, source = get_price(code) if price: prices[code] = price print(f" {code}: ${price} ({source})") else: print(f" {code}: 获取失败") print("\n[2] 更新 OpenClaw 表...") auto_pages = get_auto_pages() updated = update_auto_table(holdings, prices, auto_pages) print(f" 更新 {updated} 条") report = generate_report(holdings, prices) print(f"\n{report}") with open("/root/.openclaw/workspace/stock_daily_report.txt", "w") as f: f.write(report) print("\n完成!") if __name__ == "__main__": main()