Overall Statistics |
Total Orders 10001 Average Win 0.83% Average Loss -0.97% Compounding Annual Return -89.177% Drawdown 46.800% Expectancy -0.532 Net Profit -41.124% Sharpe Ratio -1.395 Sortino Ratio -1.408 Probabilistic Sharpe Ratio 1.620% Loss Rate 75% Win Rate 25% Profit-Loss Ratio 0.86 Alpha -0.303 Beta 1.024 Annual Standard Deviation 0.553 Annual Variance 0.306 Information Ratio -1.089 Tracking Error 0.289 Treynor Ratio -0.753 Total Fees $379.81 Estimated Strategy Capacity $44000000.00 Lowest Capacity Asset CMA R735QTJ8XC9X Portfolio Turnover 49.43% |
from AlgorithmImports import * class FundamentalFactorAlphaModel(AlphaModel): ''' An AlphaModel that generates insights based on fundamental factors. It ranks stocks according to the Return on Equity (ROE), Net Margin, and the Price/Earnings Ratio (PERatio). The model generates buy insights for the top 20% of each sector. ''' def __init__(self): self.rebalanceTime = datetime.min # Dictionary containing set of securities in each sector # e.g. {technology: set(AAPL, TSLA, ...), healthcare: set(XYZ, ABC, ...), ... } self.sectors = {} def Update(self, algorithm, data): ''' Updates this alpha model with the latest data from the algorithm. This is called each time the algorithm receives data for subscribed securities Args: algorithm: The algorithm instance data: The new data available Returns: New insights ''' if algorithm.Time <= self.rebalanceTime: return [] # Set the rebalance time to match the insight expiry self.rebalanceTime = Expiry.EndOfQuarter(algorithm.Time) insights = [] # Loop through each sector to generate insights for sector in self.sectors: securities = self.sectors[sector] # Sort securities by ROE, net margin, and PE ratio sortedByROE = sorted(securities, key=lambda x: x.Fundamentals.OperationRatios.ROE.Value, reverse=True) sortedByPM = sorted(securities, key=lambda x: x.Fundamentals.OperationRatios.NetMargin.Value, reverse=True) sortedByPE = sorted(securities, key=lambda x: x.Fundamentals.ValuationRatios.PERatio, reverse=False) # Dictionary holding a dictionary of scores for each security in the sector scores = {} for security in securities: score = sum([sortedByROE.index(security), sortedByPM.index(security), sortedByPE.index(security)]) scores[security] = score # Add best 20% of each sector to longs set (minimum 1) length = max(int(len(scores)/5), 1) for security in sorted(scores.items(), key=lambda x: x[1], reverse=False)[:length]: symbol = security[0].Symbol # Use Expiry.EndOfQuarter in this case to match Universe, Alpha and PCM insights.append(Insight.Price(symbol, Expiry.EndOfQuarter, InsightDirection.Up)) return insights def OnSecuritiesChanged(self, algorithm, changes): ''' Event fired each time the we add/remove securities from the data feed Args: algorithm: The algorithm instance that experienced the change in securities changes: The security additions and removals from the algorithm ''' # Remove security from sector set for security in changes.RemovedSecurities: for sector in self.sectors: if security in self.sectors[sector]: self.sectors[sector].remove(security) # Add security to corresponding sector set for security in changes.AddedSecurities: sector = security.Fundamentals.AssetClassification.MorningstarSectorCode if sector not in self.sectors: self.sectors[sector] = set() self.sectors[sector].add(security)
from AlgorithmImports import * class TechnicalAnalysisAlphaModel(AlphaModel): ''' An AlphaModel that generates insights based on technical analysis. It evaluates securities using a Simple Moving Average (SMA) crossover strategy to determine bullish or bearish signals. ''' def __init__(self, short_window=50, long_window=200): self.short_window = short_window self.long_window = long_window self.symbolDataBySymbol = {} class SymbolData: def __init__(self, symbol, algorithm): self.Symbol = symbol self.shortSMA = algorithm.SMA(symbol, 50, Resolution.Daily) self.longSMA = algorithm.SMA(symbol, 200, Resolution.Daily) self.IsBullish = False def update(self, algorithm): if not (self.shortSMA.IsReady and self.longSMA.IsReady): return None previousIsBullish = self.IsBullish self.IsBullish = self.shortSMA.Current.Value > self.longSMA.Current.Value if self.IsBullish and not previousIsBullish: return Insight.Price(self.Symbol, timedelta(days=15), InsightDirection.Up) elif not self.IsBullish and previousIsBullish: return Insight.Price(self.Symbol, timedelta(days=15), InsightDirection.Down) return None def Update(self, algorithm, data): insights = [] for symbol, symbolData in self.symbolDataBySymbol.items(): insight = symbolData.update(algorithm) if insight is not None: insights.append(insight) return insights def OnSecuritiesChanged(self, algorithm, changes): for added in changes.AddedSecurities: if added.Symbol not in self.symbolDataBySymbol: self.symbolDataBySymbol[added.Symbol] = self.SymbolData(added.Symbol, algorithm) for removed in changes.RemovedSecurities: if removed.Symbol in self.symbolDataBySymbol: del self.symbolDataBySymbol[removed.Symbol]
from AlgorithmImports import * class PercentBAlphaModel(AlphaModel): """ Alpha model using Bollinger Bands %B value for generating insights. Generates an 'Up' insight when %B crosses above 0.2 from below. Generates a 'Flat' insight to liquidate when %B crosses below 0.8 from above. """ def __init__(self, period=20, k=2): """ Initializes the PercentBAlphaModel with Bollinger Bands parameters. Args: period (int): The period for the Bollinger Bands calculation. k (float): The number of standard deviations specifying the distance between the bands and the moving average. """ self.period = period self.k = k self.bbIndicators = {} self.percentBWindows = {} def Update(self, algorithm, data): """ Generates insights based on the %B value from the Bollinger Bands for each symbol. Args: algorithm (QCAlgorithm): The algorithm instance. data (Slice): The current data slice. Returns: List[Insight]: A list of insights generated by this model. """ insights = [] for symbol, bb in self.bbIndicators.items(): if symbol in data and data[symbol] is not None: # Update the Bollinger Bands with the latest price price = data[symbol].Close bb.Update(data.Time, price) # Check if Bollinger Bands are ready to use if bb.IsReady: percentB = (price - bb.LowerBand.Current.Value) / \ (bb.UpperBand.Current.Value - bb.LowerBand.Current.Value) # Store the %B values in a rolling window percentBWindow = self.percentBWindows[symbol] percentBWindow.Add(percentB) if percentBWindow.IsReady: # Generate insights based on the %B value if percentB > 0.2 and percentBWindow[1] <= 0.2: insights.append(Insight.Price(symbol, timedelta(days=1), InsightDirection.Up)) elif percentB < 0.8 and percentBWindow[1] >= 0.8: insights.append(Insight.Price(symbol, timedelta(days=1), InsightDirection.Flat)) return insights def OnSecuritiesChanged(self, algorithm, changes): """ Initializes or removes Bollinger Bands indicators and their associated rolling windows for added or removed securities. """ for added in changes.AddedSecurities: symbol = added.Symbol if symbol not in self.bbIndicators: self.bbIndicators[symbol] = algorithm.BB(symbol, self.period, self.k, MovingAverageType.Simple, Resolution.Daily) self.percentBWindows[symbol] = RollingWindow[float](2) # We need only the last two values for %B for removed in changes.RemovedSecurities: symbol = removed.Symbol if symbol in self.bbIndicators: del self.bbIndicators[symbol] del self.percentBWindows[symbol]
from AlgorithmImports import * import math class FixedFractionPortfolioConstructionModel(PortfolioConstructionModel): def __init__(self, fraction=0.01, atr_multiplier=2, atrIndicators=None, riskManagementModel=None): """ Initializes the Fixed Fraction Portfolio Construction Model. Args: fraction (float): The fixed fraction of the portfolio to allocate per security. atrIndicators (Dictionary[Symbol, IndicatorBase]): A dictionary of ATR indicators for each security. riskManagementModel (RiskManagementModel): An instance of the risk management model to be used for setting trailing stops. """ self.fraction = fraction self.atr_multiplier = atr_multiplier self.atrIndicators = atrIndicators self.riskManagementModel = riskManagementModel def CreateTargets(self, algorithm, insights): """ Create portfolio targets based on the provided insights and ATR values. Additionally, calculates and sets the trailing stop percentage for each security. Args: algorithm (QCAlgorithm): The algorithm instance. insights (List[Insight]): The list of insights provided by the Alpha model. Returns: List[PortfolioTarget]: A list of portfolio targets to try and achieve. """ targets = [] for insight in insights: try: if insight.IsActive(algorithm.UtcTime) and insight.Symbol in self.atrIndicators: currentPrice = round(abs(algorithm.Securities[insight.Symbol].Price),2) atrIndicator = self.atrIndicators[insight.Symbol] if insight.Direction == InsightDirection.Flat: targets.append(PortfolioTarget(insight.Symbol, 0)) # Liquidate position continue # Move on to the next insight if atrIndicator.IsReady: scaledAtrValue = round(abs(atrIndicator.Current.Value * self.atr_multiplier), 2) total_portfolio_value = algorithm.Portfolio.TotalPortfolioValue stopLossPrice = currentPrice - scaledAtrValue riskPerShare = round(abs(currentPrice - stopLossPrice), 2) dollar_risk_per_trade = algorithm.Portfolio.TotalPortfolioValue * self.fraction quantity = math.floor(dollar_risk_per_trade / riskPerShare) # Enhanced debugging for ATR and position sizing algorithm.Debug(f"Insight: {insight.Symbol}, Type: {insight.Direction}, " + f"ATR: {atrIndicator.Current.Value:.2f}, Scaled ATR: {scaledAtrValue}, " + f"Current Price: {currentPrice}, Stop Loss Price: {stopLossPrice:.2f}, Risk in chart points: {riskPerShare}," + f"Portfolio Value: {total_portfolio_value}, Fraction: {self.fraction}, Dollar Risk Per Trade: ${dollar_risk_per_trade:.2f} " + f"Quantity: {quantity:.2f} shares") # Calculate and set the trailing stop percentage self._setTrailingStop(algorithm, insight.Symbol, currentPrice, scaledAtrValue) targets.append(PortfolioTarget(insight.Symbol, quantity)) except Exception as e: algorithm.Error(f"Error in creating target for {insight.Symbol}: {str(e)}") return targets def _setTrailingStop(self, algorithm, symbol, currentPrice, scaledAtrValue): try: trailingStopPercentage = scaledAtrValue / currentPrice if self.riskManagementModel: # Pass the scaled ATR value to the risk management model self.riskManagementModel.SetTrailingStop(algorithm, symbol, scaledAtrValue) algorithm.Log(f"Trailing stop set for {symbol}: {trailingStopPercentage*100:.2f}%" + f"ATR Multiplier: {self.atr_multiplier}, Scaled ATR: {scaledAtrValue}") except Exception as e: algorithm.Error(f"Error in setting trailing stop for {symbol}: {str(e)}")
#region imports from AlgorithmImports import * #endregion class AtrBasedTrailingStopRiskManagementModel(RiskManagementModel): ''' Evaluates each invested security against its trailing stop price, calculated using the initial ATR value. Generates liquidation targets for securities breaching their trailing stops. This Risk Model adjusts the trailing stop based on a multiplier of the ATR value at the time of position initiation, and it maintains this initial ATR value for the life of the trade. This approach aligns with the objective of using a consistent ATR value for risk management and stop-loss calculations, regardless of subsequent ATR fluctuations. Ensures consistent risk management and stop-loss calculations using initial ATR values, irrespective of ATR fluctuations. ''' def __init__(self,algorithm): ''' The model initializes dictionaries for storing trailing stop percentages, peak prices, ATR multipliers, and initial ATR values. This is a solid foundation for tracking the necessary data for each security. ''' self.algorithm = algorithm self.trailingStopPercentages = {} self.peakPrices = {} self.atrMultipliers = {} # Store the initial ATR values for each symbol self.initialAtrValues = {} def ManageRisk(self, algorithm, targets): ''' The ManageRisk method evaluates each invested security to determine if the current price breaches the trailing stop price, calculated using the initial ATR value. This ensures that the stop adjusts based on the price movement relative to the entry point, using the ATR value at trade initiation. Liquidation targets are generated for securities that breach their trailing stops, aligning with the objective of preserving capital by limiting losses. ''' updatedTargets = [] for kvp in algorithm.Securities: symbol = kvp.Key security = kvp.Value currentPrice = security.Price holdings = security.Holdings if symbol in self.trailingStopPercentages and security.Invested: # Use the initial ATR value for the trailing stop calculation initialAtrValue = self.initialAtrValues.get(symbol, 0) trailingStopPrice = self.CalculateTrailingStopPriceUsingInitialATR(symbol, initialAtrValue, holdings) if trailingStopPrice is None: algorithm.Log(f"ManageRisk: No trailing stop price calculated for {symbol}.") # Handle the None case, perhaps continue to the next loop iteration or log a warning continue if self.IsStopTriggered(self.algorithm, symbol, currentPrice, trailingStopPrice, holdings): updatedTargets.append(PortfolioTarget(symbol, 0)) algorithm.Log(f"Trailing stop triggered for {symbol}, liquidating position at current price {currentPrice} with trailing stop at {trailingStopPrice}.") self.RemoveSymbolData(symbol) else: algorithm.Log(f"Trailing stop not triggered for {symbol}. Current price: {currentPrice}, Trailing Stop: {trailingStopPrice}") return updatedTargets def SetTrailingStop(self, algorithm, symbol, scaledAtrValue): """ Sets the initial trailing stop values for a security using the scaled ATR value. Args: algorithm (QCAlgorithm): The algorithm instance. symbol (Symbol): The symbol for the security. scaledAtrValue (float): The scaled ATR value (already adjusted by the multiplier). """ initialPrice = algorithm.Securities[symbol].Price atrPercentage = scaledAtrValue / initialPrice self.initialAtrValues[symbol] = scaledAtrValue self.trailingStopPercentages[symbol] = atrPercentage self.peakPrices[symbol] = initialPrice # Initialize peak price at position entry # self.peakPrices.pop(symbol, None) # Remove this line as it's not needed and incorrect here algorithm.Log(f"Set initial trailing stop for {symbol} with ATR Percentage: {atrPercentage*100:.2f}%, based on scaled ATR Value: {scaledAtrValue} and initial price: {initialPrice}.") ##### Additional helper methods for clarity and organization #### def UpdatePeakPrices(self, symbol, high, low): """ Updates the peak prices for a symbol based on new highs for long positions and new lows for short positions. Args: symbol (Symbol): The trading symbol. high (float): The high price of the current bar. low (float): The low price of the current bar. """ # Ensure there's an existing position to manage if symbol in self.algorithm.Portfolio and self.algorithm.Portfolio[symbol].Invested: # Get the SecurityHolding object holdings = self.algorithm.Portfolio[symbol] # Update peak price for long positions based on new highs if holdings.IsLong: if symbol not in self.peakPrices or high > self.peakPrices[symbol]: self.peakPrices[symbol] = high self.algorithm.Log(f"Updated peak high for {symbol}: {high}") # Update peak price for short positions based on new lows elif holdings.IsShort: if symbol not in self.peakPrices or low < self.peakPrices[symbol]: self.peakPrices[symbol] = low self.algorithm.Log(f"Updated peak low for {symbol}: {low}") def CalculateTrailingStopPriceUsingInitialATR(self, symbol, initialAtrValue, holdings): # Retrieve the peak price and initial ATR value, safely handling missing keys peak_price = self.peakPrices.get(symbol, None) initial_atr_value = self.initialAtrValues.get(symbol, None) # If either value is not found, handle it appropriately if peak_price is None or initial_atr_value is None: self.algorithm.Debug(f"CalculateTrailingStopPriceUsingInitialATR: Missing data for {symbol}") return None # or some appropriate action # Calculate the trailing stop price using the initial ATR value retrieved safely if holdings.IsLong: return peak_price - initial_atr_value else: return peak_price + initial_atr_value def IsStopTriggered(self, algorithm, symbol, currentPrice, trailingStopPrice, holdings): # Check if trailingStopPrice is None and handle the case appropriately if trailingStopPrice is None: algorithm.Debug(f"IsStopTriggered: Trailing stop price is None for {symbol}") return False # Or handle it in a way that makes sense for your strategy if (holdings.IsLong and currentPrice <= trailingStopPrice) or \ (holdings.IsShort and currentPrice >= trailingStopPrice): return True return False def RemoveSymbolData(self, symbol): ''' The RemoveSymbolData method cleans up data for securities that are no longer part of the portfolio, maintaining the model's efficiency and accuracy. ''' # Remove the symbol's data from dictionaries after liquidation self.trailingStopPercentages.pop(symbol, None) self.peakPrices.pop(symbol, None) self.initialAtrValues.pop(symbol, None) self.algorithm.Log(f"Cleaned up data for {symbol} after liquidation.")#region imports
#region imports from AlgorithmImports import * #endregion from AlphaModel import * from PortfolioConstructionModel import FixedFractionPortfolioConstructionModel from RiskManagement import AtrBasedTrailingStopRiskManagementModel class SimpleAlgorithm(QCAlgorithm): ''' A QuantConnect algorithm that uses both coarse and fine universe selection models to create a universe of securities based on certain fundamental factors. The algorithm uses an alpha model for signal generation, equal weighting portfolio construction model, and immediate execution model for trades. ''' def Initialize(self): '''Initialize the data and resolution required, as well as the cash and start-end dates for the algorithm.''' self.SetStartDate(2020, 1, 1) self.SetEndDate(2023, 1, 1) self.SetCash(100000) # Set the Universe Resolution self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverse(self.CoarseSelectionFunction) # Alpha Model self.AddAlpha(PercentBAlphaModel()) # Initialize and set the risk management model self.riskManagementModel = AtrBasedTrailingStopRiskManagementModel(self) self.SetRiskManagement(self.riskManagementModel) # Portfolio construction model self.atrIndicators = {} # Initialize and set the portfolio construction model self.SetPortfolioConstruction(FixedFractionPortfolioConstructionModel( fraction=0.01, atr_multiplier=2, atrIndicators=self.atrIndicators, riskManagementModel=self.riskManagementModel )) # Execution model self.SetExecution(ImmediateExecutionModel()) self.last_rebalance_day = 0 def IsRebalanceDue(self, time): ''' Checks if a rebalance is due. A rebalance is due on the first trading day of each week. ''' # Calculate the number of days since the start of the algorithm days_since_start = (time - self.StartDate).days # Calculate the day of the week (0=Monday, 6=Sunday) day_of_week = time.weekday() # Determine if today is Monday (or another chosen day of the week for rebalancing) # and if at least a week has passed since the last rebalance if day_of_week == 0 and (days_since_start - self.last_rebalance_day) >= 7: self.last_rebalance_day = days_since_start return time return None def CoarseSelectionFunction(self, coarse): ''' The CoarseSelectionFunction, filters the universe of stocks to only include those whose current price is greater than $10 and whose average daily dollar volume (a proxy for liquidity) is greater than $1 million. x.Price > 10: This condition filters out all stocks with a current price of $10 or less. The idea here is to exclude stocks that may be considered too volatile or risky, which often includes lower-priced stocks. x.DollarVolume > 1e6: This condition ensures that only stocks with an average daily dollar volume greater than $1 million are included. Dollar volume is calculated by multiplying the average daily volume by the stock price, providing a measure of liquidity. A higher dollar volume generally indicates that the stock is more liquid, making it easier to enter and exit positions without significantly impacting the stock's price. ''' # Sort coarse universe by dollar volume, descending sorted_by_dollar_volume = sorted(coarse, key=lambda x: x.DollarVolume, reverse=True) # Filter to select securities with a minimum price and volume threshold # This example uses arbitrary thresholds for illustration filtered = [x.Symbol for x in sorted_by_dollar_volume if x.Price > 10 and x.DollarVolume > 1e6 and x.HasFundamentalData] # Log the filtered symbol's ticker values # self.Log(f"Filtered symbols: {[x.Value for x in filtered[:10]]}") # Return a subset of securities, for example, the top 500 by dollar volume return filtered[:500] def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: if security.Symbol not in self.atrIndicators: # Create and store the ATR indicator for the added security self.atrIndicators[security.Symbol] = self.ATR(security.Symbol, 14, MovingAverageType.Simple, Resolution.Daily) for security in changes.RemovedSecurities: if security.Symbol in self.atrIndicators: # Remove the ATR indicator for the removed security del self.atrIndicators[security.Symbol]