quant-research-env / docs /task_evaluation_guide.md
yobro4619's picture
Upload folder using huggingface_hub
74d799b verified
|
Raw
History Blame Contribute Delete
10 kB

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