feat: Telegram notifications + SSE position stream + PnL summary

- Add telegram.rs: send/edit messages via Bot API
- Add SSE position listener: position_opened → edit "Filled",
  position_closed → edit TP/SL result + send daily+alltime PnL summary
- Extend State with tg_message_id and trade details for notifications
- Add PosState to persist position info across SSE events
- Add Deal domain type and history_deals() client method
- New env: TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID, TELEGRAM_THREAD_ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
romysaputrasihananda
2026-06-11 01:28:45 +07:00
co-authored by Claude Sonnet 4.6
parent 0583b6d0fb
commit a49aee6ae0
10 changed files with 575 additions and 110 deletions
+26
View File
@@ -0,0 +1,26 @@
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::Deserialize;
use crate::serde_helpers;
#[derive(Debug, Clone, Deserialize)]
pub struct Deal {
pub ticket: u64,
#[serde(with = "serde_helpers::naive_utc_secs")]
pub time: DateTime<Utc>,
pub entry: u8, // 0 = open, 1 = close
pub magic: u64,
#[serde(deserialize_with = "serde_helpers::de_decimal")]
pub volume: Decimal,
#[serde(deserialize_with = "serde_helpers::de_decimal")]
pub price: Decimal,
#[serde(deserialize_with = "serde_helpers::de_decimal")]
pub profit: Decimal,
#[serde(deserialize_with = "serde_helpers::de_decimal")]
pub commission: Decimal,
#[serde(deserialize_with = "serde_helpers::de_decimal")]
pub swap: Decimal,
pub symbol: String,
pub comment: String,
}
+2
View File
@@ -2,6 +2,7 @@ mod serde_helpers;
pub mod account;
pub mod candle;
pub mod deal;
pub mod position;
pub mod symbol;
pub mod tick;
@@ -9,6 +10,7 @@ pub mod timeframe;
pub use account::AccountInfo;
pub use candle::Candle;
pub use deal::Deal;
pub use position::{Position, Side};
pub use symbol::Symbol;
pub use tick::Tick;
+17
View File
@@ -122,6 +122,23 @@ impl Mt5Client {
Ok(w.data)
}
pub async fn history_deals(
&self,
date_from: &str,
date_to: &str,
symbol: Option<&str>,
) -> Result<Vec<domain::Deal>, Mt5Error> {
let url = format!("{}/history/deals", self.base_url);
let mut req = self.http.get(&url)
.query(&[("date_from", date_from), ("date_to", date_to)]);
if let Some(sym) = symbol {
req = req.query(&[("symbol", sym)]);
}
let text = self.fetch_text(req).await?;
let w: DataVec<domain::Deal> = serde_json::from_str(&text)?;
Ok(w.data)
}
pub async fn place_order(
&self,
request: &TradeRequest,
+1
View File
@@ -4,4 +4,5 @@ mod types;
pub use client::Mt5Client;
pub use error::Mt5Error;
pub use domain::Deal;
pub use types::{HealthStatus, OrderCheckResult, PendingOrder, TradeRequest, TradeResult};