LiveTradingChart real-time candlestick simulation with OHLCV data, volume bars, forecast band, swing-point alert labels, and slider-controlled parameters.

Live Trading Chart

LiveTradingChart real-time candlestick simulation with OHLCV data, volume bars, forecast band, swing-point alert labels, and slider-controlled parameters.


Overview

LiveTradingChart is the real-time streaming chart component of dash-mui-charts, built for live data visualization such as trading feeds and sensor streams. The demo renders a live OHLCV candlestick simulation with volume bars, a forecast line with uncertainty bands, and alert labels on significant moves — ticks are generated by the component itself from a seed, so no Dash interval is needed to stream.

Basic streaming is Community tier. Pro features — zoom and the range slider (showSlider) — require a MUI X Pro license key passed as licenseKey (read here from MUI_PRO_API_KEY).

Simulation props are live-updatable from Dash callbacks: running, intervalMs, volatility, drift, windowSize, seed, forecastVolatility, and resetTrigger (change its value to restart). Output props updated as the simulation runs: currentPrice, tickCount, and alertHistory.

alertFormatter and alertFilter accept {'function': 'name', 'options': {...}}, resolved from the window.dashMuiChartsFunctions registry defined in assets/muiChartsFunctions.js — the demo formats prices with priceAlertFormatter.


Live simulation

Start the simulation, then drive it: speed, volatility, drift and window size are sliders; volume bars, price labels and the zoom preview are toggles. The stats row and alert log stream from the chart's output props.

# File: docs/live_trading/simulator.py

"""LiveTradingChart simulator — the whole control surface.

Rendered on /live-trading via `.. exec::`; ported verbatim from the
pre-migration pages/live_trading.py (same ids, same callbacks).
"""
import os

import dash_mantine_components as dmc
from dash import html, callback, Input, Output, State, ctx

from dash_mui_charts import LiveTradingChart

MUI_LICENSE_KEY = os.environ.get('MUI_PRO_API_KEY', '')

# Styles
stat_card = {
    'backgroundColor': 'var(--mantine-color-default)',
    'padding': '12px 20px',
    'borderRadius': '8px',
    'textAlign': 'center',
    'minWidth': '130px',
}
code_style = {
    'backgroundColor': 'var(--mantine-color-default)',
    'padding': '15px',
    'borderRadius': '5px',
    'whiteSpace': 'pre-wrap',
    'fontSize': '12px',
    'overflow': 'auto',
    'maxHeight': '300px',
}

