ML Equity Selection System

This is an independent project that selects stocks from the S&P 500 and trades them automatically. The algorithm ranks all S&P 500 stocks and holds the names that score highest relative to their peers.

The system runs end to end without supervision. Every evening after the close it refreshes its datasets, scores all 500 or so names, decides what to buy/sell/hold, and queues orders for the next morning's open. It has been running live since June 2026 on a Linux server I built for this purpose, executing across multiple accounts simultaneously (one paper account and one more recent live account that was implemented after solid paper performance).

How it works

The system is built in four stages. Collection gathers the data, prediction trains the model and scores every stock, evaluation tests both collection and prediction offline before anything goes live, and execution places the orders.

Collection is written in Rust and runs asynchronously, gathering price and volume history, index and sector context, SEC filings, news sentiment, market regime signals, and data quality flags for roughly 500 names. Every feature is then z-scored cross-sectionally by date. On each trading day, each feature is centered on that day's average across all stocks and divided by that day's standard deviation. The model therefore always compares a stock against the other stocks available on that same day rather than against its own history, leaving only relative differences.

Two separate models are trained, both aimed at the same three day window. A ridge regression predicts how much a stock will outperform or underperform the rest of the market over the next three trading days. A logistic regression estimates the probability that a stock finishes among the best performing 10% of the index over that same window. The labels for that classifier are built by ranking every stock on each date by its realized three day excess return and marking the top decile. Both models are fitted on features standardized using training data only, and the most recent stretch of dates is held out as a validation set, which is where the weighting that blends their two outputs into a single score is chosen.

Evaluation is walk-forward. The model trains on at least two years of history, is tested on the quarter immediately after, and then the window rolls forward and the whole process repeats, so nothing is ever scored on data it was trained on. Spread, slippage, and regulatory fees are all modeled to maximize realism. Every run is measured against SPY, an equal weight version of the same universe, and a naive momentum signal using identical trading rules.

Execution follows a fixed set of rules. There is a cap on how many positions can be open at once, a minimum score required to buy, a maximum holding period, take-profit and stop-loss levels, and a cash buffer. Those constants were tuned by grid search, and scored on folds held back from the tuning. Before placing any orders, the system checks that its data is fresh and that the broker state matches what it expects.

The reason the model only runs inference after market close is because we do not have any historical intra day data, and therefore instead have to train the model using data collected at market close for each historical day. This approach also removes the need for optimizing for latency, which is an advantage when going up against competitors with significantly more resources.

Results

The numbers below come from 15 walk-forward folds covering 915 out-of-sample trading days and 683 trades, using the exact policy that runs in production.

StrategyCAGRMax drawdownSharpe
This model44.9%-26.9%1.61
SPY buy and hold22.2%-19.0%1.38
Equal weight universe16.1%-18.2%1.09
Naive momentum, identical policy1.4%-3.4%0.41

In order to ensure that the generated returns did not come from the trading rules alone, a naive momentum signal with an identical execution policy was run. The resulting return of 1.4% per year confirms that the returns are derived by the model. Additionally the model was compared to the S&P 500 and an equal weight buy and hold strategy, to ensure that the algorithm returns were not solely due to broad market returns.

Live results so far

Below is the paper trading account as of August 17, 2026, up 38.2% from a starting balance of $100,000.

paper trading account balance since going live

The algorithm has not run long enough to prove anything confidently, and a paper account has no real fills, no partial executions, and no market impact. However, the walk-forward backtest, the paper account, and the live account have all been promising so far.

Potential improvements

Execution is where I believe the most improvement can be made. The trading rules are relatively basic, and every selected stock is currently treated identically regardless of how confident the model is in it. Sizing positions by prediction confidence would be a clear next step to try.

Beyond that, the algorithm needs more runtime both in paper and live trading to prove that it actually has an edge.

The source code for this project is private, since it is an active strategy trading real money. I am happy to talk through the architecture, the evaluation setup, and its limitations in more detail.