mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-27 12:40:35 +00:00
Merge pull request #4079 from OpenBB-finance/hotfix/fix4054
Fix load a benchmark that isn't in the list of limited choices + yf.Ticker().info bugs
This commit is contained in:
@@ -2,6 +2,13 @@ portfolio
|
||||
load --example
|
||||
show
|
||||
bench QQQ
|
||||
bench FAKE_BENCH
|
||||
bench TSLA
|
||||
alloc assets -t -l 4
|
||||
alloc sectors -t -l 4
|
||||
alloc countries -t -l 4
|
||||
alloc regions -t -l 4
|
||||
bench SPY
|
||||
alloc assets -t -l 4
|
||||
alloc sectors -t -l 4
|
||||
alloc countries -t -l 4
|
||||
|
||||
@@ -72,11 +72,16 @@ def get_assets_allocation(
|
||||
pd.DataFrame
|
||||
DataFrame with the portfolio's asset allocations
|
||||
"""
|
||||
benchmark_assets_allocation = pd.DataFrame(benchmark_info["holdings"])
|
||||
benchmark_assets_allocation.rename(
|
||||
columns={"symbol": "Symbol", "holdingPercent": "Benchmark"}, inplace=True
|
||||
)
|
||||
benchmark_assets_allocation.drop(columns=["holdingName"], inplace=True)
|
||||
if "holdings" in benchmark_info:
|
||||
benchmark_assets_allocation = pd.DataFrame(benchmark_info["holdings"])
|
||||
benchmark_assets_allocation.rename(
|
||||
columns={"symbol": "Symbol", "holdingPercent": "Benchmark"}, inplace=True
|
||||
)
|
||||
benchmark_assets_allocation.drop(columns=["holdingName"], inplace=True)
|
||||
else:
|
||||
benchmark_assets_allocation = pd.DataFrame(
|
||||
columns=["Symbol", "Benchmark", "Sector", "Country"]
|
||||
)
|
||||
|
||||
portfolio_assets_allocation = (
|
||||
portfolio_trades[portfolio_trades["Type"] != "CASH"]
|
||||
@@ -109,34 +114,37 @@ def get_sectors_allocation(
|
||||
Returns
|
||||
-------
|
||||
pd.DataFrame
|
||||
DataFrame with regional allocations.
|
||||
DataFrame with benchmark allocations.
|
||||
pd.DataFrame
|
||||
DataFrame with country allocations
|
||||
DataFrame with portfolio allocations
|
||||
"""
|
||||
|
||||
benchmark_sectors_allocation = (
|
||||
pd.DataFrame.from_dict(
|
||||
data={
|
||||
sector_name: allocation
|
||||
for sector in benchmark_info["sectorWeightings"]
|
||||
for sector_name, allocation in sector.items()
|
||||
},
|
||||
orient="index",
|
||||
if "sectorWeightings" in benchmark_info:
|
||||
benchmark_sectors_allocation = (
|
||||
pd.DataFrame.from_dict(
|
||||
data={
|
||||
sector_name: allocation
|
||||
for sector in benchmark_info["sectorWeightings"]
|
||||
for sector_name, allocation in sector.items()
|
||||
},
|
||||
orient="index",
|
||||
)
|
||||
.squeeze()
|
||||
.sort_values(ascending=False)
|
||||
)
|
||||
.squeeze()
|
||||
.sort_values(ascending=False)
|
||||
)
|
||||
|
||||
# Prettify sector allocations of benchmark to align with Portfolio Excel
|
||||
prettified = [
|
||||
sector.replace("_", " ").title()
|
||||
for sector in benchmark_sectors_allocation.index
|
||||
]
|
||||
# Prettify sector allocations of benchmark to align with Portfolio Excel
|
||||
prettified = [
|
||||
sector.replace("_", " ").title()
|
||||
for sector in benchmark_sectors_allocation.index
|
||||
]
|
||||
|
||||
benchmark_sectors_allocation.index = prettified
|
||||
benchmark_sectors_allocation = pd.DataFrame(benchmark_sectors_allocation)
|
||||
benchmark_sectors_allocation.reset_index(inplace=True)
|
||||
benchmark_sectors_allocation.columns = ["Sector", "Benchmark"]
|
||||
benchmark_sectors_allocation.index = prettified
|
||||
benchmark_sectors_allocation = pd.DataFrame(benchmark_sectors_allocation)
|
||||
benchmark_sectors_allocation.reset_index(inplace=True)
|
||||
benchmark_sectors_allocation.columns = ["Sector", "Benchmark"]
|
||||
else:
|
||||
benchmark_sectors_allocation = pd.DataFrame(columns=["Sector", "Benchmark"])
|
||||
|
||||
# Define portfolio sector allocation
|
||||
# Aggregate sector value for stocks and crypto
|
||||
@@ -243,9 +251,9 @@ def get_countries_allocation(
|
||||
Returns
|
||||
-------
|
||||
pd.DataFrame
|
||||
DataFrame with regional allocations.
|
||||
DataFrame with benchmark allocations.
|
||||
pd.DataFrame
|
||||
DataFrame with country allocations
|
||||
DataFrame with portfolio allocations
|
||||
"""
|
||||
|
||||
benchmark_allocation = get_symbol_allocation(
|
||||
@@ -305,7 +313,7 @@ def get_symbol_allocation(
|
||||
Returns
|
||||
-------
|
||||
pd.DataFrame
|
||||
Dictionary with country allocations
|
||||
Dictionary with category allocations
|
||||
"""
|
||||
|
||||
if category == "Region":
|
||||
@@ -332,7 +340,10 @@ def get_symbol_allocation(
|
||||
# Collect data from Fidelity about the portfolio composition of the benchmark
|
||||
URL = f"https://screener.fidelity.com/ftgw/etf/goto/snapshot/portfolioComposition.jhtml?symbols={symbol}"
|
||||
html = request(URL).content
|
||||
df_list = pd.read_html(html)
|
||||
try:
|
||||
df_list = pd.read_html(html)
|
||||
except ValueError:
|
||||
return pd.DataFrame(columns=[category, col_name])
|
||||
|
||||
# Find the ones that contain regions and countries
|
||||
for index, item in enumerate(df_list):
|
||||
|
||||
@@ -9,6 +9,7 @@ import pandas as pd
|
||||
import yfinance as yf
|
||||
|
||||
from openbb_terminal.decorators import log_start_end
|
||||
from openbb_terminal.rich_config import console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,7 +69,11 @@ def get_spy_sector_contributions(
|
||||
|
||||
# Load in info
|
||||
sp500_tickers_data = get_daily_sector_prices(start_date, end_date)
|
||||
weight_data = yf.Ticker(sectors_ticker).info["sectorWeightings"]
|
||||
try:
|
||||
weight_data = yf.Ticker(sectors_ticker).info["sectorWeightings"]
|
||||
except Exception as _: # noqa
|
||||
console.print("[red]Could not get info for S&P 500.")
|
||||
return pd.DataFrame()
|
||||
|
||||
# reformat Data
|
||||
weights: Dict[str, dict] = {"SPY": {}}
|
||||
|
||||
@@ -141,6 +141,7 @@ class PortfolioController(BaseController):
|
||||
self.portfolio_name: str = ""
|
||||
self.benchmark_name: str = ""
|
||||
self.original_benchmark_ticker = ""
|
||||
self.recalculate_alloc = False
|
||||
self.risk_free_rate = 0
|
||||
self.portlist: List[str] = os.listdir(self.DEFAULT_HOLDINGS_PATH)
|
||||
self.portfolio = None
|
||||
@@ -149,6 +150,10 @@ class PortfolioController(BaseController):
|
||||
self.update_choices()
|
||||
choices: dict = self.choices_default
|
||||
self.choices = choices
|
||||
self.choices["bench"] = {
|
||||
"--benchmark": {c: None for c in statics.BENCHMARK_CHOICES},
|
||||
"-b": "--benchmark",
|
||||
}
|
||||
self.completer = NestedCompleter.from_nested_dict(choices)
|
||||
|
||||
def update_choices(self):
|
||||
@@ -416,7 +421,6 @@ class PortfolioController(BaseController):
|
||||
dest="benchmark",
|
||||
required="-h" not in other_args,
|
||||
help="Set the benchmark for the portfolio. By default, this is SPDR S&P 500 ETF Trust (SPY).",
|
||||
choices={c: {} for c in statics.BENCHMARK_CHOICES},
|
||||
metavar="BENCHMARK",
|
||||
)
|
||||
parser.add_argument(
|
||||
@@ -437,9 +441,14 @@ class PortfolioController(BaseController):
|
||||
"[red]Please first load transactions file using 'load'[/red]"
|
||||
)
|
||||
else:
|
||||
self.benchmark_name = statics.BENCHMARK_CHOICES.get(ns_parser.benchmark)
|
||||
self.original_benchmark_ticker = ns_parser.benchmark
|
||||
self.portfolio.set_benchmark(ns_parser.benchmark, ns_parser.full_shares)
|
||||
if self.portfolio.set_benchmark(
|
||||
ns_parser.benchmark, ns_parser.full_shares
|
||||
):
|
||||
self.benchmark_name = statics.BENCHMARK_CHOICES.get(
|
||||
ns_parser.benchmark, ns_parser.benchmark
|
||||
)
|
||||
self.original_benchmark_ticker = ns_parser.benchmark
|
||||
self.recalculate_alloc = True
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def call_alloc(self, other_args: List[str]):
|
||||
@@ -481,27 +490,31 @@ class PortfolioController(BaseController):
|
||||
):
|
||||
if ns_parser.agg == "assets":
|
||||
portfolio_view.display_assets_allocation(
|
||||
self.portfolio,
|
||||
ns_parser.limit,
|
||||
ns_parser.tables,
|
||||
portfolio_engine=self.portfolio,
|
||||
limit=ns_parser.limit,
|
||||
tables=ns_parser.tables,
|
||||
recalculate=self.recalculate_alloc,
|
||||
)
|
||||
elif ns_parser.agg == "sectors":
|
||||
portfolio_view.display_sectors_allocation(
|
||||
self.portfolio,
|
||||
ns_parser.limit,
|
||||
ns_parser.tables,
|
||||
portfolio_engine=self.portfolio,
|
||||
limit=ns_parser.limit,
|
||||
tables=ns_parser.tables,
|
||||
recalculate=self.recalculate_alloc,
|
||||
)
|
||||
elif ns_parser.agg == "countries":
|
||||
portfolio_view.display_countries_allocation(
|
||||
self.portfolio,
|
||||
ns_parser.limit,
|
||||
ns_parser.tables,
|
||||
portfolio_engine=self.portfolio,
|
||||
limit=ns_parser.limit,
|
||||
tables=ns_parser.tables,
|
||||
recalculate=self.recalculate_alloc,
|
||||
)
|
||||
elif ns_parser.agg == "regions":
|
||||
portfolio_view.display_regions_allocation(
|
||||
self.portfolio,
|
||||
ns_parser.limit,
|
||||
ns_parser.tables,
|
||||
portfolio_engine=self.portfolio,
|
||||
limit=ns_parser.limit,
|
||||
tables=ns_parser.tables,
|
||||
recalculate=self.recalculate_alloc,
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
@@ -576,9 +589,13 @@ class PortfolioController(BaseController):
|
||||
bench_result = attribution_model.get_spy_sector_contributions(
|
||||
start_date, end_date
|
||||
)
|
||||
if bench_result.empty:
|
||||
return
|
||||
portfolio_result = attribution_model.get_portfolio_sector_contributions(
|
||||
start_date, self.portfolio.portfolio_trades
|
||||
)
|
||||
if portfolio_result.empty:
|
||||
return
|
||||
|
||||
# relative results - the proportions of return attribution
|
||||
if ns_parser.type == "relative":
|
||||
@@ -951,6 +968,23 @@ class PortfolioController(BaseController):
|
||||
help="Period to select start end of the year returns",
|
||||
metavar="PERIOD",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i",
|
||||
"--instrument",
|
||||
type=str,
|
||||
dest="instrument",
|
||||
default="both",
|
||||
choices=["both", "portfolio", "benchmark"],
|
||||
help="Whether to show portfolio or benchmark monthly returns. By default both are shown in one table.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g",
|
||||
"--graph",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="graph",
|
||||
help="Plot the monthly returns on a heatmap",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--show",
|
||||
@@ -965,7 +999,7 @@ class PortfolioController(BaseController):
|
||||
parser,
|
||||
other_args,
|
||||
raw=True,
|
||||
export_allowed=EXPORT_ONLY_FIGURES_ALLOWED,
|
||||
export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED,
|
||||
)
|
||||
|
||||
if ns_parser and self.portfolio is not None:
|
||||
@@ -975,10 +1009,13 @@ class PortfolioController(BaseController):
|
||||
portfolio_view.display_monthly_returns(
|
||||
self.portfolio,
|
||||
ns_parser.period,
|
||||
ns_parser.raw,
|
||||
ns_parser.instrument,
|
||||
ns_parser.graph,
|
||||
ns_parser.show_vals,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -1020,7 +1057,9 @@ class PortfolioController(BaseController):
|
||||
ns_parser.raw,
|
||||
ns_parser.limit,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -1042,9 +1081,6 @@ class PortfolioController(BaseController):
|
||||
portfolio_view.display_maximum_drawdown(
|
||||
self.portfolio,
|
||||
export=ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -1278,18 +1314,26 @@ class PortfolioController(BaseController):
|
||||
self.portfolio,
|
||||
ns_parser.risk_free_rate / 100,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "sortino":
|
||||
portfolio_view.display_sortino_ratio(
|
||||
self.portfolio,
|
||||
ns_parser.risk_free_rate / 100,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "maxdrawdown":
|
||||
portfolio_view.display_maximum_drawdown_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "rsquare":
|
||||
portfolio_view.display_rsquare(
|
||||
@@ -1301,15 +1345,27 @@ class PortfolioController(BaseController):
|
||||
)
|
||||
elif ns_parser.metric == "gaintopain":
|
||||
portfolio_view.display_gaintopain_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "trackerr":
|
||||
portfolio_view.display_tracking_error(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "information":
|
||||
portfolio_view.display_information_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "tail":
|
||||
portfolio_view.display_tail_ratio(
|
||||
@@ -1321,30 +1377,52 @@ class PortfolioController(BaseController):
|
||||
)
|
||||
elif ns_parser.metric == "commonsense":
|
||||
portfolio_view.display_common_sense_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "jensens":
|
||||
portfolio_view.display_jensens_alpha(
|
||||
self.portfolio,
|
||||
ns_parser.risk_free_rate / 100,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "calmar":
|
||||
portfolio_view.display_calmar_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "kelly":
|
||||
portfolio_view.display_kelly_criterion(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "payoff" and self.portfolio is not None:
|
||||
portfolio_view.display_payoff_ratio(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
elif ns_parser.metric == "profitfactor" and self.portfolio is not None:
|
||||
portfolio_view.display_profit_factor(
|
||||
self.portfolio, ns_parser.export, ns_parser.sheet_name
|
||||
self.portfolio,
|
||||
ns_parser.export,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -1384,7 +1462,9 @@ class PortfolioController(BaseController):
|
||||
ns_parser.period,
|
||||
ns_parser.raw,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -1431,7 +1511,9 @@ class PortfolioController(BaseController):
|
||||
ns_parser.period,
|
||||
ns_parser.risk_free_rate / 100,
|
||||
ns_parser.export,
|
||||
ns_parser.sheet_name,
|
||||
sheet_name=" ".join(ns_parser.sheet_name)
|
||||
if ns_parser.sheet_name
|
||||
else None,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ from openbb_terminal.rich_config import console
|
||||
from openbb_terminal.terminal_helper import suppress_stdout
|
||||
|
||||
# pylint: disable=E1136,W0201,R0902,C0302
|
||||
# pylint: disable=unsupported-assignment-operation,redefined-outer-name,too-many-public-methods, consider-using-f-string
|
||||
# pylint: disable=unsupported-assignment-operation,redefined-outer-name
|
||||
# pylint: too-many-public-methods, consider-using-f-string,disable=raise-missing-from
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -117,7 +118,7 @@ class PortfolioEngine:
|
||||
self.tickers_list = None
|
||||
self.tickers: Dict[Any, Any] = {}
|
||||
self.benchmark_ticker: str = ""
|
||||
self.benchmark_info = None
|
||||
self.benchmark_info: Dict[Any, Any] = {}
|
||||
self.historical_trade_data = pd.DataFrame()
|
||||
|
||||
# Portfolio
|
||||
@@ -226,7 +227,7 @@ class PortfolioEngine:
|
||||
13. Populate fields Sector, Industry and Country
|
||||
"""
|
||||
|
||||
p_bar = tqdm(range(14), desc="Preprocessing transactions")
|
||||
p_bar = tqdm(range(14), desc="Preprocessing transactions", leave=False)
|
||||
|
||||
try:
|
||||
# 0. If optional fields not in the transactions add missing
|
||||
@@ -495,7 +496,7 @@ class PortfolioEngine:
|
||||
] = info_list
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def set_benchmark(self, symbol: str = "SPY", full_shares: bool = False):
|
||||
def set_benchmark(self, symbol: str = "SPY", full_shares: bool = False) -> bool:
|
||||
"""Load benchmark into portfolio.
|
||||
|
||||
Parameters
|
||||
@@ -505,11 +506,14 @@ class PortfolioEngine:
|
||||
full_shares: bool
|
||||
Whether to mimic the portfolio trades exactly (partial shares) or round down the
|
||||
quantity to the nearest number
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
|
||||
p_bar = tqdm(range(4), desc=" Loading benchmark")
|
||||
|
||||
self.benchmark_ticker = symbol
|
||||
p_bar = tqdm(range(4), desc=" Loading benchmark", leave=False)
|
||||
|
||||
self.benchmark_historical_prices = yf.download(
|
||||
symbol,
|
||||
@@ -519,6 +523,15 @@ class PortfolioEngine:
|
||||
ignore_tz=True,
|
||||
)["Adj Close"]
|
||||
|
||||
if self.benchmark_historical_prices.empty:
|
||||
console.print(
|
||||
f"\n[red]Could not download benchmark data for {symbol}."
|
||||
" Choose another symbol.\n[/red]"
|
||||
)
|
||||
return False
|
||||
|
||||
self.benchmark_ticker = symbol
|
||||
|
||||
p_bar.n += 1
|
||||
p_bar.refresh()
|
||||
|
||||
@@ -538,11 +551,6 @@ class PortfolioEngine:
|
||||
p_bar.refresh()
|
||||
|
||||
self.benchmark_returns = self.benchmark_historical_prices.pct_change().dropna()
|
||||
self.benchmark_info = yf.Ticker(symbol).info
|
||||
|
||||
p_bar.n += 1
|
||||
p_bar.refresh()
|
||||
|
||||
(
|
||||
self.portfolio_returns,
|
||||
self.benchmark_returns,
|
||||
@@ -551,6 +559,21 @@ class PortfolioEngine:
|
||||
p_bar.n += 1
|
||||
p_bar.refresh()
|
||||
|
||||
try:
|
||||
self.benchmark_info = dict(yf.Ticker(symbol).info)
|
||||
self.benchmark_info["symbol"] = symbol
|
||||
except Exception as _: # noqa
|
||||
console.print(
|
||||
f"[red]\n\nCould not get info for {symbol}."
|
||||
" This affects 'alloc' command.[/red]\n"
|
||||
)
|
||||
return False
|
||||
|
||||
p_bar.n += 1
|
||||
p_bar.refresh()
|
||||
|
||||
return True
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def __mimic_trades_for_benchmark(self, full_shares: bool = False):
|
||||
"""Mimic trades from the transactions based on chosen benchmark assuming partial shares
|
||||
@@ -650,7 +673,9 @@ class PortfolioEngine:
|
||||
whether to use close or adjusted close prices
|
||||
"""
|
||||
|
||||
p_bar = tqdm(range(len(self.tickers)), desc=" Loading price data")
|
||||
p_bar = tqdm(
|
||||
range(len(self.tickers)), desc=" Loading price data", leave=False
|
||||
)
|
||||
|
||||
for ticker_type, data in self.tickers.items():
|
||||
price_data = yf.download(
|
||||
@@ -781,7 +806,7 @@ class PortfolioEngine:
|
||||
from any sales during the period [Cash Inflow(t)].
|
||||
"""
|
||||
|
||||
p_bar = tqdm(range(1), desc=" Calculating returns")
|
||||
p_bar = tqdm(range(1), desc=" Calculating returns", leave=False)
|
||||
|
||||
trade_data = self.historical_trade_data
|
||||
|
||||
@@ -906,6 +931,12 @@ class PortfolioEngine:
|
||||
Flag to force recalculate allocation if already exists
|
||||
"""
|
||||
|
||||
if not self.benchmark_info:
|
||||
return
|
||||
|
||||
if self.portfolio_trades.empty:
|
||||
return
|
||||
|
||||
if category == "Asset":
|
||||
if (
|
||||
self.benchmark_assets_allocation.empty
|
||||
|
||||
@@ -58,8 +58,8 @@ def generate_portfolio(
|
||||
transactions = PortfolioEngine.read_transactions(transactions_file_path)
|
||||
portfolio_engine = PortfolioEngine(transactions)
|
||||
portfolio_engine.generate_portfolio_data()
|
||||
portfolio_engine.set_benchmark(symbol=benchmark_symbol, full_shares=full_shares)
|
||||
portfolio_engine.set_risk_free_rate(risk_free_rate)
|
||||
portfolio_engine.set_benchmark(symbol=benchmark_symbol, full_shares=full_shares)
|
||||
|
||||
return portfolio_engine
|
||||
|
||||
@@ -92,7 +92,7 @@ def get_transactions(portfolio_engine: PortfolioEngine) -> pd.DataFrame:
|
||||
@log_start_end(log=logger)
|
||||
def set_benchmark(
|
||||
portfolio_engine: PortfolioEngine, symbol: str, full_shares: bool = False
|
||||
):
|
||||
) -> bool:
|
||||
"""Load benchmark into portfolio
|
||||
|
||||
Parameters
|
||||
@@ -106,6 +106,11 @@ def set_benchmark(
|
||||
Whether to mimic the portfolio trades exactly (partial shares) or round down the
|
||||
quantity to the nearest number
|
||||
|
||||
Returns
|
||||
-------
|
||||
bool
|
||||
True if successful, False otherwise
|
||||
|
||||
Examples
|
||||
--------
|
||||
>>> from openbb_terminal.sdk import openbb
|
||||
@@ -113,7 +118,7 @@ def set_benchmark(
|
||||
>>> output = openbb.portfolio.bench(p, symbol="SPY")
|
||||
"""
|
||||
|
||||
portfolio_engine.set_benchmark(symbol=symbol, full_shares=full_shares)
|
||||
return portfolio_engine.set_benchmark(symbol=symbol, full_shares=full_shares)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -366,7 +371,36 @@ def get_monthly_returns(
|
||||
],
|
||||
)
|
||||
|
||||
return monthly_returns, bench_monthly_returns
|
||||
years = [
|
||||
(year, instrument)
|
||||
for year in monthly_returns.index
|
||||
for instrument in ["Portfolio", "Benchmark", "Alpha"]
|
||||
]
|
||||
total_monthly_returns = pd.DataFrame(
|
||||
np.nan,
|
||||
columns=monthly_returns.columns,
|
||||
index=pd.MultiIndex.from_tuples(years, names=["Year", "Instrument"]),
|
||||
)
|
||||
|
||||
for year in monthly_returns.index:
|
||||
for instrument in ["Portfolio", "Benchmark", "Alpha"]:
|
||||
if instrument == "Portfolio":
|
||||
total_monthly_returns.loc[
|
||||
(year, instrument), monthly_returns.columns
|
||||
] = monthly_returns.loc[year].values
|
||||
elif instrument == "Benchmark":
|
||||
total_monthly_returns.loc[
|
||||
(year, instrument), monthly_returns.columns
|
||||
] = bench_monthly_returns.loc[year].values
|
||||
elif instrument == "Alpha":
|
||||
total_monthly_returns.loc[
|
||||
(year, instrument), monthly_returns.columns
|
||||
] = (
|
||||
monthly_returns.loc[year].values
|
||||
- bench_monthly_returns.loc[year].values
|
||||
)
|
||||
|
||||
return monthly_returns, bench_monthly_returns, total_monthly_returns
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -430,6 +464,13 @@ def join_allocation(
|
||||
pd.DataFrame
|
||||
DataFrame with portfolio and benchmark allocations
|
||||
"""
|
||||
|
||||
if portfolio.empty:
|
||||
portfolio = pd.DataFrame(columns=[column, "Portfolio"])
|
||||
|
||||
if benchmark.empty:
|
||||
benchmark = pd.DataFrame(columns=[column, "Benchmark"])
|
||||
|
||||
combined = pd.merge(portfolio, benchmark, on=column, how="left")
|
||||
combined["Difference"] = combined["Portfolio"] - combined["Benchmark"]
|
||||
combined = combined.replace(np.nan, "-")
|
||||
|
||||
@@ -145,6 +145,7 @@ def display_assets_allocation(
|
||||
portfolio_engine: PortfolioEngine,
|
||||
limit: int = 10,
|
||||
tables: bool = False,
|
||||
recalculate: bool = False,
|
||||
):
|
||||
"""Display portfolio asset allocation compared to the benchmark
|
||||
|
||||
@@ -152,16 +153,19 @@ def display_assets_allocation(
|
||||
----------
|
||||
portfolio_engine: PortfolioEngine
|
||||
Instance of PortfolioEngine class
|
||||
tables: bool
|
||||
Whether to include separate asset allocation tables
|
||||
limit: int
|
||||
The amount of assets you wish to show, by default this is set to 10
|
||||
tables: bool
|
||||
Whether to include separate asset allocation tables
|
||||
recalculate: bool
|
||||
Whether to recalculate the allocation
|
||||
"""
|
||||
|
||||
combined, portfolio_allocation, benchmark_allocation = get_assets_allocation(
|
||||
portfolio_engine=portfolio_engine,
|
||||
limit=limit,
|
||||
tables=True,
|
||||
recalculate=recalculate,
|
||||
)
|
||||
|
||||
display_category(
|
||||
@@ -179,6 +183,7 @@ def display_sectors_allocation(
|
||||
portfolio_engine: PortfolioEngine,
|
||||
limit: int = 10,
|
||||
tables: bool = False,
|
||||
recalculate: bool = False,
|
||||
):
|
||||
"""Display portfolio sector allocation compared to the benchmark
|
||||
|
||||
@@ -190,12 +195,15 @@ def display_sectors_allocation(
|
||||
The amount of assets you wish to show, by default this is set to 10
|
||||
tables: bool
|
||||
Whether to include separate asset allocation tables
|
||||
recalculate: bool
|
||||
Whether to recalculate the allocation
|
||||
"""
|
||||
|
||||
combined, portfolio_allocation, benchmark_allocation = get_sectors_allocation(
|
||||
portfolio_engine=portfolio_engine,
|
||||
limit=limit,
|
||||
tables=True,
|
||||
recalculate=recalculate,
|
||||
)
|
||||
|
||||
display_category(
|
||||
@@ -213,6 +221,7 @@ def display_countries_allocation(
|
||||
portfolio_engine: PortfolioEngine,
|
||||
limit: int = 10,
|
||||
tables: bool = False,
|
||||
recalculate: bool = False,
|
||||
):
|
||||
"""Display portfolio country allocation compared to the benchmark
|
||||
|
||||
@@ -224,12 +233,15 @@ def display_countries_allocation(
|
||||
The amount of assets you wish to show, by default this is set to 10
|
||||
tables: bool
|
||||
Whether to include separate asset allocation tables
|
||||
recalculate: bool
|
||||
Whether to recalculate the allocation
|
||||
"""
|
||||
|
||||
combined, portfolio_allocation, benchmark_allocation = get_countries_allocation(
|
||||
portfolio_engine=portfolio_engine,
|
||||
limit=limit,
|
||||
tables=True,
|
||||
recalculate=recalculate,
|
||||
)
|
||||
|
||||
display_category(
|
||||
@@ -247,6 +259,7 @@ def display_regions_allocation(
|
||||
portfolio_engine: PortfolioEngine,
|
||||
limit: int = 10,
|
||||
tables: bool = False,
|
||||
recalculate: bool = False,
|
||||
):
|
||||
"""Display portfolio region allocation compared to the benchmark
|
||||
|
||||
@@ -258,12 +271,15 @@ def display_regions_allocation(
|
||||
The amount of assets you wish to show, by default this is set to 10
|
||||
tables: bool
|
||||
Whether to include separate asset allocation tables
|
||||
recalculate: bool
|
||||
Whether to recalculate the allocation
|
||||
"""
|
||||
|
||||
combined, portfolio_allocation, benchmark_allocation = get_regions_allocation(
|
||||
portfolio_engine=portfolio_engine,
|
||||
limit=limit,
|
||||
tables=True,
|
||||
recalculate=recalculate,
|
||||
)
|
||||
|
||||
display_category(
|
||||
@@ -459,7 +475,8 @@ def display_yearly_returns(
|
||||
def display_monthly_returns(
|
||||
portfolio_engine: PortfolioEngine,
|
||||
window: str = "all",
|
||||
raw: bool = False,
|
||||
instrument: str = "both",
|
||||
graph: bool = False,
|
||||
show_vals: bool = False,
|
||||
export: str = "",
|
||||
sheet_name: str = None,
|
||||
@@ -474,27 +491,38 @@ def display_monthly_returns(
|
||||
Use `portfolio.load` to create a PortfolioEngine.
|
||||
window : str
|
||||
interval to compare cumulative returns and benchmark
|
||||
raw : False
|
||||
Display raw data from cumulative return
|
||||
instrument : str
|
||||
Display raw data from cumulative return, default is showing both the portfolio and benchmark in one table
|
||||
show_vals : False
|
||||
Show values on heatmap
|
||||
export : str
|
||||
Export certain type of data
|
||||
sheet_name : str
|
||||
Whether to export to a specific sheet name in an Excel file
|
||||
external_axes: plt.Axes
|
||||
Optional axes to display plot on
|
||||
"""
|
||||
|
||||
portfolio_returns, benchmark_returns = get_monthly_returns(portfolio_engine, window)
|
||||
portfolio_returns, benchmark_returns, total_monthly_returns = get_monthly_returns(
|
||||
portfolio_engine, window
|
||||
)
|
||||
|
||||
if raw:
|
||||
if instrument == "portfolio":
|
||||
print_rich_table(
|
||||
portfolio_returns,
|
||||
title="Monthly returns - portfolio [%]",
|
||||
headers=portfolio_returns.columns,
|
||||
show_index=True,
|
||||
)
|
||||
console.print("\n")
|
||||
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"mret_portfolio",
|
||||
portfolio_returns,
|
||||
sheet_name,
|
||||
)
|
||||
elif instrument == "benchmark":
|
||||
print_rich_table(
|
||||
benchmark_returns,
|
||||
title="Monthly returns - benchmark [%]",
|
||||
@@ -502,7 +530,49 @@ def display_monthly_returns(
|
||||
show_index=True,
|
||||
)
|
||||
|
||||
else:
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"mret_benchmark",
|
||||
benchmark_returns,
|
||||
sheet_name,
|
||||
)
|
||||
elif instrument == "both":
|
||||
# Converts multi-index into readable rich table while keeping the multi-index intact for export
|
||||
total_monthly_returns_rich = total_monthly_returns.copy()
|
||||
total_monthly_returns_rich.insert(
|
||||
0, "Instruments", total_monthly_returns_rich.index.get_level_values(1)
|
||||
)
|
||||
total_monthly_returns_rich.insert(
|
||||
0,
|
||||
"Year",
|
||||
sum(
|
||||
[
|
||||
[year, "", ""]
|
||||
for year in total_monthly_returns_rich.index.get_level_values(
|
||||
0
|
||||
).unique()
|
||||
],
|
||||
[],
|
||||
),
|
||||
)
|
||||
|
||||
print_rich_table(
|
||||
total_monthly_returns_rich,
|
||||
title="Total Monthly returns [%]",
|
||||
headers=total_monthly_returns_rich.columns,
|
||||
show_index=False,
|
||||
)
|
||||
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"mret_total",
|
||||
total_monthly_returns,
|
||||
sheet_name,
|
||||
)
|
||||
|
||||
if graph or show_vals:
|
||||
if external_axes is None:
|
||||
_, ax = plt.subplots(
|
||||
2,
|
||||
@@ -548,14 +618,6 @@ def display_monthly_returns(
|
||||
if external_axes is None:
|
||||
theme.visualize_output()
|
||||
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"mret",
|
||||
portfolio_returns,
|
||||
sheet_name,
|
||||
)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_daily_returns(
|
||||
|
||||
Reference in New Issue
Block a user