Files
MoFin/scripts/prepare_report_data.py
T

140 lines
4.9 KiB
Python

#!/usr/bin/env python3
"""
prepare_report_data.py — 为所有报告模板提供结构化参数值
输出JSON,每个字段有来源标注,LLM只读不修改
用法: python3 prepare_report_data.py [--report-type intraday|strategy|self-buy]
"""
import sqlite3, json, sys, subprocess, re, os
from pathlib import Path
from datetime import datetime
DB = Path("/home/hmo/MoFin/data/mofin.db")
HK_RATE_SCRIPT = Path("/home/hmo/MoFin/scripts/hk_rate.py")
def get_hk_rate() -> float:
r = subprocess.run(["python3", str(HK_RATE_SCRIPT)], capture_output=True, text=True, timeout=10)
m = re.search(r"[\d.]+$", r.stdout.strip())
return float(m.group()) if m else 0.93
def get_portfolio(db, hk_rate):
"""总资产/现金/仓位 — 来源: portfolio_summary + holdings实时计算"""
r = db.execute("SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1").fetchone()
cash, frozen = (r["cash"] or 0), (r["frozen_cash"] or 0)
total_hkd_mv = 0.0
total_cny_mv = 0.0
holdings = []
for r in db.execute("""
SELECT code, name, shares, price, currency, market_value, cost
FROM holdings WHERE is_active=1 AND (shares>0 OR shares IS NULL)
ORDER BY currency, code
"""):
shares = r["shares"] or 0
if shares == 0:
continue
cur = r["currency"] or "CNY"
price = r["price"] or 0
mv = r["market_value"] or (shares * price)
cost_total = (r["cost"] or 0) * shares
pnl = mv - cost_total
pnl_pct = ((price / (r["cost"] or price)) - 1) * 100 if r["cost"] and r["cost"] > 0 else 0
mv_cny = mv * hk_rate if cur == "HKD" else mv
if cur == "HKD":
total_hkd_mv += mv
else:
total_cny_mv += mv_cny
holdings.append({
"code": r["code"],
"name": r["name"],
"shares": shares,
"price": round(price, 2),
"currency": cur,
"price_display": f"HK${price:.2f}" if cur == "HKD" else f"CNY${price:.2f}",
"mv": round(mv, 2),
"mv_display": f"HK${mv:.2f}" if cur == "HKD" else f"CNY${mv:.2f}",
"pnl_pct": round(pnl_pct, 2),
"pnl_amount": round(pnl, 2),
})
stock_value_cny = total_cny_mv + total_hkd_mv * hk_rate
total_cash = cash + frozen
total_assets = stock_value_cny + total_cash
position_pct = (stock_value_cny / total_assets * 100) if total_assets > 0 else 0
return {
"cash": round(cash, 0),
"frozen_cash": round(frozen, 0),
"total_cash": round(total_cash, 0),
"stock_value_cny": round(stock_value_cny, 2),
"total_assets": round(total_assets, 2),
"position_pct": round(position_pct, 1),
"currency": "CNY",
"hk_rate": hk_rate,
"holdings": holdings,
"holdings_count": len(holdings),
"source": "portfolio_summary + holdings实时计算",
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
def get_market(db):
"""大盘数据 — 来源: market_snapshots"""
r = db.execute("SELECT * FROM market_snapshots ORDER BY id DESC LIMIT 1").fetchone()
if not r:
return {"error": "无市场数据"}
d = dict(r)
return {
"sh_index": d.get("sh_index", 0),
"sz_index": d.get("sz_index", 0),
"sh_change": d.get("sh_change_pct", 0),
"sz_change": d.get("sz_change_pct", 0),
"turnover": d.get("turnover", 0),
"advance_decline_ratio": d.get("advance_decline_ratio", 0),
"mood": d.get("mood", "unknown"),
"snapshot_time": d.get("snapshot_time", ""),
"source": "market_snapshots DB",
}
def get_cash_history(db):
"""现金变动历史 — 来源: cash_log"""
logs = []
for r in db.execute("SELECT * FROM cash_log ORDER BY id DESC LIMIT 5"):
logs.append({
"id": r["id"],
"timestamp": r["timestamp"],
"cash": r["cash_after"],
"change": r["cash_after"] - r["cash_before"],
"source": r["source"],
"note": r["note"],
"verified": bool(r["verified"]),
})
return logs
def main():
report_type = "intraday"
if len(sys.argv) > 1 and sys.argv[1].startswith("--"):
report_type = sys.argv[1].split("=")[-1] if "=" in sys.argv[1] else sys.argv[1].lstrip("-")
db = sqlite3.connect(str(DB))
db.row_factory = sqlite3.Row
hk_rate = get_hk_rate()
data = {
"_meta": {
"report_type": report_type,
"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"data_integrity": "所有数值由代码从DB获取,LLM不得修改",
},
"portfolio": get_portfolio(db, hk_rate),
"market": get_market(db),
"cash_history": get_cash_history(db),
}
print(json.dumps(data, ensure_ascii=False, indent=2))
db.close()
if __name__ == "__main__":
main()