This post considers a simpler model based on some widely used technical indicators in combination with Long-Short-Term Memory networks (LSTM) applied to an example of somewhat more volatile financial time series such as cryptocurrency. For a brief background on LSTM networks and their application to financial time series see another post and references within.
#import necessary packages
import numpy as np
import yfinance as yf
import datetime as dt
import pandas as pd
import pandas_ta as ta
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import mean_squared_error
from keras.models import Sequential
from keras.layers import Dense, LSTM, Dropout
from sklearn.model_selection import train_test_split
import matplotlib.pyplot as plt
import warnings
# Load financial data
ticker = 'BTC-USD'
start_date = dt.date.today() - dt.timedelta(days=365*2)
end_date = dt.date.today() + dt.timedelta(days=1)
df = yf.download(ticker, start_date, end_date, progress=False)
# Calculate daily returns
df['Returns'] = df['Adj Close'].pct_change()
# Plot data
fig, ax = plt.subplots(2, 1, figsize=(10, 8))
ax[0].plot(df['Close'])
ax[0].set_title('BTC-Close Price')
ax[0].set_ylabel('Daily Price')
ax[1].plot(df['Returns'])
ax[1].set_title('Daily Returns')
ax[1].set_ylabel('Returns')
plt.tight_layout()
plt.show()
# Adding the technical indicators
df['RSI'] = ta.rsi(df['Close'], timeperiod=14)
df['ATR'] = ta.atr(df['High'], df['Low'], df['Close'], timeperiod=14)
# Calculate stochastic oscillator
stoch_results = ta.stoch(df['High'], df['Low'], df['Close'], window=14, smooth_window=3)
df['%K'] = stoch_results['STOCHk_14_3_3']
df['%D'] = stoch_results['STOCHd_14_3_3']
# Create MACD
df_macd=df.ta.macd(fast=12, slow=26, signal=9, append=True)
df_bollinger=df.ta.bbands(length=20, append=True)
# Add the MACD line to df
df['MACD'] = df_macd['MACD_12_26_9']
# Add the MACD signal line to df
df['MACD_signal'] = df_macd['MACDs_12_26_9']
# Add the MACD histogram to df
df['MACD_histogram'] = df_macd['MACDh_12_26_9']
# Add the upper Bollinger Band to df
df['Bollinger_upper'] = df_bollinger['BBU_20_2.0']
# Add the middle Bollinger Band to df
df['Bollinger_middle'] = df_bollinger['BBM_20_2.0']
# Add the lower Bollinger Band to df
df['Bollinger_lower'] = df_bollinger['BBL_20_2.0']
# Calculate On-balance volume
df['OBV']=ta.obv(df['Close'], df['Volume'])
# Calculate Relative Vigor Index
df['RVI']=ta.rvi(df['Close'], length=10)
#Calculate Williams %R:
df['WR']=ta.willr(df['High'], df['Low'], df['Close'], length=14)
# Calculate Accumulation Distribution Line (ADL)
df_ad=df.ta.ad(high='High', low='Low', close='Close', volume='Volume', append=False)
df['AD']=df_ad
# Calculate Chaikin Oscillator
df['chaikin_ema3'] = df['Volume'] * (2 / (1 + 3)) * ((df['Close'] - df['Low']) - (df['High'] - df['Close'])).cumsum()
df['chaikin_ema10'] = df['Volume'] * (2 / (1 + 10)) * ((df['Close'] - df['Low']) - (df['High'] - df['Close'])).cumsum()
df['chaikin'] = ta.ema(df['chaikin_ema3'], length=3) - ta.ema(df['chaikin_ema10'], length=10)
df = df.drop(['chaikin_ema3', 'chaikin_ema10'], axis=1)
# Calculate other technical indicators
df_adx=df.ta.adx(length=14, append=False)
df_cci=df.ta.cci(length=20, c=0.015, append=False)
df_psar=df.ta.psar(af=0.02, max_af=0.2, append=False)
df_trix=df.ta.trix(length=14, append=False)
df_vortex=df.ta.vortex(length=14, append=False)
df['CCI']=df_cci
df['VTX']=df_vortex['VTXM_14']
df['ADX']=df_adx['ADX_14']
df['PSAR']=df_psar['PSARaf_0.02_0.2']
df['TRIX']=df_trix['TRIX_14_9']
#Define features
features=['RSI','AD','ATR','%K','%D','MACD_signal','Bollinger_middle','OBV','RVI','WR','chaikin','CCI','VTX','ADX','PSAR','TRIX']
X=df[features]
y=df['Returns']
df_combined = pd.concat([X, y], axis=1)
df_combined = pd.concat([X, y], axis=1)
df_combined=df_combined.dropna()
X=df_combined.drop('Returns',axis=1)
y=df_combined['Returns']
# Create a scaler object
scaler = MinMaxScaler()
# Scale the input data
X_scaled = scaler.fit_transform(X)
# Split the data into training and testing sets
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y, test_size=0.2, random_state=5)
X_train = X_train.reshape((X_train.shape[0], X_train.shape[1], 1))
# Define the LSTM model:
model = Sequential()
model.add(LSTM(64, input_shape=(X_train.shape[1],1), return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(32, return_sequences=True))
model.add(Dropout(0.2))
model.add(LSTM(16))
model.add(Dropout(0.2))
model.add(Dense(1))
# Compile the model
model.compile(loss='mean_squared_error', optimizer='adam', metrics=['accuracy'])
#Fit the model:
model.fit(X_train, y_train, epochs=30, batch_size=32, validation_data=(X_test, y_test), verbose=0)
## <keras.src.callbacks.History object at 0x0000001806C4A6D0>
# Make predictions
y_pred = model.predict(X_test,verbose=0)
# Evaluate the model
score = model.evaluate(X_test, y_test, verbose=0)
print('Test loss:', score[0])
## Test loss: 0.0005294127040542662
print('Test accuracy:', score[1])
## Test accuracy: 0.0
returns_range = y.describe().loc['max']
print("Returns Range:", returns_range)
## Returns Range: 0.1454118392886461
# Compare the test loss with the range of target returns range:
if score[0] < returns_range:
print("Test Loss is within the range of returns.")
else:
print("Test Loss is outside the range of returns.")
## Test Loss is within the range of returns.
# Plotting :
# First extract values from y_test data frame
y_test_values = y_test.values
plt.plot(y_test_values, label='True')
plt.plot(y_pred, label='Predicted')
plt.legend()
plt.show()
A reminder of some definitions of measures used in the above code:
The test loss is a measure of how well the model is performing on the test data. It represents the average difference between the actual returns and the predicted returns in the test set. In regression problems , mean squared error (MSE) is a common loss metric. The lower the test loss, the better the model is at making predictions on unseen data.
The test accuracy is a metric commonly used in classification problems, where the goal is to correctly predict the class of each sample. In regression problems like in this case (predicting returns), accuracy is not a typical metric. It might be included in the output, but it might not be meaningful in the context of this problem. For regression tasks, the focus is usually on minimizing the loss rather than achieving a high accuracy.
Results and comments:
The above model yields a test loss that is relatively small. The interpretation depends on the scale of target variable (returns). This loss test can be compared to the range of returns to assess its significance.
Additionally, an accuracy of 0.0 indicates that the accuracy metric is not applicable or not meaningful for this volatile data set.
While the test loss being within the range of returns is a positive indication, it’s important to keep in mind that the range alone might not be sufficient to fully evaluate the model’s performance. Some additional evaluation metrics; use domain-specific metrics; time-series cross-validation; rolling windows, etc, can be used to further assess the performance of this simple model.
```