gogo2/realtime.py
2025-04-01 18:43:26 +03:00

1487 lines
60 KiB
Python

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")