mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-28 21:12:16 +00:00
* remove hcp * add prams dict to statics * little controller bug * fix view bug * avoid crash if one stock * check params on setup * create parameter class * check and convert parameters using class * change params name to avoid confusion * create a parameter statics dict * change some funcs names * remove unused imports * remove default dict * optional type * add multi choices * cast only int and float * fix completer * fix bugs with parameter validation * fix bugs with parameter validation * add excel formatting * sdk needs mapping as well, controller takes care of this in terminal * small formating * add some safe guard try except * controller bugs * oops * change export path of parameters to portfolio folder * add more commands to scripts * catch optimization exception * log errors * black and exceptions * add flag to test * black * flake8 * forgot this * pylint * change defaults * fix ef default * fix bl * sync sdk defaults * sync last defaults * fix plot heat and add more choices to controller autocomplete * patch weights * fix wrong bool parsing Co-authored-by: James Maslek <jmaslek11@gmail.com>
3683 lines
138 KiB
Python
3683 lines
138 KiB
Python
""" Portfolio Optimization Controller Module """
|
|
__docformat__ = "numpy"
|
|
|
|
# pylint: disable=too-many-lines,too-many-instance-attributes
|
|
|
|
import argparse
|
|
import logging
|
|
from typing import List, Dict, Tuple
|
|
|
|
from openbb_terminal.custom_prompt_toolkit import NestedCompleter
|
|
|
|
from openbb_terminal import feature_flags as obbff
|
|
from openbb_terminal import parent_classes
|
|
from openbb_terminal.core.config.paths import (
|
|
MISCELLANEOUS_DIRECTORY,
|
|
USER_EXPORTS_DIRECTORY,
|
|
USER_PORTFOLIO_DATA_DIRECTORY,
|
|
)
|
|
from openbb_terminal.decorators import log_start_end
|
|
from openbb_terminal.helper_funcs import (
|
|
check_non_negative,
|
|
get_rf,
|
|
)
|
|
from openbb_terminal.menu import session
|
|
from openbb_terminal.parent_classes import BaseController
|
|
from openbb_terminal.portfolio.portfolio_optimization import excel_model
|
|
from openbb_terminal.portfolio.portfolio_optimization import (
|
|
optimizer_helper,
|
|
optimizer_model,
|
|
optimizer_view,
|
|
statics,
|
|
)
|
|
from openbb_terminal.portfolio.portfolio_optimization import yahoo_finance_model
|
|
from openbb_terminal.portfolio.portfolio_optimization.parameters import (
|
|
params_controller,
|
|
params_view,
|
|
)
|
|
from openbb_terminal.rich_config import console, MenuText
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def add_arguments(parser_update, parser, not_in_list):
|
|
parser_dict = vars(parser)
|
|
for i in parser_dict["_actions"]:
|
|
data_dict = vars(i)
|
|
variables = list(data_dict.keys())
|
|
if variables[0] == "option_strings" and data_dict["dest"] not in not_in_list:
|
|
args = [data_dict["option_strings"][0] + "-sa"]
|
|
if len(data_dict["option_strings"]) == 2:
|
|
args.append(data_dict["option_strings"][1] + "-sa")
|
|
if len(data_dict["option_strings"]) in [1, 2]:
|
|
parser_update.add_argument(
|
|
*args,
|
|
type=data_dict["type"],
|
|
default=data_dict["default"],
|
|
dest=data_dict["dest"] + "_sa",
|
|
choices=data_dict["choices"],
|
|
help=data_dict["help"],
|
|
)
|
|
|
|
|
|
def check_input(
|
|
input_type: str, input_list: List[str], available_list: List[str]
|
|
) -> List[str]:
|
|
"""Check if input is valid
|
|
|
|
Parameters
|
|
----------
|
|
input_type : str
|
|
Type of input
|
|
input_list : List[str]
|
|
List of input
|
|
available_list : List[str]
|
|
List of available input
|
|
|
|
Returns
|
|
-------
|
|
List[str]
|
|
Valid categories
|
|
"""
|
|
|
|
valid: List[str] = []
|
|
|
|
for i in input_list:
|
|
if i in available_list:
|
|
valid.append(i)
|
|
else:
|
|
console.print(f"[red]{input_type} '{i}' not available.[/red]\n")
|
|
return valid
|
|
|
|
|
|
def get_valid_portfolio_categories(
|
|
input_portfolios: List[str],
|
|
available_portfolios: Dict,
|
|
input_categories: List[str],
|
|
available_categories: Dict,
|
|
) -> Tuple[List[str], List[str]]:
|
|
"""Get valid portfolios and categories
|
|
|
|
Parameters
|
|
----------
|
|
input_portfolios : List[str]
|
|
List of input portfolios
|
|
available_portfolios : Dict
|
|
Dict of available portfolios
|
|
input_categories : List[str]
|
|
List of input categories
|
|
available_categories : Dict
|
|
Dict of available categories
|
|
|
|
Returns
|
|
-------
|
|
Tuple[List[str], List[str]]
|
|
Valid portfolios and categories
|
|
"""
|
|
portfolios_list = list(set(available_portfolios.keys()))
|
|
categories_list = list(set(available_categories.keys()))
|
|
|
|
if not portfolios_list:
|
|
portfolio_msg = "None. Perform some optimization to build a portfolio."
|
|
else:
|
|
if not input_portfolios:
|
|
console.print("[yellow]Please select at least one portfolio.[/yellow]\n")
|
|
portfolio_msg = ", ".join(portfolios_list)
|
|
|
|
if not categories_list:
|
|
categories_msg = "None. Attribute some categories in the loaded file."
|
|
else:
|
|
categories_msg = ", ".join(categories_list)
|
|
|
|
console.print(
|
|
f"[yellow]Current Portfolios: [/yellow]{portfolio_msg}\n",
|
|
)
|
|
|
|
console.print(
|
|
f"[yellow]Current Categories: [/yellow]{categories_msg}\n",
|
|
)
|
|
|
|
valid_portfolios = check_input(
|
|
input_type="Portfolio",
|
|
input_list=input_portfolios,
|
|
available_list=portfolios_list,
|
|
)
|
|
|
|
valid_categories = check_input(
|
|
input_type="Category",
|
|
input_list=input_categories,
|
|
available_list=categories_list,
|
|
)
|
|
|
|
return valid_portfolios, valid_categories
|
|
|
|
|
|
class PortfolioOptimizationController(BaseController):
|
|
"""Portfolio Optimization Controller class"""
|
|
|
|
DEFAULT_PORTFOLIO_DIRECTORY = MISCELLANEOUS_DIRECTORY / "portfolio_examples"
|
|
DEFAULT_ALLOCATION_DIRECTORY = DEFAULT_PORTFOLIO_DIRECTORY / "allocation"
|
|
DEFAULT_OPTIMIZATION_DIRECTORY = DEFAULT_PORTFOLIO_DIRECTORY / "optimization"
|
|
FILE_TYPE_LIST = ["xlsx", "ini"]
|
|
|
|
CHOICES_COMMANDS = [
|
|
"show",
|
|
"rpf",
|
|
"load",
|
|
"plot",
|
|
"equal",
|
|
"mktcap",
|
|
"dividend",
|
|
"property",
|
|
"maxsharpe",
|
|
"minrisk",
|
|
"maxutil",
|
|
"maxret",
|
|
"maxdiv",
|
|
"maxdecorr",
|
|
"blacklitterman",
|
|
"riskparity",
|
|
"relriskparity",
|
|
"hrp",
|
|
"herc",
|
|
"nco",
|
|
"ef",
|
|
"file",
|
|
]
|
|
CHOICES_MENUS = ["params"]
|
|
|
|
PATH = "/portfolio/po/"
|
|
CHOICES_GENERATION = True
|
|
|
|
files_available: List = list()
|
|
|
|
@classmethod
|
|
def build_allocation_file_map(cls) -> dict:
|
|
allocation_file_map = {
|
|
filepath.name: filepath
|
|
for file_type in cls.FILE_TYPE_LIST
|
|
for filepath in cls.DEFAULT_ALLOCATION_DIRECTORY.rglob(f"*.{file_type}")
|
|
}
|
|
allocation_file_map.update(
|
|
{
|
|
filepath.name: filepath
|
|
for file_type in cls.FILE_TYPE_LIST
|
|
for filepath in (USER_PORTFOLIO_DATA_DIRECTORY / "allocation").rglob(
|
|
f"*.{file_type}"
|
|
)
|
|
}
|
|
)
|
|
|
|
return allocation_file_map
|
|
|
|
@classmethod
|
|
def build_optimization_file_map(cls) -> dict:
|
|
optimization_file_map = {
|
|
filepath.name: filepath
|
|
for file_type in cls.FILE_TYPE_LIST
|
|
for filepath in cls.DEFAULT_OPTIMIZATION_DIRECTORY.rglob(f"*.{file_type}")
|
|
}
|
|
|
|
optimization_file_map.update(
|
|
{
|
|
filepath.name: filepath
|
|
for file_type in cls.FILE_TYPE_LIST
|
|
for filepath in (USER_PORTFOLIO_DATA_DIRECTORY / "optimization").rglob(
|
|
f"*.{file_type}"
|
|
)
|
|
}
|
|
)
|
|
|
|
return optimization_file_map
|
|
|
|
def __init__(
|
|
self,
|
|
tickers: List[str] = None,
|
|
portfolios: Dict = None,
|
|
categories: Dict = None,
|
|
queue: List[str] = None,
|
|
):
|
|
"""Constructor"""
|
|
super().__init__(queue)
|
|
self.current_model = None
|
|
|
|
if tickers:
|
|
self.tickers = list(set(tickers))
|
|
self.tickers.sort()
|
|
else:
|
|
self.tickers = list()
|
|
|
|
if portfolios:
|
|
self.portfolios = dict(portfolios)
|
|
else:
|
|
self.portfolios = dict()
|
|
|
|
if categories:
|
|
self.categories = dict(categories)
|
|
self.available_categories = list(self.categories.keys())
|
|
else:
|
|
self.categories = dict()
|
|
self.available_categories = list()
|
|
|
|
self.count = 0
|
|
self.current_portfolio = ""
|
|
|
|
self.allocation_file_map = self.build_allocation_file_map()
|
|
self.optimization_file_map = self.build_optimization_file_map()
|
|
|
|
self.current_file = ""
|
|
|
|
self.params: Dict = {}
|
|
|
|
if session and obbff.USE_PROMPT_TOOLKIT:
|
|
choices: dict = self.choices_default
|
|
self.choices = choices
|
|
self.completer = NestedCompleter.from_nested_dict(choices)
|
|
|
|
def update_runtime_choices(self):
|
|
if session and obbff.USE_PROMPT_TOOLKIT:
|
|
if self.portfolios:
|
|
self.choices["show"]["--portfolios"] = {
|
|
c: {} for c in list(self.portfolios.keys())
|
|
}
|
|
self.choices["rpf"]["--portfolios"] = {
|
|
c: {} for c in list(self.portfolios.keys())
|
|
}
|
|
self.choices["plot"]["--portfolios"] = {
|
|
c: {} for c in list(self.portfolios.keys())
|
|
}
|
|
self.completer = NestedCompleter.from_nested_dict(self.choices)
|
|
|
|
def print_help(self):
|
|
"""Print help"""
|
|
mt = MenuText("portfolio/po/")
|
|
mt.add_cmd("load")
|
|
mt.add_raw("\n")
|
|
mt.add_param("_loaded", self.current_portfolio or "")
|
|
mt.add_raw("\n")
|
|
mt.add_param("_tickers", ", ".join(self.tickers))
|
|
mt.add_param("_categories", ", ".join(self.available_categories))
|
|
mt.add_raw("\n")
|
|
mt.add_cmd("file")
|
|
mt.add_menu("params")
|
|
mt.add_raw("\n")
|
|
mt.add_param("_parameter", self.current_file)
|
|
mt.add_raw("\n")
|
|
mt.add_info("_mean_risk_optimization_")
|
|
mt.add_cmd("maxsharpe", self.tickers)
|
|
mt.add_cmd("minrisk", self.tickers)
|
|
mt.add_cmd("maxutil", self.tickers)
|
|
mt.add_cmd("maxret", self.tickers)
|
|
mt.add_cmd("maxdiv", self.tickers)
|
|
mt.add_cmd("maxdecorr", self.tickers)
|
|
mt.add_cmd("blacklitterman", self.tickers)
|
|
mt.add_cmd("ef", self.tickers)
|
|
mt.add_raw("\n")
|
|
|
|
mt.add_info("_risk_parity_optimization_")
|
|
mt.add_cmd("riskparity", self.tickers)
|
|
mt.add_cmd("relriskparity", self.tickers)
|
|
mt.add_raw("\n")
|
|
|
|
mt.add_info("_hierarchical_clustering_models_")
|
|
mt.add_cmd("hrp", self.tickers)
|
|
mt.add_cmd("herc", self.tickers)
|
|
mt.add_cmd("nco", self.tickers)
|
|
mt.add_raw("\n")
|
|
|
|
mt.add_info("_other_optimization_techniques_")
|
|
mt.add_cmd("equal", self.tickers)
|
|
mt.add_cmd("mktcap", self.tickers)
|
|
mt.add_cmd("dividend", self.tickers)
|
|
mt.add_cmd("property", self.tickers)
|
|
|
|
mt.add_raw("\n")
|
|
mt.add_param("_optimized_portfolio", ", ".join(self.portfolios.keys()))
|
|
mt.add_raw("\n")
|
|
|
|
mt.add_cmd("rpf", bool(self.portfolios.keys()))
|
|
mt.add_cmd("show", bool(self.portfolios.keys()))
|
|
mt.add_cmd("plot", bool(self.portfolios.keys()))
|
|
|
|
console.print(text=mt.menu_text, menu="Portfolio - Portfolio Optimization")
|
|
|
|
# pylint: disable=too-many-arguments
|
|
def po_parser(
|
|
self,
|
|
parser,
|
|
rm: bool = False,
|
|
mt: bool = False,
|
|
ct: bool = False,
|
|
p: bool = False,
|
|
s: bool = False,
|
|
e: bool = False,
|
|
lr: bool = False,
|
|
freq: bool = False,
|
|
mn: bool = False,
|
|
th: bool = False,
|
|
r: bool = False,
|
|
a: bool = False,
|
|
v: bool = True,
|
|
name: str = "",
|
|
):
|
|
"""Holds common parser arguments to eliminate repetition"""
|
|
if rm:
|
|
parser.add_argument(
|
|
"-rm",
|
|
"--risk-measure",
|
|
default=self.params["risk_measure"]
|
|
if "risk_measure" in self.params
|
|
else "MV",
|
|
dest="risk_measure",
|
|
help="""Risk measure used to optimize the portfolio. Possible values are:
|
|
'MV' : Variance
|
|
'MAD' : Mean Absolute Deviation
|
|
'MSV' : Semi Variance (Variance of negative returns)
|
|
'FLPM' : First Lower Partial Moment
|
|
'SLPM' : Second Lower Partial Moment
|
|
'CVaR' : Conditional Value at Risk
|
|
'EVaR' : Entropic Value at Risk
|
|
'WR' : Worst Realization
|
|
'ADD' : Average Drawdown of uncompounded returns
|
|
'UCI' : Ulcer Index of uncompounded returns
|
|
'CDaR' : Conditional Drawdown at Risk of uncompounded returns
|
|
'EDaR' : Entropic Drawdown at Risk of uncompounded returns
|
|
'MDD' : Maximum Drawdown of uncompounded returns
|
|
""",
|
|
choices=statics.MEAN_RISK_CHOICES,
|
|
)
|
|
if mt:
|
|
parser.add_argument(
|
|
"-mt",
|
|
"--method",
|
|
default=self.params["nan_fill_method"]
|
|
if "nan_fill_method" in self.params
|
|
else "time",
|
|
dest="nan_fill_method",
|
|
help="""Method used to fill nan values in time series, by default time.
|
|
Possible values are:
|
|
'linear': linear interpolation
|
|
'time': linear interpolation based on time index
|
|
'nearest': use nearest value to replace nan values
|
|
'zero': spline of zeroth order
|
|
'slinear': spline of first order
|
|
'quadratic': spline of second order
|
|
'cubic': spline of third order
|
|
'barycentric': builds a polynomial that pass for all points""",
|
|
choices=statics.METHOD_CHOICES,
|
|
metavar="METHOD",
|
|
)
|
|
if ct:
|
|
parser.add_argument(
|
|
"-ct",
|
|
"--categories",
|
|
dest="categories",
|
|
type=lambda s: [str(item).upper() for item in s.split(",")],
|
|
default=self.available_categories,
|
|
help="Show selected categories",
|
|
)
|
|
if p:
|
|
parser.add_argument(
|
|
"-p",
|
|
"--period",
|
|
default=self.params["historic_period"]
|
|
if "historic_period" in self.params
|
|
else "3y",
|
|
dest="historic_period",
|
|
help="""Period to get yfinance data from.
|
|
Possible frequency strings are:
|
|
'd': means days, for example '252d' means 252 days
|
|
'w': means weeks, for example '52w' means 52 weeks
|
|
'mo': means months, for example '12mo' means 12 months
|
|
'y': means years, for example '1y' means 1 year
|
|
'ytd': downloads data from beginning of year to today
|
|
'max': downloads all data available for each asset""",
|
|
choices=statics.PERIOD_CHOICES,
|
|
metavar="PERIOD",
|
|
)
|
|
if s:
|
|
parser.add_argument(
|
|
"-s",
|
|
"--start",
|
|
default=self.params["start_period"]
|
|
if "start_period" in self.params
|
|
else "",
|
|
dest="start_period",
|
|
help="""Start date to get yfinance data from. Must be in
|
|
'YYYY-MM-DD' format""",
|
|
)
|
|
if e:
|
|
parser.add_argument(
|
|
"-e",
|
|
"--end",
|
|
default=self.params["end_period"]
|
|
if "end_period" in self.params
|
|
else "",
|
|
dest="end_period",
|
|
help="""End date to get yfinance data from. Must be in
|
|
'YYYY-MM-DD' format""",
|
|
)
|
|
if lr:
|
|
parser.add_argument(
|
|
"-lr",
|
|
"--log-returns",
|
|
action="store_true",
|
|
default=self.params["log_returns"]
|
|
if "log_returns" in self.params
|
|
else False,
|
|
dest="log_returns",
|
|
help="If use logarithmic or arithmetic returns to calculate returns",
|
|
)
|
|
if freq:
|
|
parser.add_argument(
|
|
"--freq",
|
|
default=self.params["return_frequency"]
|
|
if "return_frequency" in self.params
|
|
else "d",
|
|
dest="return_frequency",
|
|
help="""Frequency used to calculate returns. Possible values are:
|
|
'd': for daily returns
|
|
'w': for weekly returns
|
|
'm': for monthly returns
|
|
""",
|
|
choices=statics.FREQ_CHOICES,
|
|
)
|
|
if mn:
|
|
parser.add_argument(
|
|
"-mn",
|
|
"--maxnan",
|
|
type=float,
|
|
default=self.params["max_nan"] if "max_nan" in self.params else 0.05,
|
|
dest="max_nan",
|
|
help="""Max percentage of nan values accepted per asset to be
|
|
considered in the optimization process""",
|
|
)
|
|
if th:
|
|
parser.add_argument(
|
|
"-th",
|
|
"--threshold",
|
|
type=float,
|
|
default=self.params["threshold_value"]
|
|
if "threshold_value" in self.params
|
|
else 0.30,
|
|
dest="threshold_value",
|
|
help="""Value used to replace outliers that are higher to threshold
|
|
in absolute value""",
|
|
)
|
|
if r:
|
|
parser.add_argument(
|
|
"-r",
|
|
"--risk-free-rate",
|
|
type=float,
|
|
dest="risk_free",
|
|
default=self.params["risk_free"]
|
|
if "risk_free" in self.params
|
|
else get_rf(),
|
|
help="""Risk-free rate of borrowing/lending. The period of the
|
|
risk-free rate must be annual""",
|
|
)
|
|
if a:
|
|
parser.add_argument(
|
|
"-a",
|
|
"--alpha",
|
|
type=float,
|
|
default=self.params["significance_level"]
|
|
if "significance_level" in self.params
|
|
else 0.05,
|
|
dest="significance_level",
|
|
help="Significance level of CVaR, EVaR, CDaR and EDaR",
|
|
)
|
|
if v:
|
|
parser.add_argument(
|
|
"-v",
|
|
"--value",
|
|
default=self.params["long_allocation"]
|
|
if "long_allocation" in self.params
|
|
else 1,
|
|
type=float,
|
|
dest="long_allocation",
|
|
help="Amount to allocate to portfolio",
|
|
)
|
|
if name:
|
|
parser.add_argument(
|
|
"--name",
|
|
type=str,
|
|
dest="name",
|
|
default=name + str(self.count),
|
|
help="Save portfolio with personalized or default name",
|
|
)
|
|
return parser
|
|
|
|
def custom_reset(self):
|
|
"""Class specific component of reset command"""
|
|
objects_to_reload = ["portfolio", "po"]
|
|
if self.current_portfolio:
|
|
objects_to_reload.append(f"load {self.current_portfolio}")
|
|
if self.current_file:
|
|
objects_to_reload.append(f"file {self.current_file}")
|
|
return objects_to_reload
|
|
|
|
@log_start_end(log=logger)
|
|
def call_file(self, other_args: List[str]):
|
|
"""Process file command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="file",
|
|
description="Select parameter file to use",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-f",
|
|
"--file",
|
|
required="-h" not in other_args,
|
|
nargs="+",
|
|
dest="file",
|
|
help="Parameter file to be used",
|
|
choices=self.optimization_file_map.keys(),
|
|
metavar="FILE",
|
|
)
|
|
if other_args and "-" not in other_args[0][0]:
|
|
other_args.insert(0, "--file")
|
|
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
|
|
if ns_parser:
|
|
self.current_file = " ".join(ns_parser.file)
|
|
|
|
if self.current_file in self.optimization_file_map:
|
|
file_location = self.optimization_file_map[self.current_file]
|
|
else:
|
|
file_location = self.current_file # type: ignore
|
|
|
|
self.params, self.current_model = params_view.load_file(file_location)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_params(self, _):
|
|
"""Process params command"""
|
|
self.queue = self.load_class(
|
|
params_controller.ParametersController,
|
|
self.current_file,
|
|
self.queue,
|
|
self.params,
|
|
self.current_model,
|
|
)
|
|
self.current_file = parent_classes.controllers[
|
|
"/portfolio/po/params/"
|
|
].current_file
|
|
self.current_model = parent_classes.controllers[
|
|
"/portfolio/po/params/"
|
|
].current_model
|
|
self.params = parent_classes.controllers["/portfolio/po/params/"].params
|
|
|
|
@log_start_end(log=logger)
|
|
def call_show(self, other_args: List[str]):
|
|
"""Show saved portfolios"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
prog="show",
|
|
description="""Show selected saved portfolios""",
|
|
)
|
|
parser.add_argument(
|
|
"-pf",
|
|
"--portfolios",
|
|
dest="portfolios",
|
|
type=lambda s: [str(item).upper() for item in s.split(",")],
|
|
default=[],
|
|
help="Show selected saved portfolios",
|
|
)
|
|
if other_args:
|
|
if "-" not in other_args[0]:
|
|
other_args.insert(0, "-pf")
|
|
|
|
parser = self.po_parser(parser, ct=True)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
|
|
if ns_parser:
|
|
portfolios, categories = get_valid_portfolio_categories(
|
|
input_portfolios=ns_parser.portfolios,
|
|
available_portfolios=self.portfolios,
|
|
input_categories=ns_parser.categories,
|
|
available_categories=self.categories,
|
|
)
|
|
|
|
for p in portfolios:
|
|
console.print("[yellow]Portfolio[/yellow]: " + p + "\n")
|
|
optimizer_view.display_show(
|
|
weights=self.portfolios[p],
|
|
tables=categories,
|
|
categories_dict=self.categories,
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_rpf(self, other_args: List[str]):
|
|
"""Remove one portfolio"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
prog="rpf",
|
|
description="""Remove one of the portfolios""",
|
|
)
|
|
parser.add_argument(
|
|
"-pf",
|
|
"--portfolios",
|
|
dest="portfolios",
|
|
type=lambda s: [str(item).upper() for item in s.split(",")],
|
|
default=[],
|
|
help="portfolios to be removed from the saved portfolios",
|
|
)
|
|
if other_args:
|
|
if "-" not in other_args[0]:
|
|
other_args.insert(0, "-pf")
|
|
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
portfolios = set(self.portfolios.keys())
|
|
for portfolio in ns_parser.portfolios:
|
|
if portfolio in portfolios:
|
|
self.portfolios.pop(portfolio)
|
|
portfolios.remove(portfolio)
|
|
console.print(f"[yellow]Removed '{portfolio}'.[/yellow]")
|
|
else:
|
|
console.print(f"[red]Portfolio '{portfolio}' does not exist.[/red]")
|
|
|
|
if self.portfolios:
|
|
console.print(
|
|
f"\n[yellow]Current Portfolios: [/yellow]{('None', ', '.join(portfolios))[bool(portfolios)]}"
|
|
)
|
|
|
|
self.update_runtime_choices()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_load(self, other_args: List[str]):
|
|
"""Load file with stocks tickers and categories"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
prog="load",
|
|
description="""Load file of stocks tickers with optional categories""",
|
|
)
|
|
parser.add_argument(
|
|
"-f",
|
|
"--file",
|
|
required="-h" not in other_args,
|
|
nargs="+",
|
|
dest="file",
|
|
help="Allocation file to be used",
|
|
choices=self.allocation_file_map.keys(),
|
|
)
|
|
if other_args and "-" not in other_args[0][0]:
|
|
other_args.insert(0, "--file")
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
|
|
if ns_parser:
|
|
filename = " ".join(ns_parser.file)
|
|
|
|
if filename in self.allocation_file_map:
|
|
file_location = self.allocation_file_map[filename]
|
|
else:
|
|
file_location = filename # type: ignore
|
|
|
|
self.tickers, self.categories = excel_model.load_allocation(file_location)
|
|
self.available_categories = list(self.categories.keys())
|
|
if "CURRENT_INVESTED_AMOUNT" in self.available_categories:
|
|
self.available_categories.remove("CURRENT_INVESTED_AMOUNT")
|
|
self.portfolios = dict()
|
|
self.update_runtime_choices()
|
|
self.current_portfolio = filename
|
|
|
|
@log_start_end(log=logger)
|
|
def call_plot(self, other_args: List[str]):
|
|
"""Process plot command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="plot",
|
|
description="Plot selected charts for portfolios",
|
|
)
|
|
parser.add_argument(
|
|
"-pf",
|
|
"--portfolios",
|
|
type=lambda s: [str(item).upper() for item in s.split(",")],
|
|
default=[],
|
|
dest="portfolios",
|
|
help="Selected portfolios that will be plotted",
|
|
)
|
|
parser.add_argument(
|
|
"-pi",
|
|
"--pie",
|
|
action="store_true",
|
|
dest="pie",
|
|
default=False,
|
|
help="Display a pie chart for weights",
|
|
)
|
|
parser.add_argument(
|
|
"-hi",
|
|
"--hist",
|
|
action="store_true",
|
|
dest="hist",
|
|
default=False,
|
|
help="Display a histogram with risk measures",
|
|
)
|
|
parser.add_argument(
|
|
"-dd",
|
|
"--drawdown",
|
|
action="store_true",
|
|
dest="dd",
|
|
default=False,
|
|
help="Display a drawdown chart with risk measures",
|
|
)
|
|
parser.add_argument(
|
|
"-rc",
|
|
"--rc-chart",
|
|
action="store_true",
|
|
dest="rc_chart",
|
|
default=False,
|
|
help="Display a risk contribution chart for assets",
|
|
)
|
|
parser.add_argument(
|
|
"-he",
|
|
"--heat",
|
|
action="store_true",
|
|
dest="heat",
|
|
default=False,
|
|
help="Display a heatmap of correlation matrix with dendrogram",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
)
|
|
if other_args:
|
|
if "-" not in other_args[0]:
|
|
other_args.insert(0, "-pf")
|
|
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
portfolios, categories = get_valid_portfolio_categories(
|
|
input_portfolios=ns_parser.portfolios,
|
|
available_portfolios=self.portfolios,
|
|
input_categories=ns_parser.categories,
|
|
available_categories=self.categories,
|
|
)
|
|
if not portfolios:
|
|
return
|
|
|
|
if not (
|
|
ns_parser.pie
|
|
or ns_parser.hist
|
|
or ns_parser.dd
|
|
or ns_parser.rc_chart
|
|
or ns_parser.heat
|
|
):
|
|
console.print(
|
|
"[yellow]Please select at least one chart to plot[/yellow]",
|
|
"[yellow]from the following: -pi, -hi, -dd, -rc, -he.[/yellow]",
|
|
)
|
|
return
|
|
|
|
stocks = []
|
|
for i in portfolios:
|
|
stocks += list(self.portfolios[i].keys())
|
|
stocks = list(set(stocks))
|
|
stocks.sort()
|
|
|
|
_, stock_returns = optimizer_model.get_equal_weights(
|
|
symbols=stocks,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
value=1,
|
|
)
|
|
|
|
if ns_parser.hist or ns_parser.dd:
|
|
for i in portfolios:
|
|
weights = self.portfolios[i]
|
|
weights = dict(
|
|
sorted(weights.items(), key=lambda x: x[1], reverse=True)
|
|
)
|
|
stocks = list(weights.keys())
|
|
|
|
# hist and dd are transversal to all categories
|
|
optimizer_view.additional_plots(
|
|
weights=weights,
|
|
data=stock_returns[stocks],
|
|
portfolio_name=i,
|
|
freq=ns_parser.return_frequency,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
a_sim=100,
|
|
beta=ns_parser.significance_level,
|
|
b_sim=100,
|
|
hist=ns_parser.hist,
|
|
dd=ns_parser.dd,
|
|
)
|
|
|
|
if ns_parser.pie or ns_parser.rc_chart or ns_parser.heat:
|
|
|
|
if not categories:
|
|
console.print(
|
|
"[yellow]Categories must be provided to use -pi, -rc or -he.[/yellow]"
|
|
)
|
|
return
|
|
|
|
for i in portfolios:
|
|
weights = self.portfolios[i]
|
|
weights = dict(
|
|
sorted(weights.items(), key=lambda x: x[1], reverse=True)
|
|
)
|
|
stocks = list(weights.keys())
|
|
|
|
# pie, rc_chart and heat apply to each category
|
|
for category in categories:
|
|
filtered_categories = dict(
|
|
filter(
|
|
lambda elem: elem[0] in stocks,
|
|
self.categories[category].items(),
|
|
)
|
|
)
|
|
optimizer_view.additional_plots(
|
|
weights=weights,
|
|
data=stock_returns[stocks],
|
|
category_dict=filtered_categories,
|
|
category=category,
|
|
portfolio_name=i,
|
|
freq=ns_parser.return_frequency,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
a_sim=100,
|
|
beta=ns_parser.significance_level,
|
|
b_sim=100,
|
|
pie=ns_parser.pie,
|
|
rc_chart=ns_parser.rc_chart,
|
|
heat=ns_parser.heat,
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_equal(self, other_args: List[str]):
|
|
"""Process equal command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="equal",
|
|
description="Returns an equally weighted portfolio",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="NAME_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
weights = optimizer_view.display_equal_weight(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
value=ns_parser.long_allocation,
|
|
table=True,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_mktcap(self, other_args: List[str]):
|
|
"""Process mktcap command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="mktcap",
|
|
description="Returns a portfolio that is weighted based on Market Cap.",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="MKTCAP_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 stocks selected to perform calculations."
|
|
)
|
|
return
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_property_weighting(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
s_property="marketCap",
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
value=ns_parser.long_allocation,
|
|
table=True,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_dividend(self, other_args: List[str]):
|
|
"""Process dividend command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="dividend",
|
|
description="Returns a portfolio that is weighted based dividend yield.",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="DIVIDEND_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 stocks selected to perform calculations."
|
|
)
|
|
return
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_property_weighting(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
s_property="dividendYield",
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
value=ns_parser.long_allocation,
|
|
table=True,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_property(self, other_args: List[str]):
|
|
"""Process property command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="property",
|
|
description="Returns a portfolio that is weighted based on selected property.",
|
|
)
|
|
parser.add_argument(
|
|
"-pr",
|
|
"--property",
|
|
required=bool("-h" not in other_args),
|
|
type=optimizer_helper.check_valid_property_type,
|
|
dest="s_property",
|
|
choices=yahoo_finance_model.yf_info_choices,
|
|
help="""Property info to weight. Use one of yfinance info options.""",
|
|
metavar="PROPERTY",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="PROPERTY_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 stocks selected to perform calculations."
|
|
)
|
|
return
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_property_weighting(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
s_property=ns_parser.s_property,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
value=ns_parser.long_allocation,
|
|
table=True,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_maxsharpe(self, other_args: List[str]):
|
|
"""Process maxsharpe command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="maxsharpe",
|
|
description="Maximizes the portfolio's return/risk ratio",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-tk",
|
|
"--target-risk",
|
|
dest="target_risk",
|
|
default=self.params["target_risk"] if "target_risk" in self.params else -1,
|
|
type=float,
|
|
help="Constraint on maximum level of portfolio's risk",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
default=self.params["expected_return"]
|
|
if "expected_return" in self.params
|
|
else "hist",
|
|
dest="expected_return",
|
|
help="Method used to estimate the expected return vector",
|
|
choices=statics.MEAN_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="MAXSHARPE_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_max_sharpe(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
target_return=ns_parser.target_return,
|
|
target_risk=ns_parser.target_risk,
|
|
mean=ns_parser.expected_return.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_max_sharpe(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
target_risk=ns_parser.target_risk_sa,
|
|
mean=ns_parser.expected_return_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_minrisk(self, other_args: List[str]):
|
|
"""Process minrisk command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="minrisk",
|
|
description="Minimizes portfolio's risk",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-tk",
|
|
"--target-risk",
|
|
dest="target_risk",
|
|
default=self.params["target_risk"] if "target_risk" in self.params else -1,
|
|
type=float,
|
|
help="Constraint on maximum level of portfolio's risk",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
default=self.params["expected_return"]
|
|
if "expected_return" in self.params
|
|
else "hist",
|
|
dest="expected_return",
|
|
help="Method used to estimate expected returns vector",
|
|
choices=statics.MEAN_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"] if "mean_ewma" in self.params else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="MINRISK_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_min_risk(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
target_return=ns_parser.target_return,
|
|
target_risk=ns_parser.target_risk,
|
|
mean=ns_parser.expected_return.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_min_risk(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
target_risk=ns_parser.target_risk_sa,
|
|
mean=ns_parser.expected_return_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_maxutil(self, other_args: List[str]):
|
|
"""Process maxutil command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="maxutil",
|
|
description="Maximizes a risk averse utility function",
|
|
)
|
|
parser.add_argument(
|
|
"-ra",
|
|
"--risk-aversion",
|
|
type=float,
|
|
dest="risk_aversion",
|
|
default=self.params["risk_aversion"]
|
|
if "risk_aversion" in self.params
|
|
else 1,
|
|
help="Risk aversion parameter",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-tk",
|
|
"--target-risk",
|
|
dest="target_risk",
|
|
default=self.params["target_risk"] if "target_risk" in self.params else -1,
|
|
type=float,
|
|
help="Constraint on maximum level of portfolio's risk",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
default=self.params["expected_return"]
|
|
if "expected_return" in self.params
|
|
else "hist",
|
|
dest="expected_return",
|
|
help="Method used to estimate the expected return vector",
|
|
choices=statics.MEAN_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="MAXUTIL_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_max_util(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
risk_aversion=ns_parser.risk_aversion,
|
|
alpha=ns_parser.significance_level,
|
|
target_return=ns_parser.target_return,
|
|
target_risk=ns_parser.target_risk,
|
|
mean=ns_parser.expected_return.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_max_util(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
risk_aversion=ns_parser.risk_aversion_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
target_risk=ns_parser.target_risk_sa,
|
|
mean=ns_parser.expected_return_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_maxret(self, other_args: List[str]):
|
|
"""Process maxret command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="maxret",
|
|
description="Maximizes the portfolio's return ",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-tk",
|
|
"--target-risk",
|
|
dest="target_risk",
|
|
default=self.params["target_risk"] if "target_risk" in self.params else -1,
|
|
type=float,
|
|
help="Constraint on maximum level of portfolio's risk",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
default=self.params["expected_return"]
|
|
if "expected_return" in self.params
|
|
else "hist",
|
|
dest="expected_return",
|
|
help="Method used to estimate the expected return vector",
|
|
choices=statics.MEAN_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="MAXRET_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_max_ret(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
target_return=ns_parser.target_return,
|
|
target_risk=ns_parser.target_risk,
|
|
mean=ns_parser.expected_return.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_max_ret(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
target_risk=ns_parser.target_risk_sa,
|
|
mean=ns_parser.expected_return_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_maxdiv(self, other_args: List[str]):
|
|
"""Process maxdiv command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="maxdiv",
|
|
description="Maximizes the portfolio's diversification ratio",
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
v=True,
|
|
name="MAXDIV_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_max_div(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_max_div(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_maxdecorr(self, other_args: List[str]):
|
|
"""Process maxdecorr command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="maxdecorr",
|
|
description="Maximizes the portfolio's decorrelation",
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
v=True,
|
|
name="MAXDECORR_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_max_decorr(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
covariance=ns_parser.covariance.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_max_decorr(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_blacklitterman(self, other_args: List[str]):
|
|
"""Process blacklitterman command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="blacklitterman",
|
|
description="Optimize portfolio using Black Litterman estimates",
|
|
)
|
|
parser.add_argument(
|
|
"-bm",
|
|
"--benchmark",
|
|
type=str,
|
|
default=None,
|
|
dest="benchmark",
|
|
help="portfolio name from current portfolio list",
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--objective",
|
|
default=self.params["objective"]
|
|
if "objective" in self.params
|
|
else "Sharpe",
|
|
dest="objective",
|
|
help="Objective function used to optimize the portfolio",
|
|
choices=statics.OBJECTIVE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-pv",
|
|
"--p-views",
|
|
type=lambda s: [
|
|
[float(item) for item in row.split(",")] for row in s.split(";")
|
|
],
|
|
default=self.params["p_views"] if "p_views" in self.params else None,
|
|
dest="p_views",
|
|
help="matrix P of analyst views",
|
|
)
|
|
parser.add_argument(
|
|
"-qv",
|
|
"--q-views",
|
|
type=lambda s: [float(item) for item in s.split(",")],
|
|
default=self.params["q_views"] if "q_views" in self.params else None,
|
|
dest="q_views",
|
|
help="matrix Q of analyst views",
|
|
)
|
|
parser.add_argument(
|
|
"-ra",
|
|
"--risk-aversion",
|
|
type=float,
|
|
dest="risk_aversion",
|
|
default=self.params["risk_aversion"]
|
|
if "risk_aversion" in self.params
|
|
else 1,
|
|
help="Risk aversion parameter",
|
|
)
|
|
parser.add_argument(
|
|
"-d",
|
|
"--delta",
|
|
default=self.params["delta"] if "delta" in self.params else None,
|
|
dest="delta",
|
|
type=float,
|
|
help="Risk aversion factor of Black Litterman model",
|
|
)
|
|
parser.add_argument(
|
|
"-eq",
|
|
"--equilibrium",
|
|
action="store_true",
|
|
default=self.params["equilibrium"]
|
|
if "equilibrium" in self.params
|
|
else True,
|
|
dest="equilibrium",
|
|
help="""If True excess returns are based on equilibrium market portfolio, if False
|
|
excess returns are calculated as historical returns minus risk free rate.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-op",
|
|
"--optimize",
|
|
action="store_false",
|
|
default=self.params["optimize"] if "optimize" in self.params else True,
|
|
dest="optimize",
|
|
help="""If True Black Litterman estimates are used as inputs of mean variance model,
|
|
if False returns equilibrium weights from Black Litterman model
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
)
|
|
parser.add_argument(
|
|
"--file",
|
|
type=lambda s: s if s.endswith(".xlsx") or len(s) == 0 else s + ".xlsx",
|
|
dest="file",
|
|
default="",
|
|
help="Upload an Excel file with views for Black Litterman model",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--download",
|
|
type=lambda s: s if s.endswith(".xlsx") or len(s) == 0 else s + ".xlsx",
|
|
dest="download",
|
|
default="",
|
|
help="Create a template to design Black Litterman model views",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
v=True,
|
|
name="BL_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
if len(ns_parser.download) > 0:
|
|
file = (
|
|
USER_EXPORTS_DIRECTORY / "portfolio" / "views" / ns_parser.download
|
|
)
|
|
|
|
excel_model.excel_bl_views(file=file, stocks=self.tickers, n=1)
|
|
return
|
|
|
|
if ns_parser.file:
|
|
excel_file = USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file
|
|
p_views, q_views = excel_model.load_bl_views(excel_file=excel_file)
|
|
else:
|
|
p_views = ns_parser.p_views
|
|
q_views = ns_parser.q_views
|
|
|
|
if ns_parser.benchmark is None:
|
|
benchmark = None
|
|
else:
|
|
benchmark = self.portfolios[ns_parser.benchmark.upper()]
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_black_litterman(
|
|
symbols=self.tickers,
|
|
p_views=p_views,
|
|
q_views=q_views,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
benchmark=benchmark,
|
|
objective=ns_parser.objective.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
risk_aversion=ns_parser.risk_aversion,
|
|
delta=ns_parser.delta,
|
|
equilibrium=ns_parser.equilibrium,
|
|
optimize=ns_parser.optimize,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
if ns_parser.file_sa:
|
|
excel_file = (
|
|
USER_PORTFOLIO_DATA_DIRECTORY / "views" / ns_parser.file_sa
|
|
)
|
|
p_views_sa, q_views_sa = excel_model.load_bl_views(
|
|
excel_file=excel_file
|
|
)
|
|
else:
|
|
p_views_sa = ns_parser.p_views_sa
|
|
q_views_sa = ns_parser.q_views_sa
|
|
|
|
weights_sa = optimizer_view.display_black_litterman(
|
|
symbols=self.tickers,
|
|
p_views=p_views_sa,
|
|
q_views=q_views_sa,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
benchmark=benchmark,
|
|
objective=ns_parser.objective_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
risk_aversion=ns_parser.risk_aversion_sa,
|
|
delta=ns_parser.delta_sa,
|
|
equilibrium=ns_parser.equilibrium_sa,
|
|
optimize=ns_parser.optimize_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
value_short=ns_parser.short_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_ef(self, other_args):
|
|
"""Process ef command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="ef",
|
|
description="""This function plots random portfolios based on their
|
|
risk and returns and shows the efficient frontier.""",
|
|
)
|
|
parser.add_argument(
|
|
"-vs",
|
|
"--value-short",
|
|
dest="short_allocation",
|
|
help="Amount to allocate to portfolio in short positions",
|
|
type=float,
|
|
default=self.params["short_allocation"]
|
|
if "short_allocation" in self.params
|
|
else 0.0,
|
|
)
|
|
if other_args and "-" not in other_args[0][0]:
|
|
other_args.insert(0, "-n")
|
|
parser.add_argument(
|
|
"-n",
|
|
"--number-portfolios",
|
|
default=self.params["amount_portfolios"]
|
|
if "amount_portfolios" in self.params
|
|
else 100,
|
|
type=check_non_negative,
|
|
dest="amount_portfolios",
|
|
help="Number of portfolios to simulate",
|
|
)
|
|
parser.add_argument(
|
|
"-se",
|
|
"--seed",
|
|
default=self.params["random_seed"] if "random_seed" in self.params else 123,
|
|
type=check_non_negative,
|
|
dest="random_seed",
|
|
help="Seed used to generate random portfolios",
|
|
)
|
|
parser.add_argument(
|
|
"-t",
|
|
"--tangency",
|
|
action="store_true",
|
|
dest="tangency",
|
|
default=self.params["tangency"] if "tangency" in self.params else False,
|
|
help="Adds the optimal line with the risk-free asset",
|
|
)
|
|
parser.add_argument(
|
|
"--no_plot",
|
|
action="store_false",
|
|
dest="plot_tickers",
|
|
default=True,
|
|
help="Whether or not to plot the tickers for the assets provided",
|
|
)
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
optimizer_view.display_ef(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
value=ns_parser.long_allocation,
|
|
value_short=ns_parser.short_allocation,
|
|
n_portfolios=ns_parser.amount_portfolios,
|
|
seed=ns_parser.random_seed,
|
|
tangency=ns_parser.tangency,
|
|
plot_tickers=ns_parser.plot_tickers,
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_riskparity(self, other_args: List[str]):
|
|
"""Process riskparity command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="riskparity",
|
|
description="""Build a risk parity portfolio based on risk
|
|
budgeting approach""",
|
|
)
|
|
parser.add_argument(
|
|
"-rm",
|
|
"--risk-measure",
|
|
default=self.params["risk_measure"]
|
|
if "risk_measure" in self.params
|
|
else "MV",
|
|
dest="risk_measure",
|
|
help="""Risk measure used to optimize the portfolio. Possible values are:
|
|
'MV' : Variance
|
|
'MAD' : Mean Absolute Deviation
|
|
'MSV' : Semi Variance (Variance of negative returns)
|
|
'FLPM' : First Lower Partial Moment
|
|
'SLPM' : Second Lower Partial Moment
|
|
'CVaR' : Conditional Value at Risk
|
|
'EVaR' : Entropic Value at Risk
|
|
'UCI' : Ulcer Index of uncompounded returns
|
|
'CDaR' : Conditional Drawdown at Risk of uncompounded returns
|
|
'EDaR' : Entropic Drawdown at Risk of uncompounded returns
|
|
""",
|
|
choices=statics.RISK_PARITY_CHOICES,
|
|
metavar="RISK-MEASURE",
|
|
)
|
|
parser.add_argument(
|
|
"-rc",
|
|
"--risk-cont",
|
|
type=lambda s: [float(item) for item in s.split(",")],
|
|
default=self.params["risk_contribution"]
|
|
if "risk_contribution" in self.params
|
|
else None,
|
|
dest="risk_contribution",
|
|
help="vector of risk contribution constraint",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="RP_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_risk_parity(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_cont=ns_parser.risk_contribution,
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
target_return=ns_parser.target_return,
|
|
value=ns_parser.long_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_risk_parity(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_cont=ns_parser.risk_contribution_sa,
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_relriskparity(self, other_args: List[str]):
|
|
"""Process relriskparity command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="relriskparity",
|
|
description="""Build a relaxed risk parity portfolio based on
|
|
least squares approach""",
|
|
)
|
|
parser.add_argument(
|
|
"-ve",
|
|
"--version",
|
|
default=self.params["risk_parity_model"]
|
|
if "risk_parity_model" in self.params
|
|
else "A",
|
|
dest="risk_parity_model",
|
|
help="""version of relaxed risk parity model: Possible values are:
|
|
'A': risk parity without regularization and penalization constraints
|
|
'B': with regularization constraint but without penalization constraint
|
|
'C': with regularization and penalization constraints""",
|
|
choices=statics.REL_RISK_PARITY_CHOICES,
|
|
metavar="VERSION",
|
|
)
|
|
parser.add_argument(
|
|
"-rc",
|
|
"--risk-cont",
|
|
type=lambda s: [float(item) for item in s.split(",")],
|
|
default=self.params["risk_contribution"]
|
|
if "risk_contribution" in self.params
|
|
else None,
|
|
dest="risk_contribution",
|
|
help="Vector of risk contribution constraints",
|
|
)
|
|
parser.add_argument(
|
|
"-pf",
|
|
"--penal-factor",
|
|
type=float,
|
|
dest="penal_factor",
|
|
default=self.params["penal_factor"] if "penal_factor" in self.params else 1,
|
|
help="""The penalization factor of penalization constraints. Only
|
|
used with version 'C'.""",
|
|
)
|
|
parser.add_argument(
|
|
"-tr",
|
|
"--target-return",
|
|
dest="target_return",
|
|
default=self.params["target_return"]
|
|
if "target_return" in self.params
|
|
else -1,
|
|
type=float,
|
|
help="Constraint on minimum level of portfolio's return",
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
v=True,
|
|
name="RRP_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_rel_risk_parity(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
version=ns_parser.risk_parity_model,
|
|
risk_cont=ns_parser.risk_contribution,
|
|
penal_factor=ns_parser.penal_factor,
|
|
target_return=ns_parser.target_return,
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_rel_risk_parity(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
version=ns_parser.risk_parity_model_sa,
|
|
risk_cont=ns_parser.risk_contribution_sa,
|
|
penal_factor=ns_parser.penal_factor_sa,
|
|
target_return=ns_parser.target_return_sa,
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_hrp(self, other_args: List[str]):
|
|
"""Process hierarchical risk parity command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="hrp",
|
|
description="Builds a hierarchical risk parity portfolio",
|
|
)
|
|
parser.add_argument(
|
|
"-cd",
|
|
"--codependence",
|
|
default=self.params["co_dependence"]
|
|
if "co_dependence" in self.params
|
|
else "pearson",
|
|
dest="co_dependence",
|
|
help="""The codependence or similarity matrix used to build the
|
|
distance metric and clusters. Possible values are:
|
|
'pearson': pearson correlation matrix
|
|
'spearman': spearman correlation matrix
|
|
'abs_pearson': absolute value of pearson correlation matrix
|
|
'abs_spearman': absolute value of spearman correlation matrix
|
|
'distance': distance correlation matrix
|
|
'mutual_info': mutual information codependence matrix
|
|
'tail': tail index codependence matrix""",
|
|
choices=statics.CODEPENDENCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-rm",
|
|
"--risk-measure",
|
|
default=self.params["risk_measure"]
|
|
if "risk_measure" in self.params
|
|
else "MV",
|
|
dest="risk_measure",
|
|
help="""Risk measure used to optimize the portfolio. Possible values are:
|
|
'MV' : Variance
|
|
'MAD' : Mean Absolute Deviation
|
|
'GMD' : Gini Mean Difference
|
|
'MSV' : Semi Variance (Variance of negative returns)
|
|
'FLPM' : First Lower Partial Moment
|
|
'SLPM' : Second Lower Partial Moment
|
|
'VaR' : Value at Risk
|
|
'CVaR' : Conditional Value at Risk
|
|
'TG' : Tail Gini
|
|
'EVaR' : Entropic Value at Risk
|
|
'WR' : Worst Realization
|
|
'RG' : Range
|
|
'CVRG' : CVaR Range
|
|
'TGRG' : Tail Gini Range
|
|
'ADD' : Average Drawdown of uncompounded returns
|
|
'UCI' : Ulcer Index of uncompounded returns
|
|
'DaR' : Drawdown at Risk of uncompounded returns
|
|
'CDaR' : Conditional Drawdown at Risk of uncompounded returns
|
|
'EDaR' : Entropic Drawdown at Risk of uncompounded returns
|
|
'MDD' : Maximum Drawdown of uncompounded returns
|
|
'ADD_Rel' : Average Drawdown of compounded returns
|
|
'UCI_Rel' : Ulcer Index of compounded returns
|
|
'DaR_Rel' : Drawdown at Risk of compounded returns
|
|
'CDaR_Rel' : Conditional Drawdown at Risk of compounded returns
|
|
'EDaR_Rel' : Entropic Drawdown at Risk of compounded returns
|
|
'MDD_Rel' : Maximum Drawdown of compounded returns
|
|
""",
|
|
choices=statics.HCP_CHOICES,
|
|
metavar="RISK-MEASURE",
|
|
)
|
|
parser.add_argument(
|
|
"-as",
|
|
"--a-sim",
|
|
type=int,
|
|
default=self.params["cvar_simulations_losses"]
|
|
if "cvar_simulations_losses" in self.params
|
|
else 100,
|
|
dest="cvar_simulations_losses",
|
|
help="""Number of CVaRs used to approximate Tail Gini of losses.
|
|
The default is 100""",
|
|
)
|
|
parser.add_argument(
|
|
"-b",
|
|
"--beta",
|
|
type=float,
|
|
default=self.params["cvar_significance"]
|
|
if "cvar_significance" in self.params
|
|
else None,
|
|
dest="cvar_significance",
|
|
help="""Significance level of CVaR and Tail Gini of gains. If
|
|
empty it duplicates alpha""",
|
|
)
|
|
parser.add_argument(
|
|
"-bs",
|
|
"--b-sim",
|
|
type=int,
|
|
default=self.params["cvar_simulations_gains"]
|
|
if "cvar_simulations_gains" in self.params
|
|
else None,
|
|
dest="cvar_simulations_gains",
|
|
help="""Number of CVaRs used to approximate Tail Gini of gains.
|
|
If empty it duplicates a_sim value""",
|
|
)
|
|
parser.add_argument(
|
|
"-lk",
|
|
"--linkage",
|
|
default=self.params["linkage"] if "linkage" in self.params else "single",
|
|
dest="linkage",
|
|
help="Linkage method of hierarchical clustering",
|
|
choices=statics.LINKAGE_CHOICES,
|
|
metavar="LINKAGE",
|
|
)
|
|
parser.add_argument(
|
|
"-k",
|
|
type=int,
|
|
default=self.params["amount_clusters"]
|
|
if "amount_clusters" in self.params
|
|
else None,
|
|
dest="amount_clusters",
|
|
help="Number of clusters specified in advance",
|
|
)
|
|
parser.add_argument(
|
|
"-mk",
|
|
"--max-k",
|
|
type=int,
|
|
default=self.params["max_clusters"]
|
|
if "max_clusters" in self.params
|
|
else 10,
|
|
dest="max_clusters",
|
|
help="""Max number of clusters used by the two difference gap
|
|
statistic to find the optimal number of clusters. If k is
|
|
empty this value is used""",
|
|
)
|
|
parser.add_argument(
|
|
"-bi",
|
|
"--bins-info",
|
|
default=self.params["amount_bins"]
|
|
if "amount_bins" in self.params
|
|
else "KN",
|
|
dest="amount_bins",
|
|
help="Number of bins used to calculate the variation of information",
|
|
choices=statics.BINS_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-at",
|
|
"--alpha-tail",
|
|
type=float,
|
|
default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
|
|
dest="alpha_tail",
|
|
help="""Significance level for lower tail dependence index, only
|
|
used when when codependence value is 'tail' """,
|
|
)
|
|
parser.add_argument(
|
|
"-lo",
|
|
"--leaf-order",
|
|
default=self.params["leaf_order"] if "leaf_order" in self.params else True,
|
|
dest="leaf_order",
|
|
help="""Indicates if the cluster are ordered so that the distance
|
|
between successive leaves is minimal""",
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="_HRP",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_hrp(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
codependence=ns_parser.co_dependence.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
a_sim=ns_parser.cvar_simulations_losses,
|
|
beta=ns_parser.cvar_significance,
|
|
b_sim=ns_parser.cvar_simulations_gains,
|
|
linkage=ns_parser.linkage.lower(),
|
|
k=ns_parser.amount_clusters,
|
|
max_k=ns_parser.max_clusters,
|
|
bins_info=ns_parser.amount_bins.upper(),
|
|
alpha_tail=ns_parser.alpha_tail,
|
|
leaf_order=ns_parser.leaf_order,
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_hrp(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
codependence=ns_parser.co_dependence_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
a_sim=ns_parser.cvar_simulations_losses_sa,
|
|
beta=ns_parser.cvar_significance_sa,
|
|
b_sim=ns_parser.cvar_simulations_gains_sa,
|
|
linkage=ns_parser.linkage_sa.lower(),
|
|
k=ns_parser.amount_clusters_sa,
|
|
max_k=ns_parser.max_clusters_sa,
|
|
bins_info=ns_parser.amount_bins_sa.upper(),
|
|
alpha_tail=ns_parser.alpha_tail_sa,
|
|
leaf_order=ns_parser.leaf_order_sa,
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_herc(self, other_args: List[str]):
|
|
"""Process hierarchical equal risk contribution command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="herc",
|
|
description="Builds a hierarchical equal risk contribution portfolio",
|
|
)
|
|
parser.add_argument(
|
|
"-cd",
|
|
"--codependence",
|
|
default="pearson",
|
|
dest="co_dependence",
|
|
help="""The codependence or similarity matrix used to build the
|
|
distance metric and clusters. Possible values are:
|
|
'pearson': pearson correlation matrix
|
|
'spearman': spearman correlation matrix
|
|
'abs_pearson': absolute value of pearson correlation matrix
|
|
'abs_spearman': absolute value of spearman correlation matrix
|
|
'distance': distance correlation matrix
|
|
'mutual_info': mutual information codependence matrix
|
|
'tail': tail index codependence matrix""",
|
|
choices=statics.CODEPENDENCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-rm",
|
|
"--risk-measure",
|
|
default=self.params["risk_measure"]
|
|
if "risk_measure" in self.params
|
|
else "MV",
|
|
dest="risk_measure",
|
|
help="""Risk measure used to optimize the portfolio. Possible values are:
|
|
'MV' : Variance
|
|
'MAD' : Mean Absolute Deviation
|
|
'GMD' : Gini Mean Difference
|
|
'MSV' : Semi Variance (Variance of negative returns)
|
|
'FLPM' : First Lower Partial Moment
|
|
'SLPM' : Second Lower Partial Moment
|
|
'VaR' : Value at Risk
|
|
'CVaR' : Conditional Value at Risk
|
|
'TG' : Tail Gini
|
|
'EVaR' : Entropic Value at Risk
|
|
'WR' : Worst Realization
|
|
'RG' : Range
|
|
'CVRG' : CVaR Range
|
|
'TGRG' : Tail Gini Range
|
|
'ADD' : Average Drawdown of uncompounded returns
|
|
'UCI' : Ulcer Index of uncompounded returns
|
|
'DaR' : Drawdown at Risk of uncompounded returns
|
|
'CDaR' : Conditional Drawdown at Risk of uncompounded returns
|
|
'EDaR' : Entropic Drawdown at Risk of uncompounded returns
|
|
'MDD' : Maximum Drawdown of uncompounded returns
|
|
'ADD_Rel' : Average Drawdown of compounded returns
|
|
'UCI_Rel' : Ulcer Index of compounded returns
|
|
'DaR_Rel' : Drawdown at Risk of compounded returns
|
|
'CDaR_Rel' : Conditional Drawdown at Risk of compounded returns
|
|
'EDaR_Rel' : Entropic Drawdown at Risk of compounded returns
|
|
'MDD_Rel' : Maximum Drawdown of compounded returns
|
|
""",
|
|
choices=statics.HCP_CHOICES,
|
|
metavar="RISK-MEASURE",
|
|
)
|
|
parser.add_argument(
|
|
"-as",
|
|
"--a-sim",
|
|
type=int,
|
|
default=self.params["cvar_simulations_losses"]
|
|
if "cvar_simulations_losses" in self.params
|
|
else 100,
|
|
dest="cvar_simulations_losses",
|
|
help="""Number of CVaRs used to approximate Tail Gini of losses.
|
|
The default is 100""",
|
|
)
|
|
parser.add_argument(
|
|
"-b",
|
|
"--beta",
|
|
type=float,
|
|
default=self.params["cvar_significance"]
|
|
if "cvar_significance" in self.params
|
|
else None,
|
|
dest="cvar_significance",
|
|
help="""Significance level of CVaR and Tail Gini of gains. If
|
|
empty it duplicates alpha""",
|
|
)
|
|
parser.add_argument(
|
|
"-bs",
|
|
"--b-sim",
|
|
type=int,
|
|
default=self.params["cvar_simulations_gains"]
|
|
if "cvar_simulations_gains" in self.params
|
|
else None,
|
|
dest="cvar_simulations_gains",
|
|
help="""Number of CVaRs used to approximate Tail Gini of gains.
|
|
If empty it duplicates a_sim value""",
|
|
)
|
|
parser.add_argument(
|
|
"-lk",
|
|
"--linkage",
|
|
default=self.params["linkage"] if "linkage" in self.params else "single",
|
|
dest="linkage",
|
|
help="Linkage method of hierarchical clustering",
|
|
choices=statics.LINKAGE_CHOICES,
|
|
metavar="LINKAGE",
|
|
)
|
|
parser.add_argument(
|
|
"-k",
|
|
type=int,
|
|
default=self.params["amount_clusters"]
|
|
if "amount_clusters" in self.params
|
|
else None,
|
|
dest="amount_clusters",
|
|
help="Number of clusters specified in advance",
|
|
)
|
|
parser.add_argument(
|
|
"-mk",
|
|
"--max-k",
|
|
type=int,
|
|
default=self.params["max_clusters"]
|
|
if "max_clusters" in self.params
|
|
else 10,
|
|
dest="max_clusters",
|
|
help="""Max number of clusters used by the two difference gap
|
|
statistic to find the optimal number of clusters. If k is
|
|
empty this value is used""",
|
|
)
|
|
parser.add_argument(
|
|
"-bi",
|
|
"--bins-info",
|
|
default=self.params["amount_bins"]
|
|
if "amount_bins" in self.params
|
|
else "KN",
|
|
dest="amount_bins",
|
|
help="Number of bins used to calculate the variation of information",
|
|
choices=statics.BINS_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-at",
|
|
"--alpha-tail",
|
|
type=float,
|
|
default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
|
|
dest="alpha_tail",
|
|
help="""Significance level for lower tail dependence index, only
|
|
used when when codependence value is 'tail' """,
|
|
)
|
|
parser.add_argument(
|
|
"-lo",
|
|
"--leaf-order",
|
|
default=self.params["leaf_order"] if "leaf_order" in self.params else True,
|
|
dest="leaf_order",
|
|
help="""Indicates if the cluster are ordered so that the distance
|
|
between successive leaves is minimal""",
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="HERC_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_herc(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
codependence=ns_parser.co_dependence.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
alpha=ns_parser.significance_level,
|
|
a_sim=ns_parser.cvar_simulations_losses,
|
|
beta=ns_parser.cvar_significance,
|
|
b_sim=ns_parser.cvar_simulations_gains,
|
|
linkage=ns_parser.linkage.lower(),
|
|
k=ns_parser.amount_clusters,
|
|
max_k=ns_parser.max_clusters,
|
|
bins_info=ns_parser.amount_bins.upper(),
|
|
alpha_tail=ns_parser.alpha_tail,
|
|
leaf_order=ns_parser.leaf_order,
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_herc(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
codependence=ns_parser.co_dependence_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
a_sim=ns_parser.cvar_simulations_losses_sa,
|
|
beta=ns_parser.cvar_significance_sa,
|
|
b_sim=ns_parser.cvar_simulations_gains_sa,
|
|
linkage=ns_parser.linkage_sa.lower(),
|
|
k=ns_parser.amount_clusters_sa,
|
|
max_k=ns_parser.max_clusters_sa,
|
|
bins_info=ns_parser.amount_bins_sa.upper(),
|
|
alpha_tail=ns_parser.alpha_tail_sa,
|
|
leaf_order=ns_parser.leaf_order_sa,
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_nco(self, other_args: List[str]):
|
|
"""Process nested clustered optimization command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="nco",
|
|
description="Builds a nested clustered optimization portfolio",
|
|
)
|
|
parser.add_argument(
|
|
"-cd",
|
|
"--codependence",
|
|
default=self.params["co_dependence"]
|
|
if "co_dependence" in self.params
|
|
else "pearson",
|
|
dest="co_dependence",
|
|
help="""The codependence or similarity matrix used to build the
|
|
distance metric and clusters. Possible values are:
|
|
'pearson': pearson correlation matrix
|
|
'spearman': spearman correlation matrix
|
|
'abs_pearson': absolute value of pearson correlation matrix
|
|
'abs_spearman': absolute value of spearman correlation matrix
|
|
'distance': distance correlation matrix
|
|
'mutual_info': mutual information codependence matrix
|
|
'tail': tail index codependence matrix""",
|
|
choices=statics.CODEPENDENCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-cv",
|
|
"--covariance",
|
|
default=self.params["covariance"]
|
|
if "covariance" in self.params
|
|
else "hist",
|
|
dest="covariance",
|
|
help="""Method used to estimate covariance matrix. Possible values are
|
|
'hist': historical method
|
|
'ewma1': exponential weighted moving average with adjust=True
|
|
'ewma2': exponential weighted moving average with adjust=False
|
|
'ledoit': Ledoit and Wolf shrinkage method
|
|
'oas': oracle shrinkage method
|
|
'shrunk': scikit-learn shrunk method
|
|
'gl': graphical lasso method
|
|
'jlogo': j-logo covariance
|
|
'fixed': takes average of eigenvalues above max Marchenko Pastour limit
|
|
'spectral': makes zero eigenvalues above max Marchenko Pastour limit
|
|
'shrink': Lopez de Prado's book shrinkage method
|
|
""",
|
|
choices=statics.COVARIANCE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--objective",
|
|
default=self.params["objective"]
|
|
if "objective" in self.params
|
|
else "MinRisk",
|
|
dest="objective",
|
|
help="Objective function used to optimize the portfolio",
|
|
choices=statics.NCO_OBJECTIVE_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-ra",
|
|
"--risk-aversion",
|
|
type=float,
|
|
dest="risk_aversion",
|
|
default=self.params["long_allocation"]
|
|
if "long_allocation" in self.params
|
|
else 1,
|
|
help="Risk aversion parameter",
|
|
)
|
|
parser.add_argument(
|
|
"-lk",
|
|
"--linkage",
|
|
default=self.params["linkage"] if "linkage" in self.params else "single",
|
|
dest="linkage",
|
|
help="Linkage method of hierarchical clustering",
|
|
choices=statics.LINKAGE_CHOICES,
|
|
metavar="LINKAGE",
|
|
)
|
|
parser.add_argument(
|
|
"-k",
|
|
type=int,
|
|
default=self.params["amount_clusters"]
|
|
if "amount_clusters" in self.params
|
|
else None,
|
|
dest="amount_clusters",
|
|
help="Number of clusters specified in advance",
|
|
)
|
|
parser.add_argument(
|
|
"-mk",
|
|
"--max-k",
|
|
type=int,
|
|
default=self.params["max_clusters"]
|
|
if "max_clusters" in self.params
|
|
else 10,
|
|
dest="max_clusters",
|
|
help="""Max number of clusters used by the two difference gap
|
|
statistic to find the optimal number of clusters. If k is
|
|
empty this value is used""",
|
|
)
|
|
parser.add_argument(
|
|
"-bi",
|
|
"--bins-info",
|
|
default=self.params["amount_bins"]
|
|
if "amount_bins" in self.params
|
|
else "KN",
|
|
dest="amount_bins",
|
|
help="Number of bins used to calculate the variation of information",
|
|
choices=statics.BINS_CHOICES,
|
|
)
|
|
parser.add_argument(
|
|
"-at",
|
|
"--alpha-tail",
|
|
type=float,
|
|
default=self.params["alpha_tail"] if "alpha_tail" in self.params else 0.05,
|
|
dest="alpha_tail",
|
|
help="""Significance level for lower tail dependence index, only
|
|
used when when codependence value is 'tail' """,
|
|
)
|
|
parser.add_argument(
|
|
"-lo",
|
|
"--leaf-order",
|
|
action="store_true",
|
|
default=self.params["leaf_order"] if "leaf_order" in self.params else True,
|
|
dest="leaf_order",
|
|
help="""indicates if the cluster are ordered so that the distance
|
|
between successive leaves is minimal""",
|
|
)
|
|
parser.add_argument(
|
|
"-de",
|
|
"--d-ewma",
|
|
type=float,
|
|
default=self.params["smoothing_factor_ewma"]
|
|
if "smoothing_factor_ewma" in self.params
|
|
else 0.94,
|
|
dest="smoothing_factor_ewma",
|
|
help="Smoothing factor for ewma estimators",
|
|
)
|
|
|
|
parser = self.po_parser(
|
|
parser,
|
|
rm=True,
|
|
mt=True,
|
|
ct=True,
|
|
p=True,
|
|
s=True,
|
|
e=True,
|
|
lr=True,
|
|
freq=True,
|
|
mn=True,
|
|
th=True,
|
|
r=True,
|
|
a=True,
|
|
v=True,
|
|
name="NCO_",
|
|
)
|
|
ns_parser = self.parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if len(self.tickers) < 2:
|
|
console.print(
|
|
"Please have at least 2 loaded tickers to calculate weights.\n"
|
|
)
|
|
return
|
|
|
|
table = True
|
|
if "historic_period_sa" in vars(ns_parser):
|
|
table = False
|
|
|
|
console.print(
|
|
"[yellow]Optimization can take time. Please be patient...\n[/yellow]"
|
|
)
|
|
|
|
weights = optimizer_view.display_nco(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period,
|
|
start_date=ns_parser.start_period,
|
|
end_date=ns_parser.end_period,
|
|
log_returns=ns_parser.log_returns,
|
|
freq=ns_parser.return_frequency,
|
|
maxnan=ns_parser.max_nan,
|
|
threshold=ns_parser.threshold_value,
|
|
method=ns_parser.nan_fill_method,
|
|
codependence=ns_parser.co_dependence.lower(),
|
|
covariance=ns_parser.covariance.lower(),
|
|
objective=ns_parser.objective.lower(),
|
|
risk_measure=ns_parser.risk_measure.lower(),
|
|
risk_free_rate=ns_parser.risk_free,
|
|
risk_aversion=ns_parser.risk_aversion,
|
|
alpha=ns_parser.significance_level,
|
|
linkage=ns_parser.linkage.lower(),
|
|
k=ns_parser.amount_clusters,
|
|
max_k=ns_parser.max_clusters,
|
|
bins_info=ns_parser.amount_bins.upper(),
|
|
alpha_tail=ns_parser.alpha_tail,
|
|
leaf_order=ns_parser.leaf_order,
|
|
d_ewma=ns_parser.smoothing_factor_ewma,
|
|
value=ns_parser.long_allocation,
|
|
table=table,
|
|
)
|
|
|
|
self.portfolios[ns_parser.name.upper()] = weights
|
|
self.count += 1
|
|
self.update_runtime_choices()
|
|
|
|
if table is False:
|
|
weights_sa = optimizer_view.display_nco(
|
|
symbols=self.tickers,
|
|
interval=ns_parser.historic_period_sa,
|
|
start_date=ns_parser.start_period_sa,
|
|
end_date=ns_parser.end_period_sa,
|
|
log_returns=ns_parser.log_returns_sa,
|
|
freq=ns_parser.return_frequency_sa,
|
|
maxnan=ns_parser.max_nan_sa,
|
|
threshold=ns_parser.threshold_value_sa,
|
|
method=ns_parser.nan_fill_method_sa,
|
|
codependence=ns_parser.co_dependence_sa.lower(),
|
|
covariance=ns_parser.covariance_sa.lower(),
|
|
objective=ns_parser.objective_sa.lower(),
|
|
risk_measure=ns_parser.risk_measure_sa.lower(),
|
|
risk_free_rate=ns_parser.risk_free_sa,
|
|
risk_aversion=ns_parser.risk_aversion_sa,
|
|
alpha=ns_parser.significance_level_sa,
|
|
linkage=ns_parser.linkage_sa.lower(),
|
|
k=ns_parser.amount_clusters_sa,
|
|
max_k=ns_parser.max_clusters_sa,
|
|
bins_info=ns_parser.amount_bins_sa.upper(),
|
|
alpha_tail=ns_parser.alpha_tail_sa,
|
|
leaf_order=ns_parser.leaf_order_sa,
|
|
d_ewma=ns_parser.smoothing_factor_ewma_sa,
|
|
value=ns_parser.long_allocation_sa,
|
|
table=table,
|
|
)
|
|
|
|
console.print("")
|
|
optimizer_view.display_weights_sa(
|
|
weights=weights, weights_sa=weights_sa
|
|
)
|
|
|
|
if not ns_parser.categories:
|
|
categories = ["ASSET_CLASS", "COUNTRY", "SECTOR", "INDUSTRY"]
|
|
else:
|
|
categories = ns_parser.categories
|
|
|
|
for category in categories:
|
|
optimizer_view.display_categories_sa(
|
|
weights=weights,
|
|
weights_sa=weights_sa,
|
|
categories=self.categories,
|
|
column=category,
|
|
title="Category - " + category.title(),
|
|
)
|