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:
+70
-418
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user