Skip to content

Commit

Permalink
Add True Momentum Oscillator (TMO)
Browse files Browse the repository at this point in the history
Added TMO indicator, defaults 14/5/3 values. See documentation for the
calculation, defaults, and variants presented by some common platforms.

Added example jupyterlab notebook showing how with the present
information is trivial to compute the variants that other platforms
present.

Added plots with matplotlib, plotly, bokeh, with conditional shading.
  • Loading branch information
luisbarrancos committed Oct 13, 2023
1 parent aaffdbe commit b13cd02
Show file tree
Hide file tree
Showing 6 changed files with 4,168 additions and 1 deletion.
4,018 changes: 4,018 additions & 0 deletions examples/TMO_indicator.ipynb

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions pandas_ta/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -1117,6 +1117,12 @@ def td_seq(self, asint=None, offset=None, show_all=None, **kwargs: DictLike):
result = td_seq(close=close, asint=asint, offset=offset, show_all=show_all, **kwargs)
return self._post_process(result, **kwargs)

def tmo(self, tmo_length=None, calc_length=None, smooth_length=None, mamode=None, compute_momentum=False, normalize_signal=False, offset=None, **kwargs: DictLike):
open_ = self._get_column(kwargs.pop("open", "open"))
close = self._get_column(kwargs.pop("close", "close"))
result = tmo(open_, close, tmo_length=tmo_length, calc_length=calc_length, smooth_length=smooth_length, mamode=mamode, compute_momentum=compute_momentum, normalize_signal=normalize_signal, offset=offset, **kwargs)
return self._post_process(result, **kwargs)

