Files
Profeto/src/llm/slices/standings.py
T
shangfangjian 1a9dc63edd fix: Standing 无 xgd 属性,用 xg_for - xg_against 计算 xG差
standings_slice._fmt 误用 row.xgd(Standing 无此列),预测时报 AttributeError。
修正:直接用 xg_for - xg_against 计算 xG差,去掉对 goal_diff 的冗余判断。
2026-09-22 10:52:26 +08:00

92 lines
4.0 KiB
Python

"""D - 联赛排名切片: 积分榜快照(支持 cutoff 的历史还原,standings)。"""
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 League, Standing
from src.llm.slices.common import MatchHeader, SliceResult
if TYPE_CHECKING:
from sqlalchemy.ext.asyncio import AsyncSession
async def standings_slice(header: MatchHeader, *, before=None, db: AsyncSession | None = None) -> SliceResult:
"""D - 联赛排名切片: 两队积分榜位置、积分、近期走势(form)、分区,评估整体实力差距。
P0-02: 支持 cutoff(before)。取 available_at<=cutoff 的每队最新快照
(DISTINCT ON team_id ORDER available_at DESC);before=None 时 cutoff=now()。
回测时可还原历史时刻榜单,不再只是"最新快照、忽略 cutoff"。
db: 可选共享 session(见 context_builder 模块 docstring)。
语义区分:
- 两队都有积分榜行 → has_data=True(明确的排名信息)
- 任一队缺失 → has_data=False(升班马/杯赛无榜,信息不完整时明确声明)
"""
# P0-02: before=None → cutoff=now()(取最新可用快照)
if before is None:
from datetime import datetime, timezone
before = datetime.now(timezone.utc)
if db is not None:
league = (await db.execute(select(League).where(League.id == header.league_id))).scalar_one_or_none()
# P0-02: DISTINCT ON (team_id) 取 available_at<=cutoff 的最新快照
rows = (
(
await db.execute(
select(Standing)
.options(selectinload(Standing.team))
.where(Standing.league_id == header.league_id)
.where(Standing.available_at <= before)
.distinct(Standing.team_id)
.order_by(Standing.team_id, Standing.available_at.desc())
)
)
.scalars()
.all()
if league
else []
)
else:
async with AsyncSessionLocal() as new_db:
return await standings_slice(header, before=before, db=new_db)
lines = [f"── 联赛排名({header.league_name}{len(rows)} 队) ──"]
n_records = 0
def _fmt(row) -> str:
zg = f" xG差 {row.xg_for - row.xg_against:+.1f}" if row.xg_for is not None and row.xg_against is not None else ""
form = f" 近5场 {row.form}" if row.form else ""
zone = f" [{row.zone}]" if row.zone else ""
return (
f" 第 {row.position} 名: {row.points} 分 / {row.played} 场 "
f"({row.won}{row.drawn}{row.lost}负, 进{row.goals_for}{row.goals_against} 净胜{row.goal_diff:+d}"
f"{zg}){form}{zone}"
)
for label, team_id in (("主队", header.home_team_id), ("客队", header.away_team_id)):
row = next((r for r in rows if r.team_id == team_id), None)
if row is None:
lines.append(f" {label}: 暂无积分榜数据(可能杯赛/赛季未开始)")
else:
n_records += 1
lines.append(f" {label} {header.home_name if label == '主队' else header.away_name}:")
lines.append(_fmt(row))
# 两队排名对比摘要
home_row = next((r for r in rows if r.team_id == header.home_team_id), None)
away_row = next((r for r in rows if r.team_id == header.away_team_id), None)
if home_row and away_row:
diff = home_row.position - away_row.position # 正数=主队排名更靠前(名次更小)
lead = f"主队排名高 {diff} 位" if diff > 0 else (f"客队排名高 {-diff} 位" if diff < 0 else "两队同排名结构")
pts_diff = home_row.points - away_row.points
lines.append(f" 排名对比: {lead}, 分差 {pts_diff:+d}")
# has_data: 两队都有行才算完整;只有一队时仍有价值,但标记不完整
has_data = n_records >= 1
return SliceResult(text="\n".join(lines), has_data=has_data, n_records=n_records)