Back to Community
Monthly Traded - Low Max Drawdown w/ Stock Market Like Returns

I will be starting an investment firm this year for clients that are close to retirement. The main focus will be on low max drawdowns with "stock market like" returns. Below is the best algo that I could come up with that trades monthly (credit given to CSSAnalytics & David Varadi in the source code). It is a blend of percentile channel trading (monthly) that is adjusted for by the volatility of the asset, mixed with 50% core ETF assets that do not change.

This algo looks good for the long term, however, in the last 5 years it has underperformed too much for this to be my go-to strategy. After this post, I will post the last 5 years backtest.

Goals:

  • Trade monthly or quarterly
  • Keep long term max drawdown as low as possible (under 16%, closer to 10% would be preferred)
  • Long only
  • Stock market like returns
  • Low calendar year losses
Clone Algorithm
13
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
# CSSAnalytics - Momentum Channels
#
# Source:
#   David Varadi
#   "A Simple Tactical Asset Allocation Portfolio with Percentile Channels"
#   (https://cssanalytics.wordpress.com/2015/01/26/a-simple-tactical-asset-allocation-portfolio-with-percentile-channels/)
#
# Put 50% of the portfolios in core assets that do not change, we only need to rebalance monthly or yearly depending on if taxable account.
# For the other 50% of the portfolio, the stock allocations will first be adjusted for volitility.  If the asset is more volitile, allocate less, less volitile, allocate more.  Those revised stock allocations will be divided into each channel for potential stock allocation.  
# For each channel, the allocation will be set to zero if under the entry percentile for that channel, if above, allocate the stock to the portfolio based on the revised volitility.
# Any unused allocation goes into the cash asset.

# Implementation notes:
#   - Removed commodity asset
#   - Added Intl. Equity asset
#   - Cash asset is 10 year US Govt Bond
#

import pandas as pd
import numpy as np

#Note: IEF had a lower drawdown  something like 10.8 vs 13.49
#core_etf, proportion = symbols('QQQ','XLP','TLT','IEF'), [0.25, 0.25, 0.25, 0.25]
# With XLP at 50 percent we have drawdown of 13.49 with returns of 187.47 beating the s and p 500.  Maybe check utilities VPU?

# These are the core assets that do not change. 50% allocation
core_etf, proportion = symbols('QQQ','XLP','TLT'), [0.25, 0.50, 0.25]

def initialize(context):
    
    #set_benchmark(sid(33652))
    
    # original active: VTI, IYR, LQD, DBC
    # original cash:   SHY
    
    # active: VTI, EFA, ICF, LQD
    # cash:   IEF
    #set_symbol_lookup_date('2014-07-11')
    #context.active = [sid(33652),sid(27100),sid(22739),sid(25899)]
    #context.active = [sid(8554),sid(22972),sid(25485),sid(26669),sid(26807)]
    # Maybe try QQQ in this mix
    # These stocks will be traded in a percentile channel, getting more aggressive as they are in more channels and switch to cash when they are not in channel
    context.active = [sid(8554),sid(22972),sid(25485),sid(26669),sid(26981)]
    #context.cash = sid(23870)
    context.cash = sid(23870)
    context.core_percent = 0.50
    context.active_percent = 0.50
    # Add all stocks to the assets including our cash stock pick
    context.assets = set(context.active + [context.cash])
    #context.channels = [60, 120, 180, 252]
    context.channels = [70, 130, 190, 262]
    # Changing the channels lookback length should give you more or less return with higher or lower drawdown
    #context.channels = [30, 60, 120, 180]

    # Where to enter the channel
    #context.entry = 0.75
    context.entry = 0.90
    # When to exit the channel
    context.exit = 0.50
    
    context.leverage = 1.00
    # Create a panda to hold the asset allocations
    context.alloc = pd.Series([0.0] * len(context.assets), index=context.assets)
    
    # Create the modes array with the stock and channel lookback length together for each stock
    context.modes = {}
    for s in context.active:
        for l in context.channels:
            context.modes[(s, l)] = 0

    # On the opening day of the month, after market open, reallocate and rebalance the portfolio
    schedule_function(
        reallocate,
        date_rules.month_end(days_offset=0),
        time_rules.market_close(minutes=30)
    )
    
    schedule_function(
        rebalance,
        date_rules.month_end(days_offset=0),
        time_rules.market_close(minutes=25)
    )
            
