fix(critical): 修复评审确认的 R1-R5 五处缺陷
R1 429 key 轮换路径调用不存在的 _km → NameError(且位于凭证脱敏日志行): 改用 src/data/key_ring._mask,全树不再有 _km 调用。 [注: 该改动已随另一工作流提交9ccda4b一并入库] R2 ingest_bzzoiro_standings 被截断(return 前无 upsert 逻辑,standings 表 永不写入、total_upserted 恒为 0):从f05dc1a移植完整实现,按 (league_id, season, team_id) upsert,保留逐联赛错误隔离,不加 db.commit()。 [注: 同上,已随9ccda4b入库] R3 src/llm/agents/orchestrator.py 完成日志把 list 喂给 %d → logging TypeError: 改为 len(ok_reports),与同文件降级日志行写法一致。 R4 mode=multi 静默丢弃调用方传入的 model:predict_match_multi 新增 model 关键字参数,经 _ACTIVE_MODEL_OVERRIDE 下传至 _agent_provider, 显式 model 优先级最高;override 生效时跳过 provider 缓存读写以免串味, 并在 predict.py 派发点透传。 R5 回测把字符串日期直接与 timestamptz 列比较:新增 _parse_date_bound 助手, 支持 YYYY-MM-DD / 完整 ISO / datetime / None,裸日期按 UTC 锚定, 结束日取当天末刻(闭区间,避免最后一天被静默排除),非法输入抛 ValueError。 新增 tests/test_review_required_fixes.py 覆盖 R1-R5(R2/R4 为行为测试), 20 项全通过。
This commit is contained in:
+36
-5
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
@@ -79,6 +79,35 @@ class BacktestSummary:
|
||||
results: list[BacktestMatchResult] = field(default_factory=list)
|
||||
|
||||
|
||||
def _parse_date_bound(value, *, end_of_day: bool) -> datetime | None:
|
||||
"""把日期入参解析成可与 timestamptz 列比较的 aware datetime。
|
||||
|
||||
支持 "YYYY-MM-DD"、完整 ISO 串(可带偏移)以及 datetime 对象;None 原样返回。
|
||||
裸日期按 UTC 锚定 —— Match.match_date 是 timestamptz,naive datetime 与之
|
||||
比较会因时区不同而偏移;start 取当天 00:00,end 取当天 23:59:59.999999
|
||||
(闭区间,否则最后一天会被静默排除)。
|
||||
|
||||
解析失败抛 ValueError(不静默吞掉):fromisoformat 对非法输入统一抛 ValueError,
|
||||
这里包一层以带上原始值,便于定位是哪个参数写错了。
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, datetime):
|
||||
dt = value
|
||||
else:
|
||||
try:
|
||||
dt = datetime.fromisoformat(str(value))
|
||||
except ValueError as e:
|
||||
raise ValueError(f"无法解析日期: {value!r}(应为 YYYY-MM-DD 或 ISO 格式)") from e
|
||||
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
# 闭区间上界:日期串解析出来是 00:00,取当天末刻才能让最后一天参与回测
|
||||
if end_of_day:
|
||||
dt = dt.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
return dt
|
||||
|
||||
|
||||
async def _get_historical_matches(
|
||||
db,
|
||||
*,
|
||||
@@ -106,10 +135,12 @@ async def _get_historical_matches(
|
||||
)
|
||||
if league_id is not None:
|
||||
stmt = stmt.where(Match.league_id == league_id)
|
||||
if date_from:
|
||||
stmt = stmt.where(Match.match_date >= date_from)
|
||||
if date_to:
|
||||
stmt = stmt.where(Match.match_date <= date_to)
|
||||
dt_from = _parse_date_bound(date_from, end_of_day=False)
|
||||
if dt_from is not None:
|
||||
stmt = stmt.where(Match.match_date >= dt_from)
|
||||
dt_to = _parse_date_bound(date_to, end_of_day=True)
|
||||
if dt_to is not None:
|
||||
stmt = stmt.where(Match.match_date <= dt_to)
|
||||
|
||||
stmt = stmt.order_by(Match.match_date.desc()).limit(limit)
|
||||
result = await db.execute(stmt)
|
||||
|
||||
Reference in New Issue
Block a user