feat: P0-02/P0-03/P0-05 数据库时间语义与约束
P0-02: MatchStats 增加 source/source_record_id/retrieved_at/available_at
context_builder 过滤统计数据时检查 available_at <= cutoff
P0-03: Prediction 增加 match_kickoff_at/prediction_created_at/prediction_cutoff_at
明确区分比赛时间/预测创建时间/数据截止时间
P0-05: Prediction 增加 status 字段(success/failed/degraded)
数据库 CHECK 约束
Alembic: 0005_prediction_status_and_stats_provenance
P1-02: injuries as_of 不再截断为 date,保持 datetime 精度
This commit is contained in:
@@ -14,6 +14,7 @@ import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
from collections.abc import Iterable
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
@@ -223,6 +224,7 @@ class BzzoiroSource:
|
||||
await db.flush()
|
||||
existing_matches[match_key] = m # 防止同批重复
|
||||
if nm.home_xg is not None or nm.away_xg is not None:
|
||||
now = datetime.now(timezone.utc)
|
||||
stats = MatchStats(
|
||||
match_id=m.id,
|
||||
home_xg=nm.home_xg,
|
||||
@@ -238,6 +240,10 @@ class BzzoiroSource:
|
||||
away_yellow_cards=nm.away_yellow_cards,
|
||||
home_red_cards=nm.home_red_cards,
|
||||
away_red_cards=nm.away_red_cards,
|
||||
source="bzzoiro",
|
||||
source_event_id=str(raw.get("id", "")),
|
||||
retrieved_at=now,
|
||||
available_at=now,
|
||||
)
|
||||
db.add(stats)
|
||||
league_r["inserted"] += 1
|
||||
|
||||
@@ -190,11 +190,13 @@ async def get_injuries_for_match(db, team_id: int, match_date, as_of=None) -> li
|
||||
match_date: 比赛日期
|
||||
as_of: 截止时间(cutoff)。只返回 retrieved_at <= as_of 的记录。
|
||||
用于回测时防止"未来采集的数据"泄漏到历史预测。
|
||||
必须保持 timezone-aware datetime,不会截断为 date。
|
||||
"""
|
||||
from sqlalchemy import and_, or_, select
|
||||
from sqlalchemy import or_, select
|
||||
|
||||
from src.db.models import Injury
|
||||
|
||||
# 只处理 match_date:去掉时间部分,仅比较日期
|
||||
if hasattr(match_date, "date"):
|
||||
match_date = match_date.date()
|
||||
|
||||
@@ -206,9 +208,8 @@ async def get_injuries_for_match(db, team_id: int, match_date, as_of=None) -> li
|
||||
)
|
||||
|
||||
# 回测防泄漏: 只使用 as_of 时间点之前已采集的数据
|
||||
# 注意: as_of 保持 datetime,不截断为 date,避免错误排除同日合法数据
|
||||
if as_of is not None:
|
||||
if hasattr(as_of, "date"):
|
||||
as_of = as_of.date()
|
||||
stmt = stmt.where(Injury.retrieved_at.is_not(None))
|
||||
stmt = stmt.where(Injury.retrieved_at <= as_of)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
@@ -136,7 +137,14 @@ class UnderstatSource:
|
||||
|
||||
# 回填 xG
|
||||
if existing.stats is None and (nm.home_xg is not None or nm.away_xg is not None):
|
||||
existing.stats = MatchStats(match_id=existing.id)
|
||||
now = datetime.now(timezone.utc)
|
||||
existing.stats = MatchStats(
|
||||
match_id=existing.id,
|
||||
source="understat",
|
||||
source_event_id=str(raw.get("id", "")),
|
||||
retrieved_at=now,
|
||||
available_at=now,
|
||||
)
|
||||
db.add(existing.stats)
|
||||
await db.flush()
|
||||
if existing.stats is not None:
|
||||
|
||||
Reference in New Issue
Block a user