From 4d9f18d21eba7d5d97c3229121a5a667707f47df Mon Sep 17 00:00:00 2001 From: Sean Date: Sun, 2 Feb 2025 16:42:53 -0500 Subject: [PATCH] Fix rejection statistics --- .../Generator/Momentum/MomentumGenerator.cs | 154 +++++++++++++----- 1 file changed, 113 insertions(+), 41 deletions(-) diff --git a/MarketDataLib/Generator/Momentum/MomentumGenerator.cs b/MarketDataLib/Generator/Momentum/MomentumGenerator.cs index 3fa8f52..fc180fe 100644 --- a/MarketDataLib/Generator/Momentum/MomentumGenerator.cs +++ b/MarketDataLib/Generator/Momentum/MomentumGenerator.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Text; using MarketData.MarketDataModel; using MarketData.DataAccess; using MarketData.Utils; using System.Linq; -using MarketData.Helper; using MarketData.Numerical; using MarketData.Cache; -using MarketData.Generator.Indicators; // Filename: MomentumGenerator.cs // Author:Sean Kessler @@ -72,57 +69,101 @@ namespace MarketData.Generator.Momentum DateTime startDateOfReturns=dateGenerator.GetPrevMonthEnd(tradeDate,2); List noTradeSymbols=Utility.ToList(config.NoTradeSymbols); List noTradeFinancialSymbols=Utility.ToList(config.NoTradeFinancialSymbols); - MomentumRejectionStatistics momentumRejectionStatistics=new MomentumRejectionStatistics(); + CandidateViolations candidateViolations = new CandidateViolations(); MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Generate momentum.. examining candidates")); - momentumRejectionStatistics.TotalItems=symbols.Count; // Go through the universe of stocks for(int index=0;indexx.Equals(symbol))){momentumRejectionStatistics.SymbolHeldList.Add(symbol);continue;} + if(symbolsHeld.Any(x=>x.Equals(symbol))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate already held.")); + continue; + } // Check if the symbol is in the no trade list (i.e.) Bitcoin etc., - if(noTradeSymbols.Any(x=>x.Equals(symbol))){momentumRejectionStatistics.NoTradeListList.Add(symbol);continue;} + if(noTradeSymbols.Any(x=>x.Equals(symbol))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate in NoTradeSymbol.")); + continue; + } // Check MarketCap, EBITDA, PE, and Revenue Per Share Fundamental fundamental=FundamentalDA.GetFundamentalMaxDate(symbol,tradeDate); - if(null==fundamental){momentumRejectionStatistics.NoFundamentalList.Add(symbol);continue;} - if(!(fundamental.MarketCap>=config.MarketCapLowerLimit)){momentumRejectionStatistics.MarketCapLimitList.Add(symbol);continue;} - if(config.UseEBITDAScreen && (double.IsNaN(fundamental.EBITDA)||fundamental.EBITDA<=0)){momentumRejectionStatistics.EBITDAScreenList.Add(symbol);continue;} - if(config.UseRevenuePerShareScreen && (double.IsNaN(fundamental.RevenuePerShare)||fundamental.RevenuePerShare<0.00)){momentumRejectionStatistics.RevenuePerShareScreenList.Add(symbol);continue;} + if(null==fundamental) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate no fundamental.")); + continue; + } + + if(!(fundamental.MarketCap>=config.MarketCapLowerLimit)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate MarketCapLimit.")); + continue; + } + + if(config.UseEBITDAScreen && (double.IsNaN(fundamental.EBITDA)||fundamental.EBITDA<=0)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate EBITDA violation.")); + continue; + } + + if(config.UseRevenuePerShareScreen && (double.IsNaN(fundamental.RevenuePerShare)||fundamental.RevenuePerShare<0.00)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate RevenuePerShare violation.")); + continue; + } // Initial PE screening. This screen checks for existance of PE and if it is availabe it must be >0.00 . There is another PE based on limits further below -// if(config.UsePEScreen && (double.IsNaN(fundamental.PE))||fundamental.PE<=0.00) if(config.UsePEScreen && (double.IsNaN(fundamental.PE)||fundamental.PE<=0.00)) { - momentumRejectionStatistics.PEScreenList.Add(symbol); + candidateViolations.Add(new CandidateViolation(symbol,"Candidate PE violation.")); continue; } // Exclude any company in the "Financial" sector CompanyProfile companyProfile=CompanyProfileDA.GetCompanyProfile(symbol); - if(null!=companyProfile&&null!=companyProfile.Sector&&noTradeFinancialSymbols.Any(x=>x.Equals(companyProfile.Sector))){momentumRejectionStatistics.FinancialSectorScreenList.Add(symbol);continue;} + if(null!=companyProfile&&null!=companyProfile.Sector&&noTradeFinancialSymbols.Any(x=>x.Equals(companyProfile.Sector))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate Financial Sector violation.")); + continue; + } // Fetch single day price Price price=GBPriceCache.GetInstance().GetPrice(symbol,tradeDate); - if(null==price){momentumRejectionStatistics.OneDayPriceScreenList.Add(symbol);continue;} + if(null==price) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate missing price on trade date.")); + continue; + } // Filter penny stocks - don't trade anything less than $1.00 - if(price.Close<1.00||price.Open<1.00){momentumRejectionStatistics.PennyStockScreenList.Add(symbol);continue;} + if(price.Close<1.00||price.Open<1.00) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate penny stock violation.")); + continue; + } // Retrieve prices Prices prices=null; prices=GBPriceCache.GetInstance().GetPrices(symbol,tradeDate,(int)MomentumGeneratorConstants.DayCount); - if(null==prices||prices.Count!=(int)MomentumGeneratorConstants.DayCount){momentumRejectionStatistics.TradeDateHistoricalPricingScreenList.Add(symbol);continue;} - float[] returns=null; + if(null==prices||prices.Count!=(int)MomentumGeneratorConstants.DayCount) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate missing price history.")); + continue; + } // calculate the one day return double return1D=prices.GetReturn1D(); // Liquidity check - if any day has volume < 10,000 then we reject it - if(((from Price xPrice in prices where xPrice.Volume<10000 select xPrice).Count())>1){momentumRejectionStatistics.LiquidityScreenList.Add(symbol);continue;} + if(((from Price xPrice in prices where xPrice.Volume<10000 select xPrice).Count())>1) + { + candidateViolations.Add(new CandidateViolation(symbol,"Liquidity violation.")); + continue; + } // Calculate velocity as a percentage range of the open price within the 252+20 day range of prices - This is used for display purposes double velocity; @@ -145,7 +186,11 @@ namespace MarketData.Generator.Momentum pricesArray=Numerics.ToDouble(benchmarkPrices.GetPricesLow()); leastSquaresResult=Numerics.LeastSquares(pricesArray); double slopeBmk=leastSquaresResult.Slope; - if(slopeBmk<0){momentumRejectionStatistics.HighBetaBenchmarkSlopeScreenList.Add(symbol);continue;} + if(slopeBmk<0) + { + candidateViolations.Add(new CandidateViolation(symbol,"Beta threshhold violation.")); + continue; + } } // *** MACDSignal detection @@ -157,8 +202,16 @@ namespace MarketData.Generator.Momentum signalsMACD=new Signals(signalsMACD.Take(config.MACDSignalDays).ToList()); int weakSellSignals=(from Signal signal in signalsMACD where signal.IsWeakSell() select signal).Count(); int strongSellSignals=(from Signal signal in signalsMACD where signal.IsStrongSell() select signal).Count(); - if(config.MACDRejectWeakSellSignals && weakSellSignals>0){momentumRejectionStatistics.MACDWeakSellScreenList.Add(symbol);continue;} - if(config.MACDRejectStrongSellSignals && strongSellSignals>0){momentumRejectionStatistics.MACDStrongSellScreenList.Add(symbol);continue;} + if(config.MACDRejectWeakSellSignals && weakSellSignals>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"MACD Reject Weak Sell violation.")); + continue; + } + if(config.MACDRejectStrongSellSignals && strongSellSignals>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"MACD Reject Strong Sell violation.")); + continue; + } } // *** Stochastics oscillator @@ -169,8 +222,16 @@ namespace MarketData.Generator.Momentum signalsStochastics=new Signals(signalsStochastics.Take(config.StochasticsSignalDays).ToList()); int weakSellCount=(from Signal signal in signalsStochastics where signal.IsWeakSell() select signal).Count(); int strongSellCount=(from Signal signal in signalsStochastics where signal.IsStrongSell() select signal).Count(); - if(config.StochasticsRejectStrongSells&&strongSellCount>0) { momentumRejectionStatistics.StochasticsStrongSellScreenList.Add(symbol); continue; } - if(config.StochasticsRejectWeakSells&&weakSellCount>0) { momentumRejectionStatistics.StochasticsWeakSellScreenList.Add(symbol); continue; } + if(config.StochasticsRejectStrongSells&&strongSellCount>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"Stochastics Oscillator Reject Strong Sell violation.")); + continue; + } + if(config.StochasticsRejectWeakSells&&weakSellCount>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"Stochastics Oscillator Reject Weak Sell violation.")); + continue; + } } // Analyst Ratings - "Downgrades" that are more than a year old (252 days) are not considered. Mean reversion.... bad companies improve, good companies decline. @@ -181,7 +242,7 @@ namespace MarketData.Generator.Momentum if(null!=analystRatings)rating=(from AnalystRating analystRating in analystRatings where analystRating.Type.Equals("Downgrades") select analystRating).FirstOrDefault(); if(null!=rating) { - momentumRejectionStatistics.AnalystRatingsScreenList.Add(symbol); + candidateViolations.Add(new CandidateViolation(symbol,"AnalystRating Downgrade violation within set period.")); continue; } @@ -189,31 +250,25 @@ namespace MarketData.Generator.Momentum prices=GBPriceCache.GetInstance().GetPrices(symbol,startDateOfReturns,(int)MomentumGeneratorConstants.DayCount); if(null==prices||(int)MomentumGeneratorConstants.DayCount!=prices.Count) { - momentumRejectionStatistics.StartDateOfReturnsHistoricalPricingScreenList.Add(symbol); + candidateViolations.Add(new CandidateViolation(symbol,"Insufficient pricing, cannot determine rank.")); continue; } // check for outliers in the return stream + float[] returns = default; returns=prices.GetReturns(); if((from float value in returns where Math.Abs(value)>.50 select value).Count()>0) { - momentumRejectionStatistics.OutliersInReturnsScreenList.Add(symbol); + candidateViolations.Add(new CandidateViolation(symbol,"Candidate pricing contains outliers in the returns.")); continue; } // Cumulative return double cumulativeReturn=prices.GetCumulativeReturn(); - if(cumulativeReturn<.10){momentumRejectionStatistics.NegativeReturnsScreenList.Add(symbol);continue;} - -// OverExtended check - //if(config.UseOverExtendedIndicator) - //{ - // bool? result=OverExtendedIndicator.IsOverextended(symbol,tradeDate,config.UseOverExtendedIndicatorDays,config.UseOverExtendedIndicatorViolationThreshhold,config.UseOverExtendedIndicatorMarginPercent); - // if(null!=result && true==result.Value) - // { - // momentumRejectionStatistics.OverExtendedScreenList.Add(symbol); - // continue; - // } - //} + if(cumulativeReturn<.10) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate cumulative returns below threshhold.")); + continue; + } // Zacks Rank. This is for informational purposes for now but may further it's use in the future. ZacksRank zacksRank=ZacksRankDA.GetZacksRankOnOrBefore(symbol,tradeDate); @@ -222,7 +277,7 @@ namespace MarketData.Generator.Momentum // The idea is to try to avoid high PE stocks as they are more likey to introduce drawdowns as backtests have shown. if(config.UseMaxPEScreen && !double.IsNaN(fundamental.PE) && fundamental.PE>config.MaxPE) { - momentumRejectionStatistics.MaxPEScreenList.Add(symbol); + candidateViolations.Add(new CandidateViolation(symbol,"PE violation.")); MomentumCandidate highPECandidate=new MomentumCandidate(); highPECandidate.AnalysisDate=tradeDate; highPECandidate.Symbol=symbol; @@ -259,9 +314,26 @@ namespace MarketData.Generator.Momentum momentumCandidate.Return1D=return1D; if(null!=zacksRank)momentumCandidate.ZacksRank=zacksRank.Rank; momentumCandidates.Add(momentumCandidate); + } // for all symbols + + if(0!=candidateViolations.Count) + { + MDTrace.WriteLine(LogLevel.DEBUG,"**************** C A N D I D A T E S U M M A R Y ************************"); + IEnumerable> groups = candidateViolations.GroupBy(x => x.ReasonCategory).OrderByDescending(group => group.Count()).Select(group => Tuple.Create(group.Key, group.Count())); + foreach(Tuple group in groups) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Group: {0} Count:{1}",group.Item1, group.Item2)); + } + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Considered : {momentumCandidates.Count+candidateViolations.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Disqualified : {candidateViolations.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Eligible : {momentumCandidates.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,"******************************************************************************************************"); + // ********************************************************* E N D C A N D I D A T E S E L E C T I O N C R I T E R I A **************************************** -// If we wind up with less than the number of required candidates then check the StrictMaxPE flag and, if allowed, add the highPECandidate (that we've accumulated but skipped) to the momentumCandidates ordering them by the Lowest PE +// If we wind up with less than the number of required candidates then check the StrictMaxPE +// flag and, if allowed, add the highPECandidate (that we've accumulated but skipped) to the momentumCandidates ordering them by the Lowest PE if(!config.StrictMaxPE && momentumCandidates.Count0) { int takeCandidates=config.MaxPositions-momentumCandidates.Count;