Overall Statistics |
Total Orders 0 Average Win 0% Average Loss 0% Compounding Annual Return 0% Drawdown 0% Expectancy 0 Start Equity 100000 End Equity 100000 Net Profit 0% Sharpe Ratio 0 Sortino Ratio 0 Probabilistic Sharpe Ratio 0% Loss Rate 0% Win Rate 0% Profit-Loss Ratio 0 Alpha 0 Beta 0 Annual Standard Deviation 0 Annual Variance 0 Information Ratio -0.439 Tracking Error 0.16 Treynor Ratio 0 Total Fees $0.00 Estimated Strategy Capacity $0 Lowest Capacity Asset Portfolio Turnover 0% |
# Imports from datetime import timedelta import pickle import traceback # QuantConnect specific imports from AlgorithmImports import * import QuantConnect as qc # Import from files # n/a """ AE Revision Notes (09/08/2023) - Updated to read the target portfolio allocation from the Object Store. (09/27/2023) - Allowing orders on the 1minute mark, since there is a 1min delay from reading update from the signal algo. (09/28/2023) - Skipping CheckTargetAllocation() if not during live trading hours. (12/06/2023) - """ # Global variables OS_KEY = "LiveAlgo" DEBUG = False ############################################################################### class CustomAlgorithm(QCAlgorithm): def Initialize(self): # Set starting date, cash and ending date of the backtest # self.SetEndDate(2017, 3, 31) self.SetCash(100000) self.SetTimeZone('US/Eastern') # AE 12/6/23 update if not self.LiveMode: self.SetSecurityInitializer( CustomSecurityInitializer( self.BrokerageModel, FuncSecuritySeeder(self.GetLastKnownPrices) ) ) # Add market symbol and create market condition instance self.bm = self.AddEquity("SPY", Resolution.Hour).Symbol self.bm_hours = self.Securities[self.bm].Exchange.Hours # Universe selection self.AddUniverse(self.CoarseSelectionFunction) self.UniverseSettings.Resolution = Resolution.Minute self.UniverseSettings.ExtendedMarketHours = False # not using this for client accounts self.UniverseSettings.MinimumTimeInUniverse = 63 self.symbol_objects = [] # Schedule function to check the target allocation if self.LiveMode: self.Schedule.On( self.DateRules.EveryDay(self.bm), self.TimeRules.Every(timedelta(minutes=1)), self.CheckTargetAllocation ) # Other settings self.Settings.FreePortfolioValuePercentage = 0.05 # Required variables self.target_allocation = {} #------------------------------------------------------------------------------- def CoarseSelectionFunction(self, coarse): stocks = list(filter(lambda x: x.HasFundamentalData, coarse)) sortedByDollarVolume = sorted( stocks, key=lambda x: x.DollarVolume, reverse=True ) symbols = [x.Symbol for x in sortedByDollarVolume[:50]] # Print universe details when live mode if self.LiveMode: self.MyLog(f"Coarse filter returned {len(symbols)} stocks.") return symbols #------------------------------------------------------------------------------- def CheckTargetAllocation(self): """Check Target Allocation.""" # AE added 9/28 # Skip until we are in live trading hours if not self.bm_hours.IsOpen(self.Time, extendedMarketHours=False): return # Get the desired target allocation from the Object Store if DEBUG: self.MyLog('CheckTargetAllocation') # Always clear the cache before getting the target allocation self.ObjectStore.Clear() if self.ObjectStore.ContainsKey(OS_KEY): deserialized = self.ObjectStore.ReadBytes(OS_KEY) target_allocation = pickle.loads(deserialized) if DEBUG: self.MyLog(f"Target allocation: {target_allocation}") # Log change in target allocation if target_allocation != self.target_allocation: self.MyLog(f"Change in target allocation: {target_allocation}") self.target_allocation = target_allocation # Update the portfolio based on the target allocation self.UpdatePortfolio(target_allocation) #------------------------------------------------------------------------------- def UpdatePortfolio(self, target_allocation): """Update the current portfolio based on the target allocation.""" # Get the current portfolio value nav = self.Portfolio.TotalPortfolioValue if DEBUG: self.MyLog(f'UpdatePortfolio(), current nav=${nav:.2f}') # Get list of tuples (symbol_objects, order shares) based type of order new_positions = [] decrease_size = [] increase_size = [] # Loop through the target allocation target_portfolio = {} for symbol_object_str, tup in target_allocation.items(): strategy = tup[0] target_weight = tup[1] dt = tup[2] if DEBUG: self.MyLog( f'UpdatePortfolio(), symbol_object_str={symbol_object_str}, ' f'strategy={strategy}, target weight={target_weight}, ' f'dt={dt}' ) # Get the actual QC symbol from the symbol string symbol_object = None for symbol in self.symbol_objects: # AE update 12/6/23 if (str(symbol) == symbol_object_str) or ((str(symbol.ID) == symbol_object_str)): symbol_object = symbol if DEBUG: self.MyLog( f'UpdatePortfolio(), {symbol_object_str} symbol ' 'object found' ) break # Check if the symbol object is not found if symbol_object is None: try: # Create the desired symbol object ticker = symbol_object_str.split(" ")[0] symbol = self.AddEquity(ticker) symbol_object = self.AddEquity( ticker, Resolution.Minute ).Symbol if DEBUG: self.MyLog( f'UpdatePortfolio(), {symbol_object_str} symbol ' f'object: {symbol_object}') except: self.MyLog( f"UpdatePortfolio(), error trying to get symbol " f"object for {symbol_object_str}" ) continue # Get the target number of shares price = self.Securities[symbol_object].Price if price != 0: target_value = nav*target_weight # How to account for the margin requirement? target_shares = int(target_value/price) current_shares = self.Portfolio[symbol_object].Quantity # Check for new position if current_shares == 0: tag = 'Initial entry' new_positions.append((symbol_object, target_shares, tag)) # Check for increasing the position size elif abs(target_shares) > abs(current_shares): tag = 'Rebalance increase' order_shares = target_shares-current_shares increase_size.append((symbol_object, order_shares, tag)) # Check for decreasing the position size elif abs(target_shares) < abs(current_shares): tag = 'Rebalance decrease' order_shares = target_shares-current_shares decrease_size.append((symbol_object, order_shares, tag)) # First handle positions that we are completely exiting positions = [ (symbol, self.Portfolio[symbol].Quantity) \ for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested ] for symbol, current_qty in positions: # AE update 12/6/23 if str(symbol) not in target_allocation.keys() and str(symbol.ID) not in target_allocation.keys(): # Liquidate the position self.MyLog( f"{symbol} not in target allocation, so liquidating the " f"open position of {current_qty} shares" ) self.Liquidate(symbol, tag='exit') # Skip if we are not on a new hour if self.Time.minute == 0 or (self.Time.minute == 59 and self.Time.second >= 50): pass # AE adding 9/27/23 elif self.Time.minute == 1: pass else: if DEBUG: self.MyLog( f'Skipping rebalance until the top of the hour: {self.Time}' ) return # Next handle orders where we are decreasing the overall position size # Only do this on the top of an hour, or if there are new positions for symbol, order_shares, tag in decrease_size: self.MyLog( f"{symbol} decreasing position size order for {order_shares} " "shares" ) self.MarketOrder(symbol, order_shares, tag=tag) # Next handle new position orders for symbol, order_shares, tag in new_positions: self.MyLog( f"{symbol} new position order for {order_shares} shares" ) self.MarketOrder(symbol, order_shares, tag=tag) # Last handle orders where we are increasing the overall position size for symbol, order_shares, tag in increase_size: self.MyLog( f"{symbol} increasing position size order for {order_shares} " "shares" ) self.MarketOrder(symbol, order_shares, tag=tag) #------------------------------------------------------------------------------- def MyLog(self, message): """Add algo time to log if live trading. Otherwise just log message.""" # Log all messages in live trading mode with local time added if self.LiveMode: self.Log(f'{self.Time}: {message}') else: self.Log(message) #------------------------------------------------------------------------------- def ResubmitOrder(self, order, msg): """Built-in event handler for orders.""" if type(order) == qc.Orders.MarketOrder \ or type(order) == qc.Orders.MarketOnOpenOrder: order_type = 'Market' elif type(order) == qc.Orders.LimitOrder: order_type = 'Limit' # Get the limit price limit_price = order.LimitPrice else: self.MyLog( f"Invalid Order, but not a market or limit order! Order type=" f"{type(order)}" ) return # Get the order message, symbol, and qty self.MyLog( f"Invalid {order_type} Order! error: {msg}" ) symbol = order.Symbol order_qty = int(order.Quantity) # Check for insufficient buying power if 'Insufficient buying power' in msg: # Get the initial margin and free margin initial_margin = float( msg.split("Initial Margin: ")[1].split(",")[0] ) free_margin = float( msg.split("Free Margin: ")[1].split(",")[0].strip('.') ) # Get the max allowed position size margin_per_share = abs(initial_margin/order_qty) max_shares = int(abs((0.95*free_margin/margin_per_share))) # Check for 'desired position your previous day...' error elif 'DESIRED POSITION YOUR PREVIOUS DAY EQUITY WITH LOAN VALUE' in msg: # Get the initial margin and previous day equity loan value initial_margin = float( msg.split("INITIAL MARGIN [")[1].split("USD")[0].replace(' ','') ) loan_value = float( msg.split("LOAN VALUE [")[1].split("USD")[0].replace(' ','') ) # Get the max allowed position size margin_per_share = abs(initial_margin/order_qty) max_shares = int(abs((0.95*loan_value/margin_per_share))) else: self.MyLog(f"Unrecognized error message: {msg}") self.MyLog(f"Will try to reduce order qty by 50%") # Try to cut order size in half max_shares = int(order_qty*0.5) # Get new qty if order_qty < 0: order_qty = -abs(max_shares) else: order_qty = abs(max_shares) if order_qty == 0: self.MyLog( f"Initial number of shares exceeds margin requirements! Reducing " f"order qty resulting in 0 share order, so it's ignored!" ) return # Otherwise valid new order qty self.MyLog( f"Initial number of shares exceeds margin requirements! Reducing " f"order qty to {order_qty}" ) # Resubmit an order with a reduced qty if order_type == 'Market': self.MarketOrder(symbol, order_qty, asynchronous=True) elif order_type == 'Limit': self.MyLog(f"Limit price={limit_price}") order = self.LimitOrder(symbol, order_qty, limit_price) # Add order to the list of limit orders # self.limit_orders.append(order) #------------------------------------------------------------------------------- def OnSecuritiesChanged(self, changes): """Event handler for changes to our universe.""" # Loop through securities added to the universe for security in changes.AddedSecurities: # Get the security symbol object symbol_object = security.Symbol # Add to our list of symbols if symbol_object not in self.symbol_objects: self.MyLog( f'OnSecuritiesChanged() adding symbol object to list: ' f'{symbol_object.Value}->{symbol_object.ID}' ) self.symbol_objects.append(symbol_object) # Loop through securities removed from the universe for security in changes.RemovedSecurities: # Get the security symbol object symbol_object = security.Symbol # Remove from our list of symbols if symbol_object in self.symbol_objects: self.symbol_objects.remove(symbol_object) #------------------------------------------------------------------------------- def OnOrderEvent(self, orderEvent): """Built-in event handler for orders.""" # Log message if self.LiveMode: self.MyLog( f"New order event: {orderEvent}, Status={orderEvent.Status}" ) # Catch invalid order if orderEvent.Status == OrderStatus.Invalid: try: # Resubmit a new order order = self.Transactions.GetOrderById(orderEvent.OrderId) # ticket = self.Transactions.GetOrderTicket(orderEvent.OrderId) # response = ticket.GetMostRecentOrderResponse() msg = orderEvent.get_Message() self.ResubmitOrder(order, msg) except: if self.LiveMode: self.MyLog( f'OnOrderEvent() exception: {traceback.format_exc()}' ) #------------------------------------------------------------------------------- # def OnEndOfAlgorithm(self): # """Built-in event handler for end of the backtest.""" # # Save the portfolio values to the object store so we can evaluate # # them in the Research environment # key = 'ZScore' # d = { # 'time': self.times, # 'value': self.navs, # 'volatility': self.market_volatilities, # 'direction': self.market_directions # } # serialized = pickle.dumps(d) # self.ObjectStore.SaveBytes(key, serialized) ############################################################################### class CustomSecurityInitializer(BrokerageModelSecurityInitializer): def __init__( self, brokerage_model: IBrokerageModel, security_seeder: ISecuritySeeder ) -> None: super().__init__(brokerage_model, security_seeder) def Initialize(self, security: Security) -> None: """ Define models to be used for securities as they are added to the algorithm's universe. """ # First, call the superclass definition # Sets the reality models of each security using the default models # of the brokerage model super().Initialize(security) # Define the buying power model to use for the security security.SetBuyingPowerModel(SecurityMarginModel(1.0))