diff --git a/README.md b/README.md index 45ab0e4..e361213 100644 --- a/README.md +++ b/README.md @@ -137,3 +137,5 @@ uv run --no-sync python scripts/check_qpk_pin_consistency.py ## License See [LICENSE](LICENSE). + +Display/notification naming configuration: [display names](docs/display-names.md). diff --git a/docs/display-names.md b/docs/display-names.md new file mode 100644 index 0000000..c5a6d74 --- /dev/null +++ b/docs/display-names.md @@ -0,0 +1,11 @@ +# Configurable display names + +QRS `platform-config.json` owns public strategy/platform labels. `display-names.config.json` owns display-only account aliases, lanes, legacy labels and aliases. `python/scripts/build_display_names.py` combines them into one fixed mapping; it never changes trading catalog metadata or runtime configuration hashes. + +Run `python3 python/scripts/build_display_names.py --repos-root /path/to/source/checkouts` from QRS, then `--check` with the same arguments. Review and commit each repository separately. Consumer `display_names.py/json` files are generated snapshots, not independently maintained maps. They avoid upgrading pinned trading dependencies just for labels. No runtime fetch is performed. Refresh the snapshots together when labels change. + +Python: optional `QSL_DISPLAY_NAMES_JSON` JSON object. Console: optional `VITE_QSL_DISPLAY_NAMES_JSON` at build time. Example (synthetic): `{"strategies":{"custom_profile":{"zh":"ETF-轮动-202610","en":"ETF-Rotation-202610"}},"accounts":{"binance":{"crypto_combo":"live"}}}`. Invalid JSON falls back to defaults; unknown labels retain the supplied fallback. Set private aliases only in the existing protected configuration, never in public source or PR bodies. + +Precedence: explicit override, generated map, caller fallback, unchanged technical key. Locale falls back to English. New strategy/variant display names use `--` when the actual origin month is established. Existing reviewed names stay until an explicit dated name is provided; do not invent a month or rename a technical ID to fit the pattern. Keep legacy aliases/descriptions. `crypto_combo` is displayed as `live` only under Binance; `paper` remains `paper`. Binance notifications can explicitly select another display alias with `QSL_NOTIFICATION_ACCOUNT_ALIAS` (for example `paper` in an isolated preview). + +Resolved names are for display only. Never pass them to trading selection, account selection, target IDs, fingerprints, release/continuity bindings, Secret names, storage/object keys, claim keys or notification deduplication. Workflow/environment IDs and report profile IDs retain their operational meaning. CI and source merges do not establish production adoption. Trading runtime changes require their separate reviewed deployment; this naming work deploys no trading service. diff --git a/docs/naming-trading-diff.zh-CN.md b/docs/naming-trading-diff.zh-CN.md new file mode 100644 index 0000000..b7dfcd9 --- /dev/null +++ b/docs/naming-trading-diff.zh-CN.md @@ -0,0 +1,14 @@ +# 交易差异说明:LongBridgePlatform + +基线:`e991261738006ddeaee9b085ad9b783ce8b6dcf7`。作者自审与 AST 检查,不声称独立人工审查。 + +只改通知显示名解析、固定显示配置副本、测试和说明。账户 key、策略 profile、RuntimeTarget、fingerprint、release/continuity binding、Secret 名称、存储 key、通知去重键均不改。交易依赖及锁文件未改;SOXL 0.679 / 总仓位 0.97 / 现金 0.03 未改。不部署交易服务。 + +现有源码中的券商、订单、风险、状态和报告持久化调用表达式及其参数比较结果: + +| 文件 | 敏感调用数 | 参数不变 | +|---|---:|---| +| `notifications/display_names.py` | 0 | 是 | +| `notifications/telegram.py` | 0 | 是 | + +新增解析器只读同目录的生成 JSON 及可选显示环境配置,不联网、不调用券商、不写持久状态。显示值不得作为机器标识再输入交易调用。固定副本来自 QRS 的生成器,避免只为改名升级交易依赖。PR 保持 draft;即使 CI 通过,也未批准实际运行版本采用。 diff --git a/notifications/display_names.json b/notifications/display_names.json new file mode 100644 index 0000000..5f2e628 --- /dev/null +++ b/notifications/display_names.json @@ -0,0 +1,241 @@ +{ + "account_aliases": { + "hk": { + "en": "HK", + "zh": "香港" + }, + "live": { + "en": "live", + "zh": "实盘" + }, + "paper": { + "en": "paper", + "zh": "模拟" + }, + "sg": { + "en": "SG", + "zh": "新加坡" + }, + "soxl": { + "en": "SOXL", + "zh": "SOXL" + }, + "tqqq": { + "en": "TQQQ", + "zh": "TQQQ" + } + }, + "accounts": { + "binance": { + "Binance": { + "en": "live", + "zh": "实盘" + }, + "binance": { + "en": "live", + "zh": "实盘" + }, + "crypto_combo": { + "en": "live", + "zh": "实盘" + }, + "default": { + "en": "live", + "zh": "实盘" + } + } + }, + "domains": { + "cn_equity": { + "en": "CN A-share", + "zh": "A股" + }, + "crypto": { + "en": "Crypto", + "zh": "加密" + }, + "hk_equity": { + "en": "HK Equity", + "zh": "港股" + }, + "us_equity": { + "en": "US Equity", + "zh": "美股" + } + }, + "lanes": { + "dry_run": { + "en": "dry run", + "zh": "演练" + }, + "live": { + "en": "live", + "zh": "实盘" + }, + "paper": { + "en": "paper", + "zh": "模拟" + }, + "research": { + "en": "research", + "zh": "研究" + }, + "shadow": { + "en": "shadow", + "zh": "影子" + } + }, + "platforms": { + "binance": "Binance", + "firstrade": "Firstrade", + "ibkr": "IBKR", + "longbridge": "LongBridge", + "qmt": "QMT", + "schwab": "Schwab" + }, + "senders": { + "quant_sentinel": { + "en": "QuantSentinel", + "zh": "量化哨兵" + } + }, + "strategies": { + "cn_chinext_growth_momentum_quality": { + "en": "CN ChiNext Growth Momentum Quality", + "zh": "创业板成长动量质量" + }, + "cn_chinext_growth_momentum_quality_snapshot": { + "en": "CN ChiNext Growth Quality Snapshot", + "zh": "创业板成长质量快照" + }, + "cn_chinext_tactical_rotation": { + "en": "CN ChiNext Tactical Rotation", + "zh": "创业板战术轮动" + }, + "cn_csi500_multi_factor_snapshot": { + "en": "CN CSI 500 Multi-Factor Snapshot", + "zh": "A股中证500多因子" + }, + "cn_dividend_quality_snapshot": { + "en": "CN Dividend Quality", + "zh": "A股红利质量" + }, + "cn_equity_combo": { + "en": "CN Alpha Combo", + "zh": "A股进取组合" + }, + "cn_index_etf_tactical_rotation": { + "en": "CN Index ETF Tactical Rotation", + "zh": "A股宽基ETF战术轮动" + }, + "cn_industry_etf_rotation": { + "en": "CN Industry ETF Rotation", + "zh": "A股行业ETF轮动" + }, + "cn_industry_etf_rotation_aggressive": { + "en": "CN ETF Rotation", + "zh": "A股ETF轮动" + }, + "cn_star_growth_momentum_quality": { + "en": "CN STAR Growth Momentum Quality", + "zh": "科创板成长动量质量" + }, + "cn_stock_momentum_rotation": { + "en": "CN Stock Momentum Rotation", + "zh": "A股股票动量轮动" + }, + "crypto_btc_dca": { + "en": "BTC DCA", + "zh": "BTC定投" + }, + "crypto_equity_combo": { + "en": "Crypto Core Combo", + "zh": "加密动量组合" + }, + "crypto_leader_rotation": { + "en": "Crypto Leader Rotation", + "zh": "加密领涨轮动" + }, + "crypto_live_pool_rotation": { + "en": "Crypto Pool Rotation", + "zh": "加密池轮动" + }, + "crypto_trend_rotation": { + "en": "Altcoin Trend", + "zh": "山寨趋势轮动" + }, + "global_etf_confidence_vol_gate": { + "en": "Global ETF Confidence Vol Gate", + "zh": "全球 ETF 置信波动门控" + }, + "global_etf_rotation": { + "en": "Global ETF Rotation", + "zh": "全球ETF轮动" + }, + "hk_equity_combo": { + "en": "HK Core Combo", + "zh": "港股恒生组合" + }, + "hk_global_etf_tactical_rotation": { + "en": "HK ETF Tactical Rotation", + "zh": "港股ETF战术轮动" + }, + "hk_low_vol_dividend_quality_snapshot": { + "en": "HK Dividend Quality", + "zh": "港股红利质量" + }, + "ibit_smart_dca": { + "en": "IBIT Bitcoin DCA", + "zh": "IBIT比特币定投" + }, + "nasdaq_sp500_smart_dca": { + "en": "NASDAQ/S&P 500 DCA", + "zh": "纳指标普定投" + }, + "qqq_tech_enhancement": { + "en": "Tech/Communication Pullback Enhancement", + "zh": "科技通信回调增强" + }, + "russell_top50_leader_rotation": { + "en": "Russell Top50 Leaders", + "zh": "罗素Top50领涨" + }, + "soxl_soxx_core_only_p2_v7_longterm_compounding_cash_reserve": { + "en": "SOXL/SOXX Core Compounding (Cash Reserve)", + "zh": "SOXL/SOXX 核心复利(现金保留)" + }, + "soxl_soxx_trend_income": { + "en": "Semiconductor Trend Income", + "zh": "半导体趋势收益" + }, + "tech_communication_pullback_enhancement": { + "en": "Tech/Communication Pullback Enhancement", + "zh": "科技通信回调增强" + }, + "tecl_xlk_trend_income": { + "en": "TECL/XLK Trend Income", + "zh": "TECL/XLK趋势收益" + }, + "tqqq_growth_income": { + "en": "NASDAQ Growth Income", + "zh": "纳斯达克增长收益" + }, + "us_equity_combo": { + "en": "US Core Income Combo", + "zh": "美股核心收益组合" + }, + "us_equity_combo_core": { + "en": "US Core Combo", + "zh": "美股核心组合" + }, + "us_equity_combo_leveraged": { + "en": "US Alpha Combo", + "zh": "美股加速组合" + } + }, + "strategy_aliases": { + "bitcoin_etf_smart_dca": "ibit_smart_dca", + "global_etf_confidence_vol_gate": "global_etf_rotation", + "global_macro_etf_rotation": "global_etf_rotation" + } +} diff --git a/notifications/display_names.py b/notifications/display_names.py new file mode 100644 index 0000000..a045cee --- /dev/null +++ b/notifications/display_names.py @@ -0,0 +1,54 @@ +"""Display-only resolver; generated defaults come from QRS platform-config.json. + +Never use resolved names for execution identity, bindings, keys or deduplication. +QSL_DISPLAY_NAMES_JSON accepts an optional JSON object with per-section overrides. +Invalid overrides fall back to bundled names so an urgent alert can still render. +""" +from __future__ import annotations + +import json +import os +from functools import lru_cache +from pathlib import Path + + +@lru_cache(maxsize=1) +def _defaults(): + return json.loads(Path(__file__).with_suffix(".json").read_text(encoding="utf-8")) + + +@lru_cache(maxsize=8) +def _overrides(raw): + try: + value = json.loads(raw or "{}") + except (ValueError, TypeError): + return {} + return value if isinstance(value, dict) else {} + + +def _text(value, locale): + if isinstance(value, dict): + language = "zh" if str(locale).lower().startswith("zh") else "en" + value = value.get(language) or value.get("en") + return value.strip() if isinstance(value, str) else "" + + +def display_name(kind, key, locale="en", *, fallback="", platform=""): + """Resolve an explicit display key, retaining unknown operator labels.""" + key = str(key or "").strip() + defaults = _defaults() + overrides = _overrides(os.environ.get("QSL_DISPLAY_NAMES_JSON", "")) + aliases = {**defaults.get("strategy_aliases", {}), **(overrides.get("strategy_aliases", {}) if isinstance(overrides.get("strategy_aliases"), dict) else {})} + canonical = aliases.get(key, key) if kind == "strategies" else key + if not isinstance(canonical, str): + canonical = key + for source in (overrides, defaults): + section = source.get(kind, {}) + if kind == "accounts" and isinstance(section, dict): + section = section.get(str(platform).lower(), {}) + if isinstance(section, dict): + first, second = (key, canonical) if source is overrides else (canonical, key) + result = _text(section.get(first), locale) or _text(section.get(second), locale) + if result: + return result + return str(fallback or key) diff --git a/notifications/telegram.py b/notifications/telegram.py index 4e77940..d9714e6 100644 --- a/notifications/telegram.py +++ b/notifications/telegram.py @@ -2,6 +2,8 @@ from __future__ import annotations +from .display_names import display_name + from quant_platform_kit.common.operational_notification_localization import resolve_operational_notification_locale import re @@ -43,7 +45,7 @@ def _break_telegram_market_symbol_auto_links(value) -> str: I18N = { "zh": { - "runtime_failure_title": "⚠️ LongBridge 策略运行失败", + "runtime_failure_title": "⚠️ {platform} 策略运行失败", "runtime_failure_context": "运行目标:{context}", "runtime_failure_result": "本次运行未正常结束,请查看最新执行报告。", "runtime_failure_action": "下一步:检查账户授权、连接及最新执行报告。", @@ -181,26 +183,6 @@ def _break_telegram_market_symbol_auto_links(value) -> str: "signal_reduce": "减仓信号", "signal_exit": "离场信号", "signal_idle": "等待信号", - "strategy_name_soxl_soxx_trend_income": "SOXL/SOXX 半导体趋势收益", - "strategy_name_tqqq_growth_income": "TQQQ 增长收益", - "strategy_name_global_etf_confidence_vol_gate": "全球 ETF 置信波动门控", - "strategy_name_global_etf_rotation": "全球 ETF 轮动", - "strategy_name_tech_communication_pullback_enhancement": "科技通信回调增强", - "strategy_name_qqq_tech_enhancement": "科技通信回调增强", - "strategy_name_russell_top50_leader_rotation": "罗素 Top50 领涨轮动", - "strategy_name_ibit_smart_dca": "IBIT 比特币 ETF 智能定投", - "strategy_name_nasdaq_sp500_smart_dca": "纳指100 / 标普500 智能定投", - "strategy_name_us_equity_combo": "美股核心组合", - "strategy_name_us_equity_combo_leveraged": "美股Alpha组合", - "strategy_name_hk_global_etf_tactical_rotation": "港股全球 ETF 战术轮动", - "strategy_name_hk_low_vol_dividend_quality_snapshot": "港股低波股息质量快照", - "strategy_name_cn_industry_etf_rotation": "A股行业ETF轮动", - "strategy_name_cn_industry_etf_rotation_aggressive": "A股行业ETF轮动(进取)", - "strategy_name_cn_stock_momentum_rotation": "A股股票动量轮动", - "strategy_name_cn_dividend_quality_snapshot": "A股股息质量快照", - "strategy_name_cn_csi500_multi_factor_snapshot": "A股中证500多因子", - "strategy_name_cn_index_etf_tactical_rotation": "A股指数ETF战术轮动", - "strategy_name_cn_equity_combo": "A股组合策略", "strategy_plugin_line": "🧩 插件:{plugin} | 启用:{enabled} | 状态:{route} | 提醒:{action}", "strategy_plugin_enabled_true": "是", "strategy_plugin_enabled_false": "否", @@ -260,7 +242,7 @@ def _break_telegram_market_symbol_auto_links(value) -> str: "strategy_plugin_guidance_taco_rebound_shadow_taco_rebound_notify_manual_review": "TACO 仅提示可能的反弹窗口;可考虑小仓位、分批、预设止损/失效条件的人工博弈,不建议一次性满仓。", }, "en": { - "runtime_failure_title": "⚠️ LongBridge strategy run failed", + "runtime_failure_title": "⚠️ {platform} strategy run failed", "runtime_failure_context": "Target: {context}", "runtime_failure_result": "The run did not finish successfully; check the latest execution report.", "runtime_failure_action": "Next: Check account authorization, connectivity and the latest execution report.", @@ -398,26 +380,6 @@ def _break_telegram_market_symbol_auto_links(value) -> str: "signal_reduce": "Reduce Signal", "signal_exit": "Exit Signal", "signal_idle": "Idle", - "strategy_name_soxl_soxx_trend_income": "SOXL/SOXX Semiconductor Trend Income", - "strategy_name_tqqq_growth_income": "TQQQ Growth Income", - "strategy_name_global_etf_confidence_vol_gate": "Global ETF Confidence Vol Gate", - "strategy_name_global_etf_rotation": "Global ETF Rotation", - "strategy_name_tech_communication_pullback_enhancement": "Tech/Communication Pullback Enhancement", - "strategy_name_qqq_tech_enhancement": "Tech/Communication Pullback Enhancement", - "strategy_name_russell_top50_leader_rotation": "Russell Top50 Leader Rotation", - "strategy_name_ibit_smart_dca": "IBIT Smart DCA", - "strategy_name_nasdaq_sp500_smart_dca": "Nasdaq 100 / S&P 500 Smart DCA", - "strategy_name_us_equity_combo": "US Equity Combo", - "strategy_name_us_equity_combo_leveraged": "US Equity Combo Leveraged", - "strategy_name_hk_global_etf_tactical_rotation": "HK Global ETF Tactical Rotation", - "strategy_name_hk_low_vol_dividend_quality_snapshot": "HK Low-Vol Dividend Quality Snapshot", - "strategy_name_cn_industry_etf_rotation": "CN Industry ETF Rotation", - "strategy_name_cn_industry_etf_rotation_aggressive": "CN Industry ETF Rotation (Aggressive)", - "strategy_name_cn_stock_momentum_rotation": "CN Stock Momentum Rotation", - "strategy_name_cn_dividend_quality_snapshot": "CN Dividend Quality Snapshot", - "strategy_name_cn_csi500_multi_factor_snapshot": "CN CSI 500 Multi-Factor Snapshot", - "strategy_name_cn_index_etf_tactical_rotation": "CN Index ETF Tactical Rotation", - "strategy_name_cn_equity_combo": "CN Equity Combo", "strategy_plugin_line": "🧩 Plugin: {plugin} | enabled: {enabled} | status: {route} | notice: {action}", "strategy_plugin_enabled_true": "yes", "strategy_plugin_enabled_false": "no", @@ -491,9 +453,14 @@ def _break_telegram_market_symbol_auto_links(value) -> str: def build_translator(lang): active_lang = resolve_operational_notification_locale(lang) def translate(key, **kwargs): + if key.startswith("strategy_name_"): + profile = key.removeprefix("strategy_name_") + return display_name("strategies", profile, active_lang, fallback=key) template = I18N[active_lang].get(key, key) + template = template.replace("{platform}", display_name("platforms", "longbridge", active_lang)) return template.format(**kwargs) if kwargs else template + translate.notification_locale = active_lang return translate @@ -514,6 +481,9 @@ def strategy_display_name( metadata: Any = None, locale: str | None = None, ) -> str: + configured = display_name("strategies", profile, locale or getattr(translate_fn, "notification_locale", "en")) + if configured != profile: + return configured if metadata is not None and _resolve_strategy_display_name is not None: return _resolve_strategy_display_name( locale or "en", diff --git a/tests/test_display_names.py b/tests/test_display_names.py new file mode 100644 index 0000000..5265518 --- /dev/null +++ b/tests/test_display_names.py @@ -0,0 +1,40 @@ +import importlib.util +import json +from pathlib import Path +from unittest.mock import patch + + +def load_resolver(): + root = Path(__file__).resolve().parents[1] + path = root / "notifications/display_names.py" + spec = importlib.util.spec_from_file_location("display_names_under_test", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.display_name + + +def test_display_mapping_and_unknown_names_do_not_rewrite_inputs(): + resolve = load_resolver() + identity = {"platform": "binance", "account_key": "crypto_combo", "strategy_profile": "crypto_live_pool_rotation"} + original = dict(identity) + assert resolve("accounts", identity["account_key"], platform=identity["platform"]) == "live" + assert resolve("accounts", "paper", platform="binance") == "paper" + assert resolve("accounts", "crypto_combo", "zh", platform="binance") == "实盘" + assert resolve("lanes", "paper", "zh") == "模拟" + assert resolve("accounts", "crypto_combo", platform="other") == "crypto_combo" + assert resolve("strategies", identity["strategy_profile"], "en") == "Crypto Pool Rotation" + assert resolve("strategies", identity["strategy_profile"], "zh-CN") == "加密池轮动" + assert resolve("strategies", "custom", fallback="Operator label") == "Operator label" + assert resolve("strategies", "global_macro_etf_rotation", "en") == "Global ETF Rotation" + assert identity == original + + +def test_display_overrides_and_bad_configuration_keep_alerts_renderable(): + resolve = load_resolver() + override = {"strategies": {"crypto_live_pool_rotation": {"zh": "加密-轮动-202609", "en": "Crypto-Rotation-202609"}}, "accounts": {"binance": {"crypto_combo": "custom live"}}} + with patch.dict("os.environ", {"QSL_DISPLAY_NAMES_JSON": json.dumps(override)}): + assert resolve("strategies", "crypto_live_pool_rotation", "en") == "Crypto-Rotation-202609" + assert resolve("accounts", "crypto_combo", platform="binance") == "custom live" + for raw in ("{", "[]", "null", '{"strategy_aliases":{"custom":[]},"strategies":[]}'): + with patch.dict("os.environ", {"QSL_DISPLAY_NAMES_JSON": raw}): + assert resolve("strategies", "custom", fallback="Urgent") == "Urgent" diff --git a/tests/test_notifications.py b/tests/test_notifications.py index 73dcbbe..949c29c 100644 --- a/tests/test_notifications.py +++ b/tests/test_notifications.py @@ -143,16 +143,16 @@ def test_build_strategy_display_name_supports_i18n(self): en_translate = build_translator("en") zh_name = build_strategy_display_name(zh_translate)("soxl_soxx_trend_income") en_name = build_strategy_display_name(en_translate)("soxl_soxx_trend_income") - self.assertEqual(zh_name, "SOXL/SOXX 半导体趋势收益") - self.assertEqual(en_name, "SOXL/SOXX Semiconductor Trend Income") + self.assertEqual(zh_name, "半导体趋势收益") + self.assertEqual(en_name, "Semiconductor Trend Income") def test_supported_strategy_profiles_have_translated_names(self): zh_name = build_strategy_display_name(build_translator("zh")) en_name = build_strategy_display_name(build_translator("en")) - self.assertEqual(zh_name("global_etf_confidence_vol_gate"), "全球 ETF 置信波动门控") - self.assertEqual(en_name("global_etf_confidence_vol_gate"), "Global ETF Confidence Vol Gate") - self.assertEqual(zh_name("hk_global_etf_tactical_rotation"), "港股全球 ETF 战术轮动") - self.assertEqual(en_name("hk_global_etf_tactical_rotation"), "HK Global ETF Tactical Rotation") + self.assertEqual(zh_name("global_etf_confidence_vol_gate"), "全球ETF轮动") + self.assertEqual(en_name("global_etf_confidence_vol_gate"), "Global ETF Rotation") + self.assertEqual(zh_name("hk_global_etf_tactical_rotation"), "港股ETF战术轮动") + self.assertEqual(en_name("hk_global_etf_tactical_rotation"), "HK ETF Tactical Rotation") for profile in SUPPORTED_STRATEGY_PROFILES: self.assertNotEqual(zh_name(profile), profile) @@ -178,7 +178,7 @@ def test_heartbeat_signal_snapshot_localizes_price_source(self): note_logs=(), translator=build_translator("zh"), separator="━━━━━━━━━━━━━━━━━━", - strategy_display_name="SOXL/SOXX 半导体趋势收益", + strategy_display_name="半导体趋势收益", dry_run_only=False, ) @@ -517,7 +517,7 @@ def test_heartbeat_localizes_strategy_diagnostics_and_source_input_status(self): note_logs=(), translator=build_translator("zh"), separator="━━━━━━━━━━━━━━━━━━", - strategy_display_name="罗素 Top50 领涨轮动", + strategy_display_name="罗素Top50领涨", dry_run_only=False, )