Files
fantabeto/dashboard/pages/03_auction.py
T
ramseshk 397078d635 Fix dashboard imports for Streamlit multi-page routing
- Add sys.path.insert at top of app.py and all 5 page files
- Fix .streamlit/config.toml CORS/XSRF conflict
- Remove duplicate st.set_page_config from page files
- Verified: all pages import cleanly, dashboard boots via streamlit run
- All 49 tests passing (31 original + 18 dashboard)
2026-08-11 15:08:09 +08:00

196 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
Budget waterfall, value scatter, grid-auction heatmap, budget slider simulator.
"""
import numpy as np
import pandas as pd
import streamlit as st
import plotly.graph_objects as go
from dashboard.warehouse import load_players, load_predictions
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.template import (
PITCH_GREEN, GOLD, RED, SKY, BG, CARD_BG, BORDER, TEXT_SECONDARY,
WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS,
)
@st.cache_data(ttl=3600)
def _get_data():
players = load_players()
preds = load_predictions()
return players, preds
def _compute_auction(players, budget, gk, df, mf, fw):
"""Greedy knapsack auction solver."""
quotas = {"P": gk, "D": df, "C": mf, "A": fw}
filled = {"P": 0, "D": 0, "C": 0, "A": 0}
remaining = budget
df = players.copy()
df["value_ratio"] = df["fv_avg"] / df["qi"].clip(lower=1)
df["estimated_price"] = df["qi"] * np.clip(np.random.RandomState(42).normal(2.5, 0.8, len(df)), 0.8, 6)
scored = []
for _, p in df.iterrows():
role = p["role"]
if role not in quotas:
continue
scored.append((p["value_ratio"] * p["fv_avg"], p))
scored.sort(key=lambda x: -x[0])
selected = []
for _, p in scored:
role = p["role"]
if filled[role] >= quotas[role]:
continue
price = p["estimated_price"]
if price > remaining:
continue
selected.append({
"player": p["player"], "role": role, "team": p["team"],
"fv_avg": p["fv_avg"], "qi": p["qi"],
"estimated_price": price,
"games_season": p.get("games_season", 30),
})
remaining -= price
filled[role] += 1
total = budget - remaining
total_fv = sum(s["fv_avg"] for s in selected)
return selected, total, total_fv, remaining
def _grid_heatmap(players):
"""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 = []
labels = []
for _, p in top.iterrows():
row = []
for mult in bid_multipliers:
bid = p["qi"] * mult
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(
z=matrix,
x=[f"{m}x QI" for m in bid_multipliers],
y=labels,
colorscale=HEATMAP_COLORS,
hovertemplate="%{y}<br>Bid: %{x}<br>Surplus: %{z:.0f}<extra></extra>",
))
fig.update_layout(
template=FANTABETO_TEMPLATE, height=350,
xaxis=dict(side="top"),
yaxis=dict(autorange="reversed"),
)
return fig
def run():
inject_css()
players, preds = _get_data()
st.markdown("## 💰 Auction War Room")
st.caption("Project Al-Cihred — Draft Strategy for 2026/27 Classic Auction")
# ── Budget controls ──
c_budget, c_gk, c_def, c_mid, c_fwd = st.columns(5)
with c_budget:
budget = st.slider("Budget (cr)", 300, 700, 500, 10)
with c_gk:
n_gk = st.number_input("GK", 1, 5, 3)
with c_def:
n_def = st.number_input("DEF", 3, 12, 8)
with c_mid:
n_mid = st.number_input("MID", 3, 12, 8)
with c_fwd:
n_fwd = st.number_input("FWD", 1, 8, 6)
selected, total_cost, total_fv, remaining = _compute_auction(
players, budget, n_gk, n_def, n_mid, n_fwd
)
# ── KPI Row ──
k1, k2, k3, k4 = st.columns(4)
with k1:
st.markdown(kpi_card("PLAYERS DRAFTED", str(len(selected)),
f"{n_gk+n_def+n_mid+n_fwd} target", SKY),
unsafe_allow_html=True)
with k2:
st.markdown(kpi_card("TOTAL SPENT", f"{total_cost:.0f} cr",
f"{remaining:.0f} cr remaining", PITCH_GREEN),
unsafe_allow_html=True)
with k3:
st.markdown(kpi_card("PROJECTED FV", f"{total_fv:.1f}",
f"{total_fv / max(total_cost, 1):.2f} cr/FV", GOLD),
unsafe_allow_html=True)
with k4:
st.markdown(kpi_card("AVG PRICE", f"{total_cost / max(len(selected), 1):.0f} cr",
"per player", SKY),
unsafe_allow_html=True)
st.divider()
# ── Budget Waterfall + Value Scatter ──
c1, c2 = st.columns([2, 3])
with c1:
section("💧 Budget Allocation")
allocations = {}
for r in ["P", "D", "C", "A"]:
allocations[r] = sum(s["estimated_price"] for s in selected if s["role"] == r)
fig = budget_waterfall(allocations)
st.plotly_chart(fig, use_container_width=True)
insight("How your budget maps across roles. Aim for ~15% GK, ~35% DEF, ~30% MID, ~20% FWD.")
with c2:
section("📈 Value Scatter")
fig = value_scatter(players)
st.plotly_chart(fig, use_container_width=True)
insight("Top-right: high FV, high price. Bottom-right: value steals. "
"Bubble size = games played. Dashed lines = cost-per-FV-point isolines.")
st.divider()
# ── Target Squad ──
section("🎯 Recommended Squad")
if selected:
squad_df = pd.DataFrame(selected)
for role in ["P", "D", "C", "A"]:
rdf = squad_df[squad_df["role"] == role]
if rdf.empty:
continue
role_name = {"P": "Goalkeepers", "D": "Defenders", "C": "Midfielders", "A": "Forwards"}[role]
st.markdown(f"**{role_name}** {role_chip(role)}")
for _, p in rdf.iterrows():
st.markdown(
f"- **{p['player']}** ({p['team']}) — "
f"FV: {p['fv_avg']:.2f} | "
f"Max bid: {p['estimated_price']:.0f} cr | "
f"Games: {p['games_season']:.0f}",
)
st.divider()
# ── Grid Auction Heatmap ──
section("🔢 Grid Auction Simulator")
fig = _grid_heatmap(players)
st.plotly_chart(fig, use_container_width=True)
insight("Green = good value at that bid multiplier. Red = overpaying. "
"Bid at the 'green' multiplier for each player.")
if __name__ == "__main__":
run()