From f5d83916a2c110e2b22aac09867cd114a3ddbe1e Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:30:29 +0800 Subject: [PATCH 1/2] fix: explain the UTC cutoff for an unclosed as_of date Co-Authored-By: Codex --- docs/advisory_contract.md | 14 ++++++++++++ src/quant_advisor_research/time_contract.py | 6 ++++-- tests/test_time_contract.py | 24 +++++++++++++++++++++ 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/docs/advisory_contract.md b/docs/advisory_contract.md index 9f43fe1..e936c33 100644 --- a/docs/advisory_contract.md +++ b/docs/advisory_contract.md @@ -28,6 +28,20 @@ policy: object The current CLI and scheduled workflows produce v6. Readers continue to accept legacy v5 reports, but no v6 field is silently accepted on a v5 report. +An explicit `as_of` is a UTC calendar date, not the invocation's local date. +Its entire UTC day must have ended before a report can be generated: +`generated_at >= reference_time`. For example, `as_of=2026-10-15` first becomes +valid at `2026-10-16T00:00:00Z` (`2026-10-16T08:00:00+08:00`). An earlier build +is rejected with the requested date, required UTC cutoff, and actual generation +time. Offsets are normalized to UTC before comparison; equality is accepted. + +Leave the workflow's `as_of` input empty to select a fully closed period: +yesterday for daily reports, the latest completed Saturday for weekly reports, +or the last day of the previous month for monthly reports, all in UTC. A manual +override keeps its requested date and is rejected if that date has not closed; +the producer does not shift the cutoff or change generation timestamps to make +an early request pass. + ### V6 default contract A v6 report uses `contract_version = model_recommendations.v6` and adds these diff --git a/src/quant_advisor_research/time_contract.py b/src/quant_advisor_research/time_contract.py index 1211751..c4854cd 100644 --- a/src/quant_advisor_research/time_contract.py +++ b/src/quant_advisor_research/time_contract.py @@ -49,7 +49,10 @@ def report_time_bounds(as_of: dt.date, generated_at: dt.datetime | str) -> Repor reference_time = canonical_reference_time(as_of) generated = normalize_aware_datetime(generated_at) if generated < reference_time: - raise TimeContractError("generated_at must be at or after reference_time") + raise TimeContractError( + f"generated_at must be at or after reference_time: as_of={as_of.isoformat()} " + f"requires generated_at >= {reference_time.isoformat()}; got {generated.isoformat()}" + ) return ReportTimeBounds(reference_time, generated, generated + dt.timedelta(days=REPORT_EXPIRY_DAYS)) @@ -178,4 +181,3 @@ def is_report_expired( if reference.tzinfo is None: reference = reference.replace(tzinfo=dt.UTC) return reference > expires_at - diff --git a/tests/test_time_contract.py b/tests/test_time_contract.py index f608e17..af36fe2 100644 --- a/tests/test_time_contract.py +++ b/tests/test_time_contract.py @@ -35,6 +35,30 @@ def test_reference_and_build_order_are_strict() -> None: report_time_bounds(dt.date(2026, 5, 30), "2026-05-30T12:00:00Z") +@pytest.mark.parametrize( + "generated_at", + ["2026-10-15T23:59:59.999999Z", "2026-10-16T07:59:59.999999+08:00"], +) +def test_unclosed_as_of_reports_the_required_utc_cutoff(generated_at: str) -> None: + with pytest.raises(TimeContractError) as exc: + report_time_bounds(dt.date(2026, 10, 15), generated_at) + + assert "as_of=2026-10-15" in str(exc.value) + assert "requires generated_at >= 2026-10-16T00:00:00+00:00" in str(exc.value) + assert "got 2026-10-15T23:59:59.999999+00:00" in str(exc.value) + + +@pytest.mark.parametrize( + "generated_at", + ["2026-10-16T00:00:00Z", "2026-10-16T08:00:00+08:00", "2026-10-15T20:00:00-04:00"], +) +def test_closed_as_of_accepts_the_exact_cutoff_in_any_offset(generated_at: str) -> None: + bounds = report_time_bounds(dt.date(2026, 10, 15), generated_at) + + assert bounds.reference_time == dt.datetime(2026, 10, 16, tzinfo=dt.UTC) + assert bounds.generated_at == bounds.reference_time + + def test_context_date_only_expiry_uses_original_offset_local_eod() -> None: assert context_expiry_instant("2026-06-30", "2026-05-30T00:30:00+08:00") == dt.datetime( 2026, 6, 30, 15, 59, 59, tzinfo=dt.UTC From fc20bcd78c5be375b5afa6f5f2ccdc1299bf781f Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 16 Oct 2026 02:31:49 +0800 Subject: [PATCH 2/2] chore: preserve existing source file spacing Co-Authored-By: Codex --- src/quant_advisor_research/time_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/quant_advisor_research/time_contract.py b/src/quant_advisor_research/time_contract.py index c4854cd..f8023fa 100644 --- a/src/quant_advisor_research/time_contract.py +++ b/src/quant_advisor_research/time_contract.py @@ -181,3 +181,4 @@ def is_report_expired( if reference.tzinfo is None: reference = reference.replace(tzinfo=dt.UTC) return reference > expires_at +