From 554f577803560c17b2b59693c3770641120ce2bb Mon Sep 17 00:00:00 2001 From: Sean Date: Thu, 19 Mar 2026 19:46:19 -0400 Subject: [PATCH] Add logic to all models to avoid selling and immediately buying back the same security as this is considered a Wash Trade and is illegal. --- .../Generator/CMMomentum/CMBacktest.cs | 3 +- .../Generator/CMTrend/CMTTrendModel..cs | 16 +-- .../Generator/MGSHMomentum/MGSHBacktest.cs | 4 +- MarketDataLib/Generator/Momentum/Backtest.cs | 3 +- MarketDataLib/Helper/MarketDataHelper.cs | 105 +++++++++++++++--- ModelHelper/CMMomentumHelper.cs | 2 + Program.cs | 1 + 7 files changed, 109 insertions(+), 25 deletions(-) diff --git a/MarketDataLib/Generator/CMMomentum/CMBacktest.cs b/MarketDataLib/Generator/CMMomentum/CMBacktest.cs index fda35e8..60797d1 100644 --- a/MarketDataLib/Generator/CMMomentum/CMBacktest.cs +++ b/MarketDataLib/Generator/CMMomentum/CMBacktest.cs @@ -392,6 +392,7 @@ namespace MarketData.Generator.CMMomentum else { Positions slotPositions = ActivePositions[slotIndex]; + List slotSymbols = slotPositions.ConvertAll(x=>x.Symbol); // capture sell symbols to exclude from purchases to eliminate wash trades SellPositions(slotPositions, TradeDate); DisplaySales(slotPositions, TradeDate); MDTrace.WriteLine(LogLevel.DEBUG, "********************* S E L L *********************"); @@ -401,7 +402,7 @@ namespace MarketData.Generator.CMMomentum ActivePositions[slotIndex].Clear(); DisplayBalance(); double cashAllocation = Math.Min(CashBalance, (ActivePositions.GetExposure() + CashBalance) / HoldingPeriod); // Even out the cash allocation so that no one slot eats up all the cash - Positions positions=BuyPositions(slotIndex,TradeDate,AnalysisDate,cashAllocation,SymbolsHeld()); + Positions positions=BuyPositions(slotIndex,TradeDate,AnalysisDate,cashAllocation,new List(SymbolsHeld().Concat(slotSymbols))); DisplayPurchases(positions, TradeDate); MDTrace.WriteLine(LogLevel.DEBUG,"********************** B U Y ********************"); positions.Display(); diff --git a/MarketDataLib/Generator/CMTrend/CMTTrendModel..cs b/MarketDataLib/Generator/CMTrend/CMTTrendModel..cs index f48669e..c735277 100644 --- a/MarketDataLib/Generator/CMTrend/CMTTrendModel..cs +++ b/MarketDataLib/Generator/CMTrend/CMTTrendModel..cs @@ -623,7 +623,7 @@ namespace MarketData.Generator.CMTrend MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TradeDate ({0}) must be greater than or equal to the max position date ({1}) in the trade file.",TradeDate.ToShortDateString(),ActivePositions.Select(x => x.PurchaseDate).Max().ToShortDateString())); return result; } - ManageOpenPositions(TradeDate); + List closedSymbols = ManageOpenPositions(TradeDate); // capture any closed symbols so we don't re-enter immediately (wash trades) ManageCandidates(TradeDate); // ************************************************************************************************************************************************************************ // **************************************************************************** N E W P O S I T I O N S ***************************************************************** @@ -641,7 +641,7 @@ namespace MarketData.Generator.CMTrend result.Success=true; return result; } - Positions positions=BuyCandidates(TradeDate,CashBalance,ActivePositions.GetSymbols()); + Positions positions=BuyCandidates(TradeDate,CashBalance,new List(ActivePositions.GetSymbols().Concat(closedSymbols))); if(null != positions && 0!=positions.Count) { MDTrace.WriteLine(LogLevel.DEBUG,"******************** B U Y ********************"); @@ -735,7 +735,7 @@ namespace MarketData.Generator.CMTrend MDTrace.WriteLine(LogLevel.DEBUG,String.Format("TradeDate ({0}) must be greater than or equal to the max position date ({1}) in the trade file.",TradeDate.ToShortDateString(),ActivePositions.Select(x => x.PurchaseDate).Max().ToShortDateString())); return result; } - ManageOpenPositions(TradeDate); + List closedSymbols = ManageOpenPositions(TradeDate); // capture any closed symbols so we don't re-enter immediately (wash trades) ManageCandidates(TradeDate); if(ActivePositions.PositionsOn(TradeDate)>=Parameters.MaxDailyPositions) { @@ -743,7 +743,7 @@ namespace MarketData.Generator.CMTrend result.Success=true; continue; } - Positions positions=BuyCandidates(TradeDate,CashBalance,ActivePositions.GetSymbols()); + Positions positions=BuyCandidates(TradeDate,CashBalance,new List(ActivePositions.GetSymbols().Concat(closedSymbols))); if(null!=positions&&0!=positions.Count) { MDTrace.WriteLine(LogLevel.DEBUG,"******************** B U Y ********************"); @@ -884,11 +884,11 @@ namespace MarketData.Generator.CMTrend // *********************************************************************************************************************************************************************** // *********************************************************************** M A N A G E O P E N P O S I T I O N S ***************************************************** // *********************************************************************************************************************************************************************** - private void ManageOpenPositions(DateTime tradeDate) + private List ManageOpenPositions(DateTime tradeDate) { - if(0==ActivePositions.Count) return; + List closedSymbols = new List(); + if(0==ActivePositions.Count) return closedSymbols; Positions closedPositions = new Positions(); - // List closedPositions=new List(); foreach(Position position in ActivePositions) { Price price=GBPriceCache.GetInstance().GetPrice(position.Symbol,tradeDate); @@ -960,6 +960,8 @@ namespace MarketData.Generator.CMTrend ActivePositions.Remove(closedPosition); } } + if(closedPositions.Count>0)closedSymbols = closedPositions.ConvertAll(x => x.Symbol); + return closedSymbols; } // ********************************************************************************************************************************************************** diff --git a/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs index 6102bf9..778f8d3 100644 --- a/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs +++ b/MarketDataLib/Generator/MGSHMomentum/MGSHBacktest.cs @@ -509,8 +509,10 @@ namespace MarketData.Generator.MGSHMomentum public void SellAndBuySlotPositions(int slotIndex) { MGSHPositions slotPositions=ActivePositions[slotIndex]; + List closedSymbols = new List(); if(!Configuration.KeepSlotPositions) // if we are not configured to KeepSlotPositions then don't sell anything just buy to the max positions allowed for the slot { + closedSymbols = slotPositions.ConvertAll(x => x.Symbol); SellPositions(slotPositions,TradeDate); DisplaySales(slotPositions, TradeDate); MDTrace.WriteLine(LogLevel.DEBUG,"********************* S E L L *********************"); @@ -524,7 +526,7 @@ namespace MarketData.Generator.MGSHMomentum int positionsToFill = MaxPositions-slotPositions.Count; double cashAllocation = (CashBalance / ((HoldingPeriod * MaxPositions) - ActivePositions.GetCount()))*positionsToFill; // split the cash between the total positions we can own less the number of positions we have MGSHPositions positions = null; - positions=BuyPositions(slotIndex, TradeDate,AnalysisDate,cashAllocation,SymbolsHeld(TradeDate), positionsToFill); + positions=BuyPositions(slotIndex, TradeDate,AnalysisDate,cashAllocation,new List(SymbolsHeld(TradeDate).Concat(closedSymbols)), positionsToFill); if(CashBalance-positions.Exposure<=0.00) { MDTrace.WriteLine(LogLevel.DEBUG,String.Format("********** Insufficient funds to make additional purchases.**************")); diff --git a/MarketDataLib/Generator/Momentum/Backtest.cs b/MarketDataLib/Generator/Momentum/Backtest.cs index 69ad222..f4ad6ec 100644 --- a/MarketDataLib/Generator/Momentum/Backtest.cs +++ b/MarketDataLib/Generator/Momentum/Backtest.cs @@ -380,6 +380,7 @@ namespace MarketData.Generator.Momentum else { Positions slotPositions=ActivePositions[slotIndex]; + List closedSymbols = slotPositions.ConvertAll(x => x.Symbol); // capture the closed symbols so we don't re-enter the position (avoid wash trades) SellPositions(slotPositions,TradeDate); MDTrace.WriteLine(LogLevel.DEBUG,"********************* S E L L *********************"); slotPositions.Display(); @@ -392,7 +393,7 @@ namespace MarketData.Generator.Momentum cashAllocation = Math.Min(CashBalance, (ActivePositions.GetExposure() + CashBalance) / (double)HoldingPeriod); MDTrace.WriteLine(LogLevel.DEBUG,String.Format("CASH ALLOCATION:{0}",Utility.FormatCurrency(cashAllocation))); Positions positions = null; - positions=BuyPositions(TradeDate,AnalysisDate,cashAllocation,SymbolsHeld(TradeDate)); + positions=BuyPositions(TradeDate,AnalysisDate,cashAllocation,new List(SymbolsHeld(TradeDate).Concat(closedSymbols))); MDTrace.WriteLine(LogLevel.DEBUG,"********************** B U Y ********************"); positions.Display(); if(CashBalance-positions.Exposure<=0.00) diff --git a/MarketDataLib/Helper/MarketDataHelper.cs b/MarketDataLib/Helper/MarketDataHelper.cs index db6867d..3a1e63f 100644 --- a/MarketDataLib/Helper/MarketDataHelper.cs +++ b/MarketDataLib/Helper/MarketDataHelper.cs @@ -5660,32 +5660,56 @@ namespace MarketData.Helper return price; } + + /// + + /// GetDailyPrices - Retrieve prices from Yahoo. This feed can retrieve historical prices from Yahoo as well as current day price + + /// + + /// + + /// + + /// + + /// + public static Prices GetDailyPrices(String symbol, DateTime startDate, DateTime endDate) + { HttpNetResponse httpNetResponse=null; + try { - if(symbol==null)return null; - CompanyProfile companyProfile=CompanyProfileDA.GetCompanyProfile(symbol); - if(null!=companyProfile && companyProfile.FreezePricing) - { - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Pricing for {0} is frozen.",symbol)); - return null; - } - startDate=startDate.Date; - endDate=endDate.Date; - if(startDate>endDate) + if(symbol==null)return null; + CompanyProfile companyProfile=CompanyProfileDA.GetCompanyProfile(symbol); + if(null!=companyProfile && companyProfile.FreezePricing) { - DateTime tempDate=startDate; - startDate=endDate; - endDate=tempDate; + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Pricing for {0} is frozen.",symbol)); + return null; + } + DateTime period1 = startDate.Date; + DateTime period2 = endDate.Date; + + if(period1>period2) + { + DateTime tempDate=period1; + period1=period2; + period2=tempDate; } - MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[MarketDataHelper:GetDailyPrices]{0} start:{1} end:{2}",symbol,Utility.DateTimeToStringMMHDDHYYYY(startDate),Utility.DateTimeToStringMMHDDHYYYY(endDate))); + if(period1.Equals(period2)) + { + DateGenerator dateGenerator=new DateGenerator(); + period1 = dateGenerator.FindPrevBusinessDay(period2); + } + + MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[MarketDataHelper:GetDailyPrices]{0} start:{1} end:{2}",symbol,Utility.DateTimeToStringMMHDDHYYYY(period1),Utility.DateTimeToStringMMHDDHYYYY(period2))); StringBuilder sb = new StringBuilder(); String strRequest; Prices prices=null; - sb.Append("https://query1.finance.yahoo.com/v7/finance/chart/").Append(symbol).Append("?period1=").Append(Utility.DateToUnixDate(startDate)).Append("&period2=").Append(Utility.DateToUnixDate(endDate)).Append("&interval=1d&events=history"); + sb.Append("https://query1.finance.yahoo.com/v7/finance/chart/").Append(symbol).Append("?period1=").Append(Utility.DateToUnixDate(period1)).Append("&period2=").Append(Utility.DateToUnixDate(period2)).Append("&interval=1d&events=history"); strRequest=sb.ToString(); MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Query:{0}",strRequest)); WebProxy webProxy=HttpNetRequest.GetProxy("GetDailyPrices"); @@ -5694,6 +5718,10 @@ namespace MarketData.Helper if(!httpNetResponse.Success) return null; JObject json=JObject.Parse(httpNetResponse.ResponseString); prices=GetPricesFromJSONString(json,symbol); + if(startDate.Date.Equals(endDate.Date)) + { + prices = new Prices(prices.Where(x => x.Date.Date.Equals(startDate.Date)).ToList()); + } return prices; } catch(Exception exception) @@ -5706,6 +5734,53 @@ namespace MarketData.Helper if(null!=httpNetResponse)httpNetResponse.Dispose(); } } + + //public static Prices GetDailyPrices(String symbol, DateTime startDate, DateTime endDate) + //{ + // HttpNetResponse httpNetResponse=null; + // try + // { + // if(symbol==null)return null; + // CompanyProfile companyProfile=CompanyProfileDA.GetCompanyProfile(symbol); + // if(null!=companyProfile && companyProfile.FreezePricing) + // { + // MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Pricing for {0} is frozen.",symbol)); + // return null; + // } + // startDate=startDate.Date; + // endDate=endDate.Date; + // if(startDate>endDate) + // { + // DateTime tempDate=startDate; + // startDate=endDate; + // endDate=tempDate; + // } + + // MDTrace.WriteLine(LogLevel.DEBUG,String.Format("[MarketDataHelper:GetDailyPrices]{0} start:{1} end:{2}",symbol,Utility.DateTimeToStringMMHDDHYYYY(startDate),Utility.DateTimeToStringMMHDDHYYYY(endDate))); + // StringBuilder sb = new StringBuilder(); + // String strRequest; + // Prices prices=null; + // sb.Append("https://query1.finance.yahoo.com/v7/finance/chart/").Append(symbol).Append("?period1=").Append(Utility.DateToUnixDate(startDate)).Append("&period2=").Append(Utility.DateToUnixDate(endDate)).Append("&interval=1d&events=history"); + // strRequest=sb.ToString(); + // MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Query:{0}",strRequest)); + // WebProxy webProxy=HttpNetRequest.GetProxy("GetDailyPrices"); + // CookieCollection cookieCollection=new CookieCollection(); + // httpNetResponse=HttpNetRequest.GetRequestNoEncodingV3B(strRequest,cookieCollection,webProxy); + // if(!httpNetResponse.Success) return null; + // JObject json=JObject.Parse(httpNetResponse.ResponseString); + // prices=GetPricesFromJSONString(json,symbol); + // return prices; + // } + // catch(Exception exception) + // { + // MDTrace.WriteLine(LogLevel.DEBUG,String.Format("Exception:{0}",exception.ToString())); + // return null; + // } + // finally + // { + // if(null!=httpNetResponse)httpNetResponse.Dispose(); + // } + //} private static Prices GetPricesFromJSONString(JObject json,String symbol) { Prices prices=new Prices(); diff --git a/ModelHelper/CMMomentumHelper.cs b/ModelHelper/CMMomentumHelper.cs index ba601b1..4212db0 100644 --- a/ModelHelper/CMMomentumHelper.cs +++ b/ModelHelper/CMMomentumHelper.cs @@ -123,6 +123,8 @@ namespace MarketData.ModelHelper else cmParams.TargetBeta=1.00; DateTime endDate=DateTime.Now; + if(commandArgs.Has("ENDDATE"))endDate=commandArgs.Get("ENDDATE"); + String pathSessionFileName=commandArgs.Coalesce("SESSIONFILE",null); if(null!=pathSessionFileName) pathSessionFileName=pathSessionFileName.Trim(); cmParams.DisplayHeader(); diff --git a/Program.cs b/Program.cs index 93fc37d..c1be1f9 100644 --- a/Program.cs +++ b/Program.cs @@ -2809,6 +2809,7 @@ namespace MarketData } MDTrace.WriteLine(LogLevel.DEBUG,"Done."); } + public static void LoadAllPricesSymbolDate(String symbol,DateTime startDate) { DateTime endDate = DateTime.Now;