private SmSchedulePricePath[] GetCrossProduct(SmDenseSchedule schedule, SmProductPriceLadder ladder) { var constraintId = schedule.Constraints.GetHashCode(); var key = new { schedule.MarkdownCount, Mask = schedule.ScheduleNumber, ladder.PriceLadderId, constraintId }; if (!_ladderPathCache.TryGetValue(key, out SmSchedulePricePath[] result))
public bool CalculatePricePath( ref SmCalcProduct product, ref SmCalcRecommendation recommendation, SmScenario scenario, int modelId, int revisionId, SmDecayHierarchy decayHierarchy, SmElasticityHierarchy elasticityHierarchy, SmProductPriceLadder priceLadder, int scheduleId, SmSchedulePricePath schedulePricePath) { var scheduleStageMax = scenario.ScheduleStageMax; var currentMarkdownType = product.CurrentMarkdownType; var accumulatedMarkdownCount = product.CurrentMarkdownCount; var totalRevenue = 0M; var totalCost = 0M; var totalMarkdownCost = 0M; var accumulatedMarkdownCountOffset = 0; var previousPrice = product.CurrentSellingPrice; var previousQuantity = product.CurrentSalesQuantity; var accumulatedStockChange = 0; var projections = new List <SmCalcRecommendationProjection>(); var weeks = schedulePricePath.Weeks; var firstWeek = schedulePricePath.Weeks[0]; var pricePath = schedulePricePath.Prices; var salesFlexFactors = product.SalesFlexFactor; for (var week = 0; week < weeks.Length; week++) { // TODO write out price change var priceChange = pricePath[week]; var flexFactor = salesFlexFactors[week]; var weekMarkdownConstraint = product.MarkdownTypeConstraint[week]; var weekMinimumRelativePercentagePriceChange = product.MinimumRelativePercentagePriceChange[week]; var weekMinDiscountNew = product.MinDiscountsNew[week]; var weekMinDiscountFurther = product.MinDiscountsFurther[week]; var weekMaxDiscountNew = product.MaxDiscountsNew[week]; var weekMaxDiscountFurther = product.MaxDiscountsFurther[week]; var price = product.CurrentSellingPrice; var currentCostPrice = product.CurrentCostPrice; if (priceChange != null) { switch (priceLadder.Type) { case SmPriceLadderType.Percent: price = product.OriginalSellingPrice * (1 - priceChange.Value); break; case SmPriceLadderType.Fixed: price = product.OriginalSellingPrice - (1 - priceChange.Value); break; default: throw new ArgumentOutOfRangeException(); } } if (price > product.CurrentSellingPrice) { product.HighPredictionCount++; return(false); } // A change in price advances stage and resets var elasticity = 0.0M; if (previousPrice != price) { accumulatedMarkdownCount++; accumulatedMarkdownCountOffset = 0; var absolutePriceChange = previousPrice - price; var relativePriceChange = (previousPrice - price) / previousPrice; if (absolutePriceChange < product.MinimumAbsolutePriceChange) { product.MinimumAbsolutePriceChangeNotMetCount++; return(false); } if (absolutePriceChange < product.MinimumAbsolutePriceChange) { product.MinimumAbsolutePriceChangeNotMetCount++; return(false); } if (relativePriceChange < weekMinimumRelativePercentagePriceChange) { product.MinimumRelativePercentagePriceChangeNotMetCount++; return(false); } currentMarkdownType = (accumulatedMarkdownCount == 1) ? MarkdownType.New : MarkdownType.Further; if (!weekMarkdownConstraint.HasFlag(currentMarkdownType)) { product.InvalidMarkdownTypeCount++; return(false); } switch (currentMarkdownType) { case MarkdownType.New: if (weekMinDiscountNew > priceChange || weekMaxDiscountNew < priceChange) { product.DiscountPercentageOutsideAllowedRangeCount++; return(false); } else { break; } case MarkdownType.Further: if (weekMinDiscountFurther > priceChange || weekMaxDiscountFurther < priceChange) { product.DiscountPercentageOutsideAllowedRangeCount++; return(false); } else { break; } default: throw new ArgumentOutOfRangeException();; } // Use the elasticity for this calculation if (accumulatedMarkdownCount > 0 && accumulatedMarkdownCountOffset == 0) { var elasticityStage = Math.Min(accumulatedMarkdownCount, Math.Min(elasticityHierarchy.MaxStage, scheduleStageMax)); elasticity = elasticityHierarchy.TryGetValue(elasticityStage, out SmElasticity e) ? e.PriceElasticity : 1.0M; } } else { currentMarkdownType = accumulatedMarkdownCount == 0 && product.CurrentSellingPrice >= product.OriginalSellingPrice ? MarkdownType.FullPrice : MarkdownType.Existing; if (!weekMarkdownConstraint.HasFlag(currentMarkdownType)) { product.InvalidMarkdownTypeCount++; return(false); } } // Get decay var decay = 1.0M; if (accumulatedMarkdownCountOffset > 0) { var decayStage = Math.Min(accumulatedMarkdownCount, Math.Min(decayHierarchy.MaxStage, scheduleStageMax)); decay = decayHierarchy.TryGetValue(decayStage, accumulatedMarkdownCountOffset, out SmDecay d) ? d.Decay : 1.0M; } // Calculate the predicted quantity sold var predictedQuantity = (int)Math.Round(accumulatedMarkdownCount == product.CurrentMarkdownCount ? previousQuantity * decay : previousQuantity * decay * (1 - (((previousPrice - price) / previousPrice) * elasticity))); // Calculate projected stock var stock = Math.Max(product.CurrentStock - accumulatedStockChange, 0); // Ensure predicted quantity sold is non-negative var adjustedQuantity = Math.Max(predictedQuantity, 0) * flexFactor; // Ensure predicted quantity sold is not more than available stock var quantity = Math.Min(stock, adjustedQuantity); // Calculate Metrics var revenue = price * quantity; var cost = currentCostPrice * quantity; var markdownCost = (previousPrice - price) * stock; projections.Add(new SmCalcRecommendationProjection { Week = firstWeek + week, Discount = priceChange ?? product.CurrentMarkdownDepth, Price = price, Quantity = (int)quantity, Revenue = revenue, Stock = stock, MarkdownCost = markdownCost, AccumulatedMarkdownCount = accumulatedMarkdownCount, MarkdownCount = accumulatedMarkdownCount - product.CurrentMarkdownCount, Decay = decay, Elasticity = elasticity, MarkdownType = currentMarkdownType }); totalRevenue += revenue; totalCost += cost; totalMarkdownCost += markdownCost; previousPrice = price; previousQuantity = (int)quantity; accumulatedStockChange += (int)quantity; accumulatedMarkdownCountOffset++; // TODO test accumulatedStockChange > CurrentStock if (totalRevenue < 0) { product.NegativeRevenueCount++; return(false); } } var terminalStock = Math.Max((product.CurrentStock - accumulatedStockChange), 0); var sellThroughTarget = product.CurrentStock - (product.CurrentStock * product.SellThroughTarget); var sellThroughTerminalStock = terminalStock == 0 ? sellThroughTarget : terminalStock; var sellThroughRate = product.SellThroughTarget > 0 ? (sellThroughTarget / sellThroughTerminalStock) : 0; // Assign values recommendation.PricePath = pricePath.Select(x => x ?? 0).ToArray(); recommendation.IsCsp = (schedulePricePath.MarkdownCount == 0); recommendation.TotalMarkdownCount = accumulatedMarkdownCount; recommendation.TotalRevenue = totalRevenue; recommendation.TotalCost = totalCost; recommendation.TotalMarkdownCost = totalMarkdownCost; recommendation.FinalDiscount = projections.Any() ? projections.Last().Discount : 0; recommendation.StockValue = product.CurrentSellingPrice * product.CurrentStock; recommendation.EstimatedProfit = totalRevenue - totalCost; recommendation.EstimatedSales = accumulatedStockChange; recommendation.TerminalStock = terminalStock; recommendation.SellThroughRate = sellThroughRate; recommendation.SellThroughTarget = sellThroughTarget; recommendation.FinalMarkdownType = currentMarkdownType; recommendation.Projections = projections; return(true); }