Psychological Line (PSY)

References

Definition

Psychological line (PSY), as an indicator, is the ratio of the number of rising periods over the total number of periods. It reflects the buying power in relation to the selling power.

If PSY is above 50%, it indicates that buyers are in control. Likewise, if it is below 50%, it indicates the sellers are in control. If the PSY moves along the 50% area, it indicates balance between the buyers and sellers and therefore there is no direction movement for the market.

Load basic packages
import pandas as pd
import numpy as np
import os
import gc
import copy
from pathlib import Path
from datetime import datetime, timedelta, time, date
#this package is to download equity price data from yahoo finance
#the source code of this package can be found here: https://github.com/ranaroussi/yfinance/blob/main
import yfinance as yf
pd.options.display.max_rows = 100
pd.options.display.max_columns = 100

import warnings
warnings.filterwarnings("ignore")

import pytorch_lightning as pl
random_seed=1234
pl.seed_everything(random_seed)
Global seed set to 1234





1234
#S&P 500 (^GSPC),  Dow Jones Industrial Average (^DJI), NASDAQ Composite (^IXIC)
#Russell 2000 (^RUT), Crude Oil Nov 21 (CL=F), Gold Dec 21 (GC=F)
#Treasury Yield 10 Years (^TNX)

#benchmark_tickers = ['^GSPC', '^DJI', '^IXIC', '^RUT',  'CL=F', 'GC=F', '^TNX']

benchmark_tickers = ['^GSPC']
tickers = benchmark_tickers + ['GSK', 'NVO', 'GKOS']
#https://github.com/ranaroussi/yfinance/blob/main/yfinance/base.py
#     def history(self, period="1mo", interval="1d",
#                 start=None, end=None, prepost=False, actions=True,
#                 auto_adjust=True, back_adjust=False,
#                 proxy=None, rounding=False, tz=None, timeout=None, **kwargs):

dfs = {}

for ticker in tickers:
    cur_data = yf.Ticker(ticker)
    hist = cur_data.history(period="max", start='2000-01-01')
    print(datetime.now(), ticker, hist.shape, hist.index.min(), hist.index.max())
    dfs[ticker] = hist
2022-08-27 19:18:31.874246 ^GSPC (5701, 7) 1999-12-31 00:00:00 2022-08-26 00:00:00
2022-08-27 19:18:32.234071 GSK (5701, 7) 1999-12-31 00:00:00 2022-08-26 00:00:00
2022-08-27 19:18:32.579770 NVO (5701, 7) 1999-12-31 00:00:00 2022-08-26 00:00:00
2022-08-27 19:18:32.733716 GKOS (1807, 7) 2015-06-25 00:00:00 2022-08-26 00:00:00
ticker = 'GKOS'
dfs[ticker].tail(5)
Open High Low Close Volume Dividends Stock Splits
Date
2022-08-22 49.290001 50.110001 48.810001 49.049999 243200 0 0
2022-08-23 49.509998 49.759998 48.439999 49.410000 260300 0 0
2022-08-24 50.290001 52.680000 49.269001 52.000000 628200 0 0
2022-08-25 52.820000 52.840000 52.040001 52.590000 349400 0 0
2022-08-26 52.310001 52.580002 49.480000 49.889999 550400 0 0
Define PSY calculation function
def cal_psy(ohlc: pd.DataFrame, period: int = 12, ma_period: int = 6, column: str = "close") -> pd.DataFrame:
    """
    PSY
    reference:  
    - https://github.com/mpquant/MyTT/blob/ea4f14857ecc46a3739a75ce2e6974b9057a6102/MyTT.py#L71

    def PSY(CLOSE,N=12, M=6):  
        PSY=COUNT(CLOSE>REF(CLOSE,1),N)/N*100
        PSYMA=MA(PSY,M)
        return RD(PSY),RD(PSYMA)


    """
    
    psy = (ohlc[column]>ohlc[column].shift(1)).rolling(period).sum()/period*100
    psy_ma =  psy.rolling(ma_period).mean()



    return pd.DataFrame(data={'PSY': psy.values, 'PSY_MA': psy_ma.values, }, index=ohlc.index)

Calculate PSY
df = dfs[ticker][['Open', 'High', 'Low', 'Close', 'Volume']]
df = df.round(2)
cal_psy
<function __main__.cal_psy(ohlc: pandas.core.frame.DataFrame, period: int = 12, ma_period: int = 6, column: str = 'close') -> pandas.core.frame.DataFrame>
df_ta = cal_psy(df, period = 20, ma_period = 9, column = "Close")
df = df.merge(df_ta, left_index = True, right_index = True, how='inner' )

