| 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)