In a realm of exotic technical indicators, such as the Relative Strength Index, Stochastic Oscillator, MACD, etc., the indicator we are going to discuss today would fit nicely, given its performance that exceeds those of the above-mentioned ones. It’s none other than the True Strength Index, shortly known as TSI.

In this article, we will first build some basic intuitions about the indicator and how it is calculated or the mathematics behind it. Then, we will move on to the programming part where we will use Python to build the indicator from scratch, construct a trading strategy, backtest the strategy and compare the results with those of the SPY ETF (an ETF particularly designed to track the movement of the S&P 500 market index). Without further ado, let’s jump into the article!

Register & Get Data

True Strength Index (TSI)

The True Strength Index (TSI) is a momentum oscillator that is primarily used by traders to determine whether a market is an upward or a downward momentum and trade along with it. It is also used to identify the current state of the market, overbought or oversold, but this is not the indicator’s main strength. The True Strength Index is composed of two components:

  • TSI Line: The first component is the TSI line itself which is calculated by first determining the actual price change (current closing price minus the previous closing price) and the absolute price change (absolute values of the actual price change). Then an EMA with 25 as the number of periods (long) is taken for both the actual price change and the absolute price change. These two EMAs are then again smoothed by a 13-day period (short) Exponential Moving Average. This process of smoothing a data series with two EMAs is known as double smoothing and the goal of doing this is to eliminate noise from the data. Now, the double-smoothed actual price change is divided by the double-smoothed absolute price change and then multiplied by 100 to obtain the readings of the TSI line. Note that the parameters (25, 13) we took into consideration are the typical setting but can be tuned accordingly. The calculation might be fuzzy but can be easily understood if we interpret it in the form of a formula or representation:
TSI LINE = [ DS. ACTUAL PC / DS. ABSOLUTE PC ] * 100

where,
DS. ACTUAL PC = Double smoothed actual price change with the length of 25 and 13
DS. ABSOLUTE PC = Double smoothed absolute price change with the length of 25 and 13
  • Signal line: The next component is the Signal line component which is the Exponential Moving Average of the TSI for a specified number of periods (falls within 7 to 12 periods). Most traders prefer periods near to 7 for day trading purposes and close to 12 for long-term investing. In this article, we are going with 12 as the number of periods since we will be dealing with a daily timeframe stock data rather than a minute timeframe. The calculation can be represented as follows:
SIGNAL LINE = EXP.MA 13 [ TSI LINE ]

That’s what the indicator is all about and the mathematics behind it. Now, let’s analyze a chart where the readings of the True Strength Index are plotted along with the closing price data of Apple to build more understanding about the indicator and its workings.

The above chart is divided into two panels: the upper panel with the plot of the closing price data of Apple and the lower panel with the readings of TSI’s components. As I said before, the TSI is primarily used to spot the momentum of the market and this can be seen clearly in the chart where the readings of the TSI above the positive territory (greater than zero) directly reveals that the market is in upward momentum and below the negative territory reveals a downward momentum in the market.

Now, let’s see how TSI can be used to determine whether the market is in the state of overbought or oversold. Usually, indicators like RSI have a standard threshold of overbought and oversold levels which is 70 and 30 respectively, and these thresholds are applicable to any tradable asset. Whereas, the levels of overbought and oversold vary from one asset to another while using the True Strength Index, and in our case, we could consider -10 as the oversold level and 10 as the overbought level. But still, it won’t be as effective as the other popular momentum oscillators.

Speaking about trading strategies, to my knowledge, we could apply three types of strategies based on the True Strength Index. The first one is the overbought and oversold levels. This one is the most common strategy among the momentum oscillators which reveals a buy signal whenever the components of the TSI go below the oversold level and a sell signal is generated whenever the readings of both the components rise above the overbought level. I personally don’t use this strategy since I believe the True Strength Index doesn’t have a good history with the concept of overbought and oversold levels.

The second trading strategy is the zero-line crossover which reveals a buy signal whenever the TSI’s components cross from below to above zero, likewise, a sell signal is revealed whenever the components of the TSI go from above the below zero. This strategy is effective but not as much as the next trading strategy we are going to discuss.

The final trading strategy and the one we are going to implement in this article is the signal line crossover which reveals a buy signal whenever the TSI line crosses from below to above the Signal line, and similarly, a sell signal is revealed whenever the TSI line goes from above to below the Signal line. This strategy is one of the most-used trading strategies among the others while using the True Strength Index due to its efficiency in the market. This trading strategy can be represented as follows:

IF PREV.TLINE < PREV.SLINE AND CUR.TLINE > CUR.SLINE ==> BUY SIGNAL
IF PREV.TLINE > PREV.SLINE AND CUR.TLINE < CUR.SLINE ==> SELL SIGNAL

That’s it! This concludes our theory part on the True Strength Index. Let’s now move on to the programming where we will use Python to first build the indicator from scratch, construct the signal line crossover trading strategy, backtest the strategy on Apple stock data and compare the results with those of SPY ETF. Let’s do some coding! Before moving on, a note on disclaimer: This article’s sole purpose is to educate people and must be considered as an information piece but not as investment advice.

Register & Get Data

Implementation in Python

The coding part is classified into various steps as follows:

1. Importing Packages
2. Extracting Stock Data from EODHD
3. True Strength Index Calculation
4. Creating the Signal line crossover Trading Strategy
5. Plotting the Trading Lists
6. Creating our Position
7. Backtesting
8. SPY ETF Comparison

We will be following the order mentioned in the above list and buckle up your seat belts to follow every upcoming coding part.

Step-1: Importing Packages

Importing the required packages into the Python environment is a non-skippable step. The primary packages are going to be Pandas to work with data, NumPy to work with arrays and for complex functions, Matplotlib for plotting purposes, and Requests to make API calls. The secondary packages are going to be Math for mathematical functions and Termcolor for font customization (optional).

Python Implementation:

# IMPORTING PACKAGES

import pandas as pd
import requests
import matplotlib.pyplot as plt
import numpy as np
from math import floor
from termcolor import colored as cl

plt.style.use('fivethirtyeight')
plt.rcParams['figure.figsize'] = (20,10)

With the required packages imported into Python, we can proceed to fetch historical data for Apple using EODHD’s OHLC split-adjusted data API endpoint.

Step-2: Extracting data from EODHD

In this phase, we’re set to retrieve the historical stock data for Apple using the OHLC split-adjusted API endpoint provided by EODHD. It’s important to note that EOD Historical Data (EODHD)is a reliable provider of financial APIs, encompassing an extensive array of market data, including historical data and economic news. Be sure to possess an EODHD account and access your secret API key, a crucial element for data extraction via the API.

Python Implementation:

# EXTRACTING STOCK DATA

def get_historical_data(symbol, start_date):
    api_key = 'YOUR API KEY'
    api_url = f'https://eodhistoricaldata.com/api/technical/{symbol}?order=a&fmt=json&from={start_date}&function=splitadjusted&api_token={api_key}'
    raw_df = requests.get(api_url).json()
    df = pd.DataFrame(raw_df)
    df.date = pd.to_datetime(df.date)
    df = df.set_index('date')
    return df

aapl = get_historical_data('AAPL', '2019-01-01')
aapl.tail()

Output:

Code Explanation: We begin by defining a function named ‘get_historical_data,’ which takes the stock symbol (‘symbol’) and the start date for historical data (‘start_date’) as parameters. Inside the function, we define the API key and URL, then retrieve the historical data in JSON format using the ‘get’ function and store it in the ‘raw_df’ variable. After cleaning and formatting the raw JSON data, we return it as a Pandas dataframe. Finally, we call this function to fetch Apple’s historical data from the start of 2019 and store it in the ‘aapl’ variable.

Step-3: True Strength Index Calculation

In this step, we are going to calculate the components of the True Strength Index by following the methods and formulas we discussed before.

Python Implementation:

# TRUE STRENGTH INDEX CALCULATION 

def get_tsi(close, long, short, signal):
diff = close - close.shift(1)
abs_diff = abs(diff)

diff_smoothed = diff.ewm(span = long, adjust = False).mean()
diff_double_smoothed = diff_smoothed.ewm(span = short, adjust = False).mean()
abs_diff_smoothed = abs_diff.ewm(span = long, adjust = False).mean()
abs_diff_double_smoothed = abs_diff_smoothed.ewm(span = short, adjust = False).mean()

tsi = (diff_double_smoothed / abs_diff_double_smoothed) * 100
signal = tsi.ewm(span = signal, adjust = False).mean()
tsi = tsi[tsi.index >= '2020-01-01'].dropna()
signal = signal[signal.index >= '2020-01-01'].dropna()

return tsi, signal

