-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathdividend_enhanced_strategy.py
More file actions
310 lines (239 loc) · 9.43 KB
/
Copy pathdividend_enhanced_strategy.py
File metadata and controls
310 lines (239 loc) · 9.43 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
306
307
308
309
310
# -*- coding: utf-8 -*-
from jqdata import *
from jqfactor import *
import pandas as pd
import numpy as np
import datetime
def initialize(context):
# 红利100指数
set_benchmark('399411.XSHE')
set_option('use_real_price', True)
set_option('avoid_future_data', True)
set_slippage(FixedSlippage(0.002))
set_order_cost(OrderCost(close_tax=0.001,
open_commission=0.0003,
close_commission=0.0003,
min_commission=5),
type='stock')
log.set_level('order', 'error')
g.stock_num = 30
g.lookback_years = 3
# 月初调仓
run_monthly(monthly_rebalance, 1, '10:30')
# 月度调仓
def monthly_rebalance(context):
target_list = get_target_list(context)
g.hold_list = list(context.portfolio.positions.keys())
# 先卖出不在目标池中的股票
for stock in g.hold_list:
if stock not in target_list:
position = context.portfolio.positions[stock]
close_position(context, position)
if len(target_list) == 0:
return
current_data = get_current_data()
total_value = context.portfolio.total_value
# 第一次等权
avg_value = total_value / len(target_list)
# 过滤掉单票金额连100股都买不起的股票
valid_list = []
for stock in target_list:
price = current_data[stock].last_price
if price is not None and (not np.isnan(price)) and price > 0:
if avg_value >= price * 100:
valid_list.append(stock)
else:
log.info("[%s] 单票分配金额%.2f不足买100股,跳过" % (stock, avg_value))
if len(valid_list) == 0:
log.info("本期无满足最小交易单位的可买标的")
return
# 对可买标的重新等权
final_avg_value = total_value / len(valid_list)
for stock in valid_list:
open_position(context, stock, final_avg_value)
def get_target_list(context):
yesterday = context.previous_date
# 1) 全A股票
stock_list = list(get_all_securities(types=['stock'], date=yesterday).index)
# 2) 过滤
stock_list = filter_pool(context, stock_list)
if len(stock_list) == 0:
return []
# 3) PB>0, ROE>0 基础因子过滤
df = get_fundamentals(
query(
valuation.code,
valuation.pb_ratio,
indicator.roe
).filter(
valuation.code.in_(stock_list),
valuation.pb_ratio > 0,
indicator.roe > 0
),
date=yesterday
)
if df is None or df.empty:
return []
candidates = list(df['code'])
# 4) 计算近3年累计股息率
div_df = calc_3y_dividend_yield(candidates, yesterday, g.lookback_years)
if div_df.empty:
return []
# 5) 排序取前30
div_df = div_df.sort_values(by='dividend_yield_3y', ascending=False)
target_list = list(div_df.head(g.stock_num * 3)['code'])
target_list = get_stock_industry(target_list, g.stock_num)
log.info('本期目标股票数: {}'.format(len(target_list)))
return target_list
def filter_pool(context, stock_list):
"""
常规过滤
"""
current_data = get_current_data()
yesterday = context.previous_date
# stock_list = filter_kcbj_stock(stock_list)
stock_list = filter_new_stock(context, stock_list)
stock_list = filter_paused_stock(stock_list)
stock_list = filter_st_stock(stock_list)
# stock_list = filter_limitup_stock(context, stock_list)
# stock_list = filter_limitdown_stock(context, stock_list)
return stock_list
def calc_3y_dividend_yield(stock_list, end_date, years=3):
"""
计算近3年累计股息率:
近3年现金分红总额 / 当前总市值
"""
if len(stock_list) == 0:
return pd.DataFrame(columns=['code', 'dividend_yield_3y'])
start_date = end_date - datetime.timedelta(days=365 * years + 30)
# 当前市值
val_df = get_fundamentals(
query(
valuation.code,
valuation.market_cap
).filter(
valuation.code.in_(stock_list)
),
date=end_date
)
if val_df is None or val_df.empty:
return pd.DataFrame(columns=['code', 'dividend_yield_3y'])
# market_cap 单位通常为亿元
val_df = val_df[['code', 'market_cap']].copy()
val_df['market_cap_rmb'] = val_df['market_cap'] * 1e8
# 近3年分红数据
q = query(
finance.STK_XR_XD.code,
finance.STK_XR_XD.report_date,
finance.STK_XR_XD.a_registration_date,
finance.STK_XR_XD.bonus_amount_rmb
).filter(
finance.STK_XR_XD.code.in_(stock_list),
finance.STK_XR_XD.a_registration_date >= start_date,
finance.STK_XR_XD.a_registration_date <= end_date
)
div_raw = finance.run_query(q)
if div_raw is None or div_raw.empty:
out = val_df[['code']].copy()
out['dividend_yield_3y'] = 0.0
return out
div_raw = div_raw[['code', 'bonus_amount_rmb']].copy()
div_sum = div_raw.groupby('code', as_index=False)['bonus_amount_rmb'].sum()
div_sum.rename(columns={'bonus_amount_rmb': 'dividend_rmb_3y'}, inplace=True)
df = pd.merge(val_df[['code', 'market_cap_rmb']], div_sum, on='code', how='left')
df['dividend_rmb_3y'] = df['dividend_rmb_3y'].fillna(0)
df['dividend_yield_3y'] = df['dividend_rmb_3y'] / df['market_cap_rmb']
return df[['code', 'dividend_yield_3y']]
#2-1 过滤停牌股票
def filter_paused_stock(stock_list):
current_data = get_current_data()
return [stock for stock in stock_list if not current_data[stock].paused]
#2-2 过滤ST及其他具有退市标签的股票
def filter_st_stock(stock_list):
current_data = get_current_data()
return [stock for stock in stock_list
if not current_data[stock].is_st
and 'ST' not in current_data[stock].name
and '*' not in current_data[stock].name
and '退' not in current_data[stock].name]
#2-3 过滤科创北交股票
def filter_kcbj_stock(stock_list):
return [stock for stock in stock_list
if not (stock[0] == '4' or stock[0] == '8' or stock[:2] == '68')]
#2-4 过滤涨停的股票
def filter_limitup_stock(context, stock_list):
last_prices = history(1, unit='1m', field='close', security_list=stock_list)
current_data = get_current_data()
return [stock for stock in stock_list if stock in context.portfolio.positions.keys()
or last_prices[stock][-1] < current_data[stock].high_limit]
#2-5 过滤跌停的股票
def filter_limitdown_stock(context, stock_list):
last_prices = history(1, unit='1m', field='close', security_list=stock_list)
current_data = get_current_data()
return [stock for stock in stock_list if (stock in context.portfolio.positions.keys()
or last_prices[stock][-1] > current_data[stock].low_limit)
]
#2-6 过滤次新股
def filter_new_stock(context,stock_list):
yesterday = context.previous_date
return [stock for stock in stock_list if not yesterday - get_security_info(stock).start_date < datetime.timedelta(days=375)]
# 获取股票所属行业
def get_stock_industry(stock_list, num_stocks):
result = get_industry(security=stock_list)
selected_stocks = []
industry_list = []
for stock_code, info in result.items():
industry_name = info['sw_l2']['industry_name']
if industry_name not in industry_list:
industry_list.append(industry_name)
selected_stocks.append(stock_code)
if len(industry_list) == num_stocks :
break
return selected_stocks
#3-1 交易模块-自定义下单
def order_target_value_(context, security, value):
current_data = get_current_data()
price = current_data[security].last_price
if price is None or np.isnan(price) or price <= 0:
return None
if value == 0:
return order_target(security, 0)
position = context.portfolio.positions[security] if security in context.portfolio.positions else None
current_amount = position.total_amount if position else 0
target_amount = int(value / price / 100) * 100
if target_amount < 100:
return None
if target_amount == current_amount:
return None
if target_amount > current_amount and target_amount - current_amount < 100:
return None
return order_target(security, target_amount)
#3-2 交易模块-开仓/调仓
def open_position(context, security, value):
order = order_target_value_(context, security, value)
if order is not None:
return True
return False
#3-3 交易模块-平仓
def close_position(context, position):
security = position.security
order = order_target_value_(context, security, 0) # 可能会因停牌失败
if order != None:
if order.status == OrderStatus.held and order.filled == order.amount:
return True
return False
#4-1 判断今天是否为空仓日
def today_is_between(context, start_date, end_date):
today = context.current_dt.strftime('%m-%d')
if (start_date <= today) and (today <= end_date):
return True
else:
return False
#4-2 清仓后次日资金可转
def close_account(context):
if g.no_trading_today_signal == True:
if len(g.hold_list) != 0:
for stock in g.hold_list:
position = context.portfolio.positions[stock]
close_position(context, position)
log.info("清仓后次日资金可转,卖出[%s]" % (stock))