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:
@@ -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} ║
|
||||
|
||||
+80
-80
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
+41
-7
@@ -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')}"
|
||||
)
|
||||
|
||||
+18
-15
@@ -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}")
|
||||
|
||||
@@ -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:
|
||||
|
||||
+34
-35
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user