def reallocate(context, data):
    
    # Buy or rebalance the core stocks, 50% of the portfolio
    for i in range(len(core_etf)):
        if data.can_trade(core_etf[i]):
            order_target_percent(core_etf[i],  proportion[i]*context.core_percent)
    
    # Get 300 days of price history for the active stock, if one of your channel lengths is greater than 300, increase it here
    h = data.history(context.active, fields="price", bar_count=300, frequency="1d")
    # Use 20-day historical volatility for risk parity position-sizing among active assets (no leverage is used). This is 1/volatility (asset A) divided by the sum of 1/volatility for all assets to determine the position size.
    hs = h.ix[-20:]
    # Set yesterday's prices for each stock
    p = h.ix[-1]
    # For risk parity, 1/volatility gives us the historic volatility adjusted stock price
    hvol = 1.0 / hs.pct_change().std()
    # Sum up all the volatility adjusted stock prices
    hvol_all = hvol.sum()
    # Set the ratios for each stock to buy based on the volatility, lower volatity means buy more. Then divide by the number of channels.  Effectively spitting the stock buys into multiple channels.
    r = (hvol / hvol_all) * 1.0 / len(context.channels)
    print("RATIOS:\n%s" % r)
    # Create another temporary panda to hold asset allocations
    alloc = pd.Series([0.0] * len(context.assets), index=context.assets)
    
    # Loop through the channels, l = lookback length of channel
    for l in context.channels:
        # Get the historical prices based on the channel length
        #values = h.ix[-l:]
        values = h.ix[-l:]
        # entry is the upper channel price.  To get it, we take all the historical prices for the channel length and sort it, then the 75th percentile position is calculated and marked as the entry price for each stock.
        entry = values.quantile(context.entry)
        # exit is the lower channel price based on the 25th percentile position
        exit = values.quantile(context.exit)
        
        for s in context.active:
            m = (s, l)
            if context.modes[m] == 0 and p[s] >= entry[s]:
                #print("entry: %s/%d" % (s.symbol, l))
                context.modes[m] = 1
            elif context.modes[m] == 1 and p[s] <= exit[s]:
                #print("exit: %s/%d" % (s.symbol, l))
                context.modes[m] = 0
                
            if context.modes[m] == 0:
                #print("alloc cash: %s/%d" % (context.cash, r[s]))
                alloc[context.cash] += r[s]
            elif context.modes[m] == 1:
                #print("alloc stock: %s/%d" % (s.symbol, r[s]))
                alloc[s] += r[s]
            print("TEMP Allocation:\n%s" % alloc)
                
    context.alloc = alloc
    
    print("Allocation:\n%s" % context.alloc)
    
def rebalance(context, data):
    for s in context.alloc.index:
        if s in data:
            print("BUY STOCK:\n%s" % s)
            order_target_percent(s, context.alloc[s] * context.leverage*context.active_percent)
            
def handle_data(context, data):
    record(lev=context.account.leverage)
    pass
There was a runtime error.
10 responses

Below is the 5 year backtest of the same strategy as above. Note the underperformance overall and in 2017, 2019.

Clone Algorithm
13
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
# CSSAnalytics - Momentum Channels
#
# Source:
#   David Varadi
#   "A Simple Tactical Asset Allocation Portfolio with Percentile Channels"
#   (https://cssanalytics.wordpress.com/2015/01/26/a-simple-tactical-asset-allocation-portfolio-with-percentile-channels/)
#
# Put 50% of the portfolios in core assets that do not change, we only need to rebalance monthly or yearly depending on if taxable account.
# For the other 50% of the portfolio, the stock allocations will first be adjusted for volitility.  If the asset is more volitile, allocate less, less volitile, allocate more.  Those revised stock allocations will be divided into each channel for potential stock allocation.  
# For each channel, the allocation will be set to zero if under the entry percentile for that channel, if above, allocate the stock to the portfolio based on the revised volitility.
# Any unused allocation goes into the cash asset.

# Implementation notes:
#   - Removed commodity asset
#   - Added Intl. Equity asset
#   - Cash asset is 10 year US Govt Bond
#

import pandas as pd
import numpy as np

#Note: IEF had a lower drawdown  something like 10.8 vs 13.49
#core_etf, proportion = symbols('QQQ','XLP','TLT','IEF'), [0.25, 0.25, 0.25, 0.25]
# With XLP at 50 percent we have drawdown of 13.49 with returns of 187.47 beating the s and p 500.  Maybe check utilities VPU?

# These are the core assets that do not change. 50% allocation
core_etf, proportion = symbols('QQQ','XLP','TLT'), [0.25, 0.50, 0.25]

