# MoFin 测试文档

> 版本：1.0 | 日期：2026-07-03 | 维护：Sisyphus

---

## 一、数据管道测试

### 1.1 价格源测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| P1 | A股价格拉取正确 | 调用 fetch_all_prices(['000657','600519']) | 返回 dict，price > 0，change_pct 有效 |
| P2 | 港股价格拉取正确 | 调用 fetch_hk_eastmoney(['00700','01888']) | 返回 dict，price > 0（HKD），change_pct 有效 |
| P3 | 东财失败→腾讯兜底 | 东财不可用时 | 自动切 `_fetch_hk_tencent_fallback`，有数据返回 |
| P4 | 价格写入 DB | 运行 price_monitor.refresh_data_prices() | holdings.price 更新，portfolio_summary.updated_at 刷新 |
| P5 | 价格不变时不写 DB | 连续两次 refresh_data_prices() | 第二次 changed=False，不写入 |

### 1.2 数据读取测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| R1 | read_portfolio 返回正确 | `pf = read_portfolio()` | holdings 数组 + total_assets/cash/frozen_cash |
| R2 | read_decisions 返回正确 | `dec = read_decisions()` | decisions 数组，每条含 code/price/stop_loss/currency |
| R3 | read_watchlist 返回正确 | `wl = read_watchlist()` | stocks 数组，每条含 code/price |
| R4 | 无数据时不崩溃 | DB 为空时调用 read_*() | 返回空数组，不抛异常 |

---

## 二、币种测试

### 2.1 存储测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| C1 | 港股 holdings.currency=HKD | 查询 01888 | `currency = 'HKD'` |
| C2 | A股 holdings.currency=CNY | 查询 000657 | `currency = 'CNY'` |
| C3 | 港股决策 currency=HKD | 查询 01888 的 holding_strategies | `currency = 'HKD'` |
| C4 | A股决策 currency=CNY | 查询 000657 的 holding_strategies | `currency = 'CNY'` |
| C5 | 非法币种被拒绝 | INSERT holdings WITH currency='USD' | CHECK 约束拒绝，写入失败 |

### 2.2 币种转换测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| C6 | is_hk_stock 正确 | 传入 '00700', '01888', '000657', 'AAPL' | HK=True×2, A=False, US=False |
| C7 | to_cny 港股转换 | `to_cny(100, '00700')` | = 100 × get_hk_rate() |
| C8 | to_cny A股不转换 | `to_cny(100, '000657')` | = 100.0（原样返回） |
| C9 | get_hk_rate 返回有效值 | 调用 get_hk_rate() | 0.85 < rate < 0.95 |
| C10 | get_hk_rate 兜底 | hk_rate 模块不可达 | 返回 0.87 |

### 2.3 跨币种隔离测试（关键！）

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| C11 | 港股price(HKD)不直接参与CNY加法 | `sum(hk_price + a_price)` 不经过转换 | **应被 calc_total_mv 拦截转换** |
| C12 | calc_total_mv 港股自动转CNY | holdings 含港股，调用 calc_total_mv() | 港股市值 × 汇率，总值 CNY |
| C13 | calc_total_assets 含港股正确 | holdings 含港股 | = calc_total_mv + cash + frozen（全部CNY） |
| C14 | P&L 同币种比较 | `(price_hkd - cost_hkd) / cost_hkd` | 同币种，不混算 |
| C15 | P&L 跨币种比较被阻止 | `(price_hkd - cost_cny)` 未经转换 | **不应出现此路径** |

---

## 三、总资产计算测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| A1 | total_mv = Σ(shares × price) | 计算并对比 | 估值一致 |
| A2 | total_assets = total_mv + cash + frozen_cash | 计算并对比 | stored = calculated |
| A3 | frozen_cash=0 不影响计算 | 冻结已清零 | total_assets = total_mv + cash |
| A4 | position_pct = total_mv / total_assets × 100 | 验证 | 值 0-100，合理 |
| A5 | total_pnl = total_mv - total_cost | 对比 stored vs calculated | 差值 < 1 元 |
| A6 | 零持仓时 total_mv=0 | holdings 为空 | total_mv = 0，total_assets = cash |

---

## 四、DB 完整性测试

### 4.1 约束测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| D1 | holdings.code UNIQUE | 重复 code INSERT | ON CONFLICT DO UPDATE |
| D2 | holdings.currency CHECK | INSERT currency='EUR' | 拒绝 |
| D3 | portfolio_summary.id=1 唯一 | INSERT id=1 两次 | ON CONFLICT DO UPDATE |
| D4 | holding_strategies.code UNIQUE INDEX | 同 code 重复 INSERT | 唯一索引约束 |
| D5 | FK: holdings.code → stocks.code | 插入不存在股票的持仓 | 外键约束 |

