100% FreeNo Signup Required
Markets
DJIA38,892.45+156.78(+0.40%)
S&P 5005,021.84+23.45(+0.47%)
NASDAQ15,927.90-45.23(-0.28%)
SPY502.18+2.34(+0.47%)
QQQ437.52-1.23(-0.28%)
AAPL189.45+1.89(+1.01%)
MSFT412.91+3.45(+0.84%)
NVDA878.35+12.56(+1.45%)
GOOGL141.28+0.78(+0.56%)
TSLA185.67-4.34(-2.28%)
META485.12+8.92(+1.87%)
ES=F5,025.50+18.25(+0.36%)
NQ=F17,845.75-32.50(-0.18%)
VIX14.23-0.45(-3.06%)
DJIA38,892.45+156.78(+0.40%)
S&P 5005,021.84+23.45(+0.47%)
NASDAQ15,927.90-45.23(-0.28%)
SPY502.18+2.34(+0.47%)
QQQ437.52-1.23(-0.28%)
AAPL189.45+1.89(+1.01%)
MSFT412.91+3.45(+0.84%)
NVDA878.35+12.56(+1.45%)
GOOGL141.28+0.78(+0.56%)
TSLA185.67-4.34(-2.28%)
META485.12+8.92(+1.87%)
ES=F5,025.50+18.25(+0.36%)
NQ=F17,845.75-32.50(-0.18%)
VIX14.23-0.45(-3.06%)
LIVE

'Autoencoders for Anomaly Detection in Trading: Unsupervised Deep Learning'

DJ

Dr. James Chen

Invalid Date

|6 min read

Autoencoders for Anomaly Detection in Trading: Unsupervised Deep Learning

Autoencoders are unsupervised neural networks that compress data into a lower-dimensional representation, then reconstruct the original. In trading, they detect market anomalies and unusual price movements that may signal trading opportunities or risks.

Understanding Autoencoders

Autoencoders consist of:

  1. Encoder: Compresses input to lower-dimensional representation (bottleneck)
  2. Bottleneck: Captures essential information
  3. Decoder: Reconstructs original input from compressed representation

When trained on normal market data, the autoencoder reconstructs similar data well but struggles with anomalies, creating a high reconstruction error that signals anomalies.

Complete Autoencoder Trading System

python
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense, Dropout
from tensorflow.keras.optimizers import Adam
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from sklearn.model_selection import train_test_split
import yfinance as yf
import matplotlib.pyplot as plt
from sklearn.metrics import confusion_matrix, classification_report
import warnings
warnings.filterwarnings('ignore')

Data preparation

def prepare_autoencoder_data(ticker, start_date, end_date): """Prepare data for autoencoder training"""

data = yf.download(ticker, start=start_date, end=end_date)
df = data.copy()

# Create comprehensive features
df['Returns'] = df['Close'].pct_change()
df['Log_Returns'] = np.log(df['Close'] / df['Close'].shift(1))
df['Volatility_10'] = df['Returns'].rolling(10).std()
df['Volatility_20'] = df['Returns'].rolling(20).std()

# Moving averages
for period in [5, 10, 20, 50]:
df[f'SMA_{period}'] = df['Close'].rolling(period).mean()
df[f'EMA_{period}'] = df['Close'].ewm(span=period).mean()
df[f'Distance_SMA_{period}'] = (df['Close'] - df[f'SMA_{period}']) / df[f'SMA_{period}']

# Momentum indicators
df['RSI_14'] = calculate_rsi(df['Close'], 14)
df['MACD'], df['MACD_Signal'], df['MACD_Hist'] = calculate_macd(df['Close'])
df['Momentum_10'] = df['Close'] - df['Close'].shift(10)

# Bollinger Bands
sma = df['Close'].rolling(20).mean()
std = df['Close'].rolling(20).std()
df['BB_Position'] = (df['Close'] - (sma - 2std)) / (4std)
df['BB_Width'] = 4*std / sma

