0206ef7cbb
Real-time orderflow trading system with absorption, initiative, sweep, exhaustion, and divergence detection. Features volume profile framing, state machine trade lifecycle, MT5 + Bybit feeds, FastAPI dashboard, and Telegram alerts for 30+ instruments. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
203 lines
7.3 KiB
Python
203 lines
7.3 KiB
Python
"""
|
|
Telegram Alert Bot
|
|
Sends formatted trading alerts via Telegram with signal details,
|
|
key levels, and suggested risk management.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from orderflow_system.signals.aggregator import AggregatedSignal
|
|
from orderflow_system.signals.profile_framing import DailyBias
|
|
from orderflow_system.data.models import Side, TradeState, TradePhase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class TelegramAlertBot:
|
|
"""
|
|
Sends trading alerts to a Telegram chat.
|
|
Requires: pip install python-telegram-bot
|
|
Set bot_token and chat_id in config/settings.py
|
|
"""
|
|
|
|
def __init__(self, bot_token: str, chat_id: str):
|
|
self.bot_token = bot_token
|
|
self.chat_id = chat_id
|
|
self._bot = None
|
|
self._enabled = bool(bot_token and chat_id)
|
|
|
|
async def initialize(self):
|
|
if not self._enabled:
|
|
logger.warning(
|
|
"Telegram bot not configured — alerts will be logged only. "
|
|
"Set TELEGRAM bot_token and chat_id in config/settings.py"
|
|
)
|
|
return
|
|
|
|
try:
|
|
from telegram import Bot
|
|
self._bot = Bot(token=self.bot_token)
|
|
me = await self._bot.get_me()
|
|
logger.info(f"Telegram bot connected: @{me.username}")
|
|
except ImportError:
|
|
logger.warning("python-telegram-bot not installed. Alerts logged only.")
|
|
self._enabled = False
|
|
except Exception as e:
|
|
logger.error(f"Failed to initialize Telegram bot: {e}")
|
|
self._enabled = False
|
|
|
|
async def send_signal_alert(
|
|
self,
|
|
instrument: str,
|
|
agg_signal: AggregatedSignal,
|
|
bias: Optional[DailyBias] = None,
|
|
trade: Optional[TradeState] = None,
|
|
):
|
|
"""Send a formatted alert for a trading signal."""
|
|
direction = "🟢 LONG" if agg_signal.direction == Side.BUY else "🔴 SHORT"
|
|
|
|
# Action-specific emoji and header
|
|
action_map = {
|
|
"enter": "🎯 ENTRY SIGNAL",
|
|
"break_even": "🔒 BREAK EVEN",
|
|
"trail": "📈 TRAIL UPDATE",
|
|
"exit": "🚪 EXIT SIGNAL",
|
|
"exit_warning": "⚠️ EXIT WARNING",
|
|
"alert_only": "📊 ALERT",
|
|
}
|
|
header = action_map.get(agg_signal.action, "📊 SIGNAL")
|
|
|
|
# Build message
|
|
lines = [
|
|
f"━━━ {header} ━━━",
|
|
f"📌 {instrument} | {direction}",
|
|
f"💪 Score: {agg_signal.composite_score:.0f}/100",
|
|
"",
|
|
]
|
|
|
|
# Signal details
|
|
for sig in agg_signal.signals:
|
|
sig_type = sig.signal_type.value.upper().replace("_", " ")
|
|
lines.append(f"🔍 {sig_type} @ {sig.price_level:.2f} (str: {sig.strength:.0f})")
|
|
if sig.details:
|
|
for k, v in sig.details.items():
|
|
lines.append(f" • {k}: {v}")
|
|
|
|
lines.append("")
|
|
|
|
# Entry action details
|
|
if agg_signal.action == "enter":
|
|
lines.extend([
|
|
"📊 TRADE SETUP:",
|
|
f" Entry: ~{agg_signal.signals[0].price_level:.2f}" if agg_signal.signals else "",
|
|
f" Stop Loss: {agg_signal.suggested_sl:.2f}",
|
|
f" Take Profit: {agg_signal.suggested_tp:.2f}",
|
|
])
|
|
if agg_signal.suggested_sl and agg_signal.signals:
|
|
entry = agg_signal.signals[0].price_level
|
|
risk = abs(entry - agg_signal.suggested_sl)
|
|
reward = abs(agg_signal.suggested_tp - entry)
|
|
if risk > 0:
|
|
lines.append(f" R:R = 1:{reward/risk:.1f}")
|
|
|
|
elif agg_signal.action == "break_even" and trade:
|
|
lines.append(f"🔒 Move SL to {trade.break_even_price:.2f}")
|
|
|
|
elif agg_signal.action == "trail" and trade:
|
|
lines.append(f"📈 Trail SL to {trade.trail_stop:.2f}")
|
|
|
|
lines.append("")
|
|
|
|
# Bias context
|
|
if bias:
|
|
bias_emoji = {
|
|
"long": "🟢", "short": "🔴",
|
|
"neutral": "⚪", "warning": "🟠"
|
|
}
|
|
b_emoji = bias_emoji.get(bias.direction.value, "⚪")
|
|
lines.extend([
|
|
f"📉 DAILY BIAS: {b_emoji} {bias.direction.value.upper()} ({bias.confidence:.0f}%)",
|
|
f" Profile: {bias.profile_shape}",
|
|
f" POC: {bias.poc:.2f} | VAH: {bias.vah:.2f} | VAL: {bias.val:.2f}",
|
|
])
|
|
if bias.merged_vah:
|
|
lines.append(f" Merged: VAH={bias.merged_vah:.2f}, VAL={bias.merged_val:.2f}")
|
|
|
|
lines.extend(["", f"📝 {agg_signal.notes}", "━━━━━━━━━━━━━━━━━━━━"])
|
|
|
|
message = "\n".join(lines)
|
|
|
|
# Send via Telegram
|
|
if self._enabled and self._bot:
|
|
try:
|
|
await self._bot.send_message(
|
|
chat_id=self.chat_id,
|
|
text=message,
|
|
parse_mode=None, # Plain text for reliability
|
|
)
|
|
logger.info(f"Alert sent to Telegram: {header} {instrument}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to send Telegram alert: {e}")
|
|
logger.info(f"Alert (local):\n{message}")
|
|
else:
|
|
# Log to console when Telegram not configured
|
|
logger.info(f"ALERT:\n{message}")
|
|
|
|
async def send_daily_bias_update(self, instrument: str, bias: DailyBias):
|
|
"""Send daily bias summary at session open."""
|
|
bias_emoji = {
|
|
"long": "🟢", "short": "🔴",
|
|
"neutral": "⚪", "warning": "🟠"
|
|
}
|
|
b_emoji = bias_emoji.get(bias.direction.value, "⚪")
|
|
|
|
lines = [
|
|
"━━━ 📊 DAILY BIAS UPDATE ━━━",
|
|
f"📌 {instrument}",
|
|
f"🧭 Bias: {b_emoji} {bias.direction.value.upper()} ({bias.confidence:.0f}%)",
|
|
f"📐 Shape: {bias.profile_shape}",
|
|
f" POC: {bias.poc:.2f}",
|
|
f" VAH: {bias.vah:.2f}",
|
|
f" VAL: {bias.val:.2f}",
|
|
]
|
|
|
|
if bias.lvn_levels:
|
|
lines.append(f" LVN: {', '.join(f'{l:.2f}' for l in bias.lvn_levels)}")
|
|
|
|
if bias.merged_vah:
|
|
lines.append(f" Merged VAH: {bias.merged_vah:.2f}")
|
|
lines.append(f" Merged VAL: {bias.merged_val:.2f}")
|
|
|
|
lines.extend(["", "🎯 QUALIFIED LEVELS:"])
|
|
for lv in bias.qualified_levels:
|
|
dir_str = "LONG" if lv.direction == Side.BUY else "SHORT"
|
|
lines.append(
|
|
f" {'🟢' if lv.direction == Side.BUY else '🔴'} "
|
|
f"{lv.level_type.value.upper()} @ {lv.price:.2f} → {dir_str} "
|
|
f"(str: {lv.strength:.0f})"
|
|
)
|
|
|
|
lines.extend([
|
|
"",
|
|
f"📝 {bias.notes}",
|
|
"━━━━━━━━━━━━━━━━━━━━",
|
|
])
|
|
|
|
message = "\n".join(lines)
|
|
|
|
if self._enabled and self._bot:
|
|
try:
|
|
await self._bot.send_message(
|
|
chat_id=self.chat_id,
|
|
text=message,
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Failed to send bias update: {e}")
|
|
logger.info(f"Bias (local):\n{message}")
|
|
else:
|
|
logger.info(f"BIAS UPDATE:\n{message}")
|