diff --git a/ChartView.py b/ChartView.py index 9fcea50..ef5f302 100644 --- a/ChartView.py +++ b/ChartView.py @@ -8,9 +8,11 @@ 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): + 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 @@ -19,10 +21,16 @@ class ChartView: 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 = {} @@ -69,7 +77,12 @@ class ChartView: 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) @@ -77,9 +90,149 @@ class ChartView: 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: @@ -102,10 +255,14 @@ class ChartView: exchange_dropdown.pack(side=tk.LEFT, padx=(0, 10)) exchange_dropdown.bind("<>", self.on_exchange_change) - # Create the figure + # Create the figure with three subplots self.fig = plt.Figure(figsize=(12, 8), dpi=100) - self.ax1 = self.fig.add_subplot(6, 1, (1, 4)) - self.ax2 = self.fig.add_subplot(6, 1, (5, 6), sharex=self.ax1) + + # 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) diff --git a/WhalesWatcher.py b/WhalesWatcher.py index 27cc300..ed878ea 100644 --- a/WhalesWatcher.py +++ b/WhalesWatcher.py @@ -1,6 +1,6 @@ import pandas as pd import time -from datetime import datetime +from datetime import datetime, timezone import logging import ccxt from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime @@ -21,12 +21,12 @@ class Alert(Base): timestamp = Column(DateTime) class WhalesWatcher: - def __init__(self): + def __init__(self, alert_queue=None): logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', filename='logs/whales_watcher.log', - filemode='a' # Append mode + filemode='a' ) console_handler = logging.StreamHandler() @@ -38,10 +38,11 @@ class WhalesWatcher: self.exchanges = {} self.symbol = 'BTC/USDT' - self.threshold = 1000000 + self.threshold = 100000 self.check_interval = 5 self.large_orders = [] self.is_running = False + self.alert_queue = alert_queue self.logger.info("WhalesWatcher initialized with symbol: %s and threshold: $%d", self.symbol, self.threshold) @@ -88,6 +89,9 @@ class WhalesWatcher: whale_orders = [] + # Get current time in UTC + current_time_utc = datetime.now().astimezone().astimezone(timezone.utc) + for bid in order_book['bids']: if (len(bid) == 3): price, amount, timestamp = bid @@ -100,7 +104,7 @@ class WhalesWatcher: 'price': price, 'amount': amount, 'value_usd': value, - 'timestamp': datetime.now().isoformat() + 'timestamp': current_time_utc.isoformat() }) for ask in order_book['asks']: @@ -115,7 +119,7 @@ class WhalesWatcher: 'price': price, 'amount': amount, 'value_usd': value, - 'timestamp': datetime.now().isoformat() + 'timestamp': current_time_utc.isoformat() }) return whale_orders @@ -154,7 +158,7 @@ class WhalesWatcher: self.logger.info(f"Saved whale activity data to {filename}") def log_alert(self, order): - """Log the alert to the database.""" + """Log the alert to the database and alert queue.""" session = self.Session() alert = Alert( exchange=order['exchange'], @@ -167,6 +171,10 @@ class WhalesWatcher: session.add(alert) session.commit() session.close() + + # Add to queue if available + if self.alert_queue is not None: + self.alert_queue.put(order) def run(self): """Run the whale watcher continuously.""" diff --git a/main_whales_watcher.py b/main_whales_watcher.py index 7071289..db439b2 100644 --- a/main_whales_watcher.py +++ b/main_whales_watcher.py @@ -1,23 +1,27 @@ from WhalesWatcher import WhalesWatcher from ChartView import ChartView import threading +import queue -def start_whales_watcher(): - whales_watcher = WhalesWatcher() +def start_whales_watcher(alert_queue): + whales_watcher = WhalesWatcher(alert_queue) whales_watcher.run() if __name__ == "__main__": + # Create a queue for passing whale alerts between threads + whale_alert_queue = queue.Queue() + # Start WhalesWatcher in a separate thread - # watcher_thread = threading.Thread(target=start_whales_watcher) - # watcher_thread.daemon = True + watcher_thread = threading.Thread(target=start_whales_watcher, args=(whale_alert_queue,)) + watcher_thread.daemon = True # Start the watcher thread - # watcher_thread.start() + watcher_thread.start() # Define exchanges for the chart - exchange_ids = ['binance', 'kraken', 'coinbase'] # Add more exchanges as needed + exchange_ids = ['binance', 'kraken', 'coinbase', 'bitfinex', 'huobi', 'kucoin', 'bybit', 'okx', 'gateio', 'mexc'] - # Start the chart viewer - chart_view = ChartView(exchange_ids) + # Start the chart viewer with alert queue + chart_view = ChartView(exchange_ids, alert_queue=whale_alert_queue) # Run the GUI (this will block until the window is closed) chart_view.start_gui() \ No newline at end of file