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
+5 -7
View File
@@ -89,13 +89,11 @@ def main():
dec["decisions"].append(stub)
changes.append(f" {stub['name']}({code}): decisions新增持仓({h['shares']}股,来自portfolio)")
# 3. Recalculate total_assets in portfolio
stock_value = 0
for h in pf.get("holdings", []):
if h.get("shares", 0) > 0 and h.get("price", 0) > 0:
stock_value += h["shares"] * h["price"]
cash = pf.get("cash", 0)
total_assets = round(stock_value + cash, 2)
# 3. Recalculate total_assets in portfolio (use mo_models for unified formula)
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mo_models import calc_total_assets
total_assets = calc_total_assets(pf)
dec_total = 0
for d in dec.get("decisions", []):
if d.get("shares", 0) > 0 and d.get("price", 0) > 0:
+3 -2
View File
@@ -88,9 +88,10 @@ def main():
print(" holding文件不含现金行,必须手动提供。可以用:")
print(f" python3 import_holding_xls.py --cash 73758.0")
# Use provided values or calculate
# Use provided values or calculate (unified formula includes frozen_cash)
if total_assets <= 0:
total_assets = total_mv_cny + cash
frozen_cash = float(args.get('frozen', pf.get('frozen_cash', 0)) or 0)
total_assets = total_mv_cny + cash + frozen_cash
if market_value <= 0:
market_value = round(total_mv_cny, 2)
+25 -17
View File
@@ -20,6 +20,26 @@ import threading
import time
from datetime import datetime, time
# ── MoFin unified model import ──────────────────────────────────────
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
from mo_models import is_hk_stock, get_hk_rate, to_cny, calc_total_assets
_USE_MO_MODELS = True
except ImportError:
_USE_MO_MODELS = False
def is_hk_stock(code):
code = str(code or '').strip().upper()
return len(code) == 5 and code.isdigit() and code[0] in ('0', '1')
def get_hk_rate():
return 0.87
def to_cny(price, code):
if price is None: return price
if is_hk_stock(code): return round(float(price) * get_hk_rate(), 2)
return price
def calc_total_assets(pf):
total_mv = sum((h.get('shares',0) or 0) * (h.get('price',0) or 0) for h in pf.get('holdings',[]))
return round(total_mv + (pf.get('cash',0) or 0) + (pf.get('frozen_cash',0) or 0), 2)
# 市场时段检查
_MARKET_HOURS = {
'ashare': (time(9, 30), time(15, 0)),
@@ -246,14 +266,9 @@ def hk_lot_size(code):
def lot_cost(code, price):
if str(code).startswith("688"):
return 200 * price
elif len(str(code)) == 5:
elif is_hk_stock(code):
lot = hk_lot_size(code)
try:
sys.path.insert(0, '/home/hmo/MoFin')
from hk_rate import hkd_to_cny
rate = hkd_to_cny()
except Exception:
rate = 0.87
rate = get_hk_rate()
return int(lot * price * rate)
else:
return 100 * price
@@ -487,13 +502,8 @@ def main():
# 直接取 portfolio.json 的总资产(导入时已做港币→人民币换算)
total_assets = pf.get("total_assets", 0) or 0
if total_assets <= 0:
# fallback: 手动算
for h in pf.get("holdings", []):
mv = h.get("shares", 0) * h.get("price", 0)
if len(str(h.get("code", ""))) <= 5: # 港股
mv *= 0.866
total_assets += mv
total_assets += available_cash
# fallback: use unified calc_total_assets from mo_models
total_assets = calc_total_assets(pf)
except Exception:
total_assets = available_cash * 5 # fallback
@@ -599,9 +609,7 @@ def main():
if hs <= 0 or hp <= 0:
continue
hmv = hs * hp
h_code = str(h.get("code", ""))
if len(h_code) <= 5:
hmv *= 0.866 # approximate HKD→CNY
# 港股价格已是 CNYprice_monitor 写入时已转),不需要再乘汇率
hpl_pct = (hp - hc) / hc * 100 if hc else 0
# 6维全面评分(越低越差,越建议卖)
+3 -8
View File
@@ -136,14 +136,9 @@ def rank_by_outlook(holdings_list, decisions_data):
return results
def is_hk_stock(code):
"""判断是否为港股(港股通标的代码通常5位)"""
return len(str(code)) <= 5
def is_a_stock(code):
"""判断是否为A股(6位代码)"""
return len(str(code)) == 6
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from mo_models import is_hk_stock, is_a_stock
def settlement_delay_note(sell_code, buy_code):