Создание интерактивной панели на Dash и Plotly с callback-и для локального и облачного запуска
Кратко о задаче
В этом руководстве показано, как собрать интерактивную финансовую панель с помощью Dash, Plotly и Bootstrap. Мы генерируем тестовые данные, проектируем отзывчивый интерфейс с элементами управления и визуализациями и связываем элементы управления с выводами через callback-и Dash, чтобы диаграммы, метрики и таблицы обновлялись в реальном времени. Подход применим как локально, так и в облаке (например, Google Colab).
Зависимости и импорты
Установите и импортируйте основные библиотеки, а затем инициализируйте seed для воспроизводимости генерируемых данных.
!pip install dash plotly pandas numpy dash-bootstrap-components
import dash
from dash import dcc, html, Input, Output, callback, dash_table
import plotly.express as px
import plotly.graph_objects as go
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import dash_bootstrap_components as dbc
print("Generating sample data...")
np.random.seed(42)
Генерация синтетических данных акций
Для прототипирования панели удобно создать синтетический набор данных с несколькими тикерами, ценами, объемами, доходностями и техническими индикаторами (скользящая средняя, волатильность). Это позволит отлаживать интерфейс без зависимости от внешних источников.
start_date = datetime(2023, 1, 1)
end_date = datetime(2024, 12, 31)
dates = pd.date_range(start=start_date, end=end_date, freq='D')
stock_names = ['AAPL', 'GOOGL', 'MSFT', 'AMZN', 'TSLA']
all_data = []
base_prices = {'AAPL': 150, 'GOOGL': 120, 'MSFT': 250, 'AMZN': 100, 'TSLA': 200}
for stock in stock_names:
print(f"Creating data for {stock}...")
base_price = base_prices[stock]
n_days = len(dates)
returns = np.random.normal(0.0005, 0.025, n_days)
prices = np.zeros(n_days)
prices[0] = base_price
for i in range(1, n_days):
prices[i] = prices[i-1] * (1 + returns[i])
volumes = np.random.lognormal(15, 0.5, n_days).astype(int)
stock_df = pd.DataFrame({
'Date': dates,
'Stock': stock,
'Price': prices,
'Volume': volumes,
'Returns': np.concatenate([[0], np.diff(prices) / prices[:-1]]),
'Sector': np.random.choice(['Technology', 'Consumer', 'Automotive'], 1)[0]
})
all_data.append(stock_df)
df = pd.concat(all_data, ignore_index=True)
df['Date'] = pd.to_datetime(df['Date'])
df_sorted = df.sort_values(['Stock', 'Date']).reset_index(drop=True)
print("Calculating technical indicators...")
df_sorted['MA_20'] = df_sorted.groupby('Stock')['Price'].transform(lambda x: x.rolling(20, min_periods=1).mean())
df_sorted['Volatility'] = df_sorted.groupby('Stock')['Returns'].transform(lambda x: x.rolling(30, min_periods=1).std())
df = df_sorted.copy()
print(f"Data generated successfully! Shape: {df.shape}")
print(f"Date range: {df['Date'].min()} to {df['Date'].max()}")
print(f"Stocks: {df['Stock'].unique().tolist()}")
Макет панели и элементы управления
Макет оформлен с использованием Bootstrap: строки, колонки и карточки. В левой колонке находятся элементы управления (выбор акций, диапазон дат, стиль графика и переключатель MA), а в правой — основной график. Ниже — метрики, вторичные графики и таблица с фильтрацией и сортировкой.
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
app.layout = dbc.Container([
dbc.Row([
dbc.Col([
html.H1(" Advanced Financial Dashboard", className="text-center mb-4"),
html.P(f"Interactive dashboard with {len(df)} data points across {len(stock_names)} stocks",
className="text-center text-muted"),
html.Hr()
])
]),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H5(" Dashboard Controls", className="card-title"),
html.Label("Select Stocks:", className="fw-bold mt-3"),
dcc.Dropdown(
id='stock-dropdown',
options=[{'label': f'{stock} ({base_prices[stock]})', 'value': stock}
for stock in stock_names],
value=['AAPL', 'GOOGL'],
multi=True,
placeholder="Choose stocks to analyze..."
),
html.Label("Date Range:", className="fw-bold mt-3"),
dcc.DatePickerRange(
id='date-picker-range',
start_date='2023-06-01',
end_date='2024-06-01',
display_format='YYYY-MM-DD',
style={'width': '100%'}
),
html.Label("Chart Style:", className="fw-bold mt-3"),
dcc.RadioItems(
id='chart-type',
options=[
{'label': ' Line Chart', 'value': 'line'},
{'label': ' Area Chart', 'value': 'area'},
{'label': ' Scatter Plot', 'value': 'scatter'}
],
value='line',
labelStyle={'display': 'block', 'margin': '5px'}
),
dbc.Checklist(
id='show-ma',
options=[{'label': ' Show Moving Average', 'value': 'show'}],
value=[],
style={'margin': '10px 0'}
),
])
], className="h-100")
], width=3),
dbc.Col([
dbc.Card([
dbc.CardHeader(" Stock Price Analysis"),
dbc.CardBody([
dcc.Graph(id='main-chart', style={'height': '450px'})
])
])
], width=9)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(id="avg-price", className="text-primary mb-0"),
html.Small("Average Price", className="text-muted")
])
])
], width=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(id="total-volume", className="text-success mb-0"),
html.Small("Total Volume", className="text-muted")
])
])
], width=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(id="price-range", className="text-info mb-0"),
html.Small("Price Range", className="text-muted")
])
])
], width=3),
dbc.Col([
dbc.Card([
dbc.CardBody([
html.H4(id="data-points", className="text-warning mb-0"),
html.Small("Data Points", className="text-muted")
])
])
], width=3)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader(" Trading Volume"),
dbc.CardBody([
dcc.Graph(id='volume-chart', style={'height': '300px'})
])
])
], width=6),
dbc.Col([
dbc.Card([
dbc.CardHeader(" Returns Distribution"),
dbc.CardBody([
dcc.Graph(id='returns-chart', style={'height': '300px'})
])
])
], width=6)
], className="mb-4"),
dbc.Row([
dbc.Col([
dbc.Card([
dbc.CardHeader(" Latest Stock Data"),
dbc.CardBody([
dash_table.DataTable(
id='data-table',
columns=[
{'name': 'Stock', 'id': 'Stock'},
{'name': 'Date', 'id': 'Date'},
{'name': 'Price ($)', 'id': 'Price', 'type': 'numeric',
'format': {'specifier': '.2f'}},
{'name': 'Volume', 'id': 'Volume', 'type': 'numeric',
'format': {'specifier': ',.0f'}},
{'name': 'Daily Return (%)', 'id': 'Returns', 'type': 'numeric',
'format': {'specifier': '.2%'}}
],
style_cell={'textAlign': 'center', 'fontSize': '14px', 'padding': '10px'},
style_header={'backgroundColor': 'rgb(230, 230, 230)', 'fontWeight': 'bold'},
style_data_conditional=[
{
'if': {'filter_query': '{Returns} > 0'},
'backgroundColor': '#d4edda'
},
{
'if': {'filter_query': '{Returns} < 0'},
'backgroundColor': '#f8d7da'
}
],
page_size=15,
sort_action="native",
filter_action="native"
)
])
])
])
])
], fluid=True)
Callback и логика обновления
Callback принимает контролы как входы и возвращает фигуры, данные таблицы и текстовые метрики. В теле callback-а выполняется фильтрация по выбранным тикерам и датам, построение основного графика в выбранном стиле, добавление скользящих средних при необходимости и вычисление сводных значений.
@callback(
[Output('main-chart', 'figure'),
Output('volume-chart', 'figure'),
Output('returns-chart', 'figure'),
Output('data-table', 'data'),
Output('avg-price', 'children'),
Output('total-volume', 'children'),
Output('price-range', 'children'),
Output('data-points', 'children')],
[Input('stock-dropdown', 'value'),
Input('date-picker-range', 'start_date'),
Input('date-picker-range', 'end_date'),
Input('chart-type', 'value'),
Input('show-ma', 'value')]
)
def update_all_charts(selected_stocks, start_date, end_date, chart_type, show_ma):
print(f"Callback triggered with stocks: {selected_stocks}")
if not selected_stocks:
selected_stocks = ['AAPL']
filtered_df = df[
(df['Stock'].isin(selected_stocks)) &
(df['Date'] >= start_date) &
(df['Date'] <= end_date)
].copy()
print(f"Filtered data shape: {filtered_df.shape}")
if filtered_df.empty:
filtered_df = df[df['Stock'].isin(selected_stocks)].copy()
print(f"Using all available data. Shape: {filtered_df.shape}")
if chart_type == 'line':
main_fig = px.line(filtered_df, x='Date', y='Price', color='Stock',
title=f'Stock Prices - {chart_type.title()} View',
labels={'Price': 'Price ($)', 'Date': 'Date'})
elif chart_type == 'area':
main_fig = px.area(filtered_df, x='Date', y='Price', color='Stock',
title=f'Stock Prices - {chart_type.title()} View',
labels={'Price': 'Price ($)', 'Date': 'Date'})
else:
main_fig = px.scatter(filtered_df, x='Date', y='Price', color='Stock',
title=f'Stock Prices - {chart_type.title()} View',
labels={'Price': 'Price ($)', 'Date': 'Date'})
if 'show' in show_ma:
for stock in selected_stocks:
stock_data = filtered_df[filtered_df['Stock'] == stock]
if not stock_data.empty:
main_fig.add_scatter(
x=stock_data['Date'],
y=stock_data['MA_20'],
mode='lines',
name=f'{stock} MA-20',
line=dict(dash='dash', width=2)
)
main_fig.update_layout(height=450, showlegend=True, hovermode='x unified')
volume_fig = px.bar(filtered_df, x='Date', y='Volume', color='Stock',
title='Daily Trading Volume',
labels={'Volume': 'Volume (shares)', 'Date': 'Date'})
volume_fig.update_layout(height=300, showlegend=True)
returns_fig = px.histogram(filtered_df.dropna(subset=['Returns']),
x='Returns', color='Stock',
title='Daily Returns Distribution',
labels={'Returns': 'Daily Returns', 'count': 'Frequency'},
nbins=50)
returns_fig.update_layout(height=300, showlegend=True)
if not filtered_df.empty:
avg_price = f"${filtered_df['Price'].mean():.2f}"
total_volume = f"{filtered_df['Volume'].sum():,.0f}"
price_range = f"${filtered_df['Price'].min():.0f} - ${filtered_df['Price'].max():.0f}"
data_points = f"{len(filtered_df):,}"
table_data = filtered_df.nlargest(100, 'Date')[
['Stock', 'Date', 'Price', 'Volume', 'Returns']
].round(4).to_dict('records')
for row in table_data:
row['Date'] = row['Date'].strftime('%Y-%m-%d') if pd.notnull(row['Date']) else ''
else:
avg_price = "No data"
total_volume = "No data"
price_range = "No data"
data_points = "0"
table_data = []
return (main_fig, volume_fig, returns_fig, table_data,
avg_price, total_volume, price_range, data_points)
Запуск приложения
В блоке if name == ‘main’ выполняется вывод краткого превью данных и запуск сервера. В средах вроде Colab используйте встроенные режимы показа или соответствующие адаптеры; для локальной разработки используйте app.run(debug=True).
if __name__ == '__main__':
print("Starting Dash app...")
print("Available data preview:")
print(df.head())
print(f"Total rows: {len(df)}")
app.run(mode='inline', port=8050, debug=True, height=1000)
# app.run(debug=True)
Рекомендации
- Разделяйте генерацию тестовых данных и получение продовых данных, чтобы быстро переключаться между режимами разработки и продакшн.
- Используйте один callback с множеством выходов, чтобы поддерживать синхронизацию компонентов интерфейса.
- При деплое рассмотрите контейнеризацию или хостинг на сервисах, поддерживающих Python веб-приложения. В Colab применяйте inline-режимы или туннелирование для внешнего доступа.