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: predicAssistantStep 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