diff --git a/application/rebalance_service.py b/application/rebalance_service.py index e4b1a72..324472f 100644 --- a/application/rebalance_service.py +++ b/application/rebalance_service.py @@ -396,7 +396,7 @@ def fetch_replanned_state(): title_key=config.notification_title_key or "rebalance_title", ) ) - else: + elif getattr(config, "notify_no_trade_cycles", True): notification_publisher.publish( notification_renderers.render_heartbeat_notification( execution=execution, @@ -410,4 +410,6 @@ def fetch_replanned_state(): title_key=config.notification_title_key or "heartbeat_title", ) ) + else: + print(config.with_prefix("notification_suppressed reason=no_trade_or_error"), flush=True) return execution_result diff --git a/application/runtime_composer.py b/application/runtime_composer.py index ce1c1fb..27326e9 100644 --- a/application/runtime_composer.py +++ b/application/runtime_composer.py @@ -251,6 +251,7 @@ def build_rebalance_config( sleeper=self.sleeper, extra_notification_lines=(market_scope_line, *plugin_lines, *plugin_error_lines), notification_title_key=notification_title_key, + notify_no_trade_cycles=False, strategy_plugin_signals=tuple(strategy_plugin_signals or ()), execution_dedup_enabled=resolve_execution_dedup_enabled( env_reader=self.env_reader, diff --git a/application/runtime_dependencies.py b/application/runtime_dependencies.py index fcaaf89..ee2eea9 100644 --- a/application/runtime_dependencies.py +++ b/application/runtime_dependencies.py @@ -32,6 +32,7 @@ class LongBridgeRebalanceConfig: sleeper: Callable[[float], None] | None = None extra_notification_lines: tuple[str, ...] = () notification_title_key: str = "" + notify_no_trade_cycles: bool = True strategy_plugin_signals: tuple[Any, ...] = () execution_dedup_enabled: bool = False execution_state_store: Any = None diff --git a/constraints.txt b/constraints.txt index 5fcff72..fefe067 100644 --- a/constraints.txt +++ b/constraints.txt @@ -3,7 +3,7 @@ # Generated: 2026-07-01 # Auto-updated by update-qpk-pin.yml on every push to QPK main. -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b0eacd2fe4884f7f2447b704a232e9a121f396c4 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b9a7df85cfc848cebcc3aa6e1d77ec34ca7611ab us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@6568c315ce3be6f7ae5b799374cf7fb44232c170 hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@e9e3058c1eaf3f43b25d50df5eb14442816e568e cn-equity-strategies @ git+https://github.com/QuantStrategyLab/CnEquityStrategies.git@f6c735c33047d7613a23d5df018ed32f394e6001 diff --git a/requirements.txt b/requirements.txt index 30b76bd..7ae756c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,6 +8,6 @@ google-cloud-secret-manager google-cloud-storage google-auth longport==3.0.23 -quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b0eacd2fe4884f7f2447b704a232e9a121f396c4 +quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@b9a7df85cfc848cebcc3aa6e1d77ec34ca7611ab us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@6568c315ce3be6f7ae5b799374cf7fb44232c170 hk-equity-strategies @ git+https://github.com/QuantStrategyLab/HkEquityStrategies.git@e9e3058c1eaf3f43b25d50df5eb14442816e568e diff --git a/scripts/check_qpk_pin_consistency.py b/scripts/check_qpk_pin_consistency.py index 360f881..02349db 100644 --- a/scripts/check_qpk_pin_consistency.py +++ b/scripts/check_qpk_pin_consistency.py @@ -2,11 +2,11 @@ """Check that all QPK git references match the canonical QPK_PIN. Usage: python scripts/check_qpk_pin_consistency.py [--fix] """ -import re, subprocess, sys +import re, sys from pathlib import Path QPK_PIN_URL = "https://raw.githubusercontent.com/QuantStrategyLab/QuantPlatformKit/main/QPK_PIN" -SHA_RE = re.compile(r"@([a-f0-9]{40})") +QPK_REF_RE = re.compile(r"QuantPlatformKit\.git@([a-f0-9]{40})") def fetch_pin() -> str: import urllib.request @@ -23,15 +23,17 @@ def main(): for path in sorted(Path.cwd().glob("**/requirements*.txt")) + sorted(Path.cwd().glob("**/pyproject.toml")): if "external" in str(path): continue content = path.read_text() - for m in SHA_RE.finditer(content): + updated = content + for m in QPK_REF_RE.finditer(content): sha = m.group(1) - if "QuantPlatformKit" not in content[max(0,m.start()-200):m.end()]: continue if sha != target: errors += 1 print(f" ❌ {path}: QPK@{sha[:12]} (expected {target[:12]})") if fix: - path.write_text(content.replace(sha, target)) + updated = updated.replace(f"QuantPlatformKit.git@{sha}", f"QuantPlatformKit.git@{target}") print(" → fixed") + if fix and updated != content: + path.write_text(updated) if errors: print(f"\n{errors} mismatch(es). Run with --fix to auto-fix.") return 1 diff --git a/tests/test_rebalance_service.py b/tests/test_rebalance_service.py index deb5a46..a72575a 100644 --- a/tests/test_rebalance_service.py +++ b/tests/test_rebalance_service.py @@ -870,14 +870,14 @@ def test_run_strategy_prefers_portfolio_port_runtime_path(self): translator=build_translator("zh"), with_prefix=lambda message: f"[HK/LongBridgeQuant] {message}", strategy_display_name="SOXL/SOXX 半导体趋势收益", + notify_no_trade_cycles=False, ), ) self.assertIs(observed["snapshot"], snapshot) self.assertIsNone(observed["account_state"]) self.assertEqual(observed["indicators"], {"soxl": {"price": 1.0}}) - self.assertEqual(len(sent_messages), 1) - self.assertIn("【心跳", sent_messages[0]) + self.assertEqual(sent_messages, []) def test_run_strategy_supports_execution_port_runtime_path(self): sent_messages = [] diff --git a/tests/test_runtime_composer.py b/tests/test_runtime_composer.py index e5565d3..f0a351e 100644 --- a/tests/test_runtime_composer.py +++ b/tests/test_runtime_composer.py @@ -8,10 +8,11 @@ sys.path.insert(0, str(ROOT)) from quant_platform_kit.common import build_runtime_target # noqa: E402 +from application import runtime_composer as runtime_composer_module from application.runtime_composer import LongBridgeRuntimeComposer -def test_runtime_composer_builds_runtime_and_config_from_local_builders(): +def test_runtime_composer_builds_runtime_and_config_from_local_builders(monkeypatch): observed = {} def fake_notification_builder(**kwargs): @@ -30,6 +31,15 @@ def fake_bootstrap_builder(**kwargs): observed["bootstrap_builder"] = kwargs return "bootstrap" + def fake_cycle_sender(**kwargs): + observed["cycle_sender"] = kwargs + return lambda message: observed.setdefault( + "sent_message", + (kwargs["telegram_token"], kwargs["telegram_chat_id"], message), + ) + + monkeypatch.setattr(runtime_composer_module, "build_cycle_sender", fake_cycle_sender) + composer = LongBridgeRuntimeComposer( project_id="project-1", secret_name="secret-1", @@ -84,10 +94,6 @@ def fake_bootstrap_builder(**kwargs): report_persister="report-persister", translator=lambda key, **_kwargs: key, prefixer_builder=lambda prefix: lambda message: f"[{prefix}] {message}", - sender_builder=lambda token, chat_id, *, with_prefix_fn: lambda message: observed.setdefault( - "sent_message", - (token, chat_id, with_prefix_fn(message)), - ), env_reader=lambda name, default="": { "K_SERVICE": "longbridge-platform", "EXECUTION_REPORT_OUTPUT_DIR": "/tmp/runtime-reports", @@ -140,6 +146,7 @@ def fake_bootstrap_builder(**kwargs): assert config.dry_run_only is True assert config.safe_haven_cash_substitute_threshold_usd == 1000.0 assert config.min_order_notional_usd == 100.0 + assert config.notify_no_trade_cycles is False assert config.execution_dedup_enabled is True assert config.execution_state_account_scope == "HK" - assert config.execution_state_store.gcs_prefix_uri == "gs://bucket/runtime-reports" + assert config.execution_state_store.cloud_prefix_uri == "gs://bucket/runtime-reports"