191 lines
4.7 KiB
TypeScript
191 lines
4.7 KiB
TypeScript
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,
|
||
},
|
||
};
|
||
}
|
||
}
|