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

Closed trade list (including MFE/MAE) analyzer



  • Hi all

    I've used Amibroker for some time and liked their trade list output. So written the analyzer for closed trades which provides similar information:

      chng%    pnl%    mae%    pricein  ticker      pnl/bar    size     value    mfe%    priceout       pnl    cumpnl  dateout       ref    nbars  dir    datein
    -------  ------  ------  ---------  --------  ---------  ------  --------  ------  ----------  --------  --------  ----------  -----  -------  -----  ----------
      -6.07   -2.82  -10.12    281.397  SPY         -271.68     159   44742.1    0.68      264.31  -2716.78  -2716.78  2018-02-14      1       10  long   2018-01-31
      -1.59   -0.71   -2.2     119.128  TLT          -68.55     363   43243.6    1.01      117.24   -685.47  -3402.25  2018-02-22      2       10  long   2018-02-07
       1.21   -0.54   -4.01    268.163  SPY          -51.3     -158  -42369.7    1.44      271.41   -513.04  -3915.29  2018-03-01      3       10  short  2018-02-14
       1.89   -0.84   -2.16    117.946  TLT          -53.82    -363  -42814.4    0.8       120.17   -807.27  -4722.56  2018-03-15      4       15  short  2018-02-22
      -1.98   -1.26   -6       270.915  SPY          -49.35     158   42804.6    3.5       265.55  -1184.35  -5906.91  2018-04-05      5       24  long   2018-03-01
      -1.57    0.71   -0.41    265.55   SPY          336.49    -161  -42753.6    2.84      261.37    672.98  -5233.93  2018-04-09      7        2  short  2018-04-05
       0.93    0.42   -1.09    119.413  TLT           19.87     359   42869.2    2.47      120.52    397.45  -4836.48  2018-04-13      6       20  long   2018-03-15
      -0.89    0.4    -1.35    119.962  TLT           77.43    -361  -43306.4    1.28      118.89    387.13  -4449.35  2018-04-20      9        5  short  2018-04-13
       2.9     1.76   -2.06    264.486  SPY           71.27     165   43126.1    3.28      272.16   1710.47  -2738.88  2018-05-11      8       24  long   2018-04-09
      -0.16    0.07   -0.61    272.585  SPY            6.96    -160  -43613.6    0.94      272.15     69.6   -2669.28  2018-05-25     11       10  short  2018-05-11
       0.95    0.32   -2.43    118.985  TLT           10.37     365   43429.6    2.97      120.11    310.98  -2358.3   2018-06-04     10       30  long   2018-04-20
      -1.18    0.26   -0.32    120.11   TLT           86.15    -182  -21860      1.41      118.69    258.44  -2099.86  2018-06-07     13        3  short  2018-06-04
       0.2     0.09   -0.01    118.69   TLT           29.76     372   44152.7    1.52      118.93     89.28  -2010.58  2018-06-12     14        3  long   2018-06-07
       2.36   -1.08   -3       118.93   TLT          -74.67    -372  -44242      0.25      121.74  -1045.32  -3055.9   2018-07-02     15       14  short  2018-06-12
      -0.78   -0.33   -2.73    275.275  SPY          -11.28     159   43768.8    1.53      273.14   -315.8   -3371.7   2018-07-06     12       28  long   2018-05-25
       1.88   -0.85   -2.3     273.14   SPY         -204.31    -159  -43429.3    0.16      278.28   -817.26  -4188.96  2018-07-12     17        4  short  2018-07-06
      -2.24   -1.01   -2.35    122.013  TLT          -64.31     353   43070.5    0.74      119.28   -964.63  -5153.59  2018-07-24     16       15  long   2018-07-02
       2.55    0.86   -0.24    278.28   SPY           45.82     156   43411.7    2.78      285.39    824.76  -4328.83  2018-08-07     18       18  long   2018-07-12
      -0.02    0.01   -1.01    119.053  TLT            0.81    -362  -43097      0.83      119.03      8.15  -4320.68  2018-08-07     19       10  short  2018-07-24
    

    Feel free to use/test it, let me know about any bugs found. The script:

    # Trade list similar to Amibroker output
    class trade_list(bt.Analyzer):
    
        def get_analysis(self):
    
            return self.trades
    
    
        def __init__(self):
    
            self.trades = []
            self.cumprofit = 0.0
    
    
        def notify_trade(self, trade):
    
            if trade.isclosed:
    
                brokervalue = self.strategy.broker.getvalue()
    
                dir = 'short'
                if trade.history[0].event.size > 0: dir = 'long'
    
                pricein = trade.history[len(trade.history)-1].status.price
                priceout = trade.history[len(trade.history)-1].event.price
                datein = bt.num2date(trade.history[0].status.dt)
                dateout = bt.num2date(trade.history[len(trade.history)-1].status.dt)
                if trade.data._timeframe >= bt.TimeFrame.Days:
                    datein = datein.date()
                    dateout = dateout.date()
    
                pcntchange = 100 * priceout / pricein - 100
                pnl = trade.history[len(trade.history)-1].status.pnlcomm
                pnlpcnt = 100 * pnl / brokervalue
                barlen = trade.history[len(trade.history)-1].status.barlen
                pbar = pnl / barlen
                self.cumprofit += pnl
    
                size = value = 0.0
                for record in trade.history:
                    if abs(size) < abs(record.status.size):
                        size = record.status.size
                        value = record.status.value
    
                highest_in_trade = max(trade.data.high.get(ago=0, size=barlen+1))
                lowest_in_trade = min(trade.data.low.get(ago=0, size=barlen+1))
                hp = 100 * (highest_in_trade - pricein) / pricein
                lp = 100 * (lowest_in_trade - pricein) / pricein
                if dir == 'long':
                    mfe = hp
                    mae = lp
                if dir == 'short':
                    mfe = -lp
                    mae = -hp
    
                self.trades.append({'ref': trade.ref, 'ticker': trade.data._name, 'dir': dir,
                     'datein': datein, 'pricein': pricein, 'dateout': dateout, 'priceout': priceout,
                     'chng%': round(pcntchange, 2), 'pnl': pnl, 'pnl%': round(pnlpcnt, 2),
                     'size': size, 'value': value, 'cumpnl': self.cumprofit,
                     'nbars': barlen, 'pnl/bar': round(pbar, 2),
                     'mfe%': round(mfe, 2), 'mae%': round(mae, 2)})
    

    It returns the list of dictionaries. In order to print it as a table above I've used tabulate module:

    from tabulate import tabulate
    ...
        # add analyzers
        cerebro.addanalyzer(trade_list, _name='trade_list')
    ...
        # run backtest
        strats = cerebro.run(tradehistory=True)
    ...
        # get analyzers data
        trade_list = strats[0].analyzers.trade_list.get_analysis()
        print (tabulate(trade_list, headers="keys"))
    

    Updated on 09/17/2018



  • Outputs -
    ref - bt's unique trade identifier
    ticker - data feed name
    datein - date and time of trade opening
    pricein - price of trade entry
    dir - long or short
    dateout - date and time of trade closing
    priceout - price of trade exit
    chng% - exit price to entry price ratio
    pnl - money profit/loss per trade
    pnl% - proft/loss in %s to broker's value at the trade closing
    cumpnl - cumulative profit/loss
    size - max position size during trade
    value - max trade value
    nbars - trade duration in bars
    pnl/bar - profit/loss per bar
    mfe% - max favorable excursion
    mae% - max adverse excursion


  • administrators

    Awesome!. Thx for sharing!



  • @ab_trader said in Closed trade list (including MFE/MAE) analyzer:

    strats[0]

    Hi, I have a simple question

    On your final lines,

    # get analyzers data
        trade_list = strats[0].analyzers.trade_list.get_analysis()
        print (tabulate(trade_list, headers="keys"))
    

    What does this code indicate?

    strats[0]
    

    I am new here, so this question might be silly :p

    Thank you for sharing!



  • Good point. I've updated initial post to include the line which run the backtest:

    strats = cerebro.run(tradehistory=True)
    

    strats will contain results of the backtest - see Docs - Cerebro - Returning the results



  • @ab_trader
    I am testing this on a live bot.

    I ran into the following issue:

    [TradeHistory([('status', AutoOrderedDict([('status', 1), ('dt', 736955.651388889), ('barlen', 0), ('size', 46), ('price', 6338.0), ('value', 291548.0), ('pnl', 0.0), ('pnlcomm', 0.0), ('tz', None)])), ('event', AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x112DBA70>), ('size', 46), ('price', 6338), ('commission', 0.0)]))]), TradeHistory([('status', AutoOrderedDict([('status', 2), ('dt', 736955.7006944445), ('barlen', 71), ('size', 0), ('price', 6338.0), ('value', 0.0), ('pnl', 506.0), ('pnlcomm', 506.0), ('tz', None)])), ('event', AutoOrderedDict([('order', <backtrader.order.SellOrder object at 0x132FBB30>), ('size', -46), ('price', 6349), ('commission', 0.0)]))])] ref:1
    data:<backmex.feed.BitMEXData object at 0x1129C5F0>
    tradeid:0
    size:0
    price:6338.0
    value:0.0
    commission:0.0
    pnl:506.0
    pnlcomm:506.0
    justopened:False
    isopen:False
    isclosed:True
    baropen:101
    dtopen:736955.651388889
    barclose:172
    dtclose:736955.7006944445
    barlen:71
    historyon:True
    history:[TradeHistory([('status', AutoOrderedDict([('status', 1), ('dt', 736955.651388889), ('barlen', 0), ('size', 46), ('price', 6338.0), ('value', 291548.0), ('pnl', 0.0), ('pnlcomm', 0.0), ('tz', None)])), ('event', AutoOrderedDict([('order', <backtrader.order.BuyOrder object at 0x112DBA70>), ('size', 46), ('price', 6338), ('commission', 0.0)]))]), TradeHistory([('status', AutoOrderedDict([('status', 2), ('dt', 736955.7006944445), ('barlen', 71), ('size', 0), ('price', 6338.0), ('value', 0.0), ('pnl', 506.0), ('pnlcomm', 506.0), ('tz', None)])), ('event', AutoOrderedDict([('order', <backtrader.order.SellOrder object at 0x132FBB30>), ('size', -46), ('price', 6349), ('commission', 0.0)]))])]
    status:2
    
      self._runnext(runstrats)
      File "D:\envs\twenv\lib\site-packages\backtrader\cerebro.py", line 1623, in _runnext
        self._brokernotify()
      File "D:\envs\twenv\lib\site-packages\backtrader\cerebro.py", line 1370, in _brokernotify
        owner._addnotification(order, quicknotify=self.p.quicknotify)
      File "D:\envs\twenv\lib\site-packages\backtrader\strategy.py", line 553, in _addnotification
        self._notify(qorders=qorders, qtrades=qtrades)
      File "D:\envs\twenv\lib\site-packages\backtrader\strategy.py", line 577, in _notify
        analyzer._notify_trade(trade)
      File "D:\envs\twenv\lib\site-packages\backtrader\analyzer.py", line 170, in _notify_trade
        self.notify_trade(trade)
      File "D:\envs\twenv\tradework\lib\backtrader\helpers.py", line 168, in notify_trade
        highest_in_trade = max(trade.data.high.get(ago=0, size=barlen+1))
      File "D:\envs\twenv\lib\site-packages\backtrader\linebuffer.py", line 182, in get
        return list(islice(self.array, start, end))
    ValueError: Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize.
    

    The first two objects are the printout of 'trade' and 'trade.history', if it helps in finding the issue.

    EDIT: I am using this with exactbars=True option in cerebro and maybe this is causing the error. I will try the same with this option off and let it be known here.



  • @kausality said in Closed trade list (including MFE/MAE) analyzer:

    I am testing this on a live bot.

    Were you able to run it?
    I am not using live trading now, so to set up a test case will take long time. I don't think that I can help you with it now.



  • @ab_trader
    Yes, the issue was because of exactbars=True being set. I turned that off and it worked.