从零实现”搭积木式实现策略“的回测系统 part IV

本文重点描述回测系统引擎最核心的问题,投资组合管理,本质上其实是一个“账本”。Portfolio负责记录整个投资组合的明细,而SymbolBroker记录了单支证券的具体信息。

SymbolBroker和Portfolio本质上是管理一个dataframe存储的交易信息。dataframe是处理金融时间序列很好的数据结构。BrokerBase是把处理时间序列里,常用的一些操作,抽象出来,在两个子类中实现复用。

updateidx是游标往前走一步,同时更新当前的时间now。getitem,set_item,分别是读取和写入当前行=idx,行=column的值。

'''
SymbolBroker和Portfolio的基类,实现一些通用功能
'''
class BrokerBase(object):
    def update_idx(self):
        '''
        更新序列游标,以及now时间索引。初始值是None
        :return:
        '''
        if self.idx is None:
            self.idx = 0
        else:
            if self.idx >= (len(self.df) - 1):
                return True
            self.idx += 1

        self.now = list(self.df.index)[self.idx]
        return False

    def get_item(self,column,idx=None):
        '''
        :param column: dataframe的列名
        :param idx: 取某一行,默认idx是当前行,可以自行指定
        :return:
        '''
        if idx is None:
            idx = self.idx
        return self.df.iloc[idx, self.df.columns.get_loc(column)]

    def set_item(self,column,value,idx=None):
        '''
        :param column: dataframe的列名
        :param value: 新的值
        :param idx:
        :return:
        '''
        if idx is None:
            idx = self.idx
        self.df.iloc[idx, self.df.columns.get_loc(column)] = value

SybmolBroker记录单支证券的交易明细。内部维护一个dataframe,包括列——price,code,position,commission,分别是收盘价,证券代码,持仓数,交易佣金。

class SymbolBroker(BrokerBase):
    def __init__(self,code,prices,commission_rate=0.0008):
        '''

        :param code: 证券代码
        :param prices: pd.Series类型,证券的收盘价
        '''
        self.code = code
        self.prices = prices
        self.commission_rate = commission_rate
        self.df = pd.DataFrame(index=prices.index,columns=['price','position','commission'])
        self.df['price'] = prices
        self.df['code'] = code
        self.df['position'] = None
        self.df['commission'] = None

        self.idx = None
        self.__last_position = None
        self.__last_commission = None

外部循环会调用SymbolBroker的Onbar函数,分别调用基类的updateidx更新游标,还有自身的updatedatas

    def onbar(self):
        self.update_idx()
        self.__update_datas()

_updatedatas的实现很简单,就是把上期的数据,同步到当期,用于可能的交易指令计算。

   def __update_datas(self):
        self.set_item('commission',0.0)
        if self.idx == 0:
            self.set_item('position',0)
        else:
            self.set_item('position', self.get_item('position',idx=self.idx - 1))

上述功能就完成了基础的单证券的记账和循环的功能,还需要支持交易指令完成账本更新。

adjusttotarget_share故名思义,是把持仓调整为目标数值(无论原来是多少)。返回值有两个,一上现金的变化,二是佣金。就如果是买入证券cash可能不够,这就需要在Portfolio计算买入份额时检查。

    def adjust_to_target_share(self,target_share):
        if target_share < 0:
            return

        price = self.get_item('price')
        delta_pos = target_share - self.get_item('position')
        commission = price * abs(delta_pos) * self.commission_rate

        self.set_item('position',target_share)
        self.set_item('commission',commission)

        if delta_pos == 0:
            logger.info('有交易信号,仓位无变化')
        else:
            action = '买入' if delta_pos > 0 else '卖出'
            logger.info('{} - 以{}的价格,{}{}:{}股'.format(self.now,price, action,self.code, abs(delta_pos)))

        # 这里有一个问题,就是cash可能不够,这就需要在Portfolio计算买入份额时检查。
        return -delta_pos*price,commission

在这个基础上很容易实现平仓功能——就是把仓位调整至0。

   def flat(self):
        return self.adjust_to_target_shares(target_shares=0)

