Skip to content

用保守公式重新平衡

原文: https://www.backtrader.com/blog/2019-07-19-rebalancing-conservative/rebalancing-conservative/

本文提出了保守公式方法:Python 中的保守公式:量化投资变得容易

*这是许多可能的再平衡方法之一,但很容易掌握。方法概述:

  • x股票选自Y(1000 只中的 100 只)

  • 甄选准则如下:

    • 低波动性
    • 高净支付收益率
    • 高动量
    • 每月重新平衡

考虑到这一点,让我们在backtrader中展示一个可能的实现

数据

即使有一个成功的策略,如果没有可用的数据,实际上也不会赢得任何东西。这意味着必须考虑数据的外观和加载方式。

假设一组CSV(“逗号分隔值”)文件可用,包含以下特征

  • ohlcv月度数据

  • 在包含净支付收益率npyv之后有一个额外的字段,以拥有ohlcvn数据集。

因此,CSV数据的格式如下所示

date, open, high, low, close, volume, npy
2001-12-31, 1.0, 1.0, 1.0, 1.0, 0.5, 3.0
2002-01-31, 2.0, 2.5, 1.1, 1.2, 3.0, 5.0
... 

即:每月一行。数据加载器引擎现在可以准备好,为它创建与backtrader一起交付的通用内置 CSV 加载器的简单扩展。

class NetPayOutData(bt.feeds.GenericCSVData):
    lines = ('npy',)  # add a line containing the net payout yield
    params = dict(
        npy=6,  # npy field is in the 6th column (0 based index)
        dtformat='%Y-%m-%d',  # fix date format a yyyy-mm-dd
        timeframe=bt.TimeFrame.Months,  # fixed the timeframe
        openinterest=-1,  # -1 indicates there is no openinterest field
    ) 

就是这样。请注意,向ohlcv数据流添加一点基础数据是多么容易。

  1. 通过使用表达式lines=('npy',)。其他常用字段(openhigh…)已经是GenericCSVData的一部分

  2. 通过params = dict(npy=6)指示加载位置。其他字段具有预定义的位置。

参数中的时间范围也已更新,以反映数据的月度性质。


实际字段和加载位置见文档-数据馈送参考-通用 CSVDATA(均可定制)


数据加载器必须用一个文件名正确地实例化,但这是以后的事情,当下面给出一个标准样板文件以获得一个完整的脚本时。

战略

让我们把这个逻辑放到一个标准的反向交易者策略中。为了使其尽可能通用和可定制,将使用与之前数据相同的params方法。

在深入研究策略之前,让我们从快速总结中考虑其中的一点。

  • x股票从Y宇宙中选择

