For code/output blocks: Use ``` (aka backtick or grave accent) in a single line before and after the block. See: http://commonmark.org/help/

How to create pyfolio round trip tearsheet?



  • Hi, I'm trying to create round_trip_tearsheet using pyfolio because it has trade stats like profit factor, average winning trade, average losing trade, percent profitable, etc. I found two ways to do it. One is by adding round_trips= True in pf.create_full_tearsheet and another is using pf.create_round_trip_tear_sheet but I failed in both.

    upon running the first one, I get some stats and charts but not the stats I want with RuntimeError: Selected KDE bandwidth is 0. Cannot estiamte density.

    Second alternative with create_round_trip_tear_sheet gives KeyError: ('block_dir', 'block_time')

    Please shed some light here on how I can get those trade stats. Thank you in advance. @ab_trader Below is my full code.

    %matplotlib inline
    from datetime import datetime
    import backtrader as bt
    import pandas as pd
    import math
    import pyfolio as pf
    import backtrader.analyzers as btanalyzers
    class SmaCross(bt.Strategy):
        params = dict(pslow=20,order_percentage= 0.99,ticker = 'NEPSE')
        
        def log(self, txt, dt=None):
                dt = dt or self.datas[0].datetime.date(0)
                print('%s, %s' % (dt.isoformat(), txt))
                
        def __init__(self):
            sma = bt.talib.SMA(self.data.close,timeperiod=self.p.pslow)
            self.crossover = bt.ind.CrossOver(self.data.close, sma, plot = True)  
        
        def next(self):
            if not self.position:  
                if self.crossover > 0: 
                    
                    amount_to_invest = (self.p.order_percentage*self.broker.cash)
                    self.size = math.floor(amount_to_invest/self.data.close)
                    self.buy(size=self.size)           
    #                 self.log('%s,Buy, %d,%d 0,%.0f' % (self.params.ticker,self.size,self.data.close[0],cerebro.broker.getvalue()))
            elif self.crossover < 0: 
                    self.close() 
    #                 self.log('%s,Sell, %d,%d 0,%.0f' % (self.params.ticker,self.size,self.data.close[0],cerebro.broker.getvalue()))
        
    cerebro = bt.Cerebro()  
    feed = pd.read_csv('test.csv',index_col = 'Date',parse_dates = True)
    data = bt.feeds.PandasData(dataname = feed)
    cerebro.adddata(data) 
    cerebro.addstrategy(SmaCross)  
    print('Starting Portfolio Value: %.0f' % cerebro.broker.getvalue())
    # strat = cerebro.run()
    print('Final Portfolio Value: %.0f' % cerebro.broker.getvalue())
    cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
    results = cerebro.run()
    strat = results[0]
    pyfoliozer = strat.analyzers.getbyname('pyfolio')
    returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
    pf.create_round_trip_tear_sheet(returns, positions, transactions)
    # pf.create_full_tear_sheet(
    #     returns,
    #     positions=positions,
    #     transactions=transactions,
    #     round_trips=True)
    
    


  • @Arun-Lama said in How to create pyfolio round trip tearsheet?:

    profit factor, average winning trade, average losing trade, percent profitable, etc.

    These items can be found in the trade analyzer.

    If you want a decent tear sheet you can use QuantStats. Just create an indicator that will give you the value of the portfolio and you can send that into QuantStats to get a tearsheet. I use the following analyzer:

    class CashMarket(bt.analyzers.Analyzer):
        """
        Analyzer returning cash and market values
        """
    
        def start(self):
            super(CashMarket, self).start()
    
        def create_analysis(self):
            self.rets = {}
            self.vals = 0.0
    
        def notify_cashvalue(self, cash, value):
            self.vals = (cash, value)
            self.rets[self.strategy.datetime.datetime()] = self.vals
    
        def get_analysis(self):
            return self.rets
    


  • @run-out said in How to create pyfolio round trip tearsheet?:

    e value

    Hi @run-out I tried the QuantStats repo with your strategy, but I had an error:

    Traceback (most recent call last):
      File "main.py", line 14, in <module>
        print(backtest(crypto))
      File "/Users/benjamincherion/Desktop/backtesting-cryptos/backtest.py", line 66, in backtest
        print(qs.stats.sharpe(analyzer))
      File "/Users/benjamincherion/Library/Python/3.7/lib/python/site-packages/quantstats/stats.py", line 234, in sharpe
        returns = _utils._prepare_returns(returns, rf, periods)
      File "/Users/benjamincherion/Library/Python/3.7/lib/python/site-packages/quantstats/utils.py", line 200, in _prepare_returns
        elif data.min() >= 0 and data.max() > 1:
    AttributeError: 'dict' object has no attribute 'min'
    

    the output of your strategy (to inject in QuantStats) is the following one:

    {
      datetime.datetime(2020, 4, 29, 0, 0): (100000.0, 100000.0),
      datetime.datetime(2020, 4, 29, 4, 0): (100000.0, 100000.0),
      ...
      datetime.datetime(2020, 8, 1, 0, 0): (811.8994327605933, 110828.6750226959)
    }
    

    is it possible to provide an example ?
    Thanks a lot !



  • Hello @balibou , I'm new to backtrader and exploring QuantStats currently. I'm wondering how do you feed your data to Quanstats. Do you have to first generate an output data with starting amount and daily returns % ? Only these 2 info are required right? tks.





  • Hi @kian-hong-Tan ! Welcome to the community !

    • Actually I have generated an output with the class CashMarket like this:
    stock = cerebro.addanalyzer(btanalyzers.CashMarket, _name='cashmarket')
    

    then inject it in QuantStats:

    qs.reports.html(stock, "SPY")
    

    But I'm stuck with an error AttributeError: 'dict' object has no attribute 'min'
    For info I have tried with dataset from a csv and data from panda file. Also I resample 1m bars to 4hours bars.
    if @run-out you can help us :)
    I suspect there is a min property to add to your CashMarket analyzer but not sure ...

    • Thanks for the link but I really prefer Quanstats one :)


  • @balibou I followed the same way but also face an error when running thru utils.py, under function _prepare_prices . Seems like data returns as None for my case.

    186 def _prepare_prices(data, base=1.):
    187     """ Converts return data into prices + cleanup """
    

    --> 188 data = data.copy()
    189 if isinstance(data, _pd.DataFrame):
    190 for col in data.columns:

    AttributeError: 'NoneType' object has no attribute 'copy'



  • @kian-hong-Tan can you print the output of this line in your code:

    stock = cerebro.addanalyzer(btanalyzers.CashMarket, _name='cashmarket')
    

    I get something like this:

    {
      datetime.datetime(2020, 4, 29, 0, 0): (100000.0, 100000.0),
      datetime.datetime(2020, 4, 29, 4, 0): (100000.0, 100000.0),
      ...
      datetime.datetime(2020, 8, 1, 0, 0): (811.8994327605933, 110828.6750226959)
    }
    


  • @balibou I got the same results:

    {datetime.datetime(2020, 7, 10, 14, 0): (99596.11138000007, 99596.11138000007), 
     datetime.datetime(2020, 6, 4, 7, 0): (100000.0, 100000.0), 
     datetime.datetime(2020, 6, 16, 10, 30): (90339.92489000004, 99872.72489000004),
    

    then i tried to reformat the datetime, giving output:

    {'2020-07-05': (108542.87369000008, 99473.46369000008), 
    '2020-07-09': (99596.11138000007, 99596.11138000007), 
    '2020-06-01': (100000.0, 100000.0), 
    

    But still doesn't work. Now having the same error message as you.



  • When you get your dictionary to this stage, you need the datetime key and the second column, which is value, to send to quantstats.

    You do this by creating a dataframe from the dictionary and then eliminating the cash column. Once you have this dataframe, you can then put it into your quantstats functions. I had success with a couple of the function but got an error on the reports.html tearsheet. This is a problem you'll have to debug in quantstats. ( I suspect this works more natively from inside Jupyter)

    df_values = pd.DataFrame(results[0].analyzers.getbyname("cash_market").get_analysis()).T
        df_values = df_values.iloc[:, 1]
        qs.extend_pandas()
    
        qs.reports.html(df_values, "SPY", output="qs.png")
        # qs.plots.snapshot(df_values, "SPY")
        # print(qs.stats.sharpe(df_values))
    


  • I did some debugging and found that a reason some functions don't work in quantstats is because the 'benchmark' it uses is yahoo finance data with a one day timeframe.

    If you try and run this on another timeframe, the returns and benchmark dataframes are different sizes. I had success with these functions by making my own benchmark dataframe with my timeframe.

    Look at the functions: _prepare_benchmark and download_returns in the quantstats utils file to accomplish this.



  • Thanks @run-out for your snippet !
    Thanks @eprice122 for your feedback, I was trying to debug it without any success !
    I am gonna have a deeper look at those functions



  • Ok so it sounds that for yfinance api:

    • intraday data cannot extend last 60 days
    • valid intervals are: 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo

    As I want to work with many timeframes (1h, 2h, 4h, ...) on different periods (weeks, months, years), I will probably have to deal with another strategy than using yahoo finance (probably still keeping SPY for benchmark, so I will just have to download SPY performance on different timeframes on csv files) ... What are your thoughts ?

    Also what does your benchmark look alike @eprice122 ?



  • @balibou Currently I have it set up so the timeframe I am testing is pulled from a DB (similar to pulling the data from a csv) and turned into the benchmark.

    """
        You can assume parsed is a dict with datetime keys and close values
        parsed = {
            datetime.datetime(2014, 2, 3, 9, 30):176.74
            datetime.datetime(2014, 2, 3, 9, 31): 177.01
            ...
        }
        """
        benchmark = (
            pd.DataFrame.from_dict(parsed, orient="index")
            .pct_change()
            .fillna(0)
            .replace([np.inf, -np.inf], float("NaN"))
            .dropna()
        ) # All these functions on the datafame were involved in the quantstat util functions I mentioned
    

    If your's isn't working as expected, I found it helpful to call _prepare_benchmark("SPY")
    then turn it to a dict and see how the benchmark dataframe would look with TimeFrame.Days.

    Also your df_values dataframe should have the same length, start & end dates as the benchmark.

    Hopefully this helps.



  • Thanks @eprice122 I'm gonna have a try tomorrow and will let you know !



  • @eprice122 if you can help to make it work, I am still struggling with the output of the html method :/

    Here is the code I'm using (you can find it in this repo with a SPY.csv file):

    from datetime import datetime
    import backtrader as bt
    import quantstats as qs
    import pandas as pd
    import csv
    
    class CashMarket(bt.analyzers.Analyzer):
      def start(self):
        super(CashMarket, self).start()
    
      def create_analysis(self):
        self.rets = {}
        self.vals = 0.0
    
      def notify_cashvalue(self, cash, value):
        self.vals = (cash, value)
        self.rets[self.strategy.datetime.datetime().strftime("%Y-%m-%d")] = self.vals
    
      def get_analysis(self):
        return self.rets
    
    class SmaCross(bt.Strategy):
      params = dict(
        pfast=10,
        pslow=30
      )
    
      def __init__(self):
        sma1 = bt.ind.SMA(period=self.p.pfast)
        sma2 = bt.ind.SMA(period=self.p.pslow)
        self.crossover = bt.ind.CrossOver(sma1, sma2)
    
      def next(self):
        if not self.position:  # not in the market
          if self.crossover > 0:
            self.buy()
    
        elif self.crossover < 0:  # in the market & cross to the downside
          self.close()
    
    
    cerebro = bt.Cerebro()
    
    data = bt.feeds.Quandl(
      dataname='MSFT',
      fromdate = datetime(2017,1,1),
      todate = datetime(2017,12,28),
      buffered= True
    )
    
    cerebro.adddata(data)
    cerebro.addstrategy(SmaCross)
    cerebro.addanalyzer(CashMarket, _name='cashmarket')
    results = cerebro.run()
    
    # ---- Format the values from results ----
    df_values = pd.DataFrame(results[0].analyzers.getbyname("cashmarket").get_analysis()).T
    df_values = df_values.iloc[:, 1]
    # ----------------------------------------
    
    # ---- Format the benchmark from SPY.csv ----
    with open('SPY.csv', mode='r') as infile:
      reader = csv.reader(infile)
      mydict = {
        datetime.strptime(rows[0], "%Y-%m-%d"): float(rows[4]) for rows in reader
      }
    
    benchmark = (
      pd.DataFrame.from_dict(mydict, orient="index").pct_change()
    )
    # -------------------------------------------
    
    qs.extend_pandas()
    qs.reports.html(df_values, benchmark=benchmark, output="qs.html")
    


  • You're almost there.

    Your dataframes need to be datetime indices. They are both string objects and in different formats.

    Secondly quantstats needs returns data. It has a utility to convert price data to returns data. Together you adjust your code as follows:

    returns = qs.utils.to_returns(df_values)
    returns.index = pd.to_datetime(returns.index)
    
    # and for the benchmark ...
    
    returns_bm = qs.utils.to_returns(benchmark)
    returns_bm.index = pd.to_datetime(returns_bm.index)
    

    At this stage you should have a tearsheet in file qs.html

    Try to work on your debugging skills. I did not have all the answers to this, I just debugged the code.

    from datetime import datetime
    import backtrader as bt
    import quantstats as qs
    import pandas as pd
    import csv
    
    class CashMarket(bt.analyzers.Analyzer):
      def start(self):
        super(CashMarket, self).start()
    
      def create_analysis(self):
        self.rets = {}
        self.vals = 0.0
    
      def notify_cashvalue(self, cash, value):
        self.vals = (cash, value)
        self.rets[self.strategy.datetime.datetime().strftime("%Y-%m-%d")] = self.vals
    
      def get_analysis(self):
        return self.rets
    
    class SmaCross(bt.Strategy):
      params = dict(
        pfast=10,
        pslow=30
      )
    
      def __init__(self):
        sma1 = bt.ind.SMA(period=self.p.pfast)
        sma2 = bt.ind.SMA(period=self.p.pslow)
        self.crossover = bt.ind.CrossOver(sma1, sma2)
    
      def next(self):
        if not self.position:  # not in the market
          if self.crossover > 0:
            self.buy()
    
        elif self.crossover < 0:  # in the market & cross to the downside
          self.close()
    
    
    cerebro = bt.Cerebro()
    
    data = bt.feeds.Quandl(
      dataname='MSFT',
      fromdate = datetime(2017,1,1),
      todate = datetime(2017,12,28),
      buffered= True
    )
    
    cerebro.adddata(data)
    cerebro.addstrategy(SmaCross)
    cerebro.addanalyzer(CashMarket, _name='cashmarket')
    results = cerebro.run()
    
    # ---- Format the values from results ----
    df_values = pd.DataFrame(results[0].analyzers.getbyname("cashmarket").get_analysis()).T
    df_values = df_values.iloc[:, 1]
    returns = qs.utils.to_returns(df_values)
    returns.index = pd.to_datetime(returns.index)
    # ----------------------------------------
    
    # ---- Format the benchmark from SPY.csv ----
    with open('data/SPY.csv', mode='r') as infile:
      reader = csv.reader(infile)
      mydict = {
        datetime.strptime(rows[0], "%Y-%m-%d"): float(rows[4]) for rows in reader
      }
    
    benchmark = (
      pd.DataFrame.from_dict(mydict, orient="index")
    )
    returns_bm = qs.utils.to_returns(benchmark)
    returns_bm.index = pd.to_datetime(returns_bm.index)
    # -------------------------------------------
    
    qs.extend_pandas()
    qs.reports.html(returns, benchmark=benchmark, output="qs.html")
    


  • Thanks @run-out for your piece of code !
    However I'm still having the error TypeError: html() got an unexpected keyword argument 'output'.

    I will investigate more on that ... If it's too complicated I guess I will have to find another solution like a custom report.



  • @run-out tks for your code. I'm able to generate the output finally, using

    qs.reports.full(df_values, "SPY")
    

    But the output is shown in the cell of jupyter notebook. I tried using qs.reports.html for both output="qs.png" and "qs.html'' but nothing appears after the code is successfully ran. I'm using Microsoft Edge browser, not sure if this matters. Do I need to install other library for html output?



  • I have stats displayed in the terminal with your piece of code @kian-hong-Tan.
    The charts are rendered in a lot of different plots, I will investigate how to deal with that


Log in to reply
 

});