- 移除重复的系统健康检查-每日(no_agent版,与LLM版重叠) - 归档42个一次性脚本(移出MoFin/scripts→archive/scripts): - fix_*: 历史数据修复(11个) - migrate_*/rollback_*: 数据迁移(2个) - 一次性数据修复: bulk/close/data_freshness等(16个) - check_*/verify_*/diagnose_*: 历史诊断工具(12个) - test_*: 测试脚本(3个) - 84个活跃脚本, 0架构违规, 31张healthy数据表
160 lines
5.1 KiB
Python
160 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
fix_portfolio_prices.py — 一次性修复脚本:从 decisions(DB) 读取 price 字段
|
|
更新 holdings 表,并重算 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 数据。
|
|
|
|
用法:
|
|
python3 fix_portfolio_prices.py # 修复 DB
|
|
python3 fix_portfolio_prices.py --check # 只检查,不修改
|
|
"""
|
|
|
|
import sys
|
|
|
|
from mo_data import read_decisions, read_portfolio
|
|
from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary
|
|
|
|
|
|
def build_price_map():
|
|
"""从 decisions(DB) 读取 {code: {price, ...}} 映射"""
|
|
try:
|
|
dec = read_decisions()
|
|
except Exception as e:
|
|
print(f"❌ 无法读取 decisions(DB): {e}", file=sys.stderr)
|
|
return {}
|
|
|
|
price_map = {}
|
|
for d in dec.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(DB): {len(price_map)} 个股票的 price 已加载")
|
|
return price_map
|
|
|
|
|
|
def fix_portfolio(check_only: bool) -> bool:
|
|
"""从 DB 读持仓,修复 price,写回 DB"""
|
|
try:
|
|
pf = read_portfolio()
|
|
except Exception as e:
|
|
print(f"❌ 无法读取 portfolio(DB): {e}", file=sys.stderr)
|
|
return False
|
|
|
|
holdings = pf.get("holdings", [])
|
|
price_map = build_price_map()
|
|
if not price_map:
|
|
print("❌ decisions 无有效 price 数据,退出")
|
|
return False
|
|
|
|
changes = 0
|
|
errors = 0
|
|
|
|
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} \u2192 {decision_price}")
|
|
elif decision_price and old_price and abs(decision_price - old_price) / max(abs(old_price), 1) > 0.05:
|
|
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 无此股 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
|
|
frozen = pf.get("frozen_cash", 0) or 0
|
|
new_total_assets = round(total_mv + cash + frozen, 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} \u2192 {new_total_assets} (变动 {new_total_assets-old_total_assets:.0f})")
|
|
changes += 1
|
|
|
|
if abs(new_position_pct - old_position_pct) > 0.5:
|
|
print(f" 📊 position_pct: {old_position_pct}% \u2192 {new_position_pct}%")
|
|
changes += 1
|
|
|
|
if changes == 0:
|
|
print(f" ✅ 无需修复({len(holdings)} 个持仓价格正常)")
|
|
return True
|
|
|
|
if check_only:
|
|
print(f" ⏸️ 检查模式: {changes} 处需修复,未写入")
|
|
return True
|
|
|
|
# 写入 DB
|
|
try:
|
|
conn = get_conn()
|
|
ok, msg = write_holdings_batch(conn, holdings)
|
|
if not ok:
|
|
print(f" ❌ 写入 holdings 失败: {msg}", file=sys.stderr)
|
|
conn.close()
|
|
return False
|
|
|
|
summary = {
|
|
"total_assets": new_total_assets,
|
|
"total_mv": round(total_mv, 2),
|
|
"stock_value": round(total_mv, 2),
|
|
"cash": cash,
|
|
"frozen_cash": frozen,
|
|
"position_pct": new_position_pct,
|
|
"currency": pf.get("currency", "CNY"),
|
|
}
|
|
ok, msg = write_portfolio_summary(conn, summary)
|
|
conn.close()
|
|
if not ok:
|
|
print(f" ❌ 写入 portfolio_summary 失败: {msg}", file=sys.stderr)
|
|
return False
|
|
|
|
print(f" ✅ DB: {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("模式:修复")
|
|
|
|
ok = fix_portfolio(check_only)
|
|
|
|
if ok:
|
|
print("\n✅ 全部完成")
|
|
return 0
|
|
else:
|
|
print("\n⚠️ 部分失败")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|