Intraday Walk‑Forward Trading Bot
Self‑retraining MLP on AAPL minute data
A single‑file algorithm (main.py
) designed for QuantConnect. It rolls a 60‑day window every Sunday night, retrains a tiny MLP under focal loss, and then trades Monday–Friday using half‑Kelly sizing clipped to 50 % gross exposure. Daily equity is guarded by a –0.5 % stop to keep tail‑risk civilised.

Why Walk‑Forward Retraining?
- Non‑stationarity: minute‑level order‑flow drifts week to week; static models stale out fast.
- GPU‑less speed: the tiny MLP (+ scaler) retrains in < 2 s on QC’s free CPU tier.
- Fresh risk metrics: weekly re‑fit rescales
StandardScaler
so ATR, RSI slopes, and time‑of‑day sinusoids stay centred. - Straightforward deployment: avoiding offline pipelines means one artifact bundle (
.keras
+ scaler npy) checked into QC storage.
Feature Engineering
Five categories stitched into a float32
vector:
- Past returns: 5‑bar micro‑momentum signal.
- RSI slope: short‑term momentum acceleration.
- Normalized ATR: dollar volatility / price ⇒ forecasts stable across regimes.
- Daily z‑score: mean‑reversion anchor vs 2‑day intraday drift.
- Time‑of‑day sin/cos: captures lunch‑time chop vs open/close frenzy without categorical edges.

Training Pipeline
# Sunday 20:00 ET cron history = self.History(self.symbol, WINDOW_DAYS, Resolution.Minute) # → engineer RSI/ATR, z‑score, sincos, returns X, y = build_samples(history) X_train, X_test = chrono_split(X, y, 0.8) scaler = StandardScaler().fit(X_train) X_train = scaler.transform(X_train) model = tf.keras.Sequential([ Dense(32, 'relu'), Dense(8, 'relu'), Dense(1, 'sigmoid')]) model.compile('adam', loss=focal_loss, metrics=['accuracy']) model.fit(X_train, y_train, epochs=10, batch_size=512, validation_split=0.1, callbacks=[EarlyStopping(patience=3)])
Why focal loss? — balances the skewed 52/48 up/down ratio better than BCE, boosting recall on rare fast drops.
Risk Management & Sizing
Position size ↔ prediction edge: half‑Kelly × 1 % risk / (ATR/$)
. Capped at 50 % gross and liquidates when daily P/L pierces −0.5 %.
edge = prob_up - 0.5 # ±0.5 → 100 % confidence risk_unit = 0.01 # 1 % of equity frac_kelly = 0.5 * edge # half‑Kelly stake_raw = frac_kelly * risk_unit / (ATR$/Close$) self.SetHoldings('AAPL', clip(stake_raw, -0.5, 0.5))
- Skip first 5 and last 30 minutes: avoid open/close gapping.
- Liquidate on
abs(edge) < 0.15
⇒ reduces churn in noise.
Live Execution Loop
Predictions throttled every 5 minutes to align with feature horizon and cut QC data costs. Public logs show edge, target, and realised P/L per tick:

Debug tip: dumping self.Debug()
into QC logs throttles after 10 MB; switched to Log.py
to persist detailed telemetry to AWS.
Lessons Learned
- MLP beats LSTM here: latency 1ms vs 20ms with no accuracy loss 🤯.
- RSI slope raw RSI; derivative captures tempo, not stretched scale.
- ATR normalisation makes Kelly sizing regime‑agnostic (½ June ’24 memestock volume ≠ August ’24 dog days).
- Walk‑forward makes backtest metrics noisy; use PSR (Probabilistic Sharpe Ratio) with bootstraps rather than mean Sharpe.
Next → Ideas
- Switch to UniverseSelection of top‑3 NASDAQ by ADV; retrain multi‑symbol head.
- Add
expanding_window_backtester.py
to benchmark walk‑forward vs static. - Shove predictions into a
n>1
output «long & short» multi‑task net to trade pairs. - Quantify latency: feed QC live ticks to a self‑hosted FastAPI, compare vs built‑in
Algorithm
.