diff --git a/METHODOLOGY.md b/METHODOLOGY.md index afa7307..ce43adb 100644 --- a/METHODOLOGY.md +++ b/METHODOLOGY.md @@ -22,22 +22,35 @@ the narrative is coherent, not pulled from Hyundai's actuals. ## 3. Core formulas +The central correction over a naive DSI model: **above-target stock is not automatically a +liquidation candidate.** Stock that will sell through within a patience window at current +demand should simply have its replenishment paused — it *bleeds off* at no clearance cost. +Only the residual still sitting after the window is a **structural overhang** worth a +clearance haircut. This is what stops the model from recommending you discount demand you +already have. + ``` dailySales = avgMonthlySales × (1 + demandShift) / 30 DSI = stock / dailySales # days of inventory on hand targetStock = targetDSI[category] × dailySales -excessUnits = max(0, stock − targetStock) -excessCapital = excessUnits × unitCost -carryingOnExcess= excessCapital × carryingRate # annual $ bled on the excess -liquidationLoss = excessCapital × liquidationDiscount # one-time clearance haircut -recaptured = excessCapital × (1 − liquidationDiscount) -netFirstYear = carryingOnExcess − liquidationLoss +excessUnits = max(0, stock − targetStock) # gross above target +windowUnits = dailySales × 30 × clearWindowMonths # what sells within the patience window +bleedUnits = min(excessUnits, windowUnits) # clears organically — no action +overhangUnits = max(0, excessUnits − bleedUnits) # structural overhang — the actionable base +overhangCapital = overhangUnits × unitCost +monthsToClear = excessUnits / (dailySales × 30) # organic sell-through of the excess + +carryOnOverhang = overhangCapital × carryingRate # annual $ bled on the overhang +liquidationLoss = overhangCapital × liquidationDiscount # one-time clearance haircut +recaptured = overhangCapital × (1 − liquidationDiscount) +netBenefit = carryOnOverhang − liquidationLoss portfolioDSI = Σ stock / Σ dailySales # volume-weighted, not a simple average ``` **Status rule (segment-specific, not one universal cutoff):** -- `At Risk` — `DSI > targetDSI` (over-stocked relative to the segment's velocity) +- `Overhang` — `overhangUnits > 0` (above target *and* won't clear within the window → clearance candidate) +- `Bleeding Off` — above target but `overhangUnits = 0` (self-resolves once replenishment pauses → no clearance) - `Stockout Risk` — `DSI < 0.5 × targetDSI` (too lean; turning away serviceable demand) - `Healthy` — otherwise @@ -53,45 +66,63 @@ segments do not. A uniform threshold would contradict the thesis. | SUV | 60 | Mainstream mid-velocity | | Volume | 45 | High turnover; lean target protects ROIC | -## 5. Decision rule +## 5. Decision rule (two steps) + +1. **Pause, don't dump.** For every above-target pool, first stop replenishment and let it + bleed off. Anything that clears within `clearWindowMonths` never warrants a clearance + haircut — liquidating demand you already have is value-destructive. +2. **Clear the overhang on net benefit.** For the structural overhang that remains, act + **only when `netBenefit > 0`** — the annual carrying cost avoided exceeds the one-time + clearance loss. Because the overhang sits *beyond* the window by construction, annual + carry is the right horizon to weigh against the one-time haircut. -For each above-target pool, act **only when `netFirstYear > 0`** — i.e. the annual -carrying cost avoided exceeds the one-time clearance loss. Recaptured capital is then -redeployed against `Stockout Risk` pools first (relieving lost-sale demand), with the -remainder funding strategic EV / growth priorities. This resolves the -redirect-vs-liquidate distinction: transfer where a constrained pool can absorb units, -clear only where none can. +Recaptured capital is then redeployed against `Stockout Risk` pools first (relieving +lost-sale demand), with the remainder funding strategic EV / growth priorities. This is why +the model clears the IONIQ 5 (soft demand → a months-long overhang) but *not* the Tucson +(above target, yet sells through in days). It also resolves the redirect-vs-liquidate +distinction: transfer where a constrained pool can absorb units, clear only where none can. ## 6. Scenario parameters (defaults & ranges) | Parameter | Default | Range | Source logic | |---|---|---|---| | `carryingRate` | 18%/yr | 8–28% | Floorplan interest (~8%) + depreciation (~7%) + storage/insurance (~3%) | -| `liquidationDiscount` | 6% | 2–15% | Wholesale/auction haircut to clear excess units | +| `liquidationDiscount` | 6% | 2–15% | Wholesale/auction haircut to clear excess units (see §12 for the channel ladder) | +| `clearWindowMonths` | 2 mo | 1–6 mo | Patience window: above-target stock that sells through within it bleeds off rather than being cleared | | `demandShift` | 0% | −20% to +20% | Demand stress test | ## 7. Baseline results (default assumptions) | Metric | Value | |---|---| -| Capital deployed | $2.87B | -| Capital above target | $412M (14% of deployed) | -| Annualized carrying cost on excess | $74M/yr | -| Capital recaptured (net of discount) | $387M | -| One-time clearance loss | $25M | -| First-year net benefit | $49M | +| Capital deployed | $2.80B | +| Capital above target (gross) | $603M (22% of deployed) | +| Structural overhang (actionable) | $320M (11% of deployed) | +| Annualized carrying cost on overhang | $58M/yr | +| Capital recaptured (net of discount) | $301M | +| One-time clearance loss | $19M | +| Net benefit | $38M | | Stockout-risk funding gap | $216M (fully covered by recaptured capital) | -| Portfolio DSI (volume-weighted) | 50 days | +| Portfolio DSI (volume-weighted) | 52 days | +| Overhang pools / bleeding-off pools | 3 / 1 | + +Note the gap between **$603M above target** and the **$320M actually cleared**: ~$283M of +above-target stock bleeds off on its own once replenishment pauses, and is never liquidated. ## 8. Sensitivity (illustrative) -- **Carrying rate is the swing factor.** At 8% the annual bleed falls to ~$33M and several - pools no longer clear the net-benefit hurdle; at 28% it rises past ~$115M and clearance is - obviously worth it. This is why the rate assumption must be owned by Finance, not assumed. -- **Clearance discount** mostly affects *how much* is recaptured, not *whether* to act, - until it approaches the carrying rate. -- **Demand shift** moves pools across thresholds — a −10% shock pushes additional volume/SUV - pools above target; +10% relieves them. The status logic recomputes live. +- **The bleed-off window decides how much is even actionable.** This is the parameter that + encodes "don't liquidate demand you still have." At a 1-month window more above-target stock + counts as overhang; widen it toward 6 months and most pools are judged self-resolving, so the + overhang — and the recommended clearance — shrinks toward zero. At the 2-month default, $320M + of the $603M above-target is overhang. +- **Carrying rate** scales the annual bleed on the overhang (≈$26M at 8%, ≈$90M at 28% on the + baseline overhang) and, with the discount, sets whether clearing clears the net-benefit hurdle. + It must be owned by Finance, not assumed. +- **Clearance discount** mostly affects *how much* is recaptured, not *whether* to act, until it + approaches the carrying rate. +- **Demand shift** moves pools across thresholds and resizes the overhang — a −10% shock lengthens + sell-through (more overhang); +10% lets more stock bleed off. The status logic recomputes live. ## 9. Known limitations (and the honest next steps) @@ -100,8 +131,9 @@ clear only where none can. - **Contribution margin not modeled**, so redeployment upside is stated as the funded demand gap, not an incremental-profit figure. *Next step: per-segment margin to value recaptured capital.* - **Steady-state demand** — no seasonality, model-year transitions, or incentive elasticity. -- **Single-period view** — `netFirstYear` mixes a recurring annual saving with a one-time loss; - a full NPV would discount multi-year carrying savings. *Next step: multi-period NPV.* +- **Single-period view** — `netBenefit` weighs one year of carry on the overhang against a one-time + loss. The bleed-off window removes the cruder "annual-vs-one-time always clears" bias, but a true + model would integrate carry over each pool's expected hold path. *Next step: multi-period NPV.* - Figures are illustrative and not Hyundai actuals. ## 10. Retail & DMS layer @@ -147,3 +179,39 @@ from the OEM layer, now visible as floorplan cost. yet modeled (would quantify the *margin* cost of aging, not just floorplan); dealer-trade logistics cost is assumed negligible; and a live DMS integration (vs. illustrative aging vectors) is the obvious productionization step. + +## 11. Whose capital is it — OEM vs. dealer floorplan + +A deliberate simplification worth stating plainly: the OEM layer values the **entire** national +pipeline at OEM wholesale cost and frames clearing it as recovering OEM capital. In reality, +once a unit is wholesaled to a franchised dealer it sits on the **dealer's floorplan**, not the +OEM's balance sheet — which is exactly what the Retail & DMS layer (§10) measures. The two +layers describe the same metal from two balance sheets: + +| Stage | Who owns it | Where the carry lands | +|---|---|---| +| In production / in-transit / port / unallocated | **OEM** | OEM working capital | +| Allocated & wholesaled to a dealer (on the lot) | **Dealer** | Dealer floorplan interest (Retail layer) | + +The OEM's *direct* capital-at-risk is therefore the un-wholesaled slice plus its exposure to +incentives and residual support; the on-lot overhang is borne by dealers and influenced by the +OEM through **allocation and incentive policy**, not by repossessing units. Figures are stated on +a single OEM-wholesale-cost basis for clarity, with the dealer view reconciled in §10. *Next step: +split the pipeline into OEM-owned vs. dealer-floorplanned and carry each at its own rate.* + +## 12. How the overhang actually clears (the "6%" unpacked) + +`liquidationDiscount` is a single blended haircut; in practice clearance runs down a ladder of +channels, each with a different cost and side effect — there is no single anonymous buyer: + +| Channel | Buyer | Typical haircut | Side effect | +|---|---|---|---| +| Retail incentive | Ordinary retail customer | ~2–8% (subvented APR/lease, cash) | Pulls demand forward; brand-equity drag on luxury | +| Fleet / commercial | Rental & commercial fleets | ~8–15% | Floods the used market later → **depresses residuals** | +| Dealer trade / wholesale | Other dealers | ~10–20% | Logistics cost; limited absorbing capacity | +| Auction / remarketing | Dealers & exporters | ~15–30% | Last resort for 90+ day units (see §10 curtailment) | + +The default 6% corresponds to a **modest retail-incentive bump** — the cheapest rung. The model +does not yet price the residual-value contamination from the fleet/auction rungs, which would feed +back into a higher effective EV carrying rate. *Next step: a channel-mix input that blends the +haircut and routes the residual hit back into `carryingRate`.* diff --git a/README.md b/README.md index 4f4f8f0..2ef3a9e 100644 --- a/README.md +++ b/README.md @@ -18,23 +18,27 @@ one place while demand goes unserved in another. ## The approach Measure each model's **Days Sales of Inventory (DSI)** against a **segment-specific target**, -then apply a **net-benefit decision rule**: clear above-target inventory *only when the annual -carrying cost avoided exceeds the one-time clearance loss*, and redeploy the recaptured capital -to relieve stockout-risk segments first. +separate stock that will **bleed off on its own** (just pause replenishment) from a genuine +**structural overhang**, then clear only that overhang *when the annual carrying cost avoided +exceeds the one-time clearance loss* — redeploying the recaptured capital to relieve +stockout-risk segments first. The point of the bleed-off step: never discount demand you +already have. ## Key results (illustrative baseline) | Metric | Value | |---|---| -| Capital deployed | **$2.87B** | -| Capital above segment target | **$412M** (14%) | -| Annualized carrying cost bled | **$74M/yr** | -| Capital recaptured | **$387M** | -| First-year net benefit | **$49M** | +| Capital deployed | **$2.80B** | +| Capital above segment target (gross) | **$603M** (22%) | +| Structural overhang (won't bleed off) | **$320M** (11%) | +| Annualized carrying cost on overhang | **$58M/yr** | +| Capital recaptured | **$301M** | +| Net benefit | **$38M** | | Demand-constrained gap funded | **$216M**, fully covered | -The recaptured capital more than covers the volume shortfall — the EV-glut and slow-luxury pools -fund the lean volume line, with ~$171M left for strategic priorities. Full derivation in +Most above-target stock isn't cleared — it *bleeds off* once replenishment pauses; only the **$320M +structural overhang** (EV-glut + slow-luxury) is liquidated. The recaptured capital more than covers +the volume shortfall, with **~$85M** left for strategic priorities. Full derivation in [METHODOLOGY.md](./METHODOLOGY.md). ## How it's structured @@ -43,9 +47,10 @@ The app is organized as four role-targeted views: 1. **Strategy Brief** — answer-first recommendation with quantified impact, then a Situation / Complication / Resolution case. *(Consulting framing.)* -2. **Decision Dashboard** — segment-targeted DSI table, corrected and accurately-labeled KPIs, - a ranked net-benefit recommendation engine, and **live scenario sliders** (carrying rate, - clearance discount, demand shift) so every figure can be stress-tested. *(Planning / analytics.)* +2. **Decision Dashboard** — segment-targeted DSI table that separates a **structural overhang** + from above-target stock that simply **bleeds off**, accurately-labeled KPIs, a ranked + net-benefit recommendation engine, and **live scenario sliders** (carrying rate, clearance + discount, bleed-off window, demand shift) so every figure can be stress-tested. *(Planning / analytics.)* 3. **Execution Plan** — phased rollout, RACI decision rights, governance KPIs, and a risk/mitigation register. *(Program management.)* 4. **Retail & DMS** — drills the OEM thesis down to the dealer P&L: DMS-sourced aging buckets, @@ -62,6 +67,8 @@ This was deliberately built to survive a probing interview: and a volume-weighted "Portfolio DSI," not a count ratio mislabeled as a financial return. - **The headline tradeoff is actually computed** — carrying cost avoided vs. clearance loss, per pool, with an explicit act/hold rule — not asserted in prose. +- **Above-target ≠ liquidate** — the model strips out inventory that sells through on its own and + only clears the structural overhang, so it never recommends discounting demand you already have. - **Targets are segment-specific**, consistent with the core thesis. - **Assumptions are owned and sourced**, with sensitivity and limitations documented openly. diff --git a/src/App.tsx b/src/App.tsx index a462451..ca8d406 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,14 +42,18 @@ interface InventoryItem { unitCost: number; // OEM wholesale cost / unit (USD) } +// Demand is calibrated so the slow segments (EV, luxury) carry genuinely soft +// run-rates — that is what creates a *structural overhang* (inventory that will +// not bleed off on its own), as opposed to above-target stock that simply needs +// a production pause. See METHODOLOGY.md §3. const HYUNDAI_INVENTORY: InventoryItem[] = [ - { id: 1, model: 'Genesis G80', segment: 'Luxury Sedan', category: 'Luxury', stock: 3400, avgMonthlySales: 1200, unitCost: 48000 }, - { id: 2, model: 'Genesis GV80', segment: 'Luxury SUV', category: 'Luxury', stock: 2600, avgMonthlySales: 950, unitCost: 55000 }, - { id: 3, model: 'Hyundai IONIQ 5', segment: 'EV SUV', category: 'EV', stock: 12000, avgMonthlySales: 3000, unitCost: 40000 }, - { id: 4, model: 'Hyundai Santa Fe', segment: 'Mid-size SUV', category: 'SUV', stock: 21000, avgMonthlySales: 12000, unitCost: 30000 }, + { id: 1, model: 'Genesis G80', segment: 'Luxury Sedan', category: 'Luxury', stock: 3400, avgMonthlySales: 650, unitCost: 48000 }, + { id: 2, model: 'Genesis GV80', segment: 'Luxury SUV', category: 'Luxury', stock: 2600, avgMonthlySales: 520, unitCost: 55000 }, + { id: 3, model: 'Hyundai IONIQ 5', segment: 'EV SUV', category: 'EV', stock: 14000, avgMonthlySales: 1600, unitCost: 40000 }, + { id: 4, model: 'Hyundai Santa Fe', segment: 'Mid-size SUV', category: 'SUV', stock: 21000, avgMonthlySales: 11000, unitCost: 30000 }, { id: 5, model: 'Hyundai Elantra', segment: 'Compact Sedan', category: 'Volume', stock: 9000, avgMonthlySales: 14000, unitCost: 18000 }, - { id: 6, model: 'Hyundai Palisade', segment: 'Full-size SUV', category: 'SUV', stock: 13500, avgMonthlySales: 9000, unitCost: 42000 }, - { id: 7, model: 'Hyundai Tucson', segment: 'Compact SUV', category: 'Volume', stock: 30000, avgMonthlySales: 15000, unitCost: 24000 }, + { id: 6, model: 'Hyundai Palisade', segment: 'Full-size SUV', category: 'SUV', stock: 13500, avgMonthlySales: 8200, unitCost: 42000 }, + { id: 7, model: 'Hyundai Tucson', segment: 'Compact SUV', category: 'Volume', stock: 24000, avgMonthlySales: 15000, unitCost: 24000 }, ]; // Segment-specific DSI targets. High-margin / low-velocity segments tolerate @@ -70,15 +74,17 @@ interface ScenarioParams { carryingRate: number; // annual carrying cost as % of unit cost (floorplan + depreciation + storage) liquidationDiscount: number; // one-time haircut to clear excess units demandShift: number; // +/- shock applied to monthly sales + clearWindowMonths: number; // patience window — excess that bleeds off within it is not a clearance candidate } const DEFAULT_PARAMS: ScenarioParams = { carryingRate: 0.18, liquidationDiscount: 0.06, demandShift: 0, + clearWindowMonths: 2, }; -type Status = 'At Risk' | 'Healthy' | 'Stockout Risk'; +type Status = 'Overhang' | 'Bleeding Off' | 'Healthy' | 'Stockout Risk'; interface ProcessedItem extends InventoryItem { dailySales: number; @@ -86,14 +92,18 @@ interface ProcessedItem extends InventoryItem { targetDSI: number; targetStock: number; status: Status; - excessUnits: number; + excessUnits: number; // units above the segment target (gross) excessCapital: number; + bleedUnits: number; // excess that clears organically within the patience window + overhangUnits: number; // structural overhang — excess that will NOT clear within the window + overhangCapital: number; + monthsToClear: number; // organic sell-through of the excess at current demand (months) shortfallUnits: number; shortfallCapital: number; - carryingOnExcess: number; // annual $ bleed on the excess - recaptured: number; // capital freed if excess cleared (net of discount) - liquidationLoss: number; // one-time loss to clear excess - netFirstYear: number; // carryingOnExcess - liquidationLoss + carryOnOverhang: number; // annual $ carry on the overhang (the recurring money at stake) + recaptured: number; // capital freed if the overhang is cleared (net of discount) + liquidationLoss: number; // one-time loss to clear the overhang + netBenefit: number; // carryOnOverhang - liquidationLoss totalCapital: number; } @@ -105,19 +115,35 @@ function process(items: InventoryItem[], p: ScenarioParams): ProcessedItem[] { const targetDSI = SEGMENT_TARGET_DSI[item.category]; const targetStock = Math.round(targetDSI * dailySales); - let status: Status = 'Healthy'; - if (dsi > targetDSI) status = 'At Risk'; - else if (dsi < targetDSI * STOCKOUT_FACTOR) status = 'Stockout Risk'; - const excessUnits = Math.max(0, item.stock - targetStock); const excessCapital = excessUnits * item.unitCost; + + // The key correction: above-target stock that would sell through within the + // patience window at current demand is NOT worth liquidating — you simply + // pause replenishment and let it bleed off. Only the remainder that will + // still be sitting after the window is a structural overhang. + const windowUnits = dailySales * 30 * p.clearWindowMonths; + const bleedUnits = Math.min(excessUnits, windowUnits); + const overhangUnits = Math.max(0, excessUnits - bleedUnits); + const overhangCapital = overhangUnits * item.unitCost; + const monthsToClear = dailySales > 0 ? excessUnits / (dailySales * 30) : 0; + const shortfallUnits = Math.max(0, targetStock - item.stock); + + let status: Status = 'Healthy'; + if (dsi < targetDSI * STOCKOUT_FACTOR) status = 'Stockout Risk'; + else if (overhangUnits > 0) status = 'Overhang'; + else if (excessUnits > 0) status = 'Bleeding Off'; + const shortfallCapital = status === 'Stockout Risk' ? shortfallUnits * item.unitCost : 0; - const carryingOnExcess = excessCapital * p.carryingRate; - const liquidationLoss = excessCapital * p.liquidationDiscount; - const recaptured = excessCapital * (1 - p.liquidationDiscount); - const netFirstYear = carryingOnExcess - liquidationLoss; + // Carry vs. clear is assessed on the overhang only — the part that will not + // self-resolve. By construction it sits beyond the window, so annual carry + // is the right horizon to weigh against the one-time clearance loss. + const carryOnOverhang = overhangCapital * p.carryingRate; + const liquidationLoss = overhangCapital * p.liquidationDiscount; + const recaptured = overhangCapital * (1 - p.liquidationDiscount); + const netBenefit = carryOnOverhang - liquidationLoss; return { ...item, @@ -128,12 +154,16 @@ function process(items: InventoryItem[], p: ScenarioParams): ProcessedItem[] { status, excessUnits, excessCapital, + bleedUnits, + overhangUnits, + overhangCapital, + monthsToClear, shortfallUnits, shortfallCapital, - carryingOnExcess, + carryOnOverhang, recaptured, liquidationLoss, - netFirstYear, + netBenefit, totalCapital: item.stock * item.unitCost, }; }); @@ -141,20 +171,22 @@ function process(items: InventoryItem[], p: ScenarioParams): ProcessedItem[] { interface Portfolio { capitalDeployed: number; - capitalAboveTarget: number; - carryingBleed: number; // annual $ bleed on above-target capital + capitalAboveTarget: number; // gross above target (before bleed-off) + overhangCapital: number; // structural overhang — the actionable base + carryingBleed: number; // annual $ carry on the overhang portfolioDSI: number; // volume-weighted days on hand recapturable: number; liquidationLossTotal: number; - netFirstYearTotal: number; + netBenefitTotal: number; stockoutGapCapital: number; - atRiskCount: number; + overhangCount: number; } function summarize(rows: ProcessedItem[]): Portfolio { const capitalDeployed = rows.reduce((s, r) => s + r.totalCapital, 0); const capitalAboveTarget = rows.reduce((s, r) => s + r.excessCapital, 0); - const carryingBleed = rows.reduce((s, r) => s + r.carryingOnExcess, 0); + const overhangCapital = rows.reduce((s, r) => s + r.overhangCapital, 0); + const carryingBleed = rows.reduce((s, r) => s + r.carryOnOverhang, 0); const totalStock = rows.reduce((s, r) => s + r.stock, 0); const totalDaily = rows.reduce((s, r) => s + r.dailySales, 0); const recapturable = rows.reduce((s, r) => s + r.recaptured, 0); @@ -163,13 +195,14 @@ function summarize(rows: ProcessedItem[]): Portfolio { return { capitalDeployed, capitalAboveTarget, + overhangCapital, carryingBleed, portfolioDSI: totalDaily > 0 ? Math.round(totalStock / totalDaily) : 0, recapturable, liquidationLossTotal, - netFirstYearTotal: carryingBleed - liquidationLossTotal, + netBenefitTotal: carryingBleed - liquidationLossTotal, stockoutGapCapital, - atRiskCount: rows.filter((r) => r.status === 'At Risk').length, + overhangCount: rows.filter((r) => r.status === 'Overhang').length, }; } @@ -190,7 +223,8 @@ const num = (n: number): string => n.toLocaleString('en-US'); const pct = (n: number): string => `${(n * 100).toFixed(0)}%`; const STATUS_STYLE: Record = { - 'At Risk': 'text-red-600 bg-red-50 border-red-200', + Overhang: 'text-red-600 bg-red-50 border-red-200', + 'Bleeding Off': 'text-sky-600 bg-sky-50 border-sky-200', Healthy: 'text-green-600 bg-green-50 border-green-200', 'Stockout Risk': 'text-amber-600 bg-amber-50 border-amber-200', }; @@ -227,7 +261,8 @@ const BriefBlock: React.FC<{ ); const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ rows, pf }) => { - const aboveTargetShare = pf.capitalDeployed > 0 ? pf.capitalAboveTarget / pf.capitalDeployed : 0; + const overhangShare = pf.capitalDeployed > 0 ? pf.overhangCapital / pf.capitalDeployed : 0; + const bleedOffCapital = Math.max(0, pf.capitalAboveTarget - pf.overhangCapital); const evRow = rows.find((r) => r.category === 'EV'); const leanRow = rows.find((r) => r.status === 'Stockout Risk'); @@ -244,8 +279,11 @@ const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ row

Recommendation

- Clear {money(pf.capitalAboveTarget)} of above-target - inventory ({pct(aboveTargetShare)} of deployed capital). This halts{' '} + Of {money(pf.capitalAboveTarget)} sitting above segment + target, only {money(pf.overhangCapital)} is a structural + overhang ({pct(overhangShare)} of deployed capital) — the rest{' '} + ({money(bleedOffCapital)}) bleeds off on its own once + replenishment pauses. Clearing the overhang halts{' '} {money(pf.carryingBleed)}/yr of carrying cost, recaptures{' '} {money(pf.recapturable)}, and fully funds the{' '} {money(pf.stockoutGapCapital)} shortfall in @@ -254,10 +292,10 @@ const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ row

{[ - { k: 'First-year net benefit', v: money(pf.netFirstYearTotal) }, + { k: 'Net benefit', v: money(pf.netBenefitTotal) }, { k: 'Recurring savings', v: `${money(pf.carryingBleed)}/yr` }, { k: 'Capital recaptured', v: money(pf.recapturable) }, - { k: 'Pools to act on', v: `${pf.atRiskCount}` }, + { k: 'Overhang pools', v: `${pf.overhangCount}` }, ].map((m) => (

{m.k}

@@ -289,7 +327,9 @@ const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ row {evRow && ( <> {evRow.model} is running at {evRow.dsi} days against a{' '} - {evRow.targetDSI}-day target — softening BEV demand showing up as on-lot capital.{' '} + {evRow.targetDSI}-day target — softening BEV demand that, at its current run-rate, would take roughly{' '} + {evRow.monthsToClear.toFixed(0)} months to sell through, + so it genuinely sits rather than bleeding off.{' '} )} {leanRow && ( @@ -298,8 +338,10 @@ const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ row — too lean — so the network is turning away volume demand it could profitably serve.{' '} )} - The net effect is {money(pf.carryingBleed)}/yr of avoidable - carrying cost while a {money(pf.stockoutGapCapital)} demand + The discipline is separating the two kinds of above-target stock: only the{' '} + {money(pf.overhangCapital)} structural overhang bleeds{' '} + {money(pf.carryingBleed)}/yr in carrying cost; the rest clears + itself. Meanwhile a {money(pf.stockoutGapCapital)} demand gap goes unfunded. @@ -307,9 +349,11 @@ const StrategyBrief: React.FC<{ rows: ProcessedItem[]; pf: Portfolio }> = ({ row icon={} tone="green" label="Resolution" - lead="A net-benefit decision rule that acts only when carrying cost avoided exceeds the clearance cost." + lead="First strip out what self-corrects; then clear only the overhang whose carrying cost exceeds the clearance loss." > - For every above-target pool, act only when{' '} + A two-step rule. First, pause replenishment and let any pool that clears within the patience window{' '} + bleed off — no haircut needed. + Then, for the structural overhang that remains, act only when{' '} carrying cost avoided > one-time clearance loss @@ -386,8 +430,8 @@ const Dashboard: React.FC<{ onReset: () => void; }> = ({ rows, pf, params, setParams, recaptured, onExecute, onExecuteAll, onReset }) => { const recommendations = rows - .filter((r) => r.status === 'At Risk') - .sort((a, b) => b.excessCapital - a.excessCapital); + .filter((r) => r.status === 'Overhang') + .sort((a, b) => b.overhangCapital - a.overhangCapital); return (
@@ -418,17 +462,17 @@ const Dashboard: React.FC<{ tip="Sum of (units in stock x OEM wholesale cost) across all models." /> Scenario Assumptions — drag to test sensitivity; every figure recomputes live
-
+
setParams((p) => ({ ...p, liquidationDiscount: v }))} /> + setParams((p) => ({ ...p, clearWindowMonths: v }))} + /> Segment Velocity Map
-
+
Healthy + Bleeding Off Stockout Risk - At Risk + Overhang
@@ -498,7 +552,7 @@ const Dashboard: React.FC<{ Model DSI / Target Status - Capital Above Target + Overhang Capital Action @@ -510,10 +564,15 @@ const Dashboard: React.FC<{

{r.segment} · {num(r.stock)} units

- + {r.dsi} / {r.targetDSI}d + {r.excessUnits > 0 && ( +

+ clears in ~{Math.round(r.monthsToClear * 30)}d +

+ )} @@ -521,23 +580,30 @@ const Dashboard: React.FC<{ - {r.excessCapital > 0 ? money(r.excessCapital) : } + {r.overhangCapital > 0 ? money(r.overhangCapital) : } - {r.status === 'At Risk' && r.netFirstYear > 0 ? ( + {r.status === 'Overhang' && r.netBenefit > 0 ? ( - ) : r.status === 'At Risk' ? ( + ) : r.status === 'Overhang' ? ( Hold · net-negative + ) : r.status === 'Bleeding Off' ? ( + + Let bleed off + ) : ( No action )} @@ -556,7 +622,7 @@ const Dashboard: React.FC<{

Recommended Actions

- — ranked by capital above target, net-benefit tested + — structural overhang only, ranked by size, net-benefit tested
-

First-year net benefit

-

= 0 ? 'text-green-600' : 'text-red-600'}`}> - {money(r.netFirstYear)} +

Net benefit

+

= 0 ? 'text-green-600' : 'text-red-600'}`}> + {money(r.netBenefit)}

@@ -1005,21 +1072,22 @@ const App: React.FC = () => { const executeOne = (id: number) => { const row = rows.find((r) => r.id === id); // Honor the net-benefit rule per row, exactly like "Execute all": only act - // when carrying cost avoided exceeds the one-time clearance loss. - if (!row || row.excessUnits <= 0 || row.netFirstYear <= 0) return; + // when carrying cost avoided exceeds the one-time clearance loss. Clear only + // the structural overhang — the bleed-off portion is left to sell organically. + if (!row || row.overhangUnits <= 0 || row.netBenefit <= 0) return; setRecaptured((c) => c + row.recaptured); - setInventory((prev) => prev.map((i) => (i.id === id ? { ...i, stock: row.targetStock } : i))); + setInventory((prev) => prev.map((i) => (i.id === id ? { ...i, stock: i.stock - row.overhangUnits } : i))); }; const executeAll = () => { - const positive = rows.filter((r) => r.status === 'At Risk' && r.netFirstYear > 0); + const positive = rows.filter((r) => r.status === 'Overhang' && r.netBenefit > 0); if (positive.length === 0) return; const freed = positive.reduce((s, r) => s + r.recaptured, 0); setRecaptured((c) => c + freed); setInventory((prev) => prev.map((i) => { const hit = positive.find((r) => r.id === i.id); - return hit ? { ...i, stock: hit.targetStock } : i; + return hit ? { ...i, stock: i.stock - hit.overhangUnits } : i; }) ); };