Sector Rotation Momentum + Mean Reversion

Hi! I am new to Q (and in fact quite new to algorithmic trading in general) and any help/input will be greatly appreciated. My algorithm is based on the common strategy where one goes long the best performing sector for the past month and holds it for a month. There is also evidence that stocks within a sector exhibit cross-sectional mean reversion behavior (Ernie Chan Chapter 4). It might be a good idea to long the worst performing stocks in the best performing sectors. I also shorten the time horizon to one week instead of one month. My basic strategy goes as follows:

1. Find the best performing sectors for the past week based on sector ETF.
2. Find the below-average performing stocks within that sector.
3. Calculate the weights based on
wi = (〈rj〉 - ri)/ Σk|〈rj〉 - rk|
where ri is the daily return of the ith stock, and is the average return of the corresponding sector ETF. This formula is inspired by Ernie Chan's book Example 4.3.
4. Apply the weights and hold it for a week.

Note that my basic algorithm is linear and has zero parameter. Therefore no data-scooping bias. However, due to the large draw-downs it may need a few improvements. For example:
1. Can add fundamental filters such as P/E ratio.
2. Can add stop loss and/or stop gain.
3. Can try different time horizon.
4. Maybe avoid trading around earning announcement?
5. Can hedge it with SPY or sector ETF.
6. Can do the mirror image: short the best performing stocks in the worst performing sector and turn it into a long-short strategy.

Also, my code is not very clean. Any suggestion is greatly appreciated.

739
Backtest from to with initial capital
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
# Backtest ID: 58c02c6b765b9d620f0744a2
There was a runtime error.
33 responses

Can you explain your weight formula? i.e what is rj, and rk, and k, and the <> brackets? I haven't read Ernie Chan's book.

Hi Stephen,

The basic idea is that if stocks exhibit cross-sectional mean reversion behavior, then the further they deviate from the sector mean return, the higher probability that the stocks will "catch up" to the mean. In my case, we want to apply more weights to those stocks that are furthest from the mean, i.e., the worst performing stocks. For example, if the sector return was 1% last week, and stock A's return was -0.1% and stock B's return was 0.2%, we put more weight in stock A than in stock B. Essentially the way we assign weights to a stock is proportional to how much the stock return deviates from the mean.

〈rj〉 denotes the mean return. ri denotes the return for individual stock i. Σk|〈rj〉 - rk|, which is the normalizer, is the sum of difference between the mean return and individual returns for all stocks whose returns are below mean.

Thanks Neo, makes sense.

Ill play around with this and see if I can improve those drawdowns. Adding stop losses and gains never seem to be a bad idea in my experience

Thanks for sharing this Neo. Has potential.

The mean referenced seems to be the long term average that the firm had been experiencing. Essay Writing Service UK

Neo. Take the red pill! Why not try to make this idea sector neutral and more likely to be eligible for the fund.

With Pipeline, you can rank stocks within each sector, and go short the top N and long the bottom N, same dollar weighting. You can use something like "Returns.groupby(Sector()).rank()" for this. If you did this across all sectors, and weighted each sector equally, you'll have a sector neutral mean reversion strategy, where each sector has equal capital allocated to it. This probably won't make any money, but...

How about if you weight each sector differently (but still dollar neutral within sectors)? Perhaps the most bearish sectors get the biggest divergence between the top and bottom N, as people panic and sell the worst stocks in that sector well below their fundamentals. Try overweighting the most bearish sectors.

I like to say openly that I don't understand clear idea about the algorithm explained above. It may be because of my fault. It is not a familiar area for me. I am working in the writing field. Now working for an essay writing service reviews company and writing is interesting field for me.

Neo,

What does context.bearishSectorReturn = -99 mean?

Watch your borrowing. Your returns are higher because of the amount of leverage. I'm going to play with it. I think it would be a better strategy if you looked at a longer time periods returns in the industry rather then 7 days. I think it will show more of a mean reversion, then just 7 days.

118
Backtest from to with initial capital
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
There was a runtime error.

Eric,

Do you know what context.bearishSectorReturn = -99 means in this code?

@Sam
I am not good at coding. Just trying to initialize it to a negative number. I guess you do not have to do that.

@ Neo ah I see gotchya. Thanks for reply!

