fixed miltiframe chart
This commit is contained in:
parent
4aefca2d6c
commit
7ea193d70f
411
realtime.py
411
realtime.py
@ -31,14 +31,22 @@ class TradeTickStorage:
|
||||
def __init__(self, max_age_seconds: int = 300): # 5 minutes by default
|
||||
self.ticks = []
|
||||
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")
|
||||
|
||||
def add_tick(self, tick: Dict):
|
||||
"""Add a new trade tick to storage"""
|
||||
self.ticks.append(tick)
|
||||
self.last_tick = tick
|
||||
logger.debug(f"Added tick: {tick}, total ticks: {len(self.ticks)}")
|
||||
# Clean up old ticks
|
||||
|
||||
# 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):
|
||||
"""Remove ticks older than max_age_seconds"""
|
||||
@ -46,8 +54,42 @@ class TradeTickStorage:
|
||||
cutoff = now - (self.max_age_seconds * 1000)
|
||||
old_count = len(self.ticks)
|
||||
self.ticks = [tick for tick in self.ticks if tick['timestamp'] >= cutoff]
|
||||
if old_count > len(self.ticks):
|
||||
logger.debug(f"Cleaned up {old_count - len(self.ticks)} old ticks")
|
||||
removed = old_count - len(self.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:
|
||||
"""Return ticks as a DataFrame"""
|
||||
@ -55,6 +97,9 @@ class TradeTickStorage:
|
||||
logger.warning("No ticks available for DataFrame conversion")
|
||||
return pd.DataFrame()
|
||||
|
||||
# Ensure we have fresh data
|
||||
self._cleanup()
|
||||
|
||||
df = pd.DataFrame(self.ticks)
|
||||
if not df.empty:
|
||||
logger.debug(f"Converting timestamps for {len(df)} ticks")
|
||||
@ -485,41 +530,111 @@ class RealTimeChart:
|
||||
self.app = dash.Dash(__name__)
|
||||
self.candlestick_data = CandlestickData()
|
||||
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}")
|
||||
|
||||
# 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
|
||||
self.app.layout = html.Div([
|
||||
html.H1(f"{symbol} Real-Time Price", style={
|
||||
'textAlign': 'center',
|
||||
'color': '#2c3e50',
|
||||
'fontFamily': 'Arial, sans-serif',
|
||||
'marginTop': '20px'
|
||||
}),
|
||||
# Header with symbol and title
|
||||
html.Div([
|
||||
html.Button('1s', id='btn-1s', n_clicks=0, style={'margin': '5px'}),
|
||||
html.Button('5s', id='btn-5s', n_clicks=0, style={'margin': '5px'}),
|
||||
html.Button('15s', id='btn-15s', n_clicks=0, style={'margin': '5px'}),
|
||||
html.Button('30s', id='btn-30s', n_clicks=0, style={'margin': '5px'}),
|
||||
html.Button('1m', id='btn-1m', n_clicks=0, style={'margin': '5px'}),
|
||||
], style={'textAlign': 'center', 'margin': '10px'}),
|
||||
dcc.Store(id='interval-store', data={'interval': 1}), # Store for current interval
|
||||
html.H1(f"{symbol} Real-Time Price Chart", style={
|
||||
'textAlign': 'center',
|
||||
'color': '#FFFFFF',
|
||||
'fontFamily': 'Arial, sans-serif',
|
||||
'margin': '10px',
|
||||
'textShadow': '2px 2px 4px #000000'
|
||||
}),
|
||||
], 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(
|
||||
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(
|
||||
id='interval-component',
|
||||
interval=500, # Update every 500ms for smoother display
|
||||
n_intervals=0
|
||||
)
|
||||
], style={
|
||||
'backgroundColor': '#f8f9fa',
|
||||
'padding': '20px'
|
||||
'backgroundColor': '#121212',
|
||||
'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(
|
||||
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-5s', 'n_clicks'),
|
||||
Input('btn-15s', 'n_clicks'),
|
||||
@ -530,22 +645,45 @@ class RealTimeChart:
|
||||
def update_interval(n1, n5, n15, n30, n60, data):
|
||||
ctx = dash.callback_context
|
||||
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]
|
||||
|
||||
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':
|
||||
return {'interval': 5}
|
||||
return ({'interval': 5},
|
||||
button_style, active_button_style, button_style, button_style, button_style)
|
||||
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':
|
||||
return {'interval': 30}
|
||||
return ({'interval': 30},
|
||||
button_style, button_style, button_style, active_button_style, button_style)
|
||||
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
|
||||
@self.app.callback(
|
||||
@ -556,26 +694,26 @@ class RealTimeChart:
|
||||
def update_chart(n, interval_data):
|
||||
try:
|
||||
interval = interval_data.get('interval', 1)
|
||||
logger.info(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]
|
||||
)
|
||||
logger.debug(f"Updating chart for {self.symbol} with interval {interval}s")
|
||||
|
||||
# Get candlesticks from tick storage
|
||||
df = self.tick_storage.get_candles(interval_seconds=interval)
|
||||
|
||||
# Debug information about the dataframe
|
||||
logger.info(f"Candles dataframe empty: {df.empty}, tick count: {len(self.tick_storage.ticks)}")
|
||||
# Get current price and stats using our enhanced methods
|
||||
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:
|
||||
logger.info(f"Candles dataframe shape: {df.shape}")
|
||||
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'}")
|
||||
logger.debug(f"Candles dataframe shape: {df.shape}, columns: {df.columns.tolist()}")
|
||||
|
||||
# Add candlestick chart
|
||||
fig.add_trace(
|
||||
@ -606,7 +744,7 @@ class RealTimeChart:
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
# Add latest price line and annotation
|
||||
# Add latest price line from the candlestick data
|
||||
latest_price = df['close'].iloc[-1]
|
||||
fig.add_shape(
|
||||
type="line",
|
||||
@ -618,6 +756,7 @@ class RealTimeChart:
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
# Annotation for last candle close price
|
||||
fig.add_annotation(
|
||||
x=df['timestamp'].max(),
|
||||
y=latest_price,
|
||||
@ -627,11 +766,56 @@ class RealTimeChart:
|
||||
xshift=50,
|
||||
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:
|
||||
# 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)}")
|
||||
if self.tick_storage.ticks:
|
||||
logger.info(f"Sample tick: {self.tick_storage.ticks[0]}")
|
||||
|
||||
# Add a message to the empty chart
|
||||
fig.add_annotation(
|
||||
@ -642,16 +826,70 @@ class RealTimeChart:
|
||||
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
|
||||
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(
|
||||
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
|
||||
xaxis_rangeslider_visible=False,
|
||||
height=800,
|
||||
height=1200, # Adjusted height for new subcharts
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(0,0,0,0)',
|
||||
font=dict(family="Arial, sans-serif", size=12, color="#2c3e50"),
|
||||
plot_bgcolor='rgba(25,25,50,1)',
|
||||
font=dict(family="Arial, sans-serif", size=12, color="white"),
|
||||
showlegend=True,
|
||||
legend=dict(
|
||||
yanchor="top",
|
||||
@ -685,27 +923,60 @@ class RealTimeChart:
|
||||
height=800,
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(0,0,0,0)'
|
||||
plot_bgcolor='rgba(25,25,50,1)'
|
||||
)
|
||||
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):
|
||||
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}")
|
||||
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
|
||||
|
||||
# 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("WebSocket not running, breaking loop")
|
||||
logger.warning(f"WebSocket connection lost for {self.symbol}, breaking loop")
|
||||
break
|
||||
|
||||
data = await ws.receive()
|
||||
@ -729,6 +1000,14 @@ class RealTimeChart:
|
||||
'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
|
||||
@ -739,7 +1018,9 @@ class RealTimeChart:
|
||||
# Log tick counts periodically
|
||||
current_time = time.time()
|
||||
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
|
||||
tick_count = 0
|
||||
|
||||
@ -748,13 +1029,33 @@ class RealTimeChart:
|
||||
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: {str(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("Waiting 5 seconds before reconnecting...")
|
||||
logger.info(f"Waiting 5 seconds before reconnecting {self.symbol} WebSocket...")
|
||||
await asyncio.sleep(5)
|
||||
|
||||
def run(self, host='localhost', port=8050):
|
||||
|
Loading…
x
Reference in New Issue
Block a user