1263 lines
34 KiB
Go
1263 lines
34 KiB
Go
|
|
package daemon
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"log"
|
|||
|
|
"regexp"
|
|||
|
|
"sort"
|
|||
|
|
"sync"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"gordenko.dev/dima/fixme"
|
|||
|
|
"gordenko.dev/dima/flashscore"
|
|||
|
|
fsmodel "gordenko.dev/dima/flashscore/model"
|
|||
|
|
"gordenko.dev/dima/onexbet"
|
|||
|
|
"gordenko.dev/dima/pretty"
|
|||
|
|
"gordenko.dev/dima/tipper/model"
|
|||
|
|
"gordenko.dev/dima/web"
|
|||
|
|
"gordenko.dev/dima/web/api"
|
|||
|
|
"gordenko.dev/dima/ws"
|
|||
|
|
"gordenko.dev/dima/ws/publisher"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
ChannelHandball = "handball"
|
|||
|
|
ChannelFootball = "football"
|
|||
|
|
ChannelTennis = "tennis"
|
|||
|
|
|
|||
|
|
// Флаги для округления тотала
|
|||
|
|
roundUp = 0
|
|||
|
|
roundDown = 1
|
|||
|
|
|
|||
|
|
// /SportFootball int64 = 1
|
|||
|
|
|
|||
|
|
MarketTotalH1 = 1
|
|||
|
|
MarketTotal = 2
|
|||
|
|
MarketHandicap = 3
|
|||
|
|
|
|||
|
|
SideOver = 1
|
|||
|
|
SideUnder = 2
|
|||
|
|
|
|||
|
|
SideHome = 1
|
|||
|
|
SideAway = 2
|
|||
|
|
|
|||
|
|
WSMessageTipsChanged = "tipsChanged"
|
|||
|
|
// WSMessageTeamMatches Обновленный список матчей после синхронизации с flashscore
|
|||
|
|
WSMessageTeamMatches = "teamMatches"
|
|||
|
|
WSMessageWhyNot = "whyNot"
|
|||
|
|
WSMessageInplayStatusChanged = "inplayStatusChanged"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var onexbetSportToFlashscoreSport = map[onexbet.OnexbetSport]int{
|
|||
|
|
onexbet.Football: flashscore.Football,
|
|||
|
|
onexbet.Handball: flashscore.Handball,
|
|||
|
|
onexbet.Tennis: flashscore.Tennis,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var flashscoreSportToOnexbetSport = map[int]onexbet.OnexbetSport{
|
|||
|
|
flashscore.Football: onexbet.Football,
|
|||
|
|
flashscore.Handball: onexbet.Handball,
|
|||
|
|
flashscore.Tennis: onexbet.Tennis,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sportToTelegramChatID = map[int]string{
|
|||
|
|
flashscore.Football: footballTelegramChatID,
|
|||
|
|
flashscore.Handball: handballTelegramChatID,
|
|||
|
|
flashscore.Tennis: tennisTelegramChatID,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type InplayStatusMessage struct {
|
|||
|
|
MatchID string `json:"matchID"`
|
|||
|
|
IsInplay bool `json:"isInplay"`
|
|||
|
|
IsLinkedToOnexbet bool `json:"isLinkedToOnexbet"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TeamMatchesMessage struct {
|
|||
|
|
SportID int `json:"sportID"`
|
|||
|
|
Matches []TeamMatch `json:"matches"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// flashscore sportID
|
|||
|
|
func getChannel(sportID int) string {
|
|||
|
|
switch sportID {
|
|||
|
|
case flashscore.Football:
|
|||
|
|
return ChannelFootball
|
|||
|
|
|
|||
|
|
case flashscore.Handball:
|
|||
|
|
return ChannelHandball
|
|||
|
|
|
|||
|
|
case flashscore.Tennis:
|
|||
|
|
return ChannelTennis
|
|||
|
|
|
|||
|
|
default:
|
|||
|
|
panic(fmt.Sprintf("Bug: channel is undefined for the sportID %d", sportID))
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TipResult struct {
|
|||
|
|
Status model.TipStatus
|
|||
|
|
Result string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type StrategyOptions struct {
|
|||
|
|
ID int64
|
|||
|
|
NotifyInTelegram bool
|
|||
|
|
IsChampAccepted func(string) bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TeamStrategy interface {
|
|||
|
|
ID() int64
|
|||
|
|
ShortName() string
|
|||
|
|
// (report) notes, isInteresting
|
|||
|
|
IsInteresting(*TeamMatch) (interface{}, bool)
|
|||
|
|
// return -> comment why not,
|
|||
|
|
GetTip(*TeamMatch, onexbet.TeamLiveMatchData) (string, *BetDetails, TipCode)
|
|||
|
|
GetResult(model.Tip, onexbet.TeamMatchResult) *TipResult
|
|||
|
|
IsNotifyInTelegram() bool
|
|||
|
|
GetTelegramTipPattern() string
|
|||
|
|
IsNeedToWatch(onexbet.LiveMatch) bool
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ChampRatingKey struct {
|
|||
|
|
ChampID string
|
|||
|
|
StrategyID int64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type ChampRating struct {
|
|||
|
|
Won int
|
|||
|
|
Lost int
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *ChampRating) GetAccuracy() (accuracy float64) {
|
|||
|
|
// total = 100%
|
|||
|
|
// won = x%
|
|||
|
|
// x = (won*100) / total
|
|||
|
|
total := s.Won + s.Lost
|
|||
|
|
if total > 0 {
|
|||
|
|
accuracy = float64(s.Won*100) / float64(total)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type FavoriteSide int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
FavoriteHome FavoriteSide = 1
|
|||
|
|
FavoriteAway FavoriteSide = 2
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type TeamMatch struct {
|
|||
|
|
// COMMON
|
|||
|
|
Score onexbet.Score `json:"score"`
|
|||
|
|
ScoreByPeriods map[int]onexbet.Score `json:"scoreByPeriods"`
|
|||
|
|
CurrentPeriod int `json:"currentPeriod"`
|
|||
|
|
// TENNIS stats
|
|||
|
|
TwoSetsBreakpoints int `json:"twoSetsBreakpoints"`
|
|||
|
|
// Для стратегий где нужно определить доматчевого фаворита
|
|||
|
|
// 0 - фаворит не определен
|
|||
|
|
FavoriteSide FavoriteSide `json:"favoriteSide"`
|
|||
|
|
MaxFavoritHandicapIn1stSet *onexbet.ParamOffer `json:"maxFavoritHandicapIn1stSet"`
|
|||
|
|
// less than or equal 1.6 (для алгоритма 4 в теннисе)
|
|||
|
|
Lte16FavoriteSide FavoriteSide `json:"lte16FavoriteSide"`
|
|||
|
|
//IsTargetPriceIn2ndSetFound bool
|
|||
|
|
// Football
|
|||
|
|
Favorite string `json:"favorite,omitempty"`
|
|||
|
|
// Handball
|
|||
|
|
// Начальный тотал (по flashscore до матча)
|
|||
|
|
BasicTotal float64 `json:"basicTotal"`
|
|||
|
|
H1BasicTotal float64 `json:"h1BasicTotal"`
|
|||
|
|
HomeGoalsPerMinute float64 `json:"homeGoalsPerMinute"`
|
|||
|
|
AwayGoalsPerMinute float64 `json:"awayGoalsPerMinute"`
|
|||
|
|
H2HGoalsPerMinute float64 `json:"h2hGoalsPerMinute"`
|
|||
|
|
GamePace float64 `json:"-"`
|
|||
|
|
GamePaceFrame int `json:"-"`
|
|||
|
|
// Main
|
|||
|
|
MatchID string `json:"matchID"`
|
|||
|
|
Home fsmodel.Team `json:"home"`
|
|||
|
|
Away fsmodel.Team `json:"away"`
|
|||
|
|
Zone fsmodel.TeamZone `json:"zone"`
|
|||
|
|
Champ fsmodel.TeamChamp `json:"champ"`
|
|||
|
|
FullChampName string `json:"fullChampName"`
|
|||
|
|
IsLinkedToOnexbet bool `json:"isLinkedToOnexbet"`
|
|||
|
|
IsInplay bool `json:"isInplay"` // узнать - отписались от обновлений или нет?
|
|||
|
|
SportID int `json:"sportID"`
|
|||
|
|
Strategies map[int64]TeamStrategy `json:"-"`
|
|||
|
|
StartTime int64 `json:"startTime"`
|
|||
|
|
ParseTime time.Time `json:"parseTime"`
|
|||
|
|
OnexbetMatchID string `json:"onexbetMatchID"`
|
|||
|
|
OnexbetStatsMatchID string `json:"onexbetStatsMatchID"`
|
|||
|
|
IsTeamsSwapped bool `json:"isTeamsSwapped"`
|
|||
|
|
Notes []StrategyNotes `json:"notes"`
|
|||
|
|
//TipTotalOver float64 `json:"tipTotalOver"`
|
|||
|
|
//TipTotalUnder float64 `json:"tipTotalUnder"`
|
|||
|
|
// Ожимаемый темп игры (голов в минуту). Начальный тотал / 60 минут
|
|||
|
|
//BasicGoalsPerMinute float64 `json:"basicGoalsPerMinute"`
|
|||
|
|
//BasicOverGoalsPerMinute float64 `json:"basicOverGoalsPerMinute"`
|
|||
|
|
//BasicUnderGoalsPerMinute float64 `json:"basicUnderGoalsPerMinute"`
|
|||
|
|
// Статистика для расчета
|
|||
|
|
HomeTeamMatches []fsmodel.TeamMatch `json:"homeTeamMatches"`
|
|||
|
|
AwayTeamMatches []fsmodel.TeamMatch `json:"awayTeamMatches"`
|
|||
|
|
H2H []fsmodel.TeamMatch `json:"h2h"`
|
|||
|
|
Odds flashscore.Odds `json:"odds"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type StrategyNotes struct {
|
|||
|
|
StrategyID int64 `json:"strategyID"`
|
|||
|
|
Notes interface{} `json:"notes"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type TipCode int
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
Waiting TipCode = 0
|
|||
|
|
Unwatch TipCode = 1
|
|||
|
|
Bet TipCode = 2
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
type BetDetails struct {
|
|||
|
|
Market int
|
|||
|
|
Side int
|
|||
|
|
Param string
|
|||
|
|
Price float64
|
|||
|
|
TelegramMessage string
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
var tipTextAlgo100Pattern = `Сигнал # %d.
|
|||
|
|
Будет ГОЛ!
|
|||
|
|
Алгоритм 100
|
|||
|
|
Первый тайм, Тотал Больше 0.5
|
|||
|
|
Футбол. %s. %s
|
|||
|
|
%s - %s
|
|||
|
|
Коэф. %.3f`
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
var tipResultTextPattern = `Сигнал # %d.
|
|||
|
|
%s
|
|||
|
|
%s
|
|||
|
|
`
|
|||
|
|
|
|||
|
|
const (
|
|||
|
|
EmptyTipStatus fixme.Code = "T1"
|
|||
|
|
WrongTipStatus fixme.Code = "T2"
|
|||
|
|
EmptyValue fixme.Code = "T3"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
var errorMessages = map[fixme.Code]string{
|
|||
|
|
EmptyTipStatus: "не указан статус",
|
|||
|
|
WrongTipStatus: "неверный статус",
|
|||
|
|
EmptyValue: "пустое значение недопустимо",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var fix = fixme.New(errorMessages)
|
|||
|
|
|
|||
|
|
type Options struct {
|
|||
|
|
Logger *log.Logger
|
|||
|
|
WsPublicServer *ws.PublicServer
|
|||
|
|
FlashscoreAPIAddr string
|
|||
|
|
OnexbetWsURL string
|
|||
|
|
OnexbetAPIURL string
|
|||
|
|
SportIDs []int
|
|||
|
|
FlashscoreModel *fsmodel.Model
|
|||
|
|
TipperModel *model.Model
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type Daemon struct {
|
|||
|
|
mutex sync.Mutex
|
|||
|
|
fsAPIClient *api.Client
|
|||
|
|
onexbetClient *ws.Client
|
|||
|
|
logger *log.Logger
|
|||
|
|
model *model.Model
|
|||
|
|
wsServer *ws.PublicServer
|
|||
|
|
publisher *publisher.PublicPublisher
|
|||
|
|
onexbetAPIClient *api.Client
|
|||
|
|
fsmodel *fsmodel.Model
|
|||
|
|
champRatings map[int]map[ChampRatingKey]*ChampRating //map[int64]map[string]*ChampRating
|
|||
|
|
//handballChampRatings map[ChampRatingKey]*ChampRating
|
|||
|
|
// Отобаранные матчи по видам спорта
|
|||
|
|
teamMatches map[int]map[string]*TeamMatch
|
|||
|
|
onexbetMatchToTeamMatch map[string]*TeamMatch
|
|||
|
|
sportStrategies map[int]map[int64]TeamStrategy
|
|||
|
|
historyProcessors map[int]func(*TeamMatch)
|
|||
|
|
sportIDs []int
|
|||
|
|
unrecognizedOnexbetMatches map[int]map[string]bool
|
|||
|
|
identifiedMatches map[string]bool // 1xbet MatchID => true
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func New(opt Options) (*Daemon, error) {
|
|||
|
|
s := new(Daemon)
|
|||
|
|
s.logger = opt.Logger
|
|||
|
|
s.sportIDs = opt.SportIDs
|
|||
|
|
s.identifiedMatches = make(map[string]bool)
|
|||
|
|
s.model = opt.TipperModel
|
|||
|
|
s.fsmodel = opt.FlashscoreModel
|
|||
|
|
|
|||
|
|
// TELEGRAM, topChamps
|
|||
|
|
/*
|
|||
|
|
s.footballStrategies[11] = NewP1Over05(StrategyOptions{
|
|||
|
|
ID: 11,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
IsChampAccepted: acceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
s.footballStrategies[22] = NewOver05(StrategyOptions{
|
|||
|
|
ID: 22,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
IsChampAccepted: acceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
s.footballStrategies[33] = NewOver15(StrategyOptions{
|
|||
|
|
ID: 33,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
IsChampAccepted: acceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// Мои стратегии - ТМ для всех чемпионатов, кроме топовых
|
|||
|
|
s.footballStrategies[701] = NewP1Under05(StrategyOptions{
|
|||
|
|
ID: 701,
|
|||
|
|
NotifyInTelegram: false,
|
|||
|
|
IsChampAccepted: nonAcceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
s.footballStrategies[702] = NewUnder05(StrategyOptions{
|
|||
|
|
ID: 702,
|
|||
|
|
NotifyInTelegram: false,
|
|||
|
|
IsChampAccepted: nonAcceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
s.footballStrategies[703] = NewUnder15(StrategyOptions{
|
|||
|
|
ID: 703,
|
|||
|
|
NotifyInTelegram: false,
|
|||
|
|
IsChampAccepted: nonAcceptedChamps(topChamps),
|
|||
|
|
})
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
if opt.FlashscoreAPIAddr == "" {
|
|||
|
|
return nil, errors.New("FlashscoreAPIAddr option is required")
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var err error
|
|||
|
|
s.fsAPIClient, err = api.NewClient(api.ClientOptions{
|
|||
|
|
URL: opt.FlashscoreAPIAddr,
|
|||
|
|
BuildRequest: api.BuildEnvelopeRequest,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("api.NewClient(flashscore): %s", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.onexbetClient, err = ws.NewClient(ws.ClientOptions{
|
|||
|
|
URL: opt.OnexbetWsURL,
|
|||
|
|
Logger: opt.Logger,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("ws.NewClient(onexbet): %s", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.onexbetClient.OnConnected(s.onOnexbetWsClientConnected)
|
|||
|
|
s.onexbetClient.Handle(onexbet.WsMsgLiveMatches, s.onOnexbetTeamLiveMatches)
|
|||
|
|
s.onexbetClient.Handle(onexbet.WsMsgTeamLiveMatchData, s.onOnexbetTeamLiveMatchData)
|
|||
|
|
s.onexbetClient.Handle(onexbet.WsMsgTennisLiveMatchData, s.onOnexbetTeamLiveMatchData)
|
|||
|
|
|
|||
|
|
s.wsServer = opt.WsPublicServer
|
|||
|
|
|
|||
|
|
s.wsServer.OnConnected(s.onWsClientConnected)
|
|||
|
|
s.wsServer.OnDisconnected(s.onWsClientDisconnected)
|
|||
|
|
|
|||
|
|
s.publisher, err = publisher.NewPublicPublisher(publisher.PublicPublisherOptions{
|
|||
|
|
WsPublicServer: s.wsServer,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.publisher.OnTopicSubscribeRequest(ChannelHandball, s.onWsSubscribeRequestToHandball)
|
|||
|
|
s.publisher.OnTopicSubscribeRequest(ChannelFootball, s.onWsSubscribeRequestToFootball)
|
|||
|
|
s.publisher.OnTopicSubscribeRequest(ChannelTennis, s.onWsSubscribeRequestToTennis)
|
|||
|
|
|
|||
|
|
s.onexbetAPIClient, err = api.NewClient(api.ClientOptions{
|
|||
|
|
URL: opt.OnexbetAPIURL,
|
|||
|
|
BuildRequest: api.BuildEnvelopeRequest,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("api.NewClient(onexbet): %s", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.teamMatches = make(map[int]map[string]*TeamMatch)
|
|||
|
|
s.onexbetMatchToTeamMatch = make(map[string]*TeamMatch)
|
|||
|
|
s.sportStrategies = make(map[int]map[int64]TeamStrategy)
|
|||
|
|
s.unrecognizedOnexbetMatches = make(map[int]map[string]bool)
|
|||
|
|
s.champRatings = make(map[int]map[ChampRatingKey]*ChampRating)
|
|||
|
|
|
|||
|
|
for _, sportID := range s.sportIDs {
|
|||
|
|
s.teamMatches[sportID] = make(map[string]*TeamMatch)
|
|||
|
|
s.sportStrategies[sportID] = make(map[int64]TeamStrategy)
|
|||
|
|
s.unrecognizedOnexbetMatches[sportID] = make(map[string]bool)
|
|||
|
|
s.champRatings[sportID] = make(map[ChampRatingKey]*ChampRating)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Набор статистики по всем чемпионатам
|
|||
|
|
|
|||
|
|
// FOOTBALL
|
|||
|
|
/*
|
|||
|
|
s.sportStrategies[flashscore.Football][1] = NewP1Over05(StrategyOptions{
|
|||
|
|
ID: 1,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
s.sportStrategies[flashscore.Football][2] = NewOver05(StrategyOptions{
|
|||
|
|
ID: 2,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
s.sportStrategies[flashscore.Football][3] = NewOver15(StrategyOptions{
|
|||
|
|
ID: 3,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Football][6] = NewFootballP1Over05(StrategyOptions{
|
|||
|
|
ID: 6,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Football][7] = NewFootballOver05(StrategyOptions{
|
|||
|
|
ID: 7,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Football][8] = NewFootballOver15(StrategyOptions{
|
|||
|
|
ID: 8,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// HANDBALL
|
|||
|
|
s.sportStrategies[flashscore.Handball][4] = NewHandballFTOver(HandballStrategyOptions{
|
|||
|
|
ID: 4,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 10,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Handball][5] = NewHandballFTUnder(HandballStrategyOptions{
|
|||
|
|
ID: 5,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 10,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// HANDBALL
|
|||
|
|
s.sportStrategies[flashscore.Handball][11] = NewHandballP1Over(HandballStrategyOptions{
|
|||
|
|
ID: 11,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 7,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Handball][12] = NewHandballP1Under(HandballStrategyOptions{
|
|||
|
|
ID: 12,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 7,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
/*
|
|||
|
|
s.sportStrategies[flashscore.Handball][13] = NewHandballP1Over(HandballStrategyOptions{
|
|||
|
|
ID: 13,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 5,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Handball][14] = NewHandballP1Under(HandballStrategyOptions{
|
|||
|
|
ID: 14,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
TotalDiff: 5,
|
|||
|
|
FixateGamePaceEveryNMinutes: 5,
|
|||
|
|
})
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
// TENNIS
|
|||
|
|
s.sportStrategies[flashscore.Tennis][15] = NewTennis3rdSetTotalUnder(StrategyOptions{
|
|||
|
|
ID: 15,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Tennis][16] = NewTennis2ndSetPlusHandicap(StrategyOptions{
|
|||
|
|
ID: 16,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Tennis][17] = NewTennis2ndSetPlusHandicapAny(StrategyOptions{
|
|||
|
|
ID: 17,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.sportStrategies[flashscore.Tennis][18] = NewTennis2ndSetFavoriteHandicap(StrategyOptions{
|
|||
|
|
ID: 18,
|
|||
|
|
NotifyInTelegram: true,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
s.historyProcessors = make(map[int]func(*TeamMatch))
|
|||
|
|
|
|||
|
|
s.historyProcessors[flashscore.Handball] = HandballHistoryProcessor
|
|||
|
|
|
|||
|
|
// Формируем рейтинг для футбольных стратегий
|
|||
|
|
for sportID, sportStrategies := range s.sportStrategies {
|
|||
|
|
|
|||
|
|
champRatings := s.champRatings[sportID]
|
|||
|
|
|
|||
|
|
for strategyID := range sportStrategies {
|
|||
|
|
var stats []model.StrategyChampStats
|
|||
|
|
|
|||
|
|
stats, err = s.model.ListStrategyChampStats(strategyID)
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, fmt.Errorf("model.ListStrategyChampStats(%d): %s",
|
|||
|
|
strategyID, err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
//buf, _ := json.MarshalIndent(stats, "", " ")
|
|||
|
|
//s.logger.Printf("Stats (%d):\n%s\n\n", strategyID, buf)
|
|||
|
|
|
|||
|
|
for _, champ := range stats {
|
|||
|
|
key := ChampRatingKey{
|
|||
|
|
ChampID: champ.ChampID,
|
|||
|
|
StrategyID: strategyID,
|
|||
|
|
}
|
|||
|
|
champRatings[key] = &ChampRating{
|
|||
|
|
Won: champ.Won,
|
|||
|
|
Lost: champ.Lost,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return s, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) ListRatedChamps(state *web.State, reply *web.Reply) (err error) {
|
|||
|
|
var sportID int
|
|||
|
|
|
|||
|
|
state.Val("sportID", &sportID)
|
|||
|
|
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
ratings, ok := s.champRatings[sportID]
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
str := ""
|
|||
|
|
|
|||
|
|
type ChampAndRating struct {
|
|||
|
|
ChampID string
|
|||
|
|
StrategyID int64
|
|||
|
|
Won int
|
|||
|
|
Lost int
|
|||
|
|
Accuracy float64
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var sorted []ChampAndRating
|
|||
|
|
|
|||
|
|
for key, rating := range ratings {
|
|||
|
|
//if (rating.Won+rating.Lost) >= 6 && rating.GetAccuracy() >= 75 {
|
|||
|
|
if rating.GetAccuracy() >= 70 {
|
|||
|
|
sorted = append(sorted, ChampAndRating{
|
|||
|
|
ChampID: key.ChampID,
|
|||
|
|
StrategyID: key.StrategyID,
|
|||
|
|
Won: rating.Won,
|
|||
|
|
Lost: rating.Lost,
|
|||
|
|
Accuracy: rating.GetAccuracy(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sort.Slice(sorted, func(i, j int) bool {
|
|||
|
|
a := sorted[i]
|
|||
|
|
b := sorted[j]
|
|||
|
|
|
|||
|
|
if a.StrategyID == b.StrategyID {
|
|||
|
|
return a.Accuracy < b.Accuracy
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return a.StrategyID < b.StrategyID
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
for _, champ := range sorted {
|
|||
|
|
str += fmt.Sprintf("<tr><td>%s (стратегия %d):</td><td>won=%d, lost=%d, accuracy=%.2f</td></tr>", champ.ChampID, champ.StrategyID, champ.Won, champ.Lost, champ.Accuracy)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
//reply.SetHeader("Content-Type", "text/pain")
|
|||
|
|
reply.WriteString(`<table style="border-spacing: 10px; border: 0px;">` + str + `</table>`)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) TryLoadAndSetTipResults() {
|
|||
|
|
tips, err := s.model.ListWaitingTips()
|
|||
|
|
if err != nil {
|
|||
|
|
err = fmt.Errorf("ListWaitingTips: %s", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.logger.Println(pretty.PSprintln("Waiting Tips", tips))
|
|||
|
|
|
|||
|
|
// Час назад
|
|||
|
|
//aHourAgo := time.Now().Unix() - 3600
|
|||
|
|
|
|||
|
|
for _, tip := range tips {
|
|||
|
|
// Если матч начался менее часа назад - пропускаем.
|
|||
|
|
//if tip.TipTime > aHourAgo {
|
|||
|
|
// continue
|
|||
|
|
//}
|
|||
|
|
|
|||
|
|
err = s.tryLoadAndSetTipResult(tip)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("trySetTipResult: %s; tipID=%d", err, tip.TipID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) Run() {
|
|||
|
|
go s.flashscoreLoader()
|
|||
|
|
go s.tipResultsLoader()
|
|||
|
|
go s.oldMatchesRemover()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) oldMatchesRemover() {
|
|||
|
|
interval := 30 * time.Minute
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(interval)
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-ticker.C:
|
|||
|
|
s.removeOldMatches()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) tipResultsLoader() {
|
|||
|
|
interval := 20 * time.Minute
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(interval)
|
|||
|
|
|
|||
|
|
s.TryLoadAndSetTipResults()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-ticker.C:
|
|||
|
|
s.TryLoadAndSetTipResults()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) flashscoreLoader() {
|
|||
|
|
interval := 1 * time.Hour
|
|||
|
|
|
|||
|
|
ticker := time.NewTicker(interval)
|
|||
|
|
|
|||
|
|
for _, sportID := range s.sportIDs {
|
|||
|
|
err := s.listUpcomingMatchReports(sportID)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("listUpcomingMatchReports(sportID=%d): %s\n",
|
|||
|
|
sportID, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.onexbetClient.ConnectAsync()
|
|||
|
|
|
|||
|
|
for {
|
|||
|
|
select {
|
|||
|
|
case <-ticker.C:
|
|||
|
|
for _, sportID := range s.sportIDs {
|
|||
|
|
err := s.listUpcomingMatchReports(sportID)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("listUpcomingMatchReports(sportID=%d): %s\n",
|
|||
|
|
sportID, err)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) removeOldMatches() {
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
a2HoursAgo := time.Now().Unix() - 7200
|
|||
|
|
|
|||
|
|
for sportID, sportMatches := range s.teamMatches {
|
|||
|
|
var count int
|
|||
|
|
for _, match := range sportMatches {
|
|||
|
|
_, isInInplay := s.onexbetMatchToTeamMatch[match.OnexbetMatchID]
|
|||
|
|
|
|||
|
|
if match.StartTime < a2HoursAgo && !isInInplay {
|
|||
|
|
// Если матч стартовал более 2 часов назад и не в Inplay - удаляем
|
|||
|
|
delete(sportMatches, match.MatchID)
|
|||
|
|
//delete(s.identifiedMatches, match.OnexbetMatchID)
|
|||
|
|
count++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Отправляем уведомления
|
|||
|
|
if count > 0 {
|
|||
|
|
s.publisher.Publish(
|
|||
|
|
getChannel(sportID),
|
|||
|
|
WSMessageTeamMatches,
|
|||
|
|
s.getTeamMatchesMessage(sportID),
|
|||
|
|
)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) listUpcomingMatchReports(sportID int) (err error) {
|
|||
|
|
var buf []byte
|
|||
|
|
err = s.fsAPIClient.Call(api.CallReq{
|
|||
|
|
FuncName: "listUpcomingMatchReports",
|
|||
|
|
In: sportID,
|
|||
|
|
Out: &buf,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var reports []flashscore.UpcomingMatchReport
|
|||
|
|
|
|||
|
|
err = json.Unmarshal(buf, &reports)
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
now := time.Now().Unix() - 10800
|
|||
|
|
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
sportStrategies := s.sportStrategies[sportID]
|
|||
|
|
|
|||
|
|
fmt.Printf("Sport strategies qty: %d\n", len(sportStrategies))
|
|||
|
|
|
|||
|
|
teamMatches := s.teamMatches[sportID]
|
|||
|
|
|
|||
|
|
// Сохраняем в этот словарь все matchID из reports, чтобы сделав проход по teamMatches,
|
|||
|
|
// обнаружить матчи, которых больше нет в reports
|
|||
|
|
//checkedMatchIDs := make(map[string]bool)
|
|||
|
|
|
|||
|
|
// Синхронизация:
|
|||
|
|
// - матчи из teamMatches, которые по времени начались - пропускаем
|
|||
|
|
// - матчи из reports, которые по времени начались - пропускаем
|
|||
|
|
// - матчи, которых нет в teamMatches - добавляем
|
|||
|
|
// - матчи, которые есть в teamMatches (и еще не начались), но нет в reports - удаляем
|
|||
|
|
// - матчи, которые есть и там и там, но не начались - обнуляем список стратегий
|
|||
|
|
// и прогоняем с нуля. Если ни одной стратегии не удовлетворяем - удаляем
|
|||
|
|
|
|||
|
|
// Первый проход по reports
|
|||
|
|
for _, report := range reports {
|
|||
|
|
//checkedMatchIDs[report.MatchID] = true
|
|||
|
|
|
|||
|
|
if report.StartTime <= now {
|
|||
|
|
// матч уже начался - пропускаем
|
|||
|
|
s.logger.Printf("match already started: %d < now %d\n", report.StartTime, now)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
match, ok := teamMatches[report.MatchID]
|
|||
|
|
if ok {
|
|||
|
|
if match.StartTime <= now {
|
|||
|
|
// матч уже начался - пропускаем
|
|||
|
|
s.logger.Printf("match already started: %d < now %d\n", match.StartTime, now)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if match.IsLinkedToOnexbet {
|
|||
|
|
// матч уже в линии 1xBet и распознан - пропускаем
|
|||
|
|
s.logger.Printf("match %s already linked\n", match.MatchID)
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
// Обнуляем стратегии чтобы пересканировать заново
|
|||
|
|
match.Strategies = make(map[int64]TeamStrategy)
|
|||
|
|
|
|||
|
|
match.ParseTime = report.ParseTime
|
|||
|
|
|
|||
|
|
match.HomeTeamMatches = report.HomeTeamMatches
|
|||
|
|
match.AwayTeamMatches = report.AwayTeamMatches
|
|||
|
|
match.H2H = report.H2H
|
|||
|
|
match.Odds = report.Odds
|
|||
|
|
|
|||
|
|
match.Notes = nil
|
|||
|
|
|
|||
|
|
} else {
|
|||
|
|
// Чтобы звездочками отметить матчи, которые не были ни разу распознаны
|
|||
|
|
home, err := s.fsmodel.GetTeam(report.Home.TeamID)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("fsmodel.GetTeam: %s; teamID=%s", err, report.Home.TeamID)
|
|||
|
|
} else {
|
|||
|
|
report.Home.WasLinked = home.WasLinked
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
away, err := s.fsmodel.GetTeam(report.Away.TeamID)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("fsmodel.GetTeam: %s; teamID=%s", err, report.Away.TeamID)
|
|||
|
|
} else {
|
|||
|
|
report.Away.WasLinked = away.WasLinked
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Добавляем новый матч
|
|||
|
|
match = &TeamMatch{
|
|||
|
|
MatchID: report.MatchID,
|
|||
|
|
Home: report.Home,
|
|||
|
|
Away: report.Away,
|
|||
|
|
Zone: report.Zone,
|
|||
|
|
Champ: report.Champ,
|
|||
|
|
FullChampName: report.FullChampName,
|
|||
|
|
StartTime: report.StartTime,
|
|||
|
|
ParseTime: report.ParseTime,
|
|||
|
|
SportID: sportID,
|
|||
|
|
Strategies: make(map[int64]TeamStrategy),
|
|||
|
|
HomeTeamMatches: report.HomeTeamMatches,
|
|||
|
|
AwayTeamMatches: report.AwayTeamMatches,
|
|||
|
|
H2H: report.H2H,
|
|||
|
|
Odds: report.Odds,
|
|||
|
|
}
|
|||
|
|
teamMatches[report.MatchID] = match
|
|||
|
|
|
|||
|
|
s.logger.Printf("match %s added\n", match.MatchID)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
historyProcessor := s.historyProcessors[sportID]
|
|||
|
|
if historyProcessor != nil {
|
|||
|
|
historyProcessor(match)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for _, strategy := range sportStrategies {
|
|||
|
|
// Сперва проверяем не было ли уже прогнозов на этот матч и эту стратегию
|
|||
|
|
var hasTip bool
|
|||
|
|
|
|||
|
|
hasTip, err = s.model.HasTip(match.MatchID, strategy.ID())
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("model.HasTip: %s\n", err)
|
|||
|
|
} else {
|
|||
|
|
if !hasTip {
|
|||
|
|
notes, isInteresting := strategy.IsInteresting(match)
|
|||
|
|
if isInteresting {
|
|||
|
|
match.Strategies[strategy.ID()] = strategy
|
|||
|
|
|
|||
|
|
match.Notes = append(match.Notes, StrategyNotes{
|
|||
|
|
StrategyID: strategy.ID(),
|
|||
|
|
Notes: notes,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if len(match.Strategies) == 0 {
|
|||
|
|
// матч никому не интересен - удаляем
|
|||
|
|
s.logger.Printf("match %s not interesting -> remove\n", match.MatchID)
|
|||
|
|
delete(teamMatches, report.MatchID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/*
|
|||
|
|
// Второй проход по teamMatches
|
|||
|
|
for matchID, match := range teamMatches {
|
|||
|
|
if checkedMatchIDs[matchID] {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
// Матча не было в reports - какндидат на удаление
|
|||
|
|
if match.StartTime <= now {
|
|||
|
|
// матч уже начался - пропускаем
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if match.IsLinkedToOnexbet {
|
|||
|
|
// матч уже в линии 1xBet и распознан - пропускаем
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
delete(teamMatches, matchID)
|
|||
|
|
}
|
|||
|
|
*/
|
|||
|
|
|
|||
|
|
s.publisher.Publish(getChannel(sportID),
|
|||
|
|
WSMessageTeamMatches, s.getTeamMatchesMessage(sportID))
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) getTeamMatchesMessage(sportID int) (msg TeamMatchesMessage) {
|
|||
|
|
teamsMatches, ok := s.teamMatches[sportID]
|
|||
|
|
if !ok {
|
|||
|
|
panic(fmt.Sprintf("Bug: getTeamMatchesMessage of unknown sportID %d\n", sportID))
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
msg.SportID = sportID
|
|||
|
|
|
|||
|
|
for _, match := range teamsMatches {
|
|||
|
|
msg.Matches = append(msg.Matches, *match)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) tryLoadAndSetTipResult(tip model.Tip) (err error) {
|
|||
|
|
if tip.OnexbetStatsMatchID == "" {
|
|||
|
|
err = fmt.Errorf("Empty OnexbetStatsMatchID")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var matchResult onexbet.TeamMatchResult
|
|||
|
|
|
|||
|
|
if tip.SportID == flashscore.Tennis {
|
|||
|
|
matchResult, err = onexbet.GetTennisMatchResult(tip.OnexbetStatsMatchID)
|
|||
|
|
if err != nil {
|
|||
|
|
err = fmt.Errorf("onexbet.GetTeamMatchResult: %s", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
matchResult, err = onexbet.GetTeamMatchResult(tip.OnexbetStatsMatchID)
|
|||
|
|
if err != nil {
|
|||
|
|
err = fmt.Errorf("onexbet.GetTeamMatchResult: %s", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.logger.Printf("%s result: %s\n", tip.OnexbetStatsMatchID, pretty.Sprint(matchResult))
|
|||
|
|
|
|||
|
|
return s.trySetTipResult(tip, matchResult)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func swapTeamMatchResult(r onexbet.TeamMatchResult) onexbet.TeamMatchResult {
|
|||
|
|
r.AwayPoints, r.HomePoints = r.HomePoints, r.AwayPoints
|
|||
|
|
|
|||
|
|
for idx, p := range r.ScoreByPeriods {
|
|||
|
|
p.AwayPoints, p.HomePoints = p.HomePoints, p.AwayPoints
|
|||
|
|
|
|||
|
|
r.ScoreByPeriods[idx] = p
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return r
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) trySetTipResult(tip model.Tip, matchResult onexbet.TeamMatchResult) (err error) {
|
|||
|
|
sportStrategies, ok := s.sportStrategies[int(tip.SportID)]
|
|||
|
|
if !ok {
|
|||
|
|
err = fmt.Errorf("No strategies found for the sportID %d", tip.SportID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
strategy, ok := sportStrategies[tip.StrategyID]
|
|||
|
|
if !ok {
|
|||
|
|
s.logger.Printf("Unknown strategyID %d", tip.StrategyID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if tip.IsTeamsSwapped {
|
|||
|
|
matchResult = swapTeamMatchResult(matchResult)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
result := strategy.GetResult(tip, matchResult)
|
|||
|
|
if result == nil {
|
|||
|
|
err = fmt.Errorf("strategy.GetResult returns nil")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
//s.logger.Printf(pretty.PSprintln)
|
|||
|
|
|
|||
|
|
err = s.model.SetTipResult(model.SetTipResultReq{
|
|||
|
|
TipID: tip.TipID,
|
|||
|
|
Status: result.Status,
|
|||
|
|
Result: result.Result,
|
|||
|
|
})
|
|||
|
|
if err != nil {
|
|||
|
|
err = fmt.Errorf("model.SetTipResult: %s", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.notifyAboutTipResult(tip, result, strategy)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) notifyAboutTipResult(tip model.Tip, result *TipResult, strategy TeamStrategy) (err error) {
|
|||
|
|
var outcome string
|
|||
|
|
|
|||
|
|
switch result.Status {
|
|||
|
|
case model.Won:
|
|||
|
|
outcome = "Выигрыш"
|
|||
|
|
|
|||
|
|
case model.Lost:
|
|||
|
|
outcome = "Проигрыш"
|
|||
|
|
|
|||
|
|
case model.Void:
|
|||
|
|
outcome = "Возврат"
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
msg := fmt.Sprintf(tipResultTextPattern, tip.TipID, result.Result, outcome)
|
|||
|
|
|
|||
|
|
// Обновляем рейтинг чемпионатов
|
|||
|
|
key := ChampRatingKey{
|
|||
|
|
ChampID: tip.ChampID,
|
|||
|
|
StrategyID: tip.StrategyID,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
champRatings, ok := s.champRatings[int(tip.SportID)]
|
|||
|
|
if ok {
|
|||
|
|
rating, ok := champRatings[key]
|
|||
|
|
if !ok {
|
|||
|
|
rating = &ChampRating{}
|
|||
|
|
champRatings[key] = rating
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if result.Status == model.Won {
|
|||
|
|
rating.Won++
|
|||
|
|
} else if result.Status == model.Lost {
|
|||
|
|
rating.Lost++
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if strategy.IsNotifyInTelegram() {
|
|||
|
|
// дополнительная проверка на isRated. У тенниса нет рейтингов по чемпионатам,
|
|||
|
|
// поэтому проверка более хитрая.
|
|||
|
|
var ok bool
|
|||
|
|
|
|||
|
|
switch tip.SportID {
|
|||
|
|
case flashscore.Tennis:
|
|||
|
|
ok = true
|
|||
|
|
|
|||
|
|
case flashscore.Handball, flashscore.Football:
|
|||
|
|
if tip.IsRated {
|
|||
|
|
ok = true
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if ok {
|
|||
|
|
telegramChatID, hasChat := sportToTelegramChatID[int(tip.SportID)]
|
|||
|
|
if hasChat {
|
|||
|
|
err = sendMessageToTelegramGroup(telegramChatID, msg)
|
|||
|
|
if err != nil {
|
|||
|
|
s.logger.Printf("send tip result to Telegram: %s\n", err)
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
s.logger.Printf("Sport %d hasn't telegram chat\n", tip.SportID)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.publisher.Publish(getChannel(int(tip.SportID)), WSMessageTipsChanged, nil)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) SetTipResultManual(in model.SetTipResultReq) (err error) {
|
|||
|
|
tip, err := s.model.GetTip(in.TipID)
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if tip == nil {
|
|||
|
|
s.logger.Printf("setTipResultManual: tipID=%d not found in db\n", in.TipID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
switch in.Status {
|
|||
|
|
case 0:
|
|||
|
|
err = fix.Field(EmptyTipStatus, "status")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
case model.Won, model.Lost, model.Void:
|
|||
|
|
// pass
|
|||
|
|
default:
|
|||
|
|
err = fix.Field(WrongTipStatus, "status")
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err = s.model.SetTipResult(in)
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Если стратегии не найдены - сообщение в телеграм отправлено не будет
|
|||
|
|
sportStrategies, ok := s.sportStrategies[int(tip.SportID)]
|
|||
|
|
if !ok {
|
|||
|
|
err = fmt.Errorf("No strategies found for the sportID %d", tip.SportID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
strategy, ok := sportStrategies[tip.StrategyID]
|
|||
|
|
if !ok {
|
|||
|
|
s.logger.Printf("Unknown strategyID %d", tip.StrategyID)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.notifyAboutTipResult(
|
|||
|
|
*tip,
|
|||
|
|
&TipResult{Status: in.Status, Result: in.Result},
|
|||
|
|
strategy,
|
|||
|
|
)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) onWsSubscribeRequestToHandball(conn *ws.Conn, channel, arg string) {
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
s.logger.Printf("ws client subscribes to channel %q\n", channel)
|
|||
|
|
|
|||
|
|
conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Handball))
|
|||
|
|
|
|||
|
|
s.publisher.SubscribeTo(conn, channel)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) onWsSubscribeRequestToFootball(conn *ws.Conn, channel, arg string) {
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
s.logger.Printf("ws client subscribes to channel %q\n", channel)
|
|||
|
|
|
|||
|
|
conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Football))
|
|||
|
|
|
|||
|
|
s.publisher.SubscribeTo(conn, channel)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) onWsSubscribeRequestToTennis(conn *ws.Conn, channel, arg string) {
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
s.logger.Printf("ws client subscribes to channel %q\n", channel)
|
|||
|
|
|
|||
|
|
conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Tennis))
|
|||
|
|
|
|||
|
|
s.publisher.SubscribeTo(conn, channel)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// func (s *Daemon) onWsSubscribeRequest(conn *ws.Conn, channel string) {
|
|||
|
|
// s.mutex.Lock()
|
|||
|
|
// defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
// s.logger.Printf("ws client subscribes to channel %q\n", channel)
|
|||
|
|
|
|||
|
|
// switch channel {
|
|||
|
|
// case ChannelHandball:
|
|||
|
|
// conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Handball))
|
|||
|
|
|
|||
|
|
// case ChannelFootball:
|
|||
|
|
// conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Football))
|
|||
|
|
|
|||
|
|
// case ChannelTennis:
|
|||
|
|
// conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(flashscore.Tennis))
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
// s.publisher.SubscribeTo(conn, channel)
|
|||
|
|
// }
|
|||
|
|
|
|||
|
|
func (s *Daemon) onWsClientConnected(conn *ws.Conn) {
|
|||
|
|
//s.mutex.Lock()
|
|||
|
|
//defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
s.logger.Printf("ws client connected\n")
|
|||
|
|
|
|||
|
|
//for _, sportID := range s.sportIDs {
|
|||
|
|
// conn.Send(WSMessageTeamMatches, s.getTeamMatchesMessage(sportID))
|
|||
|
|
//}
|
|||
|
|
|
|||
|
|
//s.logger.Printf("team matches sent\n")
|
|||
|
|
|
|||
|
|
//s.publisher.SubscribeTo(conn, Channel)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) onWsClientDisconnected(conn *ws.Conn) {
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
s.logger.Printf("ws client disconnected\n")
|
|||
|
|
|
|||
|
|
//s.publisher.UnsubscribeConn(conn)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Регулярка для вырезания буквы W из названия команды.
|
|||
|
|
// Поиск на 1xBet не дает результатов если в названии есть W, например Ajax W
|
|||
|
|
var reW = regexp.MustCompile(`\bW\b`)
|
|||
|
|
|
|||
|
|
type SearchMatchCandidatesIn struct {
|
|||
|
|
SportID int `json:"sportID"`
|
|||
|
|
Home string `json:"home"`
|
|||
|
|
Away string `json:"away"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) SearchMatchCandidates(in SearchMatchCandidatesIn) (matches []onexbet.MatchCandidate, err error) {
|
|||
|
|
in.Home = reW.ReplaceAllString(in.Home, "")
|
|||
|
|
in.Away = reW.ReplaceAllString(in.Away, "")
|
|||
|
|
|
|||
|
|
switch in.SportID {
|
|||
|
|
case flashscore.Football:
|
|||
|
|
return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Football)
|
|||
|
|
|
|||
|
|
case flashscore.Handball:
|
|||
|
|
return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Handball)
|
|||
|
|
|
|||
|
|
case flashscore.Tennis:
|
|||
|
|
return onexbet.SearchMatchCandidates(in.Home, in.Away, onexbet.Tennis)
|
|||
|
|
}
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type SearchMatchCandidatesByTeamNameIn struct {
|
|||
|
|
SportID int `json:"sportID"`
|
|||
|
|
TeamName string `json:"teamName"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) SearchMatchCandidatesByTeamName(in SearchMatchCandidatesByTeamNameIn) (matches []onexbet.MatchCandidate, err error) {
|
|||
|
|
in.TeamName = reW.ReplaceAllString(in.TeamName, "")
|
|||
|
|
|
|||
|
|
return onexbet.SearchTeamCandidates(in.TeamName, flashscoreSportToOnexbetSport[in.SportID])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type AddTeamsAliasesIn struct {
|
|||
|
|
SportID int
|
|||
|
|
Home fsmodel.TeamAlias
|
|||
|
|
Away fsmodel.TeamAlias
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) AddTeamsAliases(in AddTeamsAliasesIn) (err error) {
|
|||
|
|
err = s.fsmodel.AddTeamAlias(in.Home)
|
|||
|
|
if err != nil {
|
|||
|
|
apierr, ok := err.(fixme.Field)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if apierr.Code != fsmodel.Duplicate {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
err = nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
err = s.fsmodel.AddTeamAlias(in.Away)
|
|||
|
|
if err != nil {
|
|||
|
|
apierr, ok := err.(fixme.Field)
|
|||
|
|
if !ok {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
if apierr.Code != fsmodel.Duplicate {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
err = nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
s.mutex.Lock()
|
|||
|
|
defer s.mutex.Unlock()
|
|||
|
|
|
|||
|
|
// Сбрасываем кэш нераспознанных 1xBet матчей
|
|||
|
|
s.unrecognizedOnexbetMatches[in.SportID] = make(map[string]bool)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (s *Daemon) ListSportTips(sportID int) (tips []model.Tip, err error) {
|
|||
|
|
tips, err = s.model.ListSportTips(sportID)
|
|||
|
|
if err != nil {
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
for idx, tip := range tips {
|
|||
|
|
strategies, foundSport := s.sportStrategies[int(tip.SportID)]
|
|||
|
|
if foundSport {
|
|||
|
|
strategy, has := strategies[tip.StrategyID]
|
|||
|
|
if has {
|
|||
|
|
tip.Strategy = strategy.ShortName()
|
|||
|
|
|
|||
|
|
tips[idx] = tip
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return
|
|||
|
|
}
|