pmorissette / bt

bt - flexible backtesting for Python
http://pmorissette.github.io/bt
MIT License
2.19k stars 420 forks source link

Did i build a stop loss and trailing stop loss correctly? #377

Open brettelliot opened 2 years ago

brettelliot commented 2 years ago

Hi all,

I didn't see stop loss or trailing stop loss functionality anywhere so I tried implementing it myself. The stop loss was easy but the trailing stop loss took a little work. I figured I'd post here so that other people might benefit.. or maybe other people can tell me where I messed up!

Would love feedback on these.

Thanks, Brett

import pandas as pd 
import numpy as np
%matplotlib inline

pd.set_option('display.max_rows', None)
pd.set_option('display.max_columns', None)
pd.set_option('display.width', None)
pd.set_option('display.max_colwidth', None)

# Set trading fees
def trading_fees(q, p):
    return abs(q)*p*0.003

# Simple moving average backtest
def sma_strat(tickers, sma_per=50, start='2018-01-01', name='sma', initial_capital=1000.0, stop_pct=None, trailing=None):
    """
    Long securities that are above their n period.

    """
    # download data
    data = bt.get(tickers, start=start)

    # calc sma
    sma = data.rolling(sma_per).mean()

    signals = sma.copy()
    signals[data > sma] = True
    signals[data <= sma] = False
    signals[sma.isnull()] = False

    if stop_pct is not None:
        if trailing:
            # Use a trailing stop loss
            for ticker in data:
                df = pd.DataFrame()
                df['price'] = data[ticker]
                df['signal_change'] = signals[ticker].ne(signals[ticker].shift())
                df['trade_id'] = df['signal_change'].cumsum()
                df['trailing_stop_loss'] = df.groupby('trade_id')['price'].cummax() * (1 - stop_pct)
                signals.loc[df['price'] <= df['trailing_stop_loss'], ticker] = False
        else:
            # Use a stop loss
            stop_loss = (data*(1-stop_pct)).where((signals.shift(1) == False) & (signals == True), np.nan)
            stop_loss = stop_loss.ffill()
            signals[data <= stop_loss] = False

    tw = signals.copy()
    tw[signals == True] = 1.0 / len(tickers)
    tw[signals == False] = 0.0  

    # create strategy
    s = bt.Strategy(name, [bt.algos.WeighTarget(tw),
                                    bt.algos.Rebalance()])

    # now we create the backtest
    return bt.Backtest(s, data, integer_positions=False, initial_capital=initial_capital, commissions=trading_fees)

# Params
start = '2018-01-01'
initial_capital=1000.0
tickers = ['BTC-USD', 'ETH-USD']

sma = sma_strat(tickers, sma_per=50, start=start, name='sma', initial_capital=initial_capital)
sma_stop = sma_strat(tickers, sma_per=50, start=start, name='sma_stop', initial_capital=initial_capital, stop_pct=0.05)
sma_trailing_stop = sma_strat(tickers, sma_per=50, start=start, name='sma_trailing_stop', initial_capital=initial_capital, stop_pct=0.05, trailing=True)

# run all the backtests!
results = bt.run(sma, sma_stop, sma_trailing_stop)
results.plot(freq='m', logy=True);
results.display()
brettelliot commented 2 years ago

Ive spent more time on this and the trailing stop loss is definitely busted. I've managed to build one using a loop but the one above has issues.

posidonius commented 1 year ago

Ive spent more time on this and the trailing stop loss is definitely busted. I've managed to build one using a loop but the one above has issues.

You should avoid using a for loop and try to find a vector-based solution since you're working in Pandas. Looping through each ticker in the universe is going to be extremely inefficient.

Let me know if you ever made any progress on this btw.