Momentum Strategy: zscore/sine wave

I thought this was an interesting finding... not suited for the competition, however. The algo seems to work well during a significant downturn, but can find itself in all the wrong trades when the market is not trending...

Basic idea is to calculate the zscore over 150 days and use that value to calculate allocation. I'm moving the zscore along the sin wave to scale from 2x leverage to 0.

Feel free to hack it up.
-Jamie

83
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
import math
from pytz import timezone

trading_freq = 1 # of days
n = 150 # sample size

def initialize(context):

context.secs = sid(8554)

context.day_count = -1

set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))

def handle_data(context, data):

lev = context.account.leverage

# Trade according to freq rule
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 11 and loc_dt.minute ==0:
context.day_count += 1
pass
else:
return

if context.day_count % trading_freq != 0.0:
return

####################################
mean = data[context.secs].mavg(n)
sigma = data[context.secs].stddev(n)
price = data[context.secs].price
z = (price - mean) / sigma

target = math.sin(z)+1 # adding 1 to the sin wave to prevent shorting

# condition required to enter a trade.
if -4 <= z <= 4 and lev <=2:
order_target_percent(context.secs, target)

else:
order_target_percent(context.secs, 0)

record(lev = lev)
record(z=z)
#record(target = target)


There was a runtime error.
7 responses

So this is pretty much a bollinger band but scaled entry with a sin wave? Whats the logic behind that? If we see a pi/2 (around 1.5) move below the average, we don't position anything into the stock but if its a HUGE move (~4 sd down) then we go long full leverage? Thats not momentum.
Is there research or economic rationale for this? It seems to me like the model assumes that large moves accompany momentum while medium moves accompany mean reversion, which isn't that bad, just interesting.

Here's an improved version using a pure momentum scaling through a logistic function (1/(1+e^(-1.2x)) -- Thanks for the idea! -- 1.2 was picked arbitrarily such that at 4 stdev, the weight in SPY is nearly 200% (levered) and vice versa. I've also included a treasury option such that the strategy is short treasuries when we lever and long treasuries when we don't scale in on SPY. See the auxiliary chart. Notice that the beta is much lower while still retaining a higher alpha. The only downside is a higher Max Drawdown and slightly increased volatility by 1%

136
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
import math
from pytz import timezone

trading_freq = 1 # of days
n = 150 # sample size

def initialize(context):

context.secs = sid(8554)

context.day_count = -1

set_slippage(slippage.VolumeShareSlippage(volume_limit=0.25, price_impact=0.1))

def handle_data(context, data):

lev = context.account.leverage

# Trade according to freq rule
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 11 and loc_dt.minute ==0:
context.day_count += 1
pass
else:
return

if context.day_count % trading_freq != 0.0:
return

####################################
mean = data[context.secs].mavg(n)
sigma = data[context.secs].stddev(n)
price = data[context.secs].price
z = (price - mean) / sigma

#target = math.sin(z)+1 # adding 1 to the sin wave to prevent shorting
#target = 0.5*(math.sin(z)+1) # Long without leverage

#target = 1.0/(1+math.exp(-1.2*z)) # Pure momentum
target = 2.0/(1+math.exp(-1.2*z)) # Pure momentum with leverage

# condition required to enter a trade.
if -4 <= z <= 4 and lev <=2.5:
order_target_percent(context.secs, target)
order_target_percent(symbol('TLT'), 2-target)
else:
target = 0
order_target_percent(symbol('TLT'), 1)
order_target_percent(context.secs, 0)

record(Treasury = (1-target)*100)
record(SPY = target*100)
#record(target = target)


There was a runtime error.

How does TLT act as a hedge, and help the volatility?

Also, @good trader, you mistyped the logistic scale in the reply. Took me a bit to figure that out.

@Calvin: I think the reasoning for using TLT (iShares 20+ Year Treasury Bond ETF) is that it is relatively stable in a bear market and a good investment when momentum signals a downturn in the market. Not super sure though, I'm new at this stuff.

@ Jamie & good trader: thanks for the idea, this looks interesting!

@Calvin the TLT is what to position into when you are risk averse and dont want any money in the market (i.e in a traditional utility maximization scenario, the investor allocates optimally towards both the market portfolio and risk-free asset -- TLT as a proxy of rf in this case). As for the equation, woops, gonna fix it now.

Banks,

Great improvement! My rationale was pretty naive to be honest. I was looking for a way to be at 1x leverage when the zscore was at 0 and lever up to 2x when z = 2 and 0x when z = -2 (and i didn't want to be perfectly linear either). As an added benefit, sin(z)+1 allowed me to bring down my leverage as the zscore moved beyond 2 and increase leverage when SPY was oversold (zscore < -2).