Back

Ad Book-to-Market Anomaly

Hi Team, 

I´m new to QC & trying out some basic stuff.
Goal here is to track the breakpoints of a classic "value" portfolio (as outlined in https://www.quantconnect.com/tutorials/strategy-library/book-to-market-value-anomaly#Book-to-Market-Value-Anomaly-Source), then pick out one ticker (the one with largest market cap on the first rebalancing period) and track its inclusion  / exclusion in the value portfolio. 
More precisely I want to track its P/B at each rebalancing date and see whether it´s inside the value portfolio or not. You can see this from the custom charts I added.

Besides massive speed issues (not sure how to improve on them) I seem to misunderstand some basic concepts on the flow of the algorithm. For example, why is the P/B Ratio of the asset I´m tracking always constant? Is this the way to fetch the current P/B ratio (line 102). Equally unsure about fetching last Price on rebalancing date (line 125).. 
Then for to me inexplicable reasons the backtest broke down midway (!) in my last attempt due to line 125.

Also wondering if this type of analysis should better be done in a research notebook? But unclear how to import universes, etc. 

Always knew I was dumb & slow, but hardly been as frustrated as this. Thank you so much for taking the time to look into it

Below is the original backtest, while here is the code with some changes:

from QuantConnect.Data.UniverseSelection import *
from System.Drawing import Color
import operator
import math
import numpy as np
import pandas as pd
import scipy as sp

class BooktoMarketAnomaly(QCAlgorithm):

def Initialize(self):

self.SetStartDate(2016,1,2) # Set Start Date
self.SetEndDate(2019,3,15) # Set End Date
self.SetCash(50000) # Set Strategy Cash

self.flag0 = 1 # variable to select test-asset in first rebalancing month
self.flag1 = 1 # variable to control the monthly rebalance of coarse and fine selection function
self.flag2 = 0 # variable to control the monthly rebalance of OnData function
self.flag3 = 0 # variable to record the number of rebalancing times

# Initialize Trading Variables
self.test_asset_object = None
self.test_asset_ticker = None
self.long_list = None
self._changes = None

# Initialize Universe + Settings
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

# Initialize Benchmark
self.SetBenchmark("SPY")
self.AddEquity("SPY", Resolution.Daily)

# Schedule functions
# Trigger an event every day a specific symbol is trading --> here monthly
self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY"), Action(self.Rebalancing))

# Initialize Charts
breakpPlot = Chart('Fundamentals')
breakpPlot.AddSeries(Series('Test-Asset-PB', SeriesType.Line))
breakpPlot.AddSeries(Series('Breakpoint-Min', SeriesType.Line))
breakpPlot.AddSeries(Series('Breakpoint-Max', SeriesType.Line))
self.AddChart(breakpPlot)

stockPlot = Chart('Trade Plot')
stockPlot.AddSeries(Series('Price', SeriesType.Line, '$', Color.Blue))
stockPlot.AddSeries(Series('Buy', SeriesType.Scatter, '$', Color.Green, ScatterMarkerSymbol.Triangle))
stockPlot.AddSeries(Series('Sell', SeriesType.Scatter, '$', Color.Red, ScatterMarkerSymbol.TriangleDown))
self.AddChart(stockPlot)

def CoarseSelectionFunction(self, coarse):
if self.flag1:
self.filtered_coarse = [x.Symbol for x in coarse if (x.HasFundamentalData and x.AdjustedPrice > 5)]
return self.filtered_coarse
else:
return []

def FineSelectionFunction(self, fine):
if self.flag1:
self.flag1 = 0
self.flag2 = 1

fine = [x for x in fine if (x.ValuationRatios.PBRatio > 0)]
# Calculate the market cap and add the "MarketCap" property to fine universe object
for i in fine:
i.MarketCap = float(i.EarningReports.BasicAverageShares.ThreeMonths * (i.EarningReports.BasicEPS.TwelveMonths*i.ValuationRatios.PERatio))
# Syntax : sorted(iterable, key, reverse) --> reverse means from highest (expensive) to lowest (cheap)
top_market_cap = sorted(fine, key = lambda x: x.MarketCap, reverse=True)[:int(len(fine)*0.2)] # Pick out top 20% mcap of all stock

top_bm = sorted(top_market_cap, key = lambda x: 1 / x.ValuationRatios.PBRatio, reverse=True)[:int(len(top_market_cap)*0.2)] # From top 20% mcap pick out top 20% value stocks
self.sorted_by_bm = [i.Symbol for i in top_bm]

top_bm_ratios = [i.ValuationRatios.PBRatio for i in top_bm]
current_universe_ratios = dict(zip(top_bm, top_bm_ratios))

current_min = min(current_universe_ratios.items(), key=lambda x: x[1])
current_max = max(current_universe_ratios.items(), key=lambda x: x[1])

# Save P/B Ratio cut-off breakpoint for plot
self.breakpoint_min = current_min[1]
self.breakpoint_max = current_max[1]

# FOR TESTING
if self.flag0:
top_bm_mcaps = [i.MarketCap for i in top_bm]
first_universe_mcaps = dict(zip(top_bm, top_bm_mcaps))
max_mcap = max(first_universe_mcaps.items(), key=lambda x: x[1])

self.test_asset_object = max_mcap[0] # this is a security object just like in top_bm
self.test_asset_ticker = self.test_asset_object.SecurityReference.SecuritySymbol

self.long_list = [self.test_asset_object.Symbol]
self.test_asset_pb = self.test_asset_object.ValuationRatios.PBRatio
# Add test-asset to our securities
self.AddEquity(str(self.test_asset_ticker), Resolution.Daily)
# Close this if-statement; never return again
self.flag0 = 0
else:
self.test_asset_pb = self.test_asset_object.ValuationRatios.PBRatio
self.Debug('Current P/B Ratio: ' +str(self.test_asset_pb))

if self.test_asset_pb > self.breakpoint_max:
self.long_list = list()
else:
long_set = set(self.long_list)
long_set.add(self.test_asset_object.Symbol)
self.long_list = list(long_set)

self.flag3 += 1

return self.long_list

else:
return []

def OnData(self, data):
if self.flag3 > 0:
if self.flag2 == 1:
self.flag2 = 0

if self.test_asset_ticker:
self.test_lastPrice = self.ActiveSecurities[str(self.test_asset_ticker)].Price

# if we have no changes, do nothing
if self._changes == None: return
# liquidate removed securities
for security in self._changes.RemovedSecurities:
if security.Invested:
self.Liquidate(security.Symbol)
self.Plot("Trade Plot", "Sell", self.test_lastPrice)

for security in self._changes.AddedSecurities:
self.SetHoldings(security.Symbol, 1)
self.Plot("Trade Plot", "Buy", self.test_lastPrice)

self._changes = None
self.Plot('Fundamentals', 'Test-Asset P/B Ratio', self.test_asset_pb)
self.Plot('Fundamentals', 'Breakpoint Min', self.breakpoint_min)
self.Plot('Fundamentals', 'Breakpoint Max', self.breakpoint_max)

# this event fires whenever we have changes to our universe
def OnSecuritiesChanged(self, changes):
self._changes = changes

def Rebalancing(self):
self.flag1 = 1

 

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.


Hi F,

I've responded to another post you made regarding some of the functioning of this algorithm, and it should address your concerns here. You can view it and the accompanying backtest here. To briefly address your concerns here, you are correctly accessing price and P/B ratio in lines 125 and 102 respectively. Line 125 will fail if the Symbol that the variable self.test_asset_ticker refers to is no longer an active security in the Universe. This sort of Fundamental Data research can be done in our Research Environment and can be very useful, but Slice objects and a number of other features are not available there.

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.


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