Skip to content

Commit 8e544e4

Browse files
committed
Add crossover indicator
1 parent 0a286cb commit 8e544e4

File tree

5 files changed

+111
-4
lines changed

5 files changed

+111
-4
lines changed

README.md

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@ pip install pyindicators
1414

1515
* Native Python implementation, no external dependencies needed except for Polars or Pandas
1616
* Dataframe first approach, with support for both pandas dataframes and polars dataframes
17-
* Trend indicators
17+
* Trend indicators(#trend-indicators)
1818
* [Simple Moving Average (SMA)](#simple-moving-average-sma)
1919
* [Exponential Moving Average (EMA)](#exponential-moving-average-ema)
20-
* Momentum indicators
20+
* Momentum indicators(#momentum-indicators)
2121
* [Relative Strength Index (RSI)](#relative-strength-index-rsi)
2222
* [Relative Strength Index Wilders method (RSI)](#wilders-relative-strength-index-wilders-rsi)
23+
* Indicator helpers(#indicator-helpers)
24+
* [Crossover](#crossover)
25+
* [Is Crossover](#is-crossover)
2326

2427
## Indicators
2528

@@ -129,6 +132,47 @@ pd_df.tail(10)
129132

130133
### Indicator helpers
131134

135+
#### Crossover
136+
137+
```python
138+
from polars import DataFrame as plDataFrame
139+
from pandas import DataFrame as pdDataFrame
140+
141+
from investing_algorithm_framework import CSVOHLCVMarketDataSource
142+
from pyindicators import crossover, ema
143+
144+
# For this example the investing algorithm framework is used for dataframe creation,
145+
csv_path = "./tests/test_data/OHLCV_BTC-EUR_BINANCE_15m_2023-12-01:00:00_2023-12-25:00:00.csv"
146+
data_source = CSVOHLCVMarketDataSource(csv_file_path=csv_path)
147+
148+
pl_df = data_source.get_data()
149+
pd_df = data_source.get_data(pandas=True)
150+
151+
# Calculate EMA and crossover for Polars DataFrame
152+
pl_df = ema(pl_df, source_column="Close", period=200, result_column="EMA_200")
153+
pl_df = ema(pl_df, source_column="Close", period=50, result_column="EMA_50")
154+
pl_df = crossover(
155+
pl_df,
156+
first_column="EMA_50",
157+
second_column="EMA_200",
158+
result_column="Crossover_EMA"
159+
)
160+
pl_df.show(10)
161+
162+
# Calculate EMA and crossover for Pandas DataFrame
163+
pd_df = ema(pd_df, source_column="Close", period=200, result_column="EMA_200")
164+
pl_df = ema(pd_df, source_column="Close", period=50, result_column="EMA_50")
165+
pd_df = crossover(
166+
pd_df,
167+
first_column="EMA_50",
168+
second_column="EMA_200",
169+
result_column="Crossover_EMA"
170+
)
171+
pd_df.tail(10)
172+
```
173+
174+
![CROSSOVER](https://github.com/coding-kitties/PyIndicators/blob/main/static/images/indicators/crossover.png)
175+
132176
#### Is Crossover
133177

134178
```python

pyindicators/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1-
from .indicators import sma, rsi, is_crossover, crossunder, ema, wilders_rsi
1+
from .indicators import sma, rsi, is_crossover, crossunder, ema, wilders_rsi, \
2+
crossover, is_crossover
23

34
__all__ = [
45
'sma',
56
'is_crossover',
67
'crossunder',
8+
'crossover',
9+
'is_crossover',
710
'ema',
811
'rsi',
912
"wilders_rsi"

pyindicators/indicators/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
from .simple_moving_average import sma
2-
from .crossover import is_crossover
2+
from .crossover import is_crossover, crossover
33
from .crossunder import crossunder
44
from .exponential_moving_average import ema
55
from .rsi import rsi, wilders_rsi
66

77
__all__ = [
88
'sma',
99
'is_crossover',
10+
"crossover",
1011
'crossunder',
1112
'ema',
1213
'rsi',

pyindicators/indicators/crossover.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,65 @@
11
from typing import Union
2+
23
from pandas import DataFrame as PdDataFrame
34
from polars import DataFrame as PlDataFrame
5+
import polars as pl
6+
7+
8+
def crossover(
9+
data: Union[PdDataFrame, PlDataFrame],
10+
first_column: str,
11+
second_column: str,
12+
result_column = "crossover",
13+
data_points: int = None,
14+
strict: bool = True,
15+
) -> Union[PdDataFrame, PlDataFrame]:
16+
"""
17+
Identifies crossover points where `first_column` crosses above or below `second_column`.
18+
19+
Args:
20+
data: Pandas or Polars DataFrame
21+
first_column: Name of the first column
22+
second_column: Name of the second column
23+
result_column (optional): Name of the column to
24+
store the crossover points
25+
data_points (optional): Number of recent rows to consider (optional)
26+
strict (optional): If True, requires exact crossovers; otherwise,
27+
detects when one surpasses the other.
28+
29+
Returns:
30+
A DataFrame with crossover points marked.
31+
"""
32+
33+
# Restrict data to the last `data_points` rows if specified
34+
if data_points is not None:
35+
data = data.tail(data_points) if isinstance(data, PdDataFrame) else data.slice(-data_points)
36+
37+
# Pandas Implementation
38+
if isinstance(data, PdDataFrame):
39+
col1, col2 = data[first_column], data[second_column]
40+
prev_col1, prev_col2 = col1.shift(1), col2.shift(1)
41+
42+
if strict:
43+
crossover_mask = ((prev_col1 < prev_col2) & (col1 > col2)) | ((prev_col1 > prev_col2) & (col1 < col2))
44+
else:
45+
crossover_mask = (col1 > col2) | (col1 < col2)
46+
47+
data[result_column] = crossover_mask.astype(int)
48+
49+
# Polars Implementation
50+
elif isinstance(data, PlDataFrame):
51+
col1, col2 = data[first_column], data[second_column]
52+
prev_col1, prev_col2 = col1.shift(1), col2.shift(1)
53+
54+
if strict:
55+
crossover_mask = ((prev_col1 < prev_col2) & (col1 > col2)) | ((prev_col1 > prev_col2) & (col1 < col2))
56+
else:
57+
crossover_mask = (col1 > col2) | (col1 < col2)
58+
59+
# Convert boolean mask to 1s and 0s
60+
data = data.with_columns(pl.when(crossover_mask).then(1).otherwise(0).alias(result_column))
61+
62+
return data
463

564

665
def is_crossover(
59.3 KB
Loading

0 commit comments

Comments
 (0)