Files
tipper/daemon/handball.go
2026-07-21 05:29:37 +03:00

1166 lines
29 KiB
Go
Raw Permalink 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.
package daemon
import (
"fmt"
"math"
"strconv"
"time"
"gordenko.dev/dima/flashscore"
fsmodel "gordenko.dev/dima/flashscore/model"
"gordenko.dev/dima/onexbet"
"gordenko.dev/dima/tipper/model"
)
var (
p1TimeFrame = timeFrame{
Since: 5 * 60,
Until: 22 * 60,
}
ftTimeFrame = timeFrame{
Since: 10 * 60,
Until: 52 * 60,
}
ftOverTimeFrame = timeFrame{
Since: 10 * 60,
Until: 52 * 60,
}
)
type timeFrame struct {
Since int
Until int
}
type HandballStrategyOptions struct {
ID int64
NotifyInTelegram bool
TotalDiff float64
FixateGamePaceEveryNMinutes int
Disable bool
}
// FIX - calculator, который будет проверять флаг isMatchScoreLoaded
func HandballHistoryProcessor(match *TeamMatch) {
// Делаем reset
match.HomeGoalsPerMinute = 0
match.AwayGoalsPerMinute = 0
match.H2HGoalsPerMinute = 0
match.BasicTotal = 0
/*
match.BasicOverGoalsPerMinute = 0
match.BasicUnderGoalsPerMinute = 0
match.TipTotalOver = 0
match.TipTotalUnder = 0
*/
// Считаем
if len(match.HomeTeamMatches) > 0 {
match.HomeGoalsPerMinute = caclulateGoalsPerMinuteOfLastNMatches(match.HomeTeamMatches, 5)
}
if len(match.AwayTeamMatches) > 0 {
match.AwayGoalsPerMinute = caclulateGoalsPerMinuteOfLastNMatches(match.AwayTeamMatches, 5)
}
if len(match.H2H) > 0 {
match.H2HGoalsPerMinute = caclulateGoalsPerMinuteOfLastNMatches(match.H2H, 5)
}
// Basic Totals
match.BasicTotal = findBasicTotal(match.Odds.FullTimeTotal)
match.H1BasicTotal = findBasicTotal(match.Odds.H1Total)
}
func findBasicTotal(totals []flashscore.Total) (_ float64) {
var (
minDif float64
bestOffer flashscore.Total
isBestOfferFound bool
)
// Находим средний тотал - это когда разница между коэффициентами минимальна
for _, total := range totals {
if len(total.Offers) > 0 {
var dif float64
for _, offer := range total.Offers {
dif += math.Abs(offer.Over - offer.Under)
}
// Вычисляем для каждого тотала среднюю (среди всех букмекеров) разницу
// между Over и Under коэф.
dif = dif / float64(len(total.Offers))
if (!isBestOfferFound) || (dif < minDif) {
minDif = dif
bestOffer = total
isBestOfferFound = true
}
}
}
if isBestOfferFound {
basicTotal, err := strconv.ParseFloat(bestOffer.Total, 64)
if err != nil {
return
}
return basicTotal
}
return
}
const (
Gte = 1 // >=
Lte = 2 // <=
)
func checkGamePace(basicGoalsPerMinute float64, match *TeamMatch, upd onexbet.TeamLiveMatchData, fixateGamePaceEveryNMinutes int, moreOrLess int) (comment string, isPassed bool) {
currentGoalsPerMinute := (float64(upd.HomeGoals+upd.AwayGoals) / float64(upd.CurrentTime)) * 60
gamePaceFrame := upd.CurrentTime / (fixateGamePaceEveryNMinutes * 60)
if gamePaceFrame == 0 {
match.GamePace = currentGoalsPerMinute
} else {
if gamePaceFrame != match.GamePaceFrame {
match.GamePaceFrame = gamePaceFrame
match.GamePace = currentGoalsPerMinute
}
}
var timeStr string
if match.GamePaceFrame == 0 {
timeStr = fmt.Sprintf("%s", time.Duration(upd.CurrentTime)*time.Second)
} else {
timeStr = fmt.Sprintf("%d мин", match.GamePaceFrame*fixateGamePaceEveryNMinutes)
}
switch moreOrLess {
case Gte:
// Темп должен быть >=
if match.GamePace < basicGoalsPerMinute {
comment = fmt.Sprintf("Текущий темп %.2f (за %s) < базового темпа %.2f",
match.GamePace, timeStr, basicGoalsPerMinute)
return comment, false
}
/*
if len(match.H2H) > 0 {
// H2H матчи необязательныеs
if match.GamePace < match.H2HGoalsPerMinute {
comment = fmt.Sprintf("Текущий темп %.2f (за %s) < H2H темпа %.2f",
match.GamePace, timeStr, match.H2HGoalsPerMinute)
return comment, false
}
}
*/
case Lte:
// Темп должен быть <=
if match.GamePace > basicGoalsPerMinute {
comment = fmt.Sprintf("Текущий темп %.2f (за %s) > базового темпа %.2f",
match.GamePace, timeStr, basicGoalsPerMinute)
return comment, false
}
/*
if len(match.H2H) > 0 {
// H2H матчи необязательныеs
if match.GamePace > match.H2HGoalsPerMinute {
comment = fmt.Sprintf("Текущий темп %.2f (за %s) > H2H темпа %.2f",
match.GamePace, timeStr, match.H2HGoalsPerMinute)
return comment, false
}
}
*/
default:
panic(fmt.Sprintf("Bug: unknown moreOrLess: %d", moreOrLess))
}
return "", true
}
func correctTotal(total float64, roundDir int) float64 {
_, frac := math.Modf(total)
eps := 0.001
if math.Abs(frac-0.25) < eps {
// Если тотал завершается на 0.25 - округляем в ту или иную сторону
switch roundDir {
case roundUp:
total = math.Floor(total) + 0.5
case roundDown:
total = math.Floor(total)
}
} else if frac < eps {
// Круглый тотал: 24, 60
switch roundDir {
case roundUp:
total = total + 0.5
case roundDown:
total = total - 0.5
}
}
return total
}
type HandballFTOver struct {
id int64
notifyInTelegram bool
totalDiff float64
fixateGamePaceEveryNMinutes int
isDisabled bool
}
func NewHandballFTOver(opt HandballStrategyOptions) HandballFTOver {
if opt.ID == 0 {
panic("StrategyID not defined")
}
s := HandballFTOver{}
s.id = opt.ID
s.notifyInTelegram = opt.NotifyInTelegram
s.totalDiff = opt.TotalDiff
s.fixateGamePaceEveryNMinutes = opt.FixateGamePaceEveryNMinutes
s.isDisabled = opt.Disable
return s
}
func (s HandballFTOver) ID() int64 {
return s.id
}
func (s HandballFTOver) ShortName() string {
return fmt.Sprintf("#%d FT Over (-%v, %d min)", s.id, s.totalDiff, s.fixateGamePaceEveryNMinutes)
}
func (s HandballFTOver) IsNotifyInTelegram() bool {
return s.notifyInTelegram
}
func (s HandballFTOver) GetTelegramTipPattern() string {
return `Сигнал # %d.
Алгоритм %d. Тотал Больше в матче.
%s. %s
%s - %s
Точка входа %.1f кф. %.3f
Рекомендация: тотал больше %.1f и ниже (%.1f; %.1f)`
}
func (s HandballFTOver) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
return true
}
func caclulateGoalsPerMinuteOfLastNMatches(matches []fsmodel.TeamMatch, n int) (goalsPerMinute float64) {
if len(matches) < n {
n = len(matches)
}
count := 0
for _, m := range matches[:n] {
if m.IsMatchScoreLoaded {
goalsPerMinute += float64(m.HomeGoals+m.AwayGoals) / 60
count++
}
}
if count > 0 {
return goalsPerMinute / float64(count)
}
return
}
func (s HandballFTOver) IsInteresting(match *TeamMatch) (notes interface{}, _ bool) {
if s.isDisabled {
return
}
eps := 0.01
// Проверки на то, что полные данные
if match.BasicTotal < 1 {
return nil, false
}
tipTotal := match.BasicTotal - s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundDown)
basicGoalsPerMinute := tipTotal / 60
if math.Abs(match.H2HGoalsPerMinute) > eps {
// Если команды между собой забивают меньше чем дает бук (с учетом погрешности)
// - матч не интересен
if match.H2HGoalsPerMinute < basicGoalsPerMinute {
return
}
} else {
if math.Abs(match.HomeGoalsPerMinute) < eps {
return
}
if math.Abs(match.AwayGoalsPerMinute) < eps {
return
}
// Считаем средний темп с которым играют обе команды
avgGoalsPerMinute := (match.HomeGoalsPerMinute + match.AwayGoalsPerMinute) / 2
// Если в среднем команды забивают меньше чем дает бук (с учетом погрешности)
// - матч не интересен
if avgGoalsPerMinute < basicGoalsPerMinute {
return
}
}
return nil, true
}
func (s HandballFTOver) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (comment string, _ *BetDetails, _ TipCode) {
if upd.CurrentTime == 1800 {
return "в перерыве не ставим (багфикс)", nil, Waiting
}
if upd.CurrentTime < ftOverTimeFrame.Since {
return "еще рано", nil, Waiting
}
if upd.CurrentTime > ftOverTimeFrame.Until {
return "время вышло", nil, Unwatch
}
tipTotal := match.BasicTotal - s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundDown)
basicGoalsPerMinute := tipTotal / 60
tipTotalStr := fmt.Sprintf("%.1f", tipTotal)
if float64(upd.HomeGoals+upd.AwayGoals) > tipTotal {
return "Тотал пробит", nil, Unwatch
}
// Текущий темп матча опережает рассчитанный на истории
// проверяем коэффициенты.
var isPassed bool
comment, isPassed = checkGamePace(basicGoalsPerMinute, match, upd, s.fixateGamePaceEveryNMinutes, Gte)
if !isPassed {
return comment, nil, Waiting
}
// Задача - найти минимальный тотал <= tipTotal, чтобы коэфф. >= 1.5
tipTotal = tipTotal + 0.01 // Для удобного сравнивания float
//fmt.Printf("tipTotal: %.2f\n", tipTotal)
var (
minTotalValue float64
bestOffer onexbet.ParamOffer // строковое представление тотала
)
for _, offer := range upd.Total.Over {
if offer.Price < 1.5 {
continue
}
totalValue, err := strconv.ParseFloat(offer.Param, 64)
if err != nil {
continue
}
if totalValue > tipTotal {
//fmt.Printf("totalValue > tipTotal: %.2f > %.2f\n", totalValue, tipTotal)
continue
}
//fmt.Printf("Best: totalValue: %2.f (%s), price: %.2f\n", totalValue, offer.Param, offer.Price)
if bestOffer.Param == "" {
// инициализация
minTotalValue = totalValue
bestOffer = offer
} else {
if totalValue < minTotalValue {
minTotalValue = totalValue
bestOffer = offer
}
}
}
if bestOffer.Param == "" {
comment = fmt.Sprintf("Тотал %s и меньше с коэф. >= 1.5 не найден",
tipTotalStr)
return comment, nil, Waiting
}
return "", &BetDetails{
Market: MarketTotal,
Side: SideOver,
Param: bestOffer.Param,
Price: bestOffer.Price,
}, Bet
}
func (s HandballFTOver) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
//buf, _ := json.MarshalIndent(stats, "", " ")
//fmt.Printf("%s\n", buf)
if stats.Status != onexbet.StatusMatchCompleted {
return
}
tipTotal, err := strconv.ParseFloat(tip.Param, 64)
if err != nil {
return
}
res := &TipResult{
Result: fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints),
}
goals := float64(stats.HomePoints + stats.AwayPoints)
// Сперва проверяем что тоталы не равны
dif := goals - tipTotal
eps := 0.0001
if math.Abs(dif) < eps {
// Равны - расход
res.Status = model.Void
} else {
if goals > tipTotal {
res.Status = model.Won
} else {
res.Status = model.Lost
}
}
return res
}
type HandballFTUnder struct {
id int64
notifyInTelegram bool
totalDiff float64
fixateGamePaceEveryNMinutes int
isDisabled bool
}
func NewHandballFTUnder(opt HandballStrategyOptions) HandballFTUnder {
if opt.ID == 0 {
panic("StrategyID not defined")
}
s := HandballFTUnder{}
s.id = opt.ID
s.notifyInTelegram = opt.NotifyInTelegram
s.totalDiff = opt.TotalDiff
s.fixateGamePaceEveryNMinutes = opt.FixateGamePaceEveryNMinutes
s.isDisabled = opt.Disable
return s
}
func (s HandballFTUnder) ID() int64 {
return s.id
}
func (s HandballFTUnder) ShortName() string {
return fmt.Sprintf("#%d FT Under (+%v, %d min)", s.id, s.totalDiff, s.fixateGamePaceEveryNMinutes)
}
func (s HandballFTUnder) IsNotifyInTelegram() bool {
return s.notifyInTelegram
}
func (s HandballFTUnder) GetTelegramTipPattern() string {
return `Сигнал # %d.
Алгоритм %d. Тотал Меньше в матче.
%s. %s
%s - %s
Точка входа %.1f кф. %.3f
Рекомендация: тотал меньше %.1f и выше (%.1f; %.1f)`
}
func (s HandballFTUnder) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
return true
}
func (s HandballFTUnder) IsInteresting(match *TeamMatch) (notes interface{}, _ bool) {
if s.isDisabled {
return
}
eps := 0.01
// Проверки на то, что полные данные
if match.BasicTotal < 1 {
return nil, false
}
tipTotal := match.BasicTotal + s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundUp)
basicGoalsPerMinute := tipTotal / 60
if math.Abs(match.H2HGoalsPerMinute) > eps {
// Если команды между собой забивают больше чем дает бук (с учетом погрешности)
// - матч не интересен (ибо опасно)
if match.H2HGoalsPerMinute > basicGoalsPerMinute {
return
}
} else {
if math.Abs(match.HomeGoalsPerMinute) < eps {
return
}
if math.Abs(match.AwayGoalsPerMinute) < eps {
return
}
// Считаем средний темп с которым играют обе команды
avgGoalsPerMinute := (match.HomeGoalsPerMinute + match.AwayGoalsPerMinute) / 2
// Если в среднем команды забивают больше чем дает бук (с учетом погрешности)
// - матч не интересен (ибо опасно)
if avgGoalsPerMinute > basicGoalsPerMinute {
return
}
}
return nil, true
}
func (s HandballFTUnder) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (comment string, _ *BetDetails, _ TipCode) {
if upd.CurrentTime == 1800 {
return "в перерыве не ставим (багфикс)", nil, Waiting
}
if upd.CurrentTime < ftTimeFrame.Since {
return "еще рано", nil, Waiting
}
if upd.CurrentTime > ftTimeFrame.Until {
return "время вышло", nil, Unwatch
}
tipTotal := match.BasicTotal + s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundUp)
basicGoalsPerMinute := tipTotal / 60
tipTotalStr := fmt.Sprintf("%.1f", tipTotal)
if float64(upd.HomeGoals+upd.AwayGoals) > tipTotal {
return "Тотал пробит", nil, Unwatch
}
var isPassed bool
comment, isPassed = checkGamePace(basicGoalsPerMinute, match, upd, s.fixateGamePaceEveryNMinutes, Lte)
if !isPassed {
return comment, nil, Waiting
}
// Текущий темп матча опережает рассчитанный на истории
// проверяем коэффициенты.
// Задача - найти максимальный тотал >= tipTotal, чтобы коэфф. >= 1.5
tipTotal = tipTotal - 0.01 // Для удобного сравнивания float
var (
maxTotalValue float64
bestOffer onexbet.ParamOffer // строковое представление тотала
)
for _, offer := range upd.Total.Under {
if offer.Price < 1.5 {
continue
}
totalValue, err := strconv.ParseFloat(offer.Param, 64)
if err != nil {
continue
}
if totalValue < tipTotal {
continue
}
if bestOffer.Param == "" {
// инициализация
maxTotalValue = totalValue
bestOffer = offer
} else {
if totalValue > maxTotalValue {
maxTotalValue = totalValue
bestOffer = offer
}
}
}
if bestOffer.Param == "" {
comment = fmt.Sprintf("Тотал %s и больше с коэф. >= 1.5 не найден",
tipTotalStr)
return comment, nil, Waiting
}
return "", &BetDetails{
Market: MarketTotal,
Side: SideUnder,
Param: bestOffer.Param,
Price: bestOffer.Price,
}, Bet
}
func (s HandballFTUnder) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
//buf, _ := json.MarshalIndent(stats, "", " ")
//fmt.Printf("%s\n", buf)
if stats.Status != onexbet.StatusMatchCompleted {
return
}
tipTotal, err := strconv.ParseFloat(tip.Param, 64)
if err != nil {
return
}
res := &TipResult{
Result: fmt.Sprintf("счет: %d-%d", stats.HomePoints, stats.AwayPoints),
}
goals := float64(stats.HomePoints + stats.AwayPoints)
// Сперва проверяем что тоталы не равны
dif := goals - tipTotal
eps := 0.0001
if math.Abs(dif) < eps {
// Равны - расход
res.Status = model.Void
} else {
if goals < tipTotal {
res.Status = model.Won
} else {
res.Status = model.Lost
}
}
return res
}
// P1
type HandballP1Over struct {
id int64
notifyInTelegram bool
totalDiff float64
fixateGamePaceEveryNMinutes int
isDisabled bool
}
func NewHandballP1Over(opt HandballStrategyOptions) HandballP1Over {
if opt.ID == 0 {
panic("StrategyID not defined")
}
s := HandballP1Over{}
s.id = opt.ID
s.notifyInTelegram = opt.NotifyInTelegram
s.totalDiff = opt.TotalDiff
s.fixateGamePaceEveryNMinutes = opt.FixateGamePaceEveryNMinutes
s.isDisabled = opt.Disable
return s
}
func (s HandballP1Over) ID() int64 {
return s.id
}
func (s HandballP1Over) ShortName() string {
return fmt.Sprintf("#%d P1 Over (-%v, %d min)", s.id, s.totalDiff, s.fixateGamePaceEveryNMinutes)
}
func (s HandballP1Over) IsNotifyInTelegram() bool {
return s.notifyInTelegram
}
func (s HandballP1Over) GetTelegramTipPattern() string {
return `Сигнал # %d.
Алгоритм %d. Тотал Больше в 1м тайме.
%s. %s
%s - %s
Точка входа %.1f кф. %.3f
Рекомендация: тотал больше %.1f и ниже (%.1f; %.1f)`
}
func (s HandballP1Over) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
if liveMatch.CurrentPeriod == 1 {
return true
}
return false
}
func (s HandballP1Over) IsInteresting(match *TeamMatch) (notes interface{}, _ bool) {
if s.isDisabled {
return
}
eps := 0.01
// Проверки на то, что полные данные
if match.BasicTotal < 1 {
return nil, false
}
var tipTotal float64
/*
if match.H1BasicTotal < 1 {
tipTotal = (match.BasicTotal / 2) - s.totalDiff
} else {
tipTotal = match.H1BasicTotal - s.totalDiff
}
*/
if match.H1BasicTotal < 1 {
return
}
tipTotal = match.H1BasicTotal - s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundDown)
basicGoalsPerMinute := tipTotal / 30
if math.Abs(match.H2HGoalsPerMinute) > eps {
// Если команды между собой забивают меньше чем дает бук (с учетом погрешности)
// - матч не интересен
if match.H2HGoalsPerMinute < basicGoalsPerMinute {
return
}
} else {
if math.Abs(match.HomeGoalsPerMinute) < eps {
return
}
if math.Abs(match.AwayGoalsPerMinute) < eps {
return
}
// Считаем средний темп с которым играют обе команды
avgGoalsPerMinute := (match.HomeGoalsPerMinute + match.AwayGoalsPerMinute) / 2
// Если в среднем команды забивают меньше чем дает бук (с учетом погрешности)
// - матч не интересен
if avgGoalsPerMinute < basicGoalsPerMinute {
return
}
}
return nil, true
}
func (s HandballP1Over) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (comment string, _ *BetDetails, _ TipCode) {
if upd.CurrentTime == 1800 {
return "в перерыве не ставим (багфикс)", nil, Waiting
}
if upd.CurrentTime < p1TimeFrame.Since {
return "еще рано", nil, Waiting
}
if upd.CurrentTime > p1TimeFrame.Until {
return "время вышло", nil, Unwatch
}
var tipTotal float64
if match.H1BasicTotal < 1 {
tipTotal = (match.BasicTotal / 2) - s.totalDiff
} else {
tipTotal = match.H1BasicTotal - s.totalDiff
}
// ВАЖНО!
// После деления тотал может быть 25.25, либо просто круглый 24, 60
tipTotal = correctTotal(tipTotal, roundDown)
basicGoalsPerMinute := tipTotal / 30
tipTotalStr := fmt.Sprintf("%.1f", tipTotal)
if float64(upd.HomeGoals+upd.AwayGoals) > tipTotal {
return "Тотал пробит", nil, Unwatch
}
var isPassed bool
comment, isPassed = checkGamePace(basicGoalsPerMinute, match, upd, s.fixateGamePaceEveryNMinutes, Gte)
if !isPassed {
return comment, nil, Waiting
}
// Текущий темп матча опережает рассчитанный на истории
// проверяем коэффициенты.
// Задача - найти минимальный тотал <= tipTotal, чтобы коэфф. >= 1.5
tipTotal = tipTotal + 0.01 // Для удобного сравнивания float
//fmt.Printf("tipTotal: %.2f\n", tipTotal)
var (
minTotalValue float64
bestOffer onexbet.ParamOffer // строковое представление тотала
)
for _, offer := range upd.H1.Total.Over {
if offer.Price < 1.5 {
continue
}
totalValue, err := strconv.ParseFloat(offer.Param, 64)
if err != nil {
continue
}
if totalValue > tipTotal {
//fmt.Printf("totalValue > tipTotal: %.2f > %.2f\n", totalValue, tipTotal)
continue
}
//fmt.Printf("Best: totalValue: %2.f (%s), price: %.2f\n", totalValue, offer.Param, offer.Price)
if bestOffer.Param == "" {
// инициализация
minTotalValue = totalValue
bestOffer = offer
} else {
if totalValue < minTotalValue {
minTotalValue = totalValue
bestOffer = offer
}
}
}
if bestOffer.Param == "" {
comment = fmt.Sprintf("Тотал %s и меньше с коэф. >= 1.5 не найден",
tipTotalStr)
return comment, nil, Waiting
}
return "", &BetDetails{
Market: MarketTotalH1,
Side: SideOver,
Param: bestOffer.Param,
Price: bestOffer.Price,
}, Bet
}
func (s HandballP1Over) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
//buf, _ := json.MarshalIndent(stats, "", " ")
//fmt.Printf("%s\n", buf)
if stats.Status != onexbet.StatusMatchCompleted {
return
}
h1, ok := stats.ScoreByPeriods[1]
if !ok {
return
}
tipTotal, err := strconv.ParseFloat(tip.Param, 64)
if err != nil {
return
}
res := &TipResult{
Result: fmt.Sprintf("1й тайм: %d-%d", h1.HomePoints, h1.AwayPoints),
}
goals := float64(h1.HomePoints + h1.AwayPoints)
// Сперва проверяем что тоталы не равны
dif := goals - tipTotal
eps := 0.0001
if math.Abs(dif) < eps {
// Равны - расход
res.Status = model.Void
} else {
if goals > tipTotal {
res.Status = model.Won
} else {
res.Status = model.Lost
}
}
return res
}
type HandballP1Under struct {
id int64
notifyInTelegram bool
totalDiff float64
fixateGamePaceEveryNMinutes int
isDisabled bool
}
func NewHandballP1Under(opt HandballStrategyOptions) HandballP1Under {
if opt.ID == 0 {
panic("StrategyID not defined")
}
s := HandballP1Under{}
s.id = opt.ID
s.notifyInTelegram = opt.NotifyInTelegram
s.totalDiff = opt.TotalDiff
s.fixateGamePaceEveryNMinutes = opt.FixateGamePaceEveryNMinutes
s.isDisabled = opt.Disable
return s
}
func (s HandballP1Under) ID() int64 {
return s.id
}
func (s HandballP1Under) ShortName() string {
return fmt.Sprintf("#%d P1 Under (+%v, %d min)", s.id, s.totalDiff, s.fixateGamePaceEveryNMinutes)
}
func (s HandballP1Under) IsNotifyInTelegram() bool {
return s.notifyInTelegram
}
func (s HandballP1Under) GetTelegramTipPattern() string {
return `Сигнал # %d.
Алгоритм %d. Тотал Меньше в 1м тайме.
%s. %s
%s - %s
Точка входа %.1f кф. %.3f
Рекомендация: тотал меньше %.1f и выше (%.1f; %.1f)`
}
func (s HandballP1Under) IsNeedToWatch(liveMatch onexbet.LiveMatch) bool {
if liveMatch.CurrentPeriod == 1 {
return true
}
return false
}
func (s HandballP1Under) IsInteresting(match *TeamMatch) (notes interface{}, _ bool) {
if s.isDisabled {
return
}
eps := 0.01
// Проверки на то, что полные данные
if match.BasicTotal < 1 {
return nil, false
}
var tipTotal float64
/*
if match.H1BasicTotal < 1 {
tipTotal = (match.BasicTotal / 2) + s.totalDiff
} else {
tipTotal = match.H1BasicTotal + s.totalDiff
}
*/
if match.H1BasicTotal < 1 {
return
}
tipTotal = match.H1BasicTotal + s.totalDiff
// ВАЖНО!
// После деления тотал может быть 25.25
tipTotal = correctTotal(tipTotal, roundUp)
basicGoalsPerMinute := tipTotal / 30
if math.Abs(match.H2HGoalsPerMinute) > eps {
// Если команды между собой забивают больше чем дает бук (с учетом погрешности)
// - матч не интересен (ибо опасно)
if match.H2HGoalsPerMinute > basicGoalsPerMinute {
return
}
} else {
if math.Abs(match.HomeGoalsPerMinute) < eps {
return
}
if math.Abs(match.AwayGoalsPerMinute) < eps {
return
}
// Считаем средний темп с которым играют обе команды
avgGoalsPerMinute := (match.HomeGoalsPerMinute + match.AwayGoalsPerMinute) / 2
// Если в среднем команды забивают больше чем дает бук (с учетом погрешности)
// - матч не интересен (ибо опасно)
if avgGoalsPerMinute > basicGoalsPerMinute {
return
}
}
return nil, true
}
func (s HandballP1Under) GetTip(match *TeamMatch, upd onexbet.TeamLiveMatchData) (comment string, _ *BetDetails, _ TipCode) {
if upd.CurrentTime == 1800 {
return "в перерыве не ставим (багфикс)", nil, Waiting
}
if upd.CurrentTime < p1TimeFrame.Since {
return "еще рано", nil, Waiting
}
if upd.CurrentTime > p1TimeFrame.Until {
return "время вышло", nil, Unwatch
}
var tipTotal float64
if match.H1BasicTotal < 1 {
tipTotal = (match.BasicTotal / 2) + s.totalDiff
} else {
tipTotal = match.H1BasicTotal + s.totalDiff
}
// ВАЖНО!
// После деления тотал может быть 25.25
tipTotal = correctTotal(tipTotal, roundUp)
basicGoalsPerMinute := tipTotal / 30
tipTotalStr := fmt.Sprintf("%.1f", tipTotal)
if float64(upd.HomeGoals+upd.AwayGoals) > tipTotal {
return "Тотал пробит", nil, Unwatch
}
var isPassed bool
comment, isPassed = checkGamePace(basicGoalsPerMinute, match, upd, s.fixateGamePaceEveryNMinutes, Lte)
if !isPassed {
return comment, nil, Waiting
}
// Задача - найти максимальный тотал >= tipTotal, чтобы коэфф. >= 1.5
tipTotal = tipTotal - 0.01 // Для удобного сравнивания float
var (
maxTotalValue float64
bestOffer onexbet.ParamOffer // строковое представление тотала
)
for _, offer := range upd.H1.Total.Under {
if offer.Price < 1.5 {
continue
}
totalValue, err := strconv.ParseFloat(offer.Param, 64)
if err != nil {
continue
}
if totalValue < tipTotal {
continue
}
if bestOffer.Param == "" {
// инициализация
maxTotalValue = totalValue
bestOffer = offer
} else {
if totalValue > maxTotalValue {
maxTotalValue = totalValue
bestOffer = offer
}
}
}
if bestOffer.Param == "" {
comment = fmt.Sprintf("Тотал %s и больше с коэф. >= 1.5 не найден",
tipTotalStr)
return comment, nil, Waiting
}
return "", &BetDetails{
Market: MarketTotalH1,
Side: SideUnder,
Param: bestOffer.Param,
Price: bestOffer.Price,
}, Bet
}
func (s HandballP1Under) GetResult(tip model.Tip, stats onexbet.TeamMatchResult) (_ *TipResult) {
//buf, _ := json.MarshalIndent(stats, "", " ")
//fmt.Printf("%s\n", buf)
if stats.Status != onexbet.StatusMatchCompleted {
return
}
h1, ok := stats.ScoreByPeriods[1]
if !ok {
return
}
//if stats.H1 == nil || stats.H2 == nil {
//return
//}
tipTotal, err := strconv.ParseFloat(tip.Param, 64)
if err != nil {
return
}
res := &TipResult{
Result: fmt.Sprintf("1й тайм: %d-%d", h1.HomePoints, h1.AwayPoints),
}
goals := float64(h1.HomePoints + h1.AwayPoints)
// Сперва проверяем что тоталы не равны
dif := goals - tipTotal
eps := 0.0001
if math.Abs(dif) < eps {
// Равны - расход
res.Status = model.Void
} else {
if goals < tipTotal {
res.Status = model.Won
} else {
res.Status = model.Lost
}
}
return res
}