还有一个常用的交易更新函数,买入指定额度的现金,因为dataframe里有记录当期的收盘价,cash容易换算成希望买入的股数,然后再和当前持仓position相加,计算出需要调整到持仓数。

    def long_cash(self,cash):
        if cash <= 0:
            logger.info('long_cash的现金需要>0')
            return
        price = self.get_item('price')
        delta_pos = int(cash / price)
        target_share = delta_pos + self.get_item('position')
        return self.adjust_to_target_share(target_share=target_share)

下面具体描述Portfolio的运行机制。与SymbolBroker类似,它也维护一个dataframe的数据结构。其中数据列包含total=组合总市值,cash=现金余额,commission=当期交易佣金。另外,还维护一个SybmolBroker的dict合集brokers。

class Portfolio(BrokerBase):
    def __init__(self,data,init_cash=100000.0):
        self.data = data
        self.init_cash = init_cash
        self.brokers = {}
        for col in data.columns:
            self.brokers[col] = SymbolBroker(code=col,prices=data[col])

        self.df = pd.DataFrame(index=data.index,columns=['total','cash','commission'])
        self.df['total'] = None
        self.df['cash'] =  None
        self.df['commission'] = None

        self.idx = None
        self.now = None

外部调用update,作日常更新。除调用brokers的onbar之外,还完成自身的游标的更新与数据同步。

    def update(self):
        done = self.update_idx()
        for symbol, broker in self.brokers.items():
            broker.onbar()
        self.__update_datas()
        return done

总市值会根据持仓与当前的价值,调用_calccurrent_total计算出来。

 def __update_datas(self):
        self.set_item('commission',0.0)
        if self.idx == 0:
            self.set_item('cash',self.init_cash)
            self.set_item('total', self.init_cash)
        else:
            self.set_item('cash', self.get_item('cash',idx=self.idx - 1))
            self.set_item('total', self.__calc_current_total())
    def __calc_current_total(self):
        total = self.get_item('cash')
        for symbol,broker in self.brokers.items():
            total += broker.get_item('position') * broker.get_item('price')
        return total

如上部分代码完成Portfolio的日常更新。而step则接受外部环境传入的context,context里包含的策略执行后的结果。

context['FLAT']包含了当期需要平仓的证券代码列表。优先把这些证券平仓——把持仓调整为0。

context['LONG']包含了当期需要持仓的证券代码列表。

    def step(self,context):
        # 先处理FLAT
        if "FLAT" in context.keys():
            flats = context['FLAT']
            del context['FLAT']#处理完成要清掉,否则下次循环这个还在,strategy是没有变化
            target_shares = {flat:0 for flat in flats}
            if len(target_shares):
                self.__handle_orders(target_shares,type_of_func='target_share')


        if 'weights' in context.keys():
            weights = context['weights']
            del context['weights'] #处理完成要清掉,否则下次循环这个还在,strategy是没有变化
            cash = self.get_item('cash') * 0.98
            moneys = {symbol:cash*weight for symbol,weight in weights.items()}
            self.__handle_orders(moneys,type_of_func='cash')

完成交易指令后,需要更新cash和commission。

    def __update_cash_commission(self,delta_cash,delta_commission):
        self.set_item('cash',self.get_item('cash') + delta_cash - delta_commission)
        self.set_item('commission',self.get_item('commission')+delta_commission)
        self.set_item('total',self.get_item('total') - delta_commission)

Portfolio还对外提供统计接口,供回测完成后,显示回测结果之用。

    def statistics(self):
        self.df['returns'] = self.df['total'] / self.df['total'].shift(1) - 1
        self.df['equity'] = (self.df['returns'] + 1).cumprod()

        dfs = []
        for name, broker in self.brokers.items():
            broker.df['trade'] = broker.df['position'] - broker.df['position'].shift(1).fillna(0)
            dfs.append(broker.df)
        all = pd.concat(dfs)

        return self.df,all

后续对整个回测环境env进行介绍,把整个系统整合起来。 代码在github上开源ailabx

image

转载于:https://my.oschina.net/u/3289203/blog/2251895

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值