257 lines
7.7 KiB
TypeScript
257 lines
7.7 KiB
TypeScript
|
|
/**
|
||
|
|
* Model for EV (Expected Value) Snapshots
|
||
|
|
*
|
||
|
|
* Historical record of participant EVs and team projected points over time.
|
||
|
|
* One snapshot per participant (or team) per sport season per day — upsert on conflict.
|
||
|
|
* Snapshots are taken directly from simulation output to avoid partial-capture issues.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { database } from "~/database/context";
|
||
|
|
import { participantEvSnapshots, teamEvSnapshots } from "~/database/schema";
|
||
|
|
import { eq, and, asc } from "drizzle-orm";
|
||
|
|
|
||
|
|
// ─── Participant EV Snapshots ─────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export interface ParticipantEvSnapshotInput {
|
||
|
|
participantId: string;
|
||
|
|
sportsSeasonId: string;
|
||
|
|
snapshotDate: string; // YYYY-MM-DD
|
||
|
|
probFirst: number;
|
||
|
|
probSecond: number;
|
||
|
|
probThird: number;
|
||
|
|
probFourth: number;
|
||
|
|
probFifth: number;
|
||
|
|
probSixth: number;
|
||
|
|
probSeventh: number;
|
||
|
|
probEighth: number;
|
||
|
|
calculatedEV: number;
|
||
|
|
source: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface ParticipantEvHistoryPoint {
|
||
|
|
snapshotDate: string;
|
||
|
|
calculatedEV: number;
|
||
|
|
probFirst: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upsert a single participant EV snapshot.
|
||
|
|
* If a snapshot already exists for this (participantId, sportsSeasonId, snapshotDate),
|
||
|
|
* it will be overwritten with the new values.
|
||
|
|
*/
|
||
|
|
export async function upsertParticipantEvSnapshot(
|
||
|
|
input: ParticipantEvSnapshotInput
|
||
|
|
): Promise<void> {
|
||
|
|
const db = database();
|
||
|
|
const now = new Date();
|
||
|
|
|
||
|
|
await db
|
||
|
|
.insert(participantEvSnapshots)
|
||
|
|
.values({
|
||
|
|
participantId: input.participantId,
|
||
|
|
sportsSeasonId: input.sportsSeasonId,
|
||
|
|
snapshotDate: input.snapshotDate,
|
||
|
|
probFirst: input.probFirst.toString(),
|
||
|
|
probSecond: input.probSecond.toString(),
|
||
|
|
probThird: input.probThird.toString(),
|
||
|
|
probFourth: input.probFourth.toString(),
|
||
|
|
probFifth: input.probFifth.toString(),
|
||
|
|
probSixth: input.probSixth.toString(),
|
||
|
|
probSeventh: input.probSeventh.toString(),
|
||
|
|
probEighth: input.probEighth.toString(),
|
||
|
|
calculatedEV: input.calculatedEV.toString(),
|
||
|
|
source: input.source,
|
||
|
|
createdAt: now,
|
||
|
|
updatedAt: now,
|
||
|
|
})
|
||
|
|
.onConflictDoUpdate({
|
||
|
|
target: [
|
||
|
|
participantEvSnapshots.participantId,
|
||
|
|
participantEvSnapshots.sportsSeasonId,
|
||
|
|
participantEvSnapshots.snapshotDate,
|
||
|
|
],
|
||
|
|
set: {
|
||
|
|
probFirst: input.probFirst.toString(),
|
||
|
|
probSecond: input.probSecond.toString(),
|
||
|
|
probThird: input.probThird.toString(),
|
||
|
|
probFourth: input.probFourth.toString(),
|
||
|
|
probFifth: input.probFifth.toString(),
|
||
|
|
probSixth: input.probSixth.toString(),
|
||
|
|
probSeventh: input.probSeventh.toString(),
|
||
|
|
probEighth: input.probEighth.toString(),
|
||
|
|
calculatedEV: input.calculatedEV.toString(),
|
||
|
|
source: input.source,
|
||
|
|
updatedAt: now,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Batch upsert participant EV snapshots within a single transaction.
|
||
|
|
* All snapshots succeed or all fail together.
|
||
|
|
*/
|
||
|
|
export async function batchUpsertParticipantEvSnapshots(
|
||
|
|
inputs: ParticipantEvSnapshotInput[]
|
||
|
|
): Promise<void> {
|
||
|
|
if (inputs.length === 0) return;
|
||
|
|
|
||
|
|
const db = database();
|
||
|
|
const now = new Date();
|
||
|
|
|
||
|
|
await db.transaction(async (tx) => {
|
||
|
|
for (const input of inputs) {
|
||
|
|
await tx
|
||
|
|
.insert(participantEvSnapshots)
|
||
|
|
.values({
|
||
|
|
participantId: input.participantId,
|
||
|
|
sportsSeasonId: input.sportsSeasonId,
|
||
|
|
snapshotDate: input.snapshotDate,
|
||
|
|
probFirst: input.probFirst.toString(),
|
||
|
|
probSecond: input.probSecond.toString(),
|
||
|
|
probThird: input.probThird.toString(),
|
||
|
|
probFourth: input.probFourth.toString(),
|
||
|
|
probFifth: input.probFifth.toString(),
|
||
|
|
probSixth: input.probSixth.toString(),
|
||
|
|
probSeventh: input.probSeventh.toString(),
|
||
|
|
probEighth: input.probEighth.toString(),
|
||
|
|
calculatedEV: input.calculatedEV.toString(),
|
||
|
|
source: input.source,
|
||
|
|
createdAt: now,
|
||
|
|
updatedAt: now,
|
||
|
|
})
|
||
|
|
.onConflictDoUpdate({
|
||
|
|
target: [
|
||
|
|
participantEvSnapshots.participantId,
|
||
|
|
participantEvSnapshots.sportsSeasonId,
|
||
|
|
participantEvSnapshots.snapshotDate,
|
||
|
|
],
|
||
|
|
set: {
|
||
|
|
probFirst: input.probFirst.toString(),
|
||
|
|
probSecond: input.probSecond.toString(),
|
||
|
|
probThird: input.probThird.toString(),
|
||
|
|
probFourth: input.probFourth.toString(),
|
||
|
|
probFifth: input.probFifth.toString(),
|
||
|
|
probSixth: input.probSixth.toString(),
|
||
|
|
probSeventh: input.probSeventh.toString(),
|
||
|
|
probEighth: input.probEighth.toString(),
|
||
|
|
calculatedEV: input.calculatedEV.toString(),
|
||
|
|
source: input.source,
|
||
|
|
updatedAt: now,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get EV history for a single participant in a sport season, ordered oldest first.
|
||
|
|
* Used for EV trend charts.
|
||
|
|
*/
|
||
|
|
export async function getParticipantEvHistory(
|
||
|
|
participantId: string,
|
||
|
|
sportsSeasonId: string
|
||
|
|
): Promise<ParticipantEvHistoryPoint[]> {
|
||
|
|
const db = database();
|
||
|
|
|
||
|
|
const rows = await db
|
||
|
|
.select({
|
||
|
|
snapshotDate: participantEvSnapshots.snapshotDate,
|
||
|
|
calculatedEV: participantEvSnapshots.calculatedEV,
|
||
|
|
probFirst: participantEvSnapshots.probFirst,
|
||
|
|
})
|
||
|
|
.from(participantEvSnapshots)
|
||
|
|
.where(
|
||
|
|
and(
|
||
|
|
eq(participantEvSnapshots.participantId, participantId),
|
||
|
|
eq(participantEvSnapshots.sportsSeasonId, sportsSeasonId)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
.orderBy(asc(participantEvSnapshots.snapshotDate));
|
||
|
|
|
||
|
|
return rows.map((r) => ({
|
||
|
|
snapshotDate: r.snapshotDate,
|
||
|
|
calculatedEV: parseFloat(r.calculatedEV),
|
||
|
|
probFirst: parseFloat(r.probFirst),
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── Team EV Snapshots ────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
export interface TeamEvSnapshotInput {
|
||
|
|
teamId: string;
|
||
|
|
seasonId: string;
|
||
|
|
snapshotDate: string; // YYYY-MM-DD
|
||
|
|
projectedPoints: number;
|
||
|
|
actualPoints: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface TeamEvHistoryPoint {
|
||
|
|
snapshotDate: string;
|
||
|
|
projectedPoints: number;
|
||
|
|
actualPoints: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Upsert a team EV snapshot for a given day.
|
||
|
|
*/
|
||
|
|
export async function upsertTeamEvSnapshot(
|
||
|
|
input: TeamEvSnapshotInput
|
||
|
|
): Promise<void> {
|
||
|
|
const db = database();
|
||
|
|
const now = new Date();
|
||
|
|
|
||
|
|
await db
|
||
|
|
.insert(teamEvSnapshots)
|
||
|
|
.values({
|
||
|
|
teamId: input.teamId,
|
||
|
|
seasonId: input.seasonId,
|
||
|
|
snapshotDate: input.snapshotDate,
|
||
|
|
projectedPoints: input.projectedPoints.toString(),
|
||
|
|
actualPoints: input.actualPoints.toString(),
|
||
|
|
createdAt: now,
|
||
|
|
})
|
||
|
|
.onConflictDoUpdate({
|
||
|
|
target: [
|
||
|
|
teamEvSnapshots.teamId,
|
||
|
|
teamEvSnapshots.seasonId,
|
||
|
|
teamEvSnapshots.snapshotDate,
|
||
|
|
],
|
||
|
|
set: {
|
||
|
|
projectedPoints: input.projectedPoints.toString(),
|
||
|
|
actualPoints: input.actualPoints.toString(),
|
||
|
|
},
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Get projected points history for a team in a fantasy season, ordered oldest first.
|
||
|
|
* Used for team trend charts.
|
||
|
|
*/
|
||
|
|
export async function getTeamEvHistory(
|
||
|
|
teamId: string,
|
||
|
|
seasonId: string
|
||
|
|
): Promise<TeamEvHistoryPoint[]> {
|
||
|
|
const db = database();
|
||
|
|
|
||
|
|
const rows = await db
|
||
|
|
.select({
|
||
|
|
snapshotDate: teamEvSnapshots.snapshotDate,
|
||
|
|
projectedPoints: teamEvSnapshots.projectedPoints,
|
||
|
|
actualPoints: teamEvSnapshots.actualPoints,
|
||
|
|
})
|
||
|
|
.from(teamEvSnapshots)
|
||
|
|
.where(
|
||
|
|
and(
|
||
|
|
eq(teamEvSnapshots.teamId, teamId),
|
||
|
|
eq(teamEvSnapshots.seasonId, seasonId)
|
||
|
|
)
|
||
|
|
)
|
||
|
|
.orderBy(asc(teamEvSnapshots.snapshotDate));
|
||
|
|
|
||
|
|
return rows.map((r) => ({
|
||
|
|
snapshotDate: r.snapshotDate,
|
||
|
|
projectedPoints: parseFloat(r.projectedPoints),
|
||
|
|
actualPoints: parseFloat(r.actualPoints),
|
||
|
|
}));
|
||
|
|
}
|