Files
stockdash/stockdash_render.py
2021-02-01 21:19:33 +01:00

409 lines
18 KiB
Python

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)