<jupyter_start><jupyter_text>Triple Barrier MethodThis notebook will cover partial exercise answers:* Exercise 3.5As we go along, there will be some explanations.More importantly, this method can be applied not just within mean-reversion strategy but also other strategies as well. Most of the functions below can be found under research/Labels.Contact: <EMAIL><jupyter_code>import numpy as np
import pandas as pd
import research as rs
import matplotlib.pyplot as plt

%matplotlib inline

p = print

#pls take note of version
#numpy 1.17.3
#pandas 1.0.3
#sklearn 0.21.3

dollar = pd.read_csv('./research/Sample_data/dollar_bars.txt', 
                 sep=',', 
                 header=0, 
                 parse_dates = True, 
                 index_col=['date_time'])

def bband(data: pd.DataFrame, window: int = 21, width: float = 0.001):
    avg = data['close'].ewm(span = window).mean()
    std = avg * width
    upper = avg + std
    lower = avg - std
    return avg, upper, lower, std

dollar['ewm'], dollar['upper'], dollar['lower'], dollar['std'] = bband(dollar)

# Check for normality, serial correlation, overall statistical properties, frequency count stability
dollar['side'] = np.nan

def side_pick(data: pd.DataFrame):
    for i in np.arange(data.index.shape[0]):
        if (data['close'].iloc[i] >= data['upper'].iloc[i]):
            data['side'].iat[i] = -1
        elif (data['close'].iloc[i] <= data['lower'].iloc[i]):
            data['side'].iat[i] = 1
    return data

upper = dollar[dollar['upper'] < dollar['close']] # short signal
lower = dollar[dollar['lower'] > dollar['close']] # long signal

p("Num of times upper limit touched: {0}\nNum of times lower limit touched: {1}"
  .format(upper.count()[0], 
          lower.count()[0]))

# Recall white test as a benchmark and until this stage we filtered all those which did not meet min return
dollar = side_pick(dollar)
dollar.dropna(inplace= True)
dollar['side'].value_counts()
copy_dollar = dollar.copy() # make a back copy to be used in later exercise

copy_dollar #up till this point the below dataframe should look like this, before tri_bar func. This is our primary model.
d_vol = rs.vol(dollar['close'], span0 = 50)
events = rs.cs_filter(dollar['close'], 
                    limit = d_vol.mean())

events
vb = rs.vert_barrier(data = dollar['close'], 
                 events = events, 
                 period = 'days', 
                 freq = 1)

vb # Show some example output
tb = rs.tri_barrier(data = dollar['close'], 
                events = events, 
                trgt = d_vol, 
                min_req = 0.002, 
                num_threads = 3, 
                ptSl = [0,2], #change ptSl into [0,2]
                t1 = vb, 
                side = dollar['side'])

tb # Show some example
m_label = rs.meta_label(data = dollar['close'],
                      events = tb,
                      drop = False)

m_label # Show some example
m_label['bin'].value_counts(normalize=True)

# Here is a quick look at our 'bin' values.
# Slight imbalanced sample, but not much harm
# 51.95% of the sample based on parameter touched vertical barrier first<jupyter_output><empty_output><jupyter_text>* Exercise 3.5bHere onwards we will be using sklearn modules to perform ML related task.<jupyter_code>from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

# A quick look at what we have till date using both primary and secondary model
# as seen in previous example, only 48.04% was labeled 1. 
# Hence precision 1.0 = 0.48 (48% of the sample is relevant), while recall = 1 means fully correct (based on the 48% sample)<jupyter_output><empty_output><jupyter_text>The below function report_matrix is what we have till date using both primary (bband func) and secondary model (tri_bar func). Classification ReportAs seen in previous example, only 48.0455% was labeled 1. Hence precision 1.0 = 0.48 (48.0455% of the sample is relevant). It's basically ML's way of saying are these "features" relevant when tested.While recall = 1 means fully correct (based on the 48% sample). In the case where ML model is fitted, this result will mean the percentage of "correct" label was chosen. In short, is the ML model reliability in True positive identification based on given sample. Confusion Matrix8001 = False Positive (51.95%)7399 = True Positive (48.0455%) Accuracy ScoreIs a mere reflection of True Positive, which again is 48.0455%<jupyter_code># this func can be found under Tools/stats_rpt

