| Overall Statistics |
|
Total Trades 1742 Average Win 1.64% Average Loss -1.15% Compounding Annual Return 61.945% Drawdown 28.000% Expectancy 0.411 Net Profit 3974.795% Sharpe Ratio 1.634 Probabilistic Sharpe Ratio 88.491% Loss Rate 42% Win Rate 58% Profit-Loss Ratio 1.42 Alpha 0.381 Beta 0.677 Annual Standard Deviation 0.274 Annual Variance 0.075 Information Ratio 1.353 Tracking Error 0.258 Treynor Ratio 0.662 Total Fees $18931.94 Estimated Strategy Capacity $1100000.00 Lowest Capacity Asset USDU VMIMJSS4X2SL Portfolio Turnover 23.85% |
# region imports
from AlgorithmImports import *
from math import floor
# endregion
class RiskOnRiskOff(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2016, 1, 1)
self.SetCash(10000)
dd_period:int = 10
self.market:Symbol = self.AddEquity("SPY", Resolution.Daily).Symbol
self.spy_prices:RollingWindow = RollingWindow[float](dd_period)
self.dd_threshold:float = 0.05
# Add ETFs to universe
etfs:List[str] = ["SHY", "TECL", "TQQQ", "UPRO", "TMF", "USDU", "QID", "TBF", "IEI", "GLD", "TIP", "BSV"]
for etf in etfs:
data = self.AddEquity(etf, Resolution.Daily)
data.SetLeverage(10)
# Add ETFs for RSI calculation
self.vixm:Symbol = self.AddEquity("VIXM", Resolution.Daily).Symbol
self.vixm_rsi:RelativeStrengthIndex = self.RSI(self.vixm, 40, Resolution.Daily)
self.rsi_threshold:float = 0.69
rsi_etfs:List[str] = ["TECL", "TQQQ", "UPRO", "TMF", "QID", "TBF"]
self.rsi_symbols:Dict[str, RateOfChange] = {}
for etf in rsi_etfs:
self.rsi_symbols[etf] = self.RSI(etf, 10 if etf in ["TECL", "TQQQ", "UPRO", "TMF"] else 20, Resolution.Daily)
# Add ETFs for ROC calculation
self.roc_symbols:Dict[str, Tuple] = {}
for etf in ["BND", "BIL", "TLT"]:
self.AddEquity(etf, Resolution.Daily)
self.roc_symbols[etf] = (self.ROC(etf, 20, Resolution.Daily), self.ROC(etf, 60, Resolution.Daily))
self.SetWarmup(60, Resolution.Daily)
self.recent_day:int = -1
def OnData(self, data: Slice) -> None:
# if not (self.Time.hour == 16 and self.Time.minute == 0): return
# Store daily market prices
if self.market in data and data[self.market]:
self.spy_prices.Add(data[self.market].Close)
if self.IsWarmingUp: return
# Trading logic
if self.vixm_rsi.IsReady and \
self.spy_prices.IsReady and \
all(x.IsReady for x in self.rsi_symbols.values()) and \
all(x[0].IsReady for x in self.roc_symbols.values()) and \
all(x[1].IsReady for x in self.roc_symbols.values()):
if self.recent_day != self.Time.day:
self.recent_day = self.Time.day
if self.vixm_rsi.Current.Value / 100. > self.rsi_threshold:
should_rebalance:bool = self.liquidate(["SHY"])
if should_rebalance:
# quantity:float = floor(self.Portfolio.TotalPortfolioValue / data["SHY"].Value)
# self.MarketOnOpenOrder("SHY", quantity)
self.SetHoldings("SHY", 1)
else:
if self.roc_symbols["BND"][1].Current.Value > self.roc_symbols["BIL"][1].Current.Value:
rsi_values:Dict[str, float] = { x : self.rsi_symbols[x].Current.Value for x in ["TECL", "TQQQ", "UPRO", "TMF"] }
bottom_by_rsi:List[str] = sorted(rsi_values, key=rsi_values.get, reverse=True)[-3:]
should_rebalance:bool = self.liquidate(bottom_by_rsi)
if should_rebalance:
for etf in bottom_by_rsi:
# quantity:float = floor(self.Portfolio.TotalPortfolioValue / len(bottom_by_rsi) / data[etf].Value)
# self.MarketOnOpenOrder(etf, quantity)
self.SetHoldings(etf, 1. / len(bottom_by_rsi))
else:
if self.roc_symbols["TLT"][0].Current.Value < self.roc_symbols["BIL"][0].Current.Value:
rsi_values:Dict[str, float] = { x : self.rsi_symbols[x].Current.Value for x in ["QID", "TBF"] }
bottom_by_rsi:List[str] = sorted(rsi_values, key=rsi_values.get, reverse=True)[-1:]
should_rebalance:bool = self.liquidate(bottom_by_rsi + ["USDU"])
if should_rebalance:
# quantity:float = floor(self.Portfolio.TotalPortfolioValue / 2 / data["USDU"].Value)
# self.MarketOnOpenOrder("USDU", quantity)
# quantity:float = floor(self.Portfolio.TotalPortfolioValue / 2 / data[bottom_by_rsi[0]].Value)
# self.MarketOnOpenOrder(bottom_by_rsi[0], quantity)
self.SetHoldings("USDU", 0.5)
self.SetHoldings(bottom_by_rsi[0], 0.5)
else:
max_spy_drawdown:float = self.calculate_max_drawdown(np.array(list(self.spy_prices)[::-1]))
if max_spy_drawdown > -self.dd_threshold:
should_rebalance:bool = self.liquidate(["UPRO", "TMF"])
if should_rebalance:
# quantity:float = floor(self.Portfolio.TotalPortfolioValue * 0.55 / data["UPRO"].Value)
# self.MarketOnOpenOrder("UPRO", quantity)
# quantity:float = floor(self.Portfolio.TotalPortfolioValue * 0.45 / data["TMF"].Value)
# self.MarketOnOpenOrder("TMF", quantity)
self.SetHoldings("UPRO", 0.55)
self.SetHoldings("TMF", 0.45)
else:
tickers_to_hold:List[str] = ["IEI", "GLD", "TIP", "BSV"]
should_rebalance:bool = self.liquidate(tickers_to_hold)
if should_rebalance:
for etf in tickers_to_hold:
# quantity:float = floor(self.Portfolio.TotalPortfolioValue / len(tickers_to_hold) / data[etf].Value)
# self.MarketOnOpenOrder(etf, quantity)
self.SetHoldings(etf, 1 / len(tickers_to_hold))
def liquidate(self, tickers_to_hold:List[str]) -> bool:
should_rebalance:bool = False
invested:List[str] = [x.Key.Value for x in self.Portfolio if x.Value.Invested]
if len(invested) == 0:
should_rebalance = True
for ticker in invested:
if ticker not in tickers_to_hold:
symbol = self.Symbol(ticker)
# self.MarketOnOpenOrder(symbol, -self.Portfolio[symbol].Quantity)
self.Liquidate(ticker)
should_rebalance = True
return should_rebalance
def calculate_max_drawdown(self, prices:np.ndarray) -> float:
prices:pd.Series = pd.Series(prices)
roll_max:pd.Series = prices.cummax()
daily_dd:pd.Series = prices / roll_max - 1.0
max_daily_dd:float = daily_dd.min()
return max_daily_dd