diff --git a/MarketDataLib/Generator/MGSHMomentum/CandidateSelector.cs b/MarketDataLib/Generator/MGSHMomentum/CandidateSelector.cs new file mode 100644 index 0000000..ae7bb99 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/CandidateSelector.cs @@ -0,0 +1,95 @@ +using MarketData.DataAccess; +using MarketData.MarketDataModel; +using MarketData.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MarketData.Generator.MGSHMomentum +{ + public class CandidateSelector + { + private static int DAY_COUNT=252; + private CandidateSelector() + { + } + public static String SelectBestCandidateSymbol(MGSHQualityIndicator qualityIndicator,List candidates,String defaultCandidate,DateTime analysisDate) + { + String bestCandidate=defaultCandidate; + try + { + if(null==candidates||0==candidates.Count||Utility.IsEpoch(analysisDate)||null==defaultCandidate)return bestCandidate; + List qualityIndicatorCandidates=new List(); + foreach(String candidate in candidates) + { + Prices prices=PricingDA.GetPrices(candidate,analysisDate,DAY_COUNT); + if(null==prices||0==prices.Count)continue; + QualityIndicatorCandidate qualityIndicatorCandidate=new QualityIndicatorCandidate(); + qualityIndicatorCandidate.Symbol=candidate; + qualityIndicatorCandidate.IDIndicator=IDIndicator.Calculate(prices); + qualityIndicatorCandidate.Score=ScoreIndicator.Calculate(prices); + qualityIndicatorCandidates.Add(qualityIndicatorCandidate); + } + if(0==qualityIndicatorCandidates.Count)return bestCandidate; + if(qualityIndicator.Quality.Equals(MGSHQualityIndicator.QualityType.IDIndicator)) + { + qualityIndicatorCandidates=(from QualityIndicatorCandidate qualityIndicatorCandidate in qualityIndicatorCandidates orderby qualityIndicatorCandidate.IDIndicator ascending select qualityIndicatorCandidate).ToList(); + } + else + { + qualityIndicatorCandidates=(from QualityIndicatorCandidate qualityIndicatorCandidate in qualityIndicatorCandidates orderby qualityIndicatorCandidate.Score descending select qualityIndicatorCandidate).ToList(); + } + return qualityIndicatorCandidates.Take(1).FirstOrDefault().Symbol; + } + catch(Exception exception) + { + MDTrace.WriteLine(String.Format("{0}",exception.ToString())); + return bestCandidate; + } + } + public static QualityIndicatorCandidate SelectBestCandidate(MGSHQualityIndicator qualityIndicator,List candidates,String defaultCandidate,DateTime analysisDate) + { + QualityIndicatorCandidate bestCandidate=new QualityIndicatorCandidate(); + try + { + if(null==candidates||0==candidates.Count||Utility.IsEpoch(analysisDate)||null==defaultCandidate)return null; + List qualityIndicatorCandidates=new List(); + foreach(String candidate in candidates) + { + Prices prices=PricingDA.GetPrices(candidate,analysisDate,DAY_COUNT); + Fundamental fundamental=FundamentalDA.GetFundamentalMaxDate(candidate,analysisDate); + if(null==prices||0==prices.Count)continue; + QualityIndicatorCandidate qualityIndicatorCandidate=new QualityIndicatorCandidate(); + qualityIndicatorCandidate.Symbol=candidate; + qualityIndicatorCandidate.IDIndicator=IDIndicator.Calculate(prices); + qualityIndicatorCandidate.Score=ScoreIndicator.Calculate(prices); + qualityIndicatorCandidate.CumReturn252=prices.GetCumulativeReturn(); + qualityIndicatorCandidate.DayCount=DAY_COUNT; + qualityIndicatorCandidate.Return1D=prices.GetReturn1D(); + if(null!=fundamental) + { + qualityIndicatorCandidate.PE=fundamental.PE; + qualityIndicatorCandidate.Beta=fundamental.Beta; + } + qualityIndicatorCandidates.Add(qualityIndicatorCandidate); + } + if(0==qualityIndicatorCandidates.Count)return bestCandidate; + + if(qualityIndicator.Quality.Equals(MGSHQualityIndicator.QualityType.IDIndicator)) + { + qualityIndicatorCandidates=(from QualityIndicatorCandidate qualityIndicatorCandidate in qualityIndicatorCandidates orderby qualityIndicatorCandidate.IDIndicator ascending select qualityIndicatorCandidate).ToList(); + } + else + { + qualityIndicatorCandidates=(from QualityIndicatorCandidate qualityIndicatorCandidate in qualityIndicatorCandidates orderby qualityIndicatorCandidate.Score descending select qualityIndicatorCandidate).ToList(); + } + return qualityIndicatorCandidates.Take(1).FirstOrDefault(); + } + catch(Exception exception) + { + MDTrace.WriteLine(String.Format("{0}",exception.ToString())); + return bestCandidate; + } + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/CandidateViolation.cs b/MarketDataLib/Generator/MGSHMomentum/CandidateViolation.cs new file mode 100644 index 0000000..21efc88 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/CandidateViolation.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace MarketData.Generator.MGSHMomentum +{ + public class CandidateViolation + { + public CandidateViolation(String symbol, String reasonCategory) + { + Symbol = symbol; + ReasonCategory = reasonCategory; + } + + public String Symbol {get; set;} + public String ReasonCategory {get; set;} + } + + public class CandidateViolations : List + { + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/HedgeManager.cs b/MarketDataLib/Generator/MGSHMomentum/HedgeManager.cs new file mode 100644 index 0000000..fd66573 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/HedgeManager.cs @@ -0,0 +1,427 @@ +using System; +using System.Collections.Generic; +using MarketData.MarketDataModel; +using MarketData.Utils; +using MarketData.Cache; +using MarketData.Numerical; +using MarketData.Generator.ModelGenerators; +using StopLimit=MarketData.Generator.Model.StopLimit; + +namespace MarketData.Generator.MGSHMomentum +{ + public class HedgeManager + { + private static int PRICING_DAYS = 120; + + public HedgeManager() + { + } + + public String HedgeShortSymbol { get; set; } = "SH"; + + public bool Verbose {get;set;} = false; + + private void Display(String message) + { + if(Verbose) + { + MDTrace.WriteLine(LogLevel.DEBUG,message); + } + } + + public bool IsOpenHedgeIndicator(DateTime analysisDate, int hedgeCloseGreaterSMANDays=10, int hedgeBandBreakCheckDays=3) + { + Prices prices=GBPriceCache.GetInstance().GetPrices(HedgeShortSymbol, analysisDate, PRICING_DAYS); + + if(null==prices || !prices[0].Date.Date.Equals(analysisDate.Date)) + { + Display(String.Format("Cannot evaluate IsOpenHedgeIndicator for candidate {0} due to lack of current price on {1}", + HedgeShortSymbol,analysisDate.ToShortDateString())); + return false; + } + + BollingerBands bollingerBands=BollingerBandGenerator.GenerateBollingerBands(prices); // K is upper band, KL1 below that. L is lower band, LP1 above that + BollingerBandElementsByDate bollingerBandElementsByDate = bollingerBands.GetBollingerBandElementsByDate(); + if(!bollingerBandElementsByDate.ContainsKey(analysisDate)) + { + Display(String.Format("Cannot evaluate IsOpenHedgeIndicator for candidate {0} due to lack of current price on {1}",HedgeShortSymbol,analysisDate.ToShortDateString())); + return false; + } + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[analysisDate]; + + if(bollingerBandElement.Close < bollingerBandElement.L ) + { + Display(String.Format($"IsOpenHedgeIndicator AnalysisDate:{analysisDate.ToShortDateString()}")); + + SlopeManager slopeManager = new SlopeManager(bollingerBandElementsByDate, analysisDate){Verbose=Verbose}; + + slopeManager.DisplaySlopes(); + + if(!CloseGreaterSMAN(analysisDate, bollingerBandElementsByDate, hedgeCloseGreaterSMANDays)) + { + return false; + } + + if(!BandBreakCheck(analysisDate, bollingerBandElementsByDate, hedgeBandBreakCheckDays)) // 3 + { + return false; + } + + int bucketDays=5; + int closeLessL=GetCloseLessL(analysisDate, bollingerBandElementsByDate, bucketDays); + int closeBetweenLAndLP1=GetCloseBetweenLAndLP1(analysisDate, bollingerBandElementsByDate, bucketDays); + int closeBetweenLP1AndSMAN=GetCloseBetweenLP1AndSMAN(analysisDate, bollingerBandElementsByDate, bucketDays); + int closeBetweenSMANAndKL1=GetCloseBetweenSMANAndKL1(analysisDate, bollingerBandElementsByDate, bucketDays); + int closeBetweenKL1AndK=GetCloseBetweenKL1AndK(analysisDate, bollingerBandElementsByDate, bucketDays); + int closeGreaterK=GetCloseGreaterK(analysisDate, bollingerBandElementsByDate, bucketDays); + Display($"Close buckets K:{closeGreaterK} KLSpread:{slopeManager.GetKLSpread()}"); + +// if the close is spending a lot of time between L and LP1 then make sure the overall volatility spread is widening sufficiently in the 5, 10, 30 day terms + if(closeBetweenLAndLP1 >= bucketDays && !slopeManager.IsMatchKLSpread("??+++")) + { + return false; + } + + + Display(String.Format("Lower band break detected for {0} on {1} Price Close:{2} LP1:{3} ", + HedgeShortSymbol, + analysisDate.ToShortDateString(), + Utility.FormatCurrency(bollingerBandElement.Close), + Utility.FormatCurrency(bollingerBandElement.L) + )); + Display($"TRUE"); + return true; + } + return false; + } + + private int GetCloseLessL(DateTime analysisDate,BollingerBandElementsByDate bollingerBandElementsByDate, int days) + { + int count=0; + DateGenerator dateGenerator = new DateGenerator(); + List dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close < bollingerBandElement.L)count++; + } + return count; + } + + private int GetCloseBetweenLAndLP1(DateTime analysisDate,BollingerBandElementsByDate bollingerBandElementsByDate, int days) + { + int count=0; + DateGenerator dateGenerator = new DateGenerator(); + List dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close >= bollingerBandElement.L && bollingerBandElement.Close dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close >= bollingerBandElement.LP1 && bollingerBandElement.Close dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close >= bollingerBandElement.SMAN && bollingerBandElement.Close dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close >= bollingerBandElement.KL1 && bollingerBandElement.Close dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Close > bollingerBandElement.K )count++; + } + return count; + } + + private bool BandBreakCheck(DateTime analysisDate,BollingerBandElementsByDate bollingerBandElementsByDate, int days) + { + int lowLessLP1=0; + int closeLessLP1=0; + DateGenerator dateGenerator = new DateGenerator(); + List dates = dateGenerator.GenerateHistoricalDates(analysisDate, days +1); + dates.RemoveAt(0); + foreach(DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if(bollingerBandElement.Low <= bollingerBandElement.LP1)lowLessLP1++; + if(bollingerBandElement.Close <= bollingerBandElement.LP1)closeLessLP1++; + } + return (lowLessLP1 >= days) && (closeLessLP1 >= days); + } + + private bool CloseGreaterSMAN(DateTime analysisDate, BollingerBandElementsByDate bollingerBandElementsByDate, int days) + { + DateGenerator dateGenerator = new DateGenerator(); + List dates = dateGenerator.GenerateHistoricalDates(analysisDate, days + 1); + dates.RemoveAt(0); + foreach (DateTime date in dates) + { + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[date]; + if (bollingerBandElement.Close > bollingerBandElement.SMAN) return true; + } + return false; + } + + + /// + /// This is used to trigger an adjustment to the stop price. + /// + /// + /// + /// + public bool IsLowerBandBreakIndicator(DateTime analysisDate, MGSHPosition position) + { + BollingerBandElementsByDate bollingerBandElementsByDate = GetBollingerBandElementsByDate(analysisDate); + if (null == bollingerBandElementsByDate) return false; + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[analysisDate]; + + if (bollingerBandElement.High > bollingerBandElement.K) + { + Display(String.Format("+++ [1] Upper band break detected for {0} on {1} Purchase Date:{2} Purchase Price:{3} Price Close:{4} Price High:{5} Price High(hF):{6} KL1:{7}", + HedgeShortSymbol, + analysisDate.ToShortDateString(), + position.PurchaseDate.ToShortDateString(), + Utility.FormatCurrency(position.PurchasePrice), + Utility.FormatCurrency(bollingerBandElement.Close), + Utility.FormatCurrency(bollingerBandElement.High), + Utility.FormatCurrency(bollingerBandElement.High), + Utility.FormatCurrency(bollingerBandElement.K) + )); + return true; + } + return false; + } + + public StopLimit EvaluateStopPriceHedge(DateTime analysisDate, MGSHPosition position, int hedgeStopLimitScalingVolatilityDays, int hedgeMinDaysBetweenStopAdjustments) + { + DateGenerator dateGenerator=new DateGenerator(); + Price currentPrice=GBPriceCache.GetInstance().GetPrice(position.Symbol,analysisDate); + double trailingStop=position.InitialStopLimit+Math.Floor((currentPrice.Low-position.PurchasePrice)/position.R)*position.R; + double trailingStopScaled=trailingStop; + double daysHeld=Math.Abs(dateGenerator.DaysBetweenActual(position.PurchaseDate,analysisDate)); + + if(Utility.IsEpoch(position.LastStopAdjustment)) // we've never adjusted the stop price + { + BollingerBandElementsByDate bollingerBandElementsByDate = GetBollingerBandElementsByDate(analysisDate); + if(null == bollingerBandElementsByDate)return null; + trailingStopScaled=GetHedgeStopLimitWithScalingAverageTrueRange(analysisDate,currentPrice,position,trailingStop, hedgeStopLimitScalingVolatilityDays); + trailingStop=Math.Max(trailingStop,trailingStopScaled); + if(trailingStop>=currentPrice.Low||trailingStop>=currentPrice.Close) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be higher than the Low/Close of {2}.", + position.Symbol, + Utility.FormatCurrency(trailingStop), + Utility.FormatCurrency(currentPrice.Low))); + return null; + } + if(Numerics.Round(trailingStop).Equals(Numerics.Round(position.TrailingStopLimit))) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be the same as the existing stop limit of {2}.", + position.Symbol, + Utility.FormatCurrency(trailingStop), + Utility.FormatCurrency(position.TrailingStopLimit))); + return null; + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************** A D J U S T S T O P L I M I T ************************")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Adjusting StopLimit on {0} after {1} days for {2} from {3} to {4}. Purchase Price: {5} Current Price:{6} Spread:{7} Shares:{8}", + analysisDate.ToShortDateString(), + daysHeld, + position.Symbol, + Utility.FormatCurrency(position.TrailingStopLimit), + Utility.FormatCurrency(trailingStop), + Utility.FormatCurrency(position.PurchasePrice), + Utility.FormatCurrency(currentPrice.Close), + Utility.FormatPercent((currentPrice.Close-trailingStop)/trailingStop), + Utility.FormatNumber(position.Shares,2))); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("*****************************************************************************************")); + StopLimit newStopLimit=new StopLimit + { + Symbol=position.Symbol, + AnalysisDate=analysisDate, + PreviousStop=0.00==position.TrailingStopLimit?position.InitialStopLimit:position.TrailingStopLimit, + NewStop=trailingStop, + CurrentPriceLow=currentPrice.Low, + CurrentPriceClose=currentPrice.Close, + PriceTrendIndicatorSlope=0.00 + }; + position.TrailingStopLimit=trailingStop; + position.LastStopAdjustment=analysisDate; + return newStopLimit; + + } + else // we have already made prior stop adjustments + { + int daysSinceLastStopAdjustment=Math.Abs(dateGenerator.DaysBetweenActual(position.LastStopAdjustment,analysisDate)); + if(daysSinceLastStopAdjustment>=hedgeMinDaysBetweenStopAdjustments) + { + trailingStopScaled=GetHedgeStopLimitWithScalingAverageTrueRange(analysisDate,currentPrice,position,trailingStop,daysSinceLastStopAdjustment); + trailingStop=Math.Max(trailingStop,trailingStopScaled); + if(trailingStop>=currentPrice.Low||trailingStop>=currentPrice.Close) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be higher than the Low/Close of {2}.",position.Symbol,Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(currentPrice.Low))); + return null; + } + if(Numerics.Round(position.TrailingStopLimit) < Numerics.Round(trailingStop)) // round the stop limits to fractionals don't look like differences + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************** A D J U S T S T O P L I M I T ************************")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Adjusting StopLimit on {0} after {1} days for {2} from {3} to {4}. Purchase Price: {5} Current Price:{6} Spread:{7} Shares:{8}", + analysisDate.ToShortDateString(), + daysHeld, + position.Symbol, + Utility.FormatCurrency(position.TrailingStopLimit), + Utility.FormatCurrency(trailingStop), + Utility.FormatCurrency(position.PurchasePrice), + Utility.FormatCurrency(currentPrice.Close), + Utility.FormatPercent((currentPrice.Close-trailingStop)/trailingStop), + Utility.FormatNumber(position.Shares,2))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************************************************************************")); + StopLimit newStopLimit=new StopLimit + { + Symbol=position.Symbol, + AnalysisDate=analysisDate, + PreviousStop=0.00==position.TrailingStopLimit?position.InitialStopLimit:position.TrailingStopLimit, + NewStop=trailingStop, + CurrentPriceLow=currentPrice.Low, + CurrentPriceClose=currentPrice.Close, + PriceTrendIndicatorSlope=0.00 + }; + position.TrailingStopLimit=trailingStop; + position.LastStopAdjustment=analysisDate; + return newStopLimit; + } + else + { + double currentTrailingStopLimit=Numerics.Round(position.TrailingStopLimit); + double suggestedTrailingStopLimit=Numerics.Round(trailingStop); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Not Adjusting Stop Limit for {0} because the new trailing stop limit would be less than or equal to the current trailing stop limit. Current Trailing Stop Limit:{1}, Calculated Trailing Stop Limit:{2}.", + position.Symbol, + Utility.FormatCurrency(currentTrailingStopLimit), + Utility.FormatCurrency(suggestedTrailingStopLimit))); + return null; + } + } + else + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The trailing stop for {0} was adjusted {1} days ago. MinDaysBetweenStopAdjustments is {2}", + position.Symbol, + daysSinceLastStopAdjustment, + hedgeMinDaysBetweenStopAdjustments)); + return null; + } + } + } + + private double GetHedgeStopLimitWithScalingAverageTrueRange(DateTime analysisDate,Price currentPrice,MGSHPosition position,double stopLimitNonScaled, int stopLimitScalingVolatilityDays) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("GetHedgeStopLimitWithScalingAverageTrueRange: Symbol:{0}",position.Symbol)); + double volatility=double.NaN; + + volatility=VolatilityGenerator.CalculateVolatility(position.Symbol,analysisDate,stopLimitScalingVolatilityDays, 1.00); + if(double.IsNaN(volatility)) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Unable to calculate AverageTrueRange for {0} on {1}. Using non-scaled stop limit.",position.Symbol,analysisDate.ToShortDateString())); + return stopLimitNonScaled; + } + double stopLimit = ((currentPrice.High + currentPrice.Low)/2.00) - volatility; // We base the stop off of the midpoint of the high and the low + return stopLimit; + } + + private BollingerBandElementsByDate GetBollingerBandElementsByDate(DateTime analysisDate) + { + Prices prices=GBPriceCache.GetInstance().GetPrices(HedgeShortSymbol, analysisDate, PRICING_DAYS); + if(null==prices||!prices[0].Date.Date.Equals(analysisDate.Date)) + { + Display(String.Format("Cannot evaluate IsHedgeCloseIndicatorOn for candidate {0} due to lack of current price on {1}", + HedgeShortSymbol,analysisDate.ToShortDateString())); + return null; + } + BollingerBands bollingerBands=BollingerBandGenerator.GenerateBollingerBands(prices); // K is upper band, KL1 below that. L is lower band, LP1 above that + BollingerBandElementsByDate bollingerBandElementsByDate = bollingerBands.GetBollingerBandElementsByDate(); + if(!bollingerBandElementsByDate.ContainsKey(analysisDate)) + { + Display(String.Format("Cannot evaluate IsHedgeCloseIndicatorOn for candidate {0} due to lack of current price on {1}", + HedgeShortSymbol,analysisDate.ToShortDateString())); + return null; + } + return bollingerBandElementsByDate; + } + + public void DisplayBandSlopes(DateTime analysisDate) + { + Display($"***** B A N D S L O P E S {analysisDate.ToShortDateString()} *****"); + + Prices prices=GBPriceCache.GetInstance().GetPrices(HedgeShortSymbol, analysisDate, PRICING_DAYS); + if(null==prices||!prices[0].Date.Date.Equals(analysisDate.Date)) + { + Display(String.Format("Cannot evaluate IsHedgeCloseIndicatorOn for candidate {0} due to lack of current price on {1}", + HedgeShortSymbol,analysisDate.ToShortDateString())); + return; + } + BollingerBands bollingerBands=BollingerBandGenerator.GenerateBollingerBands(prices); // K is upper band, KL1 below that. L is lower band, LP1 above that + BollingerBandElementsByDate bollingerBandElementsByDate = bollingerBands.GetBollingerBandElementsByDate(); + if(!bollingerBandElementsByDate.ContainsKey(analysisDate)) + { + Display(String.Format("Cannot evaluate IsHedgeCloseIndicatorOn for candidate {0} due to lack of current price on {1}", + HedgeShortSymbol,analysisDate.ToShortDateString())); + return; + } + + BollingerBandElement bollingerBandElement = bollingerBandElementsByDate[analysisDate]; + + SlopeManager slopeManager = new SlopeManager(bollingerBandElementsByDate, analysisDate){Verbose=true}; + + Display("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + slopeManager.DisplaySlopes(); + Display("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + slopeManager.DisplaySlopesNumeric(); + Display("++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++"); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/IDIndicator.cs b/MarketDataLib/Generator/MGSHMomentum/IDIndicator.cs new file mode 100644 index 0000000..3d7d7e2 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/IDIndicator.cs @@ -0,0 +1,52 @@ +using System; +using MarketData.MarketDataModel; +using System.Linq; + +namespace MarketData.Generator.MGSHMomentum +{ + public class QualityIndicatorCandidate + { + public QualityIndicatorCandidate() + { + } + public String Symbol{get;set;} + public double CumReturn252{get;set;} + public double IDIndicator{get;set;} + public double Score{get;set;} + public int DayCount{get;set;} + public double PE{get;set;} + public double Beta{get;set;} + public double Return1D{get;set;} + } +// ********************************************************************************************************* + public class IDIndicator + { + private IDIndicator() + { + } +//CountOf CountOf +//Negative Positive Return Sign IDIndicator +// 50 202 0.2 1 -60.31746032 +// 100 152 0.2 1 -20.63492063 +// 0 252 0.1 1 -100 +// 0 252 0.2 1 -100 +// The lower the IDIndicator the better +// This calculator deviates from the original by using an exponential decay on the stream of returns such that weigh distant returns a bit less than current returns +// and thereby attempt to give a more accurate picture of the quality of current returns. + public static double Calculate(Prices prices) + { + double idIndicator=0.00; + double cumulativeReturn=0.00; + float[] returns=prices.GetReturns(); + ExponentialDecay exponentialDecay=new ExponentialDecay(); + exponentialDecay.Prime(returns,2.00); + for(int index=0;index0.00 select value).Count(); + double negativeCount=(from float value in returns where value<0.00 select value).Count(); + for(int index=0;index0.00?1.00:-1.00; + idIndicator=sign*((negativeCount/returns.Length)*100.00-(positiveCount/returns.Length)*100.00); + return idIndicator; + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHActivePositions.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHActivePositions.cs new file mode 100644 index 0000000..9f9b85c --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHActivePositions.cs @@ -0,0 +1,186 @@ +using MarketData.Utils; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHActivePositions : Dictionary + { + public MGSHActivePositions() + { + } + + public int GetCount() + { + int positionCount=0; + List keys = new List(Keys); + for(int index=0;index positions=this[keys[index]]; + if(null==positions||0==positions.Count)continue; + positionCount+=positions.Count; + } + return positionCount; + } + + public double GetExposure() + { + double exposure=0.00; + List keys = new List(Keys); + for(int index=0;index positions=this[keys[index]]; + if(null==positions||0==positions.Count)continue; + exposure+=(from MGSHPosition position in positions select position.Exposure).Sum(); + } + return exposure; + } + public List GetSymbols() + { + return SymbolsHeld(); + } + public MGSHPosition FindPosition(String symbol,DateTime purchaseDate) + { + MGSHPosition foundPosition=null; + List keys=new List(this.Keys); + for(int index=0;index keys=new List(this.Keys); + for(int index=0;index keys = new List(this.Keys); + for (int index = 0; index < keys.Count; index++) { + MGSHPositions positions = this[keys[index]]; + foreach (MGSHPosition slotPosition in positions) { + if (slotPosition == searchPosition) { + positions.Remove(searchPosition); + return true; + } + } + } + return false; + } + + public MGSHPositions GetPositions() + { + MGSHPositions positionsCollection=new MGSHPositions(); + List keys=new List(this.Keys); + Dictionary symbols=new Dictionary(); + for(int index=0;index SymbolsHeld() + { + List keys = new List(this.Keys); + Dictionary symbols = new Dictionary(); + for (int index = 0; index < keys.Count; index++) + { + MGSHPositions positions = this[keys[index]]; + foreach (MGSHPosition position in positions) + { + if (!symbols.ContainsKey(position.Symbol)) symbols.Add(position.Symbol, position.Symbol); + } + } + return new List(symbols.Keys); + } + public double GetMarketValue() + { + double marketValue=0.00; + List keys=new List(this.Keys); + for(int index=0;index positions=this[keys[index]]; + if(null==positions||0==positions.Count)continue; + marketValue+=(from MGSHPosition position in positions select position.MarketValue).Sum(); + } + return marketValue; + } + public double GetGainLoss() + { + double marketValue=0.00; + double exposure=0.00; + + List keys = new List(this.Keys); + for(int index=0;index positions=this[keys[index]]; + if(null==positions||0==positions.Count)continue; + exposure+=(from MGSHPosition position in positions select position.Exposure).Sum(); + marketValue+=(from MGSHPosition position in positions select position.MarketValue).Sum(); + } + return marketValue-exposure; + } + public double GetGainLossPercent() + { + double exposure=GetExposure(); + double marketValue=GetMarketValue(); + if(0.00==exposure)return exposure; + return (marketValue-exposure)/exposure; + } + public void Display() + { + List keys = new List(this.Keys); + for(int index=0;index ToNVPCollections() + { + List keys=new List(Keys); + List nvpCollectionsList=new List(); + for(int index=0;index NoTradeSymbols{get{return Utility.ToList(Configuration.NoTradeSymbols);}} + private MGSHActivePositions ActivePositions{get;set;} +// private SMSNotifications SMSNotifications{get;set;} + private StopLimits StopLimits{get;set;} + private MGSHPositions AllPositions{get;set;} + private MGSHPositions HedgePositions{get;set;} + private int Cycle{get;set;} + private DateTime TradeDate{get;set;} + private DateTime StartDate{get;set;} + private DateTime AnalysisDate{get;set;} + private String PathSessionFileName{get;set;} + +// ****************************************************************************************************************************************************** +//********************************************************** U P D A T E S E S S I O N P R I C E ***************************************************** +// ****************************************************************************************************************************************************** + public void UpdateSessionPrice(String symbol,DateTime tradeDate,double price,String pathSessionFileName) + { + MGSHSessionParams sessionParams=null; + if(null==symbol||Utility.IsEpoch(tradeDate)||null==pathSessionFileName) + { + MDTrace.WriteLine(LogLevel.DEBUG,"UpdateSessionPrice. One or more parameters are invalid."); + return; + } + PathSessionFileName=pathSessionFileName; + if(null==(sessionParams=RestoreSession())) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Error loading session file {0}",pathSessionFileName)); + return; + } + List keys=new List(ActivePositions.Keys); + bool hasChanges=false; + foreach(int key in keys) + { + MGSHPositions positions=ActivePositions[key]; + foreach(MGSHPosition position in positions) + { + if(!position.Symbol.Equals(symbol)||!position.PurchaseDate.Date.Equals(tradeDate.Date))continue; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Changing purchase price for {0} on {1} from {2} to {3}", + position.Symbol, + Utility.DateTimeToStringMMHDDHYYYY(position.PurchaseDate), + Utility.FormatCurrency(position.PurchasePrice), + Utility.FormatCurrency(price))); + position.PurchasePrice=price; + if(!hasChanges)hasChanges=true; + } + } + if(hasChanges)SaveSession(); + ActivePositions.Display(); + DisplayBalanceFromPositions(); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StartDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(StartDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TradeDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(TradeDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("AnalysisDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(AnalysisDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Next Slot:{0}",Cycle)); + } + +// ****************************************************************************************************************************************************** +//************************************************************** D I S P L A Y G A I N L O S S ***************************************************** +// ****************************************************************************************************************************************************** + public static void DisplayGainLoss(String paramPathSessionFileName) + { + Profiler profiler=new Profiler(); + ModelPerformanceSeries performanceSeries=GetModelPerformance(paramPathSessionFileName); + if(null==performanceSeries)return; + MDTrace.WriteLine("Date,Exposure,MarketValue,GainLossDOD,GainLoss,CumulativeGainLoss,R,(1+R),CumProd,CumProd-1,ClosedPositions"); + foreach(ModelPerformanceItem modelPerformanceItem in performanceSeries) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("\"{0}\",\"{1}\",\"{2}\",\"{3}\",\"{4}\",\"{5}\",\"{6}\",\"{7}\",\"{8}\",\"{9}\",\"{10}\"", + modelPerformanceItem.Date.ToShortDateString(), + Utility.FormatCurrency(modelPerformanceItem.Exposure), + Utility.FormatCurrency(modelPerformanceItem.MarketValue), + Utility.FormatCurrency(modelPerformanceItem.GainLossDOD), + Utility.FormatCurrency(modelPerformanceItem.GainLoss), + Utility.FormatCurrency(modelPerformanceItem.CumulativeGainLoss), + Utility.FormatNumber(modelPerformanceItem.R,4), + Utility.FormatNumber(modelPerformanceItem.OnePlusR,4), + Utility.FormatNumber(modelPerformanceItem.CumProd,4), + Utility.FormatNumber(modelPerformanceItem.CumProdMinusOne,4), + modelPerformanceItem.ClosedPositions)); + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Done, took {0}(ms)",profiler.End())); + } + + public static ModelPerformanceSeries GetModelPerformance(String paramPathSessionFileName) + { + try + { + MGSHSessionParams sessionParams=MGSHSessionManager.RestoreSession(paramPathSessionFileName); + return GetModelPerformance(sessionParams); + } + catch(Exception exception) + { + MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); + return null; + } + } + +// Calcualtes the expectation for the model ( Percent of Winning Trades * Average Gain)/(Percent Losing Trades * Average Loss) +// The expectation should be above zero +// Using the AllPositions collection ignored open positions and active hedge positions + public static ModelStatistics GetModelStatistics(MGSHSessionParams sessionParams) + { + ModelStatistics modelStatistics=new ModelStatistics(); + try + { + if(null==sessionParams||null==sessionParams.AllPositions||0==sessionParams.AllPositions.Count) return modelStatistics; + double totalTrades=sessionParams.AllPositions.Count; + double winningTrades=sessionParams.AllPositions.Where(x => x.GainLoss>=0.00).Count(); + double losingTrades=sessionParams.AllPositions.Where(x => x.GainLoss<0.00).Count(); + double averageWinningTrade=sessionParams.AllPositions.Where(x => x.GainLoss>=0.00).Average(x => x.GainLossPcnt)*100.00; + double averageLosingTrade=sessionParams.AllPositions.Where(x => x.GainLoss<0.00).Average(x => x.GainLossPcnt)*100.00; + double percentWinningTrades=(winningTrades/(double)sessionParams.AllPositions.Count)*100.00; + double percentLosingTrades=(losingTrades/(double)sessionParams.AllPositions.Count)*100.00; + double expectation=(percentWinningTrades*averageWinningTrade)/(percentLosingTrades*Math.Abs(averageLosingTrade)); + + modelStatistics.TotalTrades=(long)totalTrades; + modelStatistics.WinningTrades=(long)winningTrades; + modelStatistics.LosingTrades=(long)losingTrades; + modelStatistics.AverageWinningTradePercentGain=averageWinningTrade; + modelStatistics.AverageLosingTradePercentLoss=averageLosingTrade; + modelStatistics.WinningTradesPercent=percentWinningTrades; + modelStatistics.LosingTradesPercent=percentLosingTrades; + modelStatistics.Expectancy=expectation; + return modelStatistics; + } + catch(Exception exception) + { + MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); + return modelStatistics; + } + } + + public static ModelPerformanceSeries GetModelPerformance(MGSHSessionParams sessionParams) + { + Profiler profiler=new Profiler(); + ModelPerformanceSeries performanceSeries=new ModelPerformanceSeries(); + DateGenerator dateGenerator=new DateGenerator(); + try + { + if(null==sessionParams)return null; + MGSHPositions combinedPositions=sessionParams.GetCombinedPositions(); + + // Fix purchase date/sell date fall on weekend + foreach(MGSHPosition position in combinedPositions) + { + if(dateGenerator.IsWeekend(position.PurchaseDate)) + { + while(true) + { + position.PurchaseDate=dateGenerator.GetPrevBusinessDay(position.PurchaseDate); + if(!HolidayDA.IsMarketHoliday(position.PurchaseDate)) break; + } + } + if(dateGenerator.IsWeekend(position.SellDate)) + { + while(true) + { + position.SellDate=dateGenerator.GetNextBusinessDay(position.SellDate); + if(!HolidayDA.IsMarketHoliday(position.SellDate)) break; + } + } + } + // ******************************************************** + DateTime minDate=combinedPositions.Min(x => x.PurchaseDate); + DateTime maxDate=PricingDA.GetLatestDate(); + double prevGainLoss=double.NaN; + LocalPriceCache.GetInstance().RemoveDate(maxDate); + List historicalDates=dateGenerator.GenerateHistoricalDates(minDate,maxDate); + foreach(DateTime currentDate in historicalDates) + { + MGSHPositions openPositions=new MGSHPositions(combinedPositions.Where(x => (x.PurchaseDate<=currentDate&&(!Utility.IsEpoch(x.SellDate)&&x.SellDate>currentDate))||(x.PurchaseDate<=currentDate&&Utility.IsEpoch(x.SellDate))).ToList()); + MGSHPositions closedPositions=new MGSHPositions(combinedPositions.Where(x => (!Utility.IsEpoch(x.SellDate)&&x.SellDate.Equals(currentDate))).ToList()); + if(0==openPositions.Count&&0==closedPositions.Count) continue; + double gainLoss=0.00; + double gainLossClosedPositions=0.00; + double exposure=0.00; + double marketValue=0.00; + if(HolidayDA.IsMarketHoliday(currentDate)) continue; + ModelPerformanceItem performanceItem=new ModelPerformanceItem(); + foreach(MGSHPosition openPosition in openPositions) + { + exposure+=openPosition.Shares*openPosition.PurchasePrice; + if(!LocalPriceCache.GetInstance().ContainsPrice(openPosition.Symbol,currentDate)) + { + Prices prices=PricingDA.GetPricesForward(openPosition.Symbol,currentDate,90); + LocalPriceCache.GetInstance().Add(prices); + } + Price price=LocalPriceCache.GetInstance().GetPrice(openPosition.Symbol,currentDate); + if(null==price) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("No price for {0} on {1}",openPosition.Symbol,currentDate.ToShortDateString())); + } + else + { + gainLoss+=((price.Close*openPosition.Shares)-(openPosition.PurchasePrice*openPosition.Shares)); + marketValue+=(price.Close*openPosition.Shares); + } + } + foreach(MGSHPosition closedPosition in closedPositions) + { + double gainLossPosition=(closedPosition.CurrentPrice*closedPosition.Shares)-(closedPosition.PurchasePrice*closedPosition.Shares); + gainLossClosedPositions+=gainLossPosition; + } + performanceItem.Date=currentDate; + performanceItem.Exposure=exposure; + performanceItem.MarketValue=marketValue; + performanceItem.GainLossDOD=double.IsNaN(prevGainLoss)?gainLoss:(gainLoss-prevGainLoss)+gainLossClosedPositions; + performanceItem.GainLoss=gainLoss+gainLossClosedPositions; + performanceItem.ClosedPositions=closedPositions.Count>0?true:false; + performanceSeries.Add(performanceItem); + prevGainLoss=gainLoss; + } + performanceSeries.CalculatePerformance(); + return performanceSeries; + } + catch(Exception exception) + { + MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); + return null; + } + finally + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Done, total took {0}(ms)",profiler.End())); + } + } + +// ****************************************************************************************************************************************************** +//************************************************************** D I S P L A Y S E S S I O N ***************************************************** +// ****************************************************************************************************************************************************** + public void DisplaySession(String paramPathSessionFileName) + { + if(null==paramPathSessionFileName)return; + PathSessionFileName=paramPathSessionFileName; + MGSHSessionParams sessionParams=null; + if(null==(sessionParams=RestoreSession())) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Error loading session file {0}",paramPathSessionFileName)); + return; + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("SessionFile:{0} Last Updated:{1}",paramPathSessionFileName,sessionParams.LastUpdated)); + Configuration.DisplayConfiguration(); + MDTrace.WriteLine(LogLevel.DEBUG,"************** A L L P O S I T I O N S *************"); + AllPositions=new MGSHPositions((from MGSHPosition position in AllPositions orderby position.PurchaseDate ascending select position).ToList()); + AllPositions.Display(); + MDTrace.WriteLine(LogLevel.DEBUG,"************** A C T I V E P O S I T I O N S *************"); + ActivePositions.Display(); + MDTrace.WriteLine(LogLevel.DEBUG,"************** H E D G E P O S I T I O N S *************"); + HedgePositions.Display(); + MDTrace.WriteLine(LogLevel.DEBUG, "************** T O P G A I N E R S *************"); + AllPositions.DisplayTopFive(); + MDTrace.WriteLine(LogLevel.DEBUG, "************** T O P L O S E R S *************"); + AllPositions.DisplayBottomFive(); + DisplayStatistics(sessionParams); + DisplayBalanceFromPositions(); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StartDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(StartDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TradeDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(TradeDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("AnalysisDate:{0}",Utility.DateTimeToStringMMHDDHYYYY(AnalysisDate))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Next Slot:{0}",Cycle)); + } +// ************************************************************************************************************************************************************************************* +// ************************************************************************************************************************************************************************************* +// ************************************************************************************************************************************************************************************* +// ************************************************************************************************************************************************************************************* +// ****************************************************************************************************************************************************** +// ****************************************************************** B A C K T E S T ***************************************************************** +// ****************************************************************************************************************************************************** + /// + /// Ideally, startDate should be November,February,May,August + /// Use this endpoint to first launch the model on the last trading day of a given month supplying the configuration with initial settings + /// Subsequent daily runs are accomplished by calling UpdateDaily(startDate,endDate,pathSessionFileName) where the startDate=endDate=the trade date + /// after the market closes + /// + /// {ORIGINAL START DATE} + /// {TODAY} + /// The session file name + /// The configuration + /// + public MGSHBacktestResult PerformBacktest(DateTime paramStartDate,DateTime paramAnalysisDate,String paramPathSessionFileName,MGSHConfiguration configuration) + { + MGSHBacktestResult backTestResult=new MGSHBacktestResult(); + DateGenerator dateGenerator=new DateGenerator(); + Configuration=configuration; + CashBalance=Configuration.InitialCash; + HedgeCashBalance=Configuration.HedgeInitialCash; + ActivePositions=new MGSHActivePositions(); + AllPositions=new MGSHPositions(); + HedgePositions=new MGSHPositions(); + StartDate=paramStartDate; + TradeDate=paramStartDate; + AnalysisDate=paramAnalysisDate; + PathSessionFileName=paramPathSessionFileName; + MGSHSessionParams sessionParams=null; + + Cycle=0; + if(AnalysisDate.Date>Today().Date)return backTestResult; + if(Utility.IsEpoch(AnalysisDate))AnalysisDate=Today(); + TradeDate=dateGenerator.GetCurrentMonthEnd(StartDate); + if(TradeDate>AnalysisDate) + { + int startMonth=StartDate.Month; + TimeSpan timeSpan=new TimeSpan(); + if((new int[]{12,3,6,9}).Any(x=>x.Equals(startMonth)))timeSpan=new TimeSpan(30,0,0,0); + else if((new int[]{1,4,7,10}).Any(x=>x.Equals(startMonth)))timeSpan=new TimeSpan(60,0,0,0); + else if((new int[]{2,5,8,11}).Any(x=>x.Equals(startMonth)))timeSpan=new TimeSpan(90,0,0,0); + StartDate=StartDate-timeSpan; + TradeDate=dateGenerator.GetCurrentMonthEnd(StartDate); + } + if(null!=PathSessionFileName)sessionParams=RestoreSession(); + if(null!=sessionParams) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Using session file {0}, Last updated {1}",paramPathSessionFileName,sessionParams.LastUpdated)); + } + Configuration.DisplayConfiguration(); + DisplayBalance(); + + while(true) + { + if(TradeDate>AnalysisDate)break; + int slotIndex=(int)(((double)Cycle)%((double)(HoldingPeriod))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TRADE DATE {0} , ANALYSIS DATE {1}",Utility.DateTimeToStringMMHDDHYYYY(TradeDate),Utility.DateTimeToStringMMHDDHYYYY(AnalysisDate))); + if(!ActivePositions.ContainsKey(slotIndex)) + { + BuySlotPositions(slotIndex); + } + else + { + SellAndBuySlotPositions(slotIndex); + } +// Check if we are configured to use any of the daily strategies + if(Configuration.UseStopLimits || Configuration.UseHedging) + { + DateTime nextMonthEndDate = dateGenerator.GetNextMonthEnd(TradeDate); + if(nextMonthEndDate > TradeDate && nextMonthEndDate<=AnalysisDate) + { + if(null!=PathSessionFileName)SaveSession(); + UpdateDaily(dateGenerator.FindNextBusinessDay(TradeDate),nextMonthEndDate,paramPathSessionFileName); + } + } + Cycle++; + TradeDate = dateGenerator.GetNextMonthEnd(TradeDate); + if (TradeDate>AnalysisDate)break; + } // WHILE TRUE + MDTrace.WriteLine(LogLevel.DEBUG,$"RUN COMPLETE FOR ANALYSIS DATE {AnalysisDate.ToShortDateString()}."); + if(null!=PathSessionFileName)SaveSession(); + if(null==sessionParams)sessionParams=RestoreSession(); + DisplayStatistics(sessionParams); + for(int slotIndex=0;slotIndex + /// Entry point for daily runs. Make sure for daily runs that startDate=endDate=Trading Date after market close + /// + /// {ORIGINAL START DATE} + /// {TODAY} + /// The session file name + /// + public MGSHBacktestResult UpdateDaily(DateTime startDate,DateTime endDate,String pathSessionFileName) + { + DateGenerator dateGenerator = new DateGenerator(); + PathSessionFileName = pathSessionFileName; + + RestoreSession(); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("************** U P D A T E D A I L Y {0} < {1}",startDate.ToShortDateString(),endDate.ToShortDateString())); + + while(startDate < endDate) + { + bool changed = false; + if(Configuration.UseStopLimits) + { + changed = UpdateStopLimitsForActivePositions(startDate); + } + + if(Configuration.UseHedging) + { + changed = ManageHedgedPositions(startDate) ? true : changed; + } +// if(changed)DisplayBalance(); + DisplayBalance(); + startDate = dateGenerator.FindNextBusinessDay(startDate); + } + if(null!=PathSessionFileName)SaveSession(); + return new MGSHBacktestResult(true, CashBalance); + } +// ******************************************************************************************************************************************************* +// ************************************************************ H E D G E M A N A G E M E N T ******************************************************* +// ******************************************************************************************************************************************************* + /// + /// ManageHedgedPositions + /// + /// + /// True if any changed were made otherwise false + public bool ManageHedgedPositions(DateTime analysisDate) + { + HedgeManager hedgeManager = new HedgeManager(){HedgeShortSymbol=Configuration.HedgeShortSymbol,Verbose=false}; + bool changed = false; + bool openHedgeIndicator = hedgeManager.IsOpenHedgeIndicator(analysisDate,Configuration.HedgeCloseAboveSMANDays, Configuration.HedgeBandBreakCheckDays); + bool haveHedgedPositions = HedgePositions.Where(x => x.Symbol.Equals(Configuration.HedgeShortSymbol)).Count()>0?true:false; + int numHedgePositions = HedgePositions.Where(x => x.Symbol.Equals(Configuration.HedgeShortSymbol)).Count(); + +// There are no hedged positions and the indicator is off + if(!openHedgeIndicator && !haveHedgedPositions) + { + return changed; + } + +// The indicator is on and we have no hedged positions + else if(openHedgeIndicator && !haveHedgedPositions) + { + MGSHPosition shortPosition = BuyHedgePosition(analysisDate, HedgeCashBalance); + if(null != shortPosition) + { + changed = true; + MDTrace.WriteLine(LogLevel.DEBUG, "******************** B U Y ********************"); + MGSHPosition.DisplayHeader(); + shortPosition.Display(); + HedgePositions.Add(shortPosition); + HedgeCashBalance-=shortPosition.Exposure; + } + } + else if(openHedgeIndicator && haveHedgedPositions) + { + changed = ManageActiveHedgePositions(analysisDate); + } + else if(!openHedgeIndicator && haveHedgedPositions) + { + changed = ManageActiveHedgePositions(analysisDate); + } + return changed; + } + + /// + /// Attempts to purchase the hedge with the supplied cash allocation + /// + /// + /// + /// Position on success otherwise null + public MGSHPosition BuyHedgePosition(DateTime analysisDate, double cashAllocation) + { + Price shortPositionPrice = GBPriceCache.GetInstance().GetPrice(Configuration.HedgeShortSymbol,analysisDate); + if(null == shortPositionPrice) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Cannot locate a price for {Configuration.HedgeShortSymbol} on {analysisDate.ToShortDateString()}")); + return null; + } + MGSHPosition shortPosition = new MGSHPosition(); + shortPosition.Symbol = Configuration.HedgeShortSymbol; + shortPosition.PurchaseDate = analysisDate; + shortPosition.PurchasePrice = shortPositionPrice.Close; + shortPosition.CurrentPrice = shortPositionPrice.Close; + shortPosition.InitialStopLimit = shortPosition.PurchasePrice - ( shortPosition.PurchasePrice * Configuration.HedgeRiskPercentDecimal); + shortPosition.R = shortPosition.PurchasePrice - shortPosition.InitialStopLimit; + shortPosition.TrailingStopLimit = shortPosition.InitialStopLimit; + shortPosition.LastStopAdjustment = Utility.Epoch; + shortPosition.Shares = (int)Math.Floor(cashAllocation / shortPositionPrice.Close); + + if(0 == shortPosition.Shares) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Insufficient Cash Allocation to purchase shares of {Configuration.HedgeShortSymbol} on {analysisDate.ToShortDateString()}. Cash Allocation:{Utility.FormatCurrency(cashAllocation)}")); + return null; + } + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Initial stop limit for {0} on {1} with PurchsePrice:{2} is {3}, Shares:{4} Risk:{5}%", + shortPosition.Symbol, + shortPosition.PurchaseDate, + Utility.FormatCurrency(shortPosition.PurchasePrice,2), + Utility.FormatCurrency(shortPosition.InitialStopLimit,2), + Utility.FormatNumber(shortPosition.Shares,2), + Utility.FormatPercent(Configuration.HedgeRiskPercentDecimal) + )); + + return shortPosition; + } + + /// + /// Manages open hedge position by checking and adjusting stop price and determining if the hedge should exit + /// + /// + /// + public bool ManageActiveHedgePositions(DateTime analysisDate) + { + HedgeManager hedgeManager = new HedgeManager(){HedgeShortSymbol=Configuration.HedgeShortSymbol,Verbose=false}; + List closedPositions = new List(); + + foreach(MGSHPosition position in HedgePositions) + { + Price price=GBPriceCache.GetInstance().GetPrice(position.Symbol,analysisDate); + + if(null==price) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[ManageActiveHedgePositions] No price for {0} on {1}. Cannot evaluate stop limit.",position.Symbol,analysisDate.ToShortDateString())); + continue; + } + + position.CurrentPrice = price.Close; + if(price.Low < position.TrailingStopLimit && !position.PurchaseDate.Equals(analysisDate)) + { + position.SellDate = analysisDate; + position.CurrentPrice = position.TrailingStopLimit; + position.Comment = "Closed due to trailing stop."; + HedgeCashBalance+=position.MarketValue; + if(HedgeCashBalance>Configuration.HedgeInitialCash) + { + CashBalance+=HedgeCashBalance-Configuration.HedgeInitialCash; // if we made gains on the hedge and the hedge cash would grow then put proceeds above the initial hedge cash in regualr cash + HedgeCashBalance=Configuration.HedgeInitialCash; // For example, let hedge proceeeds help the portfolio invest + } + AllPositions.Add(position); + closedPositions.Add(position); + } + else if(hedgeManager.IsLowerBandBreakIndicator(analysisDate, position)) + { + StopLimit stopLimit = hedgeManager.EvaluateStopPriceHedge(analysisDate, position,Configuration.StopLimitScalingVolatilityDays,Configuration.HedgeMinDaysBetweenStopAdjustments); + if(null != stopLimit) + { + StopLimits.Add(stopLimit); + } + } + } + + if(0!=closedPositions.Count) + { + foreach(MGSHPosition position in closedPositions) + { + MDTrace.WriteLine(LogLevel.DEBUG,""); + MDTrace.WriteLine(LogLevel.DEBUG,"******************************************** S E L L S T O P L I M I T ********************************************"); + position.Display(); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Gain/Loss:{Utility.FormatCurrency(position.GainLoss)} Gain/Loss(%):{Utility.FormatPercent(position.GainLossPcnt)}")); + HedgePositions.Remove(position); + } + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("*************************************************************************************************",analysisDate.ToShortDateString())); + if(closedPositions.Count>0)return true; + return false; + } + + // ************************************************************************************************************************************************* + // ************************************************************ E N D M A N A G E H E D G I N G ************************************************ + // ************************************************************************************************************************************************* + + // ********************************************************************************************************************************************************** + // ********************************************************************** S T O P L I M I T S *************************************************************** + // ********************************************************************************************************************************************************** + public bool SetInitialStopLimitsForNewPositions(DateTime tradeDate,MGSHPositions positions) + { + if(!Configuration.UseStopLimits)return true; + + if(null == positions || 0==positions.Count) + { + return true; + } + + foreach(MGSHPosition position in positions) + { + position.InitialStopLimit = position.PurchasePrice - (position.PurchasePrice * Configuration.StopLimitRiskPercentDecimal); + position.R = position.PurchasePrice - position.InitialStopLimit; + position.TrailingStopLimit = position.InitialStopLimit; + position.LastStopAdjustment = Utility.Epoch; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Initial stop limit for {0} on {1} with PurchsePrice:{2} is {3}, Shares:{4} Risk:{5}%", + position.Symbol, + position.PurchaseDate, + Utility.FormatCurrency(position.PurchasePrice,2), + Utility.FormatCurrency(position.InitialStopLimit,2), + Utility.FormatNumber(position.Shares,2), + Utility.FormatPercent(Configuration.StopLimitRiskPercentDecimal) + )); + } + return true; + } + + /// + /// Updates stop limits for active positions and closes positions that have stop limit violations + /// + /// true if any positions were closed, otherwise returns false + public bool UpdateStopLimitsForActivePositions(DateTime analysisDate) + { + if(!Configuration.UseStopLimits)return true; + MGSHPositions closedPositions = new MGSHPositions(); + + MDTrace.WriteLine(LogLevel.DEBUG,"************************** U P D A T E S T O P L I M I T S F O R A C T I V E P O S I T I O N S ************"); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("ANALYSIS DATE:{0}",analysisDate.ToShortDateString())); + List slotKeys = new List(ActivePositions.Keys); + for(int slotIndex = 0; slotIndex< slotKeys.Count;slotIndex++) + { + MGSHPositions positions = ActivePositions[slotKeys[slotIndex]]; + foreach(MGSHPosition position in positions) + { + Price price=GBPriceCache.GetInstance().GetPrice(position.Symbol,analysisDate); + + if(null==price) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[UpdateStopLimitsForActivePositions] No price for {0} on {1}. Cannot evaluate stop limit.",position.Symbol,analysisDate.ToShortDateString())); + continue; + } + + // This is an older position for which we never set an initial stop. We will not adjust these + if(double.IsNaN(position.R) || double.IsNaN(position.TrailingStopLimit) || double.IsNaN(position.InitialStopLimit)) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[UpdateStopLimitsForActivePositions] Position {0} on {1} is legacy will not adjust stop limit.",position.Symbol,analysisDate.ToShortDateString())); + continue; + } + + position.CurrentPrice = price.Close; + if(price.Low < position.TrailingStopLimit && !position.PurchaseDate.Equals(analysisDate)) + { + position.SellDate = analysisDate; + position.CurrentPrice = position.TrailingStopLimit; + position.Comment = "Closed due to trailing stop."; + CashBalance += position.MarketValue; + AllPositions.Add(position); + closedPositions.Add(position); + } + else if(price.Low > position.PurchasePrice) // If the Low price is above our purchase price then re-evaluate the stop + { + EvaluateStopPrice(analysisDate,price,position); + } + } + } + if(0!=closedPositions.Count) + { + foreach(MGSHPosition position in closedPositions) + { + MDTrace.WriteLine(LogLevel.DEBUG,""); + MDTrace.WriteLine(LogLevel.DEBUG,"******************************************** S E L L S T O P L I M I T ********************************************"); + position.Display(); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Gain/Loss:{Utility.FormatCurrency(position.GainLoss)} Gain/Loss(%):{Utility.FormatPercent(position.GainLossPcnt)}")); + ActivePositions.Remove(position); + } + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("*************************************************************************************************",analysisDate.ToShortDateString())); + if(closedPositions.Count>0)return true; + return false; + } + + /// + /// Evaluates stop prices for slot positons to see if we need to perform updates + /// + /// true if changes were made otherwise false + private bool EvaluateStopPrice(DateTime analysisDate,Price currentPrice,MGSHPosition position) + { + bool changed = false; + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("EvaluateStopPrice: {0} on {1}",position.Symbol,analysisDate.ToShortDateString())); + DateGenerator dateGenerator=new DateGenerator(); + + // only adjust stops if we are trending up + Prices prices=GBPriceCache.GetInstance().GetPrices(position.Symbol,analysisDate,Configuration.StopLimitPriceTrendDays); + PriceTrendIndicatorResult priceTrendIndicatorResult=PriceTrendIndicator.IsUptrend(prices,Configuration.StopLimitPriceTrendDays); + if(!priceTrendIndicatorResult.IsUpTrend) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0} does not have upward price trend (higher highs and higher lows for {1} consecutive days), will not adjust stop price",position.Symbol,Configuration.StopLimitPriceTrendDays)); + return changed; + } + + // where R = Risk Per Share in $ + double trailingStop=position.InitialStopLimit+Math.Floor((currentPrice.Low-position.PurchasePrice)/position.R)*position.R; + double trailingStopScaled=trailingStop; + double daysHeld=Math.Abs(dateGenerator.DaysBetweenActual(position.PurchaseDate,analysisDate)); + if(Utility.IsEpoch(position.LastStopAdjustment)) // we've never adjusted the stop price + { + if(daysHeld>=Configuration.MinDaysBetweenInitialStopAdjustment) + { + trailingStopScaled=GetStopLimitWithScalingAverageTrueRange(analysisDate,currentPrice,position,trailingStop); + trailingStop=Math.Max(trailingStop,trailingStopScaled); + if(trailingStop>=currentPrice.Low||trailingStop>=currentPrice.Close) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be higher than the Low/Close of {2}.",position.Symbol,Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(currentPrice.Low))); + return changed; + } + if(Numerics.Round(trailingStop).Equals(Numerics.Round(position.TrailingStopLimit))) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be the same as the existing stop limit of {2}.",position.Symbol,Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(position.TrailingStopLimit))); + return changed; + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************** A D J U S T S T O P L I M I T ************************")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Adjusting StopLimit after {0} days for {1} from {2} to {3}. Purchase Price: {4} Current Price:{5} Spread:{6} Shares:{7}",daysHeld,position.Symbol,Utility.FormatCurrency(position.TrailingStopLimit),Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(position.PurchasePrice),Utility.FormatCurrency(currentPrice.Close),Utility.FormatPercent((currentPrice.Close-trailingStop)/trailingStop),Utility.FormatNumber(position.Shares,2))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("*****************************************************************************************")); + StopLimit newStopLimit=new StopLimit + { + Symbol=position.Symbol, + AnalysisDate=analysisDate, + PreviousStop=0.00==position.TrailingStopLimit?position.InitialStopLimit:position.TrailingStopLimit, + NewStop=trailingStop, + CurrentPriceLow=currentPrice.Low, + CurrentPriceClose=currentPrice.Close, + PriceTrendIndicatorSlope=priceTrendIndicatorResult.LowPriceSlope + }; + AddStopLimit(newStopLimit); + position.TrailingStopLimit=trailingStop; + position.LastStopAdjustment=analysisDate; + changed=true; + } + else + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The initial stop for {0} will not be modified until the grace period is reached. Days Held:{1} MinDaysBetweenInitialStopAdjustment:{2} ", + position.Symbol,daysHeld,Configuration.MinDaysBetweenInitialStopAdjustment)); + } + } + else // we have already made prior stop adjustments + { + int daysSinceLastStopAdjustment=Math.Abs(dateGenerator.DaysBetweenActual(position.LastStopAdjustment,analysisDate)); + if(daysSinceLastStopAdjustment>=Configuration.MinDaysBetweenStopAdjustments) + { + trailingStopScaled=GetStopLimitWithScalingAverageTrueRange(analysisDate,currentPrice,position,trailingStop); + trailingStop=Math.Max(trailingStop,trailingStopScaled); + if(trailingStop>=currentPrice.Low||trailingStop>=currentPrice.Close) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The calculated trailing stop for {0} of {1} would be higher than the Low/Close of {2}.",position.Symbol,Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(currentPrice.Low))); + return changed; + } + if(Numerics.Round(position.TrailingStopLimit) < Numerics.Round(trailingStop)) // round the stop limits to fractionals don't look like differences + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************** A D J U S T S T O P L I M I T ************************")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Adjusting StopLimit after {0} days for {1} from {2} to {3}. Purchase Price: {4} Current Price:{5} Spread:{6} Shares:{7}",daysHeld,position.Symbol,Utility.FormatCurrency(position.TrailingStopLimit),Utility.FormatCurrency(trailingStop),Utility.FormatCurrency(position.PurchasePrice),Utility.FormatCurrency(currentPrice.Close),Utility.FormatPercent((currentPrice.Close-trailingStop)/trailingStop),Utility.FormatNumber(position.Shares,2))); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("****************************************************************************************")); + StopLimit newStopLimit=new StopLimit + { + Symbol=position.Symbol, + AnalysisDate=analysisDate, + PreviousStop=0.00==position.TrailingStopLimit?position.InitialStopLimit:position.TrailingStopLimit, + NewStop=trailingStop, + CurrentPriceLow=currentPrice.Low, + CurrentPriceClose=currentPrice.Close, + PriceTrendIndicatorSlope=priceTrendIndicatorResult.LowPriceSlope + }; + AddStopLimit(newStopLimit); + position.TrailingStopLimit=trailingStop; + position.LastStopAdjustment=analysisDate; + changed = true; + } + else + { + double currentTrailingStopLimit=Numerics.Round(position.TrailingStopLimit); + double suggestedTrailingStopLimit=Numerics.Round(trailingStop); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Not Adjusting Stop Limit for {0} because the new trailing stop limit would be less than or equal to the current trailing stop limit. Current Trailing Stop Limit:{1}, Calculated Trailing Stop Limit:{2}.",position.Symbol,Utility.FormatCurrency(currentTrailingStopLimit),Utility.FormatCurrency(suggestedTrailingStopLimit))); + } + } + else + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("The trailing stop for {0} was adjusted {1} days ago. MinDaysBetweenStopAdjustments is {2}", + position.Symbol, + daysSinceLastStopAdjustment, + Configuration.MinDaysBetweenStopAdjustments)); + } + } + return changed; + } + + private double GetStopLimitWithScalingAverageTrueRange(DateTime tradeDate,Price currentPrice,MGSHPosition position,double stopLimitNonScaled) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("GetStopLimitWithScalingAverageTrueRange: Symbol:{0}",position.Symbol)); + double volatility=double.NaN; + volatility=VolatilityGenerator.CalculateVolatility(position.Symbol,tradeDate,Configuration.StopLimitScalingVolatilityDays); + if(double.IsNaN(volatility)) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Unable to calculate AverageTrueRange for {0} on {1}. Using non-scaled stop limit.",position.Symbol,tradeDate.ToShortDateString())); + return stopLimitNonScaled; + } + double stopLimit = currentPrice.Low - volatility; // We base the stop off of the low in order to give a bit more breathing room in the stop in the event that we have a wide spread between the close and the low. Backtested currentPrice.Close vs currentPrice.Low and basing off the low yields better results. + return stopLimit; + } + +// ********************************************************************************************************************************************************** +// ***************************************************** S T O P L I M I T C O L L E C T I O N M A I N T E N A N C E *********************************** +// ********************************************************************************************************************************************************** + private void AddStopLimit(StopLimit stopLimit) + { + if(null == StopLimits)StopLimits = new StopLimits(); + StopLimits.Add(stopLimit); + } + +// ****************************************************************************************************************************************************** +// ****************************************************************************************************************************************************** +// ****************************************************************************************************************************************************** + public List SymbolsHeld(DateTime tradeDate) + { + List symbolsHeld=ActivePositions.SymbolsHeld(); + if(!Configuration.IncludeTradeMasterForSymbolsHeld)return symbolsHeld; + if(null == symbolsHeld)symbolsHeld=new List(); + PortfolioTrades portfolioTrades=PortfolioDA.GetOpenTradesAsOf(tradeDate); + if(null == portfolioTrades || 0==portfolioTrades.Count)return symbolsHeld; + symbolsHeld.AddRange(portfolioTrades.Symbols.Distinct()); + return symbolsHeld; + } + +// ********************************************************************************************************************************************************** +// **************************************************************** G E T E X P O S U R E / M A R K E T V A L U E ***************************************** +// ********************************************************************************************************************************************************** + public RealtimeGainLoss GetRealtimeGainLoss(DateTime tradeDate) + { + int count=ActivePositions.Count; + double marketValue=0.00; + double exposure=0.00; + RealtimeGainLoss gainLoss=new RealtimeGainLoss(); + + for(int slotIndex=0;slotIndex positions=ActivePositions[slotIndex]; + if(null==positions||0==positions.Count)continue; + foreach(MGSHPosition position in positions) + { + Price price=GBPriceCache.GetInstance().GetRealtimePrice(position.Symbol); + if(null==price){MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Cannot price {0} on {1}",position.Symbol,Utility.DateTimeToStringMMHDDHYYYY(tradeDate)));continue;} + position.CurrentPrice=price.Close; + } + } + for(int slotIndex=0;slotIndex positions=ActivePositions[slotIndex]; + if(null==positions||0==positions.Count)continue; + exposure+=(from Position position in positions select position.Exposure).Sum(); + marketValue+=(from Position position in positions select position.MarketValue).Sum(); + } + gainLoss.Exposure=exposure; + gainLoss.MarketValue=marketValue; + return gainLoss; + } + +// ************************************************************************************************************************************************** +// **************************************************************** S E L L P O S I T I O N S ***************************************************** +// *************************************************************************************************************************************************** + private void SellPositions(MGSHPositions positions,DateTime sellDate) + { + foreach(MGSHPosition position in positions) + { + SellPosition(position, sellDate); + } + } + + private void SellPosition(MGSHPosition position,DateTime sellDate) + { + Price price=GBPriceCache.GetInstance().GetPrice(position.Symbol,sellDate); + position.SellDate=sellDate; + if(null==price) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("**********Cannot locate a price for {0} on {1}**********",position.Symbol,Utility.DateTimeToStringMMHDDHYYYY(sellDate))); + position.CurrentPrice=position.PurchasePrice; + } + else position.CurrentPrice=price.Close; + } + +// ************************************************************************************************************************************************************ +// *********************************************************** B U Y S L O T P O S I T I O N S ************************************************************ +// ************************************************************************************************************************************************************ + private MGSHPositions BuyPositions(DateTime tradeDate, DateTime analysisDate, double cash, List symbolsHeld, double maxPositions=double.NaN) + { + MGSHPositions positions = new MGSHPositions(); + if(double.IsNaN(maxPositions))maxPositions=MaxPositions; + if(0==maxPositions)return positions; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"BUY POSITIONS: TRADE DATE:{TradeDate.ToShortDateString()} CASH:{Utility.FormatCurrency(cash)} POSITIONS TO FILL:{maxPositions}")); + int positionCount = 0; + if (Configuration.BenchmarkMode) return BuyBenchmarkPositions(tradeDate, cash); + MGSHMomentumCandidates momentumCandidates = MGSHMomentumGenerator.GenerateMomentum(tradeDate, symbolsHeld, Configuration); + for (int index = 0; index < momentumCandidates.Count; index++) + { + MGSHMomentumCandidate momentumCandidate = momentumCandidates[index]; + Price price = GBPriceCache.GetInstance().GetPrice(momentumCandidate.Symbol, tradeDate); + if (null == price) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("Cannot locate a price for {0} on {1}", momentumCandidate.Symbol, Utility.DateTimeToStringMMHDDHYYYY(tradeDate))); + continue; + } + MGSHPosition position = new MGSHPosition(); + position.Symbol = momentumCandidate.Symbol; + position.CumReturn252 = momentumCandidate.CumReturn252; + position.IDIndicator = momentumCandidate.IDIndicator; + position.Score=momentumCandidate.Score; + position.MaxDrawdown = momentumCandidate.MaxDrawdown; + position.MaxUpside = momentumCandidate.MaxUpside; + position.PE = momentumCandidate.PE; + position.Beta = momentumCandidate.Beta; + position.ZacksRank = momentumCandidate.ZacksRank; + position.Velocity = momentumCandidate.Velocity; + position.Volume = momentumCandidate.Volume; + position.Return1D = position.Return1D; + position.PurchaseDate = tradeDate; + position.PurchasePrice = price.Close; + position.CurrentPrice = price.Close; + position.Shares = (int)Math.Floor((cash / (double)MaxPositions) / position.PurchasePrice); // make sure the lot is evenly divided by the MaxPositions allowble for the slot + if (0 == position.Shares) continue; // if not able to purchase any shares, for example, if the share price exceeds our purchasing power then move on to the next candidate + positions.Add(position); + positionCount++; + if (positionCount >= maxPositions) break; + } + if (0 == positions.Count && Configuration.UseFallbackCandidate) // if we don't get any signals then consider the fallback candidate options as per the configuration file + { + String fallbackCandidate = Configuration.FallbackCandidate; + if (null != Configuration.FallbackCandidateBestOf && !"".Equals(Configuration.FallbackCandidateBestOf)) + { + MGSHQualityIndicator qualityIndicator = new MGSHQualityIndicator(Configuration.QualityIndicatorType); + fallbackCandidate = CandidateSelector.SelectBestCandidateSymbol(qualityIndicator,Utility.ToList(Configuration.FallbackCandidateBestOf),Configuration.FallbackCandidate,tradeDate); + } + Price price = GBPriceCache.GetInstance().GetPrice(fallbackCandidate, tradeDate); + if (null == price) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("Cannot locate a price for {0} on {1}", fallbackCandidate, Utility.DateTimeToStringMMHDDHYYYY(tradeDate))); + return positions; + } + MGSHPosition position = new MGSHPosition(); + position.Symbol = fallbackCandidate; + position.PurchaseDate = tradeDate; + position.PurchasePrice = price.Close; + position.CurrentPrice = price.Close; + position.Shares = (int)Math.Floor(cash / position.PurchasePrice); + + if(0==position.Shares) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("************** Insufficient cash to purchase shares of {0} on {1}. Available Cash:{2} Price:{3} ************", + fallbackCandidate, + Utility.DateTimeToStringMMHDDHYYYY(tradeDate), + Utility.FormatCurrency(cash), + Utility.FormatCurrency(price.Close))); + return positions; + } + + positions.Add(position); + } + return positions; + } + + /// + /// Buys the benchmark mode symbol (BenchmarModeSymbol) if the configuration is set to BenchmarkMode (BenchmarkMode) + /// + /// Positions + private MGSHPositions BuyBenchmarkPositions(DateTime tradeDate, double cash) + { + MGSHPositions positions = new MGSHPositions(); + Price benchmarkPrice = GBPriceCache.GetInstance().GetPrice(Configuration.BenchmarkModeSymbol, tradeDate); + if (null == benchmarkPrice) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("Cannot locate a price for {0} on {1}", Configuration.BenchmarkModeSymbol, Utility.DateTimeToStringMMHDDHYYYY(tradeDate))); + return positions; + } + MGSHPosition position = new MGSHPosition(); + position.Symbol = Configuration.BenchmarkModeSymbol; + position.PurchaseDate = tradeDate; + position.PurchasePrice = benchmarkPrice.Close; + position.Shares = (int)Math.Floor(cash / position.PurchasePrice); + if (0 == position.Shares) return positions; + positions.Add(position); + return positions; + } + +// ********************************************************************************************************************************************* +// ************************************************************ E N D B U Y P O S I T I O N S *********************************************** +// ********************************************************************************************************************************************* + +//****************************************************************************************************************************************************** +//************************************************************* S T A T I S T I C S ******************************************************************** +//****************************************************************************************************************************************************** + private void DisplayStatistics(MGSHSessionParams sessionParams) + { + ModelStatistics modelStatistics = GetModelStatistics(sessionParams); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("************** M O D E L S T A T I S T I C S **************")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Trades:{modelStatistics.TotalTrades}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Winning Trades:{modelStatistics.WinningTrades}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Losing Trades:{modelStatistics.LosingTrades}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Average Winning Trade Percent Gain:{modelStatistics.AverageWinningTradePercentGain}%")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Average Losing Trade Percent Loss:{modelStatistics.AverageLosingTradePercentLoss}%")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Winning Trades Percent:{modelStatistics.WinningTradesPercent}%")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Losing Trades Percent:{modelStatistics.LosingTradesPercent}%")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Expectancy:{Utility.FormatNumber(modelStatistics.Expectancy,2)}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("**************************************************************")); + } + +//****************************************************************************************************************************************************** +//****************************************************************** D I S P L A Y B A L A N C E ***************************************************** +//****************************************************************************************************************************************************** + private void DisplayBalance() + { + MDTrace.WriteLine(LogLevel.DEBUG,"*******************************************"); + MDTrace.WriteLine(LogLevel.DEBUG,"EXPOSURE, AVAILABLE CASH, TOTAL PORTFOLIO ACCOUNT (MV+CASH), AVAILABLE HEDGE CASH, TOTAL PORTFOLIO ACCOUNT + HEDGES"); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0},{1},{2},{3},{4}", + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetExposure()+HedgePositions.Exposure)), + Utility.AddQuotes(Utility.FormatCurrency(CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(HedgeCashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+HedgePositions.MarketValue+CashBalance)) + )); + + List keys = new List(ActivePositions.Keys); + keys.Sort(); + for (int keyIndex = 0; keyIndex < keys.Count; keyIndex++) + { + MGSHPositions slotPositions = ActivePositions[keys[keyIndex]]; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("SLOT:{0} POSITIONS:{1} EXPOSURE:{2} SYMBOLS:{3}", + keys[keyIndex],slotPositions.Count, + Utility.FormatCurrency(slotPositions.Exposure), + Utility.ListToString(slotPositions.Symbols(),','))); + } + + if(HedgePositions.Count>0) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("HEDGES:{0} EXPOSURE:{1} SYMBOLS:{2}", + HedgePositions.Count, + Utility.FormatCurrency(HedgePositions.Exposure), + Configuration.HedgeShortSymbol)); + } + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TOTAL EXPOSURE: {0}",Utility.FormatCurrency(ActivePositions.GetExposure()+HedgePositions.Exposure))); + MDTrace.WriteLine(LogLevel.DEBUG,"*******************************************"); + } + + private void DisplayBalanceFromPositions() + { + MDTrace.WriteLine(LogLevel.DEBUG,"EXPOSURE, GAIN/LOSS, GAIN/LOSS(%), AVAILABLE CASH, TOTAL PORTFOLIO ACCOUNT (MV+CASH), AVAILABLE HEDGE CASH, TOTAL PORTFOLIO ACCOUNT + HEDGES"); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0},{1},{2},{3},{4},{5}", + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetExposure())), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetGainLoss())), + Utility.AddQuotes(Utility.FormatPercent(ActivePositions.GetGainLossPercent())), + Utility.AddQuotes(Utility.FormatCurrency(CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(HedgeCashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+HedgePositions.MarketValue+CashBalance)) + )); + } + + private void DisplayBalance(RealtimeGainLoss gainLoss) + { + MDTrace.WriteLine(LogLevel.DEBUG,"EXPOSURE, GAIN/LOSS, GAIN/LOSS(%), AVAILABLE CASH, TOTAL PORTFOLIO ACCOUNT (MV+CASH), AVAILABLE HEDGE CASH, TOTAL PORTFOLIO ACCOUNT + HEDGES"); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0},{1},{2},{3},{4},{5}", + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetExposure())), + Utility.AddQuotes(Utility.FormatCurrency(gainLoss.GainLoss)), + Utility.AddQuotes(Utility.FormatPercent(gainLoss.GainLossPercent)), + Utility.AddQuotes(Utility.FormatCurrency(CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+CashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(HedgeCashBalance)), + Utility.AddQuotes(Utility.FormatCurrency(ActivePositions.GetMarketValue()+HedgePositions.MarketValue+CashBalance)) + )); + } +// **************************************************************************************************************************************** +// ************************************************************* C O N T R O L T O D A Y *********************************************** +// **************************************************************************************************************************************** + public DateTime Today() + { + return DateTime.Now; + } + +// **************************************************************************************************************************************** +// **************************************************************** S E S S I O N M A N A G E M E N T *********************************** +// **************************************************************************************************************************************** + public MGSHSessionParams RestoreSession() + { + try + { + MGSHSessionManager sessionManager=new MGSHSessionManager(); + if(!MGSHSessionManager.SessionAvailable(PathSessionFileName))return null; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Restoring session from '{0}'",PathSessionFileName)); + MGSHSessionParams sessionParams=MGSHSessionManager.RestoreSession(PathSessionFileName); + TradeDate=sessionParams.TradeDate; + StartDate=sessionParams.StartDate; + Configuration=sessionParams.Configuration; + ActivePositions=sessionParams.ActivePositions; + AllPositions=sessionParams.AllPositions; + HedgePositions=sessionParams.HedgePositions; + StopLimits=sessionParams.StopLimits; + Cycle=sessionParams.Cycle; + CashBalance=sessionParams.CashBalance; + NonTradeableCash=sessionParams.NonTradeableCash; + return sessionParams; + } + catch(Exception exception) + { + MDTrace.WriteLine(LogLevel.DEBUG,exception.ToString()); + return null; + } + } + public void SaveSession() + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Saving session to '{0}'",PathSessionFileName)); + MGSHSessionParams sessionParams=new MGSHSessionParams(); + MGSHSessionManager sessionManager=new MGSHSessionManager(); + sessionParams.LastUpdated=Today(); + sessionParams.TradeDate=TradeDate; + sessionParams.StartDate=StartDate; + sessionParams.AnalysisDate=AnalysisDate; + sessionParams.Configuration=Configuration; + sessionParams.ActivePositions=ActivePositions; + sessionParams.AllPositions=AllPositions; + sessionParams.HedgePositions=HedgePositions; + sessionParams.Cycle=Cycle; + sessionParams.StopLimits=StopLimits; + sessionParams.CashBalance=CashBalance; + sessionParams.NonTradeableCash=NonTradeableCash; + sessionManager.SaveSession(sessionParams,PathSessionFileName); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHBacktestResult.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktestResult.cs new file mode 100644 index 0000000..c72027c --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktestResult.cs @@ -0,0 +1,26 @@ +using MarketData.Utils; +using System.Text; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHBacktestResult + { + public MGSHBacktestResult() + { + } + public MGSHBacktestResult(bool success, double cashBalance) + { + Success=success; + CashBalance=cashBalance; + } + public double CashBalance{get;set;} + + public bool Success{get;set;} + + public void Display() + { + StringBuilder sb = new StringBuilder(); + MDTrace.WriteLine(LogLevel.DEBUG,$"RESULT:{Success.ToString()} CASH BALANCE:{Utility.FormatCurrency(CashBalance)}"); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs new file mode 100644 index 0000000..50a4e16 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs @@ -0,0 +1,346 @@ +using System; +using MarketData.Utils; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHConfiguration + { +// Operational Settings + public bool Verbose{get;set;} +// Basic settings + public int HoldingPeriod{get;set;} + public int MaxPositions{get;set;} + public String NoTradeSymbols{get;set;} + public String NoTradeFinancialSymbols{get;set;} + public double InitialCash{get;set;} + +// Stop Limit Functionality + public bool UseStopLimits{get;set;} + public double StopLimitRiskPercentDecimal{get;set;} + public int StopLimitScalingVolatilityDays{get;set;} + public int MinDaysBetweenStopAdjustments{get;set;} + public int MinDaysBetweenInitialStopAdjustment{get;set;} + public int StopLimitPriceTrendDays{get;set;} + +// Hedging Strategy + public bool UseHedging{get;set;} + public String HedgeBenchmarkSymbol{get;set;} + public String HedgeShortSymbol{get;set;} + public double HedgeRiskPercentDecimal{get;set;} + public int HedgeMinDaysBetweenStopAdjustments; + public double HedgeInitialCash{get;set;} + public int HedgeCloseAboveSMANDays{get;set;} + public int HedgeBandBreakCheckDays{get;set;} + +// Manage buying and selling + public bool KeepSlotPositions{get;set;} // if this setting is true then we never sell slot positions, allowing the trailing stop to eventually invoke a sell. + +// Fundamental screenings + public double MarketCapLowerLimit{get;set;} + + public bool UsePEScreen{get;set;} // If set this filter will ignore any security that is either missing a PE or if the PE is present but less than 0 + + public bool UseMaxPEScreen{get;set;} // control Max PE range check + public double MaxPE{get;set;} // if UseMaxPECheck is set this setting will ignore any security who's PE is greater. If PE is missing candidate will be accepted + public bool StrictMaxPE{get;set;} // if TRUE then UseMaxPEScreen is STRICT otherwise UseMaxPEScreen is SOFT RULE where >MaxPE is AVOIDED but used to fill the quota. The quota is filled by ranking highPECandidates by PE and taking the lower PE's up to the quota. Soft rule yields best result + +// EBITDA screen + public bool UseEBITDAScreen{get;set;} + public bool UseRevenuePerShareScreen{get;set;} + +// If slope Beta Check is true then we compare the fundamental Beta to the threshhold value. If Beta>Threshhold AND the slope from the Benchmark LowPrice over BetaDays is <0 we reject + public String Benchmark{get;set;} + public bool UseLowSlopeBetaCheck{get;set;} + public int LowSlopeBetaDays{get;set;} + public double LowSlopeBetaThreshhold{get;set;} + +// MACD Settings : If MACD is being used then the process configures the MACD as per setup and eliminates candidates with a weak sell/strong sell signal in the signal days setting. + public bool UseMACD{get;set;} + public String MACDSetup="(12,26,9)"; // (8,17,9) is another alternative + public int MACDSignalDays{get;set;} + public bool MACDRejectWeakSellSignals{get;set;} + public bool MACDRejectStrongSellSignals{get;set;} + +// Stochastics Settings : If Stochastics is being used then the process eliminates candidates with a weak sell/strong sell in the signal days setting. + public bool UseStochastics{get;set;} + public int StochasticsSignalDays{get;set;} + public bool StochasticsRejectWeakSells{get;set;} + public bool StochasticsRejectStrongSells{get;set;} + +// FallbackCandidate : If this setting is true then the FallbackCandidate is purchased if there are no candidates that qualify in a given cycle + public bool UseFallbackCandidate{get;set;} + public String FallbackCandidate{get;set;} + public String FallbackCandidateBestOf{get;set;} // if this is set then the fallback candidate with the best 252 day return is selected + +// Benchmark mode : If this is set to true then purchases the benchmark symbol instead of the momentum candidates + public bool BenchmarkMode{get;set;} + public String BenchmarkModeSymbol{get;set;} + +// QualityIndicator + public String QualityIndicatorType{get;set;} // this can be either IDINDICATOR or SCOREINDICATOR. The SCOREINDICATOR was adopted from CMMomentum model + +// IncludeTradeMasterForSymbolsHeld : + public bool IncludeTradeMasterForSymbolsHeld{get;set;} // if this is set to true then use both ActivePositions within the model as well as the master trades table to determine held positions. + + public MGSHConfiguration() + { + Verbose=true; // user verbose output + BenchmarkMode=false; // set this to true if you want to run the model using just the benchmark symbol to buy. + BenchmarkModeSymbol="SPY"; // SPY is the default symbol to buy when testing + HoldingPeriod=3; // 3 is the default. Hold for three months + MaxPositions=3; // 3 is the default. Max positions per slot + NoTradeSymbols="GBTC,YOKU,PNY,RFMD,ASAZY"; // ASAZY came up as candidate during 3/30 run but not available on Robinhood + NoTradeFinancialSymbols="U.S. Private Equity,U.S. Financials,U.S. Financial Services,U.S. Banking and Investment Services,Trading-Miscellaneous,Trading--Miscellaneous,Trading--Leveraged Equity,Trading--Leveraged Debt,Trading--Leveraged Commodities,Trading--Inverse Equity,Trading--Inverse Commodities,Tactical Allocation,Specialty Finance,Japan Financials,Savings & Cooperative Banks,Option Writing,Insurance Brokers,Insurance - Specialty,Insurance - Reinsurance,Insurance - Property & Casualty,Insurance - Life,Insurance - Diversified,Global Private Equity,Global Financials,Financial Services,Financial Exchanges,Financial,China Financials,Banks - Regional - US,Banks - Regional - Latin America,Banks - Global,Asset Management,Credit Services"; + Benchmark="SPY"; // SPY is the default +// Candidate selection settings + MarketCapLowerLimit=1000000000; // 1B is the default +// PEScreen is off by default + UsePEScreen=false; // false is the default. This setting yields the most optimal performance in backtests. Checks for existance of PE and if exists ensures >0 +// UseMaxPEScreen, MaxPE, and StrictPEExclusion + UseMaxPEScreen=true; + MaxPE=40; + StrictMaxPE=false; + +// Stop Limits + UseStopLimits=false; // Flag to control the use of stop limits + StopLimitRiskPercentDecimal=.17; // The per share risk to take when setting the initial stop. This was the best setting after running a suite of tests + StopLimitScalingVolatilityDays=30; // This is the number of days to use in the average true range calculation + MinDaysBetweenInitialStopAdjustment=30; // 30 Number of days that must elapse before attempting to adjust the stop limit. This is the best setting + MinDaysBetweenStopAdjustments=30; // 30 Number of days between stop adjustments. This is after the initial stop is set... of course + StopLimitPriceTrendDays=20; // The number of days for which we want to see upward trend before adjusting a subsequent stop limit + +// Hedging Strategy + UseHedging=false; // Flag to control the use of hedging strategy + HedgeBenchmarkSymbol="SPY"; // The benchmark symbol for the hedging strategy + HedgeShortSymbol="SH"; // The symbol that is used to go short + HedgeRiskPercentDecimal=.12; // This will be the risk to assume with the hedge position. + HedgeMinDaysBetweenStopAdjustments=1; // We use a single day for hedge positions + HedgeCloseAboveSMANDays=10; // Part of open hedge indicator if Close is not above Bollinger SMAN for this many days then reject + HedgeBandBreakCheckDays=3; // Number of days that low and close must be <= LP1 in order to open hedge. If >= number of days then reject + +// Manage buying and selling + KeepSlotPositions=true; // The default is true to retain legacy functionality + +// Other settings + UseEBITDAScreen=true; // true is the default + UseRevenuePerShareScreen=true; // true is the default + UseLowSlopeBetaCheck=true; // true is the default. this yields the most optimal performance in backtests + LowSlopeBetaDays=15; // 15 is the default. This yields the most optimal performance in backtests + LowSlopeBetaThreshhold=1.00; // (1.00) is the default This yields the most optimal performance in backtests + UseMACD=true; // true is the default + MACDSetup="(12,26,9)"; // (12,26,9) + MACDSignalDays=12; // 12 is the default + MACDRejectStrongSellSignals=false; // false is the default + MACDRejectWeakSellSignals=true; // true is the default + UseStochastics=true; // true is the default + StochasticsSignalDays=3; // 3 is the default + StochasticsRejectStrongSells=true; // true is the default + StochasticsRejectWeakSells=true; // true is the default + +// Fallback candidate settings + UseFallbackCandidate=true; // True is the default + FallbackCandidate="SHV"; // "SHV" ICE U.S. Treasury Short Bond Index, "AGG" Barclays U.S. Aggregate Bond Index - AGG can have a slighty better return but is more volatile + FallbackCandidateBestOf="SHV,AGG,ACWX"; // if set then the fallback candidate is selected as the best 252 day return in this comma seperated list (i.e.) "SHV,ACWX,AGG" + // SHV,NEAR,BIL,GSY,AGG,ACWX,GSY,SCHF,IXUS,DBEF,IEFA +// Set the QualityIndicator type to the ScoreIndicator by default. + QualityIndicatorType=MGSHQualityIndicator.ToString(MGSHQualityIndicator.QualityType.ScoreIndicator); + + IncludeTradeMasterForSymbolsHeld=false; // If this is set to true then the model takes into account any open positions in other models etc., + } + public void DisplayHeader() + { + MDTrace.WriteLine(LogLevel.DEBUG,"Setting,Value"); + } + public NVPCollection ToNVPCollection() + { + NVPCollection nvpCollection=new NVPCollection(); + nvpCollection.Add(new NVP("Verbose",Verbose.ToString())); + nvpCollection.Add(new NVP("KeepSlotPositions",KeepSlotPositions.ToString())); + nvpCollection.Add(new NVP("BenchmarkMode",BenchmarkMode.ToString())); + nvpCollection.Add(new NVP("BenchmarkModeSymbol",BenchmarkModeSymbol.ToString())); + nvpCollection.Add(new NVP("HoldingPeriod",HoldingPeriod.ToString())); + nvpCollection.Add(new NVP("MaxPositions",MaxPositions.ToString())); + nvpCollection.Add(new NVP("NoTradeSymbols",NoTradeSymbols.ToString())); + nvpCollection.Add(new NVP("NoTradeFinancialSymbols",NoTradeFinancialSymbols.ToString())); + nvpCollection.Add(new NVP("Benchmark",Benchmark.ToString())); + nvpCollection.Add(new NVP("MarketCapLowerLimit",MarketCapLowerLimit.ToString())); + nvpCollection.Add(new NVP("UsePEScreen",UsePEScreen.ToString())); + nvpCollection.Add(new NVP("UseEBITDAScreen",UseEBITDAScreen.ToString())); + nvpCollection.Add(new NVP("UseRevenuePerShareScreen",UseRevenuePerShareScreen.ToString())); + nvpCollection.Add(new NVP("UseLowSlopeBetaCheck",UseLowSlopeBetaCheck.ToString())); + nvpCollection.Add(new NVP("LowSlopeBetaDays",LowSlopeBetaDays.ToString())); + nvpCollection.Add(new NVP("LowSlopeBetaThreshhold",LowSlopeBetaThreshhold.ToString())); + nvpCollection.Add(new NVP("UseMACD",UseMACD.ToString())); + nvpCollection.Add(new NVP("MACDSetup",MACDSetup.ToString())); + nvpCollection.Add(new NVP("MACDSignalDays",MACDSignalDays.ToString())); + nvpCollection.Add(new NVP("MACDRejectStrongSellSignals",MACDRejectStrongSellSignals.ToString())); + nvpCollection.Add(new NVP("MACDRejectWeakSellSignals",MACDRejectWeakSellSignals.ToString())); + nvpCollection.Add(new NVP("UseStochastics",UseStochastics.ToString())); + nvpCollection.Add(new NVP("StochasticsSignalDays",StochasticsSignalDays.ToString())); + nvpCollection.Add(new NVP("StochasticsRejectStrongSells",StochasticsRejectStrongSells.ToString())); + nvpCollection.Add(new NVP("StochasticsRejectWeakSells",StochasticsRejectWeakSells.ToString())); + nvpCollection.Add(new NVP("UseFallbackCandidate",UseFallbackCandidate.ToString())); + nvpCollection.Add(new NVP("FallbackCandidate",FallbackCandidate.ToString())); + nvpCollection.Add(new NVP("FallbackCandidateBestOf",FallbackCandidateBestOf.ToString())); + nvpCollection.Add(new NVP("UseMaxPEScreen",UseMaxPEScreen.ToString())); + nvpCollection.Add(new NVP("MaxPE",MaxPE.ToString())); + nvpCollection.Add(new NVP("StrictMaxPE",StrictMaxPE.ToString())); + nvpCollection.Add(new NVP("QualityIndicatorType",QualityIndicatorType.ToString())); + nvpCollection.Add(new NVP("IncludeTradeMasterForSymbolsHeld",IncludeTradeMasterForSymbolsHeld.ToString())); + + nvpCollection.Add(new NVP("UseStopLimits",UseStopLimits.ToString())); + nvpCollection.Add(new NVP("StopLimitRiskPercentDecimal",StopLimitRiskPercentDecimal.ToString())); + nvpCollection.Add(new NVP("StopLimitScalingVolatilityDays",StopLimitScalingVolatilityDays.ToString())); + nvpCollection.Add(new NVP("MinDaysBetweenInitialStopAdjustment",MinDaysBetweenInitialStopAdjustment.ToString())); + nvpCollection.Add(new NVP("MinDaysBetweenStopAdjustments",MinDaysBetweenStopAdjustments.ToString())); + nvpCollection.Add(new NVP("StopLimitPriceTrendDays",StopLimitPriceTrendDays.ToString())); + + nvpCollection.Add(new NVP("UseHedging",UseHedging.ToString())); + nvpCollection.Add(new NVP("HedgeBenchmarkSymbol",HedgeBenchmarkSymbol.ToString())); + nvpCollection.Add(new NVP("HedgeShortSymbol",HedgeShortSymbol.ToString())); + nvpCollection.Add(new NVP("HedgeRiskPercentDecimal",HedgeRiskPercentDecimal.ToString())); + nvpCollection.Add(new NVP("HedgeMinDaysBetweenStopAdjustments",HedgeMinDaysBetweenStopAdjustments.ToString())); + nvpCollection.Add(new NVP("HedgeInitialCash",HedgeInitialCash.ToString())); + nvpCollection.Add(new NVP("HedgeCloseAboveSMANDays",HedgeCloseAboveSMANDays.ToString())); + nvpCollection.Add(new NVP("HedgeBandBreakCheckDays",HedgeBandBreakCheckDays.ToString())); + + return nvpCollection; + } + public static MGSHConfiguration FromNVPCollection(NVPCollection nvpCollection) + { + MGSHConfiguration mgConfiguration=new MGSHConfiguration(); + NVPDictionary nvpDictionary=nvpCollection.ToDictionary(); + mgConfiguration.Verbose=nvpDictionary["Verbose"].Get(); + if(nvpDictionary.ContainsKey("KeepSlotPositions")) mgConfiguration.KeepSlotPositions=nvpDictionary["KeepSlotPositions"].Get(); + mgConfiguration.BenchmarkMode=nvpDictionary["BenchmarkMode"].Get(); + mgConfiguration.BenchmarkModeSymbol=nvpDictionary["BenchmarkModeSymbol"].Get(); + mgConfiguration.HoldingPeriod=nvpDictionary["HoldingPeriod"].Get(); + mgConfiguration.MaxPositions=nvpDictionary["MaxPositions"].Get(); + mgConfiguration.NoTradeSymbols=nvpDictionary["NoTradeSymbols"].Get(); + mgConfiguration.NoTradeFinancialSymbols=nvpDictionary["NoTradeFinancialSymbols"].Get(); + mgConfiguration.Benchmark=nvpDictionary["Benchmark"].Get(); + mgConfiguration.MarketCapLowerLimit=nvpDictionary["MarketCapLowerLimit"].Get(); + mgConfiguration.UsePEScreen=nvpDictionary["UsePEScreen"].Get(); + mgConfiguration.UseMaxPEScreen=nvpDictionary["UseMaxPEScreen"].Get(); + mgConfiguration.MaxPE=nvpDictionary["MaxPE"].Get(); + mgConfiguration.StrictMaxPE=nvpDictionary["StrictMaxPE"].Get(); + mgConfiguration.UseEBITDAScreen=nvpDictionary["UseEBITDAScreen"].Get(); + mgConfiguration.UseRevenuePerShareScreen=nvpDictionary["UseRevenuePerShareScreen"].Get(); + mgConfiguration.UseLowSlopeBetaCheck=nvpDictionary["UseLowSlopeBetaCheck"].Get(); + mgConfiguration.LowSlopeBetaDays=nvpDictionary["LowSlopeBetaDays"].Get(); + mgConfiguration.LowSlopeBetaThreshhold=nvpDictionary["LowSlopeBetaThreshhold"].Get(); + mgConfiguration.UseMACD=nvpDictionary["UseMACD"].Get(); + mgConfiguration.MACDSetup=nvpDictionary["MACDSetup"].Get(); + mgConfiguration.MACDSignalDays=nvpDictionary["MACDSignalDays"].Get(); + mgConfiguration.MACDRejectStrongSellSignals=nvpDictionary["MACDRejectStrongSellSignals"].Get(); + mgConfiguration.MACDRejectWeakSellSignals=nvpDictionary["MACDRejectWeakSellSignals"].Get(); + mgConfiguration.UseStochastics=nvpDictionary["UseStochastics"].Get(); + mgConfiguration.StochasticsSignalDays=nvpDictionary["StochasticsSignalDays"].Get(); + mgConfiguration.StochasticsRejectStrongSells=nvpDictionary["StochasticsRejectStrongSells"].Get(); + mgConfiguration.StochasticsRejectWeakSells=nvpDictionary["StochasticsRejectWeakSells"].Get(); + mgConfiguration.UseFallbackCandidate=nvpDictionary["UseFallbackCandidate"].Get(); + mgConfiguration.FallbackCandidate=nvpDictionary["FallbackCandidate"].Get(); + mgConfiguration.FallbackCandidateBestOf=nvpDictionary["FallbackCandidateBestOf"].Get(); + + if(nvpDictionary.ContainsKey("QualityIndicatorType")) mgConfiguration.QualityIndicatorType=nvpDictionary["QualityIndicatorType"].Get(); + else mgConfiguration.QualityIndicatorType=MGSHQualityIndicator.ToString(MGSHQualityIndicator.QualityType.IDIndicator); + + if(nvpDictionary.ContainsKey("IncludeTradeMasterForSymbolsHeld")) mgConfiguration.IncludeTradeMasterForSymbolsHeld=nvpDictionary["IncludeTradeMasterForSymbolsHeld"].Get(); + else mgConfiguration.IncludeTradeMasterForSymbolsHeld=false; + +// Stop Limits + if(nvpDictionary.ContainsKey("UseStopLimits")) + { + mgConfiguration.UseStopLimits = nvpDictionary["UseStopLimits"].Get(); + mgConfiguration.StopLimitRiskPercentDecimal = nvpDictionary["StopLimitRiskPercentDecimal"].Get(); + mgConfiguration.StopLimitScalingVolatilityDays = nvpDictionary["StopLimitScalingVolatilityDays"].Get(); + mgConfiguration.MinDaysBetweenInitialStopAdjustment = nvpDictionary["MinDaysBetweenInitialStopAdjustment"].Get(); + mgConfiguration.MinDaysBetweenStopAdjustments = nvpDictionary["MinDaysBetweenStopAdjustments"].Get(); + mgConfiguration.StopLimitPriceTrendDays = nvpDictionary["StopLimitPriceTrendDays"].Get(); + } + else + { + mgConfiguration.UseStopLimits = false; + } + +// Hedging + if(nvpDictionary.ContainsKey("UseHedging")) + { + mgConfiguration.UseHedging = nvpDictionary["UseHedging"].Get(); + mgConfiguration.HedgeBenchmarkSymbol = nvpDictionary["HedgeBenchmarkSymbol"].Get(); + mgConfiguration.HedgeShortSymbol = nvpDictionary["HedgeShortSymbol"].Get(); + mgConfiguration.HedgeRiskPercentDecimal = nvpDictionary["HedgeRiskPercentDecimal"].Get(); + mgConfiguration.HedgeMinDaysBetweenStopAdjustments = nvpDictionary["HedgeMinDaysBetweenStopAdjustments"].Get(); + mgConfiguration.HedgeInitialCash = nvpDictionary["HedgeInitialCash"].Get(); + mgConfiguration.HedgeCloseAboveSMANDays = nvpDictionary["HedgeCloseAboveSMANDays"].Get(); + mgConfiguration.HedgeBandBreakCheckDays = nvpDictionary["HedgeBandBreakCheckDays"].Get(); + } + else + { + mgConfiguration.UseHedging=false; + } + return mgConfiguration; + } + public void DisplayConfiguration() + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Verbose,{0}",Verbose)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("KeepSlotPositions,{0}",KeepSlotPositions)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Holding Period,{0}",HoldingPeriod)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MaxPositions,{0}",MaxPositions)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("NoTradeSymbols,{0}",NoTradeSymbols)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("NoTradeFinancialSymbols,{0}",NoTradeFinancialSymbols)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Benchmark,{0}",Benchmark)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MarketCapLowerLimit,{0}",MarketCapLowerLimit)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UsePEScreen,{0}",UsePEScreen)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseMaxPEScreen,{0}",UseMaxPEScreen)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MaxPE,{0}",MaxPE)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StrictMaxPE,{0}",StrictMaxPE)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseEBITDAScreen,{0}",UseEBITDAScreen)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseRevenuePerShareScreen,{0}",UseRevenuePerShareScreen)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseLowSlopeBetaCheck,{0}",UseLowSlopeBetaCheck)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("LowSlopeBetaDays,{0}",LowSlopeBetaDays)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("LowSlopeBetaThreshhold,{0}",LowSlopeBetaThreshhold)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseMACD,{0}",UseMACD)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MACDSetup,{0}",MACDSetup)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MACDSignalDays,{0}",MACDSignalDays)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MACDRejectStrongSellSignals,{0}",MACDRejectStrongSellSignals)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MACDRejectWeakSellSignals,{0}",MACDRejectWeakSellSignals)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseStochastics,{0}",UseStochastics)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StochasticsSignalDays,{0}",StochasticsSignalDays)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StochasticsRejectStrongSells,{0}",StochasticsRejectStrongSells)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StochasticsRejectWeakSells,{0}",StochasticsRejectWeakSells)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseFallbackCandidate,{0}",UseFallbackCandidate)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("FallbackCandidate,{0}",FallbackCandidate)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("FallbackCandidateBestOf,{0}",FallbackCandidateBestOf)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("BenchmarkMode,{0}",BenchmarkMode)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("BenchmarkSymbol,{0}",BenchmarkModeSymbol)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("QualityIndicatorType,{0}",QualityIndicatorType)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("IncludeTradeMasterForSymbolsHeld,{0}",IncludeTradeMasterForSymbolsHeld)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseStopLimits,{0}",UseStopLimits)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StopLimitRiskPercentDecimal,{0}",StopLimitRiskPercentDecimal)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StopLimitScalingVolatilityDays,{0}",StopLimitScalingVolatilityDays)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MinDaysBetweenInitialStopAdjustment,{0}",MinDaysBetweenInitialStopAdjustment)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("MinDaysBetweenStopAdjustments,{0}",MinDaysBetweenStopAdjustments)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("StopLimitPriceTrendDays,{0}",StopLimitPriceTrendDays)); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("UseHedging,{0}",UseHedging)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("HedgeBenchmarkSymbol,{0}",HedgeBenchmarkSymbol)); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("HedgeShortSymbol,{0}",HedgeShortSymbol)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeRiskPercentDecimal,{0}", HedgeRiskPercentDecimal)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeMinDaysBetweenStopAdjustments,{0}", HedgeMinDaysBetweenStopAdjustments)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeInitialCash,{0}", HedgeInitialCash)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeCloseAboveSMANDays,{0}", HedgeCloseAboveSMANDays)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeBandBreakCheckDays,{0}", HedgeBandBreakCheckDays)); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs new file mode 100644 index 0000000..ac0d717 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MarketData.Utils; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHMomentumCandidates : List + { + public MGSHMomentumCandidates() + { + } + public MGSHMomentumCandidates(List momentumCandidates) + { + foreach(MGSHMomentumCandidate momentumCandidate in momentumCandidates)Add(momentumCandidate); + } + } + public class MGSHMomentumCandidate + { + public String Symbol{get;set;} + public DateTime AnalysisDate{get;set;} + public double CumReturn252{get;set;} + public int DayCount{get;set;} + public double IDIndicator{get;set;} // This is the IDIndicator methodology used for quality. This is the default methodology + public double Score{get;set;} // This is the Score methodology used for quality. This one is taken from Andreas Clenow Momentum + public double MaxDrawdown{get;set;} + public double MaxUpside{get;set;} + public double PE{get;set;} + public double Beta{get;set;} + public double Velocity{get;set;} + public long Volume{get;set;} + public double Return1D{get;set;} + public double SharpeRatio{get;set;} + public String ZacksRank{get;set;} + + public static String Header() + { + StringBuilder sb=new StringBuilder(); + sb.Append("Symbol,AnalysisDate,Return,DayCount,IDIndicator,Score,MaxDrawdown,MaxUpside"); + return sb.ToString(); + } + public override String ToString() + { + StringBuilder sb=new StringBuilder(); + sb.Append(Symbol).Append(",").Append(AnalysisDate).Append(",").Append(Utility.FormatPercent(CumReturn252)).Append(",").Append(DayCount).Append(",").Append(IDIndicator).Append(",").Append(Score).Append(","). Append(Utility.FormatPercent(MaxDrawdown)).Append(",").Append(Utility.FormatPercent(MaxUpside)); + return sb.ToString(); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs new file mode 100644 index 0000000..018d4ce --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs @@ -0,0 +1,362 @@ +using System; +using System.Collections.Generic; +using MarketData.MarketDataModel; +using MarketData.DataAccess; +using MarketData.Utils; +using System.Linq; +using MarketData.Numerical; +using MarketData.Cache; + +// Filename: MGSHMomentumGenerator.cs +// Author:Sean Kessler +// Date:02/2025 +// Summary: This model was adapted from th MGMomentum model. This new model adds the following new functionality +// 1) Places stop limits on positions. This is configurable +// 2) Incorporates ability to buy a hedge position. This is configurable +// 3) The model does not sell all positions at month end. Instead, it holds positions until they stop out. This is configurable + +namespace MarketData.Generator.MGSHMomentum +{ + /// Generate momentum selections - + public class MGSHMomentumGenerator + { + public enum MomentumGeneratorConstants{DayCount=252}; // Trading days in one year + + private MGSHMomentumGenerator() + { + } +// These two interfaces are used by the UI so that it can capture the fallback candidates + public static MGSHMomentumCandidates GenerateMomentum(DateTime tradeDate,MGSHConfiguration config) + { + List symbolsHeld=new List(); + return new MGSHMomentumCandidates(GenerateMomentum(tradeDate,symbolsHeld,config).Take(config.MaxPositions).ToList()); + } + public static MGSHMomentumCandidates GenerateMomentumWithFallback(DateTime tradeDate,MGSHConfiguration config) + { + List symbolsHeld=new List(); + MGSHMomentumCandidates momentumCandidates=GenerateMomentum(tradeDate,symbolsHeld,config); + MGSHQualityIndicator qualityIndicator=new MGSHQualityIndicator(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); + MGSHMomentumCandidate momentumCandidate=new MGSHMomentumCandidate(); + 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 MGSHMomentumCandidates(); + momentumCandidates.Add(momentumCandidate); + } + } + } + return momentumCandidates; + } +// This interface is called by the Backtest + public static MGSHMomentumCandidates GenerateMomentum(DateTime tradeDate,List symbolsHeld,MGSHConfiguration config) + { + DateGenerator dateGenerator=new DateGenerator(); + List symbols=PricingDA.GetSymbols(); + MGSHMomentumCandidates momentumCandidates=new MGSHMomentumCandidates(); + MGSHMomentumCandidates highPECandidates=new MGSHMomentumCandidates(); + DateTime startDateOfReturns=dateGenerator.GetPrevMonthEnd(tradeDate,2); + List noTradeSymbols=Utility.ToList(config.NoTradeSymbols); + List noTradeFinancialSymbols=Utility.ToList(config.NoTradeFinancialSymbols); + CandidateViolations candidateViolations = new CandidateViolations(); + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Generate momentum.. examining candidates")); +// Go through the universe of stocks + for(int index=0;indexx.Equals(symbol))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate already held.")); + continue; + } + +// Check if the symbol is in the no trade list (i.e.) Bitcoin etc., + if(noTradeSymbols.Any(x=>x.Equals(symbol))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate in NoTradeSymbol.")); + continue; + } + +// Check MarketCap, EBITDA, PE, and Revenue Per Share + Fundamental fundamental=FundamentalDA.GetFundamentalMaxDate(symbol,tradeDate); + if(null==fundamental) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate no fundamental.")); + continue; + } + + if(!(fundamental.MarketCap>=config.MarketCapLowerLimit)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate MarketCapLimit.")); + continue; + } + + if(config.UseEBITDAScreen && (double.IsNaN(fundamental.EBITDA)||fundamental.EBITDA<=0)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate EBITDA violation.")); + continue; + } + + if(config.UseRevenuePerShareScreen && (double.IsNaN(fundamental.RevenuePerShare)||fundamental.RevenuePerShare<0.00)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate RevenuePerShare violation.")); + 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)) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate PE violation.")); + 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))) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate Financial Sector violation.")); + continue; + } + +// Fetch single day price + Price price=GBPriceCache.GetInstance().GetPrice(symbol,tradeDate); + if(null==price) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate missing price on trade date.")); + continue; + } +// Filter penny stocks - don't trade anything less than $1.00 + if(price.Close<1.00||price.Open<1.00) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate penny stock violation.")); + continue; + } + +// Retrieve prices + Prices prices=null; + prices=GBPriceCache.GetInstance().GetPrices(symbol,tradeDate,(int)MomentumGeneratorConstants.DayCount); + if(null==prices||prices.Count!=(int)MomentumGeneratorConstants.DayCount) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate missing price history.")); + continue; + } + +// 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) + { + candidateViolations.Add(new CandidateViolation(symbol,"Liquidity violation.")); + 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)MGSHMomentumGenerator.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) + { + candidateViolations.Add(new CandidateViolation(symbol,"Beta threshhold violation.")); + 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) + { + candidateViolations.Add(new CandidateViolation(symbol,"MACD Reject Weak Sell violation.")); + continue; + } + if(config.MACDRejectStrongSellSignals && strongSellSignals>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"MACD Reject Strong Sell violation.")); + 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) + { + candidateViolations.Add(new CandidateViolation(symbol,"Stochastics Oscillator Reject Strong Sell violation.")); + continue; + } + if(config.StochasticsRejectWeakSells&&weakSellCount>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"Stochastics Oscillator Reject Weak Sell violation.")); + 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.50 select value).Count()>0) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate pricing contains outliers in the returns.")); + continue; + } +// Cumulative return + double cumulativeReturn=prices.GetCumulativeReturn(); + if(cumulativeReturn<.10) + { + candidateViolations.Add(new CandidateViolation(symbol,"Candidate cumulative returns below threshhold.")); + 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) + { + candidateViolations.Add(new CandidateViolation(symbol,"PE violation.")); + MGSHMomentumCandidate highPECandidate=new MGSHMomentumCandidate(); + 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 + MGSHMomentumCandidate momentumCandidate=new MGSHMomentumCandidate(); + 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 + + if(0!=candidateViolations.Count) + { + MDTrace.WriteLine(LogLevel.DEBUG,"**************** C A N D I D A T E S U M M A R Y ************************"); + IEnumerable> groups = candidateViolations.GroupBy(x => x.ReasonCategory).OrderByDescending(group => group.Count()).Select(group => Tuple.Create(group.Key, group.Count())); + foreach(Tuple group in groups) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Group: {0} Count:{1}",group.Item1, group.Item2)); + } + } + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Considered : {momentumCandidates.Count+candidateViolations.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Disqualified : {candidateViolations.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,String.Format($"Total Eligible : {momentumCandidates.Count}")); + MDTrace.WriteLine(LogLevel.DEBUG,"******************************************************************************************************"); + +// ********************************************************* 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.Count0) + { + int takeCandidates=config.MaxPositions-momentumCandidates.Count; + highPECandidates=new MGSHMomentumCandidates(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 MGSHMomentumCandidate momentumCandidate in highPECandidates select momentumCandidate.Symbol).ToList()))); + } + + MGSHQualityIndicator qualityIndicator=new MGSHQualityIndicator(config.QualityIndicatorType); + if(qualityIndicator.Quality.Equals(MGSHQualityIndicator.QualityType.IDIndicator)) + { + momentumCandidates=new MGSHMomentumCandidates((from MGSHMomentumCandidate momentumCandidate in momentumCandidates orderby momentumCandidate.IDIndicator ascending, momentumCandidate.CumReturn252 descending, momentumCandidate.Return1D descending, momentumCandidate.Volume descending select momentumCandidate).ToList()); + } + else + { + momentumCandidates=new MGSHMomentumCandidates((from MGSHMomentumCandidate 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; + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs new file mode 100644 index 0000000..bd63ee6 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using MarketData.Utils; +using System.Linq; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHPosition + { + public MGSHPosition() + { + CurrentPrice=double.NaN; + } + public MGSHPosition(MGSHPosition position) + { + Symbol = position.Symbol; + PurchaseDate = position.PurchaseDate; + SellDate = position.SellDate; + Shares = position.Shares; + PurchasePrice = position.PurchasePrice; + CurrentPrice = position.CurrentPrice; + Volume = position.Volume; + Return1D = position.Return1D; + ZacksRank = position.ZacksRank; + CumReturn252 = position.CumReturn252; + IDIndicator = position.IDIndicator; + Score=position.Score; + MaxDrawdown = position.MaxDrawdown; + MaxUpside = position.MaxUpside; + Velocity = position.Velocity; + PE = position.PE; + Beta = position.Beta; + InitialStopLimit = position.InitialStopLimit; + TrailingStopLimit = position.TrailingStopLimit; + LastStopAdjustment = position.LastStopAdjustment; + R = position.R; + if(null!=position.Comment)Comment=string.Copy(position.Comment); + } + public String Symbol{get;set;} + public DateTime PurchaseDate{get;set;} + public DateTime SellDate{get;set;} + public double Shares{get;set;} + public double PurchasePrice{get;set;} + public double CurrentPrice{get;set;} + public double Exposure{get{return Shares*PurchasePrice;}} + public double MarketValue{get{return Shares*CurrentPrice;}} + public long Volume{get;set;} + public double Return1D{get;set;} + public double GainLoss{get{return MarketValue-Exposure;}} + public double GainLossPcnt{get{return (MarketValue-Exposure)/Exposure;}} + public String ZacksRank{get;set;} + public double CumReturn252{get;set;} + public double IDIndicator{get;set;} + public double Score{get;set;} + public double MaxDrawdown{get;set;} + public double MaxUpside{get;set;} + public double Velocity{get;set;} + public double PE{get;set;} + public double Beta{get;set;} + public double InitialStopLimit {get; set; } + public double TrailingStopLimit {get; set; } + public DateTime LastStopAdjustment {get; set; } + public double R {get; set; } + public String Comment {get; set; } + + public virtual NVPCollection ToNVPCollection() + { + NVPCollection nvpCollection=new NVPCollection(); + nvpCollection.Add(new NVP("Symbol",Symbol.ToString())); + nvpCollection.Add(new NVP("PurchaseDate",PurchaseDate.ToString())); + nvpCollection.Add(new NVP("SellDate",SellDate.ToString())); + nvpCollection.Add(new NVP("Shares",Shares.ToString())); + nvpCollection.Add(new NVP("PurchasePrice",PurchasePrice.ToString())); + nvpCollection.Add(new NVP("CurrentPrice",CurrentPrice.ToString())); + nvpCollection.Add(new NVP("Volume",Volume.ToString())); + nvpCollection.Add(new NVP("Return1D",Return1D.ToString())); + nvpCollection.Add(new NVP("ZacksRank",ZacksRank)); + nvpCollection.Add(new NVP("CumReturn252",CumReturn252.ToString())); + nvpCollection.Add(new NVP("IDIndicator",IDIndicator.ToString())); + nvpCollection.Add(new NVP("Score",Score.ToString())); + nvpCollection.Add(new NVP("MaxDrawdown",MaxDrawdown.ToString())); + nvpCollection.Add(new NVP("MaxUpside",MaxUpside.ToString())); + nvpCollection.Add(new NVP("Velocity",Velocity.ToString())); + nvpCollection.Add(new NVP("PE",PE.ToString())); + nvpCollection.Add(new NVP("Beta",Beta.ToString())); + nvpCollection.Add(new NVP("InitialStopLimit", InitialStopLimit.ToString())); + nvpCollection.Add(new NVP("TrailingStopLimit", TrailingStopLimit.ToString())); + nvpCollection.Add(new NVP("LastStopAdjustment", LastStopAdjustment.ToString())); + nvpCollection.Add(new NVP("R", R.ToString())); + nvpCollection.Add(new NVP("Comment", Comment?.ToString())); + return nvpCollection; + } + public static MGSHPosition FromNVPCollection(NVPCollection nvpCollection) + { + MGSHPosition position=new MGSHPosition(); + if(0 == nvpCollection.Count) return null; + NVPDictionary nvpDictionary=nvpCollection.ToDictionary(); + position.Symbol=nvpDictionary["Symbol"].Get(); + position.PurchaseDate=nvpDictionary["PurchaseDate"].Get(); + position.SellDate=nvpDictionary["SellDate"].Get(); + position.Shares=nvpDictionary["Shares"].Get(); + position.PurchasePrice=nvpDictionary["PurchasePrice"].Get(); + position.CurrentPrice=nvpDictionary["CurrentPrice"].Get(); + position.Volume=nvpDictionary["Volume"].Get(); + position.Return1D=nvpDictionary["Return1D"].Get(); + if(nvpDictionary.ContainsKey("ZacksRank"))position.ZacksRank=nvpDictionary["ZacksRank"].Get(); + position.CumReturn252=nvpDictionary["CumReturn252"].Get(); + position.IDIndicator=nvpDictionary["IDIndicator"].Get(); + if(nvpDictionary.ContainsKey("MaxDrawdown"))position.MaxDrawdown=nvpDictionary["MaxDrawdown"].Get(); + if(nvpDictionary.ContainsKey("MaxUpside"))position.MaxUpside=nvpDictionary["MaxUpside"].Get(); + position.Velocity=nvpDictionary["Velocity"].Get(); + position.PE=nvpDictionary["PE"].Get(); + position.Beta=nvpDictionary["Beta"].Get(); + if(nvpDictionary.ContainsKey("Score")) position.Score=nvpDictionary["Score"].Get(); + else position.Score=double.NaN; + if(nvpDictionary.ContainsKey("InitialStopLimit")) position.InitialStopLimit=nvpDictionary["InitialStopLimit"].Get(); + else position.InitialStopLimit=double.NaN; + if(nvpDictionary.ContainsKey("TrailingStopLimit")) position.TrailingStopLimit=nvpDictionary["TrailingStopLimit"].Get(); + else position.TrailingStopLimit=double.NaN; + if(nvpDictionary.ContainsKey("LastStopAdjustment")) position.LastStopAdjustment=nvpDictionary["LastStopAdjustment"].Get(); + else position.LastStopAdjustment=Utility.Epoch; + if(nvpDictionary.ContainsKey("Comment")) position.Comment=nvpDictionary["Comment"].Get(); + else position.Comment=null; + if(nvpDictionary.ContainsKey("R")) position.R=nvpDictionary["R"].Get(); + else position.R=double.NaN; + return position; + } + public static void DisplayHeader() + { + MDTrace.WriteLine(LogLevel.DEBUG, "Symbol,Purchase Date,Shares,Purchase Price,Exposure,Volume,Return1D,Sell Date,Sell Price,Market Value,Gain Loss,Gain Loss(%),CumReturn252,IDIndicator,Score,MaxDrawdown,MaxUpside,Velocity,PE,Beta,SharpeRatio,InitialStopLimit,TrailingStopLimit,LastStopAdjustment,Comment"); + } + public void Display() + { + if (Utility.IsEpoch(SellDate) && double.IsNaN(CurrentPrice)) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("{0},{1},{2},{3},{4},{5},{6},N/A,N/A,N/A,N/A,N/A,{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18}", + Symbol, + Utility.DateTimeToStringMMHDDHYYYY(PurchaseDate), + Shares, + Utility.AddQuotes(Utility.FormatCurrency(PurchasePrice)), + Utility.AddQuotes(Utility.FormatCurrency(Exposure)), + Volume, + Utility.AddQuotes(Utility.FormatPercent(Return1D)), + Utility.AddQuotes(Utility.FormatPercent(CumReturn252)), + Utility.AddQuotes(Utility.FormatNumber(IDIndicator)), + Utility.AddQuotes(Utility.FormatNumber(Score)), + Utility.AddQuotes(Utility.FormatPercent(MaxDrawdown)), + Utility.AddQuotes(Utility.FormatPercent(MaxUpside)), + Utility.AddQuotes(Utility.FormatPercent(Velocity)), + Utility.AddQuotes(Utility.FormatNumber(PE)), + Utility.AddQuotes(Utility.FormatNumber(Beta)), + Utility.AddQuotes(Utility.FormatNumber(InitialStopLimit,2)), + Utility.AddQuotes(Utility.FormatNumber(TrailingStopLimit,2)), + Utility.DateTimeToStringMMHDDHYYYY(LastStopAdjustment), + Comment + )); + } + else + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18},{19},{20},{21},{22},{23}", + Symbol, + Utility.DateTimeToStringMMHDDHYYYY(PurchaseDate), + Shares, + Utility.AddQuotes(Utility.FormatCurrency(PurchasePrice)), + Utility.AddQuotes(Utility.FormatCurrency(Exposure)), + Volume, + Utility.AddQuotes(Utility.FormatPercent(Return1D)), + Utility.DateTimeToStringMMHDDHYYYY(SellDate), + Utility.AddQuotes(Utility.FormatCurrency(CurrentPrice)), + Utility.AddQuotes(Utility.FormatCurrency(MarketValue)), + Utility.AddQuotes(Utility.FormatCurrency(GainLoss)), + Utility.FormatPercent(GainLossPcnt), + Utility.AddQuotes(Utility.FormatPercent(CumReturn252)), + Utility.AddQuotes(Utility.FormatNumber(IDIndicator)), + Utility.AddQuotes(Utility.FormatNumber(Score)), + Utility.AddQuotes(Utility.FormatPercent(MaxDrawdown)), + Utility.AddQuotes(Utility.FormatPercent(MaxUpside)), + Utility.AddQuotes(Utility.FormatPercent(Velocity)), + Utility.AddQuotes(Utility.FormatNumber(PE)), + Utility.AddQuotes(Utility.FormatNumber(Beta)), + Utility.AddQuotes(Utility.FormatNumber(InitialStopLimit,2)), + Utility.AddQuotes(Utility.FormatNumber(TrailingStopLimit,2)), + Utility.DateTimeToStringMMHDDHYYYY(LastStopAdjustment), + Comment + )); + } + } + } +// **************************************************************************************************************************************************************** + public class MGSHPositions : List + { + public MGSHPositions() + { + } + + public MGSHPositions(MGSHPositions positions) + { + foreach(MGSHPosition position in positions)Add(position); + } + + public MGSHPositions(List positions) + { + foreach(MGSHPosition position in positions)Add(position); + } + + public MGSHPositions(MGSHPosition position) + { + Add(position); + } + + public void Add(MGSHPositions positions) + { + foreach(MGSHPosition position in positions)Add(position); + } + + public double Exposure + { + get + { + return (from MGSHPosition position in this select position.PurchasePrice*position.Shares).Sum(); + } + } + + public double MarketValue + { + get + { + return (from MGSHPosition position in this select position.CurrentPrice*position.Shares).Sum(); + } + } + + public List Symbols() + { + List symbols = new List(); + foreach(MGSHPosition position in this) + { + symbols.Add(position.Symbol); + } + return symbols; + } + + public NVPCollections ToNVPCollections() + { + NVPCollections nvpCollections=new NVPCollections(); + foreach(MGSHPosition position in this) + { + nvpCollections.Add(position.ToNVPCollection()); + } + return nvpCollections; + } + + public static MGSHPositions FromNVPCollections(NVPCollections nvpCollections) + { + MGSHPositions positions=new MGSHPositions(); + foreach(NVPCollection nvpCollection in nvpCollections) + { + positions.Add(MGSHPosition.FromNVPCollection(nvpCollection)); + } + return positions; + } + + public void DisplayTopFive() + { + MGSHPositions positions = new MGSHPositions(this.OrderByDescending(x => x.GainLossPcnt).Take(5).ToList()); + MGSHPosition.DisplayHeader(); + for (int index = 0; index < positions.Count; index++) + { + MGSHPosition position = positions[index]; + position.Display(); + } + } + + public void DisplayBottomFive() + { + MGSHPositions positions = new MGSHPositions(this.OrderBy(x => x.GainLossPcnt).Take(5).ToList()); + MGSHPosition.DisplayHeader(); + for (int index = 0; index < positions.Count; index++) + { + MGSHPosition position = positions[index]; + position.Display(); + } + } + + public void Display() + { + MGSHPosition.DisplayHeader(); + for(int index=0;indexUtility.IsEpoch(x.SellDate)||double.IsNaN(x.CurrentPrice))) + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("EXPOSURE:{0}", Utility.FormatCurrency(this.Exposure))); + } + else + { + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("GAIN/LOSS {0}", Utility.FormatCurrency(this.Sum(x => x.GainLoss)))); + } + MDTrace.WriteLine(LogLevel.DEBUG,"****************************************************************************************************************************"); + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHQualityIndicator.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHQualityIndicator.cs new file mode 100644 index 0000000..20548f2 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHQualityIndicator.cs @@ -0,0 +1,49 @@ +using System; + +namespace MarketData.Generator.MGSHMomentum +{ + public class MGSHQualityIndicator + { + public enum QualityType{IDIndicator=0,ScoreIndicator=1}; + private QualityType qualityType; + public MGSHQualityIndicator() + { + qualityType=QualityType.IDIndicator; + } + public MGSHQualityIndicator(QualityType qualityType) + { + this.qualityType=qualityType; + } + public MGSHQualityIndicator(String strQualityType) + { + if(null==strQualityType) + { + qualityType=QualityType.IDIndicator; + return; + } + strQualityType=strQualityType.ToUpper(); + if("SCOREINDICATOR".Equals(strQualityType))qualityType=QualityType.ScoreIndicator; + else qualityType=QualityType.IDIndicator; + } + public override String ToString() + { + if(qualityType.Equals(QualityType.ScoreIndicator))return "SCOREINDICATOR"; + return "IDINDICATOR"; + } + public static String ToString(MGSHQualityIndicator.QualityType qualityType) + { + if(qualityType.Equals(QualityType.ScoreIndicator))return "SCOREINDICATOR"; + return "IDINDICATOR"; + } + public static QualityType ToQuality(String strQualityType) + { + if(strQualityType.Equals("SCOREINDICATOR")) return MGSHQualityIndicator.QualityType.ScoreIndicator; + return MGSHQualityIndicator.QualityType.IDIndicator; + } + public QualityType Quality + { + get{return qualityType;} + set{qualityType=value;} + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHSessionManager.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHSessionManager.cs new file mode 100644 index 0000000..24a25d8 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHSessionManager.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MarketData.Utils; +using StopLimits=MarketData.Generator.Model.StopLimits; + +namespace MarketData.Generator.MGSHMomentum +{ +// ***************************************************************************** + public class MGSHSessionManager + { + private const String SIGNATURE="MGSHSESSIONv2.00"; + + public bool SaveSession(MGSHSessionParams sessionParams,String pathSessionFile) + { + try + { + if(null==pathSessionFile)return false; + pathSessionFile=GetSessionFileName(pathSessionFile); + FileStream outStream=new FileStream(pathSessionFile,FileMode.Create); + StreamWriter streamWriter=new StreamWriter(outStream); + streamWriter.WriteLine(SIGNATURE); + streamWriter.WriteLine((new NVP("LastUpdated",sessionParams.LastUpdated.ToString())).ToString()); + streamWriter.WriteLine((new NVP("TradeDate",sessionParams.TradeDate.ToShortDateString())).ToString()); + streamWriter.WriteLine((new NVP("StartDate",sessionParams.StartDate.ToShortDateString())).ToString()); + streamWriter.WriteLine((new NVP("AnalysisDate",sessionParams.AnalysisDate.ToShortDateString())).ToString()); + streamWriter.WriteLine((new NVP("Cycle",sessionParams.Cycle.ToString())).ToString()); + streamWriter.WriteLine((new NVP("CashBalance",sessionParams.CashBalance.ToString())).ToString()); + streamWriter.WriteLine((new NVP("NonTradeableCash",sessionParams.NonTradeableCash.ToString())).ToString()); + streamWriter.WriteLine((new NVP("HedgeCashBalance",sessionParams.HedgeCashBalance.ToString())).ToString()); + + NVPCollection configurationCollection=sessionParams.Configuration.ToNVPCollection(); + streamWriter.WriteLine(configurationCollection.ToString()); + List nvpCollectionsList=sessionParams.ActivePositions.ToNVPCollections(); + int totalPositions=0; + foreach(NVPCollections nvpCollections in nvpCollectionsList) + { + List nvpCollectionsStringList=nvpCollections.ToList(); + totalPositions+=nvpCollectionsStringList.Count; + } + streamWriter.WriteLine((new NVP("TotalActivePositions",totalPositions.ToString())).ToString()); + foreach(NVPCollections nvpCollections in nvpCollectionsList) + { + List nvpCollectionsStringList=nvpCollections.ToList(); + foreach(String str in nvpCollectionsStringList) + { + streamWriter.WriteLine(str); + } + } + NVPCollections allPositionsCollections=sessionParams.AllPositions.ToNVPCollections(); + List nvpAllCollectionsStringList=allPositionsCollections.ToList(); + streamWriter.WriteLine((new NVP("TotalPositions",nvpAllCollectionsStringList.Count.ToString())).ToString()); + foreach(String str in nvpAllCollectionsStringList)streamWriter.WriteLine(str); + + if(null == sessionParams.StopLimits)sessionParams.StopLimits = new StopLimits(); + NVPCollections allStopLimitsCollections=sessionParams.StopLimits.ToNVPCollections(); + List nvpStopLimitsCollectionsStringList=allStopLimitsCollections.ToList(); + streamWriter.WriteLine((new NVP("TotalStopLimits",nvpStopLimitsCollectionsStringList.Count.ToString())).ToString()); + foreach(String str in nvpStopLimitsCollectionsStringList) streamWriter.WriteLine(str); + + if(null == sessionParams.HedgePositions)sessionParams.HedgePositions = new MGSHPositions(); + NVPCollections hedgePositionsCollections=sessionParams.HedgePositions.ToNVPCollections(); + List nvpHedgePositionsStringList=hedgePositionsCollections.ToList(); + streamWriter.WriteLine((new NVP("TotalHedgePositions",nvpHedgePositionsStringList.Count.ToString())).ToString()); + foreach(String str in nvpHedgePositionsStringList)streamWriter.WriteLine(str); + + streamWriter.Flush(); + outStream.Flush(); + streamWriter.Close(); + outStream.Close(); + outStream.Dispose(); + return true; + } + catch(Exception exception) + { + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("{0}",exception.ToString())); + return false; + } + } + public static MGSHSessionParams RestoreSession(String pathSessionFile) + { + FileStream inStream =null; + StreamReader streamReader=null; + try + { + if(!SessionAvailable(pathSessionFile))return null; + MGSHSessionParams sessionParams=new MGSHSessionParams(); + inStream =new FileStream(pathSessionFile,FileMode.Open); + streamReader=new StreamReader(inStream); + String versionInfo=streamReader.ReadLine(); + double version=double.Parse(Utility.BetweenString(versionInfo,"v",null)); + if(version>2.00)return null; + NVP lastUpdated=new NVP(streamReader.ReadLine()); + NVP tradeDate=new NVP(streamReader.ReadLine()); + NVP startDate=new NVP(streamReader.ReadLine()); + NVP analysisDate=new NVP(streamReader.ReadLine()); + NVP cycle=new NVP(streamReader.ReadLine()); + NVP cashBalance=new NVP(streamReader.ReadLine()); + NVP nonTradeableCash=new NVP(streamReader.ReadLine()); + NVP hedgeCashBalance=new NVP(streamReader.ReadLine()); + sessionParams.LastUpdated=lastUpdated.Get(); + sessionParams.TradeDate=tradeDate.Get(); + sessionParams.StartDate=startDate.Get(); + sessionParams.AnalysisDate=analysisDate.Get(); + sessionParams.Cycle=cycle.Get(); + sessionParams.CashBalance=cashBalance.Get(); + sessionParams.NonTradeableCash=nonTradeableCash.Get(); + if(2.00==version)sessionParams.HedgeCashBalance=hedgeCashBalance.Get(); + NVPCollection configurationCollection=new NVPCollection(streamReader.ReadLine()); + sessionParams.Configuration=MGSHConfiguration.FromNVPCollection(configurationCollection); + int totalActivePositions=(new NVP(streamReader.ReadLine())).Get(); + sessionParams.ActivePositions=new MGSHActivePositions(); + for(int positionIndex=0;positionIndex(); + NVPCollections nvpCollections=new NVPCollections(); + for(int positionIndex=0;positionIndex(); + sessionParams.StopLimits=new StopLimits(); + for(int stopLimitIndex=0;stopLimitIndex(); + NVPCollections nvpHedgeCollections=new NVPCollections(); + for(int hedgePositionIndex=0;hedgePositionIndex + { + public MGSHSlotPositions() + { + } + public MGSHSlotPositions(int slot,MGSHPositions positions) + { + for(int index=0;index(); + return slotPosition; + } + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/RealtimeGainLoss.cs b/MarketDataLib/Generator/MGSHMomentum/RealtimeGainLoss.cs new file mode 100644 index 0000000..39aefd6 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/RealtimeGainLoss.cs @@ -0,0 +1,10 @@ +namespace MarketData.Generator.MGSHMomentum +{ + public class RealtimeGainLoss + { + public double Exposure{get;set;} + public double MarketValue{get;set;} + public double GainLoss{get{return MarketValue-Exposure;}} + public double GainLossPercent{get{return Exposure==0?0:(MarketValue-Exposure)/Exposure;}} + } +} diff --git a/MarketDataLib/Generator/MGSHMomentum/ScoreIndicator.cs b/MarketDataLib/Generator/MGSHMomentum/ScoreIndicator.cs new file mode 100644 index 0000000..43087be --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/ScoreIndicator.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MarketData.MarketDataModel; +using MarketData.DataAccess; +using MarketData.Numerical; +using MarketData.Utils; +using System.Linq; +using MarketData.Helper; + +namespace MarketData.Generator.MGSHMomentum +{ + public class ScoreIndicator + { + private ScoreIndicator() + { + } +// This uses the same scoring mechanism as is used in Clenow Momentum which is based upon the log of returns. +// The higher the score the better the rank. The logPrices array gets filled with the most distant price in the lower indices. Hence the backward walk through the prices. + public static double Calculate(Prices prices) + { + double score=double.NaN; + if(null==prices || 0==prices.Count)return double.NaN; + double[] logPrices=new double[prices.Count]; + for(int index=prices.Count-1;index>=0;index--) + { + Price price=prices[index]; + logPrices[(prices.Count-index)-1]=Math.Log(price.Close); + } + LeastSquaresResultWithR2 leastSquaresResult=LeastSquaresHelper.CalculateLeastSquaresWithR2(logPrices); + double slope=leastSquaresResult.Slope; + double annualizedReturn=Math.Pow(Math.Exp(slope),252); + if(slope<0.00)annualizedReturn*=-1.00; + score=leastSquaresResult.RSquared*annualizedReturn; + return score; + } + } +} + diff --git a/MarketDataLib/Generator/MGSHMomentum/SlopeManager.cs b/MarketDataLib/Generator/MGSHMomentum/SlopeManager.cs new file mode 100644 index 0000000..3971756 --- /dev/null +++ b/MarketDataLib/Generator/MGSHMomentum/SlopeManager.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using MarketData.MarketDataModel; +using MarketData.Utils; +using System.Linq; +using MarketData.Numerical; +using System.Text; + +namespace MarketData.Generator.MGSHMomentum +{ + public class SlopeManager + { + private double bandSlopeSpreadKL5; + private double bandSlopeSpreadKL10; + private double bandSlopeSpreadKL30; + private double bandSlopeSpreadKL60; + private double bandSlopeSpreadKL90; + + public SlopeManager(BollingerBandElementsByDate bollingerBandElementsByDate, DateTime analysisDate) + { + bandSlopeSpreadKL5 = GetKLBandSpreadSlope(bollingerBandElementsByDate, analysisDate, 5); + bandSlopeSpreadKL10 = GetKLBandSpreadSlope(bollingerBandElementsByDate, analysisDate, 10); + bandSlopeSpreadKL30 = GetKLBandSpreadSlope(bollingerBandElementsByDate, analysisDate, 30); + bandSlopeSpreadKL60 = GetKLBandSpreadSlope(bollingerBandElementsByDate, analysisDate, 60); + bandSlopeSpreadKL90 = GetKLBandSpreadSlope(bollingerBandElementsByDate, analysisDate, 90); + } + + public bool Verbose { get; set; } = true; + + /// + /// The pattern follows.. 90, 60, 30, 10 ,5 + /// + /// ++0-0 + /// + public bool IsMatchKLSpread(String bandStringKLSpread) + { + StringBuilder sbKLSPread = new StringBuilder(); + sbKLSPread.Append(GetValue(bandSlopeSpreadKL90)).Append(GetValue(bandSlopeSpreadKL60)).Append(GetValue(bandSlopeSpreadKL30)).Append(GetValue(bandSlopeSpreadKL10)).Append(GetValue(bandSlopeSpreadKL5)); + return AreEqual(bandStringKLSpread, sbKLSPread.ToString()); + } + + public String GetKLSpread() + { + StringBuilder sbKLSPread = new StringBuilder(); + sbKLSPread.Append(GetValue(bandSlopeSpreadKL90)).Append(GetValue(bandSlopeSpreadKL60)).Append(GetValue(bandSlopeSpreadKL30)).Append(GetValue(bandSlopeSpreadKL10)).Append(GetValue(bandSlopeSpreadKL5)); + return sbKLSPread.ToString(); + } + + public int FindFirstMatchRtoL(String bandString, char match) + { + for (int index = bandString.Length - 1; index >= 0; index--) + { + if (bandString[index].Equals(match)) { + switch (index) { + case 0: + return 90; + case 1: + return 60; + case 2: + return 30; + case 3: + return 10; + case 4: + return 5; + default: + return 10; + } + } + } + return 10; + } + + /// + /// Where str1 may have a wild card '?' + /// + /// + /// + /// + private bool AreEqual(String str1,String str2) + { + if(str1.Length!=str2.Length)return false; + for(int index=0;index0.00)return "+"; + return "-"; + } + + private double GetKLBandSpreadSlope(BollingerBandElementsByDate bollingerBandElementsByDate, DateTime fromDate, int dayCount) + { + List bollingerBandElements = new List(); + DateGenerator dateGenerator = new DateGenerator(); + List historicalDates = dateGenerator.GenerateHistoricalDates(fromDate, dayCount); + foreach (DateTime historicalDate in historicalDates) { + if (!bollingerBandElementsByDate.ContainsKey(historicalDate)) { + continue; + } + bollingerBandElements.Add(bollingerBandElementsByDate[historicalDate]); + } + List spreadElements = new List(); + foreach (BollingerBandElement bollingerBandElement in bollingerBandElements) { + spreadElements.Add(bollingerBandElement.K - bollingerBandElement.L); + } + double[] elements = spreadElements.ToArray(); + elements = Numerics.Reverse(ref elements); + double[] logElements = new double[elements.Length]; + for (int index = 0; index < elements.Length; index++) { + logElements[index] = Math.Log(elements[index]); + } + LeastSquaresResultWithR2 leastSquaresResult = LeastSquaresHelper.CalculateLeastSquaresWithR2(logElements); + return leastSquaresResult.Slope; + } + + private double GetKBandSlope(BollingerBandElementsByDate bollingerBandElementsByDate,DateTime fromDate, int dayCount) + { + List bollingerBandElements = new List(); + DateGenerator dateGenerator = new DateGenerator(); + List historicalDates = dateGenerator.GenerateHistoricalDates(fromDate, dayCount); + foreach(DateTime historicalDate in historicalDates) + { + if(!bollingerBandElementsByDate.ContainsKey(historicalDate)) + { + continue; + } + bollingerBandElements.Add(bollingerBandElementsByDate[historicalDate]); + } + List bandElements = new List(); + foreach(BollingerBandElement bollingerBandElement in bollingerBandElements) + { + bandElements.Add(bollingerBandElement.K); + } + double[] elements = bandElements.ToArray(); + elements = Numerics.Reverse(ref elements); + double[] logElements = new double[elements.Length]; + for(int index=0;index bollingerBandElements = new List(); + DateGenerator dateGenerator = new DateGenerator(); + List historicalDates = dateGenerator.GenerateHistoricalDates(fromDate, dayCount); + foreach(DateTime historicalDate in historicalDates) + { + if(!bollingerBandElementsByDate.ContainsKey(historicalDate)) + { + continue; + } + bollingerBandElements.Add(bollingerBandElementsByDate[historicalDate]); + } + List bandElements = new List(); + foreach(BollingerBandElement bollingerBandElement in bollingerBandElements) + { + bandElements.Add(bollingerBandElement.L); + } + double[] elements = bandElements.ToArray(); + elements = Numerics.Reverse(ref elements); + double[] logElements = new double[elements.Length]; + for(int index=0;index