Change participant expectedValue from integer to decimal (#1)
* feat: sync odds-based EV to participants table for draft room ranking When admin enters futures odds or manual probabilities, the calculated EV is now synced from participantExpectedValues to participants.expectedValue. This closes the gap where EVs were calculated but never reflected in the draft room ranking. Also changes expectedValue from integer to decimal(10,2) for better precision, and displays "—" for participants with no odds data. https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T * fix: correct expectedValue type to string in participants route Missed instance of `expectedValue: 0` (number) that failed typecheck after schema change from integer to decimal. https://claude.ai/code/session_01JWG2zg2EMzCn6RgEufPC1T --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9211cad7d1
commit
3210ab265f
9 changed files with 54 additions and 15 deletions
|
|
@ -193,7 +193,7 @@ export function AvailableParticipantsSection({
|
|||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right font-mono font-semibold">
|
||||
{participant.expectedValue}
|
||||
{participant.expectedValue ? participant.expectedValue.toFixed(1) : "—"}
|
||||
</td>
|
||||
{hasTeam && (
|
||||
<td className="p-3 text-right">
|
||||
|
|
|
|||
|
|
@ -294,10 +294,12 @@ export async function getTopAvailableParticipant(
|
|||
allParticipants.push(...seasonParticipants);
|
||||
}
|
||||
|
||||
// Sort by EV desc, then name
|
||||
// Sort by EV desc, then name (expectedValue is a decimal string from DB)
|
||||
allParticipants.sort((a, b) => {
|
||||
if (b.expectedValue !== a.expectedValue) {
|
||||
return b.expectedValue - a.expectedValue;
|
||||
const evA = parseFloat(String(a.expectedValue)) || 0;
|
||||
const evB = parseFloat(String(b.expectedValue)) || 0;
|
||||
if (evB !== evA) {
|
||||
return evB - evA;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { database } from "~/database/context";
|
||||
import { participantExpectedValues } from "~/database/schema";
|
||||
import { participantExpectedValues, participants } from "~/database/schema";
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import type { ProbabilityDistribution, ScoringRules } from "~/services/ev-calculator";
|
||||
import { calculateEV, validateProbabilities, normalizeProbabilities } from "~/services/ev-calculator";
|
||||
|
|
@ -95,6 +95,8 @@ export async function upsertParticipantEV(
|
|||
|
||||
const now = new Date();
|
||||
|
||||
let result: ParticipantEV;
|
||||
|
||||
if (existing.length > 0) {
|
||||
// Update existing
|
||||
const updated = await db
|
||||
|
|
@ -117,7 +119,7 @@ export async function upsertParticipantEV(
|
|||
.where(eq(participantExpectedValues.id, existing[0].id))
|
||||
.returning();
|
||||
|
||||
return updated[0];
|
||||
result = updated[0];
|
||||
} else {
|
||||
// Create new
|
||||
const created = await db
|
||||
|
|
@ -141,8 +143,16 @@ export async function upsertParticipantEV(
|
|||
})
|
||||
.returning();
|
||||
|
||||
return created[0];
|
||||
result = created[0];
|
||||
}
|
||||
|
||||
// Sync calculated EV to participants table for draft room ranking
|
||||
await db
|
||||
.update(participants)
|
||||
.set({ expectedValue: expectedValue.toString(), updatedAt: now })
|
||||
.where(eq(participants.id, participantId));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -211,6 +221,12 @@ export async function deleteParticipantEV(
|
|||
eq(participantExpectedValues.sportsSeasonId, sportsSeasonId)
|
||||
)
|
||||
);
|
||||
|
||||
// Reset participant EV to 0
|
||||
await db
|
||||
.update(participants)
|
||||
.set({ expectedValue: "0", updatedAt: new Date() })
|
||||
.where(eq(participants.id, participantId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -270,16 +286,23 @@ export async function recalculateEV(
|
|||
const newEV = calculateEV(probabilities, newScoringRules);
|
||||
|
||||
const db = database();
|
||||
const now = new Date();
|
||||
const updated = await db
|
||||
.update(participantExpectedValues)
|
||||
.set({
|
||||
expectedValue: newEV.toString(),
|
||||
calculatedAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
calculatedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(eq(participantExpectedValues.id, existing.id))
|
||||
.returning();
|
||||
|
||||
// Sync recalculated EV to participants table
|
||||
await db
|
||||
.update(participants)
|
||||
.set({ expectedValue: newEV.toString(), updatedAt: now })
|
||||
.where(eq(participants.id, participantId));
|
||||
|
||||
return updated[0];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
name,
|
||||
shortName: null,
|
||||
externalId: null,
|
||||
expectedValue: 0,
|
||||
expectedValue: "0",
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ export async function action({ request, params }: Route.ActionArgs) {
|
|||
name: name.trim(),
|
||||
shortName: null,
|
||||
externalId: null,
|
||||
expectedValue: 0,
|
||||
expectedValue: "0",
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
|
|
|
|||
|
|
@ -144,6 +144,12 @@ export async function loader(args: any) {
|
|||
desc(schema.participants.expectedValue),
|
||||
asc(schema.participants.name)
|
||||
);
|
||||
|
||||
// Parse decimal expectedValue (Drizzle returns strings for decimal columns)
|
||||
availableParticipants = availableParticipants.map((p: any) => ({
|
||||
...p,
|
||||
expectedValue: parseFloat(p.expectedValue) || 0,
|
||||
}));
|
||||
}
|
||||
|
||||
// Load user's team queue if they have a team
|
||||
|
|
@ -1054,7 +1060,7 @@ export default function DraftRoom() {
|
|||
{participant.sport.name}
|
||||
</td>
|
||||
<td className="p-3 text-right font-mono font-semibold">
|
||||
{participant.expectedValue}
|
||||
{participant.expectedValue ? participant.expectedValue.toFixed(1) : "—"}
|
||||
</td>
|
||||
<td className="p-3 text-right">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ interface ExportData {
|
|||
name: string;
|
||||
shortName: string | null;
|
||||
externalId: string | null;
|
||||
expectedValue: number;
|
||||
expectedValue: number | string;
|
||||
}>;
|
||||
seasonTemplates: Array<{
|
||||
name: string;
|
||||
|
|
@ -264,7 +264,7 @@ export async function importSportsDataFromJSON(
|
|||
name: participant.name,
|
||||
shortName: participant.shortName,
|
||||
externalId: participant.externalId,
|
||||
expectedValue: participant.expectedValue ?? 0,
|
||||
expectedValue: String(participant.expectedValue ?? 0),
|
||||
});
|
||||
created++;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export const participants = pgTable("participants", {
|
|||
name: varchar("name", { length: 255 }).notNull(),
|
||||
shortName: varchar("short_name", { length: 100 }),
|
||||
externalId: varchar("external_id", { length: 255 }),
|
||||
expectedValue: integer("expected_value").notNull().default(0),
|
||||
expectedValue: decimal("expected_value", { precision: 10, scale: 2 }).notNull().default("0"),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
|
|
|||
1
drizzle/0029_change_participant_ev_to_decimal.sql
Normal file
1
drizzle/0029_change_participant_ev_to_decimal.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "participants" ALTER COLUMN "expected_value" SET DATA TYPE numeric(10, 2) USING "expected_value"::numeric(10, 2);
|
||||
|
|
@ -204,6 +204,13 @@
|
|||
"when": 1763280000000,
|
||||
"tag": "0028_add_tournament_groups",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 29,
|
||||
"version": "7",
|
||||
"when": 1763281000000,
|
||||
"tag": "0029_change_participant_ev_to_decimal",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue