Change Dockerfile so that psycopg2 can be installed correctly using pip. (hint: it requires libpq-dev and gcc.)
Add mutual fund capability modulize project structure complete
This commit is contained in:
parent
a92d6b2804
commit
764fdd356e
@ -1,4 +1,5 @@
|
||||
FROM python:alpine3.19
|
||||
FROM python:3.12-slim
|
||||
RUN apt-get update && apt-get install -y libpq-dev gcc
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
RUN pip install -U pip && pip install -r requirements.txt
|
||||
|
452
indicators.py
452
indicators.py
@ -21,8 +21,6 @@ Could also come up with a value that ties to the trading volume.
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import datetime as dt
|
||||
from numpy.fft import fft, ifft
|
||||
import scipy.signal as sig
|
||||
import plotly.express as px
|
||||
from plotly.subplots import make_subplots
|
||||
from dash import Dash, html, dcc, callback, Output, Input, State, no_update, ctx
|
||||
@ -31,440 +29,14 @@ from flask_caching import Cache
|
||||
from dash.exceptions import PreventUpdate
|
||||
from dash_auth import OIDCAuth
|
||||
import yahoo_fin.stock_info as si
|
||||
import hashlib
|
||||
from dotenv import load_dotenv
|
||||
import psycopg2
|
||||
import os
|
||||
import sys
|
||||
from sec_cik_mapper import StockMapper
|
||||
from sec_cik_mapper import StockMapper, MutualFundMapper
|
||||
from subroutines import *
|
||||
|
||||
pd.options.mode.chained_assignment = None # default='warn'
|
||||
load_dotenv()
|
||||
|
||||
def connect_db():
|
||||
conn = None
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ['DB_PATH'],
|
||||
database=os.environ['DB_NAME'],
|
||||
user=os.environ['DB_USERNAME'],
|
||||
password=os.environ['DB_PASSWORD'],
|
||||
)
|
||||
except (Exception, psycopg2.DatabaseError) as error:
|
||||
print(error)
|
||||
sys.exit(1)
|
||||
return conn
|
||||
|
||||
def get_watchlist(username : str):
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY1 = f'''CREATE TABLE IF NOT EXISTS {table_name}
|
||||
(
|
||||
tick character varying(5) NOT NULL,
|
||||
description text,
|
||||
PRIMARY KEY (tick)
|
||||
);'''
|
||||
QUERY2 = f"INSERT INTO {table_name} SELECT 'SPY', 'SPDR S&P 500 ETF Trust' WHERE NOT EXISTS (SELECT NULL FROM {table_name});"
|
||||
|
||||
QUERY3 = f"SELECT * FROM {table_name};"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY1)
|
||||
curs.execute(QUERY2)
|
||||
curs.execute(QUERY3)
|
||||
tuples_list = curs.fetchall()
|
||||
|
||||
df = pd.DataFrame(tuples_list)
|
||||
return df
|
||||
|
||||
def remove_from_db(username, tick):
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY = f"DELETE FROM {table_name} WHERE tick = '{tick}';"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY)
|
||||
|
||||
def insert_into_db(username : str, tick : str, name : str):
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY1 = f'''CREATE TABLE IF NOT EXISTS {table_name}
|
||||
(
|
||||
tick character varying(5) NOT NULL,
|
||||
description text,
|
||||
PRIMARY KEY (tick)
|
||||
);'''
|
||||
QUERY2 = f"INSERT INTO {table_name} SELECT 'SPY', 'SPDR S&P 500 ETF Trust' WHERE NOT EXISTS (SELECT NULL FROM {table_name});"
|
||||
|
||||
QUERY3 = f"INSERT INTO {table_name} VALUES ('{tick}', '{name}') ON CONFLICT DO NOTHING;"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY1)
|
||||
curs.execute(QUERY2)
|
||||
curs.execute(QUERY3)
|
||||
|
||||
def hash_password(password):
|
||||
# Encode the password as bytes
|
||||
password_bytes = password.encode('utf-8')
|
||||
|
||||
# Use SHA-256 hash function to create a hash object
|
||||
hash_object = hashlib.sha256(password_bytes)
|
||||
|
||||
# Get the hexadecimal representation of the hash
|
||||
password_hash = hash_object.hexdigest()
|
||||
|
||||
return password_hash
|
||||
|
||||
# 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 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
|
||||
|
||||
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
|
||||
@ -711,7 +283,7 @@ app.clientside_callback(
|
||||
Input("remove-button", "n_clicks"),
|
||||
State("symbols_dropdown_list", "value")
|
||||
)
|
||||
def remove_ticker(n, sym_raw):
|
||||
def remove_ticker(n, sym_raw : str):
|
||||
if n and len(sym_raw) > 0:
|
||||
sym = sym_raw.split()[0]
|
||||
remove_from_db(auth.username, sym.upper())
|
||||
@ -722,13 +294,18 @@ def remove_ticker(n, sym_raw):
|
||||
@callback(Output('signaling', component_property='data'),
|
||||
Input('input-ticker', component_property='value'))
|
||||
|
||||
def update_tickers(ticker):
|
||||
def update_tickers(ticker : str):
|
||||
if ticker:
|
||||
ticker_upper = ticker.upper()
|
||||
long_name = StockMapper().ticker_to_company_name.get(ticker_upper)
|
||||
if long_name:
|
||||
insert_into_db(auth.username, ticker_upper, long_name)
|
||||
return ticker_upper
|
||||
else:
|
||||
long_name = MutualFundMapper().ticker_to_series_id.get(ticker_upper)
|
||||
if long_name:
|
||||
insert_into_db(auth.username, ticker_upper, 'Mutual Fund - ' + long_name)
|
||||
return ticker_upper
|
||||
return no_update
|
||||
|
||||
# start / stop button callback
|
||||
@ -736,8 +313,8 @@ def update_tickers(ticker):
|
||||
Output("start-button", "children"),
|
||||
Output('symbols_dropdown_list', 'disabled'),
|
||||
Input("start-button", "n_clicks"),
|
||||
State("start-button", "children"),)
|
||||
|
||||
State("start-button", "children"),
|
||||
)
|
||||
def start_cycle(n, value):
|
||||
if n:
|
||||
if value == "Pause":
|
||||
@ -746,6 +323,13 @@ def start_cycle(n, value):
|
||||
return False, "Pause", True
|
||||
return no_update
|
||||
|
||||
# clear input
|
||||
@callback(Output('input-ticker', component_property='value'),
|
||||
Input("signaling", "data"),
|
||||
)
|
||||
def clear_input(d):
|
||||
return ''
|
||||
|
||||
# reload button callback
|
||||
@callback(Output('symbols_dropdown_list', 'options'),
|
||||
Output('interval-component', 'n_intervals'),
|
||||
|
6
subroutines/__init__.py
Normal file
6
subroutines/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
# Define the __all__ variable
|
||||
__all__ = ["security", "remove_from_db", "insert_into_db", "get_watchlist"]
|
||||
|
||||
# Import the submodules
|
||||
from .security import security, get_crossing, get_sma_slope
|
||||
from .dbutil import remove_from_db, insert_into_db, get_watchlist
|
125
subroutines/dbutil.py
Normal file
125
subroutines/dbutil.py
Normal file
@ -0,0 +1,125 @@
|
||||
import hashlib
|
||||
import psycopg2
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
|
||||
def connect_db(host, database, user, password):
|
||||
"""Connect to database
|
||||
|
||||
Returns:
|
||||
psycopg2 connector: psycopg2 postgresql connector
|
||||
"""
|
||||
conn = None
|
||||
try:
|
||||
conn = psycopg2.connect(
|
||||
host=os.environ['DB_PATH'],
|
||||
database=os.environ['DB_NAME'],
|
||||
user=os.environ['DB_USERNAME'],
|
||||
password=os.environ['DB_PASSWORD'],
|
||||
)
|
||||
except (Exception, psycopg2.DatabaseError) as error:
|
||||
print(error)
|
||||
sys.exit(1)
|
||||
return conn
|
||||
|
||||
def get_watchlist(username : str):
|
||||
"""Read list of tickers/descriptions from database
|
||||
|
||||
Args:
|
||||
username (str): database table prefix
|
||||
|
||||
Returns:
|
||||
Pandas DataFrame: it has two columns - first column is ticker, second column is description
|
||||
"""
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY1 = f'''CREATE TABLE IF NOT EXISTS {table_name}
|
||||
(
|
||||
tick character varying(5) NOT NULL,
|
||||
description text,
|
||||
PRIMARY KEY (tick)
|
||||
);'''
|
||||
QUERY2 = f"INSERT INTO {table_name} SELECT 'SPY', 'SPDR S&P 500 ETF Trust' WHERE NOT EXISTS (SELECT NULL FROM {table_name});"
|
||||
|
||||
QUERY3 = f"SELECT * FROM {table_name};"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY1)
|
||||
curs.execute(QUERY2)
|
||||
curs.execute(QUERY3)
|
||||
tuples_list = curs.fetchall()
|
||||
|
||||
df = pd.DataFrame(tuples_list)
|
||||
return df
|
||||
|
||||
def remove_from_db(username, tick):
|
||||
"""Remove a row from database table using ticker as key
|
||||
|
||||
Args:
|
||||
username (str): database table prefix
|
||||
tick (str): ticker
|
||||
"""
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY = f"DELETE FROM {table_name} WHERE tick = '{tick}';"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY)
|
||||
|
||||
def insert_into_db(username : str, tick : str, name : str):
|
||||
"""Insert ticker and description into database
|
||||
|
||||
Args:
|
||||
username (str): database table prefix - each user has its own list of tickers
|
||||
tick (str): stock or mutual fund ticker
|
||||
name (str): company name for stock, series ID for mutual fund
|
||||
"""
|
||||
if username:
|
||||
table_name = f"{username + '_watch_list'}"
|
||||
else: # username is None, use default table
|
||||
table_name = "stock_watch_list"
|
||||
|
||||
QUERY1 = f'''CREATE TABLE IF NOT EXISTS {table_name}
|
||||
(
|
||||
tick character varying(5) NOT NULL,
|
||||
description text,
|
||||
PRIMARY KEY (tick)
|
||||
);'''
|
||||
QUERY2 = f"INSERT INTO {table_name} SELECT 'SPY', 'SPDR S&P 500 ETF Trust' WHERE NOT EXISTS (SELECT NULL FROM {table_name});"
|
||||
|
||||
QUERY3 = f"INSERT INTO {table_name} VALUES ('{tick}', '{name}') ON CONFLICT DO NOTHING;"
|
||||
|
||||
with connect_db() as conn:
|
||||
with conn.cursor() as curs:
|
||||
curs.execute(QUERY1)
|
||||
curs.execute(QUERY2)
|
||||
curs.execute(QUERY3)
|
||||
|
||||
def hash_password(password : str):
|
||||
"""Generate hash from string using sha256
|
||||
|
||||
Args:
|
||||
password (str): any text
|
||||
|
||||
Returns:
|
||||
str: hash string
|
||||
"""
|
||||
# Encode the password as bytes
|
||||
password_bytes = password.encode('utf-8')
|
||||
|
||||
# Use SHA-256 hash function to create a hash object
|
||||
hash_object = hashlib.sha256(password_bytes)
|
||||
|
||||
# Get the hexadecimal representation of the hash
|
||||
password_hash = hash_object.hexdigest()
|
||||
|
||||
return password_hash
|
341
subroutines/security.py
Normal file
341
subroutines/security.py
Normal file
@ -0,0 +1,341 @@
|
||||
import numpy as np
|
||||
from numpy.fft import fft, ifft
|
||||
import scipy.signal as sig
|
||||
|
||||
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 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 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
|
||||
smoo = abs(fft_convolve(ys, padded))
|
||||
smoo[0:hsize-1] = np.nan
|
||||
data[col] = smoo
|
||||
|
||||
return data
|
Loading…
x
Reference in New Issue
Block a user