From 161755e2d30e2c715efc1fa0c24153f42af426d8 Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Fri, 7 Jul 2023 08:15:27 +0200 Subject: [PATCH 1/4] Moving the stock class in a separate file. The docstring are updated to reflect the changes --- finquant/portfolio.py | 116 +++--------------------------------------- finquant/stock.py | 112 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 110 deletions(-) create mode 100644 finquant/stock.py diff --git a/finquant/portfolio.py b/finquant/portfolio.py index ee3bda76..cda8ce77 100644 --- a/finquant/portfolio.py +++ b/finquant/portfolio.py @@ -1,17 +1,16 @@ """This module is the **core** of `FinQuant`. It provides -- a public class ``Stock`` that holds and calculates quantities of a single stock, - a public class ``Portfolio`` that holds and calculates quantities of a financial - portfolio, which is a collection of Stock instances. + portfolio, which is a collection of ``Stock`` instances (the ``Stock`` class is provided in ``finquant.stock``). - a public function ``build_portfolio()`` that automatically constructs and returns - an instance of ``Portfolio`` and instances of ``Stock``. The relevant stock - data is either retrieved through `quandl`/`yfinance` or provided by the user as a + an instance of ``Portfolio`` and instances of ``Stock``. + The relevant stock data is either retrieved through `quandl`/`yfinance` or provided by the user as a ``pandas.DataFrame`` (after loading it manually from disk/reading from file). For an example on how to use it, please read the corresponding docstring, or have a look at the examples in the sub-directory ``example``. -The classes ``Stock`` and ``Portfolio`` are designed to easily manage your -financial portfolio, and make the most common quantitative calculations, such as: +The class ``Portfolio`` is designed to easily manage your financial portfolio, +and makes the most common quantitative calculations, such as: - cumulative returns of the portfolio's stocks - daily returns of the portfolio's stocks (daily percentage change), @@ -55,114 +54,11 @@ from finquant.returns import historical_mean_return from finquant.returns import daily_returns, cumulative_returns from finquant.returns import daily_log_returns +from finquant.stock import Stock from finquant.efficient_frontier import EfficientFrontier from finquant.monte_carlo import MonteCarloOpt -class Stock(object): - """Object that contains information about a stock/fund. - To initialise the object, it requires a name, information about - the stock/fund given as one of the following data structures: - - - ``pandas.Series`` - - ``pandas.DataFrame`` - - The investment information can contain as little information as its name, - and the amount invested in it, the column labels must be ``Name`` and ``Allocation`` - respectively, but it can also contain more information, such as - - - Year - - Strategy - - CCY - - etc. - - It also requires either data, e.g. daily closing prices as a - ``pandas.DataFrame`` or ``pandas.Series``. - ``data`` must be given as a ``pandas.DataFrame``, and at least one data column - is required to containing the closing price, hence it is required to - contain one column label `` - Adj. Close`` which is used to - compute the return of investment. However, ``data`` can contain more - data in additional columns. - """ - - def __init__(self, investmentinfo, data): - """ - :Input: - :investmentinfo: ``pandas.DataFrame`` of investment information - :data: ``pandas.DataFrame`` of stock price - """ - self.name = investmentinfo.Name - self.investmentinfo = investmentinfo - self.data = data - # compute expected return and volatility of stock - self.expected_return = self.comp_expected_return() - self.volatility = self.comp_volatility() - self.skew = self._comp_skew() - self.kurtosis = self._comp_kurtosis() - - # functions to compute quantities - def comp_daily_returns(self): - """Computes the daily returns (percentage change). - See ``finquant.returns.daily_returns``. - """ - return daily_returns(self.data) - - def comp_expected_return(self, freq=252): - """Computes the Expected Return of the stock. - - :Input: - :freq: ``int`` (default: ``252``), number of trading days, default - value corresponds to trading days in a year - - :Output: - :expected_return: Expected Return of stock. - """ - return historical_mean_return(self.data, freq=freq) - - def comp_volatility(self, freq=252): - """Computes the Volatility of the stock. - - :Input: - :freq: ``int`` (default: ``252``), number of trading days, default - value corresponds to trading days in a year - - :Output: - :volatility: Volatility of stock. - """ - return self.comp_daily_returns().std() * np.sqrt(freq) - - def _comp_skew(self): - """Computes and returns the skewness of the stock.""" - return self.data.skew().values[0] - - def _comp_kurtosis(self): - """Computes and returns the Kurtosis of the stock.""" - return self.data.kurt().values[0] - - def properties(self): - """Nicely prints out the properties of the stock: Expected Return, - Volatility, Skewness, Kurtosis as well as the ``Allocation`` (and other - information provided in investmentinfo.) - """ - # nicely printing out information and quantities of the stock - string = "-" * 50 - string += "\nStock: {}".format(self.name) - string += "\nExpected Return:{:0.3f}".format(self.expected_return.values[0]) - string += "\nVolatility: {:0.3f}".format(self.volatility.values[0]) - string += "\nSkewness: {:0.5f}".format(self.skew) - string += "\nKurtosis: {:0.5f}".format(self.kurtosis) - string += "\nInformation:" - string += "\n" + str(self.investmentinfo.to_frame().transpose()) - string += "\n" - string += "-" * 50 - print(string) - - def __str__(self): - # print short description - string = "Contains information about " + str(self.name) + "." - return string - - class Portfolio(object): """Object that contains information about a investment portfolio. To initialise the object, it does not require any input. diff --git a/finquant/stock.py b/finquant/stock.py new file mode 100644 index 00000000..e0d88084 --- /dev/null +++ b/finquant/stock.py @@ -0,0 +1,112 @@ +""" This module provides a public class ``Stock`` that holds and calculates quantities of a single stock. +Istances of this class are used in the ``Portfolio`` class (provided in ``finquant.portfolio``). +Every time a new instance of ``Stock`` is added to ``Portfolio``, the quantities of the portfolio are updated. +""" + +import numpy as np +from finquant.returns import historical_mean_return +from finquant.returns import daily_returns + + +class Stock(object): + """Object that contains information about a stock/fund. + To initialise the object, it requires a name, information about + the stock/fund given as one of the following data structures: + + - ``pandas.Series`` + - ``pandas.DataFrame`` + + The investment information can contain as little information as its name, + and the amount invested in it, the column labels must be ``Name`` and ``Allocation`` + respectively, but it can also contain more information, such as + + - Year + - Strategy + - CCY + - etc. + + It also requires either data, e.g. daily closing prices as a + ``pandas.DataFrame`` or ``pandas.Series``. + ``data`` must be given as a ``pandas.DataFrame``, and at least one data column + is required to containing the closing price, hence it is required to + contain one column label `` - Adj. Close`` which is used to + compute the return of investment. However, ``data`` can contain more + data in additional columns. + """ + + def __init__(self, investmentinfo, data): + """ + :Input: + :investmentinfo: ``pandas.DataFrame`` of investment information + :data: ``pandas.DataFrame`` of stock price + """ + self.name = investmentinfo.Name + self.investmentinfo = investmentinfo + self.data = data + # compute expected return and volatility of stock + self.expected_return = self.comp_expected_return() + self.volatility = self.comp_volatility() + self.skew = self._comp_skew() + self.kurtosis = self._comp_kurtosis() + + # functions to compute quantities + def comp_daily_returns(self): + """Computes the daily returns (percentage change). + See ``finquant.returns.daily_returns``. + """ + return daily_returns(self.data) + + def comp_expected_return(self, freq=252): + """Computes the Expected Return of the stock. + + :Input: + :freq: ``int`` (default: ``252``), number of trading days, default + value corresponds to trading days in a year + + :Output: + :expected_return: Expected Return of stock. + """ + return historical_mean_return(self.data, freq=freq) + + def comp_volatility(self, freq=252): + """Computes the Volatility of the stock. + + :Input: + :freq: ``int`` (default: ``252``), number of trading days, default + value corresponds to trading days in a year + + :Output: + :volatility: Volatility of stock. + """ + return self.comp_daily_returns().std() * np.sqrt(freq) + + def _comp_skew(self): + """Computes and returns the skewness of the stock.""" + return self.data.skew().values[0] + + def _comp_kurtosis(self): + """Computes and returns the Kurtosis of the stock.""" + return self.data.kurt().values[0] + + def properties(self): + """Nicely prints out the properties of the stock: Expected Return, + Volatility, Skewness, Kurtosis as well as the ``Allocation`` (and other + information provided in investmentinfo.) + """ + # nicely printing out information and quantities of the stock + string = "-" * 50 + string += "\nStock: {}".format(self.name) + string += "\nExpected Return:{:0.3f}".format(self.expected_return.values[0]) + string += "\nVolatility: {:0.3f}".format(self.volatility.values[0]) + string += "\nSkewness: {:0.5f}".format(self.skew) + string += "\nKurtosis: {:0.5f}".format(self.kurtosis) + string += "\nInformation:" + string += "\n" + str(self.investmentinfo.to_frame().transpose()) + string += "\n" + string += "-" * 50 + print(string) + + def __str__(self): + # print short description + string = "Contains information about " + str(self.name) + "." + return string From 12dc90ba4f0f985786b077804bbb5513853e324d Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Fri, 7 Jul 2023 08:20:18 +0200 Subject: [PATCH 2/4] fixed typo in docstring --- finquant/stock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/finquant/stock.py b/finquant/stock.py index e0d88084..c1912163 100644 --- a/finquant/stock.py +++ b/finquant/stock.py @@ -1,5 +1,5 @@ """ This module provides a public class ``Stock`` that holds and calculates quantities of a single stock. -Istances of this class are used in the ``Portfolio`` class (provided in ``finquant.portfolio``). +Instances of this class are used in the ``Portfolio`` class (provided in ``finquant.portfolio``). Every time a new instance of ``Stock`` is added to ``Portfolio``, the quantities of the portfolio are updated. """ From 54bba66c4082a1fe1225d3530859ad1208461f1c Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Fri, 7 Jul 2023 16:12:03 +0200 Subject: [PATCH 3/4] moving stock_test to another file as well. --- tests/test_portfolio.py | 20 ------------- tests/test_stock.py | 65 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 tests/test_stock.py diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index a29bbdfa..7cec16eb 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -407,26 +407,6 @@ def test_buildPF_fail_26(): build_portfolio(**d) -################### -# tests for Stock # -################### - - -def test_Stock(): - d = d_pass[3] - pf = build_portfolio(**d) - # loop over all stocks stored within pf and check that values - # are equal to the ones in pf - for i in range(len(pf.stocks)): - assert isinstance(pf.get_stock(names[0]), Stock) - stock = pf.get_stock(names[i]) - assert stock.name == pf.portfolio["Name"][i] - assert all(stock.data - pf.data[stock.name].to_frame() <= strong_abse) - assert all( - stock.investmentinfo == pf.portfolio.loc[pf.portfolio["Name"] == stock.name] - ) - - ###################################### # tests for Monte Carlo optimisation # ###################################### diff --git a/tests/test_stock.py b/tests/test_stock.py new file mode 100644 index 00000000..b688e86f --- /dev/null +++ b/tests/test_stock.py @@ -0,0 +1,65 @@ +################### +# tests for Stock # +################### + +import os +import pathlib +import numpy as np +import pandas as pd +import datetime +import quandl +import yfinance +import pytest +from finquant.portfolio import build_portfolio, Stock + +# comparisons +strong_abse = 1e-15 +weak_abse = 1e-8 + +# setting quandl api key +quandl.ApiConfig.api_key = os.getenv("QUANDLAPIKEY") + +# read data from file +df_pf_path = pathlib.Path.cwd() / ".." / "data" / "ex1-portfolio.csv" +df_data_path = pathlib.Path.cwd() / ".." / "data" / "ex1-stockdata.csv" +df_pf = pd.read_csv(df_pf_path) +df_data = pd.read_csv(df_data_path, index_col="Date", parse_dates=True) +# create testing variables +names = df_pf.Name.values.tolist() +names_yf = [name.replace("WIKI/", "") for name in names] +weights_df_pf = [ + 0.31746031746031744, + 0.15873015873015872, + 0.23809523809523808, + 0.2857142857142857, +] +weights_no_df_pf = [1.0 / len(names) for i in range(len(names))] +df_pf2 = pd.DataFrame({"Allocation": weights_no_df_pf, "Name": names}) +df_pf2_yf = pd.DataFrame({"Allocation": weights_no_df_pf, "Name": names_yf}) +start_date = datetime.datetime(2015, 1, 1) +end_date = "2017-12-31" + +# create kwargs to be passed to build_portfolio +d_pass = [ + { + "names": names, + "start_date": start_date, + "end_date": end_date, + "data_api": "quandl", + } +] + + +def test_Stock(): + d = d_pass[0] + pf = build_portfolio(**d) + # loop over all stocks stored within pf and check that values + # are equal to the ones in pf + for i in range(len(pf.stocks)): + assert isinstance(pf.get_stock(names[0]), Stock) + stock = pf.get_stock(names[i]) + assert stock.name == pf.portfolio["Name"][i] + assert all(stock.data - pf.data[stock.name].to_frame() <= strong_abse) + assert all( + stock.investmentinfo == pf.portfolio.loc[pf.portfolio["Name"] == stock.name] + ) From 0892bf1c66d63a076c71dcdcb3695976c08909d5 Mon Sep 17 00:00:00 2001 From: Pietropaolo Frisoni Date: Fri, 7 Jul 2023 16:26:22 +0200 Subject: [PATCH 4/4] importing Stock class from the new file stock.py (and no more from portfolio.py) --- tests/test_portfolio.py | 3 ++- tests/test_stock.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_portfolio.py b/tests/test_portfolio.py index 7cec16eb..94d58f8b 100644 --- a/tests/test_portfolio.py +++ b/tests/test_portfolio.py @@ -11,7 +11,8 @@ import quandl import yfinance import pytest -from finquant.portfolio import build_portfolio, Stock, Portfolio +from finquant.portfolio import build_portfolio, Portfolio +from finquant.stock import Stock from finquant.efficient_frontier import EfficientFrontier # comparisons diff --git a/tests/test_stock.py b/tests/test_stock.py index b688e86f..da7b742d 100644 --- a/tests/test_stock.py +++ b/tests/test_stock.py @@ -10,7 +10,8 @@ import quandl import yfinance import pytest -from finquant.portfolio import build_portfolio, Stock +from finquant.portfolio import build_portfolio +from finquant.stock import Stock # comparisons strong_abse = 1e-15