Files
OpenBB/openbb_terminal/stocks/backtesting/bt_model.py
teh_coderer 59d8b36bb0 [IMPROVE] Fix Docstring formatting/Fix missing, incomplete type hints (#3412)
* Fixes

* Update stocks_helper.py

* update git-actions set-output to new format

* Update stocks_helper.py

* Update terminal_helper.py

* removed LineAnnotateDrawer from qa_view

* lint

* few changes

* updates

* sdk auto gen modules done

* Update stocks_helper.py

* updates to changed imports, and remove first sdk_modules

* Update generate_sdk.py

* Update generate_sdk.py

* pylint

* revert stocks_helper

* Update generate_sdk.py

* Update sdk.py

* Update generate_sdk.py

* full auto generation, added sdk.py/controllers creation

* missed enable forecasting

* added running black in subprocess after sdk files generation completes

* removed deleted sdk_arg_logger

* comment out tests

* property doc fix

* clean up

* Update generate_sdk.py

* make trailmap classes useable for doc generation

* Update generate_sdk.py

* added lineon to trailmap class for linking to func in markdown

* changed lineon to dict

* added full_path to trailmap for linking in docs

* updated portfolio

* feat: initial files

* feat: added meta head

* feat: added funcdef

* added func_def to trailmap attributes for markdown in docs, added missing type hints to covid functions

* feat: added view and merged with jaun

* Update generate_sdk.py

* Update generate_sdk.py

* Update generate_sdk.py

* Update generate_sdk.py

* init

* fix returns

* fix: random stuff

* fix: random

* fixed encoding issue on windows

* fix: generate tabs

* update

* Update generate_sdk_markdown.py

* Create .pydocstyle.ini

* added type hint classes for views

* fixes

* alt, ba

* alt-economy

* Update finviz_compare_model.py

* fixs

* Update substack_model.py

* Update generate_sdk.py

* last of my section

* porfolio

* po

* Update optimizer_model.py

* fixing more things

* few more

* keys done

* update

* fixes

* Update generate_sdk_markdown.py

* Update generate_sdk_markdown.py

* mypy forecast fix

* Update generate_sdk_markdown.py

* Update generate_sdk_markdown.py

* Update generate_sdk_markdown.py

* fixes

* forecast fixes

* one more fix

* Update coinbase_model.py

* Update generate_sdk_markdown.py

Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
Co-authored-by: jose-donato <zmcdonato@gmail.com>
Co-authored-by: andrewkenreich <andrew.kenreich@gmail.com>
2022-11-15 16:37:49 -05:00

291 lines
7.9 KiB
Python

"""Backtesting Model"""
__docformat__ = "numpy"
import logging
import warnings
import bt
import pandas as pd
import pandas_ta as ta
import yfinance as yf
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import is_intraday
from openbb_terminal.common.technical_analysis import ta_helpers
logger = logging.getLogger(__name__)
@log_start_end(log=logger)
def get_data(symbol: str, start_date: str = "2019-01-01") -> pd.DataFrame:
"""Function to replace bt.get, gets Adjusted close of symbol using yfinance.
Parameters
----------
symbol: str
Ticker to get data for
start_date: str
Start date in YYYY-MM-DD format
Returns
-------
prices: pd.DataFrame
Dataframe of Adj Close with columns = [ticker]
"""
data = yf.download(symbol, start=start_date, progress=False)
close_col = ta_helpers.check_columns(data, high=False, low=False)
if close_col is None:
return pd.DataFrame()
prices = pd.DataFrame(data[close_col])
prices.columns = [symbol]
return prices
@log_start_end(log=logger)
def buy_and_hold(symbol: str, start_date: str, name: str = "") -> bt.Backtest:
"""Generates a buy and hold backtest object for the given ticker.
Parameters
----------
symbol: str
Stock to test
start_date: str
Backtest start date, in YYYY-MM-DD format. Can be either string or datetime
name: str
Name of the backtest (for labeling purposes)
Returns
-------
bt.Backtest
Backtest object for buy and hold strategy
"""
prices = get_data(symbol, start_date)
bt_strategy = bt.Strategy(
name,
[
bt.algos.RunOnce(),
bt.algos.SelectAll(),
bt.algos.WeighEqually(),
bt.algos.Rebalance(),
],
)
return bt.Backtest(bt_strategy, prices)
@log_start_end(log=logger)
def ema_strategy(
symbol: str,
data: pd.DataFrame,
ema_length: int = 20,
spy_bt: bool = True,
no_bench: bool = False,
) -> bt.backtest.Result:
"""Perform backtest for simple EMA strategy. Buys when price>EMA(l).
Parameters
----------
symbol: str
Stock ticker
data: pd.DataFrame
Dataframe of prices
ema_length: int
Length of ema window
spy_bt: bool
Boolean to add spy comparison
no_bench: bool
Boolean to not show buy and hold comparison
Returns
-------
bt.backtest.Result
Backtest results
"""
# TODO: Help Wanted!
# Implement support for backtesting on intraday data
if is_intraday(data):
return None
data.index = pd.to_datetime(data.index.date)
symbol = symbol.lower()
ema = pd.DataFrame()
start_date = data.index[0]
close_col = ta_helpers.check_columns(data, high=False, low=False)
if close_col is None:
return bt.backtest.Result()
prices = pd.DataFrame(data[close_col])
prices.columns = [symbol]
ema[symbol] = ta.ema(prices[symbol], ema_length)
bt_strategy = bt.Strategy(
"AboveEMA",
[
bt.algos.SelectWhere(prices >= ema),
bt.algos.WeighEqually(),
bt.algos.Rebalance(),
],
)
bt_backtest = bt.Backtest(bt_strategy, prices)
backtests = [bt_backtest]
if spy_bt:
spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
backtests.append(spy_bt)
if not no_bench:
stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
backtests.append(stock_bt)
res = bt.run(*backtests)
return res
@log_start_end(log=logger)
def emacross_strategy(
symbol: str,
data: pd.DataFrame,
short_length: int = 20,
long_length: int = 50,
spy_bt: bool = True,
no_bench: bool = False,
shortable: bool = True,
) -> bt.backtest.Result:
"""Perform backtest for simple EMA strategy. Buys when price>EMA(l).
Parameters
----------
symbol : str
Stock ticker
data : pd.DataFrame
Dataframe of prices
short_length : int
Length of short ema window
long_length : int
Length of long ema window
spy_bt : bool
Boolean to add spy comparison
no_bench : bool
Boolean to not show buy and hold comparison
shortable : bool
Boolean to allow for selling of the stock at cross
Returns
-------
Result
Backtest results
"""
symbol = symbol.lower()
start_date = data.index[0]
close_col = ta_helpers.check_columns(data, low=False, high=False)
if close_col is None:
return bt.backtest.Result()
prices = pd.DataFrame(data[close_col])
prices.columns = [symbol]
short_ema = pd.DataFrame(ta.ema(prices[symbol], short_length))
short_ema.columns = [symbol]
long_ema = pd.DataFrame(ta.ema(prices[symbol], long_length))
long_ema.columns = [symbol]
# signals
signals = long_ema.copy()
signals[short_ema > long_ema] = 1.0
signals[short_ema <= long_ema] = -1.0 * shortable
signals[long_ema.isnull()] = 0.0
combined_data = bt.merge(signals, prices, short_ema, long_ema)
combined_data.columns = ["signal", "price", "ema_short", "ema_long"]
bt_strategy = bt.Strategy(
"EMACross",
[
bt.algos.WeighTarget(signals),
bt.algos.Rebalance(),
],
)
bt_backtest = bt.Backtest(bt_strategy, prices)
backtests = [bt_backtest]
if spy_bt:
spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
backtests.append(spy_bt)
if not no_bench:
stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
backtests.append(stock_bt)
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
res = bt.run(*backtests)
return res
@log_start_end(log=logger)
def rsi_strategy(
symbol: str,
data: pd.DataFrame,
periods: int = 14,
low_rsi: int = 30,
high_rsi: int = 70,
spy_bt: bool = True,
no_bench: bool = False,
shortable: bool = True,
) -> bt.backtest.Result:
"""Perform backtest for simple EMA strategy. Buys when price>EMA(l).
Parameters
----------
symbol : str
Stock ticker
data : pd.DataFrame
Dataframe of prices
periods : int
Number of periods for RSI calculation
low_rsi : int
Low RSI value to buy
high_rsi : int
High RSI value to sell
spy_bt : bool
Boolean to add spy comparison
no_bench : bool
Boolean to not show buy and hold comparison
shortable : bool
Flag to disable the ability to short sell
Returns
-------
Result
Backtest results
"""
symbol = symbol.lower()
start_date = data.index[0]
close_col = ta_helpers.check_columns(data, high=False, low=False)
if close_col is None:
return pd.DataFrame()
prices = pd.DataFrame(data[close_col])
prices.columns = [symbol]
rsi = pd.DataFrame(ta.rsi(prices[symbol], periods))
rsi.columns = [symbol]
signal = 0 * rsi.copy()
signal[rsi > high_rsi] = -1 * shortable
signal[rsi < low_rsi] = 1
signal[rsi.isnull()] = 0
merged_data = bt.merge(signal, prices)
merged_data.columns = ["signal", "price"]
warnings.simplefilter(action="ignore", category=FutureWarning)
bt_strategy = bt.Strategy(
"RSI Reversion", [bt.algos.WeighTarget(signal), bt.algos.Rebalance()]
)
bt_backtest = bt.Backtest(bt_strategy, prices)
bt_backtest = bt.Backtest(bt_strategy, prices)
backtests = [bt_backtest]
# Once the bt package replaces pd iteritems with items we can remove this
with warnings.catch_warnings():
if spy_bt:
spy_bt = buy_and_hold("spy", start_date, "SPY Hold")
backtests.append(spy_bt)
if not no_bench:
stock_bt = buy_and_hold(symbol, start_date, symbol.upper() + " Hold")
backtests.append(stock_bt)
with warnings.catch_warnings():
warnings.filterwarnings("ignore")
res = bt.run(*backtests)
return res