问题描述
解决方案
这是一个基于聚宽(JoinQuant)平台的多品种商品期货动量轮动策略。
策略逻辑说明
- 标的池:选取市场上流动性较好的商品期货品种(如螺纹钢、铁矿石、铜、化工品等)。
- 主力合约映射:自动获取各品种当前的主力合约。
- 动量因子:使用过去
N天(例如20天)的收益率作为动量指标。 - 轮动规则:
- 计算所有备选品种的动量。
- 做多:选择动量最大且为正的
K个品种(例如前3名)。 - 风控/过滤:如果排名前列的品种动量为负,则不开仓(宁缺毋滥)。
- 调仓频率:按周调仓(每周第一个交易日)。
- 资金管理:等权重分配资金。
策略代码
# -*- coding: utf-8 -*-
from jqdata import *
def initialize(context):
"""
初始化函数,设定基准、手续费、滑点、标的池等
"""
# 1. 设定基准(这里随意设定一个,期货策略主要看绝对收益)
set_benchmark('000300.XSHG')
# 2. 开启真实价格(动态复权)
set_option('use_real_price', True)
# 3. 设定账户类型为期货账户
# 初始资金设为 1,000,000 元
set_subportfolios([SubPortfolioConfig(cash=1000000, type='futures')])
# 4. 设定手续费(这里设置得相对保守,模拟真实环境)
# 开平仓万分之2
set_order_cost(OrderCost(open_commission=0.0002, close_commission=0.0002, close_today_commission=0.0002), type='futures')
# 5. 设定滑点
set_slippage(PriceRelatedSlippage(0.002), type='futures')
# 6. 定义全局变量
# 待选品种列表(涵盖黑色、有色、化工、农产品等主要板块)
# 注意:这里使用的是品种代码,不是具体合约代码
g.instruments = [
'RB', # 螺纹钢
'HC', # 热卷
'I', # 铁矿石
'J', # 焦炭
'CU', # 沪铜
'AL', # 沪铝
'ZN', # 沪锌
'AU', # 黄金
'TA', # PTA
'MA', # 甲醇
'PP', # 聚丙烯
'L', # 塑料
'RU', # 橡胶
'M', # 豆粕
'Y', # 豆油
'SR', # 白糖
]
# 动量回看窗口(天)
g.lookback_days = 20
# 持仓数量(持有动量最强的前N个)
g.top_k = 3
# 7. 设定定时运行:每周第一个交易日开盘后运行
run_weekly(weekly_adjust, weekday=1, time='09:30')
def weekly_adjust(context):
"""
周度调仓逻辑
"""
# 获取当前日期
current_date = context.current_dt.date()
# 1. 获取主力合约并计算动量
momentum_scores = []
for code in g.instruments:
# 获取该品种当前的主力合约代码
dom_contract = get_dominant_future(code)
if dom_contract is None:
continue
# 获取历史价格数据
# 取 lookback_days + 1 天的数据,以便计算收益率
# fields=['close'] 获取收盘价
hist_data = attribute_history(dom_contract, g.lookback_days + 1, '1d', ['close'])
# 如果数据长度不足,跳过
if len(hist_data) < g.lookback_days + 1:
continue
# 计算动量:(当前价格 / N天前价格) - 1
p_now = hist_data['close'][-1]
p_prev = hist_data['close'][0]
if p_prev == 0:
continue
momentum = (p_now / p_prev) - 1.0
# 记录数据:(合约代码, 动量值)
momentum_scores.append((dom_contract, momentum))
# 2. 筛选目标合约
# 按动量从大到小排序
momentum_scores.sort(key=lambda x: x[1], reverse=True)
# 筛选出动量为正的前 top_k 个合约
# 逻辑:只做多头,且要求动量大于0(趋势向上)
target_list = []
for contract, score in momentum_scores:
if score > 0:
target_list.append(contract)
if len(target_list) >= g.top_k:
break
log.info("当前日期: %s, 目标持仓: %s" % (current_date, target_list))
# 3. 执行交易
# 3.1 平仓逻辑
# 遍历当前持有的所有多单
for contract in list(context.portfolio.long_positions.keys()):
# 如果当前持仓不在目标列表中,或者主力合约发生了切换(旧合约不在目标列表),则平仓
if contract not in target_list:
order_target_value(contract, 0, side='long')
log.info("平仓: %s" % contract)
# 3.2 开仓/调仓逻辑
if len(target_list) > 0:
# 计算每个标的分配的资金
# 使用总权益的 80% 进行投资,保留 20% 作为保证金缓冲,防止波动爆仓
total_value = context.portfolio.total_value
cash_per_asset = (total_value * 0.8) / len(target_list)
for contract in target_list:
# 调整仓位到目标价值
# order_target_value 会自动处理买入或卖出以达到目标金额
# side='long' 表示做多
order_target_value(contract, cash_per_asset, side='long')
log.info("调仓: %s, 目标市值: %.2f" % (contract, cash_per_asset))
def handle_data(context, data):
"""
盘中运行函数(本策略为周频,此处留空或用于风控)
"""
pass
策略关键点解析
-
get_dominant_future(code):- 这是聚宽特有的API,用于获取某个品种(如 'RB')在当前时间点的主力合约(如 'RB2105.XSGE')。这是期货策略回测中最关键的一步,因为它解决了合约换月的问题。
-
动量计算:
- 代码中使用
(当前收盘价 / N天前收盘价) - 1来衡量动量。 g.lookback_days = 20代表使用一个月的趋势作为信号。你可以根据需要修改为 60(季度趋势)或 5(周趋势)。
- 代码中使用
-
资金管理:
total_value * 0.8: 期货自带杠杆,满仓交易风险极大。代码中预留了 20% 的资金作为保证金缓冲,避免因盘中波动导致强平。order_target_value: 这个函数非常方便,它会自动计算需要买卖的手数,使该标的的持仓市值等于目标金额。
-
主力合约切换:
- 策略逻辑中隐含了主力合约切换的处理。每周调仓时,
get_dominant_future会获取最新的主力合约。如果上周持有的合约不再是主力(或者动量掉出前三),它会在“平仓逻辑”中被卖出,并在“开仓逻辑”中买入新的主力合约。
- 策略逻辑中隐含了主力合约切换的处理。每周调仓时,
常见问题 Q&A
Q: 为什么只做多(Long Only)?
A: 商品期货虽然可以做空,但对于初学者或简单的动量策略,做多通常比做空容错率更高。商品价格往往具有“长期通胀、短期波动”的特性,且做空面临无限亏损的理论风险。如果你想加入做空逻辑,可以在筛选时选取动量为负且排名倒数的品种进行 side='short' 的开仓。
Q: 如何增加更多的品种?
A: 只需在 g.instruments 列表中添加对应的品种代码即可(例如添加 'AG' 白银, 'CF' 棉花等)。
Q: 回测时发现收益很高但回撤也很大,怎么办?
A: 动量策略本质上是趋势跟踪,在震荡市中会频繁止损(来回打脸)。优化方法包括:
- 增加波动率调整:根据品种的波动率分配仓位(波动率大的少买,小的多买)。
- 加入趋势过滤器:例如只有当价格在 60日均线之上时才做多。
- 分散化:加入更多相关性低的品种。
Q: 为什么使用 run_weekly 而不是每天判断?
A: 动量是一个中长期信号。如果每天调仓,不仅手续费高昂,而且容易被日内的市场噪音干扰。周频调仓在捕捉趋势和控制成本之间取得了较好的平衡。