Создание интерактивной панели на 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)

Рекомендации