├── README.md ├── criteria_lib.py ├── final_report.ipynb └── indicator_lib.py /README.md: -------------------------------------------------------------------------------- 1 | # Cycle-indicators 2 | Cycle indicators from “Cybernetic Analysis for Stocks and Futures” and “Cycle Analytics for Traders” realized with python numpy package 3 | 4 | ## final_report.ipynb 5 | Summary of the indicators, including 6 | * logic behind the code 7 | * range of parameters (not effective range since it may be applied to time series with different frequencies) 8 | * suggested simple trading strategy 9 | 10 | ## indicator_lib.py 11 | Indicator library. Return value include: 12 | * signal generated by the simple trading strategy 13 | * original indicator series 14 | * trigger series (if necessary) 15 | 16 | ## criteria_lib.py 17 | Use rolling return to backtest indicators' performance. 18 | 19 | Return criteria include: 20 | * sharpe ratio 21 | * MAR 22 | * annualized profit 23 | * max drawdown 24 | * average largest 10 drawdown 25 | * max drawdown ratio 26 | * average turnover 27 | 28 | Also, you can visualize indicator performance with the "performance" function. 29 | -------------------------------------------------------------------------------- /criteria_lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Spyder Editor 4 | 5 | This is a temporary script file. 6 | """ 7 | 8 | import numpy as np 9 | from numba import jit 10 | import pandas as pd 11 | import datetime 12 | from sklearn.model_selection import train_test_split 13 | 14 | import seaborn as sns 15 | import matplotlib.pyplot as plt 16 | import matplotlib.gridspec as gridspec 17 | 18 | 19 | 20 | def contract_combine(data_list: list, contract_span: pd.DataFrame): 21 | """ 22 | 根据主力列表,将数据列表进行拼接 23 | 参数: 24 | data_list——原始数据列表(包含与合约数目相同个数的dataframe) 25 | contract_span——合约日期表 26 | """ 27 | month_change = [] 28 | combine_data = pd.DataFrame(None) 29 | for i in range(len(contract_span)): 30 | if i==0: 31 | data = data_list[i] 32 | month_change.append(datetime.datetime.strptime(str(contract_span["end_date"][i]), "%Y%m%d") + datetime.timedelta(hours=17, minutes=00)) 33 | in_data = data[data.index<=month_change[i]] 34 | combine_data = pd.concat([combine_data, in_data], axis=0) 35 | else: 36 | data = data_list[i] 37 | month_change.append(datetime.datetime.strptime(str(contract_span["end_date"][i]), "%Y%m%d") + datetime.timedelta(hours=17, minutes=00)) 38 | in_data = data[(data.index>month_change[i-1]) & (data.index<=month_change[i])] 39 | combine_data = pd.concat([combine_data, in_data], axis=0) 40 | return combine_data 41 | 42 | 43 | def ann_return(daily_ret): 44 | return np.mean(daily_ret)*365 45 | 46 | def sharpe_ratio(daily_ret): 47 | sharpe_ratio = np.mean(daily_ret) / np.std(daily_ret) * np.sqrt(250) 48 | return sharpe_ratio 49 | 50 | def MAR(daily_ret): 51 | ar = ann_return(daily_ret) 52 | maxdd,_,_ = max_drawdown(daily_ret) 53 | return ar/abs(maxdd) 54 | 55 | 56 | def max_drawdown(daily_cum_ret): 57 | balance = pd.Series(daily_cum_ret.flatten()) 58 | highlevel = balance.rolling(min_periods=1, window=len(balance), center=False).max() 59 | # abs drawdown 60 | drawdown = balance - highlevel 61 | maxdd = np.min(drawdown) 62 | maxdd10 = drawdown.sort_values(ascending = True).iloc[:10].mean() 63 | # relative drawdown 64 | dd_ratio = drawdown / (highlevel + 1) 65 | maxdd_ratio = np.min(dd_ratio) 66 | return maxdd,maxdd10,maxdd_ratio 67 | 68 | def pnl(signal,price,dayclose,slip,contract,tick_size): 69 | 70 | cur_holding = np.zeros_like(signal)# num of contract 71 | asset = np.zeros_like(signal)# current asset 72 | asset[0] = 100000000 73 | ret = np.zeros_like(signal) 74 | cost = np.zeros_like(signal) 75 | n = signal.shape[0] 76 | for i in range(n): 77 | if i>0: 78 | ret[i] = cur_holding[i-1]*contract*(price[i]-price[i-1]) 79 | # ret = change of price x cur_holding - cost 80 | asset[i] = asset[i-1]+ret[i] 81 | money = signal[i]*asset[i] - cur_holding[i-1]*price[i]*contract # money to invest 82 | det_holding = (money/(price[i])/contract).astype(np.int) # unit: num of contract 83 | cost[i] = abs(det_holding)*contract*slip*tick_size # transaction cost incurred 84 | ret[i] = ret[i] - cost[i] 85 | cur_holding[i] = cur_holding[i-1] + det_holding # n of holding contract 86 | asset[i] = asset[i] - cost[i] # change of asset = ret 87 | 88 | cum_ret = asset/asset[0]-1 89 | daily_cum_ret = cum_ret[dayclose.astype(bool)] 90 | daily_ret = daily_cum_ret - np.roll(daily_cum_ret,1) 91 | daily_ret[0] = daily_cum_ret[0] 92 | dayopen = np.roll(dayclose.astype(bool),1) 93 | dayopen[0] = True 94 | open_asset = (asset+cost)[dayopen] 95 | if dayclose[-1] != 1: 96 | open_asset = open_asset[:-1] 97 | daily_ret_pct = daily_ret/open_asset # percentage daily return 98 | 99 | det_holding = cur_holding - np.roll(cur_holding,1) # compute turnover 100 | det_holding[0] = cur_holding[0] 101 | cum_det_holding = abs(det_holding).cumsum() 102 | daily_cum_det_holding = cum_det_holding[dayclose.astype(bool)] 103 | turnover = daily_cum_det_holding - np.roll(daily_cum_det_holding, 1) 104 | turnover[0] = daily_cum_det_holding[0] 105 | avg_turnover = np.mean(turnover) 106 | 107 | return daily_cum_ret, daily_ret_pct, avg_turnover, turnover 108 | 109 | 110 | def performance(paras_tuple, signal_input, signal_func, contract_span, delete,\ 111 | price, dayclose, pnl_paras, mode = 'both'): 112 | 113 | # get signal sequence for each bar 114 | signals = [] 115 | for i in range(len(contract_span)): 116 | cur_input = signal_input[i] 117 | signal = signal_func(*cur_input, *paras_tuple)[0] 118 | signal[-1] = 0 119 | signal = pd.DataFrame(signal, index = cur_input[0].index) 120 | signals.append(signal) 121 | # concatenate signal sequence and price sequence 122 | combined_signal = contract_combine(signals, contract_span) 123 | combined_signal = combined_signal[delete:] 124 | 125 | signal = combined_signal.to_numpy() 126 | price = price.to_numpy() 127 | slip, contract, tick_size = pnl_paras['slip'], pnl_paras['contract'], pnl_paras['tick_size'] 128 | 129 | cur_holding = np.zeros_like(signal) 130 | asset = np.zeros_like(signal) 131 | asset[0] = 100000000 132 | ret = np.zeros_like(signal) 133 | cost = np.zeros_like(signal) 134 | n = signal.shape[0] 135 | for i in range(n): 136 | if i > 0: 137 | ret[i] = cur_holding[i - 1] * contract * (price[i] - price[i - 1]) 138 | asset[i] = asset[i - 1] + ret[i] 139 | money = signal[i] * asset[i] - cur_holding[i - 1] * price[i] * contract 140 | det_holding = (money / (price[i]) / contract).astype(np.int) 141 | cost[i] = abs(det_holding) * contract * slip * tick_size 142 | ret[i] = ret[i] - cost[i] 143 | cur_holding[i] = cur_holding[i - 1] + det_holding 144 | asset[i] = asset[i] - cost[i] 145 | 146 | cum_ret = asset / asset[0] - 1 147 | daily_cum_ret = cum_ret[dayclose.astype(bool)] 148 | 149 | balance = pd.Series(daily_cum_ret.flatten()) 150 | highlevel = balance.rolling(min_periods=1, window=len(balance), center=False).max() 151 | drawdown = balance - highlevel 152 | df = pd.DataFrame({'ret': daily_cum_ret.flatten(), 'dd': drawdown.values, 'price': price[dayclose.astype(bool)].flatten()}, 153 | index = combined_signal.loc[dayclose.astype(bool)].index) 154 | 155 | plt.subplot(211) 156 | # ============================================================================= 157 | # plt.title('bandpass'+str(paras_tuple)) 158 | # ============================================================================= 159 | sns.lineplot(data=df['price'], label='price') 160 | plt.subplot(212) 161 | sns.lineplot(data=df['ret'], label='return') 162 | sns.lineplot(data=df['dd'], label='drawdown') 163 | plt.legend(loc='center right') 164 | 165 | 166 | def select_paras(paras_tuple, signal_input, signal_func, contract_span, delete,\ 167 | price, dayclose, pnl_paras, mode = 'train'): 168 | # get signal sequence for each bar 169 | signals = [] 170 | for i in range(len(contract_span)): 171 | cur_input = signal_input[i] 172 | signal = signal_func(*cur_input, *paras_tuple)[0] 173 | signal[-1] = 0 174 | signal = pd.DataFrame(signal, index=cur_input[0].index) 175 | signals.append(signal) 176 | 177 | # concatenate signal sequence and price sequence 178 | combined_signal = contract_combine(signals, contract_span) 179 | combined_signal = combined_signal[delete:] 180 | 181 | # train test split 182 | signal_train, signal_test = train_test_split(combined_signal, test_size=0.3, shuffle=False) 183 | 184 | if mode == 'train': 185 | signal_used = signal_train 186 | else: 187 | signal_used = signal_test 188 | 189 | # output criteria 190 | daily_cum_pct, daily_ret_pct, avg_turnover, turnover = pnl(signal_used.to_numpy(), price.to_numpy(), dayclose, 191 | **pnl_paras) 192 | sharpe = sharpe_ratio(daily_ret_pct) 193 | mar = MAR(daily_ret_pct) 194 | annprofit = ann_return(daily_ret_pct) 195 | maxdd, maxdd10, maxdd_ratio = max_drawdown(daily_cum_pct) 196 | 197 | return sharpe, mar, annprofit, maxdd, maxdd10, maxdd_ratio, avg_turnover, turnover 198 | 199 | 200 | 201 | 202 | def save_result(result, para_list, para_name): 203 | 204 | sharpe_list = [res[0] for res in result] 205 | mar_list = [res[1] for res in result] 206 | annprofit_list = [res[2] for res in result] 207 | maxdd_list = [res[3] for res in result] 208 | maxdd10_list = [res[4] for res in result] 209 | maxdd_ratio_list = [res[5] for res in result] 210 | avg_turnover_list = [res[6] for res in result] 211 | turnover_list = [res[7] for res in result] 212 | 213 | result = pd.DataFrame({'sharpe': sharpe_list, 'mar': mar_list,\ 214 | 'annprofit': annprofit_list, 'maxdd': maxdd_list, 'maxdd10':maxdd10_list, \ 215 | 'maxddratio': maxdd_ratio_list, 'avg_turnover': avg_turnover_list, \ 216 | 'turnover': turnover_list}) 217 | paras = pd.DataFrame(para_list, columns = para_name) 218 | result = pd.concat([paras,result], axis = 1) 219 | return result 220 | 221 | 222 | 223 | 224 | def show_heatmap(result, para1, para2, values, aggfunc): 225 | report_result = pd.pivot_table(result, index = [para1], columns = [para2], values = values, aggfunc = aggfunc) 226 | sns.heatmap(report_result, cmap = 'YlGnBu') 227 | plt.savefig(para1+'&'+para2+'_'+values+'_'+'.png') 228 | 229 | 230 | 231 | 232 | # ============================================================================= 233 | # 234 | # def max_drawdown(daily_ret): 235 | # balance = pd.Series(daily_ret.cumsum()) 236 | # highlevel = balance.rolling(min_periods=1, window=len(balance), center=False).max() 237 | # # abs drawdown 238 | # drawdown = balance - highlevel 239 | # maxdd = np.min(drawdown) 240 | # maxdd10 = drawdown.sort_values(ascending = True).iloc[:10].mean() 241 | # # relative drawdown 242 | # dd_ratio = drawdown/highlevel 243 | # maxdd_ratio = np.min(dd_ratio) 244 | # return maxdd,maxdd10,maxdd_ratio 245 | # 246 | # def pnl(signal,price,dayclose,slip,contract,tick_size): 247 | # 248 | # money = 100000000 249 | # money_alloc = money*signal 250 | # tmp = (money_alloc/price)/contract 251 | # holdings = tmp.astype(np.int)*contract 252 | # det_holdings = holdings-np.roll(holdings,1) 253 | # det_holdings[0] = holdings[0] 254 | # cost = slip*abs(det_holdings)*tick_size 255 | # 256 | # holdings_1 = np.roll(holdings,1) 257 | # holdings_1[0] = 0 258 | # price_1 = np.roll(price,1) 259 | # price_1[0] = price[0] 260 | # ret = (price-price_1)*holdings_1 261 | # 262 | # ret = ret - cost 263 | # ret[np.isnan(ret)] = 0 264 | # cum_ret = ret.cumsum() 265 | # daily_cum_ret = cum_ret[dayclose] 266 | # daily_ret = daily_cum_ret - np.roll(daily_cum_ret,1) 267 | # daily_ret[0] = daily_cum_ret[0] 268 | # daily_ret_pct = daily_ret/money 269 | # 270 | # cum_det_holdings = abs(det_holdings/contract).cumsum() 271 | # daily_cum_det_holdings = cum_det_holdings[dayclose] 272 | # turnover = daily_cum_det_holdings - np.roll(daily_cum_det_holdings, 1) 273 | # turnover[0] = daily_cum_det_holdings[0] 274 | # avg_turnover = np.mean(turnover) 275 | # 276 | # return daily_ret_pct, avg_turnover, turnover 277 | # 278 | # ============================================================================= -------------------------------------------------------------------------------- /final_report.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Summary of Cycle Indicators\n", 8 | "## Derivative Department, Egret Asset\n", 9 | "### Jan. 6th - April. 5th\n", 10 | "李心怡" 11 | ] 12 | }, 13 | { 14 | "cell_type": "code", 15 | "execution_count": 3, 16 | "metadata": {}, 17 | "outputs": [], 18 | "source": [ 19 | "import numpy as np" 20 | ] 21 | }, 22 | { 23 | "cell_type": "markdown", 24 | "metadata": {}, 25 | "source": [ 26 | "## I. Filters\n", 27 | "First we define some filters that might be useful in understanding indicators. (Although some are just for completeness, and ware never used by any indicators here.)\n", 28 | "### Simple Moving Average (4 period)" 29 | ] 30 | }, 31 | { 32 | "cell_type": "code", 33 | "execution_count": 4, 34 | "metadata": {}, 35 | "outputs": [], 36 | "source": [ 37 | "def sma4(series):\n", 38 | " \"\"\"\n", 39 | " 4 term simple moving average\n", 40 | " :param series: (np.array) price\n", 41 | " :return: (np.array) smoothed price\n", 42 | " \"\"\"\n", 43 | " newseries = (series + 2 * np.roll(series, 1) + 2 * np.roll(series, 2)\n", 44 | " + np.roll(series, 3)) / 6\n", 45 | " newseries[:3] = series[:3]\n", 46 | " return newseries" 47 | ] 48 | }, 49 | { 50 | "cell_type": "markdown", 51 | "metadata": {}, 52 | "source": [ 53 | "### Exponencial Moving Average\n", 54 | "cutoff $(0, +\\infty)$: for wave that has a period less than the cutoff period (i.e higher frequency part), its power is lessened to 1/2 or more." 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": 5, 60 | "metadata": {}, 61 | "outputs": [], 62 | "source": [ 63 | "def ema(series, cutoff):\n", 64 | " \"\"\"\n", 65 | " exponential moving average\n", 66 | " alpha = 1/(lag+1)\n", 67 | " :param series: (np.array) price\n", 68 | " :param cutoff: (float) cutoff period of the filter\n", 69 | " :return: (np.array) filtered price\n", 70 | " \"\"\"\n", 71 | " K = 1\n", 72 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 73 | " for i in range(1, series.shape[0]):\n", 74 | " series[i] = alpha * series[i] \\\n", 75 | " + (1 - alpha) * series[i - 1]\n", 76 | " return series" 77 | ] 78 | }, 79 | { 80 | "cell_type": "markdown", 81 | "metadata": {}, 82 | "source": [ 83 | "### Regularized EMA\n", 84 | "cutoff $(0, +\\infty)$: for wave that has a period less than the cutoff period (i.e higher frequency part), its power is lessened to 1/2 or more." 85 | ] 86 | }, 87 | { 88 | "cell_type": "code", 89 | "execution_count": 6, 90 | "metadata": {}, 91 | "outputs": [], 92 | "source": [ 93 | "def regularized_ema(series, cutoff):\n", 94 | " \"\"\"\n", 95 | " add an additional penalty term to enhance filter effect while introducing no more lag\n", 96 | " :param series: (np.array) price\n", 97 | " :param cutoff: (float) cutoff period of the filter\n", 98 | " :return: (np.array) filtered price\n", 99 | " \"\"\"\n", 100 | " K = 1\n", 101 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 102 | " l = np.exp(0.16 / alpha)\n", 103 | " newseries = np.copy(series)\n", 104 | " for i in range(2, series.shape[0]):\n", 105 | " newseries[i] = alpha / (1 + l) * series[i] \\\n", 106 | " + (1 - alpha - 2 * l) / (1 + l) * newseries[i - 1] - l / (l + 1) * newseries[i - 2]\n", 107 | " return newseries" 108 | ] 109 | }, 110 | { 111 | "cell_type": "markdown", 112 | "metadata": {}, 113 | "source": [ 114 | "### Lowpass Filter 2 poles\n", 115 | "cutoff $(0, +\\infty)$: for wave that has a period less than the cutoff period (i.e higher frequency part), its power is lessened to 1/2 or more." 116 | ] 117 | }, 118 | { 119 | "cell_type": "code", 120 | "execution_count": 7, 121 | "metadata": {}, 122 | "outputs": [], 123 | "source": [ 124 | "def lowpass2pole(series, cutoff):\n", 125 | " \"\"\"\n", 126 | " 2 pole low-pass filter\n", 127 | " :param series: (np.array) price\n", 128 | " :param cutoff: (float) cutoff period of the filter\n", 129 | " :return: (np.array) filtered price\n", 130 | " \"\"\"\n", 131 | " K = 1.414\n", 132 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 133 | " for i in range(2, series.shape[0]):\n", 134 | " series[i] = alpha ** 2 * series[i] \\\n", 135 | " + 2 * (1 - alpha) * series[i - 1] + (1 - alpha) ** 2 * series[i - 2]\n", 136 | " return series" 137 | ] 138 | }, 139 | { 140 | "cell_type": "markdown", 141 | "metadata": {}, 142 | "source": [ 143 | "### Decycler\n", 144 | "= 1 - highpass" 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 8, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "def decycler(series, cutoff):\n", 154 | " \"\"\"\n", 155 | " subtract high frequency component from the original series to decycle\n", 156 | " :param series: (np.array) price\n", 157 | " :param cutoff: (float) cutoff period of the filter\n", 158 | " :return: (np.array) decycled series\n", 159 | " \"\"\"\n", 160 | " K = 1\n", 161 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 162 | " newseries = np.copy(series)\n", 163 | " for i in range(1, series.shape[0]):\n", 164 | " newseries[i] = alpha / 2 * (series[i] + series[i - 1]) \\\n", 165 | " + (1 - alpha) * newseries[i - 1]\n", 166 | " return newseries" 167 | ] 168 | }, 169 | { 170 | "cell_type": "markdown", 171 | "metadata": {}, 172 | "source": [ 173 | "### Highpass Filter\n", 174 | "cutoff $(0, +\\infty)$: for wave that has a period greater than the cutoff period (i.e lower frequency part), its power is lessened to 1/2 or more." 175 | ] 176 | }, 177 | { 178 | "cell_type": "code", 179 | "execution_count": 9, 180 | "metadata": {}, 181 | "outputs": [], 182 | "source": [ 183 | "def highpass(series, cutoff):\n", 184 | " \"\"\"\n", 185 | " (1 pole) high-pass filter\n", 186 | " :param series: (np.array) price\n", 187 | " :param cutoff: (float) cutoff period of the filter\n", 188 | " :return: (np.array) filtered price\n", 189 | " \"\"\"\n", 190 | " K = 1\n", 191 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 192 | " newseries = np.copy(series)\n", 193 | " for i in range(1, series.shape[0]):\n", 194 | " newseries[i] = (1 - alpha / 2) * series[i] - (1 - alpha / 2) * series[i - 1] \\\n", 195 | " + (1 - alpha) * newseries[i - 1]\n", 196 | " return newseries" 197 | ] 198 | }, 199 | { 200 | "cell_type": "markdown", 201 | "metadata": {}, 202 | "source": [ 203 | "### Highpass Filter 2 poles\n", 204 | "cutoff $(0, +\\infty)$: for wave that has a period greater than the cutoff period (i.e lower frequency part), its power is lessened to 1/2 or more." 205 | ] 206 | }, 207 | { 208 | "cell_type": "code", 209 | "execution_count": 10, 210 | "metadata": {}, 211 | "outputs": [], 212 | "source": [ 213 | "def highpass2pole(series, cutoff):\n", 214 | " \"\"\"\n", 215 | " 2 pole high-pass filter\n", 216 | " :param series: (np.array) price\n", 217 | " :param cutoff: (float) cutoff period of the filter\n", 218 | " :return: (np.array) filtered price\n", 219 | " \"\"\"\n", 220 | " K = 0.707\n", 221 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 222 | " newseries = np.copy(series)\n", 223 | " for i in range(2, series.shape[0]):\n", 224 | " newseries[i] = (1 - alpha / 2) ** 2 * series[i] \\\n", 225 | " - 2 * (1 - alpha / 2) ** 2 * series[i - 1] \\\n", 226 | " + (1 - alpha / 2) ** 2 * series[i - 2] \\\n", 227 | " + 2 * (1 - alpha) * newseries[i - 1] - (1 - alpha) ** 2 * newseries[i - 2]\n", 228 | " return newseries" 229 | ] 230 | }, 231 | { 232 | "cell_type": "markdown", 233 | "metadata": {}, 234 | "source": [ 235 | "### Super Smoother 2 poles\n", 236 | "cutoff $(0, +\\infty)$: for wave that has a period less than the cutoff period (i.e higher frequency part), its power is lessened to 1/2 or more." 237 | ] 238 | }, 239 | { 240 | "cell_type": "code", 241 | "execution_count": 11, 242 | "metadata": {}, 243 | "outputs": [], 244 | "source": [ 245 | "def supersmoother2pole(series, cutoff):\n", 246 | " \"\"\"\n", 247 | " simplified 2 pole butterworth smoother\n", 248 | " :param series: (np.array) price\n", 249 | " :param cutoff: (float) cutoff period of the filter\n", 250 | " :return: (np.array) smoothed price\n", 251 | " \"\"\"\n", 252 | " a = np.exp(-1.414 * np.pi / cutoff)\n", 253 | " b = 2 * a * np.cos(1.414 * np.pi / cutoff)\n", 254 | " newseries = np.copy(series)\n", 255 | " for i in range(2, series.shape[0]):\n", 256 | " newseries[i] = (1 + a ** 2 - b) / 2 * (series[i] + series[i - 1]) \\\n", 257 | " + b * newseries[i - 1] - a ** 2 * newseries[i - 2]\n", 258 | " return newseries" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "### Super Smoother 3 poles\n", 266 | "cutoff $(0, +\\infty)$: for wave that has a period less than the cutoff period (i.e higher frequency part), its power is lessened to 1/2 or more." 267 | ] 268 | }, 269 | { 270 | "cell_type": "code", 271 | "execution_count": 12, 272 | "metadata": {}, 273 | "outputs": [], 274 | "source": [ 275 | "def supersmoother3pole(series, cutoff):\n", 276 | " \"\"\"\n", 277 | " simplified 3 pole butterworth smoother\n", 278 | " :param series: (np.array) price\n", 279 | " :param cutoff: (float) cutoff period of the filter\n", 280 | " :return: (np.array) smoothed price\n", 281 | " \"\"\"\n", 282 | " a = np.exp(-np.pi / cutoff)\n", 283 | " b = 2 * a * np.cos(1.738 * np.pi / cutoff)\n", 284 | " c = a ** 2\n", 285 | " newseries = np.copy(series)\n", 286 | " for i in range(3, series.shape[0]):\n", 287 | " newseries[i] = (1 - c ** 2 - b + b * c) * series[i] \\\n", 288 | " + (b + c) * newseries[i - 1] + (-c - b * c) * newseries[i - 2] + (c ** 2) * newseries[i - 3]\n", 289 | " return newseries" 290 | ] 291 | }, 292 | { 293 | "cell_type": "markdown", 294 | "metadata": {}, 295 | "source": [ 296 | "### Allpass filter" 297 | ] 298 | }, 299 | { 300 | "cell_type": "code", 301 | "execution_count": 13, 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "def allpass(series, alpha):\n", 306 | " \"\"\"\n", 307 | " allpass filter (used in laguerre filter)\n", 308 | " :param series: (np.array) price\n", 309 | " :param alpha: (float) damping factor\n", 310 | " :return: (np.array) filtered price\n", 311 | " \"\"\"\n", 312 | " newseries = np.copy(series)\n", 313 | " for i in range(1, series.shape[0]):\n", 314 | " newseries[i] = -alpha * series[i] + series[i - 1] \\\n", 315 | " + alpha * newseries[i - 1]\n", 316 | " return newseries" 317 | ] 318 | }, 319 | { 320 | "cell_type": "markdown", 321 | "metadata": {}, 322 | "source": [ 323 | "### Laguerre filter" 324 | ] 325 | }, 326 | { 327 | "cell_type": "code", 328 | "execution_count": 14, 329 | "metadata": {}, 330 | "outputs": [], 331 | "source": [ 332 | "def laguerre(series, gamma):\n", 333 | " \"\"\"\n", 334 | " Laguerre filter\n", 335 | " :param series: (np.array) price\n", 336 | " :param gamma: (float) damping factor\n", 337 | " :return: (np.array) filtered price\n", 338 | " \"\"\"\n", 339 | " l0 = ema(series, gamma)\n", 340 | " l1 = allpass(l0, gamma)\n", 341 | " l2 = allpass(l1, gamma)\n", 342 | " l3 = allpass(l2, gamma)\n", 343 | " return l0, l1, l2, l3" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "metadata": {}, 349 | "source": [ 350 | "### Roofing Filter" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 15, 356 | "metadata": {}, 357 | "outputs": [], 358 | "source": [ 359 | "def roofing(series, cutoff_hp, cutoff_lp):\n", 360 | " \"\"\"\n", 361 | " roofing filter\n", 362 | " :param series: (np.array) price\n", 363 | " :param cutoff_hp: (np.array) cutoff period for highpass filter\n", 364 | " :param cutoff_lp: (np.array) cutoff period for lowpass filter\n", 365 | " :return:\n", 366 | " \"\"\"\n", 367 | " hp = highpass2pole(series, cutoff_hp)\n", 368 | " newseries = supersmoother3pole(hp, cutoff_lp)\n", 369 | " return newseries" 370 | ] 371 | }, 372 | { 373 | "cell_type": "markdown", 374 | "metadata": {}, 375 | "source": [ 376 | "### Adaptive highpass filter 2 poles\n", 377 | "This filter is written for adaptive methods. It takes a series of lag rather than one constant lag as parameter. This input lag series is computed with other methods which approximate the dominant cycle period of the data, such as Hilbert Transformation method for computing period." 378 | ] 379 | }, 380 | { 381 | "cell_type": "code", 382 | "execution_count": 16, 383 | "metadata": {}, 384 | "outputs": [], 385 | "source": [ 386 | "def ad_highpass2pole(series, lag):\n", 387 | " \"\"\"\n", 388 | " 2 pole adaptive high-pass filter (variable cutoff period)\n", 389 | " :param series: (np.array) price\n", 390 | " :param lag: (np.array) lag of the filter\n", 391 | " :return: (np.array) filtered price\n", 392 | " \"\"\"\n", 393 | " alpha = 1 / (1 + lag)\n", 394 | " newseries = np.copy(series)\n", 395 | " for i in range(2, series.shape[0]):\n", 396 | " newseries[i] = (1 - alpha[i] / 2) ** 2 * series[i] \\\n", 397 | " - 2 * (1 - alpha[i] / 2) ** 2 * series[i - 1] \\\n", 398 | " + (1 - alpha[i] / 2) ** 2 * series[i - 2] \\\n", 399 | " + 2 * (1 - alpha[i]) * newseries[i - 1] - (1 - alpha[i]) ** 2 * newseries[i - 2]\n", 400 | " return newseries" 401 | ] 402 | }, 403 | { 404 | "cell_type": "markdown", 405 | "metadata": {}, 406 | "source": [ 407 | "## II. Indicator Transformer\n", 408 | "### Stochasticize" 409 | ] 410 | }, 411 | { 412 | "cell_type": "code", 413 | "execution_count": 17, 414 | "metadata": {}, 415 | "outputs": [], 416 | "source": [ 417 | "def stoch(series, period):\n", 418 | " \"\"\"\n", 419 | " stochaticize the indicator\n", 420 | " :param series: (np.array) indicator or price\n", 421 | " :param period: (int) window length\n", 422 | " :return: (np.array) series of value within [0,1]\n", 423 | " \"\"\"\n", 424 | " df = pd.Series(series)\n", 425 | " df_max = df.rolling(period, min_periods=1).max()\n", 426 | " df_min = df.rolling(period, min_periods=1).min()\n", 427 | " series = (df - df_min) / (df_max - df_min)\n", 428 | " return series.to_numpy()" 429 | ] 430 | }, 431 | { 432 | "cell_type": "markdown", 433 | "metadata": {}, 434 | "source": [ 435 | "### Fisher Transformer\n", 436 | "normalize indicators so that it satisfies statistic inference of normally distributed data\n", 437 | "\n", 438 | "period $(0, +\\infty)$: window length for applying fisher transformation and stochasticize\n", 439 | "\n", 440 | "stoch_time $(0, +\\infty)$: number of time applying stochastic transformation" 441 | ] 442 | }, 443 | { 444 | "cell_type": "code", 445 | "execution_count": 18, 446 | "metadata": {}, 447 | "outputs": [], 448 | "source": [ 449 | "def fisher(series, period, stoch_time):\n", 450 | " \"\"\"\n", 451 | " fisher transformer\n", 452 | " :param series: (np.array) indicator or price\n", 453 | " :param period: (int) window length\n", 454 | " :param stoch_time: (int) number of time applying stochastic transformation\n", 455 | " :return: (np.array) normalized series satisfying statistic inference of normally distributed data\n", 456 | " \"\"\"\n", 457 | " # stochaticize\n", 458 | " for i in range(stoch_time):\n", 459 | " series = stoch(series, period)\n", 460 | " # transform data to [-0.9999,0.9999]\n", 461 | " series = 2 * (series - 0.5)\n", 462 | " for i in range(series.shape[0]):\n", 463 | " series[i] = max(-0.9999, min(0.9999, series[i]))\n", 464 | " # apply fisher transformation\n", 465 | " series = np.log((1 + series) / (1 - series)) / 2\n", 466 | " return series" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": {}, 472 | "source": [ 473 | "## III. Method for computing period\n", 474 | "According to my observation, this approximation is not a very good one and is rather unstable. So the indicators that based on it -- Adaptive Momentum, Adaptive CCI etc. -- are also unreliable. " 475 | ] 476 | }, 477 | { 478 | "cell_type": "code", 479 | "execution_count": 19, 480 | "metadata": {}, 481 | "outputs": [], 482 | "source": [ 483 | "def hilbert(series):\n", 484 | " \"\"\"\n", 485 | " Hilbert transformation\n", 486 | " :param series: (np.array) price\n", 487 | " :return: (np.array) InPhase and Quadrature term\n", 488 | " \"\"\"\n", 489 | " Q = 0.0962 * series + 0.5796 * np.roll(series, 2) \\\n", 490 | " - 0.5796 * np.roll(series, 4) - 0.0962 * np.roll(series, 6)\n", 491 | " Q[0:6] = 0\n", 492 | " I = np.roll(series, 3)\n", 493 | " I[0:3] = 0\n", 494 | " return Q, I\n", 495 | "\n", 496 | "\n", 497 | "def compute_period(series, cutoff):\n", 498 | " \"\"\"\n", 499 | " use hilbert transformation to compute period for adaptive methods\n", 500 | " :param series: (np.array) price\n", 501 | " :param cutoff: (np.array) cutoff period for highpass filter\n", 502 | " :return: (np.array) period and cycle\n", 503 | " \"\"\"\n", 504 | " smooth = sma4(series)\n", 505 | " cycle = highpass2pole(smooth, cutoff)\n", 506 | " for i in range(2, 7):\n", 507 | " cycle[i] = (series[i] - 2 * series[i - 1] + series[i - 2]) / 4\n", 508 | " delta_phase = np.zeros_like(series)\n", 509 | " inst_period = np.zeros_like(series)\n", 510 | " period = np.zeros_like(series)\n", 511 | " Q, I = hilbert(cycle)\n", 512 | " for i in range(6, series.shape[0]):\n", 513 | " Q[i] *= 0.5 + 0.08 * inst_period[i - 1]\n", 514 | " if Q[i] != 0 and Q[i - 1] != 0:\n", 515 | " delta_phase[i] = (I[i] / Q[i] - I[i - 1] / Q[i - 1]) / (1 + I[i] * I[i - 1] / (Q[i] * Q[i - 1]))\n", 516 | " delta_phase[i] = max(0.1, min(1.1, delta_phase[i]))\n", 517 | " median_delta = np.median(delta_phase[i - 4:i + 1])\n", 518 | " if median_delta == 0:\n", 519 | " DC = 15\n", 520 | " else:\n", 521 | " DC = 6.29318 / median_delta + 0.5\n", 522 | " inst_period[i] = 0.33 * DC + 0.67 * inst_period[i - 1]\n", 523 | " period[i] = 0.15 * inst_period[i] + 0.85 * period[i - 1]\n", 524 | " return period, cycle" 525 | ] 526 | }, 527 | { 528 | "cell_type": "markdown", 529 | "metadata": {}, 530 | "source": [ 531 | "## IV. Trend Indicator\n", 532 | "### Instantaneous Trendline\n", 533 | "\n", 534 | "1) **method**\n", 535 | " \n", 536 | "Use a highpass (2 poles) filter to smooth the price series.\n", 537 | "\n", 538 | "Note: The first 7 price is computed separately by a SMA4 to reduce the influence of lack of samples at the beginning of the series, however in our case it is rather unnecessary because ① we have a initial period for contract to avoid this influence and ② the number 7 needs to be tailored for our situation as the book uses daily price input.\n", 539 | " \n", 540 | "2) **parameter range**\n", 541 | " \n", 542 | "* cutoff $(0, +\\infty)$: cutoff period of hp\n", 543 | " \n", 544 | "3) **trading strategy**\n", 545 | " \n", 546 | "$trigger = 2 * trend - lag\\_trend$\n", 547 | "\n", 548 | "Buy when trigger is above trend, short otherwise " 549 | ] 550 | }, 551 | { 552 | "cell_type": "code", 553 | "execution_count": 20, 554 | "metadata": {}, 555 | "outputs": [], 556 | "source": [ 557 | "def i_trend(series, cutoff):\n", 558 | " \"\"\"\n", 559 | " instantaneous trendline\n", 560 | " :param series: (np.array) price\n", 561 | " :param cutoff: (float) cutoff period of the hp\n", 562 | " :return: (np.array) trend and its trigger\n", 563 | " \"\"\"\n", 564 | " # compute inst trend\n", 565 | " K = 0.707\n", 566 | " alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff)\n", 567 | " it = np.copy(series)\n", 568 | " for i in range(2, 7):\n", 569 | " it[i] = (series[i] + 2 * series[i - 1] + series[i - 2]) / 4\n", 570 | " for i in range(7, series.shape[0]):\n", 571 | " it[i] = (alpha - alpha ** 2 / 4) * series[i] \\\n", 572 | " + alpha ** 2 / 2 * series[i - 1] \\\n", 573 | " - (alpha - alpha ** 2 * 3 / 4) * series[i - 2] \\\n", 574 | " + 2 * (1 - alpha) * it[i - 1] - (1 - alpha) ** 2 * it[i - 2]\n", 575 | "\n", 576 | " # compute lead 2 trigger & signal\n", 577 | " lag2 = np.roll(it, 20)\n", 578 | " lag2[:20] = it[:20]\n", 579 | " trigger = 2 * it - lag2\n", 580 | " return it, trigger" 581 | ] 582 | }, 583 | { 584 | "cell_type": "markdown", 585 | "metadata": {}, 586 | "source": [ 587 | "\n", 588 | "### Smoothed Adaptive Monmentum Indicator\n", 589 | "\n", 590 | "1) **method**\n", 591 | "\n", 592 | "compare the price in the current cycle with that in the previous cycle (same phase) to indicate an uptrend or downtrend\n", 593 | "\n", 594 | "2) **parameter range**\n", 595 | " \n", 596 | "* cutoff_period $(0, +\\infty)$: the cutoff period used to compute the period using Hilbert Transformation\n", 597 | "\n", 598 | "* cutoff_signal $(0, +\\infty)$: the cutoff period used to smooth the signal\n", 599 | " \n", 600 | "3) **trading strategy**\n", 601 | " \n", 602 | "Buy when the smoothed signal is above 0 (in a uptrend), short otherwise. " 603 | ] 604 | }, 605 | { 606 | "cell_type": "code", 607 | "execution_count": 21, 608 | "metadata": {}, 609 | "outputs": [], 610 | "source": [ 611 | "def ad_momentum(series, cutoff_period, cutoff_signal):\n", 612 | " \"\"\"\n", 613 | " smoothed adaptive momentum indicator\n", 614 | " compare the price in the current cycle with that in the previous cycle (same phase)\n", 615 | " to indicate an uptrend or downtrend\n", 616 | " :param series: (np.array) price\n", 617 | " :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation\n", 618 | " :param cutoff_signal: (float) the cutoff period used to smooth the signal\n", 619 | " :return: (np.array) momentum\n", 620 | " \"\"\"\n", 621 | " period, _ = compute_period(series, cutoff_period)\n", 622 | " period = period.astype(np.int)\n", 623 | " momen = np.zeros_like(series)\n", 624 | " for i in range(series.shape[0]):\n", 625 | " if (i - period[i]) >= 0:\n", 626 | " momen[i] = series[i] - series[i - period[i]]\n", 627 | " momen = supersmoother3pole(momen, cutoff_signal)\n", 628 | " return momen" 629 | ] 630 | }, 631 | { 632 | "cell_type": "markdown", 633 | "metadata": {}, 634 | "source": [ 635 | "## V. Oscillator\n", 636 | "### Decycler Oscillator\n", 637 | "\n", 638 | "1) **method**\n", 639 | " \n", 640 | "take the difference of 2 decyclers (highpass filters) with different cutoff to get the cycle\n", 641 | " \n", 642 | "2) **parameter range**\n", 643 | " \n", 644 | "* cutoff1 $(0, +\\infty)$: cutoff period of the first highpass filter\n", 645 | "\n", 646 | "* times $(1, +\\infty)$: $\\frac{\\text{cutoff period of the second highpass filter}}{\\text{cutoff period of the first highpass filter}}$\n", 647 | "\n", 648 | "3) **trading strategy**\n", 649 | " \n", 650 | "Buy when above 0 (in a uptrend), short otherwise. " 651 | ] 652 | }, 653 | { 654 | "cell_type": "code", 655 | "execution_count": 22, 656 | "metadata": {}, 657 | "outputs": [], 658 | "source": [ 659 | "def decycler_oscillator(series, cutoff1, times):\n", 660 | " \"\"\"\n", 661 | " decycler oscillator\n", 662 | " take the difference of 2 decyclers with different cutoff\n", 663 | " :param series: (np.array) price\n", 664 | " :param cutoff1: (float) the smaller cutoff period\n", 665 | " :param times: (float, >1) larger cutoff / smaller cutoff\n", 666 | " :return: (np.array) indicator\n", 667 | " \"\"\"\n", 668 | " cutoff2 = cutoff1 * times\n", 669 | " hp1 = highpass(series, cutoff1)\n", 670 | " hp2 = highpass(series, cutoff2)\n", 671 | " delta_hp = hp2 - hp1\n", 672 | " # >0: uptrend, <0: downtrend\n", 673 | " return delta_hp" 674 | ] 675 | }, 676 | { 677 | "cell_type": "markdown", 678 | "metadata": {}, 679 | "source": [ 680 | "### Bandpass Filter\n", 681 | "\n", 682 | "1) **method**\n", 683 | " \n", 684 | "* use a highpass filter to avoid spectral dilation\n", 685 | "\n", 686 | "* pass a bandpass filter\n", 687 | "\n", 688 | "* use \"fast attack-slow decay\" approach to normalize the indicator to $[-1, 1]$\n", 689 | " \n", 690 | "2) **parameter range**\n", 691 | " \n", 692 | "* cycle $(0, +\\infty)$: center of the bandpass period\n", 693 | "\n", 694 | "* bandwidth $(0, 2)$: $\\frac{\\text{bandwidth (in period)}}{\\text{cycle}}$\n", 695 | "\n", 696 | "3) **trading strategy**\n", 697 | " \n", 698 | "Trigger line is created by pass the bandpass result through another highpass filter.\n", 699 | "\n", 700 | "Buy when the trigger crosses over the bandpass result, short when crosses under." 701 | ] 702 | }, 703 | { 704 | "cell_type": "code", 705 | "execution_count": 23, 706 | "metadata": {}, 707 | "outputs": [], 708 | "source": [ 709 | "def bandpass(series, cycle, bandwidth):\n", 710 | " \"\"\"\n", 711 | " bandpass filter\n", 712 | " :param series: (np.array) price\n", 713 | " :param cycle: (float) cycle period\n", 714 | " :param bandwidth: (float, >0 <2) length between left and right cutoffs / cycle period\n", 715 | " :return: (np.array) indicator\n", 716 | " \"\"\"\n", 717 | " # pass a HP to avoid spectral dilation of BP\n", 718 | " hp = highpass(series, 4 * cycle / bandwidth)\n", 719 | " # bandpass filter\n", 720 | " lmd = np.cos(2 * np.pi / cycle)\n", 721 | " gamma = np.cos(2 * np.pi * bandwidth / cycle)\n", 722 | " sigma = 1 / gamma - np.sqrt(1 / gamma ** 2 - 1)\n", 723 | " bp = np.copy(hp)\n", 724 | " for i in range(2, series.shape[0]):\n", 725 | " bp[i] = (1 - sigma) / 2 * hp[i] - (1 - sigma) / 2 * hp[i - 2] \\\n", 726 | " + lmd * (1 + sigma) * bp[i - 1] - sigma * bp[i - 2]\n", 727 | " # fast attack-slow decay AGC\n", 728 | " K = 0.991\n", 729 | " peak = np.copy(bp)\n", 730 | " for i in range(series.shape[0]):\n", 731 | " if i > 0:\n", 732 | " peak[i] = peak[i - 1] * K\n", 733 | " if abs(bp[i]) > peak[i]:\n", 734 | " peak[i] = abs(bp[i])\n", 735 | " bp_normalized = bp / peak\n", 736 | " bp_normalized[np.isnan(bp_normalized)] = 0\n", 737 | " # trigger(lead) & signal\n", 738 | " trigger = highpass(bp_normalized, cycle / bandwidth / 1.5)\n", 739 | " return bp, bp_normalized, trigger" 740 | ] 741 | }, 742 | { 743 | "cell_type": "markdown", 744 | "metadata": {}, 745 | "source": [ 746 | "### Cyber Cycle Index\n", 747 | "\n", 748 | "1) **method**\n", 749 | " \n", 750 | "* smooth the price with SMA4\n", 751 | "\n", 752 | "* pass a highpass (2 poles) filter\n", 753 | "\n", 754 | "* smooth the cycle with EMA\n", 755 | " \n", 756 | "2) **parameter range**\n", 757 | " \n", 758 | "* cutoff1 $(0, +\\infty)$: cutoff period for highpass filter\n", 759 | "\n", 760 | "* cutoff2 $(0, +\\infty)$: cutoff period for EMA filter\n", 761 | "\n", 762 | "* fperiod, stoch_time: same as fisher transformation parameters\n", 763 | "\n", 764 | "3) **trading strategy**\n", 765 | " \n", 766 | "Buy when signal crosses under lag1, sell when signal crosses over lag1.\n", 767 | "\n", 768 | "Also, it requires a 'stop loss' strategy." 769 | ] 770 | }, 771 | { 772 | "cell_type": "code", 773 | "execution_count": 24, 774 | "metadata": {}, 775 | "outputs": [], 776 | "source": [ 777 | "def cci(series, cutoff1, cutoff2, fperiod=None, stoch_time=None):\n", 778 | " \"\"\"\n", 779 | " CCI - cyber cycle index\n", 780 | " delay is less than half a cycle: buy when signal cross under lag1, sell when signal cross over lag1\n", 781 | " need a 'stop loss' strategy, close out when profit<0 and bars since entry > 8(period)\n", 782 | " :param series: (np.array) price\n", 783 | " :param cutoff1: (float) cutoff period for hp\n", 784 | " :param cutoff2: (float) cutoff period for ema\n", 785 | " :param fperiod, stoch_time: (tuple: ((int)period, (int)stoch_time)) fisher transformation parameters\n", 786 | " :return: (np.array) cycle\n", 787 | " \"\"\"\n", 788 | " # compute the cycle\n", 789 | " smooth = sma4(series)\n", 790 | " cycle = highpass2pole(smooth, cutoff1)\n", 791 | " for t in range(2, 7):\n", 792 | " cycle[t] = (series[t] - 2 * series[t - 1] + series[t - 2]) / 4\n", 793 | " signal = ema(cycle, cutoff2)\n", 794 | " # apply fisher transformation\n", 795 | " if fperiod != None:\n", 796 | " signal = fisher(signal, fperiod, stoch_time)\n", 797 | " return signal" 798 | ] 799 | }, 800 | { 801 | "cell_type": "markdown", 802 | "metadata": {}, 803 | "source": [ 804 | "### center of gravity\n", 805 | "1) **method**\n", 806 | " \n", 807 | "View the price as weight to compute the center of gravity of the filter. If the center of gravity is closer to today relative to today minus the window length, it suggests that price is higher / moving upward recently.\n", 808 | " \n", 809 | "2) **parameter range**\n", 810 | " \n", 811 | "* length $(0, +\\infty)$: window length\n", 812 | "\n", 813 | "* fperiod, stoch_time: same as fisher transformation parameters\n", 814 | "\n", 815 | "3) **trading strategy**\n", 816 | " \n", 817 | "Buy when signal crosses over lag1, sell when signal crosses under lag1." 818 | ] 819 | }, 820 | { 821 | "cell_type": "code", 822 | "execution_count": 25, 823 | "metadata": {}, 824 | "outputs": [], 825 | "source": [ 826 | "def cg(series, length, fperiod=None, stoch_time=None):\n", 827 | " \"\"\"\n", 828 | " CG - center of gravity\n", 829 | " view the price as weight to compute the center of gravity of the filter\n", 830 | " :param series: (np.array) price\n", 831 | " :param length: (int) window length\n", 832 | " :param period: (int) fisher transformation parameter\n", 833 | " :param stoch_time: (int) fisher transformation parameter\n", 834 | " :return: (np.array) cg\n", 835 | " \"\"\"\n", 836 | " # compute cg\n", 837 | " num = np.zeros_like(series)\n", 838 | " denom = np.ones_like(series)\n", 839 | " for i in range(length - 1, series.shape[0]):\n", 840 | " num[i] = np.sum((np.array(range(length)) + 1) * series[i - length + 1:i + 1][::-1])\n", 841 | " denom[i] = np.sum(series[i - length + 1:i + 1])\n", 842 | " cg = -num / denom + (1 + length) / 2\n", 843 | " # apply fisher transformation\n", 844 | " if fperiod:\n", 845 | " cg = fisher(cg, fperiod, stoch_time)\n", 846 | " return cg" 847 | ] 848 | }, 849 | { 850 | "cell_type": "markdown", 851 | "metadata": {}, 852 | "source": [ 853 | "### relative vigor index\n", 854 | "1) **method**\n", 855 | " \n", 856 | "$\\text{rvi} = \\text{\"smoothed\"} \\ \\frac{\\text{c - o}}{\\text{h - l}}$\n", 857 | " \n", 858 | "2) **parameter range**\n", 859 | " \n", 860 | "* length $(0, +\\infty)$: window length\n", 861 | "\n", 862 | "* fperiod, stoch_time: same as fisher transformation parameters\n", 863 | "\n", 864 | "3) **trading strategy**\n", 865 | " \n", 866 | "Buy when signal crosses over lag1, sell when signal crosses under lag1." 867 | ] 868 | }, 869 | { 870 | "cell_type": "code", 871 | "execution_count": 26, 872 | "metadata": {}, 873 | "outputs": [], 874 | "source": [ 875 | "def rvi(o, h, l, c, length, fperiod=None, stoch_time=None):\n", 876 | " \"\"\"\n", 877 | " RVI - relative vigor index\n", 878 | " :param o: (np.array) open\n", 879 | " :param h: (np.array) high\n", 880 | " :param l: (np.array) low\n", 881 | " :param c: (np.array) close\n", 882 | " :param length: length to sum the num & denom\n", 883 | " :param period: (int) fisher transformation parameter\n", 884 | " :param stoch_time: (int) fisher transformation parameter\n", 885 | " :return: (np.array) rvi\n", 886 | " \"\"\"\n", 887 | " co = c - o\n", 888 | " hl = h - l\n", 889 | " num = sma4(co)\n", 890 | " denom = sma4(hl)\n", 891 | " rvi = np.zeros_like(o)\n", 892 | " for i in range(2 + length, o.shape[0]):\n", 893 | " rvi[i] = np.sum(num[i - length + 1:i + 1]) / np.sum(denom[i - length + 1:i + 1])\n", 894 | " # apply fisher transformation\n", 895 | " if fperiod:\n", 896 | " rvi = fisher(rvi, fperiod, stoch_time)\n", 897 | " return rvi" 898 | ] 899 | }, 900 | { 901 | "cell_type": "markdown", 902 | "metadata": {}, 903 | "source": [ 904 | "### relative strength index\n", 905 | "The famous RSI. Written here to show its similarity to the 3 cycle indicators above." 906 | ] 907 | }, 908 | { 909 | "cell_type": "code", 910 | "execution_count": 27, 911 | "metadata": {}, 912 | "outputs": [], 913 | "source": [ 914 | "def rsi(series, length, fperiod=None, stoch_time=None):\n", 915 | " \"\"\"\n", 916 | " Relative Strength Index\n", 917 | " :param series: (np.array) price\n", 918 | " :param length: length to sum the num & denom\n", 919 | " :param period: (int) fisher transformation parameter\n", 920 | " :param stoch_time: (int) fisher transformation parameter\n", 921 | " :return: (np.array) rsi\n", 922 | " \"\"\"\n", 923 | " rsi = ta.RSI(series, length)\n", 924 | " # apply fisher transformation\n", 925 | " if fperiod:\n", 926 | " rsi = fisher(rsi, fperiod, stoch_time)\n", 927 | " return rsi" 928 | ] 929 | }, 930 | { 931 | "cell_type": "markdown", 932 | "metadata": {}, 933 | "source": [ 934 | "### Laguerre RSI\n", 935 | "1) **method**\n", 936 | "\n", 937 | "Not fully understood :(\n", 938 | " \n", 939 | "2) **parameter range**\n", 940 | "\n", 941 | "* gamma $(0, 1)$: allpass filter parameter\n", 942 | "\n", 943 | "* period, stoch_time: same as fisher transformation parameters\n", 944 | "\n", 945 | "3) **trading strategy**\n", 946 | " \n", 947 | "Same as RSI." 948 | ] 949 | }, 950 | { 951 | "cell_type": "code", 952 | "execution_count": 28, 953 | "metadata": {}, 954 | "outputs": [], 955 | "source": [ 956 | "def laguerre_rsi(series, gamma, fperiod=None, stoch_time=None):\n", 957 | " \"\"\"\n", 958 | " Laguerre RSI\n", 959 | " :param series: (np.array) price\n", 960 | " :param gamma: (float) damping factor\n", 961 | " :param period: (int) fisher transformation parameter\n", 962 | " :param stoch_time: (int) fisher transformation parameter\n", 963 | " :return: (np.array) rsi\n", 964 | " \"\"\"\n", 965 | " l0, l1, l2, l3 = laguerre(series, gamma)\n", 966 | " rsi = np.zeros_like(series)\n", 967 | " for i in range(series.shape[0]):\n", 968 | " cu = 0\n", 969 | " cd = 0\n", 970 | " if l1[i] > l0[i]:\n", 971 | " cu += l1[i] - l0[i]\n", 972 | " else:\n", 973 | " cd -= l1[i] - l0[i]\n", 974 | " if l2[i] > l1[i]:\n", 975 | " cu += l2[i] - l1[i]\n", 976 | " else:\n", 977 | " cd -= l2[i] - l1[i]\n", 978 | " if l3[i] > l2[i]:\n", 979 | " cu += l3[i] - l2[i]\n", 980 | " else:\n", 981 | " cd -= l3[i] - l2[i]\n", 982 | " rsi[i] = cu / (cu + cd)\n", 983 | " # apply fisher transformation\n", 984 | " if fperiod:\n", 985 | " rsi = fisher(rsi, fperiod, stoch_time)\n", 986 | " return rsi" 987 | ] 988 | }, 989 | { 990 | "cell_type": "markdown", 991 | "metadata": {}, 992 | "source": [ 993 | "Note: The 3 indicators below requires large amounts of computation when the input max_period(max_lag) and min_period(min_lag) are large, as it needs to compute for every period between the max and the min. It can be optimized, though, if we let it memotize the situation it has computed. \n", 994 | "\n", 995 | "Within the 3 methods, this first one is the prefered one as it incurs no spectral dilation.\n", 996 | "\n", 997 | "### auto-correlation periodogram indicator\n", 998 | "\n", 999 | "1) **parameter range**\n", 1000 | "\n", 1001 | "* hp_period $(0, +\\infty)$\n", 1002 | "\n", 1003 | "* lp_period $(0, +\\infty)$\n", 1004 | "\n", 1005 | "* average_len $(0, +\\infty)$\n", 1006 | "\n", 1007 | "* max_lag, min_lag $0<\\text{min_lag}<\\text{max_lag}< +\\infty)$\n", 1008 | "\n", 1009 | "2) **trading strategy**\n", 1010 | "\n", 1011 | "Buy when signal crosses over lag1, sell when signal crosses under lag1." 1012 | ] 1013 | }, 1014 | { 1015 | "cell_type": "code", 1016 | "execution_count": 29, 1017 | "metadata": {}, 1018 | "outputs": [], 1019 | "source": [ 1020 | "def corr_periodogram(series, hp_period, lp_period, average_len, max_lag, min_lag):\n", 1021 | " \"\"\"\n", 1022 | " auto-correlation periodogram indicator: a preferred method to compute the dominant cycle\n", 1023 | " :param hp_period: highpass period to remove the trend from the original price\n", 1024 | " :param lp_period: lowpass period to smooth the original price\n", 1025 | " :param average_len: # period to compute auto-correlation\n", 1026 | " :param max_lag: max lag of period to compute auto_correlation\n", 1027 | " :param min_lag: min lag of period to compute auto_correlation\n", 1028 | " :return: (np.array) dominant cycle\n", 1029 | " \"\"\"\n", 1030 | " num = np.zeros_like(series)\n", 1031 | " denom = np.zeros_like(series)\n", 1032 | " for lag in range(min_lag, max_lag):\n", 1033 | "\n", 1034 | " rho = corr(series, hp_period, lp_period, average_len, lag)\n", 1035 | " cos_part = np.zeros_like(series)\n", 1036 | " sin_part = np.zeros_like(series)\n", 1037 | " for i in range(max_lag):\n", 1038 | " cos_part += np.roll(rho, i) * np.cos(2 * np.pi * i / max_lag)\n", 1039 | " sin_part += np.roll(rho, i) * np.sin(2 * np.pi * i / max_lag)\n", 1040 | " sqsum = cos_part ** 2 + sin_part ** 2\n", 1041 | "\n", 1042 | " for i in range(1, sqsum.shape[0]):\n", 1043 | " sqsum[i] = 0.2 * sqsum[i] + 0.8 * sqsum[i - 1]\n", 1044 | "\n", 1045 | " K = 0.991\n", 1046 | " peak = np.copy(sqsum)\n", 1047 | " for i in range(sqsum.shape[0]):\n", 1048 | " if i > 0:\n", 1049 | " peak[i] = peak[i - 1] * K\n", 1050 | " if abs(sqsum[i]) > peak[i]:\n", 1051 | " peak[i] = abs(sqsum[i])\n", 1052 | " sqsum = sqsum / peak\n", 1053 | " sqsum[np.isnan(sqsum)] = 0\n", 1054 | "\n", 1055 | " num += (sqsum > 0.5) * sqsum * lag\n", 1056 | " denom += (sqsum > 0.5) * sqsum\n", 1057 | "\n", 1058 | " dc = num / denom\n", 1059 | " dc[np.isnan(dc)] = 0\n", 1060 | " return dc" 1061 | ] 1062 | }, 1063 | { 1064 | "cell_type": "markdown", 1065 | "metadata": {}, 1066 | "source": [ 1067 | "### center of gravity indicator based on discrete Fourier tansformation\n", 1068 | "1) **parameter range**\n", 1069 | "\n", 1070 | "* hp_period $(0, +\\infty)$\n", 1071 | "\n", 1072 | "* lp_period $(0, +\\infty)$\n", 1073 | "\n", 1074 | "* max_period, min_period $0<\\text{min_period}<\\text{max_period}< +\\infty)$\n", 1075 | "\n", 1076 | "2) **trading strategy**\n", 1077 | "\n", 1078 | "Buy when signal crosses over lag1, sell when signal crosses under lag1." 1079 | ] 1080 | }, 1081 | { 1082 | "cell_type": "code", 1083 | "execution_count": 30, 1084 | "metadata": {}, 1085 | "outputs": [], 1086 | "source": [ 1087 | "def dft(series, hp_period, lp_period, dft_period):\n", 1088 | " \"\"\"\n", 1089 | " discrete Fourier Transformation\n", 1090 | " :param hp_period: highpass period to remove the trend from the original price\n", 1091 | " :param lp_period: lowpass period to smooth the original price\n", 1092 | " :param dft_period: period to compute Fourier transformation\n", 1093 | " :return: (np.array) normalized power\n", 1094 | " \"\"\"\n", 1095 | " HP = highpass2pole(series, hp_period)\n", 1096 | " filt = supersmoother2pole(HP, lp_period)\n", 1097 | "\n", 1098 | " cos_part = np.zeros_like(series)\n", 1099 | " sin_part = np.zeros_like(series)\n", 1100 | " for i in range(dft_period):\n", 1101 | " cos_part += np.roll(filt, i) * np.cos(2 * np.pi * i / dft_period) / dft_period\n", 1102 | " sin_part += np.roll(filt, i) * np.sin(2 * np.pi * i / dft_period) / dft_period\n", 1103 | " pwr = cos_part ** 2 + sin_part ** 2\n", 1104 | "\n", 1105 | " K = 0.991\n", 1106 | " peak = np.copy(pwr)\n", 1107 | " for i in range(series.shape[0]):\n", 1108 | " if i > 0:\n", 1109 | " peak[i] = peak[i - 1] * K\n", 1110 | " if abs(pwr[i]) > peak[i]:\n", 1111 | " peak[i] = abs(pwr[i])\n", 1112 | " pwr = pwr / peak\n", 1113 | " pwr[np.isnan(pwr)] = 0\n", 1114 | " return pwr\n", 1115 | "\n", 1116 | "\n", 1117 | "def dft_cg(series, hp_period, lp_period, max_period, min_period):\n", 1118 | " \"\"\"\n", 1119 | " center of gravity indicator based on discrete Fourier tansformation: indicates dominant cycle\n", 1120 | " :param hp_period: highpass period to remove the trend from the original price\n", 1121 | " :param lp_period: lowpass period to smooth the original price\n", 1122 | " :param max_period: max period to compute Fourier transformation\n", 1123 | " :param min_period: min period to compute Fourier transformation\n", 1124 | " :return: (np.array) dominant cycle\n", 1125 | " \"\"\"\n", 1126 | " num = np.zeros_like(series)\n", 1127 | " denom = np.zeros_like(series)\n", 1128 | " for period in range(min_period, max_period):\n", 1129 | " pwr = dft(series, hp_period, lp_period, period)\n", 1130 | " num += (pwr>0.5)*pwr*period\n", 1131 | " denom += (pwr>0.5)*pwr\n", 1132 | " dc = num/denom\n", 1133 | " dc[np.isnan(dc)] = 0\n", 1134 | " return dc" 1135 | ] 1136 | }, 1137 | { 1138 | "cell_type": "markdown", 1139 | "metadata": {}, 1140 | "source": [ 1141 | "### comb filter\n", 1142 | "1) **parameter range**\n", 1143 | "\n", 1144 | "* hp_period $(0, +\\infty)$\n", 1145 | "\n", 1146 | "* lp_period $(0, +\\infty)$\n", 1147 | "\n", 1148 | "* max_period, min_period $0<\\text{min_period}<\\text{max_period}< +\\infty)$\n", 1149 | "\n", 1150 | "* bandwidth $(0, 2)$\n", 1151 | "\n", 1152 | "2) **trading strategy**\n", 1153 | "\n", 1154 | "Buy when signal crosses over lag1, sell when signal crosses under lag1." 1155 | ] 1156 | }, 1157 | { 1158 | "cell_type": "code", 1159 | "execution_count": 31, 1160 | "metadata": {}, 1161 | "outputs": [], 1162 | "source": [ 1163 | "def comb(series, hp_period, lp_period, max_period, min_period, bandwidth):\n", 1164 | " \"\"\"\n", 1165 | " comb filter spectral estimate: compute the dominant cycle\n", 1166 | " :param hp_period: highpass period to remove the trend from the original price\n", 1167 | " :param lp_period: lowpass period to smooth the original price\n", 1168 | " :param max_period: max period to compute bandpass\n", 1169 | " :param min_period: min period to compute bandpass\n", 1170 | " :param bandwidth: bandwidth for bandpass filter\n", 1171 | " :return: (np.array) dominant cycle\n", 1172 | " \"\"\"\n", 1173 | " num = np.zeros_like(series)\n", 1174 | " denom = np.zeros_like(series)\n", 1175 | " HP = highpass2pole(series, hp_period)\n", 1176 | " filt = supersmoother2pole(HP, lp_period)\n", 1177 | " for period in range(min_period, max_period):\n", 1178 | "\n", 1179 | " bp, _, _ = bandpass(filt, period, bandwidth)\n", 1180 | " pwr = np.zeros_like(bp)\n", 1181 | " for i in range(period):\n", 1182 | " pwr += np.roll(bp, i) ** 2 / period ** 2\n", 1183 | "\n", 1184 | " K = 0.991\n", 1185 | " peak = np.copy(pwr)\n", 1186 | " for i in range(pwr.shape[0]):\n", 1187 | " if i > 0:\n", 1188 | " peak[i] = peak[i - 1] * K\n", 1189 | " if abs(pwr[i]) > peak[i]:\n", 1190 | " peak[i] = abs(pwr[i])\n", 1191 | " pwr = pwr / peak\n", 1192 | " pwr[np.isnan(pwr)] = 0\n", 1193 | "\n", 1194 | " num += (pwr > 0.5) * pwr * period\n", 1195 | " denom += (pwr > 0.5) * pwr\n", 1196 | "\n", 1197 | " dc = num / denom\n", 1198 | " dc[np.isnan(dc)] = 0\n", 1199 | " return dc" 1200 | ] 1201 | }, 1202 | { 1203 | "cell_type": "markdown", 1204 | "metadata": {}, 1205 | "source": [ 1206 | "## VI. Predictive Method\n", 1207 | "These indicators has the ability to actually predict the price in the future.\n", 1208 | "### Hilbert Indicator\n", 1209 | "1) **method**\n", 1210 | "\n", 1211 | "Use the Hilbert Transformation to create the real line and imaginary line.\n", 1212 | " \n", 1213 | "2) **parameter range**\n", 1214 | "\n", 1215 | "* hp_period $(0, +\\infty)$\n", 1216 | "\n", 1217 | "* lp_period $(0, +\\infty)$\n", 1218 | "\n", 1219 | "* smooth_period $(0, +\\infty)$\n", 1220 | "\n", 1221 | "3) **trading strategy**\n", 1222 | "\n", 1223 | "Buy when the imaginary line crosses above the real line, short otherwise." 1224 | ] 1225 | }, 1226 | { 1227 | "cell_type": "code", 1228 | "execution_count": 32, 1229 | "metadata": {}, 1230 | "outputs": [], 1231 | "source": [ 1232 | "def hilbert_indicator(series, hp_period, lp_period, smooth_period):\n", 1233 | " \"\"\"\n", 1234 | " hilbert transformation indicator: the real line moves as the original price, while the imaginary line as predictor\n", 1235 | " :param series: (np.array) price\n", 1236 | " :param hp_period: highpass period to remove the trend from the original price\n", 1237 | " :param lp_period: lowpass period to smooth the original price\n", 1238 | " :param smooth_period: lowpass period to smooth the imaginary line\n", 1239 | " :return: (np.array) the real and imag line\n", 1240 | " \"\"\"\n", 1241 | " HP = highpass2pole(series, hp_period)\n", 1242 | " filt = supersmoother2pole(HP, lp_period)\n", 1243 | "\n", 1244 | " K = 0.991\n", 1245 | " peak = np.copy(filt)\n", 1246 | " for i in range(series.shape[0]):\n", 1247 | " if i > 0:\n", 1248 | " peak[i] = peak[i - 1] * K\n", 1249 | " if abs(filt[i]) > peak[i]:\n", 1250 | " peak[i] = abs(filt[i])\n", 1251 | " real = filt / peak\n", 1252 | " real[np.isnan(real)] = 0\n", 1253 | "\n", 1254 | " quad = real - np.roll(real, 1)\n", 1255 | " quad[0] = 0\n", 1256 | " K = 0.991\n", 1257 | " peak = np.copy(quad)\n", 1258 | " for i in range(series.shape[0]):\n", 1259 | " if i > 0:\n", 1260 | " peak[i] = peak[i - 1] * K\n", 1261 | " if abs(quad[i]) > peak[i]:\n", 1262 | " peak[i] = abs(quad[i])\n", 1263 | " quad = quad / peak\n", 1264 | " quad[np.isnan(quad)] = 0\n", 1265 | "\n", 1266 | " imag = supersmoother2pole(quad, smooth_period)\n", 1267 | "\n", 1268 | " return real, imag" 1269 | ] 1270 | }, 1271 | { 1272 | "cell_type": "markdown", 1273 | "metadata": {}, 1274 | "source": [ 1275 | "### Sinewave Indicator\n", 1276 | "1) **method**\n", 1277 | "\n", 1278 | "Predict the price as a sinewave whose period is changing and is computed using Hilbert Transformation\n", 1279 | " \n", 1280 | "2) **parameter range**\n", 1281 | "\n", 1282 | "* cutoff_period $(0, +\\infty)$: cutoff period for Hilbert Transformation\n", 1283 | "\n", 1284 | "* lead $(0, 360)$: lead angle for the trigger\n", 1285 | "\n", 1286 | "3) **trading strategy**\n", 1287 | "\n", 1288 | "Trigger is the predicted sinewave plus an angle.\n", 1289 | "\n", 1290 | "Buy when trigger crosses over sinewave, short otherwise.\n", 1291 | " " 1292 | ] 1293 | }, 1294 | { 1295 | "cell_type": "code", 1296 | "execution_count": 33, 1297 | "metadata": {}, 1298 | "outputs": [], 1299 | "source": [ 1300 | "def sinewave(series, cutoff_period, lead=0.25 * np.pi):\n", 1301 | " \"\"\"\n", 1302 | " sinewave indicator\n", 1303 | " :param series: (np.array) price\n", 1304 | " :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation\n", 1305 | " :param lead: (float) lead angle, in radians\n", 1306 | " :return: (np.array) sinewave and leadsine\n", 1307 | " \"\"\"\n", 1308 | " # compute period\n", 1309 | " period, cycle = compute_period(series, cutoff_period)\n", 1310 | " dcperiod = period.astype(np.int)\n", 1311 | " # compute dominant cycle phase\n", 1312 | " real = np.zeros_like(series)\n", 1313 | " imag = np.zeros_like(series)\n", 1314 | " dcphase = np.zeros_like(series)\n", 1315 | " for i in range(series.shape[0]):\n", 1316 | " for j in range(dcperiod[i]):\n", 1317 | " real[i] += np.sin(2 * np.pi * j / dcperiod[i]) * cycle[i]\n", 1318 | " imag[i] += np.cos(2 * np.pi * j / dcperiod[i]) * cycle[i]\n", 1319 | " if abs(imag[i] > 0.001):\n", 1320 | " dcphase[i] = np.arctan(real[i] / imag[i])\n", 1321 | " else:\n", 1322 | " dcphase[i] = 0.5 * np.pi * np.sign(real[i])\n", 1323 | " dcphase[i] += 0.5 * np.pi\n", 1324 | " if imag[i] < 0:\n", 1325 | " dcphase[i] += np.pi\n", 1326 | " if dcphase[i] > 1.75 * np.pi:\n", 1327 | " dcphase[i] -= 2 * np.pi\n", 1328 | " # compute sinewave\n", 1329 | " sinewave = np.sin(dcphase)\n", 1330 | " leadsine = np.sin(dcphase + lead)\n", 1331 | " return sinewave, leadsine" 1332 | ] 1333 | }, 1334 | { 1335 | "cell_type": "markdown", 1336 | "metadata": {}, 1337 | "source": [ 1338 | "### Even Better Sinewave Indicator\n", 1339 | "1) **method**\n", 1340 | "\n", 1341 | "* Pass the data in a roofing filter\n", 1342 | "\n", 1343 | "* smoothed $\\frac{\\text(wave)}{\\text{power}}$\n", 1344 | " \n", 1345 | "2) **parameter range**\n", 1346 | "\n", 1347 | "* hp_period $(0, +\\infty)$\n", 1348 | "\n", 1349 | "* lp_period $(0, +\\infty)$\n", 1350 | "\n", 1351 | "3) **trading strategy**\n", 1352 | "\n", 1353 | "Buy when the normalized wave exceeds some upper bound, short when below some lower bound.\n", 1354 | " " 1355 | ] 1356 | }, 1357 | { 1358 | "cell_type": "code", 1359 | "execution_count": 34, 1360 | "metadata": {}, 1361 | "outputs": [], 1362 | "source": [ 1363 | "def better_sinewave(series, hp_period, lp_period):\n", 1364 | " \"\"\"\n", 1365 | " the even better sinewave indicator: profits better when the market is in a trend mode\n", 1366 | " :param hp_period: highpass period to remove the trend from the original price\n", 1367 | " :param lp_period: lowpass period to smooth the original price\n", 1368 | " :return: (np.array) sinewave\n", 1369 | " \"\"\"\n", 1370 | " HP = highpass(series, hp_period)\n", 1371 | " filt = supersmoother2pole(HP, lp_period)\n", 1372 | " wave = (filt + np.roll(filt,1) + np.roll(filt,2))/3\n", 1373 | " wave[0] = filt[0]\n", 1374 | " wave[1] = (filt[0]+filt[1])/2\n", 1375 | " pwr = (filt**2 + np.roll(filt,1)**2 + np.roll(filt,2)**2)/3\n", 1376 | " pwr[0] = filt[0]**2\n", 1377 | " pwr[1] = (filt[0]**2+filt[1]**2)/2\n", 1378 | " wave = wave/np.sqrt(pwr)\n", 1379 | " wave[np.isnan(wave)] = 0\n", 1380 | " return wave" 1381 | ] 1382 | }, 1383 | { 1384 | "cell_type": "markdown", 1385 | "metadata": {}, 1386 | "source": [ 1387 | "## VII. Adaptive Methods\n", 1388 | "The word adaptive means that in these indicators, period is viewed as an endogenous variable rather than a constant input parameter. Theoretically, if the indicator with an constant period works well, and if the period is approximated well, the adaptive method should work even better as it allows the period to change.\n", 1389 | "\n", 1390 | "Unfortunately in our case, as far as I know, neither of the \"if\" is satisfied." 1391 | ] 1392 | }, 1393 | { 1394 | "cell_type": "code", 1395 | "execution_count": 35, 1396 | "metadata": {}, 1397 | "outputs": [], 1398 | "source": [ 1399 | "def ad_cci(series, cutoff_period, cutoff_signal, fperiod=None, stoch_time=None):\n", 1400 | " \"\"\"\n", 1401 | " adaptive cyber cycle\n", 1402 | " :param series: (np.array) price\n", 1403 | " :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation\n", 1404 | " :param cutoff_signal: (float) the cutoff period used to smooth the signal\n", 1405 | " :param period: (int) fisher transformation parameter\n", 1406 | " :param stoch_time: (int) fisher transformation parameter\n", 1407 | " :return: (np.array) cycle\n", 1408 | " \"\"\"\n", 1409 | " # compute period\n", 1410 | " period, _ = compute_period(series, cutoff_period)\n", 1411 | " # compute the cycle\n", 1412 | " smooth = sma4(series)\n", 1413 | " cycle = ad_highpass2pole(smooth, period)\n", 1414 | " for t in range(2, 7):\n", 1415 | " cycle[t] = (series[t] - 2 * series[t - 1] + series[t - 2]) / 4\n", 1416 | " signal = ema(cycle, cutoff_signal)\n", 1417 | " # apply fisher transformation\n", 1418 | " if fperiod:\n", 1419 | " signal = fisher(signal, fperiod, stoch_time)\n", 1420 | " return signal" 1421 | ] 1422 | }, 1423 | { 1424 | "cell_type": "code", 1425 | "execution_count": 36, 1426 | "metadata": {}, 1427 | "outputs": [], 1428 | "source": [ 1429 | "def ad_cg(series, cutoff_period, fperiod=None, stoch_time=None):\n", 1430 | " \"\"\"\n", 1431 | "\n", 1432 | " :param series: (np.array) price\n", 1433 | " :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation\n", 1434 | " :param period: (int) fisher transformation parameter\n", 1435 | " :param stoch_time: (int) fisher transformation parameter\n", 1436 | " :return: (np.array) cg\n", 1437 | " \"\"\"\n", 1438 | " # compute period\n", 1439 | " period, _ = compute_period(series, cutoff_period)\n", 1440 | " length = (period / 2).astype(np.int)\n", 1441 | " # compute cg\n", 1442 | " num = np.zeros_like(series)\n", 1443 | " denom = np.ones_like(series)\n", 1444 | " for i in range(length[1] - 1, series.shape[0]):\n", 1445 | " num[i] = np.sum((np.array(range(length[i])) + 1) * series[i - length[i] + 1:i + 1][::-1])\n", 1446 | " denom[i] = np.sum(series[i - length[i] + 1:i + 1])\n", 1447 | " cg = -num / denom + (1 + length) / 2\n", 1448 | " # apply fisher transformation\n", 1449 | " if fperiod:\n", 1450 | " cg = fisher(cg, fperiod, stoch_time)\n", 1451 | " return cg" 1452 | ] 1453 | }, 1454 | { 1455 | "cell_type": "markdown", 1456 | "metadata": {}, 1457 | "source": [ 1458 | "## VIII. Turning Point Indicator\n", 1459 | "### Auto-correlation Indicator\n", 1460 | "1) **method**\n", 1461 | "\n", 1462 | "* If the correlation between ①[ now - average_len , now ] and ②[ now - average_len - lag , now - lag ] is small, it indicates a turning point\n", 1463 | " \n", 1464 | "2) **parameter range**\n", 1465 | "\n", 1466 | "* hp_period $(0, +\\infty)$\n", 1467 | "\n", 1468 | "* lp_period $(0, +\\infty)$\n", 1469 | "\n", 1470 | "* average_len $(0, +\\infty)$\n", 1471 | "\n", 1472 | "* lag $(0, +\\infty)$\n" 1473 | ] 1474 | }, 1475 | { 1476 | "cell_type": "code", 1477 | "execution_count": 37, 1478 | "metadata": {}, 1479 | "outputs": [], 1480 | "source": [ 1481 | "def corr(series, hp_period, lp_period, average_len, lag):\n", 1482 | " \"\"\"\n", 1483 | " auto-correlation indicator: indicates reversal when it's near -1\n", 1484 | " :param hp_period: highpass period to remove the trend from the original price\n", 1485 | " :param lp_period: lowpass period to smooth the original price\n", 1486 | " :param average_len: # period to compute auto-correlation\n", 1487 | " :param lag: lag of period to compute auto_correlation\n", 1488 | " :return: (np.array) correlation\n", 1489 | " \"\"\"\n", 1490 | " HP = highpass2pole(series, hp_period)\n", 1491 | " filt = supersmoother2pole(HP, lp_period)\n", 1492 | " corr = np.zeros_like(series)\n", 1493 | " for i in range(average_len + lag, series.shape[0] + 1):\n", 1494 | " s1 = filt[i - average_len:i]\n", 1495 | " s2 = filt[i - average_len - lag:i - lag]\n", 1496 | " corr[i - 1] = np.corrcoef(s1, s2)[0, 1]\n", 1497 | " return corr" 1498 | ] 1499 | }, 1500 | { 1501 | "cell_type": "markdown", 1502 | "metadata": {}, 1503 | "source": [ 1504 | "### Auto-correlation Reversal Indicator\n", 1505 | "1) **method**\n", 1506 | "\n", 1507 | "* Based on the correlation indicator, it takes the differece of 2 correlations. If the sum of change amounts to a certain threshold, it would signal a turning point.\n", 1508 | " \n", 1509 | "2) **parameter range**\n", 1510 | "\n", 1511 | "* hp_period $(0, +\\infty)$\n", 1512 | "\n", 1513 | "* lp_period $(0, +\\infty)$\n", 1514 | "\n", 1515 | "* average_len $(0, +\\infty)$\n", 1516 | "\n", 1517 | "* lag $(0, +\\infty)$\n", 1518 | "\n", 1519 | "* thresh $(0, 2*lag)$\n", 1520 | "\n" 1521 | ] 1522 | }, 1523 | { 1524 | "cell_type": "code", 1525 | "execution_count": 38, 1526 | "metadata": {}, 1527 | "outputs": [], 1528 | "source": [ 1529 | "def corr_reversal(series, hp_period, lp_period, average_len, lag, thresh):\n", 1530 | " \"\"\"\n", 1531 | " auto-correlation reversal indicator indicates the reversals of the price\n", 1532 | " :param hp_period: highpass period to remove the trend from the original price\n", 1533 | " :param lp_period: lowpass period to smooth the original price\n", 1534 | " :param average_len: # period to compute auto-correlation\n", 1535 | " :param lag: lag of period to compute auto_correlation\n", 1536 | " :param: thresh: if the num of reversal-indicator delta happens more than thresh times in the lag period,\n", 1537 | " it indicates a overall reversal\n", 1538 | " :return: (np.array) reversal indicator based on the threshold as well as the original sum of delta\n", 1539 | " \"\"\"\n", 1540 | " HP = highpass2pole(series, hp_period)\n", 1541 | " filt = supersmoother2pole(HP, lp_period)\n", 1542 | " corr = np.zeros_like(series)\n", 1543 | " for i in range(average_len + lag, series.shape[0] + 1):\n", 1544 | " s1 = filt[i - average_len:i]\n", 1545 | " s2 = filt[i - average_len - lag:i - lag]\n", 1546 | " corr[i - 1] = np.corrcoef(s1, s2)[0, 1]\n", 1547 | " corr_1 = corr > 0\n", 1548 | " corr_2 = np.roll(corr_1, 1)\n", 1549 | " delta = np.abs(corr_1 - corr_2) / 2\n", 1550 | " delta[0] = 0\n", 1551 | " sumdelta = np.zeros_like(delta)\n", 1552 | " for i in range(lag):\n", 1553 | " sumdelta += np.roll(delta, i)\n", 1554 | " reversal = sumdelta >= thresh\n", 1555 | " return reversal, sumdelta" 1556 | ] 1557 | }, 1558 | { 1559 | "cell_type": "markdown", 1560 | "metadata": {}, 1561 | "source": [ 1562 | "### Concolution Indicator\n", 1563 | "1) **method**\n", 1564 | "\n", 1565 | "* If the convolution of [ now - lookback_period , now ] is larger, it indicates a turning point.\n", 1566 | " \n", 1567 | "2) **parameter range**\n", 1568 | "\n", 1569 | "* hp_period $(0, +\\infty)$\n", 1570 | "\n", 1571 | "* lp_period $(0, +\\infty)$\n", 1572 | "\n", 1573 | "* lookback_period $(0, +\\infty)$\n" 1574 | ] 1575 | }, 1576 | { 1577 | "cell_type": "code", 1578 | "execution_count": 39, 1579 | "metadata": {}, 1580 | "outputs": [], 1581 | "source": [ 1582 | "def convolution(series, hp_period, lp_period, lookback_period):\n", 1583 | " \"\"\"\n", 1584 | " convolution indicator: use convolution within a lookback period to determine whether a turning point has occurred\n", 1585 | " :param series: (np.array) price\n", 1586 | " :param hp_period: highpass period to remove the trend from the original price\n", 1587 | " :param lp_period: lowpass period to smooth the original price\n", 1588 | " :param lookback_period: lookback period to compute convolution\n", 1589 | " :return: (np.array) normalized convolution [0,1] and original convolution [-1,1]\n", 1590 | " \"\"\"\n", 1591 | " HP = highpass2pole(series, hp_period)\n", 1592 | " filt = supersmoother2pole(HP, lp_period)\n", 1593 | "\n", 1594 | " corr = np.zeros_like(series)\n", 1595 | " for i in range(lookback_period, series.shape[0] + 1):\n", 1596 | " lookback_ = filt[i - lookback_period:i]\n", 1597 | " corr[i - 1] = np.corrcoef(lookback_, np.flip(lookback_))[0, 1]\n", 1598 | " conv = (1 + (np.exp(3 * corr) - 1) / (np.exp(3 * corr) + 1)) / 2\n", 1599 | "\n", 1600 | " return conv, corr" 1601 | ] 1602 | }, 1603 | { 1604 | "cell_type": "markdown", 1605 | "metadata": {}, 1606 | "source": [ 1607 | "## IX. Lead Indicator\n", 1608 | "1) **method**\n", 1609 | "\n", 1610 | "* This indicator generates \"lead\" in the indicators to somehow achieve the effect similiar to prediction.\n", 1611 | " \n", 1612 | "2) **parameter range**\n", 1613 | "\n", 1614 | "* alpha1 $(0, 1)$\n", 1615 | "\n", 1616 | "* alpha2 $(\\text{alpha1}, 1)$" 1617 | ] 1618 | }, 1619 | { 1620 | "cell_type": "code", 1621 | "execution_count": 40, 1622 | "metadata": {}, 1623 | "outputs": [], 1624 | "source": [ 1625 | "def lead(series, alpha1, alpha2):\n", 1626 | " \"\"\"\n", 1627 | " lead indicator\n", 1628 | " :param series: (np.array) price\n", 1629 | " :param alpha1: (float) alpha to generate lead\n", 1630 | " :param alpha2: (float) alpha to smooth while offsetting some lead\n", 1631 | " :return: (np.array) netlead\n", 1632 | " \"\"\"\n", 1633 | " assert alpha1 < alpha2\n", 1634 | " lead = np.zeros_like(series)\n", 1635 | " netlead = np.zeros_like(series)\n", 1636 | " for i in range(1, series.shape[0]):\n", 1637 | " lead[i] = 2 * series[i] + (alpha1 - 2) * series[i - 1] \\\n", 1638 | " + (1 - alpha1) * lead[i - 1]\n", 1639 | " netlead[i] = alpha2 * lead[i] + (1 - alpha2) * netlead[i - 1]\n", 1640 | " return netlead" 1641 | ] 1642 | } 1643 | ], 1644 | "metadata": { 1645 | "kernelspec": { 1646 | "display_name": "Python 3", 1647 | "language": "python", 1648 | "name": "python3" 1649 | }, 1650 | "language_info": { 1651 | "codemirror_mode": { 1652 | "name": "ipython", 1653 | "version": 3 1654 | }, 1655 | "file_extension": ".py", 1656 | "mimetype": "text/x-python", 1657 | "name": "python", 1658 | "nbconvert_exporter": "python", 1659 | "pygments_lexer": "ipython3", 1660 | "version": "3.7.4" 1661 | } 1662 | }, 1663 | "nbformat": 4, 1664 | "nbformat_minor": 2 1665 | } 1666 | -------------------------------------------------------------------------------- /indicator_lib.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Spyder Editor 4 | 5 | This is a temporary script file. 6 | """ 7 | 8 | import numpy as np 9 | import pandas as pd 10 | import matplotlib.pyplot as plt 11 | import talib as ta 12 | from sklearn.linear_model import LinearRegression 13 | 14 | 15 | # ============================================================================= 16 | # Heiken Ashi Candles 17 | # ============================================================================= 18 | def heikenashi(o, h, l, c): 19 | HAc = (o + h + l + c) / 4 20 | HAo, HAh, HAl = HAc.copy(), HAc.copy(), HAc.copy() 21 | 22 | for i in range(1, o.shape[0]): 23 | HAo[i] = (HAo[i - 1] + HAc[i - 1]) / 2 24 | HAh[i] = np.array((h[i], HAo[i], HAc[i])).max() 25 | HAl[i] = np.array((l[i], HAo[i], HAc[i])).min() 26 | 27 | return HAo, HAh, HAl, HAc 28 | 29 | 30 | # ============================================================================= 31 | # compute period for adaptive methods 32 | # ============================================================================= 33 | def hilbert(series): 34 | """ 35 | Hilbert transformation 36 | :param series: (np.array) price 37 | :return: (np.array) InPhase and Quadrature term 38 | """ 39 | Q = 0.0962 * series + 0.5796 * np.roll(series, 2) \ 40 | - 0.5796 * np.roll(series, 4) - 0.0962 * np.roll(series, 6) 41 | Q[0:6] = 0 42 | I = np.roll(series, 3) 43 | I[0:3] = 0 44 | return Q, I 45 | 46 | 47 | def compute_period(series, cutoff): 48 | smooth = sma4(series) 49 | cycle = highpass2pole(smooth, cutoff) 50 | for i in range(2, 7): 51 | cycle[i] = (series[i] - 2 * series[i - 1] + series[i - 2]) / 4 52 | delta_phase = np.zeros_like(series) 53 | inst_period = np.zeros_like(series) 54 | period = np.zeros_like(series) 55 | Q, I = hilbert(cycle) 56 | for i in range(6, series.shape[0]): 57 | Q[i] *= 0.5 + 0.08 * inst_period[i - 1] 58 | if Q[i] != 0 and Q[i - 1] != 0: 59 | delta_phase[i] = (I[i] / Q[i] - I[i - 1] / Q[i - 1]) / (1 + I[i] * I[i - 1] / (Q[i] * Q[i - 1])) 60 | delta_phase[i] = max(0.1, min(1.1, delta_phase[i])) 61 | median_delta = np.median(delta_phase[i - 4:i + 1]) 62 | if median_delta == 0: 63 | DC = 15 64 | else: 65 | DC = 6.29318 / median_delta + 0.5 66 | inst_period[i] = 0.33 * DC + 0.67 * inst_period[i - 1] 67 | period[i] = 0.15 * inst_period[i] + 0.85 * period[i - 1] 68 | return period, cycle 69 | 70 | 71 | # ============================================================================= 72 | # filters 73 | # ============================================================================= 74 | def sma4(series): 75 | """ 76 | 4 term simple moving average 77 | :param series: (np.array) price 78 | :return: (np.array) smoothed price 79 | """ 80 | newseries = (series + 2 * np.roll(series, 1) + 2 * np.roll(series, 2) 81 | + np.roll(series, 3)) / 6 82 | newseries[:3] = series[:3] 83 | return newseries 84 | 85 | 86 | def ema(series, cutoff): 87 | """ 88 | exponential moving average 89 | alpha = 1/(lag+1) 90 | :param series: (np.array) price 91 | :param cutoff: (float) cutoff period of the filter 92 | :return: (np.array) filtered price 93 | """ 94 | K = 1 95 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 96 | for i in range(1, series.shape[0]): 97 | series[i] = alpha * series[i] \ 98 | + (1 - alpha) * series[i - 1] 99 | return series 100 | 101 | 102 | def regularized_ema(series, cutoff): 103 | """ 104 | add an additional penalty term to enhance filter effect while introducing no more lag 105 | :param series: (np.array) price 106 | :param cutoff: (float) cutoff period of the filter 107 | :return: (np.array) filtered price 108 | """ 109 | K = 1 110 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 111 | l = np.exp(0.16 / alpha) 112 | newseries = np.copy(series) 113 | for i in range(2, series.shape[0]): 114 | newseries[i] = alpha / (1 + l) * series[i] \ 115 | + (1 - alpha - 2 * l) / (1 + l) * newseries[i - 1] - l / (l + 1) * newseries[i - 2] 116 | return newseries 117 | 118 | 119 | def lowpass2pole(series, cutoff): 120 | """ 121 | 2 pole low-pass filter 122 | :param series: (np.array) price 123 | :param cutoff: (float) cutoff period of the filter 124 | :return: (np.array) filtered price 125 | """ 126 | K = 1.414 127 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 128 | for i in range(2, series.shape[0]): 129 | series[i] = alpha ** 2 * series[i] \ 130 | + 2 * (1 - alpha) * series[i - 1] + (1 - alpha) ** 2 * series[i - 2] 131 | return series 132 | 133 | 134 | def decycler(series, cutoff): 135 | """ 136 | subtract high frequency component from the original series to decycle 137 | :param series: (np.array) price 138 | :param cutoff: (float) cutoff period of the filter 139 | :return: (np.array) decycled series 140 | """ 141 | K = 1 142 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 143 | newseries = np.copy(series) 144 | for i in range(1, series.shape[0]): 145 | newseries[i] = alpha / 2 * (series[i] + series[i - 1]) \ 146 | + (1 - alpha) * newseries[i - 1] 147 | return newseries 148 | 149 | 150 | def highpass(series, cutoff): 151 | """ 152 | (1 pole) high-pass filter 153 | :param series: (np.array) price 154 | :param cutoff: (float) cutoff period of the filter 155 | :return: (np.array) filtered price 156 | """ 157 | K = 1 158 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 159 | newseries = np.copy(series) 160 | for i in range(1, series.shape[0]): 161 | newseries[i] = (1 - alpha / 2) * series[i] - (1 - alpha / 2) * series[i - 1] \ 162 | + (1 - alpha) * newseries[i - 1] 163 | return newseries 164 | 165 | 166 | def highpass2pole(series, cutoff): 167 | """ 168 | 2 pole high-pass filter 169 | :param series: (np.array) price 170 | :param cutoff: (float) cutoff period of the filter 171 | :return: (np.array) filtered price 172 | """ 173 | K = 0.707 174 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 175 | newseries = np.copy(series) 176 | for i in range(2, series.shape[0]): 177 | newseries[i] = (1 - alpha / 2) ** 2 * series[i] \ 178 | - 2 * (1 - alpha / 2) ** 2 * series[i - 1] \ 179 | + (1 - alpha / 2) ** 2 * series[i - 2] \ 180 | + 2 * (1 - alpha) * newseries[i - 1] - (1 - alpha) ** 2 * newseries[i - 2] 181 | return newseries 182 | 183 | 184 | def ad_highpass2pole(series, lag): 185 | """ 186 | 2 pole adaptive high-pass filter (variable cutoff period) 187 | :param series: (np.array) price 188 | :param lag: (np.array) lag of the filter 189 | :return: (np.array) filtered price 190 | """ 191 | alpha = 1 / (1 + lag) 192 | newseries = np.copy(series) 193 | for i in range(2, series.shape[0]): 194 | newseries[i] = (1 - alpha[i] / 2) ** 2 * series[i] \ 195 | - 2 * (1 - alpha[i] / 2) ** 2 * series[i - 1] \ 196 | + (1 - alpha[i] / 2) ** 2 * series[i - 2] \ 197 | + 2 * (1 - alpha[i]) * newseries[i - 1] - (1 - alpha[i]) ** 2 * newseries[i - 2] 198 | return newseries 199 | 200 | 201 | def supersmoother2pole(series, cutoff): 202 | """ 203 | simplified 2 pole butterworth smoother 204 | :param series: (np.array) price 205 | :param cutoff: (float) cutoff period of the filter 206 | :return: (np.array) smoothed price 207 | """ 208 | a = np.exp(-1.414 * np.pi / cutoff) 209 | b = 2 * a * np.cos(1.414 * np.pi / cutoff) 210 | newseries = np.copy(series) 211 | for i in range(2, series.shape[0]): 212 | newseries[i] = (1 + a ** 2 - b) / 2 * (series[i] + series[i - 1]) \ 213 | + b * newseries[i - 1] - a ** 2 * newseries[i - 2] 214 | return newseries 215 | 216 | 217 | def supersmoother3pole(series, cutoff): 218 | """ 219 | simplified 3 pole butterworth smoother 220 | :param series: (np.array) price 221 | :param cutoff: (float) cutoff period of the filter 222 | :return: (np.array) smoothed price 223 | """ 224 | a = np.exp(-np.pi / cutoff) 225 | b = 2 * a * np.cos(1.738 * np.pi / cutoff) 226 | c = a ** 2 227 | newseries = np.copy(series) 228 | for i in range(3, series.shape[0]): 229 | newseries[i] = (1 - c ** 2 - b + b * c) * series[i] \ 230 | + (b + c) * newseries[i - 1] + (-c - b * c) * newseries[i - 2] + (c ** 2) * newseries[i - 3] 231 | return newseries 232 | 233 | 234 | def allpass(series, alpha): 235 | """ 236 | allpass filter (used in laguerre filter) 237 | :param series: (np.array) price 238 | :param alpha: (float) damping factor 239 | :return: (np.array) filtered price 240 | """ 241 | newseries = np.copy(series) 242 | for i in range(1, series.shape[0]): 243 | newseries[i] = -alpha * series[i] + series[i - 1] \ 244 | + alpha * newseries[i - 1] 245 | return newseries 246 | 247 | 248 | def laguerre(series, gamma): 249 | """ 250 | Laguerre filter 251 | :param series: (np.array) price 252 | :param gamma: (float) damping factor 253 | :return: (np.array) filtered price 254 | """ 255 | l0 = ema(series, gamma) 256 | l1 = allpass(l0, gamma) 257 | l2 = allpass(l1, gamma) 258 | l3 = allpass(l2, gamma) 259 | return l0, l1, l2, l3 260 | 261 | 262 | def roofing(series, cutoff_hp, cutoff_lp): 263 | hp = highpass2pole(series, cutoff_hp) 264 | newseries = supersmoother3pole(hp, cutoff_lp) 265 | return newseries 266 | 267 | 268 | # ============================================================================= 269 | # indicators transformer 270 | # ============================================================================= 271 | def stoch(series, period): 272 | """ 273 | stochaticize the indicator 274 | :param series: (np.array) indicator or price 275 | :param period: (int) window length 276 | :return: (np.array) series of value within [0,1] 277 | """ 278 | df = pd.Series(series) 279 | df_max = df.rolling(period, min_periods=1).max() 280 | df_min = df.rolling(period, min_periods=1).min() 281 | series = (df - df_min) / (df_max - df_min) 282 | return series.to_numpy() 283 | 284 | 285 | def fisher(series, period, stoch_time): 286 | """ 287 | fisher transformer 288 | :param series: (np.array) indicator or price 289 | :param period: (int) window length 290 | :param stoch_time: (int) number of time applying stochastic transformation 291 | :return: (np.array) normalized series satisfying statistic inference of normally distributed data 292 | """ 293 | # stochaticize 294 | for i in range(stoch_time): 295 | series = stoch(series, period) 296 | # transform data to [-0.9999,0.9999] 297 | series = 2 * (series - 0.5) 298 | for i in range(series.shape[0]): 299 | series[i] = max(-0.9999, min(0.9999, series[i])) 300 | # apply fisher transformation 301 | series = np.log((1 + series) / (1 - series)) / 2 302 | return series 303 | 304 | 305 | def inverse_fisher(series, amplifying_factor): 306 | """ 307 | inverse fisher transformer: serve as a soft limiter 308 | :param series: (np.array) indicator or price 309 | :param amplifying_factor: (double>1) if the indicator is already between 1 and -1 310 | we can amplifying it before inverse transformation to get the best of the soft limiter 311 | """ 312 | return (np.exp(amplifying_factor*series)-1)/(np.exp(amplifying_factor*series)+1) 313 | 314 | 315 | def cube(series): 316 | """ 317 | cube transformation: for compressing the squiggles near 0 318 | :param series: (np.array) indicator or price 319 | """ 320 | return series**3 321 | 322 | 323 | # ============================================================================= 324 | # signal generator 325 | # ============================================================================= 326 | def lag_signal(indicator, lag): 327 | # generate lag 328 | lag1 = np.roll(indicator, lag) 329 | lag1[0] = indicator[0] 330 | # generate signal 331 | signal = (indicator > lag1) * 1 - (indicator < lag1) * 1 332 | return signal 333 | 334 | 335 | def fix_channel_break(indicator, up=2, mid=0, dn=-2): 336 | """ 337 | fix channel break signal generator 338 | :param indicator: (np.array) indicator series 339 | :param up: (float) buy when cross over 340 | :param mid: (float) close out current position 341 | :param dn: (float) sell short when cross under 342 | :return: signal 343 | """ 344 | signal = np.zeros_like(indicator) 345 | for i in range(indicator.shape[0]): 346 | if indicator[i] > up or (signal[i - 1] > 0 and indicator[i] > mid): 347 | signal[i] = 1 348 | if indicator[i] < dn or (signal[i - 1] < 0 and indicator[i] < mid): 349 | signal[i] = -1 350 | return signal 351 | 352 | 353 | def lead(series, alpha1, alpha2): 354 | """ 355 | lead indicator 356 | :param series: (np.array) price 357 | :param alpha1: (float) alpha to generate lead 358 | :param alpha2: (float) alpha to smooth while offsetting some lead 359 | :return: (np.array) netlead 360 | """ 361 | assert alpha1 < alpha2 362 | lead = np.zeros_like(series) 363 | netlead = np.zeros_like(series) 364 | for i in range(1, series.shape[0]): 365 | lead[i] = 2 * series[i] + (alpha1 - 2) * series[i - 1] \ 366 | + (1 - alpha1) * lead[i - 1] 367 | netlead[i] = alpha2 * lead[i] + (1 - alpha2) * netlead[i - 1] 368 | return netlead 369 | 370 | 371 | # ============================================================================= 372 | # combined signal 373 | # ============================================================================= 374 | def itrend_bandpass(series, cutoff, cycle, bandwidth): 375 | i = i_trend(series, cutoff)[0] 376 | b = bandpass(series, cycle, bandwidth)[0] 377 | buy = i & b 378 | sell = i | b 379 | return (buy+sell)/2, 380 | 381 | def adm_bandpass(series, cutoff_period, cutoff_signal, cycle, bandwidth): 382 | return ad_momentum(series, cutoff_period, cutoff_signal)[0] & bandpass(series, cycle, bandwidth)[0], 383 | 384 | def roof_rsi(series, cutoff_hp, cutoff_lp, length): 385 | roof = roofing(series, cutoff_hp, cutoff_lp) 386 | return rsi(roof, length)[0], 387 | # ============================================================================= 388 | # indicators - trend 389 | # ============================================================================= 390 | def i_trend(series, cutoff): 391 | """ 392 | instantaneous trendline 393 | :param series: (np.array) price 394 | :param cutoff: (float) cutoff period of the hp 395 | :return: (np.array) signal, trend and its trigger 396 | """ 397 | # compute inst trend 398 | K = 0.707 399 | alpha = 1 + (np.sin(2 * np.pi * K / cutoff) - 1) / np.cos(2 * np.pi * K / cutoff) 400 | it = np.copy(series) 401 | for i in range(2, 7): 402 | it[i] = (series[i] + 2 * series[i - 1] + series[i - 2]) / 4 403 | for i in range(7, series.shape[0]): 404 | it[i] = (alpha - alpha ** 2 / 4) * series[i] \ 405 | + alpha ** 2 / 2 * series[i - 1] \ 406 | - (alpha - alpha ** 2 * 3 / 4) * series[i - 2] \ 407 | + 2 * (1 - alpha) * it[i - 1] - (1 - alpha) ** 2 * it[i - 2] 408 | 409 | # compute lead 2 trigger & signal 410 | lag2 = np.roll(it, 20) 411 | lag2[:20] = it[:20] 412 | trigger = 2 * it - lag2 413 | signal = (trigger > it) * 1 - (trigger < it) * 1 414 | return signal, it, trigger 415 | 416 | 417 | def ad_momentum(series, cutoff_period, cutoff_signal): 418 | """ 419 | smoothed adaptive momentum indicator 420 | compare the price in the current cycle with that in the previous cycle (same phase) 421 | to indicate an uptrend or downtrend 422 | :param series: (np.array) price 423 | :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation 424 | :param cutoff_signal: (float) the cutoff period used to smooth the signal 425 | :return: (np.array) signal & momentum 426 | """ 427 | period, _ = compute_period(series, cutoff_period) 428 | period = period.astype(np.int) 429 | momen = np.zeros_like(series) 430 | for i in range(series.shape[0]): 431 | if (i - period[i]) >= 0: 432 | momen[i] = series[i] - series[i - period[i]] 433 | momen = supersmoother3pole(momen, cutoff_signal) 434 | signal = (momen > 0) * 1 - (momen < 0) * 1 435 | return signal, momen 436 | 437 | 438 | # ============================================================================= 439 | # indicators - oscillator 440 | # ============================================================================= 441 | def decycler_oscillator(series, cutoff1, times): 442 | """ 443 | decycler oscillator 444 | take the difference of 2 decyclers with different cutoff 445 | :param series: (np.array) price 446 | :param cutoff1: (float) the smaller cutoff period 447 | :param times: (float, >1) larger cutoff / smaller cutoff 448 | :return: (np.array) signal & indicator 449 | """ 450 | cutoff2 = cutoff1 * times 451 | hp1 = highpass(series, cutoff1) 452 | hp2 = highpass(series, cutoff2) 453 | delta_hp = hp2 - hp1 454 | # >0: uptrend, <0: downtrend 455 | signal = (delta_hp > 0) * 1 - (delta_hp < 0) * 1 456 | return signal, delta_hp 457 | 458 | 459 | def bandpass(series, cycle, bandwidth): 460 | """ 461 | bandpass filter 462 | :param series: (np.array) price 463 | :param cycle: (float) cycle period 464 | :param bandwidth: (float, >0 <2) length between left and right cutoffs / cycle period 465 | :return: (np.array) signal & indicator 466 | """ 467 | # pass a HP to avoid spectral dilation of BP 468 | hp = highpass(series, 4 * cycle / bandwidth) 469 | # bandpass filter 470 | lmd = np.cos(2 * np.pi / cycle) 471 | gamma = np.cos(2 * np.pi * bandwidth / cycle) 472 | sigma = 1 / gamma - np.sqrt(1 / gamma ** 2 - 1) 473 | bp = np.copy(hp) 474 | for i in range(2, series.shape[0]): 475 | bp[i] = (1 - sigma) / 2 * hp[i] - (1 - sigma) / 2 * hp[i - 2] \ 476 | + lmd * (1 + sigma) * bp[i - 1] - sigma * bp[i - 2] 477 | # fast attack-slow decay AGC 478 | K = 0.991 479 | peak = np.copy(bp) 480 | for i in range(series.shape[0]): 481 | if i > 0: 482 | peak[i] = peak[i - 1] * K 483 | if abs(bp[i]) > peak[i]: 484 | peak[i] = abs(bp[i]) 485 | bp_normalized = bp / peak 486 | # trigger(lead) & signal 487 | trigger = highpass(bp_normalized, cycle / bandwidth / 1.5) 488 | signal = (bp_normalized < trigger) * 1 - (trigger < bp_normalized) * 1 489 | return signal, bp, bp_normalized, trigger 490 | 491 | 492 | def cci(series, cutoff1, cutoff2, fperiod=None, stoch_time=None): 493 | """ 494 | CCI - cyber cycle index 495 | delay is less than half a cycle: buy when signal cross under lag1, sell when signal cross over lag1 496 | need a 'stop loss' strategy, close out when profit<0 and bars since entry > 8(period) 497 | :param series: (np.array) price 498 | :param cutoff1: (float) cutoff period for hp 499 | :param cutoff2: (float) cutoff period for ema 500 | :param period, stoch_time: (tuple: ((int)period, (int)stoch_time)) fisher transformation parameters 501 | if = empty tuple, no fisher transformation 502 | :return: (np.array) trading signal & cycle 503 | """ 504 | # compute the cycle 505 | smooth = sma4(series) 506 | cycle = highpass2pole(smooth, cutoff1) 507 | for t in range(2, 7): 508 | cycle[t] = (series[t] - 2 * series[t - 1] + series[t - 2]) / 4 509 | signal = ema(cycle, cutoff2) 510 | # apply fisher transformation 511 | if fperiod != None: 512 | signal = fisher(signal, fperiod, stoch_time) 513 | return lag_signal(-signal, 1), signal 514 | 515 | 516 | def ad_cci(series, cutoff_period, cutoff_signal, fperiod=None, stoch_time=None): 517 | """ 518 | adaptive cyber cycle 519 | :param series: (np.array) price 520 | :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation 521 | :param cutoff_signal: (float) the cutoff period used to smooth the signal 522 | :param period: (int) fisher transformation parameter 523 | :param stoch_time: (int) fisher transformation parameter 524 | :return: (np.array) trading signal & cycle 525 | """ 526 | # compute period 527 | period, _ = compute_period(series, cutoff_period) 528 | # compute the cycle 529 | smooth = sma4(series) 530 | cycle = ad_highpass2pole(smooth, period) 531 | for t in range(2, 7): 532 | cycle[t] = (series[t] - 2 * series[t - 1] + series[t - 2]) / 4 533 | signal = ema(cycle, cutoff_signal) 534 | # apply fisher transformation 535 | if fperiod: 536 | signal = fisher(signal, fperiod, stoch_time) 537 | return lag_signal(-signal, 1), signal 538 | 539 | 540 | def cg(series, length, fperiod=None, stoch_time=None): 541 | """ 542 | CG - center of gravity 543 | view the price as weight to compute the center of gravity of the filter 544 | :param series: (np.array) price 545 | :param length: (int) length of the filter 546 | :param period: (int) fisher transformation parameter 547 | :param stoch_time: (int) fisher transformation parameter 548 | :return: (np.array) trading signal & cg 549 | """ 550 | # compute cg 551 | num = np.zeros_like(series) 552 | denom = np.ones_like(series) 553 | for i in range(length - 1, series.shape[0]): 554 | num[i] = np.sum((np.array(range(length)) + 1) * series[i - length + 1:i + 1][::-1]) 555 | denom[i] = np.sum(series[i - length + 1:i + 1]) 556 | cg = -num / denom + (1 + length) / 2 557 | # apply fisher transformation 558 | if fperiod: 559 | cg = fisher(cg, fperiod, stoch_time) 560 | return lag_signal(cg, 1), cg 561 | 562 | 563 | def ad_cg(series, cutoff_period, fperiod=None, stoch_time=None): 564 | """ 565 | 566 | :param series: (np.array) price 567 | :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation 568 | :param period: (int) fisher transformation parameter 569 | :param stoch_time: (int) fisher transformation parameter 570 | :return: (np.array) trading signal & cg 571 | """ 572 | # compute period 573 | period, _ = compute_period(series, cutoff_period) 574 | length = (period / 2).astype(np.int) 575 | # compute cg 576 | num = np.zeros_like(series) 577 | denom = np.ones_like(series) 578 | for i in range(length[1] - 1, series.shape[0]): 579 | num[i] = np.sum((np.array(range(length[i])) + 1) * series[i - length[i] + 1:i + 1][::-1]) 580 | denom[i] = np.sum(series[i - length[i] + 1:i + 1]) 581 | cg = -num / denom + (1 + length) / 2 582 | # apply fisher transformation 583 | if fperiod: 584 | cg = fisher(cg, fperiod, stoch_time) 585 | return lag_signal(cg, 1), cg 586 | 587 | 588 | def rvi(o, h, l, c, length, fperiod=None, stoch_time=None): 589 | """ 590 | RVI - relative vigor index 591 | :param o: (np.array) open 592 | :param h: (np.array) high 593 | :param l: (np.array) low 594 | :param c: (np.array) close 595 | :param length: length to sum the num & denom 596 | :param period: (int) fisher transformation parameter 597 | :param stoch_time: (int) fisher transformation parameter 598 | :return: (np.array) signal & rvi 599 | """ 600 | co = c - o 601 | hl = h - l 602 | num = sma4(co) 603 | denom = sma4(hl) 604 | rvi = np.zeros_like(o) 605 | for i in range(2 + length, o.shape[0]): 606 | rvi[i] = np.sum(num[i - length + 1:i + 1]) / np.sum(denom[i - length + 1:i + 1]) 607 | # apply fisher transformation 608 | if fperiod: 609 | rvi = fisher(rvi, fperiod, stoch_time) 610 | return lag_signal(rvi, 1), rvi 611 | 612 | 613 | def rsi(series, length, fperiod=None, stoch_time=None): 614 | """ 615 | Relative Strength Index 616 | :param series: (np.array) price 617 | :param length: length to sum the num & denom 618 | :param period: (int) fisher transformation parameter 619 | :param stoch_time: (int) fisher transformation parameter 620 | :return: (np.array) signal & rsi 621 | """ 622 | rsi = ta.RSI(series, length) 623 | # apply fisher transformation 624 | if fperiod: 625 | rsi = fisher(rsi, fperiod, stoch_time) 626 | return lag_signal(rsi, 1), rsi 627 | 628 | 629 | def laguerre_rsi(series, gamma, up=2, mid=0, dn=-2, fperiod=None, stoch_time=None): 630 | """ 631 | Laguerre RSI 632 | :param series: (np.array) price 633 | :param gamma: (float) damping factor 634 | :param period: (int) fisher transformation parameter 635 | :param stoch_time: (int) fisher transformation parameter 636 | :param up: (float) fixed channel parameter 637 | :param mid: (float) fixed channel parameter 638 | :param dn: (float) fixed channel parameter 639 | :return: (np.array) signal & rsi 640 | """ 641 | l0, l1, l2, l3 = laguerre(series, gamma) 642 | rsi = np.zeros_like(series) 643 | for i in range(series.shape[0]): 644 | cu = 0 645 | cd = 0 646 | if l1[i] > l0[i]: 647 | cu += l1[i] - l0[i] 648 | else: 649 | cd -= l1[i] - l0[i] 650 | if l2[i] > l1[i]: 651 | cu += l2[i] - l1[i] 652 | else: 653 | cd -= l2[i] - l1[i] 654 | if l3[i] > l2[i]: 655 | cu += l3[i] - l2[i] 656 | else: 657 | cd -= l3[i] - l2[i] 658 | rsi[i] = cu / (cu + cd) 659 | # apply fisher transformation 660 | if fperiod: 661 | rsi = fisher(rsi, fperiod, stoch_time) 662 | signal = fix_channel_break(rsi, up, mid, dn) 663 | return signal, rsi 664 | 665 | 666 | def sinewave(series, cutoff_period, lead=0.25 * np.pi): 667 | """ 668 | sinewave indicator 669 | :param series: (np.array) price 670 | :param cutoff_period: (float) the cutoff period used to compute the period using Hilbert Transformation 671 | :param lead: (float) lead angle, in radians 672 | :return: (np.array) signal, sinewave and leadsine 673 | """ 674 | # compute period 675 | period, cycle = compute_period(series, cutoff_period) 676 | dcperiod = period.astype(np.int) 677 | # compute dominant cycle phase 678 | real = np.zeros_like(series) 679 | imag = np.zeros_like(series) 680 | dcphase = np.zeros_like(series) 681 | for i in range(series.shape[0]): 682 | for j in range(dcperiod[i]): 683 | real[i] += np.sin(2 * np.pi * j / dcperiod[i]) * cycle[i] 684 | imag[i] += np.cos(2 * np.pi * j / dcperiod[i]) * cycle[i] 685 | if abs(imag[i] > 0.001): 686 | dcphase[i] = np.arctan(real[i] / imag[i]) 687 | else: 688 | dcphase[i] = 0.5 * np.pi * np.sign(real[i]) 689 | dcphase[i] += 0.5 * np.pi 690 | if imag[i] < 0: 691 | dcphase[i] += np.pi 692 | if dcphase[i] > 1.75 * np.pi: 693 | dcphase[i] -= 2 * np.pi 694 | # compute sinewave 695 | sinewave = np.sin(dcphase) 696 | leadsine = np.sin(dcphase + lead) 697 | signal = (leadsine > sinewave) * 1 - (leadsine < sinewave) * 1 698 | return signal, sinewave, leadsine 699 | 700 | 701 | def better_sinewave(series, hp_period, lp_period, upper_bound, lower_bound): 702 | """ 703 | the even better sinewave indicator: profits better when the market is in a trend mode 704 | :param hp_period: highpass period to remove the trend from the original price 705 | :param lp_period: lowpass period to smooth the original price 706 | :param upper_bound: long when the wave is above the upper_bound 707 | :param lower_bound: short when the wave is below the lower_bound 708 | """ 709 | HP = highpass(series, hp_period) 710 | filt = supersmoother2pole(HP, lp_period) 711 | wave = (filt + np.roll(filt,1) + np.roll(filt,2))/3 712 | wave[0] = filt[0] 713 | wave[1] = (filt[0]+filt[1])/2 714 | pwr = (filt**2 + np.roll(filt,1)**2 + np.roll(filt,2)**2)/3 715 | pwr[0] = filt[0]**2 716 | pwr[1] = (filt[0]**2+filt[1]**2)/2 717 | wave = wave/np.sqrt(pwr) 718 | wave[np.isnan(wave)] = 0 719 | signal = (wave>upper_bound)*1 - (wave 0: 751 | peak[i] = peak[i - 1] * K 752 | if abs(sqsum[i]) > peak[i]: 753 | peak[i] = abs(sqsum[i]) 754 | sqsum = sqsum / peak 755 | sqsum[np.isnan(sqsum)] = 0 756 | 757 | num += (sqsum>0.5)*sqsum*lag 758 | denom += (sqsum>0.5)*sqsum 759 | 760 | dc = num/denom 761 | dc[np.isnan(dc)] = 0 762 | return lag_signal(dc, 1), dc 763 | 764 | 765 | def dft(series, hp_period, lp_period, dft_period): 766 | """ 767 | discrete Fourier Transformation 768 | :param hp_period: highpass period to remove the trend from the original price 769 | :param lp_period: lowpass period to smooth the original price 770 | :param dft_period: period to compute Fourier transformation 771 | """ 772 | HP = highpass2pole(series, hp_period) 773 | filt = supersmoother2pole(HP, lp_period) 774 | 775 | cos_part = np.zeros_like(series) 776 | sin_part = np.zeros_like(series) 777 | for i in range(dft_period): 778 | cos_part += np.roll(filt,i)*np.cos(2*np.pi*i/dft_period)/dft_period 779 | sin_part += np.roll(filt,i)*np.sin(2*np.pi*i/dft_period)/dft_period 780 | pwr = cos_part**2+sin_part**2 781 | 782 | K = 0.991 783 | peak = np.copy(pwr) 784 | for i in range(series.shape[0]): 785 | if i > 0: 786 | peak[i] = peak[i - 1] * K 787 | if abs(pwr[i]) > peak[i]: 788 | peak[i] = abs(pwr[i]) 789 | pwr = pwr / peak 790 | pwr[np.isnan(pwr)] = 0 791 | return pwr, 792 | 793 | 794 | def dft_cg(series, hp_period, lp_period, max_period, min_period): 795 | """ 796 | center of gravity indicator based on discrete Fourier tansformation: indicates dominant cycle 797 | :param hp_period: highpass period to remove the trend from the original price 798 | :param lp_period: lowpass period to smooth the original price 799 | :param max_period: max period to compute Fourier transformation 800 | :param min_period: min period to compute Fourier transformation 801 | """ 802 | num = np.zeros_like(series) 803 | denom = np.zeros_like(series) 804 | for period in range(min_period, max_period): 805 | pwr = dft(series, hp_period, lp_period, period)[0] 806 | num += (pwr>0.5)*pwr*period 807 | denom += (pwr>0.5)*pwr 808 | dc = num/denom 809 | dc[np.isnan(dc)] = 0 810 | return lag_signal(dc, 1), dc 811 | 812 | 813 | def comb(series, hp_period, lp_period, max_period, min_period, bandwidth): 814 | """ 815 | comb filter spectral estimate: compute the dominant cycle 816 | :param hp_period: highpass period to remove the trend from the original price 817 | :param lp_period: lowpass period to smooth the original price 818 | :param max_period: max period to compute bandpass 819 | :param min_period: min period to compute bandpass 820 | :param bandwidth: bandwidth for bandpass filter 821 | """ 822 | num = np.zeros_like(series) 823 | denom = np.zeros_like(series) 824 | HP = highpass2pole(series, hp_period) 825 | filt = supersmoother2pole(HP, lp_period) 826 | for period in range(min_period, max_period): 827 | 828 | _, bp, _, _ = bandpass(filt, period, bandwidth) 829 | pwr = np.zeros_like(bp) 830 | for i in range(period): 831 | pwr+=np.roll(bp,i)**2/period**2 832 | 833 | K = 0.991 834 | peak = np.copy(pwr) 835 | for i in range(pwr.shape[0]): 836 | if i > 0: 837 | peak[i] = peak[i - 1] * K 838 | if abs(pwr[i]) > peak[i]: 839 | peak[i] = abs(pwr[i]) 840 | pwr = pwr / peak 841 | pwr[np.isnan(pwr)] = 0 842 | 843 | num += (pwr>0.5)*pwr*period 844 | denom += (pwr>0.5)*pwr 845 | 846 | dc = num/denom 847 | dc[np.isnan(dc)] = 0 848 | return lag_signal(dc, 1), dc 849 | 850 | 851 | def hilbert_indicator(series, hp_period, lp_period, smooth_period): 852 | """ 853 | hilbert transformation indicator: the real line moves as the original price, while the imaginary line as predictor 854 | :param series: (np.array) price 855 | :param hp_period: highpass period to remove the trend from the original price 856 | :param lp_period: lowpass period to smooth the original price 857 | :param smooth_period: lowpass period to smooth the imaginary line 858 | """ 859 | HP = highpass2pole(series, hp_period) 860 | filt = supersmoother2pole(HP, lp_period) 861 | 862 | K = 0.991 863 | peak = np.copy(filt) 864 | for i in range(series.shape[0]): 865 | if i > 0: 866 | peak[i] = peak[i - 1] * K 867 | if abs(filt[i]) > peak[i]: 868 | peak[i] = abs(filt[i]) 869 | real = filt / peak 870 | real[np.isnan(real)] = 0 871 | 872 | quad = real - np.roll(real,1) 873 | quad[0] = 0 874 | K = 0.991 875 | peak = np.copy(quad) 876 | for i in range(series.shape[0]): 877 | if i > 0: 878 | peak[i] = peak[i - 1] * K 879 | if abs(quad[i]) > peak[i]: 880 | peak[i] = abs(quad[i]) 881 | quad = quad / peak 882 | quad[np.isnan(quad)] = 0 883 | 884 | imag = supersmoother2pole(quad, smooth_period) 885 | 886 | signal = (imag>real)*1 - (imag0 929 | corr_2 = np.roll(corr_1,1) 930 | delta = np.abs(corr_1-corr_2)/2 931 | delta[0] = 0 932 | sumdelta = np.zeros_like(delta) 933 | for i in range(lag): 934 | sumdelta += np.roll(delta, i) 935 | reversal = sumdelta>=thresh 936 | return reversal, sumdelta 937 | 938 | 939 | def convolution(series, hp_period, lp_period, lookback_period): 940 | """ 941 | convolution indicator: use convolution within a lookback period to determine whether a turning point has occurred 942 | :param series: (np.array) price 943 | :param hp_period: highpass period to remove the trend from the original price 944 | :param lp_period: lowpass period to smooth the original price 945 | :param lookback_period: lookback period to compute convolution 946 | """ 947 | HP = highpass2pole(series, hp_period) 948 | filt = supersmoother2pole(HP, lp_period) 949 | 950 | corr = np.zeros_like(series) 951 | for i in range(lookback_period, series.shape[0]+1): 952 | lookback_ = filt[i-lookback_period:i] 953 | corr[i-1] = np.corrcoef(lookback_, np.flip(lookback_))[0,1] 954 | conv = (1+(np.exp(3*corr)-1)/(np.exp(3*corr)+1))/2 955 | 956 | return conv, corr 957 | 958 | 959 | 960 | def get_weights(diff_amt, min_weight, max_window): 961 | """ 962 | compute the weights for fractional differentiation 963 | :param diff_amt: (float) order of fractional differentiation 964 | :param min_weight: (float) lower bound for weights 965 | :param max_window: (int) upper bound for window length 966 | :return: (np.array) weights 967 | """ 968 | weights = [1.] 969 | k, ctr = 1, 1 970 | while True: 971 | weights_ = -weights[-1] * (diff_amt - k + 1) / k 972 | if abs(weights_) < min_weight: 973 | break 974 | else: 975 | weights.append(weights_) 976 | k += 1 977 | ctr += 1 978 | if ctr == max_window: 979 | break 980 | return np.array(weights) 981 | 982 | 983 | def ffd(series, diff_amt, min_weight=1e-5): 984 | """ 985 | fractional differentiation (fixed window) 986 | :param series: (np.array) price 987 | :param diff_amt: (float) order of fractional differentiation 988 | :param min_weight: (float) lower bound for weights 989 | :return: (np.array) differentiated series 990 | """ 991 | weights = get_weights(diff_amt, min_weight, series.shape[0]) 992 | window = len(weights) 993 | frac_diff = np.full(series.shape[0], np.nan) 994 | for i in range(window-1, series.shape[0]): 995 | frac_diff[i] = np.sum(weights * series[i-window+1 : i+1]) 996 | return frac_diff 997 | 998 | 999 | def ffd_ma(series, diff_amt, ma_period, min_weight=1e-5): 1000 | """ 1001 | moving average of fractional differentiation 1002 | :param series: (np.array) price 1003 | :param diff_amt: (float) order of fractional differentiation 1004 | :param ma_period: (int) ma window length 1005 | :param min_weight: (float) lower bound for weights 1006 | :return: (np.array) ma of differentiated series 1007 | """ 1008 | frac_diff = ffd(series, diff_amt, min_weight) 1009 | feac_diff_ma = ta.SMA(frac_diff, ma_period) 1010 | 1011 | return feac_diff_ma 1012 | 1013 | 1014 | def HA_candle(o, h, l, c): 1015 | """ 1016 | Heiken Ashi Candles 1017 | :param o: (np.array) open 1018 | :param h: (np.array) high 1019 | :param l: (np.array) low 1020 | :param c: (np.array) close 1021 | :return: (np.array) HA ohlc 1022 | """ 1023 | HAc = (o+h+l+c) / 4 1024 | HAo, HAh, HAl = HAc.copy(), HAc.copy(), HAc.copy() 1025 | 1026 | for i in range(1, o.shape[0]): 1027 | HAo[i] = (HAo[i - 1] + HAc[i - 1]) / 2 1028 | HAh[i] = np.array((h[i], HAo[i], HAc[i])).max() 1029 | HAl[i] = np.array((l[i], HAo[i], HAc[i])).min() 1030 | 1031 | return HAo, HAh, HAl, HAc 1032 | 1033 | 1034 | def diff(series): 1035 | """ 1036 | differentiation 1037 | :param series: (np.array) price 1038 | :return: (np.array) differentiated series 1039 | """ 1040 | series = series - np.roll(series, 1) 1041 | series[0] = 0 1042 | return series 1043 | 1044 | --------------------------------------------------------------------------------