import mplfinance as mpf import pandas as pd import matplotlib.pyplot as plt from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk import matplotlib.animation as animation import ccxt from matplotlib.dates import date2num import threading import tkinter as tk from tkinter import ttk import queue from datetime import datetime class ChartView: def __init__(self, exchange_ids, symbol='BTC/USDT', timeframe='1m', limit=100, alert_queue=None): self.exchange_ids = exchange_ids self.symbol = symbol self.timeframe = timeframe self.limit = limit self.running = False self.fig = None self.ax1 = None self.ax2 = None self.ax_whales = None # New axis for whale activity self.animation = None self.current_exchange_id = exchange_ids[0] self.root = None self.canvas = None self.alert_queue = alert_queue self.whale_markers = [] # Maintain a dict of whale alerts by exchange self.whale_alerts = {exchange_id: [] for exchange_id in exchange_ids} # Create exchanges dictionary to avoid reconnecting each time self.exchanges = {} for exchange_id in exchange_ids: self.connect_to_exchange(exchange_id) def connect_to_exchange(self, exchange_id): if exchange_id in self.exchanges: self.exchange = self.exchanges[exchange_id] return try: exchange_class = getattr(ccxt, exchange_id) exchange = exchange_class({ 'enableRateLimit': True, }) exchange.load_markets() if self.symbol not in exchange.markets: raise Exception(f"Symbol {self.symbol} not found on {exchange_id}") self.exchanges[exchange_id] = exchange self.exchange = exchange except Exception as e: raise Exception(f"Failed to connect to {exchange_id}: {str(e)}") def fetch_ohlcv(self, exchange_id): try: exchange = self.exchanges[exchange_id] ohlcv = exchange.fetch_ohlcv(self.symbol, self.timeframe, limit=self.limit) df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms') df.set_index('timestamp', inplace=True) return df except Exception as e: print(f"Error fetching OHLCV data from {exchange_id}: {str(e)}") return None def update_chart(self): if not self.running: return df = self.fetch_ohlcv(self.current_exchange_id) if df is not None: self.ax1.clear() self.ax2.clear() self.ax_whales.clear() # Process any pending whale alerts from the queue self.process_whale_queue() # Plot the chart mpf.plot(df, type='candle', ax=self.ax1, volume=self.ax2, style='yahoo', xrotation=0, ylabel='Price', ylabel_lower='Volume', show_nontrading=False) latest_price = df['close'].iloc[-1] self.ax1.set_title(f'{self.symbol} on {self.current_exchange_id} - Last: ${latest_price:.2f}') # Plot whale activity on the dedicated subplot self.plot_whale_activity(df) # Refresh the canvas self.canvas.draw() def process_whale_queue(self): """Process any new whale alerts from the queue""" if self.alert_queue is None: return # Process all available alerts while not self.alert_queue.empty(): try: alert = self.alert_queue.get_nowait() exchange_id = alert['exchange'] # Add timestamp if not present if 'timestamp' not in alert: alert['timestamp'] = datetime.now().isoformat() # Store the alert with the appropriate exchange if exchange_id in self.whale_alerts: self.whale_alerts[exchange_id].append(alert) print(f"New whale alert for {exchange_id}: {alert['type']} ${alert['value_usd']:,.2f}") self.alert_queue.task_done() except queue.Empty: break except Exception as e: print(f"Error processing whale alert from queue: {str(e)}") if self.alert_queue is not None: self.alert_queue.task_done() def plot_whale_activity(self, df): """Plot whale activity on the dedicated whale subplot""" if self.current_exchange_id not in self.whale_alerts: self.ax_whales.set_ylabel('Whale Activity') return # Get the whale alerts for current exchange exchange_alerts = self.whale_alerts[self.current_exchange_id] if not exchange_alerts: self.ax_whales.set_ylabel('Whale Activity') return try: # Create a dataframe from the whale alerts alerts_df = pd.DataFrame(exchange_alerts) # Handle the timestamp conversion carefully alerts_df['timestamp'] = pd.to_datetime(alerts_df['timestamp']) # Check if the dataframe timestamps have timezone info has_tz = False if not alerts_df.empty: has_tz = alerts_df['timestamp'].iloc[0].tzinfo is not None # Get the start and end time of the current chart start_time = df.index[0] end_time = df.index[-1] # Convert all timestamps to naive (remove timezone info) for comparison # First create a copy of the timestamp column alerts_df['plot_timestamp'] = alerts_df['timestamp'] # If timestamps have timezone, convert them to naive by replacing with their UTC equivalent if has_tz: alerts_df['plot_timestamp'] = alerts_df['timestamp'].dt.tz_localize(None) else: # If timestamps are naive, assume they're in local time and adjust by GMT+8 offset alerts_df['plot_timestamp'] = alerts_df['timestamp'] - pd.Timedelta(hours=8) # Filter to only include alerts in the visible time range visible_alerts = alerts_df[ (alerts_df['plot_timestamp'] >= start_time) & (alerts_df['plot_timestamp'] <= end_time) ] if visible_alerts.empty: self.ax_whales.set_ylabel('Whale Activity') return # Create two separate series for buy and sell orders buy_orders = visible_alerts[visible_alerts['type'] == 'bid'].copy() sell_orders = visible_alerts[visible_alerts['type'] == 'ask'].copy() # Draw the buy orders as green bars going up if not buy_orders.empty: buy_values = buy_orders['value_usd'] / 1_000_000 # Convert to millions self.ax_whales.bar(buy_orders['plot_timestamp'], buy_values, color='green', alpha=0.6, width=pd.Timedelta(minutes=1)) # Draw the sell orders as red bars going down if not sell_orders.empty: sell_values = -1 * sell_orders['value_usd'] / 1_000_000 # Convert to millions and make negative self.ax_whales.bar(sell_orders['plot_timestamp'], sell_values, color='red', alpha=0.6, width=pd.Timedelta(minutes=1)) # Format the whale activity subplot self.ax_whales.set_ylabel('Whale Activity ($M)') # Add zero line self.ax_whales.axhline(y=0, color='black', linestyle='-', alpha=0.3) # Set y-axis limits with some padding all_values = visible_alerts['value_usd'] / 1_000_000 if len(all_values) > 0: max_val = all_values.max() * 1.1 self.ax_whales.set_ylim(-max_val, max_val) # Align the x-axis with the price chart self.ax_whales.sharex(self.ax1) # Add text labels for significant whale activity for idx, row in visible_alerts.iterrows(): value_millions = row['value_usd'] / 1_000_000 sign = 1 if row['type'] == 'bid' else -1 position = sign * value_millions if abs(value_millions) > max(all_values) * 0.3: # Only label significant activity self.ax_whales.text( row['plot_timestamp'], position * 1.05, f"${abs(value_millions):.1f}M", ha='center', va='bottom' if sign > 0 else 'top', fontsize=8, color='green' if sign > 0 else 'red' ) except Exception as e: print(f"Error plotting whale activity: {str(e)}") import traceback traceback.print_exc() self.ax_whales.set_ylabel('Whale Activity - Error plotting data') def parse_timeframe_to_minutes(self, timeframe): """Convert timeframe string to minutes""" if timeframe.endswith('m'): return int(timeframe[:-1]) elif timeframe.endswith('h'): return int(timeframe[:-1]) * 60 elif timeframe.endswith('d'): return int(timeframe[:-1]) * 1440 return 1 # default to 1 minute def on_exchange_change(self, event=None): selected_exchange = self.exchange_var.get() if selected_exchange != self.current_exchange_id: self.current_exchange_id = selected_exchange self.update_chart() def start_gui(self): self.root = tk.Tk() self.root.title("Crypto Chart Viewer") self.root.geometry("1200x800") # Create frame for controls control_frame = ttk.Frame(self.root) control_frame.pack(side=tk.TOP, fill=tk.X, padx=10, pady=5) # Dropdown for selecting exchange ttk.Label(control_frame, text="Exchange:").pack(side=tk.LEFT, padx=(0, 5)) self.exchange_var = tk.StringVar(value=self.current_exchange_id) exchange_dropdown = ttk.Combobox(control_frame, textvariable=self.exchange_var, values=self.exchange_ids, width=15) exchange_dropdown.pack(side=tk.LEFT, padx=(0, 10)) exchange_dropdown.bind("<>", self.on_exchange_change) # Create the figure with three subplots self.fig = plt.Figure(figsize=(12, 8), dpi=100) # Adjust the subplot grid to add whale activity panel # Price chart (60%), Volume (20%), Whale Activity (20%) self.ax1 = self.fig.add_subplot(5, 1, (1, 3)) # Price chart - 3/5 of the space self.ax2 = self.fig.add_subplot(5, 1, 4, sharex=self.ax1) # Volume - 1/5 of the space self.ax_whales = self.fig.add_subplot(5, 1, 5, sharex=self.ax1) # Whale activity - 1/5 of the space # Create the canvas to display the figure self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) # Add toolbar toolbar = NavigationToolbar2Tk(self.canvas, self.root) toolbar.update() self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) # Initial chart update self.running = True self.update_chart() # Set up periodic updates def update(): if self.running: self.update_chart() self.root.after(10000, update) # Update every 10 seconds self.root.after(10000, update) # Handle window close def on_closing(): self.running = False self.root.destroy() self.root.protocol("WM_DELETE_WINDOW", on_closing) # Start the Tkinter event loop self.root.mainloop()