def initialize(context):
    
    #set_benchmark(sid(33652))
    
    # original active: VTI, IYR, LQD, DBC
    # original cash:   SHY
    
    # active: VTI, EFA, ICF, LQD
    # cash:   IEF
    #set_symbol_lookup_date('2014-07-11')
    #context.active = [sid(33652),sid(27100),sid(22739),sid(25899)]
    #context.active = [sid(8554),sid(22972),sid(25485),sid(26669),sid(26807)]
    # Maybe try QQQ in this mix
    # These stocks will be traded in a percentile channel, getting more aggressive as they are in more channels and switch to cash when they are not in channel
    context.active = [sid(8554),sid(22972),sid(25485),sid(26669),sid(26981)]
    #context.cash = sid(23870)
    context.cash = sid(23870)
    context.core_percent = 0.50
    context.active_percent = 0.50
    # Add all stocks to the assets including our cash stock pick
    context.assets = set(context.active + [context.cash])
    context.channels = [70, 130, 190, 262]
    # Changing the channels lookback length should give you more or less return with higher or lower drawdown
    #context.channels = [30, 60, 120, 180]

    # Where to enter the channel
    #context.entry = 0.75
    context.entry = 0.90
    # When to exit the channel
    context.exit = 0.50
    
    context.leverage = 1.00
    # Create a panda to hold the asset allocations
    context.alloc = pd.Series([0.0] * len(context.assets), index=context.assets)
    
    # Create the modes array with the stock and channel lookback length together for each stock
    context.modes = {}
    for s in context.active:
        for l in context.channels:
            context.modes[(s, l)] = 0

    # On the opening day of the month, after market open, reallocate and rebalance the portfolio
    schedule_function(
        reallocate,
        date_rules.month_start(days_offset=0),
        time_rules.market_open(minutes=4)
    )
    
    schedule_function(
        rebalance,
        date_rules.month_start(days_offset=0),
        time_rules.market_open(minutes=5)
    )
            
def reallocate(context, data):
    
    # Buy or rebalance the core stocks, 50% of the portfolio
    for i in range(len(core_etf)):
        if data.can_trade(core_etf[i]):
            order_target_percent(core_etf[i],  proportion[i]*context.core_percent)
    
    # Get 300 days of price history for the active stock, if one of your channel lengths is greater than 300, increase it here
    h = data.history(context.active, fields="price", bar_count=300, frequency="1d")
    # Use 20-day historical volatility for risk parity position-sizing among active assets (no leverage is used). This is 1/volatility (asset A) divided by the sum of 1/volatility for all assets to determine the position size.
    hs = h.ix[-20:]
    # Set yesterday's prices for each stock
    p = h.ix[-1]
    # For risk parity, 1/volatility gives us the historic volatility adjusted stock price
    hvol = 1.0 / hs.pct_change().std()
    # Sum up all the volatility adjusted stock prices
    hvol_all = hvol.sum()
    # Set the ratios for each stock to buy based on the volatility, lower volatity means buy more. Then divide by the number of channels.  Effectively spitting the stock buys into multiple channels.
    r = (hvol / hvol_all) * 1.0 / len(context.channels)
    print("RATIOS:\n%s" % r)
    # Create another temporary panda to hold asset allocations
    alloc = pd.Series([0.0] * len(context.assets), index=context.assets)
    
    # Loop through the channels, l = lookback length of channel
    for l in context.channels:
        # Get the historical prices based on the channel length
        #values = h.ix[-l:]
        values = h.ix[-l:]
        # entry is the upper channel price.  To get it, we take all the historical prices for the channel length and sort it, then the 75th percentile position is calculated and marked as the entry price for each stock.
        entry = values.quantile(context.entry)
        # exit is the lower channel price based on the 25th percentile position
        exit = values.quantile(context.exit)
        
        for s in context.active:
            m = (s, l)
            if context.modes[m] == 0 and p[s] >= entry[s]:
                #print("entry: %s/%d" % (s.symbol, l))
                context.modes[m] = 1
            elif context.modes[m] == 1 and p[s] <= exit[s]:
                #print("exit: %s/%d" % (s.symbol, l))
                context.modes[m] = 0
                
            if context.modes[m] == 0:
                #print("alloc cash: %s/%d" % (context.cash, r[s]))
                alloc[context.cash] += r[s]
            elif context.modes[m] == 1:
                #print("alloc stock: %s/%d" % (s.symbol, r[s]))
                alloc[s] += r[s]
            print("TEMP Allocation:\n%s" % alloc)
                
    context.alloc = alloc
    
    print("Allocation:\n%s" % context.alloc)
    
def rebalance(context, data):
    for s in context.alloc.index:
        if s in data:
            print("BUY STOCK:\n%s" % s)
            order_target_percent(s, context.alloc[s] * context.leverage*context.active_percent)
            
def handle_data(context, data):
    record(lev=context.account.leverage)
    pass
