mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-31 14:31:20 +00:00
654 lines
23 KiB
Python
654 lines
23 KiB
Python
"""Portfolio Controller"""
|
|
__docformat__ = "numpy"
|
|
|
|
import argparse
|
|
import logging
|
|
import os
|
|
from os import listdir
|
|
from os.path import isfile, join
|
|
from typing import Dict, List, Union
|
|
|
|
import pandas as pd
|
|
from prompt_toolkit.completion import NestedCompleter
|
|
|
|
from gamestonk_terminal import feature_flags as gtff
|
|
from gamestonk_terminal.decorators import log_start_end
|
|
from gamestonk_terminal.helper_funcs import (
|
|
EXPORT_ONLY_FIGURES_ALLOWED,
|
|
check_positive,
|
|
check_positive_float,
|
|
parse_known_args_and_warn,
|
|
print_rich_table,
|
|
valid_date,
|
|
)
|
|
from gamestonk_terminal.menu import session
|
|
from gamestonk_terminal.parent_classes import BaseController
|
|
from gamestonk_terminal.portfolio import portfolio_model, portfolio_view
|
|
from gamestonk_terminal.portfolio.portfolio_optimization import po_controller
|
|
from gamestonk_terminal.rich_config import console
|
|
|
|
# pylint: disable=R1710,E1101,C0415,W0212
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
portfolios_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "portfolios")
|
|
|
|
|
|
class PortfolioController(BaseController):
|
|
"""Portfolio Controller class"""
|
|
|
|
CHOICES_COMMANDS = [
|
|
"load",
|
|
"save",
|
|
"init",
|
|
"show",
|
|
"add",
|
|
"build",
|
|
"ar",
|
|
"rmr",
|
|
"al",
|
|
"dd",
|
|
"rolling",
|
|
"var",
|
|
"es",
|
|
]
|
|
CHOICES_MENUS = [
|
|
"bro",
|
|
"po",
|
|
"pa",
|
|
]
|
|
distributions = ["laplace", "student_t", "logistic", "normal"]
|
|
PATH = "/portfolio/"
|
|
|
|
def __init__(self, queue: List[str] = None):
|
|
"""Constructor"""
|
|
super().__init__(queue)
|
|
|
|
self.portfolio = pd.DataFrame(
|
|
columns=[
|
|
"Name",
|
|
"Type",
|
|
"Quantity",
|
|
"Date",
|
|
"Price",
|
|
"Fees",
|
|
"Premium",
|
|
"Side",
|
|
]
|
|
)
|
|
|
|
self.portfolio_name = ""
|
|
self.portlist: List[str] = os.listdir(portfolios_path)
|
|
self.portfolio = portfolio_model.Portfolio()
|
|
|
|
if session and gtff.USE_PROMPT_TOOLKIT:
|
|
choices: dict = {c: {} for c in self.controller_choices}
|
|
choices["load"] = {c: None for c in self.portlist}
|
|
choices["save"] = {c: None for c in self.portlist}
|
|
self.choices = choices
|
|
self.completer = NestedCompleter.from_nested_dict(choices)
|
|
|
|
def print_help(self):
|
|
"""Print help"""
|
|
# NOTE: See comment at `call_pa` definition
|
|
# > pa portfolio analysis, \t\t analyse portfolios
|
|
help_text = f"""[menu]
|
|
> bro brokers holdings, \t\t supports: robinhood, ally, degiro, coinbase
|
|
> po portfolio optimization, \t optimal portfolio weights from pyportfolioopt[/menu]
|
|
|
|
[info]Portfolio:[/info][cmds]
|
|
load load data into the portfolio
|
|
save save your portfolio for future use
|
|
show show existing transactions
|
|
add add a security to your portfolio
|
|
init initialize empty portfolio
|
|
build build portfolio from list of tickers and weights[/cmds]
|
|
[info]
|
|
Loaded:[/info] {self.portfolio_name or None}
|
|
|
|
[info]Graphs:[/info][cmds]
|
|
rolling display rolling metrics of portfolio and benchmark
|
|
rmr graph your returns versus the market's returns
|
|
dd display portfolio drawdown
|
|
al display allocation to given assets over period[/cmds]
|
|
|
|
[info]Risk Metrics:[/info][cmds]
|
|
var display value at risk
|
|
es display expected shortfall[/cmds]
|
|
"""
|
|
# TODO: Clean up the reports inputs
|
|
# TODO: Edit the allocation to allow the different asset classes
|
|
# [info]Reports:[/info]
|
|
# ar annual report for performance of a given portfolio
|
|
console.print(text=help_text, menu="Portfolio")
|
|
|
|
@log_start_end(log=logger)
|
|
def call_bro(self, _):
|
|
"""Process bro command"""
|
|
from gamestonk_terminal.portfolio.brokers.bro_controller import (
|
|
BrokersController,
|
|
)
|
|
|
|
self.queue = self.load_class(BrokersController, self.queue)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_po(self, _):
|
|
"""Process po command"""
|
|
if self.portfolio.empty:
|
|
tickers = []
|
|
else:
|
|
tickers = (
|
|
self.portfolio._stock_tickers
|
|
+ self.portfolio._etf_tickers
|
|
+ self.portfolio._crypto_tickers
|
|
)
|
|
self.queue = self.load_class(
|
|
po_controller.PortfolioOptimization, tickers, self.queue
|
|
)
|
|
|
|
# BUG: The commands in pa menu throw errors. First one says that it's related to
|
|
# string formatting and the second one has something to do with None being used
|
|
# instead of [] in the queue (assumption) what throws errors on the logger.
|
|
# TODO: This submenu is disabled until the bug is fixed.
|
|
# def call_pa(self, _):
|
|
# """Process pa command"""
|
|
# from gamestonk_terminal.portfolio.portfolio_analysis import pa_controller
|
|
#
|
|
# self.queue = self.queue = self.load_class(
|
|
# pa_controller.PortfolioAnalysis, self.queue
|
|
# )
|
|
|
|
@log_start_end(log=logger)
|
|
def call_init(self, other_args: List[str]):
|
|
"""Process reset command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="init",
|
|
description="Re-initialize with empty portfolio.",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
self.portfolio = portfolio_model.Portfolio()
|
|
console.print()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_load(self, other_args: List[str]):
|
|
"""Process load command"""
|
|
path = os.path.dirname(os.path.abspath(__file__))
|
|
path = os.path.abspath(os.path.join(path, "portfolios"))
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="load",
|
|
description="Load your portfolio",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
type=str,
|
|
choices=[f for f in listdir(path) if isfile(join(path, f))],
|
|
dest="name",
|
|
required="-h" not in other_args,
|
|
help="Name of file to be saved",
|
|
)
|
|
if other_args:
|
|
if "-n" not in other_args and "-h" not in other_args:
|
|
other_args.insert(0, "-n")
|
|
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
self.portfolio = portfolio_model.Portfolio.from_csv(
|
|
os.path.join(portfolios_path, ns_parser.name)
|
|
)
|
|
self.portfolio_name = ns_parser.name
|
|
console.print()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_save(self, other_args: List[str]):
|
|
"""Process save command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="save",
|
|
description="Save your portfolio",
|
|
)
|
|
parser.add_argument(
|
|
"-n",
|
|
"--name",
|
|
type=str,
|
|
dest="name",
|
|
required="-h" not in other_args,
|
|
help="Name of file to be saved",
|
|
)
|
|
if other_args and "-n" not in other_args and "-h" not in other_args:
|
|
other_args.insert(0, "-n")
|
|
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if (
|
|
".csv" in ns_parser.name
|
|
or ".xlsx" in ns_parser.name
|
|
or ".json" in ns_parser.name
|
|
):
|
|
portfolio_model.save_df(self.portfolio.trades, ns_parser.name)
|
|
else:
|
|
console.print(
|
|
"Please submit as 'filename.filetype' with filetype being csv, xlsx, or json\n"
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_show(self, _):
|
|
"""Process show command"""
|
|
if self.portfolio.empty:
|
|
logger.warning("No portfolio loaded")
|
|
console.print("[red]No portfolio loaded.[/red]\n")
|
|
return
|
|
print_rich_table(self.portfolio.trades, show_index=False)
|
|
console.print()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_add(self, other_args: List[str]):
|
|
"""Process add command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="add",
|
|
description="Adds an item to your portfolio",
|
|
)
|
|
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if not ns_parser:
|
|
return
|
|
|
|
console.print()
|
|
inputs: Dict[str, Union[str, float, int]] = {}
|
|
type_ = input("Type (stock, cash): \n")
|
|
if type_ not in ["stock", "cash"]:
|
|
logger.warning("Currently only stocks or cash supported.")
|
|
console.print("[red]Currently only stocks or cash supported.[/red]\n")
|
|
type_ = input("Type (stock, cash): \n")
|
|
if type_ not in ["stock", "cash"]:
|
|
logger.error("Two unsuccessful attempts. Exiting add")
|
|
console.print("[red]Two unsuccessful attempts. Exiting add.[/red]\n")
|
|
return
|
|
|
|
inputs["Type"] = type_.lower()
|
|
action = input("Action: (buy, sell, deposit, withdraw): \n").lower()
|
|
|
|
if type_ == "cash":
|
|
if action not in ["deposit", "withdraw"]:
|
|
console.print("Cash can only be deposit or withdraw\n")
|
|
action = input("Action: (buy, sell, deposit, withdraw): \n").lower()
|
|
if action not in ["deposit", "withdraw"]:
|
|
logger.error("Two unsuccessful attempts. Exiting add")
|
|
console.print(
|
|
"[red]Two unsuccessful attempts. Exiting add.[/red]\n"
|
|
)
|
|
return
|
|
|
|
elif type_ == "stock":
|
|
if action not in ["buy", "sell"]:
|
|
console.print("Stock can only be buy or sell\n")
|
|
if action not in ["buy", "sell"]:
|
|
logger.error("Two unsuccessful attempts. Exiting add")
|
|
console.print(
|
|
"[red]Two unsuccessful attempts. Exiting add.[/red]\n"
|
|
)
|
|
return
|
|
|
|
inputs["Side"] = action.lower()
|
|
inputs["Name"] = input("Name (ticker or cash [if depositing cash]):\n")
|
|
inputs["Date"] = valid_date(input("Purchase date (YYYY-MM-DD): \n")).strftime(
|
|
"%Y-%m-%d"
|
|
)
|
|
inputs["Quantity"] = float(input("Quantity: \n"))
|
|
inputs["Price"] = float(input("Price per share: \n"))
|
|
inputs["Fees"] = float(input("Fees: \n"))
|
|
inputs["Premium"] = ""
|
|
if self.portfolio.empty:
|
|
self.portfolio = portfolio_model.Portfolio(
|
|
pd.DataFrame.from_dict(inputs, orient="index").T
|
|
)
|
|
console.print(
|
|
f"Portfolio successfully initiialized with {inputs['Name']}.\n"
|
|
)
|
|
return
|
|
self.portfolio.add_trade(inputs)
|
|
|
|
console.print(f"{inputs['Name']} successfully added\n")
|
|
|
|
@log_start_end(log=logger)
|
|
def call_build(self, other_args: List[str]):
|
|
"""Process build command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="build",
|
|
description="Build portfolio from list of tickers and weights",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"--start",
|
|
help="Start date.",
|
|
dest="start",
|
|
default="2021-01-04",
|
|
type=valid_date,
|
|
required="-h" not in other_args,
|
|
)
|
|
parser.add_argument(
|
|
"-t",
|
|
"--tickers",
|
|
type=str,
|
|
help="List of symbols separated by commas (i.e AAPL,BTC,DOGE,SPY....)",
|
|
dest="tickers",
|
|
required="-h" not in other_args,
|
|
)
|
|
parser.add_argument(
|
|
"-c",
|
|
"--class",
|
|
help="Asset class (stock, crypto, etf), separated by commas.",
|
|
dest="classes",
|
|
type=str,
|
|
required="-h" not in other_args,
|
|
)
|
|
parser.add_argument(
|
|
"-w",
|
|
"--weights",
|
|
help="List of weights, separated by comma",
|
|
type=str,
|
|
dest="weights",
|
|
required="-h" not in other_args,
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--amount",
|
|
help="Amount to allocate initially.",
|
|
dest="amount",
|
|
default=100_000,
|
|
type=check_positive,
|
|
)
|
|
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
list_of_tickers = ns_parser.tickers.split(",")
|
|
types = ns_parser.classes.split(",")
|
|
weights = [float(w) for w in ns_parser.weights.split(",")]
|
|
self.portfolio = portfolio_model.Portfolio.from_custom_inputs_and_weights(
|
|
start_date=ns_parser.start.strftime("%Y-%m-%d"),
|
|
list_of_symbols=list_of_tickers,
|
|
list_of_weights=weights,
|
|
list_of_types=types,
|
|
amount=ns_parser.amount,
|
|
)
|
|
console.print()
|
|
|
|
@log_start_end(log=logger)
|
|
def call_al(self, other_args: List[str]):
|
|
"""Process al command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="al",
|
|
description="Display allocation",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(
|
|
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
|
|
)
|
|
if ns_parser:
|
|
if self.portfolio.empty:
|
|
console.print("[red]No portfolio loaded.[/red]\n")
|
|
return
|
|
portfolio_view.display_allocation(self.portfolio, ns_parser.export)
|
|
|
|
# def call_ar(self, other_args: List[str]):
|
|
# """Process ar command"""
|
|
# parser = argparse.ArgumentParser(
|
|
# add_help=False,
|
|
# formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
# prog="ar",
|
|
# description="Create an annual report based on given portfolio",
|
|
# )
|
|
# parser.add_argument(
|
|
# "-m",
|
|
# "--market",
|
|
# type=str,
|
|
# dest="market",
|
|
# default="SPY",
|
|
# help="Choose a ticker to be the market asset",
|
|
# )
|
|
# ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
# if ns_parser:
|
|
# if not self.portfolio.empty:
|
|
# self.portfolio.generate_holdings_from_trades()
|
|
# self.portfolio.add_benchmark(ns_parser.market)
|
|
# portfolio_view.Report(
|
|
# val, hist, ns_parser.market, 365
|
|
# ).generate_report()
|
|
# else:
|
|
# console.print("Please add items to the portfolio\n")
|
|
|
|
@log_start_end(log=logger)
|
|
def call_var(self, other_args: List[str]):
|
|
"""Process var command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="var",
|
|
description="""
|
|
Provides value at risk (short: VaR) of the selected portfolio.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
action="store_true",
|
|
default=False,
|
|
dest="use_mean",
|
|
help="If one should use the mean of the portfolio return",
|
|
)
|
|
parser.add_argument(
|
|
"-a",
|
|
"--adjusted",
|
|
action="store_true",
|
|
default=False,
|
|
dest="adjusted",
|
|
help="""
|
|
If the VaR should be adjusted for skew and kurtosis (Cornish-Fisher-Expansion)
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-s",
|
|
"--student",
|
|
action="store_true",
|
|
default=False,
|
|
dest="student_t",
|
|
help="""
|
|
If one should use the student-t distribution
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--percentile",
|
|
action="store",
|
|
dest="percentile",
|
|
type=float,
|
|
default=99.9,
|
|
help="""
|
|
Percentile used for VaR calculations, for example input 99.9 equals a 99.9 Percent VaR
|
|
""",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if self.portfolio.empty:
|
|
console.print("[red]No portfolio loaded.[/red]\n")
|
|
return
|
|
if ns_parser.adjusted and ns_parser.student_t:
|
|
console.print("Select the adjusted or the student_t parameter.\n")
|
|
else:
|
|
from gamestonk_terminal.common.quantitative_analysis import qa_view
|
|
|
|
self.portfolio.generate_holdings_from_trades()
|
|
qa_view.display_var(
|
|
self.portfolio.returns,
|
|
"Portfolio",
|
|
ns_parser.use_mean,
|
|
ns_parser.adjusted,
|
|
ns_parser.student_t,
|
|
ns_parser.percentile / 100,
|
|
True,
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_es(self, other_args: List[str]):
|
|
"""Process es command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="es",
|
|
description="""
|
|
Provides Expected Shortfall (short: ES) of the selected portfolio.
|
|
""",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--mean",
|
|
action="store_true",
|
|
default=False,
|
|
dest="use_mean",
|
|
help="If one should use the mean of the portfolios return",
|
|
)
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dist",
|
|
"--distributions",
|
|
dest="distributions",
|
|
type=str,
|
|
choices=self.distributions,
|
|
default="normal",
|
|
help="Distribution used for the calculations",
|
|
)
|
|
parser.add_argument(
|
|
"-p",
|
|
"--percentile",
|
|
action="store",
|
|
dest="percentile",
|
|
type=float,
|
|
default=99.9,
|
|
help="""
|
|
Percentile used for ES calculations, for example input 99.9 equals a 99.9 Percent Expected Shortfall
|
|
""",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
from gamestonk_terminal.common.quantitative_analysis import qa_view
|
|
|
|
if self.portfolio.empty:
|
|
console.print("[red]No portfolio loaded.[/red]\n")
|
|
return
|
|
self.portfolio.generate_holdings_from_trades()
|
|
qa_view.display_es(
|
|
self.portfolio.returns,
|
|
"Portfolio",
|
|
ns_parser.use_mean,
|
|
ns_parser.distributions,
|
|
ns_parser.percentile / 100,
|
|
True,
|
|
)
|
|
|
|
@log_start_end(log=logger)
|
|
def call_rmr(self, other_args: List[str]):
|
|
"""Process rmr command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="rmr",
|
|
description="Graph of portfolio returns versus market returns",
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--market",
|
|
type=str,
|
|
dest="market",
|
|
default="SPY",
|
|
help="Choose a ticker to be the market asset",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(parser, other_args)
|
|
if ns_parser:
|
|
if not self.portfolio.empty:
|
|
portfolio_view.display_returns_vs_bench(
|
|
self.portfolio, ns_parser.market
|
|
)
|
|
else:
|
|
logger.warning("No portfolio loaded")
|
|
console.print("[red]No portfolio loaded.[/red]")
|
|
|
|
@log_start_end(log=logger)
|
|
def call_dd(self, other_args: List[str]):
|
|
"""Process dd command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="dd",
|
|
description="Show portfolio drawdown",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(
|
|
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
|
|
)
|
|
if ns_parser:
|
|
if not self.portfolio.empty:
|
|
self.portfolio.generate_holdings_from_trades()
|
|
self.portfolio.add_benchmark("SPY")
|
|
portfolio_view.display_drawdown(self.portfolio.portfolio_value)
|
|
else:
|
|
logger.warning("No portfolio loaded")
|
|
console.print("[red]No portfolio loaded.\n[/red]")
|
|
|
|
@log_start_end(log=logger)
|
|
def call_rolling(self, other_args: List[str]):
|
|
"""Process rolling command"""
|
|
parser = argparse.ArgumentParser(
|
|
add_help=False,
|
|
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
prog="rolling",
|
|
description="Show rolling portfolio metrics vs benchmark",
|
|
)
|
|
parser.add_argument(
|
|
"-b",
|
|
"--benchmark",
|
|
type=str,
|
|
dest="benchmark",
|
|
default="SPY",
|
|
help="Choose a ticker to be the benchmark",
|
|
)
|
|
parser.add_argument(
|
|
"-l",
|
|
"--length",
|
|
type=check_positive,
|
|
dest="length",
|
|
default=60,
|
|
help="Length of rolling window",
|
|
)
|
|
parser.add_argument(
|
|
"-r",
|
|
"--rf",
|
|
type=check_positive_float,
|
|
dest="rf",
|
|
default=0.001,
|
|
help="Set risk free rate for calculations.",
|
|
)
|
|
ns_parser = parse_known_args_and_warn(
|
|
parser, other_args, export_allowed=EXPORT_ONLY_FIGURES_ALLOWED
|
|
)
|
|
if ns_parser:
|
|
if self.portfolio.empty:
|
|
logger.warning("No portfolio loaded")
|
|
console.print("[red]No portfolio loaded[/red].\n")
|
|
return
|
|
portfolio_view.display_rolling_stats(
|
|
self.portfolio,
|
|
length=ns_parser.length,
|
|
benchmark=ns_parser.benchmark,
|
|
risk_free_rate=ns_parser.rf,
|
|
)
|