量化交易初探(三)——择股与回测
+ _; h! s& x2 ? \7 ^
. P p' [& D5 ?8 F2 Y* ]- e( ^在之前的两篇文章中,我们分别展示了基于baostock模块的A股数据获取脚本和基于backtrader模块的简单择时回测框架,详见:
" o4 M) T5 ^9 k" _6 N- x在之前代码的基础上,我们将要添加择股回测的功能,这样就把量化交易的范围拓展到全市场,进一步丰富了策略选择的空间。
; @; g! l! L" b A. H$ b# s+ u7 l与之前重复的代码不再展示,这里仅放上修改的部分供参考。为了方便,这次把数据获取的部分也写到回测脚本中了,从代码规整的角度还是建议分开:
1 y' U5 U7 p2 N首先是主程序部分:: ]# [9 W, h. B
if __name__ == '__main__':/ P$ [4 @# V+ P" F: s& I
2 }0 Z+ @% o6 a5 y3 k: J
# 主程序其他部分省略 ]3 o( y8 [* U" N2 I8 |/ J
...+ L8 _( u8 p" I+ f) T
dataset = []
* \/ Q, O, O5 \; J; [2 N' n+ O codeset = []" ?0 W8 g! P$ i! a1 m/ a4 V
for item in alternative_code: # 获取备选股池中个股数据
5 D* _5 M% I f3 K/ @6 J rs = get_share_info(item, date_start, date_end, adjustflag="3")+ a/ p. u: f) }/ @
data, code = create_dataset(rs)9 s+ Z$ j; }3 `6 I7 M
dataset.append(data)$ p3 R0 `* i3 O$ t
codeset.append(code)
" L1 O, w9 `8 }6 d; }+ j# \
0 F4 e! \1 J8 B' F2 i trade_info = pd.read_csv("trade_info.csv", parse_dates=['trade_date'])
% ^0 [! M" K4 p7 P+ |! p$ k ~ 5 `. h1 s' ]9 j2 N3 ^& c
cerebro, results = run_backtest(Strategy_MA, dataset, codeset, startcash, date_start, date_end)
: W8 Q0 Z( e; C4 R: r数据获取和预处理:
/ K$ L7 w0 m3 hdef get_share_info(code, start_date, end_date, adjustflag="1"):
* E y4 M5 G" n6 d! | # 读取日频K线数据2 {$ j- [, R- c. p9 t% S
' d& o) [' s& W5 O1 C* R4 | fields = "date,code,open,high,low,close,volume"
L6 \0 ?/ R5 [1 i" k, I. i5 p9 v
/ ^ U9 o- o( p+ A2 b rs = bs.query_history_k_data_plus(code=code,8 H% ]# g \+ M* u8 d) a; v1 O" X7 ~
fields=fields, start_date=start_date, end_date=end_date, # 为空默认从2015-01-01到最近一个交易日1 w' K4 e. K" Q4 N. b
frequency='d', adjustflag=adjustflag) ( ?" _: T) ^+ J( k' w2 C0 h
# adjustflag 复权类型,不修改则默认不复权
* z* ?: O. t3 \$ ^ print('query_history_k_data_plus respond error_code:'+rs.error_code)) K& i* I/ }1 p, M5 k" b/ D9 l
print('query_history_k_data_plus respond error_msg:'+rs.error_msg)6 `; T* E: l! b# d+ T, {
data_list = []
5 W$ Y* Y! O0 V& r% x p while (rs.error_code == '0') & rs.next():2 C+ s- n. `5 p
# 获取一条记录,将记录合并在一起
4 h! g0 J4 W4 l- _0 O) r data_list.append(rs.get_row_data())
( G& }1 e2 I& \$ N6 C+ m U result = pd.DataFrame(data_list, columns=rs.fields)
* R2 q+ U& B( y- }$ S) J
. u2 g' h7 E7 l1 F return result% \5 ^( ?5 ~4 g( ]% ]3 n
/ f3 Q" C0 h- @2 o0 ?: e
def create_dataset(data_bef):
2 a9 j" @: Y/ @7 `* Q4 i& A # 将所有备选股票数据导入同一数据集/ X, `6 `4 R- [( e. S: t/ o5 `
: ~( B8 K' t# f" x4 S& {" U" z$ @% y # 日期对齐/ K( B) n0 z( U6 A4 K5 S/ Z+ y
date = pd.DataFrame(index=data_bef.index.unique()) # 获取回测区间内所有交易日
% G/ r: o% X" v+ K% j5 _% {! z code = data_bef.loc[0, 'code']
) b3 X8 x( A+ v! o. @ df = data_bef[['date','open','high','low','close','volume']].copy()
! c" O* M A: j$ ^ df.loc[:,['open','high','low','close']] = df.loc[:,['open','high','low','close']].copy().astype(float)/ B; V7 H, @, n) }2 M U
df.loc[:,['volume']] = df.loc[:,['volume']].copy().astype(int), N4 }# \, S# c) r! D- F) J( M+ n
data = pd.merge(date, df, left_index=True, right_index=True, how='left')3 U4 s. \6 [" [% c
data['date'] = pd.to_datetime(data['date']) # 这是新添加的行. ]/ c0 B5 n- k# Q9 C0 e s9 y5 R, x
data.set_index(['date'],inplace=True)! K2 b* I1 }" R( d
# 缺失值处理:
* h0 a6 j+ h/ I7 J) i7 O- F" t data.loc[:,['volume']] = data.loc[:,['volume']].fillna(0)" v8 K) O7 }$ u* k
data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(method='pad')
2 w" i' @1 ~; }8 \) R data.loc[:,['open','high','low','close']] = data.loc[:,['open','high','low','close']].fillna(0)/ \' d( B6 k! J; N
0 v0 G+ R$ F. k. o! F- E x& G return data, code: V' W! s; B2 g
回测模块:+ d% s% ~: S! M4 C0 N5 e# l' u* x5 v
class Strategy_MA(bt.Strategy):
, I- W$ \* {" b' s # 省略其他部分
# R& e |# V6 w$ ~ ...
+ x9 x6 [- a+ @) `- _; Q h. { def next(self):
$ ^7 p- L# z3 }3 }6 K 0 d# w) ?2 w( |8 M) x. ?
dt = self.datas[0].datetime.date(0) # 获取当前的回测时间点8 K5 S7 ^7 B. j+ c3 U0 Q8 }
# 如果是调仓日,则进行调仓操作4 G# e" \1 C3 L7 ^0 w# g/ \
if dt in self.trade_dates:
, Y/ B) Z2 W2 g) F0 v print("--------------{} 调仓----------".format(dt))
2 T( Z4 ^ t2 F w1 ^# t if len(self.order_list) > 0:! g! H# ?2 f. S: R( F
for od in self.order_list:
5 u7 `" x4 n5 @7 Q5 F u2 v! s self.cancel(od) # 撤销未完成订单9 ]; C$ p0 m3 d' s: d* W
self.order_list = [] # 重置订单列表
$ K/ ]2 a; s7 Z3 p4 v # 提取当前调仓日的持仓列表
: d. ? e4 G/ p4 ]/ q buy_stocks_data = self.buy_stock.query(f"trade_date=='{dt}'"), M+ W |9 J, ?' q( K& F
long_list = buy_stocks_data['sec_code'].tolist()
5 I' r, G7 _) U* l, J2 H print('long_list', long_list) # 打印持仓列表 ?: h' B V- n8 X8 @ W2 ~& c7 r
# 不再继续持有的股票进行平仓) o( E& \# z: c7 W. G# a- h9 B
sell_stock = [i for i in self.buy_stocks_pre if i not in long_list]
$ ~3 h- f8 p8 y N; Z4 L7 D$ C/ L! v/ N print('sell_stock', sell_stock) # 打印平仓列表
+ ?8 n0 B" c9 b4 W if len(sell_stock) > 0:
- c: K2 M- [9 \' B print("-----------平仓--------------"); Z: g3 ] E3 \
for stock in sell_stock:8 j; p3 @- l3 l' n6 w8 x2 t( D$ ]! Y6 \6 y: u
data = self.getdatabyname(stock)8 C4 `9 `0 O: o' l0 M% R; \% p) o
if self.getposition(data).size > 0 :
1 R1 D* K" c+ F5 L: a' @& _ od = self.close(data=data) m" I+ y6 r: A0 @$ Q
self.order_list.append(od) # 记录卖出订单6 O, p" r4 C8 U5 g
print("-----------买入此次调仓的股票--------------")
+ }* f! T4 s4 \5 D' N) |5 T- ^% m for stock in long_list:: C0 p5 {& B3 W! e6 P X
w = buy_stocks_data.query(f"sec_code=='{stock}'")['weight'].iloc[0] # 提取持仓权重/ o; U V L% _( `6 J% b9 e5 C
data = self.getdatabyname(stock)$ ~7 o7 K8 M* V, r' @7 d4 S2 K
order = self.order_target_percent(data=data, target=w*0.95) # 为减少可用资金不足的情况,留 5% 的现金做备用,该函数以多退少补的方式进行购买/卖出% R3 y4 c9 [. w" w
print()
# o% e& {, k! @( }* J- B# N self.order_list.append(order)
2 d5 V* z$ b5 `( ]9 G* k
/ c! k5 l0 [$ `' y self.buy_stocks_pre = long_list # 保存此次调仓的股票列表
$ X% D7 k V# N% C/ z j* f回测执行:. l5 W9 d: m4 |, `: O8 z9 ?
def run_backtest(strategy, dataset, codeset, startcash, start, end):
% l9 F8 o! Q/ r4 w* p# `- ] # 省略其他部分! V' U+ N. x% i8 K7 u4 ]
...
" M. x l' @6 C: H& U6 t5 y- r for item in range(len(dataset)):% o. R$ ]8 b1 b6 d" ]0 A ^9 V
code = codeset[item]
% b) X5 U7 `/ p& e cerebro.adddata(bt.feeds.PandasData(dataname=dataset[item],fromdate=datetime.strptime(start, '%Y-%m-%d'),todate=datetime.strptime(end, '%Y-%m-%d')), name=code)- Q! J; Z2 f" ?% k3 f \! S$ G
# 添加策略2 ~1 j/ E' O% u" ?
cerebro.addstrategy(strategy, end_date=datetime.strptime(end, '%Y-%m-%d'))3 l; K* Q; b" b/ g) U3 R! D
results = cerebro.run()7 F4 m$ v1 m6 g) U) y& s# H# ~
- |4 p; ~' C" q+ m return cerebro, results m9 F( m. c$ |1 d- f7 W
可以看到,这次代码中并没有直接编写择股策略,而是导入已经确定好的调仓信息"trade_info.csv"进行回测,这样我们就可以将相对复杂的策略推理部分放在另一个脚本中编写,以提升回测程序的执行效率。关于择股策略编写的部分将在之后的文章中分享,欢迎感兴趣的朋友多多批评指正。 |