forecast = rs.report_matrix(actual_data = m_label, 
                            prediction_data = None,
                            ROC = None)<jupyter_output>Classification Report
=======================================================
              precision    recall  f1-score   support

         0.0       0.00      0.00      0.00      8001
         1.0       0.48      1.00      0.65      7399

    accuracy                           0.48     15400
   macro avg       0.24      0.50      0.32     15400
weighted avg       0.23      0.48      0.31     15400


 Confusion Matrix
=======================================================
[[TN, FP]
 [FN, TP]]

[[   0 8001]
 [   0 7399]]

 Accuracy Score
=======================================================
0.48045454545454547<jupyter_text>Built a list of features.1. Volatility2. Autocorrelation3. Moving average4. log-price return (optional)5. Stationary series based on cumulative sum log-price return (optional)The last 2 items will be explained at AFML chapter 5, fractional differentiated features.<jupyter_code># Data that was copied earlier before tri_bar func, this is our primary model only

copy_dollar # Show example
# drop redundant columns and keep crossing moving avaerages
pri_dollar = copy_dollar.drop(['open', 'high', 'low', 'cum_vol', 'cum_dollar', 'cum_ticks'], axis = 1)

#include volatility, autocorrelation
pri_dollar
#include original volatility
pri_dollar['volatility'] = rs.vol(pri_dollar.close, span0 = 50)
# Optional: getting stationarity feature
pri_dollar['log_price'] = pri_dollar.close.apply(np.log)
pri_dollar['log_return'] = pri_dollar.log_price.diff()

cs_log = pri_dollar.log_price.diff().dropna().to_frame()
pri_dollar['stationary'] = rs.fracDiff_FFD(data = cs_log, d = 1.99999889 , thres = 1e-5)

rs.unit_root(pri_dollar['stationary'].dropna()) #check for stationarity
pri_dollar.dropna(inplace = True)
# autocorrelation residual feature, we will add AR features up to 2 lags
from statsmodels.tsa.arima_model import ARMA

pri_dollar['ar_0'] = ARMA(pri_dollar['stationary'], order=(0,0)).fit().resid
pri_dollar['ar_1'] = ARMA(pri_dollar['stationary'], order=(1,0)).fit().resid
pri_dollar['ar_2'] = ARMA(pri_dollar['stationary'], order=(2,0)).fit().resid
#final dataset

secondary_dollar = pri_dollar.copy()<jupyter_output><empty_output><jupyter_text>**Note* Good to include volume based or volume-weighted indicator as a predictive feature i.e. OBV, VWAP **Note**May try to add other types of trend related features as part of experimental Mathematics. (aka Trial & error)* Good to include volume based or volume-weighted indicator as a predictive feature i.e. OBV, VWAP* If not, try to add price based as predictive feature i.e. MOM, RSI<jupyter_code># Now we run all the steps to complete labels, to train random forest.
# we will use both primary & secondary model

events0 = rs.cs_filter(secondary_dollar['close'], 
                    limit = secondary_dollar['volatility'].mean())

vb0 = rs.vert_barrier(data = secondary_dollar['close'], 
                 events = events0, 
                 period = 'days', 
                 freq = 1)

tb0 = rs.tri_barrier(data = secondary_dollar['close'], 
                events = events0, 
                trgt = secondary_dollar['volatility'], 
                min_req = 0.002, 
                num_threads = 3, 
                ptSl = [0,2], #change ptSl into [0,2]
                t1 = vb0, 
                side = secondary_dollar['side'])

m_label0 = rs.meta_label(data = secondary_dollar['close'],
                      events = tb0,
                      drop = 0.05)

m_label0
m_label0['bin'].value_counts() 

