Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving stock class #80

Merged
merged 4 commits into from
Jul 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 6 additions & 110 deletions finquant/portfolio.py
Original file line number Diff line number Diff line change
@@ -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),
Expand Down Expand Up @@ -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 ``<stock_name> - 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.
Expand Down
112 changes: 112 additions & 0 deletions finquant/stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
""" This module provides a public class ``Stock`` that holds and calculates quantities of a single stock.
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.
"""

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 ``<stock_name> - 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
23 changes: 2 additions & 21 deletions tests/test_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -407,26 +408,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 #
######################################
Expand Down
66 changes: 66 additions & 0 deletions tests/test_stock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
###################
# 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
from finquant.stock import 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]
)