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
93 changes: 76 additions & 17 deletions application/execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ def _sanitize_token(value: str | None) -> str:
return safe or "none"


def _display_text(value: Any, *, fallback: str) -> str:
text = str(value).strip() if value is not None else ""
return text or fallback


def _resolve_execution_lock_path(
*,
strategy_profile: str | None,
Expand Down Expand Up @@ -240,6 +245,8 @@ def _format_target_lines(
target_weights: dict[str, float],
current_mv: dict[str, float],
equity: float,
*,
translator,
) -> list[str]:
current_weight = {
symbol: (current_mv.get(symbol, 0.0) / equity if equity > 0 else 0.0)
Expand All @@ -249,7 +256,13 @@ def _format_target_lines(
for symbol, target_weight in sorted(target_weights.items(), key=lambda item: (-item[1], item[0])):
delta = target_weight - current_weight.get(symbol, 0.0)
target_lines.append(
f"target_diff {symbol}: current={current_weight.get(symbol, 0.0):.1%} target={target_weight:.1%} delta={delta:.1%}"
translator(
"target_diff",
symbol=symbol,
current=f"{current_weight.get(symbol, 0.0):.1%}",
target=f"{target_weight:.1%}",
delta=f"{delta:.1%}",
)
)
return target_lines

Expand Down Expand Up @@ -340,7 +353,7 @@ def execute_rebalance(
if equity <= 0:
execution_summary["execution_status"] = "blocked"
execution_summary["no_op_reason"] = "no_equity"
return _finalize_result(["❌ No equity"], execution_summary, return_summary=return_summary)
return _finalize_result([translator("no_equity")], execution_summary, return_summary=return_summary)

reserved = equity * cash_reserve_ratio
investable = equity - reserved
Expand Down Expand Up @@ -377,18 +390,49 @@ def execute_rebalance(
execution_summary["no_op_reason"] = reason
execution_summary["skipped_reasons"].append(reason)
trade_logs.append(
f"pending_orders_detected profile={strategy_profile or '<unknown>'} symbols={','.join(pending_symbols)}"
translator(
"pending_orders_detected",
profile=_display_text(strategy_profile, fallback="<unknown>"),
symbols=",".join(pending_symbols),
)
)
return _finalize_result(trade_logs, execution_summary, return_summary=return_summary)

trade_logs.append(
f"profile={strategy_profile or '<unknown>'} regime={signal_metadata.get('regime')} "
f"breadth={signal_metadata.get('breadth_ratio', 0.0):.1%} "
f"target_stock={signal_metadata.get('target_stock_weight', 0.0):.1%} "
f"realized_stock={signal_metadata.get('realized_stock_weight', 0.0):.1%} "
f"snapshot_as_of={snapshot_date or '<none>'} trade_date={trade_date or '<none>'}"
" | ".join(
[
translator(
"execution_profile_detail",
profile=_display_text(strategy_profile, fallback="<unknown>"),
),
translator(
"regime_detail",
value=_display_text(signal_metadata.get("regime"), fallback="<none>"),
),
translator(
"breadth_detail",
value=f"{float(signal_metadata.get('breadth_ratio', 0.0) or 0.0):.1%}",
),
translator(
"target_stock_detail",
value=f"{float(signal_metadata.get('target_stock_weight', 0.0) or 0.0):.1%}",
),
translator(
"realized_stock_detail",
value=f"{float(signal_metadata.get('realized_stock_weight', 0.0) or 0.0):.1%}",
),
translator(
"snapshot_as_of_detail",
value=_display_text(snapshot_date, fallback="<none>"),
),
translator(
"trade_date_detail",
value=_display_text(trade_date, fallback="<none>"),
),
]
)
)
trade_logs.extend(_format_target_lines(target_weights, current_mv, equity))
trade_logs.extend(_format_target_lines(target_weights, current_mv, equity, translator=translator))

has_sell_plan = False
for symbol in all_symbols:
Expand Down Expand Up @@ -428,8 +472,13 @@ def execute_rebalance(
execution_summary["no_op_reason"] = reason
execution_summary["skipped_reasons"].append(reason)
trade_logs.append(
f"same_day_fills_detected profile={strategy_profile or '<unknown>'} mode={'dry_run' if dry_run_only else 'paper'} "
f"symbols={','.join(same_day_filled_symbols)} trade_date={trade_date}"
translator(
"same_day_fills_detected",
profile=_display_text(strategy_profile, fallback="<unknown>"),
mode="dry_run" if dry_run_only else "paper",
symbols=",".join(same_day_filled_symbols),
trade_date=_display_text(trade_date, fallback="<none>"),
)
)
return _finalize_result(trade_logs, execution_summary, return_summary=return_summary)

Expand Down Expand Up @@ -463,16 +512,26 @@ def execute_rebalance(
execution_summary["skipped_reasons"].append(reason)
execution_summary["lock_path"] = str(lock_path)
trade_logs.append(
"same_day_execution_locked "
f"profile={strategy_profile or '<unknown>'} mode={'dry_run' if dry_run_only else 'paper'} "
f"trade_date={trade_date or '<none>'} snapshot_date={snapshot_date or '<none>'} "
f"target_hash={existing.get('target_hash', '<unknown>')} lock_path={lock_path}"
translator(
"same_day_execution_locked",
profile=_display_text(strategy_profile, fallback="<unknown>"),
mode="dry_run" if dry_run_only else "paper",
trade_date=_display_text(trade_date, fallback="<none>"),
snapshot_date=_display_text(snapshot_date, fallback="<none>"),
target_hash=_display_text(existing.get("target_hash"), fallback="<unknown>"),
lock_path=str(lock_path),
)
)
return _finalize_result(trade_logs, execution_summary, return_summary=return_summary)
execution_summary["lock_path"] = str(lock_path)
trade_logs.append(
f"execution_lock_acquired mode={'dry_run' if dry_run_only else 'paper'} "
f"trade_date={trade_date or '<none>'} snapshot_date={snapshot_date or '<none>'} lock_path={lock_path}"
translator(
"execution_lock_acquired",
mode="dry_run" if dry_run_only else "paper",
trade_date=_display_text(trade_date, fallback="<none>"),
snapshot_date=_display_text(snapshot_date, fallback="<none>"),
lock_path=str(lock_path),
)
)
execution_summary["execution_status"] = "executing"

Expand Down
48 changes: 32 additions & 16 deletions application/rebalance_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
)


def _format_text(value, *, fallback: str) -> str:
text = str(value).strip() if value is not None else ""
return text or fallback


def build_dashboard(
positions,
account_values,
Expand All @@ -31,14 +36,13 @@ def build_dashboard(
avg = positions[symbol]["avg_cost"]
market_value = qty * avg
position_lines.append(f" {symbol}: {qty}股 ${market_value:,.2f}")
position_text = "\n".join(position_lines) if position_lines else " (空仓)"
position_text = "\n".join(position_lines) if position_lines else translator("empty_positions")
signal_metadata = signal_metadata or {}
target_lines = []
if target_weights:
for symbol, weight in sorted(target_weights.items(), key=lambda item: (-item[1], item[0])):
target_lines.append(f" {symbol}: {weight:.1%}")
target_text = "\n".join(target_lines) if target_lines else " (无目标持仓)"
profile_line = f"strategy_profile={strategy_profile}" if strategy_profile else "strategy_profile=<unknown>"
target_text = "\n".join(target_lines) if target_lines else translator("empty_target_weights")
regime = signal_metadata.get("regime")
breadth_ratio = signal_metadata.get("breadth_ratio")
target_stock_weight = signal_metadata.get("target_stock_weight")
Expand All @@ -51,18 +55,30 @@ def build_dashboard(
snapshot_file_timestamp = signal_metadata.get("snapshot_file_timestamp")
snapshot_decision = signal_metadata.get("snapshot_guard_decision")
diagnostics = [
profile_line,
f"regime={regime}" if regime else None,
f"breadth={breadth_ratio:.1%}" if isinstance(breadth_ratio, (int, float)) else None,
f"risk_target={target_stock_weight:.1%}" if isinstance(target_stock_weight, (int, float)) else None,
f"realized_stock={realized_stock_weight:.1%}" if isinstance(realized_stock_weight, (int, float)) else None,
f"safe_haven_target={safe_haven_weight:.1%}" if isinstance(safe_haven_weight, (int, float)) else None,
f"snapshot_decision={snapshot_decision}" if snapshot_decision else None,
f"snapshot_as_of={snapshot_as_of}" if snapshot_as_of else None,
f"snapshot_age_days={snapshot_age_days}" if isinstance(snapshot_age_days, (int, float)) else None,
f"snapshot_file_ts={snapshot_file_timestamp}" if snapshot_file_timestamp else None,
f"snapshot_path={snapshot_path}" if snapshot_path else None,
f"config_source={config_source}" if config_source else None,
translator("strategy_profile_detail", profile=_format_text(strategy_profile, fallback="<unknown>")),
translator("regime_detail", value=_format_text(regime, fallback="<none>")) if regime is not None else None,
translator("breadth_detail", value=f"{breadth_ratio:.1%}") if isinstance(breadth_ratio, (int, float)) else None,
translator("target_stock_detail", value=f"{target_stock_weight:.1%}")
if isinstance(target_stock_weight, (int, float))
else None,
translator("realized_stock_detail", value=f"{realized_stock_weight:.1%}")
if isinstance(realized_stock_weight, (int, float))
else None,
translator("safe_haven_target_detail", value=f"{safe_haven_weight:.1%}")
if isinstance(safe_haven_weight, (int, float))
else None,
translator("snapshot_decision_detail", value=_format_text(snapshot_decision, fallback="<none>"))
if snapshot_decision
else None,
translator("snapshot_as_of_detail", value=_format_text(snapshot_as_of, fallback="<none>")) if snapshot_as_of else None,
translator("snapshot_age_days_detail", value=_format_text(snapshot_age_days, fallback="<none>"))
if isinstance(snapshot_age_days, (int, float))
else None,
translator("snapshot_file_ts_detail", value=_format_text(snapshot_file_timestamp, fallback="<none>"))
if snapshot_file_timestamp
else None,
translator("snapshot_path_detail", value=_format_text(snapshot_path, fallback="<none>")) if snapshot_path else None,
translator("config_source_detail", value=_format_text(config_source, fallback="<none>")) if config_source else None,
]
diagnostics_text = " | ".join(part for part in diagnostics if part)
return (
Expand All @@ -75,7 +91,7 @@ def build_dashboard(
f"{status_icon} {status_desc}\n"
f"🎯 {signal_desc}\n"
f"{separator}\n"
f"Target Weights:\n{target_text}"
f"{translator('target_weights_title')}:\n{target_text}"
)


Expand Down
46 changes: 46 additions & 0 deletions notifications/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,29 @@
"canary_title": "🐤 【金丝雀检查】",
"equity": "净值",
"buying_power": "购买力",
"empty_positions": " (空仓)",
"empty_target_weights": " (无目标持仓)",
"target_weights_title": "目标持仓",
"strategy_profile_detail": "策略={profile}",
"execution_profile_detail": "profile={profile}",
"regime_detail": "市场阶段={value}",
"breadth_detail": "宽度={value}",
"target_stock_detail": "目标股票仓位={value}",
"realized_stock_detail": "实际股票仓位={value}",
"safe_haven_target_detail": "目标避险仓位={value}",
"snapshot_decision_detail": "快照决策={value}",
"snapshot_as_of_detail": "快照日期={value}",
"snapshot_age_days_detail": "快照账龄={value}",
"snapshot_file_ts_detail": "快照文件时间={value}",
"snapshot_path_detail": "快照路径={value}",
"config_source_detail": "配置来源={value}",
"trade_date_detail": "交易日={value}",
"target_diff": "目标差异 {symbol}: 当前={current} 目标={target} 变化={delta}",
"pending_orders_detected": "检测到未完成订单: profile={profile} symbols={symbols}",
"same_day_fills_detected": "检测到当日成交: profile={profile} mode={mode} symbols={symbols} trade_date={trade_date}",
"same_day_execution_locked": "当日执行锁已存在: profile={profile} mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} target_hash={target_hash} lock_path={lock_path}",
"execution_lock_acquired": "已获取执行锁: mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} lock_path={lock_path}",
"no_equity": "❌ 无净值",
"signal_label": "信号",
"no_trades": "✅ 无需调仓",
"emergency": "🛡️ 金丝雀应急: {n_bad}/4 坏, 全部转入 {safe}",
Expand All @@ -32,6 +55,29 @@
"canary_title": "🐤 【Canary Check】",
"equity": "Equity",
"buying_power": "Buying Power",
"empty_positions": " (No positions)",
"empty_target_weights": " (No target positions)",
"target_weights_title": "Target Weights",
"strategy_profile_detail": "strategy_profile={profile}",
"execution_profile_detail": "profile={profile}",
"regime_detail": "regime={value}",
"breadth_detail": "breadth={value}",
"target_stock_detail": "target_stock={value}",
"realized_stock_detail": "realized_stock={value}",
"safe_haven_target_detail": "safe_haven_target={value}",
"snapshot_decision_detail": "snapshot_decision={value}",
"snapshot_as_of_detail": "snapshot_as_of={value}",
"snapshot_age_days_detail": "snapshot_age_days={value}",
"snapshot_file_ts_detail": "snapshot_file_ts={value}",
"snapshot_path_detail": "snapshot_path={value}",
"config_source_detail": "config_source={value}",
"trade_date_detail": "trade_date={value}",
"target_diff": "target_diff {symbol}: current={current} target={target} delta={delta}",
"pending_orders_detected": "pending_orders_detected profile={profile} symbols={symbols}",
"same_day_fills_detected": "same_day_fills_detected profile={profile} mode={mode} symbols={symbols} trade_date={trade_date}",
"same_day_execution_locked": "same_day_execution_locked profile={profile} mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} target_hash={target_hash} lock_path={lock_path}",
"execution_lock_acquired": "execution_lock_acquired mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} lock_path={lock_path}",
"no_equity": "❌ No equity",
"signal_label": "Signal",
"no_trades": "✅ No rebalance needed",
"emergency": "🛡️ Canary Emergency: {n_bad}/4 bad, rotating to {safe}",
Expand Down
13 changes: 13 additions & 0 deletions tests/test_execution_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ def translate(key, **kwargs):
"failed": "failed {reason}",
"market_sell": "sell {symbol} {qty}",
"limit_buy": "buy {symbol} {qty} @{price}",
"target_diff": "target_diff {symbol}: current={current} target={target} delta={delta}",
"execution_profile_detail": "profile={profile}",
"regime_detail": "regime={value}",
"breadth_detail": "breadth={value}",
"target_stock_detail": "target_stock={value}",
"realized_stock_detail": "realized_stock={value}",
"snapshot_as_of_detail": "snapshot_as_of={value}",
"trade_date_detail": "trade_date={value}",
"pending_orders_detected": "pending_orders_detected profile={profile} symbols={symbols}",
"same_day_fills_detected": "same_day_fills_detected profile={profile} mode={mode} symbols={symbols} trade_date={trade_date}",
"same_day_execution_locked": "same_day_execution_locked profile={profile} mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} target_hash={target_hash} lock_path={lock_path}",
"execution_lock_acquired": "execution_lock_acquired mode={mode} trade_date={trade_date} snapshot_date={snapshot_date} lock_path={lock_path}",
"no_equity": "❌ No equity",
}
template = templates[key]
return template.format(**kwargs) if kwargs else template
Expand Down
1 change: 1 addition & 0 deletions tests/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
def test_build_translator_supports_chinese():
translate = build_translator("zh")
assert translate("equity") == "净值"
assert translate("target_weights_title") == "目标持仓"


def test_send_telegram_message_logs_non_200_response(capsys):
Expand Down
Loading