341 lines
19 KiB
C#
Executable File
341 lines
19 KiB
C#
Executable File
using MarketData.DataAccess;
|
|
using MarketData.Generator.Indicators;
|
|
using MarketData.Generator.MovingAverage;
|
|
using MarketData.MarketDataModel;
|
|
using MarketData.Numerical;
|
|
using MarketData.Utils;
|
|
|
|
namespace MarketData.Generator.CMMomentum
|
|
{
|
|
public class CMCandidateGenerator
|
|
{
|
|
private CMCandidateGenerator()
|
|
{
|
|
}
|
|
// *******************************************************************************************************************************************************************************
|
|
// ************************************************************************* G E N E R A T E C A N D I D A T E *****************************************************************
|
|
// *******************************************************************************************************************************************************************************
|
|
public static CMCandidate GenerateCandidate(String symbol, DateTime tradeDate, DateTime analysisDate, CMParams cmParams, List<String> symbolsHeld = null)
|
|
{
|
|
CMCandidate cmCandidate = new CMCandidate();
|
|
|
|
// No trade symbols
|
|
if (null!=cmParams.NoTradeSymbolsList&&cmParams.NoTradeSymbolsList.Any(x => x.Equals(symbol)))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Candidate in the No-Trade list.");
|
|
cmCandidate.ReasonCategory = String.Format("Candidate in the No-Trade list.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
// Check if the symbol is held in any open positions
|
|
if (null != symbolsHeld && symbolsHeld.Any(x => x.Equals(symbol, StringComparison.CurrentCultureIgnoreCase)))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Already held.");
|
|
cmCandidate.ReasonCategory = String.Format("Already held.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
Fundamental fundamental = FundamentalDA.GetFundamentalMaxDate(symbol, tradeDate);
|
|
if (null == fundamental)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("No fundamental.");
|
|
cmCandidate.ReasonCategory = String.Format("No fundamental.");
|
|
return cmCandidate;
|
|
}
|
|
if (!(fundamental.MarketCap >= cmParams.MarketCapLowerLimit))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("MarketCapLowerLimit constraint violation.");
|
|
cmCandidate.ReasonCategory = String.Format("MarketCapLowerLimit constraint violation.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
// Equity check
|
|
CompanyProfile companyProfile = CompanyProfileDA.GetCompanyProfile(symbol);
|
|
if (!companyProfile.IsEquity)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Candidate is not an equity.");
|
|
cmCandidate.ReasonCategory = String.Format("Candidate is not an equity.");
|
|
return cmCandidate;
|
|
}
|
|
// Moving Average check
|
|
Prices pricesDMA = PricingDA.GetPrices(symbol, tradeDate, cmParams.MovingAverageConstraintDays + 15);
|
|
if (null == pricesDMA || (cmParams.MovingAverageConstraintDays + 15) != pricesDMA.Count)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Insufficient pricing to generate moving average.");
|
|
cmCandidate.ReasonCategory = String.Format("Insufficient pricing to generate moving average.");
|
|
return cmCandidate;
|
|
}
|
|
DMAPrices dmaPrices = MovingAverageGenerator.GenerateMovingAverage(pricesDMA, cmParams.MovingAverageConstraintDays);
|
|
if (dmaPrices[0].CurrentPrice < dmaPrices[0].AVGPrice)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Current price is less than moving average.");
|
|
cmCandidate.ReasonCategory = String.Format("Current price is less than moving average.");
|
|
return cmCandidate;
|
|
}
|
|
Prices prices = PricingDA.GetPrices(symbol, tradeDate, cmParams.DayCount); // The prices come back with the most recent date in the lowest index. We want the earliest date in the lowest index
|
|
if (null == prices || cmParams.DayCount != prices.Count)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Insufficient pricing data.");
|
|
cmCandidate.ReasonCategory = String.Format("Insufficient pricing data.");
|
|
return cmCandidate;
|
|
}
|
|
// Filter penny stocks - don't trade anything less than $1.00
|
|
if (prices[0].Close < 1.00 || prices[0].Open < 1.00)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Penny stock violation {0} Close price is {1}, Open price is ", symbol, Utility.FormatCurrency(prices[0].Close), Utility.FormatCurrency(prices[0].Open));
|
|
cmCandidate.ReasonCategory = String.Format("Penny stock violation.");
|
|
return cmCandidate;
|
|
}
|
|
// Capture latest Volume - we'll do a min check later on
|
|
cmCandidate.Volume = prices[0].Volume;
|
|
// Liquidity check
|
|
if (((from Price price in prices where price.Volume < 10000 select price).Count()) > 1)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Liquidity check violation for {0}. Price volume must be above 10,000 ", symbol);
|
|
cmCandidate.ReasonCategory = String.Format("Liquidity check violation.");
|
|
return cmCandidate;
|
|
}
|
|
// OverExtended check
|
|
if(cmParams.UseOverExtendedIndicator)
|
|
{
|
|
bool? result=OverExtendedIndicator.IsOverextended(symbol,tradeDate,cmParams.UseOverExtendedIndicatorDays,cmParams.UseOverExtendedIndicatorViolationThreshhold,cmParams.UseOverExtendedIndicatorMarginPercent);
|
|
if(null!=result && true==result.Value)
|
|
{
|
|
cmCandidate.Violation=true;
|
|
cmCandidate.Reason=String.Format("OverExtended violation for {0}. The security is price overextended for TradeDate:{1}.",symbol,Utility.DateTimeToStringMMHDDHYYYY(tradeDate));
|
|
cmCandidate.ReasonCategory=String.Format("OverExtended violation. The security is price overextended.");
|
|
return cmCandidate;
|
|
}
|
|
}
|
|
// Momentum Check
|
|
if (HasReturnViolation(prices,cmParams.DailyReturnLimit)) // Check the return stream. If any daily return exceeds DailyReturnLimit then we discard.
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Daily return violation for {0}. A daily return exceeded {1}%.", symbol, cmParams.DailyReturnLimit);
|
|
cmCandidate.ReasonCategory = String.Format("Daily return violation. A daily return exceeded expectations.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
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);
|
|
cmCandidate = new CMCandidate();
|
|
cmCandidate.Symbol = symbol;
|
|
cmCandidate.AnalysisDate = analysisDate;
|
|
cmCandidate.TradeDate = tradeDate;
|
|
cmCandidate.DayCount = cmParams.DayCount;
|
|
cmCandidate.Slope = leastSquaresResult.Slope;
|
|
cmCandidate.AnnualizedReturn = Math.Pow(Math.Exp(cmCandidate.Slope), 252); //cmCandidate.AnnualizedReturn=Math.Pow(1.00+cmCandidate.Slope,252);
|
|
if (cmCandidate.Slope < 0) cmCandidate.AnnualizedReturn *= -1.00; // preserve the sign of the slope
|
|
cmCandidate.Score = leastSquaresResult.RSquared * cmCandidate.AnnualizedReturn; // The greater the score the higher the rank
|
|
cmCandidate.RSquared = leastSquaresResult.RSquared;
|
|
cmCandidate.Beta = BetaGenerator.Beta(symbol, tradeDate, cmParams.BetaMonths);
|
|
cmCandidate.BetaMonths = cmParams.BetaMonths;
|
|
if (double.IsNaN(cmCandidate.Beta))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Unable to calculate {0} month beta for {1} ", cmParams.BetaMonths, symbol);
|
|
cmCandidate.ReasonCategory = String.Format("Beta cannot be calculated.");
|
|
return cmCandidate;
|
|
}
|
|
if (cmCandidate.Beta <= 0.00)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Beta for {0} is less than or equal to zero {1}", symbol, cmCandidate.Beta);
|
|
cmCandidate.ReasonCategory = String.Format("Beta is less than or equal to zero.");
|
|
return cmCandidate;
|
|
}
|
|
if (cmParams.UseMaxBeta && cmCandidate.Beta > cmParams.MaxBeta)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Beta for {0} exceeds maximum allowed. Candidate beta {1}, Max Beta:{2}", symbol, cmCandidate.Beta,cmParams.MaxBeta);
|
|
cmCandidate.ReasonCategory = String.Format("Beta exceeds maximum allowed.");
|
|
return cmCandidate;
|
|
}
|
|
cmCandidate.SharpeRatio = SharpeRatioGenerator.GenerateSharpeRatio(cmCandidate.Symbol, tradeDate);
|
|
if (double.IsNaN(cmCandidate.SharpeRatio))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Unable to calculate Sharpe Ratio for {0}", symbol);
|
|
cmCandidate.ReasonCategory = String.Format("Unable to calculate Sharpe Ratio.");
|
|
return cmCandidate;
|
|
}
|
|
return cmCandidate;
|
|
}
|
|
// *******************************************************************************************************************************************************************************
|
|
// ************************************************************* G E N E R A T E C A N D I D A T E F O R F A L L B A C K ***************************************************
|
|
// *******************************************************************************************************************************************************************************
|
|
public static CMCandidate GenerateCandidateForFallback(String symbol, DateTime tradeDate, DateTime analysisDate, CMParams cmParams, List<String> symbolsHeld = null)
|
|
{
|
|
CMCandidate cmCandidate = new CMCandidate();
|
|
|
|
// Check MarketCap
|
|
Fundamental fundamental = FundamentalDA.GetFundamentalMaxDate(symbol, tradeDate);
|
|
if (null == fundamental)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("No fundamental for {0}.", symbol);
|
|
cmCandidate.ReasonCategory = String.Format("No fundamental.");
|
|
return cmCandidate;
|
|
}
|
|
if (!(fundamental.MarketCap >= cmParams.MarketCapLowerLimit))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("MarketCapLowerLimit constraint violation for {0}.", symbol);
|
|
cmCandidate.ReasonCategory = String.Format("MarketCapLowerLimit constraint violation.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
// Check if the symbol is held in any open positions
|
|
if (null != symbolsHeld && symbolsHeld.Any(x => x.Equals(symbol)))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("{0} is already held.", symbol);
|
|
cmCandidate.ReasonCategory = String.Format("Candidate already held.");
|
|
return cmCandidate;
|
|
}
|
|
Prices prices = PricingDA.GetPrices(symbol, tradeDate, cmParams.DayCount); // The prices come back with the most recent date in the lowest index. We want the earliest date in the lowest index
|
|
if (null == prices || cmParams.DayCount != prices.Count)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Insufficient pricing data for {0}. Required {1} days.", symbol, cmParams.DayCount);
|
|
cmCandidate.ReasonCategory = String.Format("Insufficient pricing data.");
|
|
return cmCandidate;
|
|
}
|
|
cmCandidate.Volume = prices[0].Volume;
|
|
|
|
// Momentum Check
|
|
if (HasReturnViolation(prices, cmParams.DailyReturnLimit))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Daily return violation for {0}. A daily return exceeded {1}%.", symbol, cmParams.DailyReturnLimit);
|
|
cmCandidate.ReasonCategory = String.Format("Daily return violation. A daily return exceeded the limit.");
|
|
return cmCandidate;
|
|
}
|
|
|
|
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);
|
|
cmCandidate = new CMCandidate();
|
|
cmCandidate.Symbol = symbol;
|
|
cmCandidate.AnalysisDate = analysisDate;
|
|
cmCandidate.TradeDate = tradeDate;
|
|
cmCandidate.DayCount = cmParams.DayCount;
|
|
cmCandidate.Slope = leastSquaresResult.Slope;
|
|
cmCandidate.AnnualizedReturn = Math.Pow(Math.Exp(cmCandidate.Slope), 252); //cmCandidate.AnnualizedReturn=Math.Pow(1.00+cmCandidate.Slope,252);
|
|
if (cmCandidate.Slope < 0) cmCandidate.AnnualizedReturn *= -1.00; // preserve the sign of the slope
|
|
cmCandidate.Score = leastSquaresResult.RSquared * cmCandidate.AnnualizedReturn; // The greater the score the higher the rank
|
|
cmCandidate.RSquared = leastSquaresResult.RSquared;
|
|
cmCandidate.Beta = BetaGenerator.Beta(symbol,cmParams.BetaMonths); // generate a 90 day Beta
|
|
cmCandidate.BetaMonths = cmParams.BetaMonths;
|
|
if (double.IsNaN(cmCandidate.Beta))
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Unable to calculate {0} month beta for {1} ", cmParams.BetaMonths, symbol);
|
|
cmCandidate.ReasonCategory = String.Format("Beta could not be calculated.");
|
|
return cmCandidate;
|
|
}
|
|
if (cmCandidate.Beta <= 0.00)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Beta for {0} is less than or equal to zero {1}", symbol, cmCandidate.Beta);
|
|
cmCandidate.ReasonCategory = String.Format("Beta is less than or equal to zero.");
|
|
return cmCandidate;
|
|
}
|
|
if (cmParams.UseMaxBeta && cmCandidate.Beta > cmParams.MaxBeta)
|
|
{
|
|
cmCandidate.Violation = true;
|
|
cmCandidate.Reason = String.Format("Beta for {0} exceeds maximum allowed. Candidate beta {1}, Max Beta:{2}", symbol, cmCandidate.Beta, cmParams.MaxBeta);
|
|
cmCandidate.ReasonCategory = String.Format("Beta exceeds maximum allowed.");
|
|
return cmCandidate;
|
|
}
|
|
cmCandidate.SharpeRatio = SharpeRatioGenerator.GenerateSharpeRatio(cmCandidate.Symbol, tradeDate);
|
|
if (double.IsNaN(cmCandidate.SharpeRatio)) cmCandidate.SharpeRatio = 0.00; // don't care about this for the fallback candidate
|
|
return cmCandidate;
|
|
}
|
|
// Given the list of candidates select the one candidate with the most favorable slope as an absolute fallback
|
|
// *******************************************************************************************************************************************************************************
|
|
// ************************************************************ G E N E R A T E C A N D I D A T E O F L A S T R E S O R T ***************************************************
|
|
// *******************************************************************************************************************************************************************************
|
|
public static CMCandidate GetFallbackCandidateOfLastResort(List<String> candidates, DateTime tradeDate, DateTime analysisDate, CMParams cmParams)
|
|
{
|
|
CMCandidates cmCandidates = new CMCandidates();
|
|
foreach (String candidate in candidates)
|
|
{
|
|
Prices prices = PricingDA.GetPrices(candidate, tradeDate, cmParams.DayCount); // The prices come back with the most recent date in the lowest index. We want the earliest date in the lowest index
|
|
if (null == prices || cmParams.DayCount != prices.Count) continue;
|
|
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);
|
|
CMCandidate cmCandidate = new CMCandidate();
|
|
cmCandidate = new CMCandidate();
|
|
cmCandidate.Symbol = candidate;
|
|
cmCandidate.AnalysisDate = analysisDate;
|
|
cmCandidate.TradeDate = tradeDate;
|
|
cmCandidate.DayCount = cmParams.DayCount;
|
|
cmCandidate.Slope = leastSquaresResult.Slope;
|
|
cmCandidate.AnnualizedReturn = Math.Pow(Math.Exp(cmCandidate.Slope), 252); //cmCandidate.AnnualizedReturn=Math.Pow(1.00+cmCandidate.Slope,252);
|
|
if (cmCandidate.Slope < 0) cmCandidate.AnnualizedReturn *= -1.00; // preserve the sign of the slope
|
|
cmCandidate.Score = leastSquaresResult.RSquared * cmCandidate.AnnualizedReturn; // The greater the score the higher the rank
|
|
cmCandidate.RSquared = leastSquaresResult.RSquared;
|
|
cmCandidate.Beta = BetaGenerator.Beta(candidate,tradeDate,cmParams.BetaMonths);
|
|
cmCandidate.BetaMonths = cmParams.BetaMonths;
|
|
if (cmCandidate.Beta <= 0) cmCandidate.Beta = .01;
|
|
cmCandidate.SharpeRatio = SharpeRatioGenerator.GenerateSharpeRatio(cmCandidate.Symbol, tradeDate);
|
|
if (double.IsNaN(cmCandidate.SharpeRatio)) cmCandidate.SharpeRatio = 0.00; // don't care about this for the fallback candidate
|
|
cmCandidates.Add(cmCandidate);
|
|
}
|
|
return (from CMCandidate cmCandidate in cmCandidates select cmCandidate).OrderByDescending(x => x.Slope).ToList().Take(1).FirstOrDefault();
|
|
}
|
|
//private static bool HasReturnViolation(float[] dailyReturns,double dailyReturnLimit)
|
|
//{
|
|
// foreach (float dailyReturn in dailyReturns) if (dailyReturn > dailyReturnLimit) return true;
|
|
// return false;
|
|
//}
|
|
|
|
private static bool HasReturnViolation(Prices prices, double dailyReturnLimit)
|
|
{
|
|
float[] dailyReturns = prices.GetReturns();
|
|
int index=0;
|
|
|
|
String symbol = prices.Take(1).First().Symbol;
|
|
for(;index<dailyReturns.Length;index++)
|
|
{
|
|
if (Math.Abs(dailyReturns[index]) > dailyReturnLimit)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|