Dead Stock Detection & Cascade Markdown Optimization: 4 Criteria, 4 Discount Tiers
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.
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():
| Condition | Discount | Logic |
|---|---|---|
| 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):
| Metric | Value |
|---|---|
| Total inventory value | 17.3M |
| Frozen capital | 10.3M (59.5%) |
| Dead stock SKUs | 574 of 1,000 |
| SKUs with >365 day supply | 109 |
| Markdown candidates | 884 |
| Estimated recovery | 3.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:
- Carrying costs accumulate — 15-25% of inventory value annually (warehousing, insurance, handling)
- Opportunity cost grows — each dollar of frozen capital = $0.30-0.50 in lost annual profit
- 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