def trix(self, length=None, signal=None, scalar=None, drift=None, offset=None, **kwargs: DictLike):
close = self._get_column(kwargs.pop("close", "close"))
result = trix(close=close, length=length, signal=signal, scalar=scalar, drift=drift, offset=offset, **kwargs)
Expand Down
2 changes: 1 addition & 1 deletion pandas_ta/maps.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"coppock", "cti", "er", "eri", "fisher", "inertia", "kdj", "kst",
"macd", "mom", "pgo", "ppo", "psl", "qqe", "roc", "rsi", "rsx",
"rvgi", "slope", "smi", "squeeze", "squeeze_pro", "stc", "stoch",
"stochf", "stochrsi", "td_seq", "trix", "tsi", "uo", "willr"
"stochf", "stochrsi", "td_seq", "tmo", "trix", "tsi", "uo", "willr"
],
# Overlap
"overlap": [
Expand Down
2 changes: 2 additions & 0 deletions pandas_ta/momentum/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from .stochf import stochf
from .stochrsi import stochrsi
from .td_seq import td_seq
from .tmo import tmo
from .trix import trix
from .tsi import tsi
from .uo import uo
Expand Down Expand Up @@ -79,6 +80,7 @@
"stochf",
"stochrsi",
"td_seq",
"tmo",
"trix",
"tsi",
"uo",
Expand Down
131 changes: 131 additions & 0 deletions pandas_ta/momentum/tmo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
from pandas import DataFrame, Series
from pandas_ta.overlap import ma
from pandas_ta.utils import get_offset, verify_series


def tmo(open_, close, tmo_length=None, calc_length=None, smooth_length=None, mamode=None,
compute_momentum=False, normalize_signal=False, offset=None, **kwargs):
"""True Momentum Oscillator (TMO)
The True Momentum Oscillator (TMO) is an indicator that aims to capture the
true momentum underlying the price movement of an asset over a specified time
frame. It quantifies the net buying and selling pressure by summing and then
smoothing the signum of the closing and opening price difference over the
given period, and then computing a main and smooth signal with a series of
moving averages.
Crossovers between the main and smoth signal generate potential signals for
buying and selling opportunities.
Some platforms present versions of this indicator with an optional momentum
calculation for the main TMO signal and its smooth version, as well as the
possibility to normalize the signals to the [-100,100] range, which has the
added benefit of allowing the definition of overbought and oversold regions,
typically -70 and 70.
Calculation:
Default Inputs: `tmo_length=14, calc_length=5, smooth_length=3`
EMA = Exponential Moving Average
Delta = close - open
Signum = 1 if Delta > 0, 0 if Delta = 0, -1 if Delta < 0
SUM = Summation of N given values
MA = EMA(SUM(Delta, tmo_length), calc_length)
TMO = EMA(MA, smooth_length)
TMOs = EMA(TMO, smooth_length)
TMO mom = TMO - TMO[-tmo_length]
TMOs mom = TMOs - TMOs[-tmo_length]
Sources:
https://www.tradingview.com/script/VRwDppqd-True-Momentum-Oscillator/
https://www.tradingview.com/script/65vpO7T5-True-Momentum-Oscillator-Universal-Edition/
https://www.tradingview.com/script/o9BQyaA4-True-Momentum-Oscillator/
Args:
open_ (pd.Series): Series of 'open' prices.
close (pd.Series): Series of 'close' prices.
tmo_length (int): The period for TMO calculation. Default: 14
calc_length (int): Initial moving average window. Default: 5
smooth_length (int): Main and smooth signal MA window. Default: 3
mamode (str): See ``help(ta.ma)``. Default: 'ema'
compute_momentum (bool): Compute main and smooth momentum. Default: False
normalize_signal (bool): Normalize TMO values to [-100,100]. Default: False
offset (int): How many periods to offset the result. Default: 0
Kwargs:
fillna (value, optional): pd.DataFrame.fillna(value)
fill_method (value, optional): Type of fill method
Returns:
pd.Series: main signal, smooth signal, main momentum, smooth momentum
"""

# Validate
tmo_length = int(tmo_length) if tmo_length and tmo_length > 0 else 14
calc_length = int(calc_length) if calc_length and calc_length > 0 else 5
smooth_length = int(smooth_length) if smooth_length and smooth_length > 0 else 3
mamode = mamode if isinstance(mamode, str) else "ema"
compute_momentum = compute_momentum if isinstance(compute_momentum, bool) else False
normalize_signal = normalize_signal if isinstance(normalize_signal, bool) else False

open_ = verify_series(open_, max(tmo_length, calc_length, smooth_length))
close = verify_series(close, max(tmo_length, calc_length, smooth_length))
offset = get_offset(offset)

if open_ is None or close is None:
return None

# Calculate (see documentation)
signum_values = Series(close - open_).apply(lambda x: 1 if x > 0 else (-1 if x < 0 else 0))
sum_signum = signum_values.rolling(window=tmo_length).sum()
if normalize_signal:
sum_signum = sum_signum * 100 / tmo_length

initial_ema = ma(mamode, sum_signum, length=calc_length)
main_signal = ma(mamode, initial_ema, length=smooth_length)
smooth_signal = ma(mamode, main_signal, length=smooth_length)

if compute_momentum:
mom_main = main_signal - main_signal.shift(tmo_length)
mom_smooth = smooth_signal - smooth_signal.shift(tmo_length)
else:
mom_main = Series([0] * len(main_signal), index=main_signal.index)
mom_smooth = Series([0] * len(smooth_signal), index=smooth_signal.index)

# Offset
if offset != 0:
main_signal = main_signal.shift(offset)
smooth_signal = smooth_signal.shift(offset)
mom_main = mom_main.shift(offset)
mom_smooth = mom_smooth.shift(offset)

# Fill
fill_value = kwargs.get("fillna", None)
fill_method = kwargs.get("fill_method", None)

if fill_value is not None:
main_signal.fillna(fill_value, inplace=True)
smooth_signal.fillna(fill_value, inplace=True)
mom_main.fillna(fill_value, inplace=True)
mom_smooth.fillna(fill_value, inplace=True)

if fill_method is not None:
main_signal.fillna(method=fill_method, inplace=True)
smooth_signal.fillna(method=fill_method, inplace=True)
mom_main.fillna(fill_value, inplace=True)
mom_smooth.fillna(fill_value, inplace=True)

# Name and Category
tmo_category = "momentum"
params = f"{tmo_length}_{calc_length}_{smooth_length}"

df = DataFrame({
f"TMO_{params}": main_signal,
f"TMO_Smooth_{params}": smooth_signal,
f"TMO_Main_Mom_{params}": mom_main,
f"TMO_Smooth_Mom_{params}": mom_smooth
})

df.name = f"TMO_{params}"
df.category = tmo_category

return df
10 changes: 10 additions & 0 deletions tests/test_indicator_momentum.py
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,10 @@ def test_stochrsi(df):
except Exception as ex:
error_analysis(result.iloc[:, 0], CORRELATION, ex, newline=False)

def test_tmo(self):
result = ta.tmo(df.open, df.close)
self.assertIsInstance(result, DataFrame)
self.assertEqual(result.name, "TMO_14_5_3")

def test_trix(df):
result = ta.trix(df.close)
Expand Down Expand Up @@ -772,6 +776,12 @@ def test_ext_td_seq(df):
assert list(df.columns[-2:]) == ["TD_SEQ_UPa", "TD_SEQ_DNa"]


def test_tmo(self):
result = pandas_ta.tmo(self.open, self.close)
self.assertIsInstance(result, DataFrame)
self.assertEqual(result.name, "TMO_14_5_3")


def test_ext_trix(df):
df.ta.trix(append=True)
assert list(df.columns[-2:]) == ["TRIX_30_9", "TRIXs_30_9"]
Expand Down

0 comments on commit b13cd02

Please sign in to comment.