{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "607fd8fb",
   "metadata": {},
   "source": [
    "<div style=\"background-color:#000;\"><img src=\"pqn.png\"></img></div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "49879d7a",
   "metadata": {},
   "outputs": [],
   "source": [
    "import time\n",
    "from vectorbtpro import *\n",
    "import pandas as pd\n",
    "import scipy.stats as st\n",
    "import statsmodels.tsa.stattools as ts  \n",
    "import numpy as np\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a726d955",
   "metadata": {},
   "source": [
    "### Load and save S&P 500 tickers and their data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8eed1e5d",
   "metadata": {},
   "source": [
    "First, we load the S&P 500 tickers from Wikipedia and save their historical data if it doesn't already exist. We will store the data in an HDF5 file format. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f481675b",
   "metadata": {},
   "outputs": [],
   "source": [
    "sp500_tickers = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies')[0]['Symbol'].tolist()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "95b7da8c",
   "metadata": {},
   "outputs": [],
   "source": [
    "COINT_FILE = \"coint_pvalues.pickle\"\n",
    "POOL_FILE = \"data_pool.h5\"\n",
    "START = \"2015-01-01\"\n",
    "END = \"2023-12-31\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8be1d20a",
   "metadata": {},
   "outputs": [],
   "source": [
    "if not vbt.file_exists(POOL_FILE):\n",
    "    with vbt.ProgressBar(total=len(sp500_tickers)) as pbar:\n",
    "        collected = 0\n",
    "        for symbol in sp500_tickers:\n",
    "            try:\n",
    "                data = vbt.YFData.pull(\n",
    "                    symbol,\n",
    "                    start=START,\n",
    "                    end=END,\n",
    "                    silence_warnings=True,\n",
    "                )\n",
    "                data.to_hdf(POOL_FILE)\n",
    "                collected += 1\n",
    "            except:\n",
    "                pass\n",
    "            pbar.set_prefix(f\"{symbol} ({collected})\")\n",
    "            pbar.update()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4f1e195c",
   "metadata": {},
   "source": [
    "We load the S&P 500 tickers from Wikipedia using pandas. We then check if the data file already exists. If it does not, we initialize a progress bar and start collecting historical data for each ticker using the vectorbtpro library's YFData.pull method. The collected data is then saved into an HDF5 file. If there is an error while collecting data for a ticker, we simply pass and move to the next ticker."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9870389a",
   "metadata": {},
   "source": [
    "### Filter and select valid symbols from the data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ad74b67c",
   "metadata": {},
   "source": [
    "Next, we load the saved data, filter out any symbols with missing data, and keep only valid symbols. This ensures we work with complete datasets."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4896730c",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = vbt.HDFData.pull(\n",
    "    POOL_FILE,\n",
    "    start=START,\n",
    "    end=END,\n",
    "    silence_warnings=True\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f8e9058",
   "metadata": {
    "lines_to_next_cell": 1
   },
   "outputs": [],
   "source": [
    "data = data.select_symbols([\n",
    "    k\n",
    "    for k, v in data.data.items()\n",
    "    if not v.isnull().any().any()\n",
    "])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9cac3f6d",
   "metadata": {},
   "source": [
    "We load the saved data from the HDF5 file. We then iterate over each symbol's data and check for missing values. If a symbol has any missing values, it is excluded. This ensures that we only work with complete datasets, which is crucial for accurate analysis."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cada47c4",
   "metadata": {},
   "source": [
    "### Identify cointegrated pairs of stocks"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6e2ea5ba",
   "metadata": {},
   "source": [
    "Now, we identify pairs of stocks that are cointegrated. Cointegration helps us find pairs of stocks that have a stable relationship over time, which is essential for pairs trading strategies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "36050b45",
   "metadata": {
    "lines_to_next_cell": 1
   },
   "outputs": [],
   "source": [
    "@vbt.parameterized(\n",
    "    merge_func=\"concat\",\n",
    "    engine=\"pathos\",\n",
    "    distribute=\"chunks\",\n",
    "    n_chunks=\"auto\"\n",
    ")\n",
    "def coint_pvalue(close, s1, s2):\n",
    "    return ts.coint(np.log(close[s1]), np.log(close[s2]))[1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5863250c",
   "metadata": {},
   "outputs": [],
   "source": [
    "if not vbt.file_exists(COINT_FILE):\n",
    "    coint_pvalues = coint_pvalue(\n",
    "        data.close,\n",
    "        vbt.Param(data.symbols, condition=\"s1 != s2\"),\n",
    "        vbt.Param(data.symbols)\n",
    "    )\n",
    "    vbt.save(coint_pvalues, COINT_FILE)\n",
    "else:\n",
    "    coint_pvalues = vbt.load(COINT_FILE)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "85224b4e",
   "metadata": {},
   "outputs": [],
   "source": [
    "coint_pvalues = coint_pvalues.sort_values()\n",
    "coint_pvalues.head(20)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3397e3ea",
   "metadata": {},
   "source": [
    "We define a function to calculate the cointegration p-value between two stock price series. We use the parameterized decorator to parallelize the computation. If the cointegration p-values file does not exist, we calculate the p-values for all pairs of stocks and save the results. Otherwise, we load the saved p-values. We then sort the p-values in ascending order and display the top 20 pairs with the lowest p-values, indicating the strongest cointegration."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "019e2328",
   "metadata": {},
   "source": [
    "### Analyze and visualize the selected stock pair"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e5cfedc",
   "metadata": {},
   "source": [
    "Finally, we choose a specific pair of stocks, analyze their price relationship, and visualize their spread and z-score. This helps us understand their mean-reverting behavior for potential trading opportunities."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b7ea967b",
   "metadata": {},
   "outputs": [],
   "source": [
    "S1, S2 = \"WYNN\", \"DVN\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "839b843a",
   "metadata": {},
   "outputs": [],
   "source": [
    "data.plot(column=\"Close\", symbol=[S1, S2], base=1).show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "edabec9b",
   "metadata": {},
   "outputs": [],
   "source": [
    "S1_log = np.log(data.get(\"Close\", S1))\n",
    "S2_log = np.log(data.get(\"Close\", S2))\n",
    "log_diff = (S2_log - S1_log).rename(\"Log diff\")\n",
    "fig = log_diff.vbt.plot()\n",
    "fig.add_hline(y=log_diff.mean(), line_color=\"yellow\", line_dash=\"dot\")\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fc73a803",
   "metadata": {},
   "outputs": [],
   "source": [
    "data = vbt.YFData.pull(\n",
    "    [S1, S2],\n",
    "    start=START,\n",
    "    end=END,\n",
    "    silence_warnings=True,\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e1e36a43",
   "metadata": {},
   "outputs": [],
   "source": [
    "UPPER = st.norm.ppf(1 - 0.05 / 2)\n",
    "LOWER = -st.norm.ppf(1 - 0.05 / 2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "40980b4a",
   "metadata": {},
   "outputs": [],
   "source": [
    "S1_close = data.get(\"Close\", S1)\n",
    "S2_close = data.get(\"Close\", S2)\n",
    "ols = vbt.OLS.run(S1_close, S2_close, window=vbt.Default(21))\n",
    "spread = ols.error.rename(\"Spread\")\n",
    "zscore = ols.zscore.rename(\"Z-score\")\n",
    "print(pd.concat((spread, zscore), axis=1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "81afd692",
   "metadata": {},
   "outputs": [],
   "source": [
    "upper_crossed = zscore.vbt.crossed_above(UPPER)\n",
    "lower_crossed = zscore.vbt.crossed_below(LOWER)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eb36548f",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig = zscore.vbt.plot()\n",
    "fig.add_hline(y=UPPER, line_color=\"orangered\", line_dash=\"dot\")\n",
    "fig.add_hline(y=0, line_color=\"yellow\", line_dash=\"dot\")\n",
    "fig.add_hline(y=LOWER, line_color=\"limegreen\", line_dash=\"dot\")\n",
    "upper_crossed.vbt.signals.plot_as_exits(zscore, fig=fig)\n",
    "lower_crossed.vbt.signals.plot_as_entries(zscore, fig=fig)\n",
    "fig.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e650b931",
   "metadata": {},
   "outputs": [],
   "source": [
    "long_entries = data.symbol_wrapper.fill(False)\n",
    "short_entries = data.symbol_wrapper.fill(False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8571c745",
   "metadata": {},
   "outputs": [],
   "source": [
    "short_entries.loc[upper_crossed, S1] = True\n",
    "long_entries.loc[upper_crossed, S2] = True\n",
    "long_entries.loc[lower_crossed, S1] = True\n",
    "short_entries.loc[lower_crossed, S2] = True"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "66168c29",
   "metadata": {},
   "outputs": [],
   "source": [
    "pf = vbt.Portfolio.from_signals(\n",
    "    data,\n",
    "    entries=long_entries,\n",
    "    short_entries=short_entries,\n",
    "    size=10,\n",
    "    size_type=\"valuepercent100\",\n",
    "    group_by=True,\n",
    "    cash_sharing=True,\n",
    "    call_seq=\"auto\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5332cb0",
   "metadata": {},
   "outputs": [],
   "source": [
    "pf.stats()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de8b48d9",
   "metadata": {},
   "source": [
    "We select two specific stocks and plot their closing prices. We then calculate their log-price difference and plot it to analyze their mean-reverting behavior. We pull the latest data for the selected stocks and calculate the spread and z-score using a rolling window OLS regression. We identify the points where the z-score crosses above or below the thresholds and plot these signals. Finally, we define long and short entry signals and create a portfolio based on these signals to evaluate its performance."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc7c7218",
   "metadata": {},
   "source": [
    "### Your next steps"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9b35e7cf-3a73-488f-bbe3-b818906bb040",
   "metadata": {},
   "source": [
    "Try changing the selected stock pair to explore different cointegrated pairs. You can also adjust the z-score thresholds to see how it affects your trading signals. Experiment with different window sizes in the OLS regression to find an optimal setting for your strategy."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1af444c5",
   "metadata": {},
   "source": [
    "<a href=\"https://pyquantnews.com/\">PyQuant News</a> is where finance practitioners level up with Python for quant finance, algorithmic trading, and market data analysis. Looking to get started? Check out the fastest growing, top-selling course to <a href=\"https://gettingstartedwithpythonforquantfinance.com/\">get started with Python for quant finance</a>. For educational purposes. Not investment advise. Use at your own risk."
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "main_language": "python",
   "notebook_metadata_filter": "-all"
  },
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
