Files
strategy-lab/to_explore/pyquantnews/45_AlphaLens.ipynb
David Brazda e3da60c647 daily update
2024-10-21 20:57:56 +02:00

16 KiB

No description has been provided for this image

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.

In [ ]:
import pandas as pd
from scipy import stats
import matplotlib.pyplot as plt
In [ ]:
from zipline import run_algorithm
from zipline.pipeline import Pipeline, CustomFactor
from zipline.pipeline.data import USEquityPricing
from zipline.api import (
    attach_pipeline,
    calendars,
    pipeline_output,
    date_rules,
    time_rules,
    set_commission,
    set_slippage,
    record,
    order_target_percent,
    get_open_orders,
    schedule_function
)
In [ ]:
from alphalens.utils import (
    get_clean_factor_and_forward_returns,
    get_forward_returns_columns
)
from alphalens.plotting import plot_ic_ts
from alphalens.performance import factor_information_coefficient, mean_information_coefficient
In [ ]:
import warnings
warnings.filterwarnings("ignore")

Define the number of long and short positions to hold in the portfolio

In [ ]:
N_LONGS = N_SHORTS = 10

Define a custom factor class to compute momentum

In [ ]:
class Momentum(CustomFactor):
    """Computes momentum factor for assets."""
    
    inputs = [USEquityPricing.close]

    def compute(self, today, assets, out, close):
        """Computes momentum as the ratio of latest to earliest closing price over the window."""
        out[:] = close[-1] / close[0]
In [ ]:
 

Define a pipeline to fetch and filter stocks based on momentum

In [ ]:
def make_pipeline():
    """Creates a pipeline for fetching and filtering stocks based on momentum."""
    
    twenty_day_momentum = Momentum(window_length=20)
    thirty_day_momentum = Momentum(window_length=30)

    positive_momentum = (
        (twenty_day_momentum > 1) & 
        (thirty_day_momentum > 1)
    )

    return Pipeline(
        columns={
            'longs': thirty_day_momentum.top(N_LONGS),
            'shorts': thirty_day_momentum.top(N_SHORTS),
            'ranking': thirty_day_momentum.rank(ascending=False)
        },
        screen=positive_momentum
    )
In [ ]:
 

Define a function that runs before the trading day starts

In [ ]:
def before_trading_start(context, data):
    """Fetches factor data before trading starts."""
    
    context.factor_data = pipeline_output("factor_pipeline")
In [ ]:
 

Define the initialize function to set up the algorithm

In [ ]:
def initialize(context):
    """Initializes the trading algorithm and schedules rebalancing."""
    
    attach_pipeline(make_pipeline(), "factor_pipeline")
    schedule_function(
        rebalance,
        date_rules.week_start(),
        time_rules.market_open(),
        calendar=calendars.US_EQUITIES,
    )
In [ ]:
 

Define the rebalancing function to execute trades based on factor data

In [ ]:
def rebalance(context, data):
    """Rebalances the portfolio based on factor data."""
    
    factor_data = context.factor_data
    record(factor_data=factor_data.ranking)

    assets = factor_data.index
    record(prices=data.current(assets, 'price'))

    longs = assets[factor_data.longs]
    shorts = assets[factor_data.shorts]
    divest = set(context.portfolio.positions.keys()) - set(longs.union(shorts))

    exec_trades(data, assets=divest, target_percent=0)
    exec_trades(data, assets=longs, target_percent=1 / N_LONGS)
    exec_trades(data, assets=shorts, target_percent=-1 / N_SHORTS)
In [ ]:
 

Define a function to execute trades for given assets and target percentages

In [ ]:
def exec_trades(data, assets, target_percent):
    """Executes trades for given assets to achieve target portfolio weights."""
    
    for asset in assets:
        if data.can_trade(asset) and not get_open_orders(asset):
            order_target_percent(asset, target_percent)
In [ ]:
 

Define the backtesting period

In [ ]:
start = pd.Timestamp('2015')
end = pd.Timestamp('2018')

Run the backtest using the specified start and end dates

In [ ]:
perf = run_algorithm(
    start=start,
    end=end,
    initialize=initialize,
    before_trading_start=before_trading_start,
    capital_base=100_000,
    bundle="quandl",
)

Get the final portfolio value

In [ ]:
perf.portfolio_value

Construct a DataFrame with stock prices and corresponding dates

In [ ]:
prices = pd.concat(
    [df.to_frame(d) for d, df in perf.prices.dropna().items()], 
    axis=1
).T

Convert column names to stock symbols

In [ ]:
prices.columns = [col.symbol for col in prices.columns]

Normalize the index to midnight, preserving timezone information

In [ ]:
prices.index = prices.index.normalize()

Construct a DataFrame with factor ranks and corresponding dates

In [ ]:
factor_data = pd.concat(
    [df.to_frame(d) for d, df in perf.factor_data.dropna().items()],
    axis=1
).T

Convert column names to stock symbols

In [ ]:
factor_data.columns = [col.symbol for col in factor_data.columns]

Normalize the index to midnight, preserving timezone information

In [ ]:
factor_data.index = factor_data.index.normalize()

Create a MultiIndex with date and asset

In [ ]:
factor_data = factor_data.stack()

Rename the MultiIndex levels

In [ ]:
factor_data.index.names = ['date', 'asset']

Compile forward returns, factor ranks, and factor quantiles

In [ ]:
alphalens_data = get_clean_factor_and_forward_returns(
    factor=factor_data, prices=prices, periods=(5, 10, 21, 63), quantiles=5
)

Display the compiled Alphalens data

In [ ]:
alphalens_data

Generate the information coefficient for each holding period on each date

In [ ]:
ic = factor_information_coefficient(alphalens_data)

Calculate the mean information coefficient over all periods

In [ ]:
ii = mean_information_coefficient(alphalens_data)

Plot the mean information coefficient

In [ ]:
ii.plot()
In [ ]:
 

Plot the information coefficient for the 5-day holding period

In [ ]:
plot_ic_ts(ic[["5D"]])
plt.tight_layout()

Calculate mean information coefficient per holding period per year

In [ ]:
ic_by_year = ic.resample('A').mean()
ic_by_year.index = ic_by_year.index.year

Plot the mean information coefficient by year

In [ ]:
ic_by_year.plot.bar(figsize=(14, 6))
plt.tight_layout()

PyQuant News 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 get started with Python for quant finance. For educational purposes. Not investment advise. Use at your own risk.