Example #1
0
        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))
Example #2
0
        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);
        }