use crate::error::{KrakyError, Result};
use teloxide::prelude::*;
#[cfg(feature = "analytics")]
use crate::models::{ImbalanceMetrics, ImbalanceSignal};
pub struct TelegramNotifier {
bot: Bot,
chat_id: ChatId,
}
impl TelegramNotifier {
pub fn new(token: &str, chat_id: i64) -> Self {
Self {
bot: Bot::new(token),
chat_id: ChatId(chat_id),
}
}
pub async fn send_alert(&self, message: &str) -> Result<()> {
self.bot
.send_message(self.chat_id, message)
.await
.map_err(|e| KrakyError::InvalidMessage(format!("Telegram error: {}", e)))?;
Ok(())
}
pub async fn send_price_alert(&self, symbol: &str, price: f64, context: &str) -> Result<()> {
let message = format!(
"💰 {} Price Alert\n\
Price: ${:.2}\n\
{}",
symbol, price, context
);
self.send_alert(&message).await
}
#[cfg(feature = "analytics")]
pub async fn send_imbalance_alert(
&self,
symbol: &str,
metrics: &ImbalanceMetrics,
signal: ImbalanceSignal,
) -> Result<()> {
let (emoji, signal_name, description) = match signal {
ImbalanceSignal::Bullish => (
"🟢",
"BULLISH",
"Strong buy pressure detected - more bids than asks",
),
ImbalanceSignal::Bearish => (
"🔴",
"BEARISH",
"Strong sell pressure detected - more asks than bids",
),
ImbalanceSignal::Neutral => (
"⚪",
"NEUTRAL",
"Balanced orderbook - no clear directional bias",
),
};
let message = format!(
"{} {} Orderbook Imbalance Alert\n\
\n\
📊 Signal: {}\n\
{}\n\
\n\
📈 Metrics:\n\
• Bid Volume: {:.4} BTC\n\
• Ask Volume: {:.4} BTC\n\
• Bid/Ask Ratio: {:.2}\n\
• Imbalance: {:+.2}%\n\
\n\
💡 Interpretation:\n\
{}",
emoji,
symbol,
signal_name,
"─".repeat(30),
metrics.bid_volume,
metrics.ask_volume,
metrics.bid_ask_ratio,
metrics.imbalance_ratio * 100.0,
description
);
self.send_alert(&message).await
}
pub async fn send_threshold_alert(
&self,
symbol: &str,
price: f64,
threshold: f64,
above: bool,
) -> Result<()> {
let (emoji, direction) = if above {
("📈", "above")
} else {
("📉", "below")
};
let change_pct = ((price - threshold) / threshold * 100.0).abs();
let message = format!(
"{} {} Threshold Alert\n\
\n\
Current Price: ${:.2}\n\
Threshold: ${:.2}\n\
Status: Price is {} threshold\n\
Change: {:.2}%",
emoji, symbol, price, threshold, direction, change_pct
);
self.send_alert(&message).await
}
pub async fn send_orderbook_summary(
&self,
symbol: &str,
best_bid: f64,
best_ask: f64,
spread: f64,
mid_price: f64,
) -> Result<()> {
let spread_bps = (spread / mid_price) * 10000.0;
let message = format!(
"📖 {} Orderbook Update\n\
\n\
Best Bid: ${:.2}\n\
Best Ask: ${:.2}\n\
Mid Price: ${:.2}\n\
Spread: ${:.2} ({:.1} bps)",
symbol, best_bid, best_ask, mid_price, spread, spread_bps
);
self.send_alert(&message).await
}
pub async fn send_connection_status(&self, connected: bool, details: &str) -> Result<()> {
let (emoji, status) = if connected {
("✅", "Connected")
} else {
("❌", "Disconnected")
};
let message = format!(
"{} Connection Status: {}\n\
{}",
emoji, status, details
);
self.send_alert(&message).await
}
pub async fn send_whale_alert(
&self,
symbol: &str,
side: &str,
price: f64,
volume: f64,
) -> Result<()> {
let (emoji, direction) = if side.to_lowercase() == "bid" {
("🟢", "BUY")
} else {
("🔴", "SELL")
};
let message = format!(
"🐋 {} Whale Alert!\n\
\n\
{} Large {} Order Detected\n\
{}\n\
\n\
Price: ${:.2}\n\
Volume: {:.4} {}\n\
Total Value: ${:.2}\n\
\n\
💡 A large {} order has appeared in the orderbook.\n\
This could indicate institutional activity.",
symbol,
emoji,
direction,
"─".repeat(30),
price,
volume,
symbol.split('/').next().unwrap_or(""),
price * volume,
side.to_lowercase()
);
self.send_alert(&message).await
}
pub async fn send_spread_alert(
&self,
symbol: &str,
current_spread_bps: f64,
normal_spread_bps: f64,
multiplier: f64,
) -> Result<()> {
let severity = if multiplier >= 5.0 {
("🚨", "CRITICAL")
} else if multiplier >= 3.0 {
("⚠️", "HIGH")
} else {
("⚡", "MODERATE")
};
let message = format!(
"{} {} Spread Volatility Alert\n\
\n\
Severity: {}\n\
{}\n\
\n\
Current Spread: {:.1} bps\n\
Normal Spread: {:.1} bps\n\
Multiplier: {:.1}x normal\n\
\n\
💡 Interpretation:\n\
The bid-ask spread has widened significantly, indicating\n\
reduced liquidity. This often precedes increased volatility\n\
or large price movements.",
severity.0,
symbol,
severity.1,
"─".repeat(30),
current_spread_bps,
normal_spread_bps,
multiplier
);
self.send_alert(&message).await
}
#[cfg(feature = "analytics")]
pub async fn send_divergence_alert(
&self,
symbol: &str,
price_change: f64,
orderbook_signal: ImbalanceSignal,
) -> Result<()> {
let price_direction = if price_change > 0.0 { "UP" } else { "DOWN" };
let price_emoji = if price_change > 0.0 { "📈" } else { "📉" };
let (ob_emoji, ob_signal) = match orderbook_signal {
ImbalanceSignal::Bullish => ("🟢", "BULLISH"),
ImbalanceSignal::Bearish => ("🔴", "BEARISH"),
ImbalanceSignal::Neutral => ("⚪", "NEUTRAL"),
};
let is_divergence = (price_change > 0.0
&& matches!(orderbook_signal, ImbalanceSignal::Bearish))
|| (price_change < 0.0 && matches!(orderbook_signal, ImbalanceSignal::Bullish));
if !is_divergence {
return Ok(()); }
let message = format!(
"⚡ {} Order Flow DIVERGENCE Alert\n\
\n\
🎯 Divergence Detected!\n\
{}\n\
\n\
{} Price Action: {} ({:+.2}%)\n\
{} Orderbook: {}\n\
\n\
💡 Interpretation:\n\
Price is moving {} but orderbook shows {} pressure.\n\
This divergence could indicate:\n\
• Potential trend reversal\n\
• Large hidden orders executing\n\
• Market maker positioning\n\
\n\
⚠️ Exercise caution - divergences often precede volatility.",
symbol,
"─".repeat(30),
price_emoji,
price_direction,
price_change,
ob_emoji,
ob_signal,
price_direction,
ob_signal
);
self.send_alert(&message).await
}
pub async fn send_trade_alert(
&self,
symbol: &str,
side: &str,
price: f64,
volume: f64,
) -> Result<()> {
let (emoji, direction) = if side.to_lowercase() == "buy" {
("🟢", "BUY")
} else {
("🔴", "SELL")
};
let total_value = price * volume;
let message = format!(
"💥 {} Large Trade Executed\n\
\n\
{} {} Order Filled\n\
{}\n\
\n\
Price: ${:.2}\n\
Volume: {:.4} {}\n\
Total Value: ${:.2}\n\
\n\
💡 A significant {} trade just executed.\n\
This represents real market activity.",
symbol,
emoji,
direction,
"─".repeat(30),
price,
volume,
symbol.split('/').next().unwrap_or(""),
total_value,
side.to_lowercase()
);
self.send_alert(&message).await
}
#[cfg(feature = "private")]
pub async fn send_balance_update(&self, update: &crate::models::BalanceUpdate) -> Result<()> {
if let Some(data) = update.data.first() {
let mut balance_lines = Vec::new();
for (asset, amount) in &data.balances {
balance_lines.push(format!(" {} {}", amount, asset));
}
let message = format!(
"💰 Balance Update\n\
\n\
{}\n\
{}\n\
\n\
🕐 {}\n\
\n\
Your account balances have been updated.",
"─".repeat(30),
balance_lines.join("\n"),
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
);
self.send_alert(&message).await
} else {
Ok(())
}
}
#[cfg(feature = "private")]
pub async fn send_order_update(&self, update: &crate::models::OrderUpdate) -> Result<()> {
if let Some(order) = update.data.first() {
let emoji = match order.status.as_str() {
"open" => "🟢",
"closed" => "✅",
"cancelled" => "❌",
"pending" => "⏳",
_ => "📋",
};
let status_text = match order.status.as_str() {
"open" => "OPENED".to_string(),
"closed" => "FILLED".to_string(),
"cancelled" => "CANCELLED".to_string(),
"pending" => "PENDING".to_string(),
_ => order.status.to_uppercase(),
};
let side_emoji = if order.side.to_lowercase() == "buy" {
"🟢"
} else {
"🔴"
};
let mut details = vec![
format!("{} {} Order", side_emoji, order.side.to_uppercase()),
format!("Order ID: {}", order.order_id),
format!("Type: {}", order.order_type),
];
if let Some(limit_price) = &order.limit_price {
details.push(format!("Limit Price: ${}", limit_price));
}
details.push(format!("Quantity: {}", order.order_qty));
if !order.filled_qty.is_empty() && order.filled_qty != "0" && order.filled_qty != "0.0"
{
details.push(format!("Filled: {}", order.filled_qty));
}
let message = format!(
"{} {} Order {}\n\
\n\
📊 {}\n\
{}\n\
\n\
{}\n\
\n\
🕐 {}",
emoji,
order.symbol,
status_text,
"─".repeat(30),
details.join("\n"),
"─".repeat(30),
if order.timestamp.is_empty() {
chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
} else {
order.timestamp.clone()
}
);
self.send_alert(&message).await
} else {
Ok(())
}
}
#[cfg(feature = "private")]
pub async fn send_execution_alert(
&self,
update: &crate::models::ExecutionUpdate,
) -> Result<()> {
if let Some(exec) = update.data.first() {
let (side_emoji, side_text) = if exec.side.to_lowercase() == "buy" {
("🟢", "BOUGHT")
} else {
("🔴", "SOLD")
};
let qty: f64 = exec.exec_qty.parse().unwrap_or(0.0);
let price: f64 = exec.exec_price.parse().unwrap_or(0.0);
let total_value = qty * price;
let asset = exec.symbol.split('/').next().unwrap_or("BTC");
let liquidity_emoji = if exec.liquidity.to_lowercase() == "maker" {
"🏭" } else {
"⚡" };
let message = format!(
"💥 {} Trade Executed!\n\
\n\
{} {} {} {}\n\
{}\n\
\n\
Execution ID: {}\n\
Order ID: {}\n\
\n\
Price: ${}\n\
Quantity: {} {}\n\
Total Value: ${:.2}\n\
\n\
{} Liquidity: {}\n\
\n\
🕐 {}",
exec.symbol,
side_emoji,
side_text,
exec.exec_qty,
asset,
"─".repeat(30),
exec.exec_id,
exec.order_id,
exec.exec_price,
exec.exec_qty,
asset,
total_value,
liquidity_emoji,
exec.liquidity.to_uppercase(),
if exec.timestamp.is_empty() {
chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string()
} else {
exec.timestamp.clone()
}
);
self.send_alert(&message).await
} else {
Ok(())
}
}
#[cfg(feature = "private")]
pub async fn send_portfolio_summary(
&self,
update: &crate::models::BalanceUpdate,
) -> Result<()> {
if let Some(data) = update.data.first() {
let mut crypto_balances = Vec::new();
let mut fiat_balances = Vec::new();
for (asset, amount) in &data.balances {
let line = format!(" {} {}", amount, asset);
if asset == "USD" || asset == "EUR" || asset == "GBP" {
fiat_balances.push(line);
} else {
crypto_balances.push(line);
}
}
let mut message = format!(
"📊 Portfolio Summary\n\
{}\n\
\n",
"═".repeat(30)
);
if !crypto_balances.is_empty() {
message.push_str("💎 Crypto Assets:\n");
message.push_str(&crypto_balances.join("\n"));
message.push_str("\n\n");
}
if !fiat_balances.is_empty() {
message.push_str("💵 Fiat Balances:\n");
message.push_str(&fiat_balances.join("\n"));
message.push_str("\n\n");
}
message.push_str(&format!(
"{}\n\
🕐 {}\n\
\n\
Total Assets: {}",
"═".repeat(30),
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
data.balances.len()
));
self.send_alert(&message).await
} else {
Ok(())
}
}
#[cfg(feature = "trading")]
pub async fn send_order_placed(
&self,
response: &crate::models::OrderResponse,
params: &crate::models::OrderParams,
) -> Result<()> {
let side_emoji = match params.side {
crate::models::OrderSide::Buy => "🟢",
crate::models::OrderSide::Sell => "🔴",
};
let order_type = format!("{:?}", params.order_type);
let message = format!(
"{} Order Placed\n\
{}\n\
\n\
Order ID: {}\n\
Symbol: {}\n\
Side: {} {:?}\n\
Type: {}\n\
Quantity: {}\n\
{}\n\
Status: {:?}\n\
\n\
{} Order successfully submitted to exchange",
side_emoji,
"═".repeat(35),
response.order_id,
params.symbol,
side_emoji,
params.side,
order_type,
params
.order_qty
.map(|q| format!("{:.6}", q))
.unwrap_or("N/A".to_string()),
match params.limit_price {
Some(price) => format!("Limit Price: ${:.2}", price),
None => "Market Price".to_string(),
},
response.order_status,
if params.validate.unwrap_or(false) {
"✓"
} else {
"💸"
}
);
self.send_alert(&message).await
}
#[cfg(feature = "trading")]
pub async fn send_order_filled(
&self,
symbol: &str,
side: &crate::models::OrderSide,
quantity: f64,
price: f64,
order_id: &str,
) -> Result<()> {
let side_emoji = match side {
crate::models::OrderSide::Buy => "🟢",
crate::models::OrderSide::Sell => "🔴",
};
let total_value = quantity * price;
let message = format!(
"✅ Order Filled\n\
{}\n\
\n\
Symbol: {}\n\
Side: {} {:?}\n\
Filled: {:.6}\n\
Price: ${:.2}\n\
Total: ${:.2}\n\
\n\
Order ID: {}\n\
\n\
💰 Trade executed successfully",
"═".repeat(35),
symbol,
side_emoji,
side,
quantity,
price,
total_value,
order_id
);
self.send_alert(&message).await
}
#[cfg(feature = "trading")]
pub async fn send_order_cancelled(
&self,
symbol: &str,
order_id: &str,
reason: Option<&str>,
) -> Result<()> {
let message = format!(
"🚫 Order Cancelled\n\
{}\n\
\n\
Symbol: {}\n\
Order ID: {}\n\
{}\n\
\n\
ℹ️ Order removed from orderbook",
"═".repeat(35),
symbol,
order_id,
reason.map(|r| format!("Reason: {}", r)).unwrap_or_default()
);
self.send_alert(&message).await
}
#[cfg(feature = "trading")]
pub async fn send_order_failed(
&self,
params: &crate::models::OrderParams,
error: &str,
) -> Result<()> {
let message = format!(
"❌ Order Failed\n\
{}\n\
\n\
Symbol: {}\n\
Side: {:?}\n\
Type: {:?}\n\
\n\
Error: {}\n\
\n\
⚠️ Please check order parameters and try again",
"═".repeat(35),
params.symbol,
params.side,
params.order_type,
error
);
self.send_alert(&message).await
}
#[cfg(feature = "trading")]
pub async fn send_order_amended(
&self,
response: &crate::models::AmendOrderResponse,
params: &crate::models::AmendOrderParams,
) -> Result<()> {
let mut changes = Vec::new();
if let Some(qty) = params.order_qty {
changes.push(format!("Quantity: {:.6}", qty));
}
if let Some(price) = params.limit_price {
changes.push(format!("Limit Price: ${:.2}", price));
}
if let Some(trigger) = params.trigger_price {
changes.push(format!("Trigger Price: ${:.2}", trigger));
}
let message = format!(
"📝 Order Amended\n\
{}\n\
\n\
Order ID: {}\n\
\n\
Changes:\n\
{}\n\
\n\
{} Order successfully modified",
"═".repeat(35),
response.order_id,
changes.join("\n"),
if response.success { "✅" } else { "❌" }
);
self.send_alert(&message).await
}
#[cfg(feature = "trading")]
pub async fn send_trading_summary(
&self,
total_trades: usize,
total_volume: f64,
profit_loss: f64,
win_rate: f64,
) -> Result<()> {
let pl_emoji = if profit_loss >= 0.0 { "📈" } else { "📉" };
let pl_sign = if profit_loss >= 0.0 { "+" } else { "" };
let message = format!(
"📊 Daily Trading Summary\n\
{}\n\
{}\n\
\n\
Total Trades: {}\n\
Total Volume: ${:.2}\n\
\n\
{} P&L: {}{:.2}\n\
Win Rate: {:.1}%\n\
\n\
{} End of day report",
"═".repeat(35),
chrono::Utc::now().format("%Y-%m-%d"),
total_trades,
total_volume,
pl_emoji,
pl_sign,
profit_loss,
win_rate,
"📋"
);
self.send_alert(&message).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_notifier_creation() {
let notifier = TelegramNotifier::new("test_token", 12345);
assert_eq!(notifier.chat_id, ChatId(12345));
}
#[cfg(feature = "analytics")]
#[test]
fn test_signal_formatting() {
let signals = vec![
ImbalanceSignal::Bullish,
ImbalanceSignal::Bearish,
ImbalanceSignal::Neutral,
];
for signal in signals {
let _ = match signal {
ImbalanceSignal::Bullish => "🟢",
ImbalanceSignal::Bearish => "🔴",
ImbalanceSignal::Neutral => "⚪",
};
}
}
}