I am trying to make stock price candlestick charts in python using plotly dash. The charts are generating correctly except two problems.
a) Suppose I am in 2024. When now I click and drag to past periods 2023 or 2022 or 2021 like that then the charts works well. But when I click and drag and go back to future periods from 2021 to 2022 or 2023 then each time I drag the chart zooms and candles become bigger. Can we control this behaviour?
b) The candlesticks generated initially are very conjusted. Can we make them bigger.
Attached herewith the two codes relevant for these issues. The screenshot of conjusted candles. I can provide the screen recording for zooming issue but here there is no option to attach mp4 file. Please suggest how these issues can be resolved.
First set of code - candlestick charts
# chart_generators/candlestick_chart.py
import plotly.graph_objs as go
from .base_chart import BaseChartGenerator
class CandlestickChartGenerator(BaseChartGenerator):
def generate_chart(self, df, initial_candles=250):
"""
Generate candlestick chart with indicators
:param df: DataFrame with stock data
:param initial_candles: Number of initial candles to display
:return: Plotly Figure object
"""
# Calculate indicators
for indicator in self.indicators:
df = indicator.calculate(df)
#df = self._calculate_indicators(df)
# Slice initial candles
df_window = df.iloc[-initial_candles:]
# Create candlestick trace
traces = [
go.Candlestick(
x=df_window['datetime'],
open=df_window['open'],
high=df_window['high'],
low=df_window['low'],
close=df_window['close'],
name='Price'
)
]
# Add indicator traces
for indicator in self.indicators:
traces.append(
go.Scatter(
x=df_window['datetime'],
y=df_window[indicator.column_name],
mode='lines',
name=indicator.column_name,
line=dict(width=2)
)
)
fig = go.Figure(data=traces)
# Add indicator traces
#traces.extend(self._get_indicator_traces(df_window))
# Calculate y-axis range
y_min = df_window['close'].min() * 0.95
y_max = df_window['close'].max() * 1.05
# Create figure
fig = go.Figure(data=traces)
# Update layout
fig.update_layout(
yaxis_title='Price',
xaxis_rangeslider_visible=False,
height=600,
dragmode='pan',
hovermode='x unified',
margin=dict(t=10, b=30, l=40, r=20),
xaxis=dict(range=[df_window['datetime'].iloc[0], df_window['datetime'].iloc[-1]])
)
# Update y-axis range
fig.update_yaxes(range=[y_min, y_max])
return fig
Second set of code - app_layout
# dashboard/app_layout.py
from dash import Dash, dcc, html, Input, Output, ctx
import pandas as pd
import plotly.graph_objs as go
class StockDashboard:
def __init__(self, data_source, chart_generator):
"""
Initialize Stock Dashboard
:param data_source: Data source object
:param chart_generator: Chart generation object
"""
self.data_source = data_source
self.chart_generator = chart_generator
self.app = Dash(__name__)
self.df = None
self.stock_symbol = None
def load_data(self, stock_symbol, start_date=None, end_date=None):
"""
Load stock data
:param stock_symbol: Stock symbol
:param start_date: Optional start date
:param end_date: Optional end date
"""
self.stock_symbol = stock_symbol
self.df = self.data_source.load_data(stock_symbol, start_date, end_date)
def create_layout(self):
"""Create Dash app layout"""
# Add a check for empty DataFrame
if self.df is None or self.df.empty:
raise ValueError(f"No data loaded for stock symbol: {self.stock_symbol}")
# Modify date picker initialization
start_date = self.df['datetime'].iloc[0] if not self.df.empty else None
end_date = self.df['datetime'].iloc[-1] if not self.df.empty else None
self.app.layout = html.Div([
html.H1(f'{self.stock_symbol} Stock Analysis',
style={'textAlign': 'center', 'marginBottom': '20px'}),
html.Div([
# Date Range Selector
html.Div([
html.Label('Start Date'),
dcc.DatePickerSingle(
id='start-date-picker',
date=self.df['datetime'].iloc[0],
style={'marginBottom': '10px'}
)
], style={'width': '45%', 'display': 'inline-block', 'marginRight': '5%'}),
html.Div([
html.Label('End Date'),
dcc.DatePickerSingle(
id='end-date-picker',
date=self.df['datetime'].iloc[-1],
style={'marginBottom': '10px'}
)
], style={'width': '45%', 'display': 'inline-block'}),
], style={'textAlign': 'center', 'marginBottom': '20px'}),
# Stock Chart
dcc.Graph(
id='stock-chart',
config={
'displayModeBar': True,
'scrollZoom': False
}
),
# Additional Indicators Section
html.Div([
html.H3('Indicator Analysis', style={'textAlign': 'center'}),
dcc.Dropdown(
id='indicator-selector',
options=[
{'label': 'Simple Moving Average', 'value': 'SMA'},
{'label': 'Exponential Moving Average', 'value': 'EMA'},
{'label': 'Relative Strength Index', 'value': 'RSI'}
],
multi=True,
placeholder='Select Indicators'
)
], style={'width': '80%', 'margin': 'auto', 'marginTop': '20px'})
])
def add_callbacks(self):
"""Add Dash app callbacks"""
@self.app.callback(
Output('stock-chart', 'figure'),
[Input('start-date-picker', 'date'),
Input('stock-chart', 'relayoutData')]
)
def update_chart(selected_date, relayout_data):
# Determine the triggered event
triggered_by = ctx.triggered_id
if triggered_by == 'start-date-picker': # Update from date picker
selected_date = pd.to_datetime(selected_date)
df_window = self.df[self.df['datetime'] <= selected_date].iloc[-250:]
elif triggered_by == 'stock-chart' and relayout_data: # Update from drag
if 'xaxis.range[0]' in relayout_data and 'xaxis.range[1]' in relayout_data:
start_date = pd.to_datetime(relayout_data['xaxis.range[0]'])
end_date = pd.to_datetime(relayout_data['xaxis.range[1]'])
df_window = self.df[(self.df['datetime'] >= start_date) & (self.df['datetime'] <= end_date)]
else:
df_window = self.df.iloc[-250:] # Default view
else:
df_window = self.df.iloc[-250:] # Default view
# Generate chart with the windowed dataframe
return self.chart_generator.generate_chart(df_window)
def run(self, debug=False, port=8050):
"""Run the Dash app"""
self.app.run_server(debug=debug, port=port)