March cohort is now open: How to secure your spot:

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

How to exploit the month-end flow effect for a 502% return. Fund managers report their holdings every month.

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.