16 KiB
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.
import pandas as pd from scipy import stats import matplotlib.pyplot as plt
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 )
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
import warnings warnings.filterwarnings("ignore")
Define the number of long and short positions to hold in the portfolio
N_LONGS = N_SHORTS = 10
Define a custom factor class to compute momentum
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]
Define a pipeline to fetch and filter stocks based on momentum
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 )
Define a function that runs before the trading day starts
def before_trading_start(context, data): """Fetches factor data before trading starts.""" context.factor_data = pipeline_output("factor_pipeline")
Define the initialize function to set up the algorithm
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, )
Define the rebalancing function to execute trades based on factor data
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)
Define a function to execute trades for given assets and target percentages
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)
Define the backtesting period
start = pd.Timestamp('2015') end = pd.Timestamp('2018')
Run the backtest using the specified start and end dates
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
perf.portfolio_value
Construct a DataFrame with stock prices and corresponding dates
prices = pd.concat( [df.to_frame(d) for d, df in perf.prices.dropna().items()], axis=1 ).T
Convert column names to stock symbols
prices.columns = [col.symbol for col in prices.columns]
Normalize the index to midnight, preserving timezone information
prices.index = prices.index.normalize()
Construct a DataFrame with factor ranks and corresponding dates
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
factor_data.columns = [col.symbol for col in factor_data.columns]
Normalize the index to midnight, preserving timezone information
factor_data.index = factor_data.index.normalize()
Create a MultiIndex with date and asset
factor_data = factor_data.stack()
Rename the MultiIndex levels
factor_data.index.names = ['date', 'asset']
Compile forward returns, factor ranks, and factor quantiles
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
alphalens_data
Generate the information coefficient for each holding period on each date
ic = factor_information_coefficient(alphalens_data)
Calculate the mean information coefficient over all periods
ii = mean_information_coefficient(alphalens_data)
Plot the mean information coefficient
ii.plot()
Plot the information coefficient for the 5-day holding period
plot_ic_ts(ic[["5D"]]) plt.tight_layout()
Calculate mean information coefficient per holding period per year
ic_by_year = ic.resample('A').mean() ic_by_year.index = ic_by_year.index.year
Plot the mean information coefficient by year
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.
