简体   繁体   English

如何使用 backtrader 回测投资组合组合?

[英]How to backtest portfolio compositions using backtrader?

I have a csv file / pandas dataframe which looks like this.我有一个 csv 文件 / pandas dataframe看起来像这样。 It contains various portfolio compositions for a portfolio which is re-balanced everyday according to my own calculations.它包含根据我自己的计算每天重新平衡的投资组合的各种投资组合组合。

date        asset   percentage
4-Jan-21    AAPL    12.00%
4-Jan-21    TSM     1.00%
4-Jan-21    IBM     31.00%
4-Jan-21    KO      15.00%
4-Jan-21    AMD     41.00%
5-Jan-21    DELL    23.00%
5-Jan-21    TSM     12.20%  
5-Jan-21    IBM     15.24%  
5-Jan-21    KO      1.50%   
5-Jan-21    NKE     7.50%   
5-Jan-21    TSLA    9.50%   
5-Jan-21    CSCO    3.30%   
5-Jan-21    JPM     27.76%  
6-Jan-21    AMD     45% 
6-Jan-21    BA      0.50%   
6-Jan-21    ORCL    54.50%  
7-Jan-21    AAPL    50.00%  
7-Jan-21    KO      50.00%  
...

I want to test a strategy with a 12 asset portfolio.我想测试一个包含 12 个资产组合的策略。

AAPL,TSM,IBM,KO,AMD,DELL,NKE,TSLA,CSCO,JPM,BA,ORCL

So let's say on 4Jan2021, the portfolio's composition would be 12% in apple, 1% in TSM.. etc. I want to be able to check the prices and know how many I should be holding.因此,假设在 2021 年 1 月 4 日,投资组合的构成将是苹果的 12%,TSM 的 1%。等等。我希望能够检查价格并知道我应该持有多少。

The next day, 5Jan2021, the composition will change to 23% in Dell.. etc, if the stock isn't in this list means its 0% for that day.第二天,2021 年 1 月 5 日,戴尔的成分股将变为 23%。等等,如果股票不在此列表中,则表示当天为 0%。

I have been looking at backtrader as a backtesting platform, however, the code I have seen in the repo mostly shows how to do stuff with indicators, like SMA cross over, RSI...我一直将 backtrader 视为一个回测平台,但是,我在 repo 中看到的代码主要展示了如何使用指标做一些事情,比如 SMA 交叉、RSI ......

My question is: Is it possible to create and test a portfolio based on these compositions I have so I can check the return of this strategy?我的问题是:是否可以根据我拥有的这些组合创建和测试投资组合,以便我可以检查该策略的回报? It would check this frame, and know how many stocks in a ticker to buy or sell on that particular day.它会检查这个框架,并知道在特定的一天有多少股票可以买卖。

So the universe of stocks I am buying or sell is AAPL,TSM,IBM,KO,AMD,DELL,NKE,TSLA,CSCO,JPM,BA,ORCL所以我买卖的股票范围是AAPL,TSM,IBM,KO,AMD,DELL,NKE,TSLA,CSCO,JPM,BA,ORCL

So on 4-Jan-21 it might look like,所以在 21 年 1 月 4 日,它可能看起来像,

dictionary['4Jan2021'] = {'AAPL':0.12,
                          'TSM':0.01,
                          'IBM':0.31,
                          'KO':0.15,
                          'AMD':0.41,}

On 5-Jan-21 it will look like,在 21 年 1 月 5 日,它看起来像,

dictionary['5Jan2021'] = {'DELL':0.23,
                          'TSM':0.122,
                          'IBM':0.1524,
                          'KO':0.015,
                          'NKE':0.075,
                          'TSLA':0.095,
                          'CSCO':0.033,
                          'JPM':0.2776,}    

If the ticker isnt there means its 0%.如果代码不存在意味着它的 0%。 The portfolio composition needs to change everyday.投资组合的构成每天都需要改变。

The first thing you will want to do it load your targets with your datas.你要做的第一件事就是用你的数据加载你的目标。 I like personally to attach the target to the dataline as I add it to backtrader.当我将目标添加到反向交易者时,我个人喜欢将目标附加到数据线。

tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35}

for ticker, target in tickers.items():
    data = bt.feeds.YahooFinanceData(
        dataname=ticker,
        timeframe=bt.TimeFrame.Days,
        fromdate=datetime.datetime(2019, 1, 1),
        todate=datetime.datetime(2020, 12, 31),
        reverse=False,
    )
    data.target = target
    cerebro.adddata(data, name=ticker)

In next you will want to go through each data, and determine the current allocation.在接下来您将要通过每个数据 go,并确定当前分配。 If the current allocation is too far from the desired allocation (threshold) you trade all datas.如果当前分配距离所需分配(阈值)太远,则交易所有数据。

Notice there is a buffer variable.注意有一个缓冲区变量。 This will reduce the overall value of the account for calculating units to trade.这将降低用于计算要交易的单位的账户的总价值。 This helps avoid margin.这有助于避免保证金。

You will use a dictionary to track this information.您将使用字典来跟踪此信息。

def next(self):
    track_trades = dict()
    total_value = self.broker.get_value() * (1 - self.p.buffer)

    for d in self.datas:
        track_trades[d] = dict()
        value = self.broker.get_value(datas=[d])
        allocation = value / total_value
        units_to_trade = (d.target - allocation) * total_value / d.close[0]
        track_trades[d]["units"] = units_to_trade

        # Can check to make sure there is enough distance away from ideal to trade.
        track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold

Check all the thresholds to determine if trading.检查所有阈值以确定是否交易。 If any of datas need trading, then all need trading.如果任何数据需要交易,那么所有数据都需要交易。

rebalance = False
for values in track_trades.values():
    if values['threshold']:
        rebalance = True

if not rebalance:
    return

Finally, execute your trades.最后,执行您的交易。 Always sell first to generate cash in the account and avoid margins.始终首先出售以在账户中产生现金并避免利润。

# Sell shares first
for d, value in track_trades.items():
    if value["units"] < 0:
        self.sell(d, size=value["units"])

# Buy shares second
for d, value in track_trades.items():
    if value["units"] > 0:
        self.buy(d, size=value["units"])

Here is the all of the code for your reference.这是所有代码供您参考。

import datetime
import backtrader as bt

class Strategy(bt.Strategy):

    params = (
        ("buffer", 0.05),
        ("threshold", 0.025),
    )

    def log(self, txt, dt=None):
        """ Logging function fot this strategy"""
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print("%s, %s" % (dt.date(), txt))

    def print_signal(self):
        self.log(
            f"o {self.datas[0].open[0]:7.2f} "
            f"h {self.datas[0].high[0]:7.2f} "
            f"l {self.datas[0].low[0]:7.2f} "
            f"c {self.datas[0].close[0]:7.2f} "
            f"v {self.datas[0].volume[0]:7.0f} "
        )

    def notify_order(self, order):
        """ Triggered upon changes to orders. """
        # Suppress notification if it is just a submitted order.
        if order.status == order.Submitted:
            return

        # Print out the date, security name, order number and status.
        type = "Buy" if order.isbuy() else "Sell"
        self.log(
            f"{order.data._name:<6} Order: {order.ref:3d} "
            f"Type: {type:<5}\tStatus"
            f" {order.getstatusname():<8} \t"
            f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} "
            f"Position: {self.getposition(order.data).size:5.2f}"
        )
        if order.status == order.Margin:
            return

        # Check if an order has been completed
        if order.status in [order.Completed]:
            self.log(
                f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} "
                # f"EXECUTED for: {dn} "
                f"Price: {order.executed.price:6.2f} "
                f"Cost: {order.executed.value:6.2f} "
                f"Comm: {order.executed.comm:4.2f} "
                f"Size: {order.created.size:9.4f} "
            )

    def notify_trade(self, trade):
        """Provides notification of closed trades."""
        if trade.isclosed:
            self.log(
                "{} Closed: PnL Gross {}, Net {},".format(
                    trade.data._name,
                    round(trade.pnl, 2),
                    round(trade.pnlcomm, 1),
                )
            )

    def next(self):
        track_trades = dict()
        total_value = self.broker.get_value() * (1 - self.p.buffer)

        for d in self.datas:
            track_trades[d] = dict()
            value = self.broker.get_value(datas=[d])
            allocation = value / total_value
            units_to_trade = (d.target - allocation) * total_value / d.close[0]
            track_trades[d]["units"] = units_to_trade

            # Can check to make sure there is enough distance away from ideal to trade.
            track_trades[d]["threshold"] = abs(d.target - allocation) > self.p.threshold

        rebalance = False
        for values in track_trades.values():
            if values['threshold']:
                rebalance = True

        if not rebalance:
            return

        # Sell shares first
        for d, value in track_trades.items():
            if value["units"] < 0:
                self.sell(d, size=value["units"])

        # Buy shares second
        for d, value in track_trades.items():
            if value["units"] > 0:
                self.buy(d, size=value["units"])


