AI-Pricing-Agent/config/pricing_rules.yaml

91 lines
4.9 KiB
YAML

# Pricing rules — the encoded policy of the Pricing & Profitability Analyst playbook.
# All money math in tools/margin_engine.py reads these values. Change policy here,
# not in code.
# Contribution-margin floor. A SKU must clear this at the WORST-CASE referral fee
# or it does not get Pricing Approved. Playbook standard: 25% floor, 30%+ preferred.
margin_floor: 0.25
margin_target: 0.30
# Returns reserve, taken as a % of selling price (playbook: ~2%).
returns_reserve_pct: 0.02
# Allocated monthly storage per unit, flat $ (playbook: ~$0.25 for the sheet set).
storage_alloc: 0.25
# Worst-case referral fee % used for the margin GATE. Amazon Home & Kitchen = 15%.
# We always test the floor against this, never the optimistic modeled referral.
worst_case_referral_pct: 0.15
# Rounding for customer-facing prices (charm pricing to .99). Set to null to disable.
charm_ending: 0.99
# Price-competitiveness tolerance vs the competitive median (playbook KPI: within 5%).
price_competitiveness_tolerance: 0.05
# ── inventory cover: where a price move is triggered ────────────────────────────
# These are PRICING triggers: at or below `low` the engine raises 5% to slow the burn, at or
# above `high` it cuts 5% to clear stock. They were module constants in dashboard/live_data.py;
# they live here because they are policy, and because they need to be reconcilable with the
# COSMOS Inventory Planning bands that colour the same numbers on screen.
#
# THEY DELIBERATELY DO NOT EQUAL THE COSMOS BANDS. COSMOS's Alpha scheme calls 40-70 days
# "healthy" (green) and 70-100 "high" (pink), but pink there is a REPLENISHMENT warning — it
# tells a buyer not to reorder yet. It is not an instruction to discount, and the engine here
# spends real margin when it fires.
#
# Measured on 47 SKUs of the covered product line, on live COSMOS data:
#
# thresholds raise +5% cut -5% no action
# 35 / 90 (these) 6 20 21
# 40 / 70 (band edges) 7 26 14
# 20 / 100 (red bands) 2 16 29
#
# Adopting the band edges would put six more SKUs on a 5% discount — every one of them in the
# 70-90 day window, none of them actually at risk of ageing out. So the bands stay COSMOS's
# for display parity, the triggers stay where they are, and the UI states which is which.
# To converge them, set these to 40 / 70; nothing else needs to change.
low_cover_days: 35
high_cover_days: 90
# ── competitor-driven pricing rules ─────────────────────────────────────────────
# Master switch for the two competitor rules ONLY: BUYBOX_SUPPRESSED and
# COMPETITOR_UNDERCUT. Set false and the cascade behaves exactly as it did before competitor
# data existed — it routes through the SAME fail-safe path as missing/stale data, so there is
# only ever one "pretend competitor data isn't there" code path to reason about.
#
# This is NOT the global kill switch. That one lives in the sidebar (app.py) and pauses
# APPROVALS across the board without touching the cascade. The two are independent in both
# directions: you can disable competitor rules while approvals continue normally, and you can
# pause approvals while competitor rules keep informing the verdicts a human is reading.
#
# Every other cascade rule — cost data, break-even, ad losses, inventory, profit-optimal — is
# untouched by this flag.
competitor_rules_enabled: true
# These gate the COMPETITOR branches of the live decision cascade. They live here, not in
# code, because every one of them is a policy judgement someone may want to change without
# a deploy.
# How far below us a rival has to sit before "they undercut us" is a fact rather than noise.
# 3% is above the ~2% band our own realized price already swings through as coupons toggle
# (the mattress protector moves $11.71-$12.98 on its own), so anything under it cannot be
# distinguished from our own price wobble.
competitor_undercut_material_pct: 0.03
# How far ABOVE the rival field we have to sit before it is worth remarking on while we
# still hold the Buy Box. Only ever a narrative note -- it never moves a price on its own.
competitor_premium_material_pct: 0.10
# How old competitor state may be and still inform a verdict. Matched to the Playwright
# scraper's own 6h cache TTL so the two cannot disagree about what "stale" means. Past this
# the verdict is computed competitor-BLIND rather than on a price that has since moved.
competitor_state_max_age_hours: 6.0
# The comparison WORKBOOK gets its own, much longer limit. It is a 25-35 minute batch run over
# a whole product line, not a single-ASIN scrape, so nobody re-runs it hourly and the 6h rule
# above would make every sheet unusable by the next morning. Raise or lower this as the
# refresh cadence settles; set it to null to accept a sheet of any age.
competitor_sheet_max_age_hours: 168.0