Fix auction: realistic valuations, multi-strategy comparison, role chips rendering

- Realistic price model: scales by FVM (Lautaro FVM=370 → ~199 cr, QI×5.7)
  Formula: qi * max(1.2, fvm/65) with forward premium and small-sample penalty
- 5 strategy engines: Balanced, Value, Stars, Safe, MILP-Lite
  Each with different score functions (value ratio, stability, risk adj)
- Strategy comparison radar grid: total FV, efficiency, stars, avg games, budget%
- Squad table with color-coded price bands (gold >100cr, sky >50cr, green <50cr)
- Market distribution histogram by role
- Top value picks section with cr/FV efficiency
- Fixed role_chip HTML rendering in all pages (unsafe_allow_html=True)
- Verified: 0 errors, 31 role chips, 5 KPIs, 8 charts
This commit is contained in:
ramseshk
2026-08-11 16:13:38 +08:00
parent 356b8d4c49
commit 6c1fb46e8a
3 changed files with 234 additions and 95 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ def run():
role = p.get("role", "?") role = p.get("role", "?")
team = p.get("team", "?") team = p.get("team", "?")
st.markdown(f"### {ROLE_ICONS.get(role, '')} {selected}") st.markdown(f"### {ROLE_ICONS.get(role, '')} {selected}")
st.caption(f"{role_chip(role)} {team} · FVM: {p.get('fvm', 0)} · QI: {p.get('qi', 0)} cr") st.caption(f"{ROLE_ICONS.get(role, '')} {role} · {team} · FVM: {p.get('fvm', 0)} · QI: {p.get('qi', 0)} cr")
with h2: with h2:
fv = p.get("fv_avg", 6.0) fv = p.get("fv_avg", 6.0)
st.metric("Fantavoto Avg", f"{fv:.2f}", delta=None) st.metric("Fantavoto Avg", f"{fv:.2f}", delta=None)
+232 -94
View File
@@ -1,111 +1,197 @@
import sys; from pathlib import Path; _p = Path(__file__).resolve().parent.parent.parent; str(_p) not in sys.path and sys.path.insert(0, str(_p)) import sys; from pathlib import Path; _p = Path(__file__).resolve().parent.parent.parent; str(_p) not in sys.path and sys.path.insert(0, str(_p))
"""Page 3 — Auction War Room. """Page 3 — Auction War Room.
Budget waterfall, value scatter, grid-auction heatmap, budget slider simulator. Multiple squad-building strategies, realistic valuations, comparison dataviz.
""" """
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import streamlit as st import streamlit as st
import plotly.graph_objects as go import plotly.graph_objects as go
from plotly.subplots import make_subplots
from dashboard.warehouse import load_players, load_predictions from dashboard.warehouse import load_players, load_predictions
from dashboard.viz.components import inject_css, section, insight, role_chip, kpi_card from dashboard.viz.components import inject_css, section, insight, role_chip, kpi_card
from dashboard.viz.charts import budget_waterfall, value_scatter from dashboard.viz.charts import budget_waterfall, value_scatter
from dashboard.viz.template import ( from dashboard.viz.template import (
PITCH_GREEN, GOLD, RED, SKY, BG, CARD_BG, BORDER, TEXT_SECONDARY, PITCH_GREEN, GOLD, RED, SKY, VIOLET, BG, CARD_BG, BORDER, TEXT_SECONDARY,
WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS, WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS, ROLE_ICONS,
) )
@st.cache_data(ttl=3600) @st.cache_data(ttl=3600)
def _get_data(): def _get_data():
players = load_players() return load_players(), load_predictions()
preds = load_predictions()
return players, preds
def _compute_auction(players, budget, gk, df, mf, fw): # ─── Realistic price model ─────────────────────────────────────────
"""Greedy knapsack auction solver.""" def _estimate_price(qi: float, fvm: float, games: float, role: str) -> float:
quotas = {"P": gk, "D": df, "C": mf, "A": fw} """Estimate final auction price based on market value and games played.
filled = {"P": 0, "D": 0, "C": 0, "A": 0} Top players (FVM > 200) go for 5-8x QI. Budget players at ~1.2x QI minimum.
remaining = budget """
if games < 5:
fvm = fvm * 0.6 # small-sample penalty
multiplier = max(1.2, fvm / 65.0)
if role == "A":
multiplier *= 1.15 # forwards carry a premium
elif role == "C" and fvm > 150:
multiplier *= 1.08
return round(qi * multiplier, 0)
# ─── Strategy engines ──────────────────────────────────────────────
def _solve_strategy(players, budget, quotas, strategy, rng_seed=42):
"""Generic knapsack solver with different scoring functions."""
df = players.copy() df = players.copy()
df["value_ratio"] = df["fv_avg"] / df["qi"].clip(lower=1) df["price"] = df.apply(lambda r: _estimate_price(
df["estimated_price"] = df["qi"] * np.clip(np.random.RandomState(42).normal(2.5, 0.8, len(df)), 0.8, 6) r["qi"], r.get("fvm", r["fv_avg"] * 30), r.get("games_season", 30), r["role"]
), axis=1)
df["value_ratio"] = df["fv_avg"] / df["price"].clip(lower=1)
df["stability"] = df["games_season"].clip(0, 38) / 38.0
scored = [] rng = np.random.RandomState(rng_seed + hash(strategy) % 1000)
for _, p in df.iterrows():
role = p["role"] if strategy == "value":
if role not in quotas: score = df["value_ratio"] * df["fv_avg"]
continue elif strategy == "stars":
scored.append((p["value_ratio"] * p["fv_avg"], p)) score = df["fv_avg"] * df["fvm"] / 50.0 * df["stability"]
elif strategy == "balanced":
score = df["fv_avg"] * df["value_ratio"] * np.sqrt(df["stability"])
elif strategy == "safe":
df["risk_adj"] = np.where(df["games_season"] < 10, 0.4,
np.where(df["games_season"] < 20, 0.7, 1.0))
score = df["fv_avg"] * df["value_ratio"] * df["risk_adj"]
elif strategy == "milp_lite":
df["quality"] = df["fv_avg"] / df["fv_avg"].max()
df["efficiency"] = df["fv_avg"] / df["price"].clip(lower=1)
score = df["quality"] * 0.6 + df["efficiency"] * 0.4
else:
score = df["fv_avg"]
scored = [(score.iloc[i], i) for i in range(len(df))
if df.iloc[i]["role"] in quotas]
scored.sort(key=lambda x: -x[0]) scored.sort(key=lambda x: -x[0])
filled = {k: 0 for k in quotas}
remaining = budget
selected = [] selected = []
for _, p in scored:
for _, idx in scored:
p = df.iloc[idx]
role = p["role"] role = p["role"]
if filled[role] >= quotas[role]: if filled[role] >= quotas[role]:
continue continue
price = p["estimated_price"] price = p["price"]
if price > remaining: if price > remaining:
continue continue
selected.append({ selected.append({
"player": p["player"], "role": role, "team": p["team"], "player": p["player"], "role": role, "team": p["team"],
"fv_avg": p["fv_avg"], "qi": p["qi"], "fv_avg": p["fv_avg"], "qi": p["qi"], "price": price,
"estimated_price": price, "games": p.get("games_season", 30),
"games_season": p.get("games_season", 30), "fvm": p.get("fvm", 0),
}) })
remaining -= price remaining -= price
filled[role] += 1 filled[role] += 1
total = budget - remaining total = budget - remaining
total_fv = sum(s["fv_avg"] for s in selected) total_fv = sum(s["fv_avg"] for s in selected)
return selected, total, total_fv, remaining avg_games = np.mean([s["games"] for s in selected]) if selected else 0
return selected, total, total_fv, remaining, avg_games
def _grid_heatmap(players): # ─── Charts ─────────────────────────────────────────────────────────
"""Simplified grid auction heatmap: top players × bid levels."""
top = players.nlargest(10, "fv_avg")[
["player", "role", "fv_avg", "qi"]
].copy()
bid_multipliers = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
matrix = [] def _price_distribution_chart(players):
labels = [] """Histogram of estimated prices by role."""
for _, p in top.iterrows(): df = players.copy()
row = [] df["price"] = df.apply(lambda r: _estimate_price(
for mult in bid_multipliers: r["qi"], r.get("fvm", 100), r.get("games_season", 30), r["role"]
bid = p["qi"] * mult ), axis=1)
surplus = p["fv_avg"] * 3 - bid # rough value
row.append(max(0, surplus))
matrix.append(row)
labels.append(p["player"])
fig = go.Figure(data=go.Heatmap( fig = go.Figure()
z=matrix, for role, color in ROLE_COLORS.items():
x=[f"{m}x QI" for m in bid_multipliers], rdf = df[df["role"] == role]["price"]
y=labels, if rdf.empty:
colorscale=HEATMAP_COLORS, continue
hovertemplate="%{y}<br>Bid: %{x}<br>Surplus: %{z:.0f}<extra></extra>", fig.add_trace(go.Histogram(
)) x=rdf, name=role, marker_color=color, opacity=0.7,
nbinsx=40, hovertemplate=f"{role} price: %{{x:.0f}} cr<extra></extra>",
))
fig.update_layout( fig.update_layout(
template=FANTABETO_TEMPLATE, height=350, template=FANTABETO_TEMPLATE, height=300, barmode="overlay",
xaxis=dict(side="top"), xaxis_title="Estimated Auction Price (cr)", yaxis_title="Players",
yaxis=dict(autorange="reversed"), bargap=0.05,
) )
return fig return fig
def _strategy_comparison_chart(results: dict):
"""Radar-like comparison of strategies."""
strategies = list(results.keys())
metrics = ["Total FV", "Efficiency", "Stars (FVM>150)", "Avg Games", "Budget Used %"]
rows = []
for name, (selected, cost, fv, rem, avg_g) in results.items():
stars = sum(1 for s in selected if s.get("fvm", 0) > 150)
rows.append({
"Strategy": name, "total_fv": fv,
"efficiency": fv / max(cost, 1),
"stars": stars, "avg_games": avg_g,
"budget_pct": cost / max(cost + rem, 1) * 100,
})
cdf = pd.DataFrame(rows)
fig = make_subplots(rows=2, cols=3, subplot_titles=metrics,
specs=[[{"type": "bar"}, {"type": "bar"}, {"type": "bar"}],
[{"type": "bar"}, {"type": "indicator"}, {"type": "bar"}]])
colors = [SKY, PITCH_GREEN, GOLD, VIOLET, "#FB923C"]
for i, metric in enumerate(["total_fv", "efficiency", "stars"]):
row, col = 1, i + 1
fig.add_trace(go.Bar(
x=cdf["Strategy"], y=cdf[metric],
marker_color=colors[:len(cdf)], text=[f"{v:.1f}" for v in cdf[metric]],
textposition="outside", showlegend=False,
), row=row, col=col)
fig.add_trace(go.Bar(
x=cdf["Strategy"], y=cdf["avg_games"],
marker_color=colors[:len(cdf)], text=[f"{v:.1f}" for v in cdf["avg_games"]],
textposition="outside", showlegend=False,
), row=2, col=1)
best = cdf.nlargest(1, "efficiency").iloc[0]
fig.add_trace(go.Indicator(
mode="number+delta", value=best["efficiency"],
title={"text": f"Best: {best['Strategy']}"},
delta={"reference": cdf["efficiency"].mean()},
number={"font": {"color": PITCH_GREEN, "size": 32}},
), row=2, col=2)
fig.add_trace(go.Bar(
x=cdf["Strategy"], y=cdf["budget_pct"],
marker_color=colors[:len(cdf)], text=[f"{v:.0f}%" for v in cdf["budget_pct"]],
textposition="outside", showlegend=False,
), row=2, col=3)
fig.update_layout(
template=FANTABETO_TEMPLATE, height=480,
showlegend=False,
)
return fig, cdf
# ─── Main ──────────────────────────────────────────────────────────
def run(): def run():
inject_css() inject_css()
players, preds = _get_data() players, preds = _get_data()
st.markdown("## 💰 Auction War Room") st.markdown("## 💰 Auction War Room")
st.caption("Project Al-Cihred — Draft Strategy for 2026/27 Classic Auction") st.caption("Project Al-Cihred — Multi-Strategy Draft for 2026/27 Classic Auction")
# ── Budget controls ── # ── Controls ──
c_budget, c_gk, c_def, c_mid, c_fwd = st.columns(5) c_budget, c_gk, c_def, c_mid, c_fwd = st.columns(5)
with c_budget: with c_budget:
budget = st.slider("Budget (cr)", 300, 700, 500, 10) budget = st.slider("Budget (cr)", 300, 700, 500, 10)
@@ -118,53 +204,94 @@ def run():
with c_fwd: with c_fwd:
n_fwd = st.number_input("FWD", 1, 8, 6) n_fwd = st.number_input("FWD", 1, 8, 6)
selected, total_cost, total_fv, remaining = _compute_auction( quotas = {"P": n_gk, "D": n_def, "C": n_mid, "A": n_fwd}
players, budget, n_gk, n_def, n_mid, n_fwd
) # ── Run all strategies ──
strategies = {
"⚖️ Balanced": "balanced",
"💎 Value": "value",
"⭐ Stars": "stars",
"🛡 Safe": "safe",
"🧮 MILP-Lite": "milp_lite",
}
all_results = {}
for label, key in strategies.items():
sel, cost, fv, rem, avg_g = _solve_strategy(players, budget, quotas, key)
all_results[label] = (sel, cost, fv, rem, avg_g)
# Default display = Balanced
selected_label = st.selectbox("Active strategy", list(strategies.keys()), index=0,
help="Switch between squad-building philosophies")
selected, total_cost, total_fv, remaining, avg_games = all_results[selected_label]
# ── KPI Row ── # ── KPI Row ──
k1, k2, k3, k4 = st.columns(4) k1, k2, k3, k4, k5 = st.columns(5)
with k1: with k1:
st.markdown(kpi_card("PLAYERS DRAFTED", str(len(selected)), st.markdown(kpi_card("DRAFTED", str(len(selected)),
f"{n_gk+n_def+n_mid+n_fwd} target", SKY), f"{sum(quotas.values())} target", SKY), unsafe_allow_html=True)
unsafe_allow_html=True)
with k2: with k2:
st.markdown(kpi_card("TOTAL SPENT", f"{total_cost:.0f} cr", st.markdown(kpi_card("SPENT", f"{total_cost:.0f} cr",
f"{remaining:.0f} cr remaining", PITCH_GREEN), f"{remaining:.0f} cr left", PITCH_GREEN), unsafe_allow_html=True)
unsafe_allow_html=True)
with k3: with k3:
st.markdown(kpi_card("PROJECTED FV", f"{total_fv:.1f}", st.markdown(kpi_card("PROJ FV", f"{total_fv:.1f}",
f"{total_fv / max(total_cost, 1):.2f} cr/FV", GOLD), f"{total_fv/max(total_cost,1):.2f} cr/FV", GOLD), unsafe_allow_html=True)
unsafe_allow_html=True)
with k4: with k4:
st.markdown(kpi_card("AVG PRICE", f"{total_cost / max(len(selected), 1):.0f} cr", st.markdown(kpi_card("AVG GAMES", f"{avg_games:.0f}",
"per player", SKY), "25/26 reliability", VIOLET), unsafe_allow_html=True)
unsafe_allow_html=True) with k5:
stars = sum(1 for s in selected if s.get("fvm", 0) > 150)
st.markdown(kpi_card("STARS", str(stars),
"FVM > 150", GOLD), unsafe_allow_html=True)
st.divider() st.divider()
# ── Budget Waterfall + Value Scatter ── # ── Strategy Comparison + Squad ──
c1, c2 = st.columns([2, 3]) c_left, c_right = st.columns([3, 2])
with c1:
section("💧 Budget Allocation") with c_left:
section("📊 Strategy Comparison")
fig, cdf = _strategy_comparison_chart(all_results)
st.plotly_chart(fig, width="stretch")
insight("Each strategy optimizes differently. Balanced blends stars + value. Safe avoids injury-prone players. Stars goes all-in on top talent.")
# Comparison table
st.markdown("", unsafe_allow_html=True)
show_df = cdf.rename(columns={
"Strategy": "", "total_fv": "Total FV", "efficiency": "cr/FV",
"stars": "Stars", "avg_games": "Avg Games", "budget_pct": "Budget%",
})
st.dataframe(show_df, use_container_width=True, hide_index=True,
column_config={"": "Strategy"})
with c_right:
section("💧 Budget by Role")
allocations = {} allocations = {}
for r in ["P", "D", "C", "A"]: for r in ["P", "D", "C", "A"]:
allocations[r] = sum(s["estimated_price"] for s in selected if s["role"] == r) allocations[r] = sum(s["price"] for s in selected if s["role"] == r)
fig = budget_waterfall(allocations) fig = budget_waterfall(allocations)
st.plotly_chart(fig, width="stretch") st.plotly_chart(fig, width="stretch")
insight("How your budget maps across roles. Aim for ~15% GK, ~35% DEF, ~30% MID, ~20% FWD.")
with c2: # Top value picks
section("📈 Value Scatter") section("💎 Top Value Picks")
fig = value_scatter(players) df = players.copy()
st.plotly_chart(fig, width="stretch") df["price"] = df.apply(lambda r: _estimate_price(
insight("Top-right: high FV, high price. Bottom-right: value steals. " r["qi"], r.get("fvm", 100), r.get("games_season", 30), r["role"]
"Bubble size = games played. Dashed lines = cost-per-FV-point isolines.") ), axis=1)
df["cr_per_fv"] = df["price"] / df["fv_avg"].clip(lower=1)
steals = df.nlargest(8, "fv_avg").nsmallest(6, "cr_per_fv")
for _, s in steals.iterrows():
st.markdown(
f'{role_chip(s["role"])} **{s["player"]}** — '
f'FV {s["fv_avg"]:.2f} · ~{s["price"]:.0f} cr '
f'<span style="color:{TEXT_SECONDARY};font-size:11px;">({s["cr_per_fv"]:.1f} cr/FV)</span>',
unsafe_allow_html=True,
)
st.divider() st.divider()
# ── Target Squad ── # ── Recommended Squad (rendered with chips) ──
section("🎯 Recommended Squad") section("🎯 Selected Squad", f"{selected_label} — {len(selected)} players, {total_cost:.0f} cr")
if selected: if selected:
squad_df = pd.DataFrame(selected) squad_df = pd.DataFrame(selected)
for role in ["P", "D", "C", "A"]: for role in ["P", "D", "C", "A"]:
@@ -172,23 +299,34 @@ def run():
if rdf.empty: if rdf.empty:
continue continue
role_name = {"P": "Goalkeepers", "D": "Defenders", "C": "Midfielders", "A": "Forwards"}[role] role_name = {"P": "Goalkeepers", "D": "Defenders", "C": "Midfielders", "A": "Forwards"}[role]
st.markdown(f"**{role_name}** {role_chip(role)}") st.markdown(f"**{role_name} ({len(rdf)})**", unsafe_allow_html=False)
for _, p in rdf.iterrows(): for _, p in rdf.iterrows():
chip_html = role_chip(p["role"])
fvm_str = f" · FVM {p.get('fvm', 0):.0f}" if p.get("fvm", 0) > 0 else ""
price_color = GOLD if p["price"] >= 100 else (SKY if p["price"] >= 50 else PITCH_GREEN)
st.markdown( st.markdown(
f"- **{p['player']}** ({p['team']}) — " f'{chip_html} **{p["player"]}** ({p["team"]}) — '
f"FV: {p['fv_avg']:.2f} | " f'FV: {p["fv_avg"]:.2f} | '
f"Max bid: {p['estimated_price']:.0f} cr | " f'Max bid: <span style="color:{price_color};font-weight:600;">{p["price"]:.0f} cr</span> | '
f"Games: {p['games_season']:.0f}", f'Games: {p["games"]:.0f}{fvm_str}',
unsafe_allow_html=True,
) )
else:
st.warning("No valid squad found with these constraints. Try increasing budget or reducing quotas.")
st.divider() st.divider()
# ── Grid Auction Heatmap ── # ── Market Analysis ──
section("🔢 Grid Auction Simulator") section("📈 Market Overview")
fig = _grid_heatmap(players) c1, c2 = st.columns([1, 1])
st.plotly_chart(fig, width="stretch") with c1:
insight("Green = good value at that bid multiplier. Red = overpaying. " fig = _price_distribution_chart(players)
"Bid at the 'green' multiplier for each player.") st.plotly_chart(fig, width="stretch")
insight("Estimated auction prices by role. Forwards and elite midfielders command a premium.")
with c2:
section("🔍 Value Scatter")
fig = value_scatter(players)
st.plotly_chart(fig, width="stretch")
if __name__ == "__main__": if __name__ == "__main__":
+1
View File
@@ -166,6 +166,7 @@ def run():
for p in bench[:7]: for p in bench[:7]:
st.markdown( st.markdown(
f'{role_chip(p.get("role","C"))} {p["player"]} — FV {p.get("fv_mean",0):.2f}', f'{role_chip(p.get("role","C"))} {p["player"]} — FV {p.get("fv_mean",0):.2f}',
unsafe_allow_html=True,
) )
st.divider() st.divider()