if __name__ == "__main__":

    cerebro = bt.Cerebro()

    tickers = {"FB": 0.25, "MSFT": 0.4, "TSLA": 0.35}

    for ticker, target in tickers.items():
        data = bt.feeds.YahooFinanceData(
            dataname=ticker,
            timeframe=bt.TimeFrame.Days,
            fromdate=datetime.datetime(2019, 1, 1),
            todate=datetime.datetime(2020, 12, 31),
            reverse=False,
        )
        data.target = target
        cerebro.adddata(data, name=ticker)

    cerebro.addstrategy(Strategy)

    # Execute
    cerebro.run()

#################################### ####################################
############# EDIT ############### ############# 编辑 ###############
#################################### ####################################
There was an additional requiest for adding in variable allocations per day per security.每天增加每只证券的可变分配还有一个额外的要求。 The following code accomplishes that.下面的代码实现了这一点。

import datetime
import backtrader as bt


class Strategy(bt.Strategy):

    params = (
        ("buffer", 0.05),
        ("threshold", 0.025),
    )

    def log(self, txt, dt=None):
        """ Logging function fot this strategy"""
        dt = dt or self.data.datetime[0]
        if isinstance(dt, float):
            dt = bt.num2date(dt)
        print("%s, %s" % (dt.date(), txt))

    def print_signal(self):
        self.log(
            f"o {self.datas[0].open[0]:7.2f} "
            f"h {self.datas[0].high[0]:7.2f} "
            f"l {self.datas[0].low[0]:7.2f} "
            f"c {self.datas[0].close[0]:7.2f} "
            f"v {self.datas[0].volume[0]:7.0f} "
        )

    def notify_order(self, order):
        """ Triggered upon changes to orders. """
        # Suppress notification if it is just a submitted order.
        if order.status == order.Submitted:
            return

        # Print out the date, security name, order number and status.
        type = "Buy" if order.isbuy() else "Sell"
        self.log(
            f"{order.data._name:<6} Order: {order.ref:3d} "
            f"Type: {type:<5}\tStatus"
            f" {order.getstatusname():<8} \t"
            f"Size: {order.created.size:9.4f} Price: {order.created.price:9.4f} "
            f"Position: {self.getposition(order.data).size:5.2f}"
        )
        if order.status == order.Margin:
            return

        # Check if an order has been completed
        if order.status in [order.Completed]:
            self.log(
                f"{order.data._name:<6} {('BUY' if order.isbuy() else 'SELL'):<5} "
                # f"EXECUTED for: {dn} "
                f"Price: {order.executed.price:6.2f} "
                f"Cost: {order.executed.value:6.2f} "
                f"Comm: {order.executed.comm:4.2f} "
                f"Size: {order.created.size:9.4f} "
            )

    def notify_trade(self, trade):
        """Provides notification of closed trades."""
        if trade.isclosed:
            self.log(
                "{} Closed: PnL Gross {}, Net {},".format(
                    trade.data._name,
                    round(trade.pnl, 2),
                    round(trade.pnlcomm, 1),
                )
            )

    def __init__(self):
        for d in self.datas:
            d.target = {
                datetime.datetime.strptime(date, "%d-%b-%y").date(): allocation
                for date, allocation in d.target.items()
            }

    def next(self):
        date = self.data.datetime.date()
        track_trades = dict()
        total_value = self.broker.get_value() * (1 - self.p.buffer)

        for d in self.datas:
            if date not in d.target:
                if self.getposition(d):
                    self.close(d)
                continue
            target_allocation = d.target[date]
            track_trades[d] = dict()
            value = self.broker.get_value(datas=[d])
            current_allocation = value / total_value
            net_allocation = target_allocation - current_allocation
            units_to_trade = (
                (net_allocation) * total_value / d.close[0]
            )
            track_trades[d]["units"] = units_to_trade

            # Can check to make sure there is enough distance away from ideal to trade.
            track_trades[d]["threshold"] = abs(net_allocation) > self.p.threshold

        rebalance = False
        for values in track_trades.values():
            if values["threshold"]:
                rebalance = True

        if not rebalance:
            return

        # Sell shares first
        for d, value in track_trades.items():
            if value["units"] < 0:
                self.sell(d, size=value["units"])

        # Buy shares second
        for d, value in track_trades.items():
            if value["units"] > 0:
                self.buy(d, size=value["units"])