# we still get back the same count. This is correct. 
# Tri_bar func is to calculate if vert_bar was triggered and consolidates the target.
# while label will check which are the ones that hitted vertical barriers or non-profitable will be label 0
# At this stage you may wish to run Grid search CV, but I'm skipping that.

n_estimators, max_depth, c_random_state = 500, 7, 42

# Random Forest Model
rf = RandomForestClassifier(max_depth=max_depth, 
                            n_estimators=n_estimators,
                            criterion='entropy', 
                            class_weight = None, #This will be cover in next few chapters
                            random_state=c_random_state)

X = secondary_dollar.reindex(m_label0.index) # this dataframe only contain all our features
y = m_label0['bin']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=False)

rf.fit(X_train, y_train.values.ravel())
# Performance Metrics
y_prob = rf.predict_proba(X_train)[:, 1] #here we are only interested in True positive
y_pred = rf.predict(X_train)

p('Matrix training report for primary model & secondary model\n')

rs.report_matrix(actual_data = y_train, # we need to use our train data from train_test_split
                 prediction_data = y_pred, 
                 ROC = y_prob)
# Meta-label
# Performance Metrics
y_prob = rf.predict_proba(X_test)[:, 1] #here we are only interested in True positive
y_pred = rf.predict(X_test)

p('Matrix test report for primary model & secondary model\n')

rs.report_matrix(actual_data = y_test, 
                 prediction_data = y_pred, 
                 ROC = y_prob)

rs.feat_imp(rf, X)
**Now we start to create only primary model**
events1 = rs.cs_filter(pri_dollar['close'], 
                    limit = pri_dollar['volatility'].mean())

vb1 = rs.vert_barrier(data = pri_dollar['close'], 
                 events = events1, 
                 period = 'days', 
                 freq = 1)

tb1 = rs.tri_barrier(data = pri_dollar['close'], 
                events = events1, 
                trgt = pri_dollar['volatility'], 
                min_req = 0.002, 
                num_threads = 3, 
                ptSl = [0,2], #change ptSl into [0,2]
                t1 = vb1, 
                side = None)

m_label1 = rs.meta_label(data = pri_dollar['close'],
                      events = tb1,
                      drop = 0.05) # take note we do not have a side hence we need to drop something
# Random Forest Model
rf = RandomForestClassifier(max_depth=max_depth, 
                            n_estimators=n_estimators,
                            criterion='entropy', 
                            class_weight = None, #This will be cover in next few chapters
                            random_state=c_random_state)

X = pri_dollar.reindex(m_label1.index) # this dataframe only contain all our features
y = m_label1['bin']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, shuffle=False)

rf.fit(X_train, y_train.values.ravel())
# Performance Metrics
y_prob = rf.predict_proba(X_train)[:, 1] #here we are only interested in True positive
y_pred = rf.predict(X_train)

p('Matrix training report for primary model only\n')

rs.report_matrix(actual_data = y_train, # we need to use our train data from train_test_split
                 prediction_data = y_pred, 
                 ROC = y_prob)
# Meta-label
# Performance Metrics
y_prob = rf.predict_proba(X_test)[:, 1] #here we are only interested in True positive
y_pred = rf.predict(X_test)

p('Matrix test report for primary model only\n')

rs.report_matrix(actual_data = y_test, 
                 prediction_data = y_pred, 
                 ROC = y_prob)

rs.feat_imp(rf, X)<jupyter_output>Matrix test report for primary model only

 Classification Report
=======================================================
              precision    recall  f1-score   support

        -1.0       0.46      0.73      0.56      2027
         1.0       0.60      0.31      0.41      2565

    accuracy                           0.50      4592
   macro avg       0.53      0.52      0.49      4592
weighted avg       0.54      0.50      0.48      4592


 Confusion Matrix
=======================================================
[[TN, FP]
 [FN, TP]]

[[1484  543]
 [1762  803]]

 Accuracy Score
=======================================================
0.4980400696864111