# ATR
df['ATR_14'] = calculate_atr(df, 14)
df['ATR_Ratio'] = df['ATR_14'] / df['Close']

# Volume features
if 'Volume' in df.columns:
df['Volume_SMA'] = df['Volume'].rolling(20).mean()
df['Volume_Ratio'] = df['Volume'] / df['Volume_SMA']
df['Price_Volume'] = df['Returns'] * df['Volume_Ratio']

# High-Low features
df['High_Low_Range'] = (df['High'] - df['Low']) / df['Close']
df['Close_Position'] = (df['Close'] - df['Low']) / (df['High'] - df['Low'])

# Lagged returns
for lag in [1, 2, 3, 5]:
df[f'Return_Lag_{lag}'] = df['Returns'].shift(lag)

return df.dropna()

def calculate_rsi(prices, period=14):
delta = prices.diff()
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0)
avg_gain = gain.rolling(period).mean()
avg_loss = loss.rolling(period).mean()
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi

def calculate_macd(prices, fast=12, slow=26, signal=9):
ema_fast = prices.ewm(span=fast).mean()
ema_slow = prices.ewm(span=slow).mean()
macd = ema_fast - ema_slow
signal_line = macd.ewm(span=signal).mean()
histogram = macd - signal_line
return macd, signal_line, histogram

def calculate_atr(df, period=14):
high_low = df['High'] - df['Low']
high_close = abs(df['High'] - df['Close'].shift())
low_close = abs(df['Low'] - df['Close'].shift())
ranges = pd.concat([high_low, high_close, low_close], axis=1)
true_range = ranges.max(axis=1)
atr = true_range.rolling(period).mean()
return atr

Prepare data

ticker = "AAPL" df = prepare_autoencoder_data(ticker, "2018-01-01", "2024-01-01")

Feature selection

feature_cols = [col for col in df.columns if col not in ['Open', 'High', 'Low', 'Close', 'Adj Close', 'Volume', 'Returns', 'Log_Returns']]

X = df[feature_cols].copy()
X = X.fillna(X.mean())

Normalize features

scaler = StandardScaler() X_scaled = scaler.fit_transform(X)

Split data for training

X_train, X_test = train_test_split(X_scaled, test_size=0.2, shuffle=False)

print(f"Training data shape: {X_train.shape}")
print(f"Test data shape: {X_test.shape}")

Build Autoencoder

