Spaces:
Sleeping
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: | |
| 1. Computes 20-bar and 50-bar simple moving averages on NIFTY close prices | |
| 2. Goes long (+1) when fast SMA > slow SMA, short (-1) when fast < slow | |
| 3. Stays flat (0) during warmup (first 50 bars) | |
| 4. Uses next-bar execution (signal at bar i β position at bar i+1) | |
| 5. Returns only rows where position changes | |
| ### How It's Executed | |
| 1. Agent sends code via `submit_code` β server checks syntax, scans for `import os` / `.shift(-1)` etc. | |
| 2. Agent calls `run_backtest` β server writes code to a temp file, runs it in a **subprocess** with 30s timeout | |
| 3. The subprocess calls `generate_trades(merged_df)` and saves the resulting DataFrame | |
| 4. 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)` | |
| ### 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: | |
| 1. Computes spread = `nifty_close - 0.35 * banknifty_close` | |
| 2. Computes Z-score with rolling(60) mean/std (ddof=1) | |
| 3. Enters short spread (nifty=-1, banknifty=+1) when z > 2.0 | |
| 4. Enters long spread (nifty=+1, banknifty=-1) when z < -2.0 | |
| 5. Exits (both to 0) when |z| < 0.5 | |
| 6. After each exit, enforces **30-bar cooldown** before next entry | |
| 7. Uses next-bar execution | |
| 8. 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) | |
| ### 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: | |
| 1. Explore the data to find patterns | |
| 2. Hypothesize and implement trading strategies | |
| 3. Combine multiple signals into a hedged portfolio | |
| 4. Ensure net exposure stays β€ 80% at all times | |
| 5. Avoid lookahead (no future data leakage) | |
| 6. 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): | |
| 1. **Runtime lookahead detection**: Server loads the agent's code via `importlib`, runs `generate_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 | |
| 2. **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 | |
| 3. 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 | | |