量化交易初探(三)——择股与回测

[复制链接]
查看8661 | 回复0 | 2023-9-24 20:02:48 | 显示全部楼层 |阅读模式
量化交易初探(三)——择股与回测
+ _; 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"进行回测,这样我们就可以将相对复杂的策略推理部分放在另一个脚本中编写,以提升回测程序的执行效率。关于择股策略编写的部分将在之后的文章中分享,欢迎感兴趣的朋友多多批评指正。
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

10

金钱

0

收听

0

听众
性别

新手上路

金钱
10 元