fixed miltiframe chart
This commit is contained in:
parent
4aefca2d6c
commit
7ea193d70f
413
realtime.py
413
realtime.py
@ -31,14 +31,22 @@ class TradeTickStorage:
|
|||||||
def __init__(self, max_age_seconds: int = 300): # 5 minutes by default
|
def __init__(self, max_age_seconds: int = 300): # 5 minutes by default
|
||||||
self.ticks = []
|
self.ticks = []
|
||||||
self.max_age_seconds = max_age_seconds
|
self.max_age_seconds = max_age_seconds
|
||||||
|
self.last_cleanup_time = time.time()
|
||||||
|
self.cleanup_interval = 10 # Run cleanup every 10 seconds to avoid doing it on every tick
|
||||||
|
self.last_tick = None
|
||||||
logger.info(f"Initialized TradeTickStorage with max age: {max_age_seconds} seconds")
|
logger.info(f"Initialized TradeTickStorage with max age: {max_age_seconds} seconds")
|
||||||
|
|
||||||
def add_tick(self, tick: Dict):
|
def add_tick(self, tick: Dict):
|
||||||
"""Add a new trade tick to storage"""
|
"""Add a new trade tick to storage"""
|
||||||
self.ticks.append(tick)
|
self.ticks.append(tick)
|
||||||
|
self.last_tick = tick
|
||||||
logger.debug(f"Added tick: {tick}, total ticks: {len(self.ticks)}")
|
logger.debug(f"Added tick: {tick}, total ticks: {len(self.ticks)}")
|
||||||
# Clean up old ticks
|
|
||||||
self._cleanup()
|
# Only clean up periodically rather than on every tick
|
||||||
|
current_time = time.time()
|
||||||
|
if current_time - self.last_cleanup_time > self.cleanup_interval:
|
||||||
|
self._cleanup()
|
||||||
|
self.last_cleanup_time = current_time
|
||||||
|
|
||||||
def _cleanup(self):
|
def _cleanup(self):
|
||||||
"""Remove ticks older than max_age_seconds"""
|
"""Remove ticks older than max_age_seconds"""
|
||||||
@ -46,8 +54,42 @@ class TradeTickStorage:
|
|||||||
cutoff = now - (self.max_age_seconds * 1000)
|
cutoff = now - (self.max_age_seconds * 1000)
|
||||||
old_count = len(self.ticks)
|
old_count = len(self.ticks)
|
||||||
self.ticks = [tick for tick in self.ticks if tick['timestamp'] >= cutoff]
|
self.ticks = [tick for tick in self.ticks if tick['timestamp'] >= cutoff]
|
||||||
if old_count > len(self.ticks):
|
removed = old_count - len(self.ticks)
|
||||||
logger.debug(f"Cleaned up {old_count - len(self.ticks)} old ticks")
|
if removed > 0:
|
||||||
|
logger.debug(f"Cleaned up {removed} old ticks, remaining: {len(self.ticks)}")
|
||||||
|
|
||||||
|
def get_latest_price(self) -> Optional[float]:
|
||||||
|
"""Get the latest price from the most recent tick"""
|
||||||
|
if self.last_tick:
|
||||||
|
return self.last_tick.get('price')
|
||||||
|
elif self.ticks:
|
||||||
|
# If last_tick not available but ticks exist, use the last tick
|
||||||
|
self.last_tick = self.ticks[-1]
|
||||||
|
return self.last_tick.get('price')
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_price_stats(self) -> Dict:
|
||||||
|
"""Get stats about the prices in storage"""
|
||||||
|
if not self.ticks:
|
||||||
|
return {
|
||||||
|
'min': None,
|
||||||
|
'max': None,
|
||||||
|
'latest': None,
|
||||||
|
'count': 0,
|
||||||
|
'age_seconds': 0
|
||||||
|
}
|
||||||
|
|
||||||
|
prices = [tick['price'] for tick in self.ticks]
|
||||||
|
latest_timestamp = self.ticks[-1]['timestamp']
|
||||||
|
oldest_timestamp = self.ticks[0]['timestamp']
|
||||||
|
|
||||||
|
return {
|
||||||
|
'min': min(prices),
|
||||||
|
'max': max(prices),
|
||||||
|
'latest': prices[-1],
|
||||||
|
'count': len(prices),
|
||||||
|
'age_seconds': (latest_timestamp - oldest_timestamp) / 1000
|
||||||
|
}
|
||||||
|
|
||||||
def get_ticks_as_df(self) -> pd.DataFrame:
|
def get_ticks_as_df(self) -> pd.DataFrame:
|
||||||
"""Return ticks as a DataFrame"""
|
"""Return ticks as a DataFrame"""
|
||||||
@ -55,6 +97,9 @@ class TradeTickStorage:
|
|||||||
logger.warning("No ticks available for DataFrame conversion")
|
logger.warning("No ticks available for DataFrame conversion")
|
||||||
return pd.DataFrame()
|
return pd.DataFrame()
|
||||||
|
|
||||||
|
# Ensure we have fresh data
|
||||||
|
self._cleanup()
|
||||||
|
|
||||||
df = pd.DataFrame(self.ticks)
|
df = pd.DataFrame(self.ticks)
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
logger.debug(f"Converting timestamps for {len(df)} ticks")
|
logger.debug(f"Converting timestamps for {len(df)} ticks")
|
||||||
@ -485,41 +530,111 @@ class RealTimeChart:
|
|||||||
self.app = dash.Dash(__name__)
|
self.app = dash.Dash(__name__)
|
||||||
self.candlestick_data = CandlestickData()
|
self.candlestick_data = CandlestickData()
|
||||||
self.tick_storage = TradeTickStorage(max_age_seconds=300) # Store 5 minutes of ticks
|
self.tick_storage = TradeTickStorage(max_age_seconds=300) # Store 5 minutes of ticks
|
||||||
|
self.ohlcv_cache = { # Cache for different intervals
|
||||||
|
'1s': None,
|
||||||
|
'1m': None,
|
||||||
|
'1h': None,
|
||||||
|
'1d': None
|
||||||
|
}
|
||||||
logger.info(f"Initializing RealTimeChart for {symbol}")
|
logger.info(f"Initializing RealTimeChart for {symbol}")
|
||||||
|
|
||||||
|
# Button style
|
||||||
|
button_style = {
|
||||||
|
'background-color': '#4CAF50',
|
||||||
|
'color': 'white',
|
||||||
|
'padding': '10px 15px',
|
||||||
|
'margin': '5px',
|
||||||
|
'border': 'none',
|
||||||
|
'border-radius': '5px',
|
||||||
|
'font-size': '14px',
|
||||||
|
'cursor': 'pointer',
|
||||||
|
'transition': 'background-color 0.3s',
|
||||||
|
'font-weight': 'bold'
|
||||||
|
}
|
||||||
|
|
||||||
|
active_button_style = {
|
||||||
|
**button_style,
|
||||||
|
'background-color': '#2E7D32',
|
||||||
|
'box-shadow': '0 0 5px #2E7D32'
|
||||||
|
}
|
||||||
|
|
||||||
# Initialize the layout with improved styling
|
# Initialize the layout with improved styling
|
||||||
self.app.layout = html.Div([
|
self.app.layout = html.Div([
|
||||||
html.H1(f"{symbol} Real-Time Price", style={
|
# Header with symbol and title
|
||||||
'textAlign': 'center',
|
|
||||||
'color': '#2c3e50',
|
|
||||||
'fontFamily': 'Arial, sans-serif',
|
|
||||||
'marginTop': '20px'
|
|
||||||
}),
|
|
||||||
html.Div([
|
html.Div([
|
||||||
html.Button('1s', id='btn-1s', n_clicks=0, style={'margin': '5px'}),
|
html.H1(f"{symbol} Real-Time Price Chart", style={
|
||||||
html.Button('5s', id='btn-5s', n_clicks=0, style={'margin': '5px'}),
|
'textAlign': 'center',
|
||||||
html.Button('15s', id='btn-15s', n_clicks=0, style={'margin': '5px'}),
|
'color': '#FFFFFF',
|
||||||
html.Button('30s', id='btn-30s', n_clicks=0, style={'margin': '5px'}),
|
'fontFamily': 'Arial, sans-serif',
|
||||||
html.Button('1m', id='btn-1m', n_clicks=0, style={'margin': '5px'}),
|
'margin': '10px',
|
||||||
], style={'textAlign': 'center', 'margin': '10px'}),
|
'textShadow': '2px 2px 4px #000000'
|
||||||
dcc.Store(id='interval-store', data={'interval': 1}), # Store for current interval
|
}),
|
||||||
|
], style={
|
||||||
|
'backgroundColor': '#1E1E1E',
|
||||||
|
'padding': '10px',
|
||||||
|
'borderRadius': '5px',
|
||||||
|
'marginBottom': '10px',
|
||||||
|
'boxShadow': '0 4px 8px 0 rgba(0,0,0,0.2)'
|
||||||
|
}),
|
||||||
|
|
||||||
|
# Interval selection buttons
|
||||||
|
html.Div([
|
||||||
|
html.Div("Candlestick Interval:", style={
|
||||||
|
'color': '#FFFFFF',
|
||||||
|
'marginRight': '10px',
|
||||||
|
'fontSize': '16px',
|
||||||
|
'fontWeight': 'bold'
|
||||||
|
}),
|
||||||
|
html.Button('1s', id='btn-1s', n_clicks=0, style=active_button_style),
|
||||||
|
html.Button('5s', id='btn-5s', n_clicks=0, style=button_style),
|
||||||
|
html.Button('15s', id='btn-15s', n_clicks=0, style=button_style),
|
||||||
|
html.Button('30s', id='btn-30s', n_clicks=0, style=button_style),
|
||||||
|
html.Button('1m', id='btn-1m', n_clicks=0, style=button_style),
|
||||||
|
], style={
|
||||||
|
'display': 'flex',
|
||||||
|
'alignItems': 'center',
|
||||||
|
'justifyContent': 'center',
|
||||||
|
'margin': '15px',
|
||||||
|
'backgroundColor': '#2C2C2C',
|
||||||
|
'padding': '10px',
|
||||||
|
'borderRadius': '5px'
|
||||||
|
}),
|
||||||
|
|
||||||
|
# Store for current interval
|
||||||
|
dcc.Store(id='interval-store', data={'interval': 1}),
|
||||||
|
|
||||||
|
# Main chart
|
||||||
dcc.Graph(
|
dcc.Graph(
|
||||||
id='live-chart',
|
id='live-chart',
|
||||||
style={'height': '80vh'}
|
style={
|
||||||
|
'height': '80vh',
|
||||||
|
'border': '1px solid #444444',
|
||||||
|
'borderRadius': '5px',
|
||||||
|
'boxShadow': '0 4px 8px 0 rgba(0,0,0,0.2)'
|
||||||
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# Update interval
|
||||||
dcc.Interval(
|
dcc.Interval(
|
||||||
id='interval-component',
|
id='interval-component',
|
||||||
interval=500, # Update every 500ms for smoother display
|
interval=500, # Update every 500ms for smoother display
|
||||||
n_intervals=0
|
n_intervals=0
|
||||||
)
|
)
|
||||||
], style={
|
], style={
|
||||||
'backgroundColor': '#f8f9fa',
|
'backgroundColor': '#121212',
|
||||||
'padding': '20px'
|
'padding': '20px',
|
||||||
|
'height': '100vh',
|
||||||
|
'fontFamily': 'Arial, sans-serif'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Callback to update interval based on button clicks
|
# Callback to update interval based on button clicks and update button styles
|
||||||
@self.app.callback(
|
@self.app.callback(
|
||||||
Output('interval-store', 'data'),
|
[Output('interval-store', 'data'),
|
||||||
|
Output('btn-1s', 'style'),
|
||||||
|
Output('btn-5s', 'style'),
|
||||||
|
Output('btn-15s', 'style'),
|
||||||
|
Output('btn-30s', 'style'),
|
||||||
|
Output('btn-1m', 'style')],
|
||||||
[Input('btn-1s', 'n_clicks'),
|
[Input('btn-1s', 'n_clicks'),
|
||||||
Input('btn-5s', 'n_clicks'),
|
Input('btn-5s', 'n_clicks'),
|
||||||
Input('btn-15s', 'n_clicks'),
|
Input('btn-15s', 'n_clicks'),
|
||||||
@ -530,22 +645,45 @@ class RealTimeChart:
|
|||||||
def update_interval(n1, n5, n15, n30, n60, data):
|
def update_interval(n1, n5, n15, n30, n60, data):
|
||||||
ctx = dash.callback_context
|
ctx = dash.callback_context
|
||||||
if not ctx.triggered:
|
if not ctx.triggered:
|
||||||
return data
|
# Default state (1s selected)
|
||||||
|
return ({'interval': 1},
|
||||||
|
active_button_style, button_style, button_style, button_style, button_style)
|
||||||
|
|
||||||
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
button_id = ctx.triggered[0]['prop_id'].split('.')[0]
|
||||||
|
|
||||||
if button_id == 'btn-1s':
|
if button_id == 'btn-1s':
|
||||||
return {'interval': 1}
|
return ({'interval': 1},
|
||||||
|
active_button_style, button_style, button_style, button_style, button_style)
|
||||||
elif button_id == 'btn-5s':
|
elif button_id == 'btn-5s':
|
||||||
return {'interval': 5}
|
return ({'interval': 5},
|
||||||
|
button_style, active_button_style, button_style, button_style, button_style)
|
||||||
elif button_id == 'btn-15s':
|
elif button_id == 'btn-15s':
|
||||||
return {'interval': 15}
|
return ({'interval': 15},
|
||||||
|
button_style, button_style, active_button_style, button_style, button_style)
|
||||||
elif button_id == 'btn-30s':
|
elif button_id == 'btn-30s':
|
||||||
return {'interval': 30}
|
return ({'interval': 30},
|
||||||
|
button_style, button_style, button_style, active_button_style, button_style)
|
||||||
elif button_id == 'btn-1m':
|
elif button_id == 'btn-1m':
|
||||||
return {'interval': 60}
|
return ({'interval': 60},
|
||||||
|
button_style, button_style, button_style, button_style, active_button_style)
|
||||||
|
|
||||||
return data
|
# Default case - keep current interval and highlight appropriate button
|
||||||
|
current_interval = data.get('interval', 1)
|
||||||
|
styles = [button_style] * 5 # All inactive by default
|
||||||
|
|
||||||
|
# Set active style based on current interval
|
||||||
|
if current_interval == 1:
|
||||||
|
styles[0] = active_button_style
|
||||||
|
elif current_interval == 5:
|
||||||
|
styles[1] = active_button_style
|
||||||
|
elif current_interval == 15:
|
||||||
|
styles[2] = active_button_style
|
||||||
|
elif current_interval == 30:
|
||||||
|
styles[3] = active_button_style
|
||||||
|
elif current_interval == 60:
|
||||||
|
styles[4] = active_button_style
|
||||||
|
|
||||||
|
return (data, *styles)
|
||||||
|
|
||||||
# Callback to update the chart
|
# Callback to update the chart
|
||||||
@self.app.callback(
|
@self.app.callback(
|
||||||
@ -556,26 +694,26 @@ class RealTimeChart:
|
|||||||
def update_chart(n, interval_data):
|
def update_chart(n, interval_data):
|
||||||
try:
|
try:
|
||||||
interval = interval_data.get('interval', 1)
|
interval = interval_data.get('interval', 1)
|
||||||
logger.info(f"Updating chart for {self.symbol} with interval {interval}s")
|
logger.debug(f"Updating chart for {self.symbol} with interval {interval}s")
|
||||||
|
|
||||||
fig = make_subplots(
|
|
||||||
rows=2, cols=1,
|
|
||||||
shared_xaxis=True,
|
|
||||||
vertical_spacing=0.03,
|
|
||||||
subplot_titles=(f'{self.symbol} Price ({interval}s)', 'Volume'),
|
|
||||||
row_heights=[0.7, 0.3]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get candlesticks from tick storage
|
# Get candlesticks from tick storage
|
||||||
df = self.tick_storage.get_candles(interval_seconds=interval)
|
df = self.tick_storage.get_candles(interval_seconds=interval)
|
||||||
|
|
||||||
# Debug information about the dataframe
|
# Get current price and stats using our enhanced methods
|
||||||
logger.info(f"Candles dataframe empty: {df.empty}, tick count: {len(self.tick_storage.ticks)}")
|
current_price = self.tick_storage.get_latest_price()
|
||||||
|
price_stats = self.tick_storage.get_price_stats()
|
||||||
|
|
||||||
|
logger.debug(f"Current price: {current_price}, Stats: {price_stats}")
|
||||||
|
|
||||||
|
fig = make_subplots(
|
||||||
|
rows=6, cols=1, # Adjusted to accommodate new subcharts
|
||||||
|
vertical_spacing=0.03,
|
||||||
|
subplot_titles=(f'{self.symbol} Price ({interval}s)', 'Volume', '1s OHLCV', '1m OHLCV', '1h OHLCV', '1d OHLCV'),
|
||||||
|
row_heights=[0.4, 0.1, 0.1, 0.1, 0.1, 0.1] # Adjusted heights
|
||||||
|
)
|
||||||
|
|
||||||
if not df.empty and len(df) > 0:
|
if not df.empty and len(df) > 0:
|
||||||
logger.info(f"Candles dataframe shape: {df.shape}")
|
logger.debug(f"Candles dataframe shape: {df.shape}, columns: {df.columns.tolist()}")
|
||||||
logger.info(f"Candles dataframe columns: {df.columns.tolist()}")
|
|
||||||
logger.info(f"Candles dataframe first row: {df.iloc[0].to_dict() if len(df) > 0 else 'No rows'}")
|
|
||||||
|
|
||||||
# Add candlestick chart
|
# Add candlestick chart
|
||||||
fig.add_trace(
|
fig.add_trace(
|
||||||
@ -606,7 +744,7 @@ class RealTimeChart:
|
|||||||
row=2, col=1
|
row=2, col=1
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add latest price line and annotation
|
# Add latest price line from the candlestick data
|
||||||
latest_price = df['close'].iloc[-1]
|
latest_price = df['close'].iloc[-1]
|
||||||
fig.add_shape(
|
fig.add_shape(
|
||||||
type="line",
|
type="line",
|
||||||
@ -618,6 +756,7 @@ class RealTimeChart:
|
|||||||
row=1, col=1
|
row=1, col=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Annotation for last candle close price
|
||||||
fig.add_annotation(
|
fig.add_annotation(
|
||||||
x=df['timestamp'].max(),
|
x=df['timestamp'].max(),
|
||||||
y=latest_price,
|
y=latest_price,
|
||||||
@ -627,11 +766,56 @@ class RealTimeChart:
|
|||||||
xshift=50,
|
xshift=50,
|
||||||
row=1, col=1
|
row=1, col=1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If we have a more recent price from ticks, add that too
|
||||||
|
if current_price and abs(current_price - latest_price) > 0.01:
|
||||||
|
# Add current price line
|
||||||
|
fig.add_shape(
|
||||||
|
type="line",
|
||||||
|
x0=df['timestamp'].min(),
|
||||||
|
y0=current_price,
|
||||||
|
x1=df['timestamp'].max(),
|
||||||
|
y1=current_price,
|
||||||
|
line=dict(color="cyan", width=1, dash="dot"),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add current price annotation
|
||||||
|
fig.add_annotation(
|
||||||
|
x=df['timestamp'].max(),
|
||||||
|
y=current_price,
|
||||||
|
text=f"Current: {current_price:.2f}",
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(size=14, color="cyan"),
|
||||||
|
xshift=50,
|
||||||
|
yshift=20,
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch and cache OHLCV data for different intervals
|
||||||
|
for interval_key in self.ohlcv_cache.keys():
|
||||||
|
if self.ohlcv_cache[interval_key] is None:
|
||||||
|
self.ohlcv_cache[interval_key] = self.tick_storage.get_candles(interval_seconds=self._interval_to_seconds(interval_key))
|
||||||
|
|
||||||
|
# Add OHLCV subcharts
|
||||||
|
for i, (interval_key, ohlcv_df) in enumerate(self.ohlcv_cache.items(), start=3):
|
||||||
|
if ohlcv_df is not None and not ohlcv_df.empty:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Candlestick(
|
||||||
|
x=ohlcv_df['timestamp'],
|
||||||
|
open=ohlcv_df['open'],
|
||||||
|
high=ohlcv_df['high'],
|
||||||
|
low=ohlcv_df['low'],
|
||||||
|
close=ohlcv_df['close'],
|
||||||
|
name=f'{interval_key} OHLCV',
|
||||||
|
increasing_line_color='#33CC33',
|
||||||
|
decreasing_line_color='#FF4136'
|
||||||
|
),
|
||||||
|
row=i, col=1
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# If no data, add a text annotation to the chart
|
# If no data, add a text annotation to the chart
|
||||||
logger.warning(f"No data to display for {self.symbol} - tick count: {len(self.tick_storage.ticks)}")
|
logger.warning(f"No data to display for {self.symbol} - tick count: {len(self.tick_storage.ticks)}")
|
||||||
if self.tick_storage.ticks:
|
|
||||||
logger.info(f"Sample tick: {self.tick_storage.ticks[0]}")
|
|
||||||
|
|
||||||
# Add a message to the empty chart
|
# Add a message to the empty chart
|
||||||
fig.add_annotation(
|
fig.add_annotation(
|
||||||
@ -642,16 +826,70 @@ class RealTimeChart:
|
|||||||
xref="paper", yref="paper"
|
xref="paper", yref="paper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Build info box text with all the statistics
|
||||||
|
info_lines = [f"<b>{self.symbol}</b>"]
|
||||||
|
|
||||||
|
# Add current price if available
|
||||||
|
if current_price:
|
||||||
|
info_lines.append(f"Current: <b>{current_price:.2f}</b> USDT")
|
||||||
|
|
||||||
|
# Add price statistics if available
|
||||||
|
if price_stats['count'] > 0:
|
||||||
|
# Format time range
|
||||||
|
age_text = f"{price_stats['age_seconds']:.1f}s"
|
||||||
|
if price_stats['age_seconds'] > 60:
|
||||||
|
minutes = int(price_stats['age_seconds'] / 60)
|
||||||
|
seconds = int(price_stats['age_seconds'] % 60)
|
||||||
|
age_text = f"{minutes}m {seconds}s"
|
||||||
|
|
||||||
|
# Add price range and change
|
||||||
|
if price_stats['min'] is not None and price_stats['max'] is not None:
|
||||||
|
price_range = f"Range: {price_stats['min']:.2f} - {price_stats['max']:.2f}"
|
||||||
|
info_lines.append(price_range)
|
||||||
|
|
||||||
|
# Add tick count and time range
|
||||||
|
info_lines.append(f"Ticks: {price_stats['count']} in {age_text}")
|
||||||
|
|
||||||
|
# Add candle count
|
||||||
|
candle_count = len(df) if not df.empty else 0
|
||||||
|
info_lines.append(f"Candles: {candle_count} ({interval}s)")
|
||||||
|
|
||||||
|
# Add info box to the chart
|
||||||
|
fig.add_annotation(
|
||||||
|
x=0.01,
|
||||||
|
y=0.99,
|
||||||
|
xref="paper",
|
||||||
|
yref="paper",
|
||||||
|
text="<br>".join(info_lines),
|
||||||
|
showarrow=False,
|
||||||
|
font=dict(size=12, color="white"),
|
||||||
|
align="left",
|
||||||
|
bgcolor="rgba(0,0,50,0.7)",
|
||||||
|
bordercolor="#3366CC",
|
||||||
|
borderwidth=2,
|
||||||
|
borderpad=5,
|
||||||
|
xanchor="left",
|
||||||
|
yanchor="top"
|
||||||
|
)
|
||||||
|
|
||||||
# Update layout with improved styling
|
# Update layout with improved styling
|
||||||
|
interval_text = {
|
||||||
|
1: "1 second",
|
||||||
|
5: "5 seconds",
|
||||||
|
15: "15 seconds",
|
||||||
|
30: "30 seconds",
|
||||||
|
60: "1 minute"
|
||||||
|
}.get(interval, f"{interval}s")
|
||||||
|
|
||||||
fig.update_layout(
|
fig.update_layout(
|
||||||
title_text=f"{self.symbol} Real-Time Data ({interval}s candles)",
|
title_text=f"{self.symbol} Real-Time Data ({interval_text})",
|
||||||
title_x=0.5, # Center the title
|
title_x=0.5, # Center the title
|
||||||
xaxis_rangeslider_visible=False,
|
xaxis_rangeslider_visible=False,
|
||||||
height=800,
|
height=1200, # Adjusted height for new subcharts
|
||||||
template='plotly_dark',
|
template='plotly_dark',
|
||||||
paper_bgcolor='rgba(0,0,0,0)',
|
paper_bgcolor='rgba(0,0,0,0)',
|
||||||
plot_bgcolor='rgba(0,0,0,0)',
|
plot_bgcolor='rgba(25,25,50,1)',
|
||||||
font=dict(family="Arial, sans-serif", size=12, color="#2c3e50"),
|
font=dict(family="Arial, sans-serif", size=12, color="white"),
|
||||||
showlegend=True,
|
showlegend=True,
|
||||||
legend=dict(
|
legend=dict(
|
||||||
yanchor="top",
|
yanchor="top",
|
||||||
@ -685,27 +923,60 @@ class RealTimeChart:
|
|||||||
height=800,
|
height=800,
|
||||||
template='plotly_dark',
|
template='plotly_dark',
|
||||||
paper_bgcolor='rgba(0,0,0,0)',
|
paper_bgcolor='rgba(0,0,0,0)',
|
||||||
plot_bgcolor='rgba(0,0,0,0)'
|
plot_bgcolor='rgba(25,25,50,1)'
|
||||||
)
|
)
|
||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
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):
|
async def start_websocket(self):
|
||||||
ws = ExchangeWebSocket(self.symbol)
|
ws = ExchangeWebSocket(self.symbol)
|
||||||
|
connection_attempts = 0
|
||||||
|
max_attempts = 10 # Maximum connection attempts before longer waiting period
|
||||||
|
|
||||||
while True: # Keep trying to maintain connection
|
while True: # Keep trying to maintain connection
|
||||||
|
connection_attempts += 1
|
||||||
if not await ws.connect():
|
if not await ws.connect():
|
||||||
logger.error(f"Failed to connect to exchange for {self.symbol}")
|
logger.error(f"Failed to connect to exchange for {self.symbol}")
|
||||||
await asyncio.sleep(5)
|
# 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
|
continue
|
||||||
|
|
||||||
|
# Successfully connected
|
||||||
|
connection_attempts = 0
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"WebSocket connected for {self.symbol}, beginning data collection")
|
logger.info(f"WebSocket connected for {self.symbol}, beginning data collection")
|
||||||
tick_count = 0
|
tick_count = 0
|
||||||
last_tick_count_log = time.time()
|
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:
|
while True:
|
||||||
if not ws.running:
|
if not ws.running:
|
||||||
logger.warning("WebSocket not running, breaking loop")
|
logger.warning(f"WebSocket connection lost for {self.symbol}, breaking loop")
|
||||||
break
|
break
|
||||||
|
|
||||||
data = await ws.receive()
|
data = await ws.receive()
|
||||||
@ -729,6 +1000,14 @@ class RealTimeChart:
|
|||||||
'volume': data['volume']
|
'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
|
# Store raw tick in the tick storage
|
||||||
self.tick_storage.add_tick(trade_data)
|
self.tick_storage.add_tick(trade_data)
|
||||||
tick_count += 1
|
tick_count += 1
|
||||||
@ -739,7 +1018,9 @@ class RealTimeChart:
|
|||||||
# Log tick counts periodically
|
# Log tick counts periodically
|
||||||
current_time = time.time()
|
current_time = time.time()
|
||||||
if current_time - last_tick_count_log >= 10: # Log every 10 seconds
|
if current_time - last_tick_count_log >= 10: # Log every 10 seconds
|
||||||
logger.info(f"{self.symbol}: Collected {tick_count} ticks in last {current_time - last_tick_count_log:.1f}s, total: {len(self.tick_storage.ticks)}")
|
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
|
last_tick_count_log = current_time
|
||||||
tick_count = 0
|
tick_count = 0
|
||||||
|
|
||||||
@ -747,14 +1028,34 @@ class RealTimeChart:
|
|||||||
if len(self.tick_storage.ticks) > 0:
|
if len(self.tick_storage.ticks) > 0:
|
||||||
sample_df = self.tick_storage.get_candles(interval_seconds=1)
|
sample_df = self.tick_storage.get_candles(interval_seconds=1)
|
||||||
logger.info(f"{self.symbol}: Sample candle count: {len(sample_df)}")
|
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)
|
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:
|
except Exception as e:
|
||||||
logger.error(f"Error in WebSocket loop: {str(e)}")
|
logger.error(f"Error in WebSocket loop for {self.symbol}: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
logger.error(traceback.format_exc())
|
||||||
finally:
|
finally:
|
||||||
|
logger.info(f"Closing WebSocket connection for {self.symbol}")
|
||||||
await ws.close()
|
await ws.close()
|
||||||
|
|
||||||
logger.info("Waiting 5 seconds before reconnecting...")
|
logger.info(f"Waiting 5 seconds before reconnecting {self.symbol} WebSocket...")
|
||||||
await asyncio.sleep(5)
|
await asyncio.sleep(5)
|
||||||
|
|
||||||
def run(self, host='localhost', port=8050):
|
def run(self, host='localhost', port=8050):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user