Skip to content

Commit fa2c1fa

Browse files
committed
Add peaks and divergence detection functionality
1 parent 5239356 commit fa2c1fa

File tree

10 files changed

+644
-352
lines changed

10 files changed

+644
-352
lines changed

README.md

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ pip install pyindicators
3535
* [Relative Strength Index Wilders method (Wilders RSI)](#wilders-relative-strength-index-wilders-rsi)
3636
* [Williams %R](#williams-r)
3737
* [Average Directional Index (ADX)](#average-directional-index-adx)
38+
* [Pattern recognition](#pattern-recognition)
39+
* [Detect Peaks](#detect-peaks)
40+
* [Detect Bullish Divergence](#detect-bullish-divergence)
41+
* [Detect Bearish Divergence](#detect-bearish-divergence)
3842
* [Indicator helpers](#indicator-helpers)
3943
* [Crossover](#crossover)
4044
* [Is Crossover](#is-crossover)
@@ -473,6 +477,171 @@ pd_df.tail(10)
473477

474478
![ADX](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/adx.png)
475479

480+
### Pattern Recognition
481+
482+
#### Detect Peaks
483+
484+
The detect_peaks function is used to identify peaks and lows in a given column of a DataFrame. It returns a DataFrame with two additional columns: one for higher highs and another for lower lows. The function can be used to detect peaks and lows in a DataFrame. It identifies local maxima and minima based on the specified order of neighboring points. The function can also filter out peaks and lows based on a minimum number of consecutive occurrences. This allows you to focus on significant peaks and lows that are more likely to be relevant for analysis.
485+
486+
> There is always a delay between an actual peak and the detection of that peak. This is determined by the `number_of_neighbors_to_compare` parameter. For example
487+
> if for a given column you set `number_of_neighbors_to_compare=5`, the function will look at the 5 previous and 5 next data points to determine if the current point is a peak or a low. This means that the peak or low will only be detected after the 5th data point has been processed. So say you have OHLCV data of 15 minute intervals, and you set `number_of_neighbors_to_compare=5`, the function will only detect the peak or low after the 5th data point has been processed, which means that there will be a delay of 75 minutes (5 * 15 minutes) before the peak or low is detected.
488+
489+
```python
490+
def detect_peaks(
491+
data: Union[PdDataFrame, PlDataFrame],
492+
column: str,
493+
number_of_neighbors_to_compare: int = 5,
494+
min_consecutive: int = 2
495+
) -> Union[PdDataFrame, PlDataFrame]:
496+
```
497+
498+
Example
499+
500+
```python
501+
from investing_algorithm_framework import download
502+
from pyindicators import detect_peaks
503+
504+
pl_df = download(
505+
symbol="btc/eur",
506+
market="binance",
507+
time_frame="1d",
508+
start_date="2023-12-01",
509+
end_date="2023-12-25",
510+
save=True,
511+
storage_path="./data"
512+
)
513+
514+
pd_df = download(
515+
symbol="btc/eur",
516+
market="binance",
517+
time_frame="1d",
518+
start_date="2023-12-01",
519+
end_date="2023-12-25",
520+
pandas=True,
521+
save=True,
522+
storage_path="./data"
523+
)
524+
525+
# Calculate peaks and lows for Polars DataFrame, with a neighbour comparison of 4 and minimum of 2 consecutive peaks
526+
pl_df = detect_peaks(pl_df, column="Close", number_of_neighbors_to_compare=4, min_consecutive=2)
527+
pl_df.show(10)
528+
529+
# Calculate peaks and lows for Pandas DataFrame, with a neighbour comparison of 4 and minimum of 2 consecutive peaks
530+
pd_df = detect_peaks(pd_df, column="Close", number_of_neighbors_to_compare=4, min_consecutive=2)
531+
pd_df.tail(10)
532+
```
533+
534+
![PEAKS](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/detect_peaks.png)
535+
536+
#### Detect Bullish Divergence
537+
538+
The detect_bullish_divergence function is used to identify bullish divergences between two columns in a DataFrame. It checks for bullish divergences based on the peaks and lows detected in the specified columns. The function returns a DataFrame with additional columns indicating the presence of bullish divergences.
539+
540+
A bullish divergence occurs when the price makes a lower low while the indicator makes a higher low. This suggests that the downward momentum is weakening, and a potential reversal to the upside may occur.
541+
542+
> !Important: This function expects that for two given columns there will be corresponding peaks and lows columns. This means that before you can use this function, you must first call the detect_peaks function on both columns. For example: if you want to detect bullish divergence between the "Close" column and the "RSI_14" column, you must first call detect_peaks on both columns.
543+
> If no corresponding {column}_peaks and {column}_lows columns are found, the function will raise a PyIndicatorException.
544+
545+
```python
546+
def bullish_divergence(
547+
data: Union[pd.DataFrame, pl.DataFrame],
548+
first_column: str,
549+
second_column: str,
550+
window_size=1,
551+
result_column: str = "bullish_divergence",
552+
number_of_neighbors_to_compare: int = 5,
553+
min_consecutive: int = 2
554+
) -> Union[pd.DataFrame, pl.DataFrame]:
555+
```
556+
557+
Example
558+
559+
```python
560+
from investing_algorithm_framework import download
561+
from pyindicators import bullish_divergence
562+
pl_df = download(
563+
symbol="btc/eur",
564+
market="binance",
565+
time_frame="1d",
566+
start_date="2023-12-01",
567+
end_date="2023-12-25",
568+
save=True,
569+
storage_path="./data"
570+
)
571+
pd_df = download(
572+
symbol="btc/eur",
573+
market="binance",
574+
time_frame="1d",
575+
start_date="2023-12-01",
576+
end_date="2023-12-25",
577+
pandas=True,
578+
save=True,
579+
storage_path="./data"
580+
)
581+
582+
# Calculate bullish divergence for Polars DataFrame
583+
pl_df = bullish_divergence(pl_df, first_column="Close", second_column="RSI_14", window_size=8)
584+
pl_df.show(10)
585+
586+
# Calculate bullish divergence for Pandas DataFrame
587+
pd_df = bullish_divergence(pd_df, first_column="Close", second_column="RSI_14", window_size=8)
588+
pd_df.tail(10)
589+
```
590+
591+
![BULLISH_DIVERGENCE](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/bullish_divergence.png)
592+
593+
#### Detect Bearish Divergence
594+
595+
The detect_bearish_divergence function is used to identify bearish divergences between two columns in a DataFrame. It checks for bearish divergences based on the peaks and lows detected in the specified columns. The function returns a DataFrame with additional columns indicating the presence of bearish divergences.
596+
597+
A bearish divergence occurs when the price makes a higher high while the indicator makes a lower high. This suggests that the upward momentum is weakening, and a potential reversal to the downside may occur.
598+
599+
```python
600+
def bearish_divergence(
601+
data: Union[pd.DataFrame, pl.DataFrame],
602+
first_column: str,
603+
second_column: str,
604+
window_size=1,
605+
result_column: str = "bearish_divergence",
606+
number_of_neighbors_to_compare: int = 5,
607+
min_consecutive: int = 2
608+
) -> Union[pd.DataFrame, pl.DataFrame]:
609+
```
610+
611+
Example
612+
613+
```python
614+
from investing_algorithm_framework import download
615+
from pyindicators import bearish_divergence
616+
pl_df = download(
617+
symbol="btc/eur",
618+
market="binance",
619+
time_frame="1d",
620+
start_date="2023-12-01",
621+
end_date="2023-12-25",
622+
save=True,
623+
storage_path="./data"
624+
)
625+
pd_df = download(
626+
symbol="btc/eur",
627+
market="binance",
628+
time_frame="1d",
629+
start_date="2023-12-01",
630+
end_date="2023-12-25",
631+
pandas=True,
632+
save=True,
633+
storage_path="./data"
634+
)
635+
636+
# Calculate bearish divergence for Polars DataFrame
637+
pl_df = bearish_divergence(pl_df, first_column="Close", second_column="RSI_14", window_size=8)
638+
pl_df.show(10)
639+
640+
# Calculate bearish divergence for Pandas DataFrame
641+
pd_df = bearish_divergence(pd_df, first_column="Close", second_column="RSI_14", window_size=8)
642+
pd_df.tail(10)
643+
```
644+
476645

477646
### Indicator helpers
478647

pyindicators/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from .indicators import sma, rsi, ema, wilders_rsi, adx, \
22
crossover, is_crossover, wma, macd, willr, is_crossunder, crossunder, \
3-
get_peaks, is_divergence, is_lower_low_detected, \
3+
is_lower_low_detected, is_divergence, \
44
is_below, is_above, get_slope, has_any_higher_then_threshold, \
55
has_slope_above_threshold, has_any_lower_then_threshold, \
66
has_values_above_threshold, has_values_below_threshold, is_down_trend, \
7-
is_up_trend, up_and_downtrends
7+
is_up_trend, up_and_downtrends, detect_peaks, \
8+
bearish_divergence, bullish_divergence
89
from .exceptions import PyIndicatorException
910
from .date_range import DateRange
1011

@@ -22,8 +23,6 @@
2223
'macd',
2324
'willr',
2425
'adx',
25-
'get_peaks',
26-
'is_divergence',
2726
'is_lower_low_detected',
2827
'is_below',
2928
'is_above',
@@ -38,4 +37,8 @@
3837
'is_up_trend',
3938
'up_and_downtrends',
4039
'DateRange',
40+
'detect_peaks',
41+
'bearish_divergence',
42+
'bullish_divergence',
43+
'is_divergence',
4144
]

pyindicators/indicators/__init__.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,16 @@
77
from .macd import macd
88
from .williams_percent_range import willr
99
from .adx import adx
10-
from .utils import get_peaks, is_divergence, is_lower_low_detected, \
10+
from .utils import is_lower_low_detected, \
1111
is_below, is_above, get_slope, has_any_higher_then_threshold, \
1212
has_slope_above_threshold, has_any_lower_then_threshold, \
1313
has_slope_below_threshold, has_values_above_threshold, \
14-
has_values_below_threshold
14+
has_values_below_threshold, is_divergence
1515
from .is_down_trend import is_down_trend
1616
from .is_up_trend import is_up_trend
1717
from .up_and_down_trends import up_and_downtrends
18+
from .divergence import detect_peaks, bearish_divergence, \
19+
bullish_divergence
1820

1921
__all__ = [
2022
'sma',
@@ -29,8 +31,6 @@
2931
'macd',
3032
'willr',
3133
'adx',
32-
'get_peaks',
33-
'is_divergence',
3434
'is_lower_low_detected',
3535
'is_below',
3636
'is_above',
@@ -43,5 +43,9 @@
4343
'has_values_below_threshold',
4444
'is_down_trend',
4545
'is_up_trend',
46-
'up_and_downtrends'
46+
'up_and_downtrends',
47+
'detect_peaks',
48+
'bearish_divergence',
49+
'bullish_divergence',
50+
'is_divergence',
4751
]

pyindicators/indicators/adx.py

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -172,92 +172,3 @@ def adx(
172172
# Convert the Pandas DataFrame back to a Polars DataFrame
173173
data = pl.from_pandas(data)
174174
return data
175-
176-
# copy_df = data.clone()
177-
178-
# # True Range (TR)
179-
# copy_df = copy_df.with_columns([
180-
# (pl.col("High") - pl.col("Low")).alias("H-L"),
181-
# (pl.col("High") - pl.col("Close").shift(1)
182-
# .fill_null(0)).abs().alias("H-C"),
183-
# (pl.col("Low") - pl.col("Close").shift(1)
184-
# .fill_null(0)).abs().alias("L-C")
185-
# ])
186-
187-
# copy_df = copy_df.with_columns([
188-
# pl.col("H-L").fill_null(0).alias("H-L"),
189-
# pl.col("H-C").fill_null(0).alias("H-C"),
190-
# pl.col("L-C").fill_null(0).alias("L-C")
191-
# ])
192-
193-
# copy_df = copy_df.with_columns(
194-
# pl.max_horizontal(["H-L", "H-C", "L-C"]).alias("TR")
195-
# ).drop(["H-L", "H-C", "L-C"])
196-
197-
# # ATR using Pandas
198-
# copy_df = copy_df.with_columns(
199-
# polars_ewm_mean_via_pandas(copy_df["TR"]
200-
# .fill_nan(0), alpha).alias("ATR")
201-
# )
202-
203-
# # +-DX calculation
204-
# copy_df = copy_df.with_columns([
205-
# (pl.col("High") - pl.col("High").shift(1)
206-
# .fill_null(0)).alias("H-pH"),
207-
# (pl.col("Low").shift(1).fill_null(0) - pl.col("Low")).alias("pL-L")
208-
# ])
209-
210-
# copy_df = copy_df.with_columns([
211-
# pl.when((pl.col("H-pH") > pl.col("pL-L")) & (pl.col("H-pH") > 0))
212-
# .then(pl.col("H-pH")).otherwise(0.0).alias("+DI"),
213-
# pl.when((pl.col("H-pH") < pl.col("pL-L")) & (pl.col("pL-L") > 0))
214-
# .then(pl.col("pL-L")).otherwise(0.0).alias("-DI")
215-
# ]).drop(["H-pH", "pL-L"])
216-
217-
# # Smooth DI using Pandas
218-
# copy_df = copy_df.with_columns([
219-
# polars_ewm_mean_via_pandas(copy_df["+DI"]
220-
# .fill_nan(0), alpha).alias("S+DM"),
221-
# polars_ewm_mean_via_pandas(copy_df["-DI"]
222-
# .fill_nan(0), alpha).alias("S-DM")
223-
# ])
224-
225-
# copy_df = copy_df.with_columns([
226-
# ((pl.col("S+DM") / pl.col("ATR")) * 100).alias("+DMI"),
227-
# ((pl.col("S-DM") / pl.col("ATR")) * 100).alias("-DMI")
228-
# ]).drop(["S+DM", "S-DM"])
229-
230-
# # ADX
231-
# copy_df = copy_df.with_columns(
232-
# pl.when((pl.col("+DMI") + pl.col("-DMI")) > 0)
233-
# .then(((pl.col("+DMI") - pl.col("-DMI"))
234-
# .abs()) / (pl.col("+DMI") + pl.col("-DMI")) * 100)
235-
# .otherwise(0.0).alias("DX")
236-
# )
237-
238-
# copy_df = copy_df.with_columns(
239-
# polars_ewm_mean_via_pandas(copy_df["DX"]
240-
# .fill_nan(0), alpha).alias("ADX")
241-
# ).drop(["DX", "ATR", "TR"])
242-
243-
# # Fill NaNs
244-
# copy_df = copy_df.with_columns([
245-
# pl.col("ADX").fill_nan(0).alias("ADX"),
246-
# pl.col("+DMI").fill_nan(0).alias("+DMI"),
247-
# pl.col("-DMI").fill_nan(0).alias("-DMI")
248-
# ])
249-
250-
# # Copy to original
251-
# data = data.with_columns([
252-
# copy_df["ADX"].alias(adx_result_column),
253-
# copy_df["+DMI"].alias(di_plus_result_column),
254-
# copy_df["-DMI"].alias(di_minus_result_column)
255-
# ])
256-
257-
# # Padding zeros
258-
# data = pad_zero_values_polars(
259-
# data, column=di_plus_result_column, period=period)
260-
# data = pad_zero_values_polars(
261-
# data, column=di_minus_result_column, period=period)
262-
# data = pad_zero_values_polars
263-
# (data, column=adx_result_column, period=period)

0 commit comments

Comments
 (0)