From 69587b6fbb7e4a24338c6c0d55f93d6f2f112159 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Tue, 3 Nov 2026 08:04:05 +0800 Subject: [PATCH] fix(deps): pin UES to c16c81bd with regime binding Pin UES and align the controlled V7 PAPER revision with c16c81bd. Verify installed revision metadata after frozen dependency installation. Pass independent regime binding and signal evidence into Russell context. Preserve fail-closed validation and the existing regression coverage. Supersedes #699 --- Dockerfile | 3 +- application/v7_paper_application.py | 4 +- docs/paper-strategy-risk-state.md | 23 +++++ pyproject.toml | 2 +- scripts/check_ues_revision.py | 41 +++++++++ strategy_runtime.py | 23 ++++- tests/test_russell_regime_binding.py | 129 +++++++++++++++++++++++++++ tests/test_ues_revision.py | 83 +++++++++++++++++ tests/test_v7_paper_application.py | 23 ++++- uv.lock | 4 +- 10 files changed, 327 insertions(+), 8 deletions(-) create mode 100644 scripts/check_ues_revision.py create mode 100644 tests/test_russell_regime_binding.py create mode 100644 tests/test_ues_revision.py diff --git a/Dockerfile b/Dockerfile index da6dd2f8..b9bbf758 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,8 @@ RUN apt-get update \ COPY . . RUN python -m pip install --upgrade pip uv \ - && uv sync --frozen --no-dev + && uv sync --frozen --no-dev \ + && uv run --no-sync python -m scripts.check_ues_revision # Keep the application timeout below Cloud Run/Scheduler's 600-second ceiling so # Gunicorn can return a controlled failure instead of being killed at the edge. diff --git a/application/v7_paper_application.py b/application/v7_paper_application.py index 847fa621..b68240d0 100644 --- a/application/v7_paper_application.py +++ b/application/v7_paper_application.py @@ -33,7 +33,9 @@ # The research contract's frozen source is 07b164..., while the package that # is actually approved for the disabled account process tracks the platform UES pin. # Keep research source_commit separate from approved_ues_revision. -V7_APPROVED_UES_REVISION = "8412d8338879f335ef8830678c2733e02e53e7c7" +# UES #715 fixes the Russell Top50 cash_reserve_ratio TypeError. +# Keep the controlled PAPER revision equal to the reviewed dependency pin. +V7_APPROVED_UES_REVISION = "c16c81bdf21824615f7280554559adc6c721917c" _COMMIT_PATTERN = re.compile(r"^[0-9a-fA-F]{40}$") _TICKET_PATTERN = re.compile(r"^rpt_[0-9a-fA-F]{64}$") diff --git a/docs/paper-strategy-risk-state.md b/docs/paper-strategy-risk-state.md index 99b08181..6fd20a99 100644 --- a/docs/paper-strategy-risk-state.md +++ b/docs/paper-strategy-risk-state.md @@ -20,3 +20,26 @@ does not change allocations or authorize a paper/shadow/live command. No existing LongBridge strategy emits this transition yet. Enabling the flag before a qualified strategy supplies a frozen transition will stop the cycle with an operator-visible error rather than silently forgetting state. + +## Russell regime binding + +The pinned UES Russell entrypoint requires a separately configured +`market_regime_control_expected` binding, even when position-control route +application is disabled. LongBridge reads the binding and the +`market_regime_control` signal from the existing `strategy_config_path` JSON +object on every feature-snapshot evaluation. The binding is passed as runtime +configuration; the signal is passed as a context artifact, not as a ranking +parameter. Other profiles keep their existing configuration path. + +The platform never derives an expected digest from the supplied signal. The +configuration producer must provide the independent binding fields `plugin`, +`schema_version`, `as_of`, `valid_until` and `payload_sha256`. UES validates the +plugin, schema, signal date, evaluation date range and canonical JSON SHA-256 +against the exact signal. Missing or invalid bindings and missing signals keep +ordinary decisions non-actionable; unreadable or malformed configuration stops +evaluation with a fixed diagnostic. Restarting a process does not grant an +exception, and the next evaluation re-reads configuration after revocation. + +This consumer integration does not publish a signal, supply approval, enable a +runtime target or qualify a PAPER session. Authentic signal and independent +binding provisioning remain prerequisites for deployment under owner approval. diff --git a/pyproject.toml b/pyproject.toml index 44b3876f..5632359b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ dependencies = [ "google-auth", "longport==3.0.23", "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@350dd38ece0952beef096893a7dcf8ae3871609a", - "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@8412d8338879f335ef8830678c2733e02e53e7c7", + "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@c16c81bdf21824615f7280554559adc6c721917c", "hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@fe0c32e09d454ca4388c3372bf3874097b1bbaf0", ] diff --git a/scripts/check_ues_revision.py b/scripts/check_ues_revision.py new file mode 100644 index 00000000..84d8ecc1 --- /dev/null +++ b/scripts/check_ues_revision.py @@ -0,0 +1,41 @@ +"""Fail the image build unless installed UES matches the frozen source pin.""" +from __future__ import annotations + +from importlib import metadata +import json +from pathlib import Path +import sys + +from scripts.review_shared_dependency_delta import Incomplete, resolve_pins + +ROOT = Path(__file__).resolve().parents[1] + + +def verify(root: Path) -> None: + try: + pins = resolve_pins( + (root / "pyproject.toml").read_text(encoding="utf-8"), + (root / "uv.lock").read_text(encoding="utf-8"), + ) + raw = metadata.distribution("us-equity-strategies").read_text("direct_url.json") + installed = json.loads(raw or "{}") + vcs = installed.get("vcs_info", {}) + if vcs.get("vcs") != "git" or vcs.get("commit_id") != pins["us-equity-strategies"]: + raise Incomplete("Installed UES does not match the frozen source pin") + except (OSError, ValueError, TypeError, AttributeError, metadata.PackageNotFoundError) as exc: + raise Incomplete("UES revision evidence unavailable or invalid") from exc + + +def main() -> int: + try: + verify(ROOT) + except Incomplete: + # Metadata, file paths and exception diagnostics may contain private data. + print("UES installed revision verification failed.", file=sys.stderr) + return 1 + print("UES installed revision verified against pyproject.toml and uv.lock.") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/strategy_runtime.py b/strategy_runtime.py index 98920e9b..bd10f092 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -1,8 +1,10 @@ from __future__ import annotations import os -from dataclasses import dataclass, field +import json +from dataclasses import dataclass, field, replace from datetime import datetime, timezone +from pathlib import Path from typing import Any, Callable, Mapping from quant_platform_kit.common.feature_snapshot import load_feature_snapshot_guarded @@ -303,7 +305,7 @@ def _build_runtime_risk_capabilities( return capability_payload, "verified:runtime_risk_limits" def _build_feature_snapshot_context(self, request): - return build_strategy_context_from_available_inputs( + context = build_strategy_context_from_available_inputs( entrypoint=request.entrypoint, runtime_adapter=request.runtime_adapter, as_of=request.as_of, @@ -311,6 +313,23 @@ def _build_feature_snapshot_context(self, request): runtime_config=request.runtime_config, capabilities=self._build_capital_base_capabilities(request.available_inputs), ) + if self.profile != "russell_top50_leader_rotation" or not self.runtime_settings.strategy_config_path: + return context + # The platform configuration supplies an independent expected binding. + # Never derive it from the signal or cache it across evaluations. + try: + config = json.loads(Path(self.runtime_settings.strategy_config_path).read_text(encoding="utf-8")) + if not isinstance(config, Mapping): + raise ValueError + except (OSError, ValueError, TypeError): + raise ValueError("Invalid Russell regime configuration") from None + runtime_config = dict(context.runtime_config) + if "market_regime_control_expected" in config: + runtime_config["market_regime_control_expected"] = config["market_regime_control_expected"] + artifacts = dict(context.artifacts) + if "market_regime_control" in config: + artifacts["market_regime_control"] = config["market_regime_control"] + return replace(context, runtime_config=runtime_config, artifacts=artifacts) def evaluate( self, diff --git a/tests/test_russell_regime_binding.py b/tests/test_russell_regime_binding.py new file mode 100644 index 00000000..1fda3363 --- /dev/null +++ b/tests/test_russell_regime_binding.py @@ -0,0 +1,129 @@ +"""Exercise the pinned UES entrypoint with synthetic platform configuration.""" +import hashlib +import json +from dataclasses import replace +from datetime import datetime, timezone +from types import SimpleNamespace +from unittest.mock import patch + +import pandas as pd +import pytest +from quant_platform_kit.common.models import PortfolioSnapshot +from quant_platform_kit.risk.contracts import RuntimeRiskLimits + +import strategy_runtime as runtime_module + +PROFILE = "russell_top50_leader_rotation" +DAY = "2026-11-03" + + +def regime_config(): + payload = { + "plugin": "market_regime_control", + "schema_version": "market_regime_control.v1", + "as_of": DAY, + "canonical_route": "risk_on", + } + encoded = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return { + "market_regime_control": payload, + "market_regime_control_expected": { + "plugin": "market_regime_control", + "schema_version": "market_regime_control.v1", + "as_of": DAY, + "valid_until": DAY, + "payload_sha256": hashlib.sha256(encoded.encode()).hexdigest(), + }, + } + + +@pytest.fixture +def platform(tmp_path): + path = tmp_path / "strategy.json" + path.write_text(json.dumps(regime_config())) + settings = replace(runtime_module._default_runtime_settings(PROFILE, "Synthetic Russell"), + strategy_config_path=str(path), dry_run_only=True, + feature_snapshot_path="synthetic.csv", + feature_snapshot_manifest_path="synthetic-manifest.json") + runtime = runtime_module.load_strategy_runtime( + PROFILE, runtime_settings=settings, logger=lambda _message: None, + execution_materials={"runtime_risk_limits": RuntimeRiskLimits( + allowed_symbols=("FAKEA", "FAKEB", "FAKEC", "FAKED", "BOXX"), + product_leverage_factors={symbol: 1 for symbol in ("FAKEA", "FAKEB", "FAKEC", "FAKED", "BOXX")}, + nominal_caps={symbol: 1.0 for symbol in ("FAKEA", "FAKEB", "FAKEC", "FAKED", "BOXX")}, + total_nominal_exposure_cap=1.0, total_effective_exposure_cap=1.0, max_positions=5, + )}, + ) + # Real ranking/weight construction; only the external snapshot read is replaced. + rows = [dict(symbol=symbol, sector="Synthetic", close=100.0, adv20_usd=100_000_000.0, + history_days=300, mom_3m=0.2, mom_6m=0.3, mom_12_1=0.4, + rel_mom_6m_vs_benchmark=0.1, rel_mom_6m_vs_broad_benchmark=0.1, + high_252_gap=-0.01, sma200_gap=0.1, vol_63=0.15, maxdd_126=-0.1) + for symbol in ("QQQ", "SPY", "FAKEA", "FAKEB", "FAKEC", "FAKED")] + guard = SimpleNamespace(frame=pd.DataFrame(rows), metadata={"snapshot_guard_decision": "proceed"}) + + def evaluate(): + with patch.object(runtime_module, "load_feature_snapshot_guarded", return_value=guard), \ + patch("quant_platform_kit.common.feature_snapshot_runtime.datetime") as clock: + clock.now.return_value = datetime(2026, 11, 3, tzinfo=timezone.utc) + return runtime.evaluate( + translator=lambda key, **_kwargs: key, + portfolio_snapshot=PortfolioSnapshot( + as_of=datetime(2026, 11, 3, tzinfo=timezone.utc), + total_equity=100_000.0, buying_power=100_000.0, positions=(), + ), + ).decision + return path, evaluate + + +def test_ordinary_russell_decision_keeps_target_weights(platform): + _, evaluate = platform + decision = evaluate() + assert "no_execute" not in decision.risk_flags + assert decision.diagnostics["actionable"] is True + assert any(position.symbol.startswith("FAKE") for position in decision.positions) + + +@pytest.mark.parametrize("defect", ["missing", "invalid", "digest", "expired", "missing_signal"]) +def test_russell_binding_gate_stays_closed(platform, defect): + path, evaluate = platform + config = regime_config() + if defect == "missing": + del config["market_regime_control_expected"] + elif defect == "invalid": + config["market_regime_control_expected"] = "invalid" + elif defect == "digest": + config["market_regime_control_expected"]["payload_sha256"] = "0" * 64 + elif defect == "expired": + config["market_regime_control_expected"]["valid_until"] = "2026-11-02" + else: + del config["market_regime_control"] + path.write_text(json.dumps(config)) + decision = evaluate() + assert "no_execute" in decision.risk_flags + assert not decision.positions + assert decision.diagnostics["market_regime_control_binding_valid"] is False + + +def test_next_evaluation_rereads_binding_after_revocation(platform): + path, evaluate = platform + assert "no_execute" not in evaluate().risk_flags + path.write_text(json.dumps({"market_regime_control": regime_config()["market_regime_control"]})) + assert "no_execute" in evaluate().risk_flags + + +@pytest.mark.parametrize("content", ["{", "[]"]) +def test_unreadable_configuration_blocks_without_raw_diagnostics(platform, content): + path, evaluate = platform + path.write_text(content) + with pytest.raises(ValueError, match="^Invalid Russell regime configuration$"): + evaluate() + + +def test_read_failure_blocks_without_private_exception_text(platform): + _, evaluate = platform + with patch("strategy_runtime.Path.read_text", side_effect=OSError("synthetic-private-detail")): + with pytest.raises(ValueError, match="^Invalid Russell regime configuration$") as error: + evaluate() + assert error.value.__suppress_context__ is True + assert "synthetic-private-detail" not in str(error.value) diff --git a/tests/test_ues_revision.py b/tests/test_ues_revision.py new file mode 100644 index 00000000..58035bd4 --- /dev/null +++ b/tests/test_ues_revision.py @@ -0,0 +1,83 @@ +"""Build-time checks use installed VCS metadata, never a declared environment pin.""" +import importlib +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +PIN = "c16c81bdf21824615f7280554559adc6c721917c" +OTHER = "a" * 40 + + +@pytest.fixture +def check(): + return importlib.import_module("scripts.check_ues_revision") + + +@pytest.fixture +def manifests(tmp_path): + for name in ("pyproject.toml", "uv.lock"): + (tmp_path / name).write_bytes((ROOT / name).read_bytes()) + return tmp_path + + +def installed(check, monkeypatch, payload): + monkeypatch.setattr(check.metadata, "distribution", lambda name: SimpleNamespace( + read_text=lambda filename: payload, + )) + + +def test_exact_installed_commit_passes(check, manifests, monkeypatch): + installed(check, monkeypatch, json.dumps({"vcs_info": {"vcs": "git", "commit_id": PIN}})) + assert check.verify(manifests) is None + + +@pytest.mark.parametrize("payload", [ + None, "", "{}", "null", "[]", "not json", + '{"vcs_info": null}', '{"vcs_info": []}', + json.dumps({"vcs_info": {"vcs": "git", "commit_id": OTHER}}), + json.dumps({"vcs_info": {"vcs": "git", "commit_id": PIN[:12]}}), + json.dumps({"vcs_info": {"vcs": "svn", "commit_id": PIN}}), + json.dumps({"vcs_info": {"commit_id": PIN}}), +]) +def test_missing_invalid_or_wrong_metadata_blocks(check, manifests, monkeypatch, payload): + installed(check, monkeypatch, payload) + monkeypatch.setenv("UES_REVISION", PIN) + with pytest.raises(check.Incomplete): + check.verify(manifests) + + +@pytest.mark.parametrize("filename", ["pyproject.toml", "uv.lock"]) +def test_manifest_disagreement_blocks(check, manifests, monkeypatch, filename): + installed(check, monkeypatch, json.dumps({"vcs_info": {"vcs": "git", "commit_id": PIN}})) + path = manifests / filename + path.write_text(path.read_text().replace(PIN, OTHER)) + with pytest.raises(check.Incomplete): + check.verify(manifests) + + +@pytest.mark.parametrize("filename", ["pyproject.toml", "uv.lock"]) +def test_unreadable_manifest_blocks(check, manifests, filename): + (manifests / filename).unlink() + with pytest.raises(check.Incomplete): + check.verify(manifests) + + +@pytest.mark.parametrize("error", [OSError("synthetic-private-detail"), + importlib.metadata.PackageNotFoundError("synthetic-private-detail")]) +def test_distribution_read_failure_blocks_without_diagnostics(check, manifests, monkeypatch, capsys, error): + def fail(_name): + raise error + monkeypatch.setattr(check.metadata, "distribution", fail) + monkeypatch.setattr(check, "ROOT", manifests) + assert check.main() == 1 + output = capsys.readouterr() + assert output.out == "" + assert output.err == "UES installed revision verification failed.\n" + + +def test_docker_build_checks_after_install(): + docker = (ROOT / "Dockerfile").read_text() + assert "uv sync --frozen --no-dev \\\n && uv run --no-sync python -m scripts.check_ues_revision" in docker diff --git a/tests/test_v7_paper_application.py b/tests/test_v7_paper_application.py index 137bf3f9..9002b13d 100644 --- a/tests/test_v7_paper_application.py +++ b/tests/test_v7_paper_application.py @@ -382,7 +382,7 @@ def test_qsl_application_origin_is_fixed() -> None: _endpoint("https://attacker.example", str(uuid.uuid4())) -def test_loaded_version_readback_uses_process_identity_and_stays_paused(monkeypatch) -> None: +def _loaded_v7_readback(monkeypatch): from quant_platform_kit.common.runtime_target import build_runtime_target from strategy_loader import load_strategy_entrypoint_for_profile, load_strategy_runtime_adapter_for_profile from us_equity_strategies import get_strategy_entrypoint @@ -409,13 +409,34 @@ def test_loaded_version_readback_uses_process_identity_and_stays_paused(monkeypa strategy_runtime=runtime, runtime_env={"V7_PAPER_SOURCE_COMMIT": "a" * 40, "K_REVISION": "paper-v7-00001"}, ) + return readback + + +def test_loaded_version_readback_uses_process_identity_and_stays_paused(monkeypatch) -> None: + readback = _loaded_v7_readback(monkeypatch) assert readback["activation_state"] == "applied_paused" assert readback["runtime_target_enabled"] is False + assert readback["ues_revision"] == "c16c81bdf21824615f7280554559adc6c721917c" assert readback["ues_revision"] == V7_APPROVED_UES_REVISION assert readback["revision_name"] == "paper-v7-00001" assert readback["runtime_loaded_receipt"]["schema_version"] == "runtime_loaded_receipt.v1" +@pytest.mark.parametrize("observed_revision", [ + "8412d8338879f335ef8830678c2733e02e53e7c7", "a" * 40, "", None, +]) +def test_loaded_version_readback_rejects_unapproved_ues(monkeypatch, observed_revision) -> None: + from application import v7_paper_application + + original = v7_paper_application._installed_revision + monkeypatch.setattr( + v7_paper_application, "_installed_revision", + lambda name: observed_revision if name == "us-equity-strategies" else original(name), + ) + with pytest.raises(V7PaperApplicationError, match="loaded UES revision"): + _loaded_v7_readback(monkeypatch) + + def test_bound_v7_loader_is_opt_in_and_normal_loader_stays_fail_closed(monkeypatch) -> None: from strategy_loader import load_strategy_entrypoint_for_profile diff --git a/uv.lock b/uv.lock index 69456423..15b314d8 100644 --- a/uv.lock +++ b/uv.lock @@ -730,7 +730,7 @@ requires-dist = [ { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=350dd38ece0952beef096893a7dcf8ae3871609a" }, { name = "requests" }, { name = "ruff", marker = "extra == 'test'" }, - { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=8412d8338879f335ef8830678c2733e02e53e7c7" }, + { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=c16c81bdf21824615f7280554559adc6c721917c" }, ] provides-extras = ["test"] @@ -1354,7 +1354,7 @@ wheels = [ [[package]] name = "us-equity-strategies" version = "0.7.60" -source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=8412d8338879f335ef8830678c2733e02e53e7c7#8412d8338879f335ef8830678c2733e02e53e7c7" } +source = { git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=c16c81bdf21824615f7280554559adc6c721917c#c16c81bdf21824615f7280554559adc6c721917c" } dependencies = [ { name = "pandas" }, { name = "pytz" },