--- license: mit library_name: pytorch pipeline_tag: time-series-forecasting tags: - time-series - time-series-forecasting - finance - gru - rnn - ensemble - pytorch --- # 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](reports/phase2_technical_report.md). ## 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 ```python 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](https://github.com/msrishav-28/wunder-fund-market-state-forecasting). ## License MIT. Trained only on the competition-provided data.