2022-02-20 10:38:26 +00:00
|
|
|
import datetime
|
|
|
|
import os
|
|
|
|
import random
|
|
|
|
from typing import Literal, Sequence
|
2022-02-17 10:50:48 +00:00
|
|
|
|
2022-02-20 10:38:26 +00:00
|
|
|
import pytest
|
2022-02-26 07:16:42 +00:00
|
|
|
from fincal.core import Frequency, Series
|
|
|
|
from fincal.exceptions import DateNotFoundError
|
2022-02-20 10:38:26 +00:00
|
|
|
from fincal.fincal import TimeSeries, create_date_series
|
2022-02-26 07:16:42 +00:00
|
|
|
from fincal.utils import FincalOptions
|
2022-02-20 10:38:26 +00:00
|
|
|
|
|
|
|
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
sample_data_path = os.path.join(THIS_DIR, "data")
|
|
|
|
|
|
|
|
|
2022-03-11 04:10:11 +00:00
|
|
|
def create_random_test_data(
|
2022-02-20 10:38:26 +00:00
|
|
|
frequency: str,
|
|
|
|
eomonth: bool,
|
|
|
|
n: int,
|
|
|
|
gaps: float,
|
|
|
|
month_position: Literal["start", "middle", "end"],
|
|
|
|
date_as_str: bool,
|
2022-02-22 05:59:47 +00:00
|
|
|
as_outer_type: Literal["dict", "list"] = "list",
|
|
|
|
as_inner_type: Literal["dict", "list", "tuple"] = "tuple",
|
2022-02-20 10:38:26 +00:00
|
|
|
) -> Sequence[tuple]:
|
|
|
|
start_dates = {
|
|
|
|
"start": datetime.datetime(2016, 1, 1),
|
|
|
|
"middle": datetime.datetime(2016, 1, 15),
|
|
|
|
"end": datetime.datetime(2016, 1, 31),
|
|
|
|
}
|
|
|
|
end_date = datetime.datetime(2021, 12, 31)
|
|
|
|
dates = create_date_series(start_dates[month_position], end_date, frequency=frequency, eomonth=eomonth)
|
|
|
|
dates = dates[:n]
|
|
|
|
if gaps:
|
|
|
|
num_gaps = int(len(dates) * gaps)
|
|
|
|
to_remove = random.sample(dates, num_gaps)
|
|
|
|
for i in to_remove:
|
|
|
|
dates.remove(i)
|
|
|
|
if date_as_str:
|
2022-02-22 05:59:47 +00:00
|
|
|
dates = [i.strftime("%Y-%m-%d") for i in dates]
|
2022-02-20 10:38:26 +00:00
|
|
|
|
2022-02-22 05:59:47 +00:00
|
|
|
values = [random.randint(8000, 90000) / 100 for _ in dates]
|
2022-02-20 10:38:26 +00:00
|
|
|
|
|
|
|
data = list(zip(dates, values))
|
2022-02-22 05:59:47 +00:00
|
|
|
if as_outer_type == "list":
|
|
|
|
if as_inner_type == "list":
|
2022-02-20 10:38:26 +00:00
|
|
|
data = [list(i) for i in data]
|
2022-02-22 05:59:47 +00:00
|
|
|
elif as_inner_type == "dict[1]":
|
2022-02-20 10:38:26 +00:00
|
|
|
data = [dict((i,)) for i in data]
|
2022-02-22 05:59:47 +00:00
|
|
|
elif as_inner_type == "dict[2]":
|
2022-02-20 10:38:26 +00:00
|
|
|
data = [dict(date=i, value=j) for i, j in data]
|
2022-02-22 05:59:47 +00:00
|
|
|
elif as_outer_type == "dict":
|
2022-02-20 10:38:26 +00:00
|
|
|
data = dict(data)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
2022-03-11 04:10:11 +00:00
|
|
|
def create_organised_test_data() -> dict:
|
|
|
|
"""Creates organised test data so that output is exactly same in each run"""
|
|
|
|
|
|
|
|
all_dates, all_values = [], []
|
|
|
|
prev_date, prev_number = datetime.datetime(2018, 1, 1), 1000
|
|
|
|
|
|
|
|
for i in range(1, 1000):
|
|
|
|
if i % 5 == 0:
|
|
|
|
prev_date += datetime.timedelta(days=3)
|
|
|
|
else:
|
|
|
|
prev_date += datetime.timedelta(days=1)
|
|
|
|
all_dates.append(prev_date)
|
|
|
|
|
|
|
|
for i in range(1, 1000):
|
|
|
|
rem = i % 7
|
|
|
|
if rem % 2:
|
|
|
|
prev_number -= rem
|
|
|
|
else:
|
|
|
|
prev_number += rem
|
|
|
|
all_values.append(prev_number)
|
|
|
|
|
|
|
|
return dict(zip(all_dates, all_values))
|
|
|
|
|
|
|
|
|
2022-02-20 10:38:26 +00:00
|
|
|
class TestDateSeries:
|
|
|
|
def test_daily(self):
|
|
|
|
start_date = datetime.datetime(2020, 1, 1)
|
|
|
|
end_date = datetime.datetime(2020, 12, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="D")
|
|
|
|
assert len(d) == 366
|
|
|
|
|
|
|
|
start_date = datetime.datetime(2017, 1, 1)
|
|
|
|
end_date = datetime.datetime(2017, 12, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="D")
|
|
|
|
assert len(d) == 365
|
|
|
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
create_date_series(start_date, end_date, frequency="D", eomonth=True)
|
|
|
|
|
|
|
|
def test_monthly(self):
|
|
|
|
start_date = datetime.datetime(2020, 1, 1)
|
|
|
|
end_date = datetime.datetime(2020, 12, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="M")
|
|
|
|
assert len(d) == 12
|
|
|
|
|
|
|
|
d = create_date_series(start_date, end_date, frequency="M", eomonth=True)
|
|
|
|
assert datetime.datetime(2020, 2, 29) in d
|
|
|
|
|
|
|
|
start_date = datetime.datetime(2020, 1, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="M")
|
|
|
|
assert datetime.datetime(2020, 2, 29) in d
|
|
|
|
assert datetime.datetime(2020, 8, 31) in d
|
|
|
|
assert datetime.datetime(2020, 10, 30) not in d
|
|
|
|
|
|
|
|
start_date = datetime.datetime(2020, 2, 29)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="M")
|
|
|
|
assert len(d) == 11
|
|
|
|
assert datetime.datetime(2020, 2, 29) in d
|
|
|
|
assert datetime.datetime(2020, 8, 31) not in d
|
|
|
|
assert datetime.datetime(2020, 10, 29) in d
|
|
|
|
|
|
|
|
def test_quarterly(self):
|
|
|
|
start_date = datetime.datetime(2018, 1, 1)
|
|
|
|
end_date = datetime.datetime(2020, 12, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="Q")
|
|
|
|
assert len(d) == 12
|
|
|
|
|
|
|
|
d = create_date_series(start_date, end_date, frequency="Q", eomonth=True)
|
|
|
|
assert datetime.datetime(2020, 4, 30) in d
|
|
|
|
|
|
|
|
start_date = datetime.datetime(2020, 1, 31)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="Q")
|
|
|
|
assert len(d) == 4
|
|
|
|
assert datetime.datetime(2020, 2, 29) not in d
|
|
|
|
assert max(d) == datetime.datetime(2020, 10, 31)
|
|
|
|
|
|
|
|
start_date = datetime.datetime(2020, 2, 29)
|
|
|
|
d = create_date_series(start_date, end_date, frequency="Q")
|
|
|
|
assert datetime.datetime(2020, 2, 29) in d
|
|
|
|
assert datetime.datetime(2020, 8, 31) not in d
|
|
|
|
assert datetime.datetime(2020, 11, 29) in d
|
|
|
|
|
|
|
|
d = create_date_series(start_date, end_date, frequency="Q", eomonth=True)
|
|
|
|
assert datetime.datetime(2020, 11, 30) in d
|
|
|
|
|
|
|
|
|
2022-02-24 03:48:56 +00:00
|
|
|
class TestFincalBasic:
|
2022-02-20 10:38:26 +00:00
|
|
|
def test_creation(self):
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-20 10:38:26 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
|
|
|
assert len(time_series) == 50
|
|
|
|
assert isinstance(time_series.frequency, Frequency)
|
|
|
|
assert time_series.frequency.days == 1
|
|
|
|
|
|
|
|
ffill_data = time_series.ffill()
|
|
|
|
assert len(ffill_data) == 50
|
|
|
|
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-20 10:38:26 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
|
|
|
assert len(time_series) == 450
|
|
|
|
|
2022-02-24 03:48:56 +00:00
|
|
|
def test_fill(self):
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-20 10:38:26 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
|
|
|
ffill_data = time_series.ffill()
|
2022-02-22 02:55:57 +00:00
|
|
|
assert len(ffill_data) >= 498
|
2022-02-20 10:38:26 +00:00
|
|
|
|
|
|
|
ffill_data = time_series.ffill(inplace=True)
|
|
|
|
assert ffill_data is None
|
2022-02-22 02:55:57 +00:00
|
|
|
assert len(time_series) >= 498
|
2022-02-20 10:38:26 +00:00
|
|
|
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-24 03:48:56 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
|
|
|
bfill_data = time_series.bfill()
|
|
|
|
assert len(bfill_data) >= 498
|
|
|
|
|
|
|
|
bfill_data = time_series.bfill(inplace=True)
|
|
|
|
assert bfill_data is None
|
|
|
|
assert len(time_series) >= 498
|
|
|
|
|
|
|
|
data = [("2021-01-01", 220), ("2021-01-02", 230), ("2021-03-04", 240)]
|
|
|
|
ts = TimeSeries(data, frequency="D")
|
|
|
|
ff = ts.ffill()
|
|
|
|
assert ff["2021-01-03"][1] == 230
|
|
|
|
|
|
|
|
bf = ts.bfill()
|
|
|
|
assert bf["2021-01-03"][1] == 240
|
|
|
|
|
2022-02-22 02:55:57 +00:00
|
|
|
def test_iloc_slicing(self):
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-22 02:55:57 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
|
|
|
assert time_series.iloc[0] is not None
|
|
|
|
assert time_series.iloc[:3] is not None
|
|
|
|
assert time_series.iloc[5:7] is not None
|
|
|
|
assert isinstance(time_series.iloc[0], tuple)
|
2022-03-13 08:58:42 +00:00
|
|
|
assert isinstance(time_series.iloc[10:20], TimeSeries)
|
2022-02-22 02:55:57 +00:00
|
|
|
assert len(time_series.iloc[10:20]) == 10
|
|
|
|
|
|
|
|
def test_key_slicing(self):
|
2022-03-11 04:10:11 +00:00
|
|
|
data = create_random_test_data(
|
|
|
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
|
|
|
)
|
2022-02-20 10:38:26 +00:00
|
|
|
time_series = TimeSeries(data, frequency="D")
|
2022-02-22 02:55:57 +00:00
|
|
|
available_date = time_series.iloc[5][0]
|
|
|
|
assert time_series[available_date] is not None
|
2022-02-22 05:59:47 +00:00
|
|
|
assert isinstance(time_series["dates"], Series)
|
|
|
|
assert isinstance(time_series["values"], Series)
|
2022-02-22 02:55:57 +00:00
|
|
|
assert len(time_series.dates) == 50
|
|
|
|
assert len(time_series.values) == 50
|
2022-02-24 17:54:20 +00:00
|
|
|
|
2022-02-25 02:52:32 +00:00
|
|
|
|
|
|
|
class TestReturns:
|
|
|
|
data = [
|
2022-03-05 17:53:31 +00:00
|
|
|
("2020-01-01", 10),
|
|
|
|
("2020-02-01", 12),
|
|
|
|
("2020-03-01", 14),
|
|
|
|
("2020-04-01", 16),
|
|
|
|
("2020-05-01", 18),
|
|
|
|
("2020-06-01", 20),
|
|
|
|
("2020-07-01", 22),
|
|
|
|
("2020-08-01", 24),
|
|
|
|
("2020-09-01", 26),
|
|
|
|
("2020-10-01", 28),
|
|
|
|
("2020-11-01", 30),
|
|
|
|
("2020-12-01", 32),
|
|
|
|
("2021-01-01", 34),
|
|
|
|
]
|
2022-02-25 02:52:32 +00:00
|
|
|
|
|
|
|
def test_returns_calc(self):
|
2022-03-05 17:53:31 +00:00
|
|
|
ts = TimeSeries(self.data, frequency="M")
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2021-01-01", annual_compounded_returns=False, return_period_unit="years", return_period_value=1
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert returns[1] == 2.4
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2020-04-01", annual_compounded_returns=False, return_period_unit="months", return_period_value=3
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns[1], 4) == 0.6
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="months", return_period_value=3
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns[1], 4) == 5.5536
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2020-04-01", annual_compounded_returns=False, return_period_unit="days", return_period_value=90
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns[1], 4) == 0.6
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2020-04-01", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns[1], 4) == 5.727
|
2022-03-11 04:10:11 +00:00
|
|
|
returns = ts.calculate_returns(
|
2022-03-13 08:58:42 +00:00
|
|
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
2022-03-11 04:10:11 +00:00
|
|
|
)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns[1], 4) == 5.727
|
2022-02-25 19:15:10 +00:00
|
|
|
with pytest.raises(DateNotFoundError):
|
2022-03-13 08:58:42 +00:00
|
|
|
ts.calculate_returns("2020-04-10", return_period_unit="days", return_period_value=90, as_on_match="exact")
|
2022-02-25 19:15:10 +00:00
|
|
|
with pytest.raises(DateNotFoundError):
|
2022-03-13 08:58:42 +00:00
|
|
|
ts.calculate_returns("2020-04-10", return_period_unit="days", return_period_value=90, prior_match="exact")
|
2022-02-25 02:52:32 +00:00
|
|
|
|
|
|
|
def test_date_formats(self):
|
2022-03-05 17:53:31 +00:00
|
|
|
ts = TimeSeries(self.data, frequency="M")
|
|
|
|
FincalOptions.date_format = "%d-%m-%Y"
|
2022-02-25 02:52:32 +00:00
|
|
|
with pytest.raises(ValueError):
|
2022-03-13 08:58:42 +00:00
|
|
|
ts.calculate_returns(
|
|
|
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
|
|
|
)
|
2022-02-25 02:52:32 +00:00
|
|
|
|
2022-03-13 08:58:42 +00:00
|
|
|
returns1 = ts.calculate_returns(
|
|
|
|
"2020-04-10", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d"
|
|
|
|
)
|
|
|
|
returns2 = ts.calculate_returns("10-04-2020", return_period_unit="days", return_period_value=90)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727
|
2022-02-25 04:17:53 +00:00
|
|
|
|
2022-03-05 17:53:31 +00:00
|
|
|
FincalOptions.date_format = "%m-%d-%Y"
|
2022-02-25 04:17:53 +00:00
|
|
|
with pytest.raises(ValueError):
|
2022-03-13 08:58:42 +00:00
|
|
|
ts.calculate_returns(
|
|
|
|
"2020-04-10", annual_compounded_returns=True, return_period_unit="days", return_period_value=90
|
|
|
|
)
|
2022-02-25 04:17:53 +00:00
|
|
|
|
2022-03-13 08:58:42 +00:00
|
|
|
returns1 = ts.calculate_returns(
|
|
|
|
"2020-04-10", return_period_unit="days", return_period_value=90, date_format="%Y-%m-%d"
|
|
|
|
)
|
|
|
|
returns2 = ts.calculate_returns("04-10-2020", return_period_unit="days", return_period_value=90)
|
2022-02-25 05:09:06 +00:00
|
|
|
assert round(returns1[1], 4) == round(returns2[1], 4) == 5.727
|
2022-03-02 18:05:57 +00:00
|
|
|
|
|
|
|
def test_limits(self):
|
2022-03-05 17:53:31 +00:00
|
|
|
ts = TimeSeries(self.data, frequency="M")
|
|
|
|
FincalOptions.date_format = "%Y-%m-%d"
|
2022-03-02 18:05:57 +00:00
|
|
|
with pytest.raises(DateNotFoundError):
|
2022-03-13 08:58:42 +00:00
|
|
|
ts.calculate_returns("2020-04-25", return_period_unit="days", return_period_value=90, closest_max_days=10)
|
2022-03-11 04:10:11 +00:00
|
|
|
|
|
|
|
|
|
|
|
class TestVolatility:
|
|
|
|
data = create_organised_test_data()
|
|
|
|
|
|
|
|
def test_volatility_basic(self):
|
|
|
|
ts = TimeSeries(self.data, frequency="D")
|
|
|
|
sd = ts.volatility()
|
|
|
|
assert len(ts) == 999
|
|
|
|
assert round(sd, 6) == 0.057391
|
|
|
|
|
|
|
|
sd = ts.volatility(annualize_volatility=False)
|
|
|
|
assert round(sd, 6) == 0.003004
|