orderflow_backtest/dash_components.py

262 lines
7.7 KiB
Python
Raw Normal View History

"""
Custom Dash components for the interactive visualizer.
This module provides reusable UI components including the side panel,
navigation controls, and chart containers.
"""
from dash import html, dcc
import dash_bootstrap_components as dbc
import plotly.graph_objects as go
from plotly.subplots import make_subplots
def create_side_panel():
"""
Create the side panel component for displaying hover information and controls.
Returns:
dash component: Side panel layout
"""
return dbc.Card([
dbc.CardHeader("Chart Information"),
dbc.CardBody([
html.Div(id="hover-info", children=[
html.P("Hover over charts to see detailed information")
]),
html.Hr(),
html.Div([
dbc.Button("Reset CVD", id="reset-cvd-btn", color="primary", className="me-2"),
dbc.Button("Reset Zoom", id="reset-zoom-btn", color="secondary"),
])
])
], style={"height": "100vh"})
def create_chart_container():
"""
Create the main chart container for the 4-subplot layout.
Returns:
dash component: Chart container with 4-subplot layout
"""
return dcc.Graph(
id="main-charts",
figure=create_empty_subplot_layout(),
style={"height": "100vh"},
config={
"displayModeBar": True,
"displaylogo": False,
"modeBarButtonsToRemove": ["select2d", "lasso2d"],
"modeBarButtonsToAdd": ["resetScale2d"],
"scrollZoom": True, # Enable mouse wheel zooming
"doubleClick": "reset+autosize" # Double-click to reset zoom
}
)
def create_empty_subplot_layout():
"""
Create empty 4-subplot layout matching existing visualizer structure.
Returns:
plotly.graph_objects.Figure: Empty figure with 4 subplots
"""
fig = make_subplots(
rows=4, cols=1,
shared_xaxes=True,
subplot_titles=["OHLC", "Volume", "Order Book Imbalance (OBI)", "Cumulative Volume Delta (CVD)"],
vertical_spacing=0.02
)
# Configure layout to match existing styling
fig.update_layout(
height=800,
showlegend=False,
margin=dict(l=50, r=50, t=50, b=50),
template="plotly_dark", # Professional dark theme
paper_bgcolor='rgba(0,0,0,0)', # Transparent background
plot_bgcolor='rgba(0,0,0,0)' # Transparent plot area
)
# Configure synchronized zooming and panning
configure_synchronized_axes(fig)
return fig
def configure_synchronized_axes(fig):
"""
Configure synchronized zooming and panning across all subplots.
Args:
fig: Plotly figure with subplots
"""
# Enable dragmode for panning and zooming
fig.update_layout(
dragmode='zoom',
selectdirection='h' # Restrict selection to horizontal for time-based data
)
# Configure X-axes for synchronized behavior (already shared via make_subplots)
# All subplots will automatically share zoom/pan on X-axis due to shared_xaxes=True
# Configure individual Y-axes for better UX
fig.update_yaxes(fixedrange=False, gridcolor='rgba(128,128,128,0.2)') # Allow Y-axis zooming
fig.update_xaxes(fixedrange=False, gridcolor='rgba(128,128,128,0.2)') # Allow X-axis zooming
# Enable crosshair cursor spanning all charts
fig.update_layout(hovermode='x unified')
fig.update_traces(hovertemplate='<extra></extra>') # Clean hover labels
return fig
def add_ohlc_trace(fig, ohlc_data: dict):
"""
Add OHLC candlestick trace to the first subplot.
Args:
fig: Plotly figure with subplots
ohlc_data: Dict with x, open, high, low, close arrays
"""
candlestick = go.Candlestick(
x=ohlc_data["x"],
open=ohlc_data["open"],
high=ohlc_data["high"],
low=ohlc_data["low"],
close=ohlc_data["close"],
name="OHLC"
)
fig.add_trace(candlestick, row=1, col=1)
return fig
def add_volume_trace(fig, volume_data: dict):
"""
Add Volume bar trace to the second subplot.
Args:
fig: Plotly figure with subplots
volume_data: Dict with x (timestamps) and y (volumes) arrays
"""
volume_bar = go.Bar(
x=volume_data["x"],
y=volume_data["y"],
name="Volume",
marker_color='rgba(158, 185, 243, 0.7)', # Blue with transparency
showlegend=False,
hovertemplate="Volume: %{y}<extra></extra>"
)
fig.add_trace(volume_bar, row=2, col=1)
return fig
def add_obi_trace(fig, obi_data: dict):
"""
Add OBI line trace to the third subplot.
Args:
fig: Plotly figure with subplots
obi_data: Dict with timestamp and obi arrays
"""
obi_line = go.Scatter(
x=obi_data["timestamp"],
y=obi_data["obi"],
mode='lines',
name="OBI",
line=dict(color='blue', width=2),
showlegend=False,
hovertemplate="OBI: %{y:.3f}<extra></extra>"
)
# Add horizontal reference line at y=0
fig.add_hline(y=0, line=dict(color='gray', dash='dash', width=1), row=3, col=1)
fig.add_trace(obi_line, row=3, col=1)
return fig
def add_cvd_trace(fig, cvd_data: dict):
"""
Add CVD line trace to the fourth subplot.
Args:
fig: Plotly figure with subplots
cvd_data: Dict with timestamp and cvd arrays
"""
cvd_line = go.Scatter(
x=cvd_data["timestamp"],
y=cvd_data["cvd"],
mode='lines',
name="CVD",
line=dict(color='red', width=2),
showlegend=False,
hovertemplate="CVD: %{y:.1f}<extra></extra>"
)
fig.add_trace(cvd_line, row=4, col=1)
return fig
def create_populated_chart(ohlc_data, metrics_data):
"""
Create a chart container with real data populated.
Args:
ohlc_data: List of OHLC tuples or None
metrics_data: List of Metric objects or None
Returns:
dcc.Graph component with populated data
"""
from data_adapters import format_ohlc_for_plotly, format_volume_for_plotly, format_metrics_for_plotly
# Create base subplot layout
fig = create_empty_subplot_layout()
# Add real data if available
if ohlc_data:
# Format OHLC data
ohlc_formatted = format_ohlc_for_plotly(ohlc_data)
volume_formatted = format_volume_for_plotly(ohlc_data)
# Add OHLC trace
fig = add_ohlc_trace(fig, ohlc_formatted)
# Add Volume trace
fig = add_volume_trace(fig, volume_formatted)
if metrics_data:
# Format metrics data
metrics_formatted = format_metrics_for_plotly(metrics_data)
# Add OBI and CVD traces
if metrics_formatted["obi"]["x"]: # Check if we have OBI data
obi_data = {
"timestamp": metrics_formatted["obi"]["x"],
"obi": metrics_formatted["obi"]["y"]
}
fig = add_obi_trace(fig, obi_data)
if metrics_formatted["cvd"]["x"]: # Check if we have CVD data
cvd_data = {
"timestamp": metrics_formatted["cvd"]["x"],
"cvd": metrics_formatted["cvd"]["y"]
}
fig = add_cvd_trace(fig, cvd_data)
return dcc.Graph(
id="main-charts",
figure=fig,
style={"height": "100vh"},
config={
"displayModeBar": True,
"displaylogo": False,
"modeBarButtonsToRemove": ["select2d", "lasso2d"],
"modeBarButtonsToAdd": ["pan2d", "zoom2d", "zoomIn2d", "zoomOut2d", "resetScale2d"],
"scrollZoom": True,
"doubleClick": "reset+autosize"
}
)