brackt/app/routes/admin.sports-seasons.$id.tsx

581 lines
23 KiB
TypeScript
Raw Normal View History

import { Form, Link, redirect, useNavigate } from "react-router";
import type { Route } from "./+types/admin.sports-seasons.$id";
import { findSportsSeasonById, updateSportsSeason, deleteSportsSeason } from "~/models/sports-season";
import { findParticipantsBySportsSeasonId } from "~/models/participant";
import { processSeasonStandings } from "~/models/scoring-calculator";
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
import { database } from "~/database/context";
import { participantEvSnapshots } from "~/database/schema";
import { eq, desc } from "drizzle-orm";
import { getSimulatorInfo, type SimulatorType } from "~/services/simulations/registry";
import { Button } from "~/components/ui/button";
import { Input } from "~/components/ui/input";
import { Label } from "~/components/ui/label";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "~/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "~/components/ui/select";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "~/components/ui/alert-dialog";
import { Badge } from "~/components/ui/badge";
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
import { Trash2, Users, Trophy, Calculator, CheckCircle2, Zap, AlertTriangle, Loader2 } from "lucide-react";
import { useState } from "react";
export function meta({ data }: Route.MetaArgs): Route.MetaDescriptors {
return [{ title: `${data?.sportsSeason?.name ?? "Sports Season"} - Brackt Admin` }];
}
export async function loader({ params }: Route.LoaderArgs) {
const sportsSeason = await findSportsSeasonById(params.id);
if (!sportsSeason) {
throw new Response("Sports season not found", { status: 404 });
}
const participants = await findParticipantsBySportsSeasonId(params.id);
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
// Get the most recent snapshot date (if any)
const db = database();
const lastSnapshot = await db
.select({ snapshotDate: participantEvSnapshots.snapshotDate })
.from(participantEvSnapshots)
.where(eq(participantEvSnapshots.sportsSeasonId, params.id))
.orderBy(desc(participantEvSnapshots.snapshotDate))
.limit(1);
const lastSimulatedDate = lastSnapshot[0]?.snapshotDate ?? null;
const simulatorInfo = sportsSeason.sport?.simulatorType
? getSimulatorInfo(sportsSeason.sport.simulatorType as SimulatorType)
: null;
return {
sportsSeason,
participants,
lastSimulatedDate,
simulatorInfo,
};
}
export async function action({ request, params }: Route.ActionArgs) {
const formData = await request.formData();
const intent = formData.get("intent");
if (intent === "delete") {
await deleteSportsSeason(params.id);
return redirect("/admin/sports-seasons");
}
if (intent === "finalize-standings") {
try {
await processSeasonStandings(params.id);
await updateSportsSeason(params.id, { status: "completed" });
return { success: true, message: "Standings finalized and fantasy placements assigned!" };
} catch (error) {
console.error("Error finalizing standings:", error);
return { error: "Failed to finalize standings. Please try again." };
}
}
// Update
const name = formData.get("name");
const year = formData.get("year");
const startDate = formData.get("startDate");
const endDate = formData.get("endDate");
const status = formData.get("status");
const scoringType = formData.get("scoringType");
const scoringPattern = formData.get("scoringPattern");
const totalMajors = formData.get("totalMajors");
// Validation
if (typeof name !== "string" || !name.trim()) {
return { error: "Season name is required" };
}
if (typeof year !== "string") {
return { error: "Year is required" };
}
const yearNum = parseInt(year, 10);
if (isNaN(yearNum) || yearNum < 2000 || yearNum > 2100) {
return { error: "Year must be between 2000 and 2100" };
}
if (status !== "upcoming" && status !== "active" && status !== "completed") {
return { error: "Invalid status" };
}
if (scoringType !== "playoffs" && scoringType !== "regular_season" && scoringType !== "majors") {
return { error: "Invalid scoring type" };
}
const validScoringPatterns = ["playoff_bracket", "season_standings", "qualifying_points"];
if (scoringPattern && typeof scoringPattern === "string" && !validScoringPatterns.includes(scoringPattern)) {
return { error: "Invalid scoring pattern" };
}
try {
const updateData: any = {
name: name.trim(),
year: yearNum,
startDate: typeof startDate === "string" && startDate ? startDate : null,
endDate: typeof endDate === "string" && endDate ? endDate : null,
status,
scoringType,
};
if (scoringPattern && typeof scoringPattern === "string") {
updateData.scoringPattern = scoringPattern;
}
if (totalMajors && typeof totalMajors === "string") {
const totalMajorsNum = parseInt(totalMajors, 10);
if (!isNaN(totalMajorsNum) && totalMajorsNum > 0) {
updateData.totalMajors = totalMajorsNum;
}
}
await updateSportsSeason(params.id, updateData);
return { success: true, message: "Sports season updated successfully!" };
} catch (error) {
console.error("Error updating sports season:", error);
return { error: "Failed to update sports season. Please try again." };
}
}
export default function EditSportsSeason({ loaderData, actionData }: Route.ComponentProps) {
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
const { sportsSeason, participants, lastSimulatedDate, simulatorInfo } = loaderData;
const navigate = useNavigate();
const [scoringPattern, setScoringPattern] = useState<string>(sportsSeason.scoringPattern || "");
return (
<div className="p-8">
<div className="max-w-2xl">
<div className="mb-6">
<h1 className="text-3xl font-bold">Edit Sports Season</h1>
<p className="text-muted-foreground mt-1">
{sportsSeason.sport.name} - {sportsSeason.name}
</p>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Sports Season Details</CardTitle>
<CardDescription>
Update the information for this sports season
</CardDescription>
</CardHeader>
<CardContent>
<Form method="post" className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">Season Name</Label>
<Input
id="name"
name="name"
type="text"
defaultValue={sportsSeason.name}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="year">Year</Label>
<Input
id="year"
name="year"
type="number"
min="2000"
max="2100"
defaultValue={sportsSeason.year}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="startDate">Start Date (Optional)</Label>
<Input
id="startDate"
name="startDate"
type="date"
defaultValue={sportsSeason.startDate || ""}
/>
</div>
<div className="space-y-2">
<Label htmlFor="endDate">End Date (Optional)</Label>
<Input
id="endDate"
name="endDate"
type="date"
defaultValue={sportsSeason.endDate || ""}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="status">Status</Label>
<Select name="status" defaultValue={sportsSeason.status} required>
<SelectTrigger id="status">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="upcoming">Upcoming</SelectItem>
<SelectItem value="active">Active</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="scoringType">Scoring Type</Label>
<Select name="scoringType" defaultValue={sportsSeason.scoringType} required>
<SelectTrigger id="scoringType">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="playoffs">Playoffs</SelectItem>
<SelectItem value="regular_season">Regular Season</SelectItem>
<SelectItem value="majors">Majors</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Playoffs: Team sports playoffs. Regular Season: Full season standings. Majors: Individual sport majors.
</p>
</div>
<div className="space-y-2">
<Label htmlFor="scoringPattern">Scoring Pattern (Optional)</Label>
<Select name="scoringPattern" value={scoringPattern} onValueChange={setScoringPattern}>
<SelectTrigger id="scoringPattern">
<SelectValue placeholder="Select scoring pattern (optional)" />
</SelectTrigger>
<SelectContent>
<SelectItem value="playoff_bracket">Playoff Bracket</SelectItem>
<SelectItem value="season_standings">Season Standings</SelectItem>
<SelectItem value="qualifying_points">Qualifying Points (Golf/Tennis)</SelectItem>
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Qualifying Points: For sports like Golf/Tennis where participants earn points across majors, then top 8 get fantasy points.
</p>
</div>
{scoringPattern === "qualifying_points" && (
<div className="space-y-2">
<Label htmlFor="totalMajors">Total Majors</Label>
<Input
id="totalMajors"
name="totalMajors"
type="number"
min="1"
max="10"
defaultValue={sportsSeason.totalMajors || 4}
placeholder="e.g., 4 (for Golf)"
/>
<p className="text-sm text-muted-foreground">
How many major tournaments will be tracked? (e.g., Golf has 4 majors, Tennis has 4 Grand Slams)
</p>
</div>
)}
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm">
{actionData.error}
</div>
)}
{actionData?.success && (
Redesign to dark-mode-only with navy palette and accent colors (#13) Removes light mode entirely in favour of a permanent dark theme with a navy-tinted background and three signature accents (electric blue, amber/gold, coral) exposed as CSS custom properties and Tailwind utilities (bg-electric, text-amber-accent, text-coral-accent). - Set class="dark" on <html> and apply Clerk dark base theme - Rewrite app.css: single :root palette (oklch navy values), custom --electric / --amber-accent / --coral-accent variables, remove duplicate .dark block and light-mode bg-white/bg-gray-950 rule - Install @clerk/themes for Clerk dark modal support - Replace hardcoded Tailwind colors across 30+ files: - Draft grid cells: blue-50/blue-950 → electric/15, green-50/950 → emerald/10 - Timer: green-600/yellow-600/red-600 → emerald-400/amber-accent/coral-accent - Status badges: blue-50/green-50/gray-50 → electric/emerald/muted variants - Success messages: green-500/15 text-green-700 dark:text-green-400 → emerald-500/15 text-emerald-400 - Info cards: blue-50 dark:bg-blue-950 → electric/10 - Warning cards: yellow-500 → amber-accent variants - Medal/placement badges: yellow-500/orange-600 → amber-accent/coral-accent - Movement indicators: green-600/red-600 → emerald-400/coral-accent - Connection dots: green-500/red-500 → emerald-500/coral-accent - Remove dark:hidden/dark:block logo toggle in welcome.tsx (always dark) - Update DraftGrid test assertions to match new class names Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 19:26:11 -08:00
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm">
{actionData.message}
</div>
)}
<div className="flex gap-4">
<Button type="submit" className="flex-1">
Save Changes
</Button>
<Button type="button" variant="outline" asChild>
<Link to="/admin/sports-seasons">Cancel</Link>
</Button>
</div>
</Form>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Participants</CardTitle>
<CardDescription>
{participants.length} {participants.length === 1 ? "participant" : "participants"}
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/participants`)}
>
<Users className="mr-2 h-4 w-4" />
Manage Participants
</Button>
</div>
</CardHeader>
<CardContent>
{participants.length === 0 ? (
<p className="text-sm text-muted-foreground">
No participants added yet. Add teams or players to this season.
</p>
) : (
<div className="space-y-2">
{participants.slice(0, 5).map((participant) => (
<div key={participant.id} className="text-sm">
{participant.name}
</div>
))}
{participants.length > 5 && (
<p className="text-sm text-muted-foreground">
And {participants.length - 5} more...
</p>
)}
</div>
)}
</CardContent>
</Card>
feat: implement Expected Value System with ICM probability calculator Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model for calculating participant placement probabilities from futures odds. ## Key Features ### ICM Probability Calculator - Implements Harville-Malmuth method for distributing probabilities - Converts American odds to championship probabilities - Generates P(1st) through P(8th) for all participants - Column-normalized: each placement sums to 100% across all teams - Works with any number of participants (not limited to 8) ### Admin UI - Futures Odds Entry - Enter American odds (e.g., +550, -200) for championship futures - Live preview of ICM-calculated probability distributions - Displays all 8 placement probabilities - Persists odds for editing on subsequent visits - Automatic probability normalization (removes bookmaker vig) ### Database Schema Updates - Renamed participant_expected_values.season_id → sports_season_id - Updated foreign key to reference sports_seasons instead of seasons - Added source_odds field to store original futures odds - Migration 0025: Column rename and FK update - Migration 0026: Add source_odds field ### Model Layer - participant-expected-value: CRUD operations for probability distributions - Supports multiple probability sources (manual, futures_odds, elo_simulation) - Automatic EV calculation based on league scoring rules - Probability validation and normalization ### Service Layer - icm-calculator: Harville-Malmuth probability distribution - probability-engine: Odds conversion and Elo utilities (for future use) - bracket-simulator: Monte Carlo simulation (for future hybrid approach) - ev-calculator: Expected value computation from probabilities ## Technical Details - Uses exponential decay favoring top positions for strong teams - Preserves championship probability ordering in final distributions - Row sums vary (strong teams ~100%, weak teams lower) - All probabilities between 0-1, mathematically valid - Comprehensive test suite: 97 tests passing ## Future Enhancements - Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs - Integration with league-specific scoring rules - Historical probability tracking for accuracy analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Expected Values</CardTitle>
<CardDescription>
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
{simulatorInfo
? simulatorInfo.name
: "Manage probability distributions and projected points"}
feat: implement Expected Value System with ICM probability calculator Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model for calculating participant placement probabilities from futures odds. ## Key Features ### ICM Probability Calculator - Implements Harville-Malmuth method for distributing probabilities - Converts American odds to championship probabilities - Generates P(1st) through P(8th) for all participants - Column-normalized: each placement sums to 100% across all teams - Works with any number of participants (not limited to 8) ### Admin UI - Futures Odds Entry - Enter American odds (e.g., +550, -200) for championship futures - Live preview of ICM-calculated probability distributions - Displays all 8 placement probabilities - Persists odds for editing on subsequent visits - Automatic probability normalization (removes bookmaker vig) ### Database Schema Updates - Renamed participant_expected_values.season_id → sports_season_id - Updated foreign key to reference sports_seasons instead of seasons - Added source_odds field to store original futures odds - Migration 0025: Column rename and FK update - Migration 0026: Add source_odds field ### Model Layer - participant-expected-value: CRUD operations for probability distributions - Supports multiple probability sources (manual, futures_odds, elo_simulation) - Automatic EV calculation based on league scoring rules - Probability validation and normalization ### Service Layer - icm-calculator: Harville-Malmuth probability distribution - probability-engine: Odds conversion and Elo utilities (for future use) - bracket-simulator: Monte Carlo simulation (for future hybrid approach) - ev-calculator: Expected value computation from probabilities ## Technical Details - Uses exponential decay favoring top positions for strong teams - Preserves championship probability ordering in final distributions - Row sums vary (strong teams ~100%, weak teams lower) - All probabilities between 0-1, mathematically valid - Comprehensive test suite: 97 tests passing ## Future Enhancements - Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs - Integration with league-specific scoring rules - Historical probability tracking for accuracy analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00
</CardDescription>
</div>
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
<div className="flex items-center gap-2">
{sportsSeason.simulationStatus === "failed" && (
<Badge variant="outline" className="bg-destructive/15 text-destructive border-destructive/30">
<AlertTriangle className="mr-1 h-3 w-3" />
Last run failed
</Badge>
)}
feat: implement Expected Value System with ICM probability calculator Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model for calculating participant placement probabilities from futures odds. ## Key Features ### ICM Probability Calculator - Implements Harville-Malmuth method for distributing probabilities - Converts American odds to championship probabilities - Generates P(1st) through P(8th) for all participants - Column-normalized: each placement sums to 100% across all teams - Works with any number of participants (not limited to 8) ### Admin UI - Futures Odds Entry - Enter American odds (e.g., +550, -200) for championship futures - Live preview of ICM-calculated probability distributions - Displays all 8 placement probabilities - Persists odds for editing on subsequent visits - Automatic probability normalization (removes bookmaker vig) ### Database Schema Updates - Renamed participant_expected_values.season_id → sports_season_id - Updated foreign key to reference sports_seasons instead of seasons - Added source_odds field to store original futures odds - Migration 0025: Column rename and FK update - Migration 0026: Add source_odds field ### Model Layer - participant-expected-value: CRUD operations for probability distributions - Supports multiple probability sources (manual, futures_odds, elo_simulation) - Automatic EV calculation based on league scoring rules - Probability validation and normalization ### Service Layer - icm-calculator: Harville-Malmuth probability distribution - probability-engine: Odds conversion and Elo utilities (for future use) - bracket-simulator: Monte Carlo simulation (for future hybrid approach) - ev-calculator: Expected value computation from probabilities ## Technical Details - Uses exponential decay favoring top positions for strong teams - Preserves championship probability ordering in final distributions - Row sums vary (strong teams ~100%, weak teams lower) - All probabilities between 0-1, mathematically valid - Comprehensive test suite: 97 tests passing ## Future Enhancements - Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs - Integration with league-specific scoring rules - Historical probability tracking for accuracy analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00
<Button
size="sm"
variant="outline"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/futures-odds`)}
>
<Calculator className="mr-2 h-4 w-4" />
Futures Odds
</Button>
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
{simulatorInfo && (
<Form method="post" action={`/admin/sports-seasons/${sportsSeason.id}/simulate`}>
<Button
type="submit"
size="sm"
disabled={sportsSeason.simulationStatus === "running"}
>
{sportsSeason.simulationStatus === "running" ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Running...
</>
) : (
<>
<Zap className="mr-2 h-4 w-4" />
Run Simulation
</>
)}
</Button>
</Form>
)}
feat: implement Expected Value System with ICM probability calculator Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model for calculating participant placement probabilities from futures odds. ## Key Features ### ICM Probability Calculator - Implements Harville-Malmuth method for distributing probabilities - Converts American odds to championship probabilities - Generates P(1st) through P(8th) for all participants - Column-normalized: each placement sums to 100% across all teams - Works with any number of participants (not limited to 8) ### Admin UI - Futures Odds Entry - Enter American odds (e.g., +550, -200) for championship futures - Live preview of ICM-calculated probability distributions - Displays all 8 placement probabilities - Persists odds for editing on subsequent visits - Automatic probability normalization (removes bookmaker vig) ### Database Schema Updates - Renamed participant_expected_values.season_id → sports_season_id - Updated foreign key to reference sports_seasons instead of seasons - Added source_odds field to store original futures odds - Migration 0025: Column rename and FK update - Migration 0026: Add source_odds field ### Model Layer - participant-expected-value: CRUD operations for probability distributions - Supports multiple probability sources (manual, futures_odds, elo_simulation) - Automatic EV calculation based on league scoring rules - Probability validation and normalization ### Service Layer - icm-calculator: Harville-Malmuth probability distribution - probability-engine: Odds conversion and Elo utilities (for future use) - bracket-simulator: Monte Carlo simulation (for future hybrid approach) - ev-calculator: Expected value computation from probabilities ## Technical Details - Uses exponential decay favoring top positions for strong teams - Preserves championship probability ordering in final distributions - Row sums vary (strong teams ~100%, weak teams lower) - All probabilities between 0-1, mathematically valid - Comprehensive test suite: 97 tests passing ## Future Enhancements - Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs - Integration with league-specific scoring rules - Historical probability tracking for accuracy analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
User/chris/ev f1 framework (#93) * feat: EV simulation framework with F1 Monte Carlo simulator - Add EV snapshot tables (participant_ev_snapshots, team_ev_snapshots) and simulation_status column on sports seasons - Add ev-snapshot model with upsert and history query functions - Add simulator framework: types, bracket/F1/golf simulators, registry - F1 simulator: vig-removed ICM weighted draw (pre-season) + race-by-race Monte Carlo from current standings (in-season); per-position column normalization to prevent floating-point EV drift - Add admin simulate route and Run Simulation button on sports season page - Rework futures-odds admin page to save odds then run simulation in one action - Remove recalculate-probabilities route (superseded by simulate route) - Remove EV trend chart panel and associated DB queries Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: map simulators to sports via simulatorType field Adds a `simulator_type` enum column to the `sports` table so each sport can be assigned a specific simulation algorithm rather than deriving it from the sports season's scoring pattern. - Add `simulatorTypeEnum` (f1_standings, indycar_standings, golf_qualifying_points, playoff_bracket) + `simulatorType` nullable column on `sports` table; migration 0037 - Rewrite simulator registry to key off `SimulatorType` instead of `ScoringPattern`; indycar_standings shares F1Simulator for now - `findSportsSeasonById` now returns `SportsSeasonWithSport` so callers have typed access to `sport.simulatorType` - Simulate and futures-odds actions read `sport.simulatorType`; guard fires before setting `simulationStatus: running` - Admin sport edit page gains a Simulator Type dropdown Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 15:34:31 -07:00
{simulatorInfo
? <>
{simulatorInfo.description}.{" "}
{lastSimulatedDate ? `Last simulated: ${lastSimulatedDate}.` : "No simulation has been run yet."}
{" "}Import futures odds first, then run the simulation to update EVs and save a snapshot.
</>
: "Import futures odds to set probability distributions."}
feat: implement Expected Value System with ICM probability calculator Implements Phase 5.2 of the EV system with Harville-Malmuth Independent Chip Model for calculating participant placement probabilities from futures odds. ## Key Features ### ICM Probability Calculator - Implements Harville-Malmuth method for distributing probabilities - Converts American odds to championship probabilities - Generates P(1st) through P(8th) for all participants - Column-normalized: each placement sums to 100% across all teams - Works with any number of participants (not limited to 8) ### Admin UI - Futures Odds Entry - Enter American odds (e.g., +550, -200) for championship futures - Live preview of ICM-calculated probability distributions - Displays all 8 placement probabilities - Persists odds for editing on subsequent visits - Automatic probability normalization (removes bookmaker vig) ### Database Schema Updates - Renamed participant_expected_values.season_id → sports_season_id - Updated foreign key to reference sports_seasons instead of seasons - Added source_odds field to store original futures odds - Migration 0025: Column rename and FK update - Migration 0026: Add source_odds field ### Model Layer - participant-expected-value: CRUD operations for probability distributions - Supports multiple probability sources (manual, futures_odds, elo_simulation) - Automatic EV calculation based on league scoring rules - Probability validation and normalization ### Service Layer - icm-calculator: Harville-Malmuth probability distribution - probability-engine: Odds conversion and Elo utilities (for future use) - bracket-simulator: Monte Carlo simulation (for future hybrid approach) - ev-calculator: Expected value computation from probabilities ## Technical Details - Uses exponential decay favoring top positions for strong teams - Preserves championship probability ordering in final distributions - Row sums vary (strong teams ~100%, weak teams lower) - All probabilities between 0-1, mathematically valid - Comprehensive test suite: 97 tests passing ## Future Enhancements - Hybrid approach: ICM pre-playoffs, bracket simulation during playoffs - Integration with league-specific scoring rules - Historical probability tracking for accuracy analysis 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-17 22:19:46 -08:00
</p>
</CardContent>
</Card>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Scoring Events</CardTitle>
<CardDescription>
Manage games, tournaments, and schedule entries
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/events`)}
>
<Trophy className="mr-2 h-4 w-4" />
Manage Events
</Button>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Create playoff brackets, major tournaments, or import a race/game schedule.
</p>
</CardContent>
</Card>
{sportsSeason.scoringPattern === "season_standings" && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Championship Standings</CardTitle>
<CardDescription>
Update participant positions and points as the season progresses
</CardDescription>
</div>
<Button
size="sm"
onClick={() => navigate(`/admin/sports-seasons/${sportsSeason.id}/standings`)}
>
Update Standings
</Button>
</div>
</CardHeader>
</Card>
)}
{sportsSeason.scoringPattern === "season_standings" && (
<Card className={sportsSeason.status === "completed" ? "border-emerald-500/30" : "border-amber-500/30"}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
Finalize Standings
{sportsSeason.status === "completed" && (
<Badge variant="outline" className="bg-emerald-500/15 text-emerald-400 border-emerald-500/30">
<CheckCircle2 className="mr-1 h-3 w-3" />
Finalized
</Badge>
)}
</CardTitle>
<CardDescription>
{sportsSeason.status === "completed"
? "Season standings have been finalized and fantasy placements assigned."
: "When the championship is over, finalize standings to assign fantasy placements (1st8th) to participants based on their final positions."}
</CardDescription>
</div>
</div>
</CardHeader>
{sportsSeason.status !== "completed" && (
<CardContent>
{actionData?.error && (
<div className="bg-destructive/15 text-destructive px-4 py-3 rounded-md text-sm mb-4">
{actionData.error}
</div>
)}
{actionData?.success && actionData.message?.includes("finalized") && (
<div className="bg-emerald-500/15 text-emerald-400 px-4 py-3 rounded-md text-sm mb-4">
{actionData.message}
</div>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="outline" className="border-amber-500/50 text-amber-600 hover:bg-amber-500/10 dark:text-amber-400">
<CheckCircle2 className="mr-2 h-4 w-4" />
Finalize Standings
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Finalize championship standings?</AlertDialogTitle>
<AlertDialogDescription>
This will read the current participant standings and assign fantasy placements (1st through 8th place). The season will be marked as completed. This action can be re-run to correct results if needed.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post">
<input type="hidden" name="intent" value="finalize-standings" />
<AlertDialogAction type="submit">
Finalize Standings
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
)}
</Card>
)}
<Card className="border-destructive">
<CardHeader>
<CardTitle className="text-destructive">Danger Zone</CardTitle>
<CardDescription>
Permanently delete this sports season
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">
<Trash2 className="mr-2 h-4 w-4" />
Delete Sports Season
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete the sports season "{sportsSeason.name}" and all
associated participants and results. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<Form method="post">
<input type="hidden" name="intent" value="delete" />
<AlertDialogAction type="submit" className="bg-destructive text-destructive-foreground hover:bg-destructive/90">
Delete
</AlertDialogAction>
</Form>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
</div>
</div>
</div>
);
}