This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user