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