Files
pandas-ta/examples/AIExample.ipynb
T

286 KiB
Raw Blame History

TA Analysis with Pandas TA and AI/ML

  • 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.32b0
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
[+] Downloading[yahoo]: SPY[D]
[+] Saving: /Users/kj/av_data/SPY_D.csv
[i] Runtime: 707.3915 ms (0.7074 s)
[+] Downloading[yahoo]: QQQ[D]
[+] Saving: /Users/kj/av_data/QQQ_D.csv
[i] Runtime: 696.6895 ms (0.6967 s)
[+] Downloading[yahoo]: AAPL[D]
[+] Saving: /Users/kj/av_data/AAPL_D.csv
[i] Runtime: 856.9906 ms (0.8570 s)
[+] Downloading[yahoo]: TSLA[D]
[+] Saving: /Users/kj/av_data/TSLA_D.csv
[i] Runtime: 719.1806 ms (0.7192 s)
[+] Downloading[yahoo]: BTC-USD[D]
[+] Saving: /Users/kj/av_data/BTC-USD_D.csv
[i] Runtime: 811.2178 ms (0.8112 s)

Asset Selection

In [5]:
ticker = tickers[0] # change tickers by changing the index
print(f"{ticker} {watch.data[ticker].shape}\nColumns: {', '.join(list(watch.data[ticker].columns))}")
SPY (7299, 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 = "1y"
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
2021-01-25 378.623058 379.708564 373.481570 379.333588 70402000 0 375.540128 371.941592 363.642487 325.851111 63120030.0
2021-01-26 380.340140 380.774354 378.494744 378.741455 42665300 0 376.043420 372.671364 364.207632 326.389357 63930400.0
2021-01-27 375.218403 375.317093 367.116410 369.484833 123351100 0 375.613156 372.781891 364.655645 326.893695 68147935.0
2021-01-28 371.409193 376.905930 370.945404 372.662506 94198100 0 375.400000 373.086334 365.071107 327.374311 70173815.0
2021-01-29 370.688763 371.715090 363.425564 365.201904 126765100 0 374.572037 372.991595 365.249507 327.847003 74039305.0
... ... ... ... ... ... ... ... ... ... ... ...
2022-01-14 461.190002 465.089996 459.899994 464.720001 95849600 0 469.319998 469.606500 465.989207 438.476059 78776895.0
2022-01-18 459.739990 459.959991 455.309998 456.489990 109709100 0 467.197998 469.437500 465.813499 438.746869 77486770.0
2022-01-19 458.130005 459.609985 451.459991 451.750000 109357600 0 464.617999 469.275999 465.510704 438.995168 77597910.0
2022-01-20 453.750000 458.739990 444.500000 446.750000 122379700 0 462.454999 468.460500 465.099938 439.216139 80226580.0
2022-01-21 445.559998 448.059998 437.950012 437.980011 201913500 0 459.459000 466.975000 464.544664 439.383707 87377745.0

252 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, 10) > ta.sma(asset.close, 20) # SMA(10) > SMA(20)
long = ta.ema(asset.close, 8) > ta.ema(asset.close, 21) # EMA(8) > EMA(21)
# long = ta.increasing(ta.ema(asset.close, 50))
# long = ta.macd(asset.close).iloc[:,1] > 0 # MACD Histogram is positive
# long = ta.amat(asset.close, 50, 200).AMATe_LR_2  # Long Run of AMAT(50, 200) with lookback of 2 bars

# 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)
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-01-14 0 469.319998 469.606500 465.989207 438.476059 78776895.0 467.864562 468.366344 464.226269 0.000409
2022-01-18 0 467.197998 469.437500 465.813499 438.746869 77486770.0 465.336880 467.286675 463.922886 -0.017710
2022-01-19 0 464.617999 469.275999 465.510704 438.995168 77597910.0 462.317573 465.874250 463.445517 -0.010384
2022-01-20 0 462.454999 468.460500 465.099938 439.216139 80226580.0 458.858112 464.135682 462.790791 -0.011068
2022-01-21 0 459.459000 466.975000 464.544664 439.383707 87377745.0 454.218534 461.757894 461.817820 -0.019631

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-01-14 0 -1 0 1
2022-01-18 0 0 0 0
2022-01-19 0 0 0 0
2022-01-20 0 0 0 0
2022-01-21 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}%")

trades
Out [9]:
Trades Total | Round Trip:	12 | 6
Trade Coverage: 77.78%
Signal Entry Exit
date
2021-02-23 1 382.402618 NaN
2021-03-04 -1 NaN 371.744690
2021-03-10 1 384.455292 NaN
2021-05-19 -1 NaN 406.783295
2021-05-20 1 411.159454 NaN
2021-09-17 -1 NaN 439.854675
2021-10-15 1 444.309021 NaN
2021-12-01 -1 NaN 448.922821
2021-12-08 1 467.876221 NaN
2021-12-20 -1 NaN 454.980011
2021-12-21 1 463.059998 NaN
2022-01-14 -1 NaN 464.720001
In [ ]:

Visualization

Chart Display Strings

In [10]:
extime = ta.get_time(to_string=True)
first_date, last_date = asset.index[0], asset.index[-1]
f_date = f"{first_date.day_name()} {first_date.month}-{first_date.day}-{first_date.year}"
l_date = f"{last_date.day_name()} {last_date.month}-{last_date.day}-{last_date.year}"
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)})"
ptitle = f"\n{ticker} [{tf} for {duration}({recent} bars)] from {f_date} to {l_date}\n{last_ohlcv}\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':'\nSPY [D for 1y(252 bars)] from Monday 1-25-2021 to Friday 1-21-2022\nLast OHLCV: (445.5600, 448.0600, 437.9500, 437.9800, 201913500)\nSunday January 23, 2022, NYSE: 11:44:28, Local: 15:44:28 PST, Day 23/365 (6.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 0x130230a60>

Buy and Hold Returns (PCTRET_1) vs. Cum. Active Returns (ACTRET_1)

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

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.

In [ ]: