import itertools
from datetime import datetime
import pandas as pd
import plotly.graph_objects as go
from dash import dcc, html
from src.constants import COLORS
from src.utils import normalize_prices
def setup_ticker_selection(initial_tickers: list[str]) -> html.Div:
ticker_selection = html.Div(
[
dcc.Dropdown(
id="ticker-selection",
options=[{"label": ticker, "value": ticker} for ticker in initial_tickers],
value=list(initial_tickers),
multi=True,
placeholder="No tickers selected...",
searchable=False,
clearable=False,
persisted_props=[],
style={
"flex": "3",
"fontFamily": "'Courier New', Courier, monospace",
"fontWeight": "bold",
},
),
dcc.Input(
id="ticker-input",
type="text",
placeholder="Enter ticker symbol...",
style={
"flex": "1",
"padding": "10px",
"fontSize": "14px",
"fontFamily": "'Courier New', Courier, monospace",
"borderRadius": "5px",
"border": "1px solid #ccc",
},
n_submit=0,
),
],
style={
"display": "flex",
"gap": "10px",
"marginTop": "10px",
"marginLeft": "80px",
"marginRight": "80px",
},
)
return ticker_selection
def setup_interval_buttons() -> tuple[html.Div, list[str], dict[str, int]]:
button_style = {
"padding": "10px 20px",
"borderRadius": "10px",
"cursor": "pointer",
"fontFamily": "'Courier New', Courier, monospace",
"fontWeight": "bold",
"textAlign": "center",
"marginBottom": "5px",
"marginTop": "5px",
}
interval_buttons_html = html.Div(
[
html.Button("ytd", id="btn-ytd", n_clicks=0, style=button_style),
html.Button("1mo", id="btn-1mo", n_clicks=0, style=button_style),
html.Button("6mo", id="btn-6mo", n_clicks=0, style=button_style),
html.Button("1y", id="btn-1y", n_clicks=0, style=button_style),
html.Button("2y", id="btn-2y", n_clicks=0, style=button_style),
html.Button("3y", id="btn-3y", n_clicks=0, style=button_style),
html.Button("5y", id="btn-5y", n_clicks=0, style=button_style),
html.Button("10y", id="btn-10y", n_clicks=0, style=button_style),
],
style={
"gap": "5px",
"display": "flex",
"flexWrap": "wrap",
"marginLeft": "80px",
"marginRight": "80px",
},
)
interval_buttons_ids = [
"btn-ytd",
"btn-1mo",
"btn-6mo",
"btn-1y",
"btn-2y",
"btn-3y",
"btn-5y",
"btn-10y",
]
interval_offsets = {
"btn-ytd": max(1, (datetime.now() - datetime(datetime.now().year, 1, 1)).days),
"btn-1mo": 30,
"btn-6mo": 182,
"btn-1y": 365,
"btn-2y": 2 * 365,
"btn-3y": 3 * 365,
"btn-5y": 5 * 365,
"btn-10y": 10 * 365,
}
return interval_buttons_html, interval_buttons_ids, interval_offsets
def plot_prices(
timestamps: pd.DatetimeIndex,
prices: pd.DataFrame,
rolling_changes: pd.DataFrame,
idx_range: tuple[int, int],
) -> go.Figure:
idx0, idx1 = idx_range
date_range = [timestamps[idx0], timestamps[idx1]]
prices_normalized = normalize_prices(prices, date_range)
fig = go.Figure()
# rangeslider plot
colors = itertools.cycle(COLORS)
for asset in prices.columns:
fig.add_trace(
go.Scatter(
x=timestamps,
y=rolling_changes[asset],
line=dict(color=next(colors)),
xaxis="x1",
yaxis="y1",
showlegend=False,
)
)
# main plot
colors = itertools.cycle(COLORS)
for asset in prices_normalized.columns:
y_values = 100 * prices_normalized[asset]
formatted_values = [f"{val:+.2f}%" for val in y_values]
fig.add_trace(
go.Scatter(
x=timestamps,
y=y_values,
customdata=formatted_values,
line=dict(width=3, color=next(colors)),
name=asset,
xaxis="x2",
yaxis="y2",
hovertemplate=(
""
"%{customdata} %{fullData.name}"
"
"
"%{x}"
""
),
)
)
# dummy traces to show ticks on the right
for _ in prices_normalized.columns:
fig.add_trace(
go.Scatter(
x=[],
y=[],
xaxis="x2",
yaxis="y3",
showlegend=False,
)
)
# configure axes
xaxis1_dict = dict(rangeslider=dict(visible=True, thickness=0.1), tickangle=-30, nticks=20)
xaxis2_dict = dict(matches="x1", showticklabels=False, nticks=20, showgrid=True)
xaxis1_dict["range"] = date_range # type: ignore
xaxis2_dict["range"] = date_range # type: ignore
yaxis1_dict = dict(showticklabels=False)
yaxis2_dict = dict(
autorange=True,
title="relative price change",
nticks=12,
tickformat="+d",
ticksuffix="%",
ticks="outside",
)
yaxis3_dict = dict(
matches="y2",
overlaying="y2",
side="right",
nticks=12,
tickformat="+d",
ticksuffix="%",
ticks="outside",
)
fig.update_layout(
xaxis1=xaxis1_dict,
yaxis1=yaxis1_dict,
xaxis2=xaxis2_dict,
yaxis2=yaxis2_dict,
yaxis3=yaxis3_dict,
uirevision="constant", # prevent resets from the xrange compression
font=dict(family="Courier New, Monospace", size=14, weight="bold"),
legend=dict(
title=dict(text="Tickers: "),
orientation="h",
x=0.0,
y=1.0,
xanchor="left",
yanchor="bottom",
),
margin=dict(t=50, b=10),
template="plotly",
height=600,
)
return fig