multiple pages - candle chart + raw ticks
This commit is contained in:
parent
c4a803876e
commit
158f9ebf71
647
realtime.py
647
realtime.py
@ -31,13 +31,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class TradeTickStorage:
|
||||
"""Store and manage raw trade ticks for display and candle formation"""
|
||||
def __init__(self, max_age_seconds: int = 300): # 5 minutes by default
|
||||
def __init__(self, max_age_seconds: int = 1800): # 30 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
|
||||
# Adjust cleanup interval based on max_age_seconds (more time = less frequent cleanup)
|
||||
self.cleanup_interval = min(max(10, max_age_seconds // 60), 60) # Between 10-60 seconds
|
||||
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, cleanup interval: {self.cleanup_interval} seconds")
|
||||
|
||||
def add_tick(self, tick: Dict):
|
||||
"""Add a new trade tick to storage"""
|
||||
@ -128,8 +129,17 @@ class TradeTickStorage:
|
||||
return pd.DataFrame()
|
||||
return df
|
||||
|
||||
def get_candles(self, interval_seconds: int = 1) -> pd.DataFrame:
|
||||
"""Convert ticks to OHLCV candles at specified interval"""
|
||||
def get_candles(self, interval_seconds: int = 1, start_time_ms: int = None, end_time_ms: int = None) -> pd.DataFrame:
|
||||
"""Convert ticks to OHLCV candles at specified interval with optional time range filtering
|
||||
|
||||
Args:
|
||||
interval_seconds: The interval in seconds for each candle
|
||||
start_time_ms: Optional start time in milliseconds for filtering
|
||||
end_time_ms: Optional end time in milliseconds for filtering
|
||||
|
||||
Returns:
|
||||
DataFrame with OHLCV candles
|
||||
"""
|
||||
if not self.ticks:
|
||||
logger.warning("No ticks available for candle formation")
|
||||
return pd.DataFrame()
|
||||
@ -137,10 +147,20 @@ class TradeTickStorage:
|
||||
# Ensure ticks are up to date
|
||||
self._cleanup()
|
||||
|
||||
# Convert to DataFrame
|
||||
df = self.get_ticks_as_df()
|
||||
# Get ticks from specified time range if provided
|
||||
if start_time_ms is not None or end_time_ms is not None:
|
||||
logger.debug(f"Filtering ticks for time range from {start_time_ms} to {end_time_ms}")
|
||||
filtered_ticks = self.get_ticks_from_time(start_time_ms, end_time_ms)
|
||||
if not filtered_ticks:
|
||||
logger.warning("No ticks in the specified time range")
|
||||
return pd.DataFrame()
|
||||
df = pd.DataFrame(filtered_ticks)
|
||||
else:
|
||||
# Use all available ticks
|
||||
df = self.get_ticks_as_df()
|
||||
|
||||
if df.empty:
|
||||
logger.warning("Tick DataFrame is empty after conversion")
|
||||
logger.warning("Tick DataFrame is empty after filtering/conversion")
|
||||
return pd.DataFrame()
|
||||
|
||||
logger.info(f"Preparing to create candles from {len(df)} ticks")
|
||||
@ -178,7 +198,7 @@ class TradeTickStorage:
|
||||
# Ensure no NaN values
|
||||
candles = candles.dropna()
|
||||
|
||||
logger.debug(f"Generated {len(candles)} candles from {len(self.ticks)} ticks")
|
||||
logger.debug(f"Generated {len(candles)} candles from {len(df)} ticks")
|
||||
return candles
|
||||
|
||||
except Exception as e:
|
||||
@ -187,6 +207,88 @@ class TradeTickStorage:
|
||||
logger.error(traceback.format_exc())
|
||||
return pd.DataFrame()
|
||||
|
||||
def get_ticks_from_time(self, start_time_ms: int = None, end_time_ms: int = None) -> List[Dict]:
|
||||
"""Get ticks within a specific time range
|
||||
|
||||
Args:
|
||||
start_time_ms: Start time in milliseconds (None for no lower bound)
|
||||
end_time_ms: End time in milliseconds (None for no upper bound)
|
||||
|
||||
Returns:
|
||||
List of ticks within the time range
|
||||
"""
|
||||
if not self.ticks:
|
||||
return []
|
||||
|
||||
# Ensure ticks are updated
|
||||
self._cleanup()
|
||||
|
||||
# Apply time filters if specified
|
||||
filtered_ticks = self.ticks
|
||||
if start_time_ms is not None:
|
||||
filtered_ticks = [tick for tick in filtered_ticks if tick['timestamp'] >= start_time_ms]
|
||||
if end_time_ms is not None:
|
||||
filtered_ticks = [tick for tick in filtered_ticks if tick['timestamp'] <= end_time_ms]
|
||||
|
||||
logger.debug(f"Retrieved {len(filtered_ticks)} ticks from time range {start_time_ms} to {end_time_ms}")
|
||||
return filtered_ticks
|
||||
|
||||
def get_time_based_stats(self) -> Dict:
|
||||
"""Get statistics about the ticks organized by time periods
|
||||
|
||||
Returns:
|
||||
Dictionary with statistics for different time periods
|
||||
"""
|
||||
if not self.ticks:
|
||||
return {
|
||||
'total_ticks': 0,
|
||||
'periods': {}
|
||||
}
|
||||
|
||||
# Ensure ticks are updated
|
||||
self._cleanup()
|
||||
|
||||
now = int(time.time() * 1000) # Current time in ms
|
||||
|
||||
# Define time periods to analyze
|
||||
periods = {
|
||||
'1min': now - (60 * 1000),
|
||||
'5min': now - (5 * 60 * 1000),
|
||||
'15min': now - (15 * 60 * 1000),
|
||||
'30min': now - (30 * 60 * 1000)
|
||||
}
|
||||
|
||||
stats = {
|
||||
'total_ticks': len(self.ticks),
|
||||
'oldest_tick': self.ticks[0]['timestamp'] if self.ticks else None,
|
||||
'newest_tick': self.ticks[-1]['timestamp'] if self.ticks else None,
|
||||
'time_span_seconds': (self.ticks[-1]['timestamp'] - self.ticks[0]['timestamp']) / 1000 if self.ticks else 0,
|
||||
'periods': {}
|
||||
}
|
||||
|
||||
# Calculate stats for each period
|
||||
for period_name, cutoff_time in periods.items():
|
||||
period_ticks = [tick for tick in self.ticks if tick['timestamp'] >= cutoff_time]
|
||||
|
||||
if period_ticks:
|
||||
prices = [tick['price'] for tick in period_ticks]
|
||||
volumes = [tick.get('volume', 0) for tick in period_ticks]
|
||||
|
||||
period_stats = {
|
||||
'tick_count': len(period_ticks),
|
||||
'min_price': min(prices) if prices else None,
|
||||
'max_price': max(prices) if prices else None,
|
||||
'avg_price': sum(prices) / len(prices) if prices else None,
|
||||
'last_price': period_ticks[-1]['price'] if period_ticks else None,
|
||||
'total_volume': sum(volumes),
|
||||
'ticks_per_second': len(period_ticks) / (int(period_name[:-3]) * 60) if period_ticks else 0
|
||||
}
|
||||
|
||||
stats['periods'][period_name] = period_stats
|
||||
|
||||
logger.debug(f"Generated time-based stats: {len(stats['periods'])} periods")
|
||||
return stats
|
||||
|
||||
class CandlestickData:
|
||||
def __init__(self, max_length: int = 300):
|
||||
self.timestamps = deque(maxlen=max_length)
|
||||
@ -592,9 +694,12 @@ class CandleCache:
|
||||
class RealTimeChart:
|
||||
def __init__(self, symbol: str):
|
||||
self.symbol = symbol
|
||||
self.app = dash.Dash(__name__)
|
||||
# Create a multi-page Dash app instead of a simple Dash app
|
||||
self.app = dash.Dash(__name__,
|
||||
suppress_callback_exceptions=True,
|
||||
meta_tags=[{"name": "viewport", "content": "width=device-width, initial-scale=1"}])
|
||||
self.candlestick_data = CandlestickData()
|
||||
self.tick_storage = TradeTickStorage(max_age_seconds=300) # Store 5 minutes of ticks
|
||||
self.tick_storage = TradeTickStorage(max_age_seconds=1800) # Store 30 minutes of ticks
|
||||
self.ohlcv_cache = { # Cache for different intervals
|
||||
'1s': None,
|
||||
'1m': None,
|
||||
@ -608,6 +713,10 @@ class RealTimeChart:
|
||||
# Load historical data for longer timeframes at startup
|
||||
self._load_historical_data()
|
||||
|
||||
# Setup the multi-page layout
|
||||
self._setup_app_layout()
|
||||
|
||||
def _setup_app_layout(self):
|
||||
# Button style
|
||||
button_style = {
|
||||
'background-color': '#4CAF50',
|
||||
@ -628,17 +737,50 @@ class RealTimeChart:
|
||||
'box-shadow': '0 0 5px #2E7D32'
|
||||
}
|
||||
|
||||
# Initialize the layout with improved styling
|
||||
nav_button_style = {
|
||||
'background-color': '#3f51b5',
|
||||
'color': 'white',
|
||||
'padding': '10px 15px',
|
||||
'margin': '5px',
|
||||
'border': 'none',
|
||||
'border-radius': '5px',
|
||||
'font-size': '14px',
|
||||
'cursor': 'pointer',
|
||||
'font-weight': 'bold'
|
||||
}
|
||||
|
||||
# Content div for the current page
|
||||
content_div = html.Div(id='page-content')
|
||||
|
||||
# Initialize the layout with navigation
|
||||
self.app.layout = html.Div([
|
||||
# Header with symbol and title
|
||||
html.Div([
|
||||
html.H1(f"{symbol} Real-Time Price Chart", style={
|
||||
html.H1(f"{self.symbol} Real-Time Data", style={
|
||||
'textAlign': 'center',
|
||||
'color': '#FFFFFF',
|
||||
'fontFamily': 'Arial, sans-serif',
|
||||
'margin': '10px',
|
||||
'textShadow': '2px 2px 4px #000000'
|
||||
}),
|
||||
|
||||
# Navigation bar
|
||||
html.Div([
|
||||
dcc.Link(
|
||||
html.Button('Price Chart', style=nav_button_style),
|
||||
href=f'/{self.symbol.replace("/", "-")}/chart',
|
||||
id='chart-link'
|
||||
),
|
||||
dcc.Link(
|
||||
html.Button('Raw Ticks', style=nav_button_style),
|
||||
href=f'/{self.symbol.replace("/", "-")}/ticks',
|
||||
id='ticks-link'
|
||||
),
|
||||
], style={
|
||||
'display': 'flex',
|
||||
'justifyContent': 'center',
|
||||
'margin': '10px'
|
||||
}),
|
||||
], style={
|
||||
'backgroundColor': '#1E1E1E',
|
||||
'padding': '10px',
|
||||
@ -647,6 +789,57 @@ class RealTimeChart:
|
||||
'boxShadow': '0 4px 8px 0 rgba(0,0,0,0.2)'
|
||||
}),
|
||||
|
||||
# URL Bar
|
||||
dcc.Location(id='url', refresh=False),
|
||||
|
||||
# Content div will be populated based on the URL
|
||||
content_div,
|
||||
|
||||
# Interval component for periodic updates
|
||||
dcc.Interval(
|
||||
id='interval-component',
|
||||
interval=500, # Update every 500ms for smoother display
|
||||
n_intervals=0
|
||||
)
|
||||
], style={
|
||||
'backgroundColor': '#121212',
|
||||
'padding': '20px',
|
||||
'minHeight': '100vh',
|
||||
'fontFamily': 'Arial, sans-serif'
|
||||
})
|
||||
|
||||
# Register URL callback to update page content
|
||||
@self.app.callback(
|
||||
Output('page-content', 'children'),
|
||||
[Input('url', 'pathname')]
|
||||
)
|
||||
def display_page(pathname):
|
||||
if pathname is None:
|
||||
pathname = f'/{self.symbol.replace("/", "-")}/chart'
|
||||
|
||||
symbol_path = self.symbol.replace("/", "-")
|
||||
|
||||
if pathname == f'/{symbol_path}/chart' or pathname == f'/' or pathname == f'/{symbol_path}':
|
||||
return self._get_chart_layout(button_style, active_button_style)
|
||||
elif pathname == f'/{symbol_path}/ticks':
|
||||
return self._get_ticks_layout()
|
||||
else:
|
||||
return html.Div([
|
||||
html.H1('404 - Page Not Found', style={'textAlign': 'center', 'color': 'white'})
|
||||
])
|
||||
|
||||
# Register callback to update the interval selection from button clicks
|
||||
self._setup_interval_callback(button_style, active_button_style)
|
||||
|
||||
# Register callback to update the chart data
|
||||
self._setup_chart_callback()
|
||||
|
||||
# Register callback to update the ticks data
|
||||
self._setup_ticks_callback()
|
||||
|
||||
def _get_chart_layout(self, button_style, active_button_style):
|
||||
# Chart page layout
|
||||
return html.Div([
|
||||
# Interval selection buttons
|
||||
html.Div([
|
||||
html.Div("Candlestick Interval:", style={
|
||||
@ -677,26 +870,98 @@ class RealTimeChart:
|
||||
dcc.Graph(
|
||||
id='live-chart',
|
||||
style={
|
||||
'height': '80vh',
|
||||
'height': '180vh',
|
||||
'border': '1px solid #444444',
|
||||
'borderRadius': '5px',
|
||||
'boxShadow': '0 4px 8px 0 rgba(0,0,0,0.2)'
|
||||
}
|
||||
),
|
||||
])
|
||||
|
||||
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'
|
||||
}),
|
||||
|
||||
# Update interval
|
||||
dcc.Interval(
|
||||
id='interval-component',
|
||||
interval=500, # Update every 500ms for smoother display
|
||||
n_intervals=0
|
||||
)
|
||||
], style={
|
||||
'backgroundColor': '#121212',
|
||||
'padding': '20px',
|
||||
'height': '100vh',
|
||||
'fontFamily': 'Arial, sans-serif'
|
||||
})
|
||||
|
||||
# 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):
|
||||
# Callback to update interval based on button clicks and update button styles
|
||||
@self.app.callback(
|
||||
[Output('interval-store', 'data'),
|
||||
@ -754,7 +1019,8 @@ class RealTimeChart:
|
||||
styles[4] = active_button_style
|
||||
|
||||
return (data, *styles)
|
||||
|
||||
|
||||
def _setup_chart_callback(self):
|
||||
# Callback to update the chart
|
||||
@self.app.callback(
|
||||
Output('live-chart', 'figure'),
|
||||
@ -772,14 +1038,15 @@ class RealTimeChart:
|
||||
# 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()
|
||||
time_stats = self.tick_storage.get_time_based_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,
|
||||
vertical_spacing=0.02, # Reduced for better use of vertical space
|
||||
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
|
||||
row_heights=[0.5, 0.1, 0.1, 0.1, 0.1, 0.1] # Give more space to main chart
|
||||
)
|
||||
|
||||
if not df.empty and len(df) > 0:
|
||||
@ -931,6 +1198,17 @@ class RealTimeChart:
|
||||
# Add candle count
|
||||
candle_count = len(df) if not df.empty else 0
|
||||
info_lines.append(f"Candles: {candle_count} ({interval}s)")
|
||||
|
||||
# Add time-based statistics
|
||||
if time_stats and time_stats['periods']:
|
||||
info_lines.append("<b>Time-Based Stats:</b>")
|
||||
for period, stats in time_stats['periods'].items():
|
||||
if stats['tick_count'] > 0:
|
||||
info_lines.append(f"{period}: {stats['tick_count']} ticks, {stats['ticks_per_second']:.2f}/s")
|
||||
if stats['min_price'] is not None and stats['max_price'] is not None:
|
||||
price_change = stats['last_price'] - stats['min_price']
|
||||
change_pct = (price_change / stats['min_price']) * 100 if stats['min_price'] > 0 else 0
|
||||
info_lines.append(f" Range: {stats['min_price']:.2f}-{stats['max_price']:.2f} ({change_pct:.2f}%)")
|
||||
|
||||
# Add info box to the chart
|
||||
fig.add_annotation(
|
||||
@ -963,7 +1241,7 @@ class RealTimeChart:
|
||||
title_text=f"{self.symbol} Real-Time Data ({interval_text})",
|
||||
title_x=0.5, # Center the title
|
||||
xaxis_rangeslider_visible=False,
|
||||
height=1200, # Adjusted height for new subcharts
|
||||
height=1800, # Increased height for taller display
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(25,25,50,1)',
|
||||
@ -1005,6 +1283,313 @@ class RealTimeChart:
|
||||
)
|
||||
return fig
|
||||
|
||||
def _setup_ticks_callback(self):
|
||||
# Callbacks to update the ticks data display
|
||||
|
||||
# Callback to update stats cards
|
||||
@self.app.callback(
|
||||
Output('tick-stats-cards', 'children'),
|
||||
[Input('interval-component', 'n_intervals'),
|
||||
Input('refresh-ticks-btn', 'n_clicks'),
|
||||
Input('time-window-dropdown', 'value')]
|
||||
)
|
||||
def update_tick_stats(n_intervals, n_clicks, time_window):
|
||||
# Get time window in seconds
|
||||
window_seconds = time_window if time_window else 300
|
||||
|
||||
# Calculate time range for filtering
|
||||
now = int(time.time() * 1000)
|
||||
start_time = now - (window_seconds * 1000)
|
||||
|
||||
# Get filtered ticks
|
||||
filtered_ticks = self.tick_storage.get_ticks_from_time(start_time_ms=start_time)
|
||||
|
||||
if not filtered_ticks:
|
||||
return [html.Div("No tick data available in the selected time window.",
|
||||
style={'color': 'white', 'textAlign': 'center', 'margin': '20px'})]
|
||||
|
||||
# Calculate stats
|
||||
tick_count = len(filtered_ticks)
|
||||
|
||||
prices = [tick['price'] for tick in filtered_ticks]
|
||||
min_price = min(prices) if prices else 0
|
||||
max_price = max(prices) if prices else 0
|
||||
avg_price = sum(prices) / len(prices) if prices else 0
|
||||
price_range = max_price - min_price
|
||||
|
||||
volumes = [tick.get('volume', 0) for tick in filtered_ticks]
|
||||
total_volume = sum(volumes)
|
||||
avg_volume = total_volume / len(volumes) if volumes else 0
|
||||
|
||||
first_timestamp = filtered_ticks[0]['timestamp']
|
||||
last_timestamp = filtered_ticks[-1]['timestamp']
|
||||
time_span_ms = last_timestamp - first_timestamp
|
||||
time_span_seconds = time_span_ms / 1000
|
||||
ticks_per_second = tick_count / time_span_seconds if time_span_seconds > 0 else 0
|
||||
|
||||
# Create stat cards
|
||||
card_style = {
|
||||
'backgroundColor': '#1E2130',
|
||||
'borderRadius': '5px',
|
||||
'padding': '15px',
|
||||
'marginBottom': '10px',
|
||||
'width': '23%',
|
||||
'boxShadow': '0 2px 4px rgba(0,0,0,0.2)',
|
||||
'color': 'white',
|
||||
'display': 'flex',
|
||||
'flexDirection': 'column',
|
||||
'alignItems': 'center',
|
||||
'justifyContent': 'center'
|
||||
}
|
||||
|
||||
mobile_card_style = {
|
||||
**card_style,
|
||||
'width': '100%',
|
||||
'marginBottom': '10px'
|
||||
}
|
||||
|
||||
value_style = {
|
||||
'fontSize': '24px',
|
||||
'fontWeight': 'bold',
|
||||
'color': '#4CAF50',
|
||||
'marginTop': '5px'
|
||||
}
|
||||
|
||||
label_style = {
|
||||
'fontSize': '14px',
|
||||
'color': '#AAAAAA'
|
||||
}
|
||||
|
||||
return [
|
||||
html.Div([
|
||||
html.Div("Tick Count", style=label_style),
|
||||
html.Div(f"{tick_count:,}", style=value_style),
|
||||
html.Div(f"{ticks_per_second:.2f} ticks/sec", style=label_style)
|
||||
], style=card_style),
|
||||
|
||||
html.Div([
|
||||
html.Div("Price Range", style=label_style),
|
||||
html.Div(f"{min_price:.2f} - {max_price:.2f}", style=value_style),
|
||||
html.Div(f"Range: {price_range:.2f}", style=label_style)
|
||||
], style=card_style),
|
||||
|
||||
html.Div([
|
||||
html.Div("Average Price", style=label_style),
|
||||
html.Div(f"{avg_price:.2f}", style=value_style),
|
||||
html.Div("USDT", style=label_style)
|
||||
], style=card_style),
|
||||
|
||||
html.Div([
|
||||
html.Div("Total Volume", style=label_style),
|
||||
html.Div(f"{total_volume:.8f}", style=value_style),
|
||||
html.Div(f"Avg: {avg_volume:.8f}", style=label_style)
|
||||
], style=card_style)
|
||||
]
|
||||
|
||||
# Callback to update ticks table
|
||||
@self.app.callback(
|
||||
Output('ticks-table-container', 'children'),
|
||||
[Input('interval-component', 'n_intervals'),
|
||||
Input('refresh-ticks-btn', 'n_clicks'),
|
||||
Input('time-window-dropdown', 'value')]
|
||||
)
|
||||
def update_ticks_table(n_intervals, n_clicks, time_window):
|
||||
# Get time window in seconds
|
||||
window_seconds = time_window if time_window else 300
|
||||
|
||||
# Calculate time range for filtering
|
||||
now = int(time.time() * 1000)
|
||||
start_time = now - (window_seconds * 1000)
|
||||
|
||||
# Get filtered ticks
|
||||
filtered_ticks = self.tick_storage.get_ticks_from_time(start_time_ms=start_time)
|
||||
|
||||
if not filtered_ticks:
|
||||
return html.Div("No tick data available in the selected time window.",
|
||||
style={'color': 'white', 'textAlign': 'center', 'margin': '20px'})
|
||||
|
||||
# Convert to readable format
|
||||
formatted_ticks = []
|
||||
for tick in filtered_ticks:
|
||||
timestamp_dt = datetime.fromtimestamp(tick['timestamp'] / 1000)
|
||||
formatted_time = timestamp_dt.strftime('%H:%M:%S.%f')[:-3] # Format as HH:MM:SS.mmm
|
||||
|
||||
formatted_ticks.append({
|
||||
'time': formatted_time,
|
||||
'price': f"{tick['price']:.2f}",
|
||||
'volume': f"{tick['volume']:.8f}",
|
||||
'value': f"{tick['price'] * tick['volume']:.2f}"
|
||||
})
|
||||
|
||||
# Display only the most recent ticks
|
||||
display_limit = 100
|
||||
limited_ticks = formatted_ticks[-display_limit:] if len(formatted_ticks) > display_limit else formatted_ticks
|
||||
limited_ticks.reverse() # Show most recent at the top
|
||||
|
||||
# Create table
|
||||
table_style = {
|
||||
'width': '100%',
|
||||
'borderCollapse': 'collapse',
|
||||
'color': 'white',
|
||||
'fontFamily': 'monospace'
|
||||
}
|
||||
|
||||
header_style = {
|
||||
'backgroundColor': '#1A1A1A',
|
||||
'fontWeight': 'bold',
|
||||
'padding': '8px',
|
||||
'textAlign': 'left',
|
||||
'borderBottom': '2px solid #444'
|
||||
}
|
||||
|
||||
cell_style = {
|
||||
'padding': '6px',
|
||||
'borderBottom': '1px solid #333',
|
||||
'textAlign': 'right'
|
||||
}
|
||||
|
||||
time_cell_style = {
|
||||
**cell_style,
|
||||
'textAlign': 'left'
|
||||
}
|
||||
|
||||
# Create table header
|
||||
header = html.Tr([
|
||||
html.Th("Time", style=header_style),
|
||||
html.Th("Price", style=header_style),
|
||||
html.Th("Volume", style=header_style),
|
||||
html.Th("Value (USDT)", style=header_style)
|
||||
])
|
||||
|
||||
# Create table rows
|
||||
rows = []
|
||||
for i, tick in enumerate(limited_ticks):
|
||||
row_style = {'backgroundColor': '#1E1E1E'} if i % 2 == 0 else {'backgroundColor': '#252525'}
|
||||
|
||||
rows.append(html.Tr([
|
||||
html.Td(tick['time'], style={**time_cell_style, **row_style}),
|
||||
html.Td(tick['price'], style={**cell_style, **row_style}),
|
||||
html.Td(tick['volume'], style={**cell_style, **row_style}),
|
||||
html.Td(tick['value'], style={**cell_style, **row_style})
|
||||
]))
|
||||
|
||||
return [
|
||||
html.Div([
|
||||
html.H3(f"Latest {len(limited_ticks)} Ticks (from {len(filtered_ticks)} total)",
|
||||
style={'color': 'white', 'marginBottom': '10px', 'textAlign': 'center'}),
|
||||
html.Table([html.Thead(header), html.Tbody(rows)], style=table_style)
|
||||
])
|
||||
]
|
||||
|
||||
# Callback to update price chart
|
||||
@self.app.callback(
|
||||
Output('tick-price-chart', 'figure'),
|
||||
[Input('interval-component', 'n_intervals'),
|
||||
Input('refresh-ticks-btn', 'n_clicks'),
|
||||
Input('time-window-dropdown', 'value')]
|
||||
)
|
||||
def update_price_chart(n_intervals, n_clicks, time_window):
|
||||
# Get time window in seconds
|
||||
window_seconds = time_window if time_window else 300
|
||||
|
||||
# Calculate time range for filtering
|
||||
now = int(time.time() * 1000)
|
||||
start_time = now - (window_seconds * 1000)
|
||||
|
||||
# Get filtered ticks
|
||||
filtered_ticks = self.tick_storage.get_ticks_from_time(start_time_ms=start_time)
|
||||
|
||||
# Create figure
|
||||
fig = go.Figure()
|
||||
|
||||
if not filtered_ticks:
|
||||
fig.add_annotation(
|
||||
x=0.5, y=0.5,
|
||||
text="No tick data available in the selected time window.",
|
||||
showarrow=False,
|
||||
font=dict(size=14, color="white"),
|
||||
xref="paper", yref="paper"
|
||||
)
|
||||
else:
|
||||
# Convert timestamps to datetime for better x-axis display
|
||||
timestamps = [datetime.fromtimestamp(tick['timestamp'] / 1000) for tick in filtered_ticks]
|
||||
prices = [tick['price'] for tick in filtered_ticks]
|
||||
volumes = [tick.get('volume', 0) for tick in filtered_ticks]
|
||||
|
||||
# Scale volumes for better visibility
|
||||
max_volume = max(volumes) if volumes else 1
|
||||
scaled_volumes = [vol * (max(prices) - min(prices)) / max_volume * 0.2 + min(prices) for vol in volumes]
|
||||
|
||||
# Add price line
|
||||
fig.add_trace(go.Scatter(
|
||||
x=timestamps,
|
||||
y=prices,
|
||||
mode='lines',
|
||||
name='Price',
|
||||
line=dict(color='#4CAF50', width=1.5)
|
||||
))
|
||||
|
||||
# Add volume bars
|
||||
fig.add_trace(go.Bar(
|
||||
x=timestamps,
|
||||
y=scaled_volumes,
|
||||
name='Volume',
|
||||
marker=dict(color='rgba(128, 128, 255, 0.3)'),
|
||||
opacity=0.5,
|
||||
yaxis='y2'
|
||||
))
|
||||
|
||||
# Add annotations for latest price
|
||||
latest_price = prices[-1] if prices else 0
|
||||
fig.add_annotation(
|
||||
x=timestamps[-1] if timestamps else 0,
|
||||
y=latest_price,
|
||||
text=f"{latest_price:.2f}",
|
||||
showarrow=True,
|
||||
arrowhead=1,
|
||||
arrowsize=1,
|
||||
arrowwidth=2,
|
||||
arrowcolor="#4CAF50",
|
||||
font=dict(size=12, color="#4CAF50"),
|
||||
xshift=50
|
||||
)
|
||||
|
||||
# Update layout
|
||||
fig.update_layout(
|
||||
title=f"{self.symbol} Price Movement (Last {window_seconds // 60} minutes)",
|
||||
title_x=0.5,
|
||||
xaxis=dict(
|
||||
title="Time",
|
||||
showgrid=True,
|
||||
gridcolor='rgba(128,128,128,0.2)'
|
||||
),
|
||||
yaxis=dict(
|
||||
title="Price (USDT)",
|
||||
showgrid=True,
|
||||
gridcolor='rgba(128,128,128,0.2)'
|
||||
),
|
||||
yaxis2=dict(
|
||||
title="Volume",
|
||||
overlaying='y',
|
||||
side='right',
|
||||
showgrid=False,
|
||||
showticklabels=False
|
||||
),
|
||||
template='plotly_dark',
|
||||
paper_bgcolor='rgba(0,0,0,0)',
|
||||
plot_bgcolor='rgba(25,25,50,1)',
|
||||
height=500,
|
||||
margin=dict(l=40, r=40, t=50, b=40),
|
||||
legend=dict(
|
||||
yanchor="top",
|
||||
y=0.99,
|
||||
xanchor="left",
|
||||
x=0.01
|
||||
)
|
||||
)
|
||||
|
||||
return fig
|
||||
|
||||
def _interval_to_seconds(self, interval_key: str) -> int:
|
||||
"""Convert interval key to seconds"""
|
||||
mapping = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user