del df_ta
gc.collect()
80
display(df.head(5))
display(df.tail(5))
Open High Low Close Volume PSY PSY_MA
Date
2015-06-25 29.11 31.95 28.00 31.22 7554700 NaN NaN
2015-06-26 30.39 30.39 27.51 28.00 1116500 NaN NaN
2015-06-29 27.70 28.48 27.51 28.00 386900 NaN NaN
2015-06-30 27.39 29.89 27.39 28.98 223900 NaN NaN
2015-07-01 28.83 29.00 27.87 28.00 150000 NaN NaN
Open High Low Close Volume PSY PSY_MA
Date
2022-08-22 49.29 50.11 48.81 49.05 243200 55.0 58.333333
2022-08-23 49.51 49.76 48.44 49.41 260300 60.0 58.333333
2022-08-24 50.29 52.68 49.27 52.00 628200 60.0 58.888889
2022-08-25 52.82 52.84 52.04 52.59 349400 60.0 59.444444
2022-08-26 52.31 52.58 49.48 49.89 550400 55.0 58.888889
df[['PSY', 'PSY_MA']].hist(bins=50)
array([[<AxesSubplot:title={'center':'PSY'}>,
        <AxesSubplot:title={'center':'PSY_MA'}>]], dtype=object)

png

#https://github.com/matplotlib/mplfinance
#this package help visualize financial data
import mplfinance as mpf
import matplotlib.colors as mcolors

# all_colors = list(mcolors.CSS4_COLORS.keys())#"CSS Colors"
all_colors = list(mcolors.TABLEAU_COLORS.keys()) # "Tableau Palette",
# all_colors = list(mcolors.BASE_COLORS.keys()) #"Base Colors",


#https://github.com/matplotlib/mplfinance/issues/181#issuecomment-667252575
#list of colors: https://matplotlib.org/stable/gallery/color/named_colors.html
#https://github.com/matplotlib/mplfinance/blob/master/examples/styles.ipynb

def make_3panels2(main_data, mid_panel, chart_type='candle', names=None, 
                  figratio=(14,9)):


    style = mpf.make_mpf_style(base_mpf_style='yahoo',  #charles
                               base_mpl_style = 'seaborn-whitegrid',
#                                marketcolors=mpf.make_marketcolors(up="r", down="#0000CC",inherit=True),
                               gridcolor="whitesmoke", 
                               gridstyle="--", #or None, or - for solid
                               gridaxis="both", 
                               edgecolor = 'whitesmoke',
                               facecolor = 'white', #background color within the graph edge
                               figcolor = 'white', #background color outside of the graph edge
                               y_on_right = False,
                               rc =  {'legend.fontsize': 'small',#or number
                                      #'figure.figsize': (14, 9),
                                     'axes.labelsize': 'small',
                                     'axes.titlesize':'small',
                                     'xtick.labelsize':'small',#'x-small', 'small','medium','large'
                                     'ytick.labelsize':'small'
                                     }, 
                              )   

    if (chart_type is None) or (chart_type not in ['ohlc', 'line', 'candle', 'hollow_and_filled']):
        chart_type = 'candle'
    len_dict = {'candle':2, 'ohlc':3, 'line':1, 'hollow_and_filled':2}    
        
    kwargs = dict(type=chart_type, figratio=figratio, volume=True, volume_panel=2, 
                  panel_ratios=(4,2,1), tight_layout=True, style=style, returnfig=True)
    
    if names is None:
        names = {'main_title': '', 'sub_tile': ''}
    



    added_plots = { 
        'PSY':  mpf.make_addplot(mid_panel['PSY'], panel=1, color='dodgerblue', width=1, secondary_y=False), 
        'PSY_MA':  mpf.make_addplot(mid_panel['PSY_MA'], panel=1, color='orange', width=1, secondary_y=False), 
    }
    
    fb_psy_up = dict(y1=50*np.ones(mid_panel.shape[0]),y2=mid_panel['PSY'].values,where=mid_panel['PSY'].values<50,color="#93c47d",alpha=0.6,interpolate=True)
    fb_psy_dn = dict(y1=50*np.ones(mid_panel.shape[0]),y2=mid_panel['PSY'].values,where=mid_panel['PSY'].values>50,color="#e06666",alpha=0.6,interpolate=True)


    fb_psy_up['panel'] = 1
    fb_psy_dn['panel'] = 1

    fb_bbands= [fb_psy_up, fb_psy_dn]
    

    fig, axes = mpf.plot(main_data,  **kwargs,
                         addplot=list(added_plots.values()),
                         fill_between=fb_bbands
                        )
    # add a new suptitle
    fig.suptitle(names['main_title'], y=1.05, fontsize=12, x=0.135)

    axes[0].set_title(names['sub_tile'], fontsize=10, style='italic',  loc='left')
    axes[2].set_ylabel('PSY')
    
    axes[2].legend([None]*3)
    handles = axes[2].get_legend().legendHandles
    axes[2].legend(handles=handles,labels=list(added_plots.keys()))
    

    return fig, axes
   

start = -200
end = -100#df.shape[0]

names = {'main_title': f'{ticker}', 
         'sub_tile': 'PSY: buy when PSY line cross to below 50; sell whey PSY line cross to above 50'}


aa_, bb_ = make_3panels2(df.iloc[start:end][['Open', 'High', 'Low', 'Close', 'Volume']], 
                         df.iloc[start:end][['PSY', 'PSY_MA']], 
                         chart_type='hollow_and_filled',
                         names = names,
                        
                        )

png