working charts again!!!

This commit is contained in:
Dobromir Popov
2025-05-24 01:15:16 +03:00
parent 477e5dca39
commit 0c445435d0
5 changed files with 539 additions and 38 deletions

View File

@ -1017,6 +1017,7 @@ class TickStorage:
self.current_candle = {tf: None for tf in self.timeframes}
self.last_candle_timestamp = {tf: None for tf in self.timeframes}
self.cache_dir = os.path.join(os.getcwd(), "cache", symbol.replace("/", ""))
self.cache_path = os.path.join(self.cache_dir, f"{symbol.replace('/', '')}_ticks.json") # Add missing cache_path
self.use_timescaledb = use_timescaledb
self.max_ticks = 10000 # Maximum number of ticks to store in memory
@ -1024,9 +1025,13 @@ class TickStorage:
os.makedirs(self.cache_dir, exist_ok=True)
logger.info(f"Creating new tick storage for {symbol} with timeframes {self.timeframes}")
logger.info(f"Cache directory: {self.cache_dir}")
logger.info(f"Cache file: {self.cache_path}")
if use_timescaledb:
print(f"TickStorage: TimescaleDB integration is ENABLED for {symbol}")
else:
logger.info(f"TickStorage: TimescaleDB integration is DISABLED for {symbol}")
def _save_to_cache(self):
"""Save ticks to a cache file"""
@ -1282,40 +1287,57 @@ class TickStorage:
def load_historical_data(self, symbol, limit=1000):
"""Load historical data for all timeframes"""
logger.info(f"Starting historical data load for {symbol} with limit {limit}")
# Clear existing data
self.ticks = []
self.candles = {tf: [] for tf in self.timeframes}
self.current_candle = {tf: None for tf in self.timeframes}
# Try to load ticks from cache first
logger.info("Attempting to load from cache...")
cache_loaded = self._load_from_cache()
if cache_loaded:
logger.info("Successfully loaded data from cache")
else:
logger.info("No valid cache data found")
# Check if we have TimescaleDB enabled
if self.use_timescaledb:
if self.use_timescaledb and timescaledb_handler and timescaledb_handler.enabled:
logger.info("Attempting to fetch historical data from TimescaleDB")
loaded_from_db = False
# Load candles for each timeframe from TimescaleDB
for tf in self.timeframes:
candles = timescaledb_handler.fetch_candles(symbol, tf, limit)
if candles:
self.candles[tf] = candles
loaded_from_db = True
logger.info(f"Loaded {len(candles)} {tf} candles from TimescaleDB")
try:
candles = timescaledb_handler.fetch_candles(symbol, tf, limit)
if candles:
self.candles[tf] = candles
loaded_from_db = True
logger.info(f"Loaded {len(candles)} {tf} candles from TimescaleDB")
else:
logger.info(f"No {tf} candles found in TimescaleDB")
except Exception as e:
logger.error(f"Error loading {tf} candles from TimescaleDB: {str(e)}")
if loaded_from_db:
logger.info("Successfully loaded historical data from TimescaleDB")
return True
else:
logger.info("TimescaleDB not available or disabled")
# If no TimescaleDB data and no cache, we need to get from Binance API
if not cache_loaded:
logger.info("Loading data from Binance API...")
# Create a BinanceHistoricalData instance
historical_data = BinanceHistoricalData()
# Load data for each timeframe
success_count = 0
for tf in self.timeframes:
if tf != "1s": # Skip 1s since we'll generate it from ticks
try:
logger.info(f"Fetching {tf} candles for {symbol}...")
df = historical_data.get_historical_candles(symbol, self._timeframe_to_seconds(tf), limit)
if df is not None and not df.empty:
logger.info(f"Loaded {len(df)} {tf} candles from Binance API")
@ -1334,15 +1356,23 @@ class TickStorage:
candles.append(candle)
# Also save to TimescaleDB if enabled
if self.use_timescaledb:
if self.use_timescaledb and timescaledb_handler and timescaledb_handler.enabled:
timescaledb_handler.upsert_candle(symbol, tf, candle)
self.candles[tf] = candles
success_count += 1
else:
logger.warning(f"No data returned for {tf} candles")
except Exception as e:
logger.error(f"Error loading {tf} candles: {str(e)}")
import traceback
logger.error(traceback.format_exc())
logger.info(f"Successfully loaded {success_count} timeframes from Binance API")
# For 1s, load from API if possible or compute from first available timeframe
if "1s" in self.timeframes:
logger.info("Loading 1s candles...")
# Try to get 1s data from Binance
try:
df_1s = historical_data.get_historical_candles(symbol, 1, 300) # Only need recent 1s data
@ -1363,7 +1393,7 @@ class TickStorage:
candles_1s.append(candle)
# Also save to TimescaleDB if enabled
if self.use_timescaledb:
if self.use_timescaledb and timescaledb_handler and timescaledb_handler.enabled:
timescaledb_handler.upsert_candle(symbol, "1s", candle)
self.candles["1s"] = candles_1s
@ -1372,6 +1402,7 @@ class TickStorage:
# If 1s data not available or failed to load, approximate from 1m data
if not self.candles.get("1s"):
logger.info("1s data not available, trying to approximate from 1m data...")
# If 1s data not available, we can approximate from 1m data
if "1m" in self.timeframes and self.candles["1m"]:
# For demonstration, just use the 1m candles as placeholders for 1s
@ -1381,6 +1412,7 @@ class TickStorage:
# Take the most recent 5 minutes of 1m candles
recent_1m = self.candles["1m"][-5:] if self.candles["1m"] else []
logger.info(f"Creating 1s approximations from {len(recent_1m)} 1m candles")
for candle_1m in recent_1m:
# Create 60 1s candles for each 1m candle
ts_base = candle_1m["timestamp"].timestamp()
@ -1397,17 +1429,23 @@ class TickStorage:
self.candles["1s"].append(candle_1s)
# Also save to TimescaleDB if enabled
if self.use_timescaledb:
if self.use_timescaledb and timescaledb_handler and timescaledb_handler.enabled:
timescaledb_handler.upsert_candle(symbol, "1s", candle_1s)
logger.info(f"Created {len(self.candles['1s'])} approximated 1s candles")
else:
logger.warning("No 1m data available to approximate 1s candles from")
# Set the last candle of each timeframe as the current candle
for tf in self.timeframes:
if self.candles[tf]:
self.current_candle[tf] = self.candles[tf][-1].copy()
self.last_candle_timestamp[tf] = self.current_candle[tf]["timestamp"]
logger.debug(f"Set current candle for {tf}: {self.current_candle[tf]['timestamp']}")
# If we loaded ticks from cache, rebuild candles
if cache_loaded:
logger.info("Rebuilding candles from cached ticks...")
# Clear candles
self.candles = {tf: [] for tf in self.timeframes}
self.current_candle = {tf: None for tf in self.timeframes}
@ -1419,8 +1457,17 @@ class TickStorage:
self._update_1s_candle(tick)
else:
self._update_candles_for_timeframe(tf, tick)
logger.info("Finished rebuilding candles from ticks")
return cache_loaded or (self.candles["1m"] if "1m" in self.candles else False)
# Log final results
for tf in self.timeframes:
count = len(self.candles[tf])
logger.info(f"Final {tf} candle count: {count}")
has_data = cache_loaded or any(self.candles[tf] for tf in self.timeframes)
logger.info(f"Historical data loading completed. Has data: {has_data}")
return has_data
def _try_cache_ticks(self):
"""Try to save ticks to cache periodically"""
@ -1561,6 +1608,11 @@ class RealTimeChart:
self.trades_per_minute = 0
self.trades_per_hour = 0
# Initialize trade rate tracking variables
self.trade_times = [] # Store timestamps of recent trades for rate calculation
self.last_trade_rate_calculation = datetime.now()
self.trade_rate = {"per_second": 0, "per_minute": 0, "per_hour": 0}
# Initialize interactive components
self.app = app
@ -1583,6 +1635,23 @@ class RealTimeChart:
timeframes=["1s", "1m", "5m", "15m", "1h", "4h", "1d"],
use_timescaledb=use_timescaledb
)
# Load historical data immediately for cold start
logger.info(f"Loading historical data for {symbol} during chart initialization")
try:
data_loaded = self.tick_storage.load_historical_data(symbol)
if data_loaded:
logger.info(f"Successfully loaded historical data for {symbol}")
# Log what we have
for tf in ["1s", "1m", "5m", "15m", "1h"]:
candle_count = len(self.tick_storage.candles.get(tf, []))
logger.info(f" {tf}: {candle_count} candles")
else:
logger.warning(f"Failed to load historical data for {symbol}")
except Exception as e:
logger.error(f"Error loading historical data during initialization: {str(e)}")
import traceback
logger.error(traceback.format_exc())
else:
self.tick_storage = tick_storage
@ -1592,7 +1661,7 @@ class RealTimeChart:
self.app.layout = self._create_layout()
# Register callbacks
self._register_callbacks()
self._setup_callbacks()
# Log initialization
if self.enable_logging:
@ -1653,13 +1722,10 @@ class RealTimeChart:
# Store for the selected timeframe
dcc.Store(id='interval-store', data={'interval': 1}),
# Chart content container that will be updated by callbacks
html.Div(id='chart-content', children=[
# Initial content
dcc.Graph(id='live-chart', style={"height": "600px"}),
dcc.Graph(id='secondary-charts', style={"height": "500px"}),
html.Div(id='positions-list')
])
# Chart content (without wrapper div to avoid callback issues)
dcc.Graph(id='live-chart', style={"height": "600px"}),
dcc.Graph(id='secondary-charts', style={"height": "500px"}),
html.Div(id='positions-list')
])
def _create_chart_and_controls(self):
@ -2016,26 +2082,20 @@ class RealTimeChart:
try:
# Get candles from tick storage
interval_key = self._get_interval_key(interval_seconds)
df = self.tick_storage.get_candles(interval_key)
candles_list = self.tick_storage.get_candles(interval_key)
if df is None or df.empty:
logger.warning(f"No candle data available for {interval_key}")
return [] # Return empty list if no data
if not candles_list:
logger.warning(f"No candle data available for {interval_key} - trying to load historical data")
# Try to load historical data if we don't have any
self.tick_storage.load_historical_data(self.symbol)
candles_list = self.tick_storage.get_candles(interval_key)
# Convert dataframe to list of dictionaries
candles = []
for idx, row in df.iterrows():
candle = {
'timestamp': idx,
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
candles.append(candle)
if not candles_list:
logger.error(f"Still no candle data available for {interval_key} after loading historical data")
return []
return candles
logger.info(f"Retrieved {len(candles_list)} candles for {interval_key}")
return candles_list
except Exception as e:
logger.error(f"Error getting candles: {str(e)}")
@ -2197,7 +2257,11 @@ class RealTimeChart:
def run(self, host='localhost', port=8050):
"""Run the Dash app on the specified host and port"""
try:
logger.info(f"Starting Dash app for {self.symbol} on {host}:{port}")
logger.info("="*60)
logger.info(f"🚀 STARTING WEB UI FOR {self.symbol}")
logger.info(f"📱 Web interface available at: http://{host}:{port}/")
logger.info(f"🌐 Open this URL in your browser to view the trading chart")
logger.info("="*60)
self.app.run(debug=False, use_reloader=False, host=host, port=port)
except Exception as e:
logger.error(f"Error running Dash app: {str(e)}")
@ -2253,6 +2317,166 @@ class RealTimeChart:
# Placeholder for adding NN signals if needed in the future
pass
def _update_main_chart(self, interval_seconds):
"""Update the main chart for the specified interval"""
try:
# Convert interval seconds to timeframe key
interval_key = self._get_interval_key(interval_seconds)
# Get candles for this timeframe
if interval_key not in self.tick_storage.candles or not self.tick_storage.candles[interval_key]:
logger.warning(f"No candle data available for {interval_key}")
# Return empty figure with a message
fig = go.Figure()
fig.add_annotation(
text=f"No data available for {interval_key}",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=20, color="white")
)
fig.update_layout(
template="plotly_dark",
height=600,
title=f"{self.symbol} - {interval_key} Chart",
xaxis_title="Time",
yaxis_title="Price ($)"
)
return fig
# Get candles (limit to last 500 for performance)
candles = self.tick_storage.candles[interval_key][-500:]
if not candles:
# Return empty figure if no candles
fig = go.Figure()
fig.add_annotation(
text=f"No candles available for {interval_key}",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=20, color="white")
)
fig.update_layout(
template="plotly_dark",
height=600,
title=f"{self.symbol} - {interval_key} Chart"
)
return fig
# Extract OHLC values
timestamps = [candle['timestamp'] for candle in candles]
opens = [candle['open'] for candle in candles]
highs = [candle['high'] for candle in candles]
lows = [candle['low'] for candle in candles]
closes = [candle['close'] for candle in candles]
volumes = [candle['volume'] for candle in candles]
# Create the main figure
fig = go.Figure()
# Add candlestick trace
fig.add_trace(go.Candlestick(
x=timestamps,
open=opens,
high=highs,
low=lows,
close=closes,
name=f"{self.symbol}",
increasing_line_color='rgba(0, 200, 0, 0.8)',
decreasing_line_color='rgba(255, 0, 0, 0.8)',
increasing_fillcolor='rgba(0, 200, 0, 0.3)',
decreasing_fillcolor='rgba(255, 0, 0, 0.3)'
))
# Add trade markers if we have positions
if hasattr(self, 'positions') and self.positions:
# Get recent positions (last 50 to avoid clutter)
recent_positions = self.positions[-50:] if len(self.positions) > 50 else self.positions
for position in recent_positions:
# Add entry marker
fig.add_trace(go.Scatter(
x=[position.entry_timestamp],
y=[position.entry_price],
mode='markers',
marker=dict(
symbol='triangle-up' if position.action == 'BUY' else 'triangle-down',
size=12,
color='green' if position.action == 'BUY' else 'red',
line=dict(width=2, color='white')
),
name=f"{position.action} Entry",
hovertemplate=f"<b>{position.action} Entry</b><br>" +
f"Price: ${position.entry_price:.2f}<br>" +
f"Time: {position.entry_timestamp}<br>" +
f"ID: {position.trade_id}<extra></extra>",
showlegend=False
))
# Add exit marker if position is closed
if not position.is_open and position.exit_price and position.exit_timestamp:
fig.add_trace(go.Scatter(
x=[position.exit_timestamp],
y=[position.exit_price],
mode='markers',
marker=dict(
symbol='triangle-down' if position.action == 'BUY' else 'triangle-up',
size=12,
color='blue',
line=dict(width=2, color='white')
),
name=f"{position.action} Exit",
hovertemplate=f"<b>{position.action} Exit</b><br>" +
f"Price: ${position.exit_price:.2f}<br>" +
f"Time: {position.exit_timestamp}<br>" +
f"PnL: ${position.pnl:.2f}<br>" +
f"ID: {position.trade_id}<extra></extra>",
showlegend=False
))
# Update layout
fig.update_layout(
template="plotly_dark",
height=600,
title=f"{self.symbol} - {interval_key} Chart (Live Trading)",
xaxis_title="Time",
yaxis_title="Price ($)",
showlegend=True,
margin=dict(l=0, r=0, t=40, b=0),
xaxis_rangeslider_visible=False,
hovermode='x unified'
)
# Format Y-axis with appropriate decimal places
fig.update_yaxes(tickformat=".2f")
# Format X-axis
fig.update_xaxes(
rangeslider_visible=False,
type='date'
)
return fig
except Exception as e:
logger.error(f"Error updating main chart: {str(e)}")
import traceback
logger.error(traceback.format_exc())
# Return error figure
fig = go.Figure()
fig.add_annotation(
text=f"Error loading chart: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5, showarrow=False,
font=dict(size=16, color="red")
)
fig.update_layout(
template="plotly_dark",
height=600,
title=f"{self.symbol} Chart - Error"
)
return fig
class BinanceWebSocket:
"""Binance WebSocket implementation for real-time tick data"""
def __init__(self, symbol: str):