Back

Help porting a strategy from Quantopian

I'm trying to port this strategy from Quantopian:

"""
Original by: Christopher Cain, CMT & Larry Connors
Posted here: https://www.quantopian.com/posts/new-strategy-presenting-the-quality-companies-in-an-uptrend-model-1
(Dan Whitnabl version with fixed bonds weights)
(Nathan Wells modified for performace and logging)
"""
# Quality companies in an uptrend
import quantopian.algorithm as algo

# import things need to run pipeline
from quantopian.pipeline import Pipeline

# import any built-in factors and filters being used
from quantopian.pipeline.filters import Q500US, Q1500US, Q3000US, QTradableStocksUS, StaticAssets
from quantopian.pipeline.factors import SimpleMovingAverage as SMA
from quantopian.pipeline.factors import CustomFactor, Returns

# import any needed datasets
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.data.morningstar import Fundamentals as ms

# import optimize for trade execution
import quantopian.optimize as opt
# import numpy and pandas because they rock
import numpy as np
import pandas as pd

def initialize(context):
# Set algo 'constants'...
# List of bond ETFs when market is down. Can be more than one.
context.BONDS = [symbol('IEF'), symbol('TLT')]

# Set target number of securities to hold and top ROE qty to filter
context.TARGET_SECURITIES = 5
context.TOP_ROE_QTY = 50 #First sort by ROE

# This is for the trend following filter
context.SPY = symbol('SPY')
context.TF_LOOKBACK = 200
context.TF_CURRENT_LOOKBACK = 20

# This is for the determining momentum
context.MOMENTUM_LOOKBACK_DAYS = 126 #Momentum lookback
context.MOMENTUM_SKIP_DAYS = 10
# Initialize any other variables before being used
context.stock_weights = pd.Series()
context.bond_weights = pd.Series()

# Should probably comment out the slippage and using the default
# set_slippage(slippage.FixedSlippage(spread = 0.0))
# Create and attach pipeline for fetching all data
algo.attach_pipeline(make_pipeline(context), 'pipeline')
# Schedule functions
# Separate the stock selection from the execution for flexibility
schedule_function(
select_stocks_and_set_weights,
date_rules.month_end(days_offset = 7),
time_rules.market_close(minutes = 30)
)
schedule_function(
trade,
date_rules.month_end(days_offset = 7),
time_rules.market_close(minutes = 30)
)
schedule_function(record_vars, date_rules.every_day(), time_rules.market_close())

def make_pipeline(context):
universe = Q1500US()
spy_ma50_slice = SMA(inputs=[USEquityPricing.close],
window_length=context.TF_CURRENT_LOOKBACK)[context.SPY]
spy_ma200_slice = SMA(inputs=[USEquityPricing.close],
window_length=context.TF_LOOKBACK)[context.SPY]
spy_ma_fast = SMA(inputs=[spy_ma50_slice], window_length=1)
spy_ma_slow = SMA(inputs=[spy_ma200_slice], window_length=1)
trend_up = spy_ma_fast > spy_ma_slow

cash_return = ms.cash_return.latest.rank(mask=universe) #(mask=universe)
fcf_yield = ms.fcf_yield.latest.rank(mask=universe) #(mask=universe)
roic = ms.roic.latest.rank(mask=universe) #(mask=universe)
ltd_to_eq = ms.long_term_debt_equity_ratio.latest.rank(mask=universe) #, mask=universe)
value = (cash_return + fcf_yield).rank() #(mask=universe)
quality = roic + ltd_to_eq + value
# Create a 'momentum' factor. Could also have been done with a custom factor.
returns_overall = Returns(window_length=context.MOMENTUM_LOOKBACK_DAYS+context.MOMENTUM_SKIP_DAYS)
returns_recent = Returns(window_length=context.MOMENTUM_SKIP_DAYS)
### momentum = returns_overall.log1p() - returns_recent.log1p()
momentum = returns_overall - returns_recent
# Filters for top quality and momentum to use in our selection criteria
top_quality = quality.top(context.TOP_ROE_QTY, mask=universe)
top_quality_momentum = momentum.top(context.TARGET_SECURITIES, mask=top_quality)
# Only return values we will use in our selection criteria
pipe = Pipeline(columns={
'trend_up': trend_up,
'top_quality_momentum': top_quality_momentum,
},
screen=top_quality_momentum
)
return pipe

def select_stocks_and_set_weights(context, data):
"""
Select the stocks to hold based upon data fetched in pipeline.
Then determine weight for stocks.
Finally, set bond weight to 1-total stock weight to keep portfolio fully invested
Sets context.stock_weights and context.bond_weights used in trade function
"""
# Get pipeline output and select stocks
df = algo.pipeline_output('pipeline')
current_holdings = context.portfolio.positions
# Define our rule to open/hold positions
# top momentum and don't open in a downturn but, if held, then keep
rule = 'top_quality_momentum & (trend_up or (not trend_up & index in @current_holdings))'
stocks_to_hold = df.query(rule).index

