pmorissette / bt

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

Issue with WeighTarget based on signal #310

Open ch3st3rt0n opened 3 years ago

ch3st3rt0n commented 3 years ago

I'm trying to replicate the SMA crossover strategy from one of your examples using one stock (link below as well as code) and get the same error with different stocks / periods). Here are the error messages:

/root/miniconda3/lib/python3.8/site-packages/ffn/core.py:2299: RuntimeWarning: divide by zero encountered in true_divide res = np.divide(er.mean(), std)

ExceptionTraceback (most recent call last)

in 7 bah_test = bt.Backtest(bah_strat, data) 8 # Run backtest ----> 9 bt_res = bt.run(sma20_test, sma50_test, sma100_test, ma_co_test, bah_test) ~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(*backtests) 26 # run each backtest 27 for bkt in backtests: ---> 28 bkt.run() 29 30 return Result(*backtests) ~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(self) 238 239 if not self.strategy.bankrupt: --> 240 self.strategy.run() 241 # need update after to save weights, values and such 242 self.strategy.update(dt) ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.Strategy.run() ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.AlgoStack.__call__() ~/miniconda3/lib/python3.8/site-packages/bt/algos.py in __call__(self, target) 1810 # Turn off updating while we rebalance each child 1811 for item in iteritems(targets): -> 1812 target.rebalance(item[1], child=item[0], base=base, update=False) 1813 1814 # Now update ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.StrategyBase.rebalance() ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.SecurityBase.allocate() Exception: Cannot allocate capital to 0 because price is 0 as of 2019-04-26 00:00:00 ----------- References ----------- === Link to example === https://pmorissette.github.io/bt/examples.html === Code snippet: === # Calculate indicators EMA_short = talib.EMA(data[ticker], timeperiod=10).to_frame() EMA_long = talib.EMA(data[ticker], timeperiod=40).to_frame() EMA_long.head() # Create signal signal = EMA_long.copy() signal[EMA_short > EMA_long] = 1.0 signal[EMA_short < EMA_long] = -1.0 signal[EMA_long.isnull()] = 0.0 # Define the strategy ma_co_strat = bt.Strategy('EMA_crossover', [bt.algos.WeighTarget(signal), bt.algos.Rebalance()]) # Run bt ma_co_test = bt.Backtest(ma_co_strat, data) bt_res = bt.run(ma_co_test) === Installed packages === bt 0.2.9 ffn 0.3.6 decorator 5.0.9
ostergaardl-ELS commented 3 years ago

I'm having the exact same issue.

dhiraj797 commented 3 years ago

I'm trying to replicate the SMA crossover strategy from one of your examples using one stock (link below as well as code) and get the same error with different stocks / periods). Here are the error messages:

/root/miniconda3/lib/python3.8/site-packages/ffn/core.py:2299: RuntimeWarning: divide by zero encountered in true_divide res = np.divide(er.mean(), std)

ExceptionTraceback (most recent call last) in 7 bah_test = bt.Backtest(bah_strat, data) 8 # Run backtest ----> 9 bt_res = bt.run(sma20_test, sma50_test, sma100_test, ma_co_test, bah_test)

~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(backtests) 26 # run each backtest 27 for bkt in backtests: ---> 28 bkt.run() 29 30 return Result(backtests)

~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(self) 238 239 if not self.strategy.bankrupt: --> 240 self.strategy.run() 241 # need update after to save weights, values and such 242 self.strategy.update(dt)

~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.Strategy.run()

~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.AlgoStack.call()

~/miniconda3/lib/python3.8/site-packages/bt/algos.py in call(self, target) 1810 # Turn off updating while we rebalance each child 1811 for item in iteritems(targets): -> 1812 target.rebalance(item[1], child=item[0], base=base, update=False) 1813 1814 # Now update

~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.StrategyBase.rebalance()

~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.SecurityBase.allocate()

Exception: Cannot allocate capital to 0 because price is 0 as of 2019-04-26 00:00:00

----------- References ----------- === Link to example === https://pmorissette.github.io/bt/examples.html

=== Code snippet: ===

Calculate indicators

EMA_short = talib.EMA(data[ticker], timeperiod=10).to_frame() EMA_long = talib.EMA(data[ticker], timeperiod=40).to_frame() EMA_long.head()

