fix: 全面系统修复 — delivery目标修正 + 脚本同步 + refresh_mtf_cache import修复 + clean_watchlist硬编码修复
修复清单: - cron delivery修正:10个job从deliver=origin/all改为local,消除推送错误 - refresh_mtf_cache.py:替换strategy_lifecycle.PORTFOLIO_PATH导入为mofin_db直接读DB - clean_watchlist.py:修复mo_data导入名错误和JSON硬编码路径 - MoFin/scripts/与profile scripts互相补全,双向sync到一致 - price_monitor.py:现金源从portfolio_summary表改为cash_log(Dad确认的现金权威) - 更新watchlist.json加入000850华茂股份
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user