LSTM Trading Signal Detection

Optimizing Trading Signals Using LSTM Deep Learning Models and Performing Historical Backtesting

Photo by Nimisha Mekala on Unsplash

Summary of key points of this article

Artical Difficulty:★★★☆☆

閱讀建議:本文使用RNN架構進行時間序列預測,需要對時間序列或是深度學習有基礎瞭解,可以參考前篇LSTM預測股價的文章【資料科學】LSTM,有助於對本文有更深入的了解。

Reading Recommendation: This article utilizes an RNN (Recurrent Neural Network) architecture for time series prediction. It is advisable for readers to have a foundational understanding of time series analysis or deep learning. You can refer to the previous article on 【Data Science】LSTM for a more in-depth understanding of this article.

Introdution

In the previous article, we used an LSTM model to predict stock price trends by using the past 10 days’ opening prices, highest prices, lowest prices, closing prices, and trading volumes to predict the closing price for the next day. However, we observed that the model’s performance was not very satisfactory when relying solely on yesterday’s stock price to predict tomorrow’s price. Therefore, we have decided to change our approach. This time, we aim to use the model to help us identify buy and sell points and formulate a trading strategy. We have also incorporated more feature indicators with the hope of achieving better results.

Characteristic Indicators and Introduction

We have added eight new feature indicators, four of which are technical indicators, and four are macroeconomic indicators, with the hope of enhancing our prediction results using these two facets of feature values.

Technical Indicators:

KD (Stochastic Oscillator): Represents the current price’s relative high-low changes over a specified period, indicating price momentum.

RSI (Relative Strength Index): Measures the strength of price movements, indicating the balance between buying and selling pressures.

MACD (Moving Average Convergence Divergence): Indicates the convergence or divergence of long-term and short-term moving averages, helping identify potential trend changes.

MOM (Momentum): Observes the magnitude of price changes and market trend direction.

Macroeconomic Indicators:

Taiwan Economic Composite Index: Represents a crucial macroeconomic variable reflecting economic activity and changes in the business cycle.

VIX Index: Reflects market volatility and serves as an indicator of market sentiment and panic.

Leading Indicators: Economic indicators that provide early insights into future economic trends, aiding in predicting economic conditions.

Taiwan Stock Average P/E Ratio: Calculates the average P/E ratio of listed companies, offering insights into overall market sentiment, whether it’s optimistic or pessimistic.

Editing Environment and Module Requirements

This article is based on the Windows operating system and utilizes Jupyter as the editor.

import tejapi
import pandas as pd

tejapi.ApiConfig.api_key = "Your Key"
tejapi.ApiConfig.ignoretz = True

Database Usage

0050 Adjustment of stock price (day) — ex-dividend adjustment (TWN/APRCD1)
Average price-to-earnings ratio of Taiwan stocks – overall economy (GLOBAL/ANMAR)
Taiwan’s Prosperity Countermeasure Signal – Overall Economy (GLOBAL/ANMAR)
Leading Indicators – General Economy (GLOBAL/ANMAR)
Chicago VIX Index — International Stock Price Index (GLOBAL/GIDX)

Data Loading

The data used includes the ex-dividend and adjusted stock prices, as well as the opening price, closing price, highest price, lowest price, and trading volume for the Taiwan 50 Index (0050) spanning from January 2011 to November 2022.

coid = "0050"
mdate = {'gte':'2011-01-01', 'lte':'2022-11-15'}
data = tejapi.get('TWN/APRCD1',
                          coid = coid,
                          mdate = {'gte':'2011-01-01', 'lte':'2022-11-15'},
                          paginate=True)


#Open high, close low, trading volume
data = data[["coid","mdate","open_adj","high_adj","low_adj","close_adj","amount"]]
data = data.rename(columns={"coid":"coid","mdate":"mdate","open_adj":"open",
                   "high_adj":"high","low_adj":"low","close_adj":"close","amount":"vol"})

Technical indicators(KD、RSI、MACD、MOM)

from talib import abstract
data["rsi"] = abstract.RSI(data,timeperiod=14)
data[["macd","macdsig","macdhist"]] = abstract.MACD(data)
data[["kdf","kds"]] = abstract.STOCH(data)
data["mom"] = abstract.MOM(data,timeperiod=15)
data.set_index(data["mdate"],inplace = True)

General economic indicators (Taiwan stock average price-earnings ratio, Taiwan’s business climate countermeasure signal, leading indicators, Chicago VIX index)

