|
|
|
|
@ -21,150 +21,163 @@ import kotlin.math.round |
|
|
|
|
*/ |
|
|
|
|
class CashGameOptimalDurationCalculator { |
|
|
|
|
|
|
|
|
|
companion object { |
|
|
|
|
|
|
|
|
|
private const val bucket = 60 * 60 * 1000L // the duration of bucket |
|
|
|
|
private const val bucketInterval = 4 // number of duration tests inside the bucket to find the best duration |
|
|
|
|
private const val minimumValidityCount = 10 // the number of sessions inside a bucket to start having a reasonable average |
|
|
|
|
private const val intervalValidity = 3 // the minimum number of unit between the shortest & longest valid buckets |
|
|
|
|
private const val polynomialDegree = 7 // the degree of the computed polynomial |
|
|
|
|
|
|
|
|
|
/*** |
|
|
|
|
* Starts the calculation |
|
|
|
|
* [isLive] is a boolean to indicate if we're looking at live or online games |
|
|
|
|
* return a duration or null if it could not be computed |
|
|
|
|
*/ |
|
|
|
|
fun start(isLive: Boolean): Double? { |
|
|
|
|
|
|
|
|
|
val realm = Realm.getDefaultInstance() |
|
|
|
|
|
|
|
|
|
val query = Query().add(QueryCondition.IsCash) // cash game |
|
|
|
|
query.add(if (isLive) { QueryCondition.IsLive } else { QueryCondition.IsOnline }) // live / online |
|
|
|
|
query.add(QueryCondition.EndDateNotNull) // ended |
|
|
|
|
query.add(QueryCondition.BiggestBetNotNull) // has BB value |
|
|
|
|
|
|
|
|
|
val sessions = query.queryWith(realm.where(Session::class.java)).findAll() |
|
|
|
|
val sessionsByDuration = sessions.groupBy { |
|
|
|
|
val dur = round((it.netDuration / bucket).toDouble()) * bucket |
|
|
|
|
companion object { |
|
|
|
|
|
|
|
|
|
private const val bucket = 60 * 60 * 1000L // the duration of bucket |
|
|
|
|
private const val bucketInterval = |
|
|
|
|
4 // number of duration tests inside the bucket to find the best duration |
|
|
|
|
private const val minimumValidityCount = |
|
|
|
|
10 // the number of sessions inside a bucket to start having a reasonable average |
|
|
|
|
private const val intervalValidity = |
|
|
|
|
3 // the minimum number of unit between the shortest & longest valid buckets |
|
|
|
|
private const val polynomialDegree = 7 // the degree of the computed polynomial |
|
|
|
|
|
|
|
|
|
/*** |
|
|
|
|
* Starts the calculation |
|
|
|
|
* [isLive] is a boolean to indicate if we're looking at live or online games |
|
|
|
|
* return a duration or null if it could not be computed |
|
|
|
|
*/ |
|
|
|
|
fun start(isLive: Boolean): Double? { |
|
|
|
|
|
|
|
|
|
val realm = Realm.getDefaultInstance() |
|
|
|
|
|
|
|
|
|
val query = Query().add(QueryCondition.IsCash) // cash game |
|
|
|
|
query.add( |
|
|
|
|
if (isLive) { |
|
|
|
|
QueryCondition.IsLive |
|
|
|
|
} else { |
|
|
|
|
QueryCondition.IsOnline |
|
|
|
|
} |
|
|
|
|
) // live / online |
|
|
|
|
query.add(QueryCondition.EndDateNotNull) // ended |
|
|
|
|
query.add(QueryCondition.BiggestBetNotNull) // has BB value |
|
|
|
|
|
|
|
|
|
val sessions = query.queryWith(realm.where(Session::class.java)).findAll() |
|
|
|
|
val sessionsByDuration = sessions.groupBy { |
|
|
|
|
val dur = round((it.netDuration / bucket).toDouble()) * bucket |
|
|
|
|
// Timber.d("Stop notif > key: $dur") |
|
|
|
|
dur |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// define validity interval |
|
|
|
|
var start: Double? = null |
|
|
|
|
var end: Double? = null |
|
|
|
|
var validBuckets = 0 |
|
|
|
|
|
|
|
|
|
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() |
|
|
|
|
Timber.d("Stop notif > keys: $hkeys ") |
|
|
|
|
for (key in sessionsByDuration.keys.sorted()) { |
|
|
|
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
|
|
|
|
if (start == null && sessionCount >= minimumValidityCount) { |
|
|
|
|
start = key |
|
|
|
|
} |
|
|
|
|
if (sessionCount >= minimumValidityCount) { |
|
|
|
|
end = key |
|
|
|
|
validBuckets++ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Timber.d("Stop notif > validBuckets: $validBuckets ") |
|
|
|
|
if (!(start != null && end != null && (end - start) >= intervalValidity)) { |
|
|
|
|
Timber.d("Stop notif > invalid setup: $start / $end ") |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// define if we have enough sessions |
|
|
|
|
if (sessions.size < 50) { |
|
|
|
|
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
val options = Calculator.Options() |
|
|
|
|
options.query = query |
|
|
|
|
val report = Calculator.computeStats(realm, options) |
|
|
|
|
val stdBB = report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value |
|
|
|
|
|
|
|
|
|
val p = polynomialRegression(sessions, stdBB) |
|
|
|
|
|
|
|
|
|
var bestAverage = 0.0 |
|
|
|
|
var bestHourlyRate = 0.0 |
|
|
|
|
var bestDuration = 0.0 |
|
|
|
|
var maxDuration = 0.0 |
|
|
|
|
|
|
|
|
|
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted() |
|
|
|
|
|
|
|
|
|
for (key in keys) { |
|
|
|
|
|
|
|
|
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
|
|
|
|
|
|
|
|
|
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid |
|
|
|
|
|
|
|
|
|
for (i in 0 until bucketInterval) { |
|
|
|
|
|
|
|
|
|
val duration = key + i * bucket / bucketInterval |
|
|
|
|
|
|
|
|
|
val averageResult = getBB(duration, p) |
|
|
|
|
val hourly = averageResult / duration |
|
|
|
|
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { |
|
|
|
|
bestAverage = averageResult |
|
|
|
|
bestDuration = duration |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (duration > 0 && hourly > bestHourlyRate) { |
|
|
|
|
bestHourlyRate = hourly |
|
|
|
|
} |
|
|
|
|
if (duration > maxDuration){ |
|
|
|
|
maxDuration = duration |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (bestDuration > 0.0) { |
|
|
|
|
return bestDuration |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Timber.d("Stop notif > not found, best duration: $bestDuration") |
|
|
|
|
realm.close() |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { |
|
|
|
|
var y = 0.0 |
|
|
|
|
for (i in polynomial.indices) { |
|
|
|
|
y += polynomial[i] * netDuration.pow(i) |
|
|
|
|
} |
|
|
|
|
return y |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun polynomialRegression(sessions: List<Session>, bbStandardDeviation: Double?): DoubleArray { |
|
|
|
|
|
|
|
|
|
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE |
|
|
|
|
|
|
|
|
|
val points = WeightedObservedPoints() |
|
|
|
|
val now = Date().time |
|
|
|
|
|
|
|
|
|
sessions.forEach { |
|
|
|
|
var weight = 5.0 |
|
|
|
|
|
|
|
|
|
val endTime = it.endDate?.time ?: 0L |
|
|
|
|
|
|
|
|
|
val age = now - endTime |
|
|
|
|
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point |
|
|
|
|
weight -= 1.0 |
|
|
|
|
} |
|
|
|
|
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points |
|
|
|
|
weight -= 3.0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
points.add(weight, it.netDuration.toDouble(), it.bbNet) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
dur |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// define validity interval |
|
|
|
|
var start: Double? = null |
|
|
|
|
var end: Double? = null |
|
|
|
|
var validBuckets = 0 |
|
|
|
|
|
|
|
|
|
val hkeys = sessionsByDuration.keys.map { it / 3600 / 1000.0 }.sorted() |
|
|
|
|
Timber.d("Stop notif > keys: $hkeys ") |
|
|
|
|
for (key in sessionsByDuration.keys.sorted()) { |
|
|
|
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
|
|
|
|
if (start == null && sessionCount >= minimumValidityCount) { |
|
|
|
|
start = key |
|
|
|
|
} |
|
|
|
|
if (sessionCount >= minimumValidityCount) { |
|
|
|
|
end = key |
|
|
|
|
validBuckets++ |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
Timber.d("Stop notif > validBuckets: $validBuckets ") |
|
|
|
|
if (!(start != null && end != null && (end - start) >= intervalValidity)) { |
|
|
|
|
Timber.d("Stop notif > invalid setup: $start / $end ") |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// define if we have enough sessions |
|
|
|
|
if (sessions.size < 50) { |
|
|
|
|
Timber.d("Stop notif > not enough sessions: ${sessions.size} ") |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
val options = Calculator.Options() |
|
|
|
|
options.query = query |
|
|
|
|
val report = Calculator.computeStats(realm, options) |
|
|
|
|
val stdBB = |
|
|
|
|
report.results.firstOrNull()?.computedStat(Stat.STANDARD_DEVIATION_BB)?.value |
|
|
|
|
|
|
|
|
|
val p = polynomialRegression(sessions, stdBB) |
|
|
|
|
|
|
|
|
|
var bestAverage = 0.0 |
|
|
|
|
var bestHourlyRate = 0.0 |
|
|
|
|
var bestDuration = 0.0 |
|
|
|
|
var maxDuration = 0.0 |
|
|
|
|
|
|
|
|
|
val keys = sessionsByDuration.keys.filter { it >= start && it <= end }.sorted() |
|
|
|
|
|
|
|
|
|
for (key in keys) { |
|
|
|
|
|
|
|
|
|
val sessionCount = sessionsByDuration[key]?.size ?: 0 |
|
|
|
|
|
|
|
|
|
if (sessionCount < minimumValidityCount / 2) continue // if too few sessions we don't consider the duration valid |
|
|
|
|
|
|
|
|
|
for (i in 0 until bucketInterval) { |
|
|
|
|
|
|
|
|
|
val duration = key + i * bucket / bucketInterval |
|
|
|
|
|
|
|
|
|
val averageResult = getBB(duration, p) |
|
|
|
|
val hourly = averageResult / duration |
|
|
|
|
if (averageResult > bestAverage && hourly > 2 / 3 * bestHourlyRate) { |
|
|
|
|
bestAverage = averageResult |
|
|
|
|
bestDuration = duration |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (duration > 0 && hourly > bestHourlyRate) { |
|
|
|
|
bestHourlyRate = hourly |
|
|
|
|
} |
|
|
|
|
if (duration > maxDuration) { |
|
|
|
|
maxDuration = duration |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (bestDuration > 0.0) { |
|
|
|
|
return bestDuration |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
Timber.d("Stop notif > not found, best duration: $bestDuration") |
|
|
|
|
realm.close() |
|
|
|
|
return null |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun getBB(netDuration: Double, polynomial: DoubleArray): Double { |
|
|
|
|
var y = 0.0 |
|
|
|
|
for (i in polynomial.indices) { |
|
|
|
|
y += polynomial[i] * netDuration.pow(i) |
|
|
|
|
} |
|
|
|
|
return y |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private fun polynomialRegression( |
|
|
|
|
sessions: List<Session>, |
|
|
|
|
bbStandardDeviation: Double? |
|
|
|
|
): DoubleArray { |
|
|
|
|
|
|
|
|
|
val stdBB = bbStandardDeviation ?: Double.MAX_VALUE |
|
|
|
|
|
|
|
|
|
val points = WeightedObservedPoints() |
|
|
|
|
val now = Date().time |
|
|
|
|
|
|
|
|
|
sessions.forEach { |
|
|
|
|
var weight = 5.0 |
|
|
|
|
|
|
|
|
|
val endTime = it.endDate?.time ?: 0L |
|
|
|
|
|
|
|
|
|
val age = now - endTime |
|
|
|
|
if (age > 2 * 365 * 24 * 3600 * 1000L) { // if more than 2 years loses 1 point |
|
|
|
|
weight -= 1.0 |
|
|
|
|
} |
|
|
|
|
if (it.bbNet > 2 * stdBB) { // if very big result loses 3 points |
|
|
|
|
weight -= 3.0 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
points.add(weight, it.netDuration.toDouble(), it.bbNet) |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// polynomial of 7 degree, same as iOS |
|
|
|
|
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) |
|
|
|
|
} |
|
|
|
|
// polynomial of 7 degree, same as iOS |
|
|
|
|
return PolynomialCurveFitter.create(polynomialDegree).fit(points.toList()) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
} |