#!/usr/bin/env python3 """holdings_reconciliation.py — 每日持仓数据一致性校验 在 decisions.json 和 portfolio.json 之间做双向核对: 1. 股数不一致 → 以 portfolio.json 为准(券商导入为源头真理) 2. 股票存在一个文件但不存在另一个 → 同步到双方一致 3. 总资产重新计算并写入双方 24小时内禁止修改策略参数(止盈/止损/买入区),只修股数和总资产。 """ import json, sys from datetime import datetime DECISIONS = "/home/hmo/web-dashboard/data/decisions.json" PORTFOLIO = "/home/hmo/web-dashboard/data/portfolio.json" def main(): dec = json.load(open(DECISIONS)) pf = json.load(open(PORTFOLIO)) # Build maps dmap = {d["code"]: d for d in dec.get("decisions", [])} pmap = {h["code"]: h for h in pf.get("holdings", []) if h.get("shares", 0) > 0} changes = [] # 1. Remove from decisions if not in portfolio (ghost holdings) for code in list(dmap.keys()): d = dmap[code] in_portfolio = code in pmap if not in_portfolio: if d.get("shares", 0) > 0: old = d["shares"] d["shares"] = 0 d["type"] = "自选策略" d.setdefault("changelog", []).append({ "time": datetime.now().strftime("%Y-%m-%d %H:%M"), "from": old, "to": 0, "reason": "reconciliation: 不在券商持仓" }) changes.append(f" {d.get('name','')}({code}): 清仓{old}→0股(不在portfolio)") continue # Same stock in both: sync share count (portfolio is source of truth) p_shares = pmap[code]["shares"] if d.get("shares", 0) != p_shares: old = d.get("shares", 0) d["shares"] = p_shares d["type"] = "持仓策略" d.setdefault("changelog", []).append({ "time": datetime.now().strftime("%Y-%m-%d %H:%M"), "from": old, "to": p_shares, "reason": "reconciliation: 股数与券商一致" }) changes.append(f" {d.get('name','')}({code}): 股数{old}→{p_shares}(对齐portfolio)") # 2. Add to decisions if in portfolio but not in decisions for code in pmap: h = pmap[code] if code not in dmap: # Stock is in portfolio but not in decisions → add stub stub = { "code": code, "name": h.get("name", f"STOCK_{code}"), "shares": h["shares"], "price": h.get("price", 0), "stop_loss": 0, "take_profit": 0, "entry_low": 0, "entry_high": 0, "cost": h.get("cost", 0), "type": "持仓策略", "status": "active", "timing_signal": "持有", "action": "持仓策略 | 等待技术分析完善", "tech_snapshot": "", "action_note": "reconciliation: 自动补充", "reassessed_at": datetime.now().strftime("%Y-%m-%d %H:%M"), "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M"), "changelog": [{ "time": datetime.now().strftime("%Y-%m-%d %H:%M"), "reason": "reconciliation: 券商持仓→自动补充策略" }], "trigger": {}, "analysis": {}, "currency": "CNY" } dec["decisions"].append(stub) changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)") # 3. Recalculate total_assets in portfolio (use mo_models for unified formula) import sys, os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mo_models import calc_total_assets total_assets = calc_total_assets(pf) dec_total = 0 for d in dec.get("decisions", []): if d.get("shares", 0) > 0 and d.get("price", 0) > 0: dec_total += d["shares"] * d["price"] old_total = pf.get("total_assets", 0) pf["total_assets"] = total_assets pf["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M") # 4. Report now = datetime.now().strftime("%Y-%m-%d %H:%M") print(f"【持仓一致性校验】{now}") print(f"") if changes: print(f"修正项 ({len(changes)}):") for c in changes: print(c) else: print("无差异,全部一致 ✅") print(f"") print(f"portfolio stock_value: {stock_value:.2f}") print(f"portfolio cash: {cash:.2f}") print(f"portfolio total_assets: {old_total} → {total_assets}") print(f"decisions stock_value: {dec_total:.2f}") print(f"decisions count(shares>0): {len([d for d in dec['decisions'] if d.get('shares',0)>0])}") # Write — DB 优先(强制币种约束),JSON 冷备 dec["total"] = len(dec["decisions"]) try: sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy conn = get_conn() write_holdings_batch(conn, pf.get('holdings', [])) write_portfolio_summary(conn, pf) for d in dec.get('decisions', []): write_holding_strategy(conn, d.get('code', ''), d.get('name', ''), d) conn.close() except Exception as e: print(f" [DB写入失败] {e}") json.dump(dec, open(DECISIONS, "w"), ensure_ascii=False, indent=2) json.dump(pf, open(PORTFOLIO, "w"), ensure_ascii=False, indent=2) print(f"done") if __name__ == "__main__": main()