A hedge fund that runs on crowd-sourced ML models. Every week, thousands of data scientists submit predictions on obfuscated financial data. Numerai uses the best…
Connected to the competition feed. Verify dates, eligibility and prizes on the official page.
No open Numerai competitions in the feed right now.
The feed refreshes daily. Browse every tracked platform or check again soon.
Subscribe once — every open competition's deadline shows up in your native calendar and auto-updates daily as new ones open and old ones close. No account needed.
Outlook or another app: copy the feed URL, then add a calendar from URL.
A hedge fund that runs on crowd-sourced ML models. Every week, thousands of data scientists submit predictions on obfuscated financial data. Numerai uses the best predictions to trade real money. You stake NMR (their crypto token) on your model and earn more if you're right — or lose if you're wrong.
feature_0, feature_1, etc.)# pip install numerapi
import numerapi
napi = numerapi.NumerAPI()
# Download training data
napi.download_dataset("v5.0/train.parquet", "train.parquet")
napi.download_dataset("v5.0/validation.parquet", "validation.parquet")
napi.download_dataset("v5.0/live.parquet", "live.parquet") # this week's live data
| Concept | What It Is | Why It Matters |
|---|---|---|
| Era | One time period (a "week" of stock data) | Never random-split across eras — data is temporally correlated |
| Feature neutralization | Remove market-correlated features from predictions | Reduces systemic risk; Numerai rewards unique signals |
| Era boosting | Upweight hard eras during training | Prevents overfitting to easy periods |
| CORR | Main scoring metric (Spearman correlation with targets) | Your primary signal quality measure |
| MMC | Meta Model Contribution — bonus for unique predictions | Where the real edge is; target this |
| FNC | Feature Neutral Correlation — CORR after neutralization | More stable than raw CORR |
| TC | True Contribution to Numerai's portfolio | Advanced metric used in payouts |
Never use random k-fold. Eras are correlated across time — random splits cause massive data leakage.
import pandas as pd
import numpy as np
from lightgbm import LGBMRegressor
# Load data
train = pd.read_parquet("train.parquet")
# Identify feature columns and target
feature_cols = [c for c in train.columns if c.startswith("feature_")]
target_col = "target"
era_col = "era"
# Era-aware cross-validation: hold out entire eras as test sets
def era_cv_split(df, n_splits=5):
"""Split by era groups, not individual rows."""
eras = df[era_col].unique()
era_groups = np.array_split(eras, n_splits)
for i, test_eras in enumerate(era_groups):
train_eras = np.concatenate([era_groups[j] for j in range(n_splits) if j != i])
train_idx = df[df[era_col].isin(train_eras)].index
test_idx = df[df[era_col].isin(test_eras)].index
yield train_idx, test_idx
oof_preds = np.zeros(len(train))
models = []
for fold, (tr_idx, val_idx) in enumerate(era_cv_split(train, n_splits=5)):
X_tr = train.loc[tr_idx, feature_cols]
y_tr = train.loc[tr_idx, target_col]
X_val = train.loc[val_idx, feature_cols]
y_val = train.loc[val_idx, target_col]
model = LGBMRegressor(
n_estimators=2000,
learning_rate=0.01,
num_leaves=31,
colsample_bytree=0.1, # feature subsampling — crucial for Numerai
subsample=0.5,
n_jobs=-1
)
model.fit(X_tr, y_tr, eval_set=[(X_val, y_val)], callbacks=[lgb.early_stopping(100)])
oof_preds[val_idx] = model.predict(X_val)
models.append(model)
# Score each fold (Spearman correlation per era)
fold_corr = train.loc[val_idx].groupby(era_col).apply(
lambda g: g[target_col].corr(pd.Series(oof_preds[g.index], index=g.index), method='spearman')
).mean()
print(f"Fold {fold+1} mean CORR: {fold_corr:.4f}")
def compute_era_weights(df, predictions, alpha=0.5):
"""
Upweight eras where the model performed worst.
alpha: how strongly to upweight (0 = uniform, 1 = max upweighting)
"""
era_scores = df.groupby(era_col).apply(
lambda g: g[target_col].corr(
pd.Series(predictions[g.index], index=g.index), method='spearman'
)
)
# Invert and normalize: worst eras get highest weight
era_weights = 1 / (era_scores.rank() + 1)
era_weights = alpha * era_weights / era_weights.max() + (1 - alpha)
return df[era_col].map(era_weights).values
After making predictions, neutralize against the "example predictions" (Numerai's own model) to ensure your signal adds unique information.
def neutralize(predictions: pd.Series, factors: pd.DataFrame, proportion=1.0):
"""
Remove the linear component of 'factors' from 'predictions'.
Use proportion < 1.0 to partially neutralize.
"""
from numpy.linalg import lstsq
predictions = predictions.values.reshape(-1, 1)
factors = factors.values
# Fit linear regression: predictions ~ factors
loadings, _, _, _ = lstsq(factors, predictions, rcond=None)
# Residual = predictions - factors @ loadings (neutralized)
neutralized = predictions - proportion * (factors @ loadings)
return pd.Series(neutralized.flatten())
# Usage:
example_preds = pd.read_parquet("v5.0/validation_example_preds.parquet")
neutralized_preds = neutralize(
predictions=your_predictions,
factors=example_preds[['prediction']],
proportion=0.5 # 50% neutralization — tune this
)
MMC rewards predictions that add value BEYOND what Numerai's meta-model already captures. High MMC is worth more than high CORR.
Rule of thumb: If your model is very similar to publicly shared Numerai models, your MMC will be low even if CORR is high.
To maximize MMC:
# Train on different targets and blend
TARGETS = ['target_cyrus_v4_20', 'target_victor_v4_20', 'target_ralph_v4_20']
target_preds = []
for tgt in TARGETS:
model = LGBMRegressor(n_estimators=1000, learning_rate=0.01, num_leaves=31)
model.fit(X_train, train[tgt])
target_preds.append(model.predict(X_live))
# Simple blend — diversifies your signal across targets
blended_pred = np.mean(target_preds, axis=0)
import numerapi
napi = numerapi.NumerAPI()
# Generate live predictions
live = pd.read_parquet("live.parquet")
X_live = live[feature_cols]
# Average all fold models for robustness
live_preds = np.mean([m.predict(X_live) for m in models], axis=0)
# Rank transform (Numerai prefers rank-normalized predictions)
live_preds_ranked = pd.Series(live_preds).rank(pct=True)
# Save and upload
submission = pd.DataFrame({
'id': live.index,
'prediction': live_preds_ranked
})
submission.to_csv("submission.csv", index=False)
# Upload to your model slot
napi.upload_predictions("submission.csv", model_id="your-model-id")
With 2,400 features, selection is crucial:
# Method 1: Use Numerai's feature metadata
feature_metadata = pd.read_json("features.json")
# Select "small" feature set (Numerai provides curated subsets)
small_features = feature_metadata[feature_metadata['feature_set'] == 'small'].index.tolist()
# Method 2: Select features with highest feature-era correlation stability
# (features that work consistently across eras, not just on average)
def feature_era_corr(df, feature, target):
return df.groupby(era_col).apply(
lambda g: g[feature].corr(g[target], method='spearman')
)
era_corrs = {f: feature_era_corr(train, f, target_col) for f in feature_cols[:100]}
# Keep features with consistently positive (not just high mean) era correlation
stable_features = [f for f, corrs in era_corrs.items() if (corrs > 0).mean() > 0.55]
colsample_bytree=0.1 in LightGBM — Numerai features are noisy; aggressive feature subsampling helps| Tool | Use |
|---|---|
numerapi | Official Python API for downloading data and uploading predictions |
lightgbm | Primary model — fast, strong, handles feature noise well |
catboost | Good alternative; handles missing values natively |
scipy.stats.spearmanr | Manual CORR computation |
mlfinlab | Purged CV implementation for advanced splitting |
| Resource | URL |
|---|---|
| Official docs | docs.numer.ai |
| Community forum | forum.numer.ai |
| Numerai GitHub | github.com/numerai |
| "Supermastery" notebook (public starter) | kaggle.com/code numerai tag |
| NumerBay (buy/sell signals) | numerbay.ai |