# -*- coding: utf-8 -*- """ TODO: given a list of symbols, for each stock, plot Subplot1: 1. price 2. 50 and 200 day SMA lines 3. bollinger band (200 day) Subplot2 RSI Subplot3 MACD TODO: validate the plots with online resource Created on Mon Feb 17 14:50:17 2020 Use one as buy/sell trigger and verify a bag of indicators to make final decision. Could also come up with a value that ties to the trading volume. @author: thomwang """ 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 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 yahoo_fin.stock_info as si pd.options.mode.chained_assignment = None # default='warn' # def fill_missing_data(df): # df.ffill(inplace=True) # df.bfilln(inplace=True) def fft_convolve(signal, window): fft_signal = fft(signal) fft_window = fft(window) return ifft(fft_signal * fft_window) def zero_pad(array, n): """Extends an array with zeros. array: numpy array n: length of result returns: new NumPy array """ res = np.zeros(n) res[: len(array)] = array return res def smooth(price, hsize=10, sigma=3): """ Parameters ---------- price : TYPE DataFrame. DESCRIPTION - with time index and no invalid values hsize : TYPE integer DESCRIPTION - this adds phase delay. similar to SMA window sigma : TYPE float DESCRIPTION - gaussian standard deviation affects smoothness Returns ------- TYPE DataFrame DESCRIPTION - smoothed price Doesn't offer much benefit over sma. Only theoretical values. For future different smooth functiona experiments """ data = price.copy() window = sig.gaussian(M=hsize, std=sigma) window /= window.sum() padded = zero_pad(window, data.shape[0]) for col in data.columns: ys = data[col].values smooth = abs(fft_convolve(ys, padded)) smooth[0:hsize-1] = np.nan data[col] = smooth return data class security: """ This can be a list of stocks, bonds, or otherinvestment vehicles. price - Pandas DataFrame with datetime as index sorted to chronical order """ def __init__(self, sym, price, volume=None, rfr: float = 0.01, sf: float = 252.0): """ Parameters ---------- price : TYPE pandas.DataFrame DESCRIPTION. historical adj. daily close prices of stocks under consideration volume : TYPE pandas.DataFrame DESCRIPTION. daily trading volume. The default is none. rfr : TYPE float, optional DESCRIPTION. annualized risk free rate. The default is 0.01. sf : TYPE sample frequency, optional DESCRIPTION. The default is 252 (daily). there are 252 trading days in a year. Monthly sampling frequency would be 12. And weekly sampling frequenc is 52. """ self._symbol = sym self._price = price self._volume = volume # self._symbol = price.columns.values self._rfr = rfr self._sf = sf @property def symbol(self): return self._symbol @symbol.setter def symbol(self, value): raise AttributeError('security symbol is read only') @property def price(self): return self._price @price.setter def price(self, value): raise AttributeError('security price is read only') @property def volume(self): if self._volume is None: raise ValueError('trading volume information not available') return self._volume @volume.setter def volume(self, value): raise AttributeError('security volume is read only') def sma(self, window): return self.price.rolling(window).mean() def vwma(self, window): """ Volume weighted moving average. When plotted against sma, it gives an early indicator when VWMA crosses SMA. When VWMA is above SMA, it indicates a strong upward trend and vice versa. """ price_vol = self.price * self.volume return price_vol.rolling(window).sum() / self.volume.rolling(window).sum() def vosma(self, window): return self.volume.rolling(window).mean() def ema(self, window): # default to 14 day window # EMA pre-process the first point price = self.price temp = price.iloc[0:window].mean() price.iloc[window-1] = temp price.iloc[0:(window-1)] = np.nan # process the EMA avg = price.ewm(span=window, adjust=False).mean() return avg def voema(self, window): # default to 14 day window # EMA pre-process the first point vol = self.volume temp = vol.iloc[0:window].mean() vol.iloc[window-1] = temp vol.iloc[0:(window-1)] = np.nan # process the EMA avg = vol.ewm(span=window, adjust=False).mean() return avg def rsi(self, window = 14): """ Traditional interpretation and usage of the RSI are that values of 70 or above indicate that a security is becoming overbought or overvalued and may be primed for a trend reversal or corrective pullback in price. An RSI reading of 30 or below indicates an oversold or undervalued condition. """ # use exponential averaging d_chg = self.price.diff() d_up, d_dn = d_chg.copy(), d_chg.copy() d_up[d_up < 0] = 0 d_dn[d_dn > 0] = 0 # EMA pre-process the first point temp = d_up.iloc[1:(window+1)].mean() d_up.iloc[window] = temp d_up.iloc[1:window] = np.nan temp = d_dn.iloc[1:(window+1)].mean() d_dn.iloc[window] = temp d_dn.iloc[1:window] = np.nan # process the EMA avg_up = d_up.ewm(span=window, adjust=False).mean() avg_dn = d_dn.ewm(span=window, adjust=False).mean() rs = avg_up / abs(avg_dn.values) exp_rsi = 100 - 100 / (1+rs) return exp_rsi def volume_rsi(self, window = 14): """ The volume RSI (Relative Strength Index) is quite similar to the price based RSI with difference that up-volume and down-volume are used in the RSI formula instead changes in price. If price RSI shows relation between up-moves and down-moves within an analyzed period of time by revealing which moves are stronger, the volume RSI indicator shows the relation between volume traded during these price up-moves and down-moves respectfully by revealing whether up-volume (bullish money flow) or down-volume (bearish money flow) is stronger. The same as price RSI, volume RSI oscillates around 50% center-line in the range from 0 to 100%. In technical analysis this indicator could be used in the same way as well. The simplest way of using the volume RSI would be to generate trading signals on the crossovers of the indicator and 50% center-line around which it oscillates. Here you have to remember following: volume RSI reading above 50% are considered bullish as bullish volume dominates over bearish volume; volume RSI readings below 50% are considered bearish as bearish volume overcomes bullish volume. Respectfully, technical analysis would suggest to generate buy/sell signals by following rules: Buy when indicators moves above 50% line after being below it; Sell when indicator drops below 50% line after being above it. """ # use exponential averaging volume = self.volume up_vol, dn_vol = volume.copy(), volume.copy() d_chg = self.price.diff() up_vol[d_chg < 0] = 0 dn_vol[d_chg > 0] = 0 up_vol.iloc[0] = np.nan dn_vol.iloc[0] = np.nan # EMA pre-process the first point temp = up_vol.iloc[1:(window+1)].mean() up_vol.iloc[window] = temp up_vol.iloc[1:window] = np.nan temp = dn_vol.iloc[1:(window+1)].mean() dn_vol.iloc[window] = temp dn_vol.iloc[1:window] = np.nan # EMA processing avg_up = up_vol.ewm(span=window, adjust=False).mean() avg_dn = dn_vol.ewm(span=window, adjust=False).mean() rs = avg_up / avg_dn.values exp_rsi = 100 - 100 / (1+rs) return exp_rsi def daily_returns(self): return self.price.pct_change() @property def annualized_return(self): dr = self.daily_returns() return self._sf * dr.mean() @property def annualized_stdev(self): dr = self.daily_returns() return np.sqrt(self._sf) * dr.std() @property def sharpe(self): return (self.annualized_return - self._rfr) / self.annualize_stdev def rolling_stdev(self, window): return self.price.rolling(window).std() def bollinger(self, window): """ Parameters ---------- window : TYPE int, optional DESCRIPTION - averaging window in days. Returns ------- lower, upper : TYPE pandas.DataFrame DESCRIPTION - lower band (minus 2 sigma) and the upper band. """ avg = self.sma(window) sdd2 = self.rolling_stdev(window).mul(2) lower = avg.sub(sdd2.values) upper = avg.add(sdd2.values) # low_up = lower.join(upper, lsuffix='_L', rsuffix='_U') return lower, upper def macd(self, short_wd = 12, long_wd = 26, sig_wd = 9): """ MACD Line: (12-day EMA - 26-day EMA) Signal Line: 9-day EMA of MACD Line MACD Histogram: MACD Line - Signal Line MACD is calculated by subtracting the 26-period EMA from the 12-period EMA. MACD triggers technical signals when it crosses above (to buy) or below (to sell) its signal line. The speed of crossovers is also taken as a signal of a market is overbought or oversold. MACD helps investors understand whether the bullish or bearish movement in the price is strengthening or weakening MACD historgram represents signal line crossovers that are the most common MACD signals. The signal line is a 9-day EMA of the MACD line. As a moving average of the indicator, it trails the MACD and makes it easier to spot MACD turns. A bullish crossover occurs when the MACD turns up and crosses above the signal line. A bearish crossover occurs when the MACD turns down and crosses below the signal line. Crossovers can last a few days or a few weeks, depending on the strength of the move. """ macd_short = self.ema(short_wd) macd_long = self.ema(long_wd) macd_line = macd_short - macd_long.values macd_sig = macd_line.ewm(span=sig_wd, adjust=False).mean() macd_hist = macd_line - macd_sig.values 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 ---------- stocks : TYPE instance of class 'security' Returns ------- cross : TYPE pandas DataFrame DESCRIPTION - +1 when 50 day moving average is above 200 day moving average. -1 when vice versa. transition days are of value +3 and -3 respectively. """ sma50 = stocks.sma(50) sma200 = stocks.sma(200) cross = np.sign(sma50.sub(sma200.values)) cross_diff = cross.diff() cross = cross.add(cross_diff.values) cross.columns = stocks.price.columns return cross def get_sma_slope(stocks, wd = 50): """ Parameters ---------- stocks : TYPE DESCRIPTION. wd : TYPE, optional DESCRIPTION. The default is 50. Returns ------- slope : TYPE pandas DataFrame DESCRIPTION - +1 when n day moving average is positive. -1 when negative. transition days are of value +3 and -3 respectively. """ sma = stocks.sma(wd) slope = np.sign(sma.diff()) slope_diff = slope.diff() slope = slope.add(slope_diff.values) 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 def intelligent_loop_plots(sym, tmp): # Only plot ones that are standing out meaning: # 1. outside of bollinger bands or recently crossed over (within 9 days) # 2. RSI above 70 or below 30 # 3. VoRSI above 70 or below 30 # 4. when normalized MACD hist (by dividing slower moving average) is # above 2% or below -2%. # 5. near golden cross or death cross # 6. price cross (near) 200 day moving average # 7. MACD histogram zero crossing (bullish or bearish) # symbol = ['AMZN', 'SPY', 'GOOG', 'BAC', 'BA', 'XLE', 'CTL', 'ATVI', 'JD',\ # 'COST', 'HD', 'UBER', 'XOM', 'UAL', 'LUV', 'T', 'WMT'] # matplotlib.rcParams.update({'figure.max_open_warning': 0}) price = tmp["adjclose"] vol = tmp["volume"] stk = security(sym, price, vol) rsi = stk.rsi() vorsi = stk.volume_rsi() macd, macd_sig, macd_hist, norm_hist = stk.macd() sma50 = stk.sma(50) vwma50 = stk.vwma(50) sma200 = stk.sma(200) bol_low, bol_up = stk.bollinger(200) # init plot_indicator = "[" # print('{:5}: '.format(sym), end = '') # RSI outside window (over bought / over sold) 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) 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)) 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_sign = np.sign(macd_hist_tail) macd_hist_diff = macd_hist_sign.diff() if (abs(macd_hist_diff) > 1).any(): # print('--MACD', end = '') # zero crossing plot_indicator += 'MACD, ' # Stock price crosses SMA50 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(): # print('--SMA50', end = '') 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_sign = np.sign(sma_cross_tail) sma_cross_diff = sma_cross_sign.diff() if (abs(sma_cross_diff) > 1).any(): # print('--Golden/Death', end = '') 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_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(): # print('--Bollinger', end ='') plot_indicator += 'Bollinger, ' # Price cross 200 day moving average 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() if (abs(sma200_cross_diff) > 1).any(): # print('--SMA200', end = '') plot_indicator += 'SMA200, ' # Large trading volume 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(): # print('--HiVol', end = '') plot_indicator += "HiVol, " # print(f"-- {watchlist.loc[sym, 'Notes']}") # carriage return plot_indicator += ']' # note_field = watchlist.loc[sym, 'Notes'].strip().lower() # if note_field != "watch" and ( note_field == "skip" or \ # plot_indicator =="[]" ): # continue # skipping plotting to save memory and time # plot basic price info data = price.copy().to_frame(sym) # to limit low bound when plotting in log scale bol_low.loc[sma200.divide(bol_low) > bol_up.divide(sma200).mul(3)] = np.nan data = data.join(bol_low.rename('_BOL200L')) data = data.join(bol_up.rename('_BOL200U')) data = data.join(sma200.rename('_SMA200')) data = data.join(sma50.rename('_SMA50')) data = data.join(vwma50.rename('_WVMA50')) macd = macd.to_frame('_MACD').join(macd_sig.rename('_SIG')) macd = macd.join(macd_hist.rename('_HIST')) # macd.rename(columns={sym: sym+'_MACD'}, inplace=True) rsi = rsi.to_frame('_RSI').join(vorsi.rename('_VoRSI')) # rsi.rename(columns={sym: sym+'_RSI'}, inplace=True) return data, vol.to_frame('_VOL'), macd, rsi, plot_indicator if __name__ == "__main__": 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 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 # test_sec_class() # test_smooth() # test_bollinger_sell() # test_get_orders() # Initialize the app app = Dash() watchlist = get_watchlist() symbols = watchlist.index.values.tolist() 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 = [ html.Div([ # html.Button('Reset', id="reset-button", n_clicks=0, # style={'font-size': '12px', 'width': '140px', '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': '140px', 'text-align': 'center'}), html.Button('Auto Play', id="start-button", n_clicks=0, style={'font-size': '12px', 'width': '140px', '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=len(symbols), 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") ) # 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 # # reset button callback # @callback(Output('interval-component', 'n_intervals'), # Output("start-button", "children"), # Output('symbols_dropdown_list', 'disabled'), # Input("reset-button", "n_clicks"),) # def reset_cycle(n): # if n: # return 0, "Pause", False # 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, syms): 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"]] tmp = fetch_stk_data(col_chosen, start_date) data, vol, macd, rsi, plot_ind = intelligent_loop_plots(col_chosen, 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=[False, False, False, False, False, False, True]) 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(macd_line.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.update_layout(bargap=0, bargroupgap=0) # fig.layout.xaxis.title="Time" fig.layout.yaxis.title="Volume" # fig.layout.yaxis.type="log" 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=plot_ind, title_x=0.5, margin=dict(l=30, r=20, t=50, b=20), ) # fig.update_layout(showlegend=False) # fig.update_layout(margin=dict(l=30, r=20, t=50, b=20)) return fig # return fig, syms.index(col_chosen) serve(app.server, host="0.0.0.0", port=8050, threads=7) # app.run(debug=True)