aapl['tsi'], aapl['signal_line'] = get_tsi(aapl['close'], 25, 13, 12)
aapl = aapl[aapl.index >= '2020-01-01']
aapl.tail()

Output:

Code Explanation: Firstly, we are defining a function named ‘get_tsi’ that takes a stock’s closing price data (‘close’), the lookback period for the long EMA (‘long’), the lookback period for the short EMA (‘short’), and the lookback period for the signal line (‘signal’) as parameters. Inside the function, we are first calculating and storing the actual price change (‘diff’) and the absolute price change (‘abs_diff’) into their respective variable.

Then, using the ‘ewm’ function provided by the Pandas package for determining the Exponential Moving Average, we are double smoothing the previously calculated price changes to obtain the double smoothed actual price change (‘diff_double_smoothed’) and the double smoothed absolute price change (‘abs_diff_double_smoothed’).

We are then substituting the double smoothed values into the formula of the TSI line to determine its readings. To calculate the signal line’s values, we are taking an EMA of the determined TSI line’s readings for a specified number of periods. Finally, we are returning the calculated components and calling the created function to store Apple’s TSI components’ values.

Step-4: Creating the trading strategy

In this step, we are going to implement the discussed True Strength Index signal line crossover trading strategy in Python.

Python Implementation:

# TRUE STRENGTH INDEX STRATEGY

def implement_tsi_strategy(prices, tsi, signal_line):
buy_price = []
sell_price = []
tsi_signal = []
signal = 0

for i in range(len(prices)):
if tsi[i-1] < signal_line[i-1] and tsi[i] > signal_line[i]:
if signal != 1:
buy_price.append(prices[i])
sell_price.append(np.nan)
signal = 1
tsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
tsi_signal.append(0)
elif tsi[i-1] > signal_line[i-1] and tsi[i] < signal_line[i]:
if signal != -1:
buy_price.append(np.nan)
sell_price.append(prices[i])
signal = -1
tsi_signal.append(signal)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
tsi_signal.append(0)
else:
buy_price.append(np.nan)
sell_price.append(np.nan)
tsi_signal.append(0)

return buy_price, sell_price, tsi_signal

buy_price, sell_price, tsi_signal = implement_tsi_strategy(aapl['close'], aapl['tsi'], aapl['signal_line'])

Code Explanation: First, we are defining a function named ‘implement_tsi_strategy’ which takes the stock prices (‘prices’), and the components of the True Strength Index (‘tsi’, ‘signal_line’) as parameters.

Inside the function, we are creating three empty lists (buy_price, sell_price, and tsi_signal) in which the values will be appended while creating the trading strategy.

After that, we are implementing the trading strategy through a for-loop. Inside the for-loop, we are passing certain conditions, and if the conditions are satisfied, the respective values will be appended to the empty lists. If the condition to buy the stock gets satisfied, the buying price will be appended to the ‘buy_price’ list, and the signal value will be appended as 1 representing to buy the stock. Similarly, if the condition to sell the stock gets satisfied, the selling price will be appended to the ‘sell_price’ list, and the signal value will be appended as -1 representing to sell the stock.

Finally, we are returning the lists appended with values. Then, we are calling the created function and stored the values in their respective variables. The list doesn’t make any sense unless we plot the values. So, let’s plot the values of the created trading lists.

Step-5: Plotting the trading signals

In this step, we are going to plot the created trading lists to make sense of them.

Python Implementation:

# TRUE STRENGTH INDEX TRADING SIGNALS PLOT

ax1 = plt.subplot2grid((11,1), (0,0), rowspan = 5, colspan = 1)
ax2 = plt.subplot2grid((11,1), (6,0), rowspan = 5, colspan = 1)
ax1.plot(aapl['close'], linewidth = 2)
ax1.plot(aapl.index, buy_price, marker = '^', markersize = 12, color = 'green', linewidth = 0, label = 'BUY SIGNAL')
ax1.plot(aapl.index, sell_price, marker = 'v', markersize = 12, color = 'r', linewidth = 0, label = 'SELL SIGNAL')
ax1.legend()
ax1.set_title('AAPL TSI TRADING SIGNALS')
ax2.plot(aapl['tsi'], linewidth = 2, color = 'orange', label = 'TSI LINE')
ax2.plot(aapl['signal_line'], linewidth = 2, color = '#FF006E', label = 'SIGNAL LINE')
ax2.set_title('AAPL TSI 25,13,12')
ax2.legend()
plt.show()