There was a runtime error.

@Max,

Thank you for redepositing Alex's "CSSAnalytics - A Simple Tactical Asset Allocation Portfolio with Percentile Channels"
Why didn't you post it on that thread?

@Vladimir,

I did give credit, as indicated above, however, I changed the algo a fair amount and I'm looking for some specific help related to my goals listed above. If anyone can help, it would be appreciated.

-- Max

Mr. Max Drawdown

FYI.David Varadi writes his algorithms in R.
So you took the satellite part of your algorithm from this thread.
I don't see you mentioning this.

Alex code in Q1

    h = history(300, '1d', 'price')[context.active]  
    hs = h.ix[-20:]  
    p = h.ix[-1]  
    hvol = 1.0 / hs.pct_change().std()  
    hvol_all = hvol.sum()  
    r = (hvol / hvol_all) * 1.0 / len(context.channels)  
    alloc = pd.Series([0.0] * len(context.assets), index=context.assets)  

Your code without comments

    h = data.history(context.active, fields="price", bar_count=300, frequency="1d")  
    hs = h.ix[-20:]  
    p = h.ix[-1]  
    hvol = 1.0 / hs.pct_change().std()  
    hvol_all = hvol.sum()  
    r = (hvol / hvol_all) * 1.0 / len(context.channels)  
    alloc = pd.Series([0.0] * len(context.assets), index=context.assets)  

What significant changes have you made to the code to call it your own?

I'm not claiming that any of this is my original idea. To the contrary, I'm trying to give credit to CSSAnalytics / David Varadi.

I've made the following changes:

  1. The channels lengths have been increased.
  2. I've added core assets, 50% of the portfolio.
  3. The entry has been increased from .75 to .90
  4. The assets have been changed.
  5. I've commented the code.

Although it is better, it's still not achieving my goals listed above. Any help would be appreciated.

-- Max

Mr. Max Drawdown,

Until you accept that you have re-deposited Alex's "CSSAnalytics - Simple Tactical Asset Allocation Portfolio with Percentage Channels"
possibly with some parameters changed and edit your first post here accordingly, I will continue to post code snippets Alex and your code.

First 6 lines of algo.

Alex code

# CSSAnalytics - Momentum Channels Alex version  
#  
# Source:  
#   David Varadi  
#   "A Simple Tactical Asset Allocation Portfolio with Percentile Channels"  
#   (https://cssanalytics.wordpress.com/2015/01/26/a-simple-tactical-asset-allocation-portfolio-with-percentile-channels/)  
#  

Mr.Max Drawdown code

# CSSAnalytics - Momentum Channels Mr. Max Drawdown version  
#  
# Source:  
#   David Varadi  
#   "A Simple Tactical Asset Allocation Portfolio with Percentile Channels"  
#   (https://cssanalytics.wordpress.com/2015/01/26/a-simple-tactical-asset-allocation-portfolio-with-percentile-channels/)  
#  

Where did you get the algorithm code?

I do really have about 200 algorithms which met all your requirements.

Like this:

Loading notebook preview...

I find myself quite amused here. We all source and plagiarize ideas from all over the internet. Its a dog eat dog world out there and you will not find any or much true originality. What was it Newton said about standing on the shoulders of others?

@Vladimir,

Yes, yes yes the original code came from Alex's CSSAnalytics post, I believe stemming from some of David Varadi's ideas. Not trying to hide that fact and the reason why I added / edited CSSAnalytics into the first post. Trying to obtain these goals:

Goals:

  • Trade monthly or quarterly
  • Keep long term max drawdown as low as possible (under 16%, closer to 10% would be preferred)
  • Long only
  • Stock market like returns
  • Low calendar year losses

If you would be so kind as to share one of your algorithms, that would be very much appreciated.

-- Max

@Vladimir

I agree that the lack of a clear and explicit acknowledgement of Alex's CSSAnalytics post did leave me feeling unsettled.

Given Max D's amendments made to rectify this, if your comfortable, I am really keen on learning from your insights on this algo your willing to share

@Umar Hasan,

I am really keen on learning from your insights on this algo.

I respect you as an active long time member of the Quantopian forum and am ready to discuss, but I will not do that in this thread.
Let's ask originator of this thread Mr.Max Drawdown share his insights of the code.
And why (credit given to CSSAnalytics & David Varadi in the source code) , but not to Alex?

@ Anthony F.J. Garner,

What did Newton say about standing on the shoulders of others?

If everyone who stood on Isaac Newton's shoulders behaved like Mr Max Drawdown,
we would not even know such a name Isaac Newton.
It is not clear from this thread who is the real author of the code published above.