DEV Community

Market Masters
Market Masters

Posted on

Building an AI-Assisted Algorithmic Trading Backtester in Python

Building an AI-Assisted Algorithmic Trading Backtester in Python

Most retail traders lose money because they cannot execute a plan consistently. Algorithmic trading removes emotion, but simple rule-based systems often fail in changing markets. Adding a lightweight AI layer for signal filtering can improve results, provided you keep the system simple and test it rigorously.

This article walks through a minimal Python backtester that combines a classic moving average crossover with a basic ML classifier to filter signals. The goal is not to sell a black box, it is to show the exact code and decisions required to move from manual chart watching to a reproducible process.

The Setup

We will use:

  • pandas and numpy for data handling
  • scikit-learn for a simple logistic regression or random forest classifier
  • yfinance or local CSV data for OHLCV

The strategy:

  1. Compute 20 and 50 period SMAs.
  2. Generate a raw crossover signal.
  3. Feed recent price features into a model trained on historical labels (1 if price rose 1% in next 5 bars, 0 otherwise).
  4. Only take trades where the ML model agrees with the crossover.

This hybrid approach reduces whipsaws while keeping the core logic transparent.

Data Loading and Feature Engineering

import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split

def load_data(symbol="AAPL", start="2020-01-01"):
    df = pd.read_csv(f"{symbol}.csv")  # or yfinance.download
    df["returns"] = df["close"].pct_change()
    df["sma20"] = df["close"].rolling(20).mean()
    df["sma50"] = df["close"].rolling(50).mean()
    df["volatility"] = df["returns"].rolling(20).std()
    return df.dropna()
Enter fullscreen mode Exit fullscreen mode

Add more features if needed: RSI, ATR, volume z-score. The key is to avoid lookahead bias. Every feature must be computable at the close of the current bar.

Labeling the Data

We create a forward-looking label for training:

def make_labels(df, horizon=5, threshold=0.01):
    future_ret = df["close"].shift(-horizon) / df["close"] - 1
    df["label"] = (future_ret > threshold).astype(int)
    return df.dropna()
Enter fullscreen mode Exit fullscreen mode

This is deliberately simple. In production you would also label short signals and use class weighting.

Training the Filter Model

features = ["returns", "volatility", "sma20", "sma50"]
X = df[features]
y = df["label"]

X_train, X_test, y_train, y_test = train_test_split(X, y, shuffle=False, test_size=0.3)

model = RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42)
model.fit(X_train, y_train)
print("Train accuracy:", model.score(X_train, y_train))
print("Test accuracy:", model.score(X_test, y_test))
Enter fullscreen mode Exit fullscreen mode

The test accuracy will likely be modest (52-58%). That is expected and acceptable. The model is a filter, not a crystal ball.

The Backtest Loop

def run_backtest(df, model, features, initial_capital=10000):
    position = 0
    equity = initial_capital
    trades = []

    for i in range(50, len(df)):
        row = df.iloc[i]
        signal = 1 if row["sma20"] > row["sma50"] else -1

        # AI filter
        X_live = row[features].values.reshape(1, -1)
        ml_pred = model.predict(X_live)[0]

        if signal == 1 and ml_pred == 1 and position == 0:
            position = 1
            entry_price = row["close"]
            entry_time = df.index[i]
        elif signal == -1 and position == 1:
            exit_price = row["close"]
            pnl = (exit_price - entry_price) / entry_price
            equity *= (1 + pnl)
            trades.append({"entry": entry_time, "pnl": pnl})
            position = 0

    return equity, trades
Enter fullscreen mode Exit fullscreen mode

Run the function on both in-sample and out-of-sample periods. Track maximum drawdown and win rate separately. Never optimize solely on total return.

Risk Management Additions

A real system needs position sizing and stops:

risk_per_trade = 0.01  # 1% of equity
stop_distance = df["atr"].iloc[i] * 2
shares = (equity * risk_per_trade) / stop_distance
Enter fullscreen mode Exit fullscreen mode

Also log every rejected signal. Many backtesters hide how often the AI filter actually changed the outcome. That rejection log is where the real learning happens.

Common Failure Modes

  • Overfitting the classifier on too many features or future data.
  • Ignoring transaction costs and slippage. Add at least 0.05% round-trip for liquid US stocks.
  • Assuming the model stays valid after regime shifts. Retrain quarterly and keep a rolling validation window.

The market does not care about your training accuracy. It cares whether the distribution of future returns matches the past.

Where to Go From Here

Replace the random forest with a gradient boosted model or a small LSTM if you have minute data. Add regime detection so the model knows when to stand down entirely. Most importantly, paper trade the system for at least three months before allocating real capital.

If you prefer not to build the entire stack yourself, platforms like Market Masters provide pre-built AI screeners, pattern recognition, and portfolio risk tools that already handle the data plumbing. The principles remain the same: start simple, measure everything, and never trust a strategy you have not broken yourself.

The full notebook and sample datasets are available in the repository linked below. Clone it, run the backtest on your own symbols, and modify the feature set until the out-of-sample curve stops looking like a hockey stick.

Happy building.


Want the same signals without writing the code? Try the free tier at marketmasters.ai or test Orion, their AI trading assistant, on any symbol in seconds.

Top comments (0)