diff --git a/web/dashboard.py b/web/dashboard.py
index d908018..5527c5e 100644
--- a/web/dashboard.py
+++ b/web/dashboard.py
@@ -1415,6 +1415,193 @@ class TradingDashboard:
row=1, col=1
)
+ # Add closed trades markers with profit/loss styling and connecting lines
+ if self.closed_trades and not df.empty:
+ # Get the timeframe of displayed chart
+ chart_start_time = df.index.min()
+ chart_end_time = df.index.max()
+
+ # Convert chart times to UTC for comparison
+ if isinstance(chart_start_time, pd.Timestamp):
+ chart_start_utc = chart_start_time.tz_localize(None) if chart_start_time.tz is None else chart_start_time.tz_convert('UTC').tz_localize(None)
+ chart_end_utc = chart_end_time.tz_localize(None) if chart_end_time.tz is None else chart_end_time.tz_convert('UTC').tz_localize(None)
+ else:
+ chart_start_utc = pd.to_datetime(chart_start_time).tz_localize(None)
+ chart_end_utc = pd.to_datetime(chart_end_time).tz_localize(None)
+
+ # Filter closed trades to only those within chart timeframe
+ 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
+
+ # Convert times to UTC for comparison
+ if isinstance(entry_time, datetime):
+ entry_time_utc = entry_time.astimezone(timezone.utc).replace(tzinfo=None) if entry_time.tzinfo else entry_time
+ else:
+ continue
+
+ if isinstance(exit_time, datetime):
+ exit_time_utc = exit_time.astimezone(timezone.utc).replace(tzinfo=None) if exit_time.tzinfo else exit_time
+ else:
+ continue
+
+ # Check if trade overlaps with chart timeframe
+ entry_time_pd = pd.to_datetime(entry_time_utc)
+ exit_time_pd = pd.to_datetime(exit_time_utc)
+
+ 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")
+
+ # Plot closed trades with profit/loss styling
+ profitable_entries_x = []
+ profitable_entries_y = []
+ profitable_exits_x = []
+ profitable_exits_y = []
+ losing_entries_x = []
+ losing_entries_y = []
+ losing_exits_x = []
+ losing_exits_y = []
+
+ # Collect trade points for display
+ 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)
+ side = trade.get('side', 'LONG')
+
+ if not all([entry_price, exit_price, entry_time, exit_time]):
+ continue
+
+ # Convert times to local timezone for display
+ entry_time_local = self._to_local_timezone(entry_time)
+ exit_time_local = self._to_local_timezone(exit_time)
+
+ # Determine if trade was profitable
+ is_profitable = net_pnl > 0
+
+ if is_profitable:
+ profitable_entries_x.append(entry_time_local)
+ profitable_entries_y.append(entry_price)
+ profitable_exits_x.append(exit_time_local)
+ profitable_exits_y.append(exit_price)
+ else:
+ losing_entries_x.append(entry_time_local)
+ losing_entries_y.append(entry_price)
+ losing_exits_x.append(exit_time_local)
+ losing_exits_y.append(exit_price)
+
+ # Add connecting dash line between entry and exit
+ line_color = '#00ff88' if is_profitable else '#ff6b6b'
+ fig.add_trace(
+ go.Scatter(
+ x=[entry_time_local, exit_time_local],
+ y=[entry_price, exit_price],
+ mode='lines',
+ line=dict(
+ color=line_color,
+ width=2,
+ dash='dash'
+ ),
+ name="Trade Path",
+ showlegend=False,
+ hoverinfo='skip'
+ ),
+ row=1, col=1
+ )
+
+ # Add profitable trade markers (filled triangles)
+ if profitable_entries_x:
+ # Entry markers (triangle-up for LONG, triangle-down for SHORT - filled)
+ fig.add_trace(
+ go.Scatter(
+ x=profitable_entries_x,
+ y=profitable_entries_y,
+ mode='markers',
+ marker=dict(
+ color='#00ff88', # Green fill for profitable
+ size=12,
+ symbol='triangle-up',
+ line=dict(color='white', width=1)
+ ),
+ name="Profitable Entry",
+ showlegend=True,
+ hovertemplate="PROFITABLE ENTRY
Price: $%{y:.2f}
Time: %{x}"
+ ),
+ row=1, col=1
+ )
+
+ if profitable_exits_x:
+ # Exit markers (triangle-down for LONG, triangle-up for SHORT - filled)
+ fig.add_trace(
+ go.Scatter(
+ x=profitable_exits_x,
+ y=profitable_exits_y,
+ mode='markers',
+ marker=dict(
+ color='#00ff88', # Green fill for profitable
+ size=12,
+ symbol='triangle-down',
+ line=dict(color='white', width=1)
+ ),
+ name="Profitable Exit",
+ showlegend=True,
+ hovertemplate="PROFITABLE EXIT
Price: $%{y:.2f}
Time: %{x}"
+ ),
+ row=1, col=1
+ )
+
+ # Add losing trade markers (border only triangles)
+ if losing_entries_x:
+ # Entry markers (triangle-up for LONG, triangle-down for SHORT - border only)
+ fig.add_trace(
+ go.Scatter(
+ x=losing_entries_x,
+ y=losing_entries_y,
+ mode='markers',
+ marker=dict(
+ color='rgba(255, 107, 107, 0)', # Transparent fill
+ size=12,
+ symbol='triangle-up',
+ line=dict(color='#ff6b6b', width=2) # Red border for losing
+ ),
+ name="Losing Entry",
+ showlegend=True,
+ hovertemplate="LOSING ENTRY
Price: $%{y:.2f}
Time: %{x}"
+ ),
+ row=1, col=1
+ )
+
+ if losing_exits_x:
+ # Exit markers (triangle-down for LONG, triangle-up for SHORT - border only)
+ fig.add_trace(
+ go.Scatter(
+ x=losing_exits_x,
+ y=losing_exits_y,
+ mode='markers',
+ marker=dict(
+ color='rgba(255, 107, 107, 0)', # Transparent fill
+ size=12,
+ symbol='triangle-down',
+ line=dict(color='#ff6b6b', width=2) # Red border for losing
+ ),
+ name="Losing Exit",
+ showlegend=True,
+ hovertemplate="LOSING EXIT
Price: $%{y:.2f}
Time: %{x}"
+ ),
+ row=1, col=1
+ )
+
# Update layout with current timestamp and streaming status
current_time = datetime.now().strftime("%H:%M:%S.%f")[:-3]
latest_price = df['close'].iloc[-1] if not df.empty else 0