LSTM — Long Short-Term Memory Network
LSTM (Long Short-Term Memory) eliminates the Vanishing Gradient problem of standard RNNs through three specialised gates — Forget, Input, and Output. In algorithmic trading, LSTM retains long-range dependencies in OHLCV data, allowing the model to factor in significant events from several weeks back — such as a support-level breach or central bank decision — when generating a signal today.
How Does LSTM Learn? — Three Control Gates
Real-World LSTM Performance in Financial Markets
Classification Accuracy (H4)
(OHLCV + Indicators)
Length (candles)
for Buy Signal
ExpertNevees Implementation Specification
- OHLCV: Open, High, Low, Close, Volume — last 60 candles
- RSI(14): Relative Strength Index
- ATR(14): Average True Range for volatility
- EMA(20) & EMA(50): Exponential moving averages
- Normalization: MinMaxScaler per feature
- Tensor Shape: [Batch, 60, 11]
- Type: 3-class classification with Softmax
- P(Up): Probability of rise in next 3 candles
- P(Down): Probability of fall in next 3 candles
- P(Neutral): Sideways market or no signal
- Buy Threshold: P(Up) > 0.62 → Buy signal
- Delivery: Via ZeroMQ to EA in MT5
LSTM Gate Equations
# Forget gate — decides what fraction of the previous cell state to discard f_t = sigma( W_f . [h_(t-1), x_t] + b_f ) # Input gate — decides what new information gets stored i_t = sigma( W_i . [h_(t-1), x_t] + b_i ) # Candidate values for updating the cell state C~_t = tanh( W_C . [h_(t-1), x_t] + b_C ) # Cell state update: combine old memory (scaled by forget gate) with new candidate C_t = f_t * C_(t-1) + i_t * C~_t # Output gate — decides what fraction of memory is exposed as output o_t = sigma( W_o . [h_(t-1), x_t] + b_o ) # Final hidden state passed to the next layer or output h_t = o_t * tanh(C_t)
LSTM Implementation with TensorFlow/Keras
import numpy as np import tensorflow as tf from tensorflow.keras import Sequential, Input from tensorflow.keras.layers import LSTM, Dense, Dropout, BatchNormalization from sklearn.preprocessing import MinMaxScaler # ── Stacked Bi-directional LSTM architecture for trend classification ── def build_lstm_model(seq_len=60, n_features=11, n_classes=3): model = Sequential([ Input(shape=(seq_len, n_features)), # First LSTM layer — returns full sequence for the second layer LSTM(128, return_sequences=True), BatchNormalization(), Dropout(0.25), # Second LSTM layer — collapses sequence to a single context vector LSTM(64, return_sequences=False), BatchNormalization(), Dropout(0.20), # Projection head before classifier Dense(32, activation='relu'), # Output: 3-class softmax — class 0=Down, 1=Neutral, 2=Up Dense(n_classes, activation='softmax') ]) model.compile( # learning_rate=1e-3 with cosine decay recommended for long runs optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) return model # ── Feature engineering: OHLCV + 6 technical indicators → [Batch, 60, 11] ── def make_sequences(df, seq_len=60): # Per-feature MinMax scaling applied on training split only (avoid leakage) scaler = MinMaxScaler() scaled = scaler.fit_transform(df[[ 'open', 'high', 'low', 'close', 'volume', # OHLCV 'rsi_14', 'atr_14', 'ema_20', 'ema_50', # momentum / volatility 'bb_upper', 'bb_lower' # Bollinger Band channels ]]) X, y = [], [] for i in range(seq_len, len(scaled)): X.append(scaled[i-seq_len:i]) # sliding window → shape [60, 11] y.append(get_label(df, i)) # forward-label: 0=Down, 1=Neutral, 2=Up return np.array(X), np.array(y), scaler # ── Real-time inference server: receives OHLCV from MT5 EA via ZeroMQ REP ── import zmq def signal_server(model, scaler, threshold=0.62): ctx = zmq.Context() sock = ctx.socket(zmq.REP) sock.bind("tcp://*:5555") while True: data = sock.recv_json() # receive last 60 candles from MT5 EA X = prepare_input(data, scaler) # scale + reshape → [1, 60, 11] probs = model.predict(X, verbose=0)[0] # [p_down, p_neutral, p_up] # Only emit a directional signal when confidence exceeds threshold signal = "BUY" if probs[2] > threshold else \ "SELL" if probs[0] > threshold else "HOLD" sock.send_json({"signal": signal, "confidence": float(probs.max()), "probs": probs.tolist()})
Training, Prediction & Evaluation — LSTM
# ── Training run + Out-of-Sample evaluation ── from sklearn.metrics import classification_report, accuracy_score X, y, scaler = make_sequences(df, seq_len=60) # Time-ordered split — NEVER shuffle time series data (causes lookahead leakage) split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] model = build_lstm_model(seq_len=60, n_features=11, n_classes=3) history = model.fit( X_train, y_train, validation_split=0.15, epochs=50, batch_size=32, callbacks=[tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)], verbose=1 ) # Out-of-sample prediction: probabilities per class → [Down, Neutral, Up] probs = model.predict(X_test) y_pred = probs.argmax(axis=1) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Down', 'Neutral', 'Up'])) # Single latest-window inference (what signal_server() does per tick) latest_probs = model.predict(X_test[-1:])[0] print(f"Latest signal -> Down={latest_probs[0]:.2f} Neutral={latest_probs[1]:.2f} Up={latest_probs[2]:.2f}")
Common LSTM Implementation Mistakes
-
●Scaler Data Leakage: Fitting MinMaxScaler/StandardScaler on the full dataset before the train/test split leaks future statistics into training. Fit the scaler on train only, then transform test.
-
●Raw Prices Instead of Returns: Feeding non-stationary absolute prices instead of log-returns or percent change causes the model to overfit to the price level and break down at new price ranges.
-
●Ignoring Class Imbalance: The Neutral class usually dominates. Without class_weight or resampling, the model learns to always predict Neutral to keep accuracy high — a high but useless accuracy.
-
●Shuffling the Train/Test Split: Using train_test_split with shuffle=True on time series causes lookahead bias — the model learns from future patterns during training.
For Which Use Cases is LSTM Suitable?
-
●Sideways Markets: Accuracy drops below 55% in ranging, directionless markets. LSTM is for trend detection, not trendless conditions.
-
●Retraining Required: The model needs weekly or monthly retraining with fresh data. Markets shift, and stale models degrade.
-
●Overfitting: Without Walk-Forward Validation and proper Feature Engineering, the model overfits to training data. High In-Sample accuracy alone is not sufficient.
-
●Black Swan Events: LSTM cannot predict unexpected events like geopolitical crises or sudden central bank policy shifts.
-
●Not Sufficient Alone: No model builds a profitable system by itself. Combining LSTM with quantitative risk management, broker selection, and validated parameters is essential.
LSTM vs Other Deep Learning Models
| Feature | LSTM | GRU | Transformer (TFT) | Attention Mechanism |
|---|---|---|---|---|
| Training Speed | Moderate | Fast (+20%) | Slow (GPU required) | Moderate |
| Long-term Memory | ✓ Strong | ✓ Good | ✓✓ Very Strong | ✓✓ Very Strong |
| Interpretability | Weak | Weak | Moderate | Excellent (Attention Heatmap) |
| Data Required | +5000 candles | +4000 candles | +10000 candles | +5000 candles |
| Out-of-Sample Accuracy | 65–75% | 63–72% | 68–78% | 67–76% |
| Best For | Price prediction H1–D1 | Daily retraining | Multi-step forecasting | Key candle detection |
Foundational Papers for This Model
-
Long Short-Term Memory Neural Computation, 9(8), 1735–1780View Paper ↗
-
Learning to Forget: Continual Prediction with LSTM Neural Computation, 12(10), 2451–2471View Paper ↗
Ready to Build Your LSTM Trading Bot?
ExpertNevees team trains a custom LSTM model on your target market data, performs Walk-Forward Validation, and connects it to MT5 via ZeroMQ.
Other ExpertNevees AI Models
GRU — Fast & Efficient LSTM Alternative
GRU (Gated Recurrent Unit) is a simplified variant of LSTM that uses two gates instead of three, improving training speed by 15–25% without meaningful accuracy loss — a practical choice for systems requiring daily or weekly retraining.
How Does GRU Learn? — Two Gates Instead of Three
Real GRU vs LSTM Comparison on EUR/USD H1
(5000 H1 candles)
(vs 45min LSTM)
3 Gates in LSTM
Retraining Cycle
ExpertNevees Implementation Specification
- OHLCV: Same Feature Engineering pipeline as LSTM
- Timeframe: M15 to H4 for short-term prediction
- Prediction Horizon: 1 to 5 candles ahead
- Retraining: Every 24 hours with new data
- Type: 3-class classification with Softmax
- Typical Accuracy: 2% lower than LSTM
- Inference Speed: Faster due to fewer parameters
- Delivery: Via ZeroMQ to EA in MT5
GRU Gate Equations
# Update gate — how much of the previous state to keep (merges roles of f_t and i_t) z_t = sigma( W_z . [h_(t-1), x_t] ) # Reset gate — how much of the previous memory to ignore r_t = sigma( W_r . [h_(t-1), x_t] ) # Candidate state, with the reset gate applied to previous memory h~_t = tanh( W . [r_t * h_(t-1), x_t] ) # Final hidden state — linear interpolation between old memory and new candidate h_t = (1 - z_t) * h_(t-1) + z_t * h~_t
GRU Implementation with TensorFlow/Keras
import pandas as pd import tensorflow as tf from tensorflow.keras.layers import GRU, Dense, Dropout, BatchNormalization from tensorflow.keras import Sequential, Input # ── GRU architecture — 2-gate design, 15-25% faster to train than LSTM ── def build_gru_model(seq_len=60, n_features=11, n_classes=3): model = Sequential([ Input(shape=(seq_len, n_features)), # Reset Gate + Update Gate fused — fewer params than LSTM's 3 gates GRU(128, return_sequences=True), BatchNormalization(), Dropout(0.25), # Final GRU layer outputs a single hidden state h_T GRU(64, return_sequences=False), BatchNormalization(), Dropout(0.20), Dense(32, activation='relu'), # 3-class softmax: 0=Down, 1=Neutral, 2=Up Dense(n_classes, activation='softmax') ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=1e-3), loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) return model # ── Adaptive daily retraining — fine-tunes existing weights on fresh data ── def daily_retrain_job(model_path, new_data_path): df = pd.read_csv(new_data_path) # make_sequences(): same feature pipeline as the LSTM model (11 features, window=60) X, y, scaler = make_sequences(df, seq_len=60) model = tf.keras.models.load_model(model_path) # Short fine-tune (5 epochs) avoids catastrophic forgetting model.fit(X, y, epochs=5, batch_size=32, verbose=0) model.save(model_path) # overwrite checkpoint with updated weights
Training, Prediction & Evaluation — GRU
# ── Training run + Out-of-Sample evaluation ── from sklearn.metrics import classification_report, accuracy_score X, y, scaler = make_sequences(df, seq_len=60) split = int(len(X) * 0.8) # time-ordered split — no shuffling for time series X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] model = build_gru_model(seq_len=60, n_features=11, n_classes=3) model.fit( X_train, y_train, validation_split=0.15, epochs=50, batch_size=32, callbacks=[tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)], verbose=1 ) model.save("gru_model.h5") # checkpoint used by daily_retrain_job() y_pred = model.predict(X_test).argmax(axis=1) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Down', 'Neutral', 'Up'])) # Latest-candle inference (single sample, what a live EA would call each tick) latest_probs = model.predict(X_test[-1:])[0] signal = ['SELL', 'HOLD', 'BUY'][latest_probs.argmax()] print(f"Latest signal: {signal} (confidence={latest_probs.max():.2%})")
Common GRU Implementation Mistakes
-
●Skipping Comparison with LSTM: Don't choose GRU purely for speed. On some datasets LSTM is more accurate; always benchmark both on the same data and split.
-
●Scaler Data Leakage: Exactly like LSTM, the scaler must be fit on train data only, never the full dataset.
-
●Missing Early Stopping: Without a proper patience value, the model either stops at a noisy validation-loss point or never stops and overfits.
-
●Irregular Retraining: GRU's speed is not an excuse for irregular retraining; it needs the same scheduling discipline as LSTM.
For Which Use Cases is GRU Suitable?
- ●Weaker Long-Term Dependency: For events with multi-week impact (e.g. older economic news), LSTM's three-gate structure has a theoretical edge.
- ●2–3% Accuracy Drop: In comparative backtests, GRU typically shows 2–3% lower accuracy than LSTM.
- ●Sideways Markets: Like LSTM, GRU accuracy also degrades in ranging markets without a clear trend.
GRU vs LSTM and Other Deep Learning Models
| Feature | GRU | LSTM | Transformer (TFT) |
|---|---|---|---|
| Number of Gates | 2 | 3 | No Gate (Attention) |
| Training Speed | Fast (+20%) | Moderate | Slow (GPU required) |
| Out-of-Sample Accuracy | 63–72% | 65–75% | 68–78% |
| Best For | Daily retraining | Price prediction H1–D1 | Multi-step forecasting |
Foundational Papers for This Model
-
Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation Proceedings of EMNLP 2014View Paper ↗
-
Empirical Evaluation of Gated Recurrent Neural Networks on Sequence Modeling NeurIPS 2014 Deep Learning WorkshopView Paper ↗
Ready to Build Your GRU Trading Bot?
ExpertNevees team builds a custom GRU model with daily Adaptive Retraining and connects it to MT5 via ZeroMQ.
Other ExpertNevees AI Models
MLP — Multi-Layer Perceptron
MLP (Multi-Layer Perceptron) is the simplest and fastest deep learning model for trading signal classification. Its performance depends entirely on input Feature Engineering quality, with sub-0.5ms inference.
MLP Layer Structure — From Features to Signal
20-40 Features
128 neurons + Dropout
64 neurons + Dropout
Softmax: Buy/Sell/Hold
Real-World MLP Performance in Signal Classification
Buy/Sell/Hold Classification
Features
per Prediction
Dropout Rate
ExpertNevees Implementation Specification
- RSI(14), MACD Signal, ATR(14)/Price
- Bollinger %B: Price position within band
- Candle Body/Shadow Ratio
- Volume Z-Score: Relative to 20-candle average
- Session Indicator: Asian/London/NY
- Type: Buy/Sell/Hold classification with Softmax
- Speed: Inference under 0.5ms
- Deployment: ONNX or ZMQ Socket in EA/cBot
MLP Forward Pass & Loss Function
# First hidden layer: linear combination of inputs + ReLU non-linearity a1 = ReLU( W1 . x + b1 ) # Output layer: projection into class space z2 = W2 . a1 + b2 # Map output to a probability distribution over classes (Sell/Hold/Buy) y_hat = softmax(z2) = exp(z2_i) / sum_j exp(z2_j) # Cross-Entropy loss used to optimize weights via Backpropagation Loss = - sum_i y_i * log(y_hat_i)
MLP Implementation with TensorFlow/Keras
import numpy as np import tensorflow as tf import onnxruntime as ort import tf2onnx from tensorflow.keras.layers import Dense, Dropout, BatchNormalization, Input from tensorflow.keras import Sequential, Model # ── MLP for tabular signal classification — no sequence dimension needed ── def build_mlp_model(n_features=30, n_classes=3): model = Sequential([ Input(shape=(n_features,)), # Wide first layer: captures non-linear interactions between indicators Dense(128, activation='relu'), BatchNormalization(), Dropout(0.3), # Narrower second layer: feature compression Dense(64, activation='relu'), BatchNormalization(), Dropout(0.3), # Output: Buy=2, Sell=0, Hold=1 (sparse integer labels) Dense(n_classes, activation='softmax') ]) model.compile( optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'] ) return model # ── Export to ONNX for sub-0.5ms inference inside EA via onnxruntime ── def export_to_onnx(keras_model, output_path="mlp_signal.onnx"): spec = (tf.TensorSpec((None, keras_model.input_shape[-1]), tf.float32, name="features"),) tf2onnx.convert.from_keras(keras_model, input_signature=spec, output_path=output_path) # ── ONNX inference: load once, reuse session for every tick ── def predict_onnx(session, features: np.ndarray) -> str: # features shape: [1, n_features], dtype float32 inp = {session.get_inputs()[0].name: features.astype('float32')} probs = session.run(None, inp)[0][0] # [p_sell, p_hold, p_buy] return ["SELL", "HOLD", "BUY"][probs.argmax()]
Training, Prediction & Evaluation — MLP
# ── Training run + Out-of-Sample evaluation ── from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, accuracy_score # Tabular features (no time dimension) — indicators computed per row X = feature_df[feature_columns].values # shape: [n_samples, 30] y = feature_df['label'].values # 0=Sell, 1=Hold, 2=Buy # shuffle=False keeps the test set as the most recent time window X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, shuffle=False ) model = build_mlp_model(n_features=X.shape[1], n_classes=3) model.fit( X_train, y_train, validation_split=0.15, epochs=40, batch_size=64, callbacks=[tf.keras.callbacks.EarlyStopping(patience=6, restore_best_weights=True)], verbose=1 ) y_pred = model.predict(X_test).argmax(axis=1) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Sell', 'Hold', 'Buy'])) # Export + single-sample ONNX inference (production path used in the EA) export_to_onnx(model, "mlp_signal.onnx") session = ort.InferenceSession("mlp_signal.onnx") print("Live signal:", predict_onnx(session, X_test[-1:]))
Common MLP Implementation Mistakes
-
●Dropping the Time Dimension: MLP has no inherent sense of order; without manually engineered lag features (e.g. returns from 1–10 candles back), the model loses recent-trend information.
-
●Unscaled Features: Fully-connected networks are sensitive to differing feature scales (e.g. RSI 0–100 vs returns around 0.001). Without StandardScaler, large-magnitude features dominate training.
-
●Too Many Layers for the Data Size: A deep, wide network trained on only a few thousand samples typically overfits badly; Dropout and L2 regularization are essential.
-
●Classification Accuracy ≠ Profit: 65% signal accuracy without accounting for spread, commission, and slippage cannot represent real trading outcomes.
For Which Use Cases is MLP Suitable?
- ●No Sequential Understanding: Cannot recognize sequential patterns (e.g., three consecutive bullish candles) without manual features.
- ●Heavy Feature Engineering Dependency: Without quality input, accuracy quickly approaches random-chance levels.
- ●Lower Accuracy Ceiling: Compared to Sequential models, MLP's accuracy ceiling is typically 5–8% lower.
MLP vs Other Classification Models
| Feature | MLP | Random Forest | LSTM |
|---|---|---|---|
| Inference Speed | <0.5ms | ~1ms | ~3-5ms |
| Sequential Understanding | None | None | Strong |
| Interpretability | Weak | Feature Importance | Weak |
| Out-of-Sample Accuracy | 60–70% | 62–70% | 65–75% |
Foundational Papers for This Model
-
Learning Representations by Back-Propagating Errors Nature, 323, 533–536View Paper ↗
Ready to Build Your MLP Trading Bot?
ExpertNevees team designs strong Feature Engineering, trains the MLP model, and connects it to your EA/cBot via ONNX or ZeroMQ.
Other ExpertNevees AI Models
Random Forest — Trend Classification by Ensemble Voting
Random Forest is a combination of 100–500 independent decision trees that, via majority voting, deliver the most stable trend classification model. Key advantage: interpretable Feature Importance.
in Trend
How Does Random Forest Decide? — Majority Voting
Feature Importance — Unique Advantage on XAUUSD
After training on XAUUSD data, you can see which indicator has the most predictive impact — invaluable for simplifying the strategy.
ExpertNevees Implementation Specification
- Technical Features: ATR, RSI, MACD, Volume Z-Score
- n_estimators: 300-500 trees
- max_depth: 8-12 to prevent overfitting
- Walk-Forward: 6-month windows (4 Train / 2 Test)
- Type: Up/Down/Neutral classification
- Feature Importance: Ranking of each indicator's impact
- Typical Accuracy: 62-70% Out-of-Sample
Gini Impurity & Ensemble Voting
# Gini impurity — used to pick the best split at each tree node Gini(t) = 1 - sum_c ( p_c )^2 # p_c = fraction of class-c samples in node t # Impurity reduction from a split — the tree picks the split that maximizes this Delta_Gini = Gini(parent) - ( n_L/n * Gini(L) + n_R/n * Gini(R) ) # Each tree trains on a Bootstrap sample with a random feature subset (m ~ sqrt(p)) D_b = Bootstrap(D), features_b = RandomSubset(features, m) # Final prediction: majority vote across B independent trees y_hat = mode( T_1(x), T_2(x), ..., T_B(x) )
Random Forest Implementation with Scikit-learn
import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import TimeSeriesSplit import matplotlib.pyplot as plt # ── Conservative hyperparameters tuned for noisy financial tabular data ── model = RandomForestClassifier( n_estimators=300, # 300 trees: accuracy plateaus beyond ~400 max_depth=10, # shallow depth prevents memorising tick noise min_samples_leaf=20, # at least 20 samples per leaf — key overfitting guard max_features='sqrt', # random feature subsetting ensures tree diversity class_weight='balanced', # compensates for imbalanced Up/Neutral/Down classes n_jobs=-1, # parallel training across all CPU cores random_state=42 ) # ── Purged Walk-Forward Validation — critical: no future leakage in splits ── tscv = TimeSeriesSplit(n_splits=5, gap=5) # gap=5 avoids look-ahead bias oos_scores = [] for fold, (train_idx, test_idx) in enumerate(tscv.split(X), 1): model.fit(X[train_idx], y[train_idx]) score = model.score(X[test_idx], y[test_idx]) oos_scores.append(score) print(f"Fold {fold} OOS Accuracy: {score:.2%}") print(f"Mean OOS Accuracy: {np.mean(oos_scores):.2%} ± {np.std(oos_scores):.2%}") # ── Feature Importance — reveals which indicators drive prediction ── importances = sorted( zip(feature_names, model.feature_importances_), key=lambda x: x[1], reverse=True ) for name, score in importances[:10]: print(f" {name:<20} {score:.4f}")
Final Evaluation & Prediction — Random Forest
# ── Final model fit + holdout evaluation + single-sample prediction ── from sklearn.metrics import classification_report, accuracy_score # Fit on the full Walk-Forward training window, then evaluate on the final holdout train_idx, test_idx = list(tscv.split(X))[-1] # most recent fold = closest to live data model.fit(X[train_idx], y[train_idx]) y_pred = model.predict(X[test_idx]) print(f"Holdout Accuracy: {accuracy_score(y[test_idx], y_pred):.4f}") print(classification_report(y[test_idx], y_pred, target_names=['Down', 'Neutral', 'Up'])) # Prediction with class probabilities for the newest available candle latest_probs = model.predict_proba(X[-1:])[0] signal = ['Down', 'Neutral', 'Up'][latest_probs.argmax()] print(f"Latest signal: {signal} (confidence={latest_probs.max():.2%})")
Common Random Forest Implementation Mistakes
-
●Wrong Cross-Validation: Using plain KFold instead of TimeSeriesSplit lets future data leak into training folds — reported accuracy will be falsely inflated.
-
●Misreading Feature Importance: Among correlated features (e.g. several similar indicators), importance gets split unevenly between them and can be misleading.
-
●Unbounded Tree Depth: Without limiting max_depth and min_samples_leaf, individual trees can memorize noise in the training data.
-
●Ignoring Transaction Costs in Labeling: If Buy/Sell labels are based purely on return sign (no threshold), the model also signals on tiny, worthless fluctuations.
For Which Use Cases is Random Forest Suitable?
- ●No Sequential Understanding: Treats each sample independently and cannot grasp dependencies between consecutive candles.
- ●Poor Extrapolation: Performance degrades in entirely new market conditions not seen in training data.
- ●Accuracy Ceiling: In noisy financial data, rarely exceeds 70% Out-of-Sample.
Random Forest vs Other Ensemble Models
| Feature | Random Forest | XGBoost | MLP |
|---|---|---|---|
| Training Method | Parallel (Faster) | Sequential | Backpropagation |
| Overfitting Risk | Low | Moderate (needs tuning) | Moderate |
| Interpretability | Feature Importance | SHAP Values | Weak |
| Out-of-Sample Accuracy | 62–70% | 68–74% | 60–70% |
Foundational Papers for This Model
-
Random Forests Machine Learning, 45, 5–32View Paper ↗
Ready to Build Your Random Forest System?
ExpertNevees team trains a Random Forest model with full Walk-Forward Validation and delivers a Feature Importance report.
Other ExpertNevees AI Models
XGBoost — Champion of Financial Classification
XGBoost is an ensemble algorithm based on decision trees where each new tree corrects the errors of the previous one. It has won the most prizes in Kaggle financial prediction competitions.
How Does XGBoost Learn? — Sequential Error Correction
Key Hyperparameters for Financial Data
max_depth
learning_rate
n_estimators
subsample
ExpertNevees Implementation Specification
- Features: 40 technical and statistical features
- Tuning: GridSearch or Optuna
- Min Samples: 1500 training records
- Model Storage: joblib pickle format
- Serving: FastAPI REST endpoint
- Latency: 2-8ms via HTTP
XGBoost Regularized Objective
# Overall objective: prediction loss + model-complexity penalty Obj = sum_i l(y_i, y_hat_i) + sum_k Omega(f_k) # Regularization term — penalizes number of leaves (T) and leaf-weight magnitude (w) Omega(f) = gamma * T + (1/2) * lambda * ||w||^2 # Second-order Taylor approximation of the loss around the current prediction l(y, y_hat + f) ~= l(y, y_hat) + g*f + (1/2)*h*f^2 # g,h = first and second derivatives # Split gain — the criterion used to choose the best split at each boosting step Gain = (1/2) * [ G_L^2/(H_L+lambda) + G_R^2/(H_R+lambda) - (G_L+G_R)^2/(H_L+H_R+lambda) ] - gamma
XGBoost Implementation with Optuna Tuning
import xgboost as xgb import optuna from sklearn.model_selection import TimeSeriesSplit import numpy as np # ── Optuna objective: Walk-Forward CV inside each trial prevents leakage ── def objective(trial): params = { 'max_depth': trial.suggest_int('max_depth', 4, 6), 'learning_rate': trial.suggest_float('learning_rate', 1e-2, 0.1, log=True), 'n_estimators': trial.suggest_int('n_estimators', 500, 2000), 'subsample': trial.suggest_float('subsample', 0.7, 0.9), 'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 0.9), 'reg_lambda': trial.suggest_float('reg_lambda', 1, 10), # L2 regularisation 'objective': 'multi:softprob', 'num_class': 3, 'eval_metric': 'mlogloss', 'n_jobs': -1, } # TimeSeriesSplit ensures no look-ahead bias across folds tscv = TimeSeriesSplit(n_splits=4, gap=5) oos_scores = [] for tr, te in tscv.split(X): m = xgb.XGBClassifier(**params) m.fit(X[tr], y[tr], eval_set=[(X[te], y[te])], early_stopping_rounds=30, verbose=False) oos_scores.append(m.score(X[te], y[te])) return np.mean(oos_scores) study = optuna.create_study(direction='maximize') study.optimize(objective, n_trials=100, show_progress_bar=True) # ── Serve best model via FastAPI — EA calls POST /predict per tick ── from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() best_model = xgb.XGBClassifier(**study.best_params) best_model.fit(X, y) class Features(BaseModel): values: list[float] @app.post("/predict") def predict(req: Features): probs = best_model.predict_proba([req.values])[0] return {"signal": ["SELL", "HOLD", "BUY"][probs.argmax()], "confidence": float(probs.max())}
Tuned Model Evaluation & Prediction — XGBoost
# ── Holdout evaluation of the Optuna-tuned model + single-sample prediction ── from sklearn.metrics import classification_report, accuracy_score from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False) final_model = xgb.XGBClassifier(**study.best_params) final_model.fit(X_train, y_train) y_pred = final_model.predict(X_test) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Sell', 'Hold', 'Buy'])) # Single-sample probability prediction — same call used inside the /predict endpoint probs = final_model.predict_proba(X_test[-1:])[0] print(f"Latest signal: {['SELL','HOLD','BUY'][probs.argmax()]} (confidence={probs.max():.2%})")
Common XGBoost Implementation Mistakes
-
●Leakage in Indicator Calculation: Computing indicators like SMA or RSI over a window that includes the current/future candle (instead of a proper shift) leaks data.
-
●Overfitting to Validation via Optuna: Running hundreds of trials against the same validation set overfits the hyperparameters to it; always keep a final, untouched holdout test set.
-
●No early_stopping_rounds: A fixed n_estimators without early stopping causes either overfitting or unnecessarily long training.
-
●Not Benchmarked Against a Simpler Model: If a plain Random Forest gets close to XGBoost's accuracy, the added complexity and tuning time isn't economically justified.
For Which Use Cases is XGBoost Suitable?
- ●Needs Precise Tuning: Unlike Random Forest, XGBoost is more sensitive to precise hyperparameter tuning.
- ●Higher Overfitting Risk: Sequential training without Early Stopping can overfit quickly.
- ●No Sequential Understanding: Like Random Forest, doesn't natively understand dependencies between consecutive candles.
XGBoost vs Random Forest and LSTM
| Feature | XGBoost | Random Forest | LSTM |
|---|---|---|---|
| Out-of-Sample Accuracy | 68–74% | 62–70% | 65–75% |
| Min Data Required | +1,500 samples | +2,000 samples | +5,000 candles |
| Tuning Required | High | Low | High |
| GPU Required | No | No | Preferred |
Foundational Papers for This Model
-
XGBoost: A Scalable Tree Boosting System Proceedings of the 22nd ACM SIGKDD International Conference on Knowledge Discovery and Data Mining (KDD '16)View Paper ↗
-
Predicting Chinese Stock Market Using XGBoost Multi-Objective Optimization with Optimal Weighting PeerJ Computer Science, 10, e1931View Paper ↗
Ready to Build Your XGBoost System?
ExpertNevees team optimizes the XGBoost model with Optuna Tuning and connects it to your EA/cBot via FastAPI or ZeroMQ.
Other ExpertNevees AI Models
Linear Factor — Extracting Alpha Signals from Markets
A Linear Factor Model is a method (Alpha Factor Engineering) that converts raw market data into return-predictive signals using Ridge/Lasso Regression — the most critical step in any trading ML pipeline.
Alpha Factor Categories in Algorithmic Trading
Return-3M
RSI-Momentum
EV/EBITDA
Dividend Yield
Debt/Equity
Earnings Growth
Realized Vol
Beta
Analyst Revisions
Factor Evaluation with Alphalens
ExpertNevees Implementation Specification
- Market Data: OHLCV, volume, order book
- Alternative Data: Sentiment, satellite images, XBRL
- Combination: Linear or ML-based weighting (XGBoost/RF)
- Factor Score: Final combined score per asset
- Ranking: Assets ranked by Alpha Score
- Integration: Input for classification models or HRP
Multi-Factor Linear Regression
# Fama-French style factor model: excess return as a linear combination of risk factors r_i - r_f = alpha_i + beta_1*MKT + beta_2*SMB + beta_3*HML + epsilon_i # Ridge coefficient estimate with an L2 penalty to control multicollinearity between factors beta_hat = argmin_beta ||y - X*beta||^2 + lambda * ||beta||^2 # Information Coefficient — rank correlation between predicted and actual returns IC = corr( rank(y_hat), rank(y) )
Alpha Factor Implementation with Alphalens
import pandas as pd import numpy as np from sklearn.linear_model import Ridge from sklearn.preprocessing import RobustScaler import alphalens as al # ── Build composite alpha factor from three orthogonal signal families ── def build_factor_score(df: pd.DataFrame) -> pd.DataFrame: # Momentum: 20-day price return — buy recent winners df['momentum'] = df['close'].pct_change(20) # Volatility: normalised ATR — penalise high-noise assets df['volatility'] = df['atr_14'] / df['close'] # Value: inverse P/E → lower P/E is more attractive df['value'] = -df['pe_ratio'] # Cross-sectionally scale each factor (RobustScaler handles outliers) scaler = RobustScaler() factors = scaler.fit_transform(df[['momentum', 'volatility', 'value']]) # Ridge regression learns optimal factor weights from realised forward returns model = Ridge(alpha=1.0) model.fit(factors, df['forward_return']) df['factor_score'] = model.predict(factors) return df # ── Evaluate with Alphalens — IC > 0.05 signals a statistically useful factor ── factor_data = al.utils.get_clean_factor_and_forward_returns( factor=df['factor_score'], prices=price_data, periods=(1, 5, 10) # 1-day, 5-day, 10-day forward return horizons ) ic = al.performance.factor_information_coefficient(factor_data) print(f"Mean IC: {ic.mean():.3f} IR: {(ic.mean()/ic.std()):.2f}")
Evaluation & Asset Ranking Prediction — Factor Model
# ── Train/test split + regression evaluation + latest cross-sectional prediction ── from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score X = factors # scaled [momentum, volatility, value] matrix built above y = df['forward_return'].values X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=False) model = Ridge(alpha=1.0) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(f"Test MSE: {mean_squared_error(y_test, y_pred):.6f}") print(f"Test R^2: {r2_score(y_test, y_pred):.4f}") # Latest cross-section: rank assets by predicted forward return (long top decile) latest_scores = model.predict(X[-len(symbols):]) ranked = sorted(zip(symbols, latest_scores), key=lambda x: x[1], reverse=True) print("Top ranked assets:", ranked[:5])
Common Factor Model Implementation Mistakes
-
●Look-Ahead Bias in Fundamental Data: Using EPS or financials keyed to the report date (not the actual public release date) lets the model use information that wasn't available at that time.
-
●Multicollinearity Between Factors: Correlated factors (e.g. short- and long-term momentum) destabilize Ridge/regression coefficients; check VIF before modeling.
-
●Assuming Static Factor Relationships: The relationship between a factor and forward returns shifts across time and market regimes; the model should be refit on a rolling basis.
-
●Confusing IC with Classification Accuracy: A low Information Coefficient like 0.03–0.08 is normal for a factor model; don't expect classification-style accuracy from it.
For Which Use Cases is Alpha Factor Engineering Suitable?
- ●Factors Decay: As a factor becomes widely known, its predictive power (Alpha Decay) typically diminishes.
- ●Limited Linear Dependency: Linear Factor models don't capture complex non-linear relationships as well as tree-based or deep learning models.
- ●Data Snooping: Excessive testing of different factors on the same historical data increases the risk of spurious patterns.
Linear Factor vs More Advanced Combination Methods
| Feature | Linear Factor | XGBoost | Autoencoder |
|---|---|---|---|
| Relationship Type | Linear | Non-linear | Non-linear Latent |
| Interpretability | Very High | Moderate (SHAP) | Low |
| Computation Speed | Very Fast | Fast | Moderate |
Foundational Papers for This Model
-
Common Risk Factors in the Returns on Stocks and Bonds Journal of Financial Economics, 33(1), 3–56View Paper ↗
Ready to Build Your Factor-Based Strategy?
ExpertNevees team extracts alpha factors, evaluates them with Alphalens, and prepares them as input for your ML strategies.
Other ExpertNevees AI Models
Transformer — Most Advanced Financial Prediction Model
Developed by Google DeepMind, the Temporal Fusion Transformer (TFT) combines Attention Mechanism, LSTM, and Variable Selection Network. It views all time points in parallel and uses Self-Attention to identify the most critical moments.
Why Transformer Outperforms LSTM
Each column represents a time step (candle); color intensity shows Self-Attention weight — the Transformer simultaneously attends to an FOMC meeting 3 weeks ago (bright column) and today's candle.
Real Cost of Running Transformer
GPU VRAM
for Proper Training
on GPU
Horizon (candles)
ExpertNevees Implementation Specification
- Price + Volume: Multi-timeframe OHLCV
- Economic News: Economic calendar + Sentiment Score
- Static Variables: Symbol, sector, asset type
- Multi-step Forecast: 5, 10, or 20 candles ahead
- Regime Detection: Trending/Ranging/High Vol
- Confidence Interval: Quantile Forecasts (P10/P50/P90)
Self-Attention & Multi-Head Formula
# Scaled Dot-Product Attention Attention(Q, K, V) = softmax( Q * K^T / sqrt(d_k) ) * V # Combine multiple parallel attention heads to learn different relationships at once MultiHead(Q,K,V) = Concat(head_1, ..., head_h) * W_O , head_i = Attention(Q*W_i^Q, K*W_i^K, V*W_i^V) # Pinball (Quantile) loss for probabilistic P10/P50/P90 forecasting L_q(y, y_hat) = max( q*(y - y_hat), (q-1)*(y - y_hat) )
TFT Implementation with PyTorch Forecasting
import pytorch_lightning as pl from pytorch_forecasting import TemporalFusionTransformer, TimeSeriesDataSet from pytorch_forecasting.metrics import QuantileLoss # ── Multi-source TimeSeriesDataSet ── # known_reals: features available at prediction time (e.g. calendar, macro) # unknown_reals: features only observed up to the encoder window training = TimeSeriesDataSet( df, time_idx="time_idx", target="close", group_ids=["symbol"], # one group per trading instrument max_encoder_length=60, # 60-candle lookback window max_prediction_length=10, # forecast 10 steps ahead simultaneously time_varying_known_reals=[ # available during the decoder phase "rsi", "atr", "sentiment_score", "hour_of_day", "day_of_week" # session / seasonality signals ], time_varying_unknown_reals=[ # observed only in the encoder "close", "volume", "spread" ], static_categoricals=["asset_class"], # FX, Crypto, Equity — handled by VSN target_normalizer="softplus", # ensures non-negative price targets ) # ── Build TFT with Quantile Loss for probabilistic forecasting ── # quantiles=[0.1, 0.5, 0.9] → P10/P50/P90 confidence bands per candle tft = TemporalFusionTransformer.from_dataset( training, hidden_size=64, attention_head_size=4, # Multi-Head Self-Attention heads dropout=0.2, hidden_continuous_size=16, loss=QuantileLoss(quantiles=[0.1, 0.5, 0.9]), log_interval=10, learning_rate=3e-3, ) # ── Train with early stopping to prevent overfitting ── trainer = pl.Trainer(max_epochs=30, gradient_clip_val=0.1, accelerator="gpu", devices=1) trainer.fit(tft, train_dataloaders=train_loader, val_dataloaders=val_loader) # ── Probabilistic prediction: returns P10/P50/P90 bands for 10 candles ── raw_predictions = tft.predict(val_loader, mode="raw", return_x=True) p10, p50, p90 = raw_predictions.output.prediction.unbind(dim=-1)
Forecast Accuracy Evaluation — Transformer
# ── Evaluate probabilistic forecasts + single-series prediction ── from pytorch_forecasting.metrics import QuantileLoss # Quantile loss on the validation set — lower is better; compares P10/P50/P90 to actuals val_loss = trainer.validate(tft, dataloaders=val_loader)[0] print(f"Validation Quantile Loss: {val_loss['val_loss']:.4f}") # Point forecast accuracy using the P50 (median) quantile as the prediction actuals = torch.cat([y[0] for x, y in iter(val_loader)]) mae = (p50 - actuals).abs().mean() print(f"MAE (P50 vs actual close): {mae:.5f}") # Forecast the next 10 candles for a single symbol from the latest known window new_prediction = tft.predict(new_raw_data, mode="quantiles", trainer_kwargs=dict(accelerator="cpu")) p10, p50, p90 = new_prediction[0].unbind(dim=-1) print(f"Next-candle forecast: P10={p10[0]:.5f} P50={p50[0]:.5f} P90={p90[0]:.5f}")
Common Transformer Implementation Mistakes
-
●Insufficient Data: Due to its high parameter count, a Transformer overfits badly with fewer than several thousand sequences; use LSTM or GRU for smaller datasets.
-
●Mixing Up Static vs. Time-Varying Covariates: If a static feature (like the symbol) is mistakenly defined as time-varying, the model learns the wrong pattern.
-
●Interpreting Attention Weights as Causal: High attention weights don't necessarily mean a causal relationship; they only show where the model focused, not why.
-
●Skipping Quantile Calibration Checks: If the P10–P90 band doesn't actually cover roughly 80% of real outcomes, the model's uncertainty estimates are not calibrated.
For Which Use Cases is Transformer Suitable?
- ●Impractical Without GPU: Training and inference on CPU is impractical for real projects.
- ●Highest Overfitting Risk: The most powerful model, but without rigorous Cross-Validation, In-Sample results will be entirely misleading.
- ●High Data Requirement: With fewer than 10,000 candles, the model struggles to learn meaningful patterns.
Transformer vs Other Deep Learning Models
| Feature | Transformer | LSTM | Attention |
|---|---|---|---|
| Time Processing | Parallel | Sequential | Depends on combo |
| Hardware Requirement | GPU 8GB+ required | GPU optional | Depends on base model |
| Multi-step Forecast | Native | Needs config | Needs config |
| Data Required | +10,000 candles | +5,000 candles | +5,000 candles |
Foundational Papers for This Model
-
Attention Is All You Need Advances in Neural Information Processing Systems 30 (NeurIPS 2017)View Paper ↗
-
Temporal Fusion Transformers for Interpretable Multi-horizon Time Series Forecasting International Journal of Forecasting, 37(4), 1748–1764View Paper ↗
Ready to Build Your Transformer Trading Bot?
ExpertNevees team implements the TFT model with rigorous Cross-Validation and multi-source data, connecting it to MT5 via ZeroMQ.
Other ExpertNevees AI Models
Attention — Focus-Driven Trading Models
Attention Mechanism enables the model to focus precisely on the most significant candles rather than weighting all time steps equally — much like a trader paying special attention to key candles. In one published study (Zhang et al., 2019) across the Russell 2000, DJIA and Nasdaq indices, adding attention to LSTM reduced prediction error (MAPE) by roughly 22–34% versus plain LSTM. The Attention Weight Heatmap reveals exactly why and where the model focused.
Types of Attention in Algorithmic Trading
Attention Weight Visualization — CPI Example
When the CPI rate is announced (bright column), Cross-Attention assigns high weight to that time step. The model learns gold's reaction to CPI differs from NFP. This Heat Map is valuable for verifying meaningful learning.
ExpertNevees Implementation Specification
- LSTM/GRU Output: Hidden states per time step
- Second Source (Cross): Sentiment or economic calendar
- Number of Heads: 4-8 parallel heads
- Context Vector: Weighted combination of hidden states
- Attention Weights: Visualizable as Heat Map
- Final Signal: From Context Vector to Softmax
Attention Formula over LSTM Outputs
# Raw attention score for each time step relative to the context vector e_t = v^T * tanh( W_h * h_t + b ) # Normalize scores into a probability distribution over all time steps (60 candles) alpha_t = exp(e_t) / sum_j exp(e_j) # Final context vector: weighted average of all hidden states based on importance context = sum_t alpha_t * h_t
Temporal Attention Implementation on LSTM
import tensorflow as tf from tensorflow.keras.layers import LSTM, Dense, Dropout, Layer, Input from tensorflow.keras import Model # ── Custom Temporal Attention layer — additive (Bahdanau-style) ── # Computes a scalar alignment score for each time step, then softmax-normalises class TemporalAttention(Layer): def __init__(self, units=64, **kwargs): super().__init__(**kwargs) self.W = Dense(units) # projects LSTM hidden states self.V = Dense(1) # collapses to a single scalar score def call(self, lstm_outputs, return_weights=False): # lstm_outputs shape: [batch, T, units] scores = self.V(tf.nn.tanh(self.W(lstm_outputs))) # [batch, T, 1] weights = tf.nn.softmax(scores, axis=1) # attention distribution over time # Weighted sum: high-weight time steps contribute more to prediction context = tf.reduce_sum(weights * lstm_outputs, axis=1) # [batch, units] if return_weights: return context, weights # weights → Attention Heatmap visualisation return context # ── Build LSTM + Temporal Attention model using Functional API ── inp = Input(shape=(60, 11)) # [Batch, 60 candles, 11 features] x = LSTM(128, return_sequences=True)(inp) # full sequence → all hidden states h_1 … h_60 x = Dropout(0.25)(x) context = TemporalAttention(64)(x) # squeeze sequence → context vector x = Dropout(0.2)(context) x = Dense(32, activation='relu')(x) out = Dense(3, activation='softmax')(x) # Up / Neutral / Down model = Model(inp, out) model.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
Training, Evaluation & Attention-Weight Analysis
# ── Training run + evaluation + attention-weight inspection ── from sklearn.metrics import classification_report, accuracy_score X, y, scaler = make_sequences(df, seq_len=60) split = int(len(X) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] model.fit( X_train, y_train, validation_split=0.15, epochs=50, batch_size=32, callbacks=[tf.keras.callbacks.EarlyStopping(patience=8, restore_best_weights=True)], verbose=1 ) y_pred = model.predict(X_test).argmax(axis=1) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Down', 'Neutral', 'Up'])) # Extract per-candle attention weights for the latest window — powers the Heatmap UI attention_layer = TemporalAttention(64) context, weights = attention_layer(model.layers[1](X_test[-1:]), return_weights=True) top_candles = weights[0, :, 0].numpy().argsort()[-5:][::-1] print("Most influential candles (index from window start):", top_candles)
Common Attention Interpretation Mistakes
-
●Treating Attention Weights as Causal Proof: Candles that receive high weight are not necessarily the cause of the price move; the heatmap is an interpretation tool, not proof.
-
●Not Benchmarked Against Plain LSTM: Adding attention doesn't always improve accuracy. Without a direct A/B test against a plain LSTM, the extra complexity may be wasted.
-
●Drawing Conclusions from a Single Sample: The attention pattern on one prediction can be noise; valid conclusions require averaging over many samples.
-
●Prediction Lag and Weakness in Extreme Conditions: Even the AT-LSTM authors (Zhang et al., 2019) reported that despite high accuracy, their model shows a time lag in prediction and is not sufficiently sensitive during extreme/highly volatile market conditions — a good reminder that even peer-reviewed papers honestly report their own limitations.
For Which Use Cases is Attention Suitable?
- ●O(n²) Memory Complexity: Every step accesses every other step directly, so memory consumption grows quickly with longer sequences.
- ●Not Sufficient Alone: Attention is typically used as an add-on to LSTM/GRU or within a Transformer, not standalone.
- ●Interpretation Can Be Misleading: High Attention weight doesn't always mean true causal relationship; requires careful review.
Attention vs Plain LSTM
| Feature | LSTM + Attention | Plain LSTM | Transformer |
|---|---|---|---|
| Long-term Dependency | Very Strong | Moderate (Gate) | Very Strong |
| Interpretability | Excellent (Heatmap) | Weak | Moderate |
| Out-of-Sample Accuracy | 67–76% | 65–75% | 68–78% |
Foundational Papers for This Model
-
Neural Machine Translation by Jointly Learning to Align and Translate International Conference on Learning Representations (ICLR 2015)View Paper ↗
-
AT-LSTM: An Attention-based LSTM Model for Financial Time Series Prediction IOP Conference Series: Materials Science and Engineering, 569, 052037View Paper ↗
-
Time Series Prediction Based on LSTM-Attention-LSTM Model IEEE Access, 11, 48322–48331View Paper ↗
Ready to Build Your Attention-Enhanced Model?
ExpertNevees team implements an Attention layer on your LSTM/GRU and delivers a visual Heatmap report for interpretability.
Other ExpertNevees AI Models
CNN — Visual Pattern Recognition in Charts
A CNN (Convolutional Neural Network) is a deep learning model that, by converting OHLCV data into a visual representation, learns complex patterns that can't be manually defined — from Head&Shoulders to 50+ candlestick patterns. Per a study published in PeerJ Computer Science (Mersal et al., 2025), a CNN trained on 61 Japanese candlestick patterns using real Forex data achieved 99.3% accuracy in binary trend-direction classification.
How is OHLCV Converted to an Image?
Each column is one candle; Body, upper shadow, and lower shadow display in different colors. 20-candle blocks become 20×5 pixel images (with Volume and OI).
50+ Candlestick and Chart Patterns
Double Top/Bottom
Pennant
Doji
Patterns
ExpertNevees Implementation Specification
- Candlestick Image: 20-candle block with OHLC + Volume
- Labeling: Via classic pattern detection algorithm or manual
- Augmentation: Rotation and slight noise for better generalization
- Type: Multi-class image classification
- Output: Pattern name + confidence probability
- Application: Confirming classic price-action EA signals
Convolution & Pooling Operation
# 2D convolution operation over the candlestick image with filter K S(i,j) = sum_m sum_n I(i+m, j+n) * K(m,n) # Non-linearity applied after convolution A(i,j) = ReLU( S(i,j) + b ) # Max pooling to reduce dimensionality and add robustness to small shifts P(i,j) = max( A(2i:2i+1, 2j:2j+1) )
CNN Implementation with TensorFlow
import numpy as np from tensorflow.keras.layers import (Conv2D, MaxPooling2D, GlobalAveragePooling2D, Dense, Dropout, BatchNormalization, Input) from tensorflow.keras import Sequential from tensorflow.keras.preprocessing.image import ImageDataGenerator # ── Convert a 20-candle OHLCV window into a 2D candlestick image ── # Output shape: [20, 64, 3] → time steps × price-channels × RGB def ohlcv_to_image(df_slice, img_h=64): n = len(df_slice) img = np.zeros((n, img_h, 3), dtype=np.float32) lo, hi = df_slice['low'].min(), df_slice['high'].max() def norm(v): return int((v - lo) / (hi - lo + 1e-9) * (img_h - 1)) for i, (_, row) in enumerate(df_slice.iterrows()): # Candle body: green for bullish, red for bearish color = [0, 1, 0] if row.close >= row.open else [1, 0, 0] y0, y1 = norm(min(row.open, row.close)), norm(max(row.open, row.close)) img[i, y0:y1+1, :] = color # Wicks: grey channel img[i, norm(row.low):norm(row.high)+1, :] = [0.5, 0.5, 0.5] return img # ── CNN architecture for chart pattern classification (50+ pattern classes) ── model = Sequential([ Input(shape=(20, 64, 3)), # Block 1: low-level edge and line detection Conv2D(32, (3, 3), activation='relu', padding='same'), BatchNormalization(), MaxPooling2D((2, 2)), # Block 2: composite pattern detection (Head&Shoulders, Double Top, etc.) Conv2D(64, (3, 3), activation='relu', padding='same'), BatchNormalization(), MaxPooling2D((2, 2)), Conv2D(128, (3, 3), activation='relu', padding='same'), BatchNormalization(), # Global pooling collapses spatial dims → fixed-length descriptor GlobalAveragePooling2D(), Dense(128, activation='relu'), Dropout(0.4), # 50+ pattern classes — use categorical_crossentropy with one-hot labels Dense(50, activation='softmax') ]) model.compile(optimizer='adam', loss='categorical_crossentropy', metrics=['accuracy'])
Training & Pattern Classification Evaluation — CNN
# ── Image dataset generation + training + evaluation ── from sklearn.metrics import classification_report, accuracy_score import numpy as np # Build image dataset from labelled 20-candle windows X_img = np.array([ohlcv_to_image(df.iloc[i:i+20]) for i in start_indices]) y_img = np.array(pattern_labels) # integer-encoded, one of 50 pattern classes split = int(len(X_img) * 0.8) X_train, X_test = X_img[:split], X_img[split:] y_train, y_test = y_img[:split], y_img[split:] datagen = ImageDataGenerator(horizontal_flip=False, brightness_range=[0.9, 1.1]) model.fit( datagen.flow(X_train, tf.keras.utils.to_categorical(y_train, 50), batch_size=32), validation_data=(X_test, tf.keras.utils.to_categorical(y_test, 50)), epochs=40, callbacks=[tf.keras.callbacks.EarlyStopping(patience=6, restore_best_weights=True)] ) y_pred = model.predict(X_test).argmax(axis=1) print(f"Test Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred)) # Classify the most recent 20-candle chart image latest_img = ohlcv_to_image(df.tail(20))[np.newaxis, ...] pred_class = model.predict(latest_img).argmax() print(f"Detected pattern class: {pattern_names[pred_class]}")
Common CNN Implementation Mistakes
-
●Overlapping Train/Test Windows: If adjacent 20-candle windows are split between train and test, heavy overlap leaks data and inflates reported accuracy.
-
●Pattern Class Imbalance: Among 50+ pattern classes, some are far rarer than others. Without class weighting or oversampling, the model ignores rare patterns.
-
●Sensitivity to the Image-Encoding Method: Changing the candle count or the OHLCV-to-image coloring scheme significantly shifts label distribution and accuracy; this choice must be documented and kept consistent.
-
●Pattern Detection ≠ Profitable Signal: Correctly classifying a classic pattern (e.g. Head & Shoulders) doesn't guarantee the subsequent price move will follow that pattern's classical theory.
For Which Use Cases is CNN Suitable?
- ●Needs Precise Labeling: The quality of training labels (correct patterns) determines final accuracy.
- ●Pattern ≠ Profitability: Correctly identifying a visual pattern doesn't mean a profitable trading signal; needs separate risk management.
- ●Sensitive to Scaling: Price-to-image normalization must be done carefully so patterns are comparable.
CNN vs Classic Pattern Detection
| Feature | CNN | Classic Manual Rules |
|---|---|---|
| Generalization to New Patterns | High | None |
| Training Data Needed | +3,000 samples | None |
| Rule Transparency | Opaque (Black Box) | Fully Transparent |
Foundational Papers for This Model
-
Gradient-Based Learning Applied to Document Recognition Proceedings of the IEEE, 86(11), 2278–2324View Paper ↗
-
Enhancing Market Trend Prediction Using Convolutional Neural Networks on Japanese Candlestick Patterns PeerJ Computer Science, 11, e2719View Paper ↗
Ready to Build Your CNN Model?
ExpertNevees team builds the OHLCV-to-image pipeline, trains the CNN, and connects it as a confirmation filter to your EA.
Other ExpertNevees AI Models
DDQN — Reinforcement Learning Trading Agent
Double Deep Q-Network is an approach where a trading agent learns the optimal strategy through direct interaction with the market environment — without manual data labeling.
Trading Agent Learning Loop — State, Action, Reward
Reward feeds back to the Agent so it can better evaluate the next State in the following cycle.
The Overestimation Problem in Standard DQN
Online + Target
Overestimation Bias
for Stable Training
for Faster Training
Bellman Equation & Double Q-Update
# Standard DQN target — suffers from optimistic overestimation bias in value estimates y_DQN = r + gamma * max_a Q(s', a; theta_target) # Double DQN target — action selected by the online network, evaluated by the target network (reduces bias) y_DDQN = r + gamma * Q( s', argmax_a Q(s', a; theta_online); theta_target ) # Loss function — mean squared Temporal Difference (TD) error L(theta) = E[ ( y_DDQN - Q(s, a; theta_online) )^2 ]
DDQN Implementation with OpenAI Gym
import gym import numpy as np import tensorflow as tf from collections import deque import random # ── Custom Gym environment — simulates order execution on historical OHLCV ── class TradingEnv(gym.Env): def __init__(self, df, window=60): super().__init__() self.df = df self.window = window self.action_space = gym.spaces.Discrete(3) # 0=Sell, 1=Hold, 2=Buy # State: last `window` candles × n_features self.observation_space = gym.spaces.Box( low=-np.inf, high=np.inf, shape=(window, df.shape[1]), dtype=np.float32) def step(self, action): prev_sharpe = self._calc_sharpe() self._execute_order(action) self.current_step += 1 # Reward: incremental Sharpe Ratio — penalises both loss and excess risk reward = self._calc_sharpe() - prev_sharpe done = self.current_step >= len(self.df) - 1 return self._get_obs(), reward, done, {} # ── Experience Replay Buffer (capacity: 50 k transitions) ── memory = deque(maxlen=50_000) # ── DDQN training step: decouples action selection from value estimation ── def train_step(online_net, target_net, gamma=0.99, batch_size=64): if len(memory) < batch_size: return batch = random.sample(memory, batch_size) states, actions, rewards, next_states, dones = map(np.array, zip(*batch)) # Double DQN: online net picks the best next action, target net evaluates it best_actions = online_net(next_states).numpy().argmax(axis=1) next_q = target_net(next_states).numpy() targets = rewards + gamma * next_q[np.arange(batch_size), best_actions] * (~dones) # Bellman update — only update Q-values for taken actions with tf.GradientTape() as tape: q_pred = online_net(states) q_taken = tf.reduce_sum(q_pred * tf.one_hot(actions, 3), axis=1) loss = tf.reduce_mean(tf.square(targets - q_taken)) # MSE (Huber also works) online_net.optimizer.minimize(loss, online_net.trainable_weights, tape=tape) # ── Sync target network every 500 steps to stabilise training ── if step % 500 == 0: target_net.set_weights(online_net.get_weights())
Full Agent Training & Learned-Policy Evaluation
# ── Full training loop over episodes + trained-policy inference ── online_net = build_q_network(input_shape=(60, n_features), n_actions=3) target_net = build_q_network(input_shape=(60, n_features), n_actions=3) target_net.set_weights(online_net.get_weights()) env = TradingEnv(train_df, window=60) epsilon, epsilon_min, epsilon_decay = 1.0, 0.05, 0.995 for episode in range(500): state, done, ep_reward = env.reset(), False, 0 while not done: # Epsilon-greedy exploration — decays toward pure exploitation over training if np.random.rand() < epsilon: action = env.action_space.sample() else: action = int(online_net(state[np.newaxis]).numpy().argmax()) next_state, reward, done, _ = env.step(action) memory.append((state, action, reward, next_state, done)) train_step(online_net, target_net) state, ep_reward = next_state, ep_reward + reward epsilon = max(epsilon_min, epsilon * epsilon_decay) if episode % 500 == 0: target_net.set_weights(online_net.get_weights()) if episode % 50 == 0: print(f"Episode {episode} reward={ep_reward:.3f} epsilon={epsilon:.3f}") # ── Evaluate the trained policy on unseen data (epsilon=0 → fully greedy) ── test_env = TradingEnv(test_df, window=60) state, done, total_reward = test_env.reset(), False, 0 while not done: action = int(online_net(state[np.newaxis]).numpy().argmax()) state, reward, done, _ = test_env.step(action) total_reward += reward print(f"Out-of-sample cumulative reward (Sharpe-based): {total_reward:.3f}")
Common DDQN Implementation Mistakes
-
●Poorly Designed Reward Function: The single biggest cause of DDQN failure in practice is a reward-shaping bug, not the algorithm choice; the reward must genuinely reflect trading success (e.g. risk-adjusted PnL). One real solution to this problem is learning the reward function itself from human demonstrations (instead of hand-designing it) — an approach implemented by Zhou et al. (2024) in their R-DDQN model.
-
●Simulation Not Matching the Real Broker: If the simulated spread, commission, and slippage don't match real broker conditions, the trained policy behaves differently on a live account.
-
●Overfitting to a Single Market Regime: Training only on one historical window (e.g. only the 2020–2021 bull market) makes the agent perform poorly in other regimes.
-
●No Periodic Retraining: Markets are non-stationary; a policy that worked well last year may no longer be optimal without periodic retraining.
For Which Use Cases is DDQN Suitable?
- ●Very High Data Requirement: RL needs at least 50,000 environment steps, meaning years of historical financial data.
- ●Sensitive to Regime Changes: For frequently regime-changing markets, combining RL with Market Regime Detection is essential.
- ●Reward Engineering Difficulty: Poor Reward Function design can push the Agent toward unexpected and risky behaviors.
DDQN vs Supervised Models
| Feature | DDQN (RL) | LSTM (Supervised) |
|---|---|---|
| Labeling Required | No | Yes (Up/Down/Neutral) |
| Optimization Target | Direct Sharpe Ratio | Classification Accuracy |
| Data Requirement | +50,000 steps | +5,000 candles |
| Training Complexity | Very High | Moderate |
Foundational Papers for This Model
-
Human-Level Control through Deep Reinforcement Learning Nature, 518, 529–533View Paper ↗
-
Deep Reinforcement Learning with Double Q-Learning Proceedings of the 30th AAAI Conference on Artificial IntelligenceView Paper ↗
-
R-DDQN: Optimizing Algorithmic Trading Strategies Using a Reward Network in a Double DQN Mathematics, 12(11), 1621View Paper ↗
Ready to Build Your Trading Agent?
ExpertNevees team designs and trains a custom Gym environment, Sharpe-Ratio-based Reward Function, and DDQN Agent.
Other ExpertNevees AI Models
TimeGAN — Synthetic Financial Time-Series Data
TimeGAN is a Generative Adversarial Network that generates realistic financial time-series data — for cases with insufficient historical data, crisis scenario simulation, and Data Augmentation for rare events.
Four Core Components of TimeGAN Architecture
Synthetic Data Quality Evaluation
PCA / t-SNE
Real/Synthetic Classifier Acc.
Train-Synthetic Test-Real
Mode Collapse
ExpertNevees Implementation Specification
- Real Time Series: Limited historical OHLCV
- Sequence Length: Fixed windows (e.g. 60 candles)
- Synthetic Time Series: With similar statistical properties to real data
- Application: Augmenting training dataset for LSTM/CNN/XGBoost
TimeGAN's Three Loss Functions
# Reconstruction loss — the autoencoder must be able to reconstruct the original data from latent space L_R = E[ || X - Decoder(Encoder(X)) ||^2 ] # Supervised loss — the next-step prediction in latent space must match the real data dynamics L_S = E[ || h_t - g(h_(t-1), z_t) ||^2 ] # Adversarial loss — the discriminator must tell real from synthetic, the generator must fool it L_U = E[ log D(H) ] + E[ log(1 - D(H_hat)) ]
TimeGAN Implementation with TensorFlow
import numpy as np from ydata_synthetic.synthesizers.timeseries import TimeGAN # ── TimeGAN hyperparameters — tune noise_dim and hidden_dim for your dataset ── gan_args = { 'batch_size': 128, 'lr': 5e-4, # lower LR stabilises adversarial training 'noise_dim': 32, # latent space dimension for the generator 'seq_len': 60, # must match encoder window of downstream model 'n_features': 5, # OHLCV — extend to 11 to include indicators } # gamma: weight of supervised (reconstruction) loss vs adversarial loss synth = TimeGAN(model_parameters=gan_args, hidden_dim=24, seq_len=60, n_seq=5, gamma=1) # Increase train_steps to 20 k–50 k if mode collapse appears synth.train(real_data, train_steps=10_000) # ── Sample synthetic sequences for downstream data augmentation ── synthetic_data = synth.sample(n_samples=1_000) # shape: [1000, 60, 5] # ── TSTR evaluation: Train on Synthetic, Test on Real ── # A high TSTR accuracy proves the synthetic data captures real market dynamics model_synth = train_lstm(synthetic_data) tstr_acc, _ = model_synth.evaluate(real_test_X, real_test_y, verbose=0) print(f"TSTR Accuracy (synthetic→real): {tstr_acc:.2%}")
Synthetic Data Quality Evaluation — TimeGAN
# ── Discriminative Score — the standard quality metric for synthetic time series ── # Train a classifier to distinguish real vs synthetic sequences. # Score near 0.5 = indistinguishable (ideal); near 1.0 = poor-quality synthetic data. from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score real_flat = real_data.reshape(len(real_data), -1) synth_flat = synthetic_data.reshape(len(synthetic_data), -1) X_disc = np.vstack([real_flat, synth_flat]) y_disc = np.concatenate([np.ones(len(real_flat)), np.zeros(len(synth_flat))]) X_train, X_test, y_train, y_test = train_test_split(X_disc, y_disc, test_size=0.3, random_state=42) clf = LogisticRegression(max_iter=1000) clf.fit(X_train, y_train) disc_score = accuracy_score(y_test, clf.predict(X_test)) print(f"Discriminative Score: {disc_score:.3f} (0.5 = ideal, indistinguishable from real)") # ── TSTR classification example — train an LSTM purely on synthetic sequences ── X_synth, y_synth = label_sequences(synthetic_data) # forward-return based labels model_synth = build_lstm_model(seq_len=60, n_features=5, n_classes=3) model_synth.fit(X_synth, y_synth, epochs=30, batch_size=32, verbose=0) tstr_acc = model_synth.evaluate(real_test_X, real_test_y, verbose=0)[1] print(f"TSTR Accuracy (trained on synthetic, tested on real): {tstr_acc:.2%}")
Common TimeGAN Implementation Mistakes
-
●Unnoticed Mode Collapse: If the generator produces only a few limited patterns (low diversity), a low discriminative score alone won't reveal this; sample diversity must also be checked.
-
●Too Little Real Data: Training a GAN on fewer than roughly 2,000 real sequences is typically unstable and degrades synthetic data quality.
-
●Not a Substitute for Fixing the Model: Adding synthetic data to a strategy that is fundamentally overfit doesn't fix the underlying problem; it just provides more data for the same mistake.
-
●TSTR Alone Is Not Enough: Besides TSTR (Train on Synthetic, Test on Real), TRTS (Train on Real, Test on Synthetic) should also be checked to evaluate synthetic data quality both ways.
For Which Use Cases is TimeGAN Suitable?
- ●High Hyperparameter Sensitivity: Unstable training and Mode Collapse are key challenges requiring careful tuning.
- ●Synthetic Data Isn't Real: Should never fully replace real data for final strategy evaluation.
- ●High Hardware Requirement: Stable GAN training typically requires a GPU with at least 8GB VRAM.
TimeGAN vs Other Generative/Unsupervised Models
| Feature | TimeGAN | Autoencoder (VAE) |
|---|---|---|
| Main Purpose | Generating new data | Dim. reduction / latent factors |
| Training Stability | Lower (Adversarial) | Higher |
| Primary Use | Data Augmentation | Regime Detection |
Foundational Papers for This Model
-
Time-series Generative Adversarial Networks Advances in Neural Information Processing Systems 32 (NeurIPS 2019)View Paper ↗
Ready to Leverage Synthetic Data?
ExpertNevees team trains a TimeGAN model on your historical data and evaluates synthetic data quality using Fidelity and TSTR metrics.
Other ExpertNevees AI Models
Autoencoder — Latent Risk Factor Extraction
An Autoencoder is an Encoder-Decoder neural network that builds a compressed representation of input data — used in algorithmic trading for dimensionality reduction, market regime identification, and latent risk factor extraction.
How Does Autoencoder Compress Data?
40 Features
Compression
Latent Factor
Reconstruction
40 Features
Market Regime Detection with Clustering
Regime
Regime
Regime
for Clustering
Autoencoder Reconstruction Loss
# Compress the input into a lower-dimensional latent space (bottleneck) z = Encoder(x) , dim(z) << dim(x) # Reconstruct the input from the compressed representation x_hat = Decoder(z) # Reconstruction error — the main criterion used for anomaly scoring L = || x - x_hat ||^2 = || x - Decoder(Encoder(x)) ||^2
Autoencoder Implementation for Regime Detection
import tensorflow as tf from tensorflow.keras.layers import Dense, Input, Dropout from tensorflow.keras.models import Model from sklearn.cluster import KMeans # ── Bottleneck Autoencoder for non-linear factor extraction ── # Encoder compresses 40 market features into a 2D latent embedding inp = Input(shape=(40,)) x = Dense(32, activation='relu')(inp) x = Dropout(0.1)(x) x = Dense(16, activation='relu')(x) # 2D latent space → directly visualisable as a scatter plot latent = Dense(2, activation='linear', name='latent')(x) # Decoder reconstructs the original feature vector from the latent code x = Dense(16, activation='relu')(latent) x = Dense(32, activation='relu')(x) output = Dense(40, activation='linear')(x) # MSE loss → minimise reconstruction error autoencoder = Model(inp, output) encoder = Model(inp, latent) # inference-time encoder only autoencoder.compile(optimizer='adam', loss='mse') autoencoder.fit(X_train, X_train, epochs=100, batch_size=64, verbose=0) # ── Cluster latent codes into market regimes via K-Means ── latent_features = encoder.predict(X_train, verbose=0) kmeans = KMeans(n_clusters=3, random_state=42, n_init=10) regimes = kmeans.fit_predict(latent_features) # cluster labels: 0=Trending, 1=Ranging, 2=High Volatility
Reconstruction Error & Regime/Anomaly Detection
# ── Reconstruction-error evaluation + anomaly / new-regime prediction ── import numpy as np recon = autoencoder.predict(X_test, verbose=0) recon_error = np.mean(np.square(X_test - recon), axis=1) print(f"Mean reconstruction error (test set): {recon_error.mean():.5f}") # Anomaly threshold: 95th percentile of training reconstruction error train_recon = autoencoder.predict(X_train, verbose=0) train_error = np.mean(np.square(X_train - train_recon), axis=1) threshold = np.percentile(train_error, 95) # Predict on the newest market sample latest = X_test[-1:] latest_error = np.mean(np.square(latest - autoencoder.predict(latest, verbose=0))) is_anomaly = latest_error > threshold latest_regime = kmeans.predict(encoder.predict(latest, verbose=0))[0] print(f"Latest sample: regime={['Trending','Ranging','High Volatility'][latest_regime]}, " f"anomaly={is_anomaly} (error={latest_error:.5f}, threshold={threshold:.5f})")
Common Autoencoder Implementation Mistakes
-
●Bottleneck Dimension Too Large: If the compression layer isn't small enough, the network simply memorizes the input instead of learning a meaningful representation.
-
●Static Anomaly Threshold: Choosing a fixed percentile (e.g. 95%) for the anomaly threshold is not stable across time and different volatility regimes; it should be recalibrated periodically.
-
●Unscaled Inputs: Without standardizing features, large-magnitude values dominate the reconstruction loss and small-magnitude features never get meaningfully compressed.
-
●Reconstruction Error Alone Gives No Direction: A high reconstruction error only indicates 'unusualness,' not price direction; it must be paired with a separate trading rule.
For Which Use Cases is Autoencoder Suitable?
- ●Low Interpretability: Factors extracted in Latent Space lack clear human-readable meaning.
- ●Latent Dimension Selection: The number of latent dimensions must be carefully chosen; too few or too many yields poor results.
- ●Not a Trading Signal Alone: Autoencoder output is a feature, not a Buy/Sell signal; requires a subsequent classification model.
Autoencoder vs Traditional PCA
| Feature | Deep Autoencoder | Traditional PCA |
|---|---|---|
| Reduction Type | Non-linear | Linear |
| Computation Speed | Slower (needs training) | Very Fast |
| Reconstruction Quality | Higher for complex data | Limited to linear relations |
Foundational Papers for This Model
-
Reducing the Dimensionality of Data with Neural Networks Science, 313(5786), 504–507View Paper ↗
-
Auto-Encoding Variational Bayes International Conference on Learning Representations (ICLR 2014)View Paper ↗
Ready to Extract Your Market's Hidden Factors?
ExpertNevees team designs the appropriate Autoencoder and extracts market regime or latent risk factors for your strategy.
Other ExpertNevees AI Models
HRP Portfolio — Hierarchical Risk Parity
Introduced by Marcos López de Prado, Hierarchical Risk Parity builds a portfolio via hierarchical asset clustering that is more stable than traditional Markowitz optimization without needing covariance matrix inversion.
Three Steps of the HRP Algorithm
Sample dendrogram: XAUUSD and XAGUSD (precious metals) cluster earlier than EURUSD and GBPUSD (forex) due to higher correlation.
Why is HRP More Stable than Markowitz?
Covariance Inversion
to Estimation Error
Maximum Drawdown
for N Assets
ExpertNevees Implementation Specification
- Historical Returns: Daily/weekly return series per asset
- Correlation Matrix: Computed from historical returns
- Assets: Forex, gold, crypto, stocks (Multi-Asset)
- Allocation Weights: Capital percentage per asset
- Rebalancing: Periodic reallocation (weekly/monthly)
- Risk Report: Portfolio Drawdown and Volatility
Correlation Distance & HRP Weight Allocation
# Convert the correlation matrix into a valid metric distance for hierarchical clustering d(i,j) = sqrt( 0.5 * (1 - rho(i,j)) ) # Cluster variance using inverse-variance weights within the cluster V_c = w_c^T * Sigma_c * w_c , w_c = diag(Sigma_c)^-1 / sum(diag(Sigma_c)^-1) # Recursive weight allocation between two sub-clusters at each binary split (Recursive Bisection) alpha = 1 - V_1 / (V_1 + V_2) , w_1 *= alpha , w_2 *= (1 - alpha)
HRP Implementation with PyPortfolioOpt
import pandas as pd import numpy as np from pypfopt import HRPOpt, expected_returns, risk_models # ── Build daily return matrix for multi-asset portfolio ── # Supports any mix: Forex, Gold, Crypto, Equity indices returns = pd.DataFrame({ 'XAUUSD': gold_prices.pct_change(), 'XAGUSD': silver_prices.pct_change(), 'EURUSD': eurusd_prices.pct_change(), 'GBPUSD': gbpusd_prices.pct_change(), 'BTCUSD': btc_prices.pct_change(), }).dropna() # ── Run HRP: no covariance matrix inversion needed ── # Step 1: hierarchical clustering on correlation distance matrix # Step 2: recursive bisection to assign inverse-variance weights per cluster hrp = HRPOpt(returns) weights = hrp.optimize(linkage_method='ward') # ward linkage reduces within-cluster variance hrp.clean_weights() print("Portfolio Allocation:", weights) # e.g. {'XAUUSD': 0.31, 'XAGUSD': 0.19, 'EURUSD': 0.28, 'GBPUSD': 0.13, 'BTCUSD': 0.09} # ── Evaluate expected risk-adjusted performance ── perf = hrp.portfolio_performance(verbose=True) # returns: (expected_annual_return, annual_volatility, Sharpe_ratio) # ── Monthly rebalancing check — reallocate when weights drift > 5% ── current_weights = get_current_portfolio_weights() drift = {k: abs(current_weights[k] - weights[k]) for k in weights} if max(drift.values()) > 0.05: rebalance_to_target(weights) # sends orders to MT5/cTrader API
Out-of-Sample Backtest: HRP vs Equal-Weight
# ── Out-of-sample backtest: HRP allocation vs naive Equal-Weight benchmark ── from pypfopt import HRPOpt split = int(len(returns) * 0.7) train_ret, test_ret = returns.iloc[:split], returns.iloc[split:] hrp = HRPOpt(train_ret) hrp_weights = hrp.optimize(linkage_method='ward') equal_weights = {k: 1 / len(hrp_weights) for k in hrp_weights} def portfolio_return(weights, ret_df): w = np.array([weights[c] for c in ret_df.columns]) return (ret_df.values @ w) hrp_pnl = portfolio_return(hrp_weights, test_ret) eq_pnl = portfolio_return(equal_weights, test_ret) hrp_sharpe = hrp_pnl.mean() / hrp_pnl.std() * np.sqrt(252) eq_sharpe = eq_pnl.mean() / eq_pnl.std() * np.sqrt(252) print(f"HRP Out-of-Sample Sharpe: {hrp_sharpe:.2f}") print(f"Equal-Weight Out-of-Sample Sharpe: {eq_sharpe:.2f}") print(f"HRP Max Drawdown: {(1 - (1 + hrp_pnl).cumprod() / (1 + hrp_pnl).cumprod().cummax()).max():.2%}")
Common HRP Implementation Mistakes
-
●Assuming Correlation Stability: HRP clustering is based on historical correlation; during financial crises, cross-asset correlation typically spikes toward 1, breaking down the clustering structure.
-
●Sensitivity to the Distance Metric: The (1-correlation)/2 distance formula is sensitive to outliers; return series with sharp jumps should be winsorized or filtered first.
-
●Ignores Expected Return: HRP is purely risk-based and doesn't consider expected returns; it's a 'risk-optimal' model, not a 'return-optimal' one.
-
●No Periodic Rebalancing: As asset volatilities shift over time, static HRP weights stop being optimal; periodic (e.g. monthly) rebalancing is required.
For Which Use Cases is HRP Suitable?
- ●Depends on Correlation Stability: If asset correlations change rapidly, the old clustering becomes invalid.
- ●No Return Forecasting: HRP only manages risk; it doesn't forecast future asset returns.
- ●Needs Regular Rebalancing: Without periodic reallocation, weights drift from optimal over time.
HRP vs Markowitz and Equal-Weight
| Feature | HRP | Markowitz (MVO) | Equal-Weight |
|---|---|---|---|
| Covariance Inversion | No | Yes | N/A |
| Sensitivity to Estimation Error | Low | Very High | None |
| Risk Consideration | Yes (Hierarchical) | Yes | No |
Foundational Papers for This Model
-
Building Diversified Portfolios that Outperform Out of Sample The Journal of Portfolio Management, 42(4), 59–69View Paper ↗
-
Portfolio Selection The Journal of Finance, 7(1), 77–91View Paper ↗
Ready to Optimize Your Multi-Asset Portfolio?
ExpertNevees team runs the HRP algorithm on your assets and builds an automatic Rebalancing system connected to MT5/cTrader.
Other ExpertNevees AI Models
Ensemble — Combining AI Models
Ensemble is a method that combines the output of several independent AI models (such as LSTM, XGBoost, and Random Forest) so that each model's weaknesses are covered by the others' strengths. Per the classic theory of Bates & Granger (1969), combining multiple forecasts typically yields a lower mean squared error (MSE) than the single best model alone — provided the models' errors are not highly correlated with each other.
Five Core Methods for Combining AI Models
Why Does Combining Models Usually Outperform a Single Model?
of Prediction Error
Point of Failure
Covered by Others
Market Regime Shifts
ExpertNevees Implementation Specification
- Base Model Predictions: Output/probability from each independent model (e.g. LSTM, XGBoost, RF)
- Out-of-Fold Errors: Used to estimate weights or train the meta-learner without data leakage
- Regime Feature (optional): Autoencoder output or market volatility for switching
- Final Combined Prediction: The final combined signal or predicted price
- Per-Model Weight: Each base model's contribution to the final decision, for interpretability
- Model Agreement Level: How aligned the base models are, an extra confidence signal
Combination Formulas: Averaging, Weighting, and Stacking
# Simple average across N independent base models y_hat = (1/N) * sum_i( y_hat_i ) # Bates & Granger (1969) inverse-variance weighting — lower-error models get higher weight w_i = sigma_i^-2 / sum_j( sigma_j^-2 ) , y_hat = sum_i( w_i * y_hat_i ) # Stacking (Wolpert, 1992) — a meta-learner g() learns the optimal non-linear combination y_hat = g( y_hat_1, y_hat_2, ..., y_hat_N ; theta ) # Regime-gated mixture of experts — weight each model by the probability of its matching regime y_hat = sum_r( P(regime=r | x) * y_hat_r(x) )
Correct Stacking Using Out-of-Fold Predictions
import numpy as np from sklearn.model_selection import TimeSeriesSplit, cross_val_predict from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from sklearn.metrics import accuracy_score, classification_report # ── Base models: intentionally diverse algorithms (low error correlation) ── base_models = { 'rf': RandomForestClassifier(n_estimators=300, max_depth=8, random_state=42), 'xgb': XGBClassifier(n_estimators=300, max_depth=4, learning_rate=0.05), 'lstm_probs': None, # pre-computed LSTM probabilities, see make_sequences() on the LSTM page } tscv = TimeSeriesSplit(n_splits=5) # time-ordered folds — never shuffle financial time series # ── Critical: generate OUT-OF-FOLD predictions for the meta-learner ── # Using in-sample predictions here would leak information and make the # meta-learner look far more accurate than it will be live (see Common Mistakes) oof_rf = cross_val_predict(base_models['rf'], X, y, cv=tscv, method='predict_proba')[:, 1] oof_xgb = cross_val_predict(base_models['xgb'], X, y, cv=tscv, method='predict_proba')[:, 1] oof_lstm = lstm_oof_probs # from a separate walk-forward LSTM training loop # ── Meta-learner: a simple Logistic Regression stacks the three OOF prediction columns ── meta_X = np.column_stack([oof_rf, oof_xgb, oof_lstm]) meta_model = LogisticRegression() meta_model.fit(meta_X, y) print("Learned ensemble weights:", dict(zip(['rf', 'xgb', 'lstm'], meta_model.coef_[0]))) # ── Fit final base models on ALL training data, then combine via the meta-learner ── base_models['rf'].fit(X_train, y_train) base_models['xgb'].fit(X_train, y_train) test_meta_X = np.column_stack([ base_models['rf'].predict_proba(X_test)[:, 1], base_models['xgb'].predict_proba(X_test)[:, 1], lstm_test_probs, ]) y_pred = meta_model.predict(test_meta_X) print(f"Ensemble Accuracy: {accuracy_score(y_test, y_pred):.4f}") print(classification_report(y_test, y_pred, target_names=['Down', 'Up']))
Comparing the Ensemble Against the Best Single Model
# ── Bates & Granger (1969) inverse-variance weighted average, computed from validation error ── from sklearn.metrics import mean_squared_error val_errors = { 'rf': mean_squared_error(y_val, rf_val_pred), 'xgb': mean_squared_error(y_val, xgb_val_pred), 'lstm': mean_squared_error(y_val, lstm_val_pred), } inv_var = {k: 1 / v for k, v in val_errors.items()} total = sum(inv_var.values()) weights = {k: v / total for k, v in inv_var.items()} print("Bates-Granger weights:", weights) ensemble_test_pred = ( weights['rf'] * rf_test_pred + weights['xgb'] * xgb_test_pred + weights['lstm'] * lstm_test_pred ) # ── Compare ensemble MSE against each individual model AND the naive equal-weight average ── naive_avg_pred = (rf_test_pred + xgb_test_pred + lstm_test_pred) / 3 results = { 'RF alone': mean_squared_error(y_test, rf_test_pred), 'XGBoost alone': mean_squared_error(y_test, xgb_test_pred), 'LSTM alone': mean_squared_error(y_test, lstm_test_pred), 'Equal-weight ensemble': mean_squared_error(y_test, naive_avg_pred), 'Bates-Granger ensemble': mean_squared_error(y_test, ensemble_test_pred), } for name, mse in sorted(results.items(), key=lambda x: x[1]): print(f"{name:24s} MSE = {mse:.6f}")
Common Mistakes When Combining AI Models
-
●Combining Correlated Models: If two models' errors are highly correlated (e.g. two similar tree-based models), combining them yields almost no improvement. The Bates & Granger formula ignores this correlation and assumes independent errors — genuine algorithmic diversity (e.g. tree-based + neural + linear) is a necessary condition.
-
●Data Leakage in Stacking: Training the meta-learner on in-sample (rather than out-of-fold) base-model predictions falsely inflates accuracy; this accuracy will never be reproduced in live trading.
-
●Ignoring Latency Cost: Running N models instead of one means N times the inference time; for scalping strategies needing sub-1ms latency, a heavy ensemble can be counterproductive.
-
●Averaging Uncalibrated Probabilities: Different models often have different confidence scales (e.g. a neural network's Softmax output isn't directly comparable to XGBoost's probability); calibration (e.g. Platt Scaling) should be applied before averaging.
-
●Meta-Learner Overfitting: Using a complex model (e.g. a deep neural network) as the meta-learner with few base models/validation samples overfits the combination layer; a simple linear model is usually sufficient and more stable.
For Which Use Cases is Model Combination Suitable?
- ●Higher Complexity and Harder Debugging: When the final signal comes from combining N models, understanding why a specific trade went wrong is harder than with a single model.
- ●No Guaranteed Improvement (No Free Lunch): If the base models are weak or highly correlated, the ensemble may show no improvement over the single best model.
- ●Higher Compute Cost and Development Time: Building, training, and maintaining N models instead of one requires more development time and server resources.
Voting vs. Stacking vs. Regime Switching
| Feature | Voting/Averaging | Stacking | Regime Switching |
|---|---|---|---|
| Implementation Complexity | Low | Medium | High |
| Data Leakage Risk | None | High (without OOF) | Medium |
| Interpretability | High | Medium | High |
| Adapts to Regime Change | Low | Medium | High |
Foundational Papers for This Method
-
The Combination of Forecasts Journal of the Operational Research Society, 20(4), 451–468View Paper ↗
-
Stacked Generalization Neural Networks, 5(2), 241–259View Paper ↗
-
Attention-based CNN-LSTM and XGBoost Hybrid Model for Stock Prediction arXiv:2204.02623 [q-fin.ST]View Paper ↗
-
Time Series Prediction Based on LSTM-Attention-LSTM Model IEEE Access, 11, 48322–48331View Paper ↗
Ready to Combine Several AI Models?
ExpertNevees team can combine and implement LSTM, XGBoost, Random Forest, and other models for you via Stacking or regime switching.
Base Models Commonly Used in Ensembles
MQL4 / MQL5 — MetaTrader Trading Bot Development
MQL (MetaQuotes Language) is MetaTrader's native language built on C++ foundations. MQL5 is fully Object-Oriented, supporting classes, inheritance and polymorphism. As of 2026, MetaTrader 5 leads the retail CFD market with a 62% share.
Expert Advisor Architecture in MQL5
What's New in MetaTrader 5 and MQL5?
Execution via CUDA GPU
(Replaced GDI, since 2025)
Git-Based Version Control
Override Rules since 2025
Complete Expert Advisor Structure in MQL5
// Volatility-adjusted position sizing based on ATR #includeCTrade trade; input double RiskPercent = 1.0; // risk per trade, % of balance input int AtrPeriod = 14; int atrHandle; int OnInit() { atrHandle = iATR(_Symbol, _Period, AtrPeriod); if(atrHandle == INVALID_HANDLE) return(INIT_FAILED); return(INIT_SUCCEEDED); } double CalculateLotSize(double stopLossPoints) { double atr[]; CopyBuffer(atrHandle, 0, 0, 1, atr); double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * RiskPercent / 100.0; double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); double lot = riskAmount / (stopLossPoints * tickValue); return NormalizeDouble(lot, 2); } void OnTick() { static datetime lastBar = 0; datetime currentBar = iTime(_Symbol, _Period, 0); if(currentBar == lastBar) return; // process once per new candle, not every tick lastBar = currentBar; double atr[]; CopyBuffer(atrHandle, 0, 0, 1, atr); double slPoints = atr[0] / _Point * 1.5; double lot = CalculateLotSize(slPoints); // CTrade handles requote retry, slippage and price normalization internally trade.Buy(lot, _Symbol, 0, SymbolInfoDouble(_Symbol, SYMBOL_BID) - slPoints * _Point); }
Common Mistakes in MQL5 Development
- ●Running Logic on Every Tick Instead of Every Candle: Without checking the current candle time (like the lastBar pattern above), entry logic can fire multiple times within one candle, causing unwanted duplicate trades.
- ●Not Checking for INVALID_HANDLE: If an indicator handle isn't validated in OnInit(), CopyBuffer silently fails in OnTick(), and the EA decides on invalid data without warning.
- ●Ignoring Requotes and Slippage: Sending orders without handling broker server errors (Requote, Off Quotes) in volatile markets causes missed trades or execution at very different prices; the CTrade class handles these internally.
- ●Backtesting with Poor Data Quality: Running Strategy Tester with regular Every Tick mode (not Every Tick Based on Real Ticks) produces unrealistic results, especially for scalping strategies.
For Which Use Cases is MQL5 Suitable?
- ●Limited to the MetaTrader Ecosystem: MQL5 code doesn't run directly on any other platform (cTrader, TradingView, crypto exchanges); it must be rewritten for each.
- ●Limited Machine Learning Libraries: Unlike Python, MQL5 lacks a rich ML library ecosystem; models must be trained in Python and then imported into MQL5 via ONNX.
- ●MQL4 is Being Phased Out: MetaQuotes' new development is mostly focused on MQL5; new projects should generally start directly with MQL5.
Official MQL5 Documentation and Resources
-
MQL5 Reference — Official Language Documentation MQL5.comView Docs ↗
-
What's New in MetaTrader 5 — Release Notes MetaTrader5.comView Docs ↗
Ready to Build Your MQL5 Trading Bot?
ExpertNevees, with 80+ successful MQL4/MQL5 projects, implements everything from simple Expert Advisors to ONNX-powered machine learning systems for you.
Other Trading Programming Languages
cAlgo / cBot — cTrader Trading Bot Development
cAlgo is a C#-based language developed for the cTrader platform. Unlike MQL with its own syntax, cAlgo uses the standard .NET compiler and provides full access to the C# library ecosystem. cTrader works via Direct Market Access on ECN/STP brokers.
cBot Architecture in cAlgo
Complete cBot Structure in C#
using cAlgo.API; using cAlgo.API.Indicators; [Robot(TimeZone = TimeZones.UTC, AccessRights = AccessRights.None)] public class AtrRiskBot : Robot { [Parameter("Risk %", DefaultValue = 1.0)] public double RiskPercent { get; set; } private AverageTrueRange atr; protected override void OnStart() { atr = Indicators.AverageTrueRange(14, MovingAverageType.Simple); } protected override void OnBar() { // Volatility-adjusted position size, mirrors the MQL5 ATR pattern double slPips = atr.Result.LastValue / Symbol.PipSize * 1.5; double riskAmount = Account.Balance * RiskPercent / 100; double volume = Symbol.NormalizeVolumeInUnits(riskAmount / (slPips * Symbol.PipValue)); if (Positions.Count == 0) { // ExecuteMarketOrder returns a TradeResult with slippage/fill details var result = ExecuteMarketOrder(TradeType.Buy, SymbolName, volume, "AtrRiskBot", slPips, null); if (!result.IsSuccessful) Print("Order failed: {0}", result.Error); } } }
Common Mistakes in cAlgo Development
- ●Not Checking TradeResult.IsSuccessful: Ignoring ExecuteMarketOrder's result means order failures (e.g. due to invalid volume) go unnoticed, and the bot assumes a trade was opened.
- ●Using Raw Volume Instead of NormalizeVolumeInUnits: Each symbol has a different minimum/step volume; sending unnormalized volume causes broker order rejection.
- ●Relying on OnTick for Heavy Logic: Running complex calculations in OnTick (which fires on every price change, not just new candles) can cause high processing load and latency; OnBar suits most strategies better.
For Which Use Cases is cAlgo Suitable?
- ●Smaller User Base than MetaTrader: Far fewer brokers support cTrader compared to MetaTrader; broker choice is more limited.
- ●Requires C#/.NET Knowledge: Unlike MQL which is designed to be simpler for non-specialists, learning cAlgo requires genuine familiarity with OOP concepts in C#.
Official cAlgo Documentation and Resources
-
cAlgo API Reference — Official Documentation help.ctrader.comView Docs ↗
-
cTrader Open API — Developer Documentation openapi.ctrader.comView Docs ↗
Ready to Build Your cBot on cTrader?
ExpertNevees, with 25+ successful cBot projects, implements everything from simple strategies to Open-API-connected systems for you.
Other Trading Programming Languages
PineScript v6 — Indicators and Strategies for TradingView
PineScript is TradingView's dedicated scripting language. Version 6 launched in November 2024, bringing a complete overhaul to the type system, dynamic data requests, and new drawing types; v5 is no longer updated and new features are released exclusively in v6.
What Changed in PineScript v6?
true/false, Never na
Functions for Professional Debugging
on Any Historical Bar
Automatic Indentation
Indicator vs. Strategy
Complete Strategy in PineScript v6
//@version=6 strategy("ATR Risk Strategy", overlay = true, initial_capital = 10000) riskPercent = input.float(1.0, "Risk %", minval = 0.1) atrLen = input.int(14, "ATR Length") atrVal = ta.atr(atrLen) emaFast = ta.ema(close, 20) emaSlow = ta.ema(close, 50) // v6: boolean values are always strictly true/false, never na longCondition = ta.crossover(emaFast, emaSlow) if longCondition stopDistance = atrVal * 1.5 riskAmount = strategy.equity * riskPercent / 100 qty = riskAmount / stopDistance strategy.entry("Long", strategy.long, qty = qty) strategy.exit("Exit", "Long", stop = close - stopDistance) // v6: new log.* functions for real debugging instead of plotting hacks if barstate.islast log.info("ATR: {0}, Fast EMA: {1}, Slow EMA: {2}", atrVal, emaFast, emaSlow) plot(emaFast, color = color.orange) plot(emaSlow, color = color.blue) alertcondition(longCondition, title = "Long Signal", message = "Buy {{ticker}} at {{close}}")
Common Mistakes in PineScript
- ●Repainting in request.security(): Improper use of higher-timeframe data without lookahead=barmerge.lookahead_off causes backtests to use future information, making results non-reproducible.
- ●Ignoring Realistic Commission and Slippage: Backtesting without setting commission_value and slippage in strategy() shows results far more optimistic than real performance.
- ●Not Migrating from Implicit Boolean Casts: Un-migrated v5 code relying on if myInt (instead of if myInt != 0) no longer compiles in v6 and must be explicitly rewritten.
For Which Use Cases is PineScript Suitable?
- ●No Native Automated Trade Execution: PineScript alone cannot send real orders to a broker; full automation requires connecting alerts via Webhook to an external execution service.
- ●Computational Resource Limits: Heavy scripts (nested loops, large arrays) may hit TradingView server Timeout errors.
Official PineScript Documentation and Resources
-
Pine Script v6 Language Reference Manual tradingview.comView Docs ↗
-
Migration Guide — To Pine Script Version 6 tradingview.comView Docs ↗
Ready to Build a PineScript v6 Indicator or Strategy?
ExpertNevees designs PineScript v6 indicators and strategies, and connects alerts via Webhook to automated execution in MetaTrader when needed.
Other Trading Programming Languages
Python — AI, Broker and Exchange Connectivity
Python is the dominant language for AI development in algorithmic trading — from machine learning with TensorFlow/PyTorch/Scikit-learn to direct broker and exchange connectivity. Unlike MQL5 or cAlgo which are tied to one platform, Python connects to any platform via the official MetaTrader5 library, ZeroMQ, or crypto exchange APIs.
Architecture of a Python-Based Trading System
Connecting to MetaTrader and Sending an Order with Python
import MetaTrader5 as mt5 import pandas as pd # Official library talks directly to the terminal — no ZMQ bridge required if not mt5.initialize(): print("MT5 initialize failed:", mt5.last_error()) quit() symbol = "EURUSD" account_info = mt5.account_info() balance = account_info.balance # ATR-based position sizing, same risk model used in the MQL5/cAlgo pages rates = mt5.copy_rates_from_pos(symbol, mt5.TIMEFRAME_H1, 0, 15) df = pd.DataFrame(rates) atr = (df['high'] - df['low']).rolling(14).mean().iloc[-1] risk_percent = 1.0 point = mt5.symbol_info(symbol).point sl_points = atr / point * 1.5 tick_value = mt5.symbol_info(symbol).trade_tick_value lot = round((balance * risk_percent / 100) / (sl_points * tick_value), 2) price = mt5.symbol_info_tick(symbol).ask request = { "action": mt5.TRADE_ACTION_DEAL, "symbol": symbol, "volume": lot, "type": mt5.ORDER_TYPE_BUY, "price": price, "sl": price - sl_points * point, "deviation": 10, "magic": 123456, "type_filling": mt5.ORDER_FILLING_IOC, } result = mt5.order_send(request) if result.retcode != mt5.TRADE_RETCODE_DONE: print(f"Order failed, retcode={result.retcode}") mt5.shutdown()
Common Mistakes in Python Trading Development
- ●Not Checking Order retcode: Calling order_send without checking result.retcode means order failures (invalid volume, market closed) go unnoticed.
- ●Running a Heavy Model in the Live Trading Loop: Calling model.predict() on a large neural network every tick without batching or caching optimization introduces significant latency to real-time decisions.
- ●Incorrect Timezone in Historical Data: Combining MT5 data (usually UTC or broker time) with other sources without explicit timezone alignment causes off-by-one errors in candle labeling.
- ●No Connection-Drop Handling: Simple Python scripts without automatic reconnect logic halt completely on a brief internet outage or terminal restart, leaving open trades unmonitored.
For Which Use Cases is Python Suitable?
- ●Slower Execution Than C++/C#: Being interpreted, Python is unsuitable for sub-millisecond HFT strategies; exporting the model to ONNX and running it in MQL5/cAlgo is the standard solution for those cases.
- ●MetaTrader5 Library is Windows-Only: The official MetaTrader5 Python library only works fully on Windows; Linux/Mac require Wine or a ZeroMQ bridge.
Official Documentation and Resources
-
MetaTrader5 Python Package — Official Documentation PyPI / mql5.comView Docs ↗
-
CCXT — CryptoCurrency eXchange Trading Library GitHub / docs.ccxt.comView Docs ↗
Ready to Build a Python-Based Trading System?
ExpertNevees implements AI models, broker/exchange connectivity, and ONNX model export for you in Python.
Other Trading Programming Languages
Comparing 13 AI Trading Models
No single model wins in every situation. This page places the key metrics of all 13 models — from classic Random Forest to advanced Transformers — side by side, so you can pick the right one based on available data, compute resources, and strategy type.
13 Models, 7 Key Criteria
| Model | Category | Primary Task | Min. Data | GPU Need | Training Time | Accuracy/Quality |
|---|---|---|---|---|---|---|
| Random Forest | Classic | Trend Classification | 1,000+ candles | No | Minutes | 62–70% |
| XGBoost | Classic | Trend Classification | 2,000+ candles | No | Minutes–1h (with Optuna) | 68–74% |
| MLP | Classic | Signal Classification | 2,000+ samples | Optional | Minutes | 60–70% |
| Linear Factor | Classic | Regression / Ranking | 500+ cross-sectional rows | No | Seconds–minutes | IC ≈ 0.03–0.08 |
| LSTM | Deep Learning | Time-Series Classification/Prediction | 5,000+ candles | Recommended | 1–4h (GPU) | 65–75% |
| GRU | Deep Learning | Time-Series Classification/Prediction | 5,000+ candles | Recommended | 35–90min (GPU) | 63–72% |
| Attention | Deep Learning | Classification + Interpretability | 5,000+ candles | Recommended | 1–4h (GPU) | 67–76% |
| Transformer (TFT) | Deep Learning | Multi-Horizon Forecasting | 10,000+ candles | Required | Several hours | Lowest P50 MAE among all models |
| CNN | Deep Learning | Visual Pattern Classification | 5,000+ labelled patterns | Recommended | 1–3h (GPU) | 50+ pattern classes |
| DDQN | Advanced | Reinforcement Learning (direct decision) | Simulated environment + sufficient data | Recommended | Hours–days (episodic) | Measured via cumulative Sharpe |
| TimeGAN | Advanced | Synthetic Data Generation | 2,000+ real candles | Recommended | Several hours | Discriminative Score ≈ 0.5 ideal |
| Autoencoder | Advanced | Dimensionality Reduction / Anomaly Detection | 2,000+ samples | Optional | Minutes | Lower reconstruction error is better |
| HRP Portfolio | Advanced | Portfolio Capital Allocation | Daily multi-asset returns | No | Seconds | Sharpe vs. Equal-Weight |
Where to Start Based on Your Situation
-
●High test accuracy is not enough: Always evaluate with Walk-Forward Validation or TimeSeriesSplit, never a simple random split. Every model on this page follows this principle in its sample code.
-
●Classic models are not always inferior: Random Forest or XGBoost often reach accuracy close to LSTM with less data and no GPU. Only add complexity once it proves real, measured benefit.
-
●Ensembling multiple models usually beats a single model: Combining Random Forest, XGBoost, and LSTM outputs via an MLP layer or weighted voting is typically more stable than relying on one model alone.
-
●Markets change, so must the model: Every deep and classic model on this page needs periodic retraining (daily to monthly, depending on the model). An unretrained model becomes stale within weeks.
-
●No model replaces risk management: Even the highest classification accuracy will not become a profitable strategy without correct position sizing, stop-loss, and drawdown control.
Start With One of These Models
Why Convert Your Trading Strategy Into a Bot?
No matter how carefully a trading strategy is designed, executing it manually leaves it exposed to human delay, fatigue, and emotional decision-making. Automating a strategy into a trading bot (Expert Advisor or cBot) shifts execution from a trader's in-the-moment judgment to predefined, backtestable rules — so the outcome no longer depends on the trader's psychological state at that exact moment.
Why Does Manual Execution Undermine Even a Good Strategy?
Behavioral finance research shows that the largest gap between a strategy's theoretical and actual results usually comes not from the strategy itself, but from the trader deviating from it during execution. Fear of losing gains leads to closing winners too early, greed leads to holding losing positions too long, and after a loss, the urge to recover quickly (revenge trading) pulls the trader away from their own rules. Even experienced traders unconsciously break their own written rules during periods of fatigue or high stress.
Six Technical Reasons to Convert a Strategy Into a Trading Bot
When Is Automation NOT the Right Choice?
-
●Strategy Without Precise Rules: If your entries and exits are still based on market 'feel' rather than clearly codeable rules, the strategy needs to be written down and made precise first — a bot cannot build logic that doesn't exist.
-
●Heavy Reliance on Qualitative News Analysis: Strategies that depend on human interpretation of unpredictable events (political decisions, breaking news) are hard to fully automate and usually need a semi-automated model.
-
●Insufficient Historical Data for Backtesting: Without enough data to test against, confidence in the bot's future performance has no scientific basis and increases real risk.
-
●Expecting Guaranteed Profitability: Automation improves execution, it does not remove inherent market risk. A trading bot eliminates human deviation, but it does not guarantee profit.