Fund managers report their holdings every month. They don’t want to tell investors that they lost money the latest meme stock. So they will sell the meme stocks and buy higher quality assets, like bonds.
We might be able to take advantage of this month-end flow effect by buying bonds toward the end of the month and selling them at the beginning.
The month-end flow effect is one of many strategies we explore in my top selling cohort based course, Getting Started With Python for Quant Finance.
In today’s newsletter, you’ll explore a strategy that achieves a Sharpe ratio of 1.1 over 10 years. The strategy is based on the hypothesis that fund managers rotate in and out of low quality asses over time.
Let’s go!
How to exploit the month-end flow effect for a 502% return
VectorBT provides a comprehensive suite of tools for every stage of an algorithmic trading workflow.
This includes data acquisition, signal generation, portfolio optimization, strategy simulation, hyperparameter tuning, and cross-validation.
These modular components empower users to customize their analysis. We’ll use VectorBT to backtest the month-end flow effect strategy.
Let’s dive in!
Imports and set up
We’re going to use the ETF TLT as a proxy for bonds. We’ll use VectorBT to get 10 years of data in 1 line of code. But first the imports.
import pandas as pd import numpy as np import matplotlib.pyplot as plt import vectorbt as vbt
VectorBT has a built in data downloader. We’ll download the data and grab the closing prices.
tlt = vbt.YFData.download( "TLT", start="2004-01-01" ).get("Close").to_frame() close = tlt.Close
Build the trading signals
VectorBT has a few utilities that create DataFrames to store entry and ext signals.
short_entries = pd.DataFrame.vbt.signals.empty_like(close) short_exits = pd.DataFrame.vbt.signals.empty_like(close) long_entries = pd.DataFrame.vbt.signals.empty_like(close) long_exits = pd.DataFrame.vbt.signals.empty_like(close)
This code creates pandas Series of the same shape as the Series with our closing prices. Next we’ll generate the trading signals based on the day of the month.
# Get short on the first day of the new month short_entry_mask = ~tlt.index.tz_convert(None).to_period("M").duplicated() short_entries.iloc[short_entry_mask] = True # Exit the short five days later short_exit_mask = short_entries.shift(5).fillna(False) short_exits.iloc[short_exit_mask] = True # Get long 7 days prior to the end of the month long_entry_mask = short_entries.shift(-7).fillna(False) long_entries.iloc[long_entry_mask] = True # Buy back on the last day of the month long_exit_mask = short_entries.shift(-1).fillna(False) long_exits.iloc[long_exit_mask] = True
We expect there to be positive returns, on average, toward the end of the month. We expect this because we think fund managers are buying TLT toward to the end of the month. Similarly, we expect to see negative returns, on average, toward the beginning of the month. Fund managers dump their high quality assets and go back to buying meme stocks.
Here, we get short the first trading day of every month and buy back the position after five trading days. We then get long seven days before the end of the month and exit the position on the last trading day of the month.
Run the backtest
VectorBT makes it simple to run sophisticated backtests. Here we assume trades at the closing prices. Once we run the backtest, we get a nice summary of the results.
pf = vbt.Portfolio.from_signals( close=close, entries=long_entries, exits=long_exits, short_entries=short_entries, short_exits=short_exits, freq="1d" ) pf.stats()
We pass in the closing prices, long entries and exits, short entries and short exits.
Start 2004-01-02 05:00:00+00:00 End 2024-02-08 05:00:00+00:00 Period 5060 days 00:00:00 Start Value 100.0 End Value 602.85243 Total Return [%] 502.85243 Benchmark Return [%] 112.028427 Max Gross Exposure [%] 100.0 Total Fees Paid 0.0 Max Drawdown [%] 21.079224 Max Drawdown Duration 421 days 00:00:00 Total Trades 483 Total Closed Trades 483 Total Open Trades 0 Open Trade PnL 0.0 Win Rate [%] 59.213251 Best Trade [%] 6.4653 Worst Trade [%] -11.270668 Avg Winning Trade [%] 1.652618 Avg Losing Trade [%] -1.452577 Avg Winning Trade Duration 5 days 12:05:02.097902097 Avg Losing Trade Duration 5 days 11:56:18.461538461 Profit Factor 1.669652 Expectancy 1.025157 Sharpe Ratio 1.106375 Calmar Ratio 0.656386 Omega Ratio 1.235914 Sortino Ratio 1.651188
The results are promising. Over 5,060 trading days, the strategy trades 483 times. It has total returns of 502% compared to a buy and hold of TLT with a 112% return. The strategy has a reasonable max drawdown of 21%, a win rate of 59%, and Sharpe ratio of 1.1.
Next steps
VectorBT makes it easy to optimize strategies in a rigorous way using walk forward optimization. As a next step, try altering the day of the month for the entries and exists. You’ll want to avoid overfitting so you can run a walk forward optimization to make sure the out of sample results are statistically signifiant.