Files
OpenBB/openbb_terminal/portfolio/portfolio_controller.py
montezdesousa 475ebb5acd [WIP] Migrate user, feature flags and configurations to dataclass (#4309)
* implement user and change is guest

* replace User methods

* move remaining functions to user

* fix bug

* portfolio test

* fix tests

* fix tests

* uncomment skip

* ruff

* start credentials model implementation

* change deault structure

* refacto quandl

* change default struct and file names

* bug

* start using global user

* add comments

* move update flair

* change cfg references in keys model

* reddit & fmp

* refactore additional keys

* remaing config keys

* remove unsude imports and fix hub keys

* ruff

* fix line too long

* unblock unittests

* fix keys model tests

* remove os patch

* fix account controller tests

* fix account controller tests

* fix account model

* account fixed

* fix user tests

* User Model

* User Model

* User Model

* Move session folder

* User Model : grouping the models

* User Model : fix bugs

* Config Plot + bug fix

* User Model : Refactoring plots

* Feature flags

* Remove dup

* clean comments a fix some vars

* fix feature flags controller

* refactor obbff from settings controller

* refactor a bunch of obbff variables

* fix bug blocking terminal

* fix integration tests

* refactor controllers with obbff

* refactor remaining ffs

* remove comments

* log not started

* mispelled variable

* set current user on local model

* User Model : moving files + updating

* User Preferences

* User : Refactor is_guest function + bug fix

* User : refactor is_guest function

* Fix conftest.py

* remove lost obbffs

* typo on fileoverwrite

* Plot config: move variables and start plot dpi

* User : tests

* refactor plot dpi references

* remove plot dpi imports

* refactor some config terminal leftover

* /account/sync : fix bug

* remove configs reload

* start path refactor

* installation paths vs user paths

* remove cfg_plot

* Tests : User Refactoring

* user prefix only for preferences

* user prefix only for preferences

* move user paths

* remove comment

* update apply remote configs

* fix tz not updating

* save some copys

* small fix

* create interface for credentials and preferences

* wrong docstrings fixed

* ruff core

* fix some pref setting

* login is better

* ruff

* ruff

* remove Literal

* add small comment

* Tests : User Refactoring

* Credentials Model : add DataBento

* Fix helper_funcs

* Fixing code

* Tests : fixing

* User Model : freezing the models

* Fixed Income + Databento

* Tests : fixing keys

* black

* ruff

* Tests : user model

* Tests : user model

* Fix linting

* Fix linting

* Linting

* Linting

* Fixing code and tests

* Fix tests

* mypy

* SDK

* Clean ruff cache

* Fix syntax issue

* Update fred_view

* Black

* Linting

* Update code

* Update tests

* Linting

* Linting

* Tests

* Black

* Linting

* Linting

* Mock os.listdir to avoid looking for file that doesnt exist

* Mock options paths

* lint

* Tests : fix

* Code : update

* Tests + Linting

* Tests

* Ruff

* Code : update

* Tests

* Code : update

* Fix workflow

* Code : update

---------

Co-authored-by: Chavithra PARANA <chavithra@gmail.com>
Co-authored-by: luqazino@gmail.com <luqazino@gmail.com>
Co-authored-by: James Maslek <jmaslek11@gmail.com>
2023-03-05 00:37:19 +01:00

1564 lines
54 KiB
Python

"""Portfolio Controller"""
__docformat__ = "numpy"
import argparse
import logging
import os
from datetime import date
from typing import List, Optional
import pandas as pd
from openbb_terminal.common.quantitative_analysis import qa_view
from openbb_terminal.core.config.paths import MISCELLANEOUS_DIRECTORY
from openbb_terminal.core.session.current_user import get_current_user
from openbb_terminal.custom_prompt_toolkit import NestedCompleter
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import (
EXPORT_BOTH_RAW_DATA_AND_FIGURES,
EXPORT_ONLY_FIGURES_ALLOWED,
EXPORT_ONLY_RAW_DATA_ALLOWED,
)
from openbb_terminal.menu import session
from openbb_terminal.parent_classes import BaseController
from openbb_terminal.portfolio import (
attribution_model,
portfolio_helper,
portfolio_view,
statics,
)
from openbb_terminal.portfolio.portfolio_model import generate_portfolio
from openbb_terminal.rich_config import MenuText, console
try:
from openbb_terminal.portfolio.portfolio_optimization import po_controller
OPTIMIZATION_TOOLKIT_ENABLED = True
except ModuleNotFoundError:
OPTIMIZATION_TOOLKIT_ENABLED = False
console.print(
"[yellow]"
"Portfolio Optimization Toolkit is disabled. "
"To use the Optimization features please install the toolkit following the "
"instructions here: https://github.com/OpenBB-finance/OpenBBTerminal/"
"blob/main/openbb_terminal/README.md#anaconda--python"
"\n"
"[/yellow]"
)
# pylint: disable=R1710,E1101,C0415,W0212,too-many-function-args,C0302,too-many-instance-attributes
logger = logging.getLogger(__name__)
class PortfolioController(BaseController):
"""Portfolio Controller class"""
CHOICES_COMMANDS = [
"load",
"show",
"bench",
"alloc",
"attrib",
"perf",
"yret",
"mret",
"dret",
"distr",
"holdv",
"holdp",
"maxdd",
"var",
"es",
"om",
"rvol",
"rsharpe",
"rsort",
"rbeta",
"metric",
"summary",
]
CHOICES_MENUS = [
"bro",
"po",
]
VALID_DISTRIBUTIONS = ["laplace", "student_t", "logistic", "normal"]
AGGREGATION_METRICS = ["assets", "sectors", "countries", "regions"]
VALID_METRICS = [
"volatility",
"sharpe",
"sortino",
"maxdrawdown",
"rsquare",
"skew",
"kurtosis",
"gaintopain",
"trackerr",
"information",
"tail",
"commonsense",
"jensens",
"calmar",
"kelly",
"payoff",
"profitfactor",
]
PERIODS = ["3y", "5y", "10y", "all"]
PATH = "/portfolio/"
CHOICES_GENERATION = True
def __init__(self, queue: Optional[List[str]] = None):
"""Constructor"""
super().__init__(queue)
self.file_types = ["xlsx", "csv"]
self.DEFAULT_HOLDINGS_PATH = portfolio_helper.DEFAULT_HOLDINGS_PATH
self.DATA_HOLDINGS_FILES = {
filepath.name: filepath
for file_type in self.file_types
for filepath in self.DEFAULT_HOLDINGS_PATH.rglob(f"*.{file_type}")
}
self.portfolio_df = pd.DataFrame(
columns=[
"Date",
"Name",
"Type",
"Sector",
"Industry",
"Country",
"Price",
"Quantity",
"Fees",
"Premium",
"Investment",
"Side",
"Currency",
]
)
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
if session and get_current_user().preferences.USE_PROMPT_TOOLKIT:
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):
self.DEFAULT_HOLDINGS_PATH = portfolio_helper.DEFAULT_HOLDINGS_PATH
self.DATA_HOLDINGS_FILES.update(
{
filepath.name: filepath
for file_type in self.file_types
for filepath in self.DEFAULT_HOLDINGS_PATH.rglob(f"*.{file_type}")
}
)
def print_help(self):
"""Print help"""
mt = MenuText("portfolio/")
mt.add_menu("bro")
mt.add_menu("po")
mt.add_raw("\n")
mt.add_cmd("load")
mt.add_cmd("show")
mt.add_cmd("bench")
mt.add_raw("\n")
mt.add_param("_loaded", self.portfolio_name)
mt.add_param("_riskfreerate", f"{self.risk_free_rate}%")
mt.add_param("_benchmark", self.benchmark_name)
mt.add_raw("\n")
mt.add_info("_graphs_")
mt.add_cmd("holdv", self.portfolio_name and self.benchmark_name)
mt.add_cmd("holdp", self.portfolio_name and self.benchmark_name)
mt.add_cmd("yret", self.portfolio_name and self.benchmark_name)
mt.add_cmd("mret", self.portfolio_name and self.benchmark_name)
mt.add_cmd("dret", self.portfolio_name and self.benchmark_name)
mt.add_cmd("distr", self.portfolio_name and self.benchmark_name)
mt.add_cmd("maxdd", self.portfolio_name and self.benchmark_name)
mt.add_cmd("rvol", self.portfolio_name and self.benchmark_name)
mt.add_cmd("rsharpe", self.portfolio_name and self.benchmark_name)
mt.add_cmd("rsort", self.portfolio_name and self.benchmark_name)
mt.add_cmd("rbeta", self.portfolio_name and self.benchmark_name)
mt.add_info("_metrics_")
mt.add_cmd("summary", self.portfolio_name and self.benchmark_name)
mt.add_cmd("alloc", self.portfolio_name and self.benchmark_name)
mt.add_cmd("attrib", self.portfolio_name and self.benchmark_name)
mt.add_cmd("metric", self.portfolio_name and self.benchmark_name)
mt.add_cmd("perf", self.portfolio_name and self.benchmark_name)
mt.add_info("_risk_")
mt.add_cmd("var", self.portfolio_name and self.benchmark_name)
mt.add_cmd("es", self.portfolio_name and self.benchmark_name)
mt.add_cmd("om", self.portfolio_name and self.benchmark_name)
console.print(text=mt.menu_text, menu="Portfolio - Portfolio Optimization")
self.update_choices()
def custom_reset(self):
"""Class specific component of reset command"""
objects_to_reload = ["portfolio"]
if self.portfolio_name:
objects_to_reload.append(f"load {self.portfolio_name}")
if self.original_benchmark_ticker:
objects_to_reload.append(f'bench "{self.original_benchmark_ticker}"')
return objects_to_reload
@log_start_end(log=logger)
def call_bro(self, _):
"""Process bro command"""
from openbb_terminal.portfolio.brokers.bro_controller import BrokersController
self.queue = self.load_class(BrokersController, self.queue)
@log_start_end(log=logger)
def call_po(self, _):
"""Process po command"""
if OPTIMIZATION_TOOLKIT_ENABLED:
if self.portfolio is None:
tickers = []
categories = None
else:
tickers = self.portfolio.tickers_list
transactions = self.portfolio.get_transactions()
categories = {
"ASSET_CLASS": {},
"SECTOR": {},
"INDUSTRY": {},
"COUNTRY": {},
"CURRENT_INVESTED_AMOUNT": {},
"CURRENCY": {},
}
for _, transaction in transactions.iterrows():
if transaction["Ticker"] not in categories["ASSET_CLASS"]:
categories["ASSET_CLASS"][transaction["Ticker"]] = transaction[
"Type"
]
categories["SECTOR"][transaction["Ticker"]] = transaction[
"Sector"
]
categories["INDUSTRY"][transaction["Ticker"]] = transaction[
"Industry"
]
categories["COUNTRY"][transaction["Ticker"]] = transaction[
"Country"
]
categories["CURRENT_INVESTED_AMOUNT"][
transaction["Ticker"]
] = transactions[
transactions["Ticker"] == transaction["Ticker"]
][
"Investment"
].sum()
categories["CURRENCY"][transaction["Ticker"]] = transaction[
"Currency"
]
self.queue = self.load_class(
po_controller.PortfolioOptimizationController,
tickers,
None,
categories,
self.queue,
)
else:
console.print("[yellow]Portfolio Optimization Toolkit is disabled[/yellow]")
@log_start_end(log=logger)
def call_load(self, other_args: List[str]):
"""Process load command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="load",
description="Load your portfolio transactions.",
)
parser.add_argument(
"-f",
"--file",
type=str,
dest="file",
help="The file to be loaded",
choices={c: {} for c in self.DATA_HOLDINGS_FILES},
metavar="FILE",
)
parser.add_argument(
"-n",
"--name",
type=str,
dest="name",
help="The name that you wish to give to your portfolio",
)
parser.add_argument(
"-r",
"--rfr",
type=float,
default=0,
dest="risk_free_rate",
help="Set the risk free rate.",
)
parser.add_argument(
"-e",
"--example",
help="Run an example holdings file to understand how the portfolio menu can be used.",
dest="example",
action="store_true",
default=False,
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-f")
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser and ns_parser.file or ns_parser and ns_parser.example:
if ns_parser.example:
file_location = (
MISCELLANEOUS_DIRECTORY / "portfolio" / "holdings_example.xlsx"
)
console.print(
"[green]Loading an example, please type `about` "
"to learn how to create your own Portfolio Excel sheet.[/green]\n"
)
elif ns_parser.file in self.DATA_HOLDINGS_FILES:
file_location = self.DATA_HOLDINGS_FILES[ns_parser.file]
else:
file_location = ns_parser.file # type: ignore
self.portfolio = generate_portfolio(
transactions_file_path=str(file_location),
benchmark_symbol="SPY",
risk_free_rate=ns_parser.risk_free_rate / 100,
)
if ns_parser.name:
self.portfolio_name = ns_parser.name
elif ns_parser.example:
self.portfolio_name = "OpenBB Example Portfolio"
else:
self.portfolio_name = ns_parser.file
console.print(
f"\n[bold][param]Portfolio:[/param][/bold] {self.portfolio_name}"
)
self.risk_free_rate = ns_parser.risk_free_rate
console.print(
f"[bold][param]Risk Free Rate:[/param][/bold] {self.risk_free_rate}%"
)
self.benchmark_name = "SPDR S&P 500 ETF Trust (SPY)"
console.print(
f"[bold][param]Benchmark:[/param][/bold] {self.benchmark_name}"
)
@log_start_end(log=logger)
def call_show(self, other_args: List[str]):
"""Process show command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="show",
description="Show transactions table",
)
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
limit=10,
)
if ns_parser and self.portfolio is not None:
portfolio_view.display_transactions(
self.portfolio,
show_index=False,
limit=ns_parser.limit,
export=ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_bench(self, other_args: List[str]):
"""Process bench command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="bench",
description="Load in a benchmark from a selected list or set your own based on the ticker.",
)
parser.add_argument(
"-b",
"--benchmark",
type=str,
default="SPY",
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).",
metavar="BENCHMARK",
)
parser.add_argument(
"-s",
"--full_shares",
action="store_true",
default=False,
dest="full_shares",
help="Whether to only make a trade with the benchmark when a full share can be bought (no partial shares).",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-b")
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
if self.portfolio is None:
console.print(
"[red]Please first load transactions file using 'load'[/red]"
)
else:
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]):
"""Process alloc command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="alloc",
description="""
Show your allocation to each asset or sector compared to the benchmark.
""",
)
parser.add_argument(
"-a",
"--agg",
default="assets",
choices=self.AGGREGATION_METRICS,
dest="agg",
help="The type of allocation aggregation you wish to do",
metavar="AGG",
)
parser.add_argument(
"-t",
"--tables",
action="store_true",
default=False,
dest="tables",
help="Whether to also include the assets/sectors tables of both the benchmark and the portfolio.",
)
if other_args and other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-a")
ns_parser = self.parse_known_args_and_warn(parser, other_args, limit=10)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
if ns_parser.agg == "assets":
portfolio_view.display_assets_allocation(
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(
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(
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(
portfolio_engine=self.portfolio,
limit=ns_parser.limit,
tables=ns_parser.tables,
recalculate=self.recalculate_alloc,
)
else:
console.print(
f"{ns_parser.agg} is not an available option. The options "
f"are: {', '.join(self.AGGREGATION_METRICS)}"
)
@log_start_end(log=logger)
def call_attrib(self, other_args: List[str]):
"""Process attrib command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="attrib",
description="""
Displays sector attribution of the portfolio compared to the S&P 500
""",
)
parser.add_argument(
"-p",
"--period",
type=str,
choices=statics.PERIODS,
dest="period",
default="all",
help="Period in which to calculate attribution",
)
parser.add_argument(
"-t",
"--type",
type=str,
choices=["relative", "absolute"],
dest="type",
default="relative",
help="Select between relative or absolute attribution values",
)
parser.add_argument(
"--raw",
type=bool,
dest="raw",
default=False,
const=True,
nargs="?",
help="View raw attribution values in a table",
)
if other_args and other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-a")
ns_parser = self.parse_known_args_and_warn(parser, other_args, limit=10)
if ns_parser and self.portfolio is not None:
if check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
):
if self.benchmark_name != "SPDR S&P 500 ETF Trust (SPY)":
print(
"This feature uses S&P 500 as benchmark and will disregard selected benchmark if different."
)
# sector contribution
end_date = date.today()
# set correct time period
if ns_parser.period == "all":
start_date = self.portfolio.inception_date
else:
start_date = portfolio_helper.get_start_date_from_period(
ns_parser.period
)
# calculate benchmark and portfolio contribution values
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":
categorization_result = (
attribution_model.percentage_attrib_categorizer(
bench_result, portfolio_result
)
)
portfolio_view.display_attribution_categorization(
display=categorization_result,
time_period=ns_parser.period,
attrib_type="Contributions as % of PF",
plot_fields=["S&P500 [%]", "Portfolio [%]"],
show_table=ns_parser.raw,
)
# absolute - the raw values of return attribution
if ns_parser.type == "absolute":
categorization_result = attribution_model.raw_attrib_categorizer(
bench_result, portfolio_result
)
portfolio_view.display_attribution_categorization(
display=categorization_result,
time_period=ns_parser.period,
attrib_type="Raw contributions (Return x PF Weight)",
plot_fields=["S&P500", "Portfolio"],
show_table=ns_parser.raw,
)
console.print()
@log_start_end(log=logger)
def call_perf(self, other_args: List[str]):
"""Process performance command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="performance",
description="""
Shows performance of each trade and total performance of the portfolio versus the benchmark.
""",
)
parser.add_argument(
"-t",
"--show_trades",
action="store_true",
default=False,
dest="show_trades",
help="Whether to show performance on all trades in comparison to the benchmark.",
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_performance_vs_benchmark(
self.portfolio,
ns_parser.show_trades,
)
@log_start_end(log=logger)
def call_holdv(self, other_args: List[str]):
"""Process holdv command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="holdv",
description="Display holdings of assets (absolute value)",
)
parser.add_argument(
"-u",
"--unstack",
action="store_true",
default=False,
dest="unstack",
help="Sum all assets value over time",
)
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
raw=True,
limit=10,
)
if ns_parser and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
):
portfolio_view.display_holdings_value(
self.portfolio,
ns_parser.unstack,
ns_parser.raw,
ns_parser.limit,
ns_parser.export,
ns_parser.sheet_name,
)
@log_start_end(log=logger)
def call_holdp(self, other_args: List[str]):
"""Process holdp command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="holdp",
description="Display holdings of assets (in percentage)",
)
parser.add_argument(
"-u",
"--unstack",
action="store_true",
default=False,
dest="unstack",
help="Sum all assets percentage over time",
)
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
raw=True,
limit=10,
)
if ns_parser and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
):
portfolio_view.display_holdings_percentage(
self.portfolio,
ns_parser.unstack,
ns_parser.raw,
ns_parser.limit,
ns_parser.export,
ns_parser.sheet_name,
)
@log_start_end(log=logger)
def call_var(self, other_args: List[str]):
"""Process var command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="var",
description="""
Provides value at risk (short: VaR) of the selected portfolio.
""",
)
parser.add_argument(
"-m",
"--mean",
action="store_true",
default=True,
dest="use_mean",
help="If one should use the mean of the portfolio return",
)
parser.add_argument(
"-a",
"--adjusted",
action="store_true",
default=False,
dest="adjusted",
help="""
If the VaR should be adjusted for skew and kurtosis (Cornish-Fisher-Expansion)
""",
)
parser.add_argument(
"-s",
"--student",
action="store_true",
default=False,
dest="student_t",
help="""
If one should use the student-t distribution
""",
)
parser.add_argument(
"-p",
"--percentile",
action="store",
dest="percentile",
type=float,
default=99.9,
help="""
Percentile used for VaR calculations, for example input 99.9 equals a 99.9 Percent VaR
""",
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser and self.portfolio is not None:
if self.portfolio_name:
if ns_parser.adjusted and ns_parser.student_t:
console.print(
"Select either the adjusted or the student_t parameter.\n"
)
else:
portfolio_view.display_var(
portfolio_engine=self.portfolio,
use_mean=ns_parser.use_mean,
adjusted_var=ns_parser.adjusted,
student_t=ns_parser.student_t,
percentile=ns_parser.percentile,
)
else:
console.print(
"[red]Please first define the portfolio using 'load'[/red]\n"
)
@log_start_end(log=logger)
def call_es(self, other_args: List[str]):
"""Process es command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="es",
description="""
Provides Expected Shortfall (short: ES) of the selected portfolio.
""",
)
parser.add_argument(
"-m",
"--mean",
action="store_true",
default=True,
dest="use_mean",
help="If one should use the mean of the portfolios return",
)
parser.add_argument(
"-d",
"--dist",
dest="distribution",
type=str,
choices=self.VALID_DISTRIBUTIONS,
default="normal",
help="Distribution used for the calculations",
metavar="DIST",
)
parser.add_argument(
"-p",
"--percentile",
action="store",
dest="percentile",
type=float,
default=99.9,
help="""
Percentile used for ES calculations, for example input 99.9 equals a 99.9 Percent Expected Shortfall
""",
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
if self.portfolio and self.portfolio_name:
portfolio_view.display_es(
portfolio_engine=self.portfolio,
use_mean=ns_parser.use_mean,
distribution=ns_parser.distribution,
percentile=ns_parser.percentile,
)
else:
console.print(
"[red]Please first define the portfolio using 'load'[/red]\n"
)
@log_start_end(log=logger)
def call_om(self, other_args: List[str]):
"""Process om command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="om",
description="""
Provides omega ratio of the selected portfolio.
""",
)
parser.add_argument(
"-s",
"--start",
action="store",
dest="start",
type=float,
default=0,
help="""
Start of the omega ratio threshold
""",
)
parser.add_argument(
"-e",
"--end",
action="store",
dest="end",
type=float,
default=1.5,
help="""
End of the omega ratio threshold
""",
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser and self.portfolio is not None:
if self.portfolio_name:
data = self.portfolio.portfolio_returns[1:]
qa_view.display_omega(
data,
ns_parser.start,
ns_parser.end,
)
else:
console.print(
"[red]Please define the portfolio first (via 'load')[/red]\n"
)
@log_start_end(log=logger)
def call_yret(self, other_args: List[str]):
"""Process yret command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="yret",
description="End of the year returns",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="all",
choices=self.PERIODS,
help="Period to select start end of the year returns",
metavar="PERIOD",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
raw=True,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_yearly_returns(
self.portfolio,
ns_parser.period,
ns_parser.raw,
ns_parser.export,
ns_parser.sheet_name,
)
@log_start_end(log=logger)
def call_mret(self, other_args: List[str]):
"""Process mret command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="mret",
description="Monthly returns",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="all",
choices=self.PERIODS,
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",
action="store_true",
default=False,
dest="show_vals",
help="Show monthly returns on heatmap",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
raw=True,
export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED,
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_monthly_returns(
self.portfolio,
ns_parser.period,
ns_parser.instrument,
ns_parser.graph,
ns_parser.show_vals,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_dret(self, other_args: List[str]):
"""Process dret command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="dret",
description="Daily returns",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="all",
choices=self.PERIODS,
help="Period to select start end of the year returns",
metavar="PERIOD",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
raw=True,
limit=10,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_daily_returns(
self.portfolio,
ns_parser.period,
ns_parser.raw,
ns_parser.limit,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_maxdd(self, other_args: List[str]):
"""Process maxdd command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="maxdd",
description="Show portfolio maximum drawdown",
)
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_maximum_drawdown(
self.portfolio,
export=ns_parser.export,
)
@log_start_end(log=logger)
def call_rvol(self, other_args: List[str]):
"""Process rolling volatility command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rvol",
description="Show rolling volatility portfolio vs benchmark",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="1y",
choices=statics.PERIODS,
help="Period to apply rolling window",
metavar="PERIOD",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_rolling_volatility(
self.portfolio,
window=ns_parser.period,
export=ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_rsharpe(self, other_args: List[str]):
"""Process rolling sharpe command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rsharpe",
description="Show rolling sharpe portfolio vs benchmark",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="1y",
choices=statics.PERIODS,
help="Period to apply rolling window",
metavar="PERIOD",
)
parser.add_argument(
"-r",
"--rfr",
type=float,
dest="risk_free_rate",
default=self.risk_free_rate,
help="Set risk free rate for calculations.",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_rolling_sharpe(
self.portfolio,
risk_free_rate=ns_parser.risk_free_rate / 100,
window=ns_parser.period,
export=ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_rsort(self, other_args: List[str]):
"""Process rolling sortino command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rsort",
description="Show rolling sortino portfolio vs benchmark",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="1y",
choices=statics.PERIODS,
help="Period to apply rolling window",
metavar="PERIOD",
)
parser.add_argument(
"-r",
"--rfr",
type=float,
dest="risk_free_rate",
default=self.risk_free_rate,
help="Set risk free rate for calculations.",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_rolling_sortino(
portfolio_engine=self.portfolio,
risk_free_rate=ns_parser.risk_free_rate / 100,
window=ns_parser.period,
export=ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_rbeta(self, other_args: List[str]):
"""Process rolling beta command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="rbeta",
description="Show rolling beta portfolio vs benchmark",
)
parser.add_argument(
"-p",
"--period",
type=str,
dest="period",
default="1y",
choices=statics.PERIODS,
help="Period to apply rolling window",
metavar="PERIOD",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_rolling_beta(
self.portfolio,
window=ns_parser.period,
export=ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_metric(self, other_args: List[str]):
"""Process metric command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="metric",
description="Display metric of choice for different periods",
)
parser.add_argument(
"-m",
"--metric",
type=str,
dest="metric",
default="-h" not in other_args,
choices=self.VALID_METRICS,
help="Set metric of choice",
metavar="METRIC",
)
parser.add_argument(
"-r",
"--rfr",
type=float,
dest="risk_free_rate",
default=self.risk_free_rate,
help="Set risk free rate for calculations.",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-m")
ns_parser = self.parse_known_args_and_warn(
parser, other_args, export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED
)
if ns_parser and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
):
if ns_parser.metric == "skew":
portfolio_view.display_skewness(
self.portfolio,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "kurtosis":
portfolio_view.display_kurtosis(
self.portfolio,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "volatility":
portfolio_view.display_volatility(
self.portfolio,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "sharpe":
portfolio_view.display_sharpe_ratio(
self.portfolio,
ns_parser.risk_free_rate / 100,
ns_parser.export,
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,
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,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "rsquare":
portfolio_view.display_rsquare(
self.portfolio,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "gaintopain":
portfolio_view.display_gaintopain_ratio(
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,
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,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "tail":
portfolio_view.display_tail_ratio(
self.portfolio,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
elif ns_parser.metric == "commonsense":
portfolio_view.display_common_sense_ratio(
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,
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,
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,
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,
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,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_distr(self, other_args: List[str]):
"""Process distr command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="distr",
description="Compute distribution of daily returns",
)
parser.add_argument(
"-p",
"--period",
type=str,
choices=statics.PERIODS,
dest="period",
default="all",
help="The file to be loaded",
metavar="PERIOD",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
raw=True,
export_allowed=EXPORT_BOTH_RAW_DATA_AND_FIGURES,
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_distribution_returns(
self.portfolio,
ns_parser.period,
ns_parser.raw,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
@log_start_end(log=logger)
def call_summary(self, other_args: List[str]):
"""Process summary command"""
parser = argparse.ArgumentParser(
add_help=False,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
prog="summary",
description="Display summary of portfolio vs benchmark",
)
parser.add_argument(
"-p",
"--period",
type=str,
choices=statics.PERIODS,
dest="period",
default="all",
help="The file to be loaded",
metavar="PERIOD",
)
parser.add_argument(
"-r",
"--rfr",
type=float,
dest="risk_free_rate",
default=self.risk_free_rate,
help="Set risk free rate for calculations.",
)
if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-p")
ns_parser = self.parse_known_args_and_warn(
parser,
other_args,
export_allowed=EXPORT_ONLY_RAW_DATA_ALLOWED,
)
if (
ns_parser
and self.portfolio is not None
and check_portfolio_benchmark_defined(
self.portfolio_name, self.benchmark_name
)
):
portfolio_view.display_summary(
self.portfolio,
ns_parser.period,
ns_parser.risk_free_rate / 100,
ns_parser.export,
sheet_name=" ".join(ns_parser.sheet_name)
if ns_parser.sheet_name
else None,
)
def check_portfolio_benchmark_defined(portfolio_name: str, benchmark_name: str) -> bool:
"""Check that portfolio and benchmark have been defined
Parameters
----------
portfolio_name: str
Portfolio name, will be empty if not defined
benchmark_name: str
Benchmark name, will be empty if not defined
Returns
-------
bool
If both portfolio and benchmark have been defined
"""
if not portfolio_name:
console.print("[red]Please first define the portfolio (via 'load')[/red]\n")
return False
if not benchmark_name:
console.print("[red]Please first define the benchmark (via 'bench')[/red]\n")
return False
return True