Why Write a Separate Technical Route Post

The previous post was more like a competition retrospective; this one is specifically to sort out the technical route for THU-BDC2026.

For this project, from the earliest deep model to the later LightGBM LambdaRank, the transition was not simply “switching models”; rather, my understanding of the problem changed: the stock ranking task is not just about predicting returns, but more like making cross-sectional comparisons among 300 stocks on the same day.

So afterward I focused more and more on three things:

  • the daily cross-sectional ranking;
  • the temporal separation between training and prediction;
  • whether the features can truly carry across different years.

Phase One: Transformer + Cross-Sectional Modeling

The earliest design was StockTransformer.

The input used data from the past 30 trading days; each stock had 72 technical indicators, organized overall as:

1
[batch, 300只股票, 30天, 72特征]

The model had several key modules:

  • TransformerEncoder: processes the time series of a single stock;
  • FeatureAttention: aggregates time-series information at different scales;
  • CrossStockAttention: lets stocks on the same day “see” each other;
  • MoE Ranking: uses multiple expert heads for ranking scores;
  • Score Head: outputs the final score for each stock.

The advantage of this design was strong expressiveness, and it also aligned with the intuition that stocks have relative relationships. The drawbacks were also obvious: slow training, high tuning cost, inconvenient backtest iteration, and the validation method was slightly problematic; complex models amplify the problem.

Phase Two: Turning to LambdaRank

Later I switched the main iteration to LightGBM’s lambdarank.

The reasons were practical:

First, the competition objective is essentially Top 5 ranking, and it is not necessarily about predicting an absolute return. Treating the 300 stocks each day as a group and directly optimizing NDCG@3 / NDCG@5 with a ranking objective is closer to the task.

Second, LightGBM trains much faster. A rolling backtest still takes a long time, but it is more suitable for repeated trial and error than a deep model.

Third, the feature importance of tree models is easier to inspect. It does not necessarily represent causality, but at least it lets me discover whether the model is being skewed by some weird type of feature.

Labels and Grouping

The label definition is:

1
(T+5 开盘价 - T+1 开盘价) / T+1 开盘价

That is, the model predicts the stage return from T+1 to T+5. To make different trading days comparable, I process cross-sectionally by date, then treat the stocks on the same day as one ranking group.

This is where problems most easily occur.

If the backtest date is bt_date, the training set cannot just be written as:

1
processed['日期'] < bt_date

Because for samples close to bt_date, their label may already use future opening prices. Later I changed it to leave at least a gap, to avoid the training set peeking at information near the prediction.

This step is more important than changing any model.

Feature Engineering

The features used in this project roughly fall into several categories:

Type Example Purpose
Trend/position ma_dev_60d, price_pos_20d Describe the price position relative to moving averages and the range
Candlestick pattern lower_shadow_ratio, upper_shadow_ratio Describe intraday buying and selling forces
Volume-price relationship corr_pv_20, obv_ratio Describe the relationship between volume and price changes
Cross-sectional rank *_cs_rank Make relative comparisons among 300 stocks on the same day
Market cap/industry cap_bin, sector_id Provide structural information
Capital flow 主力净流入率, 超大单占比 Capture capital behavior signals

Later I became increasingly fond of “cross-sectional ranking”. Because in stock competitions, absolute values are often not that stable, but who is stronger or weaker on the same day is closer to the task itself.

Several Key Versions

Version Main change Sharpe Cumulative return
Baseline after fixing leakage Exclude training samples containing future information -0.49 -17.11%
CutVol Remove volatility-related features 0.40 4.39%
Tushare enhancement Add capital flow and industry information 0.70 12.33%
Top-80 feature selection Keep only the top 80 by historical importance -0.07 -7.47%
Three-class label Top 30% / middle / bottom 30% -1.08 -25.77%
Industry coverage fix Industry coverage from 4.7% to 100% 0.18 -0.48%

These values are staged backtest results, not official rankings, and not investment advice.

The Most Counterintuitive Parts

Several conclusions in this project are quite counterintuitive.

First, cutting features improves performance. Volatility features were conspicuous when there was data leakage; after the fix, they instead dragged the model down. It reminded me: feature importance is not an eternal truth; it depends on the validation method.

Second, feature selection worsens performance. The Top-80 version trained much faster, but its cross-year performance was poor. In financial data, what was important in the past does not necessarily remain important in the future.

Third, fixing industry coverage also worsens performance. Industry data was fixed from 4.7% to 100%; by rights the metadata was cleaner, but the model weighted sector_id too heavily, and the result declined instead.

All three results point to one thing: quantitative models cannot be accepted based on intuition; they must be accepted through strict backtesting.

The Route I Now Prefer

If I were to do it again, I would not start by stacking very complex deep structures.

I would first use LightGBM LambdaRank to build a clean, fast, interpretable baseline, and make the following solid:

  • date grouping and label isolation;
  • rolling backtest;
  • robustness check after removing the best month;
  • feature importance drift;
  • ablation of a single change;
  • weight constraints on prediction results.

After these are stable, then consider bringing the deep model back for ensembling.

This project ultimately made me believe more: the truly advanced part in a competition is not how impressive the model name sounds, but whether you can prove your results are not accidental, not peeking at the future, and not fooled by some weird feature.