def get_ticker_data_with_technicals( ticker:str, interval:str = '1d', trade_date = datetime.date.today(), tenor:str = '2y', data_buffer_tenor:str = '1y', l_ma_periods:list = [22,11], atr_params = {'period': 13, 'use_ema': True, 'channel_dict' : None}, return_all_dates = False ): ''' Get Price Data for a Ticker and add various Technical Indicators intended for trend following strategies backtesting/ screening Args: l_ma_periods: list of moving averages periods to add return_all_dates: if False, return only data from start_date ''' start_date = (BusinessDate(trade_date)- tenor).to_date() data_start_date = (BusinessDate(start_date) - data_buffer_tenor).to_date() df = get_stocks_ohlc(tickers = tickers_parser(ticker), interval = interval, start_date = data_start_date, end_date = trade_date, proxies = get_proxies() ) for p in l_ma_periods: df = add_moving_average(df, period = p, type = 'ema') df = add_MACD(df) df = add_ATR(df, **atr_params) if atr_params else df return df if return_all_dates else df[df.index > pd.Timestamp(start_date)]
def show_upcoming_div(df, st_asset, timeframe_params, atr_period=22): with st_asset: if not isinstance(df, pd.DataFrame): st.warning('no upcoming dividends found') return None div_tickers = df['ticker'].tolist() # Get Price Data ohlc_dict = get_ohlc_data( div_tickers, start_date=timeframe_params['data_start_date'], end_date=timeframe_params['end_date'], interval=timeframe_params["interval"]) if 'ATR' not in ohlc_dict[div_tickers[0]].columns: ohlc_dict = { t: add_ATR(df.copy(), period=atr_period, use_ema=True, channel_dict=None) for t, df in ohlc_dict.items() } # Update DVD df atr = df['ticker'].apply(lambda x: ohlc_dict[x]['ATR'][-1]) df.insert(loc=5, column='ATR', value=atr) df.insert(loc=4, column='div/ATR', value=df['Div'] / atr) st.write(df) if st.checkbox('export tickers'): st.info(" ".join(div_tickers)) visualize_dvd_df(df)
def detect_vol_breakout(df, period: int = 22, threshold: float = 1, ignore_gap=True, do_buy=True, add_entry_price=False, ignore_volume=False): ''' add two new columns vol_breakout and entry_price to df ''' # assert 'impulse' in df.columns threshold *= 1 if do_buy else -1 ATR_col = 'ATR_' + str(int(period)) df = add_ATR(df, period=period, use_ema=True, col_name=ATR_col, channel_dict=None, return_TR=False) if ATR_col not in df.columns else df close_diff = (df['Close'] - df['Open']) if ignore_gap else \ (df['Close'] - df['Close'].shift()) # this doesn't seem to work, check 336.hk on 11/9 volume_confirms = df['Volume'] > df['Volume'].shift().rolling( int(period)).mean() vol_breakout = close_diff > (df[ATR_col].shift() * threshold) if do_buy else \ close_diff < (df[ATR_col].shift() * threshold) if not ignore_volume: vol_breakout = (vol_breakout & volume_confirms) df['vol_breakout'] = vol_breakout if add_entry_price: # df['entry_price'] = (df['Open'] + df['ATR'].shift() * threshold) * vol_breakout df['entry_price'] = df['Close'] * vol_breakout return df
def detect_VCP(df, ma_cascade=[100, 50, 25], lvpb_period=22, ATR_period=5, debug_mode=False, normalize_ATR=False, col_name='VCP_setup'): '''Minervini's Volatility Contraction Pattern detection returns True for when all [a] all the price MAs are in ascending order, and [b] all the ATR MAs are in descending order, and [c] there are Low Volume Pullbacks within the ATR Period. ''' assert len( ma_cascade) >= 2, 'detect_VCP: must have at least 2 moving averages' assert ma_cascade == sorted( ma_cascade)[::-1], 'detect_VCP: ma_cascade must be descending' # [a] checking for trend for p in ma_cascade: df = add_moving_average(df, period=p) s_trending = True for i in range(len(ma_cascade))[1:]: s_trending = s_trending & (df[f'ema_{ma_cascade[i]}'] > df[f'ema_{ma_cascade[i-1]}']) # [b] check for volatility contraction df = add_ATR(df, period=ATR_period, use_ema=True, channel_dict=None, normalize=normalize_ATR) df = detect_volatility_contraction(df, ma_cascade=ma_cascade, debug=debug_mode) # [c] df = detect_low_vol_pullback(df, period=lvpb_period) s_has_LVPB = df['LVPB'].rolling(ATR_period).max().shift() if debug_mode: df['VCP_trending_check'] = s_trending df['VCP_LVPB_check'] = s_has_LVPB df[col_name] = s_trending & df['VCP'] & s_has_LVPB return df
def detect_volatility_contraction(df, atr_periods=[11, 5], period: int = 100, threshold: float = 0.05, normalize=True, use_ema=True, col_name='VCP', debug=False): ''' return a column to indicate volatility contraction using ATR Args: atr_periods: check for ATRs being lower in cascading order (slowest to fastest) period: look-back period, number of bars threshold: current ATR must be less than this percentile within the look-back period ''' assert 'ATR' in df.columns, 'detect_volatiliy_contraction: must add ATR first' assert len( atr_periods ) >= 2, 'detect_volatility_contraction: must have at least two moving average periods' for p in atr_periods: df = add_ATR(df, period=p, col_name=f'ATR_{p}', use_ema=use_ema, channel_dict=None, normalize=normalize) df['ATR_percentile'] = df['ATR'].rolling( int(period)).apply(lambda A: get_percentile(x=A[-1], A=A)) df[col_name] = df['ATR_percentile'] < threshold for i in range(len(atr_periods))[1:]: ATR_slow = f'ATR_{atr_periods[i-1]}' ATR_fast = f'ATR_{atr_periods[i]}' df[col_name] = df[col_name] & (df[ATR_slow] > df[ATR_fast]) if not debug: for p in atr_periods: del df[f'ATR_{p}'] del df['ATR_percentile'] return df
def get_ATR_calc(df, period, use_ema, atr_multiplier=2, var=1000, price_col='Adj Close'): ''' return a dictionary of various ATR calcuations Args: df: dataframe of prices from yfinance for a Single Stock var: Value At Risk ''' data = add_ATR(df, period=period, use_ema=use_ema, channel_dict=None) ATR = data['ATR'][-1] price = data[price_col][-1] shares = int(var / (atr_multiplier * ATR)) # Basically always rounding down) return { 'ATR': ATR, price_col: price, 'ATR%Price': ATR / price, 'num_shares': shares, 'position_size': shares * price }
def alpha_over_beta(df): ''' a volatility breakout entry with a low vol setup ref: https://www.alphaoverbeta.net/trading-the-volatility-breakout-system/ ''' # check ATR cross over for p in [5, 14]: ATR_col = 'ATR_' + str(int(p)) df = add_ATR(df, period=p, use_ema=True, col_name=ATR_col, channel_dict=None, return_TR=False) # add 3 EMA for p in [5, 11, 22]: df = add_moving_average(df, period=p, type='ema', price_col='Close') vol_breakout = (df["ema_5"] > df["ema_11"]) & (df["ema_11"]> df["ema_22"]) & \ (df["ATR_5"] > df["ATR_14"]) & (df["ATR_5"].shift() < df["ATR_14"].shift()) df['vol_breakout'] = vol_breakout df['entry_price'] = df['Close'] * vol_breakout return df
def visualize_features(df_dict, chart_configs, atr_period, start_date, use_ema=True): ''' Args: df_dict: dictionary of {ticker: df, ...} ''' l_tickers = list(df_dict.keys()) is_single = len(l_tickers) == 1 # Add KER df_dict = { t: add_KER(df, atr_period)[df.index > pd.Timestamp(start_date)] for t, df in df_dict.items() } norm_atr = st.checkbox('normalize ATR', value=True) if is_single: # Special Handling for Single Ticker tickers = None df_dict[l_tickers[0]]['ATR'] = df_dict[l_tickers[0]]['ATR']/ df_dict[l_tickers[0]]['Close'] \ if norm_atr else df_dict[l_tickers[0]]['ATR'] atr_periods = st.text_input( 'more ATR periods to test (comma separated)') atr_periods = [int(i) for i in atr_periods.split(',') ] if atr_periods else None if atr_periods: for p in atr_periods: df = df_dict[l_tickers[0]] df_dict[l_tickers[0]] = add_ATR(df, period=p, normalize=norm_atr, use_ema=use_ema, channel_dict=None, col_name=f'ATR_{p}') # MA of ATR # str_ma_period = st.text_input('moving averages period (comma-separated for multiple periods)') # if str_ma_period: # t = df_p.columns[0] # for p in str_ma_period.split(','): # df_p[f'{p}bars_MA'] = df_p[t].rolling(int(p)).mean().shift() # ATR Time-Series df_p = df_dict[l_tickers[0]] df_p = df_p[[c for c in df_p.columns if 'ATR' in c]] fig = px.line( df_p, y=df_p.columns, labels={ 'x': 'Date', 'y': 'ATR' }, title=f'Historical Average True Range ({atr_period} bars)') show_plotly(fig) else: # View ATR time series of all given stocks atr_dict = { ticker : (df['ATR']/df['Close']).dropna().to_dict() \ if norm_atr else df['ATR'].dropna().to_dict() for ticker, df in df_dict.items() } df_p = pd.DataFrame.from_dict(atr_dict) # tickers Selection tickers = st.multiselect(f'ticker', options=[''] + list(df_dict.keys())) # ATR Time-Series fig = px.line( df_p, y=tickers if tickers else df_p.columns, labels={ 'x': 'Date', 'y': 'ATR' }, title=f'Historical Average True Range ({atr_period} bars)') show_plotly( fig ) #, height = chart_size, title = f"Price chart({interval}) for {l_tickers[0]}") # ATR Histogram fig = px.histogram( df_p, x=tickers if tickers else df_p.columns, barmode=chart_configs['barmode'], title=f'Average True Range ({atr_period} bars) Distribution', nbins=chart_configs['n_bins']) show_plotly(fig) # KER Time-Series KER_dict = { ticker: df['KER'].dropna().to_dict() for ticker, df in df_dict.items() } df_p = pd.DataFrame.from_dict(KER_dict) fig = px.line(df_p, y=tickers if tickers else df_p.columns, labels={ 'x': 'Date', 'y': 'KER' }, title=f'Historical efficiency ratio ({atr_period} bars)') show_plotly(fig) # KER histogram fig = px.histogram( df_p, x=tickers if tickers else df_p.columns, barmode=chart_configs['barmode'], title=f'Efficiency Ratio ({atr_period} bars) Distribution', nbins=chart_configs['n_bins']) show_plotly(fig) # Volume if is_single: str_ma_period = st.text_input( 'Volume moving averages period (comma-separated for multiple periods)' ) df_p = df_dict[l_tickers[0]] if str_ma_period: t = df_p.columns[0] for p in str_ma_period.split(','): df_p[f'Volume_{p}bars_MA'] = df_p["Volume"].rolling( int(p)).mean().shift() df_p = df_p[[c for c in df_p.columns if 'Volume' in c]] else: volume_dict = { ticker: df['Volume'].dropna().to_dict() for ticker, df in df_dict.items() } df_p = pd.DataFrame.from_dict(volume_dict) volume_scatter = px.line(df_p, y=tickers if tickers else df_p.columns, labels={ 'x': 'Date', 'y': 'Volume' }, title=f'volume scatter plot') volume_hist = px.histogram(df_p, x=tickers if tickers else df_p.columns, barmode=chart_configs['barmode'], title=f'Volume Distribution', nbins=chart_configs['n_bins']) show_plotly(volume_scatter) show_plotly(volume_hist)
def Main(): with st.sidebar.expander("GP"): st.info(f''' Graph Prices (open-high-low-close) * inspired by this [blog post](https://towardsdatascience.com/creating-a-finance-web-app-in-3-minutes-8273d56a39f8) and this [youtube video](https://youtu.be/OhvQN_yIgCo) * plots by Plotly with thanks to this [kaggle notebook](https://www.kaggle.com/mtszkw/technical-indicators-for-trading-stocks) ''') tickers = tickers_parser(st.text_input('enter stock ticker'), max_items = 1) with st.sidebar.expander('timeframe', expanded = True): today = datetime.date.today() end_date = st.date_input('Period End Date', value = today) if st.checkbox('pick start date'): start_date = st.date_input('Period Start Date', value = today - datetime.timedelta(days = 365)) else: tenor = st.text_input('Period', value = '6m') start_date = (BusinessDate(end_date) - tenor).to_date() st.info(f'period start date: {start_date}') # TODO: allow manual handling of data_start_date # l_interval = ['1d','1wk','1m', '2m','5m','15m','30m','60m','90m','1h','5d','1mo','3mo'] interval = st.selectbox('interval', options = ['1d', '1wk', '1mo']) is_intraday = interval.endswith(('m','h')) data_start_date = start_date if is_intraday else \ (BusinessDate(start_date) - "1y").to_date() if is_intraday: st.warning(f''' intraday data cannot extend last 60 days\n also, some features below might not work properly ''') if tickers: stock_obj = yf.Ticker(tickers) if not valid_stock(stock_obj): st.error(f''' {tickers} is an invalid ticker.\n Having trouble finding the right ticker?\n Check it out first in `DESC` :point_left: ''') return None side_config = st.sidebar.expander('charts configure', expanded = False) with side_config: show_df = st.checkbox('show price dataframe', value = False) chart_size = st.number_input('Chart Size', value = 1200, min_value = 400, max_value = 1500, step = 50) side_stock_info = get_stock_info_container(stock_obj.info, st_asset= st.sidebar) data = get_stocks_ohlc(tickers, start_date = data_start_date, end_date = end_date, interval = interval, proxies = get_proxies()) with st.expander('Indicators'): l_col, m_col , r_col = st.columns(3) with l_col: st.write('#### the moving averages') ma_type = st.selectbox('moving average type', options = ['', 'ema', 'sma', 'vwap']) periods = st.text_input('moving average periods (comma separated)', value = '22,11') if ma_type: for p in periods.split(','): data = add_moving_average(data, period = int(p), type = ma_type) st.write('#### volume-based indicators') # do_volume_profile = st.checkbox('Volume Profile') data = add_AD(data) if st.checkbox('Show Advance/ Decline') else data data = add_OBV(data) if st.checkbox('Show On Balance Volume') else data with m_col: st.write('#### MACD') do_MACD = st.checkbox('Show MACD?', value = True) fast = st.number_input('fast', value = 12) slow = st.number_input('slow', value = 26) signal = st.number_input('signal', value = 9) if do_MACD: data = add_MACD(data, fast = fast, slow = slow, signal = signal ) with r_col: st.write('#### oscillator') do_RSI = st.checkbox('RSI') data = add_RSI(data, n = st.number_input('RSI period', value = 13)) if do_RSI else data tup_RSI_hilo = st.text_input('RSI chart high and low line (comma separated):', value = '70,30').split(',') \ if do_RSI else None tup_RSI_hilo = [int(i) for i in tup_RSI_hilo] if tup_RSI_hilo else None if do_RSI: data_over_hilo_pct = sum( ((data['RSI']> tup_RSI_hilo[0]) | (data['RSI']< tup_RSI_hilo[1])) & (data.index > pd.Timestamp(start_date)) ) / len(data[data.index > pd.Timestamp(start_date)]) st.info(f""" {round(data_over_hilo_pct * 100, 2)}% within hilo\n 5% of peaks and valley should be within hilo """) st.write('#### True Range Related') atr_period = int(st.number_input('Average True Range Period', value = 13)) atr_ema = st.checkbox('use EMA for ATR', value = True) show_ATR = st.checkbox('show ATR?', value = False) if ma_type: st.write('##### ATR Channels') atr_ma_name = st.selectbox('select moving average for ATR channel', options = [''] + get_moving_average_col(data.columns)) atr_channels = st.text_input('Channel Lines (comma separated)', value = "1,2,3") \ if atr_ma_name else None fill_channels = st.checkbox('Fill Channels with color', value = False) \ if atr_ma_name else None else: atr_ma_name = None data = add_ATR(data, period = atr_period, use_ema = atr_ema, channel_dict = {atr_ma_name: [float(c) for c in atr_channels.split(',')]} \ if atr_ma_name else None ) st.write(f'##### Directional System') do_ADX = st.checkbox('Show ADX') data = add_ADX(data, period = st.number_input("ADX period", value = 13)) \ if do_ADX else data with st.expander('advanced settings'): l_col, m_col , r_col = st.columns(3) with l_col: st.write('#### Market Type Classification') mkt_class_period = int(st.number_input('peroid (match your trading time domain)', value = 66)) mkt_class = market_classification(data, period = mkt_class_period, debug = False) if mkt_class_period else None if mkt_class: side_stock_info.write(f'market is `{mkt_class}` for the last **{mkt_class_period} bars**') side_stock_info.write(f'[kaufman efficiency_ratio](https://strategyquant.com/codebase/kaufmans-efficiency-ratio-ker/) ({mkt_class_period} bars): `{round(efficiency_ratio(data, period = mkt_class_period),2)}`') st.write('#### Events') do_div = st.checkbox('show ex-dividend dates') if do_div: data = add_div_col(df_price = data, df_div = stock_obj.dividends) side_stock_info.write( stock_obj.dividends[stock_obj.dividends.index > pd.Timestamp(start_date)] ) do_earnings = st.checkbox('show earning dates') if do_earnings and isinstance(stock_obj.calendar, pd.DataFrame): data = add_event_col(df_price = data, df_events = stock_obj.calendar.T.set_index('Earnings Date'), event_col_name= "earnings") side_stock_info.write(stock_obj.calendar.T) if do_MACD and ma_type: st.write("#### Elder's Impulse System") impulse_ema = st.selectbox('select moving average for impulse', options = [''] + get_moving_average_col(data.columns)) data = add_Impulse(data, ema_name = impulse_ema) if impulse_ema else data avg_pen_data = None with m_col: if ma_type: st.write("#### Average Penetration for Entry/ SafeZone") fair_col = st.selectbox('compute average penetration below', options = [''] + get_moving_average_col(data.columns)) avg_pen_data = add_avg_penetration(df = data, fair_col = fair_col, num_of_bars = st.number_input('period (e.g. 4-6 weeks)', value = 30), # 4-6 weeks use_ema = st.checkbox('use EMA for penetration', value = False), ignore_zero = st.checkbox('ignore days without penetration', value = True), coef = st.number_input( 'SafeZone Coefficient (stops should be set at least 1x Average Penetration)', value = 1.0, step = 0.1), get_df = True, debug = True ) if fair_col else None with r_col: if do_MACD: st.write('#### MACD Bullish Divergence') if st.checkbox('Show Divergence'): data = detect_macd_divergence(data, period = st.number_input('within number of bars (should be around 3 months)', value = 66), threshold = st.number_input('current low threshold (% of previous major low)', value = 0.95), debug = True ) st.write(f'#### Detect Kangaroo Tails') tail_type = st.selectbox('Tail Type', options = ['', 0, 1, -1]) data = detect_kangaroo_tails(data, atr_threshold = st.number_input('ATR Threshold', value = 2.0), period = st.number_input('period', value = 22), tail_type = tail_type) \ if tail_type else data beta_events_to_plot, l_events_to_color, l_col_to_scatter = [], [], [] show_beta_features(data = data, l_events_to_color=l_events_to_color, l_col_to_scatter = l_col_to_scatter, atr_period = atr_period) if show_df: with st.expander(f'raw data (last updated: {data.index[-1].strftime("%c")})'): st.write(data) if isinstance(avg_pen_data, pd.DataFrame): with st.expander('Buy Entry (SafeZone)'): avg_pen_dict = { 'average penetration': avg_pen_data['avg_lp'][-1], 'ATR': avg_pen_data['ATR'][-1], 'penetration stdv': avg_pen_data['std_lp'][-1], 'number of penetrations within period': avg_pen_data['count_lp'][-1], 'last': avg_pen_data['Close'][-1], 'expected ema T+1': avg_pen_data[fair_col][-1] + (avg_pen_data[fair_col][-1] - avg_pen_data[fair_col][-2]) } avg_pen_dict = {k:round(v,2) for k,v in avg_pen_dict.items()} avg_pen_dict['buy target T+1'] = avg_pen_dict['expected ema T+1'] - avg_pen_dict['average penetration'] st.write(avg_pen_dict) plot_avg_pen = st.checkbox('plot buy SafeZone and show average penetration df') plot_target_buy = False # st.checkbox('plot target buy T+1') # if plot_avg_pen: # st.write(avg_pen_data) if not(show_ATR) and 'ATR' in data.columns: del data['ATR'] #TODO: fix tz issue for interval < 1d # see: https://stackoverflow.com/questions/16628819/convert-pandas-timezone-aware-datetimeindex-to-naive-timestamp-but-in-certain-t fig = plotly_ohlc_chart( df = data if is_intraday else data[data.index > pd.Timestamp(start_date)], vol_col = 'Volume', tup_rsi_hilo = tup_RSI_hilo, b_fill_channel = fill_channels if atr_ma_name else None ) #, show_volume_profile = do_volume_profile) # SafeZone if isinstance(avg_pen_data, pd.DataFrame): fig = add_Scatter(fig, df = avg_pen_data[avg_pen_data.index > pd.Timestamp(start_date)], target_col = 'buy_safezone') \ if plot_avg_pen else fig if plot_target_buy: fig.add_hline(y = avg_pen_dict['buy target T+1'] , line_dash = 'dot', row =1, col = 1) # Events for d in ['MACD_Divergence', 'kangaroo_tails', 'ex-dividend', 'earnings'] + beta_events_to_plot: if d in data.columns: fig = add_Scatter_Event(fig, data[data.index > pd.Timestamp(start_date)], target_col = d, anchor_col = 'Low', textposition = 'bottom center', fontsize = 8, marker_symbol = 'triangle-up', event_label = d[0]) # Color Events for d in l_events_to_color: fig = add_color_event_ohlc(fig, data[data.index > pd.Timestamp(start_date)], condition_col = d['column'], color = d['color'] ) if d['column'] in data.columns else fig # Scatter Columns for c in l_col_to_scatter: fig = add_Scatter(fig, data[data.index > pd.Timestamp(start_date)], target_col = c['column'], line_color = c['color']) show_plotly(fig, height = chart_size, title = f"Price chart({interval}) for {tickers} : {stock_obj.info['longName']}")
def show_past_div(df, st_asset, timeframe_params, atr_period): with st_asset: if not isinstance(df, pd.DataFrame): st.warning('no dividends found') return None div_tickers = df['ticker'].unique().tolist() df = df[ df['Ex-date'] < pd.Timestamp(timeframe_params['end_date'])].copy() # Get Price Data ohlc_dict = get_ohlc_data( div_tickers, start_date=timeframe_params['data_start_date'], end_date=timeframe_params['end_date'], interval=timeframe_params["interval"]) if 'ATR' not in ohlc_dict[div_tickers[0]].columns: ohlc_dict = { t: add_ATR(df.copy(), period=atr_period, use_ema=True, channel_dict=None) for t, df in ohlc_dict.items() } # add ATR, TR atr = df.apply(lambda row: get_df_value_by_date( df=ohlc_dict[row['ticker']], target_col='ATR', date=(BusinessDate(row['Ex-date']) - '1b').to_date()), axis=1) tr = df.apply( lambda row: get_df_value_by_date(df=ohlc_dict[row['ticker']], target_col='TR', date=row['Ex-date'].date()), axis=1) df.insert(loc=4, column='ATR_ex-1', value=atr) df.insert(loc=5, column='TR_ex', value=tr) df.insert(loc=4, column='div/ATR', value=df['Div'] / atr) # User-Input on Capture if st.checkbox('test dividend capture'): l_col, r_col = st.beta_columns(2) atr_factor = l_col.number_input('number of ATR to risk', value=2.0) df['capture_risk'] = df.apply( lambda row: atr_factor * row['ATR_ex-1'] if row['div/ATR'] > atr_factor else 0, axis=1) df['capture_reward'] = df.apply( lambda row: row['Div'] - row['TR_ex'] if row['capture_risk'] > 0 else 0, axis=1) df['capture_r_multiplier'] = df.apply( lambda row: row['capture_reward'] / row['capture_risk'] if row['capture_risk'] > 0 else 0, axis=1) results_dict = { 'trades': len(df[df["capture_risk"] > 0]), 'wins': len(df[df['capture_reward'] > 0]), 'expectancy': df[df["capture_risk"] > 0]['capture_r_multiplier'].mean() } results_dict[ 'win_rate'] = results_dict['wins'] / results_dict['trades'] r_col.write(results_dict) st.write(df) visualize_dvd_df(df.dropna(subset=['Div', 'ATR_ex-1']))