diff --git a/adaptive_rhythm_manager.py b/adaptive_rhythm_manager.py index 1b8cf3a..40b73ae 100644 --- a/adaptive_rhythm_manager.py +++ b/adaptive_rhythm_manager.py @@ -3,11 +3,10 @@ Adaptive Rhythm Manager - Extracted from Notebook Manages adaptive trading intervals based on volatility and session """ -import MetaTrader5 as mt +import MetaTrader5 as mt5 import pandas as pd import pandas_ta as ta -import pytz -from datetime import datetime, time +from datetime import datetime, time, timezone import logging logger = logging.getLogger(__name__) @@ -51,7 +50,7 @@ class AdaptiveRhythmManager: def get_current_session(self): """Ermittelt die aktuelle Trading-Session""" - now_utc = datetime.now(pytz.UTC).time() + now_utc = datetime.now(timezone.utc).time() # Overlap hat höchste Priorität if self.sessions['overlap'][0] <= now_utc <= self.sessions['overlap'][1]: @@ -73,13 +72,19 @@ class AdaptiveRhythmManager: def get_market_data(self): """Hole Marktdaten für ATR-Analyse""" try: - rates = mt.copy_rates_from_pos(self.symbol, mt.TIMEFRAME_H1, 0, 50) + rates = mt5.copy_rates_from_pos(self.symbol, mt5.TIMEFRAME_H1, 0, 50) if rates is None: return None - df = pd.DataFrame(rates) + # mt5 may return a structured numpy array or DataFrame depending on version + df = rates if isinstance(rates, pd.DataFrame) else pd.DataFrame(rates) df['time'] = pd.to_datetime(df['time'], unit='s') df.set_index('time', inplace=True) + + if len(df) < 14: + logger.warning(f"Not enough data for ATR calculation: {len(df)} bars") + return None + df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14) return df except Exception as e: @@ -127,6 +132,11 @@ class AdaptiveRhythmManager: else: # asian return self.intervals['medium'] if volatility == 'high' else self.intervals['slow'] + def shutdown(self): + """Trennt MT5-Verbindung sauber""" + mt5.shutdown() + logger.info("AdaptiveRhythmManager: MT5 disconnected") + def get_status_report(self): """Erstellt Status-Report""" session = self.get_current_session() @@ -141,7 +151,7 @@ class AdaptiveRhythmManager: return f""" ╔════════════════════════════════════════════════════════╗ -║ ADAPTIVE RHYTHM STATUS - {datetime.now().strftime('%H:%M:%S UTC')} ║ +║ ADAPTIVE RHYTHM STATUS - {datetime.now(timezone.utc).strftime('%H:%M:%S UTC')} ║ ╠════════════════════════════════════════════════════════╣ ║ Aktuelles Intervall: {self.current_interval:>2} Minuten ║ ║ Trading Session: {session.upper():<15} ║ diff --git a/check_market_regime.py b/check_market_regime.py index a453b36..9da07e4 100644 --- a/check_market_regime.py +++ b/check_market_regime.py @@ -4,38 +4,42 @@ Checks if market is Trending or Ranging """ -import MetaTrader5 as mt +import sys +import MetaTrader5 as mt5 import pandas as pd import numpy as np -from datetime import datetime +from datetime import datetime, timezone SYMBOL = "XAUUSD" -TIMEFRAME = mt.TIMEFRAME_M15 +TIMEFRAME = mt5.TIMEFRAME_M15 +ADX_THRESHOLD = 25 def calculate_adx(df, period=14): - """Calculate ADX indicator""" + """Calculate ADX indicator using Wilder's smoothing (EWM)""" + if len(df) < period + 1: + return float('nan') + + alpha = 1 / period - # True Range df['high_low'] = df['high'] - df['low'] df['high_close'] = np.abs(df['high'] - df['close'].shift()) df['low_close'] = np.abs(df['low'] - df['close'].shift()) df['true_range'] = df[['high_low', 'high_close', 'low_close']].max(axis=1) - # Directional Movement df['up_move'] = df['high'] - df['high'].shift() df['down_move'] = df['low'].shift() - df['low'] df['plus_dm'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0) df['minus_dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0) - # Smoothed values - df['atr'] = df['true_range'].rolling(window=period).mean() - df['plus_di'] = 100 * (df['plus_dm'].rolling(window=period).mean() / df['atr']) - df['minus_di'] = 100 * (df['minus_dm'].rolling(window=period).mean() / df['atr']) + # Wilder's smoothing via EWM (adjust=False matches the classic formula) + df['atr'] = df['true_range'].ewm(alpha=alpha, adjust=False).mean() + df['plus_di'] = 100 * (df['plus_dm'].ewm(alpha=alpha, adjust=False).mean() / df['atr']) + df['minus_di'] = 100 * (df['minus_dm'].ewm(alpha=alpha, adjust=False).mean() / df['atr']) - # ADX - df['dx'] = 100 * np.abs(df['plus_di'] - df['minus_di']) / (df['plus_di'] + df['minus_di']) - df['adx'] = df['dx'].rolling(window=period).mean() + sum_di = df['plus_di'] + df['minus_di'] + df['dx'] = np.where(sum_di == 0, 0.0, 100 * np.abs(df['plus_di'] - df['minus_di']) / sum_di) + df['adx'] = df['dx'].ewm(alpha=alpha, adjust=False).mean() return df['adx'].iloc[-1] @@ -46,88 +50,84 @@ def check_market_regime(): print(f"📊 MARKET REGIME CHECK: {SYMBOL}") print("=" * 70) - # Initialize MT5 - if not mt.initialize(): + if not mt5.initialize(): print("❌ MT5 initialization failed") return None - # Get current price - tick = mt.symbol_info_tick(SYMBOL) - if not tick: - print("❌ Could not get price data") - mt.shutdown() - return None + try: + tick = mt5.symbol_info_tick(SYMBOL) + if not tick: + print("❌ Could not get price data") + return None - current_price = tick.bid - timestamp = datetime.fromtimestamp(tick.time) + current_price = tick.bid + timestamp = datetime.fromtimestamp(tick.time, tz=timezone.utc) - print(f"\n💹 Current Price: ${current_price:.2f}") - print(f"⏰ Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}") + print(f"\n💹 Current Price: ${current_price:.2f}") + print(f"⏰ Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}") - # Get historical data for ADX calculation - rates = mt.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, 100) - if rates is None or len(rates) == 0: - print("❌ Could not get historical data") - mt.shutdown() - return None + rates = mt5.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, 100) + if rates is None or len(rates) == 0: + print("❌ Could not get historical data") + return None - df = pd.DataFrame(rates) - df['time'] = pd.to_datetime(df['time'], unit='s') + df = rates if isinstance(rates, pd.DataFrame) else pd.DataFrame(rates) + df['time'] = pd.to_datetime(df['time'], unit='s') - # Calculate ADX - adx = calculate_adx(df, period=14) + adx = calculate_adx(df, period=14) - # Determine regime - if adx < 25: - regime = "ranging" - can_trade = False - symbol = "🛑" - status = "RANGING MARKET" - decision = "Trading BLOCKED" - reason = "ADX < 25 = No clear trend" - advice = "Wait for trending market (ADX ≥ 25)" - else: - regime = "trending" - can_trade = True - symbol = "✅" - status = "TRENDING MARKET" - decision = "Trading ALLOWED" - reason = "ADX ≥ 25 = Strong trend" - advice = "Good conditions for trading!" + if np.isnan(adx): + print("❌ ADX calculation failed (not enough data)") + return None - print(f"\n📈 REGIME ANALYSIS:") - print(f" Regime: {status}") - print(f" ADX: {adx:.1f}") - print(f" Status: {symbol} {regime.upper()}") + if adx < ADX_THRESHOLD: + regime = "ranging" + can_trade = False + marker = "🛑" + status = "RANGING MARKET" + decision = "Trading BLOCKED" + reason = f"ADX < {ADX_THRESHOLD} = No clear trend" + advice = f"Wait for trending market (ADX ≥ {ADX_THRESHOLD})" + else: + regime = "trending" + can_trade = True + marker = "✅" + status = "TRENDING MARKET" + decision = "Trading ALLOWED" + reason = f"ADX ≥ {ADX_THRESHOLD} = Strong trend" + advice = "Good conditions for trading!" - print(f"\n🎯 TRADING DECISION:") - print(f" {symbol} {decision}") - print(f" 📊 {reason}") - print(f" 💡 {advice}") + print(f"\n📈 REGIME ANALYSIS:") + print(f" Regime: {status}") + print(f" ADX: {adx:.1f}") + print(f" Status: {marker} {regime.upper()}") - # Visual indicator - print(f"\n📊 ADX SCALE:") - print(" 0-20: Very Weak/Ranging ❌") - print(" 20-25: Weak/Ranging ⚠️") - print(" 25-40: Trending ✅") - print(" 40+: Strong Trending ✅✅") - print(f" YOUR ADX: {adx:.1f} {'━' * int(adx/2)}") + print(f"\n🎯 TRADING DECISION:") + print(f" {marker} {decision}") + print(f" 📊 {reason}") + print(f" 💡 {advice}") - print("\n" + "=" * 70) + print(f"\n📊 ADX SCALE:") + print(" 0-20: Very Weak/Ranging ❌") + print(" 20-25: Weak/Ranging ⚠️") + print(" 25-40: Trending ✅") + print(" 40+: Strong Trending ✅✅") + print(f" YOUR ADX: {adx:.1f} {'━' * min(int(adx / 2), 40)}") - mt.shutdown() + print("\n" + "=" * 70) + + return { + 'regime': regime, + 'adx': adx, + 'can_trade': can_trade, + 'price': current_price, + 'timestamp': timestamp + } + + finally: + mt5.shutdown() - return { - 'regime': regime, - 'adx': adx, - 'can_trade': can_trade, - 'price': current_price, - 'timestamp': timestamp - } if __name__ == "__main__": result = check_market_regime() - - if result: - import sys - sys.exit(0 if result['can_trade'] else 1) + sys.exit(0 if (result and result['can_trade']) else 1) diff --git a/check_system_status.py b/check_system_status.py index a2258ac..5df305c 100644 --- a/check_system_status.py +++ b/check_system_status.py @@ -88,18 +88,6 @@ def check_status(): # 4. RANGING VS TRENDING print("\n🔍 REGIME BREAKDOWN (Last 20 trades):") - cursor.execute(""" - SELECT - regime, - COUNT(*) as count, - SUM(CASE WHEN net_profit > 0 THEN 1 ELSE 0 END) as wins, - SUM(net_profit) as pnl - FROM trades - WHERE status = 'closed' - ORDER BY exit_time DESC - LIMIT 20 - """) - cursor.execute(""" SELECT regime, diff --git a/drawdown_protection.py b/drawdown_protection.py index 8e2b908..45caf7f 100644 --- a/drawdown_protection.py +++ b/drawdown_protection.py @@ -4,7 +4,7 @@ Schützt vor übermäßigen Verlusten durch automatische Handels-Pausen """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from trading_database import TradingDatabase from telegram_notifier import TelegramNotifier import logging @@ -44,10 +44,11 @@ class DrawdownProtection: self.max_consecutive_losses = max_consecutive_losses self.cooldown_hours = cooldown_hours - # State + # State (loaded from DB so it survives restarts) self.trading_paused = False self.pause_until = None self.pause_reason = None + self._load_state() def can_trade(self) -> tuple[bool, str]: """ @@ -103,6 +104,31 @@ class DrawdownProtection: return True, "OK" + def _load_state(self): + """Load persisted pause state from DB on startup""" + try: + pause_until_str = self.db.load_setting('drawdown_pause_until') + pause_reason = self.db.load_setting('drawdown_pause_reason') + if pause_until_str: + pause_until = datetime.fromisoformat(pause_until_str) + if pause_until > datetime.now(): + self.trading_paused = True + self.pause_until = pause_until + self.pause_reason = pause_reason + logger.info(f"Loaded active pause from DB: {pause_reason} until {pause_until}") + else: + self._clear_persisted_state() + except Exception as e: + logger.error(f"Error loading drawdown state from DB: {e}") + + def _clear_persisted_state(self): + """Clear pause state from DB""" + try: + self.db.save_setting('drawdown_pause_until', '') + self.db.save_setting('drawdown_pause_reason', '') + except Exception as e: + logger.error(f"Error clearing drawdown state: {e}") + def _get_loss_today(self) -> float: """Berechnet Verlust heute""" try: @@ -124,7 +150,7 @@ class DrawdownProtection: except Exception as e: logger.error(f"Error calculating daily loss: {e}") - return 0.0 + return float('inf') # safe: block trading when DB unreachable def _get_loss_this_week(self) -> float: """Berechnet Verlust diese Woche""" @@ -147,7 +173,7 @@ class DrawdownProtection: except Exception as e: logger.error(f"Error calculating weekly loss: {e}") - return 0.0 + return float('inf') def _get_loss_this_month(self) -> float: """Berechnet Verlust diesen Monat""" @@ -170,7 +196,7 @@ class DrawdownProtection: except Exception as e: logger.error(f"Error calculating monthly loss: {e}") - return 0.0 + return float('inf') def _get_consecutive_losses(self) -> int: """Zählt aufeinanderfolgende Verluste""" @@ -210,9 +236,16 @@ class DrawdownProtection: logger.warning(f"🛑 Trading paused: {reason}") logger.warning(f" Resuming at: {self.pause_until}") + # Persist so pause survives a restart + try: + self.db.save_setting('drawdown_pause_until', self.pause_until.isoformat()) + self.db.save_setting('drawdown_pause_reason', reason) + except Exception as e: + logger.error(f"Error persisting drawdown pause state: {e}") + if self.telegram: self.telegram.send_message( - f"🛑 **TRADING PAUSED**\n\n" + f"🛑 *TRADING PAUSED*\n\n" f"Reason: {reason}\n" f"Duration: {hours} hours\n" f"Resume at: {self.pause_until.strftime('%Y-%m-%d %H:%M')}\n\n" @@ -226,11 +259,12 @@ class DrawdownProtection: previous_reason = self.pause_reason self.pause_reason = None + self._clear_persisted_state() logger.info(f"✅ Trading resumed after: {previous_reason}") if self.telegram: self.telegram.send_message( - f"✅ **TRADING RESUMED**\n\n" + f"✅ *TRADING RESUMED*\n\n" f"Previous pause reason: {previous_reason}\n" f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}" ) diff --git a/performance_analysis.py b/performance_analysis.py index 4650732..f23d752 100644 --- a/performance_analysis.py +++ b/performance_analysis.py @@ -16,7 +16,6 @@ import json def get_connection(db_path="trading_bot.db"): """Verbindung zur Datenbank""" conn = sqlite3.connect(db_path) - conn.row_factory = sqlite3.Row return conn # ========================================== @@ -25,7 +24,12 @@ def get_connection(db_path="trading_bot.db"): def load_closed_trades(conn, exclude_historical=True): """Lade geschlossene Trades""" - query = """ + # 'historical' is a distinct status for imported legacy trades. + # exclude_historical=True → only live bot trades (status='closed') + # exclude_historical=False → live + historical imports + status_filter = "status = 'closed'" if exclude_historical else "status IN ('closed', 'historical')" + + query = f""" SELECT ticket, symbol, type, volume, entry_price, exit_price, @@ -38,20 +42,15 @@ def load_closed_trades(conn, exclude_historical=True): profit_pct, rr_ratio, exit_reason, status FROM trades - WHERE status = 'closed' + WHERE {status_filter} + ORDER BY exit_time DESC """ - if exclude_historical: - query += " AND status != 'historical'" - - query += " ORDER BY exit_time DESC" - df = pd.read_sql_query(query, conn) - # Convert datetime columns if not df.empty: - df['entry_time'] = pd.to_datetime(df['entry_time'], format='mixed') - df['exit_time'] = pd.to_datetime(df['exit_time'], format='mixed') + df['entry_time'] = pd.to_datetime(df['entry_time'], errors='coerce') + df['exit_time'] = pd.to_datetime(df['exit_time'], errors='coerce') df['duration_hours'] = (df['exit_time'] - df['entry_time']).dt.total_seconds() / 3600 df['win'] = df['net_profit'] > 0 @@ -78,7 +77,9 @@ def calculate_overall_metrics(df): avg_win = df[df['win']]['net_profit'].mean() if winning_trades > 0 else 0 avg_loss = df[~df['win']]['net_profit'].mean() if losing_trades > 0 else 0 - profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else 0 + gross_profit = df[df['win']]['net_profit'].sum() + gross_loss = abs(df[~df['win']]['net_profit'].sum()) + profit_factor = round(gross_profit / gross_loss, 2) if gross_loss > 0 else 0 avg_duration = df['duration_hours'].mean() @@ -87,7 +88,7 @@ def calculate_overall_metrics(df): df_sorted['cumulative'] = df_sorted['net_profit'].cumsum() df_sorted['running_max'] = df_sorted['cumulative'].cummax() df_sorted['drawdown'] = df_sorted['cumulative'] - df_sorted['running_max'] - max_drawdown = df_sorted['drawdown'].min() + max_drawdown = abs(df_sorted['drawdown'].min()) return { 'total_trades': total_trades, @@ -429,7 +430,7 @@ def run_performance_analysis(db_path="trading_bot.db", exclude_historical=True): conn.close() - return { + results = { 'overall': overall, 'by_session': session_df, 'by_confidence': conf_df, @@ -439,6 +440,8 @@ def run_performance_analysis(db_path="trading_bot.db", exclude_historical=True): 'by_regime': regime_df } + return results + # ========================================== # EXPORT TO JSON # ========================================== @@ -457,7 +460,7 @@ def export_analysis_to_json(results, output_file="performance_analysis.json"): 'by_regime': results['by_regime'].to_dict('records') if not results['by_regime'].empty else [] } - with open(output_file, 'w') as f: + with open(output_file, 'w', encoding='utf-8') as f: json.dump(output, f, indent=2) print(f"\n✅ Analysis exported to: {output_file}") diff --git a/performance_analysis_simple.py b/performance_analysis_simple.py index e811cf3..48299c9 100644 --- a/performance_analysis_simple.py +++ b/performance_analysis_simple.py @@ -7,6 +7,7 @@ Umfassende Performance-Auswertung mit nur SQLite import sqlite3 from datetime import datetime from collections import defaultdict +from typing import List, Dict # ========================================== # DATABASE QUERIES @@ -14,11 +15,9 @@ from collections import defaultdict def get_closed_trades(db_path="trading_bot.db", exclude_historical=True): """Lade geschlossene Trades""" - conn = sqlite3.connect(db_path) - conn.row_factory = sqlite3.Row - cursor = conn.cursor() + status_filter = "status = 'closed'" if exclude_historical else "status IN ('closed', 'historical')" - query = """ + query = f""" SELECT ticket, symbol, type, volume, entry_price, exit_price, @@ -31,19 +30,15 @@ def get_closed_trades(db_path="trading_bot.db", exclude_historical=True): profit_pct, rr_ratio, exit_reason, status FROM trades - WHERE status = 'closed' + WHERE {status_filter} + ORDER BY exit_time DESC """ - if exclude_historical: - query += " AND status != 'historical'" - - query += " ORDER BY exit_time DESC" - - cursor.execute(query) - trades = [dict(row) for row in cursor.fetchall()] - - conn.close() - return trades + with sqlite3.connect(db_path) as conn: + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + cursor.execute(query) + return [dict(row) for row in cursor.fetchall()] # ========================================== # OVERALL PERFORMANCE @@ -66,7 +61,9 @@ def calculate_overall_metrics(trades): avg_win = sum(t['net_profit'] for t in wins) / len(wins) if wins else 0 avg_loss = sum(t['net_profit'] for t in losses) / len(losses) if losses else 0 - profit_factor = abs(avg_win / avg_loss) if avg_loss != 0 else 0 + gross_profit = sum(t['net_profit'] for t in wins) + gross_loss = abs(sum(t['net_profit'] for t in losses)) + profit_factor = round(gross_profit / gross_loss, 2) if gross_loss > 0 else 0 # Calculate drawdown cumulative = 0 @@ -78,8 +75,8 @@ def calculate_overall_metrics(trades): if cumulative > max_cumulative: max_cumulative = cumulative drawdown = cumulative - max_cumulative - if drawdown < max_drawdown: - max_drawdown = drawdown + if abs(drawdown) > max_drawdown: + max_drawdown = abs(drawdown) return { 'total_trades': total_trades, @@ -172,7 +169,7 @@ def analyze_by_confidence(trades): 'avg_profit': avg_profit }) - return sorted(results, key=lambda x: x['confidence_range']) + return sorted(results, key=lambda x: int(x['confidence_range'].split('-')[0])) # ========================================== # EXIT REASON ANALYSIS @@ -215,7 +212,7 @@ def analyze_by_hour(trades): hours = defaultdict(lambda: {'trades': 0, 'wins': 0, 'profit': 0}) for trade in trades: - hour = int(trade['entry_time'][11:13]) # Extract hour from timestamp + hour = datetime.fromisoformat(trade['entry_time']).hour hours[hour]['trades'] += 1 hours[hour]['profit'] += trade['net_profit'] if trade['net_profit'] > 0: diff --git a/trading_dashboard.py b/trading_dashboard.py index 95b89e1..fcab6f9 100644 --- a/trading_dashboard.py +++ b/trading_dashboard.py @@ -10,6 +10,7 @@ import pandas as pd from datetime import datetime, timedelta import plotly.express as px import plotly.graph_objects as go +from pathlib import Path # ========================================== # PAGE CONFIG @@ -25,9 +26,14 @@ st.set_page_config( # DATABASE CONNECTION # ========================================== +DB_PATH = Path(__file__).parent / "trading_bot.db" + @st.cache_resource def get_connection(): - return sqlite3.connect("trading_bot.db", check_same_thread=False) + if not DB_PATH.exists(): + st.error(f"Database not found: {DB_PATH}") + st.stop() + return sqlite3.connect(str(DB_PATH), check_same_thread=False) conn = get_connection() @@ -43,10 +49,10 @@ col1, col2, col3 = st.columns([1, 1, 2]) with col1: if st.button("🔄 Refresh Data"): st.cache_data.clear() - st.experimental_rerun() + st.rerun() with col2: - auto_refresh = st.checkbox("Auto-refresh (30s)") + auto_refresh = st.toggle("Auto-refresh (30s)") with col3: trade_filter = st.selectbox( @@ -56,10 +62,14 @@ with col3: ) if auto_refresh: - st.markdown("*Auto-refreshing every 30 seconds...*") - import time - time.sleep(30) - st.experimental_rerun() + if "last_refresh" not in st.session_state: + st.session_state.last_refresh = datetime.now() + elapsed = (datetime.now() - st.session_state.last_refresh).total_seconds() + if elapsed >= 30: + st.session_state.last_refresh = datetime.now() + st.rerun() + else: + st.markdown(f"*Auto-refresh in {30 - int(elapsed)}s...*") # ========================================== # LOAD DATA @@ -69,38 +79,27 @@ if auto_refresh: def load_all_trades(): query = """ SELECT - ticket, - position_id, - symbol, - strategy_name, - type, - volume, - entry_price, - sl_price, - tp_price, - entry_time, - exit_time, - session, - regime, - quality, - confidence, - timeframe_alignment, - risk_amount, - risk_pct, - net_profit, - profit_pct, - rr_ratio, - status, - exit_reason + ticket, position_id, symbol, strategy_name, type, volume, + entry_price, sl_price, tp_price, entry_time, exit_time, + session, regime, quality, confidence, timeframe_alignment, + risk_amount, risk_pct, net_profit, profit_pct, rr_ratio, + status, exit_reason FROM trades ORDER BY entry_time DESC """ - return pd.read_sql_query(query, conn) + try: + return pd.read_sql_query(query, conn) + except Exception as e: + st.error(f"Error loading trades: {e}") + return pd.DataFrame() @st.cache_data(ttl=30) def load_bot_status(): - query = "SELECT * FROM bot_status ORDER BY timestamp DESC LIMIT 1" - return pd.read_sql_query(query, conn) + try: + return pd.read_sql_query("SELECT * FROM bot_status ORDER BY timestamp DESC LIMIT 1", conn) + except Exception as e: + st.error(f"Error loading bot status: {e}") + return pd.DataFrame() # Load data df_trades_raw = load_all_trades() @@ -219,7 +218,7 @@ st.subheader("⏰ Trades by Hour (UTC)") if not df_trades.empty: # Extract hour from entry_time (handle both ISO8601 and standard format) - df_trades['hour_utc'] = pd.to_datetime(df_trades['entry_time'], format='mixed').dt.hour + df_trades['hour_utc'] = pd.to_datetime(df_trades['entry_time'], errors='coerce').dt.hour # Count trades by hour hourly_dist = df_trades.groupby('hour_utc').size().reset_index(name='count') @@ -338,7 +337,7 @@ st.subheader("💰 Cumulative Profit Over Time") if closed_trades > 0: profit_timeline = df_trades[df_trades['status'] == 'closed'].copy() - profit_timeline['exit_time'] = pd.to_datetime(profit_timeline['exit_time'], format='mixed') + profit_timeline['exit_time'] = pd.to_datetime(profit_timeline['exit_time'], errors='coerce') profit_timeline = profit_timeline.sort_values('exit_time') profit_timeline['cumulative_profit'] = profit_timeline['net_profit'].cumsum()