Price Floors and Guardrails in Repricing
A repricing rule engine proposes a price; a guardrail layer decides whether that price is allowed. The two must be separate. Strategy belongs in reviewable rules, but the limits that keep you above break-even, in line with supplier agreements, and out of a penny-by-penny price war are inviolable — they cannot be left to a rule author who might reorder or disable them. This recipe builds the clamp layer that sits after the engine: a single function that takes a candidate price and either passes it through, adjusts it to a hard boundary, or blocks the change entirely, logging exactly which guard acted. It sits under automated repricing rule engines and consumes the candidate produced by rule-based repricing logic in Python; its MAP handling is driven by the constraints surfaced in detecting MAP violations across marketplaces.
Prerequisites & Input Contract
Python 3.11+ and the standard library. The clamp is a pure function of the candidate price, the product’s cost and MAP inputs, its live price, and a per-category guardrail configuration. It runs after the engine and returns a final price plus an audit of what happened.
from dataclasses import dataclass
from typing import Optional
import time
@dataclass(frozen=True)
class GuardrailConfig:
min_margin: float = 0.15 # floor = landed_cost * (1 + min_margin)
max_markup: float = 3.00 # ceiling = landed_cost * (1 + max_markup)
max_step_pct: float = 0.10 # max move per cycle, fraction of current price
cooldown_s: int = 900 # min seconds between reprices for one SKU
sanity_floor: float = 0.01 # reject absurd sub-cent proposals
@dataclass(frozen=True)
class GuardInput:
sku: str
candidate: float # proposed price from the rule engine
current_price: float # live storefront price
landed_cost: float
map_floor: Optional[float] = None # minimum advertised price, if bound
last_repriced_at: float = 0.0 # epoch seconds of previous change
The candidate is the proposed_price returned by the engine; map_floor is None when no supplier MAP applies and a hard number when one does.
Step-by-Step Implementation
The clamp runs the guards in a fixed order. Two guards can veto the change outright (MAP breach, cool-down); the rest adjust the value in place. The order matters: MAP is checked before the floor clamp, because a candidate that a floor would happily accept can still breach a supplier’s advertised minimum.
Step 1 — Compute the hard boundaries
The cost-plus floor and the ceiling are derived from landed cost. When a MAP floor is present, the effective floor is the higher of the margin floor and MAP — you may never advertise below MAP even if your margin would allow it.
def bounds(gi: GuardInput, cfg: GuardrailConfig) -> tuple[float, float]:
margin_floor = gi.landed_cost * (1 + cfg.min_margin)
floor = max(margin_floor, gi.map_floor or 0.0) # MAP wins when higher
ceiling = gi.landed_cost * (1 + cfg.max_markup)
return round(floor, 2), round(ceiling, 2)
Step 2 — Block on a MAP breach before adjusting anything
A candidate below MAP is not clamped up to MAP — it is blocked. Silently raising to MAP would hide a strategy bug and could still violate the agreement during the window before the change propagates. Blocking and logging is the compliant behavior.
def check_map(gi: GuardInput) -> Optional[str]:
if gi.map_floor is not None and gi.candidate < gi.map_floor - 1e-9:
return "map_breach" # veto: do not dispatch
return None
Step 3 — Clamp to the floor/ceiling band, then limit the step
Clamping to the band is a simple bound. The max-step clamp then caps how far the price may move from its current value in a single cycle — the single most effective brake on oscillation and price wars, because it turns a would-be 30% plunge into a controlled series of capped moves.
def clamp_band(price: float, floor: float, ceiling: float) -> tuple[float, Optional[str]]:
if price < floor:
return floor, "floor"
if price > ceiling:
return ceiling, "ceiling"
return price, None
def clamp_step(price: float, current: float, max_step_pct: float) -> tuple[float, Optional[str]]:
max_delta = current * max_step_pct
if abs(price - current) <= max_delta:
return price, None
stepped = current + max_delta if price > current else current - max_delta
return round(stepped, 2), "max_step"
Step 4 — Enforce the cool-down and assemble the result
The cool-down vetoes any change to a SKU repriced too recently, defeating two bots trading cuts every cycle. The orchestrator threads the guards together and returns the final price with the ordered list of guards that acted — the audit trail.
def apply_guardrails(gi: GuardInput, cfg: GuardrailConfig, now: float = None) -> dict:
now = time.time() if now is None else now
applied: list[str] = []
if gi.candidate < cfg.sanity_floor:
return {"sku": gi.sku, "final_price": None, "status": "rejected",
"applied": ["sanity"], "reason": "sub-cent candidate"}
if check_map(gi):
return {"sku": gi.sku, "final_price": None, "status": "blocked",
"applied": ["map_breach"], "reason": "below MAP floor"}
if now - gi.last_repriced_at < cfg.cooldown_s:
return {"sku": gi.sku, "final_price": None, "status": "held",
"applied": ["cooldown"], "reason": "within cool-down window"}
floor, ceiling = bounds(gi, cfg)
price, tag = clamp_band(gi.candidate, floor, ceiling)
if tag:
applied.append(tag)
price, tag = clamp_step(price, gi.current_price, cfg.max_step_pct)
if tag:
applied.append(tag)
return {"sku": gi.sku, "final_price": round(price, 2),
"status": "approved", "applied": applied,
"reason": "clamped" if applied else "passed clean"}
Running the clamp on a candidate that both breaks the floor and overshoots the step limit shows both guards firing in order:
cfg = GuardrailConfig()
gi = GuardInput("44182", candidate=9.90, current_price=21.99,
landed_cost=14.20, map_floor=17.99)
print(apply_guardrails(gi, cfg))
# {'sku': '44182', 'final_price': None, 'status': 'blocked',
# 'applied': ['map_breach'], 'reason': 'below MAP floor'}
With no MAP bound, the same candidate is clamped rather than blocked: the floor pulls 9.90 up to 16.33, then the max-step cap holds the move to 10% of 21.99, yielding final_price: 19.79.
Verification & Testing
Test each guard in isolation and the precedence between them, using an injected now so the cool-down is deterministic.
def test_guardrails():
cfg = GuardrailConfig()
base = dict(sku="x", current_price=20.00, landed_cost=12.00)
# MAP breach is blocked, never clamped up.
r = apply_guardrails(GuardInput(candidate=15.00, map_floor=17.99, **base), cfg, now=10_000)
assert r["status"] == "blocked" and r["final_price"] is None
# Floor clamp: candidate below margin floor is raised to it.
r = apply_guardrails(GuardInput(candidate=10.00, **base), cfg, now=10_000)
assert "floor" in r["applied"] and r["final_price"] == 13.80
# Max-step: a big legal move is capped to 10% of current.
r = apply_guardrails(GuardInput(candidate=30.00, **base), cfg, now=10_000)
assert "max_step" in r["applied"] and r["final_price"] == 22.00
# Cool-down: a recent reprice defers the change.
r = apply_guardrails(GuardInput(candidate=19.00, last_repriced_at=9_950, **base), cfg, now=10_000)
assert r["status"] == "held"
print("all guards covered")
test_guardrails() # -> all guards covered
Edge Cases & Gotchas
- Floor above the competitor’s price. When your cost-plus floor sits above what a competitor is charging, chasing them means selling below break-even. The clamp holds you at the floor rather than following the market down — the correct behavior is to lose that sale, not the margin. Surface these as a report of SKUs where floor exceeds
competitor_min, because a persistent gap is a sourcing problem, not a pricing one. - MAP breach must block, never clamp. It is tempting to raise a sub-MAP candidate to MAP and move on. Do not: a strategy that keeps proposing sub-MAP prices is broken and must be visible. Block the change, log the breach, and let the alerting in detecting MAP violations across marketplaces catch the pattern.
- Cool-down starves legitimate moves. Too long a window and a genuine market shift goes unanswered for hours. Tie the cool-down to the reprice cycle length and category volatility, and exempt manual overrides so an operator can force a change past it.
- Max-step and floors interacting. Clamp the band before the step, or a large legal correction up to the floor can itself be throttled and leave you below break-even for a cycle. The order in
apply_guardrails— floor/ceiling then step — is deliberate; the floor is a hard safety bound and takes precedence, and the step only ever narrows a move that is already inside the band.
Performance Notes
Every guard is arithmetic and comparison, so the clamp is effectively free — the same 100k-plus decisions per second the engine sustains, since it adds no allocation and no I/O. The one exception is the cool-down, which needs the SKU’s last_repriced_at; keep that in a Redis hash or an in-memory map keyed by SKU rather than a per-call database read, and refresh it in the same write that records the dispatched change. Keep the clamp synchronous next to the engine; the boundary that touches I/O is dispatching the approved price, not deciding it.
Frequently Asked Questions
Why block a sub-MAP price instead of raising it to MAP? Because a strategy that keeps proposing prices below MAP is broken, and silently correcting it hides the bug while risking a real advertised-price violation during propagation. Blocking the change and logging the breach keeps the behavior compliant and the fault visible.
What is the single most important guardrail for stopping price wars? The maximum step per cycle. Capping how far a price can move in one cycle turns a runaway plunge into a slow, bounded series of moves, and combined with a cool-down window it removes the penny-by-penny undercutting loop two competing bots fall into.
Should guardrails live inside the rule engine or outside it? Outside. Rules express strategy and change often; guardrails express inviolable limits and must not be reorderable or disableable by a rule author. Keeping the clamp as a separate layer after the engine is what makes the limits trustworthy.
Related
- Automated Repricing Rule Engines — the parent guide; this clamp is the guardrail layer it describes.
- Rule-Based Repricing Logic in Python — produces the candidate price that enters this clamp.
- Detecting MAP Violations Across Marketplaces — supplies the MAP floors and catches breach patterns the clamp blocks.