### 4.2 数据一致性测试

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| D6 | holdings 与 portfolio_summary 一致 | 对比 total_mv | 一致 |
| D7 | decisions 与 holdings 代码一一对应 | 有持仓必有决策，反之亦然 | 对上的数 ≥ 90% |
| D8 | shares > 0 必须有 cost > 0 | 检查所有持仓 | 无 cost=0 的持仓（00700 除外，已知问题） |
| D9 | 无 holding_strategies 孤岛（无对应 holdings 的决策） | 检查 | 全部有对应持仓 |
| D10 | watchlist 中的股票不在 holdings 中 | 检查 | 自选股和持仓股无重叠 |

---

## 五、JSON 移除验证

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| J1 | 无 json.load(open(portfolio.json)) | grep 代码库 | 0 匹配 |
| J2 | 无 json.load(open(decisions.json)) | grep 代码库 | 0 匹配 |
| J3 | 无 json.load(open(watchlist.json)) | grep 代码库 | 0 匹配 |
| J4 | 无 json.dump(open(portfolio.json)) | grep 代码库 | 0 匹配（注释除外） |
| J5 | mo_data.py 无 JSON fallback | 读文件内容 | 无 `json.load(open(` 出现 |
| J6 | server.py 不写 JSON 数据文件 | `_save_json` 调用 | 仅用于 market/reports/stocks（非核心数据） |

---

## 六、LLM Prompt 测试

### 6.1 Prompt 内容标准

每个 LLM Prompt 必须满足：

| 标准 | 要求 |
|------|------|
| S1 | **不引用 JSON 文件** — 所有数据来源标注为 "DB 表名" |
| S2 | **币种明确** — 提及港股时标注 "（HKD）"，A股标注 "（CNY）" |
| S3 | **禁止跨币种比较** — 如涉及计算，明确"同币种比较"或"已转 CNY 后比较" |
| S4 | **输出格式标注币种** — 港股输出价格带 "(HKD)" 后缀 |
| S5 | **数据源正确** — 不引用已废弃的 API 或 JSON 路径 |

### 6.2 Prompt 测试用例

| ID | 用例 | Prompt ID | 检查项 | 预期 |
|----|------|-----------|--------|------|
| L1 | evaluation-daily 不引用 JSON | evaluation-daily v2 | 全文搜索 `.json` | 0 匹配 |
| L2 | evaluation-daily 引用 DB 表 | evaluation-daily v2 | 搜索 `strategy_evaluations` / `accuracy_stats` | 存在 |
| L3 | knowledge-extraction 不引用 JSON | knowledge-extraction v1 | 全文搜索 `.json` | 0 匹配 |
| L4 | knowledge-extraction 引用 DB | knowledge-extraction v1 | 搜索 `holding_strategies` | 存在 |
| L5 | system-health-check 不引用 JSON | system-health-check v2 | 全文搜索 `.json` | 0 匹配 |
| L6 | 所有 prompt 无 "读 JSON" 措辞 | 全部 prompt | grep `读.*\.json` | 0 匹配 |
| L7 | 币种标注检查 | 全部 prompt | 如涉港股，检查有 "(HKD)" 标注 | 存在 |
| L8 | 无 "portfolio.json" 引用 | 全部 prompt | grep `portfolio\.json` | 0 匹配 |
| L9 | 无 "decisions.json" 引用 | 全部 prompt | grep `decisions\.json` | 0 匹配 |
| L10 | 无 "直接存 HKD 原值" 等过时措辞 | 全部 prompt | grep `直接存.*HKD\|存CNY` | 0 匹配（或不导致误解） |

### 6.3 Prompt 内容自检脚本

```python
"""Check all LLM prompts for compliance"""
import re
from prompt_manager.init_registry import ALL_VERSIONS

issues = []
for ver in ALL_VERSIONS:
    content = ver.get('content', '')
    pid = ver.get('prompt_id', '?')
    
    # S1: no JSON file references
    if '.json' in content and 'DB' not in content.split('.json')[0][-20:]:
        issues.append(f"[{pid}] references .json file: {content}")
    
    # S2/S7: HK stock price should mention HKD
    if '港股' in content and 'HKD' not in content and 'CNY' not in content:
        issues.append(f"[{pid}] mentions HK stock but no currency label")
    
    # S3: should not instruct cross-currency comparison
    if re.search(r'(港|HK).*直[接比]', content):
        issues.append(f"[{pid}] may imply cross-currency comparison")

if issues:
    for i in issues: print(f"❌ {i}")
else:
    print("✅ All prompts pass")
```

