<jupyter_start><jupyter_text>In this notebook, we explore the usage of triple-barrier method / meta-labelling to identify price signals in the market using an ML approach.  The triple-barrier is a simple labelling technique consisting of three barriers: an upper barrier, a lower barrier and a vertical barrier.   1. Upper Barrier: Threshold an observation return needs to reach to consider a BUY (1)  2. Lower Barrier: Treshold an observed return needs to reach to consider a SELL (-1)  3. Vertical Barrier: Amount of time the observation has to reach without hitting the upper or lower barrier (0) ![image.png](attachment:image.png) Meta Learning is where we make use of a secondary ML model to learn how to use the result of the first ML model. In this instance, it is termed meta labelling as we train the model to decide if we should trade or pass on the initial predict of [-1, 0, 1]. We do this by labelling the predicted outputs with the actual labels of the primary model as use the new label as prediction goal for the second model. ![image.png](attachment:image.png)<jupyter_code>data = hist.copy()
hist.dropna(axis=0, how='any', inplace=True)
factor = [2, 2] # Scale of barrier height
labels = TripleBarrier(hist['Close'], vol_span=50, barrier_horizon=5, factors=factor, label=0).labels
# Load additional features
data['log_ret'] = np.log(data['Close']).diff()
data['mom1'] = data['Close'].pct_change(periods=1)
data['mom2'] = data['Close'].pct_change(periods=2)
data['mom3'] = data['Close'].pct_change(periods=3)
data['mom4'] = data['Close'].pct_change(periods=4)
data['mom5'] = data['Close'].pct_change(periods=5)
data['volatility'] = data['Close'].rolling(window=7, min_periods=7, center=False).std()
data['autocorr_1'] = data['Close'].rolling(window=7, min_periods=7, center=False).apply(lambda x: x.autocorr(lag=1), raw=False)
data['autocorr_2'] = data['Close'].rolling(window=7, min_periods=7, center=False).apply(lambda x: x.autocorr(lag=2), raw=False)
data['autocorr_3'] = data['Close'].rolling(window=7, min_periods=7, center=False).apply(lambda x: x.autocorr(lag=3), raw=False)
data['autocorr_4'] = data['Close'].rolling(window=7, min_periods=7, center=False).apply(lambda x: x.autocorr(lag=4), raw=False)
data['autocorr_5'] = data['Close'].rolling(window=7, min_periods=7, center=False).apply(lambda x: x.autocorr(lag=5), raw=False)
data['log_t1'] = data['log_ret'].shift(1)
data['log_t1'] = data['log_ret'].shift(2)
data['log_t1'] = data['log_ret'].shift(3)
data['log_t1'] = data['log_ret'].shift(4)
data['log_t1'] = data['log_ret'].shift(5)
data['fast_mavg'] = data['Close'].rolling(window=7, min_periods=7, center=False).mean()
data['slow_mavg'] = data['Close'].rolling(window=15, min_periods=15, center=False).mean()
# Split into X and Y 
X = data.loc[labels.index, :]
X = X.dropna()
y_labels = labels.loc[X.index, :]
y = y_labels['bin']
# Use VIF to identify and drop highly correlated features 
k = X.copy()
k = k.replace([np.inf, -np.inf], np.nan).dropna(axis=1)

def calc_vif(k, threshold=5):
    vif = pd.DataFrame()
    vif["VIF Factor"] = [variance_inflation_factor(k.values, i) for i in range(k.shape[1])]
    vif["features"] = k.columns
    vif = vif.replace([np.inf, -np.inf], np.nan)
    vif = vif.dropna()
    vif["VIF Factor"] = vif[vif["VIF Factor"] < threshold]
    vif = vif.dropna()
    print(vif)
    return vif.features.values.tolist()

X = k[calc_vif(k)]
# Split into Train Test and leave the last year for OOS 
X_training_val = X['2001-01-01' : '2019-01-01']
Y_training_val = y['2001-01-01' : '2019-01-01']
X_train, X_validate, y_train, y_validate = train_test_split(X_training_val, Y_training_val, test_size=0.2, shuffle=False)
train_df = pd.concat([y_train, X_train], axis=1, join='inner')
train_df['bin'].value_counts()
# Lets Resample them 

majority = train_df[train_df['bin']==0]
mid = train_df[train_df['bin']==-1]
minority = train_df[train_df['bin']==1]

sell = resample(minority,
               replace=True,
               n_samples=majority.shape[0],
               random_state=144)

buy = resample(mid,
              replace=True,
              n_samples=majority.shape[0],
              random_state=144)

train_df = pd.concat([majority, buy, sell])
train_df = shuffle(train_df, random_state=144)
train_df['bin'].value_counts()
# Create training data
y_train = train_df['bin']
X_train = train_df.loc[:, train_df.columns != 'bin']
parameters = {'max_depth':[2, 3, 4, 5, 6, 7],
             'n_estimators':[1, 10, 25, 50, 100, 256, 512],
             'random_state':[144]}

