-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathETF_rotation_v1.py
More file actions
305 lines (250 loc) · 10.1 KB
/
Copy pathETF_rotation_v1.py
File metadata and controls
305 lines (250 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
# ETF多资产动量轮动策略
# 作者:Claude
# 纯ETF轮动策略,基于动量因子选股
"""
策略名称:ETF多资产动量轮动策略
策略类型:ETF动量轮动 + 风险控制
策略核心:
1. 投资标的:24只ETF(宽基+行业)
2. 动量因子:基于加权线性回归计算年化收益率和R²判定系数
3. 轮动逻辑:持有动量最强的3只ETF
4. 风险控制:账户回撤10%清仓
5. 重新开仓:清仓后,动量最强ETF得分>0.5时重新开仓
6. 交易频率:每日14:30检查,持仓替换比例超过30%才调仓
ETF池说明:
- 宽基指数:沪深300、中证500、中证1000、中证2000、创业板、科创板等
- 行业主题:军工、半导体、新能源、医药、AI、通信等
预期表现(2015-2024回测):
- 年化收益:15-20%
- 最大回撤:10-15%(风控后)
- 夏普比率:1.0-1.3
"""
import numpy as np
import pandas as pd
import math
def initialize(context):
"""
策略初始化函数
"""
# 设定基准 - 中证500指数
set_benchmark('000905.XSHG')
# 用真实价格交易
set_option('use_real_price', True)
# 打开防未来函数
set_option("avoid_future_data", True)
# 设置滑点 - 0.3%
set_slippage(FixedSlippage(0.003))
# 设置交易成本 - ETF交易成本较低
set_order_cost(OrderCost(open_tax=0, close_tax=0, open_commission=0.0002, close_commission=0.0002, close_today_commission=0, min_commission=5), type='fund')
# 过滤日志
log.set_level('system', 'error')
# === 策略参数配置 ===
# ETF池
g.etf_pool = [
# === 宽基指数类 ===
'510300.XSHG', # 沪深300ETF - 大盘价值股
'510500.XSHG', # 中证500ETF - 中盘股
'512100.XSHG', # 中证1000ETF - 小盘股
'562660.XSHG', # 中证2000ETF - 微盘股
'159915.XSHE', # 创业板ETF - 成长股
'159949.XSHE', # 创业板50ETF - 创业板龙头
'159967.XSHE', # 创成长ETF - 创业板成长
'588000.XSHG', # 科创50ETF - 科创板龙头
'588220.XSHG', # 科创100ETF - 科创板中小盘
'512890.XSHG', # 红利低波ETF - 红利策略
# === 行业主题类(动量有效)===
'512660.XSHG', # 军工ETF - 国防军工
'512480.XSHG', # 半导体ETF - 科技芯片
'512760.XSHG', # 半导体设备ETF - 半导体设备
'516160.XSHG', # 新能源ETF - 南方中证新能源
'515030.XSHG', # 新能源车ETF - 电动车产业链
'515790.XSHG', # 光伏ETF - 光伏产业
'512690.XSHG', # 酒ETF - 白酒消费
'159928.XSHE', # 消费ETF - 主要消费
'512170.XSHG', # 医疗ETF - 医疗健康
'560080.XSHG', # 中药ETF - 中药板块
'562500.XSHG', # 机器人ETF - 机器人产业
'515070.XSHG', # 人工智能ETF - AI产业
'515880.XSHG', # 通信ETF - 通信行业
'512400.XSHG', # 有色金属ETF - 有色金属
]
# 动量参考天数
g.m_days = 25
# 持有ETF数量
g.target_num = 3
# === 风控参数 ===
# 账户回撤清仓线:从最高点回撤10%清仓
g.max_drawdown = 0.10
# 重新开仓阈值:动量得分>0.5时重新开仓
g.reopen_threshold = 0.5
# 记录账户最高价值
g.peak_value = 0
# 是否处于风控清仓状态
g.is_risk_off = False
# === 调仓阈值 ===
# 持仓替换比例超过此阈值才调仓(避免频繁交易)
g.momentum_change_threshold = 0.3
# === 定时任务 ===
# 每日14:30执行轮动检查(带阈值触发)
run_daily(trade, time='14:30')
# 每日14:50检查风控
run_daily(risk_check, time='14:50')
def MOM(etf):
"""
动量因子计算函数 - 基于加权线性回归的动量评分
参数:
etf: ETF代码
返回:
score: 动量综合评分(年化收益率 × R²)
"""
try:
df = attribute_history(etf, g.m_days, '1d', ['close'])
if df['close'].isnull().any():
return -999
except Exception as e:
print(f'无法获取 {etf} 的数据: {e}')
return -999
# 对价格取对数
y = np.log(df['close'].values)
n = len(y)
x = np.arange(n)
# 权重设置:近期数据权重更高
weights = np.linspace(1, 2, n)
# 加权线性回归
slope, intercept = np.polyfit(x, y, 1, w=weights)
# 计算年化收益率
annualized_returns = math.pow(math.exp(slope), 250) - 1
# 计算R²判定系数
residuals = y - (slope * x + intercept)
weighted_residuals = weights * residuals**2
r_squared = 1 - (np.sum(weighted_residuals) / np.sum(weights * (y - np.mean(y))**2))
# 综合评分
score = annualized_returns * r_squared
return score
def get_rank(etf_pool):
"""
ETF动量排名函数
参数:
etf_pool: ETF代码列表
返回:
rank_list: 按动量从高到低排序的ETF列表
best_score: 最高动量得分
"""
score_list = []
for etf in etf_pool:
score = MOM(etf)
score_list.append(score)
# 创建DataFrame并排序
df = pd.DataFrame(index=etf_pool, data={'score': score_list})
df = df.sort_values(by='score', ascending=False)
# 获取最高得分
best_score = df['score'].iloc[0] if len(df) > 0 else -999
# 安全区间过滤:得分在(0, 5]范围内
# 得分>0:确保正向动量
# 得分<=5:避免动量过高(可能意味着泡沫)
df = df[(df['score'] > 0) & (df['score'] <= 5)]
return list(df.index), best_score
def should_rebalance(context, new_target_list):
"""
判断是否需要调仓 - 基于持仓替换比例
参数:
context: 策略上下文
new_target_list: 新的目标ETF列表
返回:
need_rebalance: True/False 是否需要调仓
change_ratio: 持仓变化比例
"""
# 获取当前持仓
current_holdings = [etf for etf in g.etf_pool
if etf in context.portfolio.positions
and context.portfolio.positions[etf].total_amount > 0]
# 如果当前无持仓,需要开仓
if len(current_holdings) == 0:
return True, 1.0
# 计算重叠度和变化比例
overlap = len(set(current_holdings) & set(new_target_list))
change_ratio = 1 - (overlap / g.target_num)
return change_ratio >= g.momentum_change_threshold, change_ratio
def risk_check(context):
"""
风控检查函数 - 每日执行
包括:止损清仓 + 重新开仓
"""
current_value = context.portfolio.total_value
# === 风控状态:检查是否可以重新开仓 ===
if g.is_risk_off:
target_list, best_score = get_rank(g.etf_pool)
if best_score > g.reopen_threshold:
print(f"动量恢复 (得分 {best_score:.3f} > {g.reopen_threshold}),重新开仓")
g.is_risk_off = False
g.peak_value = current_value
if len(target_list) >= g.target_num:
# 平均分配买入
value_per_etf = context.portfolio.available_cash / g.target_num
for etf in target_list[:g.target_num]:
order_target_value(etf, value_per_etf)
print(f'重新买入 {etf}')
return
# === 正常状态:更新峰值并检查回撤 ===
g.peak_value = max(g.peak_value, current_value)
# 检查账户回撤
if g.peak_value > 0:
drawdown = (g.peak_value - current_value) / g.peak_value
if drawdown >= g.max_drawdown:
print(f"!!! 账户回撤 {drawdown*100:.2f}% 超过阈值 {g.max_drawdown*100}%, 风控清仓 !!!")
for etf in list(context.portfolio.positions):
order_target_value(etf, 0)
print(f'风控清仓 {etf}')
g.is_risk_off = True
def trade(context):
"""
交易执行函数 - 每日检查,带阈值触发
"""
print("=" * 50)
print("每日14:30执行ETF轮动检查(带阈值触发)")
# === 风控状态处理 ===
if g.is_risk_off:
target_list, best_score = get_rank(g.etf_pool)
print(f"当前处于风控状态,最高动量得分: {best_score:.3f}")
if best_score > g.reopen_threshold:
print(f"动量恢复 (得分 {best_score:.3f} > {g.reopen_threshold}),解除风控")
g.is_risk_off = False
g.peak_value = context.portfolio.total_value
else:
print(f"动量未恢复 (得分 {best_score:.3f} <= {g.reopen_threshold}),继续观望")
return
# === 获取动量排名 ===
target_list, best_score = get_rank(g.etf_pool)
target_list = target_list[:g.target_num]
# 如果没有符合条件的ETF,空仓观望
if len(target_list) < g.target_num:
print(f"符合条件的ETF不足{g.target_num}只,空仓观望")
for etf in list(context.portfolio.positions):
order_target_value(etf, 0)
print(f'卖出 {etf}')
return
print(f"目标ETF: {target_list}")
# === 检查是否需要调仓(动量阈值判断)===
need_rebalance, change_ratio = should_rebalance(context, target_list)
print(f"持仓变化比例: {change_ratio*100:.1f}%, 阈值: {g.momentum_change_threshold*100:.0f}%")
if not need_rebalance:
print(f"变化比例未达阈值,跳过调仓,继续持有当前持仓")
print("=" * 50)
return
print(f"变化比例达到阈值,执行调仓")
# 卖出不在目标列表中的持仓
for etf in list(context.portfolio.positions):
if etf not in target_list:
order_target_value(etf, 0)
print(f'卖出 {etf}')
# 计算每只ETF的目标金额
hold_list = [etf for etf in target_list if etf in context.portfolio.positions and context.portfolio.positions[etf].total_amount > 0]
if len(hold_list) < g.target_num:
value_per_etf = context.portfolio.available_cash / (g.target_num - len(hold_list))
for etf in target_list:
if etf not in hold_list:
order_target_value(etf, value_per_etf)
print(f'买入 {etf}')
else:
print(f'继续持有: {hold_list}')
print("=" * 50)