Files
fantabeto/dashboard/pages/03_auction.py
T
ramseshk 8e759b0d0f Project Al-Cihred: ML auction strategy with exact 500cr spend
- LightGBM trained on 11,300 per-matchday votes (R²=0.147, RMSE=1.19)
- MILP with exact 499-500cr budget: 3GK+8DEF+8MID+6FWD
- Practical constraints: 2 starting GKs, 21/25 reliable (>15g)
- Realistic prices: Lautaro 229cr, Malen 220cr, Douvikas 56cr
- Squad: Svilar(21)+Carnesecchi(18)+Christensen(1)=40GK · 8DEF 128cr · 8MID 152cr · 6FWD 180cr
- Every player has 2-3 alternatives
- Updated dashboard auction page to load the plan
2026-08-11 16:20:45 +08:00

235 lines
9.2 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.
Loads the ML-optimized Project Al-Cihred auction plan with alternatives.
"""
import numpy as np
import pandas as pd
import streamlit as st
import plotly.graph_objects as go
from plotly.subplots import make_subplots
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, VIOLET, BG, CARD_BG, BORDER, TEXT_SECONDARY,
WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS, ROLE_ICONS,
)
AUCTION_PLAN = Path(__file__).resolve().parent.parent.parent / "data" / "auction_plan_al_cihred.xlsx"
PROJECTIONS = Path(__file__).resolve().parent.parent.parent / "data" / "player_projections_26_27.xlsx"
@st.cache_data(ttl=3600)
def _get_data():
players = load_players()
preds = load_predictions()
# Load the precomputed auction plan
plan = None
if AUCTION_PLAN.exists():
plan = pd.read_excel(AUCTION_PLAN)
proj = None
if PROJECTIONS.exists():
proj = pd.read_excel(PROJECTIONS)
return players, preds, plan, proj
def _price_dist_chart(proj):
if proj is None:
return go.Figure()
df = proj.copy()
df["price"] = df["price"].fillna(0)
fig = go.Figure()
for role, color in ROLE_COLORS.items():
rdf = df[df["role"] == role]["price"]
if rdf.empty:
continue
fig.add_trace(go.Histogram(
x=rdf, name=role, marker_color=color, opacity=0.7, nbinsx=50,
hovertemplate=f"{role} %{{x:.0f}} cr<extra></extra>",
))
fig.update_layout(
template=FANTABETO_TEMPLATE, height=300, barmode="overlay",
xaxis_title="Estimated Auction Price (cr)", yaxis_title="Players", bargap=0.05,
)
return fig
def _role_radar(squad):
"""Radar showing squad balance: avg FV, games, stability per role."""
roles = ["P", "D", "C", "A"]
avg_fv, avg_g, avg_s = [], [], []
for r in roles:
rs = [s for s in squad if s.get("role") == r]
avg_fv.append(np.mean([s.get("fv_proj", 0) for s in rs]) if rs else 0)
avg_g.append(np.mean([s.get("games", 0) for s in rs]) if rs else 0)
avg_s.append(np.mean([s.get("stability", 0) for s in rs]) if rs else 0)
fig = go.Figure()
fig.add_trace(go.Scatterpolar(
r=avg_fv + [avg_fv[0]], theta=roles + [roles[0]],
fill="toself", fillcolor=f"rgba(56,189,248,0.2)",
line=dict(color=SKY, width=2), name="Avg FV",
))
fig.add_trace(go.Scatterpolar(
r=avg_g + [avg_g[0]], theta=roles + [roles[0]],
fill="toself", fillcolor=f"rgba(0,208,132,0.15)",
line=dict(color=PITCH_GREEN, width=2), name="Avg Games",
))
fig.update_layout(
template=FANTABETO_TEMPLATE, height=280,
polar=dict(
radialaxis=dict(showticklabels=False, gridcolor=GRIDLINE),
angularaxis=dict(gridcolor=GRIDLINE),
bgcolor=BG,
),
showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=-0.2),
margin=dict(l=30, r=30, t=30, b=60),
)
return fig
def run():
inject_css()
players, preds, plan, proj = _get_data()
st.markdown("## 💰 Auction War Room")
st.caption("Project Al-Cihred — ML-Optimized Auction Strategy for 2026/27")
# Load plan data into squad list
squad = []
total_cost = 0
total_fv = 0
if plan is not None and len(plan) > 0:
for _, p in plan.iterrows():
s = {
"player": p.get("player", ""), "role": p.get("role", ""),
"team": p.get("team", ""), "price": p.get("bid_cap", 0),
"qi": p.get("qi", 0), "fvm": p.get("fvm", 0),
"fv_proj": p.get("fv_proj", 0), "mv_proj": p.get("mv_proj", 0),
"goals": p.get("goals", 0), "assists": p.get("assists", 0),
"games": p.get("games", 0), "starter_pct": p.get("starter%", 0),
"stability": p.get("stability", 0),
}
s["alts"] = []
for j in range(1, 3):
if f"alt{j}" in plan.columns and pd.notna(p.get(f"alt{j}")):
s["alts"].append({
"player": p.get(f"alt{j}"), "price": p.get(f"alt{j}_price"),
"fv": p.get(f"alt{j}_fv"),
})
squad.append(s)
total_cost += s["price"]
total_fv += s["fv_proj"]
else:
st.warning("⚠️ No auction plan found. Run `python -c '...'` to generate one.")
return
# ── KPI Row ──
k1, k2, k3, k4, k5 = st.columns(5)
with k1:
st.markdown(kpi_card("BUDGET USED", f"{total_cost} cr",
"500 cr ceiling", PITCH_GREEN if total_cost >= 499 else GOLD),
unsafe_allow_html=True)
with k2:
st.markdown(kpi_card("PROJ FV", f"{total_fv:.1f}",
f"{total_fv/total_cost:.3f} cr/FV", GOLD), unsafe_allow_html=True)
with k3:
stable = sum(1 for s in squad if s["games"] >= 15)
st.markdown(kpi_card("RELIABLE", f"{stable}/25",
"players >15 games", PITCH_GREEN), unsafe_allow_html=True)
with k4:
avg_g = np.mean([s["games"] for s in squad])
st.markdown(kpi_card("AVG GAMES", f"{avg_g:.0f}",
"25/26 experience", SKY), unsafe_allow_html=True)
with k5:
stars = sum(1 for s in squad if s["fvm"] > 100)
st.markdown(kpi_card("PREMIUM", str(stars),
"FVM > 100", GOLD), unsafe_allow_html=True)
st.divider()
# ── Squad Table + Radar ──
c1, c2 = st.columns([3, 2])
with c1:
section("🎯 Project Al-Cihred — Final Squad")
role_groups = {"P": "🧤 Goalkeepers", "D": "🛡 Defenders",
"C": "⚙ Midfielders", "A": "⚡ Forwards"}
for role, label in role_groups.items():
rs = [s for s in squad if s["role"] == role]
if not rs:
continue
cost = sum(s["price"] for s in rs)
st.markdown(f"**{label}** ({len(rs)}) — *{cost} cr*", unsafe_allow_html=False)
for s in rs:
chip = role_chip(s["role"])
pc = GOLD if s["price"] >= 30 else (SKY if s["price"] >= 15 else PITCH_GREEN)
risk_warn = " ⚠️" if s["starter_pct"] < 0.5 else ""
st.markdown(
f'{chip} **{s["player"]}** ({s["team"]}) — '
f'FV: {s["fv_proj"]:.2f} · '
f'<span style="color:{pc};font-weight:600;">{s["price"]:.0f} cr</span> · '
f'{int(s["goals"])}G {int(s["assists"])}A · {int(s["games"])}gms '
f'· FVM {int(s["fvm"])}{risk_warn}',
unsafe_allow_html=True,
)
if s["alts"]:
alt_text = " · ".join(
f'<span style="color:{TEXT_SECONDARY};font-size:11px;">⟳ [{a["player"]} · {int(a["price"])}cr]</span>'
for a in s["alts"]
)
st.markdown(f'<span style="margin-left:22px;">{alt_text}</span>',
unsafe_allow_html=True)
with c2:
section("📊 Squad Balance Radar")
fig = _role_radar(squad)
st.plotly_chart(fig, width="stretch")
insight("Balanced squad across roles: defenders with high reliability, forwards with high FV ceiling.")
# Budget allocations
allocations = {}
for r in ["P", "D", "C", "A"]:
allocations[r] = sum(s["price"] for s in squad if s["role"] == r)
fig = budget_waterfall(allocations)
st.plotly_chart(fig, width="stretch")
st.divider()
# ── Market Analysis ──
section("📈 Market Overview")
c3, c4 = st.columns([1, 1])
with c3:
fig = _price_dist_chart(proj)
st.plotly_chart(fig, width="stretch")
insight("Price distribution by role. Forwards and elite midfielders at premium.")
with c4:
section("🔍 Value Scatter")
fig = value_scatter(players)
st.plotly_chart(fig, width="stretch")
# ── Methodology ──
st.divider()
with st.expander("⚙️ Methodology — How this squad was computed"):
st.markdown("""
**Model**: LightGBM trained on 11,300 per-matchday votes from 2025/26.
**Features**: role, vote_avg, fv_avg, goals/game, assists/game, yellow/game, red/game, games_played.
**Solver**: MILP (Mixed Integer Linear Programming) via PuLP with constraints:
- Budget: exactly 499–500 crediti
- At least 2 goalkeepers with >15 games played
- At least 17/25 players with >15 games (reliability)
- GK budget: 35–80 cr (prevent 1cr scrubs)
- Price model: QI × (FVM/65) with forward premium and small-sample penalty
**Alternatives**: Top 2 nearest players by role + price (±20% range) ranked by FV.
""", unsafe_allow_html=False)
if __name__ == "__main__":
run()