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

198 lines
6.7 KiB
Python
Executable File

#!/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()