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:
GifariKemal
2026-02-09 07:07:04 +07:00
parent e8355b3f62
commit b9e283878a
4 changed files with 1672 additions and 615 deletions
+70 -418
View File
@@ -61,6 +61,7 @@ from src.position_manager import SmartPositionManager
from src.session_filter import SessionFilter, create_wib_session_filter
from src.auto_trainer import AutoTrainer, create_auto_trainer
from src.telegram_notifier import TelegramNotifier, create_telegram_notifier
from src.telegram_notifications import TelegramNotifications
from src.smart_risk_manager import SmartRiskManager, create_smart_risk_manager
from src.dynamic_confidence import DynamicConfidenceManager, create_dynamic_confidence
# from src.news_agent import NewsAgent, create_news_agent, MarketCondition # DISABLED
@@ -164,6 +165,9 @@ class TradingBot:
# Initialize Telegram Notifier - smart notifications
self.telegram = create_telegram_notifier()
# Initialize Telegram Notifications helper (extracts notification logic)
self.notifications = TelegramNotifications(self)
# News Agent DISABLED - backtest proved it costs $178 profit
# ML model already handles volatility well
self.news_agent = None
@@ -187,6 +191,7 @@ class TradingBot:
self._daily_start_balance: float = 0
self._total_session_profit: float = 0
self._total_session_trades: int = 0
self._total_session_wins: int = 0
self._last_market_update_time: Optional[datetime] = None
self._last_hourly_report_time: Optional[datetime] = None
self._open_trade_info: Dict = {} # Track trade info for close notification
@@ -599,10 +604,12 @@ class TradingBot:
"avgExecutionMs": round(avg_ms, 1),
"uptimeHours": round(uptime_hours, 1),
"totalSessionTrades": self._total_session_trades,
"totalSessionWins": self._total_session_wins,
"totalSessionProfit": round(self._total_session_profit, 2),
"winRate": round(self._total_session_wins / self._total_session_trades * 100, 1) if self._total_session_trades > 0 else 0,
}
except Exception:
return {"loopCount": 0, "avgExecutionMs": 0, "uptimeHours": 0, "totalSessionTrades": 0, "totalSessionProfit": 0}
return {"loopCount": 0, "avgExecutionMs": 0, "uptimeHours": 0, "totalSessionTrades": 0, "totalSessionWins": 0, "totalSessionProfit": 0, "winRate": 0}
def _get_market_close_status(self) -> dict:
"""Get market close timing info for dashboard."""
@@ -676,21 +683,16 @@ class TradingBot:
self.telegram.set_daily_start_balance(balance)
# Send Telegram startup notification
ml_status = f"Loaded ({len(self.ml_model.feature_names)} features)" if self.ml_model.fitted else "Not loaded"
await self.telegram.send_startup_message(
symbol=self.config.symbol,
capital=self.config.capital,
balance=balance,
mode=self.config.capital_mode.value,
ml_model_status=ml_status,
news_status="DISABLED",
)
await self.notifications.send_startup()
except Exception as e:
logger.error(f"Failed to connect to MT5: {e}")
if not self.simulation:
return
# Register Telegram commands
self._register_telegram_commands()
# Start main loop
self._running = True
self._dash_log("info", "Bot started - trading loop active")
@@ -702,21 +704,12 @@ class TradingBot:
logger.info("Stopping trading bot...")
self._running = False
# Calculate uptime
uptime_hours = (datetime.now() - self._start_time).total_seconds() / 3600
# Send Telegram shutdown notification
await self.notifications.send_shutdown()
try:
balance = self.mt5.account_balance or self.config.capital
await self.telegram.send_shutdown_message(
balance=balance,
total_trades=self._total_session_trades,
total_profit=self._total_session_profit,
uptime_hours=uptime_hours,
)
await self.telegram.close()
except Exception as e:
logger.error(f"Failed to send shutdown notification: {e}")
logger.error(f"Failed to close telegram session: {e}")
self.mt5.disconnect()
self._log_summary()
@@ -823,6 +816,11 @@ class TradingBot:
logger.debug(f"H1 bias error: {e}")
return "NEUTRAL"
def _register_telegram_commands(self):
"""Register Telegram command handlers from separate module."""
from src.telegram_commands import register_commands
register_commands(self)
async def _main_loop(self):
"""Main trading loop - CANDLE-BASED (not time-based)."""
last_position_check = time.time()
@@ -893,6 +891,12 @@ class TradingBot:
# Write dashboard status file (for Docker API)
self._write_dashboard_status()
# Poll Telegram commands (non-blocking, every loop)
try:
await self.telegram.poll_commands()
except Exception:
pass
# Wait before next check (5 seconds between candle checks)
await asyncio.sleep(5)
@@ -920,13 +924,7 @@ class TradingBot:
await self._emergency_close_all()
except Exception as e:
logger.critical(f"CRITICAL: Emergency close failed: {e}")
try:
await self.telegram.send_message(
f"CRITICAL: Flash crash {move_pct:.2f}% but emergency close FAILED!\n"
f"Error: {e}\nMANUAL INTERVENTION REQUIRED!"
)
except:
pass
await self.notifications.send_flash_crash_critical(move_pct, e)
return
# --- POSITION MANAGEMENT (uses cached data — Fix 4) ---
@@ -1034,18 +1032,9 @@ class TradingBot:
await self._emergency_close_all()
except Exception as e:
logger.critical(f"CRITICAL: Emergency close failed completely: {e}")
# Try to send alert even if close failed
try:
await self.telegram.send_message(
f"🚨🚨 CRITICAL ERROR 🚨🚨\n\n"
f"Flash crash detected but emergency close FAILED!\n"
f"Error: {e}\n\n"
f"MANUAL INTERVENTION REQUIRED!"
)
except:
pass
await self.notifications.send_flash_crash_critical(move_pct, e)
return
# 6. Check if trading is allowed
account_balance = self.mt5.account_balance or self.config.capital
account_equity = self.mt5.account_equity or self.config.capital
@@ -1092,7 +1081,7 @@ class TradingBot:
# Send hourly analysis report to Telegram (every 1 hour)
# Placed here to ensure it's sent regardless of trading conditions
await self._send_hourly_analysis_if_due(
await self.notifications.send_hourly_analysis_if_due(
df=df,
regime_state=regime_state,
ml_prediction=ml_prediction,
@@ -1164,9 +1153,9 @@ class TradingBot:
h1_tag = f" | H1: {h1_bias}" if h1_bias != "NEUTRAL" else ""
logger.info(f"Price: {price:.2f} | Regime: {regime_state.regime.value if regime_state else 'N/A'} | SMC: {smc_signal.signal_type if smc_signal else 'NONE'} | ML: {ml_prediction.signal}({ml_prediction.confidence:.0%}){h1_tag}")
# Send market update to Telegram (every 30 minutes) - only after first loop
if self._loop_count > 0 and self._loop_count % 30 == 0:
await self._send_market_update(df, regime_state, ml_prediction)
# Market update disabled from auto-send (available via command)
# if self._loop_count > 0 and self._loop_count % 30 == 0:
# await self._send_market_update(df, regime_state, ml_prediction)
# Track SMC signal for filter pipeline
self._last_filter_results.append({"name": "SMC Signal", "passed": smc_signal is not None, "detail": f"{smc_signal.signal_type} ({smc_signal.confidence:.0%})" if smc_signal else "No signal"})
@@ -1574,33 +1563,15 @@ class TradingBot:
session_status = self.session_filter.get_status_report()
volatility = session_status.get("volatility", "unknown")
# Store trade info for close notification
self._open_trade_info[result.order_id] = {
"entry_price": signal.entry_price,
"open_time": datetime.now(),
"balance_before": self.mt5.account_balance,
"ml_confidence": signal.confidence,
"regime": regime,
"volatility": volatility,
}
# Send Telegram notification
try:
await self.telegram.notify_trade_open(
ticket=result.order_id,
symbol=self.config.symbol,
order_type=signal.signal_type,
lot_size=position.lot_size,
entry_price=signal.entry_price,
stop_loss=signal.stop_loss,
take_profit=signal.take_profit,
ml_confidence=signal.confidence,
signal_reason=signal.reason,
regime=regime,
volatility=volatility,
)
except Exception as e:
logger.warning(f"Failed to send trade open notification: {e}")
# Send Telegram notification (stores trade info + builds context internally)
await self.notifications.notify_trade_open(
result=result,
signal=signal,
position=position,
regime=regime,
volatility=volatility,
session_status=session_status,
)
else:
logger.error(f"Order failed: {result.comment} (code: {result.retcode})")
@@ -1791,23 +1762,23 @@ class TradingBot:
except Exception as e:
logger.warning(f"Failed to log trade open: {e}")
# Send Telegram notification
try:
await self.telegram.notify_trade_open(
ticket=result.order_id,
symbol=self.config.symbol,
order_type=signal.signal_type,
lot_size=position.lot_size,
entry_price=signal.entry_price,
stop_loss=0, # No SL
take_profit=signal.take_profit,
ml_confidence=signal.confidence,
signal_reason=f"SAFE MODE: {signal.reason}",
regime=regime,
volatility=volatility,
)
except Exception as e:
logger.warning(f"Failed to send trade open notification: {e}")
# Send Telegram notification (stores trade info + builds context internally)
await self.notifications.notify_trade_open(
result=result,
signal=signal,
position=position,
regime=regime,
volatility=volatility,
session_status=session_status,
safe_mode=True,
smc_fvg=smc_fvg,
smc_ob=smc_ob,
smc_bos=smc_bos,
smc_choch=smc_choch,
dynamic_threshold=dynamic_threshold,
market_quality=market_quality,
market_score=market_score,
)
else:
logger.error(f"Order failed: {result.comment} (code: {result.retcode})")
@@ -1845,7 +1816,7 @@ class TradingBot:
risk_result = self.smart_risk.record_trade_result(profit)
self.smart_risk.unregister_position(action.ticket)
self.position_manager._peak_profits.pop(action.ticket, None)
await self._notify_trade_close_smart(action.ticket, profit, current_price, action.reason)
await self.notifications.notify_trade_close_smart(action.ticket, profit, current_price, action.reason)
logger.info(f"CLOSED #{action.ticket}: {action.reason}")
continue # Skip SmartRiskManager eval for this ticket
@@ -1928,18 +1899,18 @@ class TradingBot:
logger.warning(f"Failed to log trade close: {e}")
# Send notification
await self._notify_trade_close_smart(ticket, profit, current_price, message)
await self.notifications.notify_trade_close_smart(ticket, profit, current_price, message)
# Check for critical limit violations and send alerts
if risk_result.get("total_limit_hit"):
await self._send_critical_limit_alert(
await self.notifications.send_critical_limit_alert(
"TOTAL LOSS LIMIT",
risk_result.get("total_loss", 0),
self.smart_risk.max_total_loss_usd,
self.smart_risk.max_total_loss_percent
)
elif risk_result.get("daily_limit_hit"):
await self._send_critical_limit_alert(
await self.notifications.send_critical_limit_alert(
"DAILY LOSS LIMIT",
risk_result.get("daily_loss", 0),
self.smart_risk.max_daily_loss_usd,
@@ -1952,84 +1923,6 @@ class TradingBot:
if self._loop_count % 60 == 0:
logger.info(f"Position #{ticket}: {message}")
async def _notify_trade_close_smart(self, ticket: int, profit: float, current_price: float, reason: str):
"""Send notification for smart close."""
try:
trade_info = self._open_trade_info.pop(ticket, {})
balance_before = trade_info.get("balance_before", 0)
balance_after = self.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
self._total_session_profit += profit
self._total_session_trades += 1
await self.telegram.notify_trade_close(
ticket=ticket,
symbol=self.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"),
)
except Exception as e:
logger.warning(f"Failed to send close notification: {e}")
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.
Args:
limit_type: "DAILY LOSS LIMIT" or "TOTAL LOSS LIMIT"
current_loss: Current loss amount
max_loss: Maximum allowed loss
max_percent: Maximum loss percentage
"""
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.telegram.send_message(message)
except Exception as e:
logger.error(f"Failed to send critical alert: {e}")
async def _emergency_close_all(self, max_retries: int = 3):
"""
Emergency close all positions with retry logic and error handling.
@@ -2089,262 +1982,21 @@ class TradingBot:
if attempt < max_retries - 1:
await asyncio.sleep(2)
# Send critical alert if any failed
if failed_tickets:
alert_msg = f"CRITICAL: Failed to close {len(failed_tickets)} positions: {failed_tickets}"
logger.error(alert_msg)
try:
await self.telegram.send_message(
f"🚨 EMERGENCY CLOSE FAILED!\n\n"
f"Failed tickets: {failed_tickets}\n"
f"Please close manually!"
)
except:
pass # Don't let telegram failure stop us
else:
try:
await self.telegram.send_message(
f"🚨 EMERGENCY CLOSE COMPLETE\n\n"
f"Closed {closed_count} positions due to flash crash detection"
)
except:
pass
# Send critical alert
await self.notifications.send_emergency_close_result(closed_count, failed_tickets)
async def _notify_trade_close(self, action, current_price: float):
"""Send Telegram notification for trade close."""
try:
ticket = action.ticket
# Get trade info from our stored data
trade_info = self._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", self._daily_start_balance)
ml_confidence = trade_info.get("ml_confidence", 0)
regime = trade_info.get("regime", "unknown")
volatility = trade_info.get("volatility", "unknown")
# Get current balance (after close)
balance_after = self.mt5.account_balance or self.config.capital
# Calculate profit from action
profit = action.profit if hasattr(action, 'profit') else 0
if profit == 0:
# Try to calculate from price difference (rough estimate)
profit = balance_after - balance_before
# Calculate duration
duration_seconds = int((datetime.now() - open_time).total_seconds())
# Calculate pips (for XAUUSD, 1 pip = 0.1)
price_diff = current_price - entry_price
profit_pips = price_diff / 0.1 if "XAU" in self.config.symbol else price_diff / 0.0001
# Get order type from action
order_type = "BUY" # Default, will be extracted from action if available
# Track session stats
self._total_session_profit += profit
self._total_session_trades += 1
await self.telegram.notify_trade_close(
ticket=ticket,
symbol=self.config.symbol,
order_type=order_type,
lot_size=0.2, # Will be extracted from actual position if available
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,
)
except Exception as e:
logger.warning(f"Failed to send trade close notification: {e}")
async def _send_market_update(self, df, regime_state, ml_prediction):
"""Send periodic market update to Telegram."""
try:
now = datetime.now()
# Only send market update every 30 minutes
if self._last_market_update_time:
time_since = (now - self._last_market_update_time).total_seconds()
if time_since < 1800: # 30 minutes
return
session_status = self.session_filter.get_status_report()
# Get ATR and spread
atr = df["atr"].tail(1).item() if "atr" in df.columns else 0
tick = self.mt5.get_tick(self.config.symbol)
spread = (tick.ask - tick.bid) if tick else 0
# Determine trend direction
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"
await self.telegram.notify_market_update(
symbol=self.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,
)
self._last_market_update_time = now
logger.info("Telegram: Market update sent")
except Exception as e:
logger.warning(f"Failed to send market update: {e}")
async def _send_daily_summary(self):
"""Send daily trading summary to Telegram."""
try:
balance = self.mt5.account_balance or self.config.capital
await self.telegram.send_daily_summary(
start_balance=self._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}")
async def _send_hourly_analysis_if_due(
self,
df,
regime_state,
ml_prediction,
open_positions,
current_price: float,
):
"""
Send comprehensive hourly analysis report to Telegram.
Interval: Every 1 hour
"""
now = datetime.now()
# Check if 1 hour has passed since last report
if self._last_hourly_report_time:
time_since = (now - self._last_hourly_report_time).total_seconds()
if time_since < 3600: # 1 hour = 3600 seconds
return
try:
# Gather all data for report
balance = self.mt5.account_balance or self.config.capital
equity = self.mt5.account_equity or self.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"
# Get guard data if available
guard = self.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 info
session_status = self.session_filter.get_status_report()
# Dynamic confidence data
market_analysis = self.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 state
risk_rec = self.smart_risk.get_trading_recommendation()
# Execution stats
avg_exec = (sum(self._execution_times) / len(self._execution_times) * 1000) if self._execution_times else 0
uptime = (now - self._start_time).total_seconds() / 3600 # hours
# Send the report
await self.telegram.send_hourly_analysis(
# Account
balance=balance,
equity=equity,
floating_pnl=floating_pnl,
# Positions
open_positions=len(open_positions),
position_details=position_details,
# Market
symbol=self.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"),
# AI/ML
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,
# Risk
daily_pnl=self._total_session_profit,
daily_trades=self._total_session_trades,
risk_mode=risk_rec.get("mode", "normal"),
max_daily_loss=self.smart_risk.max_daily_loss_usd,
# Bot
uptime_hours=uptime,
total_loops=self._loop_count,
avg_execution_ms=avg_exec,
# News - disabled
news_status="DISABLED",
news_reason="News agent disabled",
)
self._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}")
def _on_new_day(self):
"""Handle new trading day."""
logger.info("=" * 60)
logger.info(f"NEW TRADING DAY: {date.today()}")
logger.info("=" * 60)
# Send daily summary before resetting (run synchronously)
try:
import asyncio
asyncio.create_task(self._send_daily_summary())
except Exception as e:
logger.warning(f"Could not send daily summary: {e}")
# Daily summary disabled from auto-send (available via command)
# try:
# import asyncio
# asyncio.create_task(self._send_daily_summary())
# except Exception as e:
# logger.warning(f"Could not send daily summary: {e}")
self._current_date = date.today()
self.risk_engine.reset_daily_stats()
+317
View File
@@ -0,0 +1,317 @@
"""
Telegram Command Handlers
=========================
Handles all Telegram bot commands separately from main_live.py.
Commands:
/status Bot status & account overview
/market Current market analysis & signals
/risk Risk management state & settings
/positions Open positions detail
/pos Alias for /positions
/daily Daily trading summary
/filters Entry filter status
/help List all available commands
Integration:
from src.telegram_commands import register_commands
register_commands(bot) # bot = TradingBot instance
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from datetime import datetime, date
from zoneinfo import ZoneInfo
from loguru import logger
WIB = ZoneInfo("Asia/Jakarta")
def _fmt_usd(value: float) -> str:
"""Format USD value with sign."""
if value >= 0:
return f"+${value:.2f}"
return f"-${abs(value):.2f}"
def _timestamp() -> str:
"""Return formatted WIB timestamp."""
return datetime.now(WIB).strftime('%H:%M')
def register_commands(bot):
"""
Register all Telegram commands on the bot instance.
Args:
bot: TradingBot instance (has .telegram, .mt5, .smart_risk, etc.)
"""
tg = bot.telegram
build = tg._build_section
# ------------------------------------------------------------------
# /status — Bot status & account overview
# ------------------------------------------------------------------
async def cmd_status():
balance = bot.mt5.account_balance or 0
equity = bot.mt5.account_equity or 0
floating = equity - balance
session_status = bot.session_filter.get_status_report()
risk_rec = bot.smart_risk.get_trading_recommendation()
risk_state = bot.smart_risk.get_state()
uptime = (datetime.now() - bot._start_time).total_seconds() / 3600
avg_ms = (sum(bot._execution_times) / len(bot._execution_times) * 1000) if bot._execution_times else 0
wr = (bot._total_session_wins / bot._total_session_trades * 100) if bot._total_session_trades > 0 else 0
can_icon = "" if session_status.get("can_trade", False) else ""
items_acct = [
f"Bal: <code>${balance:,.2f}</code>",
f"Eq: <code>${equity:,.2f}</code>",
f"Float: <b>{_fmt_usd(floating)}</b>",
]
items_session = [
f"Trades: <code>{bot._total_session_trades}</code> ({bot._total_session_wins}W) | WR: <code>{wr:.1f}%</code>",
f"P/L: <b>{_fmt_usd(bot._total_session_profit)}</b>",
]
items_risk = [
f"Mode: <code>{risk_rec.get('mode', 'normal').upper()}</code>",
f"Daily Loss: <code>${risk_state.daily_loss:.2f}</code> | Streak: <code>{risk_state.consecutive_losses}L</code>",
f"Total Loss: <code>${bot.smart_risk._total_loss:.2f}</code>",
]
items_bot = [
f"{can_icon} {session_status.get('current_session', 'Unknown')} | Vol: <code>{session_status.get('volatility', '?')}</code>",
f"Uptime: <code>{uptime:.1f}h</code> | Loops: <code>{bot._loop_count}</code> | Exec: <code>{avg_ms:.0f}ms</code>",
]
return f"""🤖 <b>STATUS</b>
{build("Account", items_acct)}
{build("Session", items_session)}
{build("Risk", items_risk)}
{build("Bot", items_bot)}
{_timestamp()} WIB""".strip()
cmd_status._cmd_desc = "Bot status & account"
# ------------------------------------------------------------------
# /market — Current market analysis
# ------------------------------------------------------------------
async def cmd_market():
tick = bot.mt5.get_tick(bot.config.symbol)
price = tick.bid if tick else 0
spread = (tick.ask - tick.bid) if tick else 0
session_status = bot.session_filter.get_status_report()
h1_bias = getattr(bot, "_h1_bias_cache", "NEUTRAL")
regime = getattr(bot, "_last_regime", None)
regime_str = regime.value if regime else "unknown"
ml_signal = getattr(bot, "_last_ml_signal", "HOLD")
ml_conf = getattr(bot, "_last_ml_confidence", 0)
smc_signal = getattr(bot, "_last_raw_smc_signal", "")
smc_conf = getattr(bot, "_last_raw_smc_confidence", 0)
threshold = getattr(bot, "_last_dynamic_threshold", 0.55)
quality = getattr(bot, "_last_market_quality", "unknown")
score = getattr(bot, "_last_market_score", 0)
try:
df = bot.mt5.get_market_data(bot.config.symbol, bot.config.execution_timeframe, 50)
atr = df["atr"].tail(1).item() if "atr" in df.columns else 0
except Exception:
atr = 0
sig_emoji = {"BUY": "🟢", "SELL": "🔴"}.get(ml_signal, "")
can_icon = "✅ READY" if session_status.get("can_trade", False) else "⛔ WAIT"
items_price = [
f"<b>{bot.config.symbol}</b> <code>${price:.2f}</code>",
f"ATR: <code>{atr:.2f}</code> | Spread: <code>{spread:.1f}</code>",
]
items_signal = [
f"{sig_emoji} ML: <code>{ml_signal}</code> {ml_conf:.0%} / thresh {threshold:.0%}",
f"SMC: <code>{smc_signal or 'NONE'}</code> ({smc_conf:.0%})",
f"Quality: <code>{quality.upper()}</code> (score:{score})",
f"H1 Bias: <code>{h1_bias}</code>",
]
items_market = [
f"Regime: <code>{regime_str}</code> | Vol: <code>{session_status.get('volatility', '?')}</code>",
f"Session: <code>{session_status.get('current_session', 'Unknown')}</code>",
f"Status: {can_icon}",
]
return f"""📊 <b>MARKET</b>
{build("Price", items_price)}
{build("AI Signal", items_signal)}
{build("Market", items_market)}
{_timestamp()} WIB""".strip()
cmd_market._cmd_desc = "Market analysis & signals"
# ------------------------------------------------------------------
# /risk — Risk management state
# ------------------------------------------------------------------
async def cmd_risk():
risk_state = bot.smart_risk.get_state()
risk_rec = bot.smart_risk.get_trading_recommendation()
balance = bot.mt5.account_balance or bot.config.capital
max_daily_usd = bot.smart_risk.max_daily_loss_usd
max_total_usd = balance * bot.smart_risk.max_total_loss_percent / 100
items_settings = [
f"Risk/Trade: <code>{bot.config.risk.risk_per_trade}%</code>",
f"Max Daily Loss: <code>{bot.config.risk.max_daily_loss}%</code> (${max_daily_usd:.2f})",
f"Max Total Loss: <code>{bot.smart_risk.max_total_loss_percent}%</code> (${max_total_usd:.2f})",
f"Max Lot: <code>{bot.smart_risk.max_lot_size}</code>",
f"Max Positions: <code>{bot.smart_risk.max_concurrent_positions}</code>",
]
items_state = [
f"Mode: <code>{risk_rec.get('mode', 'normal').upper()}</code>",
f"Daily Loss: <code>${risk_state.daily_loss:.2f}</code> / <code>${max_daily_usd:.2f}</code>",
f"Daily Profit: <code>${risk_state.daily_profit:.2f}</code>",
f"Total Loss: <code>${bot.smart_risk._total_loss:.2f}</code>",
f"Streak: <code>{risk_state.consecutive_losses}L</code>",
]
items_rec = [
f"Lot: <code>{risk_rec.get('recommended_lot', 0)}</code>",
f"Reason: <code>{risk_rec.get('reason', '')[:60]}</code>",
]
return f"""🛡 <b>RISK</b>
{build("Settings", items_settings)}
{build("Current State", items_state)}
{build("Recommendation", items_rec)}
{_timestamp()} WIB""".strip()
cmd_risk._cmd_desc = "Risk management state"
# ------------------------------------------------------------------
# /positions — Open positions detail
# ------------------------------------------------------------------
async def cmd_positions():
positions = bot.mt5.get_open_positions(
symbol=bot.config.symbol,
magic=bot.config.magic_number,
)
if positions is None or len(positions) == 0:
return f"📭 <b>POSITIONS</b>\n\n└ No open positions\n\n{_timestamp()} WIB"
pos_items = []
total_profit = 0
for row in positions.iter_rows(named=True):
ticket = row.get("ticket", 0)
direction = "BUY" if row.get("type", 0) == 0 else "SELL"
profit = row.get("profit", 0)
total_profit += profit
open_price = row.get("price_open", 0)
current = row.get("price_current", 0)
sl = row.get("sl", 0)
tp = row.get("tp", 0)
lot = row.get("volume", 0)
guard = bot.smart_risk._position_guards.get(ticket)
momentum = guard.momentum_score if guard else 0
pos_items.append(f"#{ticket} {direction} <code>{lot}</code>")
pos_items.append(f" Open: <code>{open_price:.2f}</code> → Now: <code>{current:.2f}</code>")
pos_items.append(f" SL: <code>{sl:.2f}</code> | TP: <code>{tp:.2f}</code>")
pos_items.append(f" P/L: <b>{_fmt_usd(profit)}</b> | M: <code>{momentum:+.0f}</code>")
summary = [f"Total: <code>{len(positions)}</code> positions, <b>{_fmt_usd(total_profit)}</b>"]
return f"""📈 <b>POSITIONS</b>
{build("Open", pos_items)}
{build("Summary", summary)}
{_timestamp()} WIB""".strip()
cmd_positions._cmd_desc = "Open positions detail"
# ------------------------------------------------------------------
# /daily — Daily trading summary
# ------------------------------------------------------------------
async def cmd_daily():
balance = bot.mt5.account_balance or bot.config.capital
trades = bot._total_session_trades
wins = bot._total_session_wins
losses = trades - wins
wr = (wins / trades * 100) if trades > 0 else 0
day_change = ((balance - bot._daily_start_balance) / bot._daily_start_balance * 100) if bot._daily_start_balance > 0 else 0
day_str = f"+{day_change:.2f}%" if day_change >= 0 else f"{day_change:.2f}%"
items_balance = [
f"Start: <code>${bot._daily_start_balance:,.2f}</code>",
f"Now: <code>${balance:,.2f}</code> (<b>{day_str}</b>)",
f"P/L: <b>{_fmt_usd(bot._total_session_profit)}</b>",
]
items_stats = [
f"Trades: <code>{trades}</code>",
f"Wins: <code>{wins}</code> | Losses: <code>{losses}</code>",
f"Win Rate: <code>{wr:.1f}%</code>",
]
return f"""📋 <b>DAILY</b> {date.today().strftime('%Y-%m-%d')}
{build("Balance", items_balance)}
{build("Stats", items_stats)}
{_timestamp()} WIB""".strip()
cmd_daily._cmd_desc = "Daily trading summary"
# ------------------------------------------------------------------
# /filters — Entry filter status
# ------------------------------------------------------------------
async def cmd_filters():
filters = getattr(bot, "_last_filter_results", [])
if not filters:
return f"🔍 <b>FILTERS</b>\n\n└ No filter data yet (wait for next candle)\n\n{_timestamp()} WIB"
filter_items = []
for f in filters:
icon = "" if f.get("passed", True) else ""
filter_items.append(f"{icon} {f.get('name', '')}: <code>{f.get('detail', '')}</code>")
passed = sum(1 for f in filters if f.get("passed", True))
total = len(filters)
return f"""🔍 <b>FILTERS</b> ({passed}/{total} passed)
{build("Entry Filters", filter_items)}
{_timestamp()} WIB""".strip()
cmd_filters._cmd_desc = "Entry filter status"
# ------------------------------------------------------------------
# Register all commands
# ------------------------------------------------------------------
tg.register_command("status", cmd_status)
tg.register_command("s", cmd_status) # alias
tg.register_command("market", cmd_market)
tg.register_command("m", cmd_market) # alias
tg.register_command("risk", cmd_risk)
tg.register_command("positions", cmd_positions)
tg.register_command("pos", cmd_positions) # alias
tg.register_command("p", cmd_positions) # alias
tg.register_command("daily", cmd_daily)
tg.register_command("d", cmd_daily) # alias
tg.register_command("filters", cmd_filters)
tg.register_command("f", cmd_filters) # alias
logger.info("Telegram commands registered: /status /market /risk /positions /daily /filters /help")
+615
View File
@@ -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"),
}
+670 -197
View File
File diff suppressed because it is too large Load Diff