From b756ca780fce84b295da0eaf48183601f3a5da8d Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 13 Sep 2026 23:18:43 +0800 Subject: [PATCH] chore: adopt owner-scoped QPK research retirement Co-Authored-By: Codex --- .github/workflows/drift-check.yml | 6 +- pyproject.toml | 2 +- qsl.toml | 2 +- .../backtest/index_etf_research_job.py | 37 ++++++++++-- tests/test_drift_workflow_config.py | 6 +- tests/test_index_etf_strict_runner.py | 56 ++++++++++++++++++- tests/test_qsl_compat_metadata.py | 2 +- uv.lock | 4 +- 8 files changed, 97 insertions(+), 18 deletions(-) diff --git a/.github/workflows/drift-check.yml b/.github/workflows/drift-check.yml index 7c5ac32..c88048e 100644 --- a/.github/workflows/drift-check.yml +++ b/.github/workflows/drift-check.yml @@ -26,7 +26,7 @@ jobs: uses: actions/checkout@v6 with: repository: QuantStrategyLab/QuantPlatformKit - ref: b5654244aa5d08bce2b4b4f931436268d57216df + ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd path: external/QuantPlatformKit - name: Set up Python @@ -194,7 +194,7 @@ jobs: contents: read issues: write id-token: write - uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@b5654244aa5d08bce2b4b4f931436268d57216df + uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd with: strategy_domain: cn_equity caller_event_name: ${{ github.event_name }} @@ -203,7 +203,7 @@ jobs: snapshot_checkout_path: external/CnEquitySnapshotPipelines snapshot_repository_ref: ${{ needs.preflight_backtests.outputs.snapshot_repository_ref }} ai_gateway_service_url: ${{ vars.AI_GATEWAY_SERVICE_URL }} - quant_platform_kit_ref: b5654244aa5d08bce2b4b4f931436268d57216df + quant_platform_kit_ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd lifecycle_preflight_artifact: lifecycle-preflight-${{ github.run_id }}-${{ github.run_attempt }} secrets: codex_audit_service_url: ${{ secrets.CODEX_AUDIT_SERVICE_URL }} diff --git a/pyproject.toml b/pyproject.toml index 0261590..431a686 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ readme = "README.md" requires-python = ">=3.11" dependencies = [ "pandas>=2.0", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b5654244aa5d08bce2b4b4f931436268d57216df", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd", ] [project.optional-dependencies] diff --git a/qsl.toml b/qsl.toml index 0e910cc..7987062 100644 --- a/qsl.toml +++ b/qsl.toml @@ -5,5 +5,5 @@ upgrade_ring = "ring_b" bundle = "2026.09.1" requires = [ "pandas>=2.0", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b5654244aa5d08bce2b4b4f931436268d57216df", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd", ] diff --git a/src/cn_equity_strategies/backtest/index_etf_research_job.py b/src/cn_equity_strategies/backtest/index_etf_research_job.py index 3aa8b08..1cba9d1 100644 --- a/src/cn_equity_strategies/backtest/index_etf_research_job.py +++ b/src/cn_equity_strategies/backtest/index_etf_research_job.py @@ -386,7 +386,9 @@ def run_index_etf_research_job( development_start: date, development_end: date, folds, locked_oos_start: date, locked_oos_end: date, purge_days: int, embargo_days: int, code_revision: str, ticket_dir, store_root, as_of, drift_score, source_revision, - record_shadow, sync_console, diagnose=None, pull_console=None, admit_new_research=None, read_pending_shadow=None, + record_shadow, sync_console, diagnose=None, summarize=None, pull_console=None, admit_new_research=None, + read_pending_shadow=None, + research_owner=None, config=None, cost_model=None, evaluation_date=None, ) -> dict[str, Any]: """Owning watcher's frozen job -> QPK's existing durable cycle. @@ -436,16 +438,43 @@ def run_index_etf_research_job( ) # A dependency without the persisted interface fails before any remote call. import inspect - if not {"research_identity", "admit_new_research", "read_pending_shadow"} <= set(inspect.signature(run_actionable_research_promotion).parameters): + required_qpk_parameters = {"research_identity", "admit_new_research", "read_pending_shadow"} + if summarize is not None: + required_qpk_parameters.add("summarize") + if research_owner is not None: + required_qpk_parameters.add("research_owner") + if not required_qpk_parameters <= set(inspect.signature(run_actionable_research_promotion).parameters): raise ValueError("cn_research_persistent_qpk_required") - result = run_actionable_research_promotion( + promotion_kwargs = dict( strategy_profile=PROFILE, domain="cn_equity", as_of=as_of, drift_score=drift_score, source_revision=source_revision, evaluation_date=evaluation_date, optimize=optimize, enforce_backtest_gates=gate, record_shadow=record_shadow, sync_console=sync_console, diagnose=diagnose, pull_console=pull_console, research_identity=identity, ticket_dir=ticket_dir, admit_new_research=admit_new_research, read_pending_shadow=read_pending_shadow, + research_owner=research_owner, ) - return {**result, "trial_records_path": str(trial_path), "experiment_store_root": str(experiment_store_root), + if summarize is not None: + promotion_kwargs["summarize"] = summarize + result = dict(run_actionable_research_promotion(**promotion_kwargs)) + # QPK may reuse or archive a saved ticket before invoking either callback. + # Do not report the newly derived experiment hash as executed evidence in + # that case; the durable ticket is the only accepted identity. + ticket_path = result.get("ticket_path") + ticket_stem = Path(ticket_path).stem if isinstance(ticket_path, str) else "" + if not ticket_stem: + ticket = result.get("ticket") + ticket_id = ticket.get("ticket_id") if isinstance(ticket, dict) else "" + ticket_stem = str(ticket_id or "") + if ticket_stem: + if (ticket_stem.startswith("rpt_") and len(ticket_stem) == 68 + and all(char in "0123456789abcdef" for char in ticket_stem[4:])): + result["research_key"] = ticket_stem[4:] + if result.get("reason") in {"saved_research_ticket_reused", "saved_research_ticket_terminal", + "research_scope_archived"} and not trial_path.exists(): + result["trial_records_path"] = None + result["experiment_store_root"] = None + return {**result, "trial_records_path": result.get("trial_records_path", str(trial_path)), + "experiment_store_root": result.get("experiment_store_root", str(experiment_store_root)), "research_identity": identity, "benchmark_method": "monthly_target_510300_same_constraints", "live_ready": False, "size_zero_required": True, "no_order": True} diff --git a/tests/test_drift_workflow_config.py b/tests/test_drift_workflow_config.py index f954700..da47c26 100644 --- a/tests/test_drift_workflow_config.py +++ b/tests/test_drift_workflow_config.py @@ -18,8 +18,8 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No assert '"conclusion": "success"' in workflow assert "cn_etf_market_history.csv" in workflow assert "repository: QuantStrategyLab/QuantPlatformKit" in workflow - assert "ref: b5654244aa5d08bce2b4b4f931436268d57216df" in workflow - assert "quant_platform_kit_ref: b5654244aa5d08bce2b4b4f931436268d57216df" in workflow + assert "ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow + assert "quant_platform_kit_ref: 5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow assert "python -m pip install --no-deps -e external/QuantPlatformKit" in workflow assert "scripts/run_walk_forward_backtest.py" in workflow assert '"--list-profiles"' in workflow @@ -30,7 +30,7 @@ def test_drift_workflow_wires_real_snapshot_history_and_preflight_bundle() -> No assert "Upload lifecycle preflight artifact" in workflow assert "actions/upload-artifact@v7" in workflow assert workflow.count("github.ref == format('refs/heads/{0}', github.event.repository.default_branch)") == 2 - assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@b5654244aa5d08bce2b4b4f931436268d57216df" in workflow + assert "uses: QuantStrategyLab/QuantPlatformKit/.github/workflows/reusable-drift-check.yml@5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" in workflow assert "strategy_domain: cn_equity" in workflow assert "caller_event_name: ${{ github.event_name }}" in workflow assert "caller_pr_head_repository: ${{ github.event.pull_request.head.repo.full_name || '' }}" in workflow diff --git a/tests/test_index_etf_strict_runner.py b/tests/test_index_etf_strict_runner.py index b2f4161..1e2ba61 100644 --- a/tests/test_index_etf_strict_runner.py +++ b/tests/test_index_etf_strict_runner.py @@ -330,6 +330,51 @@ def test_full_job_refuses_unapproved_or_synthetic_roots_before_any_callback(tmp_ assert not list(tmp_path.rglob("*.json")) +def test_full_job_forwards_optional_summary_callback_to_qpk(tmp_path, monkeypatch): + import cn_equity_strategies.backtest.index_etf_research_job as job + import sys + import types + + identity = {field: "a" * 64 for field in ( + "code_revision", "input_revision", "param_space_revision", "cost_model_revision", "validator_revision", + )} + def summary(context): + return {"status": "available", "text": "合成说明", "provider": "codex", "model": "test"} + observed = {} + owner = {"repository": "QuantStrategyLab/CnEquityStrategies", "issue_number": 123, + "watcher_issue_key": "watcher-key-123"} + + monkeypatch.setattr(job, "preflight_index_etf_research_job", lambda **_: identity) + monkeypatch.setattr(job, "make_strict_index_etf_optimizer", lambda **_: "optimize") + monkeypatch.setattr(job, "make_index_etf_promotion_gate", lambda **_: "gate") + + def checked_cycle(*, research_identity, admit_new_research, read_pending_shadow, summarize, research_owner, **kwargs): + observed.update({"research_identity": research_identity, "admit_new_research": admit_new_research, + "read_pending_shadow": read_pending_shadow, "summarize": summarize, + "research_owner": research_owner, **kwargs}) + return {"status": "parked", "reason": "research_scope_archived"} + + monkeypatch.setitem( + sys.modules, + "quant_platform_kit.strategy_lifecycle.promotion_actionable_runner", + types.SimpleNamespace(run_actionable_research_promotion=checked_cycle), + ) + result = job.run_index_etf_research_job( + development_input=object(), validation_input=object(), trusted_input_roots={"development": "dev", "validation": "val"}, + development_start=date(2024, 1, 1), development_end=date(2024, 1, 2), folds=(), + locked_oos_start=date(2025, 1, 1), locked_oos_end=date(2025, 1, 2), purge_days=1, embargo_days=1, + code_revision="c" * 40, ticket_dir=tmp_path / "tickets", store_root=tmp_path / "store", + as_of=date(2026, 9, 13), drift_score=.8, source_revision="d" * 40, + record_shadow=lambda *_: None, sync_console=lambda *_: None, summarize=summary, + research_owner=owner, + config=object(), cost_model=object(), + ) + + assert result["reason"] == "research_scope_archived" + assert observed["summarize"] is summary + assert observed["research_owner"] == owner + + def test_benchmark_retries_monthly_target_under_same_volume_constraint(): from cn_equity_strategies.backtest.index_etf_research_job import BASELINE_PARAMS days = list(pd.bdate_range("2024-01-02", periods=290).strftime("%Y-%m-%d")) @@ -396,10 +441,15 @@ def admit(*_): old_root = Path(first["trial_records_path"]).parent before = {p: p.read_bytes() for p in old_root.rglob("*.json")} third = run_index_etf_research_job(**{**kwargs, "as_of": date(2026, 9, 8), "source_revision": "e" * 40}) - assert third["research_key"] != first["research_key"] - assert third["trial_records_path"] != first["trial_records_path"] + # A changed observation revision does not create a new research scope. The + # terminal ticket remains the durable admission record, so no optimizer or + # diagnosis call is repeated. + assert third["reason"] == "saved_research_ticket_reused" + assert third["research_key"] == first["research_key"] + assert third["trial_records_path"] is None + assert third["experiment_store_root"] is None assert all(p.read_bytes() == content for p, content in before.items()) - assert calls == ["admit", "diagnose", "admit", "diagnose"] + assert calls == ["admit", "diagnose"] def test_malformed_license_record_has_fixed_failure_reason(): diff --git a/tests/test_qsl_compat_metadata.py b/tests/test_qsl_compat_metadata.py index 68e77e1..79aad33 100644 --- a/tests/test_qsl_compat_metadata.py +++ b/tests/test_qsl_compat_metadata.py @@ -3,7 +3,7 @@ ROOT = Path(__file__).resolve().parents[1] -QPK_REVISION = "b5654244aa5d08bce2b4b4f931436268d57216df" +QPK_REVISION = "5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" QPK_URL = ( "quant-platform-kit @ git+https://github.com/QuantStrategyLab/" f"QuantPlatformKit.git@{QPK_REVISION}" diff --git a/uv.lock b/uv.lock index e919448..ab8d382 100644 --- a/uv.lock +++ b/uv.lock @@ -40,7 +40,7 @@ requires-dist = [ { name = "ai-gateway-client", marker = "extra == 'research'", git = "https://github.com/QuantStrategyLab/AIAuditBridge.git?rev=60bd64a2ae059a082614181eeb845b46df395523" }, { name = "pandas", specifier = ">=2.0" }, { name = "pytest", marker = "extra == 'test'", specifier = ">=8" }, - { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=b5654244aa5d08bce2b4b4f931436268d57216df" }, + { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" }, ] provides-extras = ["test", "research"] @@ -324,7 +324,7 @@ wheels = [ [[package]] name = "quant-platform-kit" version = "1.0.0" -source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=b5654244aa5d08bce2b4b4f931436268d57216df#b5654244aa5d08bce2b4b4f931436268d57216df" } +source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd#5488048cc0fc8e818b8c4be7a9729f6f1b5fdfdd" } [[package]] name = "six"