mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-07-27 18:47:53 +00:00
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:
+17
-13
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user