A Carry Strategy with Portfolio Optimization

Attached below is a traditional carry strategy as follows:

1. At the end of each calendar quarter (Dec, Mar, June, Sept), sort all dividend yields on stocks with more than 2 billion market cap for liquidity constraint (T12M) (Wish I could of got forward Div Yield but the data's unavailable for some reason).
2. Trim off the first 3 and last 3 stocks to avoid any data bias (e.g RBS was a stock with apparently reported 103% dividend yield for a while)
3. Estimate covariance matrix using Ledoit-Wolf Shrinkage (https://repositori.upf.edu/bitstream/handle/10230/560/691.pdf?sequence=1)
4. Optimize the portfolio such that it is long only (Max 3% in any stock to ensure diversification) and maximizes the follow equation:
W' * Dividend Yield - (Lambda)*W' * Covariance Matrix * W
where Lambda is an adjustable risk aversion coefficient (More risk averse, more priority on minimizing risk), W is a vector of stock weights.
5. Re-balance each quarter

Return_spread is a plot of cum. return of the portfolio - SPY cum return.

Some Notes on this strategy:

1. The strategy generates steady alpha other than the period during the recession and for the past 2-3 years (The cum. spread flattens).
2. I have yet to contrast this with a more generic weighting method (e.g dogs of dow strategy where we just equal weight the stocks) or sorts or risk parity.
3. Long-short tends to perform poorly due to excessive leverage and creating a leverage constraint freezes the optimizer (anyone want to give this a shot?) (My constraint was SUM(ABS(W))/SUM(W) < c where c is some constant e.g 3)
4. Overall, long-short sucks due to periods of excessive leverage, long works but has beta. Alpha sometimes were generated due to luck and there are periods where premia is zero or negative.
227
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: 556df68e91b47a10bc437c3a
There was a runtime error.
9 responses

Thanks for posting, soooo much to learn...

Good stuff, lots of time and thought that went into that work.
You're already aware that you have leverage in the sense of margin (negative cash, borrowing), max on 2008-10-10.
Keep in mind that the chart doesn't reflect that, the code is still positive, just less, ~165%.
Best of luck.

What is the source of the spike in June 2010? Looks a bit like an aberration...

@Simon, tbh, I went to the exact dates and i couldn't pin down one asset with an extreme rise in price, etc. If anyone would like to clone and let me know, that'd be appreciated.

@garyha, can you show me how it was levered in 2008? I just went over the logs and at each quarter, i logged the account leveraged and never seen anything > 2

Looking at the same thing it appears that on one day between 10/6/10 and 11/6/10 the value of the stocks changes ~ +$25k but the cash balance goes from negative$435k to positive \$169k. There are no transactions logged in the period - very strange.

@garyha.. pls. print out.. the anomaly.. so that evryone will be aware if the algo is not performing properly as intended. thanks

In this backtest I added start and stop dates to track_orders() because I think the logging window would otherwise reach its hard limit, and chose to hone in on the date ranges 2007-05-07 to 2008-02-13 and 2010-04-26 to 2010-11-15, they looked sort of interesting in the custom chart. The code for adjusting them looks like this:

    # track_orders() dates to start or stop logging.
#   This is to target a date range and not overwhelm the logging window.
#   These lists can be empty like []
context.dates  = {
'active': 0,
'start' : ['2007-05-07', '2010-04-26'],
'stop'  : ['2008-02-13', '2010-11-15']
}


I missed earlier calls for my attention unfortunately.
In the output there are tons of unfilled orders for awhile, maybe part of the problem if that's an unusual number of them.
The first number in that line is supposed to be the trading minute (1 - 390) and was working in another algo earlier today, 630 is out of range so I don't know what's up with that. If someone wants to fix it, be my guest please. Edit: That's only in daily mode and the problem is taken care of with the latest update at Track Orders.

2006-09-29trade:135INFO1.00215015566
2006-09-29getCarried:196INFONumStcks, Exp. DivYd, Vy  55 6.31 0.30
2006-12-29getCarried:196INFONumStcks, Exp. DivYd, Vy  53 6.68 0.27
2007-03-30getCarried:196INFONumStcks, Exp. DivYd, Vy  49 5.73 0.28
2007-05-07track_orders:73INFO 630   Sell -242 CRE at 44.74   cash -72739
2007-05-07track_orders:73INFO 630   Sell -242 CRE at 44.74   cash -72739
2007-05-07track_orders:73INFO 630   Sell -242 CRE at 44.74   cash -72739
2007-05-07track_orders:73INFO 630   Sell -10 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -225 GTM at 42.0   cash -72739
2007-05-07track_orders:73INFO 630   Sell -348 TLD at 35.37   cash -72739
2007-05-07track_orders:73INFO 630   Sell -348 TLD at 35.37   cash -72739
2007-05-07track_orders:73INFO 630   Sell -348 TLD at 35.37   cash -72739
2007-05-07track_orders:73INFO 630   Sell -348 TLD at 35.37   cash -72739
2007-05-07track_orders:73INFO 630   Sell -1098 GEMP at 3.38   cash -72739
2007-05-07track_orders:73INFO 630   Sell -2022 PA at 24.99   cash -72739
2007-05-07track_orders:73INFO 630   Sell -2022 PA at 24.99   cash -72739
2007-05-07track_orders:73INFO 630   Sell -2022 PA at 24.99   cash -72739
2007-05-08track_orders:49INFO 630         GTM -225 unfilled
2007-05-08track_orders:49INFO 630         CRE -242 unfilled
2007-05-08track_orders:49INFO 630         GTM -225 unfilled
2007-05-08track_orders:49INFO 630         TLD -348 unfilled
2007-05-08null:nullWARNLogging limit exceeded; some messages discarded
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         CRE -242 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         TLD -348 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         GEMP -1098 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         GTM -225 unfilled
2007-05-16track_orders:49INFO 630         TLD -348 unfilled
2007-05-16track_orders:49INFO 630         CRE -242 unfilled
2007-05-16track_orders:49INFO 630         PA -2022 unfilled


My previous message in this thread had said ~165%, that's PvR in the chart. Here, it is 132%. There had been an upgrade and/or I introduced a bug.
I had mentioned a high in 2008, it's a bit different now.
Although the code is undoubtedly onto something that is useful and affects us all, it has been evolving. I'm actually doubting the specifics in the calculations and would love to have extra eyes on it from some of you and the fine folks at Quantopian. Maybe instead of RiskHi, the PvR value should be purely based on max_leverage. Then the shorts business could be dropped. The context.account.leverage value I think is definitely trustworthy (the only problem with it is when we try to use it in record() for a minute-level backtest and are missing 389 out of the 390 minutes of the trading day), so basing everything on that would simplify.

In the custom chart you can click legend items to toggle them off/on and bring the ones you're interested in, into range. In doing that, turning the large value items off and focusing on MaxLv, I see there is a jump early in 2003 so it might be handy to reset track_orders() to a date range there, or just remove the dates, make those empty lists. The logging window maybe would hit its limit however it doesn't matter if you are working just a few months in to the run because you might even have the end date set real early.

13
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: 567bbe27dc1683117c02743f
There was a runtime error.

@garyha... period 2010-2011 is definitely.... an anomaly.... if you take out ....2011-2011 the algo performs.. poorly... can someone explain... why.. the sudden spike in that period... 2010-2011 in this algo...? thanks...

7
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: 567c06c068472e1369ffd1e1
There was a runtime error.