import { Injectable, Logger } from "@nestjs/common"; import { PrismaService } from "../../../database/prisma.service"; /** * Spor Toto Analitik Servisi * - Havuz dağılım hesabı (%25/%20/%20/%35) * - Expected Value (EV) hesabı * - Devir geçmişi ve trend analizi */ @Injectable() export class TotoAnalyticsService { private readonly logger = new Logger(TotoAnalyticsService.name); constructor(private readonly prisma: PrismaService) {} /** * Havuz dağılımını hesapla * Spor Toto havuz dağılımı: * %35 → 15 bilen * %20 → 14 bilen * %20 → 13 bilen * %25 → 12 bilen */ calculatePoolDistribution(totalPool: number): { pool15: number; pool14: number; pool13: number; pool12: number; } { return { pool15: totalPool * 0.35, pool14: totalPool * 0.2, pool13: totalPool * 0.2, pool12: totalPool * 0.25, }; } /** * Expected Value hesaplama * EV = (Kazanma Olasılığı × Ödül) - Maliyet * * 15 maçın tamamını bilme olasılığı (hepsi tek tahmin): * P = (1/3)^15 ≈ 1/14,348,907 */ calculateEV( poolTotal: number, rolloverAmount: number, columnCost: number, columnCount: number, ): { totalPool: number; pool15: number; probWin15: number; ev15: number; totalCost: number; netEV: number; } { const effectivePool = poolTotal + rolloverAmount; const distribution = this.calculatePoolDistribution(effectivePool); // Basit olasılık: 1/3^15 (her maç bağımsız, 3 sonuç) const probSingleColumn = 1 / Math.pow(3, 15); // ~6.97e-8 const probWin15 = 1 - Math.pow(1 - probSingleColumn, columnCount); const totalCost = columnCost * columnCount; const ev15 = probWin15 * distribution.pool15 - totalCost; return { totalPool: effectivePool, pool15: distribution.pool15, probWin15, ev15, totalCost, netEV: ev15, }; } /** * Devir geçmişi ve trend analizi */ async getRolloverHistory(limit = 10): Promise<{ history: Array<{ gameCycleNo: number; programName: string | null; poolTotal: number | null; rolloverAmount: number | null; winners15: number; prize15: number | null; }>; averageRollover: number; consecutiveRollovers: number; }> { const bulletins = await this.prisma.totoBulletin.findMany({ where: { status: "COMPLETED" }, orderBy: { gameCycleNo: "desc" }, take: limit, include: { result: true }, }); const history = bulletins.map((b) => ({ gameCycleNo: b.gameCycleNo, programName: b.programName, poolTotal: b.poolTotal, rolloverAmount: b.rolloverAmount, winners15: b.result?.winners15 ?? 0, prize15: b.result?.prize15 ?? null, })); // Ortalama devir miktarı const rollovers = history .map((h) => h.rolloverAmount ?? 0) .filter((r) => r > 0); const averageRollover = rollovers.length > 0 ? rollovers.reduce((a, b) => a + b, 0) / rollovers.length : 0; // Ardışık devir sayısı (son kaç haftadır devir var) let consecutiveRollovers = 0; for (const h of history) { if (h.winners15 === 0) { consecutiveRollovers++; } else { break; } } return { history, averageRollover, consecutiveRollovers }; } /** * Bülten istatistikleri */ async getBulletinStats(bulletinId: string): Promise<{ poolDistribution: { pool15: number; pool14: number; pool13: number; pool12: number; } | null; ev: { totalPool: number; pool15: number; probWin15: number; ev15: number; totalCost: number; netEV: number; } | null; rolloverInfo: { averageRollover: number; consecutiveRollovers: number }; }> { const bulletin = await this.prisma.totoBulletin.findUnique({ where: { id: bulletinId }, }); if (!bulletin) { return { poolDistribution: null, ev: null, rolloverInfo: { averageRollover: 0, consecutiveRollovers: 0 }, }; } const poolDistribution = bulletin.poolTotal ? this.calculatePoolDistribution( bulletin.poolTotal + (bulletin.rolloverAmount ?? 0), ) : null; const ev = bulletin.poolTotal != null ? this.calculateEV( bulletin.poolTotal, bulletin.rolloverAmount ?? 0, 1, // birim fiyat 1, // tek kolon bazında ) : null; const rolloverInfo = await this.getRolloverHistory(20); return { poolDistribution, ev, rolloverInfo: { averageRollover: rolloverInfo.averageRollover, consecutiveRollovers: rolloverInfo.consecutiveRollovers, }, }; } }