if __name__ == "__main__":

    cerebro = bt.Cerebro()

    allocations = [
        ("AAPL", "4-Jan-21", 0.300),
        ("TSM", "4-Jan-21", 0.200),
        ("IBM", "4-Jan-21", 0.300),
        ("KO", "4-Jan-21", 0.2000),
        ("AMD", "4-Jan-21", 0.1000),
        ("DELL", "5-Jan-21", 0.200),
        ("TSM", "5-Jan-21", 0.20),
        ("IBM", "5-Jan-21", 0.1),
        ("KO", "5-Jan-21", 0.1),
        ("NKE", "5-Jan-21", 0.15),
        ("TSLA", "5-Jan-21", 0.10),
        ("CSCO", "5-Jan-21", 0.050),
        ("JPM", "5-Jan-21", 0.1),
        ("AMD", "6-Jan-21", 0.25),
        ("BA", "6-Jan-21", 0.25),
        ("ORCL", "6-Jan-21", 0.50),
        ("AAPL", "7-Jan-21", 0.5000),
        ("KO", "7-Jan-21", 0.5000),
    ]
    ticker_names = list(set([alls[0] for alls in allocations]))
    targets = {ticker: {} for ticker in ticker_names}
    for all in allocations:
        targets[all[0]].update({all[1]: all[2]})

    for ticker, target in targets.items():
        data = bt.feeds.YahooFinanceData(
            dataname=ticker,
            timeframe=bt.TimeFrame.Days,
            fromdate=datetime.datetime(2020, 12, 21),
            todate=datetime.datetime(2021, 1, 8),
            reverse=False,
        )
        data.target = target
        cerebro.adddata(data, name=ticker)

    cerebro.addstrategy(Strategy)
    cerebro.broker.setcash(1000000)

    # Execute
    cerebro.run()

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 运行 Cerebro 后,我在 backtrader 中的投资组合价值没有发生任何变化,我如何检查它是否正常工作 - After running Cerebro nothing is happening to my portfolio value in backtrader, how do i check if it is working or not 如何在 python 中使用 backtrader 正确记录交易? - How to correctly log trades using backtrader in python? 如何在Python中回溯测试策略 - How to Backtest a Strategy in Python 在 backtrader 上运行回测,我收到了许多订单取消/保证金/拒绝,而他们不应该这样做 - Running backtest on backtrader I get many BUY ORDERS CANCELLED/MARGIN/REJECTED when they should not 如何使用 ccxt 为 backtrader 创建 pandasData - How to created pandasData for backtrader with ccxt 如何在 backtrader 中检查馈送数据? - How to check fed data in backtrader? 如何使用带有本地 csv 文件的 Backtrader 和 Python 解决回测的数据馈送错误? - How to resolve datafeeds error for backtesting using Backtrader and Python with local csv file? 如何在 streamlit 中显示 backtrader 返回的图表? - How I display the graph that return by backtrader in streamlit? 使用基于百分比的佣金进行Python回测 - Python backtest using percentage based commission 如何在 CCXTStore 中添加多个时间范围? [Python,后台交易] - How to add multiple timeframes in CCXTStore? [Python, backtrader]
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM