From 6f96ba3e2205195cc4773aab86793abc6790490b Mon Sep 17 00:00:00 2001 From: Sean Date: Fri, 14 Feb 2025 18:56:11 -0500 Subject: [PATCH] BetaGenerator and some code cleanup --- .../Generator/MGSHMomentum/MGSHBacktest.cs | 7 +-- .../MGSHMomentum/MGSHConfiguration.cs | 55 +++++++++++++------ .../MGSHMomentum/MGSHMomentumCandidate.cs | 16 ++++-- .../MGSHMomentum/MGSHMomentumGenerator.cs | 33 ++++++----- .../Generator/MGSHMomentum/MGSHPositions.cs | 26 ++------- 5 files changed, 75 insertions(+), 62 deletions(-) diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs index 29c92f9..7b26d7e 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs @@ -748,7 +748,7 @@ namespace MarketData.Generator.MGSHMomentum position.Comment = "Closed due to trailing stop."; HedgeCashBalance+=position.MarketValue; -// This tries to maintain a 10% margin in the hedge cash +// This tries to maintain a hedgeCashMarginPercentDecimal margin in the hedge cash. In other words some % of the hedge gain -> Cash and some -> hedge cash, not allowing hedge cash to grow some margin beyond it's initial setting double hedgeCashMarginPercentDecimal=.10; double hedgeCashTreshholdAmount=Configuration.HedgeInitialCash*(1.00+hedgeCashMarginPercentDecimal); if(HedgeCashBalance > hedgeCashTreshholdAmount) @@ -764,13 +764,13 @@ namespace MarketData.Generator.MGSHMomentum HedgeCashBalance=0.00; } + // Retain this original code for a while until happy with the hedge cash margin approach //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); } @@ -1189,11 +1189,8 @@ namespace MarketData.Generator.MGSHMomentum 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; diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs index 16d98d7..40e2d7f 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHConfiguration.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.InteropServices; using MarketData.Utils; namespace MarketData.Generator.MGSHMomentum @@ -52,6 +53,10 @@ namespace MarketData.Generator.MGSHMomentum public bool UseEBITDAScreen{get;set;} public bool UseRevenuePerShareScreen{get;set;} +// BETA + public bool UseBetaGenerator{get;set;} // If set then the model will use the internal beta generator. Otherwise it will use Beta from Fundamental data + public int UseBetaGeneratorMonths{get;set;} // If UseBetaGenerator=true then UseBetaGeneratorMonths will prescribe the number of months over which to calculate the monthly beta + // 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;} @@ -105,6 +110,10 @@ namespace MarketData.Generator.MGSHMomentum MaxPE=40; StrictMaxPE=false; +// Beta + UseBetaGenerator=true; // The default setting is to calculate our own beta rather than use the beta from the fundamental data + UseBetaGeneratorMonths=24; // The default number of months over which to calculate our monthly return stream for the beta generator + // 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 @@ -213,6 +222,9 @@ namespace MarketData.Generator.MGSHMomentum nvpCollection.Add(new NVP("MaxPricingExceptions",MaxPricingExceptions.ToString())); + nvpCollection.Add(new NVP("UseBetaGenerator",UseBetaGenerator.ToString())); + nvpCollection.Add(new NVP("UseBetaGeneratorMonths",UseBetaGeneratorMonths.ToString())); + return nvpCollection; } public static MGSHConfiguration FromNVPCollection(NVPCollection nvpCollection) @@ -274,22 +286,29 @@ namespace MarketData.Generator.MGSHMomentum } // 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(); - mgConfiguration.HedgeATRMultiplier = nvpDictionary["HedgeATRMultiplier"].Get(); - } - else - { - mgConfiguration.UseHedging=false; - } + 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(); + mgConfiguration.HedgeATRMultiplier = nvpDictionary["HedgeATRMultiplier"].Get(); + } + else + { + mgConfiguration.UseHedging=false; + } + + if(nvpDictionary.ContainsKey("UseBetaGenerator")) + { + mgConfiguration.UseBetaGenerator = nvpDictionary["UseBetaGenerator"].Get(); + mgConfiguration.UseBetaGeneratorMonths = nvpDictionary["UseBetaGeneratorMonths"].Get(); + } + return mgConfiguration; } public void DisplayConfiguration() @@ -355,6 +374,10 @@ namespace MarketData.Generator.MGSHMomentum MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeCloseAboveSMANDays,{0}", HedgeCloseAboveSMANDays)); MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeBandBreakCheckDays,{0}", HedgeBandBreakCheckDays)); MDTrace.WriteLine(LogLevel.DEBUG, String.Format("HedgeATRMultiplier,{0}", HedgeATRMultiplier)); + + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("UseBetaGenerator,{0}", UseBetaGenerator)); + MDTrace.WriteLine(LogLevel.DEBUG, String.Format("UseBetaGeneratorMonths,{0}", UseBetaGeneratorMonths)); + } } } diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs index ac0d717..8516e11 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumCandidate.cs @@ -23,26 +23,32 @@ namespace MarketData.Generator.MGSHMomentum 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"); + sb.Append("Symbol,AnalysisDate,Return,DayCount,IDIndicator,Score"); 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)); + sb.Append(Symbol).Append(","). + Append(AnalysisDate). + Append(","). + Append(Utility.FormatPercent(CumReturn252)). + Append(",").Append(DayCount). + Append(","). + Append(IDIndicator). + Append(","). + Append(Score). + Append(","); return sb.ToString(); } } diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs index 018d4ce..a3f35cf 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHMomentumGenerator.cs @@ -6,6 +6,7 @@ using MarketData.Utils; using System.Linq; using MarketData.Numerical; using MarketData.Cache; +using System.Configuration; // Filename: MGSHMomentumGenerator.cs // Author:Sean Kessler @@ -15,6 +16,8 @@ using MarketData.Cache; // 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 +//BetaGenerator.Beta(symbol,tradeDate,cmtParams.BetaMonths); // beta months is 6 + namespace MarketData.Generator.MGSHMomentum { /// Generate momentum selections - @@ -44,7 +47,6 @@ namespace MarketData.Generator.MGSHMomentum 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; @@ -55,7 +57,6 @@ namespace MarketData.Generator.MGSHMomentum 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); } @@ -181,10 +182,23 @@ namespace MarketData.Generator.MGSHMomentum double[] pricesArray=null; LeastSquaresResult leastSquaresResult; +// Beta - first capture the fundamental beta and then determine whether we will calculate our own + double beta = fundamental.Beta; + + if(config.UseBetaGenerator) + { + beta = BetaGenerator.Beta(symbol, config.UseBetaGeneratorMonths, false); + if(double.IsNaN(beta)) + { + candidateViolations.Add(new CandidateViolation(symbol,"No Beta violation.")); + continue; + } + } + // 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) + if(config.UseLowSlopeBetaCheck && beta >= config.LowSlopeBetaThreshhold) { Prices benchmarkPrices=GBPriceCache.GetInstance().GetPrices(config.Benchmark,tradeDate,config.LowSlopeBetaDays); pricesArray=Numerics.ToDouble(benchmarkPrices.GetPricesLow()); @@ -274,9 +288,6 @@ namespace MarketData.Generator.MGSHMomentum 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) @@ -289,14 +300,11 @@ namespace MarketData.Generator.MGSHMomentum 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.Beta=beta; highPECandidate.Velocity=velocity; highPECandidate.Volume=price.Volume; highPECandidate.Return1D=return1D; - if(null!=zacksRank)highPECandidate.ZacksRank=zacksRank.Rank; highPECandidates.Add(highPECandidate); continue; } @@ -309,14 +317,11 @@ namespace MarketData.Generator.MGSHMomentum 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.Beta=beta; momentumCandidate.Velocity=velocity; momentumCandidate.Volume=price.Volume; momentumCandidate.Return1D=return1D; - if(null!=zacksRank)momentumCandidate.ZacksRank=zacksRank.Rank; momentumCandidates.Add(momentumCandidate); } // for all symbols diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs index b30d2b3..a084870 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHPositions.cs @@ -22,12 +22,9 @@ namespace MarketData.Generator.MGSHMomentum 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; @@ -50,13 +47,10 @@ namespace MarketData.Generator.MGSHMomentum position.CurrentPrice=positionToClone.CurrentPrice; position.Volume=positionToClone.Volume; position.Return1D=positionToClone.Return1D; - position.ZacksRank=positionToClone.ZacksRank; position.CumReturn252=positionToClone.CumReturn252; position.IDIndicator=positionToClone.IDIndicator; position.Score=positionToClone.Score; - position.MaxDrawdown=positionToClone.MaxDrawdown; - position.MaxUpside=positionToClone.MaxUpside; - position.Velocity=positionToClone.Velocity; + position.Velocity=positionToClone.Velocity; position.PE=positionToClone.PE; position.Beta=positionToClone.Beta; position.InitialStopLimit=positionToClone.InitialStopLimit; @@ -80,12 +74,10 @@ namespace MarketData.Generator.MGSHMomentum 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 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;} @@ -107,12 +99,9 @@ namespace MarketData.Generator.MGSHMomentum 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())); @@ -137,11 +126,8 @@ namespace MarketData.Generator.MGSHMomentum 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(); @@ -169,7 +155,7 @@ namespace MarketData.Generator.MGSHMomentum { 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}", + 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}", Symbol, Utility.DateTimeToStringMMHDDHYYYY(PurchaseDate), Shares, @@ -180,8 +166,6 @@ namespace MarketData.Generator.MGSHMomentum 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)), @@ -193,7 +177,7 @@ namespace MarketData.Generator.MGSHMomentum } 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}", + 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}", Symbol, Utility.DateTimeToStringMMHDDHYYYY(PurchaseDate), Shares, @@ -209,8 +193,6 @@ namespace MarketData.Generator.MGSHMomentum 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)),