完善评估能力:筛选参数 + degraded 排除 + 前端评估页

后端:
- settle_prediction 拒绝 degraded/failed(明确错误信息)
- get_eval_summary 支持 provider/model/prompt_version/mode 筛选
- 返回 filtered_settled/evaluated/skipped_degraded 等计数
- matches 游标分页方向修复(scheduled ASC 用 > 条件)
- available_at 加 2h 缓冲(近似完赛时间)
- bzzziro 统计字段映射注释(待真实响应验证)
- injuries 区分 no_local_data 与 success 空名单

前端:
- 新增 EvalPage(筛选控件 + 汇总卡片 + 准确率表格)
- 挂载 /admin/eval 路由与导航

测试:
- test_matches_cursor.py:游标方向
- test_available_at.py:2h 缓冲与回测防泄漏
- test_bzzoirot_stats.py:统计字段映射
- test_injuries_no_local_data.py:no_local_data vs success
- test_injuries_inserted_count.py:失败批不计入
- test_eval_excludes_degraded.py:degraded 排除准确率
This commit is contained in:
Profeto Agent
2026-09-19 09:40:24 +00:00
parent c2c4752856
commit 835d7217d0
21 changed files with 888 additions and 67 deletions
+54 -24
View File
@@ -1,10 +1,12 @@
"""回归测试: match_stats.available_at 回测防泄漏语义。
验证:
1. _is_stats_available: available_at is None + cutoff 不为 None → 不可用
2. _is_stats_available: available_at is None + cutoff is None(实盘) → 可用(兼容旧数据)
3. _is_stats_available: available_at > cutoff → 不可用
4. _is_stats_available: available_at <= cutoff → 可用
1. _is_stats_available: available_at is None + cutoff 不为 None → 不可用
2. _is_stats_available: available_at is None + cutoff is None(实盘) → 可用(兼容旧数据)
3. _is_stats_available: available_at > cutoff → 不可用
4. _is_stats_available: available_at <= cutoff → 可用
5. 写入策略: available_at = match_date + 2h 缓冲
6. cutoff 在缓冲内时不可用(available_at > cutoff → False)
"""
from __future__ import annotations
@@ -70,38 +72,66 @@ class TestIsStatsAvailable:
assert _is_stats_available(stats, before=cutoff) is False
class TestBzzoirotAvailableAt:
"""验证 bzzoiro.py 写入 available_at 使用 match_date 而非 now"""
class TestWriteBufferStrategy:
"""验证 bzzoiro/understat 写入 available_at 使用 match_date + 2h 缓冲"""
def test_bzzoiro_sets_available_at_from_match_date(self):
"""bzzoiro.py 应在创建 stats 时使用 nm.date 作为 available_at。"""
def test_bzzoirot_new_match_available_at_is_two_hours_after_kickoff(self):
"""bzzoiro 新建比赛时 available_at 应为开球 + 2 小时"""
import inspect
from src.data import bzzoiro
source = inspect.getsource(bzzoiro)
# 验证:存在 available_at = nm.date 的逻辑
assert 'available_at = nm.date if nm.date else now' in source, \
"bzzoiro.py 应使用 nm.date 作为 available_at"
# 验证:使用 timedelta(hours=2) 作为缓冲
assert 'timedelta(hours=2)' in source, \
"bzzoiro 应使用 match_date + timedelta(hours=2) 作为 available_at"
def test_bzzoiro_existing_match_uses_match_date(self):
"""bzzoiro.py 更新已有比赛时也应用 nm.date"""
def test_bzzoirot_existing_match_uses_two_hour_buffer(self):
"""bzzoiro 更新已有比赛时也应使2 小时缓冲"""
import inspect
from src.data import bzzoiro
source = inspect.getsource(bzzoiro)
# 验证两处都更新
count = source.count('available_at = nm.date if nm.date else now')
assert count == 2, f"期望 2 处使用 nm.date,实际 {count}"
# 两处写入都应使用 timedelta(hours=2)
count = source.count('timedelta(hours=2)')
assert count >= 2, f"期望至少 2 处 timedelta(hours=2),实际 {count}"
class TestUnderstatAvailableAt:
"""验证 understat.py 写入 available_at 使用 match_date。"""
def test_understat_sets_available_at_from_match_date(self):
"""understat.py 应使用 existing.match_date 作为 available_at。"""
def test_understat_uses_two_hour_buffer(self):
"""understat 回填 xG 时也应使用 2 小时缓冲。"""
import inspect
from src.data import understat
source = inspect.getsource(understat)
assert 'available_at = match_date' in source, \
"understat.py 应使用 match_date 作为 available_at"
assert 'timedelta(hours=2)' in source, \
"understat 应使用 match_date + timedelta(hours=2) 作为 available_at"
def test_cutoff_within_buffer_makes_stats_unavailable(self):
"""cutoff 在 2 小时缓冲内时,统计学不可用(回测防泄漏)。
开球:2026-01-15 20:00
available_at:2026-01-15 22:00(开球 + 2h)
cutoff:2026-01-15 21:00(开赛后 1h, statistics 尚未可用)
→ 不可用
"""
kickoff = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
available_at = kickoff + timedelta(hours=2) # 22:00
cutoff = kickoff + timedelta(hours=1) # 21:00,在缓冲内
stats = _make_stats(available_at=available_at)
assert _is_stats_available(stats, before=cutoff) is False, \
"cutoff 在 2h 缓冲内时应不可用(available_at > cutoff)"
def test_cutoff_after_buffer_makes_stats_available(self):
"""cutoff 超过 2 小时缓冲后,统计变为可用。
开球:2026-01-15 20:00
available_at:2026-01-15 22:00(开球 + 2h)
cutoff:2026-01-16 20:00(开赛后 1 天,超过缓冲)
→ 可用
"""
kickoff = datetime(2026, 1, 15, 20, 0, tzinfo=timezone.utc)
available_at = kickoff + timedelta(hours=2) # 22:00
cutoff = kickoff + timedelta(days=1) # 2026-01-16 20:00
stats = _make_stats(available_at=available_at)
assert _is_stats_available(stats, before=cutoff) is True, \
"cutoff 超过 2h 缓冲后应可用(available_at < cutoff)"