# Set desired stock weights
# Equally weight
stock_weight = 1.0 / context.TARGET_SECURITIES
context.stock_weights = pd.Series(index=stocks_to_hold, data=stock_weight)
# Set desired bond weight
# Open bond position to fill unused portfolio balance
# But always have at least 1 'share' of bonds
### bond_weight = max(1.0 - context.stock_weights.sum(), stock_weight) / len(context.BONDS)
bond_weight = max(1.0 - context.stock_weights.sum(), 0) / len(context.BONDS)
context.bond_weights = pd.Series(index=context.BONDS, data=bond_weight)
#print("Stocks to buy " + str(stocks_to_hold))
print("Stocks to buy: " + str([ str(s.symbol) for s in stocks_to_hold ]) )

#print("Bonds weight " + str(bond_weight))
def trade(context, data):
"""
Execute trades using optimize.
Expects securities (stocks and bonds) with weights to be in context.weights
"""
# Create a single series from our stock and bond weights
total_weights = pd.concat([context.stock_weights, context.bond_weights])

# Create a TargetWeights objective
target_weights = opt.TargetWeights(total_weights)

# Execute the order_optimal_portfolio method with above objective and any constraint
order_optimal_portfolio(
objective = target_weights,
constraints = []
)
#Log our holdings
log.info( [ str(s.symbol) for s in sorted(context.portfolio.positions) ] )
#print("Cash: " + str(context.portfolio.cash))
# Record our weights for insight into stock/bond mix and impact of trend following
record(stocks=context.stock_weights.sum(), bonds=context.bond_weights.sum())
def record_vars(context, data):
record(leverage = context.account.leverage)
longs = shorts = 0
for stock in context.portfolio.positions:
if context.portfolio.positions[stock].amount > 0:
longs += 1
elif context.portfolio.positions[stock].amount < 0:
shorts += 1
record(longs = longs)
record(shorts = shorts)

I've been messing around with QuantConnect, but I can't seem to get things to match. Anyone willing to help out?

Here's what I found in QuantConnect that was a little simular and I was trying to edit it in QuantConnect, trying to imitate the same results (but it's not even close):

# Taken from https://www.quantconnect.com/forum/discussion/3377/momentum-strategy-with-market-cap-and-ev-ebitda
# Created by Jing Wu
# Edited by Nathan Wells trying to mirror Original by: Christopher Cain, CMT & Larry Connors
#Posted here: https://www.quantopian.com/posts/new-strategy-presenting-the-quality-companies-in-an-uptrend-model-1



from clr import AddReference
AddReference("System.Core")
AddReference("System.Collections")
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Algorithm")

from System import *
from System.Collections.Generic import List
from QuantConnect import *
from QuantConnect.Algorithm import QCAlgorithm
from QuantConnect.Data.UniverseSelection import *
from QuantConnect.Indicators import *

import math
import numpy as np
import pandas as pd
import scipy as sp
# import statsmodels.api as sm

class FundamentalFactorAlgorithm(QCAlgorithm):

def Initialize(self):

self.SetStartDate(2019, 1, 1) #Set Start Date
self.SetEndDate(2019, 12, 31) #Set End Date
self.SetCash(10000) #Set Strategy Cash

#changed from Daily to Monthly
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
#changed from Minuite to Daily
self.spy = self.AddEquity("SPY", Resolution.Daily).Symbol
self.holding_months = 1
self.num_screener = 100
self.num_stocks = 5
self.formation_days = 126
self.lowmom = False
self.month_count = self.holding_months
self.Schedule.On(self.DateRules.MonthEnd("SPY"), self.TimeRules.AfterMarketOpen("SPY", 15), Action(self.monthly_rebalance))
self.Schedule.On(self.DateRules.MonthEnd("SPY"), self.TimeRules.AfterMarketOpen("SPY", 15), Action(self.rebalance))
# rebalance the universe selection once a month
self.rebalence_flag = 0
# make sure to run the universe selection at the start of the algorithm even it's not the manth start
self.first_month_trade_flag = 1
self.trade_flag = 0
self.symbols = None

def CoarseSelectionFunction(self, coarse):
if self.rebalence_flag or self.first_month_trade_flag:
# drop stocks which have no fundamental data or have too low prices
selected = [x for x in coarse if (x.HasFundamentalData) and (float(x.Price) > 10)]
# Drop stocks with no fundamental data
#selected = [x for x in coarse if (x.HasFundamentalData)]
### rank the stocks by dollar volume
#filtered = sorted(selected, key=lambda x: x.DollarVolume, reverse=True)



#ltd_to_eq
#ltd_to_eq = x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths
#roic
#roic = x.OperationRatios.ROIC.ThreeMonths
#fcf_yield
#fcf_yield = x.ValuationRatios.FCFYield.ThreeMonths
#cash_return
#cash_return = x.ValuationRatios.CashReturn.ThreeMonths
#quality
#quality = roic + ltd_to_eq + (cash_return + fcf_yield).rank()

#Sort by quality
#top_quality = sorted(selected, key=lambda x: x.OperationRatios.ROIC.ThreeMonths + x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths + (x.ValuationRatios.CashReturn.ThreeMonths + x.ValuationRatios.FCFYield.ThreeMonths).rank(), reverse=True)
#top_quality = sorted(selected, key=lambda x: x.OperationRatios.ROIC.ThreeMonths, reverse=True)
#return [ x.Symbol for x in selected[:50]]
return [ x.Symbol for x in selected]
else:
return self.symbols


