Skip to content

Commit

Permalink
Fix Various portfolio/po issues (#3286)
Browse files Browse the repository at this point in the history
* Allow for loading without -f flag

* Massive upgrade to params file read/write

* Better handling empty dfs and none in prompt toolkit

* Fix tr typing and handle none error

* Added options

* Fixed tests

Co-authored-by: James Maslek <jmaslek11@gmail.com>
  • Loading branch information
colin99d and jmaslek authored Nov 4, 2022
1 parent 2b517bc commit 86f2e56
Show file tree
Hide file tree
Showing 13 changed files with 395 additions and 342 deletions.
20 changes: 9 additions & 11 deletions openbb_terminal/custom_prompt_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ def get_completions(
if "-" not in text:
completer = self.options.get(first_term)
else:
if cmd in self.options:
if cmd in self.options and self.options.get(cmd):
completer = self.options.get(cmd).options.get(first_term) # type: ignore
else:
completer = self.options.get(first_term)
Expand Down Expand Up @@ -378,16 +378,14 @@ def get_completions(
if k not in self.flags_processed
}

if (
cmd
and cmd in self.options.keys()
and [
text in val
for val in [
f"{cmd} {opt}" for opt in self.options.get(cmd).options.keys() # type: ignore
]
]
):
command = self.options.get(cmd)
if command:
options = command.options # type: ignore
else:
options = {}
command_options = [f"{cmd} {opt}" for opt in options.keys()]
text_list = [text in val for val in command_options]
if cmd and cmd in self.options.keys() and text_list:
completer = WordCompleter(
list(self.options.get(cmd).options.keys()), # type: ignore
ignore_case=self.ignore_case,
Expand Down
4 changes: 1 addition & 3 deletions openbb_terminal/miscellaneous/i18n/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -491,10 +491,8 @@ en:
crypto/disc/games: top blockchain games
crypto/disc/dapps: top decentralized apps
crypto/disc/dex: top decentralized exchanges
crypto/ov/global: global crypto market info
crypto/ov/defi: global DeFi market info
crypto/ov/stables: stablecoins
crypto/ov/exchanges: top crypto exchanges
crypto/ov/exrates: coin exchange rates
crypto/ov/indexes: crypto indexes
crypto/ov/derivatives: crypto derivatives
Expand Down Expand Up @@ -968,7 +966,7 @@ en:
portfolio/po/_loaded: Portfolio loaded
portfolio/po/_tickers: Tickers
portfolio/po/_categories: Categories
portfolio/po/file: select portfolio parameter file
portfolio/po/params/load: select portfolio parameter file
portfolio/po/params: specify and show portfolio risk parameters
portfolio/po/_parameter: Parameter file
portfolio/po/_mean_risk_optimization_: Mean Risk Optimization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,11 @@ def get_mean_risk_portfolio(
threshold=threshold,
method=method,
)
if stock_returns.empty:
console.print(
"[red]Not enough data points in range to run calculations.[/red]\n"
)
return {}, pd.DataFrame()

risk_free_rate = risk_free_rate / time_factor[freq.upper()]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,9 @@ def display_max_sharpe(
value_short=value_short,
)

if stock_returns is None or stock_returns.empty:
return {}

if weights is None:
console.print("\n", "There is no solution with these parameters")
return {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@
# pylint: disable=C0302, no-else-return

import argparse
import configparser
import logging
import os
from pathlib import Path
from typing import List, Optional

from openbb_terminal.custom_prompt_toolkit import NestedCompleter

from openbb_terminal.core.config.paths import MISCELLANEOUS_DIRECTORY
from openbb_terminal import feature_flags as gtff
from openbb_terminal.decorators import log_start_end
from openbb_terminal.helper_funcs import log_and_raise
from openbb_terminal.menu import session
from openbb_terminal.parent_classes import BaseController
from openbb_terminal.portfolio.portfolio_optimization.parameters import params_view
from openbb_terminal.portfolio.portfolio_optimization.parameters.params_view import (
from openbb_terminal.portfolio.portfolio_optimization.parameters import params_helpers
from openbb_terminal.portfolio.portfolio_optimization.parameters.params_statics import (
AVAILABLE_OPTIONS,
DEFAULT_PARAMETERS,
DEFAULT_BOOL,
Expand All @@ -30,29 +26,12 @@
logger = logging.getLogger(__name__)


def check_save_file(file: str) -> str:
"""Argparse type to check parameter file to be saved"""
if file == "defaults.ini":
log_and_raise(
argparse.ArgumentTypeError(
"Cannot overwrite defaults.ini file, please save with a different name"
)
)
else:
if not file.endswith(".ini") and not file.endswith(".xlsx"):
log_and_raise(
argparse.ArgumentTypeError("File to be saved needs to be .ini or .xlsx")
)

return file


class ParametersController(BaseController):
"""Portfolio Optimization Parameters Controller class"""

CHOICES_COMMANDS = [
"set",
"file",
"load",
"save",
"new",
"clear",
Expand Down Expand Up @@ -80,56 +59,32 @@ class ParametersController(BaseController):

current_model = ""
current_file = ""
params = configparser.RawConfigParser()

def __init__(
self,
file: str,
queue: List[str] = None,
params=None,
params: Optional[dict] = None,
current_model=None,
):
"""Constructor"""
super().__init__(queue)

self.params: dict = params if params else {}
self.current_file = file
self.current_model = current_model
self.description: Optional[str] = None
self.DEFAULT_PATH = os.path.abspath(
MISCELLANEOUS_DIRECTORY / "portfolio_examples" / "optimization"
)

self.file_types = ["xlsx", "ini"]
self.DATA_FILES = {
filepath.name: filepath
for file_type in self.file_types
for filepath in Path(self.DEFAULT_PATH).rglob(f"*.{file_type}")
if filepath.is_file()
}

if params:
self.params = params
else:
pass
# TODO: Enable .ini reading
# self.params.read(
# os.path.join(
# self.DEFAULT_PATH,
# self.current_file if self.current_file else "defaults.ini",
# )
# )
# self.params.optionxform = str # type: ignore
# self.params = self.params["OPENBB"]
self.DATA_FILES = params_helpers.load_data_files()

if session and gtff.USE_PROMPT_TOOLKIT:
choices: dict = {c: {} for c in self.controller_choices}
choices["set"] = {c: None for c in self.models}
choices["set"]["--model"] = {c: None for c in self.models}
choices["set"]["-m"] = "--model"
choices["arg"] = {c: None for c in AVAILABLE_OPTIONS}
choices["file"] = {c: {} for c in self.DATA_FILES}
choices["file"]["--file"] = {c: {} for c in self.DATA_FILES}
choices["file"]["-f"] = "--file"
choices["load"] = {c: {} for c in self.DATA_FILES}
choices["load"]["--file"] = {c: {} for c in self.DATA_FILES}
choices["load"]["-f"] = "--file"
choices["save"]["--file"] = None
choices["save"]["-f"] = "--file"
choices["arg"] = {
Expand All @@ -145,7 +100,7 @@ def print_help(self):
mt = MenuText("portfolio/po/params/")
mt.add_param("_loaded", self.current_file)
mt.add_raw("\n")
mt.add_cmd("file")
mt.add_cmd("load")
mt.add_cmd("save")
mt.add_raw("\n")
mt.add_param("_model", self.current_model or "")
Expand Down Expand Up @@ -190,7 +145,7 @@ def custom_reset(self):
return []

@log_start_end(log=logger)
def call_file(self, other_args: List[str]):
def call_load(self, other_args: List[str]):
"""Process load command"""
parser = argparse.ArgumentParser(
add_help=False,
Expand All @@ -208,6 +163,8 @@ def call_file(self, other_args: List[str]):
help="Parameter file to be used",
)

if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-f")
ns_parser = self.parse_known_args_and_warn(parser, other_args)

if ns_parser:
Expand Down Expand Up @@ -235,31 +192,17 @@ def call_save(self, other_args: List[str]):
"-f",
"--file",
required=True,
type=check_save_file,
type=params_helpers.check_save_file,
dest="file",
help="Filename to be saved",
)

if other_args and "-" not in other_args[0][0]:
other_args.insert(0, "-f")
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
if ns_parser.file.endswith(".ini"):
# Create file if it does not exist
filepath = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
ns_parser.file,
)
)
Path(filepath)

with open(filepath, "w") as configfile:
self.params.write(configfile)

self.current_file = ns_parser.file
console.print()

elif ns_parser.file.endswith(".xlsx"):
console.print("It is not yet possible to save to .xlsx")
self.current_file = str(params_view.save_file(ns_parser.file, self.params))
console.print()

@log_start_end(log=logger)
def call_clear(self, other_args: List[str]):
Expand All @@ -272,11 +215,6 @@ def call_clear(self, other_args: List[str]):
)
ns_parser = self.parse_known_args_and_warn(parser, other_args)
if ns_parser:
if not self.current_file:
console.print(
"[red]Load portfolio risk parameters first using `file`.\n[/red]"
)
return
self.current_model = ""
console.print("")

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import argparse
from typing import Dict
from pathlib import Path
from openbb_terminal.helper_funcs import log_and_raise
from openbb_terminal.core.config import paths


def check_save_file(file: str) -> str:
"""Argparse type to check parameter file to be saved"""
if file == "defaults.ini":
log_and_raise(
argparse.ArgumentTypeError(
"Cannot overwrite defaults.ini file, please save with a different name"
)
)
else:
if not file.endswith(".ini"):
log_and_raise(
argparse.ArgumentTypeError("File to be saved needs to be .ini")
)

return file


def load_data_files() -> Dict[str, Path]:
"""Loads files from the misc directory and from the user's custom exports
Returns
----------
Dict[str, Path]
The dictionary of filenames and their paths
"""
default_path = paths.MISCELLANEOUS_DIRECTORY / "portfolio_examples" / "optimization"
custom_exports = paths.USER_EXPORTS_DIRECTORY / "portfolio"
data_files = {}
for directory in [default_path, custom_exports]:
for file_type in ["xlsx", "ini"]:
for filepath in Path(directory).rglob(f"*.{file_type}"):
if filepath.is_file():
data_files[filepath.name] = filepath

return data_files
Loading

0 comments on commit 86f2e56

Please sign in to comment.