Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
1 change: 1 addition & 0 deletions application/runtime_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions application/runtime_dependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 7 additions & 5 deletions scripts/check_qpk_pin_consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down
19 changes: 13 additions & 6 deletions tests/test_runtime_composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Loading