新增 matches.score_status(known/missing/unknown): - 替换 ck_matches_finished_has_score 为 ck_matches_score_integrity: known → 必须有分; missing/unknown → goals 必须 NULL(不伪造 0:0) - normalize: 完赛缺分不再静默降级为 scheduled,改设 score_status=missing - events ingest: 创建/更新 Match 同步 score_status(比分由缺变 known / 确认缺分 missing) - slices(form/h2h/home_away)/backtest: 显式加 score_status='known' 过滤完赛样本 - 迁移 0022 回填现有数据(绝不 UPDATE goals=0) 测试 tests/test_p0_score_status.py(9/9):约束存在性/Match 构造/normalize 行为。 54 相关测试全绿。
90 lines
3.9 KiB
Python
90 lines
3.9 KiB
Python
"""E - 历史交锋切片: 交手史与胜负规律(h2h)。"""
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.orm import selectinload
|
|
|
|
from src.db.base import AsyncSessionLocal
|
|
from src.db.models import Match
|
|
from src.llm.slices.common import MatchHeader, SliceResult
|
|
|
|
if TYPE_CHECKING:
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
async def h2h_slice(header: MatchHeader, *, limit: int = 8, before=None, db: AsyncSession | None = None) -> SliceResult:
|
|
"""E - 历史交锋切片: 过去数年 + 近期交手数据,提取交手规律。before=match_date 用于回测。
|
|
|
|
db: 可选共享 session,避免每个切片独立建连(见 context_builder 模块 docstring)。
|
|
"""
|
|
if db is not None:
|
|
h2h = await _get_h2h(db, header.home_team_id, header.away_team_id, before=before, limit=limit)
|
|
else:
|
|
async with AsyncSessionLocal() as new_db:
|
|
h2h = await _get_h2h(new_db, header.home_team_id, header.away_team_id, before=before, limit=limit)
|
|
lines = [f"── 历史交锋(近 {limit} 次) ──"]
|
|
n_with_score = 0
|
|
if h2h:
|
|
# 从当前主队视角统计:判断当前主队在每场交锋中是主是客
|
|
current_home_wins = current_home_draws = current_home_losses = 0
|
|
for hm in h2h:
|
|
d = hm.match_date.strftime("%Y-%m") if hm.match_date else "?"
|
|
if hm.home_goals is not None:
|
|
n_with_score += 1
|
|
# 判断当前主队当时是主队还是客队
|
|
if hm.home_team_id == header.home_team_id:
|
|
# 当前主队当时是主队
|
|
if hm.home_goals > hm.away_goals:
|
|
current_home_wins += 1
|
|
elif hm.home_goals == hm.away_goals:
|
|
current_home_draws += 1
|
|
else:
|
|
current_home_losses += 1
|
|
else:
|
|
# 当前主队当时是客队(从客队视角看赛果)
|
|
if hm.away_goals > hm.home_goals:
|
|
current_home_wins += 1
|
|
elif hm.away_goals == hm.home_goals:
|
|
current_home_draws += 1
|
|
else:
|
|
current_home_losses += 1
|
|
lines.append(f" {d}: {hm.home_team.name} {hm.home_goals}-{hm.away_goals} {hm.away_team.name}")
|
|
else:
|
|
lines.append(f" {d}: {hm.home_team.name} vs {hm.away_team.name} (无比分)")
|
|
total = current_home_wins + current_home_draws + current_home_losses
|
|
if total:
|
|
lines.append(
|
|
f" 总计 {total} 场(从当前主队 {header.home_name} 视角): "
|
|
f"{current_home_wins}胜 {current_home_draws}平 {current_home_losses}负"
|
|
)
|
|
else:
|
|
lines.append(" 无数据")
|
|
# has_data 以「有比分的交锋」为准:仅有对阵无比分时不足以支撑分析
|
|
return SliceResult(text="\n".join(lines), has_data=n_with_score > 0, n_records=n_with_score)
|
|
|
|
|
|
async def _get_h2h(db, home_id: int, away_id: int, before, *, limit: int = 5) -> list[Match]:
|
|
"""两队交锋史。需预加载 home_team / away_team(切片输出队名)。"""
|
|
stmt = (
|
|
select(Match)
|
|
.options(
|
|
selectinload(Match.home_team),
|
|
selectinload(Match.away_team),
|
|
)
|
|
.where(Match.match_status == "finished")
|
|
.where(Match.score_status == "known")
|
|
.where(Match.home_goals.is_not(None))
|
|
.where(
|
|
((Match.home_team_id == home_id) & (Match.away_team_id == away_id))
|
|
| ((Match.home_team_id == away_id) & (Match.away_team_id == home_id))
|
|
)
|
|
.order_by(Match.match_date.desc())
|
|
.limit(limit)
|
|
)
|
|
if before is not None:
|
|
stmt = stmt.where(Match.match_date < before)
|
|
result = await db.execute(stmt)
|
|
return list(result.scalars().all())
|