Spaces:
Sleeping
Quant Research Environment β Task & Evaluation Guide
Task 1: Easy β SMA Crossover Implementation
What the Agent Receives on reset()
- A description telling it to implement SMA(20)/SMA(50) crossover on
Close_nifty - The merged DataFrame (737,587 rows of minute-bar OHLC for both NIFTY and BANKNIFTY)
- The exact function signature:
generate_trades(df) -> DataFrame[bar, position]
What the Agent Must Do
Write a Python function that:
- Computes 20-bar and 50-bar simple moving averages on NIFTY close prices
- Goes long (+1) when fast SMA > slow SMA, short (-1) when fast < slow
- Stays flat (0) during warmup (first 50 bars)
- Uses next-bar execution (signal at bar i β position at bar i+1)
- Returns only rows where position changes
How It's Executed
- Agent sends code via
submit_codeβ server checks syntax, scans forimport os/.shift(-1)etc. - Agent calls
run_backtestβ server writes code to a temp file, runs it in a subprocess with 30s timeout - The subprocess calls
generate_trades(merged_df)and saves the resulting DataFrame - Server loads the result, passes it to
backtester.replay_trades_single()which replays bar-by-bar:- At each bar:
pnl = position * (close[i] - close[i-1]) - On position change: deduct
0.00009 * price * |position_change|(transaction cost) - Track cumulative PnL, peak, max drawdown
- Sharpe =
mean(bar_pnls) / std(bar_pnls) * sqrt(252 * 375)
- At each bar:
How the Score Is Computed
The grader compares backtest metrics against ground truth from the Harbor eval:
| Metric | Ground Truth | Tolerance |
|---|---|---|
| Trade count | 16,639 | Β±5% (15,807 to 17,471) |
| Total PnL | -20,630.75 | Β±2% |
| Max drawdown | 23,882.23 | Β±2% |
| Sharpe | -1.3693 | Β±0.05 absolute |
Scoring ladder (each level requires passing all previous checks):
| Score | Condition |
|---|---|
| 0.00 | Code doesn't parse (syntax error) |
| 0.05 | Parses but no generate_trades() function found |
| 0.10 | Has generate_trades() function |
| 0.20 | Code runs without crashing |
| 0.30 | Output has correct columns [bar, position] |
| 0.45 | Produces actual trades (not all zeros) |
| 0.60 | Trade count within 20% of 16,639 |
| 0.75 | Trade count within 5% AND Sharpe within 1.0 of target |
| 0.90 | Sharpe within 0.5 of target (-1.37) |
| 1.00 | ALL four metrics within their tolerances |
Real LLM result (Gemini Flash Lite): First attempt crashed (score 0.10), revised code produced 16,657 trades and Sharpe -1.3683 β all within tolerance β 1.00
Task 2: Medium β Pairs Trading with Constraints
What the Agent Receives on reset()
- A description with the Z-score pairs trading spec including pseudocode
- Two separate DataFrames (NIFTY and BANKNIFTY, aligned)
- Function signature:
generate_trades(nifty_df, banknifty_df) -> DataFrame[bar, nifty_position, banknifty_position]
What the Agent Must Do
Write a function that:
- Computes spread =
nifty_close - 0.35 * banknifty_close - Computes Z-score with rolling(60) mean/std (ddof=1)
- Enters short spread (nifty=-1, banknifty=+1) when z > 2.0
- Enters long spread (nifty=+1, banknifty=-1) when z < -2.0
- Exits (both to 0) when |z| < 0.5
- After each exit, enforces 30-bar cooldown before next entry
- Uses next-bar execution
- Positions always exactly +1, -1, or 0
How It's Executed
Same as Easy, but:
- Subprocess calls
generate_trades(nifty_df, banknifty_df)(two DataFrames instead of one) - Server uses
backtester.replay_trades_multi()which tracks both legs:- PnL =
nifty_pos * nifty_price_change + bn_pos * bn_price_change - Transaction cost applied per leg independently
- Exposure ratio checked at every bar:
|net_notional| / gross_notional - Violations recorded when ratio > 0.85 (0.80 limit + 0.05 tolerance buffer)
- PnL =
How the Score Is Computed
Ground truth:
| Metric | Ground Truth | Tolerance |
|---|---|---|
| Trades per leg | 18,815 | Β±5% |
| Spread entries | 9,408 | Β±5% |
| Total PnL | -60,226.70 | Β±2% |
| Max drawdown | 60,677.26 | Β±2% |
| Sharpe | -2.8732 | Β±0.05 absolute |
Scoring ladder:
| Score | Condition |
|---|---|
| 0.00 | Code doesn't parse |
| 0.05 | Parses but no generate_trades() |
| 0.10 | Has generate_trades() with correct signature |
| 0.20 | Code runs, produces DataFrame with 3 columns |
| 0.30 | Has non-zero positions in both instruments |
| 0.40 | Spread entries > 0 (z-score logic is directionally correct) |
| 0.55 | Trade count within 20% of 18,815 |
| 0.70 | Trade count within 10% AND Sharpe within 1.0 of target |
| 0.85 | ALL core metrics within 10% tolerance AND Sharpe within 0.5 |
| 1.00 | ALL metrics within spec tolerance + zero exposure violations |
Why models get 0.85 but not 1.00: The cooldown logic is the trap. The spec says "30-bar cooldown after exit, decrement every bar, allow entry on the bar counter reaches zero." Most LLMs get this slightly wrong β they might start counting from the wrong bar, or check cooldown before/after the z-score check in the wrong order. That produces ~18,551 trades instead of 18,815 β within 10% (scores 0.85) but outside 5% (misses 1.00).
Real LLM result (Gemini Flash Lite): Got 18,551 trades, Sharpe -2.7611 β within 10% but not 5% β 0.85
Task 3: Hard β Alpha Research
What the Agent Receives on reset()
- An open-ended description: "discover a hedged trading strategy that generates positive risk-adjusted returns"
- Same two DataFrames (NIFTY + BANKNIFTY training data, 2015-2022)
- Tips about strategy families (trend, mean-reversion, seasonality, volatility)
- Function signature:
generate_trades(nifty_df, banknifty_df) -> DataFrame[bar, nifty_position, banknifty_position] - Positions can be any float (not limited to Β±1/0)
What the Agent Must Do
No spec to follow. The agent must:
- Explore the data to find patterns
- Hypothesize and implement trading strategies
- Combine multiple signals into a hedged portfolio
- Ensure net exposure stays β€ 80% at all times
- Avoid lookahead (no future data leakage)
- Produce positive Sharpe on data it has never seen
How It's Executed
On run_backtest (during the episode):
- Same subprocess execution and
replay_trades_multi()as Medium - Graded only on training data β maximum possible score during backtest is 0.30
On submit_final (the critical difference):
- Runtime lookahead detection: Server loads the agent's code via
importlib, runsgenerate_trades()on full data, then runs it again with 375/750/1500 bars removed from the end. If trades in the middle of the data change when future bars are removed β lookahead detected β capped at 0.20 - Out-of-sample evaluation: Server loads the hidden test data (2023-2026, 282,950 bars that the agent never sees) and runs
generate_trades()on it - OOS Sharpe is computed and mapped to the final score
How the Score Is Computed
No ground truth. The grading is a gate system followed by a piecewise linear mapping of OOS Sharpe.
Gates (must pass sequentially β failing any gate caps the score at that level):
| Score | Gate |
|---|---|
| 0.00 | Code doesn't parse |
| 0.10 | Code runs but output is wrong format |
| 0.12 | Output has correct columns |
| 0.15 | Reasonable position values |
| 0.20 | Exposure constraint satisfied (net β€ 80% at all bars) |
| 0.25 | No lookahead detected (static scan + truncation test passed) |
| 0.30 | Training Sharpe β₯ 0.25 (not a flat/random strategy) |
OOS Sharpe mapping (only reached if ALL gates pass):
| OOS Sharpe | Score | Interpretation |
|---|---|---|
| β€ -1.0 | 0.30 | Bad strategy, losing money |
| -1.0 to 0.0 | 0.30 β 0.50 | Gradually less bad (linear) |
| 0.0 | 0.50 | Breakeven β this alone is a strong result |
| 0.0 to 0.5 | 0.50 β 0.65 | Profitable (linear) |
| 0.5 to 1.0 | 0.65 β 0.80 | Good alpha |
| 1.0 to 1.5 | 0.80 β 0.90 | Strong alpha |
| 1.5 to 2.0 | 0.90 β 1.00 | Exceptional |
| β₯ 2.0 | 1.00 | World-class (human expert scored 2.784) |
Why This Task Is Genuinely Hard
In the RAETH Trading Eval, across 30 trials with 6 frontier models (Claude Opus 4.6, GPT-5.4, Gemini 3.1 Pro, GPT-5.3 Codex, Grok-4, Qwen3 Coder Next), only 1 out of 30 trials produced a positive OOS Sharpe ratio.
The constraint that makes it hard is the 80% net exposure limit. Without it, models achieve Sharpe > 2.0 by simply going 100% long (recalling from training data that Indian markets went up). With the hedging requirement, that free lunch disappears and models must discover genuinely profitable hedged strategies.
| Model | Best Trial | Average | Positive OOS? |
|---|---|---|---|
| Claude Opus 4.6 | -0.328 | -2.098 | No |
| Gemini 3.1 Pro | +0.419 | -2.724 | Yes (1 trial) |
| GPT-5.4 | -3.0 | -3.0 | No |
| GPT-5.3 Codex | -3.0 | -3.3 | No |
| Grok-4 | -3.0 | -3.6 | No |
| Qwen3 Coder Next | -5.0 | -5.0 | No |
| Human Expert | +2.784 | +2.784 | Yes |
Real LLM result (Gemini Flash Lite): Produced 737,213 trades with NaN PnL and Sharpe 0.0. Passed format and exposure checks (0.20), passed lookahead scan (0.25), but training Sharpe was 0.0 < 0.25 threshold β capped at 0.25. Never reached OOS evaluation.
Summary: Score Distribution Across Tasks
| Task | What It Tests | Expected Score Range | Perfect Score Requires |
|---|---|---|---|
| Easy | Can the LLM write correct pandas code from a spec? | 0.4 β 1.0 | Exact metric match to ground truth |
| Medium | Can it handle stateful, constraint-heavy logic? | 0.2 β 0.85 | Perfect cooldown logic + all tolerances |
| Hard | Can it discover profitable strategies autonomously? | 0.1 β 0.3 (typical) | OOS Sharpe > 2.0 on hidden 2023-2026 data |