Skip to content

Instantly share code, notes, and snippets.

@hurbeana
Created April 20, 2021 15:11
Show Gist options
  • Save hurbeana/675837479b5fe8331590ddf65e67c7ef to your computer and use it in GitHub Desktop.
Save hurbeana/675837479b5fe8331590ddf65e67c7ef to your computer and use it in GitHub Desktop.
pef_exam2 formula collection

PEF Exam 2 Functions

The functions need the package scipy (for the delta normal approach)

pip install scipy

testing

To test the function run you need pytest and then just run pytest from the folder with then functions.py and the test_functions.py

from dataclasses import dataclass, field
from math import sqrt, log, exp
from enum import Enum
from scipy.stats import norm
def to_perc(num, digits=4):
return round(num * 100, digits)
def calc_sr(p_minus_1, old, new, ip=0.0, div=0.0, dividend_new_shares=1.0):
"""calculates the subscription right price"""
if ip != 0.0: # for SEO
return p_minus_1 - (
(p_minus_1 * old + (ip + div * dividend_new_shares) * new) / (old + new)
)
else: # for stocksplit
return p_minus_1 - (p_minus_1 * old) / (old + new - 1)
def correction_factor(p_minus_1, sr):
"""returns the correction factor f given the subscription right sr"""
return (p_minus_1 - sr) / p_minus_1
def compound_interest(periods, rate):
"""calculates the compound interest rate given the periods and rate"""
return (1 + rate) ** periods
def discrete_to_cont_subperiods(rate, subperiods):
"""calculates the continous interest rate given the subperiods and discrete rate"""
return (1 + rate / subperiods) ** subperiods - 1
def disc_to_cont(rate):
"""discrete rate to continous"""
return log(1 + rate)
def cont_to_disc(rate):
"""continous rate to discrete rate"""
return exp(rate) - 1
def geometric_return(buy_and_hold_return, n):
return (1 + buy_and_hold_return) ** (1 / n) - 1
def var_delta_normal(market_value, expected_return, volatility, alpha):
return -market_value * (expected_return + volatility * norm.ppf(alpha))
def utility(expected_return, a, variance, **kwargs):
return expected_return - 0.5 * a * variance
@dataclass
class Day:
priceBefore: float
priceToday: float
sr: float = 0.0
div: float = 0.0
old: int = 0
new: int = 0
ip: float = 0
name: str = ""
dividend_new_shares: float = 1 # how many % new shares get dividend
@property
def f(self):
if self.old and self.new:
self.sr = calc_sr(
self.priceBefore,
self.old,
self.new,
ip=self.ip,
div=self.div,
dividend_new_shares=self.dividend_new_shares,
)
if self.sr:
return correction_factor(self.priceBefore, self.sr)
return 1
@property
def corrected_price_minus_one(self):
return self.f * self.priceBefore
@property
def return_today(self):
return (self.priceToday - self.f * self.priceBefore + self.div) / (
self.f * self.priceBefore
)
class ReturnCalculator:
def __init__(self):
self.first_closing_price = 0.0
self.days = []
def add_day(self, closing_price: float, **kwargs):
""" adds a day to the returncalculator """
if self.first_closing_price == 0.0:
self.first_closing_price = closing_price
else:
try:
last_closing_price = self.days[-1].priceToday
except IndexError:
last_closing_price = self.first_closing_price
self.days.append(Day(last_closing_price, closing_price, **kwargs))
@property
def returns(self):
""" calculated returns for all given days """
return list(map(lambda x: x.return_today, self.days))
@property
def buy_and_hold_return(self):
""" Buy and Hold returns """
bhr = 1
for ret in self.returns:
bhr *= 1 + ret
return bhr - 1
@property
def geometric_return(self):
return geometric_return(self.buy_and_hold_return, len(self.days))
@dataclass
class SeoPortfolio:
seo_day: Day
shares: float
cash_value: float
stock_value: float = field(init=False)
def __post_init__(self):
# calculate stock value with price of today, this case is when she ignores the SEO
self.stock_value = self.seo_day.priceBefore * self.shares
def exercise_sr(self, before_trading=False):
new_shares = self.shares / self.seo_day.old * self.seo_day.new
buy_price = self.seo_day.ip * new_shares
self.cash_value -= buy_price
self.shares += new_shares
if before_trading:
self.stock_value = self.shares * self.seo_day.corrected_price_minus_one
else:
self.stock_value = self.shares * self.seo_day.priceToday
def sell_sr(self, before_trading=False):
new_cash = self.shares * self.seo_day.sr
self.cash_value += new_cash
if before_trading:
self.stock_value = self.shares * self.seo_day.corrected_price_minus_one
else:
self.stock_value = self.shares * self.seo_day.priceToday
@property
def total_value(self):
return self.cash_value + self.stock_value
@dataclass
class Portfolio:
era: float # expected return a
erb: float # expected return b
stda: float # standard deviation a
stdb: float # standard deviation b
corr: float # correlation a and b
xa: float = 0.0 # amount of a (in percent)
xb: float = field(init=False) # amount of b (in percent)
def __post_init__(self):
if self.xa == 0.0:
self.xa = (self.stdb ** 2 - self.corr * self.stda * self.stdb) / (
self.stda ** 2 + self.stdb ** 2 - 2 * self.corr * self.stda * self.stdb
)
self.xb = 1 - self.xa
@property
def volatility(self):
return sqrt(
self.xa ** 2 * self.stda ** 2
+ self.xb ** 2 * self.stdb ** 2
+ 2 * self.xa * self.xb * self.corr * self.stda * self.stdb
)
@property
def expected_return(self):
return self.xa * self.era + self.xb * self.erb
@dataclass
class CAPMPortfolio:
market_return: float # market expected return
market_vol: float # market volatility
risk_free: float # risk free expected return
@property
def capm_equation(self):
"""CAPM/CML equation given the parameters of the CAPM/CML"""
return (
f"E[R_P] = "
f"{self.risk_free * 100}% "
f"+ {round((self.market_return - self.risk_free) / self.market_vol, 4)} "
f"* sigma_P"
)
@property
def sml_equation(self):
"""SML equation given the parameters of the market portfolio"""
return (
f"E[R_P] = "
f"{self.risk_free * 100}% + "
f"beta_P * "
f"{(self.market_return - self.risk_free) * 100}%"
)
def risk(self, expected_return):
"""risk of the given portfolio with expected return"""
return (
(expected_return - self.risk_free)
/ (self.market_return - self.risk_free)
* self.market_vol
)
def xm(self, expected_return):
"""amount of risky assets to be bought to achieve given expected return"""
return (expected_return - self.risk_free) / (
-self.risk_free + self.market_return
)
def xf(self, expected_return):
"""amount of risk free assets to be bought to achieve given expected return"""
return 1 - self.xm(expected_return)
def market_investment(self, expected_return, investment):
"""amount of risky assets invested to achieve given expected return"""
return self.xm(expected_return) * investment
def risk_free_investment(self, expected_return, investment):
"""amount of risk free assets invested to achieve given expected return"""
return self.xf(expected_return) * investment
def return_on_investment(self, investment_risk_free, investment_market):
"""
calculates the amount of expected cash at the end of the year
it is the initial investment + returns
"""
return (
investment_market
+ investment_risk_free
+ self.risk_free * investment_risk_free
+ self.market_return * investment_market
)
def expected_return(self, xf, xm):
"""expected return given amount of risk free assets (xf) and market assets (xm) in percent"""
return xf * self.risk_free + xm * self.market_return
def volatility(self, xm):
"""volatility of the portfolio and amount of percent of market assets"""
return xm * self.market_vol
def beta(self, xm):
return xm
def beta_of_stock(self, volatility, correlation, **kwargs):
"""calculates the beta of a stock in relation to the market"""
return volatility / self.market_vol * correlation
def sml_return(self, volatility, correlation, **kwargs):
"""returns the expected return based on the security market line (according to the stocks beta)"""
return self.risk_free * (
1 + self.beta_of_stock(volatility, correlation, **kwargs)
)
def evaluate(self, expected_return, volatility, correlation, **kwargs):
"""return whether a stock is overvalued/undervalued or just right on the SML"""
sml_return = self.sml_return(volatility, correlation, **kwargs)
if sml_return > expected_return:
return Valuation.OVERVALUED
elif sml_return < expected_return:
return Valuation.UNDERVALUED
return Valuation.JUST_RIGHT
def utility(self, expected_return, a, **kwargs):
if "xm" in kwargs:
return utility(expected_return, a, self.volatility(kwargs.get("xm")) ** 2)
elif "volatility" in kwargs:
return utility(expected_return, a, kwargs.get("volatility") ** 2)
class Valuation(Enum):
JUST_RIGHT = 0
OVERVALUED = 1
UNDERVALUED = 2
from functions import *
# all exercises from https://tuwel.tuwien.ac.at/mod/assign/view.php?id=1073839
def test_returns_ex6_abs_inc():
"""THE2 Ex6"""
r = ReturnCalculator()
# ABS Inc.
r.add_day(50.0)
r.add_day(51.50)
r.add_day(49.7)
r.add_day(10.8, new=5, old=1)
r.add_day(11.5)
r.add_day(10.9)
assert list(map(lambda x: to_perc(x), r.returns)) == [
3.0,
-3.4951,
8.6519,
6.4815,
-5.2174,
]
assert to_perc(r.buy_and_hold_return) == 9.0
def test_returns_ex6_hp_inc():
r = ReturnCalculator()
# HP Inc.
r.add_day(22.1)
r.add_day(23.05)
r.add_day(23.4)
r.add_day(23.2, div=0.6)
r.add_day(22.7)
r.add_day(24.03)
assert list(map(lambda x: to_perc(x), r.returns)) == [
4.2986,
1.5184,
1.7094,
-2.1552,
5.859,
]
assert to_perc(r.buy_and_hold_return) == 11.5451
def test_ex7_sr():
assert round(calc_sr(p_minus_1=5.44, old=29, new=25, ip=1.07), 2) == 2.02
def test_ex7_expected_share_price():
p_minus_1 = 5.44
assert (
round(
p_minus_1 - round(calc_sr(p_minus_1=p_minus_1, old=29, new=25, ip=1.07), 2),
2,
)
== 3.42
)
# most exercises from
# https://tuwel.tuwien.ac.at/pluginfile.php/2400775/mod_folder/content/0/PEF_Additional%20exercises%20to%20prepare%20for%20test%202.pdf?forcedownload=1
def test_additional_ex_1a():
years = 2019 - 1800
rate = 2.3 / 100
assert round(compound_interest(years, rate), 2) == 145.47
def test_additional_ex_1b():
years = 2019 - 1800
rate = 4.5 / 100
assert round(compound_interest(years, rate), 2) == 15362.7
def test_additional_ex_3a():
rate = 4.5
assert to_perc(discrete_to_cont_subperiods(rate / 100, 12), 2) == 4.59
def test_additional_ex_3b():
rate = 12
assert to_perc(discrete_to_cont_subperiods(rate / 100, 12), 2) == 12.68
def test_additional_ex_4a1i():
seo_day = Day(96, 78, old=2, new=1, sr=22, ip=30)
portfolio = SeoPortfolio(seo_day, 1000, 25000)
portfolio.exercise_sr(before_trading=True)
assert portfolio.stock_value == 111000
assert portfolio.cash_value == 10000
assert portfolio.total_value == 121000
def test_additional_ex_4a1ii():
seo_day = Day(96, 78, old=2, new=1, sr=22, ip=30)
portfolio = SeoPortfolio(seo_day, 1000, 25000)
portfolio.sell_sr(before_trading=True)
assert portfolio.stock_value == 74000
assert portfolio.cash_value == 47000
assert portfolio.total_value == 121000
def test_additional_ex_4a2i():
seo_day = Day(96, 78, old=2, new=1, sr=22, ip=30)
portfolio = SeoPortfolio(seo_day, 1000, 25000)
portfolio.exercise_sr(before_trading=False)
assert portfolio.stock_value == 117000
assert portfolio.cash_value == 10000
assert portfolio.total_value == 127000
def test_additional_ex_4a2ii():
seo_day = Day(96, 78, old=2, new=1, sr=22, ip=30)
portfolio = SeoPortfolio(seo_day, 1000, 25000)
portfolio.sell_sr(before_trading=False)
assert portfolio.stock_value == 78000
assert portfolio.cash_value == 47000
assert portfolio.total_value == 125000
def test_additional_ex_4b():
r = ReturnCalculator()
r.add_day(95)
r.add_day(96)
r.add_day(78, sr=22)
# alternatively:
# r.add_day(78, old=2, new=1, ip=30)
r.add_day(77.5)
r.add_day(76, div=4)
r.add_day(75)
assert list(map(lambda x: to_perc(x, 3), r.returns)) == [
1.053,
5.405,
-0.641,
3.226,
-1.316,
]
def test_additional_ex_5a():
p = Portfolio(era=10 / 100, erb=18 / 100, stda=15 / 100, stdb=30 / 100, corr=0.1)
assert to_perc(p.xa, 2) == 82.61
assert to_perc(p.xb, 2) == 17.39
def test_additional_ex_5b():
p = Portfolio(era=10 / 100, erb=18 / 100, stda=15 / 100, stdb=30 / 100, corr=0.1)
assert to_perc(p.volatility, 2) == 13.92
def test_additional_ex_5c():
p = Portfolio(era=10 / 100, erb=18 / 100, stda=15 / 100, stdb=30 / 100, corr=0.1)
assert to_perc(p.expected_return, 2) == 11.39
def test_additional_ex_6a():
p = Portfolio(
era=15 / 100, erb=20 / 100, stda=20 / 100, stdb=22 / 100, corr=0.5, xa=0.6
)
assert to_perc(p.expected_return, 3) == 17.0
assert to_perc(p.volatility, 3) == 18.084
def test_additional_ex_6b():
# correlation = 0
p = Portfolio(
era=15 / 100, erb=20 / 100, stda=20 / 100, stdb=22 / 100, corr=0.0, xa=0.6
)
assert to_perc(p.expected_return, 3) == 17.0
assert to_perc(p.volatility, 3) == 14.881
# correlation = -0.5
p = Portfolio(
era=15 / 100, erb=20 / 100, stda=20 / 100, stdb=22 / 100, corr=-0.5, xa=0.6
)
assert to_perc(p.expected_return, 3) == 17.0
assert to_perc(p.volatility, 3) == 10.763
def test_additional_ex_6c():
p = Portfolio(era=15 / 100, erb=20 / 100, stda=20 / 100, stdb=22 / 100, corr=0.5)
assert to_perc(p.xa, 2) == 59.46
assert to_perc(p.xb, 2) == 40.54
def test_additional_ex_7a():
p = Portfolio(era=18 / 100, erb=12 / 100, stda=40 / 100, stdb=25 / 100, corr=0.3)
assert to_perc(p.xa, 0) == 20
assert to_perc(p.xb, 0) == 80
assert to_perc(p.expected_return, 1) == 13.2
assert to_perc(p.volatility, 1) == 23.7
def test_additional_ex_7b():
p = Portfolio(era=18 / 100, erb=12 / 100, stda=40 / 100, stdb=25 / 100, corr=-1)
assert to_perc(p.xa, 2) == 38.46
assert to_perc(p.xb, 2) == 61.54
assert to_perc(p.expected_return, 2) == 14.31
assert to_perc(p.volatility, 0) == 0
def test_additional_ex_8a():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=18 / 100, risk_free=1 / 100
)
assert capm_p.capm_equation == "E[R_P] = 1.0% + 0.5 * sigma_P"
def test_additional_ex_8b():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=18 / 100, risk_free=1 / 100
)
assert to_perc(capm_p.risk(expected_return=4 / 100), 0) == 6
def test_additional_ex_8c():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=18 / 100, risk_free=1 / 100
)
assert to_perc(capm_p.xm(expected_return=4 / 100), 4) == 33.3333
assert to_perc(capm_p.xf(expected_return=4 / 100), 4) == 66.6667
assert (
round(capm_p.market_investment(expected_return=4 / 100, investment=1000), 2)
== 333.33
)
assert (
round(capm_p.risk_free_investment(expected_return=4 / 100, investment=1000), 2)
== 666.67
)
def test_additional_ex_8d():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=18 / 100, risk_free=1 / 100
)
assert capm_p.return_on_investment(300, 700) == 1073
def test_additional_ex_9a():
capm_p = CAPMPortfolio(
market_return=8 / 100, market_vol=15 / 100, risk_free=4 / 100
)
stock_a = {"expected_return": 5 / 100, "volatility": 20 / 100, "correlation": 0.7}
stock_b = {"expected_return": 15 / 100, "volatility": 30 / 100, "correlation": 0.5}
stock_c = {"expected_return": 10 / 100, "volatility": 40 / 100, "correlation": 0.8}
assert capm_p.sml_equation == "E[R_P] = 4.0% + beta_P * 4.0%"
assert round(capm_p.beta_of_stock(**stock_a), 3) == 0.933
assert round(capm_p.beta_of_stock(**stock_b), 3) == 1.000
assert round(capm_p.beta_of_stock(**stock_c), 3) == 2.133
assert capm_p.evaluate(**stock_a) == Valuation.OVERVALUED
assert capm_p.evaluate(**stock_b) == Valuation.UNDERVALUED
assert capm_p.evaluate(**stock_c) == Valuation.OVERVALUED
def test_additional_ex_10a():
capm_p = CAPMPortfolio(
market_return=11 / 100, market_vol=25 / 100, risk_free=3 / 100
)
er = 9 / 100
assert to_perc(capm_p.xm(expected_return=er), 0) == 75
assert to_perc(capm_p.xf(expected_return=er), 0) == 25
def test_additional_ex_10b():
capm_p = CAPMPortfolio(
market_return=11 / 100, market_vol=25 / 100, risk_free=3 / 100
)
assert to_perc(capm_p.volatility(0.75), 2) == 18.75
def test_additional_ex_10c():
capm_p = CAPMPortfolio(
market_return=11 / 100, market_vol=25 / 100, risk_free=3 / 100
)
assert capm_p.beta(0.75) == 0.75
def test_additional_ex_10d():
capm_p = CAPMPortfolio(
market_return=-10 / 100, market_vol=25 / 100, risk_free=3 / 100
)
assert to_perc(capm_p.expected_return(0.25, 0.75), 2) == -6.75
# Some more exercises from VOWI
# https://vowi.fsinf.at/wiki/TU_Wien:Project_and_Enterprise_Financing_VU_(Aussenegg)/Exam_2_-_26.03.2019
def test_seo_a():
assert round(calc_sr(30, 10, 1, 20), 5) == 0.90909
def test_seo_bi():
seo_day = Day(
30, 0, old=10, new=1, ip=20
) # second parameter doesn't matter, because its before trading
seo_portfolio = SeoPortfolio(seo_day, 100, 2000)
assert seo_portfolio.stock_value == 3000
assert seo_portfolio.cash_value == 2000
assert seo_portfolio.total_value == 5000
def test_seo_bii():
seo_day = Day(
30, 0, old=10, new=1, ip=20
) # second parameter doesn't matter, because its before trading
seo_portfolio = SeoPortfolio(seo_day, 100, 2000)
seo_portfolio.exercise_sr(before_trading=True)
assert seo_portfolio.stock_value == 3200
assert seo_portfolio.cash_value == 1800
assert seo_portfolio.total_value == 5000
def test_returns_table1():
r = ReturnCalculator()
r.add_day(199.01)
r.add_day(204.09, div=0.5)
r.add_day(214.1)
assert to_perc(r.buy_and_hold_return, 3) == 7.846
def test_returns_table2():
r = ReturnCalculator()
r.add_day(189.45)
r.add_day(194.1)
r.add_day(47.75, new=4, old=1)
r.add_day(45.13)
assert to_perc(r.buy_and_hold_return, 3) == -4.714
def test_portfolio_a():
p = Portfolio(
era=15 / 100, erb=10 / 100, stda=20 / 100, stdb=15 / 100, corr=0.1, xa=0.9
)
assert to_perc(p.expected_return, 1) == 14.5
assert to_perc(p.volatility, 2) == 18.21
def test_portfolio_b():
p = Portfolio(era=15 / 100, erb=10 / 100, stda=20 / 100, stdb=15 / 100, corr=0.1)
assert to_perc(p.xa, 4) == 34.5133
assert to_perc(p.xb, 4) == 65.4867
assert to_perc(p.expected_return, 4) == 11.7257
assert to_perc(p.volatility, 4) == 12.5578
def test_capm_a():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=30 / 100, risk_free=1 / 100
)
assert to_perc(capm_p.xm(7 / 100), 2) == 66.67
assert to_perc(capm_p.xf(7 / 100), 2) == 33.33
def test_capm_b():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=30 / 100, risk_free=1 / 100
)
assert to_perc(capm_p.volatility(2 / 3), 2) == 20
def test_capm_c():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=30 / 100, risk_free=1 / 100
)
assert capm_p.beta(2 / 3) == 2 / 3
def test_capm_d():
capm_p = CAPMPortfolio(
market_return=10 / 100, market_vol=30 / 100, risk_free=1 / 100
)
assert round(capm_p.utility(expected_return=7 / 100, a=2, xm=2 / 3), 2) == 0.03
# exercises from the slides
def test_returns_slides_11():
r = ReturnCalculator()
r.add_day(98.52)
r.add_day(98.73)
r.add_day(101.35)
r.add_day(100.00)
r.add_day(100.55)
assert list(map(lambda x: to_perc(x, 5), r.returns)), [
0.21315,
2.65370,
-1.33202,
0.55000,
]
def test_returns_slides_14():
r = ReturnCalculator()
r.add_day(97.41)
r.add_day(98.94)
r.add_day(94.87, div=3.0)
r.add_day(94.14)
r.add_day(93.96)
assert list(map(lambda x: to_perc(x, 3), r.returns)), [
1.571,
-1.081,
-0.769,
-0.191,
]
def test_cont_returns_slides_30():
assert to_perc(discrete_to_cont_subperiods(10 / 100, 2), 2) == 10.25
def test_cont_to_discrete_32():
assert to_perc(cont_to_disc(10 / 100), 4) == 10.5171
def test_discrete_to_cont_32():
assert to_perc(disc_to_cont(10.5171 / 100), 4) == 10
def test_returns_cont_34():
r = ReturnCalculator()
r.add_day(98.52)
r.add_day(98.73)
r.add_day(101.35)
r.add_day(100.00)
r.add_day(100.55)
cont = list(map(lambda x: to_perc(disc_to_cont(x), 5), r.returns))
assert cont == [0.21293, 2.61910, -1.34097, 0.54849]
def test_geometric_return_42():
assert to_perc(geometric_return(1.1162 * 1.3749 * 1.4361 - 1, 3), 3) == 30.137
def test_var_delta_normal_82():
assert round(var_delta_normal(279500, 0.048 / 100, 2.921 / 100, 0.01), 2) == 18858.6
assert (
round(var_delta_normal(279500, 0.048 / 100, 2.921 / 100, 0.05), 2) == 13294.75
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment