量化交易初探(三)——择股与回测
; _, b7 {0 Y% S7 h) s1 g4 L' F# y- L3 G8 M
在之前的两篇文章中,我们分别展示了基于baostock模块的A股数据获取脚本和基于backtrader模块的简单择时回测框架,详见: 7 z8 v1 z& m. g) G
在之前代码的基础上,我们将要添加择股回测的功能,这样就把量化交易的范围拓展到全市场,进一步丰富了策略选择的空间。
3 p/ Z J+ i+ g0 g( \- b) N与之前重复的代码不再展示,这里仅放上修改的部分供参考。为了方便,这次把数据获取的部分也写到回测脚本中了,从代码规整的角度还是建议分开:8 L2 C# @) v1 p5 B- y8 r; w0 G8 W
首先是主程序部分:
3 I* L8 c9 S5 Hif __name__ == '__main__':
" w1 X. x/ M# e ?6 d
4 Y- ^0 J( B8 j7 ^ # 主程序其他部分省略( g* @4 E( q- d6 B9 |7 z6 ]
...6 |& I+ {' Z' ~) \6 f
dataset = []
" k& S6 H1 X0 X) j$ s% e& N codeset = []
: q5 m c5 R# X( h* {6 @8 s, m7 L for item in alternative_code: # 获取备选股池中个股数据
; A9 l6 P1 g$ d, z$ b rs = get_share_info(item, date_start, date_end, adjustflag="3"), k6 H4 Z4 {* N
data, code = create_dataset(rs)5 P7 s& z7 l# c' }
dataset.append(data) Z# L/ A h0 W& X/ z
codeset.append(code)% n0 Z# B6 F+ u
" Z" c: A0 Q4 f1 v( p8 x trade_info = pd.read_csv("trade_info.csv", parse_dates=['trade_date'])
$ [3 x# D! f, L/ H* @$ }5 t
$ j( S& w- l1 b& e2 r7 b cerebro, results = run_backtest(Strategy_MA, dataset, codeset, startcash, date_start, date_end)
' e+ u9 F G5 R2 E" u% F数据获取和预处理:& ^0 m- X( L/ j. {$ R
def get_share_info(code, start_date, end_date, adjustflag="1"):
+ |8 F& f* ]7 A( T: p # 读取日频K线数据
' t. D% N; Q h9 R( {) T 2 ]. E& Y* r0 i& w/ q) b' T
fields = "date,code,open,high,low,close,volume"- R! x0 ? m* @- M& y1 T
. f# P& j7 h6 O- o$ I
rs = bs.query_history_k_data_plus(code=code," P2 H- I9 k4 t/ Z
fields=fields, start_date=start_date, end_date=end_date, # 为空默认从2015-01-01到最近一个交易日
& d0 P2 i" v/ H7 w2 x* P8 Q, ]( S frequency='d', adjustflag=adjustflag)
% X# `5 R7 o. a2 _3 u% \* n # adjustflag 复权类型,不修改则默认不复权
5 Y, K/ G/ |+ ?8 l* L# [& x# c2 }1 z print('query_history_k_data_plus respond error_code:'+rs.error_code)
! S2 }. I W9 f" Q# [ print('query_history_k_data_plus respond error_msg:'+rs.error_msg)' ]3 f' O. C( L* _% O
data_list = []1 ^8 V7 u* g2 \& A5 L7 M
while (rs.error_code == '0') & rs.next():. v1 x) p6 K9 z# z5 c
# 获取一条记录,将记录合并在一起
3 f; S) o/ J' e) _- l data_list.append(rs.get_row_data())
7 C5 F1 p) R; u result = pd.DataFrame(data_list, columns=rs.fields)% ~/ b& t% g. F8 i j4 P
4 [$ ]4 V8 o& Q# S% E return result
/ _ I4 P. a0 ^: o
) p; }. `" v/ Y0 ~" }2 pdef create_dataset(data_bef):
% D) o) b9 g% d# Z # 将所有备选股票数据导入同一数据集! p, B7 R0 I" } t! L, _; u
, E1 _9 y( f1 C; R8 `$ U! ]
# 日期对齐
& c& X; U# N# u. R b' C date = pd.DataFrame(index=data_bef.index.unique()) # 获取回测区间内所有交易日$ U: n. b( l6 C* _. U/ i0 a5 P9 g
code = data_bef.loc[0, 'code']
' q& [9 u8 R7 E6 N9 f' H! j$ S& m df = data_bef[['date','open','high','low','close','volume']].copy()2 k9 ^, H/ w* s- q$ W
df.loc[:,['open','high','low','close']] = df.loc[:,['open','high','low','close']].copy().astype(float)
* V1 j/ |7 o. v1 n df.loc[:,['volume']] = df.loc[:,['volume']].copy().astype(int). g, D/ G* ?8 |
data = pd.merge(date, df, left_index=True, right_index=True, how='left')$ k* g( }2 O% X4 L$ J" ~
data['date'] = pd.to_datetime(data['date']) # 这是新添加的行
# |$ W/ Y9 |( C3 K, k3 X/ q1 c* d data.set_index(['date'],inplace=True)( F0 R3 U) O' K0 j9 v$ C- h0 d& L
# 缺失值处理:
; Z! e& z' T# D% I2 ^/ x data.loc[:,['volume']] = data.loc[:,['volume']].fillna(0)
1 g! `9 j& a8 U: r$ A/ ]+ N1 b data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(method='pad')' A$ U9 L2 j9 B3 Y6 Y, O
data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(0)
6 X; b; \' N: O- U" g- W& g, S# G" ^6 E2 e
return data, code* d" |0 U5 a' m$ M
回测模块:
# ~3 g; d; a( `# Tclass Strategy_MA(bt.Strategy):
3 d2 w! t: q' H! X5 O # 省略其他部分: C/ E0 g' }0 y7 r3 }
...9 n+ G# E4 n# A: c% q: D
def next(self):
/ T& i Y; p7 j/ V \
) ]1 ^8 @2 j* X6 w/ D dt = self.datas[0].datetime.date(0) # 获取当前的回测时间点1 Q- l8 o6 K# r! L* M8 T0 F
# 如果是调仓日,则进行调仓操作5 [3 r1 Q! x! @$ z6 \# A6 T
if dt in self.trade_dates:
. w1 I. w. X* T, o D! }3 `, c print("--------------{} 调仓----------".format(dt))/ \) z5 O- |0 w6 k2 K
if len(self.order_list) > 0:
/ t, F2 `- ^+ m; l for od in self.order_list:
5 y; D* z% [/ i0 E9 f5 M% o5 [ self.cancel(od) # 撤销未完成订单
% ]: l; ?" n# h6 Z# E8 S self.order_list = [] # 重置订单列表
: a& u1 _8 y3 E f5 W# [. H/ g # 提取当前调仓日的持仓列表/ f( Y' f3 h, c4 P% P
buy_stocks_data = self.buy_stock.query(f"trade_date=='{dt}'")9 W# i& Z! B f& }
long_list = buy_stocks_data['sec_code'].tolist()! R: d' ]7 L3 X" T' E4 R
print('long_list', long_list) # 打印持仓列表
3 |5 Q& B* j/ h% f# }3 Z' x # 不再继续持有的股票进行平仓
: O. e1 y- Z3 ~: q6 {& t sell_stock = [i for i in self.buy_stocks_pre if i not in long_list]* M7 `; E8 ~* @5 ]# O
print('sell_stock', sell_stock) # 打印平仓列表
' @. h% `5 z9 i9 \4 e% v if len(sell_stock) > 0:
0 t0 E" `" t& B$ g# }) F print("-----------平仓--------------")/ T0 n# g$ g( i2 s0 s
for stock in sell_stock:
/ a# Y! ~! {" A0 I U data = self.getdatabyname(stock) v( o; @4 p5 m0 p
if self.getposition(data).size > 0 :
, }* X0 w) e- T od = self.close(data=data)7 \3 A0 U, W- d3 T9 o7 M
self.order_list.append(od) # 记录卖出订单5 O) A" `9 W9 l! k7 `9 p% Y
print("-----------买入此次调仓的股票--------------")
7 [) S0 _+ f$ h g, }: @" [& V for stock in long_list:# o9 Z, j; {$ N/ t4 c
w = buy_stocks_data.query(f"sec_code=='{stock}'")['weight'].iloc[0] # 提取持仓权重, j( o9 N8 E4 p7 v. F
data = self.getdatabyname(stock)
; E, X t7 L& O8 N: _ order = self.order_target_percent(data=data, target=w*0.95) # 为减少可用资金不足的情况,留 5% 的现金做备用,该函数以多退少补的方式进行购买/卖出$ m, A6 N2 {' q( [3 v: i
print()4 L4 }3 Z% D. s6 x* k* G+ L
self.order_list.append(order)
) z' L( q& Y i, y! x ( c8 j* h( R* B. y( j
self.buy_stocks_pre = long_list # 保存此次调仓的股票列表 / R9 A$ g/ L5 g' U/ E
回测执行:
% U! m2 y7 N7 b' u0 ~) Ydef run_backtest(strategy, dataset, codeset, startcash, start, end):: ~: o6 S. u# u0 G+ X3 k
# 省略其他部分1 z7 A& w6 R# ~% o9 r' g8 L
...
9 M7 P' T6 ^6 _ for item in range(len(dataset)):
% W- j4 O+ n/ G code = codeset[item]
+ @3 b& w! s+ r* } cerebro.adddata(bt.feeds.PandasData(dataname=dataset[item],fromdate=datetime.strptime(start, '%Y-%m-%d'),todate=datetime.strptime(end, '%Y-%m-%d')), name=code)
9 M" D E W _) L) F # 添加策略% g1 h) {3 B; D) n( q1 ?
cerebro.addstrategy(strategy, end_date=datetime.strptime(end, '%Y-%m-%d'))# v f9 E, Z& F! P, K* o: Y
results = cerebro.run()
1 c- ?" D- u: \2 u
$ M5 C+ N. q9 Q5 C% Z/ c. d return cerebro, results/ a/ H5 z. v2 O3 s5 T
可以看到,这次代码中并没有直接编写择股策略,而是导入已经确定好的调仓信息"trade_info.csv"进行回测,这样我们就可以将相对复杂的策略推理部分放在另一个脚本中编写,以提升回测程序的执行效率。关于择股策略编写的部分将在之后的文章中分享,欢迎感兴趣的朋友多多批评指正。 |