Investigating A VIX Trading Signal¶
Python Imports¶
In [1]:
# Standard Library
import datetime
import io
import os
import random
import sys
import warnings
from datetime import datetime, timedelta
from pathlib import Path
# Data Handling
import numpy as np
import pandas as pd
# Data Visualization
import matplotlib.dates as mdates
import matplotlib.pyplot as plt
import matplotlib.ticker as mtick
import seaborn as sns
from matplotlib.ticker import FormatStrFormatter, FuncFormatter, MultipleLocator
# Data Sources
import yfinance as yf
# Statistical Analysis
import statsmodels.api as sm
# Machine Learning
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
# Suppress warnings
warnings.filterwarnings("ignore")
Add Directories To Path¶
In [2]:
# Add the source subdirectory to the system path to allow import config from settings.py
current_directory = Path(os.getcwd())
website_base_directory = current_directory.parent.parent.parent
src_directory = website_base_directory / "src"
sys.path.append(str(src_directory)) if str(src_directory) not in sys.path else None
# Import settings.py
from settings import config
# Add configured directories from config to path
SOURCE_DIR = config("SOURCE_DIR")
sys.path.append(str(Path(SOURCE_DIR))) if str(Path(SOURCE_DIR)) not in sys.path else None
# Add other configured directories
BASE_DIR = config("BASE_DIR")
CONTENT_DIR = config("CONTENT_DIR")
POSTS_DIR = config("POSTS_DIR")
PAGES_DIR = config("PAGES_DIR")
PUBLIC_DIR = config("PUBLIC_DIR")
SOURCE_DIR = config("SOURCE_DIR")
DATA_DIR = config("DATA_DIR")
DATA_MANUAL_DIR = config("DATA_MANUAL_DIR")
# Print system path
for i, path in enumerate(sys.path):
print(f"{i}: {path}")
0: /usr/lib/python313.zip 1: /usr/lib/python3.13 2: /usr/lib/python3.13/lib-dynload 3: 4: /home/jared/python-virtual-envs/general_313/lib/python3.13/site-packages 5: /home/jared/Cloud_Storage/Dropbox/Websites/jaredszajkowski.github.io/src
Track Index Dependencies¶
In [3]:
# Create file to track markdown dependencies
dep_file = Path("index_dep.txt")
dep_file.write_text("")
Out[3]:
0
Python Functions¶
In [4]:
from calc_vix_trade_pnl import calc_vix_trade_pnl
from df_info import df_info
from df_info_markdown import df_info_markdown
from export_track_md_deps import export_track_md_deps
from load_data import load_data
from pandas_set_decimal_places import pandas_set_decimal_places
from plot_price import plot_price
from plot_stats import plot_stats
from plot_vix_with_trades import plot_vix_with_trades
from yf_pull_data import yf_pull_data
Data Overview (VIX)¶
Acquire CBOE Volatility Index (VIX) Data¶
In [5]:
yf_pull_data(
base_directory=DATA_DIR,
ticker="^VIX",
source="Yahoo_Finance",
asset_class="Indices",
excel_export=True,
pickle_export=True,
output_confirmation=True,
)
YF.download() has changed argument auto_adjust default to True
[*********************100%***********************] 1 of 1 completed
The first and last date of data for ^VIX is:
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 1990-01-02 | 17.24 | 17.24 | 17.24 | 17.24 | 0 |
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2025-10-07 | 17.24 | 17.549999 | 16.190001 | 16.58 | 0 |
Yahoo Finance data complete for ^VIX --------------------
Out[5]:
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 1990-01-02 | 17.240000 | 17.240000 | 17.240000 | 17.240000 | 0 |
| 1990-01-03 | 18.190001 | 18.190001 | 18.190001 | 18.190001 | 0 |
| 1990-01-04 | 19.219999 | 19.219999 | 19.219999 | 19.219999 | 0 |
| 1990-01-05 | 20.110001 | 20.110001 | 20.110001 | 20.110001 | 0 |
| 1990-01-08 | 20.260000 | 20.260000 | 20.260000 | 20.260000 | 0 |
| ... | ... | ... | ... | ... | ... |
| 2025-10-01 | 16.290001 | 17.280001 | 15.980000 | 17.280001 | 0 |
| 2025-10-02 | 16.629999 | 16.920000 | 15.930000 | 16.120001 | 0 |
| 2025-10-03 | 16.650000 | 17.059999 | 16.200001 | 16.350000 | 0 |
| 2025-10-06 | 16.370001 | 17.090000 | 16.290001 | 16.740000 | 0 |
| 2025-10-07 | 17.240000 | 17.549999 | 16.190001 | 16.580000 | 0 |
9008 rows × 5 columns
Load Data - VIX¶
In [6]:
# Set decimal places
pandas_set_decimal_places(2)
# VIX
vix = load_data(
base_directory=DATA_DIR,
ticker="^VIX",
source="Yahoo_Finance",
asset_class="Indices",
timeframe="Daily",
file_format="excel",
)
# Set 'Date' column as datetime
vix['Date'] = pd.to_datetime(vix['Date'])
# Drop 'Volume'
vix.drop(columns = {'Volume'}, inplace = True)
# Set Date as index
vix.set_index('Date', inplace = True)
# Check to see if there are any NaN values
vix[vix['High'].isna()]
# Forward fill to clean up missing data
vix['High'] = vix['High'].ffill()
DataFrame Info - VIX¶
In [7]:
df_info(vix)
The columns, shape, and data types are: <class 'pandas.core.frame.DataFrame'> DatetimeIndex: 9008 entries, 1990-01-02 to 2025-10-07 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Close 9008 non-null float64 1 High 9008 non-null float64 2 Low 9008 non-null float64 3 Open 9008 non-null float64 dtypes: float64(4) memory usage: 351.9 KB None The first 5 rows are:
| Close | High | Low | Open | |
|---|---|---|---|---|
| Date | ||||
| 1990-01-02 | 17.24 | 17.24 | 17.24 | 17.24 |
| 1990-01-03 | 18.19 | 18.19 | 18.19 | 18.19 |
| 1990-01-04 | 19.22 | 19.22 | 19.22 | 19.22 |
| 1990-01-05 | 20.11 | 20.11 | 20.11 | 20.11 |
| 1990-01-08 | 20.26 | 20.26 | 20.26 | 20.26 |
The last 5 rows are:
| Close | High | Low | Open | |
|---|---|---|---|---|
| Date | ||||
| 2025-10-01 | 16.29 | 17.28 | 15.98 | 17.28 |
| 2025-10-02 | 16.63 | 16.92 | 15.93 | 16.12 |
| 2025-10-03 | 16.65 | 17.06 | 16.20 | 16.35 |
| 2025-10-06 | 16.37 | 17.09 | 16.29 | 16.74 |
| 2025-10-07 | 17.24 | 17.55 | 16.19 | 16.58 |
In [8]:
# Copy this <!-- INSERT_01_VIX_DF_Info_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_DF_Info.md", content=df_info_markdown(vix))
✅ Exported and tracked: 01_VIX_DF_Info.md
Statistics - VIX¶
In [9]:
vix_stats = vix.describe()
num_std = range(-1, 6) # Adjusted to include -1 to 5
for num in num_std:
vix_stats.loc[f"mean + {num} std"] = {
'Open': vix_stats.loc['mean']['Open'] + num * vix_stats.loc['std']['Open'],
'High': vix_stats.loc['mean']['High'] + num * vix_stats.loc['std']['High'],
'Low': vix_stats.loc['mean']['Low'] + num * vix_stats.loc['std']['Low'],
'Close': vix_stats.loc['mean']['Close'] + num * vix_stats.loc['std']['Close'],
}
display(vix_stats)
| Close | High | Low | Open | |
|---|---|---|---|---|
| count | 9008.00 | 9008.00 | 9008.00 | 9008.00 |
| mean | 19.47 | 20.38 | 18.79 | 19.56 |
| std | 7.81 | 8.36 | 7.36 | 7.88 |
| min | 9.14 | 9.31 | 8.56 | 9.01 |
| 25% | 13.91 | 14.57 | 13.43 | 13.96 |
| 50% | 17.61 | 18.32 | 17.02 | 17.66 |
| 75% | 22.78 | 23.77 | 22.11 | 22.93 |
| max | 82.69 | 89.53 | 72.76 | 82.69 |
| mean + -1 std | 11.66 | 12.01 | 11.43 | 11.68 |
| mean + 0 std | 19.47 | 20.38 | 18.79 | 19.56 |
| mean + 1 std | 27.27 | 28.74 | 26.16 | 27.44 |
| mean + 2 std | 35.08 | 37.10 | 33.52 | 35.32 |
| mean + 3 std | 42.88 | 45.46 | 40.88 | 43.20 |
| mean + 4 std | 50.69 | 53.83 | 48.24 | 51.08 |
| mean + 5 std | 58.50 | 62.19 | 55.60 | 58.96 |
In [10]:
# Copy this <!-- INSERT_01_VIX_Stats_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Stats.md", content=vix_stats.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_VIX_Stats.md
In [11]:
# Group by year and calculate mean and std for OHLC
vix_stats_by_year = vix.groupby(vix.index.year)[["Open", "High", "Low", "Close"]].agg(["mean", "std" ,"min", "max"])
# Flatten the column MultiIndex
vix_stats_by_year.columns = ['_'.join(col).strip() for col in vix_stats_by_year.columns.values]
vix_stats_by_year.index.name = "Year"
display(vix_stats_by_year)
| Open_mean | Open_std | Open_min | Open_max | High_mean | High_std | High_min | High_max | Low_mean | Low_std | Low_min | Low_max | Close_mean | Close_std | Close_min | Close_max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Year | ||||||||||||||||
| 1990 | 23.06 | 4.74 | 14.72 | 36.47 | 23.06 | 4.74 | 14.72 | 36.47 | 23.06 | 4.74 | 14.72 | 36.47 | 23.06 | 4.74 | 14.72 | 36.47 |
| 1991 | 18.38 | 3.68 | 13.95 | 36.20 | 18.38 | 3.68 | 13.95 | 36.20 | 18.38 | 3.68 | 13.95 | 36.20 | 18.38 | 3.68 | 13.95 | 36.20 |
| 1992 | 15.23 | 2.26 | 10.29 | 20.67 | 16.03 | 2.19 | 11.90 | 25.13 | 14.85 | 2.14 | 10.29 | 19.67 | 15.45 | 2.12 | 11.51 | 21.02 |
| 1993 | 12.70 | 1.37 | 9.18 | 16.20 | 13.34 | 1.40 | 9.55 | 18.31 | 12.25 | 1.28 | 8.89 | 15.77 | 12.69 | 1.33 | 9.31 | 17.30 |
| 1994 | 13.79 | 2.06 | 9.86 | 23.61 | 14.58 | 2.28 | 10.31 | 28.30 | 13.38 | 1.99 | 9.59 | 23.61 | 13.93 | 2.07 | 9.94 | 23.87 |
| 1995 | 12.27 | 1.03 | 10.29 | 15.79 | 12.93 | 1.07 | 10.95 | 16.99 | 11.96 | 0.98 | 10.06 | 14.97 | 12.39 | 0.97 | 10.36 | 15.74 |
| 1996 | 16.31 | 1.92 | 11.24 | 23.90 | 16.99 | 2.12 | 12.29 | 27.05 | 15.94 | 1.82 | 11.11 | 21.43 | 16.44 | 1.94 | 12.00 | 21.99 |
| 1997 | 22.43 | 4.33 | 16.67 | 45.69 | 23.11 | 4.56 | 18.02 | 48.64 | 21.85 | 3.98 | 16.36 | 36.43 | 22.38 | 4.14 | 17.09 | 38.20 |
| 1998 | 25.68 | 6.96 | 16.42 | 47.95 | 26.61 | 7.36 | 16.50 | 49.53 | 24.89 | 6.58 | 16.10 | 45.58 | 25.60 | 6.86 | 16.23 | 45.74 |
| 1999 | 24.39 | 2.90 | 18.05 | 32.62 | 25.20 | 3.01 | 18.48 | 33.66 | 23.75 | 2.76 | 17.07 | 31.13 | 24.37 | 2.88 | 17.42 | 32.98 |
| 2000 | 23.41 | 3.43 | 16.81 | 33.70 | 24.10 | 3.66 | 17.06 | 34.31 | 22.75 | 3.19 | 16.28 | 30.56 | 23.32 | 3.41 | 16.53 | 33.49 |
| 2001 | 26.04 | 4.98 | 19.21 | 48.93 | 26.64 | 5.19 | 19.37 | 49.35 | 25.22 | 4.61 | 18.74 | 42.66 | 25.75 | 4.78 | 18.76 | 43.74 |
| 2002 | 27.53 | 7.03 | 17.23 | 48.17 | 28.28 | 7.25 | 17.51 | 48.46 | 26.60 | 6.64 | 17.02 | 42.05 | 27.29 | 6.91 | 17.40 | 45.08 |
| 2003 | 22.21 | 5.31 | 15.59 | 35.21 | 22.61 | 5.35 | 16.19 | 35.66 | 21.64 | 5.18 | 14.66 | 33.99 | 21.98 | 5.24 | 15.58 | 34.69 |
| 2004 | 15.59 | 1.93 | 11.41 | 21.06 | 16.05 | 2.02 | 11.64 | 22.67 | 15.05 | 1.79 | 11.14 | 20.61 | 15.48 | 1.92 | 11.23 | 21.58 |
| 2005 | 12.84 | 1.44 | 10.23 | 18.33 | 13.28 | 1.59 | 10.48 | 18.59 | 12.39 | 1.32 | 9.88 | 16.41 | 12.81 | 1.47 | 10.23 | 17.74 |
| 2006 | 12.90 | 2.18 | 9.68 | 23.45 | 13.33 | 2.46 | 10.06 | 23.81 | 12.38 | 1.96 | 9.39 | 21.45 | 12.81 | 2.25 | 9.90 | 23.81 |
| 2007 | 17.59 | 5.36 | 9.99 | 32.68 | 18.44 | 5.76 | 10.26 | 37.50 | 16.75 | 4.95 | 9.70 | 30.44 | 17.54 | 5.36 | 9.89 | 31.09 |
| 2008 | 32.83 | 16.41 | 16.30 | 80.74 | 34.57 | 17.83 | 17.84 | 89.53 | 30.96 | 14.96 | 15.82 | 72.76 | 32.69 | 16.38 | 16.30 | 80.86 |
| 2009 | 31.75 | 9.20 | 19.54 | 52.65 | 32.78 | 9.61 | 19.67 | 57.36 | 30.50 | 8.63 | 19.25 | 49.27 | 31.48 | 9.08 | 19.47 | 56.65 |
| 2010 | 22.73 | 5.29 | 15.44 | 47.66 | 23.69 | 5.82 | 16.00 | 48.20 | 21.69 | 4.61 | 15.23 | 40.30 | 22.55 | 5.27 | 15.45 | 45.79 |
| 2011 | 24.27 | 8.17 | 14.31 | 46.18 | 25.40 | 8.78 | 14.99 | 48.00 | 23.15 | 7.59 | 14.27 | 41.51 | 24.20 | 8.14 | 14.62 | 48.00 |
| 2012 | 17.93 | 2.60 | 13.68 | 26.35 | 18.59 | 2.72 | 14.08 | 27.73 | 17.21 | 2.37 | 13.30 | 25.72 | 17.80 | 2.54 | 13.45 | 26.66 |
| 2013 | 14.29 | 1.67 | 11.52 | 20.87 | 14.82 | 1.88 | 11.75 | 21.91 | 13.80 | 1.51 | 11.05 | 19.04 | 14.23 | 1.74 | 11.30 | 20.49 |
| 2014 | 14.23 | 2.65 | 10.40 | 29.26 | 14.95 | 3.02 | 10.76 | 31.06 | 13.61 | 2.21 | 10.28 | 24.64 | 14.17 | 2.62 | 10.32 | 25.27 |
| 2015 | 16.71 | 3.99 | 11.77 | 31.91 | 17.79 | 5.03 | 12.22 | 53.29 | 15.85 | 3.65 | 10.88 | 29.91 | 16.67 | 4.34 | 11.95 | 40.74 |
| 2016 | 16.01 | 4.05 | 11.32 | 29.01 | 16.85 | 4.40 | 11.49 | 32.09 | 15.16 | 3.66 | 10.93 | 26.67 | 15.83 | 3.97 | 11.27 | 28.14 |
| 2017 | 11.14 | 1.34 | 9.23 | 16.19 | 11.72 | 1.54 | 9.52 | 17.28 | 10.64 | 1.16 | 8.56 | 14.97 | 11.09 | 1.36 | 9.14 | 16.04 |
| 2018 | 16.63 | 5.01 | 9.01 | 37.32 | 18.03 | 6.12 | 9.31 | 50.30 | 15.53 | 4.25 | 8.92 | 29.66 | 16.64 | 5.09 | 9.15 | 37.32 |
| 2019 | 15.57 | 2.74 | 11.55 | 27.54 | 16.41 | 3.06 | 11.79 | 28.53 | 14.76 | 2.38 | 11.03 | 24.05 | 15.39 | 2.61 | 11.54 | 25.45 |
| 2020 | 29.52 | 12.45 | 12.20 | 82.69 | 31.46 | 13.89 | 12.42 | 85.47 | 27.50 | 10.85 | 11.75 | 70.37 | 29.25 | 12.34 | 12.10 | 82.69 |
| 2021 | 19.83 | 3.47 | 15.02 | 35.16 | 21.12 | 4.22 | 15.54 | 37.51 | 18.65 | 2.93 | 14.10 | 29.24 | 19.66 | 3.62 | 15.01 | 37.21 |
| 2022 | 25.98 | 4.30 | 16.57 | 37.50 | 27.25 | 4.59 | 17.81 | 38.94 | 24.69 | 3.91 | 16.34 | 33.11 | 25.62 | 4.22 | 16.60 | 36.45 |
| 2023 | 17.12 | 3.17 | 11.96 | 27.77 | 17.83 | 3.58 | 12.46 | 30.81 | 16.36 | 2.89 | 11.81 | 24.00 | 16.87 | 3.14 | 12.07 | 26.52 |
| 2024 | 15.69 | 3.14 | 11.53 | 33.71 | 16.65 | 4.73 | 12.23 | 65.73 | 14.92 | 2.58 | 10.62 | 24.02 | 15.61 | 3.36 | 11.86 | 38.57 |
| 2025 | 19.55 | 6.12 | 14.31 | 60.13 | 20.79 | 7.43 | 14.69 | 60.13 | 18.41 | 4.63 | 14.12 | 38.58 | 19.30 | 5.83 | 14.22 | 52.33 |
In [12]:
# Copy this <!-- INSERT_01_VIX_Stats_By_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Stats_By_Year.md", content=vix_stats_by_year.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_VIX_Stats_By_Year.md
In [13]:
# Group by month and calculate mean and std for OHLC
vix_stats_by_month = vix.groupby(vix.index.month)[["Open", "High", "Low", "Close"]].agg(["mean", "std", "min", "max"])
# Flatten the column MultiIndex
vix_stats_by_month.columns = ['_'.join(col).strip() for col in vix_stats_by_month.columns.values]
vix_stats_by_month.index.name = "Month"
display(vix_stats_by_month)
| Open_mean | Open_std | Open_min | Open_max | High_mean | High_std | High_min | High_max | Low_mean | Low_std | Low_min | Low_max | Close_mean | Close_std | Close_min | Close_max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Month | ||||||||||||||||
| 1 | 19.34 | 7.21 | 9.01 | 51.52 | 20.13 | 7.58 | 9.31 | 57.36 | 18.60 | 6.87 | 8.92 | 49.27 | 19.22 | 7.17 | 9.15 | 56.65 |
| 2 | 19.67 | 7.22 | 10.19 | 52.50 | 20.51 | 7.65 | 10.26 | 53.16 | 18.90 | 6.81 | 9.70 | 48.97 | 19.58 | 7.13 | 10.02 | 52.62 |
| 3 | 20.47 | 9.63 | 10.59 | 82.69 | 21.39 | 10.49 | 11.24 | 85.47 | 19.54 | 8.65 | 10.53 | 70.37 | 20.35 | 9.56 | 10.74 | 82.69 |
| 4 | 19.43 | 7.48 | 10.39 | 60.13 | 20.24 | 7.93 | 10.89 | 60.59 | 18.65 | 6.88 | 10.22 | 52.76 | 19.29 | 7.28 | 10.36 | 57.06 |
| 5 | 18.60 | 6.04 | 9.75 | 47.66 | 19.40 | 6.43 | 10.14 | 48.20 | 17.89 | 5.63 | 9.56 | 40.30 | 18.51 | 5.96 | 9.77 | 45.79 |
| 6 | 18.46 | 5.75 | 9.79 | 44.09 | 19.15 | 6.02 | 10.28 | 44.44 | 17.73 | 5.40 | 9.37 | 34.97 | 18.34 | 5.68 | 9.75 | 40.79 |
| 7 | 17.83 | 5.67 | 9.18 | 48.17 | 18.53 | 5.90 | 9.52 | 48.46 | 17.21 | 5.41 | 8.84 | 42.05 | 17.76 | 5.60 | 9.36 | 44.92 |
| 8 | 19.09 | 6.67 | 10.04 | 45.34 | 20.03 | 7.38 | 10.32 | 65.73 | 18.35 | 6.32 | 9.52 | 41.77 | 19.09 | 6.80 | 9.93 | 48.00 |
| 9 | 20.37 | 8.23 | 9.59 | 48.93 | 21.21 | 8.55 | 9.83 | 49.35 | 19.62 | 7.82 | 9.36 | 43.74 | 20.29 | 8.12 | 9.51 | 46.72 |
| 10 | 21.80 | 10.25 | 9.23 | 79.13 | 22.79 | 11.08 | 9.62 | 89.53 | 20.90 | 9.49 | 9.11 | 67.80 | 21.71 | 10.22 | 9.19 | 80.06 |
| 11 | 20.34 | 9.65 | 9.31 | 80.74 | 21.04 | 10.03 | 9.74 | 81.48 | 19.55 | 9.02 | 8.56 | 72.76 | 20.16 | 9.52 | 9.14 | 80.86 |
| 12 | 19.34 | 8.26 | 9.36 | 66.68 | 20.09 | 8.53 | 9.55 | 68.60 | 18.63 | 7.88 | 8.89 | 62.31 | 19.29 | 8.16 | 9.31 | 68.51 |
In [14]:
# Copy this <!-- INSERT_01_VIX_Stats_By_Month_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Stats_By_Month.md", content=vix_stats_by_month.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_VIX_Stats_By_Month.md
Deciles - VIX¶
In [15]:
vix_deciles = vix.quantile(np.arange(0, 1.1, 0.1))
display(vix_deciles)
| Close | High | Low | Open | |
|---|---|---|---|---|
| 0.00 | 9.14 | 9.31 | 8.56 | 9.01 |
| 0.10 | 12.13 | 12.65 | 11.73 | 12.15 |
| 0.20 | 13.29 | 13.89 | 12.88 | 13.34 |
| 0.30 | 14.64 | 15.33 | 14.14 | 14.72 |
| 0.40 | 16.10 | 16.76 | 15.56 | 16.13 |
| 0.50 | 17.61 | 18.32 | 17.02 | 17.66 |
| 0.60 | 19.49 | 20.34 | 18.95 | 19.62 |
| 0.70 | 21.57 | 22.57 | 20.92 | 21.70 |
| 0.80 | 24.26 | 25.25 | 23.42 | 24.34 |
| 0.90 | 28.66 | 29.95 | 27.73 | 28.81 |
| 1.00 | 82.69 | 89.53 | 72.76 | 82.69 |
In [16]:
# Copy this <!-- INSERT_01_VIX_Deciles_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Deciles.md", content=vix_deciles.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_VIX_Deciles.md
In [17]:
# Group by year for deciles
vix_deciles_by_year = vix.groupby(vix.index.year)[["Open", "High", "Low", "Close"]].quantile(np.arange(0, 1.1, 0.1))
display(vix_deciles_by_year)
| Open | High | Low | Close | ||
|---|---|---|---|---|---|
| Date | |||||
| 1990 | 0.00 | 14.72 | 14.72 | 14.72 | 14.72 |
| 0.10 | 17.18 | 17.18 | 17.18 | 17.18 | |
| 0.20 | 18.47 | 18.47 | 18.47 | 18.47 | |
| 0.30 | 20.08 | 20.08 | 20.08 | 20.08 | |
| 0.40 | 21.15 | 21.15 | 21.15 | 21.15 | |
| ... | ... | ... | ... | ... | ... |
| 2025 | 0.60 | 18.29 | 19.38 | 17.51 | 18.14 |
| 0.70 | 19.78 | 21.07 | 18.67 | 19.31 | |
| 0.80 | 22.06 | 23.52 | 20.41 | 21.77 | |
| 0.90 | 24.92 | 26.93 | 24.23 | 24.84 | |
| 1.00 | 60.13 | 60.13 | 38.58 | 52.33 |
396 rows × 4 columns
In [18]:
# Copy this <!-- INSERT_01_VIX_Deciles_By_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Deciles_By_Year.md", content=vix_deciles_by_year.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_VIX_Deciles_By_Year.md
In [19]:
current_year = datetime.now().year
last_year = current_year - 1
print(f"Last year: {last_year}")
vix_deciles_last_year = vix_deciles_by_year.loc[last_year]
display(vix_deciles_last_year)
print(f"Current year: {current_year}")
vix_deciles_current_year = vix_deciles_by_year.loc[current_year]
display(vix_deciles_current_year)
Last year: 2024
| Open | High | Low | Close | |
|---|---|---|---|---|
| 0.00 | 11.53 | 12.23 | 10.62 | 11.86 |
| 0.10 | 12.79 | 13.13 | 12.36 | 12.63 |
| 0.20 | 13.20 | 13.56 | 12.84 | 13.06 |
| 0.30 | 13.68 | 14.10 | 13.26 | 13.45 |
| 0.40 | 14.17 | 14.62 | 13.61 | 14.03 |
| 0.50 | 14.90 | 15.41 | 14.13 | 14.63 |
| 0.60 | 15.47 | 16.47 | 14.86 | 15.38 |
| 0.70 | 16.33 | 17.47 | 15.56 | 16.31 |
| 0.80 | 17.61 | 19.30 | 16.68 | 18.02 |
| 0.90 | 20.44 | 21.14 | 18.91 | 19.89 |
| 1.00 | 33.71 | 65.73 | 24.02 | 38.57 |
Current year: 2025
| Open | High | Low | Close | |
|---|---|---|---|---|
| 0.00 | 14.31 | 14.69 | 14.12 | 14.22 |
| 0.10 | 15.19 | 15.82 | 14.74 | 15.10 |
| 0.20 | 15.87 | 16.38 | 15.28 | 15.69 |
| 0.30 | 16.41 | 16.99 | 15.75 | 16.28 |
| 0.40 | 16.77 | 17.44 | 16.20 | 16.63 |
| 0.50 | 17.54 | 18.35 | 16.68 | 17.21 |
| 0.60 | 18.29 | 19.38 | 17.51 | 18.14 |
| 0.70 | 19.78 | 21.07 | 18.67 | 19.31 |
| 0.80 | 22.06 | 23.52 | 20.41 | 21.77 |
| 0.90 | 24.92 | 26.93 | 24.23 | 24.84 |
| 1.00 | 60.13 | 60.13 | 38.58 | 52.33 |
In [20]:
# Copy this <!-- INSERT_01_Last_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_Last_Year.md", content=f"{last_year}")
# Copy this <!-- INSERT_01_VIX_Deciles_Last_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Deciles_Last_Year.md", content=vix_deciles_last_year.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_Last_Year.md ✅ Exported and tracked: 01_VIX_Deciles_Last_Year.md
In [21]:
# Copy this <!-- INSERT_01_Current_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_Current_Year.md", content=f"{current_year}")
# Copy this <!-- INSERT_01_VIX_Deciles_Current_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="01_VIX_Deciles_Current_Year.md", content=vix_deciles_current_year.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 01_Current_Year.md ✅ Exported and tracked: 01_VIX_Deciles_Current_Year.md
Time Between Levels¶
In [22]:
import pandas as pd
import math
from typing import Literal
Op = Literal["==", ">=", ">", "<=", "<"]
def compare(value: float, threshold: float, op: Op) -> bool:
if op == "==":
return value == threshold
if op == ">=":
return value >= threshold
if op == ">":
return value > threshold
if op == "<=":
return value <= threshold
if op == "<":
return value < threshold
raise ValueError(f"Unsupported op: {op}")
def compute_waits(
df: pd.DataFrame,
high_col: str = "High",
trigger_a: float = 20.0,
trigger_b: float = 20.0,
op_a: Op = ">=",
op_b: Op = ">=",
strictly_after: bool = True,
) -> pd.DataFrame:
"""
For each day i where df[high_col] op_a trigger_a, find the next day j
(j > i if strictly_after else j >= i) where df[high_col] op_b trigger_b.
"""
df = df.sort_index()
idx = df.index
highs = df[high_col].values
n = len(df)
rows = []
for i in range(n):
if compare(highs[i], trigger_a, op_a):
start_j = i + 1 if strictly_after else i
j_found = None
for j in range(start_j, n):
if compare(highs[j], trigger_b, op_b):
j_found = j
break
if j_found is None:
next_date = pd.NaT if isinstance(idx, pd.DatetimeIndex) else None
next_high = math.nan
wait_td = math.nan
wait_cd = math.nan
else:
next_date = idx[j_found]
next_high = float(highs[j_found])
wait_td = j_found - i
if isinstance(idx, pd.DatetimeIndex):
wait_cd = (idx[j_found].normalize() - idx[i].normalize()).days
else:
wait_cd = math.nan
rows.append(
{
"date_a": idx[i],
"high_at_a": float(highs[i]),
"date_b": next_date,
"high_at_b": next_high,
"wait_trading_days": wait_td,
"wait_calendar_days": wait_cd,
}
)
return pd.DataFrame(rows)
In [23]:
vix.tail(20)
Out[23]:
| Close | High | Low | Open | |
|---|---|---|---|---|
| Date | ||||
| 2025-09-10 | 15.35 | 15.63 | 14.63 | 14.98 |
| 2025-09-11 | 14.71 | 15.24 | 14.65 | 15.19 |
| 2025-09-12 | 14.76 | 14.97 | 14.41 | 14.62 |
| 2025-09-15 | 15.69 | 15.84 | 14.92 | 15.14 |
| 2025-09-16 | 16.36 | 16.38 | 15.44 | 15.60 |
| 2025-09-17 | 15.72 | 16.75 | 15.26 | 16.42 |
| 2025-09-18 | 15.70 | 15.80 | 14.33 | 14.93 |
| 2025-09-19 | 15.45 | 16.13 | 15.29 | 15.76 |
| 2025-09-22 | 16.10 | 16.35 | 15.78 | 16.14 |
| 2025-09-23 | 16.64 | 17.07 | 15.95 | 16.10 |
| 2025-09-24 | 16.18 | 17.21 | 16.18 | 16.57 |
| 2025-09-25 | 16.74 | 17.74 | 16.32 | 16.55 |
| 2025-09-26 | 15.29 | 17.05 | 15.29 | 16.89 |
| 2025-09-29 | 16.12 | 16.29 | 15.74 | 15.84 |
| 2025-09-30 | 16.28 | 16.70 | 16.02 | 16.49 |
| 2025-10-01 | 16.29 | 17.28 | 15.98 | 17.28 |
| 2025-10-02 | 16.63 | 16.92 | 15.93 | 16.12 |
| 2025-10-03 | 16.65 | 17.06 | 16.20 | 16.35 |
| 2025-10-06 | 16.37 | 17.09 | 16.29 | 16.74 |
| 2025-10-07 | 17.24 | 17.55 | 16.19 | 16.58 |
In [24]:
# When daily high <= 15, how long until next daily high >= 20?
res_lt15_to_gt20 = compute_waits(
df=vix,
high_col="High",
trigger_a=15,
trigger_b=20,
op_a="<=",
op_b=">=",
strictly_after=True,
)
In [25]:
res_lt15_to_gt20
Out[25]:
| date_a | high_at_a | date_b | high_at_b | wait_trading_days | wait_calendar_days | |
|---|---|---|---|---|---|---|
| 0 | 1990-06-21 | 14.72 | 1990-07-23 | 23.68 | 21.00 | 32.00 |
| 1 | 1991-03-14 | 14.94 | 1991-04-09 | 20.12 | 17.00 | 26.00 |
| 2 | 1991-03-15 | 14.90 | 1991-04-09 | 20.12 | 16.00 | 25.00 |
| 3 | 1991-08-13 | 14.73 | 1991-08-19 | 21.19 | 4.00 | 6.00 |
| 4 | 1991-08-22 | 14.59 | 1991-11-15 | 21.18 | 60.00 | 85.00 |
| ... | ... | ... | ... | ... | ... | ... |
| 2526 | 2024-12-13 | 14.25 | 2024-12-18 | 28.32 | 3.00 | 5.00 |
| 2527 | 2024-12-16 | 14.69 | 2024-12-18 | 28.32 | 2.00 | 2.00 |
| 2528 | 2025-08-13 | 14.81 | NaT | NaN | NaN | NaN |
| 2529 | 2025-08-28 | 14.69 | NaT | NaN | NaN | NaN |
| 2530 | 2025-09-12 | 14.97 | NaT | NaN | NaN | NaN |
2531 rows × 6 columns
In [26]:
# Plot histogram for wait times
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.hist(res_lt15_to_gt20['wait_trading_days'].dropna(), bins=200, alpha=0.5, label='LT 15 to GT 20')
plt.xlabel('Days')
plt.ylabel('Frequency')
plt.title('Wait Times for VIX Highs')
plt.legend()
plt.show()
In [27]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from typing import Optional, Tuple
def compute_wait_cdf(
waits_df: pd.DataFrame,
column: str = "wait_trading_days",
) -> pd.DataFrame:
"""
Compute the empirical CDF for wait times.
Returns a DataFrame with:
- 'wait': unique wait values (sorted)
- 'count': frequency for each wait
- 'cdf': cumulative probability P(Wait <= x)
- 'ccdf': complementary CDF = 1 - cdf (P(Wait > x))
"""
# Drop NaNs; don't cast to int (keep original type)
waits = waits_df[column].dropna().to_numpy()
if waits.size == 0:
return pd.DataFrame(columns=["wait", "count", "cdf", "ccdf"])
# Unique values and counts
vals, counts = np.unique(waits, return_counts=True)
cum_counts = np.cumsum(counts)
n = waits.size
cdf = cum_counts / n
ccdf = 1.0 - cdf
out = pd.DataFrame(
{"wait": vals, "count": counts, "cdf": cdf, "ccdf": ccdf}
)
return out
# def plot_wait_cdf(
# cdf_df: pd.DataFrame,
# title: str = "CDF of Wait Times",
# show_ccdf: bool = False,
# ax: Optional[plt.Axes] = None,
# ) -> Tuple[plt.Axes, Optional[plt.Axes]]:
# """
# Plot the empirical CDF (and optionally the complementary CDF).
# Returns the axis (or axes).
# """
# if cdf_df.empty:
# raise ValueError("CDF DataFrame is empty; nothing to plot.")
# if ax is None:
# fig, ax = plt.subplots(figsize=(7, 4))
# # Right-continuous step (ECDF)
# ax.step(cdf_df["wait"], cdf_df["cdf"], where="post")
# ax.set_xlabel("Wait (days)")
# ax.set_ylabel("Cumulative probability (CDF)")
# ax.set_title(title)
# ax.grid(True)
# ax_ccdf = None
# if show_ccdf:
# fig2, ax_ccdf = plt.subplots(figsize=(7, 4))
# ax_ccdf.step(cdf_df["wait"], cdf_df["ccdf"], where="post")
# ax_ccdf.set_xlabel("Wait (days)")
# ax_ccdf.set_ylabel("Tail probability (1 - CDF)")
# ax_ccdf.set_title(f"Complementary CDF — {title}")
# ax_ccdf.grid(True)
# return ax, ax_ccdf
In [28]:
cdf_df = compute_wait_cdf(
waits_df=res_lt15_to_gt20,
column="wait_trading_days")
cdf_df
Out[28]:
| wait | count | cdf | ccdf | |
|---|---|---|---|---|
| 0 | 1.00 | 1 | 0.00 | 1.00 |
| 1 | 2.00 | 7 | 0.00 | 1.00 |
| 2 | 3.00 | 11 | 0.01 | 0.99 |
| 3 | 4.00 | 15 | 0.01 | 0.99 |
| 4 | 5.00 | 15 | 0.02 | 0.98 |
| ... | ... | ... | ... | ... |
| 461 | 472.00 | 1 | 1.00 | 0.00 |
| 462 | 478.00 | 1 | 1.00 | 0.00 |
| 463 | 479.00 | 1 | 1.00 | 0.00 |
| 464 | 493.00 | 1 | 1.00 | 0.00 |
| 465 | 494.00 | 1 | 1.00 | 0.00 |
466 rows × 4 columns
In [29]:
# Plot CDF
plt.figure(figsize=(12, 6))
plt.plot(cdf_df['wait'], cdf_df['cdf'])
plt.title('CDF of LT 15 to GT 20')
plt.xlabel('Wait')
plt.ylabel('CDF')
plt.grid()
plt.show()
In [30]:
import pandas as pd
import numpy as np
def empirical_hit_probabilities(
df: pd.DataFrame,
high_col: str,
thresholds,
horizons,
) -> pd.DataFrame:
"""
Compute empirical probability of reaching a threshold high within given horizons.
Parameters
----------
df : pd.DataFrame
Sorted daily data with a High column.
high_col : str
Name of the column with daily highs.
thresholds : list of floats
Target levels (e.g. [18,19,20,21,22]).
horizons : list of ints
Lookahead windows in trading days.
Returns
-------
pd.DataFrame
Rows = horizons, Cols = thresholds, entries = probability [0,1].
"""
df = df.sort_index()
highs = df[high_col].values
n = len(highs)
probs = pd.DataFrame(index=horizons, columns=thresholds, dtype=float)
for h in horizons:
valid_starts = n - h # last h days have incomplete windows
if valid_starts <= 0:
continue
window_max = np.array([highs[i+1:i+h+1].max() for i in range(valid_starts)])
# note: exclude same day (i+1:...) so it's "future" only
for thr in thresholds:
hits = (window_max >= thr).sum()
probs.loc[h, thr] = hits / valid_starts
return probs
In [31]:
probs = empirical_hit_probabilities(
df=vix,
high_col="High",
thresholds=range(15, 30),
horizons=range(5, 101,5),
)
display(probs)
| 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 0.78 | 0.73 | 0.66 | 0.59 | 0.54 | 0.49 | 0.44 | 0.39 | 0.35 | 0.30 | 0.26 | 0.23 | 0.20 | 0.18 | 0.16 |
| 10 | 0.83 | 0.78 | 0.71 | 0.64 | 0.59 | 0.54 | 0.49 | 0.43 | 0.40 | 0.35 | 0.30 | 0.27 | 0.25 | 0.22 | 0.20 |
| 15 | 0.85 | 0.81 | 0.75 | 0.68 | 0.62 | 0.57 | 0.53 | 0.47 | 0.43 | 0.38 | 0.34 | 0.31 | 0.28 | 0.25 | 0.22 |
| 20 | 0.87 | 0.83 | 0.77 | 0.71 | 0.65 | 0.60 | 0.56 | 0.49 | 0.46 | 0.41 | 0.37 | 0.33 | 0.31 | 0.28 | 0.25 |
| 25 | 0.89 | 0.85 | 0.79 | 0.74 | 0.68 | 0.63 | 0.59 | 0.52 | 0.49 | 0.43 | 0.39 | 0.36 | 0.33 | 0.31 | 0.27 |
| 30 | 0.91 | 0.87 | 0.81 | 0.75 | 0.70 | 0.65 | 0.61 | 0.54 | 0.51 | 0.45 | 0.41 | 0.38 | 0.36 | 0.33 | 0.29 |
| 35 | 0.92 | 0.89 | 0.83 | 0.77 | 0.72 | 0.67 | 0.63 | 0.56 | 0.53 | 0.47 | 0.43 | 0.40 | 0.38 | 0.35 | 0.31 |
| 40 | 0.93 | 0.90 | 0.84 | 0.79 | 0.74 | 0.69 | 0.65 | 0.58 | 0.55 | 0.49 | 0.45 | 0.42 | 0.40 | 0.37 | 0.33 |
| 45 | 0.93 | 0.91 | 0.85 | 0.80 | 0.75 | 0.71 | 0.67 | 0.59 | 0.56 | 0.50 | 0.47 | 0.44 | 0.42 | 0.39 | 0.34 |
| 50 | 0.94 | 0.92 | 0.86 | 0.81 | 0.77 | 0.72 | 0.69 | 0.61 | 0.58 | 0.51 | 0.48 | 0.45 | 0.43 | 0.40 | 0.36 |
| 55 | 0.95 | 0.92 | 0.87 | 0.82 | 0.78 | 0.74 | 0.71 | 0.62 | 0.59 | 0.53 | 0.50 | 0.47 | 0.45 | 0.41 | 0.37 |
| 60 | 0.95 | 0.93 | 0.88 | 0.83 | 0.79 | 0.75 | 0.72 | 0.64 | 0.61 | 0.54 | 0.51 | 0.48 | 0.46 | 0.43 | 0.39 |
| 65 | 0.96 | 0.94 | 0.89 | 0.84 | 0.81 | 0.76 | 0.74 | 0.65 | 0.62 | 0.55 | 0.52 | 0.50 | 0.48 | 0.44 | 0.40 |
| 70 | 0.96 | 0.94 | 0.90 | 0.85 | 0.82 | 0.77 | 0.75 | 0.66 | 0.63 | 0.56 | 0.53 | 0.51 | 0.49 | 0.45 | 0.41 |
| 75 | 0.97 | 0.95 | 0.91 | 0.86 | 0.83 | 0.78 | 0.76 | 0.67 | 0.64 | 0.57 | 0.54 | 0.52 | 0.50 | 0.46 | 0.42 |
| 80 | 0.97 | 0.95 | 0.91 | 0.86 | 0.83 | 0.79 | 0.77 | 0.68 | 0.66 | 0.58 | 0.55 | 0.53 | 0.51 | 0.47 | 0.43 |
| 85 | 0.97 | 0.96 | 0.92 | 0.87 | 0.84 | 0.80 | 0.78 | 0.69 | 0.67 | 0.59 | 0.57 | 0.54 | 0.53 | 0.49 | 0.45 |
| 90 | 0.98 | 0.96 | 0.92 | 0.88 | 0.85 | 0.81 | 0.80 | 0.70 | 0.67 | 0.60 | 0.57 | 0.55 | 0.54 | 0.50 | 0.46 |
| 95 | 0.98 | 0.97 | 0.93 | 0.88 | 0.85 | 0.82 | 0.80 | 0.71 | 0.68 | 0.60 | 0.58 | 0.56 | 0.55 | 0.51 | 0.47 |
| 100 | 0.98 | 0.97 | 0.93 | 0.89 | 0.86 | 0.83 | 0.81 | 0.71 | 0.69 | 0.61 | 0.59 | 0.57 | 0.55 | 0.51 | 0.48 |
In [32]:
import pandas as pd
import numpy as np
def conditional_hit_probabilities(
df: pd.DataFrame,
today_high: float,
high_col: str,
thresholds,
horizons,
tolerance: float, # how close history must be to today's high
) -> pd.DataFrame:
"""
Conditional probability of hitting thresholds within horizons,
given today's high is near `today_high`.
Parameters
----------
df : pd.DataFrame
Daily data with highs.
today_high : float
Today's observed high.
high_col : str
Column containing the daily highs.
thresholds : list of floats
Target levels to evaluate (e.g., [18,19,20,...]).
horizons : list of ints
Lookahead windows (in trading days).
tolerance : float
Acceptable deviation from today's high when finding historical analogues.
Returns
-------
pd.DataFrame
Probabilities indexed by horizon (rows) and thresholds (columns).
"""
df = df.sort_index()
highs = df[high_col].values
n = len(highs)
# Find indices where the high ~ today's high
candidates = np.where((highs >= today_high - tolerance) & (highs <= today_high + tolerance))[0]
if len(candidates) == 0:
raise ValueError("No historical days found within tolerance of today's high")
probs = pd.DataFrame(index=horizons, columns=thresholds, dtype=float)
for h in horizons:
valid_hits = 0
total = 0
for i in candidates:
if i + h < n: # need full window
window_max = highs[i+1:i+h+1].max() # strictly future days
for thr in thresholds:
if np.isnan(probs.loc[h, thr]):
probs.loc[h, thr] = 0.0
if window_max >= thr:
probs.loc[h, thr] += 1
total += 1
if total > 0:
probs.loc[h] /= total # normalize to probability
else:
probs.loc[h] = np.nan # no valid examples
return probs
In [33]:
# Get yesterday's high as an example
yesterday = vix.iloc[-2]
yesterday_high = vix['High'].iloc[-2]
display(yesterday)
display(yesterday_high)
cond_probs = conditional_hit_probabilities(
df=vix,
today_high=yesterday_high,
high_col="High",
thresholds=range(15, 31),
horizons=range(5, 71, 5),
tolerance=0.25
)
display(cond_probs)
Close 16.37 High 17.09 Low 16.29 Open 16.74 Name: 2025-10-06 00:00:00, dtype: float64
np.float64(17.09000015258789)
| 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 5 | 0.99 | 0.96 | 0.79 | 0.47 | 0.29 | 0.19 | 0.13 | 0.05 | 0.04 | 0.03 | 0.02 | 0.02 | 0.01 | 0.01 | 0.01 | 0.01 |
| 10 | 0.99 | 0.98 | 0.85 | 0.64 | 0.44 | 0.32 | 0.26 | 0.14 | 0.12 | 0.07 | 0.06 | 0.06 | 0.03 | 0.03 | 0.02 | 0.02 |
| 15 | 1.00 | 0.98 | 0.91 | 0.73 | 0.52 | 0.40 | 0.33 | 0.19 | 0.17 | 0.11 | 0.09 | 0.08 | 0.06 | 0.06 | 0.04 | 0.03 |
| 20 | 1.00 | 0.98 | 0.94 | 0.76 | 0.58 | 0.45 | 0.38 | 0.24 | 0.21 | 0.16 | 0.14 | 0.12 | 0.10 | 0.09 | 0.06 | 0.06 |
| 25 | 1.00 | 0.98 | 0.93 | 0.81 | 0.64 | 0.49 | 0.43 | 0.28 | 0.25 | 0.20 | 0.19 | 0.17 | 0.14 | 0.12 | 0.09 | 0.08 |
| 30 | 1.00 | 0.98 | 0.94 | 0.84 | 0.69 | 0.55 | 0.47 | 0.31 | 0.29 | 0.23 | 0.22 | 0.20 | 0.18 | 0.15 | 0.10 | 0.09 |
| 35 | 1.00 | 0.99 | 0.96 | 0.86 | 0.72 | 0.60 | 0.50 | 0.35 | 0.32 | 0.26 | 0.25 | 0.23 | 0.21 | 0.18 | 0.14 | 0.13 |
| 40 | 1.00 | 0.99 | 0.96 | 0.87 | 0.75 | 0.63 | 0.53 | 0.37 | 0.35 | 0.29 | 0.28 | 0.26 | 0.23 | 0.19 | 0.14 | 0.14 |
| 45 | 1.00 | 0.99 | 0.96 | 0.90 | 0.79 | 0.67 | 0.58 | 0.40 | 0.38 | 0.30 | 0.29 | 0.27 | 0.24 | 0.20 | 0.16 | 0.15 |
| 50 | 1.00 | 1.00 | 0.97 | 0.90 | 0.82 | 0.70 | 0.62 | 0.45 | 0.41 | 0.33 | 0.32 | 0.30 | 0.28 | 0.23 | 0.18 | 0.17 |
| 55 | 1.00 | 1.00 | 0.97 | 0.90 | 0.83 | 0.73 | 0.65 | 0.47 | 0.43 | 0.35 | 0.34 | 0.32 | 0.30 | 0.24 | 0.20 | 0.19 |
| 60 | 1.00 | 1.00 | 0.97 | 0.90 | 0.85 | 0.74 | 0.66 | 0.48 | 0.44 | 0.36 | 0.35 | 0.33 | 0.32 | 0.26 | 0.21 | 0.21 |
| 65 | 1.00 | 1.00 | 0.97 | 0.92 | 0.86 | 0.76 | 0.68 | 0.50 | 0.45 | 0.38 | 0.37 | 0.36 | 0.34 | 0.28 | 0.23 | 0.23 |
| 70 | 1.00 | 1.00 | 0.97 | 0.93 | 0.88 | 0.77 | 0.70 | 0.51 | 0.45 | 0.38 | 0.37 | 0.37 | 0.35 | 0.29 | 0.23 | 0.23 |
Plots - VIX¶
Histogram Distribution - VIX¶
In [34]:
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Histogram
plt.hist([vix['High']], label=['High'], bins=200, edgecolor='black', color='steelblue')
# Plot a vertical line at the mean, mean + 1 std, and mean + 2 std
plt.axvline(vix_stats.loc['mean + -1 std']['High'], color='brown', linestyle='dashed', linewidth=1, label=f'High Mean - 1 std: {vix_stats.loc['mean + -1 std']['High']:.2f}')
plt.axvline(vix_stats.loc['mean']['High'], color='red', linestyle='dashed', linewidth=1, label=f'High Mean: {vix_stats.loc['mean']['High']:.2f}')
plt.axvline(vix_stats.loc['mean + 1 std']['High'], color='green', linestyle='dashed', linewidth=1, label=f'High Mean + 1 std: {vix_stats.loc['mean + 1 std']['High']:.2f}')
plt.axvline(vix_stats.loc['mean + 2 std']['High'], color='orange', linestyle='dashed', linewidth=1, label=f'High Mean + 2 std: {vix_stats.loc['mean + 2 std']['High']:.2f}')
plt.axvline(vix_stats.loc['mean + 3 std']['High'], color='black', linestyle='dashed', linewidth=1, label=f'High Mean + 3 std: {vix_stats.loc['mean + 3 std']['High']:.2f}')
plt.axvline(vix_stats.loc['mean + 4 std']['High'], color='yellow', linestyle='dashed', linewidth=1, label=f'High Mean + 4 std: {vix_stats.loc['mean + 4 std']['High']:.2f}')
# Set X axis
x_tick_spacing = 5 # Specify the interval for y-axis ticks
plt.gca().xaxis.set_major_locator(MultipleLocator(x_tick_spacing))
plt.xlabel("VIX", fontsize=10)
plt.xticks(rotation=0, fontsize=8)
# Set Y axis
y_tick_spacing = 25 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("# Of Datapoints", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title("CBOE Volatility Index (VIX) Histogram (200 Bins)", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig("01_Histogram+Mean+SD.png", dpi=300, bbox_inches="tight")
plt.show()
Historical Data - VIX¶
In [35]:
plot_price(
price_df=vix,
plot_start_date=None,
plot_end_date="2009-12-31",
plot_columns=["High", "Low"],
title="CBOE Volatility Index (VIX), 1990 - 2009",
x_label="Date",
x_format="Year",
y_label="VIX",
y_format="Decimal",
y_tick_spacing=5,
grid=True,
legend=True,
export_plot=True,
plot_file_name="01_VIX_Plot_1990-2009",
)
In [36]:
plot_price(
price_df=vix,
plot_start_date="2010-01-01",
plot_end_date=None,
plot_columns=["High", "Low"],
title="CBOE Volatility Index (VIX), 2010 - Present",
x_label="Date",
x_format="Year",
y_label="VIX",
y_format="Decimal",
y_tick_spacing=5,
grid=True,
legend=True,
export_plot=True,
plot_file_name="01_VIX_Plot_2010-Present",
)
Stats By Year - VIX¶
In [37]:
plot_stats(
stats_df=vix_stats_by_year,
plot_columns=["Open_mean", "High_mean", "Low_mean", "Close_mean"],
title="VIX Mean OHLC By Year",
x_label="Year",
x_rotation=45,
x_tick_spacing=1,
y_label="Price",
y_tick_spacing=1,
grid=True,
legend=True,
export_plot=True,
plot_file_name="01_VIX_Stats_By_Year"
)
Stats By Month - VIX¶
In [38]:
plot_stats(
stats_df=vix_stats_by_month,
plot_columns=["Open_mean", "High_mean", "Low_mean", "Close_mean"],
title="VIX Mean OHLC By Month",
x_label="Month",
x_rotation=0,
x_tick_spacing=1,
y_label="Price",
y_tick_spacing=1,
grid=True,
legend=True,
export_plot=True,
plot_file_name="01_VIX_Stats_By_Month"
)
Data Overview (VVIX)¶
Acquire CBOE VVIX Data¶
In [39]:
yf_pull_data(
base_directory=DATA_DIR,
ticker="^VVIX",
source="Yahoo_Finance",
asset_class="Indices",
excel_export=True,
pickle_export=True,
output_confirmation=True,
)
[*********************100%***********************] 1 of 1 completed
The first and last date of data for ^VVIX is:
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2007-01-03 | 87.63 | 87.63 | 87.63 | 87.63 | 0 |
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2025-10-06 | 95.38 | 98.49 | 95.35 | 96.92 | 0 |
Yahoo Finance data complete for ^VVIX --------------------
Out[39]:
| Close | High | Low | Open | Volume | |
|---|---|---|---|---|---|
| Date | |||||
| 2007-01-03 | 87.63 | 87.63 | 87.63 | 87.63 | 0 |
| 2007-01-04 | 88.19 | 88.19 | 88.19 | 88.19 | 0 |
| 2007-01-05 | 90.17 | 90.17 | 90.17 | 90.17 | 0 |
| 2007-01-08 | 92.04 | 92.04 | 92.04 | 92.04 | 0 |
| 2007-01-09 | 92.76 | 92.76 | 92.76 | 92.76 | 0 |
| ... | ... | ... | ... | ... | ... |
| 2025-09-30 | 95.89 | 98.29 | 94.78 | 97.59 | 0 |
| 2025-10-01 | 93.22 | 96.60 | 92.36 | 96.60 | 0 |
| 2025-10-02 | 94.10 | 94.32 | 91.96 | 92.42 | 0 |
| 2025-10-03 | 94.79 | 95.86 | 93.45 | 93.82 | 0 |
| 2025-10-06 | 95.38 | 98.49 | 95.35 | 96.92 | 0 |
4711 rows × 5 columns
Load Data - VVIX¶
In [40]:
# Set decimal places
pandas_set_decimal_places(2)
# VVIX
vvix = load_data(
base_directory=DATA_DIR,
ticker="^VVIX",
source="Yahoo_Finance",
asset_class="Indices",
timeframe="Daily",
file_format="excel",
)
# Set 'Date' column as datetime
vvix['Date'] = pd.to_datetime(vvix['Date'])
# Drop 'Volume'
vvix.drop(columns = {'Volume'}, inplace = True)
# Set Date as index
vvix.set_index('Date', inplace = True)
# Check to see if there are any NaN values
vvix[vvix['High'].isna()]
# Forward fill to clean up missing data
vvix['High'] = vvix['High'].ffill()
DataFrame Info - VVIX¶
In [41]:
df_info(vvix)
The columns, shape, and data types are: <class 'pandas.core.frame.DataFrame'> DatetimeIndex: 4711 entries, 2007-01-03 to 2025-10-06 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Close 4711 non-null float64 1 High 4711 non-null float64 2 Low 4711 non-null float64 3 Open 4711 non-null float64 dtypes: float64(4) memory usage: 184.0 KB None The first 5 rows are:
| Close | High | Low | Open | |
|---|---|---|---|---|
| Date | ||||
| 2007-01-03 | 87.63 | 87.63 | 87.63 | 87.63 |
| 2007-01-04 | 88.19 | 88.19 | 88.19 | 88.19 |
| 2007-01-05 | 90.17 | 90.17 | 90.17 | 90.17 |
| 2007-01-08 | 92.04 | 92.04 | 92.04 | 92.04 |
| 2007-01-09 | 92.76 | 92.76 | 92.76 | 92.76 |
The last 5 rows are:
| Close | High | Low | Open | |
|---|---|---|---|---|
| Date | ||||
| 2025-09-30 | 95.89 | 98.29 | 94.78 | 97.59 |
| 2025-10-01 | 93.22 | 96.60 | 92.36 | 96.60 |
| 2025-10-02 | 94.10 | 94.32 | 91.96 | 92.42 |
| 2025-10-03 | 94.79 | 95.86 | 93.45 | 93.82 |
| 2025-10-06 | 95.38 | 98.49 | 95.35 | 96.92 |
In [42]:
# Copy this <!-- INSERT_02_VVIX_DF_Info_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_VVIX_DF_Info.md", content=df_info_markdown(vix))
✅ Exported and tracked: 02_VVIX_DF_Info.md
Statistics - VVIX¶
In [43]:
vvix_stats = vvix.describe()
num_std = [-1, 0, 1, 2, 3, 4, 5]
for num in num_std:
vvix_stats.loc[f"mean + {num} std"] = {
'Open': vvix_stats.loc['mean']['Open'] + num * vvix_stats.loc['std']['Open'],
'High': vvix_stats.loc['mean']['High'] + num * vvix_stats.loc['std']['High'],
'Low': vvix_stats.loc['mean']['Low'] + num * vvix_stats.loc['std']['Low'],
'Close': vvix_stats.loc['mean']['Close'] + num * vvix_stats.loc['std']['Close'],
}
display(vvix_stats)
| Close | High | Low | Open | |
|---|---|---|---|---|
| count | 4711.00 | 4711.00 | 4711.00 | 4711.00 |
| mean | 93.55 | 95.62 | 91.98 | 93.80 |
| std | 16.31 | 17.93 | 14.98 | 16.37 |
| min | 59.74 | 59.74 | 59.31 | 59.31 |
| 25% | 82.49 | 83.63 | 81.56 | 82.69 |
| 50% | 90.71 | 92.47 | 89.58 | 91.03 |
| 75% | 102.05 | 104.87 | 99.82 | 102.45 |
| max | 207.59 | 212.22 | 187.27 | 212.22 |
| mean + -1 std | 77.24 | 77.69 | 77.01 | 77.43 |
| mean + 0 std | 93.55 | 95.62 | 91.98 | 93.80 |
| mean + 1 std | 109.87 | 113.56 | 106.96 | 110.17 |
| mean + 2 std | 126.18 | 131.49 | 121.94 | 126.54 |
| mean + 3 std | 142.50 | 149.42 | 136.92 | 142.91 |
| mean + 4 std | 158.81 | 167.36 | 151.89 | 159.28 |
| mean + 5 std | 175.12 | 185.29 | 166.87 | 175.65 |
In [44]:
# Copy this <!-- INSERT_02_VVIX_Stats_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_VVIX_Stats.md", content=vvix_stats.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 02_VVIX_Stats.md
In [45]:
# Group by year and calculate mean and std for OHLC
vvix_stats_by_year = vvix.groupby(vvix.index.year)[["Open", "High", "Low", "Close"]].agg(["mean", "std", "min", "max"])
# Flatten the column MultiIndex
vvix_stats_by_year.columns = ['_'.join(col).strip() for col in vvix_stats_by_year.columns.values]
vvix_stats_by_year.index.name = "Year"
display(vvix_stats_by_year)
| Open_mean | Open_std | Open_min | Open_max | High_mean | High_std | High_min | High_max | Low_mean | Low_std | Low_min | Low_max | Close_mean | Close_std | Close_min | Close_max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Year | ||||||||||||||||
| 2007 | 87.68 | 13.31 | 63.52 | 142.99 | 87.68 | 13.31 | 63.52 | 142.99 | 87.68 | 13.31 | 63.52 | 142.99 | 87.68 | 13.31 | 63.52 | 142.99 |
| 2008 | 81.85 | 15.60 | 59.74 | 134.87 | 81.85 | 15.60 | 59.74 | 134.87 | 81.85 | 15.60 | 59.74 | 134.87 | 81.85 | 15.60 | 59.74 | 134.87 |
| 2009 | 79.78 | 8.63 | 64.95 | 104.02 | 79.78 | 8.63 | 64.95 | 104.02 | 79.78 | 8.63 | 64.95 | 104.02 | 79.78 | 8.63 | 64.95 | 104.02 |
| 2010 | 88.36 | 13.07 | 64.87 | 145.12 | 88.36 | 13.07 | 64.87 | 145.12 | 88.36 | 13.07 | 64.87 | 145.12 | 88.36 | 13.07 | 64.87 | 145.12 |
| 2011 | 92.94 | 10.21 | 75.94 | 134.63 | 92.94 | 10.21 | 75.94 | 134.63 | 92.94 | 10.21 | 75.94 | 134.63 | 92.94 | 10.21 | 75.94 | 134.63 |
| 2012 | 94.84 | 8.38 | 78.42 | 117.44 | 94.84 | 8.38 | 78.42 | 117.44 | 94.84 | 8.38 | 78.42 | 117.44 | 94.84 | 8.38 | 78.42 | 117.44 |
| 2013 | 80.52 | 8.97 | 62.71 | 111.43 | 80.52 | 8.97 | 62.71 | 111.43 | 80.52 | 8.97 | 62.71 | 111.43 | 80.52 | 8.97 | 62.71 | 111.43 |
| 2014 | 83.01 | 14.33 | 61.76 | 138.60 | 83.01 | 14.33 | 61.76 | 138.60 | 83.01 | 14.33 | 61.76 | 138.60 | 83.01 | 14.33 | 61.76 | 138.60 |
| 2015 | 95.44 | 15.59 | 73.07 | 212.22 | 98.47 | 16.39 | 76.41 | 212.22 | 92.15 | 13.35 | 72.20 | 148.68 | 94.82 | 14.75 | 73.18 | 168.75 |
| 2016 | 93.36 | 10.02 | 77.96 | 131.95 | 95.82 | 10.86 | 78.86 | 132.42 | 90.54 | 8.99 | 76.17 | 115.15 | 92.80 | 10.07 | 76.17 | 125.13 |
| 2017 | 90.50 | 8.65 | 75.09 | 134.98 | 92.94 | 9.64 | 77.34 | 135.32 | 87.85 | 7.78 | 71.75 | 117.29 | 90.01 | 8.80 | 75.64 | 135.32 |
| 2018 | 102.60 | 13.22 | 83.70 | 176.72 | 106.27 | 16.26 | 85.00 | 203.73 | 99.17 | 11.31 | 82.60 | 165.35 | 102.26 | 14.04 | 83.21 | 180.61 |
| 2019 | 91.28 | 8.43 | 75.58 | 112.75 | 93.61 | 8.98 | 75.95 | 117.63 | 88.90 | 7.86 | 74.36 | 111.48 | 91.03 | 8.36 | 74.98 | 114.40 |
| 2020 | 118.64 | 19.32 | 88.39 | 203.03 | 121.91 | 20.88 | 88.54 | 209.76 | 115.05 | 17.37 | 85.31 | 187.27 | 118.36 | 19.39 | 86.87 | 207.59 |
| 2021 | 115.51 | 9.37 | 96.09 | 151.35 | 119.29 | 11.70 | 98.36 | 168.78 | 111.99 | 8.14 | 95.92 | 144.19 | 115.32 | 10.20 | 97.09 | 157.69 |
| 2022 | 102.58 | 18.01 | 76.48 | 161.09 | 105.32 | 19.16 | 77.93 | 172.82 | 99.17 | 16.81 | 76.13 | 153.26 | 101.81 | 17.81 | 77.05 | 154.38 |
| 2023 | 90.95 | 8.64 | 74.43 | 127.73 | 93.72 | 9.98 | 75.31 | 137.65 | 88.01 | 7.37 | 72.27 | 119.64 | 90.34 | 8.38 | 73.88 | 124.75 |
| 2024 | 92.88 | 15.06 | 59.31 | 169.68 | 97.32 | 18.33 | 74.79 | 192.49 | 89.51 | 13.16 | 59.31 | 137.05 | 92.81 | 15.60 | 73.26 | 173.32 |
| 2025 | 102.07 | 13.46 | 83.19 | 186.33 | 106.39 | 16.21 | 85.82 | 189.03 | 98.45 | 10.50 | 81.73 | 146.51 | 101.39 | 12.76 | 81.89 | 170.92 |
In [46]:
# Copy this <!-- INSERT_02_VVIX_Stats_By_Year_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_VVIX_Stats_By_Year.md", content=vvix_stats_by_year.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 02_VVIX_Stats_By_Year.md
In [47]:
# Group by month and calculate mean and std for OHLC
vvix_stats_by_month = vvix.groupby(vvix.index.month)[["Open", "High", "Low", "Close"]].agg(["mean", "std", "min", "max"])
# Flatten the column MultiIndex
vvix_stats_by_month.columns = ['_'.join(col).strip() for col in vvix_stats_by_month.columns.values]
vvix_stats_by_month.index.name = "Year"
display(vvix_stats_by_month)
| Open_mean | Open_std | Open_min | Open_max | High_mean | High_std | High_min | High_max | Low_mean | Low_std | Low_min | Low_max | Close_mean | Close_std | Close_min | Close_max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Year | ||||||||||||||||
| 1 | 92.46 | 15.63 | 64.87 | 161.09 | 94.37 | 17.63 | 64.87 | 172.82 | 90.69 | 14.23 | 64.87 | 153.26 | 92.23 | 15.78 | 64.87 | 157.69 |
| 2 | 93.49 | 18.24 | 65.47 | 176.72 | 95.39 | 20.70 | 65.47 | 203.73 | 91.39 | 16.43 | 65.47 | 165.35 | 93.13 | 18.58 | 65.47 | 180.61 |
| 3 | 95.30 | 21.66 | 66.97 | 203.03 | 97.38 | 23.56 | 66.97 | 209.76 | 92.94 | 19.51 | 66.97 | 187.27 | 94.89 | 21.59 | 66.97 | 207.59 |
| 4 | 92.18 | 19.03 | 59.74 | 186.33 | 94.01 | 20.57 | 59.74 | 189.03 | 90.30 | 17.21 | 59.74 | 152.01 | 91.88 | 18.60 | 59.74 | 170.92 |
| 5 | 92.25 | 16.93 | 61.76 | 145.18 | 93.95 | 17.99 | 61.76 | 151.50 | 90.54 | 16.14 | 61.76 | 145.12 | 91.79 | 16.79 | 61.76 | 146.28 |
| 6 | 93.16 | 14.86 | 63.52 | 155.48 | 94.76 | 16.11 | 63.52 | 172.21 | 91.49 | 13.79 | 63.52 | 140.15 | 92.98 | 14.83 | 63.52 | 151.60 |
| 7 | 90.10 | 12.82 | 67.21 | 138.42 | 91.63 | 13.88 | 67.21 | 149.60 | 88.60 | 11.94 | 67.21 | 133.82 | 89.98 | 12.78 | 67.21 | 139.54 |
| 8 | 96.84 | 16.53 | 68.05 | 212.22 | 98.99 | 18.33 | 68.05 | 212.22 | 94.67 | 14.50 | 68.05 | 148.68 | 96.61 | 16.24 | 68.05 | 173.32 |
| 9 | 94.91 | 13.70 | 67.94 | 135.17 | 96.84 | 15.36 | 67.94 | 147.14 | 93.04 | 12.20 | 67.94 | 128.46 | 94.58 | 13.44 | 67.94 | 138.93 |
| 10 | 97.71 | 13.94 | 64.97 | 149.60 | 99.39 | 15.04 | 64.97 | 154.99 | 96.11 | 13.29 | 64.97 | 144.55 | 97.48 | 14.09 | 64.97 | 152.01 |
| 11 | 93.53 | 14.17 | 63.77 | 142.68 | 95.07 | 15.36 | 63.77 | 161.76 | 91.98 | 13.39 | 63.77 | 140.44 | 93.28 | 14.24 | 63.77 | 149.74 |
| 12 | 93.35 | 15.03 | 59.31 | 151.35 | 95.33 | 16.63 | 62.71 | 168.37 | 91.78 | 13.70 | 59.31 | 144.19 | 93.46 | 15.07 | 62.71 | 156.10 |
In [48]:
# Copy this <!-- INSERT_02_VVIX_Stats_By_Month_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_VVIX_Stats_By_Month.md", content=vvix_stats_by_month.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 02_VVIX_Stats_By_Month.md
Deciles - VVIX¶
In [49]:
vvix_deciles = vvix.quantile(np.arange(0, 1.1, 0.1))
display(vvix_deciles)
| Close | High | Low | Open | |
|---|---|---|---|---|
| 0.00 | 59.74 | 59.74 | 59.31 | 59.31 |
| 0.10 | 75.96 | 76.17 | 75.52 | 75.93 |
| 0.20 | 80.74 | 81.55 | 79.95 | 80.86 |
| 0.30 | 84.07 | 85.41 | 83.16 | 84.38 |
| 0.40 | 87.38 | 88.78 | 86.21 | 87.68 |
| 0.50 | 90.71 | 92.47 | 89.58 | 91.03 |
| 0.60 | 94.35 | 96.33 | 93.12 | 94.55 |
| 0.70 | 99.07 | 101.49 | 97.36 | 99.35 |
| 0.80 | 105.85 | 109.28 | 103.69 | 106.34 |
| 0.90 | 115.06 | 118.65 | 112.35 | 115.27 |
| 1.00 | 207.59 | 212.22 | 187.27 | 212.22 |
In [50]:
# Copy this <!-- INSERT_02_VVIX_Deciles_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="02_VVIX_Deciles.md", content=vvix_deciles.to_markdown(floatfmt=".2f"))
✅ Exported and tracked: 02_VVIX_Deciles.md
Plots - VVIX¶
Histogram Distribution - VVIX¶
In [51]:
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Histogram
plt.hist([vvix['High']], label=['High'], bins=200, edgecolor='black', color='steelblue')
# Plot a vertical line at the mean, mean + 1 std, and mean + 2 std
plt.axvline(vvix_stats.loc['mean + -1 std']['High'], color='brown', linestyle='dashed', linewidth=1, label=f'Mean - 1 std: {vvix_stats.loc['mean + -1 std']['High']:.2f}')
plt.axvline(vvix_stats.loc['mean']['High'], color='red', linestyle='dashed', linewidth=1, label=f'Mean: {vvix_stats.loc['mean']['High']:.2f}')
plt.axvline(vvix_stats.loc['mean + 1 std']['High'], color='green', linestyle='dashed', linewidth=1, label=f'Mean + 1 std: {vvix_stats.loc['mean + 1 std']['High']:.2f}')
plt.axvline(vvix_stats.loc['mean + 2 std']['High'], color='orange', linestyle='dashed', linewidth=1, label=f'Mean + 2 std: {vvix_stats.loc['mean + 2 std']['High']:.2f}')
plt.axvline(vvix_stats.loc['mean + 3 std']['High'], color='black', linestyle='dashed', linewidth=1, label=f'Mean + 3 std: {vvix_stats.loc['mean + 3 std']['High']:.2f}')
plt.axvline(vvix_stats.loc['mean + 4 std']['High'], color='yellow', linestyle='dashed', linewidth=1, label=f'Mean + 4 std: {vvix_stats.loc['mean + 4 std']['High']:.2f}')
# Set X axis
x_tick_spacing = 5 # Specify the interval for y-axis ticks
plt.gca().xaxis.set_major_locator(MultipleLocator(x_tick_spacing))
plt.xlabel("VVIX", fontsize=10)
plt.xticks(rotation=0, fontsize=8)
# Set Y axis
y_tick_spacing = 25 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("# Of Datapoints", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title("CBOE VVIX Histogram (200 Bins)", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig("02_Histogram+Mean+SD.png", dpi=300, bbox_inches="tight")
plt.show()
Historical Data - VVIX¶
In [52]:
plot_price(
price_df=vvix,
plot_start_date=None,
plot_end_date="2016-12-31",
plot_columns=["High", "Low"],
title="CBOE VVIX, 2007 - 2016",
x_label="Date",
x_format="Year",
y_label="VIX",
y_format="Decimal",
y_tick_spacing=15,
grid=True,
legend=True,
export_plot=True,
plot_file_name="02_VVIX_Plot_2007-2016",
)
In [53]:
plot_price(
price_df=vvix,
plot_start_date="2017-01-01",
plot_end_date=None,
plot_columns=["High", "Low"],
title="CBOE VVIX, 2017 - Present",
x_label="Date",
x_format="Year",
y_label="VIX",
y_format="Decimal",
y_tick_spacing=15,
grid=True,
legend=True,
export_plot=True,
plot_file_name="02_VVIX_Plot_2017-Present",
)
Stats By Year - VVIX¶
In [54]:
plot_stats(
stats_df=vvix_stats_by_year,
plot_columns=["Open_mean", "High_mean", "Low_mean", "Close_mean"],
title="VVIX Mean OHLC By Year",
x_label="Year",
x_rotation=45,
x_tick_spacing=1,
y_label="Price",
y_tick_spacing=5,
grid=True,
legend=True,
export_plot=True,
plot_file_name="02_VVIX_Stats_By_Year"
)
Stats By Month - VVIX¶
In [55]:
plot_stats(
stats_df=vvix_stats_by_month,
plot_columns=["Open_mean", "High_mean", "Low_mean", "Close_mean"],
title="VVIX Mean OHLC By Month",
x_label="Month",
x_rotation=0,
x_tick_spacing=1,
y_label="Price",
y_tick_spacing=1,
grid=True,
legend=True,
export_plot=True,
plot_file_name="02_VVIX_Stats_By_Month"
)
In [56]:
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Plot data
plt.plot(vvix[vvix.index > '2023-12-31'].index, vvix[vvix.index > '2023-12-31']['High'], label='High', linestyle='-', color='steelblue', linewidth=1.5)
plt.plot(vvix[vvix.index > '2023-12-31'].index, vvix[vvix.index > '2023-12-31']['Low'], label='Low', linestyle='-', color='brown', linewidth=1.5)
# Set X axis
plt.gca().xaxis.set_major_locator(mdates.MonthLocator())
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.xlabel("Date", fontsize=10)
plt.xticks(rotation=45, fontsize=8)
# Set Y axis
y_tick_spacing = 5 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("VVIX", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title("CBOE VVIX, 2024 - Present", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig("02_VVIX_Plot_2024-Present.png", dpi=300, bbox_inches="tight")
plt.show()
In [57]:
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Plot data
plt.plot(vvix[vvix.index > '2024-12-31'].index, vvix[vvix.index > '2024-12-31']['High'], label='High', linestyle='-', color='steelblue', linewidth=1.5)
plt.plot(vvix[vvix.index > '2024-12-31'].index, vvix[vvix.index > '2024-12-31']['Low'], label='Low', linestyle='-', color='brown', linewidth=1.5)
# Set X axis
plt.gca().xaxis.set_major_locator(mdates.MonthLocator())
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.xlabel("Date", fontsize=10)
plt.xticks(rotation=45, fontsize=8)
# Set Y axis
y_tick_spacing = 5 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("VVIX", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title("CBOE VVIX, 2025 - Present", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig("02_VVIX_Plot_2025-Present.png", dpi=300, bbox_inches="tight")
plt.show()
Investigating A Signal¶
Determining A Spike Level¶
In [58]:
# Define the spike multiplier for detecting significant spikes
spike_level = 1.25
# =========================
# Simple Moving Averages (SMA)
# =========================
# Calculate 10-period SMA of 'High'
vix['High_SMA_10'] = vix['High'].rolling(window=10).mean()
# Shift the 10-period SMA by 1 to compare with current 'High'
vix['High_SMA_10_Shift'] = vix['High_SMA_10'].shift(1)
# Calculate the spike level based on shifted SMA and spike multiplier
vix['Spike_Level_SMA'] = vix['High_SMA_10_Shift'] * spike_level
# Calculate 20-period SMA of 'High'
vix['High_SMA_20'] = vix['High'].rolling(window=20).mean()
# Determine if 'High' exceeds the spike level (indicates a spike)
vix['Spike_SMA'] = vix['High'] >= vix['Spike_Level_SMA']
# Calculate 50-period SMA of 'High' for trend analysis
vix['High_SMA_50'] = vix['High'].rolling(window=50).mean()
# =========================
# Exponential Moving Averages (EMA)
# =========================
# Calculate 10-period EMA of 'High'
vix['High_EMA_10'] = vix['High'].ewm(span=10, adjust=False).mean()
# Shift the 10-period EMA by 1 to compare with current 'High'
vix['High_EMA_10_Shift'] = vix['High_EMA_10'].shift(1)
# Calculate the spike level based on shifted EMA and spike multiplier
vix['Spike_Level_EMA'] = vix['High_EMA_10_Shift'] * spike_level
# Calculate 20-period EMA of 'High'
vix['High_EMA_20'] = vix['High'].ewm(span=20, adjust=False).mean()
# Determine if 'High' exceeds the spike level (indicates a spike)
vix['Spike_EMA'] = vix['High'] >= vix['Spike_Level_EMA']
# Calculate 50-period EMA of 'High' for trend analysis
vix['High_EMA_50'] = vix['High'].ewm(span=50, adjust=False).mean()
In [59]:
display(vix)
| Close | High | Low | Open | High_SMA_10 | High_SMA_10_Shift | Spike_Level_SMA | High_SMA_20 | Spike_SMA | High_SMA_50 | High_EMA_10 | High_EMA_10_Shift | Spike_Level_EMA | High_EMA_20 | Spike_EMA | High_EMA_50 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 1990-01-02 | 17.24 | 17.24 | 17.24 | 17.24 | NaN | NaN | NaN | NaN | False | NaN | 17.24 | NaN | NaN | 17.24 | False | 17.24 |
| 1990-01-03 | 18.19 | 18.19 | 18.19 | 18.19 | NaN | NaN | NaN | NaN | False | NaN | 17.41 | 17.24 | 21.55 | 17.33 | False | 17.28 |
| 1990-01-04 | 19.22 | 19.22 | 19.22 | 19.22 | NaN | NaN | NaN | NaN | False | NaN | 17.74 | 17.41 | 21.77 | 17.51 | False | 17.35 |
| 1990-01-05 | 20.11 | 20.11 | 20.11 | 20.11 | NaN | NaN | NaN | NaN | False | NaN | 18.17 | 17.74 | 22.18 | 17.76 | False | 17.46 |
| 1990-01-08 | 20.26 | 20.26 | 20.26 | 20.26 | NaN | NaN | NaN | NaN | False | NaN | 18.55 | 18.17 | 22.71 | 18.00 | False | 17.57 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2025-10-01 | 16.29 | 17.28 | 15.98 | 17.28 | 16.76 | 16.71 | 20.89 | 16.36 | False | 16.56 | 16.78 | 16.67 | 20.84 | 16.61 | False | 16.96 |
| 2025-10-02 | 16.63 | 16.92 | 15.93 | 16.12 | 16.87 | 16.76 | 20.95 | 16.39 | False | 16.58 | 16.81 | 16.78 | 20.98 | 16.64 | False | 16.96 |
| 2025-10-03 | 16.65 | 17.06 | 16.20 | 16.35 | 16.97 | 16.87 | 21.09 | 16.39 | False | 16.61 | 16.85 | 16.81 | 21.01 | 16.68 | False | 16.96 |
| 2025-10-06 | 16.37 | 17.09 | 16.29 | 16.74 | 17.04 | 16.97 | 21.21 | 16.47 | False | 16.64 | 16.90 | 16.85 | 21.07 | 16.72 | False | 16.96 |
| 2025-10-07 | 17.24 | 17.55 | 16.19 | 16.58 | 17.09 | 17.04 | 21.30 | 16.55 | False | 16.68 | 17.02 | 16.90 | 21.12 | 16.80 | False | 16.99 |
9008 rows × 16 columns
In [60]:
vix[vix['High'] >= 50]
Out[60]:
| Close | High | Low | Open | High_SMA_10 | High_SMA_10_Shift | Spike_Level_SMA | High_SMA_20 | Spike_SMA | High_SMA_50 | High_EMA_10 | High_EMA_10_Shift | Spike_Level_EMA | High_EMA_20 | Spike_EMA | High_EMA_50 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Date | ||||||||||||||||
| 2008-10-06 | 52.05 | 58.24 | 45.12 | 45.12 | 42.92 | 40.52 | 50.65 | 37.24 | True | 28.17 | 44.33 | 41.24 | 51.55 | 38.82 | True | 31.65 |
| 2008-10-07 | 53.68 | 54.19 | 47.03 | 52.05 | 44.73 | 42.92 | 53.65 | 38.66 | True | 28.76 | 46.12 | 44.33 | 55.41 | 40.29 | False | 32.53 |
| 2008-10-08 | 57.53 | 59.06 | 51.90 | 53.68 | 46.97 | 44.73 | 55.91 | 40.34 | True | 29.46 | 48.47 | 46.12 | 57.65 | 42.07 | True | 33.57 |
| 2008-10-09 | 63.92 | 64.92 | 52.54 | 57.57 | 49.94 | 46.97 | 58.71 | 42.27 | True | 30.31 | 51.46 | 48.47 | 60.59 | 44.25 | True | 34.80 |
| 2008-10-10 | 69.95 | 76.94 | 65.63 | 65.85 | 53.99 | 49.94 | 62.42 | 44.79 | True | 31.39 | 56.10 | 51.46 | 64.33 | 47.36 | True | 36.46 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 2024-08-05 | 38.57 | 65.73 | 23.39 | 23.39 | 23.84 | 18.95 | 23.69 | 19.11 | True | 15.66 | 28.04 | 19.66 | 24.58 | 22.15 | True | 17.62 |
| 2025-04-07 | 46.98 | 60.13 | 38.58 | 60.13 | 28.60 | 24.51 | 30.63 | 26.10 | True | 22.35 | 33.61 | 27.72 | 34.65 | 28.48 | True | 23.95 |
| 2025-04-08 | 52.33 | 57.52 | 36.48 | 44.04 | 32.58 | 28.60 | 35.76 | 27.50 | True | 23.05 | 37.96 | 33.61 | 42.01 | 31.25 | True | 25.27 |
| 2025-04-09 | 33.62 | 57.96 | 31.90 | 50.98 | 36.47 | 32.58 | 40.72 | 29.05 | True | 23.84 | 41.60 | 37.96 | 47.45 | 33.79 | True | 26.55 |
| 2025-04-10 | 40.72 | 54.87 | 34.44 | 34.44 | 40.03 | 36.47 | 45.59 | 30.49 | True | 24.58 | 44.01 | 41.60 | 51.99 | 35.80 | True | 27.66 |
97 rows × 16 columns
Spike Counts (Signals) By Year¶
In [61]:
# Ensure the index is a DatetimeIndex
vix.index = pd.to_datetime(vix.index)
# Create a new column for the year extracted from the date index
vix['Year'] = vix.index.year
# Group by year and the "Spike_SMA" and "Spike_EMA" columns, then count occurrences
spike_count_SMA = vix.groupby(['Year', 'Spike_SMA']).size().unstack(fill_value=0)
display(spike_count_SMA)
| Spike_SMA | False | True |
|---|---|---|
| Year | ||
| 1990 | 248 | 5 |
| 1991 | 249 | 4 |
| 1992 | 250 | 4 |
| 1993 | 251 | 2 |
| 1994 | 243 | 9 |
| 1995 | 252 | 0 |
| 1996 | 248 | 6 |
| 1997 | 247 | 6 |
| 1998 | 243 | 9 |
| 1999 | 250 | 2 |
| 2000 | 248 | 4 |
| 2001 | 240 | 8 |
| 2002 | 248 | 4 |
| 2003 | 251 | 1 |
| 2004 | 250 | 2 |
| 2005 | 250 | 2 |
| 2006 | 242 | 9 |
| 2007 | 239 | 12 |
| 2008 | 238 | 15 |
| 2009 | 249 | 3 |
| 2010 | 239 | 13 |
| 2011 | 240 | 12 |
| 2012 | 248 | 2 |
| 2013 | 249 | 3 |
| 2014 | 235 | 17 |
| 2015 | 240 | 12 |
| 2016 | 234 | 18 |
| 2017 | 244 | 7 |
| 2018 | 228 | 23 |
| 2019 | 241 | 11 |
| 2020 | 224 | 29 |
| 2021 | 235 | 17 |
| 2022 | 239 | 12 |
| 2023 | 246 | 4 |
| 2024 | 237 | 15 |
| 2025 | 178 | 13 |
In [62]:
# Copy this <!-- INSERT_08_Spike_Counts_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="08_Spike_Counts.md", content=spike_count_SMA.to_markdown())
✅ Exported and tracked: 08_Spike_Counts.md
In [63]:
# Ensure the index is a DatetimeIndex
vix.index = pd.to_datetime(vix.index)
# Create a new column for the year extracted from the date index
vix['Year'] = vix.index.year
# Group by year and the "Spike_SMA" and "Spike_EMA" columns, then count occurrences
spike_count_EMA = vix.groupby(['Year', 'Spike_EMA']).size().unstack(fill_value=0)
display(spike_count_EMA)
| Spike_EMA | False | True |
|---|---|---|
| Year | ||
| 1990 | 247 | 6 |
| 1991 | 251 | 2 |
| 1992 | 253 | 1 |
| 1993 | 251 | 2 |
| 1994 | 247 | 5 |
| 1995 | 252 | 0 |
| 1996 | 252 | 2 |
| 1997 | 250 | 3 |
| 1998 | 246 | 6 |
| 1999 | 250 | 2 |
| 2000 | 250 | 2 |
| 2001 | 241 | 7 |
| 2002 | 250 | 2 |
| 2003 | 251 | 1 |
| 2004 | 251 | 1 |
| 2005 | 250 | 2 |
| 2006 | 248 | 3 |
| 2007 | 242 | 9 |
| 2008 | 240 | 13 |
| 2009 | 251 | 1 |
| 2010 | 243 | 9 |
| 2011 | 242 | 10 |
| 2012 | 250 | 0 |
| 2013 | 250 | 2 |
| 2014 | 236 | 16 |
| 2015 | 243 | 9 |
| 2016 | 238 | 14 |
| 2017 | 244 | 7 |
| 2018 | 230 | 21 |
| 2019 | 242 | 10 |
| 2020 | 228 | 25 |
| 2021 | 239 | 13 |
| 2022 | 244 | 7 |
| 2023 | 248 | 2 |
| 2024 | 244 | 8 |
| 2025 | 181 | 10 |
In [64]:
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Bar positions
x = np.arange(len(spike_count_SMA[True].index))
width = 0.35
# Plot SMA bars
plt.bar(x - width / 2, spike_count_SMA[True].values, width, color="steelblue", label="Spike Counts Using SMA")
# Plot EMA bars
plt.bar(x + width / 2, spike_count_EMA[True].values, width, color="forestgreen", label="Spike Counts Using EMA")
# Set X axis
# x_tick_spacing = 5 # Specify the interval for y-axis ticks
# plt.gca().xaxis.set_major_locator(MultipleLocator(x_tick_spacing))
plt.xlabel("Year", fontsize=10)
plt.xticks(x, spike_count_SMA[True].index, rotation=45, fontsize=8)
plt.xlim(x[0] - 2 * width, x[-1] + 2 * width)
# # Set Y axis
y_tick_spacing = 2 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("Count", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title("Yearly Totals Of Spike Counts", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig("08_Spike_Counts.png", dpi=300, bbox_inches="tight")
plt.show()
Spike Counts (Signals) Plots By Year¶
In [65]:
def vix_plot(start_year, end_year):
# Start and end dates
start_date = start_year + '-01-01'
end_date = end_year + '-12-31'
# Create temporary dataframe for the specified date range
vix_temp = vix[(vix.index >= start_date) & (vix.index <= end_date)]
# Plotting
plt.figure(figsize=(12, 6), facecolor="#F5F5F5")
# Plot data
plt.plot(vix_temp.index, vix_temp['High'], label='High', linestyle='-', color='steelblue', linewidth=1)
plt.plot(vix_temp.index, vix_temp['Low'], label='Low', linestyle='-', color='brown', linewidth=1)
plt.plot(vix_temp.index, vix_temp['High_SMA_10'], label='10 Day High SMA', linestyle='-', color='red', linewidth=1)
plt.plot(vix_temp.index, vix_temp['High_SMA_20'], label='20 Day High SMA', linestyle='-', color='orange', linewidth=1)
plt.plot(vix_temp.index, vix_temp['High_SMA_50'], label='50 Day High SMA', linestyle='-', color='green', linewidth=1)
plt.scatter(vix_temp[vix_temp['Spike_SMA'] == True].index, vix_temp[vix_temp['Spike_SMA'] == True]['High'], label='Spike (High > 1.25 * 10 Day High SMA)', linestyle='-', color='black', s=20)
# Set X axis
plt.gca().xaxis.set_major_locator(mdates.MonthLocator())
plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
plt.xlabel("Date", fontsize=10)
plt.xticks(rotation=45, fontsize=8)
# Set Y axis
y_tick_spacing = 5 # Specify the interval for y-axis ticks
plt.gca().yaxis.set_major_locator(MultipleLocator(y_tick_spacing))
plt.ylabel("VIX", fontsize=10)
plt.yticks(fontsize=8)
# Set title, layout, grid, and legend
plt.title(f"CBOE Volatility Index (VIX), {start_year} - {end_year}", fontsize=12)
plt.tight_layout()
plt.grid(True, linestyle='--', alpha=0.7)
plt.legend(fontsize=9)
# Save figure and display plot
plt.savefig(f"09_VIX_SMA_Spike_{start_year}_{end_year}.png", dpi=300, bbox_inches="tight")
plt.show()
Yearly Plots¶
In [66]:
for year in range(1990, 2026):
vix_plot(str(year), str(year))
Spike Counts (Signals) Plots By Decade¶
1990 - 1994¶
In [67]:
vix_plot('1990', '1994')
1995 - 1999¶
In [68]:
vix_plot('1995', '1999')
2000 - 2004¶
In [69]:
vix_plot('2000', '2004')
2005 - 2009¶
In [70]:
vix_plot('2005', '2009')
2010 - 2014¶
In [71]:
vix_plot('2010', '2014')
2015 - 2019¶
In [72]:
vix_plot('2015', '2019')
2020 - 2024¶
In [73]:
vix_plot('2020', '2024')
2025 - Present¶
In [74]:
vix_plot('2025', '2029')
Trading History¶
Trades Executed¶
In [75]:
# from schwab_order_history import schwab_order_history
In [76]:
# from datetime import datetime
# import pandas as pd
# # Define your date ranges
# range_2024 = {
# "from": "2024-01-01T00:00:00.000Z",
# "to": "2024-12-31T23:59:59.000Z",
# }
# range_2025 = {
# "from": "2025-01-01T00:00:00.000Z",
# "to": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"),
# }
# # Pull both sets of orders
# df_2024 = schwab_order_history(
# max_results=1000, # or whatever large number you want
# from_entered_time=range_2024["from"],
# to_entered_time=range_2024["to"],
# account_id=None, # or pass your specific encrypted account ID
# )
# df_2025 = schwab_order_history(
# max_results=1000,
# from_entered_time=range_2025["from"],
# to_entered_time=range_2025["to"],
# account_id=None,
# )
# # Combine the two dataframes
# df_all = pd.concat([df_2024, df_2025], ignore_index=True)
In [77]:
# df_2024
In [78]:
# # Filter for symbols that start with "VIX"
# df_vix = df_all[df_all["symbol"].str.startswith("VIX")].copy()
# df_vix = df_vix.sort_values(by=['symbol', 'execution_time'], ascending=[True, True])
In [79]:
# df_vix
Trades Executed¶
In [80]:
# Import CSV file of VIX transactions from IRA and Brokerage accounts
vix_transactions_IRA = pd.read_csv(DATA_MANUAL_DIR / "VIX_Transactions_IRA.csv")
vix_transactions_Brokerage = pd.read_excel(DATA_MANUAL_DIR / "VIX_Transactions_Brokerage.xlsx", sheet_name="VIX_Transactions_Brokerage")
In [81]:
# Combine the two DataFrames
vix_transactions = pd.concat([vix_transactions_IRA, vix_transactions_Brokerage], ignore_index=True)
# Drop unnecessary columns
vix_transactions.drop(columns = {'Description'}, inplace=True)
# Convert Amount, Price, and Fees & Comm columns to numeric
vix_transactions['Amount'] = vix_transactions['Amount'].replace({'\$': '', ',': ''}, regex=True).astype(float)
vix_transactions['Price'] = vix_transactions['Price'].replace({'\$': '', ',': ''}, regex=True).astype(float)
vix_transactions['Fees & Comm'] = vix_transactions['Fees & Comm'].replace({'\$': '', ',': ''}, regex=True).astype(float)
# Convert Amount column to absolute values
vix_transactions['Amount'] = abs(vix_transactions['Amount'])
# Extract date for option expiration with regex (MM/DD/YYYY)
vix_transactions["Exp_Date"] = vix_transactions["Symbol"].str.extract(r'(\d{2}/\d{2}/\d{4})')
# Extract date for option strike price with regex and convert to float
vix_transactions["Strike_Price"] = vix_transactions["Symbol"].str.extract(r'(\d{2}\.\d{2})').astype(float)
# Convert expiration date and trade date to datetime
vix_transactions["Exp_Date"] = pd.to_datetime(vix_transactions["Exp_Date"], format="%m/%d/%Y")
vix_transactions['Date'] = pd.to_datetime(vix_transactions['Date'])
# Rename date to trade date
vix_transactions.rename(columns={'Date': 'Trade_Date'}, inplace=True)
# Sort by Exp_Date, then Strike_Price, then Trade_Date
vix_transactions.sort_values(by=['Exp_Date', 'Strike_Price', 'Trade_Date'], ascending=[True, True, True], inplace=True)
# Reset index
vix_transactions.reset_index(drop=True, inplace=True)
vix_transactions
Out[81]:
| Trade_Date | Action | Symbol | Quantity | Price | Fees & Comm | Amount | Approx_VIX_Level | Comments | Exp_Date | Strike_Price | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2024-08-05 | Buy to Open | VIX 09/18/2024 34.00 P | 1 | 10.95 | 1.08 | 1096.08 | 34.33 | NaN | 2024-09-18 | 34.00 |
| 1 | 2024-08-21 | Sell to Close | VIX 09/18/2024 34.00 P | 1 | 17.95 | 1.08 | 1793.92 | 16.49 | NaN | 2024-09-18 | 34.00 |
| 2 | 2024-08-05 | Buy to Open | VIX 10/16/2024 40.00 P | 1 | 16.35 | 1.08 | 1636.08 | 42.71 | NaN | 2024-10-16 | 40.00 |
| 3 | 2024-09-18 | Sell to Close | VIX 10/16/2024 40.00 P | 1 | 21.54 | 1.08 | 2152.92 | 18.85 | NaN | 2024-10-16 | 40.00 |
| 4 | 2024-08-07 | Buy to Open | VIX 11/20/2024 25.00 P | 2 | 5.90 | 2.16 | 1182.16 | 27.11 | NaN | 2024-11-20 | 25.00 |
| 5 | 2024-11-04 | Sell to Close | VIX 11/20/2024 25.00 P | 2 | 6.10 | 2.16 | 1217.84 | 22.43 | NaN | 2024-11-20 | 25.00 |
| 6 | 2024-08-06 | Buy to Open | VIX 12/18/2024 30.00 P | 1 | 10.25 | 1.08 | 1026.08 | 32.27 | NaN | 2024-12-18 | 30.00 |
| 7 | 2024-11-27 | Sell to Close | VIX 12/18/2024 30.00 P | 1 | 14.95 | 1.08 | 1493.92 | 14.04 | NaN | 2024-12-18 | 30.00 |
| 8 | 2025-03-04 | Buy to Open | VIX 04/16/2025 25.00 P | 5 | 5.65 | 5.40 | 2830.40 | 25.74 | NaN | 2025-04-16 | 25.00 |
| 9 | 2025-03-24 | Sell to Close | VIX 04/16/2025 25.00 P | 5 | 7.00 | 5.40 | 3494.60 | 18.01 | NaN | 2025-04-16 | 25.00 |
| 10 | 2025-03-10 | Buy to Open | VIX 05/21/2025 26.00 P | 5 | 7.10 | 5.40 | 3555.40 | 27.54 | Missed opportunity to close position for 20% p... | 2025-05-21 | 26.00 |
| 11 | 2025-04-04 | Buy to Open | VIX 05/21/2025 26.00 P | 10 | 4.10 | 10.81 | 4110.81 | 38.88 | Averaged down on existing position | 2025-05-21 | 26.00 |
| 12 | 2025-04-24 | Sell to Close | VIX 05/21/2025 26.00 P | 7 | 3.50 | 7.57 | 2442.43 | 27.37 | Sold half of position due to vol spike concern... | 2025-05-21 | 26.00 |
| 13 | 2025-05-02 | Sell to Close | VIX 05/21/2025 26.00 P | 4 | 4.35 | 4.32 | 1735.68 | 22.73 | Sold half of remaining position due to vol spi... | 2025-05-21 | 26.00 |
| 14 | 2025-05-07 | Sell to Close | VIX 05/21/2025 26.00 P | 4 | 3.55 | 4.32 | 1415.68 | 24.49 | Closed position ahead of Fed’s (Powell’s) comm... | 2025-05-21 | 26.00 |
| 15 | 2025-04-04 | Buy to Open | VIX 05/21/2025 37.00 P | 3 | 13.20 | 3.24 | 3963.24 | 36.46 | NaN | 2025-05-21 | 37.00 |
| 16 | 2025-05-07 | Sell to Close | VIX 05/21/2025 37.00 P | 3 | 13.75 | 3.24 | 4121.76 | 24.51 | Closed position ahead of Fed’s (Powell’s) comm... | 2025-05-21 | 37.00 |
| 17 | 2025-04-08 | Buy to Open | VIX 05/21/2025 50.00 P | 2 | 21.15 | 2.16 | 4232.16 | NaN | NaN | 2025-05-21 | 50.00 |
| 18 | 2025-04-24 | Sell to Close | VIX 05/21/2025 50.00 P | 1 | 25.30 | 1.08 | 2528.92 | NaN | NaN | 2025-05-21 | 50.00 |
| 19 | 2025-04-25 | Sell to Close | VIX 05/21/2025 50.00 P | 1 | 25.65 | 1.08 | 2563.92 | NaN | NaN | 2025-05-21 | 50.00 |
| 20 | 2025-04-03 | Buy to Open | VIX 06/18/2025 27.00 P | 8 | 7.05 | 8.65 | 5648.65 | 27.62 | NaN | 2025-06-18 | 27.00 |
| 21 | 2025-04-08 | Buy to Open | VIX 06/18/2025 27.00 P | 4 | 4.55 | 4.32 | 1824.32 | 55.44 | Averaged down on existing position | 2025-06-18 | 27.00 |
| 22 | 2025-05-12 | Sell to Close | VIX 06/18/2025 27.00 P | 6 | 7.55 | 6.49 | 4523.51 | 19.05 | Market up on positive news of lowering tariffs... | 2025-06-18 | 27.00 |
| 23 | 2025-05-12 | Sell to Close | VIX 06/18/2025 27.00 P | 6 | 7.40 | 6.49 | 4433.51 | 19.47 | Market up on positive news of lowering tariffs... | 2025-06-18 | 27.00 |
| 24 | 2025-04-04 | Buy to Open | VIX 06/18/2025 36.00 P | 3 | 13.40 | 3.24 | 4023.24 | 36.61 | NaN | 2025-06-18 | 36.00 |
| 25 | 2025-05-12 | Sell to Close | VIX 06/18/2025 36.00 P | 3 | 16.00 | 3.24 | 4796.76 | 19.14 | Market up on positive news of lowering tariffs... | 2025-06-18 | 36.00 |
| 26 | 2025-04-07 | Buy to Open | VIX 06/18/2025 45.00 P | 2 | 18.85 | 2.16 | 3772.16 | 53.65 | NaN | 2025-06-18 | 45.00 |
| 27 | 2025-05-12 | Sell to Close | VIX 06/18/2025 45.00 P | 2 | 25.00 | 2.16 | 4997.84 | 19.24 | Market up on positive news of lowering tariffs... | 2025-06-18 | 45.00 |
| 28 | 2025-04-03 | Buy to Open | VIX 07/16/2025 29.00 P | 5 | 8.55 | 5.40 | 4280.40 | 29.03 | NaN | 2025-07-16 | 29.00 |
| 29 | 2025-05-13 | Sell to Close | VIX 07/16/2025 29.00 P | 3 | 10.40 | 3.24 | 3116.76 | 17.72 | NaN | 2025-07-16 | 29.00 |
| 30 | 2025-05-13 | Sell to Close | VIX 07/16/2025 29.00 P | 2 | 10.30 | 2.16 | 2057.84 | 17.68 | NaN | 2025-07-16 | 29.00 |
| 31 | 2025-04-04 | Buy to Open | VIX 07/16/2025 36.00 P | 3 | 13.80 | 3.24 | 4143.24 | 36.95 | NaN | 2025-07-16 | 36.00 |
| 32 | 2025-05-13 | Sell to Close | VIX 07/16/2025 36.00 P | 1 | 17.00 | 1.08 | 1698.92 | 17.79 | NaN | 2025-07-16 | 36.00 |
| 33 | 2025-05-13 | Sell to Close | VIX 07/16/2025 36.00 P | 2 | 16.90 | 2.16 | 3377.84 | 17.72 | NaN | 2025-07-16 | 36.00 |
| 34 | 2025-04-07 | Buy to Open | VIX 07/16/2025 45.00 P | 2 | 21.55 | 2.16 | 4312.16 | 46.17 | NaN | 2025-07-16 | 45.00 |
| 35 | 2025-05-13 | Sell to Close | VIX 07/16/2025 45.00 P | 2 | 25.65 | 2.16 | 5127.84 | 17.96 | NaN | 2025-07-16 | 45.00 |
| 36 | 2025-04-07 | Buy to Open | VIX 08/20/2025 45.00 P | 2 | 21.75 | 2.16 | 4352.16 | 49.07 | NaN | 2025-08-20 | 45.00 |
| 37 | 2025-05-13 | Sell to Close | VIX 08/20/2025 45.00 P | 2 | 25.40 | 2.16 | 5077.84 | 18.06 | NaN | 2025-08-20 | 45.00 |
| 38 | 2025-06-26 | Buy to Open | VIX 09/17/2025 20.00 C | 10 | 3.00 | 10.81 | 3010.81 | 16.37 | Opened long dated call position; VIX level at ... | 2025-09-17 | 20.00 |
| 39 | 2025-08-01 | Sell to Close | VIX 09/17/2025 20.00 C | 5 | 3.05 | 5.40 | 1519.60 | 20.48 | Sold half of position due to theta drag, held ... | 2025-09-17 | 20.00 |
| 40 | 2025-08-12 | Buy to Open | VIX 09/17/2025 20.00 C | 10 | 1.54 | 9.31 | 1549.31 | 14.87 | Doubled existing position to reduce cost basis | 2025-09-17 | 20.00 |
| 41 | 2025-08-12 | Buy to Open | VIX 10/22/2025 19.00 C | 10 | 3.05 | 10.81 | 3060.81 | 15.22 | NaN | 2025-10-22 | 19.00 |
| 42 | 2025-07-23 | Buy to Open | VIX 10/22/2025 21.00 C | 10 | 2.92 | 10.81 | 2930.81 | 15.40 | Continued low volatility, opened long dated ca... | 2025-10-22 | 21.00 |
| 43 | 2025-06-26 | Buy to Open | VIX 10/22/2025 22.00 C | 10 | 2.94 | 10.81 | 2950.81 | 16.43 | Opened long dated call position; VIX level at ... | 2025-10-22 | 22.00 |
| 44 | 2025-07-17 | Buy to Open | VIX 10/22/2025 23.00 C | 10 | 2.75 | 10.81 | 2760.81 | 16.86 | Continued low volatility, opened long dated ca... | 2025-10-22 | 23.00 |
| 45 | 2025-08-13 | Buy to Open | VIX 11/19/2025 20.00 C | 10 | 3.26 | 10.81 | 3270.81 | 14.56 | VIX at ~0 decile based on the YTD VIX data | 2025-11-19 | 20.00 |
| 46 | 2025-08-12 | Buy to Open | VIX 11/19/2025 21.00 C | 10 | 3.00 | 10.81 | 3010.81 | 15.17 | NaN | 2025-11-19 | 21.00 |
In [82]:
vix_transactions_no_exp = vix_transactions.drop(columns=['Exp_Date', 'Strike_Price'])
vix_transactions_no_exp
Out[82]:
| Trade_Date | Action | Symbol | Quantity | Price | Fees & Comm | Amount | Approx_VIX_Level | Comments | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2024-08-05 | Buy to Open | VIX 09/18/2024 34.00 P | 1 | 10.95 | 1.08 | 1096.08 | 34.33 | NaN |
| 1 | 2024-08-21 | Sell to Close | VIX 09/18/2024 34.00 P | 1 | 17.95 | 1.08 | 1793.92 | 16.49 | NaN |
| 2 | 2024-08-05 | Buy to Open | VIX 10/16/2024 40.00 P | 1 | 16.35 | 1.08 | 1636.08 | 42.71 | NaN |
| 3 | 2024-09-18 | Sell to Close | VIX 10/16/2024 40.00 P | 1 | 21.54 | 1.08 | 2152.92 | 18.85 | NaN |
| 4 | 2024-08-07 | Buy to Open | VIX 11/20/2024 25.00 P | 2 | 5.90 | 2.16 | 1182.16 | 27.11 | NaN |
| 5 | 2024-11-04 | Sell to Close | VIX 11/20/2024 25.00 P | 2 | 6.10 | 2.16 | 1217.84 | 22.43 | NaN |
| 6 | 2024-08-06 | Buy to Open | VIX 12/18/2024 30.00 P | 1 | 10.25 | 1.08 | 1026.08 | 32.27 | NaN |
| 7 | 2024-11-27 | Sell to Close | VIX 12/18/2024 30.00 P | 1 | 14.95 | 1.08 | 1493.92 | 14.04 | NaN |
| 8 | 2025-03-04 | Buy to Open | VIX 04/16/2025 25.00 P | 5 | 5.65 | 5.40 | 2830.40 | 25.74 | NaN |
| 9 | 2025-03-24 | Sell to Close | VIX 04/16/2025 25.00 P | 5 | 7.00 | 5.40 | 3494.60 | 18.01 | NaN |
| 10 | 2025-03-10 | Buy to Open | VIX 05/21/2025 26.00 P | 5 | 7.10 | 5.40 | 3555.40 | 27.54 | Missed opportunity to close position for 20% p... |
| 11 | 2025-04-04 | Buy to Open | VIX 05/21/2025 26.00 P | 10 | 4.10 | 10.81 | 4110.81 | 38.88 | Averaged down on existing position |
| 12 | 2025-04-24 | Sell to Close | VIX 05/21/2025 26.00 P | 7 | 3.50 | 7.57 | 2442.43 | 27.37 | Sold half of position due to vol spike concern... |
| 13 | 2025-05-02 | Sell to Close | VIX 05/21/2025 26.00 P | 4 | 4.35 | 4.32 | 1735.68 | 22.73 | Sold half of remaining position due to vol spi... |
| 14 | 2025-05-07 | Sell to Close | VIX 05/21/2025 26.00 P | 4 | 3.55 | 4.32 | 1415.68 | 24.49 | Closed position ahead of Fed’s (Powell’s) comm... |
| 15 | 2025-04-04 | Buy to Open | VIX 05/21/2025 37.00 P | 3 | 13.20 | 3.24 | 3963.24 | 36.46 | NaN |
| 16 | 2025-05-07 | Sell to Close | VIX 05/21/2025 37.00 P | 3 | 13.75 | 3.24 | 4121.76 | 24.51 | Closed position ahead of Fed’s (Powell’s) comm... |
| 17 | 2025-04-08 | Buy to Open | VIX 05/21/2025 50.00 P | 2 | 21.15 | 2.16 | 4232.16 | NaN | NaN |
| 18 | 2025-04-24 | Sell to Close | VIX 05/21/2025 50.00 P | 1 | 25.30 | 1.08 | 2528.92 | NaN | NaN |
| 19 | 2025-04-25 | Sell to Close | VIX 05/21/2025 50.00 P | 1 | 25.65 | 1.08 | 2563.92 | NaN | NaN |
| 20 | 2025-04-03 | Buy to Open | VIX 06/18/2025 27.00 P | 8 | 7.05 | 8.65 | 5648.65 | 27.62 | NaN |
| 21 | 2025-04-08 | Buy to Open | VIX 06/18/2025 27.00 P | 4 | 4.55 | 4.32 | 1824.32 | 55.44 | Averaged down on existing position |
| 22 | 2025-05-12 | Sell to Close | VIX 06/18/2025 27.00 P | 6 | 7.55 | 6.49 | 4523.51 | 19.05 | Market up on positive news of lowering tariffs... |
| 23 | 2025-05-12 | Sell to Close | VIX 06/18/2025 27.00 P | 6 | 7.40 | 6.49 | 4433.51 | 19.47 | Market up on positive news of lowering tariffs... |
| 24 | 2025-04-04 | Buy to Open | VIX 06/18/2025 36.00 P | 3 | 13.40 | 3.24 | 4023.24 | 36.61 | NaN |
| 25 | 2025-05-12 | Sell to Close | VIX 06/18/2025 36.00 P | 3 | 16.00 | 3.24 | 4796.76 | 19.14 | Market up on positive news of lowering tariffs... |
| 26 | 2025-04-07 | Buy to Open | VIX 06/18/2025 45.00 P | 2 | 18.85 | 2.16 | 3772.16 | 53.65 | NaN |
| 27 | 2025-05-12 | Sell to Close | VIX 06/18/2025 45.00 P | 2 | 25.00 | 2.16 | 4997.84 | 19.24 | Market up on positive news of lowering tariffs... |
| 28 | 2025-04-03 | Buy to Open | VIX 07/16/2025 29.00 P | 5 | 8.55 | 5.40 | 4280.40 | 29.03 | NaN |
| 29 | 2025-05-13 | Sell to Close | VIX 07/16/2025 29.00 P | 3 | 10.40 | 3.24 | 3116.76 | 17.72 | NaN |
| 30 | 2025-05-13 | Sell to Close | VIX 07/16/2025 29.00 P | 2 | 10.30 | 2.16 | 2057.84 | 17.68 | NaN |
| 31 | 2025-04-04 | Buy to Open | VIX 07/16/2025 36.00 P | 3 | 13.80 | 3.24 | 4143.24 | 36.95 | NaN |
| 32 | 2025-05-13 | Sell to Close | VIX 07/16/2025 36.00 P | 1 | 17.00 | 1.08 | 1698.92 | 17.79 | NaN |
| 33 | 2025-05-13 | Sell to Close | VIX 07/16/2025 36.00 P | 2 | 16.90 | 2.16 | 3377.84 | 17.72 | NaN |
| 34 | 2025-04-07 | Buy to Open | VIX 07/16/2025 45.00 P | 2 | 21.55 | 2.16 | 4312.16 | 46.17 | NaN |
| 35 | 2025-05-13 | Sell to Close | VIX 07/16/2025 45.00 P | 2 | 25.65 | 2.16 | 5127.84 | 17.96 | NaN |
| 36 | 2025-04-07 | Buy to Open | VIX 08/20/2025 45.00 P | 2 | 21.75 | 2.16 | 4352.16 | 49.07 | NaN |
| 37 | 2025-05-13 | Sell to Close | VIX 08/20/2025 45.00 P | 2 | 25.40 | 2.16 | 5077.84 | 18.06 | NaN |
| 38 | 2025-06-26 | Buy to Open | VIX 09/17/2025 20.00 C | 10 | 3.00 | 10.81 | 3010.81 | 16.37 | Opened long dated call position; VIX level at ... |
| 39 | 2025-08-01 | Sell to Close | VIX 09/17/2025 20.00 C | 5 | 3.05 | 5.40 | 1519.60 | 20.48 | Sold half of position due to theta drag, held ... |
| 40 | 2025-08-12 | Buy to Open | VIX 09/17/2025 20.00 C | 10 | 1.54 | 9.31 | 1549.31 | 14.87 | Doubled existing position to reduce cost basis |
| 41 | 2025-08-12 | Buy to Open | VIX 10/22/2025 19.00 C | 10 | 3.05 | 10.81 | 3060.81 | 15.22 | NaN |
| 42 | 2025-07-23 | Buy to Open | VIX 10/22/2025 21.00 C | 10 | 2.92 | 10.81 | 2930.81 | 15.40 | Continued low volatility, opened long dated ca... |
| 43 | 2025-06-26 | Buy to Open | VIX 10/22/2025 22.00 C | 10 | 2.94 | 10.81 | 2950.81 | 16.43 | Opened long dated call position; VIX level at ... |
| 44 | 2025-07-17 | Buy to Open | VIX 10/22/2025 23.00 C | 10 | 2.75 | 10.81 | 2760.81 | 16.86 | Continued low volatility, opened long dated ca... |
| 45 | 2025-08-13 | Buy to Open | VIX 11/19/2025 20.00 C | 10 | 3.26 | 10.81 | 3270.81 | 14.56 | VIX at ~0 decile based on the YTD VIX data |
| 46 | 2025-08-12 | Buy to Open | VIX 11/19/2025 21.00 C | 10 | 3.00 | 10.81 | 3010.81 | 15.17 | NaN |
In [83]:
# Copy this <!-- INSERT_10_Trades_Executed_HERE --> to index_temp.md
export_track_md_deps(dep_file=dep_file, md_filename="10_Trades_Executed.md", content=vix_transactions_no_exp.to_markdown(index=False, floatfmt=".2f"))
✅ Exported and tracked: 10_Trades_Executed.md
Volatility In August 2024¶
In [84]:
# Variables to be modifed
esd = "2024-09-18" # Expiration Start Date
eed = "2024-12-18" # Expiration End Date
tsd = "2024-08-05" # Trade Start Date
ted = "2024-11-27" # Trade End Date
index_number = "11"
x_tick_spacing = 10
y_tick_spacing = 5
############################################
## Do not modify the code below this line ##
############################################
trades, closed_pos, open_pos, per_pnl, pnl, tot_opened_pos_mkt_val, tot_closed_pos_mkt_val = calc_vix_trade_pnl(
transaction_df=vix_transactions,
exp_start_date=esd,
exp_end_date=eed,
trade_start_date=tsd,
trade_end_date=ted,
)
# Convert to datetime objects
tsd_dt = datetime.strptime(tsd, "%Y-%m-%d")
ted_dt = datetime.strptime(ted, "%Y-%m-%d")
# Adjust the plot start and end dates
plot_start = tsd_dt - timedelta(days=10)
plot_end = ted_dt + timedelta(days=10)
plot_vix_with_trades(
vix_price_df=vix,
trades_df=trades,
plot_start_date=plot_start.strftime("%Y-%m-%d"),
plot_end_date=plot_end.strftime("%Y-%m-%d"),
x_tick_spacing=x_tick_spacing,
y_tick_spacing=y_tick_spacing,
index_number=index_number,
export_plot=True,
)
print(f"<!-- INSERT_{index_number}_Closed_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Open_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Opened_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Closed_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_PnL_HERE -->")
print(f"<!-- INSERT_{index_number}_Percent_PnL_HERE -->")
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Closed_Positions.md", content=closed_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Open_Positions.md", content=open_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Opened_Position_Market_Value.md", content=tot_opened_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Closed_Position_Market_Value.md", content=tot_closed_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_PnL.md", content=pnl)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Percent_PnL.md", content=per_pnl)
<!-- INSERT_11_Closed_Positions_HERE --> <!-- INSERT_11_Open_Positions_HERE --> <!-- INSERT_11_Total_Opened_Position_Market_Value_HERE --> <!-- INSERT_11_Total_Closed_Position_Market_Value_HERE --> <!-- INSERT_11_PnL_HERE --> <!-- INSERT_11_Percent_PnL_HERE --> ✅ Exported and tracked: 11_Closed_Positions.md ✅ Exported and tracked: 11_Open_Positions.md ✅ Exported and tracked: 11_Total_Opened_Position_Market_Value.md ✅ Exported and tracked: 11_Total_Closed_Position_Market_Value.md ✅ Exported and tracked: 11_PnL.md ✅ Exported and tracked: 11_Percent_PnL.md
Volatility In March 2025¶
In [85]:
# Variables to be modifed
esd = "2025-04-16" # Expiration Start Date
eed = "2025-04-16" # Expiration End Date
tsd = "2025-03-04" # Trade Start Date
ted = "2025-03-24" # Trade End Date
index_number = "12"
x_tick_spacing = 2
y_tick_spacing = 2
############################################
## Do not modify the code below this line ##
############################################
trades, closed_pos, open_pos, per_pnl, pnl, tot_opened_pos_mkt_val, tot_closed_pos_mkt_val = calc_vix_trade_pnl(
transaction_df=vix_transactions,
exp_start_date=esd,
exp_end_date=eed,
trade_start_date=tsd,
trade_end_date=ted,
)
# Convert to datetime objects
tsd_dt = datetime.strptime(tsd, "%Y-%m-%d")
ted_dt = datetime.strptime(ted, "%Y-%m-%d")
# Adjust the plot start and end dates
plot_start = tsd_dt - timedelta(days=10)
plot_end = ted_dt + timedelta(days=10)
plot_vix_with_trades(
vix_price_df=vix,
trades_df=trades,
plot_start_date=plot_start.strftime("%Y-%m-%d"),
plot_end_date=plot_end.strftime("%Y-%m-%d"),
x_tick_spacing=x_tick_spacing,
y_tick_spacing=y_tick_spacing,
index_number=index_number,
export_plot=True,
)
print(f"<!-- INSERT_{index_number}_Closed_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Open_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Opened_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Closed_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_PnL_HERE -->")
print(f"<!-- INSERT_{index_number}_Percent_PnL_HERE -->")
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Closed_Positions.md", content=closed_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Open_Positions.md", content=open_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Opened_Position_Market_Value.md", content=tot_opened_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Closed_Position_Market_Value.md", content=tot_closed_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_PnL.md", content=pnl)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Percent_PnL.md", content=per_pnl)
<!-- INSERT_12_Closed_Positions_HERE --> <!-- INSERT_12_Open_Positions_HERE --> <!-- INSERT_12_Total_Opened_Position_Market_Value_HERE --> <!-- INSERT_12_Total_Closed_Position_Market_Value_HERE --> <!-- INSERT_12_PnL_HERE --> <!-- INSERT_12_Percent_PnL_HERE --> ✅ Exported and tracked: 12_Closed_Positions.md ✅ Exported and tracked: 12_Open_Positions.md ✅ Exported and tracked: 12_Total_Opened_Position_Market_Value.md ✅ Exported and tracked: 12_Total_Closed_Position_Market_Value.md ✅ Exported and tracked: 12_PnL.md ✅ Exported and tracked: 12_Percent_PnL.md
Volatility In April 2025¶
In [86]:
# Variables to be modifed
esd = "2025-05-21" # Expiration Start Date
eed = "2025-08-20" # Expiration End Date
tsd = "2025-03-10" # Trade Start Date
ted = "2025-05-13" # Trade End Date
index_number = "13"
x_tick_spacing = 5
y_tick_spacing = 5
############################################
## Do not modify the code below this line ##
############################################
trades, closed_pos, open_pos, per_pnl, pnl, tot_opened_pos_mkt_val, tot_closed_pos_mkt_val = calc_vix_trade_pnl(
transaction_df=vix_transactions,
exp_start_date=esd,
exp_end_date=eed,
trade_start_date=tsd,
trade_end_date=ted,
)
# Convert to datetime objects
tsd_dt = datetime.strptime(tsd, "%Y-%m-%d")
ted_dt = datetime.strptime(ted, "%Y-%m-%d")
# Adjust the plot start and end dates
plot_start = tsd_dt - timedelta(days=10)
plot_end = ted_dt + timedelta(days=10)
plot_vix_with_trades(
vix_price_df=vix,
trades_df=trades,
plot_start_date=plot_start.strftime("%Y-%m-%d"),
plot_end_date=plot_end.strftime("%Y-%m-%d"),
x_tick_spacing=x_tick_spacing,
y_tick_spacing=y_tick_spacing,
index_number=index_number,
export_plot=True,
)
print(f"<!-- INSERT_{index_number}_Closed_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Open_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Opened_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Closed_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_PnL_HERE -->")
print(f"<!-- INSERT_{index_number}_Percent_PnL_HERE -->")
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Closed_Positions.md", content=closed_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Open_Positions.md", content=open_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Opened_Position_Market_Value.md", content=tot_opened_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Closed_Position_Market_Value.md", content=tot_closed_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_PnL.md", content=pnl)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Percent_PnL.md", content=per_pnl)
<!-- INSERT_13_Closed_Positions_HERE --> <!-- INSERT_13_Open_Positions_HERE --> <!-- INSERT_13_Total_Opened_Position_Market_Value_HERE --> <!-- INSERT_13_Total_Closed_Position_Market_Value_HERE --> <!-- INSERT_13_PnL_HERE --> <!-- INSERT_13_Percent_PnL_HERE --> ✅ Exported and tracked: 13_Closed_Positions.md ✅ Exported and tracked: 13_Open_Positions.md ✅ Exported and tracked: 13_Total_Opened_Position_Market_Value.md ✅ Exported and tracked: 13_Total_Closed_Position_Market_Value.md ✅ Exported and tracked: 13_PnL.md ✅ Exported and tracked: 13_Percent_PnL.md
Low Volatility In June 2025¶
In [87]:
# Variables to be modifed
esd = "2025-09-17" # Expiration Start Date
eed = "2025-12-31" # Expiration End Date
tsd = "2025-06-26" # Trade Start Date
ted = "2025-12-31" # Trade End Date
index_number = "14"
x_tick_spacing = 5
y_tick_spacing = 1
############################################
## Do not modify the code below this line ##
############################################
trades, closed_pos, open_pos, per_pnl, pnl, tot_opened_pos_mkt_val, tot_closed_pos_mkt_val = calc_vix_trade_pnl(
transaction_df=vix_transactions,
exp_start_date=esd,
exp_end_date=eed,
trade_start_date=tsd,
trade_end_date=ted,
)
# Convert to datetime objects
tsd_dt = datetime.strptime(tsd, "%Y-%m-%d")
ted_dt = datetime.strptime(ted, "%Y-%m-%d")
# Adjust the plot start and end dates
plot_start = tsd_dt - timedelta(days=10)
plot_end = ted_dt + timedelta(days=10)
plot_vix_with_trades(
vix_price_df=vix,
trades_df=trades,
plot_start_date=plot_start.strftime("%Y-%m-%d"),
plot_end_date=plot_end.strftime("%Y-%m-%d"),
x_tick_spacing=x_tick_spacing,
y_tick_spacing=y_tick_spacing,
index_number=index_number,
export_plot=True,
)
print(f"<!-- INSERT_{index_number}_Closed_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Open_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Opened_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Closed_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_PnL_HERE -->")
print(f"<!-- INSERT_{index_number}_Percent_PnL_HERE -->")
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Closed_Positions.md", content=closed_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Open_Positions.md", content=open_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Opened_Position_Market_Value.md", content=tot_opened_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Closed_Position_Market_Value.md", content=tot_closed_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_PnL.md", content=pnl)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Percent_PnL.md", content=per_pnl)
<!-- INSERT_14_Closed_Positions_HERE --> <!-- INSERT_14_Open_Positions_HERE --> <!-- INSERT_14_Total_Opened_Position_Market_Value_HERE --> <!-- INSERT_14_Total_Closed_Position_Market_Value_HERE --> <!-- INSERT_14_PnL_HERE --> <!-- INSERT_14_Percent_PnL_HERE --> ✅ Exported and tracked: 14_Closed_Positions.md ✅ Exported and tracked: 14_Open_Positions.md ✅ Exported and tracked: 14_Total_Opened_Position_Market_Value.md ✅ Exported and tracked: 14_Total_Closed_Position_Market_Value.md ✅ Exported and tracked: 14_PnL.md ✅ Exported and tracked: 14_Percent_PnL.md
Complete Trade History¶
In [88]:
# Variables to be modifed
esd = None
eed = None
tsd = None
ted = None
index_number = "99"
############################################
## Do not modify the code below this line ##
############################################
trades, closed_pos, open_pos, per_pnl, pnl, tot_opened_pos_mkt_val, tot_closed_pos_mkt_val = calc_vix_trade_pnl(
transaction_df=vix_transactions,
exp_start_date=esd,
exp_end_date=eed,
trade_start_date=tsd,
trade_end_date=ted,
)
print(f"<!-- INSERT_{index_number}_Closed_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Open_Positions_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Opened_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_Total_Closed_Position_Market_Value_HERE -->")
print(f"<!-- INSERT_{index_number}_PnL_HERE -->")
print(f"<!-- INSERT_{index_number}_Percent_PnL_HERE -->")
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Closed_Positions.md", content=closed_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Open_Positions.md", content=open_pos.to_markdown(index=False, floatfmt=".2f"))
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Opened_Position_Market_Value.md", content=tot_opened_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Total_Closed_Position_Market_Value.md", content=tot_closed_pos_mkt_val)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_PnL.md", content=pnl)
export_track_md_deps(dep_file=dep_file, md_filename=f"{index_number}_Percent_PnL.md", content=per_pnl)
<!-- INSERT_99_Closed_Positions_HERE --> <!-- INSERT_99_Open_Positions_HERE --> <!-- INSERT_99_Total_Opened_Position_Market_Value_HERE --> <!-- INSERT_99_Total_Closed_Position_Market_Value_HERE --> <!-- INSERT_99_PnL_HERE --> <!-- INSERT_99_Percent_PnL_HERE --> ✅ Exported and tracked: 99_Closed_Positions.md ✅ Exported and tracked: 99_Open_Positions.md ✅ Exported and tracked: 99_Total_Opened_Position_Market_Value.md ✅ Exported and tracked: 99_Total_Closed_Position_Market_Value.md ✅ Exported and tracked: 99_PnL.md ✅ Exported and tracked: 99_Percent_PnL.md