From caeb22c62c4b18f7494b4424090edade38bbb28c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 18 Sep 2026 04:39:57 +0800 Subject: [PATCH] fix(receipts): emit no_signal and no_rebalance from clear cycle reasons Pin QPK for the expanded receipt outcomes, map only explicit no_op reasons, and default blank no-target cycles to no_signal without rewriting other no-ops. Co-authored-by: Cursor --- .github/workflows/ci.yml | 2 +- application/execution_receipt_adapter.py | 23 +++++++++++ application/rebalance_service.py | 16 +++++++- pyproject.toml | 4 +- qsl.toml | 2 +- tests/test_execution_receipt_adapter.py | 51 ++++++++++++++++++++++++ uv.lock | 6 +-- 7 files changed, 95 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c4d47b..9220f19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: - name: Check QPK pin consistency run: | set -euo pipefail - QPK_EXPECTED_PIN=0f2fde65023906f9f813fd1149a8dd6fbadbdd7f uv run --no-sync python scripts/check_qpk_pin_consistency.py + QPK_EXPECTED_PIN=f982aea79476cadd54d074f7c0447c1111658968 uv run --no-sync python scripts/check_qpk_pin_consistency.py - name: Ensure uv.lock matches pyproject.toml run: uv lock --check diff --git a/application/execution_receipt_adapter.py b/application/execution_receipt_adapter.py index cf67732..f43918e 100644 --- a/application/execution_receipt_adapter.py +++ b/application/execution_receipt_adapter.py @@ -16,6 +16,10 @@ _PARTIAL_FILL_KEYS = ("orders_partially_filled", "option_orders_partially_filled") _FILLED_KEYS = ("orders_filled", "option_orders_filled") _FAILURE_STATUSES = frozenset({"error", "failed", "failure"}) +# Only promote these explicit cycle reasons. Broad no-ops stay ``no_action`` +# so existing digests and projections keep digest-compatible outcomes. +_NO_SIGNAL_REASON_HEADS = frozenset({"no_signal"}) +_NO_REBALANCE_REASON_HEADS = frozenset({"no_rebalance", "target_diff_below_threshold"}) def attach_cycle_execution_receipt( @@ -54,6 +58,11 @@ def attach_cycle_execution_receipt( risk_blocked=status == "blocked" and not execution_failed, failed=execution_failed or status in _FAILURE_STATUSES, ) + if outcome == "no_action" and not bool(report.get("dry_run")): + explicit = _explicit_non_action_outcome(summary) + if explicit is not None: + outcome = explicit + confirmation = "not_applicable" return attach_runtime_execution_receipt( report, outcome=outcome, @@ -84,5 +93,19 @@ def _combined_summary( return {**dict(reconciliation_record or {}), **dict(execution_summary or {})} +def _explicit_non_action_outcome(summary: Mapping[str, Any]) -> str | None: + """Map only explicit cycle reasons to no_signal / no_rebalance.""" + + reason = str(summary.get("no_op_reason") or "").strip().lower() + if not reason: + return None + head = reason.split(":", 1)[0].strip() + if head in _NO_SIGNAL_REASON_HEADS: + return "no_signal" + if head in _NO_REBALANCE_REASON_HEADS: + return "no_rebalance" + return None + + def _has_any(summary: Mapping[str, Any], keys: tuple[str, ...]) -> bool: return any(bool(tuple(summary.get(key) or ())) for key in keys) diff --git a/application/rebalance_service.py b/application/rebalance_service.py index 24c45ae..18c03ea 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -998,7 +998,13 @@ def run_strategy_core( signal_metadata=signal_metadata, target_weights=None, execution_summary=blocked_summary, - no_op_reason=execution_blocked_reason or no_op_reason or fail_reason or decision, + no_op_reason=( + execution_blocked_reason + or no_op_reason + or fail_reason + or decision + or "no_signal" + ), ) record_path = write_reconciliation_record(record, output_path=config.reconciliation_output_path) print( @@ -1019,7 +1025,13 @@ def run_strategy_core( "notification_suppressed " + json.dumps( { - "reason": execution_blocked_reason or no_op_reason or fail_reason or decision, + "reason": ( + execution_blocked_reason + or no_op_reason + or fail_reason + or decision + or "no_signal" + ), "strategy_profile": signal_metadata.get("strategy_profile"), }, ensure_ascii=False, diff --git a/pyproject.toml b/pyproject.toml index cd75f59..78ab3bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ dependencies = [ "google-cloud-secret-manager", "google-cloud-storage", "yfinance", - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@0f2fde65023906f9f813fd1149a8dd6fbadbdd7f", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f982aea79476cadd54d074f7c0447c1111658968", "us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@4a3943883cd6b5bbfe32a559e56a91b40a81b7ce", "hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@709e5e1cde7841aed538d94eb26b552b46cb7806", ] @@ -64,5 +64,5 @@ include = [ [tool.uv] override-dependencies = [ - "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@0f2fde65023906f9f813fd1149a8dd6fbadbdd7f", + "quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@f982aea79476cadd54d074f7c0447c1111658968", ] diff --git a/qsl.toml b/qsl.toml index df369c6..9f1035a 100644 --- a/qsl.toml +++ b/qsl.toml @@ -5,7 +5,7 @@ upgrade_ring = "ring_d" allow_legacy = false [qsl.requires] -quant_platform_kit = "0f2fde65023906f9f813fd1149a8dd6fbadbdd7f" +quant_platform_kit = "f982aea79476cadd54d074f7c0447c1111658968" us_equity_strategies = "4a3943883cd6b5bbfe32a559e56a91b40a81b7ce" hk_equity_strategies = "709e5e1cde7841aed538d94eb26b552b46cb7806" diff --git a/tests/test_execution_receipt_adapter.py b/tests/test_execution_receipt_adapter.py index 461dc7b..d52a6cd 100644 --- a/tests/test_execution_receipt_adapter.py +++ b/tests/test_execution_receipt_adapter.py @@ -75,6 +75,57 @@ def test_expected_block_is_risk_blocked(self) -> None: self.assertEqual(report["execution_receipt"]["outcome"], "risk_blocked") + def test_explicit_no_signal_reason_is_no_signal(self) -> None: + report = _report() + + attach_cycle_execution_receipt( + report, + {}, + {"no_op_reason": "no_signal"}, + execution_failed=False, + ) + + self.assertEqual(report["execution_receipt"]["outcome"], "no_signal") + self.assertEqual(report["execution_receipt"]["broker_confirmation"], "not_applicable") + + def test_target_diff_below_threshold_is_no_rebalance(self) -> None: + report = _report() + + attach_cycle_execution_receipt( + report, + {"execution_status": "no_op", "no_op_reason": "target_diff_below_threshold"}, + {}, + execution_failed=False, + ) + + self.assertEqual(report["execution_receipt"]["outcome"], "no_rebalance") + self.assertEqual(report["execution_receipt"]["broker_confirmation"], "not_applicable") + + def test_ambiguous_no_op_reason_stays_no_action(self) -> None: + report = _report() + + attach_cycle_execution_receipt( + report, + {"execution_status": "no_op", "no_op_reason": "outside_execution_window"}, + {}, + execution_failed=False, + ) + + self.assertEqual(report["execution_receipt"]["outcome"], "no_action") + + def test_dry_run_keeps_no_action_even_with_explicit_reason(self) -> None: + report = _report() + report["dry_run"] = True + + attach_cycle_execution_receipt( + report, + {"no_op_reason": "target_diff_below_threshold"}, + {}, + execution_failed=False, + ) + + self.assertEqual(report["execution_receipt"]["outcome"], "no_action") + @pytest.mark.parametrize("revision", [None, "abc1234", "A" * 40]) def test_attested_invalid_revision_still_rejects_receipt(revision): diff --git a/uv.lock b/uv.lock index 255bf8a..f0c5603 100644 --- a/uv.lock +++ b/uv.lock @@ -17,7 +17,7 @@ resolution-markers = [ ] [manifest] -overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=0f2fde65023906f9f813fd1149a8dd6fbadbdd7f" }] +overrides = [{ name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=f982aea79476cadd54d074f7c0447c1111658968" }] [[package]] name = "beautifulsoup4" @@ -791,7 +791,7 @@ requires-dist = [ { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-cov", marker = "extra == 'test'" }, { name = "pytz" }, - { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=0f2fde65023906f9f813fd1149a8dd6fbadbdd7f" }, + { name = "quant-platform-kit", git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=f982aea79476cadd54d074f7c0447c1111658968" }, { name = "requests" }, { name = "ruff", marker = "extra == 'test'" }, { name = "us-equity-strategies", git = "https://github.com/QuantStrategyLab/UsEquityStrategies.git?rev=4a3943883cd6b5bbfe32a559e56a91b40a81b7ce" }, @@ -1327,7 +1327,7 @@ wheels = [ [[package]] name = "quant-platform-kit" version = "1.0.0" -source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=0f2fde65023906f9f813fd1149a8dd6fbadbdd7f#0f2fde65023906f9f813fd1149a8dd6fbadbdd7f" } +source = { git = "https://github.com/QuantStrategyLab/QuantPlatformKit.git?rev=f982aea79476cadd54d074f7c0447c1111658968#f982aea79476cadd54d074f7c0447c1111658968" } [[package]] name = "requests"