Files
MoFin/docs/TEST_PLAN.md
T

11 KiB
Raw Blame History

MoFin 测试文档

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


一、数据管道测试

1.1 价格源测试

ID 用例 步骤 预期
P1 A股价格拉取正确 调用 fetch_all_prices(['000657','600519']) 返回 dictprice > 0change_pct 有效
P2 港股价格拉取正确 调用 fetch_hk_eastmoney(['00700','01888']) 返回 dictprice > 0HKD),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 = 0total_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 内容自检脚本

"""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 正常

十、快速验证命令

# 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"