---

## 七、API 测试

| ID | 用例 | 端点 | 预期 |
|----|------|------|------|
| API1 | 获取持仓 | GET /api/portfolio | 200, holdings[] 数组 |
| API2 | 获取概览 | GET /api/overview | 200, total_assets > 0 |
| API3 | 获取决策 | GET /api/decisions | 200, decisions[] 数组 |
| API4 | 写入持仓 | POST /api/update/portfolio | 200, DB 写入成功 |
| API5 | 写入决策 | POST /api/decisions/add | 200, holding_strategies 更新 |
| API6 | API 响应含 currency 字段 | GET /api/portfolio | 每个 holding 有 currency='HKD' 或 'CNY' |
| API7 | 重启后 API 正常 | kill + restart Flask | 200 响应 |

---

## 八、Cron 测试

| ID | 用例 | 命令 | 预期 |
|----|------|------|------|
| CR1 | price_monitor 能跑完 | `python3 price_monitor.py` | 30 秒内完成，无 TypeError |
| CR2 | market_watch 能跑完 | `python3 market_watch.py` | 正常退出 |
| CR3 | market_screener 能跑完 | `python3 market_screener.py` | 正常退出 |
| CR4 | stale_push_wlin 能跑完 | `python3 scripts/stale_push_wlin.py` | 正常退出 |
| CR5 | 东财不可用时走腾讯 | 东财 down | price_monitor 仍完整拉取 HK 价格 |
| CR6 | cron 日志无错误 | 查看 /tmp/price_monitor.log | 无 Exception/Error |

---

## 九、边界用例

| ID | 用例 | 步骤 | 预期 |
|----|------|------|------|
| E1 | price=None 不崩溃 | holdings 中某行 price=NULL | `s.get('price') or 0` → 0 |
| E2 | cost=None 不崩溃 | holdings 中某行 cost=NULL | 计算时用 0 |
| E3 | shares=0 不崩溃 | 已清仓的残留持仓 | 市值计算正确（0） |
| E4 | 港股 0 开头代码不被误判 | '000657' vs '00700' | is_hk_stock 正确区分 |
| E5 | 汇率 API 不可达时用缓存 | 断网 | hk_rate 返回上次缓存值 |
| E6 | 数据库被锁 | 多进程写入 | SQLite WAL 模式，不阻塞 |
| E7 | 空 holdings 列表 | 全部清仓 | total_mv=0, total_assets=cash 正常 |

---

## 十、快速验证命令

```bash
# 1. 数据完整性
python3 -c "from mo_data import read_portfolio,read_decisions; pf=read_portfolio(); dec=read_decisions(); print(f'holds={len(pf.get(\"holdings\",[]))} dec={len(dec.get(\"decisions\",[]))} cash={pf.get(\"cash\")}')"

# 2. 币种审计
python3 -c "
from mo_data import read_portfolio
from mo_models import is_hk_stock
pf=read_portfolio()
ok=True
for h in pf.get('holdings',[]):
    code=h.get('code',''); c=h.get('currency','')
    if is_hk_stock(str(code)) and c!='HKD': ok=False; print(f'FAIL HK: {code} {c}')
    elif not is_hk_stock(str(code)) and c!='CNY': ok=False; print(f'FAIL A: {code} {c}')
if ok: print('ALL CORRECT')
"

# 3. 总资产验证
python3 -c "
from mo_data import read_portfolio
from mo_models import calc_total_assets
pf=read_portfolio()
stored=pf.get('total_assets',0)
calc=calc_total_assets(pf)
print(f'stored={stored:.2f} calc={calc:.2f} diff={abs(stored-calc):.2f}')
print('PASS' if abs(stored-calc)<1 else 'FAIL')
"

# 4. LLM Prompt 审计
python3 -c "
from prompt_manager.init_registry import ALL_VERSIONS
import re
issues=[]
for v in ALL_VERSIONS:
    c=v.get('content','')
    pid=v.get('prompt_id','?')
    if '.json' in c and not any(x in c for x in ['DB','表','sqlite']):
        issues.append(f'{pid} refs .json')
if issues:
    for i in issues: print(f'FAIL: {i}')
else:
    print('ALL CLEAN')
"

# 5. JSON 代码残留检查
grep -rn 'json\.load\|json\.dump' --include='*.py' . | grep -v '#\|__pycache__\|test_\|inspect_\|check_\|verify_\|diagnose_\|audit_\|deep_\|list_\|rollback_\|fix_' | grep 'portfolio.json\|decisions.json\|watchlist.json' && echo "FAIL" || echo "PASS"
```