component = html.Div([
    # ── Controls Row 1: Buttons + Toggles ─────────────────────────────────
    html.Div([
        html.Div([
            dmc.Button("Start", id='lt-start-btn', color='green', size='md',
                       style={'marginRight': '10px'}),
            dmc.Button("Stop", id='lt-stop-btn', color='yellow', size='md', variant='outline',
                       style={'marginRight': '10px'}),
            dmc.Button("Reset", id='lt-reset-btn', color='red', size='md', variant='outline'),
        ], style={'display': 'flex', 'alignItems': 'center'}),
        html.Div([
            dmc.Switch(id='lt-volume-toggle', label='Volume Bars', checked=True, size='md'),
            dmc.Switch(id='lt-labels-toggle', label='Price Labels', checked=False, size='md'),
            dmc.Switch(id='lt-slider-toggle', label='Zoom Preview', checked=True, size='md'),
        ], style={'display': 'flex', 'gap': '25px', 'alignItems': 'center'}),
    ], style={'display': 'flex', 'justifyContent': 'space-between', 'marginBottom': '15px'}),

    # ── Controls Row 2: Sliders ───────────────────────────────────────────
    html.Div([
        html.Div([
            html.Label("Speed (ms/tick)", style={'fontWeight': 500, 'marginBottom': '5px', 'display': 'block'}),
            dmc.Slider(
                id='lt-speed', value=200, min=50, max=1000, step=50,
                marks=[{'value': 50, 'label': '50'}, {'value': 200, 'label': '200'},
                       {'value': 500, 'label': '500'}, {'value': 1000, 'label': '1s'}],
                style={'maxWidth': '280px'},
            ),
        ], style={'flex': '1'}),
        html.Div([
            html.Label("Volatility", style={'fontWeight': 500, 'marginBottom': '5px', 'display': 'block'}),
            dmc.Slider(
                id='lt-volatility', value=2.0, min=0.5, max=8.0, step=0.5,
                marks=[{'value': 0.5, 'label': '0.5'}, {'value': 2.0, 'label': '2.0'},
                       {'value': 5.0, 'label': '5.0'}, {'value': 8.0, 'label': '8.0'}],
                style={'maxWidth': '280px'},
            ),
        ], style={'flex': '1'}),
        html.Div([
            html.Label("Drift", style={'fontWeight': 500, 'marginBottom': '5px', 'display': 'block'}),
            dmc.Slider(
                id='lt-drift', value=0.1, min=-0.5, max=0.5, step=0.05,
                marks=[{'value': -0.5, 'label': '-0.5'}, {'value': 0, 'label': '0'},
                       {'value': 0.5, 'label': '+0.5'}],
                style={'maxWidth': '280px'},
            ),
        ], style={'flex': '1'}),
        html.Div([
            html.Label("Window", style={'fontWeight': 500, 'marginBottom': '5px', 'display': 'block'}),
            dmc.Slider(
                id='lt-window', value=80, min=30, max=200, step=10,
                marks=[{'value': 30, 'label': '30'}, {'value': 80, 'label': '80'},
                       {'value': 200, 'label': '200'}],
                style={'maxWidth': '280px'},
            ),
        ], style={'flex': '1'}),
    ], style={'display': 'flex', 'gap': '30px', 'marginBottom': '20px'}),

    # ── Stats Row ─────────────────────────────────────────────────────────
    html.Div([
        html.Div([
            html.Div("Price", style={'fontSize': '11px', 'color': 'var(--mantine-color-dimmed)'}),
            html.Div(id='lt-price-display', children="$100.00",
                     style={'fontSize': '22px', 'fontWeight': 'bold'}),
        ], style=stat_card),
        html.Div([
            html.Div("Ticks", style={'fontSize': '11px', 'color': 'var(--mantine-color-dimmed)'}),
            html.Div(id='lt-tick-display', children="0",
                     style={'fontSize': '22px', 'fontWeight': 'bold'}),
        ], style=stat_card),
        html.Div([
            html.Div("Alerts", style={'fontSize': '11px', 'color': 'var(--mantine-color-dimmed)'}),
            html.Div(id='lt-alert-count', children="0",
                     style={'fontSize': '22px', 'fontWeight': 'bold'}),
        ], style=stat_card),
        html.Div([
            html.Div("Status", style={'fontSize': '11px', 'color': 'var(--mantine-color-dimmed)'}),
            html.Div(id='lt-status', children="Stopped",
                     style={'fontSize': '22px', 'fontWeight': 'bold', 'color': '#999'}),
        ], style=stat_card),
    ], style={'display': 'flex', 'gap': '12px', 'marginBottom': '15px'}),

    # ── Chart ─────────────────────────────────────────────────────────────
    LiveTradingChart(
        id='lt-chart',
        licenseKey=MUI_LICENSE_KEY,
        height=520,
        running=False,
        intervalMs=200,
        seed=42,
        windowSize=80,
        forecastSize=20,
        initialPrice=100,
        volatility=0.02,
        drift=0.001,
        forecastVolatility=1.5,
        # Swing-point alert detection (labels at extreme highs/lows only)
        alertLookback=5,          # Candles on each side to confirm swing point
        alertMinDistance=10,       # Min ticks between alerts
        maxVisibleAlerts=6,        # Cap visible labels in window
        # Functions-as-props: custom label formatting via JS registry
        alertFormatter={
            'function': 'priceAlertFormatter',
            'options': {'decimals': 2},
        },
        # Uncomment to use custom alert detection logic:
        # alertFilter={
        #     'function': 'swingAlertFilter',
        #     'options': {'lookback': 7},
        # },
        showVolume=True,
        showLabels=False,
        showSlider=True,
        volumeHeightPct=20,
        margin={'left': 75, 'right': 30, 'top': 20, 'bottom': 50},
    ),

    # ── Alert History ─────────────────────────────────────────────────────
    html.H4("Recent Alerts", style={'marginTop': '15px'}),
    html.Pre(id='lt-alert-log', children="Alerts will appear here...", style=code_style),

], style={'padding': '20px'})


