Files
marketdata/MarketDataLib/Generator/CMMomentum/CMCandidateGenerator.cs
Sean 2882559651 Fix GetMonthlyPrices in Prices. It was not correctly returning monthly prices.
This was causing an issue in the SharpeRatioGenerator whereby the SharpeRatio was not being calculated correctly where the asof date would
fall on a weekend of holiday.
Also, added a ReasonCategory to the CMCanidate as well as a Violation summary line in the model output to show where violations occur.
Had I implemented this previously I might have detected the SharpeRatio issue sooner.
Also added GetFundamentalMaxDateTop in the FundamentalDA which I used during debugging but is not currently being used anywhere.
2025-02-02 14:59:14 -05:00

341 lines
18 KiB
C#

using MarketData.DataAccess;
using MarketData.Generator.Indicators;
using MarketData.Generator.MovingAverage;
using MarketData.MarketDataModel;
using MarketData.Numerical;
using MarketData.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
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();
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;
}
// 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("Already held.");
cmCandidate.ReasonCategory = String.Format("Already held.");
return 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;
}
// 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;
}
}
}