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.

#QuantConnect#Python#TensorFlow#Sklearn#Pandas#Kelly Sizing
Equity curve placeholder
View on QuantConnect →

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.
Feature importance placeholder

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:

Live logs placeholder

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

  1. Switch to UniverseSelection of top‑3 NASDAQ by ADV; retrain multi‑symbol head.
  2. Add expanding_window_backtester.py to benchmark walk‑forward vs static.
  3. Shove predictions into a n>1 output «long & short» multi‑task net to trade pairs.
  4. Quantify latency: feed QC live ticks to a self‑hosted FastAPI, compare vs built‑in Algorithm.