|
|
@ -3,18 +3,37 @@ import math |
|
|
|
import random |
|
|
|
|
|
|
|
import pytest |
|
|
|
from dateutil.relativedelta import relativedelta |
|
|
|
from fincal.core import AllFrequencies, Frequency |
|
|
|
from fincal.exceptions import DateNotFoundError |
|
|
|
from fincal.fincal import TimeSeries, create_date_series |
|
|
|
from fincal.utils import FincalOptions |
|
|
|
|
|
|
|
|
|
|
|
def create_prices(s0: float, mu: float, sigma: float, num_prices: int) -> list: |
|
|
|
"""Generates a price following a geometric brownian motion process based on the input of the arguments: |
|
|
|
- s0: Asset inital price. |
|
|
|
- mu: Interest rate expressed annual terms. |
|
|
|
- sigma: Volatility expressed annual terms. |
|
|
|
- seed: seed for the random number generator |
|
|
|
- num_prices: number of prices to generate |
|
|
|
"""Generates a price following a geometric brownian motion process based on the input of the arguments. |
|
|
|
|
|
|
|
Since this function is used only to generate data for tests, the seed is fixed as 1234. |
|
|
|
Many of the tests rely on exact values generated using this seed. |
|
|
|
If the seed is changed, those tests will fail. |
|
|
|
|
|
|
|
Parameters: |
|
|
|
------------ |
|
|
|
s0: float |
|
|
|
Asset inital price. |
|
|
|
|
|
|
|
mu: float |
|
|
|
Interest rate expressed annual terms. |
|
|
|
|
|
|
|
sigma: float |
|
|
|
Volatility expressed annual terms. |
|
|
|
|
|
|
|
num_prices: int |
|
|
|
number of prices to generate |
|
|
|
|
|
|
|
Returns: |
|
|
|
-------- |
|
|
|
Returns a list of values generated using GBM algorithm |
|
|
|
""" |
|
|
|
|
|
|
|
random.seed(1234) # WARNING! Changing the seed will cause most tests to fail |
|
|
@ -28,68 +47,124 @@ def create_prices(s0: float, mu: float, sigma: float, num_prices: int) -> list: |
|
|
|
return all_values |
|
|
|
|
|
|
|
|
|
|
|
def create_data(): |
|
|
|
"""Creates TimeSeries data""" |
|
|
|
def create_test_timeseries( |
|
|
|
frequency: Frequency, num: int = 1000, skip_weekends: bool = False, mu: float = 0.1, sigma: float = 0.05 |
|
|
|
) -> TimeSeries: |
|
|
|
"""Creates TimeSeries data |
|
|
|
|
|
|
|
Parameters: |
|
|
|
----------- |
|
|
|
frequency: Frequency |
|
|
|
The frequency of the time series data to be generated. |
|
|
|
|
|
|
|
num: int |
|
|
|
Number of date: value pairs to be generated. |
|
|
|
|
|
|
|
dates = create_date_series("2017-01-01", "2020-10-31", "D", skip_weekends=True) |
|
|
|
values = create_prices(1000, 0.1, 0.05, 1000) |
|
|
|
ts = TimeSeries(dict(zip(dates, values)), frequency="D") |
|
|
|
skip_weekends: bool |
|
|
|
Whether weekends (saturday, sunday) should be skipped. |
|
|
|
Gets used only if the frequency is daily. |
|
|
|
|
|
|
|
mu: float |
|
|
|
Mean return for the values. |
|
|
|
|
|
|
|
sigma: float |
|
|
|
standard deviation of the values. |
|
|
|
|
|
|
|
Returns: |
|
|
|
-------- |
|
|
|
Returns a TimeSeries object |
|
|
|
""" |
|
|
|
|
|
|
|
start_date = datetime.datetime(2017, 1, 1) |
|
|
|
timedelta_dict = { |
|
|
|
frequency.freq_type: int(frequency.value * num * (7 / 5 if frequency == "D" and skip_weekends else 1)) |
|
|
|
} |
|
|
|
end_date = start_date + relativedelta(**timedelta_dict) |
|
|
|
dates = create_date_series(start_date, end_date, frequency.symbol, skip_weekends=skip_weekends) |
|
|
|
values = create_prices(1000, mu, sigma, num) |
|
|
|
ts = TimeSeries(dict(zip(dates, values)), frequency=frequency.symbol) |
|
|
|
return ts |
|
|
|
|
|
|
|
|
|
|
|
class TestReturns: |
|
|
|
def test_returns_calc(self): |
|
|
|
ts = create_data() |
|
|
|
ts = create_test_timeseries() |
|
|
|
returns = ts.calculate_returns( |
|
|
|
"2020-01-01", annual_compounded_returns=False, interval_type="years", interval_value=1 |
|
|
|
"2020-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1 |
|
|
|
) |
|
|
|
assert round(returns[1], 6) == 0.112913 |
|
|
|
|
|
|
|
returns = ts.calculate_returns( |
|
|
|
"2020-04-01", annual_compounded_returns=False, interval_type="months", interval_value=3 |
|
|
|
"2020-04-01", annual_compounded_returns=False, return_period_unit="months", return_period_value=3 |
|
|
|
) |
|
|
|
assert round(returns[1], 6) == 0.015908 |
|
|
|
|
|
|
|
returns = ts.calculate_returns( |
|
|
|
"2020-04-01", annual_compounded_returns=True, interval_type="months", interval_value=3 |
|
|
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="months", return_period_value=3 |
|
|
|
) |
|
|
|
assert round(returns[1], 6) == 0.065167 |
|
|
|
|
|
|
|
returns = ts.calculate_returns( |
|
|
|
"2020-04-01", annual_compounded_returns=False, interval_type="days", interval_value=90 |
|
|
|
"2020-04-01", annual_compounded_returns=False, return_period_unit="days", return_period_value=90 |
|
|
|
) |
|
|
|
assert round(returns[1], 6) == 0.017673 |
|
|
|
|
|
|
|
returns = ts.calculate_returns( |
|
|
|
"2020-04-01", annual_compounded_returns=True, interval_type="days", interval_value=90 |
|
|
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 |
|
|
|
) |
|
|
|
assert round(returns[1], 6) == 0.073632 |
|
|
|
|
|
|
|
with pytest.raises(DateNotFoundError): |
|
|
|
ts.calculate_returns("2020-04-04", interval_type="days", interval_value=90, as_on_match="exact") |
|
|
|
ts.calculate_returns("2020-04-04", return_period_unit="days", return_period_value=90, as_on_match="exact") |
|
|
|
with pytest.raises(DateNotFoundError): |
|
|
|
ts.calculate_returns("2020-04-04", interval_type="months", interval_value=3, prior_match="exact") |
|
|
|
ts.calculate_returns("2020-04-04", return_period_unit="months", return_period_value=3, prior_match="exact") |
|
|
|
|
|
|
|
def test_date_formats(self): |
|
|
|
ts = create_data() |
|
|
|
ts = create_test_timeseries() |
|
|
|
FincalOptions.date_format = "%d-%m-%Y" |
|
|
|
with pytest.raises(ValueError): |
|
|
|
ts.calculate_returns("2020-04-10", annual_compounded_returns=True, interval_type="days", interval_value=90) |
|
|
|
ts.calculate_returns( |
|
|
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 |
|
|
|
) |
|
|
|
|
|
|
|
returns1 = ts.calculate_returns("2020-04-01", interval_type="days", interval_value=90, date_format="%Y-%m-%d") |
|
|
|
returns2 = ts.calculate_returns("01-04-2020", interval_type="days", interval_value=90) |
|
|
|
returns1 = ts.calculate_returns( |
|
|
|
"2020-04-01", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" |
|
|
|
) |
|
|
|
returns2 = ts.calculate_returns("01-04-2020", return_period_unit="days", return_period_value=90) |
|
|
|
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632 |
|
|
|
|
|
|
|
FincalOptions.date_format = "%m-%d-%Y" |
|
|
|
with pytest.raises(ValueError): |
|
|
|
ts.calculate_returns("2020-04-01", annual_compounded_returns=True, interval_type="days", interval_value=90) |
|
|
|
ts.calculate_returns( |
|
|
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90 |
|
|
|
) |
|
|
|
|
|
|
|
returns1 = ts.calculate_returns("2020-04-01", interval_type="days", interval_value=90, date_format="%Y-%m-%d") |
|
|
|
returns2 = ts.calculate_returns("04-01-2020", interval_type="days", interval_value=90) |
|
|
|
returns1 = ts.calculate_returns( |
|
|
|
"2020-04-01", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d" |
|
|
|
) |
|
|
|
returns2 = ts.calculate_returns("04-01-2020", return_period_unit="days", return_period_value=90) |
|
|
|
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632 |
|
|
|
|
|
|
|
def test_limits(self): |
|
|
|
ts = create_data() |
|
|
|
FincalOptions.date_format = "%Y-%m-%d" |
|
|
|
ts = create_test_timeseries() |
|
|
|
with pytest.raises(DateNotFoundError): |
|
|
|
ts.calculate_returns("2020-11-25", interval_type="days", interval_value=90, closest_max_days=10) |
|
|
|
ts.calculate_returns("2020-11-25", return_period_unit="days", return_period_value=90, closest_max_days=10) |
|
|
|
|
|
|
|
|
|
|
|
class TestVolatility: |
|
|
|
def test_daily_ts(self): |
|
|
|
ts = create_test_timeseries(AllFrequencies.D) |
|
|
|
assert len(ts) == 1000 |
|
|
|
sd = ts.volatility(annualize_volatility=False) |
|
|
|
assert round(sd, 6) == 0.002622 |
|
|
|
sd = ts.volatility() |
|
|
|
assert round(sd, 6) == 0.050098 |
|
|
|
sd = ts.volatility(annual_compounded_returns=True) |
|
|
|
assert round(sd, 4) == 37.9329 |
|
|
|
sd = ts.volatility(return_period_unit="months", annual_compounded_returns=True) |
|
|
|
assert round(sd, 4) == 0.6778 |
|
|
|
sd = ts.volatility(return_period_unit="years") |
|
|
|
assert round(sd, 6) == 0.023164 |
|
|
|
sd = ts.volatility(from_date="2017-10-01", to_date="2019-08-31", annualize_volatility=True) |
|
|
|
assert round(sd, 6) == 0.050559 |
|
|
|