@ Neo and @ Eric,

I am with Eric on the leverage. I will attempt to add a leverage constraint as well Eric, along with move time frame out from 7 days to maybe 20 days for a trading month instead of week and see how that plays into alpha, beta, and drawdown.....

@ Nero,

One last question. Why do you set context.prev_day_wd = context.today_wd ,

then define

if context.today_wd > context.prev_day_wd:
return

else

# week start

?

*@Neo

@Eric

Where do you find the case where leverage>1? Since the weights are distributed according to the said formula, the sum of the weights cannot exceed 1 by definition.
As for the holding period, yes you can experiment the algo with holding period longer than one week. It might also have good theoretical reasons to do so. I just have not got the chance to try it yet.

@ neo,

Why do you set context.prev_day_wd = context.today_wd ,
then define
if context.today_wd > context.prev_day_wd:
return
else

# week start

?

@Sam

I do that to figure out if today is the first business day of the week. I do nothing and return if it is NOT the week start.

@Eric @ Neo

Eric,

I see Neo's point on definition and leverage cannot exceed 1 with this trading rule. Am I missing something ? If so, we can apply a leverage constraint.

@Neo
I added leverage and the amount of long positions your algorithm had. Controlling Leverage is probably one of the most important things. Usually if your returns are really high you are borrowing a lot.

This is the code I added, in case you can't see my backtest.

def count_positions(context,data):

longs = 0
for position in context.portfolio.positions.itervalues():
if position.amount > 0:
longs += 1
leverage=context.account.leverage
# asset=context.portfolio.portfolio_value
# record(asset=asset)
record(longs=longs)
record(leverage=leverage)

118
Backtest from to with initial capital
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
There was a runtime error.

@ Eric,

What does this leverage set the algo at with this code you added? Is that maximum leverage 1, or 100% based on this? and no more?

@ Eric,

Reason I ask is because I do not see much change from his original algo, even when applying your code for leverage.

IF you run this algo back to jan 2003, even with the leverage code you have added, it tremendously returns compared to benchmark (roughly 700 % vs. benchmark at 200%)

That is even with the leverage code you have added.

I didn't fix the leverage. I recorded the amount of leverage in the algorithm so I could see the spikes. I see sometimes it hits 1.4 or 1.5. There's some flawed logic in the weighting structure. Usually it is the first thing I check when analyzing an algorithm. I am not sure how to fix it.

12
Backtest from to with initial capital
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
# Backtest ID: 58dbe4b895ba1a1b743cc4ff
There was a runtime error.

@Eric

We can add some code to keep track of the sum of the weights to determine what went wrong and how leverage>1 happened.

hacked track_orders to include leverage value

2003-05-30 13:00 WARN Your order for -5518 shares of AYE failed to fill by the end of day and was canceled.
2003-06-02 06:31 _orders:278 INFO    1      Sold -341 KMI at 51.05   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1      Sold -2204 TXU at 20.20   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1         AYE -5518 unfilled canceled
2003-06-02 06:31 _orders:278 INFO    1   Buy 101 ED at 42.98   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 590 KSE at 35.39   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 141 PPL at 40.44   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 312 SRE at 27.21   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 339 EXC at 57.22   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 97 CIN at 37.94   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 8 ETR at 51.66   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 159 D at 63.25   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 261 DTE at 43.38   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 39 AEE at 45.50   cash 61875  lv 0.44
2003-06-02 06:31 _orders:278 INFO    1   Buy 1174 TXU at 20.20   cash 61875  lv 0.44
2003-06-02 06:32 _orders:278 INFO    2         ED 101 unfilled
2003-06-02 06:32 _orders:278 INFO    2         TXU 1174 unfilled
2003-06-02 06:32 _orders:278 INFO    2         SRE 312 unfilled
2003-06-02 06:32 _orders:278 INFO    2         EXC 339 unfilled
2003-06-02 06:32 _orders:278 INFO    2         PPL 141 unfilled
2003-06-02 06:32 _orders:278 INFO    2         ETR 8 unfilled
2003-06-02 06:32 _orders:278 INFO    2         DTE 261 unfilled
2003-06-02 06:33 _orders:278 INFO    3      Bot 97 CIN at 38.10   cash -37812  lv 1.34
2003-06-02 06:33 _orders:278 INFO    3         ED 101 unfilled
2003-06-02 06:33 _orders:278 INFO    3      Bot 590 KSE at 35.31   cash -37812  lv 1.34
2003-06-02 06:33 _orders:278 INFO    3      Bot 159 D at 63.23   cash -37812  lv 1.34
2003-06-02 06:33 _orders:278 INFO    3         PPL 141 unfilled
2003-06-02 06:33 _orders:278 INFO    3         ETR 8 unfilled
2003-06-02 06:33 _orders:278 INFO    3      Bot 39 AEE at 45.50   cash -37812  lv 1.34
2003-06-02 06:34 _orders:278 INFO    4         ED 101 unfilled
2003-06-02 06:34 _orders:278 INFO    4      Bot 1174 TXU at 20.39   cash -43942  lv 1.40

