Agent Conversation: anthropic/claude-opus-4.6
Task: quant-concept-drift
Concept Drift Adaptation in Stock Prediction on CSI300
Objective
Design and implement a stock prediction model that handles concept drift and temporal distribution shift in CSI300 stocks. Your code goes in custom_model.py. Three reference implementations (TRA, AdaRNN, LightGBM) are provided as read-only.
Evaluation
Signal quality: IC, ICIR, Rank IC. Portfolio (TopkDropout, top 50, drop 5): Annualized Return, Max Drawdown, Information Ratio. Automatic via qlib's workflow.
Evaluation uses three fixed temporal regimes on the same CSI300 universe:
csi300: long-horizon split ending in the 2017-2020 regimecsi300_shifted: shifted split with a 2016-2018 test regimecsi300_recent(hidden): the most recent 2019-2020 regime
This task is about temporal drift adaptation, not cross-universe transfer.
Workflow Configuration
workflow_config.yaml lines 13-26 and 32-45 are editable. This covers the model plus dataset adapter/processor configuration needed by methods like TRA. 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 (concept drift adaptation)
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 for concept drift adaptation.
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 Alpha158 features (158 engineered
27: features per stock per day). Alpha158 computes rolling statistics over
28: multiple windows (5, 10, 20, 30, 60 days) from raw OHLCV data:
29: - Rolling mean, std, and max/min of returns and volume
30: - Momentum indicators (ROC at various horizons)
31: - K-line ratios (KLEN, KLOW, KSFT, etc.)
32: - Rolling correlation/covariance between price and volume (CORR, CORD)
33: - Volatility measures (VSTD, WVMA, residual-based RESI, RSQR)
34: Features are pre-normalized (RobustZScoreNorm) and NaN-filled.
35:
Tool Input
1{
2 "op": "replace",
3 "filename": "qlib/custom_model.py",
4 "start_line": 16,
5 "end_line": 103,
6 "content": "# =====================================================================\n# EDITABLE: CustomModel — implement your stock prediction model here\n# =====================================================================\nimport lightgbm as lgb\n\n\nclass CustomModel(Model):\n \"\"\"Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.\n\n Novel contributions:\n 1. Distribution-calibrated sample weighting based on quarterly feature-space\n similarity to the validation period (targets concept drift directly)\n 2. Synergistic temporal-distributional weight product for ensemble diversity\n 3. Validation IC-weighted ensemble aggregation across drift-aware members\n \"\"\"\n\n def __init__(self):\n super().__init__()\n self.models, self.ensw, self.fitted = [], [], False\n self.params = {\n \"objective\": \"mse\", \"colsample_bytree\": 0.8879, \"learning_rate\": 0.2,\n \"subsample\": 0.8789, \"lambda_l1\": 205.6999, \"lambda_l2\": 580.9768,\n \"max_depth\": 8, \"num_leaves\": 210, \"num_threads\": 20, \"verbosity\": -1,\n }\n\n def _date_ic(self, preds, labels, index):\n \"\"\"Compute mean daily Information Coefficient (Pearson).\"\"\"\n df = pd.DataFrame({\"p\": preds, \"l\": labels}, index=index)\n ics = [g[\"p\"].corr(g[\"l\"]) for _, g in df.groupby(level=0) if len(g) > 5]\n return np.nanmean(ics) if ics else 0.0\n\n def _dist_weights(self, x_np, dates, x_va_np):\n \"\"\"Novel: per-sample weights from quarterly feature distribution drift.\n\n Computes standardized mean distance between each training quarter's\n feature distribution and the validation distribution, then converts\n to exponential similarity scores as sample weights.\"\"\"\n va_mu = np.nanmean(x_va_np, axis=0)\n va_sd = np.nanstd(x_va_np, axis=0) + 1e-8\n qtrs = dates.to_period('Q')\n qd = {}\n for q in qtrs.unique():\n m = qtrs == q\n qd[q] = np.mean(((np.nanmean(x_np[m], axis=0) - va_mu) / va_sd) ** 2)\n d = np.array([qd[q] for q in qtrs])\n return np.exp(-d / (np.median(d) + 1e-8))\n\n def fit(self, dataset: DatasetH):\n df_tr = dataset.prepare(\"train\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n df_va = dataset.prepare(\"valid\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n x_tr, y_tr = df_tr[\"feature\"], df_tr[\"label\"].values.ravel()\n x_va, y_va = df_va[\"feature\"], df_va[\"label\"].values.ravel()\n dates = x_tr.index.get_level_values(0)\n ud = sorted(dates.unique()); nd = len(ud)\n d2i = {d: i for i, d in enumerate(ud)}\n si = np.array([d2i[d] for d in dates])\n # Four weighting schemes: uniform, temporal, distributional, synergistic\n w_u = np.ones(len(x_tr))\n w_e = np.exp(-(nd - 1 - si) * np.log(2) / (nd * 0.35))\n w_d = self._dist_weights(x_tr.values, dates, x_va.values)\n w_c = w_e * w_d # Novel: multiplicative temporal x distributional\n self.models, ics = [], []\n for w in [w_u, w_e, w_d, w_c]:\n w = w / (w.mean() + 1e-8)\n dt = lgb.Dataset(x_tr.values, label=y_tr, weight=w, free_raw_data=False)\n dv = lgb.Dataset(x_va.values, label=y_va, free_raw_data=False)\n m = lgb.train(self.params, dt, num_boost_round=1000,\n valid_sets=[dt, dv], valid_names=[\"train\", \"valid\"],\n callbacks=[lgb.early_stopping(50), lgb.log_evaluation(200)])\n self.models.append(m)\n ics.append(self._date_ic(m.predict(x_va.values), y_va, x_va.index))\n ic = np.clip(np.array(ics), 0, None)\n self.ensw = ic / (ic.sum() + 1e-8) if ic.sum() > 0 else np.ones(4) / 4\n self.fitted = True\n\n def predict(self, dataset: DatasetH, segment=\"test\"):\n if not self.fitted:\n raise ValueError(\"Not fitted!\")\n x = dataset.prepare(segment, col_set=\"feature\", data_key=DataHandlerLP.DK_I)\n p = sum(w * m.predict(x.values) for w, m in zip(self.ensw, self.models))\n return pd.Series(p, index=x.index, name=\"score\")\n"
7}OK: Replaced lines 16..103 with 81 line(s) in qlib/custom_model.py. Editable range: 16–96.
[Current file: qlib/custom_model.py | editable: 16–96 | total: 96 lines]
13|
14| DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
15|
16 # =====================================================================
17 # EDITABLE: CustomModel — implement your stock prediction model here
18 # =====================================================================
19 import lightgbm as lgb
20
21
22 class CustomModel(Model):
23 """Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.
24
25 Novel contributions:
26 1. Distribution-calibrated sample weighting based on quarterly feature-space
27 similarity to the validation period (targets concept drift directly)
28 2. Synergistic temporal-distributional weight product for ensemble diversity
29 3. Validation IC-weighted ensemb
[Test #1] (2 tests remaining; call submit(n=1) to submit this result)
### csi300 (scripts/csi300.sh)
Signal analysis (csi300):
SIGNAL_METRIC IC=0.043266
SIGNAL_METRIC ICIR=0.323401
SIGNAL_METRIC Rank_IC=0.055553
SIGNAL_METRIC Rank_ICIR=0.425731
Portfolio analysis (csi300):
PORTFOLIO_METRIC annualized_return=0.102461
PORTFOLIO_METRIC max_drawdown=-0.055109
PORTFOLIO_METRIC information_ratio=1.420095
### csi300_shifted (scripts/csi300_shifted.sh)
Signal analysis (csi300_shifted):
SIGNAL_METRIC IC=0.054324
SIGNAL_METRIC ICIR=0.407661
SIGNAL_METRIC Rank_IC=0.065434
SIGNAL_METRIC Rank_ICIR=0.500455
Portfolio analysis (csi300_shifted):
PORTFOLIO_METRIC annualized_return=0.157499
PORTFOLIO_METRIC max_drawdown=-0.087505
PORTFOLIO_METRIC information_ratio=1.840586
[Leaderboard] Results saved: {'ic_csi300': 0.043266, 'icir_csi300': 0.323401, 'rank_ic_csi300': 0.055553, 'rank_icir_csi300': 0.425731, 'annualized_return_csi300': 0.102461, 'max_drawdown_csi300': -0.055109, 'information_ratio_csi3
Tool Input
1{
2 "op": "replace",
3 "filename": "qlib/custom_model.py",
4 "start_line": 16,
5 "end_line": 96,
6 "content": "# =====================================================================\n# EDITABLE: CustomModel — implement your stock prediction model here\n# =====================================================================\nimport lightgbm as lgb\n\n\nclass CustomModel(Model):\n \"\"\"Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.\n\n Novel contributions:\n 1. Distribution-calibrated sample weighting via quarterly feature-space\n similarity to validation (targets concept drift directly)\n 2. Synergistic temporal x distributional weight product\n 3. Recent-emphasis model for capturing late-period patterns\n 4. ICIR-weighted adaptive-temperature softmax ensemble aggregation\n \"\"\"\n\n def __init__(self):\n super().__init__()\n self.models, self.ensw, self.fitted = [], [], False\n self.params = {\n \"objective\": \"mse\", \"colsample_bytree\": 0.8879, \"learning_rate\": 0.2,\n \"subsample\": 0.8789, \"lambda_l1\": 205.6999, \"lambda_l2\": 580.9768,\n \"max_depth\": 8, \"num_leaves\": 210, \"num_threads\": 20, \"verbosity\": -1,\n }\n\n def _icir(self, preds, labels, index):\n \"\"\"Compute ICIR (IC Information Ratio) for robust model ranking.\"\"\"\n df = pd.DataFrame({\"p\": preds, \"l\": labels}, index=index)\n ics = [g[\"p\"].corr(g[\"l\"]) for _, g in df.groupby(level=0) if len(g) > 5]\n ics = [x for x in ics if not np.isnan(x)]\n if len(ics) < 2:\n return 0.0\n return np.mean(ics) / (np.std(ics) + 1e-8)\n\n def _dist_weights(self, x_np, dates, x_va_np):\n \"\"\"Novel: per-sample weights from quarterly feature distribution drift.\n Maps each training quarter to an exponential similarity score based on\n standardized mean distance to the validation feature distribution.\"\"\"\n va_mu = np.nanmean(x_va_np, axis=0)\n va_sd = np.nanstd(x_va_np, axis=0) + 1e-8\n qtrs = dates.to_period('Q')\n qd = {}\n for q in qtrs.unique():\n m = qtrs == q\n qd[q] = np.mean(((np.nanmean(x_np[m], axis=0) - va_mu) / va_sd) ** 2)\n d = np.array([qd[q] for q in qtrs])\n return np.exp(-d / (np.median(d) + 1e-8))\n\n def fit(self, dataset: DatasetH):\n df_tr = dataset.prepare(\"train\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n df_va = dataset.prepare(\"valid\", col_set=[\"feature\", \"label\"], data_key=DataHandlerLP.DK_L)\n x_tr, y_tr = df_tr[\"feature\"], df_tr[\"label\"].values.ravel()\n x_va, y_va = df_va[\"feature\"], df_va[\"label\"].values.ravel()\n dates = x_tr.index.get_level_values(0)\n ud = sorted(dates.unique()); nd = len(ud)\n d2i = {d: i for i, d in enumerate(ud)}\n si = np.array([d2i[d] for d in dates])\n # Five weighting schemes for temporal ensemble diversity\n w_u = np.ones(len(x_tr))\n w_e = np.exp(-(nd - 1 - si) * np.log(2) / (nd * 0.35))\n w_d = self._dist_weights(x_tr.values, dates, x_va.values)\n w_c = w_e * w_d # Novel: synergistic temporal x distributional\n w_r = np.where(si >= int(nd * 0.6), 1.0, 0.05) # Recent 40% emphasis\n self.models, scores = [], []\n for w in [w_u, w_e, w_d, w_c, w_r]:\n w = w / (w.mean() + 1e-8)\n dt = lgb.Dataset(x_tr.values, label=y_tr, weight=w, free_raw_data=False)\n dv = lgb.Dataset(x_va.values, label=y_va, free_raw_data=False)\n m = lgb.train(self.params, dt, num_boost_round=1000,\n valid_sets=[dt, dv], valid_names=[\"train\", \"valid\"],\n callbacks=[lgb.early_stopping(50), lgb.log_evaluation(200)])\n self.models.append(m)\n scores.append(self._icir(m.predict(x_va.values), y_va, x_va.index))\n # Adaptive-temperature softmax from ICIR scores\n s = np.clip(np.array(scores), 0, None)\n if s.sum() > 0:\n tau = np.std(s) + 1e-8\n self.ensw = np.exp(s / tau)\n self.ensw /= self.ensw.sum()\n else:\n self.ensw = np.ones(5) / 5\n self.fitted = True\n\n def predict(self, dataset: DatasetH, segment=\"test\"):\n if not self.fitted:\n raise ValueError(\"Not fitted!\")\n x = dataset.prepare(segment, col_set=\"feature\", data_key=DataHandlerLP.DK_I)\n p = sum(w * m.predict(x.values) for w, m in zip(self.ensw, self.models))\n return pd.Series(p, index=x.index, name=\"score\")\n"
7}OK: Replaced lines 16..96 with 90 line(s) in qlib/custom_model.py. Editable range: 16–105.
[Current file: qlib/custom_model.py | editable: 16–105 | total: 105 lines]
13|
14| DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
15|
16 # =====================================================================
17 # EDITABLE: CustomModel — implement your stock prediction model here
18 # =====================================================================
19 import lightgbm as lgb
20
21
22 class CustomModel(Model):
23 """Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.
24
25 Novel contributions:
26 1. Distribution-calibrated sample weighting via quarterly feature-space
27 similarity to validation (targets concept drift directly)
28 2. Synergistic temporal x distributional weight product
29 3. Recent-emphasis model for capturing late-period patterns
30
[Test #2] (1 test remaining; call submit(n=2) to submit this result)
### csi300 (scripts/csi300.sh)
Signal analysis (csi300):
SIGNAL_METRIC IC=0.043840
SIGNAL_METRIC ICIR=0.329308
SIGNAL_METRIC Rank_IC=0.055905
SIGNAL_METRIC Rank_ICIR=0.430172
Portfolio analysis (csi300):
PORTFOLIO_METRIC annualized_return=0.108803
PORTFOLIO_METRIC max_drawdown=-0.071332
PORTFOLIO_METRIC information_ratio=1.511539
### csi300_shifted (scripts/csi300_shifted.sh)
Signal analysis (csi300_shifted):
SIGNAL_METRIC IC=0.053630
SIGNAL_METRIC ICIR=0.408133
SIGNAL_METRIC Rank_IC=0.063140
SIGNAL_METRIC Rank_ICIR=0.489801
Portfolio analysis (csi300_shifted):
PORTFOLIO_METRIC annualized_return=0.135064
PORTFOLIO_METRIC max_drawdown=-0.105245
PORTFOLIO_METRIC information_ratio=1.541432
[Leaderboard] Results saved: {'ic_csi300': 0.04384, 'icir_csi300': 0.329308, 'rank_ic_csi300': 0.055905, 'rank_icir_csi300': 0.430172, 'annualized_return_csi300': 0.108803, 'max_drawdown_csi300': -0.071332, 'information_ratio_csi300
Tool Input
1{
2 "op": "replace",
3 "filename": "qlib/custom_model.py",
4 "start_line": 42,
5 "end_line": 49,
6 "content": " def _date_ic(self, preds, labels, index):\n \"\"\"Compute mean daily IC for ensemble weight calibration.\"\"\"\n df = pd.DataFrame({\"p\": preds, \"l\": labels}, index=index)\n ics = [g[\"p\"].corr(g[\"l\"]) for _, g in df.groupby(level=0) if len(g) > 5]\n return np.nanmean(ics) if ics else 0.0"
7}OK: Replaced lines 42..49 with 5 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 # =====================================================================
17 # EDITABLE: CustomModel — implement your stock prediction model here
18 # =====================================================================
19 import lightgbm as lgb
20
21
22 class CustomModel(Model):
23 """Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.
24
25 Novel contributions:
26 1. Distribution-calibrated sample weighting via quarterly feature-space
27 similarity to validation (targets concept drift directly)
28 2. Synergistic temporal x distributional weight product
29 3. Recent-emphasis model for capturing late-period patterns
30
Tool Input
1{
2 "op": "replace",
3 "filename": "qlib/custom_model.py",
4 "start_line": 86,
5 "end_line": 94,
6 "content": " scores.append(self._date_ic(m.predict(x_va.values), y_va, x_va.index))\n # Proportional IC-weighted ensemble aggregation\n ic = np.clip(np.array(scores), 0, None)\n self.ensw = ic / (ic.sum() + 1e-8) if ic.sum() > 0 else np.ones(5) / 5"
7}OK: Replaced lines 86..94 with 4 line(s) in qlib/custom_model.py. Editable range: 16–97.
[Current file: qlib/custom_model.py | editable: 16–97 | total: 97 lines]
13|
14| DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
15|
16 # =====================================================================
17 # EDITABLE: CustomModel — implement your stock prediction model here
18 # =====================================================================
19 import lightgbm as lgb
20
21
22 class CustomModel(Model):
23 """Drift-Aware Ensemble with Distribution-Calibrated Temporal Weighting.
24
25 Novel contributions:
26 1. Distribution-calibrated sample weighting via quarterly feature-space
27 similarity to validation (targets concept drift directly)
28 2. Synergistic temporal x distributional weight product
29 3. Recent-emphasis model for capturing late-period patterns
30
[Test #3] (0 tests remaining) ### csi300 (scripts/csi300.sh) Signal analysis (csi300): SIGNAL_METRIC IC=0.042892 SIGNAL_METRIC ICIR=0.318607 SIGNAL_METRIC Rank_IC=0.055484 SIGNAL_METRIC Rank_ICIR=0.421731 Portfolio analysis (csi300): PORTFOLIO_METRIC annualized_return=0.060292 PORTFOLIO_METRIC max_drawdown=-0.113930 PORTFOLIO_METRIC information_ratio=0.842856 ### csi300_shifted (scripts/csi300_shifted.sh) Signal analysis (csi300_shifted): SIGNAL_METRIC IC=0.053972 SIGNAL_METRIC ICIR=0.400204 SIGNAL_METRIC Rank_IC=0.065776 SIGNAL_METRIC Rank_ICIR=0.497033 Portfolio analysis (csi300_shifted): PORTFOLIO_METRIC annualized_return=0.189268 PORTFOLIO_METRIC max_drawdown=-0.085920 PORTFOLIO_METRIC information_ratio=2.209840 ### csi300_recent (scripts/csi300_recent.sh) Signal analysis (csi300_recent): SIGNAL_METRIC IC=0.024103 SIGNAL_METRIC ICIR=0.168440 SIGNAL_METRIC Rank_IC=0.046124 SIGNAL_METRIC Rank_ICIR=0.321751 Portfolio analysis (csi300_recent): PORTFOLIO_METRIC annualized_return=0.05