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