import asyncio import json import logging from typing import Dict, List, Optional import websockets import plotly.graph_objects as go from plotly.subplots import make_subplots import dash from dash import html, dcc from dash.dependencies import Input, Output import pandas as pd import numpy as np from collections import deque import time from threading import Thread import requests import os from datetime import datetime, timedelta import pytz import tzlocal import threading import random import dash_bootstrap_components as dbc import uuid class BinanceHistoricalData: """ Class for fetching historical price data from Binance. """ def __init__(self): self.base_url = "https://api.binance.com/api/v3" def get_historical_candles(self, symbol, interval_seconds=3600, limit=1000): """ Fetch historical candles from Binance API. Args: symbol (str): Trading pair symbol (e.g., "BTC/USDT") interval_seconds (int): Timeframe in seconds (e.g., 3600 for 1h) limit (int): Number of candles to fetch Returns: pd.DataFrame: DataFrame with OHLCV data """ # Convert interval_seconds to Binance interval format interval_map = { 1: "1s", 60: "1m", 300: "5m", 900: "15m", 1800: "30m", 3600: "1h", 14400: "4h", 86400: "1d" } interval = interval_map.get(interval_seconds, "1h") # Format symbol for Binance API (remove slash) formatted_symbol = symbol.replace("/", "") try: # Build URL for klines endpoint url = f"{self.base_url}/klines" params = { "symbol": formatted_symbol, "interval": interval, "limit": limit } # Make the request response = requests.get(url, params=params) response.raise_for_status() # Parse the response data = response.json() # Create dataframe df = pd.DataFrame(data, columns=[ "timestamp", "open", "high", "low", "close", "volume", "close_time", "quote_asset_volume", "number_of_trades", "taker_buy_base_asset_volume", "taker_buy_quote_asset_volume", "ignore" ]) # Convert timestamp to datetime df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms") # Convert price columns to float for col in ["open", "high", "low", "close", "volume"]: df[col] = df[col].astype(float) # Sort by timestamp df = df.sort_values("timestamp") logger.info(f"Fetched {len(df)} candles for {symbol} ({interval})") return df except Exception as e: logger.error(f"Error fetching historical data from Binance: {str(e)}") # Return empty dataframe on error return pd.DataFrame() def get_recent_trades(self, symbol, limit=1000): """Get recent trades for a symbol""" formatted_symbol = symbol.replace("/", "") try: url = f"{self.base_url}/trades" params = { "symbol": formatted_symbol, "limit": limit } response = requests.get(url, params=params) response.raise_for_status() data = response.json() # Create dataframe df = pd.DataFrame(data) df["time"] = pd.to_datetime(df["time"], unit="ms") df["price"] = df["price"].astype(float) df["qty"] = df["qty"].astype(float) return df except Exception as e: logger.error(f"Error fetching recent trades: {str(e)}") return pd.DataFrame() # Configure logging with more detailed format logging.basicConfig( level=logging.INFO, # Changed to DEBUG for more detailed logs format='%(asctime)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s', handlers=[ logging.StreamHandler(), logging.FileHandler('realtime_chart.log') ] ) logger = logging.getLogger(__name__) # Neural Network integration (conditional import) NN_ENABLED = os.environ.get('ENABLE_NN_MODELS', '0') == '1' nn_orchestrator = None nn_inference_thread = None if NN_ENABLED: try: import sys # Add project root to sys.path if needed project_root = os.path.dirname(os.path.abspath(__file__)) if project_root not in sys.path: sys.path.append(project_root) from NN.main import NeuralNetworkOrchestrator logger.info("Neural Network module enabled") except ImportError as e: logger.warning(f"Failed to import Neural Network module, disabling NN features: {str(e)}") NN_ENABLED = False # NN utility functions def setup_neural_network(): """Initialize the neural network components if enabled""" global nn_orchestrator, NN_ENABLED if not NN_ENABLED: return False try: # Get configuration from environment variables or use defaults symbol = os.environ.get('NN_SYMBOL', 'ETH/USDT') timeframes = os.environ.get('NN_TIMEFRAMES', '1m,5m,1h,4h,1d').split(',') output_size = int(os.environ.get('NN_OUTPUT_SIZE', '3')) # 3 for BUY/HOLD/SELL # Configure the orchestrator config = { 'symbol': symbol, 'timeframes': timeframes, 'window_size': int(os.environ.get('NN_WINDOW_SIZE', '20')), 'n_features': 5, # OHLCV 'output_size': output_size, 'model_dir': 'NN/models/saved', 'data_dir': 'NN/data' } # Initialize the orchestrator logger.info(f"Initializing Neural Network Orchestrator with config: {config}") nn_orchestrator = NeuralNetworkOrchestrator(config) # Start inference thread if enabled inference_interval = int(os.environ.get('NN_INFERENCE_INTERVAL', '60')) if inference_interval > 0: start_nn_inference_thread(inference_interval) return True except Exception as e: logger.error(f"Error setting up neural network: {str(e)}") import traceback logger.error(traceback.format_exc()) NN_ENABLED = False return False def start_nn_inference_thread(interval_seconds): """Start a background thread to periodically run inference with the neural network""" global nn_inference_thread if not NN_ENABLED or nn_orchestrator is None: logger.warning("Cannot start inference thread - Neural Network not enabled or initialized") return False def inference_worker(): """Worker function for the inference thread""" model_type = os.environ.get('NN_MODEL_TYPE', 'cnn') timeframe = os.environ.get('NN_TIMEFRAME', '1h') logger.info(f"Starting neural network inference thread with {interval_seconds}s interval") logger.info(f"Using model type: {model_type}, timeframe: {timeframe}") # Wait a bit for charts to initialize time.sleep(5) # Track active charts active_charts = [] while True: try: # Find active charts if we don't have them yet if not active_charts and 'charts' in globals(): active_charts = globals()['charts'] logger.info(f"Found {len(active_charts)} active charts for NN signals") # Run inference result = nn_orchestrator.run_inference_pipeline( model_type=model_type, timeframe=timeframe ) if result: # Log the result logger.info(f"Neural network inference result: {result}") # Add signal to charts if active_charts: try: if 'action' in result: action = result['action'] timestamp = datetime.fromisoformat(result['timestamp'].replace('Z', '+00:00')) # Get probability if available probability = None if 'probability' in result: probability = result['probability'] elif 'probabilities' in result: probability = result['probabilities'].get(action, None) # Add signal to each chart for chart in active_charts: if hasattr(chart, 'add_nn_signal'): chart.add_nn_signal(action, timestamp, probability) except Exception as e: logger.error(f"Error adding NN signal to chart: {str(e)}") import traceback logger.error(traceback.format_exc()) # Sleep for the interval time.sleep(interval_seconds) except Exception as e: logger.error(f"Error in inference thread: {str(e)}") import traceback logger.error(traceback.format_exc()) time.sleep(5) # Wait a bit before retrying # Create and start the thread nn_inference_thread = threading.Thread(target=inference_worker, daemon=True) nn_inference_thread.start() return True # Try to get local timezone, default to Sofia/EET if not available try: local_timezone = tzlocal.get_localzone() # Get timezone name safely try: tz_name = str(local_timezone) # Handle case where it might be zoneinfo.ZoneInfo object instead of pytz timezone if hasattr(local_timezone, 'zone'): tz_name = local_timezone.zone elif hasattr(local_timezone, 'key'): tz_name = local_timezone.key else: tz_name = str(local_timezone) except: tz_name = "Local" logger.info(f"Detected local timezone: {local_timezone} ({tz_name})") except Exception as e: logger.warning(f"Could not detect local timezone: {str(e)}. Defaulting to Sofia/EET") local_timezone = pytz.timezone('Europe/Sofia') tz_name = "Europe/Sofia" def convert_to_local_time(timestamp): """Convert timestamp to local timezone""" try: if isinstance(timestamp, pd.Timestamp): dt = timestamp.to_pydatetime() elif isinstance(timestamp, np.datetime64): dt = pd.Timestamp(timestamp).to_pydatetime() elif isinstance(timestamp, str): dt = pd.to_datetime(timestamp).to_pydatetime() else: dt = timestamp # If datetime is naive (no timezone), assume it's UTC if dt.tzinfo is None: dt = dt.replace(tzinfo=pytz.UTC) # Convert to local timezone local_dt = dt.astimezone(local_timezone) return local_dt except Exception as e: logger.error(f"Error converting timestamp to local time: {str(e)}") return timestamp class TickStorage: """Simple storage for ticks and candles""" def __init__(self): self.ticks = [] self.candles = {} self.latest_price = None def add_tick(self, price, volume=0, timestamp=None): """Add a tick to the storage""" if timestamp is None: timestamp = datetime.now() tick = { 'price': price, 'volume': volume, 'timestamp': timestamp } self.ticks.append(tick) self.latest_price = price # Keep only last 10000 ticks if len(self.ticks) > 10000: self.ticks = self.ticks[-10000:] # Update candles self._update_candles(tick) def get_latest_price(self): """Get the latest price""" return self.latest_price def _update_candles(self, tick): """Update candles with the new tick""" intervals = { '1m': 60, '5m': 300, '15m': 900, '1h': 3600, '4h': 14400, '1d': 86400 } for interval_key, seconds in intervals.items(): if interval_key not in self.candles: self.candles[interval_key] = [] # Get or create the current candle current_candle = self._get_current_candle(interval_key, tick['timestamp'], seconds) # Update the candle with the new tick if current_candle['high'] < tick['price']: current_candle['high'] = tick['price'] if current_candle['low'] > tick['price']: current_candle['low'] = tick['price'] current_candle['close'] = tick['price'] current_candle['volume'] += tick['volume'] def _get_current_candle(self, interval_key, timestamp, interval_seconds): """Get the current candle for the given interval, or create a new one""" # Calculate the candle start time candle_start = timestamp.replace( microsecond=0, second=0, minute=(timestamp.minute // (interval_seconds // 60)) * (interval_seconds // 60) ) if interval_seconds >= 3600: # For hourly or higher hours = (timestamp.hour // (interval_seconds // 3600)) * (interval_seconds // 3600) candle_start = candle_start.replace(hour=hours) if interval_seconds >= 86400: # For daily candle_start = candle_start.replace(hour=0) # Check if we already have a candle for this time for candle in self.candles[interval_key]: if candle['timestamp'] == candle_start: return candle # Create a new candle candle = { 'timestamp': candle_start, 'open': self.latest_price if self.latest_price is not None else tick['price'], 'high': tick['price'], 'low': tick['price'], 'close': tick['price'], 'volume': 0 } self.candles[interval_key].append(candle) return candle def get_candles(self, interval='1m'): """Get candles for the given interval""" if interval not in self.candles or not self.candles[interval]: return None # Convert to DataFrame df = pd.DataFrame(self.candles[interval]) df.set_index('timestamp', inplace=True) return df def load_from_file(self, file_path): """Load ticks from a file""" try: df = pd.read_csv(file_path) for _, row in df.iterrows(): if 'timestamp' in row: timestamp = pd.to_datetime(row['timestamp']) else: timestamp = None self.add_tick( price=row.get('price', row.get('close', 0)), volume=row.get('volume', 0), timestamp=timestamp ) logger.info(f"Loaded {len(df)} ticks from {file_path}") except Exception as e: logger.error(f"Error loading ticks from file: {str(e)}") def load_historical_data(self, historical_data, symbol): """Load historical data""" try: df = historical_data.get_historical_candles(symbol) if df is not None and not df.empty: for _, row in df.iterrows(): self.add_tick( price=row['close'], volume=row['volume'], timestamp=row['timestamp'] ) logger.info(f"Loaded {len(df)} historical candles for {symbol}") except Exception as e: logger.error(f"Error loading historical data: {str(e)}") import traceback logger.error(traceback.format_exc()) class Position: """Class representing a trading position""" def __init__(self, action, entry_price, amount, timestamp=None, trade_id=None): self.action = action # BUY or SELL self.entry_price = entry_price self.amount = amount self.timestamp = timestamp or datetime.now() self.status = "OPEN" # OPEN or CLOSED self.exit_price = None self.exit_timestamp = None self.pnl = 0.0 self.trade_id = trade_id or str(uuid.uuid4()) def close(self, exit_price, exit_timestamp=None): """Close the position with an exit price""" self.exit_price = exit_price self.exit_timestamp = exit_timestamp or datetime.now() self.status = "CLOSED" # Calculate PnL if self.action == "BUY": self.pnl = (self.exit_price - self.entry_price) * self.amount else: # SELL self.pnl = (self.entry_price - self.exit_price) * self.amount return self.pnl class RealTimeChart: """Real-time chart using Dash and Plotly""" def __init__(self, symbol, data_path=None, historical_data=None, exchange=None, timeframe='1m'): """Initialize the RealTimeChart class""" self.symbol = symbol self.exchange = exchange self.app = dash.Dash(__name__, external_stylesheets=[dbc.themes.DARKLY]) self.tick_storage = TickStorage() self.historical_data = historical_data self.data_path = data_path self.current_interval = '1m' # Default interval self.fig = None # Will hold the main chart figure self.positions = [] # List to hold position objects self.balance = 1000.0 # Starting balance self.last_action = None # Last trading action self._setup_app_layout() # Run the app in a separate thread threading.Thread(target=self._run_app, daemon=True).start() def _setup_app_layout(self): """Set up the app layout and callbacks""" # Define styling for interval buttons button_style = { 'backgroundColor': '#2C2C2C', 'color': 'white', 'border': 'none', 'padding': '10px 15px', 'margin': '5px', 'borderRadius': '5px', 'cursor': 'pointer', 'fontWeight': 'bold' } active_button_style = { **button_style, 'backgroundColor': '#4CAF50', 'boxShadow': '0 2px 4px rgba(0,0,0,0.5)' } # Create tab layout self.app.layout = dbc.Tabs([ dbc.Tab(self._get_chart_layout(button_style, active_button_style), label="Chart", tab_id="chart-tab"), # No longer need ticks tab as it's causing errors ], id="tabs") # Set up callbacks self._setup_interval_callback(button_style, active_button_style) self._setup_chart_callback() self._setup_position_list_callback() self._setup_trading_status_callback() # We've removed the ticks callback, so don't call it # self._setup_ticks_callback() def _get_chart_layout(self, button_style, active_button_style): """Get the chart layout""" return html.Div([ # Trading stats header at the top html.Div([ html.Div([ html.Div([ html.Span("Signal: ", style={'fontWeight': 'bold', 'marginRight': '5px'}), html.Span("NONE", id='current-signal-value', style={'color': 'white'}) ], style={'marginRight': '20px', 'display': 'inline-block'}), html.Div([ html.Span("Position: ", style={'fontWeight': 'bold', 'marginRight': '5px'}), html.Span("NONE", id='current-position-value', style={'color': 'white'}) ], style={'marginRight': '20px', 'display': 'inline-block'}), html.Div([ html.Span("Balance: ", style={'fontWeight': 'bold', 'marginRight': '5px'}), html.Span("$0.00", id='current-balance-value', style={'color': 'white'}) ], style={'marginRight': '20px', 'display': 'inline-block'}), html.Div([ html.Span("Session PnL: ", style={'fontWeight': 'bold', 'marginRight': '5px'}), html.Span("$0.00", id='current-pnl-value', style={'color': 'white'}) ], style={'display': 'inline-block'}) ], style={ 'padding': '10px', 'backgroundColor': '#222222', 'borderRadius': '5px', 'marginBottom': '10px', 'border': '1px solid #444444' }) ]), # Recent Trades Table (compact at top) html.Div([ html.H4("Recent Trades", style={'color': 'white', 'margin': '5px 0', 'fontSize': '14px'}), html.Table([ html.Thead(html.Tr([ html.Th("Status", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}), html.Th("Amount", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}), html.Th("Entry Price", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}), html.Th("Exit Price", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}), html.Th("PnL", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}), html.Th("Time", style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px', 'fontWeight': 'bold', 'backgroundColor': '#333333'}) ])), html.Tbody(id='position-list', children=[ html.Tr([html.Td("No positions yet", colSpan=6, style={'textAlign': 'center', 'padding': '4px', 'fontSize': '12px'})]) ]) ], style={ 'width': '100%', 'borderCollapse': 'collapse', 'fontSize': '12px', 'backgroundColor': '#222222', 'color': 'white', 'marginBottom': '10px' }) ], style={'marginBottom': '10px'}), # Chart area dcc.Graph( id='real-time-chart', style={'height': 'calc(100vh - 250px)'}, # Adjusted to account for header and table config={ 'displayModeBar': True, 'scrollZoom': True, 'modeBarButtonsToRemove': ['lasso2d', 'select2d'] } ), # Interval selector html.Div([ html.Button('1m', id='1m-interval', n_clicks=0, style=active_button_style if self.current_interval == '1m' else button_style), html.Button('5m', id='5m-interval', n_clicks=0, style=active_button_style if self.current_interval == '5m' else button_style), html.Button('15m', id='15m-interval', n_clicks=0, style=active_button_style if self.current_interval == '15m' else button_style), html.Button('1h', id='1h-interval', n_clicks=0, style=active_button_style if self.current_interval == '1h' else button_style), html.Button('4h', id='4h-interval', n_clicks=0, style=active_button_style if self.current_interval == '4h' else button_style), html.Button('1d', id='1d-interval', n_clicks=0, style=active_button_style if self.current_interval == '1d' else button_style), ], style={'textAlign': 'center', 'marginTop': '10px'}), # Interval component for automatic updates dcc.Interval( id='chart-interval', interval=300, # Refresh every 300ms for better real-time updates n_intervals=0 ) ], style={ 'backgroundColor': '#121212', 'padding': '20px', 'color': 'white', 'height': '100vh', 'boxSizing': 'border-box' }) def _get_ticks_layout(self): # Ticks data page layout return html.Div([ # Header and controls html.Div([ html.H2(f"{self.symbol} Raw Tick Data (Last 5 Minutes)", style={ 'textAlign': 'center', 'color': '#FFFFFF', 'margin': '10px 0' }), # Refresh button html.Button('Refresh Data', id='refresh-ticks-btn', n_clicks=0, style={ 'backgroundColor': '#4CAF50', 'color': 'white', 'padding': '10px 20px', 'margin': '10px auto', 'border': 'none', 'borderRadius': '5px', 'fontSize': '14px', 'cursor': 'pointer', 'display': 'block' }), # Time window selector html.Div([ html.Label("Time Window:", style={'color': 'white', 'marginRight': '10px'}), dcc.Dropdown( id='time-window-dropdown', options=[ {'label': 'Last 1 minute', 'value': 60}, {'label': 'Last 5 minutes', 'value': 300}, {'label': 'Last 15 minutes', 'value': 900}, {'label': 'Last 30 minutes', 'value': 1800}, ], value=300, # Default to 5 minutes style={'width': '200px', 'backgroundColor': '#2C2C2C', 'color': 'black'} ) ], style={ 'display': 'flex', 'alignItems': 'center', 'justifyContent': 'center', 'margin': '10px' }), ], style={ 'backgroundColor': '#2C2C2C', 'padding': '10px', 'borderRadius': '5px', 'marginBottom': '15px' }), # Stats cards html.Div(id='tick-stats-cards', style={ 'display': 'flex', 'flexWrap': 'wrap', 'justifyContent': 'space-around', 'marginBottom': '15px' }), # Ticks data table html.Div(id='ticks-table-container', style={ 'backgroundColor': '#232323', 'padding': '10px', 'borderRadius': '5px', 'overflowX': 'auto' }), # Price movement chart html.Div([ html.H3("Price Movement", style={ 'textAlign': 'center', 'color': '#FFFFFF', 'margin': '10px 0' }), dcc.Graph(id='tick-price-chart') ], style={ 'backgroundColor': '#232323', 'padding': '10px', 'borderRadius': '5px', 'marginTop': '15px' }) ]) def _setup_interval_callback(self, button_style, active_button_style): """Set up the callback for interval selection buttons""" @self.app.callback( [ Output('1m-interval', 'style'), Output('5m-interval', 'style'), Output('15m-interval', 'style'), Output('1h-interval', 'style'), Output('4h-interval', 'style'), Output('1d-interval', 'style') ], [ Input('1m-interval', 'n_clicks'), Input('5m-interval', 'n_clicks'), Input('15m-interval', 'n_clicks'), Input('1h-interval', 'n_clicks'), Input('4h-interval', 'n_clicks'), Input('1d-interval', 'n_clicks') ] ) def update_interval_buttons(n1, n5, n15, n1h, n4h, n1d): ctx = callback_context # Default styles (all inactive) styles = { '1m': button_style.copy(), '5m': button_style.copy(), '15m': button_style.copy(), '1h': button_style.copy(), '4h': button_style.copy(), '1d': button_style.copy() } # If no button clicked yet, use default interval if not ctx.triggered: styles[self.current_interval] = active_button_style.copy() return [styles['1m'], styles['5m'], styles['15m'], styles['1h'], styles['4h'], styles['1d']] # Get the button ID that was clicked button_id = ctx.triggered[0]['prop_id'].split('.')[0] # Map button ID to interval interval_map = { '1m-interval': '1m', '5m-interval': '5m', '15m-interval': '15m', '1h-interval': '1h', '4h-interval': '4h', '1d-interval': '1d' } # Update the current interval based on clicked button self.current_interval = interval_map.get(button_id, self.current_interval) # Set active style for selected interval styles[self.current_interval] = active_button_style.copy() # Update the chart with the new interval self._update_chart() return [styles['1m'], styles['5m'], styles['15m'], styles['1h'], styles['4h'], styles['1d']] def _setup_chart_callback(self): """Set up the callback for the chart updates""" @self.app.callback( Output('real-time-chart', 'figure'), [Input('chart-interval', 'n_intervals')] ) def update_chart(n_intervals): try: # Create the main figure if it doesn't exist yet if self.fig is None: self._initialize_chart() # Update the chart data self._update_chart() return self.fig except Exception as e: logger.error(f"Error updating chart: {str(e)}") import traceback logger.error(traceback.format_exc()) # Return empty figure on error return { 'data': [], 'layout': { 'title': 'Error updating chart', 'annotations': [{ 'text': str(e), 'showarrow': False, 'font': {'color': 'red'} }] } } def _setup_position_list_callback(self): """Set up the callback for the position list""" @self.app.callback( Output('position-list', 'children'), [Input('chart-interval', 'n_intervals')] ) def update_position_list(n): if not self.positions: return [html.Tr([html.Td("No positions yet", colSpan=6)])] return self._get_position_list_rows() def _setup_trading_status_callback(self): """Set up the callback for the trading status fields""" @self.app.callback( [ Output('current-signal-value', 'children'), Output('current-position-value', 'children'), Output('current-balance-value', 'children'), Output('current-pnl-value', 'children'), Output('current-signal-value', 'style'), Output('current-position-value', 'style') ], [Input('chart-interval', 'n_intervals')] ) def update_trading_status(n): # Get the current signal current_signal = "NONE" signal_style = {'color': 'white'} if hasattr(self, 'last_action') and self.last_action: current_signal = self.last_action if current_signal == "BUY": signal_style = {'color': 'green', 'fontWeight': 'bold'} elif current_signal == "SELL": signal_style = {'color': 'red', 'fontWeight': 'bold'} # Get the current position current_position = "NONE" position_style = {'color': 'white'} # Check if we have any open positions open_positions = [p for p in self.positions if p.status == "OPEN"] if open_positions: current_position = f"{open_positions[0].action} {open_positions[0].amount:.4f}" if open_positions[0].action == "BUY": position_style = {'color': 'green', 'fontWeight': 'bold'} else: position_style = {'color': 'red', 'fontWeight': 'bold'} # Get the current balance and session PnL current_balance = f"${self.balance:.2f}" if hasattr(self, 'balance') else "$0.00" # Calculate session PnL session_pnl = 0 for position in self.positions: if position.status == "CLOSED": session_pnl += position.pnl # Format PnL with color pnl_text = f"${session_pnl:.2f}" return current_signal, current_position, current_balance, pnl_text, signal_style, position_style def _add_manual_trade_inputs(self): # Add manual trade inputs self.app.layout.children.append( html.Div([ html.H3("Add Manual Trade"), dcc.Input(id='manual-price', type='number', placeholder='Price'), dcc.Input(id='manual-volume', type='number', placeholder='Volume'), dcc.Input(id='manual-pnl', type='number', placeholder='PnL'), dcc.Input(id='manual-action', type='text', placeholder='Action'), html.Button('Add Trade', id='add-manual-trade') ]) ) def _interval_to_seconds(self, interval_key: str) -> int: """Convert interval key to seconds""" mapping = { '1s': 1, '1m': 60, '1h': 3600, '1d': 86400 } return mapping.get(interval_key, 1) async def start_websocket(self): ws = ExchangeWebSocket(self.symbol) connection_attempts = 0 max_attempts = 10 # Maximum connection attempts before longer waiting period while True: # Keep trying to maintain connection connection_attempts += 1 if not await ws.connect(): logger.error(f"Failed to connect to exchange for {self.symbol}") # Gradually increase wait time based on number of connection failures wait_time = min(5 * connection_attempts, 60) # Cap at 60 seconds logger.warning(f"Waiting {wait_time} seconds before retry (attempt {connection_attempts})") if connection_attempts >= max_attempts: logger.warning(f"Reached {max_attempts} connection attempts, taking a longer break") await asyncio.sleep(120) # 2 minutes wait after max attempts connection_attempts = 0 # Reset counter else: await asyncio.sleep(wait_time) continue # Successfully connected connection_attempts = 0 try: logger.info(f"WebSocket connected for {self.symbol}, beginning data collection") tick_count = 0 last_tick_count_log = time.time() last_status_report = time.time() # Track stats for reporting price_min = float('inf') price_max = float('-inf') price_last = None volume_total = 0 start_collection_time = time.time() while True: if not ws.running: logger.warning(f"WebSocket connection lost for {self.symbol}, breaking loop") break data = await ws.receive() if data: if data.get('type') == 'kline': # Use kline data directly for candlestick trade_data = { 'timestamp': data['timestamp'], 'price': data['price'], 'volume': data['volume'], 'open': data['open'], 'high': data['high'], 'low': data['low'] } logger.debug(f"Received kline data: {data}") else: # Use trade data trade_data = { 'timestamp': data['timestamp'], 'price': data['price'], 'volume': data['volume'] } # Update stats price = trade_data['price'] volume = trade_data['volume'] price_min = min(price_min, price) price_max = max(price_max, price) price_last = price volume_total += volume # Store raw tick in the tick storage self.tick_storage.add_tick(trade_data) tick_count += 1 # Also update the old candlestick data for backward compatibility # Add check to ensure the candlestick_data attribute exists before using it if hasattr(self, 'candlestick_data'): self.candlestick_data.update_from_trade(trade_data) # Log tick counts periodically current_time = time.time() if current_time - last_tick_count_log >= 10: # Log every 10 seconds elapsed = current_time - last_tick_count_log tps = tick_count / elapsed if elapsed > 0 else 0 logger.info(f"{self.symbol}: Collected {tick_count} ticks in last {elapsed:.1f}s ({tps:.2f} ticks/sec), total: {len(self.tick_storage.ticks)}") last_tick_count_log = current_time tick_count = 0 # Check if ticks are being converted to candles if len(self.tick_storage.ticks) > 0: sample_df = self.tick_storage.get_candles(interval_seconds=1) logger.info(f"{self.symbol}: Sample candle count: {len(sample_df)}") # Periodic status report (every 60 seconds) if current_time - last_status_report >= 60: elapsed_total = current_time - start_collection_time logger.info(f"{self.symbol} Status Report:") logger.info(f" Collection time: {elapsed_total:.1f} seconds") logger.info(f" Price range: {price_min:.2f} - {price_max:.2f} (last: {price_last:.2f})") logger.info(f" Total volume: {volume_total:.8f}") logger.info(f" Active ticks in storage: {len(self.tick_storage.ticks)}") # Reset stats for next period last_status_report = current_time price_min = float('inf') if price_last is None else price_last price_max = float('-inf') if price_last is None else price_last volume_total = 0 await asyncio.sleep(0.01) except websockets.exceptions.ConnectionClosed as e: logger.error(f"WebSocket connection closed for {self.symbol}: {str(e)}") except Exception as e: logger.error(f"Error in WebSocket loop for {self.symbol}: {str(e)}") import traceback logger.error(traceback.format_exc()) finally: logger.info(f"Closing WebSocket connection for {self.symbol}") await ws.close() logger.info(f"Waiting 5 seconds before reconnecting {self.symbol} WebSocket...") await asyncio.sleep(5) def _run_app(self): """Run the Dash app""" try: logger.info(f"Starting Dash app for {self.symbol}") # Updated to use app.run instead of app.run_server (which is deprecated) self.app.run(debug=False, use_reloader=False, port=8050) except Exception as e: logger.error(f"Error running Dash app: {str(e)}") logger.error(traceback.format_exc()) return def add_trade(self, price, timestamp=None, pnl=None, amount=0.1, action="BUY", trade_type="MARKET"): """Add a trade to the chart Args: price: Trade price timestamp: Trade timestamp (datetime or milliseconds) pnl: Profit and Loss (for SELL trades) amount: Trade amount action: Trade action (BUY or SELL) trade_type: Trade type (MARKET, LIMIT, etc.) """ try: # Convert timestamp to datetime if it's a number if timestamp is None: timestamp = datetime.now() elif isinstance(timestamp, (int, float)): timestamp = datetime.fromtimestamp(timestamp / 1000) # Process the trade based on action if action == "BUY": # Create a new position position = Position( action="BUY", entry_price=price, amount=amount, timestamp=timestamp ) self.positions.append(position) # Update last action self.last_action = "BUY" elif action == "SELL": # Find an open BUY position to close, or create a new SELL position open_buy_position = None for pos in self.positions: if pos.status == "OPEN" and pos.action == "BUY": open_buy_position = pos break if open_buy_position: # Close the position pnl_value = open_buy_position.close(price, timestamp) # Update balance self.balance += pnl_value # If pnl was provided, use it instead if pnl is not None: open_buy_position.pnl = pnl self.balance = self.balance - pnl_value + pnl else: # Create a standalone SELL position position = Position( action="SELL", entry_price=price, amount=amount, timestamp=timestamp ) # Set it as closed with the same price position.close(price, timestamp) # Set PnL if provided if pnl is not None: position.pnl = pnl self.balance += pnl self.positions.append(position) # Update last action self.last_action = "SELL" # Log the trade logger.info(f"Added {action} trade: price={price}, amount={amount}, time={timestamp}, PnL={pnl}") # Trigger more frequent chart updates for immediate visibility if hasattr(self, 'fig') and self.fig is not None: self._update_chart() except Exception as e: logger.error(f"Error adding trade: {str(e)}") import traceback logger.error(traceback.format_exc()) def update_trading_info(self, signal=None, position=None, balance=None, pnl=None): """Update the current trading information to be displayed on the chart Args: signal: Current signal (BUY, SELL, HOLD) position: Current position size balance: Current session balance pnl: Current session PnL """ if signal is not None: if signal in ['BUY', 'SELL', 'HOLD']: self.current_signal = signal self.signal_time = datetime.now() else: logger.warning(f"Invalid signal type: {signal}") if position is not None: self.current_position = position if balance is not None: self.session_balance = balance if pnl is not None: self.session_pnl = pnl logger.debug(f"Updated trading info: Signal={self.current_signal}, Position={self.current_position}, Balance=${self.session_balance:.2f}, PnL={self.session_pnl:.4f}") def _get_position_list_rows(self): """Generate rows for the position table""" if not self.positions: return [html.Tr([html.Td("No positions yet", colSpan=6)])] position_rows = [] # Sort positions by time (most recent first) sorted_positions = sorted(self.positions, key=lambda x: x.timestamp if hasattr(x, 'timestamp') else datetime.now(), reverse=True) # Take only the most recent 5 positions for position in sorted_positions[:5]: # Format time time_obj = position.timestamp if hasattr(position, 'timestamp') else datetime.now() if isinstance(time_obj, datetime): # If trade is from a different day, include the date today = datetime.now().date() if time_obj.date() == today: time_str = time_obj.strftime('%H:%M:%S') else: time_str = time_obj.strftime('%m-%d %H:%M:%S') else: time_str = str(time_obj) # Format prices with proper decimal places entry_price = position.entry_price if hasattr(position, 'entry_price') else 'N/A' if isinstance(entry_price, (int, float)): entry_price_str = f"${entry_price:.6f}" else: entry_price_str = str(entry_price) # For exit price, use close_price for closed positions or current market price for open ones if position.status == "CLOSED" and hasattr(position, 'exit_price'): exit_price = position.exit_price else: exit_price = self.tick_storage.get_latest_price() if position.status == "OPEN" else 'N/A' if isinstance(exit_price, (int, float)): exit_price_str = f"${exit_price:.6f}" else: exit_price_str = str(exit_price) # Format amount amount = position.amount if hasattr(position, 'amount') else 0.1 amount_str = f"{amount:.4f} BTC" # Format PnL if position.status == "CLOSED": pnl = position.pnl if hasattr(position, 'pnl') else 0 pnl_str = f"${pnl:.2f}" pnl_color = '#00FF00' if pnl >= 0 else '#FF0000' elif position.status == "OPEN" and position.action == "BUY": # Calculate unrealized PnL for open positions if isinstance(exit_price, (int, float)) and isinstance(entry_price, (int, float)): unrealized_pnl = (exit_price - entry_price) * amount pnl_str = f"${unrealized_pnl:.2f} (unrealized)" pnl_color = '#00FF00' if unrealized_pnl >= 0 else '#FF0000' else: pnl_str = 'N/A' pnl_color = '#FFFFFF' else: pnl_str = 'N/A' pnl_color = '#FFFFFF' # Set action/status color and text if position.status == 'OPEN': status_color = '#00AAFF' # Blue for open positions status_text = f"OPEN ({position.action})" elif position.status == 'CLOSED': if hasattr(position, 'pnl') and isinstance(position.pnl, (int, float)): status_color = '#00FF00' if position.pnl >= 0 else '#FF0000' # Green/Red based on profit else: status_color = '#FFCC00' # Yellow if PnL unknown status_text = "CLOSED" else: status_color = '#00FF00' if position.action == 'BUY' else '#FF0000' status_text = position.action # Create table row with more compact styling position_rows.append(html.Tr([ html.Td(status_text, style={'color': status_color, 'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}), html.Td(amount_str, style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}), html.Td(entry_price_str, style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}), html.Td(exit_price_str, style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}), html.Td(pnl_str, style={'color': pnl_color, 'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}), html.Td(time_str, style={'padding': '4px 8px', 'border': '1px solid #444', 'fontSize': '12px'}) ])) return position_rows def _initialize_chart(self): """Initialize the chart figure""" # Create a figure with subplots for price and volume self.fig = make_subplots( rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.8, 0.2], subplot_titles=(f"{self.symbol} Price Chart", "Volume") ) # Set up initial empty traces self.fig.add_trace( go.Candlestick( x=[], open=[], high=[], low=[], close=[], name='Price', increasing={'line': {'color': '#26A69A', 'width': 1}, 'fillcolor': '#26A69A'}, decreasing={'line': {'color': '#EF5350', 'width': 1}, 'fillcolor': '#EF5350'} ), row=1, col=1 ) # Add volume trace self.fig.add_trace( go.Bar( x=[], y=[], name='Volume', marker={'color': '#888888'} ), row=2, col=1 ) # Add empty traces for buy/sell markers self.fig.add_trace( go.Scatter( x=[], y=[], mode='markers', name='BUY', marker=dict( symbol='triangle-up', size=12, color='rgba(0,255,0,0.8)', line=dict(width=1, color='darkgreen') ), showlegend=True ), row=1, col=1 ) self.fig.add_trace( go.Scatter( x=[], y=[], mode='markers', name='SELL', marker=dict( symbol='triangle-down', size=12, color='rgba(255,0,0,0.8)', line=dict(width=1, color='darkred') ), showlegend=True ), row=1, col=1 ) # Update layout self.fig.update_layout( title=f"{self.symbol} Real-Time Trading Chart", title_x=0.5, template='plotly_dark', paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(25,25,50,1)', height=800, xaxis_rangeslider_visible=False, legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="center", x=0.5 ) ) # Update axes styling self.fig.update_xaxes( showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)', zeroline=False ) self.fig.update_yaxes( showgrid=True, gridwidth=1, gridcolor='rgba(128,128,128,0.2)', zeroline=False ) # Do an initial update to populate the chart self._update_chart() def _update_chart(self): """Update the chart with the latest data""" try: # Get candlesticks data for the current interval df = self.tick_storage.get_candles(interval=self.current_interval) if df is None or df.empty: logger.warning(f"No candle data available for {self.current_interval}") return # Limit the number of candles to display (show 500 for context) df = df.tail(500) # Update candlestick data self.fig.update_traces( x=df.index, open=df['open'], high=df['high'], low=df['low'], close=df['close'], selector=dict(type='candlestick') ) # Update volume bars with colors based on price movement colors = ['rgba(0,255,0,0.5)' if close >= open else 'rgba(255,0,0,0.5)' for open, close in zip(df['open'], df['close'])] self.fig.update_traces( x=df.index, y=df['volume'], marker_color=colors, selector=dict(type='bar') ) # Calculate y-axis range with padding for better visibility if len(df) > 0: low_min = df['low'].min() high_max = df['high'].max() price_range = high_max - low_min y_min = low_min - (price_range * 0.05) # 5% padding below y_max = high_max + (price_range * 0.05) # 5% padding above # Update y-axis range self.fig.update_yaxes(range=[y_min, y_max], row=1, col=1) # Update Buy/Sell markers if hasattr(self, 'positions') and self.positions: # Collect buy and sell points buy_times = [] buy_prices = [] sell_times = [] sell_prices = [] for position in self.positions: # Handle buy trades if position.action == "BUY": buy_times.append(position.timestamp) buy_prices.append(position.entry_price) # Handle sell trades or closed positions if position.status == "CLOSED" and hasattr(position, 'exit_timestamp') and hasattr(position, 'exit_price'): sell_times.append(position.exit_timestamp) sell_prices.append(position.exit_price) # Update buy markers trace self.fig.update_traces( x=buy_times, y=buy_prices, selector=dict(name='BUY') ) # Update sell markers trace self.fig.update_traces( x=sell_times, y=sell_prices, selector=dict(name='SELL') ) # Update chart title with the current interval self.fig.update_layout( title=f"{self.symbol} Real-Time Chart ({self.current_interval})" ) except Exception as e: logger.error(f"Error in _update_chart: {str(e)}") import traceback logger.error(traceback.format_exc()) async def main(): global charts # Make charts globally accessible for NN integration symbols = ["ETH/USDT", "ETH/USDT"] logger.info(f"Starting application for symbols: {symbols}") # Initialize neural network if enabled if NN_ENABLED: logger.info("Initializing Neural Network integration...") if setup_neural_network(): logger.info("Neural Network integration initialized successfully") else: logger.warning("Neural Network integration failed to initialize") charts = [] websocket_tasks = [] # Create a chart and websocket task for each symbol for symbol in symbols: chart = RealTimeChart(symbol) charts.append(chart) websocket_tasks.append(asyncio.create_task(chart.start_websocket())) # Run Dash in a separate thread to not block the event loop server_threads = [] for i, chart in enumerate(charts): port = 8050 + i # Use different ports for each chart logger.info(f"Starting chart for {chart.symbol} on port {port}") thread = Thread(target=lambda c=chart, p=port: c.run(port=p)) # Ensure correct port is passed thread.daemon = True thread.start() server_threads.append(thread) logger.info(f"Thread started for {chart.symbol} on port {port}") try: # Keep the main task running while True: await asyncio.sleep(1) except KeyboardInterrupt: logger.info("Shutting down...") except Exception as e: logger.error(f"Unexpected error: {str(e)}") finally: for task in websocket_tasks: task.cancel() try: await task except asyncio.CancelledError: pass if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: logger.info("Application terminated by user")