def grid_search(X, y):
    rf = RandomForestClassifier(criterion='entropy')
    clf = GridSearchCV(rf, parameters, cv=4, scoring='f1_macro', n_jobs=-1)
    clf.fit(X, y)
    return clf.best_params_['n_estimators'], clf.best_params_['max_depth']

n_estimators, depth = grid_search(X_train, y_train)<jupyter_output><empty_output><jupyter_text>Primary Model Get the predictions of the first model [-1, 0, 1]<jupyter_code># Random Forest Model
rf = RandomForestClassifier(max_depth=depth, n_estimators=n_estimators, criterion='entropy', random_state=144)
rf.fit(X_train, y_train.values.ravel())
# Metrics
y_pred = rf.predict(X_validate)
print(classification_report(y_validate, y_pred))
print("Confusion Matrix")
print(confusion_matrix(y_validate, y_pred))
print("Accuracy")
print(accuracy_score(y_validate, y_pred))
def binary_label(y_pred, y_test):
    bin_label = np.zeros_like(y_pred)
    for i in range(y_pred.shape[0]):
        if y_pred[i] != 0 and y_pred[i]*y_test[i] > 0:
            bin_label[i] = 1 
    return bin_label

print(classification_report(binary_label(y_pred, y_validate), y_pred != 0))
print("Confusion Matrix")
print(confusion_matrix(binary_label(y_pred, y_validate), y_pred != 0))
print("Accuracy")
print(accuracy_score(binary_label(y_pred, y_validate), y_pred != 0))<jupyter_output>precision    recall  f1-score   support

           0       1.00      0.30      0.47       713
           1       0.10      1.00      0.18        55

    accuracy                           0.35       768
   macro avg       0.55      0.65      0.32       768
weighted avg       0.94      0.35      0.45       768

Confusion Matrix
[[217 496]
 [  0  55]]
Accuracy
0.3541666666666667<jupyter_text>Secondary Model<jupyter_code>y_train_pred = rf.predict(X_train)
X_train_meta = np.hstack([y_train_pred[:, None], X_train])
X_test_meta = np.hstack([y_pred[:, None], X_validate])
y_train_meta = binary_label(y_train_pred, y_train)
sm = SMOTE()
X_train_meta_res, y_train_meta_res = sm.fit_sample(X_train_meta, y_train_meta)
n_estimators, depth = grid_search(X_train_meta_res, y_train_meta_res)
rf2 = RandomForestClassifier(max_depth=depth, n_estimators=n_estimators, criterion='entropy', random_state=144)
model_s = rf2.fit(X_train_meta_res, y_train_meta_res)
y_pred_meta = model_s.predict(X_test_meta)

print("Confusion Matrix")
print(confusion_matrix(binary_label(y_pred, y_validate), (y_pred * y_pred_meta) != 0))
print("Accuracy")
print(accuracy_score(binary_label(y_pred, y_validate), (y_pred * y_pred_meta) != 0))<jupyter_output>Confusion Matrix
[[284 429]
 [ 10  45]]
Accuracy
0.4283854166666667<jupyter_text>OOS Sample Results<jupyter_code>X_oos = X['2019-01-01':'2020-01-01']
y_oos = y['2019-01-01':'2020-01-01']
y_pred = rf.predict(X_oos)
X_test_meta = np.hstack([y_pred[:,None], X_oos])
y_pred_meta = model_s.predict(X_test_meta)
print(classification_report(binary_label(y_pred, y_validate), (y_pred*y_pred_meta) != 0))
print("Confusion Matrix")
print(confusion_matrix(binary_label(y_pred, y_validate), (y_pred*y_pred_meta) != 0))
print("Accuracy")
print(accuracy_score(binary_label(y_pred, y_validate), (y_pred*y_pred_meta) != 0))
predictions = y_pred * y_pred_meta

# Compare against market returns
price = hist['Open']
ret = price[X_oos.index]
d = pd.DataFrame()
d['stock_returns'] = np.log(ret) - np.log(ret.shift(1))
d['strategy_returns'] = d['stock_returns'] * predictions
d[['stock_returns', 'strategy_returns']].cumsum().plot(figsize=(15,8))<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1479171863307558, 'fraction_numerical': 0.03891244699426291, 'mean_word_length': 5.294348508634223, 'pattern_counts': {'":': 0, '<': 16, '<?xml version=': 0, '>': 16, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '621530', 'license': 'MIT', 'n_tokens_mistral': 3132, 'n_tokens_neox': 2930, 'n_words': 631, 'path': 'Machine Learning Approach/Predicting Price Signals - Machine Learning Approach.ipynb', 'repo_name': 'zhengyiwong/Predicting-Price-Signals', 'size': '8018'}