diff --git a/price_monitor.py b/price_monitor.py index bad4f11..1dfa554 100644 --- a/price_monitor.py +++ b/price_monitor.py @@ -250,7 +250,7 @@ def refresh_data_prices(): if s['code'] in prices: price, _, change_pct = prices[s['code']] if price > 0: - # 港股API返回HKD,直接存HKD原值。港股标价存HKD,A股标价存CNY + # 港股存 HKD 原值(跟股软一致),总资产汇总时由 calc_total_assets 统一转 CNY old = s.get('price') or 0 if abs(old - price) > 0.001: s['price'] = round(price, 2) @@ -299,11 +299,11 @@ def refresh_data_prices(): if s['code'] in prices: price, _, change_pct = prices[s['code']] if price > 0: + # 港股存 HKD 原值(跟股软一致) old = s.get('price') or 0 if abs(old - price) > 0.001: s['price'] = round(price, 2) s['change_pct'] = float(change_pct) if change_pct else 0 - s['currency'] = 'HKD' if is_hk_stock(s['code']) else 'CNY' updated += 1 changed = True if changed: diff --git a/scripts/close_audit.py b/scripts/close_audit.py index 1d757e5..375f9b9 100644 --- a/scripts/close_audit.py +++ b/scripts/close_audit.py @@ -33,8 +33,10 @@ for h in pf.get('holdings', []): is_hk = is_hk_stock(code) flag = 'HK' if is_hk else 'A ' - # 检查币种 - if is_hk and curr != 'CNY': + # 检查币种:港股应为 HKD,A股应为 CNY + if is_hk and curr != 'HKD': + hk_cny_issues.append(f"{code} {name}: currency={curr} (应为HKD)") + if not is_hk and curr != 'CNY': hk_cny_issues.append(f"{code} {name}: currency={curr} (应为CNY)") # 检查 cost=0 的股票 @@ -55,8 +57,11 @@ print(f" stored cash: {pf.get('cash')}") print(f" stored frozen_cash: {pf.get('frozen_cash')}") print(f" stored total_pnl: {pf.get('total_pnl')}") -calc_ta = total_mv + (pf.get('cash') or 0) + (pf.get('frozen_cash') or 0) -print(f" calculated total_assets: {calc_ta:.2f}") +# 用 mo_models 计算(已处理 HKD→CNY) +calc_ta = calc_total_assets(pf) +calc_mv = calc_total_mv(pf.get('holdings', [])) +print(f" calculated total_mv (CNY): {calc_mv:.2f}") +print(f" calculated total_assets (CNY): {calc_ta:.2f}") if abs((pf.get('total_assets') or 0) - calc_ta) > 1: errors.append(f"total_assets mismatch: stored={pf.get('total_assets')} vs calc={calc_ta:.2f}") @@ -68,12 +73,15 @@ hkds = 0 none_curr = 0 for d in dec.get('decisions', []): c = d.get('currency', '') + code = d.get('code', '') if c == 'CNY': cnys += 1 elif c == 'HKD': hkds += 1 else: none_curr += 1 -print(f" CNY: {cnys} HKD: {hkds} None: {none_curr}") -if hkds > 0: - errors.append(f"Decisions: {hkds} entries still have currency=HKD") + # 港股应标 HKD,A 股应标 CNY + if is_hk_stock(str(code)) and c != 'HKD': + errors.append(f"决策 {code} currency={c} (应为HKD)") + elif not is_hk_stock(str(code)) and c != 'CNY': + errors.append(f"决策 {code} currency={c} (应为CNY)") # ── 4. 错误汇总 ── print(f"\n{'='*40}") diff --git a/scripts/rollback_currency.py b/scripts/rollback_currency.py new file mode 100644 index 0000000..e95bff2 --- /dev/null +++ b/scripts/rollback_currency.py @@ -0,0 +1,68 @@ +"""回滚:港股 holdings/decisions cost 从 CNY 转回 HKD,currency=HKD""" +import sqlite3, sys +sys.path.insert(0, '/home/hmo/MoFin') +from mo_models import is_hk_stock, get_hk_rate, calc_total_assets, calc_total_mv, calc_position_pct +from datetime import datetime + +rate = get_hk_rate() +print(f"HK_RATE: {rate}") + +db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') + +# ── 1. holdings: 港股 currency=HKD, cost 转回 HKD ── +rows = db.execute("SELECT code, name, cost, price, shares, currency FROM holdings WHERE is_active=1").fetchall() +fixed_cost = 0 +for r in rows: + code, name, cost, price, shares, curr = r + if is_hk_stock(str(code)): + # 成本从 CNY 转回 HKD: cost_cny / rate + cost_hkd = round(((cost or 0)) / rate, 2) if (cost or 0) else 0 + db.execute("UPDATE holdings SET cost=?, currency='HKD' WHERE code=?", (cost_hkd, code)) + print(f" HOLD {code} {name}: cost {cost or 0:.2f}CNY → {cost_hkd:.2f}HKD, curr={curr}→HKD") + fixed_cost += 1 + +# ── 2. holding_strategies: currency=HKD, cost/price/stop_loss/take_profit 转 HKD ── +rows = db.execute("SELECT code, name, cost, price, stop_loss, take_profit, currency FROM holding_strategies WHERE status IN ('active','updated')").fetchall() +fixed_strat = 0 +for r in rows: + code, name, cost, price, sl, tp, curr = r + if is_hk_stock(str(code)): + cost_hkd = round(((cost or 0)) / rate, 2) if (cost or 0) else 0 + price_hkd = round(((price or 0)) / rate, 2) if (price or 0) else 0 + sl_hkd = round(((sl or 0)) / rate, 2) if (sl or 0) else 0 + tp_hkd = round(((tp or 0)) / rate, 2) if (tp or 0) else 0 + db.execute("UPDATE holding_strategies SET cost=?, price=?, stop_loss=?, take_profit=?, currency='HKD' WHERE code=?", + (cost_hkd, price_hkd, sl_hkd, tp_hkd, code)) + print(f" STRAT {code} {name}: c {cost or 0}→{cost_hkd} p {price or 0}→{price_hkd} curr={curr}→HKD") + fixed_strat += 1 + +db.commit() + +# ── 3. portfolio_summary: 用 calc_total_assets 重算(已处理 HKD→CNY)── +rows = db.execute("SELECT code, name, shares, cost, price, currency FROM holdings WHERE is_active=1").fetchall() +holdings = [dict(zip(['code','name','shares','cost','price','currency'], r)) for r in rows] +sr = db.execute("SELECT cash, frozen_cash FROM portfolio_summary WHERE id=1").fetchone() +pf = {'holdings': holdings, 'cash': sr[0] or 0, 'frozen_cash': sr[1] or 0} + +mv_cny = calc_total_mv(holdings) +ta_cny = calc_total_assets(pf) +pp = calc_position_pct(pf) + +# P&L: 港股按 HKD 算个股 P&L,汇总时转 CNY +total_pnl = 0 +for h in holdings: + p = h['price'] or 0 + c = h['cost'] or 0 + s = h['shares'] or 0 + if is_hk_stock(str(h['code'])): + total_pnl += (p - c) * s * rate # HKD P&L → CNY + else: + total_pnl += (p - c) * s + +db.execute("UPDATE portfolio_summary SET total_mv=?, total_assets=?, total_pnl=?, position_pct=?, updated_at=? WHERE id=1", + (round(mv_cny,2), round(ta_cny,2), round(total_pnl,2), pp, datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) + +print(f"\nportfolio_summary: mv_cny={mv_cny:.2f} ta_cny={ta_cny:.2f} pnl_cny={total_pnl:.2f} pos={pp}%") +print(f"\nDone. Fixed {fixed_cost} holdings + {fixed_strat} strategies.") +db.commit() +db.close() diff --git a/strategy_lifecycle.py b/strategy_lifecycle.py index 81c839e..8883882 100644 --- a/strategy_lifecycle.py +++ b/strategy_lifecycle.py @@ -84,11 +84,11 @@ STRATEGY_QUALITY_GATES = [ "fix": "tech_snapshot 包含强撑/弱撑/弱压/强压至少3个数值" }, { - "id": "GATE_CURRENCY", - "severity": "CRITICAL", - "desc": "港股决策金额应为港币标价(currency=HKD),A股为CNY", - "check": lambda d: d.get("currency") in ("HKD", "CNY") or not d.get("code"), - "fix": "设置 d['currency']='HKD' for HK stocks" + "id": "GATE_CURRENCY_SET", + "desc": "港股必须标 currency=HKD(个股存原币种,汇总时由calc_total_assets转CNY)", + "check": lambda d: not is_hk_stock(d.get("code","")) or d.get("currency") == "HKD", + "severity": "HIGH", + "fix": "设置 d['currency']='HKD'" }, # --- 第4条 CRITICAL 红线:9维交叉验证 (2026-07-02 Dad要求) --- # 策略不能只有价格数字,必须有证据经过了多维分析: @@ -2293,10 +2293,10 @@ def regenerate_all(stdout=True): sector_ctx_str = f"大盘上涨比{market_breadth}%" new_entry = { "code": code, "name": name, "price": price, - "cost": old_entry.get("cost", cost) if old_entry else cost, - "shares": shares, - "avg_price": old_entry.get("avg_price", 0), - "currency": "HKD" if is_hk_stock(code) else "CNY", + "cost": old_entry.get("cost", cost) if old_entry else cost, # 优先保留旧成本(holding.xls权威) + "shares": shares, # 当前实际持仓股数(不继承旧决策的可能为0的值) + "avg_price": old_entry.get("avg_price", 0), # 保留持仓均价 + "currency": "HKD" if is_hk_stock(str(code)) else "CNY", "action": result["action"], "stop_loss": result.get("stop_loss"), "entry_low": result["entry_low"], @@ -2526,7 +2526,7 @@ def regenerate_all(stdout=True): write_holdings_batch(conn, existing_pf.get('holdings', [])) write_portfolio_summary(conn, existing_pf) for s in wl.get('stocks', []): - s.setdefault('currency', 'HKD') if is_hk_stock(str(s.get('code',''))) else s.setdefault('currency', 'CNY') + s.setdefault('currency', 'CNY') write_watchlist_stock(conn, s) for d in decisions: # ── 策略质量门禁 ──