feat: improve Telegram message formatting

- Consistent header: icon + bold status + symbol · side
- Prices wrapped in <code> for monospace rendering
- TP/SL shown on separate lines (Entry/TP/SL aligned)
- Closed trades: entry → exit with vol + balance on one line
- PnL summary: trades · wins/losses · WR% with bold net P&L
- Add fp() helper for f64 price without trailing zeros

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
romysaputrasihananda
2026-06-11 02:11:27 +07:00
parent 954cf6b8cf
commit 4fdc3da4ed
2 changed files with 50 additions and 37 deletions
+17 -13
View File
@@ -16,26 +16,30 @@ async fn main() -> Result<()> {
let tg = TelegramConfig { token, chat_id, thread_id };
let http = Client::new();
// 1. Pending
tracing::info!("step 1: PENDING");
let msg_id = tg.send(&http,
"🟡 <b>PENDING</b>\nXAUUSDm Long\nEntry: 3325.50\nSL: 3320.00 TP: 3333.75\nVol: 0.03 lot RR: 1.5",
// ── Flow: pending → filled → TP hit ──────────────────────────────────────
let msg = tg.send(&http,
"🟡 <b>PENDING</b>\nXAUUSDm · Long\n\nEntry <code>3325.50</code>\nTP <code>3333.75</code>\nSL <code>3320.00</code>\nVol 0.03 lot · RR 1:1.5"
).await?;
tracing::info!(msg_id, "sent");
tracing::info!(msg_id = msg, "PENDING sent");
sleep(Duration::from_secs(3)).await;
// 2. Filled
tracing::info!("step 2: FILLED");
tg.edit(&http, msg_id,
"⚡ <b>FILLED</b>\nXAUUSDm Long\nEntry: 3325.50\nSL: 3320.00 TP: 3333.75\nVol: 0.03 lot",
tg.edit(&http, msg,
"⚡ <b>FILLED</b>\nXAUUSDm · Long\n\nEntry <code>3325.50</code>\nTP <code>3333.75</code>\nSL <code>3320.00</code>\nVol 0.03 lot"
).await?;
tracing::info!(msg_id = msg, "FILLED edited");
sleep(Duration::from_secs(3)).await;
// 3. Result
tracing::info!("step 3: TP HIT");
tg.edit(&http, msg_id,
"✅ <b>TP HIT +12.68</b>\nXAUUSDm Long\n3325.50 → 3333.75\nVol: 0.03 lot\nBal: $5012.68",
tg.edit(&http, msg,
"✅ <b>TAKE PROFIT +12.68</b>\nXAUUSDm · Long\n\n<code>3325.50</code> → <code>3333.75</code>\nVol 0.03 lot · Bal $5012.68"
).await?;
tracing::info!(msg_id = msg, "TP HIT edited");
sleep(Duration::from_secs(3)).await;
// ── PnL summary ───────────────────────────────────────────────────────────
tg.send(&http,
"📊 <b>PnL Summary</b>\n\n<b>Today</b> · XAUUSDm · 2026-06-11\n2 trades · 1W 1L · WR 50%\nNet <b>+$5.18</b>\n\n<b>All-time</b>\n17 trades · 10W 7L · WR 59%\nNet <b>+$482.41</b>"
).await?;
tracing::info!("PnL summary sent");
println!("\nDone ✅ check Telegram");
Ok(())
+33 -24
View File
@@ -246,9 +246,9 @@ async fn tick(
}
if let (Some(tg), Some(msg_id)) = (&cfg.telegram, state.tg_message_id) {
let text = format!(
"⏱ <b>EXPIRED</b>\n{} {}\nEntry: {:.5}\nNo fill after {} candles",
symbol, state.side, state.entry,
cfg.fvg_expiry_candles,
"⏱ <b>EXPIRED</b>\n{} · {}\n\nEntry <code>{}</code>\nUnfilled after {} candles",
symbol, state.side,
fp(state.entry), cfg.fvg_expiry_candles,
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -353,9 +353,9 @@ async fn tick(
// send Telegram "Pending" message
let tg_msg_id = if let Some(tg) = &cfg.telegram {
let text = format!(
"🟡 <b>PENDING</b>\n{} {}\nEntry: {}\nSL: {} TP: {}\nVol: {} lot RR: {:.1}",
"🟡 <b>PENDING</b>\n{} · {}\n\nEntry <code>{}</code>\nTP <code>{}</code>\nSL <code>{}</code>\nVol {} lot · RR 1:{:.1}",
symbol, side_str,
fmt_price(fvg.entry, prec), fmt_price(sl, prec), fmt_price(tp, prec),
fmt_price(fvg.entry, prec), fmt_price(tp, prec), fmt_price(sl, prec),
volume, rr,
);
match tg.send(http, &text).await {
@@ -493,8 +493,9 @@ async fn on_position_opened(
if let (Some(tg), Some(msg_id)) = (tg, tg_msg_id) {
let text = format!(
"⚡ <b>FILLED</b>\n{} {}\nEntry: {:.5}\nSL: {:.5} TP: {:.5}\nVol: {:.2} lot",
symbol, ps.side, entry, sl, tp, volume,
"⚡ <b>FILLED</b>\n{} · {}\n\nEntry <code>{}</code>\nTP <code>{}</code>\nSL <code>{}</code>\nVol {:.2} lot",
symbol, ps.side,
fp(entry), fp(tp), fp(sl), volume,
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -516,7 +517,7 @@ async fn on_position_closed(
let Some(tg) = tg else { return };
let Some(msg_id) = ps.as_ref().and_then(|s| s.tg_message_id) else { return };
let (icon, label) = if profit >= 0.0 { ("", "TP HIT") } else { ("", "SL HIT") };
let (icon, label) = if profit >= 0.0 { ("", "TAKE PROFIT") } else { ("", "STOP LOSS") };
let exit = payload.price_current.unwrap_or(0.0);
let entry = ps.as_ref().map(|s| s.entry).unwrap_or(payload.price_open.unwrap_or(0.0));
let volume = payload.volume.unwrap_or(ps.as_ref().map(|s| s.volume).unwrap_or(0.0));
@@ -525,11 +526,11 @@ async fn on_position_closed(
let acct_bal = mt5.account().await.ok().map(|a| a.balance).unwrap_or(Decimal::ZERO);
let text = format!(
"{} <b>{} {:+.2}</b>\n{} {}\n{:.5}{:.5}\nVol: {:.2} lot\nBal: ${}",
"{} <b>{} {:+.2}</b>\n{} · {}\n\n<code>{}</code> → <code>{}</code>\nVol {:.2} lot · Bal ${:.2}",
icon, label, profit,
symbol, side_str,
entry, exit,
volume, acct_bal,
fp(entry), fp(exit),
volume, d2f(acct_bal),
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -550,8 +551,9 @@ async fn on_position_modified(
if let (Some(tg), Some(msg_id)) = (tg, ps.tg_message_id) {
let text = format!(
"🔄 <b>MODIFIED</b>\n{} {}\nEntry: {:.5}\nSL: {:.5} TP: {:.5}\nVol: {:.2} lot",
symbol, ps.side, ps.entry, ps.sl, ps.tp, ps.volume,
"🔄 <b>MODIFIED</b>\n{} · {}\n\nEntry <code>{}</code>\nTP <code>{}</code>\nSL <code>{}</code>\nVol {:.2} lot",
symbol, ps.side,
fp(ps.entry), fp(ps.tp), fp(ps.sl), ps.volume,
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -573,8 +575,8 @@ async fn on_order_cancelled(
let side_str = state.as_ref().map(|s| s.side.as_str()).unwrap_or("?");
let entry = state.as_ref().map(|s| s.entry).unwrap_or(0.0);
let text = format!(
"🚫 <b>CANCELLED</b>\n{} {}\nEntry: {:.5}",
symbol, side_str, entry,
"🚫 <b>CANCELLED</b>\n{} · {}\n\nEntry <code>{}</code>",
symbol, side_str, fp(entry),
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -598,8 +600,9 @@ async fn on_order_modified(
if let (Some(tg), Some(msg_id)) = (tg, state.tg_message_id) {
let text = format!(
"🔄 <b>ORDER MODIFIED</b>\n{} {}\nEntry: {:.5}\nSL: {:.5} TP: {:.5}",
symbol, state.side, state.entry, state.sl, state.tp,
"🔄 <b>ORDER MODIFIED</b>\n{} · {}\n\nEntry <code>{}</code>\nTP <code>{}</code>\nSL <code>{}</code>",
symbol, state.side,
fp(state.entry), fp(state.tp), fp(state.sl),
);
let _ = tg.edit(http, msg_id, &text).await;
}
@@ -624,27 +627,27 @@ async fn send_pnl_summary(
mt5.history_deals(&epoch_str, &now_str, Some(symbol)),
)?;
let summary_text = |label: &str, deals: &[domain::Deal]| {
let summary_text = |deals: &[domain::Deal]| {
let closing: Vec<_> = deals.iter()
.filter(|d| d.entry == 1 && d.magic == MAGIC)
.collect();
let total = closing.len();
let wins = closing.iter().filter(|d| d.profit > Decimal::ZERO).count();
let losses = total - wins;
let profit: Decimal = closing.iter().map(|d| d.profit + d.commission + d.swap).sum();
let net: Decimal = closing.iter().map(|d| d.profit + d.commission + d.swap).sum();
let wr = if total > 0 { wins as f64 / total as f64 * 100.0 } else { 0.0 };
format!(
"{}\nTrades: {} ({}W / {}L) WR: {:.0}%\nProfit: ${:+.2}",
label, total, wins, losses, wr, profit,
"{} trades · {}W {}L · WR {:.0}%\nNet <b>{:+.2}</b>",
total, wins, losses, wr, net,
)
};
let today_date = now.format("%Y-%m-%d").to_string();
let text = format!(
"📊 <b>Today</b> {} {}\n{}\n\n📈 <b>All-time</b>\n{}",
"📊 <b>PnL Summary</b>\n\n<b>Today</b> · {} · {}\n{}\n\n<b>All-time</b>\n{}",
symbol, today_date,
summary_text("", &today_deals),
summary_text("", &all_deals),
summary_text(&today_deals),
summary_text(&all_deals),
);
tg.send(http, &text).await?;
@@ -653,6 +656,12 @@ async fn send_pnl_summary(
// ── helpers ───────────────────────────────────────────────────────────────────
fn fp(v: f64) -> String {
let s = format!("{v:.5}");
let s = s.trim_end_matches('0');
s.trim_end_matches('.').to_string()
}
fn timeframe_minutes(tf: Timeframe) -> i64 {
match tf {
Timeframe::M1 => 1,