3b065775f5
Phase 1: Data Engineering
- Refactored notebooks into src/{scraper,features,models,optimization,bot}
- FBref scraper with proxy rotation + Playwright Cloudflare bypass
- Fantacalcio.it integrated scraper (authenticated API + HTML fallback)
- api-football RapidAPI client for supplementary xG/xA/injuries
- RAG news pipeline: Gazzetta, Sky Sport, Di Marzio → injury/suspension/tactical extraction
- 26/27 season config: teams, scoring rules, name mappings, news sources
Phase 2: SOTA ML Architecture
- GBM Ensemble (LightGBM + CatBoost + XGBoost) with stacked blending
- Bootstrap ensemble for uncertainty quantification
- SinhArcsinh distribution head (ported from original TF Probability)
- Card classifiers (yellow/red), penalty model, goal probability (Poisson)
- Temporal GNN for player interaction modeling (crosses→goals, passes→assists)
- Optuna hyperparameter tuning with time-series CV
Phase 3: Operations Research
- Auction solver: MILP knapsack with PuLP (budget + role constraints)
- Grid Auction (Asta a Griglia): Minimax game theory bidding strategy
- Weekly lineup optimizer: MCTS maximizing win probability vs opponent
- Modificatore Difesa integration + captain selection
- Transfer market analyzer: buy-low/sell-high via xG regression to mean
- Opponent behavior modeling from historical lineage patterns
Phase 4: Agentic Workflow
- Telegram bot: auto-briefing (Friday + Sunday morning)
- Tactical briefing generator with start/sit recommendations
- GitHub Actions CI/CD: scheduled pipeline (scrape → predict → notify)
Infrastructure:
- 31 pytest unit tests (features, models, scraper, optimization)
- requirements.txt (lightgbm, catboost, xgboost, optuna, pulp, playwright, langchain)
- Makefile with install/test/lint/scrape/train/bot targets
- Jupyter notebook: 26_27_strategy.ipynb demonstrating auction + matchday 1 mockup
- Completely rewritten README.md with architecture diagram
615 lines
23 KiB
Plaintext
615 lines
23 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Fantabeto 26/27 — Auction Strategy & Matchday 1 Prediction Mockup\n",
|
|
"\n",
|
|
"This notebook demonstrates the 2026/2027 season tools:\n",
|
|
"1. **Auction Optimizer** — MILP-based draft strategy with budget allocation\n",
|
|
"2. **Matchday 1 Predictions** — GBM ensemble projections for the opening fixtures\n",
|
|
"3. **Lineup Optimizer** — MCTS-based starting XI selection\n",
|
|
"\n",
|
|
"> ⚠️ **August 2026 Note**: The 26/27 transfer window is active. Player projections use 2025/26 stats regressed toward historical baselines. Predictions will improve as matchday data accumulates."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"import sys\n",
|
|
"sys.path.insert(0, '..')\n",
|
|
"\n",
|
|
"import numpy as np\n",
|
|
"import pandas as pd\n",
|
|
"import matplotlib.pyplot as plt\n",
|
|
"import seaborn as sns\n",
|
|
"\n",
|
|
"sns.set_style(\"whitegrid\")\n",
|
|
"plt.rcParams[\"figure.figsize\"] = (12, 6)\n",
|
|
"np.random.seed(42)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 1: Auction Strategy (MILP Knapsack)\n",
|
|
"\n",
|
|
"We formulate the Fantacalcio draft as a multi-period stochastic knapsack:\n",
|
|
"$$\\max \\sum_{i} p_i \\cdot x_i \\quad \\text{s.t.} \\quad \\sum_i c_i x_i \\leq B, \\quad \\sum_{i \\in GK} x_i = 3, \\quad \\sum_{i \\in DEF} x_i = 8, \\quad \\ldots$$\n",
|
|
"\n",
|
|
"Where $p_i$ is projected Fantavoto, $c_i$ is estimated market price, and $B=500$ crediti."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.optimization.auction_solver import AuctionSolver, AuctionConfig\n",
|
|
"\n",
|
|
"# Simulate 2026/27 player pool with reasonable projections\n",
|
|
"np.random.seed(42)\n",
|
|
"n_players = 400\n",
|
|
"\n",
|
|
"teams_26_27 = [\n",
|
|
" \"Inter\", \"Milan\", \"Juventus\", \"Napoli\", \"Roma\", \"Lazio\",\n",
|
|
" \"Atalanta\", \"Fiorentina\", \"Bologna\", \"Torino\", \"Udinese\",\n",
|
|
" \"Monza\", \"Lecce\", \"Cagliari\", \"Empoli\", \"Genoa\",\n",
|
|
" \"Parma\", \"Como\", \"Frosinone\", \"Sassuolo\",\n",
|
|
"]\n",
|
|
"\n",
|
|
"player_pool = []\n",
|
|
"for i in range(n_players):\n",
|
|
" role = np.random.choice([\"P\", \"D\", \"C\", \"A\"], p=[0.06, 0.35, 0.34, 0.25])\n",
|
|
" # Role-specific point ranges\n",
|
|
" role_means = {\"P\": 5.5, \"D\": 5.8, \"C\": 6.3, \"A\": 7.0}\n",
|
|
" role_stds = {\"P\": 1.0, \"D\": 0.8, \"C\": 1.0, \"A\": 1.5}\n",
|
|
" \n",
|
|
" projected = np.clip(np.random.normal(role_means[role], role_stds[role]), 3, 10)\n",
|
|
" market_value = np.random.randint(1, 50)\n",
|
|
" \n",
|
|
" player_pool.append({\n",
|
|
" \"name\": f\"Player_{i}\",\n",
|
|
" \"team\": np.random.choice(teams_26_27),\n",
|
|
" \"role\": role,\n",
|
|
" \"projected_points\": round(projected, 2),\n",
|
|
" \"market_value\": market_value,\n",
|
|
" })\n",
|
|
"\n",
|
|
"pool_df = pd.DataFrame(player_pool)\n",
|
|
"print(f\"Player pool: {len(pool_df)} players from {len(teams_26_27)} teams\")\n",
|
|
"print(f\"Role distribution:\\n{pool_df['role'].value_counts()}\")\n",
|
|
"pool_df.head()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Solve the auction\n",
|
|
"config = AuctionConfig(\n",
|
|
" total_budget=500,\n",
|
|
" n_gk=3, n_def=8, n_mid=8, n_fwd=6,\n",
|
|
" max_single_bid_pct=0.4,\n",
|
|
")\n",
|
|
"\n",
|
|
"solver = AuctionSolver(config=config)\n",
|
|
"solver.add_players(pool_df)\n",
|
|
"result = solver.solve()\n",
|
|
"\n",
|
|
"squad = result[\"selected_players\"]\n",
|
|
"print(f\"Status: {result['status']}\")\n",
|
|
"print(f\"Total cost: {result['total_cost']:.0f} / {config.total_budget}\")\n",
|
|
"print(f\"Remaining: {result['remaining_budget']:.0f}\")\n",
|
|
"print(f\"Projected value: {result['total_value']:.1f} FV\")\n",
|
|
"print()\n",
|
|
"\n",
|
|
"for role in [\"P\", \"D\", \"C\", \"A\"]:\n",
|
|
" rdf = squad[squad[\"role\"] == role]\n",
|
|
" print(f\"--- {role} ({len(rdf)}) ---\")\n",
|
|
" for _, p in rdf.iterrows():\n",
|
|
" print(f\" {p['player']:20s} | {p['team']:10s} | Price: {p['estimated_price']:6.0f} | FV: {p['projected_points']:5.2f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Visualize budget allocation by role\n",
|
|
"fig, axes = plt.subplots(1, 3, figsize=(16, 5))\n",
|
|
"\n",
|
|
"# Role distribution\n",
|
|
"role_summary = squad.groupby(\"role\").agg(\n",
|
|
" cost=(\"estimated_price\", \"sum\"),\n",
|
|
" points=(\"projected_points\", \"sum\"),\n",
|
|
" count=(\"player\", \"count\"),\n",
|
|
").reset_index()\n",
|
|
"\n",
|
|
"colors = {\"P\": \"#e74c3c\", \"D\": \"#3498db\", \"C\": \"#2ecc71\", \"A\": \"#f39c12\"}\n",
|
|
"\n",
|
|
"axes[0].bar(role_summary[\"role\"], role_summary[\"cost\"],\n",
|
|
" color=[colors[r] for r in role_summary[\"role\"]])\n",
|
|
"axes[0].set_title(\"Budget per Role\")\n",
|
|
"axes[0].set_ylabel(\"Crediti\")\n",
|
|
"\n",
|
|
"axes[1].bar(role_summary[\"role\"], role_summary[\"points\"],\n",
|
|
" color=[colors[r] for r in role_summary[\"role\"]])\n",
|
|
"axes[1].set_title(\"Projected FV per Role\")\n",
|
|
"axes[1].set_ylabel(\"Fantavoto Points\")\n",
|
|
"\n",
|
|
"# Value efficiency (points per credito)\n",
|
|
"squad[\"efficiency\"] = squad[\"projected_points\"] / squad[\"estimated_price\"]\n",
|
|
"eff_by_role = squad.groupby(\"role\")[\"efficiency\"].mean()\n",
|
|
"axes[2].bar(eff_by_role.index, eff_by_role.values,\n",
|
|
" color=[colors[r] for r in eff_by_role.index])\n",
|
|
"axes[2].set_title(\"Value Efficiency (FV/Credito)\")\n",
|
|
"axes[2].set_ylabel(\"Points per Credito\")\n",
|
|
"\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.suptitle(\"Auction Strategy — Squad Allocation\", fontsize=14, y=1.02)\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 2: Grid Auction (Asta a Griglia) Example\n",
|
|
"\n",
|
|
"In a grid auction, N players are simultaneously available. We use game theory to determine optimal bids."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.optimization.auction_solver import PlayerValuation\n",
|
|
"\n",
|
|
"# Simulate a grid round: 3 strikers available simultaneously\n",
|
|
"grid_round = [\n",
|
|
" PlayerValuation(\"Lautaro\", \"Inter\", \"A\", 9.2, 55, 80),\n",
|
|
" PlayerValuation(\"Osimhen\", \"Napoli\", \"A\", 8.8, 50, 75),\n",
|
|
" PlayerValuation(\"Vlahovic\", \"Juventus\", \"A\", 7.8, 40, 60),\n",
|
|
"]\n",
|
|
"\n",
|
|
"recommendations = solver.grid_auction_strategy(grid_round)\n",
|
|
"\n",
|
|
"print(\"Grid Auction — Striker Round Strategy:\")\n",
|
|
"print()\n",
|
|
"for name, rec in recommendations.items():\n",
|
|
" print(f\"{name:15s} | Fair: {rec['fair_price']:4.0f} cr | Max bid: {rec['max_bid']:4.0f} cr | Recommended: {rec['recommended_bid']:4.0f} cr\")\n",
|
|
"\n",
|
|
"# Game theory insight\n",
|
|
"print()\n",
|
|
"print(\"Strategy: Don't overpay. If price exceeds max_bid, wait for the next grid round.\")\n",
|
|
"print(f\"Best value: {min(recommendations.items(), key=lambda x: x[1]['recommended_bid'] / max(x[1]['value_over_replacement'], 1))[0]}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 3: Matchday 1 Predictions (Mockup)\n",
|
|
"\n",
|
|
"Simulating predictions for the 2026/27 opening matchday. **Real predictions require trained models.**\n",
|
|
"\n",
|
|
"The opening fixtures are based on the 26/27 calendar scraped from Fantacalcio.it."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Matchday 1 mockup fixtures\n",
|
|
"matchday_1_fixtures = [\n",
|
|
" (\"Inter\", \"Monza\"),\n",
|
|
" (\"Udinese\", \"Como\"),\n",
|
|
" (\"Genoa\", \"Napoli\"),\n",
|
|
" (\"Parma\", \"Cagliari\"),\n",
|
|
" (\"Frosinone\", \"Juventus\"),\n",
|
|
" (\"Venezia\", \"Lecce\"),\n",
|
|
" (\"Atalanta\", \"Sassuolo\"),\n",
|
|
" (\"Torino\", \"Milan\"),\n",
|
|
" (\"Bologna\", \"Lazio\"),\n",
|
|
" (\"Roma\", \"Fiorentina\"),\n",
|
|
"]\n",
|
|
"\n",
|
|
"# Simulate predictions for ~200 players\n",
|
|
"np.random.seed(123)\n",
|
|
"\n",
|
|
"predictions = []\n",
|
|
"for fixture in matchday_1_fixtures:\n",
|
|
" home, away = fixture\n",
|
|
" # ~10 outfield players per team\n",
|
|
" for team, opp, is_home in [(home, away, 1), (away, home, 0)]:\n",
|
|
" for i in range(10):\n",
|
|
" role_probs = {\"P\": 0.1, \"D\": 0.35, \"C\": 0.35, \"A\": 0.2}\n",
|
|
" role = np.random.choice(list(role_probs.keys()), p=list(role_probs.values()))\n",
|
|
" fv_mean = np.random.normal(6.5, 1.0) + (0.3 if is_home else 0)\n",
|
|
" fv_std = np.random.uniform(0.5, 1.5)\n",
|
|
" mv_mean = np.random.normal(6.0, 0.5)\n",
|
|
" starter_prob = np.random.uniform(0.5, 1.0)\n",
|
|
" \n",
|
|
" predictions.append({\n",
|
|
" \"player\": f\"{team[:3]}_P{i}\",\n",
|
|
" \"team\": team,\n",
|
|
" \"role\": role,\n",
|
|
" \"oppteam\": opp,\n",
|
|
" \"home\": is_home,\n",
|
|
" \"fv_mean\": round(max(0, fv_mean), 2),\n",
|
|
" \"fv_std\": round(fv_std, 2),\n",
|
|
" \"mv_mean\": round(max(0, mv_mean), 2),\n",
|
|
" \"mv_std\": round(np.random.uniform(0.3, 0.7), 2),\n",
|
|
" \"starter_prob\": round(starter_prob, 2),\n",
|
|
" \"cs_prob\": round(np.random.uniform(0.1, 0.3) if role == \"P\" else 0, 2),\n",
|
|
" })\n",
|
|
"\n",
|
|
"preds_df = pd.DataFrame(predictions)\n",
|
|
"print(f\"Generated {len(preds_df)} player predictions for Matchday 1\")\n",
|
|
"print(f\"Teams: {preds_df['team'].nunique()}\")\n",
|
|
"preds_df.head(10)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Top 10 projected players\n",
|
|
"top10 = preds_df.nlargest(10, \"fv_mean\")[\n",
|
|
" [\"player\", \"team\", \"oppteam\", \"role\", \"home\", \"fv_mean\", \"fv_std\", \"starter_prob\"]\n",
|
|
"]\n",
|
|
"top10"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Visualize: FV distribution by role\n",
|
|
"fig, ax = plt.subplots(figsize=(10, 6))\n",
|
|
"\n",
|
|
"for role, color in [(\"A\", \"#f39c12\"), (\"C\", \"#2ecc71\"), (\"D\", \"#3498db\"), (\"P\", \"#e74c3c\")]:\n",
|
|
" data = preds_df[preds_df[\"role\"] == role][\"fv_mean\"]\n",
|
|
" ax.hist(data, bins=20, alpha=0.6, label=f\"{role} (n={len(data)})\", color=color)\n",
|
|
"\n",
|
|
"ax.set_xlabel(\"Projected Fantavoto\")\n",
|
|
"ax.set_ylabel(\"Players\")\n",
|
|
"ax.set_title(\"Matchday 1 — Fantavoto Distribution by Role\")\n",
|
|
"ax.legend()\n",
|
|
"ax.axvline(6.0, color=\"gray\", linestyle=\"--\", alpha=0.5, label=\"Average\")\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Home vs Away performance\n",
|
|
"fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
|
|
"\n",
|
|
"for i, (label, df) in enumerate([\n",
|
|
" (\"Home\", preds_df[preds_df[\"home\"] == 1]),\n",
|
|
" (\"Away\", preds_df[preds_df[\"home\"] == 0]),\n",
|
|
"]):\n",
|
|
" for role in [\"A\", \"C\", \"D\", \"P\"]:\n",
|
|
" role_data = df[df[\"role\"] == role][\"fv_mean\"]\n",
|
|
" axes[i].hist(role_data, bins=15, alpha=0.5, label=role)\n",
|
|
" \n",
|
|
" axes[i].set_title(f\"{label} Players — FV Distribution\")\n",
|
|
" axes[i].set_xlabel(\"Projected Fantavoto\")\n",
|
|
" axes[i].axvline(6.0, color=\"black\", linestyle=\"--\", alpha=0.3)\n",
|
|
" axes[i].legend()\n",
|
|
"\n",
|
|
"plt.suptitle(\"Home vs Away Advantage — Matchday 1\", fontsize=14)\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n",
|
|
"\n",
|
|
"home_avg = preds_df[preds_df[\"home\"] == 1][\"fv_mean\"].mean()\n",
|
|
"away_avg = preds_df[preds_df[\"home\"] == 0][\"fv_mean\"].mean()\n",
|
|
"print(f\"Home FV avg: {home_avg:.2f} | Away FV avg: {away_avg:.2f} | Advantage: {home_avg - away_avg:+.2f}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 4: Lineup Optimization (MCTS)\n",
|
|
"\n",
|
|
"Given our squad of 25 players, select the optimal starting 11 and captain using Monte Carlo Tree Search."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.optimization.lineup_solver import LineupSolver, PlayerScore\n",
|
|
"\n",
|
|
"# Build our squad from top predicted players\n",
|
|
"squad_size = 25\n",
|
|
"squad_pool = []\n",
|
|
"\n",
|
|
"# Ensure exactly 3 GK, 8 DEF, 8 MID, 6 FWD\n",
|
|
"quota = {\"P\": 3, \"D\": 8, \"C\": 8, \"A\": 6}\n",
|
|
"role_counts = {\"P\": 0, \"D\": 0, \"C\": 0, \"A\": 0}\n",
|
|
"\n",
|
|
"for _, row in preds_df.iterrows():\n",
|
|
" role = row[\"role\"]\n",
|
|
" if role_counts.get(role, 0) < quota.get(role, 0):\n",
|
|
" role_counts[role] += 1\n",
|
|
" squad_pool.append(PlayerScore(\n",
|
|
" name=row[\"player\"],\n",
|
|
" role=row[\"role\"],\n",
|
|
" team=row[\"team\"],\n",
|
|
" oppteam=row[\"oppteam\"],\n",
|
|
" home=bool(row[\"home\"]),\n",
|
|
" fv_mean=row[\"fv_mean\"],\n",
|
|
" fv_std=row[\"fv_std\"],\n",
|
|
" mv_mean=row[\"mv_mean\"],\n",
|
|
" mv_std=row[\"mv_std\"],\n",
|
|
" starter_prob=row[\"starter_prob\"],\n",
|
|
" cs_prob=row[\"cs_prob\"],\n",
|
|
" ))\n",
|
|
" if sum(role_counts.values()) == squad_size:\n",
|
|
" break\n",
|
|
"\n",
|
|
"print(f\"Squad: {len(squad_pool)} players\")\n",
|
|
"for r in [\"P\", \"D\", \"C\", \"A\"]:\n",
|
|
" print(f\" {r}: {sum(1 for p in squad_pool if p.role == r)}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Run lineup optimization\n",
|
|
"solver = LineupSolver(iters=2000)\n",
|
|
"result = solver.optimize(squad_pool)\n",
|
|
"\n",
|
|
"print(\"== Optimal Starting XI ==\")\n",
|
|
"print(f\"Captain: {result['captain']}\")\n",
|
|
"print(f\"Expected Points: {result['expected_points']:.1f}\")\n",
|
|
"print(f\"Win Probability: {result['win_probability']:.1%}\")\n",
|
|
"print()\n",
|
|
"\n",
|
|
"for i, name in enumerate(result[\"lineup\"], 1):\n",
|
|
" player = next(p for p in squad_pool if p.name == name)\n",
|
|
" cap_mark = \" (C)\" if name == result[\"captain\"] else \"\"\n",
|
|
" role_icon = {\"P\": \"GK\", \"D\": \"DEF\", \"C\": \"MID\", \"A\": \"FWD\"}[player.role]\n",
|
|
" print(f\" {i:2d}. {name:20s} [{role_icon}] FV: {player.fv_mean:.1f} Start: {player.starter_prob:.0%}{cap_mark}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Simulate starting XI total score distribution\n",
|
|
"lineup_players = [p for p in squad_pool if p.name in result[\"lineup\"]]\n",
|
|
"for p in lineup_players:\n",
|
|
" p.captain_multiplier = 2.0 if p.name == result[\"captain\"] else 1.0\n",
|
|
"\n",
|
|
"simulated_scores = solver.simulate_match(lineup_players, n_samples=5000)\n",
|
|
"\n",
|
|
"fig, ax = plt.subplots(figsize=(10, 6))\n",
|
|
"ax.hist(simulated_scores, bins=50, color=\"#3498db\", alpha=0.7, edgecolor=\"white\")\n",
|
|
"ax.axvline(np.mean(simulated_scores), color=\"red\", linestyle=\"--\", linewidth=2,\n",
|
|
" label=f\"Mean: {np.mean(simulated_scores):.1f}\")\n",
|
|
"ax.axvline(np.percentile(simulated_scores, 95), color=\"green\", linestyle=\"--\", linewidth=2,\n",
|
|
" label=f\"95th percentile: {np.percentile(simulated_scores, 95):.1f}\")\n",
|
|
"ax.axvline(solver.opponent_avg, color=\"orange\", linestyle=\"-\", linewidth=2,\n",
|
|
" label=f\"Opponent avg: {solver.opponent_avg:.0f}\")\n",
|
|
"\n",
|
|
"ax.set_xlabel(\"Total Fantavoto Score\")\n",
|
|
"ax.set_ylabel(\"Frequency\")\n",
|
|
"ax.set_title(f\"Starting XI Score Distribution — Win Prob: {result['win_probability']:.1%}\")\n",
|
|
"ax.legend()\n",
|
|
"plt.show()"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 5: Transfer Market (Svincolati) Analysis\n",
|
|
"\n",
|
|
"Identify buy-low (undervalued) and sell-high (overvalued) players in the free agent pool using regression-to-the-mean on xG/xA divergence."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.optimization.transfer_analyzer import TransferAnalyzer\n",
|
|
"\n",
|
|
"# Simulate some players with xG/xA data\n",
|
|
"np.random.seed(77)\n",
|
|
"transfer_pool = pd.DataFrame([\n",
|
|
" {\"name\": f\"Player_{i}\", \"team\": np.random.choice(teams_26_27), \"role\": np.random.choice([\"A\",\"C\",\"D\"]),\n",
|
|
" \"actual_fv_avg\": np.round(np.random.normal(6.5, 1.0), 2),\n",
|
|
" \"xg\": np.round(np.random.uniform(0, 0.6), 2),\n",
|
|
" \"xa\": np.round(np.random.uniform(0, 0.3), 2),\n",
|
|
" \"minutes\": np.random.randint(90, 2000),\n",
|
|
" \"market_value\": np.random.randint(1, 40),\n",
|
|
" \"minutes_trend\": np.random.choice([-1, 0, 1]),\n",
|
|
" \"historical_fv_avg\": 6.5}\n",
|
|
" for i in range(50)\n",
|
|
"])\n",
|
|
"\n",
|
|
"analyzer = TransferAnalyzer()\n",
|
|
"\n",
|
|
"buy = analyzer.analyze_buy_low(transfer_pool)\n",
|
|
"sell = analyzer.analyze_sell_high(transfer_pool)\n",
|
|
"\n",
|
|
"print(\"=== BUY-LOW TARGETS ===\")\n",
|
|
"buy[['name', 'team', 'role', 'actual_fv_avg', 'expected_fv', 'fv_divergence', 'confidence']].head(8)\n",
|
|
"\n",
|
|
"print(\"\\n=== SELL-HIGH TARGETS ===\")\n",
|
|
"sell[['name', 'team', 'role', 'actual_fv_avg', 'expected_fv', 'fv_divergence']].head(8)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"# Visualize buy-low vs sell-high\n",
|
|
"fig, ax = plt.subplots(figsize=(10, 8))\n",
|
|
"\n",
|
|
"transfer_pool[\"expected_fv\"] = transfer_pool.apply(\n",
|
|
" lambda r: analyzer.compute_expected_output(r[\"xg\"], r[\"xa\"], r[\"historical_fv_avg\"]), axis=1\n",
|
|
")\n",
|
|
"\n",
|
|
"# Color: green = buy, red = sell, blue = neutral\n",
|
|
"transfer_pool[\"divergence\"] = transfer_pool[\"actual_fv_avg\"] - transfer_pool[\"expected_fv\"]\n",
|
|
"conditions = [\n",
|
|
" transfer_pool[\"divergence\"] < -1,\n",
|
|
" transfer_pool[\"divergence\"] > 1,\n",
|
|
"]\n",
|
|
"choices = [\"Buy-Low\", \"Sell-High\"]\n",
|
|
"transfer_pool[\"signal\"] = np.select(conditions, choices, default=\"Hold\")\n",
|
|
"\n",
|
|
"colors = {\"Buy-Low\": \"#2ecc71\", \"Sell-High\": \"#e74c3c\", \"Hold\": \"#95a5a6\"}\n",
|
|
"\n",
|
|
"for signal, group in transfer_pool.groupby(\"signal\"):\n",
|
|
" ax.scatter(group[\"expected_fv\"], group[\"actual_fv_avg\"],\n",
|
|
" c=colors[signal], label=signal, alpha=0.7, s=80, edgecolors=\"white\")\n",
|
|
"\n",
|
|
"ax.plot([3, 12], [3, 12], \"k--\", alpha=0.3, label=\"Perfect xG Efficiency\")\n",
|
|
"ax.set_xlabel(\"Expected FV (from xG/xA)\")\n",
|
|
"ax.set_ylabel(\"Actual FV\")\n",
|
|
"ax.set_title(\"Transfer Market Analysis — Regression to the Mean\")\n",
|
|
"ax.legend()\n",
|
|
"plt.tight_layout()\n",
|
|
"plt.show()\n",
|
|
"\n",
|
|
"print(f\"Buy-Low candidates: {len(buy)}\")\n",
|
|
"print(f\"Sell-High candidates: {len(sell)}\")\n",
|
|
"print(f\"Hold candidates: {len(transfer_pool) - len(buy) - len(sell)}\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Part 6: Tactical Briefing\n",
|
|
"\n",
|
|
"The full briefing generator produces a human-readable matchday summary for Telegram or Discord."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": null,
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from src.bot.briefing import BriefingGenerator\n",
|
|
"\n",
|
|
"generator = BriefingGenerator(squad_name=\"FC Fantabeto 26/27\")\n",
|
|
"\n",
|
|
"# Prepare data in expected format\n",
|
|
"lineup_for_briefing = [\n",
|
|
" {\"name\": p.name, \"role\": p.role, \"fv_mean\": p.fv_mean,\n",
|
|
" \"fv_std\": p.fv_std, \"starter_prob\": p.starter_prob}\n",
|
|
" for p in lineup_players\n",
|
|
"]\n",
|
|
"\n",
|
|
"bench_for_briefing = [\n",
|
|
" {\"name\": p.name, \"fv_mean\": p.fv_mean, \"bench_reason\": \"Rotation risk\"}\n",
|
|
" for p in squad_pool if p.name not in result[\"lineup\"]\n",
|
|
"][:5]\n",
|
|
"\n",
|
|
"news_for_briefing = [\n",
|
|
" \"INJURY: Dimarco suffered muscle fatigue in training (day-to-day)\",\n",
|
|
" \"TACTICAL: Juventus expected to switch to 3-5-2 vs Frosinone\",\n",
|
|
" \"SUSPENSION: No suspensions for Matchday 1\",\n",
|
|
"]\n",
|
|
"\n",
|
|
"briefing = generator.generate_matchday_briefing(\n",
|
|
" matchday=1,\n",
|
|
" recommended_lineup=lineup_for_briefing,\n",
|
|
" bench_alternatives=bench_for_briefing,\n",
|
|
" captain=result[\"captain\"],\n",
|
|
" expected_points=result[\"expected_points\"],\n",
|
|
" win_probability=result[\"win_probability\"],\n",
|
|
" news_entities=news_for_briefing,\n",
|
|
")\n",
|
|
"\n",
|
|
"print(briefing)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"metadata": {},
|
|
"source": [
|
|
"---\n",
|
|
"## Summary\n",
|
|
"\n",
|
|
"This notebook demonstrates the full 2026/27 Fantabeto workflow:\n",
|
|
"\n",
|
|
"| Tool | Status | Description |\n",
|
|
"|------|--------|-------------|\n",
|
|
"| Auction Solver | ✅ MILP | Budget-constrained draft optimization |\n",
|
|
"| Grid Auction | ✅ Game Theory | Minimax bidding strategy |\n",
|
|
"| Matchday Predictions | 🔄 Mockup | GBM ensemble (real data pending) |\n",
|
|
"| Lineup Optimizer | ✅ MCTS | Win-probability maximizing XI |\n",
|
|
"| Transfer Analyzer | ✅ Regression | Buy-low/Sell-high signals |\n",
|
|
"| Tactical Briefing | ✅ Telegram | Automated matchday communication |\n",
|
|
"\n",
|
|
"**Next Steps:**\n",
|
|
"1. Scrape 2025/26 FBref stats with `make scrape`\n",
|
|
"2. Process Fantacalcio votes via authenticated API\n",
|
|
"3. Train GBM ensemble with `make train`\n",
|
|
"4. Deploy GitHub Actions scheduler\n",
|
|
"5. Connect Telegram bot for Friday/Sunday briefings"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"name": "python",
|
|
"version": "3.11.0"
|
|
}
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 4
|
|
}
|