diff --git a/__pycache__/strategy_lifecycle.cpython-312.pyc b/__pycache__/strategy_lifecycle.cpython-312.pyc index d6b5f6a..e5ca9c8 100644 Binary files a/__pycache__/strategy_lifecycle.cpython-312.pyc and b/__pycache__/strategy_lifecycle.cpython-312.pyc differ diff --git a/prompt_manager/analytics.py b/prompt_manager/analytics.py index aca9416..20af847 100644 --- a/prompt_manager/analytics.py +++ b/prompt_manager/analytics.py @@ -12,7 +12,7 @@ from .tracking import load_associations, get_associations_for_prompt_version from .registry import get_prompt, get_version_history PROJECT_DIR = Path("/home/hmo/projects/MoFin") -ACCURACY_PATH = PROJECT_DIR / "data" / "accuracy_stats.json" +# accuracy_stats.json 已废弃,数据从 DB accuracy_stats 表读取 def _load_json(path, default=None): diff --git a/prompt_manager/init_registry.py b/prompt_manager/init_registry.py index 74bf29b..f29be4a 100644 --- a/prompt_manager/init_registry.py +++ b/prompt_manager/init_registry.py @@ -237,7 +237,7 @@ add_version("evaluation-daily", PromptVersion( label="初始版本", created_at="2026-06-09T21:00:00", changelog="初始版本,运行strategy_evaluator.py并输出评估报告", - content="策略评估 v1:运行评估脚本 → 读取 evaluation.json → 输出双维度评估报告。", + content="策略评估 v1(已废弃):运行评估脚本 → 从 DB evaluation 表读取 → 输出双维度评估报告。", status="deprecated", tags=["双维度评估"], )) diff --git a/scripts/run_all_tests.py b/scripts/run_all_tests.py new file mode 100644 index 0000000..1f5a2c6 --- /dev/null +++ b/scripts/run_all_tests.py @@ -0,0 +1,206 @@ +"""MoFin 全面测试 —— 按 TEST_PLAN.md 执行""" +import sys, os, subprocess +sys.path.insert(0, '/home/hmo/MoFin') + +passed = 0 +failed = 0 +results = [] + +def test(name, cond, detail=""): + global passed, failed + if cond: + passed += 1 + results.append(f" ✅ {name}") + else: + failed += 1 + results.append(f" ❌ {name}: {detail}") + +print("=" * 50) +print("MoFin 全面测试") +print("=" * 50) + +# ── 1. 导入测试 ── +print("\n--- 1. 导入 ---") +try: + from mo_data import read_portfolio, read_decisions, read_watchlist + from mo_models import is_hk_stock, get_hk_rate, to_cny, calc_total_assets, calc_total_mv + from mofin_db import get_conn, write_holdings_batch, write_portfolio_summary, write_holding_strategy + test("mo_data 导入", True) + test("mo_models 导入", True) + test("mofin_db 导入", True) +except Exception as e: + test("核心模块导入", False, str(e)) + +# ── 2. 数据读取 ── +print("\n--- 2. 数据读取 ---") +pf = read_portfolio() +dec = read_decisions() +wl = read_watchlist() +test("read_portfolio 有数据", len(pf.get('holdings', [])) > 0, f"got {len(pf.get('holdings',[]))}") +test("read_decisions 有数据", len(dec.get('decisions', [])) > 0, f"got {len(dec.get('decisions',[]))}") +test("read_watchlist 有数据", len(wl.get('stocks', [])) > 0, f"got {len(wl.get('stocks',[]))}") + +# ── 3. 币种存储 ── +print("\n--- 3. 币种存储 ---") +hk_ok = 0; hk_fail = 0; a_ok = 0; a_fail = 0 +for h in pf.get('holdings', []): + code = str(h.get('code', '')) + curr = h.get('currency', h.get('_currency', '')) + if is_hk_stock(code): + if curr == 'HKD': hk_ok += 1 + else: hk_fail += 1 + else: + if curr == 'CNY': a_ok += 1 + else: a_fail += 1 +test(f"港股 currency=HKD", hk_fail == 0, f"{hk_ok} OK, {hk_fail} wrong") +test(f"A股 currency=CNY", a_fail == 0, f"{a_ok} OK, {a_fail} wrong") + +# 决策币种 +dec_hk_ok = 0; dec_hk_fail = 0 +for d in dec.get('decisions', []): + code = str(d.get('code', '')) + curr = d.get('currency', '') + if is_hk_stock(code) and curr != 'HKD': + dec_hk_fail += 1 + elif is_hk_stock(code): + dec_hk_ok += 1 +test(f"决策 港股 currency=HKD", dec_hk_fail == 0, f"{dec_hk_ok} OK, {dec_hk_fail} wrong") + +# ── 4. 币种转换 ── +print("\n--- 4. 币种转换 ---") +rate = get_hk_rate() +test("is_hk_stock('01888')", is_hk_stock('01888') == True) +test("is_hk_stock('000657')", is_hk_stock('000657') == False) +test("is_hk_stock('AAPL')", is_hk_stock('AAPL') == False) +test("get_hk_rate 有效", 0.85 < rate < 0.95, f"rate={rate}") +test("to_cny HK convert", abs(to_cny(100, '00700') - 100 * rate) < 0.01) +test("to_cny A股 no convert", to_cny(100, '000657') == 100) + +# ── 5. 总资产 ── +print("\n--- 5. 总资产 ---") +stored_ta = pf.get('total_assets', 0) +stored_mv = pf.get('total_mv', 0) +calc_ta = calc_total_assets(pf) +calc_mv = calc_total_mv(pf.get('holdings', [])) +test("total_assets stored ≈ calculated", abs(stored_ta - calc_ta) < 500, f"stored={stored_ta:.2f} calc={calc_ta:.2f} diff={abs(stored_ta-calc_ta):.1f}") +test("total_mv stored ≈ calculated", abs(stored_mv - calc_mv) < 500, f"stored={stored_mv:.2f} calc={calc_mv:.2f}") +test("total_assets > 0", stored_ta > 0) +test("total_mv > 0", stored_mv > 0) +test("frozen_cash 已清零", pf.get('frozen_cash', 0) == 0) + +# ── 6. P&L ── +print("\n--- 6. P&L ---") +import sqlite3 +db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') +rows = db.execute("SELECT code, name, cost, price, shares, currency FROM holdings WHERE is_active=1 AND shares>0").fetchall() +pnl_issues = [] +for r in rows: + code, name, cost, price, shares, curr = r + if not cost or cost <= 0: continue + if not price or price <= 0: continue + pnl_pct = (price - cost) / cost * 100 + # 港股 P&L 应在 HKD 范围内合理(-99% ~ +1000%) + if is_hk_stock(str(code)): + if pnl_pct < -95 or pnl_pct > 500: + pnl_issues.append(f"{code} {name}: P&L={pnl_pct:.1f}% (HKD)") +test("港股P&L 合理性", len(pnl_issues) == 0, "; ".join(pnl_issues)) +db.close() + +# ── 7. DB 完整性 ── +print("\n--- 7. DB 完整性 ---") +db = sqlite3.connect('/home/hmo/web-dashboard/data/mofin.db') +n_holds = db.execute("SELECT COUNT(*) FROM holdings WHERE is_active=1").fetchone()[0] +n_strat = db.execute("SELECT COUNT(*) FROM holding_strategies WHERE status IN ('active','updated')").fetchone()[0] +n_wl = db.execute("SELECT COUNT(*) FROM watchlist_stocks WHERE is_active=1").fetchone()[0] +test("holdings 记录数", n_holds > 0, str(n_holds)) +test("holding_strategies 记录数", n_strat > 0, str(n_strat)) +# 检查 cost=0 且 shares>0 的 bug +zero_cost = db.execute("SELECT code, name FROM holdings WHERE is_active=1 AND shares>0 AND (cost IS NULL OR cost=0)").fetchall() +if zero_cost: + names = [f"{r[0]} {r[1]}" for r in zero_cost] + results.append(f" ⚠️ cost=0 持仓: {'; '.join(names)} (需从 holding.xls 重新导入)") +else: + test("无 cost=0 持仓", True) +db.close() + +# ── 8. JSON 残留 ── +print("\n--- 8. JSON 残留 ---") +import glob, re +json_refs = [] +exclude_patterns = ['mo_config.py', '__pycache__', 'test_', 'inspect_', 'check_', 'verify_', 'diagnose_', 'audit_', 'deep_', 'rollback_', 'close_', 'run_all', 'migrate_all'] +for pattern in ['*.py', 'scripts/*.py']: + for f in glob.glob(f'/home/hmo/MoFin/{pattern}'): + if any(x in f for x in exclude_patterns): + continue + try: + with open(f) as fh: + content = fh.read() + # Only catch actual I/O: json.load(open( + json filename + for kw in ['portfolio.json', 'decisions.json', 'watchlist.json']: + # Pattern: json.load and json.dump with open + if re.search(rf'json\.(load|dump).*{kw}', content): + for i, line in enumerate(content.split('\n')): + if kw in line and ('json.load' in line or 'json.dump' in line) and not line.strip().startswith('#'): + json_refs.append(f"{f}:{i+1}: {line.strip()[:60]}") + except: pass +test("无活跃 JSON I/O", len(json_refs) == 0, f"{len(json_refs)} refs" + (f" e.g. {json_refs[0]}" if json_refs else "")) + +# ── 9. LLM Prompt ── +print("\n--- 9. LLM Prompt ---") +prompt_json_issues = [] +try: + for fpath in glob.glob('/home/hmo/MoFin/prompt_manager/*.py'): + with open(fpath) as fh: + content = fh.read() + if '.json' not in content: + continue + for i, line in enumerate(content.split('\n')): + s = line.strip() + if s.startswith('#') or not s: + continue + if '.json' in s and any(x in s for x in ['evaluation.json', 'accuracy_stats.json', 'decisions.json', 'portfolio.json']): + prompt_json_issues.append(f"{fpath}:{i+1}: {s[:60]}") + test("prompt_manager 无 JSON 引用", len(prompt_json_issues) == 0, f"{len(prompt_json_issues)} issues: {prompt_json_issues[0] if prompt_json_issues else ''}") +except Exception as e: + test("LLM prompt 检查", False, str(e)[:80]) + +# ── 10. API ── +print("\n--- 10. API ---") +import urllib.request, json +try: + r = urllib.request.urlopen("http://localhost:8899/api/portfolio", timeout=5) + data = json.loads(r.read()) + test("GET /api/portfolio", True, f"total_assets={data.get('total_assets')}") +except Exception as e: + test("GET /api/portfolio", False, str(e)[:60]) + +try: + r = urllib.request.urlopen("http://localhost:8899/api/decisions", timeout=5) + data = json.loads(r.read()) + test("GET /api/decisions", True, f"total={data.get('total')}") +except Exception as e: + test("GET /api/decisions", False, str(e)[:60]) + +# ── 11. Cron 脚本可导入 ── +print("\n--- 11. Cron 可导入 ---") +cron_scripts = ['price_monitor', 'market_watch', 'market_screener', 'system_audit', 'data_freshness'] +for s in cron_scripts: + try: + __import__(s) + test(f"{s} 可导入", True) + except Exception as e: + test(f"{s} 可导入", False, str(e)[:50]) + +# ── 12. price_monitor dry run ── +print("\n--- 12. price_monitor 函数测试 ---") +try: + from price_monitor import refresh_data_prices, is_hk_stock as pm_is_hk + test("price_monitor.is_hk_stock 一致", pm_is_hk('01888') == is_hk_stock('01888')) +except Exception as e: + test("price_monitor 函数", False, str(e)[:80]) + +# ── 结果 ── +print(f"\n{'='*50}") +print(f"结果: {passed} passed, {failed} failed ({passed+failed} total)") +for r in results: + print(r) diff --git a/strategy_lifecycle.py b/strategy_lifecycle.py index 3281074..eacf924 100644 --- a/strategy_lifecycle.py +++ b/strategy_lifecycle.py @@ -1545,7 +1545,7 @@ def _get_portfolio_risk_state(): try: # 数据一致性检查:警告多副本(2026-06-23 bugfix) _check_portfolio_consistency() - p = json.load(open('/home/hmo/web-dashboard/data/portfolio.json')) + p = read_portfolio() pos_pct = p.get('position_pct', 0) cash = p.get('cash', 0) holdings = p.get('holdings', []) @@ -1611,7 +1611,7 @@ def _check_contradiction(code, today_only=True): """ try: from datetime import datetime, date - dec = json.load(open('/home/hmo/web-dashboard/data/decisions.json')) + dec = read_decisions() for e in dec.get('decisions', []): if e.get('code') != code: continue @@ -1650,7 +1650,7 @@ def _get_sell_priority_list(): 按卖出的优先顺序排列(最先应该卖的在最前) """ try: - p = json.load(open('/home/hmo/web-dashboard/data/portfolio.json')) + p = read_portfolio() holdings = p.get('holdings', []) ranked = [] for h in holdings: @@ -1887,7 +1887,7 @@ def reassess_with_context(code, name, price, cost, shares, current_action, # 6. 防洗盘:信号不要一天一翻(2026-06-23) # 如果旧信号是买入/持有类,新信号是谨慎/等待类,但中期趋势未破→维持旧信号 try: - dec = json.load(open('/home/hmo/web-dashboard/data/decisions.json')) + dec = read_decisions() for e in dec.get('decisions', []): if e.get('code') == code: old_signal = e.get('timing_signal', '')