From 53e03afb4eea88456af3d3a9e6aaf0613ef18c81 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:57:36 +0800 Subject: [PATCH] Localize Binance strategy display names --- application/cycle_service.py | 9 ++++-- main.py | 8 ++++- notify_i18n_support.py | 35 +++++++++++++++++--- reporting/status_reports.py | 2 ++ runtime_config_support.py | 20 ++++++++++++ runtime_support.py | 12 +++++-- strategy_registry.py | 10 ++++++ strategy_runtime.py | 9 +++++- tests/test_cycle_service.py | 10 +++++- tests/test_runtime_config_support.py | 4 +++ tests/test_status_reports.py | 48 ++++++++++++++++++++++++++++ tests/test_strategy_runtime.py | 1 + 12 files changed, 156 insertions(+), 12 deletions(-) diff --git a/application/cycle_service.py b/application/cycle_service.py index c3e07e5..441f00a 100644 --- a/application/cycle_service.py +++ b/application/cycle_service.py @@ -205,6 +205,7 @@ def execute_strategy_cycle( prices["BTCUSDT"], btc_snapshot, btc_target_ratio, + getattr(runtime, "strategy_display_name_localized", "") or getattr(runtime, "strategy_display_name", ""), notifier_fn=lambda text: runtime_notify(runtime, report, text), ) @@ -247,9 +248,13 @@ def run_live_cycle( platform="binance", deploy_target=os.getenv("LOG_DEPLOY_TARGET", "vps"), service_name=os.getenv("SERVICE_NAME", "binance-platform"), - strategy_profile=os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation"), + strategy_profile=str(getattr(runtime, "strategy_profile", "") or os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation")), run_id=str(getattr(runtime, "run_id", "") or ""), - extra_fields={"dry_run": bool(getattr(runtime, "dry_run", False))}, + extra_fields={ + "dry_run": bool(getattr(runtime, "dry_run", False)), + "strategy_display_name": str(getattr(runtime, "strategy_display_name", "") or ""), + "strategy_display_name_localized": str(getattr(runtime, "strategy_display_name_localized", "") or ""), + }, ) emit_runtime_log( log_context, diff --git a/main.py b/main.py index f8c3cbd..b35a2a4 100644 --- a/main.py +++ b/main.py @@ -10,7 +10,7 @@ import sys import traceback from entrypoints.cli import run_cli_entrypoint -from notify_i18n_support import translate as t +from notify_i18n_support import build_strategy_display_name, translate as t from degraded_mode_support import ( format_trend_pool_source_logs as dm_format_trend_pool_source_logs, load_trend_universe_from_live_pool as dm_load_trend_universe_from_live_pool, @@ -501,8 +501,13 @@ def maybe_send_periodic_btc_status_report( btc_price, btc_snapshot, btc_target_ratio, + strategy_display_name=None, notifier_fn=None, ): + resolved_strategy_display_name = strategy_display_name or build_strategy_display_name(t)( + getattr(STRATEGY_RUNTIME, "profile", "crypto_leader_rotation"), + fallback_name="Crypto Leader Rotation", + ) return report_maybe_send_periodic_btc_status_report( state, tg_token, @@ -515,6 +520,7 @@ def maybe_send_periodic_btc_status_report( btc_price, btc_snapshot, btc_target_ratio, + resolved_strategy_display_name, translate_fn=t, separator=SEPARATOR, notifier_fn=notifier_fn, diff --git a/notify_i18n_support.py b/notify_i18n_support.py index 46464bc..24aabb6 100644 --- a/notify_i18n_support.py +++ b/notify_i18n_support.py @@ -13,6 +13,7 @@ "firestore_write_failed": "Firestore write failed: {error}", "telegram_send_failed": "Telegram send failed", "heartbeat_title": "💓 【Strategy Heartbeat】", + "strategy_label": "🧭 Strategy: {name}", "time_utc": "🕐 UTC Time", "total_equity": "💰 Total Equity", "trend_equity": "📈 Trend Holdings", @@ -128,6 +129,7 @@ "usdt_unavailable_for_trend_buy": "USDT unavailable for trend buy", "usdt_unavailable_for_btc_dca_buy": "USDT unavailable for BTC DCA buy", "btc_unavailable_for_dca_sell": "BTC unavailable for DCA sell", + "strategy_name_crypto_leader_rotation": "Crypto Leader Rotation", }, "zh": { "telegram_prefix": "🤖 加密量化助手", @@ -135,6 +137,7 @@ "firestore_write_failed": "Firestore 写入状态失败: {error}", "telegram_send_failed": "Telegram 发送失败", "heartbeat_title": "💓 【策略心跳】", + "strategy_label": "🧭 策略: {name}", "time_utc": "🕐 UTC 时间", "total_equity": "💰 总净值", "trend_equity": "📈 趋势层持仓", @@ -250,6 +253,7 @@ "usdt_unavailable_for_trend_buy": "USDT 不足,无法执行趋势买入", "usdt_unavailable_for_btc_dca_buy": "USDT 不足,无法执行 BTC 定投买入", "btc_unavailable_for_dca_sell": "BTC 不足,无法执行定投止盈卖出", + "strategy_name_crypto_leader_rotation": "加密领涨轮动", }, } @@ -261,12 +265,33 @@ def get_notify_lang() -> str: return DEFAULT_NOTIFY_LANG +def build_translator(lang: str): + def translator(key: str, **kwargs) -> str: + active_lang = lang if lang in SUPPORTED_NOTIFY_LANGS else DEFAULT_NOTIFY_LANG + template = _TEXTS.get(active_lang, _TEXTS[DEFAULT_NOTIFY_LANG]).get(key) + if template is None: + template = _TEXTS[DEFAULT_NOTIFY_LANG].get(key, key) + return template.format(**kwargs) if kwargs else template + + return translator + + def translate(key: str, **kwargs) -> str: - lang = get_notify_lang() - template = _TEXTS.get(lang, _TEXTS[DEFAULT_NOTIFY_LANG]).get(key) - if template is None: - template = _TEXTS[DEFAULT_NOTIFY_LANG].get(key, key) - return template.format(**kwargs) if kwargs else template + return build_translator(get_notify_lang())(key, **kwargs) + + +def build_strategy_display_name(translate_fn): + def strategy_display_name(profile: str, *, fallback_name: str | None = None) -> str: + key = f"strategy_name_{str(profile or '').strip()}" + translated = translate_fn(key) + if translated != key: + return translated + fallback = str(fallback_name or "").strip() + if fallback: + return fallback + return str(profile or "").strip() + + return strategy_display_name def build_telegram_message(text: str) -> str: diff --git a/reporting/status_reports.py b/reporting/status_reports.py index e037e9c..be5d26f 100644 --- a/reporting/status_reports.py +++ b/reporting/status_reports.py @@ -38,6 +38,7 @@ def maybe_send_periodic_btc_status_report( btc_price, btc_snapshot, btc_target_ratio, + strategy_display_name, *, translate_fn, separator, @@ -51,6 +52,7 @@ def maybe_send_periodic_btc_status_report( gate_text = translate_fn("gate_on") if btc_snapshot["regime_on"] else translate_fn("gate_off") text = ( f"{translate_fn('heartbeat_title')}\n" + f"{translate_fn('strategy_label', name=strategy_display_name)}\n" f"{translate_fn('time_utc')}: {now_utc.strftime('%Y-%m-%d %H:%M')}\n" f"{separator}\n" f"{translate_fn('total_equity')}: ${total_equity:.2f}\n" diff --git a/runtime_config_support.py b/runtime_config_support.py index 46ae2ce..c6ceb25 100644 --- a/runtime_config_support.py +++ b/runtime_config_support.py @@ -5,10 +5,12 @@ from datetime import datetime, timezone from typing import Any, Callable +from notify_i18n_support import build_strategy_display_name, build_translator, get_notify_lang from runtime_support import ExecutionRuntime from strategy_registry import ( BINANCE_PLATFORM, resolve_strategy_definition, + resolve_strategy_metadata, ) @@ -38,14 +40,25 @@ class CycleExecutionSettings: btc_status_report_interval_hours: int allow_new_trend_entries_on_degraded: bool strategy_profile: str + strategy_display_name: str + strategy_display_name_localized: str strategy_domain: str def load_cycle_execution_settings() -> CycleExecutionSettings: + notify_lang = get_notify_lang() strategy_definition = resolve_strategy_definition( os.getenv("STRATEGY_PROFILE"), platform_id=BINANCE_PLATFORM, ) + strategy_metadata = resolve_strategy_metadata( + strategy_definition.profile, + platform_id=BINANCE_PLATFORM, + ) + strategy_display_name_localized = build_strategy_display_name(build_translator(notify_lang))( + strategy_definition.profile, + fallback_name=strategy_metadata.display_name, + ) return CycleExecutionSettings( btc_status_report_interval_hours=max(1, min(24, get_env_int("BTC_STATUS_REPORT_INTERVAL_HOURS", 24))), allow_new_trend_entries_on_degraded=get_env_bool( @@ -53,6 +66,8 @@ def load_cycle_execution_settings() -> CycleExecutionSettings: False, ), strategy_profile=strategy_definition.profile, + strategy_display_name=strategy_metadata.display_name, + strategy_display_name_localized=strategy_display_name_localized, strategy_domain=strategy_definition.domain, ) @@ -65,9 +80,14 @@ def build_live_runtime( notifier: Callable[..., Any] | None = None, ) -> ExecutionRuntime: runtime_now = now_utc or datetime.now(timezone.utc) + cycle_settings = load_cycle_execution_settings() return ExecutionRuntime( dry_run=False, now_utc=runtime_now, + strategy_profile=cycle_settings.strategy_profile, + strategy_domain=cycle_settings.strategy_domain, + strategy_display_name=cycle_settings.strategy_display_name, + strategy_display_name_localized=cycle_settings.strategy_display_name_localized, api_key=os.getenv("BINANCE_API_KEY", ""), api_secret=os.getenv("BINANCE_API_SECRET", ""), tg_token=os.getenv("TG_TOKEN", ""), diff --git a/runtime_support.py b/runtime_support.py index 955f293..d299c1f 100644 --- a/runtime_support.py +++ b/runtime_support.py @@ -11,6 +11,10 @@ class ExecutionRuntime: dry_run: bool = False run_id: str = "" now_utc: Optional[datetime] = None + strategy_profile: str = "" + strategy_domain: str = "" + strategy_display_name: str = "" + strategy_display_name_localized: str = "" client: Any = None api_key: str = "" api_secret: str = "" @@ -38,8 +42,8 @@ def build_execution_report(runtime): platform="binance", deploy_target=os.getenv("LOG_DEPLOY_TARGET", "vps"), service_name=os.getenv("SERVICE_NAME", "binance-platform"), - strategy_profile=os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation"), - strategy_domain=os.getenv("STRATEGY_DOMAIN", "crypto"), + strategy_profile=str(runtime.strategy_profile or os.getenv("STRATEGY_PROFILE", "crypto_leader_rotation")), + strategy_domain=str(runtime.strategy_domain or os.getenv("STRATEGY_DOMAIN", "crypto")), run_id=str(runtime.run_id), run_source="github_actions" if os.getenv("GITHUB_RUN_ID") or os.getenv("GITHUB_ACTIONS") else "runtime", dry_run=bool(runtime.dry_run), @@ -74,6 +78,10 @@ def build_execution_report(runtime): "circuit_breaker_triggered": False, "degraded_mode_level": None, "upstream_pool_symbols": [], + "summary": { + "strategy_display_name": str(runtime.strategy_display_name or ""), + "strategy_display_name_localized": str(runtime.strategy_display_name_localized or ""), + }, }) return report diff --git a/strategy_registry.py b/strategy_registry.py index c589636..3918259 100644 --- a/strategy_registry.py +++ b/strategy_registry.py @@ -7,6 +7,7 @@ PlatformStrategyPolicy, StrategyDefinition, build_platform_profile_matrix, + get_catalog_strategy_metadata, get_enabled_profiles_for_platform, resolve_platform_strategy_definition, ) @@ -52,3 +53,12 @@ def resolve_strategy_definition( strategy_catalog=STRATEGY_CATALOG, policy=PLATFORM_POLICY, ) + + +def resolve_strategy_metadata( + raw_value: str | None, + *, + platform_id: str, +): + definition = resolve_strategy_definition(raw_value, platform_id=platform_id) + return get_catalog_strategy_metadata(STRATEGY_CATALOG, definition.profile) diff --git a/strategy_runtime.py b/strategy_runtime.py index 0757575..cdbc8e7 100644 --- a/strategy_runtime.py +++ b/strategy_runtime.py @@ -8,6 +8,7 @@ from quant_platform_kit.strategy_contracts import StrategyContext, StrategyDecision, StrategyEntrypoint from strategy_loader import load_strategy_entrypoint_for_profile +from strategy_registry import BINANCE_PLATFORM, resolve_strategy_metadata from trend_pool_support import get_default_live_pool_candidates as tp_get_default_live_pool_candidates @@ -115,7 +116,13 @@ def evaluate( return StrategyEvaluationResult( decision=decision, account_metrics=dict(account_metrics), - metadata={"strategy_profile": self.profile}, + metadata={ + "strategy_profile": self.profile, + "strategy_display_name": resolve_strategy_metadata( + self.profile, + platform_id=BINANCE_PLATFORM, + ).display_name, + }, ) diff --git a/tests/test_cycle_service.py b/tests/test_cycle_service.py index 48cfd35..638d662 100644 --- a/tests/test_cycle_service.py +++ b/tests/test_cycle_service.py @@ -72,7 +72,13 @@ def test_run_live_cycle_emits_structured_runtime_events(self): clear=False, ): report, _output_path = run_live_cycle( - runtime_builder=lambda: SimpleNamespace(run_id="run-001", dry_run=True), + runtime_builder=lambda: SimpleNamespace( + run_id="run-001", + dry_run=True, + strategy_profile="crypto_leader_rotation", + strategy_display_name="Crypto Leader Rotation", + strategy_display_name_localized="加密领涨轮动", + ), execute_cycle=lambda _runtime: { "status": "ok", "log_lines": ["line-1", "line-2"], @@ -96,6 +102,8 @@ def test_run_live_cycle_emits_structured_runtime_events(self): end_log = json.loads(observed["printed"][2]) self.assertEqual(start_log["event"], "strategy_cycle_started") self.assertEqual(start_log["strategy_profile"], "crypto_leader_rotation") + self.assertEqual(start_log["strategy_display_name"], "Crypto Leader Rotation") + self.assertEqual(start_log["strategy_display_name_localized"], "加密领涨轮动") self.assertEqual(start_log["run_id"], "run-001") self.assertEqual(end_log["event"], "strategy_cycle_completed") self.assertEqual(end_log["status"], "ok") diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 2d81fe3..374159f 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -33,6 +33,8 @@ def test_load_cycle_execution_settings_clamps_interval_and_reads_degraded_flag(s self.assertEqual(settings.btc_status_report_interval_hours, 24) self.assertTrue(settings.allow_new_trend_entries_on_degraded) self.assertEqual(settings.strategy_profile, DEFAULT_STRATEGY_PROFILE) + self.assertEqual(settings.strategy_display_name, "Crypto Leader Rotation") + self.assertEqual(settings.strategy_display_name_localized, "Crypto Leader Rotation") self.assertEqual(settings.strategy_domain, CRYPTO_DOMAIN) def test_load_cycle_execution_settings_rejects_unknown_strategy_profile(self): @@ -73,6 +75,8 @@ def test_build_live_runtime_reads_env_and_preserves_injected_hooks(self): self.assertEqual(runtime.api_secret, "api-secret") self.assertEqual(runtime.tg_token, "tg-token") self.assertEqual(runtime.tg_chat_id, "chat-id") + self.assertEqual(runtime.strategy_profile, DEFAULT_STRATEGY_PROFILE) + self.assertEqual(runtime.strategy_display_name, "Crypto Leader Rotation") self.assertIs(runtime.state_loader, state_loader) self.assertIs(runtime.state_writer, state_writer) self.assertIs(runtime.notifier, notifier) diff --git a/tests/test_status_reports.py b/tests/test_status_reports.py index eafc1f2..b613050 100644 --- a/tests/test_status_reports.py +++ b/tests/test_status_reports.py @@ -5,6 +5,7 @@ append_rotation_summary, build_btc_manual_hint, get_periodic_report_bucket, + maybe_send_periodic_btc_status_report, ) @@ -86,5 +87,52 @@ def test_append_rotation_summary_formats_expected_lines(self): ) + def test_periodic_status_report_includes_strategy_display_name(self): + state = {} + observed = [] + + def translate_strategy(key, **kwargs): + mapping = { + "heartbeat_title": "heartbeat", + "strategy_label": "strategy={name}", + "time_utc": "time", + "total_equity": "equity", + "trend_equity": "trend", + "btc_price": "btc", + "ahr999": "ahr", + "zscore": "z", + "zscore_threshold": "threshold", + "btc_target": "target", + "btc_gate": "gate", + "gate_on": "on", + "gate_off": "off", + "manual_hint": "hint", + "manual_hint_low_value": "low", + } + template = mapping[key] + return template.format(**kwargs) if kwargs else template + + maybe_send_periodic_btc_status_report( + state, + "token", + "chat-id", + SimpleNamespace(hour=8, strftime=lambda fmt: "2026-03-29 08:00" if "%H:%M" in fmt else "20260329"), + 4, + 1000.0, + 250.0, + 0.01, + 65000.0, + {"ahr999": 0.7, "zscore": 1.2, "sell_trigger": 3.0, "regime_on": True}, + 0.3, + "加密领涨轮动", + translate_fn=translate_strategy, + separator="---", + notifier_fn=observed.append, + ) + + self.assertTrue(observed) + self.assertIn("strategy=加密领涨轮动", observed[0]) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_strategy_runtime.py b/tests/test_strategy_runtime.py index acce954..45c8745 100644 --- a/tests/test_strategy_runtime.py +++ b/tests/test_strategy_runtime.py @@ -101,6 +101,7 @@ def test_strategy_runtime_evaluate_returns_decision_with_buy_sell_diagnostics(se ) diagnostics = evaluation.decision.diagnostics + self.assertEqual(evaluation.metadata["strategy_display_name"], "Crypto Leader Rotation") self.assertIn("planned_trend_buys", diagnostics) self.assertIn("eligible_buy_symbols", diagnostics) self.assertIn("sell_reasons", diagnostics)