From bc10ef854216c2187e5885ce38d2dda69d55f45b Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Sat, 3 Dec 2022 21:16:13 +0200 Subject: [PATCH 01/11] something like this --- ta/volume.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/ta/volume.py b/ta/volume.py index 7655ed79..3b817cda 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -267,18 +267,31 @@ class VolumePriceTrendIndicator(IndicatorMixin): fillna(bool): if True, fill nan values. """ - def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False): + def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: int|None = None, dropnans:bool = False): self._close = close self._volume = volume - self._fillna = fillna + self._fillna = fillna #This should never be used here like it was before `self._close.shift(1, fill_value=self._close.mean()`. That thing ruins indicator until it's influence will be miserable. + + self._smoothing_factor = smoothing_factor + if not isinstance(self._smoothing_factor, (int, float)): raise TypeError("Smoothing factor must be an integer.")# Float in case of 10. or something like this + else: + if self._smoothing_factor != int(self._smoothing_factor): + print("You have provided smoothing factor as float such that not equal to integer! We will force it to be an integer.") + self._smoothing_factor = int(self._smoothing_factor) + + if self._smoothing_factor <= 1: raise ValueError("Smoothing factor must be bigger than 1.") + + self._dropnans = dropnans + if not isinstance(self._dropnans, bool): raise TypeError("dropnans must be boolean.") + self._run() def _run(self): - vpt = self._volume * ( - (self._close - self._close.shift(1, fill_value=self._close.mean())) - / self._close.shift(1, fill_value=self._close.mean()) - ) - self._vpt = vpt.shift(1, fill_value=vpt.mean()) + vpt + self._vpt = (self._closes.pct_change() * self._volume).cumsum() + if self._smoothing_factor: + min_periods = 0 if self._fillna else self._window + self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() + if self._dropnans: self._vpt = self._vpt.dropna() def volume_price_trend(self) -> pd.Series: """Volume-price trend (VPT) From 1702e7d3922ad1acb2fa419beb726a6d54c8bdf8 Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Sat, 3 Dec 2022 21:47:37 +0200 Subject: [PATCH 02/11] fixed type hinting --- ta/volume.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ta/volume.py b/ta/volume.py index 3b817cda..54189a42 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -8,7 +8,7 @@ import numpy as np import pandas as pd - +from typing import Union from ta.utils import IndicatorMixin, _ema @@ -267,7 +267,7 @@ class VolumePriceTrendIndicator(IndicatorMixin): fillna(bool): if True, fill nan values. """ - def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: int|None = None, dropnans:bool = False): + def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: Union[int,None] = None, dropnans:bool = False): self._close = close self._volume = volume self._fillna = fillna #This should never be used here like it was before `self._close.shift(1, fill_value=self._close.mean()`. That thing ruins indicator until it's influence will be miserable. From cbcc458079420dc2b932cd6d2585d0947d6555b5 Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Sun, 4 Dec 2022 12:21:51 +0200 Subject: [PATCH 03/11] fixed import and logical error --- ta/volume.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ta/volume.py b/ta/volume.py index 54189a42..b70766ff 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -8,7 +8,6 @@ import numpy as np import pandas as pd -from typing import Union from ta.utils import IndicatorMixin, _ema @@ -267,19 +266,20 @@ class VolumePriceTrendIndicator(IndicatorMixin): fillna(bool): if True, fill nan values. """ - def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: Union[int,None] = None, dropnans:bool = False): + def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: int = None, dropnans:bool = False): self._close = close self._volume = volume self._fillna = fillna #This should never be used here like it was before `self._close.shift(1, fill_value=self._close.mean()`. That thing ruins indicator until it's influence will be miserable. self._smoothing_factor = smoothing_factor - if not isinstance(self._smoothing_factor, (int, float)): raise TypeError("Smoothing factor must be an integer.")# Float in case of 10. or something like this - else: - if self._smoothing_factor != int(self._smoothing_factor): - print("You have provided smoothing factor as float such that not equal to integer! We will force it to be an integer.") - self._smoothing_factor = int(self._smoothing_factor) - - if self._smoothing_factor <= 1: raise ValueError("Smoothing factor must be bigger than 1.") + if smoothing_factor is not None: + if not isinstance(self._smoothing_factor, (int, float)): raise TypeError("Smoothing factor must be an integer.")# Float in case of 10. or something like this + else: + if self._smoothing_factor != int(self._smoothing_factor): + print("You have provided smoothing factor as float such that not equal to integer! We will force it to be an integer.") + self._smoothing_factor = int(self._smoothing_factor) + + if self._smoothing_factor <= 1: raise ValueError("Smoothing factor must be bigger than 1.") self._dropnans = dropnans if not isinstance(self._dropnans, bool): raise TypeError("dropnans must be boolean.") @@ -287,7 +287,7 @@ def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, sm self._run() def _run(self): - self._vpt = (self._closes.pct_change() * self._volume).cumsum() + self._vpt = (self._closes.pct_change().fillna(self._close.mean()) * self._volume).cumsum() #.fillna(self._close.mean()) is BAD if self._smoothing_factor: min_periods = 0 if self._fillna else self._window self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() From 2dc646f861951e3676e2e752d88017d135844c44 Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Sun, 4 Dec 2022 12:24:15 +0200 Subject: [PATCH 04/11] import --- ta/volume.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ta/volume.py b/ta/volume.py index b70766ff..4486abfd 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -8,6 +8,7 @@ import numpy as np import pandas as pd + from ta.utils import IndicatorMixin, _ema From 17b8dea361472dca50f345c32212b0999090d092 Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Sun, 4 Dec 2022 12:27:07 +0200 Subject: [PATCH 05/11] mistyping --- ta/volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ta/volume.py b/ta/volume.py index 4486abfd..f4ff7fc2 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -288,7 +288,7 @@ def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, sm self._run() def _run(self): - self._vpt = (self._closes.pct_change().fillna(self._close.mean()) * self._volume).cumsum() #.fillna(self._close.mean()) is BAD + self._vpt = (self._close.pct_change().fillna(self._close.mean()) * self._volume).cumsum() #.fillna(self._close.mean()) is BAD if self._smoothing_factor: min_periods = 0 if self._fillna else self._window self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() From e679c30a70eb83e61128c276814982159825d05b Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:54:18 +0200 Subject: [PATCH 06/11] made changes to vpt and added unit tests --- ta/volume.py | 28 +++++++----------- test/data/cs-vpt.csv | 31 ++++++++++++++++++++ test/unit/volume.py | 67 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 108 insertions(+), 18 deletions(-) create mode 100644 test/data/cs-vpt.csv diff --git a/ta/volume.py b/ta/volume.py index f4ff7fc2..956b804b 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -264,31 +264,21 @@ class VolumePriceTrendIndicator(IndicatorMixin): Args: close(pandas.Series): dataset 'Close' column. volume(pandas.Series): dataset 'Volume' column. - fillna(bool): if True, fill nan values. + fillna(bool)=False: if True, fill nan values. DO NOT RECCOMEND to set it True. + smoothing_factor(int)=None: will smooth default VPT implementation with SMA. + dropnans(bool)=False: drop nans after indicator calculated. """ def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, smoothing_factor: int = None, dropnans:bool = False): self._close = close self._volume = volume - self._fillna = fillna #This should never be used here like it was before `self._close.shift(1, fill_value=self._close.mean()`. That thing ruins indicator until it's influence will be miserable. - + self._fillna = fillna self._smoothing_factor = smoothing_factor - if smoothing_factor is not None: - if not isinstance(self._smoothing_factor, (int, float)): raise TypeError("Smoothing factor must be an integer.")# Float in case of 10. or something like this - else: - if self._smoothing_factor != int(self._smoothing_factor): - print("You have provided smoothing factor as float such that not equal to integer! We will force it to be an integer.") - self._smoothing_factor = int(self._smoothing_factor) - - if self._smoothing_factor <= 1: raise ValueError("Smoothing factor must be bigger than 1.") - self._dropnans = dropnans - if not isinstance(self._dropnans, bool): raise TypeError("dropnans must be boolean.") - self._run() def _run(self): - self._vpt = (self._close.pct_change().fillna(self._close.mean()) * self._volume).cumsum() #.fillna(self._close.mean()) is BAD + self._vpt = (self._close.pct_change() * self._volume).cumsum() if self._smoothing_factor: min_periods = 0 if self._fillna else self._window self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() @@ -622,7 +612,7 @@ def sma_ease_of_movement(high, low, volume, window=14, fillna=False): ).sma_ease_of_movement() -def volume_price_trend(close, volume, fillna=False): +def volume_price_trend(close, volume, fillna=False, smoothing_factor: int=None, dropnans: bool=False): """Volume-price trend (VPT) Is based on a running cumulative volume that adds or substracts a multiple @@ -634,13 +624,15 @@ def volume_price_trend(close, volume, fillna=False): Args: close(pandas.Series): dataset 'Close' column. volume(pandas.Series): dataset 'Volume' column. - fillna(bool): if True, fill nan values. + fillna(bool)=False: if True, fill nan values. DO NOT RECCOMEND to set it True. + smoothing_factor(int)=None: will smooth default VPT implementation with SMA. + dropnans(bool)=False: drop nans after indicator calculated. Returns: pandas.Series: New feature generated. """ return VolumePriceTrendIndicator( - close=close, volume=volume, fillna=fillna + close=close, volume=volume, fillna=fillna, smoothing_factor=smoothing_factor, dropnans=dropnans ).volume_price_trend() diff --git a/test/data/cs-vpt.csv b/test/data/cs-vpt.csv new file mode 100644 index 00000000..09739ffa --- /dev/null +++ b/test/data/cs-vpt.csv @@ -0,0 +1,31 @@ +,Close,Volume,pct_change,pct_change x Volume,unsmoothed vpt,14-smoothed vpt +3-Dec-10,24.7486,18730.144,,,, +6-Dec-10,24.7088,12271.74,-0.0016081717753730906,-19.735065902716972,-19.735065902716972, +7-Dec-10,25.0373,24691.414,0.013294858511947005,328.26885558990745,308.53378968719045, +8-Dec-10,25.545,18357.606,0.02027774560355966,372.25086435838045,680.784654045571, +9-Dec-10,25.0672,22964.08,-0.018704247406537533,-429.52583378352045,251.2588202620505, +10-Dec-10,25.107,15918.948,0.0015877321759112384,25.275025946257855,276.5338462083084, +13-Dec-10,24.888,16067.044,-0.008722666985302774,-140.147474250207,136.38637195810136, +14-Dec-10,24.9975,16568.487,0.004399710703953508,72.89654960221455,209.2829215603159, +15-Dec-10,25.0473,16018.729,0.0019921992199221084,31.912499417943653,241.19542097825956, +16-Dec-10,25.336,9773.569,0.011526192443896077,112.65203715769694,353.8474581359565, +17-Dec-10,25.0572,22572.712000000003,-0.011004104831070283,-248.3924891695582,105.45496896639833, +20-Dec-10,25.4455,12986.669,0.015496543907539406,201.24848637118086,306.70345533757916, +21-Dec-10,25.5649,10906.659,0.004692381757088748,51.17820772238781,357.88166305996697, +22-Dec-10,25.555,5799.259,-0.00038724970565118255,-2.2457613407449712,355.635901719222, +23-Dec-10,25.4057,7395.274,-0.005842300919585264,-43.20541609078499,312.430485628437,276.8710494031886 +27-Dec-10,25.3658,5818.161999999999,-0.0015705137036177153,-9.137503150867852,303.2929824775691,299.94448143035186 +28-Dec-10,25.0373,7164.726,-0.012950508164536578,-92.78684255966749,210.50613991790163,292.94250644683126 +29-Dec-10,24.9178,5672.914000000001,-0.004772878864733765,-27.076131332052285,183.43000858584935,257.41717462827967 +30-Dec-10,24.878,5624.741999999999,-0.0015972517637993233,-8.984129080416132,174.44587950543323,251.93053600280703 +31-Dec-10,24.9676,5023.469,0.003601575689364145,18.09240382667441,192.53828333210762,245.93085294022126 +3-Jan-11,25.0473,7457.090999999999,0.003192137009564444,23.804056164789927,216.34233949689755,251.64199347870672 +4-Jan-11,24.45,11798.008999999998,-0.023846881699823963,-281.3457249164584,-65.00338541956083,232.05011440871553 +5-Jan-11,24.5694,12366.132,0.0048834355828222265,60.38920903067658,-4.614176388884246,214.49228602534808 +6-Jan-11,24.0219,13294.865,-0.022283816454614414,-296.26033144887725,-300.8745078377615,167.72643131293967 +7-Jan-11,23.8825,9256.87,-0.0058030380611024945,-53.717968936677856,-354.5924767744393,134.8658994743084 +10-Jan-11,24.2011,9690.604,0.013340311943891958,129.27568028472717,-225.31679648971215,96.86445291521618 +11-Jan-11,24.2807,8870.318000000001,0.0032891066934974678,29.175422307251075,-196.14137418246108,57.291378826471316 +12-Jan-11,24.3305,7168.965,0.0020510117088881064,14.703631155609024,-181.43774302685205,18.928975630323173 +13-Jan-11,24.44,11356.18,0.0045005240336204455,51.10876102011983,-130.32898200673222,-12.696700629331774 +14-Jan-11,25.0,13379.374,0.022913256955810146,306.56503436988544,176.23605236315322,-21.772195637504336 diff --git a/test/unit/volume.py b/test/unit/volume.py index ad48fdde..baae66fb 100644 --- a/test/unit/volume.py +++ b/test/unit/volume.py @@ -9,6 +9,7 @@ MFIIndicator, OnBalanceVolumeIndicator, VolumeWeightedAveragePrice, + VolumePriceTrendIndicator, acc_dist_index, ease_of_movement, force_index, @@ -16,6 +17,7 @@ on_balance_volume, sma_ease_of_movement, volume_weighted_average_price, + volume_price_trend ) @@ -253,6 +255,71 @@ def test_vwap2(self): self._df[target].tail(), result.tail(), check_names=False ) +class TestVolumePriceTrendIndicator(unittest.TestCase): + """ + Original VPT: https://en.wikipedia.org/wiki/Volume%E2%80%93price_trend + One more: https://www.barchart.com/education/technical-indicators/price_volume_trend + According to TradingView: PVT = [((CurrentClose - PreviousClose) / PreviousClose) x Volume] + PreviousPVT + + Smoothed version (by Alex Orekhov (everget)): https://ru.tradingview.com/script/3Ah2ALck-Price-Volume-Trend/ + His script is using `pvt` (TradingView built-in variable) as described in TradingView documentation of PVT and just smoothing it with ema or sma by choice. + You can find smoothing here (13 row of script): + `signal = signalType == "EMA" ? ema(pvt, signalLength) : sma(pvt, signalLength)` + """ + + _filename = "test/data/cs-vpt.csv" + + @classmethod + def setUpClass(cls): + cls._df = pd.read_csv(cls._filename, sep=",") + cls._params = dict( # default VPT params, unsmoothed + close=cls._df["Close"], + volume=cls._df["Volume"], + fillna=False, + smoothing_factor=None, + dropnans=False + ) + cls._params_smoothed = dict( # smoothed VPT params + close=cls._df["Close"], + volume=cls._df["Volume"], + fillna=False, + smoothing_factor=14, + dropnans=False + ) + cls._indicator_default = VolumePriceTrendIndicator(**cls._params) + cls._indicator_smoothed = VolumePriceTrendIndicator(**cls._params_smoothed) + + @classmethod + def tearDownClass(cls): + del cls._df + + def test_vpt1(self): + target = "unsmoothed vpt" + result = volume_price_trend(**self._params) + pd.testing.assert_series_equal( + self._df[target].tail(), result.tail(), check_names=False + ) + + def test_vpt2(self): + target = "unsmoothed vpt" + result = self._indicator_default.volume_price_trend() + pd.testing.assert_series_equal( + self._df[target].tail(), result.tail(), check_names=False + ) + + def test_vpt3(self): + target = "14-smoothed vpt" + result = volume_price_trend(**self._params_smoothed) + pd.testing.assert_series_equal( + self._df[target].tail(), result.tail(), check_names=False + ) + + def test_vpt4(self): + target = "14-smoothed vpt" + result = self._indicator_default.volume_price_trend() + pd.testing.assert_series_equal( + self._df[target].tail(), result.tail(), check_names=False + ) if __name__ == "__main__": unittest.main() From cd24a983ddb32c58f5b4cf398a2a79ffa4cec29e Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Wed, 1 Nov 2023 01:37:28 +0200 Subject: [PATCH 07/11] I guess I needed that change =) --- test/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/__init__.py b/test/__init__.py index 2310ac1e..6308977b 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -70,4 +70,5 @@ "TestMFIIndicator", "TestOnBalanceVolumeIndicator", "TestVolumeWeightedAveragePrice", + "TestVolumePriceTrendIndicator", ] From c18a8f4599d2c69273ebc2e3b6dfc0eac2bce76d Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:52:20 +0200 Subject: [PATCH 08/11] why i do this kind of mistakes all the time... --- test/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/__init__.py b/test/__init__.py index 6308977b..8177a1c9 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -36,6 +36,7 @@ TestMFIIndicator, TestOnBalanceVolumeIndicator, TestVolumeWeightedAveragePrice, + TestVolumePriceTrendIndicator, ) __all__ = [ From 6dce628721411b189431185fe4c95f648bfea099 Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:55:59 +0200 Subject: [PATCH 09/11] my lsp doesn't work right now, that's why ... --- ta/volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ta/volume.py b/ta/volume.py index 956b804b..7a3774b7 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -280,7 +280,7 @@ def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, sm def _run(self): self._vpt = (self._close.pct_change() * self._volume).cumsum() if self._smoothing_factor: - min_periods = 0 if self._fillna else self._window + min_periods = 0 if self._fillna else self.smoothing_factor self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() if self._dropnans: self._vpt = self._vpt.dropna() From 7a28437a6778ad5ac0ec0fc2f96fb2a30b41194a Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Wed, 1 Nov 2023 11:59:41 +0200 Subject: [PATCH 10/11] lsp lsp lsp... --- ta/volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ta/volume.py b/ta/volume.py index 7a3774b7..5947c14c 100644 --- a/ta/volume.py +++ b/ta/volume.py @@ -280,7 +280,7 @@ def __init__(self, close: pd.Series, volume: pd.Series, fillna: bool = False, sm def _run(self): self._vpt = (self._close.pct_change() * self._volume).cumsum() if self._smoothing_factor: - min_periods = 0 if self._fillna else self.smoothing_factor + min_periods = 0 if self._fillna else self._smoothing_factor self._vpt = self._vpt.rolling(self._smoothing_factor, min_periods=min_periods).mean() if self._dropnans: self._vpt = self._vpt.dropna() From f7df6d8c5bdadf5f9820f2a5079bb76d7179d4fb Mon Sep 17 00:00:00 2001 From: Groni3000 <72792408+Groni3000@users.noreply.github.com> Date: Wed, 1 Nov 2023 12:16:56 +0200 Subject: [PATCH 11/11] Once more ... and ... --- test/unit/volume.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/volume.py b/test/unit/volume.py index baae66fb..52db1b4d 100644 --- a/test/unit/volume.py +++ b/test/unit/volume.py @@ -316,7 +316,7 @@ def test_vpt3(self): def test_vpt4(self): target = "14-smoothed vpt" - result = self._indicator_default.volume_price_trend() + result = self._indicator_smoothed.volume_price_trend() pd.testing.assert_series_equal( self._df[target].tail(), result.tail(), check_names=False )