量化交易初探(三)——择股与回测" I. k$ w5 g" z, y& E
5 Z c5 A8 ]+ T/ A/ T+ W2 q4 m4 S
在之前的两篇文章中,我们分别展示了基于baostock模块的A股数据获取脚本和基于backtrader模块的简单择时回测框架,详见: 7 Q7 t4 v; o4 z# Y: X9 X
在之前代码的基础上,我们将要添加择股回测的功能,这样就把量化交易的范围拓展到全市场,进一步丰富了策略选择的空间。
8 m- m- _/ p8 \( y与之前重复的代码不再展示,这里仅放上修改的部分供参考。为了方便,这次把数据获取的部分也写到回测脚本中了,从代码规整的角度还是建议分开:
. w- B9 s, |+ |首先是主程序部分:# O3 F, u- O0 i; K" {5 M: m( U( I3 G
if __name__ == '__main__':. H/ U1 `; A3 N; i+ S `" N
, d4 |, a! w: ?" y( q3 {
# 主程序其他部分省略
9 y, j- v. {% {4 D2 W ...
3 F5 s$ O. j# w$ _8 \! u dataset = [] \, ]3 e7 G( Q5 ^- Q! G1 G
codeset = []
& t# N$ z- G. r% X8 M" k for item in alternative_code: # 获取备选股池中个股数据
: L0 i! ?9 Z' F* w: O rs = get_share_info(item, date_start, date_end, adjustflag="3")8 j7 m7 v; }( N8 a
data, code = create_dataset(rs)
8 w/ o7 g4 n* N" e% S1 E3 O dataset.append(data)% @6 v1 O: j' x8 [' u3 g/ K
codeset.append(code)
! L. A% @& { _8 q - f% i" P9 `/ Z: N7 z8 W, e+ o
trade_info = pd.read_csv("trade_info.csv", parse_dates=['trade_date'])
( J& ^; p% @5 t5 Q e, {9 J2 G+ G5 E, _8 r 7 z, ], G$ t8 m* S+ L
cerebro, results = run_backtest(Strategy_MA, dataset, codeset, startcash, date_start, date_end)
' ^9 s9 Q) G1 M数据获取和预处理:
' E; @: a9 T: P5 b, ]def get_share_info(code, start_date, end_date, adjustflag="1"):
% R |( L4 O) r7 f # 读取日频K线数据
0 x5 _ s- ~4 F8 {+ X2 s( {
4 ]4 S& X& j0 [- E fields = "date,code,open,high,low,close,volume"
( N7 \6 F X6 d+ Q/ w x! e0 E- |, g9 d2 U& P
rs = bs.query_history_k_data_plus(code=code,) F4 E2 h1 Z) R" M
fields=fields, start_date=start_date, end_date=end_date, # 为空默认从2015-01-01到最近一个交易日
~+ ]! B" ?0 Z1 T9 g frequency='d', adjustflag=adjustflag)
' T+ Q( R( l/ T& p5 l/ Z+ Y # adjustflag 复权类型,不修改则默认不复权/ I0 j! L# _; }& r+ i( h1 r
print('query_history_k_data_plus respond error_code:'+rs.error_code)4 {7 F8 a; o) C" F9 q; U9 z
print('query_history_k_data_plus respond error_msg:'+rs.error_msg)
+ ?$ S, t+ I- B) F1 i) B data_list = []
+ X- _1 }1 Z3 F3 n0 n9 ]9 x while (rs.error_code == '0') & rs.next():' b, S" `) @* @8 s
# 获取一条记录,将记录合并在一起) M$ f+ h8 W" N8 |$ t# v9 O2 C5 a
data_list.append(rs.get_row_data())% O! \0 C3 t# v" `$ N8 c; D- t3 [% y
result = pd.DataFrame(data_list, columns=rs.fields)
8 |0 U8 h* ?' Z& P
+ c+ ?4 |+ |( |0 Y1 F7 G& r- b8 x! ~ return result
H5 M3 ]" Z9 P" |
) `: G1 }: ?% {" zdef create_dataset(data_bef):
9 t1 @3 [) [% i- W2 y7 f3 X # 将所有备选股票数据导入同一数据集# x4 a- I* o7 i# i8 ~+ V9 b5 N
n+ `, o0 i9 d
# 日期对齐/ A7 T3 g" E2 S7 E5 Z1 Z% ?
date = pd.DataFrame(index=data_bef.index.unique()) # 获取回测区间内所有交易日* L4 P4 }% L3 |3 A$ G7 e
code = data_bef.loc[0, 'code']
. {) l! @! g8 p df = data_bef[['date','open','high','low','close','volume']].copy()
) Z6 h" k8 c- `4 Y; u+ N& M df.loc[:,['open','high','low','close']] = df.loc[:,['open','high','low','close']].copy().astype(float)
: `* {2 L% |5 X0 |7 x; Y' O4 ^ df.loc[:,['volume']] = df.loc[:,['volume']].copy().astype(int)+ j. |1 w2 v3 u% p1 K9 H, f* |
data = pd.merge(date, df, left_index=True, right_index=True, how='left')
$ T) m- I L% `/ S, ?, Q/ A, u data['date'] = pd.to_datetime(data['date']) # 这是新添加的行0 ^! u+ v% ?+ c: Y1 @: A f7 H
data.set_index(['date'],inplace=True)
& i3 c9 O1 }4 E; k q # 缺失值处理:
: c3 I a- m* G5 Y; ~9 _. _ data.loc[:,['volume']] = data.loc[:,['volume']].fillna(0)
; C" y+ l' I u1 H6 Q data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(method='pad')! _% p X! i( g5 G. Y3 p
data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(0) G/ j) j* X5 t7 A3 o7 _
i0 e: F$ `: n
return data, code
: J3 Q) W) L: ^6 T- A回测模块:" {; F M9 R0 Z; A& E
class Strategy_MA(bt.Strategy):
$ Z' q; r6 I& c; _8 ~) H # 省略其他部分
8 P2 M* D' r% [) f5 f .../ B5 U v$ `# N0 U
def next(self):2 S5 m. i% _, W# l
) {5 Q$ l+ d6 Z4 `1 T) @; u
dt = self.datas[0].datetime.date(0) # 获取当前的回测时间点
9 e5 y6 y' ^) g+ ~0 T' }" c# H # 如果是调仓日,则进行调仓操作: E9 J2 `- z6 t4 b7 ~; A
if dt in self.trade_dates:
' Q0 _9 ?% q! [8 Y5 \% p! M print("--------------{} 调仓----------".format(dt))
5 P3 p" O* L. o$ z: p- [ if len(self.order_list) > 0:; `4 ? B4 I8 B; \
for od in self.order_list:. ^+ K1 K/ p ^( b0 U& D) |2 b
self.cancel(od) # 撤销未完成订单
8 i( m6 \$ K. c8 t3 K( w self.order_list = [] # 重置订单列表, D& k4 t7 r% m5 Z% O- P
# 提取当前调仓日的持仓列表
4 d% ]5 `) l& o! x4 s/ f buy_stocks_data = self.buy_stock.query(f"trade_date=='{dt}'")5 [/ [6 ^( f. B- K; `
long_list = buy_stocks_data['sec_code'].tolist()3 @7 w: H( P ~# o: V8 H; w, O
print('long_list', long_list) # 打印持仓列表4 i, v) S/ g$ C7 I) Q
# 不再继续持有的股票进行平仓
# _* c! k! A( Z* K2 x sell_stock = [i for i in self.buy_stocks_pre if i not in long_list]
4 o- H$ P/ L8 y. _. x$ a print('sell_stock', sell_stock) # 打印平仓列表
' M9 m4 ?6 k2 b2 x+ @ if len(sell_stock) > 0:
" q' U& E' a5 }" y print("-----------平仓--------------")$ k2 \8 K/ q- M/ c
for stock in sell_stock:4 d5 L. n/ E$ _: Y# R. s. T7 D
data = self.getdatabyname(stock)* |. h0 T/ T/ b) T4 _
if self.getposition(data).size > 0 :9 Z! @ ^ c2 T
od = self.close(data=data)! J% s* G4 O W( t; K4 {6 F
self.order_list.append(od) # 记录卖出订单, u. v9 y. \! K
print("-----------买入此次调仓的股票--------------")
, C5 v1 i; _$ u; s4 G! b0 R for stock in long_list: r7 U7 v- }2 Z( p* Q. g( G
w = buy_stocks_data.query(f"sec_code=='{stock}'")['weight'].iloc[0] # 提取持仓权重
! S2 t! w4 h$ L: s2 D, M0 r7 u data = self.getdatabyname(stock)
% I/ Z( _4 @+ @+ ^- O& C: c5 X order = self.order_target_percent(data=data, target=w*0.95) # 为减少可用资金不足的情况,留 5% 的现金做备用,该函数以多退少补的方式进行购买/卖出
6 k; N* L! f5 v, D- b* r print()
% }* D; P# d5 s3 y) ^& e- X self.order_list.append(order)8 S: J' [/ y, r! `
! I9 \: B7 Z" d3 u6 q" y self.buy_stocks_pre = long_list # 保存此次调仓的股票列表
! i& g h6 k7 r' D1 g# y, N回测执行:
: j. z0 K1 B0 C, e' R' v {: ~* [# Jdef run_backtest(strategy, dataset, codeset, startcash, start, end):
2 r' K2 a4 j: g- d" v # 省略其他部分
, ^, h* i' l9 e ...1 d1 O& S# a) w4 t
for item in range(len(dataset)):
1 ^2 U. S9 C8 J8 o+ f code = codeset[item]
, g) p$ c& g) u: }4 z/ A+ W cerebro.adddata(bt.feeds.PandasData(dataname=dataset[item],fromdate=datetime.strptime(start, '%Y-%m-%d'),todate=datetime.strptime(end, '%Y-%m-%d')), name=code)
O& e# Q' f# y9 C6 Z # 添加策略
* Y8 X0 F0 y) \( _ cerebro.addstrategy(strategy, end_date=datetime.strptime(end, '%Y-%m-%d'))9 E. T$ |5 h7 \
results = cerebro.run()
" ~! q u6 x: @+ R9 U& W6 u) y 5 U, z9 i2 @8 `, }2 o: ]- T5 p7 b. m- d
return cerebro, results
7 z1 [; o R( @7 f. @1 O+ |可以看到,这次代码中并没有直接编写择股策略,而是导入已经确定好的调仓信息"trade_info.csv"进行回测,这样我们就可以将相对复杂的策略推理部分放在另一个脚本中编写,以提升回测程序的执行效率。关于择股策略编写的部分将在之后的文章中分享,欢迎感兴趣的朋友多多批评指正。 |