| Overall Statistics |
|
Total Orders 2412 Average Win 0.18% Average Loss -0.12% Compounding Annual Return 11.236% Drawdown 16.400% Expectancy 0.467 Start Equity 540000 End Equity 1108394.31 Net Profit 105.258% Sharpe Ratio 0.583 Sortino Ratio 0.585 Probabilistic Sharpe Ratio 28.549% Loss Rate 42% Win Rate 58% Profit-Loss Ratio 1.51 Alpha 0.023 Beta 0.395 Annual Standard Deviation 0.096 Annual Variance 0.009 Information Ratio -0.224 Tracking Error 0.121 Treynor Ratio 0.141 Total Fees $2520.92 Estimated Strategy Capacity $15000000.00 Lowest Capacity Asset ATH S9AP9FK1TBHH Portfolio Turnover 2.51% |
# https://quantpedia.com/strategies/lottery-effect-in-stocks/
#
# The investment universe contains all stocks on NYSE, AMEX, and NASDAQ. Stocks are then sorted into deciles based on maximum daily returns during
# the past month. The investor goes long on stocks with the lowest maximum daily returns in the previous month and goes short on stocks with the
# highest maximum daily returns. Portfolios are value-weighted and rebalanced monthly.
#
# QC implementation changes:
# - Universe consists of 500 most liquid stocks traded on NYSE, AMEX, or NASDAQ.
import numpy as np
from AlgorithmImports import *
class LotteryEffectinStocks(QCAlgorithm):
def Initialize(self):
# self.SetStartDate(2000, 1, 1)
self.SetStartDate(2016, 1, 1)
self.SetCash(100000)
self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.coarse_count = 500
# Daily close data.
self.data = {}
self.period = 21
self.weight = {}
self.selection_flag = False
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(10)
def CoarseSelectionFunction(self, coarse):
# Update the rolling window every day.
for stock in coarse:
symbol = stock.Symbol
# Store monthly price.
if symbol in self.data:
self.data[symbol].update(stock.AdjustedPrice)
if not self.selection_flag:
return Universe.Unchanged
# selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']
selected = [x.Symbol
for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'],
key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]]
# Warmup price rolling windows.
for symbol in selected:
if symbol in self.data:
continue
self.data[symbol] = SymbolData(self.period)
history = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes = history.loc[symbol].close
for time, close in closes.iteritems():
self.data[symbol].update(close)
return [x for x in selected if self.data[x].is_ready()]
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))]
max_daily_performance = { x : self.data[x.Symbol].max_daily_performance() for x in fine }
long = []
short = []
if len(max_daily_performance) >= 10:
# Maximum return sorting.
sorted_by_max_ret = sorted(max_daily_performance.items(), key = lambda x:x[1], reverse = True)
decile = int(len(sorted_by_max_ret) / 10)
long = [x[0] for x in sorted_by_max_ret[-decile:]]
short = [x[0] for x in sorted_by_max_ret[:decile]]
# Market cap weighting.
total_market_cap_long = sum([x.MarketCap for x in long])
for stock in long:
self.weight[stock.Symbol] = stock.MarketCap / total_market_cap_long
total_market_cap_short = sum([x.MarketCap for x in short])
for stock in short:
self.weight[stock.Symbol] = -stock.MarketCap / total_market_cap_long
return [x[0] for x in self.weight.items()]
def OnData(self, data):
if not self.selection_flag:
return
self.selection_flag = False
# Trade execution.
stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
if symbol not in self.weight:
self.Liquidate(symbol)
for symbol, w in self.weight.items():
self.SetHoldings(symbol, w)
self.weight.clear()
def Selection(self):
self.selection_flag = True
class SymbolData():
def __init__(self, period) -> None:
self.prices = RollingWindow[float](period)
def update(self, value) -> None:
self.prices.Add(value)
def is_ready(self) -> bool:
return self.prices.IsReady
def max_daily_performance(self) -> float:
closes = np.array([x for x in self.prices])
daily_returns = (closes[:-1] - closes[1:]) / closes[1:]
return max(daily_returns)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))# https://quantpedia.com/strategies/lottery-effect-in-stocks/
#
# The investment universe contains all stocks on NYSE, AMEX, and NASDAQ. Stocks are then sorted into deciles based on maximum daily returns during
# the past month. The investor goes long on stocks with the lowest maximum daily returns in the previous month and goes short on stocks with the
# highest maximum daily returns. Portfolios are value-weighted and rebalanced monthly.
#
# QC implementation changes:
# - Universe consists of 500 most liquid stocks traded on NYSE, AMEX, or NASDAQ.
from AlgorithmImports import *
# from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel
# from Risk import MaximumDrawdownPercentPerSecurity
import numpy as np
from dateutil.relativedelta import relativedelta
from math import ceil
from itertools import chain
class LotteryEffectinStocks(QCAlgorithm):
def Initialize(self):
# self.SetStartDate(2000, 1, 1)
# self.SetStartDate(2011, 1, 1)
self.SetStartDate(2018, 1, 1)
# self.SetEndDate(2016, 7, 1)
self.SetEndDate(2024, 9, 30)
# self.SetCash(100000)
self.SetCash(540000)
self.saltare_allocation = 0.25
self.max_short_size = 0.05
# self.copycat_cash_weight = 0
self.copycat_cash_weight = 0.5
# self.copycat_cash_weight = 1
# self.lottery_cash_weight = 0
self.lottery_cash_weight = 0.5
# self.lottery_cash_weight = 1
# self.symbol = self.AddEquity('SPY', Resolution.Daily).Symbol
self.symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
############################################
# Crisis Alpha Portfolio
self.symbols = ['TLT', 'IEF', 'IEI', 'SHY']
self.treasuries_1year = 'BIL'
crisis_tickers = ['TLT', 'IEF', 'IEI', 'SHY', 'BIL']
self.crisis_symbols = [ Symbol.Create(crisis_ticker, SecurityType.Equity, Market.USA) for crisis_ticker in crisis_tickers]
# period = 10
# self.SetWarmUp(period, Resolution.Daily)
copycat_period = 300 # For Minute Resolution
self.SetWarmUp(copycat_period)
# Monthly etf price.
self.copycat_data = {}
for symbol in self.symbols + [self.treasuries_1year]:
data = self.AddEquity(symbol, Resolution.Minute)
self.copycat_data[symbol] = CopycatSymbolData(symbol, copycat_period)
self.last_month = -1
############################################
self.copycat_weight = {}
self.investors_preferences = {}
# self.symbol = self.AddEquity('SPY', Resolution.Minute).Symbol
# # Use to Plot Benchmark
# # self.AddEquity("SPY", Resolution.Daily)
# self.benchmarkTicker = 'SPY'
# self.SetBenchmark(self.benchmarkTicker)
# # self.initBenchmarkPrice = None
# # Use if Plotting Long or Short Position of Benchmark
# self.initBenchmarkPrice = 0
# # self.benchmarkExposure = -0.5
# # self.benchmarkExposure = -1
# self.benchmarkExposure = 1
self.hedge_symbol = self.AddEquity('VIXM', Resolution.Minute).Symbol
self.hedge_weight = 0
self.hedge_two_symbol = self.AddEquity('GDX', Resolution.Minute).Symbol
self.hedge_two_weight = 0
self.months_lag = 2 # Lag for getting investors preferences report
# csv_string_file = self.Download('https://www.dropbox.com/s/1lfjrmly07lf0u3/full_investors_preferences%20copy%2015.csv?dl=1')
# csv_string_file = self.Download('https://www.dropbox.com/s/l7qxq9hiivsonag/full_investors_preferences%20copy%2016.csv?dl=1')
# csv_string_file = self.Download('https://www.dropbox.com/s/8qkm5ywnm8vs837/full_investors_preferences_16_copy.csv?dl=1')
csv_string_file = self.Download('https://www.dropbox.com/s/pgk4patc3zwajgp/full_investors_preferences_17.csv?dl=1')
lines = csv_string_file.split('\n') # This splittig works with the MarketWatch Scrape
dates = []
# Skip csv header in loop
for line in lines[1:]:
line_split = line.split(';')
date = datetime.strptime(line_split[0], "%d.%m.%Y").date()
self.investors_preferences[date] = {}
for ticker in line_split[1:]:
if ticker not in self.investors_preferences[date]:
self.investors_preferences[date][ticker] = 0
self.investors_preferences[date][ticker] += 1
self.month_counter = 0
self.copycat_selection_flag = False
self.copycat_quarterly_selection_flag = False
# self.copycat_selection_flag = True
self.copycat_crisis_flag = False
# self.copycat_data_selection_flag = True
# Number of stocks in Coarse Universe
self.NumberOfSymbolsCoarse = 500
# Number of sorted stocks in the fine selection subset using the valuation ratio, EV to EBITDA (EV/EBITDA)
self.NumberOfSymbolsFine = 20
# Final number of stocks in security list, after sorted by the valuation ratio, Return on Assets (ROA)
self.NumberOfSymbolsInPortfolio = 10
self.lastMonth = -1
self.dollarVolumeBySymbol = {}
############################################
self.coarse_count = 500
# Daily close data.
self.data = {}
self.period = 21
self.weight = {}
self.selection_flag = False
# self.UniverseSettings.Resolution = Resolution.Daily
self.UniverseSettings.Resolution = Resolution.Minute
# self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
self.universeCopycat = self.AddUniverse(self.CopycatCoarseSelectionFunction, self.CopycatSelectFine)
self.universeLottery = self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)
# Only needed if storing crisis symbols monthly prices in OnData
# self.universeCrisis = self.AddUniverse(self.CoarseCrisisFunction, self.FineCrisisFunction)
# Copycat Portfolio
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol,2), self.CopycatSelection)
# self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol,3), self.CopycatSelection)
# Crisis Alpha Portfolio
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol), self.CopycatRebalance)
# self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.AfterMarketOpen(self.symbol,1), self.CopycatRebalance)
# Lottery Portfolio
self.Schedule.On(self.DateRules.MonthEnd(self.symbol), self.TimeRules.BeforeMarketClose(self.symbol), self.Selection)
def OnSecuritiesChanged(self, changes):
for security in changes.AddedSecurities:
security.SetFeeModel(CustomFeeModel())
security.SetLeverage(10)
def CopycatCoarseSelectionFunction(self, coarse):
if not self.copycat_selection_flag and not self.copycat_quarterly_selection_flag:
return Universe.Unchanged
self.Log(str(self.Time) + "CoarseSelectionFunction")
# # Used to filter for the most common holdings only
# selected_report = {k: v for k, v in selected_report.items() if int(v) > 4} # My Addition
# # selected_report = {k: v for k, v in selected_report.items() if int(v) < 2} # My Addition
# Select universe based on report
top = sorted([x for x in coarse if x.HasFundamentalData],
key=lambda x: x.DollarVolume, reverse=True)[:self.NumberOfSymbolsCoarse]
self.dollarVolumeBySymbol = { i.Symbol: i.DollarVolume for i in top }
# self.copycat_quarterly_selection_flag = False
return list(self.dollarVolumeBySymbol.keys())
def CopycatSelectFine(self, fine):
selected_report = None
min_date = self.Time.date() - relativedelta(months=self.months_lag+1) # quarterly data
max_date = self.Time.date()
self.Log(str(self.Time) + "SelectFine")
for date in self.investors_preferences:
# Get latest report
if date >= min_date and date <= max_date:
selected_report = self.investors_preferences[date]
# Report might not be selected, because there are no data for that date
if selected_report is None:
return Universe.Unchanged
# Used to filter for the most common holdings only
selected_report = {k: v for k, v in selected_report.items() if int(v) > 20} # My Addition
filteredFine = [x for x in fine if x.Symbol.Value in selected_report
and x.CompanyReference.CountryId == "USA"
and (x.CompanyReference.PrimaryExchangeID == "NYS" or x.CompanyReference.PrimaryExchangeID == "NAS")
and (self.Time - x.SecurityReference.IPODate).days > 180
and x.EarningReports.BasicAverageShares.ThreeMonths * x.EarningReports.BasicEPS.TwelveMonths * x.ValuationRatios.PERatio > 5e8]
count = len(filteredFine)
if count == 0: return []
myDict = dict()
percent = self.NumberOfSymbolsFine / count
# select stocks with top dollar volume in every single sector
for key in ["N", "M", "U", "T", "B", "I"]:
value = [x for x in filteredFine if x.CompanyReference.IndustryTemplateCode == key]
value = sorted(value, key=lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True)
myDict[key] = value[:ceil(len(value) * percent)]
# myDict[key] = value[:ceil(len(value) * 1)]
# stocks in QC500 universe
topFine = chain.from_iterable(myDict.values())
# Magic Formula:
## Rank stocks by Enterprise Value to EBITDA (EV/EBITDA)
## Rank subset of previously ranked stocks (EV/EBITDA), using the valuation ratio Return on Assets (ROA)
# sort stocks in the security universe of QC500 based on Enterprise Value to EBITDA valuation ratio
sortedByEVToEBITDA = sorted(topFine, key=lambda x: x.ValuationRatios.EVToEBITDA , reverse=True)
# sortedByEVToEBITDA = sorted(filteredFine, key=lambda x: x.ValuationRatios.EVToEBITDA , reverse=True)
# sort subset of stocks that have been sorted by Enterprise Value to EBITDA, based on the valuation ratio Return on Assets (ROA)
# sortedByROA = sorted(sortedByEVToEBITDA[:self.NumberOfSymbolsFine], key=lambda x: x.ValuationRatios.ForwardROA, reverse=False)
# If using it must match fine_selected variable
sortedByROA = sorted(sortedByEVToEBITDA[:self.NumberOfSymbolsInPortfolio], key=lambda x: x.ValuationRatios.ForwardROA, reverse=False)
selected = [x.Symbol for x in sortedByROA]
symbol_selected = [x.Symbol.Value for x in sortedByROA]
# self.Debug(str(symbol_selected))
# self.Log(str(symbol_selected))
# new_selected_report = dict(((key, selected_report[key]) for key in symbol_selected))
new_selected_report = {}
for k in set(selected_report).intersection(symbol_selected):
new_selected_report[k]= selected_report[k]
# self.Debug(str(new_selected_report))
# self.Log(str(new_selected_report))
# Calculate total preferences votes for selected report
# total_preferences_votes = sum([x[1] for x in selected_report.items()])
total_preferences_votes = sum([x[1] for x in new_selected_report.items()])
# self.Debug(str(total_preferences_votes))
# Calculate weight for each stock in selected universe
for symbol in selected:
# for symbol in coarse_selected:
# weight = total stock preferences votes / total investor votes in selected report
# self.copycat_weight[symbol] = selected_report[symbol.Value] / total_preferences_votes
# self.copycat_weight[symbol] = new_selected_report[symbol.Value] / total_preferences_votes
self.copycat_weight[symbol] = 1 / len(selected)
# self.hedge_weight = (1 - sum([x[1] for x in self.copycat_weight.items()])) / 2
self.hedge_weight = 1.1 - sum([x[1] for x in self.copycat_weight.items()])
self.hedge_two_weight = (1 - sum([x[1] for x in self.copycat_weight.items()])) / 2
# self.hedge_two_weight = 1.1 - sum([x[1] for x in self.copycat_weight.items()])
# retrieve list of securites in portfolio
# fine_selected = [f.Symbol for f in sortedByROA[:self.NumberOfSymbolsFine]]
# If using it must match sortedByROA variable
fine_selected = [f.Symbol for f in sortedByROA[:self.NumberOfSymbolsInPortfolio]]
return fine_selected
def CoarseSelectionFunction(self, coarse):
# Update the rolling window every day.
for stock in coarse:
symbol = stock.Symbol
# Store monthly price.
if symbol in self.data:
self.data[symbol].update(stock.AdjustedPrice)
# IMPORTANT: Update Crisis Symbols here instead of in OnData to avoid a race condition
if symbol.Value in self.symbols + [self.treasuries_1year]:
# self.data[symbol].update(stock.AdjustedPrice)
self.copycat_data[symbol.Value].update(stock.AdjustedPrice)
# self.Debug(f"{str(self.Time)} Crisis Symbol: {symbol.Value} Price: {stock.AdjustedPrice}")
# if not self.selection_flag:
# return Universe.Unchanged
if self.selection_flag:
# selected = [x.Symbol for x in coarse if x.HasFundamentalData and x.Market == 'usa']
selected = [x.Symbol
for x in sorted([x for x in coarse if x.HasFundamentalData and x.Market == 'usa'],
key = lambda x: x.DollarVolume, reverse = True)[:self.coarse_count]]
# Warmup price rolling windows.
for symbol in selected:
if symbol in self.data:
continue
self.data[symbol] = SymbolData(self.period)
history = self.History(symbol, self.period, Resolution.Daily)
if history.empty:
self.Log(f"Not enough data for {symbol} yet.")
continue
closes = history.loc[symbol].close
# for time, close in closes.iteritems():
for time, close in closes.items():
self.data[symbol].update(close)
return [x for x in selected if self.data[x].is_ready()]
else:
return self.crisis_symbols
# return Universe.Unchanged
def FineSelectionFunction(self, fine):
fine = [x for x in fine if x.MarketCap != 0 and ((x.SecurityReference.ExchangeId == "NYS") or (x.SecurityReference.ExchangeId == "NAS") or (x.SecurityReference.ExchangeId == "ASE"))]
max_daily_performance = { x : self.data[x.Symbol].max_daily_performance() for x in fine }
long = []
short = []
if len(max_daily_performance) >= 10:
# Maximum return sorting.
sorted_by_max_ret = sorted(max_daily_performance.items(), key = lambda x:x[1], reverse = True)
decile = int(len(sorted_by_max_ret) / 10)
long = [x[0] for x in sorted_by_max_ret[-decile:]]
short = [x[0] for x in sorted_by_max_ret[:decile]]
# Market cap weighting.
total_market_cap_long = sum([x.MarketCap for x in long])
for stock in long:
self.weight[stock.Symbol] = stock.MarketCap / total_market_cap_long
total_market_cap_short = sum([x.MarketCap for x in short])
for stock in short:
# self.weight[stock.Symbol] = -stock.MarketCap / total_market_cap_long
# self.weight[stock.Symbol] = max(-0.285, -stock.MarketCap / total_market_cap_long)
self.weight[stock.Symbol] = max(-(self.max_short_size/(self.saltare_allocation * self.lottery_cash_weight)), -stock.MarketCap / total_market_cap_long)
return [x[0] for x in self.weight.items()]
# def CoarseCrisisFunction(self, coarse):
# # Only needed if storing crisis symbols monthly prices in OnData
# return self.crisis_symbols
# def FineCrisisFunction(self, fine):
# # Only needed if storing crisis symbols monthly prices in OnData
# return self.crisis_symbols
def OnData(self, data):
# if not self.selection_flag:
# return
# self.selection_flag = False
current_time = self.Time # For Minute Resolution
if (current_time.hour == 9 and current_time.minute == 35): # For Minute Resolution
if self.selection_flag:
# Trade execution.
already_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in already_invested:
# if symbol not in self.weight:
if symbol not in self.weight and symbol.Value not in [x.Value for x in self.universeCopycat.Members.Keys]:
# if symbol not in self.weight and symbol not in self.copycat_weight:
if symbol != self.treasuries_1year:
# self.Liquidate(symbol)
self.SetHoldings(symbol, 0, False, "1Liquidated")
if symbol not in self.weight and self.Portfolio[symbol].IsShort:
# This prevnts the Shorts from growing too large over time
# self.Liquidate(symbol)
self.SetHoldings(symbol, 0, False, "2Liquidated")
for symbol, w in self.weight.items():
# self.SetHoldings(symbol, w)
# if symbol not in stocks_invested:
if symbol.Value not in [x.Value for x in self.universeCopycat.Members.Keys]:
# if symbol not in self.copycat_weight:
# self.SetHoldings(symbol, w)
quantity = self.CalculateOrderQuantity(symbol, self.lottery_cash_weight * w)
# quantity = self.CalculateOrderQuantity(symbol, 0 * w)
if quantity:
self.MarketOrder(symbol, quantity)
# continue
self.weight.clear()
self.selection_flag = False
############################################
# Crisis Alpha Portfolio
# if self.last_month == self.Time.month: return
# if self.last_month != self.Time.month:
# self.last_month = self.Time.month
if self.last_month == self.Time.day: return
if self.last_month != self.Time.day: # For Minute Resolution
self.last_month = self.Time.day
# self.Log(str(self.Time.year))
# self.Debug(str(self.Time.year))
# Store monthly prices.
# for symbol in self.symbols + [self.treasuries_1year]:
# symbol_obj = self.Symbol(symbol)
# if symbol_obj in data.Keys:
# if data[symbol_obj]:
# price = data[symbol_obj].Value
# if price != 0:
# self.copycat_data[symbol].update(price)
# # self.Debug(f"{str(self.Time)} Crisis Symbol: {symbol} Price: {price}")
############################################
if (current_time.hour == 9 and current_time.minute == 31): # For Minute Resolution
# if (current_time.hour == 9 and current_time.minute == 32): # For Minute Resolution
# self.Log(f"Copycat universe includes: {sorted([x.Value for x in self.universeCopycat.Members.Keys])}")
# self.Log(f"Copycat universe size: {len([x.Value for x in self.universeCopycat.Members.Keys])}")
# self.Log(f"Lottery universe includes: {[x.Value for x in self.universeLottery.Members.Keys]}")
# self.Log(f"Lottery universe size: {len([x.Value for x in self.universeLottery.Members.Keys])}")
############################################
# This is a Crisis Flag is Override that will nuke all Long positions
# if self.Time.year == 2022 or self.Time.year == 2023:
# self.copycat_crisis_flag = True
# self.Log(str(self.Time) + "Crisis Flag is Override to On")
if self.Time.year == 2022:
self.copycat_crisis_flag = True
crisis_months = [1,2,3,4,5,6,8,9,10,11]
if self.Time.year == 2023:
if self.Time.month in crisis_months:
self.copycat_crisis_flag = True
############################################
if self.copycat_crisis_flag:
self.Log(str(self.Time) + "Crisis Flag is True")
current_invest = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in current_invest:
## This will nuke all positions in a Crisis
## if symbol not in self.weight and symbol not in [symbol.symbol.Value for symbol in self.managed_symbols]:
# if symbol != self.treasuries_1year:
if symbol != self.treasuries_1year and self.Portfolio[symbol].IsLong:
# # self.SetHoldings(symbol, 0)
self.SetHoldings(symbol, 0, False, "3Liquidated")
# if symbol.Value not in [x.Value for x in self.universeLottery.Members.Keys]:
# This will leave some Copycat universe positions that are also part of Lottery universe
# So it does not nuke all positions
# if symbol != self.treasuries_1year:
# self.SetHoldings(symbol, 0, False, "3Liquidated")
# else:
# if symbol.Value in [x.Value for x in self.universeLottery.Members.Keys]:
# if symbol.Value not in [x.Value for x in self.universeCopycat.Members.Keys]:
# if symbol != self.treasuries_1year and self.Portfolio[symbol].IsLong:
# self.SetHoldings(symbol, 0, False, "Reduced")
if self.treasuries_1year not in current_invest:
# self.SetHoldings(self.treasuries_1year, 1)
self.SetHoldings(self.treasuries_1year, self.copycat_cash_weight)
self.copycat_crisis_flag = False # This gets 307%
self.copycat_selection_flag = False
else:
if not self.copycat_selection_flag and not self.copycat_quarterly_selection_flag:
return
self.copycat_selection_flag = False
self.copycat_quarterly_selection_flag = False
# if self.Time.year == 2021 or self.Time.year == 2022 or self.Time.year == 2023:
# self.copycat_crisis_flag = True
# self.Log(str(self.Time) + "Crisis Flag is Override to On")
# Trade Execution
stocks_invested = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in stocks_invested:
if symbol not in self.copycat_weight:
## This will nuke all positions not in self.copycat_weight
if str(symbol.Value) == str(self.hedge_symbol) or str(symbol.Value) == str(self.hedge_two_symbol):
## This will leave some Copycat universe positions that are also part of Lottery universe
## So it does not nuke all positions
# if symbol.Value in [x.Value for x in self.universeLottery.Members.Keys] or str(symbol.Value) == str(self.hedge_symbol) or str(symbol.Value) == str(self.hedge_two_symbol):
continue
elif symbol.Value in [x.Value for x in self.universeLottery.Members.Keys] and self.Portfolio[symbol].IsShort:
continue
elif symbol.Value in [x.Value for x in self.universeLottery.Members.Keys] and self.Portfolio[symbol].IsLong:
# self.SetHoldings(symbol, 0.01, False, "2Reduced")
self.SetHoldings(symbol, 0.005, False, "2Reduced")
else:
self.SetHoldings(symbol, 0, False, "4Liquidated")
# if self.copycat_crisis_flag:
# if symbol.Value == self.treasuries_1year:
# continue
# else:
# self.SetHoldings(symbol, 0, False, "4Liquidated")
for symbol, w in self.copycat_weight.items():
quantity = self.CalculateOrderQuantity(symbol, self.copycat_cash_weight * w)
if quantity:
self.MarketOrder(symbol, quantity)
self.copycat_weight.clear()
def CopycatRebalance(self):
# self.Liquidate()
self.Log(str(self.Time) + "Rebalancing")
# Etfs with positive momentum. - symbol -> (performance, volatility)
positive_mom = {x : (self.copycat_data[x].performance(), self.copycat_data[x].volatility()) for x in self.symbols if self.copycat_data[x].is_ready() and self.copycat_data[x].performance() > 0}
# self.Log(str(self.Time) + "Is BIL Ready?")
if not self.copycat_data[self.treasuries_1year].is_ready(): return
# self.Log(str(self.Time) + "YES! BIL is Ready!")
if len(positive_mom) > 0:
self.Log(str(self.Time) + "Positive IS mom > 0!")
if self.Time.month % 3 == 2:
self.copycat_crisis_flag = False
self.Log(str(self.Time) + "This is Not the Month!")
else:
self.Log(str(self.Time) + "BIL Invested Check")
current_invest = [x.Key for x in self.Portfolio if x.Value.Invested]
for symbol in current_invest:
if symbol == self.treasuries_1year:
self.copycat_selection_flag = True
self.copycat_crisis_flag = False
self.Log(str(self.Time) + "BIL Already Invested!")
else:
self.copycat_crisis_flag = True
self.Log(str(self.Time) + "Crisis Flag is On")
# if self.Time.year == 2021 or self.Time.year == 2022 or self.Time.year == 2023:
# self.copycat_crisis_flag = True
# self.Log(str(self.Time) + "Crisis Flag is Override to On")
def CopycatSelection(self):
self.Log(str(self.Time) + "Selection")
if self.Time.month % 3 == 2:
# if self.Time.month % 3 == 0: # For Minute Resolution
# self.copycat_selection_flag = True
self.copycat_quarterly_selection_flag = True
self.Log(str(self.Time) + "Selection Flag is True")
# else:
# self.copycat_quarterly_selection_flag = False
def Selection(self):
self.selection_flag = True
class CopycatSymbolData():
def __init__(self, symbol, period):
self.Symbol = symbol
self.Price = RollingWindow[float](period)
def update(self, value):
self.Price.Add(value)
def is_ready(self) -> bool:
return self.Price.IsReady
def performance(self, values_to_skip = 0) -> float:
closes = [x for x in self.Price][values_to_skip:]
return (closes[0] / closes[-1] - 1)
def volatility(self):
prices = np.array([x for x in self.Price])
daily_returns = prices[:-1] / prices[1:] - 1
return np.std(daily_returns)
class SymbolData():
def __init__(self, period) -> None:
self.prices = RollingWindow[float](period)
def update(self, value) -> None:
self.prices.Add(value)
def is_ready(self) -> bool:
return self.prices.IsReady
def max_daily_performance(self) -> float:
closes = np.array([x for x in self.prices])
daily_returns = (closes[:-1] - closes[1:]) / closes[1:]
return max(daily_returns)
# Custom fee model.
class CustomFeeModel(FeeModel):
def GetOrderFee(self, parameters):
fee = parameters.Security.Price * parameters.Order.AbsoluteQuantity * 0.00005
return OrderFee(CashAmount(fee, "USD"))