2026年华东杯数学建模
C题 收益率预测问题
原题再现:
金融市场中,高波动标的常出现剧烈价格变化。本题从多个不同类型的交易市场中选取30支高波动标的,提供其1分钟OHLCVK线数据。
请基于 OHLCV 数据进行特征设计,建立模型预测未来 5 分钟和 60 分钟收益率,需兼顾普通行情和极端行情下的预测能力。撰写一篇论文,对特征、模型与结果进行分析。若有余力可进一步探索多市场、多标的数据的联合建模能力(可选)。评测平台 http://118.196.107.100:8000
数据说明 每支标的提供1分钟频率数据:Open、High、Low、Close、Volume。价格字段已做统一的基准化处理,以减小不同标的价格量级差异带来的影响。同时提供未来5分钟收益率与未来60分钟收益率两个预测标签(已计算)
整体求解过程概述(摘要)
针对子问题一(未来5分钟收益率预测),为了提取分钟OHLCV中的微观价格惯性、局部反转、波动聚集与量价耦合信息,首先实施时间戳标准化、重复样本消除、OHLC逻辑一致性校验和成交量组内插补;随后构造对数收益滞后、多尺度动量、实现波动率、Parkinson与Garman-Klass区间波动率、K线形态、成交量异常、Amihud流动性以及横截面秩等72项特征。鉴于短周期收益具有高噪声和弱信号特征,引入带RobustScaler的Ridge回归作为低方差基准,并与HistGradientBoosting、ExtraTrees及融合模型比较。仿真实验中,验证集预先选定Ridge;其测试RMSE为0.001593,较零预测基线降低6.34%,时点平均RankIC为0.335,方向准确率为61.49%。
针对子问题二(未来60分钟收益率预测),考虑到预测窗口显著增长,短期K线噪声的相对影响下降,而慢趋势、波动状态、下行风险与市场共振的重要性上升。在问题一统一清洗和基础特征体系上进一步引入120分钟动量、波动率的波动率、下行波动率、市场平均收益和跨标的相对强弱特征,避免重复执行相同的数据处理。鉴于原始回归振幅容易因行情均值漂移而失真,引入仅基于验证集估计的横截面秩校准,并按综合损失选择Ridge与HistGB融合模型。测试集中融合模型RMSE为0.007949,时点平均RankIC为0.426,方向准确率为61.21%;同时,单一Ridge的测试RMSE为0.007808,说明长周期任务存在"排序能力较强而幅值估计仍需收缩"的典型特征。
针对子问题三(普通行情、极端行情与多市场联合建模),为了兼顾尖峰厚尾分布下的风险识别能力,本文以训练集绝对收益95%分位数定义极端样本,并采用连续尾部加权损失提高模型对大幅波动的关注度;同时按权益类、期货类、数字资产类进行分组检验,考察联合模型的迁移能力。结果表明,5分钟Ridge在极端样本上的RMSE由基线0.006964降至0.006221,极端方向准确率达到82.08%;60分钟融合模型在三类市场中均保持正的时点平均RankIC。进一步的特征噪声扰动与极端阈值敏感性分析表明,输入噪声提高至20%时,两任务RMSE增幅仍较小,但极端阈值越高,有效尾部样本数快速下降,提示真实竞赛中应采用分层交叉验证和置信区间报告。
综上,本文构建了"数据理解与探索---数据预处理---特征工程---模型构建---模型训练与评估---结果解释与洞察"的完整可复现流程。模型在短周期任务中突出低方差线性结构,在长周期任务中通过多尺度状态特征与横截面校准增强排序稳定性,并以净化时间划分、零预测基线、多模型对照、尾部评价、市场分组和噪声扰动完成验证。该方案能够直接替换为官方数据重新训练,但仿真结果只用于方法展示,最终成绩应以评测平台统一执行为准。
模型假设:
假设1:信息可得性。在时刻t预测未来收益时,Open_t、High_t、Low_t、Close_t、Volume_t以及t之前的历史记录均已完整可得;任何t之后的价格、成交量和标签均不可进入特征。
假设2:匿名标的一致性。asset_id在训练、验证和测试文件中保持稳定,但其真实身份未知且不需要识别;市场类别若由数据提供,可作为内部特征使用。
假设3:价格基准化不改变收益结构。题目已经对价格字段进行统一基准化,乘法尺度变换不改变对数收益、OHLC相对区间和未来收益率,因此本文主要使用无量纲量价特征。
假设4:时间戳可排序。所有记录的时间戳能够转换为统一时间类型;同一标的同一时间戳若重复,保留最后一条记录作为数据更新后的最终值。
假设5:极端状态由训练集定义。极端行情阈值只能由训练集绝对标签分布估计,本文采用95%分位数;验证与测试阶段沿用该阈值,不重新查看其标签分布。
假设6:交易成本不属于评分目标。题目要求预测收益率而非构造可交易策略,因此多空累计曲线仅用于验证预测排序,不作为实际投资收益;不引入手续费、冲击成本或持仓约束。
假设7:仿真数据只用于流程复现。当前数值实验使用研究型仿真数据,仿真机制包含慢趋势、波动聚集、跳跃延续和量价耦合,但其参数不代表任何真实市场。
问题分析:
问题一(5 分钟收益率预测)问题分析
本题属于高频金融时序有监督回归建模问题,核心任务基于分钟级 OHLCV 原始量价数据,构建短期收益预测模型。核心难点:分钟收益噪声强、线性可预测信号微弱,原始价格量纲差异大、多标的波动率异质性显著;数据存在成交量缺失、时序重复脏样本,若随机划分训练集会产生未来函数泄露;单模型易过拟合短时随机波动,高维量价特征存在严重多重共线性。建模思路:统一搭建标准化时序预处理流水线,修正 OHLC 逻辑、插补成交量、构建严格时序分割并设置 60 分钟净化隔离带阻断信息泄露;构建 72 维多尺度量价特征,覆盖滞后收益、区间波动率、K 线形态、流动性与横截面相对指标;采用带 L2 正则的 Ridge 线性模型作为低方差基准,搭配梯度提升、随机树集成做非线性对照,引入极端收益加权损失提升大幅波动样本拟合精度;以零收益预测为基准,从整体误差、横截面 RankIC、涨跌方向准确率、极端行情误差多维度完成模型筛选,输出适配短周期高频数据的稳健预测底座,为长周期、极端场景建模提供统一特征与数据处理框架。
问题二(60 分钟收益率预测)问题分析
本题属于长周期跨标的时序回归优化问题,完全复用问题一的数据清洗、特征底层与时序划分规则,仅针对长周期收益特性拓展特征体系。核心难点:60 分钟收益离散度、厚尾程度远高于 5 分钟,市场整体均值漂移会扭曲绝对收益预测幅值;短期 K 线瞬时冲击信号失效,持续波动、跨标的相对强弱、中长期趋势成为核心驱动;单一误差指标无法区分 "幅值预测偏差" 与 "标的相对排序优劣"。建模思路:在基础特征上新增中长期动量、下行波动率、波动率波动、市场共振指标;设计横截面秩校准方法,消除全市场公共漂移对预测的干扰,仅保留标的相对强弱信息;搭建 Ridge 线性模型与梯度提升融合框架,基于验证集损失倒数动态分配融合权重;统一沿用多维度评价体系,横向对比长短周期模型性能差异,量化长周期下排序能力与绝对收益预测的权衡关系,输出兼顾误差与横截面区分度的长周期预测方案。
问题三(极端行情 + 多市场联合建模)问题分析
本题属于带尾部风险约束的跨市场泛化验证问题,依托前两问双周期完整预测框架开展鲁棒性拓展研究。核心难点:极端涨跌样本占比极低,普通均方损失会弱化尾部拟合效果;权益、期货、数字资产三类市场波动机制存在异质性,统一模型易出现部分市场预测失效;输入测量噪声、极端阈值选取会显著改变尾部预测结论。建模思路:仅用训练集收益 95% 分位数固定极端行情判定标准,构造连续递增样本权重放大大幅波动样本训练权重;按市场类型分组分层检验模型 RankIC、极端预测准确率,验证联合建模跨市场泛化能力;设计特征噪声扰动、极端阈值网格扫描两组敏感性实验,量化模型稳定性;构建 "点收益预测 + 极端风险标签" 双层输出体系,在不改动基础预测模型的前提下,实现普通行情、剧烈行情分层识别,完整验证整套特征、建模、评估流程在噪声、多市场、厚尾行情下的鲁棒性。
模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
程序代码:(代码和文档not free)
python
from __future__ import annotations
import argparse
import json
import math
import os
import random
import warnings
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import Dict, Iterable, List, Optional, Sequence, Tuple
import joblib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import font_manager
from scipy.stats import pearsonr, spearmanr
from sklearn.base import clone
from sklearn.compose import TransformedTargetRegressor
from sklearn.ensemble import ExtraTreesRegressor, HistGradientBoostingRegressor
from sklearn.impute import SimpleImputer
from sklearn.inspection import permutation_importance
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import RobustScaler
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", category=FutureWarning)
SEED = 20260806
HORIZONS = (5, 60)
OHLCV = ["Open", "High", "Low", "Close", "Volume"]
def set_global_seed(seed: int = SEED) -> None:
random.seed(seed)
np.random.seed(seed)
def set_chinese_font() -> None:
candidates = [
"Noto Sans CJK SC", "Noto Serif CJK SC", "Microsoft YaHei",
"SimHei", "Arial Unicode MS", "AR PL UMing CN",
]
installed = {f.name for f in font_manager.fontManager.ttflist}
for font in candidates:
if font in installed:
plt.rcParams["font.sans-serif"] = [font]
break
plt.rcParams["axes.unicode_minus"] = False
plt.rcParams["figure.dpi"] = 130
plt.rcParams["savefig.dpi"] = 180
plt.rcParams["axes.grid"] = True
plt.rcParams["grid.alpha"] = 0.22
plt.rcParams["axes.spines.top"] = False
plt.rcParams["axes.spines.right"] = False
@dataclass
class SplitInfo:
train_end: str
valid_start: str
valid_end: str
test_start: str
purge_minutes: int
n_train: int
n_valid: int
n_test: int
@dataclass
class HorizonResult:
horizon: int
best_model: str
extreme_threshold: float
validation_metrics: Dict[str, Dict[str, float]]
test_metrics: Dict[str, Dict[str, float]]
blend_weight_hist: float
blend_weight_ridge: float
def canonicalize_columns(df: pd.DataFrame) -> pd.DataFrame:
lookup = {str(c).strip().lower(): c for c in df.columns}
aliases = {
"timestamp": ["timestamp", "datetime", "date_time", "time", "date"],
"asset_id": ["asset_id", "symbol", "instrument", "code", "ticker", "id"],
"Open": ["open", "o"], "High": ["high", "h"], "Low": ["low", "l"],
"Close": ["close", "c"], "Volume": ["volume", "vol", "v", "amount"],
"target_5": ["target_5", "return_5", "ret_5", "y5", "label_5"],
"target_60": ["target_60", "return_60", "ret_60", "y60", "label_60"],
"market": ["market", "market_type", "exchange_type"],
}
ren = {}
for std, names in aliases.items():
for name in names:
if name in lookup:
ren[lookup[name]] = std
break
return df.rename(columns=ren)
def load_ohlcv(path: Optional[str]) -> Optional[pd.DataFrame]:
if not path:
return None
p = Path(path)
if not p.exists():
raise FileNotFoundError(f"输入路径不存在:{p}")
files = [p] if p.is_file() else sorted(p.glob("*.csv"))
if not files:
raise FileNotFoundError(f"目录中未找到CSV:{p}")
frames = []
for f in files:
d = pd.read_csv(f)
d = canonicalize_columns(d)
if "asset_id" not in d.columns:
d["asset_id"] = f.stem
frames.append(d)
df = pd.concat(frames, ignore_index=True)
required = {"timestamp", "asset_id", *OHLCV}
missing = required - set(df.columns)
if missing:
raise ValueError(f"缺少必要字段:{sorted(missing)}")
if "market" not in df.columns:
df["market"] = "UNKNOWN"
return df
def generate_synthetic_ohlcv(
n_assets: int = 30,
n_minutes: int = 1800,
seed: int = SEED,
) -> pd.DataFrame:
"""构造多市场、波动聚集、可预测慢趋势、跳跃延续和量价耦合的匿名分钟数据。"""
rng = np.random.default_rng(seed)
timestamps = pd.date_range("2025-01-02 09:30:00", periods=n_minutes, freq="min")
market_names = np.array(["权益类", "期货类", "数字资产类"])
market_of = np.repeat(market_names, math.ceil(n_assets / 3))[:n_assets]
# 公共市场因子含慢变漂移与高/低波状态;慢漂移使60分钟目标具有可学习结构。
common = {}
for m_idx, market in enumerate(market_names):
r = np.zeros(n_minutes)
drift = np.zeros(n_minutes)
high_vol = False
impulse = 0.0
base = [0.00024, 0.00034, 0.00046][m_idx]
for t in range(1, n_minutes):
if rng.random() < 0.006 + 0.0015 * m_idx:
high_vol = not high_vol
drift[t] = 0.995 * drift[t - 1] + rng.normal(0, base * 0.014)
if rng.random() < 0.0035:
impulse += rng.normal(0, base * 4.2)
impulse *= 0.76
sigma = base * (2.2 if high_vol else 1.0)
r[t] = drift[t] + impulse + 0.24 * r[t - 1] + 0.72 * sigma * rng.standard_t(df=7) / math.sqrt(7 / 5)
common[market] = np.clip(r, -0.012, 0.012)
frames = []
for a in range(n_assets):
market = market_of[a]
m_idx = list(market_names).index(market)
beta = rng.uniform(0.45, 1.05)
base = rng.uniform(0.00020, 0.00048) * (1 + 0.15 * m_idx)
ret = np.zeros(n_minutes)
alpha = np.zeros(n_minutes)
impulse = 0.0
sigma2 = base ** 2
for t in range(2, n_minutes):
# 标的特有慢趋势具有持久性,对长周期收益提供弱但稳定的预测信息。
alpha[t] = 0.996 * alpha[t - 1] + rng.normal(0, base * 0.012)
if rng.random() < 0.0022:
impulse += rng.normal(0, base * 5.5)
impulse *= 0.70
omega = base ** 2 * (1 - 0.07 - 0.90)
leverage = 1.35 if ret[t - 1] < -2.0 * base else 1.0
sigma2 = omega + 0.07 * ret[t - 1] ** 2 * leverage + 0.90 * sigma2
eps = 0.68 * math.sqrt(max(sigma2, base ** 2 * 0.30)) * rng.standard_t(df=7) / math.sqrt(7 / 5)
# 短期惯性与5分钟反转并存,使树模型和线性模型均有识别空间。
micro = 0.34 * ret[t - 1] + 0.08 * ret[t - 2] - 0.025 * ret[t - 5] if t >= 5 else 0.34 * ret[t - 1] + 0.08 * ret[t - 2]
ret[t] = beta * common[market][t] + alpha[t] + impulse + micro + eps
ret = np.clip(ret, -0.018, 0.018)
close = np.exp(np.log(1.0 + 0.02 * rng.random()) + np.cumsum(ret))
open_ = np.r_[close[0], close[:-1] * np.exp(rng.normal(0, base * 0.12, n_minutes - 1))]
intrabar = np.abs(rng.normal(0, base * 0.60, n_minutes)) + 0.25 * np.abs(ret)
high = np.maximum(open_, close) * np.exp(intrabar)
low = np.minimum(open_, close) * np.exp(-intrabar)
base_vol = rng.lognormal(mean=10.0 + 0.22 * m_idx + rng.normal(0, 0.18), sigma=0.38, size=n_minutes)
volume = base_vol * (1 + 220 * np.abs(ret) + 90 * np.sqrt(np.maximum(sigma2, 0)))
miss_idx = rng.choice(n_minutes, size=max(2, n_minutes // 450), replace=False)
volume[miss_idx] = np.nan
frame = pd.DataFrame({
"timestamp": timestamps, "asset_id": f"A{a+1:02d}", "market": market,
"Open": open_, "High": high, "Low": low, "Close": close, "Volume": volume,
})
frames.append(frame)
df = pd.concat(frames, ignore_index=True)
dup = df.sample(n=min(30, len(df) // 1000), random_state=seed)
return pd.concat([df, dup], ignore_index=True)
def data_quality_report(df: pd.DataFrame) -> pd.DataFrame:
rows = []
for c in ["timestamp", "asset_id", "market", *OHLCV, "target_5", "target_60"]:
if c not in df.columns:
continue
s = df[c]
rows.append({
"字段": c,
"数据类型": str(s.dtype),
"非空数": int(s.notna().sum()),
"缺失率": float(s.isna().mean()),
"唯一值数": int(s.nunique(dropna=True)),
})
return pd.DataFrame(rows)
def clean_data(df: pd.DataFrame) -> Tuple[pd.DataFrame, Dict[str, int]]:
d = canonicalize_columns(df.copy())
before = len(d)
d["timestamp"] = pd.to_datetime(d["timestamp"], errors="coerce")
d["asset_id"] = d["asset_id"].astype(str)
d["market"] = d.get("market", "UNKNOWN").astype(str)
for c in OHLCV + ["target_5", "target_60"]:
if c in d.columns:
d[c] = pd.to_numeric(d[c], errors="coerce")
d = d.dropna(subset=["timestamp", "asset_id", "Open", "High", "Low", "Close"])
d = d.sort_values(["asset_id", "timestamp"]).drop_duplicates(["asset_id", "timestamp"], keep="last")
duplicate_removed = before - len(d)
# 逻辑一致性修复:High不低于OHLC最大值,Low不高于OHLC最小值
ohlc_max = d[["Open", "High", "Low", "Close"]].max(axis=1)
ohlc_min = d[["Open", "High", "Low", "Close"]].min(axis=1)
invalid_high = int((d["High"] < ohlc_max).sum())
invalid_low = int((d["Low"] > ohlc_min).sum())
d["High"] = np.maximum(d["High"], ohlc_max)
d["Low"] = np.minimum(d["Low"], ohlc_min)
# 价格非正记录不可用于对数收益,直接剔除;成交量非正视为缺失
nonpositive_price = int((d[["Open", "High", "Low", "Close"]] <= 0).any(axis=1).sum())
d = d[(d[["Open", "High", "Low", "Close"]] > 0).all(axis=1)].copy()
d.loc[d["Volume"] <= 0, "Volume"] = np.nan
missing_volume_before = int(d["Volume"].isna().sum())
d["Volume"] = d.groupby("asset_id")["Volume"].transform(lambda s: s.ffill().bfill())
d["Volume"] = d["Volume"].fillna(d["Volume"].median())
for h in HORIZONS:
col = f"target_{h}"
if col not in d.columns or d[col].isna().all():
d[col] = d.groupby("asset_id")["Close"].transform(lambda s: s.shift(-h) / s - 1.0)
stats = {
"原始记录数": int(before),
"去重及无效时间后记录数": int(len(d)),
"删除重复或无效记录数": int(duplicate_removed),
"修复High逻辑冲突数": invalid_high,
"修复Low逻辑冲突数": invalid_low,
"删除非正价格记录数": nonpositive_price,
"插补成交量缺失数": missing_volume_before,
}
return d.sort_values(["timestamp", "asset_id"]).reset_index(drop=True), stats
def rolling_rsi(ret: pd.Series, window: int = 14) -> pd.Series:
gain = ret.clip(lower=0).rolling(window, min_periods=window).mean()
loss = (-ret.clip(upper=0)).rolling(window, min_periods=window).mean()
rs = gain / (loss + 1e-12)
return 100 - 100 / (1 + rs)
def add_time_series_features(group: pd.DataFrame) -> pd.DataFrame:
g = group.sort_values("timestamp").copy()
close = g["Close"]
log_close = np.log(close)
ret1 = log_close.diff()
g["ret_1"] = ret1
for lag in [2, 3, 5, 10, 15, 30, 60, 120]:
g[f"ret_{lag}"] = log_close.diff(lag)
for lag in [1, 2, 3, 5, 10, 30, 60]:
g[f"ret1_lag_{lag}"] = ret1.shift(lag)
for w in [5, 10, 15, 30, 60, 120]:
g[f"rv_{w}"] = ret1.rolling(w, min_periods=max(3, w // 3)).std()
g[f"mom_{w}"] = ret1.rolling(w, min_periods=max(3, w // 3)).mean()
g[f"volume_z_{w}"] = (
np.log1p(g["Volume"]) - np.log1p(g["Volume"]).rolling(w, min_periods=max(3, w // 3)).mean()
) / (np.log1p(g["Volume"]).rolling(w, min_periods=max(3, w // 3)).std() + 1e-12)
g["hl_range"] = np.log(g["High"] / g["Low"])
g["oc_body"] = np.log(g["Close"] / g["Open"])
g["upper_shadow"] = np.log(g["High"] / np.maximum(g["Open"], g["Close"]))
g["lower_shadow"] = np.log(np.minimum(g["Open"], g["Close"]) / g["Low"])
g["close_location"] = (g["Close"] - g["Low"]) / (g["High"] - g["Low"] + 1e-12)
g["parkinson_var"] = (np.log(g["High"] / g["Low"]) ** 2) / (4 * np.log(2))
g["gk_var"] = 0.5 * np.log(g["High"] / g["Low"]) ** 2 - (2 * np.log(2) - 1) * np.log(g["Close"] / g["Open"]) ** 2
g["log_volume"] = np.log1p(g["Volume"])
g["volume_change"] = g["log_volume"].diff()
g["amihud_30"] = (ret1.abs() / (g["Volume"] + 1e-12)).rolling(30, min_periods=10).mean()
g["rsi_14"] = rolling_rsi(ret1, 14)
ema12 = close.ewm(span=12, adjust=False).mean()
ema26 = close.ewm(span=26, adjust=False).mean()
g["ema_gap_12"] = close / ema12 - 1
g["ema_gap_26"] = close / ema26 - 1
g["macd"] = ema12 / ema26 - 1
g["vol_of_vol_60"] = g["rv_15"].rolling(60, min_periods=20).std()
g["downside_vol_30"] = ret1.where(ret1 < 0, 0).rolling(30, min_periods=10).std()
g["jump_score"] = ret1.abs() / (g["rv_60"] + 1e-12)
return g