data1 = tejapi.get('GLOBAL/ANMAR',
mdate = mdate,
coid = "SA15",
paginate=True)
data1.set_index(data1["mdate"],inplace = True)
data1 = data1.resample('D').ffill()
data = pd.merge(data,data1["val"],how='left', left_index=True, right_index=True)
data.rename({"val":"pe"}, axis=1, inplace=True)
#芝加哥VIX指數
data2 = tejapi.get('GLOBAL/GIDX',
coid = "SB82",
mdate = mdate,
paginate=True)
data2.set_index(data2["mdate"],inplace = True)
data = pd.merge(data,data2["val"],how='left', left_index=True, right_index=True)
data.rename({"val":"vix"}, axis=1, inplace=True)
#景氣對策訊號
data3 = tejapi.get('GLOBAL/ANMAR',
coid = "EA1101",
mdate = mdate,
paginate=True)
data3.set_index(data3["mdate"],inplace = True)
data3 = data3.resample('D').ffill()
data = pd.merge(data,data3["val"],how='left', left_index=True, right_index=True)
data.rename({"val":"light"}, axis=1, inplace=True)
#領先指標
data4 = tejapi.get('GLOBAL/ANMAR',
coid = "EB0101",
mdate = mdate,
paginate=True)
data4.set_index(data4["mdate"],inplace = True)
data4 = data4.resample('D').ffill()
data = pd.merge(data,data4["val"],how='left', left_index=True, right_index=True)
data.rename({"val":"advance"}, axis=1, inplace=True)

刪除空值與無用欄位

data.set_index(data["mdate"],inplace=True)
data = data.fillna(method="pad",axis=0)
data = data.dropna(axis=0)
del data["coid"]
del data["mdate"]
data
Data Organization Chart

Buy and Sell Signals

We have chosen to define trends by combining moving averages with momentum indicators. A simple criterion for identifying an upward trend is when MA10 > MA20 and RSI10 > RSI20.

data["short_mom"] = data["rsi"].rolling(window=10,min_periods=1,center=False).mean()
data["long_mom"] = data["rsi"].rolling(window=20,min_periods=1,center=False).mean()
data["short_mov"] = data["close"].rolling(window=10,min_periods=1,center=False).mean()
data["long_mov"] = data["close"].rolling(window=20,min_periods=1,center=False).mean()

Labels
An upward trend is marked as 1, otherwise it is marked as 0.

import numpy as np
data['label'] = np.where(data.short_mov > data.long_mov, 1, 0)
data = data.drop(columns=["short_mov"])
data = data.drop(columns=["long_mov"])
data = data.drop(columns=["short_mom"])
data = data.drop(columns=["long_mom"])

Observing Data Distribution

It can be observed that the data distribution is not overly skewed, but due to the overall upward trend in the market, a higher number of upward trends is a normal occurrence.

Data Distribution

Data Preprocessing

Data Standardization

X = data.drop('label', axis = 1)
from sklearn.preprocessing import StandardScaler
X[X.columns] = StandardScaler().fit_transform(X[X.columns])
y = pd.DataFrame({"label":data.label})

Cut into learning samples and test samples, the ratio is 7:3
The training data time range is 2011.02.25–2019.05.08
The test data time range is 2019.05.09–2022.11.15

import numpy as np
split = int(len(data)*0.7)
train_X = X.iloc[:split,:].copy()
test_X = X.iloc[split:].copy()
train_y = y.iloc[:split,:].copy()
test_y = y.iloc[split:].copy()

X_train, y_train, X_test, y_test = np.array(train_X), np.array(train_y), np.array(test_X), np.array(test_y)

Reshaping Data into Three Dimensions to Suit the Subsequent Model

X_train = np.reshape(X_train, (X_train.shape[0],1,16))
y_train = np.reshape(y_train, (y_train.shape[0],1,1))
X_test = np.reshape(X_test, (X_test.shape[0],1,16))
y_test = np.reshape(y_test, (X_test.shape[0],1,1))

LSTM Model

Incorporate the Model

Include four LSTM layers with Dropout to prevent overfitting

from keras.models import Sequential
from keras.layers import Dense
from keras.layers import LSTM
from keras.layers import Dropout
from keras.layers import BatchNormalization

regressor = Sequential()
regressor.add(LSTM(units = 32, return_sequences = True, input_shape = (X_train.shape[1], X_train.shape[2])))
regressor.add(BatchNormalization())
regressor.add(Dropout(0.35))
regressor.add(LSTM(units = 32, return_sequences = True))
regressor.add(Dropout(0.35))
regressor.add(LSTM(units = 32, return_sequences = True))
regressor.add(Dropout(0.35))
regressor.add(LSTM(units = 32))
regressor.add(Dropout(0.35))
regressor.add(Dense(units = 1,activation="sigmoid"))
regressor.compile(optimizer = 'adam', loss="binary_crossentropy",metrics=["accuracy"])
regressor.summary()
Model Structure (Part I)
Model Structure (Part II)

Model Results (Training Set)

Set the number of epochs to 100.

train_history = regressor.fit(X_train,y_train,
batch_size=200,
epochs=100,verbose=2,
validation_split=0.2)

Model Evaluation

By examining the Model Loss chart, it’s evident that during the training process, the two lines converge, indicating that the model did not overfit.

