| Overall Statistics |
|
Total Trades 417 Average Win 4.98% Average Loss -2.65% Compounding Annual Return -10.980% Drawdown 88.000% Expectancy 0.154 Net Profit -37.222% Sharpe Ratio 0.233 Probabilistic Sharpe Ratio 1.040% Loss Rate 60% Win Rate 40% Profit-Loss Ratio 1.88 Alpha 0.288 Beta -0.257 Annual Standard Deviation 1.088 Annual Variance 1.183 Information Ratio 0.107 Tracking Error 1.108 Treynor Ratio -0.984 Total Fees $19143.23 Estimated Strategy Capacity $160000.00 Lowest Capacity Asset AAC VUE7AQ4QYD0L |
#region imports
from AlgorithmImports import *
#endregion
# https://quantpedia.com/Screener/Details/77
from QuantConnect.Data.UniverseSelection import *
from QuantConnect.Python import PythonData
from collections import deque
from datetime import datetime
import math
import numpy as np
import pandas as pd
import scipy as sp
from decimal import Decimal
class BetaFactorInStocks(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2018, 1, 1)
self.SetEndDate(2022, 1, 1)
self.SetCash(1000000)
self.UniverseSettings.Resolution = Resolution.Daily
self.AddUniverse(self.CoarseSelectionFunction)
self.AddEquity("SPY", Resolution.Daily)
# Add Wilshire 5000 Total Market Index data from Dropbox
self.price5000 = self.AddData(Fred, Fred.Wilshire.Price5000, Resolution.Daily).Symbol
# Setup a RollingWindow to hold market return
self.market_return = RollingWindow[float](252)
# Use a ROC indicator to convert market price index into return, and save it to the RollingWindow
self.roc = self.ROC(self.price5000, 1)
self.roc.Updated += lambda sender, updated: self.market_return.Add(updated.Value)
# Warm up
hist = self.History(self.price5000, 253, Resolution.Daily)
for point in hist.itertuples():
self.roc.Update(point.Index[1], point.value)
self.data = {}
self.monthly_rebalance = False
self.long = None
self.short = None
self.Schedule.On(self.DateRules.MonthStart("SPY"), self.TimeRules.AfterMarketOpen("SPY"), self.rebalance)
def CoarseSelectionFunction(self, coarse):
for c in coarse:
if c.Symbol not in self.data:
self.data[c.Symbol] = SymbolData(c.Symbol)
self.data[c.Symbol].Update(c.EndTime, c.AdjustedPrice)
if self.monthly_rebalance:
filtered_data = {symbol: data for symbol, data in self.data.items() if data.last_price > 5 and data.IsReady()}
if len(filtered_data) > 10:
# sort the dictionary in ascending order by beta value
sorted_beta = sorted(filtered_data, key = lambda x: filtered_data[x].beta(self.market_return))
self.long = sorted_beta[:5]
self.short = sorted_beta[-5:]
return self.long + self.short
else:
self.monthly_rebalance = False
return []
else:
return []
def rebalance(self):
self.monthly_rebalance = True
def OnData(self, data):
if not self.monthly_rebalance: return
# Liquidate symbols not in the universe anymore
for symbol in self.Portfolio.Keys:
if self.Portfolio[symbol].Invested and symbol not in self.long + self.short:
self.Liquidate(symbol)
if self.long is None or self.short is None: return
long_scale_factor = 0.5/sum(range(1,len(self.long)+1))
for rank, symbol in enumerate(self.long):
self.SetHoldings(symbol, (len(self.long)-rank+1)*long_scale_factor)
short_scale_factor = 0.5/sum(range(1,len(self.long)+1))
for rank, symbol in enumerate(self.short):
self.SetHoldings(symbol, -(rank+1)*short_scale_factor)
self.monthly_rebalance = False
self.long = None
self.short = None
class SymbolData:
def __init__(self, symbol):
self.Symbol = symbol
self.last_price = 0
self.returns = RollingWindow[float](252)
self.roc = RateOfChange(1)
self.roc.Updated += lambda sender, updated: self.returns.Add(updated.Value)
def Update(self, time, price):
if price != 0:
self.last_price = price
self.roc.Update(time, price)
def IsReady(self):
return self.roc.IsReady and self.returns.IsReady
def beta(self, market_ret):
asset_return = np.array(list(self.returns), dtype=np.float32)
market_return = np.array(list(market_ret), dtype=np.float32)
return np.cov(asset_return, market_return)[0][1]/np.var(market_return)