"""Utopia Pricing Agent โ€” one-page dashboard (CRAI design system). Utopia Brands look (crai.utopiabrands.com): off-white cards on warm cream paper, teal primary, coral accent, navy chrome. Sidebar with filters, stat tiles, action pills, and a recommendation queue where clicking a SKU row drops down the advanced analysis (price & demand, inventory, scenarios, competitors, PPC, costs, AI reasoning). All numbers come from the live COSMOS pipeline โ€” fees, 6-month history, elasticity, actual profit. Run: streamlit run app.py """ from __future__ import annotations import hmac import os import sys from pathlib import Path # Make src/ + project root importable when launched via `streamlit run`. ROOT = Path(__file__).resolve().parent for p in (str(ROOT / "src"), str(ROOT)): if p not in sys.path: sys.path.insert(0, p) from datetime import date, datetime, timedelta import pandas as pd import streamlit as st from plotly.subplots import make_subplots import plotly.graph_objects as go from dashboard import theme # Ranking rules live in the data layer so they can be tested without booting # Streamlit โ€” importing app.py would execute the whole page. from dashboard.line import LINE_SORTS, line_sort_key as _line_sort_key from dashboard.live_data import ( FALLBACK_ELASTICITY, REASON_LABELS, REFERRAL_PCT, VARIABLE, modelled, reproject_inventory, scenarios_for_window, ) TODAY = date.today() st.set_page_config(page_title="Utopia Pricing Agent", page_icon="๐Ÿงญ", layout="wide", initial_sidebar_state="auto") theme.register_template() def _require_password() -> None: """Shared-password gate, active only when APP_PASSWORD is set. The app authenticates to COSMOS with the credentials in .env and shows live cost, margin and sales data, so on a shared network the URL alone is enough for anyone to read it. Set APP_PASSWORD before binding to 0.0.0.0. Left unset (the default, and the local-only case) nothing changes. This is a doorlock, not an identity system: one shared secret, no accounts, no audit of who looked. For anything beyond an internal LAN, put it behind a real reverse proxy with SSO. """ expected = os.environ.get("APP_PASSWORD") if not expected or st.session_state.get("_authed"): return st.markdown( '
๐Ÿ”’ Utopia Pricing Agent
' '
This dashboard shows live cost and margin data. ' 'Enter the shared password to continue.
', unsafe_allow_html=True) _, mid, _ = st.columns([1, 2, 1]) with mid: pw = st.text_input("Password", type="password", label_visibility="collapsed", placeholder="Shared password") if pw: # compare_digest so a wrong guess takes the same time as a right one if hmac.compare_digest(pw, expected): st.session_state["_authed"] = True st.rerun() else: st.error("Incorrect password.") st.stop() st.markdown(""" """, unsafe_allow_html=True) # Gate runs after the stylesheet so the lock screen is styled like the rest. _require_password() ACTION_GLYPH = {"Increase": "โ†‘", "Decrease": "โ†“", "Maintain": "โ†’", "Investigate": "๐Ÿ”"} # The per-SKU section switcher (replaces st.tabs so "View more" can jump to a section). # Ordered by the questions a reviewer actually asks, in order: what's the shape of # this product, can I trust the model, what are my options, why this, then reference. # "Track record" was previously buried at the bottom of the last tab โ€” it is the one # thing that says how wrong the engine has been, so it sits second. VIEW_KEYS = ["price", "track", "scenarios", "why", "inventory", "repricing", "competitors", "ppc", "costs"] VIEW_LABELS = { "price": "๐Ÿ“ˆ Price & demand", "track": "๐ŸŽฏ Track record", "scenarios": "๐Ÿงฎ Scenarios", "why": "๐Ÿ” Why this", "inventory": "๐Ÿ“ฆ Inventory outlook", "repricing": "๐Ÿ” Stock at the new price", "competitors": "๐ŸฅŠ Competitors", "ppc": "๐Ÿ“ฃ PPC", "costs": "๐Ÿ’ฐ Costs", } VIEW_SCENARIOS = "scenarios" # Internal objective codes โ†’ language a human uses. OBJECTIVE_LABELS = { "max_profit": "maximise profit", "margin_protection": "protect margin", "low_stock_protection": "protect low stock", "overstock_reduction": "clear overstock", "fix_data_first": "fix the data first", "observed_best": "match the best price we've run", "fix_ad_efficiency": "fix ad efficiency", } # Inventory band codes โ†’ what they mean. INV_CLASS_LABELS = {"alpha": "Alpha (fast-moving)", "beta": "Beta (slower-moving)"} MARKETS = { # marketplace code โ†’ (flag, short label) "AMAZON_USA": ("๐Ÿ‡บ๐Ÿ‡ธ", "US"), "AMAZON_CA": ("๐Ÿ‡จ๐Ÿ‡ฆ", "CA"), "AMAZON_UK": ("๐Ÿ‡ฌ๐Ÿ‡ง", "UK"), "AMAZON_DE": ("๐Ÿ‡ฉ๐Ÿ‡ช", "DE"), "AMAZON_FR": ("๐Ÿ‡ซ๐Ÿ‡ท", "FR"), "AMAZON_IT": ("๐Ÿ‡ฎ๐Ÿ‡น", "IT"), "AMAZON_ES": ("๐Ÿ‡ช๐Ÿ‡ธ", "ES"), "AMAZON_NL": ("๐Ÿ‡ณ๐Ÿ‡ฑ", "NL"), "AMAZON_SE": ("๐Ÿ‡ธ๐Ÿ‡ช", "SE"), "AMAZON_PL": ("๐Ÿ‡ต๐Ÿ‡ฑ", "PL"), "AMAZON_BE": ("๐Ÿ‡ง๐Ÿ‡ช", "BE"), "AMAZON_TR": ("๐Ÿ‡น๐Ÿ‡ท", "TR"), "AMAZON_IE": ("๐Ÿ‡ฎ๐Ÿ‡ช", "IE"), "AMAZON_AU": ("๐Ÿ‡ฆ๐Ÿ‡บ", "AU"), } def mkt_label(m: str) -> str: flag, short = MARKETS.get(m, ("๐ŸŒ", m)) return f"{flag} {short}" # ---------------------------------------------------------------- data loading def _load_live(skus: tuple, with_comp: bool, progress=None) -> dict: """Session-cached loader. Not @st.cache_data โ€” the progress callback writes to a live Streamlit element, which the cache's element-replay can't handle. We cache the pure result in session_state instead, so progress stays live on cold loads.""" cache = st.session_state.setdefault("_live_cache", {}) key = (skus, with_comp) if key in cache: return cache[key] from dashboard.live_data import get_live_data data = get_live_data(skus, with_competitive=with_comp, progress_cb=progress) cache[key] = data return data def _clear_live_cache(): st.session_state["_live_cache"] = {} def _amazon_url(asin: str | None, marketplace: str = "AMAZON_USA") -> str | None: """Amazon product-detail URL for an ASIN, per marketplace domain.""" if not asin or asin == "โ€”": return None domains = { "AMAZON_USA": "com", "AMAZON_CA": "ca", "AMAZON_UK": "co.uk", "AMAZON_DE": "de", "AMAZON_FR": "fr", "AMAZON_IT": "it", "AMAZON_ES": "es", "AMAZON_NL": "nl", "AMAZON_SE": "se", "AMAZON_PL": "pl", "AMAZON_BE": "com.be", "AMAZON_TR": "com.tr", "AMAZON_IE": "ie", "AMAZON_AU": "com.au", } return f"https://www.amazon.{domains.get(marketplace, 'com')}/dp/{asin}" @st.cache_data(ttl=3600, show_spinner=False) def _find_skus(prefix: str, limit: int) -> tuple: from config.settings import get_settings from pricing_agent.analyze import _build_service svc = _build_service(get_settings()) return tuple(svc.list_skus(limit=limit, sku_prefix=prefix or None)) @st.cache_data(ttl=3600, show_spinner=False) def _find_line(prefix: str) -> list[dict]: """Every product in a line, with stock and velocity โ€” one COSMOS call.""" from config.settings import get_settings from pricing_agent.analyze import _build_service svc = _build_service(get_settings()) return [{"sku": p.sku, "asin": p.asin, "size": p.size, "inventory": p.inventory, "avg_7d": p.avg_7d, "avg_30d": p.avg_30d, "avg_6m": p.avg_6m, "cover_days": p.cover_days, "min_po_quantity": p.min_po_quantity, "potential_daily_sale": p.potential_daily_sale} for p in svc.find_line(prefix)] def _parse_skus(raw: str) -> tuple: parts = [p.strip() for chunk in raw.replace(",", "\n").splitlines() for p in [chunk] if p.strip()] seen, out = set(), [] for p in parts: if p.upper() not in seen: seen.add(p.upper()) out.append(p) return tuple(out) # ---------------------------------------------------------------- helpers def esc(s: str) -> str: """Escape $ so Streamlit markdown never enters LaTeX math mode.""" return s.replace("$", "\\$") def dash(v, fmt: str = "{}") -> str: """Format a value, or an em-dash when it is missing.""" if v is None or (isinstance(v, float) and pd.isna(v)): return "โ€”" return fmt.format(v) def compact_money(v: float) -> str: """$33k rather than $32,974 โ€” false precision from a model with a known error band reads as certainty the number does not have.""" a = abs(v) sign = "-" if v < 0 else "" if a >= 1_000_000: return f"{sign}${a / 1_000_000:.1f}M" if a >= 1_000: return f"{sign}${a / 1_000:,.0f}k" return f"{sign}${a:,.0f}" def model_error_pct(d: dict) -> float | None: """This SKU's backtested error, as a fraction โ€” the width of any honest range.""" bt = d.get("backtest") or {} sel = bt.get("selected") mape = (bt.get("scores", {}).get(sel, {}) or {}).get("mape") if sel else None return (mape / 100.0) if mape else None def money_range(v: float, err: float | None) -> str: """A point estimate widened by the model's own measured error.""" if not err: return compact_money(v) lo, hi = v * (1 - err), v * (1 + err) return f"{compact_money(min(lo, hi))}โ€“{compact_money(max(lo, hi))}" def unit_take_home(price: float, d: dict) -> float: """Per-unit take-home under this SKU's COSMOS fee model.""" rp = d.get("referral_pct", REFERRAL_PCT) ret = d.get("returns_pct", 0.0) var = d.get("variable", VARIABLE) return price * (1 - rp - ret) - d["cfg"]["fba"] - d["cfg"]["cost"] - var def _price_move_html(delta_pct: float) -> str: """Colored arrow + percentage chip for a price move vs current.""" if abs(delta_pct) < 0.05: return 'โ†’ 0.0%' if delta_pct > 0: return (f'โ–ฒ +{delta_pct:.1f}%') return (f'โ–ผ {delta_pct:.1f}%') WINDOW_OPTS = {"7 days": 7, "14 days": 14, "30 days": 30, "90 days": 90, "6 months": 180} DEFAULT_WINDOW = "90 days" # one month is mostly noise; 90d spans real price variation def render_scenarios(d: dict, key_prefix: str, compact: bool = False): """Scenario view on ONE price basis: revenue + ad-inclusive net profit, coloured price moves, and per-row marketing advice. A window filter drives the averaging: units/day, revenue, ad spend and profit all come from the SAME window, so revenue รท units/day == the actual average selling price.""" base_econ = d.get("scen_econ") or [] if not base_econ: st.info("No scenario economics available for this product.") return # โ”€โ”€ The recommendation itself. This is the engine's decision, not a separate # re-derivation from a different window โ€” showing one number here and another # in the table below is how the two used to disagree by 3.5x. โ”€โ”€ tgt = d.get("target_price", d["rec_price"]) if d["action"] == "Maintain": body = (f"Hold at ${d['current_price']:.2f} โ€” nothing in the evidence " f"beats the current price.") else: arrow = "Raise" if d["rec_price"] > d["current_price"] else "Lower" body = (f"{arrow} from ${d['current_price']:.2f} to " f"${d['rec_price']:.2f} ({d['delta_pct']:+.1f}%)") if d.get("stepped") and abs(tgt - d["rec_price"]) > 0.01: body += (f", stepping toward ${tgt:.2f} โ€” one move at a time so the " f"demand response can be read before the next one") body += "." if d.get("impact_30d"): body += f" Projected {d['impact_30d']:+,.0f}/30d net profit." ob = d.get("observed_price_max") if ob: body += (f" Highest price with a real sample behind it: ${ob:.2f}.") st.markdown( f'
' f'๐Ÿ’ก Recommendation: ' f'{body}
', unsafe_allow_html=True, ) # Averaging-window filter โ€” everything below derives from this one window. wl = st.segmented_control( "Averaging window", list(WINDOW_OPTS), key=f"win_{key_prefix}", default=DEFAULT_WINDOW, help="Units/day, revenue, ad spend and profit are all " "averaged over this window, so they always reconcile.") days = WINDOW_OPTS.get(wl or DEFAULT_WINDOW, 90) if days == d.get("scen_window", 90): econ = base_econ summary = d.get("scen_summary") best_key = d.get("scen_best_key") facts = None else: econ, meta, facts = scenarios_for_window(d, days) summary = meta.get("summary") best_key = meta.get("best_key") cur = next((x for x in econ if x["key"] == "current"), econ[0]) price_check = (cur["revenue_30d"] / cur["units_30d"]) if cur["units_30d"] else 0.0 best = next((x for x in econ if x["key"] == best_key), None) # ONE caveat block, not four scattered ones. Every projection on this tab # carries the same two qualifications, so they are stated once, together. el = d.get("elasticity_detail") or {} caveats = [] if not d.get("elasticity_actionable"): caveats.append("**Elasticity is not usable for pricing on this SKU** โ€” " + (el.get("why") or "the fit does not establish a price/volume link") + ". The recommendation comes from prices this product has " "actually run, not from the curve below.") if d.get("profit_optimal_blocked_reason"): line = f"**No profit-optimal price published** โ€” {d['profit_optimal_blocked_reason']}." if d.get("profit_optimal_unconstrained"): line += (f" Taken literally the fit implies pricing at " f"${d['profit_optimal_unconstrained']:.2f}, which is itself evidence " f"the fit is unusable.") caveats.append(line) if caveats: st.warning(esc("โš ๏ธ " + "\n\n".join(caveats))) a1, a2, a3 = st.columns(3) a1.metric(f"Units/day (last {days}d)", f"{cur['units_day']:,}", f"avg sold ${price_check:,.2f}/unit", delta_color="off") a2.metric("Net profit /30d", f"${cur['net_30d']:,.0f}", f"on ${cur['revenue_30d']:,.0f} revenue", delta_color="off") if best: lbl = "Best price = hold" if best["key"] == "current" else "Best price (projected)" a3.metric(lbl, f"${best['price']:.2f}", f"{best['net_vs_current']:+,.0f}/30d vs now", delta_color="off") rows_html = [] for x in econ: is_best = x["key"] == best_key is_cur = x["key"] == "current" emoji = "๐ŸŸฐ" if is_cur else ("๐Ÿ“ˆ" if x["price"] > cur["price"] else "๐Ÿ“‰") row_bg = "#eef6f3" if is_best else ("#fbf6ee" if is_cur else "transparent") star = " โญ" if is_best else "" net_color = "#0a6f65" if x["net_30d"] >= 0 else "#c9442b" # Current row: show the AVG PRICE SOLD (revenue รท units) so price ร— units ร— 30 = # revenue reconciles. It sits below list when there are promos, and varies by # window because the avg selling price differed period to period. if is_cur and x["units_30d"]: eff = x["revenue_30d"] / x["units_30d"] price_cell = (f'${eff:,.2f}' f'
avg sold ยท ' f'{days}d
' f'
list ' f'${x.get("list_price", x["price"]):.2f}
') move_cell = 'actual' else: # Provenance: a price this SKU has really run is evidence; one it has # never run is extrapolation, and the table must not look the same. obs = x.get("observed_days") or 0 tag = (f'
' f'โœ“ ran {obs}d
' if obs else '
never tested
') price_cell = f'${x["price"]:.2f}{tag}' move_cell = _price_move_html(x["delta_pct"]) rows_html.append( f'' f'{emoji} {esc(x["label"])}{star}' f'{price_cell}' f'{move_cell}' f'{x["units_day"]:,}' f'${x["revenue_30d"]:,.0f}' f'${x["ad_30d"]:,.0f}' f'${x["net_30d"]:,.0f}' f'{x["net_margin_pct"]:.1f}%' # No esc() inside the raw HTML table โ€” Streamlit does not run LaTeX on # these cells, so escaping would render a literal backslash. + ("" if compact else f'{x["advice"]}') + '' ) # Column headers with hover tooltips explaining how each number is computed. elas = d["cfg"].get("elasticity", -1.3) eld = d.get("elasticity_detail") or {} el_note = (f"elasticity {elas:.2f}" + (f" (95% CI {eld['ci_low']} to {eld['ci_high']}, " f"{'usable' if d.get('elasticity_actionable') else 'NOT statistically usable'})" if eld.get("ci_low") is not None else "")) anchor_p = cur.get("price", d["current_price"]) tacos_now = cur["ad_30d"] / cur["revenue_30d"] * 100 if cur["revenue_30d"] else 0.0 def th(label, tip, left=False): align = "left" if left else "center" return (f'' f'{label}' f' โ“˜') headers = ( th("Scenario", f"Current = your actual last-{days}-day facts. Every other row is " f"a what-if. 'โœ“ ran Nd' means this price really has been live for " f"N days โ€” that row is evidence, not a projection.", left=True) + th("Price", "Current = your AVERAGE price sold over the window (revenue รท " "units); your list price is shown beneath it. Other rows = the " "price you would set.") + th("Move", "Percentage change vs your current list price.") + th("Units/day", f"Projected demand = current units ร— (new price รท the price " f"customers actually paid, ${anchor_p:,.2f}) ^ elasticity. " f"Anchoring on the paid price โ€” not list โ€” is what keeps every " f"row on one basis. {el_note}.") + th("Revenue/30d", "Units/day ร— 30 ร— price.") + th("Ad spend/30d", f"Ad cost PER UNIT ร— units, with per-unit cost fitted " f"against price from your own history (a higher price " f"converts worse, so each sale costs more). Current TACoS " f"{tacos_now:.1f}%.") + th("Net profit/30d", "Take-home โˆ’ storage โˆ’ ad spend. No realization factor: " "a single multiplier fitted at one price also distorts the " "difference between prices, which is the whole question.") + th("Net margin", "Net profit รท revenue.") + ("" if compact else th("Marketing advice", "Plain-language read of the price / volume / ad-spend " "trade-off for this row.", left=True)) ) st.markdown( '
' '' + headers + '' + "".join(rows_html) + '
', unsafe_allow_html=True, ) st.caption("โœ“ = this price has really been run ยท โญ = highest projected net profit") # Methodology lives behind a disclosure. It answers "how is this computed?", # which is a question asked once โ€” not on every read of the table. The same # detail is on each column header, but hover is dead on touch and keyboard, # so this is the accessible copy of it. with st.expander("How these numbers are calculated"): anchor = cur.get("price", d["current_price"]) adm = d.get("ad_cost_model") or {} ad_note = "held flat per unit" if adm and adm.get("r2", 0) >= 0.25: ad_note = (f"rising ${adm['slope']:+.2f}/unit per $1 of price โ€” fitted on " f"{adm['days']} days of your own data (rยฒ={adm['r2']:.2f}), " f"because a higher price converts worse and costs more per sale") st.markdown(esc( f"- **Window** โ€” every figure is averaged over the last **{days} days**, so " f"units, revenue, ad spend and profit always reconcile.\n" f"- **Current row** โ€” COSMOS fact: actual booked revenue, ad spend and " f"profit. Its price is your **average sold** (${price_check:,.2f}), not your " f"list price (${d['current_price']:.2f}); the two differ whenever there are " f"promotions or coupons.\n" f"- **Projected rows** โ€” anchored at the price customers actually paid " f"(${anchor:,.2f}), not list, so every row sits on one basis.\n" f"- **Units/day** โ€” current units ร— (new price รท ${anchor:,.2f}) ^ " f"elasticity, where {el_note}.\n" f"- **Ad spend** โ€” ad cost per unit ร— units, with per-unit cost {ad_note}.\n" f"- **Net profit** โ€” take-home โˆ’ storage (${d.get('storage_30d', 0):,.0f}/30d) " f"โˆ’ ad spend" + (f", less a ${d['gap_per_unit']:.2f}/unit unmodeled cost measured against " f"actual booked profit" if d.get("gap_per_unit") else "") + ".\n" f"- Reconciliation check: ${price_check:,.2f} ร— {cur['units_day']:,} units " f"ร— 30 โ‰ˆ ${cur['revenue_30d']:,.0f} revenue.")) def set_status(sku: str, status: str): st.session_state.status[sku] = status if status == "Pending": st.session_state.modified.pop(sku, None) icon = {"Approved": "โœ…", "Rejected": "๐Ÿšซ", "Pending": "๐Ÿ”„"}[status] st.toast(f"{sku} โ†’ {status}", icon=icon) def apply_modify(sku: str, details: dict): new_price = float(st.session_state[f"mod_{sku}"]) d = details[sku] st.session_state.modified[sku] = new_price st.session_state.status[sku] = "Approved" over_cap = abs(new_price - d["current_price"]) / d["current_price"] > 0.05 note = " ยท exceeds 5% step cap โ†’ elevated approval tier" if over_cap else "" st.toast(f"{sku} approved at modified ${new_price:.2f}{note}", icon="โœ๏ธ") def bulk_approve(skus: list): for s in skus: st.session_state.status[s] = "Approved" st.toast(f"Bulk-approved {len(skus)} high-confidence recommendations", icon="โœ…") def reset_workspace(): st.session_state.status = {} st.session_state.modified = {} st.session_state.action_sel = "All" # Also drop the analysis cache. Without this, 'Reset workspace' cleared the # approve/reject marks but kept every previously computed SKU in # st.session_state['_live_cache'], so re-analysing returned the SAME numbers # from before โ€” including after a code change โ€” and the button looked broken. st.session_state.pop("_live_cache", None) st.toast("Workspace reset", icon="๐Ÿ”„") def set_action(a: str): st.session_state.action_sel = a def goto_view(sku: str, view: str): """Jump a SKU's section switcher to `view` (used by the View more button).""" st.session_state[f"view_{sku}"] = view # ---------------------------------------------------------------- charts def price_demand_chart(d) -> go.Figure: """Two stacked panels, one shared x-axis โ€” never a dual-axis chart.""" hist, fc = d["hist"], d["forecast"] fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.5, 0.5], vertical_spacing=0.12, subplot_titles=("Price ($)", "Units / day")) fig.add_trace(go.Scatter(x=hist["date"], y=hist["price"], name="Our price", line=dict(color=theme.SERIES["us"], width=2, shape="hv")), row=1, col=1) has_comp_hist = hist["comp_median"].notna().any() if has_comp_hist: fig.add_trace(go.Scatter(x=hist["date"], y=hist["comp_median"], name="Competitor median", line=dict(color=theme.SERIES["competitor"], width=2)), row=1, col=1) if d["change_dates"]: ys = [float(hist.loc[hist["date"] == dt, "price"].iloc[0]) for dt in d["change_dates"]] fig.add_trace(go.Scatter(x=d["change_dates"], y=ys, mode="markers", name="Price change", marker=dict(symbol="triangle-down", size=10, color=theme.SERIES["us"])), row=1, col=1) lo = float(min(hist["price"].min(), hist["comp_median"].min()) if has_comp_hist else hist["price"].min()) hi = float(max(hist["price"].max(), hist["comp_median"].max()) if has_comp_hist else hist["price"].max()) span = hi - lo if hi > lo else max(hi * 0.1, 0.1) guides = [(d["min_price"], f"min ${d['min_price']:.2f}", theme.STATUS["critical"]), (d["max_price"], f"max ${d['max_price']:.2f}", theme.STATUS["serious"]), (d["break_even"], f"break-even ${d['break_even']:.2f}", theme.MUTED)] if d.get("comp_median_now"): guides.append((d["comp_median_now"], f"competitor median ${d['comp_median_now']:.2f}", theme.SERIES["competitor"])) for y, txt, color in guides: if y and lo - span * 0.6 <= y <= hi + span * 0.6: fig.add_hline(y=y, line_dash="dot", line_color=color, line_width=1, annotation_text=txt, annotation_font_size=10, annotation_font_color=theme.MUTED, row=1, col=1) fig.add_trace(go.Scatter(x=hist["date"], y=hist["units"], name="Units", line=dict(color=theme.SERIES["us"], width=1.5)), row=2, col=1) fig.add_trace(go.Scatter(x=fc["date"], y=fc["p90"], line=dict(width=0), showlegend=False, hoverinfo="skip"), row=2, col=1) fig.add_trace(go.Scatter(x=fc["date"], y=fc["p10"], line=dict(width=0), fill="tonexty", fillcolor=theme.US_BAND, name="Forecast p10โ€“p90", hoverinfo="skip"), row=2, col=1) fig.add_trace(go.Scatter(x=fc["date"], y=fc["p50"], name="Forecast p50", line=dict(color=theme.SERIES["us"], width=2, dash="dash")), row=2, col=1) today = pd.Timestamp(TODAY) fig.add_shape(type="line", x0=today, x1=today, y0=0, y1=1, xref="x", yref="paper", line=dict(color=theme.AXIS, width=1, dash="dot")) fig.add_annotation(x=today, y=1.02, xref="x", yref="paper", text="today", showarrow=False, font=dict(size=10, color=theme.MUTED), xanchor="left") fig.update_yaxes(rangemode="tozero", row=2, col=1) fig.update_layout(height=440) return fig def inventory_chart(d) -> go.Figure: """Projected inventory over COSMOS's own INVP weekly snapshots, with arrivals.""" proj = d.get("inv_projection") or [] fig = go.Figure() if not proj: return fig dates = [pd.Timestamp(p["date"]) for p in proj] units = [float(p["inventory"] or 0) for p in proj] fig.add_trace(go.Scatter(x=dates, y=units, name="Projected inventory", mode="lines+markers", line=dict(color=theme.SERIES["us"], width=2))) fig.add_hline(y=0, line_color=theme.STATUS["critical"], line_dash="dot", line_width=1) for p, dt, u in zip(proj, dates, units): arr = float(p["warehouse_arrival"] or 0) if arr > 0: fig.add_annotation(x=dt, y=u, text=f"๐Ÿšš +{arr:,.0f}", showarrow=True, arrowhead=2, font=dict(size=10, color=theme.INK2)) fig.update_yaxes(rangemode="tozero", title_text="units") fig.update_layout(height=280, showlegend=False) return fig # Cover-day color bands (Alpha/Beta scheme, matched to the planning sheet) def _bucket_bg(cover: float, inv_class: str) -> str: """Cell shading by projected cover days, per the SKU's Alpha/Beta band scheme. Thresholds and colours both live in `theme.COVER_BANDS` โ€” one definition, so the inventory grid, the cover tile and anything added later cannot drift into shading the same number three different ways. """ return theme.cover_band(cover, inv_class)[0] def ppc_chart(d) -> go.Figure: """Daily PPC spend (thin) with 7-day moving average (bold), last 90 days.""" h = d["hist"].tail(90) ma = h["ad_spend"].rolling(7, min_periods=1).mean() fig = go.Figure() fig.add_trace(go.Scatter(x=h["date"], y=h["ad_spend"], name="Daily spend", line=dict(color=theme.MUTED, width=1))) fig.add_trace(go.Scatter(x=h["date"], y=ma, name="7-day average", line=dict(color=theme.SERIES["us"], width=2))) fig.update_yaxes(rangemode="tozero", title_text="spend ($/day)") fig.update_layout(height=280) return fig def inventory_table_html(d) -> str: """COSMOS INVP weekly projection table โ€” REAL data from invp-insight dateMap: projected units, inventory value, cover days (blue), and incoming arrivals.""" proj = d.get("inv_projection") or [] cfg, hist = d["cfg"], d["hist"] asin = d.get("asin") or f"B0{abs(hash(cfg['sku'])) % 10**8:08d}" asin_url = _amazon_url(asin, cfg.get("marketplace", "AMAZON_USA")) asin_html = (f'{asin}' if asin_url else asin) # COSMOS's OWN daily-sale figures, not ours re-derived from sales-insight history. The # weekly cells below come straight from the INVP dateMap and always matched; the header did # not, so one SKU showed 56 / 62 / 65 here against 64 / 130 / 67 in Inventory Planning. # # Falls back to the history means only where COSMOS returns nothing, so a gap still shows a # number rather than a dash โ€” but the two tools agree wherever COSMOS has an answer. _st = d.get("invp_stats") or {} d7 = _st.get("avg_sale") or _st.get("avg_7d") or hist["units"].tail(7).mean() # The middle figure in COSMOS's header is POTENTIAL daily sale โ€” its own demand estimate, # not a trailing average. It is a different quantity from the 30-day mean that used to sit # here, so it is labelled for what it is. d_pot = _st.get("potential_daily_sale") d6m = _st.get("avg_6m") or hist["units"].tail(90).mean() reviews = f" ({cfg['reviews']:,})" if cfg.get("reviews") else "" if not proj: return ('
' 'No INVP projection available for this product from COSMOS.
') # Match the COSMOS Inventory Planning header exactly. # # `proj[0].inventory` is the FIRST PROJECTED WEEK, which already has that # week's arrival folded in (5,029 = 3,999 on hand + 1,030 landing), so showing # it as stock overstated the shelf by the inbound load. COSMOS shows them apart. # # Inbound is likewise the IMMEDIATE arrival, not every arrival on the horizon: # summing the lot gave 1,770 against COSMOS's 1,030 by sweeping in +90 and +650 # that land weeks later. Those are real, so they are reported separately. on_hand = float((d.get("invp_stats") or {}).get("inventory") or proj[0]["inventory"] or 0) total_inbound = float(proj[0]["warehouse_arrival"] or 0) later_inbound = sum(float(p["warehouse_arrival"] or 0) for p in proj[1:]) heads = "".join( f'{pd.Timestamp(p["date"]):%m/%d/%Y}' for p in proj ) cells = [] for p in proj: units = float(p["inventory"] or 0) value = float(p["inventory_value"] or (units * cfg["cost"])) cover = float(p["cover_days"]) if p["cover_days"] is not None else 0.0 arr = float(p["warehouse_arrival"] or 0) cells.append( f'' f'
{units:,.0f}
' f'
${value:,.0f}
' f'
{cover:,.0f}' f'{arr:,.0f}
' ) return ( '
' f'{heads}' "" f'' f'' f'{"".join(cells)}' "
Product DescriptionDaily Sale
{cfg["title"]}
' f'
{cfg["sku"]} ยท {asin_html}{reviews}
' f'
๐Ÿ“ฆ {on_hand:,.0f} on hand ยท ๐Ÿšš {total_inbound:,.0f} inbound' f'{f" (+{later_inbound:,.0f} later)" if later_inbound else ""} ยท ' f'class {cfg["inv_class"].capitalize()}
{d7:,.0f}
' + (f'
{d_pot:,.0f} ยท potential
' if d_pot else '') + f'
{d6m:,.0f} ยท 6mo avg
" ) # ---------------------------------------------------------------- sidebar if "action_sel" not in st.session_state: st.session_state.action_sel = "All" if "status" not in st.session_state: st.session_state.status = {} if "modified" not in st.session_state: st.session_state.modified = {} if "live_skus" not in st.session_state: st.session_state.live_skus = () with st.sidebar: st.markdown( '', unsafe_allow_html=True, ) st.markdown("") st.caption("ANALYZE") with st.expander("Load products", expanded=not st.session_state.live_skus): mode = st.radio("Mode", ["Single product", "Product line"], horizontal=True, label_visibility="collapsed") with_comp = st.checkbox("Include competitor prices", value=False, help="Adds Buy Box + rival offers per product. Slower; " "needs the competitor-data token configured in .env.") if mode == "Single product": one = st.text_input("Product SKU", placeholder="e.g. UB-PILLOW-QUEEN", help="Enter one Amazon SKU to analyze.") if st.button("๐Ÿ”Ž Analyze product", use_container_width=True, type="primary"): skus = _parse_skus(one) if not skus: st.warning("Enter a SKU first.") else: _clear_live_cache() st.session_state.live_skus = skus[:1] st.session_state.live_comp = with_comp else: # Two steps on purpose: find the WHOLE line first, then decide what # to analyze. Asking for a count up front means choosing blind โ€” and # silently truncating a 99-product line to the first 15. prefix = st.text_input("Product line / SKU prefix", placeholder="e.g. UBMICROFIBERDUVET", help="Finds every product whose SKU contains this text.") if st.button("๐Ÿ”Ž Find products", use_container_width=True): if not prefix.strip(): st.warning("Enter a product-line prefix first.") else: with st.spinner("Finding every product in this lineโ€ฆ"): st.session_state.line = _find_line(prefix.strip()) st.session_state.line_prefix = prefix.strip() paste = st.text_area("โ€ฆor paste an explicit SKU list", placeholder="SKU-001, SKU-002, SKU-003", height=68) if paste.strip() and st.button("๐Ÿ”Ž Analyze pasted list", use_container_width=True, type="primary"): skus = _parse_skus(paste) if not skus: st.warning("Could not read any SKUs from that list.") else: _clear_live_cache() st.session_state.live_skus = skus st.session_state.live_comp = with_comp st.divider() st.caption("FILTERS") q = st.text_input("Search", placeholder="Search SKU or productโ€ฆ") # ------------------------------------------------- product-line picker (main area) line = st.session_state.get("line") or [] if line and not st.session_state.live_skus: st.markdown(f"## {len(line)} products in `{st.session_state.get('line_prefix', '')}`") in_stock = [p for p in line if p["inventory"] > 0] selling = [p for p in line if p["avg_30d"] > 0] st.caption(f"{len(in_stock)} hold stock ยท {len(selling)} sold in the last 30 days ยท " f"{sum(p['inventory'] for p in line):,.0f} units on hand") f1, f2, f3 = st.columns([1.4, 1.4, 1.2]) with f1: order = st.selectbox( "Rank by", list(LINE_SORTS), help="Which products matter most to look at first.") with f2: sizes = sorted({p["size"] for p in line}) pick_sizes = st.multiselect("Size", sizes, default=[], help="Bedding size, read from the SKU. " "Empty = all sizes.") with f3: n_no_stock = len(line) - len(in_stock) only_stock = st.toggle( f"Only products with stock ({len(in_stock)})", value=True, help=f"{n_no_stock} of {len(line)} products hold no inventory. A price " f"change does nothing for a SKU with nothing to sell, so they are " f"hidden by default โ€” switch this off to include them.") pool = [p for p in line if (not only_stock or p["inventory"] > 0) and (not pick_sizes or p["size"] in pick_sizes)] key, reverse = LINE_SORTS[order] pool = sorted(pool, key=lambda p: _line_sort_key(p, key), reverse=reverse) if not pool: st.warning("No products match those filters โ€” loosen them to continue.") st.stop() # Say why the pool is smaller than the line. Without this the headline reads # "99 products" while the slider stops at 68, and nothing on screen explains it. hidden = len(line) - len(pool) if hidden: why = [] if only_stock and n_no_stock: why.append(f"{n_no_stock} have no stock") if pick_sizes: why.append(f"size filter: {', '.join(pick_sizes)}") st.info(esc( f"Showing **{len(pool)} of {len(line)}** โ€” {hidden} hidden " f"({'; '.join(why)}). Clear the filters above to reach all {len(line)}.")) if len(pool) == 1: # a slider needs a range to be a slider how_many = 1 st.caption("1 product matches โ€” it will be analyzed on its own.") else: how_many = st.slider( f"How many to analyze โ€” up to {len(pool)}", 1, len(pool), min(10, len(pool)), help="Each product takes roughly 30โ€“60 seconds against COSMOS, so start " "small and widen once you trust the results.") chosen = pool[:how_many] mins = max(1, round(how_many * 45 / 60)) est = (f"about **{mins} minute{'s' if mins != 1 else ''}**" if mins < 60 else f"about **{mins / 60:.1f} hours** โ€” consider a smaller batch") st.caption(f"Analyzing the top **{how_many}** of {len(pool)} by *{order.lower()}* " f"ยท {est}") st.dataframe( pd.DataFrame([{ "": "โ–ถ" if i < how_many else "", "SKU": p["sku"], "Size": p["size"], "Inventory": p["inventory"], "Units/day (30d)": p["avg_30d"], "Cover (days)": p["cover_days"], "Min PO": p["min_po_quantity"], } for i, p in enumerate(pool)]), hide_index=True, use_container_width=True, height=320, column_config={ "": st.column_config.TextColumn(width="small", help="Included in this run"), "Inventory": st.column_config.NumberColumn(format="%d"), "Units/day (30d)": st.column_config.NumberColumn(format="%d"), "Min PO": st.column_config.NumberColumn(format="%d"), }) b1, b2 = st.columns([1, 3]) with b1: if st.button(f"โ–ถ Analyze {how_many}", type="primary", use_container_width=True): _clear_live_cache() st.session_state.live_skus = tuple(p["sku"] for p in chosen) st.session_state.live_comp = st.session_state.get("live_comp", False) st.rerun() with b2: if st.button("โ†ฉ Clear this line", use_container_width=True): st.session_state.line = [] st.rerun() st.stop() # ---------------------------------------------------------------- load data if not st.session_state.live_skus: st.markdown("## Utopia Pricing Agent") st.info("Open the sidebar and choose what to analyze:\n\n" "- **Single product** โ€” enter one SKU for a full price analysis.\n" "- **Product line** โ€” enter a SKU prefix and press **Find products**. " "You'll see the whole line with stock and sales, then choose how many " "to analyze and in what order.") st.stop() n = len(st.session_state.live_skus) _loader = st.empty() def _on_progress(frac: float, message: str): frac = min(max(frac, 0.0), 1.0) pct = int(round(frac * 100)) # Segmented bar (filled vs empty) for a crisp, on-brand look. fill = int(round(frac * 34)) bar = ("" + "โ–ˆ" * fill + "" + "" + "โ–ˆ" * (34 - fill) + "") try: _loader.markdown( f"""
โš™๏ธ Analyzing against COSMOS
{n} product{'s' if n > 1 else ''} ยท live fees, 6-month history, elasticity & bulk economics
{bar}
{pct}%
{esc(message)}
""", unsafe_allow_html=True, ) except Exception: pass _on_progress(0.0, "Connecting to COSMOSโ€ฆ") data = _load_live(st.session_state.live_skus, st.session_state.get("live_comp", False), progress=_on_progress) _loader.empty() if data["errors"]: st.warning("Some SKUs could not be analyzed: " + esc( " ยท ".join(f"**{s}** ({m})" for s, m in data["errors"].items()))) summary, details = data["summary"], data["details"] for sku in details: # keep statuses across reruns; init the new ones st.session_state.status.setdefault(sku, "Pending") if summary.empty: st.info("No SKUs produced an analysis. Check the warnings above (typical causes: " "wrong SKU code, or the SKU has no sales/price data in COSMOS).") st.stop() # ---------------------------------------------------------------- sidebar filters (cont.) with st.sidebar: f_mkt = st.selectbox("Marketplace", ["All"] + sorted(summary["marketplace"].unique()), format_func=lambda m: "๐ŸŒ All" if m == "All" else mkt_label(m)) f_conf = st.selectbox( "Model trust", ["All", "High", "Medium", "Low", "None"], help="How far this SKU's model may be trusted: backtest accuracy, breadth " "of price evidence, elasticity usability and data completeness. Only " "High may be bulk-approved.") f_stat = st.selectbox("Status", ["All", "Pending", "Approved", "Rejected"]) st.divider() st.caption("CONTROLS") kill = st.toggle("๐Ÿ›‘ Global kill switch", help="Emergency stop: pauses all approvals.") st.button("โ†บ Reset workspace", on_click=reset_workspace, use_container_width=True) st.divider() st.caption("DATA STATUS") st.caption(f"Prices as of {TODAY:%b %d, %Y}\n\n" f"{len(details)} SKUs ยท 6-month COSMOS history\n\n" + ("Competitor prices: included\n\n" if st.session_state.get("live_comp") else "Competitor prices: not included\n\n") + "Engine v1 ยท live COSMOS data") # ---------------------------------------------------------------- header hour = datetime.now().hour greet = "Good morning" if hour < 12 else ("Good afternoon" if hour < 18 else "Good evening") pending = [s for s, v in st.session_state.status.items() if v == "Pending" and s in details] # Bulk approval requires a model that has been SCORED on that SKU's own history, # not merely a confident-looking label. `trust.auto_approve` is High only when the # backtest, the evidence base and the data completeness all pass. bulk_eligible = [s for s in pending if (details[s].get("trust") or {}).get("auto_approve") and details[s]["action"] in ("Increase", "Decrease") and abs(details[s]["delta_pct"]) <= 5] h1, h2 = st.columns([3, 1.3]) with h1: # The headline slot goes to the thing the reviewer is here to do, not to a # greeting. The greeting keeps the warmth, at caption size where it belongs. n_pend = len(pending) if len(details) == 1: # Lead with the product and the verdict. "1 recommendation to review" is a queue # heading, and on a single product there is no queue โ€” the reader already knows what # they opened and wants to know what to do about it. _hs = next(iter(details)) _hd = details[_hs] _title = (_hd.get("cfg") or {}).get("title") or _hs _act = _hd["action"] _verb = {"Increase": "Raise", "Decrease": "Lower", "Maintain": "Hold", "Investigate": "Investigate"}.get(_act, _act) if _act in ("Increase", "Decrease"): _hl = (f"{_verb} to ${_hd['rec_price']:,.2f} " f"({_hd['delta_pct']:+.1f}% from ${_hd['current_price']:,.2f})") elif _act == "Maintain": _hl = f"Hold at ${_hd['current_price']:,.2f}" else: _hl = f"Investigate โ€” hold at ${_hd['current_price']:,.2f}" st.markdown(f"## {esc(_hl)}") _why = ", ".join(REASON_LABELS.get(c, c) for c in _hd["reasons"]) or "no signals" st.caption(esc(f"{_title} ยท {_hs} ยท {_why}")) else: st.markdown(f"## {n_pend} recommendation{'s' if n_pend != 1 else ''} to review") st.caption(f"{greet}, Utopia ๐Ÿ‘‹ ยท sorted by expected profit impact") with h2: st.markdown("") # Explain a control that cannot fire, rather than leaving a dead button. if not bulk_eligible and not kill: low_trust = [s for s in pending if not (details[s].get("trust") or {}).get("auto_approve")] why_blocked = (f"{len(low_trust)} of {n_pend} below High trust" if low_trust else "nothing eligible") else: why_blocked = "" st.button(f"โœ… Bulk approve ยท {len(bulk_eligible)}", disabled=kill or not bulk_eligible, use_container_width=True, on_click=bulk_approve, args=(bulk_eligible,), help=("Bulk approval needs a model scored on that SKU's own history. " + (f"Blocked: {why_blocked}." if why_blocked else "Approves pending High-trust recommendations within the 5% step cap."))) if why_blocked: st.caption(esc(f"Needs High trust ยท {why_blocked}")) if kill: st.error("๐Ÿ›‘ **Kill switch active** โ€” approvals and price pushes are paused across all " "marketplaces. Toggle it off in the sidebar to resume.") # ---------------------------------------------------------------- stat tiles needing_action = [s for s in pending if details[s]["action"] != "Maintain"] opportunity = sum(max(details[s]["impact_30d"], 0) for s in pending) # Same thresholds the decision logic uses, imported rather than restated so the # tiles and the recommendations can never drift apart. from dashboard.live_data import HIGH_COVER_DAYS, LOW_COVER_DAYS stockout_risk = sum(1 for s in details if 0 < details[s]["cover_days"] <= LOW_COVER_DAYS) overstock_risk = sum(1 for s in details if details[s]["cover_days"] >= HIGH_COVER_DAYS) # The opportunity figure inherits the model's measured error, so it is shown as a # band. A precise-looking $32,974 from a model backtested at ยฑ58% reads as a promise. _errs = [e for s in pending if (e := model_error_pct(details[s])) is not None] _err = max(_errs) if _errs else None # ONE SKU is a different question from a portfolio, so it gets different tiles. # # The counts above answer "where in my catalogue should I look?" โ€” they are triage. On a # single product that question is already answered, and the same tiles degenerate into # tautologies: "1 SKU needing action" out of one, "0 stockout-risk SKUs", "1 pending # approval". Nothing there tells a seller whether to take the price. # # So on a single product the header answers the seller's questions instead: what does this # move earn, what margin does it leave, how much room is there before it loses money, will # stock last, is it selling, and who else is on the listing. if len(details) == 1: _sku = next(iter(details)) _d = details[_sku] _e1 = model_error_pct(_d) _econ = {x["key"]: x for x in (_d.get("scen_econ") or [])} _cur_e = _econ.get("current") or {} # The row at the price we are RECOMMENDING TODAY, not the rung the cascade named. When a # move is step-capped those differ: `rec_key` here is the $20.00 destination while the # headline recommends $17.94, and quoting the destination's margin beside a $17.94 headline # would promise 13.4% for a move that delivers 6.6%. _rec_e = next((x for x in (_d.get("scen_econ") or []) if abs(x["price"] - _d["rec_price"]) < 0.011), _econ.get(_d.get("rec_key")) or {}) _fi = _d.get("floor_info") or {} _floor = _fi.get("floor") _cover = _d.get("cover_days") _out = _d.get("outlook") or {} _cm = _d.get("comp_meta") or {} _ppc = _d.get("ppc") or {} # 1. What the move is worth. Held to the same error band as the portfolio figure. _impact = _d.get("impact_30d") or 0.0 _t_impact = ("๐Ÿ’ฐ", money_range(_impact, _e1) if _impact else "โ€”", "Impact ยท 30d if approved", (f"net of ads + storage ยท ยฑ{_e1 * 100:.0f}% model error" if _e1 else "net of advertising and storage")) # 2. Margin now vs after. The number a seller judges a price by โ€” so both halves have to # be the same model at two prices. The current row's headline margin is computed from # BOOKED profit at the average price customers paid; comparing that to a projection makes # the move look better or worse than it is. On this SKU booked reads 6.7% while the model # at today's price reads 3.3%, so "6.7% โ†’ 6.6%" said the move achieved nothing when it # actually roughly doubles margin. _m_rec = modelled(_rec_e).get("net_margin_pct") _m_now = modelled(_cur_e).get("net_margin_pct") _t_margin = ("๐Ÿ“Š", f"{_m_rec:.1f}%" if _m_rec is not None else "โ€”", "Net margin after the move", (f"now {_m_now:.1f}% ยท same model, both prices" if _m_now is not None else "after ads and storage")) # 3. Headroom to the floor โ€” how much room before the price stops paying. if _floor and _d.get("current_price"): _head = (_d["current_price"] - _floor) / _d["current_price"] * 100 _t_floor = ("๐Ÿ›Ÿ", f"{_head:+.1f}%", "Headroom above the floor", f"floor ${_floor:,.2f} ({_fi.get('binding', 'break-even')}) ยท " f"today ${_d['current_price']:,.2f}") else: _t_floor = ("๐Ÿ›Ÿ", "โ€”", "Headroom above the floor", "no floor โ€” cost data missing") # 4. Will the stock last? A date beats a threshold count on a single SKU. # # Both readings are shown, because COSMOS's Inventory Planning grid prints the LONGER # one and a single unexplained number here reads as a mismatch with their tool. They are # different questions: ours is what is on the shelf now, COSMOS's counts stock still in # transit. Neither is wrong; the pricing rules use the shorter one deliberately, because # a SKU cannot sell a unit that has not landed. _inv_cls = ((_d.get("cfg") or {}).get("inv_class") or "alpha").lower() _onhand = _d.get("cover_days_onhand") _band_hex, _band_word = theme.cover_band(_cover, _inv_cls) _so, _arr = _out.get("stockout_date"), _out.get("next_arrival_date") if _so and _arr: _cover_note = (f"{_band_word} ยท out ~{_so:%d %b} ยท restock {_arr:%d %b}" + (" โ€” gap" if _so < _arr else " โ€” covered")) elif _so: _cover_note = f"{_band_word} ยท out ~{_so:%d %b} ยท no restock scheduled" else: _cover_note = f"{_band_word} ยท {_inv_cls.capitalize()}-class band" # Matches the COSMOS Inventory Planning grid. On-hand is shown beside it because the two # differ by stock still in transit, and a stockout judged on undelivered units is worth # being able to see. if _onhand is not None and _cover is not None and _onhand != _cover: _cover_note += f" ยท {_onhand:,} d on hand, rest inbound" # A COSMOS band and a pricing trigger are different things, and the colour is the loudest # thing on the tile โ€” so where they disagree, say so. Without this a pink 82-day SKU reads # as "the engine is about to discount this", when the cut rule does not fire until 90. if _cover is not None and _band_word in ("high", "low") and not ( 0 < _cover <= LOW_COVER_DAYS or _cover >= HIGH_COVER_DAYS): _edge = HIGH_COVER_DAYS if _band_word == "high" else LOW_COVER_DAYS _cover_note += (f" ยท COSMOS band only โ€” no price move until {_edge} d") _t_cover = ("๐Ÿ“ฆ", f"{_cover:,} d" if _cover is not None else "โ€”", "Inventory cover", _cover_note, _band_hex) # 5. Is it actually selling? _u = _d.get("units_day") _t_vel = ("โšก", f"{_u:,.0f}/day" if _u else "โ€”", "Sales velocity", (f"TACoS {_ppc['tacos']:.1f}%" if _ppc.get("tacos") is not None else "30-day average")) # 6. Who else is on this listing โ€” the one tile that can invalidate all the others. if _cm.get("state_usable"): _bb = str(_cm.get("state") or "โ€”").replace("_", " ").title() _riv = _cm.get("rivals") or 0 _t_comp = ("๐Ÿ†", _bb, "Buy Box", (f"{_riv} rival(s) ยท median ${_cm['median']:,.2f}" if _cm.get("median") else f"{_riv} rival(s)")) else: _t_comp = ("๐Ÿ†", "N/A", "Buy Box", "not covered by the competitor sheet" if not _cm.get("sheet_covers_sku") else "no usable competitor data") tiles = [_t_impact, _t_margin, _t_floor, _t_cover, _t_vel, _t_comp] else: tiles = [ ("โšก", str(len(needing_action)), "SKUs needing action", "queue below, ranked by impact"), ("๐Ÿ’ฐ", money_range(opportunity, _err), "Profit opportunity ยท 30d", (f"open positive impacts ยท ยฑ{_err * 100:.0f}% model error" if _err else "sum of open positive impacts")), ("โณ", str(len(pending)), "Pending approvals", f"{len(bulk_eligible)} bulk-eligible"), ("๐Ÿ“‰", str(stockout_risk), "Stockout-risk SKUs", f"cover โ‰ค {LOW_COVER_DAYS} days"), ("๐Ÿ“ฆ", str(overstock_risk), "Overstock-risk SKUs", f"cover โ‰ฅ {HIGH_COVER_DAYS} days"), ] st.markdown('
' # Tiles are (icon, value, label, note) with an OPTIONAL 5th accent colour, so a # banded reading can tint its icon without every other tile growing a field. + "".join(theme.tile(*t) for t in tiles) + '
', unsafe_allow_html=True) st.markdown("") # ---------------------------------------------------------------- action pills counts = summary["action"].value_counts().to_dict() pills = [("All", "All", len(summary)), ("Increase", "โ†‘ Raise", counts.get("Increase", 0)), ("Decrease", "โ†“ Lower", counts.get("Decrease", 0)), ("Maintain", "โ†’ Hold", counts.get("Maintain", 0)), ("Investigate", "๐Ÿ” Check", counts.get("Investigate", 0))] with st.container(key="pillrow"): # keyed so the CSS can keep these pills tight pcols = st.columns([0.8, 1, 1, 1, 1, 2]) for col, (a, lbl, n) in zip(pcols, pills): col.button(f"{lbl} ยท {n}", key=f"pill_{a}", type="primary" if st.session_state.action_sel == a else "secondary", use_container_width=True, on_click=set_action, args=(a,), help=f"Show {a.lower()} recommendations" if a != "All" else "Show all") # ---------------------------------------------------------------- queue rows = summary.copy() if f_mkt != "All": rows = rows[rows["marketplace"] == f_mkt] if f_conf != "All": rows = rows[rows["trust_tier"] == f_conf] if f_stat != "All": rows = rows[[st.session_state.status[s] == f_stat for s in rows["sku"]]] if st.session_state.action_sel != "All": rows = rows[rows["action"] == st.session_state.action_sel] if q: mask = (rows["sku"].str.contains(q, case=False, regex=False) | rows["title"].str.contains(q, case=False, regex=False)) rows = rows[mask] if rows.empty: st.info("No recommendations match the current filters.") for _, r in rows.iterrows(): sku = r["sku"] d = details[sku] status = st.session_state.status[sku] modified_price = st.session_state.modified.get(sku) tier, score = d["confidence"] stat_icon = theme.REC_STATUS[status][1] glyph = ACTION_GLYPH[d["action"]] # Header: SKU ยท action+price ยท reason. ASIN, marketplace and inventory band moved # into the card โ€” six competing facts in one line had no hierarchy and wrapped. asin = d.get("asin") or d["competitors"].iloc[0]["asin"] inv_cls = d["cfg"]["inv_class"].lower() trust = d.get("trust") or {} target = modified_price if modified_price is not None else d["rec_price"] mod_tag = " (modified)" if modified_price is not None else "" if d["action"] == "Increase": act = (f":green[**โ†‘ RAISE ${d['current_price']:.2f} โ†’ ${target:.2f} " f"(+{abs(d['delta_pct']):.1f}%){mod_tag}**]") elif d["action"] == "Decrease": act = (f":red[**โ†“ LOWER ${d['current_price']:.2f} โ†’ ${target:.2f} " f"(โˆ’{abs(d['delta_pct']):.1f}%){mod_tag}**]") elif d["action"] == "Investigate": act = f":orange[**๐Ÿ” INVESTIGATE ยท hold ${d['current_price']:.2f}**]" else: act = f":gray[**โ†’ HOLD ${d['current_price']:.2f}**]" # Trust rides in the row header, so it is visible before the row is opened. trust_mark = {"High": "", "Medium": " ยท โš ๏ธ medium trust", "Low": " ยท โš ๏ธ low trust", "None": " ยท โ›” data incomplete"}.get( trust.get("tier", ""), "") label = esc(f"{stat_icon} **{sku}** ยท {act} ยท {r['primary_reason']}{trust_mark}") with st.expander(label): # ---------- decision card ---------- # Keyed container so the responsive CSS can stack these three columns on a # narrow window instead of squeezing the metrics. with st.container(key=f"deccard_{sku}"): c1, c2, c3 = st.columns([2.4, 2.2, 1.4]) with c1: asin_url = _amazon_url(asin, r["marketplace"]) asin_md = f"[{asin}]({asin_url})" if asin_url else esc(str(asin)) st.caption(f"{r['title']} ยท ASIN {asin_md} ยท {mkt_label(r['marketplace'])}" f" ยท {INV_CLASS_LABELS.get(inv_cls, inv_cls)}") st.markdown(esc(f"#### {glyph} {d['action']}" + ( f" to **${d['rec_price']:.2f}**" if d["action"] in ("Increase", "Decrease") else ""))) st.markdown(esc(f"**Current price ${d['current_price']:.2f}**")) chips = "".join(theme.chip(REASON_LABELS.get(c, c), theme.BRAND) for c in d["reasons"]) err = model_error_pct(d) trust_chip = "" if trust: tcol = {"High": theme.STATUS["good"], "Medium": theme.STATUS["warning"], "Low": theme.STATUS["serious"], "None": theme.STATUS["critical"]}[trust["tier"]] tlabel = f"๐ŸŽฏ {trust['tier']} trust" if err: tlabel += f" ยท ยฑ{err * 100:.0f}% backtested" trust_chip = theme.chip(tlabel, tcol) # Only ONE reliability badge. The old confidence tier was derived # from days-with-sales and happily read "Medium (62)" for a SKU # whose elasticity could not be told apart from zero โ€” shown next # to "Low trust" it just contradicted it. The tier is still used # internally to width the forecast band. st.markdown(trust_chip + theme.status_badge(status) + chips, unsafe_allow_html=True) if trust.get("tier") in ("Low", "None"): st.caption(esc(f"โ†’ {trust['permits']} ยท see **๐ŸŽฏ Track record**")) if modified_price is not None: st.caption(esc(f"โœ๏ธ Approved at modified price ${modified_price:.2f} " f"(engine recommended ${d['rec_price']:.2f})")) for c in (d.get("clamps") or []): st.caption(esc(f"๐ŸŽฏ {c}")) # Guardrail detail is reference, not a decision input โ€” one line of # it belongs on the card, the rest behind a disclosure. fl = d.get("floor_info") or {} with st.popover("๐Ÿ›ก๏ธ Guardrails & break-even", use_container_width=True): st.markdown(esc( f"**Allowed range ${d['min_price']:.2f}โ€“${d['max_price']:.2f}** ยท " f"objective: {OBJECTIVE_LABELS.get(r['objective'], r['objective'])}")) be_rows = [("Fees + COGS only", d["break_even"], "ignores advertising")] if d.get("break_even_with_ads"): be_rows.append(("Including advertising", d["break_even_with_ads"], "at current ad cost per unit")) if d.get("break_even_empirical"): be_rows.append(("From actual booked profit", d["break_even_empirical"], "carries every cost COSMOS books")) st.dataframe( pd.DataFrame(be_rows, columns=["Break-even", "Price", "Basis"]), hide_index=True, use_container_width=True, column_config={"Price": st.column_config.NumberColumn(format="$%.2f")}) if fl.get("binding") and fl["binding"] != "accounting": st.caption(esc( f"Floor is set by the **{fl['binding']}** figure " f"(${fl['floors'][fl['binding']]:.2f}). The fees-only number " f"(${fl['floors']['accounting']:.2f}) ignores advertising, so " f"pricing to it loses money on every unit.")) with c2: # Calibrated net profit for the headline metric (matches the tables). _econ = {x["key"]: x for x in (d.get("scen_econ") or [])} _rec_e = _econ.get(d["rec_key"]) _cur_e = _econ.get("current") rec_units = _rec_e["units_day"] if _rec_e else round(d["rec_units_day"]) cur_units = _cur_e["units_day"] if _cur_e else round(d["units_day"]) rec_net = _rec_e["net_30d"] if _rec_e else d["rec_profit_30d"] cur_net = _cur_e["net_30d"] if _cur_e else d["profit_30d"] m1, m2, m3, m4 = st.columns(4) m1.metric("Units/day", f"{rec_units:,}", f"{rec_units - cur_units:+,} vs now", delta_color="off") # Point estimate stays the headline so it can be scanned; the range # sits underneath. A range in the value slot has no break # opportunity and shatters character-by-character in a narrow column. # Point estimate as the headline, uncertainty as a short suffix. The # delta cell is ~110px wide, so the full range belongs on the # Scenarios tab, not here. m2.metric("Net profit/30d", compact_money(rec_net), (f"ยฑ{err * 100:.0f}% model error" if err else f"{'+' if rec_net >= cur_net else 'โˆ’'}" f"{compact_money(abs(rec_net - cur_net))} vs now"), delta_color="off") # Cover in days, with the DATE it runs out โ€” "26 days" is abstract, # "out on 24 Aug" is a diary entry someone can act on. ol = d.get("outlook") or {} so = ol.get("stockout_date") m3.metric("Cover", f"{d['rec_cover_days']} d", (f"out ~{so:%d %b}" if so else f"{d['rec_cover_days'] - d['cover_days']:+d} d"), delta_color="off") m4.metric("PPC/day", f"${d['ppc']['spend_day']:,.0f}", f"TACoS {d['ppc']['tacos']:.1f}%", delta_color="off") with c3: # The primary action states what the evidence actually supports. At # Low trust a bold "Approve" invites a commitment the model cannot # back โ€” the same click, honestly labelled, is a test. tier = trust.get("tier", "Medium") if tier == "None": st.button("โ›” Blocked โ€” fix data", key=f"ap_{sku}", disabled=True, use_container_width=True, help="; ".join(trust.get("reasons", [])) or "data incomplete") elif tier == "Low": st.button(f"๐Ÿงช Start test at ${d['rec_price']:.2f}", key=f"ap_{sku}", type="primary", disabled=kill or status == "Approved", on_click=set_status, args=(sku, "Approved"), use_container_width=True, help="Backtest error is high on this SKU, so treat the move " "as an experiment: run it, then re-read the response.") else: st.button("โœ… Approve", key=f"ap_{sku}", type="primary", disabled=kill or status == "Approved", on_click=set_status, args=(sku, "Approved"), use_container_width=True) with st.popover("โœ๏ธ Modify", use_container_width=True, disabled=kill): st.number_input("New price ($)", key=f"mod_{sku}", min_value=float(d["min_price"]), max_value=float(d["max_price"]), value=float(min(max(d["rec_price"], d["min_price"]), d["max_price"])), step=0.05, format="%.2f") st.caption(esc(f"Allowed ${d['min_price']:.2f}โ€“${d['max_price']:.2f} ยท " ">5% from current needs elevated approval")) st.button("Apply & approve", key=f"modb_{sku}", on_click=apply_modify, args=(sku, details), use_container_width=True) st.button("โœ–๏ธ Reject", key=f"rj_{sku}", disabled=kill or status == "Rejected", on_click=set_status, args=(sku, "Rejected"), use_container_width=True) if status != "Pending": st.button("โ†ฉ๏ธ Undo", key=f"un_{sku}", on_click=set_status, args=(sku, "Pending"), use_container_width=True) # ---------- strategy options ---------- scen = d["scenarios"].set_index("scenario") if d["action"] == "Investigate": st.info("No price options proposed โ€” investigate first. A change without a " "confirmed cause is blocked by the root-cause gate.") else: # Calibrated economics keyed by scenario, so this table matches the # Scenarios tab (net profit anchored to actual booked profit). econ_by_key = {x["key"]: x for x in (d.get("scen_econ") or [])} cover_by_key = {k: scen.loc[k]["cover_days"] for k in scen.index} def opt_row(label, key): e = econ_by_key.get(key) if not e: return None return { "Option": label, "Price": e["price"], "Units/day": e["units_day"], "Profit/30d": e["net_30d"], "Margin %": e["net_margin_pct"], "Cover (days)": int(cover_by_key.get(key, 0)), } # One row per distinct price. Rungs can legitimately share a price (e.g. # nothing in the grid is bolder than the recommendation), so merge their # labels instead of rendering the same row twice. # # Two things this table used to say that were not true: # # 1. It called the first row "Current" at a price that is NOT the current price. # That row is real booked history, so its price is the AVERAGE SOLD price over # the window (revenue / units) โ€” deliberately, so revenue reconciles. But the # rungs below it are built from today's price, so on one SKU the table read # Current $18.18 / Conservative $17.43 and the "conservative" option looked like # a cut on an Increase recommendation. Same column, two different bases, one # label. Naming the basis is what makes the column readable. # 2. It starred "Recommended" against the TARGET price while the header recommended # the step-capped price for today. A row labelled Recommended has to be the # thing being recommended. win = d.get("scen_window") or 90 rec_now, tgt = float(d["rec_price"]), float(d["target_price"]) merged: dict[str, list[str]] = {} for name, key in [(f"Current ยท avg sold ({win}d)", "current")] \ + list(d["strategy_keys"].items()): merged.setdefault(key, []).append(name) # Which grid key are we actually moving to today? rec_key_now = next( (k for k, e in econ_by_key.items() if abs(e["price"] - rec_now) < 0.011), None) opt_rows = [] for key, names in merged.items(): e = econ_by_key.get(key) label = " ยท ".join(names) # This rung is the destination, not this week's move โ€” said once, over the # whole merged label, not once per name it merged. if d.get("stepped") and e is not None and abs(e["price"] - tgt) < 0.011: label = f"Target (~14d) ยท {label}" star = (rec_key_now is None and "Recommended" in names) or key == rec_key_now opt_rows.append(opt_row(("โ˜… " if star else "") + label, key)) # The step-capped price may not be one of the named rungs. If it is a grid row that # nothing else labelled, surface it โ€” otherwise the price in the header appears # nowhere in the table underneath it. if rec_key_now is not None and rec_key_now not in merged: row = opt_row("โ˜… Recommended now (5% step cap)", rec_key_now) if row: opt_rows.append(row) opt_rows = [x for x in opt_rows if x] opt_rows.sort(key=lambda x: x["Price"]) with st.container(key=f"optrow_{sku}"): oc1, oc2 = st.columns([4, 1]) with oc1: st.dataframe( pd.DataFrame(opt_rows), hide_index=True, use_container_width=True, column_config={ "Price": st.column_config.NumberColumn(format="$%.2f"), "Units/day": st.column_config.NumberColumn(format="%d"), "Profit/30d": st.column_config.NumberColumn(format="$%d"), "Margin %": st.column_config.NumberColumn(format="%.1f%%"), }, ) cur_econ = econ_by_key.get("current") or {} cur_sold = cur_econ.get("price") cap = ("Profit/30d is net of advertising and storage โ€” same " "basis as the Scenarios tab.") # Reconcile the two prices ON SCREEN. They are both correct and they are # different things; without this the reader is left to conclude one is a bug. if cur_sold and abs(cur_sold - d["current_price"]) >= 0.01: cap += (f" The Current row is **${cur_sold:,.2f}** โ€” the average price " f"actually *sold* over the last {win} days (revenue รท units), " f"which is what its profit is computed from. Today's price is " f"**${d['current_price']:,.2f}**, and every option below is " f"calculated from that, so an option can sit below the Current " f"row without being a price cut.") st.caption(cap) with oc2: st.button("๐Ÿ“Š View more", key=f"vm_{sku}", use_container_width=True, on_click=goto_view, args=(sku, VIEW_SCENARIOS), help="Jump to the full Scenarios breakdown") # ---------- section switcher (jumpable, unlike native tabs) ---------- view_key = f"view_{sku}" if view_key not in st.session_state: st.session_state[view_key] = VIEW_KEYS[0] sel = st.segmented_control("Section", VIEW_KEYS, key=view_key, format_func=lambda k: VIEW_LABELS[k], label_visibility="collapsed") if sel is None: # ignore an accidental deselect sel = st.session_state[view_key] or VIEW_KEYS[0] if sel == "price": st.plotly_chart(price_demand_chart(d), use_container_width=True, config={"displayModeBar": False}, key=f"pd_{sku}") with st.popover("View as table"): st.dataframe(d["hist"].tail(30), hide_index=True, use_container_width=True) elif sel == "inventory": # Dated outlook up top: when it empties, when relief lands, and how # many days of the last month were spent effectively out of stock. ol = d.get("outlook") or {} oos, known = d.get("stockout_days_30d") or (0, 0) i1, i2, i3 = st.columns(3) so, arr = ol.get("stockout_date"), ol.get("next_arrival_date") if so: src = ("COSMOS projection" if ol.get("stockout_source") == "projection" else "extrapolated from velocity") i1.metric("Runs out", f"{so:%d %b}", f"{ol.get('days_to_stockout')} days ยท {src}", delta_color="off") else: i1.metric("Runs out", "not in horizon", f"within {ol.get('horizon_days') or 0} days", delta_color="off") if arr: gap = (arr - so).days if so else None i2.metric("Next arrival", f"{arr:%d %b}", (f"{gap:+d} days vs stockout" if gap is not None else f"{ol.get('next_arrival_units', 0):,.0f} units"), delta_color="off") else: i2.metric("Next arrival", "none scheduled", f"within {ol.get('horizon_days') or 0} days", delta_color="off") i3.metric("Days out of stock", f"{oos}", f"of {known} days with a reading" if known else "no inventory readings", delta_color="off") if so and arr and (arr - so).days > 0: st.warning(esc( f"โš ๏ธ Stock runs out around **{so:%d %b}** but the next arrival is " f"**{arr:%d %b}** โ€” **{(arr - so).days} days uncovered.** Raising " f"price slows the burn; it does not create stock.")) st.markdown(inventory_table_html(d), unsafe_allow_html=True) st.caption( "Cell: big = projected units ยท purple = inventory value ยท blue = days of " "cover ยท right = incoming units. Shading per this SKU's " f"{d['cfg']['inv_class'].capitalize()}-class cover bands." ) mp = d.get("min_po") or {} if mp.get("qty"): po_note = (f"COSMOS suggests a min PO of **{mp['qty']:,.0f} units**" + (f" within **{mp['days']:,.0f} days**" if mp.get("days") else "") + " to avoid stockout.") else: po_note = "No minimum-PO recommendation from COSMOS." st.caption(f"**Real COSMOS INVP projection** (invp-insight dateMap โ€” projected " f"units, value, cover days & warehouse arrivals). {po_note}") st.plotly_chart(inventory_chart(d), use_container_width=True, config={"displayModeBar": False}, key=f"inv_{sku}") elif sel == "repricing": # What the recommended price does to the SHELF. The Inventory tab above is # COSMOS's projection at TODAY's price, so a raise or a cut is otherwise shown # with no stock consequence at all. econ = {x["key"]: x for x in (d.get("scen_econ") or [])} cur_e = econ.get("current") or {} # The row at the price we actually recommend today, not the rung the cascade # named โ€” those differ whenever the move is step-capped. rec_e = next((x for x in (d.get("scen_econ") or []) if abs(x["price"] - d["rec_price"]) < 0.011), None) # Demand response from the PRICE RATIO, not from the scenario table's unit # counts. Those are rounded to whole units, and at low volume the rounding eats # the entire signal: one real SKU sells 10 a month, and a 5% cut should lift that # to 10.7 โ€” both round to 10, so the re-projection reported a price cut with no # effect on stock whatsoever. # # (p_new / p_now) ** elasticity is exact, and it is the same relationship the # Scenarios tab applies: their common anchor cancels when you take the ratio of # two rows, so this cannot drift from that table. _ed = d.get("elasticity_detail") or {} el = _ed.get("elasticity") or FALLBACK_ELASTICITY u_now = float(d.get("units_day") or 0.0) ratio = ((d["rec_price"] / d["current_price"]) ** el if d.get("current_price") else 1.0) u_new = u_now * ratio rows = reproject_inventory(d.get("inv_projection") or [], u_now or 0, u_new or 0) if d["action"] == "Maintain" or abs(d["rec_price"] - d["current_price"]) < 0.005: st.info("**No price change is recommended**, so the stock outlook is the " "one on the Inventory tab. Nothing to re-project.") elif not rows: st.warning(esc( "Cannot re-project the shelf for this SKU โ€” that needs COSMOS's weekly " "projection and a demand response at both prices, and one of them is " "missing. The Inventory tab still shows the outlook at today's price.")) else: pct = (u_new / u_now - 1) * 100 if u_now else 0.0 verb = "slows" if u_new < u_now else "speeds up" r1, r2, r3 = st.columns(3) r1.metric("Price", f"${d['rec_price']:,.2f}", f"{d['delta_pct']:+.1f}% vs ${d['current_price']:,.2f}", delta_color="off") r2.metric("Units/day", f"{u_new:,.1f}", f"{pct:+.1f}% vs {u_now:,.1f} today", delta_color="off") last = rows[-1] r3.metric("Stock at horizon", f"{last['units_new']:,}", f"{last['delta_units']:+,} vs today's price", delta_color="off") # COSMOS sometimes projects a completely flat shelf โ€” same units every week, # no draw at all. That happens on very low-velocity SKUs (one real example # holds 167 units and COSMOS shows 167 for all eleven weeks). There is then no # depletion to re-scale, so the units column cannot move however the price # changes, and only cover responds. Say so: a table of "+0" with no # explanation reads as a broken feature rather than an upstream gap. if all(r["delta_units"] == 0 for r in rows): st.info(esc( "COSMOS projects **no depletion** for this SKU โ€” the same " f"{rows[0]['units_now']:,} units in every one of its " f"{len(rows)} weekly snapshots. There is no draw to re-scale, so the " "unit columns cannot respond to a price change; only cover days move, " "because those are computed from our own velocity.")) first_out = next((r for r in rows if r["stockout"]), None) if first_out: st.error(esc( f"At ${d['rec_price']:,.2f} the shelf empties by " f"**{pd.Timestamp(first_out['date']):%d %b}** โ€” earlier than COSMOS " f"projects at today's price. A cut sells the remaining stock faster; " f"it does not create any.")) else: st.success(esc( f"Demand {verb} by {abs(pct):.1f}%, so the shelf lasts " f"{'longer' if u_new < u_now else 'less time'}. No stockout inside " f"COSMOS's {len(rows)}-week horizon at this price.")) st.dataframe( pd.DataFrame([{ "Week": pd.Timestamp(r["date"]).strftime("%m/%d/%Y"), "Units (today's price)": r["units_now"], "Units (new price)": r["units_new"], "Change": r["delta_units"], "Cover now": r["cover_now"], "Cover new": r["cover_new"], "Arriving": int(r["arrival"]), } for r in rows]), hide_index=True, use_container_width=True, column_config={ "Units (today's price)": st.column_config.NumberColumn(format="%d"), "Units (new price)": st.column_config.NumberColumn(format="%d"), "Change": st.column_config.NumberColumn(format="%+d"), "Cover now": st.column_config.NumberColumn(format="%d d"), "Cover new": st.column_config.NumberColumn(format="%d d"), "Arriving": st.column_config.NumberColumn(format="%d"), }, ) st.caption( "**This table is OUR projection, not COSMOS's.** The Inventory tab is " "COSMOS's own forecast at today's price; this re-runs it at the " "recommended price by scaling each week's SALES with the same elasticity " "the Scenarios tab uses. Arrivals are unchanged โ€” a PO already placed " "does not move because we re-priced โ€” and unit value stays on COSMOS's " "cost basis. Stock is floored at zero; a negative shelf is not a forecast. " "**Both cover columns divide by OUR velocity** so the difference between " "them is the price effect alone โ€” which is why they can sit a day or two " "off the Inventory tab, where COSMOS's own figure is shown verbatim." ) elif sel == "scenarios": render_scenarios(d, key_prefix=f"sc_{sku}") elif sel == "competitors": comp = d["competitors"] meta = d.get("comp_meta") or {} rivals = comp[~comp["is_us"]] # third-party sellers only if len(rivals) == 0: # Nothing to compare against โ€” show a message, NOT your own listing. if meta.get("sheet_covers_sku"): # The workbook covers this SKU but produced no usable rival: every match # was fuzzy, or the only rivals had their own Buy Box suppressed. Say which, # because "no data" and "data we refused to price against" are different. st.warning( "๐Ÿ“‹ **The comparison sheet covers this SKU but has no priceable " "rival.** Every matched rival was either a NEAR colour match " "(unverified) or had its own Buy Box suppressed, so none may move a " "price. Open the workbook to see the raw pairings.") elif not meta.get("scraped"): st.info("๐Ÿ” **Competitor prices not scraped.** Tick **Include " "competitor prices** in the sidebar and reload to pull Buy " "Box + rival offers for this ASIN." + (f" This SKU is also outside the comparison sheet, which " f"covers {meta['sheet_covered_skus']} SKU(s) of " f"{meta['sheet_line']}." if meta.get("sheet_line") else "")) else: bb = meta.get("buy_box_status") bb_price = meta.get("buy_box_price") own = meta.get("own_offers", 0) detail = "" if bb == "WON" and bb_price: detail = (f" You hold the **Buy Box at ${bb_price:.2f}**" + (f" across {own} of your own offers" if own > 1 else "") + ".") st.success("โœ… **No competitors on this ASIN.** Utopia Brands is the " f"only seller โ€” no third-party offers were found.{detail} " "Pricing is driven by your own economics, not rivals.") else: # Real competitors exist โ†’ show ONLY the rival sellers. # # The two sources describe different things and must not share a sentence: an # Apify row is another seller on OUR listing, a sheet row is a rival brand's own # listing. Calling the latter "an offer on this ASIN" would be simply untrue. if meta.get("source") == "comparison-sheet": as_of = meta.get("sheet_as_of") st.caption( f"**{len(rivals)} like-for-like rival(s)** from the comparison sheet โ€” " f"each is a *different brand's own listing* matched on size + colour, " f"not an offer on your ASIN" + (f" ยท {meta['exact_rivals']} exact match(es) priceable" if meta.get("exact_rivals") is not None else "") + (f" ยท median ${meta['median']:,.2f}" if meta.get("median") else "") + f" ยท your price ${d['current_price']:.2f}" + (f" ยท sheet dated {as_of:%d %b %Y}" if as_of else "") + ".") else: st.caption(f"**{len(rivals)} competitor offer(s)** on this ASIN" + (f" ยท competitor median ${meta['median']:,.2f}" if meta.get("median") else "") + f" ยท your price ${d['current_price']:.2f}.") disp = pd.DataFrame({ "Seller": rivals["seller"], "ASIN": rivals["asin"].map(lambda a: _amazon_url(a, r["marketplace"])), "ASIN code": rivals["asin"], "Effective price": rivals["effective_price"].map(lambda v: f"${v:,.2f}"), "Coupon": rivals["coupon"].map(lambda v: f"${v:,.2f}" if v else "โ€”"), "Badge": rivals["badge"], "In stock": rivals["in_stock"].map({True: "Yes", False: "No"}), "Scraped": rivals["freshness"], }) st.dataframe( disp, hide_index=True, use_container_width=True, column_config={ "ASIN": st.column_config.LinkColumn( "ASIN", display_text=r"/dp/([A-Z0-9]+)", help="Open on Amazon"), "ASIN code": None, }, ) st.caption("Third-party sellers on the same ASIN. Your own offers are " "excluded. Prices are the effective (after-coupon) offer price.") elif sel == "ppc": p = d["ppc"] st.markdown("**Whole business โ€” from sales-insight (last 30 days)**") b1, b2, b3, b4 = st.columns(4) b1.metric("Total revenue/30d", dash(p.get("revenue_30d"), "${:,.0f}"), f"{p.get('units_30d', 0):,} units") b2.metric("Ad budget spent/30d", dash(p["spend_30d"], "${:,.0f}"), "marketing + promotion") b3.metric("Ad spend/day", dash(p["spend_day"], "${:,.0f}")) b4.metric("TACoS", dash(p["tacos"], "{:.1f}%"), "ad spend รท TOTAL revenue") if p.get("acos") is not None: st.markdown("**Advertising only โ€” from the Amazon Ads data COSMOS " "proxies (`marketing/dashboard`)**") a1, a2, a3, a4 = st.columns(4) a1.metric("ACoS", dash(p["acos"], "{:.1f}%"), "ad spend รท ad-attributed sales") a2.metric("Ad-attributed sales", dash(p["ad_sales_30d"], "${:,.0f}"), f"{p['ad_units_30d']:,} units" if p.get("ad_units_30d") else "") a3.metric("ROAS", dash(p["roas"], "{:.2f}ร—"), "sales per $1 of ad spend") a4.metric("Assigned budget", dash(p["daily_budget"], "${:,.0f}"), dash(p["budget_utilization"], "{:.0f}% used")) e1, e2, e3, e4 = st.columns(4) e1.metric("CPC", dash(p["cpc"], "${:.2f}")) e2.metric("Clicks", dash(p["clicks"], "{:,}")) e3.metric("Impressions", dash(p["impressions"], "{:,}")) e4.metric("Conversion", dash(p["conversion"], "{:.2f}%"), "orders รท clicks") if p.get("ad_share_pct") is not None: st.info(esc( f"๐Ÿ“ฃ Ads can claim **{p['ad_share_pct']:.0f}%** of units " f"({p['ad_units_30d']:,} of {p['units_30d']:,}); the rest is " f"organic. ACoS **{p['acos']:.1f}%** is the cost of the " f"attributed portion โ€” TACoS **{p['tacos']:.1f}%** spreads the " f"same spend over all revenue. Judge campaigns on ACoS; judge " f"the SKU's price on TACoS.")) else: st.info("No advertising campaigns found for this SKU in the last 30 " "days, so ACoS / ad-attributed sales are not applicable.") st.plotly_chart(ppc_chart(d), use_container_width=True, config={"displayModeBar": False}, key=f"ppc_{sku}") st.caption( "Two different denominators, both real: **TACoS** = total ad spend รท " "**total** revenue (from `sales-insight`, includes promotions); " "**ACoS** = ad spend รท **ad-attributed** sales (from the Ads data). " "The pricing model uses total ad cost per unit, because a price change " "moves all units โ€” not only the ad-attributed ones.") elif sel == "costs": cur_p, rec_p = d["current_price"], d["rec_price"] rp = d.get("referral_pct", REFERRAL_PCT) ret = d.get("returns_pct", 0.0) var = d.get("variable", VARIABLE) cost_rows = [ ("Selling price", cur_p, rec_p), (f"Referral fee ({rp * 100:.1f}%)", -cur_p * rp, -rec_p * rp), ("FBA fee", -d["cfg"]["fba"], -d["cfg"]["fba"]), ("Landed cost", -d["cfg"]["cost"], -d["cfg"]["cost"]), ] if ret: cost_rows.append((f"Returns reserve ({ret * 100:.1f}%)", -cur_p * ret, -rec_p * ret)) cost_rows += [ ("Other fees / variable", -var, -var), ("Take-home / unit", unit_take_home(cur_p, d), unit_take_home(rec_p, d)), ] cdf = pd.DataFrame(cost_rows, columns=["Line", "At current price", "At recommended"]) st.dataframe(cdf, hide_index=True, use_container_width=True, column_config={ "At current price": st.column_config.NumberColumn(format="$%.2f"), "At recommended": st.column_config.NumberColumn(format="$%.2f"), }) st.caption(esc(f"Break-even ${d['break_even']:.2f} ยท guardrail floor " f"${d['min_price']:.2f} ยท ceiling ${d['max_price']:.2f}")) if d["cfg"]["cost"] <= 0 and d["cfg"]["fba"] <= 0: st.warning("โš ๏ธ COSMOS returned **no cost data** for this SKU โ€” every line " "above that depends on COGS/FBA is wrong until the product's " "costs are filled in COSMOS/Inventory Planner.") elif sel == "track": # How wrong has this model been on THIS SKU? Previously buried at the # bottom of the last tab, under the narration it is meant to qualify. tr, bt = d.get("trust") or {}, d.get("backtest") if tr: icon = {"High": "๐ŸŸข", "Medium": "๐ŸŸก", "Low": "๐ŸŸ ", "None": "โ›”"}[tr["tier"]] st.markdown(f"### {icon} {tr['tier']} trust") st.markdown(esc(f"**What this permits:** {tr['permits']}")) if tr["reasons"]: st.markdown("**Why it isn't higher**") for why in tr["reasons"]: st.markdown(esc(f"- {why}")) st.divider() if bt: sc = bt["scores"] st.markdown(esc( f"**Backtest** โ€” fitted on older history, then asked to predict the " f"profit actually booked over {bt['n_folds']} held-out " f"{bt['holdout_days']}-day window(s), at the price really charged:")) names = {"anchored": "Anchored (no calibration)", "anchored_gap": "Anchored + per-unit gap", "persistence": "Assume last month repeats (baseline)", "legacy": "Previous engine (TACoS + factor)"} st.dataframe( pd.DataFrame([ {"Method": ("โ†’ " if m == bt["selected"] else "") + names[m], "Avg error $/30d": sc[m]["mae"], "Error % of actual": sc[m]["mape"], "Bias $/30d": sc[m]["bias"]} for m in ("anchored", "anchored_gap", "persistence", "legacy")]), hide_index=True, use_container_width=True, column_config={ "Avg error $/30d": st.column_config.NumberColumn(format="$%d"), "Error % of actual": st.column_config.NumberColumn(format="%.1f%%"), "Bias $/30d": st.column_config.NumberColumn(format="$%d"), }) gp = d.get("gap_per_unit") or 0.0 st.caption(esc( f"โ†’ marks the method actually used for this SKU's projections, " f"chosen by this table" + (f" (applying a **${gp:.2f}/unit** unmodeled cost)" if gp else "") + ". Lower is better. **Bias** shows the direction of the miss: " "positive means the model flatters this SKU. Beating *'assume last " "month repeats'* is the minimum bar โ€” a model that can't has added " "nothing, and drops this SKU to Low trust.")) with st.popover("Per-fold detail"): st.dataframe(pd.DataFrame([ {"Train days": f["train_days"], "Held-out days": f["test_days"], "Price charged": f["test_price"], "Actual $/30d": f["actual_net_30d"], "Selected": f["pred"][bt["selected"]], "Persistence": f["pred"]["persistence"], "Previous": f["pred"]["legacy"]} for f in bt["folds"]]), hide_index=True, use_container_width=True) else: st.info("Not enough history to hold any out โ€” this SKU's model is " "unscored, so recommendations stay human-approved.") elif sel == "why": st.markdown(esc(d["explanation"])) ev = d.get("evidence") or {} if any(v is not None for v in ev.values()): st.markdown("**Evidence from COSMOS actuals (6 months)**") lines = [] if ev.get("actual_profit_per_unit") is not None: lines.append(f"- Actual profit/unit (ads incl.): " f"**${ev['actual_profit_per_unit']:.2f}**") if ev.get("unprofitable_months"): lines.append(f"- Months that lost money: **{ev['unprofitable_months']}**") if ev.get("best_observed_price") is not None: lines.append(f"- Best observed price: **${ev['best_observed_price']:.2f}** " f"(${ev.get('best_observed_profit_day') or 0:,.0f}/day actual)") if ev.get("unmodeled_cost_gap") is not None: lines.append(f"- Model vs actual gap: **${ev['unmodeled_cost_gap']:.2f}/unit**") if ev.get("suggested_price") is not None: lines.append(f"- Margin-floor suggested price: " f"**${ev['suggested_price']:.2f}**") if ev.get("trend"): lines.append(f"- Sales trend: **{ev['trend']}**") st.markdown(esc("\n".join(lines))) st.markdown("**Root-cause checks**") for check, result in d["root_cause"]: ok = result.startswith(("confirmed", "positive", "detected")) icon = "โœ…" if ok else ("โš ๏ธ" if ("missing" in result or "not feasible" in result) else "โ–ซ๏ธ") st.markdown(esc(f"{icon} {check} โ€” *{result}*")) st.markdown("**Risks**") for risk, mit in d["risks"]: st.markdown(f"- {risk} โ€” *{mit}*") st.caption("Every number here comes from the deterministic engine over " "COSMOS data โ€” see **๐ŸŽฏ Track record** for how accurate it has " "been on this SKU.") st.divider() st.caption("Utopia Pricing Agent ยท live COSMOS data ยท read-only (no prices are " "written back) ยท legacy analyst UI: `streamlit run legacy_app.py`")