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 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
@@ -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,10 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.py
5
+ pine_ta/__init__.py
6
+ pine_ta.egg-info/PKG-INFO
7
+ pine_ta.egg-info/SOURCES.txt
8
+ pine_ta.egg-info/dependency_links.txt
9
+ pine_ta.egg-info/requires.txt
10
+ pine_ta.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ pandas
2
+ numpy
@@ -0,0 +1 @@
1
+ pine_ta
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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
+ )