df_plot = df_tech.copy()
df_plot['date'] = pd.to_datetime(df_plot['date'])
df_plot = df_plot.sort_values(['ticker', 'date'])
tickers = df_plot['ticker'].unique()
# Ensure datetime
df_plot['date'] = pd.to_datetime(df_plot['date'])
# Get full date range for padding
min_date = df_plot['date'].min()
max_date = df_plot['date'].max()
date_padding = pd.Timedelta(days=10)
fig = make_subplots(
rows=3, cols=1,
shared_xaxes=True,
vertical_spacing=0.08,
row_heights=[0.5, 0.25, 0.25],
subplot_titles=("Price & Moving Averages", "RSI", "MACD")
)
TRACES_PER_TICKER = 7
visibility_matrix = []
for i, ticker in enumerate(tickers):
data = df_plot[df_plot['ticker'] == ticker]
visible = [False] * (len(tickers) * TRACES_PER_TICKER)
base = i * TRACES_PER_TICKER
for j in range(TRACES_PER_TICKER):
visible[base + j] = True
visibility_matrix.append(visible)
# ================= PRICE =================
fig.add_trace(go.Scatter(
x=data['date'], y=data['close'],
mode='lines',
name='Close',
legendgroup=f'price_{ticker}',
legend='legend1',
visible=(i==0),
hovertemplate="Date: %{x|%Y-%m-%d}<br>Close: %{y:.2f}<extra></extra>"
), row=1, col=1)
fig.add_trace(go.Scatter(
x=data['date'], y=data['SMA50'],
mode='lines',
name='SMA50',
legendgroup=f'price_{ticker}',
legend='legend1',
visible=(i==0),
hovertemplate="Date: %{x|%Y-%m-%d}<br>SMA50: %{y:.2f}<extra></extra>"
), row=1, col=1)
fig.add_trace(go.Scatter(
x=data['date'], y=data['SMA200'],
mode='lines',
name='SMA200',
legendgroup=f'price_{ticker}',
legend='legend1',
visible=(i==0),
hovertemplate="Date: %{x|%Y-%m-%d}<br>SMA200: %{y:.2f}<extra></extra>"
), row=1, col=1)
# ================= RSI =================
fig.add_trace(go.Scatter(
x=data['date'], y=data['RSI'],
mode='lines',
name='RSI',
legendgroup=f'rsi_{ticker}',
legend='legend2',
visible=(i==0),
hovertemplate="Date: %{x|%Y-%m-%d}<br>RSI: %{y:.2f}<extra></extra>"
), row=2, col=1)
# ================= MACD =================
fig.add_trace(go.Scatter(
x=data['date'], y=data['MACD'],
mode='lines',
name='MACD',
legendgroup=f'macd_{ticker}',
legend='legend3',
visible=(i==0),
line=dict(color='blue'),
hovertemplate="Date: %{x|%Y-%m-%d}<br>MACD: %{y:.4f}<extra></extra>"
), row=3, col=1)
fig.add_trace(go.Scatter(
x=data['date'], y=data['MACD_signal'],
mode='lines',
name='Signal',
legendgroup=f'macd_{ticker}',
legend='legend3',
visible=(i==0),
line=dict(color='orange'),
hovertemplate="Date: %{x|%Y-%m-%d}<br>Signal: %{y:.4f}<extra></extra>"
), row=3, col=1)
fig.add_trace(go.Scatter(
x=data['date'], y=data['MACD_hist'],
mode='lines',
name='Histogram',
legendgroup=f'macd_{ticker}',
legend='legend3',
visible=(i==0),
line=dict(color='red', dash='dot'),
hovertemplate="Date: %{x|%Y-%m-%d}<br>Histogram: %{y:.4f}<extra></extra>"
), row=3, col=1)
# Dropdown
buttons = []
for i, ticker in enumerate(tickers):
buttons.append(dict(
label=ticker,
method="update",
args=[
{"visible": visibility_matrix[i]},
{"title": f"{ticker} - Technical Dashboard"}
]
))
# Reference lines
fig.add_hline(y=70, line_dash="dash", row=2, col=1)
fig.add_hline(y=30, line_dash="dash", row=2, col=1)
fig.add_hline(y=0, line_dash="dash", row=3, col=1)
fig.update_layout(
height=1000,
hovermode="x unified",
legend1=dict(x=1.02, y=0.92, xanchor="left"),
legend2=dict(x=1.02, y=0.55, xanchor="left"),
legend3=dict(x=1.02, y=0.18, xanchor="left"),
updatemenus=[dict(
buttons=buttons,
direction="down",
x=0.01,
y=1.08,
xanchor="left",
yanchor="top"
)],
margin=dict(l=60, r=160, t=80, b=60)
)
# Add padding so first & last dates are visible
fig.update_xaxes(
type="date",
range=[min_date - date_padding, max_date + date_padding]
)
fig.show()