Compare commits
4 Commits
793d5b1ad7
...
6bbdac35ec
Author | SHA1 | Date | |
---|---|---|---|
6bbdac35ec | |||
f00305771b | |||
c481e2b786 | |||
d757479cca |
@ -37,6 +37,7 @@ def date_parser(*pos):
|
|||||||
|
|
||||||
Each of the dates is automatically parsed into a datetime.datetime object from string.
|
Each of the dates is automatically parsed into a datetime.datetime object from string.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def parse_dates(func):
|
def parse_dates(func):
|
||||||
def wrapper_func(*args, **kwargs):
|
def wrapper_func(*args, **kwargs):
|
||||||
date_format = kwargs.get("date_format", None)
|
date_format = kwargs.get("date_format", None)
|
||||||
@ -49,9 +50,15 @@ def date_parser(*pos):
|
|||||||
date = kwargs.get(kwarg, None)
|
date = kwargs.get(kwarg, None)
|
||||||
in_args = False
|
in_args = False
|
||||||
if date is None:
|
if date is None:
|
||||||
date = args[j]
|
try:
|
||||||
|
date = args[j]
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
in_args = True
|
in_args = True
|
||||||
|
|
||||||
|
if date is None:
|
||||||
|
continue
|
||||||
|
|
||||||
parsed_date = _parse_date(date, date_format)
|
parsed_date = _parse_date(date, date_format)
|
||||||
if not in_args:
|
if not in_args:
|
||||||
kwargs[kwarg] = parsed_date
|
kwargs[kwarg] = parsed_date
|
||||||
|
@ -7,8 +7,13 @@ from typing import Iterable, List, Literal, Mapping, Union
|
|||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
|
|
||||||
from .core import AllFrequencies, TimeSeriesCore, date_parser
|
from .core import AllFrequencies, Series, TimeSeriesCore, date_parser
|
||||||
from .utils import _find_closest_date, _interval_to_years, _preprocess_match_options
|
from .utils import (
|
||||||
|
FincalOptions,
|
||||||
|
_find_closest_date,
|
||||||
|
_interval_to_years,
|
||||||
|
_preprocess_match_options,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@date_parser(0, 1)
|
@date_parser(0, 1)
|
||||||
@ -17,6 +22,7 @@ def create_date_series(
|
|||||||
end_date: Union[str, datetime.datetime],
|
end_date: Union[str, datetime.datetime],
|
||||||
frequency: Literal["D", "W", "M", "Q", "H", "Y"],
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"],
|
||||||
eomonth: bool = False,
|
eomonth: bool = False,
|
||||||
|
skip_weekends: bool = False,
|
||||||
) -> List[datetime.datetime]:
|
) -> List[datetime.datetime]:
|
||||||
"""Create a date series with a specified frequency
|
"""Create a date series with a specified frequency
|
||||||
|
|
||||||
@ -53,8 +59,6 @@ def create_date_series(
|
|||||||
if eomonth and frequency.days < AllFrequencies.M.days:
|
if eomonth and frequency.days < AllFrequencies.M.days:
|
||||||
raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}")
|
raise ValueError(f"eomonth cannot be set to True if frequency is higher than {AllFrequencies.M.name}")
|
||||||
|
|
||||||
# start_date = _parse_date(start_date)
|
|
||||||
# end_date = _parse_date(end_date)
|
|
||||||
datediff = (end_date - start_date).days / frequency.days + 1
|
datediff = (end_date - start_date).days / frequency.days + 1
|
||||||
dates = []
|
dates = []
|
||||||
|
|
||||||
@ -67,9 +71,12 @@ def create_date_series(
|
|||||||
date = date.replace(day=1).replace(month=next_month) - relativedelta(days=1)
|
date = date.replace(day=1).replace(month=next_month) - relativedelta(days=1)
|
||||||
|
|
||||||
if date <= end_date:
|
if date <= end_date:
|
||||||
dates.append(date)
|
if frequency.days > 1 or not skip_weekends:
|
||||||
|
dates.append(date)
|
||||||
|
elif date.weekday() < 5:
|
||||||
|
dates.append(date)
|
||||||
|
|
||||||
return dates
|
return Series(dates, data_type="date")
|
||||||
|
|
||||||
|
|
||||||
class TimeSeries(TimeSeriesCore):
|
class TimeSeries(TimeSeriesCore):
|
||||||
@ -387,8 +394,8 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
@date_parser(1, 2)
|
@date_parser(1, 2)
|
||||||
def volatility(
|
def volatility(
|
||||||
self,
|
self,
|
||||||
from_date: Union[datetime.date, str],
|
from_date: Union[datetime.date, str] = None,
|
||||||
to_date: Union[datetime.date, str],
|
to_date: Union[datetime.date, str] = None,
|
||||||
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
frequency: Literal["D", "W", "M", "Q", "H", "Y"] = None,
|
||||||
as_on_match: str = "closest",
|
as_on_match: str = "closest",
|
||||||
prior_match: str = "closest",
|
prior_match: str = "closest",
|
||||||
@ -399,6 +406,7 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
interval_value: int = 1,
|
interval_value: int = 1,
|
||||||
date_format: str = None,
|
date_format: str = None,
|
||||||
annualize_volatility: bool = True,
|
annualize_volatility: bool = True,
|
||||||
|
traded_days: int = None,
|
||||||
):
|
):
|
||||||
"""Calculates the volatility of the time series.add()
|
"""Calculates the volatility of the time series.add()
|
||||||
|
|
||||||
@ -414,6 +422,11 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
raise ValueError(f"Invalid argument for frequency {frequency}")
|
raise ValueError(f"Invalid argument for frequency {frequency}")
|
||||||
|
|
||||||
|
if from_date is None:
|
||||||
|
from_date = self.start_date + relativedelta(**{interval_type: interval_value})
|
||||||
|
if to_date is None:
|
||||||
|
to_date = self.end_date
|
||||||
|
|
||||||
if annual_compounded_returns is None:
|
if annual_compounded_returns is None:
|
||||||
annual_compounded_returns = False if frequency.days <= 366 else True
|
annual_compounded_returns = False if frequency.days <= 366 else True
|
||||||
|
|
||||||
@ -431,10 +444,13 @@ class TimeSeries(TimeSeriesCore):
|
|||||||
)
|
)
|
||||||
sd = statistics.stdev(rolling_returns.values)
|
sd = statistics.stdev(rolling_returns.values)
|
||||||
if annualize_volatility:
|
if annualize_volatility:
|
||||||
|
if traded_days is None:
|
||||||
|
traded_days = FincalOptions.traded_days
|
||||||
|
|
||||||
if interval_type == "months":
|
if interval_type == "months":
|
||||||
sd *= math.sqrt(12)
|
sd *= math.sqrt(12)
|
||||||
elif interval_type == "days":
|
elif interval_type == "days":
|
||||||
sd *= math.sqrt(252)
|
sd *= math.sqrt(traded_days)
|
||||||
|
|
||||||
return sd
|
return sd
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from .exceptions import DateNotFoundError, DateOutOfRangeError
|
|||||||
class FincalOptions:
|
class FincalOptions:
|
||||||
date_format: str = "%Y-%m-%d"
|
date_format: str = "%Y-%m-%d"
|
||||||
closest: str = "before" # after
|
closest: str = "before" # after
|
||||||
|
traded_days: int = 365
|
||||||
|
|
||||||
|
|
||||||
def _parse_date(date: str, date_format: str = None):
|
def _parse_date(date: str, date_format: str = None):
|
||||||
|
@ -13,7 +13,7 @@ THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
|||||||
sample_data_path = os.path.join(THIS_DIR, "data")
|
sample_data_path = os.path.join(THIS_DIR, "data")
|
||||||
|
|
||||||
|
|
||||||
def create_test_data(
|
def create_random_test_data(
|
||||||
frequency: str,
|
frequency: str,
|
||||||
eomonth: bool,
|
eomonth: bool,
|
||||||
n: int,
|
n: int,
|
||||||
@ -55,6 +55,30 @@ def create_test_data(
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
class TestDateSeries:
|
class TestDateSeries:
|
||||||
def test_daily(self):
|
def test_daily(self):
|
||||||
start_date = datetime.datetime(2020, 1, 1)
|
start_date = datetime.datetime(2020, 1, 1)
|
||||||
@ -119,7 +143,9 @@ class TestDateSeries:
|
|||||||
|
|
||||||
class TestFincalBasic:
|
class TestFincalBasic:
|
||||||
def test_creation(self):
|
def test_creation(self):
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
assert len(time_series) == 50
|
assert len(time_series) == 50
|
||||||
assert isinstance(time_series.frequency, Frequency)
|
assert isinstance(time_series.frequency, Frequency)
|
||||||
@ -128,12 +154,16 @@ class TestFincalBasic:
|
|||||||
ffill_data = time_series.ffill()
|
ffill_data = time_series.ffill()
|
||||||
assert len(ffill_data) == 50
|
assert len(ffill_data) == 50
|
||||||
|
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
assert len(time_series) == 450
|
assert len(time_series) == 450
|
||||||
|
|
||||||
def test_fill(self):
|
def test_fill(self):
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
ffill_data = time_series.ffill()
|
ffill_data = time_series.ffill()
|
||||||
assert len(ffill_data) >= 498
|
assert len(ffill_data) >= 498
|
||||||
@ -142,7 +172,9 @@ class TestFincalBasic:
|
|||||||
assert ffill_data is None
|
assert ffill_data is None
|
||||||
assert len(time_series) >= 498
|
assert len(time_series) >= 498
|
||||||
|
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=500, gaps=0.1, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
bfill_data = time_series.bfill()
|
bfill_data = time_series.bfill()
|
||||||
assert len(bfill_data) >= 498
|
assert len(bfill_data) >= 498
|
||||||
@ -160,7 +192,9 @@ class TestFincalBasic:
|
|||||||
assert bf["2021-01-03"][1] == 240
|
assert bf["2021-01-03"][1] == 240
|
||||||
|
|
||||||
def test_iloc_slicing(self):
|
def test_iloc_slicing(self):
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
assert time_series.iloc[0] is not None
|
assert time_series.iloc[0] is not None
|
||||||
assert time_series.iloc[:3] is not None
|
assert time_series.iloc[:3] is not None
|
||||||
@ -170,7 +204,9 @@ class TestFincalBasic:
|
|||||||
assert len(time_series.iloc[10:20]) == 10
|
assert len(time_series.iloc[10:20]) == 10
|
||||||
|
|
||||||
def test_key_slicing(self):
|
def test_key_slicing(self):
|
||||||
data = create_test_data(frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True)
|
data = create_random_test_data(
|
||||||
|
frequency="D", eomonth=False, n=50, gaps=0, month_position="start", date_as_str=True
|
||||||
|
)
|
||||||
time_series = TimeSeries(data, frequency="D")
|
time_series = TimeSeries(data, frequency="D")
|
||||||
available_date = time_series.iloc[5][0]
|
available_date = time_series.iloc[5][0]
|
||||||
assert time_series[available_date] is not None
|
assert time_series[available_date] is not None
|
||||||
@ -199,17 +235,29 @@ class TestReturns:
|
|||||||
|
|
||||||
def test_returns_calc(self):
|
def test_returns_calc(self):
|
||||||
ts = TimeSeries(self.data, frequency="M")
|
ts = TimeSeries(self.data, frequency="M")
|
||||||
returns = ts.calculate_returns("2021-01-01", annual_compounded_returns=False, interval_type="years", interval_value=1)
|
returns = ts.calculate_returns(
|
||||||
|
"2021-01-01", annual_compounded_returns=False, interval_type="years", interval_value=1
|
||||||
|
)
|
||||||
assert returns[1] == 2.4
|
assert returns[1] == 2.4
|
||||||
returns = ts.calculate_returns("2020-04-01", annual_compounded_returns=False, interval_type="months", interval_value=3)
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=False, interval_type="months", interval_value=3
|
||||||
|
)
|
||||||
assert round(returns[1], 4) == 0.6
|
assert round(returns[1], 4) == 0.6
|
||||||
returns = ts.calculate_returns("2020-04-01", annual_compounded_returns=True, interval_type="months", interval_value=3)
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=True, interval_type="months", interval_value=3
|
||||||
|
)
|
||||||
assert round(returns[1], 4) == 5.5536
|
assert round(returns[1], 4) == 5.5536
|
||||||
returns = ts.calculate_returns("2020-04-01", annual_compounded_returns=False, interval_type="days", interval_value=90)
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=False, interval_type="days", interval_value=90
|
||||||
|
)
|
||||||
assert round(returns[1], 4) == 0.6
|
assert round(returns[1], 4) == 0.6
|
||||||
returns = ts.calculate_returns("2020-04-01", annual_compounded_returns=True, interval_type="days", interval_value=90)
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=True, interval_type="days", interval_value=90
|
||||||
|
)
|
||||||
assert round(returns[1], 4) == 5.727
|
assert round(returns[1], 4) == 5.727
|
||||||
returns = ts.calculate_returns("2020-04-10", annual_compounded_returns=True, interval_type="days", interval_value=90)
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-10", annual_compounded_returns=True, interval_type="days", interval_value=90
|
||||||
|
)
|
||||||
assert round(returns[1], 4) == 5.727
|
assert round(returns[1], 4) == 5.727
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, as_on_match="exact")
|
ts.calculate_returns("2020-04-10", interval_type="days", interval_value=90, as_on_match="exact")
|
||||||
@ -239,3 +287,16 @@ class TestReturns:
|
|||||||
FincalOptions.date_format = "%Y-%m-%d"
|
FincalOptions.date_format = "%Y-%m-%d"
|
||||||
with pytest.raises(DateNotFoundError):
|
with pytest.raises(DateNotFoundError):
|
||||||
ts.calculate_returns("2020-04-25", interval_type="days", interval_value=90, closest_max_days=10)
|
ts.calculate_returns("2020-04-25", interval_type="days", interval_value=90, closest_max_days=10)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
95
tests/test_fincal2.py
Normal file
95
tests/test_fincal2.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import datetime
|
||||||
|
import math
|
||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
random.seed(1234) # WARNING! Changing the seed will cause most tests to fail
|
||||||
|
all_values = []
|
||||||
|
for _ in range(num_prices):
|
||||||
|
s0 *= math.exp(
|
||||||
|
(mu - 0.5 * sigma**2) * (1.0 / 365.0) + sigma * math.sqrt(1.0 / 365.0) * random.gauss(mu=0, sigma=1)
|
||||||
|
)
|
||||||
|
all_values.append(round(s0, 2))
|
||||||
|
|
||||||
|
return all_values
|
||||||
|
|
||||||
|
|
||||||
|
def create_data():
|
||||||
|
"""Creates TimeSeries data"""
|
||||||
|
|
||||||
|
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")
|
||||||
|
return ts
|
||||||
|
|
||||||
|
|
||||||
|
class TestReturns:
|
||||||
|
def test_returns_calc(self):
|
||||||
|
ts = create_data()
|
||||||
|
returns = ts.calculate_returns(
|
||||||
|
"2020-01-01", annual_compounded_returns=False, interval_type="years", interval_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
|
||||||
|
)
|
||||||
|
assert round(returns[1], 6) == 0.015908
|
||||||
|
|
||||||
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=True, interval_type="months", interval_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
|
||||||
|
)
|
||||||
|
assert round(returns[1], 6) == 0.017673
|
||||||
|
|
||||||
|
returns = ts.calculate_returns(
|
||||||
|
"2020-04-01", annual_compounded_returns=True, interval_type="days", interval_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")
|
||||||
|
with pytest.raises(DateNotFoundError):
|
||||||
|
ts.calculate_returns("2020-04-04", interval_type="months", interval_value=3, prior_match="exact")
|
||||||
|
|
||||||
|
def test_date_formats(self):
|
||||||
|
ts = create_data()
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
assert round(returns1[1], 6) == round(returns2[1], 6) == 0.073632
|
||||||
|
|
||||||
|
def test_limits(self):
|
||||||
|
ts = create_data()
|
||||||
|
FincalOptions.date_format = "%Y-%m-%d"
|
||||||
|
with pytest.raises(DateNotFoundError):
|
||||||
|
ts.calculate_returns("2020-11-25", interval_type="days", interval_value=90, closest_max_days=10)
|
Loading…
Reference in New Issue
Block a user