287 lines
17 KiB
C#
287 lines
17 KiB
C#
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
|
|
// Date:01/2018
|
|
|
|
namespace MarketData.Generator.Momentum
|
|
{
|
|
/// <summary>Generate momentum selections - </summary>
|
|
public class MomentumGenerator
|
|
{
|
|
public enum MomentumGeneratorConstants{DayCount=252}; // Trading days in one year
|
|
|
|
private MomentumGenerator()
|
|
{
|
|
}
|
|
// These two interfaces are used by the UI so that it can capture the fallback candidates
|
|
public static MomentumCandidates GenerateMomentum(DateTime tradeDate,MGConfiguration config)
|
|
{
|
|
List<String> symbolsHeld=new List<String>();
|
|
return new MomentumCandidates(GenerateMomentum(tradeDate,symbolsHeld,config).Take(config.MaxPositions).ToList());
|
|
}
|
|
public static MomentumCandidates GenerateMomentumWithFallback(DateTime tradeDate,MGConfiguration config)
|
|
{
|
|
List<String> symbolsHeld=new List<String>();
|
|
MomentumCandidates momentumCandidates=GenerateMomentum(tradeDate,symbolsHeld,config);
|
|
QualityIndicator qualityIndicator=new QualityIndicator(config.QualityIndicatorType);
|
|
if((null==momentumCandidates||0==momentumCandidates.Count)&&config.UseFallbackCandidate)
|
|
{
|
|
QualityIndicatorCandidate bestCandidate=null;
|
|
if(null!=config.FallbackCandidateBestOf && !"".Equals(config.FallbackCandidateBestOf))
|
|
{
|
|
bestCandidate=CandidateSelector.SelectBestCandidate(qualityIndicator,Utility.ToList(config.FallbackCandidateBestOf),config.FallbackCandidate,tradeDate);
|
|
if(null!=bestCandidate)
|
|
{
|
|
ZacksRank zacksRank=ZacksRankDA.GetZacksRankOnOrBefore(bestCandidate.Symbol,tradeDate);
|
|
MomentumCandidate momentumCandidate=new MomentumCandidate();
|
|
momentumCandidate.Symbol=bestCandidate.Symbol;
|
|
momentumCandidate.AnalysisDate=tradeDate;
|
|
momentumCandidate.CumReturn252=bestCandidate.CumReturn252;
|
|
momentumCandidate.IDIndicator=bestCandidate.IDIndicator;
|
|
momentumCandidate.Score=bestCandidate.Score;
|
|
momentumCandidate.DayCount=bestCandidate.DayCount;
|
|
momentumCandidate.PE=bestCandidate.PE;
|
|
momentumCandidate.Beta=bestCandidate.Beta;
|
|
momentumCandidate.Return1D=bestCandidate.Return1D;
|
|
if(null!=zacksRank)momentumCandidate.ZacksRank=zacksRank.Rank;
|
|
momentumCandidates=new MomentumCandidates();
|
|
momentumCandidates.Add(momentumCandidate);
|
|
}
|
|
}
|
|
}
|
|
return momentumCandidates;
|
|
}
|
|
// This interface is called by the Backtest
|
|
public static MomentumCandidates GenerateMomentum(DateTime tradeDate,List<String> symbolsHeld,MGConfiguration config)
|
|
{
|
|
DateGenerator dateGenerator=new DateGenerator();
|
|
List<String> symbols=PricingDA.GetSymbols();
|
|
MomentumCandidates momentumCandidates=new MomentumCandidates();
|
|
MomentumCandidates highPECandidates=new MomentumCandidates();
|
|
DateTime startDateOfReturns=dateGenerator.GetPrevMonthEnd(tradeDate,2);
|
|
List<String> noTradeSymbols=Utility.ToList(config.NoTradeSymbols);
|
|
List<String> noTradeFinancialSymbols=Utility.ToList(config.NoTradeFinancialSymbols);
|
|
MomentumRejectionStatistics momentumRejectionStatistics=new MomentumRejectionStatistics();
|
|
|
|
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Generate momentum.. examining candidates"));
|
|
momentumRejectionStatistics.TotalItems=symbols.Count;
|
|
// Go through the universe of stocks
|
|
for(int index=0;index<symbols.Count;index++)
|
|
{
|
|
String symbol=symbols[index];
|
|
if(0==(index%500))Console.WriteLine("Processing item {0} of {1}",index+1,symbols.Count);
|
|
// Check if the symbol is held in any open positions
|
|
if(symbolsHeld.Any(x=>x.Equals(symbol))){momentumRejectionStatistics.SymbolHeldList.Add(symbol);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;}
|
|
|
|
// 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;}
|
|
|
|
// 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);
|
|
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;}
|
|
|
|
// Fetch single day price
|
|
Price price=GBPriceCache.GetInstance().GetPrice(symbol,tradeDate);
|
|
if(null==price){momentumRejectionStatistics.OneDayPriceScreenList.Add(symbol);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;}
|
|
|
|
// 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;
|
|
|
|
// 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;}
|
|
|
|
// 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;
|
|
Prices velocityPrices=GBPriceCache.GetInstance().GetPrices(symbol,tradeDate,(int)MomentumGenerator.MomentumGeneratorConstants.DayCount+20);
|
|
double priceHigh=(from Price selectPrice in velocityPrices select selectPrice.Open).Max();
|
|
double priceLow=(from Price selectPrice in velocityPrices select selectPrice.Open).Min();
|
|
if(0.00==priceHigh-priceLow)velocity=0.00;
|
|
else velocity=((price.Open-priceLow)*(100/(priceHigh-priceLow)))/100.00;
|
|
|
|
// Price slopes - These are used for display purposes
|
|
double[] pricesArray=null;
|
|
LeastSquaresResult leastSquaresResult;
|
|
|
|
// Get the benchmark pricing low pricing data and check the slope of previous lows; only if Beta of candidate is >= LowSlopeBetaThreshhold
|
|
// The idea behind this check is that a high beta stock will track to the benchmark. So if the benchmark lows are forming a downward pattern then we
|
|
// assume that this is a somewhat bearish condition. The config has the setting at a 15 day check and the threshold beta set to 1.00
|
|
if(config.UseLowSlopeBetaCheck && fundamental.Beta>=config.LowSlopeBetaThreshhold)
|
|
{
|
|
Prices benchmarkPrices=GBPriceCache.GetInstance().GetPrices(config.Benchmark,tradeDate,config.LowSlopeBetaDays);
|
|
pricesArray=Numerics.ToDouble(benchmarkPrices.GetPricesLow());
|
|
leastSquaresResult=Numerics.LeastSquares(pricesArray);
|
|
double slopeBmk=leastSquaresResult.Slope;
|
|
if(slopeBmk<0){momentumRejectionStatistics.HighBetaBenchmarkSlopeScreenList.Add(symbol);continue;}
|
|
}
|
|
|
|
// *** MACDSignal detection
|
|
if(config.UseMACD)
|
|
{
|
|
MACDSetup macdSetup=new MACDSetup(config.MACDSetup);
|
|
MACDSignals macdSignals=MACDGenerator.GenerateMACD(prices,macdSetup);
|
|
Signals signalsMACD = SignalGenerator.GenerateSignals(macdSignals);
|
|
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;}
|
|
}
|
|
|
|
// *** Stochastics oscillator
|
|
if(config.UseStochastics)
|
|
{
|
|
Stochastics stochastics=StochasticsGenerator.GenerateStochastics(prices);
|
|
Signals signalsStochastics=new Signals(SignalGenerator.GenerateSignals(stochastics).OrderByDescending(x => x.SignalDate).ToList());
|
|
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; }
|
|
}
|
|
|
|
// Analyst Ratings - "Downgrades" that are more than a year old (252 days) are not considered. Mean reversion.... bad companies improve, good companies decline.
|
|
DateTime minRatingDate=dateGenerator.GenerateHistoricalDate(startDateOfReturns,(int)MomentumGeneratorConstants.DayCount);
|
|
AnalystRatings analystRatings=AnalystRatingsDA.GetAnalystRatingsMaxDateNoZacks(symbol,tradeDate);
|
|
analystRatings.RemoveAll(x => x.Date<minRatingDate);
|
|
AnalystRating rating=null;
|
|
if(null!=analystRatings)rating=(from AnalystRating analystRating in analystRatings where analystRating.Type.Equals("Downgrades") select analystRating).FirstOrDefault();
|
|
if(null!=rating)
|
|
{
|
|
momentumRejectionStatistics.AnalystRatingsScreenList.Add(symbol);
|
|
continue;
|
|
}
|
|
|
|
// The cumulative returns for the ranking skip to the previous month to eliminate short term reversal anomaly (Wesley Gray : Quantum Momentum)
|
|
prices=GBPriceCache.GetInstance().GetPrices(symbol,startDateOfReturns,(int)MomentumGeneratorConstants.DayCount);
|
|
if(null==prices||(int)MomentumGeneratorConstants.DayCount!=prices.Count)
|
|
{
|
|
momentumRejectionStatistics.StartDateOfReturnsHistoricalPricingScreenList.Add(symbol);
|
|
continue;
|
|
}
|
|
|
|
// check for outliers in the return stream
|
|
returns=prices.GetReturns();
|
|
if((from float value in returns where Math.Abs(value)>.50 select value).Count()>0)
|
|
{
|
|
momentumRejectionStatistics.OutliersInReturnsScreenList.Add(symbol);
|
|
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;
|
|
// }
|
|
//}
|
|
|
|
// Zacks Rank. This is for informational purposes for now but may further it's use in the future.
|
|
ZacksRank zacksRank=ZacksRankDA.GetZacksRankOnOrBefore(symbol,tradeDate);
|
|
|
|
// Apply the PEScreening last because there an option to permit the inclusion of the high PE candidates if we have no other available candidates.
|
|
// 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);
|
|
MomentumCandidate highPECandidate=new MomentumCandidate();
|
|
highPECandidate.AnalysisDate=tradeDate;
|
|
highPECandidate.Symbol=symbol;
|
|
highPECandidate.CumReturn252=prices.GetCumulativeReturn();
|
|
highPECandidate.DayCount=(int)MomentumGeneratorConstants.DayCount;
|
|
highPECandidate.IDIndicator=IDIndicator.Calculate(prices);
|
|
highPECandidate.Score=ScoreIndicator.Calculate(prices);
|
|
highPECandidate.MaxDrawdown=prices.MaxDrawdown();
|
|
highPECandidate.MaxUpside=prices.MaxUpside();
|
|
highPECandidate.PE=fundamental.PE;
|
|
highPECandidate.Beta=fundamental.Beta;
|
|
highPECandidate.Velocity=velocity;
|
|
highPECandidate.Volume=price.Volume;
|
|
highPECandidate.Return1D=return1D;
|
|
if(null!=zacksRank)highPECandidate.ZacksRank=zacksRank.Rank;
|
|
highPECandidates.Add(highPECandidate);
|
|
continue;
|
|
}
|
|
// *********************************************************************** C A N D I D A T E A C C E P T A N C E *******************************************************
|
|
// At this point whatever remains is taken so initialize the candidate and add to list
|
|
MomentumCandidate momentumCandidate=new MomentumCandidate();
|
|
momentumCandidate.AnalysisDate=tradeDate;
|
|
momentumCandidate.Symbol=symbol;
|
|
momentumCandidate.CumReturn252=prices.GetCumulativeReturn();
|
|
momentumCandidate.DayCount=(int)MomentumGeneratorConstants.DayCount;
|
|
momentumCandidate.IDIndicator=IDIndicator.Calculate(prices);
|
|
momentumCandidate.Score=ScoreIndicator.Calculate(prices);
|
|
momentumCandidate.MaxDrawdown=prices.MaxDrawdown();
|
|
momentumCandidate.MaxUpside=prices.MaxUpside();
|
|
momentumCandidate.PE=fundamental.PE;
|
|
momentumCandidate.Beta=fundamental.Beta;
|
|
momentumCandidate.Velocity=velocity;
|
|
momentumCandidate.Volume=price.Volume;
|
|
momentumCandidate.Return1D=return1D;
|
|
if(null!=zacksRank)momentumCandidate.ZacksRank=zacksRank.Rank;
|
|
momentumCandidates.Add(momentumCandidate);
|
|
} // for all symbols
|
|
// ********************************************************* 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(!config.StrictMaxPE && momentumCandidates.Count<config.MaxPositions && highPECandidates.Count>0)
|
|
{
|
|
int takeCandidates=config.MaxPositions-momentumCandidates.Count;
|
|
highPECandidates=new MomentumCandidates(highPECandidates.OrderBy(x=>x.PE).Take(takeCandidates).ToList());
|
|
momentumCandidates.AddRange(highPECandidates);
|
|
if(config.Verbose)MDTrace.WriteLine(LogLevel.DEBUG,String.Format("High PE Candidates,{0}",Utility.FromList((from MomentumCandidate momentumCandidate in highPECandidates select momentumCandidate.Symbol).ToList())));
|
|
}
|
|
|
|
QualityIndicator qualityIndicator=new QualityIndicator(config.QualityIndicatorType);
|
|
if(qualityIndicator.Quality.Equals(QualityIndicator.QualityType.IDIndicator))
|
|
{
|
|
momentumCandidates=new MomentumCandidates((from MomentumCandidate momentumCandidate in momentumCandidates orderby momentumCandidate.IDIndicator ascending, momentumCandidate.CumReturn252 descending, momentumCandidate.Return1D descending, momentumCandidate.Volume descending select momentumCandidate).ToList());
|
|
}
|
|
else
|
|
{
|
|
momentumCandidates=new MomentumCandidates((from MomentumCandidate momentumCandidate in momentumCandidates orderby momentumCandidate.Score descending,momentumCandidate.CumReturn252 descending,momentumCandidate.Return1D descending,momentumCandidate.Volume descending select momentumCandidate).ToList());
|
|
}
|
|
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MomentumGenertor.GenerateMomentum:{0} candidates",momentumCandidates.Count()));
|
|
return momentumCandidates;
|
|
}
|
|
}
|
|
}
|