import { eq, and } from "drizzle-orm"; import { database } from "~/database/context"; import * as schema from "~/database/schema"; export type PlayoffMatchOdds = typeof schema.playoffMatchOdds.$inferSelect; export type NewPlayoffMatchOdds = typeof schema.playoffMatchOdds.$inferInsert; // ─── Pure helpers ──────────────────────────────────────────────────────────── /** * Convert American moneyline odds to raw implied probability (includes vig). * * Positive odds (e.g. +150): probability = 100 / (odds + 100) * Negative odds (e.g. -110): probability = |odds| / (|odds| + 100) * * Returns a value in [0, 1]. */ export function americanToImpliedProbability(moneyline: number): number { if (moneyline === 0) return 0; if (moneyline > 0) { return 100 / (moneyline + 100); } const abs = Math.abs(moneyline); return abs / (abs + 100); } /** * Convert an implied probability (0–1) to American moneyline odds. * * prob >= 0.5 → negative (favourite): -(prob / (1 - prob)) * 100 * prob < 0.5 → positive (underdog): ((1 - prob) / prob) * 100 * * Returns rounded integer. */ export function impliedProbabilityToAmerican(probability: number): number { if (probability <= 0 || probability >= 1) { throw new RangeError("Probability must be strictly between 0 and 1"); } if (probability >= 0.5) { return Math.round(-(probability / (1 - probability)) * 100); } return Math.round(((1 - probability) / probability) * 100); } /** * Remove the vig from a two-way market to get fair probabilities summing to 1. * * Returns [fairP1, fairP2]. */ export function normalizeOdds( moneyline1: number, moneyline2: number ): [number, number] { const raw1 = americanToImpliedProbability(moneyline1); const raw2 = americanToImpliedProbability(moneyline2); const total = raw1 + raw2; return [raw1 / total, raw2 / total]; } // ─── DB operations ─────────────────────────────────────────────────────────── export async function findOddsByMatchId( playoffMatchId: string ): Promise { const db = database(); return await db.query.playoffMatchOdds.findMany({ where: eq(schema.playoffMatchOdds.playoffMatchId, playoffMatchId), with: { participant: true }, }); } export async function findOddsForParticipant( playoffMatchId: string, participantId: string ): Promise { const db = database(); return await db.query.playoffMatchOdds.findFirst({ where: and( eq(schema.playoffMatchOdds.playoffMatchId, playoffMatchId), eq(schema.playoffMatchOdds.participantId, participantId) ), }); } /** * Insert or update odds for a participant in a matchup. * One row per (playoffMatchId, participantId) — overwritten on each call. * Implied probability is computed automatically from moneylineOdds. */ export async function upsertMatchOdds( playoffMatchId: string, participantId: string, data: { moneylineOdds: number; oddsSource?: string; } ): Promise { const db = database(); const impliedProbability = americanToImpliedProbability(data.moneylineOdds).toFixed(4); const payload = { ...data, impliedProbability }; const existing = await findOddsForParticipant(playoffMatchId, participantId); if (existing) { const [updated] = await db .update(schema.playoffMatchOdds) .set({ ...payload, recordedAt: new Date(), updatedAt: new Date() }) .where(eq(schema.playoffMatchOdds.id, existing.id)) .returning(); return updated; } const [created] = await db .insert(schema.playoffMatchOdds) .values({ playoffMatchId, participantId, ...payload }) .returning(); return created; } export async function deleteOddsByMatchId(playoffMatchId: string): Promise { const db = database(); await db .delete(schema.playoffMatchOdds) .where(eq(schema.playoffMatchOdds.playoffMatchId, playoffMatchId)); } export async function deleteOddsForParticipant( playoffMatchId: string, participantId: string ): Promise { const db = database(); await db .delete(schema.playoffMatchOdds) .where( and( eq(schema.playoffMatchOdds.playoffMatchId, playoffMatchId), eq(schema.playoffMatchOdds.participantId, participantId) ) ); }