简介:2022年美国大学生数学竞赛C题杰出论文(O奖)成果,主题是比特币与黄金的日间交易策略建模,面向备战美赛的大学生、研究生以及金融量化交易爱好者。论文基于2016年9月11日至2021年9月10日的黄金与比特币历史价格数据,构建了一个可自动给出每日买卖决策的模型。模型核心为即时逻辑分析,识别市场特征,综合应用均值回归与动量理论,加权平衡两种策略,同时引入归一化权重因子W抑制噪声,避免错误交易。其中,均值回归假设价格终将回到长期均值,动量理论则捕捉趋势延续机会,二者互补增强了模型的适应能力。针对极端行情,模型还设有独立判断逻辑,并考虑每笔交易成本,使策略更贴近真实操作。论文完整展示了问题重述、条件假设、模型建立、灵敏度分析与结论评价,章节结构清晰,公式与流程图丰富。资源共1个PDF文件,压缩包大小约978KB,为英文原版全文。目前已有147人学习,可作为特等奖论文结构参考、量化策略设计模板,以及数学建模写作的进阶读物。
1. 2022美赛C题O奖论文,到底哪里值得读
收到一份名字叫2022年美赛优秀论文集-C题O奖论文-2229059【英文】.pdf的文件时,多数人的第一反应是收藏,第二反应才是打开。但要是真把O奖论文当小说看,大概率看两页就关掉了——因为O奖论文呈现的是“最终成果”,不是“思考过程”。对于正在备赛或想提升数据建模能力的工程师来说,这份PDF真正的价值不在于那段漂亮的Executive Summary,而在于它暴露出来的解题套路:数据怎么清洗、特征怎么构造、模型怎么选、结果怎么论证,每一步都有章法可循。
MCM/ICM的C题历来是“数据挖掘题”,2022年的C题围绕比特币交易展开,给了市场数据、币币交易对和用户交易记录。O奖论文2229059拿到最高奖,靠的不是某一个大模型横空出世,而是把“赚钱策略”这一模糊问题拆成了可量化、可验证、可解释的若干子问题。读这篇PDF时,我建议你带着三个问题:它是怎么把业务问题翻译成数学问题的?它对数据的预处理做了哪些我当时没想到的动作?它的评价指标为什么选那一个而不是另一个?下文的拆解,就是顺着这三条线展开的。方向对了,复现才不会变成抄作业。
2. 拆解O奖论文的定题与建模框架
2.1 先看它如何把“C题”翻译成数学语言
美赛C题几乎不会直接告诉你“请建立一个分类模型”,它给的是一个业务场景,比如2022年的“交易策略”。O奖论文2229059的第一步,通常是把这句话拆成三件事:预测目标(明天涨还是跌)、时间粒度(按分钟、小时还是天)、约束条件(手续费、滑点、风险容忍度)。翻译得好不好,直接决定后面所有工作是否有意义。
这里有一个容易被忽略的点:O奖论文几乎都会在开头明确“我们不做预测,我们做决策”。这句话看似绕,实际上改变了整个建模框架。预测模型优化的是误差(比如MSE、准确率),决策模型优化的是收益或风险调整后收益。同样是LSTM,预测框架下你关心预测值和真实值的偏差;决策框架下你关心按照预测方向开仓后,扣掉手续费是否还能赚钱。两者在特征选择、阈值设定、样本加权上都有本质差异。你读PDF时,重点看它的目标函数写的是什么,这比看它画了多少张图更重要。
2.2 从摘要反推它的评审得分点
美赛评审不是看模型复杂度,而是看“问题重述—假设—建模—求解—验证—敏感性分析”这条链是否完整。O奖论文的摘要通常会在200词内交代:做了什么、用了什么数据、模型精度是多少、策略收益是多少。你可以把摘要中的数字当作路标,反推它正文里花了多少篇幅去论证这些数字从哪里来。
以比特币交易为例,这类论文的套路高度一致:先做数据清洗和探索性分析(EDA),然后构造特征(价格衍生指标、情绪指标、市场微观结构指标),再建模(常见的组合是XGBoost加LSTM,或者纯树模型加规则),最后做回测和稳健性检验。O奖论文2229059的特别之处,大概率不是模型多新奇,而是它的验证环节做到了“别人没想到”的程度,比如把手续费的影响单独拆出来做敏感性分析。你在读PDF时,对照这个框架去标记它每一步的具体做法,比自己从头读要高效得多。
# 一段用于描述“预测框架 vs 决策框架”差异的伪代码示例 import numpy as np def trading_decision(pred, actual, threshold=0.01, fee=0.002): """ 决策框架:不追求预测完全准确,只追求扣费后期望收益为正 pred : 预测的涨跌幅 actual: 实际的涨跌幅 fee : 单边手续费率 """ direction = 1 if pred > threshold else (-1 if pred < -threshold else 0) if direction == 0: return 0, 0.0 profit = direction * actual - fee return direction, profit # 预测框架:只算误差 pred = np.array([0.02, -0.01, 0.0, 0.015]) actual = np.array([0.015, 0.005, -0.01, 0.01]) mse = np.mean((pred - actual) ** 2)这段代码说明了一个关键逻辑:预测框架下的均方误差可能很小,但决策框架下只要方向错了,误差再小也亏钱。O奖论文的思路通常是把这两者结合:用预测结果作为信号,再叠加一个决策层来过滤低置信度信号。你在写自己的论文时,这个threshold和fee就是必须做敏感性分析的两个参数。
2.3 数据描述与假设:O奖论文的细节藏在这里
大多数参赛队的数据处理是“读入—去空—标准化—建模”四步走,但O奖论文会在数据描述部分多写两样东西:数据的业务含义和样本的时间覆盖面(比如是否包含2017年的大牛市、2020年的312事件)。不要小看这两个信息,它决定了模型训练集和验证集的划分方式。
2022年C题的数据涉及多个交易对,比如BTC/USDT、ETH/BTC等,这些交易对之间存在套利关系。O奖论文通常会把“相关性”作为特征直接喂进模型,而不是先分析相关性是否稳定。这里有一个常见的坑:比特币市场在极端行情下,所有币种的相关性会趋近于1,这时若你的模型结构依赖于相关性矩阵(比如马科维茨组合优化),结果就直接崩了。O奖论文2229059的做法往往是加一个“状态变量”,把市场切成常规和极端两个状态,分别建模。这一点非常值得你在自己的项目里学习:与其让模型自己去拟合非线性,不如主动告诉它“现在处于什么市场状态”。
3. 比特币C题的三大特征工程路径
3.1 交易数据里的“微观结构特征”,比K线更有区分度
读O奖论文,你会发现它的特征列表里不只有开盘价、收盘价、成交量这些常见字段,更多的是订单簿快照衍生出来的指标。已知高频金融数据里,VWAP(成交量加权平均价格)的偏离度、买卖价差、订单流不平衡、大单成交占比,这些微观结构特征在预测短期价格方向时的信息量远大于传统技术指标。C题的公开数据里不一定有订单簿,但有每一笔交易的记录,所以可以通过聚合计算还原出类似的微观结构特征。
常见做法是按时间窗口(比如5分钟)聚合交易记录,计算:
- 主动买盘成交量占比:用成交方向(主动买/主动卖)来估计买卖压力;
- 大单阈值:把单笔成交量超过该币种过去24小时平均成交量若干倍的交易标记为大单;
- 大单净占比:一段窗口内,大单主动买入量减去大单主动卖出量,再除以总成交量。
import pandas as pd def build_microstructure_features(df, window='5T', large_mult=3.0): """ 从逐笔交易数据中构建微观结构特征 df需包含列: timestamp, price, volume, side(side>0为主动买, side<0为主动卖) window: 重采样窗口, '5T'表示5分钟 large_mult: 判断大单的倍数阈值 """ df = df.copy() df['ts'] = pd.to_datetime(df['timestamp']) df.set_index('ts', inplace=True) # 计算单笔成交金额, 判断大单 df['amount'] = df['price'] * df['volume'] # 过去24小时滚动均值作为基准(避免使用未来数据) df['avg_amount'] = df['amount'].rolling('24H').mean().shift(1) df['is_large'] = df['amount'] > df['avg_amount'] * large_mult # 按窗口聚合 grouped = df.resample(window).agg( buy_vol=('volume', lambda s: s[df.loc[s.index, 'side'] > 0].sum()), large_buy_vol=('volume', lambda s: s[(df.loc[s.index, 'side'] > 0) & df.loc[s.index, 'is_large']].sum()), total_vol=('volume', 'sum'), large_vol=('volume', lambda s: s[df.loc[s.index, 'is_large']].sum()), close=('price', 'last'), ) grouped['buy_ratio'] = grouped['buy_vol'] / grouped['total_vol'] grouped['large_net_buy_ratio'] = (2 * grouped['large_buy_vol'] - grouped['large_vol']) / grouped['large_vol'] return grouped.dropna(subset=['large_net_buy_ratio'])这段代码的关键在于三个参数:window决定特征的时间粒度,5分钟适合短时交易,但如果你想做日频预测,可以改成'1D';large_mult决定大单的判定阈值,3倍比较保守,如果你发现大单特征几乎不变,说明这个阈值设太高了;rolling().shift(1)是防止数据泄漏的核心,在计算基准时用过去24小时的均值,并且向后平移一个周期。O奖论文里对这类特征的描述往往会配上分布图,说明大单特征在涨跌样本上的差异是显著的。如果你的模型里大单特征贡献度始终很低,检查一下是不是聚合窗口选得太长,把大单信号稀释掉了。
3.2 文本情绪特征:不是必须,但O奖论文常用来加分
2022年C题只给了交易数据,没给新闻或社交数据,所以如果论文里出现了情绪特征,那要么是从交易行为里推断的(比如异常成交量)、要么是作者自己抓取了外部数据。后者有风险:美赛不允许在比赛期间从外部获取数据吗?规定是可以使用外部数据源,但评审会更看重你“只用题目数据也能做出好结果”。因此,大多数O奖论文把情绪处理成“市场内部情绪”而非“文本情绪”。
内部情绪特征怎么做?常见的一个代理变量是“换手率异常度”:某币种在某个窗口内的换手率超过其自身历史均值的多少倍。另一个是“价格冲击系数”:每单位成交量引发的价格变动,如果这个系数突然变大,说明市场处于恐慌或狂热状态,这两者对方向预测都有价值。这类特征的好处是不依赖额外数据源,且解释性强——评审看到你用一个成交量特征描述“市场情绪”时,通常不会质疑,因为逻辑链是完整的。
特征名称 计算方式 业务含义 buy_ratio 主动买量 / 总成交量 买卖压力 large_net_buy_ratio (2*大单买量 - 大单量) / 大单量 主力方向 volatility_ratio 当前窗口波动率 / 过去24h波动率 恐慌或狂热 price_impact 平均价格变动 / 平均成交量 市场深度及流动性3.3 时间窗口与标签构造:如何定义“涨跌”决定了模型上限
这是全篇最容易踩坑的地方。很多人直接定义“未来一小时收盘价 > 当前价 => 1”,然后训练一个分类器,但O奖论文会在这个定义上多做两件事:一是加入手续费阈值的缓冲带,二是做多标签预测。加入缓冲带的意思是:只有当未来收益超过某个阈值(比如0.5%)时才标为上涨,低于阈值但没亏钱的标为0,亏钱超过阈值标为-1。这个缓冲带直接过滤掉了大量“噪音标签”,模型学起来会容易很多。
关于多标签,常见做法是同时预测三个时间尺度:15分钟、1小时、4小时的方向,然后把三个信号按逻辑(比如至少两个方向一致才下单)输出。这样做的本质是利用多个时间尺度的投票来降低假信号率。O奖论文2229059几乎肯定做了类似处理,因为纯单尺度模型在回测里的收益曲线很难做到平滑。
def build_labels(df, horizon='1H', fee=0.002, up_th=0.01, down_th=0.01): """ 构造三分类标签: 1=显著上涨, 0=变化不大, -1=显著下跌 horizon: 预测的时间跨度 fee: 手续费率, 用于构造缓冲区 up_th/down_th: 相对当前价格的涨跌幅阈值, 需要大于fee """ df = df.copy() future_close = df['close'].shift(-1) # 滞后未来价格 df['ret_future'] = (future_close - df['close']) / df['close'] # 保证金阈值要覆盖手续费, 否则模型可能学到“赚了手续费”的假信号 assert up_th > fee, "up_th must be greater than fee to avoid trivial signals" def classify(r): if r > up_th: return 1 elif r < -down_th: return -1 else: return 0 df['label'] = df['ret_future'].apply(classify) # 删除最后horizon长度内无法计算未来收益的样本 return df.dropna(subset=['label'])注意这段代码里的shift(-1)与3.1节里的shift(1)是相反方向的:前者是未来信息,后者是过去信息。标签必须用未来信息,特征必须只用历史信息,这两者在代码里很容易写混。写完这段代码后,建议立刻打印一下三个类别的样本占比,如果某一个类别占比超过90%,说明你的阈值设得太极端,模型学不到东西,还容易过拟合到多数类上。
4. 模型选型与参数配置:从树模型到时序模型
4.1 为什么树模型在C题里常常打赢深度学习
读近几年的O奖论文,你会发现一个反直觉的现象:很多获奖队伍放弃了LSTM、Transformer,转而用XGBoost或LightGBM拿下O奖。原因很简单:C题给的数据量通常只有几十万行,特征维度几十到几百,这个规模下树模型训练快、调参容易、特征重要性可直接解释,而深度模型需要大量数据才能发挥优势,且调参时间不可控。O奖评审在乎的是结果可复现、逻辑可解释,树模型在这方面天然有优势。
4.2 XGBoost在价格方向预测上的参数怎么设
以XGBoost为例,最小可运行的参数设置通常包含:树的数量控制在300—500棵(用早停确定)、最大深度设3—5(防过拟合,金融数据噪声极大)、学习率0.01—0.05(打配合用)、正样本权重设为其反面样本的比例(处理三分类中多数类是0的问题)。上面这些参数并不神奇,关键在验证方式:金融时间序列不能随机打乱做K折交叉验证,那样会把未来数据泄漏到训练集里。必须用时间序列滑窗验证,比如用第1—180天训练、第181—200天验证,然后平移。
import xgboost as xgb def train_xgb_timeseries(df_features, df_labels, train_days, val_days): """ 时间序列滑窗验证与XGBoost训练 df_features: 按时间排列的特征矩阵(含'date'列) df_labels: 对应的标签序列 """ # 按日期划分, 而不是按行数随机划分(关键!) train_mask = df_features['date'] < train_days val_mask = (df_features['date'] >= train_days) & (df_features['date'] < val_days) model = xgb.XGBClassifier( n_estimators=500, max_depth=4, learning_rate=0.03, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=2.0, # 若正样本较少, 调整此类权重 eval_metric='logloss', early_stopping_rounds=30, random_state=42 ) model.fit( df_features.loc[train_mask].drop(columns=['date', 'label']), df_labels.loc[train_mask], eval_set=[(df_features.loc[train_mask].drop(columns=['date', 'label']), df_labels.loc[train_mask]), (df_features.loc[val_mask].drop(columns=['date', 'label']), df_labels.loc[val_mask])], verbose=False ) return model参数说明:scale_pos_weight这个参数在二分类里含义明确,多分类时需要配合sample_weight使用,或直接调整每个类别的权重数组。代码里设成2.0是经验值,实际要根据你样本中正负类的比例来定。eval_metric用logloss而不是auc,是因为当标签是三分类且类别不平衡时,logloss对概率的校准程度更敏感。early_stopping_rounds=30配合n_estimators=500意味着大多数时候模型会在200棵左右停下来,不会真的跑满500棵。你不要把这个参数组合当成万能公式,它是“起点”,不是“终点”。
4.3 LSTM做时序预测时的输入构造与陷阱
树模型虽然好用,但如果论文里完全没有时序模型,评审可能会觉得“技术单一”。O奖论文的常见策略是“双轨制”:XGBoost作为主模型,LSTM作为对照或集成另一路。LSTM的结构很简单:输入是连续T个时间步的特征序列,输出是下一时刻的方向或收益。陷阱在于特征序列的构造方式——很多人把close价格直接喂进去,却忘了价格是非平稳序列,模型学到的是“以前的价格水平”,而不是“变化的方向”。O奖论文的做法通常是先做差分或计算收益率,再做标准化。
import numpy as np from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout def build_lstm_sequence(df, feature_cols, lookback=24, horizon=1): """ 构造LSTM输入序列: 每个样本是过去lookback步的特征序列, 预测horizon步后的收益率方向 df: 已按时间排序的特征表 feature_cols: 需要进入模型的特征列(最好是收益率、波动率等平稳序列) """ data = df[feature_cols].values X, y = [], [] for i in range(lookback, len(data) - horizon): X.append(data[i - lookback:i]) y.append(1 if df['label'].iloc[i + horizon] > 0 else 0) return np.array(X), np.array(y)提醒一点:这里把标签简化为二分类,但在O奖论文里他们会用三分类并把中间类别从损失函数里降权。实际做法是在模型输出层改用3个神经元、softmax激活,类别权重通过class_weight参数传给fit函数。相比XGBoost,调LSTM的时间成本高很多,所以通常建议在树模型已经把特征验证有效之后,再投入时间做LSTM,而不是一开始就死磕深度学习。
5. 从预测到决策:O奖论文里的阈值、回测与敏感性分析
5.1 最优阈值怎么求?网格搜索加回测成本函数
大多数参赛队把模型输出的概率按0.5截断分成两类,O奖论文很少这么干。金融问题里错误方向惩罚不对称:预测上涨但实际下跌,亏的是本金;预测下跌但实际没跌,损失的只是机会成本。所以O奖论文会专门划出一段验证集,遍历不同概率阈值,选择“扣掉手续费后样本外收益最大”的那个阈值,而不是选择默认的0.5。此处“收益最大”指策略收益,不是准确率。
def optimal_threshold(val_probs, val_labels, fee=0.002, n_thresholds=100): """ 在验证集上搜索最优下单阈值 val_probs: 模型输出的上涨概率 val_labels: 三分类标签(1上涨, 0无变化, -1下跌) """ best_th, best_profit = 0.5, -np.inf thresholds = np.linspace(0.5, 0.95, n_thresholds) for th in thresholds: profit = 0.0 for i in range(len(val_probs)): if val_probs[i] >= th: # 预测上涨, 做多 profit += (1 if val_labels[i] == 1 else -1) - fee elif val_probs[i] <= 1 - th: # 预测下跌, 做空(简化: 收益为负变动减去手续费) profit += (-1 if val_labels[i] == -1 else 1) - fee if profit > best_profit: best_profit, best_th = profit, th return best_th, best_profit这里fee是单边费率,实际回测时买入和卖出各收一次,因此代码里的成本计算偏乐观。你在自己的论文里复现时,可以把- fee改成- 2 * fee,这样更贴近真实交易。阈值搜索范围通常在0.5—0.95,因为如果阈值低于0.5,意味着模型认为不涨的概率不到一半就做空,信号质量太差且手续费损耗严重。
5.2 回测框架里的三个坑:前视偏差、手续费、滑点
O奖论文不会回避回测,但它会把回测写得很“谦虚”:既展示净值曲线,又讨论37个参数的敏感性。回测框架常见的坑就三个:前视偏差(用了未来数据)、手续费设为零、滑点设为常数。美赛数据是历史数据,你不可能模拟真实滑点,所以O奖论文通常会做“可变手续费”的敏感性分析,而不是假装滑点不存在。
| 参数 | 基准值 | 敏感范围 | 对结果的影响 |
|---|---|---|---|
| 手续费率 | 0.10% | 0%—0.5% | 收益随费率线性下降 |
| 滑点 | 0.05% | 0%—0.2% | 高阈值策略受影响更大 |
| 预测时间粒度 | 1小时 | 15分钟—4小时 | 15分钟信号噪音大, 4小时信噪比更高但机会更少 |
| 训练窗口长度 | 180天 | 90—360天 | 窗口太短模型波动大, 太长无法适应市场状态切换 |
5.3 敏感性分析的意义与快速实现方式
敏感性分析是评审最容易查的环节:如果你对每个关键参数只给了一组结果,评审会认为你没想过“如果参数变了结果还成立吗”。也不一定非要精心设计全套蒙特卡洛,更常见的做法是控制变量:固定其他参数,把某个参数在合理范围里均匀取5—10个值,然后看策略收益的变化曲线。如果曲线是平滑的单调变化,说明策略对参数不敏感;如果曲线剧烈跳动,说明策略过拟合了参数。O奖论文里常见的是对手续费做这种曲线,因为它既容易做又最能展示稳健性。对预测时间粒度和阈值做了敏感性的论文,说明作者对模型输出把握较强。你在自己的复现中,把上述表格的参数各跑一遍,画成曲线放进论文附录,评审印象会明显提升。
6. 收尾:用敏感性分析验证O奖论文的可复现性,不必逐行复现
读2229059论文到最后一章,不要急着挑它用了什么小众模型,先挑它画的敏感性分析图。通常O奖论文会放一张收益对手续费率的曲线**,横轴从0%到1%,纵轴是累计收益。如果曲线在0.1%—0.3%附近有明显突变,说明那个策略其实很脆弱;如果曲线平滑,说明策略对成本不敏感,这篇论文的结论才真正值得参考。
实操时,建议你顺手做这几件事:把它的10个核心特征复制到自己的数据上,用XGBoost跑一个baseline,然后在同一验证集上对比不同特征组合的表现。模型精度上超过它并不是第一目标,更值得关注的是,它的特征组合在你的验证集上是否依然稳定,不同区间收益差异大不大。对于2022年C题,数据里存在明显的牛熊切换,建议把验证集至少切成三段,分别看收益,三段都正且有超额收益才是完整结果。如果某一段出现大幅亏损,就去查这一段时间的数据里有什么异常(比如某个交易所宕机导致的极端波动)。发现这类问题对读者来说,比复现曲线本身更值得写进自己的博客笔记里。
本文还有配套的精品资源,点击获取