Telegram alert redesign: verdict-first layout + informational 100-pip TP
format_signal_message restyled (display-only, zero signal-logic impact): verdict header first (signal - pair - TF - quality), trade setup moved above the detail, per-indicator breakdown condensed to one arrow line (_indicator_summary), heavy dividers dropped. New TP 100p line in every setup block: entry +/- 100 pips ($10.00 on XAU) with its R-multiple vs the stop (_INFO_TP_PIPS, formatter-only - not stamped on TradeSetup, not in CLI/JSON/backtester). Trailing-plan logic unchanged (stamped per-TF params, pullback fallback). Removed dead _indicator_line and _SIGNAL_TAG. Tests 210 -> 212 (100p TP math BUY/SELL/no-setup, one-line indicator summary). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BoZKi67Ct6QTeiFJmybXSw
This commit is contained in:
+73
-53
@@ -223,15 +223,6 @@ _QUALITY_EMOJI = {
|
||||
"A+": "🏆", "A": "🥇", "B": "🥈", "C": "🥉", "D": "⚠️", "F": "❌",
|
||||
}
|
||||
|
||||
_SIGNAL_TAG = {
|
||||
SignalStrength.STRONG_BUY: "Strong Buy",
|
||||
SignalStrength.BUY: "Bullish",
|
||||
SignalStrength.NEUTRAL: "Neutral",
|
||||
SignalStrength.SELL: "Bearish",
|
||||
SignalStrength.STRONG_SELL: "Strong Sell",
|
||||
}
|
||||
|
||||
|
||||
def _signal_label(sig: SignalStrength) -> str:
|
||||
return sig.name.replace("_", " ")
|
||||
|
||||
@@ -246,10 +237,36 @@ def _mtf_line(report: SignalReport) -> str:
|
||||
return " | ".join(parts)
|
||||
|
||||
|
||||
def _indicator_line(name: str, ind) -> str:
|
||||
tag = _SIGNAL_TAG.get(ind.signal, "Neutral")
|
||||
short = ind.description.split(" | ")[0] if " | " in ind.description else ind.description
|
||||
return f"[{tag}] *{_escape_md(ind.name)}*: {_escape_md(short)}"
|
||||
# Informational fixed-pip target shown on every trade setup (display-only,
|
||||
# not part of the backtested exit model).
|
||||
_INFO_TP_PIPS = 100
|
||||
|
||||
_IND_SHORT = {
|
||||
"ema_trend": "EMA", "rsi": "RSI", "macd": "MACD", "bollinger": "BB",
|
||||
"adx": "ADX", "roc": "ROC", "keltner": "KC", "divergence": "Div",
|
||||
"candlestick": "Cndl",
|
||||
}
|
||||
|
||||
_IND_ARROW = {
|
||||
SignalStrength.STRONG_BUY: "⇈",
|
||||
SignalStrength.BUY: "↑",
|
||||
SignalStrength.NEUTRAL: "→",
|
||||
SignalStrength.SELL: "↓",
|
||||
SignalStrength.STRONG_SELL: "⇊",
|
||||
}
|
||||
|
||||
|
||||
def _indicator_summary(report: SignalReport) -> str:
|
||||
"""One-line condensed indicator readout: 'EMA↑ RSI↑ MACD→ …'."""
|
||||
if not report.primary_analysis:
|
||||
return ""
|
||||
parts = []
|
||||
for name, ind in report.primary_analysis.indicators.items():
|
||||
if name.startswith("_") or ind.weight == 0:
|
||||
continue
|
||||
label = _IND_SHORT.get(name, name)
|
||||
parts.append(f"{label}{_IND_ARROW.get(ind.signal, '→')}")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def _live_price_line(symbol: str) -> Optional[str]:
|
||||
@@ -276,46 +293,26 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
emoji = _SIGNAL_EMOJI.get(sig, "⬜")
|
||||
qe = _QUALITY_EMOJI.get(report.signal_quality, "")
|
||||
label = _signal_label(sig)
|
||||
mtf_tag = "✅ MTF aligned" if report.mtf_agreement else "❌ MTF diverged"
|
||||
|
||||
lines: list[str] = []
|
||||
mtf_tag = "MTF ✅" if report.mtf_agreement else "MTF ❌"
|
||||
tf = report.primary_analysis.timeframe if report.primary_analysis else ""
|
||||
|
||||
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
||||
regime_str = _escape_md(report.market_regime.replace("_", " ").title())
|
||||
vol_str = _escape_md(str(report.volatility_regime))
|
||||
lines += [
|
||||
f"🔍 *{report.display_name} Signal Analysis* ({report.primary_analysis.timeframe if report.primary_analysis else ''})",
|
||||
f"🕐 {now}",
|
||||
"",
|
||||
f"💰 Close: *{_fmt_price(report.current_price)}*",
|
||||
|
||||
lines: list[str] = [
|
||||
f"{emoji} *{label} · {report.display_name} · {tf}* {qe}{report.signal_quality}",
|
||||
f"🕐 {now} · 💰 {_fmt_price(report.current_price)}",
|
||||
]
|
||||
live = _live_price_line(report.symbol)
|
||||
if live:
|
||||
lines.append(live)
|
||||
lines += [
|
||||
f"📊 Regime: {regime_str} | Vol: {vol_str}",
|
||||
"",
|
||||
"━━━━━━━━━━━━━━━━━━━━",
|
||||
f"{emoji} *{label}* {qe}{report.signal_quality} | Score: {report.final_score:+.1f} | {report.final_confidence:.0f}% conf",
|
||||
f"_{_escape_md(report.action_recommendation)}_",
|
||||
f"{mtf_tag}",
|
||||
f"Score *{report.final_score:+.1f}* · Conf *{report.final_confidence:.0f}%* · {mtf_tag}",
|
||||
f"📊 Regime: {regime_str} · Vol: {vol_str}",
|
||||
]
|
||||
|
||||
mtf = _mtf_line(report)
|
||||
if mtf:
|
||||
lines += ["", f"📈 *MTF*: {mtf}"]
|
||||
|
||||
if report.primary_analysis:
|
||||
lines += ["", "━━━━━━━━━━━━━━━━━━━━", "📋 *Indicators*"]
|
||||
for name, ind in report.primary_analysis.indicators.items():
|
||||
if name.startswith("_") or ind.weight == 0:
|
||||
continue
|
||||
lines.append(_indicator_line(name, ind))
|
||||
|
||||
if report.warnings:
|
||||
lines.append("")
|
||||
for w in report.warnings:
|
||||
lines.append(f"⚠️ {_escape_md(w)}")
|
||||
if report.action_recommendation:
|
||||
lines.append(f"_{_escape_md(report.action_recommendation)}_")
|
||||
|
||||
if report.trade_setup and sig not in (SignalStrength.NEUTRAL,):
|
||||
ts = report.trade_setup
|
||||
@@ -324,16 +321,28 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
reward_usd = ts.reward_pips * ts.pip_value
|
||||
lines += [
|
||||
"",
|
||||
"━━━━━━━━━━━━━━━━━━━━",
|
||||
f"🚀 *TRADE SETUP*",
|
||||
"🚀 *TRADE SETUP*",
|
||||
f"{dir_emoji} Direction: *{ts.direction}*",
|
||||
f"▶ Entry: `{_fmt_price(ts.entry_price)}`",
|
||||
f"🛑 SL: `{_fmt_price(ts.stop_loss)}` (${risk_usd:.2f} / {ts.risk_pips:.0f} pips)",
|
||||
f"🎯 TP1: `{_fmt_price(ts.take_profit_1)}` (${reward_usd:.2f} / {ts.reward_pips:.0f} pips)",
|
||||
f"▶ Entry `{_fmt_price(ts.entry_price)}`",
|
||||
f"🛑 SL `{_fmt_price(ts.stop_loss)}` (${risk_usd:.2f} / {ts.risk_pips:.0f}p)",
|
||||
f"🎯 TP1 `{_fmt_price(ts.take_profit_1)}` (${reward_usd:.2f} / {ts.reward_pips:.0f}p)",
|
||||
]
|
||||
if ts.take_profit_2:
|
||||
lines.append(f"🎯 TP2: `{_fmt_price(ts.take_profit_2)}`")
|
||||
lines.append(f"⚖️ RR: 1:{ts.risk_reward:.1f}")
|
||||
lines.append(f"🎯 TP2 `{_fmt_price(ts.take_profit_2)}`")
|
||||
|
||||
# Informational fixed-pip target: where a 100-pip TP would sit for
|
||||
# this setup (display-only — not part of the backtested exit model).
|
||||
info_dist = _INFO_TP_PIPS * ts.pip_value
|
||||
if ts.direction == "BUY":
|
||||
info_price = ts.entry_price + info_dist
|
||||
else:
|
||||
info_price = ts.entry_price - info_dist
|
||||
risk_dist = abs(ts.entry_price - ts.stop_loss)
|
||||
info_r = f" · ≈{info_dist / risk_dist:.1f}R" if risk_dist > 0 else ""
|
||||
lines.append(
|
||||
f"🎯 TP {_INFO_TP_PIPS}p: `{_fmt_price(info_price)}` (${info_dist:.2f}{info_r})"
|
||||
)
|
||||
lines.append(f"⚖️ RR 1:{ts.risk_reward:.1f}")
|
||||
|
||||
# Trailing-stop management plan (matches backtested exit model:
|
||||
# TP1 is advisory, the trail is the exit). Params are stamped on
|
||||
@@ -348,7 +357,6 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
arm_r = sig_cfg.pb_trail_arm_r if is_pb else sig_cfg.trail_arm_r
|
||||
dist_r = sig_cfg.pb_trail_dist_r if is_pb else sig_cfg.trail_dist_r
|
||||
if arm_r > 0:
|
||||
risk_dist = abs(ts.entry_price - ts.stop_loss)
|
||||
if ts.direction == "BUY":
|
||||
arm_price = ts.entry_price + arm_r * risk_dist
|
||||
else:
|
||||
@@ -356,12 +364,24 @@ def format_signal_message(report: SignalReport) -> str:
|
||||
trail_dist = dist_r * risk_dist
|
||||
tag = " (pullback)" if is_pb else ""
|
||||
lines.append(
|
||||
f"🔄 Trail{tag}: once price hits `{_fmt_price(arm_price)}` "
|
||||
f"🔄 Trail{tag}: arm `{_fmt_price(arm_price)}` "
|
||||
f"(+{arm_r:.1f}R), trail SL ${trail_dist:.2f} "
|
||||
f"behind best price — TP1 is a guide, let it run"
|
||||
)
|
||||
|
||||
lines += ["", "━━━━━━━━━━━━━━━━━━━━", _DISCLAIMER]
|
||||
extra: list[str] = []
|
||||
mtf = _mtf_line(report)
|
||||
if mtf:
|
||||
extra.append(f"📈 MTF: {mtf}")
|
||||
ind_summary = _indicator_summary(report)
|
||||
if ind_summary:
|
||||
extra.append(f"📋 {ind_summary}")
|
||||
for w in report.warnings:
|
||||
extra.append(f"⚠️ {_escape_md(w)}")
|
||||
if extra:
|
||||
lines += [""] + extra
|
||||
|
||||
lines += ["", _DISCLAIMER]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user