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
38 changes: 36 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Record hashes for the prefixed notification text

When a normal cycle is run with notification_delivery_events, application/runtime_notification_adapters.py:38-46 records compact_text_sha256 and compact_text_length from the original message after calling send_message. Prefixing only inside send_tg_message means Telegram receives [acct] ..., but the report's delivery event still hashes and sizes the unprefixed text, so account-scoped dry-run delivery logs become unverifiable/inaccurate while claiming only hash and length are persisted. Apply the prefix before the adapter records the delivery metadata, or have the adapter record the actual text passed to the sink.

Useful? React with 👍 / 👎.



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):
Expand Down Expand Up @@ -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,
Expand Down
72 changes: 72 additions & 0 deletions tests/test_request_handling.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
import types

from application.cycle_result import StrategyCycleResult
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Loading