price_monitor 汇总值重算 + total_assets修正
问题:price_monitor每2分钟更新个股价,但不更新 total_market_value/total_assets/cash/position_pct, 这些字段停留在import_holding_xls快照值,已严重过期。 导致报告显示错误的总资产和仓位。 修复: - 每次更新个股价后,实时重算 total_market_value = sum(shares*price) - cash 从 stale_report(Dad截图确认的可用现金)同步 - total_assets = market_value + available_cash + freeze - 避免价格无变化时不触发更新(timeout fallback保留)
This commit is contained in:
@@ -193,6 +193,40 @@ def refresh_data_prices():
|
|||||||
wl['updated_at'] = datetime.now().isoformat()
|
wl['updated_at'] = datetime.now().isoformat()
|
||||||
json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
|
json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# --- 汇总值重算(2026-06-29 bugfix: 之前price_monitor只更新个股价,不更新汇总)---
|
||||||
|
# total_market_value / total_assets / cash / position_pct 来自import_holding_xls快照
|
||||||
|
# 价格更新后必须同步刷新汇总,否则报告使用过期汇总 → 现金/资产/仓位全错
|
||||||
|
try:
|
||||||
|
live_market_value = sum(
|
||||||
|
h.get('shares', 0) * h.get('price', 0)
|
||||||
|
for h in pf.get('holdings', [])
|
||||||
|
)
|
||||||
|
# 从 stale_report 读可用现金(Dad截图确认值)
|
||||||
|
stale_cash = 0
|
||||||
|
try:
|
||||||
|
sr = json.load(open('/home/hmo/web-dashboard/data/strategy_staleness_report.json'))
|
||||||
|
fallback_cash = pf.get('cash', 0) or 0
|
||||||
|
stale_cash = sr.get('portfolio', {}).get('cash', fallback_cash)
|
||||||
|
except:
|
||||||
|
stale_cash = pf.get('cash', 0) or 0
|
||||||
|
|
||||||
|
old_mv = pf.get('total_market_value', 0)
|
||||||
|
if abs(old_mv - live_market_value) > 0.01:
|
||||||
|
pf['total_market_value'] = round(live_market_value, 2)
|
||||||
|
|
||||||
|
old_cash = pf.get('cash', 0)
|
||||||
|
if abs(old_cash - stale_cash) > 0.01:
|
||||||
|
pf['cash'] = stale_cash
|
||||||
|
|
||||||
|
pf['total_assets'] = round(live_market_value + stale_cash, 2)
|
||||||
|
if pf['total_assets'] > 0:
|
||||||
|
pf['position_pct'] = round(live_market_value / pf['total_assets'] * 100, 2)
|
||||||
|
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
|
||||||
|
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f" [汇总重算失败] {e}", flush=True)
|
||||||
|
# --- 结束汇总重算 ---
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -423,12 +423,17 @@ def batch_fetch_prices(codes):
|
|||||||
|
|
||||||
|
|
||||||
def get_price_tencent(code):
|
def get_price_tencent(code):
|
||||||
"""获取实时价格,自动识别A股/港股"""
|
"""获取实时价格,港股转CNY统一存CNY"""
|
||||||
|
try:
|
||||||
|
from currency_utils import to_cny, is_hk_stock
|
||||||
|
except ImportError:
|
||||||
|
to_cny = lambda v, r=None: v
|
||||||
|
is_hk_stock = lambda c: len(str(c).strip()) == 5 and str(c).strip().isdigit()
|
||||||
try:
|
try:
|
||||||
raw_code = code.split('_')[0]
|
raw_code = code.split('_')[0]
|
||||||
if not raw_code:
|
if not raw_code:
|
||||||
return None
|
return None
|
||||||
if len(raw_code) == 5 and raw_code.isdigit():
|
if is_hk_stock(raw_code):
|
||||||
prefix = "hk"
|
prefix = "hk"
|
||||||
elif raw_code.startswith("6") or raw_code.startswith("5"):
|
elif raw_code.startswith("6") or raw_code.startswith("5"):
|
||||||
prefix = "sh"
|
prefix = "sh"
|
||||||
@@ -442,8 +447,11 @@ def get_price_tencent(code):
|
|||||||
return float(fields[i]) if fields[i].strip() else 0.0
|
return float(fields[i]) if fields[i].strip() else 0.0
|
||||||
except:
|
except:
|
||||||
return 0.0
|
return 0.0
|
||||||
|
price = f(3)
|
||||||
|
if is_hk_stock(raw_code) and price > 0:
|
||||||
|
price = to_cny(price)
|
||||||
return {
|
return {
|
||||||
"price": f(3), "close": f(4), "high": f(33), "low": f(34),
|
"price": price, "close": f(4), "high": f(33), "low": f(34),
|
||||||
"code": raw_code,
|
"code": raw_code,
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+14
-1
@@ -116,7 +116,20 @@ def audit_advice(conn):
|
|||||||
# ── 5. 组合健康 ──
|
# ── 5. 组合健康 ──
|
||||||
def audit_portfolio(conn):
|
def audit_portfolio(conn):
|
||||||
try:
|
try:
|
||||||
pos = conn.execute("SELECT SUM(position_pct) FROM holdings WHERE is_active=1").fetchone()[0] or 0
|
# 优先从 portfolio.json 读总仓位(更准确,基于实际市值/总资产)
|
||||||
|
pj_path = WEB_DATA / "portfolio.json"
|
||||||
|
if not pj_path.exists():
|
||||||
|
pj_path = DATA_DIR / "portfolio.json"
|
||||||
|
if pj_path.exists():
|
||||||
|
pj = json.loads(pj_path.read_text())
|
||||||
|
pos = pj.get("position_pct", 0)
|
||||||
|
cash = pj.get("cash", 0)
|
||||||
|
available = pj.get("available_cash", cash)
|
||||||
|
else:
|
||||||
|
# 兜底:SQLite position_pct 之和
|
||||||
|
pos = conn.execute("SELECT SUM(position_pct) FROM holdings WHERE is_active=1").fetchone()[0] or 0
|
||||||
|
available = 0
|
||||||
|
|
||||||
log_ok("组合", f"总仓位{pos:.1f}%")
|
log_ok("组合", f"总仓位{pos:.1f}%")
|
||||||
if pos > 90:
|
if pos > 90:
|
||||||
log_issue("组合", "MEDIUM", f"仓位{pos:.1f}%超过90%,现金紧张")
|
log_issue("组合", "MEDIUM", f"仓位{pos:.1f}%超过90%,现金紧张")
|
||||||
|
|||||||
Reference in New Issue
Block a user