策略本身并不负责向宇宙中添加股票,而是负责选择。如果代码中的xY是固定的,则可能只添加了 50 只股票,仍然尝试选择 100 只。为应对此类情况,将采取以下措施:

  • 有一个值为0.10(即:10%selperc参数,表示要从宇宙中选择的库存量。

    这意味着,如果存在 1000 只股票,则只会选择 100 只;如果宇宙由 50 只股票组成,则只会选择 5 只股票。

至于股票的排名公式,如下所示:

  • (momentum * net payout) / volatility

    这意味着那些动量更大、回报更高、波动性更低的人得分更高。

对于momentum将使用RateOfChange指标(又名ROC,该指标衡量期间价格变化的比率。

net payout已经是数据馈送的一部分。

为了计算volatility,将使用股票n-periods返回的StandardDeviationn-periods,因为东西将作为参数保留)。

有了这些信息,策略已经可以初始化,使用正确的参数和指标设置和计算,这些将在以后的每个月迭代中使用。

首先是声明和参数

class St(bt.Strategy):
    params = dict(
        selcperc=0.10,  # percentage of stocks to select from the universe
        rperiod=1,  # period for the returns calculation, default 1 period
        vperiod=36,  # lookback period for volatility - default 36 periods
        mperiod=12,  # lookback period for momentum - default 12 periods
        reserve=0.05  # 5% reserve capital
    ) 

请注意,上面没有提到的内容已经添加,这是一个参数reserve=0.05(即5%,用于计算每只股票的分配百分比,将储备资本保留在银行中。虽然对于模拟来说,人们可能希望使用 100%的资本,但这样做可能会遇到常见的问题,例如价格差距、浮点精度,最终会错过一些市场条目。

在做其他事情之前,先创建一个小的日志记录方法,它允许记录投资组合是如何重新平衡的。

 def log(self, arg):
        print('{} {}'.format(self.datetime.date(), arg)) 

__init__方法开始时,计算要排名的股票数量,并应用储备资本参数确定银行的每股百分比。

 def __init__(self):
        # calculate 1st the amount of stocks that will be selected
        self.selnum = int(len(self.datas) * self.p.selcperc)

        # allocation perc per stock
        # reserve kept to make sure orders are not rejected due to
        # margin. Prices are calculated when known (close), but orders can only
        # be executed next day (opening price). Price can gap upwards
        self.perctarget = (1.0 - self.p.reserve) % self.selnum 

最后初始化结束,计算波动率和动量的每股指标,然后将其应用于每股排名公式计算。

 # returns, volatilities and momentums
        rs = [bt.ind.PctChange(d, period=self.p.rperiod) for d in self.datas]
        vs = [bt.ind.StdDev(ret, period=self.p.vperiod) for ret in rs]
        ms = [bt.ind.ROC(d, period=self.p.mperiod) for d in self.datas]

        # simple rank formula: (momentum * net payout) / volatility
        # the highest ranked: low vol, large momentum, large payout
        self.ranks = {d: d.npy * m / v for d, v, m in zip(self.datas, vs, ms)} 

现在是每个月迭代的时候了。排名可在self.ranks字典中找到。每次迭代都必须对键/值对进行排序,以确定哪些项目必须离开,哪些项目必须成为投资组合的一部分(保留或添加)

 def next(self):
        # sort data and current rank
        ranks = sorted(
            self.ranks.items(),  # get the (d, rank), pair
            key=lambda x: x[1][0],  # use rank (elem 1) and current time "0"
            reverse=True,  # highest ranked 1st ... please
        ) 

iterable 按相反顺序排序,因为排名公式为排名最高的股票提供了更高的分数。

重新平衡现在已经到期。

再平衡 1:排名靠前,有未平仓的股票

 # put top ranked in dict with data as key to test for presence
        rtop = dict(ranks[:self.selnum])

        # For logging purposes of stocks leaving the portfolio
        rbot = dict(ranks[self.selnum:]) 

这里发生了一些 Python 的诡计,因为使用了dict。原因是,如果将排名靠前的股票放入一个list中,Python会在内部使用操作符==来检查操作符in是否存在。虽然不太可能,但两支股票在同一天的价值是相同的。当使用dict时,在检查是否存在作为键一部分的项时,使用哈希值。

:出于记录目的,还创建了rbot排名垫底的)以及rtop中不存在的股票。

为了以后区分必须离开投资组合的股票、必须重新平衡的股票和排名靠前的股票,我们准备了一份投资组合中当前的股票列表。

 # prepare quick lookup list of stocks currently holding a position
        posdata = [d for d, pos in self.getpositions().items() if pos] 

重新平衡 2:出售不再排名靠前的产品

就像在现实世界中一样,在backtrader生态系统中,为了确保有足够的现金,必须先卖出再买入。

 # remove those no longer top ranked
        # do this first to issue sell orders and free cash
        for d in (d for d in posdata if d not in rtop):
            self.log('Exit {} - Rank {:.2f}'.format(d._name, rbot[d][0]))
            self.order_target_percent(d, target=0.0) 

当前未平仓且不再排名靠前的股票被出售(即target=0.0

一个简单的self.close(data)在这里就足够了,而不是明确说明目标百分比。

再平衡 3:为所有排名靠前的股票发布目标订单

投资组合的总价值随着时间的推移而变化,投资组合中已经存在的股票可能必须略微增加/减少当前头寸,以匹配预期的百分比。order_target_percent是进入市场的理想方法,因为它会自动计算是否需要buysell订单。

 # rebalance those already top ranked and still there
        for d in (d for d in posdata if d in rtop):
            self.log('Rebal {} - Rank {:.2f}'.format(d._name, rtop[d][0]))
            self.order_target_percent(d, target=self.perctarget)
            del rtop[d]  # remove it, to simplify next iteration 

在将新股票添加到投资组合之前,需要对已有头寸的股票进行再平衡,因为新股票只会发出buy订单并消耗现金。在重新平衡后,将现有股票从rtop[data].pop()中移除,rtop中的剩余股票是将新添加到投资组合中的股票。

 # issue a target order for the newly top ranked stocks
        # do this last, as this will generate buy orders consuming cash
        for d in rtop:
            self.log('Enter {} - Rank {:.2f}'.format(d._name, rtop[d][0]))
            self.order_target_percent(d, target=self.perctarget) 

运行它并评估它!

只有数据加载器类和策略是不够的。就像其他框架一样,需要一些样板文件。下面的代码使之成为可能。

def run(args=None):
    args = parse_args(args)

    cerebro = bt.Cerebro()

    # Data feed kwargs
    dkwargs = dict(**eval('dict(' + args.dargs + ')'))

    # Parse from/to-date
    dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
    if args.fromdate:
        fmt = dtfmt + tmfmt * ('T' in args.fromdate)
        dkwargs['fromdate'] = datetime.datetime.strptime(args.fromdate, fmt)

    if args.todate:
        fmt = dtfmt + tmfmt * ('T' in args.todate)
        dkwargs['todate'] = datetime.datetime.strptime(args.todate, fmt)

    # add all the data files available in the directory datadir
    for fname in glob.glob(os.path.join(args.datadir, '*')):
        data = NetPayOutData(dataname=fname, **dkwargs)
        cerebro.adddata(data)

    # add strategy
    cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))

    # set the cash
    cerebro.broker.setcash(args.cash)

    cerebro.run()  # execute it all

    # Basic performance evaluation ... final value ... minus starting cash
    pnl = cerebro.broker.get_value() - args.cash
    print('Profit ... or Loss: {:.2f}'.format(pnl)) 

