diff --git a/main.py b/main.py index 357082c..8be540d 100644 --- a/main.py +++ b/main.py @@ -633,7 +633,41 @@ def send_tg_message(message): telegram_chat_id=TG_CHAT_ID, webhook_url=_NOTIFICATION_WEBHOOK_URL, ) - sender(message) + sender(_with_platform_notification_prefix(message)) + + +def _platform_notification_prefix() -> str: + runtime_target = getattr(RUNTIME_SETTINGS, "runtime_target", None) + account_selector = getattr(runtime_target, "account_selector", None) or () + for selector in account_selector: + label = _normalize_account_prefix_label(selector) + if label: + return f"[{label}]" + raw_scope = ( + getattr(runtime_target, "account_scope", None) + or ACCOUNT_GROUP + ) + label = _normalize_account_prefix_label(raw_scope) + if not label: + return "" + return f"[{label}]" + + +def _normalize_account_prefix_label(value: object) -> str: + label = str(value or "").strip().upper() + if not label or label in {"DEFAULT", "LIVE", "US"}: + return "" + if label.startswith("LIVE-U") and len(label) > len("LIVE-U"): + return label.removeprefix("LIVE-") + return label + + +def _with_platform_notification_prefix(message: str) -> str: + prefix = _platform_notification_prefix() + text = str(message or "") + if not prefix or text.lstrip().startswith(prefix): + return text + return f"{prefix} {text}" def publish_notification(*, detailed_text, compact_text): @@ -696,7 +730,7 @@ def _notify_runtime_error(exc: Exception, *, route_label: str | None = None) -> message = _runtime_error_notification_message(exc, route_label=route_label) for token, chat_id in targets: send_telegram_message( - message, + _with_platform_notification_prefix(message), token=token, chat_id=chat_id, requests_module=requests, diff --git a/tests/test_request_handling.py b/tests/test_request_handling.py index faa0294..91b0c97 100644 --- a/tests/test_request_handling.py +++ b/tests/test_request_handling.py @@ -1,3 +1,4 @@ +import json import types from application.cycle_result import StrategyCycleResult @@ -29,6 +30,35 @@ def test_health_route_returns_ok(strategy_module): assert body == "OK" +def test_platform_notification_prefix_stays_empty_for_default_account(strategy_module): + assert strategy_module._platform_notification_prefix() == "" + assert strategy_module._with_platform_notification_prefix("hello") == "hello" + + +def test_platform_notification_prefix_uses_ibkr_account_scope(strategy_module_factory): + module = strategy_module_factory( + ACCOUNT_GROUP="live-u1234567", + IB_ACCOUNT_GROUP_CONFIG_JSON=( + '{"groups":{"live-u1234567":{"ib_gateway_instance_name":"127.0.0.1",' + '"ib_gateway_mode":"live","ib_client_id":1,"account_ids":["U1234567"]}}}' + ), + RUNTIME_TARGET_JSON=json.dumps( + { + "platform_id": "ibkr", + "strategy_profile": "global_etf_rotation", + "dry_run_only": False, + "execution_mode": "live", + "account_selector": ["U1234567"], + "account_scope": "live-u1234567", + }, + separators=(",", ":"), + ), + ) + + assert module._platform_notification_prefix() == "[U1234567]" + assert module._with_platform_notification_prefix("hello") == "[U1234567] hello" + + def test_handle_request_get_returns_safe_message(strategy_module, monkeypatch): def fail_if_called(): raise AssertionError("GET should not execute strategy") @@ -755,3 +785,45 @@ def fake_post(_url, *, json, timeout): assert "IBKR 策略运行失败" in text assert "账户组:" in text assert "错误: RuntimeError: boom" in text + + +def test_handle_request_runtime_error_fallback_prefixes_account_scope(strategy_module_factory, monkeypatch): + strategy_module = strategy_module_factory( + ACCOUNT_GROUP="live-u1234567", + IB_ACCOUNT_GROUP_CONFIG_JSON=( + '{"groups":{"live-u1234567":{"ib_gateway_instance_name":"127.0.0.1",' + '"ib_gateway_mode":"live","ib_client_id":1,"account_ids":["U1234567"]}}}' + ), + RUNTIME_TARGET_JSON=json.dumps( + { + "platform_id": "ibkr", + "strategy_profile": "global_etf_rotation", + "dry_run_only": False, + "execution_mode": "live", + "account_selector": ["U1234567"], + "account_scope": "live-u1234567", + }, + separators=(",", ":"), + ), + ) + sent_payloads = [] + + class FakeResponse: + status_code = 200 + text = "ok" + + def fake_post(_url, *, json, timeout): + sent_payloads.append((json, timeout)) + return FakeResponse() + + monkeypatch.setattr(strategy_module, "_handle_request", lambda: (_ for _ in ()).throw(RuntimeError("boom"))) + monkeypatch.setattr(strategy_module, "TG_TOKEN", "token-1") + monkeypatch.setattr(strategy_module, "TG_CHAT_ID", "chat-1") + monkeypatch.setattr(strategy_module.requests, "post", fake_post) + + with strategy_module.app.test_request_context("/run", method="POST"): + body, status = strategy_module.handle_request() + + assert status == 500 + assert body == "Error" + assert sent_payloads[0][0]["text"].startswith("[U1234567] IBKR strategy run failed")