{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "af91626b",
   "metadata": {},
   "source": [
    "<div style=\"background-color:#000;\"><img src=\"pqn.png\"></img></div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42f5827b",
   "metadata": {},
   "source": [
    "This notebook implements a momentum-based trading strategy using Zipline and performs performance analysis with Alphalens. It defines custom factors to measure stock momentum and uses these factors to build a trading pipeline. The strategy is then backtested over a specified period, and results are analyzed using information coefficients and other metrics. This is useful for quantitative trading, backtesting, and performance evaluation of trading strategies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "45bbe3ff",
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "from scipy import stats\n",
    "import matplotlib.pyplot as plt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f537421d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from zipline import run_algorithm\n",
    "from zipline.pipeline import Pipeline, CustomFactor\n",
    "from zipline.pipeline.data import USEquityPricing\n",
    "from zipline.api import (\n",
    "    attach_pipeline,\n",
    "    calendars,\n",
    "    pipeline_output,\n",
    "    date_rules,\n",
    "    time_rules,\n",
    "    set_commission,\n",
    "    set_slippage,\n",
    "    record,\n",
    "    order_target_percent,\n",
    "    get_open_orders,\n",
    "    schedule_function\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8182c403",
   "metadata": {},
   "outputs": [],
   "source": [
    "from alphalens.utils import (\n",
    "    get_clean_factor_and_forward_returns,\n",
    "    get_forward_returns_columns\n",
    ")\n",
    "from alphalens.plotting import plot_ic_ts\n",
    "from alphalens.performance import factor_information_coefficient, mean_information_coefficient"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16f879d5",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9aa346e9",
   "metadata": {},
   "source": [
    "Define the number of long and short positions to hold in the portfolio"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c5a639c8",
   "metadata": {},
   "outputs": [],
   "source": [
    "N_LONGS = N_SHORTS = 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd497509",
   "metadata": {},
   "source": [
    "Define a custom factor class to compute momentum"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f9d77ed6",
   "metadata": {},
   "outputs": [],
   "source": [
    "class Momentum(CustomFactor):\n",
    "    \"\"\"Computes momentum factor for assets.\"\"\"\n",
    "    \n",
    "    inputs = [USEquityPricing.close]\n",
    "\n",
    "    def compute(self, today, assets, out, close):\n",
    "        \"\"\"Computes momentum as the ratio of latest to earliest closing price over the window.\"\"\"\n",
    "        out[:] = close[-1] / close[0]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17b1ae3f",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "977f61af",
   "metadata": {},
   "source": [
    "Define a pipeline to fetch and filter stocks based on momentum"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ea9970cd",
   "metadata": {},
   "outputs": [],
   "source": [
    "def make_pipeline():\n",
    "    \"\"\"Creates a pipeline for fetching and filtering stocks based on momentum.\"\"\"\n",
    "    \n",
    "    twenty_day_momentum = Momentum(window_length=20)\n",
    "    thirty_day_momentum = Momentum(window_length=30)\n",
    "\n",
    "    positive_momentum = (\n",
    "        (twenty_day_momentum > 1) & \n",
    "        (thirty_day_momentum > 1)\n",
    "    )\n",
    "\n",
    "    return Pipeline(\n",
    "        columns={\n",
    "            'longs': thirty_day_momentum.top(N_LONGS),\n",
    "            'shorts': thirty_day_momentum.top(N_SHORTS),\n",
    "            'ranking': thirty_day_momentum.rank(ascending=False)\n",
    "        },\n",
    "        screen=positive_momentum\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1c8c73f8",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "1494ac97",
   "metadata": {},
   "source": [
    "Define a function that runs before the trading day starts"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ac225d0c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def before_trading_start(context, data):\n",
    "    \"\"\"Fetches factor data before trading starts.\"\"\"\n",
    "    \n",
    "    context.factor_data = pipeline_output(\"factor_pipeline\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c7b1854b",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "f1bd1bb6",
   "metadata": {},
   "source": [
    "Define the initialize function to set up the algorithm"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cf08b200",
   "metadata": {},
   "outputs": [],
   "source": [
    "def initialize(context):\n",
    "    \"\"\"Initializes the trading algorithm and schedules rebalancing.\"\"\"\n",
    "    \n",
    "    attach_pipeline(make_pipeline(), \"factor_pipeline\")\n",
    "    schedule_function(\n",
    "        rebalance,\n",
    "        date_rules.week_start(),\n",
    "        time_rules.market_open(),\n",
    "        calendar=calendars.US_EQUITIES,\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cda91bd5",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "a1eebf33",
   "metadata": {},
   "source": [
    "Define the rebalancing function to execute trades based on factor data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dd4c8964",
   "metadata": {},
   "outputs": [],
   "source": [
    "def rebalance(context, data):\n",
    "    \"\"\"Rebalances the portfolio based on factor data.\"\"\"\n",
    "    \n",
    "    factor_data = context.factor_data\n",
    "    record(factor_data=factor_data.ranking)\n",
    "\n",
    "    assets = factor_data.index\n",
    "    record(prices=data.current(assets, 'price'))\n",
    "\n",
    "    longs = assets[factor_data.longs]\n",
    "    shorts = assets[factor_data.shorts]\n",
    "    divest = set(context.portfolio.positions.keys()) - set(longs.union(shorts))\n",
    "\n",
    "    exec_trades(data, assets=divest, target_percent=0)\n",
    "    exec_trades(data, assets=longs, target_percent=1 / N_LONGS)\n",
    "    exec_trades(data, assets=shorts, target_percent=-1 / N_SHORTS)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25e1653e",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "ae353e46",
   "metadata": {},
   "source": [
    "Define a function to execute trades for given assets and target percentages"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2d55bfdf",
   "metadata": {},
   "outputs": [],
   "source": [
    "def exec_trades(data, assets, target_percent):\n",
    "    \"\"\"Executes trades for given assets to achieve target portfolio weights.\"\"\"\n",
    "    \n",
    "    for asset in assets:\n",
    "        if data.can_trade(asset) and not get_open_orders(asset):\n",
    "            order_target_percent(asset, target_percent)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6b8b6b5a",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "17866c5a",
   "metadata": {},
   "source": [
    "Define the backtesting period"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cffe0933",
   "metadata": {},
   "outputs": [],
   "source": [
    "start = pd.Timestamp('2015')\n",
    "end = pd.Timestamp('2018')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9cc5a44e",
   "metadata": {},
   "source": [
    "Run the backtest using the specified start and end dates"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9bdcefdf",
   "metadata": {},
   "outputs": [],
   "source": [
    "perf = run_algorithm(\n",
    "    start=start,\n",
    "    end=end,\n",
    "    initialize=initialize,\n",
    "    before_trading_start=before_trading_start,\n",
    "    capital_base=100_000,\n",
    "    bundle=\"quandl\",\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b744283",
   "metadata": {},
   "source": [
    "Get the final portfolio value"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fe5fc4f4",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "perf.portfolio_value"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "76841808",
   "metadata": {},
   "source": [
    "Construct a DataFrame with stock prices and corresponding dates"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d155f3a5",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices = pd.concat(\n",
    "    [df.to_frame(d) for d, df in perf.prices.dropna().items()], \n",
    "    axis=1\n",
    ").T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b843ad7",
   "metadata": {},
   "source": [
    "Convert column names to stock symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d2c08533",
   "metadata": {},
   "outputs": [],
   "source": [
    "prices.columns = [col.symbol for col in prices.columns]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "80f6ea3a",
   "metadata": {},
   "source": [
    "Normalize the index to midnight, preserving timezone information"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0e039f8",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "prices.index = prices.index.normalize()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "024c97d3",
   "metadata": {},
   "source": [
    "Construct a DataFrame with factor ranks and corresponding dates"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "eca244f6",
   "metadata": {},
   "outputs": [],
   "source": [
    "factor_data = pd.concat(\n",
    "    [df.to_frame(d) for d, df in perf.factor_data.dropna().items()],\n",
    "    axis=1\n",
    ").T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "37bcda09",
   "metadata": {},
   "source": [
    "Convert column names to stock symbols"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0e14e9b5",
   "metadata": {},
   "outputs": [],
   "source": [
    "factor_data.columns = [col.symbol for col in factor_data.columns]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e0827b1d",
   "metadata": {},
   "source": [
    "Normalize the index to midnight, preserving timezone information"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "50c1200d",
   "metadata": {},
   "outputs": [],
   "source": [
    "factor_data.index = factor_data.index.normalize()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ecbce84",
   "metadata": {},
   "source": [
    "Create a MultiIndex with date and asset"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "79888180",
   "metadata": {},
   "outputs": [],
   "source": [
    "factor_data = factor_data.stack()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "675d5dc9",
   "metadata": {},
   "source": [
    "Rename the MultiIndex levels"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2050795f",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "factor_data.index.names = ['date', 'asset']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6a5240f",
   "metadata": {},
   "source": [
    "Compile forward returns, factor ranks, and factor quantiles"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fe1db4bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "alphalens_data = get_clean_factor_and_forward_returns(\n",
    "    factor=factor_data, prices=prices, periods=(5, 10, 21, 63), quantiles=5\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cda0df84",
   "metadata": {},
   "source": [
    "Display the compiled Alphalens data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "70e9a889",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "alphalens_data"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "38144b65",
   "metadata": {},
   "source": [
    "Generate the information coefficient for each holding period on each date"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3af97fa7",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "ic = factor_information_coefficient(alphalens_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b256162d",
   "metadata": {},
   "source": [
    "Calculate the mean information coefficient over all periods"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "254c0617",
   "metadata": {},
   "outputs": [],
   "source": [
    "ii = mean_information_coefficient(alphalens_data)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3007dad5",
   "metadata": {},
   "source": [
    "Plot the mean information coefficient"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3ba48590",
   "metadata": {},
   "outputs": [],
   "source": [
    "ii.plot()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56d5a42a",
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "id": "f12e37c8",
   "metadata": {},
   "source": [
    "Plot the information coefficient for the 5-day holding period"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56d4b894",
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_ic_ts(ic[[\"5D\"]])\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b24451da",
   "metadata": {},
   "source": [
    "Calculate mean information coefficient per holding period per year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dda93f62",
   "metadata": {},
   "outputs": [],
   "source": [
    "ic_by_year = ic.resample('A').mean()\n",
    "ic_by_year.index = ic_by_year.index.year"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a2167b6a",
   "metadata": {},
   "source": [
    "Plot the mean information coefficient by year"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1ea9e9bb",
   "metadata": {},
   "outputs": [],
   "source": [
    "ic_by_year.plot.bar(figsize=(14, 6))\n",
    "plt.tight_layout()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2003869d",
   "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
}
