CryptoMarketParser/ChartView.py
Simon Moisy fcc0342fd8 Add whale activity plotting and alert processing to ChartView
- Introduced a new subplot for whale activity in ChartView (WIP)
- Implemented alert queue processing to handle whale alerts.
- Updated WhalesWatcher to support alert queue integration.
2025-03-25 17:00:53 +08:00

296 lines
12 KiB
Python

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("<<ComboboxSelected>>", 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()