refactor: separate all Telegram code from main_live.py into dedicated modules
- telegram_notifier.py: low-level API (send, format, poll, command system) - telegram_commands.py: command handlers (/status /market /risk /positions /daily /filters /help) - telegram_notifications.py: notification helpers (startup, shutdown, trade open/close, hourly, alerts) - main_live.py reduced by ~400 lines — only 4 infrastructure calls remain (set_balance, close, poll) - Auto-send limited to: startup, hourly report, trade open, trade close - Market update & daily summary available on-demand via Telegram commands - Win rate tracking added to trade close notifications Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e8355b3f62
commit
b9e283878a
@@ -0,0 +1,615 @@
|
||||
"""
|
||||
Telegram Notification Helpers
|
||||
=============================
|
||||
Extracts all notification logic from main_live.py into a single module.
|
||||
|
||||
This module handles:
|
||||
- Building context dicts from bot state
|
||||
- Sending trade open/close notifications
|
||||
- Sending market updates, hourly reports
|
||||
- Sending critical alerts, emergency notifications
|
||||
- Startup & shutdown notifications
|
||||
|
||||
Integration:
|
||||
from src.telegram_notifications import TelegramNotifications
|
||||
self.notifications = TelegramNotifications(bot)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TelegramNotifications:
|
||||
"""
|
||||
High-level notification helper that reads bot state and sends
|
||||
formatted Telegram messages via bot.telegram (TelegramNotifier).
|
||||
"""
|
||||
|
||||
def __init__(self, bot):
|
||||
"""
|
||||
Args:
|
||||
bot: TradingBot instance (has .telegram, .mt5, .smart_risk, etc.)
|
||||
"""
|
||||
self.bot = bot
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Startup notification
|
||||
# ------------------------------------------------------------------
|
||||
async def send_startup(self):
|
||||
"""Send bot startup notification with full context."""
|
||||
bot = self.bot
|
||||
balance = bot.mt5.account_balance or bot.config.capital
|
||||
session_status = bot.session_filter.get_status_report()
|
||||
risk_state = bot.smart_risk.get_state()
|
||||
risk_rec = bot.smart_risk.get_trading_recommendation()
|
||||
ml_status = (
|
||||
f"Loaded ({len(bot.ml_model.feature_names)} features)"
|
||||
if bot.ml_model.fitted
|
||||
else "Not loaded"
|
||||
)
|
||||
|
||||
ctx = {
|
||||
"risk_per_trade": bot.config.risk.risk_per_trade,
|
||||
"max_daily_loss": bot.config.risk.max_daily_loss,
|
||||
"max_total_loss": bot.smart_risk.max_total_loss_percent,
|
||||
"max_lot": bot.smart_risk.max_lot_size,
|
||||
"max_positions": bot.smart_risk.max_concurrent_positions,
|
||||
"cooldown_seconds": bot._trade_cooldown_seconds,
|
||||
"daily_loss": risk_state.daily_loss,
|
||||
"total_loss": bot.smart_risk._total_loss,
|
||||
"consecutive_losses": risk_state.consecutive_losses,
|
||||
"risk_mode": risk_rec.get("mode", "normal"),
|
||||
"session": session_status.get("current_session", "Unknown"),
|
||||
"can_trade": session_status.get("can_trade", False),
|
||||
"volatility": session_status.get("volatility", "unknown"),
|
||||
}
|
||||
|
||||
await bot.telegram.send_startup_message(
|
||||
symbol=bot.config.symbol,
|
||||
capital=bot.config.capital,
|
||||
balance=balance,
|
||||
mode=bot.config.capital_mode.value,
|
||||
ml_model_status=ml_status,
|
||||
news_status="DISABLED",
|
||||
context=ctx,
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Shutdown notification
|
||||
# ------------------------------------------------------------------
|
||||
async def send_shutdown(self):
|
||||
"""Send bot shutdown notification with session summary."""
|
||||
bot = self.bot
|
||||
try:
|
||||
balance = bot.mt5.account_balance or bot.config.capital
|
||||
uptime_hours = (datetime.now() - bot._start_time).total_seconds() / 3600
|
||||
risk_state = bot.smart_risk.get_state()
|
||||
|
||||
ctx = {
|
||||
"risk_mode": bot.smart_risk.get_trading_recommendation().get("mode", "normal"),
|
||||
"daily_loss": risk_state.daily_loss,
|
||||
"daily_profit": risk_state.daily_profit,
|
||||
"total_loss": bot.smart_risk._total_loss,
|
||||
"consecutive_losses": risk_state.consecutive_losses,
|
||||
"session": bot.session_filter.get_status_report().get("current_session", "Unknown"),
|
||||
}
|
||||
|
||||
await bot.telegram.send_shutdown_message(
|
||||
balance=balance,
|
||||
total_trades=bot._total_session_trades,
|
||||
total_profit=bot._total_session_profit,
|
||||
uptime_hours=uptime_hours,
|
||||
context=ctx,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send shutdown notification: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trade close — smart position manager
|
||||
# ------------------------------------------------------------------
|
||||
async def notify_trade_close_smart(
|
||||
self,
|
||||
ticket: int,
|
||||
profit: float,
|
||||
current_price: float,
|
||||
reason: str,
|
||||
):
|
||||
"""Send notification for smart close (from SmartRiskManager)."""
|
||||
bot = self.bot
|
||||
try:
|
||||
trade_info = bot._open_trade_info.pop(ticket, {})
|
||||
|
||||
balance_before = trade_info.get("balance_before", 0)
|
||||
balance_after = bot.mt5.account_balance or 0
|
||||
entry_price = trade_info.get("entry_price", current_price)
|
||||
duration = int(
|
||||
(datetime.now() - trade_info.get("open_time", datetime.now())).total_seconds()
|
||||
)
|
||||
|
||||
# Track stats
|
||||
bot._total_session_profit += profit
|
||||
bot._total_session_trades += 1
|
||||
if profit > 0:
|
||||
bot._total_session_wins += 1
|
||||
|
||||
ctx = self._build_close_context(reason)
|
||||
|
||||
await bot.telegram.notify_trade_close(
|
||||
ticket=ticket,
|
||||
symbol=bot.config.symbol,
|
||||
order_type=trade_info.get("direction", "BUY"),
|
||||
lot_size=trade_info.get("lot_size", 0.01),
|
||||
entry_price=entry_price,
|
||||
close_price=current_price,
|
||||
profit=profit,
|
||||
profit_pips=(current_price - entry_price) / 0.1,
|
||||
balance_before=balance_before,
|
||||
balance_after=balance_after,
|
||||
duration_seconds=duration,
|
||||
ml_confidence=trade_info.get("ml_confidence", 0),
|
||||
regime=trade_info.get("regime", "unknown"),
|
||||
volatility=trade_info.get("volatility", "unknown"),
|
||||
context=ctx,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send close notification: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trade close — position manager action
|
||||
# ------------------------------------------------------------------
|
||||
async def notify_trade_close_action(self, action, current_price: float):
|
||||
"""Send notification for close via PositionManager action."""
|
||||
bot = self.bot
|
||||
try:
|
||||
ticket = action.ticket
|
||||
trade_info = bot._open_trade_info.pop(ticket, {})
|
||||
entry_price = trade_info.get("entry_price", current_price)
|
||||
open_time = trade_info.get("open_time", datetime.now())
|
||||
balance_before = trade_info.get("balance_before", bot._daily_start_balance)
|
||||
ml_confidence = trade_info.get("ml_confidence", 0)
|
||||
regime = trade_info.get("regime", "unknown")
|
||||
volatility = trade_info.get("volatility", "unknown")
|
||||
|
||||
balance_after = bot.mt5.account_balance or bot.config.capital
|
||||
|
||||
profit = action.profit if hasattr(action, "profit") else 0
|
||||
if profit == 0:
|
||||
profit = balance_after - balance_before
|
||||
|
||||
duration_seconds = int((datetime.now() - open_time).total_seconds())
|
||||
|
||||
price_diff = current_price - entry_price
|
||||
profit_pips = price_diff / 0.1 if "XAU" in bot.config.symbol else price_diff / 0.0001
|
||||
|
||||
# Track session stats
|
||||
bot._total_session_profit += profit
|
||||
bot._total_session_trades += 1
|
||||
if profit > 0:
|
||||
bot._total_session_wins += 1
|
||||
|
||||
exit_reason = action.reason if hasattr(action, "reason") else "position_manager"
|
||||
ctx = self._build_close_context(exit_reason)
|
||||
|
||||
await bot.telegram.notify_trade_close(
|
||||
ticket=ticket,
|
||||
symbol=bot.config.symbol,
|
||||
order_type=trade_info.get("direction", "BUY"),
|
||||
lot_size=trade_info.get("lot_size", 0.01),
|
||||
entry_price=entry_price,
|
||||
close_price=current_price,
|
||||
profit=profit,
|
||||
profit_pips=profit_pips,
|
||||
balance_before=balance_before,
|
||||
balance_after=balance_after,
|
||||
duration_seconds=duration_seconds,
|
||||
ml_confidence=ml_confidence,
|
||||
regime=regime,
|
||||
volatility=volatility,
|
||||
context=ctx,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send trade close notification: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Trade open notification
|
||||
# ------------------------------------------------------------------
|
||||
async def notify_trade_open(
|
||||
self,
|
||||
result,
|
||||
signal,
|
||||
position,
|
||||
regime: str,
|
||||
volatility: str,
|
||||
session_status: dict,
|
||||
*,
|
||||
safe_mode: bool = False,
|
||||
smc_fvg: bool = False,
|
||||
smc_ob: bool = False,
|
||||
smc_bos: bool = False,
|
||||
smc_choch: bool = False,
|
||||
dynamic_threshold=None,
|
||||
market_quality=None,
|
||||
market_score=None,
|
||||
):
|
||||
"""Send trade open notification."""
|
||||
bot = self.bot
|
||||
|
||||
# Store trade info for close notification (only if not already stored)
|
||||
# Safe mode pre-stores with actual fill price/lot, so don't overwrite
|
||||
if result.order_id not in bot._open_trade_info:
|
||||
bot._open_trade_info[result.order_id] = {
|
||||
"entry_price": signal.entry_price,
|
||||
"open_time": datetime.now(),
|
||||
"balance_before": bot.mt5.account_balance,
|
||||
"ml_confidence": signal.confidence,
|
||||
"regime": regime,
|
||||
"volatility": volatility,
|
||||
"direction": signal.signal_type,
|
||||
"lot_size": position.lot_size,
|
||||
}
|
||||
|
||||
risk_state = bot.smart_risk.get_state()
|
||||
risk_rec = bot.smart_risk.get_trading_recommendation()
|
||||
|
||||
ctx = {
|
||||
"dynamic_threshold": (
|
||||
float(dynamic_threshold)
|
||||
if dynamic_threshold is not None
|
||||
else getattr(bot, "_last_dynamic_threshold", bot.config.ml.confidence_threshold)
|
||||
),
|
||||
"market_quality": (
|
||||
str(market_quality)
|
||||
if market_quality is not None
|
||||
else getattr(bot, "_last_market_quality", "unknown")
|
||||
),
|
||||
"market_score": (
|
||||
int(market_score)
|
||||
if market_score is not None
|
||||
else getattr(bot, "_last_market_score", 0)
|
||||
),
|
||||
"smc_signal": getattr(bot, "_last_raw_smc_signal", ""),
|
||||
"smc_confidence": getattr(bot, "_last_raw_smc_confidence", 0),
|
||||
"smc_fvg": smc_fvg or getattr(signal, "fvg_detected", False),
|
||||
"smc_ob": smc_ob or getattr(signal, "ob_detected", False),
|
||||
"smc_bos": smc_bos or getattr(signal, "bos_detected", False),
|
||||
"smc_choch": smc_choch or getattr(signal, "choch_detected", False),
|
||||
"session": session_status.get("current_session", "Unknown"),
|
||||
"h1_bias": getattr(bot, "_h1_bias_cache", "NEUTRAL"),
|
||||
"risk_mode": risk_rec.get("mode", "normal"),
|
||||
"daily_loss": risk_state.daily_loss,
|
||||
"consecutive_losses": risk_state.consecutive_losses,
|
||||
"entry_filters": getattr(bot, "_last_filter_results", []),
|
||||
}
|
||||
|
||||
reason = f"SAFE MODE: {signal.reason}" if safe_mode else signal.reason
|
||||
sl = 0 if safe_mode else signal.stop_loss
|
||||
|
||||
try:
|
||||
await bot.telegram.notify_trade_open(
|
||||
ticket=result.order_id,
|
||||
symbol=bot.config.symbol,
|
||||
order_type=signal.signal_type,
|
||||
lot_size=position.lot_size,
|
||||
entry_price=signal.entry_price,
|
||||
stop_loss=sl,
|
||||
take_profit=signal.take_profit,
|
||||
ml_confidence=signal.confidence,
|
||||
signal_reason=reason,
|
||||
regime=regime,
|
||||
volatility=volatility,
|
||||
context=ctx,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send trade open notification: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Critical limit alert
|
||||
# ------------------------------------------------------------------
|
||||
async def send_critical_limit_alert(
|
||||
self,
|
||||
limit_type: str,
|
||||
current_loss: float,
|
||||
max_loss: float,
|
||||
max_percent: float,
|
||||
):
|
||||
"""Send critical alert when loss limits are reached."""
|
||||
logger.critical("=" * 60)
|
||||
logger.critical(f"CRITICAL: {limit_type} REACHED!")
|
||||
logger.critical(f"Loss: ${current_loss:.2f} / ${max_loss:.2f} ({max_percent}%)")
|
||||
logger.critical("TRADING HAS BEEN STOPPED!")
|
||||
logger.critical("=" * 60)
|
||||
|
||||
try:
|
||||
if limit_type == "TOTAL LOSS LIMIT":
|
||||
message = (
|
||||
f"🚨🚨 CRITICAL: TOTAL LOSS LIMIT REACHED 🚨🚨\n\n"
|
||||
f"Total Loss: ${current_loss:.2f}\n"
|
||||
f"Limit: ${max_loss:.2f} ({max_percent}%)\n\n"
|
||||
f"⛔ TRADING STOPPED PERMANENTLY\n"
|
||||
f"Manual reset required to resume trading.\n\n"
|
||||
f"Please review your trading strategy."
|
||||
)
|
||||
else:
|
||||
message = (
|
||||
f"🚨 DAILY LOSS LIMIT REACHED 🚨\n\n"
|
||||
f"Daily Loss: ${current_loss:.2f}\n"
|
||||
f"Limit: ${max_loss:.2f} ({max_percent}%)\n\n"
|
||||
f"⛔ TRADING STOPPED FOR TODAY\n"
|
||||
f"Will resume tomorrow automatically."
|
||||
)
|
||||
|
||||
await self.bot.telegram.send_message(message)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send critical alert: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Emergency close notification
|
||||
# ------------------------------------------------------------------
|
||||
async def send_emergency_close_result(
|
||||
self,
|
||||
closed_count: int,
|
||||
failed_tickets: list,
|
||||
):
|
||||
"""Send notification after emergency close attempt."""
|
||||
try:
|
||||
if failed_tickets:
|
||||
await self.bot.telegram.send_message(
|
||||
f"🚨 EMERGENCY CLOSE FAILED!\n\n"
|
||||
f"Failed tickets: {failed_tickets}\n"
|
||||
f"Please close manually!"
|
||||
)
|
||||
else:
|
||||
await self.bot.telegram.send_message(
|
||||
f"🚨 EMERGENCY CLOSE COMPLETE\n\n"
|
||||
f"Closed {closed_count} positions due to flash crash detection"
|
||||
)
|
||||
except Exception:
|
||||
pass # Don't let telegram failure stop us
|
||||
|
||||
async def send_flash_crash_critical(self, move_pct: float, error):
|
||||
"""Send critical alert when flash crash emergency close fails."""
|
||||
try:
|
||||
await self.bot.telegram.send_message(
|
||||
f"🚨🚨 CRITICAL ERROR 🚨🚨\n\n"
|
||||
f"Flash crash detected but emergency close FAILED!\n"
|
||||
f"Error: {error}\n\n"
|
||||
f"MANUAL INTERVENTION REQUIRED!"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Market update (on-demand via command, not auto-sent)
|
||||
# ------------------------------------------------------------------
|
||||
async def send_market_update(self, df, regime_state, ml_prediction):
|
||||
"""Send market update to Telegram."""
|
||||
bot = self.bot
|
||||
try:
|
||||
now = datetime.now()
|
||||
|
||||
if bot._last_market_update_time:
|
||||
time_since = (now - bot._last_market_update_time).total_seconds()
|
||||
if time_since < 1800:
|
||||
return
|
||||
|
||||
session_status = bot.session_filter.get_status_report()
|
||||
|
||||
atr = df["atr"].tail(1).item() if "atr" in df.columns else 0
|
||||
tick = bot.mt5.get_tick(bot.config.symbol)
|
||||
spread = (tick.ask - tick.bid) if tick else 0
|
||||
|
||||
if "ema_9" in df.columns and "ema_21" in df.columns:
|
||||
ema_9 = df["ema_9"].tail(1).item()
|
||||
ema_21 = df["ema_21"].tail(1).item()
|
||||
trend_direction = "UPTREND" if ema_9 > ema_21 else "DOWNTREND"
|
||||
else:
|
||||
trend_direction = "NEUTRAL"
|
||||
|
||||
ctx = {
|
||||
"h1_bias": getattr(bot, "_h1_bias_cache", "NEUTRAL"),
|
||||
"dynamic_threshold": getattr(bot, "_last_dynamic_threshold", 0.55),
|
||||
"market_quality": getattr(bot, "_last_market_quality", "unknown"),
|
||||
"market_score": getattr(bot, "_last_market_score", 0),
|
||||
"smc_signal": getattr(bot, "_last_raw_smc_signal", ""),
|
||||
"smc_confidence": getattr(bot, "_last_raw_smc_confidence", 0),
|
||||
"consecutive_losses": bot.smart_risk.get_state().consecutive_losses,
|
||||
"risk_mode": bot.smart_risk.get_trading_recommendation().get("mode", "normal"),
|
||||
"daily_loss": bot.smart_risk.get_state().daily_loss,
|
||||
"session_trades": bot._total_session_trades,
|
||||
"session_profit": bot._total_session_profit,
|
||||
}
|
||||
|
||||
await bot.telegram.notify_market_update(
|
||||
symbol=bot.config.symbol,
|
||||
price=df["close"].tail(1).item(),
|
||||
regime=regime_state.regime.value if regime_state else "unknown",
|
||||
volatility=session_status.get("volatility", "unknown"),
|
||||
ml_signal=ml_prediction.signal,
|
||||
ml_confidence=ml_prediction.confidence,
|
||||
trend_direction=trend_direction,
|
||||
session=session_status.get("current_session", "Unknown"),
|
||||
can_trade=session_status.get("can_trade", True),
|
||||
atr=atr,
|
||||
spread=spread,
|
||||
context=ctx,
|
||||
)
|
||||
|
||||
bot._last_market_update_time = now
|
||||
logger.info("Telegram: Market update sent")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send market update: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Daily summary
|
||||
# ------------------------------------------------------------------
|
||||
async def send_daily_summary(self):
|
||||
"""Send daily trading summary to Telegram."""
|
||||
bot = self.bot
|
||||
try:
|
||||
balance = bot.mt5.account_balance or bot.config.capital
|
||||
await bot.telegram.send_daily_summary(
|
||||
start_balance=bot._daily_start_balance,
|
||||
end_balance=balance,
|
||||
)
|
||||
logger.info("Telegram: Daily summary sent")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send daily summary: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Hourly analysis report
|
||||
# ------------------------------------------------------------------
|
||||
async def send_hourly_analysis_if_due(
|
||||
self,
|
||||
df,
|
||||
regime_state,
|
||||
ml_prediction,
|
||||
open_positions,
|
||||
current_price: float,
|
||||
):
|
||||
"""Send comprehensive hourly analysis report. Interval: 1 hour."""
|
||||
bot = self.bot
|
||||
now = datetime.now()
|
||||
|
||||
if bot._last_hourly_report_time:
|
||||
time_since = (now - bot._last_hourly_report_time).total_seconds()
|
||||
if time_since < 3600:
|
||||
return
|
||||
|
||||
try:
|
||||
balance = bot.mt5.account_balance or bot.config.capital
|
||||
equity = bot.mt5.account_equity or bot.config.capital
|
||||
floating_pnl = equity - balance
|
||||
|
||||
# Position details with Smart Risk data
|
||||
position_details = []
|
||||
for row in open_positions.iter_rows(named=True):
|
||||
ticket = row["ticket"]
|
||||
profit = row.get("profit", 0)
|
||||
position_type = row.get("type", 0)
|
||||
direction = "BUY" if position_type == 0 else "SELL"
|
||||
|
||||
guard = bot.smart_risk._position_guards.get(ticket)
|
||||
momentum = guard.momentum_score if guard else 0
|
||||
tp_prob = guard.get_tp_probability() if guard else 50
|
||||
|
||||
position_details.append({
|
||||
"ticket": ticket,
|
||||
"direction": direction,
|
||||
"profit": profit,
|
||||
"momentum": momentum,
|
||||
"tp_probability": tp_prob,
|
||||
})
|
||||
|
||||
session_status = bot.session_filter.get_status_report()
|
||||
|
||||
market_analysis = bot.dynamic_confidence.analyze_market(
|
||||
session=session_status.get("current_session", "Unknown"),
|
||||
regime=regime_state.regime.value if regime_state else "unknown",
|
||||
volatility=session_status.get("volatility", "medium"),
|
||||
trend_direction=regime_state.regime.value if regime_state else "neutral",
|
||||
has_smc_signal=False,
|
||||
ml_signal=ml_prediction.signal,
|
||||
ml_confidence=ml_prediction.confidence,
|
||||
)
|
||||
|
||||
risk_rec = bot.smart_risk.get_trading_recommendation()
|
||||
|
||||
avg_exec = (
|
||||
(sum(bot._execution_times) / len(bot._execution_times) * 1000)
|
||||
if bot._execution_times
|
||||
else 0
|
||||
)
|
||||
uptime = (now - bot._start_time).total_seconds() / 3600
|
||||
|
||||
# Get ATR and spread
|
||||
atr = 0
|
||||
spread = 0
|
||||
try:
|
||||
df_latest = bot.mt5.get_market_data(
|
||||
symbol=bot.config.symbol,
|
||||
timeframe=bot.config.execution_timeframe,
|
||||
count=50,
|
||||
)
|
||||
if len(df_latest) > 0 and "atr" in df_latest.columns:
|
||||
atr = df_latest["atr"].tail(1).item()
|
||||
tick = bot.mt5.get_tick(bot.config.symbol)
|
||||
spread = (tick.ask - tick.bid) if tick else 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
ctx = {
|
||||
"h1_bias": getattr(bot, "_h1_bias_cache", "NEUTRAL"),
|
||||
"smc_signal": getattr(bot, "_last_raw_smc_signal", ""),
|
||||
"smc_confidence": getattr(bot, "_last_raw_smc_confidence", 0),
|
||||
"atr": atr,
|
||||
"spread": spread,
|
||||
"total_loss": bot.smart_risk._total_loss,
|
||||
"consecutive_losses": bot.smart_risk.get_state().consecutive_losses,
|
||||
"entry_filters": getattr(bot, "_last_filter_results", []),
|
||||
}
|
||||
|
||||
await bot.telegram.send_hourly_analysis(
|
||||
balance=balance,
|
||||
equity=equity,
|
||||
floating_pnl=floating_pnl,
|
||||
open_positions=len(open_positions),
|
||||
position_details=position_details,
|
||||
symbol=bot.config.symbol,
|
||||
current_price=current_price,
|
||||
session=session_status.get("current_session", "Unknown"),
|
||||
regime=regime_state.regime.value if regime_state else "unknown",
|
||||
volatility=session_status.get("volatility", "unknown"),
|
||||
ml_signal=ml_prediction.signal,
|
||||
ml_confidence=ml_prediction.confidence,
|
||||
dynamic_threshold=market_analysis.confidence_threshold,
|
||||
market_quality=market_analysis.quality.value,
|
||||
market_score=market_analysis.score,
|
||||
daily_pnl=bot._total_session_profit,
|
||||
daily_trades=bot._total_session_trades,
|
||||
risk_mode=risk_rec.get("mode", "normal"),
|
||||
max_daily_loss=bot.smart_risk.max_daily_loss_usd,
|
||||
uptime_hours=uptime,
|
||||
total_loops=bot._loop_count,
|
||||
avg_execution_ms=avg_exec,
|
||||
news_status="DISABLED",
|
||||
news_reason="News agent disabled",
|
||||
context=ctx,
|
||||
)
|
||||
|
||||
bot._last_hourly_report_time = now
|
||||
logger.info("Telegram: Hourly analysis report sent")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to send hourly analysis: {e}")
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ------------------------------------------------------------------
|
||||
def _build_close_context(self, exit_reason: str) -> dict:
|
||||
"""Build context dict for trade close notifications."""
|
||||
bot = self.bot
|
||||
risk_state = bot.smart_risk.get_state()
|
||||
win_rate = (
|
||||
(bot._total_session_wins / bot._total_session_trades * 100)
|
||||
if bot._total_session_trades > 0
|
||||
else 0
|
||||
)
|
||||
return {
|
||||
"exit_reason": exit_reason,
|
||||
"risk_mode": bot.smart_risk.get_trading_recommendation().get("mode", "normal"),
|
||||
"daily_loss": risk_state.daily_loss,
|
||||
"daily_profit": risk_state.daily_profit,
|
||||
"consecutive_losses": risk_state.consecutive_losses,
|
||||
"total_loss": bot.smart_risk._total_loss,
|
||||
"session_trades": bot._total_session_trades,
|
||||
"session_wins": bot._total_session_wins,
|
||||
"session_profit": bot._total_session_profit,
|
||||
"win_rate": win_rate,
|
||||
"session": bot.session_filter.get_status_report().get("current_session", "Unknown"),
|
||||
}
|
||||
Reference in New Issue
Block a user