Create signal

signal = EMA_long.copy() signal[EMA_short > EMA_long] = 1.0 signal[EMA_short < EMA_long] = -1.0 signal[EMA_long.isnull()] = 0.0

Define the strategy

ma_co_strat = bt.Strategy('EMA_crossover', [bt.algos.WeighTarget(signal), bt.algos.Rebalance()])

Run bt

ma_co_test = bt.Backtest(ma_co_strat, data) bt_res = bt.run(ma_co_test)

=== Installed packages === bt 0.2.9 ffn 0.3.6 decorator 5.0.9

Hey..Did you resolve tuple out of range error during bt.get?

ch3st3rt0n commented 3 years ago

I'm trying to replicate the SMA crossover strategy from one of your examples using one stock (link below as well as code) and get the same error with different stocks / periods). Here are the error messages: /root/miniconda3/lib/python3.8/site-packages/ffn/core.py:2299: RuntimeWarning: divide by zero encountered in true_divide res = np.divide(er.mean(), std) ExceptionTraceback (most recent call last) in 7 bah_test = bt.Backtest(bah_strat, data) 8 # Run backtest ----> 9 bt_res = bt.run(sma20_test, sma50_test, sma100_test, ma_co_test, bah_test) ~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(backtests) 26 # run each backtest 27 for bkt in backtests: ---> 28 bkt.run() 29 30 return Result(backtests) ~/miniconda3/lib/python3.8/site-packages/bt/backtest.py in run(self) 238 239 if not self.strategy.bankrupt: --> 240 self.strategy.run() 241 # need update after to save weights, values and such 242 self.strategy.update(dt) ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.Strategy.run() ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.AlgoStack.call() ~/miniconda3/lib/python3.8/site-packages/bt/algos.py in call(self, target) 1810 # Turn off updating while we rebalance each child 1811 for item in iteritems(targets): -> 1812 target.rebalance(item[1], child=item[0], base=base, update=False) 1813 1814 # Now update ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.StrategyBase.rebalance() ~/miniconda3/lib/python3.8/site-packages/bt/core.cpython-38-x86_64-linux-gnu.so in bt.core.SecurityBase.allocate() Exception: Cannot allocate capital to 0 because price is 0 as of 2019-04-26 00:00:00 ----------- References ----------- === Link to example === https://pmorissette.github.io/bt/examples.html === Code snippet: ===

Calculate indicators

EMA_short = talib.EMA(data[ticker], timeperiod=10).to_frame() EMA_long = talib.EMA(data[ticker], timeperiod=40).to_frame() EMA_long.head()

Create signal

signal = EMA_long.copy() signal[EMA_short > EMA_long] = 1.0 signal[EMA_short < EMA_long] = -1.0 signal[EMA_long.isnull()] = 0.0

Define the strategy

ma_co_strat = bt.Strategy('EMA_crossover', [bt.algos.WeighTarget(signal), bt.algos.Rebalance()])

Run bt

ma_co_test = bt.Backtest(ma_co_strat, data) bt_res = bt.run(ma_co_test) === Installed packages === bt 0.2.9 ffn 0.3.6 decorator 5.0.9

Hey..Did you resolve tuple out of range error during bt.get?

No, I got this error too and wasn't able to get this function working. I ended up with a workaround pulling the data myself from yahoo and passing it to bt directly.

ostergaardl-ELS commented 3 years ago

I can't seem to get bt.algos.WeighTarget() to work in any scenario when backtesting, unless if I set the signal series to all zeroes (which of course doesn't do anything).

danilogalisteu commented 3 years ago

ma_co_strat = bt.Strategy('EMA_crossover', [bt.algos.WeighTarget(signal), bt.algos.Rebalance()])

Does it work if you add bt.algos.SelectAll()? This is how I use it:

s1 = bt.Strategy('s1', [bt.algos.SelectAll(),
                        bt.algos.WeighTarget(strat),
                        bt.algos.Rebalance()])

b1 = bt.Backtest(s1, data,
                 initial_capital=capital,
                 commissions=comm,
                 integer_positions=integer_positions)

res = bt.run(b1)
res.display()
res.plot()
res.plot_weights('s1')

