Overall Statistics |
Total Orders 811 Average Win 4.19% Average Loss -5.73% Compounding Annual Return 7.368% Drawdown 61.400% Expectancy 0.112 Start Equity 10000 End Equity 53846.09 Net Profit 438.461% Sharpe Ratio 0.25 Sortino Ratio 0.148 Probabilistic Sharpe Ratio 0.004% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 0.73 Alpha 0.03 Beta 0.76 Annual Standard Deviation 0.263 Annual Variance 0.069 Information Ratio 0.079 Tracking Error 0.237 Treynor Ratio 0.087 Total Fees $10728.49 Estimated Strategy Capacity $11000000.00 Lowest Capacity Asset TQQQ UK280CGTCB51 Portfolio Turnover 9.36% |
# region imports from AlgorithmImports import * # endregion # Your New Python File class PortfolioManager(): def __init__(self, algo): self.algo = algo def HasHoldings(self, symbol): if self.algo.Securities[symbol].Type == SecurityType.Crypto: ## TODO: Explore a better way to do this. Not clear how base_currency.amount should be used min_lot_size = self.algo.securities[symbol].symbol_properties.lot_size asset = self.algo.securities[symbol] base_currency = asset.base_currency # quantity = min(asset.holdings.quantity, base_currency.amount) quantity = abs(asset.holdings.quantity) # abs(self.securities[self.symbol].symbol_properties.lot_size - self.securities[self.symbol].holdings.quantity) > 0.000000001 return abs(quantity) >= self.algo.securities[symbol].symbol_properties.minimum_order_size # return abs(quantity - min_lot_size) > min_lot_size else: return self.algo.Portfolio[symbol].Invested
# region imports from AlgorithmImports import * # endregion # Your New Python File class TradeUtils(): def __init__(self, algo): self.algo = algo # Convenience method to liquidate with a message def LiquidateWithMsg(self, symbol, exitReason): pnl = round(100 * self.algo.Portfolio[symbol].UnrealizedProfitPercent,2) biasText = 'Long' if (self.algo.Portfolio[symbol].IsLong) else 'Short' winlossText = 'win' if pnl > 0 else 'loss' orderNote = f"[{pnl}% {winlossText}] {exitReason} | Exiting {biasText} position" # If Crypto, call LiquidateMarketOrder if self.algo.Securities[symbol].Type == SecurityType.Crypto: self.LiquidateMarketOrder(symbol=symbol, tag=orderNote) else: self.algo.liquidate(symbol, tag=orderNote) ## Liquidate via market order. Necessary for crypto def LiquidateMarketOrder(self, symbol, tag): crypto = self.algo.securities[symbol] base_currency = crypto.base_currency # Avoid negative amount after liquidate quantity = min(crypto.holdings.quantity, base_currency.amount) # Round down to observe the lot size lot_size = crypto.symbol_properties.lot_size; quantity = (round(quantity / lot_size) - 1) * lot_size if self.is_valid_order_size(crypto, quantity): # self.algo.debug(f"------------ [START] Market Order: liquidation start") self.algo.debug(f" Liquidating: {quantity} units of {symbol.Value}") self.algo.market_order(symbol, -quantity, tag=tag) self.algo.debug(f"Market Order liquidation was Successful") self.algo.debug(f" Leftover: {crypto.holdings.quantity} units of {symbol.Value}") self.algo.debug(f"------------ [END] Market Order liquidation") if( abs(crypto.holdings.quantity) > lot_size): self.LiquidateMarketOrder(symbol, tag="reomving trailing coins") else: self.algo.debug(f"ERROR ERRROR ---- ") self.algo.debug(f"ERROR ERRROR Invalid order size: {quantity}") # Brokerages have different order size rules # Binance considers the minimum volume (price x quantity): def is_valid_order_size(self, crypto, quantity): return abs(crypto.price * quantity) > crypto.symbol_properties.minimum_order_size def HasHoldings(self, symbol): if self.algo.Securities[symbol].Type == SecurityType.Crypto: ## TODO: Explore a better way to do this. Not clear how base_currency.amount should be used min_lot_size = self.algo.securities[symbol].symbol_properties.lot_size asset = self.algo.securities[symbol] base_currency = asset.base_currency # quantity = min(asset.holdings.quantity, base_currency.amount) quantity = abs(asset.holdings.quantity) # abs(self.securities[self.symbol].symbol_properties.lot_size - self.securities[self.symbol].holdings.quantity) > 0.000000001 return abs(quantity) >= self.algo.securities[symbol].symbol_properties.minimum_order_size # return abs(quantity - min_lot_size) > min_lot_size else: return self.algo.Portfolio[symbol].Invested
from AlgorithmImports import * from PortfolioManager import * from TradeUtils import * class IBSMeanReversionOnIndex(QCAlgorithm): """ Inspired by Quantitativo: https://www.quantitativo.com/p/robustness-of-the-211-sharpe-mean Additional reference: https://tradingstrategy.medium.com/the-internal-bar-strength-ibs-indicator-trading-strategies-rules-video-e638f135edb6 Entry rules: ----------------------- Compute the rolling mean of High minus Low over the last 25 days; Compute the IBS indicator: (Close - Low) / (High - Low); Compute a lower band as the rolling High over the last 10 days minus 2.5 x the rolling mean of High mins Low (first bullet); Go long whenever SPY closes under the lower band (3rd bullet) and IBS is lower than 0.3; Exit rules ----------------------- - Close the trade whenever the SPY close is higher than yesterday's high; - Close the trade whenever the price is lower than the 300-SMA. """ ## Initialize the algo ## ------------------------ def Initialize(self): self.FolioMgr = PortfolioManager(self) self.TradeMgr = TradeUtils(self) # Init backtest params, etc self.ticker = "TQQQ" # Ticker symbol to trade self.SetBenchmark("SPY") # Benchmark for reporting (buy and hold) self.SetStartDate(2000, 12, 1) # Backtest start date # self.SetEndDate(2023, 7, 11) # Backtest end date self.SetCash(10000) # Starting portfolio balance # Subscrbe to a minute data feed (minute bars) self.symbol = self.AddEquity(self.ticker, Resolution.Minute).symbol # Set up a rollingwindow to store consolidated daily bars self.dailyBars = RollingWindow[TradeBar](2) # Set up the daily bar consolidator self.dailyConsolidator = TradeBarConsolidator(timedelta(days=1)) self.dailyConsolidator.DataConsolidated += self.OnDailyBarFormed self.SubscriptionManager.AddConsolidator(self.symbol, self.dailyConsolidator) # 300 SMA self.sma_300 = self.sma(self.symbol, 300, Resolution.Daily) # Schedule a daily chron job to check for signals at the open self.Schedule.On(self.DateRules.EveryDay(), \ self.TimeRules.AfterMarketOpen(self.ticker, 5), self.CheckForSignals) def OnDailyBarFormed(self, val, dailyBar): self.dailyBars.add(dailyBar) def CheckForSignals(self): if not self.Portfolio.Invested: if self.EntrySignalFired(): self.SetHoldings(self.ticker, 1) else: if self.ExitSignalFired(): self.LiquidateWithMsg(self.symbol,f"IBS Signal Exit") # self.Liquidate(tag=f"IBS Signal Exit") ## Go long when: ## - SPy closes under lower band ## - IBS < 0.3 ## ------------------------------ def EntrySignalFired(self): if self.dailyBars.IsReady: # if SPY closes under the lower band (3rd bullet) # If IBS is lower than 0.3; # IBS= (Close-Low)/(High-Low) lastBar = self.dailyBars[0] if(lastBar.high != lastBar.low): ibsValue = (lastBar.close - lastBar.low) / (lastBar.high - lastBar.low) return (ibsValue < 0.2 ) return False ## Exit the trade when: # - SPY close is higher than yesterday's high; # - price is lower than the 300-SMA. ## ------------------------------------------------------------------------------- def ExitSignalFired(self): if self.dailyBars.IsReady: # if SPY closes under the lower band (3rd bullet) # If IBS is lower than 0.3; # IBS= (Close-Low)/(High-Low) lastBar = self.dailyBars[0] if(lastBar.high != lastBar.low): ibsValue = (lastBar.close - lastBar.low) / (lastBar.high - lastBar.low) return (ibsValue > 0.8 ) return False # if self.Portfolio.Invested: # if (self.dailyBars[0].close > self.dailyBars[1].high): # self.Liquidate(tag=f"Exit @ last close > prev high: {self.dailyBars[0].close} > {self.dailyBars[1].high}") # Convenience method to liquidate with PnL message def LiquidateWithMsg(self, symbol, exitReason): pnl = round(100 * self.Portfolio[symbol].UnrealizedProfitPercent,2) biasText = 'Long' if (self.Portfolio[symbol].IsLong) else 'Short' winlossText = 'win' if pnl > 0 else 'loss' orderNote = f"[{pnl}% {winlossText}] {exitReason} | Exiting {biasText} position" self.liquidate(symbol, tag=orderNote)