from datetime import datetime import time import flask import pandas as pd import dash_table import plotly.graph_objects as go import dash_core_components as dcc import dash_html_components as html from dash import Dash from dash_table.Format import Format from dash.dependencies import Input, Output from plotly.subplots import make_subplots import stockdash_loader as sdl colorway = ["lightslategray", '#FF4F00', '#375CB1', '#FF7400', '#FFF400', '#FF0056'] used_columns = ['symbol', 'shortName', 'sector', 'industry', 'country', 'marketCap', 'enterpriseValue', 'dividendRate', 'trailingPE', 'forwardPE', 'enterpriseToEbitda', 'shortRatio'] PAGE_SIZE = 20 app = Dash(__name__) server = app.server start_time = time.time() print("----- Starting STOCKDASH @ %s -----" % datetime.fromtimestamp(start_time)) kpi_data = sdl.load_from_db(sdl.db_kpi, orderby="marketCap DESC") print("Data loaded after %ss" % (time.time()-start_time)) comp_kpi = kpi_data[used_columns] app.layout = html.Div(children=[ html.Div(className='row', children=[html.Div(className='three columns div-user-controls', children=[ html.H2('STOCKDASH'), html.P('''Visualising data with Plotly - Dash'''), html.P('''Pick one or more KPIs from the dropdown below.'''), html.Div( className='div-for-dropdown', children=[ dcc.Dropdown(id='stockselector', options=[{'label': i, 'value': i} for i in comp_kpi._get_numeric_data().columns], multi=True, value=[comp_kpi._get_numeric_data().columns[0]], style={'backgroundColor': '#1E1E1E'}, className='stockselector') ], style={'color': '#1E1E1E'}), html.P(id="total-stocks"), dcc.Markdown( children=[ "Source: [thiessen.io](https://www.thiessen.io)" ]) ]), html.Div(className='nine columns div-for-charts bg-grey', style={'padding': 0}, children=[ dash_table.DataTable( id='company-kpi-data', columns= [{"name": i, "id": i, 'deletable': True, 'type': 'numeric', 'format': Format(group=',')} if i in comp_kpi._get_numeric_data().columns else {"name": i, "id": i, 'deletable': True} for i in comp_kpi.columns], style_as_list_view=True, style_data_conditional=[{ 'if': {'column_editable': False}, 'backgroundColor': 'rgba(50, 50, 50, 0.5)', 'textAlign': 'left', 'color': 'white', 'padding': 7 }], style_filter_conditional=[{ 'if': {'column_editable': False}, 'backgroundColor': 'rgba(40, 40, 40,0.5)', 'textAlign': 'left', 'color': 'white' }], style_header_conditional=[{ 'if': {'column_editable': False}, 'backgroundColor': 'rgba(30, 30, 30,0.5)', 'textAlign': 'left', 'fontWeight': 'bold', 'color': 'white' }], page_current=0, page_size=PAGE_SIZE, page_action='custom', filter_action='custom', filter_query='', sort_action='custom', sort_mode='multi', sort_by=[] ), dcc.Graph( id='bar-chart-marketcap', className='bg-grey', hoverData={'points': [{'x': 'AAPL'}]}, animate=False), dcc.Graph( id='timeseries-chart-price', className='bg-grey', config={'displayModeBar': False}, animate=False), dcc.Graph( id='recom-bar-chart', className='bg-grey', config={'displayModeBar': False}, animate=False) ]) ]) ]) @app.callback(Output('bar-chart-marketcap', 'figure'), [Input('company-kpi-data', 'data'), Input('stockselector', 'value'), Input('bar-chart-marketcap', 'clickData')]) def update_graph(data, selected_columns, clickData): used_symbols = [x['symbol'] for x in data] figure = go.Figure( layout=go.Layout( colorway=colorway, template='plotly_dark', paper_bgcolor='rgba(0, 0, 0, 0)', plot_bgcolor='rgba(0, 0, 0, 0)', margin={'b': 15}, hovermode='x', autosize=True, title={'text': 'Market Data', 'font': {'color': 'white'}, 'x': 0.5} )) val = dict() val["xaxis"] = dict(domain=[0.15, 0.85]) for i, column in enumerate(selected_columns): i += 1 figure.add_trace(go.Bar(name=column, x=used_symbols, y=[x[column] for x in data], marker_color=['lightslategray',] * len(data), yaxis='y' + str(i), offsetgroup=i)) val["yaxis%s" % i] = dict( title=column, titlefont=dict(color=colorway[i - 1]), tickfont=dict(color=colorway[i - 1]), ) if i == 2: val["yaxis2"].update(dict( anchor="x", overlaying="y", side="right" )) elif i == 3: val["yaxis3"].update(dict( anchor="free", overlaying="y", side="left", position=0.10 )) elif i == 4: val["yaxis4"].update(dict( anchor="free", overlaying="y", side="right", position=0.90 )) figure.update_layout(val) figure.update_yaxes( showgrid=True, zeroline=True, zerolinewidth=1, zerolinecolor='White', ) if clickData is not None: i = 0 for subFig in figure['data']: color=[colorway[i],] * len(data) color[clickData['points'][0]['pointNumber']] = 'crimson' subFig['marker']['color'] = color i = i +1 return figure @app.callback(Output("total-stocks", "children"), [Input('company-kpi-data', 'data')]) def update_total_stocks(data): stocks_picked = len(comp_kpi) return "Total Number of Stocks loaded: %s" % stocks_picked @app.callback(Output('recom-bar-chart', 'figure'), [Input('company-kpi-data', 'data')]) def update_graph(data): used_symbols = [x['symbol'] for x in data] # Modify Recommendation Data where_clause = "symbol IN ('"+"','".join(used_symbols)+"')" rec_data = sdl.load_from_db(sdl.db_rec, where=where_clause) rec_data_mod = pd.concat([rec_data, pd.get_dummies(rec_data['To Grade'], prefix='grade')], axis=1) rec_data_mod.drop(['To Grade', 'From Grade', 'Action'], axis=1, inplace=True) rec_data_mod['Date'] = pd.to_datetime(rec_data_mod['Date']) df2 = rec_data_mod.groupby([pd.Grouper(key='Date', freq='Y'), pd.Grouper('symbol')]).agg(['sum']) df2['Positive'] = df2['grade_Buy'] + df2['grade_Outperform'] + df2['grade_Market Outperform'] + df2[ 'grade_Overweight'] + df2['grade_Positive'] + df2['grade_Strong Buy'] df2['Neutral'] = df2['grade_Equal-Weight'] + df2['grade_Hold'] + df2['grade_Neutral'] df2['Negative'] = df2['grade_Market Underperform'] + df2['grade_Reduce'] + df2['grade_Sell'] + df2[ 'grade_Underweight'] columns = ['Positive', 'Neutral', 'Negative'] rec_data_mod = df2[columns] df = rec_data_mod.loc['2020-12-31'].reset_index() df_tmp = df.loc[df['symbol'].isin(used_symbols)] figure = go.Figure(layout=go.Layout( colorway=colorway, template='plotly_dark', paper_bgcolor='rgba(0, 0, 0, 0)', plot_bgcolor='rgba(0, 0, 0, 0)', margin={'b': 15}, hovermode='x', autosize=True, title={'text': 'Recommendation Data', 'font': {'color': 'white'}, 'x': 0.5}, barmode='stack' )) figure.add_trace( go.Bar(x=used_symbols, y=df_tmp['Positive'].tolist(), name='Positive Outlook', marker_color='#41B3A3')) figure.add_trace( go.Bar(x=used_symbols, y=df_tmp['Neutral'].tolist(), name='Neutral Outlook', marker_color='#E8A87C')) figure.add_trace( go.Bar(x=used_symbols, y=df_tmp['Negative'].tolist(), name='Negative Outlook', marker_color='#E27D60')) return figure @app.callback(Output('timeseries-chart-price', 'figure'), Input('bar-chart-marketcap', 'clickData'), Input('company-kpi-data', 'data')) def update_graph(clickData, kpi_data): if clickData is None: used_symbol = kpi_data[0]['symbol'] else: used_symbol = clickData['points'][0]['x'] where_clause = "symbol = '%s'" % used_symbol his_data = sdl.load_from_db(sdl.db_his, where=where_clause, limit=1000, orderby="Date DESC") where_clause = "symbol = '%s' and Date >= '%s'" % (used_symbol, his_data['Date'].min()) div_data = sdl.load_from_db(sdl.db_div, where=where_clause, orderby="Date DESC") # Calculate rolling window his_data['priceMA50'] = his_data['Close'].rolling(window=50, min_periods=1).mean() std_dev = his_data['Close'].rolling(window=50, min_periods=1).std() his_data['priceMA50_lstd'] = his_data['priceMA50'] - 2*std_dev his_data['priceMA50_hstd'] = his_data['priceMA50'] + 2*std_dev his_data['priceMA200'] = his_data['Close'].rolling(window=200, min_periods=1).mean() his_data['diffMA50_200'] = his_data['priceMA50'] - his_data['priceMA200'] fig = make_subplots(rows=3, cols=1, row_heights=[0.7, 0.2, 0.1], shared_xaxes=True, vertical_spacing=0.07) fig.add_trace(go.Candlestick(x=his_data['Date'], open=his_data['Open'], high=his_data['High'], low=his_data['Low'], close=his_data['Close'], name=used_symbol), row=1, col=1) columns = ['priceMA50', 'priceMA200'] for column in columns: fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data[column], mode='lines', opacity=0.7, name=used_symbol + "-" + column, textposition='bottom center'), row=1, col=1) fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data['priceMA50_lstd'], mode='lines', opacity=0.7, line=dict(color='#ffdd70', width=1, dash='dash'), name=used_symbol + "-" + 'Lower Band', textposition='bottom center'), row=1, col=1) fig.add_trace(go.Scatter(x=his_data['Date'], y=his_data['priceMA50_hstd'], mode='lines', opacity=0.7, line=dict(color='#ffdd70', width=1, dash='dash'), name=used_symbol + "-" + 'Higher Band', textposition='bottom center'), row=1, col=1) fig.update_yaxes(showgrid=True, zeroline=False, showspikes=True, spikemode='across', spikesnap='cursor', showline=False, spikedash='solid') fig.update_xaxes(showgrid=True, zeroline=False, rangeslider_visible=False, showticklabels=False, showspikes=True, spikemode='across', spikesnap='cursor', showline=False, spikedash='solid', rangebreaks=[ dict(bounds=["sat", "mon"]), #hide weekends #dict(values=["2015-12-25", "2016-01-01"]) # hide Christmas and New Year's ]) fig.update_layout( colorway=colorway, template='plotly_dark', paper_bgcolor='rgba(0, 0, 0, 0)', plot_bgcolor='rgba(0, 0, 0, 0)', autosize=True, height=800, hovermode='x unified', hoverlabel=dict(font=dict(color='black')), title={'text': 'Stock Prices', 'font': {'color': 'white'}, 'x': 0.5}, xaxis={'range': [his_data['Date'].min(), his_data['Date'].max()], 'showticklabels': True, 'rangeselector_bgcolor':'rgba(0, 22, 0, 0)', 'rangeselector': dict( buttons=list([ dict(count=1, label="1m", step="month", stepmode="backward"), dict(count=6, label="6m", step="month", stepmode="backward"), dict(count=1, label="YTD", step="year", stepmode="todate"), dict(count=1, label="1y", step="year", stepmode="backward"), dict(step="all") ]))}, yaxis1={'autorange': True, 'fixedrange': False}, legend=dict(y=1, x=0), dragmode='pan') fig.add_trace( go.Bar(x=his_data['Date'], y=his_data['Volume'], marker_color='#3399ff', name='Volume'), row=2, col=1) fig.add_trace( go.Scatter(x=div_data['Date'], y=div_data['Dividends'], marker_color='#fae823', name='Dividends', line=dict( shape='hv' )), row=3, col=1) return fig def split_filter_part(filter_part): operators = [['ge ', '>='], ['le ', '<='], ['lt ', '<'], ['gt ', '>'], ['ne ', '!='], ['eq ', '='], ['contains '], ['datestartswith ']] for operator_type in operators: for operator in operator_type: if operator in filter_part: name_part, value_part = filter_part.split(operator, 1) name = name_part[name_part.find('{') + 1: name_part.rfind('}')] value_part = value_part.strip() v0 = value_part[0] if v0 == value_part[-1] and v0 in ("'", '"', '`'): value = value_part[1: -1].replace('\\' + v0, v0) else: try: value = float(value_part) except ValueError: value = value_part return name, operator_type[0].strip(), value return [None] * 3 @app.callback( Output('company-kpi-data', "data"), Input('company-kpi-data', "page_current"), Input('company-kpi-data', "page_size"), Input('company-kpi-data', "sort_by"), Input('company-kpi-data', 'filter_query')) def update_table(page_current, page_size, sort_by, filter): filtering_expressions = filter.split(' && ') dff = comp_kpi for filter_part in filtering_expressions: col_name, operator, filter_value = split_filter_part(filter_part) if operator in ('eq', 'ne', 'lt', 'le', 'gt', 'ge'): dff = dff.loc[getattr(dff[col_name], operator)(filter_value)] elif operator == 'contains': dff = dff.loc[dff[col_name].str.contains(filter_value)] elif operator == 'datestartswith': dff = dff.loc[dff[col_name].str.startswith(filter_value)] if len(sort_by): dff = dff.sort_values( [col['column_id'] for col in sort_by], ascending=[ col['direction'] == 'asc' for col in sort_by ], inplace=False ) page = page_current size = page_size return dff.iloc[page * size: (page + 1) * size].to_dict('records') app.run_server(debug=True, port=18051, threaded=True)