# ── Callbacks ─────────────────────────────────────────────────────────────

@callback(
    Output('lt-chart', 'running'),
    Input('lt-start-btn', 'n_clicks'),
    Input('lt-stop-btn', 'n_clicks'),
    prevent_initial_call=True,
)
def toggle_running(start_clicks, stop_clicks):
    return ctx.triggered_id == 'lt-start-btn'


@callback(
    Output('lt-chart', 'resetTrigger'),
    Input('lt-reset-btn', 'n_clicks'),
    prevent_initial_call=True,
)
def reset_chart(n_clicks):
    return n_clicks or 0


@callback(Output('lt-chart', 'intervalMs'), Input('lt-speed', 'value'))
def update_speed(val):
    return val or 200


@callback(Output('lt-chart', 'volatility'), Input('lt-volatility', 'value'))
def update_volatility(val):
    return (val or 2.0) / 100


@callback(Output('lt-chart', 'drift'), Input('lt-drift', 'value'))
def update_drift(val):
    return (val or 0) / 100


@callback(Output('lt-chart', 'windowSize'), Input('lt-window', 'value'))
def update_window(val):
    return val or 80


@callback(Output('lt-chart', 'showVolume'), Input('lt-volume-toggle', 'checked'))
def toggle_volume(checked):
    return checked if checked is not None else True


@callback(Output('lt-chart', 'showLabels'), Input('lt-labels-toggle', 'checked'))
def toggle_labels(checked):
    return checked if checked is not None else False


@callback(Output('lt-chart', 'showSlider'), Input('lt-slider-toggle', 'checked'))
def toggle_slider(checked):
    return checked if checked is not None else True


@callback(
    Output('lt-price-display', 'children'),
    Output('lt-price-display', 'style'),
    Output('lt-tick-display', 'children'),
    Output('lt-status', 'children'),
    Output('lt-status', 'style'),
    Input('lt-chart', 'currentPrice'),
    Input('lt-chart', 'tickCount'),
    State('lt-chart', 'running'),
)
def update_stats(price, ticks, is_running):
    price_val = price if price is not None else 100
    color = '#4caf50' if price_val >= 100 else '#f44336'
    return (
        f"${price_val:,.2f}",
        {'fontSize': '22px', 'fontWeight': 'bold', 'color': color},
        str(ticks or 0),
        "Running" if is_running else "Stopped",
        {'fontSize': '22px', 'fontWeight': 'bold', 'color': '#4caf50' if is_running else '#999'},
    )


@callback(
    Output('lt-alert-count', 'children'),
    Output('lt-alert-log', 'children'),
    Input('lt-chart', 'alertHistory'),
)
def update_alerts(alerts):
    if not alerts:
        return "0", "Alerts will appear here..."
    recent = list(reversed(alerts[-15:]))
    lines = []
    for a in recent:
        d = "UP" if a.get('type') == 'up' else "DN"
        lines.append(f"[Tick {a.get('tick', '?'):>5}]  {d}  ${a.get('price', 0):>8.2f}  ({a.get('message', '')})")
    return str(len(alerts)), "\n".join(lines)

:defaultExpanded: false :withExpandedButton: true


Related pages


Source: /live-trading

Note for AI agents: This is the static, prerendered view of an interactive Dash application served because we detected a non-JS user agent. Full prose docs: