Files
pandas-ta/examples/TA_Analysis.ipynb
T

279 KiB
Raw Blame History

TA Analysis with Pandas TA

  • This is a Work in Progress and subject to change!
  • Contributions are welcome and accepted!
  • Examples below are for educational purposes only.
  • NOTE: The watchlist module is independent of Pandas TA. To easily use it, copy it from your local pandas_ta installation directory into your project directory.

Required Packages

Uncomment the packages you need to install or are missing
In [1]:
#!pip install numpy
#!pip install pandas
#!pip install mplfinance
#!pip install pandas-datareader
#!pip install requests_cache
#!pip install tqdm
#!pip install alphaVantage-api # Required for Watchlist
In [2]:
%pylab inline
import datetime as dt
import random as rnd
from sys import float_info as sflt

from tqdm import tqdm

import numpy as np
import pandas as pd
pd.set_option("max_rows", 100)
pd.set_option("max_columns", 20)

import mplfinance as mpf
import pandas_ta as ta

from tqdm.notebook import trange, tqdm

from watchlist import colors, Watchlist # Is this failing? If so, copy it locally. See above.

print(f"Numpy v{np.__version__}")
print(f"Pandas v{pd.__version__}")
print(f"mplfinance v{mpf.__version__}")
print(f"\nPandas TA v{ta.version}\nTo install the Latest Version:\n$ pip install -U git+https://github.com/twopirllc/pandas-ta\n")
%matplotlib inline
Populating the interactive namespace from numpy and matplotlib
Numpy v1.20.3
Pandas v1.3.0
mplfinance v0.12.7a17

Pandas TA v0.3.48b0
To install the Latest Version:
$ pip install -U git+https://github.com/twopirllc/pandas-ta

MISC Function(s)

In [3]:
def recent_bars(df, tf: str = "1y"):
    # All Data: 0, Last Four Years: 0.25, Last Two Years: 0.5, This Year: 1, Last Half Year: 2, Last Quarter: 4
    yearly_divisor = {"all": 0, "10y": 0.1, "5y": 0.2, "4y": 0.25, "3y": 1./3, "2y": 0.5, "1y": 1, "6mo": 2, "3mo": 4}
    yd = yearly_divisor[tf] if tf in yearly_divisor.keys() else 0
    return int(ta.RATE["TRADING_DAYS_PER_YEAR"] / yd) if yd > 0 else df.shape[0]

Data Collection

In [4]:
tf = "D"
tickers = ["SPY", "QQQ", "AAPL", "TSLA", "BTC-USD"]
watch = Watchlist(tickers, tf=tf, ds_name="yahoo", timed=True)
# watch.study = ta.CommonStudy # If you have a Custom Study, you can use it here.
watch.load(tickers, analyze=True, verbose=False)
[!] Loading All: SPY, QQQ, AAPL, TSLA, BTC-USD
[i] Loaded SPY[D]: SPY_D.csv
[i] Analysis Time: 32.4298 ms (0.0324 s) for 5 columns (avg 6.4872 ms / col).
[i] Loaded QQQ[D]: QQQ_D.csv
[i] Analysis Time: 2.6830 ms (0.0027 s) for 5 columns (avg 0.5372 ms / col).
[i] Loaded AAPL[D]: AAPL_D.csv
[i] Analysis Time: 2.8270 ms (0.0028 s) for 5 columns (avg 0.5660 ms / col).
[i] Loaded TSLA[D]: TSLA_D.csv
[i] Analysis Time: 2.4430 ms (0.0024 s) for 5 columns (avg 0.4892 ms / col).
[i] Loaded BTC-USD[D]: BTC-USD_D.csv
[i] Analysis Time: 2.4138 ms (0.0024 s) for 5 columns (avg 0.4833 ms / col).

Asset Selection

In [5]:
ticker = tickers[1] # change tickers by changing the index
print(f"{ticker} {watch.data[ticker].shape}\nColumns: {', '.join(list(watch.data[ticker].columns))}")
QQQ (5782, 12)
Columns: Open, High, Low, Close, Volume, Dividends, Stock Splits, SMA_10, SMA_20, SMA_50, SMA_200, VOL_SMA_20

Trim it

In [6]:
duration = "5y"
asset = watch.data[ticker]
recent = recent_bars(asset, duration)
asset.columns = asset.columns.str.lower()
asset.drop(columns=["dividends", "split"], errors="ignore", inplace=True)
asset = asset.copy().tail(recent)
asset
Out [6]:
open high low close volume stock splits sma_10 sma_20 sma_50 sma_200 vol_sma_20
Date
2017-02-28 125.757033 125.814899 125.052975 125.400185 16598600 0.0 125.240086 123.480899 119.942171 112.361493 16326435.00
2017-03-01 126.200704 127.001200 125.959587 126.769737 25813800 0.0 125.520745 123.812194 120.161973 112.490696 16662570.00
2017-03-02 126.740806 126.760085 125.978868 126.133186 19951400 0.0 125.664453 124.070189 120.377628 112.610403 16517045.00
2017-03-03 126.046393 126.403242 125.708824 126.364670 13722700 0.0 125.835163 124.345063 120.588461 112.737677 16448110.00
2017-03-06 125.949898 126.297107 125.583410 126.084923 12026200 0.0 125.923891 124.588589 120.784826 112.861734 16201560.00
... ... ... ... ... ... ... ... ... ... ... ...
2022-02-22 338.489990 344.040009 334.350006 338.079987 85967100 0.0 351.604001 353.118498 372.665317 366.637045 85912250.00
2022-02-23 341.320007 342.179993 329.100006 329.420013 86215400 0.0 348.634003 352.334000 371.303671 366.661107 84003100.00
2022-02-24 318.839996 341.040009 318.260010 340.489990 130614100 0.0 346.010001 352.129999 370.278278 366.742762 83205830.00
2022-02-25 341.309998 345.980011 337.390015 345.769989 78776600 0.0 344.744000 352.363498 369.439581 366.892766 82381875.00
2022-02-28 342.510010 348.540009 342.149994 344.209991 48205859 0.0 344.459000 351.983998 368.392910 367.022765 79383772.95

1260 rows × 11 columns

Trend Creation

A Trend is the result of some calculation or condition of one or more indicators. For simplicity, a Trend is either True or 1 and No Trend is False or 0. Using the Hello World of Trends, the Golden/Death Cross, it's Trend is Long when long = ma(close, 50) > ma(close, 200) and Short when short = ma(close, 50) < ma(close, 200) .

In [7]:
# Example Long Trends
# long = ta.sma(asset.close, 50) > ta.sma(asset.close, 200) # SMA(50) > SMA(200) "Golden/Death Cross"
long = ta.sma(asset.close, 20) > ta.sma(asset.close, 50) # SMA(20) > SMA(50)
# long = ta.ema(asset.close, 8) > ta.ema(asset.close, 21) # EMA(8) > EMA(21)
# long = ta.increasing(ta.ema(asset.close, 20))
# long = ta.macd(asset.close).iloc[:,1] > 0 # MACD Histogram is positive

# long &= ta.increasing(ta.ema(asset.close, 50), 2) # Uncomment for further long restrictions, in this case when EMA(50) is increasing/sloping upwards
# long = 1 - long # uncomment to create a short signal of the trend

asset.ta.ema(length=8, sma=False, append=True)
asset.ta.ema(length=21, sma=False, append=True)
asset.ta.ema(length=50, sma=False, append=True)
asset.ta.percent_return(append=True, cumulative=False)
print("TA Columns Added:")
asset[asset.columns[5:]].tail()
Out [7]:
TA Columns Added:
stock splits sma_10 sma_20 sma_50 sma_200 vol_sma_20 EMA_8 EMA_21 EMA_50 PCTRET_1
Date
2022-02-22 0.0 351.604001 353.118498 372.665317 366.637045 85912250.00 347.759289 355.318573 366.335378 -0.010044
2022-02-23 0.0 348.634003 352.334000 371.303671 366.661107 84003100.00 343.683894 352.964158 364.887717 -0.025615
2022-02-24 0.0 346.010001 352.129999 370.278278 366.742762 83205830.00 342.974138 351.830143 363.930943 0.033604
2022-02-25 0.0 344.744000 352.363498 369.439581 366.892766 82381875.00 343.595438 351.279220 363.218749 0.015507
2022-02-28 0.0 344.459000 351.983998 368.392910 367.022765 79383772.95 343.732005 350.636563 362.473308 -0.004512

