Skip to content

Commit

Permalink
Performance Ratios (OpenBB-finance#922)
Browse files Browse the repository at this point in the history
* Added portfolio ratios

* Fixed graph displaying glitch

* Fixed formatting issues

* Remaned variables

* Fixed fraction function

* Removed function

Co-authored-by: Colin Delahunty <[email protected]>
  • Loading branch information
colin99d and Colin Delahunty authored Nov 6, 2021
1 parent 068f22d commit cb03631
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 40 deletions.
6 changes: 3 additions & 3 deletions gamestonk_terminal/portfolio/portfolio_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ def is_ticker(ticker: str) -> bool:
Returns
----------
answer : bool
bool
Whether the string is a ticker
"""
item = yf.Ticker(ticker)
Expand All @@ -31,7 +31,7 @@ def beta_word(beta: float) -> str:
Returns
----------
text : str
str
The description of the beta
"""
if abs(1 - beta) > 3:
Expand All @@ -56,7 +56,7 @@ def clean_name(name: str) -> str:
Returns
----------
text : str
str
A cleaned value
"""
return name.replace("beta_", "").upper()
17 changes: 13 additions & 4 deletions gamestonk_terminal/portfolio/portfolio_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def get_main_text(df: pd.DataFrame) -> str:
t_debt = (
"Margin was not used this year. This reduces this risk of the portfolio."
)
string = (
text = (
f"Your portfolio's performance for the period was {df['return'][-1]:.2%}. This was"
f" {'greater' if df['return'][-1] > df[('Market', 'Return')][-1] else 'less'} than"
f" the market return of {df[('Market', 'Return')][-1]:.2%}. The variance for the"
Expand All @@ -399,7 +399,7 @@ def get_main_text(df: pd.DataFrame) -> str:
f" various analytics from the portfolio. Read below to see the moving beta for a"
f" stock."
)
return string
return text


def get_beta_text(df: pd.DataFrame) -> str:
Expand All @@ -418,7 +418,7 @@ def get_beta_text(df: pd.DataFrame) -> str:
betas = df[list(filter(lambda score: "beta" in score, list(df.columns)))]
high = betas.idxmax(axis=1)
low = betas.idxmin(axis=1)
string = (
text = (
"Beta is how strongly a portfolio's movements correlate with the market's movements."
" A stock with a high beta is considered to be riskier. The beginning beta for the period"
f" was {portfolio_helper.beta_word(df['total'][0])} at {df['total'][0]:.2f}. This went"
Expand All @@ -428,4 +428,13 @@ def get_beta_text(df: pd.DataFrame) -> str:
f" {portfolio_helper.clean_name(high[-1] if df['total'][-1] > 1 else low[-1])}, which had"
f" an ending beta of {df[high[-1]][-1] if df['total'][-1] > 1 else df[low[-1]][-1]:.2f}."
)
return string
return text


performance_text = (
"The Sharpe ratio is a measure of reward to total volatility. A Sharpe ratio above one is"
" considered acceptable. The Treynor ratio is a measure of systematic risk to reward."
" Alpha is the average return above what CAPM predicts. This measure should be above zero"
". The information ratio is the excess return on systematic risk. An information ratio of"
" 0.4 to 0.6 is considered good."
)
88 changes: 57 additions & 31 deletions gamestonk_terminal/portfolio/portfolio_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from io import BytesIO
from os import path

import numpy as np
import pandas as pd
from tabulate import tabulate
from reportlab.lib.pagesizes import letter
Expand All @@ -18,7 +19,10 @@

from gamestonk_terminal.config_plot import PLOT_DPI
from gamestonk_terminal import feature_flags as gtff
from gamestonk_terminal.portfolio import portfolio_model, yfinance_model
from gamestonk_terminal.portfolio import (
portfolio_model,
yfinance_model,
)
from gamestonk_terminal.portfolio import reportlab_helpers
from gamestonk_terminal.helper_funcs import get_rf
from gamestonk_terminal.portfolio.portfolio_optimization import optimizer_model
Expand Down Expand Up @@ -96,7 +100,6 @@ def plot_overall_return(
img : ImageReader
Overal return graph
"""
plt.close("all")
fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(comb.index, comb["return"], color="tab:blue", label="Portfolio")
ax.plot(comb.index, comb[("Market", "Return")], color="orange", label=m_tick)
Expand Down Expand Up @@ -132,11 +135,11 @@ def plot_overall_return(
plt.show()
print("")
return None

image_data = BytesIO()
fig.savefig(image_data, format="png")
image_data.seek(0)
return ImageReader(image_data)
imgdata = BytesIO()
fig.savefig(imgdata, format="png")
plt.close("all")
imgdata.seek(0)
return ImageReader(imgdata)


def plot_rolling_beta(df: pd.DataFrame) -> ImageReader:
Expand All @@ -152,7 +155,6 @@ def plot_rolling_beta(df: pd.DataFrame) -> ImageReader:
img : ImageReader
Rolling beta graph
"""
plt.close("all")

fig, ax = plt.subplots(figsize=(10, 5))
ax.plot(
Expand Down Expand Up @@ -183,16 +185,18 @@ def plot_rolling_beta(df: pd.DataFrame) -> ImageReader:
)
ax.set_facecolor("white")
fig.autofmt_xdate()
image_data = BytesIO()
fig.savefig(image_data, format="png")
image_data.seek(0)
return ImageReader(image_data)
imgdata = BytesIO()
fig.savefig(imgdata, format="png")
plt.close("all")
imgdata.seek(0)
return ImageReader(imgdata)


def plot_ef(
stocks: List[str],
variance: float,
per_ret: float,
rf_rate: float,
period: str = "3mo",
n_portfolios: int = 300,
risk_free: bool = False,
Expand All @@ -207,6 +211,8 @@ def plot_ef(
The variance for the portfolio
per_ret : float
The portfolio's return for the portfolio
rf_rate : float
The risk free rate
period : str
The period to track
n_portfolios : int
Expand All @@ -222,17 +228,16 @@ def plot_ef(
ax.scatter(stds, rets, marker=".", c=sharpes, cmap="viridis_r")
plotting.plot_efficient_frontier(ef, ax=ax, show_assets=True)
# Find the tangency portfolio
rfrate = get_rf()
ret_sharpe, std_sharpe, _ = ef.portfolio_performance(risk_free_rate=rfrate)
ret_sharpe, std_sharpe, _ = ef.portfolio_performance(risk_free_rate=rf_rate)
ax.scatter(std_sharpe, ret_sharpe, marker="*", s=100, c="r", label="Max Sharpe")
plt.plot(variance, per_ret, "ro", label="Portfolio")
# Add risk free line
if risk_free:
y = ret_sharpe * 1.2
m = (ret_sharpe - rfrate) / std_sharpe
x2 = (y - rfrate) / m
m = (ret_sharpe - rf_rate) / std_sharpe
x2 = (y - rf_rate) / m
x = [0, x2]
y = [rfrate, y]
y = [rf_rate, y]
line = Line2D(x, y, color="#FF0000", label="Capital Allocation Line")
ax.set_xlim(xmin=min(stds) * 0.8)
ax.add_line(line)
Expand All @@ -244,10 +249,11 @@ def plot_ef(
if gtff.USE_ION:
plt.ion()

image_data = BytesIO()
fig.savefig(image_data, format="png")
image_data.seek(0)
return ImageReader(image_data)
imgdata = BytesIO()
fig.savefig(imgdata, format="png")
plt.close("all")
imgdata.seek(0)
return ImageReader(imgdata)


class Report:
Expand Down Expand Up @@ -283,6 +289,10 @@ def __init__(self, df: pd.DataFrame, hist: pd.DataFrame, m_tick: str, n: int):
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.rf = get_rf()
self.betas = portfolio_model.get_rolling_beta(
self.df, self.hist, self.df_m, 365
)

def generate_report(self) -> None:
d = path.dirname(path.abspath(__file__)).replace(
Expand All @@ -297,25 +307,41 @@ def generate_report(self) -> None:
report = canvas.Canvas(loc, pagesize=letter)
reportlab_helpers.base_format(report, "Overview")
self.generate_pg1(report)
self.generate_pg2(report, self.df_m)
self.generate_pg2(report)
report.save()
print("File save in:\n", loc, "\n")

def generate_pg1(self, report: canvas.Canvas) -> None:
report.drawImage(
plot_overall_return(self.returns, self.m_tick, False), 15, 400, 600, 300
)
main_t = portfolio_model.get_main_text(self.returns)
reportlab_helpers.draw_paragraph(report, main_t, 30, 410, 550, 200)
main_text = portfolio_model.get_main_text(self.returns)
reportlab_helpers.draw_paragraph(report, main_text, 30, 410, 550, 200)
current_return = self.returns["return"][-1]
beta = self.betas["total"][-1]
market_return = self.returns[("Market", "Return")][-1]
sharpe = f"{(current_return - self.rf)/ np.std(self.returns['return']):.2f}"
treynor = f"{(current_return - self.rf)/ beta:.2f}" if beta > 0 else "N/A"
alpha = f"{current_return - (self.rf + beta * (market_return - self.rf)):.2f}"
information = (
f"{float(alpha)/ (np.std(self.returns['return'] - market_return)):.2f}"
)
perf = [
["Sharpe", sharpe],
["Treynor", treynor],
["Alpha", alpha],
["Information", information],
]
reportlab_helpers.draw_table(report, "Performance", 540, 300, 30, perf)
reportlab_helpers.draw_paragraph(
report, portfolio_model.performance_text, 140, 290, 460, 200
)
report.showPage()

def generate_pg2(self, report: canvas.Canvas, df_m: pd.DataFrame) -> None:
def generate_pg2(self, report: canvas.Canvas) -> None:
reportlab_helpers.base_format(report, "Portfolio Analysis")
if "Holding" in self.df.columns:
rolling_beta = portfolio_model.get_rolling_beta(
self.df, self.hist, df_m, 365
)
report.drawImage(plot_rolling_beta(rolling_beta), 15, 400, 600, 300)
main_t = portfolio_model.get_beta_text(rolling_beta)
report.drawImage(plot_rolling_beta(self.betas), 15, 400, 600, 300)
main_t = portfolio_model.get_beta_text(self.betas)
reportlab_helpers.draw_paragraph(report, main_t, 30, 410, 550, 200)
# report.drawImage(plot_ef(uniques, self.variance, self.returns["return"][-1]), 15, 65, 600, 300)
# report.drawImage(plot_ef(uniques, self.variance, self.returns["return"][-1], self.rf), 15, 65, 600, 300)
72 changes: 70 additions & 2 deletions gamestonk_terminal/portfolio/reportlab_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
__docformat__ = "numpy"

from datetime import datetime
from typing import List

from reportlab.lib import colors
from reportlab.pdfgen import canvas
from reportlab.lib.styles import ParagraphStyle
from reportlab.platypus import Paragraph
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
from reportlab.platypus import Paragraph, Table, TableStyle


def base_format(report: canvas.Canvas, header: str) -> None:
Expand All @@ -32,8 +34,74 @@ def base_format(report: canvas.Canvas, header: str) -> None:
def draw_paragraph(
report: canvas.Canvas, msg: str, x: int, y: int, max_width: int, max_height: int
) -> None:
"""Draws a given paragraph
Parameters
----------
report : canvas.Canvas
The report to be formatted
msg : str
The contents of the paragraph
x : int
The x coordinate for the paragraph
y : int
The y coordinate for the paragraph
max_width : int
The maximum width allowed for the paragraph
max_height : int
The maximum height allowed for the paragraph
"""
message_style = ParagraphStyle("Normal")
message = msg.replace("\n", "<br />")
paragraph = Paragraph(message, style=message_style)
_, h = paragraph.wrap(max_width, max_height)
paragraph.drawOn(report, x, y - h)


def draw_table(
report: canvas.Canvas,
header_txt: str,
aW: int,
aH: int,
x: int,
data: List[List[str]],
) -> None:
"""Draw a table at given coordinates
Parameters
----------
report : canvas.Canvas
The report to be formatted
header_txt : str
The header for the table
aW : int
The width for the table
aH : int
The height for the table
x : int
The x coordinate for the table
data : List[List[str]]
Data to show
"""
style = getSampleStyleSheet()["BodyText"]
header = Paragraph(f"<bold><font size=14>{header_txt}</font></bold>", style)

t = Table(data)
t.setStyle(
TableStyle(
[
("BOX", (0, 0), (-1, -1), 0.25, colors.black),
("INNERGRID", (0, 0), (-1, -1), 0.25, colors.black),
]
)
)

for each in range(len(data)):
bg_color = colors.whitesmoke if each % 2 == 0 else colors.lightgrey
t.setStyle(TableStyle([("BACKGROUND", (0, each), (-1, each), bg_color)]))

_, h = header.wrap(aW, aH)
header.drawOn(report, x, aH)
aH = aH - h
_, h = t.wrap(aW, aH)
t.drawOn(report, x, aH - h)

0 comments on commit cb03631

Please sign in to comment.