Skip to content

Commit 66077dc

Browse files
committed
Add EMA and SMA images
1 parent d7f0d6b commit 66077dc

File tree

12 files changed

+2179
-40
lines changed

12 files changed

+2179
-40
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,4 +142,6 @@ lib/
142142
bumpversion.egg-info/
143143
*.sqlite3
144144

145-
.vscode/
145+
.vscode/
146+
147+
test.ipynb

README.md

Lines changed: 65 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ PyIndicators is a powerful and user-friendly Python library for technical analys
44

55
## Features
66

7-
* Native Python implementation
7+
* Native Python implementation, no external dependencies needed except for Polars or Pandas
88
* Dataframe first approach, with support for both pandas dataframes and polars dataframes
9+
* Trend indicators
10+
* [Simple Moving Average (SMA)](#simple-moving-average-sma)
11+
* [Exponential Moving Average (EMA)](#exponential-moving-average-ema)
12+
* Momentum indicators
913

1014
## Indicators
1115

@@ -14,48 +18,53 @@ PyIndicators is a powerful and user-friendly Python library for technical analys
1418
#### Simple Moving Average (SMA)
1519

1620
```python
17-
from polars import DataFrame as plDataFrame
18-
from pandas import DataFrame as pdDataFrame
21+
from investing_algorithm_framework import CSVOHLCVMarketDataSource
1922

2023
from pyindicators import sma
2124

22-
# Polars DataFrame
23-
pl_df = plDataFrame({"close": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
25+
# For this example the investing algorithm framework is used for dataframe creation,
26+
csv_path = "./tests/test_data/OHLCV_BTC-EUR_BINANCE_15m_2023-12-01:00:00_2023-12-25:00:00.csv"
27+
data_source = CSVOHLCVMarketDataSource(csv_file_path=csv_path)
2428

25-
# Pandas DataFrame
26-
pd_df = pdDataFrame({"close": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
29+
pl_df = data_source.get_data()
30+
pd_df = data_source.get_data(pandas=True)
2731

2832
# Calculate SMA for Polars DataFrame
29-
pl_df = sma(pl_df, "close", 3)
33+
pl_df = sma(pl_df, source_column="Close", period=200, result_column="SMA_200")
3034
pl_df.show(10)
3135

3236
# Calculate SMA for Pandas DataFrame
33-
pd_df = sma(pd_df, "close", 3)
34-
print(pd_df)
37+
pd_df = sma(pd_df, source_column="Close", period=200, result_column="SMA_200")
38+
pd_df.tail(10)
3539
```
3640

41+
![SMA](./static/images/indicators/sma.png)
42+
3743
#### Exponential Moving Average (EMA)
3844

3945
```python
40-
from polars import DataFrame as plDataFrame
41-
from pandas import DataFrame as pdDataFrame
46+
from investing_algorithm_framework import CSVOHLCVMarketDataSource
4247

4348
from pyindicators import ema
4449

45-
# Polars DataFrame
46-
pl_df = plDataFrame({"close": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
47-
# Pandas DataFrame
48-
pd_df = pdDataFrame({"close": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]})
50+
# For this example the investing algorithm framework is used for dataframe creation,
51+
csv_path = "./tests/test_data/OHLCV_BTC-EUR_BINANCE_15m_2023-12-01:00:00_2023-12-25:00:00.csv"
52+
data_source = CSVOHLCVMarketDataSource(csv_file_path=csv_path)
53+
54+
pl_df = data_source.get_data()
55+
pd_df = data_source.get_data(pandas=True)
4956

50-
# Calculate EMA for Polars DataFrame
51-
pl_df = ema(pl_df, "close", 3)
57+
# Calculate SMA for Polars DataFrame
58+
pl_df = ema(pl_df, source_column="Close", period=200, result_column="EMA_200")
5259
pl_df.show(10)
5360

54-
# Calculate EMA for Pandas DataFrame
55-
pd_df = ema(pd_df, "close", 3)
56-
print(pd_df)
61+
# Calculate SMA for Pandas DataFrame
62+
pd_df = ema(pd_df, source_column="Close", period=200, result_column="EMA_200")
63+
pd_df.tail(10)
5764
```
5865

66+
![EMA](./static/images/indicators/ema.png)
67+
5968
### Momentum Indicators
6069

6170
#### Relative Strength Index (RSI)
@@ -79,3 +88,38 @@ pl_df.show(10)
7988
pd_df = rsi(pd_df, "close", 14)
8089
print(pd_df)
8190
```
91+
92+
### Indicator helpers
93+
94+
#### Is Crossover
95+
96+
```python
97+
from polars import DataFrame as plDataFrame
98+
from pandas import DataFrame as pdDataFrame
99+
100+
from pyindicators import is_crossover
101+
102+
# Polars DataFrame
103+
pl_df = plDataFrame({
104+
"EMA_50": [200, 201, 202, 203, 204, 205, 206, 208, 208, 210],
105+
"EMA_200": [200, 201, 202, 203, 204, 205, 206, 207, 209, 209],
106+
"DateTime": pd.date_range("2021-01-01", periods=10, freq="D")
107+
})
108+
# Pandas DataFrame
109+
pd_df = pdDataFrame({
110+
"EMA_50": [200, 201, 202, 203, 204, 205, 206, 208, 208, 210],
111+
"EMA_200": [200, 201, 202, 203, 204, 205, 206, 207, 209, 209],
112+
"DateTime": pd.date_range("2021-01-01", periods=10, freq="D")
113+
})
114+
115+
if is_crossover(
116+
pl_df, first_column="EMA_50", second_column="EMA_200", data_points=3
117+
):
118+
print("Crossover detected in Polars DataFrame")
119+
120+
121+
if is_crossover(
122+
pd_df, first_column="EMA_50", second_column="EMA_200", data_points=3
123+
):
124+
print("Crossover detected in Pandas DataFrame")
125+
```

poetry.lock

Lines changed: 1838 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyindicators/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
from .indicators import sma, rsi, crossover, crossunder, ema
1+
from .indicators import sma, rsi, is_crossover, crossunder, ema
22

33
__all__ = [
44
'sma',
5-
'crossover',
5+
'is_crossover',
66
'crossunder',
77
'ema',
88
'rsi',

pyindicators/indicators/__init__.py

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

77
__all__ = [
88
'sma',
9-
'crossover',
9+
'is_crossover',
1010
'crossunder',
1111
'ema',
1212
'rsi',
Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,70 @@
1-
import pandas as pd
1+
from typing import Union
2+
from pandas import DataFrame as PdDataFrame
3+
from polars import DataFrame as PlDataFrame
24

35

4-
def crossover(series_a: pd.Series, series_b: pd.Series):
6+
def is_crossover(
7+
data: Union[PdDataFrame, PlDataFrame],
8+
first_column: str,
9+
second_column: str,
10+
data_points: int = None,
11+
strict=True,
12+
) -> bool:
513
"""
6-
Returns a boolean series indicating if series_a has crossed over series_b
14+
Returns a boolean when the first series crosses above the second series at any point or within the last n data points.
15+
16+
Args:
17+
data (Union[PdDataFrame, PlDataFrame]): The input data.
18+
first_column (str): The name of the first series.
19+
second_column (str): The name of the second series.
20+
data_points (int, optional): The number of data points to consider. Defaults to None.
21+
strict (bool, optional): If True, the first series must be strictly greater than the second series. If False, the first series must be greater than or equal to the second series. Defaults to True.
22+
23+
Returns:
24+
bool: Returns True if the first series crosses above the second series at any point or within the last n data points.
725
"""
8-
return (series_a > series_b).astype(int).diff().astype('Int64') == 1
26+
27+
if len(data) < 2:
28+
return False
29+
30+
if data_points is None:
31+
data_points = len(data) - 1
32+
33+
if isinstance(data, PdDataFrame):
34+
35+
# Loop through the data points and check if the first key
36+
# is greater than the second key
37+
for i in range(data_points, 0, -1):
38+
39+
if strict:
40+
if data[first_column].iloc[-(i + 1)] \
41+
< data[second_column].iloc[-(i + 1)] \
42+
and data[first_column].iloc[-i] \
43+
> data[second_column].iloc[-i]:
44+
return True
45+
else:
46+
if data[first_column].iloc[-(i + 1)] \
47+
<= data[second_column].iloc[-(i + 1)] \
48+
and data[first_column].iloc[-i] >= \
49+
data[second_column].iloc[-i]:
50+
return True
51+
52+
else:
53+
# Loop through the data points and check if the first key
54+
# is greater than the second key
55+
for i in range(data_points, 0, -1):
56+
57+
if strict:
58+
if data[first_column][-i - 1] \
59+
< data[second_column][-i - 1] \
60+
and data[first_column][-i] \
61+
> data[second_column][-i]:
62+
return True
63+
else:
64+
if data[first_column][-i - 1] \
65+
<= data[second_column][-i - 1] \
66+
and data[first_column][-i] >= \
67+
data[second_column][-i]:
68+
return True
69+
70+
return False
Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,51 @@
1-
import pandas as pd
1+
from typing import Union
2+
from pandas import DataFrame as PdDataFrame
3+
from polars import DataFrame as PlDataFrame
4+
from pyindicators.exceptions import PyIndicatorException
25

6+
def ema(
7+
data: Union[PdDataFrame, PlDataFrame],
8+
source_column: str,
9+
period: int,
10+
result_column: str = None,
11+
):
12+
"""
13+
Function to calculate the Exponential Moving Average (EMA) of a series.
314
4-
def ema(series: pd.Series, period, adjust=False):
5-
return series.ewm(span=period, adjust=adjust).mean()
15+
Args:
16+
data (Union[PdDataFrame, PlDataFrame]): The input data.
17+
source_column (str): The name of the series.
18+
period (int): The period for the exponential moving average.
19+
result_column (str, optional): The name of the column to store the
20+
exponential moving average. Defaults to None.
621
22+
Returns:
23+
Union[PdDataFrame, PlDataFrame]: Returns a DataFrame with the EMA of the series.
24+
"""
725

26+
if len(data) < period:
27+
raise PyIndicatorException(
28+
"The data must be larger than the period " +
29+
f"{period} to calculate the EMA. The data " +
30+
f"only contains {len(data)} data points."
31+
)
32+
33+
if result_column is None:
34+
result_column = f"EMA_{source_column}_{period}"
35+
36+
if isinstance(data, PdDataFrame):
37+
data[result_column] = data[source_column].ewm(span=period, adjust=False).mean()
38+
else:
39+
# Polars does not have a direct EWM function, so we implement it manually
40+
alpha = 2 / (period + 1)
41+
ema_values = []
42+
ema_prev = data[source_column][0] # Initialize with the first value
43+
44+
for price in data[source_column]:
45+
ema_current = (price * alpha) + (ema_prev * (1 - alpha))
46+
ema_values.append(ema_current)
47+
ema_prev = ema_current
48+
49+
data = data.with_columns(pl.Series(result_column, ema_values))
50+
51+
return data
Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
1-
import pandas as pd
1+
from typing import Union
2+
from pandas import DataFrame as PdDataFrame
3+
from polars import DataFrame as PlDataFrame
4+
from pyindicators.exceptions import PyIndicatorException
25

36

4-
def sma(series: pd.Series, timeperiod=14):
5-
return series.rolling(window=timeperiod).mean()
7+
def sma(
8+
data: Union[PdDataFrame, PlDataFrame],
9+
source_column: str,
10+
period: int,
11+
result_column: str = None,
12+
):
13+
"""
14+
Function to calculate the simple moving average of a series.
15+
16+
Args:
17+
data (Union[PdDataFrame, PlDataFrame]): The input data.
18+
source_column (str): The name of the series.
19+
period (int): The period for the simple moving average.
20+
result_column (str, optional): The name of the column to store the
21+
simple moving average. Defaults to None.
22+
23+
Returns:
24+
Union[PdDataFrame, PlDataFrame]: Returns a DataFrame with the simple moving average of the series.
25+
"""
26+
27+
if len(data) < period:
28+
raise PyIndicatorException(
29+
"The data must be larger than the period " +
30+
f"{period} to calculate the SMA. The data " +
31+
f"only contains {len(data)} data points."
32+
)
33+
34+
if result_column is None:
35+
result_column = f"SMA_{source_column}_{period}"
36+
37+
if isinstance(data, PdDataFrame):
38+
data[result_column] = data[source_column].rolling(window=period).mean()
39+
else:
40+
data = data.with_column(
41+
data[source_column]
42+
.rolling(window=period).mean(),
43+
result_column
44+
)
45+
46+
return data

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
[tool.poetry]
22
name = "pyindicators"
33
version = "0.1.0"
4-
description = "PyIndicators is a powerful Python library designed to simplify technical analysis in financial markets. Whether you're a seasoned quant or a novice trader, PyIndicators provides a user-friendly interface for integrating a wide range of technical indicators into your data analysis and trading strategies."
4+
description = "PyIndicators is a powerful and user-friendly Python library for technical analysis indicators and metrics. Written entirely in Python, it requires no external dependencies, ensuring seamless integration and ease of use."
55
authors = ["Marc van Duyn"]
66
readme = "README.md"
77

88
[tool.poetry.dependencies]
99
python = "^3.9"
1010
pandas = "^2.1.4"
11-
11+
polars = "^0.46.0"
1212

1313
[tool.poetry.group.dev.dependencies]
1414
ta-lib = "^0.4.28"
15-
investing-algorithm-framework = "^2.1.0"
15+
investing-algorithm-framework = "5.0.0"
1616

1717
[build-system]
1818
requires = ["poetry-core"]
1919
build-backend = "poetry.core.masonry.api"
20+
Črtomirova ulica 22, 1000 Ljubljana, Slovenia

static/images/indicators/ema.png

76 KB
Loading

0 commit comments

Comments
 (0)