Files
pandas-ta/examples/AIExample.ipynb
T

268 KiB
Raw Blame History

Strategy 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.2
Pandas v1.2.4
mplfinance v0.12.7a12

Pandas TA v0.2.74b0
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.strategy = ta.CommonStrategy # If you have a Custom Strategy, 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: 476.9447 ms (0.4769 s)
[+] Downloading[yahoo]: QQQ[D]
[+] Saving: /Users/kj/av_data/QQQ_D.csv
[i] Runtime: 455.1703 ms (0.4552 s)
[+] Downloading[yahoo]: AAPL[D]
[+] Saving: /Users/kj/av_data/AAPL_D.csv
[i] Runtime: 460.0315 ms (0.4600 s)
[+] Downloading[yahoo]: TSLA[D]
[+] Saving: /Users/kj/av_data/TSLA_D.csv
[i] Runtime: 456.2752 ms (0.4563 s)
[+] Downloading[yahoo]: BTC-USD[D]
[+] Saving: /Users/kj/av_data/BTC-USD_D.csv
[i] Runtime: 451.6120 ms (0.4516 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 (7108, 12)
Columns: open, high, low, close, volume, dividends, split, 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 sma_10 sma_20 sma_50 sma_200 vol_sma_20
date
2020-04-22 273.956662 276.564830 272.539388 274.694824 93524600 274.489117 263.075163 277.084857 293.063143 167661845.0
2020-04-23 276.062859 279.458417 274.350332 274.675110 104709700 274.986145 264.664179 276.017570 292.996336 157925815.0
2020-04-24 276.299118 279.222243 274.104305 278.503754 85166000 275.455618 265.735499 274.984586 292.941791 149302475.0
2020-04-27 280.619817 283.720094 280.127709 282.519348 77896600 276.576642 267.390459 274.038959 292.903932 141980245.0
2020-04-28 286.426668 286.800675 280.895376 281.220184 105270000 276.767578 268.575457 273.056781 292.853083 138675270.0
... ... ... ... ... ... ... ... ... ... ...
2021-04-15 413.739990 416.160004 413.690002 415.869995 60229800 409.150995 400.666098 392.662884 356.918501 83970505.0
2021-04-16 417.250000 417.910004 415.730011 417.260010 82004500 410.815997 402.018999 393.396016 357.480658 82315105.0
2021-04-19 416.260010 416.739990 413.790009 415.209991 78498500 411.700998 403.305498 394.001630 358.021889 80601140.0
2021-04-20 413.910004 415.089996 410.589996 412.170013 81851800 412.306000 404.284499 394.516144 358.539468 81004800.0
2021-04-21 411.510010 416.290009 411.359985 416.070007 66345300 413.254001 405.612999 395.052841 359.052723 79787735.0

252 rows × 10 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:
sma_10 sma_20 sma_50 sma_200 vol_sma_20 EMA_8 EMA_21 EMA_50 PCTRET_1
date
2021-04-15 409.150995 400.666098 392.662884 356.918501 83970505.0 410.171168 402.758751 393.431087 0.010742
2021-04-16 410.815997 402.018999 393.396016 357.480658 82315105.0 411.746466 404.077047 394.365555 0.003342
2021-04-19 411.700998 403.305498 394.001630 358.021889 80601140.0 412.516138 405.089133 395.182984 -0.004913
2021-04-20 412.306000 404.284499 394.516144 358.539468 81004800.0 412.439222 405.732850 395.849142 -0.007322
2021-04-21 413.254001 405.612999 395.052841 359.052723 79787735.0 413.246063 406.672591 396.642117 0.009462

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
2021-04-15 1 0 0 0
2021-04-16 1 0 0 0
2021-04-19 1 0 0 0
2021-04-20 1 0 0 0
2021-04-21 1 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]:
Current Trade:
Price Entry | Last:	385.9061 | 416.0700
Unrealized PnL | %:	30.1639 | 7.8164%

Trades Total | Round Trip:	7 | 3
Trade Coverage: 81.35%
Signal Entry Exit
date
2020-05-19 1 287.361664 NaN
2020-09-10 -1 NaN 330.066162
2020-10-02 1 331.337799 NaN
2020-10-27 -1 NaN 335.684937
2020-11-04 1 340.965088 NaN
2021-03-02 -1 NaN 385.278137
2021-03-09 1 385.906067 NaN
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 Wednesday 4-22-2020 to Wednesday 4-21-2021\nLast OHLCV: (411.5100, 416.2900, 411.3600, 416.0700, 66345300)\nWednesday April 21, 2021, NYSE: 17:08:21, Local: 21:08:21 PDT, Day 111/365 (30.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 * 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 0x12d86cbb0>

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

In [15]:
asset[["PCTRET_1", "ACTRET_1"]].cumsum().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 0x12da79790>

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.