diff --git a/indicators.py b/indicators.py index b2db4fe..7d29670 100644 --- a/indicators.py +++ b/indicators.py @@ -21,20 +21,13 @@ Could also come up with a value that ties to the trading volume. import pandas as pd import numpy as np import datetime as dt -# from util import get_data, get_price_volume, plot_data, get_watchlist -from util import get_price_volume, plot_data, get_watchlist -# from marketsim import compute_portvals, compute_portfolio_stats, normalize_data -# import matplotlib.pyplot as plt -# import matplotlib +from util import get_watchlist from numpy.fft import fft, ifft import scipy.signal as sig import plotly.express as px from plotly.subplots import make_subplots -# import plotly.graph_objects as go from dash import Dash, html, dcc, callback, Output, Input, State, no_update from waitress import serve -import json -import io from flask_caching import Cache from dash.exceptions import PreventUpdate import dash_auth @@ -106,7 +99,6 @@ def smooth(price, hsize=10, sigma=3): return data - class security: """ This can be a list of stocks, bonds, or otherinvestment vehicles. @@ -350,100 +342,6 @@ class security: norm_hist = macd_hist.div(macd_long.values) return macd_line, macd_sig, macd_hist, norm_hist - -# def bollinger_sell(stock, wd=200): -# """ -# Parameters -# ---------- -# stock : TYPE class 'serurity' -# wd : TYPE, int, optional -# DESCRIPTION. Moving average windows. The default is 200. - -# Returns -# ------- -# TYPE DataFrame -# DESCRIPTION - +1 when stock price is above bollinger upper band -# . -1 when vice versa. transition days are of value +3 and -3 -# respectively. A value of -3 is a sell signal -# """ -# _, bol_up = stock.bollinger(wd) -# # bol_up = bol_up[bol_up.columns[-1]].to_frame() -# # bol_up = bol_up.iloc[:, [-1]] -# sell = np.sign(stock.price.sub(bol_up.values)) -# sell_diff = sell.diff() - -# return sell.add(sell_diff.values) - - -# def bollinger_buy(stock, wd=200): -# """ -# Parameters -# ---------- -# stock : TYPE class 'serurity' -# wd : TYPE, int, optional -# DESCRIPTION. Moving average windows. The default is 200. - -# Returns -# ------- -# TYPE DataFrame -# DESCRIPTION - +1 when stock price is above bollinger lower band -# . -1 when vice versa. transition days are of value +3 and -3 -# respectively. A value of +3 is a buy signal -# """ -# bol_low, _ = stock.bollinger(wd) -# buy = np.sign(stock.price.sub(bol_low.values)) -# buy_diff = buy.diff() - -# return buy.add(buy_diff.values) - -# def simple_bollinger_strategy(stk): - -# # buy orders -# buy = bollinger_buy(stk, 190) -# buy_orders = buy[np.any(buy>2, axis=1)] - -# sell = bollinger_sell(stk, 190) -# sell_orders = sell[np.any(sell<-2, axis=1)] - -# orders = pd.concat([buy_orders, sell_orders]) -# orders = orders.sort_index() - -# order_list = pd.DataFrame(columns = ['Date', 'Symbol', 'Order', 'Shares']) -# for index, row in orders.iterrows(): -# for sym in orders.columns.values: -# if row[sym] > 2: # buy order -# order_list = order_list.append({'Date' : index, 'Symbol' : sym, -# 'Order' : 'BUY', 'Shares' : 100}, ignore_index = True ) -# elif row[sym] < -2: # sell order -# order_list = order_list.append({'Date' : index, 'Symbol' : sym, -# 'Order' : 'SELL', 'Shares' : 100}, ignore_index = True ) -# order_list = order_list.set_index('Date') - -# return order_list - -# def plot_against_sym(df, sym=['SPY']): -# df_temp = df.copy() -# df_sym = get_data(sym, pd.to_datetime(df_temp.index.values), addSPY=False) -# df_temp[sym[0]] = df_sym.values -# df_temp = normalize_data(df_temp) -# plot_data(df_temp) -# return df_sym - - -# def test_bollinger_sell(): -# sd = dt.datetime(2010,1,1) -# # ed = dt.datetime.today() -# ed = dt.datetime(2012,12,31) -# symbol = ['XOM'] -# dates = dates = pd.date_range(sd, ed) -# prices = get_data(symbol, dates, addSPY=False) -# # prices = prices.dropna() -# stk = security(prices) -# sell = bollinger_sell(stk) -# plot_data(sell) -# buy = bollinger_buy(stk, 190) -# plot_data(buy) - def get_crossing(stocks): """ Parameters @@ -488,49 +386,9 @@ def get_sma_slope(stocks, wd = 50): return slope - -# def modified_bollinger_strategy(stk): - -# rsi = stk.rsi() -# btemp = pd.DataFrame() -# # buy orders -# buy = bollinger_buy(stk, 190) -# buy_orders = buy[np.any(buy>2, axis=1)] -# for col in buy_orders.columns: -# buy_stk = buy_orders[col] -# buy_stk = buy_stk[buy_stk > 2] -# buy_stk = buy_stk[rsi[col].loc[buy_stk.index] < 70] -# btemp = btemp.join(buy_stk, how='outer') - -# stemp = pd.DataFrame() -# sell = bollinger_sell(stk, 190) -# sell_orders = sell[np.any(sell<-2, axis=1)] -# for col in sell_orders.columns: -# sell_stk = sell_orders[col] -# sell_stk = sell_stk[sell_stk < -2] -# sell_stk = sell_stk[rsi[col].loc[sell_stk.index] > 30] -# stemp = stemp.join(sell_stk, how='outer') - -# orders = pd.concat([btemp, stemp]) - -# # TODO - refine orders based on slope -# # TODO - revine further based on other conditions (RSI, MACD) -# # TODO - transaction shares determination - -# orders = orders.sort_index() - -# order_list = pd.DataFrame(columns = ['Date', 'Symbol', 'Order', 'Shares']) -# for index, row in orders.iterrows(): -# for sym in orders.columns.values: -# if row[sym] > 2: # buy order -# order_list = order_list.append({'Date' : index, 'Symbol' : sym, -# 'Order' : 'BUY', 'Shares' : 100}, ignore_index = True ) -# elif row[sym] < -2: # sell order -# order_list = order_list.append({'Date' : index, 'Symbol' : sym, -# 'Order' : 'SELL', 'Shares' : 100}, ignore_index = True ) -# order_list = order_list.set_index('Date') - -# return order_list +LB_YEAR = 3 # years of stock data to retrieve +# PLT_YEAR = 2 # number of years data to plot +LB_TRIGGER = 5 # days to lookback for triggering events def intelligent_loop_plots(sym, stk_data): # Only plot ones that are standing out meaning: @@ -563,25 +421,25 @@ def intelligent_loop_plots(sym, stk_data): # print('{:5}: '.format(sym), end = '') # RSI outside window (over bought / over sold) - rsi_tail = rsi.tail(lb_trigger) + rsi_tail = rsi.tail(LB_TRIGGER) if (rsi_tail >= 70).any() or (rsi_tail <= 30).any(): # print('--RSI', end = '') plot_indicator += 'RSI, ' # VoRSI outside window (over bought / over sold) - vorsi_tail = vorsi.tail(lb_trigger) + vorsi_tail = vorsi.tail(LB_TRIGGER) if (vorsi_tail >= 70).any() or (vorsi_tail <= 30).any(): # print('--VoRSI', end = '') plot_indicator += 'VoRSI, ' # Normalized MACD histogram out of 3% range - norm_hist_tail = abs(norm_hist.tail(lb_trigger)) + norm_hist_tail = abs(norm_hist.tail(LB_TRIGGER)) if (abs(norm_hist_tail) >= 0.02).any(): # print('--MACD/R', end = '') # outside normal range plot_indicator += 'MACD/R, ' # MACD histogram zero crossing - macd_hist_tail = macd_hist.tail(lb_trigger) + macd_hist_tail = macd_hist.tail(LB_TRIGGER) macd_hist_sign = np.sign(macd_hist_tail) macd_hist_diff = macd_hist_sign.diff() if (abs(macd_hist_diff) > 1).any(): @@ -589,7 +447,7 @@ def intelligent_loop_plots(sym, stk_data): plot_indicator += 'MACD, ' # Stock price crosses SMA50 - sma50_cross_tail = sma50.tail(lb_trigger) - price.tail(lb_trigger) + sma50_cross_tail = sma50.tail(LB_TRIGGER) - price.tail(LB_TRIGGER) sma50_cross_sign = np.sign(sma50_cross_tail) sma50_cross_diff = sma50_cross_sign.diff() if (abs(sma50_cross_diff) > 1).any(): @@ -597,7 +455,7 @@ def intelligent_loop_plots(sym, stk_data): plot_indicator += 'SMA50, ' # Death cross or golden cross - SMA50 vs SMA200 - sma_cross_tail = sma50.tail(lb_trigger) - sma200.tail(lb_trigger).values + sma_cross_tail = sma50.tail(LB_TRIGGER) - sma200.tail(LB_TRIGGER).values sma_cross_sign = np.sign(sma_cross_tail) sma_cross_diff = sma_cross_sign.diff() if (abs(sma_cross_diff) > 1).any(): @@ -605,9 +463,9 @@ def intelligent_loop_plots(sym, stk_data): plot_indicator += 'Golden/Death, ' # Price outside bollinger band or crossing - price_tail = price.tail(lb_trigger) - bol_low_tail = bol_low.tail(lb_trigger) - bol_up_tail = bol_up.tail(lb_trigger) + price_tail = price.tail(LB_TRIGGER) + bol_low_tail = bol_low.tail(LB_TRIGGER) + bol_up_tail = bol_up.tail(LB_TRIGGER) price_high = price_tail - bol_up_tail.values price_low = price_tail - bol_low_tail.values if (price_high >= 0).any() or (price_low <= 0).any(): @@ -615,7 +473,7 @@ def intelligent_loop_plots(sym, stk_data): plot_indicator += 'Bollinger, ' # Price cross 200 day moving average - sma200_tail = sma200.tail(lb_trigger) + sma200_tail = sma200.tail(LB_TRIGGER) sma200_cross = price_tail - sma200_tail.values sma200_cross_sign = np.sign(sma200_cross) sma200_cross_diff = sma200_cross_sign.diff() @@ -624,7 +482,7 @@ def intelligent_loop_plots(sym, stk_data): plot_indicator += 'SMA200, ' # Large trading volume trigger - volume_tail = vol.tail(lb_trigger) + volume_tail = vol.tail(LB_TRIGGER) vol_mean = vol.tail(50).mean() vol_std = vol.tail(50).std() if ((volume_tail[1] - vol_mean - 2*vol_std) > 0).any(): @@ -667,214 +525,206 @@ def user_login(username, password): return True return False -if __name__ == "__main__": +# Initialize the app +app = Dash() +dash_auth.BasicAuth( + app, + # VALID_USERNAME_PASSWORD_PAIRS, + auth_func=user_login, + secret_key="MK8dyS6PyDDuEuzrmqa7dJTJZ7eH2Jkh", +) - lb_year = 3 # years of stock data to retrieve - plt_year = 2 # number of years data to plot - lb_trigger = 5 # days to lookback for triggering events +watchlist = get_watchlist() +# symbols = watchlist.index.values.tolist() +symbols = (watchlist.index.values + " - " + watchlist["Sub Segment"]).tolist() - # Initialize the app - app = Dash() - dash_auth.BasicAuth( - app, - # VALID_USERNAME_PASSWORD_PAIRS, - auth_func=user_login, - secret_key="MK8dyS6PyDDuEuzrmqa7dJTJZ7eH2Jkh", - ) +CACHE_CONFIG = {'CACHE_TYPE': 'SimpleCache'} +cache = Cache() +cache.init_app(app.server, config=CACHE_CONFIG) - watchlist = get_watchlist() - # symbols = watchlist.index.values.tolist() - symbols = (watchlist.index.values + " - " + watchlist["Sub Segment"]).tolist() +@cache.memoize(timeout=14400) # cache timeout set to 4 hours +def fetch_stk_data(sym, sd): + return si.get_data(sym, start_date=sd)[["adjclose", "volume"]] - CACHE_CONFIG = {'CACHE_TYPE': 'SimpleCache'} - cache = Cache() - cache.init_app(app.server, config=CACHE_CONFIG) - - @cache.memoize(timeout=14400) # cache timeout set to 4 hours - def fetch_stk_data(sym, sd): - return si.get_data(sym, start_date=sd)[["adjclose", "volume"]] - - # App layout - app.layout = [ +# App layout +app.layout = [ + html.Div([ + html.Button('Reload', id="reload-button", n_clicks=0, + style={'font-size': '12px', 'width': '120px', 'display': 'inline-block', 'margin-bottom': '10px', 'margin-right': '5px', 'height':'36px', 'verticalAlign': 'top'}), html.Div([ - html.Button('Reload', id="reload-button", n_clicks=0, - style={'font-size': '12px', 'width': '120px', 'display': 'inline-block', 'margin-bottom': '10px', 'margin-right': '5px', 'height':'36px', 'verticalAlign': 'top'}), - html.Div([ - dcc.Dropdown(symbols, symbols[0], id='symbols_dropdown_list',), - ], style={'width': '330px', 'text-align': 'center'}), - html.Button('Auto Play', id="start-button", n_clicks=0, - style={'font-size': '12px', 'width': '120px', 'display': 'inline-block', 'margin-bottom': '10px', 'margin-right': '5px', 'height':'36px', 'verticalAlign': 'top'}), - ], style={'display':'flex', 'justify-content':'center'}), + dcc.Dropdown(symbols, symbols[0], id='symbols_dropdown_list',), + ], style={'width': '330px', 'text-align': 'center'}), + html.Button('Auto Play', id="start-button", n_clicks=0, + style={'font-size': '12px', 'width': '120px', 'display': 'inline-block', 'margin-bottom': '10px', 'margin-right': '5px', 'height':'36px', 'verticalAlign': 'top'}), + ], style={'display':'flex', 'justify-content':'center'}), - dcc.Graph( - figure={}, - id='controls-and-graph', - style={'height':'85vh'} - ), - # dcc.Store(id="signal"), - # dcc.Store(id="dropdown_index", data='0'), - dcc.Interval( - id='interval-component', - interval=3*1000, # in milliseconds - n_intervals=0, - max_intervals=2*len(symbols), - # max_intervals=3, # for testing - disabled=True, - ), - ] + dcc.Graph( + figure={}, + id='controls-and-graph', + style={'height':'85vh'} + ), + # dcc.Store(id="signal"), + # dcc.Store(id="dropdown_index", data='0'), + dcc.Interval( + id='interval-component', + interval=3*1000, # in milliseconds + n_intervals=0, + max_intervals=2*len(symbols), + # max_intervals=3, # for testing + disabled=True, + ), +] - app.clientside_callback( - """ - function(id) { - document.addEventListener("keyup", function(event) { - if (event.key == ' ') { - document.getElementById('start-button').click() - event.stopPropogation() - } - }); - return window.dash_clientside.no_update - } - """, - Output("start-button", "id"), - Input("start-button", "id") - ) +app.clientside_callback( + """ + function(id) { + document.addEventListener("keyup", function(event) { + if (event.key == ' ') { + document.getElementById('start-button').click() + event.stopPropogation() + } + }); + return window.dash_clientside.no_update + } + """, + Output("start-button", "id"), + Input("start-button", "id") +) - # start / stop button callback - @callback(Output('interval-component', 'disabled'), - Output("start-button", "children"), - Output('symbols_dropdown_list', 'disabled'), - Input("start-button", "n_clicks"), - State("start-button", "children"),) - - def start_cycle(n, value): - if n: - if value == "Pause": - return True, "Auto Play", False - else: - return False, "Pause", True - return no_update +# start / stop button callback +@callback(Output('interval-component', 'disabled'), + Output("start-button", "children"), + Output('symbols_dropdown_list', 'disabled'), + Input("start-button", "n_clicks"), + State("start-button", "children"),) - # reload button callback - @callback(Output('symbols_dropdown_list', 'options'), - Output('interval-component', 'n_intervals'), - Input("reload-button", "n_clicks"),) - - def reload_syms(n): - if n: - watchlist = get_watchlist() - symbols = (watchlist.index.values + " - " + watchlist["Sub Segment"]).tolist() - return symbols, 0 - return no_update - - # interval callback - @callback(Output('symbols_dropdown_list', 'value'), - Input("interval-component", "n_intervals"), - State('symbols_dropdown_list', 'options'), - # State('dropdown-index', 'data'), - ) - def cycle_syms(n, syms): - if syms: - return syms[n % len(syms)] +def start_cycle(n, value): + if n: + if value == "Pause": + return True, "Auto Play", False else: - return no_update + return False, "Pause", True + return no_update - # dropdown callback - @callback( - Output(component_id='controls-and-graph', component_property='figure'), - # Output("dropdown-index", "data"), - Input(component_id='symbols_dropdown_list', component_property='value'), - # State('symbols_dropdown_list', 'options') - # Input(component_id='signal', component_property='data'), +# reload button callback +@callback(Output('symbols_dropdown_list', 'options'), + Output('interval-component', 'n_intervals'), + Input("reload-button", "n_clicks"),) + +def reload_syms(n): + if n: + watchlist = get_watchlist() + symbols = (watchlist.index.values + " - " + watchlist["Sub Segment"]).tolist() + return symbols, 0 + return no_update + +# interval callback +@callback(Output('symbols_dropdown_list', 'value'), + Input("interval-component", "n_intervals"), + State('symbols_dropdown_list', 'options'), + # State('dropdown-index', 'data'), + ) +def cycle_syms(n, syms): + if syms: + return syms[n % len(syms)] + else: + return no_update + +# dropdown callback +@callback( + Output(component_id='controls-and-graph', component_property='figure'), + # Output("dropdown-index", "data"), + Input(component_id='symbols_dropdown_list', component_property='value'), + # State('symbols_dropdown_list', 'options') + # Input(component_id='signal', component_property='data'), +) + +def update_graph(col_chosen): + if not col_chosen: + raise PreventUpdate + # return no_update + + sym = col_chosen.split()[0] + end_date = dt.datetime.today() + start_date = end_date - dt.timedelta(days = 365 * LB_YEAR) + # plot_sd = end_date - dt.timedelta(days = 365 * plt_year) + # plot_ed = end_date + tmp = fetch_stk_data(sym, start_date) # cached function all + data, vol, macd, rsi, plot_ind = intelligent_loop_plots(sym, tmp) + + fig = make_subplots( + rows=3, + cols=1, + shared_xaxes=True, + row_heights=[0.6, 0.2, 0.2], + vertical_spacing=0.02, + specs=[[{"secondary_y": True}],[{"secondary_y": False}],[{"secondary_y": False}]], ) - def update_graph(col_chosen): - if not col_chosen: - raise PreventUpdate - # return no_update - - # get data - # tmp = si.get_data(sym, start_date=sd)[["adjclose", "volume"]] - sym = col_chosen.split()[0] - end_date = dt.datetime.today() - start_date = end_date - dt.timedelta(days = 365 * lb_year) - # plot_sd = end_date - dt.timedelta(days = 365 * plt_year) - # plot_ed = end_date - tmp = fetch_stk_data(sym, start_date) # cached function all - data, vol, macd, rsi, plot_ind = intelligent_loop_plots(sym, tmp) - - fig = make_subplots( - rows=3, - cols=1, - shared_xaxes=True, - row_heights=[0.6, 0.2, 0.2], - vertical_spacing=0.02, - specs=[[{"secondary_y": True}],[{"secondary_y": False}],[{"secondary_y": False}]], - ) - - price_line = px.line( - data, - x=data.index, - y=data.columns.to_list(), - # y=[sym, sym+'_BOL200L', sym+'_BOL200U', sym+'_SMA200', sym+'_SMA50', sym+'_WVMA50'], - ) - - volume_line = px.bar( - vol, - x=vol.index, - y=vol.columns.to_list(), - ) - - macd_line = px.line( - macd, - x=macd.index, - y=['_MACD', '_SIG'], - ) - - macd_neg = macd.copy() - macd_pos = macd.copy() - macd_neg[macd_neg>0] = 0 - macd_pos[macd_pos<0] = 0 - - macd_hist_pos = px.line( - macd_pos, - x=macd_pos.index, - y=['_HIST'], - ) - macd_hist_pos.update_traces(fill='tozeroy', line_color='rgba(0,100,0,0.5)', showlegend=False) - - macd_hist_neg = px.line( - macd_neg, - x=macd_neg.index, - y=['_HIST'], - ) - macd_hist_neg.update_traces(fill='tozeroy', line_color='rgba(100,0,0,0.5)', showlegend=False) - - rsi_line = px.line( - rsi, - x=rsi.index, - y=['_RSI', '_VoRSI'], - ) - fig.add_traces(price_line.data + volume_line.data, rows=1, cols=1, secondary_ys=[True, True, True, True, True, True, False]) - fig.add_traces(macd_line.data + macd_hist_pos.data + macd_hist_neg.data, rows=2, cols=1) - fig.add_traces(rsi_line.data, rows=3, cols=1) - - # fig.update_traces(marker_color = 'rgba(0,0,250,0.5)', - # marker_line_width = 0, - # selector=dict(type="bar"), - - fig.layout.yaxis.title="Volume" - fig.layout.yaxis2.title="Price ($)" - fig.layout.yaxis2.type="log" - fig.layout.yaxis3.title="MACD" - fig.layout.yaxis4.title="RSI/VoRSI" - fig.layout.yaxis.griddash="dash" - - fig.update_layout(title_text=sym+' - '+plot_ind, title_x=0.5, - margin=dict(l=30, r=20, t=50, b=20), - ) - # fig.update_layout(showlegend=False) - - return fig + price_line = px.line( + data, + x=data.index, + y=data.columns.to_list(), + ) - serve(app.server, host="0.0.0.0", port=8050, threads=7) + volume_line = px.bar( + vol, + x=vol.index, + y=vol.columns.to_list(), + ) + + macd_line = px.line( + macd, + x=macd.index, + y=['_MACD', '_SIG'], + ) + + macd_neg = macd.copy() + macd_pos = macd.copy() + macd_neg[macd_neg>0] = 0 + macd_pos[macd_pos<0] = 0 + + macd_hist_pos = px.line( + macd_pos, + x=macd_pos.index, + y=['_HIST'], + ) + macd_hist_pos.update_traces(fill='tozeroy', line_color='rgba(0,100,0,0.5)', showlegend=False) + + macd_hist_neg = px.line( + macd_neg, + x=macd_neg.index, + y=['_HIST'], + ) + macd_hist_neg.update_traces(fill='tozeroy', line_color='rgba(100,0,0,0.5)', showlegend=False) + + rsi_line = px.line( + rsi, + x=rsi.index, + y=['_RSI', '_VoRSI'], + ) + fig.add_traces(price_line.data + volume_line.data, rows=1, cols=1, secondary_ys=[True, True, True, True, True, True, False]) + fig.add_traces(macd_line.data + macd_hist_pos.data + macd_hist_neg.data, rows=2, cols=1) + fig.add_traces(rsi_line.data, rows=3, cols=1) + + # fig.update_traces(marker_color = 'rgba(0,0,250,0.5)', + # marker_line_width = 0, + # selector=dict(type="bar"), + + fig.layout.yaxis.title="Volume" + fig.layout.yaxis2.title="Price ($)" + fig.layout.yaxis2.type="log" + fig.layout.yaxis3.title="MACD" + fig.layout.yaxis4.title="RSI/VoRSI" + fig.layout.yaxis.griddash="dash" + + fig.update_layout(title_text=sym+' - '+plot_ind, title_x=0.5, + margin=dict(l=30, r=20, t=50, b=20), + ) + # fig.update_layout(showlegend=False) + + return fig + +if __name__ == "__main__": + serve(app.server, host="0.0.0.0", port=8050, threads=7) # using production quality WSGI server Waitress # app.run(debug=True)