65
Backtest from to with initial capital
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
# Backtest ID: 58dc63db85d87a1795d1c60e
There was a runtime error.

@ Blue

Thank you very much for this very helpful. So if we wanted to go about and restrict this algo to a maximum leverage of 2 at ALL times and never more,
what line of code would you include to allow for this?

def initialize(context):
context.target_leverage = 2

def place_order(context, data):
...
order_value(context.stock_to_long[i], context.stock_to_long_weights[i] * context.portfolio.cash * context.target_leverage)
#order_target_percent(context.stock_to_long[i], context.stock_to_long_weights[i])
...
order_value(context.stock_to_short[i], -context.stock_to_short_weights[i] * context.portfolio.cash * .5 * context.target_leverage)
#order_target_percent(context.stock_to_short[i], -context.stock_to_short_weights[i])
...


On shorting, the .5 is because I'm of the belief that it is best to keep 2x cash per short value for a leverage of 1. What if the target leverage is 2? You find out.

You can try something more dynamic with leverage as a divisor. As leverage drifts above the target, proportionally reduce allocations and below target, increase. As is, this leverage is often low. You can find out why if you want to work with it.

def initialize(context):
context.target_leverage = 2

def place_order(context, data):
c = context     # easier for me to read/edit
#log.info("in place order.")
try:
if len(c.stock_to_long)>0:
for i in range(len(c.stock_to_long)):
val = c.target_leverage * c.stock_to_long_weights[i] * c.portfolio.cash
if c.account.leverage > .7:     # in part avoiding divide by zero
val /= c.account.leverage
order_value(c.stock_to_long[i], val)
#order_target_percent(c.stock_to_long[i], c.stock_to_long_weights[i])
c.stock_long = c.stock_to_long
c.stock_to_long = []
if c.enable_short_stock:
if len(c.stock_to_short)>0:
for i in range(len(c.stock_to_short)):
val = c.target_leverage * (-c.stock_to_short_weights[i] * c.portfolio.cash * .5) / (c.account.leverage)
if c.account.leverage > .7:
val /= c.account.leverage
order_value(c.stock_to_short[i], val)
#order_target_percent(c.stock_to_short[i], -c.stock_to_short_weights[i])
c.stock_short = c.stock_to_short
c.stock_to_short = []
track_orders(context, data)


For more control and more of a solid footing, so unfilled orders won't toss you around in a land of unpredictability, schedule shorting separately and give them some time to fill and run a functon to cancel all open orders before longs.

As a best practice, I think we should all drop the common practice of selling and buying in the same minute, queue allocations that would result in a buy for later. 1) Not easy 2) The resulting certainty, clarity and control are worth the effort.

@ Blue this is great. I will play around with the first option (something more basic) first and see what spits out.

Will keep you updated on results. Thanks very much for assisting us here.

Sam

@ Blue. I am trying to use your second block of code and getting a strange error that says I have an unexpected unindent on line 214, which is
" def liquidate (context,data)" starting point. Any way you can take a look at my source and see where I could be going wrong? Thanks in advance.

4
Backtest from to with initial capital
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
# Backtest ID: 58db20a95854191d551d572f
There was a runtime error.

@ Blue sorry that is the wrong code. Correct code that Is not working shown below:

import numpy as np
import scipy
import pandas as pd
from pytz import timezone

