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
"""
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}
+42 -42
View File
@@ -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,77 +50,72 @@ 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)
try:
tick = mt5.symbol_info_tick(SYMBOL)
if not tick:
print("❌ Could not get price data")
mt.shutdown()
return None
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"⏰ 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 = mt.copy_rates_from_pos(SYMBOL, TIMEFRAME, 0, 100)
rates = mt5.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
df = pd.DataFrame(rates)
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)
# Determine regime
if adx < 25:
if np.isnan(adx):
print("❌ ADX calculation failed (not enough data)")
return None
if adx < ADX_THRESHOLD:
regime = "ranging"
can_trade = False
symbol = "🛑"
marker = "🛑"
status = "RANGING MARKET"
decision = "Trading BLOCKED"
reason = "ADX < 25 = No clear trend"
advice = "Wait for trending market (ADX ≥ 25)"
reason = f"ADX < {ADX_THRESHOLD} = No clear trend"
advice = f"Wait for trending market (ADX ≥ {ADX_THRESHOLD})"
else:
regime = "trending"
can_trade = True
symbol = ""
marker = ""
status = "TRENDING MARKET"
decision = "Trading ALLOWED"
reason = "ADX ≥ 25 = Strong trend"
reason = f"ADX ≥ {ADX_THRESHOLD} = Strong trend"
advice = "Good conditions for trading!"
print(f"\n📈 REGIME ANALYSIS:")
print(f" Regime: {status}")
print(f" ADX: {adx:.1f}")
print(f" Status: {symbol} {regime.upper()}")
print(f" Status: {marker} {regime.upper()}")
print(f"\n🎯 TRADING DECISION:")
print(f" {symbol} {decision}")
print(f" {marker} {decision}")
print(f" 📊 {reason}")
print(f" 💡 {advice}")
# 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" YOUR ADX: {adx:.1f} {'' * min(int(adx / 2), 40)}")
print("\n" + "=" * 70)
mt.shutdown()
return {
'regime': regime,
'adx': adx,
@@ -125,9 +124,10 @@ def check_market_regime():
'timestamp': timestamp
}
finally:
mt5.shutdown()
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)
-12
View File
@@ -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
View File
@@ -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
View File
@@ -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}")
+16 -19
View 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"
with sqlite3.connect(db_path) as conn:
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute(query)
trades = [dict(row) for row in cursor.fetchall()]
conn.close()
return trades
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:
+33 -34
View File
@@ -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
"""
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()