265 lines
11 KiB
Markdown
265 lines
11 KiB
Markdown
# 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"
|
||
```
|