Files
iddaai-be/src/modules/spor-toto/services/toto-analytics.service.ts
T
2026-04-16 17:21:48 +03:00

191 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
},
};
}
}