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
63 changes: 56 additions & 7 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,31 @@ def _localize_notification_text(text: str, *, translator) -> str:
return localized


def _render_notification_context_text(
notification_context: Mapping[str, object] | None,
*,
translator,
fallback: str = "",
) -> str:
if not isinstance(notification_context, Mapping):
return fallback
key = str(notification_context.get("code") or "").strip()
if not key:
return fallback
params = dict(notification_context.get("params") or {})
rendered = translator(key, **params)
return fallback if rendered == key else str(rendered)


def _translate_snapshot_guard_decision(decision: object, *, translator) -> str:
value = str(decision or "").strip()
if not value:
return ""
key = f"snapshot_guard_decision_{value}"
translated = translator(key)
return value if translated == key else str(translated)


def _split_detail_segment(text: str) -> list[str]:
value = str(text or "").strip()
if not value:
Expand Down Expand Up @@ -585,13 +610,37 @@ def run_strategy_core(
decision = signal_metadata.get("snapshot_guard_decision")
no_op_reason = signal_metadata.get("no_op_reason")
fail_reason = signal_metadata.get("fail_reason")
no_op_text = config.translator("no_trades")
if decision:
no_op_text = f"{no_op_text} | {_localize_notification_text(f'decision={decision}', translator=config.translator)}"
if no_op_reason:
no_op_text = f"{no_op_text} | {_localize_notification_text(f'reason={no_op_reason}', translator=config.translator)}"
if fail_reason:
no_op_text = f"{no_op_text} | {_localize_notification_text(f'fail_reason={fail_reason}', translator=config.translator)}"
notification_context = signal_metadata.get("notification_context")
status_context = (
notification_context.get("status")
if isinstance(notification_context, Mapping)
else None
)
rendered_status = _render_notification_context_text(
status_context,
translator=config.translator,
fallback="",
)
no_op_segments = [config.translator("no_trades")]
if rendered_status:
no_op_segments.append(rendered_status)
else:
if decision:
no_op_segments.append(
config.translator(
"snapshot_decision_detail",
value=_translate_snapshot_guard_decision(decision, translator=config.translator),
)
)
if no_op_reason:
no_op_segments.append(
_localize_notification_text(f"reason={no_op_reason}", translator=config.translator)
)
if fail_reason:
no_op_segments.append(
_localize_notification_text(f"fail_reason={fail_reason}", translator=config.translator)
)
no_op_text = " | ".join(segment for segment in no_op_segments if str(segment).strip())
no_op_text = "\n".join(_split_labeled_text(no_op_text))
record = build_reconciliation_record(
strategy_profile=signal_metadata.get("strategy_profile"),
Expand Down
12 changes: 12 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@
"partial_sell_batch": "⚠️ 卖单部分成交 {count}个标的: {details}",
"no_equity": "❌ 无净值",
"signal_label": "信号",
"signal_monthly_snapshot_waiting": "月度快照节奏 | 等待进入执行窗口",
"status_monthly_snapshot_waiting_window": "不执行 | 原因=当前不在月度执行窗口 | 快照日期={snapshot_as_of} | 允许日期={allowed_dates}",
"status_no_execution_window_after_snapshot": "不执行 | 原因=快照后没有可用执行窗口 | 快照日期={snapshot_as_of}",
"snapshot_guard_decision_proceed": "继续",
"snapshot_guard_decision_no_op": "不执行",
"snapshot_guard_decision_fail_closed": "关闭执行",
"market_status_risk_on": "🚀 风险开启({asset})",
"market_status_delever": "🛡️ 降杠杆({asset})",
"signal_risk_on": "SOXL 站上 {window} 日均线,持有 SOXL,交易层风险仓位 {ratio}",
Expand Down Expand Up @@ -132,6 +138,12 @@
"partial_sell_batch": "⚠️ Partial sell fills for {count} symbols: {details}",
"no_equity": "❌ No equity",
"signal_label": "Signal",
"signal_monthly_snapshot_waiting": "monthly snapshot cadence | waiting inside execution window",
"status_monthly_snapshot_waiting_window": "no-op | reason=outside monthly execution window | snapshot_as_of={snapshot_as_of} | allowed={allowed_dates}",
"status_no_execution_window_after_snapshot": "no-op | reason=no execution window after snapshot | snapshot_as_of={snapshot_as_of}",
"snapshot_guard_decision_proceed": "proceed",
"snapshot_guard_decision_no_op": "no_op",
"snapshot_guard_decision_fail_closed": "fail_closed",
"market_status_risk_on": "🚀 RISK-ON ({asset})",
"market_status_delever": "🛡️ DE-LEVER ({asset})",
"signal_risk_on": "SOXL above {window}d MA, hold SOXL, risk {ratio}",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
flask
gunicorn
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@ac550fee3ca33bbf28c8a886848dede4e4e09a1b
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@0f4373f69c9d9f800eda476ff213b7013e51aba4
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@9f55c58b6854ae700c47a684262eec10e763306a
pandas
numpy
requests
Expand Down
51 changes: 51 additions & 0 deletions tests/test_rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,3 +368,54 @@ def disconnect(self):
assert payload_path is not None
payload = json.loads(payload_path.read_text(encoding="utf-8"))
assert payload["no_op_reason"] == "outside_execution_window"


def test_run_strategy_core_prefers_structured_noop_status_in_zh():
observed = {"messages": []}

class FakeIB:
def isConnected(self):
return True

def disconnect(self):
return None

result = run_strategy_core(
connect_ib=lambda: FakeIB(),
get_current_portfolio=lambda _ib: ({}, {"equity": 1000.0, "buying_power": 500.0}),
compute_signals=lambda _ib, _holdings: (
None,
"monthly snapshot cadence | waiting inside execution window",
False,
"no-op | reason=outside_monthly_execution_window",
{
"strategy_profile": "mega_cap_leader_rotation_top50_balanced",
"trade_date": "2026-04-22",
"snapshot_as_of": "2026-04-16",
"snapshot_guard_decision": "proceed",
"no_op_reason": "outside_monthly_execution_window snapshot=2026-04-16 allowed=2026-04-17,2026-04-20,2026-04-21",
"managed_symbols": ("AAPL", "MSFT", "BOXX"),
"dry_run_only": True,
"notification_context": {
"signal": {"code": "signal_monthly_snapshot_waiting", "params": {}},
"status": {
"code": "status_monthly_snapshot_waiting_window",
"params": {
"snapshot_as_of": "2026-04-16",
"allowed_dates": "2026-04-17, 2026-04-20, 2026-04-21",
},
},
},
},
),
execute_rebalance=lambda *_args, **_kwargs: [],
send_tg_message=lambda message: observed["messages"].append(message),
translator=build_translator("zh"),
separator="---",
strategy_display_name="Mega Cap Top50 平衡龙头轮动",
)

assert result.result == "OK - heartbeat"
assert observed["messages"]
assert "当前不在月度执行窗口" in observed["messages"][0]
assert "决策=proceed" not in observed["messages"][0]