mirror of
https://github.com/OpenBB-finance/OpenBB.git
synced 2026-10-31 22:41:19 +00:00
Portfolio class (#1280)
* remerge into main ? * tests again * change the example csv * squash all the bugs * Improve `add` interface * Add warning on loading portfolio with no cash * left a rogue print * oopsie. hugo * oopsie. hugo * Add back a new `al` function + port name * test Co-authored-by: Colin Delahunty <72827203+colin99d@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,6 @@ from gamestonk_terminal.helper_classes import ModelsNamespace as _models
|
||||
from .brokers import bro_api as bro
|
||||
from .portfolio_analysis import pa_api as pa
|
||||
from .portfolio_optimization import po_api as po
|
||||
from .portfolio_view import show_df as show
|
||||
from .portfolio_view import Report as ar
|
||||
from .portfolio_view import plot_overall_return as rmr
|
||||
|
||||
|
||||
@@ -5,8 +5,7 @@ import argparse
|
||||
import os
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
from typing import List
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from prompt_toolkit.completion import NestedCompleter
|
||||
import pandas as pd
|
||||
@@ -18,6 +17,7 @@ from gamestonk_terminal.helper_funcs import (
|
||||
check_positive_float,
|
||||
check_positive,
|
||||
EXPORT_ONLY_FIGURES_ALLOWED,
|
||||
print_rich_table,
|
||||
)
|
||||
from gamestonk_terminal.menu import session
|
||||
|
||||
@@ -25,18 +25,30 @@ from gamestonk_terminal.portfolio.portfolio_optimization import po_controller
|
||||
from gamestonk_terminal.portfolio import (
|
||||
portfolio_view,
|
||||
portfolio_model,
|
||||
yfinance_model,
|
||||
portfolio_helper,
|
||||
)
|
||||
from gamestonk_terminal.helper_funcs import parse_known_args_and_warn
|
||||
|
||||
# pylint: disable=R1710,E1101,C0415
|
||||
|
||||
portfolios_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "portfolios")
|
||||
|
||||
|
||||
class PortfolioController(BaseController):
|
||||
"""Portfolio Controller class"""
|
||||
|
||||
CHOICES_COMMANDS = ["load", "save", "show", "add", "rmv", "ar", "rmr", "al", "mdd"]
|
||||
CHOICES_COMMANDS = [
|
||||
"load",
|
||||
"save",
|
||||
"init",
|
||||
"show",
|
||||
"add",
|
||||
"build",
|
||||
"ar",
|
||||
"rmr",
|
||||
"al",
|
||||
"dd",
|
||||
"rolling",
|
||||
]
|
||||
CHOICES_MENUS = [
|
||||
"bro",
|
||||
"po",
|
||||
@@ -61,13 +73,20 @@ class PortfolioController(BaseController):
|
||||
]
|
||||
)
|
||||
|
||||
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"""
|
||||
help_text = """[menu]
|
||||
help_text = f"""[menu]
|
||||
> bro brokers holdings, \t\t supports: robinhood, ally, degiro, coinbase
|
||||
> po portfolio optimization, \t optimal portfolio weights from pyportfolioopt
|
||||
> pa portfolio analysis, \t\t analyse portfolios[/menu]
|
||||
@@ -77,16 +96,21 @@ class PortfolioController(BaseController):
|
||||
save save your portfolio for future use
|
||||
show show existing transactions
|
||||
add add a security to your portfolio
|
||||
rmv remove a security from your portfolio[/cmds]
|
||||
|
||||
[info]Reports:[/info][cmds]
|
||||
ar annual report for performance of a given 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
|
||||
al displays the allocation of the portfolio
|
||||
mdd display portfolio drawdown[/cmds]
|
||||
dd display portfolio drawdown
|
||||
al display allocation to given assets over period[/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")
|
||||
|
||||
def call_bro(self, _):
|
||||
@@ -111,6 +135,19 @@ class PortfolioController(BaseController):
|
||||
pa_controller.PortfolioAnalysis, self.queue
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
def call_load(self, other_args: List[str]):
|
||||
"""Process load command"""
|
||||
path = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -136,8 +173,11 @@ class PortfolioController(BaseController):
|
||||
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if ns_parser:
|
||||
self.portfolio = portfolio_model.load_df(ns_parser.name)
|
||||
console.print("")
|
||||
self.portfolio = portfolio_model.Portfolio.from_csv(
|
||||
os.path.join(portfolios_path, ns_parser.name)
|
||||
)
|
||||
self.portfolio_name = ns_parser.name
|
||||
console.print()
|
||||
|
||||
def call_save(self, other_args: List[str]):
|
||||
"""Process save command"""
|
||||
@@ -165,7 +205,7 @@ class PortfolioController(BaseController):
|
||||
or ".xlsx" in ns_parser.name
|
||||
or ".json" in ns_parser.name
|
||||
):
|
||||
portfolio_model.save_df(self.portfolio, 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"
|
||||
@@ -173,7 +213,11 @@ class PortfolioController(BaseController):
|
||||
|
||||
def call_show(self, _):
|
||||
"""Process show command"""
|
||||
portfolio_view.show_df(self.portfolio, False)
|
||||
if self.portfolio.empty:
|
||||
console.print("[red]No portfolio loaded.[/red]\n")
|
||||
return
|
||||
print_rich_table(self.portfolio.trades, show_index=False)
|
||||
console.print()
|
||||
|
||||
def call_add(self, other_args: List[str]):
|
||||
"""Process add command"""
|
||||
@@ -183,140 +227,169 @@ class PortfolioController(BaseController):
|
||||
prog="add",
|
||||
description="Adds an item to your portfolio",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
type=str,
|
||||
dest="name",
|
||||
required="-h" not in other_args,
|
||||
help="Name of item to be added (for example a ticker for a stock)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--type",
|
||||
dest="type",
|
||||
type=lambda s: s.lower(),
|
||||
choices=["stock", "cash", "etf"], # "bond", "option", "crypto",
|
||||
default="stock",
|
||||
help="Type of asset to add",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q",
|
||||
"--quantity",
|
||||
dest="quantity",
|
||||
type=check_positive_float,
|
||||
default=1,
|
||||
help="Amounts of the asset owned",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--date",
|
||||
dest="date",
|
||||
type=valid_date,
|
||||
default=datetime.now(),
|
||||
help="Date: yyyy/mm/dd",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--price",
|
||||
dest="price",
|
||||
type=check_positive_float,
|
||||
required="-h" not in other_args,
|
||||
help="Price purchased for asset",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--fees",
|
||||
dest="fees",
|
||||
type=check_positive_float,
|
||||
help="Fees paid for transaction",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--action",
|
||||
type=lambda s: s.lower(),
|
||||
dest="action",
|
||||
choices=["buy", "sell", "interest", "deposit", "withdrawal"],
|
||||
default="deposit" if "cash" in other_args else "buy",
|
||||
help="Select what you did in the transaction",
|
||||
)
|
||||
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 not ns_parser:
|
||||
return
|
||||
if ns_parser.type == "cash" and ns_parser.action not in [
|
||||
"deposit",
|
||||
"withdrawal",
|
||||
]:
|
||||
console.print("Cash can only be deposited or withdrew\n")
|
||||
return
|
||||
if ns_parser.type != "cash" and ns_parser.action in ["deposit", "withdrawal"]:
|
||||
console.print("Only cash can be deposited or withdrew\n")
|
||||
return
|
||||
|
||||
if ns_parser.type == "stock":
|
||||
if not portfolio_helper.is_ticker(ns_parser.name):
|
||||
console.print("Invalid ticker\n")
|
||||
console.print()
|
||||
inputs: Dict[str, Union[str, float, int]] = {}
|
||||
type_ = input("Type (stock, cash): \n")
|
||||
if type_ not in ["stock", "cash"]:
|
||||
console.print("[red]Currently only stocks or cash supported.[/red]\n")
|
||||
type_ = input("Type (stock, cash): \n")
|
||||
if type_ not in ["stock", "cash"]:
|
||||
console.print("[red]Two unsuccessful attempts. Exiting add.[/red]\n")
|
||||
return
|
||||
|
||||
data = {
|
||||
"Name": ns_parser.name,
|
||||
"Type": ns_parser.type,
|
||||
"Quantity": ns_parser.quantity,
|
||||
"Date": ns_parser.date,
|
||||
"Price": ns_parser.price,
|
||||
"Fees": ns_parser.fees,
|
||||
"Premium": None, # ns_parser.premium
|
||||
"Side": ns_parser.action,
|
||||
}
|
||||
self.portfolio = self.portfolio.append([data])
|
||||
self.portfolio.index = list(range(0, len(self.portfolio.values)))
|
||||
console.print(f"{ns_parser.name.upper()} successfully added\n")
|
||||
inputs["Type"] = type_.lower()
|
||||
action = input("Action: (buy, sell, deposit, withdraw): \n").lower()
|
||||
|
||||
def call_rmv(self, _):
|
||||
"""Process rmv command"""
|
||||
portfolio_view.show_df(self.portfolio, True)
|
||||
to_rmv = int(input("\nType the index number you want to remove:\n"))
|
||||
if 0 <= to_rmv < len(self.portfolio.index):
|
||||
self.portfolio = self.portfolio.drop(self.portfolio.index[to_rmv])
|
||||
self.portfolio.index = list(range(0, len(self.portfolio.values)))
|
||||
else:
|
||||
console.print(
|
||||
f"Invalid index please use an integer between 0 and {len(self.portfolio.index)-1}\n"
|
||||
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"]:
|
||||
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"]:
|
||||
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)
|
||||
|
||||
def call_ar(self, other_args: List[str]):
|
||||
"""Process ar command"""
|
||||
console.print(f"{inputs['Name']} successfully added\n")
|
||||
|
||||
def call_build(self, other_args: List[str]):
|
||||
"""Process build command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
prog="ar",
|
||||
description="Create an annual report based on given portfolio",
|
||||
prog="build",
|
||||
description="Build portfolio from list of tickers and weights",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--market",
|
||||
type=str,
|
||||
dest="market",
|
||||
default="SPY",
|
||||
help="Choose a ticker to be the market asset",
|
||||
"-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:
|
||||
if not self.portfolio.empty:
|
||||
val, hist = portfolio_model.convert_df(self.portfolio)
|
||||
if not val.empty:
|
||||
portfolio_view.Report(
|
||||
val, hist, ns_parser.market, 365
|
||||
).generate_report()
|
||||
else:
|
||||
console.print("Cannot generate a graph from an empty dataframe\n")
|
||||
else:
|
||||
console.print("Please add items to the portfolio\n")
|
||||
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()
|
||||
|
||||
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:
|
||||
portfolio_view.display_allocation(self.portfolio, ns_parser.export)
|
||||
console.print()
|
||||
|
||||
# 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")
|
||||
|
||||
def call_rmr(self, other_args: List[str]):
|
||||
"""Process rmr command"""
|
||||
@@ -337,71 +410,19 @@ class PortfolioController(BaseController):
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if ns_parser:
|
||||
if not self.portfolio.empty:
|
||||
val, _ = portfolio_model.convert_df(self.portfolio)
|
||||
if not val.empty:
|
||||
df_m = yfinance_model.get_market(val.index[0], ns_parser.market)
|
||||
returns, _ = portfolio_model.get_return(val, df_m, 365)
|
||||
portfolio_view.plot_overall_return(returns, ns_parser.market, True)
|
||||
else:
|
||||
console.print("Cannot generate a graph from an empty dataframe\n")
|
||||
else:
|
||||
console.print("Please add items to the portfolio\n")
|
||||
|
||||
def call_al(self, other_args: List[str]):
|
||||
"""Process al command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
prog="al",
|
||||
description="Gives allocation",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--limit",
|
||||
help="Number to show",
|
||||
type=check_positive,
|
||||
default=20,
|
||||
dest="limit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--noetf",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="no_etf_positions",
|
||||
help="If etf positions should not be added to the portfolio",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-g",
|
||||
"--graph",
|
||||
action="store_true",
|
||||
default=False,
|
||||
dest="graph",
|
||||
help="Display table or pi chart of holdings",
|
||||
)
|
||||
ns_parser = parse_known_args_and_warn(parser, other_args)
|
||||
if not self.portfolio.empty:
|
||||
val, hist = portfolio_model.convert_df(self.portfolio)
|
||||
if not val.empty:
|
||||
df = portfolio_model.get_allocation(
|
||||
val,
|
||||
hist,
|
||||
self.portfolio,
|
||||
ns_parser.limit,
|
||||
ns_parser.no_etf_positions,
|
||||
portfolio_view.display_returns_vs_bench(
|
||||
self.portfolio, ns_parser.market
|
||||
)
|
||||
portfolio_view.display_allocation(df, ns_parser.graph)
|
||||
else:
|
||||
console.print("Cannot generate a graph from an empty dataframe\n")
|
||||
else:
|
||||
console.print("Please add items to the portfolio\n")
|
||||
console.print("[red]No portfolio loaded.[/red]")
|
||||
console.print()
|
||||
|
||||
def call_mdd(self, other_args: List[str]):
|
||||
"""Process mdd command"""
|
||||
def call_dd(self, other_args: List[str]):
|
||||
"""Process dd command"""
|
||||
parser = argparse.ArgumentParser(
|
||||
add_help=False,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
prog="mdd",
|
||||
prog="dd",
|
||||
description="Show portfolio drawdown",
|
||||
)
|
||||
ns_parser = parse_known_args_and_warn(
|
||||
@@ -409,10 +430,55 @@ class PortfolioController(BaseController):
|
||||
)
|
||||
if ns_parser:
|
||||
if not self.portfolio.empty:
|
||||
val, _ = portfolio_model.convert_df(self.portfolio)
|
||||
if not val.empty:
|
||||
df_m = yfinance_model.get_market(val.index[0], "SPY")
|
||||
returns, _ = portfolio_model.get_return(val, df_m, 365)
|
||||
portfolio_view.display_drawdown(returns[["return"]].dropna())
|
||||
self.portfolio.generate_holdings_from_trades()
|
||||
self.portfolio.add_benchmark("SPY")
|
||||
portfolio_view.display_drawdown(self.portfolio.portfolio_value)
|
||||
else:
|
||||
console.print("[red]No portfolio loaded.\n[/red]")
|
||||
|
||||
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:
|
||||
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,
|
||||
)
|
||||
console.print()
|
||||
|
||||
@@ -1,31 +1,28 @@
|
||||
"""Portfolio Model"""
|
||||
__docformat__ = "numpy"
|
||||
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
from datetime import date, datetime, timedelta
|
||||
from typing import List
|
||||
from datetime import timedelta, datetime
|
||||
from typing import Dict, List, Union
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import statsmodels.api as sm
|
||||
from statsmodels.regression.rolling import RollingOLS
|
||||
import yfinance as yf
|
||||
from pycoingecko import CoinGeckoAPI
|
||||
|
||||
from gamestonk_terminal.decorators import log_start_end
|
||||
from gamestonk_terminal.etf import stockanalysis_model
|
||||
from gamestonk_terminal.portfolio import (
|
||||
portfolio_helper,
|
||||
portfolio_view,
|
||||
yfinance_model,
|
||||
)
|
||||
from gamestonk_terminal.rich_config import console
|
||||
from gamestonk_terminal.decorators import log_start_end
|
||||
|
||||
# pylint: disable=E1136
|
||||
# pylint: disable=E1136,W0201,R0902
|
||||
# pylint: disable=unsupported-assignment-operation
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
cg = CoinGeckoAPI()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -49,311 +46,6 @@ def save_df(df: pd.DataFrame, name: str) -> None:
|
||||
df.to_excel(path, index=False, engine="openpyxl")
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def load_df(name: str) -> pd.DataFrame:
|
||||
"""Load the portfolio from a csv
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the string
|
||||
|
||||
Returns
|
||||
----------
|
||||
data : pd.DataFrame
|
||||
A DataFrame with historical trading information
|
||||
"""
|
||||
if ".csv" not in name and ".xlsx" not in name and ".json" not in name:
|
||||
console.print(
|
||||
"Please submit as 'filename.filetype' with filetype being csv, xlsx, or json\n"
|
||||
)
|
||||
return pd.DataFrame()
|
||||
|
||||
try:
|
||||
if ".csv" in name:
|
||||
df = pd.read_csv(f"gamestonk_terminal/portfolio/portfolios/{name}")
|
||||
elif ".json" in name:
|
||||
df = pd.read_json(f"gamestonk_terminal/portfolio/portfolios/{name}")
|
||||
elif ".xlsx" in name:
|
||||
df = pd.read_excel(
|
||||
f"gamestonk_terminal/portfolio/portfolios/{name}", engine="openpyxl"
|
||||
)
|
||||
|
||||
df.index = list(range(0, len(df.values)))
|
||||
df["Name"] = df["Name"].str.lower()
|
||||
df["Type"] = df["Type"].str.lower()
|
||||
df["Date"] = pd.to_datetime(df["Date"], format="%Y/%m/%d")
|
||||
|
||||
for item in ["Quantity", "Price", "Fees", "Premium"]:
|
||||
result = any(df[item] < 0)
|
||||
if result:
|
||||
console.print(
|
||||
f"The column '{item}' has a negative value. Ensure all values are positive."
|
||||
)
|
||||
return pd.DataFrame()
|
||||
|
||||
if len(df[~df["Type"].isin(["cash", "stock"])].index):
|
||||
console.print(
|
||||
"Warning: 'Type' other than 'cash' and 'stock' will be ignored."
|
||||
)
|
||||
|
||||
if len(
|
||||
df[
|
||||
~df["Side"]
|
||||
.str.lower()
|
||||
.isin(["buy", "sell", "interest", "deposit", "withdrawal"])
|
||||
].index
|
||||
):
|
||||
console.print(
|
||||
"Warning: 'Side' must be buy, sell, interest, deposit, or withdrawal"
|
||||
)
|
||||
return pd.DataFrame()
|
||||
|
||||
return df
|
||||
except FileNotFoundError:
|
||||
portfolio_view.load_info()
|
||||
return pd.DataFrame()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def add_values(
|
||||
log: pd.DataFrame, changes: pd.DataFrame, cashes: pd.DataFrame
|
||||
) -> pd.DataFrame:
|
||||
"""Creates a new df with performance results
|
||||
|
||||
Parameters
|
||||
----------
|
||||
log : pd.DataFrame
|
||||
The dataframe that will have daily holdings
|
||||
changes : pd.DataFrame
|
||||
Transactions that changed holdings
|
||||
cashes : pd.DataFrame
|
||||
Cash changing transactions
|
||||
|
||||
Returns
|
||||
----------
|
||||
log : pd.DataFrame
|
||||
A dataframe with daily holdings
|
||||
"""
|
||||
for index, _ in log.iterrows():
|
||||
# Add stocks to dataframe
|
||||
values = changes[changes["Date"] == index]
|
||||
if len(values.index) > 0:
|
||||
for _, sub_row in values.iterrows():
|
||||
ticker = sub_row["Name"]
|
||||
quantity = sub_row["Quantity"]
|
||||
price = sub_row["Price"]
|
||||
fees = sub_row["Fees"]
|
||||
if math.isnan(fees):
|
||||
fees = 0
|
||||
sign = -1 if sub_row["Side"].lower() == "sell" else 1
|
||||
pos1 = log.cumsum().at[index, ("Quantity", ticker)] > 0
|
||||
pos2 = (quantity * sign) > 0
|
||||
|
||||
if sub_row["Side"].lower() == "interest":
|
||||
log.at[index, ("Cost Basis", ticker)] = (
|
||||
log.at[index, ("Cost Basis", ticker)] + quantity * price
|
||||
)
|
||||
log.at[index, ("Cash", "Cash")] = log.at[
|
||||
index, ("Cash", "Cash")
|
||||
] - (quantity * price)
|
||||
|
||||
elif (
|
||||
pos1 == pos2
|
||||
or log.cumsum().at[index, ("Quantity", ticker)] == 0
|
||||
or (quantity * sign) == 0
|
||||
):
|
||||
log.at[index, ("Quantity", ticker)] = (
|
||||
log.at[index, ("Quantity", ticker)] + quantity * sign
|
||||
)
|
||||
log.at[index, ("Cost Basis", ticker)] = (
|
||||
log.at[index, ("Cost Basis", ticker)]
|
||||
+ fees
|
||||
+ quantity * sign * price
|
||||
)
|
||||
log.at[index, ("Cash", "Cash")] = log.at[
|
||||
index, ("Cash", "Cash")
|
||||
] - (fees + quantity * sign * price)
|
||||
else:
|
||||
rev = (
|
||||
log.at[index, ("Profit", ticker)] + quantity * sign * price * -1
|
||||
)
|
||||
wa_cost = (
|
||||
quantity / log.cumsum().at[index, ("Quantity", ticker)]
|
||||
) * log.cumsum().at[index, ("Cost Basis", ticker)]
|
||||
log.at[index, ("Profit", ticker)] = rev - wa_cost - fees
|
||||
log.at[index, ("Cash", "Cash")] = (
|
||||
log.at[index, ("Cash", "Cash")] + rev - fees
|
||||
)
|
||||
log.at[index, ("Quantity", ticker)] = (
|
||||
log.at[index, ("Quantity", ticker)] + quantity * sign
|
||||
)
|
||||
log.at[index, ("Cost Basis", ticker)] = (
|
||||
log.at[index, ("Cost Basis", ticker)] - wa_cost
|
||||
)
|
||||
cash_vals = cashes[cashes["Date"] == index]
|
||||
if len(cash_vals.index) > 0:
|
||||
for _, sub_row in cash_vals.iterrows():
|
||||
amount = sub_row["Price"]
|
||||
quantity = sub_row["Quantity"]
|
||||
if sub_row["Side"] == "deposit":
|
||||
d = 1
|
||||
elif sub_row["Side"] == "withdrawal":
|
||||
d = -1
|
||||
else:
|
||||
raise ValueError("Cash type must be deposit or withdrawal")
|
||||
log.at[index, ("Cash", "Cash")] = (
|
||||
log.at[index, ("Cash", "Cash")] + d * amount * quantity
|
||||
)
|
||||
log.at[index, ("Cash", "User")] = (
|
||||
log.at[index, ("Cash", "User")] + d * amount * quantity
|
||||
)
|
||||
return log
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def merge_dataframes(
|
||||
log: pd.DataFrame,
|
||||
hist: pd.DataFrame,
|
||||
changes: pd.DataFrame,
|
||||
divs: pd.DataFrame,
|
||||
uniques: List[str],
|
||||
) -> pd.DataFrame:
|
||||
"""Merge dataframes to create final dataframe
|
||||
|
||||
Parameters
|
||||
----------
|
||||
log : pd.DataFrame
|
||||
The new dataframe with daily holdings
|
||||
hist : pd.DataFrame
|
||||
Historical returns for stocks in portfolio
|
||||
changes : pd.DataFrame
|
||||
A log of past transactions
|
||||
divs : pd.DataFrame
|
||||
The dividend history for stocks in portfolio
|
||||
uniques: List[str]
|
||||
A list of stocks in the portfolio
|
||||
|
||||
Returns
|
||||
----------
|
||||
comb : pd.DataFrame
|
||||
Thew new aggregated dataframe
|
||||
"""
|
||||
comb = pd.merge(log, hist, how="left", left_index=True, right_index=True)
|
||||
comb = comb.fillna(method="ffill")
|
||||
comb = pd.merge(comb, divs, how="left", left_index=True, right_index=True)
|
||||
comb = comb.fillna(0)
|
||||
|
||||
for uni in uniques:
|
||||
comb[("Quantity", uni)] = comb[("Quantity", uni)].cumsum()
|
||||
comb[("Cost Basis", uni)] = comb[("Cost Basis", uni)].cumsum()
|
||||
comb[("Cash", "Cash")] = comb[("Cash", "Cash")] + (
|
||||
comb[("Quantity", uni)] * comb[("Dividend", uni)]
|
||||
)
|
||||
comb[("Holding", uni)] = (
|
||||
np.where(
|
||||
comb[("Quantity", uni)] > 0,
|
||||
comb[("Close", uni)],
|
||||
(2 * comb[("Close", uni)][0] - comb[("Close", uni)]),
|
||||
)
|
||||
* comb[("Quantity", uni)]
|
||||
)
|
||||
comb[("Profit", uni)] = comb[("Profit", uni)].cumsum()
|
||||
comb[("Cash", "Cash")] = comb[("Cash", "Cash")].cumsum()
|
||||
if len(changes["Date"]) > 0:
|
||||
comb["holdings"] = comb.groupby(level=0, axis=1).sum()["Holding"]
|
||||
comb["profits"] = comb.groupby(level=0, axis=1).sum()["Profit"]
|
||||
comb["total_prof"] = comb["holdings"] + comb["profits"]
|
||||
comb["total_cost"] = comb.groupby(level=0, axis=1).sum()["Cost Basis"]
|
||||
return comb
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def convert_df(portfolio: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Converts a df from activity to daily holdings
|
||||
|
||||
Parameters
|
||||
----------
|
||||
portfolio : pd.DataFrame
|
||||
The dataframe of transactions
|
||||
|
||||
Returns
|
||||
----------
|
||||
data : pd.DataFrame
|
||||
A dataframe with daily holdings of portfolio
|
||||
hist : pd.DataFrame
|
||||
The historical performance of tickers in portfolio
|
||||
"""
|
||||
changes = portfolio.copy()
|
||||
# Transactions sorted for only stocks and etfs
|
||||
cashes = changes[changes["Type"] == "cash"]
|
||||
if cashes.empty:
|
||||
raise ValueError("Brokers require cash, input cash deposits")
|
||||
changes = changes[(changes["Type"] == "stock") | (changes["Type"] == "etf")]
|
||||
uniques = list(set(changes["Name"].tolist()))
|
||||
if uniques:
|
||||
hist = yfinance_model.get_stocks(uniques, min(changes["Date"]))
|
||||
divs = yfinance_model.get_dividends(uniques)
|
||||
else:
|
||||
hist, divs = pd.DataFrame(), pd.DataFrame()
|
||||
|
||||
divs = divs.fillna(0)
|
||||
mini = min(cashes["Date"].to_list() + changes["Date"].to_list())
|
||||
days = pd.date_range(mini, date.today() - timedelta(days=1), freq="d")
|
||||
zeros = [0 for _ in uniques]
|
||||
data = [zeros + zeros + zeros + [0] for _ in days]
|
||||
vals = ["Quantity", "Cost Basis", "Profit"]
|
||||
arrays = [[x for _ in uniques for x in vals] + ["Cash"], uniques * 3 + ["Cash"]]
|
||||
tuples = list(zip(*arrays))
|
||||
headers = pd.MultiIndex.from_tuples(tuples, names=["first", "second"])
|
||||
log = pd.DataFrame(data, columns=headers, index=days)
|
||||
log[("Cash", "User")] = 0
|
||||
log = add_values(log, changes, cashes)
|
||||
comb = merge_dataframes(log, hist, changes, divs, uniques)
|
||||
|
||||
return comb, hist
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_return(df: pd.DataFrame, df_m: pd.DataFrame, n: int) -> pd.DataFrame:
|
||||
"""Adds cumulative returns to a holdings df
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe of daily holdings
|
||||
df_m : pd.DataFrame
|
||||
The dataframe of market performance
|
||||
n : int
|
||||
The period to get returns for
|
||||
|
||||
Returns
|
||||
----------
|
||||
comb : pd.DataFrame
|
||||
Dataframe with holdings and returns
|
||||
"""
|
||||
df = df.copy()
|
||||
df = df[df.index >= datetime.now() - timedelta(days=n + 1)]
|
||||
comb = pd.merge(df, df_m, how="left", left_index=True, right_index=True)
|
||||
comb = comb.fillna(method="ffill")
|
||||
comb = comb.dropna()
|
||||
comb["return"] = (
|
||||
comb[("Cash", "Cash")]
|
||||
+ (0 if "holdings" not in comb.columns else comb["holdings"])
|
||||
) / (
|
||||
comb[("Cash", "Cash")].shift(1)
|
||||
+ comb[("Cash", "User")]
|
||||
+ (0 if "holdings" not in comb.columns else comb["holdings"].shift(1))
|
||||
)
|
||||
comb["return"] = comb["return"].fillna(1)
|
||||
variance = np.var(comb["return"])
|
||||
comb["return"] = comb["return"].cumprod() - 1
|
||||
comb[("Market", "Return")] = (
|
||||
comb[("Market", "Close")] - comb[("Market", "Close")][0]
|
||||
) / comb[("Market", "Close")][0]
|
||||
return comb, variance
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_rolling_beta(
|
||||
df: pd.DataFrame, hist: pd.DataFrame, mark: pd.DataFrame, n: pd.DataFrame
|
||||
@@ -400,139 +92,6 @@ def get_rolling_beta(
|
||||
return comb
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def fix_etf_allocation(
|
||||
data: pd.DataFrame,
|
||||
df_new: pd.DataFrame,
|
||||
etf: str,
|
||||
no_etf_positions: bool,
|
||||
number: int,
|
||||
):
|
||||
"""Adjusting the dataframe to etf positions, by adding etf holdings to identical portfolio positions or
|
||||
if 'add_etf_positions' is true add a new position
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pd.DataFrame
|
||||
The dataframe of positions
|
||||
df_new: pd.DataFrame
|
||||
A dataframe of etf holdings
|
||||
etf: str
|
||||
String of the etf
|
||||
no_etf_positions: bool
|
||||
If new positions should not be added to the 'data' dataframe for etf holdings
|
||||
number: int
|
||||
Number of etf positions to be checked
|
||||
|
||||
Returns
|
||||
----------
|
||||
data: pd.DataFrame
|
||||
etf adjusted dataframe
|
||||
"""
|
||||
i = 0
|
||||
length = int(df_new.index.size) - 1
|
||||
while i <= (
|
||||
length ^ ((number ^ length) & -(number < length))
|
||||
): # gets minimum of number and length
|
||||
if df_new.iloc[[i]].index[0].lower() in data.index:
|
||||
change = (
|
||||
float(df_new.iloc[i]["% Of Etf"].replace("%", ""))
|
||||
* data["value"].loc[data.index == etf][0]
|
||||
/ 100
|
||||
)
|
||||
data.loc[data.index == df_new.iloc[[i]].index[0].lower(), "value"] += change
|
||||
elif not no_etf_positions:
|
||||
change = (
|
||||
float(df_new.iloc[i]["% Of Etf"].replace("%", ""))
|
||||
* data["value"].loc[data.index == etf][0]
|
||||
/ 100
|
||||
)
|
||||
data.loc[len(data.index)] = [f"{etf}_etf", change]
|
||||
data.rename(
|
||||
index={len(data.index) - 1: df_new.iloc[[i]].index[0].lower()},
|
||||
inplace=True,
|
||||
)
|
||||
data.loc[data.index == etf, "value"] -= change
|
||||
data.loc[data.index == etf, "value"] -= change
|
||||
i += 1
|
||||
data.rename(
|
||||
index={data.loc[data.index == etf].index[0]: f"{etf}_rest"}, inplace=True
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_allocation(
|
||||
data: pd.DataFrame,
|
||||
hist: pd.DataFrame,
|
||||
portfolio: pd.DataFrame,
|
||||
number: int,
|
||||
no_etf_positions: bool,
|
||||
) -> pd.DataFrame:
|
||||
"""Formatting the dataframe to an allocation dataframe of the positions
|
||||
|
||||
Parameters
|
||||
----------
|
||||
data : pd.DataFrame
|
||||
The dataframe of positions
|
||||
hist: pd.DataFrame
|
||||
A dataframe of the positions historical performances
|
||||
portfolio: pd.DataFrame
|
||||
Portfolio dataframe
|
||||
number: int
|
||||
Number of etf positions to be checked
|
||||
no_etf_positions: bool
|
||||
If new positions should not be added to the 'data' dataframe for etf holdings
|
||||
|
||||
Returns
|
||||
----------
|
||||
df: pd.DataFrame
|
||||
Dataframe of allocation
|
||||
"""
|
||||
# Fixing dataframe formatting
|
||||
df = data["Quantity"].tail(1).transpose()
|
||||
df.columns = ["amount"]
|
||||
df = df.sort_index(axis=0, ascending=True)
|
||||
hist = hist.tail(1).transpose()
|
||||
hist = hist.reset_index(["first"])
|
||||
hist.columns = ["position", "value"]
|
||||
hist = pd.DataFrame(hist["value"])
|
||||
|
||||
i = 0
|
||||
value = []
|
||||
while i < df.index.size:
|
||||
value.append(df["amount"].iloc[i] * hist["value"].iloc[i])
|
||||
i += 1
|
||||
df["value"] = value
|
||||
|
||||
i = 0
|
||||
length = df.index.size
|
||||
while i < length:
|
||||
row_index = df.iloc[[i]].index[0]
|
||||
if (
|
||||
portfolio["Type"].loc[portfolio["Name"] == row_index].to_list()[0] == "etf"
|
||||
): # check if position is an etf
|
||||
df = fix_etf_allocation(
|
||||
df,
|
||||
stockanalysis_model.get_etf_holdings(row_index),
|
||||
row_index,
|
||||
no_etf_positions,
|
||||
number,
|
||||
)
|
||||
if df.iloc[i].loc["value"] < 0:
|
||||
# Converting negative positions to "shorts" in order to correctly calculate allocation
|
||||
df.loc[row_index, "value"] = abs(df.loc[row_index, "value"])
|
||||
df.loc[row_index, "amount"] = abs(df.loc[row_index, "amount"])
|
||||
df.rename(index={row_index: f"short_{row_index}"}, inplace=True)
|
||||
i += 1
|
||||
|
||||
total_value = df["value"].sum()
|
||||
df["pct_allocation"] = df["value"] / total_value * 100 # calculate allocation
|
||||
df = df.sort_values(by="pct_allocation", ascending=False)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_main_text(df: pd.DataFrame) -> str:
|
||||
"""Get main performance summary from a dataframe with returns
|
||||
@@ -639,3 +198,343 @@ def calculate_drawdown(input_series: pd.Series, is_returns: bool = False) -> pd.
|
||||
rolling_max = input_series.cummax()
|
||||
drawdown = (input_series - rolling_max) / rolling_max
|
||||
return drawdown
|
||||
|
||||
|
||||
class Portfolio:
|
||||
"""
|
||||
Class for portfolio analysis in GST
|
||||
|
||||
Attributes
|
||||
-------
|
||||
portfolio_value: pd.Series
|
||||
Series containing value at each day
|
||||
returns: pd.Series
|
||||
Series containing daily returns
|
||||
ItemizedReturns: pd.DataFrame
|
||||
Dataframe of Holdings by class
|
||||
portfolio: pd.DataFrame
|
||||
Dataframe containing all relevant holdings data
|
||||
|
||||
Methods
|
||||
-------
|
||||
add_trade:
|
||||
Adds a trade to the dataframe of trades
|
||||
generate_holdings_from_trades:
|
||||
Takes a list of trades and converts to holdings on each day
|
||||
get_yahoo_close_prices:
|
||||
Gets close prices or adj close prices from yfinance
|
||||
add_benchmark
|
||||
Adds a benchmark to the class
|
||||
|
||||
"""
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def __init__(self, trades: pd.DataFrame = pd.DataFrame(), rf=0):
|
||||
"""Initialize Portfolio class"""
|
||||
# Allow for empty initialization
|
||||
self.empty = True
|
||||
self.rf = rf
|
||||
if not trades.empty:
|
||||
if "cash" not in trades.Name.to_list():
|
||||
console.print(
|
||||
"[red]No initial cash deposit. Calculations may be off as this assumes trading from a "
|
||||
"funded account[/red]."
|
||||
)
|
||||
# Load in trades df and do some quick editing
|
||||
trades.Name = trades.Name.map(lambda x: x.upper())
|
||||
trades["Side"] = trades["Side"].map(
|
||||
lambda x: 1
|
||||
if x.lower() in ["deposit", "buy"]
|
||||
else (-1 if x.lower() == "sell" else 0)
|
||||
)
|
||||
trades["Value"] = trades.Quantity * trades.Price * trades.Side
|
||||
# Make selling negative for cumulative sum of quantity later
|
||||
trades["Quantity"] = trades["Quantity"] * trades["Side"]
|
||||
# Should be simply extended for crypto/bonds etc
|
||||
# Treat etf as stock for yfinance historical.
|
||||
self._stock_tickers = list(
|
||||
set(trades[trades.Type == "stock"].Name.to_list())
|
||||
)
|
||||
|
||||
self._etf_tickers = list(set(trades[trades.Type == "etf"].Name.to_list()))
|
||||
|
||||
self._crypto_tickers = list(
|
||||
set(trades[trades.Type == "crypto"].Name.to_list())
|
||||
)
|
||||
self._start_date = trades.Date[0]
|
||||
# Copy pandas notation
|
||||
self.empty = False
|
||||
self.trades = trades
|
||||
self.portfolio = pd.DataFrame()
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def add_trade(self, trade_info: Dict):
|
||||
self.trades = self.trades.append([trade_info])
|
||||
self.empty = False
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def add_rf(self, risk_free_rate: float):
|
||||
"""Sets the risk free rate for calculation purposes"""
|
||||
self.rf = risk_free_rate
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def generate_holdings_from_trades(self, yfinance_use_close: bool = False):
|
||||
"""Generates portfolio data from list of trades"""
|
||||
# Load historical prices from yahoo (option to get close instead of adj close)
|
||||
self.get_yahoo_close_prices(use_close=yfinance_use_close)
|
||||
self.get_crypto_yfinance()
|
||||
|
||||
# Pivot list of trades on Name. This will give a multi-index df where the multiindex are the individual ticker
|
||||
# and the values from the table
|
||||
portfolio = self.trades.pivot(
|
||||
index="Date",
|
||||
columns="Name",
|
||||
values=["Quantity", "Price", "Fees", "Premium", "Side", "Value"],
|
||||
)
|
||||
# Merge with historical close prices (and fillna)
|
||||
portfolio = pd.merge(
|
||||
portfolio,
|
||||
self._historical_prices,
|
||||
how="right",
|
||||
left_index=True,
|
||||
right_index=True,
|
||||
).fillna(0)
|
||||
|
||||
is_there_stock_or_etf = self._stock_tickers + self._etf_tickers
|
||||
|
||||
# Merge with crypto
|
||||
if self._crypto_tickers:
|
||||
portfolio = pd.merge(
|
||||
portfolio,
|
||||
self._historical_crypto,
|
||||
how="left" if is_there_stock_or_etf else "right",
|
||||
left_index=True,
|
||||
right_index=True,
|
||||
).fillna(0)
|
||||
else:
|
||||
portfolio[pd.MultiIndex.from_product([["Close"], ["crypto"]])] = 0
|
||||
|
||||
# Add cumulative Quantity held
|
||||
portfolio["Quantity"] = portfolio["Quantity"].cumsum()
|
||||
|
||||
# Add holdings for each 'type'. Will check for tickers matching type.
|
||||
|
||||
if self._stock_tickers:
|
||||
# Find end of day holdings for each stock
|
||||
portfolio[
|
||||
pd.MultiIndex.from_product([["StockHoldings"], self._stock_tickers])
|
||||
] = (
|
||||
portfolio["Quantity"][self._stock_tickers]
|
||||
* portfolio["Close"][self._stock_tickers]
|
||||
)
|
||||
else:
|
||||
portfolio[pd.MultiIndex.from_product([["StockHoldings"], ["temp"]])] = 0
|
||||
|
||||
if self._etf_tickers:
|
||||
# Find end of day holdings for each ETF
|
||||
portfolio[
|
||||
pd.MultiIndex.from_product([["ETFHoldings"], self._etf_tickers])
|
||||
] = (
|
||||
portfolio["Quantity"][self._etf_tickers]
|
||||
* portfolio["Close"][self._etf_tickers]
|
||||
)
|
||||
else:
|
||||
portfolio[pd.MultiIndex.from_product([["ETFHoldings"], ["temp"]])] = 0
|
||||
if self._crypto_tickers:
|
||||
# Find end of day holdings for each stock
|
||||
portfolio[
|
||||
pd.MultiIndex.from_product([["CryptoHoldings"], self._crypto_tickers])
|
||||
] = (
|
||||
portfolio["Quantity"][self._crypto_tickers]
|
||||
* portfolio["Close"][self._crypto_tickers]
|
||||
)
|
||||
else:
|
||||
portfolio[pd.MultiIndex.from_product([["CryptoHoldings"], ["temp"]])] = 0
|
||||
|
||||
# Find amount of cash held in account. Defined as deposited cash - stocks bought + stocks sold
|
||||
portfolio["CashHold"] = portfolio["Value"]["CASH"] - portfolio["Value"][
|
||||
self._stock_tickers + self._etf_tickers + self._crypto_tickers
|
||||
].sum(axis=1)
|
||||
|
||||
# Subtract Fees or Premiums from cash holdings
|
||||
portfolio["CashHold"] = (
|
||||
portfolio["CashHold"]
|
||||
- portfolio["Fees"].sum(axis=1)
|
||||
- portfolio["Premium"].sum(axis=1)
|
||||
)
|
||||
portfolio["CashHold"] = portfolio["CashHold"].cumsum()
|
||||
|
||||
portfolio["TotalHoldings"] = (
|
||||
portfolio["CashHold"]
|
||||
+ portfolio["StockHoldings"].sum(axis=1)
|
||||
+ portfolio["ETFHoldings"].sum(axis=1)
|
||||
+ portfolio["CryptoHoldings"].sum(axis=1)
|
||||
)
|
||||
|
||||
self.portfolio_value = portfolio["TotalHoldings"]
|
||||
self.returns = portfolio["TotalHoldings"].pct_change().dropna()
|
||||
self.ItemizedHoldings = pd.DataFrame(
|
||||
{
|
||||
"Stocks": portfolio["StockHoldings"][self._stock_tickers].sum(axis=1),
|
||||
"ETFs": portfolio["ETFHoldings"][self._etf_tickers].sum(axis=1),
|
||||
"Crypto": portfolio["CryptoHoldings"][self._crypto_tickers].sum(axis=1),
|
||||
"Cash": portfolio["CashHold"],
|
||||
}
|
||||
)
|
||||
self.portfolio = portfolio.copy()
|
||||
|
||||
# TODO: Add back dividends
|
||||
@log_start_end(log=logger)
|
||||
def get_yahoo_close_prices(self, use_close: bool = False):
|
||||
"""Gets historical adj close prices for tickers in list of trades"""
|
||||
tickers_to_download = self._stock_tickers + self._etf_tickers
|
||||
if tickers_to_download:
|
||||
if not use_close:
|
||||
self._historical_prices = yf.download(
|
||||
tickers_to_download, start=self._start_date, progress=False
|
||||
)["Adj Close"]
|
||||
else:
|
||||
self._historical_prices = yf.download(
|
||||
tickers_to_download, start=self._start_date, progress=False
|
||||
)["Close"]
|
||||
|
||||
# Adjust for case of only 1 ticker
|
||||
if len(tickers_to_download) == 1:
|
||||
self._historical_prices = pd.DataFrame(self._historical_prices)
|
||||
self._historical_prices.columns = tickers_to_download
|
||||
|
||||
else:
|
||||
data_for_index_purposes = yf.download(
|
||||
"AAPL", start=self._start_date, progress=False
|
||||
)["Adj Close"]
|
||||
self._historical_prices = pd.DataFrame()
|
||||
self._historical_prices.index = data_for_index_purposes.index
|
||||
self._historical_prices["stock"] = 0
|
||||
|
||||
self._historical_prices["CASH"] = 1
|
||||
|
||||
# Make columns a multi-index. This helps the merging.
|
||||
self._historical_prices.columns = pd.MultiIndex.from_product(
|
||||
[["Close"], self._historical_prices.columns]
|
||||
)
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_crypto_yfinance(self):
|
||||
"""Gets historical coin data from coingecko"""
|
||||
if self._crypto_tickers:
|
||||
list_of_coins = [f"{coin}-USD" for coin in self._crypto_tickers]
|
||||
self._historical_crypto = yf.download(
|
||||
list_of_coins, start=self._start_date, progress=False
|
||||
)["Close"]
|
||||
|
||||
if len(list_of_coins) == 1:
|
||||
self._historical_crypto = pd.DataFrame(self._historical_crypto)
|
||||
self._historical_crypto.columns = list_of_coins
|
||||
|
||||
self._historical_crypto.columns = pd.MultiIndex.from_product(
|
||||
[["Close"], [col[:-4] for col in self._historical_crypto.columns]]
|
||||
)
|
||||
|
||||
else:
|
||||
self._historical_crypto = pd.DataFrame()
|
||||
self._historical_crypto[
|
||||
pd.MultiIndex.from_product([["Close"], ["crypto"]])
|
||||
] = 0
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def add_benchmark(self, benchmark: str):
|
||||
"""Adds benchmark dataframe"""
|
||||
self.benchmark = yf.download(benchmark, start=self._start_date, progress=False)[
|
||||
"Adj Close"
|
||||
]
|
||||
self.benchmark_returns = self.benchmark.pct_change().dropna()
|
||||
|
||||
@classmethod
|
||||
@log_start_end(log=logger)
|
||||
def from_csv(cls, csv_path: str):
|
||||
"""Class method that generates a portfolio object from a csv file
|
||||
|
||||
Parameters
|
||||
----------
|
||||
csv_path: str
|
||||
Path to csv of trade data
|
||||
|
||||
Returns
|
||||
-------
|
||||
Portfolio
|
||||
Initialized portfolio object
|
||||
"""
|
||||
# Load in a list of trades
|
||||
trades = pd.read_csv(csv_path)
|
||||
# Convert the date to what pandas understands
|
||||
trades.Date = pd.to_datetime(trades.Date)
|
||||
# Build the portfolio object
|
||||
return cls(trades)
|
||||
|
||||
@classmethod
|
||||
@log_start_end(log=logger)
|
||||
def from_custom_inputs_and_weights(
|
||||
cls,
|
||||
start_date: str,
|
||||
list_of_symbols: List[str],
|
||||
list_of_weights: List[float],
|
||||
list_of_types: List[str],
|
||||
amount: float = 100_000,
|
||||
):
|
||||
"""Create a class instance from supplied weights and tickers.
|
||||
This first will generate a dataframe of trades then pass to generator.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
start_date: str
|
||||
Start date
|
||||
list_of_symbols: List[str]
|
||||
List of symbols to consider
|
||||
list_of_weights: List[float]
|
||||
List of weights (in theory negative should be fine), Must add to 1
|
||||
list_of_types: List[str]
|
||||
List of asset type (stock or crypto)
|
||||
amount: float
|
||||
Amount for portfolio allocation
|
||||
|
||||
Returns
|
||||
-------
|
||||
Portfolio
|
||||
Class instance
|
||||
"""
|
||||
if not np.isclose(np.sum(list_of_weights), 1, 0.03):
|
||||
console.print("[red]Weights do not add to 1[/red].")
|
||||
return cls()
|
||||
list_of_amounts = [weight * amount for weight in list_of_weights]
|
||||
# Name,Type,Quantity,Date,Price,Fees,Premium,Side
|
||||
inputs: Dict[str, List[Union[str, float, int]]] = {}
|
||||
inputs["Name"] = ["cash"]
|
||||
inputs["Type"] = ["cash"]
|
||||
inputs["Quantity"] = [1]
|
||||
inputs["Price"] = [amount]
|
||||
inputs["Fees"] = [0]
|
||||
inputs["Premium"] = [0]
|
||||
inputs["Side"] = ["deposit"]
|
||||
inputs["Date"] = [start_date]
|
||||
for ticker, type_, amounts in zip(
|
||||
list_of_symbols, list_of_types, list_of_amounts
|
||||
):
|
||||
inputs["Name"].append(ticker)
|
||||
inputs["Type"].append(type_)
|
||||
inputs["Fees"].append(0)
|
||||
inputs["Premium"].append(0)
|
||||
inputs["Side"].append("buy")
|
||||
if type_ == "crypto":
|
||||
ticker += "-USD"
|
||||
# Find how much of each ticker we can buy
|
||||
temp = yf.download(ticker, start=start_date, progress=False)["Adj Close"]
|
||||
price_on_date = temp[0]
|
||||
inputs["Date"].append(temp.index[0].strftime("%Y-%m-%d"))
|
||||
inputs["Quantity"].append(amounts / price_on_date)
|
||||
inputs["Price"].append(price_on_date)
|
||||
trades = pd.DataFrame.from_dict(inputs)
|
||||
trades.Date = pd.to_datetime(trades.Date)
|
||||
# Make sure to fix the 'cash' date if it is weekend
|
||||
if trades.Date[1] != trades.Date[0]:
|
||||
trades.Date[0] = trades.Date[1]
|
||||
return cls(trades)
|
||||
|
||||
@@ -2,36 +2,35 @@
|
||||
__docformat__ = "numpy"
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import List, Optional
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from os import path
|
||||
from typing import List
|
||||
import os
|
||||
|
||||
import matplotlib.ticker as mtick
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.lines import Line2D
|
||||
from pypfopt import plotting
|
||||
from reportlab.lib.pagesizes import letter
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from reportlab.pdfgen import canvas
|
||||
from reportlab.lib.utils import ImageReader
|
||||
from matplotlib.lines import Line2D
|
||||
from matplotlib import pyplot as plt
|
||||
import matplotlib.ticker as mtick
|
||||
from pypfopt import plotting
|
||||
|
||||
from gamestonk_terminal import feature_flags as gtff
|
||||
from gamestonk_terminal.config_plot import PLOT_DPI
|
||||
from gamestonk_terminal.decorators import log_start_end
|
||||
from gamestonk_terminal.helper_funcs import (
|
||||
export_data,
|
||||
get_rf,
|
||||
plot_autoscale,
|
||||
print_rich_table,
|
||||
)
|
||||
from gamestonk_terminal import feature_flags as gtff
|
||||
from gamestonk_terminal.portfolio import (
|
||||
portfolio_model,
|
||||
reportlab_helpers,
|
||||
yfinance_model,
|
||||
)
|
||||
from gamestonk_terminal.portfolio import reportlab_helpers
|
||||
from gamestonk_terminal.helper_funcs import (
|
||||
get_rf,
|
||||
plot_autoscale,
|
||||
export_data,
|
||||
)
|
||||
from gamestonk_terminal.decorators import log_start_end
|
||||
from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_model
|
||||
from gamestonk_terminal.rich_config import console
|
||||
|
||||
@@ -65,25 +64,177 @@ In order to load a CSV do the following:
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def show_df(df: pd.DataFrame, show: bool) -> None:
|
||||
"""Shows the given dataframe
|
||||
def display_returns_vs_bench(
|
||||
portfolio: portfolio_model.Portfolio,
|
||||
benchmark: str = "SPY",
|
||||
external_axes: Optional[plt.Axes] = None,
|
||||
):
|
||||
"""Display portfolio returns vs benchmark
|
||||
|
||||
Parameters
|
||||
----------
|
||||
df : pd.DataFrame
|
||||
The dataframe to be shown
|
||||
show : bool
|
||||
Whether to show the dataframe index
|
||||
portfolio: Portfolio
|
||||
Custom portfolio object with trade list
|
||||
benchmark: str
|
||||
Symbol for benchmark. Defaults to SPY
|
||||
external_axes: plt.Axes
|
||||
Optional axes to display plot on
|
||||
|
||||
"""
|
||||
if not external_axes:
|
||||
fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
|
||||
else:
|
||||
ax = external_axes
|
||||
|
||||
df = df.dropna(how="all", axis=1).fillna("")
|
||||
portfolio.generate_holdings_from_trades()
|
||||
portfolio.add_benchmark(benchmark)
|
||||
|
||||
print_rich_table(
|
||||
df,
|
||||
headers=list(df.columns),
|
||||
show_index=show,
|
||||
cumulative_returns = (1 + portfolio.returns).cumprod()
|
||||
benchmark_c_returns = (1 + portfolio.benchmark_returns).cumprod()
|
||||
|
||||
ax.plot(cumulative_returns.index, cumulative_returns, label="Portfolio")
|
||||
ax.plot(benchmark_c_returns.index, benchmark_c_returns, label="Benchmark")
|
||||
ax.grid()
|
||||
ax.legend(loc=0)
|
||||
if not external_axes:
|
||||
if gtff.USE_ION:
|
||||
plt.ion()
|
||||
fig.autofmt_xdate()
|
||||
fig.tight_layout()
|
||||
plt.show()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_allocation(portfolio: portfolio_model.Portfolio, export: str = ""):
|
||||
"""Display allocation of assets vs time
|
||||
|
||||
Parameters
|
||||
----------
|
||||
portfolio: Portfolio
|
||||
Portfolio object with trades loaded
|
||||
export: str
|
||||
Format to export plot
|
||||
"""
|
||||
try:
|
||||
portfolio.generate_holdings_from_trades()
|
||||
all_holdings = pd.concat(
|
||||
[
|
||||
portfolio.portfolio["StockHoldings"],
|
||||
portfolio.portfolio["ETFHoldings"],
|
||||
portfolio.portfolio["CryptoHoldings"],
|
||||
],
|
||||
axis=1,
|
||||
)
|
||||
all_holdings = all_holdings.drop(columns=["temp"])
|
||||
fig, ax = plt.subplots(figsize=plot_autoscale(), dpi=PLOT_DPI)
|
||||
all_holdings.plot(ax=ax)
|
||||
ax.set_title("Individual Holdings")
|
||||
ax.legend(loc="upper left")
|
||||
fig.tight_layout(pad=2)
|
||||
if gtff.USE_ION:
|
||||
plt.ion()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
plt.show()
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"rolling",
|
||||
)
|
||||
console.print("")
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_rolling_stats(
|
||||
portfolio: portfolio_model.Portfolio,
|
||||
length: int = 60,
|
||||
benchmark: str = "SPY",
|
||||
risk_free_rate: float = 0,
|
||||
external_axes: Optional[List[plt.Axes]] = None,
|
||||
export: str = "",
|
||||
):
|
||||
"""Display portfolio returns vs benchmark
|
||||
|
||||
Parameters
|
||||
----------
|
||||
portfolio: Portfolio
|
||||
Custom portfolio object with trade list
|
||||
length: int
|
||||
Length of rolling window
|
||||
benchmark: str
|
||||
Symbol for benchmark. Defaults to SPY
|
||||
risk_free_rate: float
|
||||
Value to use for risk free rate in sharpe/other calculations
|
||||
external_axes: Optional[List[plt.Axes]]
|
||||
Optional axes to display plot on
|
||||
export: str
|
||||
Export to file
|
||||
"""
|
||||
portfolio.generate_holdings_from_trades()
|
||||
portfolio.add_benchmark(benchmark)
|
||||
portfolio.add_rf(risk_free_rate)
|
||||
if not external_axes:
|
||||
fig, ax = plt.subplots(4, 1, figsize=(8, 8), dpi=PLOT_DPI, sharex=True)
|
||||
else:
|
||||
ax = external_axes
|
||||
rolling_volatility = portfolio.returns.rolling(length).std()
|
||||
rolling_volatility_bench = portfolio.benchmark_returns.rolling(length).std()
|
||||
|
||||
rolling_sharpe = portfolio.returns.rolling(length).apply(
|
||||
lambda x: (x.mean() - risk_free_rate) / x.std()
|
||||
)
|
||||
rolling_sharpe_bench = portfolio.benchmark_returns.rolling(length).apply(
|
||||
lambda x: (x.mean() - risk_free_rate) / x.std()
|
||||
)
|
||||
|
||||
rolling_volatility.plot(ax=ax[1])
|
||||
rolling_volatility_bench.plot(ax=ax[1])
|
||||
ax[1].set_title("Rolling Volatility")
|
||||
|
||||
rolling_sharpe.plot(ax=ax[2])
|
||||
rolling_sharpe_bench.plot(ax=ax[2])
|
||||
ax[2].set_title("Rolling Sharpe Ratio")
|
||||
|
||||
# Rolling beta is defined as Cov(Port,Bench)/var(Bench)
|
||||
covs = (
|
||||
pd.DataFrame(
|
||||
{"Portfolio": portfolio.returns, "Benchmark": portfolio.benchmark_returns}
|
||||
)
|
||||
.dropna(axis=0)
|
||||
.rolling(length)
|
||||
.cov()
|
||||
.unstack()
|
||||
.dropna()
|
||||
)
|
||||
rolling_beta = covs["Portfolio"]["Benchmark"] / covs["Benchmark"]["Benchmark"]
|
||||
rolling_beta.plot(ax=ax[3])
|
||||
ax[3].set_title("Rolling Beta to Benchmark")
|
||||
|
||||
c_returns = (1 + portfolio.returns).cumprod()
|
||||
bench_c_rets = (1 + portfolio.benchmark_returns).cumprod()
|
||||
|
||||
ax[0].plot(c_returns.index, c_returns)
|
||||
ax[0].plot(bench_c_rets.index, bench_c_rets)
|
||||
ax[0].set_title("Cumulative Returns")
|
||||
|
||||
if not external_axes:
|
||||
|
||||
for a in ax[0], ax[1], ax[2]:
|
||||
a.legend(["Portfolio", "Benchmark"], loc="upper left")
|
||||
for a in ax[0], ax[1], ax[2], ax[3]:
|
||||
a.set_xlim(portfolio.returns.index[0], portfolio.returns.index[-1])
|
||||
a.set_xlabel([])
|
||||
a.grid("on")
|
||||
|
||||
ax[3].set_xlabel("Date")
|
||||
if gtff.USE_ION:
|
||||
plt.ion()
|
||||
fig.tight_layout(pad=2)
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"rolling",
|
||||
)
|
||||
plt.show()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -258,39 +409,39 @@ def plot_ef(
|
||||
return ImageReader(imgdata)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def display_allocation(data: pd.DataFrame, graph: bool):
|
||||
"""Displays allocation
|
||||
Parameters
|
||||
----------
|
||||
data: pd.DataFrame
|
||||
The portfolio allocation dataframe
|
||||
graph: bool
|
||||
If pie chart shall be displayed with table"""
|
||||
|
||||
print_rich_table(data, headers=list(data.columns), title="Allocation")
|
||||
console.print("")
|
||||
|
||||
if graph:
|
||||
graph_data = data[data["pct_allocation"] >= 5].copy()
|
||||
if not graph_data.empty:
|
||||
graph_data.loc["Other"] = [
|
||||
"NA",
|
||||
data["value"].sum() - graph_data["value"].sum(),
|
||||
100 - graph_data["value"].sum(),
|
||||
]
|
||||
labels = graph_data.index.values
|
||||
sizes = graph_data["value"].to_list()
|
||||
else:
|
||||
labels = data.index.values
|
||||
sizes = data["value"].to_list()
|
||||
fig, ax = plt.subplots()
|
||||
ax.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=90)
|
||||
ax.axis("equal")
|
||||
ax.set_title("Portfolio Allocation")
|
||||
fig.set_tight_layout(True)
|
||||
|
||||
plt.show()
|
||||
# @log_start_end(log=logger)
|
||||
# def display_allocation2(data: pd.DataFrame, graph: bool):
|
||||
# """Displays allocation
|
||||
# Parameters
|
||||
# ----------
|
||||
# data: pd.DataFrame
|
||||
# The portfolio allocation dataframe
|
||||
# graph: bool
|
||||
# If pie chart shall be displayed with table"""
|
||||
#
|
||||
# print_rich_table(data, headers=list(data.columns), title="Allocation")
|
||||
# console.print("")
|
||||
#
|
||||
# if graph:
|
||||
# graph_data = data[data["pct_allocation"] >= 5].copy()
|
||||
# if not graph_data.empty:
|
||||
# graph_data.loc["Other"] = [
|
||||
# "NA",
|
||||
# data["value"].sum() - graph_data["value"].sum(),
|
||||
# 100 - graph_data["value"].sum(),
|
||||
# ]
|
||||
# labels = graph_data.index.values
|
||||
# sizes = graph_data["value"].to_list()
|
||||
# else:
|
||||
# labels = data.index.values
|
||||
# sizes = data["value"].to_list()
|
||||
# fig, ax = plt.subplots()
|
||||
# ax.pie(sizes, labels=labels, autopct="%1.1f%%", startangle=90)
|
||||
# ax.axis("equal")
|
||||
# ax.set_title("Portfolio Allocation")
|
||||
# fig.set_tight_layout(True)
|
||||
#
|
||||
# plt.show()
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
@@ -308,10 +459,10 @@ def display_drawdown(holdings: pd.DataFrame, export: str = ""):
|
||||
drawdown = portfolio_model.calculate_drawdown(holdings)
|
||||
fig, ax = plt.subplots(2, 1, figsize=plot_autoscale(), dpi=PLOT_DPI)
|
||||
ax[0].plot(holdings.index, holdings)
|
||||
ax[0].set_title("Cumulative Returns")
|
||||
ax[0].set_title("Holdings")
|
||||
|
||||
ax[1].plot(holdings.index, drawdown)
|
||||
ax[1].fill_between(holdings.index, np.asarray(drawdown["return"]), alpha=0.4)
|
||||
ax[1].fill_between(holdings.index, np.asarray(drawdown), alpha=0.4)
|
||||
ax[1].set_title("Portfolio Drawdown")
|
||||
ax[0].grid()
|
||||
ax[1].grid()
|
||||
@@ -324,13 +475,13 @@ def display_drawdown(holdings: pd.DataFrame, export: str = ""):
|
||||
export_data(
|
||||
export,
|
||||
os.path.dirname(os.path.abspath(__file__)),
|
||||
"mdd",
|
||||
"dd",
|
||||
)
|
||||
|
||||
|
||||
class Report:
|
||||
@log_start_end(log=logger)
|
||||
def __init__(self, df: pd.DataFrame, hist: pd.DataFrame, m_tick: str, n: int):
|
||||
def __init__(self, df: pd.DataFrame, hist: pd.DataFrame, m_tick: str):
|
||||
"""Generate financial reports.
|
||||
Financial reports allow users to show the how they have been performing in
|
||||
trades. This allows for a simple way to show progress and analyze metrics
|
||||
@@ -342,8 +493,8 @@ class Report:
|
||||
The dataframe with previous holdings information
|
||||
hist : pd.DataFrame
|
||||
The dataframe with previous prices for stocks in the portfolio
|
||||
m_tick : str
|
||||
The market asset to be identified
|
||||
df_m : pd.DataFrame
|
||||
Dataframe of benchmark
|
||||
n : int
|
||||
The number of days to analyze
|
||||
|
||||
@@ -361,7 +512,8 @@ class Report:
|
||||
self.hist = hist
|
||||
self.m_tick = m_tick
|
||||
self.df_m = yfinance_model.get_market(self.df.index[0], self.m_tick)
|
||||
self.returns, self.variance = portfolio_model.get_return(df, self.df_m, n)
|
||||
# self.returns, self.variance = portfolio_model.get_return(df, self.df_m, n)
|
||||
self.returns = pd.DataFrame()
|
||||
self.rf = get_rf()
|
||||
self.betas = portfolio_model.get_rolling_beta(
|
||||
self.df, self.hist, self.df_m, 365
|
||||
|
||||
3
gamestonk_terminal/portfolio/portfolios/bitcoin.csv
vendored
Normal file
3
gamestonk_terminal/portfolio/portfolios/bitcoin.csv
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
Name,Type,Quantity,Date,Price,Fees,Premium,Side
|
||||
cash,cash,1,2021/7/20,30000,0,0,deposit
|
||||
btc,crypto,1,2021/7/20,29790,0,0,Buy
|
||||
|
@@ -1,11 +1,6 @@
|
||||
Name,Type,Quantity,Date,Price,Fees,Premium,Side
|
||||
cash,cash,1.0,2019/6/22,2000.0,,,deposit
|
||||
amzn,stock,300.0,2019/10/21,2000.0,20.0,,Buy
|
||||
amzn,stock,100.0,2019/10/21,2100.0,20.0,,Sell
|
||||
amc,stock,400.0,2019/10/21,20.0,20.0,,Buy
|
||||
tsla,stock,10.0,2020/10/10,200.0,20.0,,Sell
|
||||
gme,stock,1.0,2020/10/20,50.0,,,Buy
|
||||
gme,stock,1.0,2021/10/20,50.0,,,Buy
|
||||
amzn,stock,50.0,2021/10/20,2000.0,20.0,,Sell
|
||||
tsla,stock,15.0,2021/10/12,200.0,20.0,,Sell
|
||||
tsla,stock,15.0,2021/10/21,20,0,,Interest
|
||||
cash,cash,1.0,2021/3/24,2000.0,0,0,deposit
|
||||
aapl,stock,10,2021/3/24,120.7,0,0,Buy
|
||||
msft,stock,3,2021/3/26,232.8,0,0,Buy
|
||||
btc,crypto,.006,2021/5/21,37430,0,0,Buy
|
||||
aapl,stock,5,2021/7/2,139.8,0,0,Sell
|
||||
|
||||
|
@@ -1,5 +0,0 @@
|
||||
Name,Type,Quantity,Date,Price,Fees,Premium,Side
|
||||
cash,cash,1.0,2021/3/24,2000.0,,,deposit
|
||||
aapl,stock,10,2021/3/24,120.7,0,0,Buy
|
||||
msft,stock,3,2021/3/26,232.8,0,0,Buy
|
||||
aapl,stock,5,2021/7/2,139.8,0,0,Sell
|
||||
|
@@ -2,20 +2,14 @@
|
||||
__docformat__ = "numpy"
|
||||
|
||||
import difflib
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import pandas as pd
|
||||
import yfinance as yf
|
||||
|
||||
from gamestonk_terminal.decorators import log_start_end
|
||||
import pandas as pd
|
||||
from gamestonk_terminal.stocks.sector_industry_analysis import financedatabase_model
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_stocks(tickers: List[str], start: datetime) -> pd.DataFrame:
|
||||
"""Gets historic data for list of tickers
|
||||
|
||||
@@ -49,7 +43,6 @@ def get_stocks(tickers: List[str], start: datetime) -> pd.DataFrame:
|
||||
return df
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_dividends(tickers: List[str]) -> pd.DataFrame:
|
||||
"""Past dividends for list of tickers
|
||||
|
||||
@@ -71,7 +64,6 @@ def get_dividends(tickers: List[str]) -> pd.DataFrame:
|
||||
return pd.concat(dfs)
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_market(start: datetime, ticker: str = "SPY") -> pd.DataFrame:
|
||||
"""Get historical data for market asset
|
||||
|
||||
@@ -95,7 +87,6 @@ def get_market(start: datetime, ticker: str = "SPY") -> pd.DataFrame:
|
||||
return df.to_frame(name=("Market", "Close"))
|
||||
|
||||
|
||||
@log_start_end(log=logger)
|
||||
def get_country(ticker):
|
||||
country = "NA"
|
||||
data = yf.utils.get_json(f"https://finance.yahoo.com/quote/{ticker}")
|
||||
|
||||
@@ -222,7 +222,7 @@ def test_call_func_expect_queue(expected_queue, func, queue):
|
||||
(
|
||||
"call_show",
|
||||
[],
|
||||
"portfolio_view.show_df",
|
||||
"",
|
||||
[],
|
||||
dict(),
|
||||
),
|
||||
|
||||
@@ -8,13 +8,14 @@ Portfolio:
|
||||
save save your portfolio for future use
|
||||
show show existing transactions
|
||||
add add a security to your portfolio
|
||||
rmv remove a security from your portfolio
|
||||
init initialize empty portfolio
|
||||
build build portfolio from list of tickers and weights
|
||||
|
||||
Reports:
|
||||
ar annual report for performance of a given portfolio
|
||||
Loaded: None
|
||||
|
||||
Graphs:
|
||||
rolling display rolling metrics of portfolio and benchmark
|
||||
rmr graph your returns versus the market's returns
|
||||
al displays the allocation of the portfolio
|
||||
mdd display portfolio drawdown
|
||||
dd display portfolio drawdown
|
||||
al display allocation to given assets over period
|
||||
|
||||
|
||||
14
website/content/portfolio/al/_index.md
vendored
14
website/content/portfolio/al/_index.md
vendored
@@ -1,13 +1,11 @@
|
||||
```
|
||||
usage: al [-l] [-n] [-g] [-h]
|
||||
usage: al [-h] [--export {png,jpg,pdf,svg}]
|
||||
```
|
||||
|
||||
Displays the allocation of the portfolio
|
||||
|
||||
Display allocation
|
||||
```
|
||||
optional arguments:
|
||||
-l, --limit number of etf positions to go through (default: 20)
|
||||
-n, --noetf if etf positions should not be added to the portfolio (default: False)
|
||||
-g, --graph display pi chart of holdings (default: False)
|
||||
-h, --help show this help message (default: False)
|
||||
```
|
||||
--export {png,jpg,pdf,svg}
|
||||
Export figure into png, jpg, pdf, svg (default: )
|
||||
|
||||
```
|
||||
33
website/content/portfolio/build/_index.md
vendored
Normal file
33
website/content/portfolio/build/_index.md
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
```
|
||||
usage: build [-s START] [-t TICKERS] [-c CLASSES] [-w WEIGHTS] [-a AMOUNT] [-h]
|
||||
```
|
||||
|
||||
Build portfolio from list of tickers and weights
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-s START, --start START
|
||||
Start date. (default: 2021-01-04)
|
||||
-t TICKERS, --tickers TICKERS
|
||||
List of symbols separated by commas (i.e AAPL,BTC,DOGE,SPY....) (default: None)
|
||||
-c CLASSES, --class CLASSES
|
||||
Asset class (stock, crypto, etf), separated by commas. (default: None)
|
||||
-w WEIGHTS, --weights WEIGHTS
|
||||
List of weights, separated by comma (default: None)
|
||||
-a AMOUNT, --amount AMOUNT
|
||||
Amount to allocate initially. (default: 100000)
|
||||
-h, --help show this help message (default: False)
|
||||
|
||||
```
|
||||
|
||||
The method of listing trades has resulted in some confusion, so this method is meant to just give portfolio statistics
|
||||
for a custom assortment of tickers and weights. The drawback is that this assumes everything is bought on the same day
|
||||
at 'the closing price'.
|
||||
|
||||
An example of a build is below. This assumes that $100,000 dollars was invested on Jan 5 2018,
|
||||
50% allocated to AAPL, and then 25% to bitcoin and 25% to NFLX.
|
||||
|
||||
```
|
||||
build -t aapl,btc,nflx -c stock,crypto,stock -w .5,.25,.25 -s 2018-01-05 -a 100_000
|
||||
```
|
||||
Currently the weights should add to 1 (so technically shorting is allowed as long as you have a net weights of 1).
|
||||
9
website/content/portfolio/init/_index.md
vendored
Normal file
9
website/content/portfolio/init/_index.md
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
```
|
||||
usage: init [-h]
|
||||
```
|
||||
Re-initialize with empty portfolio.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-h, --help show this help message (default: False)
|
||||
```
|
||||
10
website/content/portfolio/rmv/_index.md
vendored
10
website/content/portfolio/rmv/_index.md
vendored
@@ -1,10 +0,0 @@
|
||||
```
|
||||
usage: rmv [-h]
|
||||
```
|
||||
|
||||
Removes an item from the portfolio
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-h, --help show this help message (default: False)
|
||||
```
|
||||
20
website/content/portfolio/rolling/_index.md
vendored
Normal file
20
website/content/portfolio/rolling/_index.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
```
|
||||
usage: rolling [-b BENCHMARK] [-l LENGTH] [-r RF] [-h] [--export {png,jpg,pdf,svg}]
|
||||
```
|
||||
Show rolling portfolio metrics vs benchmark.
|
||||
|
||||
The plots shown are: cumulative returns vs benchmark, rolling volatility of portfolio and benchmark,
|
||||
rolling sharpe ratio of portfolio and benchmark and the rolling beta with respect to the benchmark.
|
||||
|
||||
```
|
||||
optional arguments:
|
||||
-b BENCHMARK, --benchmark BENCHMARK
|
||||
Choose a ticker to be the benchmark (default: SPY)
|
||||
-l LENGTH, --length LENGTH
|
||||
Length of rolling window (default: 60)
|
||||
-r RF, --rf RF Set risk free rate for calculations. (default: 0.001)
|
||||
-h, --help show this help message (default: False)
|
||||
--export {png,jpg,pdf,svg}
|
||||
Export figure into png, jpg, pdf, svg (default: )
|
||||
|
||||
```
|
||||
10
website/data/menu/main.yml
vendored
10
website/data/menu/main.yml
vendored
@@ -1122,14 +1122,18 @@ main:
|
||||
ref: "/portfolio/show"
|
||||
- name: add
|
||||
ref: "/portfolio/add"
|
||||
- name: rmv
|
||||
ref: "/portfolio/rmv"
|
||||
- name: ar
|
||||
ref: "/portfolio/ar"
|
||||
- name: rmr
|
||||
ref: "/portfolio/rmr"
|
||||
- name: mdd
|
||||
ref: "portfolio/mdd"
|
||||
ref: "portfolio/dd"
|
||||
- name: build
|
||||
ref: "portfolio/build"
|
||||
- name: init
|
||||
ref: "portfolio/init"
|
||||
- name: rolling
|
||||
ref: "portfolio/rolling"
|
||||
- name: forex
|
||||
ref: "/forex"
|
||||
sub:
|
||||
|
||||
Reference in New Issue
Block a user