StockChartingDash/indicators.py

1018 lines
35 KiB
Python

# -*- 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
# 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 test_get_orders():
# sd = dt.datetime(2000,2,1)
# # ed = dt.datetime.today()
# ed = dt.datetime(2012,9,12)
# symbol = ['INTC', 'XOM', 'MSFT']
# dates = dates = pd.date_range(sd, ed)
# prices = get_data(symbol, dates, addSPY=False)
# stk = security(prices)
# # order_list = simple_bollinger_strategy(stk)
# order_list = modified_bollinger_strategy(stk)
# # print(order_list)
# port_val = compute_portvals(order_list,100000,9.95,0.005)
# if isinstance(port_val, pd.DataFrame):
# port_val = port_val[port_val.columns[0]].to_frame() # just get the first column
# else:
# print("warning, code did not return a DataFrame")
# price_SPY = plot_against_sym(port_val)
# rfr=0
# sf=252
# cr, adr, sddr, sr = compute_portfolio_stats(port_val, [1.0], rfr, sf)
# crSP,adrSP,sddrSP,srSP = compute_portfolio_stats(price_SPY, [1.0], rfr, sf)
# # Compare portfolio against $SPX
# print("\nDate Range: {} to {}".format(sd.date(), ed.date()))
# print()
# print("Sharpe Ratio: {}, {}".format(sr, srSP))
# print()
# print("Cumulative Return: {}, {}".format(cr, crSP))
# print()
# print("Standard Deviation: {}, {}".format(sddr, sddrSP))
# print()
# print("Average Daily Return: {}, {}".format(adr, adrSP))
# print()
# print("Final Portfolio Value: {:.2f}".format(port_val['Portfolio'].iloc[-1]))
def plot_basic(stk, axs):
data = stk.price.copy()
lower, upper = stk.bollinger(200)
data = data.join(lower, rsuffix = '_BOL200L')
data = data.join(upper, rsuffix = '_BOL200U')
data = data.join(stk.sma(200), rsuffix = '_SMA200')
data = data.join(stk.sma(50), rsuffix = '_SMA50')
data = data.join(stk.vwma(50), rsuffix = '_WVMA50')
plot_data(data, axs, ylabel='Price ($)')
def plot_rsi(rsi, axs):
poscol = 'green'
negcol = 'red'
axs.plot(rsi.index.values, rsi.iloc[:,0])
axs.axhline(70, color=negcol, ls='dotted')
axs.axhline(30, color=poscol, ls='dotted')
axs.fill_between(rsi.index.values, rsi.iloc[:,0], 70,
where=rsi.iloc[:,0]>=70, facecolor=negcol, edgecolor=negcol, alpha=0.5)
axs.fill_between(rsi.index.values, rsi.iloc[:,0], 30,
where=rsi.iloc[:,0]<=30, facecolor=poscol, edgecolor=poscol, alpha=0.5)
axs.set_yticks([30, 50, 70])
axs.set_ylabel('RSI')
axs.grid()
def plot_volume_rsi(rsi, axs):
poscol = 'green'
negcol = 'red'
axs.plot(rsi.index.values, rsi.iloc[:,0])
axs.axhline(70, color=negcol, ls='dotted')
axs.axhline(30, color=poscol, ls='dotted')
axs.fill_between(rsi.index.values, rsi.iloc[:,0], 70,
where=rsi.iloc[:,0]>=70, facecolor=negcol, edgecolor=negcol, alpha=0.5)
axs.fill_between(rsi.index.values, rsi.iloc[:,0], 30,
where=rsi.iloc[:,0]<=30, facecolor=poscol, edgecolor=poscol, alpha=0.5)
axs.set_yticks([30, 50, 70])
axs.set_ylabel('VoRSI')
axs.grid()
def plot_macd(macd, macd_sig, macd_hist, axs):
poscol = 'green'
negcol = 'red'
axs.plot(macd.index.values, macd.iloc[:, 0], label='MACD')
axs.legend()
axs.plot(macd.index.values, macd_sig.iloc[:, 0])
axs.plot(macd.index.values, macd_hist.iloc[:, 0])
axs.fill_between(macd.index.values, macd_hist.iloc[:, 0], 0,
where=macd_hist.iloc[:, 0]>0, facecolor=poscol, edgecolor=poscol, alpha=0.5)
axs.fill_between(macd.index.values, macd_hist.iloc[:, 0], 0,
where=macd_hist.iloc[:, 0]<0, facecolor=negcol, edgecolor=negcol, alpha=0.5)
axs.set_ylabel('MACD')
axs.grid()
def intelligent_loop_plots(sym):
# 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})
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
ed = dt.datetime.today()
sd = ed - dt.timedelta(days = 365 * lb_year)
plot_sd = ed - dt.timedelta(days = 365 * plt_year)
plot_ed = ed
tmp = si.get_data(sym, start_date=sd)[["adjclose", "volume"]]
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
# Plotting
# fig, (axs0, axs1, axs2, axs3) = plt.subplots(4, sharex=True,
# gridspec_kw={'hspace': 0, 'height_ratios': [3, 1, 1, 1]},
# figsize=(16, 12))
# # fig.suptitle('{} - {} - {} - {}'.format(sym,\
# # watchlist.loc[sym, 'Segment'], watchlist.loc[sym, 'Sub Segment']))
# axs0.set_title('{} - {} - {} - {} - {}'.format(sym,\
# watchlist.loc[sym, 'Segment'], watchlist.loc[sym, 'Sub Segment'],\
# watchlist.loc[sym, 'Notes'], plot_indicator))
# axs0.set_xlim([plot_sd, plot_ed])
# 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__":
# test_sec_class()
# test_smooth()
# test_bollinger_sell()
# test_get_orders()
# Initialize the app
app = Dash()
# CACHE_CONFIG = {'CACHE_TYPE': 'SimpleCache'}
# cache = Cache()
# cache.init_app(app.server, config=CACHE_CONFIG)
watchlist = get_watchlist()
symbols = watchlist.index.values.tolist()
# App layout
app.layout = [
# html.Button('Refresh Data', id='button', n_clicks=0),
html.Button('Auto Play', id="start-button", n_clicks=0),
# html.Div(children='Pick A Symbol from Dropdown List: '),
# html.Hr(),
# dcc.RadioItems(options=['pop', 'lifeExp', 'gdpPercap'], value='lifeExp', id='symbols_dropdown_list'),
# dcc.Dropdown(['pop', 'lifeExp', 'gdpPercap'], 'lifeExp', id='symbols_dropdown_list'),
dcc.Dropdown(symbols, symbols[0], id='symbols_dropdown_list'),
# dash_table.DataTable(data=df.to_dict('records'), page_size=6),
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=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 n%2 == 0:
if value == "Pause":
return True, "Auto Play", False
else:
return False, "Pause", True
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
# @cache.memoize(timeout=14400) # cache timeout set to 4 hours
# def global_store():
# j_obj = intelligent_loop_plots()
# return j_obj
# retrieve data button callbacks
# @callback(
# Output("button", "disabled", allow_duplicate=True),
# Input("button", "n_clicks"),
# prevent_initial_call=True,
# )
# def disable_btn(n):
# if n:
# return True
# return no_update
# @callback(
# Output(component_id='signal', component_property='data'),
# # Output(component_id='button', component_property='disabled'),
# Input(component_id='button', component_property='n_clicks'),
# prevent_initial_call=True,
# )
# def get_data_cb(clicks):
# # global all_plot_sym, all_plot_ind, all_data, all_vol, all_macd, all_rsi
# # if clicks == 0:
# # return # no update
# if not clicks or clicks == 0:
# raise PreventUpdate
# print("get data")
# global_store()
# print("data retrieved")
# # unpacking
# # json_data = json.loads(j_obj)
# # all_plot_sym = json_data["all_plot_sym"]
# # all_plot_ind = json_data["all_plot_ind"]
# # all_data = pd.read_json(io.StringIO(json_data['all_data']))
# # all_vol = pd.read_json(io.StringIO(json_data['all_vol']))
# # all_macd = pd.read_json(io.StringIO(json_data['all_macd']))
# # all_rsi = pd.read_json(io.StringIO(json_data['all_rsi']))
# return clicks
# Add controls to build the interaction
# callback when data retrieve finishes triggered by signal
# @callback(
# Output(component_id='symbols_dropdown_list', component_property='options'),
# Output(component_id='button', component_property='disabled'),
# Output(component_id='start-button', component_property='disabled'),
# Input(component_id='signal', component_property='data'),
# )
# def update_dropdown(in_data):
# if not in_data or in_data == 0:
# raise PreventUpdate
# # return no_update
# print(f"input: {in_data}")
# j_obj = global_store()
# # unpacking
# json_data = json.loads(j_obj)
# all_plot_sym = json_data["all_plot_sym"]
# print("dropdown menu options updated")
# # all_plot_sym = ["SPY", "AMZN"]
# return sorted(all_plot_sym), False, False
# 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
# col_chosen = "SPY"
# j_obj = global_store()
# # unpacking
# json_data = json.loads(j_obj)
# # all_plot_sym = json_data["all_plot_sym"]
# all_plot_ind = json_data["all_plot_ind"]
# all_data = pd.read_json(io.StringIO(json_data['all_data']))
# all_vol = pd.read_json(io.StringIO(json_data['all_vol']))
# all_macd = pd.read_json(io.StringIO(json_data['all_macd']))
# all_rsi = pd.read_json(io.StringIO(json_data['all_rsi']))
# get data
data, vol, macd, rsi, plot_ind = intelligent_loop_plots(col_chosen)
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(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="Price"
fig.layout.yaxis.type="log"
fig.layout.yaxis2.title="Volume"
fig.layout.yaxis3.title="MACD"
fig.layout.yaxis4.title="RSI/VoRSI"
fig.update_layout(title_text=plot_ind)
# 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)