Skip to content

Commit

Permalink
feat: TQQQ relative strength
Browse files Browse the repository at this point in the history
  • Loading branch information
namuan committed Jul 7, 2024
1 parent c905414 commit 6a28240
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 3 deletions.
2 changes: 1 addition & 1 deletion common/market.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def download_earnings_between(date_from, date_to):
return {}


def download_with_yf(ticker, period, interval):
def download_ticker_with_interval(ticker, period, interval):
try:
opts = dict(
tickers=ticker,
Expand Down
4 changes: 2 additions & 2 deletions common/plotting.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import numpy as np
import pandas as pd

from common.market import download_with_yf
from common.market import download_ticker_with_interval
from common.subprocess_runner import open_file


def plot_intraday(ticker, period="1d", interval="1m"):
data = download_with_yf(ticker, period=period, interval=interval)
data = download_ticker_with_interval(ticker, period=period, interval=interval)
print(f"Plotting {ticker}")
intraday = data.resample("30T").agg(
{
Expand Down
183 changes: 183 additions & 0 deletions tqqq-relative-strength.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env python3
"""
Analyzes and visualizes the relative performance of TQQQ against QQQ holdings.
Download QQQ holdings as a CSV File (Click on the Excel Download link)
https://www.invesco.com/us/financial-products/etfs/holdings?audienceType=Investor&ticker=QQQ
# Specify a custom CSV file for QQQ holdings
python tqqq-relative-strength.py --qqq-csv path/to/qqq_holdings.csv
"""
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from datetime import datetime, timedelta
from pathlib import Path

import pandas as pd
import seaborn as sns
from matplotlib import pyplot as plt
from matplotlib.dates import DateFormatter
from stockstats import StockDataFrame

from common.logger import setup_logging
from common.market import download_ticker_data


def parse_args():
parser = ArgumentParser(
description=__doc__, formatter_class=RawDescriptionHelpFormatter
)
parser.add_argument(
"--qqq-csv",
required=True,
help="Path to the CSV file containing QQQ holdings",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
dest="verbose",
help="Increase verbosity of logging output",
)
return parser.parse_args()


def get_cached_data(cache_file: Path):
if cache_file.exists():
cache_modification_time = datetime.fromtimestamp(cache_file.stat().st_mtime)
if datetime.now() - cache_modification_time < timedelta(days=1):
return pd.read_csv(cache_file, index_col=0, parse_dates=True)
return None


def save_to_cache(df: pd.DataFrame, cache_file: Path):
cache_file.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(cache_file)


def get_asset_data(symbol, start_date, end_date):
cache_dir = Path("output")
cache_file = cache_dir / f"{symbol}_{start_date}_{end_date}.csv"
cached_data = get_cached_data(cache_file)

if cached_data is not None:
print(f"Using cached data for {symbol}")
return StockDataFrame.retype(cached_data)

print(f"Downloading new data for {symbol}")
data = StockDataFrame.retype(
download_ticker_data(symbol, start=start_date, end=end_date)
)
save_to_cache(data, cache_file)
return data


def get_qqq_holdings(csv_file_path):
df = pd.read_csv(csv_file_path)
return df["Holding Ticker"].tolist()


def main():
focused_stock = "TQQQ"
qqq_holdings = get_qqq_holdings(args.qqq_csv)
stocks = [focused_stock] + qqq_holdings
start_date = "2010-01-01"
end_date = "2024-01-01"
df = pd.DataFrame()
for stock in stocks:
data = get_asset_data(stock, start_date, end_date)
df[stock] = data["close"]

df_pct = df.pct_change()
df_cum_pct = (1 + df_pct).cumprod() - 1

fig, ax = plt.subplots(figsize=(16, 8))

# Set up the plot style
sns.set_style("whitegrid")
sns.set_palette("cool")

focused_color = "#1E90FF"
other_color = "gray"

# Plot other stocks
for stock in qqq_holdings:
ax.plot(
df_cum_pct.index,
df_cum_pct[stock] * 100,
label=stock if stock == qqq_holdings[0] else "",
color=other_color,
alpha=0.5,
linewidth=1,
)

# Plot focused stock
ax.plot(
df_cum_pct.index,
df_cum_pct[focused_stock] * 100,
linewidth=2,
label=focused_stock,
color=focused_color,
)

# Styling
ax.set_title(
f"Cumulative Stock Performance: {start_date} to {end_date}",
fontsize=20,
fontweight="bold",
pad=20,
)
ax.set_xlabel("Date", fontsize=14, labelpad=10)
ax.set_ylabel("Cumulative Percentage Change", fontsize=14, labelpad=10)

# Format x-axis
ax.xaxis.set_major_formatter(DateFormatter("%b %Y"))
plt.xticks(rotation=45)

# Format y-axis as percentage
ax.yaxis.set_major_formatter(
plt.FuncFormatter(lambda y, _: "{:.0%}".format(y / 100))
)

# Add horizontal line at 0%
ax.axhline(y=0, color="red", linestyle="--", linewidth=1, alpha=0.5)

# Customize grid
ax.grid(True, linestyle=":", alpha=0.2)

# Remove top and right spines
ax.spines["top"].set_visible(False)
ax.spines["right"].set_visible(False)

# Add annotations
focused_stock_final_value = df_cum_pct[focused_stock].iloc[-1] * 100
stocks_above_focused = 0
for stock in stocks:
final_value = df_cum_pct[stock].iloc[-1] * 100
if stock == focused_stock or final_value > focused_stock_final_value:
color = focused_color if stock == focused_stock else other_color
ax.annotate(
f"{stock}: {final_value:.2f}%",
xy=(df_cum_pct.index[-1], final_value),
xytext=(10, 10),
textcoords="offset points",
color=color,
fontweight="bold",
fontsize=10,
arrowprops=dict(arrowstyle="->", color=color),
)
if stock != focused_stock and final_value > focused_stock_final_value:
stocks_above_focused += 1

# Print the number of stocks with pct change greater than the focused stock
print(
f"Number of stocks with percentage change greater than {focused_stock}: {stocks_above_focused}"
)
plt.tight_layout()
plt.show()


if __name__ == "__main__":
args = parse_args()
setup_logging(args.verbose)
main()

0 comments on commit 6a28240

Please sign in to comment.