How to exploit the month-end flow effect for a 502% return

February 10, 2024
Facebook logo.
Twitter logo.
LinkedIn logo.

How to exploit the month-end flow effect for a 502% return

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.

1import pandas as pd
2import numpy as np
3import matplotlib.pyplot as plt
4import vectorbt as vbt

VectorBT has a built in data downloader. We’ll download the data and grab the closing prices.

1tlt = vbt.YFData.download(
2    "TLT", 
3    start="2004-01-01"
4).get("Close").to_frame()
5close = tlt.Close

Build the trading signals

VectorBT has a few utilities that create DataFrames to store entry and ext signals.

1short_entries = pd.DataFrame.vbt.signals.empty_like(close)
2short_exits = pd.DataFrame.vbt.signals.empty_like(close)
3long_entries = pd.DataFrame.vbt.signals.empty_like(close)
4long_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.

1# Get short on the first day of the new month
2short_entry_mask = ~tlt.index.tz_convert(None).to_period("M").duplicated()
3short_entries.iloc[short_entry_mask] = True
4
5# Exit the short five days later
6short_exit_mask = short_entries.shift(5).fillna(False)
7short_exits.iloc[short_exit_mask] = True
8
9# Get long 7 days prior to the end of the month
10long_entry_mask = short_entries.shift(-7).fillna(False)
11long_entries.iloc[long_entry_mask] = True
12
13# Buy back on the last day of the month
14long_exit_mask = short_entries.shift(-1).fillna(False)
15long_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.

1pf =  vbt.Portfolio.from_signals(
2    close=close,
3    entries=long_entries,
4    exits=long_exits,
5    short_entries=short_entries,
6    short_exits=short_exits,
7    freq="1d"
8)
9
10pf.stats()

We pass in the closing prices, long entries and exits, short entries and short exits.

1Start                         2004-01-02 05:00:00+00:00
2End                           2024-02-08 05:00:00+00:00
3Period                               5060 days 00:00:00
4Start Value                                       100.0
5End Value                                     602.85243
6Total Return [%]                              502.85243
7Benchmark Return [%]                         112.028427
8Max Gross Exposure [%]                            100.0
9Total Fees Paid                                     0.0
10Max Drawdown [%]                              21.079224
11Max Drawdown Duration                 421 days 00:00:00
12Total Trades                                        483
13Total Closed Trades                                 483
14Total Open Trades                                     0
15Open Trade PnL                                      0.0
16Win Rate [%]                                  59.213251
17Best Trade [%]                                   6.4653
18Worst Trade [%]                              -11.270668
19Avg Winning Trade [%]                          1.652618
20Avg Losing Trade [%]                          -1.452577
21Avg Winning Trade Duration    5 days 12:05:02.097902097
22Avg Losing Trade Duration     5 days 11:56:18.461538461
23Profit Factor                                  1.669652
24Expectancy                                     1.025157
25Sharpe Ratio                                   1.106375
26Calmar Ratio                                   0.656386
27Omega Ratio                                    1.235914
28Sortino 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.

Man with glasses and a wristwatch, wearing a white shirt, looking thoughtfully at a laptop with a data screen in the background.