Here strat has the same shape as data, but it could have less rows (only when you need a rebalance).

ostergaardl-ELS commented 3 years ago

ma_co_strat = bt.Strategy('EMA_crossover', [bt.algos.WeighTarget(signal), bt.algos.Rebalance()])

Does it work if you add bt.algos.SelectAll()? This is how I use it:

s1 = bt.Strategy('s1', [bt.algos.SelectAll(),
                        bt.algos.WeighTarget(strat),
                        bt.algos.Rebalance()])

b1 = bt.Backtest(s1, data,
                 initial_capital=capital,
                 commissions=comm,
                 integer_positions=integer_positions)

res = bt.run(b1)
res.display()
res.plot()
res.plot_weights('s1')

Here strat has the same shape as data, but it could have less rows (only when you need a rebalance).

Thanks for the tip, but it didn't help to include SelectAll(). Same error. I made sure the data and signal shapes were the same too. Any other ideas?

danilogalisteu commented 3 years ago

I'm running the master branch.

ostergaardl-ELS commented 3 years ago

I tried running the master branch too. It looks like the error occurs when the signal flips to a non-zero value for the first time.

samuel-cmcc commented 3 years ago

The columns of the DataFrame you pass into WeighTarget must be the same as the securities of your backtest. When you are generating the EMAs in your code, you are using to_frame() to convert the Series into DataFrames. The issue is that because these Series have no name, the resulting DataFrame's column name becomes '0' (check the output when you run EMA_long.head()).

The easiest solution would be to rename the column in signal. Rename the column so that it matches the security name in data and your backtest should work.

signal = EMA_long.copy()
signal[EMA_short > EMA_long] = 1.0
signal[EMA_short < EMA_long] = -1.0
signal[EMA_long.isnull()] = 0.0
signal.columns = [ticker]
ch3st3rt0n commented 3 years ago

The columns of the DataFrame you pass into WeighTarget must be the same as the securities of your backtest. When you are generating the EMAs in your code, you are using to_frame() to convert the Series into DataFrames. The issue is that because these Series have no name, the resulting DataFrame's column name becomes '0' (check the output when you run EMA_long.head()).

The easiest solution would be to rename the column in signal. Rename the column so that it matches the security name in data and your backtest should work.

signal = EMA_long.copy()
signal[EMA_short > EMA_long] = 1.0
signal[EMA_short < EMA_long] = -1.0
signal[EMA_long.isnull()] = 0.0
signal.columns = [ticker]

Thanks very much - this fixed it! I was using for backtesting 1 stock and didn't realize that the column names were missing.

EduardoLoz12 commented 3 years ago

Hello, so i have almost the same error and cant fix it.

Price=data1["Adj Close"].to_frame() Signal=data1["Signals"].to_frame() Signal.columns=[Price]

bt_strategy=bt.Strategy("Strategye", [bt.algos.WeighTarget(Signal), bt.algos.Rebalance()]) bt_backtest=bt.Backtest(bt_strategy, Price) bt_result=bt.run(bt_backtest) bt_result.plot(title='Backtest result')

This gives me this error.

Traceback (most recent call last): File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\main.py", line 79, in bt_result=bt.run(btbacktest) File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\backtest.py", line 28, in run bkt.run() File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\backtest.py", line 240, in run self.strategy.run() File "bt\core.py", line 2101, in bt.core.Strategy.run File "bt\core.py", line 2040, in bt.core.AlgoStack.call File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\algos.py", line 1812, in call target.rebalance(item[1], child=item[0], base=base, update=False) File "bt\core.py", line 1016, in bt.core.StrategyBase.rebalance File "bt\core.py", line 1508, in bt.core.SecurityBase.allocate Exception: Cannot allocate capital to ('Adj Close',) because price is 0 as of 2017-01-11 00:00:00

Im new in this programing world and im kinda lost.

danilogalisteu commented 3 years ago

@EduardoLoz12 in your third line Signal.columns=[Price] you set the name of the column as the DataFrame Price. That doesn't seem right. See that, in the post above yours, the line signal.columns = [ticker] sets the column name to a string stored in the variable ticker.

From the previous posts, you should set the same column names in the Price and in the Signal frames, such as:

