441 lines
20 KiB
C#
441 lines
20 KiB
C#
using MarketData.Cache;
|
|
using MarketData.DataAccess;
|
|
using MarketData.Generator.Indicators;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|