#!/usr/bin/env python3 """ fix_portfolio_prices.py — 一次性修复脚本:从 decisions.json 读取 price 字段 更新两个 canonical portfolio.json,并重算 total_assets/position_pct。 背景:strategy_lifecycle.regenerate_all() 在旧版本中会 从 DB query_holdings()(不含 price/change_pct)覆盖写入 portfolio.json,导致 price_monitor 维护的实时价丢失。 该 bug 已在 strategy_lifecycle.py:1790 修复(保留 price 字段), 此脚本用于修复已损坏的 portfolio.json。 用法: python3 fix_portfolio_prices.py # 修复两个 canonical 文件 python3 fix_portfolio_prices.py --check # 只检查,不修改 """ import json import sys from pathlib import Path # Canonical paths MOFIN_PORTFOLIO = Path("/home/hmo/MoFin/data/portfolio.json") DASHBOARD_PORTFOLIO = Path("/home/hmo/web-dashboard/data/portfolio.json") DECISIONS_PATH = Path("/home/hmo/web-dashboard/data/decisions.json") def load_decisions(): """从 decisions.json 读取 {code: {price, ...}} 映射""" try: with open(DECISIONS_PATH) as f: raw = json.load(f) except Exception as e: print(f"❌ 无法读取 {DECISIONS_PATH}: {e}", file=sys.stderr) return {} price_map = {} for d in raw.get("decisions", []): code = d.get("code", "") price = d.get("price", 0) or d.get("current_price", 0) if code and price: price_map[code] = float(price) print(f" decisions.json: {len(price_map)} 个股票的 price 已加载") return price_map def fix_portfolio(path: Path, price_map: dict, check_only: bool): """修复一个 portfolio.json 文件""" if not path.exists(): print(f"❌ {path} 不存在,跳过") return False try: with open(path) as f: pf = json.load(f) except Exception as e: print(f"❌ 无法读取 {path}: {e}", file=sys.stderr) return False changes = 0 errors = 0 holdings = pf.get("holdings", []) for h in holdings: code = h.get("code", "") if not code: continue old_price = h.get("price", 0) decision_price = price_map.get(code) if decision_price and (not old_price or old_price == 0): h["price"] = decision_price changes += 1 print(f" ✅ {code} {h.get('name','')}: price {old_price} → {decision_price}") elif decision_price and old_price and abs(decision_price - old_price) / max(abs(old_price), 1) > 0.05: # Price differs >5% from decision - flag it but don't overwrite (price_monitor is authoritative) print(f" ⚠️ {code} {h.get('name','')}: 当前价 {old_price} vs decisions {decision_price} (偏离>{5:.0f}%),保留当前价") elif not decision_price: errors += 1 if old_price == 0 or not old_price: print(f" ❌ {code} {h.get('name','')}: decisions.json 无此股 price 数据,当前 price={old_price}") # 重算 total_assets / position_pct total_mv = 0 for h in holdings: price = h.get("price", 0) or 0 shares = h.get("shares", 0) or 0 total_mv += price * shares cash = pf.get("cash", 0) or 0 new_total_assets = round(total_mv + cash, 2) new_position_pct = round(total_mv / new_total_assets * 100, 2) if new_total_assets > 0 else 0 old_total_assets = pf.get("total_assets", 0) old_position_pct = pf.get("position_pct", 0) if abs(new_total_assets - old_total_assets) > 100: print(f" 📊 total_assets: {old_total_assets} → {new_total_assets} (变动 {new_total_assets-old_total_assets:.0f})") pf["total_assets"] = new_total_assets changes += 1 if abs(new_position_pct - old_position_pct) > 0.5: print(f" 📊 position_pct: {old_position_pct}% → {new_position_pct}%") pf["position_pct"] = new_position_pct changes += 1 pf["updated_at"] = __import__("datetime").datetime.now().strftime('%Y-%m-%d %H:%M') if changes == 0: print(f" ✅ {path.name}: 无需修复({len(holdings)} 个持仓价格正常)") return True if check_only: print(f" ⏸️ 检查模式: {changes} 处需修复,未写入") return True try: with open(path, "w") as f: json.dump(pf, f, indent=2, ensure_ascii=False) print(f" ✅ {path.name}: {changes} 处已修复,已写入") return True except Exception as e: print(f" ❌ 写入失败: {e}", file=sys.stderr) return False def main(): check_only = "--check" in sys.argv print("=== fix_portfolio_prices.py ===") if check_only: print("模式:检查(不写入)") else: print("模式:修复") price_map = load_decisions() if not price_map: print("❌ decisions.json 无有效 price 数据,退出") return 1 print(f"\n--- {MOFIN_PORTFOLIO.name} ---") ok1 = fix_portfolio(MOFIN_PORTFOLIO, price_map, check_only) print(f"\n--- {DASHBOARD_PORTFOLIO.name} ---") ok2 = fix_portfolio(DASHBOARD_PORTFOLIO, price_map, check_only) if ok1 and ok2: print("\n✅ 全部完成") return 0 else: print("\n⚠️ 部分失败") return 1 if __name__ == "__main__": sys.exit(main())