from quantopian.algorithm import attach_pipeline, pipeline_output
from quantopian.pipeline import Pipeline, CustomFactor
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.factors import AverageDollarVolume, Returns
from quantopian.pipeline.filters.morningstar import Q500US, Q1500US
from quantopian.pipeline.classifiers.morningstar import Sector
import datetime

def initialize(context):

context.target_leverage = 2
context.mx_lvrg  = 0
context.cash_low = 0

context.xly = sid(19662) #Consumer Discretionary 102
context.xlp = sid(19659) #Consumer Staples 205
context.xle = sid(19655) #Energy 309
context.xlf = sid(19656) #Financials 103
#context.xlv = sid(19661) #Health Care 306
context.xli = sid(19657) #Industrials 310
context.xlb = sid(19654) #Materials 101
#context.xlre = sid(49472) #Real estate 104
context.xlk = sid(19658) #Technology  311
context.xlu = sid(19660) #Utilities   207

context.sectors = [context.xly,
context.xlp,
context.xle,
context.xlf,
# context.xlv,
context.xli,
context.xlb,
#context.xlre,
context.xlk,
context.xlu
]

context.sector_id = [102,
205,
309,
103,
#306,
310,
101,
#104,
311,
207
]

context.stock_long = []
context.stock_short = []

schedule_function(func = liquidate, date_rule = date_rules.week_end(0), time_rule = time_rules.market_close(minutes = 1))

schedule_function(func = place_order, date_rule = date_rules.week_start(0), time_rule = time_rules.market_open())

schedule_function(count_positions, date_rules.every_day(), time_rules.market_close())

attach_pipeline(make_pipeline(context), 'mean_reversion')

#set_long_only()
context.enable_short_stock = True

context.today_wd = 0
context.today_month = 0


def make_pipeline(context):

weekly_return = Returns(window_length=6, mask=mask)
pipe = Pipeline(
columns={
'weekly_return': weekly_return,
'sector': Sector(),
},
# combined_alpha will be NaN for all stocks not in our universe,
# but we also want to make sure that we have a sector code for everything
screen=weekly_return.notnull() & Sector().notnull(),
)
return pipe


print get_datetime('US/Eastern')
context.prev_day_wd = context.today_wd
context.today_wd = get_datetime('US/Eastern').weekday()

#context.prev_day_month = context.today_month
#context.today_month = get_datetime('US/Eastern').month
if context.today_wd > context.prev_day_wd: # used to see if today is the first business day of the week; do nothing and return if it is NOT the week start
#if context.today_month == context.prev_day_month:
return
else: # if it is the first trading day of the week
# week start
trackSectorPerformance(context, data)
if context.bullishSectorReturn > 0 and context.bearishSectorReturn < 0:
getBelowAverageStocksInBestPerformingSector(context, data)


def trackSectorPerformance(context, data):
log.info("in trackSectorPerformance.")
# Rank sectors based on weekly return
prices = data.history(context.sectors, 'price', 7, '1d')[:-1]
daily_ret = prices.pct_change(5)[1:].as_matrix(context.sectors)
weekly_ret = daily_ret[4]

bull = sorted(range(len(weekly_ret)), key=lambda i: weekly_ret[i])[-1] # lambda used to sort weekly returns
context.bullishSector = context.sector_id[bull] # identify bullish sectors by ID
context.bullishSectorReturn = weekly_ret[bull]  # identify weekly return for each bullish sector
print context.bullishSectorReturn

log.info(context.bullishSector)

context.bearishSectorReturn = -99
if context.enable_short_stock:
bear = sorted(range(len(weekly_ret)), key=lambda i: weekly_ret[i])[0] # lambda used to sort weekly returns
context.bearishSector = context.sector_id[bear]  # identify bearish sectors by ID
context.bearishSectorReturn = weekly_ret[bear]  # identify weekly return for each bearish sector
print context.bullishSectorReturn

log.info("Done trackSectorPerformance.")


def getBelowAverageStocksInBestPerformingSector(context, data):
log.info("in getBelowAverageStocksInBestPerformingSector.")
pipeline_data = pipeline_output('mean_reversion')
bullish_data = pipeline_data[pipeline_data['sector'] == context.bullishSector]

