invent.saleinvent.sale
Research

Dead Stock Detection & Cascade Markdown Optimization: 4 Criteria, 4 Discount Tiers

Dead StockMarkdownsCapital

4-criteria dead stock filter (7 days without sales, not off-season, stock ≥ P10, balance > 0), frozen capital calculation, cascade markdown system at 50/25/15% by seasonality type, and release ratio percentile analysis on real retail chain data.

January 2026

Dead stock — products without a single sale for an extended period — is the largest silent loss in retail. Industry estimates place the global dead stock burden at approximately ~$540 billion annually. But the problem isn't that dead stock exists — it's that standard ERPs can't distinguish truly dead products from seasonally dormant ones. Our system solves this through a 4-criteria filter integrated with the seasonality engine and cascade markdown optimization across 4 tiers.

Why Standard ERP Reporting Fails

Every ERP can generate a "products without sales for N days" report. But a fixed threshold creates two types of errors:

False positives. Seasonal allergy products in December, sunscreen in January, cold remedies in summer — these products aren't dead, they're off-season. Liquidating them means repurchasing at full cost in 3-6 months. A double loss.

Misses. In retail, a specialty product may legitimately sell once every 4 months. A "90 days without sales" threshold passes it as normal. But if that product has 2.3 years of stock (real case: diagnostic test strips — 847 days of supply) — that's a catastrophic capital freeze.

The 4-Criteria Filter

A product is classified as dead only if all four criteria are met simultaneously:

Criterion 1: No sales in 7 days (DEAD_STOCK_RECENT_SALES_DAYS = 7). A deliberately aggressive threshold — subsequent criteria filter out false positives. The function calculate_product_last_sale_dates() scans historical transactions within a 90-day window (DEAD_STOCK_HISTORY_DAYS = 90) and determines the last sale date for each SKU.

Criterion 2: Not off-season (weekly_seasonality_type ≠ 0). The key innovation. The seasonality engine classifies every product-week into one of 5 types. Type 0 is "off-season" — zero sales are expected. Products in type 0 are excluded from dead stock classification regardless of idle duration. The function is_off_season() checks the current week's type for the product's category.

Criterion 3: Stock ≥ P10 percentile (DEAD_STOCK_STOCK_PERCENTILE = 10). Current stock level exceeds the 10th percentile for the category. Filters out products with trivially small quantities — a 2-unit balance isn't worth the operational cost of liquidation. The percentile is calculated by calculate_percentile() across all products with positive balance.

Criterion 4: Positive balance (current_stock > 0). The product physically exists. ERP phantoms (products in the system but not on the shelf) are surprisingly common and pollute the report.

Frozen Capital Calculation

For each dead product:

  • Frozen Capital = current_balance × avg_purchase_price

Cost basis is used, not shelf price. You can't recover shelf price for dead stock — realistic recovery is a fraction of cost. The function calculate_inventory_cost() from the financial_metrics module performs this calculation for all products, normalizing by seasonality.

Cascade Markdown System

After identifying dead stock, the system assigns the optimal discount percentage via calculate_markdown_discount():

ConditionDiscountLogic
Dead stock (all 4 criteria)50%Maximize velocity, accept capital loss
Discount season, type 3 (long block)25%Post-peak clearance
Spot, type 4 (short block)15%Short window, preserve margin
High season with excess (type 1-2, stock >30 days)25%Sell while demand exists

Non-trivial case: a product is dead today but entering peak season in 3 weeks. The system recommends holding, not discounting — because the function has_upcoming_high_season() checks 4 weeks ahead and finds an approaching type 1 or 2. This recommendation preserves full margin on a product that would have been needlessly liquidated.

Release Ratio Percentile Analysis

Alongside direct dead stock classification, a financial analysis runs via release ratio — the ratio of inventory cost to its release rate:

  • release_ratio = inventory_cost / stock_release_rate

Where stock_release_rate (function calculate_stock_release_rate()) is the daily decrease in inventory value, normalized by seasonality. A high release ratio means: lots of money frozen, releasing slowly.

The function calculate_release_ratio_percentiles() computes percentiles across all products (excluding dead stock and near-zero inventory). Then:

  • Bottom 20% by release ratio (slowest) → 50% discount
  • Bottom 40% → 25% discount

This complements direct classification: a product may not pass the 4-criteria dead stock filter (it has occasional sales), but its release ratio is catastrophically poor — money is frozen disproportionately to release speed.

Financial Metrics

The financial_metrics.py module calculates for each product:

  • inventory_cost — current_balance × avg_purchase_price
  • stock_release_rate — daily value decrease, seasonality-normalized
  • margin_rate — daily margin generation, seasonality-normalized
  • turnover_rate — turnover velocity
  • total_profit — (retail_price − purchase_price) × qty_sold

All metrics account for seasonality coefficients for proper normalization. Without this, a product with a winter peak would look "slow" in summer, and a summer product would look "slow" in winter.

Urgency Classification

The coverage.py module assigns each product an urgency level based on days of stock:

  • high — less than 7 days of stock
  • medium — 7-14 days
  • low — more than 14 days

If stock_days < 30, the function calculate_recommended_stock() recommends doubling inventory. This links dead stock detection to the inverse problem: while capital is frozen in dead products, critical A-class products are running out.

Results: Retail Chain Case Study

Deployment across 8 locations (1,000 SKUs, 78 categories, 72 suppliers):

MetricValue
Total inventory value17.3M
Frozen capital10.3M (59.5%)
Dead stock SKUs574 of 1,000
SKUs with >365 day supply109
Markdown candidates884
Estimated recovery3.9M
A-class products (80% of profit)84 SKUs (8%)

Most extreme cases: diagnostic test strips — 847 days of supply (2.3 years). Two enzyme supplement SKUs — combined >2.6M frozen capital with zero sales in 365 days.

ABC analysis revealed a paradox: 84 products generate 80% of profit, and several were at risk of stockout. Mineral supplement (48% margin) had only 25 days of supply. Capital was locked in products nobody was buying while the profit engines were running dry.

The Compounding Effect

Dead stock isn't a static problem. Every month that frozen capital sits on shelves:

  1. Carrying costs accumulate — 15-25% of inventory value annually (warehousing, insurance, handling)
  2. Opportunity cost grows — each dollar of frozen capital = $0.30-0.50 in lost annual profit
  3. Recovery value declines — perishable goods approach expiry, electronics become obsolete, fashion items go out of style

The compounding effect over 12 months can exceed 30% of the annual procurement budget.

Key Takeaways

  • The 4-criteria filter with seasonality integration eliminates the most expensive retail error — liquidating seasonal inventory that would have sold on its own.
  • Cascade markdowns at 50/25/15% are tied to seasonality types, not intuition. Dead stock → 50%. Post-peak → 25%. Spot window → 15%.
  • Release ratio percentiles catch products that aren't formally dead (occasional sales) but freeze capital disproportionately.
  • Dead stock is a capital allocation problem, not an inventory problem. 10.3M locked in non-selling products is 10.3M unavailable for purchasing the 84 A-class SKUs that generate 80% of profit.

Research & Insights

Start free audit
Dead Stock Detection & Cascade Markdown Optimization: 4 Criteria, 4 Discount Tiers — invent.sale