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
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
RUN pip install -U pip && pip install -r requirements.txt
|
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 pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import datetime as dt
|
import datetime as dt
|
||||||
from numpy.fft import fft, ifft
|
|
||||||
import scipy.signal as sig
|
|
||||||
import plotly.express as px
|
import plotly.express as px
|
||||||
from plotly.subplots import make_subplots
|
from plotly.subplots import make_subplots
|
||||||
from dash import Dash, html, dcc, callback, Output, Input, State, no_update, ctx
|
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.exceptions import PreventUpdate
|
||||||
from dash_auth import OIDCAuth
|
from dash_auth import OIDCAuth
|
||||||
import yahoo_fin.stock_info as si
|
import yahoo_fin.stock_info as si
|
||||||
import hashlib
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import psycopg2
|
|
||||||
import os
|
import os
|
||||||
import sys
|
from sec_cik_mapper import StockMapper, MutualFundMapper
|
||||||
from sec_cik_mapper import StockMapper
|
from subroutines import *
|
||||||
|
|
||||||
pd.options.mode.chained_assignment = None # default='warn'
|
pd.options.mode.chained_assignment = None # default='warn'
|
||||||
load_dotenv()
|
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
|
LB_YEAR = 3 # years of stock data to retrieve
|
||||||
# PLT_YEAR = 2 # number of years data to plot
|
# PLT_YEAR = 2 # number of years data to plot
|
||||||
LB_TRIGGER = 5 # days to lookback for triggering events
|
LB_TRIGGER = 5 # days to lookback for triggering events
|
||||||
@ -711,7 +283,7 @@ app.clientside_callback(
|
|||||||
Input("remove-button", "n_clicks"),
|
Input("remove-button", "n_clicks"),
|
||||||
State("symbols_dropdown_list", "value")
|
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:
|
if n and len(sym_raw) > 0:
|
||||||
sym = sym_raw.split()[0]
|
sym = sym_raw.split()[0]
|
||||||
remove_from_db(auth.username, sym.upper())
|
remove_from_db(auth.username, sym.upper())
|
||||||
@ -722,13 +294,18 @@ def remove_ticker(n, sym_raw):
|
|||||||
@callback(Output('signaling', component_property='data'),
|
@callback(Output('signaling', component_property='data'),
|
||||||
Input('input-ticker', component_property='value'))
|
Input('input-ticker', component_property='value'))
|
||||||
|
|
||||||
def update_tickers(ticker):
|
def update_tickers(ticker : str):
|
||||||
if ticker:
|
if ticker:
|
||||||
ticker_upper = ticker.upper()
|
ticker_upper = ticker.upper()
|
||||||
long_name = StockMapper().ticker_to_company_name.get(ticker_upper)
|
long_name = StockMapper().ticker_to_company_name.get(ticker_upper)
|
||||||
if long_name:
|
if long_name:
|
||||||
insert_into_db(auth.username, ticker_upper, long_name)
|
insert_into_db(auth.username, ticker_upper, long_name)
|
||||||
return ticker_upper
|
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
|
return no_update
|
||||||
|
|
||||||
# start / stop button callback
|
# start / stop button callback
|
||||||
@ -736,8 +313,8 @@ def update_tickers(ticker):
|
|||||||
Output("start-button", "children"),
|
Output("start-button", "children"),
|
||||||
Output('symbols_dropdown_list', 'disabled'),
|
Output('symbols_dropdown_list', 'disabled'),
|
||||||
Input("start-button", "n_clicks"),
|
Input("start-button", "n_clicks"),
|
||||||
State("start-button", "children"),)
|
State("start-button", "children"),
|
||||||
|
)
|
||||||
def start_cycle(n, value):
|
def start_cycle(n, value):
|
||||||
if n:
|
if n:
|
||||||
if value == "Pause":
|
if value == "Pause":
|
||||||
@ -746,6 +323,13 @@ def start_cycle(n, value):
|
|||||||
return False, "Pause", True
|
return False, "Pause", True
|
||||||
return no_update
|
return no_update
|
||||||
|
|
||||||
|
# clear input
|
||||||
|
@callback(Output('input-ticker', component_property='value'),
|
||||||
|
Input("signaling", "data"),
|
||||||
|
)
|
||||||
|
def clear_input(d):
|
||||||
|
return ''
|
||||||
|
|
||||||
# reload button callback
|
# reload button callback
|
||||||
@callback(Output('symbols_dropdown_list', 'options'),
|
@callback(Output('symbols_dropdown_list', 'options'),
|
||||||
Output('interval-component', 'n_intervals'),
|
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