Price = data1["Adj Close"].to_frame()
Price.columns = [ticker]
Signal = data1["Signals"].to_frame()
Signal.columns = [ticker]

or:

Price = data1["Adj Close"].to_frame(name=ticker)
Signal = data1["Signals"].to_frame(name=ticker)

where ticker is a string variable that you defined beforehand (the name of your stock/ETF/whatever) that will become the name of the column.

EduardoLoz12 commented 3 years ago

@danilogalisteu Thank you for the fast reply!

Alright, so i did this. I put "ticker" as a string name for both columns (not sure if that's right though). Both are <class 'pandas.core.frame.DataFrame'> and has the same column names when i print those df.

Price = data1["Adj Close"].to_frame(name="ticker") Signal = data1["Signals"].to_frame(name="ticker")

bt_strategy=bt.Strategy("Strategye",[bt.algos.WeighTarget(Signal), bt.algos.Rebalance()]) bt_backtest=bt.Backtest(bt_strategy, Price) bt_result=bt.run(bt_backtest)

But now i have a different error:

Traceback (most recent call last): File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\main.py", line 99, in bt_result=bt.run(bt_backtest) File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\backtest.py", line 28, in run bkt.run() File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\backtest.py", line 240, in run self.strategy.run() File "bt\core.py", line 2101, in bt.core.Strategy.run File "bt\core.py", line 2040, in bt.core.AlgoStack.call File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\bt\algos.py", line 1812, in call target.rebalance(item[1], child=item[0], base=base, update=False) File "bt\core.py", line 955, in bt.core.StrategyBase.rebalance File "C:\Users\E.Lozada\PycharmProjects\CryptoRobot\venv\lib\site-packages\pandas\core\series.py", line 185, in wrapper raise TypeError(f"cannot convert the series to {converter}") TypeError: cannot convert the series to <class 'float'>

Also, the only way to make bt work is if you get your data from bt.get()? Because I've got my data from web.DataReader() to get "high","low","Adj Close", etc to build some other indicators. But at the end in my second line Signal = data1["Signals"].to_frame(name=ticker) is a dataframe with 1,-1,0. Just as the example from the beginning.

Sorry if I can not explain myself better, I'm just trying to build my first code.

danilogalisteu commented 3 years ago

Also, the only way to make bt work is if you get your data from bt.get()?

No, any numerical series/dataframe with a datetime index should work. bt.get() just gets the adjusted close prices from Yahoo by default.

It's hard to help you at all without being able to look at your code and run it or at least look at the data you are using. The last error tells you that something should have float type but doesn't. Can you at least show data1 or the Price and Signal dataframes?

EduardoLoz12 commented 3 years ago

Thanks, sure I can share with you the data of data1. data1Excel.xlsx. I still cant fix this problem. When i run this code without the bt.algos.Rebalance() it works but give me all the data spots of bt.display() = 0. I dont know if that means something??

bt_strategy=bt.Strategy("Strategye",[bt.algos.WeighTarget(Signal), bt.algos.Rebalance()]) bt_backtest=bt.Backtest(bt_strategy, Price) bt_result=bt.run(bt_backtest)

bt_result.display()

Stat Strategye


Start 2016-12-31 End 2021-11-10 Risk-free rate 0.00%

Total Return 0.00% Daily Sharpe - Daily Sortino - CAGR 0.00% Max Drawdown 0.00% Calmar Ratio -

danilogalisteu commented 2 years ago

Before I run the code, this is what I see in the relevant columns of your data:

Date Adj Close Signals
2016-12-29 00:00:00 973.497 0
2016-12-30 00:00:00 961.238 0
2016-12-31 00:00:00 963.743 0
2017-01-01 00:00:00 998.325 0
2017-01-02 00:00:00 1021.75 0
2017-01-03 00:00:00 1043.84 0
2017-01-04 00:00:00 1154.73 0
2017-01-05 00:00:00 1013.38 0
2017-01-06 00:00:00 902.201 1
2017-01-07 00:00:00 908.585 0
2017-01-08 00:00:00 911.199 0
2017-01-09 00:00:00 902.828 0

Remember that Signal is actually a weight, and not a signal to buy or sell (bt.algos.WeighTarget(Signal)). At the moment, you are buying the asset as your whole portfolio on 2017-01-06 at the price of 902.201 and selling it all on the next day at the price of 908.585. Is this what you want?

EduardoLoz12 commented 2 years ago

@danilogalisteu No, actually. I was trying to code that Signal=1 = Buy, Signal=-1 = Sell. Am i using the wrong algos? Should i change the Signal data to weigh type?

danilogalisteu commented 2 years ago

You should change your signal to stay at 1 when you want to be long and go to 0 when you want to be flat, if that's what you want.

Regarding the data, your dataframe has two lines for the date 2017-03-26 and that's where it goes down for some reason. You should make sure your data has only one line for each date. I ran the code like this (there is a comment for the fix):

import pandas as pd
import bt

data1  = pd.read_excel(r"C:\Users\danil\Downloads\data1Excel.xlsx", sheet_name='Data', index_col='Date', parse_dates=['Date'])

# this line removes rows with duplicated index values, keeping the last one
data1  = data1[~data1.index.duplicated(keep='last')]

Price  = data1["Adj Close"].to_frame(name="ticker")
Signal = data1["Signals"].to_frame(name="ticker")

bt_strategy = bt.Strategy("Strategye", [bt.algos.WeighTarget(Signal), bt.algos.Rebalance()])
bt_backtest = bt.Backtest(bt_strategy, Price)

bt_result = bt.run(bt_backtest)
bt_result.display()
bt_result.plot()
danilogalisteu commented 2 years ago

This would be the weight calculation from the signal if you want to go long at signal=1 and go flat at signal=-1.

import numpy as np
import pandas as pd
import bt

from matplotlib import use
use('Qt5Agg')

data1  = pd.read_excel(r"C:\Users\danil\Downloads\data1Excel.xlsx", sheet_name='Data', index_col='Date', parse_dates=['Date'])

# this line removes rows with duplicated index values, keeping the last one
data1  = data1[~data1.index.duplicated(keep='last')]

Price  = data1["Adj Close"].to_frame(name="ticker")
Signal = data1["Signals"].to_frame(name="ticker")

# go long at signal=1 and go flat at signal=-1
Weight = Signal.replace(0, np.nan).replace(-1, 0).ffill().replace(np.nan, 0)

bt_strategy = bt.Strategy("Strategye", [bt.algos.WeighTarget(Weight), bt.algos.Rebalance()])
bt_backtest = bt.Backtest(bt_strategy, Price)

bt_result = bt.run(bt_backtest)
bt_result.display()

bt_result.plot()

The equity curve is something:

image

The results are below.

Stat                 Strategye
-------------------  -----------
Start                2016-12-28
End                  2021-11-08
Risk-free rate       0.00%

Total Return         1419.91%
Daily Sharpe         1.02
Daily Sortino        1.71
CAGR                 75.01%
Max Drawdown         -71.35%
Calmar Ratio         1.05

MTD                  7.50%
3m                   15.84%
6m                   15.84%
YTD                  130.48%
1Y                   289.28%
3Y (ann.)            118.14%
5Y (ann.)            75.01%
10Y (ann.)           -
Since Incep. (ann.)  75.01%

Daily Sharpe         1.02
Daily Sortino        1.71
Daily Mean (ann.)    51.37%
Daily Vol (ann.)     50.39%
Daily Skew           0.81
Daily Kurt           11.03
Best Day             25.21%
Worst Day            -18.73%

Monthly Sharpe       1.20
Monthly Sortino      3.28
Monthly Mean (ann.)  72.10%
Monthly Vol (ann.)   59.90%
Monthly Skew         1.13
Monthly Kurt         2.15
Best Month           58.43%
Worst Month          -31.28%

Yearly Sharpe        1.00
Yearly Sortino       4.24
Yearly Mean          108.43%
Yearly Vol           108.21%
Yearly Skew          -0.71
Yearly Kurt          1.73
Best Year            241.61%
Worst Year           -57.16%

Avg. Drawdown        -11.71%
Avg. Drawdown Days   55.80
Avg. Up Month        18.25%
Avg. Down Month      -5.05%
Win Year %           80.00%
Win 12m %            83.67%
EduardoLoz12 commented 2 years ago

@danilogalisteu Thank you very much! I used everything you coded and it worked! Will keep working on my trading bot now.