Skip to content

Commit e49753c

Browse files
committed
Add up_and_down_trends indicator
1 parent 7ed9d61 commit e49753c

File tree

4 files changed

+195
-0
lines changed

4 files changed

+195
-0
lines changed

pyindicators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
has_values_above_threshold, has_values_below_threshold, is_down_trend, \
77
is_up_trend
88
from .exceptions import PyIndicatorException
9+
from .date_range import DateRange
910

1011
__all__ = [
1112
'sma',
@@ -35,4 +36,5 @@
3536
'PyIndicatorException',
3637
'is_down_trend',
3738
'is_up_trend',
39+
'DateRange',
3840
]

pyindicators/date_range.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from datetime import datetime
2+
from typing import Union
3+
4+
5+
class DateRange:
6+
"""
7+
DateRange class. This class is used to define a date range and the name of
8+
the range. Also, it can be used to store trading metadata such as
9+
classification of the trend (Up or Down).
10+
"""
11+
12+
def __init__(
13+
self,
14+
start_date: datetime,
15+
end_date: datetime,
16+
name: str,
17+
up_trend: bool = False,
18+
down_trend: bool = False
19+
):
20+
self.start_date = start_date
21+
self.end_date = end_date
22+
self.name = name
23+
self._up_trend = up_trend
24+
self._down_trend = down_trend
25+
26+
@property
27+
def up_trend(self) -> Union[bool, None]:
28+
29+
if self._up_trend and not self._down_trend:
30+
return True
31+
else:
32+
return None
33+
34+
@up_trend.setter
35+
def up_trend(self, value: bool):
36+
self._up_trend = value
37+
38+
@property
39+
def down_trend(self) -> Union[bool, None]:
40+
41+
if self._down_trend and not self._up_trend:
42+
return True
43+
else:
44+
return None
45+
46+
@down_trend.setter
47+
def down_trend(self, value: bool):
48+
self._down_trend = value
49+
50+
def __str__(self):
51+
return f"DateRange({self.start_date}, {self.end_date}, {self.name})"
52+
53+
def __repr__(self):
54+
return f"DateRange(Name: {self.name} " + \
55+
f"Start date: {self.start_date} " + \
56+
f"End date: {self.end_date})"

pyindicators/indicators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
has_values_below_threshold
1515
from .is_down_trend import is_down_trend
1616
from .is_up_trend import is_up_trend
17+
from .up_and_down_trends import up_and_downtrends
1718

1819
__all__ = [
1920
'sma',
@@ -42,4 +43,5 @@
4243
'has_values_below_threshold',
4344
'is_down_trend',
4445
'is_up_trend',
46+
'up_and_downtrends'
4547
]
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
from typing import Union, List
2+
from datetime import timedelta
3+
4+
from pandas import DataFrame as PdDataFrame
5+
from polars import DataFrame as PlDataFrame
6+
import pandas as pd
7+
8+
from .exponential_moving_average import ema
9+
from .utils import is_above
10+
from pyindicators.date_range import DateRange
11+
from pyindicators.exceptions import PyIndicatorException
12+
13+
14+
def up_and_downtrends(
15+
data: Union[PdDataFrame, PlDataFrame]
16+
) -> List[DateRange]:
17+
"""
18+
Function to get the up and down trends of a pandas dataframe.
19+
20+
Params:
21+
data: pd.Dataframe - instance of pandas Dateframe
22+
containing OHLCV data.
23+
24+
Returns:
25+
List of date ranges that with up_trend and down_trend
26+
flags specified.
27+
"""
28+
29+
# Check if the data is larger then 200 data points
30+
if len(data) < 200:
31+
raise PyIndicatorException(
32+
"The data must be larger than 200 data " +
33+
"points to determine up and down trends."
34+
)
35+
36+
if isinstance(data, PlDataFrame):
37+
# Convert Polars DataFrame to Pandas DataFrame
38+
data = data.to_pandas()
39+
40+
selection = data.copy()
41+
selection = ema(
42+
selection,
43+
source_column="Close",
44+
period=50,
45+
result_column="SMA_Close_50"
46+
)
47+
selection = ema(
48+
selection,
49+
source_column="Close",
50+
period=200,
51+
result_column="SMA_Close_200"
52+
)
53+
54+
# Make selections based on the trend
55+
current_trend = None
56+
start_date_range = selection.index[0]
57+
date_ranges = []
58+
59+
for idx, row in enumerate(selection.itertuples(index=True), start=1):
60+
selected_rows = selection.iloc[:idx]
61+
62+
# Check if last row is null for the SMA_50 and SMA_200
63+
if pd.isnull(selected_rows["SMA_Close_50"].iloc[-1]) \
64+
or pd.isnull(selected_rows["SMA_Close_200"].iloc[-1]):
65+
continue
66+
67+
if is_above(
68+
selected_rows,
69+
fast_column="SMA_Close_50",
70+
slow_column="SMA_Close_200"
71+
):
72+
if current_trend != 'Up':
73+
74+
if current_trend is not None:
75+
end_date = selection.loc[
76+
row.Index - timedelta(days=1)
77+
].name
78+
date_ranges.append(
79+
DateRange(
80+
start_date=start_date_range,
81+
end_date=end_date,
82+
name=current_trend,
83+
down_trend=True
84+
)
85+
)
86+
start_date_range = row.Index
87+
current_trend = 'Up'
88+
else:
89+
current_trend = 'Up'
90+
start_date_range = row.Index
91+
else:
92+
93+
if current_trend != 'Down':
94+
95+
if current_trend is not None:
96+
end_date = selection.loc[
97+
row.Index - timedelta(days=1)
98+
].name
99+
date_ranges.append(
100+
DateRange(
101+
start_date=start_date_range,
102+
end_date=end_date,
103+
name=current_trend,
104+
up_trend=True
105+
)
106+
)
107+
start_date_range = row.Index
108+
current_trend = 'Down'
109+
else:
110+
current_trend = 'Down'
111+
start_date_range = row.Index
112+
113+
if current_trend is not None:
114+
end_date = selection.index[-1]
115+
116+
if current_trend == 'Up':
117+
date_ranges.append(
118+
DateRange(
119+
start_date=start_date_range,
120+
end_date=end_date,
121+
name=current_trend,
122+
up_trend=True
123+
)
124+
)
125+
else:
126+
date_ranges.append(
127+
DateRange(
128+
start_date=start_date_range,
129+
end_date=end_date,
130+
name=current_trend,
131+
down_trend=True
132+
)
133+
)
134+
135+
return date_ranges

0 commit comments

Comments
 (0)