Overall Statistics |
Total Orders 160 Average Win 4.24% Average Loss -0.98% Compounding Annual Return 83.418% Drawdown 12.100% Expectancy 2.419 Start Equity 1000000 End Equity 1364389.23 Net Profit 36.439% Sharpe Ratio 1.811 Sortino Ratio 2.823 Probabilistic Sharpe Ratio 69.386% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 4.32 Alpha 0.204 Beta 1.947 Annual Standard Deviation 0.297 Annual Variance 0.088 Information Ratio 1.443 Tracking Error 0.254 Treynor Ratio 0.276 Total Fees $94.37 Estimated Strategy Capacity $2600000.00 Lowest Capacity Asset BIOA R735QTJ8XC9X Portfolio Turnover 5.21% |
#region imports from AlgorithmImports import * #endregion def CalculateTrendIndicators(self): top_pct= 0.1 # Moving average calculation moving_averages = {} for symbol, prices in self.historical_data.items(): if len(prices) >= self.lookback: moving_averages[symbol] = prices.mean() top_symbols = sorted(moving_averages, key=moving_averages.get, reverse=True)[:int(len(moving_averages) * top_pct)] # # Compounded Return Calculations # moving_averages = {} # compounded_returns = {} # for symbol, prices in self.historical_data.items(): # if len(prices) >= self.lookback: # daily_returns = prices.pct_change().dropna() # compounded_return = (1 + daily_returns).prod() - 1 # compounded_returns[symbol] = compounded_return # top_symbols = sorted(compounded_returns, key=compounded_returns.get, reverse=True)[:int(len(compounded_returns) * top_pct)] return top_symbols
#region imports from AlgorithmImports import * from pypfopt import BlackLittermanModel import pandas as pd import numpy as np #endregion def OptimizePortfolio(self, mu, S): # Black-Litterman views (neutral in this case) Q = pd.Series(index=mu.index, data=mu.values) P = np.identity(len(mu.index)) # Optimize via Black-Litterman bl = BlackLittermanModel(S,Q=Q, P=P, pi=mu) bl_weights = bl.bl_weights() # Normalize weights total_weight = sum(bl_weights.values()) normalized_weights = {symbol: weight / total_weight for symbol, weight in bl_weights.items()} return normalized_weights
#region imports from pypfopt import risk_models, expected_returns from AlgorithmImports import * #endregion def CalculateRiskParameters(self, top_symbols): # Get historical prices for selected symbols selected_history = self.History(top_symbols, self.lookback, Resolution.Daily)['close'].unstack(level=0) mu = expected_returns.mean_historical_return(selected_history) S = risk_models.sample_cov(selected_history) return mu, S
#region imports from AlgorithmImports import * #endregion def Execute_Trades(self, position_list): # Place market orders for symbol, weight in position_list.items(): self.SetHoldings(symbol, weight) def Exit_Positions(self, position_list): # Liquidate positions not in the target weights for holding in self.Portfolio.Values: if holding.Symbol not in position_list and holding.Invested: self.Liquidate(holding.Symbol)
# region imports from AlgorithmImports import * from Alpha_Models import CalculateTrendIndicators from Risk_Models import CalculateRiskParameters from Portfolio_Construction import OptimizePortfolio from Trade_Execution import Execute_Trades, Exit_Positions # endregion class NCSU_Strategy_2024_Q3(QCAlgorithm): def Initialize(self): self.SetStartDate(2023, 12, 1) # Set Start Date self.SetEndDate(2024, 12, 31) # Set End Date self.SetCash(1000000) # Set Strategy Cash # ETF to get constituents from self.etf = "SPY" self.universe_settings.leverage = 2.0 self.AddEquity(self.etf, Resolution.Daily) self.UniverseSettings.Resolution = Resolution.Daily self.AddUniverseSelection(ETFConstituentsUniverseSelectionModel(self.etf)) self.historical_data = {} self.lookback = 252 # MAX lookback period for moving average calculation self.rebalanceTime = None self.rebalanced = False self.Schedule.On(self.DateRules.MonthStart(self.etf), self.TimeRules.AfterMarketOpen(self.etf, 20), self.Rebalance) def OnSecuritiesChanged(self, changes): # Evaluate if performance is better by trading out of holdings dropped from ETF for security in changes.AddedSecurities: self.historical_data[security.Symbol] = self.History(security.Symbol, self.lookback, Resolution.Daily)['close'] for security in changes.RemovedSecurities: if security.Symbol in self.historical_data: del self.historical_data[security.Symbol] if self.Portfolio[security.Symbol].Invested: self.Liquidate(security.Symbol) self.Debug(f"Liquidating {security.Symbol} as it is removed from the ETF") def Rebalance(self): # Check if today is the first day of the month and if we have already rebalanced if self.Time.day == 1 and self.rebalanced: self.Debug("Already rebalanced this month") return # Rebalancing logic self.Debug(f"Rebalancing on {self.Time}") # Alpha Model Output sorted_symbols = CalculateTrendIndicators(self) # Risk Model Output mu, S = CalculateRiskParameters(self, top_symbols=sorted_symbols) # Portfolio Construction Output target_positions = OptimizePortfolio(self, mu=mu, S=S) Exit_Positions(self, position_list=target_positions) Execute_Trades(self, position_list=target_positions) # Set rebalanced flag to True self.rebalanced = True def CheckPositions(self): # If no positions are on, call Rebalance if not self.Portfolio.Invested: self.Debug("No positions on, calling Rebalance") self.Rebalance() # Reset rebalanced flag if it's not Wednesday if self.Time.day != 1: self.rebalanced = False