Spaces:
Sleeping
Sleeping
File size: 10,026 Bytes
74d799b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 | # 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 |
|