Overall Statistics |
Total Orders 451 Average Win 2.77% Average Loss -1.69% Compounding Annual Return 33.269% Drawdown 34.200% Expectancy 0.846 Start Equity 30000 End Equity 690619.07 Net Profit 2202.064% Sharpe Ratio 1.045 Sortino Ratio 1.18 Probabilistic Sharpe Ratio 52.657% Loss Rate 30% Win Rate 70% Profit-Loss Ratio 1.64 Alpha 0.131 Beta 1.191 Annual Standard Deviation 0.219 Annual Variance 0.048 Information Ratio 1.046 Tracking Error 0.141 Treynor Ratio 0.192 Total Fees $1270.92 Estimated Strategy Capacity $150000000.00 Lowest Capacity Asset MSFT R735QTJ8XC9X Portfolio Turnover 3.59% |
# region imports from AlgorithmImports import * # endregion class MarketCapFactor: def __init__(self, security): self._security = security @property def value(self): return self._security.fundamentals.market_cap class SortinoFactor: def __init__(self, algorithm, symbol, lookback): self._sortino = algorithm.sortino(symbol, lookback, resolution=Resolution.DAILY) @property def value(self): return self._sortino.current.value class KERFactor: def __init__(self, algorithm, symbol, lookback): self._ker = algorithm.ker(symbol, lookback, resolution=Resolution.DAILY) @property def value(self): return self._ker.current.value class CorrFactor: def __init__(self, algorithm, symbol, reference, lookback): self._c = algorithm.c(symbol, reference, lookback, correlation_type=CorrelationType.Pearson, resolution=Resolution.DAILY) @property def value(self): return 1 - abs(self._c.current.value) class ROCFactor: def __init__(self, algorithm, symbol, lookback): self._roc = algorithm.roc(symbol, lookback, resolution=Resolution.DAILY) @property def value(self): return self._roc.current.value class QualityFactor: """Test quality/profitability factor""" def __init__(self, security): self._security = security @property def value(self): try: metrics = [] fundamentals = self._security.fundamentals # Gross margin gross_margin = fundamentals.operation_ratios.gross_margin if hasattr(gross_margin, 'value'): metrics.append(gross_margin.value) # Operating margin op_margin = fundamentals.operation_ratios.operation_margin if hasattr(op_margin, 'value'): metrics.append(op_margin.value) # ROE roe = fundamentals.operation_ratios.roe if hasattr(roe, 'value'): metrics.append(roe.value) # ROA roa = fundamentals.operation_ratios.roa if hasattr(roa, 'value'): metrics.append(roa.value) # Return average if we have valid metrics return np.mean(metrics) if metrics else np.nan except: return np.nan class ValueFactor: """Test composite value factor""" def __init__(self, security): self._security = security @property def value(self): try: ratios = [] valuation = self._security.fundamentals.valuation_ratios # Price/Book ratios.append(1/valuation.pb_ratio) # Price/Earnings ratios.append(1/valuation.pe_ratio) # Price/Sales ratios.append(1/valuation.ps_ratio) # Price/Cash Flow ratios.append(1/valuation.pcf_ratio) return np.mean(ratios) if ratios else np.nan except: return np.nan class MomentumFactor: """Price momentum factor""" def __init__(self, algorithm, security, lookback=252): self._algorithm = algorithm self._security = security self._lookback = lookback @property def value(self): try: # Get the price history history = self._algorithm.History( self._security.Symbol, self._lookback + 21, Resolution.Daily ) if len(history) < self._lookback: return np.nan # Calculate 12-1 month momentum (skip most recent month) return history.close[-21]/history.close[0] - 1 except: return np.nan class GrowthFactor: """Growth metrics""" def __init__(self, security): self._security = security @property def value(self): try: metrics = [] # Revenue growth if not np.isnan(self._security.fundamentals.operation_ratios.revenue_growth.value): metrics.append(self._security.fundamentals.operation_ratios.revenue_growth.value) # Net income growth if not np.isnan(self._security.fundamentals.operation_ratios.net_income_growth.value): metrics.append(self._security.fundamentals.operation_ratios.net_income_growth.value) # Operating income growth if not np.isnan(self._security.fundamentals.operation_ratios.operation_income_growth.value): metrics.append(self._security.fundamentals.operation_ratios.operation_income_growth.value) return np.nanmean(metrics) if metrics else np.nan except: return np.nan class OCFConversionFactor: """Operating Cash Flow Conversion (OCF/EBITDA) factor""" def __init__(self, security): self._security = security @property def value(self): try: fundamentals = self._security.fundamentals cash_flow = fundamentals.financial_statements.cash_flow_statement income = fundamentals.financial_statements.income_statement # Get operating cash flow if hasattr(cash_flow, 'operating_cash_flow'): ocf = cash_flow.operating_cash_flow.value else: return np.nan # Calculate EBITDA if hasattr(income, 'ebitda'): ebitda = income.ebitda.value else: return np.nan # Avoid division by zero if ebitda == 0: return np.nan return ocf/ebitda except: return np.nan class FCFYieldFactor: """Free Cash Flow Yield (FCF/Enterprise Value) factor""" def __init__(self, security): self._security = security @property def value(self): try: fundamentals = self._security.fundamentals cash_flow = fundamentals.financial_statements.cash_flow_statement # Calculate FCF (Operating Cash Flow - CapEx) if hasattr(cash_flow, 'operating_cash_flow') and hasattr(cash_flow, 'capital_expenditure'): fcf = cash_flow.operating_cash_flow.value - abs(cash_flow.capital_expenditure.value) else: return np.nan # Get enterprise value if hasattr(fundamentals, 'market_cap') and hasattr(fundamentals.financial_statements.balance_sheet, 'total_debt'): enterprise_value = fundamentals.market_cap + fundamentals.financial_statements.balance_sheet.total_debt.value else: return np.nan # Avoid division by zero if enterprise_value == 0: return np.nan return fcf/enterprise_value except: return np.nan class InstitutionalOwnershipFactor: """Institutional Ownership Changes factor""" def __init__(self, algorithm, security, lookback=63): # ~3 months self._algorithm = algorithm self._security = security self._lookback = lookback @property def value(self): try: # Get fundamental history history = list(self._algorithm.History[Fundamental]( self._security.Symbol, self._lookback )) if len(history) < 2: # Need at least 2 points for change return np.nan # Get number of institutional holders from security reference data current_inst = history[-1].security_reference.institutional_holders previous_inst = history[0].security_reference.institutional_holders if previous_inst == 0: return np.nan return (current_inst - previous_inst) / previous_inst except: return np.nan class BuybackYieldFactor: """Buy-back Yield (Net Stock Repurchases/Market Cap) factor""" def __init__(self, security): self._security = security @property def value(self): try: fundamentals = self._security.fundamentals cash_flow = fundamentals.financial_statements.cash_flow_statement # Get stock repurchases if hasattr(cash_flow, 'repurchase_of_capital_stock'): repurchases = abs(cash_flow.repurchase_of_capital_stock.value) # Make positive else: return np.nan # Get market cap if hasattr(fundamentals, 'market_cap'): market_cap = fundamentals.market_cap else: return np.nan # Avoid division by zero if market_cap == 0: return np.nan return repurchases/market_cap except: return np.nan class BetaAdjustedVolatilityFactor: """Beta-adjusted Volatility Ratio (recent vs historical volatility) factor""" def __init__(self, algorithm, security, recent_window=21, historical_window=252): self._algorithm = algorithm self._security = security self._recent_window = recent_window # ~1 month self._historical_window = historical_window # ~1 year @property def value(self): try: # Get price history history = self._algorithm.History( self._security.Symbol, self._historical_window, Resolution.Daily ) if len(history) < self._historical_window: return np.nan # Calculate recent and historical volatility recent_returns = history.close[-self._recent_window:].pct_change().dropna() historical_returns = history.close.pct_change().dropna() recent_vol = recent_returns.std() * np.sqrt(252) # Annualize historical_vol = historical_returns.std() * np.sqrt(252) # Avoid division by zero if historical_vol == 0: return np.nan return recent_vol/historical_vol except: return np.nan # except Exception as e: # # Print exception details # print(f"Exception type: {type(e).__name__}") # print(f"Exception message: {e}")
from AlgorithmImports import * import numpy as np class MarketCapFactor: def __init__(self, security): self._security = security @property def value(self): try: return self._security.fundamentals.market_cap except: return np.nan def validate_value(value): """Helper to validate numeric values""" if value is None: return False if not isinstance(value, (int, float)): return False try: return not np.isnan(value) and not np.isinf(value) except: return False class QualityFactor: """Test quality/profitability factor""" def __init__(self, security): self._security = security @property def value(self): try: metrics = [] fundamentals = self._security.fundamentals # Gross margin gross_margin = fundamentals.operation_ratios.gross_margin if hasattr(gross_margin, 'value'): metrics.append(gross_margin.value) # Operating margin op_margin = fundamentals.operation_ratios.operation_margin if hasattr(op_margin, 'value'): metrics.append(op_margin.value) # ROE roe = fundamentals.operation_ratios.roe if hasattr(roe, 'value'): metrics.append(roe.value) # ROA roa = fundamentals.operation_ratios.roa if hasattr(roa, 'value'): metrics.append(roa.value) # Return average if we have valid metrics return np.mean(metrics) if metrics else np.nan except: return np.nan class ValueFactor: """Test composite value factor""" def __init__(self, security): self._security = security @property def value(self): try: ratios = [] valuation = self._security.fundamentals.valuation_ratios # Price/Book pb = valuation.pb_ratio if hasattr(pb, 'value') and pb.value > 0: ratios.append(1/pb.value) # Price/Earnings pe = valuation.pe_ratio if hasattr(pe, 'value') and pe.value > 0: ratios.append(1/pe.value) # Price/Sales ps = valuation.ps_ratio if hasattr(ps, 'value') and ps.value > 0: ratios.append(1/ps.value) # Price/Cash Flow pcf = valuation.pcf_ratio if hasattr(pcf, 'value') and pcf.value > 0: ratios.append(1/pcf.value) return np.mean(ratios) if ratios else np.nan except: return np.nan class MomentumFactor: """Price momentum factor""" def __init__(self, algorithm, security, lookback=252): self._algorithm = algorithm self._security = security self._lookback = lookback @property def value(self): try: # Get the price history history = self._algorithm.History( self._security.Symbol, self._lookback + 21, Resolution.Daily ) if len(history) < self._lookback: return np.nan # Calculate 12-1 month momentum (skip most recent month) return history.close[-21]/history.close[0] - 1 except: return np.nan class GrowthFactor: """Growth metrics""" def __init__(self, security): self._security = security @property def value(self): try: metrics = [] # Revenue growth if not np.isnan(self._security.fundamentals.operation_ratios.revenue_growth): metrics.append(self._security.fundamentals.operation_ratios.revenue_growth) # Net income growth if not np.isnan(self._security.fundamentals.operation_ratios.net_income_growth): metrics.append(self._security.fundamentals.operation_ratios.net_income_growth) # Operating income growth if not np.isnan(self._security.fundamentals.operation_ratios.operation_income_growth): metrics.append(self._security.fundamentals.operation_ratios.operation_income_growth) return np.nanmean(metrics) if metrics else np.nan except: return np.nan class SortinoFactor: """Keep original Sortino factor for comparison""" def __init__(self, algorithm, symbol, lookback): self._sortino = algorithm.sortino(symbol, lookback, resolution=Resolution.DAILY) @property def value(self): try: return self._sortino.current.value except: return np.nan
# region imports from AlgorithmImports import * from itertools import chain, combinations from scipy import optimize from scipy.optimize import Bounds from factors import * # endregion """ SPY Constituents ----------------- Best Sharpe - doenst beat market -------------- Top 20 universe_size = 20 # we pick top 20 weights portfolio_size = 20 # we ultimately select 20 top scores factorUniverse = 34 // UseEMA = 1 SortinoFactor, ValueFactor,QualityFactor Best Profit - flogs market after -------------- Top 20 universe_size = 20 # we pick top 20 weights portfolio_size = 20 # we ultimately select 20 top scores factorUniverse = 16 // UseEMA = 0 KERFactor, QualityFactor Great - GO live -------------- Top 5 universe_size = 5 # we pick top 5 weights portfolio_size = 5 # we ultimately select 5 top scores factorUniverse = 32 // UseEMA = 1 SortinoFactor, KERFactor, QualityFactor ######################################## ##### GO LIVE ######################################## ## ## Great - KER + FCFYield ## -------------- ## Top 5 ## universe_size = 5 # we pick top 5 weights ## portfolio_size = 5 # we ultimately select 5 top scores ## use EMA = 1 ## KERFactor, FCFYieldFactor ## (factor universe = 10 -- with second batch of factors) ## ################################################################################ Great - BuyBackYield + KER -------------- Top 5 universe_size = 5 # we pick top 5 weights portfolio_size = 5 # we ultimately select 5 top scores use EMA = 0 KERFactor, BuyBackYieldFactor (factor universe = 11 -- with second batch of factors) """ class FactorWeightOptimizationAlgorithm(QCAlgorithm): def initialize(self): ######## DATE self.SetStartDate(2014, 1, 1) # self.SetEndDate(2021, 12, 20) # self.SetStartDate(2010, 1, 1) self.set_cash(30000) self.settings.automatic_indicator_warm_up = True # self.spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA) ticker = "SPY" self.set_benchmark(ticker) self.spy = self.add_equity(ticker).symbol self.ema50 = self.ema(self.spy,50, Resolution.Daily) self.ema200 = self.ema(self.spy,200, Resolution.Daily) self.ema500 = self.ema(self.spy,500, Resolution.Daily) self.ema200.updated += self.OnEMAUpdated # Add a universe of hourly data. self.universe_settings.resolution = Resolution.HOUR self.universe_size = self.get_parameter('universe_size', 5) # Add variable to track last portfolio value self.last_rebalance_portfolio_value = None # Use the separate universe filter function. self._universe = self.add_universe(self.universe.etf(self.spy, universe_filter_func=self.universe_filter)) self._lookback = self.get_parameter('lookback', 21) # Set a 21-day trading lookback. # Create a Schedule Event to rebalance the portfolio. self.schedule.on(self.date_rules.month_start(self.spy), self.time_rules.after_market_open(self.spy, 31), self._rebalance) def universe_filter(self, constituents): """ Filters and sorts the given constituents based on their weight, returning the top symbols. """ filtered_constituents = [c for c in constituents if c.weight] sorted_constituents = sorted(filtered_constituents, key=lambda c: c.weight) return [c.symbol for c in sorted_constituents[-self.universe_size:]] def OnEMAUpdated(self, sender, bar): # self.plot("series", self.ema50.current.value) # self.plot("series", self.ema200.current.value) self.Plot('EMAx', 'SPY50', self.ema50.current.value) self.Plot('EMAx', 'SPY200', self.ema200.current.value) self.Plot('EMAx', 'SPY500', self.ema500.current.value) def on_securities_changed(self, changes): for security in changes.added_securities: # Create factors for assets that enter the universe. # security.factors = [MarketCapFactor(security), SortinoFactor(self, security.symbol, self._lookback)] # Go Live CAMO Factors # security.factors = [ # SortinoFactor(self, security.symbol, self._lookback), # KERFactor(self, security.symbol, self._lookback), # QualityFactor(security), # ] # allFactors = [ # MarketCapFactor(security), # SortinoFactor(self, security.symbol, self._lookback), # KERFactor(self, security.symbol, self._lookback), # ValueFactor(security), # QualityFactor(security), # GrowthFactor(security) # ] # # security.factors = self.get_factor_subset_by_index(allFactors, self.get_parameter('factorUniverse', 32)) allFactors = [ KERFactor(self, security.symbol, self._lookback), # Keeping KER for trend efficiency SortinoFactor(self, security.symbol, self._lookback), # Keeping Sortino for downside risk QualityFactor(security), # Keeping core Quality metrics OCFConversionFactor(security), # New cash flow quality metric FCFYieldFactor(security), # New value metric BuybackYieldFactor(security), # New management confidence metric BetaAdjustedVolatilityFactor(self, security, 21, 252) # New risk regime metric ] # security.factors = self.get_factor_subset_by_index(allFactors, self.get_parameter('factorUniverse', 32)) ## DEBUG : Print Subsets ############################################################ # all_subsets = list(chain.from_iterable(combinations(allFactors, r) for r in range(1, len(allFactors) + 1))) # for index, content in enumerate(all_subsets): # self.Log(f"{index} - position {index} {content}") # self.quit("message") # quit ############################################################ # security.factors = [ # KERFactor(self, security.symbol, self._lookback), # Keeping KER for trend efficiency # SortinoFactor(self, security.symbol, self._lookback), # Keeping Sortino for downside risk # QualityFactor(security), # Keeping core Quality metrics # OCFConversionFactor(security), # New cash flow quality metric # FCFYieldFactor(security), # New value metric # # InstitutionalOwnershipFactor(self, security, 63), # New smart money flow metric # BuybackYieldFactor(security), # New management confidence metric # BetaAdjustedVolatilityFactor(self, security, 21, 252) # New risk regime metric # ] this = 0 def get_factor_subset_by_index(self, array, subset_param): # Generate all subsets all_subsets = list(chain.from_iterable(combinations(array, r) for r in range(1, len(array) + 1))) # Handle out-of-bounds subset_param if subset_param < 0 or subset_param >= len(all_subsets): raise ValueError(f"subset_param must be in range 0 to {len(all_subsets) - 1}.") # Return the subset at the specified index return list(all_subsets[subset_param]) def OnData(self,slice): if( self.get_parameter("useEMA", 1) == 1): self.liquidateIfEMABearish() if (int(self.get_parameter("useEquityTrailStop",1)) == 1): # trailing stop if not hasattr(self,"trailEquityStop"): self.trailEquityStop = self.Portfolio.TotalPortfolioValue * 0.80 else: if self.Portfolio.TotalPortfolioValue <= self.trailEquityStop: self.Liquidate(tag="trail equity stop") self.trailEquityStop = self.Portfolio.TotalPortfolioValue * 0.80 return self.trailEquityStop = max(self.trailEquityStop,self.Portfolio.TotalPortfolioValue * 0.90) def isEMABearish(self): return (self.ema50.current.value < self.ema500.current.value ) def liquidateIfEMABearish(self): if( self.ema500.is_ready ): if(self.isEMABearish()): self.liquidate(tag="EMA is Bearish") def _rebalance(self): try: # Calculate and log profit since last rebalance current_value = self.Portfolio.TotalPortfolioValue if self.last_rebalance_portfolio_value is not None: profit_pct = ((current_value - self.last_rebalance_portfolio_value) / self.last_rebalance_portfolio_value) * 100 self.Log(f"Profit since last rebalance: {profit_pct:.2f}%") # Update the last rebalance value self.last_rebalance_portfolio_value = current_value if( self.get_parameter("useEMA", 1) == 1): if(self.isEMABearish()): self.liquidateIfEMABearish() return # Get raw factor values of the universe constituents. factors_df = pd.DataFrame() for symbol in self._universe.selected: for i, factors in enumerate(self.securities[symbol].factors): factors_df.loc[symbol, i] = factors.value if factors_df.empty: self.Debug("No factor data available for rebalancing") return # Calculate the factor z-scores. factor_zscores = (factors_df - factors_df.mean()) / factors_df.std() # Safely get historical data and calculate returns try: history_df = self.history(list(self._universe.selected), self._lookback, Resolution.DAILY) # Check if we have the expected data structure if not isinstance(history_df, pd.DataFrame) or 'close' not in history_df: self.Debug(f"Historical data format unexpected: {type(history_df)}") return # Unstack and calculate returns, handling any missing data price_df = history_df.close.unstack(0) if price_df.empty: self.Debug("No price data available for calculation") return # Calculate returns and handle any missing values trailing_return = price_df.pct_change(self._lookback-1).iloc[-1] trailing_return = trailing_return.fillna(0) # Replace any NaN values with 0 if trailing_return.empty: self.Debug("No valid return data available") return except Exception as e: self.Debug(f"Error calculating returns: {str(e)}") return # Run optimization only if we have valid data num_factors = factors_df.shape[1] try: factor_weights = optimize.minimize( lambda weights: -(np.dot(factor_zscores, weights) * trailing_return).sum(), x0=np.array([1.0/num_factors] * num_factors), method='Nelder-Mead', bounds=Bounds([0] * num_factors, [1] * num_factors), options={'maxiter': 10} ).x except Exception as e: self.Debug(f"Optimization failed: {str(e)}") return # Calculate the portfolio weights portfolio_weights = (factor_zscores * factor_weights).sum(axis=1) portfolio_weights = portfolio_weights[portfolio_weights > 0] portfolio_weights = portfolio_weights.nlargest(self.get_parameter('portfolio_size', 5)) if portfolio_weights.empty: self.Debug("No valid portfolio weights calculated") return # Log the date without time # Log portfolio weights, one per line formatted_weights = "\n\t".join([f"{x.Value} - {round((y/portfolio_weights.sum())*100, 2)}%" for x, y in portfolio_weights.items()]) self.Log(f"{self.Time.strftime('%Y-%m-%d')} -- {formatted_weights}") # Set holdings only if we have valid weights if not portfolio_weights.empty: self.set_holdings([PortfolioTarget(symbol, weight/portfolio_weights.sum()) for symbol, weight in portfolio_weights.items()], True) except BaseException as e: self.Debug(f"Error During: {str(e)}") return
from AlgorithmImports import * from scipy import optimize from scipy.optimize import Bounds from collections import defaultdict import numpy as np class FactorTestAlgorithm(QCAlgorithm): def initialize(self): self.SetStartDate(2014, 12, 31) self.SetCash(10_000_000) self.settings.automatic_indicator_warm_up = True spy = Symbol.create('SPY', SecurityType.EQUITY, Market.USA) # Add a universe of hourly data self.universe_settings.resolution = Resolution.HOUR universe_size = self.get_parameter('universe_size', 50) # Reduced for testing self._universe = self.add_universe(self.universe.etf(spy, universe_filter_func=lambda constituents: [c.symbol for c in sorted( [c for c in constituents if c.weight], key=lambda c: c.weight )[-universe_size:]] )) self._lookback = self.get_parameter('lookback', 21) # Create a Schedule Event to rebalance the portfolio self.schedule.on(self.date_rules.month_start(spy), self.time_rules.after_market_open(spy, 31), self._rebalance) def log_fundamental_data(self, symbol, data_checks): """ Helper to log fundamental data values data_checks: list of tuples (name, path to check) """ security = self.securities[symbol] self.Debug(f"\nLogging fundamental data for {symbol}:") for name, path in data_checks: try: # Get the fundamental field field = eval(f"security.fundamentals.{path}") # Access the numeric value value = field.value if hasattr(field, 'value') else field if isinstance(value, (int, float)): self.Debug(f"{name}: {value:.4f}") else: self.Debug(f"{name}: No valid value") except Exception as e: self.Debug(f"{name}: Not Available - {str(e)}") def is_valid_numeric(value): """Helper to check if a value is valid for calculations""" if value is None: return False if not isinstance(value, (int, float)): return False try: return not np.isnan(value) and not np.isinf(value) except: return False def validate_value_factors(self, symbol): """Test value factor data availability""" self.Debug(f"\nValidating Value Factors for {symbol}") self.log_fundamental_data(symbol, [ ("Price/Book", "valuation_ratios.pb_ratio"), ("Price/Earnings", "valuation_ratios.pe_ratio"), ("Price/Sales", "valuation_ratios.ps_ratio"), ("Price/CFO", "valuation_ratios.pcf_ratio") ]) def validate_quality_factors(self, symbol): """Test quality/profitability factor data availability""" self.Debug(f"\nValidating Quality Factors for {symbol}") self.log_fundamental_data(symbol, [ ("Gross Margin", "operation_ratios.gross_margin"), ("Operating Margin", "operation_ratios.operation_margin"), ("ROA", "operation_ratios.roa"), ("ROE", "operation_ratios.roe"), ("Net Income", "financial_statements.income_statement.net_income") ]) def validate_growth_factors(self, symbol): """Test growth factor data availability""" self.Debug(f"\nValidating Growth Factors for {symbol}") self.log_fundamental_data(symbol, [ ("Revenue Growth", "operation_ratios.revenue_growth"), ("Net Income Growth", "operation_ratios.net_income_growth"), ("Operating Income Growth", "operation_ratios.operation_income_growth") ]) def _rebalance(self): """Monthly data quality analysis""" self.Debug("\n=== Monthly Factor Data Analysis ===") # Collect factor values factor_data = defaultdict(list) for symbol in self._universe.selected: security = self.securities[symbol] # Test Value Metrics try: pb = security.fundamentals.valuation_ratios.pb_ratio if hasattr(pb, 'value'): factor_data["P/B"].append(pb.value) except: pass try: op_margin = security.fundamentals.operation_ratios.operation_margin if hasattr(op_margin, 'value'): factor_data["Operating Margin"].append(op_margin.value) except: pass try: roe = security.fundamentals.operation_ratios.roe if hasattr(roe, 'value'): factor_data["ROE"].append(roe.value) except: pass # Log statistics for each factor self.Debug("\nFactor Statistics:") for factor_name, values in factor_data.items(): if values: # Only process if we have valid values values = [v for v in values if isinstance(v, (int, float)) and not np.isnan(v)] if values: self.Debug(f"\n{factor_name}:") self.Debug(f"Available data: {len(values)}/{len(self._universe.selected)}") self.Debug(f"Mean: {np.mean(values):.4f}") self.Debug(f"Std: {np.std(values):.4f}") self.Debug(f"Range: [{np.min(values):.4f}, {np.max(values):.4f}]") else: self.Debug(f"\n{factor_name}: No valid numeric values") else: self.Debug(f"\n{factor_name}: No data collected") def validate_momentum_data(self, symbol): """Test price momentum data availability""" try: history = self.History(symbol, self._lookback + 21, Resolution.Daily) if len(history) >= self._lookback: mom = history.close[-21]/history.close[0] - 1 self.Debug(f"\n{symbol} 12-1m Momentum: {mom:.2%}") else: self.Debug(f"\n{symbol} Insufficient price history") except Exception as e: self.Debug(f"\n{symbol} Momentum calculation failed: {str(e)}") def on_securities_changed(self, changes): """Handle security additions and removals""" for security in changes.added_securities: # Run validation tests self.validate_value_factors(security.symbol) self.validate_quality_factors(security.symbol) self.validate_growth_factors(security.symbol) self.validate_momentum_data(security.symbol)