diff --git a/backend/src/alphapulse/calendar/worker.py b/backend/src/alphapulse/calendar/worker.py index 2e7fbe0..c21abff 100644 --- a/backend/src/alphapulse/calendar/worker.py +++ b/backend/src/alphapulse/calendar/worker.py @@ -179,6 +179,8 @@ class CalendarWorker: return response, raw_id if period < row.period or (period == row.period and stage == "preliminary" and row.stage == "final"): return None + if row.first_received_at is not None: + return None # 最新页已换期;保留已取得的当期值,不误报历史缺口。 raise ValueError("密歇根当前页已晚于所选历史期次,未取得当期公告") if row.source == "bls": code = "empsit" if row.family == "jobs" else row.family @@ -202,6 +204,8 @@ class CalendarWorker: published = release_date(response.text).date() if row.source == "bls" and published < date: return None # 到点后官网仍缓存上一期,等待当期公告,不填旧值。 + if row.source == "bls" and published > date and row.first_received_at is not None: + return None # 滚动最新页不再包含本期,不能据此否认已取得的结果。 if published != date: raise ValueError("官方公告日期与日历不符,历史公布值尚未取得") return response, raw_id diff --git a/backend/tests/test_calendar.py b/backend/tests/test_calendar.py index b8511b5..b16edc5 100644 --- a/backend/tests/test_calendar.py +++ b/backend/tests/test_calendar.py @@ -158,7 +158,8 @@ def test_result_date_mismatch_waits_without_using_stale_release(trading): response = httpx.Response(200, text='
October 14, 2026', request=httpx.Request("GET", "https://www.bls.gov/example")) worker.fetch = AsyncMock(return_value=(response, 1)) - row = SimpleNamespace(source="bls", family="cpi", scheduled_at=datetime(2026, 11, 10, 13, 30, tzinfo=UTC)) + row = SimpleNamespace(source="bls", family="cpi", scheduled_at=datetime(2026, 11, 10, 13, 30, tzinfo=UTC), + first_received_at=None) assert await worker.result_page(row) is None row.scheduled_at = datetime(2026, 9, 10, 12, 30, tzinfo=UTC) with pytest.raises(ValueError, match="日期与日历不符"): @@ -169,6 +170,46 @@ def test_result_date_mismatch_waits_without_using_stale_release(trading): asyncio.run(check()) +@pytest.mark.parametrize("source", ["bls", "umich"]) +def test_rolling_page_rollover_preserves_received_result_without_source_error(trading, monkeypatch, source): + import httpx + from alphapulse.calendar import worker as module + + async def check(): + first_received = datetime(2026, 9, 24, 14, tzinfo=UTC) + values = {"cpi_yoy" if source == "bls" else "umich_sentiment": {"actual": "2.5", "previous": "2.4"}} + row = SimpleNamespace(id=1, source=source, family="cpi" if source == "bls" else "umich", + scheduled_at=first_received, first_received_at=first_received, + period="2026-09", stage="final" if source == "umich" else None, + values=values.copy(), revision_no=3, error="旧的采集错误") + session = SimpleNamespace(scalar=AsyncMock(return_value=row), commit=AsyncMock()) + + class Context: + async def __aenter__(self): + return session + async def __aexit__(self, *args): + pass + + monkeypatch.setattr(module, "SessionLocal", Context) + html = ('October 22, 2026' if source == "bls" else + '| Oct | Sep | |
| 2026 | 2026 | |
| Index of Consumer Sentiment | 49.0 | 48.1 |