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", )) 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'{s["price"]:.0f} cr ยท ' 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'โŸณ [{a["player"]} ยท {int(a["price"])}cr]' for a in s["alts"] ) st.markdown(f'{alt_text}', 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()