Skip to main content

Winning My First Quant Competition: A SignalNet Story

· 5 min read
SignalNet Team
Building the Signal Network

A first-person account of competing in SignalNet's Genesis Round — from "I have no idea what I'm doing" to finishing in the top 10.

The Starting Line

I'd been lurking in quant Twitter for months. Reading papers about factor models. Watching people flex their Sharpe ratios. Feeling like I'd never be smart enough to compete.

Then I saw SignalNet's Genesis Round announcement. The pitch was simple: download a dataset, predict which stocks will outperform, stake some tokens, and get scored against reality. No PhD required. No Bloomberg terminal. Just your laptop and whatever edge you can find.

I signed up figuring I'd learn something, even if I finished last.

Day 1: The Data

The feature dataset was... opaque. 503 stocks, 98 features, all anonymized. No column names, no descriptions. Just feature_0 through feature_97 with values between 0 and 1.

My first reaction: How am I supposed to build a model when I don't even know what the features represent?

But that's the point. SignalNet encrypts the features so nobody has a data advantage. You can't just download the P/E ratio from Yahoo Finance and call it a day. You need to find signal in the structure of the data itself.

I started with the basics:

from signalnet import Client

client = Client(api_key="sk-...")
features = client.download_features(round_id=1)
train = client.get_training_data()

print(f"Features shape: {features.shape}") # (503, 98)
print(f"Training rounds: {len(train)}") # 30+ weeks of history

Day 2-3: The Naive Model

My first model was embarrassingly simple. Gradient boosting with default parameters:

from sklearn.ensemble import GradientBoostingRegressor

model = GradientBoostingRegressor(n_estimators=100)
model.fit(X_train, y_train)
predictions = model.predict(features)

I submitted it, staked 500 SIGNAL (the minimum was 100, but I wanted some skin in the game), and waited.

The provisional score after day 1: IC = +0.008

Positive, but barely above noise. Not terrible for a first try, but not going to win anything.

Day 4-5: Feature Engineering

I realized the raw features weren't enough. I started engineering:

  1. Feature interactions — multiplying pairs of features that had high individual predictive power
  2. Rolling statistics — since I had 30 weeks of training data, I could compute feature means and standard deviations across time
  3. Noise identification — I noticed ~13 features had near-zero correlation with the target across all training periods. I dropped them. (Later learned this was intentional — SignalNet adds noise features as part of the challenge.)
# Find and remove likely noise features
correlations = train_features.corrwith(train_targets)
noise_features = correlations[correlations.abs() < 0.01].index
clean_features = features.drop(columns=noise_features)

Day 6: The Breakthrough

The turning point came when I read about feature neutralization in the SignalNet docs. The idea: remove your predictions' correlation with the most common features so your signal is orthogonal to the crowd.

predictions = client.neutralize(predictions)

One line of code. My provisional IC jumped from +0.012 to +0.028 overnight.

Why? Because without neutralization, my model was just predicting "momentum stocks go up" — something every other contributor was also predicting. After neutralization, my model was expressing its unique view, which is what TC (True Contribution) rewards.

Day 7-10: The Ensemble

For my final submission, I combined three models:

  1. Gradient Boosting — good at finding nonlinear relationships
  2. Ridge Regression — good at regularized linear signal
  3. Random Forest — good at capturing interactions
pred_gb = model_gb.predict(features)
pred_ridge = model_ridge.predict(features)
pred_rf = model_rf.predict(features)

# Equal-weight ensemble
final = (pred_gb + pred_ridge + pred_rf) / 3
final = client.neutralize(final)

I staked 2,000 SIGNAL on the ensemble. More than my first attempt. I believed in it.

The Wait

Twenty trading days is a long time when you have money on the line.

I checked the provisional scores obsessively for the first week. They bounced around — +0.03 one day, +0.01 the next, -0.005 on a bad day. The docs warned that provisional scores are noisy, but knowing that intellectually and living it emotionally are different things.

By week two, I forced myself to stop checking daily and focused on building better models for Round 2.

The Results

When Round 1 resolved, I opened the leaderboard with my heart pounding.

Rank: #8 out of 47 contributors.

My scores:

  • IC: +0.034
  • TC: +0.021
  • MMC: +0.018
  • Final Score: +0.028

Payout: +1,400 SIGNAL on a 2,000 SIGNAL stake. A 70% return in one round.

What I Learned

1. Feature neutralization is not optional. Without it, you're competing to predict the most obvious signal. With it, you're competing to find what others miss.

2. Ensembles beat single models. My gradient booster alone scored +0.019. The ensemble scored +0.028. Diversification works at the model level too.

3. Noise features are a gift. Identifying and removing them gave my model a ~15% IC boost. They're a free puzzle that most people don't solve.

4. The provisional scores are noisy. Don't panic-rebuild your model on day 3 because the IC dipped negative. Wait for the full 20 days.

5. Staking focus matters. Concentrate your stake on your best model. I considered splitting across three models but went all-in on the ensemble. That was the right call.

What's Next

I'm hooked. Round 2 is live and I've already submitted. This time I'm experimenting with:

  • Feature selection using information-theoretic criteria
  • Sector-neutral predictions (not just feature-neutral)
  • A neural network as a fourth ensemble member

The quant competition I thought I'd never be smart enough for? Turns out you don't need to be smart. You need to be curious, systematic, and willing to let the data speak.

See you on the leaderboard.

— A Genesis Round contributor