Skip to content

Add up_and_down_trends indicator #2

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions pyindicators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
has_values_above_threshold, has_values_below_threshold, is_down_trend, \
is_up_trend
from .exceptions import PyIndicatorException
from .date_range import DateRange

__all__ = [
'sma',
Expand Down Expand Up @@ -35,4 +36,5 @@
'PyIndicatorException',
'is_down_trend',
'is_up_trend',
'DateRange',
]
56 changes: 56 additions & 0 deletions pyindicators/date_range.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from datetime import datetime
from typing import Union


class DateRange:
"""
DateRange class. This class is used to define a date range and the name of
the range. Also, it can be used to store trading metadata such as
classification of the trend (Up or Down).
"""

def __init__(
self,
start_date: datetime,
end_date: datetime,
name: str,
up_trend: bool = False,
down_trend: bool = False
):
self.start_date = start_date
self.end_date = end_date
self.name = name
self._up_trend = up_trend
self._down_trend = down_trend

@property
def up_trend(self) -> Union[bool, None]:

if self._up_trend and not self._down_trend:
return True
else:
return None

@up_trend.setter
def up_trend(self, value: bool):
self._up_trend = value

@property
def down_trend(self) -> Union[bool, None]:

if self._down_trend and not self._up_trend:
return True
else:
return None

@down_trend.setter
def down_trend(self, value: bool):
self._down_trend = value

def __str__(self):
return f"DateRange({self.start_date}, {self.end_date}, {self.name})"

def __repr__(self):
return f"DateRange(Name: {self.name} " + \
f"Start date: {self.start_date} " + \
f"End date: {self.end_date})"
2 changes: 2 additions & 0 deletions pyindicators/indicators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
has_values_below_threshold
from .is_down_trend import is_down_trend
from .is_up_trend import is_up_trend
from .up_and_down_trends import up_and_downtrends

__all__ = [
'sma',
Expand Down Expand Up @@ -42,4 +43,5 @@
'has_values_below_threshold',
'is_down_trend',
'is_up_trend',
'up_and_downtrends'
]
135 changes: 135 additions & 0 deletions pyindicators/indicators/up_and_down_trends.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from typing import Union, List
from datetime import timedelta

from pandas import DataFrame as PdDataFrame
from polars import DataFrame as PlDataFrame
import pandas as pd

from .exponential_moving_average import ema
from .utils import is_above
from pyindicators.date_range import DateRange
from pyindicators.exceptions import PyIndicatorException


def up_and_downtrends(
data: Union[PdDataFrame, PlDataFrame]
) -> List[DateRange]:
"""
Function to get the up and down trends of a pandas dataframe.

Params:
data: pd.Dataframe - instance of pandas Dateframe
containing OHLCV data.

Returns:
List of date ranges that with up_trend and down_trend
flags specified.
"""

# Check if the data is larger then 200 data points
if len(data) < 200:
raise PyIndicatorException(
"The data must be larger than 200 data " +
"points to determine up and down trends."
)

if isinstance(data, PlDataFrame):
# Convert Polars DataFrame to Pandas DataFrame
data = data.to_pandas()

selection = data.copy()
selection = ema(
selection,
source_column="Close",
period=50,
result_column="SMA_Close_50"
)
selection = ema(
selection,
source_column="Close",
period=200,
result_column="SMA_Close_200"
)

# Make selections based on the trend
current_trend = None
start_date_range = selection.index[0]
date_ranges = []

for idx, row in enumerate(selection.itertuples(index=True), start=1):
selected_rows = selection.iloc[:idx]

# Check if last row is null for the SMA_50 and SMA_200
if pd.isnull(selected_rows["SMA_Close_50"].iloc[-1]) \
or pd.isnull(selected_rows["SMA_Close_200"].iloc[-1]):
continue

if is_above(
selected_rows,
fast_column="SMA_Close_50",
slow_column="SMA_Close_200"
):
if current_trend != 'Up':

if current_trend is not None:
end_date = selection.loc[
row.Index - timedelta(days=1)
].name
date_ranges.append(
DateRange(
start_date=start_date_range,
end_date=end_date,
name=current_trend,
down_trend=True
)
)
start_date_range = row.Index
current_trend = 'Up'
else:
current_trend = 'Up'
start_date_range = row.Index
else:

if current_trend != 'Down':

if current_trend is not None:
end_date = selection.loc[
row.Index - timedelta(days=1)
].name
date_ranges.append(
DateRange(
start_date=start_date_range,
end_date=end_date,
name=current_trend,
up_trend=True
)
)
start_date_range = row.Index
current_trend = 'Down'
else:
current_trend = 'Down'
start_date_range = row.Index

if current_trend is not None:
end_date = selection.index[-1]

if current_trend == 'Up':
date_ranges.append(
DateRange(
start_date=start_date_range,
end_date=end_date,
name=current_trend,
up_trend=True
)
)
else:
date_ranges.append(
DateRange(
start_date=start_date_range,
end_date=end_date,
name=current_trend,
down_trend=True
)
)

return date_ranges
Loading