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