Overall Statistics |
Total Trades 1337 Average Win 0.66% Average Loss -0.94% Compounding Annual Return 97.928% Drawdown 51.000% Expectancy 0.125 Net Profit 99.337% Sharpe Ratio 1.502 Probabilistic Sharpe Ratio 52.639% Loss Rate 34% Win Rate 66% Profit-Loss Ratio 0.70 Alpha -0.019 Beta 0.463 Annual Standard Deviation 0.645 Annual Variance 0.415 Information Ratio -1.758 Tracking Error 0.663 Treynor Ratio 2.092 Total Fees $83017.67 Estimated Strategy Capacity $33000.00 Lowest Capacity Asset OXTUSD XJ |
# region imports from AlgorithmImports import * # endregion class CryptoMomentum(QCAlgorithm): def Initialize(self): self.SetStartDate(2020, 1, 1) self.SetEndDate(2021, 1, 1) self.SetCash(100000) self.SetBrokerageModel(BrokerageName.GDAX, AccountType.Cash) self.UniverseSettings.Resolution = Resolution.Hour self.Settings.FreePortfolioValuePercentage = 0.05 self.AddUniverse(CryptoCoarseFundamentalUniverse(Market.GDAX, self.UniverseSettings, self.universe_filter)) self.symbol_data_by_symbol = {} #symbol over which other crypto prices will be divided by; either BTC or ETH usually self.base_ticker = "BTCUSD" self.base_symbol = self.AddCrypto(self.base_ticker, Resolution.Hour).Symbol self.fast_ma = self.EMA(self.base_symbol, 24*15) self.slow_ma = self.EMA(self.base_symbol, 24*50) self.SetWarmUp(24*20, Resolution.Hour) self.SetBenchmark(self.base_symbol) def universe_filter(self, crypto_coarse: List[CryptoCoarseFundamental]) -> List[Symbol]: restricted_tickers = ['DAIUSD', 'DAIUSDC', 'USDC'] universe = [] #universe = [cf.Symbol for cf in crypto_coarse if "ETHUSD" == cf.Symbol.Value] for cf in crypto_coarse: if cf.Symbol.Value not in restricted_tickers and cf.Volume > 100000: if "USD" in cf.Symbol.Value and "USDC" not in cf.Symbol.Value: universe.append(cf.Symbol) return universe def OnSecuritiesChanged(self, changes: SecurityChanges) -> None: for security in changes.AddedSecurities: self.symbol_data_by_symbol[security.Symbol] = SymbolData(self, security.Symbol, self.base_symbol) # If you have a dynamic universe, track removed securities for security in changes.RemovedSecurities: self.Liquidate(security.Symbol) symbol_data = self.symbol_data_by_symbol.pop(security.Symbol, None) if symbol_data: symbol_data.dispose() def OnData(self, data: Slice): #daily trigger if data.Time.hour != 12: return buy_symbols = [] sell_symbols = [] sorted_symbol_data = sorted(self.symbol_data_by_symbol.values(),key=lambda x: x.rate_of_change, reverse=True) #top 5 momentum for symbol_data in sorted_symbol_data[:5]: symbol = symbol_data.symbol if self.base_ticker in symbol.Value: continue if symbol_data.sma_fast.Current.Value > symbol_data.sma_slow.Current.Value and symbol_data.sma_slow.IsReady: buy_symbols.append(symbol) #self.Plot("moving averages", symbol_data.sma_fast, symbol_data.sma_slow) for symbol in self.symbol_data_by_symbol.keys(): if symbol not in buy_symbols: self.Liquidate(symbol) if len(buy_symbols) > 0: ''' refine logic so minor buys/sells aren't triggered (i.e. within +/- few percentage points of target) ''' port_value = self.Portfolio.TotalPortfolioValue target_weight = (1/len(buy_symbols))*.95 target_value = target_weight * port_value if target_weight > 0.2: target_weight = 0.2 portfolio_targets = [] for symbol in buy_symbols: portfolio_targets.append(PortfolioTarget(symbol, target_weight)) self.SetHoldings(portfolio_targets) else: self.Liquidate() class SymbolData: def __init__(self, algorithm, symbol, base_symbol): self.algorithm = algorithm self.symbol = symbol self.base_symbol = base_symbol self.sma_fast = ExponentialMovingAverage(24*1) self.sma_slow = ExponentialMovingAverage(24*10) self.rate_of_change = 0 # Create a consolidator to update the indicator self.consolidator = TradeBarConsolidator(1) self.consolidator.DataConsolidated += self.OnDataConsolidated # Register the consolidator to update the indicator self.algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidator) def OnDataConsolidated(self, sender: object, consolidated_bar: TradeBar) -> None: base_close = self.algorithm.Securities[self.base_symbol].Close #calculate moving averages relative to the base symbol self.sma_fast.Update(consolidated_bar.EndTime, consolidated_bar.Close/base_close) self.sma_slow.Update(consolidated_bar.EndTime, consolidated_bar.Close/base_close) self.rate_of_change= self.sma_fast.Current.Value/self.sma_slow.Current.Value # If you have a dynamic universe, remove consolidators for the securities removed from the universe def dispose(self) -> None: self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.consolidator)