-
Notifications
You must be signed in to change notification settings - Fork 10
Equity processing within OSEM
One of the basic functions of OSEM is creating and manipulating cash flows from different classes of financial instruments. This page shows the processing of company shares held within a portfolio.
An ownership interest or a share represents ownership of a small piece of a legal entity. The most common of them being listed companies. In OSEM these shares provide two kinds of benefits. A periotic dividend and its intrinsic value (The share can be sold to another entity).
The enterprise and with it the equity share is assumed to grow at a constant rate every year. At periodic periods, the dividend is paid out. The size of the dividend determined by the value of the share. A % of the market value referred here as dividend yield.
This page will present the example on 3 equity positions:
Asset_ID | Asset_Type | NACE | Issue_Date | Dividend_Yield | Frequency | Market_Price | Terminal | Default_Probability | Growth_Rate |
---|---|---|---|---|---|---|---|---|---|
1 | Equity_Share | A1.4.5 | 3/12/2021 | 0.03 | 1 | 94 | 1 | 0.03 | 0.01 |
2 | Equity_Share | B5.2.0 | 3/12/2021 | 0.05 | 1 | 92 | 1 | 0.03 | 0.02 |
3 | Equity_Share | B8.9.3 | 3/12/2019 | 0.04 | 1 | 96 | 1 | 0.03 | 0.04 |
This page has 4 sections.
- The first section shows how to import the necessary information about equity positions, the overall economic environment as well as other parameters.
- The next section imports the term structure which is needed later on.
- The third section shows how to generate the cash flows for a set of equities.
- The last section shows how a hypothetical equity can be calibrated if the user wishes to preform a risk-neutral run.
The packages and imports needed in this script are the following:
import datetime
import os
import pandas as pd
from ConfigurationClass import Configuration
from CurvesClass import Curves
from EquityClasses import *
from ImportData import get_EquityShare, get_settings, import_SWEiopa, \
get_configuration
base_folder = os.getcwd() # Get current working directory
Most of the run settings are saved in the configuration file:
conf: Configuration
conf = get_configuration(os.path.join(base_folder, "ALM.ini"), os)
These lines of code just extract the absolute location of different files:
parameters_file = conf.input_parameters
equity_portfolio_file = conf.input_equity_portfolio
The settings object holds data about file locations, information about the run settings and model parameters such as modelling date.
settings = get_settings(parameters_file)
The EquityShare object contains information about each equity position. This includes:
- Asset_ID
- NACE
- Issuer
- Issue_Date
- Dividend_Yield
- Frequency
- Market_Price
- Growth_Rate
equity_input_generator = get_EquityShare(equity_portfolio_file)
equity_input = {equity_share.asset_id: equity_share for equity_share in equity_input_generator}
EquitySharePortfolio object contains all EquityShare objects in a dictionary.
equity_portfolio = EquitySharePortfolio(equity_input)
import_SWEiopa() reads the necessary data about the current yield curve. One of these parameters (the ufr or ultimate forward rate) is necessary in the equity example as ufr is used in the Gordon growth formula to calculate the terminal value of the equity position. Inside OSEM, the parameters related to the yield curve are saved in the Curves object.
[maturities_country, curve_country, extra_param, Qb] = import_SWEiopa(settings.EIOPA_param_file,
settings.EIOPA_curves_file, settings.country)
# Curves object with information about term structure
curves = Curves(extra_param["UFR"] / 100, settings.precision, settings.tau, settings.modelling_date,
settings.country)
ufr = extra_param["UFR"]/100 # ultimate forward rate
precision = float(settings.precision) # Numeric precision of the optimisation
# Targeted distance between the extrapolated curve and the ufr at the convergence point
tau = float(settings.tau) # 1 basis point
curves.SetObservedTermStructure(maturity_vec=curve_country.index.tolist(), yield_vec=curve_country.values)
curves.CalcFwdRates()
curves.ProjectForwardRate(settings.n_proj_years)
The first computation step inside the OSEM equity preparation process is the identification of all the unique dates and dividend size amounts. The representation of assets in terms of individual cash flows on the time-line is one of the core principles of OSEM. This is done by two functions. One for dividend dates and another for terminal rates.
Both functions generate a list of dictionaries containing the date of a cash flow and the amount. Same is also true for the terminal amount calculation.
The dividend size is calculated using the dividend yield provided as input for each equity position. However the market value changes as time moves forward. To account for this, the growth rate and the time fraction between the modelling date and the date of the cash flow is used to calculate a future market value.
ToDo Formulas
The same logic is applied to the calculation of terminal rates.
dividend_dates = equity_portfolio.create_dividend_dates(settings.modelling_date, settings.end_date)
terminal_dates = equity_portfolio.create_terminal_dates(modelling_date=settings.modelling_date,
terminal_date=settings.end_date,
terminal_rate=curves.ufr)
All cash flows can be represented in a matrix with all possible cash flow dates as columns and all equities as rows. The non-zero entries then represent the value of the cash flow at that date. The first step is to calculate the unique dates for the entire portfolio of equities. This is done by the unique_dates_profiles() function.
The same logic can be applied to terminal dates.
Both can then conveniently be represented as DataFrames.
Note that a vector of growth rates is also provided as output. This makes it simpler to increase the market value of the portfolio as OSEM moves from one modelling period to the next one.
unique_list = equity_portfolio.unique_dates_profile(dividend_dates)
unique_terminal_list = equity_portfolio.unique_dates_profile(terminal_dates)
[market_price_df, growth_rate_df] = equity_portfolio.init_equity_portfolio_to_dataframe(settings.modelling_date)
The create_cashflow_dataframe() function converts the list of dictionaries of cashflows and dates, into a single DataFrame:
cash_flows = create_cashflow_dataframe(dividend_dates, unique_list)
# Dataframe with terminal cash flows
terminal_cash_flows = create_cashflow_dataframe(terminal_dates, unique_terminal_list)
Where the function is defined as:
def create_cashflow_dataframe(cash_flow_dates, unique_dates) -> pd.DataFrame:
cash_flows = pd.DataFrame(data=np.zeros((len(cash_flow_dates), len(unique_dates))),
columns=unique_dates) # Dataframe of cashflows (columns are dates, rows, assets)
counter = 0
for asset in cash_flow_dates:
keys = asset.keys()
for key in keys:
cash_flows[key].iloc[counter] = asset[key]
counter += 1
return cash_flows
The cash flows for this section can then be shown as:
display(cash_flows)
2023-12-03 | 2024-12-03 | 2025-12-03 | 2026-12-03 | 2027-12-03 | 2028-12-03 | 2029-12-03 | 2030-12-03 | 2031-12-03 | 2032-12-03 | ... | 2063-12-03 | 2064-12-03 | 2065-12-03 | 2066-12-03 | 2067-12-03 | 2068-12-03 | 2069-12-03 | 2070-12-03 | 2071-12-03 | 2072-12-03 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
2.836786 | 2.865193 | 2.893805 | 2.922704 | 2.951890 | 2.981450 | 3.011223 | 3.041294 | 3.071665 | 3.102424 | ... | 4.222438 | 4.264720 | 4.307309 | 4.350323 | 4.393766 | 4.437764 | 4.482081 | 4.526840 | 4.572046 | 4.617830 |
4.654653 | 4.747875 | 4.842701 | 4.939422 | 5.038074 | 5.138974 | 5.241612 | 5.346299 | 5.453077 | 5.562290 | ... | 10.272092 | 10.477818 | 10.687085 | 10.900531 | 11.118241 | 11.340913 | 11.567418 | 11.798446 | 12.034089 | 12.275104 |
3.930888 | 4.088343 | 4.251648 | 4.421477 | 4.598089 | 4.782269 | 4.973293 | 5.171948 | 5.378537 | 5.593979 | ... | 18.852033 | 19.607166 | 20.390359 | 21.204835 | 22.051846 | 22.935150 | 23.851276 | 24.803996 | 25.794772 | 26.828002 |
And the cash flows related to the terminal value at the end of the modelling period:
display(terminal_cash_flows)
2073-04-29 |
---|
154.544895 |
247.465156 |
681.363655 |
The cashflows can be used to price the current market value of the bond, implied by the assumed economic parameters.
Note that this pricing is done using the risk free rate as the discounting factor. In practice, the price of risk for an equity share is positive.
This example will show pricing at the modelling date.
proj_period = 0
for i_equity in range(0,3):
temp_dividend = cash_flows.iloc[i_equity].to_dict()
temp_terminal = terminal_cash_flows.iloc[i_equity].to_dict()
price = equity_portfolio.equity_share[i_equity + 1].price_share(temp_dividend, temp_terminal, settings.modelling_date, proj_period, curves)
print(price)
Price |
---|
110.30416346 |
210.09806865 |
344.79612043 |
In a real world run, the growth rate of each equity is brough in as a modelling assumption and must be calculated externaly. If the user is interested in a risk-neutral run, the performance of each asset must equal to that of the risk free rate. In OSEM this is done using the growth rate as the calibrating parameter. To do this, a bisection algorithm is used to calibrate the growth rate such that the discounted cash flows of each equity equal to the current market price.
For this demonstration, a single equity position is created, the growth rate is calibrated and then the present value of the calibrated equity is compared to the assumed current market value.
Note that the projection period is selected as 0 meaning the initial modelling date. But the calibration works also for other projection periods. The only change needed would be to change the market price.
proj_period = 0
test_share_1 = EquityShare(asset_id=1, nace='A1.4.5', issuer=None, issue_date=datetime.date(2021, 12, 3), dividend_yield=0.03, frequency=1, market_price=94.0, growth_rate=0.01)
The callibration:
opt_growth = test_share_1.bisection_growth(-1, 1, settings.modelling_date, settings.end_date, proj_period, curves, 0.00000001,100000)
Using the calibrated growth rate, the cash flows can be generated:
test_dividends = test_share_1.create_single_cash_flows(settings.modelling_date, settings.end_date, opt_growth)
test_terminal = test_share_1.create_single_terminal(settings.modelling_date, settings.end_date, curves.ufr, opt_growth)
If the calibration was performed correctly, the present value calculated by discounting future cash flows with the assumed risk free rate should be equal to the initial observed market price.
This can be evaluated by comparing the observed market price:
print(test_share_1.market_price)
Original market price |
---|
94.0 |
and the calculated price:
print(test_share_1.price_share(test_dividends, test_terminal, settings.modelling_date, proj_period, curves))
Calculated price |
---|
93.99999792 |