Wunder Fund RNN Challenge — Causal GRU Ensemble

A 5-member ensemble of causal GRUs that predicts the next 32-feature market state from a stream of prior states, built for the (finished) Wunder Fund market-state forecasting challenge. Inference is online (one row at a time), single-CPU-core, and deterministic.

📦 Code, training pipeline, and full write-up: https://github.com/msrishav-28/wunder-fund-market-state-forecasting

Results

Cross-validated R² (sequence-grouped folds, the leaderboard-comparable metric):

Model Dev CV mean R²
Ridge causal baseline 0.327
Single causal GRU (d256) 0.389
GRU ensemble (3×d256 + 2×d384) 0.3922

For reference, the competition's public leaderboard #1 was 0.3920 and the finals winner 0.3964; this ensemble's cross-validated 0.3922 matches the public top. Single-core inference is 4.17 ms/prediction (~32 min for the full ~517-sequence test, limit 60), and predictions are deterministic.

Why a GRU

The features are pre-whitened (≈N(0,1), no heavy tails) and mean-reverting (predicting the current state gives R² ≈ −0.37). A VAR(p) linear model saturates at ≈0.32, so the remaining signal is nonlinear temporal structure — captured by a unidirectional GRU that emits a next-state prediction at every step and carries its hidden state across the sequence (O(1) work per online prediction). Training and stepwise inference are numerically identical because a GRU is a pure recurrence. See the technical report.

Files

gru/d256_s{42,123,7}.pt   # 3 GRU members, d_model=256, 2 layers
gru/d384_s{42,123}.pt     # 2 GRU members, d_model=384, 2 layers
solution.py               # competition entry point (online PredictionModel)
src/                      # model + inference code
config/folds.json         # locked sequence-grouped CV folds
reports/                  # technical report

Each checkpoint stores model_state_dict + a config describing the architecture, so src/models/sequence_inference.py::load_gru_checkpoint rebuilds it without external metadata.

Usage

import numpy as np, torch
from src.models.sequence_inference import GRUStatefulPredictionModel, load_gru_checkpoint
from src.models.ensemble_predictor import EnsemblePredictionModel
from pathlib import Path

members = [GRUStatefulPredictionModel(load_gru_checkpoint(p))
           for p in sorted(Path("gru").glob("*.pt"))]
model = EnsemblePredictionModel(members)          # equal-weight average

class DP:  # minimal DataPoint
    def __init__(s, seq_ix, step, need, state):
        s.seq_ix, s.step_in_seq, s.need_prediction, s.state = seq_ix, step, need, state

# feed states one at a time; reset on a new seq_ix; predicts the NEXT state
for t in range(1000):
    pred = model.predict(DP(0, t, t >= 100, np.random.randn(32).astype("float32")))
    # pred is None during warm-up (steps 0..99), else an (32,) float32 vector

Or load the packaged solution.py directly (auto-discovers models/submission/gru/*.pt).

Training

Trained on an NVIDIA RTX 3050 (4 GB) at ~1.8 s/epoch; full-sequence BPTT with a loss masked to the scored steps. Reproduction commands are in the GitHub README.

License

MIT. Trained only on the competition-provided data.

Downloads last month

-

Downloads are not tracked for this model. How to track
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support