Skip to content

Commit e1100cb

Browse files
authored
Polish snapshot heartbeat i18n (#41)
1 parent 880ddc1 commit e1100cb

4 files changed

Lines changed: 120 additions & 8 deletions

File tree

application/rebalance_service.py

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,31 @@ def _localize_notification_text(text: str, *, translator) -> str:
131131
return localized
132132

133133

134+
def _render_notification_context_text(
135+
notification_context: Mapping[str, object] | None,
136+
*,
137+
translator,
138+
fallback: str = "",
139+
) -> str:
140+
if not isinstance(notification_context, Mapping):
141+
return fallback
142+
key = str(notification_context.get("code") or "").strip()
143+
if not key:
144+
return fallback
145+
params = dict(notification_context.get("params") or {})
146+
rendered = translator(key, **params)
147+
return fallback if rendered == key else str(rendered)
148+
149+
150+
def _translate_snapshot_guard_decision(decision: object, *, translator) -> str:
151+
value = str(decision or "").strip()
152+
if not value:
153+
return ""
154+
key = f"snapshot_guard_decision_{value}"
155+
translated = translator(key)
156+
return value if translated == key else str(translated)
157+
158+
134159
def _split_detail_segment(text: str) -> list[str]:
135160
value = str(text or "").strip()
136161
if not value:
@@ -585,13 +610,37 @@ def run_strategy_core(
585610
decision = signal_metadata.get("snapshot_guard_decision")
586611
no_op_reason = signal_metadata.get("no_op_reason")
587612
fail_reason = signal_metadata.get("fail_reason")
588-
no_op_text = config.translator("no_trades")
589-
if decision:
590-
no_op_text = f"{no_op_text} | {_localize_notification_text(f'decision={decision}', translator=config.translator)}"
591-
if no_op_reason:
592-
no_op_text = f"{no_op_text} | {_localize_notification_text(f'reason={no_op_reason}', translator=config.translator)}"
593-
if fail_reason:
594-
no_op_text = f"{no_op_text} | {_localize_notification_text(f'fail_reason={fail_reason}', translator=config.translator)}"
613+
notification_context = signal_metadata.get("notification_context")
614+
status_context = (
615+
notification_context.get("status")
616+
if isinstance(notification_context, Mapping)
617+
else None
618+
)
619+
rendered_status = _render_notification_context_text(
620+
status_context,
621+
translator=config.translator,
622+
fallback="",
623+
)
624+
no_op_segments = [config.translator("no_trades")]
625+
if rendered_status:
626+
no_op_segments.append(rendered_status)
627+
else:
628+
if decision:
629+
no_op_segments.append(
630+
config.translator(
631+
"snapshot_decision_detail",
632+
value=_translate_snapshot_guard_decision(decision, translator=config.translator),
633+
)
634+
)
635+
if no_op_reason:
636+
no_op_segments.append(
637+
_localize_notification_text(f"reason={no_op_reason}", translator=config.translator)
638+
)
639+
if fail_reason:
640+
no_op_segments.append(
641+
_localize_notification_text(f"fail_reason={fail_reason}", translator=config.translator)
642+
)
643+
no_op_text = " | ".join(segment for segment in no_op_segments if str(segment).strip())
595644
no_op_text = "\n".join(_split_labeled_text(no_op_text))
596645
record = build_reconciliation_record(
597646
strategy_profile=signal_metadata.get("strategy_profile"),

notifications/telegram.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@
5050
"partial_sell_batch": "⚠️ 卖单部分成交 {count}个标的: {details}",
5151
"no_equity": "❌ 无净值",
5252
"signal_label": "信号",
53+
"signal_monthly_snapshot_waiting": "月度快照节奏 | 等待进入执行窗口",
54+
"status_monthly_snapshot_waiting_window": "不执行 | 原因=当前不在月度执行窗口 | 快照日期={snapshot_as_of} | 允许日期={allowed_dates}",
55+
"status_no_execution_window_after_snapshot": "不执行 | 原因=快照后没有可用执行窗口 | 快照日期={snapshot_as_of}",
56+
"snapshot_guard_decision_proceed": "继续",
57+
"snapshot_guard_decision_no_op": "不执行",
58+
"snapshot_guard_decision_fail_closed": "关闭执行",
5359
"market_status_risk_on": "🚀 风险开启({asset})",
5460
"market_status_delever": "🛡️ 降杠杆({asset})",
5561
"signal_risk_on": "SOXL 站上 {window} 日均线,持有 SOXL,交易层风险仓位 {ratio}",
@@ -132,6 +138,12 @@
132138
"partial_sell_batch": "⚠️ Partial sell fills for {count} symbols: {details}",
133139
"no_equity": "❌ No equity",
134140
"signal_label": "Signal",
141+
"signal_monthly_snapshot_waiting": "monthly snapshot cadence | waiting inside execution window",
142+
"status_monthly_snapshot_waiting_window": "no-op | reason=outside monthly execution window | snapshot_as_of={snapshot_as_of} | allowed={allowed_dates}",
143+
"status_no_execution_window_after_snapshot": "no-op | reason=no execution window after snapshot | snapshot_as_of={snapshot_as_of}",
144+
"snapshot_guard_decision_proceed": "proceed",
145+
"snapshot_guard_decision_no_op": "no_op",
146+
"snapshot_guard_decision_fail_closed": "fail_closed",
135147
"market_status_risk_on": "🚀 RISK-ON ({asset})",
136148
"market_status_delever": "🛡️ DE-LEVER ({asset})",
137149
"signal_risk_on": "SOXL above {window}d MA, hold SOXL, risk {ratio}",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ac550fee3ca33bbf28c8a886848dede4e4e09a1b
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@0f4373f69c9d9f800eda476ff213b7013e51aba4
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9f55c58b6854ae700c47a684262eec10e763306a
55
pandas
66
numpy
77
requests

tests/test_rebalance_service.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,3 +368,54 @@ def disconnect(self):
368368
assert payload_path is not None
369369
payload = json.loads(payload_path.read_text(encoding="utf-8"))
370370
assert payload["no_op_reason"] == "outside_execution_window"
371+
372+
373+
def test_run_strategy_core_prefers_structured_noop_status_in_zh():
374+
observed = {"messages": []}
375+
376+
class FakeIB:
377+
def isConnected(self):
378+
return True
379+
380+
def disconnect(self):
381+
return None
382+
383+
result = run_strategy_core(
384+
connect_ib=lambda: FakeIB(),
385+
get_current_portfolio=lambda _ib: ({}, {"equity": 1000.0, "buying_power": 500.0}),
386+
compute_signals=lambda _ib, _holdings: (
387+
None,
388+
"monthly snapshot cadence | waiting inside execution window",
389+
False,
390+
"no-op | reason=outside_monthly_execution_window",
391+
{
392+
"strategy_profile": "mega_cap_leader_rotation_top50_balanced",
393+
"trade_date": "2026-04-22",
394+
"snapshot_as_of": "2026-04-16",
395+
"snapshot_guard_decision": "proceed",
396+
"no_op_reason": "outside_monthly_execution_window snapshot=2026-04-16 allowed=2026-04-17,2026-04-20,2026-04-21",
397+
"managed_symbols": ("AAPL", "MSFT", "BOXX"),
398+
"dry_run_only": True,
399+
"notification_context": {
400+
"signal": {"code": "signal_monthly_snapshot_waiting", "params": {}},
401+
"status": {
402+
"code": "status_monthly_snapshot_waiting_window",
403+
"params": {
404+
"snapshot_as_of": "2026-04-16",
405+
"allowed_dates": "2026-04-17, 2026-04-20, 2026-04-21",
406+
},
407+
},
408+
},
409+
},
410+
),
411+
execute_rebalance=lambda *_args, **_kwargs: [],
412+
send_tg_message=lambda message: observed["messages"].append(message),
413+
translator=build_translator("zh"),
414+
separator="---",
415+
strategy_display_name="Mega Cap Top50 平衡龙头轮动",
416+
)
417+
418+
assert result.result == "OK - heartbeat"
419+
assert observed["messages"]
420+
assert "当前不在月度执行窗口" in observed["messages"][0]
421+
assert "决策=proceed" not in observed["messages"][0]

0 commit comments

Comments
 (0)