1018 lines
35 KiB
Python
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)
|