def FineSelectionFunction(self, fine):
if self.rebalence_flag or self.first_month_trade_flag:
#Sort by quality
top_quality = sorted(fine, key=lambda x: x.OperationRatios.ROIC.OneYear + x.OperationRatios.LongTermDebtEquityRatio.OneYear + (x.ValuationRatios.CashReturn + x.ValuationRatios.FCFYield), reverse=True)[:self.num_screener]
#top_momentum = sorted(top_quality, key=lambda x: MOM(x.Symbol, 126, Resolution.Daily), reverse=True)[:self.num_stocks]

#try:
# filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0)
# and (x.EarningReports.BasicAverageShares.ThreeMonths > 0)
# and x.EarningReports.BasicAverageShares.ThreeMonths * (x.EarningReports.BasicEPS.TwelveMonths*x.ValuationRatios.PERatio) > 2e9]
#except:
# filtered_fine = [x for x in fine if (x.ValuationRatios.EVToEBITDA > 0)
# and (x.EarningReports.BasicAverageShares.ThreeMonths > 0)]

#top = sorted(filtered_fine, key = lambda x: x.ValuationRatios.EVToEBITDA, reverse=True)[:self.num_screener]
top_return_month = sorted(top_quality, key= lambda x: x.ValuationRatios.PriceChange1M, reverse = True)[:self.num_stocks]

self.symbols = [x.Symbol for x in top_return_month]
#Can't seem to use a MOM here...
#momentum = MOM(symbol, 126, Resolution.Daily)
self.rebalence_flag = 0
self.first_month_trade_flag = 0
self.trade_flag = 1
return self.symbols
else:
return self.symbols

def OnData(self, data):
pass

def monthly_rebalance(self):
self.rebalence_flag = 1

def rebalance(self):
#Looks like this sells if they drop below the mean of SPY, so I disabled it
#spy_hist = self.History([self.spy], self.formation_days, Resolution.Daily).loc[str(self.spy)]['close']
#if self.Securities[self.spy].Price < spy_hist.mean():
# for symbol in self.Portfolio.Keys:
# if symbol.Value != "TLT":
# self.Liquidate()
# self.AddEquity("TLT")
# self.SetHoldings("TLT", 1)
# return

if self.symbols is None: return
chosen_df = self.calc_return(self.symbols)
chosen_df = chosen_df.iloc[:self.num_stocks]

self.existing_pos = 0
add_symbols = []
for symbol in self.Portfolio.Keys:
#if symbol.Value == 'SPY': continue
if (symbol.Value not in chosen_df.index):
#self.SetHoldings(symbol, 0)
self.Liquidate(symbol)
elif (symbol.Value in chosen_df.index):
self.existing_pos += 1

weight = 0.99/len(chosen_df)
for symbol in chosen_df.index:
self.AddEquity(symbol)
self.SetHoldings(symbol, weight)

def calc_return(self, stocks):
#Need to change this to just be an uptrend or downtrend...and buy bonds in downtrend.
hist = self.History(stocks, self.formation_days, Resolution.Daily)
current = self.History(stocks, 10, Resolution.Daily)

self.price = {}
ret = {}

for symbol in stocks:
if str(symbol) in hist.index.levels[0] and str(symbol) in current.index.levels[0]:
self.price[symbol.Value] = list(hist.loc[str(symbol)]['close'])
self.price[symbol.Value].append(current.loc[str(symbol)]['close'][0])

for symbol in self.price.keys():
ret[symbol] = (self.price[symbol][-1] - self.price[symbol][0]) / self.price[symbol][0]
df_ret = pd.DataFrame.from_dict(ret, orient='index')
df_ret.columns = ['return']
sort_return = df_ret.sort_values(by = ['return'], ascending = self.lowmom)

return sort_return

Appreciate any help - thanks!

Update Backtest







0

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 QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect 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. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.


I would suggest starting with a simple Buy-and-Hold strategy on both platforms first, it would be good for learning the LEAN API.

The Quantopian one generally goes Universe Selection -> Factors -> Pipeline  -> Portfolio Optimization (some of which are executed in the same function), whereas QuantConnect is more modular and goes Universe Selection -> Indicators -> Alpha Model -> Portfolio Optimization -> Execution. I would suggest going breaking down the strategy by each module, and making sure the results are the same (i.e. through a series of Debugging statements). There's not really an equivalent of make_pipeline() here, and Indicators can be written as either an Indicator object (with the Update method updating the values as data comes in, and OnSecuritiesChanged handling what happens when universe changes) or in OnData (which is called every time data comes in; may be more intuitive at first when coming from Quantopian).

As for why results are different, the logic between the two does not look the same. There are also references to methods such as Q1500US() which is proprietary to Quantopian.

0

Yes, probably a good idea. And thanks for the explaination of differences. That is helpful as well.

0

Update Backtest





0

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 QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect 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. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.


Loading...

This discussion is closed