Back to Community
One Of The Most Striking Equity Market Anomalies Explained

Hi,

Just to share this paper with you guys. Looks interesting :)

It is surprising how little attention academic literature has devoted
to understand equity market returns around the turn of the month,
despite the observations of Lakonishok and Smidt (1988) and McConnell
and Xu (2008) among others that most of the returns accrue during a
four-day period, from the last trading day to the third trading day of
the month.

We find that the market returns are abnormally high also on the three
days before the turn of the month.

In fact, combining the two observations, we find that since 1926, one
could have held the S&P 500 index for only seven business days a month
and pocketed almost the entire market return with forty percent lower
volatility compared to a buy and hold strategy.

Since 1987, all of the positive equity returns have accrued during
these seven trading days, and the average returns during the rest of
the month have been negative. Odgen (1990) relates the high returns at
the beginning of the month to the monthly payment cycle – the fact
that large part of investors’ cash receipts are obtained on the last
or the first business day of the month. Our findings lend additional
support to this hypothesis.

In "Dash for Cash: Month-End Liquidity Needs and the Predictability of
Stock Returns" -working paper we explore the turn of the month
phenomenon further and discover new, previously unidentified patterns
in equity returns.

SSRN-id2528692.pdf

https://www.scribd.com/doc/247874125/SSRN-id2528692-pdf
http://www.zerohedge.com/news/2014-11-22/one-most-striking-equity-market-anomalies-explained

12 responses

It seems like Quantopian is well suited to backtest this idea.

Anyone willing to QA my script?

I want to ensure that I coded it up correctly.

Dan

Clone Algorithm
4
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# eom.py

# run_algo.py -f eom.py --data-frequency daily --start 2014-01-01 --end 2014-11-20 --capital_base 1811000 --output eom_output.pickle --symbols ^GSPC

# Report on perf dataframe from:
# ipython notebook

# ref:

# https://www.quantopian.com/posts/one-of-the-most-striking-equity-market-anomalies-explained

# Buy on 27th sell on 4th

import datetime

def initialize(context):
    context.tkr = symbol('SPY')

def handle_data(context, data):
    tkr = context.tkr
    # I should get the day of the month.
    # google: strftime day of the month
    adate = data[tkr].datetime
    dom   = int(datetime.datetime.strftime(adate,'%d'))
    if (dom > 0) & (dom < 5):
        order_target_percent(tkr, 1.0)
    if (dom > 4) & (dom < 27):
        order_target_percent(tkr, 0.0)
    if dom > 26:
        order_target_percent(tkr, 1.0)

There was a runtime error.

@Dan
Something like this?

Up to 2005 it is surprising that it moves with the market so much considering it spends most of its time without an open position, however after then it drifts off.

Does the paper say anything about Long/Short direction?

N.B. I didn't read the paper.

Clone Algorithm
1
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# eom.py

# run_algo.py -f eom.py --data-frequency daily --start 2014-01-01 --end 2014-11-20 --capital_base 1811000 --output eom_output.pickle --symbols ^GSPC

# Report on perf dataframe from:
# ipython notebook

# ref:

# https://www.quantopian.com/posts/one-of-the-most-striking-equity-market-anomalies-explained

# Buy on 27th sell on 4th

import datetime

def initialize(context):
    context.tkr = symbol('SPY')
    schedule_function(sellTkr,date_rules.month_start(days_offset=2))#approx
    schedule_function(buyTkr,date_rules.month_end(days_offset=1))#approx

def handle_data(context, data):
    pass
        
def buyTkr(context, data):
    log.info("buying")
    order_target_percent(context.tkr, 1.0)
    
def sellTkr(context, data):
    log.info("selling")
    order_target_percent(context.tkr, 0.0)
There was a runtime error.

I've been messing around with this in Excel, looks interesting.

@jj
schedule_function() !
I like.

I tossed this together using the new schedule function feature in minute mode. It buys at the open on the third to last day of the month, and sells at the close on the third day of the month. Adding in the last few days of the month makes a big difference compared to buying on the morning of the last day.

Clone Algorithm
105
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month


def initialize(context):
    set_commission(commission.PerShare(cost=0.0, min_trade_cost=0.0))
    set_slippage(slippage.FixedSlippage(spread=0.00))
    
    context.tkr = symbol('SPY')
    
    schedule_function(
        func=open_position,
        date_rule=date_rules.month_end(days_offset=2),
        time_rule=time_rules.market_open()
    )
    schedule_function(
        func=close_position,
        date_rule=date_rules.month_start(days_offset=2),
        time_rule=time_rules.market_close()
    )
    

def open_position(context, data):
    order_target_percent(context.tkr, 1.0)
    
def close_position(context, data):
    order_target(context.tkr, 0)
    
    
def handle_data(context, data):
    pass
   
There was a runtime error.

From the source code (copied below) it looks like the offset is +2 days, i.e. two days past month end. How do you make it 2 days before month end? schedule_function won't take negative days

schedule_function(
func=open_position,
date_rule=date_rules.month_end(days_offset=2),
time_rule=time_rules.market_open()
)

This code snippet run the function on the 3rd -to-last trading day of the month:

date_rule=date_rules.month_end(days_offset=2),  

If you use an offset of 0, the function will run on the last trading day of the month. So if you want something to run 2 days before the month end you can use,

date_rule=date_rules.month_end(days_offset=1)  

The API documentation for schedule_function is available here: https://www.quantopian.com/help#api-schedulefunction

cheers,
Alisa

Disclaimer

The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances.

Thanks for the quick response. I guess the offset for the month_start function is forward in time? It's not clear in the documentation.

ah - we'll get that cleared up!

Thanks!

As a Python programmer I'm used to the relativedelta function that takes +/- time increments

As you can see in the link bellow this logic gives very stable profits.

  • Buying @ Open Monday of LAST week of the month
  • Sell @ Close of 2nd trading day after the open

https://www.evernote.com/shard/s35/sh/cb4c8870-bcd3-41d4-8ebd-343a02451ecc/de700bb32c05f050

I've implemented the algo in a non-elegant manner but still it can happen sell order is not given on the 2nd trading day (when it occurs the next month). Also when I check the time the order is executed with the .created property applied to the order object it returns an error.

Clone Algorithm
20
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
# Buy last monday of the month and sell of 2nd trading day after the open

import datetime

def initialize(context):
    
    context.order = None
    context.day = ''
    context.tkr = symbol('SPY')
    schedule_function(
                      buyTkr,
                      date_rules.week_start(days_offset=0)
                      )
    
def handle_data(context, data):
    sellTkr(context, data)
    pass
        
def buyTkr(context, data):
    if 23 <= data[context.tkr].datetime.day <= 31 :
        print("buying", data[context.tkr].datetime.day, data[context.tkr].datetime.month)
        context.order = True
        context.day = data[context.tkr].datetime.day
        order = order_target_percent(context.tkr, 1.0)
        order_info = get_order(order)
        print('created', order_info) 
    
def sellTkr(context, data):
    if context.order == True and (data[context.tkr].datetime.day) == context.day + 3:
        order_target_percent(context.tkr, 0.0)
        context.order = False
        print("selling", data[context.tkr].datetime.day, data[context.tkr].datetime.month)
There was a runtime error.

Some idea on how to find last Monday of a month without importing the calendar module ? using datetime and time module