Files
marketdata/MarketDataLib/Generator/CMTrend/CMTCandidateGenerator.cs

442 lines
20 KiB
C#

using MarketData.Cache;
using MarketData.DataAccess;
using MarketData.Generator.Indicators;
using MarketData.Generator.MovingAverage;
using MarketData.Helper;
using MarketData.MarketDataModel;
using MarketData.Numerical;
using MarketData.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MarketData.Generator.CMTrend
{
public class CMTCandidateGenerator
{
private static readonly int PRICING_DAYS=252;
public CMTCandidateGenerator()
{
}
// *******************************************************************************************************************************************************************************
// ******************************************************************* G E N E R A T E C A N D I D A T E - M A R C M I N E R V I N I ****************************************
// *******************************************************************************************************************************************************************************
public static CMTCandidate GenerateCandidate(String symbol,DateTime tradeDate,CMTParams cmtParams,List<String> symbolsHeld=null)
{
CMTCandidate cmtCandidate=new CMTCandidate();
try
{
// Check MarketCap
Fundamental fundamental=FundamentalDA.GetFundamentalMaxDate(symbol,tradeDate);
if(null==fundamental)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("No fundamental for {0}.",symbol);
return cmtCandidate;
}
if(!(fundamental.MarketCap>=cmtParams.MarketCapLowerLimit))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("MarketCapLowerLimit constraint violation for {0}.",symbol);
return cmtCandidate;
}
// Check if the symbol is held in any open positions
if(null!=symbolsHeld&&symbolsHeld.Any(x => x.Equals(symbol)))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("{0} is already held.",symbol);
return cmtCandidate;
}
// No trade symbols
if(null!=cmtParams.NoTradeSymbolsList&&cmtParams.NoTradeSymbolsList.Any(x => x.Equals(symbol)))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("{0} is in the No-Trade list.",symbol);
return cmtCandidate;
}
// Equity check
CompanyProfile companyProfile=CompanyProfileDA.GetCompanyProfile(symbol);
if(null==companyProfile)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("No company profile found for {0}.",symbol);
return cmtCandidate;
}
if(!companyProfile.IsEquity)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("{0} is not an equity. {1}.",symbol,companyProfile.SecurityType);
return cmtCandidate;
}
// Sector Check
if(cmtParams.UseTradeOnlySectors)
{
List<String> validSectors=Utility.ToList(cmtParams.UseTradeOnlySectorsSectors);
if(null==companyProfile.Sector)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Invalid sector for {0}. Found {1} expected one of {2}.",symbol,null==companyProfile.Sector?"(Null)":companyProfile.Sector,Utility.ListToString(validSectors.ToList<String>()));
return cmtCandidate;
}
if(!validSectors.Any(x => x.Equals(companyProfile.Sector)))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Invalid sector for {0}. Found {1} expected one of {2}.",symbol,null==companyProfile.Sector?"(Null)":companyProfile.Sector,Utility.ListToString(validSectors.ToList<String>()));
return cmtCandidate;
}
}
// setup for trend analysis
Prices prices=GBPriceCache.GetInstance().GetPrices(symbol,tradeDate,PRICING_DAYS);
if(null==prices||prices.Count<PRICING_DAYS)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Insufficient pricing history, {0} days required.",PRICING_DAYS);
return cmtCandidate;
}
// Current Price Check
Price currentPrice=prices[0];
if(currentPrice.Date!=tradeDate)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="pricing date!=trade date.";
return cmtCandidate;
}
// Liquidity check - if any day has volume < MinVolume then we reject it
int belowThreshholdVolumeCount=(from Price xPrice in prices where xPrice.Volume<cmtParams.MinVolume select xPrice).Count();
if(cmtParams.LiquidityCheck&&belowThreshholdVolumeCount>0)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Insufficient liquidity.");
return cmtCandidate;
}
// Setup for the moving averages checks
Prices prices50=new Prices(prices.Take(50).ToList());
Prices prices15=new Prices(prices.Take(15).ToList());
Prices prices150=new Prices(prices.Take(150).ToList());
Prices prices200=new Prices(prices.Take(200).ToList());
DMAPrices dma50Prices=MovingAverageGenerator.GenerateMovingAverage(prices50,prices50.Count);
DMAPrices dma150Prices=MovingAverageGenerator.GenerateMovingAverage(prices150,prices150.Count);
DMAPrices dma200Prices=MovingAverageGenerator.GenerateMovingAverage(prices200,prices200.Count);
double dma50Close=dma50Prices[0].AVGPrice;
double dma150Close=dma150Prices[0].AVGPrice;
double dma200Close=dma200Prices[0].AVGPrice;
double volatility=prices15.Volatility();
// Trend #1 check. Check that current price is greater than 150 day moving average and current price is greater than 200 day moving average
if(currentPrice.Close<dma150Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#1 Violation : currentPrice.Close<=dma150Close.";
return cmtCandidate;
}
if(currentPrice.Close<dma200Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason="Trend#1 Violation : currentPrice.Close<=dma200Close.";
return cmtCandidate;
}
// Trend #2 check. Check that 150 day moving average is greater than the 200 day moving average
if(dma150Close<dma200Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#2 Violation : dma150Close<=dma200Close.";
return cmtCandidate;
}
// Trend #4 check : The 50 day moving average must be greater than the 150 day moving average
if(dma50Close<=dma150Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#4 Violation : (dma50Close<=dma150Close)&&(dma50Close<=dma200Close).";
return cmtCandidate;
}
// Trend #4a check : The 50 day moving average must be greater than the 200 day moving average
if(dma50Close<=dma200Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#4a Violation : dma50Close<=dma200Close.";
return cmtCandidate;
}
// Trend #5 check : The current price must be greater than the 50 day moving average
if(currentPrice.Close<=dma50Close)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#5 Violation : currentPrice.Close<=dma50Close.";
return cmtCandidate;
}
// Trend #6 check. Evaluate the MinPercentReturnOver52WeekLow
double weekLow52=prices.Min(x => x.Close);
double latestClose=currentPrice.Close;
double percentReturnOver52WeekLow=((latestClose-weekLow52)/weekLow52)*100.00;
if(percentReturnOver52WeekLow<cmtParams.MinPercentReturnOver52WeekLow)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Trend#6 Violation : percentReturnOver52WeekLow<={0}.",cmtParams.MinPercentReturnOver52WeekLow);
return cmtCandidate;
}
// Trend #7 check. Evaluate the MinPercentReturnProximityTo52WeekHigh
double weekHigh52=prices.Max(x => x.Close);
double percentReturnProximityTo52WeekHigh=double.NaN;
if(latestClose<weekHigh52) percentReturnProximityTo52WeekHigh=((weekHigh52-latestClose)/latestClose)*100.00;
else percentReturnProximityTo52WeekHigh=((latestClose-weekHigh52)/weekHigh52)*100.00;
if(latestClose<weekHigh52&&percentReturnProximityTo52WeekHigh>cmtParams.MinPercentReturnProximityTo52WeekHigh)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Trend#7 Violation :PercentReturnProximityTo52WeekHigh<{0}.",cmtParams.MinPercentReturnProximityTo52WeekHigh);
return cmtCandidate;
}
// Trend #8 check. Evaluate the RSI
RSICollection rsiCollection=RSIGenerator.GenerateRSI(symbol,currentPrice.Date,30); // generate a 14 day standard RSI with 30 days of pricing data
if(null==rsiCollection||0==rsiCollection.Count||rsiCollection[rsiCollection.Count-1].RSI<cmtParams.MinRSI)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Trend#8 Violation : rsiCollection[rsiCollection.Count-1].RSI<{0}.",cmtParams.MinRSI);
return cmtCandidate;
}
// Trend #3 check : check required days of increasing 200 day moving averages
DateGenerator dateGenerator=new DateGenerator();
List<double> dma200List=new List<double>();
DateTime historicalDate=dateGenerator.GenerateHistoricalDate(currentPrice.Date,cmtParams.DMA200Horizon+10);
List<DateTime> historicalDates=PricingDA.GetPricingDatesBetween(historicalDate,currentPrice.Date);
historicalDates=historicalDates.Take(cmtParams.DMA200Horizon).ToList();
if(historicalDates.Count<cmtParams.DMA200Horizon)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Trend#3a Violation : Insufficient data to calulate DMA(200). Requires {0} days of DMA(200)",cmtParams.DMA200Horizon);
return cmtCandidate;
}
foreach(DateTime date in historicalDates)
{
Prices historicalPrices=GBPriceCache.GetInstance().GetPrices(symbol,date,MovingAverageGenerator.DayCount200);
if(null==historicalPrices||historicalPrices.Count<MovingAverageGenerator.DayCount200) continue;
dma200Prices=MovingAverageGenerator.GenerateMovingAverage(historicalPrices,MovingAverageGenerator.DayCount200);
dma200List.Insert(0,dma200Prices[0].AVGPrice); // The lowest index should wind up with most historical price. This way we calculate the proper slope
}
double[] averages=dma200List.ToArray();
double slope=Numerics.Slope(ref averages);
if(slope<=0)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=String.Format("Trend#3 Violation : Slope of {0} days of 200DMA >0.",cmtParams.DMA200Horizon);
return cmtCandidate;
}
// Trend check ensure that prices are trending higher
if(cmtParams.UsePriceSlopeIndicator)
{
int dayCount=cmtParams.UsePriceSlopeIndicatorDays;
Prices pricesTrend=GBPriceCache.GetInstance().GetPrices(symbol,tradeDate,dayCount);
double[] pricesLow=Numerics.ToDouble(pricesTrend.GetPricesLow());
LeastSquaresResult lsr=LeastSquaresHelper.CalculateLeastSquares(pricesLow);
if(lsr.Slope<=0.00)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Price trend violation {0}. The {1} pricing slope is {2}",symbol,dayCount,Utility.FormatNumber(lsr.Slope,6));
return cmtCandidate;
}
}
// Filter penny stocks - don't trade anything less than $1.00
if(currentPrice.Close<1.00||currentPrice.Open<1.00)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Penny stock violation {0} Close price is {1}, Open price is ",symbol,Utility.FormatCurrency(currentPrice.Close),Utility.FormatCurrency(currentPrice.Open));
return cmtCandidate;
}
// Capture latest Volume - we'll do a min check later on
cmtCandidate.Volume=currentPrice.Volume;
// Daily Return Check
float[] dailyReturns=prices.GetReturns(); // First we build the returns (before we reverse the pricing direction)
if(HasReturnViolation(dailyReturns,cmtParams.DailyReturnLimit)) // Check the return stream. If any daily return exceeds DailyReturnLimit then we discard.
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Daily return violation for {0}. A daily return exceeded {1}%.",symbol,cmtParams.DailyReturnLimit);
return cmtCandidate;
}
// check for outliers in the return stream
if((from float value in dailyReturns where Math.Abs(value)>cmtParams.DailyReturnLimit select value).Count()>0)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Outlier encountered in return stream for {0}. Limit {1}",symbol,cmtParams.DailyReturnLimit);
return cmtCandidate;
}
// EBITDA screen
bool UseEBITDAScreen=true;
if(UseEBITDAScreen&&(double.IsNaN(fundamental.EBITDA)||fundamental.EBITDA<=0))
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#14 Violation : EBITDA";
return cmtCandidate;
}
bool UsePEScreen=true;
if(UsePEScreen&&(double.IsNaN(fundamental.PE))||fundamental.PE<=0.00)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#14 Violation : UsePEScreen";
return cmtCandidate;
}
// Setup for next tests
double profitMarginSlope=double.NaN;
DateTime minDate=DateTime.MinValue;
DateTime maxDate=DateTime.MinValue;
float[] values;
// Revenue per share screen
bool UseRevenuePerShareScreen=true;
if(UseRevenuePerShareScreen&&(double.IsNaN(fundamental.RevenuePerShare)||fundamental.RevenuePerShare<0.00))
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#14 Violation : Revenue Per Share";
return cmtCandidate;
}
// Trend#9 - My check Increasing EPS
double epsSlope=double.NaN;
if(companyProfile.IsEquity&&cmtParams.EPSCheck)
{
TimeSeriesCollection epsTimeSeries=FundamentalDA.GetEPS(symbol,currentPrice.Date);
if(null==epsTimeSeries||epsTimeSeries.Count<3)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#9 Violation : No EPS time series to perform check.";
return cmtCandidate;
}
epsTimeSeries=new TimeSeriesCollection(epsTimeSeries.Take(3).ToList());
minDate=epsTimeSeries.Min(x => x.AsOf);
maxDate=epsTimeSeries.Max(x => x.AsOf);
values=epsTimeSeries.ToFloat();
values=Numerics.Reverse(ref values);
epsSlope=Numerics.Slope(values);
if(epsSlope<=0)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#9 Violation : epsSlope<=0.";
return cmtCandidate;
}
}
// Trend#10 - My check - Increasing profit margin
if(companyProfile.IsEquity&&cmtParams.ProfitMarginCheck)
{
TimeSeriesCollection profitMarginTimeSeries=IncomeStatementDA.GetProfitMarginMaxAsOf(symbol,currentPrice.Date,IncomeStatement.PeriodType.Quarterly);
if(null==profitMarginTimeSeries||profitMarginTimeSeries.Count<3)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#10 Violation : No Profit Margin series to perform check.";
return cmtCandidate;
}
profitMarginTimeSeries=new TimeSeriesCollection(profitMarginTimeSeries.Take(3).ToList());
minDate=profitMarginTimeSeries.Min(x => x.AsOf);
maxDate=profitMarginTimeSeries.Max(x => x.AsOf);
values=profitMarginTimeSeries.ToFloat();
values=Numerics.Reverse(ref values);
profitMarginSlope=Numerics.Slope(values);
if(profitMarginSlope<=0)
{
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason="Trend#10 Violation : profitMarginSlope<=0.";
return cmtCandidate;
}
}
// Calculate the Score
prices.Reverse(); // Reverse the series here.
double[] logPrices=null;
logPrices=new double[prices.Count];
for(int index=0;index<prices.Count;index++)
{
Price price=prices[index];
logPrices[index]=Math.Log(price.Close);
}
LeastSquaresResultWithR2 leastSquaresResult=LeastSquaresHelper.CalculateLeastSquaresWithR2(logPrices);
cmtCandidate=new CMTCandidate();
cmtCandidate.EPSSlope=epsSlope;
cmtCandidate.PriceSlope=leastSquaresResult.Slope;
cmtCandidate.ProfitMarginSlope=profitMarginSlope;
cmtCandidate.Symbol=symbol;
cmtCandidate.AnalysisDate=tradeDate;
cmtCandidate.Slope=leastSquaresResult.Slope;
cmtCandidate.Volatility=volatility;
cmtCandidate.AnnualizedReturn=Math.Pow(Math.Exp(cmtCandidate.Slope),252); //cmCandidate.AnnualizedReturn=Math.Pow(1.00+cmCandidate.Slope,252);
if(cmtCandidate.Slope<0) cmtCandidate.AnnualizedReturn*=-1.00; // preserve the sign of the slope
cmtCandidate.Score=leastSquaresResult.RSquared*cmtCandidate.AnnualizedReturn; // The greater the score the higher the rank
cmtCandidate.RSquared=leastSquaresResult.RSquared;
cmtCandidate.Beta=BetaGenerator.Beta(symbol,tradeDate,cmtParams.BetaMonths);
cmtCandidate.BetaMonths=cmtParams.BetaMonths;
if(double.IsNaN(cmtCandidate.Beta))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Unable to calculate {0} month beta for {1} ",cmtParams.BetaMonths,symbol);
return cmtCandidate;
}
if(cmtCandidate.Beta<=0.00)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Beta for {0} is less than or equal to zero {1}",symbol,cmtCandidate.Beta);
return cmtCandidate;
}
if(cmtParams.UseMaxBeta&&cmtCandidate.Beta>cmtParams.MaxBeta)
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Beta for {0} exceeds maximum allowed. Candidate beta {1}, Max Beta:{2}",symbol,cmtCandidate.Beta,cmtParams.MaxBeta);
return cmtCandidate;
}
cmtCandidate.SharpeRatio=SharpeRatioGenerator.GenerateSharpeRatio(cmtCandidate.Symbol,tradeDate);
if(double.IsNaN(cmtCandidate.SharpeRatio))
{
cmtCandidate.Violation=true;
cmtCandidate.Reason=String.Format("Unable to calculate Sharpe Ratio for {0}",symbol);
return cmtCandidate;
}
return cmtCandidate;
}
catch(Exception exception)
{
MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString()));
cmtCandidate.Violation=true;
cmtCandidate.Symbol=symbol;
cmtCandidate.Reason=exception.ToString();
return cmtCandidate;
}
}
private static bool HasReturnViolation(float[] dailyReturns,double dailyReturnLimit)
{
foreach(float dailyReturn in dailyReturns) if(Math.Abs(dailyReturn)>dailyReturnLimit) return true;
return false;
}
}
}