import matplotlib.pyplot as plt
loss = train_history.history["loss"]
var_loss = train_history.history["val_loss"]
plt.plot(loss,label="loss")
plt.plot(var_loss,label="val_loss")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.title("model loss")
plt.legend(["train","valid"],loc = "upper left")
收斂情形

Variable Importance

It shows the importance level of different features. It indicates that MACD, Taiwan Stock Average P/E Ratio, and RSI are important features.

from tqdm.notebook import tqdm
results = []
print(' Computing LSTM feature importance...')
# COMPUTE BASELINE (NO SHUFFLE)
oof_preds = regressor.predict(X_test, verbose=0).squeeze()
baseline_mae = np.mean(np.abs(oof_preds-y_test))

results.append({'feature':'BASELINE','mae':baseline_mae})

for k in tqdm(range(len(list(test_X.columns)))):

# SHUFFLE FEATURE K
save_col = X_test[:,:,k].copy()
np.random.shuffle(X_test[:,:,k])

# COMPUTE OOF MAE WITH FEATURE K SHUFFLED
oof_preds = regressor.predict(X_test, verbose=0).squeeze()
mae = np.mean(np.abs( oof_preds-y_test ))
results.append({'feature':test_X.columns[k],'mae':mae})
X_test[:,:,k] = save_col
特徵重要性

Model Results (Test Set)

The accuracy on the test set is as high as 95.49%, indicating that the LSTM model can effectively execute our strategy.

regressor.evaluate(X_test, y_test,verbose=1)

Comparing Real Labels and Model Predictions (Predictions)

Visualization of the Strategy

LSTM Strategy Trend Prediction Chart, where red represents an upward trend, and green represents a downward trend.

import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import datetime as dt

df = result.copy()
df = df.resample('D').ffill()

t = mdates.drange(df.index[0], df.index[-1], dt.timedelta(hours = 24))
y = np.array(df.Close[:-1])

fig, ax = plt.subplots()
ax.plot_date(t, y, 'b-', color = 'black')
for i in range(len(df)):
if df.Predict[i] == 1:
ax.axvspan(
mdates.datestr2num(df.index[i].strftime('%Y-%m-%d')) - 0.5,
mdates.datestr2num(df.index[i].strftime('%Y-%m-%d')) + 0.5,
facecolor = 'red', edgecolor = 'none', alpha = 0.5
)
else:
ax.axvspan(
mdates.datestr2num(df.index[i].strftime('%Y-%m-%d')) - 0.5,
mdates.datestr2num(df.index[i].strftime('%Y-%m-%d')) + 0.5,
facecolor = 'green', edgecolor = 'none', alpha = 0.5
)
fig.autofmt_xdate()
fig.set_size_inches(20,10.5)

Strategy Backtesting

When the trend signal is upward, buy one position and hold it. When the trend signal turns downward, sell the original position and take a short position, holding it until the next signal indicates an upward trend, at which point, close the position.

*Note: This strategy does not account for transaction costs, and all capital is used for entering and exiting positions.

test_data = data.iloc[split:].copy()
backtest = pd.DataFrame(index=result.index)
backtest["r_signal"] = list(test_data["label"])
backtest["p_signal"] = list(result["Predict"])
backtest["m_return"] = list(test_data["close"].pct_change())

backtest["r_signal"] = backtest["r_signal"].replace(0,-1)
backtest["p_signal"] = backtest["p_signal"].replace(0,-1)
backtest["a_return"] = backtest["m_return"]*backtest["r_signal"].shift(1)
backtest["s_return"] = backtest["m_return"]*backtest["p_signal"].shift(1)
backtest[["m_return","s_return","a_return"]].cumsum().hist()
backtest[["m_return","s_return","a_return"]].cumsum().plot()
Backtesting Result


The cumulative return of the LSTM strategy is 82.6%
The actual strategy (MA+MOM) cumulative return is 71.3%
The cumulative return for large-cap Buy and Hold is 52%

Orange: LSTM strategy; Blue: Buy and hold; Green: Actual strategy

Conclusion

The main focus of this study was whether LSTM could accurately identify buy and sell points according to our predefined original strategy. The result was affirmative, with a high accuracy of 95.49%, and a cumulative return of 82.6% in backtesting. This performance even outperformed the original strategy and significantly beat the market’s return of 52%. We believe that one reason for outperforming the original strategy is that LSTM generated fewer trading signals during consolidation periods, avoiding the frequent whipsawing that can lead to reduced trading performance.

Lastly, we would like to reiterate that the assets mentioned in this article are for illustrative purposes only and do not constitute recommendations or advice regarding any financial products. Therefore, if readers are interested in topics such as strategy development, performance testing, empirical research, etc., they are welcome to explore the solutions available in theTEJ E Shop, which provide comprehensive databases and tools for various analyses and assessments.

Complete Code

Further Reading

Related l=Link

Back