diff --git a/NN/models/saved/checkpoint_metadata.json b/NN/models/saved/checkpoint_metadata.json
index 3cb26aa..9d5b504 100644
--- a/NN/models/saved/checkpoint_metadata.json
+++ b/NN/models/saved/checkpoint_metadata.json
@@ -122,5 +122,67 @@
"wandb_run_id": null,
"wandb_artifact_name": null
}
+ ],
+ "extrema_trainer": [
+ {
+ "checkpoint_id": "extrema_trainer_20250624_221645",
+ "model_name": "extrema_trainer",
+ "model_type": "extrema_trainer",
+ "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221645.pt",
+ "created_at": "2025-06-24T22:16:45.728299",
+ "file_size_mb": 0.0013427734375,
+ "performance_score": 0.1,
+ "accuracy": 0.0,
+ "loss": null,
+ "val_accuracy": null,
+ "val_loss": null,
+ "reward": null,
+ "pnl": null,
+ "epoch": null,
+ "training_time_hours": null,
+ "total_parameters": null,
+ "wandb_run_id": null,
+ "wandb_artifact_name": null
+ },
+ {
+ "checkpoint_id": "extrema_trainer_20250624_221915",
+ "model_name": "extrema_trainer",
+ "model_type": "extrema_trainer",
+ "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_221915.pt",
+ "created_at": "2025-06-24T22:19:15.325368",
+ "file_size_mb": 0.0013427734375,
+ "performance_score": 0.1,
+ "accuracy": 0.0,
+ "loss": null,
+ "val_accuracy": null,
+ "val_loss": null,
+ "reward": null,
+ "pnl": null,
+ "epoch": null,
+ "training_time_hours": null,
+ "total_parameters": null,
+ "wandb_run_id": null,
+ "wandb_artifact_name": null
+ },
+ {
+ "checkpoint_id": "extrema_trainer_20250624_222303",
+ "model_name": "extrema_trainer",
+ "model_type": "extrema_trainer",
+ "file_path": "NN\\models\\saved\\extrema_trainer\\extrema_trainer_20250624_222303.pt",
+ "created_at": "2025-06-24T22:23:03.283194",
+ "file_size_mb": 0.0013427734375,
+ "performance_score": 0.1,
+ "accuracy": 0.0,
+ "loss": null,
+ "val_accuracy": null,
+ "val_loss": null,
+ "reward": null,
+ "pnl": null,
+ "epoch": null,
+ "training_time_hours": null,
+ "total_parameters": null,
+ "wandb_run_id": null,
+ "wandb_artifact_name": null
+ }
]
}
\ No newline at end of file
diff --git a/_dev/notes.md b/_dev/notes.md
index 30f5853..d3c91d1 100644
--- a/_dev/notes.md
+++ b/_dev/notes.md
@@ -16,6 +16,14 @@ do we load the best model for each model type? or we do a cold start each time?
we stopped showing executed trades on the chart. let's add them back
.
update chart every second as well.
+the list with closed trades is not updated. clear session button does not clear all data.
+
+add buttons for quick manual buy/sell (max 1 lot. sell closes long, buy closes short if already open position exists)
+
+
+
+
+
>> Training
diff --git a/add_current_trade.py b/add_current_trade.py
new file mode 100644
index 0000000..1ce0eb9
--- /dev/null
+++ b/add_current_trade.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python3
+
+import json
+from datetime import datetime
+import time
+
+def add_current_trade():
+ """Add a trade with current timestamp for immediate visibility"""
+ now = datetime.now()
+
+ # Create a trade that just happened
+ current_trade = {
+ 'trade_id': 999,
+ 'symbol': 'ETHUSDT',
+ 'side': 'LONG',
+ 'entry_time': (now - timedelta(seconds=30)).isoformat(), # 30 seconds ago
+ 'exit_time': now.isoformat(), # Just now
+ 'entry_price': 2434.50,
+ 'exit_price': 2434.70,
+ 'size': 0.001,
+ 'fees': 0.05,
+ 'net_pnl': 0.15, # Small profit
+ 'mexc_executed': True,
+ 'duration_seconds': 30,
+ 'leverage': 50.0,
+ 'gross_pnl': 0.20,
+ 'fee_type': 'TAKER',
+ 'fee_rate': 0.0005
+ }
+
+ # Load existing trades
+ try:
+ with open('closed_trades_history.json', 'r') as f:
+ trades = json.load(f)
+ except:
+ trades = []
+
+ # Add the current trade
+ trades.append(current_trade)
+
+ # Save back
+ with open('closed_trades_history.json', 'w') as f:
+ json.dump(trades, f, indent=2)
+
+ print(f"ā
Added current trade: LONG @ {current_trade['entry_time']} -> {current_trade['exit_time']}")
+ print(f" Entry: ${current_trade['entry_price']} | Exit: ${current_trade['exit_price']} | P&L: ${current_trade['net_pnl']}")
+
+if __name__ == "__main__":
+ from datetime import timedelta
+ add_current_trade()
\ No newline at end of file
diff --git a/test_manual_trading.py b/test_manual_trading.py
new file mode 100644
index 0000000..433c15a
--- /dev/null
+++ b/test_manual_trading.py
@@ -0,0 +1,88 @@
+#!/usr/bin/env python3
+"""
+Test script for manual trading buttons functionality
+"""
+
+import requests
+import json
+import time
+from datetime import datetime
+
+def test_manual_trading():
+ """Test the manual trading buttons functionality"""
+ print("Testing manual trading buttons...")
+
+ # Check if dashboard is running
+ try:
+ response = requests.get("http://127.0.0.1:8050", timeout=5)
+ if response.status_code == 200:
+ print("ā
Dashboard is running on port 8050")
+ else:
+ print(f"ā Dashboard returned status code: {response.status_code}")
+ return
+ except Exception as e:
+ print(f"ā Dashboard not accessible: {e}")
+ return
+
+ # Check if trades file exists
+ try:
+ with open('closed_trades_history.json', 'r') as f:
+ trades = json.load(f)
+ print(f"š Current trades in history: {len(trades)}")
+ if trades:
+ latest_trade = trades[-1]
+ print(f" Latest trade: {latest_trade.get('side')} at ${latest_trade.get('exit_price', latest_trade.get('entry_price'))}")
+ except FileNotFoundError:
+ print("š No trades history file found (this is normal for fresh start)")
+ except Exception as e:
+ print(f"ā Error reading trades file: {e}")
+
+ print("\nšÆ Manual Trading Test Instructions:")
+ print("1. Open dashboard at http://127.0.0.1:8050")
+ print("2. Look for the 'MANUAL BUY' and 'MANUAL SELL' buttons")
+ print("3. Click 'MANUAL BUY' to create a test long position")
+ print("4. Wait a few seconds, then click 'MANUAL SELL' to close and create short")
+ print("5. Check the chart for green triangles showing trade entry/exit points")
+ print("6. Check the 'Closed Trades' table for trade records")
+
+ print("\nš Expected Results:")
+ print("- Green triangles should appear on the price chart at trade execution times")
+ print("- Dashed lines should connect entry and exit points")
+ print("- Trade records should appear in the closed trades table")
+ print("- Session P&L should update with trade profits/losses")
+
+ print("\nš Monitoring trades file...")
+ initial_count = 0
+ try:
+ with open('closed_trades_history.json', 'r') as f:
+ initial_count = len(json.load(f))
+ except:
+ pass
+
+ print(f"Initial trade count: {initial_count}")
+ print("Watching for new trades... (Press Ctrl+C to stop)")
+
+ try:
+ while True:
+ time.sleep(2)
+ try:
+ with open('closed_trades_history.json', 'r') as f:
+ current_trades = json.load(f)
+ current_count = len(current_trades)
+
+ if current_count > initial_count:
+ new_trades = current_trades[initial_count:]
+ for trade in new_trades:
+ print(f"š NEW TRADE: {trade.get('side')} | Entry: ${trade.get('entry_price'):.2f} | Exit: ${trade.get('exit_price'):.2f} | P&L: ${trade.get('net_pnl'):.2f}")
+ initial_count = current_count
+
+ except FileNotFoundError:
+ pass
+ except Exception as e:
+ print(f"Error monitoring trades: {e}")
+
+ except KeyboardInterrupt:
+ print("\nā
Test monitoring stopped")
+
+if __name__ == "__main__":
+ test_manual_trading()
\ No newline at end of file
diff --git a/web/dashboard.py b/web/dashboard.py
index 1300a8b..5169216 100644
--- a/web/dashboard.py
+++ b/web/dashboard.py
@@ -309,7 +309,9 @@ class TradingDashboard:
self.closed_trades = [] # List of all closed trades with full details
# Load existing closed trades from file
+ logger.info("DASHBOARD: Loading closed trades from file...")
self._load_closed_trades_from_file()
+ logger.info(f"DASHBOARD: Loaded {len(self.closed_trades)} closed trades")
# Signal execution settings for scalping - REMOVED FREQUENCY LIMITS
self.min_confidence_threshold = 0.30 # Start lower to allow learning
@@ -840,6 +842,7 @@ class TradingDashboard:
], className="card-body text-center p-2")
], className="card bg-light", style={"height": "60px"}),
], style={"display": "grid", "gridTemplateColumns": "repeat(4, 1fr)", "gap": "8px", "width": "60%"}),
+
# Right side - Merged: Recent Signals & Model Training - 2 columns
html.Div([
@@ -869,13 +872,28 @@ class TradingDashboard:
# Charts row - Now full width since training moved up
html.Div([
- # Price chart - Full width
+ # Price chart - Full width with manual trading buttons
html.Div([
html.Div([
- html.H6([
- html.I(className="fas fa-chart-candlestick me-2"),
- "Live 1s Price & Volume Chart (WebSocket Stream)"
- ], className="card-title mb-2"),
+ # Chart header with manual trading buttons
+ html.Div([
+ html.H6([
+ html.I(className="fas fa-chart-candlestick me-2"),
+ "Live 1s Price & Volume Chart (WebSocket Stream)"
+ ], className="card-title mb-0"),
+ html.Div([
+ html.Button([
+ html.I(className="fas fa-arrow-up me-1"),
+ "BUY"
+ ], id="manual-buy-btn", className="btn btn-success btn-sm me-2",
+ style={"fontSize": "10px", "padding": "2px 8px"}),
+ html.Button([
+ html.I(className="fas fa-arrow-down me-1"),
+ "SELL"
+ ], id="manual-sell-btn", className="btn btn-danger btn-sm",
+ style={"fontSize": "10px", "padding": "2px 8px"})
+ ], className="d-flex")
+ ], className="d-flex justify-content-between align-items-center mb-2"),
dcc.Graph(id="price-chart", style={"height": "400px"})
], className="card-body p-2")
], className="card", style={"width": "100%"}),
@@ -1172,25 +1190,30 @@ class TradingDashboard:
]
position_class = "fw-bold mb-0 small"
else:
- position_text = "No Position"
- position_class = "text-muted mb-0 small"
+ # Show HOLD when no position is open
+ from dash import html
+ position_text = [
+ html.Span("[HOLD] ", className="text-warning fw-bold"),
+ html.Span("No Position - Waiting for Signal", className="text-muted")
+ ]
+ position_class = "fw-bold mb-0 small"
# MEXC status (simple)
mexc_status = "LIVE" if (self.trading_executor and self.trading_executor.trading_enabled and not self.trading_executor.simulation_mode) else "SIM"
- # CHART OPTIMIZATION - Real-time chart updates every 1 second
+ # OPTIMIZED CHART - Using new optimized version with trade caching
if is_chart_update:
try:
if hasattr(self, '_cached_chart_data_time'):
cache_time = self._cached_chart_data_time
- if time.time() - cache_time < 5: # Use cached chart if < 5s old for faster updates
+ if time.time() - cache_time < 3: # Use cached chart if < 3s old for faster updates
price_chart = getattr(self, '_cached_price_chart', None)
else:
- price_chart = self._create_price_chart_optimized(symbol, current_price)
+ price_chart = self._create_price_chart_optimized_v2(symbol)
self._cached_price_chart = price_chart
self._cached_chart_data_time = time.time()
else:
- price_chart = self._create_price_chart_optimized(symbol, current_price)
+ price_chart = self._create_price_chart_optimized_v2(symbol)
self._cached_price_chart = price_chart
self._cached_chart_data_time = time.time()
except Exception as e:
@@ -1382,6 +1405,83 @@ class TradingDashboard:
except Exception as e:
logger.error(f"Error updating leverage: {e}")
return f"{self.leverage_multiplier:.0f}x", "Error", "badge bg-secondary"
+
+ # Manual Buy button callback
+ @self.app.callback(
+ Output('recent-decisions', 'children', allow_duplicate=True),
+ [Input('manual-buy-btn', 'n_clicks')],
+ prevent_initial_call=True
+ )
+ def manual_buy(n_clicks):
+ """Execute manual buy order"""
+ if n_clicks and n_clicks > 0:
+ try:
+ symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT"
+ current_price = self.get_realtime_price(symbol) or 2434.0
+
+ # Create manual trading decision
+ manual_decision = {
+ 'action': 'BUY',
+ 'symbol': symbol,
+ 'price': current_price,
+ 'size': 0.001, # Small test size (max 1 lot)
+ 'confidence': 1.0, # Manual trades have 100% confidence
+ 'timestamp': datetime.now(),
+ 'source': 'MANUAL_BUY',
+ 'mexc_executed': False, # Mark as manual/test trade
+ 'usd_size': current_price * 0.001
+ }
+
+ # Process the trading decision
+ self._process_trading_decision(manual_decision)
+
+ logger.info(f"MANUAL: BUY executed at ${current_price:.2f}")
+ return dash.no_update
+
+ except Exception as e:
+ logger.error(f"Error executing manual buy: {e}")
+ return dash.no_update
+
+ return dash.no_update
+
+ # Manual Sell button callback
+ @self.app.callback(
+ Output('recent-decisions', 'children', allow_duplicate=True),
+ [Input('manual-sell-btn', 'n_clicks')],
+ prevent_initial_call=True
+ )
+ def manual_sell(n_clicks):
+ """Execute manual sell order"""
+ if n_clicks and n_clicks > 0:
+ try:
+ symbol = self.config.symbols[0] if self.config.symbols else "ETH/USDT"
+ current_price = self.get_realtime_price(symbol) or 2434.0
+
+ # Create manual trading decision
+ manual_decision = {
+ 'action': 'SELL',
+ 'symbol': symbol,
+ 'price': current_price,
+ 'size': 0.001, # Small test size (max 1 lot)
+ 'confidence': 1.0, # Manual trades have 100% confidence
+ 'timestamp': datetime.now(),
+ 'source': 'MANUAL_SELL',
+ 'mexc_executed': False, # Mark as manual/test trade
+ 'usd_size': current_price * 0.001
+ }
+
+ # Process the trading decision
+ self._process_trading_decision(manual_decision)
+
+ logger.info(f"MANUAL: SELL executed at ${current_price:.2f}")
+ return dash.no_update
+
+ except Exception as e:
+ logger.error(f"Error executing manual sell: {e}")
+ return dash.no_update
+
+ return dash.no_update
+
def _simulate_price_update(self, symbol: str, base_price: float) -> float:
"""
@@ -1439,19 +1539,18 @@ class TradingDashboard:
"""Create price chart with volume and Williams pivot points from cached data"""
try:
# For Williams Market Structure, we need 1s data for proper recursive analysis
- # Get 5 minutes (300 seconds) of 1s data for accurate pivot calculation
+ # Get 4 hours (240 minutes) of 1m data for better trade visibility
df_1s = None
df_1m = None
- # Try to get 1s data first for Williams analysis
+ # Try to get 1s data first for Williams analysis (reduced to 10 minutes for performance)
try:
- df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=False)
+ df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=600, refresh=False)
if df_1s is None or df_1s.empty:
logger.warning("[CHART] No 1s cached data available, trying fresh 1s data")
df_1s = self.data_provider.get_historical_data(symbol, '1s', limit=300, refresh=True)
if df_1s is not None and not df_1s.empty:
- logger.debug(f"[CHART] Using {len(df_1s)} 1s bars for Williams analysis")
# Aggregate 1s data to 1m for chart display (cleaner visualization)
df = self._aggregate_1s_to_1m(df_1s)
actual_timeframe = '1sā1m'
@@ -1461,14 +1560,14 @@ class TradingDashboard:
logger.warning(f"[CHART] Error getting 1s data: {e}")
df_1s = None
- # Fallback to 1m data if 1s not available
+ # Fallback to 1m data if 1s not available (4 hours for historical trades)
if df_1s is None:
- df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=False)
+ df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=False)
if df is None or df.empty:
logger.warning("[CHART] No cached 1m data available, trying fresh 1m data")
try:
- df = self.data_provider.get_historical_data(symbol, '1m', limit=30, refresh=True)
+ df = self.data_provider.get_historical_data(symbol, '1m', limit=240, refresh=True)
if df is not None and not df.empty:
# Ensure timezone consistency for fresh data
df = self._ensure_timezone_consistency(df)
@@ -1491,7 +1590,6 @@ class TradingDashboard:
# Ensure timezone consistency for cached data
df = self._ensure_timezone_consistency(df)
actual_timeframe = '1m'
- logger.debug(f"[CHART] Using {len(df)} 1m bars from cached data in {self.timezone}")
# Final check: ensure we have valid data with proper index
if df is None or df.empty:
@@ -1542,9 +1640,7 @@ class TradingDashboard:
pivot_points = self._get_williams_pivot_points_for_chart(williams_data, chart_df=df)
if pivot_points:
self._add_williams_pivot_points_to_chart(fig, pivot_points, row=1)
- logger.info(f"[CHART] Added Williams pivot points using {actual_timeframe} data")
- else:
- logger.debug("[CHART] No Williams pivot points calculated")
+ logger.debug(f"[CHART] Added Williams pivot points using {actual_timeframe} data")
except Exception as e:
logger.debug(f"Error adding Williams pivot points to chart: {e}")
@@ -1632,7 +1728,7 @@ class TradingDashboard:
elif decision['action'] == 'SELL':
sell_decisions.append((decision, signal_type))
- logger.debug(f"[CHART] Showing {len(buy_decisions)} BUY and {len(sell_decisions)} SELL signals in chart timeframe")
+
# Add BUY markers with different styles for executed vs ignored
executed_buys = [d[0] for d in buy_decisions if d[1] == 'EXECUTED']
@@ -1766,7 +1862,9 @@ class TradingDashboard:
if (chart_start_utc <= entry_time_pd <= chart_end_utc) or (chart_start_utc <= exit_time_pd <= chart_end_utc):
chart_trades.append(trade)
- logger.debug(f"[CHART] Showing {len(chart_trades)} closed trades on chart")
+ # Minimal logging - only show count
+ if len(chart_trades) > 0:
+ logger.debug(f"[CHART] Showing {len(chart_trades)} trades on chart")
# Plot closed trades with profit/loss styling
profitable_entries_x = []
@@ -2926,9 +3024,12 @@ class TradingDashboard:
import json
from pathlib import Path
+ logger.info("LOAD_TRADES: Checking for closed_trades_history.json...")
if Path('closed_trades_history.json').exists():
+ logger.info("LOAD_TRADES: File exists, loading...")
with open('closed_trades_history.json', 'r') as f:
trades_data = json.load(f)
+ logger.info(f"LOAD_TRADES: Raw data loaded: {len(trades_data)} trades")
# Convert string dates back to datetime objects
for trade in trades_data:
@@ -6035,6 +6136,188 @@ class TradingDashboard:
except Exception as e:
logger.debug(f"Signal processing error: {e}")
+ def _create_price_chart_optimized_v2(self, symbol: str) -> go.Figure:
+ """OPTIMIZED: Create price chart with cached trade filtering and minimal logging"""
+ try:
+ chart_start = time.time()
+
+ # STEP 1: Get chart data with minimal API calls
+ df = None
+ actual_timeframe = '1m'
+
+ # Try cached 1m data first (fastest)
+ df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=False)
+ if df is None or df.empty:
+ # Fallback to fresh data only if needed
+ df = self.data_provider.get_historical_data(symbol, '1m', limit=120, refresh=True)
+ if df is None or df.empty:
+ return self._create_empty_chart(f"{symbol} Chart", "No data available")
+
+ # STEP 2: Ensure proper timezone (cached result)
+ if not hasattr(self, '_tz_cache_time') or time.time() - self._tz_cache_time > 300: # 5min cache
+ df = self._ensure_timezone_consistency(df)
+ self._tz_cache_time = time.time()
+
+ # STEP 3: Create base chart quickly
+ fig = make_subplots(
+ rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1,
+ subplot_titles=(f'{symbol} Price ({actual_timeframe.upper()})', 'Volume'),
+ row_heights=[0.7, 0.3]
+ )
+
+ # STEP 4: Add price line (main trace)
+ fig.add_trace(
+ go.Scatter(
+ x=df.index, y=df['close'], mode='lines', name=f"{symbol} Price",
+ line=dict(color='#00ff88', width=2),
+ hovertemplate='$%{y:.2f}
%{x}'
+ ), row=1, col=1
+ )
+
+ # STEP 5: Add volume (if available)
+ if 'volume' in df.columns:
+ fig.add_trace(
+ go.Bar(x=df.index, y=df['volume'], name='Volume',
+ marker_color='rgba(158, 158, 158, 0.6)'), row=2, col=1
+ )
+
+ # STEP 6: OPTIMIZED TRADE VISUALIZATION - with caching
+ if self.closed_trades:
+ # Cache trade filtering results for 30 seconds
+ cache_key = f"trades_{len(self.closed_trades)}_{df.index.min()}_{df.index.max()}"
+ if (not hasattr(self, '_trade_cache') or
+ self._trade_cache.get('key') != cache_key or
+ time.time() - self._trade_cache.get('time', 0) > 30):
+
+ # Filter trades to chart timeframe (expensive operation)
+ chart_start_utc = df.index.min().tz_localize(None) if df.index.min().tz else df.index.min()
+ chart_end_utc = df.index.max().tz_localize(None) if df.index.max().tz else df.index.max()
+
+ chart_trades = []
+ for trade in self.closed_trades:
+ if not isinstance(trade, dict):
+ continue
+
+ entry_time = trade.get('entry_time')
+ exit_time = trade.get('exit_time')
+ if not entry_time or not exit_time:
+ continue
+
+ # Quick timezone conversion
+ try:
+ if isinstance(entry_time, datetime):
+ entry_utc = entry_time.replace(tzinfo=None) if not entry_time.tzinfo else entry_time.astimezone(timezone.utc).replace(tzinfo=None)
+ else:
+ continue
+
+ if isinstance(exit_time, datetime):
+ exit_utc = exit_time.replace(tzinfo=None) if not exit_time.tzinfo else exit_time.astimezone(timezone.utc).replace(tzinfo=None)
+ else:
+ continue
+
+ # Check if trade overlaps with chart
+ entry_pd = pd.to_datetime(entry_utc)
+ exit_pd = pd.to_datetime(exit_utc)
+
+ if (chart_start_utc <= entry_pd <= chart_end_utc) or (chart_start_utc <= exit_pd <= chart_end_utc):
+ chart_trades.append(trade)
+ except:
+ continue # Skip problematic trades
+
+ # Cache the result
+ self._trade_cache = {
+ 'key': cache_key,
+ 'time': time.time(),
+ 'trades': chart_trades
+ }
+ else:
+ # Use cached trades
+ chart_trades = self._trade_cache['trades']
+
+ # STEP 7: Render trade markers (optimized)
+ if chart_trades:
+ profitable_entries_x, profitable_entries_y = [], []
+ profitable_exits_x, profitable_exits_y = [], []
+
+ for trade in chart_trades:
+ entry_price = trade.get('entry_price', 0)
+ exit_price = trade.get('exit_price', 0)
+ entry_time = trade.get('entry_time')
+ exit_time = trade.get('exit_time')
+ net_pnl = trade.get('net_pnl', 0)
+
+ if not all([entry_price, exit_price, entry_time, exit_time]):
+ continue
+
+ # Convert to local time for display
+ entry_local = self._to_local_timezone(entry_time)
+ exit_local = self._to_local_timezone(exit_time)
+
+ # Only show profitable trades as filled markers (cleaner UI)
+ if net_pnl > 0:
+ profitable_entries_x.append(entry_local)
+ profitable_entries_y.append(entry_price)
+ profitable_exits_x.append(exit_local)
+ profitable_exits_y.append(exit_price)
+
+ # Add connecting line for all trades
+ line_color = '#00ff88' if net_pnl > 0 else '#ff6b6b'
+ fig.add_trace(
+ go.Scatter(
+ x=[entry_local, exit_local], y=[entry_price, exit_price],
+ mode='lines', line=dict(color=line_color, width=2, dash='dash'),
+ name="Trade", showlegend=False, hoverinfo='skip'
+ ), row=1, col=1
+ )
+
+ # Add profitable trade markers
+ if profitable_entries_x:
+ fig.add_trace(
+ go.Scatter(
+ x=profitable_entries_x, y=profitable_entries_y, mode='markers',
+ marker=dict(color='#00ff88', size=12, symbol='triangle-up',
+ line=dict(color='white', width=1)),
+ name="Profitable Entry", showlegend=True,
+ hovertemplate="ENTRY
$%{y:.2f}
%{x}"
+ ), row=1, col=1
+ )
+
+ if profitable_exits_x:
+ fig.add_trace(
+ go.Scatter(
+ x=profitable_exits_x, y=profitable_exits_y, mode='markers',
+ marker=dict(color='#00ff88', size=12, symbol='triangle-down',
+ line=dict(color='white', width=1)),
+ name="Profitable Exit", showlegend=True,
+ hovertemplate="EXIT
$%{y:.2f}
%{x}"
+ ), row=1, col=1
+ )
+
+ # STEP 8: Update layout efficiently
+ latest_price = df['close'].iloc[-1] if not df.empty else 0
+ current_time = datetime.now().strftime("%H:%M:%S")
+
+ fig.update_layout(
+ title=f"{symbol} | ${latest_price:.2f} | {current_time}",
+ template="plotly_dark", height=400, xaxis_rangeslider_visible=False,
+ margin=dict(l=20, r=20, t=50, b=20),
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
+ )
+
+ fig.update_yaxes(title_text="Price ($)", row=1, col=1)
+ fig.update_yaxes(title_text="Volume", row=2, col=1)
+
+ # Performance logging (minimal)
+ chart_time = (time.time() - chart_start) * 1000
+ if chart_time > 200: # Only log slow charts
+ logger.warning(f"[CHART] Slow chart render: {chart_time:.0f}ms")
+
+ return fig
+
+ except Exception as e:
+ logger.error(f"Optimized chart error: {e}")
+ return self._create_empty_chart(f"{symbol} Chart", f"Chart Error: {str(e)}")
+
def _create_price_chart_optimized(self, symbol, current_price):
"""Optimized chart creation with minimal data fetching"""
try: