pine-ta 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pine_ta-0.1.0/LICENSE +21 -0
- pine_ta-0.1.0/PKG-INFO +65 -0
- pine_ta-0.1.0/README.md +39 -0
- pine_ta-0.1.0/pine_ta/__init__.py +523 -0
- pine_ta-0.1.0/pine_ta.egg-info/PKG-INFO +65 -0
- pine_ta-0.1.0/pine_ta.egg-info/SOURCES.txt +10 -0
- pine_ta-0.1.0/pine_ta.egg-info/dependency_links.txt +1 -0
- pine_ta-0.1.0/pine_ta.egg-info/requires.txt +2 -0
- pine_ta-0.1.0/pine_ta.egg-info/top_level.txt +1 -0
- pine_ta-0.1.0/pyproject.toml +3 -0
- pine_ta-0.1.0/setup.cfg +4 -0
- pine_ta-0.1.0/setup.py +20 -0
pine_ta-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Huzaifa Zahoor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
pine_ta-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pine_ta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure pandas/numpy technical indicators inspired by Pine Script
|
|
5
|
+
Home-page: https://github.com/huzaifazahoor/pine_ta
|
|
6
|
+
Author: Huzaifa Zahoor
|
|
7
|
+
Author-email: huzaifazahoor654@email.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# pine_ta
|
|
28
|
+
|
|
29
|
+
**pine_ta** is a pure `pandas` and `numpy` implementation of popular technical indicators, inspired by Pine Script. No C extensions. No TA-Lib. Built for flexibility and easy integration with any DataFrame.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- RSI, MACD, ADX, ATR, Bollinger Bands
|
|
34
|
+
- Stochastic, CCI, MFI, OBV, ROC, and more
|
|
35
|
+
- Matching Pine Script behavior (RMA, EMA, WMA)
|
|
36
|
+
- Single class API: `TechnicalIndicators`
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install pine_ta
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
*(coming soon on PyPI)*
|
|
45
|
+
|
|
46
|
+
## Example Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import pandas as pd
|
|
50
|
+
from pine_ta import TechnicalIndicators
|
|
51
|
+
|
|
52
|
+
# Assume df is a DataFrame with 'open', 'high', 'low', 'close', 'volume'
|
|
53
|
+
df['rsi'] = TechnicalIndicators.rsi(df)
|
|
54
|
+
df['macd'], df['signal'], df['hist'] = TechnicalIndicators.macd(df)
|
|
55
|
+
df['adx'] = TechnicalIndicators.adx(df)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- pandas
|
|
61
|
+
- numpy
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
pine_ta-0.1.0/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pine_ta
|
|
2
|
+
|
|
3
|
+
**pine_ta** is a pure `pandas` and `numpy` implementation of popular technical indicators, inspired by Pine Script. No C extensions. No TA-Lib. Built for flexibility and easy integration with any DataFrame.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- RSI, MACD, ADX, ATR, Bollinger Bands
|
|
8
|
+
- Stochastic, CCI, MFI, OBV, ROC, and more
|
|
9
|
+
- Matching Pine Script behavior (RMA, EMA, WMA)
|
|
10
|
+
- Single class API: `TechnicalIndicators`
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
pip install pine_ta
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
*(coming soon on PyPI)*
|
|
19
|
+
|
|
20
|
+
## Example Usage
|
|
21
|
+
|
|
22
|
+
```python
|
|
23
|
+
import pandas as pd
|
|
24
|
+
from pine_ta import TechnicalIndicators
|
|
25
|
+
|
|
26
|
+
# Assume df is a DataFrame with 'open', 'high', 'low', 'close', 'volume'
|
|
27
|
+
df['rsi'] = TechnicalIndicators.rsi(df)
|
|
28
|
+
df['macd'], df['signal'], df['hist'] = TechnicalIndicators.macd(df)
|
|
29
|
+
df['adx'] = TechnicalIndicators.adx(df)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Requirements
|
|
33
|
+
|
|
34
|
+
- pandas
|
|
35
|
+
- numpy
|
|
36
|
+
|
|
37
|
+
## License
|
|
38
|
+
|
|
39
|
+
MIT
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TechnicalIndicators:
|
|
6
|
+
"""Calculate technical indicators without external dependencies."""
|
|
7
|
+
|
|
8
|
+
@staticmethod
|
|
9
|
+
def rsi(df, period=14):
|
|
10
|
+
"""Calculate RSI (Relative Strength Index) for a DataFrame."""
|
|
11
|
+
# Calculate price changes
|
|
12
|
+
delta = df["close"].diff()
|
|
13
|
+
|
|
14
|
+
# Separate gains and losses
|
|
15
|
+
gains = delta.where(delta > 0, 0)
|
|
16
|
+
losses = -delta.where(delta < 0, 0)
|
|
17
|
+
|
|
18
|
+
# Calculate RMA (Running Moving Average) using EWM
|
|
19
|
+
# Pine Script's RMA is equivalent to EWM with alpha = 1/period
|
|
20
|
+
avg_gains = gains.ewm(alpha=1 / period, adjust=False).mean()
|
|
21
|
+
avg_losses = losses.ewm(alpha=1 / period, adjust=False).mean()
|
|
22
|
+
|
|
23
|
+
# Calculate RS and RSI
|
|
24
|
+
rs = avg_gains / avg_losses
|
|
25
|
+
rsi = 100 - (100 / (1 + rs))
|
|
26
|
+
|
|
27
|
+
# Handle edge cases
|
|
28
|
+
rsi = rsi.fillna(0) # When avg_losses is 0, RSI should be 100
|
|
29
|
+
rsi = rsi.replace([float("inf"), -float("inf")], 100)
|
|
30
|
+
|
|
31
|
+
return rsi
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def ema(src, length):
|
|
35
|
+
"""Calculate EMA using Pine Script's formula."""
|
|
36
|
+
alpha = 2 / (length + 1)
|
|
37
|
+
ema = src.copy()
|
|
38
|
+
|
|
39
|
+
for i in range(1, len(src)):
|
|
40
|
+
if pd.isna(ema.iloc[i - 1]):
|
|
41
|
+
ema.iloc[i] = src.iloc[i]
|
|
42
|
+
else:
|
|
43
|
+
ema.iloc[i] = alpha * src.iloc[i] + (1 - alpha) * ema.iloc[i - 1]
|
|
44
|
+
|
|
45
|
+
return ema
|
|
46
|
+
|
|
47
|
+
@staticmethod
|
|
48
|
+
def macd(
|
|
49
|
+
df,
|
|
50
|
+
fast_length=12,
|
|
51
|
+
slow_length=26,
|
|
52
|
+
signal_length=9,
|
|
53
|
+
ma_type="EMA",
|
|
54
|
+
signal_type="EMA",
|
|
55
|
+
):
|
|
56
|
+
"""Calculate MACD with correct EMA implementation."""
|
|
57
|
+
src = df["close"]
|
|
58
|
+
|
|
59
|
+
if ma_type == "SMA":
|
|
60
|
+
fast_ma = src.rolling(window=fast_length).mean()
|
|
61
|
+
slow_ma = src.rolling(window=slow_length).mean()
|
|
62
|
+
else: # EMA
|
|
63
|
+
fast_ma = TechnicalIndicators.ema(src, fast_length)
|
|
64
|
+
slow_ma = TechnicalIndicators.ema(src, slow_length)
|
|
65
|
+
|
|
66
|
+
macd = fast_ma - slow_ma
|
|
67
|
+
|
|
68
|
+
if signal_type == "SMA":
|
|
69
|
+
signal = macd.rolling(window=signal_length).mean()
|
|
70
|
+
else: # EMA
|
|
71
|
+
signal = TechnicalIndicators.ema(macd, signal_length)
|
|
72
|
+
|
|
73
|
+
histogram = macd - signal
|
|
74
|
+
|
|
75
|
+
return macd, signal, histogram
|
|
76
|
+
|
|
77
|
+
@staticmethod
|
|
78
|
+
def rma(src, length):
|
|
79
|
+
"""Calculate RMA (Running Moving Average) used in RSI and ADX."""
|
|
80
|
+
alpha = 1 / length
|
|
81
|
+
rma = src.copy()
|
|
82
|
+
|
|
83
|
+
# First non-NaN value uses SMA
|
|
84
|
+
first_valid_idx = src.first_valid_index()
|
|
85
|
+
if first_valid_idx is not None:
|
|
86
|
+
# Convert to integer position
|
|
87
|
+
first_valid_pos = src.index.get_loc(first_valid_idx)
|
|
88
|
+
end_pos = min(first_valid_pos + length, len(src))
|
|
89
|
+
|
|
90
|
+
# Calculate initial SMA
|
|
91
|
+
rma.iloc[first_valid_pos:end_pos] = (
|
|
92
|
+
src.iloc[first_valid_pos:end_pos]
|
|
93
|
+
.rolling(window=length, min_periods=1)
|
|
94
|
+
.mean()
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Apply RMA formula for subsequent values
|
|
98
|
+
for i in range(end_pos, len(src)):
|
|
99
|
+
if pd.notna(rma.iloc[i - 1]) and pd.notna(src.iloc[i]):
|
|
100
|
+
rma.iloc[i] = alpha * src.iloc[i] + (1 - alpha) * rma.iloc[i - 1]
|
|
101
|
+
|
|
102
|
+
return rma
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def adx(df, dilen=14, adxlen=14):
|
|
106
|
+
"""Calculate ADX (Average Directional Index)."""
|
|
107
|
+
# Calculate directional movement
|
|
108
|
+
high_change = df["high"].diff()
|
|
109
|
+
low_change = -df["low"].diff()
|
|
110
|
+
|
|
111
|
+
# Plus and Minus DM
|
|
112
|
+
plus_dm = pd.Series(0.0, index=df.index)
|
|
113
|
+
minus_dm = pd.Series(0.0, index=df.index)
|
|
114
|
+
|
|
115
|
+
mask_plus = (high_change > low_change) & (high_change > 0)
|
|
116
|
+
mask_minus = (low_change > high_change) & (low_change > 0)
|
|
117
|
+
|
|
118
|
+
plus_dm[mask_plus] = high_change[mask_plus]
|
|
119
|
+
minus_dm[mask_minus] = low_change[mask_minus]
|
|
120
|
+
|
|
121
|
+
# True Range
|
|
122
|
+
high_low = df["high"] - df["low"]
|
|
123
|
+
high_close = abs(df["high"] - df["close"].shift(1))
|
|
124
|
+
low_close = abs(df["low"] - df["close"].shift(1))
|
|
125
|
+
|
|
126
|
+
true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
|
127
|
+
atr = TechnicalIndicators.rma(true_range, dilen)
|
|
128
|
+
|
|
129
|
+
# Directional Indicators
|
|
130
|
+
plus_di = 100 * TechnicalIndicators.rma(plus_dm, dilen) / atr
|
|
131
|
+
minus_di = 100 * TechnicalIndicators.rma(minus_dm, dilen) / atr
|
|
132
|
+
|
|
133
|
+
# ADX
|
|
134
|
+
di_sum = plus_di + minus_di
|
|
135
|
+
di_diff = abs(plus_di - minus_di)
|
|
136
|
+
dx = 100 * di_diff / di_sum.replace(0, 1)
|
|
137
|
+
|
|
138
|
+
adx = TechnicalIndicators.rma(dx, adxlen)
|
|
139
|
+
|
|
140
|
+
return adx.fillna(0)
|
|
141
|
+
|
|
142
|
+
@staticmethod
|
|
143
|
+
def ma_envelope_slope(df, period=50, lookback=5):
|
|
144
|
+
"""Calculate the slope of the MA envelope median line."""
|
|
145
|
+
ma = df["close"].rolling(window=period).mean()
|
|
146
|
+
|
|
147
|
+
if ma.notna().sum() < lookback:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
recent_ma = ma.dropna().tail(lookback)
|
|
151
|
+
x = np.arange(len(recent_ma))
|
|
152
|
+
y = recent_ma.values
|
|
153
|
+
|
|
154
|
+
slope = np.polyfit(x, y, 1)[0]
|
|
155
|
+
return (slope / recent_ma.mean()) * 100
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def awesome_oscillator(df, fast_period=5, slow_period=34):
|
|
159
|
+
"""Calculate Awesome Oscillator (AO)."""
|
|
160
|
+
hl2 = (df["high"] + df["low"]) / 2
|
|
161
|
+
ao = (
|
|
162
|
+
hl2.rolling(window=fast_period).mean()
|
|
163
|
+
- hl2.rolling(window=slow_period).mean()
|
|
164
|
+
)
|
|
165
|
+
return ao
|
|
166
|
+
|
|
167
|
+
@staticmethod
|
|
168
|
+
def wma(src, length):
|
|
169
|
+
"""Calculate WMA (Weighted Moving Average)."""
|
|
170
|
+
weights = np.arange(length, 0, -1)
|
|
171
|
+
wma = pd.Series(np.nan, index=src.index)
|
|
172
|
+
|
|
173
|
+
for i in range(length - 1, len(src)):
|
|
174
|
+
window = src.iloc[i - length + 1 : i + 1]
|
|
175
|
+
if window.notna().all():
|
|
176
|
+
wma.iloc[i] = np.sum(window.values * weights) / np.sum(weights)
|
|
177
|
+
|
|
178
|
+
return wma
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def atr(df, length=14, smoothing="RMA"):
|
|
182
|
+
"""Calculate ATR (Average True Range)."""
|
|
183
|
+
# Calculate True Range
|
|
184
|
+
high_low = df["high"] - df["low"]
|
|
185
|
+
high_close = abs(df["high"] - df["close"].shift(1))
|
|
186
|
+
low_close = abs(df["low"] - df["close"].shift(1))
|
|
187
|
+
|
|
188
|
+
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
|
189
|
+
|
|
190
|
+
# Apply smoothing
|
|
191
|
+
if smoothing == "RMA":
|
|
192
|
+
return TechnicalIndicators.rma(tr, length)
|
|
193
|
+
elif smoothing == "SMA":
|
|
194
|
+
return tr.rolling(window=length).mean()
|
|
195
|
+
elif smoothing == "EMA":
|
|
196
|
+
return TechnicalIndicators.ema(tr, length)
|
|
197
|
+
elif smoothing == "WMA":
|
|
198
|
+
return TechnicalIndicators.wma(tr, length)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def vwma(src, volume, length):
|
|
202
|
+
"""Calculate VWMA (Volume Weighted Moving Average)."""
|
|
203
|
+
return (src * volume).rolling(window=length).sum() / volume.rolling(
|
|
204
|
+
window=length
|
|
205
|
+
).sum()
|
|
206
|
+
|
|
207
|
+
@staticmethod
|
|
208
|
+
def stdev(src, length, biased=True):
|
|
209
|
+
"""Calculate standard deviation."""
|
|
210
|
+
if biased:
|
|
211
|
+
return src.rolling(window=length).std(ddof=0)
|
|
212
|
+
else:
|
|
213
|
+
return src.rolling(window=length).std(ddof=1)
|
|
214
|
+
|
|
215
|
+
@staticmethod
|
|
216
|
+
def bollinger_bands(df, length=20, mult=2.0, ma_type="SMA", src_col="close"):
|
|
217
|
+
"""Calculate Bollinger Bands."""
|
|
218
|
+
src = df[src_col]
|
|
219
|
+
|
|
220
|
+
# Calculate basis MA
|
|
221
|
+
if ma_type == "SMA":
|
|
222
|
+
basis = src.rolling(window=length).mean()
|
|
223
|
+
elif ma_type == "EMA":
|
|
224
|
+
basis = TechnicalIndicators.ema(src, length)
|
|
225
|
+
elif ma_type == "SMMA (RMA)" or ma_type == "RMA":
|
|
226
|
+
basis = TechnicalIndicators.rma(src, length)
|
|
227
|
+
elif ma_type == "WMA":
|
|
228
|
+
basis = TechnicalIndicators.wma(src, length)
|
|
229
|
+
elif ma_type == "VWMA":
|
|
230
|
+
basis = TechnicalIndicators.vwma(src, df["volume"], length)
|
|
231
|
+
|
|
232
|
+
# Calculate bands
|
|
233
|
+
dev = mult * TechnicalIndicators.stdev(src, length)
|
|
234
|
+
upper = basis + dev
|
|
235
|
+
lower = basis - dev
|
|
236
|
+
|
|
237
|
+
return basis, upper, lower
|
|
238
|
+
|
|
239
|
+
@staticmethod
|
|
240
|
+
def keltner_channels(
|
|
241
|
+
df,
|
|
242
|
+
length=20,
|
|
243
|
+
mult=2.0,
|
|
244
|
+
src_col="close",
|
|
245
|
+
use_exp=True,
|
|
246
|
+
bands_style="Average True Range",
|
|
247
|
+
atr_length=10,
|
|
248
|
+
):
|
|
249
|
+
"""Calculate Keltner Channels."""
|
|
250
|
+
src = df[src_col]
|
|
251
|
+
|
|
252
|
+
# Calculate MA
|
|
253
|
+
if use_exp:
|
|
254
|
+
ma = TechnicalIndicators.ema(src, length)
|
|
255
|
+
else:
|
|
256
|
+
ma = src.rolling(window=length).mean()
|
|
257
|
+
|
|
258
|
+
# Calculate range
|
|
259
|
+
if bands_style == "True Range":
|
|
260
|
+
rangema = TechnicalIndicators.true_range(df)
|
|
261
|
+
elif bands_style == "Average True Range":
|
|
262
|
+
rangema = TechnicalIndicators.atr(df, length=atr_length, smoothing="RMA")
|
|
263
|
+
else: # "Range"
|
|
264
|
+
rangema = TechnicalIndicators.rma(df["high"] - df["low"], length)
|
|
265
|
+
|
|
266
|
+
# Calculate bands
|
|
267
|
+
upper = ma + rangema * mult
|
|
268
|
+
lower = ma - rangema * mult
|
|
269
|
+
|
|
270
|
+
return ma, upper, lower
|
|
271
|
+
|
|
272
|
+
@staticmethod
|
|
273
|
+
def true_range(df):
|
|
274
|
+
"""Calculate True Range."""
|
|
275
|
+
high_low = df["high"] - df["low"]
|
|
276
|
+
high_close = abs(df["high"] - df["close"].shift(1))
|
|
277
|
+
low_close = abs(df["low"] - df["close"].shift(1))
|
|
278
|
+
|
|
279
|
+
tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
|
|
280
|
+
return tr
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def mean_deviation(src, length):
|
|
284
|
+
"""Calculate mean deviation (ta.dev in Pine Script)."""
|
|
285
|
+
return src.rolling(window=length).apply(
|
|
286
|
+
lambda x: np.mean(np.abs(x - np.mean(x))), raw=True
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
@staticmethod
|
|
290
|
+
def cci(df, length=20, src_type="hlc3"):
|
|
291
|
+
"""Calculate Commodity Channel Index."""
|
|
292
|
+
if src_type == "hlc3":
|
|
293
|
+
src = (df["high"] + df["low"] + df["close"]) / 3
|
|
294
|
+
else:
|
|
295
|
+
src = df[src_type]
|
|
296
|
+
|
|
297
|
+
sma = src.rolling(window=length).mean()
|
|
298
|
+
mad = TechnicalIndicators.mean_deviation(src, length)
|
|
299
|
+
cci = (src - sma) / (0.015 * mad)
|
|
300
|
+
|
|
301
|
+
return cci
|
|
302
|
+
|
|
303
|
+
@staticmethod
|
|
304
|
+
def smi(df, length_k=10, length_d=3, length_ema=3):
|
|
305
|
+
"""Calculate Stochastic Momentum Index."""
|
|
306
|
+
# Calculate highest high and lowest low
|
|
307
|
+
highest_high = df["high"].rolling(window=length_k).max()
|
|
308
|
+
lowest_low = df["low"].rolling(window=length_k).min()
|
|
309
|
+
|
|
310
|
+
# Calculate ranges
|
|
311
|
+
highest_lowest_range = highest_high - lowest_low
|
|
312
|
+
relative_range = df["close"] - (highest_high + lowest_low) / 2
|
|
313
|
+
|
|
314
|
+
# Double EMA smoothing
|
|
315
|
+
relative_ema1 = TechnicalIndicators.ema(relative_range, length_d)
|
|
316
|
+
relative_ema2 = TechnicalIndicators.ema(relative_ema1, length_d)
|
|
317
|
+
|
|
318
|
+
range_ema1 = TechnicalIndicators.ema(highest_lowest_range, length_d)
|
|
319
|
+
range_ema2 = TechnicalIndicators.ema(range_ema1, length_d)
|
|
320
|
+
|
|
321
|
+
# Calculate SMI
|
|
322
|
+
smi = 200 * (relative_ema2 / range_ema2)
|
|
323
|
+
|
|
324
|
+
# SMI-based EMA
|
|
325
|
+
smi_ema = TechnicalIndicators.ema(smi, length_ema)
|
|
326
|
+
|
|
327
|
+
return smi, smi_ema
|
|
328
|
+
|
|
329
|
+
@staticmethod
|
|
330
|
+
def williams_percent_range(df, length=14, src_col="close"):
|
|
331
|
+
"""Calculate Williams %R."""
|
|
332
|
+
src = df[src_col]
|
|
333
|
+
highest = df["high"].rolling(window=length).max()
|
|
334
|
+
lowest = df["low"].rolling(window=length).min()
|
|
335
|
+
|
|
336
|
+
williams_r = 100 * (src - highest) / (highest - lowest)
|
|
337
|
+
return williams_r
|
|
338
|
+
|
|
339
|
+
@staticmethod
|
|
340
|
+
def stochastic(df, fastk_period=14, slowk_period=3, slowd_period=3):
|
|
341
|
+
"""Calculate Stochastic Oscillator (%K and %D)."""
|
|
342
|
+
lowest_low = df["low"].rolling(window=fastk_period).min()
|
|
343
|
+
highest_high = df["high"].rolling(window=fastk_period).max()
|
|
344
|
+
|
|
345
|
+
# Fast %K
|
|
346
|
+
fast_k = 100 * ((df["close"] - lowest_low) / (highest_high - lowest_low))
|
|
347
|
+
|
|
348
|
+
# Slow %K (smoothed Fast %K)
|
|
349
|
+
slow_k = fast_k.rolling(window=slowk_period).mean()
|
|
350
|
+
|
|
351
|
+
# Slow %D (smoothed Slow %K)
|
|
352
|
+
slow_d = slow_k.rolling(window=slowd_period).mean()
|
|
353
|
+
|
|
354
|
+
return slow_k, slow_d
|
|
355
|
+
|
|
356
|
+
@staticmethod
|
|
357
|
+
def roc(df, length=14, src_col="close"):
|
|
358
|
+
"""Calculate Rate of Change (ROC)."""
|
|
359
|
+
src = df[src_col]
|
|
360
|
+
src_shifted = src.shift(length)
|
|
361
|
+
|
|
362
|
+
# ROC = 100 * (current - previous) / previous
|
|
363
|
+
roc = 100 * (src - src_shifted) / src_shifted
|
|
364
|
+
|
|
365
|
+
# Handle division by zero
|
|
366
|
+
roc = roc.replace([float("inf"), -float("inf")], np.nan)
|
|
367
|
+
|
|
368
|
+
return roc
|
|
369
|
+
|
|
370
|
+
@staticmethod
|
|
371
|
+
def momentum(df, length=10, src_col="close"):
|
|
372
|
+
"""Calculate Momentum indicator."""
|
|
373
|
+
src = df[src_col]
|
|
374
|
+
|
|
375
|
+
# Momentum = current price - price n periods ago
|
|
376
|
+
mom = src - src.shift(length)
|
|
377
|
+
|
|
378
|
+
return mom
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def rvi(df, length=10, ema_length=14):
|
|
382
|
+
"""Calculate Relative Volatility Index (RVI)."""
|
|
383
|
+
src = df["close"]
|
|
384
|
+
|
|
385
|
+
# Calculate standard deviation
|
|
386
|
+
stddev = TechnicalIndicators.stdev(src, length)
|
|
387
|
+
|
|
388
|
+
# Calculate price change
|
|
389
|
+
change = src.diff()
|
|
390
|
+
|
|
391
|
+
# Upper: EMA of stddev when price goes up (change > 0)
|
|
392
|
+
upper_values = stddev.where(change > 0, 0)
|
|
393
|
+
upper = TechnicalIndicators.ema(upper_values, ema_length)
|
|
394
|
+
|
|
395
|
+
# Lower: EMA of stddev when price goes down (change <= 0)
|
|
396
|
+
lower_values = stddev.where(change <= 0, 0)
|
|
397
|
+
lower = TechnicalIndicators.ema(lower_values, ema_length)
|
|
398
|
+
|
|
399
|
+
# Calculate RVI
|
|
400
|
+
rvi = 100 * upper / (upper + lower)
|
|
401
|
+
|
|
402
|
+
# Handle division by zero
|
|
403
|
+
rvi = rvi.replace([float("inf"), -float("inf")], np.nan)
|
|
404
|
+
rvi = rvi.fillna(50) # When both upper and lower are 0
|
|
405
|
+
|
|
406
|
+
return rvi
|
|
407
|
+
|
|
408
|
+
@staticmethod
|
|
409
|
+
def obv(df):
|
|
410
|
+
"""Calculate On-Balance Volume (OBV)."""
|
|
411
|
+
src = df["close"]
|
|
412
|
+
volume = df["volume"]
|
|
413
|
+
|
|
414
|
+
# Calculate price change
|
|
415
|
+
change = src.diff()
|
|
416
|
+
|
|
417
|
+
# Sign of change: 1 if positive, -1 if negative, 0 if no change
|
|
418
|
+
sign = np.sign(change)
|
|
419
|
+
|
|
420
|
+
# OBV = cumulative sum of (sign * volume)
|
|
421
|
+
obv = (sign * volume).cumsum()
|
|
422
|
+
|
|
423
|
+
# Handle NaN in first row
|
|
424
|
+
obv = obv.fillna(0)
|
|
425
|
+
|
|
426
|
+
return obv
|
|
427
|
+
|
|
428
|
+
@staticmethod
|
|
429
|
+
def mfi(df, length=14):
|
|
430
|
+
"""Calculate Money Flow Index (MFI)."""
|
|
431
|
+
hlc3 = (df["high"] + df["low"] + df["close"]) / 3
|
|
432
|
+
volume = df["volume"]
|
|
433
|
+
|
|
434
|
+
# Calculate price change
|
|
435
|
+
change = hlc3.diff()
|
|
436
|
+
|
|
437
|
+
# Positive money flow: volume * hlc3 when price goes up
|
|
438
|
+
positive_mf = (volume * hlc3).where(change > 0, 0)
|
|
439
|
+
|
|
440
|
+
# Negative money flow: volume * hlc3 when price goes down
|
|
441
|
+
negative_mf = (volume * hlc3).where(change < 0, 0)
|
|
442
|
+
|
|
443
|
+
# Sum over period
|
|
444
|
+
upper = positive_mf.rolling(window=length).sum()
|
|
445
|
+
lower = negative_mf.rolling(window=length).sum()
|
|
446
|
+
|
|
447
|
+
# Calculate MFI
|
|
448
|
+
mfi = 100 - (100 / (1 + upper / lower))
|
|
449
|
+
|
|
450
|
+
# Handle division by zero
|
|
451
|
+
mfi = mfi.replace([float("inf"), -float("inf")], 100)
|
|
452
|
+
mfi = mfi.fillna(50)
|
|
453
|
+
|
|
454
|
+
return mfi
|
|
455
|
+
|
|
456
|
+
@staticmethod
|
|
457
|
+
def performance_index(df, benchmark_df, period=21):
|
|
458
|
+
"""
|
|
459
|
+
Calculate Performance Index against benchmark (S&P 500).
|
|
460
|
+
PI = (Stock Price / Benchmark Price) * (Benchmark MA / Stock MA)
|
|
461
|
+
"""
|
|
462
|
+
if benchmark_df.empty:
|
|
463
|
+
return pd.Series(np.nan, index=df.index)
|
|
464
|
+
# Ensure date columns are datetime
|
|
465
|
+
df = df.copy()
|
|
466
|
+
benchmark_df = benchmark_df.copy()
|
|
467
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
468
|
+
benchmark_df["date"] = pd.to_datetime(benchmark_df["date"])
|
|
469
|
+
|
|
470
|
+
# Merge on date
|
|
471
|
+
merged = pd.merge(
|
|
472
|
+
df[["date", "close", "symbol"]],
|
|
473
|
+
benchmark_df[["date", "close"]],
|
|
474
|
+
on="date",
|
|
475
|
+
suffixes=("", "_benchmark"),
|
|
476
|
+
how="left",
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# Calculate moving averages
|
|
480
|
+
merged["stock_ma"] = merged["close"].rolling(window=period).mean()
|
|
481
|
+
merged["benchmark_ma"] = merged["close_benchmark"].rolling(window=period).mean()
|
|
482
|
+
|
|
483
|
+
# Calculate Performance Index
|
|
484
|
+
merged["pi"] = (merged["close"] / merged["close_benchmark"]) * (
|
|
485
|
+
merged["benchmark_ma"] / merged["stock_ma"]
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
# Handle division by zero
|
|
489
|
+
merged["pi"] = merged["pi"].replace([float("inf"), -float("inf")], np.nan)
|
|
490
|
+
|
|
491
|
+
return merged["pi"]
|
|
492
|
+
|
|
493
|
+
@staticmethod
|
|
494
|
+
def crs(df, benchmark_df, period=21):
|
|
495
|
+
"""
|
|
496
|
+
Calculate Comparative Relative Strength (CRS) vs benchmark.
|
|
497
|
+
CRS = Stock Price / Benchmark Price
|
|
498
|
+
"""
|
|
499
|
+
if benchmark_df.empty:
|
|
500
|
+
return pd.Series(np.nan, index=df.index)
|
|
501
|
+
|
|
502
|
+
# Ensure date columns are datetime
|
|
503
|
+
df = df.copy()
|
|
504
|
+
benchmark_df = benchmark_df.copy()
|
|
505
|
+
df["date"] = pd.to_datetime(df["date"])
|
|
506
|
+
benchmark_df["date"] = pd.to_datetime(benchmark_df["date"])
|
|
507
|
+
|
|
508
|
+
# Merge on date
|
|
509
|
+
merged = pd.merge(
|
|
510
|
+
df[["date", "close", "symbol"]],
|
|
511
|
+
benchmark_df[["date", "close"]],
|
|
512
|
+
on="date",
|
|
513
|
+
suffixes=("", "_benchmark"),
|
|
514
|
+
how="left",
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Calculate CRS
|
|
518
|
+
crs = merged["close"] / merged["close_benchmark"]
|
|
519
|
+
|
|
520
|
+
# Handle division by zero
|
|
521
|
+
crs = crs.replace([float("inf"), -float("inf")], np.nan)
|
|
522
|
+
|
|
523
|
+
return crs
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pine_ta
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pure pandas/numpy technical indicators inspired by Pine Script
|
|
5
|
+
Home-page: https://github.com/huzaifazahoor/pine_ta
|
|
6
|
+
Author: Huzaifa Zahoor
|
|
7
|
+
Author-email: huzaifazahoor654@email.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.7
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: pandas
|
|
15
|
+
Requires-Dist: numpy
|
|
16
|
+
Dynamic: author
|
|
17
|
+
Dynamic: author-email
|
|
18
|
+
Dynamic: classifier
|
|
19
|
+
Dynamic: description
|
|
20
|
+
Dynamic: description-content-type
|
|
21
|
+
Dynamic: home-page
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
26
|
+
|
|
27
|
+
# pine_ta
|
|
28
|
+
|
|
29
|
+
**pine_ta** is a pure `pandas` and `numpy` implementation of popular technical indicators, inspired by Pine Script. No C extensions. No TA-Lib. Built for flexibility and easy integration with any DataFrame.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- RSI, MACD, ADX, ATR, Bollinger Bands
|
|
34
|
+
- Stochastic, CCI, MFI, OBV, ROC, and more
|
|
35
|
+
- Matching Pine Script behavior (RMA, EMA, WMA)
|
|
36
|
+
- Single class API: `TechnicalIndicators`
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
pip install pine_ta
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
*(coming soon on PyPI)*
|
|
45
|
+
|
|
46
|
+
## Example Usage
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
import pandas as pd
|
|
50
|
+
from pine_ta import TechnicalIndicators
|
|
51
|
+
|
|
52
|
+
# Assume df is a DataFrame with 'open', 'high', 'low', 'close', 'volume'
|
|
53
|
+
df['rsi'] = TechnicalIndicators.rsi(df)
|
|
54
|
+
df['macd'], df['signal'], df['hist'] = TechnicalIndicators.macd(df)
|
|
55
|
+
df['adx'] = TechnicalIndicators.adx(df)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Requirements
|
|
59
|
+
|
|
60
|
+
- pandas
|
|
61
|
+
- numpy
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pine_ta
|
pine_ta-0.1.0/setup.cfg
ADDED
pine_ta-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="pine_ta",
|
|
5
|
+
version="0.1.0",
|
|
6
|
+
packages=find_packages(),
|
|
7
|
+
install_requires=["pandas", "numpy"],
|
|
8
|
+
author="Huzaifa Zahoor",
|
|
9
|
+
author_email="huzaifazahoor654@email.com",
|
|
10
|
+
description="Pure pandas/numpy technical indicators inspired by Pine Script",
|
|
11
|
+
long_description=open("README.md").read(),
|
|
12
|
+
long_description_content_type="text/markdown",
|
|
13
|
+
url="https://github.com/huzaifazahoor/pine_ta",
|
|
14
|
+
classifiers=[
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
],
|
|
19
|
+
python_requires=">=3.7",
|
|
20
|
+
)
|