Output:

Code Explanation: We are plotting the components of the True Strength Index along with the buy and sell signals generated by the signal line crossover trading strategy. We can observe that whenever the previous reading of the TSI line is below the previous reading of the Signal Line and the current reading of the TSI line is above the current reading of the Signal line, a green-colored buy signal is plotted in the chart. Similarly, whenever the previous reading of the TSI line is above the previous reading of the Signal Line and the current reading of the TSI line is below the current reading of the Signal line, a red-colored sell signal is plotted in the chart.

Step-6: Creating our Position

In this step, we are going to create a list that indicates 1 if we hold the stock or 0 if we don’t own or hold the stock.

Python Implementation:

# STOCK POSITION

position = []
for i in range(len(tsi_signal)):
if tsi_signal[i] > 1:
position.append(0)
else:
position.append(1)

for i in range(len(aapl['close'])):
if tsi_signal[i] == 1:
position[i] = 1
elif tsi_signal[i] == -1:
position[i] = 0
else:
position[i] = position[i-1]

close_price = aapl['close']
tsi = aapl['tsi']
signal_line = aapl['signal_line']
tsi_signal = pd.DataFrame(tsi_signal).rename(columns = {0:'tsi_signal'}).set_index(aapl.index)
position = pd.DataFrame(position).rename(columns = {0:'tsi_position'}).set_index(aapl.index)

frames = [close_price, tsi, signal_line, tsi_signal, position]
strategy = pd.concat(frames, join = 'inner', axis = 1)

strategy

Output:

Code Explanation: First, we are creating an empty list named ‘position’. We are passing two for-loops, one is to generate values for the ‘position’ list to just match the length of the ‘signal’ list. The other for-loop is the one we are using to generate actual position values. Inside the second for-loop, we are iterating over the values of the ‘signal’ list, and the values of the ‘position’ list get appended concerning which condition gets satisfied. The value of the position remains 1 if we hold the stock or remains 0 if we sold or don’t own the stock. Finally, we are doing some data manipulations to combine all the created lists into one dataframe.

From the output being shown, we can see that in the first two rows our position in the stock has remained 1 (since there isn’t any change in the True Strength Index signal) but our position suddenly turned to -1 as we sold the stock when the True Strength Index trading signal represents a sell signal (-1). Our position will remain 0 until some changes in the trading signal occur. Now it’s time to do implement some backtesting process!

Step-7: Backtesting

Before moving on, it is essential to know what backtesting is. Backtesting is the process of seeing how well our trading strategy has performed on the given stock data. In our case, we are going to implement a backtesting process for our True Strength Index trading strategy over the Apple stock data.

Python Implementation:

# BACKTESTING

aapl_ret = pd.DataFrame(np.diff(aapl['close'])).rename(columns = {0:'returns'})
tsi_strategy_ret = []

for i in range(len(aapl_ret)):
    returns = aapl_ret['returns'][i]*strategy['tsi_position'][i]
    tsi_strategy_ret.append(returns)
    
tsi_strategy_ret_df = pd.DataFrame(tsi_strategy_ret).rename(columns = {0:'tsi_returns'})
investment_value = 100000
tsi_investment_ret = []

for i in range(len(tsi_strategy_ret_df['tsi_returns'])):
    number_of_stocks = floor(investment_value/aapl['close'][i])
    returns = number_of_stocks*tsi_strategy_ret_df['tsi_returns'][i]
    tsi_investment_ret.append(returns)

tsi_investment_ret_df = pd.DataFrame(tsi_investment_ret).rename(columns = {0:'investment_returns'})
total_investment_ret = round(sum(tsi_investment_ret_df['investment_returns']), 2)
profit_percentage = floor((total_investment_ret/investment_value)*100)
print(cl('Profit gained from the tsi strategy by investing $100k in AAPL : {}'.format(total_investment_ret), attrs = ['bold']))
print(cl('Profit percentage of the tsi strategy : {}%'.format(profit_percentage), attrs = ['bold']))

Output:

Profit gained from the tsi strategy by investing $100k in AAPL : 95230.25
Profit percentage of the tsi strategy : 95%

