fix: stage missing code review fixes (7 files)

Files were edited but not staged in earlier commits:
- adaptive_rhythm_manager.py: mt→mt5, pytz→timezone, get_volatility_level, shutdown()
- check_market_regime.py: ADX_THRESHOLD, Wilder EWM, try/finally, UTC timestamp, sys import
- check_system_status.py: remove duplicate cursor.execute
- drawdown_protection.py: float(inf), persist pause state, DB save_setting, Markdown fix
- performance_analysis.py: KeyError export fix, profit factor, drawdown positive, SQL filter
- performance_analysis_simple.py: fromisoformat, numeric bin sort, profit factor
- trading_dashboard.py: st.rerun(), session_state auto-refresh, pathlib DB path, errors=coerce

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 12:25:11 +02:00
co-authored by Claude Sonnet 4.6
parent 5f11d9693c
commit dd2611eaef
7 changed files with 207 additions and 176 deletions
+17 -7
View File
@@ -3,11 +3,10 @@ Adaptive Rhythm Manager - Extracted from Notebook
Manages adaptive trading intervals based on volatility and session Manages adaptive trading intervals based on volatility and session
""" """
import MetaTrader5 as mt import MetaTrader5 as mt5
import pandas as pd import pandas as pd
import pandas_ta as ta import pandas_ta as ta
import pytz from datetime import datetime, time, timezone
from datetime import datetime, time
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,7 +50,7 @@ class AdaptiveRhythmManager:
def get_current_session(self): def get_current_session(self):
"""Ermittelt die aktuelle Trading-Session""" """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 # Overlap hat höchste Priorität
if self.sessions['overlap'][0] <= now_utc <= self.sessions['overlap'][1]: if self.sessions['overlap'][0] <= now_utc <= self.sessions['overlap'][1]:
@@ -73,13 +72,19 @@ class AdaptiveRhythmManager:
def get_market_data(self): def get_market_data(self):
"""Hole Marktdaten für ATR-Analyse""" """Hole Marktdaten für ATR-Analyse"""
try: 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: if rates is None:
return 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['time'] = pd.to_datetime(df['time'], unit='s')
df.set_index('time', inplace=True) 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) df['atr'] = ta.atr(df['high'], df['low'], df['close'], length=14)
return df return df
except Exception as e: except Exception as e:
@@ -127,6 +132,11 @@ class AdaptiveRhythmManager:
else: # asian else: # asian
return self.intervals['medium'] if volatility == 'high' else self.intervals['slow'] 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): def get_status_report(self):
"""Erstellt Status-Report""" """Erstellt Status-Report"""
session = self.get_current_session() session = self.get_current_session()
@@ -141,7 +151,7 @@ class AdaptiveRhythmManager:
return f""" 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 ║ ║ Aktuelles Intervall: {self.current_interval:>2} Minuten ║
║ Trading Session: {session.upper():<15} ║ Trading Session: {session.upper():<15}
+80 -80
View File
@@ -4,38 +4,42 @@
Checks if market is Trending or Ranging Checks if market is Trending or Ranging
""" """
import MetaTrader5 as mt import sys
import MetaTrader5 as mt5
import pandas as pd import pandas as pd
import numpy as np import numpy as np
from datetime import datetime from datetime import datetime, timezone
SYMBOL = "XAUUSD" SYMBOL = "XAUUSD"
TIMEFRAME = mt.TIMEFRAME_M15 TIMEFRAME = mt5.TIMEFRAME_M15
ADX_THRESHOLD = 25
def calculate_adx(df, period=14): 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_low'] = df['high'] - df['low']
df['high_close'] = np.abs(df['high'] - df['close'].shift()) df['high_close'] = np.abs(df['high'] - df['close'].shift())
df['low_close'] = np.abs(df['low'] - 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) df['true_range'] = df[['high_low', 'high_close', 'low_close']].max(axis=1)
# Directional Movement
df['up_move'] = df['high'] - df['high'].shift() df['up_move'] = df['high'] - df['high'].shift()
df['down_move'] = df['low'].shift() - df['low'] 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['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) df['minus_dm'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0)
# Smoothed values # Wilder's smoothing via EWM (adjust=False matches the classic formula)
df['atr'] = df['true_range'].rolling(window=period).mean() df['atr'] = df['true_range'].ewm(alpha=alpha, adjust=False).mean()
df['plus_di'] = 100 * (df['plus_dm'].rolling(window=period).mean() / df['atr']) df['plus_di'] = 100 * (df['plus_dm'].ewm(alpha=alpha, adjust=False).mean() / df['atr'])
df['minus_di'] = 100 * (df['minus_dm'].rolling(window=period).mean() / df['atr']) df['minus_di'] = 100 * (df['minus_dm'].ewm(alpha=alpha, adjust=False).mean() / df['atr'])
# ADX sum_di = df['plus_di'] + df['minus_di']
df['dx'] = 100 * np.abs(df['plus_di'] - df['minus_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'].rolling(window=period).mean() df['adx'] = df['dx'].ewm(alpha=alpha, adjust=False).mean()
return df['adx'].iloc[-1] return df['adx'].iloc[-1]
@@ -46,88 +50,84 @@ def check_market_regime():
print(f"📊 MARKET REGIME CHECK: {SYMBOL}") print(f"📊 MARKET REGIME CHECK: {SYMBOL}")
print("=" * 70) print("=" * 70)
# Initialize MT5 if not mt5.initialize():
if not mt.initialize():
print("❌ MT5 initialization failed") print("❌ MT5 initialization failed")
return None return None
# Get current price try:
tick = mt.symbol_info_tick(SYMBOL) tick = mt5.symbol_info_tick(SYMBOL)
if not tick: if not tick:
print("❌ Could not get price data") print("❌ Could not get price data")
mt.shutdown() return None
return None
current_price = tick.bid current_price = tick.bid
timestamp = datetime.fromtimestamp(tick.time) timestamp = datetime.fromtimestamp(tick.time, tz=timezone.utc)
print(f"\n💹 Current Price: ${current_price:.2f}") print(f"\n💹 Current Price: ${current_price:.2f}")
print(f"⏰ Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S')}") print(f"⏰ Time: {timestamp.strftime('%Y-%m-%d %H:%M:%S UTC')}")
# Get historical data for ADX calculation rates = mt5.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, 100)
rates = mt.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, 100) if rates is None or len(rates) == 0:
if rates is None or len(rates) == 0: print("❌ Could not get historical data")
print("❌ Could not get historical data") return None
mt.shutdown()
return None
df = pd.DataFrame(rates) df = rates if isinstance(rates, pd.DataFrame) else pd.DataFrame(rates)
df['time'] = pd.to_datetime(df['time'], unit='s') 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 np.isnan(adx):
if adx < 25: print("❌ ADX calculation failed (not enough data)")
regime = "ranging" return None
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!"
print(f"\n📈 REGIME ANALYSIS:") if adx < ADX_THRESHOLD:
print(f" Regime: {status}") regime = "ranging"
print(f" ADX: {adx:.1f}") can_trade = False
print(f" Status: {symbol} {regime.upper()}") 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"\n📈 REGIME ANALYSIS:")
print(f" {symbol} {decision}") print(f" Regime: {status}")
print(f" 📊 {reason}") print(f" ADX: {adx:.1f}")
print(f" 💡 {advice}") print(f" Status: {marker} {regime.upper()}")
# Visual indicator print(f"\n🎯 TRADING DECISION:")
print(f"\n📊 ADX SCALE:") print(f" {marker} {decision}")
print(" 0-20: Very Weak/Ranging ❌") print(f" 📊 {reason}")
print(" 20-25: Weak/Ranging ⚠️") print(f" 💡 {advice}")
print(" 25-40: Trending ✅")
print(" 40+: Strong Trending ✅✅")
print(f" YOUR ADX: {adx:.1f} {'' * int(adx/2)}")
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__": if __name__ == "__main__":
result = check_market_regime() result = check_market_regime()
sys.exit(0 if (result and result['can_trade']) else 1)
if result:
import sys
sys.exit(0 if result['can_trade'] else 1)
-12
View File
@@ -88,18 +88,6 @@ def check_status():
# 4. RANGING VS TRENDING # 4. RANGING VS TRENDING
print("\n🔍 REGIME BREAKDOWN (Last 20 trades):") 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(""" cursor.execute("""
SELECT SELECT
regime, regime,
+41 -7
View File
@@ -4,7 +4,7 @@
Schützt vor übermäßigen Verlusten durch automatische Handels-Pausen 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 trading_database import TradingDatabase
from telegram_notifier import TelegramNotifier from telegram_notifier import TelegramNotifier
import logging import logging
@@ -44,10 +44,11 @@ class DrawdownProtection:
self.max_consecutive_losses = max_consecutive_losses self.max_consecutive_losses = max_consecutive_losses
self.cooldown_hours = cooldown_hours self.cooldown_hours = cooldown_hours
# State # State (loaded from DB so it survives restarts)
self.trading_paused = False self.trading_paused = False
self.pause_until = None self.pause_until = None
self.pause_reason = None self.pause_reason = None
self._load_state()
def can_trade(self) -> tuple[bool, str]: def can_trade(self) -> tuple[bool, str]:
""" """
@@ -103,6 +104,31 @@ class DrawdownProtection:
return True, "OK" 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: def _get_loss_today(self) -> float:
"""Berechnet Verlust heute""" """Berechnet Verlust heute"""
try: try:
@@ -124,7 +150,7 @@ class DrawdownProtection:
except Exception as e: except Exception as e:
logger.error(f"Error calculating daily loss: {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: def _get_loss_this_week(self) -> float:
"""Berechnet Verlust diese Woche""" """Berechnet Verlust diese Woche"""
@@ -147,7 +173,7 @@ class DrawdownProtection:
except Exception as e: except Exception as e:
logger.error(f"Error calculating weekly loss: {e}") logger.error(f"Error calculating weekly loss: {e}")
return 0.0 return float('inf')
def _get_loss_this_month(self) -> float: def _get_loss_this_month(self) -> float:
"""Berechnet Verlust diesen Monat""" """Berechnet Verlust diesen Monat"""
@@ -170,7 +196,7 @@ class DrawdownProtection:
except Exception as e: except Exception as e:
logger.error(f"Error calculating monthly loss: {e}") logger.error(f"Error calculating monthly loss: {e}")
return 0.0 return float('inf')
def _get_consecutive_losses(self) -> int: def _get_consecutive_losses(self) -> int:
"""Zählt aufeinanderfolgende Verluste""" """Zählt aufeinanderfolgende Verluste"""
@@ -210,9 +236,16 @@ class DrawdownProtection:
logger.warning(f"🛑 Trading paused: {reason}") logger.warning(f"🛑 Trading paused: {reason}")
logger.warning(f" Resuming at: {self.pause_until}") 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: if self.telegram:
self.telegram.send_message( self.telegram.send_message(
f"🛑 **TRADING PAUSED**\n\n" f"🛑 *TRADING PAUSED*\n\n"
f"Reason: {reason}\n" f"Reason: {reason}\n"
f"Duration: {hours} hours\n" f"Duration: {hours} hours\n"
f"Resume at: {self.pause_until.strftime('%Y-%m-%d %H:%M')}\n\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 previous_reason = self.pause_reason
self.pause_reason = None self.pause_reason = None
self._clear_persisted_state()
logger.info(f"✅ Trading resumed after: {previous_reason}") logger.info(f"✅ Trading resumed after: {previous_reason}")
if self.telegram: if self.telegram:
self.telegram.send_message( self.telegram.send_message(
f"**TRADING RESUMED**\n\n" f"✅ *TRADING RESUMED*\n\n"
f"Previous pause reason: {previous_reason}\n" f"Previous pause reason: {previous_reason}\n"
f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}" f"Time: {datetime.now().strftime('%Y-%m-%d %H:%M')}"
) )
+18 -15
View File
@@ -16,7 +16,6 @@ import json
def get_connection(db_path="trading_bot.db"): def get_connection(db_path="trading_bot.db"):
"""Verbindung zur Datenbank""" """Verbindung zur Datenbank"""
conn = sqlite3.connect(db_path) conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn return conn
# ========================================== # ==========================================
@@ -25,7 +24,12 @@ def get_connection(db_path="trading_bot.db"):
def load_closed_trades(conn, exclude_historical=True): def load_closed_trades(conn, exclude_historical=True):
"""Lade geschlossene Trades""" """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 SELECT
ticket, symbol, type, volume, ticket, symbol, type, volume,
entry_price, exit_price, entry_price, exit_price,
@@ -38,20 +42,15 @@ def load_closed_trades(conn, exclude_historical=True):
profit_pct, rr_ratio, profit_pct, rr_ratio,
exit_reason, status exit_reason, status
FROM trades 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) df = pd.read_sql_query(query, conn)
# Convert datetime columns
if not df.empty: if not df.empty:
df['entry_time'] = pd.to_datetime(df['entry_time'], format='mixed') df['entry_time'] = pd.to_datetime(df['entry_time'], errors='coerce')
df['exit_time'] = pd.to_datetime(df['exit_time'], format='mixed') 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['duration_hours'] = (df['exit_time'] - df['entry_time']).dt.total_seconds() / 3600
df['win'] = df['net_profit'] > 0 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_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 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() 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['cumulative'] = df_sorted['net_profit'].cumsum()
df_sorted['running_max'] = df_sorted['cumulative'].cummax() df_sorted['running_max'] = df_sorted['cumulative'].cummax()
df_sorted['drawdown'] = df_sorted['cumulative'] - df_sorted['running_max'] df_sorted['drawdown'] = df_sorted['cumulative'] - df_sorted['running_max']
max_drawdown = df_sorted['drawdown'].min() max_drawdown = abs(df_sorted['drawdown'].min())
return { return {
'total_trades': total_trades, 'total_trades': total_trades,
@@ -429,7 +430,7 @@ def run_performance_analysis(db_path="trading_bot.db", exclude_historical=True):
conn.close() conn.close()
return { results = {
'overall': overall, 'overall': overall,
'by_session': session_df, 'by_session': session_df,
'by_confidence': conf_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 'by_regime': regime_df
} }
return results
# ========================================== # ==========================================
# EXPORT TO JSON # 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 [] '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) json.dump(output, f, indent=2)
print(f"\n✅ Analysis exported to: {output_file}") print(f"\n✅ Analysis exported to: {output_file}")
+17 -20
View File
@@ -7,6 +7,7 @@ Umfassende Performance-Auswertung mit nur SQLite
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
from typing import List, Dict
# ========================================== # ==========================================
# DATABASE QUERIES # DATABASE QUERIES
@@ -14,11 +15,9 @@ from collections import defaultdict
def get_closed_trades(db_path="trading_bot.db", exclude_historical=True): def get_closed_trades(db_path="trading_bot.db", exclude_historical=True):
"""Lade geschlossene Trades""" """Lade geschlossene Trades"""
conn = sqlite3.connect(db_path) status_filter = "status = 'closed'" if exclude_historical else "status IN ('closed', 'historical')"
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = """ query = f"""
SELECT SELECT
ticket, symbol, type, volume, ticket, symbol, type, volume,
entry_price, exit_price, entry_price, exit_price,
@@ -31,19 +30,15 @@ def get_closed_trades(db_path="trading_bot.db", exclude_historical=True):
profit_pct, rr_ratio, profit_pct, rr_ratio,
exit_reason, status exit_reason, status
FROM trades FROM trades
WHERE status = 'closed' WHERE {status_filter}
ORDER BY exit_time DESC
""" """
if exclude_historical: with sqlite3.connect(db_path) as conn:
query += " AND status != 'historical'" conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query += " ORDER BY exit_time DESC" cursor.execute(query)
return [dict(row) for row in cursor.fetchall()]
cursor.execute(query)
trades = [dict(row) for row in cursor.fetchall()]
conn.close()
return trades
# ========================================== # ==========================================
# OVERALL PERFORMANCE # 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_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 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 # Calculate drawdown
cumulative = 0 cumulative = 0
@@ -78,8 +75,8 @@ def calculate_overall_metrics(trades):
if cumulative > max_cumulative: if cumulative > max_cumulative:
max_cumulative = cumulative max_cumulative = cumulative
drawdown = cumulative - max_cumulative drawdown = cumulative - max_cumulative
if drawdown < max_drawdown: if abs(drawdown) > max_drawdown:
max_drawdown = drawdown max_drawdown = abs(drawdown)
return { return {
'total_trades': total_trades, 'total_trades': total_trades,
@@ -172,7 +169,7 @@ def analyze_by_confidence(trades):
'avg_profit': avg_profit '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 # EXIT REASON ANALYSIS
@@ -215,7 +212,7 @@ def analyze_by_hour(trades):
hours = defaultdict(lambda: {'trades': 0, 'wins': 0, 'profit': 0}) hours = defaultdict(lambda: {'trades': 0, 'wins': 0, 'profit': 0})
for trade in trades: 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]['trades'] += 1
hours[hour]['profit'] += trade['net_profit'] hours[hour]['profit'] += trade['net_profit']
if trade['net_profit'] > 0: if trade['net_profit'] > 0:
+34 -35
View File
@@ -10,6 +10,7 @@ import pandas as pd
from datetime import datetime, timedelta from datetime import datetime, timedelta
import plotly.express as px import plotly.express as px
import plotly.graph_objects as go import plotly.graph_objects as go
from pathlib import Path
# ========================================== # ==========================================
# PAGE CONFIG # PAGE CONFIG
@@ -25,9 +26,14 @@ st.set_page_config(
# DATABASE CONNECTION # DATABASE CONNECTION
# ========================================== # ==========================================
DB_PATH = Path(__file__).parent / "trading_bot.db"
@st.cache_resource @st.cache_resource
def get_connection(): 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() conn = get_connection()
@@ -43,10 +49,10 @@ col1, col2, col3 = st.columns([1, 1, 2])
with col1: with col1:
if st.button("🔄 Refresh Data"): if st.button("🔄 Refresh Data"):
st.cache_data.clear() st.cache_data.clear()
st.experimental_rerun() st.rerun()
with col2: with col2:
auto_refresh = st.checkbox("Auto-refresh (30s)") auto_refresh = st.toggle("Auto-refresh (30s)")
with col3: with col3:
trade_filter = st.selectbox( trade_filter = st.selectbox(
@@ -56,10 +62,14 @@ with col3:
) )
if auto_refresh: if auto_refresh:
st.markdown("*Auto-refreshing every 30 seconds...*") if "last_refresh" not in st.session_state:
import time st.session_state.last_refresh = datetime.now()
time.sleep(30) elapsed = (datetime.now() - st.session_state.last_refresh).total_seconds()
st.experimental_rerun() 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 # LOAD DATA
@@ -69,38 +79,27 @@ if auto_refresh:
def load_all_trades(): def load_all_trades():
query = """ query = """
SELECT SELECT
ticket, ticket, position_id, symbol, strategy_name, type, volume,
position_id, entry_price, sl_price, tp_price, entry_time, exit_time,
symbol, session, regime, quality, confidence, timeframe_alignment,
strategy_name, risk_amount, risk_pct, net_profit, profit_pct, rr_ratio,
type, status, exit_reason
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 FROM trades
ORDER BY entry_time DESC 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) @st.cache_data(ttl=30)
def load_bot_status(): def load_bot_status():
query = "SELECT * FROM bot_status ORDER BY timestamp DESC LIMIT 1" try:
return pd.read_sql_query(query, conn) 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 # Load data
df_trades_raw = load_all_trades() df_trades_raw = load_all_trades()
@@ -219,7 +218,7 @@ st.subheader("⏰ Trades by Hour (UTC)")
if not df_trades.empty: if not df_trades.empty:
# Extract hour from entry_time (handle both ISO8601 and standard format) # 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 # Count trades by hour
hourly_dist = df_trades.groupby('hour_utc').size().reset_index(name='count') 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: if closed_trades > 0:
profit_timeline = df_trades[df_trades['status'] == 'closed'].copy() 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 = profit_timeline.sort_values('exit_time')
profit_timeline['cumulative_profit'] = profit_timeline['net_profit'].cumsum() profit_timeline['cumulative_profit'] = profit_timeline['net_profit'].cumsum()