2. 商品市场基础
在跨品种动量与组合构建中,策略的收益来源并不只来自价格方向判断,还深受合约结构、换月规则、资金占用与摩擦成本影响。若忽略这些基础机制,回测中看似稳定的动量信号,往往会在实盘中被展期损耗与交易成本侵蚀。本节将围绕五个核心概念展开:期货合约、主力连续、展期收益、保证金与交易成本,并说明它们如何共同决定策略的真实表现。
一、核心原理:从单一合约到可交易序列
期货合约是交易所标准化的远期协议,约定了标的商品、数量、交割月份、交割地点与最后交易日。每个品种同时挂牌多个到期月合约,例如螺纹钢可能同时存在rb2501、rb2505、rb2510等。单一合约存在生命周期:临近交割时流动性下降、保证金提高、个人客户可能被限制持仓,因此不能直接拿某一合约做长期动量回测。
为解决连续性问题,行业通常构造“主力连续”或“复权连续”序列。主力连续通过规则识别每个交易日的主力合约,并在换月时拼接价格。常见拼接方式有两类:
- 价格拼接:直接取主力合约收盘价,换月时价格跳空保留,适合观察真实报价,但会产生伪跳空。
- 复权拼接:换月时用价差调整历史价格,使序列连续,适合计算收益率与动量,但价格水平不再对应真实报价。
展期收益(Roll Yield)是换月行为的直接经济后果。当远月合约价格低于近月(贴水结构,Backwardation),多头换月时卖出高价近月、买入低价远月,可获得正展期收益;当远月高于近月(升水结构,Contango),多头换月则承担负展期收益。对于原油、农产品等库存周期明显的品种,展期收益甚至可能超过价格方向收益,成为动量策略的重要收益或成本来源。
二、方法流程:保证金、交易成本与回测口径
保证金是期货交易的资金门槛。交易所设定最低保证金比例,期货公司通常加收若干百分点。保证金分为开仓保证金与维持保证金:当账户权益低于维持保证金时,会触发追加保证金或强制平仓。在组合构建中,保证金决定了名义本金与资金占用之间的杠杆关系,也直接影响策略的夏普比率与回撤幅度。
交易成本包括手续费、滑点与冲击成本。手续费按手数或成交金额收取;滑点取决于买卖价差与下单方式;冲击成本与下单量占市场成交量的比例相关。对于跨品种动量策略,由于需要同时持有多空多个品种,换月与调仓频率较高,成本核算必须逐笔计入。
建议的回测流程如下:
- 确定品种池与主力合约识别规则(如持仓量最大)。
- 构造复权连续价格序列,计算各周期动量信号。
- 按信号生成目标持仓,并映射到具体合约手数。
- 逐日计算保证金占用、手续费、滑点与展期损益。
- 输出净值曲线、换手率、成本占比与分品种归因。
关键参数示例:主力识别阈值可设为持仓量最大且成交量不低于次主力80%;换月提前3个交易日执行;手续费按交易所标准加1.5倍;滑点按一跳的50%估算。
三、案例/场景:展期收益如何改变动量结论
假设某策略在原油上观察到近月合约3个月涨幅20%,信号为做多。若直接使用价格拼接序列,动量收益约为20%。但若该品种处于深度Contango,每月换月损失约2%,三个月展期成本约6%,则实际多头收益降至约14%。反之,若处于Backwardation,展期收益可能额外贡献正收益。
再考虑保证金与成本:假设组合名义本金1000万元,保证金比例12%,则占用资金120万元;若年化手续费与滑点合计0.8%,则每年摩擦成本约8万元。对于年化波动15%的商品组合,这部分成本会显著拉低净夏普。因此,跨品种动量策略必须在信号层面之外,加入展期结构与成本约束。
| 概念 | 作用 | 对策略的影响 | 常见处理方式 |
|---|---|---|---|
| 期货合约 | 标准化交易单位 | 决定交割月与流动性 | 按持仓量选主力 |
| 主力连续 | 拼接可回测序列 | 影响动量计算连续性 | 复权拼接算收益率 |
| 展期收益 | 换月价差损益 | 可正可负,影响净收益 | 单独归因并计入成本 |
| 保证金 | 杠杆与资金占用 | 影响回撤与夏普 | 按交易所加公司比例 |
| 交易成本 | 手续费与滑点 | 高频调仓时侵蚀收益 | 逐笔计入并做敏感性分析 |
四、误区与复盘
常见误区一:直接用主力连续价格计算收益率,忽略换月跳空,导致动量信号被伪跳空污染。
常见误区二:只计算手续费,忽略滑点与冲击成本,使回测收益虚高。
常见误区三:把保证金比例当作固定常数,未考虑交易所临时提高保证金的风险。
复盘时应重点检查:换月日前后信号是否异常;展期损益是否被单独记录;成本假设是否与实盘成交一致;保证金占用是否随波动率上升而被动放大。只有把这些基础概念纳入统一框架,跨品种动量与组合构建才具备可复现、可归因、可实盘的基础。
# 主力连续复权拼接伪代码
for each date t:
main_contract = argmax(open_interest)
if main_contract != prev_contract:
roll_gap = price[main_contract] - price[prev_contract]
adjusted_price = raw_price - cumulative_roll_gap
else:
adjusted_price = raw_price
momentum = adjusted_price / adjusted_price.shift(N) - 1