在执行以下操作的情况下:

  • 解析参数并使其可用(这显然是可选的,因为一切都可以硬编码,但好的实践就是好的实践)

  • 创建一个cerebro引擎实例。是的,这是西班牙语中【大脑】的意思,是负责在黑暗中协调管弦乐动作的框架的一部分。尽管它可以接受多个选项,但是缺省值应该足以满足大多数用例。

  • 加载数据文件,通过简单的目录扫描args.datadir完成,所有文件都加载NetPayOutData并添加到cerebro实例中

  • 添加策略

  • 设置现金,默认为1,000,000。考虑到这个用例是针对500宇宙中的100股票的,所以有一些备用现金似乎是公平的。这也是一个可以改变的论点。

  • 并致电cerebro.run()

  • 最后对性能进行了评价

为了能够直接从命令行运行具有不同参数的程序,下面提供了一个启用了argparse的样板文件,以及完整的代码

绩效评估

以最终结果值的形式添加的简单绩效评估,即:最终资产净值减去起始现金。

backtrader生态系统提供了一套内置的性能分析器,也可以使用,如:SharpeRatioVariability-Weighted ReturnSQN等。参见文件-分析仪参考

全集

最后,将大部分工作作为一个整体呈现。享受

```py import argparse import datetime import glob import os.path

import backtrader as bt

class NetPayOutData(bt.feeds.GenericCSVData): lines = ('npy',) # add a line containing the net payout yield params = dict( npy=6, # npy field is in the 6th column (0 based index) dtformat='%Y-%m-%d', # fix date format a yyyy-mm-dd timeframe=bt.TimeFrame.Months, # fixed the timeframe openinterest=-1, # -1 indicates there is no openinterest field )

class St(bt.Strategy): params = dict( selcperc=0.10, # percentage of stocks to select from the universe rperiod=1, # period for the returns calculation, default 1 period vperiod=36, # lookback period for volatility - default 36 periods mperiod=12, # lookback period for momentum - default 12 periods reserve=0.05 # 5% reserve capital )

def log(self, arg):
    print('{} {}'.format(self.datetime.date(), arg))

def __init__(self):
    # calculate 1st the amount of stocks that will be selected
    self.selnum = int(len(self.datas) * self.p.selcperc)

    # allocation perc per stock
    # reserve kept to make sure orders are not rejected due to
    # margin. Prices are calculated when known (close), but orders can only
    # be executed next day (opening price). Price can gap upwards
    self.perctarget = (1.0 - self.p.reserve) / self.selnum

    # returns, volatilities and momentums
    rs = [bt.ind.PctChange(d, period=self.p.rperiod) for d in self.datas]
    vs = [bt.ind.StdDev(ret, period=self.p.vperiod) for ret in rs]
    ms = [bt.ind.ROC(d, period=self.p.mperiod) for d in self.datas]

    # simple rank formula: (momentum * net payout) / volatility
    # the highest ranked: low vol, large momentum, large payout
    self.ranks = {d: d.npy * m / v for d, v, m in zip(self.datas, vs, ms)}

def next(self):
    # sort data and current rank
    ranks = sorted(
        self.ranks.items(),  # get the (d, rank), pair
        key=lambda x: x[1][0],  # use rank (elem 1) and current time "0"
        reverse=True,  # highest ranked 1st ... please
    )

    # put top ranked in dict with data as key to test for presence
    rtop = dict(ranks[:self.selnum])

    # For logging purposes of stocks leaving the portfolio
    rbot = dict(ranks[self.selnum:])

    # prepare quick lookup list of stocks currently holding a position
    posdata = [d for d, pos in self.getpositions().items() if pos]

    # remove those no longer top ranked
    # do this first to issue sell orders and free cash
    for d in (d for d in posdata if d not in rtop):
        self.log('Leave {} - Rank {:.2f}'.format(d._name, rbot[d][0]))
        self.order_target_percent(d, target=0.0)

    # rebalance those already top ranked and still there
    for d in (d for d in posdata if d in rtop):
        self.log('Rebal {} - Rank {:.2f}'.format(d._name, rtop[d][0]))
        self.order_target_percent(d, target=self.perctarget)
        del rtop[d]  # remove it, to simplify next iteration

    # issue a target order for the newly top ranked stocks
    # do this last, as this will generate buy orders consuming cash
    for d in rtop:
        self.log('Enter {} - Rank {:.2f}'.format(d._name, rtop[d][0]))
        self.order_target_percent(d, target=self.perctarget)

def run(args=None): args = parse_args(args)

cerebro = bt.Cerebro()

# Data feed kwargs
dkwargs = dict(**eval('dict(' + args.dargs + ')'))

# Parse from/to-date
dtfmt, tmfmt = '%Y-%m-%d', 'T%H:%M:%S'
if args.fromdate:
    fmt = dtfmt + tmfmt * ('T' in args.fromdate)
    dkwargs['fromdate'] = datetime.datetime.strptime(args.fromdate, fmt)

if args.todate:
    fmt = dtfmt + tmfmt * ('T' in args.todate)
    dkwargs['todate'] = datetime.datetime.strptime(args.todate, fmt)

# add all the data files available in the directory datadir
for fname in glob.glob(os.path.join(args.datadir, '*')):
    data = NetPayOutData(dataname=fname, **dkwargs)
    cerebro.adddata(data)

# add strategy
cerebro.addstrategy(St, **eval('dict(' + args.strat + ')'))

# set the cash
cerebro.broker.setcash(args.cash)

cerebro.run()  # execute it all

# Basic performance evaluation ... final value ... minus starting cash
pnl = cerebro.broker.get_value() - args.cash
print('Profit ... or Loss: {:.2f}'.format(pnl))

def parse_args(pargs=None): parser = argparse.ArgumentParser( formatter_class=argparse.ArgumentDefaultsHelpFormatter, description=('Rebalancing with the Conservative Formula'), )

parser.add_argument('--datadir', required=True,
                    help='Directory with data files')

parser.add_argument('--dargs', default='',
                    metavar='kwargs', help='kwargs in k1=v1,k2=v2 format')

# Defaults for dates
parser.add_argument('--fromdate', required=False, default='',
                    help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')

parser.add_argument('--todate', required=False, default='',
                    help='Date[time] in YYYY-MM-DD[THH:MM:SS] format')

parser.add_argument('--cerebro', required=False, default='',
                    metavar='kwargs', help='kwargs in k1=v1,k2=v2 format')

parser.add_argument('--cash', default=1000000.0, type=float,
                    metavar='kwargs', help='kwargs in k1=v1,k2=v2 format')

parser.add_argument('--strat', required=False, default='',
                    metavar='kwargs', help='kwargs in k1=v1,k2=v2 format')

return parser.parse_args(pargs)

if name == 'main': run() ```*


我们一直在努力

apachecn/AiLearning

【布客】中文翻译组