Trend Signals

Given a Trend, Trend Signals returns the Trend, Trades, Entries and Exits as boolean integers. When asbool=True, it returns Trends, Entries and Exits as boolean values which is helpful when combined with the vectorbt backtesting package.

In [8]:
trendy = asset.ta.tsignals(long, asbool=False, append=True)
trendy.tail()
Out [8]:
TS_Trends TS_Trades TS_Entries TS_Exits
Date
2022-02-22 0 0 0 0
2022-02-23 0 0 0 0
2022-02-24 0 0 0 0
2022-02-25 0 0 0 0
2022-02-28 0 0 0 0

Trend Entries & Exits & Trade Table

This is a simple way to reduce the Asset DataFrame to a Trade Table with Dates, Signals, and Entries and Exits. Gives you an idea what to expect before running through a backtester such as vectorbt.

In [9]:
entries = trendy.TS_Entries * asset.close
entries = entries[~np.isclose(entries, 0)]
entries.dropna(inplace=True)
entries.name = "Entry"

exits = trendy.TS_Exits * asset.close
exits = exits[~np.isclose(exits, 0)]
exits.dropna(inplace=True)
exits.name = "Exit"

total_trades = trendy.TS_Trades.abs().sum()
rt_trades = int(trendy.TS_Trades.abs().sum() // 2)

all_trades = trendy.TS_Trades.copy().fillna(0)
all_trades = all_trades[all_trades != 0]

trades = pd.DataFrame({
    "Signal": all_trades,
    entries.name: entries,
    exits.name: exits
})

# Show some stats if there is an active trade (when there is an odd number of round trip trades)
if total_trades % 2 != 0:
    unrealized_pnl = asset.close.iloc[-1] - entries.iloc[-1]
    unrealized_pnl_pct_change = 100 * ((asset.close.iloc[-1] / entries.iloc[-1]) - 1)
    print("Current Trade:")
    print(f"Price Entry | Last:\t{entries.iloc[-1]:.4f} | {asset.close.iloc[-1]:.4f}")
    print(f"Unrealized PnL | %:\t{unrealized_pnl:.4f} | {unrealized_pnl_pct_change:.4f}%")
print(f"\nTrades Total | Round Trip:\t{total_trades} | {rt_trades}")
print(f"Trade Coverage: {100 * asset.TS_Trends.sum() / asset.shape[0]:.2f}%")

tradelist = trades
if rt_trades > 10:
    tradelist = trades.tail(10)
tradelist
Out [9]:
Trades Total | Round Trip:	22 | 11
Trade Coverage: 73.17%
Signal Entry Exit
Date
2020-04-27 1 213.766617 NaN
2020-10-01 -1 NaN 280.388550
2020-10-16 1 286.607239 NaN
2021-03-11 -1 NaN 316.515167
2021-04-13 1 339.395142 NaN
2021-05-26 -1 NaN 332.947998
2021-06-16 1 339.803650 NaN
2021-10-05 -1 NaN 356.924133
2021-11-02 1 388.553711 NaN
2022-01-07 -1 NaN 379.859985
In [ ]:

Visualization

Chart Display Strings

In [10]:
extime = ta.get_time(to_string=True)
first_date, last_date = asset.index[0], asset.index[-1]
last_ohlcv = f"Last OHLCV: ({asset.iloc[-1].open:.4f}, {asset.iloc[-1].high:.4f}, {asset.iloc[-1].low:.4f}, {asset.iloc[-1].close:.4f}, {int(asset.iloc[-1].volume)})"
_oc_change = asset.iloc[-1].open - asset.iloc[-1].close
_oc_change_pct = _oc_change / asset.iloc[-1].open
oc_change = f"{_oc_change:.4f} ({100 * _oc_change_pct:.4f} %)"
ptitle = f"\n{ticker} [{tf} for {duration}({recent} bars)]\n{last_ohlcv}, Change (%): {oc_change}\n{extime}"

Trade Chart

In [11]:
# chart = asset["close"] #asset[["close", "SMA_10", "SMA_20", "SMA_50", "SMA_200"]]
# chart = asset[["close", "SMA_10", "SMA_20"]]
chart = asset[["close", "EMA_8", "EMA_21", "EMA_50"]]
chart.plot(figsize=(16, 10), color=colors("BkGrOrRd"), title=ptitle, grid=True)
Out [11]:
<AxesSubplot:title={'center':'\nQQQ [D for 5y(1260 bars)]\nLast OHLCV: (342.5100, 348.5400, 342.1500, 344.2100, 48205859), Change (%): -1.7000 (-0.4963 %)\nMonday February 28, 2022, NYSE: 7:09:11, Local: 11:09:11 PST, Day 59/365 (16.00%)'}, xlabel='Date'>

Trends are either a Trend (1) or No Trend (0) depending on the Trend passed into *Trend Signals

In [12]:
long_trend = trendy.TS_Trends
short_trend = 1 - long_trend

long_trend.plot(figsize=(16, 0.85), kind="area", stacked=True, color=colors()[0], alpha=0.25) # Green Area
short_trend.plot(figsize=(16, 0.85), kind="area", stacked=True, color=colors()[1], alpha=0.25) # Red Area
Out [12]:
<AxesSubplot:xlabel='Date'>

Trades or Trade Signals

The Trades are either Enter (1) or Exit (-1) or No Position/Action (0). These are based on the Trend passed into Trend Signals whether they are Long or Short Trends.

In [13]:
trendy.TS_Trades.plot(figsize=(16, 1.5), color=colors("BkBl")[0], grid=True)
Out [13]:
<AxesSubplot:xlabel='Date'>

Active Returns

Active Returns are returns made during the course of the Trend. They are simply the product of the returns and the Trend

In [14]:
asset["ACTRET_1"] = trendy.TS_Trends.shift(1) * asset.PCTRET_1
asset[["PCTRET_1", "ACTRET_1"]].plot(figsize=(16, 3), color=colors("GyOr"), alpha=1, grid=True).axhline(0, color="black")
Out [14]:
<matplotlib.lines.Line2D at 0x1316d0ee0>

Buy and Hold Returns (PCTRET_1)

In [15]:
((asset.PCTRET_1 + 1).cumprod() - 1).plot(figsize=(16, 3), kind="area", stacked=False, color=colors("GyOr"), title="B&H Percent Returns", alpha=0.4, grid=True).axhline(0, color="black")
Out [15]:
<matplotlib.lines.Line2D at 0x13205efd0>

Cum. Active Returns (ACTRET_1)

In [16]:
((asset.ACTRET_1 + 1).cumprod() - 1).plot(figsize=(16, 3), kind="area", stacked=False, color=colors("GyOr")[-1], title="B&H Cum. Active Returns", alpha=0.4, grid=True).axhline(0, color="black")
Out [16]:
<matplotlib.lines.Line2D at 0x131f41820>

Disclaimer

  • All investments involve risk, and the past performance of a security, industry, sector, market, financial product, trading strategy, or individuals trading does not guarantee future results or returns. Investors are fully responsible for any investment decisions they make. Such decisions should be based solely on an evaluation of their financial circumstances, investment objectives, risk tolerance, and liquidity needs.

  • Any opinions, news, research, analyses, prices, or other information offered is provided as general market commentary, and does not constitute investment advice. I will not accept liability for any loss or damage, including without limitation any loss of profit, which may arise directly or indirectly from use of or reliance on such information.