Code Explanation: First, we are calculating the returns of the Apple stock using the ‘diff’ function provided by the NumPy package and we have stored it as a dataframe in the ‘aapl_ret’ variable. Next, we are passing a for-loop to iterate over the values of the ‘aapl_ret’ variable to calculate the returns we gained from our TSI trading strategy, and these returns values are appended to the ‘tsi_strategy_ret’ list. Next, we are converting the ‘tsi_strategy_ret’ list into a dataframe and storing it in the ‘tsi_strategy_ret_df’ variable.

Next comes the backtesting process. We are going to backtest our strategy by investing a hundred thousand USD into our trading strategy. So first, we are storing the amount of investment into the ‘investment_value’ variable. After that, we are calculating the number of Apple stocks we can buy using the investment amount. You can notice that I’ve used the ‘floor’ function provided by the Math package because, while dividing the investment amount by the closing price of Apple stock, it spits out an output with decimal numbers. The number of stocks should be an integer but not a decimal number. Using the ‘floor’ function, we can cut out the decimals. Remember that the ‘floor’ function is way more complex than the ‘round’ function. Then, we are passing a for-loop to find the investment returns followed by some data manipulation tasks.

Finally, we are printing the total return we got by investing a hundred thousand into our trading strategy and it is revealed that we have made an approximate profit of ninety-five thousand USD in one year. That’s not bad! Now, let’s compare our returns with SPY ETF (an ETF designed to track the S&P 500 stock market index) returns.

Step-8: SPY ETF Comparison

While this step is optional, it is highly recommended as it enables us to assess the performance of our trading strategy against a benchmark (SPY ETF). Here, we’ll extract SPY ETF data using the ‘get_historical_data’ function and compare the returns from the SPY ETF with the returns generated by our trading strategy applied to Apple stock.

Python Implementation:

# SPY ETF COMPARISON

def get_benchmark(start_date, investment_value):
    spy = get_historical_data('SPY', start_date)['close']
    benchmark = pd.DataFrame(np.diff(spy)).rename(columns = {0:'benchmark_returns'})
    
    investment_value = investment_value
    benchmark_investment_ret = []
    
    for i in range(len(benchmark['benchmark_returns'])):
        number_of_stocks = floor(investment_value/spy[i])
        returns = number_of_stocks*benchmark['benchmark_returns'][i]
        benchmark_investment_ret.append(returns)

    benchmark_investment_ret_df = pd.DataFrame(benchmark_investment_ret).rename(columns = {0:'investment_returns'})
    return benchmark_investment_ret_df

benchmark = get_benchmark('2020-01-01', 100000)
investment_value = 100000
total_benchmark_investment_ret = round(sum(benchmark['investment_returns']), 2)
benchmark_profit_percentage = floor((total_benchmark_investment_ret/investment_value)*100)
print(cl('Benchmark profit by investing $100k : {}'.format(total_benchmark_investment_ret), attrs = ['bold']))
print(cl('Benchmark Profit percentage : {}%'.format(benchmark_profit_percentage), attrs = ['bold']))
print(cl('TSI Strategy profit is {}% higher than the Benchmark Profit'.format(profit_percentage - benchmark_profit_percentage), attrs = ['bold']))

Output:

Benchmark profit by investing $100k : 40181.36
Benchmark Profit percentage : 40%
TSI Strategy profit is 55% higher than the Benchmark Profit

Code Explanation: The code used in this step is almost similar to the one used in the previous backtesting step but, instead of investing in Apple, we are investing in SPY ETF by not implementing any trading strategies. From the output, we can see that our True Strength Index signal line crossover trading strategy has outperformed the SPY ETF by 55%. That’s great!

Register & Get Data

Final Thoughts!

After an overwhelming process of crushing both theory and programming parts, we have successfully learned what the True Strength Index is all about, the mathematics behind the indicator, and the implementation of a profitable trading strategy based on it. Even though we managed to surpass the results of the SPY ETF, we are still lagging in one specific space.

In this article, our algorithm buys and sells the same amount of stocks for all trades but that would not be an optimal decision to make while trading in the real world since it’s arbitrary, and here is where the concept of position sizing comes into play. For those who don’t have an idea about this concept, position sizing is the process of allocating a share of our portfolio to a specific stock we would like to trade. What we could do is, we can deploy a position-sizing system that assesses the risk involved in each trade and allocate the number of shares accordingly. This will not only boost the performance substantially but also prevents us from facing unexpected losses. So, I highly recommend you follow up with this concept and try implementing it into a trading strategy.

With that being said, you have reached the end of the article. Hope you learned something new and useful from this article.

Do you enjoy our articles?

We can send new ones right to your email box