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 |