#Filter out above average return stocks
below_average_bullish_data = bullish_data[bullish_data['weekly_return'] < context.bullishSectorReturn]
below_average_bullish_data = below_average_bullish_data.sort_values(by = 'weekly_return')
context.stock_to_long = below_average_bullish_data.index[:]
stock_to_long_returns  = below_average_bullish_data['weekly_return']

#Calculate weights based on returns
context.stock_to_long_weights = context.bullishSectorReturn - stock_to_long_returns
context.stock_to_long_weights = context.stock_to_long_weights.div(context.stock_to_long_weights.sum())

print context.stock_to_long_weights.sum()

if context.enable_short_stock:
bearish_data = pipeline_data[pipeline_data['sector'] == context.bearishSector]

#context.stock_to_short = bearish_data.sort_values(by = 'weekly_return').index[-3:]
#Filter out above average return stocks
above_average_bearish_data = bearish_data[bearish_data['weekly_return'] > context.bearishSectorReturn]
above_average_bearish_data = above_average_bearish_data.sort_values(by = 'weekly_return')
context.stock_to_short = above_average_bearish_data.index[:]
stock_to_short_returns  = above_average_bearish_data['weekly_return']

#Calculate weights based on returns
context.stock_to_short_weights = stock_to_short_returns - context.bearishSectorReturn
context.stock_to_short_weights = context.stock_to_short_weights.div(context.stock_to_short_weights.sum())

log.info("done getBelowAverageStocksInBestPerformingSector.")


def place_order(context, data):

log.info("in place order.")

try:
if len(context.stock_to_long)>0:
for i in range(len(c.stock_to_long)):
val = context.target_leverage * context.stock_to_long_weights[i] * context.portfolio.cash
if context.account.leverage > .7:     # in part avoiding divide by zero
val /= context.account.leverage
order_value(context.stock_to_long[i], val)
order_target_percent(context.stock_to_long[i], context.stock_to_long_weights[i])
context.stock_long = context.stock_to_long
context.stock_to_long = []

if context.enable_short_stock:
if len(context.stock_to_short)>0:
for i in range(len(context.stock_to_short)):
val = context.target_leverage * (-context.stock_to_short_weights[i] * context.portfolio.cash * .5) / (context.account.leverage)
if context.account.leverage > .7:
val /= context.account.leverage
order_value(context.stock_to_short[i], val)
order_target_percent(context.stock_to_short[i], -context.stock_to_short_weights[i])
context.stock_short = context.stock_to_short
context.stock_to_short = []
track_orders(context, data)


def liquidate(context, data):

log.info("in liquidate.")

try:
if len(context.stock_long)>0:
log.info("Liquidating longs.")
for i in range(len(context.stock_long)):
order_target_percent(context.stock_long[i], 0)

if context.enable_short_stock:
if len(context.stock_short)>0:
for i in range(len(context.stock_short)):
order_target_percent(context.stock_short[i], 0)

except KeyError as k:
# so what exactly is update_universe doing that I'm ending up here?
print(k)

# KEEP TRACK OF LEVERAGE BY SCHEDULING FUNCTION ASSIGNED ABOVE AS count_positions


def count_positions(context,data):

longs = 0
for position in context.portfolio.positions.itervalues():
if position.amount > 0:
longs += 1
leverage=context.account.leverage
# asset=context.portfolio.portfolio_value
# record(asset=asset)
record(longs=longs)
record(leverage=leverage)


@Sam Khorsand were you able to resolve the error?

I discovered that to a very reliable degree that if at the end of each December you short the best performing sector and long the second best performing factor, this sort of superstitious hocus pocus actually reliably performs year after year after year. So basically the best sector mean reverts, and the second best has momentum. See my Hedged Bridesmaid example that demonstrates this this phenomenon with ETFs. Of course there's no guarantee the market will continue to behave this way, but so far it does. This year for example, it expects that financials will outperform energy. You could use the calculation from my algo in order to determine which sector to weight more heavily to short or long -- it won't make your algo Q Fund-worthy because it's by its nature not sector neutral, but it will likely lead to increased returns -- that's 0.04 extra alpha sitting there.

Hi Viridian,
"... to a very reliable degree .... at the end of each December you short the best performing sector and long the second best"

Yes, maybe it works. Think of funds taking profit in best sector, etc. How many samples (1 year = 1 sample) backtest?