input_dim = X_train.shape[1] encoding_dim = max(input_dim // 3, 5) # Bottleneck dimension

Encoder

inputs = Input(shape=(input_dim,)) encoded = Dense(input_dim, activation='relu')(inputs) encoded = Dropout(0.2)(encoded) encoded = Dense(int(encoding_dim * 2), activation='relu')(encoded) encoded = Dropout(0.2)(encoded) encoded = Dense(encoding_dim, activation='relu', name='bottleneck')(encoded)

Decoder

decoded = Dense(int(encoding_dim * 2), activation='relu')(encoded) decoded = Dropout(0.2)(decoded) decoded = Dense(input_dim, activation='relu')(decoded) decoded = Dense(input_dim, activation='linear')(decoded)

Autoencoder model

autoencoder = Model(inputs, decoded) autoencoder.compile(optimizer=Adam(learning_rate=0.001), loss='mse')

print("\nAutoencoder Architecture:")
autoencoder.summary()

Train autoencoder

history = autoencoder.fit( X_train, X_train, epochs=100, batch_size=32, validation_data=(X_test, X_test), shuffle=True, verbose=1 )

Plot training history

plt.figure(figsize=(12, 5)) plt.plot(history.history['loss'], label='Train Loss') plt.plot(history.history['val_loss'], label='Val Loss') plt.xlabel('Epoch') plt.ylabel('Loss') plt.title('Autoencoder Training History') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()

Calculate reconstruction error

train_predictions = autoencoder.predict(X_train) train_error = np.mean(np.square(X_train - train_predictions), axis=1)

test_predictions = autoencoder.predict(X_test)
test_error = np.mean(np.square(X_test - test_predictions), axis=1)

Determine anomaly threshold (95th percentile of training error)

threshold = np.percentile(train_error, 95)

print(f"\nAnomaly Detection Results:")
print(f"Training error - Mean: {train_error.mean():.6f}, Std: {train_error.std():.6f}")
print(f"Test error - Mean: {test_error.mean():.6f}, Std: {test_error.std():.6f}")
print(f"Anomaly threshold (95th percentile): {threshold:.6f}")

Identify anomalies

test_anomalies = (test_error > threshold).astype(int) print(f"Anomalies detected in test set: {test_anomalies.sum()} / {len(test_anomalies)} ({test_anomalies.mean():.2%})")

Visualize reconstruction errors

fig, axes = plt.subplots(2, 1, figsize=(14, 8))

Train errors

axes[0].plot(train_error, label='Reconstruction Error', alpha=0.7) axes[0].axhline(y=threshold, color='r', linestyle='--', label='Anomaly Threshold') axes[0].set_ylabel('Reconstruction Error') axes[0].set_title('Training Set Reconstruction Error') axes[0].legend() axes[0].grid(True, alpha=0.3)

Test errors

axes[1].plot(test_error, label='Reconstruction Error', alpha=0.7) axes[1].axhline(y=threshold, color='r', linestyle='--', label='Anomaly Threshold') anomaly_indices = np.where(test_anomalies)[0] axes[1].scatter(anomaly_indices, test_error[anomaly_indices], color='r', s=50, label='Anomalies') axes[1].set_xlabel('Sample') axes[1].set_ylabel('Reconstruction Error') axes[1].set_title('Test Set Reconstruction Error') axes[1].legend() axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

Variational Autoencoder (VAE) for Probabilistic Modeling

python
# Variational Autoencoder for better anomaly detection
from tensorflow.keras.layers import Lambda
from tensorflow.keras import backend as K

latent_dim = 8

Sampling layer for VAE

def sampling(args): z_mean, z_log_var = args batch = K.shape(z_mean)[0] dim = K.int_shape(z_mean)[1] epsilon = K.random_normal(shape=(batch, dim)) return z_mean + K.exp(0.5 z_log_var) epsilon

Build VAE

inputs = Input(shape=(input_dim,)) x = Dense(input_dim, activation='relu')(inputs) x = Dropout(0.2)(x) x = Dense(64, activation='relu')(x)

z_mean = Dense(latent_dim, name='z_mean')(x)
z_log_var = Dense(latent_dim, name='z_log_var')(x)

z = Lambda(sampling, output_shape=(latent_dim,), name='z')([z_mean, z_log_var])

Decoder

decoder_inputs = Input(shape=(latent_dim,)) x = Dense(64, activation='relu')(decoder_inputs) x = Dropout(0.2)(x) outputs = Dense(input_dim, activation='linear')(x)

decoder_model = Model(decoder_inputs, outputs, name='decoder')

vae_outputs = decoder_model(z)
vae = Model(inputs, vae_outputs, name='vae')

VAE loss

def vae_loss(x, x_decoded_mean): reconstruction_loss = tf.keras.losses.mse(x, x_decoded_mean) reconstruction_loss *= input_dim kl_loss = 1 + z_log_var - K.square(z_mean) - K.exp(z_log_var) kl_loss = K.sum(kl_loss, axis=-1) kl_loss *= -0.5 return K.mean(reconstruction_loss + kl_loss)

vae.compile(optimizer=Adam(learning_rate=0.001), loss=vae_loss)

Train VAE

vae_history = vae.fit( X_train, X_train, epochs=100, batch_size=32, validation_data=(X_test, X_test), verbose=0 )

Calculate VAE reconstruction error

vae_train_pred = vae.predict(X_train) vae_train_error = np.mean(np.square(X_train - vae_train_pred), axis=1)

vae_test_pred = vae.predict(X_test)
vae_test_error = np.mean(np.square(X_test - vae_test_pred), axis=1)

vae_threshold = np.percentile(vae_train_error, 95)

print(f"VAE Anomaly threshold: {vae_threshold:.6f}")
vae_anomalies = (vae_test_error > vae_threshold).astype(int)
print(f"VAE Anomalies detected: {vae_anomalies.sum()}")

Trading Strategy Using Anomalies

python
def create_anomaly_trading_signals(anomalies, prices, window=5):
    """Generate trading signals from detected anomalies"""

signals = []

for i in range(len(anomalies)):
if i < window:
signals.append('HOLD')
continue

# Count anomalies in recent window
recent_anomalies = anomalies[i-window:i].sum()

if recent_anomalies >= 2:
# Potential mean reversion opportunity
signals.append('BUY')
else:
signals.append('HOLD')

return signals

Generate signals

anomaly_signals = create_anomaly_trading_signals(test_anomalies, df['Close'].iloc[-len(test_anomalies):])

Backtest anomaly-based strategy

def backtest_anomaly_strategy(df, signals, initial_capital=10000): """Backtest anomaly detection strategy"""

cash = initial_capital
position = 0
portfolio_values = []
trades = []

prices = df['Close'].values[-len(signals):]

for i in range(len(signals)):
if i >= len(prices):
break

price = prices[i]

if signals[i] == 'BUY' and cash > 0 and i < len(prices) - 5:
# Check if price recovered
future_price = prices[min(i+5, len(prices)-1)]
if future_price > price * 1.02: # Profit opportunity
position = (cash * 0.9) / price
cash = cash * 0.1
trades.append(('BUY', price))

elif signals[i] == 'SELL' and position > 0:
cash = position price 0.9
position = 0
trades.append(('SELL', price))

portfolio_value = cash + position * price
portfolio_values.append(portfolio_value)

return np.array(portfolio_values), trades

portfolio_values, trades = backtest_anomaly_strategy(
df.iloc[-len(test_anomalies):],
anomaly_signals,
initial_capital=10000
)

Performance metrics

returns = np.diff(portfolio_values) / portfolio_values[:-1] total_return = (portfolio_values[-1] - 10000) / 10000 sharpe_ratio = np.mean(returns) / (np.std(returns) + 1e-8) * np.sqrt(252)

print(f"\nAnomaly Detection Strategy Results:")
print(f"Total Return: {total_return:.2%}")
print(f"Sharpe Ratio: {sharpe_ratio:.4f}")
print(f"Total Trades: {len(trades)}")

Visualize

plt.figure(figsize=(14, 6)) plt.plot(portfolio_values, linewidth=2, label='Portfolio Value') plt.axhline(y=10000, color='r', linestyle='--', label='Initial Capital') plt.xlabel('Days') plt.ylabel('Portfolio Value ($)') plt.title('Autoencoder Anomaly Detection Strategy') plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()

Ensemble Anomaly Detection

python
# Combine multiple anomaly detection methods
def ensemble_anomaly_detection(ae_errors, vae_errors, threshold=0.95):
    """Combine multiple autoencoder results"""

ae_threshold = np.percentile(ae_errors, threshold)
vae_threshold = np.percentile(vae_errors, threshold)

ae_anom = ae_errors > ae_threshold
vae_anom = vae_errors > vae_threshold

# Require both methods to agree
ensemble_anom = ae_anom & vae_anom

# Or vote-based (at least 2/3 methods agree)
return ensemble_anom

ensemble_anomalies = ensemble_anomaly_detection(test_error, vae_test_error)
print(f"Ensemble anomalies: {ensemble_anomalies.sum()}")

Conclusion

Autoencoders provide powerful unsupervised anomaly detection capabilities for trading. By learning normal market patterns and identifying deviations, they can signal trading opportunities or warn of potential risks. When combined with proper trading strategies and risk management, autoencoder-based systems offer a novel approach to quantitative trading.

Related Articles