refactor: phase 0-2 MoFin architecture reform — single source of truth

Phase 0 (止血):
- mo_models.py: unified calc_total_assets(), is_hk_stock(), get_hk_rate() — single source of truth
- Fixed 3 files missing frozen_cash: holdings_reconciliation, server, import_holding_xls
- Fixed stale_push_wlin: unified is_hk_stock detection, removed hardcoded 0.866
- Fixed price_monitor: consolidated 2 duplicate total_assets blocks into mo_models calls
- Fixed stock_scorer: replaced broken len()<=5 is_hk_stock heuristic
- Fixed strategy_lifecycle: replaced non-existent currency_utils import with mo_models

Phase 1 (DSA adapter):
- mo_provider.py: wraps DSA DataFetcherManager (16 fetchers, auto-fallback)
  - TDX relay as primary, DSA as backup for realtime/kline/news/fundamentals

Phase 2 (Integration):
- mo_bridge.py: injects DSA market review + news context into MoFin analysis prompts
- Graceful degradation if DSA not installed

Infrastructure:
- mo_config.py: centralized Config singleton replacing scattered hardcoded paths
- All 11 changed files pass python compile check

Impact: total_assets now computed in ONE place (mo_models).
        is_hk_stock now ONE implementation (no more false negatives).
        HK rate now ONE source (hk_rate API → cache → 0.87 fallback).
        No more hardcoded 0.866/0.8664/0.8700 divergence.
This commit is contained in:
hmo
2026-06-29 23:25:54 +08:00
parent 7d49470aeb
commit 6abc2e45b0
11 changed files with 954 additions and 69 deletions
+18 -33
View File
@@ -10,6 +10,9 @@ import sys
import time
from datetime import datetime
# ── MoFin unified model ──────────────────────────────────────────────
from mo_models import is_hk_stock, get_hk_rate, calc_total_assets, calc_total_mv, calc_position_pct
DECISIONS_PATH = "/home/hmo/web-dashboard/data/decisions.json"
PORTFOLIO_PATH = "/home/hmo/web-dashboard/data/portfolio.json"
WATCHLIST_PATH = "/home/hmo/web-dashboard/data/watchlist.json"
@@ -26,10 +29,9 @@ except ImportError:
HAS_REASSESS = False
try:
from hk_rate import hkd_to_cny
HK_RATE = hkd_to_cny()
HK_RATE = get_hk_rate()
except Exception:
HK_RATE = 0.8700 # fallback
HK_RATE = 0.87 # ultimate fallback
# 分支系统与情景检测
try:
@@ -152,8 +154,8 @@ def refresh_data_prices():
if s['code'] in prices:
price, _, change_pct = prices[s['code']]
if price > 0:
# 港股:API返回HKD,需转RMB2026-06-23 bugfix
if str(s['code']).startswith(('0','1')) and len(str(s['code']))==5:
# 港股:API返回HKD,需转RMB
if is_hk_stock(s['code']):
price = round(price * HK_RATE, 2)
old = s.get('price', 0)
if abs(old - price) > 0.001:
@@ -163,16 +165,10 @@ def refresh_data_prices():
changed = True
if changed:
pf['updated_at'] = datetime.now().strftime('%Y-%m-%d %H:%M')
# 统一计算总资产:持仓市值 + 现金(所有港股价已×HK_RATE转CNY
pf['total_mv'] = round(sum(
h.get('shares',0) * h.get('price',0) for h in pf.get('holdings',[])
), 2)
# total_assets = 持仓市值 + 可用现金 + 冻结资金(缺一不可!2026-06-29 bugfix
# cash = 可用资金(从截图/导入/成交记录来的,price_monitor不动它)
# frozen_cash = 冻结资金(T+2未交收/挂单占用)
available = float(pf.get('cash', 0) or 0)
frozen = float(pf.get('frozen_cash', 0) or 0)
pf['total_assets'] = round(pf['total_mv'] + available + frozen, 2)
# 统一计算总资产mo_models 唯一公式
pf['total_mv'] = calc_total_mv(pf.get('holdings', []))
pf['total_assets'] = calc_total_assets(pf)
pf['position_pct'] = calc_position_pct(pf)
json.dump(pf, open(PORTFOLIO_PATH, 'w'), ensure_ascii=False, indent=2)
elif pf.get('updated_at'):
# 即使价格无变化,每10分钟刷新一次updated_at,防健康检查误报
@@ -190,8 +186,8 @@ def refresh_data_prices():
if s['code'] in prices:
price, _, change_pct = prices[s['code']]
if price > 0:
# 港股:API返回HKD,需转RMB2026-06-23 bugfix
if str(s['code']).startswith(('0','1')) and len(str(s['code']))==5:
# 港股:API返回HKD,需转RMB
if is_hk_stock(s['code']):
price = round(price * HK_RATE, 2)
old = s.get('price', 0)
if abs(old - price) > 0.001:
@@ -203,28 +199,17 @@ def refresh_data_prices():
wl['updated_at'] = datetime.now().isoformat()
json.dump(wl, open(WATCHLIST_PATH, 'w'), ensure_ascii=False, indent=2)
# --- 汇总值重算(2026-06-29 bugfix: 之前price_monitor只更新个股价,不更新汇总---
# --- 汇总值重算(使用 mo_models 唯一公式---
try:
live_market_value = sum(
h.get('shares', 0) * h.get('price', 0)
for h in pf.get('holdings', [])
)
# 现金:绝不重算。保留上次的值(来自截图/导入/手动修改)。
# 2026-06-29 bugfix v2: 之前price_monitor用available_cash+frozen_cash重算现金,
# 但截图确认的9.2万被旧冻结数据(3.9万)覆盖=113k,导致cash来回跳
# 修正:price_monitor只更新market_value,不碰cash
live_market_value = calc_total_mv(pf.get('holdings', []))
old_mv = pf.get('total_mv', 0)
if abs(old_mv - live_market_value) > 0.01:
pf['total_mv'] = round(live_market_value, 2)
# total_assets = 持仓市值 + 可用现金 + 冻结资金(重复!同步上一处公式)
available = float(pf.get('cash', 0) or 0)
frozen = float(pf.get('frozen_cash', 0) or 0)
pf['total_assets'] = round(live_market_value + available + frozen, 2)
pf['total_assets'] = calc_total_assets(pf)
if pf['total_assets'] > 0:
pf['position_pct'] = round(live_market_value / pf['total_assets'] * 100, 2)
pf['position_pct'] = calc_position_pct(pf)
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: