Agent Conversation: anthropic/claude-opus-4.6

Model: anthropic/claude-opus-4.6
Total steps: 5
Messages: 11
UserStep 0

Task: quant-stock-prediction

Quantitative Stock Prediction on CSI300

Objective

Design and implement a stock prediction model that forecasts next-day returns for CSI300 stocks. Your code goes in custom_model.py. Three reference implementations (LightGBM, LSTM, Transformer) are provided as read-only.

Evaluation

Signal quality: IC, ICIR, Rank IC. Portfolio (TopkDropout, top 50, drop 5): Annualized Return, Max Drawdown, Information Ratio. Evaluation is automatic via qlib's workflow.

Workflow Configuration

workflow_config.yaml lines 13-25 and 31-44 are editable. This is the model plus input-adapter/preprocessor block: you may change the dataset class (e.g., to TSDatasetH) or processors if your model needs a different input view. Instruments, date ranges, train/valid/test splits, and evaluation settings are fixed.

qlib/custom_model.py [EDITABLE — lines 16–103 only]

     1: # Custom stock prediction model for MLS-Bench
     2: #
     3: # EDITABLE section: CustomModel class with fit() and predict() methods.
     4: # FIXED sections: imports below.
     5: import numpy as np
     6: import pandas as pd
     7: import torch
     8: import torch.nn as nn
     9: import torch.nn.functional as F
    10: from qlib.model.base import Model
    11: from qlib.data.dataset import DatasetH
    12: from qlib.data.dataset.handler import DataHandlerLP
    13: 
    14: DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    15: 
    16: # =====================================================================
    17: # EDITABLE: CustomModel — implement your stock prediction model here
    18: # =====================================================================
    19: class CustomModel(Model):
    20:     """Custom stock prediction model.
    21: 
    22:     You must implement:
    23:         fit(dataset)    — train the model on the training data
    24:         predict(dataset, segment="test")return predictions as pd.Series
    25: 
    26:     The dataset is a qlib DatasetH with Alpha360 features (360 features per
    27:     stock per day). The 360 features come from 6 base features
    28:     (open/close/high/low/volume/vwap ratios) x 60 days of history.
    29: 
    30:     For temporal models, features can be reshaped:
    31:         x.reshape(N, 6, 60).permute(0, 2, 1) -> [N, 60, 6]
    32:     giving 60 time steps of 6 features each.
    33: 
    34:     Segments: "train", "valid", "test".
    35: 
    36:     Getting data from the dataset:
    37:         df_train = dataset.prepare("train", col_set=["feature", "label"],
    38:                                     data_key=DataHandlerLP.DK_L)
    39:         features = df_train["feature"]   # DataFrame: (n_samples, 360)
    40:         labels = df_train["label"]       # DataFrame: (n_samples, 1)
    41: 
    42:     The label is: Ref($close, -2) / Ref($close, -1) - 1
    43:     (i.e., the return from T+1 to T+2, predicted at time T)
    44: 
    45:     predic
AssistantStep 1edit
Tool Input
1{
2  "op": "replace",
3  "filename": "qlib/custom_model.py",
4  "start_line": 16,
5  "end_line": 103,
6  "content": "import copy, torch.optim as optim\n\n\nclass GRUAttn(nn.Module):\n    \"\"\"GRU with temporal attention pooling for stock features.\"\"\"\n    def __init__(self, d_feat=6, hid=64, n_layers=2, drop=0.0):\n        super().__init__()\n        self.d_feat = d_feat\n        self.gru = nn.GRU(d_feat, hid, n_layers, batch_first=True, dropout=drop)\n        self.att = nn.Linear(hid, 1)\n        self.fc = nn.Linear(hid, 1)\n    def forward(self, x):\n        x = x.reshape(len(x), self.d_feat, -1).permute(0, 2, 1)  # [N,T,F]\n        h, _ = self.gru(x)                                         # [N,T,H]\n        w = torch.softmax(self.att(h), dim=1)                      # [N,T,1]\n        return self.fc((h * w).sum(1)).squeeze()                    # [N]\n\n\nclass CustomModel(Model):\n    \"\"\"GRU-Attention with IC-MSE joint loss for stock prediction.\n    Novel: differentiable Pearson-IC loss term optimizes ranking quality.\"\"\"\n    def __init__(self):\n        super().__init__()\n        self.device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n        self.net = GRUAttn(d_feat=6, hid=64, n_layers=2, drop=0.0).to(self.device)\n        self.opt = optim.Adam(self.net.parameters(), lr=1e-3, weight_decay=1e-5)\n        self.n_epochs, self.bs, self.early_stop = 200, 2048, 20\n        self.lam = 1.0  # IC loss weight\n        self.fitted = False\n    @staticmethod\n    def _ic(p, t):\n        \"\"\"Differentiable Pearson correlation (IC).\"\"\"\n        pc, tc = p - p.mean(), t - t.mean()\n        return (pc * tc).sum() / (pc.norm() * tc.norm() + 1e-8)\n    def _loss(self, pred, label):\n        m = ~torch.isnan(label)\n        p, t = pred[m], label[m]\n        if len(p) < 2: return (p - t).pow(2).mean()\n        return (p - t).pow(2).mean() - self.lam * self._ic(p, t)\n    def _run(self, x, y, train):\n        self.net.train(train)\n        idx = np.arange(len(x))\n        if train: np.random.shuffle(idx)\n        total, cnt = 0.0, 0\n        cm = torch.enable_grad() if train else torch.no_grad()\n        with cm:\n            for s in range(0, len(idx), self.bs):\n                b = idx[s:s + self.bs]\n                xb = torch.FloatTensor(x[b]).to(self.device)\n                yb = torch.FloatTensor(y[b]).to(self.device)\n                out = self.net(xb)\n                loss = self._loss(out, yb)\n                if train:\n                    self.opt.zero_grad()\n                    loss.backward()\n                    torch.nn.utils.clip_grad_value_(self.net.parameters(), 3.0)\n                    self.opt.step()\n                total += loss.item() * len(b)\n                cnt += len(b)\n        return total / cnt\n    def fit(self, dataset):\n        df_t = dataset.prepare(\"train\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n        df_v = dataset.prepare(\"valid\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n        xt = np.nan_to_num(df_t[\"feature\"].values)\n        yt = np.nan_to_num(np.squeeze(df_t[\"label\"].values))\n        xv = np.nan_to_num(df_v[\"feature\"].values)\n        yv = np.nan_to_num(np.squeeze(df_v[\"label\"].values))\n        best_loss, best_ep = np.inf, 0\n        best_w = copy.deepcopy(self.net.state_dict())\n        for ep in range(self.n_epochs):\n            self._run(xt, yt, train=True)\n            vl = self._run(xv, yv, train=False)\n            if vl < best_loss:\n                best_loss, best_ep = vl, ep\n                best_w = copy.deepcopy(self.net.state_dict())\n            if ep - best_ep >= self.early_stop: break\n        self.net.load_state_dict(best_w)\n        self.fitted = True\n    def predict(self, dataset, segment=\"test\"):\n        if not self.fitted: raise ValueError(\"not fitted\")\n        x_df = dataset.prepare(segment, col_set=\"feature\", data_key=DataHandlerLP.DK_I)\n        x = np.nan_to_num(x_df.values)\n        self.net.eval()\n        parts = []\n        with torch.no_grad():\n            for s in range(0, len(x), self.bs):\n                xb = torch.FloatTensor(x[s:s + self.bs]).to(self.device)\n                parts.append(self.net(xb).reshape(-1).cpu().numpy())\n        return pd.Series(np.concatenate(parts), index=x_df.index)"
7}
Tool ResultStep 1
OK: Replaced lines 16..103 with 89 line(s) in qlib/custom_model.py. Editable range: 16–104.

[Current file: qlib/custom_model.py | editable: 16–104 | total: 104 lines]
    13| 
    14| DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    15| 
    16  import copy, torch.optim as optim
    17  
    18  
    19  class GRUAttn(nn.Module):
    20      """GRU with temporal attention pooling for stock features."""
    21      def __init__(self, d_feat=6, hid=64, n_layers=2, drop=0.0):
    22          super().__init__()
    23          self.d_feat = d_feat
    24          self.gru = nn.GRU(d_feat, hid, n_layers, batch_first=True, dropout=drop)
    25          self.att = nn.Linear(hid, 1)
    26          self.fc = nn.Linear(hid, 1)
    27      def forward(self, x):
    28          x = x.reshape(len(x), self.d_feat, -1).permute(0, 2, 1)  # [N,T,F]
    29          h, _ = self.gru(x)                                         # [N,T,H]
    30          w = torch.softmax(self.att(h), dim=1)     
AssistantStep 2test
Tool ResultStep 2
[Test #1] (2 tests remaining; call submit(n=1) to submit this result)

### csi300 (scripts/csi300.sh)
Signal analysis (csi300):
SIGNAL_METRIC IC=0.047638
SIGNAL_METRIC ICIR=0.360097
SIGNAL_METRIC Rank_IC=0.059284
SIGNAL_METRIC Rank_ICIR=0.464296
Portfolio analysis (csi300):
PORTFOLIO_METRIC annualized_return=0.073167
PORTFOLIO_METRIC max_drawdown=-0.093226
PORTFOLIO_METRIC information_ratio=1.213326

### csi100 (scripts/csi100.sh)
Signal analysis (csi100):
SIGNAL_METRIC IC=0.031912
SIGNAL_METRIC ICIR=0.162454
SIGNAL_METRIC Rank_IC=0.044360
SIGNAL_METRIC Rank_ICIR=0.232779
Portfolio analysis (csi100):
PORTFOLIO_METRIC annualized_return=-0.015853
PORTFOLIO_METRIC max_drawdown=-0.198724
PORTFOLIO_METRIC information_ratio=-0.259479

[Leaderboard] Results saved: {'ic_csi300': 0.047638, 'icir_csi300': 0.360097, 'rank_ic_csi300': 0.059284, 'rank_icir_csi300': 0.464296, 'annualized_return_csi300': 0.073167, 'max_drawdown_csi300': -0.093226, 'information_ratio_csi300': 1.213326, 'elapsed_csi300
AssistantStep 3edit
Tool Input
1{
2  "op": "replace",
3  "filename": "qlib/custom_model.py",
4  "start_line": 16,
5  "end_line": 104,
6  "content": "import copy, torch.optim as optim\n\n\nclass GRUAttn(nn.Module):\n    \"\"\"GRU with temporal attention and time-step masking.\"\"\"\n    def __init__(self, d_feat=6, hid=64, n_layers=2):\n        super().__init__()\n        self.d_feat = d_feat\n        self.gru = nn.GRU(d_feat, hid, n_layers, batch_first=True)\n        self.att = nn.Linear(hid, 1)\n        self.fc = nn.Linear(hid, 1)\n    def forward(self, x, mask_p=0.0):\n        x = x.reshape(len(x), self.d_feat, -1).permute(0, 2, 1)\n        if mask_p > 0 and self.training:\n            m = (torch.rand(x.shape[0], x.shape[1], 1, device=x.device) > mask_p).float()\n            x = x * m / (1 - mask_p)\n        h, _ = self.gru(x)\n        w = torch.softmax(self.att(h), dim=1)\n        return self.fc((h * w).sum(1)).squeeze()\n\n\nclass CustomModel(Model):\n    \"\"\"GRU-Attention with cross-sectional IC loss and temporal masking.\n    Novel: (1) date-aligned batching for true cross-sectional IC,\n    (2) temporal masking for robust feature learning.\"\"\"\n    def __init__(self):\n        super().__init__()\n        self.device = torch.device(\"cuda:0\" if torch.cuda.is_available() else \"cpu\")\n        self.net = GRUAttn(d_feat=6, hid=64, n_layers=2).to(self.device)\n        self.opt = optim.Adam(self.net.parameters(), lr=1e-3, weight_decay=5e-5)\n        self.n_epochs, self.early_stop, self.lam, self.acc = 200, 20, 2.0, 4\n        self.mask_p, self.fitted = 0.1, False\n    @staticmethod\n    def _ic(p, t):\n        pc, tc = p - p.mean(), t - t.mean()\n        return (pc * tc).sum() / (pc.norm() * tc.norm() + 1e-8)\n    def _group(self, df):\n        gs = []\n        for _, g in df.groupby(level=0):\n            x, y = np.nan_to_num(g[\"feature\"].values), np.nan_to_num(np.squeeze(g[\"label\"].values))\n            if y.ndim > 0 and len(y) >= 10: gs.append((x, y))\n        return gs\n    def _epoch(self, gs, train):\n        self.net.train(train)\n        if train: np.random.shuffle(gs)\n        total, cnt = 0.0, 0\n        if train: self.opt.zero_grad()\n        cm = torch.enable_grad() if train else torch.no_grad()\n        with cm:\n            for i, (x, y) in enumerate(gs):\n                xb = torch.FloatTensor(x).to(self.device)\n                yb = torch.FloatTensor(y).to(self.device)\n                pred = self.net(xb, mask_p=self.mask_p if train else 0.0)\n                raw = (pred - yb).pow(2).mean() - self.lam * self._ic(pred, yb)\n                if train:\n                    (raw / self.acc).backward()\n                    if (i + 1) % self.acc == 0 or i == len(gs) - 1:\n                        torch.nn.utils.clip_grad_value_(self.net.parameters(), 3.0)\n                        self.opt.step(); self.opt.zero_grad()\n                total += raw.item() * len(y); cnt += len(y)\n        return total / cnt\n    def fit(self, dataset):\n        df_t = dataset.prepare(\"train\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n        df_v = dataset.prepare(\"valid\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n        tg, vg = self._group(df_t), self._group(df_v)\n        best_loss, best_ep = np.inf, 0\n        best_w = copy.deepcopy(self.net.state_dict())\n        for ep in range(self.n_epochs):\n            self._epoch(tg, True)\n            vl = self._epoch(vg, False)\n            if vl < best_loss:\n                best_loss, best_ep = vl, ep\n                best_w = copy.deepcopy(self.net.state_dict())\n            if ep - best_ep >= self.early_stop: break\n        self.net.load_state_dict(best_w)\n        self.fitted = True\n    def predict(self, dataset, segment=\"test\"):\n        if not self.fitted: raise ValueError(\"not fitted\")\n        x_df = dataset.prepare(segment, col_set=\"feature\", data_key=DataHandlerLP.DK_I)\n        x, bs = np.nan_to_num(x_df.values), 4096\n        self.net.eval()\n        parts = []\n        with torch.no_grad():\n            for s in range(0, len(x), bs):\n                xb = torch.FloatTensor(x[s:s + bs]).to(self.device)\n                parts.append(self.net(xb).reshape(-1).cpu().numpy())\n        return pd.Series(np.concatenate(parts), index=x_df.index)"
7}
Tool ResultStep 3
OK: Replaced lines 16..104 with 87 line(s) in qlib/custom_model.py. Editable range: 16–102.

[Current file: qlib/custom_model.py | editable: 16–102 | total: 102 lines]
    13| 
    14| DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
    15| 
    16  import copy, torch.optim as optim
    17  
    18  
    19  class GRUAttn(nn.Module):
    20      """GRU with temporal attention and time-step masking."""
    21      def __init__(self, d_feat=6, hid=64, n_layers=2):
    22          super().__init__()
    23          self.d_feat = d_feat
    24          self.gru = nn.GRU(d_feat, hid, n_layers, batch_first=True)
    25          self.att = nn.Linear(hid, 1)
    26          self.fc = nn.Linear(hid, 1)
    27      def forward(self, x, mask_p=0.0):
    28          x = x.reshape(len(x), self.d_feat, -1).permute(0, 2, 1)
    29          if mask_p > 0 and self.training:
    30              m = (torch.rand(x.shape[0], x.shape[1], 1, device=x.device) > mask_p).float()
    31              x = 
AssistantStep 4test
Tool ResultStep 4
[Test #2] (1 test remaining; call submit(n=2) to submit this result)

### csi300 (scripts/csi300.sh)
Signal analysis (csi300):
SIGNAL_METRIC IC=0.040032
SIGNAL_METRIC ICIR=0.270015
SIGNAL_METRIC Rank_IC=0.057510
SIGNAL_METRIC Rank_ICIR=0.418670
Portfolio analysis (csi300):
PORTFOLIO_METRIC annualized_return=0.082516
PORTFOLIO_METRIC max_drawdown=-0.084643
PORTFOLIO_METRIC information_ratio=1.109254

### csi100 (scripts/csi100.sh)
Signal analysis (csi100):
SIGNAL_METRIC IC=0.040499
SIGNAL_METRIC ICIR=0.204234
SIGNAL_METRIC Rank_IC=0.048282
SIGNAL_METRIC Rank_ICIR=0.257571
Portfolio analysis (csi100):
PORTFOLIO_METRIC annualized_return=-0.007057
PORTFOLIO_METRIC max_drawdown=-0.124315
PORTFOLIO_METRIC information_ratio=-0.119202

[Leaderboard] Results saved: {'ic_csi300': 0.040032, 'icir_csi300': 0.270015, 'rank_ic_csi300': 0.05751, 'rank_icir_csi300': 0.41867, 'annualized_return_csi300': 0.082516, 'max_drawdown_csi300': -0.084643, 'information_ratio_csi300': 1.109254, 'elapsed_csi300': 
AssistantStep 5submit
Tool Input
1{
2  "n": 1
3}
Tool ResultStep 5
[submit] Submitting result from test #1 as final.

### csi300 (scripts/csi300.sh)
Signal analysis (csi300):
SIGNAL_METRIC IC=0.047638
SIGNAL_METRIC ICIR=0.360097
SIGNAL_METRIC Rank_IC=0.059284
SIGNAL_METRIC Rank_ICIR=0.464296
Portfolio analysis (csi300):
PORTFOLIO_METRIC annualized_return=0.073167
PORTFOLIO_METRIC max_drawdown=-0.093226
PORTFOLIO_METRIC information_ratio=1.213326

### csi100 (scripts/csi100.sh)
Signal analysis (csi100):
SIGNAL_METRIC IC=0.031912
SIGNAL_METRIC ICIR=0.162454
SIGNAL_METRIC Rank_IC=0.044360
SIGNAL_METRIC Rank_ICIR=0.232779
Portfolio analysis (csi100):
PORTFOLIO_METRIC annualized_return=-0.015853
PORTFOLIO_METRIC max_drawdown=-0.198724
PORTFOLIO_METRIC information_ratio=-0.259479

### csi300_recent (scripts/csi300_recent.sh)
Signal analysis (csi300_recent):
SIGNAL_METRIC IC=0.030860
SIGNAL_METRIC ICIR=0.242499
SIGNAL_METRIC Rank_IC=0.047672
SIGNAL_METRIC Rank_ICIR=0.362749
Portfolio analysis (csi300_recent):
PORTFOLIO_METRIC annualized_return=0.087018
PORTF